added moss

This commit is contained in:
2026-09-11 09:14:51 +08:00
parent 2b663d50ad
commit 07261d092b
8 changed files with 669 additions and 9 deletions

View File

@@ -83,6 +83,7 @@ pip install -r requirements.txt
| `N` / `P` | 下一首 / 上一首 | | `N` / `P` | 下一首 / 上一首 |
| `[` / `]` | 减速 / 加速(0.5x - 2.0x) | | `[` / `]` | 减速 / 加速(0.5x - 2.0x) |
| `L` | 切换循环模式(关 / 单曲 / 列表) | | `L` | 切换循环模式(关 / 单曲 / 列表) |
| 双击画面 | 进入 / 退出全屏 |
| `F` | 视频全屏切换 | | `F` | 视频全屏切换 |
| `Esc` | 退出全屏 | | `Esc` | 退出全屏 |
| `Ctrl+O` | 打开文件(macOS 亦支持 `Command+O`) | | `Ctrl+O` | 打开文件(macOS 亦支持 `Command+O`) |

View File

@@ -10,6 +10,7 @@
常用快捷键: 常用快捷键:
Space 播放 / 暂停 Space 播放 / 暂停
← / → 后退 / 前进 10 秒 ← / → 后退 / 前进 10 秒
双击画面 进入 / 退出全屏
Shift+← / → 后退 / 前进 60 秒 Shift+← / → 后退 / 前进 60 秒
↑ / ↓ 音量 +5 / -5 ↑ / ↓ 音量 +5 / -5
Ctrl+↑ / ↓ 快进步长 +1 / -1 秒(macOS 亦支持 Command+↑ / ↓) Ctrl+↑ / ↓ 快进步长 +1 / -1 秒(macOS 亦支持 Command+↑ / ↓)
@@ -991,6 +992,7 @@ class MediaPlayerApp(tk.Tk):
toolbar = ttk.Frame(self, padding=(8, 8)) toolbar = ttk.Frame(self, padding=(8, 8))
toolbar.pack(fill="x") toolbar.pack(fill="x")
self.toolbar = toolbar
ttk.Button(toolbar, text="打开文件…", command=self.open_files, takefocus=0).pack(side="left", padx=(0, 4)) ttk.Button(toolbar, text="打开文件…", command=self.open_files, takefocus=0).pack(side="left", padx=(0, 4))
ttk.Button(toolbar, text="打开文件夹…", command=self.open_folder, takefocus=0).pack(side="left", padx=(0, 8)) ttk.Button(toolbar, text="打开文件夹…", command=self.open_folder, takefocus=0).pack(side="left", padx=(0, 8))
@@ -1027,6 +1029,7 @@ class MediaPlayerApp(tk.Tk):
main.pack(fill="both", expand=True) main.pack(fill="both", expand=True)
main.columnconfigure(0, weight=1) main.columnconfigure(0, weight=1)
main.rowconfigure(0, weight=1) main.rowconfigure(0, weight=1)
self.main = main
self.video_frame = tk.Frame(main, bg="#101010") self.video_frame = tk.Frame(main, bg="#101010")
self.video_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 8)) self.video_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
@@ -1038,6 +1041,7 @@ class MediaPlayerApp(tk.Tk):
hint_text = "打开文件开始播放\n(视频将在独立窗口中显示)" hint_text = "打开文件开始播放\n(视频将在独立窗口中显示)"
self.video_hint = tk.Label(self.video_frame, text=hint_text, bg="#101010", fg="#8a8a8a") self.video_hint = tk.Label(self.video_frame, text=hint_text, bg="#101010", fg="#8a8a8a")
self.video_hint.place(relx=0.5, rely=0.5, anchor="center") self.video_hint.place(relx=0.5, rely=0.5, anchor="center")
self.video_frame.bind("<Double-Button-1>", self._on_video_double_click)
side = ttk.Frame(main, width=260) side = ttk.Frame(main, width=260)
side.grid(row=0, column=1, sticky="ns") side.grid(row=0, column=1, sticky="ns")
@@ -1057,6 +1061,7 @@ class MediaPlayerApp(tk.Tk):
scrollbar = ttk.Scrollbar(side, orient="vertical", command=self.listbox.yview) scrollbar = ttk.Scrollbar(side, orient="vertical", command=self.listbox.yview)
scrollbar.grid(row=0, column=1, sticky="ns") scrollbar.grid(row=0, column=1, sticky="ns")
self.listbox.configure(yscrollcommand=scrollbar.set) self.listbox.configure(yscrollcommand=scrollbar.set)
self.side = side
self._playlist_menu = tk.Menu(self, tearoff=0) self._playlist_menu = tk.Menu(self, tearoff=0)
self._playlist_menu.add_command(label="播放选中曲目", command=self.play_selected) self._playlist_menu.add_command(label="播放选中曲目", command=self.play_selected)
self._playlist_menu.add_separator() self._playlist_menu.add_separator()
@@ -1071,6 +1076,7 @@ class MediaPlayerApp(tk.Tk):
bottom = ttk.Frame(self, padding=(8, 6)) bottom = ttk.Frame(self, padding=(8, 6))
bottom.pack(fill="x") bottom.pack(fill="x")
self.bottom = bottom
self.seekbar = SeekBar(bottom, command=self._on_seek_command) self.seekbar = SeekBar(bottom, command=self._on_seek_command)
self.seekbar.pack(fill="x") self.seekbar.pack(fill="x")
@@ -2124,18 +2130,42 @@ class MediaPlayerApp(tk.Tk):
self._update_status() self._update_status()
def toggle_fullscreen(self): def toggle_fullscreen(self):
self._fullscreen = not self._fullscreen if self.engine.EMBEDDABLE:
if isinstance(self.engine, AvEngine): self._set_app_fullscreen(not self._fullscreen)
self.attributes("-fullscreen", self._fullscreen)
else: else:
self.engine.set_fullscreen(self._fullscreen) self.engine.set_fullscreen(not self._fullscreen)
self._fullscreen = not self._fullscreen
def leave_fullscreen(self): def leave_fullscreen(self):
self._fullscreen = False if self.engine.EMBEDDABLE:
if isinstance(self.engine, AvEngine): self._set_app_fullscreen(False)
self.attributes("-fullscreen", False)
else: else:
self.engine.set_fullscreen(False) self.engine.set_fullscreen(False)
self._fullscreen = False
def _on_video_double_click(self, _event=None):
if self.engine.EMBEDDABLE:
self.toggle_fullscreen()
return "break"
def _set_app_fullscreen(self, on):
on = bool(on)
if on == self._fullscreen:
return
if on:
self.toolbar.pack_forget()
self.bottom.pack_forget()
self.side.grid_remove()
self.video_frame.grid_configure(padx=0)
self.attributes("-fullscreen", True)
self.lift()
else:
self.attributes("-fullscreen", False)
self.side.grid()
self.video_frame.grid_configure(padx=(0, 8))
self.toolbar.pack(before=self.main, side="top", fill="x")
self.bottom.pack(after=self.main, side="bottom", fill="x")
self._fullscreen = on
def play_selected(self): def play_selected(self):
selection = self.listbox.curselection() selection = self.listbox.curselection()

460
moss/README.md Normal file
View File

@@ -0,0 +1,460 @@
# MOSS-Transcribe-Diarize 转写服务部署
基于 [OpenMOSS/MOSS-Transcribe-Diarize](https://github.com/OpenMOSS/MOSS-Transcribe-Diarize)
0.9B 的端到端语音转写服务。单模型联合完成转写与说话人分离,输出带
`[S01]`、`[S02]` 等说话人标签和精确时间戳的结构化转写结果,适合多人对话、
会议、播客、访谈等多说话人场景。
与 Whisper 的区别:Whisper 只做语音转文字,不区分说话人;MOSS 在转写的
同时输出说话人标签,`sentence_api` 无需额外集成即可自动按说话人分句。
## 目录结构
```text
moss/
README.md 本文档
systemd/moss-transcribe.service systemd 服务模板
verify.sh 部署验证脚本
```
## 硬件与系统要求
| 项目 | 要求 |
|------|------|
| GPU | NVIDIA,显存 ≥ 8 GB(推荐 16 GB 以上) |
| CUDA | 12.9+(vLLM 方案)或 13.0+(SGLang Omni 方案) |
| 驱动 | ≥ 535(支持 CUDA 12.x)或 ≥ 570(支持 CUDA 13) |
| 系统 | Ubuntu 22.04 / 24.04 |
| Python | 3.12 |
| 其他 | FFmpeg、Git、curl |
模型权重约 2 GB(bf16),推理时加上音频编码器和 KV 缓存实际占用约 3–6 GB。
如果与其他 GPU 服务共存,需确保剩余显存充足。
## 推理后端选择
MOSS 官方提供两种推理后端,按 CUDA 版本选择:
| 后端 | CUDA 要求 | 适用场景 |
|------|-----------|----------|
| **SGLang Omni**(推荐) | CUDA 13.0+ | 官方推荐后端,长音频性能更好 |
| **vLLM** | CUDA 12.x / 13.x | CUDA 12 环境或 SGLang 安装遇到问题时使用 |
两者提供相同的 OpenAI 兼容接口(`POST /v1/audio/transcriptions`),
切换后端不需要改 `sentence_api` 的任何配置。
---
## 方案 A:SGLang Omni 部署(CUDA 13,推荐)
### 前置条件
确认驱动支持 CUDA 13:
```bash
nvidia-smi
# 查看 CUDA Version 列,需 ≥ 13.0
```
### 安装系统依赖
```bash
sudo apt update
sudo apt install -y ffmpeg git curl
```
### 安装 uv
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
# 重新加载 shell 或执行 source ~/.bashrc 使 uv 命令生效
```
### 创建目录和虚拟环境
```bash
sudo mkdir -p /opt/moss-transcribe
sudo chown "$USER":"$USER" /opt/moss-transcribe
cd /opt/moss-transcribe
git clone https://github.com/OpenMOSS/MOSS-Transcribe-Diarize.git
cd MOSS-Transcribe-Diarize
uv venv --python 3.12 .venv
source .venv/bin/activate
```
### 安装推理框架
```bash
uv pip install -e ".[torch-runtime]" --torch-backend=auto
uv pip install sglang-omni
```
### 下载模型
```bash
# 国内服务器使用 HF 镜像加速
export HF_ENDPOINT=https://hf-mirror.com
hf download OpenMOSS-Team/MOSS-Transcribe-Diarize
```
模型下载到 `~/.cache/huggingface/hub/`,约 2 GB。
### 启动服务
```bash
source /opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin/activate
sgl-omni serve \
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
--host 127.0.0.1 \
--port 8001 \
--max-running-requests 4 \
--cuda-graph-max-bs 4 \
--mem-fraction-static 0.35
```
**参数说明:**
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--host` | `127.0.0.1` | 只监听本机,不要暴露公网 |
| `--port` | `8001` | 服务端口(Whisper 用 9000,避免冲突) |
| `--max-running-requests` | `4` | 最大并发请求数;显存充足可调大 |
| `--cuda-graph-max-bs` | `4` | CUDA Graph 批大小上限,与并发数一致 |
| `--mem-fraction-static` | `0.80` | 静态显存分配比例;与其他 GPU 服务共存时调低 |
> 与 llama-server、Whisper 等其他 GPU 服务共存时,将
> `--mem-fraction-static` 设为 `0.30`–`0.40`(约 7–10 GB),避免 OOM。
> 独占 GPU 时可设为 `0.80` 获得最大并发能力。
首次启动会加载模型到 GPU,等待日志出现 `Uvicorn running on ...` 即启动完成。
---
## 方案 B:vLLM 部署(CUDA 12 / 13 兼容)
如果 CUDA 版本低于 13.0 或 SGLang Omni 安装遇到兼容性问题,改用 vLLM。
### 前置条件
同方案 A,但不要求 CUDA 13。
### 安装系统依赖和 uv
与方案 A 相同(见上文)。
### 创建目录和虚拟环境
```bash
sudo mkdir -p /opt/moss-transcribe
sudo chown "$USER":"$USER" /opt/moss-transcribe
cd /opt/moss-transcribe
uv venv --python 3.12 .venv
source .venv/bin/activate
```
### 安装 vLLM
vLLM 从 0.27.1 起包含 MOSS-Transcribe-Diarize 的模型注册。
```bash
# CUDA 13 环境
uv pip install -U "vllm[audio]" --torch-backend=auto
```
如果安装的 vLLM 版本不支持 MOSS 模型(启动时报 `model not found`),
需要使用官方指定的 pinned nightly 构建:
```bash
# CUDA 12 环境(cu129)
uv pip install -U vllm \
--torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129
```
> 上游构建地址可能随版本更新,正式部署前对照
> [MOSS 官方仓库](https://github.com/OpenMOSS/MOSS-Transcribe-Diarize) Quickstart。
### 启动服务
```bash
source /opt/moss-transcribe/.venv/bin/activate
vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize \
--host 127.0.0.1 \
--port 8001 \
--trust-remote-code
```
> `--trust-remote-code` 是必需的:模型使用自定义 HuggingFace processor 代码。
首次启动会自动从 HuggingFace 下载模型权重。国内服务器建议在启动前设置:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
---
## 注册为 systemd 服务
### SGLang Omni 版本
复制服务文件到系统目录并修改实际路径:
```bash
sudo cp /path/to/repo/moss/systemd/moss-transcribe.service /etc/systemd/system/
```
确认 `ExecStart` 路径与实际安装位置一致:
```ini
ExecStart=/opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin/sgl-omni serve ...
```
### vLLM 版本
将 `ExecStart` 改为:
```ini
ExecStart=/opt/moss-transcribe/.venv/bin/vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize --host 127.0.0.1 --port 8001 --trust-remote-code
```
并删除 `Environment=HF_ENDPOINT=...` 以外的 SGLang 特有参数。
### 启用并启动
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now moss-transcribe
sudo journalctl -u moss-transcribe -f # 观察启动日志
```
首次启动会下载模型权重,看到 `running on ...` 表示服务就绪。
---
## 验证部署
使用仓库自带的验证脚本:
```bash
cd /opt/moss-transcribe
bash verify.sh
# 用真实语音文件验证(推荐)
bash verify.sh --file /path/to/multi-speaker.wav
# 同时检查 sentence_api 集成状态
bash verify.sh --api-url http://127.0.0.1:8000
```
或手动发起转写请求:
```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 \
-F max_new_tokens=65536
```
**正常响应**应包含:
- `text`:完整转写文本
- `segments`:分段数组,每段包含 `start`、`end`、`text`、`speaker`
```json
{
"text": "大家好,今天我们讨论项目进度。 好的,我先汇报我这边的进展。",
"segments": [
{"start": 0.48, "end": 2.12, "text": "大家好,今天我们讨论项目进度。", "speaker": "S01"},
{"start": 2.55, "end": 5.80, "text": "好的,我先汇报我这边的进展。", "speaker": "S02"}
]
}
```
**说话人标签说明:** `S01`、`S02` 等由模型自动分配,是匿名编号。
同一说话人在同一段音频中标签一致;不同音频之间标签无关联。
---
## 接入 sentence_api
在 `sentence_api/.env` 中确认以下配置:
```dotenv
MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions
MOSS_MODEL=OpenMOSS-Team/MOSS-Transcribe-Diarize
```
然后重建 API 容器:
```bash
cd /opt/oral-trainer/sentence_api
docker compose up -d --build
curl http://127.0.0.1:8000/healthz # 确认 moss_configured 为 true
```
已上传的视频可在管理后台点击"重新处理",重新转写后会带上说话人标签。
---
## 从 Whisper 切换到 MOSS
如果服务器上同时部署了 Whisper(Speaches)和 MOSS,切换步骤:
1. **停止 Whisper 服务**(释放约 4 GB 显存):
```bash
cd /opt/whisper
docker compose down
sudo systemctl disable --now whisper-transcribe # 如果注册了 systemd
```
2. **修改 `sentence_api/.env`**:
```dotenv
MOSS_TRANSCRIBE_URL=http://127.0.0.1:8001/v1/audio/transcriptions
MOSS_MODEL=OpenMOSS-Team/MOSS-Transcribe-Diarize
```
3. **重建 API**:
```bash
cd /opt/oral-trainer/sentence_api
docker compose up -d --build
```
4. **验证**:
```bash
curl http://127.0.0.1:8000/healthz
```
不需要删除 Whisper 的部署文件,后续切回只需恢复 `.env` 并重新启动。
---
## 显存分配参考
以 NVIDIA L4(24 GB)为例,MOSS 与其他服务共存的显存分配:
| 服务 | 显存占用 | 说明 |
|------|----------|------|
| llama-server | 8.4 GB | 保留 |
| MOSS (SGLang, mem-fraction=0.35) | ~8 GB | 新增 |
| Speaches (Whisper) | 4 GB | 停掉后释放 |
| **MOSS + llama-server** | ~16 GB / 24 GB | 安全 |
独占 GPU 时可将 `--mem-fraction-static` 调到 `0.80`(约 19 GB),
`--max-running-requests` 调到 `16`,支持更多并发。
---
## API 参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `file` | file | 必填 | 上传的音频/视频文件 |
| `model` | string | 服务端默认 | 模型标识符 |
| `language` | string | 未设置 | 可选的语言提示(如 `zh`、`en`) |
| `response_format` | string | `json` | `json`(原始文本)、`verbose_json`(分段)或 `text` |
| `temperature` | float | `0.0` | 采样温度,转写建议保持 `0` |
| `max_new_tokens` | int | `5120` | 最大生成 token 数;长音频/多人对话调大到 `65536` |
| `prompt` | string | 未设置 | 可选的自定义指令或热词提示 |
### 热词提示
通过 `prompt` 参数为专有名词、人名、产品名等提供提示,可提高识别准确率:
```bash
curl -X POST http://127.0.0.1:8001/v1/audio/transcriptions \
-F model=OpenMOSS-Team/MOSS-Transcribe-Diarize \
-F file=@meeting.wav \
-F response_format=verbose_json \
-F prompt="热词提示:MOSS, Transcribe, Diarize, vLLM" \
-F temperature=0
```
---
## 常见问题
### 模型下载慢或失败
```bash
# 方法一:使用 HF 镜像
export HF_ENDPOINT=https://hf-mirror.com
hf download OpenMOSS-Team/MOSS-Transcribe-Diarize
# 方法二:配置 HTTP 代理
export HTTP_PROXY=http://your-proxy:port
export HTTPS_PROXY=http://your-proxy:port
```
### 启动报 CUDA out of memory
与其他 GPU 服务共存时,降低 `--mem-fraction-static`(如 `0.30`);
或停掉暂时不需要的服务(如 Whisper)释放显存。
### 启动报 model not found
vLLM 版本过低,不包含 MOSS 模型注册。使用官方指定的 pinned nightly
(见方案 B 安装部分),或升级到 vLLM ≥ 0.27.1。
### 长音频转写截断
请求时添加 `-F max_new_tokens=65536`,确保解码器能完整生成整段转写。
### 说话人标签偶尔混淆
多人重叠说话(同时说话)时 0.9B 模型可能偶尔混淆,这是模型本身的局限。
MOSS 采用端到端 joint diarization,比 Whisper + pyannote 拼接方案更稳定,
但如果场景对说话人准确率要求极高,可以考虑使用在线的 MOSS Pro 版本。
### 服务只监听 127.0.0.1,外部怎么访问
这是有意设计——转写服务不应暴露公网。`sentence_api` 通过 Docker 内部
网络或宿主机 `127.0.0.1` 访问即可。如果 `sentence_api` 运行在 Docker
容器里且无法访问宿主机的 `127.0.0.1:8001`,将 `MOSS_TRANSCRIBE_URL` 改为
`http://host.docker.internal:8001/v1/audio/transcriptions`(Linux 需在
`docker-compose.yml` 中添加 `extra_hosts: ["host.docker.internal:host-gateway"]`)。
---
## 更新与回滚
### 更新模型
```bash
export HF_ENDPOINT=https://hf-mirror.com
hf download OpenMOSS-Team/MOSS-Transcribe-Diarize
sudo systemctl restart moss-transcribe
```
### 更新推理框架
```bash
cd /opt/moss-transcribe/MOSS-Transcribe-Diarize
source .venv/bin/activate
uv pip install -U sglang-omni # 或 vllm
sudo systemctl restart moss-transcribe
```
### 回滚
```bash
# 回滚到上一个 systemd 配置
sudo systemctl revert moss-transcribe
# 或手动恢复备份的服务文件
sudo cp /path/to/backup/moss-transcribe.service.bak /etc/systemd/system/moss-transcribe.service
sudo systemctl daemon-reload
sudo systemctl restart moss-transcribe
```

View File

@@ -0,0 +1,16 @@
[Unit]
Description=MOSS Transcribe Diarize Service (vLLM)
After=network-online.target
[Service]
Type=simple
User=oraltrainer
WorkingDirectory=/opt/moss-transcribe
Environment=HF_ENDPOINT=https://hf-mirror.com
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

View File

@@ -0,0 +1,22 @@
[Unit]
Description=MOSS Transcribe Diarize Service (SGLang Omni)
After=network-online.target
[Service]
Type=simple
User=oraltrainer
WorkingDirectory=/opt/moss-transcribe/MOSS-Transcribe-Diarize
Environment=HF_ENDPOINT=https://hf-mirror.com
ExecStart=/opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin/sgl-omni serve \
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
--host 127.0.0.1 \
--port 8001 \
--max-running-requests 4 \
--cuda-graph-max-bs 4 \
--mem-fraction-static 0.35
Restart=on-failure
RestartSec=10
TimeoutStartSec=1800
[Install]
WantedBy=multi-user.target

130
moss/verify.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
#
# 验证 MOSS-Transcribe-Diarize 转写服务:
# 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:8001 --model OpenMOSS-Team/MOSS-Transcribe-Diarize
# ./verify.sh --api-url http://127.0.0.1:8000
set -euo pipefail
MOSS_URL="${MOSS_URL:-http://127.0.0.1:8001}"
MOSS_MODEL="${MOSS_MODEL:-OpenMOSS-Team/MOSS-Transcribe-Diarize}"
TEST_FILE=""
API_URL=""
usage() {
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
}
while [[ $# -gt 0 ]]; do
case "$1" in
--url) MOSS_URL="$2"; shift 2 ;;
--model) MOSS_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 "==> MOSS 地址: $MOSS_URL"
echo "==> 模型: $MOSS_MODEL"
echo "==> 检查 GPU"
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi --query-gpu=name,memory.total,memory.used,driver_version --format=csv,noheader | head -5
else
echo "警告: 未找到 nvidia-smi,无法确认 GPU 状态。"
fi
echo "==> 检查服务健康"
if curl -fsS --max-time 10 "$MOSS_URL/health" > /dev/null 2>&1; then
echo "OK: /health 返回 200"
elif curl -fsS --max-time 10 "$MOSS_URL/healthz" > /dev/null 2>&1; then
echo "OK: /healthz 返回 200"
else
echo "警告: /health 和 /healthz 均不可达,服务可能尚未就绪。"
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 600 \
-X POST "$MOSS_URL/v1/audio/transcriptions" \
-F "model=$MOSS_MODEL" \
-F "file=@$AUDIO_FILE" \
-F "response_format=verbose_json" \
-F "temperature=0" \
-F "max_new_tokens=65536")"
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)"
speakers = set()
for index, segment in enumerate(segments):
start = segment.get("start")
end = segment.get("end")
speaker = segment.get("speaker")
if not (isinstance(start, (int, float)) and isinstance(end, (int, float)) and end > start >= 0):
raise AssertionError(f"segments[{index}] 缺少合法 start/end")
if speaker:
speakers.add(speaker)
print(f"OK: text={text!r}")
print(f"OK: segments={len(segments)} 条")
if speakers:
print(f"OK: 说话人={sorted(speakers)}")
else:
print("注意: 未见 speaker 字段;单说话人或模型未输出标签(正常弦音无多人对话)。")
'
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/multi-speaker.wav"

View File

@@ -5,7 +5,8 @@
- `oral-trainer-api`:视频上传、管理后台、边界查询、Range 视频播放和朗读评分。 - `oral-trainer-api`:视频上传、管理后台、边界查询、Range 视频播放和朗读评分。
- GPU 转写服务(二选一,只在服务器内网监听): - GPU 转写服务(二选一,只在服务器内网监听):
- `whisper/`:faster-whisper(Speaches),推荐,部署最简单,见第 2A 节。 - `whisper/`:faster-whisper(Speaches),推荐,部署最简单,见第 2A 节。
- `MOSS-Transcribe-Diarize`:带说话人分离,见第 2 节。 - `MOSS-Transcribe-Diarize`:带说话人分离,见第 2 节和
[`moss/README.md`](../moss/README.md)。
推荐 Ubuntu 22.04/24.04、Python 3.12、FFmpeg、NVIDIA GPU 和已安装的 NVIDIA 驱动。MOSS 0.9B 的实际显存占用受推理框架、并发和音频长度影响,生产环境建议从 16 GB 以上显存开始验证。 推荐 Ubuntu 22.04/24.04、Python 3.12、FFmpeg、NVIDIA GPU 和已安装的 NVIDIA 驱动。MOSS 0.9B 的实际显存占用受推理框架、并发和音频长度影响,生产环境建议从 16 GB 以上显存开始验证。

View File

@@ -84,4 +84,4 @@ L4(24 GB 显存)可同时常驻两个模型;Speaches 支持在请求的 `m
`environment` 里加 `HTTP_PROXY`/`HTTPS_PROXY` 后 `docker compose up -d`。 `environment` 里加 `HTTP_PROXY`/`HTTPS_PROXY` 后 `docker compose up -d`。
- 容器报 CUDA 错误:确认宿主机驱动 ≥ 535,并已安装 NVIDIA Container Toolkit。 - 容器报 CUDA 错误:确认宿主机驱动 ≥ 535,并已安装 NVIDIA Container Toolkit。
- 转写结果没有说话人:Whisper 不做说话人分离,需要该功能请改用 - 转写结果没有说话人:Whisper 不做说话人分离,需要该功能请改用
`MOSS-Transcribe-Diarize`(见 `sentence_api/DEPLOYMENT.md`)。 `MOSS-Transcribe-Diarize`(见 [`moss/README.md`](../moss/README.md))。