Files
audio2text/docs/ARCHITECTURE.md
audio2text dev 5f6a242114 docs: README 分层重构 — 主页精简到 150 行 + 6 个子文档
原 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)。无内容丢失。
2026-07-06 22:59:05 +08:00

6.2 KiB
Raw Permalink Blame History

返回主页

架构与原理

本文档覆盖核心设计原理断句算法、模型不共驻显存策略、GPU 利用率优化、缓存清理机制。


断句与时间戳重算原理

Whisper 原始 segment 的断句通常很混乱:每段不是完整句子,时间戳也不对齐句界。 segmenter.py 基于词级时间戳重组,两路策略:

精确路(word_timestamps=true,默认)

  1. 汇集所有词的 (text, start, end)
  2. 句末标点. ! ? ;)切句。
  3. 超长句(> max_words_per_line 或 > max_duration_seconds)按逗号, : —)再拆; 无逗号则按词数等分。
  4. 每条字幕的时间戳:start = 首词.startend = 末词.end精确无误

匀速估算路(无词级时间戳时 fallback

段内按字符数比例分配时间 —— 即「短时匀速」假设,零模型开销:

句start = 段start + (前缀字符数 / 段总字符数) × 段时长

SRT 规范化

最后统一处理:单条 17 秒过短合并、≤2 行、每行 ≤42 字符(按词折行)。


模型不共驻(显存策略)

ASR 与翻译模型不会同时驻留 GPUmodel_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.6GB3090 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 的残留 进程崩溃 / 异常退出留下

清理策略

  1. 超期任务Task.created_at 早于 now - cache_retention_days(默认 7 天)的任务, 删除其全部产物,并删除对应的 TaskUploadSession 行——避免历史页出现指向已删 文件的死链接。
  2. 孤儿扫描output_dir / work_dir 下名为 task_<id> 但 DB 中已无该 Task 的目录 (崩溃残留),按目录 mtime 判超期后删除。
  3. DB 一致性:删任务时先删关联的 UploadSessionFK再删 Task,保持引用完整。

触发时机

  • 启动时跑一次:容器启动 lifespan 中立即执行(purge_expired_cache),清掉停机期间 超期的产物。
  • 后台定时循环:守护线程 cache-cleanercache_cleanup_interval_hours(默认 24h 循环执行,随进程退出而终止。
  • 手动触发(调试用):进容器跑 python -m app.services.cache_cleaner,打印清理统计 JSON。

相关配置(storage 段)

字段 默认 说明
cache_retention_days 7 保留天数。0 = 禁用清理(产物永久保留)
cache_cleanup_interval_hours 24 定时循环间隔(小时)

与上传会话 reaper 的区别

机制 清理对象 判定 触发
reaperreaper.py 被放弃的分片上传会话(未 complete 的) status=pendingupdated_atchunk_session_ttl_seconds300s 仅启动时一次
cache_cleaner(本节) 已完成/失败任务的产物 + 崩溃孤儿 created_atcache_retention_days7d/ 孤儿 mtime 超期 启动一次 + 定时循环

后台清理线程与请求线程并发写同一 SQLite 库,database.py 已设 busy_timeout=30s 拿锁时阻塞等待而非立即报 database is locked。单 worker 部署下无并发写入压力。