add whisper
This commit is contained in:
@@ -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. 运维与备份
|
||||
|
||||
|
||||
14
whisper/.env.example
Normal file
14
whisper/.env.example
Normal file
@@ -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
|
||||
86
whisper/README.md
Normal file
86
whisper/README.md
Normal file
@@ -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`)。
|
||||
28
whisper/docker-compose.yml
Normal file
28
whisper/docker-compose.yml
Normal file
@@ -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:
|
||||
16
whisper/systemd/whisper-transcribe.service
Normal file
16
whisper/systemd/whisper-transcribe.service
Normal file
@@ -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
|
||||
118
whisper/verify.sh
Executable file
118
whisper/verify.sh
Executable file
@@ -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"
|
||||
Reference in New Issue
Block a user