faster-whisper 的 GPU 利用率呈尖刺波(峰=批量解码满载,谷=CPU 提取 Mel 特征 + 处理结果时 GPU 空闲),平均利用率低。瓶颈不在算力而在 CPU/GPU 未重叠。 - batch_size 16→32:拉长单次 GPU 解码时间,相对掩盖 CPU 特征提取间隙, 尖刺变宽变平,平均利用率上升。turbo FP16 仅 ~1.6GB,3090 24G 充裕。 - beam_size 5→2:turbo 模型鲁棒,候选数 5→2 大幅减少解码步数,让 GPU 峰更密、间隙更短。保留 1 个候选做歧义发音保险,质量损失小。 - beam_size 从硬编码提到 config 可调,CPU/CPU 模板/GPU/示例 四份配置对齐 - /health 增加 asr_beam_size,模型加载日志同步输出 batch+beam word_timestamps 保留 True:segmenter 强依赖词级时间戳做精确断句, 关闭会触发匀速估算退化路径,得不偿失。
audio2text
音频 / 视频转双语字幕服务。上传视频 → ffmpeg 提取音频 → faster-whisper 识别英语 → 断句 + 时间戳重算 → NLLB 翻译为中文 → 输出双语 SRT。全程跑在 Docker 容器里,自带 网页上传界面,支持大文件分片上传与断点续传。
仿照 zikai 的 server/ 风格分层(controllers → services),CPU 开发 / GPU 生产
同一份代码,仅靠 config.yaml 的 device + 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)。
/docs(Swagger UI)受 Basic Auth 保护。
架构
Spring 风格分层,HTTP 边界(controllers)与业务逻辑(services)分离:
audio2text/
├── Dockerfile # 一份 Dockerfile,ARG 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 加载的类型化 Settings(pydantic)
├── 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 # 共享 DTO(Word/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/huggingface(HF 标准缓存)。幂等:已下过的模型自动跳过。换 config 的模型
名后重跑即可补下新模型,无需重建镜像。
CPU 模型选型
| 组件 | 模型 | 大小 | 说明 |
|---|---|---|---|
| ASR | tiny.en |
~39M | Whisper 同系列最小,英文专用版(比通用 tiny 在英语上更准) |
| 翻译 | Helsinki-NLP/opus-mt-en-zh |
~300MB | 最轻量英译中。NLLB 同系列最小 distilled-600M 需 ~2.4GB,2GB 机 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 |
~3GB(FP16) | 8x 速度,质量接近 large-v3 |
| 翻译 | facebook/nllb-200-distilled-1.3B |
~2.5GB(FP16) | 质量最好的蒸馏版 |
ASR 与翻译不共驻:翻译阶段先卸载 Whisper 释放显存,独占跑大 batch(batch_size=16),
两者峰值显存互不叠加,远低于 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 # 重新启动
两套配置的差异仅在 6 项(其余字段完全一致):
| 字段 | CPU(config.cpu.yaml) |
GPU(config.gpu.yaml) |
|---|---|---|
asr.model |
tiny.en |
large-v3-turbo |
asr.device |
cpu |
cuda |
asr.compute_type |
int8 |
float16 |
translation.model |
Helsinki-NLP/opus-mt-en-zh |
facebook/nllb-200-distilled-1.3B |
translation.device |
cpu |
cuda |
translation.batch_size |
8 |
16 |
启动后的入口
两种模式通用:
| 入口 | 地址 |
|---|---|
| 主页 | 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)两份配置完全一致,仅以下 6 项不同:
| 字段 | config.cpu.yaml |
config.gpu.yaml |
|---|---|---|
asr.model |
tiny.en |
large-v3-turbo |
asr.device |
cpu |
cuda |
asr.compute_type |
int8 |
float16 |
translation.model |
Helsinki-NLP/opus-mt-en-zh |
facebook/nllb-200-distilled-1.3B |
translation.device |
cpu |
cuda |
translation.batch_size |
8 |
16 |
完整字段
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 |
过滤静音段,提升识别质量与速度 |
translation — 翻译(NLLB-200)
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
model |
str | facebook/nllb-200-distilled-1.3B |
HuggingFace 模型名。GPU 生产用 1.3B(质量最好);CPU dev 用 Helsinki-NLP/opus-mt-en-zh(~300MB,2GB 机可跑)。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 | 16 |
翻译批量大小。不与 ASR 共驻时显存独占,可用大 batch |
max_length |
int | 256 |
单条翻译最大 token 数 |
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
translation:
model: facebook/nllb-200-distilled-1.3B
device: cpu # GPU: cuda
src_lang: eng_Latn
tgt_lang: zho_Hans
batch_size: 16
max_length: 256
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 的残留 | 进程崩溃 / 异常退出留下 |
清理策略
- 超期任务:
Task.created_at早于now - cache_retention_days(默认 7 天)的任务, 删除其全部产物,并删除对应的Task与UploadSession行——避免历史页出现指向已删 文件的死链接。 - 孤儿扫描:
output_dir/work_dir下名为task_<id>但 DB 中已无该 Task 的目录 (崩溃残留),按目录mtime判超期后删除。 - 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 一致)
- 建会话
POST /api/tasks/chunk-uploads,body 含filename/size_bytes/chunk_size/total_chunks,返回upload_id。 - 查状态
GET .../status,返回uploaded_chunks(已传分片下标列表)。 断点续传时先查此接口,只补传缺失分片。 - 传分片
POST .../chunks/{index},body 为原始二进制。分片可乱序、可重传覆盖。 - 完成
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,默认)
- 汇集所有词的
(text, start, end)。 - 按句末标点(
. ! ? ;)切句。 - 超长句(>
max_words_per_line或 >max_duration_seconds)按逗号(, : —)再拆; 无逗号则按词数等分。 - 每条字幕的时间戳:
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,互不叠加,远低于显存上限。
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。
Volume 挂载
| 容器路径 | 宿主路径 | 用途 |
|---|---|---|
/data |
./data |
上传视频、中间音频、输出字幕、SQLite 数据库 |
/models |
./models |
模型缓存(HF + ctranslate2),跨容器复用避免重下 |
/app/config.yaml |
./config.yaml |
配置文件(只读挂载) |
镜像本身无状态、无敏感数据。
docker-compose
docker-compose.yml 提供 audio2text-cpu / audio2text-gpu 两个 profile:
docker compose --profile cpu up -d # CPU
docker compose --profile gpu up -d # GPU(需 nvidia runtime)
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(~300MB),2GB 内存开发机即可跑通完整流程。
若想在 CPU 上验证 NLLB 翻译质量,可手动改 translation.model:
facebook/nllb-200-distilled-600M(~1.2GB,同系列最小)——需 ≥4GB 内存,2GB 机会 OOM。facebook/nllb-200-distilled-1.3B(~2.5GB,GPU 生产同款)——需 ~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 显式启动。