Initial commit: lesson-highlights generator
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
# Piano Highlight Generator App - 技术设计
|
||||
|
||||
> 设计日期:2026-05-02
|
||||
> 版本:1.0
|
||||
> 状态:Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
| 层级 | 技术 | 选型理由 |
|
||||
|------|------|----------|
|
||||
| GUI 框架 | PySide6 (Qt for Python) | LGPL 许可,功能完备,信号槽机制适合异步更新 |
|
||||
| 打包工具 | Nuitka | 编译为 C,性能好,体积小 |
|
||||
| 状态持久化 | JSON 文件 | 简单,无需数据库依赖 |
|
||||
| 核心模块 | 复用现有脚本 | video.py, subtitle.py, llm.py, corrections.py |
|
||||
| 配置格式 | YAML/JSON | 用户友好,可读性好 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
```
|
||||
piano-highlight-app/
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # 应用入口
|
||||
│ ├── app.py # QMainWindow 主窗口
|
||||
│ ├── gui/ # GUI 组件
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── config_panel.py # 配置面板
|
||||
│ │ ├── progress_view.py # 进度监控
|
||||
│ │ ├── title_editor.py # 标题编辑器
|
||||
│ │ └── log_view.py # 日志窗口
|
||||
│ ├── logic/ # 业务逻辑
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── config_manager.py # 配置管理
|
||||
│ │ ├── pipeline_controller.py # 流水线控制
|
||||
│ │ ├── state_manager.py # 状态管理
|
||||
│ │ └── worker.py # 后台工作线程
|
||||
│ └── core/ # 核心模块(复用)
|
||||
│ ├── __init__.py
|
||||
│ ├── constants.py # 常量
|
||||
│ ├── utils.py # 工具函数
|
||||
│ ├── video.py # 视频处理
|
||||
│ ├── subtitle.py # 字幕处理
|
||||
│ ├── llm.py # LLM 调用
|
||||
│ └── corrections.py # 纠错规则
|
||||
├── assets/ # 资源文件
|
||||
│ └── icons/
|
||||
├── requirements.txt # 依赖
|
||||
├── pyproject.toml # 项目配置
|
||||
├── nuitka_options.py # Nuitka 打包配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心类设计
|
||||
|
||||
### 3.1 StateManager(状态管理)
|
||||
|
||||
```python
|
||||
class StateManager:
|
||||
"""状态管理器 - 负责状态持久化"""
|
||||
|
||||
def __init__(self, state_file: str):
|
||||
self.state_file = state_file
|
||||
self.state = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
"""从文件加载状态"""
|
||||
|
||||
def save(self):
|
||||
"""保存状态到文件"""
|
||||
|
||||
def get_current_step(self) -> int:
|
||||
"""获取当前步骤"""
|
||||
|
||||
def set_step_status(self, step: str, status: str):
|
||||
"""设置步骤状态 (pending/in_progress/completed/failed)"""
|
||||
|
||||
def update_clip_status(self, clip_index: int, **kwargs):
|
||||
"""更新 clip 状态"""
|
||||
|
||||
def get_clip_titles(self) -> list:
|
||||
"""获取所有 clip 的标题(含用户修改)"""
|
||||
```
|
||||
|
||||
### 3.2 PipelineController(流水线控制)
|
||||
|
||||
```python
|
||||
class PipelineController:
|
||||
"""流水线控制器 - 管理处理流程"""
|
||||
|
||||
# 步骤定义
|
||||
STEPS = [
|
||||
'ready',
|
||||
'extracting',
|
||||
'transcribing',
|
||||
'title_correcting',
|
||||
'generating_subtitles',
|
||||
'merging',
|
||||
'burning',
|
||||
'completed'
|
||||
]
|
||||
|
||||
def __init__(self, config: dict, state_manager: StateManager):
|
||||
self.config = config
|
||||
self.state = state_manager
|
||||
self.is_paused = False
|
||||
self.is_stopped = False
|
||||
|
||||
def run(self, worker: Worker):
|
||||
"""运行流水线"""
|
||||
|
||||
def pause(self):
|
||||
"""暂停流水线"""
|
||||
|
||||
def resume(self):
|
||||
"""恢复流水线"""
|
||||
|
||||
def stop(self):
|
||||
"""停止流水线"""
|
||||
|
||||
def step_extracting(self):
|
||||
"""Step 1: 提取片段"""
|
||||
|
||||
def step_transcribing(self):
|
||||
"""Step 2: 转录"""
|
||||
|
||||
def step_title_correcting(self) -> list:
|
||||
"""Step 3: 标题纠正 - 返回需要用户确认的标题"""
|
||||
# 返回标题列表,用户可以在此介入修改
|
||||
|
||||
def step_generating_subtitles(self):
|
||||
"""Step 4: 生成字幕"""
|
||||
|
||||
def step_merging(self):
|
||||
"""Step 5: 合并视频"""
|
||||
|
||||
def step_burning(self):
|
||||
"""Step 6: 烧录字幕"""
|
||||
```
|
||||
|
||||
### 3.3 Worker(后台工作线程)
|
||||
|
||||
```python
|
||||
class Worker(QThread):
|
||||
"""后台工作线程 - 在独立线程中执行流水线"""
|
||||
|
||||
progress_signal = pyqtSignal(str, int, str) # step, percent, message
|
||||
clip_completed_signal = pyqtSignal(int) # clip_index
|
||||
step_completed_signal = pyqtSignal(str) # step_name
|
||||
titles_ready_signal = pyqtSignal(list) # 标题列表,等待用户确认
|
||||
finished_signal = pyqtSignal(bool, str) # success, message
|
||||
log_signal = pyqtSignal(str) # 日志消息
|
||||
|
||||
def __init__(self, controller: PipelineController):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
|
||||
def run(self):
|
||||
"""执行流水线(可暂停)"""
|
||||
|
||||
def request_pause(self):
|
||||
"""请求暂停(由 UI 调用)"""
|
||||
```
|
||||
|
||||
### 3.4 ConfigPanel(配置面板)
|
||||
|
||||
```python
|
||||
class ConfigPanel(QWidget):
|
||||
"""配置面板"""
|
||||
|
||||
config_changed_signal = pyqtSignal(dict)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化 UI"""
|
||||
# API 配置组
|
||||
# - API Host (QLineEdit)
|
||||
# - API Key (QLineEdit, 密码模式)
|
||||
# - 模型选择 (QComboBox)
|
||||
|
||||
# 视频配置组
|
||||
# - 视频文件选择 (QLineEdit + QPushButton)
|
||||
# - 输出目录选择 (QLineEdit + QPushButton)
|
||||
|
||||
# Whisper 配置组
|
||||
# - 模型选择 (QComboBox: base/small/medium/large)
|
||||
# - 模型路径 (QLineEdit)
|
||||
|
||||
def load_config(self, config: dict):
|
||||
"""加载配置到 UI"""
|
||||
|
||||
def get_config(self) -> dict:
|
||||
"""从 UI 获取配置"""
|
||||
|
||||
def validate(self) -> tuple:
|
||||
"""验证配置有效性"""
|
||||
# 返回 (is_valid, error_message)
|
||||
```
|
||||
|
||||
### 3.5 ProgressView(进度视图)
|
||||
|
||||
```python
|
||||
class ProgressView(QWidget):
|
||||
"""进度监控视图"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化 UI"""
|
||||
# 当前步骤标签 (QLabel)
|
||||
# 整体进度条 (QProgressBar)
|
||||
# Clip 进度 (QLabel: "Clip 3/14")
|
||||
# 日志文本框 (QTextEdit, 只读)
|
||||
# 控制按钮 (开始/暂停/停止/继续)
|
||||
|
||||
def update_progress(self, step: str, percent: int, message: str):
|
||||
"""更新进度显示"""
|
||||
|
||||
def append_log(self, message: str):
|
||||
"""追加日志"""
|
||||
|
||||
def set_clip_progress(self, current: int, total: int):
|
||||
"""设置 Clip 进度"""
|
||||
|
||||
def enable_controls(self, can_start: bool, can_pause: bool, can_stop: bool, can_resume: bool):
|
||||
"""设置控制按钮状态"""
|
||||
```
|
||||
|
||||
### 3.6 ContentEditor(内容编辑器 - 人工介入点)
|
||||
|
||||
```python
|
||||
class ContentEditor(QWidget):
|
||||
"""内容编辑器 - 用于人工介入修改标题和字幕内容"""
|
||||
|
||||
content_confirmed_signal = pyqtSignal(dict) # 用户确认的内容 {clip_index: {title, subtitles}}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化 UI"""
|
||||
# 标签页:标题编辑 / 字幕编辑
|
||||
# 标题编辑:
|
||||
# - Clip # | 原始标题 | LLM建议 | 用户修改 | 操作
|
||||
# - 编辑按钮 (QPushButton)
|
||||
# 字幕编辑:
|
||||
# - 按Clip分页,每个Clip显示其字幕内容
|
||||
# - 每个字幕段可编辑(原始文本 → 纠正后文本 → 用户修改)
|
||||
# 确认按钮 (QPushButton)
|
||||
|
||||
def set_content(self, clips_data: dict):
|
||||
"""设置内容供用户编辑
|
||||
clips_data: {
|
||||
clip_index: {
|
||||
'title': {...},
|
||||
'subtitles': [...]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def get_user_content(self) -> dict:
|
||||
"""获取用户修改后的内容"""
|
||||
|
||||
def edit_clip_title(self, clip_index: int):
|
||||
"""编辑单个Clip的标题 - 弹出对话框"""
|
||||
|
||||
def edit_clip_subtitle(self, clip_index: int, subtitle_index: int):
|
||||
"""编辑单个字幕段 - 弹出对话框"""
|
||||
```
|
||||
|
||||
### 3.7 SubtitleSegmentEditor(字幕段编辑器)
|
||||
|
||||
```python
|
||||
class SubtitleSegmentEditor(QWidget):
|
||||
"""单个字幕片段的编辑器"""
|
||||
|
||||
def __init__(self, segment_data: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
# 显示:时间范围、原始文本、规则纠正后、LLM纠正后、用户可编辑
|
||||
|
||||
def get_corrected_text(self) -> str:
|
||||
"""获取用户修改后的文本"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 流水线状态机
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
┌─────────►│ Ready │◄────────┐
|
||||
│ └────┬────┘ │
|
||||
│ │ start() │ reset()
|
||||
│ ▼ │
|
||||
│ ┌─────────┐ │
|
||||
│ ┌─────│Extracting│─────┐ │
|
||||
│ │ └────┬────┘ │ │
|
||||
│ │ pause │ completed │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌───────────┐ │ │
|
||||
│ └──►│Transcribing│◄────┘ │
|
||||
│ └─────┬─────┘ │
|
||||
│ pause │ │ completed │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │Title Correcting │◄──┐ │ 人工介入点
|
||||
│ └────────┬────────┘ │ │ 用户可暂停
|
||||
│ │ completed │ │ 修改标题
|
||||
│ ▼ │ │
|
||||
│ ┌──────────────────┐ │ │
|
||||
│ │Generating Subtitles│───┘ │
|
||||
│ └─────────┬────────┘ │
|
||||
│ pause │ │ completed │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Merging │◄──┘ │
|
||||
│ └──────┬───────┘ │
|
||||
│ pause│ │ completed │
|
||||
│ ▼ │
|
||||
│ ┌───────────┐ │
|
||||
│ │ Burning │◄─────────────┘
|
||||
│ └─────┬─────┘
|
||||
│ pause│ │ completed
|
||||
│ ▼
|
||||
│ ┌───────────┐
|
||||
└────│ Completed │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 信号流设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ UI Layer │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ ConfigPanel │ │ ProgressView │ │ TitleEditor │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ config_changed │ titles_ready │ titles_confirmed │
|
||||
└─────────┼──────────────────┼──────────────────┼──────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Controller Layer │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ PipelineController │ │
|
||||
│ │ - manage_workflow() │ │
|
||||
│ │ - handle_pause() / handle_resume() │ │
|
||||
│ │ - collect_user_titles() │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Worker Thread │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Worker (QThread) │ │
|
||||
│ │ - runs pipeline steps │ │
|
||||
│ │ - emits progress/titles signals │ │
|
||||
│ │ - respects pause/stop flags │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 信号定义
|
||||
|
||||
| 信号 | 方向 | 参数 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `config_changed` | UI → Controller | dict | 配置变更 |
|
||||
| `start_pipeline` | Controller → Worker | dict, StateManager | 启动流水线 |
|
||||
| `progress_signal` | Worker → UI | step, percent, message | 进度更新 |
|
||||
| `titles_ready_signal` | Worker → UI | list | 标题列表准备好,等待确认 |
|
||||
| `titles_confirmed_signal` | UI → Controller | list | 用户确认的标题 |
|
||||
| `pause_pipeline` | UI → Worker | - | 请求暂停 |
|
||||
| `resume_pipeline` | UI → Worker | - | 请求恢复 |
|
||||
| `stop_pipeline` | UI → Worker | - | 请求停止 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 状态文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"app_version": "1.0.0",
|
||||
"project_name": "福田夜校-03月18日",
|
||||
"created_at": "2026-05-02T10:00:00",
|
||||
"updated_at": "2026-05-02T10:30:00",
|
||||
|
||||
"config": {
|
||||
"video_src": "D:/path/to/video.mp4",
|
||||
"output_dir": "D:/path/to/output",
|
||||
"api_key": "xxx",
|
||||
"api_host": "https://ark.cn-beijing.volces.com/api/coding/v3",
|
||||
"whisper_model": "large",
|
||||
"whisper_model_path": "D:/AI/LM-Models/faster-whisper/large-v3",
|
||||
"video_params": {
|
||||
"fade_duration": 1,
|
||||
"title_fontsize": 90,
|
||||
"title_color": "FFFF00",
|
||||
"subtitle_fontsize": 24,
|
||||
"subtitle_color": "FFFFFF"
|
||||
}
|
||||
},
|
||||
|
||||
"pipeline": {
|
||||
"current_step": 3,
|
||||
"steps": {
|
||||
"extracting": {"status": "completed", "started_at": "...", "completed_at": "..."},
|
||||
"transcribing": {"status": "completed", "started_at": "...", "completed_at": "..."},
|
||||
"title_correcting": {"status": "in_progress", "started_at": "...", "completed_at": null},
|
||||
"generating_subtitles": {"status": "pending", "started_at": null, "completed_at": null},
|
||||
"merging": {"status": "pending", "started_at": null, "completed_at": null},
|
||||
"burning": {"status": "pending", "started_at": null, "completed_at": null}
|
||||
}
|
||||
},
|
||||
|
||||
"clips": [
|
||||
{
|
||||
"index": 1,
|
||||
"title_original": "弹奏",
|
||||
"title_llm": "弹奏",
|
||||
"title_user": null,
|
||||
"title_final": "弹奏",
|
||||
"start": 412,
|
||||
"end": 442,
|
||||
"status": "completed",
|
||||
"clip_path": "intermediates/clip1_fade.mp4",
|
||||
"json_path": "intermediates/clip1.json",
|
||||
"transcription_completed_at": "..."
|
||||
}
|
||||
],
|
||||
|
||||
"outputs": {
|
||||
"subtitle_title_path": "subs/v1_title.srt",
|
||||
"subtitle_content_path": "subs/v1_content.srt",
|
||||
"merged_video_path": "concat_merged.mp4",
|
||||
"final_video_path": "v1_final.mp4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理策略
|
||||
|
||||
| 错误类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 配置无效 | 阻止开始,提示用户修正 |
|
||||
| API 调用失败 | 重试 3 次,仍失败则暂停流水线,等待用户处理 |
|
||||
| 视频文件不存在 | 暂停,提示用户选择其他文件 |
|
||||
| 磁盘空间不足 | 暂停,提示用户清理空间 |
|
||||
| 处理异常崩溃 | 状态已持久化,重启后可恢复 |
|
||||
| 用户取消 | 保存当前进度,清理临时文件(可选) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 打包配置 (Nuitka)
|
||||
|
||||
```python
|
||||
# nuitka_options.py
|
||||
import nuitka
|
||||
|
||||
nuitka.compile(
|
||||
script="src/main.py",
|
||||
mode="standalone",
|
||||
output_dir="dist",
|
||||
windows_icon="assets/icon.ico",
|
||||
include_qt_plugins=["qt_plugins/styles", "qt_plugins/imageformats"],
|
||||
data_files=[
|
||||
("assets/icons", "assets/icons"),
|
||||
],
|
||||
remove_output_dir=True,
|
||||
onefile=True, # 打包成单个 exe
|
||||
company_name="Piano Tools",
|
||||
product_name="Piano Highlight Generator",
|
||||
product_version="1.0.0",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 依赖清单
|
||||
|
||||
```
|
||||
PySide6>=6.6.0
|
||||
pyyaml>=6.0
|
||||
requests>=2.31.0
|
||||
pypinyin>=0.50.0
|
||||
faster-whisper>=1.0.0 # 可选,如需本地转录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发优先级
|
||||
|
||||
| 优先级 | 模块 | 工期估计 |
|
||||
|--------|------|----------|
|
||||
| P0 | 项目骨架 + ConfigPanel + StateManager | 2h |
|
||||
| P0 | ProgressView + Worker 集成 | 2h |
|
||||
| P0 | PipelineController 核心逻辑 | 2h |
|
||||
| P1 | TitleEditor 标题编辑器 | 1.5h |
|
||||
| P2 | 完善错误处理和边界情况 | 1h |
|
||||
| P2 | 打包配置 + 测试 | 1.5h |
|
||||
| P3 | README 和用户文档 | 0.5h |
|
||||
|
||||
**总工期估计:约 10 小时**
|
||||
Reference in New Issue
Block a user