261 lines
8.6 KiB
Markdown
261 lines
8.6 KiB
Markdown
# Piano Highlight Generator - GUI 编辑功能实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 实现 GUI 编辑功能,支持增删改知识点和字幕,CLI/GUI 共用项目文件体系,底层原子化复用。
|
||
|
||
**Architecture:**
|
||
- `generated_config.yaml` = 项目文件(元信息 + clips 配置)
|
||
- `config.ini` = 全局配置(API 密钥等),不跟项目
|
||
- 底层原子操作在 core/,CLI 和 GUI 共用
|
||
- 按需重生成(只重烧受影响的 clip)
|
||
|
||
**Tech Stack:** PySide6 GUI, YAML, faster-whisper, FFmpeg
|
||
|
||
---
|
||
|
||
## Phase 0: 配置体系重构
|
||
|
||
### Task 1: 项目文件增加元信息字段
|
||
|
||
**Files:**
|
||
- Modify: `src/core/pipeline.py` — Pipeline.run() 结束时写 `video_src`、`ppt_path`、`max_total_duration` 到 generated_config.yaml
|
||
- Modify: `src/core/ppt_parser.py` — `_create_config()` 写入 `video_src` 和 `ppt_path`
|
||
|
||
**Steps:**
|
||
- [ ] 在 `Pipeline.run()` 或 `step_generate_subtitles` 结束后,读取 config 中的 `video_src`/`ppt_path`,写入 generated_config.yaml
|
||
- [ ] 确保 `_create_config()` 包含 `max_total_duration` 字段
|
||
- [ ] 验证:跑完 run.bat 后,generated_config.yaml 包含 video_src 和 ppt_path
|
||
|
||
---
|
||
|
||
### Task 2: config.py 重构(项目路径 vs 全局配置分离)
|
||
|
||
**Files:**
|
||
- Modify: `config.py` — 移除 VIDEO/PPT/OUTPUT,API 配置留在 config.ini
|
||
- Modify: `run.py` — 从 config.py 只读 API 相关字段,项目路径由 CLI 参数指定
|
||
- Modify: `src/cli.py` — 确认 --video/--ppt/--output 参数能正确传入 pipeline
|
||
|
||
**Steps:**
|
||
- [ ] 修改 `config.py`:只保留 API_KEY/API_HOST/PYTHON/CLI_DIR,移除 VIDEO/PPT/OUTPUT/MAX_TOTAL_DURATION
|
||
- [ ] 修改 `run.py`:从 config.py 读 API 配置,VIDEO/PPT/OUTPUT 通过 cli.py 参数传入(cli.py 已有 --video/--ppt/--output)
|
||
- [ ] 验证:`run.bat` 能正常跑完整流程
|
||
|
||
---
|
||
|
||
## Phase 1: 底层原子化
|
||
|
||
### Task 3: 提取标题匹配函数
|
||
|
||
**Files:**
|
||
- Modify: `src/core/ppt_parser.py` — 提取 `_find_title_in_transcript(title, transcript_segments)` 函数
|
||
- Create: `src/core/subtitle_matcher.py` (可选,如果逻辑复杂则独立)
|
||
|
||
**Steps:**
|
||
- [ ] 在 `ppt_parser.py` 中提取 `_find_title_in_transcript(title, corrected_segments)`:
|
||
- 输入:标题文字 + corrected_transcript.json 的 segments 列表
|
||
- 处理:在每个 segment 的 text 中搜索标题关键词(子串匹配)
|
||
- 返回:`(start, end)` 或 `None`(匹配不到)
|
||
- [ ] 写单元测试验证:mock segments 数据,测试匹配返回正确时间戳,匹配不到返回 None
|
||
- [ ] 验证:corrected_transcript.json 存在情况下,给定一个已知标题能找到对应时间段
|
||
|
||
---
|
||
|
||
### Task 4: reextract_clip — 单标题重新匹配
|
||
|
||
**Files:**
|
||
- Modify: `src/core/pipeline.py` — 增加 `reextract_clip(clip_index, new_title)` 方法
|
||
|
||
**Steps:**
|
||
- [ ] 在 Pipeline 类中增加 `reextract_clip(self, clip_index, new_title)`:
|
||
```python
|
||
def reextract_clip(self, clip_index, new_title):
|
||
clip = self.clips[clip_index]
|
||
# 加载 corrected_transcript.json
|
||
# 调用 _find_title_in_transcript(new_title, segments)
|
||
# 匹配到 → 更新 clip['start']/clip['end']
|
||
# 匹配不到 → clip['matched'] = False(或标记)
|
||
# 调用 _merge_overlapping_clips 如有必要
|
||
# 保存 updated config to generated_config.yaml
|
||
# 删除 clip_index 对应的 json(触发重生成)
|
||
```
|
||
- [ ] 写测试:用 lesson1 的 output,跑完后 reextract 某个 clip 改标题,验证 config 更新
|
||
|
||
---
|
||
|
||
### Task 5: delete_clip — 删除 clip
|
||
|
||
**Files:**
|
||
- Modify: `src/core/pipeline.py` — 增加 `delete_clip(clip_index)` 方法
|
||
|
||
**Steps:**
|
||
- [ ] 在 Pipeline 类中增加 `delete_clip(self, clip_index)`:
|
||
```python
|
||
def delete_clip(self, clip_index):
|
||
# 从 self.clips 删除该 clip
|
||
# 删除 intermediates/clipN.json 和 clipN.mp4
|
||
# 保存 updated config to generated_config.yaml
|
||
```
|
||
- [ ] 写测试:跑完后删一个 clip,验证 json/mp4 删除、config 更新
|
||
|
||
---
|
||
|
||
### Task 6: add_clip_by_title — 新增知识点
|
||
|
||
**Files:**
|
||
- Modify: `src/core/pipeline.py` — 增加 `add_clip_by_title(new_title)` 方法
|
||
|
||
**Steps:**
|
||
- [ ] 在 Pipeline 类中增加 `add_clip_by_title(self, new_title)`:
|
||
```python
|
||
def add_clip_by_title(self, new_title):
|
||
# 调用 _find_title_in_transcript 匹配时间段
|
||
# 匹配到 → 判断是否与现有 clip 重叠 → 合并处理
|
||
# 匹配不到 → 标记 matched=False,不加入 self.clips(或加到待确认列表)
|
||
# 保存 updated config
|
||
```
|
||
- [ ] 写测试:加一个新标题,验证 config 更新、json 不生成(待确认状态)
|
||
|
||
---
|
||
|
||
### Task 7: reburn_titles / reburn_subtitles — 部分重烧
|
||
|
||
**Files:**
|
||
- Modify: `src/core/pipeline.py` — 增加 `reburn_titles()` 和 `reburn_subtitles(user_texts=None)` 方法
|
||
- Modify: `src/core/subtitle.py` — 检查 `generate_from_clips` 能否跳过 LLM 校正直接用用户文本
|
||
|
||
**Steps:**
|
||
- [ ] `reburn_titles(self)`:
|
||
```python
|
||
def reburn_titles(self):
|
||
# 用已有的 clip configs(不重生成 json)
|
||
# 调用 subtitle_pipeline.generate_from_clips
|
||
# 烧录标题轨到 subs/v1_title.srt
|
||
```
|
||
- [ ] `reburn_subtitles(self, user_texts=None)`:
|
||
```python
|
||
def reburn_subtitles(self, user_texts=None):
|
||
# user_texts: 可选,直接用用户文本烧字幕,跳过 LLM 校正
|
||
# 读取 v1_content.srt 或直接用传入的文本
|
||
# 烧录字幕轨到 subs/v1_content.srt
|
||
```
|
||
- [ ] 修改 `burn_only.py` 适配新的 Pipeline 方法
|
||
|
||
---
|
||
|
||
## Phase 2: GUI 重构
|
||
|
||
### Task 8: GUI 两种启动模式
|
||
|
||
**Files:**
|
||
- Modify: `src/gui.py` — 重构为分步界面(启动页 → 处理/编辑页)
|
||
|
||
**Steps:**
|
||
- [ ] 重构 GUI 启动页:两个选项按钮
|
||
- "新建项目" → 跳转文件选择(视频+PPT+输出目录)
|
||
- "打开已有项目" → 打开目录选择框 → 加载 generated_config.yaml
|
||
- [ ] 实现"打开已有项目":
|
||
```python
|
||
def load_project(self, output_dir):
|
||
config = load_yaml(os.path.join(output_dir, 'generated_config.yaml'))
|
||
# 设置 video_src, ppt_path, output_dir
|
||
# 初始化 Pipeline(config)
|
||
# 加载 clips 列表显示
|
||
# 加载字幕预览
|
||
```
|
||
- [ ] 验证:打开 lesson1 output 目录,能正确显示 clip 列表
|
||
|
||
---
|
||
|
||
### Task 9: GUI 编辑界面
|
||
|
||
**Files:**
|
||
- Modify: `src/gui.py` — 增加编辑界面组件
|
||
|
||
**Steps:**
|
||
- [ ] 左侧 clip 列表:
|
||
- QListWidget 显示 clip 标题列表
|
||
- 双击编辑标题
|
||
- 右键菜单:删除、新增
|
||
- 未匹配 clip 显示红色/警告图标
|
||
- [ ] 右侧字幕预览:
|
||
- QTextEdit 显示 v1_content.srt 内容
|
||
- 用户可直接编辑
|
||
- [ ] 底部"应用"按钮:
|
||
- 收集所有修改
|
||
- 调用底层原子操作
|
||
- 进度显示
|
||
- 重烧后刷新预览
|
||
|
||
---
|
||
|
||
### Task 10: 应用按钮 — 原子操作集成
|
||
|
||
**Files:**
|
||
- Modify: `src/gui.py` — 应用按钮连接到 Pipeline 原子方法
|
||
|
||
**Steps:**
|
||
- [ ] 应用按钮逻辑:
|
||
```python
|
||
def on_apply(self):
|
||
# 检测变化:
|
||
# - clip 标题改了 → reextract_clip(i, new_title)
|
||
# - clip 删了 → delete_clip(i)
|
||
# - 新增知识点 → add_clip_by_title(title)
|
||
# - 字幕改了 → reburn_subtitles(user_texts)
|
||
# - 任一 clip 变了 → reburn_titles()
|
||
# 最后调用 Pipeline.step_burn() 或 reburn 最终视频
|
||
```
|
||
- [ ] 未匹配 clip 不参与重烧,显示提示
|
||
- [ ] 验证:改标题 → 点应用 → final.mp4 更新
|
||
|
||
---
|
||
|
||
### Task 11: CLI/GUI 互操作测试
|
||
|
||
**Steps:**
|
||
- [ ] CLI run.bat 跑 lesson1 → 生成完整 output
|
||
- [ ] GUI 打开同一 output 目录
|
||
- [ ] 改 clip3 标题为"新标题" → 点应用 → 验证 config 更新、final.mp4 更新
|
||
- [ ] GUI 删 clip5 → 点应用 → 验证 config 更新、final.mp4 更新
|
||
- [ ] GUI 改字幕文本 → 点应用 → 验证 final.mp4 更新
|
||
|
||
---
|
||
|
||
## Phase 3: 收尾
|
||
|
||
### Task 12: 删除死代码
|
||
|
||
**Files:**
|
||
- Identify: 检查 gui.py 中被替换掉的旧代码
|
||
- Remove: 删除不再使用的 UI 组件和逻辑
|
||
|
||
---
|
||
|
||
### Task 13: 更新文档
|
||
|
||
**Files:**
|
||
- Modify: `docs/USAGE.md` — 增加 GUI 编辑说明
|
||
|
||
---
|
||
|
||
### Task 14: commit
|
||
|
||
**Steps:**
|
||
- [ ] git add -A
|
||
- [ ] commit: "feat: GUI edit mode with clip/subtitle editing, config separation"
|
||
- [ ] push
|
||
|
||
---
|
||
|
||
## 自检清单
|
||
|
||
- [ ] 所有修改文件有备份/可回滚
|
||
- [ ] 每个 task 后验证功能正常
|
||
- [ ] CLI 完整流程测试通过
|
||
- [ ] GUI 新建项目测试通过
|
||
- [ ] GUI 打开已有项目测试通过
|
||
- [ ] 改/删/增 clip 后 final.mp4 正确更新
|
||
- [ ] 字幕编辑后 final.mp4 正确更新
|
||
- [ ] 无新增警告或 lint 错误
|