roster-match README

The project's README from github.com/charlenemercury/roster-match. As one markdown file: README.md.

roster-match

把听到的一个中文名字对到一份花名册上,并且在拿不准的时候说自己拿不准。

仓库:https://github.com/charlenemercury/roster-match · 零依赖 · Node >= 14

npm install roster-match
const { RosterMatch } = require('roster-match');
const M = new RosterMatch();

M.match('杨林', [{ id: 1, name: '杨凌' }, { id: 2, name: '李明' }]);
// → { student: { id: 1, name: '杨凌' },
//     kind: 'oneLetter', score: 85, why: '一个字的读音差一个字母',
//     alts: [], ambiguous: false, confident: true }

这是一个开发者组件,不是开箱即用的语音录入软件。 它不做语音识别、不提取分数、不写数据库。那些各家都不一样,也都比匹配本身更需要小心。

它解决什么问题

有人对着手机说一句「杨林交了,李明没交」,语音识别把名字听成了别的字 —— 「杨凌」被听成「杨林」。要把这句话落到具体的人头上,就得在名单里按读音找人。

这个包只干这一件事,外加一个把整段话按人切开的小工具。

为什么不直接用通用的模糊匹配库

通用库比的是字符串相似度;中文名字要比的是读音,而且读音有自己的结构:

最后两条是这个包和"把拼音拼成字符串再算编辑距离"的主要区别: 它按音节比,并且知道哪些字在姓的位置上换个音。

分层与判定

命中哪一层,返回值里的 kind 就是什么:

kind 分 什么情况 例子
exact 100 姓名一模一样 李明 → 李明
samePinyin 95 音节逐个相同 李鸣 → 李明
oneLetter 85 有一个字的读音差一个字母 杨林 → 杨凌(lin / ling)
surnameNear 78 同姓,名字读音接近
accent 70 口音归一后相同 刘兰 → 刘楠(n/l 不分)
sameChars 65 名字里有两个字一样 陈思达 → 陈思远(打字打错生僻字时读音根本不像)
loneSurname 62 名单里只有这一个这个姓
nearPinyin 55 读音接近

判定用的是 kind,不是分数。 你可以通过 scores 调排序,但调不出一个假的 exact。

而且只有前四层(exact / samePinyin / oneLetter / surnameNear)才够格标成 confident。 「名单里只有一个姓李」这种猜测,给它 99 分也还是猜测。这条可以用 reliable 选项调整。

该看哪两个字段

const r = M.match(said, roster);
if (!r)                 // 没人说得通
else if (r.ambiguous)   // 还有别人也说得通 → 把 r.alts 摆出来让人选
else if (r.confident)   // 层级可靠、分数够高、没有并列
else                    // 有人选,但心里没底 → 也该让人确认

⚠️ confident 是这套启发式的自信程度,不是识别正确率。 85 分不等于 85% 准确。它为真也不代表可以直接写库。

最要紧的一条设计原则

拿不准的时候要老实说拿不准。

匹配错一个人,代价是对方对着一条错记录发愣,而且多半发现不了; 说一句「拿不准,你选一下」,代价只是多点一下。所以这个包宁可多报不确定。

一个具体体现:语音只能挑一种汉字写法。名单里同时有「李明」和「李鸣」时, 识别结果正好写成「李明」不能证明说的就是那个人 —— 所以默认把同音的也摆进 alts。

new RosterMatch();                 // 默认:输入来自语音 → 同音的一律并列
new RosterMatch({ voice: false }); // 确定是键盘打的 → 写法对上就算确定

把一整段话按人切开

const { splitSpeech } = require('roster-match');
splitSpeech('杨凌90+30李明80+20');   // → ['杨凌90+30', '李明80+20']

语音识别经常一个标点都不给。只按标点切的话整句是一段,贪婪匹配会把名字吃成 「杨凌90+30李明」——只出一个人,还对错了人。

规则:数字后面紧跟汉字 = 换了一个人;后面跟的是 + + 加 . 或另一个数字时,说明还在同一个人身上。 全角数字会先归一成半角;名字和数字之间的空格不当分隔(「李明 92」不切开)。

支持:名字 + 阿拉伯数字,人与人之间有标点、空格或直接连读。 不支持,而且会原样留在一段里(不会误切成两个人): 中文数字连读「李明九十分王红八十」、加减分描述「李明90分扣2分」。 这是刻意的 —— 中文数字和业务规则该由上层决定,不该在这里无限追加正则去猜。 (识别 加 扣 减 得 共 总 这几个字,就是为了不把它们当成下一个人的姓。)

上线前必做:查缺字

这类工具最容易悄悄失效 —— 名字里有个字查不到读音,就匹配不上,而且不报错。

M.auditRoster(roster);   // → ['翀', '龑']  这些字表里没有读音

拿你的真实名单在本地跑一遍,缺的字补进 pinyin-table.js 再上线。 覆盖 GB2312 + GBK 和扩展区的检测,但姓名里的生僻字永远补不完。

返回空不等于这张表足够覆盖你的名单,只说明这几个字查得到音。 另外:别把真实名单贴到公开 issue 里求补字,只贴那几个单字。

名单的要求

每个人必须有唯一的 id。缺了或者重了会直接抛错 —— 不能悄悄当成同一个人,那会把两个同名的人合并掉。

可调的地方

new RosterMatch({
  voice: false,               // 输入是键盘打的,不是语音
  accent: ['zcs', 'nasal'],   // 只开平翘舌和前后鼻音,关掉 n/l 和 h/f
  surnames: { 澹: 'tan' },     // 补充多音姓的读音
  accept: 90,                 // 分数线调高 → 更多情况要人确认
  tie: 12,                    // 和第一名差 12 分以内都算并列
  scores: { oneLetter: 80 },  // 单独调某一层的分数(只影响排序)
  table: myTable,             // 换一张拼音表
});

accent 默认全开是给西南官话区用的(平翘舌、前后鼻音、n/l、h/f 都不分)。 别的地方按需关掉 —— 开得越多,误判越多。

要不要再配一个大模型

这个包本身不需要模型 —— 纯规则、离线、即时、免费,没有额度也没有延迟。

模型能补上规则做不到的几类:口误改口(「林可晴88,哦不对是89」)、说法不规整 (「后面那几个都没交」)、两个人读音完全一样要靠上下文判断。真要加,建议这么分工:

选什么模型:别用推理模型

按读音在名单里挑人是查表对音,不是推理任务。推理模型会先写一大段思维链, 而思维链的字数也算进 max_tokens —— 预算被吃光时返回的正文是空字符串, finish_reason 是 length,看起来像"模型没响应",其实是被自己的思考挤掉了。

同一份请求实测(47 人名单,一次认 8 个名字,max_tokens=3500,temperature 0):

模型 耗时 结果
MiniMax-M3(推理型) 25–27 秒 正文时常为空(finish_reason=length,思考写了 8000+ 字)
MiniMax-M3,预算调到 8000 67 秒 正文仍为空,思考写了 22000+ 字
MiniMax-Text-01 3.7 秒 正常
abab6.5s-chat 5.4 秒 正常

准确率呢?拿 10 个同音听错的用例(「王浩燃→王浩然」「何智远→何知远」「林可青→林可晴」 这类)对照,三个模型都是 10/10 —— 推理没带来任何准确率收益,只带来了延迟和空返回。

注意第二行:把 max_tokens 调大反而更糟,它只会想得更久。这个直觉是反的。

所以:

环境

零运行时依赖,Node 14+。目前是 CommonJS,浏览器里要用得先打包(esbuild / webpack / vite 都行), 不能直接 <script src> 引。

来历与边界

从一个真实教学场景里抽出来的。真正有用的三件事都写在上面了: 按音节比而不是拼成字符串、拿不准就交给人、缺字要查得出来。

还没有公开的评测集和使用量数据,所以不宣称比现有方案更准。 要做完整的语音录入,这个包之外还需要:语音服务选型、数值范围校验、写入幂等、撤销、权限 —— 这些都不在这里。

许可证

AGPL-3.0-only(全文见 LICENSE)。下面是提要,以许可证全文为准:

什么时候才需要另谈商业许可? 不是因为"要收费",而是因为你不想承担上面这些开源义务。 只要你愿意照 AGPL 办,直接用就行,不用找任何人。

判断具体场景请看 GNU 官方说明:https://www.gnu.org/licenses/gpl-faq.html 和 https://www.gnu.org/licenses/why-affero-gpl.html。这里的提要不构成法律意见。

内置拼音数据来自 pypinyin(MIT), 上游版权与许可全文见 NOTICE.md。

想提 PR?

先看 CLA.md,在第一个 PR 里回一句"我已阅读并同意本项目的 CLA"就行, 不用写真名、不用扫描件。

里面有一条没有例外:不要提交任何真实的个人信息, 尤其是未成年人的姓名、学号、成绩、照片。测试数据必须是编的。 这个仓库里现有的「杨凌」「李明」「顾言七」全是编出来的。

roster-match (charlenemercury/roster-match)

Searching Database…

这是什么?一个零依赖的 JavaScript 库,用于把语音识别听到的中文姓名(或任何乱码转写文本)匹配到名单中的正确条目,按拼音音节进行比较,并在不确定时给出带理由的候选,而不是强行下结论。
这是完整的语音转文字或考勤解决方案吗?不是,它只是面向开发者的姓名匹配组件。它不做语音识别,不提取分数,也不向数据库写入任何内容——这些部分留给集成方完成。
在项目中怎么使用它?在 Node.js 14+ 中直接 require() 引入即可,因为它零运行时依赖且以 CommonJS 形式发布;如果要在浏览器中使用,需要先用 esbuild、webpack 或 vite 打包。
运行它需要 Python、API 密钥或任何外部服务吗?不需要。它是纯规则匹配,没有依赖、没有网络调用、也没有 API 密钥;一切都在本地即时完成。
匹配结果有多可靠?匹配结果分为八档(exact、samePinyin、oneLetter、surnameNear、accent、sameChars、loneSurname、nearPinyin);只有前四档可以标记为“confident”,较低档位上即使数值分数很高也只是猜测,不是经过核实的身份。
它还能把一句很长的语音拆分成每个人的单独记录吗?可以,一个辅助函数会在数字后紧跟一个汉字时拆分转写文本,把这当作一个新的人;而如果后面跟的是'+'、'.'或另一个数字,则分数仍属于同一个人;对于用汉字书写的数字或加减表述,它会刻意保持不拆分,而不是去猜。
把它部署到我真实的名单之前,我应该检查什么?先在本地用你真实的姓名列表跑一次匹配器,找出内置拼音表中缺失的字并把它们添加到 pinyin-table.js——缺失的字会静默匹配失败,不会抛出错误。
为什么不直接用通用的模糊字符串匹配库来处理中文姓名?通用库比较的是字符相似度,但中文姓名需要按音节比较:同音字读音完全相同,有些姓氏读音会变化(单/曾/区/仇/解/查),而且拼接起来的拼音串可能在不相关的姓名之间发生冲突,这些情况简单的编辑距离方法都会处理错。
对于棘手的情况,我应该在上面再套一层 LLM 吗?只用于规则捕获不了的情况,比如句子中间的自我纠正;作者推荐使用非推理模型,因为推理模型会把 token 预算耗在隐藏思考上,还可能返回空响应。所有分数计算都保持在本地,而且在采信模型输出之前,仍要先让它通过本库自带的置信度检查。
Size
0.1 MB
Stars
104
Updated
CommitOct 16
Age
22d
Commits · 90 Days