## CosyVoice FP16 加速 + 文档更新 + AI改写界面重构 + 标题字幕面板重排与视频帧预览 (Day 28)

### 概述

CosyVoice 3.0 声音克隆服务开启 FP16 半精度推理，预估提速 30-40%。同步更新 4 个项目文档。重构 AI 改写文案界面（RewriteModal 两步流程 + ScriptExtractionModal 逻辑抽取）。前端将"标题与字幕"面板从第二步移至第四步（素材编辑之后），样式预览窗口背景从紫粉渐变改为视频片头帧截图，实现所见即所得。

---

## ✅ 改动内容

### 1. CosyVoice FP16 半精度加速

- **问题**: CosyVoice 3.0 以 FP32 全精度运行，RTF (Real-Time Factor) 约 0.9-1.35x，生成 2 分钟音频需要约 2 分钟
- **根因**: `AutoModel()` 初始化时未传入 `fp16=True`，LLM 推理和 Flow Matching (DiT) 均在 FP32 下运行
- **修复**: 一行改动开启 FP16 自动混合精度

```python
# 旧: _model = AutoModel(model_dir=str(MODEL_DIR))
# 新:
_model = AutoModel(model_dir=str(MODEL_DIR), fp16=True)
```

- **生效机制**: `CosyVoice3Model` 在 `llm_job()` 和 `token2wav()` 中通过 `torch.cuda.amp.autocast(self.fp16)` 自动将计算转为 FP16
- **预期效果**:
  - 推理速度提升 30-40%
  - 显存占用降低 ~30%
  - 语音质量基本无损（0.5B 模型 FP16 精度充足）
- **验证**: 服务重启后自检通过，健康检查 `ready: true`

### 2. 文档全面更新 (4 个文件)

补充 Day 27 新增的 MuseTalk 混合唇形同步方案、性能优化、Remotion 并发渲染等内容到所有相关文档。

#### README.md
- 项目描述更新为 "LatentSync 1.6 + MuseTalk 1.5 混合唇形同步"
- 唇形同步功能描述改为混合方案（短视频 LatentSync，长视频 MuseTalk）
- 技术栈表新增 MuseTalk 1.5
- 项目结构新增 `models/MuseTalk/`
- 服务架构表新增 MuseTalk (端口 8011)
- 文档中心新增 MuseTalk 部署指南链接
- 性能优化描述新增降频检测 + Remotion 16 并发

#### DEPLOY_MANUAL.md
- GPU 分配说明更新 (GPU0=MuseTalk+CosyVoice, GPU1=LatentSync)
- 步骤 3 拆分为 3a (LatentSync) + 3b (MuseTalk)
- 环境变量表新增 7 个 MuseTalk 变量，移除过时的 `DOUYIN_COOKIE`
- LatentSync 推理步数默认值 20→16
- 测试运行新增 MuseTalk 启动终端
- PM2 管理新增 MuseTalk 服务（第 5 项）
- 端口检查、日志查看命令新增 8011/vigent2-musetalk

#### SUBTITLE_DEPLOY.md
- 技术架构图更新为 LatentSync/MuseTalk 混合路由
- 新增唇形同步路由说明
- Remotion 配置表新增 `concurrency` 参数 (默认 16)
- GPU 分配说明更新
- 更新日志新增 v1.3.0 条目

#### BACKEND_README.md
- 健康检查接口描述更新为含 LatentSync + MuseTalk + 混合路由阈值
- 环境变量配置新增 MuseTalk 相关变量
- 服务集成指南新增"唇形同步混合路由"章节

---

### 3. AI 改写文案界面重构

#### RewriteModal 重构

将 AI 改写弹窗改为两步式流程，提升交互体验：

**第一步 — 配置与触发**：
- 自定义提示词输入（可选），自动持久化到 localStorage
- "开始改写"按钮触发 `/api/ai/rewrite` 请求

**第二步 — 结果对比与选择**：
- 上方：AI 改写结果 + "使用此结果"按钮（紫粉渐变色，醒目）
- 下方：原文对比 + "保留原文"按钮（灰色低调）
- 底部：可"重新改写"（重回第一步，保留自定义提示词）
- ESC 快捷键关闭

#### ScriptExtractionModal 逻辑抽取

将文案提取模态框的全部业务逻辑抽取到独立 hook `useScriptExtraction`：

- **useScriptExtraction.ts** (新建): 管理 URL/文件双模式输入、拖拽上传、提取请求、步骤状态机 (config → processing → result)、剪贴板复制
- **ScriptExtractionModal.tsx**: 纯展示组件，消费 hook 返回值，新增 ESC/Enter 快捷键

#### ScriptEditor 工具栏调整

- 按钮组右对齐 (`justify-end`)，统一高度 `h-7` 和圆角
- "历史文案"按钮用灰色 (bg-gray-600) 区分辅助功能
- "文案提取助手"用紫色 (bg-purple-600) 表示主功能
- "AI多语言"用绿渐变 (emerald-teal)，"AI生成标题标签"用蓝渐变 (blue-cyan)
- "AI智能改写"和"保存文案"移至文本框下方状态栏

---

### 4. 标题字幕面板重排 + 视频帧背景预览

#### 面板顺序重排

将 `<TitleSubtitlePanel>` 从第二步移至第四步（素材编辑之后），使用户在设置标题字幕样式时已经完成了素材选择和时间轴编排。

新顺序：
```
一、文案提取与编辑（不变）
二、配音（原三）
三、素材编辑（原四）
四、标题与字幕（原二）→ 移到素材编辑之后
```

#### 新建 useVideoFrameCapture hook

从视频 URL 截取 0.1s 处帧画面，返回 JPEG data URL：

- 创建 `<video>` 元素，设置 `crossOrigin="anonymous"`（素材存储在 Supabase Storage 跨域地址）
- 先绑定 `loadedmetadata` / `canplay` / `seeked` / `error` 事件监听，再设 src（避免事件丢失）
- `loadedmetadata` 或 `canplay` 触发后 seek 到 0.1s，`seeked` 回调中用 canvas `drawImage` 截帧
- canvas 缩放到 480px 宽再编码（预览窗口最大 280px，节省内存）
- `canvas.toDataURL("image/jpeg", 0.7)` 导出
- 防御 `videoWidth/videoHeight` 为 0 的边界情况
- try-catch 防 canvas taint，失败返回 null（降级渐变）
- `isActive` 标志 + `seeked` 去重标志防止 stale 和重复更新
- 截图完成后清理 video 元素释放内存

#### 按需截取（性能优化）

只在样式预览窗口打开时才触发截取：

```typescript
const materialPosterUrl = useVideoFrameCapture(
  showStylePreview ? firstTimelineMaterialUrl : null
);
```

截取源优先使用**时间轴第一段素材**（用户拖拽排序后的真实片头），回退到 `selectedMaterials[0]`（未生成配音、时间轴为空时）。

#### 预览背景替换

`FloatingStylePreview` 有视频帧时直接显示原始画面（不加半透明，保证颜色真实），文字靠描边保证可读性；无视频帧时降级为原紫粉渐变背景。

#### 踩坑记录

1. **CORS tainted canvas**: 素材文件存储在 Supabase Storage (`api.hbyrkj.top`)，是跨域签名链接。必须设 `video.crossOrigin = "anonymous"` 才能让 canvas `toDataURL` 不被 SecurityError 拦截
2. **时间轴为空**: `useTimelineEditor` 在 `audioDuration <= 0`（未选配音）时返回空数组，需回退到 `selectedMaterials[0]`
3. **事件监听顺序**: 必须先绑定事件监听再设 `video.src`，否则快速加载时事件可能丢失

---

## 📁 修改文件清单

| 文件 | 改动 |
|------|------|
| `models/CosyVoice/cosyvoice_server.py` | `AutoModel()` 新增 `fp16=True` 参数 |
| `README.md` | 混合唇形同步描述、技术栈、服务架构、项目结构更新 |
| `Docs/DEPLOY_MANUAL.md` | MuseTalk 部署步骤、环境变量、PM2 管理、端口检查 |
| `Docs/SUBTITLE_DEPLOY.md` | 架构图、Remotion concurrency、GPU 分配、更新日志 |
| `Docs/BACKEND_README.md` | 健康检查、环境变量、混合路由章节 |
| `frontend/.../RewriteModal.tsx` | 两步式改写流程（自定义提示词 → 结果对比） |
| `frontend/.../script-extraction/useScriptExtraction.ts` | **新建** — 文案提取逻辑 hook |
| `frontend/.../ScriptExtractionModal.tsx` | 纯展示组件，消费 hook，新增快捷键 |
| `frontend/.../ScriptEditor.tsx` | 工具栏右对齐 + 按钮分色 + 改写/保存移至底部 |
| `frontend/.../useVideoFrameCapture.ts` | **新建** — 视频帧截取 hook，crossOrigin + canvas 缩放 |
| `frontend/.../useHomeController.ts` | 新增 useMemo 计算素材 URL，调用帧截取 hook，showStylePreview 门控 |
| `frontend/.../HomePage.tsx` | 面板重排（二↔四互换），编号更新，透传 materialPosterUrl |
| `frontend/.../TitleSubtitlePanel.tsx` | 编号"二"→"四"，新增 previewBackgroundUrl prop |
| `frontend/.../FloatingStylePreview.tsx` | 新增 previewBackgroundUrl prop，条件渲染视频帧/渐变背景 |

---

## 🔍 验证

- CosyVoice 重启成功，健康检查 `{"ready": true}`
- 自检推理通过（7.2s for "你好"）
- FP16 通过 `torch.cuda.amp.autocast(self.fp16)` 在 LLM 和 Flow Matching 阶段生效
- `npx tsc --noEmit` — 零错误
- AI 改写：自定义提示词持久化 → 改写结果 + 原文对比 → "使用此结果"/"保留原文"
- 文案提取：URL / 文件双模式 → 处理中动画 → 结果填入
- 面板顺序：一→文案、二→配音、三→素材编辑、四→标题与字幕
- 样式预览背景：有素材时显示真实视频片头帧，无素材降级紫粉渐变
- 预览关闭时不触发截取，不浪费资源

---

## 💡 CosyVoice 性能分析备注

### 当前性能基线 (FP32, 优化前)

| 文本长度 | 音频时长 | 推理耗时 | RTF |
|----------|----------|----------|-----|
| 42 字 | 9.8s | 13.2s | 1.35x |
| 89 字 | 18.2s | 20.3s | 1.12x |
| ~530 字 | 115.8s | 107.7s | 0.93x |
| ~670 字 | 143.5s | 131.6s | 0.92x |

### 未来可选优化（收益递减，暂不实施）

| 优化项 | 预期提升 | 复杂度 |
|--------|----------|--------|
| TensorRT (DiT 模块) | +20-30% | 需编译 .plan 引擎 |
| torch.compile() | +10-20% | 一行代码，但首次编译慢 |
| vLLM (LLM 模块) | +10-15% | 额外依赖 |

---

## MuseTalk 合成阶段性能优化

### 概述

MuseTalk v2 优化后总耗时从 1799s 降到 819s（2.2x），但合成阶段（Phase 6）仍占 462.2s (56.4%)，是最大单一瓶颈。本次优化两个方向：纯 numpy blending 替代 PIL 转换、FFmpeg pipe + NVENC GPU 硬编码替代双重编码。

### 1. 纯 numpy blending 替代 PIL（blending.py）

- **问题**: `get_image_blending` 每帧做 3 次 numpy↔PIL 转换 + BGR↔RGB 通道翻转，纯粹浪费
- **方案**: 新增 `get_image_blending_fast()` 函数
  - 全程保持 BGR numpy 数组，不做 PIL 转换和通道翻转
  - mask 混合用 numpy 向量化广播 `mask * (1/255)` 替代 `PIL.paste with mask`
  - 原 `get_image_blending` 保留作为 fallback
- **降级链**: `blending_fast` → `blending`（PIL）→ `get_image`（完整重算）

### 2. FFmpeg pipe + NVENC 硬编码替代双重编码（server.py）

**优化前（双重编码）**:
```
Phase 6: 逐帧 → cv2.VideoWriter (mp4v CPU 软编码) → temp_raw.mp4
Phase 7: FFmpeg 读 temp_raw.mp4 → H.264 CPU 重编码 + 合并音频 → output.mp4
```

**优化后（单次 GPU 编码）**:
```
Phase 6: 逐帧 → FFmpeg stdin pipe (rawvideo → h264_nvenc GPU 编码) → temp_raw.mp4
Phase 7: FFmpeg 只做音频合并 (-c:v copy -c:a copy) → output.mp4  （秒级）
```

- NVENC 参数: `-c:v h264_nvenc -preset p4 -cq 20 -pix_fmt yuv420p`
- RTX 3090 NVENC 专用芯片编码，不占 CUDA 核心，编码速度 >500fps

### 3. FFmpeg 进程资源管理加固

- `try/finally` 包裹写帧循环，确保异常时 `proc.stdin.close()` 执行
- `proc.wait()` 后读 stderr 再关闭，避免缓冲区死锁
- stderr decode 加 `errors="ignore"` 防止非 UTF-8 崩溃

### 4. `run_ffmpeg` 安全改进

- 去掉 `shell=True`，改用列表传参，避免路径特殊字符导致命令注入
- Phase 7 FFmpeg 命令从字符串拼接改为列表传参

### 调优过程

| 版本 | Phase 6 | Phase 7 | 总计 | 结论 |
|------|---------|---------|------|------|
| Day27 基线 | 462s | 38s | 819s | — |
| v1: libx264 -preset medium | 548s | 0.3s | 854s | CPU 编码背压，反而更慢 |
| v2: h264_nvenc（当前） | 待测 | 待测 | 待测 | NVENC 零背压，预估 Phase 6 < 200s |

### 修改文件

| 文件 | 改动 |
|------|------|
| `models/MuseTalk/musetalk/utils/blending.py` | 新增 `get_image_blending_fast()` 纯 numpy 函数 |
| `models/MuseTalk/scripts/server.py` | Phase 6: FFmpeg pipe + NVENC + blending_fast；Phase 7: -c:v copy；`run_ffmpeg` 去掉 shell=True |
