从“夯”到“拉”:11 个 AI PPT Skill 怎么选?

现在做 PPT,已经不一定要从空白页开始手工排版。配合 Codex、Claude、WorkBuddy 等 Agent,一个合适的 PPT Skill 可以在几分钟内完成内容拆分、页面设计甚至文件导出。

但这些工具的路线差异很大:有的输出真正可编辑的 PowerPoint,有的专注动画和视觉表现,还有的只是把整页图片装进 PPTX。本文根据实际交付能力,将 11 个有代表性的开源项目分档整理,并总结它们各自适合的使用场景。

能导出 PPTX,不等于内容真正可编辑。如果页面只是一张图片,那么修改一个数字也可能需要整页重做。

说明:以下分档属于使用视角下的主观评价,并非严格 Benchmark,也不以 GitHub Star 数量作为排名依据。

AI 做 PPT 的三条主要路线

1. 可编辑 PPTX

文字、形状和图表都是真正的 PowerPoint 对象,生成后可以继续在 Office 中修改。正式汇报、客户提案及需要多人协作改稿的项目,应优先考虑这一类。

2. HTML 演示

直接生成网页形式的演示文稿,动画、交互和视觉表现通常更强,适合发布会、公开课及技术分享。不过,多人协作和传统 Office 工作流可能不够方便。

3. 图片式 PPT

AI 将每一页生成为完整图片,再装入 PPTX。它的视觉上限较高,适合时间紧、追求一次性展示效果的场景,但页面元素难以独立编辑,中文文字也可能出现生成错误。

完整项目合集可查看:awesome-presentation-skills

夯:可编辑 PPT 类的完整方案

ppt-master

类型:可编辑 PPTX

这是本文评价最高的一项。它可以接收 PDF、Word、网页和 Markdown 等多种内容,覆盖拆页、配图、讲者备注和质量检查等完整流程,最终导出的文字与形状仍可编辑。

适合:正式汇报、客户交付及需要后续编辑的高要求项目。

注意:生成流程相对较重,但对于正式交付而言通常值得等待。

顶级:视觉设计能力突出

huashu-design

类型:HTML 为主,也可导出 PPTX

它的工作方式很像真实设计师:当需求不够明确时,会先提供三套可预览的视觉方向,确认后再继续制作。这种“先选方向、再完成整套设计”的流程,能显著降低沟通成本。

适合:发布会、品牌展示和视觉要求较高的项目。

注意:用于日常周报可能显得过重。

guizang-ppt-skill

类型:单文件 HTML 演示

这是一套作者风格鲜明的 HTML 演示方案,提供电子杂志和瑞士国际主义等视觉方向,现场展示时具有较强辨识度。

适合:追求风格化视觉的现场演讲和个人展示。

注意:多人往返改稿时不如可编辑 PPTX 方便。

人上人:能力成熟,各有专长

frontend-slides

类型:HTML 演示

当用户还不清楚自己想要什么风格时,它会先生成真实预览图供选择,比用文字反复解释“高级一点”更直接。可选风格丰富,但最终设计的个性可能不如 guizang-ppt-skill 鲜明。

Anthropic pptx

类型:可编辑 PPTX

官方方案的优势是稳定:既能从零新建,也能编辑现有文件或沿用模板,产出的文件便于继续处理。它没有固定的招牌风格,因此最终视觉效果更依赖素材质量和提示要求。

GordenPPTSkill

类型:可编辑 PPTX

面向中文职场场景,内置 19 套模板,覆盖年终总结、述职和工作汇报,并会检查文字溢出和字号一致性。

注意:内置模板对商业使用有限制,客户项目应先核对授权。

html-ppt-skill

类型:HTML 演示

提供演讲者模式、计时器、36 套主题和 31 种布局,适合技术分享和公开课。主题和布局数量较多,使用前最好先缩小候选范围,避免在选择上耗费过多时间。

MiniMax pptx-generator

类型:可编辑 PPTX

基于 PptxGenJS,既能读取现有 PPT,也能从空白开始制作,文字和图表均方便后续修改。默认页面较为朴素,更适合作为可靠底稿或内部汇报方案。

baoyu-slide-deck

类型:图片式 PPT

通过图像生成模型逐页绘制,视觉上限较高。适合制作周期短、只追求现场效果的一次性演示。

注意:中文可能生成错误,后期修改文字也较困难。

NPC:适合特定专业场景

CyberPPT

类型:可编辑 PPTX

专注高信息密度的咨询类演示。流程会先整理证据与故事线,再规划逐页蓝图,最终还原为主要文字可编辑的 PPTX。

适合:行业研究、咨询报告和董事会材料。

注意:流程较重、适用面偏窄,普通周报使用成本较高。

拉:快速转换,但设计流程较弱

md-slides

类型:Markdown 转多格式演示

可根据场景选择 Marp、Pandoc、Beamer 或 Reveal.js,将同一份 Markdown 导出为 PDF、PPTX 或 HTML,临时制作课件非常方便。

注意:自带模板较为基础,逐页设计主要依赖模型临场发挥,与前面提供完整设计流程的方案存在明显差距。

最终应该怎么选?

使用需求推荐方案
领导或客户需要继续修改ppt-master、Anthropic pptx、GordenPPTSkill
品牌发布会或高要求提案huashu-design、guizang-ppt-skill
现场分享或技术演讲frontend-slides、html-ppt-skill、guizang-ppt-skill
内部汇报或可靠底稿MiniMax pptx-generator
行业研究或董事会材料CyberPPT
时间很紧,只追求视觉冲击baoyu-slide-deck
Markdown 快速转课件md-slides

真正重要的不是榜单名次,而是先回答两个问题:

  1. 这份演示最终要交给谁?
  2. 生成之后还需不需要频繁修改?

如果后续修改是刚需,就优先选择真正可编辑的 PPTX;如果演示只使用一次,而且现场视觉效果最重要,HTML 或图片式方案可能更合适。


项目合集:awesome-presentation-skills

参考原文:从夯到拉,锐评 AI 做 PPT Skill

AGI 真的来了,还是我们已经习惯了它?

每当一个新模型发布,大家总会问同一个问题:

它是不是已经接近 AGI 了?

过去,我们判断模型强不强,通常看它会不会答题、写文章、写代码,或者在某个 benchmark 上拿到更高的分数。

但现在,评价一个模型,似乎不能只看它“会不会回答”,还要看它能不能真正把一件事情做完。

它是否能够理解一个模糊的目标?
是否能够自己拆分任务?
是否能够调用工具、操作软件、检查结果,并在出错后继续修正?

这可能才是大模型能力正在发生变化的地方。

一、模型开始从“回答问题”走向“完成任务”

过去的 AI 更像一个知识问答系统。

你提出问题,它给出答案。你要求写一段代码,它生成代码。你让它总结一篇文章,它返回一份摘要。

但现在,模型正在逐渐越过“生成内容”这条边界。

它可以打开文件,读取数据,修改表格;可以编写代码并运行测试;可以分析财务模型,搭建三维场景;也可以根据一组资料,完成一套相对复杂的工作流程。

这些事情的共同点是:

模型不再只负责说出一个答案,而是开始参与答案产生之前和之后的整个过程。

真正有价值的地方,不是它能写出一段看起来不错的文字,而是它能否理解目标、规划步骤、使用工具,并且对最终结果进行检查。

这意味着,AI 正在从“工具”变成“协作者”。

当然,它距离一个真正可靠的智能体仍然有很长的路。它会犯错,会误解需求,会在复杂任务中迷失,也可能自信地给出错误结论。

但重要的是,它已经开始表现出一种过去并不明显的能力:

它不仅能够完成单个动作,还在尝试完成一段完整的工作。

二、真正的分水岭,不只是分数更高

模型在知识问答、数学推理和代码测试上的成绩越来越高,这当然值得关注。

但这些测试大多是在相对清晰的规则下进行的。问题是什么、输入是什么、输出应该是什么,通常都有比较明确的边界。

现实世界却不是这样。

现实中的任务往往没有标准答案,也不会提前告诉你应该使用什么方法。你需要自己判断问题、寻找路径、验证结果,甚至在失败之后重新开始。

所以,比“答对一道题”更重要的,也许是:

  • 能不能理解一个陌生环境;
  • 能不能学习没有明说的规则;
  • 能不能把一个大目标拆成多个小任务;
  • 能不能根据反馈调整方案;
  • 能不能在信息不完整的情况下继续行动。

这也是为什么,模型能否操作电脑、调用工具、完成复杂流程,正在成为衡量其能力的重要标准。

因为从这里开始,模型面对的就不再只是一个输入框,而是一个充满不确定性的真实世界。

它必须处理文件、界面、错误、限制和意外情况。它不只是“知道应该怎么做”,还要真正去做,并且确认自己做对了。

这可能比单纯的知识记忆更接近我们对“通用智能”的想象。

三、我们可能正在逐渐习惯 AGI

我经常会想起一个变化。

几年前,模型能够写出一段像样的代码,就足以让人惊叹。后来,它开始帮我们修改项目、分析日志、生成测试。再后来,我们逐渐习惯于把一部分工作直接交给它。

曾经需要专门演示的能力,慢慢变成了产品中的一个普通按钮。

技术进步最特别的地方就在这里:

它并不总是通过一个巨大的瞬间改变世界,而是通过无数个微小的变化,慢慢改变我们的日常。

我们期待 AGI 像一次巨大的爆炸那样出现。

某一天,一个模型突然拥有完整的推理能力、长期记忆、自主意识和解决一切问题的能力,然后人类清楚地知道:AGI 到来了。

但现实可能不会这样发展。

AGI 也许更像潮水。

它先进入写作,再进入编程;先进入办公,再进入研究;先帮助人完成局部任务,再逐渐承担完整流程。

每一次变化都只前进一点点,所以我们很难准确指出:

究竟是哪一个时刻,机器真正跨过了那条界线。

也许未来某一天,我们回头看,才会发现很多原本由人独立完成的事情,早就已经交给了模型。

而我们的反应可能只是:

“现在的 AI,确实比以前好用了一些。”

写在最后

我不知道 AGI 究竟应该如何定义,也不知道它会在什么时候被正式宣布。

但我越来越觉得,这个问题的答案可能并不只属于实验室,也不只属于某一家公司或某一个模型。

它更可能存在于我们的日常工作里,存在于那些已经被 AI 改变、却没有被我们认真记录的细节中。

当模型开始替我们写代码、做分析、操作软件、完成任务时,我们真正需要思考的,也许不只是它已经有多聪明。

我们还需要思考:

人在这样的时代里,应该保留什么能力?
哪些事情可以交给机器,哪些事情必须由人负责?
当执行变得越来越便宜,判断、创造和选择是否会变得更加重要?

也许 AGI 并不会在某一天突然敲响大门。

它可能早就在一点点改变我们的工作方式、思考方式和生活方式。

等我们终于意识到它已经到来时,
它或许已经来了很久。

只是那条边界没有被标记,
那个瞬间也没有发出声音。

夏天就要结束了

摘自 砚知 知乎:https://zhuanlan.zhihu.com/p/2076686845782635303

那天在地铁上刷到了一个抖音,然后开始哈哈大笑。是真的止不住。

笑到旁边的人已经开始看我了,我还是停不下来。最后实在没办法,只好假装在打电话,才算勉强应付过去。

等出了地铁站再看手机的时候,才发现其实手机一直停留在抖音的界面上。

我甚至已经不记得刚才到底在笑什么了。

但那一刻是真的很开心。

最近越来越想去看看过去的东西。

这件事说起来有一点沉重。

我最近越来越不开心,倒也算不上难过。只是单纯地开心不起来。

岁月的痕迹好像越来越重了。

我在悄无声息的失去着某些东西。

不可否认,我现在的成长是很大的,甚至可以说是巨大的。无论是人情世故,还是为人处世,都和以前有了质的飞跃。

我比以前更懂得怎么和别人相处,也更懂得怎么处理很多事情。

可也正因为这样,我才越来越明显地感觉到,自己身上有一些东西正在慢慢消失。

我不知道它是真的流逝了,还是只是被岁月一点一点地覆盖住了。

也不知道我应该去远方找寻,还是去自身挖掘。

我站在原地,无措和茫然环顾在我的身边,

就像万青《十万嬉皮》里面说的那样,

前已无通路,后不见归途。

前几天重新翻以前的摘抄,看到了一句话。

“这个夏天就要结束了。”

内心突然就被触动了一下。

我突然理解了那天在 KTV 里阿师和我说的同样的话。

夏天就要结束了。

就这么平淡地结束了。

时间悄悄悄悄地溜走了,我甚至都没有什么反应。

不是觉得自己老了。

只是突然发现,原来夏天真的结束了。

那天我和阿师说,没事的,我们可以期待下一个夏天。

秋天的胡同和冬天的北平,依然值得我们去探寻。

阿师说:”那不是夏天”。

于是我送给阿师一个礼物。

一个夏天才可以打开的礼物。

我和他说,那我送你一个明年夏天才才可以打开的礼物吧,等下个夏天你再打开。

这样你就不会去考虑讨厌的秋天和寒冷的冬天了。

你会一直满心期待下一个夏天。

现在回想起来,醍醐灌顶,觉得自己活该有这么多朋友。

总觉得未来还有很多夏天。

还有很多事情可以做。

还有很多人会一直在。

所以并没有觉得什么东西真的会结束。

后来才慢慢发现,好像不是这样的。

有些夏天结束了,就是结束了。

有些人见过最后一面的时候,我们自己并不知道那是最后一面。

我家小区楼下有一个胖胖的保安。

每次我加班到家,基本都是他在值班。

他每次看到我都会站起来,然后笑着说一句:“晚上好。”

有时候加班加到很晚,整个人已经很累了,回到楼下,和他说一句晚上好,就会莫名其妙地开心一阵子。

我觉得他应该也是开心的。

因为我们能感觉到,对方说的那句“晚上好”都是真的发自内心。

至于为什么这么确定,我其实也说不出来。

可能有些东西就是能感觉到。

我突然想起《20岁》里的那句诗:

“我见青山多妩媚,料青山见我应如是。”

我好像一直很喜欢这种东西。

一个人对另一个人释放一点善意,然后又从对方那里得到一点善意。

不需要认识很久。

甚至不需要认识。

一句招呼或者一个动作就够了。

前几天坐电梯的时候,看到一对爷爷奶奶带着一个小女孩。

小女孩很可爱,我就顺便夸了几句,又和他们闲聊了一会儿。

我说这个女孩真可爱,那个爷爷让小孩子说谢谢叔叔,

本来想着说怎么能叫叔叔呢,快叫哥哥,话到嗓子眼的时候死活说不出来了,

电梯的镜子不是什么好东西,看着里面胡子拉碴的另一个我,

突然就释然了,所以话到嘴边的时候变成了:“我好像5年前还会和他们说让他们叫哥哥呢”。

爷爷说她的爸爸今年32了。

我本想多说几句,电梯到了。

我和他们道别,然后很开心地回家。

我很喜欢这样的闲聊,和陌生人毫无理由的闲聊,

夸一下他们的孩子,或者单纯的说他今天看起来真的很精神。

就像良田抖音力那样,毫无理由的夸另外一个人,

很开心,非常开心。

我后来想了一下,好像和陌生人闲聊这件事情一直都会让我觉得很放松。

以前不知道为什么。

现在大概有点明白了。

可能让我觉得开心的并不是聊天本身。

而是聊天时候互相散发的善意。

是一个陌生人愿意和你说几句话,愿意笑着回应你,愿意在短短几分钟里把一点自己的生活分给你。

这些事情其实都很小。

小到可能过几天就想不起来了。

但当时确实会开心。

然后回到家,一个人面对空荡荡的屋子。

我突然反应过来。

我的爷爷奶奶都已经不在了啊。

没有很难过,就是突然想到了。

然后就觉得,原来已经这么久了。

我甚至不知道自己是什么时候习惯这件事情的。

好像也没有哪一天特别正式地接受过。

只是某一天开始,家里少了两个人。

再后来,提到他们的时候,已经只剩下“以前”。

以前他们还在。

以前他们会这样。

以前他们会那样。

以前。

这个词有时候真的很重。

我最近好像越来越喜欢这个词,又越来越害怕这个词。

因为只要说出“以前”,就意味着它已经不属于现在了。

而我现在越来越想去看看以前。

想看看以前的自己。

想看看以前喜欢的东西。

想看看以前写下来的那些话。

想看看那些我以为自己不会忘记,最后却还是慢慢忘记了的事情。

有时候会觉得,我是不是把以前的自己弄丢了。

但仔细想想,又好像没有。

就好像忒修斯之船那样,

我到底还是不是我,

这个东西不能深究,

太多人因为想太多然后想不开了。

而我比他们笨,我知道我想不明白,所以我不想。

早就想找机会重新去看看那些这辈子只见过一次的故人了。

当时旅游见到的共犯,

莫名其妙一起在海边喝酒的囚徒,

网上玩了那么多年还是只知道他叫“宇宙无敌暴龙战士”的网友,

还有南京和老君山,冬天的新疆,

都要回去看看。

趁着现在时间还够,

趁着我还是我。

PS:我对未来有点恐惧,非常恐惧。恐惧的东西太多了,

得到东西的时候一定会失去东西我恐惧、

岁月收回赋予我的少年心气我恐惧、

生活重担一点点加大压力我恐惧、

让我开心的东西渐渐没办法获得乐趣我恐惧、

《我是个胆小鬼》(别名: 我是个废物)

我恐惧夏天会越来越短。
恐惧那些说着“明年再见”的人,最后真的没有再见。
恐惧某一次道别之后,就真的没有下一次。
恐惧我习惯了失去,以至于后来失去什么的时候,竟然都不会觉得难过了。
恐惧有一天我回到曾经去过的地方,那里还在,可我已经完全认不出自己当时为什么会那么开心。

我恐惧朋友会慢慢走散。
恐惧大家都会有自己的生活,自己的家庭,自己的责任,自己的不得已。
恐惧那些曾经可以随时见面的人,最后只能躺在通讯录里,偶尔点进去看一眼,却不知道该说些什么。
恐惧我们会从“走,出来喝酒”变成“以后有机会再聚”,
再从“以后有机会再聚”变成朋友圈里的点赞之交。

我恐惧父母会变老。
恐惧某一天回家的时候,突然发现他们已经没有以前那么有力气了。
恐惧他们开始记不清一些事情。
恐惧电话那头的声音变得越来越轻。
也恐惧有一天,“回家”这个词会突然失去它原本的意义。

我恐惧自己会越来越忙。
忙着工作,忙着赚钱,忙着解决那些不得不解决的问题。
忙到没有时间去南京,没有时间去老君山,没有时间再去新疆。
忙到那些“以后一定要去”的地方,最后都只剩下地图上的一个标记。
忙到曾经答应过自己一定要做的事情,一件一件变成“算了”。

我恐惧有一天,我会开始觉得这些都没什么。
觉得夏天结束很正常,朋友走散很正常,老人离开很正常,人变得成熟很正常。
觉得所有失去都只是人生的一部分。
然后很平静地接受这一切。
平静到甚至忘记了,曾经的自己其实会为这些事情难过,会为一场陌生人的善意开心,会因为一句“晚上好”而觉得这一天突然变得很好。

我最恐惧的,
其实可能不是未来。

而是有一天未来真的来了,
我却已经不再是那个会因为一条抖音在地铁里笑到停不下来的人。

我怕我还记得那个夏天,
却再也找不到那个夏天里的自己。

所以趁着现在时间还够,
趁着那些人还在,
趁着我还会因为陌生人的一句话开心,
趁着我还愿意去很远的地方,
趁着我还没有变成一个什么都觉得“没关系”的人。

我想再多看几次夏天。

因为我现在终于知道了,

有些夏天结束了,
就是结束了。

关于《夏天就要结束了》,一点想说的 – chenpaopao

我们这一代人面对成长时一种隐秘的恐惧,我们越来越会生活,却越来越不会快乐;越来越懂得体面和分寸,却越来越难以毫无顾虑地喜欢一个人、一件事,或者一个季节。

在夏天尚未结束之前

我最近常常觉得,时间并不是向前走的。

它更像一条没有尽头的地铁线,站名一个接一个地亮起,我们站在里面,被人群推着往前。手机屏幕上的消息不断刷新,工作群在深夜里闪烁,外卖软件提醒我“还有十分钟送达”,日历一页一页地翻过去。等我终于抬起头,才发现窗外的树已经换了颜色。

原来夏天就要结束了。

它没有举行告别仪式,也没有给我留下足够郑重的时间。它只是突然变短,像一段没有保存的语音,像聊天框里那句还没来得及回复的话,像我们总说“下次见”,却不知道哪一次真的就是最后一次。

小时候我相信,夏天是不会结束的。

暑假很长,蝉鸣很长,傍晚的风很长。我们可以在楼下玩到天黑,可以因为一根冰棍和朋友争论半天,可以趴在窗边看一场暴雨,觉得整个世界都被洗得干干净净。那时候的快乐很轻,不需要理由,也不需要证明。

可现在,快乐似乎也变得需要预约。

见朋友要提前约时间,旅行要计算预算,回家要看假期安排,连难过都不能持续太久,因为第二天还要上班,还要回复消息,还要装作自己一切正常。我们逐渐学会把生活整理得井井有条,却把自己弄丢在这些秩序里。

有时候,我也会突然因为一件很小的事情开心起来。

比如在便利店买水时,店员对我说了一句“路上小心”;比如下雨天有人顺手替我扶住了门;比如在地铁里听到一首很久以前喜欢的歌,恍惚间又想起那个无忧无虑的自己。

这些瞬间短得几乎不值一提,却会让我觉得,生活并没有完全变坏。

原来人与人之间的善意,本来就不需要多么盛大。一句问候,一个眼神,一次不经意的让路,都足以让一个普通的下午变得柔软。我们每个人都在庞大的城市里匆忙奔走,像一颗颗互不相干的尘埃,可有时,只需要一个陌生人轻轻吹来一阵风,我们就会重新感到自己还活着。

我也开始越来越频繁地想起“以前”。

以前家里还有老人,逢年过节总有人在厨房里忙碌;以前朋友可以随时见面,不需要先确认彼此有没有空;以前我喜欢很多东西,喜欢走很远的路,喜欢在陌生城市里漫无目的地闲逛,喜欢在凌晨写一些没有意义的话。

现在那些人和事,有的已经离开,有的正在慢慢远去。

我们并不是在某个具体的时刻失去它们的。很多告别都没有声音,它们发生在一次次“以后再说”里,发生在聊天框逐渐变长的回复间隔里,发生在通讯录里那个很久没有点开的名字里。

所以我开始害怕“以后”。

害怕以后越来越忙,忙到没有时间去见想见的人;害怕父母慢慢变老,而我只在电话里匆匆问一句“最近还好吗”;害怕有一天,我终于拥有了所谓的成熟,却失去了那个会因为陌生人的善意而开心很久的自己。

但我又不愿意把这种恐惧只当作恐惧。

至少现在,我还可以在一个普通的晚上给朋友发消息,问他要不要出来喝酒;还可以临时买一张车票,去看一座没有去过的城市;还可以在电梯里夸一句陌生小孩可爱,在路边停下来看看晚霞,在夏天结束之前,再认真地吹一次晚风。

我想,所谓“趁着我还是我”,并不是要拼命留住过去,而是在还来得及的时候,继续对世界保持一点敏感。

继续为小事开心,继续愿意奔赴,继续在分别之前好好道别,在重逢之前认真期待。

因为我终于明白,有些夏天结束了,就是结束了。

但只要我还愿意热爱,新的夏天就不只是日历上的某一天。它也可以是一场临时起意的旅行,一次久别重逢,一句真诚的“晚上好”,或者某个平凡夜晚里,我忽然意识到:

我还没有变成一个对什么都说“没关系”的人。

而这大概就是,夏天留给我最后的礼物。

G-STAR: 端到端全局说话人追踪属性识别

paper: https://arxiv.org/pdf/2603.10468

G-STAR是一个LLM-based端到端多说话人ASR系统:1)Sortformer风格流式说话人追踪模块 + 到达顺序说话人缓存(AOSC);2)Speech-LLM转录主干(Qwen2-7B-Instruct + LoRA);3)交错时序融合(K:1插入)结合声学和说话人嵌入;4)缓存一致的SOT解码。三阶段训练:会议式ASR预训练→局部SA-ASR训练→全局SA-ASR训练。

💡 为什么值得关注

     LLM-based ASR近两年进展比较显著,但在多说话人ASR场景,依然需要持续持续探索。G-STAR解决了局部说话人日志与全局身份一致性难以兼顾的问题,将“时间感知的说话人跟踪”与“基于LLM的转录生成”耦合在一个可端到端训练的框架中,从而在“何时”、“何人”、“说了什么”这三个维度的信息上实现协同。此外,G-STAR支持分块流式推理且无需用户提前进行语音注册。对做会议转录、语音助手的团队有直接参考价值。

多方会议转写不只是“把语音变成文字”。系统还需要判断每句话是谁说的、发生在什么时间,并且在长音频被切成多个片段处理时,让同一个人的说话人编号始终保持一致。G-STAR 的主要贡献,就是把带缓存的全局说话人跟踪器与 Speech-LLM 生成模型连接起来,让文字、时间戳和全局说话人标签在同一条件上下文中联合生成。

一、论文要解决什么问题

给定长音频波形,模型需要输出一组带说话人和时间边界的文本片段:

\( Y=\left\{\left(s_n,\tau_n^{\mathrm{st}},\tau_n^{\mathrm{ed}},y_n\right)\right\}_{n=1}^{N} \)

其中,\(s_n\) 是说话人身份,\(\tau_n^{\mathrm{st}}\) 和 \(\tau_n^{\mathrm{ed}}\) 是起止时间,\(y_n\) 是对应文本。真正困难的地方在于:长会议通常必须分块推理,但说话人身份不能在每个块中重新编号。例如第一块中的 spk1,到了第十块仍然应该指向同一个真实说话人。

此前方法通常只解决其中一部分问题:SpeakerLM 更擅长块内说话人建模,但缺少显式的跨块全局身份关联;JEDIS-LLM 使用说话人缓存维持全局标签,却没有细粒度时间边界;TagSpeech 强化了时间锚点和说话人提示,但没有解决长音频分块推理中的会议级身份链接。G-STAR 的目标是同时覆盖时间戳、重叠语音、说话人归属和跨块全局一致性。

二、G-STAR 的总体设计

G-STAR 由三部分组成:ASR 声学分支、说话人跟踪分支,以及维护全局身份的 Arrival-Order Speaker Cache(AOSC)。两条分支生成的特征按照时间顺序交错融合,随后送入大语言模型,以 Serialized Output Training(SOT)格式输出文字、时间戳和全局说话人标签。

1. 分块处理与全局输出格式

长音频 \(x\) 被划分为连续的 \(T\) 个块:

\( x=\left\{x^{(t)}\right\}_{t=1}^{T} \)

每个块生成一个序列化结果,结构可以表示为:

\( z^{(t)}=\left[\langle t_{\mathrm{st}}\rangle,\;w,\;\langle t_{\mathrm{ed}}\rangle,\;\langle \mathrm{spk}=k\rangle\right]^{*} \)

这里的 \(k\) 不是当前块内的临时编号,而是由 AOSC 维护的会议级到达顺序编号。

2. ASR 声学分支

音频编码器首先把第 \(t\) 个音频块转换为帧级声学特征:

\( H^{(t)}=f_{\mathrm{enc}}\left(x^{(t)}\right)\in\mathbb{R}^{L_t\times d_h} \)

随后 ASR projector 将其映射到 LLM 的嵌入空间:

\( U^{(t)}=g_1\left(H^{(t)}\right)\in\mathbb{R}^{L_t\times d_{\mathrm{llm}}} \)

实现中,音频编码器和投影器初始化自 FireRed-LLM,语言模型使用 Qwen2-7B-Instruct,并继承 FireRed-LLM 的 LoRA 权重。

3. 说话人跟踪分支与 AOSC

说话人分支采用 Streaming Sortformer。它不只读取当前音频,还读取上一块保留下来的缓存:

\( S^{(t)},C^{(t)}=f_{\mathrm{trk}}\left(x^{(t)},C^{(t-1)}\right),\qquad S^{(t)}\in\mathbb{R}^{M_t\times d_s} \)

其中 \(C^{(t-1)}\) 是历史 AOSC 状态,\(S^{(t)}\) 是当前块的帧同步说话人提示。实现中,该分支初始化自 NVIDIA 的 diar_streaming_sortformer_4spk-v2;说话人 projector 由步长为 5 的一维卷积降采样模块和两层 MLP 组成,再把提示映射到 LLM 空间:

\( V^{(t)}=g_2\left(S^{(t)}\right)\in\mathbb{R}^{M_t\times d_{\mathrm{llm}}} \)

AOSC 按说话人在会议中首次出现的顺序保存紧凑状态。新说话人出现时分配下一个槽位;历史说话人再次出现时,系统从缓存中找回原槽位。因此,全局编号具有可解释性,也避免了不同音频块之间的标签置换漂移。

4. 交错时间融合

论文没有在最后阶段才把 ASR 与说话人时间轴做“后融合”,而是把说话人特征直接插入声学 token 流。设插入步长为 \(K\),融合结果为:

\( E^{(t)}=\operatorname{Interleave}\left(U^{(t)},V^{(t)};K\right)\in\mathbb{R}^{N_t\times d_{\mathrm{llm}}} \) \( N_t\approx L_t+\left\lceil\frac{L_t}{K}\right\rceil \)

具体来说,每经过 \(K\) 个声学位置就插入一次说话人提示。如果两条分支的帧率不同,则使用确定性的最近邻或线性重采样对齐。论文默认重点验证 \(K=5\),即在信息密度和对词汇建模的干扰之间取折中。

5. 全局 SOT 解码

LLM 在文本提示、融合后的声学/说话人表示以及已生成 token 的条件下进行自回归解码:

\( p\left(z^{(t)}\mid x^{(t)},C^{(t-1)}\right)=\prod_{m=1}^{\left|z^{(t)}\right|}p\left(z_m^{(t)}\mid p,E^{(t)},z_{<m}^{(t)}\right) \)

由于 <spk=k> 与 AOSC 中第 \(k\) 个到达顺序槽位绑定,模型可以在逐块解码时直接生成全局一致的说话人标签,不再依赖会后全局聚类。

三、三阶段训练策略

  1. 会议风格 ASR 预训练:让音频编码器、ASR projector 和 LLM 适应对话及会议语音。
  2. 局部 SA-ASR 跨模态对齐:在最长 20 秒的分段语音上学习时间戳、文本和块内说话人标签。Figure 2 显示说话人跟踪模块在前两个阶段冻结。
  3. 全局 SA-ASR 端到端微调:引入 AOSC 和全局标签,训练跨块一致的说话人归属。同时使用四个数据集构造的 90 秒片段独立调优 Sortformer,以增强长音频跟踪能力。

Speech-LLM 各阶段保持可训练模块一致:ASR projector、说话人 projector 和 LoRA adapter。LoRA rank 为 64,缩放因子为 16,dropout 为 0.05。训练样本最大打包长度为 12,000 tokens。

前两个阶段分别训练 20,000 steps,峰值学习率为 \(5\times10^{-5}\);全局阶段训练 5,000 steps,峰值学习率降为 \(2\times10^{-5}\)。均使用 AdamW、0.01 warmup ratio 和余弦退火。Sortformer 调优采用学习率 \(10^{-4}\)、batch size 4,共训练 5 epochs。

为了让模型更加重视结构 token,论文使用分层交叉熵:时间戳 token 权重为 1.5,说话人标签 token 权重为 2。可将其直观写成:

\( \mathcal{L}=\mathcal{L}_{\mathrm{lexical}}+1.5\,\mathcal{L}_{\mathrm{timestamp}}+2\,\mathcal{L}_{\mathrm{speaker}} \)

四、数据集与评价协议

公开实验使用四个会议或对话数据集:MLC 英文子集、AMI、Fisher,以及只使用音频模态的 Candor。训练音频被切分为最长 20 秒的片段。内部模型还使用中文对话数据;公开配置则使用 AISHELL-4 和 AliMeeting 等公开会议数据作为对应数据来源。

  • 局部设置:输入不超过 20 秒,使用 oracle VAD/分段,重点考察后端的联合转写和说话人归属能力。
  • 全局设置:输入完整会议,各系统使用自己的 VAD 或分段前端并逐块推理,更接近真实部署,但 VAD 差异也会进入最终结果。
  • 指标:cpWER 衡量考虑说话人匹配后的转写错误,DER 衡量说话人日志错误;两者都是越低越好。
  • DER collar:公开实验为 0,内部测试集为 0.5 秒。模型幻觉产生的额外片段会被计入 cpWER 和 DER 错误。

论文还实现了一个受控后融合基线:ASR 使用同一 Speech-LLM 后端,VAD 切出语音段,CTC 强制对齐补充词级时间戳,Sortformer 生成全局说话人时间轴,最后再按时间重叠关系合并。这个对照可以较好地区分“组件更强”和“跟踪条件直接参与生成”之间的差异。

五、局部实验结果

表 1 对应论文 Table 1,单位为 cpWER/DER(%),输入最长 20 秒,oracle VAD,DER collar=0。

系统AMIFisherMLCCandor
Sortformer(仅 DER)— / 29.87— / 18.33— / 17.76— / 30.92
Parakeet(仅 cpWER)24.62 / —27.73 / —25.90 / —27.44 / —
VibeVoice-ASR30.51 / 31.9915.18 / 17.6821.74 / 14.0122.12 / 30.89
MOSS-Diarizen25.13 / 32.2011.69 / 21.6114.16 / 10.5816.38 / 31.76
G-STAR24.86 / 19.0010.29 / 8.1813.90 / 6.4914.54 / 17.56

G-STAR 在四个数据集上都显著降低了 DER,并且在 Fisher、MLC 和 Candor 上获得最低 cpWER。AMI 的 cpWER 为 24.86,与 Parakeet 的 24.62 基本相当,但 DER 从 Sortformer 的 29.87 降到 19.00。结果说明,说话人提示被注入 LLM 后,并没有以牺牲词汇识别为代价,反而提升了联合生成的稳定性。

六、完整会议的全局实验

表 2 对应论文 Table 2,单位为 cpWER/DER(%),完整会议逐块推理,DER collar=0。

系统FisherMLCCandorAMI
Sortformer(仅 DER)— / 15.21— / 21.92— / 18.03— / 28.35
Parakeet(仅 cpWER)24.41 / —31.03 / —26.92 / —35.70 / —
VibeVoice-ASR25.03 / 27.1525.41 / 19.8327.24 / 25.6834.19 / 39.95
受控后融合级联21.01 / 23.4123.18 / 21.3817.62 / 17.6739.52 / 37.63
G-STAR16.44 / 16.8517.15 / 14.2515.17 / 24.8930.85 / 32.23

G-STAR 在 Fisher、MLC、Candor 和 AMI 上都取得最低的会议级 cpWER。相对使用相近主干组件的受控后融合方案,cpWER 分别从 21.01、23.18、17.62、39.52 降至 16.44、17.15、15.17、30.85,相对降幅约为 21.8%、26.0%、13.9% 和 21.9%。这说明改进并不只是来自更强的 ASR 或说话人模块,而是因为说话人跟踪信息在生成过程中直接参与了决策。

不过,DER 结果需要客观看待:G-STAR 在 MLC 上最好,但 Fisher 的 Sortformer、Candor 的受控后融合方案以及 AMI 的 Sortformer 都得到更低 DER。换言之,G-STAR 的优势重点是最终的“谁说了什么”联合转写,而不是在所有场景中取代专门的说话人日志系统。

内部域外会议测试

内部测试集包含 0.50 小时双人会议、0.49 小时三人会议和 1.86 小时四人会议。论文将三至四人场景合并报告,DER collar 为 0.5 秒。

系统2 人3–4 人平均
VibeVoice-ASR11.10 / 14.8354.48 / 38.3347.64 / 34.62
聚类式 Pipeline23.56 / 14.2041.14 / 30.7638.37 / 28.15
G-STAR10.42 / 4.8638.85 / 28.5934.37 / 24.88

G-STAR 的平均 cpWER/DER 为 34.37/24.88,优于 VibeVoice-ASR 的 47.64/34.62,也优于聚类式 Pipeline 的 38.37/28.15。双人场景提升最明显,DER 只有 4.86;三至四人会议仍然明显更难,说明说话人数增加后,全局跟踪和重叠语音依旧是主要挑战。

七、消融实验说明了什么

表 3 对应论文 Table 4,单位为 cpWER/DER(%)。

分层 CE交错时间融合AMIFisherCandor
26.33 / 21.0610.88 / 10.2414.97 / 20.21
28.63 / 21.2814.23 / 9.0218.30 / 18.10
24.86 / 19.0010.29 / 8.1814.54 / 17.56

两项设计的作用并不相同。交错时间融合对 cpWER 的帮助更大,说明周期性注入说话人提示能够协助 LLM 同时生成正确文字和结构 token;分层交叉熵对 DER 的改善更直接,因为更高的时间戳与说话人 token 权重会强化边界和说话人切换预测。两者同时启用时,三个数据集都得到最佳综合结果。

Figure 3 进一步比较了 MLC 上不同插入步长:

插入步长 KcpWERDER
114.206.72
513.906.49
1013.946.51

\(K=1\) 时提示过密,非词汇条件可能干扰语言建模;\(K=10\) 时提示偏稀疏;\(K=5\) 在 cpWER 和 DER 上取得最优平衡。

八、论文的主要创新点

  1. 把说话人归属从后处理变成生成条件。说话人时间信息在 LLM 解码前进入统一嵌入序列,而不是 ASR 完成后再做时间轴拼接。
  2. 用 AOSC 解决跨块身份漂移。按首次到达顺序维护说话人槽位,使长会议中的全局标签具备持续状态和可解释编号。
  3. 统一生成文字、时间戳和全局说话人标签。SOT 输出接口让多个结构化目标被组织为单一自回归序列,同时保留重叠语音片段。
  4. 兼顾模块化训练和端到端优化。说话人跟踪器可以独立调优,Speech-LLM 又能在其提示条件下联合优化,适合训练数据来源不一致或存在域偏移的情况。
  5. 建立局部与全局两套评价协议。oracle 分段实验隔离后端能力,完整会议实验则验证 VAD、跟踪、时间戳和转写共同作用下的真实表现。

九、局限性

第一,论文采用分块推理,但没有完整评估严格实时流式部署,尚缺少端到端延迟、缓存内存开销和在线缓存更新稳定性数据。第二,训练数据规模和多样性仍然有限,跨语言、复杂声学环境、更多说话人以及更强重叠场景的泛化能力还有提升空间。第三,从公开实验可以看到,模型对最终 cpWER 的优化非常稳定,但纯 DER 并非全面领先;如果应用只关注高精度说话人日志,专门的 diarization 系统仍可能更合适。

十、总结

G-STAR 最值得关注的不是简单增加一个说话人编码器,而是重新定义了说话人归属在 Speech-LLM 中的位置:它不再是 ASR 后面的拼接步骤,而是影响生成过程的显式条件。AOSC 负责记住“谁已经出现过”,交错时间融合负责在合适的时间点把信息交给 LLM,全局 SOT 则把文字、时间戳和身份转换为可直接阅读的统一序列。

实验表明,这一路线尤其适合长会议的“谁说了什么”任务:G-STAR 在四个完整会议基准上都获得最低 cpWER,并在内部域外数据上继续保持优势。其下一步关键问题,是把这种缓存条件生成机制推进到低延迟、严格在线的真实流式系统中。


说话人验证:Hybrid Enrollment + Neural Re-scoring 论文解读

本文讨论的是短时长说话人验证(Short-duration Speaker Verification, SDSV):在智能音箱、对话终端或用户自定义关键词唤醒场景中,系统先检测到一句很短的目标短语,再判断这句话是不是注册用户本人说的。

这类测试语音通常短于 3 秒,身份信息很少,且更容易受噪声、音素覆盖和短语内容变化影响。论文的核心思路不是重新训练一个大声纹模型,而是冻结已有说话人骨干模型,在其上训练一个轻量 neural verifier:注册端同时使用文本相关(TD)短语和文本无关(TI)较长语音,查询端仍是 TD 短语,通过全局余弦相似度和双向帧级 cross-attention 做神经重打分。

1. 任务背景:短语音声纹验证为什么难?

在用户自定义关键词(UDKWS)系统中,典型链路是:先通过 keyword spotting 找到用户说出的目标短语,再对这个短语片段做说话人验证。问题在于,这个片段通常只有 0.8 到 3 秒。相比长语音声纹验证,短语级音频中的说话人信息更少,固定维度 embedding 加余弦相似度的传统后端更容易出现分数不稳定。

图 1:针对自定义短语的短时长说话人验证任务示意图。首先,输入的语音会被一个自定义关键词检测模块进行处理,然后会被用来与文本依赖型或文本独立型的注册语音进行验证。

论文把注册方式分为两类:

  • TD enrollment:注册语音和查询语音是同一类短语,内容一致,音素更对齐,但注册时长也很短,说话人信息不足。
  • TI enrollment:注册语音不要求和查询短语内容一致,可以更长,身份信息更稳定,但存在文本内容不匹配。

论文的关键观察是:TD 的文本一致性有优势,但受限于短时长;TI 有内容 mismatch,但随着注册时长增加,speaker representation 会越来越稳定。因此,真实系统里不应该只押 TD 或 TI 单一路线,而应该把二者互补起来。

2. 方法总览:冻结骨干,只训练轻量验证器

论文提出的框架由两部分组成:一个冻结的 pretrained speaker backbone,以及一个可训练的 neural verifier。骨干模型负责提取 utterance-level 和 frame-level speaker features;verifier 负责把 TD、TI 与 query 之间的全局和局部证据融合成最终验证分数。

设 TI 注册语音为 \(X_{\mathrm{ti}}^e\),TD 注册语音为 \(X_{\mathrm{td}}^e\),TD 查询语音为 \(X_{\mathrm{td}}^q\)。冻结骨干会输出句级表示和帧级表示:

\( X_{\mathrm{ti}}^e \rightarrow (E_{\mathrm{ti},u}^e, E_{\mathrm{ti},f}^e),\quad X_{\mathrm{td}}^e \rightarrow (E_{\mathrm{td},u}^e, E_{\mathrm{td},f}^e),\quad X_{\mathrm{td}}^q \rightarrow (E_{\mathrm{td},u}^q, E_{\mathrm{td},f}^q) \)

这里 \(u\) 表示 utterance-level embedding,\(f\) 表示 frame-level feature。论文使用的骨干包括 ECAPA-TDNN、CAM++ 和 ERes2Net-L,全部在 Vox2 上预训练,并在本文训练中保持冻结。这一点对工程落地很重要:不需要推倒重训声纹模型,只需在已有声纹模型上加轻量重打分头。

3. 全局相似度:同时保留 TI 身份稳定性和 TD 内容一致性

verifier 首先计算两个 utterance-level 余弦相似度:

\( S_{\mathrm{ti}}=\cos(E_{\mathrm{ti},u}^e,E_{\mathrm{td},u}^q),\quad S_{\mathrm{td}}=\cos(E_{\mathrm{td},u}^e,E_{\mathrm{td},u}^q) \)

\(S_{\mathrm{ti}}\) 更偏向捕获稳定的说话人身份信息,\(S_{\mathrm{td}}\) 更偏向利用短语内容一致带来的匹配优势。单看这两个分数仍然是传统 embedding 后端思路,所以论文进一步引入帧级 cross-attention 来处理短语内部的局部对齐问题。

4. Parallel Cross-Attention:在帧级别重新对齐短语证据

短时长语音的问题不是只有“信息少”,还包括局部音素和时间位置不稳定。论文使用共享的 parallel cross-attention 模块,对 TD 注册短语和 TD 查询短语的 frame-level features 做双向比较。

注册到查询方向:

\( \tilde{Z}^{e}= \mathrm{CrossAtt.} (Q=E_{\mathrm{td},f}^{e},K=E_{\mathrm{td},f}^{q},V=E_{\mathrm{td},f}^{q}) \)

查询到注册方向:

\( \tilde{Z}^{q}= \mathrm{CrossAtt.} (Q=E_{\mathrm{td},f}^{q},K=E_{\mathrm{td},f}^{e},V=E_{\mathrm{td},f}^{e}) \)

随后对两个方向的输出做时间维 max pooling,并拼接得到局部匹配特征:

\( h_f=[\max(\tilde{Z}^{e}) \Vert \max(\tilde{Z}^{q})] \)

这一步是论文方法的关键:它不再把短语直接压成一个向量硬比,而是让注册短语和查询短语在帧级别互相“看见”对方,从短语内部找到更细粒度的匹配证据。中文解读里强调的“注册看查询、查询看注册,把短序列里对得上的局部证据捞出来”,对应的就是这个双向 cross-attention 模块。

5. 融合决策与训练目标

最终,模型把局部帧级特征 \(h_f\)、TI 全局相似度 \(S_{\mathrm{ti}}\) 和 TD 全局相似度 \(S_{\mathrm{td}}\) 输入轻量 MLP,输出最终验证分数:

\( S=\sigma(F(h_f,S_{\mathrm{ti}},S_{\mathrm{td}})) \)

其中 \(\sigma(\cdot)\) 是 sigmoid 函数。训练使用二分类交叉熵:

\( \mathcal{L}=- \left[ y\log S+(1-y)\log(1-S) \right] \)

\(y \in \{0,1\}\) 表示 enrollment 和 query 是否来自同一说话人。实验中 verifier 包含线性投影层和对称 cross-attention 模块,attention 为 8 heads,hidden dimension 为 128;训练在单张 RTX 4090 上进行,batch size 256,训练 25k steps。

6. VoxPhrase 数据集:从 VoxCeleb 自动切出短语级声纹验证数据

论文的另一个重要贡献是构建 VoxPhrase,用来模拟用户自定义短语下的 SDSV。构建流程是:先对 VoxCeleb 语音做 ASR 获得 transcript,再用 forced alignment 生成词或短语级时间戳,随后通过 S2Phrase 脚本把长语音切成 0.8 到 3 秒的短语片段,并过滤低质量对齐结果。每个短语片段保留 speaker identity 和 waveform。

VoxPhrase 的关键规模如下:

  • 训练集来自 Vox2-dev:5,994 个说话人,215,432 个短语。
  • Eval-1 来自 Vox1:1,251 个说话人,23,036 个短语。
  • Eval-2 来自 Vox2-test:118 个说话人,2,310 个短语。
  • Eval-3 / Eval-4 来自 DeepMine,用于 OOD 测试,短语分别是 “ok google”(约 2 秒)和 “my voice is my password”(约 3 秒)。

为了让评测更接近真实难例,论文还设计了 hard example mining。具体做法是先按说话人聚合短语样本,用预训练 SV 模型构造 speaker prototype,再计算说话人之间的相似度,把“相似但不同人”的配对选为 hard negatives。Eval-1 中 trials 包括 top-1% 565,242、top-5% 903,678、top-10% 1,041,902 和 random 1,382,110;Eval-2 中对应为 26,904、52,702、65,086 和 95,900。

7. 实验设置:三个强声纹骨干 + 多种注册方式

论文使用三个开源说话人模型作为冻结骨干:ECAPA-TDNN(20.8M 参数,embedding 维度 192)、CAM++(7.2M 参数,embedding 维度 512)和 ERes2Net-L(20.5M 参数,embedding 维度 192)。它们在 VoxCeleb-O 上的基础 EER 分别为 0.86、0.65 和 0.57,说明骨干本身已经是强基线。

对比的 enrollment 设置包括:10 秒 TI、3 秒 TI、TD phrase(0.8–3 秒),以及加入 verifier 后的混合注册神经重打分。指标使用 Equal Error Rate(EER,越低越好),并报告不同 hard-negative 难度下的结果。

8. 主要结果:混合注册 + 神经重打分跨骨干稳定提升

Table 2 的核心结论是:在 3 秒或 10 秒 TI 注册条件下,TI 通常优于纯 TD,因为更长注册音频提供了更稳定的身份信息;但当 TI 极短时,TD 的短语内容一致性又变得重要。因此最稳的方案是 TD + TI 混合注册,再通过 neural verifier 重新打分。

几个代表性数字如下:

  • ECAPA-TDNN:Eval-1 平均 EER 从 10 秒 TI 的 6.59 降到 5.75;3 秒 TI 从 8.23 降到 6.45;TD phrase 从 10.06 降到 9.27。
  • CAM++:Eval-1 平均 EER 从 10 秒 TI 的 6.44 降到 5.35;3 秒 TI 从 8.15 降到 6.03;TD phrase 从 9.15 降到 8.31。
  • ERes2Net-L:Eval-1 平均 EER 从 10 秒 TI 的 5.27 降到 4.54;3 秒 TI 从 6.51 降到 5.13;TD phrase 从 7.96 降到 7.22。

在最难的 top-1% hard-negative 场景中,改进同样明显。例如 CAM++ 的 10 秒 TI top-1% EER 从 11.33 降到 9.58,3 秒 TI 从 13.34 降到 10.47;ERes2Net-L 的 10 秒 TI top-1% EER 从 9.32 降到 8.17,3 秒 TI 从 11.02 降到 8.99。说明该方法不是只在容易样本上调分,而是在相似说话人构成的 hard cases 中也有效。

9. TI 时长分析:什么时候 TI 强,什么时候 TD 强?

Figure 4 分析了 TI enrollment duration 对 EER 的影响。论文报告,在 CAM++ 的 Eval-1 random 设置下,纯 TD enrollment 的 EER 为 3.62%,加入 verifier 后降到 3.09%。而 3 秒 TI enrollment 的 EER 为 8.86%,表现较差;随着 TI 时长从 1 秒增加到 10 秒,EER 持续下降。当 TI 时长超过 3 秒时,TI 开始优于 TD;当 TI 小于 2 秒时,TI 仍弱于 TD。

这组实验解释了论文方法为什么要做 hybrid enrollment:TI 不是天然更好,它依赖足够时长;TD 也不是过时方案,在极短注册语音下,短语内容一致性仍然有价值。简单把 TI 和 TD 分数平均并不够,论文提到 10 秒时 TI+TD(mean) 几乎收敛到 TI-only(2.03% vs. 1.98%),而混合注册 + 神经重打分能进一步达到 1.6%。真正带来差距的是可学习的 frame-level re-scoring,而不是机械平均。

10. OOD 结果:DeepMine 上也能降 EER

论文还在 DeepMine 构造的 Eval-3 / Eval-4 上做 out-of-distribution 评估。这里的短语分别是 “ok google” 和 “my voice is my password”。结果显示,在 OOD 场景中 TD enrollment 通常优于短时 TI enrollment,因为文本一致性更重要;但混合注册 + verifier 仍然取得最好结果。

CAM++ 上,Eval-3 / Eval-4 的 EER 从纯 TD 的 8.17 / 6.19 降到混合方案的 6.71 / 3.48。ERes2Net-L 上,从纯 TD 的 6.97 / 4.54 降到 4.88 / 2.38。这个结果说明,verifier 学到的不是只适配 VoxPhrase 域内数据的打分偏置,而是对短语级验证中的局部匹配确实有泛化帮助。

11. 创新点总结

  • 面向真实 UDKWS 的 SDSV 设定:论文关注用户自定义短语,而不是固定口令或预定义说话人集合,更贴近智能设备中的实际声纹核验链路。
  • VoxPhrase 数据集:从 VoxCeleb 自动构建 0.8–3 秒短语级声纹验证数据,并加入 hard example mining,使评测能覆盖相似说话人的难例。
  • Hybrid enrollment:把 TI 的稳定身份信息和 TD 的短语一致性结合起来,避免单一路线在不同注册时长下失效。
  • 冻结骨干 + 轻量 verifier:不修改 ECAPA-TDNN、CAM++、ERes2Net-L 等强声纹模型,只训练小型重打分模块,部署成本更低。
  • Parallel cross-attention:在 TD 注册短语和 TD 查询短语之间做双向帧级交互,弥补单个 utterance embedding 对局部短语证据建模不足的问题。
  • 实验结论清晰:TI 超过 TD 需要足够注册时长;极短 TI 下 TD 更稳;简单平均不足,神经重打分才是提升 EER 的关键。

12. 局限

这篇论文的优点是问题定义明确、工程路线轻量、实验覆盖多个骨干和 OOD 数据。但也有一些边界需要注意。第一,VoxPhrase 是从 VoxCeleb 自动切分得到,虽然规模大,但仍依赖 ASR 和 forced alignment 质量;真实设备中的远场噪声、回声、唤醒误检可能更复杂。第二,论文主要使用 EER 评估,没有展开真实产品中常见的固定 FAR/FRR 操作点分析。第三,方法需要注册端同时具备 TD 和 TI 语音,若用户注册流程只允许一句极短短语,混合注册收益会受限。

整体来看,这篇工作的价值不在于提出一个很大的声纹 backbone,而在于把短语级声纹验证中的两个实际矛盾讲清楚:短 TD 有内容一致性但身份信息少,长 TI 有身份稳定性但内容不一致。Hybrid enrollment 提供两类证据,parallel cross-attention 负责细粒度对齐,neural re-scoring 再学习如何融合它们。对于智能音箱、车载语音、个人助理和用户自定义关键词系统,这是一条比较务实的改进路线。

参考

Zhiqi Ai, Han Cheng, Shiyi Mu, Zhiyong Chen, Yongjin Zhou, Shugong Xu. Stabilizing Short Duration Speaker Verification through Neural Re-scoring with Hybrid Enrollment. arXiv:2606.16115v1, 2026. https://arxiv.org/abs/2606.16115

中文解读参考:短语音不到 3 秒,说话人验证怎么稳住?https://mp.weixin.qq.com/s/yM0XWdtOntNFMqWh3HW0hw

BPE分词算法-原理和代码实现

三种主流的Subword算法,它们分别是:Byte Pair Encoding (BPE)、WordPiece和Unigram Language Model。

执行分词的算法模型称为分词器(Tokenizer),划分好的一个个词称为Token(中文叫词元,为啥不直接叫Word?接着往后看),这个过程称为Tokenization

我们将一个个的token(可以理解为小片段)表示向量,我们分词的目的就是尽可能的让这些向量蕴含更多有用的信息,然后把这些向量输入到算法模型中。

由于一篇文本的词往往太多了,为了方便算法模型训练,我们会选取出频率(也可能是其它的权重)最高的若干个词组成一个词表(Vocabulary)

‼️古典分词方法的缺点

一个句子,使用不同的规则,将有许多种不同的分词结果。古典分词方法的缺点非常明显:

  • 对于未在词表中出现的词(Out Of Vocabulary, OOV),模型将无法处理(未知符号标记为 [UNK])。
  • 词表中的低频词/稀疏词在模型训无法得到训练(因为词表大小有限,太大的话会影响效率)。
  • ⭐️很多语言难以用空格进行分词,例如英语单词的多形态,”look”衍生出的”looks”, “looking”, “looked”,其实都是一个意思,但是在词表中却被当作不同的词处理,模型也无法通过old, older, oldest之间的关系学到smart, smarter, smartest之间的关系。这一方面增加了训练冗余,另一方面也造成了大词汇量问题。

Character embedding,是一种更为极端的分词方法,直接把一个词分成一个一个的字母和特殊符号。虽然能解决OOV问题,也避免了大词汇量问题,但缺点也太明显了,粒度太细,训练花费的成本太高,但这种思想或许我们后面会用到。

BERT算法的横空出世,NLP中的很多领域都被颠覆性的改变了,BERT也称为了一个非常主流的NLP算法。由于BERT的特性,要求分词方法也必须作出改变。这就对应提出了Subword算法(或成为WordPiece),该算法已经成为一种标配。

基于子词的分词方法(Subword Tokenization)

可见不论是传统分词算法的局限性,还是BERT的横空出世,都要求我们提出新的分词算法,下面就轮到本文的主角登场:基于子词的分词方法(Subword Tokenization),简称为Subword算法,意思就是把一个词切成更小的一块一块的子词。如果我们能使用将一个token分成多个subtokens,上面的问题就能很好的解决

这种方法的目的是通过一个有限的词表来解决所有单词的分词问题,同时尽可能将结果中token的数目降到最低。例如,可以用更小的词片段来组成更大的词,例如:

unfortunately” = “un” + “for” + “tun” + “ate” + “ly”。

可以看到,有点类似英语中的词根词缀拼词法,其中的这些小片段又可以用来构造其他词。可见这样做,既可以降低词表的大小同时对相近词也能更好地处理

Subword与传统分词方法的比较

  • 传统词表示方法无法很好的处理未知或罕见的词汇(OOV问题)。
  • 传统词tokenization方法不利于模型学习词缀之间的关系,例如模型学到的“old”, “older”, and “oldest”之间的关系无法泛化到“smart”, “smarter”, and “smartest”。
  • Character embedding作为OOV的解决方法粒度太细。
  • Subword粒度在词与字符之间,能够较好的平衡OOV问题。

目前有三种主流的Subword算法,它们分别是:Byte Pair Encoding (BPE)、WordPiece和Unigram Language Model。

字节对编码(BPE, Byte Pair Encoding)

字节对编码(BPE, Byte Pair Encoder),又称digram coding双字母组合编码,是一种数据压缩算法,用来在固定大小的词表中实现可变⻓度的子词。该算法简单有效,因而目前它是最流行的方法。

BPE首先将词分成单个字符,然后依次用另一个字符替换频率最高一对字符,直到循环次数结束。

接下来详细介绍BPE在分词中的算法过程:

算法过程

  1. 准备语料库,确定期望的subword词表大小等参数
  2. 通常在每个单词末尾添加后缀</w>,统计每个单词出现的频率,例如,low的频率为5,那么我们将其改写为"l o w </ w>”:5 注:停止符</w>的意义在于标明subword是词后缀。举例来说:st不加</w>可以出现在词首,如st ar;加了</w>表明该子词位于词尾,如we st</w>,二者意义截然不同
  3. 将语料库中所有单词拆分为单个字符,用所有单个字符建立最初的词典,并统计每个字符的频率,本阶段的subword的粒度是字符
  4. 挑出频次最高的符号对,比如说 th 组成的 th,将新字符加入词表,然后将语料中所有该字符对融合(merge),即所有 th 都变为 th。 注:新字符依然可以参与后续的merge,有点类似哈夫曼树,BPE实际上就是一种贪心算法
  5. 重复遍历 2和 3 操作,直到词表中单词数达到设定量下一个最高频数为1,如果已经打到设定量,其余的词汇直接丢弃

注:看似我们要维护两张表,一个词表,一个字符表,实际上只有一张,词表只是为了我们方便理解。</w> 是为了明确 subword 是否是词尾,它在训练、merge 过程中和普通字符一样对待,在最终输出时作为重建原词的标记使用。

我们举一个完整的例子,来直观地看一下这个过程:

1、获取语料库,这样一段话为例:FloydHub is the fastest way to build, train and deploy deep learning models. Build deep learning models in the cloud. Train deep learning models.

2、拆分,加后缀,统计词频:

3、建立词表,统计字符频率(顺便排个序):

4、以第一次迭代为例,将字符频率最高的de替换为de,后面依次迭代:

5、更新词表 继续迭代直到达到预设的subwords词表大小或下一个最高频的字节对出现频率为1。

如果将词表大小设置为10,最终的结果为:

d e
r n
rn i
rni n
rnin g</w>
o de
ode l
m odel
l o
l e

这样我们就得到了更加合适的词表,这个词表可能会出现一些不是单词的组合,但是其本身有意义的一种形式

BPE的优点

上面例子中的语料库很小,知识为了方便我们理解BPE的过程,但实际中语料库往往非常非常大,无法给每个词(token)都放在词表中。BPE的优点就在于,可以很有效地平衡词典大小和编码步骤数(将语料编码所需要的token数量)。

随着合并的次数增加,词表大小通常先增加后减小。迭代次数太小,大部分还是字母,没什么意义;迭代次数多,又重新变回了原来那几个词。所以词表大小要取一个中间值。

BPE的缺点

  • 对于同一个句子, 例如Hello world,如图所示,可能会有不同的Subword序列。不同的Subword序列会产生完全不同的id序列表示,这种歧义可能在解码阶段无法解决。在翻译任务中,不同的id序列可能翻译出不同的句子,这显然是错误的。
  • 在训练任务中,如果能对不同的Subword进行训练的话,将增加模型的健壮性,能够容忍更多的噪声,而BPE的贪心算法无法对随机分布进行学习。

个人理解:我感觉缺点直接可以忽略

BPE的适用范围

BPE一般适用在欧美语言拉丁语系中,因为欧美语言大多是字符形式,涉及前缀、后缀的单词比较多。而中文的汉字一般不用BPE进行编码,因为中文是字无法进行拆分。对中文的处理通常只有分词和分字两种。理论上分词效果更好,更好的区别语义。分字效率高、简洁,因为常用的字不过3000字,词表更加简短。

BPE的实现

实现代码如下:

import re, collections

def get_vocab(filename):
    vocab = collections.defaultdict(int)
    with open(filename, 'r', encoding='utf-8') as fhand:
        for line in fhand:
            words = line.strip().split()
            for word in words:
                vocab[' '.join(list(word)) + ' </w>'] += 1
    return vocab

def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    return pairs

def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

def get_tokens(vocab):
    tokens = collections.defaultdict(int)
    for word, freq in vocab.items():
        word_tokens = word.split()
        for token in word_tokens:
            tokens[token] += freq
    return tokens

跑一个例子试一下,这里已经对原句子进行了预处理:

vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
print('==========')
print('Tokens Before BPE')
tokens = get_tokens(vocab)
print('Tokens: {}'.format(tokens))
print('Number of tokens: {}'.format(len(tokens)))
print('==========')

num_merges = 5
for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print('Iter: {}'.format(i))
    print('Best pair: {}'.format(best))
    tokens = get_tokens(vocab)
    print('Tokens: {}'.format(tokens))
    print('Number of tokens: {}'.format(len(token

结果:

==========
Tokens Before BPE
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 17, 'r': 2, 'n': 6, 's': 9, 't': 9, 'i': 3, 'd': 3})
Number of tokens: 11
==========
Iter: 0
Best pair: ('e', 's')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'es': 9, 't': 9, 'i': 3, 'd': 3})
Number of tokens: 11
==========
Iter: 1
Best pair: ('es', 't')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'est': 9, 'i': 3, 'd': 3})
Number of tokens: 10
==========
Iter: 2
Best pair: ('est', '</w>')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 10
==========
Iter: 3
Best pair: ('l', 'o')
Tokens: defaultdict(<class 'int'>, {'lo': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 9
==========
Iter: 4
Best pair: ('lo', 'w')
Tokens: defaultdict(<class 'int'>, {'low': 7, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'w': 9, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 9
==========

编码与解码

上面的过程称为编码。解码过程比较简单,如果相邻子词间没有中止符,则将两子词直接拼接,否则两子词之间添加分隔符。 如果仍然有子字符串没被替换但所有token都已迭代完毕,则将剩余的子词替换为特殊token,如<unk>。例如:

# 编码序列
["the</w>", "high", "est</w>", "moun", "tain</w>"]

# 解码序列
"the</w> highest</w> mountain</w>"

如何调包使用BPE

BPE 可以直接用最经典的 subword-nmt 包,不需要自己实现

Paraformer-基于神经网络的热词方案

在使用语音识别服务进行语音转文字的过程中,大多数情况下模型能正确地预测高频词汇,但是对诸如人名地名、命名实体等词频较低或与用户强相关的词汇,模型往往会识别为一个发音相近的其他结果,这使得语音识别模型在日常生活中、垂直领域落地时并不完美。

contextual_paraformer模型

论文:FunASR: A Fundamental End-to-End Speech Recognition Toolkit   

NN热词定制化–CLAS

如何能够利用神经网络的建模与拟合能力,将用户自定义的热词纳入端到端语音识别模型的解码过程中,输出热词定制化的识别结果是ASR领域多年来备受关注的问题之一。在2018年,Google提出了Contextual Listen, Attend and Spell (CLAS)框架,在LAS这一经典的E2E ASR模型中进行了基于神经网络的热词定制化。CLAS主要通过两个核心思想进行热词建模:1.在训练阶段从label中随机采样文本片段模拟热词;2.在decoder的建模中引入额外的attention以建立文本隐状态与热词embedding的注意力连接;后续大量的工作证明了CLAS方案的有效性,在近几年出现了CPP-Network,NAM,Col-Dec CIF,Contextual RNN-T等等基于不同ASR基础框架的热词定制化工作,其算法核心均与上述两点一致。在对通义实验室自研的非自回归端到端语音识别模型Paraformer进行NN热词定制化支持时,我们首先采用了结合CLAS算法的方案,开源了工业级Contextual-Paraformer模型,有很强的热词召回能力。

如图2(a)所示。Paraformer是一个单步非自回归(NAR)模型,结合了基于语言模型的快速采样模块,以增强NAR解码器捕捉标记之间依赖关系的能力。

Paraformer由两个核心模块组成:预测器和采样器。预测器模块用于生成声学嵌入,捕捉输入语音信号中的信息。在训练过程中,采样器模块通过随机替换标记到声学嵌入中,结合目标嵌入生成语义嵌入。这种方法使得模型能够捕捉不同标记之间的相互依赖关系,并提高模型的整体性能。然而,在推理过程中,采样器模块处于非激活状态,声学嵌入仅通过单次传递输出最终预测结果。这种方法确保了更快的推理时间和更低的延迟。

Contextual Paraformer提供了通过利用命名实体自定义热词的功能,从而增强了激励机制,并提高了召回率和准确性。为了扩展基本的Paraformer模型,增加了两个附加模块——热词嵌入器和解码器最后一层的多头注意力,如图2(c)所示。

我们将热词表示为 𝒘 = 𝒘₁, …, 𝒘ₙ,作为输入传递给我们的热词嵌入器 。热词嵌入器由一个嵌入层和一个LSTM层组成,LSTM层接受上下文热词作为输入,并通过使用LSTM的最后状态生成一个嵌入,记作 𝑬𝒉。具体来说,某个热词首先被输入到热词嵌入器中,生成一系列隐藏状态。然后,我们使用最后一个隐藏状态作为热词的嵌入,捕捉输入序列的上下文信息。

  • 步骤1:Embedding 层将每个 token 映射为一个密集向量:xi=EmbedLayer(wi)(i=1…n),输出形状:[n × dₑₘ_bₑd]
  • 步骤 2:向 LSTM 编码将整个 token 序列输入到一个单向 LSTM(或 GRU)中:hi​=LSTM(xi​,hi−1​)
  • 步骤 3:取 LSTM 的最后隐藏状态作为表示:Eh​=hn​ 这是一个固定长度向量(维度 d_h),代表整个热词含义。

为了捕捉热词嵌入 𝑬𝒉 与 FSMN 记忆块最后一层的输出 𝑬𝒔′ 之间的关系,我们采用了多头注意力模块。然后,我们将 𝑬𝒔′ 和上下文注意力 𝑬𝒄 连​​接起来。此操作在公式中形式化:

我们使用一维卷积层( C​o​n​v​1​d )来降低其维数以匹配隐藏状态 𝑬𝒔′ ,后者作为后续层的输入。值得注意的是,除了这一修改之外,我们的 Contextual Paraformer 的其他流程与标准 Paraformer 的流程相同。

训练过程中,热词会在每个训练批次中从目标中随机生成。至于推理,我们可以通过向模型提供命名实体列表来指定热词。

CT-Transformer:

CT‑Transformer 旨在实时联合完成标点预测与非流畅语(disfluency)检测,并通过可控延迟机制确保满足流式输出约束,提升下游应用(如对话系统、翻译)对 Partial Decode 的实时性支持

Controllable Time-delay Transformer是达摩院语音团队提出的高效后处理框架中的标点模块。本项目为中文通用标点模型,模型可以被应用于文本类输入的标点预测,也可应用于语音识别结果的后处理步骤,协助语音识别模块输出具有可读性的文本结果。

Controllable Time-delay Transformer 模型结构如上图所示,由 Embedding、Encoder 和 Predictor 三部分组成。Embedding 是词向量叠加位置向量。Encoder可以采用不同的网络结构,例如self-attention,conformer,SAN-M等。Predictor 预测每个token后的标点类型。

在模型的选择上采用了性能优越的Transformer模型。Transformer模型在获得良好性能的同时,由于模型自身序列化输入等特性,会给系统带来较大时延。常规的Transformer可以看到未来的全部信息,导致标点会依赖很远的未来信息。这会给用户带来一种标点一直在变化刷新,长时间结果不固定的不良感受。基于这一问题,我们创新性的提出了可控时延的Transformer模型(Controllable Time-Delay Transformer, CT-Transformer),在模型性能无损失的情况下,有效控制标点的延时。

CT‑Self‑Attention 的核心目标是:限定每一层可访问的未来 token 数量,从而控制模型的整体延迟

引入一个控制 mask M,对不允许访问的位置打分为 −∞,得到:

新一代 SeACo-Paraformer 模型

热词定制化 (Hotword Customization)是针对低频偏僻词语识别而出现的语音模型研究。通过基于WFST神经网络的热词定制化方案,模型允许用户在识别语音时预设一些已知的先验词汇,将识别结果中发音相近的词汇识别或修正为用户预期的结果。本文介绍阿里巴巴通义实验室语音团队自研的新一代基于神经网络的热词定制化模型SeACo-Paraformer(Semantic-Augmented Contextual-Paraformer),较前一代基于CLAS的Contextual-Paraformer有着生效稳定,训练灵活,召回率更高等优势。

WFST热词激励方案从解码过程入手,召回稳定,但是需要在ASR模型推理之外进行基于N-gram的解码,并且对于一些训练数据中出现较少的词,ASR模型提供的后验概率过低,导致候选路径中没有包含待激励的词,此时基于WFST的热词增强大概率失效。

在Contextual-Paraformer开源一年之后,我们进一步开源新一代的NN热词定制化模型SeACo-Paraformer,旨在解决随机初始化CLAS模型生效不稳定的问题,同时进一步提升热词召回率。

SeACo-Paraformer在Paraformer的encoder-predictor-decoder框架中引入了用于热词建模的bias decoder,通过与感知热词位置的label计算loss进行显式的热词预测训练,在解码阶段将热词后验概率与原始ASR后验概率进行加权融合,实现了更加稳定的热词召回

NN热词方案对decoder状态与热词embedding进行attention计算以捕捉相关性,在热词数量上升时attention会由于稀疏问题导致注意力分散SeACo-Paraformer利用了bias decoder中深层attention的score进行了注意力预计算与筛选(Attention Score Filtering,ASF),实验表明ASF能够缓解热词数量增加导致的召回性能损失。

考虑一个语音特征 x1:T​ 和对应的文本y1:L​,我们在Paraformer推理过程中保留CIF输出 E1:L′​并行解码器隐藏状态(在输出层之前)D1:L′​

然后,从大小为 bs 的 y1:L​ 批次中随机采样出 n 个热词,记作 H1:n​。我们在这里使用4个超参数来控制采样过程:rb​ 用于控制批次采样的比例,其他批次的前向传播将使用默认的热词 ⟨blank⟩;ru​ 类似于rb​,但在一个活跃批次内部的发音级别进行控制,活跃批次中平均采样的热词数量为 ru×bs+1(其中一个是默认的热词);lmin​ 和 lmax​ 用于控制采样热词的最小和最大长度。

然后,热词列表中的字符序列会通过偏置编码器进行嵌入,偏置编码器包含一个嵌入层(与ASR嵌入共享参数)和一个LSTM层。

Z1:n​∈Rn×d 被去除维度并在第0维度上重复以进行批处理计算。接下来进入SeACo-Paraformer的主要部分。在偏置解码器内部,热词的偏置信息通过注意力机制引入到声学嵌入 E1:L′​ 和解码器隐藏状态 D1:L′​ 中。

偏置解码器由多个多头注意力层和前馈层组成。通过偏置声学嵌入和偏置解码器隐藏状态,可以通过输出层获得偏置概率 PASR1:L′​需要注意的是,一个额外的标记(计为#,表示无偏)会被添加到ASR输出词汇表中,以标记非热词位置的输出

根据偏置概率 Pb1:L′​,可以使用热词位置感知准则来更新与偏置相关的参数,其中非热词位置的标签被替换为 #(如图 1 中的 Lbias​ 所示)。

在冻结经过充分训练的 Paraformer 模型的情况下,我们通过引入偏置输出层、偏置解码器和偏置编码器,并使用随机采样的热词及其对应目标进行训练,使 ASR 系统具备热词上下文化能力。值得注意的是,偏置相关参数的训练是独立于 ASR 训练的,因此可以使用专门的热词数据(例如低频语言短语)和训练策略,而不会影响 ASR 的整体性能。

对于使用给定热词列表进行 SeACo-Paraformer 推理的第 i 步,我们得到语境化 ASR 的最终合并概率为:

当没有传入热词或未检测到热词时,SeACo-Paraformer仅使用 𝐏A⁢S⁢Ri 。 λ 是一个可调参数,用于调整信任偏差解码器输出的程度。

在实际应用中,随着输入的热词数量的扩大,热词激活的性能会相应下降——偏见解码器内的交叉注意很难在 ASR 解码器输出 𝐃1:L′ 和大规模稀疏热词嵌入 𝐙1:n 之间建立正确的联系。为了使 SeACo-Paraformer 能够使用大规模热词列表进行热词定制,我们提出了注意分数过滤(ASF)策略。首先对全热词列表进行偏见解码器推理,得到注意分数矩阵 𝐀∈RL×n ,其中 L 是输出 token 的长度, n 是热词的数量。然后,我们将 L 中各个步骤的分数相加,得到每个热词的注意分数。根据注意分数,我们可以挑选出最活跃的 k 热词,从而进行真正有效的偏见解码器推理。细粒度的上下文知识选择相比,我们的偏差解码器由多个交叉注意层组成,我们发现最后一层的分数对于过滤最有效。

热词理论上无限制,但为了兼顾性能和效果,建议热词长度不超过10,个数不超过1k,权重1~100

WeTextProcessing-文本[逆]正则化

Github:https://github.com/wenet-e2e/WeTextProcessing

摘自:https://mp.weixin.qq.com/s/q_11lck78qcjylHCi6wVsQ

Funasr仓库:

Motivation

文本正则化(Text Normalization,TN)和反正则化(Inverse Text Normalization,ITN)是构建一个完整的语音交互系统不可或缺的部分。前者广泛用于语音合成系统的前端处理,而后者则在语音识别系统的识别文本上屏显示时影响着字幕的观感体验。

当前学术界中被广泛研究的 TN / ITN 系统主要有三种类型:

  • 基于语法规则的 WFST [1]:这种系统由大量特定于语言的语法组成,优点是准确可控,可以快速修 bug ,缺点是对于容易产生歧义的文本不够鲁棒。
  • 基于神经网络的端到端模型 [2]:构建这种模型时,挑战从撰写更精确的语法规则变成了标注和收集覆盖范围更广的数据。端到端模型的一个主要缺点是会产生无法恢复的错误,这时经系统转换后的文字可能在语法上是合理的,但却与原始文本的语义大相径庭。此外,对于 badcase 的修复也不如规则的方式快捷。
  • 同时使用规则语法和神经网络的混合系统 [3]:在混合框架中,只有当系统没有找到匹配的语法规则才会转用神经网络。这种方式比较好地权衡了规则和 NN 的优劣,但是对计算资源提出了更高的要求。

鉴于以上三种系统的优劣,WeTextProcessing 选择实现基于语法规则的WFST 方案。在全球范围内的开源TN/ITN 项目中,目前受众最广泛的是谷歌公司推出的C++ 框架 Sparrowhawk [4] 。该框架的不足之处是它仅仅是一个规则执行引擎,谷歌公司并没有开源相关语言的语法规则。此外,Sparrowhawk 的实现依赖了许多第三方开源库(包括 OpenFst 、Thrax 、re2 、protobuf ),导致整体框架不够简便、轻量化。另一个较为成熟的项目是英伟达公司开源的 nemo_text_processing [5],该项目依旧使用Sparrowhawk 作为生产环境下的部署工具。与谷歌不同的是,该项目还开源了诸如英语、德语、俄语等多种语言的规则语法。在中文 TN / ITN 规则领域,Jiayu 等第三方个人开发者曾开源出一套定制化的中文 TN / ITN 规则库 chinese_text_normalization [6]

站在这些优秀开源项目的肩膀上,WeTextProcessing秉承 简单易用 和Production First & Production Ready 的原则,为中文专门设计和实现一款开源易用的 TN / ITN 工具,它不仅仅包含了包含一套完整的中文 TN / ITN 规则语法,同时也提供了一个可以一键 pip install 使用的 py工具包以及比Sparrowhawk 依赖项更少(生产环境下仅依赖 OpenFst )的整体更轻量化的 C++ 规则处理引擎。

快速上手

一键install,六行代码搞定文本处理!

# install
pip install WeTextProcessing

# tn usage
>>> from tn.chinese.normalizer import Normalizer
>>> normalizer = Normalizer()
>>> normalizer.normalize("2.5平方电线")

# itn usage
>>> from itn.chinese.inverse_normalizer import InverseNormalizer
>>> invnormalizer = InverseNormalizer()
>>> invnormalizer.normalize("二点五平方电线")

技术细节

TN 和 ITN 的流程都是包含三个部分:Tagger, Reorder 和 Verbalizer。Tagger 负责对输入的文本进行解析,得到结构化的信息。Reorder 负责对结构化信息进行顺序的调整。最终 Verbalizer 负责将重排序之后的结构化信息拼接起来。

TN 流程

ITN 流程

语法规则设计

WeTextProcessing 使用 pynini [7] 来编写和编译规则语法,规则语法可以将一个字符串转换为另一个字符串。规则语法通常可以表示为一个 WFST,pynini 的底层使用了 OpenFst 来实现 WFST 相关的功能。使用 pynini 编写的规则语法示例如下图所示:

  • digits = zero | digit 的 | 操作符表示 WFST 理论中的 union 操作;
  • cross(‘十’, ‘1’) 表示 WFST 理论中弧上的输入是“十”,输出是“1”,WFST 从一个状态转到另一个状态时若经过该弧则说明系统匹配到了“十”并成功将其转换为了“1”;
  • delete(‘十’) 表示弧上的输入是“十”,输出是空,即经过该弧时会删除“十”;
  • digit + delete(‘十’) 中 + 表示WFST理论中的 concat 操作,它将两个fst连起来;
  • accep(‘兆’) 表示弧的输入和输出都是“兆”,此时 WFST 相当于一个 FSA;
  • addzero**2addzero**3 分别表示将 addzero 重复两次和三次;
  • digits.ques 和 digits.plus 则分别表示将 digits 重复零到一次 和 重复一到无穷次

此外还有一些语法特性,比如下图中:

  • add_weight(Char().tagger, 100) 表示为 Char().tagger 这条路径赋予权重(路径长度)为 100。当有多条路径都可以匹配当前输入时,我们取最短路径作为终选结果。例如“一点零五分”最终会被 ITN 成 “1:05” 而不是 “1.05分”。
  • insert(‘ ‘) 表示弧上的输入和输出分别是“”和“ ”,即经过该弧时会强制插入一个空格。
  • processor @ tagger.optimize() 中 @ 表示将两个 fst 进行 compose 操作,optimize() 表示对 tagger 进行 epsilon-removal,determinization 以及 minimization [8]
  • ‘[EOS]’ 表示正则表达式中匹配到的 string 的结尾,同理这里没有列出的 ‘[BOS]’ 则表示开头 [9]

更多详尽的说明请参考pynini 的相关文档 [7]。对于本文所构建的所有WFST,我们采用 OpenFst 中默认的热带半环作为其类型,做出这个选择的原因是此类型对求网格图中的最短路径的操作有效率优势,其路径权重的计算仅需对沿路径的所有弧的权重进行简单求和。

进阶用法

如何快速修 badcase

当遇到 badcase 的时候,我们首先需要确定 badcase 属于什么类型,日期?时间?还是分数等等?是没有转换,还是转换成了其他类型。然后再去相对应的 rules 中进行修复,可能需要改代码,也可能需要改 tsv 文件。

比如若 ITN 系统将 “三心二意” 错误转成了 “3心2意” 则有两种解决方案:

  1. 在 whitelist.tsv 添加相关的映射放弃相关词汇的转换
  2. 将enable_standalone_number设置为False,此时系统对不带单位的数字不会进行转换

值得注意的是,WeTextProcessing 大多数失败案例是由于上下文歧义或特殊案例造成的长尾问题。例如,“三点五分” 可以是时间 “3:05” 也可以是量词 “3.5 分” 表示运动员得分。编写语法时若考虑更多的上下文可以一定程度上缓解这种情况,例如,如果 “三点五分” 前面有单词 “得到” ,则将其检测为运动员得分。当然,这种打补丁的方式并不能适用于所有情况。出于这个原因,如果想要设计一个能够覆盖 100% 场景的系统,语法的数量将不可避免呈指数级增长。其他常见的失败案例是由于定义不完整。例如,如果没有预定义 “千瓦时” 到 “kwh” 的度量缩写转换,系统将无法转换 “两百千瓦时” 为 “200kwh” 。这个问题相对来说容易解决,仅需在已有的量词类中添加所需的转换规则。

生产环境部署

对于想要自己对规则进行DIY的用户,可以通过以下方式获得自己的规则文件并部署到不同的环境中。

git clone https://github.com/wenet-e2e/WeTextProcessing.git
cd WeTextProcessing
# `overwrite_cache` will rebuild all rules according to
#   your modifications on tn/chinese/rules/xx.py (itn/chinese/rules/xx.py).
#   After rebuild, you can find new far files at `$PWD/tn` and `$PWD/itn`.
python normalize.py --text "2.5平方电线" --overwrite_cache
python inverse_normalize.py --text "二点五平方电线" --overwrite_cache

在已经pip安装好的工具包中使用自己的规则:

# tn usage
>>> from tn.chinese.normalizer import Normalizer
>>> normalizer = Normalizer(cache_dir="PATH_TO_GIT_CLONED_WETEXTPROCESSING/tn")
>>> normalizer.normalize("2.5平方电线")# itn usage
>>> from itn.chinese.inverse_normalizer import InverseNormalizer
>>> invnormalizer = InverseNormalizer(cache_dir="PATH_TO_GIT_CLONED_WETEXTPROCESSING/itn")
>>> invnormalizer.normalize("二点五平方电线")

在C++中使用自己的规则:

cmake -B build -S runtime -DCMAKE_BUILD_TYPE=Releasecmake --build build
# tn usage
./build/bin/processor_main --far PATH_TO_GIT_CLONED_WETEXTPROCESSING/tn/zh_tn_normalizer.far --text "2.5平方电线"
# itn usage
./build/bin/processor_main --far PATH_TO_GIT_CLONED_WETEXTPROCESSING/itn/zh_itn_normalizer.far --text "二点五平方电线"

总结和展望

未来,WeTextProcessing 的工作将聚焦在对 Corner Case 的规则修补:相比于规则撰写,设计一套合理的测试集是一件更为困难的事情,这是因为实际生产过程中总会遇到数不清的 corner case 。WeTextProcessing 中虽然提供了一个简单的单元测试和示例测试,但其覆盖场景仍未能达到 100% 。在未来,WeTextProcessing 的重点方向之一就是越来越多地投入部署到真实的线上环境中,以身试错,case by case 分析当前规则存在的可能漏洞并加以弥补。

参考资料

[1] Peter Ebden and Richard Sproat, “The kestrel TTS text normalization system,” Nat. Lang. Eng., vol. 21, no. 3, pp. 333–353, 2015.

[2] Courtney Mansfield, Ming Sun, Yuzong Liu, Ankur Gandhe, and Björn Hoffmeister, “Neural text normalization with subword units,” in Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, NAACL-HLT 2019, Minneapolis, MN, USA, June 2-7, 2019, Volume 2 (Industry Papers), Anastassia Loukina, Michelle Morales, and Rohit Kumar, Eds. 2019, pp. 190–196, Association for Computational Linguistics.

[3] Richard Sproat and Navdeep Jaitly, “An RNN model of text normalization,” in Interspeech 2017, 18th Annual Conference of the International Speech Communication Association, Stockholm, Sweden, August 20-24, 2017, Francisco Lacerda, Ed. 2017, pp. 754–758, ISCA.

[4] Peter Ebden and Richar Sproat, “Sparrowhawk,” 2022, https://github.com/google/sparrowhawk.

[5] Yang Zhang, “nemo_text_processing,” 2022, https://github.com/NVIDIA/NeMo/tree/main/nemo_text_processing.

[6] Jiayu Du, “chinese_text_normalization,” 2022, https://github.com/speechio/chinese_text_normalization.

[7] K. Gorman. 2016. Pynini: A Python library for weighted finite-state grammar compilation. In Proceedings of the ACL Workshop on Statistical NLP and Weighted Automata, pages 75-80.

[8] https://www.opengrm.org/twiki/bin/view/GRM/PyniniOptimizeDoc

[9] https://www.openfst.org/twiki/bin/view/GRM/ThraxQuickTour

向云端

向云端
山那边
海里面
真实的我应该走向哪边
日落前
风来临
石墩下我在盘腿坐着
人到底靠什么来定义丑恶
神啊你在哪
山啊我害怕
海啊也带不走
尽头到底有没有
如果你很难过
不如先收拾你的房间
别再辗转反侧
等会儿阳光会照在你的旁边
没洗的别再攒着
换个新的发型买个好看的包
压抑焦虑心情忐忑
就去养只爱你的猫
不是不如意
也许你并没围着自己的赛道走
你真的很美丽
为什么常说自己的外貌丑
或许你像我一样很胖
生活让你感到卑贱
那么当你听到这首歌的时候
选择与我共同蜕变
也许你现在很难过
也许正躺在被窝
也许你现在很迷茫
正在酒吧里坐着
也许你在工作
或者刚刚分手了
也许你在山脚下
会情不自禁的哼出这首歌
向云端
山那边
海里面
真实的你在于怎么选择
神啊你在哪
山啊我害怕
海啊带走哀愁
就像带走每条河流

郭源潮

你说你知道他们的世界
悲歌三首买一切
买昆仑落脚 蓬莱放思想
买人们的争执酿酒汤
买公主坟的乌鸦
事发之木和东窗之麻
买胭脂河里船行渔歌
黄金世界中万物法则
你我都一样 将被遗忘 郭源潮
你的病也和我的一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
买石灰街车站的海鸥
山水禽兽和年少一梦
买太平湖底陈年水墨
哥本哈根的童年传说
其实你我都一样 终将被遗忘 郭源潮
你的病也和我的一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
其实你我都一样 终将被遗忘 郭源潮
你的病也和我一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
你我山前没相见 山后别相逢
你我山前没相见 山后别相逢