不熟悉语音唤醒的人看此文前 可以先了解一下语音唤醒的一些基本情况发展现状,评价标准等,以免偏颇。
最近做了一个windows下的语音唤醒功能。一些情况记录如下:
之前想过使用百度或科大讯飞的唤醒功能,但百度的目前不支持windows,讯飞使用的是c++.. 后来搜到windows下可以使用mycroft-precise做语音唤醒功能 就拿来试试了。
1, 首先奇怪的是,知乎网友说 windows下可以使用mycroft-precise,但git-hub页面上的介绍: Precise is designed to run on Linux. It is known to work on a variety of Linux distributions including Debian, Ubuntu and Raspbian. It probably operates on other *nx distributions. 看起来不支持windows。
那windows下 到底能否玩起来呢? 我先下了源码测试和看看 发现至少如下两点有影响:
如果只使用唤醒功能,windows下是可以玩起来的,我现在就跑起来了。例如可以下载已经训练好的hey-mycroft.pb 试试。 个人试的 使用默认和修改后的控制参数的值 能唤醒但成功率都不太高,也就是假负比较高。(也可能是我发音不标准)
(1) 但是训练模型需要收集语音,如果使用 precise-collect, 在windows下会有问题。因为collect 需要 from termios import tcsetattr, tcgetattr, TCSADRAIN 这句话在linux下无需特别安装什么就能执行成功,但windows下pip怎么也找不到termios这个包。 后来才了解到 termios与串口开发/POSIX规范相关 win可以用Cygwin代替。不过我这里 暂时就没追着修改测试这个了,用了另一种方法: 普通录音机程序 录得.m4a文件,再使用ffmpeg转成 precise要求的16000采样频率等要求的格式。 ffmpeg 我去年因为magenta折腾过,现在有点connect the dots的感觉,嘿嘿。
(2) precise-listen 时, on_activation的调用函数 会调用 site-packages\precise\util.py 里的activate_notify 这里的 play_audio 在win下可能会失败。 linux应该也要装东西才OK。 在window下要做修改才能跑通。
我最终的代码 继承了PreciseRunner类, 调用的on_activation的函数是自己写的,不会有这个问题。
2,关于训练的语音来源(以下唤醒词以"小律小律"为例)
由于我手上的linux机器都没有声卡,不能试用 precise-collect,就用windows的录音机采集 wake-word 和 not-wake-word。
根据官网的提示,在录wake-word 时,前面都空了一两秒钟,后面立即结束。一般一条记录两三秒。
录not-wake-word时,官网的提示没强调这个,我就随便录了几条几秒的记录,后来某些记录都录了1分钟左右或更长了。
折腾训练几天后, 再看到源码中这句话, 崩溃了五分钟:
if len(audio) > pr.max_samples:
audio = audio[-pr.max_samples:]
用于训练的语音长度 实际只取了后面最多 pr.max_samples长度,而这个的默认值,是buffer_t * sample_rate = 1.5 * 16000 等于1.5秒时长。 所有的wake-word not-wake-word 都只取了后1.5时长... 对我的wake-word 影响小点,而not-wake-word 很多录的都没用上,导致训练得不够..
意识到这之后, 我又开始拿起goldwave, 更精确的处理 录好转换后的音频了。(回想人生 再次connect the dots) 因为两三秒的东西,windows的播放器里看不清楚的,用goldwave或其他音频处理工具, 能准确的控制前后的空白部分,剪成预期想要的样子(总长1.5s以内 前面需要留白的留白)。用这样处理后的数据训练 效果提升了一些些。 (其实刚开始时效果不明显,当我发现train_data代码中的cache后 再崩溃了五分钟..其实之前也发现了本地有个.cache文件夹 但竟然没点进去看没联想到一起..)
我想着 wake-word 前面的空白有啥用呢 是不是去掉空白直接用"小律小律"部分,效果会更好呢? 实际效果掉得很厉害,很难识别成功。 后来想想 可能是因为 真正有意愿说唤醒词小律小律之前 一般会有一点空白,而一段话中的小律小律,不期望被用来唤醒?
3,调整真正/真负偏向之间的控制参数 有两个:trigger_level=3, sensitivity=0.5 trigger_level 这是一个什么参数? 刚开始我也花了好些时间在上面,现在 便于理解的说 (并不是大白话说),就是如果把这个值调小, 在说"小律小" 或"小律" 或"小"字时,(不需要等"小律小律"全部说完), 就可能激活了。
4,发现相同或相似的韵母 很容易被误唤醒。 比如 小聚小聚 小旭小旭 小玉小玉 小拘小拘 小菊小菊 小举小举 等等。 我下了百度的demo apk,用小度小度唤醒时,说小雾小雾 小律小律 小都小都 小读小读 小赌小赌等 也很容易被误唤醒。 搜了一下,不止我一个人发现,还有人说 每次都用傻度傻度唤醒.. 当然了 我的傻律 效果还更差一点..
关于第3 和4 这里我们多看几层源码吧:
(1):关键的 TriggerDetector.update代码 在不知道实际处理逻辑时有点难以理解。
TriggerDetector.activation 可以理解为 chunk连续激活(激活趋势为连续升)次数。 chunk来自于流,一直在更新,如果连续被激活超过trigger_level次, 就认为唤醒了。
chunk_activated 为false时, 如果activation > 0 就要自减1, 这是因为要达到的效果是:激活趋势为连续升。 如果前面的chunk激活了,后面一个chunk又没有激活,则这个连续激活 要减1次。
self.activation = -(8 * 2048) // self.chunk_size
分母的chunk_size 默认值为Engine.chunk_size 2048: Higher numbers decrease CPU usage but increase latency / Higher values are less computationally expensive 数字越大越省cpu但会变慢.
分子 为什么是(8 * 2048)呢?.. 好吧 写本文时 再拾起以前的疑问..可能是如下依据:https://wenjie.store/archives/about-bytebuf-3 :chunk划分为2048个Page,每个Page大小为8kb,Page是给ByteBuf分配内存的最小调度单位 ..(不懂java 逃 不确定用在这里是否正确,这个文章里的chunk比precise里的chunk 看起来要大 page对chunk?.. 欢迎知道的大侠指导指正)
如果认为已经唤醒(has_activated为True) 或本chunk激活且最近曾经唤醒过(chunk_activated and self.activation < 0) 则保持目前的唤醒状态,不会再次唤醒 self.activation计算出来为-8(后续的chunk未激活会递增至0),可以理解为 前8个chunk 都是唤醒的激活的 无需/不能再次唤醒
def update(self, prob):
# type: (float) -> bool
"""Returns whether the new prediction caused an activation"""
chunk_activated = prob > 1.0 - self.sensitivity
# print(" ------------- ", self.sensitivity, round(prob, 3), chunk_activated, self.activation) # 这句打印帮你看得更清 再加个 has_activated后的打印
if chunk_activated or self.activation < 0:
self.activation += 1
has_activated = self.activation > self.trigger_level
if has_activated or chunk_activated and self.activation < 0:
self.activation = -(8 * 2048) // self.chunk_size
if has_activated:
return True
elif self.activation > 0:
self.activation -= 1
return False
(2):为啥相同的韵母 模型会分不太清? go~ 去训练模型的代码看看:
TrainData.from_folder 是个classmethod, 从find_wavs函数中得到wav文件 没啥特殊的, data.load 里,vectorizer 函数参数是关键。 它使用归一化后的wav转np.array文件, 用 Vectorizer.mfccs 处理得到特征。
vectorizers = {
Vectorizer.mels: lambda x: mel_spec(
x, pr.sample_rate, (pr.window_samples, pr.hop_samples),
num_filt=pr.n_filt, fft_size=pr.n_fft
),
Vectorizer.mfccs: lambda x: mfcc_spec(
x, pr.sample_rate, (pr.window_samples, pr.hop_samples),
num_filt=pr.n_filt, fft_size=pr.n_fft, num_coeffs=pr.n_mfcc
),
Vectorizer.speechpy_mfccs: lambda x: __import__('speechpy').feature.mfcc(
x, pr.sample_rate, pr.window_t, pr.hop_t, pr.n_mfcc, pr.n_filt, pr.n_fft
)
}
而 mfcc_spec的代码,跟网上很多介绍计算mfcc的步骤基本都是能对应上的。(现学现炒..)
def mfcc_spec(audio, sample_rate, window_stride=(160, 80),
fft_size=512, num_filt=20, num_coeffs=13, return_parts=False):
"""Calculates mel frequency cepstrum coefficient spectrogram"""
powers = power_spec(audio, window_stride, fft_size)
if powers.size == 0:
return np.empty((0, min(num_filt, num_coeffs)))
filters = filterbanks(sample_rate, num_filt, powers.shape[1])
mels = safe_log(np.dot(powers, filters.T)) # Mel energies (condensed spectrogram)
mfccs = dct(mels, norm='ortho')[:, :num_coeffs] # machine readable spectrogram
mfccs[:, 0] = safe_log(np.sum(powers, 1)) # Replace first band with log energies
if return_parts:
return powers, filters, mels, mfccs
else:
return mfccs
看样子 对于不同的样本,filterbanks的结果 大体是一样的 会造成mels的值 不同的的原因 主要在powers上,也就是power_spec的计算: np.fft.rfft 离散傅立叶变换,再计算得到的复数的实部虚部平方和除以fft_size。
def chop_array(arr, window_size, hop_size):
"""chop_array([1,2,3], 2, 1) -> [[1,2], [2,3]]"""
return [arr[i - window_size:i] for i in range(window_size, len(arr) + 1, hop_size)]
def power_spec(audio: np.ndarray, window_stride=(160, 80), fft_size=512):
"""Calculates power spectrogram"""
frames = chop_array(audio, *window_stride) or np.empty((0, window_stride[0]))
fft = np.fft.rfft(frames, n=fft_size)
return (fft.real ** 2 + fft.imag ** 2) / fft_size
好吧 到这里我还是没找到 为什么/怎样改进 对相似韵母的识别情况.. 百度都没能改进的,哪能这么快被我找到,呵呵,不过将来不保证 哈哈
5, 另外的,项目中激活后需要实现说话录音功能。语音检测要用VAD技术。python有个库 webrtcvad可以用。当我找到这些例子,又再觉得python真友好,也感谢别人的分享,不然我自己很难/几乎不可能折腾出来。当然如果有国产语言 也对程序员友好生态又好 就太好啦。
关键的判断 active = vad.is_speech(chunk, default_rate) C源码我还没学习 原理可参考 https://www.cnblogs.com/dylancao/p/7663755.html
webrtc的vad检测原理是根据人声的频谱范围,把输入的频谱分成六个子带(80Hz~250Hz,250Hz~500Hz,500Hz~1K,1K~2K,2K~3K,3K~4K。)分别计算这六个子带的能量。然后使用 高斯模型的概率密度函数做运算,得出一个 对数似然比函数。对数似然比 分为全局和局部,全局是六个子带之加权之和,而局部是指每一个子带则是局部,所以语音判决会先判断子带,子带判断没有时会判断全局,只要有一方过了,就算有语音。
写这些的过程,又多自问自答了一些问题,还是挺有收获。 语音方面我基础薄弱,上面的理解很可能有错误,欢迎指正,谢谢!