拼音词组匹配替换

Hint

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

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

Hint

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

使用场景

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

xuán jiè xīn piàn

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

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

fú nán rén

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

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

使用限制

只支持对汉字进行替换。

只支持对汉字进行替换。

只支持对汉字进行替换。

Hint

重要的事情,重复三遍。

Note

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

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

Hint

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

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

使用方法

需要用到2个文件。

文件名

说明

下载地址

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-rule-fsts=./hr-xuan-jie-replace.fst \
  ./hr-xuan-jie-test.wav

Hint

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

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

  • --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-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="", 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-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 站视频演示如下: