From e33dfc3031e8f18190ea2cec29f68d067dc2869c Mon Sep 17 00:00:00 2001 From: Kevin Wong Date: Tue, 10 Feb 2026 13:31:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/BACKEND_DEV.md | 3 +- Docs/BACKEND_README.md | 28 +- Docs/DEPLOY_MANUAL.md | 3 + Docs/DevLogs/Day23.md | 546 ++++++++++++++++++ Docs/FRONTEND_DEV.md | 15 + Docs/FRONTEND_README.md | 20 +- Docs/QWEN3_TTS_DEPLOY.md | 11 +- Docs/SUBTITLE_DEPLOY.md | 13 +- Docs/task_complete.md | 48 +- README.md | 9 +- backend/app/main.py | 2 + .../app/modules/generated_audios/__init__.py | 0 .../app/modules/generated_audios/router.py | 77 +++ .../app/modules/generated_audios/schemas.py | 30 + .../app/modules/generated_audios/service.py | 263 +++++++++ backend/app/modules/videos/schemas.py | 9 + backend/app/modules/videos/workflow.py | 146 +++-- backend/app/services/storage.py | 3 +- backend/app/services/video_service.py | 54 +- backend/scripts/watchdog.py | 41 +- frontend/package-lock.json | 9 +- frontend/package.json | 3 +- frontend/src/app/layout.tsx | 1 - .../features/home/model/useGeneratedAudios.ts | 192 ++++++ .../features/home/model/useHomeController.ts | 136 ++++- .../features/home/model/useHomePersistence.ts | 16 + .../src/features/home/model/useMaterials.ts | 52 ++ .../features/home/model/useSavedScripts.ts | 51 ++ .../features/home/model/useTimelineEditor.ts | 246 ++++++++ frontend/src/features/home/ui/ClipTrimmer.tsx | 293 ++++++++++ .../features/home/ui/GeneratedAudiosPanel.tsx | 224 +++++++ frontend/src/features/home/ui/HomePage.tsx | 156 +++-- .../src/features/home/ui/MaterialSelector.tsx | 122 +--- .../src/features/home/ui/ScriptEditor.tsx | 112 +++- .../src/features/home/ui/TimelineEditor.tsx | 283 +++++++++ frontend/src/shared/types/material.ts | 1 + models/Qwen3-TTS/qwen_tts_server.py | 14 +- run_qwen_tts.sh | 6 +- 38 files changed, 2956 insertions(+), 282 deletions(-) create mode 100644 Docs/DevLogs/Day23.md create mode 100644 backend/app/modules/generated_audios/__init__.py create mode 100644 backend/app/modules/generated_audios/router.py create mode 100644 backend/app/modules/generated_audios/schemas.py create mode 100644 backend/app/modules/generated_audios/service.py create mode 100644 frontend/src/features/home/model/useGeneratedAudios.ts create mode 100644 frontend/src/features/home/model/useSavedScripts.ts create mode 100644 frontend/src/features/home/model/useTimelineEditor.ts create mode 100644 frontend/src/features/home/ui/ClipTrimmer.tsx create mode 100644 frontend/src/features/home/ui/GeneratedAudiosPanel.tsx create mode 100644 frontend/src/features/home/ui/TimelineEditor.tsx diff --git a/Docs/BACKEND_DEV.md b/Docs/BACKEND_DEV.md index a5fe4b1..dbfb446 100644 --- a/Docs/BACKEND_DEV.md +++ b/Docs/BACKEND_DEV.md @@ -33,9 +33,10 @@ backend/ │ │ ├── materials/ # 素材管理(router/schemas/service) │ │ ├── publish/ # 多平台发布 │ │ ├── auth/ # 认证与会话 -│ │ ├── ai/ # AI 功能(标题标签生成等) +│ │ ├── ai/ # AI 功能(标题标签生成、多语言翻译) │ │ ├── assets/ # 静态资源(字体/样式/BGM) │ │ ├── ref_audios/ # 声音克隆参考音频(router/schemas/service) +│ │ ├── generated_audios/ # 预生成配音管理(router/schemas/service) │ │ ├── login_helper/ # 扫码登录辅助 │ │ ├── tools/ # 工具接口(router/schemas/service) │ │ └── admin/ # 管理员功能 diff --git a/Docs/BACKEND_README.md b/Docs/BACKEND_README.md index 7c4b697..f5f43b0 100644 --- a/Docs/BACKEND_README.md +++ b/Docs/BACKEND_README.md @@ -19,12 +19,13 @@ backend/ │ │ ├── materials/ # 素材管理(router/schemas/service) │ │ ├── publish/ # 多平台发布 │ │ ├── auth/ # 认证与会话 -│ │ ├── ai/ # AI 功能(标题标签生成) -│ │ ├── assets/ # 静态资源(字体/样式/BGM) -│ │ ├── ref_audios/ # 声音克隆参考音频(router/schemas/service) -│ │ ├── login_helper/ # 扫码登录辅助 -│ │ ├── tools/ # 工具接口(router/schemas/service) -│ │ └── admin/ # 管理员功能 +│ │ ├── ai/ # AI 功能(标题标签生成、多语言翻译) +│ │ ├── assets/ # 静态资源(字体/样式/BGM) +│ │ ├── ref_audios/ # 声音克隆参考音频(router/schemas/service) +│ │ ├── generated_audios/ # 预生成配音管理(router/schemas/service) +│ │ ├── login_helper/ # 扫码登录辅助 +│ │ ├── tools/ # 工具接口(router/schemas/service) +│ │ └── admin/ # 管理员功能 │ ├── repositories/ # Supabase 数据访问 │ ├── services/ # 外部服务集成 (TTS/Remotion/Storage/Uploader 等) │ └── tests/ # 单元测试与集成测试 @@ -83,11 +84,19 @@ backend/ 7. **AI 功能 (AI)** * `POST /api/ai/generate-meta`: AI 生成标题和标签 + * `POST /api/ai/translate`: AI 多语言翻译(支持 9 种目标语言) -8. **工具 (Tools)** +8. **预生成配音 (Generated Audios)** + * `POST /api/generated-audios/generate`: 异步生成配音(返回 task_id) + * `GET /api/generated-audios/tasks/{task_id}`: 轮询生成进度 + * `GET /api/generated-audios`: 列出用户所有配音 + * `DELETE /api/generated-audios/{audio_id}`: 删除配音 + * `PUT /api/generated-audios/{audio_id}`: 重命名配音 + +9. **工具 (Tools)** * `POST /api/tools/extract-script`: 从视频链接提取文案 -9. **健康检查** +10. **健康检查** * `GET /api/lipsync/health`: LatentSync 服务健康状态 * `GET /api/voiceclone/health`: Qwen3-TTS 服务健康状态 @@ -113,6 +122,9 @@ backend/ - `tts_mode`: TTS 模式 (`edgetts` / `voiceclone`) - `voice`: EdgeTTS 音色 ID(edgetts 模式) - `ref_audio_id` / `ref_text`: 参考音频 ID 与文本(voiceclone 模式) +- `generated_audio_id`: 预生成配音 ID(存在时跳过内联 TTS,使用已生成的配音文件) +- `custom_assignments`: 自定义素材分配数组(每项含 `material_path` / `start` / `end` / `source_start`),存在时跳过 Whisper 均分 +- `language`: TTS 语言(默认自动检测,声音克隆时透传给 Qwen3-TTS) - `title`: 片头标题文字 - `subtitle_style_id`: 字幕样式 ID - `title_style_id`: 标题样式 ID diff --git a/Docs/DEPLOY_MANUAL.md b/Docs/DEPLOY_MANUAL.md index 11f0aa6..5ec3ab5 100644 --- a/Docs/DEPLOY_MANUAL.md +++ b/Docs/DEPLOY_MANUAL.md @@ -165,6 +165,8 @@ playwright install chromium CREATE POLICY "Allow public read" ON storage.objects FOR SELECT TO anon USING (bucket_id = 'materials' OR bucket_id = 'outputs'); EOF ``` + +> **注意**:后端启动时会自动创建额外的存储桶(`ref-audios`、`generated-audios`),无需手动创建。 --- @@ -570,6 +572,7 @@ pm2 logs vigent2-qwen-tts | `next` | React 框架 | | `swr` | 数据请求与缓存 | | `tailwindcss` | CSS 样式 | +| `wavesurfer.js` | 音频波形(时间轴编辑器) | ### LatentSync 关键依赖 diff --git a/Docs/DevLogs/Day23.md b/Docs/DevLogs/Day23.md new file mode 100644 index 0000000..bc8f409 --- /dev/null +++ b/Docs/DevLogs/Day23.md @@ -0,0 +1,546 @@ +## 🎙️ 配音前置重构 — 第一阶段 (Day 23) + +### 概述 + +将配音从视频生成流程中独立出来,实现"先生成配音 → 选中配音 → 再选素材 → 生成视频"的新工作流。用户可以独立管理配音(生成/试听/改名/删除/选择),并在选中配音后看到时长信息,为第二阶段的素材时间轴编排奠定数据基础。 + +**旧流程**: 文案 + 选素材 → 一键生成(内联 TTS → Whisper → 均分 → LipSync → 合成) +**新流程**: 文案 → 配音方式 → **生成配音** → 选中配音 → 选素材 → 背景音乐 → 生成视频 + +--- + +### 一、后端:新增 `generated_audios` 模块 + +#### 模块结构 + +``` +backend/app/modules/generated_audios/ +├── __init__.py +├── router.py # 5 个 API 端点 +├── schemas.py # 请求/响应模型 +└── service.py # 生成/列表/删除/改名 +``` + +#### API 端点 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/generated-audios/generate` | 异步生成配音(返回 task_id) | +| GET | `/api/generated-audios/tasks/{task_id}` | 轮询生成进度 | +| GET | `/api/generated-audios` | 列出用户所有配音 | +| DELETE | `/api/generated-audios/{audio_id}` | 删除配音 | +| PUT | `/api/generated-audios/{audio_id}` | 改名 | + +#### 存储方案 + +- Supabase 存储桶:`generated-audios`(启动时自动创建) +- 音频文件:`{user_id}/{timestamp}_audio.wav` +- 元数据文件:`{user_id}/{timestamp}_audio.json`(含 display_name、text、tts_mode、duration_sec 等) + +#### 生成流程 + +复用现有 `TTSService` / `voice_clone_service` / `task_store`: + +``` +POST /generate → 创建 task → BackgroundTask: + 1. edgetts → TTSService.generate_audio() + voiceclone → 下载 ref_audio → voice_clone_service.generate_audio() + 2. ffprobe 获取时长 + 3. 上传 .wav + .json 到 generated-audios 桶 + 4. 更新 task(status=completed, output={audio_id, duration_sec, ...}) +``` + +--- + +### 二、后端:修改视频生成 workflow + +#### `GenerateRequest` 新增字段 + +```python +generated_audio_id: Optional[str] = None # 预生成配音 ID(存在时跳过内联 TTS) +``` + +#### `workflow.py` TTS 阶段新增分支 + +```python +if req.generated_audio_id: + # 下载预生成配音 + 从元数据读取 language +elif req.tts_mode == "voiceclone": + # 原有声音克隆逻辑 +else: + # 原有 EdgeTTS 逻辑 +``` + +向后兼容:不传 `generated_audio_id` 时,原有内联 TTS 流程不受影响。 + +--- + +### 三、前端:新增配音列表 hook + 面板 + +#### `useGeneratedAudios.ts` + +- 状态:`generatedAudios[]`、`selectedAudio`、`isGeneratingAudio`、`audioTask` +- 方法:`fetchGeneratedAudios()`、`generateAudio()`、`deleteAudio()`、`renameAudio()`、`selectAudio()` +- 轮询:生成后 1s 轮询 task 状态,完成后自动刷新列表并选中最新配音 +- 独立于视频生成的 TaskContext(不互相干扰) + +#### `GeneratedAudiosPanel.tsx` + +- 每条配音:播放/暂停、名称、时长、重命名、删除 +- 选中态:`border-purple-500 bg-purple-500/20` +- 内嵌进度条(生成中显示) +- 底部显示选中配音的原始文案(截断) +- 播放逻辑自包含于面板内(`new Audio()` + play/pause toggle) + +--- + +### 四、前端:UI 面板重排序 + +**旧顺序**: MaterialSelector → ScriptEditor → TitleSubtitle → VoiceSelector → BgmPanel → GenerateActionBar + +**新顺序**: +1. ScriptEditor(文案编辑) +2. TitleSubtitlePanel(标题与字幕样式) +3. VoiceSelector(配音方式) +4. **GeneratedAudiosPanel**(配音列表)← 新增 +5. MaterialSelector(视频素材)← 后移,需选中配音才解锁 +6. BgmPanel(背景音乐) +7. GenerateActionBar(生成视频) + +#### 素材区门控 + +未选中配音时,素材区显示半透明遮罩 + "请先生成并选中配音"提示。素材上传/预览/改名/删除始终可用,仅选择勾选被遮罩。 + +#### 时长信息 + +选中配音后,MaterialSelector 顶部显示: +``` +当前配音: 45.2 秒 | 已选 3 个素材(自动均分每段 ~15.1 秒) +``` + +#### 生成按钮条件更新 + +```typescript +// 旧条件 +disabled={isGenerating || selectedMaterials.length === 0 || (ttsMode === "voiceclone" && !selectedRefAudio)} +// 新条件 +disabled={isGenerating || selectedMaterials.length === 0 || !selectedAudio} +``` + +--- + +### 五、持久化 + +`useHomePersistence` 新增 `selectedAudioId` 的 localStorage 读写,刷新页面后恢复选中的配音。 + +--- + +### 涉及文件汇总 + +#### 后端新增 + +| 文件 | 说明 | +|------|------| +| `backend/app/modules/generated_audios/__init__.py` | 模块标记 | +| `backend/app/modules/generated_audios/router.py` | 5 个 API 端点 | +| `backend/app/modules/generated_audios/service.py` | 生成/列表/删除/改名 | +| `backend/app/modules/generated_audios/schemas.py` | 请求/响应模型 | + +#### 后端修改 + +| 文件 | 变更 | +|------|------| +| `backend/app/main.py` | 注册 generated_audios 路由 | +| `backend/app/services/storage.py` | 新增 `BUCKET_GENERATED_AUDIOS`,启动时自动创建桶 | +| `backend/app/modules/videos/schemas.py` | `GenerateRequest` 新增 `generated_audio_id` 字段 | +| `backend/app/modules/videos/workflow.py` | TTS 阶段新增预生成音频分支 | + +#### 前端新增 + +| 文件 | 说明 | +|------|------| +| `frontend/src/features/home/model/useGeneratedAudios.ts` | 配音列表 hook | +| `frontend/src/features/home/ui/GeneratedAudiosPanel.tsx` | 配音列表面板 | + +#### 前端修改 + +| 文件 | 变更 | +|------|------| +| `frontend/src/features/home/ui/HomePage.tsx` | 面板重排序 + 素材区门控 + 插入 GeneratedAudiosPanel | +| `frontend/src/features/home/ui/MaterialSelector.tsx` | 新增 `selectedAudioDuration` prop + 时长信息显示 | +| `frontend/src/features/home/ui/GenerateActionBar.tsx` | 禁用条件改为 `!selectedAudio` | +| `frontend/src/features/home/model/useHomeController.ts` | 集成 useGeneratedAudios、新增 handleGenerateAudio、修改 handleGenerate 使用 generated_audio_id | +| `frontend/src/features/home/model/useHomePersistence.ts` | 新增 selectedAudioId 持久化 | + +--- + +## 🎞️ 素材时间轴编排 — 第二阶段 (Day 23) + +### 概述 + +在第一阶段"配音前置"基础上,新增**时间轴编辑器**,用户可以: +1. 在音频波形上查看各素材块的时长分配 +2. 拖拽分割线调整每段素材的时长(无缝铺满,调整一段自动压缩/扩展相邻段) +3. 为每段素材设置**源视频截取起点**(从视频任意位置开始,而非始终从头) + +**旧行为**: 多素材时自动均分(`_split_equal`),无法控制每段时长和源视频起始点 +**新行为**: 时间轴编辑器可视化分配 + 拖拽调整 + ClipTrimmer 截取设置 + +--- + +### 一、后端改动 + +#### 1.1 新增 `CustomAssignment` 模型 + +```python +# backend/app/modules/videos/schemas.py +class CustomAssignment(BaseModel): + material_path: str + start: float # 音频时间轴起点 + end: float # 音频时间轴终点 + source_start: float = 0.0 # 源视频截取起点 +``` + +`GenerateRequest` 新增 `custom_assignments: Optional[List[CustomAssignment]] = None`。存在时跳过 Whisper 均分,直接使用用户定义的分配。 + +#### 1.2 `prepare_segment` 支持 `source_start` + +```python +def prepare_segment(self, video_path, target_duration, output_path, + target_resolution=None, source_start: float = 0.0): +``` + +关键逻辑: +- `source_start > 0` 时使用 `-ss` 快速 seek,并强制重编码(避免 stream copy 关键帧不精确) +- 当需要循环且有 `source_start` 时,先裁剪出 `source_start` 到视频结尾的片段,再循环裁剪后的文件(避免 `stream_loop` 从视频 0s 开始循环) +- 裁剪临时文件在 `finally` 中自动清理 + +#### 1.3 `workflow.py` 支持 `custom_assignments` + +- **多素材模式**: `custom_assignments` 存在时,直接使用用户分配(仍运行 Whisper 生成字幕),每个 `prepare_segment` 调用传入 `source_start` +- **单素材模式**: `custom_assignments` 有 1 条且 `source_start > 0` 时,先截取片段再传入 LatentSync +- **向后兼容**: `custom_assignments` 为 `None` 时完全走旧路径 + +--- + +### 二、前端新增组件 + +#### 2.1 `useTimelineEditor.ts` — 时间轴段管理 hook + +```typescript +interface TimelineSegment { + id: string; // React key + materialId: string; // 素材 ID + materialName: string; // 显示名 + start: number; // 音频时间轴开始秒数 + end: number; // 音频时间轴结束秒数 + sourceStart: number; // 源视频截取起点(默认 0) + sourceEnd: number; // 源视频截取终点(0 = 到结尾) + color: string; // 色块颜色 +} +``` + +核心方法: +- `initSegments()`: selectedMaterials 变化时按数量均分 audioDuration +- `resizeSegment(id, newEnd)`: 拖拽右边界,约束每段最小 1s +- `setSourceRange(id, sourceStart, sourceEnd)`: 设置截取范围 +- `toCustomAssignments()`: 转为后端 `CustomAssignment[]` 格式 + +#### 2.2 `TimelineEditor.tsx` — 波形 + 色块时间轴 + +- **wavesurfer.js** 渲染音频波形(仅展示,不播放) +- 色块层按比例排列,显示素材名 + 时长 + 截取标记 +- 色块间分割线可拖拽(`onPointerDown/Move/Up` 实现连续像素拖拽) +- 点击色块打开 ClipTrimmer + +#### 2.3 `ClipTrimmer.tsx` — 素材截取模态框 + +- HTML5 `