把听到的一个中文名字对到一份花名册上,并且在拿不准的时候说自己拿不准。
仓库:https://github.com/charlenemercury/roster-match · 零依赖 · Node >= 14
npm install roster-matchconst { 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 }这是一个开发者组件,不是开箱即用的语音录入软件。 它不做语音识别、不提取分数、不写数据库。那些各家都不一样,也都比匹配本身更需要小心。
有人对着手机说一句「杨林交了,李明没交」,语音识别把名字听成了别的字 —— 「杨凌」被听成「杨林」。要把这句话落到具体的人头上,就得在名单里按读音找人。
这个包只干这一件事,外加一个把整段话按人切开的小工具。
通用库比的是字符串相似度;中文名字要比的是读音,而且读音有自己的结构:
- 「李明」和「李鸣」后一个字完全不同,却是同一个音
- 「陈思远」和「陈思达」两个字一样,读音差得远
- 「王西安」(wang-xi-an)和「王先」(wang-xian)拼起来一模一样,但显然不是一个人
- 「单」平时读 dān,做姓读 shàn;「曾」「区」「仇」「解」「查」都是这样
最后两条是这个包和"把拼音拼成字符串再算编辑距离"的主要区别: 它按音节比,并且知道哪些字在姓的位置上换个音。
命中哪一层,返回值里的 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 调大反而更糟,它只会想得更久。这个直觉是反的。
所以:
- 选不带思维链的通用文本模型,temperature 设 0,要求只输出 JSON
- 万一只有推理模型可用,
max_tokens至少给 3000 以上,并且把空正文当失败处理 - 课堂/窗口期场景的硬指标是几秒内出结果:老师站在讲台上,等不了 30 秒
零运行时依赖,Node 14+。目前是 CommonJS,浏览器里要用得先打包(esbuild / webpack / vite 都行),
不能直接 <script src> 引。
从一个真实教学场景里抽出来的。真正有用的三件事都写在上面了: 按音节比而不是拼成字符串、拿不准就交给人、缺字要查得出来。
还没有公开的评测集和使用量数据,所以不宣称比现有方案更准。 要做完整的语音录入,这个包之外还需要:语音服务选型、数值范围校验、写入幂等、撤销、权限 —— 这些都不在这里。
AGPL-3.0-only(全文见 LICENSE)。下面是提要,以许可证全文为准:
- 商用是允许的。 AGPL 不禁止收费、不禁止用在商业产品里 —— 它要求的是在分发或提供网络服务时让使用者拿到对应源码。
- 自己用、不改、不给别人用:没有额外义务。
- 改了之后让别人通过网络使用它(第 13 条):要向这些使用者提供你修改后的对应源码 —— 内网部署也算,只要使用的人不是你自己。
- 分发(给别人拷贝、装进产品发出去):同样要给源码,并且按 AGPL 继续授权。
什么时候才需要另谈商业许可? 不是因为"要收费",而是因为你不想承担上面这些开源义务。 只要你愿意照 AGPL 办,直接用就行,不用找任何人。
判断具体场景请看 GNU 官方说明:https://www.gnu.org/licenses/gpl-faq.html 和 https://www.gnu.org/licenses/why-affero-gpl.html。这里的提要不构成法律意见。
内置拼音数据来自 pypinyin(MIT),
上游版权与许可全文见 NOTICE.md。
先看 CLA.md,在第一个 PR 里回一句"我已阅读并同意本项目的 CLA"就行,
不用写真名、不用扫描件。
里面有一条没有例外:不要提交任何真实的个人信息, 尤其是未成年人的姓名、学号、成绩、照片。测试数据必须是编的。 这个仓库里现有的「杨凌」「李明」「顾言七」全是编出来的。