方言语料库怎么建:3550 条词、3129 个音频之后,我们才明白的 6 个不可逆坑

2026-10-20 · 爱智工坊 · 方言语料实录

做方言语音(苏州话)这件事,最贵的不是模型,是语料。模型可以换、可以重训,语料一旦错了位,就是永久损失——你没法把已经覆盖掉的声音找回来。

我们用一台常驻的方言数字人(小苏)做真实对话采集,再把语料同步到训练机上。到目前为止手上是 3550 条词条的标准词库、3129 个音频文件(wav 2086 + webm 1043)、累计 419.63 MB。数量看着还行,但真正值得写下来的是:我们在这条链路上踩过的 6 个坑,每一个都曾经在"没有任何报错"的情况下,安静地把数据变成废料。

下面每一条都给出实测数字与判据,不含凭据、不含真实 IP。如果你也在建方言语料库(粤语、闽南语、吴语都一样),这几条大概率能帮你少烧两个月。

一、turn_id 只要"每天从 1 开始",音频就会互相覆盖

最初的落盘设计很直觉:每轮对话一个 turn_id,音频按 asst_%06d.wav 命名,全部扔进同一个 audio/ 扁平目录。

问题在于唯一性域和命名空间不一致:turn_id 的唯一边界是"当天",而文件名是全局扁平的。于是第二天有人再说话,turn 1..N 直接覆盖第一天的同名文件。

实测证据(2026-09-04 巡检):09-02 有 263 行语料,09-03 只有 62 行,两天的 turn 集合完全重叠 1~62;asst_000001.wav 的 mtime 全部变成 09-03 下午。
抽样比对:09-02 的 turn=1 文本是「倷覅乱动哉,我正看监控呢。」,09-03 的 turn=1 是「倷个问题有点模糊…」——两条文本指向同一个音频文件。

更隐蔽的是:conversations_20260902.train.jsonl 是 09-02 21:01 生成的,早于被覆盖的时刻,所以文件里还老老实实写着 audio_trusted: true。审计字段说"可信",数据其实已经烂了。09-02 那份 15 条 train 集,15/15 全灭。

这个问题不是一次性的。每出现一个新的对话日,就吃掉前一个对话日的 turn 1..N:

日期当天落盘被吃掉的前一日损失
09-09208 条(turn 1..208)09-02 / 09-0388 + 62 条音频
09-10151 条(turn 1..151)09-09(train 132 行)96/132 行失效,存活 1 天

历史永久损失最终定格在 09-02 15 + 09-03 30 + 09-09 96 + 09-10 77 = 218 行 train 集。这些不是"待修复",是没了。

怎么根治的(v21):让唯一性域和命名空间对齐——音频文件名带日期,audio/asst_20260911_000079.wav。同一时期还加了模块级分配器:进程内用锁串行发号,进程重启时从"当日 JSONL 里最大的 turn_id"续接,并在记录里写 schema: "v14" 作为可信标记。

修复的验证方式很干脆:看同步输出里「音频:新增 / 更新」两个数字。修复前每天都有"更新几百",修复后某一轮直接 `新增 1128 / 更新 0`——更新数归零,才叫真的不再覆盖。

二、字幕慢一轮:把推送时机挂在"开始说话"上是错的

方言数字人要把普通话译文当字幕打出来。旧实现是在 TTS_BEGIN_SPEAKING 事件里推字幕,看着很合理——"开始说了,就该出字幕"。

但此刻本轮的方言译文还没翻完,代码读到的是上一轮的 _mandarin_result,却给它打上本轮的 seq。结果字幕永远慢一句,而且因为 seq 是对的,日志里看不出任何异常。

修法(v16):译文由翻译回调在译完那一刻就推(_emit_subtitle),推到之前校验"翻的这句"仍然是"正在说的这句"(比对 _mandarin_src_text,不一致就丢弃 stale),seq 取推送当时的 _tts_seq。

这条经验可以推广:任何"给异步结果打同步序号"的地方,都要在推送时校验源是否还是那个源,否则你得到的是"序号正确、内容错位",比直接报错难查十倍。

三、质量分级不是"过滤噪声",是区分"三种不同的坏"

很多人做语料清洗是"按关键词删"。我们最后落成的是三档分级,因为这三种坏的性质完全不同:

分级判定处置
noise空识别 / 单字 / 纯语气词 / 重复字串 / 与上一条完全相同且过短不落盘音频(连音频都不留,省空间也省误用)
suspect助手回了"没听懂"兜底话术,或复述用户原话后反问降级到 review,人工复核
ok正常对话进 train

实测比例值得警惕:2026-09-02 当天 222 行里,助手"没听懂"兜底命中约 60 行(28%),其中 40 次是同一句。根因不是 ASR 差,而是麦克风音频无门槛直送云端 RTC——唤醒词闸门被旁路,旁边人聊天、电视声全被当成对小苏说话。

兜底话术必须做成一项可枚举的规则表,而不是靠"感觉像敷衍"。我们最终收进去的模式包括:倷个问题有点模糊、讲清爽点、倷讲啥、倷要讲啥、我听勿清爽、格倒蛮快 等。

然后是三份派生集,原始文件一行不动:

注意那条"旧 schema 一律不进 train":旧 schema 即使质量标 ok 也只能进 review,因为它的音频是"最后写入者胜"的启发式信任,而历史上出现过 turn 号复用。

四、落盘率暴跌时,先查采集侧,别查同步链路

这是最容易被误判的一类事故。我们有一段时期"每天有人说话、几乎不落盘",每次都有人去查同步脚本、查磁盘、查权限——全是对的,问题在麦克风。

判据不是看 eos(用户说完一句)有多少,而是看前端 cancel 与 eos 的比例:

日期eos(说话)cancel(判空录音)比例落盘
09-12(健康)3748762%432
09-18(异常)13011518117%4
09-27(异常)1750145383%3
判据要写成比例,不能写成 "cancel > eos"。09-27 就是 83%(低于 100%),落盘率却同样被打到 3/1750。健康日约 2%,异常日超过 50%,这才是可用的线。

两个同源旁证:09-27 当天 ASR 耗时 日志只有 2 次(绝大多数句子在 ffmpeg 解码前就死了),而 head=43c38100(非 EBML 首块,即残块)刷了 8168 条。

根因链条是这样的:前端 VAD 用"说话音量 rms vs 自适应底噪"的比值判定,阈值 1.5 倍。某台展示机麦克风增益偏低、环境底噪偏高,实测 rms=0.00266 底噪=0.00975,比值过不了 1.5 → 每句都判空录音并发 cancel。而前端 cancel 时不发 eos、不发 cancel-ack,已发出的残块滞留在服务端,下一句开头就变成非 EBML 头 → ffmpeg 解码失败 → 整句作废。所以 cancel 不只丢当前句,还会毒化下一句。

还有一个更隐蔽的分支:过了 VAD 但没过唤醒词。09-20 实测 eos 8 次 → ASR 8 次 → 只有 2 句出文本 → 2 句全被"非唤醒丢弃" → 0 次回复、0 落盘。这时 cancel 是 0,看起来一切正常。

所以判定要三态,而不是两态:

grep -a '收到 eos'      fullbody_dh.log   # 用户说了话
grep -a '\[回复\]'      fullbody_dh.log   # 助手真回了(对话已发生)
grep -a '\[听-非唤醒丢弃\]' fullbody_dh.log   # 没喊唤醒词,句子没进对话

只有「[回复] > 0 且 落盘 == 0」才是落盘链路故障。
「eos > 0 且 [回复] == 0」是门控/采集问题,改同步脚本一辈子也补不回来。

五、主动产线和被动记录是两条路,混在一起必然烧额度

做方言数据有两条完全不同的产线,产物落在不同目录、有不同的故障模式。我们早期把它们当成一件事讨论,浪费了很多时间:

被动记录(真人对话)主动产线(批量合成)
触发真人跟数字人说话才记不需要人,自己造文本自己合成
内容来源真实用户语音大模型现编方言文本 + 云端 TTS
日量级几十~几百行300+ 请求 / 2~3G 音频
主要风险音频错位、VAD 误判额度被吃光、文本方言不纯

额度这条线必须硬止于 0 点。在线语音合成额度是账号级 500 次/天、0 点重置,而合成产线和线上数字人共用同一个 APPID。我们曾经把窗口设成跨零点(22-08),结果某天产线 318 次 + 数字人 186 次 = 504/500,直接把当天额度打光,白天数字人"掉普通话"——用户听到的是一台突然不会说方言的机器,而日志里没有任何"错误"。

现在窗口固定 22:00~23:59,另留 TTS_RESERVE = 20 次给夜间应急,0 点整收工。验证方式是三口径互证:manifest 里的 ts 按小时分桶、当日计数器差值、以及日志里 00:0x 轮次是否变成"窗口外"。只要次日 manifest 的记录数为 0,才算真的硬止住了。

并发这里有个反直觉的点:TTS 按次数计费,不限并发(和按并发数计费的语音识别不同)。窗口只有 2 小时,单流约 55~75 秒/次,8 路并发实测 4 路 8 次全成功 0 失败。所以该调的是并发,不是窗口。

六、最贵的一次教训:改一个"切分字数",让 585 条成品全部重做

合成产线按 job_id = sha1(切分后的文本) 做去重。这句话本身没问题,问题是切分参数决定切分点,切分点决定 job_id。

于是当有人"顺手把切分字数改回默认值"时,所有历史 job_id 全部失配,已完成去重集清零,585 条已合成好的音频会被重新合成一遍——按每天 500 次额度算,等于白烧两天。

更坑的是取值链:

位置值是否生效
脚本里 --target-chars 的 argparse 默认值1800❌ 出厂默认,从未生效
启动脚本 loop_synth.sh 第 23 行的实际传参2500✅ 真正决定切分的就是这里

铁证是产物分布:585 条成品的字数中位数 2487、最大 2500。如果只看脚本默认值,会得出"1800 才是默认"的结论,然后"改回默认"——那正好是把全部成品废掉的操作。

可迁移的规则:凡是用内容哈希做去重的流水线,任何影响切分/预处理的参数都必须视为不可变配置,改动前要同步迁移哈希映射表,并先备份 manifest。允许改的只有窗口、并发、限流这类"不影响输出内容"的参数。

七、方言文本生成:9B 裸产出只有 20% 能用

主动产线要文本,一开始调线上大模型,每天约 80 万字、成本约 10 元/天。后来切到本地 9B(零成本、也更符合"最终全本地"的方向),立刻遇到质量问题:

拿 10 条真实选题实测,裸产出过纯度门只有 2/10(20%);加一层确定性归一化后升到 80%;补齐 咋/咱 这类北方词后接近 100%。

而且泄漏的方言远不止"上海话"一种。实测被拦下的标记包括:

归一化表有个必须注意的实现细节:长词必须排在短词前面(先把 侬笃 换掉,再处理 侬;先 咋样 再 咋),否则会被短词先吃掉,留下半截。

最后一定要有一道"拒收"而不是"修补"的网:归一化之后仍然带外方言的,直接不入库、重生成,绝不放进语料。修补出来的"近似方言"比没有更糟——它会污染后续所有训练。

另外一个反直觉发现:线上大模型也在漏上海话。同一套日志里,线上链路同样有 banned:勒海,伐、推板 被 skip 的记录。所以"方言纯度门"不是小模型的补丁,而是任何方言语料生产的必备件。

八、我们现在的最小可行做法(可直接抄)

  1. 命名空间对齐唯一性域:音频文件名带日期,或按日分子目录,二者取一,永远不要扁平 + 按日重数。
  2. 审计字段不能代替审计:audio_trusted: true 是生成时刻的判断,不保证之后没被覆盖;每次同步后重跑一次覆盖检测。
  3. 原始文件只增不改,所有清洗结果写派生集;派生集可随时重算,原始集不可重建。
  4. 先判"哪一侧坏了"再动手:采集侧(VAD/唤醒词)与链路侧(同步/落盘)的修法完全相反,判据是"回复数"与"落盘数"的组合,不是 eos 数。
  5. 额度类资源必须硬止于重置点,并留 reserve;跨零点窗口是最典型的自伤。
  6. 内容哈希去重 + 可变预处理参数 = 定时炸弹,把切分参数当版本号管理。

如果你正在做方言/小语种语料,最省时间的顺序其实是倒过来的:先把「唯一性 + 命名 + 去重键」这三件事钉死,再去调模型和降噪。这三件事错了,后面所有努力都会在某一天被静默清零。

常见问题

方言语音语料库至少需要多少条才够训练 TTS?

没有通用数字,但有个更实用的判断:先看"可信配对"的数量,而不是文件总数。我们的 3129 个音频里,真正能进 train(音频与文本严格配对、质量 ok、助手真听懂)的比例远低于总量。按我们的经验,单日几百条落盘里最终只有几十条进 train 是正常的——质量优先,别用总数自欺。

为什么不能直接用"最后写入者胜"来信任音频文件?

因为覆盖是静默的。文件名相同、写入成功、无任何报错,唯一留下痕迹的是 mtime。当你的目录是扁平的、turn 号每天重数时,"最后写入者"往往不是这条文本对应的那次录音。唯一的根治是让文件名或目录结构本身携带日期。

方言 TTS 合成,为什么额度会在半夜被吃光?

通常是把批量合成的运行窗口设成跨零点(例如 22 点到次日 8 点)。额度按自然日 0 点重置,跨零点意味着前半段吃当天、后半段吃次日,白天线上服务就没额度了。表现是"数字人突然不说方言了",而日志里没有错误。正确做法是窗口硬止于 23:59。

大模型生成的方言文本能直接进语料吗?

不能。实测未经归一化的裸产出合格率只有 20%,且泄漏不限于邻近方言(吴语、粤语、西南、北方词、日文字形都会混进来)。必须做"确定性替换 + 外方言拒收"两道闸,且长词替换要排在短词之前。线上大模型同样会漏,别以为换大模型就不用做纯度门。

没有真人对话,能建方言语音库吗?

能,但要分清两条路的用途:合成产线可以快速堆量、覆盖发音组合,但缺少真实对话的韵律与口语化表达;真人对话量小、噪声多,却是最接近真实使用分布的数据。我们的做法是两条并行:合成补覆盖,真人补真实。

如果你需要现成的苏州话词库(3550 条,含拼音与释义),可以直接看 数字商品页;需要按项目定制词表或语料清洗,走 服务页。

查看苏州话词库(含 100 条免费样本)