使用 Web Speech API

Web Speech API 提供了两类不同方向的函数——语音识别和语音合成 (也被称为文本转为语音,英语简写是 tts)——开启了有趣的新可用性和控制机制。这篇文章提供了这两个方向的简单介绍,并且都带有例子。

Speech recognition

Speech recognition(语音识别) 涉及三个过程:首先,需要设备的麦克风接收这段语音;其次,speech recognition service(语音识别服务器) 会根据一系列语法 (基本上,语法是你希望在具体的应用中能够识别出来的词汇) 来检查这段语音;最后,当一个单词或者短语被成功识别后,结果会以文本字符串的形式返回 (结果可以有多个),以及更多的行为可以设置被触发。

Web Speech API 有一个主要的控制接口——SpeechRecognition (en-US),外加一些如表示语法、表示结果等等亲密相关的接口。通常,设备都有可使用的默认语音识别系统,大部分现代操作系统使用这个语音识别系统来处理语音命令,比如 Mac OS X 上的 Dictation,iOS 上的 Siri,Win10 上的 Cortana,Android Speech 等等。

Demo

为了简单展示 Web speech recognition 的作用,我们写了一个 demo——Speech color changer。点击屏幕之后,说出 HTML 颜色关键字 (网页里罗列的单词就是),接下来应用的背景颜色就会变成你说的颜色。

The UI of an app titled Speech Color changer. It invites the user to tap the screen and say a color, and then it turns the background of the app that colour. In this case it has turned the background red.

为了跑这个 demo,可以 clone Github 仓库 (上面甩出的就是,或者directly download),可以在支持的移动端浏览器 (比如 Chrome) 导航到 live demo URL 直接观看 (亲测 desktop browser 也是可以的,不过只能是 Chrome),也可以通过 WebIDE 作为一个 app 加载到 Firefox OS(Firefox OS 使用 API 的权限问题见下文)。

Browser support

对于 Web Speech API speech recognition(语音识别) 的支持,在各浏览器中还不成熟,还在发展,现在主要的限制如下:

  • Firefox 桌面端和移动端在 Gecko 44+ 中都支持,并且是没有前缀的,它可以在about:config 中把 media.webspeech.recognition.enable 设置为 true 打开。权限设置/UI 还没有整理出来,所以权限还不能被用户使用,也就是不能用。不过很快会修复吧~
  • Firefox OS 2.5+ 也支持,但作为一个特权 API(privileged API) 需要权限,因此你需要在manifest.webapp (也可以通过 WebIDE 下载,或者使应用得到验证后在 Firefox Marketplace 可使用) 如下设置:
    json
    "permissions": {
      "audio-capture" : {
        "description" : "Audio capture"
      },
      "speech-recognition" : {
        "description" : "Speech recognition"
      }
    }
    
    json
    "type": "privileged"
    
  • Chrome 桌面端和 Android 端自 version 33 以来均支持,但是带有前缀,所以你需要使用带有前缀的版本,比如:webkitSpeechRecognition

HTML 和 CSS

对于这个应用来说,HTML 和 CSS 部分是无足轻重的。仅仅只有一个标题,一个介绍段落和一个 div 用来输出 check 的结果。

html
<h1>Speech color changer</h1>
<p>Tap/click then say a color to change the background color of the app.</p>
<div>
  <p class="output"><em>...diagnostic messages</em></p>
</div>

CSS 也只是提供了简单的响应式样式,跨设备看上去也是 ok 的。

JavaScript

JavaScript 部分会介绍更多细节。

浏览器支持

之前有说到过,Chrome 现在支持的是带有前缀的 speech recognition,因此在 code 开始部分得加些内容保证在需要前缀的 Chrome 和不需要前缀的像 Firefox 中,使用的 object 都是正确的。

js
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition;
var SpeechGrammarList = SpeechGrammarList || webkitSpeechGrammarList;
var SpeechRecognitionEvent =
  SpeechRecognitionEvent || webkitSpeechRecognitionEvent;

The grammar

这部分是我们的代码定义希望应用能够识别的语法。语法放在下面定义的变量grammar中:

js
var colors = [ 'aqua' , 'azure' , 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral' ... ];
var grammar = '#JSGF V1.0; grammar colors; public <color> = ' + colors.join(' | ') + ' ;'

语法格式使用的是 JSpeech Grammar FormatJSGF)——你可以在前面的链接中了解更多关于语法格式的规范。不过现在,让我们快速地浏览它:

  • 每一行用分号分隔,和 js 中一样
  • 第一行——#JSGF V1.0——说的是语法使用的格式和版本。这总是需要首先包括在内
  • 第二行表示我们想要识别的term 的类型 (这里就是colors)。public 声明这是一条公共规则,尖括号中的字符串定义需要识别term 的名字 (这里就是color),等号后面的是这个term 可以被识别和接受的具体值。得注意每一个值如何被一个管道符号分割开的
  • 你可以按照上面的结构,在多行中,想定义多少就定义多少terms ,也可以包括相当复杂的语法定义。对于我们这个简单的 demo,就把语法定义的简单些

将 grammer 插入 speech recognition

接下来是使用 SpeechRecognition() (en-US) 构造函数,定义一个 speech recognition 实例,控制对于这个应用的识别。还需要使用 SpeechGrammarList() (en-US) 构造函数,创立一个 speech grammer list 对象,包含我们的语法。

js
var recognition = new SpeechRecognition();
var speechRecognitionList = new SpeechGrammarList();

使用 SpeechGrammarList.addFromString() (en-US) 把语法添加到列表 (list),这个方法接收两个参数,第一个是我们想要添加的包含语法内容的字符串,第二个是对添加的这条语法的权重 (权重值范围是 0~1),权重其实是相对于其他语法,这一条语法的重要程度。添加到列表的语法就是可用的,并且是一个SpeechGrammar 实例。

js
speechRecognitionList.addFromString(grammar, 1);

我们然后通过设置 SpeechRecognition.grammars (en-US) 属性值,把我们的SpeechGrammarList (en-US) 添加到 speech recognition 实例。在继续往前走之前,我们还需要设置 recognition 实例其他的一些属性:

  • SpeechRecognition.lang (en-US) :设置识别的是什么语言。这个设定是良好的做好,因此墙裂推荐~
  • SpeechRecognition.interimResults (en-US) :定义 speech recognition 系统要不要返回临时结果 (interim results),还是只返回最终结果。对于这个简单 demo,只返回最终结果就够了。
  • SpeechRecognition.maxAlternatives (en-US) :定义每次结果返回的可能匹配值的数量。这有时有用,比如要的结果不明确,你想要用一个列表展示所有可能值,让用户自己从中选择一个正确的。但这里这个简单 demo 就不用了,因此我们设置为 1(1 也就是默认值)。
js
recognition.grammars = speechRecognitionList;
//recognition.continuous = false;
recognition.lang = "en-US";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

备注: SpeechRecognition.continuous (en-US) 控制的是每一次允许多个结果被捕捉 (比如在这个 demo 中连着说两个颜色关键字,都可以被捕捉),或者一次只能识别一个结果。代码中它被注释掉的原因是,在 Gecko 中它还不被支持,所以如果把它加进去会破坏这个应用。你可以在收到第一个结果后简单地停止识别,从而得到类似的结果,稍后将会看到。

开始语音识别

在获取输出的<div> 和 html 元素引用之后 (这些我们可以用来待会输出语音识别诊断的结果,更新应用的背景颜色),我们添加了一个onclick 事件处理,作用是当屏幕被点击后,语音识别服务将开启——这通过调用 SpeechRecognition.start() (en-US) 实现。forEach() 方法内部的工作是,为每个颜色关键字添加一个这个颜色的背景色,这样就直观知道了这个颜色关键字指向什么颜色。

js
var diagnostic = document.querySelector(".output");
var bg = document.querySelector("html");
var hints = document.querySelector(".hints");

var colorHTML = "";
colors.forEach(function (v, i, a) {
  console.log(v, i);
  colorHTML += '<span style="background-color:' + v + ';"> ' + v + " </span>";
});
hints.innerHTML =
  "Tap/click then say a color to change the background color of the app. Try " +
  colorHTML +
  ".";

document.body.onclick = function () {
  recognition.start();
  console.log("Ready to receive a color command.");
};

接收、处理结果

一旦语音识别开始,有许多 event handlers 可以用于做结果返回的后续操作,除了识别的结果外还有些零碎的相关信息可供操作 (可查看 SpeechRecognition event handlers list (en-US) )。最常见会使用的一个是 SpeechRecognition.onresult (en-US) ,这在收到一个成功的结果时候触发。

js
recognition.onresult = function (event) {
  var last = event.results.length - 1;
  var color = event.results[last][0].transcript;
  diagnostic.textContent = "Result received: " + color + ".";
  bg.style.backgroundColor = color;
  console.log("Confidence: " + event.results[0][0].confidence);
};

代码中第三行看上去有一点复杂,让我们一步一步解释以下。SpeechRecognitionEvent.results (en-US) 属性返回的是一个SpeechRecognitionResultList (en-US) 对象 (这个对象会包含SpeechRecognitionResult (en-US) 对象们),它有一个 getter,所以它包含的这些对象可以像一个数组被访问到——所以[last] 返回的是排在最后位置 (最新) 的SpeechRecognitionResult对象。每个SpeechRecognitionResult 对象包含的 SpeechRecognitionAlternative (en-US) 对象含有一个被识别的单词。这些SpeechRecognitionResult 对象也有一个 getter,所以[0] 返回的是其中包含的第一个SpeechRecognitionAlternative (en-US) 对象。最后返回的transcript属性就是被识别单词的字符串,把背景颜色设置为这个颜色,并在 UI 中报告出这个结果信息。

也使用了 SpeechRecognition.onspeechend (en-US) 这个 handle 停止语音识别服务 (具体工作在SpeechRecognition.stop() (en-US)) ,一旦一个单词被识别就不能再说咯 (必须再点击屏幕再次开启语音识别)

js
recognition.onspeechend = function () {
  recognition.stop();
};

处理 error 和未能识别的语音

最后两个 handlers 处理的两种情况,一种是你说的内容不在定义的语法中所以识别不了,另一种是出现了 error。

SpeechRecognition.onnomatch (en-US) 支持的就是第一种,但是得注意它似乎在 Firefox 或者 Chrome 中触发会有问题;它只是返回任何被识别的内容:

js
recognition.onnomatch = function (event) {
  diagnostic.textContent = "I didnt recognise that color.";
};

SpeechRecognition.onerror (en-US) 处理的是第二种情况,识别成功了但是有 error 出现——SpeechRecognitionError.error (en-US) 属性包含的信息就是返回的确切的 error 是什么。

js
recognition.onerror = function (event) {
  diagnostic.textContent = "Error occurred in recognition: " + event.error;
};

Speech synthesis

语音合成 (也被称作是文本转为语音,英语简写是 tts) 包括接收 app 中需要语音合成的文本,再在设备麦克风播放出来这两个过程。

Web Speech API 对此有一个主要控制接口——SpeechSynthesis ,外加一些处理如何表示要被合成的文本 (也被称为 utterances),用什么声音来播出 utterances 等工作的相关接口。同样的,许多操作系统都有自己的某种语音合成系统,在这个任务中我们调用可用的 API 来使用语音合成系统。

Demo

为了展示 Web 语音合成的简单使用,我们提供了一个例子——Speak easy synthesis 。例子是一套表单控件,包括输入需要被合成的文本,设置音调、语速和发出文本时需要的语音。在输入文本之后,按下Enter/Return键使它播放。

UI of an app called speak easy synthesis. It has an input field in which to input text to be synthesised, slider controls to change the rate and pitch of the speech, and a drop down menu to choose between different voices.

想跑这个例子,你可以 git clone Github 仓库中的部分 (或者直接下载),在桌面版支持的浏览器打开 index.html 文件,或者在移动端浏览器直接导向 live demo URL ,像 Chrome 和 Firefox OS。

浏览器支持

Web Speech API 语音合成部分在各浏览器中还是在发展,还不成熟,现在有以下几个限制点:

  • Firefox 桌面版和移动版在 Gecko 42+(Windows)/44+ 支持,但是没有前缀,可以通过将media.webspeech.synth.enabled标志在about:config中转为true来启用。
  • Firefox OS 2.5+ 支持,但是默认的,不需要任何权限。
  • Chrome 桌面版和安卓版自 33 版以来都支持,但是没有前缀

HTML 和 CSS

HTML 和 CSS 还是无足轻重,只是简单包含一个标题,一段介绍文字,以及一个表格带有一些简单控制功能。<select> 元素初始时空的,之后会用 js 使用<option> 填充。

html
<h1>Speech synthesiser</h1>

<p>
  Enter some text in the input below and press return to hear it. change voices
  using the dropdown menu.
</p>

<form>
  <input type="text" class="txt" />
  <div>
    <label for="rate">Rate</label
    ><input type="range" min="0.5" max="2" value="1" step="0.1" id="rate" />
    <div class="rate-value">1</div>
    <div class="clearfix"></div>
  </div>
  <div>
    <label for="pitch">Pitch</label
    ><input type="range" min="0" max="2" value="1" step="0.1" id="pitch" />
    <div class="pitch-value">1</div>
    <div class="clearfix"></div>
  </div>
  <select></select>
</form>

JavaScript

让我们看看 js 在这个 app 中的强大表现。

设置变量

首先我们获得 UI 中涉及的 DOM 元素的引用,但更有趣的是,我们获得了Window.speechSynthesis (en-US) 的引用。这是 API 的入口点——它返回了SpeechSynthesis 的一个实例,对于 web 语音合成的控制接口。

js
var synth = window.speechSynthesis;

var inputForm = document.querySelector("form");
var inputTxt = document.querySelector(".txt");
var voiceSelect = document.querySelector("select");

var pitch = document.querySelector("#pitch");
var pitchValue = document.querySelector(".pitch-value");
var rate = document.querySelector("#rate");
var rateValue = document.querySelector(".rate-value");

var voices = [];

填充 select 元素

为使用设备可使用的不同的语音选项填充<select>元素,我们写了populateVoiceList() 方法。首先调用SpeechSynthesis.getVoices() ,这个函数返回包含所有可用语音 (SpeechSynthesisVoice (en-US)对象) 的列表。接下来循环这个列表,每次创建一个<option> 元素,设置它的文本内容以显示声音的名称(从SpeechSynthesisVoice.name获取),语音的语言(从SpeechSynthesisVoice.lang获取),如果某个语音是合成引擎默认的 (检查SpeechSynthesisVoice.default (en-US)属性返回true) 在文本内容后面添加-- DEFAULT

对于每个option元素,我们也创建了data- 属性,属性值是语音的名字和语言,这样在之后我们可以轻松获取这个信息。之后把所有的option元素作为孩子添加到select 元素。

js
function populateVoiceList() {
  voices = synth.getVoices();

  for (i = 0; i < voices.length; i++) {
    var option = document.createElement("option");
    option.textContent = voices[i].name + " (" + voices[i].lang + ")";

    if (voices[i].default) {
      option.textContent += " -- DEFAULT";
    }

    option.setAttribute("data-lang", voices[i].lang);
    option.setAttribute("data-name", voices[i].name);
    voiceSelect.appendChild(option);
  }
}

接下来是运行这个函数,我们做如下代码工作。因为 Firefox 不支持SpeechSynthesis.onvoiceschanged (en-US) ,所以很常规地只是返回语音对象列表当SpeechSynthesis.getVoices() 被触发。但是 Chrome 就有点不同了,在SpeechSynthesis.getVoices() 被触发时,先要等待事件触发 (有点绕~按照下面代码,populateVoiceList 函数在 Firefox 运行一次,在 Chrome 中运行两次):

js
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
  speechSynthesis.onvoiceschanged = populateVoiceList;
}

说出输入的文本

接下来我们创建一个事件处理器 (handler),开始说出在文本框中输入的文本。我们把onsubmit 处理器挂在表单上,当Enter/Return 被按下,对应行为就会发生。我们首先通过构造函数创建一个新的SpeechSynthesisUtterance() (en-US) 实例——把文本输入框中的值作为参数传递。

接下来,我们需要弄清楚使用哪种语音。使用HTMLSelectElement (en-US) selectedOptions 属性返回当前选中的 <option> 元素。然后使用元素的data-name属性,找到 SpeechSynthesisVoice (en-US) 对象的name匹配data-name 的值。把匹配的语音对象设置为SpeechSynthesisUtterance.voice 的属性值。

最后,我们设置 SpeechSynthesisUtterance.pitch (en-US)SpeechSynthesisUtterance.rate (en-US) 属性值为对应范围表单元素中的值。哈哈所有准备工作就绪,调用 SpeechSynthesis.speak() (en-US) 开始说话。把 SpeechSynthesisUtterance (en-US) 实例作为参数传递。

js
inputForm.onsubmit = function(event) {
  event.preventDefault();

  var utterThis = new SpeechSynthesisUtterance(inputTxt.value);
  var selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name');
  for(i = 0; i < voices.length ; i++) {
    if(voices[i].name === selectedOption) {
      utterThis.voice = voices[i];
    }
  }
  utterThis.pitch = pitch.value;
  utterThis.rate = rate.value;
  synth.speak(utterThis);

在事件处理器的最后部分,我们加入了一个 SpeechSynthesisUtterance.onpause (en-US) 处理器,来展示SpeechSynthesisEvent (en-US) 如何可以很好地使用。当 SpeechSynthesis.pause() (en-US) 被调用,这将返回一条消息,报告该语音暂停的字符编号和名称。

js
utterThis.onpause = function (event) {
  var char = event.utterance.text.charAt(event.charIndex);
  console.log(
    "Speech paused at character " +
      event.charIndex +
      ' of "' +
      event.utterance.text +
      '", which is "' +
      char +
      '".',
  );
};

最后,我们在文本输入框添加了 blur() (en-US) 方法。这主要是在 Firefox 操作系统上隐藏键盘

js
  inputTxt.blur();
}

Updating the displayed pitch and rate values

代码的最后部分,在每次滑动条移动时,简单地更新pitch/rate在 UI 中展示的值。

js
pitch.onchange = function () {
  pitchValue.textContent = pitch.value;
};

rate.onchange = function () {
  rateValue.textContent = rate.value;
};