拼音词组匹配替换

Hint

For English-speaking users, this page is for users using Chinese ASR models.

本文描述如何使用预定义的拼音规则,对匹配规则的汉字,进行替换。

Hint

本文所描述的方法,与 hotwrods (热词)是不同的技术。它们互相独立、毫不相关。

使用场景

举个例子,如果一个人说了下面 4 个字:

xuán jiè xīn piàn

我们如何让模型输出 玄戒芯片, 而不是 玄界芯片,或者 悬界心骗 呢?

又举个例子,如果某人 fh 分不清,把 湖南人 说成了:

 nán rén

我们如何让模型输出 湖南人 呢?

又双叒叕举一个例子,对于一些专有名词,比如,弓头安装机载传感器 等,如何准确的识别出来, 而不是识别成 公投安装基载传感器 等词组呢?

使用限制

只支持对汉字进行替换。

只支持对汉字进行替换。

只支持对汉字进行替换。

Hint

重要的事情,重复三遍。

Note

目前没计划实现对非汉字的字符进行替换。

支持 sherpa-onnx 里面,所有能输出中文的语音识别模型。不管是流式还是非流式, 不管采用何种解码方法,都支持。例如,非流式的 SenseVoice,流式的 Zipformer 等等。

Hint

输出中英文的模型,也支持。但只能替换识别结果中的中文。非中文字符,会原样保留,不做任何替换。

规则文件,需要提前生成好。目前不支持动态修改规则文件。

使用方法

需要用到3个文件。

文件名

说明

下载地址

dict.tar.bz2

用于jieba 分词
通用
请解压,得到 dict 文件夹

https://github.com/k2-fsa/sherpa-onnx/releases/tag/hr-files

lexicon.txt

用于汉字转拼音
通用

https://github.com/k2-fsa/sherpa-onnx/releases/tag/hr-files

replace.fst

不通用
用户自己提供

如何生成,请见本文后半部分

生成replace.fst

接下来,我们描述如何生成 replace.fst

为了方便用户输入规则,我们不采用 xuán jiè xīn piàn, 而是使用 xuan2 jie4 xin1 pian4 。即用 1,2,3,4 来代替第一声、第二声、第三声和第四声。

Hint

如果是轻声,请用第一声代替。比如 ma1 ma1

首先,我们需要安装 pynini

Hint

对于 Linux 用户,请使用:

pip install --only-binary :all: pynini

对于非 Linux 用户,请找一台 Linux 系统的电脑,变成 Linux 用户。

任何有关 pynini 的安装问题,请去 https://github.com/kylebgorman/pynini/issues 提问。

友情提示:我们还提供一个 colab ,供在线生成规则文件。

安装好 pynini 后,我们用下面的代码,生成针对本文开始部分提及的3个问题的规则文件:

import pynini
from pynini import cdrewrite
from pynini.lib import byte, utf8

sigma = utf8.VALID_UTF8_CHAR.star

rule1 = pynini.cross("xuan2jie4xin1pian4", "玄戒芯片")

# 针对前鼻音和后鼻音不分的情况
#
# 注意:可以指定多个规则,都替换成同一个词组
rule2 = pynini.cross("xuan2jie4xing1pian4", "玄戒芯片")

rule3 = pynini.cross("fu2nan2ren2", "湖南人")

rule4 = pynini.cross("gong1tou2an1zhuang1", "弓头安装")

rule5 = pynini.cross("ji1zai3chuan2gan3qi4", "机载传感器")

# 可以指定多个规则,覆盖可能的发音
rule6 = pynini.cross("ji1zai4chuan2gan3qi4", "机载传感器")

# 本例子只有6条规则。你可以添加任意多条规则。
rule = (rule1 | rule2 | rule3 | rule4 | rule5 | rule6).optimize()
rule = cdrewrite(rule, "", "", sigma)

rule.write("replace.fst")

测试

我们把生成的 replace.fst 重名为了 hr-xuan-jie-replace.fst 。 你可以在 https://github.com/k2-fsa/sherpa-onnx/releases/tag/hr-files 下载它。在同一个页面,你可以下载下面的测试音频 hr-xuan-jie-test.wav

Wave filename Content
hr-xuan-jie-test.wav

下面我们使用 SenseVoice 模型 去识别这个音频。分两种情况:

    1. 不使用本文的方法

    1. 使用本文的方法

请大家注意比较两种方法识别的结果。

(1) 不使用本文的方法

./build/bin/sherpa-onnx-offline \
  --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt \
  --sense-voice-model=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx \
  --debug=0 \
  ./hr-xuan-jie-test.wav

输出的 log 如下:

/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/parse-options.cc:Read:372 ./build/bin/sherpa-onnx-offline --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt --sense-voice-model=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx --debug=0 ./hr-xuan-jie-test.wav 

OfflineRecognizerConfig(feat_config=FeatureExtractorConfig(sampling_rate=16000, feature_dim=80, low_freq=20, high_freq=-400, dither=0, normalize_samples=True, snip_edges=False), model_config=OfflineModelConfig(transducer=OfflineTransducerModelConfig(encoder_filename="", decoder_filename="", joiner_filename=""), paraformer=OfflineParaformerModelConfig(model=""), nemo_ctc=OfflineNemoEncDecCtcModelConfig(model=""), whisper=OfflineWhisperModelConfig(encoder="", decoder="", language="", task="transcribe", tail_paddings=-1), fire_red_asr=OfflineFireRedAsrModelConfig(encoder="", decoder=""), tdnn=OfflineTdnnModelConfig(model=""), zipformer_ctc=OfflineZipformerCtcModelConfig(model=""), wenet_ctc=OfflineWenetCtcModelConfig(model=""), sense_voice=OfflineSenseVoiceModelConfig(model="./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx", language="auto", use_itn=False), moonshine=OfflineMoonshineModelConfig(preprocessor="", encoder="", uncached_decoder="", cached_decoder=""), dolphin=OfflineDolphinModelConfig(model=""), telespeech_ctc="", tokens="./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt", num_threads=2, debug=False, provider="cpu", model_type="", modeling_unit="cjkchar", bpe_vocab=""), lm_config=OfflineLMConfig(model="", scale=0.5), ctc_fst_decoder_config=OfflineCtcFstDecoderConfig(graph="", max_active=3000), decoding_method="greedy_search", max_active_paths=4, hotwords_file="", hotwords_score=1.5, blank_penalty=0, rule_fsts="", rule_fars="", hr=HomophoneReplacerConfig(dict_dir="", lexicon="", rule_fsts=""))
Creating recognizer ...
Started
Done!

./hr-xuan-jie-test.wav
{"lang": "<|zh|>", "emotion": "<|NEUTRAL|>", "event": "<|Speech|>", "text": "下面是一个测试悬界芯片湖南人工投安装基载传感器", "timestamps": [0.60, 0.84, 1.08, 1.26, 1.38, 1.56, 1.80, 2.88, 3.00, 3.48, 3.66, 5.34, 5.46, 5.64, 7.14, 7.26, 7.62, 7.80, 9.24, 9.42, 9.78, 9.90, 10.14], "tokens":["下", "面", "是", "一", "个", "测", "试", "悬", "界", "芯", "片", "湖", "南", "人", "工", "投", "安", "装", "基", "载", "传", "感", "器"], "words": []}
----
num threads: 2
decoding method: greedy_search
Elapsed seconds: 0.629 s
Real time factor (RTF): 0.629 / 11.673 = 0.054

(2) 使用本文的方法

./build/bin/sherpa-onnx-offline \
  --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt \
  --sense-voice-model=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx \
  --debug=0 \
  --hr-lexicon=./lexicon.txt \
  --hr-dict-dir=./dict \
  --hr-rule-fsts=./hr-xuan-jie-replace.fst \
  ./hr-xuan-jie-test.wav

Hint

上述命令行工具,指定了3个参数:

  • --hr-lexicon: 通用。下载地址

  • --hr-dict-dir: 通用。下载地址

  • --hr-rule-fsts: 为我们自己生成的规则文件。不通用。

如果你是通过调用 API 的方式使用,请设置 OfflineRecongizerConfig 或者 OnlineRecognizerConfig 里面的成员 hr

输出的 log 如下:

/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/parse-options.cc:Read:372 ./build/bin/sherpa-onnx-offline --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt --sense-voice-model=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx --debug=0 --hr-lexicon=./lexicon.txt --hr-dict-dir=./dict --hr-rule-fsts=./hr-xuan-jie-replace.fst ./hr-xuan-jie-test.wav 

OfflineRecognizerConfig(feat_config=FeatureExtractorConfig(sampling_rate=16000, feature_dim=80, low_freq=20, high_freq=-400, dither=0, normalize_samples=True, snip_edges=False), model_config=OfflineModelConfig(transducer=OfflineTransducerModelConfig(encoder_filename="", decoder_filename="", joiner_filename=""), paraformer=OfflineParaformerModelConfig(model=""), nemo_ctc=OfflineNemoEncDecCtcModelConfig(model=""), whisper=OfflineWhisperModelConfig(encoder="", decoder="", language="", task="transcribe", tail_paddings=-1), fire_red_asr=OfflineFireRedAsrModelConfig(encoder="", decoder=""), tdnn=OfflineTdnnModelConfig(model=""), zipformer_ctc=OfflineZipformerCtcModelConfig(model=""), wenet_ctc=OfflineWenetCtcModelConfig(model=""), sense_voice=OfflineSenseVoiceModelConfig(model="./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx", language="auto", use_itn=False), moonshine=OfflineMoonshineModelConfig(preprocessor="", encoder="", uncached_decoder="", cached_decoder=""), dolphin=OfflineDolphinModelConfig(model=""), telespeech_ctc="", tokens="./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt", num_threads=2, debug=False, provider="cpu", model_type="", modeling_unit="cjkchar", bpe_vocab=""), lm_config=OfflineLMConfig(model="", scale=0.5), ctc_fst_decoder_config=OfflineCtcFstDecoderConfig(graph="", max_active=3000), decoding_method="greedy_search", max_active_paths=4, hotwords_file="", hotwords_score=1.5, blank_penalty=0, rule_fsts="", rule_fars="", hr=HomophoneReplacerConfig(dict_dir="./dict", lexicon="./lexicon.txt", rule_fsts="./hr-xuan-jie-replace.fst"))
Creating recognizer ...
Started
Done!

./hr-xuan-jie-test.wav
{"lang": "<|zh|>", "emotion": "<|NEUTRAL|>", "event": "<|Speech|>", "text": "下面是一个测试玄戒芯片湖南人弓头安装机载传感器", "timestamps": [0.60, 0.84, 1.08, 1.26, 1.38, 1.56, 1.80, 2.88, 3.00, 3.48, 3.66, 5.34, 5.46, 5.64, 7.14, 7.26, 7.62, 7.80, 9.24, 9.42, 9.78, 9.90, 10.14], "tokens":["下", "面", "是", "一", "个", "测", "试", "悬", "界", "芯", "片", "湖", "南", "人", "工", "投", "安", "装", "基", "载", "传", "感", "器"], "words": []}
----
num threads: 2
decoding method: greedy_search
Elapsed seconds: 0.692 s
Real time factor (RTF): 0.692 / 11.673 = 0.059

(3) 结果比较

方法

识别结果

不用本文的方法

下面是一个测试 悬界芯片 湖南人 工投安装 基载传感器

用本文的方法

下面是一个测试 玄戒芯片 湖南人 弓头安装 机载传感器

调试

./build/bin/sherpa-onnx-offline \
  --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt \
  --sense-voice-model=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx \
  --debug=1 \
  --hr-lexicon=./lexicon.txt \
  --hr-dict-dir=./dict \
  --hr-rule-fsts=./hr-xuan-jie-replace.fst \
  ./hr-xuan-jie-test.wav

设置 --deubg=1 可以输出如下调试信息

Started
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:157 Input text: '下面是一个测试悬界芯片湖南人工投安装基载传感器'
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:165 After jieba: 下面_是_一个_测试_悬界_芯片_湖南_人工_投_安装_基载_传感器
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 下面 xia4mian4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 是 shi4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 一个 yi2ge4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 测试 ce4shi4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 悬界 xuan2jie4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 芯片 xin1pian4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 湖南 hu2nan2
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 人工 ren2gong1
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 投 tou2
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 安装 an1zhuang1
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 基载 ji1zai4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:186 传感器 chuan2gan3qi4
/Users/fangjun/open-source/sherpa-onnx/sherpa-onnx/csrc/homophone-replacer.cc:Apply:198 Output text: '下面是一个测试玄戒芯片湖南人头安装机载传感器'
Done!

./hr-xuan-jie-test.wav
{"lang": "<|zh|>", "emotion": "<|NEUTRAL|>", "event": "<|Speech|>", "text": "下面是一个测试玄戒芯片湖南人弓头安装机载传感器", "timestamps": [0.60, 0.84, 1.08, 1.26, 1.38, 1.56, 1.80, 2.88, 3.00, 3.48, 3.66, 5.34, 5.46, 5.64, 7.14, 7.26, 7.62, 7.80, 9.24, 9.42, 9.78, 9.90, 10.14], "tokens":["下", "面", "是", "一", "个", "测", "试", "悬", "界", "芯", "片", "湖", "南", "人", "工", "投", "安", "装", "基", "载", "传", "感", "器"], "words": []}
----
num threads: 2
decoding method: greedy_search
Elapsed seconds: 0.583 s
Real time factor (RTF): 0.583 / 11.673 = 0.050

大家可以根据调试信息中的拼音,去调整对应的规则。对于一条规则,只有它其中的拼音全部匹配上时,才会替换。 比如 xuan2jie4xin1pian4 这条规则,无法匹配 玄界 或者 新片 。只有 玄界新片 一起出现时, 才会匹配成功,替换成 玄戒芯片

Warning

对于上面的例子 玄界新片 , 如果没有替换成 玄戒芯片 ,那么需要看调试信息中, 这个字的发音,是 pian1 还是 pian4 。如果是 pian1, 那么,你还需要加一条规则, 把 xuan2jie4xin1pian1 变成 玄戒芯片 。 添加的规则如下:

rule7 = pynini.cross("xuan2jie4xin1pian1", "玄戒芯片")

视频演示

B 站视频演示如下: