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

682 lines
29 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-说明)
- [依赖](#依赖)
- [常见问题](#常见问题)
---
## 功能特性
- **主页** `/`:上传入口(拖拽 / 选择文件多文件、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 两个 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 均可。
### 步骤
```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.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=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`~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 | `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
### 清理什么
| 产物 | 路径 | 何时产生 |
|---|---|---|
| 字幕输出 | `<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,互不叠加,远低于显存上限。
---
## 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~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` 显式启动。