From 5f6a24211439ff940c338ff14c0704d0348a0d56 Mon Sep 17 00:00:00 2001 From: audio2text dev Date: Mon, 6 Jul 2026 22:59:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E5=88=86=E5=B1=82=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20=E2=80=94=20=E4=B8=BB=E9=A1=B5=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E5=88=B0=20150=20=E8=A1=8C=20+=206=20=E4=B8=AA=E5=AD=90?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原 883 行单体 README 信息密度过高且重复(配置差异表出现 2 次、缓存说明 散落多处)。按主题拆分: 主页 README.md (150行): - 一句话简介 + 功能特性(精简) + 架构(目录树+数据流) + 快速开始 - 文档索引表(链接到 6 个子文档,每行一句话说明) - 入口地址表 + 依赖(精简) docs/ 子文档(原样搬运,不重写): - DEPLOYMENT.md (170行) CPU/GPU 部署、模型选型、CPU↔GPU 切换 - CONFIG.md (187行) 配置差异表、完整字段表、配置示例 - DOCKER.md (185行) 构建/重建/缓存分层/until根因/Volume - API.md (70行) HTTP接口表、分片上传协议、示例 - ARCHITECTURE.md(140行) 断句算法、显存策略、GPU优化、缓存清理 - FAQ.md (42行) 6 条常见问题 每个子文档顶部加「← 返回主页」链接,相关处加交叉引用 (如 DEPLOYMENT 提到缓存时链接 DOCKER.md)。无内容丢失。 --- README.md | 825 +++---------------------------------------- docs/API.md | 70 ++++ docs/ARCHITECTURE.md | 140 ++++++++ docs/CONFIG.md | 187 ++++++++++ docs/DEPLOYMENT.md | 170 +++++++++ docs/DOCKER.md | 185 ++++++++++ docs/FAQ.md | 42 +++ 7 files changed, 840 insertions(+), 779 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CONFIG.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/DOCKER.md create mode 100644 docs/FAQ.md diff --git a/README.md b/README.md index 376c184..471d15f 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,17 @@ --- -## 目录 - -- [功能特性](#功能特性) -- [架构](#架构) -- [部署:CPU 开发环境](#部署cpu-开发环境) -- [部署:GPU 生产环境](#部署gpu-生产环境) -- [配置文件说明](#配置文件说明) -- [缓存清理与定时任务](#缓存清理与定时任务) -- [HTTP 接口](#http-接口) -- [断句与时间戳重算原理](#断句与时间戳重算原理) -- [模型不共驻(显存策略)](#模型不共驻显存策略) -- [Docker 说明](#docker-说明) - - [新建 / 重建容器](#新建--重建容器) - - [缓存分层与删除边界](#缓存分层与删除边界) - - [⚠️ 不要用 `docker builder prune --filter until`](#-不要用-docker-builder-prune---filter-until) -- [GPU 利用率优化](#gpu-利用率优化) -- [依赖](#依赖) -- [常见问题](#常见问题) - ---- - ## 功能特性 -- **主页** `/`:上传入口(拖拽 / 选择文件,多文件、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 保护。 +- **网页上传**:拖拽 / 选择文件,多文件并发、4 MiB 分片、断点续传 +- **双语字幕**:英文在上、中文在下,亦可单独下载英文 / 中文字幕 +- **faster-whisper 转写**:词级时间戳,断句精确(取首末词时间戳) +- **NLLB-200 英译中**:ASR 与翻译模型不共驻,翻译时独占显存跑大 batch +- **任务状态机**:`queued → uploading → extracting → transcribing → segmenting → translating → done` +- **实时日志页**:按级别分层(debug=详细子步骤 / info=阶段转换 / error=完整 traceback) +- **定时缓存清理**:任务产物默认保留 7 天,超期连同 DB 记录一并删除 +- **SQLite 持久化**(自包含,无需外部 DB) +- `/docs`(Swagger UI)受 Basic Auth 保护 --- @@ -64,7 +36,7 @@ audio2text/ │ └── prefetch_models.{sh,py} # 预拉模型权重到 ./models volume(避免首次启动下载) ├── requirements.txt ├── config.example.yaml # 配置模板(复制为 config.yaml 后填值) -├── README.md +├── docs/ # 详细文档(见下方索引) └── app/ ├── main.py # FastAPI 应用工厂 ├── config.py # 从 config.yaml 加载的类型化 Settings(pydantic) @@ -81,6 +53,7 @@ audio2text/ │ ├── segmenter.py # 断句 + 时间戳重算(纯算法,零模型依赖) │ ├── translate_service.py # NLLB 翻译 │ ├── model_manager.py # 模型加载/卸载(不共驻核心) + │ ├── scheduler.py # ffmpeg 串行队列 + GPU 调度线程 │ ├── pipeline.py # 编排:提取→识别→断句→翻译→写SRT │ ├── srt_writer.py # SRT 写入 + 双语合并 │ ├── log_buffer.py # 内存日志缓冲(供 /logs 页面查询) @@ -107,777 +80,71 @@ audio2text/ upload_router ──► upload_service ──► UploadSession(SQLite) + 分片落盘 │ complete ▼ -创建 Task(queued) ──► pipeline 后台线程 +创建 Task(queued) ──► scheduler │ - ├─ 1. ffmpeg_service.extract_audio → 16k mono wav + ├─ ffmpeg 串行队列(最多 1 个并发,其余排队)→ 16k mono wav │ (按配置删原始视频) - ├─ 2. model_manager.get_asr → asr_service.transcribe → segments(带词级时间戳) - ├─ 3. segmenter.resegment → 规范字幕条目(精确/估算两路) - ├─ 4. model_manager.unload_asr → get_translator + ├─ GPU 调度线程(单线程,模型复用): + │ get_asr → asr_service.transcribe → segments(带词级时间戳) + │ unload_asr → get_translator │ translate_service.translate → 中文译文(独占显存大 batch) - └─ 5. srt_writer → en.srt / zh.srt / bilingual.srt + └─ srt_writer → en.srt / zh.srt / bilingual.srt 更新 Task(done) + 写 output_dir ``` --- -## 部署:CPU 开发环境 +## 快速开始 -CPU 模式用于本地开发与流程验证,模型选同系列最小尺寸,2GB 内存开发机即可跑通完整流程。 - -### 前置要求 - -- Docker(用于构建镜像 + 运行容器) -- 约 500 MB 磁盘(模型缓存)+ 上传视频空间 - -CPU 模式**不需要** NVIDIA 驱动,普通 Linux / macOS / WSL 均可。 - -### 步骤 +### CPU 开发环境(2GB 内存即可) ```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 # 构建镜像 + 生成 config.yaml +./start.sh # 启动容器(端口 8000) ``` -`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,之后容器启动 -即用、无需联网: +### GPU 生产环境(需 NVIDIA GPU + nvidia runtime) ```bash -./scripts/prefetch_models.sh # 读 config.yaml(当前激活配置) -./scripts/prefetch_models.sh config.gpu.yaml # 读指定配置(如切换到 GPU 前预拉大模型) +AUDIO2TEXT_VARIANT=gpu ./setup.sh +./start.sh # 自动检测 GPU 镜像 + nvidia-smi,端口 8001 ``` -脚本用已构建的镜像跑一次性容器,读配置里的 `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 内存)。 - -### 自定义端口 - -```bash -AUDIO2TEXT_PORT=9000 ./start.sh -``` +启动后打开 `http://127.0.0.1:8000/`,拖入视频即可。详细部署流程、模型选型、CPU↔GPU 切换 +见 [部署指南](./docs/DEPLOYMENT.md)。 --- -## 部署: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=32`), -两者峰值显存互不叠加,远低于 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 # 重新启动 -``` - -两套配置的差异仅在 8 项(其余字段完全一致): - -| 字段 | CPU(`config.cpu.yaml`) | GPU(`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` | - -### 启动后的入口 - -两种模式通用: +## 入口 | 入口 | 地址 | |---|---| -| 主页 | `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/` | +| 历史任务 | `http://127.0.0.1:8000/history` | +| 实时日志 | `http://127.0.0.1:8000/logs` | +| API 文档 | `http://127.0.0.1:8000/docs`(Basic Auth) | | 健康检查 | `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)两份配置完全一致,仅以下 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.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` | 过滤静音段,提升识别质量与速度 | -| `batch_size` | int | `8`(CPU)/ `32`(GPU) | `BatchedInferencePipeline` 批量解码的音频块数。GPU 拉大 batch 拉长单次 GPU 解码时间,掩盖 CPU 提取 Mel 特征的间隙,提升平均利用率 | -| `beam_size` | int | `5`(CPU)/ `2`(GPU) | 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`(~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 | `8`(CPU)/ `32`(GPU) | 翻译批量大小。不与 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 | - -### 配置示例 - -```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 - batch_size: 8 # GPU: 32(拉长单次 GPU 解码,掩盖 CPU 特征提取间隙) - beam_size: 5 # GPU: 2(turbo 鲁棒可降,候选数↓解码步数↓) - -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: - -### 清理什么 - -| 产物 | 路径 | 何时产生 | -|---|---|---| -| 字幕输出 | `/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,互不叠加,远低于显存上限。 - ---- - -## 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.6GB,3090 24G 充裕 | -| `asr.beam_size` | 5 | **2** | 解码候选数 5→2,每步计算量与解码步数下降 → 峰更密、间隙更短。turbo 鲁棒,保留 1 个候选做歧义发音保险,质量损失小 | - -### 为什么不关 `word_timestamps` - -`segmenter.py` 强依赖词级时间戳做精确断句——只要任一 segment 没词级时间戳,就整体退化 -到匀速估算路(时间戳按字符数比例估算),字幕精度下降明显。所以 `word_timestamps=true` -必须保留,即使它是 CPU↔GPU 同步开销的来源之一。 - -### 验证方法 - -```bash -# 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`。 - -#### 首次新建(新机器 / 全新拉取代码后) - -```bash -# 1. 构建镜像 + 生成 config.yaml(CPU 默认) -./setup.sh -# GPU:AUDIO2TEXT_VARIANT=gpu ./setup.sh - -# 2. 启动容器 -./start.sh -# GPU:start.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)。 - -```bash -# 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` 是只读挂载,改完重启容器即生效,**无需重建镜像**: - -```bash -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,二次构建会快 -很多。 - -```bash -# 编辑 requirements.txt 后 -./setup.sh # 或 docker build --build-arg VARIANT=gpu -t audio2text:gpu . -./stop.sh && ./start.sh -``` - -#### docker compose(替代脚本) - -```bash -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)。 - -正确做法: - -```bash -# ✅ 想清理磁盘、释放 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](https://github.com/moby/buildkit/issues/2414)。 - -#### 什么时候需要主动清缓存 - -- **磁盘紧张**:`docker builder prune -f` + `docker image prune` 释放空间 -- **依赖换了 torch / CUDA 大版本**:BuildKit 可能复用了不兼容的旧 wheel,清 pip 缓存 - mount 强制重下:`docker builder prune -f --filter "type=exec.cachemount"` -- **换 VARIANT(cpu↔gpu)**:不需要清——两条构建链独立,缓存互不干扰 -- **想从零验证构建**:`docker builder prune -af` 清全部,模拟新机器首次构建 - -#### 模型缓存(`./models`) - -模型权重在 `./models` volume(容器内 `HF_HOME=/models/huggingface`、 -`CT2_CACHE=/models/ctranslate2`),跨容器复用。首次启动下载,之后秒起。 - -```bash -# 查看模型缓存大小 -du -sh ./models - -# 删了强制重下(GPU 大模型 ~5.5GB,建议用 prefetch 脚本提前下好) -rm -rf ./models -./scripts/prefetch_models.sh config.gpu.yaml -``` - -### Volume 挂载 - -| 容器路径 | 宿主路径 | 用途 | 删除影响 | -|---|---|---|---| -| `/data` | `./data`(CPU)/ `./data-gpu`(GPU) | 上传视频、中间音频、输出字幕、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"` 验证。 +## 文档索引 + +详细文档按主题拆分,主页只保留核心速览: + +| 文档 | 内容 | +|---|---| +| [部署指南](./docs/DEPLOYMENT.md) | CPU / GPU 完整部署流程、前置要求、模型选型、CPU↔GPU 切换、自定义端口 | +| [配置文件说明](./docs/CONFIG.md) | CPU/GPU 配置差异表、全部字段说明(server/storage/asr/translation/...)、配置示例 | +| [Docker 说明](./docs/DOCKER.md) | 镜像构建、新建/重建/改配置/改依赖四种场景、**缓存分层与删除边界**、⚠️ until filter 失效根因、Volume 挂载 | +| [HTTP 接口](./docs/API.md) | 接口一览表、分片上传协议、请求/响应示例 | +| [架构与原理](./docs/ARCHITECTURE.md) | 断句算法、模型不共驻显存策略、GPU 利用率优化、缓存清理机制 | +| [常见问题](./docs/FAQ.md) | CPU 跑 NLLB、模型下载、断点续传、保留原始视频、自动清理等 | --- ## 依赖 -### Python(`requirements.txt`) +- **Python**:FastAPI + uvicorn + SQLAlchemy + faster-whisper + transformers(torch 按 VARIANT 分叉,CPU/GPU 装不同 wheel)。完整列表见 `requirements.txt` +- **系统**:ffmpeg(镜像内 apt 装)、patchelf(修复 ctranslate2 可执行栈)。GPU 需宿主 NVIDIA 驱动 + nvidia container runtime -| 包 | 用途 | -|---|---| -| `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` 显式启动。 +镜像构建与依赖安装细节见 [Docker 说明](./docs/DOCKER.md)。 diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..26b49d6 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,70 @@ +← [返回主页](../README.md) + +# 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` | 无 | 清空日志缓冲 | + +--- + +## 分片上传协议 + +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 +``` + +健康检查(含设备与模型配置): + +```bash +curl -s http://127.0.0.1:8001/health | python -m json.tool +# → {"status":"ok","cuda_available":true,"gpu":"NVIDIA GeForce RTX 3090", +# "asr_model":"large-v3-turbo","asr_batch_size":32,"asr_beam_size":2,...} +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..5437a5d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,140 @@ +← [返回主页](../README.md) + +# 架构与原理 + +本文档覆盖核心设计原理:断句算法、模型不共驻显存策略、GPU 利用率优化、缓存清理机制。 + +--- + +## 断句与时间戳重算原理 + +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,互不叠加,远低于显存上限。 + +--- + +## 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.6GB,3090 24G 充裕 | +| `asr.beam_size` | 5 | **2** | 解码候选数 5→2,每步计算量与解码步数下降 → 峰更密、间隙更短。turbo 鲁棒,保留 1 个候选做歧义发音保险,质量损失小 | + +### 为什么不关 `word_timestamps` + +`segmenter.py` 强依赖词级时间戳做精确断句——只要任一 segment 没词级时间戳,就整体退化 +到匀速估算路(时间戳按字符数比例估算),字幕精度下降明显。所以 `word_timestamps=true` +必须保留,即使它是 CPU↔GPU 同步开销的来源之一。 + +### 验证方法 + +```bash +# 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 对比字幕确认无歧义发音处的降级。 + +--- + +## 缓存清理与定时任务 + +每个任务落盘的产物(字幕、中间音频、保留的原始视频)会持续占用磁盘。容器内置定时 +清理(`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 部署下无并发写入压力。 diff --git a/docs/CONFIG.md b/docs/CONFIG.md new file mode 100644 index 0000000..caa24a3 --- /dev/null +++ b/docs/CONFIG.md @@ -0,0 +1,187 @@ +← [返回主页](../README.md) + +# 配置文件说明 + +项目预置两份配置文件,`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 做类型校验,缺字段时回退默认值。 + +部署流程见 [部署指南](./DEPLOYMENT.md)。 + +--- + +## 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.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` | 过滤静音段,提升识别质量与速度 | +| `batch_size` | int | `8`(CPU)/ `32`(GPU) | `BatchedInferencePipeline` 批量解码的音频块数。GPU 拉大 batch 拉长单次 GPU 解码时间,掩盖 CPU 提取 Mel 特征的间隙,提升平均利用率 | +| `beam_size` | int | `5`(CPU)/ `2`(GPU) | 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`(~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 | `8`(CPU)/ `32`(GPU) | 翻译批量大小。不与 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 | + +--- + +## 配置示例 + +```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 + batch_size: 8 # GPU: 32(拉长单次 GPU 解码,掩盖 CPU 特征提取间隙) + beam_size: 5 # GPU: 2(turbo 鲁棒可降,候选数↓解码步数↓) + +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" +``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..eba7098 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,170 @@ +← [返回主页](../README.md) + +# 部署指南 + +CPU 开发 / GPU 生产同一份代码,仅靠 `AUDIO2TEXT_VARIANT` 切换镜像 + 配置。 +本文档覆盖两种环境的完整部署流程、模型选型与切换方法。 + +构建 / 重建镜像的 Docker 操作细节见 [Docker 说明](./DOCKER.md); +配置字段含义见 [配置文件说明](./CONFIG.md)。 + +--- + +## 部署: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/`,拖入视频或音频文件即可。 + +### 预拉模型(避免首次启动卡在下载) + +容器首次处理任务时会从 HuggingFace 下载模型,大模型(GPU 的 large-v3-turbo ~3GB + +NLLB-1.3B ~2.5GB)下载耗时较长。可用预拉脚本提前下好到 `./models` volume,之后容器启动 +即用、无需联网: + +```bash +./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 内存)。 + +### 自定义端口 + +```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=32`), +两者峰值显存互不叠加,远低于 24G 上限。模型缓存(`./models` volume)跨容器复用, +CPU→GPU 切换时 NLLB/Whisper 大模型首次下载、之后秒起。 + +GPU 利用率调优(batch_size / beam_size 选择依据)见 +[架构与原理 - GPU 利用率优化](./ARCHITECTURE.md#gpu-利用率优化)。 + +### 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 # 重新启动 +``` + +两套配置的差异仅在 8 项(其余字段完全一致),完整对照见 +[配置文件说明 - CPU/GPU 差异](./CONFIG.md#cpu--gpu-两份配置的差异)。 + +### 启动后的入口 + +两种模式通用: + +| 入口 | 地址 | +|---|---| +| 主页 | `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` | + +### 验证 GPU 配置生效 + +```bash +curl -s http://127.0.0.1:8001/health | python -m json.tool +# 应见 cuda_available=true, gpu="NVIDIA GeForce RTX 3090", +# asr_batch_size=32, asr_beam_size=2, asr_model=large-v3-turbo +``` diff --git a/docs/DOCKER.md b/docs/DOCKER.md new file mode 100644 index 0000000..7e586b6 --- /dev/null +++ b/docs/DOCKER.md @@ -0,0 +1,185 @@ +← [返回主页](../README.md) + +# Docker 说明 + +一份 Dockerfile 出 CPU / GPU 两个镜像,依赖层缓存复用,改代码秒级重建。本文档覆盖 +构建、重建、缓存管理与 Volume 挂载。部署流程见 [部署指南](./DEPLOYMENT.md)。 + +--- + +## 一份 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`。 + +安全约束:PyTorch CPU wheel 与 GPU(CUDA) wheel 是两个不兼容二进制包,CPU 版 +`torch.cuda.is_available()=False`,GPU 版 `=True`。torch 必须按 VARIANT 分叉装不同 wheel, +绝不能跨 variant 共享依赖层。deps 阶段用 `FROM base-${VARIANT}`,CPU/GPU 是两条独立 +构建链,各自装对应 torch。 + +--- + +## 新建 / 重建容器 + +项目提供 `setup.sh` / `start.sh` / `stop.sh` 包装脚本,也可直接用 `docker` / `docker compose`。 + +### 首次新建(新机器 / 全新拉取代码后) + +```bash +# 1. 构建镜像 + 生成 config.yaml(CPU 默认) +./setup.sh +# GPU:AUDIO2TEXT_VARIANT=gpu ./setup.sh + +# 2. 启动容器 +./start.sh +# GPU:start.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)。 + +```bash +# 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` 是只读挂载,改完重启容器即生效,**无需重建镜像**: + +```bash +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,二次构建会快 +很多。 + +```bash +# 编辑 requirements.txt 后 +./setup.sh # 或 docker build --build-arg VARIANT=gpu -t audio2text:gpu . +./stop.sh && ./start.sh +``` + +### docker compose(替代脚本) + +```bash +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)。 + +正确做法: + +```bash +# ✅ 想清理磁盘、释放 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](https://github.com/moby/buildkit/issues/2414)。 + +### 什么时候需要主动清缓存 + +- **磁盘紧张**:`docker builder prune -f` + `docker image prune` 释放空间 +- **依赖换了 torch / CUDA 大版本**:BuildKit 可能复用了不兼容的旧 wheel,清 pip 缓存 + mount 强制重下:`docker builder prune -f --filter "type=exec.cachemount"` +- **换 VARIANT(cpu↔gpu)**:不需要清——两条构建链独立,缓存互不干扰 +- **想从零验证构建**:`docker builder prune -af` 清全部,模拟新机器首次构建 + +### 模型缓存(`./models`) + +模型权重在 `./models` volume(容器内 `HF_HOME=/models/huggingface`、 +`CT2_CACHE=/models/ctranslate2`),跨容器复用。首次启动下载,之后秒起。 + +```bash +# 查看模型缓存大小 +du -sh ./models + +# 删了强制重下(GPU 大模型 ~5.5GB,建议用 prefetch 脚本提前下好) +rm -rf ./models +./scripts/prefetch_models.sh config.gpu.yaml +``` + +--- + +## Volume 挂载 + +| 容器路径 | 宿主路径 | 用途 | 删除影响 | +|---|---|---|---| +| `/data` | `./data`(CPU)/ `./data-gpu`(GPU) | 上传视频、中间音频、输出字幕、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"` 验证。 diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..baf77fc --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,42 @@ +← [返回主页](../README.md) + +# 常见问题 + +--- + +### 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` 显式启动。