构建物联网设备语音控制前端
苹果、谷歌和亚马逊正在将语音控制提升到新的水平。但是语音控制可以成为一个 DIY 项目吗?事实证明,它可以。而且,它并没有你想象的那么难。
Siri、Alexa 和 Google Home 都可以将语音命令转换为基本活动,特别是如果这些活动仅仅涉及共享音乐和电影等数字文件。与家庭自动化的集成也是可能的,尽管可能没有用户期望的那么简单——至少目前还没有。
尽管如此,将语音命令转换为动作的想法对创客界来说仍然很有吸引力。三大巨头的 offerings 看起来像是盒子里的魔法,但我们都知道这只是软件和硬件。这里没有魔法。如果真是这样,有人可能会问,任何人如何构建魔法盒子呢?
事实证明,仅使用一个在线 API 和许多免费提供的库,这个过程并不像看起来那么复杂。本文介绍了 Jarvis 项目,这是一个 Java 应用程序,用于捕获音频、转换为文本、提取和执行命令以及通过语音响应用户。它还探讨了与集成这些组件以获得编程结果相关的编程问题。这意味着不涉及机器学习或神经网络。最终目标是让一组关键词触发调用特定方法来执行操作。
API 和消息传递Jarvis 在几年前开始作为一个实验,看看语音控制在 DIY 项目中是否可行。第一步是确定已经存在哪些开源支持。几周的研究发现了一些可能在各种语言中进行的项目。这项研究记录在源代码库中 docs/notes.txt 文件中包含的文本文档中。编程语言的最终选择基于语音转文本 API 和自然语言处理器库的选择。
由于 Jarvis 是实验性的(它后来升级为 IronMan 项目中的一个工具),它最初的要求是尽可能容易地使其工作。Java 中的音频采集非常直接,并且比 C 或其他语言更简单易用。更重要的是,一旦收集到音频,就需要一个将其转换为文本的 API。为此找到的最简单的 API 是 Google 的 Cloud Speech REST API。由于音频收集和 REST 接口在 Java 中都相当容易处理,因此 Java 似乎是该项目的可能编程语言选择。
在音频转换为文本后,下一步将是某种文本分析。这种分析被称为自然语言处理。Apache OpenNLP 库 正是做这件事的,它可以简单地将文本字符串分解为其组成部分。由于这也是一个 Java 库,因此选择 Java 作为项目语言就完成了。
最初,Google API 的使用包括非公共接口——基本上是使用 Chrome 浏览器内部隐藏的接口。这些接口消失了,并被当前的公共 Google Cloud Speech API 所取代。此外,还使用了 Google 的 Translate 文本转语音功能,但该接口在被公众滥用后被移除。因此,最近集成了一个替代解决方案:espeak-ng。Espeak 是一个用于语音合成的命令行工具。它可以与 mbrola 项目的声音集成,以帮助产生更好(至少不那么像计算机)的声音。可以将其视为改进版的 Stephen Hawking 声音。最重要的是,可以直接从 Java 调用 espeak 来生成音频文件,然后 Java 可以使用主机的音频系统播放这些文件。
有了 API、工具和库集,现在可以设计程序流程了。Jarvis 利用以下执行线程
- 捕获音频并将其录制到文件中。
- 将音频文件转换为文本。
- 将文本处理为词性。
- 分析词性以进行命令处理。
- 提供语音响应。
Jarvis 还包括一个简单的 UI,主要用于调试目的,它可以绘制线程处理时间图并显示语音模式。每个线程都使用一个短延迟循环来检查来自其他线程的入站消息。录制线程查找高于配置级别的音频,然后在级别降至阈值以下时将其保存到文件。这会导致消息传递到下一个线程以进行持续处理,依此类推。甚至 UI 线程也会收到遥测数据以进行显示。

图 1. Jarvis 的 UI 主要用于监控进程,而不是直接用于用户交互。
先决条件为了使用 Jarvis,您将需要一个麦克风和一些扬声器。在测试中使用了 PulseAudio 插件,因为它允许启用和禁用输入源,这意味着我不会一直启用音频输入设备。如果黑客深入我的世界,让他们听到我的每一个动作是没有意义的。
可以使用任何麦克风,但对于远程开发计划,即无论我在房间的哪个位置都应该拾取音频,我选择了一个 Blue Snowball iCE 电容式麦克风。这对于播客来说质量很好,并且只要我说话声音稍微大一点,它就能很好地拾取房间内任何位置的清晰录音。
只有当您想听到 Jarvis 的响应时才需要扬声器。如果您没有用于计算机的扬声器,这不会阻止 Jarvis 以其他方式处理您的语音命令。
从语音到文本Java 通过 javax.sound.sampled.DataLine
和 javax.sound.sampled.AudioSystem
类提供对从 Linux 音频子系统读取的支持。使用这些类的第一步是设置一个具有所需采样率和相关配置的 AudioFormat
类
AudioFormat getAudioFormat() {
float sampleRate = 16000.0F;
int sampleSizeInBits = 16;
int channels = 1;
boolean signed = true;
boolean bigEndian = false;
return new AudioFormat(sampleRate, sampleSizeInBits,
↪channels, signed, bigEndian);
}
这些设置稍后用于确定音频采样的阈值级别。然后将 DataLine
传递给此类,以设置来自 Linux 音频子系统的缓冲音频数据。AudioSystem
使用 DataLine
获取 Line 对象,这是与实际音频的连接
audioFormat = getAudioFormat();
DataLine.Info dataLineInfo = new DataLine.Info(
↪TargetDataLine.class, audioFormat);
line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
然后打开 line 对象,启动 Java 音频数据采样。测试 line 的缓冲区内容,如果找到任何内容,则计算该缓冲区的音频级别。如果级别高于硬编码的阈值,则开始音频录制。录制将音频缓冲区写入流缓冲区。当级别降至阈值以下时,录制停止,输出流关闭。这称为代码片段。如果代码片段大小非零,则将其排队以使用 javaFLACEncoder 转换为 WAV 格式
int count = line.read(buffer, 0, buffer.length);
if ( count != 0 )
{
float level = calculateLevel(buffer,0,0);
if ( !recording )
{
if ( level >= threshold )
recording++;
}
else
{
if ( level < threshold )
{
recording=0;
Snippet snippet = new Snippet();
snippet.setBytes( out.toByteArray() );
if ( snippet.size() != 0 )
snippets.add( snippet );
}
}
计算阈值需要运行音频缓冲区的长度以找到最大整数值。然后将最大值从 0.0 归一化到 1.0,即最大音量的百分比。该百分比用于与阈值级别进行比较。然后将编码的 WAV 文件排队以转换为文本。
将音频文件转换为文本需要通过 Google 的 Cloud Speech API,这是一个 REST API,需要 API 密钥,并且有财务成本,尽管对于普通 Jarvis 用户来说成本非常低(实际上为零)。Jarvis 旨在允许用户使用自己的密钥,因为源代码中未提供密钥。
Google 的 API 要求在 HTTP 消息正文中的 JSON 对象中传递音频文件作为 Base64 编码的数据,其中目标 URL 是 REST API。返回数据也是一个 JSON 对象,其中填充了转换后的文本和其他数据。Jarvis 使用自定义类 GAPI 来保存返回的文本数据,并处理 JSON 解析以提取字段供其他类使用。SimpleJSON 库用于所有 JSON 操作
Path path = Paths.get( audiofile.getPath() );
byte[] data = Base64.getEncoder().encode( Files.readAllBytes(path) );
audiofile.delete();
if ( (data != null) || (data.length.0) )
{
String request = "https://speech.googleapis.com/v"
↪+ Cli.get(Cli.S_GAPIV) + "/speech:recognize?key=" + apikey;
JSONObject config = new JSONObject();
JSONObject audio = new JSONObject();
JSONObject parent = new JSONObject();
config.put("encoding","FLAC");
config.put("sampleRateHertz", new Integer(16000));
config.put("languageCode", "en-US");
audio.put("content", new String(data));
parent.put("config", config);
parent.put("audio", audio);
...use HttpURLConnection to POST the message...
...get response with a BufferedReader object...
...queue text from response for command processing...
}
从文本到命令
命令处理将文本响应转发到 GAPI 对象,以确定下一步操作。如果 Google 找到了可识别的文本,则将其排队,以便 Jarvis 的 NLP 类通过 Apache OpenNLP 库将其转换为词性。这会将文本标记化为动词、名词、名称等的集合。NLP 类用于识别命令是否是针对 Jarvis 的(它必须包含该名称)
private boolean forJarvis(NLP nlp)
{
String[] words = nlp.getNames();
if ( words == null )
return false;
for(int i=0; i<words.length; i++)
{
if ( words[i].equalsIgnoreCase("jarvis") )
return true;
}
words = nlp.getNoun();
if ( words == null )
return false;
for(int i=0; i<words.length; i++)
{
if ( words[i].equalsIgnoreCase("jarvis") )
return true;
}
return false;
}
搜索针对 Jarvis 的请求中的各种格式和顺序的关键词,以识别命令。以下代码用于响应“你在那里吗”、“你醒了吗”、“你听到我说话吗”或“你能听到我说话吗”命令。Java String matches()
方法用于 glob 相关关键词,从而更容易找到命令的变体
private boolean isAwake(NLP nlp)
{
int i=0;
String[] token = nlp.getTokens();
boolean toJarvis = false;
String[] tag = nlp.getTags();
if ( tag != null )
{
String words = "are|do|can";
for(i=1; i<tag.length; i++)
{
if ( tag[i].startsWith("PR") )
{
if ( token[i].equalsIgnoreCase("you") )
{
if ( token[i-1].toLowerCase().
↪matches("("+words+")") )
{
toJarvis = true;
break;
}
}
}
}
}
if ( !toJarvis )
return false;
token = nlp.getVerbs();
if ( token != null )
{
String words = "listen|hear";
for(i=0; i<token.length; i++)
{
if ( token[i].toLowerCase()
↪.matches("("+words+").*") )
return true;
}
}
token = nlp.getAdverbs();
if ( token != null )
{
String words = "awake|there";
for(i=0; i<token.length; i++)
{
if ( token[i].toLowerCase()
↪.matches("("+words+").*") )
return true;
}
}
return false;
}
一旦找到命令,就可以将其转换为操作。Jarvis 的这部分仍在不断发展,但其目的是使用 REST API 与 PiBox 服务器联系以获取命令。PiBox 服务器将负责联系 IoT 设备(如电灯开关或窗帘)以执行适当的操作。
这种类型的编程响应效率极低。命令的顺序处理速度很慢,并且随着命令的增加只会变得更糟。但是,它可以用作对家庭自动化命令的简单支持的合理实现。
让你的作品发出声音在处理(或在远程 PiBox 服务器上排队处理)操作后,可以将语音响应排队。在上面的示例中,响应是“是的,我在这里。我可以帮助你吗?” 此文本放置在 Jarvis 的内部 Message 对象之一中,以便在 Speaker
线程上排队。
从 Message
对象中提取文本,并使用 espeak-ng
实用程序构建命令行。Espeak 是您可以从 Linux 发行版软件包管理系统中安装的软件包。在 Fedora 上,命令是
sudo dnf install espeak-ng
请注意,有原始的 espeak
和重新启动的 espeak-ng
。它们似乎为 Jarvis 产生相同的结果,并且具有相同的命令名称 (espeak
),因此两者都应该有效。
espeak
程序将文本作为输入并输出音频文件。然后读取该文件并使用 Java 的声音支持播放
String cmd = "espeak-ng -z -k40 -l1 -g0.8 -p 78 -s 215 -v mb-us1
↪-f " + messageFilename + " -w " + audioFilename;
Utils.runCmd(cmd);
file = new File(audioFilename);
InputStream in = new FileInputStream(audioFilename);
AudioStream audioStream = new AudioStream(in);
AudioPlayer.player.start(audioStream);
file.delete();
在此示例中,runCmd()
方法是 Java 进程管理的 Jarvis 包装器,用于简化外部命令的运行。
Jarvis 依赖于 Google 的 Cloud Speech API 将音频文件转换为文本。此 API 也需要 Google 提供的 API 密钥。由于此服务不是免费的,因此 Jarvis 允许开发人员将其自己的 API 密钥放置在 docs 目录中,构建系统将找到它。或者,如果您在不重建的情况下运行 Jarvis,只需将 apikey
放置在 ~/.jarvis 目录中即可。
此 DIY 系统并非完全安全。使用 Google Cloud Speech API 意味着通过互联网将音频文件传输到 Google,Google 会将其转换为文本。这意味着 Google 可以访问音频、转换后的文本以及两者的来源。如果您担心隐私,请务必在使用 Jarvis 之前考虑此问题。
本文中的源代码已从实际 Jarvis 代码中简化,仅为了解释目的。请注意,Jarvis 源代码虽然是用 Java 编写的,但并非旨在与 Maven 或在 Eclipse 中构建。构建系统是手动制作的,旨在从带有 Ant 的命令行运行。使用 ant jarvis
构建,ant jarvis.run
从构建工件运行,如果用于 Fedora、Red Hat 或 CentOS 系统,则使用 ant rpm
生成 RPM 文件。有关受支持目标的列表,请参阅 ant -p
。另请注意,Jarvis 是一个由 C 开发人员编写的 Java 程序。程序员请注意。
Jarvis 中的命令处理是编程的,这意味着这里没有人工智能。命令是从文本中解析出来的,并松散地分组,以允许命令的变体,例如“你是”、“你能”和“你是否”都指代相同的命令处理轨道。
应该扩展此机制以支持配置语言,以便可以在不重写和扩展代码或重新编译的情况下扩展命令。目前正在考虑与此相关的想法,但尚未开始设计或实施。
要使 Jarvis 超越简单的编程响应,需要与 AI 后端集成。Google 的 TensorFlow 项目提供了一个 Java API,是合乎逻辑的下一步。但是,对于基本的家庭自动化,集成 AI 有点过度工程化。目前没有在 Jarvis 中集成 TensorFlow 或其他 AI 的计划。
Jarvis 仍然只是一个实验,并且有很多限制。消息流经一系列线程会显着降低处理速度,并表现为响应和操作延迟。尽管如此,Jarvis 的未来在我的家中仍然光明。IronMan 项目将 Jarvis 与 PiBox 服务器集成,以将命令分发到 IoT 设备。很快,我就可以走进我的办公室说“Jarvis,开灯”。这确实是一个非常棒的主意。