315 lines
9.4 KiB
Markdown
315 lines
9.4 KiB
Markdown
# 口语训练视频服务部署指南
|
||
|
||
本文部署以下两个进程:
|
||
|
||
- `oral-trainer-api`:视频上传、管理后台、边界查询、Range 视频播放和朗读评分。
|
||
- `MOSS-Transcribe-Diarize`:GPU 转写服务,只在服务器内网监听。
|
||
|
||
推荐 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` | 内部语音转写 |
|
||
| FastAPI | `127.0.0.1:8000` | 内部应用服务 |
|
||
| Nginx | `0.0.0.0:443` | 对外 HTTPS |
|
||
|
||
不要把 MOSS 的 `8001` 端口直接暴露到公网。
|
||
|
||
## 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://video_service.d1kt.cn
|
||
MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions
|
||
```
|
||
|
||
启动:
|
||
|
||
```bash
|
||
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`。如果在
|
||
Docker Desktop 上做本地测试,可移除 `network_mode: host`,恢复端口映射,并把
|
||
MOSS 地址改为 `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://video_service.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. 配置 Nginx 与 HTTPS
|
||
|
||
```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`:允许长视频处理和慢速上传。
|
||
|
||
签发证书:
|
||
|
||
```bash
|
||
sudo apt install -y certbot python3-certbot-nginx
|
||
sudo certbot --nginx -d video_service.d1kt.cn
|
||
```
|
||
|
||
证书完成后验证:
|
||
|
||
```bash
|
||
curl https://video_service.d1kt.cn/healthz
|
||
```
|
||
|
||
管理后台地址:
|
||
|
||
```text
|
||
https://video_service.d1kt.cn/admin
|
||
```
|
||
|
||
后台的管理密钥保存在当前浏览器的 `localStorage`,接口请求通过 `X-Admin-Key` 发送。
|
||
`CLIENT_API_KEY` 应通过 Android 构建配置注入 `OralTrainerSdkConfig.assessmentApiKey`,
|
||
不要硬编码进公开代码仓库。SDK 会在评分请求中发送 `X-Client-Key`。公开视频列表和播放
|
||
接口仍可交给 CDN 缓存,GPU 评分接口则受到密钥保护。
|
||
|
||
## 5. 上传与处理流程
|
||
|
||
后台上传后,服务会:
|
||
|
||
1. 管理后台通过 raw body 流式写入临时文件并同步计算 SHA-256,不会先在系统临时目录保留完整副本。兼容的 multipart 接口仍以 4 MB 分块复制。
|
||
2. 保存到 `data/v/{sha256}.{extension}`。
|
||
3. 使用 FFmpeg 提取 16 kHz 单声道 WAV。
|
||
4. 请求 MOSS 的 `verbose_json` 转写接口。
|
||
5. 以 MOSS 时间戳生成句子边界,并计算每句原音有效语音时长。
|
||
6. 把视频和句子写入 SQLite,状态变为 `ready`。
|
||
|
||
查询处理状态:
|
||
|
||
```bash
|
||
curl https://video_service.d1kt.cn/api/v1/videos
|
||
curl https://video_service.d1kt.cn/api/v1/videos/{sha256}
|
||
```
|
||
|
||
如果 MOSS 未配置,上传仍会使用原来的静音检测生成边界,但句子文本为空,无法开始测验。配置好 MOSS 后可在后台点击“重新处理”。
|
||
|
||
## 6. 视频随机播放
|
||
|
||
移动端播放地址为:
|
||
|
||
```text
|
||
GET /api/v1/videos/{sha256}/content
|
||
```
|
||
|
||
服务支持 HTTP Range,Media3/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://video_service.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 内容匹配误报为音素发音质量。
|
||
|
||
服务只向 MOSS 发送语言提示,不发送标准句子作为 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` 状态,可从后台执行“重新处理”。
|