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)。无内容丢失。
This commit is contained in:
140
docs/ARCHITECTURE.md
Normal file
140
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,140 @@
|
||||
← [返回主页](../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:
|
||||
|
||||
### 清理什么
|
||||
|
||||
| 产物 | 路径 | 何时产生 |
|
||||
|---|---|---|
|
||||
| 字幕输出 | `<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 天)的任务,
|
||||
删除其全部产物,并删除对应的 `Task` 与 `UploadSession` 行——避免历史页出现指向已删
|
||||
文件的死链接。
|
||||
2. **孤儿扫描**:`output_dir` / `work_dir` 下名为 `task_<id>` 但 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 部署下无并发写入压力。
|
||||
Reference in New Issue
Block a user