← [返回主页](../README.md) # 架构与原理 本文档覆盖核心设计原理:断句算法、模型不共驻显存策略、GPU 利用率优化、缓存清理机制。 --- ## 断句与时间戳重算原理 Whisper 原始 segment 的断句通常很混乱:每段不是完整句子,时间戳也不对齐句界。 `segmenter.py` 基于词级时间戳重组,两路策略: ### 精确路(`word_timestamps=true`,默认) 1. 汇集所有词的 `(text, start, end)`。 2. 按**句末标点**(`. ! ? ;`)切句。 3. 超长句(> `max_words_per_line` 或 > `max_duration_seconds`)按**逗号**(`, : —`)再拆; 无逗号则按词数等分。 4. 每条字幕的时间戳:`start = 首词.start`,`end = 末词.end`,**精确无误**。 ### 匀速估算路(无词级时间戳时 fallback) 段内按字符数比例分配时间 —— 即「短时匀速」假设,零模型开销: ``` 句start = 段start + (前缀字符数 / 段总字符数) × 段时长 ``` ### SRT 规范化 最后统一处理:单条 1–7 秒(过短合并)、≤2 行、每行 ≤42 字符(按词折行)。 --- ## 模型不共驻(显存策略) ASR 与翻译模型**不会同时驻留 GPU**。`model_manager.py` 单例跟踪当前加载的模型类型: - `get_translator()`:若 ASR 在内存 → 先 `del WhisperModel` + `gc.collect()` + `torch.cuda.empty_cache()` 释放显存 → 再加载 NLLB。 - `get_asr()`:若翻译器在内存 → 先卸载 → 再加载 Whisper。 翻译阶段独占显存,因此可用大 `batch_size`。24G 3090 上:Whisper large-v3-turbo FP16 ~3GB / NLLB-1.3B FP16 ~2.5GB,互不叠加,远低于显存上限。 --- ## GPU 利用率优化 faster-whisper 的 GPU 利用率曲线常呈**尖刺波**(峰=批量解码满载,谷=CPU 提取 Mel 特征 + 处理结果时 GPU 空闲),平均利用率偏低。瓶颈不在 GPU 算力,而在 CPU 特征提取与 GPU 解码未重叠: ``` CPU: [VAD+切片+Mel特征 N个chunk] ──► [处理结果] ──► [VAD+切片+Mel特征] ──► ... GPU: (空闲) [批量解码] (空闲) [批量解码] ``` `BatchedInferencePipeline` 内部把音频按 30s chunk 切分,凑够 `batch_size` 个 chunk 一次性 送 GPU 解码。每批解码完后回到 CPU 处理结果 + 提取下一批 Mel 特征,这期间 GPU 空闲。 ### 已做的优化(GPU 配置) | 参数 | 旧值 | 新值 | 作用 | |---|---|---|---| | `asr.batch_size` | 16 | **32** | 单次 GPU 解码时长翻倍,CPU 特征提取间隙占比减半 → 尖刺变宽、谷底变浅,平均利用率上升。turbo FP16 仅 ~1.6GB,3090 24G 充裕 | | `asr.beam_size` | 5 | **2** | 解码候选数 5→2,每步计算量与解码步数下降 → 峰更密、间隙更短。turbo 鲁棒,保留 1 个候选做歧义发音保险,质量损失小 | ### 为什么不关 `word_timestamps` `segmenter.py` 强依赖词级时间戳做精确断句——只要任一 segment 没词级时间戳,就整体退化 到匀速估算路(时间戳按字符数比例估算),字幕精度下降明显。所以 `word_timestamps=true` 必须保留,即使它是 CPU↔GPU 同步开销的来源之一。 ### 验证方法 ```bash # 1. 确认配置生效 curl -s http://127.0.0.1:8001/health | python -m json.tool # 应见 asr_batch_size=32, asr_beam_size=2 # 2. 跑长视频(如 test/1-5.mp4),观察 GPU 利用率曲线 nvidia-smi dmon -s u # 实时 GPU 利用率(d=dec u=util) # 3. 对比字幕质量(可选):同一视频改前改后 SRT diff ``` 优化后尖刺应比之前密且谷底变浅,平均利用率上升。`beam_size=2` 对 turbo 模型质量损失 极小,但仍建议用同一视频 A/B 对比字幕确认无歧义发音处的降级。 --- ## 缓存清理与定时任务 每个任务落盘的产物(字幕、中间音频、保留的原始视频)会持续占用磁盘。容器内置定时 清理(`app/services/cache_cleaner.py`),无需外部 cron: ### 清理什么 | 产物 | 路径 | 何时产生 | |---|---|---| | 字幕输出 | `/task_/` | 任务完成 | | 中间音频 | `/task_.wav` | `keep_audio=true` 且管线未删时残留 | | 保留的原始视频 | `/yyyy/mm/.` | `delete_original_after_extract=false` 时 | | 孤儿目录 | 上述目录中无对应 Task 的残留 | 进程崩溃 / 异常退出留下 | ### 清理策略 1. **超期任务**:`Task.created_at` 早于 `now - cache_retention_days`(默认 7 天)的任务, 删除其全部产物,并删除对应的 `Task` 与 `UploadSession` 行——避免历史页出现指向已删 文件的死链接。 2. **孤儿扫描**:`output_dir` / `work_dir` 下名为 `task_` 但 DB 中已无该 Task 的目录 (崩溃残留),按目录 `mtime` 判超期后删除。 3. **DB 一致性**:删任务时先删关联的 `UploadSession`(FK),再删 `Task`,保持引用完整。 ### 触发时机 - **启动时跑一次**:容器启动 lifespan 中立即执行(`purge_expired_cache`),清掉停机期间 超期的产物。 - **后台定时循环**:守护线程 `cache-cleaner` 按 `cache_cleanup_interval_hours`(默认 24h) 循环执行,随进程退出而终止。 - **手动触发**(调试用):进容器跑 `python -m app.services.cache_cleaner`,打印清理统计 JSON。 ### 相关配置(`storage` 段) | 字段 | 默认 | 说明 | |---|---|---| | `cache_retention_days` | `7` | 保留天数。`0` = 禁用清理(产物永久保留) | | `cache_cleanup_interval_hours` | `24` | 定时循环间隔(小时) | ### 与上传会话 reaper 的区别 | 机制 | 清理对象 | 判定 | 触发 | |---|---|---|---| | **reaper**(`reaper.py`) | 被放弃的**分片上传会话**(未 complete 的) | `status=pending` 且 `updated_at` 超 `chunk_session_ttl_seconds`(300s) | 仅启动时一次 | | **cache_cleaner**(本节) | 已完成/失败**任务的产物** + 崩溃孤儿 | `created_at` 超 `cache_retention_days`(7d)/ 孤儿 mtime 超期 | 启动一次 + 定时循环 | > 后台清理线程与请求线程并发写同一 SQLite 库,`database.py` 已设 `busy_timeout=30s`, > 拿锁时阻塞等待而非立即报 `database is locked`。单 worker 部署下无并发写入压力。