首页 前端知识 HTML5 语音 API 入门指南(二)

HTML5 语音 API 入门指南(二)

2024-08-23 20:08:35 前端知识 前端哥 300 162 我要收藏

原文:Introducing the HTML5 Web Speech API

协议:CC BY-NC-SA 4.0

五、项目:留下评论反馈

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

不管你留下了什么样的反馈,你都有可能必须输入你的评论;如果你能用你的声音做到这一点会怎么样?是的,虽然看起来很新奇,但这是展示使用语音 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 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 中看到这个功能的截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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来区分原始的源演示。

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 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);
    }
    
    

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2

安阳要调用的第一个函数

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

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

解决造型问题

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

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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文件开始——我们已经有了一个占位符函数,所以继续寻找这行代码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 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 个月里收到的几封电子邮件,寻找我们如何能够扩展我们所能提供的内容的想法。这里列出了一些想法,让你的创意源源不断:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

如果你想知道倍半句是什么意思,它在这个上下文中有点讽刺意味。它可以表示有很多音节,在这里非常贴切!

  • 一个更复杂的功能是尝试将产品添加到亚马逊的网络购物车中——这确实涉及到注册他们的一个 APIs 如果你很好奇,可以看看 https://docs.aws.amazon.com/AWSECommerceService/latest/DG/AddingItemstoaCart.html 上的文档了解更多细节。

希望这能让你有所思考——我们真的只是被我们的想象力和我们想走多远所限制!做到这一点的关键是尽可能保持模块化——如果我们考虑更改 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变量——快速搜索文件的其余部分不会显示对它的任何其他引用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示的截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2

我们完成的演示,准备接受输入

要操作它,我建议使用这种方法,您的回答用粗体文本表示:

  • 单击开始搜索。然后等待最初的欢迎信息出现并被朗读。

  • 出现提示时,说搜索餐馆

  • 根据对菜肴类型的要求,说我更喜欢本地菜

  • 当提示价格范围时,说中档

  • 当机器人确认它已经找到了一些结果,说是的,请

这听起来像是我们将测试引向一个已知的场景,而没有预料到用户可能会说什么,但这是故意的。这有利于展示演示在技术上运行得有多好,以及用户提出的问题是否自然——在这种情况下,我怀疑还有改进的空间!

暂时把这个放在一边,我们可以开始看看图 7-3 中的演示是如何工作的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-3

我们实际演示的第一部分…

如果我们逐步完成每一步,我们可以看到最后的结果,如图 7-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-6

正在使用在线 JSON 编辑器

如果您使用 JSON Editor Online,您可以单击每个键值(在突出显示的示例中位于冒号的左侧)并获得文件中该值的完整路径。在突出显示的示例中,我们将以这段代码结束,这段代码可以放入我们的演示中:

$("<p>").text(
  "Latitude: " + returnedData[i].restaurant.timings
),

还有一大堆其他值可以尝试,所以请随意浏览文件并决定尝试哪一个!

摘要

哇,那是一些怪物项目!语音 API 是可以在各种情况下使用的技术之一,比如查找餐馆的详细信息。我们已经在这一章中介绍了一些有用的技术,所以让我们停下来回顾一下我们所学的内容。

我们首先设置场景、参数和业务逻辑,以便在我们的项目中使用,然后探索我们将如何构建我们的演示,并设置一些关于它的使用的期望。然后,我们开始为我们的项目设置初始标记,然后添加脚本的各个部分,例如说话的设备或查找餐馆细节。

接下来,我们测试了我们的演示,然后详细分析了我们的代码,以理解后者是如何工作的,并看到了与早期 Speech API 示例的相似之处。然后,我们总结了这一章,看看我们如何进一步发展——这探讨了一些关于改进我们现有代码的想法,以及添加新功能,作为将其开发成可以呈现在真实客户面前的东西的一部分。

好吧,我们继续。我们还有更多要讲的!请举手你们中有多少人使用在线音乐流媒体服务,比如 Deezer 或 Spotify?我敢打赌,你们中会有相当一部分人适用这一点:如果我们可以用声音来控制我们演奏音乐的方式,会怎么样?是的,你没听错。且听我说,且听下回分解。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/16643.html
标签
VKDoc
评论
发布的文章

HTML5 基本框架

2024-09-01 23:09:50

HTML5取消边框的方法

2024-09-01 23:09:50

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!