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

audio2text

音频 / 视频转双语字幕服务。上传视频 → ffmpeg 提取音频 → faster-whisper 识别英语 → 断句 + 时间戳重算 → NLLB 翻译为中文 → 输出双语 SRT。全程跑在 Docker 容器里,自带 网页上传界面,支持大文件分片上传与断点续传。

仿照 zikai 的 server/ 风格分层(controllers → servicesCPU 开发 / GPU 生产 同一份代码,仅靠 config.yamldevice + model + compute_type 三项切换。


目录


功能特性

  • 主页 /:上传入口(拖拽 / 选择文件多文件、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
  • /docsSwagger 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 均可。

步骤

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之后容器启动 即用、无需联网:

./scripts/prefetch_models.sh                  # 读 config.yaml当前激活配置
./scripts/prefetch_models.sh config.gpu.yaml  # 读指定配置(如切换到 GPU 前预拉大模型)

脚本用已构建的镜像跑一次性容器,读配置里的 asr.model / translation.model,下载到 ./models/huggingfaceHF 标准缓存)。幂等:已下过的模型自动跳过。换 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 内存)。

自定义端口

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 可用:

nvidia-smi                            # 宿主能看到 GPU
docker run --rm --gpus all nvidia/cuda:12.1.0-runtime-ubuntu22.04 nvidia-smi
# 上面容器内也能列出 GPU 即说明 nvidia runtime 已就绪

步骤

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 显式启动:

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 释放显存,独占跑大 batchbatch_size=32 两者峰值显存互不叠加,远低于 24G 上限。模型缓存(./models volume跨容器复用 CPU→GPU 切换时 NLLB/Whisper 大模型首次下载、之后秒起。

CPU ↔ GPU 切换

同一份代码,仅靠 AUDIO2TEXT_VARIANT 切换镜像 + 配置:

AUDIO2TEXT_VARIANT=gpu ./setup.sh   # 切到 GPU构建 gpu 镜像 + config.gpu.yaml
AUDIO2TEXT_VARIANT=cpu ./setup.sh   # 切回 CPU构建 cpu 镜像 + config.cpu.yaml
./start.sh                          # 重新启动

两套配置的差异仅在 8 项(其余字段完全一致):

字段 CPUconfig.cpu.yaml GPUconfig.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/docsBasic Auth凭据见 config.yaml docs 段)
健康检查 http://127.0.0.1:8000/health
任务列表 http://127.0.0.1:8000/api/tasks

配置文件说明

项目预置两份配置文件,setup.shAUDIO2TEXT_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.en39M英文专用同系列最小GPU prod 用 large-v3-turbo8x 速度,质量接近 large-v3
device str cpu cpucuda
compute_type str int8 CPU 用 int8GPU 用 float16
language str en 识别语言,仅英语
word_timestamps bool true 词级时间戳:让断句精确(取首末词时间戳)而非纯匀速估算。建议开
vad_filter bool true 过滤静音段,提升识别质量与速度
batch_size int 8CPU/ 32GPU BatchedInferencePipeline 批量解码的音频块数。GPU 拉大 batch 拉长单次 GPU 解码时间,掩盖 CPU 提取 Mel 特征的间隙,提升平均利用率
beam_size int 5CPU/ 2GPU 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 cpucuda
src_lang str eng_Latn NLLB 语言码:英语
tgt_lang str zho_Hans NLLB 语言码:简体中文
batch_size int 8CPU/ 32GPU 翻译批量大小。不与 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

配置示例

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 天)的任务, 删除其全部产物,并删除对应的 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 部署下无并发写入压力。


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-uploadsbody 含 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

请求/响应示例

创建会话:

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}

查任务状态:

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,...}

下载字幕:

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 = 首词.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 对比字幕确认无歧义发音处的降级。


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

首次新建(新机器 / 全新拉取代码后)

# 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

# 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 是只读挂载,改完重启容器即生效,无需重建镜像

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二次构建会快 很多。

# 编辑 requirements.txt 后
./setup.sh   # 或 docker build --build-arg VARIANT=gpu -t audio2text:gpu .
./stop.sh && ./start.sh

docker compose替代脚本

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)。

正确做法:

# ✅ 想清理磁盘、释放 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

什么时候需要主动清缓存

  • 磁盘紧张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/huggingfaceCT2_CACHE=/models/ctranslate2),跨容器复用。首次启动下载,之后秒起。

# 查看模型缓存大小
du -sh ./models

# 删了强制重下GPU 大模型 ~5.5GB,建议用 prefetch 脚本提前下好)
rm -rf ./models
./scripts/prefetch_models.sh config.gpu.yaml

Volume 挂载

容器路径 宿主路径 用途 删除影响
/data ./dataCPU/ ./data-gpuGPU 上传视频、中间音频、输出字幕、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" 验证。


依赖

Pythonrequirements.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 volumeHF_HOME=/models/huggingfaceCT2_CACHE=/models/ctranslate2)。 首次启动下载,之后跨容器复用秒起。删除 ./models 目录会强制重下。

Q: 上传大视频中断了怎么办?

分片上传支持断点续传。重新上传同一文件时,前端先调 status 接口查已传分片,只补传缺失的。 分片可乱序、可重传覆盖。

Q: 怎么保留原始视频不删?

config.yamlprocessing.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 显式启动。

Description
No description provided
Readme 311 KiB
Languages
Python 92.6%
Dockerfile 4.2%
Shell 3.2%