diff --git a/README.md b/README.md index 29b39a7..394a956 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ pip install -r requirements.txt | `N` / `P` | 下一首 / 上一首 | | `[` / `]` | 减速 / 加速(0.5x - 2.0x) | | `L` | 切换循环模式(关 / 单曲 / 列表) | +| 双击画面 | 进入 / 退出全屏 | | `F` | 视频全屏切换 | | `Esc` | 退出全屏 | | `Ctrl+O` | 打开文件(macOS 亦支持 `Command+O`) | diff --git a/media_player.py b/media_player.py index 4119b9f..cdd8cdd 100644 --- a/media_player.py +++ b/media_player.py @@ -10,6 +10,7 @@ 常用快捷键: Space 播放 / 暂停 ← / → 后退 / 前进 10 秒 + 双击画面 进入 / 退出全屏 Shift+← / → 后退 / 前进 60 秒 ↑ / ↓ 音量 +5 / -5 Ctrl+↑ / ↓ 快进步长 +1 / -1 秒(macOS 亦支持 Command+↑ / ↓) @@ -991,6 +992,7 @@ class MediaPlayerApp(tk.Tk): toolbar = ttk.Frame(self, padding=(8, 8)) 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_folder, takefocus=0).pack(side="left", padx=(0, 8)) @@ -1027,6 +1029,7 @@ class MediaPlayerApp(tk.Tk): main.pack(fill="both", expand=True) main.columnconfigure(0, weight=1) main.rowconfigure(0, weight=1) + self.main = main self.video_frame = tk.Frame(main, bg="#101010") self.video_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 8)) @@ -1038,6 +1041,7 @@ class MediaPlayerApp(tk.Tk): hint_text = "打开文件开始播放\n(视频将在独立窗口中显示)" 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_frame.bind("", self._on_video_double_click) side = ttk.Frame(main, width=260) 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.grid(row=0, column=1, sticky="ns") self.listbox.configure(yscrollcommand=scrollbar.set) + self.side = side self._playlist_menu = tk.Menu(self, tearoff=0) self._playlist_menu.add_command(label="播放选中曲目", command=self.play_selected) self._playlist_menu.add_separator() @@ -1071,6 +1076,7 @@ class MediaPlayerApp(tk.Tk): bottom = ttk.Frame(self, padding=(8, 6)) bottom.pack(fill="x") + self.bottom = bottom self.seekbar = SeekBar(bottom, command=self._on_seek_command) self.seekbar.pack(fill="x") @@ -2124,18 +2130,42 @@ class MediaPlayerApp(tk.Tk): self._update_status() def toggle_fullscreen(self): - self._fullscreen = not self._fullscreen - if isinstance(self.engine, AvEngine): - self.attributes("-fullscreen", self._fullscreen) + if self.engine.EMBEDDABLE: + self._set_app_fullscreen(not self._fullscreen) else: - self.engine.set_fullscreen(self._fullscreen) + self.engine.set_fullscreen(not self._fullscreen) + self._fullscreen = not self._fullscreen def leave_fullscreen(self): - self._fullscreen = False - if isinstance(self.engine, AvEngine): - self.attributes("-fullscreen", False) + if self.engine.EMBEDDABLE: + self._set_app_fullscreen(False) else: 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): selection = self.listbox.curselection() diff --git a/moss/README.md b/moss/README.md new file mode 100644 index 0000000..7472963 --- /dev/null +++ b/moss/README.md @@ -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 +``` diff --git a/moss/systemd/moss-transcribe-vllm.service b/moss/systemd/moss-transcribe-vllm.service new file mode 100644 index 0000000..02b01c6 --- /dev/null +++ b/moss/systemd/moss-transcribe-vllm.service @@ -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 diff --git a/moss/systemd/moss-transcribe.service b/moss/systemd/moss-transcribe.service new file mode 100644 index 0000000..a2fb6c7 --- /dev/null +++ b/moss/systemd/moss-transcribe.service @@ -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 diff --git a/moss/verify.sh b/moss/verify.sh new file mode 100755 index 0000000..7c79816 --- /dev/null +++ b/moss/verify.sh @@ -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" diff --git a/sentence_api/DEPLOYMENT.md b/sentence_api/DEPLOYMENT.md index 9d57d74..7981f51 100644 --- a/sentence_api/DEPLOYMENT.md +++ b/sentence_api/DEPLOYMENT.md @@ -5,7 +5,8 @@ - `oral-trainer-api`:视频上传、管理后台、边界查询、Range 视频播放和朗读评分。 - GPU 转写服务(二选一,只在服务器内网监听): - `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 以上显存开始验证。 diff --git a/whisper/README.md b/whisper/README.md index 37be755..e511e10 100644 --- a/whisper/README.md +++ b/whisper/README.md @@ -84,4 +84,4 @@ L4(24 GB 显存)可同时常驻两个模型;Speaches 支持在请求的 `m `environment` 里加 `HTTP_PROXY`/`HTTPS_PROXY` 后 `docker compose up -d`。 - 容器报 CUDA 错误:确认宿主机驱动 ≥ 535,并已安装 NVIDIA Container Toolkit。 - 转写结果没有说话人:Whisper 不做说话人分离,需要该功能请改用 - `MOSS-Transcribe-Diarize`(见 `sentence_api/DEPLOYMENT.md`)。 + `MOSS-Transcribe-Diarize`(见 [`moss/README.md`](../moss/README.md))。