↓跳过正文

把本地语音转写换成 MOSS:一个晚上做完选型,靠的是自己攒下的数据

·6133 字·13 分钟
Rollin
作者
Rollin
机器人产品规划 × AI 全栈独立开发者,铁人三项爱好者。

上下文的威力是无与伦比的强大的,下面这篇文章基本上AI基于我做这件事情的上下文和我的个人画像写出来的。当我下午五点多开会得知Moss能识别的这么好的时候,真就是迫不及待的就用在自己的产品上了。

10月9日晚上 19:36,我把 MOSS-Transcribe-Diarize 的链接丢给 Claude Code,问了两个问题:这个能不能用?比我现在的人声分离好不好?21:12 拿到结论:在我自己的三段录音上,它在速度、分人、文字、专名四项上都比原来强,值得换。当晚定下主转写换成它,第二天在它上面做好了声纹认人,10月11日合进碎碎念。

模型是别人做的,我能做的是比对和决定。比对能一个晚上做完,靠的是这一年半攒下的东西:原始录音一条没删,转写稿我自己改过,专名有一份从我的语料里长出来的词典,说话人我愿意花时间标。下面按顺序写:为什么要分出谁说的、原来的做法差在哪、为什么能比得这么快、比出来的数、怎么接进来。

原来和现在的两种做法

为什么一定要知道是谁说的
#

碎碎念记我每天的录音、截图、随手记,再从里面学关于我和我身边的人、事、项目的记忆。它的记忆层定过一条规矩:录音转写稿只学项目,不学人。原因是稿子里没有说话人,一句「这个我来跟」,分不清是我说的还是别人说的,学进去就会把别人的话记到我头上,导致AI做的会议纪要待办压根没法看。

能分出谁说的以后,录音里的四件事才用得上:

  • 精准知道是谁说的。 转写稿按人分段,段首是名字和时间;纪要可以按人名写。
  • 回头追溯。 搜一句话,看到是谁在第几分几秒说的,点开就是原声。几周后再争论「当时是谁定的」,不用靠记忆。
  • 按人分任务。 会上谁说了「我来跟」,任务就归谁,不再是一句不知道归属的「待办」。
  • 识别观点。 同一个问题谁支持、谁反对,各自的理由是什么。我自己说过的观点,记忆层终于可以学。

所以这次要的不只是「转得更准」,是「每句话都知道是谁的」。

原来的做法差在哪
#

服务跑在我的 Mac Studio(M3 Ultra,96 GB)上,用 GPU(MPS)推理。两个模型:FireRedASR 的 AED-L(1.1B 参数,中文强)负责转写,pyannote 的 speaker-diarization-3.1 负责分人。对外有两个接口,问题各不相同。

正式在用的接口:不分人,靠事后纠正
#

碎碎念平时调的是不分人的接口,每天的转写稿都出自这里。

  • 没有说话人。 上一节说的四件事,一件都做不了。
  • 30 秒一块,块和块之间没有上下文。 FireRedASR 的编码器用自注意力,占的内存随音频长度的平方涨,35 分钟的录音一次过要 205 GiB。服务只能按静音切成 30 秒一块,逐块识别。一句话被切在两块之间,前后就接不上。
  • 没有标点。 稿子是一长串字。
  • 专名只能事后改。 FireRedASR 没有热词或上下文提示的入口,人名、产品名、型号写错了,只能靠出稿后的专名纠正:规则替换、按读音找近音词、本机 Ollama 上的 qwen3:30b-a3b-instruct-2507 看上下文改,还有一层用 FireRedASR 的候选结果做声学证据。最后这层实测一次都没命中过。
  • 纠正本身会误改。 词典每加一个词,就可能多一种误改。这次把「MOSS」加进词典的当天,普通词「模式」就开始被改成 MOSS。

分人的接口:先切人,再一段段猜
#

分人的接口一直在,碎碎念没用它,日志里一共只有 16 次调用,看起来都是测试。它的做法是先让 pyannote 切出每个人说话的段落,再把每段单独送去识别。问题出在切:

  • 段一短,模型就猜。 没有上下文,FireRedASR 会把字认成读音相近的别的词,还会凭空编出整句话。
  • 短于 0.2 秒的段直接丢掉。 「嗯」「对」这种插话没了。
  • 两人同时说话,重叠部分只算给一个人。
  • pyannote 会并人、拆人。 一问一答的两个人被并成一个人,一句话被拆给两个人。
  • 慢。 22、31、111 分钟的三段录音,分别要 282、352、1461 秒。

同一段录音、同一时间点,三种做法写出来的字(「正式稿」是我改过的正式转写稿):

时间分人接口正式稿MOSS
10:13公寓吗功率吗功率吗?
20:02看电视好牛逼看电视我看电机。
20:09年级那个表格那个那个表格电机那个表格
20:32是人才经济学知道吗是离开经济区你知道吗是原来电机选的尺寸吗?
10:48很久没工作了今天想让你看看我的老婆(没有这句)(没有这句)

最后一行是分人接口凭空编的,录音里没人说过。

为什么一个晚上就能比完
#

评估一个转写模型,难的不是跑模型,是知道它对不对:要有标准答案,要有一把能量「专名写对几处」的尺子,要知道每个声音是谁。这些我手上都有,不用临时去做:

  • 原始录音一条没删。 碎碎念里存着 54 条录音,37.4 小时,最早一条是 2025年5月7日。挑哪三条比,同一份音频让两套系统各跑一遍,随时可以。
  • 转写稿我改过。 正式稿出来以后,听错的人名、产品名我会手改。这三段改过的稿子就是参考稿,用来数专名在录音里出现了几次。
  • 专名词典是自己长出来的。 504 个专名,其中 57 个有记录在案的听错写法,一共 134 种。词典有三个来源:一份种子勘误表、从我自己的文档里抽出来的词、我改稿时回流的错法。它在这次评估里当了两回事:挑 55 个有听错记录的专名直接当热词喂给 MOSS;再拿它当尺子,数每套系统写对了几处。
  • 纠正日志保留原稿。 每次转写,纠正前的原文和改了哪些都记一份,不覆盖。哪些错是模型的、哪些是纠正程序改出来的,分得开。
  • 说话人我自己标。 第一轮比完,Claude 给三段录音里的 16 个声音做了一个标注网页,每个声音放几段原声。我第二天早上听一遍、填上名字,把结果贴回去,认人的门槛就是拿这份标注定的。

没有这些,第一步就得先人工逐字听写一段当标准答案、再把说话人一个个标出来,那是几天的活,而且只能比一次。现在这些数据就在那儿,每出一个新模型都能拿同一套再比一遍。

这次的时间线:

时间发生了什么
10月9日 19:36提问
19:39第一轮判断:我的正式稿没有说话人,要比的是「从没有到有」,以及和闲置的分人接口比
19:55让它跑三段录音
21:12出结论
21:48出声纹认人的接入方案
22:05我拍板:主转写换 MOSS,记录录音里其他人的声纹
23:36转写服务的新接口跑通
10月10日 09:36我把 16 个声音的标注贴回去
09:45认人门槛定了
13:18标注写进碎碎念,App 里能看到说话人
10月11日 13:07合进碎碎念主分支

MOSS-Transcribe-Diarize 是什么
#

  • 模思智能(OpenMOSS)2026年7月9日开源,Apache-2.0 协议。
  • 0.9B 参数:Whisper-Medium 的音频编码器,接一个 Qwen3-0.6B 结构的解码器,支持 50 多种语言。
  • 一次输出文字、说话人编号和时间戳,格式是 [开始秒][S01]说的话[结束秒],带标点。
  • 提示词里能带热词,写成 热词提示:A, B, C。
  • 官方只给了 CUDA 的跑法。我用的是社区转好的 MLX 8-bit 版,1.3 GB,mlx-audio 0.5.8 已经内置了这个模型。
  • 上下文 128k。音频每秒占 12.5 个 token,再给输出留出位置,一次大约能放 90 分钟。这是按上下文估的,官方没写上限。

官方自己报的成绩里,会议数据集 AISHELL-4 的 cpCER(字错和说话人归错一起算)是 15.83,纯字错率 14.84,两者只差 1 左右,说明它很少把话归错人。但官方的对比表里既没有 pyannote 这种先切人再识别的做法,也没有 FireRedASR,跟我原来那套差多少,只能自己比。

比出来的数
#

三段多人讨论的录音:22、31、111 分钟,每段 4 到 6 个人说话。111 分钟那段超过一次的上限,在 54 分钟处的静音切成两段。每段跑三遍:原来的分人接口(关掉专名纠正)、MOSS、MOSS 加热词。没有逐字的标准答案,只比数得清的东西,文字准不准靠抽查。

速度
#

22 分钟31 分钟111 分钟
分人接口282 秒352 秒1461 秒
MOSS64 秒125 秒656 秒
MOSS + 热词65 秒126 秒660 秒
MOSS 每秒生成的 token14111373–78

MOSS 快 2.2 到 4.4 倍。带热词多花 1 到 4 秒。录音越长,每秒生成的 token 越少:111 分钟那段的前后两半,分别生成了 1.8 万和 2.9 万个 token。

分人
#

22 分钟31 分钟111 分钟
分人接口分出几个人559
MOSS 分出几个人549(两段相加,同一个人被算了两次)

抽查 31 分钟那段:pyannote 把一问一答的两个人并成了一个人,又把一句话拆给了两个人;这些地方 MOSS 都分对了。整段看,pyannote 把 68% 的说话时间归给了同一个人,MOSS 分出来说得最多的人占 52%。

MOSS 也没有漏段:pyannote 判定有人说话的地方,没有哪一段被 MOSS 漏掉 6 秒以上。

文字
#

上面那张逐句对照表就是抽查的结果:MOSS 比我改过的正式稿还准,带标点;分人接口最差。这条没有标准答案撑着,只算抽查。

专名
#

以我改过的正式稿为参照,31 分钟那段里专名出现 23 次,111 分钟那段 101 次。各套写对了几处:

31 分钟(23 处)111 分钟(101 处)
分人接口(不纠正)227
MOSS066
MOSS + 专名纠正1798
MOSS + 热词697
MOSS + 热词 + 专名纠正20100
  • 31 分钟那段型号和厂商名多,MOSS 自己一处没写对。热词能救回一部分,比如「俯仰」不带热词时写成「抚养」,带热词后写出了 6 处(正式稿里只认出 2 处);两个厂商名和一个芯片型号带了热词也没改过来,要靠后面的纠正。
  • 111 分钟那段带热词时,MOSS 还多写出了 57 处专名,正式稿里没有。抽查下来多数是正式稿自己漏了:比如 Pogo Pin(一种弹簧针连接器),正式稿写的是「霍克片」「破口病」。
  • 热词有副作用。55 个全塞进去以后,一段讨论电路的录音里,「MOS 管」的 MOS 被改成了 MOSS。热词要按录音的领域挑,不能一份全量。
  • 专名纠正换到 MOSS 后面照样能用。纠正有四层:规则、读音、本地大模型、FireRedASR 的候选结果。前三层只看文字,跟哪个模型转写的无关;第四层换了模型就用不上,但它本来就没命中过。

它的短处
#

  • 一次放不下的长录音切段以后,同一个人前后两段的编号对不上:前段叫 S01,后段可能叫 S02。
  • 它也会并人、拆人,这是标注以后才看出来的:111 分钟那段的 S01 里混进了我的一段话;22 分钟那段有一个编号是两个人;还有一个人被拆成了两个编号。

决定:主转写换 MOSS,认人另起一层
#

10月9日晚上我定了:碎碎念的主转写换成 MOSS。原来不分人的接口留着,给别的调用方用;新接口失败时,App 退回老接口。

MOSS 只管一条录音里有几个声音、各自说了什么。S01 只在这一条录音里有效,它不知道 S01 是谁。要认出是谁,得自己加一层声纹。我提的要求是:系统把录音里的声音列出来,我来标这是谁;标过的人,声纹跟着这个人的记忆走,App 以后一直认得他;哪怕将来换了模型,顶多重新算一次声纹。落成四条规定:

  • 分人、声纹、认人三层分开。 换掉任何一层,另外两层不动。
  • 存原声,不只存向量。 每个人的声纹是 vault 里的几段原声片段,算出来的向量只当缓存,按声纹模型分开存。换模型就用原声在后台重算,不用我重标。
  • 转写稿里只写 S01、S02,「S01 是谁」另存一层。 改名、合并人物、改标注都不用重写转写稿。
  • 只有我确认过的标注才往声纹里加样本。 自动认出的、给建议的都不加,免得认错一次以后越认越偏。

声纹只留在本机和我自己的 iCloud 里,不进任何大模型的提示词。

接进来以后
#

转写服务
#

新加一个 /v2/transcribe 接口:

  1. MOSS 一次出文字、编号和时间戳;
  2. 用 WeSpeaker ResNet34(pyannote 自带的那个)给每个声音算一个平均声纹;
  3. 超过 85 分钟就在静音处切段,按声纹把前后段的编号对上;
  4. 4 秒以上、不跟别人重叠的句子,每句单独带一个声纹;
  5. 最后照常过专名纠正,热词按录音的领域挑。

三段录音走一遍新接口,分别用了 87、156、797 秒,原来的接口输出跟改动前逐字一致。

声纹分得开吗
#

先看 WeSpeaker 能不能把人分开:同一个人在三段录音里的声纹,两两相似度 0.85 到 0.88;不同的人大多低于 0.5,最高 0.63。按这个定了认人的门槛:

档门槛依据
自动认出相似度 ≥ 0.70,并且比第二像的人高出 0.10不同人之间最高 0.63
给建议,等我确认≥ 0.55同一个人的分数一次都没低于它
未标注更低

一段样本够不够
#

我问过:每个人存一段声音不够吗?拿标注试了:跨录音能检验的认人有 6 次,每人只给 1 段样本,6 次第一名全部认对,但有 1 次分数只有 0.65,到不了自动线,只能给建议;每条录音各加 1 段样本后,6 次全部 ≥ 0.73。定下来的做法:第一次标注只存 1 段,之后每确认一条新录音加 1 段,最多 5 段。同一个人在会议室、手机外放、走廊里录出来的声音不一样,样本要来自不同的录音才有用。

端到端试了一遍:只在 22 分钟那段里标出「我」,另外两段里的我就被自动认了出来,分数 0.84 和 0.82。

被并错的人怎么办:逐句复核
#

并人是最危险的错。111 分钟那段的 S01 主体是另一位同事,但混进了我的一段话;要是整个 S01 标成他,我那段话就会记到他头上。

它还会带偏声纹:S01 的平均声纹跟我的相似度到了 0.697,离自动认出只差 0.003。现在算平均声纹前先剔掉跟主体不像的句子,降到 0.604。

再加一层逐句复核:4 秒以上、不跟别人重叠的句子各有一份声纹,逐句跟已知的人比。用我的标注试,S01 里有 9 句被改了归属,8 句改给我,集中在第 60 到 65 分钟,1 句改给另一个人;三个干净的声音一句都没被误改。

下面是这段录音标完后的样子(同事的名字打了码)。S01 后面写着「其中 5 句是我」:1 句是我手标的,另外 4 句是逐句复核从别人名下找回来的;S04 和 S07 是同一个人,MOSS 把他拆成了两个编号,标注时归到一起:

111 分钟那段录音标完以后的说话人列表

一条新录音进来时是这样:8 个声音,系统认出 S06 可能是我,等我点确认;其余 7 个还没认出来,等我听一两段再标。标过一次,以后的录音就自动认:

一条新录音的说话人列表

App 里现在能做的
#

  • 录音详情里「说话人」一节:每个声音一行,能试听,能标注、确认、改标、说「不是」。
  • 「转译内容」只显示按人分的版本:同一个人连着说的归一段,段首是名字和开始时间,单句能改归属,顶部能搜索。
  • 录音的「人物」跟着说话人标注走。
  • 人物页有「声纹」一节,能试听、删样本;合并人物时声纹跟着并,删人时声纹一起删。

还没做的是纪要按人名重写,和让记忆层从我本人说的话里学东西。

踩过的坑
#

  • Hugging Face 的 xet 下载会卡住。 设 HF_HUB_DISABLE_XET=1 走普通 HTTP。
  • mlx-audio 少一个依赖。 要自己补装 jinja2。装进服务原有的环境时用 --no-deps,免得它升级服务已经在用的包。
  • mlx-audio 的 hotwords= 参数会把默认指令整个换掉。 不传提示词、只传热词时,模型收到的只有一串热词,没有「按说话人分段、带时间戳」那段指令。要自己拼完整的提示词,热词接在后面。
  • 词典学会「MOSS」以后开始误改。 纠正程序把普通词「模式」改成 MOSS,把「模思智能」改成 MOSS智能。原因是按读音找近音词时,近音词没有跟原词一样受上下文条件约束。修法是近音词也要过上下文条件,原文里已经写对的专名不再改。
  • 给整段音频算声纹会报错。 结束时间的浮点误差会超出音频长度,App 建声纹时每次都撞上;现在留了 1 毫秒余量。
  • 标注一度没写进真实数据。 第一轮标注只拿去定了门槛、跑了临时副本,App 里看到的还是「未标注」,我问了才补写进去。

几点体会
#

  • 选型的速度取决于手里有没有自己的评测集。 官方榜单用的是公开数据集,我的录音是我自己的口音、我自己的专名、我身边的人。三段自己的录音、一份自己的词典、一份自己的标注,一个晚上就能比完;下一次出新模型,同一套数据再跑一遍。
  • 原始数据别删。 录音、改过的稿子、纠正前的原文,平时看着占地方,评估的时候它们就是标准答案。
  • 标注是一次性投入。 16 个声音听一遍,门槛就定了;以后每标一个人,后面所有录音都受益。
  • 一次出文字和说话人,比先切再识别好在上下文。 原来分人接口的乱猜和编句,都出在被切短的段落上。
  • 模型会错,就留一层我能改的。 MOSS 会并人、拆人,我用声纹、一次人工标注和逐句复核兜底。以后出了更好的分人模型,换上去,重算一遍声纹就行。

大家有什么好用的模型Mac Studio上跑的模型可以分享给我一下,或者说对我这个方案有什么更好的建议可以指正。