added moss
This commit is contained in:
@@ -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`) |
|
||||||
|
|||||||
@@ -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
460
moss/README.md
Normal 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
|
||||||
|
```
|
||||||
16
moss/systemd/moss-transcribe-vllm.service
Normal file
16
moss/systemd/moss-transcribe-vllm.service
Normal 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
|
||||||
22
moss/systemd/moss-transcribe.service
Normal file
22
moss/systemd/moss-transcribe.service
Normal 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
130
moss/verify.sh
Executable 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"
|
||||||
@@ -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 以上显存开始验证。
|
||||||
|
|
||||||
|
|||||||
@@ -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))。
|
||||||
|
|||||||
Reference in New Issue
Block a user