add whisper

This commit is contained in:
2026-08-16 15:52:40 +08:00
parent 6e4d93cea6
commit 53cbcde543
6 changed files with 323 additions and 9 deletions

14
whisper/.env.example Normal file
View 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
View File

@@ -0,0 +1,86 @@
# Whisper 转写服务Speaches / faster-whisper
基于 [Speaches](https://github.com/speaches-ai/speaches)(原 faster-whisper-server
OpenAI 兼容语音转写服务,使用 faster-whisperCTranslate2推理比原版 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 倍,精度略低 |
L424 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`)。

View 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:

View 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
View 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"