feat(moss): support vLLM diarized responses

Signed-off-by: LauraGPT <[email protected]>
This commit is contained in:
LauraGPT
2026-08-29 19:19:25 +00:00
parent f0c5b4a149
commit 14e154ad2a
8 changed files with 314 additions and 70 deletions
+53 -16
View File
@@ -21,7 +21,9 @@ This guide was verified against:
- source: `OpenMOSS/MOSS-Transcribe-Diarize@cb765f2b0fe6f7a298aa2002e2281ae693d1f3c3`
- model: `OpenMOSS-Team/MOSS-Transcribe-Diarize@e8681d68e7042738ffca8ac8212bc8fcb1131ab8`
- license: Apache-2.0 in the pinned upstream source and model metadata
- vLLM nightly index: `68b4a1d582818e67adc903bf1b8fc5a5447da2fa`
- vLLM release: `v0.27.1@6e448d0ea9bf3d88d898b65449ca6dc2aec170ac`
- vLLM CUDA 12.9 x86_64 wheel SHA-256:
`bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b`
Pin all three revisions. The model uses `trust_remote_code`; do not execute a
floating model revision in a production service.
@@ -76,36 +78,48 @@ model = AutoModel(
backend="vllm",
vllm_base_url="http://127.0.0.1:8898/v1",
vllm_model="moss-transcribe-diarize",
vllm_response_format="diarized_json",
disable_update=True,
)
result = model.generate("audio.wav")[0]
```
The vLLM adapter sends the documented OpenAI-compatible multipart request and
normalizes the returned raw text. Authentication can be supplied with
`vllm_api_key`; keep it in a secret store rather than source code.
normalizes the official speaker-attributed `segments` directly into
`sentence_info`. Authentication can be supplied with `vllm_api_key`; keep it
in a secret store rather than source code.
`vllm_response_format="diarized_json"` is the structured production path on
the pinned vLLM revision. The compatibility default remains `json` so existing
clients keep the exact tagged generation in `raw_text`; in structured mode,
`raw_text` is the cleaned `text` returned by vLLM and the authoritative speaker
metadata is in `sentence_info`.
## Choose a serving path
| Path | Environment | Response contract | Use when |
|---|---|---|---|
| vLLM | CUDA 12 (`cu129`) or CUDA 13 (`cu130`) | `response_format=json` returns raw `[start][Sxx]text[end]` text | You already operate vLLM or need its scheduler |
| vLLM | CUDA 12 (`cu129`) or CUDA 13 (`cu130`) | `response_format=diarized_json` returns OpenAI-compatible speaker segments; `response_format=json` keeps raw `[start][Sxx]text[end]` text | You already operate vLLM or need its scheduler |
| SGLang Omni | CUDA 13 in the current upstream guide | `response_format=verbose_json` returns parsed segments | You need structured speaker segments directly from the API |
| Transformers | PyTorch process | Python objects and raw tagged text | Evaluation, debugging, or custom preprocessing |
The two HTTP backends share `/v1/audio/transcriptions`, but their documented
response formats are not interchangeable. Do not promise vLLM `segments`
without testing the exact vLLM revision.
response formats are not interchangeable. The pinned vLLM revision supports
`diarized_json`; test the exact server revision before promising `segments`.
## vLLM
Create an isolated environment and install the upstream-pinned nightly build:
Create an isolated environment and install the verified vLLM release:
```bash
uv venv --python 3.12 .venv-moss
uv pip install --python .venv-moss/bin/python -U 'vllm[audio]' \
--torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129
curl -fL \
https://github.com/vllm-project/vllm/releases/download/v0.27.1/vllm-0.27.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl \
-o vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl
echo "bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl" \
| sha256sum -c -
uv pip install --python .venv-moss/bin/python --torch-backend=auto \
"vllm[audio] @ file://$PWD/vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl"
```
The `audio` extra is required. A plain `vllm` install can start the server but
@@ -124,13 +138,13 @@ CUDA_VISIBLE_DEVICES=0 .venv-moss/bin/vllm serve \
--port 8898
```
Submit audio and validate the speaker-tagged text:
Submit audio and validate the official speaker-attributed response:
```bash
curl -fsS http://127.0.0.1:8898/v1/audio/transcriptions \
-F file=@audio.wav \
-F model=moss-transcribe-diarize \
-F response_format=json \
-F response_format=diarized_json \
-F temperature=0 \
| tee moss-transcription.json
@@ -140,19 +154,36 @@ import json
with open("moss-transcription.json", encoding="utf-8") as stream:
payload = json.load(stream)
text = payload.get("text", "")
segments = payload.get("segments", [])
assert text.strip(), payload
assert "[S01]" in text, text
print(text)
assert segments, payload
assert all(
isinstance(item.get("speaker"), str)
and item.get("text")
and item.get("start") <= item.get("end")
for item in segments
), payload
print(text, sorted({item["speaker"] for item in segments}))
PY
```
For an audit of the model's compact tagged generation, repeat the request with
`response_format=json` and verify `[S01]` appears in the returned `text`.
For long recordings, test `max_completion_tokens` against the longest expected
meeting. A larger limit can increase memory use and tail latency.
### Reproduced vLLM contract
The FunASR ecosystem validation used vLLM
`0.23.1rc1.dev949+g68b4a1d58`, Torch `2.11.0+cu129`, and one H100 80GB:
The structured-response validation used vLLM 0.27.1, Torch
`2.13.0+cu129`, and one H100 80GB. The 15.1685-second two-speaker probe
(`43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8`)
returned two `diarized_json` segments labelled `S01` and `S02`; the same
request through the FunASR adapter returned two monotonic `sentence_info`
entries with the same speakers.
Earlier raw-response validation on vLLM
`0.23.1rc1.dev949+g68b4a1d58`, Torch `2.11.0+cu129`, and one H100 80GB found:
- the bundled 6.000-second sample
(`ea03e1f473ad1618a03da3327a545369cb8f6f06cb0f4115535e5a866167d47e`)
@@ -165,6 +196,12 @@ This proves the pinned API and speaker-return contract on those inputs. It is
not a diarization accuracy result, overlap test, throughput benchmark, or
production capacity claim.
The older nightly commit
`68b4a1d582818e67adc903bf1b8fc5a5447da2fa` predates vLLM
[`#48543`](https://github.com/vllm-project/vllm/pull/48543). It accepts
`response_format=json` but returns HTTP 400 for `diarized_json`; upgrade to the
verified release instead of silently retrying an expensive transcription.
The FunASR adapter was also tested on both `backend="hf"` and
`backend="vllm"` with model revision
`e8681d68e7042738ffca8ac8212bc8fcb1131ab8`. The 15.1685-second two-speaker
+50 -15
View File
@@ -19,7 +19,9 @@ MOSS-Transcribe-Diarize 会联合生成转写、时间戳和 `[S01]` 等说话
- 源码:`OpenMOSS/MOSS-Transcribe-Diarize@cb765f2b0fe6f7a298aa2002e2281ae693d1f3c3`
- 模型:`OpenMOSS-Team/MOSS-Transcribe-Diarize@e8681d68e7042738ffca8ac8212bc8fcb1131ab8`
- 许可证:固定上游源码和模型元数据中的 Apache-2.0
- vLLM nightly 索引:`68b4a1d582818e67adc903bf1b8fc5a5447da2fa`
- vLLM 发布版:`v0.27.1@6e448d0ea9bf3d88d898b65449ca6dc2aec170ac`
- vLLM CUDA 12.9 x86_64 wheel SHA-256:
`bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b`
三者都要固定。模型依赖 `trust_remote_code`,生产服务不要执行浮动的模型 revision。
@@ -68,34 +70,46 @@ model = AutoModel(
backend="vllm",
vllm_base_url="http://127.0.0.1:8898/v1",
vllm_model="moss-transcribe-diarize",
vllm_response_format="diarized_json",
disable_update=True,
)
result = model.generate("audio.wav")[0]
```
vLLM 适配器发送官方文档中的 OpenAI-compatible multipart 请求,并统一解析返回文本。
认证可通过 `vllm_api_key` 提供;请放在 secret store,不要写入源码。
vLLM 适配器发送官方文档中的 OpenAI-compatible multipart 请求,并把官方返回的
speaker `segments` 直接映射到 `sentence_info`。认证可通过 `vllm_api_key` 提供;
请放在 secret store,不要写入源码。
固定 vLLM revision 的生产结构化路径是
`vllm_response_format="diarized_json"`。为保持已有客户端兼容,默认值仍是 `json`,
此时 `raw_text` 保留原始标签生成;结构化模式下,`raw_text` 是 vLLM 返回的清理后
`text`,权威说话人信息位于 `sentence_info`。
## 选择服务后端
| 路径 | 环境 | 响应契约 | 适用场景 |
|---|---|---|---|
| vLLM | CUDA 12(`cu129`)或 CUDA 13(`cu130`) | `response_format=json` 返回原始 `[start][Sxx]text[end]` 文本 | 已运行 vLLM,或需要其调度能力 |
| vLLM | CUDA 12(`cu129`)或 CUDA 13(`cu130`) | `response_format=diarized_json` 返回 OpenAI-compatible 说话人 segments;`response_format=json` 保留原始 `[start][Sxx]text[end]` 文本 | 已运行 vLLM,或需要其调度能力 |
| SGLang Omni | 当前上游指南为 CUDA 13 | `response_format=verbose_json` 返回解析后的 segments | API 需要直接返回结构化说话人分段 |
| Transformers | PyTorch 进程 | Python 对象和原始标签文本 | 评测、排障或自定义预处理 |
两个 HTTP 后端都使用 `/v1/audio/transcriptions`,但官方文档中的响应格式不能混为
一谈。没有对 exact vLLM revision 做验证时,不要承诺 vLLM 返回 `segments`。
一谈。固定的 vLLM revision 支持 `diarized_json`;承诺 `segments` 前仍要验证实际
部署的 exact server revision。
## vLLM
创建隔离环境并安装上游固定的 nightly build:
创建隔离环境并安装已验证的 vLLM 发布版:
```bash
uv venv --python 3.12 .venv-moss
uv pip install --python .venv-moss/bin/python -U 'vllm[audio]' \
--torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129
curl -fL \
https://github.com/vllm-project/vllm/releases/download/v0.27.1/vllm-0.27.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl \
-o vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl
echo "bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl" \
| sha256sum -c -
uv pip install --python .venv-moss/bin/python --torch-backend=auto \
"vllm[audio] @ file://$PWD/vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl"
```
必须安装 `audio` extra。只安装 plain `vllm` 时服务可以启动,但因为没有音频解码器,
@@ -113,13 +127,13 @@ CUDA_VISIBLE_DEVICES=0 .venv-moss/bin/vllm serve \
--port 8898
```
提交音频并验证说话人标签文本:
提交音频并验证官方结构化说话人响应:
```bash
curl -fsS http://127.0.0.1:8898/v1/audio/transcriptions \
-F file=@audio.wav \
-F model=moss-transcribe-diarize \
-F response_format=json \
-F response_format=diarized_json \
-F temperature=0 \
| tee moss-transcription.json
@@ -129,18 +143,34 @@ import json
with open("moss-transcription.json", encoding="utf-8") as stream:
payload = json.load(stream)
text = payload.get("text", "")
segments = payload.get("segments", [])
assert text.strip(), payload
assert "[S01]" in text, text
print(text)
assert segments, payload
assert all(
isinstance(item.get("speaker"), str)
and item.get("text")
and item.get("start") <= item.get("end")
for item in segments
), payload
print(text, sorted({item["speaker"] for item in segments}))
PY
```
如需审计模型原始紧凑标签生成,请把请求改为 `response_format=json`,并验证返回
`text` 中包含 `[S01]`。
长音频需要按业务最长会议验证 `max_completion_tokens`。增大上限也会影响显存和尾延迟。
### 已复现的 vLLM 契约
FunASR 生态验证使用 vLLM `0.23.1rc1.dev949+g68b4a1d58`、Torch
`2.11.0+cu129` 和一张 H100 80GB:
结构化响应验证使用 vLLM 0.27.1、Torch `2.13.0+cu129` 和一张 H100 80GB。
15.1685 秒双说话人样例
(`43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8`)
返回两个 `diarized_json` segments,标签为 `S01`、`S02`;同一请求通过 FunASR
适配器后,返回两个时间单调且说话人一致的 `sentence_info`。
较早的 raw-response 验证使用 vLLM `0.23.1rc1.dev949+g68b4a1d58`、Torch
`2.11.0+cu129` 和一张 H100 80GB,结果如下:
- 仓库内 6.000 秒样例
(`ea03e1f473ad1618a03da3327a545369cb8f6f06cb0f4115535e5a866167d47e`)
@@ -152,6 +182,11 @@ FunASR 生态验证使用 vLLM `0.23.1rc1.dev949+g68b4a1d58`、Torch
这证明固定版本对这些输入满足 API 和说话人返回契约,不是 diarization 精度、重叠语音、
吞吐或生产容量结论。
旧 nightly commit `68b4a1d582818e67adc903bf1b8fc5a5447da2fa` 早于 vLLM
[`#48543`](https://github.com/vllm-project/vllm/pull/48543):它支持
`response_format=json`,但 `diarized_json` 会返回 HTTP 400。应升级到已验证发布版,
不要静默重试一次成本较高的转写请求。
FunASR 适配器还分别使用 `backend="hf"` 和 `backend="vllm"`,按模型 revision
`e8681d68e7042738ffca8ac8212bc8fcb1131ab8` 做了真实测试。15.1685 秒的双说话人
样例(`43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8`)通过
+81 -5
View File
@@ -7,6 +7,7 @@ https://github.com/OpenMOSS/MOSS-Transcribe-Diarize
import copy
import io
import math
import mimetypes
import os
import re
@@ -127,6 +128,66 @@ def _result_from_transcript(key, transcript):
}
def _result_from_diarized_response(key, response):
"""Normalize vLLM's OpenAI-compatible diarized JSON response."""
text = response.get("text") if isinstance(response, dict) else None
segments = response.get("segments") if isinstance(response, dict) else None
if not isinstance(text, str) or not isinstance(segments, list):
raise RuntimeError(
"vLLM diarized_json response requires text and segments fields"
)
sentence_info = []
timestamps = []
for index, segment in enumerate(segments):
if not isinstance(segment, dict):
raise RuntimeError(
"vLLM diarized_json segment %d must be an object" % index
)
start = segment.get("start")
end = segment.get("end")
segment_text = segment.get("text")
speaker = segment.get("speaker")
numeric = (
isinstance(start, (int, float))
and not isinstance(start, bool)
and isinstance(end, (int, float))
and not isinstance(end, bool)
)
if (
not numeric
or not math.isfinite(start)
or not math.isfinite(end)
or start < 0
or end < start
or not isinstance(segment_text, str)
or not isinstance(speaker, str)
or not speaker
):
raise RuntimeError(
"vLLM diarized_json segment %d has an invalid contract" % index
)
timestamp = [int(round(start * 1000)), int(round(end * 1000))]
timestamps.append(timestamp)
sentence_info.append(
{
"start": timestamp[0],
"end": timestamp[1],
"text": segment_text,
"sentence": segment_text,
"spk": speaker,
"timestamp": [timestamp],
}
)
return {
"key": key,
"text": text,
"raw_text": text,
"timestamp": timestamps,
"sentence_info": sentence_info,
}
@tables.register("model_classes", "MOSS-Transcribe-Diarize")
@tables.register("model_classes", DEFAULT_MODEL)
class MossTranscribeDiarize(nn.Module):
@@ -157,6 +218,9 @@ class MossTranscribeDiarize(nn.Module):
self.vllm_model = kwargs.get("vllm_model", DEFAULT_MODEL)
self.vllm_api_key = kwargs.get("vllm_api_key", "EMPTY")
self.vllm_timeout = float(kwargs.get("vllm_timeout", 600.0))
self.vllm_response_format = kwargs.get("vllm_response_format", "json").lower()
if self.vllm_response_format not in {"json", "diarized_json"}:
raise ValueError("vllm_response_format must be 'json' or 'diarized_json'")
self.http_session = kwargs.get("http_session")
self.hf_model = None
@@ -240,10 +304,18 @@ class MossTranscribeDiarize(nn.Module):
results = []
for index, audio in enumerate(inputs):
if self.backend == "vllm":
transcript = self._transcribe_vllm(audio, **kwargs)
response = self._transcribe_vllm(audio, **kwargs)
if self.vllm_response_format == "diarized_json":
results.append(
_result_from_diarized_response(keys[index], response)
)
else:
results.append(
_result_from_transcript(keys[index], response["text"])
)
else:
transcript = self._transcribe_hf(audio, **kwargs)
results.append(_result_from_transcript(keys[index], transcript))
results.append(_result_from_transcript(keys[index], transcript))
return results, {
"batch_data_time": max(audio_duration, 1e-9),
"forward": time.perf_counter() - started,
@@ -324,7 +396,7 @@ class MossTranscribeDiarize(nn.Module):
filename, payload, content_type = self._multipart_audio(audio)
data = {
"model": self.vllm_model,
"response_format": "json",
"response_format": self.vllm_response_format,
"temperature": str(kwargs.get("temperature", 0)),
}
prompt = kwargs.get("prompt")
@@ -345,11 +417,15 @@ class MossTranscribeDiarize(nn.Module):
)
response.raise_for_status()
result = response.json()
if not isinstance(result, dict) or not isinstance(result.get("text"), str):
if not isinstance(result, dict):
raise RuntimeError("vLLM transcription response must be a JSON object")
if self.vllm_response_format == "json" and not isinstance(
result.get("text"), str
):
raise RuntimeError(
"vLLM transcription response did not contain a text field"
)
return result["text"]
return result
@staticmethod
def _multipart_audio(audio):
+6 -2
View File
@@ -25,15 +25,19 @@ def test_moss_guides_pin_upstream_and_separate_serving_contracts(guide: Path) ->
"cb765f2b0fe6f7a298aa2002e2281ae693d1f3c3",
"OpenMOSS-Team/MOSS-Transcribe-Diarize",
"e8681d68e7042738ffca8ac8212bc8fcb1131ab8",
"68b4a1d582818e67adc903bf1b8fc5a5447da2fa",
"6e448d0ea9bf3d88d898b65449ca6dc2aec170ac",
"bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b",
"vllm[audio]",
"0.23.1rc1.dev949+g68b4a1d58",
"vLLM 0.27.1",
"68b4a1d582818e67adc903bf1b8fc5a5447da2fa",
"dbb32bcfed2e8226bedf64248a9f4a44685b293a4696d18fb4cfa701b04db912",
"43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8",
"S01 -> S02 -> S01",
"/v1/audio/transcriptions",
"response_format=json",
"response_format=diarized_json",
"response_format=verbose_json",
'vllm_response_format="diarized_json"',
"[S01]",
"Apache-2.0",
"sentence_info",
@@ -138,6 +138,89 @@ class MossVllmBackendTest(unittest.TestCase):
self.assertEqual(content_type, "audio/wav")
response.raise_for_status.assert_called_once_with()
def test_normalizes_official_vllm_diarized_json_response(self):
session = MagicMock()
response = MagicMock()
response.json.return_value = {
"task": "transcribe",
"duration": 3.5,
"text": "hello again",
"segments": [
{
"type": "transcript.text.segment",
"id": "seg_0",
"start": 0.25,
"end": 1.5,
"text": "hello",
"speaker": "S01",
},
{
"type": "transcript.text.segment",
"id": "seg_1",
"start": 2.0,
"end": 3.5,
"text": "again",
"speaker": "S02",
},
],
}
session.post.return_value = response
model = MossTranscribeDiarize(
backend="vllm",
vllm_base_url="http://vllm.test:8000/v1",
vllm_response_format="diarized_json",
http_session=session,
)
results, _ = model.inference(
[np.zeros(1600, dtype=np.float32)], key=["meeting"]
)
self.assertEqual(
session.post.call_args.kwargs["data"]["response_format"],
"diarized_json",
)
self.assertEqual(results[0]["text"], "hello again")
self.assertEqual(results[0]["raw_text"], "hello again")
self.assertEqual(results[0]["timestamp"], [[250, 1500], [2000, 3500]])
self.assertEqual(
[(item["spk"], item["text"]) for item in results[0]["sentence_info"]],
[("S01", "hello"), ("S02", "again")],
)
def test_rejects_unsupported_vllm_response_format(self):
with self.assertRaisesRegex(ValueError, "json.*diarized_json"):
MossTranscribeDiarize(
backend="vllm",
vllm_base_url="http://vllm.test:8000/v1",
vllm_response_format="verbose_json",
)
def test_rejects_malformed_vllm_diarized_segment(self):
session = MagicMock()
response = MagicMock()
response.json.return_value = {
"text": "broken",
"segments": [
{
"start": 2.0,
"end": 1.0,
"text": "broken",
"speaker": "S01",
}
],
}
session.post.return_value = response
model = MossTranscribeDiarize(
backend="vllm",
vllm_base_url="http://vllm.test:8000/v1",
vllm_response_format="diarized_json",
http_session=session,
)
with self.assertRaisesRegex(RuntimeError, "segment 0.*invalid contract"):
model.inference([np.zeros(1600, dtype=np.float32)], key=["meeting"])
if __name__ == "__main__":
unittest.main()
+22 -20
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"verified": "2026-08-29",
"verified": "2026-08-30",
"deployments": [
{
"id": "moss-transcribe-diarize",
@@ -13,10 +13,10 @@
"models": ["OpenMOSS-Team/MOSS-Transcribe-Diarize (third-party Apache-2.0 model)"],
"operating_systems": ["Linux"],
"interfaces": ["OpenAI-compatible HTTP", "vLLM", "SGLang Omni", "Transformers"],
"tested": {"funasr": "AutoModel adapter@a3a2de0a; third-party model@e8681d68", "runtime": "Transformers 5.16.1 + vLLM 0.23.1rc1.dev949+g68b4a1d58 / Torch 2.11.0+cu129 / H100 80GB", "verified": "2026-08-29"},
"tested": {"funasr": "AutoModel diarized_json adapter; third-party model@e8681d68", "runtime": "vLLM 0.27.1 / Torch 2.13.0+cu129 / H100 80GB", "verified": "2026-08-30"},
"commands": {
"install": [
"uv venv --python 3.12 .venv-moss && uv pip install --python .venv-moss/bin/python -U 'vllm[audio]' --torch-backend=auto --extra-index-url https://wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129",
"uv venv --python 3.12 .venv-moss && curl -fL https://github.com/vllm-project/vllm/releases/download/v0.27.1/vllm-0.27.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl -o vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl && echo 'bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl' | sha256sum -c - && uv pip install --python .venv-moss/bin/python --torch-backend=auto \"vllm[audio] @ file://$PWD/vllm-0.27.1+cu129-cp38-abi3-manylinux_2_28_x86_64.whl\"",
"HF_HUB_ENABLE_HF_TRANSFER=1 hf download OpenMOSS-Team/MOSS-Transcribe-Diarize --revision e8681d68e7042738ffca8ac8212bc8fcb1131ab8 --local-dir .models/moss-transcribe-diarize"
],
"launch": [
@@ -27,9 +27,9 @@
"curl -fsS http://127.0.0.1:8898/v1/models"
],
"smoke": [
"curl -fsS http://127.0.0.1:8898/v1/audio/transcriptions -F file=@runtime/llama.cpp/tests/sample.wav -F model=moss-transcribe-diarize -F response_format=json -F temperature=0 | tee /tmp/moss-transcription.json",
"python - <<'PY'\nimport json\n\nwith open('/tmp/moss-transcription.json', encoding='utf-8') as stream:\n payload = json.load(stream)\ntext = payload.get('text', '')\nassert text.strip(), payload\nassert '[S01]' in text, text\nprint(text)\nPY",
"python - <<'PY'\nfrom funasr import AutoModel\n\nmodel = AutoModel(model='OpenMOSS-Team/MOSS-Transcribe-Diarize', backend='vllm', vllm_base_url='http://127.0.0.1:8898/v1', vllm_model='moss-transcribe-diarize', disable_update=True)\nresult = model.generate('runtime/llama.cpp/tests/sample.wav')[0]\nassert result['raw_text'] and result['sentence_info'], result\nprint(result['text'])\nprint(result['sentence_info'])\nPY"
"curl -fsS http://127.0.0.1:8898/v1/audio/transcriptions -F file=@runtime/llama.cpp/tests/sample.wav -F model=moss-transcribe-diarize -F response_format=diarized_json -F temperature=0 | tee /tmp/moss-transcription.json",
"python - <<'PY'\nimport json\n\nwith open('/tmp/moss-transcription.json', encoding='utf-8') as stream:\n payload = json.load(stream)\ntext = payload.get('text', '')\nsegments = payload.get('segments', [])\nassert text.strip() and segments, payload\nassert all(isinstance(item.get('speaker'), str) and item.get('text') and item.get('start') <= item.get('end') for item in segments), payload\nprint(text, sorted({item['speaker'] for item in segments}))\nPY",
"python - <<'PY'\nfrom funasr import AutoModel\n\nmodel = AutoModel(model='OpenMOSS-Team/MOSS-Transcribe-Diarize', backend='vllm', vllm_base_url='http://127.0.0.1:8898/v1', vllm_model='moss-transcribe-diarize', vllm_response_format='diarized_json', disable_update=True)\nresult = model.generate('runtime/llama.cpp/tests/sample.wav')[0]\nassert result['raw_text'] and result['sentence_info'], result\nprint(result['text'])\nprint(result['sentence_info'])\nPY"
]
},
"evidence": [
@@ -37,22 +37,24 @@
{"label": "upstream source pinned at cb765f2", "url": "https://github.com/OpenMOSS/MOSS-Transcribe-Diarize/commit/cb765f2b0fe6f7a298aa2002e2281ae693d1f3c3"},
{"label": "official model snapshot pinned at e8681d68", "url": "https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize/tree/e8681d68e7042738ffca8ac8212bc8fcb1131ab8"},
{"label": "official vLLM and SGLang Omni serving guide", "url": "https://github.com/OpenMOSS/MOSS-Transcribe-Diarize/blob/cb765f2b0fe6f7a298aa2002e2281ae693d1f3c3/README.md"},
{"label": "vLLM official diarized_json response contract", "url": "https://github.com/vllm-project/vllm/pull/48543"},
{"label": "verified vLLM 0.27.1 release", "url": "https://github.com/vllm-project/vllm/releases/tag/v0.27.1"},
{"label": "FunASR ecosystem integration guide", "url": "https://github.com/modelscope/FunASR/blob/main/docs/moss_transcribe_diarize.md"},
{"label": "FunASR AutoModel adapter merge and exact-head CI", "url": "https://github.com/modelscope/FunASR/pull/3558"}
],
"benchmarks": [
{
"model": "OpenMOSS-Team/MOSS-Transcribe-Diarize@e8681d68",
"runtime": "vLLM 0.23.1rc1.dev949+g68b4a1d58 / Torch 2.11.0+cu129",
"runtime": "vLLM 0.27.1 / Torch 2.13.0+cu129",
"hardware": "NVIDIA H100 80GB HBM3",
"workload": "OpenAI-compatible transcription API smoke test plus synthetic A-to-B-to-A speaker-return probe",
"audio": "Bundled 6.000 s Mandarin sample; synthetic 19.147 s file built as sample A + 0.8 s silence + sample B + 0.8 s silence + sample A",
"settings": "Pinned model and vLLM revisions; vllm[audio]; response_format=json; temperature=0; one request at a time",
"timing_scope": "Client wall time after /health became ready; local HTTP and decoding included; model download and engine startup excluded",
"result": "Both requests returned HTTP 200. The 6.000 s sample produced S01 with timestamps 0.96-5.94. The 19.147 s probe produced S01 -> S02 -> S01 with timestamps covering 0.96-19.08. Observed client times were 1.034 s and 0.171 s respectively.",
"qualification": "A functional API contract and speaker-return probe on synthetic concatenation, not a diarization accuracy study, overlap test, throughput benchmark, or production capacity promise.",
"workload": "OpenAI-compatible diarized_json API smoke plus FunASR AutoModel structured-response mapping",
"audio": "15.1685 s two-speaker probe; SHA256 43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8",
"settings": "Pinned model revision; vLLM 0.27.1; vllm[audio]; response_format=diarized_json; temperature=0; max_completion_tokens=768",
"timing_scope": "Functional response validation after /health became ready; no latency or throughput claim",
"result": "Direct vLLM HTTP and the FunASR adapter each returned two non-empty monotonic segments with speakers S01 and S02.",
"qualification": "A structured response contract smoke, not a diarization accuracy study, overlap test, throughput benchmark, or production capacity promise.",
"source": "https://github.com/modelscope/FunASR/blob/main/docs/moss_transcribe_diarize.md",
"verified": "2026-08-29"
"verified": "2026-08-30"
}
],
"translations": {
@@ -62,11 +64,11 @@
"fit": ["会议、访谈、播客和通话的多人长音频", "希望通过同一个模型输出文本、时间戳与说话人编号", "已有 NVIDIA GPU,并需要 OpenAI 兼容的音频转写接口"],
"not_fit": ["实时流式字幕或低延迟端点检测", "CPU、桌面边缘设备或 Windows 原生部署", "要求 FunASR 自有模型权重的项目"],
"selection_reason": "模型端到端联合生成转写与说话人标签,用户不必再拼接外部 VAD、ASR 和 diarization 管线;FunASR AutoModel 为 Transformers 与 vLLM 提供统一的结构化结果。",
"primary_limitation": "这是 OpenMOSS 的第三方模型,不是 FunASR 模型;“无需外部 VAD”不表示模型内部没有分块或分段。当前 vLLM 路径固定 nightly commit,升级前必须重跑真实多人长音频。",
"primary_limitation": "这是 OpenMOSS 的第三方模型,不是 FunASR 模型;“无需外部 VAD”不表示模型内部没有分块或分段。当前 vLLM 路径固定 0.27.1 发布版,升级前必须重跑真实多人长音频。",
"status_label": "社区验证",
"operations": ["固定模型 revision、FunASR adapter merge、vLLM nightly commit、CUDA/Torch 与 trust_remote_code 审计结果", "用真实会议验证说话人一致性、重叠语音、长静音、时间戳和最大生成长度", "FunASR AutoModel 保留 raw_text,并统一返回 text、timestamp 和带 spk 的 sentence_info;直接调用 vLLM 时 json 仍是原始 [Sxx] 标记文本"],
"operations": ["固定模型 revision、FunASR adapter merge、vLLM 0.27.1、CUDA/Torch 与 trust_remote_code 审计结果", "用真实会议验证说话人一致性、重叠语音、长静音、时间戳和最大生成长度", "FunASR AutoModel 可用 vLLM diarized_json 直接统一 text、timestamp 和带 spk 的 sentence_info;json 兼容路径仍在 raw_text 保留原始 [Sxx] 标记文本"],
"security": ["把服务绑定内网,由网关完成认证、TLS、限流及音频大小/时长限制", "固定并审计 trust_remote_code 对应的模型 revision,不执行浮动 main 代码", "隔离模型缓存、上传临时目录和生成日志,按数据保留策略清理原始音频"],
"troubleshooting": ["启动前确认 CUDA 12 使用 cu129 nightly 源,CUDA 13 使用上游文档列出的 cu130 源", "长音频被截断时提高 max_completion_tokens,并同时监控显存、延迟和输出完整性", "直接调用 vLLM 时 json 没有 segments 字段是当前接口边界;使用 FunASR AutoModel 归一化结果,或改用并单独验证 SGLang Omni verbose_json"]
"troubleshooting": ["CUDA 12 使用已校验 SHA256 的 vLLM 0.27.1 cu129 wheel;其他 CUDA 栈需按上游发布资产重新验证", "长音频被截断时提高 max_completion_tokens,并同时监控显存、延迟和输出完整性", "需要结构化说话人 segments 时使用 vLLM diarized_json;旧 68b4a1d5 nightly 会返回 HTTP 400,json 只返回原始 [Sxx] 标签文本"]
},
"en": {
"name": "MOSS unified transcription and diarization",
@@ -74,11 +76,11 @@
"fit": ["Multi-speaker meetings, interviews, podcasts, and calls", "One model output for text, timestamps, and speaker identifiers", "NVIDIA GPU deployments that need an OpenAI-compatible audio transcription endpoint"],
"not_fit": ["Realtime streaming captions or low-latency endpoint detection", "CPU, desktop-edge, or native Windows deployments", "Projects that require FunASR-owned model weights"],
"selection_reason": "The model jointly emits transcription and speaker labels so users do not need to assemble an external VAD, ASR, and diarization pipeline; FunASR AutoModel provides one structured result across Transformers and vLLM.",
"primary_limitation": "This is an OpenMOSS third-party model, not a FunASR model; no external VAD requirement does not imply an absence of internal segmentation. The vLLM path pins a nightly commit and must be revalidated on real long multi-speaker audio before upgrades.",
"primary_limitation": "This is an OpenMOSS third-party model, not a FunASR model; no external VAD requirement does not imply an absence of internal segmentation. The vLLM path pins release 0.27.1 and must be revalidated on real long multi-speaker audio before upgrades.",
"status_label": "Community verified",
"operations": ["Pin the model revision, FunASR adapter merge, vLLM nightly commit, CUDA/Torch stack, and trust_remote_code audit", "Validate speaker consistency, overlap, long silence, timestamps, and generation limits on real meetings", "FunASR AutoModel preserves raw_text and normalizes text, timestamp, and sentence_info with spk; direct vLLM json still returns raw [Sxx]-tagged text"],
"operations": ["Pin the model revision, FunASR adapter merge, vLLM 0.27.1, CUDA/Torch stack, and trust_remote_code audit", "Validate speaker consistency, overlap, long silence, timestamps, and generation limits on real meetings", "FunASR AutoModel can map vLLM diarized_json directly into text, timestamp, and sentence_info with spk; the json compatibility path still preserves raw [Sxx]-tagged text in raw_text"],
"security": ["Bind workers to a private network and put authentication, TLS, rate limits, and audio size/duration limits at the gateway", "Pin and audit the trust_remote_code model revision instead of executing a floating main revision", "Isolate model caches, upload directories, and generation logs; remove source audio according to retention policy"],
"troubleshooting": ["Use the cu129 nightly source for CUDA 12 and the upstream cu130 source for CUDA 13", "If long audio is truncated, raise max_completion_tokens while monitoring memory, latency, and output completeness", "A direct vLLM json response without segments is the current API boundary; normalize it with FunASR AutoModel or use and separately validate SGLang Omni verbose_json"]
"troubleshooting": ["For CUDA 12, use the SHA256-verified vLLM 0.27.1 cu129 wheel; revalidate another CUDA stack against its upstream release asset", "If long audio is truncated, raise max_completion_tokens while monitoring memory, latency, and output completeness", "Use vLLM diarized_json for structured speaker segments; the old 68b4a1d5 nightly returns HTTP 400, while json only returns raw [Sxx]-tagged text"]
}
}
},
@@ -186,7 +186,9 @@ for (const viewport of [
await expect(page.getByText('FunASR AutoModel', { exact: false }).first()).toBeVisible();
await expect(page.getByText('OpenMOSS', { exact: false }).first()).toBeVisible();
await expect(page.getByText('sentence_info', { exact: false }).first()).toBeVisible();
await expect(page.getByText('diarized_json', { exact: false }).first()).toBeVisible();
await expect(page.locator('a[href="https://github.com/modelscope/FunASR/pull/3558"]')).toBeVisible();
await expect(page.locator('a[href="https://github.com/vllm-project/vllm/pull/48543"]')).toBeVisible();
await expect(page.locator('a[href="/en/deploy/moss-transcribe-diarize.html"]')).toBeVisible();
const layout = await page.evaluate(() => ({
+17 -12
View File
@@ -107,14 +107,15 @@ def test_moss_transcribe_diarize_contract_tracks_third_party_upstream(valid_regi
'OpenMOSS-Team/MOSS-Transcribe-Diarize (third-party Apache-2.0 model)'
]
assert entry['tested'] == {
'funasr': 'AutoModel adapter@a3a2de0a; third-party model@e8681d68',
'runtime': 'Transformers 5.16.1 + vLLM 0.23.1rc1.dev949+g68b4a1d58 / Torch 2.11.0+cu129 / H100 80GB',
'verified': '2026-08-29',
'funasr': 'AutoModel diarized_json adapter; third-party model@e8681d68',
'runtime': 'vLLM 0.27.1 / Torch 2.13.0+cu129 / H100 80GB',
'verified': '2026-08-30',
}
install = '\n'.join(entry['commands']['install'])
assert 'vllm[audio]' in install
assert 'wheels.vllm.ai/68b4a1d582818e67adc903bf1b8fc5a5447da2fa/cu129' in install
assert 'vllm-0.27.1%2Bcu129' in install
assert 'bf0d52faa2a51e7a01c6856a7a8a2d1307fd0ff711415d34168a67ffac0fa47b' in install
assert 'OpenMOSS-Team/MOSS-Transcribe-Diarize' in install
assert 'e8681d68e7042738ffca8ac8212bc8fcb1131ab8' in install
@@ -123,10 +124,11 @@ def test_moss_transcribe_diarize_contract_tracks_third_party_upstream(valid_regi
assert '--trust-remote-code' in launch
smoke = '\n'.join(entry['commands']['smoke'])
assert '/v1/audio/transcriptions' in smoke
assert 'response_format=json' in smoke
assert '[S01]' in smoke
assert 'response_format=diarized_json' in smoke
assert "payload.get('segments'" in smoke
assert "from funasr import AutoModel" in smoke
assert "backend='vllm'" in smoke
assert "vllm_response_format='diarized_json'" in smoke
assert "result['raw_text']" in smoke
assert "result['sentence_info']" in smoke
@@ -141,6 +143,7 @@ def test_moss_transcribe_diarize_contract_tracks_third_party_upstream(valid_regi
'e8681d68e7042738ffca8ac8212bc8fcb1131ab8'
) in evidence_urls
assert 'https://github.com/modelscope/FunASR/pull/3558' in evidence_urls
assert 'https://github.com/vllm-project/vllm/pull/48543' in evidence_urls
english = entry['translations']['en']
assert 'OpenMOSS' in english['summary']
@@ -151,14 +154,16 @@ def test_moss_transcribe_diarize_contract_tracks_third_party_upstream(valid_regi
assert 'FunASR AutoModel API' not in ' '.join(english['not_fit'])
assert 'sentence_info' in english['operations'][-1]
assert 'raw_text' in english['operations'][-1]
assert 'diarized_json' in english['operations'][-1]
assert 'diarized_json' in english['troubleshooting'][-1]
assert 'internal segmentation' in english['primary_limitation']
assert 'FunASR model' in english['primary_limitation']
assert any(
benchmark['hardware'] == 'NVIDIA H100 80GB HBM3'
and '6.000 s' in benchmark['audio']
and '19.147 s' in benchmark['audio']
and 'S01 -> S02 -> S01' in benchmark['result']
and 'functional API contract' in benchmark['qualification']
and '15.1685 s' in benchmark['audio']
and '43dccc068506439cb633b382b6b98185baa837363d08cc5f7152ca89b0fdc3c8' in benchmark['audio']
and 'S01 and S02' in benchmark['result']
and 'contract smoke' in benchmark['qualification']
for benchmark in entry['benchmarks']
)
@@ -167,7 +172,7 @@ def test_audio_cpp_contract_tracks_mainline_nano_and_sensevoice(valid_registry):
entry = next(item for item in valid_registry['deployments'] if item['id'] == 'audio-cpp')
llama_cpp = next(item for item in valid_registry['deployments'] if item['id'] == 'llama-cpp')
assert valid_registry['verified'] == '2026-08-29'
assert valid_registry['verified'] == '2026-08-30'
assert entry['maturity'] == 'community-verified'
assert entry['selector_rank'] > llama_cpp['selector_rank']
assert entry['tested'] == {
@@ -333,7 +338,7 @@ def test_sensevoice_native_server_contract_tracks_merged_runtime(valid_registry)
llama_cpp = next(item for item in valid_registry['deployments'] if item['id'] == 'llama-cpp')
audio_cpp = next(item for item in valid_registry['deployments'] if item['id'] == 'audio-cpp')
assert valid_registry['verified'] == '2026-08-29'
assert valid_registry['verified'] == '2026-08-30'
assert entry['maturity'] == 'production-verified'
assert llama_cpp['selector_rank'] < entry['selector_rank'] < audio_cpp['selector_rank']
assert entry['tested'] == {