MOSS-Transcribe-Diarize 0.9B 私有化部署实战:一个模型完成会议转写、说话人识别与时间戳
以前搭建一套多人会议语音处理系统,往往需要部署三个相互独立的模型。
ASR 负责语音转文字,Speaker Diarization 负责判断谁在什么时候发言,Forced Aligner 负责把文本重新对齐到原始音频。
一套典型链路是:
会议录音
↓
Qwen3-ASR / Whisper
↓
转写文本
↓
Sortformer / pyannote
↓
说话人时间轴
↓
ForcedAligner
↓
文本、说话人和时间戳融合
↓
会议纪要
这套架构已经能够解决大多数会议处理需求,但真正部署以后,会发现麻烦往往不在单个模型,而在模型之间的数据衔接。
ASR 和 Diarization 对语音边界的判断未必一致;说话人可能在一句话中途发生切换;长会议切片以后,不同片段的 Speaker ID 也可能发生重排。
如果再考虑三个模型各自的 Python 环境、GPU 显存、推理框架和接口协议,一套会议语音链路很容易变成多个独立服务。
2026 年 7 月,OpenMOSS 开源了 MOSS-Transcribe-Diarize 0.9B,提供了一种不同的思路:让模型直接从音频中联合生成转写文本、说话人标签和时间戳。
它不再要求先完成 ASR,再额外调用一个 Speaker Diarization 模型来组织发言关系。
这次我们重点研究三个问题:
- 一个 0.9B 模型究竟能替代原来多少模块?
- 如何把它部署成可以长期运行的内网语音服务?
- 原有会议系统需要怎样调整接口,才能接收这种联合建模结果?
整篇从 Ubuntu 服务器开始,逐步完成模型离线下载、Python 推理、结构化输出解析、FastAPI、Docker、长音频处理以及业务集成。
一、MOSS-Transcribe-Diarize 和普通 ASR 有什么不同?
传统 ASR 的输出通常是:
{
"text": "接口联调预计周五完成。"
}
经过时间对齐以后,可以得到:
{
"start": 12.32,
"end": 15.48,
"text": "接口联调预计周五完成。"
}
再加入说话人模块,最终才能变成:
{
"speaker": "speaker_1",
"start": 12.32,
"end": 15.48,
"text": "接口联调预计周五完成。"
}
MOSS-Transcribe-Diarize 的区别,是直接生成类似这样的文本序列:
[0.48][S01]今天先确认一下项目进度。[3.20]
[3.84][S02]接口联调预计周五完成。[7.12]
[7.56][S01]好的,测试环境也同步准备。[11.30]
这里包含三种信息:
| 字段 | 作用 |
|---|---|
[0.48] |
当前发言开始时间,单位为秒 |
[S01] |
匿名说话人标签 |
| 正文 | 识别出的语音文本 |
[3.20] |
当前发言结束时间 |
与传统分模块架构相比,它最大的变化是文本和说话人标签由同一个模型生成,能够减少外部 ASR 与 Diarization 的结果匹配工作。
根据 OpenMOSS 官方模型说明,0.9B 版本支持 50 多种语言、热词提示以及最长 90 分钟的单次音频处理。模型权重采用 Apache-2.0 License。
不过需要区分两个概念。
MOSS 输出的 [S01]、[S02] 是相对于当前录音的匿名说话人编号,并不代表模型已经识别出某个具体人员的真实身份。
另外,模型生成的是带时间范围的发言片段,不等同于专用 Forced Aligner 输出的逐字级精确时间戳。
如果业务需要逐字字幕、高精度时间定位或真实姓名绑定,仍然可能需要后续模块。
二、先看模型架构:0.9B 为什么能同时处理这几种任务?
MOSS-Transcribe-Diarize 并不是简单把 Whisper 和声纹聚类打包到一个 Python 项目里。
它采用的是音频编码器与文本解码器相结合的架构。
根据官方公开的模型结构,主要包括:
| 组件 | 技术结构 |
|---|---|
| 音频编码器 | Whisper-Medium 配置 |
| 文本主干 | Qwen3-0.6B 风格因果解码器 |
| 输入音频 | 16kHz |
| 音频特征 | 80维 Mel 特征 |
| 音频处理 | 30秒特征分块 |
| 音频文本桥接 | 时间维度合并与 MLP |
| 输出 | 文本、Speaker 标签和时间戳 |
可以简单理解成:
会议音频
↓
Whisper Audio Encoder
↓
Audio Features
↓
Temporal Merge + MLP
↓
Qwen-style Decoder
↓
[start][speaker]text[end]
音频编码器负责提取语音特征,文本解码器则根据这些音频信息生成包含结构化标记的结果。
这里还有一个值得注意的细节。
模型虽然只有 0.9B 参数,但并不意味着 0.9B 模型就一定能够在任意显卡上轻松处理 90 分钟会议。
长音频产生的音频特征、注意力中间结果和生成 token,同样会影响显存占用。
因此我们仍然需要根据实际 GPU 配置设计长音频处理和并发策略。
三、部署前准备:先固定 Python、CUDA 和项目版本
先检查宿主机。
nvidia-smi
确认 GPU、驱动版本以及当前显存占用。
python3 --version
docker --version
OpenMOSS 官方项目采用 Python 3.12 和 Transformers 5.x 进行验证,因此新建环境时尽量不要直接复用已有的 Whisper 或 Qwen3-ASR 虚拟环境。
创建独立目录:
mkdir -p /data/moss-meeting
cd /data/moss-meeting
mkdir -p models logs tmp tests
建议目录结构:
/data/moss-meeting/
├── app/
│ ├── server.py
│ └── benchmark.py
│
├── source/
│ └── MOSS-Transcribe-Diarize/
│
├── models/
│ └── MOSS-Transcribe-Diarize/
│
├── tests/
│ └── meeting.wav
│
├── tmp/
├── logs/
└── docker/
└── Dockerfile
将代码、模型权重、临时音频和日志分开存放。
这种目录安排看起来很普通,但等到后面 Docker 镜像升级、模型版本切换、客户环境迁移时,会省掉很多麻烦。
四、安装官方项目与推理环境
先安装 FFmpeg:
sudo apt-get update
sudo apt-get install -y \
ffmpeg \
libsndfile1 \
git
创建环境:
conda create \
-n moss-meeting \
python=3.12 \
-y
conda activate moss-meeting
安装 uv:
python -m pip install -U uv
拉取官方项目:
cd /data/moss-meeting/source
git clone \
https://github.com/OpenMOSS/MOSS-Transcribe-Diarize.git
cd MOSS-Transcribe-Diarize
为了后续复现,记录当前源码提交:
git rev-parse HEAD
按照官方环境方式安装:
uv pip install -e ".[torch-runtime]" \
--torch-backend=auto
这会安装项目依赖和 PyTorch 运行环境。
对于已经固定 CUDA、驱动和 PyTorch 的生产机器,也可以先安装经过验证的 PyTorch 版本,再安装项目自身。
安装以后检查:
python - <<'PY'
import torch
import transformers
import moss_transcribe_diarize
print("torch:", torch.__version__)
print("transformers:", transformers.__version__)
print("cuda:", torch.cuda.is_available())
if torch.cuda.is_available():
print("gpu:", torch.cuda.get_device_name(0))
PY
如果能够正确导入模块,并且 torch.cuda.is_available() 返回 True,就可以继续。
不过需要注意:PyTorch 能看到 GPU,并不代表 MOSS 模型一定能在当前显卡上完整运行。
模型加载和实际推理仍然需要单独验证。
五、正式部署不要让模型在首次启动时自动下载
开发阶段可以直接通过 Hugging Face 模型名称加载:
model_id = "OpenMOSS-Team/MOSS-Transcribe-Diarize"
首次调用时,Transformers 会从模型仓库下载权重及对应配置。
但企业内网部署不能依赖这种方式。
更适合的流程是:
联网准备机器
↓
下载模型
↓
锁定版本
↓
检查文件完整性
↓
打包传输
↓
目标内网服务器
↓
从本地路径加载
安装下载工具:
python -m pip install -U huggingface_hub
下载:
hf download \
OpenMOSS-Team/MOSS-Transcribe-Diarize \
--local-dir \
/data/moss-meeting/models/MOSS-Transcribe-Diarize
模型下载完成后检查:
du -sh \
/data/moss-meeting/models/MOSS-Transcribe-Diarize
再生成完整性校验文件:
cd /data/moss-meeting/models/MOSS-Transcribe-Diarize
find . -type f \
! -name SHA256SUMS \
-print0 \
| sort -z \
| xargs -0 sha256sum \
> SHA256SUMS
之后可以使用:
sha256sum -c SHA256SUMS
验证复制后的文件是否一致。
MOSS 使用自定义 Transformers 模型代码,加载时需要 trust_remote_code=True。因此正式交付前不应只检查权重,还应固定并审查模型仓库对应的 Python 实现。
六、先跑通最小推理,不要急着封装 FastAPI
完成环境安装后,先准备一段普通话会议录音。
ffmpeg -y \
-i original_meeting.m4a \
-ar 16000 \
-ac 1 \
-c:a pcm_s16le \
/data/moss-meeting/tests/meeting.wav
这里统一为:
16kHz
Mono
PCM WAV
然后新建 test_moss.py:
import torch
from transformers import (
AutoModelForCausalLM,
AutoProcessor,
)
from moss_transcribe_diarize import (
parse_transcript,
)
from moss_transcribe_diarize.inference_utils import (
build_transcription_messages,
generate_transcription,
resolve_device,
)
MODEL_PATH = (
"/data/moss-meeting/models/"
"MOSS-Transcribe-Diarize"
)
AUDIO_PATH = (
"/data/moss-meeting/tests/meeting.wav"
)
device = resolve_device("auto")
dtype = (
torch.bfloat16
if device.type == "cuda"
else torch.float32
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
dtype="auto",
attn_implementation="sdpa",
).to(dtype=dtype).to(device).eval()
processor = AutoProcessor.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
)
messages = build_transcription_messages(
AUDIO_PATH
)
result = generate_transcription(
model,
processor,
messages,
max_new_tokens=8192,
do_sample=False,
device=device,
dtype=dtype,
)
print("Raw Output:")
print(result["text"])
print("\nParsed Segments:")
for segment in parse_transcript(
result["text"]
):
print(
segment.start,
segment.end,
segment.speaker,
segment.text,
)
这里使用的是官方提供的 generate_transcription() 和 parse_transcript(),而不是自己拼接 Transformers 的音频 Prompt。
执行:
python test_moss.py
正常情况下,模型首先产生带时间戳和 Speaker 标签的文本序列,然后通过解析器转换为多个结构化 Segment。
一段会议的输出格式可以类似:
[1.24][S01]今天先确认一下项目上线计划。[5.82]
[6.12][S02]接口联调周五之前可以完成。[10.36]
[11.04][S01]那测试环境同步准备。[14.27]
注意,这里展示的是输出结构示例,并不是固定的识别结果。
实际说话人数量、分段长度和内容由输入音频决定。
七、为什么不建议自己用正则表达式解析模型输出?
看到输出格式以后,很容易想到:
import re
然后自行匹配:
[start][speaker]text[end]
这种方法可以快速制作 Demo,但对于生产环境并不稳妥。
原因是模型生成的是一条连续序列。
其中可能存在:
多个片段连续拼接
文本中的数字
额外声学事件标记
不完整时间戳
异常空文本
输出被max_new_tokens截断
因此官方已经提供的解析器应该优先使用。
我们可以在官方 parse_transcript() 外面再增加一层业务数据转换:
from moss_transcribe_diarize import (
parse_transcript,
)
def normalize_segments(raw_text: str):
result = []
for item in parse_transcript(raw_text):
start_ms = round(
float(item.start) * 1000
)
end_ms = round(
float(item.end) * 1000
)
text = item.text.strip()
if end_ms <= start_ms:
continue
if not text:
continue
result.append({
"speaker_id": str(item.speaker),
"start_ms": start_ms,
"end_ms": end_ms,
"text": text,
})
return result
最终:
[
{
"speaker_id": "S01",
"start_ms": 1240,
"end_ms": 5820,
"text": "今天先确认一下项目上线计划。"
},
{
"speaker_id": "S02",
"start_ms": 6120,
"end_ms": 10360,
"text": "接口联调周五之前可以完成。"
}
]
这一步看起来只是格式转换,却是新模型接入已有会议系统时最重要的适配层。
在熙瑾会悟的接入设计中,可以保留原有会议数据协议,让 MOSS 的联合识别结果经过 Adapter 转换后直接进入逐字稿、会议回听和纪要处理链路。
这样业务层不需要知道底层究竟采用了三个独立模型,还是一个联合模型。
八、企业会议里最实用的一个能力:热词提示
通用 ASR 经常遇到一个问题:
普通文字识别没有太大问题,但一到企业名称、产品型号、技术缩写,就容易出现错误。
比如:
Prometheus
Grafana
昇腾910B
CUDA
Kubernetes
这些词在日常语音中出现频率不高,但在技术会议里很常见。
MOSS 支持通过 Prompt 提供热词提示。
官方默认的结构化转写指令可以扩展为:
prompt = """
请将音频转写为文本,每一段需以起始时间戳
和说话人编号([S01]、[S02]、[S03]…)
开头,正文为对应的语音内容,
并在段末标注结束时间戳。
热词提示:
Prometheus, Grafana, 昇腾910B,
Kubernetes, CUDA
"""
通过官方 build_transcription_messages() 的 Prompt 参数构造对应消息,即可把热词提示纳入模型输入。
但需要注意:
热词提示不等于硬约束词典。
它是在生成时提供上下文线索,并不保证指定专业术语一定正确。
因此企业术语场景仍然建议保留自己的测试集,单独统计专业词命中率。
九、生产服务化:把模型加载和会议请求分开
模型推理跑通以后,下一步是将其封装成内部 HTTP 服务。
这里最不建议的写法是:
@app.post("/transcribe")
def transcribe(...):
model = AutoModelForCausalLM.from_pretrained(
MODEL_PATH
)
每个请求都重新加载一次模型。
正确思路应该是:
服务启动
↓
加载模型
↓
GPU常驻
↓
等待请求
↓
处理音频
↓
返回结构化结果
↓
等待下一次请求
新建 server.py。
首先初始化:
import os
import asyncio
import shutil
import tempfile
import time
from pathlib import Path
import torch
from fastapi import (
FastAPI,
File,
HTTPException,
UploadFile,
)
from transformers import (
AutoModelForCausalLM,
AutoProcessor,
)
from moss_transcribe_diarize import (
parse_transcript,
)
from moss_transcribe_diarize.inference_utils import (
build_transcription_messages,
generate_transcription,
resolve_device,
)
MODEL_PATH = os.getenv(
"MODEL_PATH",
"/models/MOSS-Transcribe-Diarize",
)
TMP_ROOT = Path(
os.getenv("TMP_ROOT", "/app/tmp")
)
TMP_ROOT.mkdir(
parents=True,
exist_ok=True,
)
device = resolve_device("auto")
dtype = (
torch.bfloat16
if device.type == "cuda"
else torch.float32
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
dtype="auto",
attn_implementation="sdpa",
).to(dtype=dtype).to(device).eval()
processor = AutoProcessor.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
)
app = FastAPI(
title="MOSS Meeting ASR",
version="1.0.0",
)
gpu_lock = asyncio.Lock()
这里先采用单模型、单请求推理锁。
它不是最终吞吐最高的方案,但适合在第一轮部署时排除多请求显存竞争问题。
增加推理函数:
def run_inference(audio_path: str):
messages = build_transcription_messages(
audio_path
)
result = generate_transcription(
model,
processor,
messages,
max_new_tokens=8192,
do_sample=False,
device=device,
dtype=dtype,
)
segments = []
for item in parse_transcript(
result["text"]
):
segments.append({
"speaker_id": str(item.speaker),
"start_ms": int(item.start * 1000),
"end_ms": int(item.end * 1000),
"text": item.text.strip(),
})
return {
"raw_text": result["text"],
"segments": segments,
}
再增加音频标准化:
import subprocess
def normalize_audio(
input_path: str,
output_path: str,
):
command = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-y",
"-i", input_path,
"-ar", "16000",
"-ac", "1",
"-c:a", "pcm_s16le",
output_path,
]
subprocess.run(
command,
check=True,
timeout=300,
)
HTTP 接口:
@app.get("/health")
def health():
return {
"status": "ok",
"model": "MOSS-Transcribe-Diarize",
"device": str(device),
}
@app.post("/api/v1/transcribe")
async def transcribe(
audio: UploadFile = File(...),
):
work_dir = Path(
tempfile.mkdtemp(
dir=TMP_ROOT
)
)
input_path = work_dir / "input_audio"
normalized_path = work_dir / "audio.wav"
try:
with input_path.open("wb") as f:
while True:
chunk = await audio.read(
1024 * 1024
)
if not chunk:
break
f.write(chunk)
await asyncio.to_thread(
normalize_audio,
str(input_path),
str(normalized_path),
)
started = time.perf_counter()
async with gpu_lock:
result = await asyncio.to_thread(
run_inference,
str(normalized_path),
)
cost = (
time.perf_counter() - started
)
return {
"process_sec": round(cost, 3),
**result,
}
except Exception as exc:
print(
"MOSS_ERROR",
type(exc).__name__,
str(exc),
)
raise HTTPException(
status_code=500,
detail="会议转写失败",
) from exc
finally:
shutil.rmtree(
work_dir,
ignore_errors=True,
)
这段代码适合作为服务化起点。正式对外提供上传接口时,还需要由网关限制文件大小、认证访问来源,并对音频时长进行检查,避免异常请求无限占用 GPU。
启动:
uvicorn server:app \
--host 127.0.0.1 \
--port 18230 \
--workers 1
请求:
curl -X POST \
http://127.0.0.1:18230/api/v1/transcribe \
-F "audio=@meeting.wav"
返回的核心内容:
{
"process_sec": 12.38,
"segments": [
{
"speaker_id": "S01",
"start_ms": 1240,
"end_ms": 5820,
"text": "今天先确认一下项目上线计划。"
}
]
}
这里的处理耗时仅用于展示接口字段,不代表特定硬件的实测速度。
十、Docker部署:模型权重与运行镜像分离
完成 Python 验证以后,再考虑 Docker。
对私有化部署来说,我更建议:
Docker Image
↓
Python运行环境
模型推理代码
API服务
Model Directory
↓
MOSS权重
Tokenizer
Processor
自定义模型代码
Config
↓
端口
GPU
模型路径
临时目录
不要把所有东西都直接打进一个不断更新的大镜像。
尤其是模型权重,不建议在每次业务代码更新时重复复制。
可以先使用 Python 3.12 基础镜像构建运行环境。
Dockerfile:
FROM python:3.12-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
ffmpeg \
libsndfile1 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install \
--no-cache-dir \
--index-url \
https://download.pytorch.org/whl/cu128 \
torch torchaudio
COPY MOSS-Transcribe-Diarize \
/opt/moss-source
RUN python -m pip install \
--no-cache-dir \
/opt/moss-source
WORKDIR /app
COPY server.py /app/server.py
RUN mkdir -p /app/tmp
ENV MODEL_PATH=/models/MOSS-Transcribe-Diarize
ENV TMP_ROOT=/app/tmp
EXPOSE 18230
CMD ["python", "-m", "uvicorn", \
"server:app", \
"--host", "0.0.0.0", \
"--port", "18230", \
"--workers", "1"]
这份 Dockerfile 使用 CUDA 12.8 的 PyTorch wheel 路线,目标机器需要匹配的 NVIDIA 驱动及 Container Toolkit。实际交付时应固定 PyTorch、Transformers 和项目源码版本,而不是长期使用浮动依赖。
构建:
docker build \
-t moss-meeting:v1.0.0 \
.
启动:
docker run -d \
--name moss-meeting \
--restart unless-stopped \
--gpus '"device=0"' \
--shm-size=4g \
-p 127.0.0.1:18230:18230 \
-v /data/moss-meeting/models/MOSS-Transcribe-Diarize:/models/MOSS-Transcribe-Diarize:ro \
-v /data/moss-meeting/tmp:/app/tmp \
-e MODEL_PATH=/models/MOSS-Transcribe-Diarize \
-e HF_HUB_OFFLINE=1 \
-e TRANSFORMERS_OFFLINE=1 \
moss-meeting:v1.0.0
这里有三个关键细节。
第一个是:
--gpus device=0
固定目标 GPU。
第二个是模型目录:
:ro
只读挂载,防止运行过程意外修改模型。
第三个是:
HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
要求 Hugging Face 相关组件在运行阶段优先使用本地资源,避免启动时重新访问远程模型仓库。
十一、为什么官方还推荐 SGLang Omni 和 vLLM?
上面的 Transformers 路线适合验证模型能力和封装基础服务。
但真正需要多任务并发时,单 Python 服务并不是唯一选择。
MOSS 官方目前提供三种主要推理方向:
| 路径 | 更适合的用途 | 特点 |
|---|---|---|
| Transformers | 研发验证、定制处理 | 接口灵活,方便调试 |
| vLLM | GPU服务化 | 支持统一转写接口与推理调度 |
| SGLang Omni | 长音频、高吞吐场景 | 官方推荐的优化服务路线 |
SGLang Omni 当前主要面向 CUDA 13 环境,提供兼容 OpenAI 的 /v1/audio/transcriptions 接口。
在已经安装并验证 SGLang Omni 的环境中,可以使用:
sgl-omni serve \
--model-path \
OpenMOSS-Team/MOSS-Transcribe-Diarize \
--port 8000 \
--max-running-requests 16 \
--cuda-graph-max-bs 16 \
--mem-fraction-static 0.80
如果要获得解析后的 Speaker Segments,可以请求:
curl -X POST \
http://127.0.0.1:8000/v1/audio/transcriptions \
-F model=OpenMOSS-Team/MOSS-Transcribe-Diarize \
-F file=@meeting.wav \
-F response_format=verbose_json
这里的 verbose_json 比普通 json 更适合会议系统,因为它可以直接返回结构化分段,而不是只返回原始转写文本。
对于 CUDA 12 环境,可以考虑官方文档中的 vLLM 路线。
但需要注意:普通 vLLM 稳定版不一定包含 MOSS-Transcribe-Diarize 的模型注册。
官方提供了特定的 vLLM nightly 构建,因此正式部署前应按照仓库 README 固定支持该模型的运行时版本。
这也是为什么实际迁移时,我更建议先验证 Transformers 路径,再切换推理后端。
这样可以把问题分成两类:
模型本身是否正确
以及:
服务框架是否正确
不会把两者混在一起排查。
十二、长会议处理:支持90分钟不等于所有环境都能一次跑完
MOSS 官方介绍了最长 90 分钟的单次音频处理能力。
但对于生产环境,这个数字仍然需要结合显存、生成长度和服务并发一起考虑。
例如一段一个小时的会议,除了输入音频特征以外,还会产生大量转写 token:
音频
↓
音频特征
↓
文本解码
↓
时间戳
Speaker标签
转写文字
如果仍然使用:
max_new_tokens=2048
就可能导致长会议转写没有生成完整。
官方 SGLang Omni 文档也建议对长音频增大 max_new_tokens,甚至给出了 65536 的设置示例。
不过这个参数同样会影响显存和生成开销,不应该无条件调到最大。
我更建议按三个阶段验证:
| 音频长度 | 验证目标 |
|---|---|
| 1~3分钟 | API与输出格式正确 |
| 10~20分钟 | 常规会议显存、速度 |
| 60~90分钟 | 长音频稳定性和完整性 |
对于资源有限的服务器,仍然可以设置业务侧切片。
例如每段 10 分钟,保留适当重叠区域。
def build_chunks(
duration_sec: float,
chunk_sec: float = 600,
overlap_sec: float = 10,
):
chunks = []
start = 0.0
while start < duration_sec:
end = min(
start + chunk_sec,
duration_sec,
)
chunks.append({
"start": start,
"end": end,
})
if end >= duration_sec:
break
start = end - overlap_sec
return chunks
但联合模型同样存在跨切片 Speaker ID 问题。
第一段中的:
S01 = 张工
不代表第二段:
S01 = 张工
如果把音频拆开独立推理,匿名 Speaker 标签就需要重新建立跨片段映射。
因此对 MOSS 来说,一次性处理完整会议除了减少接口调用,还有保持说话人上下文一致性的价值。
是否切片,需要结合显存、长会议错误率和 Speaker 连续性进行选择。
十三、怎么衡量新模型是否真的比原来的三模型链路更好?
这是本文最值得做的实验。
以前的架构:
Qwen3-ASR
+
Sortformer
+
ForcedAligner
现在的架构:
MOSS-Transcribe-Diarize
不能只比较:
用了几个模型
还要比较识别质量和工程开销。
建议测试至少包括:
| 指标 | 三模型链路 | MOSS联合模型 |
|---|---|---|
| CER | 单独测量 | 单独测量 |
| cpCER | 单独测量 | 单独测量 |
| DER | 单独测量 | 单独测量 |
| RTF | 整链路计算 | 整链路计算 |
| 峰值显存 | 三模块峰值及调度方式 | 单模型峰值 |
| 服务数量 | 多个模型服务 | 一个联合推理服务 |
| Speaker一致性 | 依赖融合与跨段处理 | 需要检验联合输出 |
| 时间戳精度 | 可使用ForcedAligner | 模型直接生成段级时间 |
| 长会议失败率 | 单独统计 | 单独统计 |
| 运维复杂度 | 多套模型依赖 | 单套核心模型依赖 |
这里建议重点关注三个指标。
CER(字符错误率)衡量文本识别质量。
cpCER(最小排列拼接字符错误率)综合考察不同说话人对应的转写内容,比较适合研究带 Speaker 标签的转写任务。
DER(说话人分离错误率)则单独衡量说话人时间轴,包括漏检、误检和说话人混淆。
cpCER 与 DER 并不是同一个指标,不能只测 cpCER 就认定说话人时间轴已经足够准确。
OpenMOSS 官方在 AISHELL-4 测试中公布了 0.9B 模型的 CER 为 14.84%、cpCER 为 15.83%,在 Alimeeting 上也提供了对应测试结果。
这些是官方公开测试集的数据,不能直接当成企业实际会议的准确率。
更有价值的做法,是用自己的中文多人会议录音跑一轮 A/B 测试。
性能测试建议
测试前先固定:
GPU型号
驱动版本
CUDA环境
模型Revision
PyTorch版本
推理框架版本
音频格式
测试样本
输出长度
随后计算:
def calculate_rtf(
process_sec: float,
audio_sec: float,
):
if audio_sec <= 0:
raise ValueError(
"audio_sec must be positive"
)
return process_sec / audio_sec
例如一段 600 秒音频,处理耗时 48 秒:
RTF = 48 / 600
= 0.08
表示离线处理速度快于实时播放速度。
需要特别注意,三模型链路的耗时应该统计完整处理过程,而不是只拿 Qwen3-ASR 的推理耗时与 MOSS 比较。
否则结果会明显偏向原有架构。
十四、接入熙瑾会悟时,最值得保留的是统一Adapter
从模型选型角度看,MOSS 最吸引人的地方是降低多模型融合复杂度。
但从会议产品角度看,更值得保留的反而是统一接口。
不管底层采用:
Whisper
+
Speaker模型
+
ForcedAligner
还是:
MOSS-Transcribe-Diarize
上层都应该得到相同的数据结构:
{
"meeting_id": "M20261022001",
"segments": [
{
"speaker_id": "S01",
"start_ms": 1240,
"end_ms": 5820,
"text": "今天先确认一下项目上线计划。"
}
]
}
这样会议业务层才能继续复用:
逐字稿展示
↓
Speaker标签
↓
点击文字回听
↓
会议摘要
↓
决策与待办提取
↓
归档与全文检索
对于熙瑾会悟的模型接入设计,可以让旧的多模块链路与新的 MOSS Adapter 并存。
上线前先使用同一批会议进行双路推理:
同一份会议音频
│
├── 原ASR+Speaker链路
│ ↓
│ 结构化文本A
│
└── MOSS联合模型
↓
结构化文本B
通过统一的数据协议比较两个结果。
这样迁移不是一次性替换所有旧组件,而是先完成验证、再进行切换。
如果新模型在某些方言、特殊多人场景或者长会议上出现问题,仍然能够回退到原来的处理路径。
十五、真正部署时,最容易忽略的几个问题
1. Speaker标签不是人员姓名
S01 和 S02 只是匿名标签。
如果业务要求展示张工、李工或者王经理,仍然需要人工绑定、独立声纹身份识别等方式。
Diarization 和 Speaker Identification 不能混为一谈。
2. 输出格式正确不等于结果一定完整
模型可能因为生成长度限制而提前结束。
因此除了检查 HTTP 200,还应该检查:
是否有有效Segment
最后一个Segment结束时间
原始音频总时长
是否存在异常时间戳
是否出现大量重复文本
特别是长会议,不能因为接口正常返回就认为完整识别了全部内容。
3. 多人重叠说话仍然要单独验收
联合建模减少了模型间融合步骤,但并不意味着多人同时说话的问题已经消失。
仍然需要测试:
两人抢话
多人快速轮换
远场拾音
同声线人员
中英文混说
嘈杂会议室
4. 不要把模型权重体积当成显存需求
0.9B 参数只是模型规模。
实际显存还与音频长度、注意力实现、生成长度、并发数和推理框架有关。
长会议尤其需要实际监控:
nvidia-smi
以及服务日志中的:
CUDA out of memory
5. 模型版本与运行时版本应当一起固定
MOSS 使用自定义 Transformers 代码。
因此不能只记录:
MOSS 0.9B
而应该同时记录:
模型Revision
代码Commit
Transformers版本
PyTorch版本
推理后端版本
Docker镜像Tag
对于内网环境,版本可追溯往往比频繁追新版本更重要。
十六、总结:多模型会议链路,开始出现新的工程选择
过去我们习惯把一套会议语音系统拆成:
ASR
+
Diarization
+
Forced Alignment
这种分模块结构具有很强的可控性,各个模块可以独立优化,也方便针对特殊语言和业务场景进行调节。
MOSS-Transcribe-Diarize 则提供了另一种思路。
会议音频
↓
MOSS-Transcribe-Diarize
↓
文本 + Speaker + 时间戳
↓
会议业务系统
它把原来需要多个模型配合完成的任务放到了同一条生成链路中,减少了独立 Speaker 服务和时间轴融合的工作。
对于熙瑾会悟这样的完整会议系统,这种技术变化带来的价值不只是少部署一个模型,而是可以重新考虑语音处理层的架构边界:哪些任务仍然需要独立服务,哪些能力已经可以由联合模型一次完成。
当然,联合建模也不是所有场景的最终答案。
如果业务需要非常精细的逐字时间戳、已注册人员实名识别、复杂重叠语音处理或者特定方言优化,分模块架构仍然可能更加合适。
真正值得做的不是立即删除原来的 ASR、Sortformer 和 ForcedAligner,而是让新模型通过统一接口进入现有系统,再用真实会议数据决定是否替换。
从多个模型拼接到一个模型联合输出,减少的是模型之间的衔接复杂度;真正决定能否进入生产环境的,仍然是识别质量、说话人一致性、长会议稳定性以及完整的部署管理能力。
浙公网安备 33010602011771号