# 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-说明) - [依赖](#依赖) - [常见问题](#常见问题) --- ## 功能特性 - **主页** `/`:上传入口(拖拽 / 选择文件,多文件、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 两个 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 加载的类型化 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 均可。 ### 步骤 ```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/`,拖入视频或音频文件即可。 ### 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 内存)。 ### 自定义端口 ```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` | ~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` 切换镜像 + 配置: ```bash 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 | ### 配置示例 ```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 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: ### 清理什么 | 产物 | 路径 | 何时产生 | |---|---|---| | 字幕输出 | `/task_/` | 任务完成 | | 中间音频 | `/task_.wav` | `keep_audio=true` 且管线未删时残留 | | 保留的原始视频 | `/yyyy/mm/.` | `delete_original_after_extract=false` 时 | | 孤儿目录 | 上述目录中无对应 Task 的残留 | 进程崩溃 / 异常退出留下 | ### 清理策略 1. **超期任务**:`Task.created_at` 早于 `now - cache_retention_days`(默认 7 天)的任务, 删除其全部产物,并删除对应的 `Task` 与 `UploadSession` 行——避免历史页出现指向已删 文件的死链接。 2. **孤儿扫描**:`output_dir` / `work_dir` 下名为 `task_` 但 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 规范化 最后统一处理:单条 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: ```bash 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` 显式启动。