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

141 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

← [返回主页](../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 规范化
最后统一处理:单条 17 秒过短合并、≤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.6GB3090 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 部署下无并发写入压力。