Files
audio2text/README.md
zikai 00e2a95fb7 Initial commit: audio2text 双语字幕生成服务
- 音频/视频转双语(英/中)SRT 字幕,Docker 容器化,CPU 开发/GPU 生产同一份代码
- faster-whisper ASR(词级时间戳) + 断句时间戳重算 + NLLB 翻译(模型不共驻)
- 分片上传(断点续传) + SQLite 持久化 + 主页/历史/日志页面
- 历史页文件名搜索;缓存定时清理(默认保留7天,可配置)
- 双 Dockerfile(cpu/gpu) + setup/start/stop 脚本
2026-07-06 06:54:19 +00:00

29 KiB
Raw Blame History

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 两个 profile
├── setup.sh / start.sh / stop.sh # 安装 / 启动 / 停止(包装 docker 命令)
├── 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/,拖入视频或音频文件即可。

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=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 项(其余字段完全一致):

字段 CPUconfig.cpu.yaml GPUconfig.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/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两份配置完全一致仅以下 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.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 过滤静音段,提升识别质量与速度

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 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 的残留 进程崩溃 / 异常退出留下

清理策略

  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,互不叠加,远低于显存上限。


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" 验证。


依赖

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