From 53cbcde543edb323cc044c0a53ec1293af9eec17 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Sun, 16 Aug 2026 15:52:40 +0800 Subject: [PATCH] add whisper --- sentence_api/DEPLOYMENT.md | 70 ++++++++++-- whisper/.env.example | 14 +++ whisper/README.md | 86 +++++++++++++++ whisper/docker-compose.yml | 28 +++++ whisper/systemd/whisper-transcribe.service | 16 +++ whisper/verify.sh | 118 +++++++++++++++++++++ 6 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 whisper/.env.example create mode 100644 whisper/README.md create mode 100644 whisper/docker-compose.yml create mode 100644 whisper/systemd/whisper-transcribe.service create mode 100755 whisper/verify.sh diff --git a/sentence_api/DEPLOYMENT.md b/sentence_api/DEPLOYMENT.md index a8d1738..0a7d9fb 100644 --- a/sentence_api/DEPLOYMENT.md +++ b/sentence_api/DEPLOYMENT.md @@ -3,7 +3,9 @@ 本文部署以下两个进程: - `oral-trainer-api`:视频上传、管理后台、边界查询、Range 视频播放和朗读评分。 -- `MOSS-Transcribe-Diarize`:GPU 转写服务,只在服务器内网监听。 +- GPU 转写服务(二选一,只在服务器内网监听): + - `whisper/`:faster-whisper(Speaches),推荐,部署最简单,见第 2A 节。 + - `MOSS-Transcribe-Diarize`:带说话人分离,见第 2 节。 推荐 Ubuntu 22.04/24.04、Python 3.12、FFmpeg、NVIDIA GPU 和已安装的 NVIDIA 驱动。MOSS 0.9B 的实际显存占用受推理框架、并发和音频长度影响,生产环境建议从 16 GB 以上显存开始验证。 @@ -26,10 +28,56 @@ | 服务 | 监听地址 | 用途 | | --- | --- | --- | | MOSS | `127.0.0.1:8001` | 内部语音转写 | +| Whisper | `127.0.0.1:9000` | 内部语音转写(Whisper 方案,见第 2A 节) | | FastAPI | `127.0.0.1:8000` | 内部应用服务 | | Nginx | `0.0.0.0:443` | 对外 HTTPS | -不要把 MOSS 的 `8001` 端口直接暴露到公网。 +不要把 MOSS/Whisper 的转写端口直接暴露到公网。 + +## 2A. 部署 Whisper(推荐) + +基于 faster-whisper(Speaches)的 OpenAI 兼容转写服务,接口与 MOSS 相同 +(`POST /v1/audio/transcriptions`,支持 `verbose_json`),因此接入 `sentence_api` +时应用代码无需改动。部署文件在仓库的 [`whisper/`](../whisper/) 目录。 + +前置条件:Docker、NVIDIA 驱动(≥ 535,支持 CUDA 12.x)和 NVIDIA Container Toolkit。 +镜像自带 CUDA 12.6 运行时,宿主机不需要再装 CUDA。 + +```bash +sudo mkdir -p /opt/whisper +sudo chown "$USER":"$USER" /opt/whisper +cp -r whisper/* /opt/whisper/ +cd /opt/whisper + +cp .env.example .env # 按需修改 WHISPER_MODEL +docker compose up -d +docker compose logs -f whisper # 首次启动下载模型(约 3 GB) +``` + +验证: + +```bash +/opt/whisper/verify.sh +``` + +注册为 systemd 服务(可选,模板位于 `whisper/systemd/`): + +```bash +sudo cp whisper/systemd/whisper-transcribe.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now whisper-transcribe +sudo journalctl -u whisper-transcribe -f +``` + +接入 API 时,把 `.env` 改为: + +```dotenv +MOSS_TRANSCRIBE_URL=http://127.0.0.1:9000/v1/audio/transcriptions +MOSS_MODEL=Systran/faster-whisper-large-v3 +``` + +变量名沿用 `MOSS_*` 前缀,实际指向 Whisper 即可。模型选型与常见问题见 +[`whisper/README.md`](../whisper/README.md)。 ## 2. 部署 MOSS @@ -119,6 +167,10 @@ PUBLIC_BASE_URL=https://video_service.d1kt.cn MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions ``` +若使用 Whisper 方案(第 2A 节),把 `MOSS_TRANSCRIBE_URL` 改为 +`http://127.0.0.1:9000/v1/audio/transcriptions`,并把 `MOSS_MODEL` 设为 +`Systran/faster-whisper-large-v3`。 + 启动: ```bash @@ -126,10 +178,10 @@ docker compose up -d --build docker compose logs -f oral-trainer-api ``` -Compose 配置使用 Linux 的 host network,使容器可以访问只监听 -`127.0.0.1:8001` 的 MOSS,同时 API 也只监听 `127.0.0.1:8000`。如果在 +Compose 配置使用 Linux 的 host network,使容器可以访问只监听本机的 +转写服务(MOSS `127.0.0.1:8001` 或 Whisper `127.0.0.1:9000`),同时 API 也只监听 `127.0.0.1:8000`。如果在 Docker Desktop 上做本地测试,可移除 `network_mode: host`,恢复端口映射,并把 -MOSS 地址改为 `host.docker.internal`。 +转写服务地址改为 `host.docker.internal`。 检查: @@ -233,8 +285,8 @@ https://video_service.d1kt.cn/admin 1. 管理后台通过 raw body 流式写入临时文件并同步计算 SHA-256,不会先在系统临时目录保留完整副本。兼容的 multipart 接口仍以 4 MB 分块复制。 2. 保存到 `data/v/{sha256}.{extension}`。 3. 使用 FFmpeg 提取 16 kHz 单声道 WAV。 -4. 请求 MOSS 的 `verbose_json` 转写接口。 -5. 以 MOSS 时间戳生成句子边界,并计算每句原音有效语音时长。 +4. 请求转写服务(MOSS 或 Whisper)的 `verbose_json` 接口。 +5. 以转写时间戳生成句子边界,并计算每句原音有效语音时长。 6. 把视频和句子写入 SQLite,状态变为 `ready`。 查询处理状态: @@ -244,7 +296,7 @@ curl https://video_service.d1kt.cn/api/v1/videos curl https://video_service.d1kt.cn/api/v1/videos/{sha256} ``` -如果 MOSS 未配置,上传仍会使用原来的静音检测生成边界,但句子文本为空,无法开始测验。配置好 MOSS 后可在后台点击“重新处理”。 +如果转写服务未配置,上传仍会使用原来的静音检测生成边界,但句子文本为空,无法开始测验。配置好转写服务后可在后台点击“重新处理”。 ## 6. 视频随机播放 @@ -291,7 +343,7 @@ curl -X POST \ 总分不低于 `ASSESSMENT_PASS_SCORE`(默认 70)即通过。`pronunciation_score` 和 `prosody_score` 目前返回 `null`,避免把 ASR 内容匹配误报为音素发音质量。 -服务只向 MOSS 发送语言提示,不发送标准句子作为 prompt 或热词,避免标准答案诱导识别结果。 +服务只向转写服务发送语言提示,不发送标准句子作为 prompt 或热词,避免标准答案诱导识别结果。 ## 8. 运维与备份 diff --git a/whisper/.env.example b/whisper/.env.example new file mode 100644 index 0000000..d7e4b06 --- /dev/null +++ b/whisper/.env.example @@ -0,0 +1,14 @@ +# 模型选择: +# Systran/faster-whisper-large-v3 准确率最高(float16 约 6 GB 显存) +# Systran/faster-whisper-large-v3-turbo 速度快约 8 倍(float16 约 3 GB 显存) +WHISPER_MODEL=Systran/faster-whisper-large-v3 + +# 服务只监听本机,避免暴露公网 +WHISPER_PORT=9000 + +WHISPER_DEVICE=cuda +WHISPER_COMPUTE_TYPE=float16 +WHISPER_CPU_THREADS=4 + +# -1 表示模型常驻显存;空闲自动卸载可改为 600 +WHISPER_TTL=-1 diff --git a/whisper/README.md b/whisper/README.md new file mode 100644 index 0000000..d802d6c --- /dev/null +++ b/whisper/README.md @@ -0,0 +1,86 @@ +# Whisper 转写服务(Speaches / faster-whisper) + +基于 [Speaches](https://github.com/speaches-ai/speaches)(原 faster-whisper-server)的 +OpenAI 兼容语音转写服务,使用 faster-whisper(CTranslate2)推理,比原版 Whisper +更快、显存占用更低。提供 `POST /v1/audio/transcriptions` 接口,可直接替换 +`sentence_api` 里的 MOSS 转写地址,应用代码无需改动。 + +## 目录结构 + +```text +whisper/ + docker-compose.yml GPU 服务编排(ghcr.io/speaches-ai/speaches:latest-cuda) + .env.example 配置模板(模型、端口、计算类型) + systemd/whisper-transcribe.service systemd 模板 + verify.sh 部署验证脚本 +``` + +## 快速部署 + +前置条件:Ubuntu 22.04/24.04、Docker、NVIDIA 驱动(≥ 535,支持 CUDA 12.x)、 +NVIDIA Container Toolkit(安装方法见 `sentence_api/DEPLOYMENT.md` 或 NVIDIA 官方文档)。 +镜像自带 CUDA 12.6 运行时,宿主机不需要再装 CUDA。 + +```bash +sudo mkdir -p /opt/whisper +sudo chown "$USER":"$USER" /opt/whisper +cp -r whisper/* /opt/whisper/ +cd /opt/whisper + +cp .env.example .env # 按需修改 WHISPER_MODEL +docker compose up -d +docker compose logs -f whisper # 首次启动下载模型(约 3 GB),看到启动完成即可 +``` + +验证: + +```bash +/opt/whisper/verify.sh +``` + +## 注册为 systemd 服务 + +```bash +sudo cp whisper/systemd/whisper-transcribe.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now whisper-transcribe +sudo journalctl -u whisper-transcribe -f +``` + +模板假设代码位于 `/opt/whisper`,如路径不同请修改 +`WorkingDirectory`、`ExecStart`、`ExecStop` 三处。 + +## 接入 sentence_api + +在 `/opt/oral-trainer/sentence_api/.env` 中: + +```dotenv +MOSS_TRANSCRIBE_URL=http://127.0.0.1:9000/v1/audio/transcriptions +MOSS_MODEL=Systran/faster-whisper-large-v3 +``` + +然后重建 API 容器: + +```bash +cd /opt/oral-trainer/sentence_api +docker compose up -d --build +curl http://127.0.0.1:8000/healthz # moss_configured 应为 true +``` + +## 模型选择 + +| 模型 | 显存(float16) | 特点 | +| --- | --- | --- | +| `Systran/faster-whisper-large-v3` | 约 6 GB | 准确率最高,默认 | +| `Systran/faster-whisper-large-v3-turbo` | 约 3 GB | 快约 8 倍,精度略低 | + +L4(24 GB 显存)可同时常驻两个模型;Speaches 支持在请求的 `model` 参数里切换模型 +并自动加载。服务只监听 `127.0.0.1:9000`,请勿直接暴露公网。 + +## 常见问题 + +- 首次启动下载模型慢或失败:在 `docker-compose.yml` 中取消 `HF_ENDPOINT=https://hf-mirror.com` + 注释后 `docker compose up -d`。 +- 容器报 CUDA 错误:确认宿主机驱动 ≥ 535,并已安装 NVIDIA Container Toolkit。 +- 转写结果没有说话人:Whisper 不做说话人分离,需要该功能请改用 + `MOSS-Transcribe-Diarize`(见 `sentence_api/DEPLOYMENT.md`)。 diff --git a/whisper/docker-compose.yml b/whisper/docker-compose.yml new file mode 100644 index 0000000..a47fa8e --- /dev/null +++ b/whisper/docker-compose.yml @@ -0,0 +1,28 @@ +services: + whisper: + image: ghcr.io/speaches-ai/speaches:latest-cuda + container_name: whisper-transcribe + restart: unless-stopped + ports: + - "127.0.0.1:${WHISPER_PORT:-9000}:8000" + environment: + WHISPER__MODEL: ${WHISPER_MODEL:-Systran/faster-whisper-large-v3} + WHISPER__DEVICE: ${WHISPER_DEVICE:-cuda} + WHISPER__COMPUTE_TYPE: ${WHISPER_COMPUTE_TYPE:-float16} + WHISPER__CPU_THREADS: ${WHISPER_CPU_THREADS:-4} + # -1 表示模型常驻显存;改为正数(秒)可让空闲模型自动卸载 + WHISPER__TTL: ${WHISPER_TTL:--1} + # 服务器无法直连 Hugging Face 时取消注释(国内镜像): + # HF_ENDPOINT: https://hf-mirror.com + volumes: + - hf-hub-cache:/home/ubuntu/.cache/huggingface/hub + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + +volumes: + hf-hub-cache: diff --git a/whisper/systemd/whisper-transcribe.service b/whisper/systemd/whisper-transcribe.service new file mode 100644 index 0000000..7e60d33 --- /dev/null +++ b/whisper/systemd/whisper-transcribe.service @@ -0,0 +1,16 @@ +[Unit] +Description=Whisper transcription service (Speaches / faster-whisper) +Wants=docker.service network-online.target +After=docker.service network-online.target + +[Service] +Type=simple +WorkingDirectory=/opt/whisper +ExecStart=/usr/bin/docker compose --file /opt/whisper/docker-compose.yml up +ExecStop=/usr/bin/docker compose --file /opt/whisper/docker-compose.yml down +Restart=on-failure +RestartSec=10 +TimeoutStartSec=600 + +[Install] +WantedBy=multi-user.target diff --git a/whisper/verify.sh b/whisper/verify.sh new file mode 100755 index 0000000..ee04f64 --- /dev/null +++ b/whisper/verify.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# 验证 Whisper 转写服务: +# 1. 检查 GPU 与容器运行状态 +# 2. 用测试音频发起 verbose_json 转写请求,校验响应契约 +# 3. (可选)检查 oral-trainer-api 的集成状态 +# +# 用法: +# ./verify.sh +# ./verify.sh --file /path/to/speech.wav +# ./verify.sh --url http://127.0.0.1:9000 --model Systran/faster-whisper-large-v3 +# ./verify.sh --api-url http://127.0.0.1:8000 + +set -euo pipefail + +WHISPER_URL="${WHISPER_URL:-http://127.0.0.1:9000}" +WHISPER_MODEL="${WHISPER_MODEL:-Systran/faster-whisper-large-v3}" +TEST_FILE="" +API_URL="" + +usage() { + sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//' +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --url) WHISPER_URL="$2"; shift 2 ;; + --model) WHISPER_MODEL="$2"; shift 2 ;; + --file) TEST_FILE="$2"; shift 2 ;; + --api-url) API_URL="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "未知参数: $1"; usage; exit 1 ;; + esac +done + +echo "==> Whisper 地址: $WHISPER_URL" +echo "==> 模型: $WHISPER_MODEL" + +echo "==> 检查 GPU" +if command -v nvidia-smi >/dev/null 2>&1; then + nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader | head -5 +else + echo "警告: 未找到 nvidia-smi,请确认 NVIDIA 驱动与容器运行时已安装(容器需要 --gpus 支持)。" +fi + +echo "==> 检查容器状态" +if command -v docker >/dev/null 2>&1 && [[ -f docker-compose.yml ]]; then + docker compose ps --status running | sed -n '1,3p' +else + echo "警告: 未在当前目录发现 docker-compose.yml,跳过容器状态检查。" +fi + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +if [[ -n "$TEST_FILE" ]]; then + AUDIO_FILE="$TEST_FILE" +else + echo "==> 生成测试音频(3 秒 440Hz 正弦波)" + if ! command -v ffmpeg >/dev/null 2>&1; then + echo "错误: 未找到 ffmpeg,请安装或用 --file 指定真实语音文件。" >&2 + exit 1 + fi + AUDIO_FILE="$TMP_DIR/tone.wav" + ffmpeg -hide_banner -loglevel error -f lavfi \ + -i "sine=frequency=440:duration=3" -ar 16000 -ac 1 -y "$AUDIO_FILE" +fi + +if [[ ! -f "$AUDIO_FILE" ]]; then + echo "错误: 音频文件不存在: $AUDIO_FILE" >&2 + exit 1 +fi + +echo "==> 发起转写请求(verbose_json)" +START_TS="$(date +%s)" +RESPONSE="$(curl -sS --max-time 300 \ + -X POST "$WHISPER_URL/v1/audio/transcriptions" \ + -F "model=$WHISPER_MODEL" \ + -F "file=@$AUDIO_FILE" \ + -F "response_format=verbose_json" \ + -F "temperature=0")" +ELAPSED="$(( $(date +%s) - START_TS ))" + +echo "$RESPONSE" | python3 -c ' +import json, sys + +payload = json.load(sys.stdin) +text = payload.get("text") +segments = payload.get("segments") +assert isinstance(text, str), "响应缺少 text 字段" +assert isinstance(segments, list), "响应缺少 segments 字段(需要 verbose_json)" + +for index, segment in enumerate(segments): + start = segment.get("start") + end = segment.get("end") + if not (isinstance(start, (int, float)) and isinstance(end, (int, float)) and end > start >= 0): + raise AssertionError(f"segments[{index}] 缺少合法 start/end") + +print(f"OK: text={text!r}") +print(f"OK: segments={len(segments)} 条") +' +echo "==> 转写耗时: ${ELAPSED}s" + +if [[ -n "$API_URL" ]]; then + echo "==> 检查 API 集成 ($API_URL/healthz)" + curl -sS --max-time 15 "$API_URL/healthz" | python3 -c ' +import json, sys + +payload = json.load(sys.stdin) +configured = payload.get("moss_configured") +if configured is not True: + raise SystemExit(f"错误: moss_configured={configured!r},请检查 .env 中的 MOSS_TRANSCRIBE_URL") +print("OK: moss_configured=true") +' +fi + +echo "全部检查通过。建议再用真实语音文件复核转写质量:" +echo " $0 --file /path/to/speech.wav"