522 lines
18 KiB
Markdown
522 lines
18 KiB
Markdown
# 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 小时**
|