HTML5-语音-API-入门指南-全-

HTML5 语音 API 入门指南(全)

原文:Introducing the HTML5 Web Speech API

协议:CC BY-NC-SA 4.0

一、入门指南

API 简介

“嘿 Alexa,几点了…?”

在一个智能助理(SA)设备的时代,我敢打赌,这些话在世界范围内每天都会被说上几十次——无论在哪里;智能助手变得非常受欢迎。事实上,Juniper Research 预测,智能助理的数量将从 2018 年底的 25 亿增加到 2023 年的 80 亿。想象一下——通过语音改变电视频道(已经成为可能,仅这一项就有望在未来五年内增长 120%)或者只是做一些平凡的事情,比如从亚马逊之类的网站重新订购商品。

但是我跑题了。智能助手很棒,但是如果我们可以用它们来控制我们在线网站或应用的功能会怎么样呢?“怎么会?”我听到你问了。好吧,我来介绍一下 HTML5 语音 API 它使用与智能助手相同的原理,将语音转换为文本,反之亦然。它现在可以在浏览器中使用,尽管还有些实验性。

最初创建于 2012 年,但现在才真正全面投入使用,这种实验性的 API 可以通过语音的力量来执行各种不同的任务。如何使用它来将产品添加到购物车并支付它们——所有这些都通过语音远程完成?加入语音功能为我们打开了一些真正的可能性。在本书的整个过程中,我们将详细探索其中的一些,向您展示我们如何很好地使用这个 API。在我们这样做之前,有一点家务我们必须先处理。在我们继续这个 API 的旅程之前,让我们先把这个覆盖掉。

如果你想深入了解这个 API 是如何构建的,以及浏览器厂商必须遵循的标准,那么看看 W3C 在 https://wicg.github.io/speech-api/ 为这个 API 制定的指导方针。当心——这会导致枯燥无味的阅读!

设置我们的开发环境

我很确定没有人喜欢 admin,但是在这个例子中,在使用 API 之前,我们必须执行几个任务。

别担心,它们很简单。这是我们需要做的:

  • 该 API 只能在安全的 HTTPS 环境中工作(是的,甚至不要尝试在 HTTP 下运行它——它不能工作——这意味着我们需要一些安全的网络空间来用于我们的演示。有几种方法可以实现这一点:

    • 最简单的是使用 CodePen(https://www.codepen.io)——你将需要创建一个帐户来保存工作,但如果你还没有一个可以使用的帐户,注册是免费的。

    • 你有其他项目可以临时使用的网络空间吗?只要它能在 HTTPS 的统治下得到保障,那么这将对我们的演示起作用。

    • 如果您碰巧是使用 MS tech stack 的开发人员,您可以创建一个 ASP.Net 核心 web 应用,选择“为 HTTPS 配置”,并在运行该应用时,在提示信任自签名证书时单击“确定”。这将很好地适用于本书中的演示。

    • 你可以试着运行一个本地的网络服务器——网上有很多。我个人最喜欢的是 MAMP PRO,可从 https://www.mamp.info 买到。这是一个在 Windows 和 Mac 上运行的付费选项;这使得生成我们需要使用的 SSL 证书变得轻而易举。或者,如果您安装了 Node.js 之类的程序,那么您可以使用一个本地 web 服务器( https://github.com/lwsjs/local-web-server ),或者创建自己的程序。您需要为它创建一个证书,并将其添加到您的证书库中——创建证书的简便方法在 https://bit.ly/30RjAD0 中有所介绍。

  • 下一个重要任务是准备一个合适的麦克风——毕竟,没有麦克风我们显然走不远!你可能已经有一个了;如果没有,几乎任何麦克风都可以正常工作。我个人倾向于使用麦克风/耳机组合,就像通过 Skype 通话一样。你应该可以通过亚马逊或者当地的音像店买到相对便宜的。

注意一点如果你是笔记本电脑用户,那么你可以使用笔记本电脑内置的任何麦克风。缺点是接收效果不会很好——你可能会发现自己不得不非常前倾才能获得最好的接收效果!

  • 对于我们所有的演示,我们将使用一个中心项目文件夹——出于本书的目的,我将假设您已经创建了一个名为 speech 的文件夹,它存储在您的 C: drive 的根目录下。确切的位置并不重要;如果您选择了不同的位置,那么当我们来完成演示时,您需要相应地调整位置。

太棒了。现在没有了管理员,我们可以专注于有趣的事情了!HTML5 Speech API(或“API”)由两部分组成:第一部分是 SpeechSynthesis API ,它负责将任何给定的文本作为语音复述出来。第二,相比之下——套用一句话——演讲识别 API 做的和它名字里说的差不多。我们可以说出一个短语,如果它与预先配置好的文本相匹配,它就可以执行我们在收到该短语时分配的任何数量的任务。

我们可以深入了解它们是如何工作的,但我知道你渴望深入了解,对吗?绝对的。所以事不宜迟,让我们快速演示两次,这样在本书后面的项目中使用 API 之前,您就可以了解它是如何工作的。

不要担心这意味着什么——我们绝对会在每次练习后详细研究代码!我们将依次研究这两者,首先从 SpeechSynthesis API 开始。

实现我们的第一个示例

尽管这两种 API 都需要一点配置才能工作,但它们相对容易设置;两者都不需要使用任何特定的框架或外部库来进行基本操作。

为了理解我的意思,我用 CodePen 做了两个快速演示——它们演示了入门所需的基础知识,并将形成我们将在本书后面的项目中使用的代码。让我们依次看一下每一个,从使用 SpeechSynthesis API 将文本作为语音读回开始。

将文本作为语音回读

我们的第一个练习将保持简单,并使用 CodePen 来托管我们的代码;为此,如果您想保存您的工作以供将来参考,您需要创建一个帐户。如果你以前没有用过 CodePen,那么不要担心——它是免费注册的!这是开始使用 API 的好方法。在随后的演示中,我们将使用一些更具本地特色的东西。

本书示例中使用的所有代码都可以在本书附带的代码下载中找到。我们将在大多数演示中混合使用 ECMAScript 2015 和普通 JavaScript 如果您想使用 ECMAScript 的新版本,可能需要进行调整。

Reading Back Text

假设你已经注册了,现在有一个可以使用的 CodePen 帐户,让我们开始创建我们的第一个例子:

img/490753_1_En_1_Fig1_HTML.jpg

图 1-1

我们完整的文本到语音转换演示

  1. 首先,打开你的浏览器,然后导航到 https://codepen.io ,用你的账户信息登录。完成后,点击左边的笔来创建我们的演示。

  2. 我们需要为这个演示添加标记——为此,继续将以下代码添加到 HTML 窗口:

    <link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
    
    <div id="page-wrapper">
      <h2>Introducing HTML5 Speech API: Reading Text back as Speech</h2>
      <p id="msg"></p>
      <input type="text" name="speech-msg" id="speech-msg">
      <div class="option">
        <label for="voice">Voice</label>
        <select name="voice" id="voice"></select>
        <button id="speak">Speak</button>
      </div>
    </div>
    
    
  3. 如果我们现在运行它,我们的演示将看起来非常普通——更不用说它实际上不会像预期的那样工作了!我们可以轻松解决这个问题。让我们首先添加一些基本的风格,使我们的演示更像样。有几个样式要添加进去,所以我们将一个块一个块地做。在每个块之间留一条线,当你把它加入到演示中:

    *, *:before, *:after { box-sizing: border-box; }
    
    html { font-family: 'Open Sans', sans-serif; font-size: 100%; }
    
    #page-wrapper { width: 640px; background: #ffffff; padding: 16px; margin: 32px auto; border-top: 5px solid #9d9d9d; box-shadow: 0 2px 10px rgba(0,0,0,0.8); }
    
    h2 { margin-top: 0; }
    
    
  4. 我们需要添加一些样式来表明我们的浏览器是否支持 API:

    #msg { font-size: 14px; line-height: 22px; }
    #msg.not-supported strong { color: #cc0000; }
    #msg > span { font-size: 24px; vertical-align: bottom; }
    #msg > span.ok { color: #00ff00; }
    #msg > span.notok { color: #ff0000; }
    
    
  5. 接下来是声音下拉菜单的样式:

    #voice { margin: 0 70px 0 -70px; vertical-align: super; }
    
    
  6. 对于 API 来说,我们需要有一种输入文本的方法来将它转换成语音。为此,添加以下样式规则:

    input[type="text"] { width: 100%; padding: 8px; font-size: 19px;
    border-radius: 3px; border: 1px solid #d9d9d9; box-shadow: 0 2px 3px rgba(0,0,0,0.1) inset; }
    
    label { display: inline-block; float: left; width: 150px; }
    
    .option { margin: 16px 0; }
    
    
  7. 样式的最后一个元素是演示右下角的 Speak 按钮:

    button { display: inline-block; border-radius: 3px; border: none; font-size: 14px; padding: 8px 12px; background: #dcdcdc;
    border-bottom: 2px solid #9d9d9d; color: #000000; -webkit-font-smoothing: antialiased; font-weight: bold; margin: 0; width: 20%; text-align: center; }
    
    button:hover, button:focus { opacity: 0.75; cursor: pointer; }
    button:active { opacity: 1; box-shadow: 0 -3px 10px rgba(0, 0, 0, 0.1) inset; }
    
    
  8. 有了合适的样式,我们现在可以把注意力转移到添加胶水上了。我不是指字面上的意思,而是比喻意义上的!我们需要添加的所有代码都在 CodePen 的 JS 窗口中;我们首先检查我们的浏览器是否支持 API:

    var supportMsg = document.getElementById('msg');
    
    if ('speechSynthesis' in window) {
      supportMsg.innerHTML = '<span class="ok">☑</span> Your browser <strong>supports</strong> speech synthesis.';
    } else {
      supportMsg.innerHTML = '<span class="notok">☒</span> Sorry your browser <strong>does not support</strong> speech synthesis.';
      supportMsg.classList.add('not-supported');
    }
    
    
  9. 接下来,我们定义三个变量来存储对演示中元素的引用:

    var button = document.getElementById('speak');
    var speechMsgInput = document.getElementById('speech-msg');
    var voiceSelect = document.getElementById('voice');
    
    
  10. 当使用 API 时,我们可以使用各种不同的声音来回放语音——我们需要在使用它们之前将它们加载到我们的演示中。为此,继续添加以下几行:

```html
function loadVoices() {
  var voices = speechSynthesis.getVoices();

  voices.forEach(function(voice, i) {
    var option = document.createElement('option');
    option.value = voice.name;
    option.innerHTML = voice.name;
    voiceSelect.appendChild(option);
  });
}

loadVoices();

window.speechSynthesis.onvoiceschanged = function(e) {
  loadVoices();
};

```
  1. 我们开始演示的真正内容——这是我们看到我们添加的文本被转换成语音的地方!为此,在前一个块之后留下一行,并添加以下代码:
```html
function speak(text) {
  var msg = new SpeechSynthesisUtterance();
  msg.text = text;

  if (voiceSelect.value) {
    msg.voice = speechSynthesis.getVoices()
.filter(function(voice) {
      return voice.name == voiceSelect.value;
      })[0];
  }

  window.speechSynthesis.speak(msg);
}

```
  1. 我们快到了。最后一步是添加一个事件处理程序,当我们点击 Speak 按钮时,这个事件处理程序触发从文本到语音的转换:
```html
button.addEventListener('click', function(e) {
  if (speechMsgInput.value.length > 0) {
    speak(speechMsgInput.value);
  }
});

```
  1. 继续保存您的工作。如果一切正常,我们应该会看到类似于图 1-1 所示的截图。

试着输入一些文本,然后点击语音按钮。如果一切按预期进行,那么你会听到有人向你复述你的话。如果你从下拉列表中选择一个声音,你会听到你的话带着口音说回来;根据你输入的内容,你会得到一些非常有趣的结果!

这个演示的完整版本可以在本书附带的代码下载中找到——它在readingback文件夹中。

在这个阶段,我们现在已经有了基本的设置,允许我们的浏览器读回我们想要的任何文本——当然这听起来可能还是有点机械。然而,当使用一个仍然是实验性的 API 时,这是可以预料的!

除此之外,我敢打赌你的脑海中有两个问题:这个 API 是如何工作的?更重要的是,即使它在技术上仍然是一个非官方的 API,它仍然可以安全使用吗?不要担心——这些问题以及更多问题的答案将在本章稍后揭晓。让我们首先更详细地探讨一下我们的演示是如何工作的。

了解发生了什么

如果我们仔细看看我们的代码,你可能会觉得它看起来有点复杂——但实际上,它非常简单。

我们从一些简单的 HTML 标记和样式开始,在屏幕上显示一个输入框,用于播放内容。我们还有一个下拉菜单,我们将使用它来列出可用的声音。真正神奇的事情发生在我们使用的脚本中——首先执行检查,看看我们的浏览器是否支持 API,并显示合适的消息。

假设您的浏览器支持 API(过去 3-4 年的大多数浏览器都支持),那么我们为页面上的各种元素定义一些占位符变量。然后,在用结果填充下拉菜单之前,我们(通过loadVoices()函数)遍历可用的声音。特别值得注意的是对loadVoices()的第二次调用;这是必要的,因为 Chrome 异步加载它们。

值得注意的是,额外的声音(以“Chrome…”开头)是作为与谷歌交互的 API 的一部分添加的,因此只出现在 Chrome 中。

如果我们跳到演示的结尾,我们可以看到按钮元素的事件处理程序;这将调用speak()函数,该函数创建一个新的SpeechSynthesisUtterance()对象的发声,作为一个发言请求。然后它会检查以确保我们选择了一种声音,这是通过使用speechSynthesis.getVoices()功能完成的。如果选择了一种声音,那么 API 会将该声音排队,并通过 PC 的扬声器以音频的形式呈现出来。

好吧,我们继续。我们已经探索了如何将文本呈现为语音的基础。然而这只是故事的一半。如何将口头内容转换成文本?我们可以通过使用 SpeechRecognition API 来做到这一点——这需要更多一点的努力,所以让我们进入两个演示中的第二个,看看让我们的笔记本电脑说话涉及到什么。

将语音转换为文本

通过我们电脑的扬声器(甚至是耳机)来表达内容的能力当然是有用的,但是有一点局限性。如果我们可以让浏览器使用我们的声音来执行一些事情,会怎么样?我们可以使用两个语音 API 中的第二个来实现。我来介绍一下 SpeechRecognition API!

这个姊妹 API 允许我们对着任何连接到 PC 的麦克风说话,让我们的浏览器执行任何方式的预配置任务,从简单的转录任务到搜索离您给定位置最近的餐馆。我们将在本书的后面探索如何在项目中使用这个 API 的一些例子,但是现在,让我们实现一个简单的演示,这样您就可以看到 API 是如何工作的。

当处理使用语音识别 API 的演示时,我不推荐使用 Firefox 尽管 Mozilla Developer Network (MDN)站点上的文档表明它是受支持的,但事实并非如此,您很可能会在控制台日志中出现“speech recognition is not a constructor”错误。

“What Did I Say?”

让我们继续下一个练习:

  1. 我们首先浏览到 https://www.codepen.io ,然后点击笔。确保您已经使用在第一个练习中创建的帐户登录。

  2. 我们的演示使用了字体 Awesome 作为麦克风图标,您将很快看到它的使用——为此,我们需要添加两个 CSS 库的引用。继续并点击设置➤ CSS。然后在对话框底部的备用槽中添加以下链接:

    https://use.fontawesome.com/releases/v5.0.8/css/fontawesome.css
    https://use.fontawesome.com/releases/v5.0.8/css/solid.css
    
    
  3. 接下来,切换到 HTML 窗格,添加以下标记,这将构成我们演示的基础:

    <link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
    
    <div id="page-wrapper">
      <h2>Introducing HTML5 Speech API: Converting Speech to Text</h2>
    
      <button>
        <i class="fa fa-microphone"></i> Click and talk to me!
      </button>
      <div class="response">
        <span class="output_log"></span>
      </div>
    
      <p class="output">You said: <strong class="output_result"></strong></p>
      <span class="voice">Spoken voice: US English</span>
    </div>
    
    
  4. 就其本身而言,我们的标记肯定不会赢得任何风格方面的奖项!为了解决这个问题,我们需要添加一些样式,使我们的演示看起来像样。为此,将以下规则添加到 CSS 窗格中,从一些基本规则开始,为我们的演示设置容器的样式:

    *, *:before, *:after { box-sizing: border-box; }
    html { font-family: 'Open Sans', sans-serif; font-size: 100%; }
    
    #page-wrapper { width: 640px; background: #ffffff; padding: 16px; margin: 32px auto; border-top: 5px solid #9d9d9d; box-shadow: 0 2px 10px rgba(0,0,0,0.8); }
    
    h2 { margin-top: 0; }
    
    
  5. 接下来是我们需要设计通话按钮的规则:

    button { color: #0000000; background: #dcdcdc; border-radius: 6px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); font-size: 19px; padding: 8px 16px; margin-right: 15px; }
    button:focus { outline: 0; }
    
    input[type=text] { border-radius: 6px; font-size: 19px; padding: 8px; box-shadow: inset 0 0 5px #666; width: 300px; margin-bottom: 8px; }
    
    
  6. 我们的下一个规则利用字体 Awesome 在通话按钮上显示一个合适的麦克风图标:

    .fa-microphone:before { content: "\f130"; }
    
    
  7. 一旦输出被转录,下一组规则将对输出进行样式化,以及置信度和所使用的声音特征:

    .output_log { font-family: monospace; font-size: 24px; color: #999; display: inline-block; }
    .output { height: 50px; font-size: 19px; color: #000000; margin-top: 30px; }
    
    .response { padding-left: 260px; margin-top: -35px; height: 50px}
    .voice { float: right; margin-top: -20px; }
    
    
  8. 好了,我们有了标记,看起来还不错。少了什么?啊,是的,让这一切工作的脚本!为此,继续将以下代码添加到 JS 窗格中。我们有一大块代码,所以让我们从一些变量声明开始,一个块一个块地分解它:

    'use strict';
    
    const log = document.querySelector('.output_log');
    const output = document.querySelector('.output_result');
    
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognition();
    
    recognition.interimResults = true;
    recognition.maxAlternatives = 1;
    
    
  9. 接下来是触发麦克风的事件处理程序。留下一个空行,然后添加以下代码:

    document.querySelector('button').addEventListener('click', () => {
      let recogLang = 'en-US';
      recognition.lang = recogLang.value;
      recognition.start();
    });
    
    
  10. 当使用语音识别 API 时,我们触发了许多我们必须响应的事件;第一个识别我们何时开始说话。继续将下面几行添加到我们的 CodePen 演示的 JS 窗格中:

```html
recognition.addEventListener('speechstart', () => {
  log.textContent = 'Speech has been detected.';
});

```
  1. 留下一个空行,然后添加这些行——这个事件处理程序负责识别和记录我们对麦克风说的任何话,并计算准确性的置信水平:
```html
recognition.addEventListener('result', (e) => {
  log.textContent = 'Result has been detected.';

  let last = e.results.length - 1;
  let text = e.results[last][0].transcript;

  output.textContent = text;

  log.textContent = 'Confidence: ' + (e.results[0][0].confidence * 100).toFixed(2) + "%";
});

```
  1. 我们差不多完成了,但是还需要添加两个事件处理程序——它们负责在我们完成时关闭识别 API,并在屏幕上显示任何可能出现的错误。留下一行然后放入下面的代码:
```html
recognition.addEventListener('speechend', () => {
  recognition.stop();
});

recognition.addEventListener('error', (e) => {
  output.textContent = 'Error: ' + e.error;
});

```
  1. 至此,我们完成了代码编辑。继续并点击保存按钮来保存我们的工作。

这个演示的完整版本可以在本书附带的代码下载中找到——它在whatdidIsay文件夹中。

在这一点上,我们应该可以运行我们的演示了,但是如果您这样做,很可能您不会得到任何响应。怎么会这样原因很简单,我们必须在浏览器中授权使用我们电脑的麦克风。可以通过网站证书细节中的设置条目来激活它,但这不是最干净的方法。有一种更好的方法来提示访问,我将在下一个练习中演示。

允许使用麦克风

使用 Speech API 时,有一件事我们必须记住——出于安全原因,默认情况下对麦克风的访问是禁用的;在使用它之前,我们必须明确地启用它。

这很容易做到,尽管具体步骤会因浏览器而异——它涉及到在我们的演示中添加几行代码来请求访问麦克风,并根据提示更改设置。我们将在下一个练习中看到如何做到这一点,假设您使用 Chrome 作为浏览器。

Adjusting Permissions

让我们开始设置权限:

  1. 点击它。确保选择了“总是允许https://codepen.io……”选项。然后单击完成。

  2. 刷新窗口。图标将变为纯黑色,不显示禁止的十字符号。

  3. 首先,在 Chrome 中浏览麦克风设置,你可以通过chrome://settings/content/microphone进入。确保“访问前询问…”的滑块位于右侧。

  4. 在单独的选项卡中,切换回您在上一个练习中创建的 CodePen 中的 SpeechRecognition API 演示。寻找这一行:

    const output = document.querySelector('.output_result');
    
    
  5. 在它下面留一行空白,然后加入这个代码:

    navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
    
    
  6. 向下滚动代码,直到到达末尾。然后添加这一小块代码:

      })
    .catch(function(err) {
      console.log(err);
    });
    
    
  7. 接下来,单击 JS 窗格最右边的下拉箭头。当它弹出的时候,你会看到一个关于 Tidy JS 的条目。单击它可以正确地重新格式化代码。

  8. Save the update and then refresh the page. If all is well, you will see an icon appear at the end of the address bar (Figure 1-2).

    img/490753_1_En_1_Fig2_HTML.jpg

    图 1-2

    麦克风支持已被添加,但被禁用…

尝试点击“点击和我说话!”按钮,然后对着麦克风说话。如果一切正常,我们应该会看到类似于图 1-3 所示的屏幕截图,其中显示了口语测试短语的结果,以及置信度。

img/490753_1_En_1_Fig3_HTML.jpg

图 1-3

对着我们的麦克风说话的结果…

交谈时,您是否注意到红点/圆圈是如何出现在浏览器窗口选项卡中的(如背页图 1-4 所示)?这表示麦克风处于活动状态,将录制任何语音。

img/490753_1_En_1_Fig4_HTML.jpg

图 1-4

红点表示活跃的麦克风

如果我们点击“点击和我说话!”按钮,这个红色的圆形图标将会消失,表示麦克风已经关闭。在我们之前的演示中,我们利用了navigator.mediaDevices.getUserMedia()来实现这一点——这是我们在任何实现语音的站点都必须做的事情,因为我们不能确定用户是否已经启用了他们的麦克风!

如果你想了解更多关于使用navigator.mediaDevices.getUserMedia()的知识,在 Mozilla 开发者网站 https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 上有一篇有用的文章。

设置访问:一种替代方法

然而(就像生活中的许多事情一样),有一种不同的方式来破解这个难题;它不需要代码,但是它不是一个干净的方法。这涉及到像我们以前一样设置正确的权限,但这次是去一个我们知道可以使用麦克风的站点。

Enabling the Microphone: An Alternative Method

这种方法假设使用 Chrome,尽管 Firefox 和其他浏览器也可能使用类似的方法:

img/490753_1_En_1_Fig5_HTML.jpg

图 1-5

从浏览器请求访问麦克风

  1. 在单独的选项卡中,浏览到chrome://settings/content/siteDetails?site=https%3A%2F%2Fcodepen.io,然后确保麦克风的条目设置为“询问”。

  2. 返回到运行 CodePen 演示的选项卡,并刷新窗口。您应该会看到一个提示,如图 1-5 所示。

在最后几页中,我们做了三个练习。重要的是要注意,当使用这个 API 时,需要做一些额外的工作。除了发起对 API 的请求之外,我们还必须添加代码来启用对麦克风的访问。稍后我们将再次讨论使用它的主题(和安全含义),但是现在,让我们更详细地回顾一下我们在前两个练习中使用的代码。

打破我们的代码

和前面的文本到语音转换演示一样,我们从一些基本的标记和样式开始,给我们一个按钮,我们可以用它来激活记录我们的声音,以及两个占位符槽,用于转换后的文本和置信度。

然而,神奇之处在于我们添加的 JavaScript 代码。我们首先在标记中定义对.output_元素的引用。接下来,我们定义window.SpeechRecognition``as a reference to the API;请注意,我们将它设置为 OR 语句,以确保涵盖那些仍然需要供应商前缀支持的浏览器。作为其中的一部分,我们还设置了两个属性:recognition.interimResults设置为 true,以便在从语音转换成文本时显示文本。另一个是recognition.maxAlternatives,设置为 1,当语音识别服务识别出它时,最多显示一个备选单词。

值得注意的是,我们的大部分 JavaScript 代码将被封装在一个navigator.mediaDevices.getUserMedia()块中,所以一旦我们启用了对麦克风的访问,它就会运行。

然后,我们的代码包含一组事件处理程序,以识别不同的事件:第一个事件是通过单击“单击并和我说话!”按钮。这将设置要使用的语言(美国英语)并启动识别服务。第二个事件处理程序speechstart,负责识别我们何时开始说话,并记录任何说话的内容。最后两个(resulterror)在我们停止讲话或出现错误时被触发,比如对麦克风的访问被阻止。在这个扩展演示的最后部分,我们将探索启用麦克风的几个选项;我们讨论代码路径如何对用户更好。

好吧,我们继续。现在我们已经了解了这两个 API,是时候深入研究一些理论了,看看这些 API 是如何工作的!我们将在下一章更详细地检查每个 API,但是现在,让我们回答两个关键问题:这些 API 支持得如何(我能提供后备支持吗)?如何管理远程访问他人麦克风的安全隐患?

允许浏览器支持

回想一下本章的开头——还记得我提到的“实验 API”这个词吗?是的,不得不说,这些 API 还没有达到官方的地位。然而,在你跑到山上思考“我让自己进来是为了什么?”,没有听起来那么糟糕!让我解释一下我的意思。

诚然,该 API 仍处于试验阶段——我们可以通过仔细的研究来考虑这一点,并在我们只是增强现有服务而不是取代它的基础上工作。首先,我们的第一站应该是像 CanIUse.com 这样的网站;快速检查表明,SpeechSynthesis API 具有出色的支持,至少在桌面上是如此(图 1-6 )。

img/490753_1_En_1_Fig6_HTML.jpg

图 1-6

语音合成 API 的浏览器支持

相比之下,对语音识别 API 的支持就不那么先进了,如图 1-7 所示。

img/490753_1_En_1_Fig7_HTML.jpg

图 1-7

浏览器对语音识别 API 的支持

来源:cani use . com/# search = speech

我们可以清楚地看到,对语音合成 API 的支持没有那么先进,但是图表隐藏了一个秘密:Safari 确实支持这两种 API!虽然像 CanIUse.com 这样的网站是一个很好的起点,但它的准确性取决于它所基于的信息。尽可能多地检查每个浏览器供应商的支持确实是值得的;否则,我们可能会将未来的财务信息建立在不准确的信息上。

现在,我听到你问,“移动呢?”对这两种 API 的支持仍在开发中;虽然它尚未扩展到所有平台,但它涵盖了 Android(Chrome 和 Firefox)和三星的主要平台。

既然我们知道了每个浏览器提供的支持级别,那么那些不支持任何一种 API 的浏览器呢?有没有后备方案或我们可以使用的其他替代方案…?

提供后备支持

在讨论结束时,这两个问题的答案并不像我们希望的那样简单。让我解释一下我的意思。

这个问题的核心在于重要的一点——语音合成 API 依赖于使用谷歌的神经人工智能能力来解码文本,并以选定的风格将文本作为语音返回。那么这对我们意味着什么呢?对谷歌合成 API 的依赖意味着支持将仅限于较新的浏览器;这涵盖了除 IE 之外的所有最新的桌面浏览器。对于那些支持移动设备的人来说,它只适用于 Android 的 Chrome 或 Firefox,三星互联网和两个较小的专业浏览器。

目前,从严格意义上来说,确实没有合适的退路。虽然对一些人来说这可能令人失望,但有一种观点认为,在浏览器支持方面,我们应该向前看,而不是向后看。IE 不支持语音合成 API 对很多人来说并不奇怪;那些不支持该 API 的移动平台(如 Android 浏览器)加起来占总使用量的 5%左右,因此这可以放心地打折扣。同样有一种观点认为,我们不应该依赖 API 来获得我们站点或应用的核心功能;语音提供应该增强基本服务,而不是取代它。

如果我们切换到语音识别 API,支持就是另一回事了——支持仍然处于初级阶段。它仅限于桌面支持的 Edge、Firefox 和 Chrome 的最新版本;移动世界的大部分支持都落在了 Android 平台的 Chrome 上。同样的观点也适用于展望未来;诸如语音之类的 API 应该被看作是一种逐渐增强体验的工具。

谈到渐进式改进,我们可以考虑几个选项。这些是

  • responsive voice–这是一项商业服务,可从 https://responsivevoice.com 获得;它提供了额外的支持,如更好的导航可访问性,但这需要每月 39 美元的价格,这将需要考虑到任何运营成本。

  • Annyang 是 Tal Ater 的免费库,旨在让语音识别 API 更容易使用;这是在麻省理工学院许可下从 https://www.talater.com/annyang/ 获得的。

但是这些方法的缺点是它们只能逐步增强已经支持 API 的浏览器所提供的服务;这为我们应该鼓励人们尽可能使用更新的浏览器的观点增加了额外的份量!

了解安全问题

在本章的过程中,我们已经第一次了解了 Speech API,并了解了它的基本工作原理。然而,我确信(和任何新技术一样)有一个迫切的问题我们还没有问:安全和隐私呢?随着现在生效的全欧洲范围的 GDPR 立法的存在,隐私问题已经成为突出的问题;这并不比使用语音 API 更重要。

主要考虑的是在使用语音 API 时获得使用麦克风的许可;过去,每当在不安全的 HTTP 环境中发出请求时,都会出现这种情况。曾几何时,这是不必要的,但可疑的网站开始利用广告和诈骗。因此,谷歌(现在还有其他公司)强制要求在 HTTPS 安全的环境中使用 API,并且使用麦克风的许可必须由用户明确给出。

如果您想了解这方面的技术原因,详细信息请参见关于此漏洞的官方错误报告,该报告列在 https://bugs.chromium.org/p/chromium/issues/detail?id=812767

作为一个用户,在一个完全安全的网站上,在音频被捕获之前,你可能只被要求一次许可;在页面刷新之前,同一会话中的后续使用将使用相同的权限。对于一些人来说,这可能被视为一个漏洞,因为一个安全的网页可以有效地记录任何内容,一旦它被授权。这是由于 Chrome API 与谷歌交互的事实,所以不会停留在你的浏览器范围内!

那么,我们能做些什么来帮助维护我们的安全和隐私呢?使用 API 时,我们需要记住几件事:

  • 尽管 Chrome 中使用语音识别 API 的任何页面都可以与谷歌进行交互,但发送给谷歌的唯一信息是音频记录、网站的域、默认浏览器语言和网站的当前语言设置(不发送 cookies)。如果在不同的浏览器中使用语音识别 API,它不会与 Google 交互。

  • 如果您正在使用语音识别 API,请确保您没有创建任何包含敏感信息的事件处理程序,这些信息可能会被发送到 Google。理想情况下,这些信息应该存储在本地,发送的任何命令实际上都是打开访问的钥匙。

  • 整个页面都可以访问音频捕获的输出,因此,如果您的页面或站点受到威胁,可以读取音频实例中的数据。这使得我们有责任确保访问安全(这已经成为许多网站的默认设置),同时也确保我们在适当安全和更新的服务器上使用高质量的证书。

  • API(尤其是语音识别 API)仍然处于不断变化的状态;谷歌的角色有可能在未来某个时候发生变化或被终止。在 W3C 正式认可在浏览器中使用 API 之前,任何东西都不能被认为是官方的。

  • 此时,我建议仔细检查你网站的分析,探索哪些浏览器支持这个 API。如果有足够的需求,那么你可以考虑开始添加功能,但正如前面提到的,我强烈建议采取谨慎和有分寸的方法,以便为客户保持良好的体验。

好吧,确实有些值得思考的东西!希望这不会让你分心;与任何新技术一样,拥抱它很重要,但要采取有分寸的方法,而不是盲目地投入!在本书的整个过程中,我们将更详细地挖掘 API,并在许多示例项目中使用它,这样您就能感受到它在实际环境中的用法。想象一下:使用 API 将产品添加到购物车中并为其付款,而这一切都用您的声音来完成,怎么样?

摘要

在现代智能助手(如亚马逊的 Alexa)的时代,创建可以使用语音控制的网络应用的能力开辟了一些真正有趣的可能性。我们同样必须考虑如何最好地利用 API,尤其是在用户最关心隐私的时候!在本章的过程中,我们已经开始详细了解语音 API 让我们花点时间更详细地回顾一下我们所学的内容。

我们首先介绍了语音合成和识别 API,然后快速看一下开始使用这些 API 进行开发需要什么。

然后,我们继续实现我们的第一个例子——我们从读回文本作为语音开始,然后切换到使用语音识别 API 创建一个例子。然后,我们简要讨论了如何为这两个 API 中的第二个启用对麦克风的访问,然后探讨了在使用 API 时提供支持以及考虑隐私和安全的一些问题。

唷!伙计们,我们才刚刚开始。希望你已经准备好真正投入到细节中!接下来,我们将更详细地了解 API,同时创建一个更实用的示例,并探索如何为不同的语言提供更多的支持。就像有人用荷兰语说的那样,Laten we doorgaan,或者让我们继续干吧!

二、更详细地探索 API

理解 API 术语

“太好了!我的电脑现在可以说话并识别我的声音。但是我在代码中看到的 SpeechSynthesisUtterance 关键字是什么意思…?”

这是个很好的问题。既然您已经看到了运行中的 API,我敢打赌您一定很想了解它是如何结合在一起的,对吗?我们只是触及了让我们的电脑说话或识别我们声音的基础。我们能做的还有很多!

在本章的课程中,我们将在使用它(或它们——取决于你如何看待它)之前,深入研究 API 背后的一些理论,以创建一些更实用的东西。与此同时,我们还将赋予我们的代码一点国际风味——是的,我们不局限于只说英语!在本章的后面,一切将变得更清楚,但是现在,让我们从把语音识别 API 分解成它的组成部分开始。

探索语音合成 API

回头看看我们为第一个演示创建的代码,我们的 PC 将一些示例文本作为语音回放。乍一看,似乎我们需要相当多的代码来实现这一点,对吗?如果我说你只用一行代码就能做到这一点,会怎么样?

是的,你没听错。该演示的关键围绕这一行代码:

window.speechSynthesis.speak(msg);

在这里我们调用对speechSynthesis的调用,并要求它说出msg的值。就其本身而言,这是行不通的,但是如果我们稍微改变一下,变成这样:

speechSynthesis.speak(new SpeechSynthesisUtterance('Hello, my name is [insert your name here]'))

当在浏览器控制台中执行时,它会工作得很好(如果你使用 Firefox,你可能需要允许在控制台中粘贴)。继续,把你的名字放进去,试一试!然而,除了这个简单的一行程序之外,我们还可以用 API 做更多的事情。我在代码中看到的这个SpeechSynthesisUtterance()或者对getVoices()的调用是怎么回事?一个是对象,另一个是方法。让我们更深入地了解一下这个 API 是如何工作的。

分解 API

语音识别 API 的核心是语音合成接口;这是我们进入语音服务的界面。我们可以使用许多方法来控制活动,但在此之前,我们必须首先定义SpeechSynthesisUtterance对象。这个对象代表一个语音请求,我们向其中传递一个字符串,浏览器应该大声读出:

const utterance = new SpeechSynthesisUtterance('Hey')

一旦定义好了,我们就可以用它来调整单个的语音属性,比如表 2-1 中列出的那些,更完整的列表在本书后面的附录中。

表 2-1

SpeechSynthesisUtterance 对象的属性

|

财产

|

目的

|
| --- | --- |
| 话语速率 | 设置速度,接受[0.1 和 10]之间的值,默认为 1。 |
| 话语.音调 | 设置间距,接受[0 和 2]之间的值,默认为 1。 |
| 话语量 | 设置音量,接受[0 和 1]之间的值,默认为 1。 |
| 话语.郎 | 设置语言(值使用当前最佳实践 47 [BCP47]语言标记,如 en-US 或 it-IT)。 |
| 话语.文本 | 您可以将它作为属性传递,而不是在构造函数中设置它。文本最多可包含 32767 个字符。 |
| 话语声音: | 设置声音(下面会详细介绍)。 |

如果我们把这些放在一个简单的例子中,我们可以在一个控制台会话中运行(不要忘记按照指示添加我们的名字!),它看起来会像这样:

const utterance = new SpeechSynthesisUtterance('Hey, my name is [insert your name here]')
utterance.pitch = 1.5
utterance.volume = 0.5
utterance.rate = 8
speechSynthesis.speak(utterance)

然后我们可以使用speak(), pause(), resume(),cancel()方法来控制SpeechSynthesis对象。

在我们的下一个练习中,我们将充分利用这个额外的功能,并扩展我们从第一章开始的原始演示,以包括对返回的语音提供更好控制的选项,作为我们的下一个演示。当我们完成后,我们的演示将看起来像图 2-1 所示的截图。

img/490753_1_En_2_Fig1_HTML.jpg

图 2-1

我们更新的语音合成演示,增加了控件

我们要做的改变相对来说比较简单,但是很好的说明了我们如何开始开发我们的原创。让我们更详细地了解一下需要什么。

改进我们的演讲合成演示

在我们的下一个练习中,我们将添加三个滑块来控制音量、音高和速率等级别,以及暂停和继续朗读内容的按钮。

Adding Functionality

让我们开始添加演示所需的额外标记:

本演示所需的所有代码都在本书附带的代码下载中的updating speechsynthesis文件夹中。

  1. 我们将从浏览回你在 CodePen 中创建的演示开始,回到第一章——在那里,确保你登录,这样我们可以保存对演示的更改。

  2. 首先,寻找这段代码:

    <div class="option">
      <label for="voice">Voice</label>
      <select name="voice" id="voice"></select>
      <button id="speak">Speak</button>
    </div>
    
    
  3. 紧接在这个块的下面(并且在关闭页面包装器<div>之前),插入下面的代码——这增加了音量、速率和音调水平的滑块:

    <div class="option">
      <label for="volume">Volume</label>
      <input type="range" min="0" max="1" step="0.1" name="volume" id="volume" value="1">
    </div>
    <div class="option">
      <label for="rate">Rate</label>
      <input type="range" min="0.1" max="10" step="0.1" name="rate" id="rate" value="1">
    </div>
    <div class="option">
      <label for="pitch">Pitch</label>
      <input type="range" min="0" max="2" step="0.1" name="pitch" id="pitch" value="1">
    </div>
    
    
  4. 接下来,查找这一行代码,并将其从标记中的当前位置删除:

    <button id="speak">Speak</button>
    
    
  5. 向下滚动到标记的末尾,在紧接结束的</div>之前添加以下三行,突出显示:

      <button id="speak">Speak</button>
      <button id="pause">Pause</button>
      <button id="resume">Resume</button>
    </div>
    
    
  6. 标记就绪后,我们需要对样式进行一些调整;否则,元素将无法在页面上正确显示。为此,继续注释掉或删除#voice样式规则中突出显示的代码行:

    #voice { /*margin-left: -70px;*/ margin-right: 70px; vertical-align: super; }
    
    
  7. 我们添加的范围滑块也需要调整。继续将它添加到input[type="text"]规则的下面,在该规则之后留一个空行:

    input[type="range"] { width: 300px; }
    
    
  8. 是时候添加 JavaScript 代码,为我们的新按钮和范围控件注入活力了。查找button变量声明,然后添加下面突出显示的代码:

    var button = document.getElementById('speak');
    var pause = document.getElementById('pause');
    var resume = document.getElementById('resume');
    
    
  9. 接下来,留下一个空行,然后添加以下声明——这些是我们用来调整音量、速率和音高的每个范围滑块的缓存引用:

    // Get the attribute controls.
    var volumeInput = document.getElementById('volume');
    var rateInput = document.getElementById('rate');
    var pitchInput = document.getElementById('pitch');
    
    
  10. 向下滚动,直到看到onvoiceschanged事件处理程序。然后在其下方留下一个空行,并添加这个新的错误处理程序:

```html
window.speechSynthesis.onerror = function(event) {
  console.log('Speech recognition error detected: ' + event.error);
  console.log('Additional information: ' + event.message);
}

```
  1. 下一个块是speak()函数——在里面,寻找msg.text = text,然后留下一个空行,并添加这些赋值:
```html
// Set the attributes.
msg.volume = parseFloat(volumeInput.value);
msg.rate = parseFloat(rateInput.value);
msg.pitch = parseFloat(pitchInput.value);

```
  1. 我们快完成了。滚动到 JS 代码部分的末尾,然后留下一个空行,并添加这两个事件处理程序。第一个负责暂停语音内容:
```html
// Set up an event listener for when the 'pause' button is clicked.
pause.addEventListener('click', function(e) {
  if (speechMsgInput.value.length > 0 && speechSynthesis.speaking) {
    speechSynthesis.pause();
  }
});

```
  1. 当单击 resume 按钮时,第二个事件处理程序被触发——为此,在前一个处理程序后留一个空行,并添加以下代码:
```html
// Set up an event listener for when the 'resume' button is clicked.
resume.addEventListener('click', function(e) {
  if (speechSynthesis.paused) {
    speechSynthesis.resume();
  }
});

```
  1. 我们已经完成了代码添加。请确保保存您的工作。如果一切正常,我们应该会看到类似于本练习开始时显示的屏幕截图。

尝试运行演示程序,在文本框中添加一些内容,然后改变控件。稍加练习,就能产生一些有趣的效果!我们的代码现在开始成形,并给了我们可以使用的更完整的东西。让我们更详细地快速回顾一下我们对代码所做的更改。

剖析我们的代码

我们通过添加一些标记来创建合适的范围滑块来控制音量、音高和速率设置——在所有情况下,我们都使用标准的输入元素并将它们标记为 HTML5 范围类型。随后,我们添加了两个新按钮,用于暂停和恢复语音内容。

真正的奇迹出现在我们加入剧本的时候。我们首先添加对我们创建的两个新按钮的引用;这些分别被分配了 idpauseresume,

接下来,我们创建了对三个范围滑块的引用;这些分别被称为volumeInputrateInput,pitchInput、??。然后,我们在speak()函数中添加声明,以捕捉为这些范围滑块设置的值,然后根据需要将它们分配给SpeechSynthesisUtterance对象。然后,我们添加了三个新的事件处理程序来结束演示——第一个用于将生成的任何错误呈现到控制台,第二个用于在计算机说话时暂停内容,第三个用于在用户单击恢复按钮时恢复内容。

这很简单,对吧?这只是我们可以做出的改变的一部分。妹子 API 呢,语音识别?正如我们很快会看到的,这一个需要一个不同的思维定势来做出改变。让我们更详细地看看我们可以做出的一些改变,以增强整体体验。

探索语音识别 API

我们已经探索了如何让浏览器说话,但是如何识别我们说的话呢?在我们在第一章中创建的演示中,我们遇到了诸如navigator.mediaDevices.getUserMedia()speechstart事件处理程序和recognition.interimResults这样的术语。他们都是做什么的?

第一个严格来说不是 SpeechRecognition API 的一部分;我们用它来控制从浏览器中对麦克风的访问。然而,另外两个确实是 API 的一部分;与 SpeechSynthesis API 不同,这不是一个我们可以在控制台中作为一行程序运行的 API。相反,在使用这个 API 时,我们需要指定一些设置——最关键的一点是在我们做任何事情之前允许访问麦克风!

分解 API

SpeechRecognition API 的核心是 SpeechRecognition 接口;这控制对浏览器中语音识别界面的访问。我们首先必须定义对此的引用;一旦就位,我们就可以使用这行代码创建 API 接口的实例:

const recognition = new SpeechRecognition();

值得注意的是,在 Chrome 中,这个 API 利用一个基于远程服务器的识别引擎来处理所有请求。这意味着它不能离线工作——为此,我们必须使用不同的浏览器,比如 Firefox。

然后,我们可以为设置指定合适的值,如interimResultsmaxAlternatives,以及合适的事件处理程序来停止或启动语音服务。让我们更详细地看看其中的一些设置;这些在表 2-2 中列出。

表 2-2

SpeechRecognition API 的属性

|

财产

|

财产用途

|
| --- | --- |
| 演讲认知。语法 | 返回并设置 SpeechGrammar 对象的集合,这些对象代表 SpeechRecognition API 的当前实例可以理解的语法。 |
| 演讲识别。lang | 返回并设置当前演讲人识别的语言。如果未指定,则默认为 HTML lang 属性值,或者用户代理的语言设置(如果也未设置)。 |
| 演讲识别。连续 | 控制是为每个识别返回连续的结果,还是只返回一个结果。默认为 single(或 false)。 |
| 演讲认知. interim 结果 | 控制是否应该返回中期结果(true)或不返回(false)。临时结果是尚未最终确定的结果(例如,speechrecognitionresult . is final 属性为 false)。 |
| speech recognition . max alternatives | 设置每个结果提供的最大备选项数。默认值为 1。 |
| SpeechRecognition.serviceURI | 指定当前语音识别用来处理实际识别的语音识别服务的位置。默认值是用户代理的默认语音服务。 |

一旦我们为SpeechRecognition对象定义了我们选择的设置,我们就可以使用三种方法来控制它。我们可以start()服务,stop()它,或者abort()引用一个当前的SpeechRecognition对象,就像我们在本章前面所做的语音合成演示一样。

本书末尾的附录中提供了 API 命令的完整列表。

然而,与语音合成 API 不同,以完全相同的方式定制体验的选项并不多;尽管如此,我们仍然可以实现一些改变来改善体验。记住这一点,让我们来看看我们可以增加我们的原始演示。

更新我们的演讲识别演示

当使用 SpeechSynthesis API 演示时,我们能够从 API 中添加一些额外的属性来帮助微调用户体验;SpeechRecognition API 的情况并非如此。相反,我们将采取不同的策略;我们将添加一些额外的错误管理功能和更好的控制来使用navigator.mediaDevices.getUserMedia()自动关闭麦克风。

Expanding the Options

出于本练习的目的,我们将在更小的块中检查每个变更;任何视觉变化的屏幕截图将在适当的时候显示。

本演示的代码可以在本书附带的代码下载中找到——在updating speechrecognition文件夹中查找。

让我们开始吧:

  1. 首先,在 CodePen 网站上浏览到您在第一章中创建的语音识别——确保您也登录了,这样您就可以保存您的更改。

  2. 接下来,查找这一行代码,并在它下面添加以下内容(突出显示),在新代码后面留下一行:

    recognition.interimResults = true;
    recognition.maxAlternatives = 1;
    recognition.continuous = true;
    
    
  3. 我们要实现的第一个变化是开始改进错误处理——目前,我们正在逐字逐句地排除错误消息,这看起来不太好。通过一些修改,我们可以使它更友好,所以继续修改错误事件处理程序,如下所示:

    recognition.addEventListener("error", e => {
      if (e.error == "no-speech") {
        output.textContent = "Error: no speech detected";
      } else { output.textContent = "Error: " + e.error;  }
    });
    
    

值得注意的是,如果以后需要,我们可以用其他错误代码来扩展它。

img/490753_1_En_2_Fig3_HTML.jpg

图 2-3

我们更新的语音识别演示

  1. 第三个也是最后一个变化是对麦克风的关闭施加更多的控制——有时我们可能想要控制它何时关闭,而不是让它看起来有自己的想法!幸运的是,对此的更改非常简单——首先是在我们的 HTML 标记中添加一个元素,如下所示:

    <p class="output">You said: <strong class="output_result"> </strong></p>
    <button id="micoff">Turn off</button>
    
    
  2. 第二个变化需要我们添加一个新的事件处理程序——我们可以控制何时关闭麦克风,而不是依赖语音识别 API 自动关闭或试图转录它听到的不是故意的语音。为此,查找这行代码:

    recognition.continuous = true
    
    

    然后留下一个空行,放入下面的代码:

    document.getElementById("micoff").addEventListener("click", () => {
      stream.getTracks().forEach(function(track) { track.stop() });
      console.log("off");
    });
    
    
  3. 我们已经做了所有需要的改变。继续保存您的工作成果。如果一切正常,我们应该会看到类似于图 2-3 所示的截图,在这里我们可以看到我们改进的错误处理。

  4. The second change will be an auto turn-off for the speech recognition engine – after all, we don’t necessarily want our microphone to stay enabled if we’re not using it for a period of time, right? For this change, look for the speechend event handler, then leave a blank line, and add in this function:

    recognition.onspeechend = function() {
      log.textContent = 'You were quiet for a while so voice recognition turned itself off.';
      stream.getTracks().forEach(function(track) { track.stop() });
      console.log("off");
    }
    
    

    We can see the result of this change in Figure 2-2.

    img/490753_1_En_2_Fig2_HTML.jpg

    图 2-2

    增加了自动关闭功能

如果您尝试单击“关闭”按钮关闭麦克风,请耐心等待,红色指示灯可能需要几秒钟才会消失!

在研究这个演示的代码时,我对使用语音识别 API 时可以触发的事件的明显交叉数量感到震惊。

正是由于这个原因,尽管我们没有像语音合成 API 那样多的配置选项可以调整,但语音识别 API 中不同的事件处理程序仍然会使我们出错!记住这一点,让我们更详细地看看我们刚刚完成的演示中的代码。

理解代码

在过去的几页中,我们采用了不同的方法来开发我们的原始演示,这一次,我们通过增加或改善整体体验来扩展它,而不是简单地添加更多选项。让我们花点时间更详细地回顾一下我们对原始演示所做的更改。

我们从更新错误事件处理程序开始,在那里我们检查了no-speech错误属性,并向用户返回了一个更容易接受的消息。我们的下一个变化实现了一个自动关闭选项——在使用语音识别 API 时,我们必须记住一点(我们不希望它在没有控制的情况下运行!)

我们做的最后一个改变是改变自动关闭功能——自动关闭是一个有用的功能,但有时我们可能希望控制这种情况何时发生。这是特别有用的,有助于防止我们的麦克风自动录制东西,这是不应该共享的!

好了,我们该继续前进了。我们已经探索了如何实现语音识别和合成 API 的基础知识;是时候让它们有更实际的用途了!为了展示我们如何将它们结合在一起,我们将创建一个简单的视频播放器,可以通过语音控制;它会用声音确认我们要求的任何动作,而不是在屏幕上显示消息。这将使用我们创建的两个演示中的原则。让我们深入研究一下,看看这是如何工作的。

创造一个更实际的例子

在下一个练习中,我们将为使用 HTML5 <video>元素的视频播放器添加基本的语音功能。

我们现在将着重于添加播放和暂停命令,但是我们可以在以后很容易地添加额外的命令,例如增加音量或静音。完成后,看起来会像图 2-4 所示的截图。

img/490753_1_En_2_Fig4_HTML.jpg

图 2-4

我们的声控视频播放器

在您设置演示时,仔细看看一些函数和处理程序——希望您能从以前的演示中认出一些元素!记住这一点,让我们开始我们的演示。

Adding Speech Capabilities to Video

下一个演示有几个要求:你需要确保你有一个合适的视频可用(MP4 格式可以;代码下载里有个例子视频如果没有合适的)。我们将建立一个 CodePen 会话,因此在继续之前,请确保您已经浏览了位于 https://codepen.io 的网站并已登录:

  1. 我们将开始添加我们需要作为演示基础的标记——为此,继续将HTML.txt文件的内容从practical example文件夹复制到右侧的 HTML 窗格中。

  2. 接下来,让我们添加一些基本的样式,这样我们至少可以让我们的演示看起来像模像样——为此,继续将CSS.txt文件的内容添加到 CSS 窗格中。

  3. 我们现在可以把注意力转向真正重要的部分——我们的剧本!有一个很好的块要添加,所以我们将一个块一个块地做,从一些变量声明开始。继续在 JS 窗格的顶部添加以下代码行:

    "use strict";
    
    const log = document.querySelector(".output_log");
    const output = document.querySelector(".output");
    const confidence = document.querySelector(".confidence em");
    
    // Simple function that checks existence of s in str
    var userSaid = function(str, s) {
      return str.indexOf(s) > -1;
    };
    
    
  4. 下一个代码块负责将我们选择的视频加载到我们的视频播放器中——这样我们就可以在发出命令时准备好播放。留下一个空行,然后添加以下代码:

    video_file.onchange = function() {
      var files = this.files;
      var file = URL.createObjectURL(files[0]);
      video_player.src = file;
    };
    
    
  5. 接下来的部分有点棘手——我们需要允许用户请求访问他们的麦克风。为此,我们使用navigator.mediaDevices.getUserMedia;我们将首先添加这个结构。留下一个空行,然后添加这个方法调用:

    navigator.mediaDevices
      .getUserMedia({ audio: true })
      .then(function(stream) {
    
    ...add in code here...
    
    }).catch(function(err) {
      console.log(err);
    });
    
    
  6. 有了这些,我们现在可以开始添加操作语音识别 API 所需的各种组件;我们首先需要定义 API 的一个实例。继续添加,替换文本...add in code here... :

    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognition();
    
    
  7. 接下来是我们的第一个事件处理程序,我们将使用它来调用对麦克风的访问。为此,在 SpeechRecognition API 声明后留下一行,然后添加以下代码:

    document.querySelector("button").addEventListener("click", () => {
      let recoglang = "en-US";
      recognition.lang = recoglang;
      recognition.continuous = true;
      recognition.start();
    });
    
    
  8. 我们现在需要添加事件处理程序,负责在我们开始说话时打开 API,或者在适当的时候关闭它;在前一个处理程序后留出一行空白,然后添加以下代码:

    recognition.addEventListener("speechstart", e => {
      log.textContent = "Speech has been detected.";
    });
    
    recognition.addEventListener("speechend", e => {
      recognition.stop();
    });
    
    recognition.onspeechend = function() {
      log.textContent =
        "You were quiet for a while so voice recognition turned itself off.";
        stream.getTracks().forEach(function(track) {
          track.stop();
        });
      console.log("off");
    };
    
    
  9. 下一个模块是真正神奇的地方——它通过将我们的口头命令转换成它能识别的东西并翻译成适当的命令来控制我们的视频播放器。在前一个事件处理程序后留下一行,然后放入以下代码:

    // Process the results when they are returned from the recogniser
    recognition.onresult = function(e) {
    // Check each result starting from the last one
    
    for (var i = e.resultIndex; i < e.results.length; ++i) {
    var str = e.results[i][0].transcript;
    
    console.log("Recognised: " + str);
    // If the user said 'video' then parse it further
    if (request(str, "video")) {
    // Play the video
    if (request(str, "play")) {
    video_player.play();
    log.innerHTML = "playing video...";
    } else if (request(str, "pause")) {
    // Stop the video
    video_player.pause();
    log.innerHTML = "video paused...";
    }
    }
    }
    confidence.textContent =
    (e.results[0][0].confidence * 100).toFixed(2) + "%";
    };
    
    
  10. 我们快完成了。要添加的最后一个事件处理程序将负责一些基本的错误捕获。继续添加这个事件处理程序,在前一个块之后留出一行空白:

```html
recognition.addEventListener("error", e => {
  if (e.error == "no-speech") {
    log.textContent = "Error: no speech detected";
  } else {
    log.textContent = "Error: " + e.error;
  }
});

```
  1. 保存您的工作。如果一切正常,我们应该会看到类似于本练习开始时显示的屏幕截图。

尝试使用“选择文件”按钮选择一个视频,然后说“视频播放”开始播放——是的,这有点小花招,但它确实提供了一个有效的观点。谁说你总是要按下按钮才能启动一个东西?是的,我绝对会认为自己是一个老派方法的粉丝,但总有一天,一个人必须适应…!

如果你仔细看看这个演示中的代码,你会发现它使用了我们在早期演示中已经见过的术语;总的来说,大部分现在应该开始看起来熟悉了!但是有一个例外——我没有看到结果的事件处理程序;我看到的这个.onresult句柄是什么…?

详细研究代码

啊哈!对于我们跟踪 API 最终输出的方式来说,这是一个重要的改变!它的工作方式与我们之前使用的结果事件处理程序类似,但是有一点不同:该事件处理程序只被调用一次。我将很快解释我的意思,但是首先,让我们更详细地检查我们的代码。我将把重点放在 JavaScript 上,因为使用的 CSS 和 HTML 是非常标准的,应该是不言自明的。

我们从声明一些变量开始,这些变量用于存储对标记中元素的引用,并帮助在我们的口头输入中查找文本(稍后将详细介绍)。然后,我们继续创建一个基本函数,将我们选择的视频加载到我们的视频播放器中,准备根据命令播放它。

下一个模块是我们演示真正关键的开始——在声明 API 实例之前(取决于我们使用的浏览器),我们初始化对navigator.getUserMedia()的调用以允许访问我们的麦克风。

然后,我们添加了一个事件处理程序,用各种属性初始化我们的 API 实例——lang 被设置为 US English 和 continuous,以防止 API 在打开之前关闭过快。接下来出现了三个事件处理程序来响应语音——speechstart在 API 检测到语音内容时启动,speechend将终止它,onspeechend将识别 API 是否安静并自动关闭。

我们演示的真正重点是接下来——这里我们使用了onresult。这与我们之前使用的 result 事件处理程序不同,它不会触发一次(result 会),而是在每次我们说话并且 API 检测到我们停止说话时触发。我应该指出,这是而不是完全停止说话,更多的是我们发出的每个命令之间的停顿!该函数使用 for 循环解析结果,将每个结果依次分配给 str,然后根据听到的内容执行适当的视频命令。因此,如果我们说“播放视频”,它会单独搜索每个单词。根据它听到的内容,它会检测到我们说了视频,因此它会检查我们是否说了播放或暂停。如果我们说播放(正如我们在这里所做的),它会暂停视频并在屏幕上显示确认。

好吧,让我们开始吧!尽管我们到目前为止做的许多演示可能都是英文的,但有一件事我们应该牢记在心:对不同语言的支持呢?在一个全球互联的时代,我们不能假设人们只会说英语(或者实际上只会一种语言!).我们绝对应该考虑增加对不同语言的支持;值得庆幸的是,使用语音 API 时,我们可以轻松做到这一点。让我们深入了解一下我们需要做些什么来更详细地添加多语言支持。

走向多语言

在一个理想的世界里,如果我们都说同一种语言就太好了——毕竟,我们可以和不同国家的人交流,不会有误解……但是那会很无聊!

用不同的语言和别人说话是有道理的;拥抱不同的文化和语言为假期或旅行增添了额外的元素。这同样适用于阅读文本,例如在博物馆里找到的文本;当然,你可能不太懂,但你仍然会对这个国家过去的历史有所了解。

但是我跑题了。回到现实,我们已经讨论了如何将语音转换成文本,反之亦然。那么其他语言呢?我们并不都说英语(或者实际上是同一种语言),那么这在两种 API 中是如何工作的呢?

探索对语言的支持

使用语音识别或语音合成 API 的好处之一是它支持其他语言——有许多不同的选项可供我们使用。正如我们很快会看到的,确切的数字将取决于我们使用的浏览器;这可能多达 21 个,也可能少到只有 3 个!

我们已经在第一章创建的语音合成演示中提到了语言支持——还记得那个演示中显示的相当有趣的名字列表吗?我们可以在图 2-5 中看到它的摘录。

img/490753_1_En_2_Fig5_HTML.jpg

图 2-5

可用于语音合成 API 的语言(摘录)

为了实现这一点,我们创建了一个loadVoices()函数来遍历每个语言选项,然后将它添加到下拉菜单中。然后,在将更改应用到 SpeechRecognition 对象之前,我们使用了getVoices()方法来选择我们选择的语言。

如果您想知道我们是如何做到这一点的,请尝试在您的浏览器控制台中运行这个简单的示例——我建议您在一个 CodePen 演示的控制台日志中运行它,这样您就可以触发对麦克风的访问:

console.log(`Voices #: {speechSynthesis.getVoices().length}`);
speechSynthesis.getVoices().forEach(voice => {
  console.log(voice.name, voice.lang)
});

在这一点上,值得注意的是,这应该在大多数现代浏览器中工作。你可能会发现,尽管你在 SpeechSynthesis API 中使用这段代码时会遇到跨浏览器的问题——在 Chrome 的一些旧版本中,我们使用的原始代码无法运行。

它在 Firefox 和 Edge 中运行良好(对于那些 Mac 用户来说,可能还有 Safari);相反,在使用之前,您可能会发现必须使用回调来显示列表。获取声音以显示列表:

const voiceschanged = () => {
  console.log(`Voices #: ${speechSynthesis.getVoices().length}`);
  speechSynthesis.getVoices().forEach(voice => {
    console.log(voice.name, voice.lang)
  })
}
speechSynthesis.onvoiceschanged = voiceschanged

不过你可能会发现,使用 Chrome 时返回的语言数量有所不同——以“Google…”开头的额外语言只有在网络连接有效的情况下才可用。在图 2-6 中完整显示了该列表的副本。

img/490753_1_En_2_Fig6_HTML.jpg

图 2-6

API 中支持的语言列表

否则会减少;Edge 目前显示三个,Firefox 显示两个。可以说,这只是使用语音 API 时需要考虑的另一点!

相比之下,在语音合成 API 中添加语言支持变得更加有趣——我们不仅可以选择语言,甚至可以设置方言!这确实需要更多的工作来实现。在下一个练习中,我们将很快看到如何实现这一点。

设置自定义语言

如果我们在使用语音识别 API 时需要设置语言支持,我们必须采取不同的策略——不是简单地从 API 调用列表,而是提供一个列表。这实际上采取了双数组的形式。这解释起来有点复杂,所以请原谅我;我将使用下一个练习中的摘录。

我们从数组开始,注意到不是所有的条目都相等吗?好吧,在你说话之前,我指的是数字,不是里面的文字!在大多数情况下,我们只有语言和 BCP47 代码(比如 af-ZA),但是在最后一个例子中,我们有三个值:

var langs =
    [['Afrikaans',       ['af-ZA']],
     ['Bahasa Indonesia',['id-ID']],
     ['Bahasa Melayu',   ['ms-MY']],
     ['Català',          ['ca-ES']],
     ['Čeština',         ['cs-CZ']],
     ['Deutsch',         ['de-DE']],
     ['English',         ['en-AU', 'Australia'],
...
(abridged for brevity)

BCP47 或最佳当前实践 47 是用于识别人类语言的 IEFT 国际标准,例如德语的 de-DE。如果你想了解更多,那就去维基百科的 https://en.wikipedia.org/wiki/IETF_language_tag 找一篇好的介绍文章。

然后,我们使用这样的结构遍历数组:

for (var i = 0; i < langs.length; i++) {
  select_language.options[i] = new Option(langs[i][0], i);
}

将它放入一个对象中,我们可以从中选择默认显示的项目(在本例中为英语):

select_language.selectedIndex = 6;

这本身不会对 API 产生任何影响;为了让它工作,我们需要增加一个功能。在高层次上,我们再次遍历数组,但是这一次挑选出方言值(在这种情况下,来自第二列的值),然后将它们添加到一个<select>下拉框中。然后,我们需要设置可见性,这样,如果我们选择一种有多种方言的语言,方言下拉列表就会相应地显示或隐藏。希望这将开始有一些意义;要了解这在实践中是如何工作的,让我们快速进入下一个练习,在这里我们将看到这段代码如何融入我们的演示中。

Allowing for Languages in Speech Recognition

在下一个练习中,我们需要回到我们在第一章中创建的语音合成演示——为了保留您之前代码的副本,我建议您登录 CodePen 并点击 Fork 按钮。我们的演示将从您准备好编辑 HTML 代码开始,因此在继续本演示中的步骤之前,请确保您已经到了这一步。

尽管我们将要做的一些改变很简单,但其他的更复杂。我建议你一定要为这本书下载一份代码副本;所有内容都将保存在language support文件夹中。

假设您在那里,让我们开始更新我们的演示:

img/490753_1_En_2_Fig7_HTML.jpg

图 2-7

对我们的演示说法语的结果

  1. 第一个变化确实出现在我们的 HTML 标记中,所以请查找这一行并将其注释掉:

    <span class="voice">Spoken voice: US English</span>
    
    
  2. 接下来,将这个块直接插入到它的下面:

    <span class="voice">
      Spoken voice and dialect:
        <div id="div_language">
          <select id="select_language" onchange="updateCountry()">
    </select>
          <select id="select_dialect"></select>
        </div>
      </span>
    
    
  3. 我们现在需要调整新的下拉列表的位置——为此,在 CSS 窗格的底部添加下面的 CSS 样式:

    .voice { float: right;  margin-top: -20px; }
    
    
  4. 但是真正的变化是在我们的 JavaScript 代码中(当然!)–为此,继续从代码下载中打开一个JS.txt文件的副本,然后查找这行代码:var langs =(大约在第 7 行)。

  5. 复制它和下面的行,直到(包括)这一行后面的右括号:

      select_dialect.style.visibility = list[1].length == 1 ? 'hidden' : 'visible';
    }
    
    
  6. 将代码下载中的 JavaScript 内容粘贴到这一行之后,在它和您的新块之间留一行:

    const output = document.querySelector(".output_result");
    
    
  7. 好的,下一个变化:向下滚动直到你看到这个事件处理程序的开始:

    document.querySelector("button").addEventListener("click", ()
    
    
  8. 注释掉该函数中的let recoglang = "en-US",替换为:

    recognition.lang = select_dialect.value;
    
    
  9. 向下滚动,直到看到这一行:output.textContent = text;

  10. 接下来,添加一个空行,然后放入这行代码,在右括号和圆括号之前:

```html
log.textContent = "Confidence: " + (e.results[0][0].confidence * 100).toFixed(2) + "%";

```
  1. 此时,我们应该已经完成了所有的代码更改;继续保存您的工作。

  2. 尝试运行演示。如果一切正常,我们应该有类似于图 2-7 所示的截图。试着把声音换成不同的语言,然后说点什么——希望你或你的朋友能知道足够多的单词来说些有意义的话!

从演示中可以看出,这说明我确实懂一些法语;我的西班牙语也还过得去,虽然和我的水平相差甚远!除此之外,我们还在这个演示中添加了一个非常重要的特性,值得我们更详细地了解一下——让我们花点时间来更详细地了解一下它是如何工作的。

打破我们的代码

如果我们仔细看看我们刚刚编写的代码,您可能会发现一个奇怪的地方——如果您没有发现,也不要担心,因为它不会立即显现出来!我会给你一个开始的线索:它与我们赋予语言的价值有关——它不是你最初可能期望的那样…

好吧,我跑题了。回到我们的演示,我们首先注释掉原始文本,指出使用了哪种语言;我们用两个下拉菜单替换了它,一个用于语言,另一个用于方言。然后我们引入大量代码,首先建立一个数组langs,存储语言和方言值。接下来我们用一个for循环遍历第一组值,并将每个值插入到select_language下拉列表中。然后,我们为语言和方言属性设置了两个默认值——在本例中是英语。

接下来是updateCountry()函数——它看起来有点复杂,但不难理解。我们只是清除了方言下拉列表(select_dialect),然后用第二列数据中的值填充它(在本例中,我们有前面谈到的 BCP47 值)。

剩下的变化很小——我们将来自select_dialect下拉列表的输出值重新分配给recognition.lang,并在output_log span 元素中添加了一个置信度声明。有道理?嗯,会的,如果只有一个困扰的问题。为什么我们看起来像是在设置方言值,而不是语言值…?

语言和方言的区别

如果我把这一部分的主题作为一个问题问你,希望你会说语言是我们会说的东西,方言实际上是那种语言的地区变体…或者类似的东西!然而,如果我说,至少在这个 API 的上下文中,这两者实际上是同一个事物的两个部分,并且在某些情况下是相同的,你会感到非常困惑!什么给了…?

答案就在五个字里——bcp 47。这是我之前提到的国际标准,我们可以看到 pt-BR 或葡萄牙语的巴西方言等代码。但是真正的诀窍在于我们如何在代码中利用这一点——尽管我们选择了语言和方言(后者可用),但直到我们选择了那个方言值,我们才得到实际使用的值。

例如,如果我们选择葡萄牙语方言,我们将得到 pt-BR;这是 lang 属性进行语音识别所需的值。实际上,在通过方言下拉列表选择真正的语言用于我们的演示之前,我们使用语言下拉列表来过滤我们的选择。

好吧,我们继续。在我们进入构建项目的实际乐趣之前,我们还需要探索一个特性!正如我希望你已经从演示中看到的,语音识别发展得很好,但它并不完美。可能有些时候,我们会想伸出援助之手。我给你介绍一下SpeechRecognition.Grammars

利用语法对象

在本章的整个过程中,我们已经更详细地探索了语音 API,涵盖了诸如添加多语言支持、对何时可以使用麦克风提供更好的控制,以及当我们在使用 API 时遇到错误时细化返回的内容等功能。

然而,可能有些情况下我们需要这种帮助——这就是 SpeechRecognition API 的语法部分可以发挥作用的地方。然而,这个特性有点奇怪,而且可能会带来一些麻烦。为什么?

许多人发现它最多令人困惑,或者实际上没有做他们原本期望它做的事情。一部分原因可能是因为最初的规范是什么时候写的;这是在单词识别率没有现在这么好的时候完成的,所以它需要一些东西来提供可以被描述为提升的东西。

因此,对SpeechGrammarList界面的支持很差——目前只有 Chrome 支持。它还利用了 JSpeech 语法格式(或 JSGF ),这种格式已经从大多数浏览器中删除了。因此,除非绝对必要,否则我不建议使用该功能,并且请注意,使用该功能需要您自担风险,并且它很可能会在未来被删除。

如果你想了解技术细节和关于移除提议的讨论,请访问 W3C GitHub 网站 https://github.com/w3c/speech-api/pull/57https://github.com/w3c/speech-api/pull/58

摘要

当使用语音 API 时,我们可以使用许多选项;当我们在第一章第一次介绍 API 时,我们已经介绍了其中的一些。在本章的课程中,我们在我们所学的基础上增加了额外的选项。让我们花点时间回顾一下我们所学的内容。

我们通过创建演示来更详细地探索语音合成和语音识别 APIs 在向每个演示添加功能之前,我们首先介绍了每个 API 中可用的更多选项。

接着,我们看了看如何在使用 API 时添加多语言支持。我们探讨了每个 API 背后的基本原则以及如何设置自定义语言。紧接着是一个演示,然后探索设置语言和方言属性之间的区别,以及两者如何相互作用以给出我们想要的语言设置。

然后我们看了一下语音语法界面,结束了这一章。我们讨论了如何使用它,但是有计划在将来放弃对它的支持;我们讨论了为什么会出现这种情况的一些原因,以及它在实践中如何影响或不影响您的代码。

唷!涵盖了很多,是吧?嗯,节奏不会慢下来——事情会变得越来越有趣!在接下来的几章中,我们将实现一些示例项目来说明如何在实际环境中使用 API。这将涵盖从留下口头审查反馈到自动化部分或全部购买过程的任何事情;我们真的只是被我们的想象力所限制!首先,我们将从一些相对较新的网站开始。例如,使用 API 开发聊天机器人怎么样?翻到下一页,了解我们如何开始与您的网站进行适当的对话…

三、支持移动设备

“Juniper Research 预测,智能助理的数量将从 2018 年底的 25 亿增加到 2023 年的 80 亿,增长两倍。”

还记得第一章第一章开头的那段令人震惊的话吗?考虑到移动使用现在已经超过了桌面,这是一个强大的组合!但是——我听到你说:“这两个事实的意义是什么?”好吧,让我全部透露。

到目前为止,在前面的章节中,你可能已经注意到使用桌面作为我们的环境。这本身没有错,但它忽略了一个关键点:使用移动设备怎么样?鉴于越来越多的人使用智能设备购买产品,那么在使用 Web 语音 API 时考虑移动设备是绝对有意义的。

在本章的课程中,我们将看一下我们在前面章节中创建的一些演示,并探索如何在移动设备上使用它们。根据您目前所看到的,您可能认为这不应该是一个问题,因为大多数最新的浏览器都支持桌面上的 API,对吗?嗯,事情并不像看上去的那样——做好做决定的准备。

支持语音合成 API

是的,最后一个评论可能看起来有点有趣,但是我们将做出一些决定,关于我们如何在移动环境中使用 API!让我解释一下我的意思,首先从语音合成(图 3-1 )开始,说明在更流行的移动平台上对 API 的支持程度。

img/490753_1_En_3_Fig1_HTML.jpg

图 3-1

支持语音合成 API 来源:CanIUse.com

哎哟!这看起来不如台式机好,对吧?授予的覆盖范围不像标准桌面用户那样广泛,但是由于有太多不同的可用平台,支持不那么一致也就不足为奇了!然而,这并不像看起来那么糟糕——要理解为什么取决于我们对一个关键问题做出有意识的决定:我们希望在多大程度上支持谷歌浏览器?

分解数字

为了理解最后一个问题的答案,我们应该首先看看谁支持这个 API 以及这个浏览器的当前使用情况。表 3-1 显示了从图 3-1 中呈现的信息的更详细版本,其中我们可以看到哪些更流行的浏览器支持该 API。

表 3-1

支持移动设备上的语音合成 API

|

移动浏览器

|

支持?

|

截至 2019 年 12 月的使用百分比

|
| --- | --- | --- |
| iOS 浏览器 | 是 | Two point eight nine |
| 迷你歌剧 | 不 | One point one seven |
| 安卓浏览器 | 不 | Zero |
| 歌剧手机 | 不 | Zero point zero one |
| 安卓版 Chrome 浏览器 | 是 | Thirty-five point one six |
| 安卓火狐 | 是 | Zero point two three |
| 适用于 Android 的 UC 浏览器 | 不 | Two point eight eight |
| 三星互联网 | 是 | Two point seven three |
| 手机 QQ 浏览器 | 是 | Zero point two |
| 百度浏览器 | 不 | Zero |
| KaiOS 浏览器 | 是 | Zero point two |

显而易见,谷歌 Chrome 的使用率远远超过了所有其他浏览器的总和,几乎是 3 比 1!因此,它提出了我们应该支持谁的问题,特别是对于任何最低可行产品(或 MVP)。

由于所有其他浏览器制造商都不支持移动设备上的 API,或者该浏览器的使用率远低于 5%,所以专注于 Chrome 是有意义的。要真正把重点放在家里(好像这是必要的!),我们可以看到图 3-2 中使用了多少 Chrome。

img/490753_1_En_3_Fig2_HTML.jpg

图 3-2

截至 2019 年 12 月的 Chrome 使用情况来源:CanIUse.com

削减对如此多浏览器的支持似乎有些过激,但在当今世界,我们需要务实:我们有资源或时间为所有不同的浏览器开发吗?对 Chrome 的支持远远超过其他浏览器,因此专注于这款浏览器并仅在收入足够大以保证部署资源的情况下包括其他浏览器是有商业意义的(例如对于非常大的客户)。

支持语音识别 API

我们已经探索了对语音合成 API 的支持。它和它的姐妹——语音识别 API 相比如何?

嗯,乍一看,支持并不是那么好——在某些方面,这并不是一个真正的冲击,因为这个 API 比语音合成 API 更复杂,所以支持没有那个 API 那么先进。我们可以在背面的图 3-3 中看到流行移动平台的概要。

img/490753_1_En_3_Fig3_HTML.jpg

图 3-3

支持移动设备上的语音识别 API 来源:CanIUse.com

乍一看,主要的区别是对这个 API 的任何支持还没有达到完全批准的状态(而另一个 API 已经达到了);这只是意味着我们在使用这个 API 时需要使用前缀-webkit。我们很快就会看到,这没什么大不了的;然而真正的问题在于使用浏览器的人数!为了理解我的意思,让我们深入下去,更详细地看看这些数字,就像我们对语音合成 API 所做的那样。

理解数字

如果我们看看谁支持语音识别 API 的细节,我们会看到使用每个浏览器的人数和以前一样多。不过这一次,每个浏览器对 API 的支持比支持语音合成 API 的少 25%(允许使用供应商前缀,并且一个浏览器需要手动启用)。我们可以看到表 3-2 中列出的结果。

表 3-2

支持移动设备上的语音识别 API

|

移动浏览器

|

支持?

|

截至 2019 年 12 月的使用百分比

|
| --- | --- | --- |
| iOS 浏览器 | 不 | Two point eight nine |
| 迷你歌剧 | 不 | One point one seven |
| 安卓浏览器 | 不 | Zero |
| 歌剧手机 | 不 | Zero point zero one |
| Android 版 chrome(使用 webkit 前缀) | 是–部分 | Thirty-five point one six |
| 安卓火狐 | 不 | Zero point two three |
| 适用于 Android 的 UC 浏览器 | 不 | Two point eight eight |
| 三星互联网(使用 webkit 前缀) | 是–部分 | Two point seven three |
| QQ 浏览器(使用 webkit 前缀) | 是–部分 | Zero point two |
| 百度浏览器(使用 webkit 前缀) | 是–部分 | Zero |
| KaiOS 浏览器(使用 webkit 前缀) | 可以启用 | Zero point two |

那么 Chrome 在这个列表中脱颖而出就不足为奇了,就像它在语音合成 API 中的表现一样。如果我们将鼠标悬停在 CanIUse.com 网站的数字上,我们将看到与之前显示的结果相同的结果!

对表 3-2 中显示的数字的检查表明,集中精力开发谷歌 Chrome 是非常明智的;任何花在其他浏览器上的时间都应该只给大客户,在那里收入机会可以证明所需要的努力是值得的!既然我们已经看到了这两个 API 的数字,那就有必要花点时间总结一下为什么我们应该考虑只为 Chrome 开发:

  • Chrome 是最受欢迎的,所以我们将通过专注于这款浏览器获得最大的曝光率,我们可以在移动和桌面环境中重用相同的核心功能。

  • 如果(但愿不会)我们遇到任何问题,我们应该很快看到它们出现,然后可以更快地决定停用或暂停语音选项。在使用率低得多的浏览器上很难发现问题,我们可能不会像在 Chrome 上那样很快发现问题。

好了,鉴于 Chrome 的受欢迎程度和支持水平,我们已经概述了关注 Chrome 的理由;是时候实际一点了!在我们这样做之前,关于本章中的演示,有几点我们需要掩盖;这是为了确保您在测试每次练习的结果时获得最佳效果。

几个先决条件

在本章的过程中,我们将重温一些我们在 CodePen 中创建的来自前面章节的练习,目的是使它们适合在移动设备上显示,比如你的手机。

我们当然可以创建新的演示——这种方法没有错,但更有益的方法是看看如何轻松地将现有的演示应用到移动平台上。考虑到这一点,有几点需要注意:

  • 代码可以在我们在桌面上创建的笔内编辑,但是为了在测试我们的演示时获得最佳效果,它应该在手机上显示。

  • 为了每个演示的目的,也为了证明我们之前的讨论,我们将只使用 Chrome 鉴于 Chrome 的使用水平远远高于所有其他浏览器,使用最流行的浏览器是有意义的!

  • 我们将像以前一样使用本书的代码下载——在开始本章的演示之前,请确保您手头有一份副本。

记住这一点,让我们开始开发一些代码,从检查浏览器对 API 的支持开始。

检查对 API 的支持

我们的第一个演示将有助于确定所选的浏览器是否支持 APIs 值得指出的是,如果我们决定只使用 Chrome(如前所述),那么这个测试似乎有点多余!

不过,还是值得一跑;我们不仅检查支持,而且我们还将使用不同的方法来实现相同的结果。这两种方法都没有比另一种更好的理由;每一种都可以正常工作,您可以选择在自己的项目中使用哪一种。

Establishing Support for Speech APIs

我们将从检查语音合成 API 开始,但是代码也将与语音识别 API 一起工作(只需在代码中将单词“Synthesis”的所有实例替换为“Recognition”,然后保存并运行它)。

要确定浏览器是否支持 API,请执行以下步骤:

  1. 接下来,打开来自HTML.txt的代码的副本,并将内容粘贴到我们的笔的 HTML 窗格中。

  2. Once saved, we can test the results – for this, go ahead and browse to your CodePen from your cell phone, then make sure you have Editor View displayed, and hit the Console button. We don’t need to do anything. If your browser supports either API, then we will see confirmation of this in the CodePen console, as shown in Figure 3-5.

    img/490753_1_En_3_Fig5_HTML.jpg

    图 3-5

    证明我们的手机支持 API

  3. 我们首先从代码下载中提取一个checksupport文件夹的副本,并将其保存到我们的项目区域。

  4. 接下来,继续浏览至 https://codepen.io 的 CodePen。然后使用你在第一章中使用的账户信息登录。

  5. On the left, choose Create ➤ Pen. Then copy and paste the contents of JS.txt into the JS pane of our Pen – make sure you save the Pen! If all is well, we should have something akin to the screenshot shown in Figure 3-4.

    img/490753_1_En_3_Fig4_HTML.jpg

    图 3-4

    在 CodePen 的 JS 窗格中输入的代码

太棒了!我们已经确认我们的移动浏览器可以支持 API(是的,我假设你已经使用了 Chrome!).

这意味着我们现在可以继续前进,并开始调整我们以前的练习,以在移动设备上显示。在此之前,我想快速浏览一下一个小技巧:计算出可用的视区。是的,我知道你可能会问这和语音 API 有什么关系,但是这是有原因的;请忍耐,我会解释一切。

确定可用空间

任何花时间为移动设备设计的人无疑会意识到页面上可用空间的限制——这是一个老问题,当视窗区域如此之小时,提供什么。

这在使用语音 API 时尤其重要——我们必须注意显示元素需要多少空间,例如语音合成 API 所需的输入字段,或者使用语音识别 API 时显示转录文本所需的空间。

这就是使用代码计算出视窗区域可能有所帮助的地方——不仅是为了确定我们在代码中有多少空间,而且也是为了设置 Chrome 的响应视图以适应手机的可用空间。让我们更详细地看看这在现实中意味着什么,以及它们如何帮助语音 API。

使用代码设置可用空间

由于空间非常珍贵,我们需要计算出我们能够使用多少空间。我们可以使用 JavaScript 自动获取值,而不是试图猜测值或通过反复试验来计算。

这对于在多种设备上进行测试来说是非常好的,因此当涉及到为语音 API 布局元素时,我们可以感受到我们将不得不使用多少空间。为此,您可以使用我在 CodePen 中设置的 https://codepen.io/alexlibby/pen/MWYVBGJ 功能。

顺便说一句,不要忘记添加正确的元标签——你将需要这样的东西:<meta name="viewport" content="width=device-width, initial-scale=1.0">

配置 Chrome 的响应模式

此外,我们可以使用这些值来帮助设置 Chrome 的响应模式(官方称之为移动仿真模式)。不言而喻,没有两个手机会有相同的空间或视窗,所以为了帮助这一点,我们可以设置自己的自定义区域。让我们在下一个练习中了解一下。

Setting up Viewports in Chrome

如果你想练习这些步骤,下一个练习可以在 Chrome for mobile 或 desktop 上进行;最终你需要在 Chrome 中设置它,以在测试中获得最佳效果。我们可以使用以下步骤设置视窗:

  1. 单击左侧的下拉菜单,并选择编辑➤添加自定义设备…

  2. 在设备名称字段中,输入您的手机或所选移动设备的品牌和型号。

  3. 下面是三个字段–在左边输入宽度,在中间输入高度,右边的字段保持不变。确保使用代理字符串设置为移动。

  4. 点击添加。现在,在测试语音 API 时,您可以将它设置为您选择的视窗区域。

  5. 启动 Chrome,浏览到一个站点——我假设我们将使用 CodePen,它非常适合测试响应视图。

  6. 接下来,我们需要启用响应模式,这可以使用 Ctrl+Shift+I (Windows 和 Linux)或 Cmd+Shift+I (Mac)来完成。

  7. At the top of the resized page will be an option to choose a different viewport; it will look something akin to the screenshot shown in Figure 3-6.

    img/490753_1_En_3_Fig6_HTML.jpg

    图 3-6

    Chrome 的响应模式选项

这只是使用 Speech APIs 时要考虑的一个小方面——我们在这里只涉及了两个方法,并没有探究返回值的一些奇怪之处。但是,它应该会给你一个很好的提示,告诉你可能会有多少空间,这样你就可以设置一个简单的方法来分配空间和测试特性,而不必完全在移动设备上工作或使用 BrowserStack 之类的外部服务。

这并不是说我们应该忽略 BrowserStack 之类的服务,它们执行一个有用的功能——这是为了让这个小技巧在开发期间,在完成正确的测试之前发挥作用!

好吧,让我们回到 API 的话题上。既然我们有了一个快速而简单的方法来计算出可用的视口区域,那么是时候让我们进入 API,看看它们是如何在移动设备上工作的了!我怀疑你可能在想我们必须做出很多改变,对吗?如果不是对 API 本身,至少对样式,肯定…?

嗯,我不想让人失望,但答案是否定的——如果我们对自己的造型很小心,那么我们应该不需要太多的调整。让我们通过改编本书前面的两个 CodePen 演示来测试这个理论,看看它是如何工作的。

实现语音合成 API

还记得第一章中的这个演示(如图 3-7 所示)吗?

img/490753_1_En_3_Fig7_HTML.jpg

图 3-7

我们最初的语音合成 API 演示来自第章第一部分

这是一个简单的演示,展示了我们如何实现语音合成——它允许我们在输入字段中输入任何我们希望的文本,然后在要求计算机将文本呈现为语音之前,调整语音和音高等设置。

希望你还有第一章第一章的版本保存为钢笔——你有吗?如果没有,我建议你使用来自readingback演示的代码再次设置它;以后的演示也会用到它!

假设你已经重新设置了它,或者有你之前创建的版本的链接,试着在你的手机上运行它。如果您在输入栏中输入了内容,请点击“朗读”;你应该会发现 Chrome 会把它还原成语音。

问题是用户界面看起来不是很好,不是吗?这是和以前一样的代码,但是这一次我们需要来回滚动——这是一种让人厌烦的方式!具有讽刺意味的是,这整个设置是我提倡只使用 Chrome 的原因之一,至少在不久的将来是这样。我们不需要接触语音合成 API 所需的任何 JavaScript,而是可以专注于调整我们的标记和样式,以更好地适应可用空间。为了理解我的意思,让我们来测试一下,并调整演示以更好地适应您的手机,作为下一个练习的一部分。

适应手机的设计

在下一个练习中,我们将重复使用第一章中的一个演示(我知道,这并不是很久以前的事,尽管看起来可能不是这样!).我们将进行一些调整,以确保它更好地适应有限的可用空间,同时确保功能仍按预期运行。准备好进去看看了吗?

Speaking on a Mobile

让我们按照以下步骤继续更新我们的演示:

img/490753_1_En_3_Fig8_HTML.jpg

图 3-8

我们更新的语音合成 API,运行在手机上

  1. 我们将首先在 https://codepen.io 浏览到 CodePen,然后使用你在第一章中使用的相同账户信息登录。

  2. 切换到 HTML 窗格。然后把页面标题周围的<h1>标签改成<h3>

  3. 接下来,切换到 CSS 窗格,并在该窗格的底部添加以下 CSS 修改:

    /* ADAPTATIONS FOR MOBILE */
    h3 { margin: 0; }
    #page-wrapper { width: 350px; margin: 13px auto; padding: 5px 16px; }
    #voice { vertical-align: super; width: 320px; margin-left: -3px; }
    button { width: 28%; }
    input[type="text"] { padding: 2px 5px; font-size: 16px; }
    
    
  4. 保存笔。如果一切正常,我们应该看到我们的演示的风格已经更新;我们可以在图 3-8 的截图中看到证据。

虽然这是一个过于简单的演示,但它表明如果我们足够勇敢,只在 Chrome 上工作,那么就没有必要改变与语音合成 API 相关的核心功能!我们所要做的只是调整一些样式,让可视 UI 更好地适应可用空间,而核心 JavaScript 代码保持不变。

不过,还有一件小事,那就是……德国被选为我们的默认声音了吗?的确是;在移动设备中使用语音合成 API 的一个奇怪之处在于,与在标准桌面上看到的声音相比,您可能会发现您选择了不同的声音作为默认声音。如果我们点击下拉菜单,我们可以看到德国确实是默认的,如图 3-9 所示。

img/490753_1_En_3_Fig9_HTML.jpg

图 3-9

手机上的默认语音不同…

在这一点上,我敢打赌,你们中的一些人可能会问,“语音识别 API 怎么样?”假设我们选择只使用 Chrome,我们需要做什么样的改变呢?这些都是很好的问题,我很高兴地告诉大家,当涉及到更新我们的代码时,我们可以在这里应用相同的原则。为了理解我的意思,让我们深入了解一下这些变化的更多细节。

实现语音识别 API

尽管对语音识别 API 的支持没有那么先进,但我们完全可以应用同样的变化。如果我们要在手机上运行谷歌 Chrome 的原始演示版本,图 3-10 显示了它的样子。

img/490753_1_En_3_Fig10_HTML.jpg

图 3-10

我们在手机上的原始语音识别演示

不太好,是吧?试着点击一下,跟我说话!按钮,对着电话的麦克风说些什么。它会在屏幕上呈现一些东西,但不容易阅读,对不对?不过最棒的是,为了让这个演示更好地工作,我们只需要对我们的样式做最小的改动。我们的下一个练习将更详细地探讨这些变化是什么。

适应移动应用

在我们刚刚完成的前一个练习中,我们看到了如何调整我们的设计,以使语音合成 API 更好地适应可用空间,并且我们不必更改任何用于创建该功能的 JavaScript 代码。重要的是,我们也可以将相同的原理用于语音识别 API。让我们在下一个练习中探索这意味着什么。

Recognizing Speech on a Mobile

好的,让我们按照以下步骤继续:

img/490753_1_En_3_Fig11_HTML.jpg

图 3-11

使用 API 在手机上识别语音

  1. 我们将首先在 https://codepen.io 浏览到 CodePen,然后使用你在第一章中使用的相同账户信息登录。

  2. 切换到 HTML 窗格,然后将页面标题周围的<h1>标签改为<h3>

  3. 接下来,切换到 CSS 窗格,并在该窗格的底部添加以下 CSS 修改:

    /* ADAPTATIONS FOR MOBILE */
    h3 { margin: 0 0 20px 0; }
    
    #page-wrapper { width: 350px; }
    
    .voice { float: right; margin-top: 5px; }
    
    .response { padding-left: 0px; margin-top: 0px; height: inherit; }
    
    .output_log { font-size: 20px; margin-top: 5px; }
    
    
  4. 保存笔。如果一切正常,我们应该看到我们的演示的风格已经更新;我们可以在背页的图 3-11 中看到这一点的证据,其中口语单词已经被呈现,并且 API 由于不活动而被关闭。

稍等片刻。有些选择器看起来很眼熟,对吧?是的,这确实是正确的,尽管如果你仔细观察在中定义的属性,会发现有一些不同。

我不确定这是纯粹的偶然还是设计(老实说,桌面版本最先出现!),但这表明尽管对这两种 API 的支持在不同的浏览器中并不平等,但基本的 JavaScript 代码在这两种情况下都保持不变。

虽然它确实假设我们只使用 Chrome——虽然你们中的一些人可能会担心这似乎限制了我们的选择,但值得记住的是,API(在撰写本文时)仍处于不断变化的状态,即使它们在现阶段运行得相当好。在这个阶段限制功能是完全可以接受的,因为我们正在提供一个新功能,而且我们可以更容易地监控新功能的使用情况。

好吧,我们继续。我们已经单独讨论了这两个 API,但是把它们放在一起怎么样呢?没问题,这是我们将在本书后面的项目中做得更多的事情,但是现在,让我们看看在为移动环境编码时这可能是如何工作的。

把它放在一起:一个实际的例子

在这一章中,我们已经看到了 API 在移动设备上是如何工作的,如果我们乐于使用 Chrome,这可以减少我们需要做的 JavaScript 修改量!

是时候将这两个 API 结合起来进行本章的另一个演示了——为此,我们将设置一个小应用来告诉我们我最喜欢的城市之一哥本哈根的时间。在这个演示中,我们将使用两个 API——语音识别 API 来请求它告诉我们时间,语音合成 API 给我们响应。让我们开始吧,看看如何更详细地设置我们的演示。

如果你想使用不同的城市,那么你需要改变时区——维基百科有一个广泛的合适时区列表,在 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

Getting Time

要创建我们的演示,请遵循以下步骤:

img/490753_1_En_3_Fig12_HTML.jpg

图 3-12

我们的实际例子

  1. 保存后,我们可以测试结果——为此,请从手机上浏览到 CodePen 演示,然后确保显示编辑器视图,并点击控制台按钮。

  2. 我们不需要做任何事。如果您的浏览器支持任一 API,那么我们将在 CodePen 控制台中看到确认,如图 3-12 所示。

  3. 我们首先从代码下载中提取一个practicalexample文件夹的副本,并将其保存到我们的项目区域。

  4. 接下来,继续浏览到 https://codepen.io 的 CodePen,然后使用你在第一章中使用的相同账户信息登录。

  5. 在左侧,选择“创建➤钢笔”。

  6. 我们需要添加几个外部库来帮助演示,为此,单击设置➤ CSS,然后在对话框底部的槽中添加这两个链接:

    https://use.fontawesome.com/releases/v5.0.8/css/fontawesome.css

    https://use.fontawesome.com/releases/v5.0.8/css/solid.css

  7. 接下来,在同一个对话框中单击 JavaScript–这一次,添加此链接,这将有助于获得我们所选城市的正确时间:

    https://cdn.jsdelivr.net/npm/luxon@1.21.3/build/global/luxon.min.js

  8. 点击保存并关闭。然后将JS.txt的内容复制粘贴到我们笔的 JS 窗格中。

  9. 接下来,打开来自HTML.txt的代码的副本,并将内容粘贴到我们的笔的 HTML 窗格中。

  10. 继续对 CSS.txt 文件做同样的事情,将它粘贴到 CSS 窗格中。

    确保您点击了保存按钮或按 Ctrl+S(或 Cmd+S)来保存您的工作!

在最后一个练习中,我们将两个早期演示的代码放在一起,并修改了 UI,使我们能够获取和显示时间。虽然代码现在应该开始变得更熟悉了,但还是值得花点时间更详细地浏览一下代码,看看 API 如何在移动环境中协同工作。

详细剖析代码

再看一下我们在这个演示中使用的 JavaScript 当然涉及到相当多的内容,但大部分都不是新的。它是从我们在本书前面创建的两个演示中提取的。这同样适用于所使用的 CSS 和标记;虽然我们删除了一些不需要的元素(比如输入字段),但是剩下的都是标准的 HTML,直接来自相同的两个演示。

真正神奇的是我们使用的 JavaScript 在检查我们的浏览器是否支持合成 API 之前,我们通过定义一些变量来缓存元素。理想情况下,我们会在这里包含对识别 API 的检查,但是考虑到我们使用的是 Chrome,我们也会在这里得到同样的正面响应。

接下来是我们以前用过的相同的loadVoices()函数;这将从浏览器中获取可用的声音,并将它们加载到下拉框中。然后我们用onerror事件来捕捉任何问题;这将在控制台日志区域显示任何内容。然后,我们用speak()函数完成这个演示的第一部分,这个函数是我们在前面的演示中创建的,并在我们的实际例子中重用。

该代码块的后半部分用于语音识别 API 第一个函数是一个新函数,它使用 Luxon 时间库来获取我们选择的城市(在本例中是哥本哈根)的当前时间。在将时区切换到哥本哈根并相应地重新格式化时间之前,我们使用luxon.DateTime.local()设置初始时间值。

然后,我们继续将语音识别 API 的一个实例定义为一个对象,然后分配一些属性,比如是否显示interimResults,是否有多个选择,或者是否将 API 设置为运行continuously

接下来是 click 事件处理程序——虽然我们的代码会自动触发麦克风,但我们仍然需要它来启动识别 API,以及设置语言(这里它被设置为en-GB,但如果需要,我们可以将其设置为任何适当的值)。然后我们有和以前一样的事件处理程序,用于speechstartspeechendonspeechend,error。唯一改变的处理程序是result——大部分保持不变,但我们添加了一个块来分割转录的文本,然后确定我们是否说出了单词“Copenhagen ”,如果是这样,就做出相应的响应。

使用移动设备:附录

我们在本章中构建的演示非常简单——如果有人认为生活很简单,实现 API 是轻而易举的事情,这是可以理解的!但是事情并不总是像它们看起来的那样;在我们进入下一章之前,我想留给你一个想法。

在为这本书做研究时,我的初衷是创建一个可以在移动设备上工作的语音控制视频播放器——毕竟,我们为桌面做了一个,所以这应该只是一个调整风格的问题,对吗?嗯,答案是肯定的,也可能是否定的

组装演示非常容易——大部分工作已经在前面完成了,所以我重用了代码并关闭了一些关于关闭服务的选项。然而,语音识别在手机上的工作方式似乎有所不同——是的,Chrome 确实受到支持,但我怀疑不是每个浏览器(支持 API)都提供相同的一致水平的支持!我可以在手机上播放视频演示,但它会很快停止识别服务,或者可能引发网络错误。

我认为这可能是由于 CodePen 的工作方式和它的频繁刷新——演示在桌面上运行良好,因此指出了环境中某个地方的潜在问题!这一点需要记住——您需要彻底测试您的解决方案,以确保它们如客户预期的那样工作。

你可以看到我在 CodePen 创建的视频 demo,在https://codepen.io/alexlibby/pen/xxbpOBN;如果你需要一个视频样本,试着从 https://file-examples.com/index.php/sample-video-files/sample-mp4-files/ 下载一个。

摘要

提到“使用移动设备工作”,你可能会让任何开发人员不寒而栗——如果没有额外的移动平台,跨桌面工作已经够难的了!不过,语音 API 是功能性问题较少的一个领域。在本章的整个过程中,我们已经探索了如何使现有的代码适应移动设备,假设我们乐于根据所使用的浏览器类型来限制暴露。我们已经在本章中讨论了一些重要的主题,所以让我们休息一下,回顾一下我们所学的内容。

我们首先检查了当前对语音合成和识别 API 的支持水平,然后才理解限制接触 Chrome 在短期内有什么意义。然后,我们讨论了如何确定您选择的移动浏览器是否确实支持 API,然后探索了一个快速技巧来计算可用的屏幕空间以及这对 API 的重要性。

接下来,我们进行了两个原始演示(每个 API 一个),并将其转换为在移动平台上工作,然后才理解我们必须做出什么样的改变才能让它们运行。然后,我们通过更新一个更实际的例子来结束这一章,最后,我们思考了如何在移动设备上使用 API。

唷!理论现在结束了。娱乐时间到了!在这一点上,我们开始使用一些示例项目。用你的声音来寻找附近的餐馆,询问时间,甚至支付产品费用怎么样?这些只是我们将在即将到来的项目中介绍的三个技巧;我们将从给出反馈这一至关重要的任务开始,以及我们如何对一个古老的问题进行新的阐述……感兴趣吗?请继续关注我,因为我会反馈我们如何使用 API(是的,这绝对是双关语!)下一章。

四、组合 API:构建聊天机器人

在上几章的课程中,我们已经详细研究了 Speech API,并使用它建立了一些基本的语音功能示例。然而,这仅仅是开始,我们还可以做更多的事情!利用 API 为我们提供了许多创新的想法,这还是在我们对提供给客户的功能进行个性化之前。

在本书的剩余部分,我们将充分利用 API 来构建各种项目,展示我们如何添加语音功能。这将包括从留下语音反馈到结账过程自动化部分等功能。不过现在,我们将把这两个 API 结合在一起,构建一个简单的聊天机器人,它将响应一些基本短语,并在屏幕上显示结果。我相信有人曾经说过,我们必须从某个地方开始,所以没有比设置场景和探索如何使用聊天机器人对我们有益的更好的地方了。

为什么要使用聊天机器人?

那么,我们为什么要使用聊天机器人呢?是什么让他们如此特别?

传统上,公司有客户服务团队,他们可能会处理各种不同的请求,从安排退款到帮助诊断您的互联网接入问题。这将成为一种昂贵的资源使用,尤其是当客户可能经常问同样类型的问题时!只要小心,我们可以创建一个聊天机器人来为我们处理这些问题,这有助于将员工从需要人工干预的更复杂的查询中解放出来。

这是好事吗?好吧,可以,也可以不可以。聊天机器人可以被设置成允许针对特定任务进行基于上下文的对话;虽然这让员工可以处理更复杂的请求,但如果聊天机器人没有配置为最佳体验,同样会导致问题!从某种意义上说,如果我们决定使用机器人,我们应该更加重视让客户感觉特别——它们可以很好地完成日常任务,但如果他们觉得我们对机器人的使用不够完美,我们会被视为廉价并让客户却步。这一点尤其重要,因为 Gartner 等重量级公司预测,到 2020 年,30%的浏览将由使用无屏幕设备的用户完成。这意味着聊天机器人的使用将会增加,特别是在社交媒体领域——毕竟,在哪里最有可能找到人,特别是如果他们需要抱怨糟糕的服务?

构建聊天机器人时需要考虑的事项

好的,我们已经决定我们需要建立一些东西,但是它应该是什么,它应该为谁服务?

这些都是好问题;构建一个机器人不应该被视为省钱的借口,而是可以帮助增加现有人员,让他们承担更高要求或更复杂的查询。不过,我们考虑哪个利基市场并不重要。我们应该将一些最佳实践视为构建 bot 的第一步:

  • 你的顾客或用户会希望只由真人来服务吗?

  • 您的用例是否更适合替代渠道——例如网站或本地应用?

  • 你如何让终端用户知道他们在和一个机器人或者一个真人聊天?对话可能会从前者开始,但有时可能需要将它们交给一名现场代理。

  • 机器人需要处理多少任务?您的机器人可能会收集各种信息,但理想情况下应该负责处理每个流中的一两个项目——这是一个质量超过数量的问题!

  • 机器人会对您的环境产生多大影响——自动化少量任务是否会为您的公司带来真正的好处,或者回报是否不值得付出努力?

在这一点上,您可能认为我们稍微偏离了使用语音 API 的主题,但这是有充分理由的:如果基本对话不是最佳的,那么添加语音功能就没有什么意义!重要的是,不仅要考虑要使用的声音和他们是否可以选择使用哪个等话题,还要考虑对话是否自然,是否包含正确的短语,以及我们的回答是否符合客户在与我们的聊天机器人互动时会使用的短语。

机器人的缺点

创造一个机器人,尤其是一个会说话的机器人,是件好事,但他们可能会有一个潜在的缺点——他们只能模拟人类的互动。一个机器人的好坏取决于它的配置;从功能上来说,它可能是完美的,但如果使用的短语和术语选择不当,那么这只会让人们望而却步!

他们是否会说话并不重要,事实上,如果他们的谈话不自然,增加语音功能只会让客户更加沮丧。任何依靠语音交互来完成在线任务的人都会特别感受到这一点。这意味着,作为使用我们在本书中探索的语音 API 创建东西的一部分,我们绝对需要考虑一些主题,如使用正确的声音,以及在配置我们的机器人时正确的术语或措辞。

这是一个值得进行大量研究的领域——你能承担的越多越好!作为其中的一部分,了解可用的机器人类型很重要,因为这不仅会影响我们如何构建它们,还会影响它们的语音功能。机器人有各种各样的伪装,但可以大致分为两种不同的类型。让我们依次看一下它们,并更详细地看一下它们是如何相互叠加的。

不同类型的聊天机器人

为了帮助理解和最小化使用机器人的缺点,我们可以将它们大致分为两个不同的组:事务型(或无状态)和会话型(有状态)。这对我们意味着什么?嗯,有一些关键的区别:

  • 事务性或无状态机器人不需要历史记录——每个请求都被视为离散的,机器人只需要理解用户的请求就可以采取行动。事务型机器人非常适合自动化快速任务,我们期待简单的结果,例如检索当前的互联网带宽使用情况。

  • 对话式或状态式机器人依靠历史和信息收集来完成任务。在这种情况下,机器人可以提出问题,解析响应,并根据用户的响应确定下一步行动。这种类型的 bot 非常适合自动化更长、更复杂的任务,这些任务有多种可能的结果,但可以在构建过程中预测到。

考虑到这一点,让我们把它变成更实际的东西。我们已经指出,每种机器人类型更适合某些任务;表 4-1 显示了这些任务的一些示例。

表 4-1

bot 类型的一些实例

|

bot 的类型

|

一些实际应用的例子

|
| --- | --- |
| 交易机器人 | 交易型机器人无法记住之前与用户的交互,也无法与用户保持长时间的对话:Alexa 关灯、播放歌曲或启动/解除室内警报通过短信确认预约谷歌助手检查和报告天气 |
| 对话机器人 | 对话机器人维护对话的状态,并在对话之间传递信息:在餐厅预订——机器人需要知道聚会的规模、预订时间和座位偏好,以便进行有效的预订进行多问题调查采访用户以报告问题 |

唷!我们几乎已经到了开始建造我们自己的工厂的时候了。我保证!我知道看起来我们已经讨论了很多理论,但这很重要:增加语音功能只是成功的一半。决定性因素(用一个战斗术语?)是我们需要做的,以确保当我们的机器人说话时,它看起来很自然,并且对我们的客户和我们的初始需求都有预期的效果。

如果你想更深入地研究构建聊天机器人背后的理论,在聊天机器人杂志网站上有一篇很棒的文章,网址是 https://chatbotsmagazine.com/how-to-develop-a-chatbot-from-scratch-62bed1adab8 c

好了,我们终于完成了理论。让我们转向更实际的问题吧!我们将构建一个简单的例子,在屏幕上同时呈现口头和视觉上的响应;和任何项目一样,让我们从设置本章将要构建的背景开始。

设置背景

我的一个朋友给我发了一封电子邮件,提出了一个相当有趣的请求:

“嘿,亚历克斯,你知道我在网上开了一家小公司,卖覆盆子酱包,对吧?嗯,我真的想添加一些东西来帮助我的客户更容易找到工具包!我知道你喜欢尝试新东西。想帮我创造点什么吗?如果可以的话,我很想让它有所创新。有想法吗?”

好吧,我承认:那是一个虚构的朋友,但除此之外,这正是我喜欢做的事情!如果这是一个真实的请求,我的第一反应将是创建一个聊天机器人,我们可以在其中添加语音功能。出于本书的目的,我们将保持简单,仅限于搜索不同的 Raspberry Pi 第 4 版电路板。同样的原则也可以用于搜索其他相关产品(我们将在本章末尾更多地讨论这一点)。

我们将为我虚构的朋友 Hazel(是的,这个名字在早期的演示中应该很熟悉)构建一个聊天机器人,他经营着一家名为 PiShack 的公司。我们的演示将主要基于文本,但包括一些简单的元素,如在我们的对话中显示图像和基本的 HTML 标记。聊天机器人将用于查找 Raspberry 4 板产品,然后在屏幕上显示选择的产品,并向客户提供链接和基本股票信息。

保持事物在范围内

与任何项目一样,我们需要定义应该包括什么的参数,以帮助保持事情在正轨上。

谢天谢地,这对于我们的演示来说非常简单;我们将进行一次基本对话,引导客户从三种板类型中选择一种。根据他们的选择,我们将显示该电路板类型的图像,以及零件号和库存可用性。然后,我们将模拟显示一个虚拟产品页面的链接;对于本演示,我们将不包括产品页面。可以说,这可以链接到现有项目中的任何页面,因为这只是一个标准链接-确切的链接可以生成,这取决于我们的客户的反应。

好吧,记住这一点,让我们继续。现在我们已经设置好了场景,我们需要构建我们演示的各种元素,这样我们就可以更详细地看到它们是如何组合在一起的。

构建我们的演示

对于我们的演示,我选择保持简单,强调除非必要,否则不要使用额外的工具。有几个原因:第一个原因是,为我们的聊天机器人引入依赖关系可能会引入与我们项目中的其他元素不兼容的软件。

有几十个不同的聊天机器人库可用,但我决定在这个项目中使用的是 RiveScript。可从 https://www.rivescript.com/ 获得,它是一种开源脚本语言,有许多不同的语言解释器,如 Python,Go,或者,在我们的情况下,JavaScript。虽然这是个人的选择,但是使用这个库有几个好处:

  • 它(解释器,而不是库)是用纯 JavaScript 编写的,所以依赖性很小;如果需要,可以在 Node.js 下运行一个版本,尽管对于简单的使用来说这不是必需的。

  • 它的语法非常容易学习——您可以非常快速地整理一个基本的配置文件,从而有更多的时间来微调与聊天机器人交互的触发器和响应。

  • 它不是由要求您在线编辑 XML 文件或复杂配置的大型商业公司制作的——您需要的只是一个文本编辑器和您的想象力!

  • 它是开源的,所以如果需要可以修改;如果您有问题,那么其他开发人员可能会帮助提供修复,或者您可以根据自己的需要进行调整。

  • 它可以托管在内容交付网络(或 CDN)链接上,以便从本地点快速访问;以这种方式交付的内容也将被缓存,这使得它更快。如果需要,我们还可以在 CDN 链路无法运行时提供本地备用方案。

在这一点上,有必要问一个问题:我还可以使用什么其他选项?有几十种,但是许多依赖于复杂的 API 或者必须在线管理。这不一定是一件坏事,但是当开始使用聊天机器人,特别是 Web 语音 API 时,它确实增加了一层额外的复杂性!除此之外,让我们花一点时间来更详细地介绍一些现在或将来可能感兴趣的替代方案。

可用的替代工具

在为这本书进行研究时,我遇到了几十种不同的工具和库,它们提供了构建聊天机器人的能力——我选择在我们即将构建的演示中不使用其中的许多工具和库,主要是因为它们已经有一段时间没有更新了,涉及复杂的设置,必须在线设置,或者被绑定到一个专有产品中,如果情况发生变化,很难摆脱这些工具和库。

也就是说,我确实遇到了一些有趣的例子,它们设置起来并不复杂,值得一试:

  • wit . ai-可从 https://wit.ai/ 获得,这个脸书拥有的平台是开源的,易于设置和使用。它有各种可用的集成,包括 Node.js,因此可以很好地与语音识别和合成 API 一起工作。

  • BotUI–这是一个简单易用的框架,可从 https://botui.org/ 获得;它处理每个触发器/响应的结构比 RiveScript 更严格,需要将每一对都构建到主代码中,而不是配置文件中。

  • 如果你曾经花时间为 CMS 系统开发代码,那么你肯定听说过 WordPress。位于 https://botpress.io/ 的 Botpress 将自己描述为“聊天机器人的 WordPress”,在这里任何人都可以为聊天机器人创建和重用模块。这是一个混合产品,虽然主要是开源的,但它也有针对更多企业级需求的许可,因此这将是未来发展的一个好方法。这个库还有一个可视化编辑器,在与 Node.js 集成之前,可以很容易地构建初始的 chatbot 触发器和响应。

需要注意的重要一点是,将语音 API 集成到聊天机器人中不太可能是简单的“轻触开关”或设置配置参数的事情。任何这样的整合都需要努力——多少取决于我们使用的聊天机器人库!

好了,设置的下一步是为我们的文本编辑器添加语法高亮支持。RiveScript 提供了一些更流行的插件,如 Sublime Text 或 Atom。在进入更紧迫的问题之前,让我们赶快把这件事解决掉吧!

如果你使用一个更专业的编辑器或者一个没有在 https://www.rivescript.com/plugins#text-editors 中列出的编辑器,那么请随意跳到下一节;这不会影响演示的操作方式。

添加文本编辑器支持

虽然我们在编辑 RiveScript 文件时几乎可以使用任何语法荧光笔(毕竟它们只是纯文本文件),但添加一个专用的荧光笔绝对有助于使您的代码更容易阅读。

RiveScript 为更受欢迎的编辑提供了几个,分别是https://www.rivescript.com/plugins#text-editors;这包括 Atom、Sublime Text 和 Emacs。图 4-1 展示了在安装了插件的 Atom 中运行代码时的样子(见下页)。

img/490753_1_En_4_Fig1_HTML.jpg

图 4-1

文本编辑器中 RiveScript 语法的屏幕截图

我相信你会同意,这当然有助于阅读(并随后理解)代码!假设你已经安装了一个合适的语法荧光笔(本章后面的截图将展示 Sublime 文本中的例子),让我们继续并完成剩下的准备过程。

将工具放置到位

对于我们的下一个项目,我们将需要利用一些额外的工具来帮助开发和运行。让我们看看我们需要什么:

  • 我们将需要一些网络空间,已经使用 HTTPS 访问安全-你可以使用测试服务器上的网络空间或安装一个本地网络服务器,如 MAMP 专业为此目的。这个 web 服务器特别擅长创建 SSL 证书;您将需要一些东西来允许 Web 语音 API 正确运行!这是一个商业产品,从 https://www.mamp.info 开始提供,适用于 Windows 和 Mac 平台。如果你更喜欢手动操作,那么我建议你试试 Daksh Shah 的脚本,可以在他的 GitHub repo 的 https://github.com/dakshshah96/local-cert-generator/ 找到。这包含安装 Linux 和 Mac 证书的说明;使用“为 windows 安装证书”在线搜索有关如何为 Windows 安装证书的文章。

  • 为了方便起见,我们将为这一章建立一个项目文件夹——出于本书的目的,我将假设你称它为 speech,并且它在你本地 PC 的硬盘上。如果您使用不同的方法,请根据需要调整演示中的步骤。

  • 我们将用来构建聊天机器人的主要工具是 RiveScript 库,可从 https://www.rivescript.com/ 获得。这是一种用于编写聊天机器人的基于 JavaScript 的语言,它有各种不同的接口,很容易学习。该库以 CDN 格式提供,也可以使用 Node.js 安装——为了简单起见,我们将在演示中使用前者。

Note

为了这个演示的目的,我将假设项目区域被设置为在https://speech/下工作;如果您的环境不同,或者您更喜欢继续使用 CodePen,那么演示可以在这种环境下工作;使用本地设置会给你更多的控制。

好了,有了这三个管理任务,让我们继续,开始构建我们的演示!

构建我们的聊天机器人

我们的演示将包含相当多的代码,所以我们将通过两个练习把它们放在一起;在我们继续第二部分之前,这将给你一个休息的机会。第二部分将负责用我们选择的问题和答案来配置聊天机器人;在此之前,让我们先来看看如何设置我们的聊天机器人的功能。

Building the Chatbot, Part 1: The Functionality

我们演示的第一步是获得 RiveScript 的最新副本——可以从 https://www.rivescript.com 获得。出于本练习的目的,我们将使用从unpkg.com获得的 CDN 版本,它已经在 HTML 标记文件中设置好了。

Node.js 也有一个版本——详情请见 https://www.rivescript.com/interpreters#js

让我们继续构建演示的第一部分:

  1. 我们将从本书附带的代码下载中提取一个chatbot文件夹的副本开始;将它保存到我们项目区域的根目录。

  2. 在该文件夹中,在js子文件夹中创建一个新文件,将其另存为script.js

  3. 我们将使用它来为我们的聊天机器人设置功能,为此我们将有相当多的代码。不要担心,我们会一条一条来!我们首先声明将在整个演示中使用的全局变量,并初始化 RiveScript 的一个实例:

    let bot = new RiveScript();
    
    const message_container = document.querySelector('.messages');
    const form = document.querySelector('form');
    const input_box = document.querySelector('input');
    const question = document.querySelector('#help');
    const voiceSelect = document.getElementById('voice');
    
    
  4. 接下来,漏掉一行,然后添加以下函数和函数调用——它们负责将声音加载到我们的演示中:

    function loadVoices() {
      var voices = window.speechSynthesis.getVoices();
    
      voices.forEach(function(voice, i) {
        var option = document.createElement('option');
        option.value = voice.name;
        option.innerHTML = voice.name;
        voiceSelect.appendChild(option);
      });
    }
    
    loadVoices();
    
    // Chrome loads voices asynchronously.
    window.speechSynthesis.onvoiceschanged = function(e) {
      loadVoices();
    };
    
    
  5. 接下来的两个函数完成了我们演示的第一部分——第一个函数负责基本的错误处理,而第二个函数负责在请求时发出声音。添加上一步下面的代码,中间留一个空行:

    window.speechSynthesis.onerror = function(event) {
      console.log('Speech recognition error detected: ' + event.error);
      console.log('Additional information: ' + event.message);
    };
    
    function speak(text) {
      var msg = new SpeechSynthesisUtterance();
      msg.text = text;
    
      if (voiceSelect.value) {
        msg.voice = speechSynthesis.getVoices().filter(function(voice) {
          return voice.name == voiceSelect.value;
        })[0];
      }
    
      speechSynthesis.speak(msg);
    }
    
    
  6. 现在,我们继续设置和配置我们的聊天机器人——我们从声明一个常量来导入我们的触发器和响应开始。下一行需要放在 speak()函数之后,中间留一个空行:

    const brains = [ './js/brain.rive' ];
    
    
  7. 接下来,继续添加这个事件处理程序——它管理任何使用聊天机器人的人提交的每个问题:

    form.addEventListener('submit', (e) => {
      e.preventDefault();
      selfReply(input_box.value);
      input_box.value = “;
    });
    
    
  8. 我们现在需要在屏幕上呈现每个问题(或触发)和适当的回答——这是下面两个函数的责任:

    function botReply(message){
      message_container.innerHTML += `<div class="bot">${message}</div>`;
      location.href = '#edge';
    }
    
    function selfReply(message){
      var response;
    
      response = message.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
      message_container.innerHTML += `<div class="self">${message}</div>`;
      location.href = '#edge';
    
      bot.reply("local-user", response).then(function(reply) {
        botReply(reply);
        speak(reply);
      });
    }
    
    
  9. 本演示的脚本部分已经接近尾声;我们还要添加两个函数和一个事件处理程序。在上一个步骤之后留出一行空白,然后添加以下内容:

    function botReady(){
      bot.sortReplies();
      botReply('Hello, my name is David. How can I be of help?');
    }
    
    function botNotReady(err){
      console.log("An error has occurred.", err);
    }
    
    question.addEventListener("click", function() {
      speak("hello. my name is David. How can I be of help?");
      bot.loadFile(brains + "?" + parseInt(Math.random() * 100000)).then(botReady).catch(botNotReady);
    });
    
    
  10. 现在,继续保存文件,我们可以暂时将其最小化,然后进入本演示的下一部分。

在这个阶段,我们有一个半完整的演示,但是如果我们现在运行它,它不会做很多事情!这样做的原因是,我们还有一部分需要添加:我们聊天机器人的问题和答案。尽管设置这一部分相对简单,但仍有大量代码需要完成。让我们深入了解一下细节。

配置我们的聊天机器人

为了让我们的聊天机器人可以运行,我们将使用 RiveScript 库;它为各种语言提供了不同的解释器,比如 Python、Go 或 JavaScript。

这是一种很容易学习的语言,尽管它有一个怪癖,需要一点时间来适应:我们预先配置机器人的所有问题都必须是小写的!谢天谢地,在屏幕上显示它们时,这不是问题;我将在下一个演示结束时解释更多,但让我们把注意力集中在如何配置我们的聊天机器人并准备好使用。

Building the Chatbot, Part 2: The Functionality

让我们通过添加聊天机器人缺少的问题和答案来完成我们的演示。我们将在每个模块中添加内容,但为了有助于编辑,我还会在不同的地方添加截图,以便您可以检查进度:

  1. 首先创建一个空白文件,将其作为brain.rive保存在我们在本演示的第一部分中创建的 chatbot 文件夹的js子文件夹中。

  2. 在文件的顶部,继续添加这一行——这将强制 RiveScript 编译器使用 2.0 版的 RiveScript 规范:

    ! version = 2.0
    
    
  3. 我们的文件包含了一个简单的 RiveScript 函数,我们用它来确保我们客户的名字是大写的(在本章的后面会有更多的介绍)。留下一行然后加入下面的代码:

    > object keepname javascript
      var newName
      for (var i = 0; i < args.length; i++) {
        newName = args[i]
      }
    
      return newName.charAt(0).toUpperCase() + newName.slice(1)
    < object
    
    
  4. 接下来,我们开始添加每个陈述(成对出现-一个问题和一个答案)。你会注意到每个都以+或-开头;前者是问题或触发器,而–表示回应。继续添加第一个,客户可以向机器人问好并收到适当的响应:

    + hello
    - hello, what is your name?
    
    
  5. 接下来的三个陈述有点复杂,这一次,顾客说出他们的名字,并说出他们想要什么:

    + hi my name is * im looking for a raspberry pi 4
    - <set name=<star>>Nice to meet you, <call>keepname <star></call>. No problem, I have 3 available. Are you looking for a particular version?
    - <set name=<star>>Nice meeting you, <call>keepname <star></call>. No problem, I have 3 available. Are you looking for a particular version?
    
    

此时,如果一切正常,我们的 brain.rive 文件中应该有以下代码,如图 4-2 所示。

img/490753_1_En_4_Fig2_HTML.jpg

图 4-2

brain.rive 文件的第一部分

让我们继续下一部分:

  1. 列表中的下一个问题包含一个条件语句,这一次,我们将询问客户更喜欢查看哪个版本:

    + * versions do you have available
    - I have ones that come with 1 gigabyte 2 gigabyte or 4 gigabyte RAM. Which would you prefer?
    
    
  2. 在接下来的一对陈述中,客户确认他们想要看到哪个版本;我们展示该产品的适当图像:

    + i would prefer the (1|2|4) gigabyte version
    - <set piversion=<star>>Excellent, here is a picture: <img src="img/<star>.webp">
    
    

让我们暂停一下。如果一切顺利,图 4-3 显示了我们现在应该拥有的代码,作为我们brain.rive文件的下一部分。

img/490753_1_En_4_Fig3_HTML.jpg

图 4-3

brain.rive 文件的第二部分

让我们继续添加代码:

  1. 接下来是一个简单的问题——这一次,我们要查看所需产品是否有货:

    + is this one in stock
    - Yes it is: we have more than 10 available for immediate despatch
    
    
  2. 接下来的部分是最复杂的部分,在这里,我们建立了一些关于产品的简要细节和一个链接,让客户可以直接导航到该产品的产品页面:

    + how can i get it
    - No problem, here is a link directly to the product page for the 4 gigabyte version of the Raspberry Pi <get version>:
    ^ <span class="productname"><p><h2>Raspberry Pi 4 - <get piversion>GB RAM</h2><img src="img/<get piversion>.webp"></p><p class="stock">More than 10 in stock</p><p class="stockid">PSH047</p><a class="productlink" href="rasp<get piversion>.html">Go to product page</a></span>
    ^ Just click on Add to Cart when you get there, to add it to your basket. Is there anything else I can help with today?
    
    

在图 4-4 中,我们可以更清楚地看到我们的代码应该是什么样子。

img/490753_1_En_4_Fig4_HTML.jpg

图 4-4

brain.rive 文件中的下一个代码块

  1. 然后,我们以两个问题结束——第一个承认不需要进一步的帮助,第二个是一个通用的总括问题,以防我们的聊天机器人在理解问题时出现问题:

    + no thats fine thankyou
    - You're welcome, thankyou for choosing to use PiShack's Raspberry Pi Selector tool today
    
    + *
    - Sorry, I did not get what you said
    - I am afraid that I do not understand you
    - I did not get it
    - Sorry, can you please elaborate that for me?
    
    

我们可以在图 4-5 中看到 brain.rive 文件的最终部分。

img/490753_1_En_4_Fig5_HTML.jpg

图 4-5

brain.rive 文件的最后一部分

img/490753_1_En_4_Fig6_HTML.jpg

图 4-6

我们完成的聊天机器人,在对话的开始

  1. 此时,继续保存文件,我们现在可以测试结果了!为此,浏览至https://speech/chatbot,点击提问,开始输入信息,如图 4-6 摘录所示。

当您测试您的演示时,您可能会发现使用 RiveScript 的一个特殊的怪癖 brain.rive 文件被缓存,如果您随后对其进行更改,这将使您更难确定您运行的是一个更新的版本!有一个快速的技巧可以帮助你做到这一点,尽管它只在你使用 Chrome 的时候有效。只需点击并按住 reload 按钮,强制其显示一个清除缓存的选项,并执行硬重新加载,如图 4-7 所示。

img/490753_1_En_4_Fig7_HTML.jpg

图 4-7

使用 Chrome 执行硬重新加载

好了,我们完成了构建,现在我们应该有了一个工作聊天机器人的基础,它可以发出每个响应并在屏幕上显示出来。我们在本书的前面已经看到了用于前者的大部分代码。然而,这个演示展示了一些有用的观点,所以让我们深入研究一下这个代码。

详细研究代码

如果我们仔细看看我们刚刚创建的演示中的代码,我可以想象您的第一反应会是什么——哎呀!是的,代码看起来确实有点复杂,但实际上它比乍看起来要简单。让我们从 HTML 标记开始,一块一块地把它拆开。

剖析我们的 HTML 标记

这个文件中的大部分内容相当简单——一旦定义了对 CSS 样式文件的引用,我们就设置一个#page-wrapper div 来包含我们所有的内容。然后我们创建一个.voicechoice部分来放置允许我们选择使用哪种语言的下拉菜单,以及询问问题的初始按钮。

接下来是.chat部分,我们用它来呈现我们与机器人的对话;消息呈现在.messages <div>元素中。然后我们有一个表单来提交每个问题,最后引用 RiveScript 库和我们的自定义script.js文件。

拆开 script . js:Web 语音 API

我们已经介绍了演示中最简单的部分,即标记。这是事情变得更有趣的地方!script.jsbrain.rive文件是最神奇的地方——在前者中,我们将语音/音频代码与我们的聊天机器人功能相结合,而在后者中,我们为我们的聊天机器人存储各种问题和回答。让我们打开script.js文件的副本,更详细地看看我们的聊天机器人演示是如何工作的。

在定义一系列变量之前,我们首先将 RiveScript 的一个实例初始化为一个对象,以缓存 HTML 标记中的各种元素。我们代码中的第一个函数loadVoices(),负责调用语音合成 API 来获取我们将在代码中使用的各种声音,比如英语(英国)。值得注意的是,我们指定引用来调用这个函数两次;这是为了允许一些旧的浏览器(特别是 Chrome),它们要求我们异步加载下拉菜单。大多数情况下,我们会简单的调用loadVoices();对于那些需要它的浏览器,下拉列表将使用来自window.SpeechSynthesis接口的onvoiceschanged事件处理程序来填充。

继续,我们创建的下一个函数是onerror事件处理程序,同样来自于window.SpeechSynthesis接口;这是对使用接口时出现的任何错误的基本概括。现在,我们简单地呈现使用event.errorerror.message给出的错误类型。值得注意的是event.error会给出一个具体的错误代码,比如音频捕获。任何error.message语句都应该由我们作为开发者来定义;该规范没有定义要使用的确切措辞。

错误代码列表可在 MDN 网站 https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionError/error 获得。

这部分代码分解的最后一个功能是speak()——这是我们表达内容的地方!首先初始化一个新的SynthesisUtterance实例,然后定义要使用的文本(例如,来自机器人的响应),以及应该使用的声音。假设没有发现问题,那么就由 API 使用speechSynthesis.speak(msg)语句来说。

唷!我们已经完成了演示的大部分,尽管还有一部分:配置我们的机器人!我建议在这个阶段休息一下——也许去喝一杯或者呼吸一些新鲜空气。一旦你准备好继续,让我们继续探索用于配置我们的机器人更详细的语句。

了解我们的机器人是如何配置的

尽管大部分神奇的事情发生在我们的script.js文件中,但是如果没有 bot 配置文件brain.rive,我们的演示将是不完整的。

快速浏览一下这个文件,我们应该能认出一些元素——毕竟,它的大部分看起来像纯文本,开头是一些基本的 JavaScript 代码,对吗?是的,你这么说是对的,但是 RiveScript 有一些不寻常的字符关键字,我们需要在这段代码中注意。让我们从代码的顶部开始,一点一点地浏览它——在我们这样做之前,现在是一个快速了解 RiveScript 如何工作的好机会。

探索 RiveScript 如何工作:概述

我们使用 RiveScript 创建的任何配置都存储为.rive文件。.rive文件的一个共同特征是,你会看到大多数行以感叹号、加号或减号开头,箭头和括号会出现几次,就像我们的例子一样。这很重要,因为它们定义了所使用的语句类型。表 4-2 中列出了我们在示例中使用的方法。

表 4-2

brain.rive 中使用的特殊字符类型

|

性格;角色;字母

|

目的

|
| --- | --- |
| +或加号 | 这表示来自用户的触发问题。 |
| *或星形 | 这充当了一个占位符来接受来自用户的数据,比如一个名字或者像“哪个版本…”或者“什么版本…”这样的问题(就像我们的例子一样)。 |
| -或者负号 | 这充当了机器人对用户的响应。 |
| ( )和|或括号和管道符号 | 当一起使用时,这表示一种选择——rive script 将以类似于星号占位符的方式处理它接收到的内容,但这一次,我们将选择限制在三个选项中的一个,即 1、2 或 4。 |
| ^或箭头 | 这是一个换行符,在这种情况下,响应最好在多行中提供。 |
| !或者感叹号 | 这表示一个 RiveScript 指令,比如指定使用哪个版本的规范。 |

在大多数情况下,我们可能会使用加号或减号(如我们的例子)。考虑到这一点,让我们更详细地逐一探究这些语句。

详细剖析 brain.rive 文件

我们从! version=2.0开始,它告诉 RiveScript 我们正在开发库的 2.0 规范;如果这被设置为一个较低的数字(例如,早期版本),那么我们的代码就有可能无法按预期工作。

现在,我们将跳转到第 12 行,在那里我们有+ hello——我们将很快回到> object...< object标签中的代码。第 12 行的代码应该是不言自明的;此时,用户将输入 hello 作为我们的初始触发器,机器人将对此做出相应的响应。

下一个街区更有趣一点。这里,我们使用+符号指定了一个触发问题;在这个例子中,我们使用了星星。星号是用户给定的一段特定文本的占位符——例如,如果他们使用了名称标记,那么给定的文本将等同于"hi my name is Mark im looking for a raspberry pi 4."。这本身很简单,但是看看响应:我看到的<call>标记是什么?那么<set name=....>代码又是怎么回事呢…?

前者是对 RiveScript/JavaScript 函数的调用。还记得我在本节开始时说过要跳过的代码吗?这就是这个函数的代码——我们用它来确保不管传递给它什么名字,它总是以首字母大写的形式出现在屏幕上。值得注意的是,RiveScript 在触发器中使用时将始终以小写形式格式化变量;我们使用这个函数来显示更适合用户的东西。

接下来的三个问题遵循类似的原则,其中触发器文本是小写的,我们在这三个问题的第一个中使用了一个星号占位符。不过有一个例外:管道和支架的使用。这里我们指定了一些可以被识别的选项;与星号不同的是,任何东西都可能与陈述相匹配,唯一允许的匹配将是数字12,4。然后,我们利用<star>占位符中匹配的数字来设置一个名为piversion的变量(我们稍后会用到),作为插值标签的一部分,为所选版本的 Raspberry Pi 板显示适当的图像。

继续前进,下一个街区是最大的——它看起来很吓人,但实际上,它并没有那么复杂!有两件事需要注意:首先,我们<get piversion>并使用它在屏幕上的一小块 HTML 标记中呈现产品名称和图像。第二种是使用^或帽子符号;这允许我们将来自 bot 的响应分成几行。我相信你可以想象,像我们这样的一段文字如果组合成一行,看起来会很糟糕。这使我们更容易在屏幕上观看。

然后,我们以两个触发器结束–最后一个触发器来自客户,确认这是他们唯一需要帮助的事情,以及来自机器人的适当确认。最后一个触发器是一个通用的总括触发器,如果出现问题,它就会发挥作用:这很可能是因为用户输入了与我们预先编写的响应不匹配的内容。我们提供了许多可供机器人使用的替代方案;如果它需要在与用户的对话中使用它,它会依次自动选择一个。

唷!这是一个冗长的解释。如果你能走到这一步,那就太好了!在这个演示中有很多内容要介绍,但是希望它向您展示了在使用自动聊天机器人时,我们如何利用 Speech API 来添加额外的维度。我们只是触及了可能的表面,更不用说我们应该考虑的了;后者有几个要点,让我们暂停一下喘口气。去喝杯咖啡或饮料,让我们继续深入一些领域,在这些领域中,我们可以将我们的演示开发成一个功能更加完整的示例。

更进一步

在本章的过程中,我们构建了一个简单的聊天机器人,允许我们从三个 Raspberry Pi 4 板中选择一个,并询问它们的可用性以及如何购买。这是一个直截了当的请求,但是正如您可能已经看到的,还有一些改进的空间!

在这种情况下,我们应该如何调整体验呢?一个领域是我们用过的触发问题;它们有些僵硬,感觉不像我们演示中那样自然或直观。这是需要考虑的一个方面。以下是一些帮助你开始的想法:

  • 添加多语言支持–虽然英语被广泛使用,但并不是每个人都会说英语!由于文化差异,它也引入了误解的风险;能够用客户的母语交谈消除了这种风险,让他们感到更受欢迎。

  • 让它成为一个双向的过程——我们关注的只是视觉上和口头上呈现我们的回答,但是如何让你也可以用语言表达你的问题呢?这将特别吸引那些可能有障碍的人,在那里使用键盘将是困难的或不可能的。

    在本书的后面部分,当我们构建 Alexa 的(简单)克隆时,我们会看到类似的东西。

  • 微调使用的短语——我们使用的短语是有目的的,但我认为还有改进的空间。例如,我们可能会将某些单词缩写,如“我是”缩写成“我是”,但我们的聊天机器人不允许这样做!当然,这可能更多地与我们如何配置聊天机器人有关,但不要忘记,我们在聊天机器人中放入的内容最终会影响它作为语音输出的方式。

  • 包括其他产品——重要的是要考虑我们如何才能最好地做到这一点,以及语音合成配置所需的变化;这种改变需要使得将来添加其他产品变得更加容易,并且最大限度地减少麻烦。

我确信,为了开发我们的项目,我们能够或者可能想要做更多的事情,但是现在,我想把注意力集中在一个特别的变化上:增加语言支持。

网络语音 API 的一个伟大之处在于,我们不以任何方式仅限于英语。我们完全可以添加对多种不同语言的支持!为了证明这一点,在我们的下一个演示中,我们将通过添加法语支持来更新原始聊天机器人。让我们更详细地看看需要做哪些更改来实现这次更新。

添加语言支持

对于这个演示,我们将使用原始聊天机器人的现有副本,但添加了语言支持-我选择了法语,因为我会说法语。我们可以很容易地修改代码以使用不同的语言,或者根据需要使用多种语言。我们将通过几个步骤来更新我们的代码。让我们更详细地看看需要什么。

这个演示使用了 www.gosquared.com/resources/flag-icons/ 的旗帜图标——如果你喜欢使用不同的东西,你也可以使用你自己的。

更新我们的演示

为了更新我们的演示,我们需要进行四项更改:

  1. 第一个是更新我们的标记和样式,以便我们添加我们使用的每个国家的旗帜——在本例中,是美国英语和法语。

  2. 我们需要基于设置一个变量来更新语音合成配置,以接受我们的语言选择。

  3. 接下来是翻译——我们必须创建一个翻译成每种新语言的brain.rive配置文件的版本,并重新配置我们的script.js文件以适当地导入每个版本。

  4. 所需的最后一项更改是添加事件处理程序,根据需要将SpeechSynthesisUtterance.lang设置为我们选择的语言。

考虑到这一点,让我们开始设置我们的演示吧!如前所述,我们将增加法语语言支持——如果您愿意,可以随意将其更改为另一种语言,但您需要手动更新brain.rive文件中的翻译文本。

Adding Language Support

开始之前,我们需要做几件事:

  1. 从本章前面的原始演示中复制一个您创建的chatbot文件夹,并将其作为chat language保存在我们的项目文件夹的根目录下。

  2. 从本书附带的代码下载副本中,解压brain config文件夹,并将内容复制到chat language文件夹下的js子文件夹中。这些包含了我们的brain.rive文件的更新版本,有英语和法语版本。

  3. 从相同的代码下载中,继续提取img文件夹——将其保存在chat language文件夹中img文件夹的顶部。这将添加我们将在演示中使用的两个旗帜图标。

一旦你完成了这个,继续这些步骤:

  1. 我们需要做的第一组改变是在我们的标记中——我们将引入两个标志作为语言选择器。打开index.html,查找从<button id="help"...开始的代码行,然后在它之前添加该代码块:

    <section class="flags">
      <span class="en-us"><img src="img/en-us.png" alt="en-us">EN</span>|
      <span class="fr-fr"><img src="img/fr-fr.png" alt="fr fr">FR</span>
    </section>
    
    
  2. 接下来,将 disabled 属性添加到<button>标签中,如下所示:

    <button id="help" disabled>Ask a question</button>
    
    
  3. 继续保存此文件-保持打开状态,但现在可以将其最小化。

  4. 在这一点上,切换到scripts.js文件——我们在这里做了一些改变,从定义一些额外的变量开始。在第一行代码之后,添加如下声明:

    let bot = new RiveScript();
    let langSupport, intro, brains;
    
    
  5. 接下来,我们需要缓存更多的元素作为变量——为此,继续添加以下四行代码,紧跟在const question =...行之后:

    const voiceSelect = document.getElementById('voice');
    const english = document.querySelector(".en-us");
    const french = document.querySelector(".fr-fr");
    const voice = document.querySelector(".voicechoice");
    
    
  6. 现在我们引入了多语言支持,我们不能硬编码我们的初始问候。相反,我们将把它们作为变量提供,所以在前面的代码块之后留下一行,并添加这两个声明:

    const enIntro = "Hello. my name is Hazel. How can I be of help?";
    const frIntro = "Bonjour. Je m'appelle Hélène. Comment puis-je vois aider?";
    
    
  7. 向下滚动直到到达speak()功能。到目前为止,该语言被隐式设置为'en-us';这需要改变!为此,查找speakSynthesis.speak语句,然后修改该函数的最后一部分,如下所示:

        })[0];
      }
    
      msg.lang = langSupport;
    
      speechSynthesis.speak(msg);
    }
    
    
  8. 接下来,删除以const brains = [...开头的行,替换为:

    function setLanguage(langUsed, selIndex, langIntro) {
      voiceSelect.selectedIndex = selIndex;
      langSupport = langUsed;
      intro = langIntro;
      brains = [ './js/brain-' + langSupport + '.rive' ];
      question.disabled = false;
    }
    
    
  9. 我们现在需要添加两个函数来处理当我们点击标志时会发生什么——为此,留下一行,然后放入下面的代码:

    english.addEventListener("click", function() {
      setLanguage('en-us', 3, enIntro);
      question.innerHTML = "Ask a question";
    });
    
    french.addEventListener("click", function() {
      setLanguage('fr-fr', 8, frIntro);
      question.innerHTML = "Poser une question";
    });
    
    
  10. 在我们完成编辑这个文件之前,还有两个变化要做——下一个变化是改变botReady()函数。向下滚动到它,然后按照下面突出显示的内容进行编辑:

```html
function botReady(){
  bot.sortReplies();
  botReply(intro);
}

```
  1. 最后要做的改变是相似的——这里我们需要改变我们称呼开场白的方式。向下滚动到问题事件处理程序,然后根据指示更新speak()调用:
```html
question.addEventListener("click", function() {
  speak(intro);
  bot.loadFile(brains + "?" + parseInt(Math.random() * 100000)).then(botReady).catch(botNotReady);
});

```
  1. 保存文件-我们现在可以最小化它。

  2. 接下来,启动styles.css文件,并在文件末尾添加以下样式:

```html
/* flags */
section.flags {
  width: 150px;
  float: right;
  margin-top: -30px;
}

section.flags img { vertical-align: middle; padding-right: 5px;}

section.flags img:hover { cursor: pointer; }

button { width: 30%; padding: 10px 15px; }

```
  1. At this point, go ahead and save the file – we can now test the results! For this, browse to https://speech/chatbot, then click Ask a question, and start to enter information as shown in the extract in Figure 4-8.
![img/490753_1_En_4_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/intro-h5-web-speech-api/img/490753_1_En_4_Fig8_HTML.jpg)

图 4-8

我们更新的演示,现在有法语语言支持

唷!又一个怪物演示!这看起来很多,但实际上大部分代码都是一次性的修改;这将是我们需要适应转换成使用不同语言的代码。

一旦完成,接下来就是简单地添加标志(和它的标记),潜在的一点样式,以及每个附加标志所需的事件处理程序。诚然,我们的代码可以写得更有效,以自动识别新的标志并正确处理它们,但嘿,我们必须从某处开始!

剖析代码

好吧,改变一下策略,我们在最新的更新中涵盖了很多变化,那么这些变化是如何适应的呢?乍一看,看起来我们确实做了一些改动,但实际上,我们的演示并没有什么异常复杂的地方。也就是说,让我们花点时间更详细地回顾一下我们所做的更改。

我们从添加标志标记开始;这是标准的 HTML,用来在聊天机器人的右边显示旗帜图标。我们为此添加了一个容器——将来我们可以很容易地添加更多的行,这些行指向我们想要添加到演示中的任何附加标志。同时,我们为按钮添加了一个禁用的属性——这是为了防止人们在点击其中一个标志之前使用它。

接下来,我们添加了一些额外的变量,其中一些将用于缓存页面上的新标志元素。然后我们添加了两个最重要的变化——第一个是敬礼。我们不能将这些硬编码到我们的演示中,所以我们需要将适当的文本作为变量传入(在本例中是enIntrofrIntro)。我们接着在这行字里加上:msg.lang = langSupport;这阻止了SpeechSynthesis界面默认假设语言支持是美国英语,当点击我们选择的标志时将会是任何语言。

接下来的三个变化更加重要——这里我们设置了一个通用的 setLanguage()函数,将voiceSelect下拉菜单更改为我们选择的语言(对于法语,它选择 Google French,依此类推)。然后,我们为 SpeechSynthesis 接口设置适当的 BCP47 代码(例如,"fr-fr"代表法语),并使用它来定义我们应该使用哪个大脑配置文件(在本例中,应该是brain-fr-fr.rive)。如果一切正常,我们就从“提问”按钮中删除 disabled 属性,这样我们的客户就可以使用它了。

接下来的两个事件处理程序调用我们刚刚定义的setLanguage()函数,我们将适当的 BCP47 代码、要使用的语音索引和我们的开场白传递给它。与此同时,我们还更新了“提问”按钮上的文本,根据选择的按钮,文本可以是英语,也可以是法语。虽然它们的工作方式相似,但我们已经设置它们为所选语言传递适当的值——对于我们决定添加到演示中的任何其他语言,这些值都是重复的。

剩下的两个变化非常简单——因为我们不能硬编码开场白,我们必须将文本作为变量传入。这里我们使用了一个通用的 intro 变量,在演示的前面,我们已经将特定语言变量的文本传递给了这个变量。

摘要

聊天机器人是一项肯定会存在的技术——研究表明,它们的使用将在未来几年内呈爆炸式增长,因此确保它们尽可能有效并且客户参与度不会因此下降非常重要!我们已经讨论了一些要点,关于如何在使用聊天机器人时添加语音合成 API 来提供额外的优势;让我们花点时间来回顾一下我们在这一章中学到了什么。

在为本章的项目演示做准备之前,我们从一些基本的理论开始,比如为什么我们应该使用聊天机器人,可以使用的不同类型,以及使用它们的一些缺点。我们花了一点时间来构建我们的演示的各种元素,然后讨论了一些我们将来可能会考虑使用的替代方案。

然后,我们进入了构建我们的机器人的重要阶段——在运行构建和配置我们的机器人的主要步骤之前,我们首先添加了文本编辑器语法支持。构建完成后,我们详细研究了我们创建的代码,包括完成使我们的 bot 正确运行的配置文件。然后,我们总结了这一章,看看我们可以做些什么来改进我们的机器人,特别强调在我们的演示中添加额外的语言支持。

唷!当然是通过聊天机器人的短暂停留之旅!当我们试着构建一个 Alexa 克隆时,我们将在后面重新讨论本章涉及的一些主题,但是让我们做一些要求稍微低一点的事情。你遇到过多少次在网站上留下反馈的请求?通常这可能是通过电子邮件,甚至是评论部分。不过,我们有可能必须以书面形式提供反馈。太老套了。如果我们可以口头上做,然后让网站把它转换成文本呢?是的,这看起来像是极度的懒惰,但是,嘿,我完全支持创新!好奇吗?好吧,听我说,我将在下一章揭示一切。

五、项目:留下评论反馈

你多久会觉得有必要留下关于购物体验的反馈?希望你至少这样做过一次;虽然我怀疑是否有人会发现它并对此做些什么,但这种怀疑还是存在的。

不管你留下了什么样的反馈,你都有可能必须输入你的评论;如果你能用你的声音做到这一点会怎么样?是的,虽然看起来很新奇,但这是展示使用语音 API 的完美方式。在这一章中,我们将建立一个基本的产品页面,并添加语音反馈功能,它会自动将我们的评论转录成书面文本。

设置场景

你浏览时遇到的几乎每一个电子商务网站都会有某种形式的反馈机制——它可能是专门建立的事情,或者是由合作伙伴或供应商作为第三方服务提供的东西。冒着听起来乏味的风险,它是如何提供的几乎无关紧要。任何在互联网上进行交易的公司都应该提供某种形式的机制;否则,他们很可能会很快失去客户!

在大多数情况下,反馈表格通常是你必须打出你的回答的表格——这没有错,但这是一种老派的做事方式。事实上,有人可能会问,“还有什么其他选择?”你可以使用调查问卷,但最终,提供的定性反馈同样重要,如果不是更重要的话!

如果我们能把事情颠倒过来,口头上提供给他们,会怎么样?是的,你没听错——与其花时间费力地把它打出来,不如让我们口头表达出来。听起来很复杂,对吧?嗯,也许不是。我们已经介绍了语音识别 API 形式的基本工具。让我们来看看设置它需要什么,以及它如何成为一个真正强大的工具。

保持事物在范围内

为了让这个项目成功,你可能会想我们需要很多额外的工具,对吗?错,我们不需要任何!在我解释原因之前,让我们快速了解一下我们将在这个项目中包括哪些内容,以及哪些内容将超出范围:

  • 我们将把我们的演示限制在记录和转录口头反馈,然后在屏幕上呈现出来——后者将带有适当的日期和时间戳。

  • 我们的演示最初将侧重于用英语记录反馈,但在本章的后面,我们将着眼于提供对至少一种其他语言的支持。

  • 我们不会将我们的评论中留下的任何内容记录到数据库或通过电子邮件提交;这超出了本演示的范围。

考虑到这一点,让我们来看看我们演示的架构,更详细地了解一下其中涉及的内容。

构建我们的演示

在前一节的开始,我做了一个看起来很大胆的声明,我们不需要任何额外的软件来设置我们的反馈:是时候兑现这个承诺了!好吧,开始了。

在某种意义上,我们不需要任何额外的软件——核心功能可以通过使用语音识别 API 来提供,并使用标准功能来配置它,以记录和转录口语内容。然而,如果我们确实想做一些事情,比如记录反馈供以后阅读,那么是的,我们显然需要一个合适的存储系统和适当的中间件来解析和存储内容。然而,这超出了本书的范围——我们将重点关注如何将内容转录并呈现在屏幕上。

建立我们的评论小组

现在我们已经介绍了我们架构的基本部分,让我们开始构建我们的演示——我们将首先关注构建核心审查面板,然后在本书的后面部分探索如何添加多语言支持。

值得注意的是,我们将主要关注使我们的演示工作所需的 JavaScript 所有的 HTML 和 CSS 样式都将预先配置,直接来自本书附带的代码下载。

Building the Review Panel

本章项目的第一步是构建评论面板,但在开始之前,我们需要做一件事。继续从本书附带的代码下载中提取 reviews 文件夹的副本——保存到我们的项目区域。

准备就绪后,让我们开始编写演示代码:

如果您在演示过程中遇到任何问题,那么在本书附带的代码下载中有一个完成版本——它在 reviews 文件夹中,在 finished version 子文件夹下。

  1. 我们首先打开一个新文件,然后将它作为scripts.js保存到reviews文件夹中的js子文件夹中。

  2. 我们有一大块代码要添加,我们将一个块一个块地添加——第一个是一组引用 DOM 中各种元素的变量,再加上一个我们在说话时用作占位符的变量:

    var transcript = document.getElementById('transcript');
    var log = document.getElementById('log');
    var start = document.getElementById('speechButton');
    var clearbtn = document.getElementById('clearall-btn');
    var submitbtn = document.getElementById('submit-btn');
    var review = document.getElementById('reviews');
    var unsupported = document.getElementById('unsupported');
    var speaking = false;
    
    
  3. 接下来,我们需要设置脚本的基本框架——我们用它来确定我们的浏览器是否支持语音识别 API。在变量后留一个空行,然后添加这个块:

    window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || null;
    
    if (window.SpeechRecognition === null) {
      unsupported.classList.remove('hidden');
      start.classList.add('hidden');
    } else {
      ...add code in here...
    }
    
    
  4. 我们现在可以开始添加我们的演示代码了——我们从初始化和配置语音识别 API 的实例开始。继续用下面的代码替换...add code in here...行:

    var recognition = new window.SpeechRecognition();
    
    // Recogniser doesn't stop listening even if the user pauses
    recognition.continuous = true;
    
    
  5. 现在已经初始化了 API 的一个实例,我们可以开始响应事件了。第一个是 onresult 处理程序;为此,在步骤 3 中的代码后留下一行,然后添加这个事件处理程序:

    // Start recognising
    recognition.onresult = function(event) {
      transcript.textContent = ";
      for (var i = event.resultIndex; i < event.results.length; i++) {
        if (event.results[i].isFinal) {
          transcript.textContent = event.results[i][0].transcript;
        } else {
          transcript.textContent += event.results[i][0].transcript;
        }
      }
    };
    
    
  6. 接下来,我们需要为任何出错的情况设置陷阱——为此,在 onresult 处理程序后留出一行空白,然后添加以下代码:

    // Listen for errors
    recognition.onerror = function(event) {
      log.innerHTML = 'Recognition error: ' + event.message + '<br />' + log.innerHTML;
    };
    
    
  7. 我们现在正处于本演示最重要的部分之一——开始和停止记录我们的反馈的方法!我们还要添加两个事件处理程序,所以让我们添加第一个,它将在我们开始或停止记录时触发。在第 5 步的代码后留一行空白,然后添加:

    start.addEventListener('click', function() {
      if (!speaking) {
        speaking = true;
        start.classList.toggle('stop');
    
        recognition.interimResults = document.querySelector('input[name="recognition-type"][value="interim"]').checked;
        try {
          recognition.start();
          log.innerHTML = 'Start speaking now - click to stop';
        } catch (ex) {
          log.innerHTML = 'Recognition error:' + ex.message;
        }
      } else {
        recognition.stop();
        start.classList.toggle('stop');
        log.innerHTML = 'Recognition stopped - click to speak';
        speaking = false;
      }
    });
    
    
  8. 第二个事件处理程序负责提交我们转录的记录作为反馈——为此,在开始处理程序后留出一行空白,并放入以下代码:

    submitbtn.addEventListener('click', function() {
      let p = document.createElement('p');
      var textnode = document.createTextNode(transcript.value);
      p.appendChild(textnode);
      review.appendChild(p);
    
      let today = dayjs().format('ddd, MMMM D YYYY [at] H:HH');
      let s = document.createElement('small');
      textnode = document.createTextNode(today);
      s.appendChild(textnode);
      review.appendChild(s);
    
      let hr = document.createElement('hr');
      review.appendChild(hr);
      transcript.textContent = ";
    });
    
    clearbtn.addEventListener('click', function() {
      transcript.textContent = ";
    });
    
    
  9. We’re almost there. All that remains is to save our code, so go ahead and do that now. Once done, fire up your browser, and then browse to https://speech/reviews/. If all is well, we should see something akin to the screenshot in Figure 5-1.

    img/490753_1_En_5_Fig1_HTML.png

    图 5-1

    我们已完成的审查系统

现在,我们应该有了一个工作演示,我们可以对着麦克风说话,识别语音 API 将其转录为书面内容。虽然看起来我们已经写了相当多的代码,但基本原理和我们在第一章第一次见到的一样,并在第二章第三章开始发展。为了理解我的意思,让我们深入到代码中,更详细地了解它是如何结合在一起的。

详细分解代码

我相信有人曾经说过,我们必须从某个地方开始——没有比为我们的演示预先配置的 HTML 标记更好的地方了。

如果我们仔细看看,不应该有任何异常复杂的东西;这个演示使用标准的 HTML 和 CSS 来构建我们的基本表单页面。除此之外,让我们快速查看一下为我们设置的更详细的内容。

探索 HTML

核心部分以一个用于评论的空

开始,后面是不支持的 div,如果浏览器不支持 API,我们用它来通知。

接下来,我们设置“添加您的评论”部分,为此,我们有两个单选按钮,#final#interim。这些分别控制 API 是在最后还是在我们说话的时候呈现转录的代码。然后我们有了我们的#transcript文本区域,我们把它设置为只读;单击start按钮后,我们开始在这里添加内容。

完成后,单击开始按钮将关闭麦克风。然后我们有习惯的 submit 按钮,它将内容发布到屏幕上的 reviews div 中。这是通过调用 DayJS 库来完成的——它用于格式化每个评论中发布的日期。当我们剖析这个演示的脚本时,我们将很快回到这个问题。

探索 JavaScript

相比之下,我们的 JavaScript 代码显然更复杂——这可能会让您望而却步,但不用担心。这不是我们以前没有用过的东西,至少在 API 的范围内是这样的!让我们更详细地分解代码,看看它们是如何组合在一起的。

我们首先在标记中声明对各种元素的引用,然后调用window.SpeechRecognition来确定我们的浏览器是否支持 API。如果呈现为 null,我们会显示一条措辞恰当的消息;否则,我们首先将 API 的一个实例初始化为识别。同时,我们将.continuous属性设置为 true,以防止 API 在一段时间后或在不活动的情况下停止监听。

我们使用的第一个事件处理程序(也可以说是最重要的)是onresult——它负责记录我们所说的内容。重新审视这一点很重要,特别是event.results[i][0].transcript的使用。

我们可以在图 5-2 中看到这个功能的截图。

img/490753_1_En_5_Fig2_HTML.jpg

图 5-2

我们演示中的 onResult 函数

一旦我们遍历了所有的results,任何包含内容的都将作为类型为SpeechRecognitionResultList的对象返回;这包含了SpeechRecognitionResult对象,使用 getter 属性可以像访问数组一样访问这些对象。

第一个[0]返回位置0处的SpeechRecognitionResult——这实际上是最终答案,应该呈现在屏幕上。然而,如果已经设置了speechRecognition.maxAlternatives属性,我们将会看到存储在SpeechRecognitionAlternative对象中的替代项。在我们的例子中,没有设置 maxAlternatives 属性,所以屏幕上显示的只是最终答案。

相比之下,下一个事件处理程序很简单——这里我们截取onerror,并呈现屏幕上生成的任何错误,以及相应的消息。

这可能从无演讲到中止演讲——你可以在 Mozilla MDN 网站 https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognitionError/error 上看到完整的列表。

接下来,我们有三个事件处理器中的第一个,用于记录、转录和submit(或显示)我们的反馈。第一个是 start,连接在麦克风按钮上;我们计算出我们是否已经在说话。如果没有,我们就激活麦克风,然后再决定是显示中期结果还是最终文章。然后我们运行一个try...catch块,在其中我们运行recognition .start()来开始记录我们的讲话。完成后,我们停止语音识别 API 并将样式翻转回来,准备再次开始记录。

第二个事件处理程序与submitbtn相关,允许我们将屏幕上的内容提交到反馈区域。我们首先使用createElement('p')动态创建一个段落,然后将transcript.value的内容分配给它。然后,我们使用 DayJS 库计算并格式化记录的日期——我们当然可以使用标准的 JavaScript,但是使用 JavaScript 时,日期操作可能会很笨拙!

如果你想了解更多关于这个库的信息,可以在 https://github.com/iamkun/dayjs 下载 DayJS 库。

然后,在我们添加一个动态生成的水平规则元素以将其与下一个评论反馈分开之前,使用review.appendChild(s)将这些内容和抄本的内容一起添加到 DOM 中的评论区域。在第三个也是最后一个事件处理程序中,我们使用clearbtn来触发清空脚本文本区域的内容,这样就可以准备好记录下一个评论了。

现在,我们有了一个工作演示,这很好,但是在一个更现实的环境中,比如一个产品页面,托管怎么样呢?如果我们已经正确地计划了我们的演示,这应该是将代码复制到更大的模板中的问题,我们不应该对代码做太多的修改。让我们开始吧,看看会发生什么…

将其添加到产品页面

对于我们的下一个演示,我们将把评论演示合并到一个新生的 Raspberry Pi 零售商的基本产品页面中——我创建了一个非常基本的页面,它肯定不会赢得任何奖项,但应该足以看到我们的评论小组在更实际的环境中工作!让我们进去看看。

Demo: Merging the Review Panel

在我们开始之前,我们需要在您的文本编辑器中打开 reviews demo 和 product page demo 的源文件夹——两者的副本都在本书附带的代码下载中的 merge 文件夹中。

出于演示的目的,我将使用文件夹名productpagereviews来区分原始的源演示。

在继续这些步骤之前,请确保两个文件夹都已在文本编辑器中打开:

img/490753_1_En_5_Fig3_HTML.jpg

图 5-3

合并后的审查小组

  1. 我们需要做的第一个更改是 reviews 文件夹中的 index.html 文件——请注意这一行:<div id="reviews">

  2. 从这一行复制到(并包括)<div id="log">Click the microphone to start speaking</div>。然后将它粘贴到 productpage 文件夹中的index .html文件中的这一行-<h1>Product Reviews</h1> –下面。

  3. 接下来,从 productpage 文件夹的index.html文件中删除这一行:

    <p>Insert reviews block here</p>
    
    
  4. 我们的评论小组使用 DayJS 库来格式化发布评论的日期——为此,我们需要将调用转移到 DayJS 库。继续将下面的代码行:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.8.16/dayjs.min.js"></script>
    
    

    添加到 productpage 文件夹中脚本文件的调用之上:

    <script src="js/scripts.js"></script>
    
    
  5. 我们现在需要更新样式以允许添加审查面板——为此,继续将所有样式从审查版本的styles.css文件复制到 productpage 文件夹的 CSS 文件中。

  6. 我们快完成了。继续将 reviews 文件夹中的scripts.js文件的内容复制到 productpage 文件夹中的scripts.js文件的顶部。

  7. 我们需要为我们的麦克风按钮复制整个 mic.png 图像–将img文件夹从 reviews 文件夹复制到 productpage 文件夹。

  8. 最后一步是删除这两行:

    <h1>Product Reviews</h1>
    <p>Insert reviews block here</p>
    
    
  9. 继续保存文件,我们现在可以预览我们的结果。为此,请浏览至https://speech/productpage/。如果一切正常,我们应该会看到类似于图 5-3 所示的截图。

不幸的是,这个截屏没有做到公平——为了感受一下它在实际中是如何工作的,我建议运行本书附带的代码下载中的演示。它位于 productpage 文件夹中,理想情况下应该作为一个安全的 URL 运行。所有代码都不应该是陌生的;虽然合并后的版本会有点粗糙,但它给了我们一个优化代码的绝佳机会,比如 CSS 样式!

好吧,我们继续。我们已经建立了我们的审查系统;在这一点上,我们应该有一些东西,让我们可以用英语记录反馈,并以适当的方式显示在屏幕上。问题是,在现代互联网时代,不是每个人都说英语!这意味着我们的演示只有在英语市场或客户可以将英语作为第二语言的市场中才真正有效。

幸运的是,这很容易解决——我们已经在前一章中使用了 how 的一些原则!考虑到这一点,让我们深入研究,看看我们需要做些什么来让我们的复习系统接受和转录不仅仅是英语…

添加语言支持

在这个我们应该拥抱不同文化的现代,对母语不是英语的顾客表示支持是很重要的。然而,添加对额外语言的支持可能是一把双刃剑——从技术上来说,添加支持可能非常容易,但是应该选择支持哪些语言呢?

答案(部分)将取决于谷歌的支持——如果客户使用 Chrome,它会提供支持。谷歌(根据 BCP47 协议)支持的国家列表可在 https://cloud.google.com/speech-to-text/docs/languages 获得。但这还不是结束,我们还应该问更多的问题,包括:

  • 我们的客户在使用哪些浏览器?这一点很重要,因为这在很大程度上取决于你的客户使用的浏览器:如果是 Chrome(或最新版本的 Edge),那么支持会相当好——谷歌提供了一系列不同的语言作为这种支持的一部分。然而,如果你的客户更喜欢 IE 或 Safari,那么提供语言支持将是一个争论点,因为这两种浏览器都不支持 API!

  • 如果我们决定不提供对特定语言的支持,我们如何避免疏远客户?很明显,一种只有少数用户使用的语言不会因为经济可行性而被增加;然而,如果那个客户恰好是你的主要收入来源呢?是不是“谁喊得最响,谁先被听到”?是的,我知道这是一个极端的例子,但它表明了优先级是关键!

  • 假设我们增加了对更多语言的支持,您是否有足够的资源来支持使用该功能的客户?毕竟,如果他们不厌其烦地用他们自己的语言留下反馈,如果我们只能用英语回复,这多少会破坏这个选项的整个目的。是的,我们可以使用 Google Translate 这样的服务,但是这是一个很差的替代品,无法提供来自团队真实成员的回复!

正如我们所看到的,简单地在技术上增加支持只是难题的一部分;为了解决这个问题(并为我们的客户提供最好的支持),我们必须考虑全局。我们已经谈到了一些我们可能会问的问题,所以现在是我们讨论技术问题的时候了。让我们深入研究并考虑我们需要添加或修改的代码,以使我们的审查系统能够适应更多的语言。

更新演示

对于我们的下一个演示,我们将添加对讲法语的客户的支持,我们可以添加任何数量的不同语言,但法语恰好是我会说的一种语言!(好吧,已经有一段时间我不得不全职说了,不过我跑题了……)

我们需要对我们的演示进行一些更改,概括来说,如下所示:

  • 我们需要找到合适的旗帜图标——在我们的演示中,我们将使用在第三章中已经有的图标。然而,如果你想尝试不同的语言,那么像 https://www.gosquared.com/resources/flag-icons/ 这样的网站将是一个很好的开始。

  • 我们将需要添加标记和样式来托管这些标志——请记住,如果我们要添加的不仅仅是法语,我们可能需要考虑重新定位元素,以腾出额外的空间,或者改变样式以使它们正确匹配。

  • 当我们使用语音识别 API 时,我们需要改变配置选项,这样它就不会硬编码为默认的美国英语,而是可以根据请求接受其他语言。

  • 我们需要添加事件处理程序,以允许客户选择语言并相应地更新 API 配置选项。

这可能看起来很多,但在现实中,改变是非常容易的。为了理解我的意思,让我们开始更新我们的演示。

Adding Language Support

我们需要做的第一个改变是我们的标记:

img/490753_1_En_5_Fig4_HTML.jpg

图 5-4

我们更新的演示,有法语选项

  1. 我们将首先打开一个index.html的副本,然后寻找这个块:

    <div class="button-wrapper">
      <div id="speechButton" class="start"></div>
    </div>
    
    
  2. 紧接在它的下面,为我们的标志插入下面的代码:

    <section class="flags">
      <span class="intro">Choose language:</span>
      <span class="en-us"><img src="img/en-us.png" alt="en-us">EN</span>|
      <span class="fr-fr"><img src="img/fr-fr.png" alt="fr-fr">FR</span>
    </section>
    
    
  3. 继续保存文件-我们可以关闭它,因为不需要它。接下来,打开scripts.js,然后向下滚动到这一行:

    var unsupported = document.getElementById('unsupported');
    
    
  4. 在它的正下方,继续添加这些变量声明——确保在const french...语句之后留出一行空白:

    var speaking = false;
    var chosenLang = 'en-us';
    const english = document.querySelector("span.en-us");
    const french = document.querySelector("span.fr-fr");
    
    
  5. 向下滚动几行。然后在recognition.continuous = true下面,继续添加这一行:

    recognition.lang = chosenLang;
    
    
  6. 接下来,寻找clearbtn事件处理程序——在它下面留一个空行,然后添加这个事件处理程序,负责将英语设置为我们选择的语言:

    english.addEventListener("click", function() {
      recognition.lang = 'en-us';
      english.style.fontWeight = 'bold';
      french.style.fontWeight = 'normal';
    });
    
    
  7. 我们又添加了一个事件处理程序——这个负责设置法语,当被选中时:

    french.addEventListener("click", function() {
      recognition.lang = 'fr-fr';
    english.style.fontWeight = 'normal';
    french.style.fontWeight = 'bold';
    });
    
    
  8. 继续保存该文件,因为不再需要它,所以可以在此时将其关闭。一旦关闭,打开styles.css,在样式表的底部添加以下规则:

    /* CSS Changes */
    span.intro {
    padding-right: 10px;
    vertical-align: baseline;
    }
    
    /* flags */
    section > span.en-us,
    section > span.fr-fr {
    padding: 2px 5px 0 0;
    }
    
    section > span.en-us > img,
    section > span.fr-fr > img {
    vertical-align: middle;
    padding: 3px;
    }
    
    section > span.en-us > img:hover,
    section > span.fr-fr > img:hover {
    cursor: pointer;
    }
    
    
  9. 保存并关闭该文件。至此,我们现在可以测试结果了!为此,浏览至https://speech/reviewslang,点击提问,开始输入信息,如图 5-4 摘录所示。

看看修改我们的演示让我们说法语有多容易?最棒的是 SpeechRecognition API 支持许多不同的语言,所以我们可以很容易地支持更多的语言。

重要的是要注意,我们已经对这个演示中需要的东西进行了硬编码;如果我们要添加更多的语言,优化我们的代码是值得的,这样我们可以更有效地重用现有的样式。也就是说,在这个演示中,为了支持额外的语言,做了一些重要的更改,所以让我们花点时间来更详细地浏览一下代码。

剖析代码

在最后几页的过程中,我们对代码做了一些修改。第一个是添加适当的标记,作为我们选择的标志(在这个例子中,包括美国英语和法语)的脚手架。然后我们切换到scripts.js文件并添加了一些变量——两个用于帮助配置 API ( speakingchosenLang),两个作为对 DOM 中元素的引用:englishfrench

接下来,我们必须改变 API 实例的默认语言——因为我们现在不能使用默认的'us-en'(或美国英语),我们需要告诉它应该使用哪种语言。为此,我们将 c hosenLang的值赋给recognition.lang;默认设置为'en-us'(因此保持现状)。然而,现在可以通过使用接下来的两个事件处理程序对englishfrench进行更新。这里我们将recognition.lang设置为'en-us''fr-fr',这取决于点击了哪个标志;我们还将屏幕上的ENFR文本设置为粗体,并取消选择其他标志的文本。

然后,我们对演示进行了一些简单的样式更改,以考虑到旗帜的存在。这些完全适合放在transcript textarea 元素之下,但是如果我们要添加更多的元素,那么我们可能要考虑对 UI 更广泛的影响,并移动一些其他元素以便更好地适合。

好吧,让我们改变策略。在本章的过程中,我们已经利用语音识别实现了一个有用的反馈机制的开端,它可以适用于任何希望为客户提供评论机会的网站。这是一个很好的方式来获得意见,我们可以用它来帮助改善我们的报价,但它可能会带来一些我们需要考虑的问题。这么说吧,如果我们不小心的话,它们可能会回来咬我们!为了理解我的意思,让我们更详细地看看更广阔的图景。

留下评论:附言

与任何新技术一样,经常会有一些缺点——毕竟,这仍然是相对较新的技术,在标准最终确定之前肯定会有变化!尽管如此,有三点值得我们特别注意:

  • 我们需要考虑的第一件事是客户可能会有什么反应,特别是如果他们有糟糕的体验!作为任何 UX 设计的一部分,我们应该考虑实施一些内部规则。例如,如果顾客在他们的评论中使用亵渎的话会怎么样?如果他们有不太完美的经历,他们可能会觉得有理由表达自己的观点,但我们显然不希望我们的评论中充斥着令人讨厌的词语!

  • 第二个要考虑的问题是垃圾邮件——是的,这可能看起来有点奇怪,但是随着技术的发展,技术上没有什么可以阻止人们向你的反馈机制发送垃圾邮件!这是否会成为现实,只有时间能告诉我们,但是当你为你的网站实现一个声音激活的审查系统时,这是值得考虑的事情。

  • 对谷歌支持某些浏览器功能的依赖将是一个问题——不是因为谷歌很可能很快就会倒闭,而是因为他们可能希望开始将目前免费提供的支持货币化。这确实意味着,在支持方面,我们在某种程度上受谷歌的支配;可能会有一个时候,一种语言可能不被支持,所以我们将不得不迅速作出反应,以尽量减少任何问题,如果支持被删除。

简而言之,在这些问题上我们可能无能为力,但我们可以建立一些保护。例如,我们可能会要求用户必须登录才能留下评论或内置一些东西来监控特定单词的实例,我们可以在转录我们的内容时尝试过滤掉这些单词。

还有,那个支持?嗯,我们硬编码了我们的条目来证明我们的演示作品,但是这不是很有效。相反,我们可以使我们的代码更加动态——它可以搜索配置文件中存在的任何条目。根据找到的内容,它会遍历这些内容并自动构建内容。这意味着,只要存在诸如标志之类的媒体,我们需要做的就是打开或关闭支持;我们的代码将自动计算出支持哪些语言,并向我们的网页添加适当的条目。

好了,这一章我们就要结束了,但是还有一件事需要考虑——进一步开发我们的解决方案怎么样?当然,这完全取决于你的要求和你的想象力的创造性;首先,让我们来看看一些想法,看看如何添加到您的解决方案中,以帮助您的客户提升体验。

更进一步

好了,我们已经建立了一个基本的演示,它允许我们用英语或法语交谈,并让它以书面形式转录和发布我们的评论。问题是“下一步去哪里?”嗯,我们可以做一些事情。让我们来看看:

  • One element that is clearly missing from our demo is a rating – this is a good opportunity to allow customers to provide an objective figure, in addition to qualitative feedback. We could simply implement a suitable mechanism, such as the RateIt plugin from https://github.com/gjunge/rateit.js, but what about doing this verbally? How we achieve this will depend on the structure used, but it should be possible to provide the rating verbally and for it to be translated into the appropriate star rating. As an example, adding a rating could look like the example screenshot shown in Figure 5-5.

    img/490753_1_En_5_Fig5_HTML.jpg

    图 5-5

    我们的模拟评级明星

  • 我们的演示允许我们在页面上发布评论,但这只是故事的一部分——我们绝对应该考虑使用这些反馈,并在适当的时候对客户做出回应。然而,后者意味着我们至少需要一种联系方式,比如电子邮件地址。我们如何实现这一目标?一种方法可能是鼓励客户注册一个帐户,这样我们就可以获得该电子邮件地址——这当然会对 GDPR 等隐私立法产生影响,这是我们需要考虑的。

  • 如果支持客户反馈管理的资源是一个问题,那么我们可以考虑使用一个 API,如 Google Translate,至少将我们转录的内容转换成英语或我们的母语(如果不是英语)。这是有代价的——我们只能希望了解谷歌翻译提供了什么,因为机器翻译的内容并不完美!

这只是让你开始的几个想法——如果我们运营的网站类型适合这样的额外服务,我们甚至可以考虑添加额外服务,如头像!不言而喻的是,如果我们添加额外的选项,那么这些需要经过彻底的测试,以确保它们提供价值,而不是作为一个噱头出现在我们的客户面前。

摘要

客户反馈对任何企业都是至关重要的,无论业务规模有多大——最终,我们企业的成功将取决于我们收到的意见,以及我们如何回应或采取什么行动来改进自己。显然,让反馈过程尽可能简单很重要——还有什么比留下口头评论更好的方式呢?在本章中,我们已经介绍了实现这一目标的基本步骤;让我们花点时间更详细地回顾一下我们所学的内容。

我们首先介绍了这一章的主题,然后快速设置场景并确定我们将如何确定范围和构建我们的演示。然后,我们继续构建表单,在探索代码如何详细工作之前,同时注意与前面章节的相似之处。

然后,在深入研究语言支持主题之前,我们看了看如何将这一点融入到更真实的示例中——我们讨论了修改演示所需的步骤,然后探讨了关于提供口头反馈的缺点以及我们可以在何处开发项目来为客户引入新功能的一些最终要点。

好吧,我们不会就此罢休;是时候进入我们的下一章了!请举手,你们中有多少人拥有智能助手,比如谷歌助手、Siri 或亚马逊 Alexa?微软的联合创始人之一比尔·盖茨曾经说过,语音和演讲将成为网络界面的标准部分——随着 Siri、Alexa 和谷歌助手的出现,他没有错!我们已经有了很多技术来为网站构建一个简单的 Alexa 版本。对什么感兴趣?不要走开,我将在下一章揭示更多。

六、项目:构建 Alexa

“阿列克谢,几点了…?”

对你们中的一些人来说,我敢打赌这是一个在你们家太常见的短语——我怀疑这不是如果的问题,而是有多少个!

在过去的几年里,亚马逊 Alexa 或谷歌助手等智能助手(或 SAs)的增长呈爆炸式增长;我们不得不通过搜索网站、报纸或书籍来获取信息碎片的日子已经一去不复返了。事实上,微软的联合创始人之一比尔·盖茨曾经说过,他相信语音和语音输出将成为[网络]界面的标准部分——随着 Siri、Alexa 和谷歌助手的出现,他没有错!

这让我想到——我们已经了解了智能助理的两项核心技术,即语音合成和识别。我们能否建造一些东西来模仿 Alexa 等助手的工作方式?它可能没有硬件等效物那么强大,但它可以使用这两种 API 来创建一些有用的东西。只要我们使它模块化,那么我们就可以添加功能,以帮助它在未来发展成更有价值的东西。

记住这一点,在本章的课程中,我们将利用语音识别和合成 API 来创建一个简单的 Alexa 风格的语音助手;我们将学习如何使它模块化,这样就很容易添加更多的技能来帮助扩展它的功能。

设置场景

我们的下一个项目将是一个更简单的项目——这是一个放松的机会,因为我知道本书后面的内容会很密集!让我给你介绍一下 Rachel——她会告诉你当地的时间,纽约的时间(稍后会有更多的介绍),天气预报等等。

我们将从一些简单的任务开始,说明在中添加功能是多么容易。让我们首先更详细地看一下我们将如何设计我们的演示。

构建我们的演示

我们已经了解了这个项目核心的两个 APIs 现在,他们应该开始看起来有点熟悉了。然而,对于这个项目,我们将添加一个小的转折。

我们将利用一个库来为我们做一些工作,而不是手工硬编码语音识别和合成 API。这是我们将利用的少数几个选项之一,所以让我们来完整地看一下这个列表:

  • ann yang——这个库是我们在本书中使用的语音识别 API 的包装器;可从 https://www.talater.com/annyang/ 获得。

  • speech kitt–这是一个与 annyang 协同工作的 GUI,可以从 https://github.com/TalAter/SpeechKITT 下载。GUI 库已经有几年的历史了,但是它提供了对 annyang 的原生支持,并且仍然可以很好地满足我们的需求。

    如果你想知道 GUI 库中对 KITT 的引用,这个库是以 80 年代的美剧《霹雳游侠》命名的。你甚至可以在 SpeechKITT 的 GitHub 页面上看到男主角大卫·霍索夫的照片!

  • luxon–用于日期和时间,以及时区支持;这可从 https://moment.github.io/luxon/index.html 得到。

  • open weather map——我们提出的请求之一与获取天气有关;为此,我们将使用 https://openweathermap.org/ 提供的 API。

  • pix abay——如果你碰巧已经有了一个智能助手,这可能是你在这样的演示中不会看到的;毕竟智能助手是不能显示图片的,除非你恰好配置了智能助手使用你的 PC 作为显示机制!我们把它放在这里是为了探索如何使用像 Pixabay 这样的服务来显示图像;我们将在本章的后面更多地讨论这是否是正确的方法。

  • jQuery——这是一种必要的邪恶。我们利用它来解决 SpeechKITT GUI 的局限性。我们将在这一章的后面探讨更多的原因。

    另外,我会推荐让 JSON 编辑器在线网页( https://jsoneditoronline.org/ )在你的浏览器中打开;这是一个很棒的 JSON 编辑器,对于浏览我们使用的一些服务返回的原始数据很有用。

我们的演示将展示一些简单的请求;我们可以以此为基础添加更多使用不同 API 的特性。这是我们将在本章后面探讨的内容,但是现在,让我们继续编写我们的演示程序。

构建我们的演示

就编码我们的演示而言,与以前的项目相比,这看起来就像是在公园里散步!就结构而言,我们的演示将非常简单——除了演示所需的一些标记和样式之外,将只添加一个元素。这将动态完成,并将用于触发我们发出的所有请求。

在我们看一下我们编写的代码将如何使我们的演示变得生动之前,让我们继续努力并设置好标记。

创建标记

我们的第一个任务是为这个小演示设置标记——这个非常简单。我们甚至不需要为麦克风触发器提供占位符,因为 SpeechKITT GUI 会为我们动态创建占位符。让我们从我们的标记开始,更详细地研究一下代码。

Setting Up The Markup

要设置标记,请执行以下步骤:

  1. 此时,您可以关闭任何打开的文件。给自己留一个打开的空白文件,准备开始下一个练习,这个练习很快就会开始。

  2. 我们将开始为我们的项目创建一个新文件夹——在我们的项目区域的根目录下保存为rachel

  3. 接下来,继续为我们的基本标记创建一个新文件;添加以下代码:

    <!DOCTYPE html>
    <html>
    <head>
      <title>Introducing HTML5 Speech API: Building an Alexa Clone</title>
      <link href="https://fonts.googleapis.com/css?family=Open+Sans
    &display=swap" rel="stylesheet">
    </head>
    <body>
      <div id="page-wrapper">
        <h2>Introducing HTML5 Speech API: Building an Alexa-style Smart Assistant</h2>
        <section>
            Rachel's voice: <select name="voice" id="voice"></select>
        </section>
      </div>
      <script src="js/annyang.min.js"></script>
      <script src="js/speechkitt.min.js"></script>
      <script src="js/jquery.min.js"></script>
      <script src="js/luxon.min.js"></script>
      <script src="js/scripts.js"></script>
    </body>
    </html>
    
    
  4. 将文件另存为index.html–我们现在可以关闭它。下一个练习将负责添加脚本功能。

  5. 我们还有最后一步要做——我们需要从本书附带的代码下载中复制一些 JavaScript 文件和 CSS 样式。继续提取以下文件的副本,并将它们放入我们之前创建的rachel文件夹下的子文件夹中:

    • styles.css–放入新的css子文件夹

    • 下面放入一个新的js子文件夹:annyang.jsjquery.min.jsluxon.min.js,speechkitt.min.js

我们现在已经有了标记——代码没有什么复杂或不寻常的地方。我们简单地建立了我们的基本框架,并包含了一些 JavaScript 和 CSS 文件;当我们开始开发使我们的演示变得生动的脚本时,奇迹就会出现。

让我们的演示栩栩如生

在我们开始添加 JavaScript 代码之前,我们需要做一件小事——在 https://home.openweathermap.org/users/sign_up 注册一个免费账户。

这将需要几个小时才能被 OpenWeather 的团队激活;一旦你从 OpenWeather 团队那里收到一封带有密钥的欢迎邮件,你就可以认为它已经设置好了。在陷入代码开发之前,您可能需要考虑这一点!假设您已经注册,并收到电子邮件确认您的帐户现在是活跃的,让我们开始我们的演示。

Demo: Adding Functionality

要设置我们的演示,请遵循以下步骤:

  1. 下一个任务是在一个不同的地方表达时间——我选择了纽约,那里恰好是阿普瑞斯出版社的所在地。继续在上一步之后插入下面的代码,中间留一个空行:

    // Rachel, what time is it in New York?
    var timeinnewyork = function() {
      var NYTime = luxon.DateTime.local().setZone('America/New_York').toLocaleString(luxon.DateTime.TIME_WITH_LONG_OFFSET);
      speak("The time in New York is " + NYTime);
    }
    
    
  2. 我们已经讨论了两个不同地点的时间,但是日期呢?没问题,代码如下:

    // Rachel, what is today's date?
    var DateNow = function() {
      var localdate = luxon.DateTime.local().toLocaleString(luxon.DateTime.DATE_SIMPLE);
      speak("The date is " + localdate);
    }
    
    
  3. 我喜欢一个好的笑话,所以只有看我们是否能在这个演示中包括一对夫妇才是明智的;如果你有一个真正的 Alexa,那么我敢肯定你会看到电子邮件建议你问它一个笑话!下面是第一个:

    // Rachel tell a funny joke:
    var telljoke = function() {
      speak("Why do we tell actors to break a leg? Because every play has a cast");
    }
    
    
  4. 下一个笑话似乎更适合我们设计师和开发人员,至少从字体类型的使用来看;继续在上一步的代码之后添加这段代码,中间留一个空行:

    var tellsecondjoke = function() {
      speak('Helvetica and Times New Roman walk into a bar. The bar tender shouts "Get Out of here - we don\'t serve your type!"');
    }
    
    
  5. 另一个明显要问 Rachel 的问题是天气——出于演示的目的,我将它硬编码为我最喜欢的度假目的地之一,或者哥本哈根市。为此,在步骤 9 的代码后添加一个空行,然后放入以下代码:

    按照本练习的开始,您需要用 OpenWeather 中的 API 密钥替换

  6. 首先,我们需要为我们的脚本创建一个新文件——为此,在我们在前一个练习中创建的rachel文件夹下的js子文件夹中创建scripts.js

  7. 我们现在可以开始添加代码了。有很多内容需要讨论,我们将一个块一个块地讨论。第一个块负责加载 Rachel 的声音——在scripts.js文件的顶部添加以下代码:

    const voiceSelect = document.getElementById('voice');
    
    function loadVoices() {
      var voices = window.speechSynthesis.getVoices();
    
      voices.forEach(function(voice, i) {
          var option = document.createElement('option');
          option.value = voice.name;
          option.innerHTML = voice.name;
          voiceSelect.appendChild(option);
      });
    }
    
    loadVoices();
    
    // Chrome loads voices asynchronously.
    window.speechSynthesis.onvoiceschanged = function(e) {
      loadVoices();
    };
    
    
  8. 加载了 Rachel 的声音后,我们现在可以让她说话,并在出现任何错误时进行标记。在上一步之后留一行空白,然后加入这个函数来管理基本的错误处理:

    window.speechSynthesis.onerror = function(event) {
      console.log('Speech recognition error detected: ' + event.error);
      console.log('Additional information: ' + event.message);
    };
    
    
  9. 下一个函数让 Rachel 说话——继续在前一个函数之后添加以下代码,中间留一个空行:

    function speak(text) {
      var msg = new SpeechSynthesisUtterance();
      msg.text = text;
    
      if (voiceSelect.value) {
        msg.voice = speechSynthesis.getVoices().filter(function(voice) {
          return voice.name == voiceSelect.value;
        })[0];
      }
      speechSynthesis.speak(msg);
    }
    
    
  10. We come to the interesting part – now that Rachel can talk, it’s time she said something! The first example will be to articulate the current time:

    // Rachel, what time is it now?
    var timeNow = function() {
      var localtime = luxon.DateTime.local().toLocaleString(luxon.DateTime.TIME_SIMPLE);
      speak("The time is " + localtime);
    }
    
    

    无论你住在世界的哪个地方,提到的时间都是当地时间。

img/490753_1_En_6_Fig1_HTML.jpg

图 6-1

我们的最终结果——见到“瑞秋”的所有荣耀…

  1. 下一个函数负责从维基百科获取一些示例数据——碰巧的是,我收到了一封来自亚马逊的电子邮件,为我的 Alexa 建议了这个主题!在前一个函数下留一个空行,然后添加这段代码——注意 url 值应该在一行上,而不是跨两行,如下所示:

    // Rachel, Wikipedia "artificial intelligence"
    var wikipedia = function() {
      $.ajax({
        method:'GET',
        crossDomain: true,
        url: 'https://en.wikipedia.org/api/rest_v1/page/summary
        /Artificial_intelligence',
        dataType: "json",
        async: true,
        success: function(response){
          speak("Here is the extract from Wikipedia on artificial intelligence: " + response.extract);
        }
      });
    }
    
    
  2. 对于最后一个选项,我们将在本章的后面回到这个选项——现在添加它,很快一切就会变得清晰:

    // Rachel, show me a picture of...
    var flickr = function() { console.log("This to follow"); }
    
    
  3. 这个练习快结束了。最后一部分负责初始化 annyang 和 SpeechKITT。像以前一样留一个空行,然后输入下面的代码:

    if (annyang) {
      var commands = {
        'Rachel what time is it': timeNow,
        'Rachel tell a joke': telljoke,
        'Rachel tell another joke': tellsecondjoke,
        'Rachel what time is it in New York': timeinnewyork,
        'Rachel what is the weather like in Copenhagen': weather,
        'Rachel wikipedia artificial intelligence': wikipedia,
        'Rachel show me a picture of some orchids': flickr
      }
    
      // Add our commands to annyang, then tell KITT to use annyang:
      annyang.addCommands(commands);
      SpeechKITT.annyang();
    
      // Define a stylesheet for KITT to use
      SpeechKITT.setStylesheet('css/styles.css');
    
      // Render KITT's interface
      SpeechKITT.vroom();
    }
    
    $(document).ready(function() {
      $("#skitt-ui").insertAfter($("h2"));
    });
    
    
  4. 继续保存文件,我们现在可以预览我们的工作结果了!在浏览器中浏览到https://speech/rachel/;如果一切正常,我们应该会看到类似于图 6-1 中的截图。

// Rachel, what is the weather in Copenhagen?
var weather = function() {
  var yourappid = "<INSERT YOUR APP KEY HERE>";

  $.ajax({
    method:'GET',
    crossDomain: true,
    url: 'https://api.openweathermap.org/data/2.5/weather
?q=copenhagen,dk&appid=' + yourappid,
    dataType: "json",
    async: true,
    success: function(response){
      speak("The temperature in Copenhagen is currently: " + parseInt(response.main.temp - 273.15) + " degrees");
    }
  });
}

在这一阶段,我们现在有了一个功能演示——Rachel 活了过来,能够响应一些简单的请求。尽管我们所使用的代码并不特别复杂,并且现在应该相对熟悉了,但是我们的演示强调了一些我们应该更仔细考虑的要点。在此之前,让我们深入了解一下代码的更多细节。

破解密码

与本书前面的一些演示(以及那些即将到来的演示)相比,这个演示看起来就像在公园里散步一样!我们已经能够重用早期项目中的一些代码,即语音合成 API 其余的来自我们在本章前面介绍的 annyang 库。

这段代码的主要焦点在 scripts.js 文件中——在这里,我们首先缓存对标记中使用的voice下拉菜单的引用,然后调用loadVoices()函数将来自 Google 的声音加载到这个下拉元素中。和以前一样,我们还加入了onvoiceschanged功能——一些早期版本的 Chrome 会异步加载语音,这只能通过这种方法来实现。(在 Chrome 的最新版本中,这个问题会变得不那么严重,所以为了兼容,这个功能已经包含在内了。)

接下来,我们使用onerror事件处理程序实现了一些基本的错误检查——这使用error代码和message属性将任何错误的细节呈现到控制台区域。然后我们定义了speak()函数,它与前面的练习相同;这里我们设置了一个新的SpeechSynthesisUtterance()实例,在调用.speak()来表达文本之前,将传递到函数中的文本分配给它,并设置要使用的声音。

至此,我们有了一组函数。让我们跳到 annyang 的初始化函数,从这行代码开始:if (annyang) {。在这里,我们设置了 annyang 的实例,并告诉它使用 SpeechKITT GUI 和我们指定的styles.css样式表。

值得注意的是,SpeechKITT 使用.vroom()方法启动;这是对这个 GUI 的灵感的引用,可以很容易地用做同样事情的render()代替。

我们现在有了一个基本的配置——如果我们回到大约第 40 行(var timeNow = function() {),我们可以看到几个简单函数中的第一个,每次 annyang 识别到请求时都会调用这些函数,比如这个(图 6-2 )。

img/490753_1_En_6_Fig2_HTML.jpg

图 6-2

安阳要调用的第一个函数

如果我说“瑞秋,现在几点了?”,annyang 将调用这个timeNow()函数并显示响应,这将是您所在地方的当地时间。函数调用是在脚本末尾的var commands = {...}对象中定义的——当 annyang 确定某个函数调用与用户的响应相匹配时,就会执行这些函数调用。

好吧,我们继续。我会说,这是解释的结束,但如果只是!事实上,该项目揭示了一些需要进一步探索的问题和领域;让我们从第一个开始,这是一个造型上的挑战。如果您运行过 annyang 的示例演示(显示在 https://github.com/TalAter/SpeechKITT 作为一个单独的演示),您会注意到触发器位于屏幕底部,这并不总是符合人们的需要!这是由于配置问题(或限制——取决于您的观点)。让我们开始吧,我会解释一切。

解决造型问题

在我们的项目中,我确信你已经注意到了 jQuery 在脚本文件底部的少量使用,并且之前我提到这是一种“必要的邪恶”——这是有充分理由的,所以让我解释一下我的意思。

如果我们使用 SpeechKITT 网站提供的原始 CSS 样式运行我们的演示,您会发现麦克风触发器位于屏幕的左下角。

单独使用 CSS 来移动它是没有用的——这个特殊的元素是动态生成的,所以为了正确地移动它,我们需要使用 JavaScript 或 jQuery!为了方便起见,我在这个实例中使用了 jQuery 来做这项工作;这使它非常整洁,尽管这是以导入一个大型库为代价的。不过,这是否对你有用是另一回事。这将取决于您是否已经在使用 jQuery。如果不是,那么纯 JavaScript 将是更好的选择,尽管这样做的代码不是很简洁!我们可以在图 6-3 中看到问题的根源,其中麦克风元件在我们的控制台中突出显示。

img/490753_1_En_6_Fig3_HTML.jpg

图 6-3

原始 SpeechKITT 演示中的麦克风触发器

然而,做一个简单的元素移动并不是它的结束——我们还需要做一些其他的改变,这样我们才能按照我们想要的那样设计我们的演示。我们做的其他改动都是 CSS 相关的。没有特定的顺序,他们是

  • 我们删除了原始演示中的两个媒体查询,它们碍事,影响了用于设计演示的特定格式。我确信媒体的提问是有用的,但是原始演示中的提问不适合这个特殊的例子,所以无论如何都需要修改!

  • 然后我们删除了这条规则——原因有点复杂:

#skitt-ui {  display: block !important; }

我不喜欢使用!重要的指令,因为它经常被错误地使用和滥用!如果可以的话,我希望至少去掉其中一个——反对#skitt-ui的那个更有可能。

我们还需要修改一个模块——在#skitt-ui规则中,删除了以下条目(突出显示):

#skitt-ui {
  height: 50px;
  display: inline-block;
  background-color: #2980B9;
  z-index: 200;
  border-radius: 25px;
  outline: none;
     position: fixed;
     bottom: 20px;
     left: 20px;
  border: none;
  box-shadow: rgba(0,0,0,0.2) 0px 4px 8px;
  cursor: default;
  font-family: Lato, Helvetica, Arial, sans-serif;
  font-size: 16px
}

做出这些改变意味着我们可以有效地将麦克风触发器重新定位在屏幕上的任何位置,而不用担心它的位置!

好吧,让我们改变策略。到目前为止,我们已经探索了如何添加一些口头示例,在这些示例中,我们可以向用户口头表达回应,例如当前时间或天气。

不过,我们确实需要做出选择:视觉内容呢?是的,这不是你对标准 Alexa 的期望(尽管不是不可能),但当我们在浏览器中工作时,我们可以考虑是否要在屏幕上显示内容。这是我们将在下一个项目中更多利用的东西,但是现在,展示一些简单的东西怎么样,比如说一个像 Flickr 或者 Pixabay.com 这样的网站?

添加新功能

既然 Rachel 已经设置好并可以运行了,我们可以添加各种不同的特性。唯一的限制因素是我们的想象力和我们是否能让它为我们服务。

这确实提出了一个好问题:我们应该添加什么样的功能?在大多数情况下,人们可能会认为它们应该只是口头上的——这确实取决于我们想要模仿一个真正的智能助理的程度(不,我也不是指人类中的一员!)另一方面,也可以说这并不适用,因为你可以创造各种各样的技能,而不全是基于口头的。选择,选择…

除此之外,在我们的下一个练习中,我们将使用一点诗意的许可,并假设我们可以利用我们的电脑屏幕以及接受口头输入。我们将展示一张图片库中的随机图片。这将是兰花(这碰巧是我最喜欢的花,但你可以使用任何类别,如汽车,相机,人,等等。).雷切尔将从图片库网站上调出一系列图像,并在屏幕上随机显示一张。让我们更详细地看看我们需要做的更改。

Adding An Image

要添加图片选项,请执行以下步骤:

  1. 我们将从编辑我们的script.js文件开始——我们已经有了一个占位符函数,所以继续寻找这行代码:

img/490753_1_En_6_Fig4_HTML.jpg

图 6-4

显示来自 Pixabay 的图片作为附加功能

  1. 删除注释,然后加入以下代码:

    // Rachel, show me a picture of some orchids
    
    var pixabay = function() {
      var API_KEY = '<INSERT APP ID HERE>’;
      var URL = "https://pixabay.com/api/?key=" + API_KEY + "&q=" + encodeURIComponent('orchids');
    
      $.getJSON(URL, function(data){
        function getRandomInt(max) {
          return Math.floor(Math.random() * Math.floor(max));
        }
    
        if (parseInt(data.totalHits) > 0) {
          var randomImg = getRandomInt(20);
          console.log(randomImg);
          $("<div class="imgPreview"><img src=" + data.hits[randomImg].largeImageURL +"></div>").insertAfter($("#skitt-ui"));
        } else {
          console.log('No hits');
        }
      });
    };
    
    
  2. 保存文件–我们不需要它保持打开状态,因此您可以关闭它。

  3. 接下来,切换到styles.css文件,一直滚动到底部。

  4. 继续,放入以下代码,然后保存文件:

    /* Additions to allow for image */
    .imgPreview { margin-left: auto; margin-right: auto; display: block; width: 300px; margin-top: 20px; }
    
    .imgPreview img { width: 300px; }
    
    
  5. 我们现在可以预览我们更改的结果,为此,浏览到https://speech/rachel,然后单击白色麦克风。对着麦克风清晰地说出“瑞秋,给我看看一些兰花的照片”。如果一切正常,我们应该会看到一个随机的图像出现,类似于图 6-4 中的截图。

console.log("This to follow");

一个很好的,容易做的改变。当然,并不是所有的改变都这么简单,但是只要有一点创造力,我相信我们可以找到更多可以用类似方式添加的东西!

也就是说,它确实突出了关于这段代码的模块化本质以及添加新特性是多么容易的几个有用点。记住这一点,让我们更详细地回顾一下这段代码,看看我们是如何对我们的演示进行这一更改的。

详细研究代码

为了让这个演示运行,我需要选择一个具有可用 API 的图片库——我确实考虑过 Flickr,但是他们当前的 API 并不容易添加到我们的演示中!我选择了 Pixabay,因为他们的更简单;它们可能没有 Flickr 那么多图片,也没有 Flickr 那么出名,但这对于本演示来说并不重要。

当我们在本章开始设置 Rachel 时所做的第一个改变;这是为了添加到命令中以执行返回我们的图像的函数:

var commands = {
  ...
  'Rachel show me a picture of some orchids': pixabay
};

为了允许代码在那时继续工作,我们在中放置了一个占位符函数,它向控制台呈现一条消息。然而,在本练习中,我们用一个 URL 替换了控制台日志消息,该 URL 将构成我们对 Pixabay 的请求的基础——正在编码的类别,以允许在 URL 中使用引号。

然后,我们使用 AJAX 调用来获取图像列表——它可以返回任意数量的 URL,但是只要它至少返回一个,我们就选择 1 到 20 之间的一个随机数,并使用它来显示返回的 JSON 对象的 largeImageURL 属性。然后用它在屏幕上创建一个空的 div 元素,在里面我们渲染我们选择的图像。

好吧,我们继续。到目前为止,我们的演示一直在美国英语操作。这完全没问题,但不是每个人都说英语;包含对其他语言的支持怎么样?值得庆幸的是,这相对容易做到——这意味着要做一些改变,所以让我们深入了解一下。

添加对不同语言的支持

在使用语音识别或合成 API 时,我们已经在一些早期项目中看到,添加语言支持相对简单。是的,可能会有一些变化,但没有太繁重。这同样适用于我们在本章中使用的 annyang 库。

对于我们的下一个演示,我将让 Rachel 开始说法语(主要是因为这是我会说的语言,所以我可以检查它是否有效)——如果您喜欢使用不同的语言,请随时相应地更新文本。

Adding Support For Languages

我们需要做一些更改,所以让我们开始吧:

img/490753_1_En_6_Fig6_HTML.jpg

图 6-6

我们最新的法语版《瑞秋》

  1. 好的,继续保存然后关闭文件;我们现在可以预览结果。启动浏览器,然后浏览至https://speech/rachel-language。如果一切正常,我们应该会看到如图 6-6 所示的截图,其中麦克风符号已经被点击,准备发言。

  2. 接下来是weather()函数——为此,按照指示替换speak...行:

    speak("La température à Copenhague est maintenant : " + parseInt(response.main.temp - 273.15) + " degrees");
    
    
  3. 我们需要类似于wikipedia()函数的东西——继续修改它,如下所示:

    success: function(response){
      speak("Voici l'extrait de Wikipedia sur l'intelligence artificielle: " + response.extract);
    }
    
    
  4. 最后一个变化是修改 var commands ={…}块中给出的名称——为此,我们将使用 Hélène,因为这更像法语。用 Hélène 替换单词 Rachel 的每个实例,这样就有了:

      var commands = {
        'Hélène quelle heure est-il': timeNow,
    'Hélène raconte une blague': telljoke,
    'Hélène raconte une autre blague': tellsecondjoke,
    'Hélène quelle heure est-il à New York': timeinnewyork,
    'Hélène quel temps fait-il à Copenhague': weather,
    'Hélène wikipedia intelligence artificielle': wikipedia,
    'Hélène montre-moi une photo d\'orchidées': flickr
      }
    
    
  5. 我们快完成了。我们需要检查或更改的最后两件事是语言和确保我们已经本地化了 annyang 库。滚动到scripts.js库的底部,寻找这一行:

    // Add our commands to annyang
    annyang.addCommands(commands);
    
    
  6. 继续添加这个.setLanguage命令,就在那一行的下面:

    annyang.setLanguage('fr-FR');
    
    
  7. 最后一个变化是本地化我们的 speechKITT 库——为此,关闭 scripts.js(现在我们已经完成了),然后打开speechKITT

    .min.js.

  8. Find this line: u="What can I help you with?" Replace it as indicated:

    u="Qu\'est-ce que je peux vous aider?"
    
    

    You can see a screenshot of how it should look in Figure 6-5.

    img/490753_1_En_6_Fig5_HTML.jpg

    图 6-5

    更新 speechKITT.min.js 文件...

    我建议进行搜索和替换——这会容易得多!

  9. 首先,复制现在已经完成的rachel文件夹,并在我们的项目区域的根目录下保存为rachel-language

  10. 我们需要做的第一个改变是替换speak(text)函数——为此,用下面的代码替换现有的版本:

    function speak(text) {
      var msg = new SpeechSynthesisUtterance();
      msg.text = text;
      msg.lang = 'fr-fr';
    
      speechSynthesis.speak(msg);
    }
    
    
  11. 接下来,向下滚动一点,直到看到 timeNow 函数——将speak...行替换为:

    speak("Le temps est maintenant " + localtime);
    
    
  12. 我们需要为timeinnewyork函数做一些类似的事情——继续用下面的代码替换speak...行:

    speak("TLe temps à New York est maintenant " + NYTime);
    
    
  13. The dateNow function also needs to be updated – for this, replace the speak... line with this line of code:

    speak("Le date aujourd'hui est " + localdate);
    
    

    我们现在将跳过这两个笑话函数,我将在本练习结束时解释更多内容。

现在,我们有了一个演示。让我们尝试运行 Pixabay 命令,看看 Rachel 如何响应。按理说,我们应该得到一些兰花的随机图像,当然…?这个假设没有错。这是完全有效的,只是这一次,我们得到的是绝对……零的平方根。怎么回事?

破解密码

我们的演示看起来不工作有一个很好的原因——这看起来有点疯狂,但实际上我们的代码没有任何问题!是的,我知道这看起来有点奇怪,但是请相信我:代码在语法上是有效的。在我揭示根本原因之前,让我们快速掩盖我们为本地化我们的演示所做的更改。

我们的演示有四个不同的地方需要改变。我们的第一个变化是替换了speak(text) {函数,这样它将返回法语语音,而不是原来的美国英语。然后,我们将每个speak()函数调用更新为法语版本,然后将每个命令修改为类似的法语版本。我们最后的更改组是更新 annyang 和 speech kitt——我们应用了setLanguage命令来告诉 annyang 响应法语命令,并更新 speechKITT.min.js 以法语显示本地化的提示文本。

现在,当代码完全有效时,缺少声音,为什么事情看起来不工作?嗯,这是语音识别 API 的一个怪癖:它发现某些单词很难理解和正确表达,所以会保持沉默。这种情况下的罪魁祸首是法语名称 Hélène 的使用——解决方法是删除它,并用不同的名称替换它。在这种情况下,我会建议像“亚历克斯”这样的名字;在你找到有效的方法之前,这很大程度上是一个反复试验的过程。代码的其余部分工作正常,因此只需删除“Hélène”就可以了。

至于这是否是一个 bug,这是有争议的——更多的是因为 API 仍然是一项正在进行的工作,所以在它完全成熟并能够表达这些错误的词语之前,仍然需要一些技术开发。这也解释了为什么当你更新完这个演示时,你可能会使用两三个名字——原始演示中的“Rachel ”,这个演示中的“Hélène ”,以及你选择用来替换它的任何名字!

好吧,我们继续。我们已经探索了如何使用 annyang 来简化语音识别 API 的实现(并作为手动硬编码的替代方案)。接下来去哪里?我们可以做一些事情来帮助进一步改进和开发我们的代码,所以让我们花一点时间来探索如何更详细地更新它。

提高性能

希望现在,如果你已经更新了演示,我们有一个 Rachel 的工作版本,本地化为法语使用(或者你自己的语言,如果你选择使用其他语言)。这是一个简单的演示,展示了使用不同于英语的语言是多么容易——然而,我们的演示揭示了一些我们应该考虑纠正的事情!让我们更详细地看看:

  • 我们的演示使用了五个不同的脚本文件,包括我们创建的核心文件——这有点过分了!如果可以的话,我们绝对应该考虑减少对库的依赖:一个快速的方法是将 scripts.js 末尾的 jQuery 代码改为普通的 JavaScript。(我用 jQuery 只是为了方便!)

  • 如果您仔细看看我们演示的法语版本的代码,您会发现我没有更新这两个笑话条目。这是故意的;我选择的笑话不太可能翻译成法语,所以我们应该考虑用法语笑话或其他完全不同的东西来代替它们。这是很重要的一点——很明显,对于一个默认语言是美国英语的工具来说,不是所有的东西都能以同样的方式翻译成不同的语言!

  • 我绝对会考虑合理化用于调用 OpenWeather 和 Wikipedia 的 JSON 代码;核心代码在功能上是相同的,但是返回的响应当然是不同的。这是一个很好的例子,我们可以模块化这个特殊的选项,以便在多个命令之间共享,如果我们决定添加更多使用它的命令。

  • 我们应该使用 annyang 吗?我知道这听起来可能很疯狂(考虑到这一章是关于使用它的),但是它的使用是有代价的。我们当然可以合并我们的小文件,但是我们应该考虑这样做是否值得,或者我们是否应该手动编写代码并放弃使用 annyang。

  • 我们的代码中有一点小差错。你发现哪里了吗?如果仔细观察,我们已经指定了一个函数来调用纽约的时间。问题是它是基于 GMT+5 的——这对英国(我所在的地方)来说没问题,但对法国来说就不行了!这是我们在本地化应用时需要考虑的因素;我们不仅需要改变语言,还需要确保我们的功能也有意义。

  • 在我们的演示中,我们还使用了 Pixabay 图片库——这在技术上没有任何问题,但这是我们应该使用的东西吗,因为智能助理将做的大多数事情都可能是口头的。当然,我们可以说他们能做的一些事情依赖于使用个人电脑或笔记本电脑。我想这完全取决于你想要多接近地模仿一个真实的设备!

这只是我们需要考虑改变的一些事情,我相信你会发现更多!这确实表明,在像我们这样的演示中,我们不能简单地依赖于本地化代码时更新文本。我们还需要考虑因为我们的国家发生了变化而导致价值观发生变化的方面(比如时区)。这也意味着,如果您的目标国家倾向于以不同的方式做事(例如,使用更多的移动设备),那么这也需要纳入我们的演示中。

好吧,我们继续。假设我们做了这些改变,下一步是哪里?这种功能完全可以扩展。让我们来看看一些想法,以帮助你开始。

更进一步

“啊哈,接下来是哪里?”我想知道。就像他们说的,世界是我们的。我不知道这句话是从哪里来的,但正如它所暗示的,我们可以自由地添加各种不同的功能,只要我们能编写出技术上可行的东西。

为了帮助解决这个问题,我查阅了过去 6 个月里收到的几封电子邮件,寻找我们如何能够扩展我们所能提供的内容的想法。这里列出了一些想法,让你的创意源源不断:

img/490753_1_En_6_Fig7_HTML.jpg

图 6-7

来自亚马逊的(部分)电子邮件

  • 播放当地电台——这并不容易;如果您可以获得您最喜欢的广播电台在线播放器的 URL,您可以远程发出请求,并使用一点 JavaScript 来自动点击您可能遇到的任何播放按钮。

  • 找到离你最近的超市/当地商店——这可能需要依靠谷歌的 API,但是如果你想避免使用那个庞然大物,你可以使用浏览器中已经可用的地理定位 API,为你硬编码值。一旦进入,使用哈弗辛公式(我们将在下一章看到它的使用)来计算距离就很简单了。它可能不那么漂亮,但它至少允许您编写一些代码来证明它是有效的!

  • 找到一个包含 X 的食谱,其中 X 是你最喜欢的食物——为此,我建议向谷歌发出一个请求,看看它会返回什么,或者你可以尝试使用一个服务,如 Spoonacular API ( https://spoonacular.com/food-api ),就像我们在下一章中如何使用 API 一样。

  • 把浏览器中页面元素的背景色换成不同的颜色(模拟把光线换成不同的颜色)——好吧,这个很简单,但最重要的是原理!它的灵感来自于你现在能买到的一系列智能灯泡,比如飞利浦 Hue 系统;你可以在 https://mdn.github.io/web-speech-api/speech-color-changer/ 看到如何实现的演示。

  • 数一个单词的音节——是的,这的确来自亚马逊发来的一封电子邮件;图 6-7 为(部分)截图。

这听起来可能不寻常,但实际上,这并不困难——我们可以使用类似的函数来计算我们选择的单词的音节数:

function new_count(word) {
  word = word.toLowerCase();
  if(word.length <= 3) { return 1; }
  word = word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ");
  word = word.replace(/^y/, ");
  return word.match(/[aeiouy]{1,2}/g).length;
  }

console.log(new_count('sesquipedalian'));  // the answer is 5

如果你想知道倍半句是什么意思,它在这个上下文中有点讽刺意味。它可以表示有很多音节,在这里非常贴切!

希望这能让你有所思考——我们真的只是被我们的想象力和我们想走多远所限制!做到这一点的关键是尽可能保持模块化——如果我们考虑更改 commands 块以接受来自 JSON 文件的命令,那么我们可以保持核心代码不变,继续编辑 JSON 文件以进行任何更新。

摘要

智能助手的创建看起来似乎是一个复杂的过程,但实际上,核心技术的设置非常简单!在本章的过程中,我们已经探索了如何使用语音 API 来创建一个智能助理的工作(如果不是基本的)版本——我们已经为它分配了许多功能,但在未来可以随时添加它们。在本章中,我们已经谈到了一些有趣的概念,所以让我们花点时间来回顾一下我们所学的内容。

我们首先为这一章设置场景,并探索如何构建我们的演示;我们提到了使用一个替代的语音库,为我们的演示提供一点变化。

接下来是构建过程,我们添加了标记和脚本,让它变得栩栩如生;然后我们把它拆开,才理解最初阻止我们的演示阐明任何反应的花絮。然后,在讨论如何为我们的演示添加语言支持之前,我们继续探索如何添加新的功能,以添加图像为例。然后,我们简要介绍了我们应该做出的一些重要更改,以及如何将我们的演示开发成更有用的产品应用,从而结束了这一章。

休息吧!是的,这是一个简单的章节,但故意如此。一个怪物马上就要来了!我们的下一章将探讨在使用语音 API 时,如何使用一些 API 服务来获取数据。有人要吃的吗?我将在下一章解释这个请求,以及更多内容…

七、项目:寻找餐馆

“这些编码弄得我都饿了……肯定是时候了,对吧……?”

是的,该吃点东西了!我不想呆在家里,我想出去。麻烦的是,去哪里?我喜欢什么样的食物?我们可以在网上看看,但那太老套了。为什么不简单地让我们的电脑告诉我们附近哪家餐馆供应我们喜欢的食物?

是的,我们可以使用 Speech APIs 和 Zomato 餐馆搜索服务的强大功能来为我们完成这项工作。在本章的过程中,我们将探索如何将 API 与其他服务一起使用,以创建一些创新的应用来帮助满足这种渴望,并让您为更多的编码做好准备。

设置场景

在为第三章的 Raspberry Pi 板演示做研究的时候,它让我想到,我们能不能使用语音 API 来创建一个更有用的应用,动态地获取它的源代码?好的,答案几乎肯定是“如何”,而不是“如果”,而是“听我说”。一切很快就会明朗。

如果我们再看一下那个演示的代码,你会发现它大部分都是硬编码的;毕竟,这更多的是关于语音 API,而不是找到一项以我最喜欢的两种食物命名的技术…但是我跑题了!为了使使用语音 API 更加有用,我们应该尝试将它绑定到一个数据源,比如 JSON 或 SQL。

这恰好是我们下一个项目的主题。在本章中,我们将创建一个简单的应用,在捷克共和国美丽的城市布拉格寻找合适的就餐地点。为什么是布拉格?嗯,在我开始写这本书之前,我碰巧在假期参观了它——这是一个如此美丽的城市,有华丽的建筑,当然,还有很多餐馆可以去。

好了,记住这一点,我们需要开始构建我们的应用;第一步是设置我们将在演示中包含的参数,所以让我们深入了解一下,更详细地了解一下。

设定我们项目的参数

与任何项目一样,我们需要设定我们将包含的最小可行产品的界限,至少对于本书来说是这样。

这对这个演示特别重要,因为它有潜力发展成更大的东西;与此同时,我们需要意识到,它不会是生产就绪,但至少会给我们机会开发更适合生产使用的东西。

因此,记住这一点,让我们为我们的演示设置场景。请允许我向您介绍“Gofer Good Food”,这是一个概念验证机器人应用,用于在布拉格及其周边地区寻找优秀的餐厅。这种应用可以由当地旅游局免费下载;为了方便起见,我们将创建一个桌面版本的初始 MVP 来探索它是如何工作的。幸运的是,在本书的前面,我们已经使用了我们需要的技术之一。除了语音 API 之外,让我们来看看完整的功能列表:

  • 我们将使用 Zomato.js API 来查找我们的餐馆——尽管我们以布拉格为例,但同样的原则也适用于 API 支持的任何城市或地区。

  • 搜索阶段的所有响应都是基于音频的——这既包括我们寻找合适餐厅的请求,也包括我们应用的响应。

  • 任何显示餐厅详细信息的回答(如地图、电话号码等)。)将呈现在屏幕上。

  • 我们将利用一个服务来提供一个基本的地图工具,显示我们在城市中的位置(我们将在本章的后面把它扩展到餐馆)。

  • 使用货币转换过程来显示您选择的货币的当地价格-对于本书,我们将保持美元,但原则对于其他货币将是相同的。

  • 我们将使用经度和纬度值计算出您的位置,并使用这些值计算出您选择的餐厅距离您有多远。

太好了!我们有很多可以开始的地方——我确信我们可以提出更多的想法来进一步发展这一点。我们将在这一章的后面谈到一些观点。现在,我们将继续为我们的应用确定业务逻辑,但在此之前,我们需要掩盖一些重要的事情:围绕我们的概念证明将如何工作设定预期。

设定期望

在这一点上,我大概能听到你这样说的声音:“啊哦,你所说的……期望……是什么意思?“这是一个合理的问题,但我们称此演示为概念验证是有充分理由的。我再解释一下。

在本书有限的篇幅内,我们永远也不能指望做一个像《正义》这样的全尺寸演示;事实上,我们可以很容易地填满一整本书的页面!我们还有一个额外的复杂性,即正在使用的两种核心技术(聊天机器人框架和语音 API)有点像粉笔和奶酪——两者都不提供对彼此的本地支持,但只要稍加劝说,它们就可以一起工作。

这确实意味着事情可能不是 100%完美的——但如果是,那么生活将会很无聊,对吗?我是那种相信把船推出去看看事情能走多远的人;是的,我们可能会发现它们不起作用,但我们不知道,直到我们尝试!

考虑到这一点,我强烈建议以开放的态度对待这个项目 Speech APIs 在不同的框架下都能很好地工作,所以这在很大程度上是一个判断某件事情是否可行以及可以走多远的问题。下一个项目将不会是生产就绪,但应该给我们提供了很多机会来进一步发展这一原则,使之更加成熟,真正的人可以使用!

好了,警告够了。让我们将注意力转移到确定这个应用的业务逻辑上,这样我们就可以看到它在现实中是如何工作的,以及在以后的日子里我们可能有机会在哪里进行开发。

确定业务逻辑

为了这个项目的目的,当涉及到确定返回给我们的用户的餐馆时,我们可以要求所有方式的细节——事情是这样的,一旦你问了一个,这是问其他人的相同过程!

考虑到这一点,我们将重点问两个问题:第一个是顾客想要哪种菜肴,第二个是价格范围。通过这种方式,我们可以保持相当大的选择范围,并为您日后扩展提供了一个很好的机会。我们将从通过一个按钮发起请求开始,但使用一个单独的按钮来启用每个响应的麦克风——后者将保持在更靠近应用底部的位置,以便不会模糊我们客户的结果(这是 UX 风格的原因,而不是技术原因!)

好吧,我们继续。既然我们已经解决了我们将要做的基本问题,那么是时候了解技术并解决我们将如何为我们的演示提供动力了。我们已经利用了这个应用所需的两个关键 API,但是我们还需要其他 APIs 让我们更详细地看看我们的项目需要使用的工具。

设计我们的项目

我们可以使用各种不同的工具来完成这个项目,所有这些工具都有各自的特点或缺点,但出于演示的目的,我选择使用以下服务:

  • zomato——他们整理了全球数千家餐厅的详细信息,并提供了一个基于 API 的服务,我们可以从中获得详细信息,如菜肴、典型价格、评论等。我们将利用他们的免费 API,来获取我们的应用所需的细节。数据采用 JSON 格式——为了方便起见,我们将使用 jQuery 来消费和呈现数据。我们同样可以使用普通的 JavaScript。

    注意使用这个 API 确实需要在 https://developers.zomato.com/api 注册他们的服务;这是免费的,只要你保持在他们的日常使用率的范围内。

  • rive script–我们在第三章中利用了这一点;这一次,我们将使用语音合成和识别 API 来实现双向语音。

  • 谷歌地图——虽然我个人并不喜欢使用谷歌,但它确实提供了很好的地图服务;我们可以将它嵌入到我们的演示中,这样我们就可以看到我们在布拉格的位置。

  • 我们将在 https://www.exchangerate-api.com/ 使用免费的货币转换器 API,将当地货币转换为美元——如果我们愿意,我们可以对此进行硬编码,但是添加 API 调用将使事情变得更有趣!

  • 我们还将利用 SessionStorage API 来临时存储来自餐馆搜索的值,以便我们的机器人可以使用它们。这样做有逻辑上的原因;我们将在本章末尾更详细地探讨这一点。

  • 作为奖励——如果空间允许——我们将简要介绍一下在显示电话号码时使用点击呼叫的方法。大多数手机会自动这样做,但如果我们采取一些简单的步骤来正确地重新格式化电话号码,我们可以增加我们的机会。

在这一点上,我们需要注意一些限制:

  • 对于 Zomato API,我们将在本地托管 JSON 文件的副本。唯一的原因是速度:JSON 文件超过 6500 行,非常庞大!别担心。我们就不修改了。我将在项目结束时解释切换到使用 Zomato 托管的版本需要做哪些更改。

  • 我们的托管版本将只使用返回的前 20 个名称;我们将在这一章的后面讨论扩展它需要什么样的改变。

好了,现在我们知道了将要使用的技术,是时候开始写代码了!为了使事情变得简单,我们将把它分成几个阶段:第一个阶段是设置我们需要的基本文件和文件夹,所以让我们更详细地看一下所涉及的内容。

设置初始标记和样式

我们很快就会看到,这个演示中有相当多的代码。出于演示的目的,我们将跳过 HTML 标记和样式;这是标准代码,基于我们在之前的演示中使用的代码。相反,我们将把注意力完全集中在关键部分,即 JavaScript 上,看看需要什么来使我们的应用按预期运行。

Setting Up The Basics

在进入代码的真正内容之前,我们需要掩盖一些事情。让我们更详细地看看这个:

  1. 我们使用了 Freepik 从 Flaticon 网站 https://www.flaticon.com/free-icon/placeholder-filled-point_58960 下载的地理定位 SVG 我已经将它包含在代码下载中。如果您想使用替代方案,请相应地修改代码。

  2. 我建议准备一个 JSON 编辑器——在 https://onlinejsoneditor.com/ 有一个很棒的在线编辑器。Zomato 生成的 JSON 文件非常大,所以有一些可以让我们过滤数据的东西将是一个很大的帮助!

  3. 您将需要我们在本练习开始时提到的来自 Zomato 的 API 密匙。

  4. 我们首先从本书附带的代码下载文件夹中提取一个zomato文件夹的副本——然后保存在我们的项目区域。

    如果您在本章的后续演示中看到任何对“迷你项目区域”的引用,它们指的是这个 zomato 文件夹。

好了,有了这些,让我们开始演示吧。

初始化我们的项目

让我们的演示运行所需的大部分工作将是创建我们的脚本文件——这将涵盖语音 API 和我们对 Zomato 数据的调用。

Initializing The Project

第一步是为我们的代码建立一个空白文件——打开您的文本编辑器,然后创建一个新文件,并在继续这些步骤之前,将其保存为迷你项目文件夹的js子文件夹中的script.js:

  1. 现在,我们已经设置了初始声明——继续并保存文件。

  2. 保持文件打开,因为我们将在下一个练习中继续。

  3. 我们有相当多的代码要添加——第一部分将设置基本函数,并添加一些变量声明。继续,按照指示添加以下代码:

    /*jshint esversion: 6 */
    
    (function () {
      "use strict";
    
      let bot = new RiveScript();
    
      const message_container = document.querySelector('.messages');
      const question = document.querySelector('#help');
      const voiceSelect = document.getElementById('voice');
      const mylat = document.querySelector("span.lat");
      const mylon = document.querySelector("span.lon");
      const output = document.querySelector(".output_result");
    
      var cuisineType = sessionStorage.getItem("cuisine");
      var rating = sessionStorage.getItem("priceRange");
      var restCount = 0;
      var takeaway = "";
    
      /****************************************************/
    }());
    
    
  4. Next, we will add in a simple function to take care of working out where we are located in Prague. Leave a blank line after the takeaway variable declaration, and then add in the following code:

      mylat.innerHTML = "50.0904752";
      mylon.innerHTML = "14.3889708";
    
        /*function getLocation() {
        navigator.geolocation.getCurrentPosition((loc) => {
          mylat.innerHTML = loc.coords.latitude;
          mylon.innerHTML = loc.coords.longitude;
        })
      }
    
      getLocation();*/
    
    

    你会注意到这被注释掉了——这是故意的。我们将在本章后面揭示原因。

乍一看,你可能会认为只有四个步骤似乎是一个非常短的练习!这是一个很好的观点,但嘿,我们需要从某个地方开始,我相信你不会感谢我跳进深水区,对不对?不要担心——我们还有很多代码要写。让我们进入下一部分,我们开始让我们的机器人和我们说话。

让我们的机器人说话

好吧,最后一个评论可能听起来像是我们在鼓励一个顽劣的孩子继续胡作非为,但这与事实相去甚远!实际上,下一个演示是让我们的应用具有说话的能力。这是一个两阶段的过程,我们定义我们的应用应该如何说话;“说什么”在后面的演示中出现。

Adding Speech Capabilities

记住这一点,让我们开始吧:

  1. 在前一个程序块末尾的注释行之后,保留一行空白,然后添加这个函数——它会负责将声音加载到我们的演示中:

     function loadVoices() {
        var voices = window.speechSynthesis.getVoices();
    
        voices.forEach(function(voice, i) {
          var option = document.createElement('option');
          option.value = voice.name;
          option.innerHTML = voice.name;
          voiceSelect.appendChild(option);
        });
      }
    
      loadVoices();
    
    
  2. 我们需要添加第二个函数——对于 Chrome 的某些版本,语音必须异步加载,所以添加这个事件处理程序:

    // Chrome loads voices asynchronously.
    window.speechSynthesis.onvoiceschanged = function(e) {
      loadVoices();
    };
    
    
  3. 下一个函数负责在浏览器控制台中呈现错误消息,如果我们的应用在操作过程中抛出错误消息的话。为此,留出一个空行,然后添加以下代码:

    window.speechSynthesis.onerror = function(event) {
      console.log('Speech recognition error detected: ' + event.error);
      console.log('Additional information: ' + event.message);
    };
    
    
  4. 接下来是应用这一部分的关键所在——在这里,我们根据代码中的请求,清晰地表达我们的机器人提供的每条消息。为此,在onerror事件处理程序下面添加以下代码:

    function speak(text) {
        var msg = new SpeechSynthesisUtterance();
        msg.text = text;
    
        if (voiceSelect.value) {
          msg.voice = speechSynthesis.getVoices().filter(function(voice) {
            return voice.name == voiceSelect.value;
          })[0];
        }
    
        speechSynthesis.speak(msg);
      }
    
    

    本节的其余部分将切换到我们需要为我们的机器人添加的代码——我们将从声明一个对用于配置机器人的brain.rive文件的引用开始。为此,在前一个函数的右括号后面添加接下来的三行,中间留一个空行:

    const brains = [
      './js/brain.rive'
    ];
    
    
  5. 我们之前已经看到了接下来的两个函数,尽管是第一个函数的简单版本——我们需要添加代码来处理我们的机器人如何在屏幕上呈现响应。继续在 brains const 声明下面添加以下代码:

    function botReply(message){
      if (message.indexOf("No problem") != -1) {
    
        $.when(getRestaurants()).then(function() {
          restCount = sessionStorage.getItem("restCount");
          message = "No problem, here is the " + restCount + " I've found:";
          message_container.innerHTML += `<div class="bot">${message}</div>`;
        }).then(function(){
          $(".here").css("display", "block");
          output.textContent = "";
        });
      } else {
        message_container.innerHTML += `<div class="bot">${message}</div>`;
      }
    
      location.href = '#edge';
    }
    
    
  6. 接下来,我们需要添加在与机器人交互时,负责在屏幕上呈现我们的响应的函数:

    function selfReply(message){
      var response;
    
      response = message.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
    
      if (response.indexOf("No problem") != 1) {
        restCount = sessionStorage.getItem("restCount");
        message = "No problem, here is the " + restCount + " I've found:";
      }
    
      message_container.innerHTML += `<div class="self">${message}</div>`;
      location.href = '#edge';
    
      bot.reply("local-user", response).then(function(reply) {
        botReply(reply);
        speak(reply);
      });
    }
    
    
  7. 有了这两个函数,我们需要再添加三个来管理我们的机器人的初始化——第一个是这个:

    function botReady(){
      bot.sortReplies();
      botReply('Hi there! Hungry? Looking for a restaurant here in Prague?');
    }
    
    
  8. 第二个负责如果机器人无法初始化会发生什么:

    function botNotReady(err){
      console.log("An error has occurred.", err);
    }
    
    
  9. 我们的机器人不能自动初始化(我们稍后会解释原因)——为了解决这个问题,我们需要为 Start a search 按钮添加一个事件处理程序。为此,继续添加以下代码:

    question.addEventListener("click", function() {
      speak("Hi there! Hungry? Looking for a restaurant here in Prague?");
      bot.loadFile(brains + "?" + parseInt(Math.random() * 100000)).then(botReady).catch(botNotReady);
    });
    
    /****************************************************/
    
    
  10. 我们已经完成了这一部分。继续保存代码。让文件保持打开状态,因为我们将很快继续下一部分。

好了,我们完成了第一部分,但还有很多要做!现在,我们应该已经有了基本的容器函数,以及我们的初始变量和让我们的机器人说话过程的第一部分。

这个项目的下一部分是事情变得有点复杂的地方——在我们可以让我们的机器人说出它发现了什么之前,我们必须首先让它找到一些可以谈论的东西!是的,下一部分是我们去挖掘符合我们标准的餐馆的细节。让我们深入研究一下,更详细地了解一下其中的机制。

获取餐厅详细信息

下一部分将变得更加有趣——我们可以真正开始展示语音 API 如何与我们可以消费的其他服务协同工作。

在接下来的几页中,我们将使用前面提到的 Zomato 服务获取所选餐馆的详细信息,并将结果组合成可以在屏幕上显示的格式。

Searching For Restaurants

让我们从添加代码开始:

  1. 我们需要添加的第一部分负责计算纬度和经度两点之间的距离,这样我们就可以指出餐馆离我们现在的位置有多远。为此,在前一个事件处理程序下面留一个空行,然后添加这个函数:

    function distance(lat1, lon1, lat2, lon2) {
      var p = 0.017453292519943295;    // Math.PI / 180
      var c = Math.cos;
      var a = 0.5 - c((lat2 - lat1) * p)/2 +
              c(lat1 * p) * c(lat2 * p) *
              (1 - c((lon2 - lon1) * p))/2;
    
      return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
    }
    
    
  2. 接下来是本部分的关键部分——给 Zomato 打电话,获取符合我们选择标准的餐馆的详细信息。为此,我们要添加一个有点长的函数,所以我们将把它分成几个部分;先添加这部分:

    function getRestaurants() {
      $.ajax({
        method:'GET',
        crossDomain: true,
        url: 'js/restaurants-prague.json',
        dataType: "json",
        async: true,
        headers: {
          "user-key": "c697ba51c6b29523f885bb3a8b279c93"
        },
        success: function(response){
    
    < ADD IN CODE HERE >
    
        }
      });
    }
    /***************************************************/
    
    
  3. 我们现在可以添加三个代码块来完成这项工作——第一个代码块用于根据我们的选择标准过滤 JSON 文件。继续操作,插入以下代码行,替换上一步中的<ADD IN CODE HERE>注释:

    /* filter on cuisine type and user rating */
    var returnedData = $.grep(response.restaurants, function (element, index) {
      return ((element.restaurant.cuisines == cuisineType) && (element.restaurant.price_range == rating));
    });
    
    
  4. 下一个代码块负责将找到的餐馆数量存储为 sessionStorage 值——这用于更新从我们的机器人返回的响应。继续在 grep 函数下面添加这几行代码,中间留一行空白:

    // Work out how many restaurants and store in session Storage
    restCount = (returnedData.length == 1 ? "1 restaurant" : returnedData.length + " restaurants");
    sessionStorage.setItem('restCountValue', restCount);
    
    
  5. 接下来是演示这一部分的真正内容——在这里,我们从经过过滤的 JSON 数据中检索各种值,并将它们呈现在屏幕上。这采用了一组嵌套的 for…语句的形式——继续在上一步之后添加以下代码,中间留一个空行:

      for(var i=0; i<returnedData.length; i++){
        var distanceaway = distance(mylat.innerHTML, mylon.innerHTML, returnedData[i].restaurant.location.latitude, returnedData[i].restaurant.location.longitude);
    
        for(var x=0; x<returnedData[i].restaurant.highlights.length; x++){
          if (returnedData[i].restaurant.highlights[x] == "Takeaway Available") {
            takeaway = "Yes";
          }
        }
    
        var newDiv = $("<div class="card">");
          newDiv.append(
            $("<div class='card-body'>").append(
            $("<span>").html("<img src=" + returnedData[i].restaurant.thumb + "><h1><a href=" + returnedData[i].restaurant.menu_url + ">"+returnedData[i].restaurant.name+"</a></h1><img class="rating_img" src='./img/" + returnedData[i].restaurant.price_range + ".png'><span class="distance"><img src='./img/location.svg'>" + distanceaway.toFixed(2) + " kms</span>"),
            $("<p>").html("Tel. Nos: " + returnedData[i].restaurant.phone_numbers),
            $("<p>").html("Rating: <span class="av_rating">" + returnedData[i].restaurant.user_rating.aggregate_rating + " / 5 </span>"),
         $("<p>").text("Address: " + returnedData[i].restaurant
         .location.address),
         $("<p>").text("Cuisine: " + returnedData[i].restaurant.
          cuisines),
         $("<p>").text("Average cost for two: " + returnedData[i].restaurant.average_cost_for_two + " " + returnedData[i].restaurant.currency + " (or USD " + amt + ")"),
    
         $("<p>").text("Is takeaway available: " + takeaway),
         $("<p>").text("Latitude: " + returnedData[i].restaurant
         .location.latitude),
         $("<p>").text("Longitude: " + returnedData[i].restaurant
         .location.longitude),
         $("<p>").html("<a href=" + returnedData[i].restaurant.url + ">Link to Restaurant</a>")
       )
     );
     $(".here").append(newDiv);
    
      // reset
      distanceaway = 0;
    }
    
    
  6. 唷!那是一些功能,是吧?不要担心,我们已经完成了这一部分。我们还需要添加一个部分来完成这个文件。继续保存您到目前为止所做的工作——您可以保持文件打开,因为我们很快就会回来添加剩余的代码。

好的,我们进展不错。这个文件的大部分代码已经完成。在切换到配置我们的机器人之前,我们还有一部分要做,那就是添加语音识别 API。

我们将使用它向应用口述我们的选择——这意味着我们现在可以简单地说话,应用会将其翻译成书面文本,而不是输入文本(就像我们在本书前面的演示中所做的那样)。让我们深入研究一下如何将早期演示中的代码重用到更实用的东西中。

添加语音输入功能

对于 script.js 文件的最后一部分,我们需要添加代码,以允许我们的机器人识别来自我们的口头命令;希望您能认出早期演示中的大部分代码,尽管我们已经将它重新用于我们的应用中!

实际上,语音识别 API(和它的姐妹,语音合成 API)的大部分基本框架不太可能随着项目的不同而发生巨大的变化;它可能看起来有所不同,但是如果仔细观察代码,您会看到出现了相同的结构,例如 speechstart 和 result。记住这一点,让我们看看如何重用早期演示中的代码来完成项目的这一部分。

Adding Speech

好了,让我们继续为 script.js 文件添加最后一部分代码:

  1. 我们将在前面的注释下留出一个空行,然后添加这个getUserMedia调用:

    navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
    
    <ADD CODE IN HERE >
    
        }).catch(function(err) {
        console.log(err);
      });
    
    
  2. 接下来,留下一个空行,然后用这些变量和属性声明:

    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognition();
    
    recognition.interimResults = false;
    recognition.maxAlternatives = 1;
    recognition.continous = true;
    
    

    替换短语<ADD CODE IN HERE>

  3. 接下来是负责管理语音识别服务或在事件发生时做出响应的事件处理程序。第一个是当我们点击Click and talk to me!按钮时启动服务:

    document.querySelector("section.speech > button") .addEventListener("click", () => {
      let recogLang = "en-US";
      recognition.lang = recogLang.value;
      recognition.start();
    });
    
    
  4. 我们需要添加的下一个事件处理程序负责检测语音的存在,也就是说,我们已经开始说话了。为此,留出一个空行,然后添加以下代码:

    recognition.addEventListener("speechstart", () => {
      console.log = "Speech has been detected.";
    });
    
    
  5. 同样,当语音识别服务检测到一个单词或短语被正确识别并返回到我们的应用时,我们有一个事件处理程序来处理。我们用它来触发我们的机器人显示下一个问题——为此,继续添加代码:

    recognition.addEventListener("result", (e) => {
      console.log = "Result has been detected.";
    
      let last = e.results.length - 1;
      let text = e.results[last][0].transcript;
      output.textContent = text;
      selfReply(output.textContent);
    });
    
    
  6. 我们还有两个事件处理程序。当我们结束谈话时,我们需要向 API 发出信号;speechend 事件处理程序为我们处理这些:

    recognition.addEventListener("speechend", () => {
      recognition.stop();
    });
    
    
  7. 如果出现任何错误,我们需要在浏览器的控制台区域显示适当的错误消息。为此,我们使用了恰当命名的错误事件——在前面的事件处理程序之后添加以下代码:

    recognition.addEventListener("error", (e) => {
      console.textContent = "Error: " + e.error;
    });
    
    
  8. 我们现在已经完成了该文件,请继续保存您的工作,然后暂时关闭它。剩下的代码我们将添加到一个单独的文件中。

唷!我们完成了——至少这个文件完成了!诚然,这需要做很多工作,但设置它以便我们可以更早地测试更改将是棘手的,并使组装步骤更加复杂。不过,如果你能走到这一步,那就做得很好。休息一下,喝点酒庆祝一下!

好了,回到现实,我们还有一部分要处理,那就是告诉我们的机器人说什么。虽然我们有好几个步骤要完成,但我向您保证代码会简单得多,所以事不宜迟,让我们深入了解一下。

配置机器人

下一部分对您来说应该有些熟悉,至少在构造方面——是时候为我们的机器人设置各种响应,以便作为客户向我们表达。为此,我们将使用 RiveScript bot 框架,与本书前面的演示方式类似;这一次,我们将扩展我们在演示中首次使用的一些功能。

Adding Speech

好了,我们开始吧:

  1. 配置我们的机器人的第一步是打开一个新文件,然后添加这条语句——这告诉我们的机器人使用 RiveScript 解释器的版本 2:

    ! version = 2.0
    
    
  2. 接下来,留下一个空行,然后添加第一个函数——这个函数负责在继续下一个问题之前,用 Zomato 可以识别的格式替换其中一种食物类型,并在正确的情况下将其呈现到 sessionStorage 中:

    > object foodtype javascript
      var newFood
      for (var i = 0; i < args.length; i++) { newFood = args[i] }
    
      if (newFood == "local") { newFood = "Czech" }
      newFood = newFood.charAt(0).toUpperCase() + newFood.slice(1)
      sessionStorage.setItem("cuisine", newFood)
    
      return "Do you have a price range in mind - budget, midrange, or expensive?"
    < object
    
    
  3. 我们要添加第二个函数——为此,保留一个空行,然后添加以下代码,以保存 Zomato 提供的可用价格范围值的正确值:

    > object rating javascript
        var priceRange
        for (var i = 0; i < args.length; i++) {
          priceRange = args[i]
        }
    
        if (priceRange == "budget") { priceRange = 1}
        if (priceRange == "midrange") { priceRange = 2}
        if (priceRange == "highend") { priceRange = 3}
        sessionStorage.setItem("priceRange", priceRange)
    
        return "Ok let us see what I can find..."
    < object
    
    
  4. 我们的第三个(也是最后一个)函数获取 Zomato 找到的餐馆数量,然后作为与机器人对话的一部分呈现出来:

    > object restCount javascript
        return sessionStorage.getItem("restCountValue")
    < object
    
    
  5. 接下来,我们需要开始添加语句来模拟我们将与机器人进行的对话。为此,我们将从第一个问题的回答开始——留下一个空行,然后添加代码:

    + search restaurants
    - Ok. Searching for a restaurant - what cuisine would you like? Indian, Italian or something else?
    
    
  6. 我们现在需要注意所需的食物类型——为此,在上一步之后继续添加以下代码,中间留一个空行:

    + i would prefer (chinese|indian|local|mexican) please
    - <call>foodtype <star></call>
    
    
  7. 我们需要问的最后一个问题是价格范围——为此,继续添加以下代码,中间留一个空行:

    + (budget|midrange|expensive)
    - <call>rating <star></call>
    ^ I have found a selection of restaurants for you! Would you like to see the restaurants I've found?
    
    
  8. 最后一步是确认我们希望看到符合我们所选标准的可用餐馆——为此,保留一个空行,然后添加以下代码:

    + yes please
    - No problem, here is the <call>restCount</call> I've found:
    
    
  9. 我们还需要添加最后一个模块——这是一个通用的总括模块,以防我们没有说出正确的文本或者 API 没有识别出我们所说的内容。留下一行,然后放入下面的代码:

    + *
    - Sorry, I did not get what you said
    - I am afraid that I do not understand you
    - I did not get it
    - Sorry, can you please elaborate that for me?
    
    
  10. 至此,我们完成了编辑。将文件保存为迷你项目区的js子文件夹中的brain.rive,然后关闭它。

我们差不多到了可以测试我们项目的时候了,但是在此之前,我们还需要修复一个部分。是的,我确实说过我们已经完成了script.js,但是如果你仔细观察,你可能会发现一个问题。

好吧,我承认“问题”可能是一个太强烈的词,但尽管如此,如果我们不这样做下一部分,那么很可能你会看到(或 USDundefined)出现在最终的结果!是的,在“搜索餐馆详细信息”演示的第 5 步中,我们放入了一个变量distanceaway来显示转换后的美元金额,但是没有放入任何东西来执行转换…哦!

别担心。这很容易解决。它允许我们使用另一个 API,所以让我们更详细地看看这个特性是如何融入我们的整个演示中的。

将货币兑换成美元

如果我们仔细看看代码(如图 7-1 所示),我们确实可以看到被引用的distanceaway变量——快速搜索文件的其余部分不会显示对它的任何其他引用。

img/490753_1_En_7_Fig1_HTML.jpg

图 7-1

正在使用的 distanceaway 变量…

这是一个简单的解决办法,所以让我们开始添加它作为我们的下一个演示。

Displaying Usd Conversion

要解决此问题,请按照下列步骤操作:

  1. 我们将首先重新打开 script.js 文件,然后查找位于距离函数之前的注释行。

  2. 在它下面留一个空行,然后添加这个变量声明:

    var amt;
    
    
  3. 在那个声明下面,留一个空行,然后放入这个函数:

    $.getJSON('https://api.exchangerate-api.com/v4/latest/CZK', function(data) {
      var currencies = [];
      $.each(data.rates, function(currency, rate) {
        if (currency == "USD") {
          amt = (rate * 300).toFixed(2);
        }
      });
    })
    
    
  4. 继续保存文件,我们可以关闭它。您的演示现在终于完成了!

好了,这次我们的代码真的完成了!完成最后一个函数后,让我们将注意力转向测试演示,这样您就可以看到这两个 Speech APIs 如何与演示中使用的其他服务进行交互。

测试演示

接下来是最有趣的部分,也可能是最令人头疼的部分:是时候测试我们的演示了!

为此,您需要浏览到https://speech/restaurant;第一次启动演示时,我们会看到类似于图 7-2 所示的截图。

img/490753_1_En_7_Fig2_HTML.jpg

图 7-2

我们完成的演示,准备接受输入

要操作它,我建议使用这种方法,您的回答用粗体文本表示:

  • 单击开始搜索。然后等待最初的欢迎信息出现并被朗读。

  • 出现提示时,说搜索餐馆

  • 根据对菜肴类型的要求,说我更喜欢本地菜

  • 当提示价格范围时,说中档

  • 当机器人确认它已经找到了一些结果,说是的,请

这听起来像是我们将测试引向一个已知的场景,而没有预料到用户可能会说什么,但这是故意的。这有利于展示演示在技术上运行得有多好,以及用户提出的问题是否自然——在这种情况下,我怀疑还有改进的空间!

暂时把这个放在一边,我们可以开始看看图 7-3 中的演示是如何工作的。

img/490753_1_En_7_Fig3_HTML.jpg

图 7-3

我们实际演示的第一部分...

如果我们逐步完成每一步,我们可以看到最后的结果,如图 7-4 所示。

img/490753_1_En_7_Fig4_HTML.jpg

图 7-4

实际演示的第二部分

正如我们从图 7-4 的截图中看到的,我们已经包括了每家餐厅的基本细节,以及将顾客带回到该餐厅的 Zomato 网站的链接——这对于查看额外的细节很有用,例如营业时间和评论。我们在这里添加的细节完全可以调整——每个细节都是从该餐馆的 JSON 对象中获取的,并在适当的时候显示为文本(或 HTML 标记)。

我们将在这一章的后半部分进一步阐述这一点。

好了,现在是最重要的部分了?我们的代码是如何工作的?我们已经在本演示的过程中介绍了一些有用的 API,所以在我们了解如何在未来的开发中改进代码之前,让我们花点时间来更详细地回顾一下我们使用了哪些 API。

详细剖析我们的代码

在本章的过程中,我们已经讨论了大量的代码——其中大部分位于我们的script.jsbrain.rive配置文件中。它大部分使用了我们在早期演示中介绍过的原理,所以现在应该开始熟悉了!

然而,考虑到所使用的两种核心技术并不相互提供本地支持,值得花些时间来探索我们是如何设法让这两种技术更详细地相互通信的。记住这一点,在依次进入 script.js 和 brain.rive 文件之前,让我们首先更详细地看一下 HTML 标记。

剖析我们的 HTML 标记

这个文件中的大部分内容相当简单——一旦定义了对 CSS 样式文件的引用,我们就设置一个#page-wrapper div 来包含我们所有的内容。然后我们创建一个.voicechoice部分来放置下拉菜单,允许我们选择机器人应该使用哪种语言,并为客户显示默认的语音设置。

接下来是.location部分,我们用它来呈现代表我们选择的酒店的经度和纬度坐标的硬编码值,以及显示酒店在布拉格的位置的(工作中的)Google Maps 图像。

然后我们有#help部分,这是主持对话的地方;最后一个条目被我们的脚本文件重新格式化,以存放从 Zomato 数据中找到的结果(稍后将详细介绍)。然后我们用.speech部分来完成演示的这一部分,它包含启动语音识别服务的按钮,以及一个位置合适的.output区域来显示我们的响应。

分解主脚本文件

这是事情变得更有趣并开始走到一起的地方——我们的 script.js 文件包含了运行我们的演示所需的大部分代码。我们从一组变量声明开始;这包含了一个注释掉的代码块,我们将使用 HTML5 地理定位 API 来提供我们的位置(在本章的后面会详细介绍)。

语音合成 API 和我们的机器人

声明完成后,我们就有了loadVoices()函数,它用于将 Google 提供的声音加载到我们在标记文件中设置的.output_result下拉框中。请注意,我们还为onvoiceschanged事件提供了一个事件处理程序——一些版本的 Chrome 要求异步加载这些声音,尽管在最新版本的 Chrome 中这应该不是什么问题。然后我们转移到speak()函数,在这里我们配置一个新的SpeechSynthesisUtterance接口实例,然后从voiceSelect下拉菜单中选择声音,从 msg 变量中选择文本。

接下来,我们进入机器人运行所需的代码——我们从一个声明开始,在运行botReply()selfReply()函数之前,该声明存储了对我们的brain.rive文件的引用。这些函数稍微复杂一些,所以下面是它们的功能分类,从botReply函数开始,我们用它来呈现来自聊天机器人的响应:

  • 屏幕上显示的来自我们的机器人的所有消息都通过消息占位符变量传递——我们首先检查这个变量的内容。

  • 如果消息变量包含文本“No problem”,我们使用它作为触发器,首先搜索 Zomato 提供的 JSON,然后存储返回的餐馆数量。

  • 然后,我们相应地调整消息,然后在屏幕上构建标记并呈现在.here div 中,并重置.output_log div。

  • 但是,如果我们没有找到“没问题”的实例,那么我们只需在屏幕上显示消息,然后转到客户的下一个响应。

    如果您想知道为什么我们使用“没问题”,这很简单:我们需要使用一个触发短语来拦截和修改返回给用户的消息。在我们的演示中,其他地方没有任何文本!

我们的两个回复函数中的另一个是selfReply——这是一个用于将我们答案的文本转换成我们的机器人可以处理的东西的函数。按顺序,事情是这样的:

  • 我们首先分配 response 变量——这用于存储我们请求的文本副本,然后将其转换为小写并从中删除标点符号或特殊字符(为了 RiveScript 正确运行,必须省略这些)。

  • 然后我们检查消息的内容——和以前一样,如果它包含“没问题”,我们截取它并修改它以显示我们从会话存储中获得的餐馆计数值。

  • 然后,我们用适当的标记重新格式化消息,然后将其呈现在屏幕上,并将其推送到 speak 函数以进行口头回应。

然后,我们用两个系统函数和一个事件处理程序来完成演示的这一部分——前两个用于准备运行的机器人(botReady),以及如果机器人不可用会发生什么(botNotReady)。然后我们在问题事件处理程序中使用这两者,它在初始化我们的演示之前加载brain.rive文件。

获取餐馆数据

下一部分可能看起来最复杂,但实际上,大部分内容都被 AJAX 请求占用了,我们请求 AJAX 将数据加载到我们的演示中。我们从两个函数开始:第一个调用由 ExchangeRate-API.com 提供的服务,获得我们使用的每个价格的美元等值。第二个是距离,用于计算每家餐馆离我们现在的位置有多远。

这一部分的核心部分是getRestaurants()函数——这是我们加载 JSON 文件的地方,然后使用$.grep过滤掉任何不符合我们要求的的餐馆。然后,我们计算出返回了多少家餐馆(restCount),然后将其作为一个值存储在会话存储中。然后,我们遍历每个餐馆,首先计算出离我们位置的距离(distanceaway),然后如果它提供外卖,将结果呈现为附加到。屏幕上的元素。

语音识别 API 的使用

脚本文件的最后一部分包含了我们用来识别口头命令的代码——大部分代码都是在早期的演示中重用的,所以在这个阶段应该开始熟悉了!尽管如此,还是值得更详细地回顾一下。

我们首先使用getUserMedia允许我们从浏览器访问我们的麦克风——一旦初始化,我们就定义一个SpeechRecognition对象,基于我们的浏览器支持哪个版本。同时,我们设置了一些属性,包括interimResultscontinuous

然后,我们有一组事件处理程序来管理我们说话时发生的事情。当我们点击位于演示底部的按钮时,第一个初始化服务。然后我们有speechStartresult,它们分别标识是否有任何东西被说出或者我们是否有一个被服务识别的结果。在 result 事件处理程序中,我们得到一份副本,并将其分配给文本变量,然后在屏幕上显示它,并将其传递给我们的机器人以口头表达它。剩下的两个事件处理程序,speechenderror,负责在我们结束谈话时或者在使用我们的演示时出现错误时处理。

浏览 bot 配置文件

我们代码的最后一部分是我们用于机器人的brain.rive配置文件——与脚本文件相比,它看起来像是在公园里悠闲地散步!

我们首先声明应该使用哪个版本的 RiveScript 解释器——在这个项目中,我们使用版本 2。接下来是三个 RiveScript 函数——它们在 RiveScript 中被称为对象,但工作方式与标准函数相同。首先,我们遍历传递的所有参数,然后用local的实例替换Czech(Zomato 需要的),然后在会话存储中存储一个重新格式化的版本。然后我们返回下一个句子来使用,这个句子询问我们的客户想要使用哪个价格范围。

第二个函数以类似的方式工作——不过这一次,我们将客户指定的价格范围转换成一个数字。后者是 Zomato 正确操作所需要的。我们通过返回我们的机器人将为我们搜索数据库的确认来完善这个函数。在第三个函数中,我们简单地获取找到的餐馆数量的值,然后在屏幕上显示给客户。

配置文件的其余部分包含了我们用来与机器人进行对话的每个语句——注意,我们使用了相同的+符号来表示机器人的触发器,并且在每个响应前都有一个符号。有几个<call...>语句在使用;这些函数调用文件开头指定的函数(或对象)。最后一部分(以*符号开始)是一个通用的总括,如果机器人在理解我们所说的内容方面有问题,或者它与它期望在屏幕上看到的内容不匹配,它就会开始工作。

更进一步

现在我们有了一个工作演示,我们从这里去哪里?好吧,如果我们决定进一步发展,我们可以考虑将一些选项整合到这样的应用中。让我们来看看一个选择:

  • 添加错误处理和系统消息——我们的演示依赖于我们说出正确的命令,但是即使有最好的意愿,我们也不总是做对!演示需要一些东西来帮助通知用户什么时候有问题,以及如何最好地处理它。

  • 检查我们的机器人对话中使用的陈述——有些情况下,我们可能想看一下暂停,例如当我们说,“好的,让我们看看我能找到什么……”目前,这直接跳到寻找答案,这不太现实!

  • 我们已经将 Zomato 提供的纬度和经度值作为数字插入,但是如何使用 Google Maps 或 OpenStreetMap 等服务将它们转换为地图链接呢?

  • 增加可信度——语音识别 API 仍在开发中;它很擅长识别内容,只要你说清楚。如果报告的置信度较低,提供一些视觉反馈将有助于鼓励用户改变他们的方法。

  • 为要说的内容提供更好的视觉提示——我们在brain.rive文件中放了一些有限的选项,但是演示根本没有指出要说什么!改善这种情况的一个很好的方法是在每个问题下用较小的字体显示一些文字,这样客户就知道该说些什么来引发合适的回答。

  • 增加语言支持——在当今世界,这几乎是一个必不可少的先决条件;这甚至可能有助于将诸如平均餐费之类的值本地化为客户的本地货币,而不是以不熟悉的货币提供它们!

  • 将演示中使用的两个按钮合并成一个——我们必须提供一个来触发语音合成 API,因为浏览器不允许在页面加载后自动触发。这将涉及触发speak()函数所需代码的一些返工,以及测试代码中需要调用它的地方。

  • 有时候,我们可能无法从演示中获得完整的结果,所以机器人最终会说没有任何餐馆,而事实可能并非如此!JSON 文件非常大,有没有更好的方法让它更有弹性,更不容易误报?

  • 我们甚至可以检查是否可以直接从应用中预订;这可能只是一封电子邮件或一个电话,但任何有助于客户的事情都会受到重视!

虽然我们确实可以做很多事情来改进和扩展我们的演示,但我想提出三个简单的变化,我们现在可以毫不费力地做出这些变化。这些是使用tel:格式格式化联系号码,添加基于地理位置的功能,并扩展我们向客户显示的数据量。这些都是我们可以实现的简单更改,所以不再赘述,让我们深入了解一下,从联系人详细信息的格式开始。

格式化电话号码

对于我们将要讨论的三个变化中的第一个,我想探索一下演示中列出的电话号码是如何格式化的。

是的,我知道这听起来有点傻,但是请容忍我,一切都会变得清晰。

这不是我们可能从笔记本电脑或台式机上做的事情,但对于那些从智能设备访问互联网的用户,我们可以自动格式化号码,以允许他们直接从页面上被调用。这就是所谓的“点击呼叫”服务——只需做一些简单的改变,我们就可以相应地显示我们所有的号码。

让我们来看看我们需要实现的变化:

  • 号码应该以国际拨号格式提供,带有加号、国家代码、区号和号码。使用图 7-4 中的 nae maso 餐厅,我们可以这样写我们的链接:

  • 尽管这不是必须的,但是像这个例子中所示的那样添加连字符将有助于更好的检测。

  • 移动浏览器应该自动检测数字,尽管 Mobile Safari 会更进一步,自动将其转换为正确的格式。如果您想禁用它,以便在所有浏览器中保持一致的格式,请在 HTML 标记的头部添加以下 meta 标记:

<a href="tel:+420-2-22312533">+ 420 (2)-22312533</a>

<meta name="format-detection" content="telephone=no">

这些是我们可以对我们的演示进行的简单更改,并将为任何在可以拨打或接听电话的设备上使用该应用的人提供额外的帮助。

好吧,让我们改变策略。我们三个变化中的第二个是以位置为中心;我们的演示目前被硬编码到一个酒店,以证明它的工作,但如果你不能住在那里是没有用的!让我们修改代码,使其更加动态。我们将在下一个练习中这样做。

添加基于位置的设施点

我们的演示使用了基于布拉格郊区的酒店 Savoy 的硬编码细节——这是一家我很幸运能够入住的华丽酒店,但作为一家五星级酒店,我知道不是每个人都能负担得起!因此,考虑到这一点,我们应该使我们的位置值更加动态——没有比使用地理定位 API 更好的方法了。

对这个 API 的支持在桌面浏览器中不是问题;它被所有主流浏览器覆盖,如图 7-5 所示。

img/490753_1_En_7_Fig5_HTML.jpg

图 7-5

支持 HTML 地理定位 API 来源:caniuse.com

对于移动设备也没有真正的担忧;众所周知,除了 Opera Mini 之外,所有的 API 都提供了本地支持。(Opera Mini 的使用率很低,所以这也不太可能是个问题!)

记住这一点,让我们来看看如何更新我们的演示——我们已经做了一些必要的艰苦工作,所以让我们来看看需要什么来完成它并使它运行。

Demo: Adding Location-Based Details

添加基本的地理定位非常容易,至少可以给我们当前的经度和纬度值。为此,请按照下列步骤操作:

  1. 作为一个快速测试,打开一个浏览器的控制台区域,然后放入以下代码:

    navigator.geolocation.getCurrentPosition(function(location) {
      console.log(location.coords.latitude);
      console.log(location.coords.longitude);
      console.log(location.coords.accuracy);
    });
    navigator.geolocation.getCurrentPosition((loc) => {
      console.log('The location in lat lon format is: [', loc.coords.latitude, ',', loc.coords.longitude, ']');
    
    
  2. 这将为我们提供类似于我们最初在演示中硬编码的值。

  3. 既然我们已经测试了它,我们需要调整我们的代码——我们已经在我们的演示中包含了该代码的功能部分,尽管我们还没有启用它。为此,请查找这些行并将其注释掉:

    mylat.innerHTML = "50.0904752";
    mylon.innerHTML = "14.3889708";
    
    
  4. 接下来,删除紧随其后的块周围的注释,从function getLocation()...到(包括)函数调用getLocation()

  5. 保存更改——您的代码现在可以识别位置,不再依赖于固定值。如果您刷新演示,您将看到应用在屏幕上显示的每个餐厅的距离值的新数字。

这样做的不利方面意味着我们将需要在其他地方进行变革;否则,我们可能会得到一些异常高的值,因为它会根据您在世界上的任何地方进行计算,这不太可能是在布拉格!

不过这个演示最棒的地方是,我们可以将其更改为报告世界上成千上万个地方的餐馆,因此在您居住的地方附近一定会有一些可用的信息。

显示关于餐馆的更多详细信息

我们的第三个也是最后一个变化更多的是一个品味的问题——有一大堆不同的价值观我们可以融入到我们的演示中!这可能包括诸如营业时间、网上递送或是否可预订餐桌等例子;这是一个细读原始 JSON 数据并选择我们想要显示的细节的问题。

为此,我建议将原始 JSON 文件复制到 JSON 编辑器中,然后用它来浏览数据。像 JSON Editor Online ( https://jsoneditoronline.org/ )这样的在线工具非常适合这个目的,如图 7-6 所示。

img/490753_1_En_7_Fig6_HTML.jpg

图 7-6

正在使用在线 JSON 编辑器

如果您使用 JSON Editor Online,您可以单击每个键值(在突出显示的示例中位于冒号的左侧)并获得文件中该值的完整路径。在突出显示的示例中,我们将以这段代码结束,这段代码可以放入我们的演示中:

$("<p>").text(
  "Latitude: " + returnedData[i].restaurant.timings
),

还有一大堆其他值可以尝试,所以请随意浏览文件并决定尝试哪一个!

摘要

哇,那是一些怪物项目!语音 API 是可以在各种情况下使用的技术之一,比如查找餐馆的详细信息。我们已经在这一章中介绍了一些有用的技术,所以让我们停下来回顾一下我们所学的内容。

我们首先设置场景、参数和业务逻辑,以便在我们的项目中使用,然后探索我们将如何构建我们的演示,并设置一些关于它的使用的期望。然后,我们开始为我们的项目设置初始标记,然后添加脚本的各个部分,例如说话的设备或查找餐馆细节。

接下来,我们测试了我们的演示,然后详细分析了我们的代码,以理解后者是如何工作的,并看到了与早期 Speech API 示例的相似之处。然后,我们总结了这一章,看看我们如何进一步发展——这探讨了一些关于改进我们现有代码的想法,以及添加新功能,作为将其开发成可以呈现在真实客户面前的东西的一部分。

好吧,我们继续。我们还有更多要讲的!请举手你们中有多少人使用在线音乐流媒体服务,比如 Deezer 或 Spotify?我敢打赌,你们中会有相当一部分人适用这一点:如果我们可以用声音来控制我们演奏音乐的方式,会怎么样?是的,你没听错。且听我说,且听下回分解。

八、项目:查找和播放音乐

我喜欢听音乐——当我花几个小时开发代码或写书的时候,听听你最喜欢的艺术家的音乐是有道理的。只要我手里也有一杯酒,我就很开心。但是我跑题了。

在数字化我的全部音乐收藏和发现在线音乐流的乐趣之前,我常常费力地翻阅数百张 CD。我们只能说这是一个折衷的收藏;它是什么并不重要——它是一张 CD,然后点击播放。自从改用 Spotify 以来,那些日子已经一去不复返了——找到我的音乐变得容易多了,我也不用担心空间问题了!

自从改用 Spotify 后,我开始思考,有没有可能用我的声音来控制它?在我的研究过程中,我还没有发现任何人做过类似的事情,至少在 Spotify 上;这是否意味着没有人设法做到这一点,或有这样做的愿望?我想知道。由于我热衷于划船,探索未知的领域,我想,为什么不试一试呢?

为我们的项目设置背景

在本章的过程中,我们将组装一个快速而肮脏的应用,从我们的浏览器中使用 Spotify API 来播放选定的专辑。作为其中的一部分,我们将增加以下功能:

  • 该应用将(几乎)完全通过语音控制,使用语音识别 API——唯一不可控的部分将是初始授权过程和从真正的 Spotify 客户端的第一次播放(更多信息请见下文)。

  • 我们将能够执行基本任务,例如播放或暂停音乐,向前或向后跳过一首歌曲,以及将专辑添加到 Spotify 中您保存的专辑列表中。

  • 我们将显示一个曲目列表,以及每个曲目的长度,加上同一艺术家的专辑列表-后者将包括专辑名称和图像。

  • 我们将提供一个选项来搜索相似名字的艺术家-结果将显示他们的名字和 Spotify ID,我们也将使用它们来获得他们的专辑。

这只是我们使用 Spotify API 所能实现的一小部分——我们还可以做一大堆其他事情,但空间限制意味着我们无法涵盖所有内容!

希望这会给你一个平衡的混合功能,我们可以用我们的声音控制,以及如何在这种情况下使用 Spotify API 的感觉;作为预览,你可以在图 8-1 中看到一张完整演示的截图。

img/490753_1_En_8_Fig1_HTML.jpg

图 8-1

我们完成演示的预览

好吧,我们继续。我相信你会问一个问题:当我们有很多其他服务可以使用时,比如 Deezer,Google Play Music,甚至亚马逊音乐,为什么还要选择 Spotify?这是一个很好的问题,所以我们来看看选择使用 Spotify 的原因。

为什么选择 Spotify?

当与在线音乐流媒体服务合作时,我们有一个相当健康的选项列表可供选择——有些你可能从 Spotify 或 Deezer 等电视广告中了解到,其他则来自已经涉足这一领域的老牌公司,如亚马逊或谷歌 Play Music。对于这一章,我选择使用 Spotify,有几个原因:

  • 与许多服务一样,你总是需要注册——Spotify 的帐户要求很低,并且很容易用代码建立一个基本的认证系统(正如我们将在后面的演示中看到的)。

  • 使用 Spotify 的一个重要原因是,我已经是付费用户了——当然,还有其他渠道提供类似的服务,但如果你已经使用了市场领导者,那么使用它们就没有意义了。他们提供各种各样的音乐,尽管分辨率不高,但这一限制并不重要,因为这个练习是关于用我们的声音远程控制服务,而不是服务提供的声音质量水平!

  • 随着 Deezer、Spotify 和 Amazon Music 等公司的出现,在线音乐流媒体正在发展成为一个健康的市场,但它们中的大多数都有一个共同点——它们似乎都很难使用标准的客户端技术与每个服务的 API 进行交互!唯一的例外是 Spotify——你很快就会看到,我们将利用第三方包装库来帮助运行我们的代码,这是 Spotify 可用的有限数量之一。(事实上,有一个服务甚至不让我登录他们的 API 区域……)

好吧,我们继续。现在我们已经讨论了我们将使用的音乐服务,我们需要探索如何构建我们的演示。除了 Spotify 和语音 API 之外,我们还将使用许多工具(当然!),所以让我们深入了解一下我们将使用什么来更详细地构建我们的演示。

构建我们的演示

为了构建我们的演示,除了 Spotify 和 Speech APIs 之外,我们还将使用几个工具。我选择使用的方法如下:

  • Vue.js 用于授权框架代码——我可以使用 React 之类的工具,但是这增加了不必要的复杂性。Vue.js 让事情变得美好而简单,默认情况下不需要使用 Node.js 等服务器端工具来展示使用 Spotify 的基本操作。授权访问 Spotify 的一个很好的例子是李·马汀在 https://codepen.io/leemartin/pen/EOxxYR 进行的 CodePen 演示——我们将以此为基础进行演示。

    我们稍后将更多地讨论这个过程的授权部分。

  • 我们可以直接与 Spotify 的 API 交互,但为了简单起见,我们将使用 José Perez 的包装库,该库可从 https://github.com/JMPerez/spotify-web-api-js 获得。

  • jQuery——这纯粹是为了方便;在理想的情况下,我们应该重构代码来使用 Vue 或者普通的 JavaScript!为了透明起见,我们将使用 jQuery 的最新版本,在撰写本文时是 3.4.1。可以使用 jQuery 的其他版本,尽管您需要测试以确保您的代码仍然工作。

好了,有了我们的主要工具,我们现在可以开始编码了!但是——我听到你说——授权是怎么回事?是的,与任何 API 一样,我们需要成为授权用户才能访问服务;服务提供商需要一种方法来跟踪使用情况,并为所有用户维持适当的服务水平。尽管这并不影响我们对语音 API 的使用,但它仍然是我们演示的一个关键部分,所以让我们深入研究一下,更详细地看一下我们的选项。

授权我们的演示

当使用 Spotify API 时,我们应用的一个关键部分将是我们和 Spotify 之间的授权过程;这是为了允许注册访问的 API,所以我们可以流音乐。

这是一个由两部分组成的过程。第一个,我们向 Spotify 注册应用的地方,我们稍后会介绍;现在,让我们假设这种情况已经发生,并看看在与 Spotify 合作时授权可能发生的各种方式。

选择方法

假设我们已经向 Spotify 注册了我们的应用,有三种方法可以授权我们使用 Spotify API。它们如下:

  • 将我们自己授权为用户,可以定期刷新——使用授权码方法。

  • 为用户设置临时授权–使用隐式授权方法。

  • 为应用设置授权,授权可以定期刷新-使用客户端凭证流方法。

我们可以在表 8-1 中看到这些对比。

表 8-1

授权访问 Spotify API 的方法

|

流动

|

访问用户资源

|

需要密钥(服务器端)

|

访问令牌刷新

|
| --- | --- | --- | --- |
| 授权代码 | 是 | 是 | 是 |
| 客户端凭据 | 不 | 是 | 不 |
| 隐性资助 | 是 | 不 | 不 |

来源:Spotify 开发者门户

出于演示的目的,我们将使用隐式授权流选项——这是为完全用 JavaScript 编写的客户端设计的,不需要使用服务器端代码来操作。确实有一个以spotify-web-api-node形式存在的服务器端选项,但是为了展示语音 API 是如何工作的,在服务器端运行它只是增加了一层不必要的复杂性。毕竟,为什么要把事情复杂化,对吗?

使用我们选择的方法的含义

对于我们的演示,我们选择使用隐式授权流方法来授权访问——这个标准是由互联网工程任务组或 IEFT 创建的 RFC-6749。在我们的情况下,使用这种方法是最好的选择,原因如下:

  • 隐式授权流适用于完全使用 JavaScript 实现并在资源所有者的浏览器中运行的客户端。

  • 您不需要任何服务器端代码来使用它——这消除了对复杂的服务器端工具的需要,例如 Node.js

  • 改进了请求的速率限制,但是没有提供刷新令牌。

    你可以在 IETF 网站的 https://tools.ietf.org/html/rfc6749#section-4.2 上看到关于这种方法如何工作的更深入的讨论。

这对我们意味着什么?我们可以直接访问 Spotify 账户服务,使用 API 提供的访问令牌,该令牌以类似于 https://accounts.spotify.com/authorize 的东西开始。

整个过程是在客户端执行的,不涉及密钥,但是访问令牌是短暂的,需要手动刷新,并且当它们过期时没有选项来延长它们。完整的请求将包括查询字符串中的参数,我们可以在表 8-2 中看到这些参数(以及我们演示的后面部分)。

表 8-2

隐式流授权所需的各种属性

|

询问参数

|

价值

|
| --- | --- |
| 客户端 id | 必选。Spotify 在注册时向您提供的客户 ID。 |
| 响应类型 | 必选。设置为“令牌” |
| 重定向 uri | 必选。用户授予/拒绝权限后重定向到的注册 URI。 |
| 状态 | 可选,但强烈推荐。状态对于关联请求和响应非常有用——使用值可以额外保证连接是真正的请求。 |
| 范围 | 可选。用空格分隔的作用域列表。 |
| 显示对话框 | 可选。如果应用已经获得批准,则强制用户再次批准该应用。 |

来源:Spotify 开发者门户

除了看到它的运行,理解它如何组合在一起的最好方法是把它看作一个流程图——我们可以在图 8-2 中看到流程是如何运行的。

img/490753_1_En_8_Fig2_HTML.jpg

图 8-2

我们的演示源的授权过程:Spotify 开发者门户

尽管选择一种方法会有一些限制,但理解这一点很重要——毕竟,我们不可能免费得到任何东西,没有可能影响我们做事方式的东西!

幸运的是,这些限制并不太严重,如果我们决定使用 Spotify 提供的其他授权方法之一,它们的影响可能会减少。不过,那是另一个故事了。与此同时,让我们更详细地看看约束可能如何影响我们的演示。

使用这种方法的限制

虽然我们选择的方法使事情变得简单,并且最适合我们的需求,但仍然有一些约束需要我们注意,这意味着我们不能完全通过语音控制一切。让我们来看看它们是什么:

  • 在运行我们的演示之前,我们需要启动 Spotify 并运行一首歌曲几秒钟(这是我们必须定期做的事情)。如果我们根本不运行 Spotify,你会看到控制台出现错误,专辑也不会播放。

  • 我们需要使用 Spotify 的浏览器版本来进行演示;如果您尝试使用桌面应用,您会发现这两者互不影响,并且您还可能会发现桌面应用中播放的另一张专辑与您的演示中播放的专辑无关!

  • 当运行演示的授权部分时,我们必须为 Spotify 单击 Accept 按钮以允许访问。不幸的是,这在我们的演示中不能通过语音实现,因此我们不能完全用声音控制事情!

好了,现在我们已经介绍了授权访问 Spotify 的基础知识,是时候开始开发代码了。我知道在我们这么做之前,这可能看起来像是一个漫长的等待,但是 Spotify API 并不像它应该的那样简单;我们已经讨论了一些关于如何访问 API 的要点。

现在我们已经讨论了这个问题,我们可以开始设置我们的应用了;第一项任务是建立一个集成,以便 Spotify 将来自我们应用的呼叫识别为正版,并提供适当的内容。

设置先决条件

当使用 API 时,我们经常不得不建立某种形式的帐户或集成——这是必要的,但也是重要的,以便服务提供商可以管理需求并保证注册用户的访问安全。

使用 Spotify 也是一样——我们演示的第一步是设置一个集成,所以让我们深入了解一下为我们的应用创建集成所需的步骤。

Dealing With Prerequisites

要设置集成,请执行以下步骤:

  1. 继续填写所需的详细信息(用红星表示)–您可以使用以下内容作为向导来帮助您完成向导:(我们目前正在以纯粹的开发能力工作,但是如果您决定进行商业化,请确保您相应地设置了一个集成。需要注意的是,一旦集成被启用,您就不能编辑该选项。

    • 你在开发商业整合吗?–

    • 我知道这个应用不是用于商业用途的-

    • 我知道我不能迁移…–

    • 我理解并同意…–

  2. 点击提交。此时,将显示详细信息,并向您展示您的应用的仪表盘。

  3. 您将会看到一个标记为“显示客户端机密”的链接,以及您的客户端 ID。单击该链接,然后记下这两个 id,因为我们将在稍后的演示中使用这两个 id。

  4. 我们将从下载本书附带的代码下载副本开始——继续将spotify文件夹保存到我们的项目区域。这将包含相关的样式和 Vue.js 和 jQuery 库,供我们使用。

  5. 接下来,我们需要在 https://developer.spotify.com/dashboard/ 登录 Spotify 的开发者仪表盘——为此,你需要创建一个免费账户,或者如果你已经是该服务的现有订户,也可以使用现有账户。

  6. Once logged in, click Create a Client ID – you will see a modal appear, similar to the (partial) screenshot shown in Figure 8-3.

    img/490753_1_En_8_Fig3_HTML.jpg

    图 8-3

    在显示器上创建客户端 ID 模式

我们现在准备开始开发代码了!在我们这样做之前,有几件事需要注意:

添加一个图标完全是自愿的;你当然可以决定不使用它,它不会影响你的演示如何运行!我这样做纯粹是为了防止我们的应用抛出找不到合适图标的错误。

您可能会发现,如果您在每次演示后都测试代码,并不是所有的代码都能正常工作。不要惊慌;这是意料之中的!我们覆盖了很多代码,所以我们需要分部分来做——在最终演示结束时,一切都会迎刃而解。

在这个阶段,具备了所有的先决条件,我们现在可以开始编写代码了。有相当多的代码需要处理,所以我们将分阶段完成——第一项任务是建立基本的授权框架,这样我们就可以开始添加代码来与 Spotify 的 API 进行交互。

创建框架

我们现在真的到了可以写一些代码的阶段了!我们的应用集成已经设置好并准备好使用,我们可以将注意力转向为我们的应用设置代码。在这个项目的过程中,有相当多的代码需要完成,所以我把它分成了三个阶段的过程;第一阶段负责基本的授权过程。

为了避免重复发明轮子,我将使用李·马汀的 CodePen 演示(你可以在 https://codepen.io/leemartin/pen/EOxxYR?editors=1010 看到原文)。这使用 Vue.js 框架来布局代码。如果您不熟悉这个框架,也不用担心——在基本层面上,它保持了标记和 JavaScript 代码之间的分离。练习结束后,我们将详细讲解每一部分。

Setting Up Our Skeleton Code

要设置我们的授权框架并准备好投入使用,请执行以下步骤:

  1. 我们首先打开index.html的副本,然后向下滚动(或寻找)标记为<!—INSERT CODE HERE -->的评论。

  2. 我们有相当多的代码要添加,所以我们将分阶段完成——首先,删除注释,然后添加几个空行。

  3. 接下来,继续插入这个块——这将负责我们的语音工具周围的标记:

    <main id="app">
      <h2>Introducing HTML5 Speech API: <br>Controlling Spotify by Voice</h2>
      <template v-if="me">
        <div id="speech">
          <button>
             <i class="fa fa-microphone"></i> Click and talk to me!
          </button>
    
          <p class="output">You said: <br><strong class="output_result"></strong></p>
          <span class="voice">Spoken voice: US English</span>
          <p>Responses:</p>
          <div class="response">
            <span class="output_log"></span>
          </div>
        </div>
    
    
  4. 我们还有几个模块要介绍——我们需要添加的下一个模块将通过提供图像、艺术家和曲目数量等细节来处理我们正在播放的专辑的显示。在前一个代码块之后留出一行空白,然后添加以下代码:

    <div id="currentalbum">
    <span class="albumimage"><img src="img/100.png" /></span>
    <span class="albumartist"></span>
    <span class="albumname"></span>
    <span class="trackcount"></span>
    <span class="year"></span>
    <span class="albumtype"></span>
    <span class="albumID"></span>
    <span class="artistID"></span>
    </div>
    
    
  5. 授权过程不需要下一个块,但现在添加它更容易——这个块将设置控制音乐播放所需的按钮。继续添加下面的代码,在前一个代码块之后留下一个空行:

    <button @click="playmusic"><i class="fa fa-microphone"></i> Play music</button>
    <button @click="pausemusic"><i class="fa fa-microphone"></i> Pause music</button>
    <button @click="playnexttrack"><i class="fa fa-microphone"></i> Go forward 1 track</button>
    <button @click="playprevioustrack"><i class="fa fa-microphone"></i> Go back 1 track</button>
    <button @click="addtosavedalbums"><i class="fa fa-microphone"></i> Add to saved albums</button>
    
    
  6. 我们正在取得良好的进展。下一部分为曲目列表设置了占位符,用于我们在演示中播放的专辑。继续添加以下代码行,首先在前一个代码块之后留下一个空行:

    <div id="albumlist">
      <p>Track listing:</p>
      <ul></ul>
    </div>
    
    
  7. 下一部分负责显示同一艺术家的其他专辑——在前一部分之后留出一个空行,然后插入以下代码:

    <div id="otheralbums">
      <span>Other albums by Artist:</span><button @click="getalbumsbyartist"><i class="fa fa-microphone"></i> Get Albums</button>
      <ul></ul>
    </div>
    
    
  8. 我们还有一个显示其他同名艺术家的部分——为此,在前面的块之后添加以下代码:

    <div id="artistlisting">
      Search for Artist: <input v-model="searchartist"><button @click="searchartistsbyname"><i class="fa fa-microphone"></i> Search</button>
      <span>Chosen artist: {{searchartist}}</span>
      <div id="artistlist"><ul></ul></div>
    </div>
    
    
  9. 我们几乎完成了标记。还有两个部分需要添加:一个确认你已经登录的隐藏信息块和我们的 Vue 模板的结束代码。继续在上一步之后添加以下代码,中间留一个空行:

        <div id="info">{{ me }}</div>
    
      </template>
      <template v-else>
        <button @click="login">Login with Spotify</button>
      </template>
    </main>
    
    
  10. 在这一点上,保存文件-让它暂时打开。我们将稍作休息,但很快将继续代码。

我们现在已经准备好了我们的标记,可以使用了——但是它不会做很多事情,因为我们还没有添加脚本代码来使它可以操作。

我们将很快添加这一点。请随意去喝杯咖啡或饮料,休息一下,因为我们还有很多代码要添加!假设您准备好了,让我们继续演示的下一部分,添加授权代码。

从 Spotify 获得授权

在我们的下一个演示中,我们应该开始看到事情的发生——这是我们添加代码来启动授权请求并希望得到批准的地方!好吧,这听起来比实际情况更复杂,因为这一切都发生在后台,只需要我们点击一下按钮。为了理解我的意思,让我们在下一个演示中加入代码。

Making Our Authentication Process Operational

要让我们的演示授权使用 Spotify,请执行以下步骤:

  1. 本演示的第一项任务是在上一个演示的结束标签</main>后添加几个空行,然后插入以下代码——这将为我们提供基本的 Vue 对象,我们将使用该对象启动对 Spotify 的授权:

    <script>
      const app = new Vue({
        el: '#app',
        data() {
          return {
            client_id: 'bf253330696448f696dc45889f3fd61c',
            scopes: 'user-top-read playlist-read-collaborative playlist-read-collaborative playlist-modify-public playlist-read-private playlist-modify-private streaming app-remote-control user-modify-playback-state user-read-currently-playing user-read-playback-state user-library-modify',
            redirect_uri: 'https://speech/spotify',
            me: null,
            albumname: 'Not listed',
            searchartist: null,
            createplist: null
          }
        },
        methods: {
          <!—ADD IN ADDITIONAL METHODS HERE -->
        }
        })
    </script>
    
    
  2. 我们需要添加更多的配置功能——首先是实际调用 Spotify 来请求授权。继续在 methods 对象中添加以下代码,替换标记为<!-- ADD IN ADDITIONAL METHODS HERE -->的注释:

    login() {
      let popup = window.open(`https://accounts.spotify.com/authorize?client_id=${this.client_id}&response_type=token&redirect_uri=${this.redirect_uri}&scope=${this.scopes}&show_dialog=true`, 'Login with Spotify', 'width=600,height=800')
    
              window.spotifyCallback = (payload) => {
                popup.close()
    
                fetch('https://api.spotify.com/v1/me', {
                  headers: {
                    'Authorization': `Bearer ${payload}`
                  }
                }).then(response => {
                  return response.json()
                }).then(data => {
                  this.me = data
                })
                spotifyApi = new SpotifyWebApi({
                clientId: '<ADD IN CLIENT ID HERE>',
                clientSecret: '<ADD IN CLIENT SECRET HERE>'
              });
              spotifyApi.setAccessToken(payload);
              }
            },
    
    

    您将看到几行与 clientID 和 clientSecret 值相关的代码;这里需要它们是有原因的,尽管它们不用于授权过程。在这个演示之后,我们将讨论它的重要性。

  3. 我们还需要添加一个对象——这将触发调用,向 Spotify 发起请求。为此,在我们的 Vue 对象的结束})之前,紧接着前一个块的结束}之后添加以下代码:

    mounted() {
         this.token = window.location.hash.substr(1).
                      split('&')[0].split("=")[1]
    
                 if (this.token) {
                   // alert(this.token)
                   window.opener.spotifyCallback(this.token)
                 }
            }
    
    
  4. Go ahead and save the file, but leave it open (or minimized) – we’re now ready to test our work! For this, browse to https://speech/spotify/. If all is well, we should see the initial login button displayed, as indicated in Figure 8-4.

    img/490753_1_En_8_Fig4_HTML.jpg

    图 8-4

    我们演示中 Spotify 的初始登录按钮

如果我们单击 Login with Spotify 按钮,我们应该会看到一个弹出窗口,请求访问我们的演示以使用 Spotify API。图 8-5 显示了该请求的(部分)截图。

img/490753_1_En_8_Fig5_HTML.jpg

图 8-5

授权我们演示的(部分)请求

破解密码

在上两次演示过程中,我们已经建立了授权访问 Spotify API 所需的基本框架;这暴露了一些有用的指针,所以让我们更详细地看一下代码,以了解它们是如何组合在一起的。

第一个演示的第一部分很简单;在本章的后面,我们设置了所有的标记,包括操作语音功能所需的标记。唯一需要注意的是在演示的第 5 步中使用了@符号;这些是对我们在第二个演示中创建的 Vue 对象中的函数的调用(它们的操作方式与普通 JavaScript 中的onclick="...."类似)。我们还使用了双花括号——这些只是占位符,它们由 Vue 中捆绑的 Handlebars 库代替实际值。

这两个演示的真正关键在第二部分——对于外行来说,Vue 的工作原理是创建(并初始化)一个配置对象。我们通过将 app const 定义为 Vue 的一个新实例来单击 off,并向其中传递一个目标元素("#app");然后,我们在数据对象中定义一些值——分别是我们的客户端 ID、允许的范围、用于授权的重定向 URL,以及用于演示的一些其他占位符。

接下来,我们创建一个方法对象,在其中我们设置了login()对象(或函数);这定义了一个包含我们用来请求访问的 URL 的弹出变量。一旦 Vue 实例在演示中被挂载(),这将启动一个回调,调用login()对象。这会向 Spotify 发出请求,然后 Spotify 会相应地进行处理。假设它成功了,我们得到了一个响应——它隐藏在演示中,因为我们不需要一直显示它。同时,我们创建了一个新的spotify-web-api库实例,在其中定义了 clientID 和 clientSecret 值,为我们开始使用 Spotify 做好了准备。

来自 Spotify 的流媒体内容

随着我们的授权流程现在开始运作,是时候添加代码来从 Spotify 流式传输内容了。为此,我们将利用 José Perez 的spotify-web-api-js包装器库;这使用基于承诺的语法包装了对每个 Spotify 端点的相关调用。

剧透这是一个冗长的演示。如果你需要休息,请随时暂停!

为了提醒您完成后它会是什么样子,我们可以在图 8-6 中看到我们完成的应用的部分截图,一旦我们点击登录 Spotify 并确定以允许授权。

img/490753_1_En_8_Fig6_HTML.jpg

图 8-6

预览我们的 Spotify 演示…

好吧,让我们开始我们的代码。

Making Use Of The Spotify Api

要将 Spotify 中的内容流式传输到我们的演示中,请按照以下步骤操作:

  1. 返回到我们在之前的练习中打开的index.html文件,然后向下滚动到开始的<script>标记,并在它的正下方添加这行代码,如下所示:

      <script>
        var spotifyApi, albumIDplaying, artistimage;
    
    
  2. 我们需要添加一个助手函数来将时间从毫秒转换成更合理的形式。为此,将此代码添加到上一步中变量声明的下方,中间留一个空行:

        function msToMinAndSec(millis) {
          var minutes = Math.floor(millis / 60000);
          var seconds = ((millis % 60000) / 1000).toFixed(0);
          return minutes + ":" + (seconds < 10 ? '0' : ") + seconds;
        }
    
    
  3. 接下来,回头看看我们创建的login()对象。您应该会看到类似这样的内容:

  4. 我们需要从我们的 Spotify 帐户中添加客户端 ID 和客户端 secrets 值;为此,继续用您在本章前面创建的客户机和秘密 id 替换注释。

  5. 下一个任务是开始添加在我们的演示中操作各种特性的函数。首先是添加一些东西,让我们做最重要的部分:播放音乐!为此,我们有一个实质性的功能要添加进去;它的第一部分负责从 Spotify 获取当前状态,并在屏幕上显示专辑详细信息:

    playmusic() {
    // get current playing album
    spotifyApi.getMyCurrentPlaybackState()
    .then(function(data) {
      spotifyApi.play(data);
      sessionStorage.setItem("chosenalbum", data.item.album.id);
      sessionStorage.setItem("chosenartist", data.item.artists[0]
    .id);
    
         var albumtype = data.item.album.album_type;
    
         $(".year").html(data.item.album.release_date.substring(0,4));
         $(".albumtype").html(albumtype.charAt(0).toUpperCase() + albumtype.slice(1));
         $(".trackcount").html(data.item.album.total_tracks + " tracks");
         $(".albumID").html(data.item.album.id);
         $(".artistID").html(data.item.artists[0].id);
         $("#currentalbum > span.albumname").html(data.item.album.name);
         $("#currentalbum > span.albumartist").html(data.item.artists[0].name);
         $("#currentalbum > span.albumimage > img").attr("src", data.item.album.images[1].url);
       }, function(err) {
         console.error(err);
       });
    
    
  6. 接下来,留下一个空行——我们现在需要添加play()函数的右半部分。为此,加入以下代码,它负责在屏幕上显示曲目的详细信息和时间:

       // get album tracks
       spotifyApi.getAlbumTracks(sessionStorage.getItem("chosenalbum"))
       .then(function(data) {
         var tracklength;
    
         $("#albumlist > ul > li").remove();
    
         data.items.map( function(item) {
           spotifyApi.getAudioFeaturesForTrack(item.id)
           .then(function(response) {
             tracklength = response.duration_ms;
             $("#albumlist > ul").append(`<li>${item.track_number}: ${item.name} - ${msToMinAndSec(tracklength)}</li>`);
           });
         });
       }, function(err) {
         console.error(err);
       });
     },
    
    
  7. 相比之下,接下来我们需要添加的三个对象看起来很小!三个函数中的第一个是允许我们暂停音乐所需的函数——继续,将它添加到步骤 6 中函数的结尾}的正下方:

    pausemusic() {
      // stop music
      spotifyApi.pause();
    },
    
    
  8. 其次,类似地,我们需要添加一些东西,让我们能够前进一个轨道——将它添加到pausemusic()函数的正下方:

    playnexttrack() {
      // play next track
      spotifyApi.skipToNext();
    },
    
    
  9. 对于这些小函数中的第三个,在前一个块的正下方添加以下代码,以允许我们返回一个轨道:

          playprevioustrack() {
            // play previous track
            spotifyApi.skipToPrevious();
          },
    
    
  10. 我们正在取得良好的进展,尽管仍有大量代码需要添加。下一个功能将允许我们按名称搜索艺术家。继续添加以下代码,紧跟在playprevioustrack()对象的结束},之后:

    searchartistsbyname() {
      // search artists by name
    var artistquery = $("#artistlisting > input").val();
    
    spotifyApi.searchArtists(artistquery)
     .then(function(data) {
      data.artists.items.map( function(item) {
        if (item.images.length == 0) {
          artistimage = "https://speech/spotify/img/noimage.png";
        } else {
          artistimage = item.images[2].url
        }
    
        $("#artistlist > ul").append(`<li><span><img src ="${artistimage}"></span><span>${item.name} - ${item.id}</span>
    </li>`);
      });
    }, function(err) {
      console.error(err);
    });
    },
    
    
  11. 我们需要添加的下一个函数是检索我们选择的艺术家的专辑列表——这由下面的代码负责,我们需要将它添加到步骤 10 中的上一个对象的正下方:

    getalbumsbyartist() {
      // get albums by a certain artist
      var selectedartist = sessionStorage.getItem("chosenartist");
    
      spotifyApi.getArtistAlbums(selectedartist)
        .then(function(data) {
          data.items.map( function(item) {
            $("#otheralbums > ul").append(`<li><span><img src="${item.images[2].url}"></span><span>${item.name}</span></li>`);
        });
      }, function(err) {
        console.error(err);
      });
    },
    
    
  12. 我们快完成了。添加的最后一个功能将负责将选择的专辑添加到我们在 Spotify 中保存的专辑中。与其他函数相比,这是一个短函数;继续在getalbumsbyartist()对象的结束}下面添加以下代码:

      addtosavedalbums() {
        // add to saved albums
        var getalbum = $(".albumID").text();
        spotifyApi.addToMySavedAlbums([getalbum]);
      },
    },
    
    
  13. 不可否认,这最后一段代码有点骗人——我们用它来帮助将我们选择的艺术家的 Spotify ID 设置到会话存储中。向下滚动到结束的</script>标签,然后留下一行,并添加以下代码:

```html
<script>
  $(document).ready(function() {
    $("body").on("click", "#artistlist ul li", function() {
      var chosenartist = $(this).text();
      var chosen = chosenartist.split(" ");
      sessionStorage.setItem("chosenartist", chosen.pop());
    });
  });
</script>

```
  1. 现在,我们可以保存我们的工作了——继续,休息一下!这可能看起来有很多代码,但是我向你保证,我们已经添加了大部分代码,相比之下,下一部分(通过语音控制我们的演示)会短得多。

  2. 当你准备好了,让我们预览一下我们的工作成果——为此,请浏览https://speech/spotify/;如果一切顺利,我们应该会看到类似于这个庞大练习开始时显示的屏幕截图。

    spotifyApi = new SpotifyWebApi({
      clientId: '<ADD IN CLIENT ID HERE>',
    clientSecret: '<ADD IN CLIENT SECRET HERE>'
    });
  spotifyApi.setAccessToken(payload);
  }
},

在这一点上,我们可以让 index.html 文件打开,但最小化-我们将在这个项目的最后一部分重新访问它。

如果你成功地用一个工作演示做到了这一点,我必须向你表示祝贺——最后一个练习肯定是一个怪物!虽然很高兴看到这个庞然大物开始变得像一些可操作的东西,但是这是一个更详细地研究代码的好机会;它展示了几个有用的点,值得更多的关注。

理解代码

我们首先设置了一些在代码中使用的变量,比如 SpotifyAPI 包装器实例和相册图像的新占位符。然后,我们添加了一个 helper 函数,将 Spotify 为每个曲目返回的时间转换为更有用的时间,然后使用包装器库用我们的客户端详细信息启动 Spotify 实例。

接下来,我们添加了许多不同的事件处理程序,它们利用了包装器库;尽管它们都以不同的方式工作,但大多数都基于相同的原则,即使用基于承诺的语法。一个很好的例子是playmusic()函数,它看起来像这样:

playmusic() {  // get current playing album
  spotifyApi.getMyCurrentPlaybackState()
  .then(function(data) {
    spotifyApi.play(data);
    sessionStorage.setItem("chosenalbum", data.item.album.id);
    sessionStorage.setItem("chosenartist", data.item.artists[0].id);

    $(".year").html(data.item.album.release_date
.substring(0,4));
    $(".albumtype").html(data.item.album.album_type);
    $(".trackcount").html(data.item.album.total_tracks + " tracks");
    $(".albumID").html(data.item.album.id);
    ...
    albumIDplaying = data.item.album.id;
    sessionStorage.setItem("chosenartist", data.item.artists[0].id);
  }, function(err) {
    console.error(err);
  });

在我们的演示中,当我们点击播放按钮时,该功能被触发——我们首先从 Spotify 获取当前播放状态(因此我们需要运行 web 客户端几秒钟!).然后,我们将当前艺术家和专辑的值压入会话存储,然后开始播放并输出各种值,如专辑图像、曲目数量和每首曲目的名称。使用会话存储有点麻烦,但却是必要的。它强制 spotifyApi 库在正确的时间为专辑获取正确的 ID;否则,我们可能会以它试图传递一个空值而告终,结果得到一个 400 错误(或者没有列表)!

其他函数以类似的方式工作(有一些例外)——每个函数都使用基于承诺的语法从 Spotify 请求带有数据的 JSON 对象,然后提取相关数据并在屏幕上的适当位置显示。

与 Spotify 交谈

最后,我们可以让我们的应用识别我们的声音!是的,看起来我们在触及项目的真正关键(和本书的重点)之前已经讨论了很多。然而,如前所述,Spotify API 并不是最容易使用的;Jose 的库向抽象出大量代码迈出了一大步,但这仍然意味着我们必须向项目中添加相当多的代码。

谢天谢地,您将要看到的大部分代码现在应该已经相当熟悉了;我们已经使用了以前演示中的大部分内容,这有利于重用。唯一真正的变化是在result()处理程序中,我们告诉我们的演示程序当它识别出有效的语音时该做什么。一旦我们完成了修改,我们可以在图 8-7 中看到事情的预览。

img/490753_1_En_8_Fig7_HTML.jpg

图 8-7

我们的 Spotify 演示,支持语音控制

考虑到这一点,让我们深入研究一下让我们的应用更详细地识别口头命令所需的代码。

Talking To Our Demo

要添加语音功能,请按照下列步骤操作:

  1. 我们将从返回到之前演示中打开的 index.html 开始。向下滚动到底部的第二个<script>块,然后在DOM.ready()功能的结束});前添加一个空行。

  2. 我们现在需要添加代码,使我们的演示能够通过语音识别命令——我们有大量的代码要添加,所以让我们一个块一个块地做吧。第一个块放在我们刚刚添加的空行下面,它将为语音识别 API 的操作声明一些变量和属性:

    const output = $(".output_result");
    navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
    
      const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
      const recognition = new SpeechRecognition();
    
      recognition.interimResults = false;
      recognition.maxAlternatives = 1;
      recognition.continuous = true;
    
    
  3. 我们有一系列的事件监听器要添加——第一个将在我们点击“点击并跟我说话”时触发语音识别服务!按钮。对于每个事件处理程序,在前一个事件处理程序之后留一个空行,然后依次添加代码块:

    $("body").on("click", "#speech > button", function() {
      let recogLang = "en-US";
      recognition.lang = recogLang.value;
      recognition.start();
    });
    
    
  4. 下一个事件处理程序是我们在之前的演示中使用过的——当演示检测到麦克风处于活动状态,并且检测到声音,但不一定识别为语音时触发:

    recognition.addEventListener("speechstart", () => {
      $(".output_log").text("Speech has been detected.");
    });
    
    
  5. 我们之前已经看到了这个下一个处理程序的开始——它决定了所说的任何东西是否可以被识别为语音,如果是这样的话,就对其进行操作:

    recognition.addEventListener("result", (e) => {
      $(".output_log").html("Result has been detected.");
    
      let last = e.results.length - 1;
      let text = e.results[last][0].transcript;
    
      $(".output_result").html(text);
    
      /* Play music */
      if (text.indexOf("play music") != -1) {
        $("#app > button:nth-child(4)").trigger("click");
      }
    
      /* Pause music */
      if (text.indexOf("pause music") != -1) {
        $("#app > button:nth-child(5)").trigger("click");
      }
    
      /* Go forward */
      if (text.indexOf("go forward") != -1) {
        $("#app > button:nth-child(7)").trigger("click");
      }
    
      /* Go back */
      if (text.indexOf("go back") != -1) {
        $("#app > button:nth-child(8)").trigger("click");
      }
    
      /* Save album */
      if (text.indexOf("save album") != -1) {
        $("#app > button:nth-child(9)").trigger("click");
      }
    
      /* Get other albums */
      if (text.indexOf("get other albums") != -1) {
        $("#otheralbums > button").trigger("click");
      }
    
      /* Search artist */
      if (text.indexOf("search artist") != -1) {
        $("#artistlist > input").val("enigma");
        $("#artistlist > button").trigger("click");
      }
    
      $(".output_log").html("Confidence: " + (e.results[0][0].confidence * 100).toFixed(2) + "%");
    });
    
    
  6. 我们又添加了三个事件处理程序——下一个程序检测我们是否停止了说话,如果是这种情况,就会触发onspeechend处理程序:

    recognition.addEventListener("speechend", () => {
      recognition.stop();
    });
    
    recognition.onspeechend = function() {
      $(".output_log").html("You were quiet for a while so voice recognition turned itself off.");
      //console.log("off");
    };
    
    
  7. 最后一个事件处理程序负责基本的错误处理——在生产环境中,这可能会更深入,但是对于我们的目的来说,只需在控制台中呈现原始的错误消息就足够了:

      recognition.addEventListener("error", e => {
        if (e.error == "no-speech") {
          $(".output_result").html("Error: no speech detected");
        } else {
          $(".output_result").html("Error: " + e.error);
        }
      });
    }).catch(function(err){
      console.log(err);
    });
    
    
  8. 我们已经完成了对文件的编辑,请继续并保存它。我们现在可以预览我们的工作结果;为此,请浏览至https://speech/spotify/。如果一切正常,我们应该首先看到类似于图 8-7 所示的截图。

在这最后一个演示的过程中,我们回顾了在本书前面的第一个项目中看到的一些函数;它显示了添加语音功能是多么容易,而且大部分代码可以轻而易举地重用。与许多项目一样,结果处理程序经常改变;记住这一点,让我们更详细地看一下代码,看看它们是如何组合在一起的。

详细研究代码

我们在本演示中使用的许多代码现在应该开始看起来有点熟悉了 API 的美妙之处在于我们可以设置许多股票处理程序,并在我们的项目中重用它们。当然,有些(比如结果处理器——more anon)可能需要改变,但是许多(比如onspeechend)可以在不同的项目中保持不变。考虑到这一点,让我们更详细地看一下代码。

我们首先声明了一些变量和属性,包括缓存.output_result,定义一个语音识别 API 实例为SpeechRecognition ,,并为 API 设置interimResultsmaxAlternatives,continuous属性。

然后我们开始设置事件处理程序来响应 API——第一个允许我们使用en-US作为默认语言来触发 API。请注意,这不是使用事件处理程序的标准 jQuery 语法——我们必须推迟它,以便一旦按钮出现在页面上就可以分配它(直到我们登录后才可用)。

我们添加的下一个处理程序是针对speechstart事件的——这是直接从一个早期的项目中提取的,这表明在使用语音 API 时重用代码是多么容易。然后,我们转向一个相当长的结果事件处理程序——它看起来很长,但是大部分都是定义当 API 识别某些短语时应该发生什么。该处理程序转录结果属性的内容,然后将其输出到屏幕上,并确定是否可以执行所提供的任何操作。

然后,我们添加了三个事件处理程序,它们是从以前的项目中继承来的:两个用于确定我们是否已经停止说话(speechendonspeechend),最后一个用于提供基本的错误日志记录(error)。值得注意的是,两个speechend处理程序的工作方式略有不同——第一个决定我们是否已经停止说话,第二个控制当我们停止说话时会发生什么,因此可以关闭 API。

更进一步

希望到现在为止,你已经有机会玩这个演示了——当然有些地方有点粗糙,但我们应该记住,这一章不是关于使用 Spotify,而是我们如何设置语音 API 来与 Spotify 内容进行交互!

然而,我们可以做一些事情来改善界面,并开始为我们的应用添加更多的润色:

  • 你可能已经注意到,曲目列表的排序顺序似乎不正确。这是有原因的;一旦从 API 中检索到列表,它就与每个条目在包含列表的<span>元素中的插入顺序有关。为了解决这个问题,我们可以将条目推入一个对象数组,然后在屏幕上显示新的顺序之前使用.sort方法对它们进行排序。(只是确认一下,曲目 id 是正确的,只是顺序不对!)

  • 对于你们当中的纯粹主义者来说,你们可能已经发现了一些曲目的时间稍微有点晚;这很可能是由于我们将原来的毫秒值转换成更容易阅读和识别的值。这可能不是一个真正的问题,但可能值得试验一下这个函数,看看是否可以改进。

  • 回想一下本书前面的内容——在许多项目中,我们实现了对另一种语言(法语)的支持。我们没有理由不能在这里添加同样的支持;我们需要更新 UI 来反映这些变化,并为每个特性添加正确的触发命令,比如播放音乐。

  • 这个演示中非常缺乏的一个方面是来自应用的任何形式的反馈——我们可以看到我们口头发出的命令,但是使用语音合成 API 来确认某个操作已经发生怎么样?

  • 返回的相册列表可能相当大;这是我们绝对可以改进的一个方面!我们的演示显示了从 API 返回的前 20 个项目,但是我们应该考虑将它限制为前五个(?),如果需要,可以选择显示扩展列表。

  • 我们已经实现了navigator.mediaDevices.getUserMedia方法来启动对麦克风的访问(我们在第一章中使用过),但是目前关闭按钮是不可操作的。理想情况下,我们应该提供一个关闭语音功能的选项,但这需要对代码进行重构,因为按钮最初是不可见的。

这应该给你一个我们可以去的地方的味道和一些想法让你开始。在 José的 wrapper 库中,我们可以使用更多的选项,所以我建议通读他的网站和 Spotify 开发者门户的文档——有很多内容可以帮助你将应用开发成未来某个时候可以成为语音制作解决方案的东西。

摘要

为任何应用添加语音功能都开启了一个可能性的世界,尤其是对于我们这些能力较弱的人——或者可能只是懒惰的人!在本章的过程中,我们一直致力于为使用 Spotify API 的项目添加语音功能;让我们花一点时间来回顾一下我们在本章中所讲的内容。

在探索为什么我们选择 Spotify 作为我们的 API 提供商之前,我们首先设置了我们项目的背景。然后,我们进入了一个简短的讨论,围绕我们将如何设计我们的项目,以及如何获得 Spotify 授权的 API(以及这对我们的项目意味着什么)。

然后,我们设置了基本的认证过程,首先解决了一些先决条件;然后,我们将注意力转向添加代码,以允许我们从 Spotify 流式播放内容。最后但并非最不重要的是,我们随后添加了代码,允许我们与 Spotify 进行口头交互,然后探索一些想法,以帮助我们开始将我们的项目进一步开发成可以在生产环境中使用的东西。

好了,休息一下,伙计们,因为我们有另一个大项目要做了!人们不会不注意到网上购物在过去几年中是如何真正起飞的;越来越多的人使用设备在线购物,他们不一定需要在屏幕上看到结果。解决这个问题的一个方法是在结账过程中加入语音功能——听我说,我会在下一章向你展示如何做到这一点。

九、项目:采购流程自动化

我们几乎就要完成这本书了,但是我们还有一个项目等待着我们!我敢肯定,你已经花了几个小时试图找到一个特定的产品,然后把它放在一个篮子里,并通过了几个屏幕来完成结帐,对不对?用更复杂的篮子真的很痛苦。如果我们能够利用我们声音的力量来自动化这个过程的一部分,会怎么样?

借助语音 API 的强大功能,我们应该能够口头要求网站找到产品并将其添加到购物篮中,然后使用支付请求 API 结账——所有这一切都无需触摸任何键盘。听起来不可能?在本章的过程中,我将向你展示这个想法的大部分现在可能只是现实。我们将介绍将语音功能添加到流程核心部分所需的各个步骤,以便您可以看到我们可以为客户节省多少时间和精力。

设置场景

在我们深入研究如何实现这样一个项目的细节之前,我想回答一个问题,我相信至少你们中的一些人会提出这个问题:我们为什么要这样做?嗯,有两个很好的理由,如果不是三个的话:亚马逊(或谷歌——取决于你的亲和力),可访问性,以及…嗯,为什么不呢?在你们都认为我已经完全失去了情节之前,让我解释一下我说的那个有点神秘的答案是什么意思。

首先,亚马逊,或者谷歌,是因为智能助理的发明。我们在书的前面提到过这个主题,越来越多的搜索是如何在没有视觉显示的情况下实现的。他们是如何做到的?嗯,是通过智能助手的使用!当你可以让亚马逊的 Alexa 或苹果的 Siri 为你工作时,为什么要花时间在网站上搜索呢?

第二,或可访问性,是当今包容世界的一个关键因素——我们已经有了屏幕阅读器,它可以(或多或少地)在网站周围帮助视力有缺陷的人。问题是,它要求我们开发人员花费大量的时间和资源来增加基于 ARIA 的可访问性能力——这是有用的,但存在风险,它可能不会对每个网站都很好,或限制我们如何做事。相反,为什么不让网站为我们做这项工作呢?我们总是可以从提供基本功能和可访问性标签开始,但是添加语音功能将使我们更加灵活,并提供更加个性化的内容。

“为什么不”的第三个也是最后一个原因和罐头上说的一模一样——为什么不呢?我们永远不应该因为需要保证安全或只做我们知道的事情而感到压抑;过了一段时间,这变得很无聊,我们开始没有食欲!我们绝对应该寻求突破我们所能达到的极限——它可能总是有效,也可能不总是有效,但毕竟,你只有尝试过才会知道,对吗?

带着那个惊天动地的想法,我们继续吧。我们将很快看看我们将如何为这一章设计我们的项目,但在此之前,让我们把注意力转向我们将如何保持事情在范围内,并确保这个项目保持在正轨上。

保持事物在范围内

在这一点上,我们必须小心——如果我们不小心,建立一个电子商务商店,更不用说添加语音功能,可以很容易地填满整本书的页面!

考虑到这一点,我们将把自己限制在几个关键的过程中,这样您就可以了解如何设置语音选项,并将其扩展到您自己项目的其他领域。我们将涉及的两个关键领域是

  • 将产品添加到购物车中——我们将为一个有四种产品的商店设置代码(这应该足以显示一点多样性;不管我们卖的是限量系列还是上百种产品,流程都是一样的)。

  • 通过输入卡的详细信息并点击支付按钮来结帐-这将模拟付款被发送并收到积极的回应作为回报。

我们将不讨论如何构建我们的商店,因为所需的代码量太大了,在本章的篇幅中我们无法做到。相反,我们将假设我们有一个基本的,但功能,商店工作,并在此基础上继续,我们将增加语音功能。

在本章的最后,我们将通过一些提示让你开始将商店扩展到其他领域。

好了,现在我们已经设置好了场景,是时候深入研究我们将要使用的工具了;我们已经遇到并使用了两个(以语音 API 的形式),但是我们将在我们的项目中使用一些其他的。

设计项目

与任何项目一样,我们可以使用各种不同工具中的任何一种来完成工作——不言而喻,显然没有一种工具是我们可以(或者应该)使用的!我们已经知道我们将使用我们之前看到的语音 API,但是为了让它工作,我们需要引入一些额外的技术。它们如下:

Note

使用 jQuery 只是为了方便;在一个理想的世界中,我们会设法不再使用它,并可能专注于使用普通的 JavaScript 或 React 或 Vue 等框架。

好了,说完了,让我们继续。是时候陷进编码了!在接下来的几页中,我们将涉及相当多的代码;由于篇幅原因,我们将主要关注用于语音功能的 JavaScript,因为 HTML 标记和 CSS 样式是标准的。考虑到这一点,让我们深入研究一下代码的细节。

准备我们的购物车

是时候开始设置东西了。对于这本书的最后一个项目,我们将创建一个基本的商店来销售有限范围的饼干——不仅仅是我妈妈常说的任何一种老饼干,而是那些非常柔软和耐嚼的饼干……嗯……但是我跑题了!

从技术角度来看,我们的演示将是基于两个 CodePen 演示的代码和来自早期演示的语音识别代码的融合;该店铺是维吉尔·帕纳从 https://codepen.io/virgilpana/pen/ZYqJXN 的一支笔的删减版,弹出的支付形式是基于梅康·路易斯在 https://codepen.io/mycnlz/pen/reLOZV 的那支笔。从头开始创造一些东西是可能的,但是考虑到本书中可用空间的限制,我们不可能公正地完成它!

Setting up the Shopping Cart

先不说这个,让我们从开店开始:

img/490753_1_En_9_Fig1_HTML.jpg

图 9-1

我们最初的商店

  1. 我们将从本书附带的代码下载中提取一个shop文件夹的副本开始;继续并将其保存到我们的项目区域。

  2. 启动浏览器,然后前往https://speech/shop/。如果一切正常,我们应该会看到商店出现,类似于图 9-1 中的截图。

设定期望

在这一点上,我们有了一个(各种)功能商店——当然它不会是完美的,但它足以满足我们的需求。尽管设定正确的期望值很重要,但为了保持透明度,本章中有几点需要牢记:

  • 代码还没有投入生产——事实上,人们期望在商店展厅和结账流程中看到的许多功能还没有出现。这一章不是关于建立购物车,而是建立如何通过使用口头命令使它们可用。正是因为这个原因,我们将更多地关注这样做的技术,而不是商店本身。

  • 由于篇幅原因,我将重点介绍从画廊选择产品并进行(模拟)购买的关键部分——有人可能会说搜索产品也是必不可少的,但如果我们不能将它添加到我们的购物篮中,那就没有任何好处了!关键是,我们网站的“支持语音”的原则对网站的大部分都是一样的,所以我们总是可以修改现有的代码来为其他领域工作。

解决了这个问题,让我们把注意力转向我知道你们都在等待的一点——更新我们的演示!别担心。它只有几行,但是在我们开始之前,让我们更详细地看看更新我们的演示所涉及的步骤。

添加语音功能

好了,关键时刻到了!嗯,也许这有点太俗气了,因为我们店里的产品是饼干,但我离题了…总之,回到现实。

在添加语音功能时,我们可以分阶段进行;以下是我们需要遵循的步骤:

  • 为我们的麦克风和响应添加标记和样式。

  • 调整产品的价格。

  • 添加脚本功能以使其可操作。

  • 添加基本样式,使我们的演示看起来像样。

我们流程的第一步是添加控制麦克风并在屏幕上呈现任何响应的标记——一旦进入,我们就可以测试它以确认我们的网站正在接收语音,并且一旦我们的产品标记和脚本就绪,我们就能够对其采取行动。让我们深入研究一下如何添加标记,这样我们就可以开始看到我们的语音能力正在形成。

为我们的麦克风插入标记

更新演示的第一部分非常简单——我们首先需要为麦克风和各种消息或响应添加标记,然后添加可视指示器,以便客户知道使用麦克风时要说什么。

在第一部分中没有什么特别复杂的东西,尽管数据标签有一个有趣的用法;演示结束后,我们将探究使用它们的原因。让我们首先继续更新或添加相关的标记到我们的演示中。

Adding Speech Part 1: The Microphone Markup

要添加我们的标记,请按照下列步骤操作:

  1. 我们所有的修改都在作为下载一部分的商店文件夹中的index.html文件中——继续并在您常用的文本编辑器中打开它。

  2. 接下来,查找<div id="sidebar" ....块,并在它的结束</div>元素之前添加这个标记。我们最终应该得到这样的结果:

      <button id="microphone">
        <i class="fa fa-microphone"></i> Click and talk to me!
      </button>
      <div class="response">
        <span class="output_log"></span>
      </div>
      <p class="output">You said: <strong class="output_result">
    </strong></p>
      <span class="voice">Spoken voice: US English</span>
    </div>
    
    
  3. 继续保存文件,然后最小化它,我们将需要在下一个练习中恢复它。

  4. 接下来,我们需要为麦克风按钮和响应文本添加一些基本样式。为此,打开styles.css文件,一直滚动到底部。

  5. 在底部,添加以下样式规则:

    /* SPEECH RECOGNITION --------------------------- */
    .speechterm { padding-left: 15px; font-style: italic; }
    i.fa.fa-microphone { padding-right: 10px; }
    p.output { padding: 10px 0; }
    #microphone { margin-top: 20px; }
    #microphone:hover { background-color: darkgrey; }
    
    
  6. 此时,保存您的工作,并关闭 styles.css 文件–我们已经添加了本演示所需的所有内容。

  7. We can now preview the results of our work – for this, browse to https://speech/shop/. If all is well, we should see something akin to the screenshot shown in Figure 9-2.

    img/490753_1_En_9_Fig2_HTML.jpg

    图 9-2

    更新的商店,增加了麦克风选项

在这个阶段,我们还没有真正做出任何重大的改变——我们现在有了麦克风按钮的基础,以及用于用户各种响应或 API 返回消息的空间。我们将在下一个练习结束时进行,而不是在此时探索代码更改。所以,事不宜迟,让我们快速前进,更详细地探索需要更新哪些内容来支持 API 图库中的每个产品。

改变我们的产品价格

插入麦克风标记后,我们现在可以将注意力转向“启用”每个 cookie 以用于我们的语音识别功能。

我说“启用”是因为没有更好的方式来表达它,但我们所做的只是调整我们的标记,使我们的代码更容易识别并向我们的篮子中添加正确的 cookie。相信我——现在可能没有意义,但一旦我们完成练习,一切都会变得清晰!记住这一点,让我们开始更新标记。

Adding Speech Part 2: Updating Product Markup

要更新我们的产品标记,请按照下列步骤操作:

img/490753_1_En_9_Fig3_HTML.jpg

图 9-3

“我们现在可以和我们的饼干说话了……”

  1. 第一阶段是恢复到我们在之前的练习中打开的index.html文件——演示的这一部分的所有更改都在这个文件中。

  2. 第一个变化是更新 Cherry bake well cookie——查找add_to_cart元素,并按照指示添加数据标签:

    <div class="add_to_cart" data-product="cherry bakewell">Add to cart</div>
    
    
  3. Next, scroll down a couple of lines and insert this markup immediately below the <span class="product_price"... line:

      <span class="product_price">$0.50</span>
      <span class="speechterm"><i class="fa fa-microphone"></i>"Add a cherry"</span>
    
    

    这将为 cookie 添加一个麦克风和合适的文本,这样我们的客户就可以知道如何口头请求。

  4. 我们需要对剩下的三种产品重复步骤 2 和 3——对于下一种饼干(黑巧克力),按照指示添加数据标签:

    <div class="add_to_cart" data-product="dark chocolate">Add to cart</div>
    
    
  5. 在 cookie 的product_price标记的正下方,添加这行代码——这将为我们的语音识别功能“启用”黑巧克力 cookie:

    <span class="speechterm"><i class="fa fa-microphone"></i>"Add a dark"</span>
    
    
  6. 我们还需要对树莓和白巧克力曲奇进行类似的修改——为此,添加高亮显示的代码:

    <div class="add_to_cart" data-product="raspberry">Add to cart</div>
    
    
  7. 与之前的 cookie 类似,我们也需要添加麦克风标记,为此,在该 cookie 的 product_price 行的正下方添加以下行:

    <span class="speechterm"><i class="fa fa-microphone"></i>"Add a raspberry"</span>
    
    
  8. 最后,但绝不是最不重要的,继续添加数据标签,如太妃糖 cookie 所示:

    <div class="add_to_cart" data-product="toffee">Add to cart</div>
    
    
  9. 对于这个产品,我们还需要做一个修改——在它的 product_price 加价行的正下方,添加以下代码:

    <span class="speechterm"><i class="fa fa-microphone"></i>"Add a toffee"</span>
    
    
  10. 继续保存文件-我们现在预览结果!为此,浏览至https://speech/shop;如果一切正常,我们应该看到所有四个 cookies 现在都有一个视觉指示器,指示在使用我们的麦克风时需要什么(图 9-3 )。

嗯,光是看着那块饼干就让我觉得很饿。咯咯!暂时把食物的想法放在一边,我们添加到代码中的变化可能看起来有点不寻常,但正如所承诺的,疯狂中有方法!在我解释更多之前,让我们更详细地看一下代码。

剖析代码

我们使用的大部分标记非常相似——我们所做的改变分为两个阵营:第一个是在麦克风按钮周围添加标记,第二个是为每个产品调整我们的标记。

第一个块将一个标准的<button>元素添加到我们的标记中,加上一个 div 元素和两个 spans 后者用于显示来自 API 的响应(例如任何错误)、来自用户的响应以及 API 使用的声音的指示。接着,我们在每个 cookie 中添加了一个data-product标签,以及一个使用麦克风时要求什么的可视化指示,以。speechterm斯潘。

现在,正如所承诺的,使用数据标签是有原因的。正如您将在后面的代码中看到的,我们使用一个通用的.add_to_cart类来触发将任何 cookie 添加到篮子中。原则上,这似乎是一个明智的想法,对不对?

错误——如果我们单独使用它,我们会有一个问题:它会一次添加同一个 cookie 的四个实例!原因在于 jQuery 的工作方式——.add_to_cart类将应用于所有四个产品,因为我们对每个产品使用相同的类。

为了解决这个问题,我们添加了数据标签,这样我们就有了对每个 cookie 的特定引用。然而关键在于如何触发添加项目的调用——我们使用绑定到每个add_to_cart div 的数据标签属性。动态引用意味着我们可以将一个add_to_cart div 的实例传递给事件处理程序:

$('[data-product="' + cookieChosen + '"]').trigger("click");

如果它现在不完全有意义,请不要担心——当我们检查为使我们的演示工作而添加的代码时,我们将再次讨论它!

添加脚本功能

继续,我们清单上的下一个任务是添加我们需要操作语音特性的代码。这其中的大部分你会在之前的演示中看到,所以现在应该不会完全陌生;它的关键在于我们如何将我们的语音响应转换成我们的代码可以识别的东西,并用来添加到适当的 cookie 中。为了让你感受一下它的样子,你可以在图 9-4 中看到完整的文章。

img/490753_1_En_9_Fig4_HTML.jpg

图 9-4

一个曲奇用我们的声音加入了篮子

记住这一点,让我们把注意力转向设置我们的演示。

Adding Speech Part 3: Making IT Work

让我们开始添加演示脚本:

  1. 在本练习中,我们所做的所有更改都将保存在script.js文件中,所以请在您常用的文本编辑器中打开它。

  2. 向下滚动到底部,直到你看到这些评论:

    /* SPEECH RECOGNITION ------------------------------ */
    /* Code to be added here */
    
    
  3. 这就是我们要添加代码的地方——有相当多的部分要添加,所以我们将一个一个地进行。

  4. 第一块是添加一些变量或对象声明,并为一些语音识别 API 设置值。继续留一个空行,然后在上一步的第二个注释下面添加以下代码:

    const log = document.querySelector(".output_log");
    const output = document.querySelector(".output_result");
    
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognition();
    
    recognition.interimResults = false;
    recognition.maxAlternatives = 1;
    recognition.continuous = true;
    
    
  5. 我们需要添加的第一个事件处理程序负责启用我们的麦克风——这里我们设置了几个属性来配置语音识别 API 的实例。添加以下代码,在上一步的代码后留下一个空行:

    document.querySelector("#microphone").addEventListener("click", () => {
        let recogLang = "en-GB";
        recognition.lang = recogLang.value;
        recognition.start();
      });
    
    
  6. 我们添加的下一个处理程序在检测到语音时触发——包括背景噪音!为此,添加以下代码,在上一步的代码后留下一个空行:

      recognition.addEventListener("speechstart", () => {
        log.textContent = "Speech has been detected.";
      });
    
    
  7. 下一步是魔法开始发生的地方——在这里,我们检测说了什么,解析内容,并决定最终采取什么行动。这是一段相当长的代码,所以我们将把它分成几个部分——要设置基本的处理程序,继续添加这段代码,在上一步的代码后留下一个空行:

      recognition.addEventListener("result", (e) => {
        log.textContent = "Result has been detected.";
    
        let last = e.results.length - 1;
        let text = e.results[last][0].transcript;
    
        output.textContent = text;
    
        // ACTION CODE HERE
    
        log.textContent = "Confidence: " + (e.results[0][0].confidence * 100).toFixed(2) + "%";
      });
    
    
  8. 现在有了基本的处理程序,我们可以开始扩展它了。在前面的步骤中查找这行代码——// ACTION CODE HERE——然后用这个条件块替换它:

        // SR - "Add an X to the basket"
        if (text.search(/\badd\b/)) {
          var request = text.split(" ").pop();
    
          console.log(request);
          var cookieChosen;
    
          if (request == "cherry") {
            cookieChosen = "cherry bakewell";
          }
    
          if (request == "dark") {
            cookieChosen = "dark chocolate";
          }
    
          if (request == "raspberry") {
            cookieChosen = "raspberry";
          }
    
          if (request == "toffee") {
            cookieChosen = "toffee";
          }
    
          $('[data-product="' + cookieChosen + '"]').trigger("click");
    
    
  9. 对于这个条件块的第三和最后一部分,继续在上一步的data-product赋值下面添加以下行,中间留一个空行:

          /* ----------------- */
    
          /* click on checkout */
          if (text.indexOf("check") != -1) {
            $("#checkout").trigger("click");
          }
    
          /* ----------------- */
    
          /* enter credit card number */
          if (text.indexOf("credit card") != -1) {
            $("#cardnumber").val("4111111111111111");
          }
    
          /* ----------------- */
          /* enter card date   */
          if (text.indexOf("expiry") != -1) {
            $("#cardexpiration").val("10/2022");
          }
    
          /* ----------------- */
          /* enter CVV number  */
          if (text.indexOf("security") != -1) {
            $("#cardcvc").val("672");
          }
    
          /* ----------------- */
          /* click on purchase */
          if (text.indexOf("purchase") != -1) {
            $("div.card-form > button > span").trigger("click");
          }
        }
    
    
  10. 我们还剩下三个事件处理程序,与上一个事件处理程序相比,它们看起来很简单!当 API 检测到听不到更多的语音时,下一个要添加的将被触发:

```html
  recognition.addEventListener("speechend", () => {
    recognition.stop();
  });

```
  1. 下一个事件处理程序也在没有检测到更多语音时触发,但是有一个微妙的区别——这个事件处理程序在 API 关闭时触发。继续添加下面的代码,在前面的事件处理程序后留下一个空行:
```html
  recognition.onspeechend = function() {
    log.textContent = 'You were quiet for a while so voice recognition turned itself off.';
    console.log("off");
  };

```
  1. 最后但绝不是最不重要的,我们需要实现一些基本的错误处理——现在,我们只是在屏幕上呈现 API 生成的任何错误。继续添加以下代码:
```html
  recognition.addEventListener("error", e => {
    if (e.error == "no-speech") {
      output.textContent = "Error: no speech detected";
    } else {
      output.textContent = "Error: " + e.error;
    }
  });

```
  1. 至此,我们完成了对文件的编辑。继续保存它,然后关闭文件。我们现在可以预览我们的结果。浏览到https://speech/shop/,点击麦克风按钮,然后尝试对着麦克风说“加一颗樱桃”。如果一切正常,我们应该会看到类似于本练习开始时显示的屏幕截图。

我们现在有了一个添加到购物车的工作流程——我们应该能够将四个 cookies 中的任何一个添加到我们的购物车中。我们已经看到它与 Cherry Bakewell 一起工作(如图 9-4 所示),但对一些人来说,这个故事可能有刺!这是我们以前见过的事情(还记得 Alexa 克隆演示吗?)–在我们探索它是什么之前,让我们更详细地浏览一下代码,因为与我们在早期演示中使用的代码相比,有几个关键的变化。

打破我们的代码

在本章的过程中,我们已经讲述了相当多的代码,作为添加语音功能的一部分——其中大部分现在应该看起来很熟悉了,特别是因为我们已经使用了本书早期演示中的部分内容。也就是说,更详细地浏览我们添加的代码仍然是一个好主意——有一个关键部分我们需要知道,所以让我们深入并更详细地看一看。

我们通过定义几个常数开始了语音识别部分——我们使用.output_log显示来自 API 的消息,使用.output_result显示来自客户的转录文本。然后,我们创建一个语音识别 API 的新实例;这使用本机实现或供应商前缀版本,具体取决于所使用的浏览器。除此之外,我们还设置了三个属性——interimResults为 false(因此我们只得到最终结果),maxAlternatives为 1(我们专注于获得原始的、检测到的单词,而不是可能的替代词),以及continuous为 true(因此语音识别 API 不会太快关闭)。

然后我们有了一系列事件处理程序。第一种允许客户在浏览器中启用他们的麦克风;这将在启动识别服务之前,将语言设置为美国英语("en-US")。接下来是speechstart事件处理程序,一旦检测到任何语音文本(不一定是来自客户的!).

接下来是这个演示的关键部分——这是一个扩展的结果事件处理程序。在将口述文本的内容分配给 text 变量之前,它首先检测服务是否识别了口述文本。然后,我们使用pop()方法分割这个变量的内容,并获取最后一个条目。这很重要,因为它存储在cookieChosen变量中;我们用它来触发右边的添加到购物车按钮。

值得暂停一会儿,因为浏览代码不会发现任何可以被称为纯粹的添加到购物车按钮处理程序的东西!我们在本章的前面提到了这一点,这是有充分理由的——我们可以创建一些分配一个唯一的 ID 或类的东西,但是正确地做到这一点是很棘手的。我们最终可能会有很多处理程序,或者一个非常丑陋的一刀切的方法。

相反,我们使用了一个data-product标签——我们动态地将从cookieChosen保存的值连接到触发点击处理程序的事件中,该处理程序触发右按钮。这是可行的,如果你仔细观察我们的标记,你会看到数据标签与add_to_cart div 相对,如图 9-5 所示。

img/490753_1_En_9_Fig5_HTML.jpg

图 9-5

标记中使用的数据标签的示例

对于该事件处理程序的其余部分,我们简单地使用了一组条件检查——如果我们转录的文本结果包含某些词,如cardsecurity,expiry,我们就在适当的字段中输入测试值。事件处理程序的最后一步是添加一个提交付款的触发机制——我们在演示中模拟了这一点,但如果这是在生产中使用的话,付款将在这一点上进行。

你会注意到在这个演示中使用了伪造的信用卡信息。这是而不是推荐的做法;它们是为了说明这个演示的一个缺陷。我们将在本章的后面讨论这对我们意味着什么。

代码的剩余部分包含我们在之前的演示中使用的事件处理程序——我们有两个speechend处理程序和一个处理程序来处理我们演示中的基本错误。不过,有两个speechend处理程序是有原因的:第一个(speechend)在服务检测到我们已经停止说话时触发(因此关闭它自己);一旦发生这种情况,第二个(onspeechend)就会启动,并在屏幕上为我们的客户显示适当的信息。

好吧,我们继续。我们已经构建了一个基本的购物车,它使用自定义的结账流程。有一个相对较新的 API,旨在跨所有浏览器标准化结帐表单。问题是,我们能把同样的原理应用到语音上吗?在一个理想的世界里,不应该有任何不同,除了这一次我们可能没有那么幸运。为了理解我的意思,让我们看看这种变化会如何影响我们的战略,以及我们是否需要重新考虑我们的计划。

退房的另一种方法

从我记事起(这要追溯到 20 多年前!),任何通过互联网购买商品的人无疑都经历过定制的或从诸如光化学等商业产品开发的结账过程。这(当时)没什么问题,但现在许多都被视为笨重且难以维护——这通常是整个采购流程中最容易遗漏的地方!

在过去的几年中,W3C 和浏览器供应商一直在开发一种可以直接在浏览器中使用的标准化 API——现在被称为支付请求 API。虽然它在每个浏览器中看起来都不一样,但它提供了一个标准框架,支付提供商可以在其中插入自己的支付处理程序,而不必担心 UI 或用户体验。

在我们的下一个练习中,我们将利用这个 API 来生成一个简单的付款结账——它不会有 API 附带的所有功能,但至少允许我们运行结账流程。作为一个品尝者,图 9-6 展示了一旦我们实现了请求支付 API 所需的更改,我们的演示将会是什么样子。

img/490753_1_En_9_Fig6_HTML.jpg

图 9-6

我们的另一种支付方式

现在我们已经看到了它的样子,让我们开始修改我们的演示。

Using the Payment Request API

对于这个演示,我建议从上一个演示中复制一份完整的shop文件夹,然后保存为shop-alternative;我们将以此为基础,用付款申请 API 取代人工结账。

如果您遇到困难,在本书附带的代码下载中有这个演示的完整版本;它在车间-替代-完成版本文件夹中。

要完成交换,请执行以下步骤:

  1. 我们编辑完了。继续保存 index.html 和 script.js 它们现在可以关闭了。

  2. 现在,我们可以预览更改的结果了——启动浏览器,然后导航到https://speech/shop-alternative。如果一切正常,我们应该会看到类似于图 9-6 所示的视图,这里显示的是我们的可选结帐表单。

  3. 第一个任务是在我们的标记文件中去掉付款部分——为此,寻找以<!--- PAYMENT....开始的行,然后从这里向下移动到<div id="header">之前的结束</div>标记。

  4. 接下来,切换到 script.js,这样我们可以删除 modal,因为不再需要它了。查找以/* MODAL ----开始的行,然后删除它,并将代码向下移动到/* PAYMENT FORM...行之前的结束});

  5. 我们还需要删除原来的支付冻结–查找并删除从/* PAYMENT FORM ---...开始的冻结–删除到/* SPEECH RECOGNITION ---...之前。

  6. 我们有一个新的代码块要插入,作为我们的支付处理程序的替换——继续插入这个代码,作为我们的处理程序的第一部分:

      /* PAYMENT FORM USING PAYMENT REQUEST API---------- */
      const methodData = [{
        supportedMethods: 'basic-card',
        data: {
          supportedNetworks: ['visa', 'mastercard', 'amex']
        }
      }];
    
    
  7. 付款请求的真正内容来自下一个事件处理程序——保留一行空白,然后在methodData常量下面添加以下代码:

      document.getElementById('checkout').onclick = function (e) {
        if(window.PaymentRequest) {
          let subtotal = Number(countCookies * 0.50);
          let tax = 1.99;
          let shipping = 2.99;
    
          const details = {
            total: {
              label: 'Total due',
              amount: { currency: 'USD', value: (subtotal + tax + shipping).toFixed(2) }
            },
            displayItems: [{
              label: 'Sub-total',
              amount: { currency: 'USD', value: subtotal.toFixed(2) }
            }, {
              label: 'Delivery',
              amount: { currency: 'USD', value: 2.99 }
            }, {
              label: 'Sales Tax',
              amount: { currency: 'USD', value: tax.toFixed(2) }
            }]
          };
    
        const options = { requestPayerEmail: true };
        const request = new PaymentRequest(methodData, details, options);
    
        //Show the Native UI
        request
          .show()
          .then(function(result) {
            result.complete('success')
              .then(console.log(JSON.stringify(result)));
          }).catch(function(err) {
            console.error(err.message);
          });
        } else {
          // Fallback to traditional checkout
        }
      };
    
    
  8. We’re almost done. There is one last block of code to remove. In the Speech Recognition block , look for and remove this code, as it is no longer needed:

          /* ----------------- */
          /* enter credit card number */
          if (text.indexOf("credit card") != -1) {
            $("#cardnumber").val("4111111111111111");
          }
    
          /* ----------------- */
          /* enter card date   */
          if (text.indexOf("expiry") != -1) {
            $("#cardexpiration").val("10/2022");
          }
    
          /* ----------------- */
          /* enter CVV number  */
          if (text.indexOf("security") != -1) {
            $("#cardcvc").val("672");
          }
    
    

    这可能看起来有点奇怪,但删除它有一个很好的理由——一切将很快揭晓。

在这个练习的过程中,我们已经剥离了原始的付款表单,并用付款请求 API 中的一个实例替换了它。这看起来没问题,但是请注意,作为步骤 6 的一部分,我们是如何删除在原始版本中完成的一些检查的?我暗示这在当时看起来很奇怪,但这是有很好的理由的——为了解释和更多,让我们深入了解一下更详细的变化。

破解密码

在这一章的过程中,我们做了一些彻底的改变。我们首先从标记文件中删除了原始的支付代码,以及模态代码。这两个都不是必需的,因为表单将由浏览器中的支付请求 API 提供。

接下来,我们做了类似的事情,但是在script.js文件中——我们驱逐了那里的整个支付块,因为一旦我们输入新的支付请求代码,原始代码将是多余的。

我们演示的真正关键是新的支付请求 API 代码;我们从声明一个methodData常量开始,它定义了我们的浏览器允许的可接受的支付方式。我们坚持使用basic-card,这是现成可用的方法;这是一种不安全的方法,不应该在实践中使用,但只用于测试目的是可以的。

然后,我们添加了一个事件处理程序,只要单击#checkout div 就会触发该事件处理程序;这可以通过鼠标或口头进行,就像我们在本演示的原始版本中所做的那样。这首先是对window.PaymentRequest的检查,看看我们的浏览器是否支持它——假设它支持,我们为subtotaltax,shipping定义一组变量(所有其他变量已经在代码的其他地方声明了)。

在下一个常量(details)中,我们定义了一个对象,该对象包含要在表单中显示的标签文本和金额,然后将付款请求 API 的实例初始化为 Request。这就是所谓的承诺;我们首先show()表单,然后要么触发在控制台显示结果的.complete()方法,要么通过catch()陷阱抛出一个错误,以声明我们的支付过程出现了问题。

如果你有兴趣了解更多关于支付请求 API 的知识,那么你可以参考我的书,由 Apress 出版的《用支付请求 API 结账》。

减少功能:注意

在我们结束这个演示之前,有一些事情我们应该考虑一下——还记得我说过我们需要从代码中删除一大块条件检查吗?你可以在图 9-7 中看到我的意思,这里我们已经删除了用于检查信用卡号、有效期和 CVV 安全号码的原始 if 语句。

img/490753_1_En_9_Fig7_HTML.jpg

图 9-7

我们更新的演示,无条件检查

我们不能包含这些支票的原因很简单——付款申请 API 表单内置于浏览器中,因此不能暴露出来让我们与之交互。这意味着,虽然我们篮子的其余部分可以通过语音控制,但我们无法控制结账表单本身!

在某些方面,这可以被视为降低了网站的可访问性——因此,这意味着我们必须提供一个后备或设置它,以便支付请求 API 可以在标准支付结帐过程中启用。伟大的事情是,虽然 API 仍然处于不断变化的状态;虽然它现在使用起来足够稳定,但在它获得官方地位之前,事情可能会发生变化,所以谁知道呢?对语音 API 的支持可以得到很好的改进!

我们继续吧。我们的演示已经完成,但这并不是故事的结尾!不幸的是,这个故事有一点刺痛,这将影响我们的商店。为了理解其中的原因,让我们深入了解一下在给电子商务网站添加语音功能时可能会遇到的一些陷阱。

探索我们解决方案的缺陷

是时候坦白了。是的,我听到了。你可能在想,啊哦…我想知道他指的是什么?小心谨慎是对的,但不要担心。事情并不像看起来那么糟!语音 API 仍然非常新,还没有达到 W3C 推荐的候选标准。这并不意味着我们不能使用它们,而是我们需要保持一定的谨慎。让我们更详细地看看这些陷阱:

  • 第一个是你可能会对使用的一些视觉标签感到惊讶——注意它们都没有给出全名,而是像“加一颗樱桃”这样的东西?有一个很好的理由-如果我们使用全名,我们会发现不是所有的 cookies 都可以添加!原因是我们在 Alexa clone 演示中提到的:API 很难识别某些单词,特别是在不同音节之间几乎没有差异的情况下。这不是可以固定的,但是可以微调;我们需要小心选择哪些词来选择我们的产品,只有测试才能确定使用的最佳组合。

  • 如果您尝试点击麦克风按钮来启用语音,然后口头要求网站添加一个产品,您很可能会发现,对于第一个产品,您必须这样做两次。API 需要一段时间才能完全激活,因此它的潜在客户会在麦克风完全准备好之前尝试添加产品。为了解决这个问题,我们可以实现一个Promise()来使麦克风提示只在特定时间后出现——这是一个微小的变化,但绝对值得一做!

  • 正如我们在支付请求 API 中看到的那样,我们的能力有限——我们将能够使用语音显示表单,但从那以后,就必须打字或点击按钮。这确实意味着(至少目前)这可能是一个不太有吸引力的选择,并且可能只对那些不想使用语音服务的人开放。这并不好,因为支付请求 API 旨在简化流程;然而,由于这个 API 还没有成为主流,我们将不得不使用现有的 API!

  • 我们在代码中设置的属性是我们需要仔细考虑的——在我们生活的这个多元文化世界中,不是每个人都能说英语,更不用说美国英语了,这是语音 API 的默认值!虽然设定一个值很容易,但是设定正确的值就比较难了——我们是根据一个网站的固定语言来设定,还是根据我们的客户来自哪个国家来设定?这很大程度上取决于你如何运营你的网站——是一个单一的多语言网站(不利于搜索引擎优化)还是多个网站,具有相同的品牌但使用不同的语言?

  • 你会从演示中注意到我们硬编码了信用卡的详细信息——实际上,我们这样做是为了提供提交结账表单的流程,而不是以为借口将任何值硬编码到我们的解决方案中!我们可以使用现有的 API 作为识别每个输入数字的基础,但是要做到这一点,我们可能必须说“数字一”,而不仅仅是“一”。这是一个平衡可靠性和不激怒客户的愿望的问题,因为输入任何细节都需要太长时间!

希望这能给我们一些思考。它不应该阻止我们使用 API 我们可以解决这些限制。它确实强调了确保我们仔细考虑使用 API 的更广泛含义的重要性,并且我们将这一点考虑到在我们的解决方案中使用这些 API 的任何开发工作中。

好吧,我们继续。既然我们已经构建了基本的演示并为其添加了语音功能,那么是时候考虑如何扩展我们的演示了。我想到了一些事情,可以帮助你开始;让我们深入研究一下,更详细地看看它们。

更进一步

如果有人问我我们如何进一步推进我们的项目,我想我通常的回答会是“世界是你的牡蛎”——因为你可以去你想去的任何地方,只要你能让它工作起来!似乎有点讽刺的是,这个短语并不是来自于一个技术起源,而是可以追溯到莎士比亚的温莎的风流娘儿们,已经有 400 多年的历史了!但是我跑题了…

反正回到现实,一个人能做什么?嗯,除了添加人们期望在购物篮和结账流程中看到的剩余功能之外,我们还可以查看和实现一些东西。让我们更详细地看看这些想法:

  • 其中一个明显的优势是更好的语言支持——还记得我们在演示中如何将 recognition.lang 设置为“en-US”吗?嗯,我们可以研究实现一个你经常在网站上看到的语言选择器的可能性,以及为页面设置语言;它可以用于同时设置合适的语言值。例如,对于位于爱沙尼亚等国家的站点,他们说芬兰语、俄语和英语以及其他语言,您可以设置诸如fi-FIru-RUen-USet-EE(用于爱沙尼亚)之类的值。这将允许我们的语音 API 更好地识别基于该国方言的文本。

  • 继续语言支持的主题,我们将如何着手本地化我们的网站,接受其他语言的请求?一种解决方案可能是使用 JSON 来提供每个触发短语的本地语言等价物(例如我们的演示中每个麦克风符号所使用的那些)。我们可以调用其中的每一个,而不是将它们硬编码到我们的演示中。

  • 我们已经使用语音识别 API 来添加产品或触发结账过程——使用语音合成 API 来给出已完成操作的口头指示怎么样?我们没有任何东西来指示每个动作发生的时间(除了在屏幕上看到它)——如果有东西来告诉那些有视力障碍的人一个动作已经完成,那将会很有帮助。

  • 完全避免使用信用卡,实施 Google Pay 等更现代的支付方式如何?有许多不同的公司提供这种支持,如 Braintree 你可以在 https://developers.braintreepayments.com/guides/google-pay/client-side/javascript/v3 看到他们如何使用 JavaScript 设置支付的例子。这里的想法是,如果我们可以实现一些东西(当然,假设您有一个合适的帐户),那么提供一个链接来发起支付请求应该很容易。

这只是让你开始的几个想法——我相信你能想出更多,但是正如本节的导语所说,世界真的是你的牡蛎!这完全是一个思考的问题,你可以在你的网站中使用语音功能,并给予适当的考虑,它是否真的会帮助客户,或者只是被视为一个噱头,客户会很高兴没有它!

摘要

Web Speech API 是一个实现起来很简单的工具,但是对于使站点更容易访问来说是非常棒的——尽管它仍处于开发阶段!在本章的过程中,我们已经探索了如何在一个基本的购物车和结帐过程中使用它;让我们复习一下我们所学的内容。

我们以介绍这一章和设置场景开始。然后,在介绍用于构建最终解决方案的工具之前,我们讨论了本章的内容。同时,我们简要地讨论了设定期望值,因为我们不可能涵盖购买过程的每一个部分,而是将重点放在本章的核心要素上。

接下来,我们深入研究添加代码,使我们的语音能力滴答作响;在为我们的演示添加脚本之前,我们研究了修改标记所需的更改。然后,我们继续探索使用相对较新的付款请求 API 的另一种结账流程,看看这会如何影响我们在演示中使用语音。

然后,在探索一些我们可以遵循的途径来帮助扩展和扩充我们的演示以供生产使用之前,我们通过查看在结帐过程的上下文中使用 Speech APIs 时需要注意的一些陷阱来结束这一章。

唷!我们已经到了这本书的结尾。多好的旅程啊!我希望你能像我写这本书一样喜欢在这个项目中工作,并且你现在对如何在你未来的项目中使用 Speech APIs 有了更好的理解。

posted @ 2024-08-19 15:43  绝不原创的飞龙  阅读(20)  评论(0编辑  收藏  举报