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…

What is this?A zero-dependency JavaScript library that matches a Chinese name heard through speech recognition (or any garbled transcript) to the correct entry in a roster, comparing by pinyin syllables and surfacing candidates with reasons instead of forcing a decision when it is unsure.
Is this a full speech-to-text or attendance solution?No, it is only the name-matching component for developers. It does not do speech recognition, does not extract scores, and does not write anything to a database — those parts are left to the integrator.
How do I use it in a project?Just require() it directly in Node.js 14+ since it has zero runtime dependencies and ships as CommonJS; to use it in a browser you first need to bundle it with esbuild, webpack, or vite.
Do I need Python, an API key, or any external service to run it?No. It is pure rule-based matching with no dependencies, no network calls, and no API keys; everything runs locally and instantly.
How reliable is a match?Matches fall into eight tiers (exact, samePinyin, oneLetter, surnameNear, accent, sameChars, loneSurname, nearPinyin); only the top four tiers can be marked 'confident', and a high numeric score on a lower tier is still just a guess, not a verified identity.
Can it also split one long spoken sentence into separate per-person records?Yes, a helper splits a transcript whenever a digit is immediately followed by a Chinese character, treating that as a new person, while a following '+', '.', or another digit means the score still belongs to the same person; it deliberately leaves Chinese-written numbers or addition/subtraction phrasing unsplit rather than guessing.
What should I check before deploying it on my real roster?Run the matcher once against your actual name list locally to find any characters missing from the built-in pinyin table and add them to pinyin-table.js — a missing character fails to match silently, without throwing an error.
Why not just use a generic fuzzy string-matching library for Chinese names?Generic libraries compare character similarity, but Chinese names need to be compared by syllable: homophone characters read identically, some surnames change pronunciation (单/曾/区/仇/解/查), and concatenated pinyin strings can collide between unrelated names, all of which a plain edit-distance approach gets wrong.
Should I bolt an LLM on top for the tricky cases?Only for things rules cannot catch, like self-corrections mid-sentence; the author recommends a non-reasoning model since reasoning models burn the token budget on hidden thinking and can return empty responses, keeping all score math local and still passing the model's output through this library's own confidence check before trusting it.
Size
0.1 MB
Stars
104
Updated
CommitOct 16
Age
19d
Commits · 90 Days