原 883 行单体 README 信息密度过高且重复(配置差异表出现 2 次、缓存说明 散落多处)。按主题拆分: 主页 README.md (150行): - 一句话简介 + 功能特性(精简) + 架构(目录树+数据流) + 快速开始 - 文档索引表(链接到 6 个子文档,每行一句话说明) - 入口地址表 + 依赖(精简) docs/ 子文档(原样搬运,不重写): - DEPLOYMENT.md (170行) CPU/GPU 部署、模型选型、CPU↔GPU 切换 - CONFIG.md (187行) 配置差异表、完整字段表、配置示例 - DOCKER.md (185行) 构建/重建/缓存分层/until根因/Volume - API.md (70行) HTTP接口表、分片上传协议、示例 - ARCHITECTURE.md(140行) 断句算法、显存策略、GPU优化、缓存清理 - FAQ.md (42行) 6 条常见问题 每个子文档顶部加「← 返回主页」链接,相关处加交叉引用 (如 DEPLOYMENT 提到缓存时链接 DOCKER.md)。无内容丢失。
6.2 KiB
← 返回主页
架构与原理
本文档覆盖核心设计原理:断句算法、模型不共驻显存策略、GPU 利用率优化、缓存清理机制。
断句与时间戳重算原理
Whisper 原始 segment 的断句通常很混乱:每段不是完整句子,时间戳也不对齐句界。
segmenter.py 基于词级时间戳重组,两路策略:
精确路(word_timestamps=true,默认)
- 汇集所有词的
(text, start, end)。 - 按句末标点(
. ! ? ;)切句。 - 超长句(>
max_words_per_line或 >max_duration_seconds)按逗号(, : —)再拆; 无逗号则按词数等分。 - 每条字幕的时间戳:
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 同步开销的来源之一。
验证方法
# 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:
清理什么
| 产物 | 路径 | 何时产生 |
|---|---|---|
| 字幕输出 | <output_dir>/task_<id>/ |
任务完成 |
| 中间音频 | <work_dir>/task_<id>.wav |
keep_audio=true 且管线未删时残留 |
| 保留的原始视频 | <upload_dir>/yyyy/mm/<uuid>.<ext> |
delete_original_after_extract=false 时 |
| 孤儿目录 | 上述目录中无对应 Task 的残留 | 进程崩溃 / 异常退出留下 |
清理策略
- 超期任务:
Task.created_at早于now - cache_retention_days(默认 7 天)的任务, 删除其全部产物,并删除对应的Task与UploadSession行——避免历史页出现指向已删 文件的死链接。 - 孤儿扫描:
output_dir/work_dir下名为task_<id>但 DB 中已无该 Task 的目录 (崩溃残留),按目录mtime判超期后删除。 - 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 部署下无并发写入压力。