Files
mediaplayer/sentence_api/DEPLOYMENT.md
2026-08-17 14:28:37 +08:00

457 lines
15 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.
# 口语训练视频服务部署指南
本文部署以下两个进程:
- `oral-trainer-api`视频上传、管理后台、边界查询、Range 视频播放和朗读评分。
- GPU 转写服务(二选一,只在服务器内网监听):
- `whisper/`faster-whisperSpeaches推荐部署最简单见第 2A 节。
- `MOSS-Transcribe-Diarize`:带说话人分离,见第 2 节。
推荐 Ubuntu 22.04/24.04、Python 3.12、FFmpeg、NVIDIA GPU 和已安装的 NVIDIA 驱动。MOSS 0.9B 的实际显存占用受推理框架、并发和音频长度影响,生产环境建议从 16 GB 以上显存开始验证。
## 1. 目录与端口
本文假设代码位于 `/opt/oral-trainer`
```text
/opt/oral-trainer/
sentence_api/
data/
v/ 上传的视频
work/ 临时转码文件
attempts/ 可选的学生录音
oral_trainer.sqlite3
```
端口规划:
| 服务 | 监听地址 | 用途 |
| --- | --- | --- |
| 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:80` | 对内 HTTP 回源HTTPS 由总出口终结) |
不要把 MOSS/Whisper 的转写端口直接暴露到公网。
## 2A. 部署 Whisper推荐
基于 faster-whisperSpeaches的 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 -a whisper/. /opt/whisper/ # 用 whisper/. 而不是 whisper/*,否则不会复制 .env.example 等隐藏文件
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
官方模型为 `OpenMOSS-Team/MOSS-Transcribe-Diarize`。CUDA 12 环境可使用官方当前说明中的 vLLM 构建:
```bash
sudo apt update
sudo apt install -y ffmpeg git curl
curl -LsSf https://astral.sh/uv/install.sh | sh
sudo mkdir -p /opt/moss-transcribe
sudo chown "$USER":"$USER" /opt/moss-transcribe
cd /opt/moss-transcribe
uv venv --python 3.12 .venv
. .venv/bin/activate
uv pip install -U vllm \
--torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129
```
启动模型:
```bash
. /opt/moss-transcribe/.venv/bin/activate
vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize \
--host 127.0.0.1 \
--port 8001 \
--trust-remote-code
```
生产环境可把它注册为 systemd 服务:
```ini
# /etc/systemd/system/moss-transcribe.service
[Unit]
Description=MOSS Transcribe Service
After=network-online.target
[Service]
Type=simple
User=oraltrainer
WorkingDirectory=/opt/moss-transcribe
ExecStart=/opt/moss-transcribe/.venv/bin/vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize --host 127.0.0.1 --port 8001 --trust-remote-code
Restart=on-failure
RestartSec=10
TimeoutStartSec=1800
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now moss-transcribe
sudo journalctl -u moss-transcribe -f
```
模型第一次启动会下载权重。CUDA 13 服务器也可以按照 MOSS 官方仓库说明改用 SGLang Omni。推理框架的安装地址可能随上游版本更新正式部署前应对照 [MOSS 官方仓库](https://github.com/OpenMOSS/MOSS-Transcribe-Diarize) 的 Quickstart。
验证:
```bash
curl -X POST http://127.0.0.1:8001/v1/audio/transcriptions \
-F model=OpenMOSS-Team/MOSS-Transcribe-Diarize \
-F file=@/path/to/test.wav \
-F response_format=verbose_json \
-F temperature=0
```
响应应包含 `text`;开启 `verbose_json` 后应包含带 `start``end``text``segments`
## 3. 部署 API
### Docker Compose 方式
```bash
cd /opt/oral-trainer/sentence_api
cp .env.example .env
openssl rand -hex 32
```
分别生成两个随机值,填入 `.env``ADMIN_API_KEY``CLIENT_API_KEY`,并确认:
```dotenv
PUBLIC_BASE_URL=https://videoservice.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
docker compose up -d --build
docker compose logs -f oral-trainer-api
```
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`,恢复端口映射,并把
转写服务地址改为 `host.docker.internal`
检查:
```bash
curl http://127.0.0.1:8000/healthz
```
`moss_configured` 应为 `true`。这里只表示地址已配置,实际连通性会在上传视频或评分时验证。
### 不使用 Docker
```bash
cd /opt/oral-trainer
python3.12 -m venv .venv-api
. .venv-api/bin/activate
python -m pip install -r sentence_api/requirements.txt
export ORAL_TRAINER_DATA_DIR=/opt/oral-trainer-data
export PUBLIC_BASE_URL=https://videoservice.d1kt.cn
export ADMIN_API_KEY='替换为随机密钥'
export MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions
python -m uvicorn sentence_api.main:app \
--host 127.0.0.1 --port 8000 --workers 1 \
--proxy-headers --forwarded-allow-ips='*'
```
不使用 Docker 时可创建 API 的 systemd 服务:
```ini
# /etc/systemd/system/oral-trainer-api.service
[Unit]
Description=Oral Trainer Video API
After=network-online.target moss-transcribe.service
[Service]
Type=simple
User=oraltrainer
WorkingDirectory=/opt/oral-trainer
EnvironmentFile=/opt/oral-trainer/sentence_api/.env
ExecStart=/opt/oral-trainer/.venv-api/bin/python -m uvicorn sentence_api.main:app --host 127.0.0.1 --port 8000 --workers 1 --proxy-headers --forwarded-allow-ips=*
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now oral-trainer-api
sudo journalctl -u oral-trainer-api -f
```
当前上传后的处理任务运行在 API 进程中,因此只能使用一个 Uvicorn worker。需要多机或多 worker 时,应把 `VideoProcessor.process` 迁移到 Celery、RQ 或其他持久化任务队列。
## 4. 配置 NginxHTTP 回源)
本机 Nginx 只监听 80 端口提供 HTTP不在本机终结 HTTPS。总出口线上边缘网关
`https://videoservice.d1kt.cn` 终结 TLS 证书后,把请求以 HTTP 转发到本机
内网 IP 的 80 端口。因此本机不需要证书,也不需要安装 certbot。
```bash
sudo cp sentence_api/nginx.conf.example /etc/nginx/sites-available/oral-trainer
sudo ln -s /etc/nginx/sites-available/oral-trainer /etc/nginx/sites-enabled/oral-trainer
sudo nginx -t
sudo systemctl reload nginx
```
配置文件中的关键项:
- `client_max_body_size 12g`:允许超过 1 GB 的视频。
- `proxy_request_buffering off`:上传时直接流向 FastAPI避免 Nginx 再完整缓存一份。
- `proxy_force_ranges on`:保留 Android 随机拖动播放所需的 HTTP Range。
- `proxy_read_timeout 7200s`:允许长视频处理和慢速上传。
- `X-Forwarded-Proto`透传边缘网关标记的原始协议https让 API 在需要时能识别
用户实际走的 HTTPS若网关不设置该头可把配置改回 `$scheme`
本地验证(走本机 Nginx
```bash
curl -H 'Host: videoservice.d1kt.cn' http://127.0.0.1/healthz
```
公网验证(经总出口映射,需先在网关配置好 `https://videoservice.d1kt.cn`
到本机内网 IP:80 的映射):
```bash
curl https://videoservice.d1kt.cn/healthz
```
证书的申请与续期都在总出口完成,本机无需任何证书配置。
管理后台地址:
```text
https://videoservice.d1kt.cn/admin
```
后台的管理密钥保存在当前浏览器的 `localStorage`,接口请求通过 `X-Admin-Key` 发送。
`CLIENT_API_KEY` 应通过 Android 构建配置注入 `OralTrainerSdkConfig.assessmentApiKey`
不要硬编码进公开代码仓库。SDK 会在评分请求中发送 `X-Client-Key`。公开视频列表和播放
接口仍可交给 CDN 缓存GPU 评分接口则受到密钥保护。
### 4A. 如果要在服务器本机终结 HTTPS可选
若总出口不做 TLS 终结,而是由服务器本机 nginx 提供 HTTPS443 的 `server` 块必须
包含与 80 块相同的上传相关指令,否则大文件上传会被掐断:
```nginx
server {
listen 443 ssl;
server_name videoservice.d1kt.cn;
ssl_certificate /etc/letsencrypt/live/videoservice.d1kt.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/videoservice.d1kt.cn/privkey.pem;
client_max_body_size 12g;
client_body_timeout 7200s;
send_timeout 7200s;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_request_buffering off;
proxy_read_timeout 7200s;
proxy_send_timeout 7200s;
}
location ~ "^/api/v1/videos/[0-9a-fA-F]{64}/content$" {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_force_ranges on;
proxy_buffering off;
}
}
```
签发证书并应用:
```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d videoservice.d1kt.cn
```
### 4B. 上传中断ClientDisconnect排查
后台日志出现 `starlette.requests.ClientDisconnect` 表示浏览器到后端之间某处的连接在
请求体传完之前被断开。先确认实际生效的 nginx 配置:
```bash
sudo nginx -T | grep -E "client_max_body_size|client_body_timeout|proxy_request_buffering"
```
最常见的原因是新加的 HTTPS `server` 块漏掉了上传指令nginx 默认
`client_max_body_size` 只有 1 MB超过会直接返回 413 并断开连接,后端表现为
`ClientDisconnect`。其他常见原因:
- 总出口(网关/CDN有更小的请求体大小或更短的上传超时需在网关侧放行。
- `client_body_timeout`(默认 60s过短网络慢时上传暂停过久会被掐断按上文设
为 7200s。
- 浏览器/客户端主动取消或网络中断。
绕过网关在本机回源验证(能通则问题在网关):
```bash
curl -X PUT -H 'Host: videoservice.d1kt.cn' -H "X-Admin-Key: $ADMIN_KEY" \
--data-binary @large.mp4 \
'http://127.0.0.1/api/v1/admin/videos/raw?filename=large.mp4' \
-o /dev/null -w '%{http_code}\n'
```
API 已对 `ClientDisconnect` 做优雅处理:连接中断时返回 400 并记录一条 WARNING
不再产生 500 堆栈日志。
## 5. 上传与处理流程
后台上传后,服务会:
1. 管理后台通过 raw body 流式写入临时文件并同步计算 SHA-256不会先在系统临时目录保留完整副本。兼容的 multipart 接口仍以 4 MB 分块复制。
2. 保存到 `data/v/{sha256}.{extension}`
3. 使用 FFmpeg 提取 16 kHz 单声道 WAV。
4. 请求转写服务MOSS 或 Whisper`verbose_json` 接口。
5. 以转写时间戳生成句子边界,并计算每句原音有效语音时长。
6. 把视频和句子写入 SQLite状态变为 `ready`
查询处理状态:
```bash
curl https://videoservice.d1kt.cn/api/v1/videos
curl https://videoservice.d1kt.cn/api/v1/videos/{sha256}
```
如果转写服务未配置,上传仍会使用原来的静音检测生成边界,但句子文本为空,无法开始测验。配置好转写服务后可在后台点击“重新处理”。
## 6. 视频随机播放
移动端播放地址为:
```text
GET /api/v1/videos/{sha256}/content
```
服务支持 HTTP RangeMedia3/ExoPlayer 可以随机 seek并继续使用 SDK 现有的本地缓存。第一阶段不必强制改成 HLS。
上传前建议把 MP4 处理成 H.264/AAC 并把 `moov` 元数据移动到文件头:
```bash
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
```
如果原始编码不被 Android 广泛支持,再转码:
```bash
ffmpeg -i input.mkv \
-c:v libx264 -preset medium -crf 22 \
-c:a aac -b:a 128k -movflags +faststart output.mp4
```
当并发量明显增长或网络波动较大时,再增加 HLS/DASH 转码和对象存储/CDN。
## 7. 朗读评分接口
```bash
curl -X POST \
https://videoservice.d1kt.cn/api/v1/videos/{sha256}/sentences/0/assessments \
-H 'X-Client-Key: 替换为客户端密钥' \
-F audio=@student.wav \
-F language=en
```
当前 `asr-fluency-v1` 评分为:
```text
总分 = 内容分 * 80% + 流畅度 * 20%
流畅度 = 有效语音时长分 * 35% + 停顿分 * 40% + 语速分 * 25%
```
总分不低于 `ASSESSMENT_PASS_SCORE`(默认 70即通过。`pronunciation_score``prosody_score` 目前返回 `null`,避免把 ASR 内容匹配误报为音素发音质量。
服务只向转写服务发送语言提示,不发送标准句子作为 prompt 或热词,避免标准答案诱导识别结果。
## 8. 运维与备份
需要持久化备份:
```text
sentence_api/data/v/
sentence_api/data/oral_trainer.sqlite3
```
`work/` 可以清理。`KEEP_ATTEMPT_AUDIO=false` 时学生录音在评分完成后自动删除;设为 `true` 时还需备份和定期清理 `attempts/`,并在产品隐私政策中说明保存期限。
SQLite 在线备份示例:
```bash
sqlite3 sentence_api/data/oral_trainer.sqlite3 \
".backup '/backup/oral_trainer-$(date +%F).sqlite3'"
```
升级前先备份数据库和 `data/v/`。API 重启期间被中断的视频会保留 `processing` 状态,可从后台执行“重新处理”。
## 9. 更新与回滚
日常代码更新、镜像升级与回滚流程见 [`UPDATE.md`](UPDATE.md)。