整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用TensorFlow进行语音识别.js – 语音命令

我还是个孩子的时候,几乎每个超级英雄都有一台语音控制的计算机。所以你可以想象我第一次遇到Alexa对我来说是一次深刻的经历。我心里的孩子非常高兴和兴奋。当然,然后我的工程直觉开始发挥作用,我分析了这些设备是如何工作的。

事实证明,他们有神经网络来处理这个复杂的问题。事实上,神经网络大大简化了这个问题,以至于今天使用Python在计算机上制作这些应用程序之一非常容易。但情况并非总是如此。第一次尝试是在 1952 年进行的。由三位贝尔实验室研究人员撰写。

他们建立了一个具有10个单词词汇的单扬声器数字识别系统。然而,到1980年代,这一数字急剧增长。词汇量增长到20,000个单词,第一批商业产品开始出现。Dragon Dictate是首批此类产品之一,最初售价为9,000美元。Alexa今天更实惠,对吧?

但是,今天我们可以在浏览器中使用Tensorflo.js执行语音识别。在本文中,我们将介绍:

  1. 迁移学习
  2. 语音识别如何工作?
  3. 演示
  4. 使用Tensorflow实现.js

1. 迁移学习

从历史上看,图像分类是普及深度神经网络的问题,尤其是视觉类型的神经网络——卷积神经网络(CNN)。今天,迁移学习用于其他类型的机器学习任务,如NLP和语音识别。我们不会详细介绍什么是 CNN 以及它们是如何工作的。然而,我们可以说CNN在2012年打破了ImageNet大规模视觉识别挑战赛(ILSVRC)的记录后得到了普及。

该竞赛评估大规模对象检测和图像分类的算法。他们提供的数据集包含 1000 个图像类别和超过 1 万张图像。图像分类算法的目标是正确预测对象属于哪个类。自2年以来。本次比赛的每位获胜者都使用了CNN。

训练深度神经网络可能具有计算性和耗时性。要获得真正好的结果,您需要大量的计算能力,这意味着大量的GPU,这意味着......嗯,很多钱。当然,您可以训练这些大型架构并在云环境中获得SOTA结果,但这也非常昂贵。

有一段时间,这些架构对普通开发人员不可用。然而,迁移学习的概念改变了这种情况。特别是,对于这个问题,我们今天正在解决 - 图像分类。今天,我们可以使用最先进的架构,这些架构在 ImageNet 竞赛中获胜,这要归功于迁移学习和预训练模型。

1.1 预训练模型

此时,人们可能会想知道“什么是预训练模型?从本质上讲,预训练模型是以前在大型数据集(例如 ImageNet 数据集)上训练的保存网络 。 有两种方法可以使用它们。您可以将其用作开箱即用的解决方案,也可以将其与迁移学习一起使用。 由于大型数据集通常用于某些全局解决方案,因此您可以自定义预先训练的模型并将其专门用于某些问题。

通过这种方式,您可以利用一些最著名的神经网络,而不会在训练上浪费太多时间和资源。此外,您还可以 通过修改所选图层的行为来微调这些模型。整个想法围绕着使用较低层的预训练CNN模型,并添加额外的层,这些层将为特定问题定制架构。

从本质上讲,严肃的迁移学习模型通常由两部分组成。我们称它们为骨干和头脑。 主干通常是在 ImageNet 数据集上预先训练的深度架构,没有顶层。Head 是图像分类模型的一部分,用于预测自定义类。

这些层将添加到预训练模型的顶部。有了这些系统,我们有两个阶段:瓶颈和培训阶段。在瓶颈阶段,特定数据集的图像通过主干架构运行,并存储结果。在训练阶段,来自主干的存储输出用于训练自定义层。

有几个领域适合使用预先训练的模型,语音识别就是其中之一。此模型称为语音命令识别器。从本质上讲,它是一个JavaScript模块,可以识别由简单英语单词组成的口语命令

默认词汇18w”包括以下单词:从“零”到“九”、“向上”、“向下”、“向左”、“向右”、“开始”、“停止”、“是”、“否”的数字。还提供其他类别的“未知单词”和“背景噪音”。除了已经提到的“18w”字典之外,还有更小的字典“directional4w”可用。它只包含四个方向词(“上”、“下”、“左”、“右”)。

2. 语音识别如何工作?

当涉及到神经网络和音频的组合时,有很多方法。语音通常使用某种递归神经网络或LSTM来处理。但是,语音命令识别器使用称为卷积神经网络的简单体系结构,用于小占用量关键字发现。

这种方法基于我们在上一篇文章中研究的图像识别和卷积神经网络。乍一看,这可能会令人困惑,因为音频是一个跨时间的一维连续信号,而不是 2D 空间问题。

2.1 谱图

此体系结构使用频谱图。这是信号频率频谱随时间变化的视觉表示。从本质上讲,定义了单词应该适合的时间窗口。

这是通过将音频信号样本分组到段来完成的。完成此操作后,将分析频率的强度,并定义具有可能单词的。然后将这些片段转换为频谱图,例如用于单词识别的单通道图像:

然后,使用这种预处理制作的图像被馈送到多层卷积神经网络中。

3. 演示

您可能已经注意到,此页面要求您允许使用麦克风。这是因为我们在此页面中嵌入了实现演示。为了使此演示正常工作,您必须允许它使用麦克风。

现在,您可以使用命令“向上”,“向下”,“向左”和“右”在下面的画布上绘制。继续尝试一下:

4. 使用TensorFlow实现.js

4.1 网页文件

首先,让我们看一下我们实现的 index.html 文件。在上一篇文章中,我们介绍了几种安装TensorFlow.js的方法。其中之一是将其集成到HTML文件的脚本标记中。这也是我们在这里的做法。除此之外,我们需要为预训练的模型添加一个额外的脚本标记。以下是索引.html的外观:

<html>
  <head>    
    <script src="https://unpkg.com/@tensorflow/tfjs@0.15.3/dist/tf.js"></script>
    <script src="https://unpkg.com/@tensorflow-models/speech-commands@0.3.0/dist/speech-commands.min.js"></script>
  </head>
  <body>
    <section class='title-area'>
        <h1>TensorFlow.js Speech Recognition</h1>
        <p class='subtitle'>Using pretrained models for speech recognition</p>
    </section>
    <canvas id="canvas" width="1000" height="800" style="border:1px solid #c3c3c3;"></canvas>
    <script src="script.js"></script>
  </body>
</html>

包含此实现的 JavaScript 代码位于 script.js 中。此文件应与 index.html 文件位于同一文件夹中。为了运行整个过程,您所要做的就是在浏览器中打开索引.html并允许它使用您的麦克风。

4.2 脚本文件

现在,让我们检查整个实现所在的 script.js 文件。以下是主运行函数的外观:

async function run() {
 recognizer = speechCommands.create('BROWSER_FFT', 'directional4w');
 await recognizer.ensureModelLoaded();

 var canvas = document.getElementById("canvas");
 var contex = canvas.getContext("2d");
 contex.lineWidth = 10;
 contex.lineJoin = 'round';
 
 var positionx = 400;
 var positiony = 500;

 predict(contex, positionx, positiony);
}

在这里我们可以看到应用程序的工作流程。首先,我们创建模型的实例并将其分配给全局变量识别器。我们使用“directional4w字典,因为我们只需要“up”,“down”,“left”和“right”命令。

然后我们等待模型加载完成。如果您的互联网连接速度较慢,这可能需要一些时间。完成后,我们初始化执行绘图画布。最后,调用预测方法。以下是该函数内部发生的情况:

function calculateNewPosition(positionx, positiony, direction)
{
    return {
        'up' : [positionx, positiony - 10],
        'down': [positionx, positiony + 10],
        'left' : [positionx - 10, positiony],
        'right' : [positionx + 10, positiony],
        'default': [positionx, positiony]
    }[direction];
}

function predict(contex, positionx, positiony) {
 const words = recognizer.wordLabels();
 recognizer.listen(({scores}) => {
   scores = Array.from(scores).map((s, i) => ({score: s, word: words[i]}));
   scores.sort((s1, s2) => s2.score - s1.score);

    var direction = scores[0].word;
    var [x1, y1] = calculateNewPosition(positionx, positiony, direction);

    contex.moveTo(positionx,positiony);
    contex.lineTo(x1, y1);
    contex.closePath();
    contex.stroke();

    positionx = x1;
    positiony = y1;
 }, {probabilityThreshold: 0.75});
}

这种方法正在做繁重的工作。从本质上讲,它运行一个无限循环,其中识别器正在倾听您正在说的话。请注意,我们正在使用参数 probabilityThreshold

此参数定义是否应调用回调函数。实质上,仅当最大概率分数大于此阈值时,才会调用回调函数。当我们得到这个词时,我们就得到了我们应该画的方向。

然后我们使用函数 calculateNewPosition 计算线尾的坐标。该步长为 10 像素,这意味着行的长度将为 10 像素。您可以同时使用概率阈值和此长度值。获得新坐标后,我们使用画布绘制线条。就是这样。很简单,对吧?

结论

在本文中,我们看到了如何轻松使用预先训练的 TensorFlow.js 模型。它们是一些简单应用程序的良好起点。我们甚至构建了一个此类应用程序的示例,您可以使用它使用语音命令进行绘制。这很酷,可能性是无穷无尽的。当然,您可以进一步训练这些模型,获得更好的结果,并将它们用于更复杂的解决方案。这意味着,您可以真正利用迁移学习。然而,这是另一个时代的故事。



原文标题:Speech Recognition with TensorFlow.js – Voice Commands

原文链接:https://rubikscode.net/2022/05/11/drawing-with-voice-speech-recognition-with-tensorflow-js/

作者:Nikola M. Zivkovic

编译:LCR

文共6655字,预计学习时长13分钟


本文阐述了如何利用Tensorflow编写一个基本的端到端自动语音识别(Automatic Speech Recognition,ASR)系统,详细介绍了最小神经网络的各个组成部分以及可将音频转为可读文本的前缀束搜索解码器。

虽然当下关于如何搭建基础机器学习系统的文献或资料有很多,但是大部分都是围绕计算机视觉和自然语言处理展开的,极少有文章就语音识别展开介绍。本文旨在填补这一空缺,帮助初学者降低入门难度,提高学习自信。

前提

初学者需要熟练掌握:

· 神经网络的组成

· 如何训练神经网络

· 如何利用语言模型求得词序的概率

概述

· 音频预处理:将原始音频转换为可用作神经网络输入的数据

· 神经网络:搭建一个简单的神经网络,用于将音频特征转换为文本中可能出现的字符的概率分布

· CTC损失:计算不使用相应字符标注音频时间步长的损失

· 解码:利用前缀束搜索和语言模型,根据各个时间步长的概率分布生成文本

本文重点讲解了神经网络、CTC损失和解码。

音频预处理

搭建语音识别系统,首先需要将音频转换为特征矩阵,并输入到神经网络中。完成这一步的简单方法就是创建频谱图。

def create_spectrogram(signals):
 stfts = tf.signal.stft(signals, fft_length=256)
 spectrograms = tf.math.pow(tf.abs(stfts), 0.5)
 return spectrograms


这一方法会计算出音频信号的短时傅里叶变换(Short-time Fourier Transform)以及功率谱,其最终输出可直接用作神经网络输入的频谱图矩阵。其他方法包括滤波器组和MFCC(Mel频率倒谱系数)等。

了解更多音频预处理知识:https://haythamfayek.com/2016/04/21/speech-processing-for-machine-learning.html


神经网络

下图展现了一个简单的神经网络结构。

语音识别基本结构


频谱图输入可以看作是每个时间步长的向量。1D卷积层从各个向量中提取出特征,形成特征向量序列,并输入LSTM层进一步处理。LSTM层(或双LSTM层)的输入则传递至全连接层。利用softmax激活函数,可得出每个时间步长的字符概率分布。整个网络将会用CTC损失函数进行训练(CTC即Connectionist Temporal Classification,是一种时序分类算法)。熟悉整个建模流程后可尝试使用更复杂的模型。

class ASR(tf.keras.Model):
 def __init__(self, filters, kernel_size, conv_stride, conv_border, n_lstm_units, n_dense_units):
 super(ASR, self).__init__()
 self.conv_layer = tf.keras.layers.Conv1D(filters,
 kernel_size,
 strides=conv_stride,
 padding=conv_border,
 activation='relu')
 self.lstm_layer = tf.keras.layers.LSTM(n_lstm_units,
 return_sequences=True,
 activation='tanh')
 self.lstm_layer_back = tf.keras.layers.LSTM(n_lstm_units,
 return_sequences=True,
 go_backwards=True,
 activation='tanh')
 self.blstm_layer = tf.keras.layers.Bidirectional(self.lstm_layer, backward_layer=self.lstm_layer_back)
 self.dense_layer = tf.keras.layers.Dense(n_dense_units)
 def call(self, x):
 x = self.conv_layer(x)
 x = self.blstm_layer(x)
 x = self.dense_layer(x)
 return x


为什么使用CTC呢?搭建神经网络旨在预测每个时间步长的字符。然而现有的标签并不是各个时间步长的字符,仅仅是音频的转换文本。而文本的各个字符可能横跨多个步长。如果对音频的各个时间步长进行标记,C-A-T就会变成C-C-C-A-A-T-T。而每隔一段时间,如10毫秒,对音频数据集进行标注,并不是一个切实可行的方法。CTC则解决上了上述问题。CTC并不需要标记每个时间步长。它忽略了文本中每个字符的位置和实际相位差,把神经网络的整个概率矩阵输入和相应的文本作为输入。

CTC 损失计算

输出矩阵示例


假设真实的数据标签为CAT,在四个时间步长中,有序列C-C-A-T,C-A-A-T,C-A-T-T,_-C-A-T,C-A-T-_与真实数据相对应。将这些序列的概率相加,可得到真实数据的概率。根据输出的概率矩阵,将序列的各个字符的概率相乘,可得到单个序列的概率。则上述序列的总概率为0.0288+0.0144+0.0036+0.0576+0.0012=0.1056。CTC损失则为该概率的负对数。Tensorflow自带损失函数文件。

解码

由上文的神经网络,可输出一个CTC矩阵。这一矩阵给出了各个时间步长中每个字符在其字符集中的概率。利用前缀束搜索,可从CTC矩阵中得出所需的文本。

除了字母和空格符,CTC矩阵的字符集还包括两种特别的标记(token,也称为令牌)——空白标记和字符串结束标记。

空白标记的作用:CTC矩阵中的时间步长通常比较小,如10毫秒。因此,句子中的一个字符会横跨多个时间步长。如,C-A-T会变成C-C-C-A-A-T-T。所以,需要将CTC矩阵中出现该问题的字符串中的重复部分折叠,消除重复。那么像FUNNY这种本来就有两个重复字符(N)的词要怎么办呢?在这种情况下,就可以使用空白标记,将其插入两个N中间,就可以防止N被折叠。而这么做实际上并没有在文本中添加任何东西,也就不会影响其内容或形式。因此,F-F-U-N-[空白]-N-N-Y最终会变成FUNNY。

结束标记的作用:字符串的结束表示着一句话的结束。对字符串结束标记后的时间步长进行解码不会给候选字符串增加任何内容。


步骤


初始化

· 准备一个初始列表。列表包括多个候选字符串,一个空白字符串,以及各个字符串在不同时间步长以空白标记结束的概率,和以非空白标记结束的概率。在时刻0,空白字符串以空白标记结束的概率为1,以非空白标记结束的概率则为0。

迭代

· 选择一个候选字符串,将字符一个一个添加进去。计算拓展后的字符串在时刻1以空白标记和非空白标记结束的概率。将拓展字符串及其概率记录到列表中。将拓展字符串作为新的候选字符串,在下一时刻重复上述步骤。

· 情况A:如果添加的字符是空白标记,则保持候选字符串不变。

· 情况B:如果添加的字符是空格符,则根据语言模型将概率与和候选字符串的概率成比例的数字相乘。这一步可以防止错误拼写变成最佳候选字符串。如,避免COOL被拼成KUL输出。

· 情况C:如果添加的字符和候选字符串的最后一个字符相同,(以候选字符串FUN和字符N为例),则生成两个新的候选字符串,FUNN和FUN。生成FUN的概率取决于FUN以空白标记结束的概率。生成FUNN的概率则取决于FUN以非空白标记结束的概率。因此,如果FUN以非空白标记结束,则去除额外的字符N。

输出

经过所有时间步长迭代得出的最佳候选字符串就是输出。

为了加快这一过程,可作出如下两个修改。

1.在每一个时间步长,去除其他字符串,仅留下最佳的K个候选字符串。具体操作为:根据字符串以空白和非空白标记结束的概率之和,对候选字符串进行分类。

2.去除矩阵中概率之和低于某个阈值(如0.001)的字符。

具体操作细节可参考如下代码。

def prefix_beam_search(ctc, 
 alphabet, 
 blank_token, 
 end_token, 
 space_token, 
 lm, 
 k=25, 
 alpha=0.30, 
 beta=5, 
 prune=0.001):
 '''
 function to perform prefix beam search on output ctc matrix and return the best string
 :param ctc: output matrix
 :param alphabet: list of strings in the order their probabilties are present in ctc output
 :param blank_token: string representing blank token
 :param end_token: string representing end token
 :param space_token: string representing space token
 :param lm: function to calculate language model probability of given string
 :param k: threshold for selecting the k best prefixes at each timestep
 :param alpha: language model weight (b/w 0 and 1)
 :param beta: language model compensation (should be proportional to alpha)
 :param prune: threshold on the output matrix probability of a character. 
 If the probability of a character is less than this threshold, we do not extend the prefix with it
 :return: best string
 '''
 zero_pad = np.zeros((ctc.shape[0]+1,ctc.shape[1]))
 zero_pad[1:,:] = ctc
 ctc = zero_pad
 total_timesteps = ctc.shape[0]
 # #### Initialization ####
 null_token = ''
 Pb, Pnb = Cache(), Cache()
 Pb.add(0,null_token,1)
 Pnb.add(0,null_token,0)
 prefix_list = [null_token]
 
 # #### Iterations ####
 for timestep in range(1, total_timesteps):
 pruned_alphabet = [alphabet[i] for i in np.where(ctc[timestep] > prune)[0]]
 for prefix in prefix_list:
 if len(prefix) > 0 and prefix[-1] == end_token:
 Pb.add(timestep,prefix,Pb.get(timestep - 1,prefix)) 
 Pnb.add(timestep,prefix,Pnb.get(timestep - 1,prefix))
 continue 
 for character in pruned_alphabet:
 character_index = alphabet.index(character)
 # #### Iterations : Case A ####
 if character == blank_token:
 value = Pb.get(timestep,prefix) + ctc[timestep][character_index] * (Pb.get(timestep - 1,prefix) + Pnb.get(timestep - 1,prefix))
 Pb.add(timestep,prefix,value)
 else:
 prefix_extended = prefix + character
 # #### Iterations : Case C ####
 if len(prefix) > 0 and character == prefix[-1]:
 value = Pnb.get(timestep,prefix_extended) + ctc[timestep][character_index] * Pb.get(timestep-1,prefix)
 Pnb.add(timestep,prefix_extended,value)
 value = Pnb.get(timestep,prefix) + ctc[timestep][character_index] * Pnb.get(timestep-1,prefix)
 Pnb.add(timestep,prefix,value)
 # #### Iterations : Case B ####
 elif len(prefix.replace(space_token, '')) > 0 and character in (space_token, end_token):
 lm_prob = lm(prefix_extended.strip(space_token + end_token)) ** alpha
 value = Pnb.get(timestep,prefix_extended) + lm_prob * ctc[timestep][character_index] * (Pb.get(timestep-1,prefix) + Pnb.get(timestep-1,prefix))
 Pnb.add(timestep,prefix_extended,value) 
 else:
 value = Pnb.get(timestep,prefix_extended) + ctc[timestep][character_index] * (Pb.get(timestep-1,prefix) + Pnb.get(timestep-1,prefix))
 Pnb.add(timestep,prefix_extended,value)
 if prefix_extended not in prefix_list:
 value = Pb.get(timestep,prefix_extended) + ctc[timestep][-1] * (Pb.get(timestep-1,prefix_extended) + Pnb.get(timestep-1,prefix_extended))
 Pb.add(timestep,prefix_extended,value)
 value = Pnb.get(timestep,prefix_extended) + ctc[timestep][character_index] * Pnb.get(timestep-1,prefix_extended)
 Pnb.add(timestep,prefix_extended,value)
 prefix_list = get_k_most_probable_prefixes(Pb,Pnb,timestep,k,beta)
 # #### Output ####
 return prefix_list[0].strip(end_token)


这样,一个基础的语音识别系统就完成了。对上述步骤进行复杂化,可以得到更优的结果,如,搭建更大的神经网络和利用音频预处理技巧。

完整代码:https://github.com/apoorvnandan/speech-recognition-primer


注意事项:

1. 文中代码使用的是TensorFlow2.0系统,举例使用的音频文件选自LibriSpeech数据库(http://www.openslr.org/12)。

2. 文中代码并不包括训练音频数据集的批量处理生成器。读者需要自己编写。

3. 读者亦需自己编写解码部分的语言模型函数。最简单的方法就是基于语料库生成一部二元语法字典并计算字符概率。


留言 点赞 关注

我们一起分享AI学习与发展的干货

如需转载,请后台留言,遵守转载规范

家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

什么是 PocketSphinx.js

PocketSphinx.js 是一个完全在 Web 浏览器中运行语音识别器,建立在以下核心能力之上:

  • 用 C (PocketSphinx) 编写的语音识别器使用 Emscripten 转换为 JavaScript 或 WebAssembly
  • 使用 web audio API 的 audio recorder, 录音机可以独立使用来构建其他类型的与音频相关的 Web 应用程序

PocketSphinx.js 项目包括几个可以独立使用的组件:

  • pocketsphinx.js:一个由 emscripten 生成的 JavaScript 库,基本上是 PocketSphinx 包装以提供更简单的 API,并编译成 JavaScript 或 WebAssembly。
  • pocketsphinx.wasm:编译后的 WebAssembly 文件(如果编译成 WebAssembly)。
  • recognizer.js:Web Worker 内 pocketsphinx.js 的包装器,用于卸载 UI 线程,免于下载和运行大型 JavaScript 文件以及运行成本高昂的语音识别过程。
  • audioRecorder.js:一个基于 Recorderjs 的录音库,将 recorder 的样本转换为适当的采样率并将其传递给识别器。
  • callbackManager.js:一个小实用程序,用于通过调用和回调而不是消息传递与 Web Workers 进行交互。

目前 PocketSphinx.js 已经在 Github 通过 MIT 协议开源,有超过 1.5k 的 star,是一个值得关注的前端优质开源项目。

如何使用 PocketSphinx.js

pocketsphinx.js 使用规范

pocketsphinx.js 文件可以直接包含到 HTML 文件中,但由于其相当大(几 MB,取决于编译和打包文件时使用的优化级别),下载和加载它需要时间并影响 UI 线程。 因此,大多数情况应该在 Web Worker 中使用,例如使用 recognizer.js。

该 API 基于 embind,可能应该查看 emscripten 文档中的该部分,以了解如何与 emscripten 生成的 JavaScript 进行交互。 Pocketsphinx.js 的早期版本使用 C 风格的 API,现已弃用,但仍然可以在 OBSOLETE_API 分支中使用。

请注意,如果使用 WebAssembly 版本,则需要 pocketsphinx.js 和 pocketsphinx.wasm。 一旦 pocketsphinx.js 加载到页面中,它将通过查看根文件夹中的文件开始加载和编译 pocketsphinx.wasm。 可以在加载 pocketsphinx.js 之前设置备用 URL:

var Module = {
    locateFile: function() {return "/path/to/pocketsphinx.wasm";}
    }

另外,由于 pocketsphinx.wasm 是异步加载,因此需要等到其加载并编译后才能执行任何操作。一种方法是为 onRuntimeInitialized 添加回调:

  <script type="text/javascript">
    var Module = {
        locateFile: function() {return "/path/to/pocketsphinx.wasm";},
        onRuntimeInitialized: function() {
        // 开始使用具体方法
        }
      };
 </script>
 <script src="path/to/pocketsphinx.js"></script>

作为第一个示例,以下代码创建一个新的识别器:

var recognizer = new Module.Recognizer();
/* ..创建一个识别器... */
recognizer.delete();

对 pocketsphinx.js 函数的调用是同步的,这也是可能需要将其加载到 Web Worker 中的原因。

大多数调用返回 ResultType 对象,该对象可以是以下之一:

  • SUCCESS,如果操作执行成功。
  • BAD_STATE,如果当前状态不允许该操作。
  • BAD_ARGUMENT,如果提供的参数无效。
  • RUNTIME_ERROR,如果识别器中存在运行时错误。

在 JavaScript 中,这些值可以称为 Module.ReturnType.SUCCESS、Module.ReturnType.BAD_STATE 等。例如:

var recognizer = new Module.Recognizer();
/* ..创建一个识别器.. */
if (recognizer.reInit(config) != Module.ReturnType.SUCCESS)
    alert("Error while recognizer is re-initialized");

Recognizer 对象

pocketsphinx.js 的入口是 Recognizer 对象。 开发者可以根据需要创建任意数量的实例,但很多情况下可能不需要并且希望节省内存。 创建新实例时,可以给出一个可选的 Config 对象,该对象将用于设置用于初始化 Pocketsphinx 的参数。

var config = new Module.Config();
config.push_back(["-fwdflat", "no"]);
var recognizer = new Module.recognizer(config);
config.delete();
/* ..删除一个识别器. */
recognizer.delete();

此时将初始化一个 recognizer,并将 “-fwdflat” 设置为 “no”。

如果在编译 pocketsphinx.js 时包含了多个声学模型,则可以通过设置 “-hmm” 参数来选择应使用哪一个。 假设有两个模型,一种用于英语,一种用于法语,并且已使用 -DHMM_FOLDERS="english;french" 编译了库,则可以通过在 Config 对象中设置正确的值来使用法语模型初始化识别器:

var config = new Module.Config();
config.push_back(["-hmm", "french"]);
var recognizer = new Module.recognizer(config);

如果没有给出 “-hmm” 参数,或者给它一个无效值,则将使用列表中的第一个模型(此处为英语)。

同样,应该使用 recognizer 配置参数来加载之前打包在 pocketshinx.js 中的统计语言模型(“-lm”)或字典(“-dict”)。 请注意,如果要使用 SLM,则还必须有一个包含 SLM 中使用的单词的词典文件。

此外,在创建实例后,可以通过调用 reInit 来使用新参数重新初始化识别器对象,例如:

var config_english = new Module.Config();
config_english.push_back(["-hmm", "english"]);
var config_french = new Module.Config();
config_french.push_back(["-hmm", "french"]);
var recognizer = new Module.recognizer(config_english);
/* ..个性化识别参数. */
if (recognizer.reInit(config_french) != Module.ReturnType.SUCCESS)
    alert("Error while recognizer is re-initialized");

识别音频 Recognizing audio

要识别音频,必须首先调用 start 来初始化识别,然后通过调用来向识别器提供音频数据以进行处理,最后在完成后调用 stop。 在识别期间和识别之后,可以通过调用 getHyp 来检索已识别的字符串。

在调用 start 之前,必须确保当前的语言模型是正确的,主要是最后发生的情况:

  • 如果刚刚向识别器提供语法或关键字搜索,它将自动用作当前语言模型。
  • 如果对 switchSearch 的调用成功,则将在下一次调用中使用指定的搜索来启动。
  • 如果 SLM 打包在 pocketsphinx.js 中,并通过将参数添加到实例化(或重新初始化)识别器时使用的 Config 对象来加载,则该模型就是当前的语言模型。

对 process 的调用必须包含 AudioBuffer 对象形式的音频缓冲区。 AudioBuffer 对象可以重复使用,但必须包含以 16kHz 录制的 2 字节整数形式的音频样本(除非声学模型使用不同的特性)。

var array = ... // array that contains an audio buffer
var buffer = new Module.AudioBuffer();
for (var i = 0 ; i < array.length ; i++)
    buffer.push_back(array[i]); // Feed the array with audio data
var output = recognizer.start(); // Starts recognition on current language model
output = recognizer.process(buffer); // Processes the buffer
var hyp = recognizer.getHyp(); // Gets the current recognized string (hypothesis)
/* ... */
for (var i = 0 ; i < array.length ; i++)
    buffer.set(i, array[i]); // Feed buffer with new data
output = recognizer.process(buffer);
hyp = recognizer.getHyp();
/* ... */
output = recognizer.stop();
// Gets the final recognized string:
var final_hyp = recognizer.getHyp();
buffer.delete();

将 recognizer.js 连接到录音机

PocketSphinx.js 包含一个基于 Web Audio API 的音频录制库,该库访问麦克风、获取音频样本、将其转换为适当的采样率(默认声学模型为 16kHz),并将其发送到识别器, 该库源自 Recorderjs。

在 HTML 文件中包含 audioRecorder.js,并确保 audioRecorderWorker.js 位于同一文件夹中。 要使用它,请创建一个 AudioRecorder 的新实例,并将 MediaStreamSource 作为参数。 截至今天,Google Chrome 和 Firefox (25+) 都实现了,还需要将识别器属性设置为识别器工作线程,如上所述。

// Deal with prefixed APIs
window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Instantiating AudioContext
try {
    var audioContext = new AudioContext();
} catch (e) {
    console.log("Error initializing Web Audio");
}

var recorder;
// Callback once the user authorizes access to the microphone:
function startUserMedia(stream) {
    var input = audioContext.createMediaStreamSource(stream);
    recorder = new AudioRecorder(input);
    // We can, for instance, add a recognizer as consumer
    if (recognizer) recorder.consumers.push(recognizer);
  };

// Call getUserMedia
if (navigator.mediaDevices.getUserMedia)
    navigator.mediaDevices.getUserMedia({audio: true})
                            .then(startUserMedia)
                            .catch(function(e) {
                                console.log("No live audio input in this browser");
                            });
else console.log("No web audio support in this browser");

recorder 启动并运行后,可以通过以下方式开始和停止录音和识别:

// To start recording:
recorder.start();
// The hypothesis is periodically sent by the recognizer, as described previously
// To stop recording:
recorder.stop();  // The final hypothesis is sent

AudioRecorder 的构造函数可以采用可选的配置对象。 此配置可以包含一个回调函数,该函数在录制过程中出现错误时执行。 截至目前,唯一可能的错误是输入样本静音时,还可以包括输出采样率,如果使用 8kHz 音频的声学模型可能需要设置该输出采样率。

var audioRecorderConfig = {
    errorCallback: function(x) {alert("Error from recorder: " + x);},
    outputSampleRate: 8000
    };
recorder = new AudioRecorder(input, audioRecorderConfig);

本文总结

本文主要和大家介绍 PocketSphinx.js,其是一个完全在 Web 浏览器中运行语音识别器,建立在 PocketSphinx,使用 web audio API 的 audio recorder 之上。 因为篇幅问题,关于 PocketSphinx.js 只是做了一个简短的介绍,但是文末的参考资料以及个人主页提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。

参考资料

https://github.com/syl22-00/pocketsphinx.js

https://www.instructables.com/Introduction-to-Pocketsphinx-for-Voice-Controled-A/

https://www.youtube.com/watch?v=J-bQQiCMV5k

https://levelup.gitconnected.com/how-to-use-web-workers-api-b1ef96c46fc0