Files
audio2text/README.md
audio2text dev 4625650fc8 docs: README 补全新建/重建流程 + 缓存删除边界 + GPU 利用率优化
- 新增「新建/重建容器」子节:首次新建、重建镜像(改代码)、改配置、改依赖
  四种场景的明确操作,区分何时需要重建镜像、何时只需重启
- 新增「缓存分层与删除边界」:BuildKit/pip/模型/运行时数据四类缓存的删除
  影响对照表 + 何时主动清缓存的指引
- 记录之前踩的缓存失效根因:docker builder prune --filter until=Nm 会清掉
  稳定 base 层(CACHED 跳过的层访问时间不刷新 → 被误判可回收)。明确禁止
  使用 until filter,给出正确替代写法
- 新增「GPU 利用率优化」章节:faster-whisper 尖刺波成因(CPU Mel 特征提取
  与 GPU 解码未重叠)+ batch_size 16→32 / beam_size 5→2 的参数选择依据 +
  为什么不能关 word_timestamps(segmenter 强依赖)+ 验证方法
- 配置差异表从 6 项补到 8 项(加 asr.batch_size / asr.beam_size),修正
  translation.batch_size 16→32
- asr/translation 字段表补齐 batch_size / beam_size / sort_by_length 行
- 配置示例同步更新(加 beam_size、sort_by_length,修正 batch_size)
2026-07-06 22:35:33 +08:00

884 lines
39 KiB
Markdown
Raw 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.

# audio2text
音频 / 视频转双语字幕服务。上传视频 → ffmpeg 提取音频 → faster-whisper 识别英语 →
断句 + 时间戳重算 → NLLB 翻译为中文 → 输出双语 SRT。全程跑在 Docker 容器里,自带
网页上传界面,支持大文件分片上传与断点续传。
仿照 zikai 的 `server/` 风格分层(`controllers → services`**CPU 开发 / GPU 生产
同一份代码**,仅靠 `config.yaml``device` + `model` + `compute_type` 三项切换。
---
## 目录
- [功能特性](#功能特性)
- [架构](#架构)
- [部署CPU 开发环境](#部署cpu-开发环境)
- [部署GPU 生产环境](#部署gpu-生产环境)
- [配置文件说明](#配置文件说明)
- [缓存清理与定时任务](#缓存清理与定时任务)
- [HTTP 接口](#http-接口)
- [断句与时间戳重算原理](#断句与时间戳重算原理)
- [模型不共驻(显存策略)](#模型不共驻显存策略)
- [Docker 说明](#docker-说明)
- [新建 / 重建容器](#新建--重建容器)
- [缓存分层与删除边界](#缓存分层与删除边界)
- [⚠️ 不要用 `docker builder prune --filter until`](#-不要用-docker-builder-prune---filter-until)
- [GPU 利用率优化](#gpu-利用率优化)
- [依赖](#依赖)
- [常见问题](#常见问题)
---
## 功能特性
- **主页** `/`:上传入口(拖拽 / 选择文件多文件、4 MiB 分片、断点续传)+ 最近 10 个任务的实时进度卡片,完成的可直接下载字幕。
- **历史任务页** `/history`:分页查看所有历史任务,可下载完成的字幕。
- **实时日志页** `/logs`按级别分层查看——debug=详细子步骤、info=仅阶段转换、error=完整 traceback。
- **大视频处理**:接收完成后用 ffmpeg 提取 16 kHz 单声道 PCM 音频;是否删原始视频由配置决定。
- **faster-whisper 转写英语**,带词级时间戳。
- **断句 + 时间戳重算**:按句末标点(`. ! ? ;`)切句、超长句按逗号拆,时间戳取首末词精确值;
无词级时间戳时退化为段内匀速估算。
- **NLLB-200 英译中**ASR 与翻译模型**不共驻**,翻译时卸载 Whisper 独占显存跑大 batch。
- **双语合并 SRT** 输出(英文在上、中文在下),亦可单独下载英文 / 中文字幕。
- **任务状态机**`queued → extracting → transcribing → segmenting → translating → done`
页面自动轮询进度。
- **定时缓存清理**:任务产物(字幕 / 中间音频 / 保留的原始视频)默认保留 7 天,超期后
连同 DB 记录一并删除;容器内后台线程定时执行(启动时跑一次,默认每 24 小时一次),
保留期与间隔均可配置。
- **SQLite 持久化**(自包含,无需外部 DB
- `/docs`Swagger UI受 Basic Auth 保护。
---
## 架构
Spring 风格分层HTTP 边界controllers与业务逻辑services分离
```
audio2text/
├── Dockerfile # 一份 DockerfileARG VARIANT=cpu|gpu 出两个镜像
├── docker-compose.yml # cpu / gpu / dev 三个 profile
├── setup.sh / start.sh / stop.sh # 安装 / 启动 / 停止(包装 docker 命令)
├── scripts/
│ └── prefetch_models.{sh,py} # 预拉模型权重到 ./models volume避免首次启动下载
├── requirements.txt
├── config.example.yaml # 配置模板(复制为 config.yaml 后填值)
├── README.md
└── app/
├── main.py # FastAPI 应用工厂
├── config.py # 从 config.yaml 加载的类型化 Settingspydantic
├── database.py # SQLite 引擎 / Session / Base / get_db
├── security.py # /docs 的 Basic Auth常量时间比较
├── controllers/ # FastAPI 路由 —— HTTP 边界
│ ├── upload_router.py # 分片上传(建会话/查状态/传片/complete
│ └── task_router.py # 任务列表 / 状态 / 下载字幕
├── services/ # 业务逻辑
│ ├── types.py # 共享 DTOWord/Segment/Subtitle纯 dataclass
│ ├── upload_service.py # 分片上传会话 + 拼接 + 创建任务
│ ├── ffmpeg_service.py # 提取音频 16k mono pcm
│ ├── asr_service.py # faster-whisper 转写
│ ├── segmenter.py # 断句 + 时间戳重算(纯算法,零模型依赖)
│ ├── translate_service.py # NLLB 翻译
│ ├── model_manager.py # 模型加载/卸载(不共驻核心)
│ ├── pipeline.py # 编排提取→识别→断句→翻译→写SRT
│ ├── srt_writer.py # SRT 写入 + 双语合并
│ ├── log_buffer.py # 内存日志缓冲(供 /logs 页面查询)
│ ├── reaper.py # 清理被放弃的上传会话(短 TTL
│ └── cache_cleaner.py # 定时清理超期任务产物 + 孤儿目录(长保留期)
├── models/ # SQLAlchemy ORM
│ ├── task.py # Task转写任务状态机
│ └── upload_session.py # UploadSession分片会话含 task_id FK
├── schemas/ # pydantic 请求/响应 DTO
│ └── task.py
└── views/
├── _shared.py # 共享前端资产BASE_CSS + SHARED_JS + 上传协议 + 页面骨架)
├── home_html.py # 主页(上传入口 + 最近任务卡片)
├── history_html.py # 历史任务分页表格(含搜索)
└── logs_html.py # 实时日志页
```
### 数据流
```
浏览器 /(主页)
│ 分片上传 (4 MiB/片, 可断点续传)
upload_router ──► upload_service ──► UploadSession(SQLite) + 分片落盘
│ complete
创建 Task(queued) ──► pipeline 后台线程
├─ 1. ffmpeg_service.extract_audio → 16k mono wav
│ (按配置删原始视频)
├─ 2. model_manager.get_asr → asr_service.transcribe → segments(带词级时间戳)
├─ 3. segmenter.resegment → 规范字幕条目(精确/估算两路)
├─ 4. model_manager.unload_asr → get_translator
│ translate_service.translate → 中文译文(独占显存大 batch
└─ 5. srt_writer → en.srt / zh.srt / bilingual.srt
更新 Task(done) + 写 output_dir
```
---
## 部署CPU 开发环境
CPU 模式用于本地开发与流程验证模型选同系列最小尺寸2GB 内存开发机即可跑通完整流程。
### 前置要求
- Docker用于构建镜像 + 运行容器)
- 约 500 MB 磁盘(模型缓存)+ 上传视频空间
CPU 模式**不需要** NVIDIA 驱动,普通 Linux / macOS / WSL 均可。
### 步骤
```bash
cd /root/zikai/audio2text
# 1. 构建 CPU 镜像 + 复制 config.cpu.yaml → config.yaml
./setup.sh # 默认 AUDIO2TEXT_VARIANT=cpu
# 2. 启动容器(默认端口 8000
./start.sh
# 3. 停止 / 重启
./stop.sh
./start.sh
```
`setup.sh` 做三件事:检查 docker → 构建 `audio2text:cpu` 镜像 → 把 `config.cpu.yaml`
复制为 `config.yaml`(运行时实际读取的文件)。可重复执行;改完配置后重新 `cp` 并重启即可,
无需重建镜像。
首次启动会下载模型Whisper `tiny.en` ~39M + opus-mt ~300MB`./models` volume
之后秒起。启动后浏览器打开 `http://127.0.0.1:8000/`,拖入视频或音频文件即可。
### 预拉模型(避免首次启动卡在下载)
容器首次处理任务时会从 HuggingFace 下载模型大模型GPU 的 large-v3-turbo ~3GB +
NLLB-1.3B ~2.5GB)下载耗时较长。可用预拉脚本提前下好到 `./models` volume之后容器启动
即用、无需联网:
```bash
./scripts/prefetch_models.sh # 读 config.yaml当前激活配置
./scripts/prefetch_models.sh config.gpu.yaml # 读指定配置(如切换到 GPU 前预拉大模型)
```
脚本用已构建的镜像跑一次性容器,读配置里的 `asr.model` / `translation.model`,下载到
`./models/huggingface`HF 标准缓存)。**幂等**:已下过的模型自动跳过。换 config 的模型
名后重跑即可补下新模型,无需重建镜像。
### CPU 模型选型
| 组件 | 模型 | 大小 | 说明 |
|---|---|---|---|
| ASR | `tiny.en` | ~39M | Whisper 同系列最小,英文专用版(比通用 `tiny` 在英语上更准) |
| 翻译 | `Helsinki-NLP/opus-mt-en-zh` | ~300MB | 最轻量英译中。NLLB 同系列最小 `distilled-600M` 需 ~2.4GB2GB 机 OOM故回退 |
> 翻译质量与 GPU 的 NLLB-1.3B 有差异,但**完整流程一致**(提取→识别→断句→翻译→双语 SRT
> 足以验证端到端逻辑。如需在 CPU 上验证 NLLB 翻译质量,可把 `translation.model` 改为
> `nllb-200-distilled-600M`(需 ≥4GB 内存)或 `nllb-200-distilled-1.3B`(需 ~5GB 内存)。
### 自定义端口
```bash
AUDIO2TEXT_PORT=9000 ./start.sh
```
---
## 部署GPU 生产环境
GPU 模式用于生产模型质量优先NVIDIA 3090 24G 上几 GB 视频几分钟出字幕。
### 前置要求
- Docker
- **NVIDIA GPU 驱动**(宿主机)
- **nvidia container runtime**(让容器能用 GPU安装 `nvidia-container-toolkit`
- 约 6 GB 磁盘模型缓存large-v3-turbo ~3GB + NLLB-1.3B ~2.5GB
验证 GPU 可用:
```bash
nvidia-smi # 宿主能看到 GPU
docker run --rm --gpus all nvidia/cuda:12.1.0-runtime-ubuntu22.04 nvidia-smi
# 上面容器内也能列出 GPU 即说明 nvidia runtime 已就绪
```
### 步骤
```bash
cd /root/zikai/audio2text
# 1. 构建 GPU 镜像 + 复制 config.gpu.yaml → config.yaml
AUDIO2TEXT_VARIANT=gpu ./setup.sh
# 2. 启动容器start.sh 检测到 gpu 镜像 + nvidia-smi 自动加 --gpus all
./start.sh
# 3. 停止 / 重启
./stop.sh
./start.sh
```
`start.sh` 的镜像选择逻辑:若本机存在 `audio2text:gpu` 镜像**且**有 `nvidia-smi`,自动用
GPU 模式(`--gpus all`);否则回退 CPU 镜像。也可用 docker compose 显式启动:
```bash
docker compose --profile gpu up -d --build # GPU
docker compose --profile cpu up -d --build # CPU
```
### GPU 模型选型
| 组件 | 模型 | 显存 | 说明 |
|---|---|---|---|
| ASR | `large-v3-turbo` | ~3GBFP16 | 8x 速度,质量接近 large-v3 |
| 翻译 | `facebook/nllb-200-distilled-1.3B` | ~2.5GBFP16 | 质量最好的蒸馏版 |
ASR 与翻译**不共驻**:翻译阶段先卸载 Whisper 释放显存,独占跑大 batch`batch_size=32`
两者峰值显存互不叠加,远低于 24G 上限。模型缓存(`./models` volume跨容器复用
CPU→GPU 切换时 NLLB/Whisper 大模型首次下载、之后秒起。
### CPU ↔ GPU 切换
同一份代码,仅靠 `AUDIO2TEXT_VARIANT` 切换镜像 + 配置:
```bash
AUDIO2TEXT_VARIANT=gpu ./setup.sh # 切到 GPU构建 gpu 镜像 + config.gpu.yaml
AUDIO2TEXT_VARIANT=cpu ./setup.sh # 切回 CPU构建 cpu 镜像 + config.cpu.yaml
./start.sh # 重新启动
```
两套配置的差异仅在 8 项(其余字段完全一致):
| 字段 | CPU`config.cpu.yaml` | GPU`config.gpu.yaml` |
|---|---|---|
| `asr.model` | `tiny.en` | `large-v3-turbo` |
| `asr.device` | `cpu` | `cuda` |
| `asr.compute_type` | `int8` | `float16` |
| `asr.batch_size` | `8` | `32` |
| `asr.beam_size` | `5` | `2` |
| `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` |
| `translation.device` | `cpu` | `cuda` |
| `translation.batch_size` | `8` | `32` |
### 启动后的入口
两种模式通用:
| 入口 | 地址 |
|---|---|
| 主页 | `http://127.0.0.1:8000/`(上传入口 + 最近 10 任务进度卡片) |
| 历史任务 | `http://127.0.0.1:8000/history`(分页查看所有任务,可按文件名搜索、下载字幕) |
| 日志页 | `http://127.0.0.1:8000/logs`(按级别分层、自动刷新) |
| API 文档 | `http://127.0.0.1:8000/docs`Basic Auth凭据见 config.yaml `docs` 段) |
| 健康检查 | `http://127.0.0.1:8000/health` |
| 任务列表 | `http://127.0.0.1:8000/api/tasks` |
---
## 配置文件说明
项目预置两份配置文件,`setup.sh``AUDIO2TEXT_VARIANT` 自动复制对应文件为
`config.yaml`(运行时实际读取的文件,不入库):
| 文件 | 激活方式 | 说明 |
|---|---|---|
| `config.cpu.yaml` | `./setup.sh`(默认) | CPU 开发,最小模型 |
| `config.gpu.yaml` | `AUDIO2TEXT_VARIANT=gpu ./setup.sh` | GPU 生产,质量优先 |
| `config.example.yaml` | — | 带完整注释的字段参考模板 |
也可手动切换:`cp config.gpu.yaml config.yaml` 后重启容器即可,无需重建镜像(镜像不含配置)。
运行时通过环境变量 `CONFIG_PATH` 指定路径(容器内默认 `/app/config.yaml`)。所有路径相对
容器内文件系统。`config.py` 用 pydantic 做类型校验,缺字段时回退默认值。
### CPU / GPU 两份配置的差异
其余字段存储、断句、日志、docs两份配置完全一致仅以下 8 项不同:
| 字段 | `config.cpu.yaml` | `config.gpu.yaml` |
|---|---|---|
| `asr.model` | `tiny.en` | `large-v3-turbo` |
| `asr.device` | `cpu` | `cuda` |
| `asr.compute_type` | `int8` | `float16` |
| `asr.batch_size` | `8` | `32` |
| `asr.beam_size` | `5` | `2` |
| `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` |
| `translation.device` | `cpu` | `cuda` |
| `translation.batch_size` | `8` | `32` |
### 完整字段
#### `server` — 服务监听
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `host` | str | `0.0.0.0` | 容器内监听地址(由 `docker -p` 映射到宿主) |
| `port` | int | `8000` | 容器内监听端口 |
| `workers` | int | `1` | uvicorn worker 数。ML 推理为重,固定单 worker 避免显存重复占用 |
#### `storage` — 文件存储
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `upload_dir` | str | `/data/uploads` | 上传视频落盘根目录(按 `yyyy/mm` 分子目录) |
| `work_dir` | str | `/data/.work` | 分片会话暂存 + 中间音频 + SQLite 数据库 |
| `output_dir` | str | `/data/outputs` | 生成的 SRT 字幕输出目录 |
| `chunk_bytes` | int | `1048576` | 流式分片大小1 MiB。注意前端上传页固定 4 MiB此项影响服务端缓冲 |
| `chunk_session_ttl_seconds` | int | `300` | 被放弃的分片会话存活秒数,超时后后台 reaper 清理(短 TTL与下方缓存清理不同 |
| `cache_retention_days` | int | `7` | 任务产物(字幕 / 中间音频 / 保留的原始视频)保留天数;超期任务连同 DB 记录一并删除。`0` = 禁用清理 |
| `cache_cleanup_interval_hours` | int | `24` | 定时清理间隔(小时)。容器启动时跑一次,之后按此间隔循环 |
#### `processing` — 处理流程
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `delete_original_after_extract` | bool | `true` | 提取音频成功后删除原始视频,省空间。`false` 则保留视频 |
| `keep_audio` | bool | `false` | 任务完成后是否保留中间 wav。`false` 则只留字幕、删 wav |
#### `asr` — 语音识别faster-whisper
| 字段 | 类型 | 默认CPU | 说明 |
|---|---|---|---|
| `model` | str | `tiny.en` | Whisper 模型名。CPU dev 用 `tiny.en`39M英文专用同系列最小GPU prod 用 `large-v3-turbo`8x 速度,质量接近 large-v3 |
| `device` | str | `cpu` | `cpu``cuda` |
| `compute_type` | str | `int8` | CPU 用 `int8`GPU 用 `float16` |
| `language` | str | `en` | 识别语言,仅英语 |
| `word_timestamps` | bool | `true` | 词级时间戳:让断句精确(取首末词时间戳)而非纯匀速估算。建议开 |
| `vad_filter` | bool | `true` | 过滤静音段,提升识别质量与速度 |
| `batch_size` | int | `8`CPU/ `32`GPU | `BatchedInferencePipeline` 批量解码的音频块数。GPU 拉大 batch 拉长单次 GPU 解码时间,掩盖 CPU 提取 Mel 特征的间隙,提升平均利用率 |
| `beam_size` | int | `5`CPU/ `2`GPU | beam search 宽度。GPU turbo 模型鲁棒,降到 2 减少解码候选数与步数加速明显、质量损失小CPU 无加速诉求保持默认 5 |
#### `translation` — 翻译NLLB-200
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `model` | str | `facebook/nllb-200-distilled-1.3B` | HuggingFace 模型名。GPU 生产用 1.3B质量最好CPU dev 用 `Helsinki-NLP/opus-mt-en-zh`~300MB2GB 机可跑。NLLB 同系列最小为 `distilled-600M`~1.2GB,需 ≥4GB 内存) |
| `device` | str | `cpu` | `cpu``cuda` |
| `src_lang` | str | `eng_Latn` | NLLB 语言码:英语 |
| `tgt_lang` | str | `zho_Hans` | NLLB 语言码:简体中文 |
| `batch_size` | int | `8`CPU/ `32`GPU | 翻译批量大小。不与 ASR 共驻时显存独占,可用大 batch |
| `max_length` | int | `256` | 单条翻译最大 token 数 |
| `sort_by_length` | bool | `true` | 按句子长度排序后分批,减少批内 padding 浪费GPU 收益大) |
#### `segmentation` — 断句与字幕规范化
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `max_words_per_line` | int | `14` | 单行最多词数,超出按逗号拆分 |
| `max_duration_seconds` | float | `7.0` | 单条字幕最长 7 秒 |
| `min_duration_seconds` | float | `1.0` | 单条字幕最短 1 秒(太短则与下条合并) |
| `max_chars_per_line` | int | `42` | SRT 规范:每行 ≤42 字符超出按词折行≤2 行) |
#### `logging` — 日志
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `level` | str | `info` | 控制台输出的最低级别:`debug` / `info` / `warning` / `error`。不影响 `/logs` 页面(页面可自由切换级别查看) |
| `buffer_size` | int | `2000` | `/logs` 页面内存缓冲条数(有界 deque旧记录自动淘汰 |
日志分层语义:
| 级别 | 内容 | 示例 |
|---|---|---|
| **debug**(详细) | 子步骤ffmpeg 命令、模型加载/卸载、转写逐段、翻译逐批进度 | `加载 ASR 模型 model=tiny.en device=cpu` / `ffmpeg 命令ffmpeg -y ...` |
| **info**(简略) | 仅任务阶段转换,看当前进行到哪一步 | `任务 1 [transcribing 55%] 识别出 3 段` |
| **error**(详细) | 完整 traceback文件名+行号+调用链),可点击展开 | `任务 1 失败ffmpeg 失败 (code=183)...` + traceback |
> **注意**`logging.level` 只控制控制台输出级别。`/logs` 页面始终全量缓冲DEBUG 起),
> 页面上的级别按钮是查询过滤,不受此配置限制——所以控制台设 `info` 保持简略,而 `/logs`
> 页面切到 DEBUG 仍能看到所有详细子步骤。
#### `docs` — API 文档保护
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `enabled` | bool | `true` | 是否开启 `/docs` `/redoc` `/openapi.json` |
| `username` | str | `admin` | Basic Auth 用户名 |
| `password` | str | `CHANGE_ME` | Basic Auth 明文密码(常量时间比较)。**部署前务必修改** |
| `realm` | str | `audio2text docs` | WWW-Authenticate realm |
### 配置示例
```yaml
server:
host: 0.0.0.0
port: 8000
workers: 1
storage:
upload_dir: /data/uploads
work_dir: /data/.work
output_dir: /data/outputs
chunk_bytes: 1048576
chunk_session_ttl_seconds: 300
cache_retention_days: 7 # 任务产物保留天数超期清理0=禁用)
cache_cleanup_interval_hours: 24 # 定时清理间隔(启动时跑一次,之后循环)
processing:
delete_original_after_extract: true
keep_audio: false
asr:
model: tiny.en # GPU: large-v3-turbo
device: cpu # GPU: cuda
compute_type: int8 # GPU: float16
language: en
word_timestamps: true
vad_filter: true
batch_size: 8 # GPU: 32(拉长单次 GPU 解码,掩盖 CPU 特征提取间隙)
beam_size: 5 # GPU: 2turbo 鲁棒可降,候选数↓解码步数↓)
translation:
model: facebook/nllb-200-distilled-1.3B
device: cpu # GPU: cuda
src_lang: eng_Latn
tgt_lang: zho_Hans
batch_size: 8 # GPU: 32(显存独占可用大 batch
max_length: 256
sort_by_length: true # 按长度排序分批,减少 padding 浪费
segmentation:
max_words_per_line: 14
max_duration_seconds: 7.0
min_duration_seconds: 1.0
max_chars_per_line: 42
logging:
level: info # debug | info | warning | error控制台输出最低级别
buffer_size: 2000
docs:
enabled: true
username: admin
password: "CHANGE_ME"
realm: "audio2text docs"
```
---
## 缓存清理与定时任务
每个任务落盘的产物(字幕、中间音频、保留的原始视频)会持续占用磁盘。容器内置定时
清理(`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 部署下无并发写入压力。
---
## HTTP 接口
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET | `/` | 无 | 主页(上传入口 + 最近 10 任务进度卡片) |
| GET | `/health` | 无 | 存活探针 |
| GET | `/history` | 无 | 历史任务页(分页表格,可按文件名搜索、下载字幕) |
| GET | `/logs` | 无 | 实时日志页(按级别过滤、自动刷新、可展开 traceback |
| GET | `/docs` `/redoc` | Basic Auth | API 文档 |
| POST | `/api/tasks/chunk-uploads` | 无 | 创建分片上传会话 |
| GET | `/api/tasks/chunk-uploads/{id}/status` | 无 | 查已传分片(断点续传) |
| POST | `/api/tasks/chunk-uploads/{id}/chunks/{index}` | 无 | 上传单个分片(原始二进制 body |
| POST | `/api/tasks/chunk-uploads/{id}/complete` | 无 | 拼接 + 创建转写任务 |
| GET | `/api/tasks` | 无 | 任务列表(`limit` / `offset` 分页,`q` 按文件名模糊搜索) |
| GET | `/api/tasks/{id}` | 无 | 任务状态status / progress / error |
| GET | `/api/tasks/{id}/subtitle?type=bilingual\|en\|zh` | 无 | 下载字幕 |
| GET | `/api/logs?level=debug\|info\|warning\|error&tail=N` | 无 | 查询日志(按级别过滤,最近 N 条) |
| DELETE | `/api/logs` | 无 | 清空日志缓冲 |
### 分片上传协议(与 server 一致)
1. **建会话** `POST /api/tasks/chunk-uploads`body 含 `filename` / `size_bytes` /
`chunk_size` / `total_chunks`,返回 `upload_id`
2. **查状态** `GET .../status`,返回 `uploaded_chunks`(已传分片下标列表)。
断点续传时先查此接口,只补传缺失分片。
3. **传分片** `POST .../chunks/{index}`body 为原始二进制。分片可乱序、可重传覆盖。
4. **完成** `POST .../complete`,服务端按 index 顺序拼接为正式视频文件,创建转写 Task
并入队。complete 幂等:重复调用返回同一 `task_id`
### 请求/响应示例
创建会话:
```bash
curl -X POST http://127.0.0.1:8000/api/tasks/chunk-uploads \
-H 'Content-Type: application/json' \
-d '{"filename":"demo.mp4","size_bytes":10485760,"chunk_size":4194304,"total_chunks":3}'
# → {"upload_id":"a1b2...","filename":"demo.mp4","size_bytes":10485760,"chunk_size":4194304,"total_chunks":3}
```
查任务状态:
```bash
curl http://127.0.0.1:8000/api/tasks/1
# → {"id":1,"filename":"demo.mp4","status":"done","progress":100.0,"error":null,"has_subtitle":true,...}
```
下载字幕:
```bash
curl -OJ http://127.0.0.1:8000/api/tasks/1/subtitle?type=bilingual
```
---
## 断句与时间戳重算原理
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 对比字幕确认无歧义发音处的降级。
---
## Docker 说明
### 一份 Dockerfile两个镜像
`ARG VARIANT=cpu|gpu` 控制基础镜像与 torch 轮子:
| VARIANT | 基础镜像 | torch |
|---|---|---|
| `cpu`(默认) | `python:3.12-slim` | CPU 版(`--index-url .../whl/cpu` |
| `gpu` | `nvidia/cuda:12.1.0-runtime-ubuntu22.04` | CUDA 版 |
两个镜像的 Python 依赖列表(`requirements.txt`)完全一致,仅 torch 不同。镜像内 apt 装
`ffmpeg` + `patchelf`
### 新建 / 重建容器
项目提供 `setup.sh` / `start.sh` / `stop.sh` 包装脚本,也可直接用 `docker` / `docker compose`
#### 首次新建(新机器 / 全新拉取代码后)
```bash
# 1. 构建镜像 + 生成 config.yamlCPU 默认)
./setup.sh
# GPUAUDIO2TEXT_VARIANT=gpu ./setup.sh
# 2. 启动容器
./start.sh
# GPUstart.sh 检测到 audio2text:gpu 镜像 + nvidia-smi 自动加 --gpus all
```
`setup.sh` 做三件事:检查 docker → 构建 `audio2text:{variant}` 镜像 → 把
`config.{variant}.yaml` 复制为 `config.yaml`(运行时实际读取的文件)。
#### 重建镜像(改了 app 代码或 requirements 后)
依赖层apt + pip + torch由 BuildKit 缓存挂载复用,只有 `COPY app` 层重建,通常
30 秒内完成。**重建不会动运行时数据**`./data` / `./models` 是挂载的 volume
```bash
# CPU直接重跑 setup.sh幂等会复用缓存层
./setup.sh
# 或显式构建:
docker build --build-arg VARIANT=cpu -t audio2text:cpu .
# GPU
AUDIO2TEXT_VARIANT=gpu ./setup.sh
# 或:
docker build --build-arg VARIANT=gpu -t audio2text:gpu .
# 重建后重启容器(替换运行中的旧镜像):
./stop.sh && ./start.sh
```
#### 改配置(不重建镜像)
`config.yaml` 是只读挂载,改完重启容器即生效,**无需重建镜像**
```bash
cp config.gpu.yaml config.yaml # 切换配置(或直接编辑 config.yaml
./stop.sh && ./start.sh
```
#### 改依赖requirements.txt / torch 版本)
会触发 deps 层重建,耗时较长(重装 torch + 全部依赖CPU ~3 分钟GPU ~5 分钟)。
BuildKit 的 pip 缓存挂载(`/root/.cache/pip`)跨构建复用已下载的 wheel二次构建会快
很多。
```bash
# 编辑 requirements.txt 后
./setup.sh # 或 docker build --build-arg VARIANT=gpu -t audio2text:gpu .
./stop.sh && ./start.sh
```
#### docker compose替代脚本
```bash
docker compose --profile dev up -d --build # 开发:源码挂载 + uvicorn reload改代码零重建
docker compose --profile cpu up -d --build # CPU 生产
docker compose --profile gpu up -d --build # GPU 生产(需 nvidia runtime
```
### 缓存分层与删除边界
这套构建涉及三类缓存,**删除策略截然不同**,乱删会导致全量重建:
| 缓存类型 | 位置 | 存什么 | 能删吗 | 删了会怎样 |
|---|---|---|---|---|
| **BuildKit 构建缓存** | Docker 内部(`docker builder` 管理) | Dockerfile 各层base / deps / final的构建产物 | ⚠️ 谨慎,见下方 | 命中失效 → 该层及下游全量重建 |
| **pip wheel 缓存** | BuildKit cache mount `/root/.cache/pip` | 下载过的 `.whl` 文件 | ✅ 可删 | 下次构建重新下载 wheel不重编译 |
| **模型缓存** | `./models` volume容器内 `/models` | Whisper / NLLB 权重HF + ctranslate2 | ✅ 可删 | 下次启动重新下载模型(~5.5GB GPU |
| **运行时数据** | `./data` volume容器内 `/data` | 上传视频 / 中间音频 / 输出字幕 / SQLite | ⚠️ 视情况 | 删了任务历史和产物全没 |
### ⚠️ 不要用 `docker builder prune --filter until`
**这是踩过的坑**。BuildKit 的 `--filter "until=30m"`(或任意时长)会清除"最近 N 分钟未
访问"的缓存层。问题在于:**稳定的基础层**(如 `base-gpu` 的 apt 装 python3.12)只在
首次构建时执行一次,之后每次构建都直接 CACHED 跳过——它的"最后访问时间"一直停在首次
构建那一刻,永远不会更新。于是 `--filter "until=..."` 会把这些**仍然在用的稳定层**当成
"很久没访问"清掉,导致下一次构建从 base 层开始全量重来GPU 镜像 ~10 分钟 + 重新下载
torch ~2.5GB)。
正确做法:
```bash
# ✅ 想清理磁盘、释放 BuildKit 缓存:用不带 filter 的 prune清全部未引用缓存
docker builder prune -f
# 或只清 dangling悬挂的、无引用的中间层
docker builder prune -f --filter "type=regular"
# ✅ 清旧镜像(不影响构建缓存)
docker image prune -a # 删所有未被容器使用的镜像
docker image prune # 只删 dangling 镜像
# ✅ 清 pip wheel 缓存BuildKit cache mount安全
docker builder prune -f --filter "type=exec.cachemount"
# ❌ 永远不要这样用——会清掉仍在用的稳定 base 层
docker builder prune -f --filter "until=30m"
docker builder prune -f --filter "until=24h"
```
> 根因BuildKit 的 `until` filter 按"最后访问时间"判定,而非"是否仍在被引用"。CACHED
> 跳过的层不会刷新访问时间,于是被误判为可回收。这是 BuildKit 的已知行为,不是 bug
> 但对"稳定 base + 频繁改代码"的构建模式特别致命。详见
> [moby/buildkit#2414](https://github.com/moby/buildkit/issues/2414)。
#### 什么时候需要主动清缓存
- **磁盘紧张**`docker builder prune -f` + `docker image prune` 释放空间
- **依赖换了 torch / CUDA 大版本**BuildKit 可能复用了不兼容的旧 wheel清 pip 缓存
mount 强制重下:`docker builder prune -f --filter "type=exec.cachemount"`
- **换 VARIANTcpu↔gpu**:不需要清——两条构建链独立,缓存互不干扰
- **想从零验证构建**`docker builder prune -af` 清全部,模拟新机器首次构建
#### 模型缓存(`./models`
模型权重在 `./models` volume容器内 `HF_HOME=/models/huggingface`
`CT2_CACHE=/models/ctranslate2`),跨容器复用。首次启动下载,之后秒起。
```bash
# 查看模型缓存大小
du -sh ./models
# 删了强制重下GPU 大模型 ~5.5GB,建议用 prefetch 脚本提前下好)
rm -rf ./models
./scripts/prefetch_models.sh config.gpu.yaml
```
### Volume 挂载
| 容器路径 | 宿主路径 | 用途 | 删除影响 |
|---|---|---|---|
| `/data` | `./data`CPU/ `./data-gpu`GPU | 上传视频、中间音频、输出字幕、SQLite | 任务历史和产物全没 |
| `/models` | `./models` | 模型缓存HF + ctranslate2跨容器复用 | 下次启动重下模型 |
| `/app/config.yaml` | `./config.yaml`(只读) | 配置文件 | 改配置需重启容器 |
镜像本身无状态、无敏感数据。
### ctranslate2 可执行栈修复
ctranslate2 的 `.so`(在 `ctranslate2.libs/` 隐藏目录)带 PT_GNU_STACK 可执行栈标志,
在某些内核 + Docker 组合下会报 `cannot enable executable stack as shared object requires`
Dockerfile 在构建时用 `patchelf --clear-execstack` 清掉该标志,无需放宽容器安全策略。
构建末尾有 `python -c "import ctranslate2"` 验证。
---
## 依赖
### Python`requirements.txt`
| 包 | 用途 |
|---|---|
| `fastapi` + `uvicorn[standard]` + `python-multipart` | Web 服务 |
| `pydantic` + `pydantic-settings` | 配置类型校验 |
| `PyYAML` | 读 config.yaml |
| `SQLAlchemy` | SQLite ORM |
| `faster-whisper` + `ctranslate2` | 语音识别 |
| `transformers` + `sentencepiece` + `accelerate` | NLLB 翻译 |
| `psutil` | 进程信息 |
torch 单独安装CPU / CUDA 轮子不同),不在 requirements.txt 中。
### 系统
- `ffmpeg`(镜像内 apt 装)—— 提取音频
- `patchelf`(镜像内 apt 装)—— 修复 ctranslate2 可执行栈
- GPU 镜像额外需要宿主 NVIDIA 驱动 + nvidia container runtime
---
## 常见问题
### Q: CPU 开发机能跑 NLLB 吗?
`config.cpu.yaml` 默认用 opus-mt-en-zh~300MB2GB 内存开发机即可跑通完整流程。
若想在 CPU 上验证 NLLB 翻译质量,可手动改 `translation.model`
- `facebook/nllb-200-distilled-600M`~1.2GB,同系列最小)——需 ≥4GB 内存2GB 机会 OOM。
- `facebook/nllb-200-distilled-1.3B`~2.5GBGPU 生产同款)——需 ~5GB 内存。
生产环境3090 24G用 NLLB-1.3B 质量最好。
### Q: 模型下载到哪里?每次启动都重下吗?
模型缓存到 `/models` volume`HF_HOME=/models/huggingface``CT2_CACHE=/models/ctranslate2`)。
首次启动下载,之后跨容器复用秒起。删除 `./models` 目录会强制重下。
### Q: 上传大视频中断了怎么办?
分片上传支持断点续传。重新上传同一文件时,前端先调 `status` 接口查已传分片,只补传缺失的。
分片可乱序、可重传覆盖。
### Q: 怎么保留原始视频不删?
`config.yaml``processing.delete_original_after_extract` 改为 `false`
注意:保留的视频仍受缓存清理策略约束——任务超期(默认 7 天)后会被 `cache_cleaner`
连同字幕一起删除。想永久保留请把 `storage.cache_retention_days` 设为 `0`(禁用清理)。
### Q: 字幕 / 任务记录多久会被自动清理?能禁用吗?
默认保留 7 天(`storage.cache_retention_days`)。超期任务的字幕、中间音频、保留的原始
视频连同 DB 记录一并删除,启动时跑一次 + 每 `cache_cleanup_interval_hours`(默认 24h
循环一次。设 `cache_retention_days: 0` 可禁用自动清理(产物永久保留,需自行管理磁盘)。
手动触发:`docker exec audio2text python -m app.services.cache_cleaner`
### Q: GPU 镜像构建好了但 start.sh 还是用 CPU
`start.sh` 检测到 `audio2text:gpu` 镜像**且**本机有 `nvidia-smi` 才用 GPU。确认宿主装了
NVIDIA 驱动 + nvidia container runtime。也可用 `docker compose --profile gpu up -d` 显式启动。