Web Speech APIを使う

Web Speech API は、音声認識と音声合成(text to speech または tts としても知られています)という 2 つの異なる分野の機能を提供しており、アクセシビリティと制御メカニズムに興味深い新しい可能性をもたらします。この記事では、両方の分野の簡単な紹介とデモを提供します。

音声認識

音声認識ではデバイスのマイクを通して音声を受信し、音声認識サービスによって文法のリスト(基本的には特定のアプリで認識させたいボキャブラリー)と照合されます。単語やフレーズが正常に認識されると、結果(または結果のリスト)がテキスト文字列として返され、その結果としてさらなるアクションを開始することができます。

Web Speech API には、このための主要なコントローラインターフェイスである SpeechRecognition と、文法や結果などを表現するためのいくつかの密接に関連したインターフェースがあります。一般的には、デバイス上で利用可能なデフォルトの音声認識システムが音声認識に使用されます — 最近のほとんどのOSには音声コマンドを発行するための音声認識システムが搭載されています。macOSのDictation、iOSのSiri、Windows 10のCortana、AndroidのSpeechなどを考えてみてください。

注釈: Chrome などの一部のブラウザでは、Web ページで音声認識を使用するためにサーバーベースの認識エンジンが必要です。音声が認識処理のためにウェブサービスに送信されるため、オフラインでは機能しません。

デモ

Web音声認識の簡単な使い方を示すために、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.

デモを実行するには、それが一部となっているGithubリポジトリをクローン(または直接ダウンロード)し、サポートされているデスクトップブラウザでHTML indexファイルを開くか、ChromeのようなサポートされているモバイルブラウザでライブデモのURLに移動することができます。

ブラウザ対応

Web Speech API 音声認識のサポートは、通常 Chrome for Desktop と Android に限られています — Chrome はバージョン 33 付近からサポートしていますが、プレフィックス付きのインターフェイスを使用しているため、 webkitSpeechRecognition などのプレフィックス付きバージョンを含める必要があります。

HTMLとCSS

アプリのHTMLとCSSは本当に簡単です。タイトル、説明段落、診断メッセージを出力するdivがあるだけです。

<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では、デバイスをまたいでも問題なく見えるように、非常にシンプルなレスポンシブ・スタイリングを提供しています。

JavaScript

JavaScriptをもう少し詳しく見てみましょう。

Chrome対応

前述したように、Chrome は現在プレフィックス付きのプロパティで音声認識をサポートしているので、適切なオブジェクトを Chrome に供給し、そして将来的な実装でプレフィックスなしの機能をサポートする可能性も踏まえ、以下の行をコードの最初に追加しています。

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

文法

コードの次の部分では、アプリが認識する文法を定義します。次の変数は文法を保持するために定義されています。

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

使用されている文法形式は JSpeech Grammar Format (JSGF) です — それについての詳細はリンク先の仕様書を参照してください。しかし、今のところは手っ取り早く実行してみましょう。

  • 行の区切りはJavaScriptと同じようにセミコロンで区切られています。
  • 最初の行 — #JSGF V1.0; — は、使用されているフォーマットとバージョンを示します。これは常に最初に含める必要があります。
  • 2行目は認識したい用語のタイプを示します。public はパブリックルールであることを宣言し、角括弧内の文字列はこの用語の認識名 (color) を定義し、等号の後に続く項目のリストは、用語の適切な値として認識され受け入れられる代替値です。それぞれがパイプ文字で区切られていることに注意してください。
  • 上記の構造に沿って別の行に好きなだけ多くの用語を定義し、かなり複雑な文法定義を含めることができます。この基本的なデモのために私たちは物事をシンプルにしています。 

文法を音声認識にプラグインする

次にやるべきことは、アプリケーションの認識を制御する音声認識インスタンスを定義することです。これは SpeechRecognition() コンストラクタを使用して行います。また、SpeechGrammarList() (en-US) コンストラクタを使用して、文法を含む新しい音声文法リストも作成します。

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

SpeechGrammarList.addFromString() (en-US) メソッドを使ってリストに grammar を追加します。 このメソッドは追加したい文字列をパラメータとして受けとり、さらにオプションで、リスト内で利用可能な他の文法との関係においてこの文法の重要度を指定する重み値を受け取ります(0から1までの範囲で指定できます)。追加された文法はSpeechGrammar オブジェクトのインスタンスとしてリスト内で利用できます。

speechRecognitionList.addFromString(grammar, 1);

次に、 SpeechGrammarList (en-US) を SpeechRecognition.grammars プロパティの値に設定することで、音声認識インスタンスに SpeechGrammarList (en-US) を追加します。次に進む前に、認識インスタンスの他のいくつかのプロパティも設定します。

  • SpeechRecognition.continuous: 認識が開始されるたびに連続した結果をキャプチャする (true) か、または単一の結果だけをキャプチャする (false) かを制御します。
  • SpeechRecognition.lang: 認識の言語を設定します。これを設定することは良い習慣であるため、推奨されます。
  • SpeechRecognition.interimResults: 音声認識システムが中間的な結果を返すか、最終的な結果だけを返すか定義します。このシンプルなデモでは最終的な結果で十分です。
  • SpeechRecognition.maxAlternatives: 結果ごとに返される代替候補数を設定します。これは、結果が完全に明確ではなく、ユーザーが正しいものを選択できるように代替候補のリストを表示したい場合などに便利な場合があります。しかし、このシンプルなデモでは必要ないのでここでは1つだけ指定します(これは実際にはデフォルトです)。
recognition.grammars = speechRecognitionList;
recognition.continuous = false;
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;

音声認識の開始

出力 <div> とHTML要素への参照を取得(診断メッセージを出力したり、後でアプリの背景色を更新したりできるようにするため)した後、画面がタップ/クリックされたときに音声認識サービスが開始されるように onclick ハンドラを実装します。これは SpeechRecognition.start() を呼び出すことで実現しています。 forEach() メソッドは何色を言っているかを示す色付きインジケータを出力するために使われています。

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.');
}

結果の受け取りとハンドリング

音声認識が開始されると、結果やその他の周辺情報を取得するために使用できる多くのイベントハンドラがあります(SpeechRecognition のイベントハンドラのリスト を参照してください)。最も一般的なものは SpeechRecognition.onresult で、成功した結果を受信したときに発火されます。

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

ここの2行目はちょっと複雑そうなので、順を追って説明していきましょう。SpeechRecognitionEvent.results (en-US)プロパティは、 SpeechRecognitionResult オブジェクトを含む SpeechRecognitionResultList (en-US) オブジェクトを返します。これはゲッターを持っているので配列のようにアクセスでき、最初の[0]は0の位置にあるSpeechRecognitionResultを返します。各 SpeechRecognitionResult オブジェクトには、個々に認識された単語を含む SpeechRecognitionAlternative オブジェクトが含まれています。これらは配列のようにアクセスできるようにゲッターも持っています  — 2 番目の[0]は、したがって位置 0 の SpeechRecognitionAlternative を返します。次に、その transcript プロパティを返して個々の認識結果を含む文字列を文字列として取得し、背景色をその色に設定し、認識された色をUIの診断メッセージとして報告します。

また、 SpeechRecognition.onspeechend ハンドラを使用して音声認識サービスの実行を停止します(1つの単語が認識され、それが発話され終わったら SpeechRecognition.stop()) を使用します)。

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

エラーや認識されない発話のハンドリング

最後の 2 つのハンドラは、定義された文法にない音声が認識されたケースやエラーが発生したケースを処理するためのものです。SpeechRecognition.onnomatch は最初に言及したケースを処理することになっているようですが、今のところ正しく動作しているようには見えないことに注意してください(とにかく認識されたものを返すだけです)。

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

SpeechRecognition.onerror は、認識に成功して実際にエラーが発生したケースを処理します — SpeechRecognitionError.error (en-US) プロパティには、返された実際のエラーが含まれます。

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

Speech synthesis

Speech synthesis (aka text-to-speech, or tts) involves receiving synthesising text contained within an app to speech, and playing it out of a device's speaker or audio output connection.

The Web Speech API has a main controller interface for this — SpeechSynthesis — plus a number of closely-related interfaces for representing text to be synthesised (known as utterances), voices to be used for the utterance, etc. Again, most OSes have some kind of speech synthesis system, which will be used by the API for this task as available.

Demo

To show simple usage of Web speech synthesis, we've provided a demo called Speak easy synthesis. This includes a set of form controls for entering text to be synthesised, and setting the pitch, rate, and voice to use when the text is uttered. After you have entered your text, you can press Enter/Return to hear it spoken.

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.

To run the demo, you can clone (or directly download) the Github repo it is part of, open the HTML index file in a supporting desktop browser, or navigate to the live demo URL in a supporting mobile browser like Chrome, or Firefox OS.

Browser support

Support for Web Speech API speech synthesis is still getting there across mainstream browsers, and is currently limited to the following:

  • Firefox desktop and mobile support it in Gecko 42+ (Windows)/44+, without prefixes, and it can be turned on by flipping the media.webspeech.synth.enabled flag to true in about:config.

  • Firefox OS 2.5+ supports it, by default, and without the need for any permissions.

  • Chrome for Desktop and Android have supported it since around version 33, without prefixes.

HTML and CSS

The HTML and CSS are again pretty trivial, simply containing a title, some instructions for use, and a form with some simple controls. The <select> element is initially empty, but is populated with <option>s via JavaScript (see later on.)

<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

Let's investigate the JavaScript that powers this app.

Setting variables

First of all, we capture references to all the DOM elements involved in the UI, but more interestingly, we capture a reference to Window.speechSynthesis. This is API's entry point — it returns an instance of SpeechSynthesis, the controller interface for web speech synthesis.

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 = [];

Populating the select element

To populate the <select> element with the different voice options the device has available, we've written a populateVoiceList() function. We first invoke SpeechSynthesis.getVoices() (en-US), which returns a list of all the available voices, represented by SpeechSynthesisVoice (en-US) objects. We then loop through this list — for each voice we create an <option> element, set its text content to display the name of the voice (grabbed from SpeechSynthesisVoice.name (en-US)), the language of the voice (grabbed from SpeechSynthesisVoice.lang (en-US)), and -- DEFAULT if the voice is the default voice for the synthesis engine (checked by seeing if SpeechSynthesisVoice.default (en-US) returns true.)

We also create data- attributes for each option, containing the name and language of the associated voice, so we can grab them easily later on, and then append the options as children of the select.

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

When we come to run the function, we do the following. This is because Firefox doesn't support SpeechSynthesis.onvoiceschanged (en-US), and will just return a list of voices when SpeechSynthesis.getVoices() (en-US) is fired. With Chrome however, you have to wait for the event to fire before populating the list, hence the if statement seen below.

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

Speaking the entered text

Next, we create an event handler to start speaking the text entered into the text field. We are using an onsubmit (en-US) handler on the form so that the action happens when Enter/Return is pressed. We first create a new SpeechSynthesisUtterance() (en-US) instance using its constructor — this is passed the text input's value as a parameter.

Next, we need to figure out which voice to use. We use the HTMLSelectElement selectedOptions property to return the currently selected <option> element. We then use this element's data-name attribute, finding the SpeechSynthesisVoice (en-US) object whose name matches this attribute's value. We set the matching voice object to be the value of the SpeechSynthesisUtterance.voice (en-US) property.

Finally, we set the SpeechSynthesisUtterance.pitch (en-US) and SpeechSynthesisUtterance.rate (en-US) to the values of the relevant range form elements. Then, with all necessary preparations made, we start the utterance being spoken by invoking SpeechSynthesis.speak() (en-US), passing it the SpeechSynthesisUtterance instance as a parameter.

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

In the final part of the handler, we include an SpeechSynthesisUtterance.onpause (en-US) handler to demonstrate how SpeechSynthesisEvent (en-US) can be put to good use. When SpeechSynthesis.pause() (en-US) is invoked, this returns a message reporting the character number and name that the speech was paused at.

   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 + '".');
  }

Finally, we call blur() on the text input. This is mainly to hide the keyboard on Firefox OS.

  inputTxt.blur();
}

Updating the displayed pitch and rate values

The last part of the code simply updates the pitch/rate values displayed in the UI, each time the slider positions are moved.

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

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