mindsurf-omni(sft_merge

139M 参数的中文语音对话模型。听语音、说语音,中间不经过文本——音频和文字在模型内部是同一条序列上的 token。

权重文件 sft_merge_768.pth, sha256 d67c744019f786e851247fea712953f62573161b08318f704a872bee89a82753

这张卡怎么读

只写量过的数字,每条都带测量误差。判定只有三种:改善、劣化、无法区分。 「无法区分」不是「一样好」,是这批数据分不出谁好谁坏——想下结论得加样本量。

有些指标标着「仅报告」,意思是它这次不参与通过或失败的判定,只印出来看。 原因是它分辨不了我们关心的那点差别,比如单看一边的错字率,这批数据能分辨的最小差别是 0.0529,而我们关心的是 0.0500,比它的分辨能力还小,那它说什么都不作数。

所有数字取自代码仓库的 configs/release/headline_numbers.json, 那是唯一真源,每条带 source 指向产出它的原始读数文件。

它是怎么来的

不是重训出来的。父模型 sft_graft_frozen 上做过两次目标不同的 DPO,一次冲质量、 一次冲长度,两次都只动了负责思考的那半(Thinker),所以可以直接把两次的权重差加起来:

sft_merge = sft_graft_frozen + Δ(质量 DPO) + Δ(长度 DPO)

相加之前先量了这两个改动会不会互相抵消。在 Thinker 的 91 个张量上,两个 Δ 的方向 余弦是 +0.1329(这一遍数出来是 9478 万个数,比 Thinker 的 8986 万多 491.52 万—— 词嵌入和输出头共享同一份权重,按张量遍历时被算了两遍)——接近垂直,也就是各改各的、不冲突,所以相加之后两个效果都保住了。 (对照:合并后的总位移是 1.7545,两个 Δ 各自是 0.9617 和 1.3499;如果完全垂直, 勾股定理算出来应该是 1.6575,实测比它略大一点,正是那点正相关。)

负责发声的那半(Talker 加音频投影层,104 个张量)与父模型逐位相同,一个比特都没动。

只跑了一个配置(两个 Δ 都按原样相加),没有扫参数。

结构

参数量 来源
Thinker(负责思考) 89,864,448 我们自己的中文基座
Talker(负责发声) 47,050,754 上游 MiniMind-O 发布权重
把两半接起来的部分 2,168,320 不属于任何一半
合计 139,083,522

Talker 只通过 talker.embed_proj 读 Thinker 的隐状态。接口窄到可以整半替换, 这个模型换的就是那半。音频编解码用 Mimi, 8 个码本、每秒 12.5 帧。

量过的

语音

160 句固定文本,和父模型逐句配对比较(同一句两边各念一遍再相减,这样比只看平均值更灵敏), 念出来的音频交给第三方识别器 paraformer-zh 听回文字:

本模型 对父模型
错字率(CER) 0.0962 ± 0.0176 无法区分(+0.0059,误差 ±0.0314)
音质自动分(UTMOS) 2.4399 ± 0.0498 无法区分(-0.0127,误差 ±0.0985)
说不出话的比例 0/160
音色相似度(12 音色 × 20 句) 见过 0.6487 / 没见过 0.6044 父模型 0.6470 / 0.6054,误差 ±0.015,没有退化

单看一边的错字率只能仅报告,理由见开头。有判定资格的是右边那两条配对读数。

音色相似度的满分不是 1.0。 把存好的参考音频原样解码回来再重新抽一次说话人向量, 和存档的比也只有 0.8409——中间差掉的是编码器压缩时丢掉的身份信息,每条生成音频都要付这笔。 所以上游报的 0.6472 应该读成「拿到了编码器允许范围的 76%」。

0.0962 不要拿去和级联那条路的 0.0359 直接比。 160 条参考文本里有 71 条含阿拉伯数字, 这 71 条的错字率是 0.1482,其余 89 条是 0.0548。差的大半不是发音, 是「2021 念成二零二一」这种正确行为被判官罚——而级联那条也被同样罚, 两个数在这一项上不可比。把数字折叠成中文读法之后,全部是 0.0730

这几行可以自己重算,仓库里有逐句读数:

python scripts/evaluate_speech.py \
  --candidate artifacts/merge/speech-sft_merge-mos.jsonl \
  --reference artifacts/merge/speech-parent-mos.jsonl

人耳

四个人盲听打分,每人拿一份随机顺序。这是这个项目唯一一把人耳尺子。 分两个包发出去,两个包问的问题不一样,同一个 edge-tts 在两个包里不是同一个分,别混着引

系统 MOS(1–5) 缺陷标记
听模型(分辨两套模型) edge-tts(参照上限) 4.023 ± 0.108 0
我们的 graft 臂 2.738 ± 0.125 43
上游官方 sft_omni 2.738 ± 0.119 42
听合成器(分辨两个合成器) edge-tts 3.871 ± 0.064
voxcpm 3.476 ± 0.157

我们和上游官方权重到小数点后三位相同。在这之前,「与官方无法区分」只有 UTMOS 一把自动尺子撑着,现在有一把不可能继承 UTMOS 盲点的仪器落在了同一处。

但这个 panel 不作门控判定,算分脚本自己会拒绝出判词:每系统 20 条, 要认证 0.29 的差需要每人 197 到 271 条。20 条买到的是两样自动指标给不了的东西—— 人耳立刻能听见的粗大缺陷,以及 UTMOS 的排序和人是否一致(排序比量级省样本量得多)。 自然度的门控仍然在 UTMOS,它在 n=160 上有资格。

这句话的读法要精确。 盲听包里的音频出自 graft 臂,不是 sft_merge 本身。 两者的关系是量过的:sft_merge 的 Talker 和音频投影层与父逐位相同, 且它对父的错字率与音质分配对判定都是无法区分。这条链就是全部依据。

2.738 在 1 到 5 的刻度上落在「听着费力」和「勉强可以」之间。它不好听, 只是没比上游差——差距主要来自编码器而不是模型,换个 Talker 也改善不了。

延迟

实测 预算
用户说完到出第一声 145.1 ± 4.1 ms,P95 189 3000 ms
发出打断到真的停 P50 0.26 ms / P95 0.40 ms(40 轮) 200 ms

打断不是软停。 取消之后 GPU 占用在 0.5 秒内从 41% 掉到 0;同一轮完整跑完要 4953 ms, 被取消的那一轮只花 132 ms。停的是计算本身,不是「不再往外发」。

产品指标

608 条提问上重算:

本模型 父模型
回复中位数 55 字 / 念 11.8 秒 26.0 秒
用户说一秒,模型说几秒 6.2 13.7
问完 5 秒时还没说完 81% 93%
空回复 / 复读 0 / 1

长度就是这个 checkpoint 存在的理由。另一个候选 sft_dpo2 判据全过, 但回复念 24.2 秒——30 秒级的回复是会让人不想用第二次的缺陷。

对话质量

状态
盲评对父模型(608 条) 0.6424 ± 0.0468 仅报告:开跑前登记的位置检查没过(实测残余偏倚 -0.0033)
长度受控对 sft_len 0.5918 ± 0.0664 过关:位置 0.5138、长度偏差 0.5092,都在中性附近

判官是 deepseek-ai/DeepSeek-V3.2,提示词的 sha256 和选边种子都写进了产出文件。 「长度受控」的意思是先把两边回复长度配平再比,免得判官其实在奖励长度。 控住长度、判官实测中性、位置也干净之后合并体仍然赢 59.2%,那只能是质量那个 Δ 的贡献—— 前面「两个 Δ 接近垂直」的预测在这里兑现了。

边界

这些是量到底的结论,判据都写在跑之前。

一、「把回复变短没有顺带弄坏对话能力」这件事,我们证不了

衡量对话能力的办法本身也在衡量长度,两件事混在一个数里分不开。四把尺子都试过,各死各的:

尺子 为什么不行
chat_nll(原版) 两批中立参照对「谁更好」只有 20.3% 给出一致答案,而分差和参照长度的相关系数是 +0.601——它在量长度
按参照长度分层再比 控住长度之后一致性掉到 1.9%,信号全没了
改看「答得上率」 拿一次已知的质量崩坏去试它,它读不出来(+0.0872 落在 ±0.1061 的噪声里)
长度配平过的参照集 一致性升到 94.9%,原毛病没了,但那批参照本身偏短,模型变短之后自然更配它——分不开「答得更好」和「长度刚好对上」

要闭合这一轴大约需要 3600 条提问,现有 608 条,差六倍。

sft_merge 就是选了这个代价:要 12 秒的回复,就得接受这一轴无法认证; 要一份判据全过的交付,就得接受回复念 24 秒。

需要说明的是,测不了的只是改变长度的干预。对不改长度的干预,chat_nll 原来那把 尺子本身没有这个混淆——sft_dpo2 就是这么过的,两批中立参照一致率 58.9%,够格。

二、情绪能换,但做不成一个可以调大调小的旋钮

换一条带情绪的参考录音,输出就带情绪,这一半是真的:同一个人的平静版和激动版两条参考, 输出音高差 +54.7 Hz,12 个音色全是这个方向,不用训练。

卡住的是「重训一个能控情绪的版本」这条路,它的前提是情绪标签可信,而四个人的盲听把这个前提证伪了: 人对 emotion2vec 自动标签的一致率是 31–42%,而人对人是 41–65%。 两个数都低说明这个任务本来就难;但人彼此对得上、却都对不上标签,那是标签系统性偏离人耳。

条件化训练也真跑过一轮:主判据音高零效应,四条情绪臂对中性全部无法区分, 而同批的对照臂先过了——所以是真的没效果,不是仪器读不出来。

情绪和音色纠缠是学界的公开问题,解法要独立编码器加解耦损失再重训。 架构改动这个项目一开始就排除了。

三、打断做到了执行,没做到判断

调用方说停就真停,见上面的 0.26 ms 和 GPU 归零。但「什么时候该停」没做—— 那需要一批「模型正在说话、用户同时开口」的录音,并且标明每次重叠是不是真的想打断 (用户也可能只是「嗯嗯」附和)。我们手上两份训练语料都没有这种数据, 而自己把两段录音混在一起合成重叠,模型能学到的只有「有没有人声」,那用一个语音活动检测器就够了, 不需要训练。所以这个判断留在调用方。

其余量到的短板

12 个音色只有 6 个真的听得出是不同的人。 拿生成的音频去 12 选 1 认人(20 句固定文本): serenaericuncle_fudylanarthur 是 20/20,moon 17/20; vivian 16/20、jennifer 6/20 属于边缘;cherryethanmomo0/20chelsie 1/20—— 这四个听起来塌成了 serena 和 moon。别把后四个当独立音色投放。

不是测法的问题:把参考音频原样解码回去认,12 个全认对。是生成的音频里带的身份信息不够。 推理侧也没有参数能救——说话人引导强度扫过三档,四个坏音色没有一个上到 15/20, 而 moon 反而掉到 10/20。

多轮只测到三轮。 不崩,上下文利用率 82% 且不随轮次下降, 但「能接上前文回答」的比例逐轮走低:42%、24%、14%。要说明的是第 2、3 轮的问题是刻意设计的省略句, 题目本身在变难,这个下降里有多少来自上下文、多少来自题目,我们没有拆开。

这是把历史一起送进模型量出来的。语音进语音出的那条路(Thinker-Talker 端到端)做不到多轮: 用户那一轮在模型眼里是音频 token,而历史只能以文本传递,所以它每轮都是第一轮。 要多轮就走「识别成文字再生成」的那条路。

自然度的天花板不在模型。 与真人的差距里 79% 来自编解码器、21% 来自 Talker, 而我们已经拿到编解码器允许范围的 92.2%。

没有发布的那个候选

sft_dpo2 五条判据全过,「对话建模不退」那一轴是认证过的(一致率 58.9% 够格, 两批中立参照都判无法区分)——因为它没改长度,chat_nll 那把尺子对它有效。 它的权重不在这个仓库里。写在这里是因为取舍应当可见:它换来的是 24.2 秒的中位回复。

仓库里有什么

README.md                       ← 这张卡
sft_merge_768.pth        456 MB  成品(Thinker 那半按 fp32 落盘)
tokenizer/               464 KB  tokenizer.json + tokenizer_config.json
configs/                         许可记录、对外数字真源、语音系统提示

中间产物不在这里(t2a_*sft_dpo*sft_len)。

怎么加载

模型类来自上游 MiniMind-Omodel/ 包 (model_omni.MiniMindOmni)。这是一个嫁接体,加载有一处不显然的地方:

config = model_omni.OmniConfig(hidden_size=768, num_hidden_layers=8, use_moe=False)

Thinker 要加宽(intermediate_size=3584num_key_value_heads=8), Talker 必须保持上游默认——两半的配置不同。一起加宽会让 Talker 那 20 个张量形状对不上, 而且不会报错,只是那半停在随机初始化上照样吐码,解出来是噪声。

可运行的权威实现是代码仓库里 src/mindsurf_omni/service/native.pyload_native_model:它会核对参数总数、逐条检查缺失的张量,对不上就报错而不是凑合跑。

音频编解码器要另外取:kyutai/mimi,仓库里钉了 revision。

怎么采样

自己写推理代码的话,这几个值是我们量过之后定下来的,直接照抄比用库的默认值省事:

为什么
temperature 0.0 贪心解码,同一个问题给同一个答案。0.7 时同题问三遍的字符相似度只有 0.270
repetition_penalty 1.1 贪心会复读,这个数是量出来的下限,见下表
top_p 0.9 上游默认,没测过
max_tokens 512 上游默认,没测过

608 条探针上,复读(连续重复 20 字以上)的回复数:

设置 复读的回复 最长的重复段
T 0.4 + 惩罚 1.0 1/608 226 字
T 0.0 + 惩罚 1.05 2/608 486 字
T 0.0 + 惩罚 1.1 0/608 10 字
T 0.0 + 惩罚 1.2 0/608 0

1.2 也干净,但把回复中位数从 59 字拉到 69 字,念出来多一秒多,所以取 1.1。

温度从 0.7 换到 0.0 之后,拿 608 条探针盲评过质量:判官 deepseek-ai/DeepSeek-V3.2, 胜率 0.5073 ± 0.0529,判定是无法区分——也就是没证据说更好,也没证据说更差, 换它是为了稳定性和不复读。逐条读数在代码仓库的 artifacts/sampling/

降温治不了编造。 「南京有什么好吃的」答出过「鸿门大酒楼」,那是 139M 模型的事实性问题, 跟采样无关,温度调低只会让它每次稳定地编同一个。

音频那一侧的采样参数写死在上游的生成函数里,请求参数碰不到,也不建议动: 关掉它的 1.05 重复惩罚,24 条里有 3 条退化成复读。

许可与署名

不可商用。 约束来自文本基座——它从训练数据继承 CC-BY-NC-4.0, 这条传导到每一个衍生物,包括本模型。

必须署名 Mimi(CC-BY-4.0,钉了 revision)。

不在这个仓库里的

语音识别器(SenseVoice)和情绪标注器(emotion2vec)没有随附。两者都挂 FunASR Model Open Source License Agreement v1.1,该协议没有明确的再分发条款, 所以我们依赖它们、但不替它们分发。请自行从 ModelScope 获取。

训练语料同理,没有随附。

人工盲听材料在另一个仓库: oscar0403/mindsurf-omni-listening

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support