Skill: siyou315/video-to-doc (version 1.0.0)

--- name: video-to-doc description: 将操作视频自动转换为图文并茂的Word操作指南文档，支持智能截图、语音转录、LLM内容提炼和流程图生成 version: 1.0.0 --- # Video to Doc 技能 将视频教程转换为图文并茂的Word操作指南文档。 ## 功能描述 本技能能够： 1. 自动分析视频文件，提取关键帧截图 2. **使用扣子平台内置 read_image 工具识别截图中的界面元素** 3. 生成结构化的图文操作指南Word文档 ## 适用场景 - 软件操作教程视频 → 操作指南文档 - 系统演示视频 → 用户手册 - 培训视频 → 培训材料 - 产品演示视频 → 产品说明文档 ## 使用前提 ### 必需依赖 ```bash # 安装 ffmpeg（视频处理） # Ubuntu/Debian sudo apt install ffmpeg # macOS brew install ffmpeg # 安装 Python 依赖 pip install python-docx pillow faster-whisper ``` ### 可选配置 语音转录支持多种方案： - **方案A（推荐）**：使用扣子平台官方「语音转文本」插件（免费配额每日20次） - **方案B**：本地 faster-whisper 模型（首次运行自动下载，约75MB） - **方案C**：OpenAI Whisper API（需要API Key） LLM内容提炼使用扣子平台内置大模型能力，无需额外配置。 ## 工作流程 ### 新版流程（使用 read_image） 本版本采用**主对话分析 + 脚本合并**的架构，充分利用扣子平台内置的 read_image 视觉理解能力： ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Python脚本 │ │ 主对话 │ │ Python脚本 │ │ 提取截图/语音 │ → │ read_image分析 │ → │ 合并生成文档 │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` #### 步骤1：提取截图和语音（Python 脚本） ```bash python video_to_doc.py video.mp4 ``` 脚本自动完成： 1. 智能截图（根据视频时长自适应） 2. 音频提取 3. 语音转录 完成后输出： ``` [3/6] 等待截图分析... 截图目录: video_frames 分析结果保存到: video_frames_analysis.json ``` 并打印标记： ``` [MARKER_FOR_MAIN_AGENT] WAIT_FOR_FRAME_ANALYSIS frames_dir=video_frames analysis_output=video_frames_analysis.json frame_count=15 [/MARKER_FOR_MAIN_AGENT] ``` #### 步骤2：截图分析（主对话用 read_image） 主对话收到 `WAIT_FOR_FRAME_ANALYSIS` 标记后： 1. **读取截图列表**：`glob.glob("video_frames/*.jpg")` 2. **逐个分析**：使用 `read_image` 工具分析每张截图 3. **汇总结果**：按格式生成 JSON **read_image 分析提示词模板**： ``` 分析截图中的： 1. UI元素：按钮、表单、菜单等（text + type） 2. 文本内容：界面上显示的主要文字 3. 操作提示：根据画面推断下一步操作 格式要求： - ui_elements: [{"text": "文字", "type": "按钮/输入框/菜单项/..."}] - text_content: 界面主要内容摘要 - action_hint: 简短的步骤描述（如"点击确定按钮"） ``` **输出格式**（保存到 `video_frames_analysis.json`）： ```json { "frames": [ { "timestamp": 0, "ui_elements": [ {"text": "新增", "type": "按钮"}, {"text": "请输入名称", "type": "输入框"} ], "text_content": "员工管理页面，显示新增和删除按钮", "action_hint": "点击新增按钮添加员工" }, { "timestamp": 6, "ui_elements": [ {"text": "确定", "type": "按钮"}, {"text": "取消", "type": "按钮"} ], "text_content": "新增员工对话框", "action_hint": "填写信息后点击确定" } ] } ``` #### 步骤3：继续生成文档 主对话分析完成后，运行： ```bash python video_to_doc.py --continue ``` 脚本自动完成： 1. 合并截图分析和语音转录 2. 时间戳对齐 3. 生成Word文档 ### 调用方式汇总 ```bash # 完整流程 python video_to_doc.py video.mp4 # 步骤1：提取截图和语音 # [主对话用 read_image 分析截图] python video_to_doc.py --continue # 步骤3：生成文档 # 可选参数 python video_to_doc.py video.mp4 -o my.docx # 指定输出文件 python video_to_doc.py video.mp4 -m 20 # 最多20张截图 python video_to_doc.py video.mp4 --no-audio # 跳过语音转录 ``` ## 智能截图策略 ### 自动策略选择 系统根据视频特征自动选择最优截图策略： | 视频时长 | 自动策略 | 预计帧数 | |---------|---------|---------| | ≤30秒 | 每3秒一帧 | ~10张 | | 30-60秒 | 每4秒一帧 | ~15张 | | 1-3分钟 | 每6秒一帧 | ~20张 | | 3-10分钟 | 每10秒一帧 | ~30张 | | >10分钟 | 每15秒一帧 或 场景检测 | 自适应 | ### 场景变化检测（自动触发） - 当预计帧数超过30张时，自动切换到场景变化检测 - 只截取画面明显变化的帧，避免冗余 - 如果检测帧数太少，自动回退到间隔模式 ### 用户可强制指定 ```bash # 固定间隔 python smart_extract.py video.mp4 -s interval_5 # 场景检测 python smart_extract.py video.mp4 -s scene # 限制最大帧数 python smart_extract.py video.mp4 -m 20 ``` ## 截图分析说明 ### read_image 分析要点 主对话使用 `read_image` 分析截图时，关注： 1. **UI元素识别** - 按钮（确定、取消、新增、删除等） - 输入框、复选框、下拉菜单 - 导航菜单、标签页 - 图标（带文字提示的） 2. **文本内容提取** - 表格内容（关键字段） - 表单标签 - 提示信息 - 错误信息 3. **操作推断** - 根据按钮位置推断操作顺序 - 根据表单字段推断填写顺序 - 根据上下文推断下一步 ### 分析质量提升技巧 - **批量分析**：可一次性分析多张截图 - **保持一致性**：使用统一的分析格式 - **时间上下文**：考虑相邻帧的关系 ## 脚本说明 | 脚本 | 功能 | |------|------| | `video_to_doc.py` | 主入口，流程控制 | | `scripts/smart_extract.py` | 智能截图 | | `scripts/transcribe_audio.py` | 语音转录 | | `scripts/merge_and_generate.py` | 合并截图分析和语音转录 | | `scripts/generate_docx.py` | 生成Word文档 | ## LLM内容提炼 ### 提炼目标 将口语化的语音转录内容转换为规范的操作说明： - "然后我们点这个" → "点击【XX】按钮" - "先去输入" → "在「XX」输入框填写" - 去除口头禅、重复表述 - 精简步骤数量 ### 提炼流程 1. **读取分析数据**：截图分析结果 + 语音转录结果 2. **合并时间戳**：将语音内容与截图对应 3. **LLM提炼**：将口语化描述转为规范操作说明 4. **合并步骤**：将多个小步骤合并为核心步骤 ### 提炼提示词 ``` 将以下操作视频的内容提炼为规范的操作指南： 截图分析：{frames_analysis} 语音转录：{audio_transcript} 要求： 1. 提取核心操作步骤（控制在4-6步） 2. 使用规范表述： - 点击操作：【按钮名称】 - 输入字段：「字段名称」 - 示例值：直接文本 3. 去除口语化表达和重复内容 4. 每个步骤包含：标题 + 操作描述 ``` ## 流程图生成 ### 流程图格式 在文档开头添加横向箭头流程图，使用表格实现： ``` 进入系统 → 填写信息 → 拍照上传 → 查询管理 → 完成 ``` ### 实现方式 ```python # Word中用表格实现横向流程图 table = doc.add_table(rows=1, cols=9) flow_steps = ['进入系统', '→', '填写信息', '→', '拍照上传', '→', '查询管理', '→', '完成'] ``` ### 节点数量 - 核心步骤 ≤ 4个：直接展示 - 核心步骤 5-6个：合并相似步骤 - 核心步骤 > 6个：提取主要流程 ## 文档输出规范 ### 文档结构 ``` 一、操作概述 二、操作流程图 三、详细操作步骤 四、注意事项 ``` ### 操作标记规范 | 标记类型 | 格式 | 颜色 | 示例 | |----------|------|------|------| | 按钮 | 【名称】 | 蓝色加粗 | 【拍照】【上传】 | | 输入框 | 「名称」 | 绿色加粗 | 「合同号」「备注」 | | 示例值 | 直接文本 | 紫色 | DFD-BH20251226CSXT01 | ### 内容精简原则 - 每个步骤控制在2-3句话 - 只保留核心操作，去除冗余描述 - 截图选择：每个步骤配1张关键截图 - 注意事项：控制在3-5条 ## 技术架构 ``` video_to_doc.py ├── 提取截图 → smart_extract.py ├── 语音转录 → transcribe_audio.py ├── [主对话] → read_image 分析截图 ├── [主对话] → LLM 提炼内容 └── 合并生成 → merge_and_generate.py → generate_docx.py ``` **关键设计**： - Python 脚本负责：文件处理、音频处理、数据合并、文档生成 - 主对话负责：视觉理解、截图分析、内容提炼、操作推断 - 分工明确，优势互补 ## 常见问题 ### Q: 为什么截图分析要在主对话做？ A: read_image 是扣子平台的内置视觉理解工具，可以直接理解截图内容，无需额外调用 API 或模型。 ### Q: 可以跳过语音转录吗？ A: 可以，使用 `--no-audio` 参数。但建议保留语音，可以让操作说明更准确。 ### Q: 分析结果文件是什么格式？ A: JSON 格式，包含 `frames` 数组，每个帧包含 `timestamp`、`ui_elements`、`text_content`、`action_hint`。 ### Q: 如何调整截图数量？ A: 使用 `-m` 参数，如 `-m 20` 表示最多20张截图。

path: README.md
content: # Video to Doc - 视频转操作指南

将操作视频自动转换为图文并茂的Word操作指南文档。

## 功能特点

- 🎬 **智能截图提取** - 根据视频时长自适应调整截图间隔
- 🎤 **语音转录** - 支持 faster-whisper 本地模型或在线API
- 🤖 **LLM内容提炼** - 将口语化描述转为规范操作说明
- 📊 **流程图生成** - 自动生成横向箭头流程图
- 📝 **图文并茂** - 每个步骤配关键截图

## 适用场景

| 输入 | 输出 |
|------|------|
| 软件操作教程视频 | 操作指南文档 |
| 系统演示视频 | 用户手册 |
| 培训视频 | 培训材料 |
| 产品演示视频 | 产品说明文档 |

## 使用方式

### 1. 准备视频文件

支持常见格式：mp4, wmv, avi, mov 等

### 2. 运行技能

```bash
python video_to_doc.py your_video.mp4
```

### 3. 获取文档

生成的Word文档包含：
- 操作概述
- 流程图概览
- 详细操作步骤（含截图）
- 注意事项

## 依赖要求

### 一键安装

```bash
cd skill-video-to-doc
bash scripts/install_deps.sh
```

### 手动安装

- **ffmpeg** - 视频处理（必需）
- **python-docx** - 文档生成（必需）
- **pillow** - 图像处理（必需）
- **faster-whisper** - 语音转录（可选，首次运行自动下载模型）

## 输出示例

文档结构：
```
一、操作概述
二、操作流程图
三、详细操作步骤
四、注意事项
```

## 许可证

MIT License


path: generate_doc.js
content: const { Document, Packer, Paragraph, TextRun, ImageRun, AlignmentType, HeadingLevel, BorderStyle } = require('docx');
const fs = require('fs');

// 配置
const CONFIG = {
  title: "备货拍照操作指南",
  framesDir: "./备货拍照_1775826078104_0_xsi8_frames",
  outputPath: "../备货拍照/备货拍照_操作指南.docx",
  imageWidth: 450,
  imageHeight: 280
};

// 转录文本分析 - 提取关键操作步骤
const transcript = JSON.parse(fs.readFileSync('./备货拍照_1775826078104_0_xsi8_audio_transcript.json', 'utf8'));

// 将文本片段按时间分组为操作步骤
const steps = [];
let currentStep = null;
let stepIndex = 0;

// 根据转录内容识别操作步骤
const rawSegments = transcript.segments.map(s => ({ time: s.start, text: s.text }));
const fullText = rawSegments.map(s => s.text).join('');

// 识别关键操作节点
const keyOperations = [
  { time: 0, title: "步骤1：进入功能入口", desc: "在系统右上角切换功能，找到「库存」模块中的「备货拍照」功能" },
  { time: 11, title: "步骤2：输入合同号", desc: "点击输入框，手动输入或点击「合同号读取」获取合同号" },
  { time: 22, title: "步骤3：输入统号", desc: "在统号输入框中输入统号信息" },
  { time: 32, title: "步骤4：输入备入（可选）", desc: "「备入」字段为可选项，根据需要填写" },
  { time: 36, title: "步骤5：拍照上传", desc: "点击「拍照」按钮，在手机端完成拍照，确认照片展示后点击「上传」" },
  { time: 53, title: "步骤6：查看上传结果", desc: "在PC端打开「福建文档管理」，搜索并查询已上传的备货照片" },
  { time: 83, title: "步骤7：下载或预览", desc: "在文档管理中可以「预览」图片或「下载」到本地" }
];

// 生成文档内容
const children = [];

// 标题
children.push(
  new Paragraph({
    children: [new TextRun({ text: CONFIG.title, bold: true, size: 36 })],
    alignment: AlignmentType.CENTER,
    spacing: { after: 400 }
  })
);

// 操作概述
children.push(
  new Paragraph({
    children: [new TextRun({ text: "操作概述", bold: true, size: 28 })],
    spacing: { before: 200, after: 200 }
  })
);

children.push(
  new Paragraph({
    children: [new TextRun({ 
      text: "本文档介绍WMS系统中「备货拍照」功能的完整操作流程，包括进入功能、输入信息、拍照上传以及查看下载等步骤。",
      size: 22 
    })],
    spacing: { after: 300 }
  })
);

// 插入截图和对应说明
const frameFiles = fs.readdirSync(CONFIG.framesDir)
  .filter(f => f.endsWith('.jpg'))
  .sort();

console.log(`找到 ${frameFiles.length} 张截图`);

// 计算每个截图对应的时间范围
const videoDuration = 94;
const interval = videoDuration / (frameFiles.length + 1);

// 遍历关键操作步骤匹配截图
for (let i = 0; i < keyOperations.length && i < frameFiles.length; i++) {
  const frameIdx = Math.floor(keyOperations[i].time / interval);
  if (frameIdx >= frameFiles.length) continue;
  
  const frameFile = frameFiles[frameIdx];
  const framePath = `${CONFIG.framesDir}/${frameFile}`;
  
  if (!fs.existsSync(framePath)) {
    console.log(`截图不存在: ${framePath}`);
    continue;
  }
  
  const imageBuffer = fs.readFileSync(framePath);
  const imageBase64 = imageBuffer.toString('base64');
  
  // 步骤标题
  children.push(
    new Paragraph({
      children: [new TextRun({ text: keyOperations[i].title, bold: true, size: 26, color: "1976D2" })],
      spacing: { before: 300, after: 150 }
    })
  );
  
  // 步骤说明
  children.push(
    new Paragraph({
      children: [new TextRun({ text: keyOperations[i].desc, size: 22 })],
      spacing: { after: 200 }
    })
  );
  
  // 插入截图
  children.push(
    new Paragraph({
      children: [
        new ImageRun({
          data: imageBuffer,
          transformation: {
            width: CONFIG.imageWidth,
            height: CONFIG.imageHeight
          },
          type: "jpg"
        })
      ],
      alignment: AlignmentType.CENTER,
      spacing: { after: 300 }
    })
  );
  
  console.log(`已处理: ${keyOperations[i].title}`);
}

// 注意事项
children.push(
  new Paragraph({
    children: [new TextRun({ text: "注意事项", bold: true, size: 28 })],
    spacing: { before: 400, after: 200 }
  })
);

const notes = [
  "合同号和统号为必填项，备入字段可选",
  "拍照时请确保光线充足，照片清晰可辨",
  "上传成功后可在「福建文档管理」中查看和下载",
  "建议使用PC端Chrome或Firefox浏览器获得最佳体验"
];

notes.forEach(note => {
  children.push(
    new Paragraph({
      children: [new TextRun({ text: `• ${note}`, size: 22 })],
      spacing: { after: 100 }
    })
  );
});

// 创建文档
const doc = new Document({
  sections: [{
    properties: {},
    children: children
  }]
});

// 保存文档
Packer.toBuffer(doc).then(buffer => {
  const outputPath = CONFIG.outputPath;
  fs.writeFileSync(outputPath, buffer);
  console.log(`\n文档已生成: ${outputPath}`);
}).catch(err => {
  console.error('生成文档失败:', err);
});


path: package.json
content: {
  "dependencies": {
    "docx": "^9.6.1"
  }
}


path: references/guide.md
content: # 视频转文档操作指南

本文档详细介绍如何使用 video-to-doc 技能将视频转换为图文并茂的操作指南文档。

## 完整工作流程

### 1. 准备视频文件

确保视频文件格式支持：
- 常见格式：mp4, wmv, avi, mov, mkv
- 建议时长：1-5分钟（过长视频会产生过多截图）
- 建议分辨率：1080p或以上（确保截图清晰）

### 2. 智能提取关键帧（推荐）

使用 `scripts/smart_extract.py` 脚本，自动选择最优策略：

```bash
# 自动模式（推荐）
python scripts/smart_extract.py video.mp4

# 指定输出目录
python scripts/smart_extract.py video.mp4 -o ./frames

# 限制最大帧数
python scripts/smart_extract.py video.mp4 -m 20

# 强制使用场景检测
python scripts/smart_extract.py video.mp4 -s scene

# 强制每5秒一帧
python scripts/smart_extract.py video.mp4 -s interval_5
```

**智能策略说明：**

| 视频时长 | 自动策略 | 原因 |
|---------|---------|------|
| ≤30秒 | 每3秒一帧 | 短视频需要密集采样 |
| 30-60秒 | 每4秒一帧 | 中等密度 |
| 1-3分钟 | 每6秒一帧 | 标准密度 |
| 3-10分钟 | 每10秒一帧 | 稀疏采样 |
| >10分钟 | 每15秒一帧 或 场景检测 | 避免帧数过多 |

**场景变化检测：**
- 当预计帧数 > 30张时自动启用
- 只截取画面变化明显的帧
- 检测阈值默认 0.3（可调整）
- 如果帧数 < 5，自动回退间隔模式

### 2.5 提取并转录音频（重要）

**为什么需要音频转录？**
- 很多操作视频有语音讲解
- 画面可能遗漏关键操作细节
- 语音解说能补充重要注意事项

**步骤1：提取音频**
```bash
# 提取音频（16kHz单声道，适合语音识别）
ffmpeg -i video.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 audio.wav -y
```

**步骤2：选择ASR服务并转录**

使用 `scripts/transcribe_audio.py` 脚本：

```bash
# 方案A：本地 Whisper（免费，但需要等待）
pip install openai-whisper
python transcribe_audio.py audio.wav local

# 方案B：OpenAI Whisper API（快速，需要API Key）
pip install openai
python transcribe_audio.py audio.wav openai --api-key YOUR_KEY

# 方案C：使用更大的本地模型（更准确）
python transcribe_audio.py audio.wav local --model small
# 可选模型: tiny, base, small, medium, large
```

**转录结果格式**：
```json
{
  "segments": [
    {"start": 0.0, "end": 5.2, "text": "现在我们进入预期到货通知模块"},
    {"start": 6.0, "end": 12.5, "text": "点击新增按钮创建一个新的通知"},
    {"start": 13.0, "end": 20.0, "text": "类型选择生产退料"}
  ]
}
```

**步骤3：音频与截图对齐**
- 每张截图有时间戳（如 frame_01.jpg 对应 0-6秒）
- 根据时间戳匹配对应的转录文字
- 在文档中结合画面信息和语音说明

### 3. 分析截图内容与整合音频

使用 `read_image` 工具分析截图：

```
read_image(["./frames/frame_01.jpg", "./frames/frame_02.jpg"])
```

**分析要点**：
- 识别界面类型（表单、列表、对话框等）
- 提取关键文字信息
- 理解操作逻辑和流程
- 记录重要字段和参数

**同时读取音频转录结果**：
```bash
# 读取转录文件
cat audio_transcript.json
```

**整合画面与音频信息**：
| 截图 | 时间段 | 画面信息 | 语音说明 |
|-----|-------|---------|---------|
| frame_01.jpg | 0-6秒 | 进入预期到货通知模块 | "现在我们进入预期到货通知模块" |
| frame_02.jpg | 6-12秒 | 点击新增按钮 | "点击新增按钮创建新通知" |
| frame_03.jpg | 12-18秒 | 填写表单 | "类型选择生产退料，注意..." |

**注意**：`read_image` 有调用频率限制，建议分批处理：
- 每次处理 3-4 张截图
- 遇到频率限制时等待几秒再继续

### 4. 编写文档内容

使用 `scripts/create_doc_template.js` 作为模板：

```javascript
// 修改配置项
const CONFIG = {
  title: "XXX操作指南",
  framesDir: "./frames",
  frameFiles: ['frame_01', 'frame_02', ...],
  outputName: "操作指南.docx"
};

// 定义步骤
const STEPS = [
  {
    title: "步骤1：xxx",
    description: "操作说明文字",
    imageFile: "frame_01",
    imageCaption: "图1：xxx界面",
    bullets: [
      { label: "要点1：", content: "详细说明" }
    ]
  }
];
```

### 5. 生成Word文档

```bash
# 安装依赖（首次使用）
npm install docx

# 运行脚本
node create_doc_template.js
```

## 文档结构规范

### 推荐结构

```
一、操作概述
   - 简要说明操作目的和适用场景
   - 1-2段话即可

二、系统信息（如适用）
   - 表格形式展示系统名称、模块、版本等

三、操作步骤详解
   步骤1：xxx
   - 操作说明
   - 截图（居中）
   - 图片说明（斜体灰色）
   - 要点列表（可选）

   步骤2：xxx
   ...

四、注意事项
   - 编号列表形式
   - 每条简洁明了

五、关键字段说明（如适用）
   - 表格形式
   - 列：字段名称、说明、示例值
```

### 样式规范

| 元素 | 字体 | 大小 | 颜色 |
|-----|------|-----|------|
| 标题 | 微软雅黑 | 48pt(24号) | #1F4E79 |
| 一级标题 | 微软雅黑 | 32pt(16号) | #2E75B6 |
| 二级标题 | 微软雅黑 | 28pt(14号) | #5B9BD5 |
| 正文 | 微软雅黑 | 24pt(12号) | #000000 |
| 图片说明 | 微软雅黑 | 20pt(10号) | #666666(斜体) |

## 常见问题

### Q: 截图太多怎么办？
A: 筛选关键步骤的截图，一个文档控制在6-10张图片为宜。

### Q: 视频太长怎么办？
A: 可以增加帧间隔，比如设为10秒或15秒一帧。

### Q: 截图不清晰怎么办？
A: 提高视频分辨率，或在 ffmpeg 中使用 `-q:v 1` 参数（最高质量）。

### Q: 如何处理多语言？
A: 在文档样式中修改默认字体，如英文文档使用 Arial。

## 最佳实践

1. **视频准备**
   - 确保操作演示完整、流畅
   - 避免无关操作或中断
   - 重要界面适当停留

2. **截图筛选**
   - 选择操作转折点的截图
   - 避免重复或相似的截图
   - 确保每张截图都有说明价值

3. **文档编写**
   - 步骤标题用动词开头
   - 描述简洁、准确
   - 要点列表补充细节
   - 图片说明概括内容

4. **质量检查**
   - 检查图片是否正确显示
   - 检查文字是否有错别字
   - 检查格式是否一致


path: scripts/align_and_generate.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
对齐截图和语音，生成文档内容（优化版）

使用提炼后的语音内容生成规范的操作步骤

用法:
    python align_and_generate.py <提炼后的JSON> <截图目录> <输出文件>
"""

import os
import sys
import json
from pathlib import Path
from typing import List, Dict, Any, Optional


def load_json(file_path: str) -> dict:
    """加载 JSON 文件"""
    with open(file_path, "r", encoding="utf-8") as f:
        return json.load(f)


def get_frames(frames_dir: str) -> list:
    """
    获取截图列表
    
    Returns:
        [(文件名, 时间戳), ...]
    """
    frames = []
    
    # 尝试多种排序方式
    frame_files = list(Path(frames_dir).glob("*.jpg")) + \
                  list(Path(frames_dir).glob("*.png")) + \
                  list(Path(frames_dir).glob("*.jpeg"))
    
    frame_files = sorted(frame_files)
    
    for i, f in enumerate(frame_files):
        # 时间戳按序号估算（每6秒一帧）
        timestamp = i * 6
        frames.append((f.name, timestamp))
    
    return frames


def match_frame_to_segment(timestamp: float, frames: list, tolerance: int = 5) -> str:
    """
    根据时间戳匹配截图
    
    Args:
        timestamp: 语音片段开始时间
        frames: 截图列表 [(文件名, 时间戳), ...]
        tolerance: 容差（秒）
    
    Returns:
        匹配的图片文件名
    """
    for frame_name, frame_time in frames:
        if abs(frame_time - timestamp) <= tolerance:
            return frame_name
    
    # 找最近的
    if frames:
        closest = min(frames, key=lambda x: abs(x[1] - timestamp))
        return closest[0]
    
    return ""


def generate_steps_from_refined(refined_file: str, frames_dir: str, output_file: str) -> Dict:
    """
    从提炼后的语音生成操作步骤
    
    Args:
        refined_file: 提炼后的 JSON 文件
        frames_dir: 截图目录
        output_file: 输出文件
    """
    refined = load_json(refined_file)
    segments = refined.get("segments", [])
    frames = get_frames(frames_dir)
    
    steps = []
    for i, seg in enumerate(segments):
        # 匹配截图
        frame_name = match_frame_to_segment(seg["start"], frames)
        
        step = {
            "step_num": i + 1,
            "title": seg.get("title", f"步骤{i+1}"),
            "image_path": frame_name,
            "timestamp": seg.get("start", 0),
            "description": seg.get("description", ""),
            "voice_original": seg.get("voice_original", "")
        }
        steps.append(step)
    
    # 生成文档结构
    result = {
        "title": "操作指南",
        "overview": f"本文档共 {len(steps)} 个操作步骤，请按顺序执行。",
        "steps": steps,
        "notes": [
            "请按步骤顺序操作",
            "如有疑问请参考原视频教程",
            "界面元素名称以文档中标注的「」符号为准"
        ]
    }
    
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)
    
    print(f"生成完成: {output_file}")
    print(f"共 {len(steps)} 个步骤")
    return result


def generate_steps_from_frames_and_transcript(
    frames_analysis_file: str,
    transcript_file: str,
    output_file: str
) -> Dict:
    """
    从截图分析和语音转录生成步骤（旧版兼容）
    
    Args:
        frames_analysis_file: 截图分析文件
        transcript_file: 语音转录文件
        output_file: 输出文件
    """
    frames_data = load_json(frames_analysis_file)
    transcript_data = load_json(transcript_file)
    
    frames = frames_data.get("frames", [])
    segments = transcript_data.get("segments", [])
    
    steps = []
    
    # 匹配逻辑：按时间顺序交错合并
    frame_idx = 0
    for seg in segments:
        seg_start = seg.get("start", 0)
        
        # 找到最接近的截图
        while frame_idx < len(frames):
            frame = frames[frame_idx]
            frame_time = frame.get("timestamp", 0)
            
            if frame_time >= seg_start - 3:  # 截图时间在语音前3秒内
                break
            frame_idx += 1
        
        if frame_idx >= len(frames):
            break
        
        frame = frames[frame_idx]
        
        step = {
            "step_num": len(steps) + 1,
            "title": frame.get("action_hint", f"步骤{len(steps)+1}"),
            "image_path": frame.get("image_path", ""),
            "timestamp": frame_time,
            "voice_text": seg.get("text", ""),
            "description": frame.get("action_hint", ""),
            "ui_elements": frame.get("ui_elements", [])
        }
        steps.append(step)
        frame_idx += 1
    
    result = {
        "title": "操作指南",
        "overview": f"本文档共 {len(steps)} 个操作步骤",
        "steps": steps,
        "notes": ["请按步骤顺序操作"]
    }
    
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)
    
    return result


def main():
    if len(sys.argv) < 3:
        print("用法:")
        print("  # 新版（推荐）：使用提炼后的JSON")
        print("  python align_and_generate.py <提炼后的JSON> <截图目录> <输出文件>")
        print("")
        print("  # 旧版兼容：使用截图分析和语音转录")
        print("  python align_and_generate.py <截图分析文件> <语音转录文件> <输出文件> [video_name]")
        sys.exit(1)
    
    arg1 = sys.argv[1]
    
    if len(sys.argv) == 4:
        # 新版用法：refined_json frames_dir output
        refined_file = arg1
        frames_dir = sys.argv[2]
        output_file = sys.argv[3]
        generate_steps_from_refined(refined_file, frames_dir, output_file)
    else:
        # 旧版用法：frames_analysis transcript output video_name
        frames_analysis_file = arg1
        transcript_file = sys.argv[2]
        output_file = sys.argv[3]
        video_name = sys.argv[4] if len(sys.argv) > 4 else "操作指南"
        generate_steps_from_frames_and_transcript(
            frames_analysis_file, transcript_file, output_file
        )


if __name__ == "__main__":
    main()


path: scripts/create_doc_template.js
content: /**
 * 视频转图文文档生成模板
 * 
 * 使用方法：
 * 1. 修改顶部的配置项
 * 2. 根据实际截图内容修改步骤描述
 * 3. 运行：node create_doc_template.js
 */

const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, 
        Header, Footer, AlignmentType, HeadingLevel, BorderStyle, WidthType, 
        ShadingType, LevelFormat, PageNumber } = require('docx');
const fs = require('fs');

// ==================== 配置项 ====================
const CONFIG = {
  // 文档标题
  title: "操作指南",
  
  // 截图目录（相对于脚本所在目录）
  framesDir: "./frames",
  
  // 截图文件名列表（不含扩展名）
  frameFiles: ['frame_01', 'frame_02', 'frame_03', 'frame_04', 'frame_05', 'frame_06'],
  
  // 输出文件名
  outputName: "操作指南.docx",
  
  // 图片显示尺寸
  imageWidth: 500,
  imageHeight: 268
};

// ==================== 内容定义 ====================

/**
 * 操作步骤定义
 * 格式：{ title, description, imageFile, imageCaption, bullets }
 */
const STEPS = [
  {
    title: "步骤1：进入系统",
    description: "登录系统后，点击导航栏进入目标模块。",
    imageFile: "frame_01",
    imageCaption: "图1：系统界面",
    bullets: [
      { label: "操作说明：", content: "点击对应菜单项" }
    ]
  },
  {
    title: "步骤2：新建记录",
    description: "点击「新增」按钮，创建新记录。",
    imageFile: "frame_02",
    imageCaption: "图2：新建界面",
    bullets: []
  }
  // ... 添加更多步骤
];

/**
 * 系统信息表格（可选）
 */
const SYSTEM_INFO = [
  { field: "系统名称", value: "XXX系统" },
  { field: "功能模块", value: "XXX模块" }
];

/**
 * 注意事项列表
 */
const NOTES = [
  "注意保存操作",
  "确认数据准确性"
];

/**
 * 关键字段说明表格（可选）
 */
const FIELDS = [
  { name: "字段1", desc: "说明", example: "示例" },
  { name: "字段2", desc: "说明", example: "示例" }
];

// ==================== 文档生成代码 ====================

// 表格边框样式
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder };

// 读取图片
function loadImages() {
  const images = {};
  CONFIG.frameFiles.forEach(f => {
    const path = `${CONFIG.framesDir}/${f}.jpg`;
    if (fs.existsSync(path)) {
      images[f] = fs.readFileSync(path);
    }
  });
  return images;
}

// 创建系统信息表格
function createInfoTable(info) {
  return new Table({
    columnWidths: [3000, 6360],
    rows: info.map(item => new TableRow({
      children: [
        new TableCell({ 
          borders: cellBorders, 
          width: { size: 3000, type: WidthType.DXA }, 
          shading: { fill: "E7F3FF", type: ShadingType.CLEAR },
          children: [new Paragraph({ children: [new TextRun({ text: item.field, bold: true })] })] 
        }),
        new TableCell({ 
          borders: cellBorders, 
          width: { size: 6360, type: WidthType.DXA },
          children: [new Paragraph({ children: [new TextRun(item.value)] })] 
        })
      ]
    }))
  });
}

// 创建步骤段落
function createStepParagraphs(step, images, bulletRef) {
  const paragraphs = [
    new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun(step.title)] }),
    new Paragraph({ children: [new TextRun(step.description)] })
  ];
  
  // 添加要点列表
  step.bullets.forEach(b => {
    paragraphs.push(new Paragraph({ 
      numbering: { reference: bulletRef, level: 0 }, 
      children: [new TextRun({ text: b.label, bold: true }), new TextRun(b.content)] 
    }));
  });
  
  // 添加图片
  if (step.imageFile && images[step.imageFile]) {
    paragraphs.push(new Paragraph({ 
      alignment: AlignmentType.CENTER, 
      spacing: { before: 200, after: 200 },
      children: [new ImageRun({ 
        type: "jpg", 
        data: images[step.imageFile], 
        transformation: { width: CONFIG.imageWidth, height: CONFIG.imageHeight },
        altText: { title: step.title, description: step.imageCaption, name: step.imageFile }
      })] 
    }));
    paragraphs.push(new Paragraph({ 
      alignment: AlignmentType.CENTER, 
      children: [new TextRun({ text: step.imageCaption, size: 20, color: "666666", italics: true })] 
    }));
  }
  
  return paragraphs;
}

// 创建字段表格
function createFieldsTable(fields) {
  return new Table({
    columnWidths: [2500, 3500, 3360],
    rows: [
      // 表头
      new TableRow({ tableHeader: true, children: ['字段名称', '说明', '示例值'].map(text => 
        new TableCell({ 
          borders: cellBorders, 
          shading: { fill: "2E75B6", type: ShadingType.CLEAR },
          children: [new Paragraph({ 
            alignment: AlignmentType.CENTER, 
            children: [new TextRun({ text, bold: true, color: "FFFFFF" })] 
          })] 
        })
      )}),
      // 数据行
      ...fields.map(f => new TableRow({
        children: [
          new TableCell({ borders: cellBorders, children: [new Paragraph({ children: [new TextRun(f.name)] })] }),
          new TableCell({ borders: cellBorders, children: [new Paragraph({ children: [new TextRun(f.desc)] })] }),
          new TableCell({ borders: cellBorders, children: [new Paragraph({ children: [new TextRun(f.example)] })] })
        ]
      }))
    ]
  });
}

// 主函数
function main() {
  const images = loadImages();
  
  // 构建文档内容
  const children = [
    // 标题
    new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun(CONFIG.title)] }),
    
    // 概述
    new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("一、操作概述")] }),
    new Paragraph({ children: [new TextRun("本文档介绍相关操作流程。")] })
  ];
  
  // 系统信息（如有）
  if (SYSTEM_INFO.length > 0) {
    children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("二、系统信息")] }));
    children.push(createInfoTable(SYSTEM_INFO));
  }
  
  // 操作步骤
  children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("三、操作步骤详解")] }));
  STEPS.forEach(step => {
    children.push(...createStepParagraphs(step, images, "bullet-list"));
  });
  
  // 注意事项
  if (NOTES.length > 0) {
    children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("四、注意事项")] }));
    NOTES.forEach((note, i) => {
      children.push(new Paragraph({ 
        numbering: { reference: "step-list", level: 0 }, 
        children: [new TextRun(note)] 
      }));
    });
  }
  
  // 字段说明（如有）
  if (FIELDS.length > 0) {
    children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("五、关键字段说明")] }));
    children.push(createFieldsTable(FIELDS));
  }
  
  // 创建文档
  const doc = new Document({
    styles: {
      default: { document: { run: { font: "微软雅黑", size: 24 } } },
      paragraphStyles: [
        { id: "Title", name: "Title", basedOn: "Normal",
          run: { size: 48, bold: true, color: "1F4E79", font: "微软雅黑" },
          paragraph: { spacing: { before: 240, after: 360 }, alignment: AlignmentType.CENTER } },
        { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
          run: { size: 32, bold: true, color: "2E75B6", font: "微软雅黑" },
          paragraph: { spacing: { before: 360, after: 180 }, outlineLevel: 0 } },
        { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
          run: { size: 28, bold: true, color: "5B9BD5", font: "微软雅黑" },
          paragraph: { spacing: { before: 240, after: 120 }, outlineLevel: 1 } }
      ]
    },
    numbering: {
      config: [
        { reference: "step-list",
          levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
            style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
        { reference: "bullet-list",
          levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
            style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }
      ]
    },
    sections: [{
      properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } },
      headers: {
        default: new Header({ children: [new Paragraph({ 
          alignment: AlignmentType.RIGHT,
          children: [new TextRun({ text: "操作指南", size: 20, color: "808080" })]
        })] })
      },
      footers: {
        default: new Footer({ children: [new Paragraph({ 
          alignment: AlignmentType.CENTER,
          children: [new TextRun({ text: "第 ", size: 20 }), new TextRun({ children: [PageNumber.CURRENT], size: 20 }), 
                     new TextRun({ text: " 页", size: 20 })]
        })] })
      },
      children
    }]
  });
  
  // 生成文档
  Packer.toBuffer(doc).then(buffer => {
    fs.writeFileSync(CONFIG.outputName, buffer);
    console.log(`文档生成成功：${CONFIG.outputName}`);
  });
}

main();


path: scripts/extract_frames.sh
content: #!/bin/bash
# 视频关键帧提取脚本
# 使用方法：bash extract_frames.sh <视频文件路径> [输出目录] [帧间隔秒数]

# 参数检查
if [ -z "$1" ]; then
    echo "使用方法: bash extract_frames.sh <视频文件> [输出目录] [间隔秒数]"
    echo "示例: bash extract_frames.sh video.mp4 ./frames 6"
    exit 1
fi

VIDEO_FILE="$1"
OUTPUT_DIR="${2:-./frames}"
INTERVAL="${3:-6}"

# 检查视频文件
if [ ! -f "$VIDEO_FILE" ]; then
    echo "错误：视频文件不存在: $VIDEO_FILE"
    exit 1
fi

# 检查 ffmpeg
if ! command -v ffmpeg &> /dev/null; then
    echo "错误：未安装 ffmpeg，请先安装"
    exit 1
fi

# 创建输出目录
mkdir -p "$OUTPUT_DIR"

# 获取视频时长
DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$VIDEO_FILE")
echo "视频时长: ${DURATION}秒"

# 计算预期帧数
EXPECTED_FRAMES=$(echo "scale=0; $DURATION / $INTERVAL" | bc)
echo "预计提取帧数: ${EXPECTED_FRAMES}张"

# 提取关键帧
echo "正在提取关键帧..."
ffmpeg -i "$VIDEO_FILE" -vf "fps=1/$INTERVAL" -q:v 2 "$OUTPUT_DIR/frame_%02d.jpg" -y 2>&1 | tail -3

# 统计结果
FRAME_COUNT=$(ls -1 "$OUTPUT_DIR"/frame_*.jpg 2>/dev/null | wc -l)
echo "完成！共提取 ${FRAME_COUNT} 张截图到 ${OUTPUT_DIR}"

# 列出文件
echo ""
echo "截图文件列表："
ls -lh "$OUTPUT_DIR"/frame_*.jpg 2>/dev/null | awk '{print $NF, $5}'


path: scripts/generate_docx.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
根据结构化内容生成 Word 文档
使用 python-docx 库生成专业的图文操作指南

用法:
    python generate_docx.py <内容文件> <截图目录> <输出文件>
"""

import os
import sys
import json
import re
from pathlib import Path
from typing import List, Dict, Any, Optional

# 尝试导入 docx
try:
    from docx import Document
    from docx.shared import Inches, Pt, RGBColor, Cm
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.enum.style import WD_STYLE_TYPE
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement
except ImportError:
    print("错误: 请先安装 python-docx")
    print("运行: pip install python-docx")
    sys.exit(1)


def set_cell_shading(cell, color: str):
    """设置单元格背景色"""
    shading_elm = OxmlElement("w:shd")
    shading_elm.set(qn("w:fill"), color)
    cell._tc.get_or_add_tcPr().append(shading_elm)


def add_hyperlink(paragraph, url: str, text: str):
    """添加超链接"""
    part = paragraph.part
    r_id = part.relate_to(
        url,
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
        is_external=True
    )
    
    hyperlink = OxmlElement("w:hyperlink")
    hyperlink.set(qn("r:id"), r_id)
    
    new_run = OxmlElement("w:r")
    rPr = OxmlElement("w:rPr")
    
    # 蓝色下划线样式
    color = OxmlElement("w:color")
    color.set(qn("w:val"), "0000FF")
    rPr.append(color)
    
    u = OxmlElement("w:u")
    u.set(qn("w:val"), "single")
    rPr.append(u)
    
    new_run.append(rPr)
    text_elem = OxmlElement("w:t")
    text_elem.text = text
    new_run.append(text_elem)
    
    hyperlink.append(new_run)
    paragraph._p.append(hyperlink)
    
    return hyperlink


def generate_word_doc(
    content_file: str,
    frames_dir: str,
    output_file: str,
    title_font: str = "微软雅黑",
    body_font: str = "微软雅黑",
    title_size: int = 22,
    heading_size: int = 16,
    body_size: int = 11
) -> bool:
    """
    生成 Word 文档
    
    Args:
        content_file: 内容JSON文件
        frames_dir: 截图目录
        output_file: 输出文件路径
        title_font: 标题字体
        body_font: 正文字体
        title_size: 标题字号
        heading_size: 标题字号
        body_size: 正文字号
    
    Returns:
        是否成功
    """
    # 加载内容
    with open(content_file, "r", encoding="utf-8") as f:
        content = json.load(f)
    
    # 创建文档
    doc = Document()
    
    # 设置默认字体
    style = doc.styles["Normal"]
    style.font.name = body_font
    style._element.rPr.rFonts.set(qn("w:eastAsia"), body_font)
    style.font.size = Pt(body_size)
    
    # ===== 标题 =====
    title = doc.add_heading("", 0)
    title_run = title.add_run(content.get("title", "操作指南"))
    title_run.font.name = title_font
    title_run._element.rPr.rFonts.set(qn("w:eastAsia"), title_font)
    title_run.font.size = Pt(title_size)
    title_run.font.bold = True
    title_run.font.color.rgb = RGBColor(31, 78, 121)  # 深蓝色
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    # 副标题
    if content.get("subtitle"):
        subtitle = doc.add_paragraph()
        sub_run = subtitle.add_run(content["subtitle"])
        sub_run.font.size = Pt(14)
        sub_run.font.color.rgb = RGBColor(89, 89, 89)
        subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    doc.add_paragraph()  # 空行
    
    # ===== 一、操作概述 =====
    doc.add_heading("一、操作概述", level=1)
    overview = doc.add_paragraph()
    overview_run = overview.add_run(content.get("overview", "本文档提供详细的操作指南。"))
    overview_run.font.size = Pt(body_size)
    
    doc.add_paragraph()  # 空行
    
    # ===== 二、操作流程图 =====
    doc.add_heading("二、操作流程图", level=1)
    
    # 提取步骤关键词生成流程图
    steps = content.get("steps", [])
    if steps:
        # 生成流程节点
        flow_nodes = []
        for step in steps:
            step_title = step.get("title", f"步骤{step.get('step_num', len(flow_nodes)+1)}")
            # 简化标题
            if "：" in step_title:
                step_title = step_title.split("：")[-1]
            # 提取关键词（取前4个字或整个标题）
            if len(step_title) > 6:
                flow_nodes.append(step_title[:6] + "...")
            else:
                flow_nodes.append(step_title)
        
        # 添加开始和结束节点
        flow_nodes = ["进入系统"] + flow_nodes + ["完成"]
        
        # 创建流程图表格
        num_nodes = len(flow_nodes)
        num_cols = num_nodes * 2 - 1  # 节点 + 箭头
        table = doc.add_table(rows=1, cols=num_cols)
        table.alignment = WD_ALIGN_PARAGRAPH.CENTER
        cells = table.rows[0].cells
        
        for i, node in enumerate(flow_nodes):
            # 节点单元格
            cell_idx = i * 2
            cell = cells[cell_idx]
            p = cell.paragraphs[0]
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            run = p.add_run(node)
            run.font.name = body_font
            run._element.rPr.rFonts.set(qn("w:eastAsia"), body_font)
            run.font.size = Pt(9)
            run.font.bold = True
            run.font.color.rgb = RGBColor(0, 102, 204)
            
            # 箭头单元格
            if i < len(flow_nodes) - 1:
                arrow_cell = cells[cell_idx + 1]
                arrow_p = arrow_cell.paragraphs[0]
                arrow_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
                arrow_run = arrow_p.add_run("→")
                arrow_run.font.size = Pt(14)
                arrow_run.font.bold = True
                arrow_run.font.color.rgb = RGBColor(100, 100, 100)
        
        doc.add_paragraph()  # 空行
    
    # ===== 三、详细操作步骤 =====
    doc.add_heading("三、详细操作步骤", level=1)
    
    steps = content.get("steps", [])
    for step in steps:
        # 步骤标题（使用提炼后的标题，不再重复步骤序号）
        step_title = step.get("title", f"步骤{step['step_num']}")
        # 确保标题不包含无意义的"步骤X：步骤X"重复
        if step_title.startswith(f"步骤{step['step_num']}："):
            step_title = step_title.split("：")[-1]
        elif step_title == f"步骤{step['step_num']}":
            step_title = f"操作{step['step_num']}"
        
        doc.add_heading(f"步骤{step['step_num']}：{step_title}", level=2)
        
        # 操作说明（提炼后的规范描述，支持标记格式）
        if step.get("description"):
            # 如果是换行分隔的多条，转为列表
            desc_lines = step["description"].strip().split("\n")
            for line in desc_lines:
                line = line.strip()
                if line:
                    # 移除序号前缀（如"1. "）
                    if re.match(r'^\d+[.、]', line):
                        line = re.sub(r'^\d+[.、]\s*', '', line)
                    p = doc.add_paragraph()
                    
                    # 解析标记格式：【按钮】蓝色加粗，「输入框」绿色加粗
                    # 简单实现：检测并应用颜色
                    run = p.add_run(line)
                    run.font.name = body_font
                    run._element.rPr.rFonts.set(qn("w:eastAsia"), body_font)
                    
                    # TODO: 可扩展为更精细的格式化
                    p.paragraph_format.left_indent = Cm(0.5)
        
        # 截图
        image_path = step.get("image_path", "")
        if image_path:
            # 拼接完整路径：frames_dir + 图片文件名
            image_filename = os.path.basename(image_path) if "/" in image_path else image_path
            full_image_path = os.path.join(frames_dir, image_filename)
            
            if os.path.exists(full_image_path):
                try:
                    # 添加图片
                    p_img = doc.add_paragraph()
                    p_img.alignment = WD_ALIGN_PARAGRAPH.CENTER
                    run = p_img.add_run()
                    run.add_picture(full_image_path, width=Inches(5.5))
                    
                    # 图片说明
                    p_caption = doc.add_paragraph()
                    caption_run = p_caption.add_run(f"图{step['step_num']}：{step_title}")
                    caption_run.font.size = Pt(9)
                    caption_run.font.italic = True
                    caption_run.font.color.rgb = RGBColor(89, 89, 89)
                    p_caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
                except Exception as e:
                    doc.add_paragraph(f"[图片加载失败: {image_filename}]")
            else:
                doc.add_paragraph(f"[图片不存在: {image_filename}]")
        else:
            doc.add_paragraph("[无截图]")
        
        doc.add_paragraph()  # 空行
    
    # ===== 四、注意事项 =====
    doc.add_heading("四、注意事项", level=1)
    notes = content.get("notes", ["请按步骤顺序操作"])
    for note in notes:
        p = doc.add_paragraph(note, style="List Bullet")
        p.paragraph_format.left_indent = Cm(0.5)
    
    # 添加页脚
    section = doc.sections[0]
    footer = section.footer
    footer_para = footer.paragraphs[0]
    footer_para.text = f"{content.get('title', '操作指南')} - 第"
    footer_para.add_run("PAGE")
    footer_para.add_run("页 / 共")
    footer_para.add_run("NUMPAGES")
    footer_para.add_run("页")
    footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    # 保存
    doc.save(output_file)
    print(f"文档已保存: {output_file}")
    return True


def main():
    if len(sys.argv) < 4:
        print("用法: python generate_docx.py <内容文件> <截图目录> <输出文件>")
        print("\n示例:")
        print("  python generate_docx.py content.json ./frames output.docx")
        print("\n流程说明:")
        print("  1. 使用 analyze_frames.py 生成截图分析")
        print("  2. 使用 transcribe_audio.py 生成语音转录")
        print("  3. 使用 align_and_generate.py 生成内容JSON")
        print("  4. 使用本脚本生成 Word 文档")
        sys.exit(1)
    
    content_file = sys.argv[1]
    frames_dir = sys.argv[2]
    output_file = sys.argv[3]
    
    # 检查文件
    if not os.path.exists(content_file):
        print(f"错误: 内容文件不存在: {content_file}")
        sys.exit(1)
    
    if not os.path.exists(frames_dir):
        print(f"警告: 截图目录不存在: {frames_dir}")
    
    # 生成文档
    try:
        success = generate_word_doc(content_file, frames_dir, output_file)
        if success:
            print(f"\n✓ Word 文档生成成功!")
            print(f"  文件: {output_file}")
    except Exception as e:
        print(f"生成失败: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/install_deps.sh
content: #!/bin/bash
# Video to Doc 技能依赖安装脚本

echo "=== Video to Doc 依赖安装 ==="

# 检测操作系统
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
    OS="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
    OS="macos"
else
    echo "不支持的操作系统: $OSTYPE"
    exit 1
fi

# 安装 ffmpeg
echo ""
echo "[1/3] 检查 ffmpeg..."
if command -v ffmpeg &> /dev/null; then
    echo "✓ ffmpeg 已安装"
else
    echo "正在安装 ffmpeg..."
    if [ "$OS" == "linux" ]; then
        sudo apt update && sudo apt install -y ffmpeg
    elif [ "$OS" == "macos" ]; then
        brew install ffmpeg
    fi
fi

# 安装 Python 依赖
echo ""
echo "[2/3] 安装 Python 依赖..."
pip install python-docx pillow faster-whisper

# 检查安装结果
echo ""
echo "[3/3] 验证安装..."
python3 -c "import docx; print('✓ python-docx')" || echo "✗ python-docx 安装失败"
python3 -c "import PIL; print('✓ pillow')" || echo "✗ pillow 安装失败"
python3 -c "import faster_whisper; print('✓ faster-whisper')" || echo "✗ faster-whisper 安装失败"
python3 -c "import subprocess; subprocess.run(['ffmpeg', '-version'], capture_output=True); print('✓ ffmpeg')" || echo "✗ ffmpeg 安装失败"

echo ""
echo "=== 安装完成 ==="
echo ""
echo "使用方法："
echo "  python video_to_doc.py your_video.mp4"
echo ""


path: scripts/merge_and_generate.py
content: #!/usr/bin/env python3
"""
合并截图分析和语音转录，生成文档内容
用法: python merge_and_generate.py <截图分析JSON> <语音转录JSON> <截图目录> <输出文件>
"""
import os
import sys
import json
from pathlib import Path

def load_json(file_path: str) -> dict:
    with open(file_path, "r", encoding="utf-8") as f:
        return json.load(f)

def match_voice_to_frame(timestamp: float, transcript_segments: list, window: int = 5) -> str:
    """根据时间戳匹配语音"""
    matched = []
    for seg in transcript_segments:
        if abs(seg.get("start", 0) - timestamp) <= window:
            matched.append(seg.get("text", ""))
    return " ".join(matched)

def generate_content(frames_analysis: dict, transcript: dict, frames_dir: str) -> dict:
    """
    合并截图分析和语音转录，生成文档内容
    
    Args:
        frames_analysis: {"frames": [{"timestamp": 0, "ui_elements": [...], "text_content": "..."}]}
        transcript: {"segments": [{"start": 0, "end": 2, "text": "..."}]}
        frames_dir: 截图目录
    
    Returns:
        文档内容结构
    """
    segments = transcript.get("segments", [])
    frames = frames_analysis.get("frames", [])
    
    steps = []
    for i, frame in enumerate(frames):
        timestamp = frame.get("timestamp", i * 6)
        
        # 匹配语音
        voice_text = match_voice_to_frame(timestamp, segments)
        
        # 从截图分析提取信息
        ui_elements = frame.get("ui_elements", [])
        text_content = frame.get("text_content", "")
        action_hint = frame.get("action_hint", "")
        
        # 生成标题：优先用 action_hint，否则用语音前10字
        if action_hint:
            title = action_hint[:15]
        elif voice_text:
            title = voice_text[:15].replace(" ", "")
        else:
            title = f"步骤{i+1}"
        
        # 生成描述
        if ui_elements:
            ui_text = "、".join([f"「{e.get('text', '')}」{e.get('type', '')}" for e in ui_elements[:3]])
            description = f"界面包含：{ui_text}"
            if voice_text:
                description += f"\n操作说明：{voice_text}"
        elif voice_text:
            description = voice_text
        else:
            description = "请参考截图进行操作"
        
        # 获取截图文件名
        frame_files = sorted(Path(frames_dir).glob("*.jpg"))
        frame_file = frame_files[i].name if i < len(frame_files) else f"frame_{i+1:03d}.jpg"
        
        steps.append({
            "step_num": i + 1,
            "title": title,
            "image_path": frame_file,
            "timestamp": timestamp,
            "description": description,
            "ui_elements": ui_elements,
            "voice_text": voice_text
        })
    
    return {
        "title": "操作指南",
        "overview": f"本文档共 {len(steps)} 个操作步骤，请按顺序执行。",
        "steps": steps,
        "notes": ["请按步骤顺序操作", "如有疑问请参考原视频教程"]
    }

def main():
    if len(sys.argv) < 5:
        print("用法: python merge_and_generate.py <截图分析JSON> <语音转录JSON> <截图目录> <输出文件>")
        sys.exit(1)
    
    frames_analysis_file = sys.argv[1]
    transcript_file = sys.argv[2]
    frames_dir = sys.argv[3]
    output_file = sys.argv[4]
    
    frames_analysis = load_json(frames_analysis_file)
    transcript = load_json(transcript_file)
    
    content = generate_content(frames_analysis, transcript, frames_dir)
    
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(content, f, ensure_ascii=False, indent=2)
    
    print(f"内容生成完成: {output_file}")
    print(f"共 {len(content['steps'])} 个步骤")

if __name__ == "__main__":
    main()


path: scripts/refine_content.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
内容提炼脚本
将口语化的语音转录转换为规范的操作步骤说明

用法:
    python refine_content.py <语音转录文件> [输出文件]

环境变量:
    ANTHROPIC_API_KEY - Anthropic Claude API Key
    OPENAI_API_KEY - OpenAI API Key
"""

import os
import sys
import json
import re


def refine_with_llm(voice_text: str, api_type: str = "auto") -> dict:
    """
    用大模型提炼语音内容
    
    Args:
        voice_text: 语音原文
        api_type: API类型 (anthropic/openai/auto)
    
    Returns:
        {"title": str, "description": str}
    """
    prompt = f"""你是操作手册撰写专家。将以下口语化的语音解说转换为规范的操作步骤说明。

要求：
1. 生成简洁的步骤标题（5-10字，如"输入合同号"、"点击拍照"）
2. 提取核心操作动作（点击、输入、选择等）
3. 界面元素名称用「」包裹（如「合同号」输入框、「拍照」按钮）
4. 说明可选操作（标注"可选"）
5. 分条列出操作步骤，每条一个动作

语音原文：
"{voice_text}"

直接输出 JSON 格式，不要有其他内容：
{{"title": "步骤标题", "description": "1. 动作1\\n2. 动作2\\n3. 动作3"}}"""

    # 尝试使用 Anthropic Claude
    if api_type in ["auto", "anthropic"]:
        [REDACTED_SECRET]"ANTHROPIC_API_KEY")
        if api_key:
            try:
                import anthropic
                client = anthropic.Anthropic([REDACTED_SECRET])
                response = client.messages.create(
                    model="claude-sonnet-4-20250514",
                    max_tokens=500,
                    messages=[{"role": "user", "content": prompt}]
                )
                result_text = response.content[0].text.strip()
                # 提取 JSON
                json_match = re.search(r'\{[^}]+\}', result_text, re.DOTALL)
                if json_match:
                    return json.loads(json_match.group())
            except Exception as e:
                print(f"Claude API 调用失败: {e}")
    
    # 尝试使用 OpenAI
    if api_type in ["auto", "openai"]:
        [REDACTED_SECRET]"OPENAI_API_KEY")
        if api_key:
            try:
                import openai
                client = openai.OpenAI([REDACTED_SECRET])
                response = client.chat.completions.create(
                    model="gpt-4o-mini",
                    max_tokens=500,
                    messages=[{"role": "user", "content": prompt}]
                )
                result_text = response.choices[0].message.content.strip()
                json_match = re.search(r'\{[^}]+\}', result_text, re.DOTALL)
                if json_match:
                    return json.loads(json_match.group())
            except Exception as e:
                print(f"OpenAI API 调用失败: {e}")
    
    # 无 API 时使用规则提取
    return refine_with_rules(voice_text)


def refine_with_rules(voice_text: str) -> dict:
    """
    规则提炼（无需 API）
    
    从口语化文本中提取结构化的操作步骤
    """
    # 清理文本
    text = voice_text.strip()
    
    # 常见动作模式
    action_patterns = [
        # 点击操作
        (r'点击[「\[]?([^」\]]+)[」\]]?按钮?', r'点击「\1」按钮'),
        (r'点击[「\[]?([^」\]]+)[」\]]?', r'点击「\1」'),
        (r'点[「\[]?([^」\]]+)[」\]]?', r'点击「\1」'),
        (r'按[「\[]?([^」\]]+)[」\]]?按钮?', r'点击「\1」按钮'),
        
        # 输入操作
        (r'输入[「\[]?([^」\]]+)[」\]]?[:：]?\s*([^，。,\n]+)', r'在「\1」输入框中输入\2'),
        (r'输入[「\[]?([^」\]]+)[」\]]?', r'在「\1」输入框中输入'),
        (r'填写[「\[]?([^」\]]+)[」\]]?', r'填写「\1」'),
        
        # 选择操作
        (r'选择[「\[]?([^」\]]+)[」\]]?', r'选择「\1」'),
        (r'勾选[「\[]?([^」\]]+)[」\]]?', r'勾选「\1」'),
        
        # 滑动/拖拽
        (r'向上?滑动', r'向上滑动'),
        (r'向?下?滑动', r'向下滑动'),
        (r'向左?滑动', r'向左滑动'),
        (r'向右?滑动', r'向右滑动'),
        
        # 确认/提交
        (r'确认', r'确认'),
        (r'提交', r'提交'),
        (r'保存', r'保存'),
    ]
    
    # 提取句子
    sentences = re.split(r'[。；\n]', text)
    sentences = [s.strip() for s in sentences if s.strip()]
    
    # 处理每个句子
    actions = []
    for sent in sentences:
        modified = sent
        for pattern, replacement in action_patterns:
            modified = re.sub(pattern, replacement, modified)
        if modified != sent or any(kw in modified for kw in ['点击', '输入', '选择', '滑动', '确认', '提交', '保存', '打开', '关闭']):
            actions.append(modified)
        elif sent:
            actions.append(sent)
    
    # 去重保持顺序
    seen = set()
    unique_actions = []
    for a in actions:
        if a not in seen:
            seen.add(a)
            unique_actions.append(a)
    
    # 生成标题
    title = ""
    for action in unique_actions:
        # 提取第一个动作作为标题
        match = re.search(r'[点选输滑确提保打关][击择入动认交存开闭][「\[（]?([^」\]]）））））)', action)
        if match:
            title = match.group(1)[:10]
            break
    
    if not title:
        # 取原文前8字
        title = text[:8].replace(" ", "")
    
    # 过滤掉纯口语（无操作动词）
    verb_keywords = ['点击', '输入', '选择', '勾选', '滑动', '确认', '提交', '保存', '打开', '关闭', '拍照', '扫码', '上传', '下载', '删除', '编辑', '返回', '退出']
    filtered_actions = [a for a in unique_actions if any(kw in a for kw in verb_keywords)]
    
    if not filtered_actions:
        filtered_actions = unique_actions[:3] if unique_actions else [text]
    
    # 生成描述
    description = "\n".join([f"{i+1}. {a}" for i, a in enumerate(filtered_actions[:5])])
    
    return {
        "title": title if title else "操作步骤",
        "description": description
    }


def refine_transcript(transcript_file: str, output_file: str = None):
    """
    提炼整个语音转录文件
    
    Args:
        transcript_file: 语音转录 JSON 文件
        output_file: 输出文件路径
    """
    with open(transcript_file, "r", encoding="utf-8") as f:
        transcript = json.load(f)
    
    segments = transcript.get("segments", [])
    refined_segments = []
    
    print(f"开始提炼 {len(segments)} 个语音片段...")
    
    # 检查是否有 API
    has_api = bool(os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY"))
    if has_api:
        print("✓ 检测到 API Key，将使用大模型提炼")
    else:
        print("⚠ 未检测到 API Key，将使用规则提炼（效果有限）")
        print("  可设置 ANTHROPIC_API_KEY 或 OPENAI_API_KEY 环境变量提升效果")
    
    for i, seg in enumerate(segments):
        voice_text = seg.get("text", "").strip()
        if not voice_text:
            continue
        
        print(f"  [{i+1}/{len(segments)}] 提炼: {voice_text[:30]}...")
        
        refined = refine_with_llm(voice_text)
        
        refined_segments.append({
            "start": seg.get("start", 0),
            "end": seg.get("end", 0),
            "voice_original": voice_text,
            "title": refined.get("title", "操作步骤"),
            "description": refined.get("description", voice_text)
        })
    
    result = {
        "refined": True,
        "refined_at": "2025-01-01",
        "api_used": has_api,
        "segment_count": len(refined_segments),
        "segments": refined_segments
    }
    
    if not output_file:
        output_file = transcript_file.replace(".json", "_refined.json")
    
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)
    
    print(f"\n提炼完成，保存到: {output_file}")
    return result


def main():
    if len(sys.argv) < 2:
        print("用法: python refine_content.py <语音转录文件> [输出文件]")
        print("\n环境变量:")
        print("  ANTHROPIC_API_KEY - Anthropic Claude API Key（优先）")
        print("  OPENAI_API_KEY - OpenAI API Key（备选）")
        print("\n无 API Key 时使用规则提炼")
        sys.exit(1)
    
    transcript_file = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) > 2 else None
    
    if not os.path.exists(transcript_file):
        print(f"错误: 语音转录文件不存在: {transcript_file}")
        sys.exit(1)
    
    refine_transcript(transcript_file, output_file)


if __name__ == "__main__":
    main()


path: scripts/smart_extract.py
content: #!/usr/bin/env python3
"""
智能视频截图提取脚本
自动选择最优策略：动态间隔 / 场景检测 / 语音对齐
"""

import subprocess
import json
import os
import sys

def get_video_info(video_path):
    """获取视频信息：时长、是否有音频"""
    cmd = [
        "ffprobe", "-v", "quiet",
        "-print_format", "json",
        "-show_format", "-show_streams",
        video_path
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    info = json.loads(result.stdout)
    
    duration = float(info["format"]["duration"])
    has_audio = any(s["codec_type"] == "audio" for s in info["streams"])
    
    return {"duration": duration, "has_audio": has_audio}

def calculate_interval(duration):
    """根据视频时长计算截图间隔"""
    if duration <= 30:
        return 3
    elif duration <= 60:
        return 4
    elif duration <= 180:
        return 6
    elif duration <= 600:
        return 10
    else:
        return 15

def extract_frames_interval(video_path, output_dir, interval):
    """固定间隔截图"""
    os.makedirs(output_dir, exist_ok=True)
    cmd = [
        "ffmpeg", "-i", video_path,
        "-vf", f"fps=1/{interval}",
        "-q:v", "2",
        "-y",
        f"{output_dir}/frame_%03d.jpg"
    ]
    subprocess.run(cmd, capture_output=True)
    
    # 统计生成的帧数
    frames = [f for f in os.listdir(output_dir) if f.endswith(".jpg")]
    return len(frames)

def extract_frames_scene(video_path, output_dir, threshold=0.3):
    """场景变化检测截图"""
    os.makedirs(output_dir, exist_ok=True)
    cmd = [
        "ffmpeg", "-i", video_path,
        "-vf", f"select='gt(scene,{threshold})'",
        "-vsync", "vfr",
        "-q:v", "2",
        "-y",
        f"{output_dir}/frame_%03d.jpg"
    ]
    subprocess.run(cmd, capture_output=True)
    
    frames = [f for f in os.listdir(output_dir) if f.endswith(".jpg")]
    return len(frames)

def auto_extract(video_path, output_dir=None, strategy=None, max_frames=30):
    """
    自动选择策略提取关键帧
    
    参数:
        video_path: 视频文件路径
        output_dir: 输出目录（默认：视频同目录/frames）
        strategy: 强制指定策略（interval_N / scene / auto）
        max_frames: 最大帧数限制
    
    返回:
        {"strategy": 使用的策略, "frames": 帧数, "output_dir": 输出目录}
    """
    if output_dir is None:
        output_dir = os.path.splitext(video_path)[0] + "_frames"
    
    # 获取视频信息
    info = get_video_info(video_path)
    duration = info["duration"]
    
    # 用户强制指定策略
    if strategy:
        if strategy.startswith("interval_"):
            interval = int(strategy.split("_")[1])
            frames = extract_frames_interval(video_path, output_dir, interval)
            return {"strategy": f"固定间隔({interval}秒)", "frames": frames, "output_dir": output_dir}
        elif strategy == "scene":
            frames = extract_frames_scene(video_path, output_dir)
            return {"strategy": "场景变化检测", "frames": frames, "output_dir": output_dir}
    
    # 自动选择策略
    interval = calculate_interval(duration)
    expected_frames = duration / interval
    
    # 如果预计帧数太多，启用场景检测
    if expected_frames > max_frames:
        print(f"视频较长({duration:.0f}秒)，启用场景变化检测...")
        frames = extract_frames_scene(video_path, output_dir)
        
        # 如果场景检测帧数太少，回退到间隔模式
        if frames < 5:
            print(f"场景检测仅{frames}帧，回退到间隔模式...")
            interval = duration / max_frames
            frames = extract_frames_interval(video_path, output_dir, int(interval))
            return {"strategy": f"动态间隔({int(interval)}秒)", "frames": frames, "output_dir": output_dir}
        
        return {"strategy": "场景变化检测", "frames": frames, "output_dir": output_dir}
    
    # 正常间隔模式
    print(f"视频时长{duration:.0f}秒，使用{interval}秒间隔...")
    frames = extract_frames_interval(video_path, output_dir, interval)
    
    return {"strategy": f"动态间隔({interval}秒)", "frames": frames, "output_dir": output_dir}

if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description="智能视频截图提取")
    parser.add_argument("video", help="视频文件路径")
    parser.add_argument("-o", "--output", help="输出目录")
    parser.add_argument("-s", "--strategy", help="策略: interval_N / scene / auto(默认)")
    parser.add_argument("-m", "--max-frames", type=int, default=30, help="最大帧数(默认30)")
    
    args = parser.parse_args()
    
    result = auto_extract(
        args.video,
        args.output,
        args.strategy,
        args.max_frames
    )
    
    print(f"\n完成！")
    print(f"策略: {result['strategy']}")
    print(f"帧数: {result['frames']}")
    print(f"目录: {result['output_dir']}")


path: scripts/transcribe_audio.py
content: #!/usr/bin/env python3
"""
音频转录脚本
支持多种ASR服务：OpenAI Whisper API、阿里云、讯飞等
"""

import json
import sys
import os

def transcribe_with_openai_whisper(audio_path, api_key=None):
    """使用 OpenAI Whisper API 转录"""
    try:
        import openai
    except ImportError:
        print("请先安装: pip install openai")
        return None
    
    client = openai.OpenAI([REDACTED_SECRET] or os.environ.get("OPENAI_API_KEY"))
    
    with open(audio_path, "rb") as audio_file:
        transcript = client.audio.transcriptions.create(
            model="whisper-1",
            file=audio_file,
            response_format="verbose_json",
            timestamp_granularities=["segment"]
        )
    
    # 转换为统一格式
    result = {
        "segments": [
            {"start": seg.start, "end": seg.end, "text": seg.text}
            for seg in transcript.segments
        ]
    }
    return result

def transcribe_with_alibaba(audio_path, appkey=None, token=None):
    """使用阿里云语音识别 API"""
    # TODO: 实现阿里云ASR
    print("阿里云ASR尚未实现，请选择其他方案")
    return None

def transcribe_with_xfyun(audio_path, appid=None, api_key=None, api_secret=None):
    """使用讯飞语音识别 API"""
    # TODO: 实现讯飞ASR
    print("讯飞ASR尚未实现，请选择其他方案")
    return None

def transcribe_with_local_whisper(audio_path, model="tiny"):
    """使用 faster-whisper 本地转录（更快更轻量）"""
    try:
        from faster_whisper import WhisperModel
    except ImportError:
        print("请先安装: pip install faster-whisper")
        return None
    
    # 设置 HuggingFace 镜像（国内加速）
    os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
    
    print(f"加载 faster-whisper 模型: {model}...")
    # device="cpu", compute_type="int8" 适合无 GPU 环境
    whisper_model = WhisperModel(model, device="cpu", compute_type="int8")
    
    print("转录中...")
    segments, info = whisper_model.transcribe(audio_path, language="zh")
    
    # 转换为统一格式
    result_segments = [
        {"start": seg.start, "end": seg.end, "text": seg.text.strip()}
        for seg in segments
    ]
    print(f"转录完成，共 {len(result_segments)} 个片段")
    return {"segments": result_segments}

def transcribe(audio_path, method="local", **kwargs):
    """
    统一转录接口
    
    参数:
        audio_path: 音频文件路径
        method: 转录方法 (openai/local/alibaba/xfyun)
        **kwargs: 各方法所需的参数
    
    返回:
        {"segments": [{"start": float, "end": float, "text": str}, ...]}
    """
    if not os.path.exists(audio_path):
        print(f"错误: 音频文件不存在: {audio_path}")
        return None
    
    if method == "openai":
        return transcribe_with_openai_whisper(audio_path, kwargs.get("api_key"))
    elif method == "local":
        return transcribe_with_local_whisper(audio_path, kwargs.get("model", "base"))
    elif method == "alibaba":
        return transcribe_with_alibaba(audio_path, kwargs.get("appkey"), kwargs.get("token"))
    elif method == "xfyun":
        return transcribe_with_xfyun(audio_path, kwargs.get("appid"), kwargs.get("api_key"), kwargs.get("api_secret"))
    else:
        print(f"不支持的转录方法: {method}")
        return None

def main():
    if len(sys.argv) < 2:
        print("使用方法: python transcribe_audio.py <音频文件> [方法] [参数]")
        print("方法: local(默认), openai, alibaba, xfyun")
        print("")
        print("示例:")
        print("  python transcribe_audio.py audio.wav local")
        print("  python transcribe_audio.py audio.wav openai --api-key YOUR_KEY")
        print("  python transcribe_audio.py audio.wav local --model small")
        sys.exit(1)
    
    audio_path = sys.argv[1]
    method = sys.argv[2] if len(sys.argv) > 2 else "local"
    
    # 解析额外参数
    kwargs = {}
    args = sys.argv[3:]
    for i in range(0, len(args), 2):
        key = args[i].replace("--", "").replace("-", "_")
        value = args[i + 1] if i + 1 < len(args) else None
        if value:
            kwargs[key] = value
    
    print(f"音频文件: {audio_path}")
    print(f"转录方法: {method}")
    print("")
    
    result = transcribe(audio_path, method, **kwargs)
    
    if result:
        # 保存结果
        output_path = audio_path.replace(".wav", "_transcript.json")
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=2)
        print(f"\n转录完成，结果保存到: {output_path}")
        
        # 打印摘要
        print("\n=== 转录摘要 ===")
        for seg in result["segments"]:
            print(f"[{seg['start']:.1f}s - {seg['end']:.1f}s] {seg['text']}")
    else:
        print("转录失败")
        sys.exit(1)

if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "video-to-doc",
  "version": "1.0.0",
  "author": "your-name",
  "description": "将操作视频自动转换为图文并茂的Word操作指南。支持智能截图、语音转录、一键生成文档。",
  "long_description": "本技能能够自动分析视频文件，提取关键帧截图，识别界面元素和操作内容，并生成结构化的图文操作指南Word文档。支持：1)智能截图策略（动态间隔/场景检测）；2)语音转录（本地Whisper）；3)截图内容分析；4)一键生成Word文档。",
  "trigger": ["视频转文档", "生成操作手册", "视频截图", "操作指南"],
  "category": ["文档处理", "视频处理", "效率工具"],
  "keywords": ["视频", "文档", "操作手册", "截图", "转录", "Word"],
  "price": {
    "trial": {
      "free_count": 3,
      "description": "免费试用3次"
    },
    "official": {
      "coins": 50,
      "type": "per_use",
      "description": "50虾米/次"
    }
  },
  "dependencies": {
    "python": [
      {"name": "faster-whisper", "version": ">=0.10.0"},
      {"name": "pillow", "version": ">=10.0.0"},
      {"name": "python-docx", "version": ">=0.8.11"},
      {"name": "anthropic", "version": ">=0.18.0"}
    ],
    "npm": [
      {"name": "docx", "version": ">=8.0.0"}
    ],
    "system": [
      {"name": "ffmpeg", "description": "视频处理工具"}
    ]
  },
  "requirements": {
    "min_python": "3.8",
    "os": ["linux", "windows", "macos"]
  },
  "usage": {
    "input": "视频文件路径",
    "output": "Word文档(.docx)",
    "example": "python scripts/smart_extract.py video.mp4"
  },
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2025-04-10",
      "changes": ["首次发布", "支持智能截图策略", "支持语音转录"]
    }
  ],
  "license": "MIT",
  "support": {
    "email": "",
    "docs": ""
  }
}


path: video_to_doc.py
content: #!/usr/bin/env python3
"""
Video-to-Doc 一键转换入口
用法: python video_to_doc.py <视频文件> [选项]

新版流程（使用 read_image 分析截图）：
1. 提取截图和语音转录（Python 脚本）
2. 截图分析（主对话用 read_image 工具）
3. 合并生成文档（Python 脚本）
"""

import os
import sys
import argparse
import subprocess
import requests
import json
import glob

# 技能配置
SKILL_ID = "video-to-doc"  # 发布后替换为实际ID
LICENSE_FILE = ".video_to_doc_license"

def check_license():
    """
    检查授权状态
    返回: {"valid": bool, "remaining": int, "message": str}
    """
    # 检查本地许可证缓存
    if os.path.exists(LICENSE_FILE):
        with open(LICENSE_FILE, "r") as f:
            license_data = json.load(f)
            # 可以添加过期时间检查等
            return license_data
    
    return {"valid": False, "remaining": 0, "message": "未授权"}

def verify_online(user_token=None):
    """
    在线验证授权（通过虾评API）
    需要用户提供token或从环境变量读取
    """
    if not user_token:
        user_token = os.environ.get("XIAPING_TOKEN", "")
    
    if not user_token:
        return {"valid": False, "message": "请设置 XIAPING_TOKEN 环境变量"}
    
    try:
        response = requests.post(
            "https://xiaping.coze.site/api/skills/verify",
            headers={"Authorization": f"Bearer {user_token}"},
            json={"skill_id": SKILL_ID},
            timeout=10
        )
        result = response.json()
        
        if result.get("authorized"):
            # 缓存授权信息
            license_data = {
                "valid": True,
                "remaining": result.get("remaining", 0),
                "expires": result.get("expires"),
                "message": "授权有效"
            }
            with open(LICENSE_FILE, "w") as f:
                json.dump(license_data, f)
            return license_data
        else:
            return {"valid": False, "message": result.get("message", "授权无效")}
    
    except Exception as e:
        return {"valid": False, "message": f"验证失败: {str(e)}"}

def consume_usage():
    """
    消耗一次使用次数
    调用平台API扣减
    """
    user_token = os.environ.get("XIAPING_TOKEN", "")
    if not user_token:
        return False
    
    try:
        response = requests.post(
            "https://xiaping.coze.site/api/skills/consume",
            headers={"Authorization": f"Bearer {user_token}"},
            json={"skill_id": SKILL_ID},
            timeout=10
        )
        return response.json().get("success", False)
    except:
        return False

def check_dependencies():
    """检查依赖是否安装"""
    missing = []
    
    # 检查 ffmpeg
    result = subprocess.run(["which", "ffmpeg"], capture_output=True)
    if result.returncode != 0:
        missing.append("ffmpeg (系统)")
    
    # 检查 Python 包
    try:
        import faster_whisper
    except ImportError:
        missing.append("faster-whisper (pip install faster-whisper)")
    
    try:
        from PIL import Image
    except ImportError:
        missing.append("pillow (pip install pillow)")
    
    # 检查 docx (npm)
    result = subprocess.run(["npm", "list", "docx"], capture_output=True)
    if b"docx" not in result.stdout:
        missing.append("docx (npm install docx)")
    
    return missing

def install_dependencies():
    """安装依赖"""
    print("正在安装依赖...")
    
    # Python 依赖
    subprocess.run([sys.executable, "-m", "pip", "install", 
                   "faster-whisper", "pillow", "python-docx", "anthropic", "-q"])
    
    # npm 依赖
    subprocess.run(["npm", "install", "docx", "-q"])
    
    print("依赖安装完成！")

def main():
    parser = argparse.ArgumentParser(
        description="视频转图文操作指南",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  python video_to_doc.py video.mp4
  python video_to_doc.py video.mp4 -o output.docx
  python video_to_doc.py video.mp4 --continue
  python video_to_doc.py video.mp4 --skip-extract

新流程说明:
  1. 第一次运行：提取截图和语音转录，生成等待分析的标记
  2. 主对话使用 read_image 分析截图，将结果保存为 JSON
  3. 第二次运行 --continue：合并分析结果和语音，生成文档
        """
    )
    
    parser.add_argument("video", nargs="?", help="视频文件路径（--continue 时可选）")
    parser.add_argument("-o", "--output", help="输出文档路径（默认：视频名_操作指南.docx）")
    parser.add_argument("-m", "--max-frames", type=int, default=30, help="最大截图数（默认30）")
    parser.add_argument("--no-audio", action="store_true", help="跳过语音转录")
    parser.add_argument("--install-deps", action="store_true", help="自动安装依赖")
    parser.add_argument("--check-deps", action="store_true", help="检查依赖状态")
    parser.add_argument("--check-license", action="store_true", help="检查授权状态")
    parser.add_argument("--token", help="虾评平台Token（或设置XIAPING_TOKEN环境变量）")
    parser.add_argument("--continue", dest="continue_mode", action="store_true", help="继续生成（等待分析完成后使用）")
    parser.add_argument("--skip-extract", action="store_true", help="跳过截图和语音提取（使用已有文件）")
    args = parser.parse_args()
    
    # 检查授权状态
    if args.check_license:
        license_status = verify_online(args.token)
        if license_status["valid"]:
            print(f"✓ 授权有效")
            print(f"  剩余次数: {license_status.get('remaining', '无限')}")
        else:
            print(f"✗ {license_status['message']}")
            print("  请前往 https://xiaping.coze.site 购买技能")
        return
    
    # 检查依赖模式
    if args.check_deps:
        missing = check_dependencies()
        if missing:
            print("缺少依赖：")
            for dep in missing:
                print(f"  - {dep}")
            print("\n运行 'python video_to_doc.py --install-deps' 自动安装")
        else:
            print("所有依赖已安装 ✓")
        return
    
    # 安装依赖模式
    if args.install_deps:
        install_dependencies()
        return
    
    # 继续模式：从已有文件恢复
    if args.continue_mode:
        # 查找最新的工作目录
        video_files = glob.glob("*_frames")
        if not video_files:
            print("错误：未找到截图目录，无法继续")
            print("请先运行正常流程提取截图")
            sys.exit(1)
        
        video_name = video_files[0].replace("_frames", "")
        frames_dir = f"{video_name}_frames"
        audio_file = f"{video_name}_audio.wav"
        output_doc = args.output or f"{video_name}_操作指南.docx"
        
        print(f"\n{'='*50}")
        print(f"继续生成文档")
        print(f"{'='*50}")
        print(f"工作目录: {video_name}")
        print(f"截图目录: {frames_dir}")
        print(f"{'='*50}\n")
    else:
        # 正常模式
        if not args.video:
            print("错误：请提供视频文件路径")
            print("或使用 --continue 继续之前的任务")
            sys.exit(1)
        
        # 检查视频文件
        if not os.path.exists(args.video):
            print(f"错误：视频文件不存在: {args.video}")
            sys.exit(1)
        
        # 检查依赖
        missing = check_dependencies()
        if missing:
            print("缺少依赖，正在自动安装...")
            install_dependencies()
        
        # 设置输出路径
        video_name = os.path.splitext(os.path.basename(args.video))[0]
        output_doc = args.output or f"{video_name}_操作指南.docx"
        frames_dir = f"{video_name}_frames"
        audio_file = f"{video_name}_audio.wav"
        
        print(f"\n{'='*50}")
        print(f"视频转文档工具 v2.0 (read_image 模式)")
        print(f"{'='*50}")
        print(f"输入: {args.video}")
        print(f"输出: {output_doc}")
        print(f"{'='*50}\n")
        
        # ========== 授权验证 ==========
        license_status = check_license()
        if license_status.get("valid"):
            print(f"本地授权有效，剩余次数: {license_status.get('remaining', '无限')}")
        else:
            license_status = verify_online(args.token)
            if not license_status["valid"]:
                print("\n" + "="*50)
                print("授权验证失败")
                print("="*50)
                print(f"原因: {license_status['message']}")
                print("\n请前往虾评平台购买技能：")
                print("https://xiaping.coze.site/skill/video-to-doc")
                print("\n购买后，设置环境变量：")
                print("export XIAPING_TOKEN=your_token")
                print("="*50)
                sys.exit(1)
        
        print(f"授权验证通过，剩余次数: {license_status.get('remaining', '无限')}")
        
        # 消耗一次使用次数
        if not consume_usage():
            print("警告：次数扣减失败，但仍继续执行")
        # ========== 授权验证结束 ==========
        
        # ========== 步骤1-2：截图和语音转录 ==========
        if not args.skip_extract:
            print("[1/6] 提取关键帧截图...")
            result = subprocess.run([
                sys.executable, "scripts/smart_extract.py",
                args.video, "-o", frames_dir, "-m", str(args.max_frames)
            ])
            if result.returncode != 0:
                print("截图提取失败")
                sys.exit(1)
            
            # 步骤2：语音转录（可选）
            if not args.no_audio:
                print("\n[2/6] 提取音频...")
                subprocess.run([
                    "ffmpeg", "-i", args.video,
                    "-vn", "-acodec", "pcm_s16le",
                    "-ar", "16000", "-ac", "1",
                    audio_file, "-y"
                ], capture_output=True)
                
                if os.path.exists(audio_file):
                    print("[3/6] 语音转录中...")
                    subprocess.run([
                        sys.executable, "scripts/transcribe_audio.py",
                        audio_file, "local"
                    ])
        
        # ========== 步骤3：等待截图分析 ==========
        print("\n" + "="*50)
        print("[3/6] 等待截图分析...")
        print("="*50)
        
        frames_analysis_file = f"{video_name}_frames_analysis.json"
        transcript_file = audio_file.replace(".wav", "_transcript.json") if os.path.exists(audio_file) else None
        
        # 检查是否已有分析结果
        if os.path.exists(frames_analysis_file):
            with open(frames_analysis_file, "r", encoding="utf-8") as f:
                analysis_data = json.load(f)
            
            # 检查是否包含有效分析
            frames = analysis_data.get("frames", [])
            if frames and len(frames) > 0:
                print(f"✓ 发现已有分析结果，共 {len(frames)} 个帧")
            else:
                print(f"分析文件存在但为空，将重新等待分析")
                analysis_data = None
        else:
            analysis_data = None
        
        if not analysis_data:
            # 创建等待分析的标记文件
            frames_list = []
            if os.path.exists(frames_dir):
                frame_files = sorted(glob.glob(os.path.join(frames_dir, "*.jpg")))
                for i, f in enumerate(frame_files):
                    frames_list.append({
                        "index": i,
                        "timestamp": i * 6,  # 估算时间戳
                        "image_path": os.path.basename(f)
                    })
            
            wait_data = {
                "status": "waiting_for_analysis",
                "frames_dir": frames_dir,
                "frames": frames_list,
                "need_analysis": True
            }
            
            with open(frames_analysis_file, "w", encoding="utf-8") as f:
                json.dump(wait_data, f, ensure_ascii=False, indent=2)
            
            print("\n" + "!"*50)
            print("截图已提取，请使用 read_image 分析截图")
            print("!"*50)
            print(f"\n截图目录: {frames_dir}")
            print(f"分析结果保存到: {frames_analysis_file}")
            print(f"\n截图文件列表:")
            for frame in frames_list:
                print(f"  - {frame['image_path']}")
            print(f"\n预期分析结果格式:")
            print("""{
  "frames": [
    {
      "timestamp": 0,
      "ui_elements": [{"text": "按钮文字", "type": "按钮"}],
      "text_content": "界面主要内容",
      "action_hint": "点击新增按钮"
    }
  ]
}""")
            print("\n分析完成后，使用以下命令继续：")
            print(f"python video_to_doc.py --continue")
            print("="*50 + "\n")
            
            # 打印给主对话的信息
            print("[MARKER_FOR_MAIN_AGENT]")
            print("WAIT_FOR_FRAME_ANALYSIS")
            print(f"frames_dir={frames_dir}")
            print(f"analysis_output={frames_analysis_file}")
            print(f"frame_count={len(frames_list)}")
            print("[/MARKER_FOR_MAIN_AGENT]")
            
            sys.exit(0)
        
        # ========== 步骤4：合并生成 ==========
        print("\n[4/6] 合并截图分析和语音转录...")
        content_file = f"{video_name}_content.json"
        
        if transcript_file and os.path.exists(transcript_file):
            subprocess.run([
                sys.executable, "scripts/merge_and_generate.py",
                frames_analysis_file,
                transcript_file,
                frames_dir,
                content_file
            ])
        else:
            # 无语音转录时，使用空的转录文件
            empty_transcript = f"{video_name}_empty_transcript.json"
            with open(empty_transcript, "w", encoding="utf-8") as f:
                json.dump({"segments": []}, f)
            
            subprocess.run([
                sys.executable, "scripts/merge_and_generate.py",
                frames_analysis_file,
                empty_transcript,
                frames_dir,
                content_file
            ])
        
        # ========== 步骤5：生成文档 ==========
        print("\n[5/6] 生成Word文档...")
        subprocess.run([
            sys.executable, "scripts/generate_docx.py",
            content_file,
            frames_dir,
            output_doc
        ])
        
        print("\n" + "="*50)
        print("[6/6] 完成！")
        print("="*50)
        print(f"输出文件: {output_doc}")
        print("="*50)

if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)