CMUS狮身人面像(八)-使用 PocketSphinx 构建应用程序

在本教程中,我们将使用 PocketSphinx 演练一个简单的 C 代码示例。 这与源代码中的live_portaudio.c 示例完全对应。所以,TL;DR,你可以尝试编译它:

cmake -G Ninja -S. -B build
cmake --build --target live_portaudio

构建 PocketSphinx

首先,通过从GitHub 版本页面下载版本或 使用 git 克隆源来获取源代码

有关更多详细信息,请参阅下载页面或 自述文件

PocketSphinx 使用CMake来管理多个平台上的配置和构建。默认情况下,它构建静态库和二进制文件,可以简单地从源目录使用它们。您还可以将其安装在系统范围内或用户目录中。在安装它的情况下,它将 pkg-config 允许您查找库和头文件目录和名称,但这并不是真正必要的,因为只有一个头文件(<pocketsphinx.h>)和一个库(-lpocketsphinx)。

出于本教程的目的,我们将其安装在本地目录中。

在类 Unix 系统(包括 MacOS)上安装

确保您安装了 CMake、PortAudio 和可用的 C 编译器。在 GNU/Linux 上,您可以使用包管理器,无论它是什么。例如在 Ubuntu/Debian/etc 上:

sudo apt install build-essential cmake ninja-build portaudio19-dev

在 MacOS 上,这将要求您最少安装“Xcode 命令行工具”。如果您已经安装了 Homebrew,那么您已经拥有了这些。然后,您可以使用官方安装程序Homebrew安装 CMake 。对于 PortAudio...我不知道,使用 Homebrew,我猜:

brew install cmake portaudio

我们假设您将 PocketSphinx 安装在 cmusphinx主目录中名为的目录中。它应该像这样简单(假设已安装 CMake):

cmake -S . -B build -DCMAKE_INSTALL_PREFIX=$HOME/cmusphinx
cmake --build build --target install

如果您足够幸运安装了Ninja ,则可以使构建速度加快很多倍(在 2009 年的 Intel 处理器 上完成需要 1.7 秒),只需添加-G Ninja到上面的第一行即可。

视窗

当然,Windows 在各个方面都更加复杂。做每件事都有很多种方法,但所有方法都不方便且令人沮丧。阻力最小的方法是安装 MSYS2,启动 MSYS2 shell(最好是 UCRT64版本),然后安装 CMake、PortAudio 和 Ninja 以及编译器:

pacman -S mingw-w64-ucrt-x86_64-gcc cmake ninja mingw-x64-ucrt-x86_64-portaudio

现在构建:

cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=$HOME/cmusphinx
cmake --build build --target install

请注意,由于您正在运行 MSYS2 版本的 CMake,因此您 不能直接使用 Windows-y 路径(如$LOCALAPPDATA/cmusphinx 或 (恐怖))%LOCALAPPDATA%/cmusphinx作为CMAKE_INSTALL_PREFIX,因为它无法理解驱动器号。您可以 cygpath为此使用,例如:

cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=$(cygpath $LOCALAPPDATA)/cmusphinx
cmake --build build --target install

配置

现在您已经安装了 PocketSphinx,您必须设置一个小环境变量以确保它可以找到其模型。当您运行上面的第一个cmake命令时,您可能会看到这样的行:

MODELDIR="/home/user/cmusphinx/share/pocketsphinx/model"

您需要设置POCKETSPHINX_PATH到该目录。在不久的将来,安装目标也会告诉您这一点,并且还会创建一个小脚本来为您执行此操作,但现在您必须手动执行此操作:

export POCKETSPHINX_PATH=$HOME/cmusphinx/share/pocketsphinx/model

使用 Pocketsphinx API

好吧,让我们来看看代码吧!提醒一下,您可以在 https://github.com/cmusphinx/pocketsphinx/blob/master/examples/live_portaudio.c 查看整个内容

一般原则

该 API 的参考文档位于 PocketSphinx: PocketSphinx API Documentation。我们将创建并使用三种不透明类型(如类)来配置识别器、检测输入中的语音片段并识别语音:

一般来说,PocketSphinx 类型有一个名为 的函数TYPE_init (用于创建类型的实例)和一个名为的函数(TYPE_free 用于释放类型的实例)。一般规则是,如果您在代码中创建了一个实例,则始终需要释放它,如果您没有创建它(即它是由某些 API 函数返回给您的),则不应释放它。唯一的例外是像 ps_seg_t和 这样的迭代器类型ps_alignment_iter_t,如果您在列表末尾之前停止对它们进行迭代,则必须“释放”它们。

这听起来很令人困惑,但如果你仔细想想,这是有道理的!如有疑问,请记住:“在许多应用程序中内存泄漏是完全可以接受的”

初始化

首先,我们将使用一组默认参数创建 ps_config_t ,其中还包括默认的声学和语言模型:

config = ps_config_init(NULL);
ps_default_search_args(config);

ps_config_t有一堆相关的函数来从中获取信息。我们将特别使用其中之一来获取采样率, 稍后我们将使用它来初始化 PortAudio 。

(请注意,您可以以相反的方式执行此操作,并使用音频流提供的采样率来初始化识别器,这在某些情况下更有意义)

跳过初始化 PortAudio 的细节,我们现在将 初始化识别器,它被称为“解码器”,原因如下:

if ((decoder = ps_init(config)) == NULL)
    E_FATAL("PocketSphinx decoder init failed\n");

还有端点,它是检测音频流中语音的组件。是的,有很多零。您可以在文档中查看它们的含义:

if ((ep = ps_endpointer_init(0, 0.0, 0, 0, 0)) == NULL)
    E_FATAL("PocketSphinx endpointer init failed\n");

端点定位

端点的工作原理是消耗一“帧”音频数据并返回一“帧”语音数据(如果检测到的话)。再次强调,由于我们这里使用的是 C 语言,因此请务必记住:

  • 输入数据归调用者所有
  • 输出数据由端点拥有

这意味着您可以简单地为输入音频分配一个缓冲区并在整个应用程序中重用它,并且您不应该对端点返回的语音帧执行任何操作,除了:

  • 将它们直接传递给解码器
  • 如果您想保存它们,请将它们复制到其他地方

顺便说一句,这意味着您也不应该尝试在多个线程之间共享端点或解码器。

你怎么知道要分配什么?端点告诉你 ps_endpointer_frame_size

frame_size = ps_endpointer_frame_size(ep);
if ((frame = malloc(frame_size * sizeof(frame[0]))) == NULL)
    E_FATAL_SYSTEM("Failed to allocate frame");

如果您不关心与古老的 C 编译器的兼容性,您可以简单地执行此操作,而不必担心稍后释放任何内容:

int16_t frame[ps_endpointer_frame_size(ep)];

(顺便说一句,PocketSphinx 的未来版本可能会完全放弃 C89 兼容性,因为它已经可疑地兼容 C89)

需要注意的是,您只能将这个大小的缓冲区传递给端点。对于某些低级音频 API,这意味着您必须进行一些缓冲。幸运的是,PortAudio 允许您请求缓冲区大小,我们在打开音频流时会这样做:

if ((err = Pa_OpenDefaultStream(&stream, 1, 0, paInt16,
                                ps_config_int(config, "samprate"),
                                frame_size, NULL, NULL)) != paNoError)
    E_FATAL("Failed to open PortAudio stream: %s\n",
            Pa_GetErrorText(err));

现在,您的应用程序将等待获取一些音频缓冲区,这通常涉及回调函数(恶心)或漂亮、简单的循环(万岁)。幸运的是,PortAudio 为我们提供了第二种选择。所以,我们坐在一个循环中,如下所示:

  1. 检查我们当前是否处于演讲部分 ps_endpointer_in_speech
  2. 等待音频缓冲区。
  3. 使用 将其传递给端点ps_endpointer_process。如果是 NULL,则返回步骤 1。
  4. 如果我们之前没有进入过语音部分,请开始识别语音。
  5. 将语音缓冲区传递给识别器。
  6. 如果我们不再处于语音部分( ps_endpointer_in_speech再次检查),则停止识别语音并获取识别结果。

加工

在我们识别任何语音之前,我们需要通过调用 来开始“话语” ps_start_utt。然后我们可以使用 传递音频缓冲区(在本例中为任意大小)ps_process_raw。这有几个选项,我们不会在此处用于实时模式识别,但在其他情况下可能有用 - 人们可以指示它简单地缓冲音频而不实际进行任何识别(在计算机非常慢的情况下) ),或者将整个缓冲区视为单个话语(在一次识别整个文件时很有用,因为它提供了更好的准确性)。

取得成果

可以ps_get_hyp在调用ps_start_utt和之间的任何时刻使用来请求识别结果ps_end_utt。这个函数只是返回一个字符串——如果你想要分词,你可以使用 ps_seg_iter.

与语音缓冲区一样,您没有分配该字符串,不应该释放它,也不应该对它执行任何操作,除了:

  • 打印出来或采取其他一些立即行动。
  • 如果您想保存或存储以供以后使用,请复制它。

同样,关于不在ps_decoder_t线程之间共享的警告。

代码

具体来说,整个事情是这样的:

while (!global_done) {
    const int16 *speech;
    int prev_in_speech = ps_endpointer_in_speech(ep);
    if ((err = Pa_ReadStream(stream, frame, frame_size)) != paNoError) {
        E_ERROR("Error in PortAudio read: %s\n",
            Pa_GetErrorText(err));
        break;
    }
    speech = ps_endpointer_process(ep, frame);
    if (speech != NULL) {
        const char *hyp;
        if (!prev_in_speech)
            ps_start_utt(decoder);
        if (ps_process_raw(decoder, speech, frame_size, FALSE, FALSE) < 0)
            E_FATAL("ps_process_raw() failed\n");
        if (!ps_endpointer_in_speech(ep)) {
            ps_end_utt(decoder);
            if ((hyp = ps_get_hyp(decoder, NULL)) != NULL) {
                printf("%s\n", hyp);
                fflush(stdout);
            }
        }
    }
}

如何编译?嗯……当然,实际的示例可以使用 CMake 构建,如顶部所述,但对于您自己的代码,您需要知道如何编译和链接它。如果您使用上面的命令安装,您将在 中找到 PocketSphinx 标头, $HOME/cmusphinx/include并在 中找到库$HOME/cmusphinx/lib。对于 PortAudio,他们会……在某个地方,但pkg-config可以告诉您。所以,如果您的代码位于example.c

cc -o example example.c \
    -I$HOME/cmusphinx/include -L$HOME/cmusphinx/lib -lpocketsphinx -lm \
    $(pkg-config --static --libs --cflags portaudio-2.0)

高级用法

有关 API 的更高级用法,请查看 API 参考。

  • 对于分词,API 提供了一个迭代器对象,用于迭代单词序列。这个迭代器对象是一个抽象类型,提供了一些访问器来获取时间点、分数,以及最有趣的是每个单词的后验概率。
  • 可以通过该方法获取整个话语的置信度ps_get_prob
  • 如果需要,您可以访问晶格。
  • 您可以配置多个搜索并在运行时在它们之间切换。

搜索次数

作为开发人员,您可以配置多个具有不同语法和语言模型的“搜索”对象,并在运行时在它们之间切换,以为用户提供交互体验。

有多种可能的搜索模式:

  • 关键字:有效地查找关键短语并忽略其他语音。它允许配置检测阈值。
  • 语法:根据 JSGF 语法识别语音。与关键词搜索不同,语法搜索不会忽略语法中不存在的单词,而是尝试识别它们。
  • ngram/lm:使用语言模型识别自然语音。
  • allphone:使用语音语言模型识别音素。

每个搜索都有一个名称,并且可以通过名称进行引用。名称是特定于应用程序的。该功能ps_set_search允许激活先前按名称添加的搜索。

为了添加搜索,需要指向描述搜索的语法/语言模型。语法的位置特定于应用程序。如果仅需要简单的识别,则添加单个搜索或仅使用配置选项配置所需的模式就足够了。

搜索的具体设计取决于您的应用程序。例如,您可能希望首先侦听激活关键字,一旦识别到该关键字,就切换到 ngram 搜索以识别实际命令。一旦识别出该命令,您可以切换到语法搜索以识别确认,然后切换回关键字监听模式以等待另一个命令。

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟V-SpHeNIC

支持科研技术

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值