How To Build a Text-to-Speech App with Web Speech API
序章
何らかの音声体験を提供するアプリを操作した可能性が非常に高くなります。 テキストメッセージや通知を声に出して読むなど、テキスト読み上げ機能を備えたアプリである可能性があります。 また、SiriやGoogleアシスタントなどの音声認識機能を備えたアプリの場合もあります。
HTML5の登場により、Webプラットフォームで利用可能なAPIの数が急速に増加しました。 Web Speech API と呼ばれるAPIがいくつかあり、さまざまな種類の音声アプリケーションとWeb用のエクスペリエンスをシームレスに構築できるように開発されています。 これらのAPIはまだかなり実験的なものですが、最近のすべてのブラウザーでそれらのほとんどのサポートが増えています。
この記事では、ランダムな引用を取得し、引用を表示し、ブラウザがテキスト読み上げを使用して引用を読み上げる機能をユーザーに提供するアプリケーションを構築します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
このチュートリアルは、ノードv14.4.0、npm
v6.14.5、axios
v0.19.2、cors
v2.8.5、express
v4.17.1、およびjQueryv3.5.1。
WebSpeechAPIの使用
Web Speech APIには、次の2つの主要なインターフェイスがあります。
-
SpeechSynthesis-テキスト読み上げアプリケーション用。 これにより、アプリはデバイスの音声合成装置を使用してテキストコンテンツを読み上げることができます。 使用可能な音声タイプは
SpeechSynthesisVoice
オブジェクトで表され、発声されるテキストはSpeechSynthesisUtterance
オブジェクトで表されます。 ブラウザのサポートの詳細については、SpeechSynthesis
インターフェイスのサポートテーブルを参照してください。 -
SpeechRecognition-非同期音声認識を必要とするアプリケーション向け。 これにより、アプリは音声入力から音声コンテキストを認識できます。
SpeechRecognition
オブジェクトは、コンストラクターを使用して作成できます。SpeechGrammar
インターフェースは、アプリが認識すべき文法のセットを表すために存在します。 ブラウザのサポートの詳細については、SpeechRecognition
インターフェイスのサポートテーブルを参照してください。
このチュートリアルでは、SpeechSynthesis
に焦点を当てます。
参照の取得
SpeechSynthesis
オブジェクトへの参照を取得するには、次の1行のコードで実行できます。
var synthesis = window.speechSynthesis;
次のコードスニペットは、ブラウザのサポートを確認する方法を示しています。
if ('speechSynthesis' in window) {
var synthesis = window.speechSynthesis;
} else {
console.log('Text-to-speech not supported.');
}
SpeechSynthesis
が提供する機能を使用する前に、ブラウザでサポートされているかどうかを確認すると非常に便利です。
利用可能な音声の取得
このステップでは、既存のコードに基づいて、利用可能な音声を取得します。 getVoices()
メソッドは、デバイスで使用可能なすべての音声を表すSpeechSynthesisVoice
オブジェクトのリストを返します。
次のコードスニペットを見てください。
if ('speechSynthesis' in window) {
var synthesis = window.speechSynthesis;
// Regex to match all English language tags e.g en, en-US, en-GB
var langRegex = /^en(-[a-z]{2})?$/i;
// Get the available voices and filter the list to only have English speakers
var voices = synthesis
.getVoices()
.filter((voice) => langRegex.test(voice.lang));
// Log the properties of the voices in the list
voices.forEach(function (voice) {
console.log({
name: voice.name,
lang: voice.lang,
uri: voice.voiceURI,
local: voice.localService,
default: voice.default,
});
});
} else {
console.log('Text-to-speech not supported.');
}
コードのこのセクションでは、デバイスで使用可能な音声のリストを取得し、langRegex
正規表現を使用してリストをフィルタリングして、英語を話す人だけの音声を取得できるようにします。 最後に、リスト内の音声をループして、それぞれのプロパティをコンソールに記録します。
発話の構築
このステップでは、SpeechSynthesisUtterance
コンストラクターを使用し、使用可能なプロパティの値を設定して、音声発話を作成します。
次のコードスニペットは、テキスト"Hello World"
を読むための音声発話を作成します。
if ('speechSynthesis' in window) {
var synthesis = window.speechSynthesis;
// Get the first `en` language voice in the list
var voice = synthesis.getVoices().filter(function (voice) {
return voice.lang === 'en';
})[0];
// Create an utterance object
var utterance = new SpeechSynthesisUtterance('Hello World');
// Set utterance properties
utterance.voice = voice;
utterance.pitch = 1.5;
utterance.rate = 1.25;
utterance.volume = 0.8;
// Speak the utterance
synthesis.speak(utterance);
} else {
console.log('Text-to-speech not supported.');
}
ここでは、使用可能な音声のリストから最初のen
言語の音声を取得します。 次に、SpeechSynthesisUtterance
コンストラクターを使用して新しい発話を作成します。 次に、voice
、pitch
、rate
、volume
などのいくつかのプロパティを発話オブジェクトに設定します。 最後に、SpeechSynthesis
のspeak()
メソッドを使用して発話を話します。
注:発話で話すことができるテキストのサイズには制限があります。 各発話で話せるテキストの最大長は32,767文字です。
コンストラクターで発声するテキストを渡したことに注意してください。
発話オブジェクトのtext
プロパティを設定して、発話するテキストを設定することもできます。
簡単な例を次に示します。
var synthesis = window.speechSynthesis;
var utterance = new SpeechSynthesisUtterance("Hello World");
// This overrides the text "Hello World" and is uttered instead
utterance.text = "My name is Glad.";
synthesis.speak(utterance);
これは、コンストラクターで渡されたテキストをオーバーライドします。
発話を話す
前のコードスニペットでは、SpeechSynthesis
インスタンスでspeak()
メソッドを呼び出して発話を話しました。 これで、SpeechSynthesisUtterance
インスタンスをspeak()
メソッドの引数として渡して発話を話すことができます。
var synthesis = window.speechSynthesis;
var utterance1 = new SpeechSynthesisUtterance("Hello World");
var utterance2 = new SpeechSynthesisUtterance("My name is Glad.");
var utterance3 = new SpeechSynthesisUtterance("I'm a web developer from Nigeria.");
synthesis.speak(utterance1);
synthesis.speak(utterance2);
synthesis.speak(utterance3);
SpeechSynthesis
インスタンスでは、発話の一時停止、再開、キャンセルなど、他にもいくつかのことができます。 したがって、pause()
、resume()
、およびcancel()
メソッドは、SpeechSynthesis
インスタンスでも使用できます。
ステップ1—テキスト読み上げアプリを作成する
SpeechSynthesis
インターフェースの基本的な側面を見てきました。 次に、テキスト読み上げアプリケーションの作成を開始します。 始める前に、マシンにNodeとnpmがインストールされていることを確認してください。
ターミナルで次のコマンドを実行して、アプリのプロジェクトをセットアップし、依存関係をインストールします。
新しいプロジェクトディレクトリを作成します。
- mkdir web-speech-app
新しく作成されたプロジェクトディレクトリに移動します。
- cd web-speech-app
プロジェクトを初期化します。
- npm init -y
プロジェクトに必要な依存関係をインストールします-express
、cors
、およびaxios
:
- npm install express cors axios
package.json
ファイルの"scripts"
セクションを次のスニペットのように変更します。
"scripts": {
"start": "node server.js"
}
アプリケーションのプロジェクトを初期化したので、Expressを使用してアプリのサーバーをセットアップします。
新しいserver.js
ファイルを作成し、それに次のコンテンツを追加します。
const cors = require('cors');
const path = require('path');
const axios = require('axios');
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;
app.set('port', PORT);
// Enable CORS (Cross-Origin Resource Sharing)
app.use(cors());
// Serve static files from the /public directory
app.use('/', express.static(path.join(__dirname, 'public')));
// A simple endpoint for fetching a random quote from QuotesOnDesign
app.get('/api/quote', (req, res) => {
axios
.get(
'https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand'
)
.then((response) => {
const [post] = response.data;
const { title, content } = post || {};
return title && content
? res.json({ status: 'success', data: { title, content } })
: res
.status(500)
.json({ status: 'failed', message: 'Could not fetch quote.' });
})
.catch((err) =>
res
.status(500)
.json({ status: 'failed', message: 'Could not fetch quote.' })
);
});
app.listen(PORT, () => console.log(`> App server is running on port ${PORT}.`));
ここでは、Expressを使用してノードサーバーをセットアップします。 cors()
ミドルウェアを使用してCORS(クロスオリジンリクエストシェアリング)を有効にしました。 また、express.static()
ミドルウェアを使用して、プロジェクトルートの/public
ディレクトリから静的ファイルを提供します。 これにより、すぐに作成するインデックスページを提供できるようになります。
最後に、 QuotesOnDesignAPIサービスからランダムな見積もりを取得するためのGET
/api/quote
ルートを設定します。 axios (PromiseベースのHTTPクライアントライブラリ)を使用してHTTPリクエストを作成しています。
QuotesOnDesignAPIからのサンプル応答は次のようになります。
Output[
{
"title": { "rendered": "Victor Papanek" },
"content": {
"rendered": "<p>Any attempt to separate design, to make it a thing-by-itself, works counter to the inherent value of design as the primary, underlying matrix of life.</p>\n",
"protected": false
}
}
]
注: QuotesOnDesignのAPIの変更の詳細については、4.0と5.0の間の変更を文書化したページを参照してください。
見積もりを正常にフェッチすると、見積もりのtitle
とcontent
がJSON応答のdata
フィールドに返されます。 そうしないと、500
HTTPステータスコードを含む失敗したJSON応答が返されます。
次に、アプリビューのインデックスページを作成します。
まず、プロジェクトのルートに新しいpublic
フォルダーを作成します。
- mkdir public
次に、新しく作成したpublic
フォルダーに新しいindex.html
ファイルを作成し、次のコンテンツを追加します。
<html>
<head>
<title>Daily Quotes</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>
<body class="position-absolute h-100 w-100">
<div id="app" class="d-flex flex-wrap align-items-center align-content-center p-5 mx-auto w-50 position-relative"></div>
<script src="https://unpkg.com/jquery/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="main.js"></script>
</body>
</html>
これにより、アプリのすべての動的コンテンツのマウントポイントとして機能する<div id="app">
が1つだけのアプリの基本的なインデックスページが作成されます。
また、Bootstrap CDNへのリンクを追加して、アプリのデフォルトの Bootstrap4スタイルを取得しました。 また、DOM操作とAJAXリクエスト用の jQuery と、エレガントなSVGアイコン用のフェザーアイコンも含まれています。
ステップ2—メインスクリプトを作成する
これで、アプリを強化する最後の部分であるメインスクリプトにたどり着きました。 アプリのpublic
ディレクトリに新しいmain.js
ファイルを作成し、それに次のコンテンツを追加します。
jQuery(function ($) {
let app = $('#app');
let SYNTHESIS = null;
let VOICES = null;
let QUOTE_TEXT = null;
let QUOTE_PERSON = null;
let VOICE_SPEAKING = false;
let VOICE_PAUSED = false;
let VOICE_COMPLETE = false;
let iconProps = {
'stroke-width': 1,
'width': 48,
'height': 48,
'class': 'text-secondary d-none',
'style': 'cursor: pointer'
};
function iconSVG(icon) {}
function showControl(control) {}
function hideControl(control) {}
function getVoices() {}
function resetVoice() {}
function fetchNewQuote() {}
function renderQuote(quote) {}
function renderVoiceControls(synthesis, voice) {}
function updateVoiceControls() {}
function initialize() {}
initialize();
});
このコードは、jQuery
を使用して、DOMのロード時に関数を実行します。 #app
要素への参照を取得し、いくつかの変数を初期化します。 また、次のセクションで実装するいくつかの空の関数を宣言します。 最後に、initialize()
関数を呼び出してアプリケーションを初期化します。
iconProps
変数には、フェザーアイコンをSVGとしてDOMにレンダリングするために使用されるいくつかのプロパティが含まれています。
そのコードを配置すると、関数の実装を開始する準備が整います。 public/main.js
ファイルを変更して、次の機能を実装します。
// Gets the SVG markup for a Feather icon
function iconSVG(icon) {
let props = $.extend(iconProps, { id: icon });
return feather.icons[icon].toSvg(props);
}
// Shows an element
function showControl(control) {
control.addClass('d-inline-block').removeClass('d-none');
}
// Hides an element
function hideControl(control) {
control.addClass('d-none').removeClass('d-inline-block');
}
// Get the available voices, filter the list to have only English filters
function getVoices() {
// Regex to match all English language tags e.g en, en-US, en-GB
let langRegex = /^en(-[a-z]{2})?$/i;
// Get the available voices and filter the list to only have English speakers
VOICES = SYNTHESIS.getVoices()
.filter(function (voice) {
return langRegex.test(voice.lang);
})
.map(function (voice) {
return {
voice: voice,
name: voice.name,
lang: voice.lang.toUpperCase(),
};
});
}
// Reset the voice variables to the defaults
function resetVoice() {
VOICE_SPEAKING = false;
VOICE_PAUSED = false;
VOICE_COMPLETE = false;
}
iconSVG(icon)
関数は、引数としてフェザーアイコン名の文字列('play-circle'
など)を取り、アイコンのSVGマークアップを返します。 フェザーのWebサイトをチェックして、使用可能なフェザーアイコンの完全なリストを確認してください。 APIの詳細については、フェザーのドキュメントも確認してください。
getVoices()
関数は、SYNTHESIS
オブジェクトを使用して、デバイスで使用可能なすべての音声のリストをフェッチします。 次に、正規表現を使用してリストをフィルタリングし、英語を話す人だけの声を取得します。
次に、DOMで引用符をフェッチおよびレンダリングするための関数を実装します。 public/main.js
ファイルを変更して、次の機能を実装します。
function fetchNewQuote() {
// Clean up the #app element
app.html('');
// Reset the quote variables
QUOTE_TEXT = null;
QUOTE_PERSON = null;
// Reset the voice variables
resetVoice();
// Pick a voice at random from the VOICES list
let voice =
VOICES && VOICES.length > 0
? VOICES[Math.floor(Math.random() * VOICES.length)]
: null;
// Fetch a quote from the API and render the quote and voice controls
$.get('/api/quote', function (quote) {
renderQuote(quote.data);
SYNTHESIS && renderVoiceControls(SYNTHESIS, voice || null);
});
}
function renderQuote(quote) {
// Create some markup for the quote elements
let quotePerson = $('<h1 id="quote-person" class="mb-2 w-100"></h1>');
let quoteText = $('<div id="quote-text" class="h3 py-5 mb-4 w-100 font-weight-light text-secondary border-bottom border-gray"></div>');
// Add the quote data to the markup
quotePerson.html(quote.title.rendered);
quoteText.html(quote.content.rendered);
// Attach the quote elements to the DOM
app.append(quotePerson);
app.append(quoteText);
// Update the quote variables with the new data
QUOTE_TEXT = quoteText.text();
QUOTE_PERSON = quotePerson.text();
}
ここでfetchNewQuote()
メソッドでは、最初にアプリの要素と変数をリセットします。 次に、VOICES
変数に格納されている音声のリストから、Math.random()
を使用して音声をランダムに選択します。 $.get()
を使用して、/api/quote
エンドポイントにAJAXリクエストを行い、ランダムな見積もりを取得し、見積もりデータを音声コントロールと一緒にビューにレンダリングします。
renderQuote(quote)
メソッドは、引数としてquoteオブジェクトを受け取り、その内容をDOMに追加します。 最後に、引用変数QUOTE_TEXT
およびQUOTE_PERSON
を更新します。
fetchNewQuote()
関数を見ると、renderVoiceControls()
関数を呼び出していることがわかります。 この機能は、音声出力の再生、一時停止、および停止のコントロールをレンダリングする役割を果たします。 また、使用中の現在の音声と言語をレンダリングします。
public/main.js
ファイルに次の変更を加えて、renderVoiceControls()
機能を実装します。
function renderVoiceControls(synthesis, voice) {
let controlsPane = $('<div id="voice-controls-pane" class="d-flex flex-wrap w-100 align-items-center align-content-center justify-content-between"></div>');
let voiceControls = $('<div id="voice-controls"></div>');
// Create the SVG elements for the voice control buttons
let playButton = $(iconSVG('play-circle'));
let pauseButton = $(iconSVG('pause-circle'));
let stopButton = $(iconSVG('stop-circle'));
// Helper function to enable pause state for the voice output
let paused = function () {
VOICE_PAUSED = true;
updateVoiceControls();
};
// Helper function to disable pause state for the voice output
let resumed = function () {
VOICE_PAUSED = false;
updateVoiceControls();
};
// Click event handler for the play button
playButton.on('click', function (evt) {});
// Click event handler for the pause button
pauseButton.on('click', function (evt) {});
// Click event handler for the stop button
stopButton.on('click', function (evt) {});
// Add the voice controls to their parent element
voiceControls.append(playButton);
voiceControls.append(pauseButton);
voiceControls.append(stopButton);
// Add the voice controls parent to the controlsPane element
controlsPane.append(voiceControls);
// If voice is available, add the voice info element to the controlsPane
if (voice) {
let currentVoice = $('<div class="text-secondary font-weight-normal"><span class="text-dark font-weight-bold">' + voice.name + '</span> (' + voice.lang + ')</div>');
controlsPane.append(currentVoice);
}
// Add the controlsPane to the DOM
app.append(controlsPane);
// Show the play button
showControl(playButton);
}
ここでは、音声コントロールとコントロールペインのコンテナ要素を作成します。 前に作成したiconSVG()
関数を使用して、コントロールボタンのSVGマークアップを取得し、ボタン要素も作成します。 paused()
およびresumed()
ヘルパー関数を定義します。これらの関数は、ボタンのイベントハンドラーを設定するときに使用されます。
最後に、音声制御ボタンと音声情報をDOMにレンダリングします。 また、最初は再生ボタンのみが表示されるように構成されています。
次に、前のセクションで定義した音声制御ボタンのクリックイベントハンドラーを実装します。
次のコードスニペットに示すように、イベントハンドラーを設定します。
// Click event handler for the play button
playButton.on('click', function (evt) {
evt.preventDefault();
if (VOICE_SPEAKING) {
// If voice is paused, it is resumed when the playButton is clicked
if (VOICE_PAUSED) synthesis.resume();
return resumed();
} else {
// Create utterances for the quote and the person
let quoteUtterance = new SpeechSynthesisUtterance(QUOTE_TEXT);
let personUtterance = new SpeechSynthesisUtterance(QUOTE_PERSON);
// Set the voice for the utterances if available
if (voice) {
quoteUtterance.voice = voice.voice;
personUtterance.voice = voice.voice;
}
// Set event listeners for the quote utterance
quoteUtterance.onpause = paused;
quoteUtterance.onresume = resumed;
quoteUtterance.onboundary = updateVoiceControls;
// Set the listener to activate speaking state when the quote utterance starts
quoteUtterance.onstart = function (evt) {
VOICE_COMPLETE = false;
VOICE_SPEAKING = true;
updateVoiceControls();
};
// Set event listeners for the person utterance
personUtterance.onpause = paused;
personUtterance.onresume = resumed;
personUtterance.onboundary = updateVoiceControls;
// Refresh the app and fetch a new quote when the person utterance ends
personUtterance.onend = fetchNewQuote;
// Speak the utterances
synthesis.speak(quoteUtterance);
synthesis.speak(personUtterance);
}
});
// Click event handler for the pause button
pauseButton.on('click', function (evt) {
evt.preventDefault();
// Pause the utterance if it is not in paused state
if (VOICE_SPEAKING) synthesis.pause();
return paused();
});
// Click event handler for the stop button
stopButton.on('click', function (evt) {
evt.preventDefault();
// Clear the utterances queue
if (VOICE_SPEAKING) synthesis.cancel();
resetVoice();
// Set the complete status of the voice output
VOICE_COMPLETE = true;
updateVoiceControls();
});
ここでは、音声コントロールボタンのクリックイベントリスナーを設定します。 再生ボタンをクリックすると、quoteUtterance
、personUtterance
の順に発話が始まります。 ただし、音声出力が一時停止状態の場合は再開します。
quoteUtterance
のonstart
イベントハンドラーでVOICE_SPEAKING
をtrue
に設定します。 personUtterance
が終了すると、アプリは新しい見積もりを更新して取得します。
Pause ボタンは音声出力を一時停止し、 Stop ボタンは音声出力を終了し、SpeechSynthesis
インターフェース。 コードは毎回updateVoiceControls()
関数を呼び出して、適切なボタンを表示します。
前のコードスニペットで、updateVoiceControls()
関数をいくつか呼び出して参照しました。 この関数は、音声コントロールを更新して、音声状態変数に基づいて適切なコントロールを表示する役割を果たします。
public/main.js
ファイルに次の変更を加えて、updateVoiceControls()
機能を実装します。
function updateVoiceControls() {
// Get a reference to each control button
let playButton = $('#play-circle');
let pauseButton = $('#pause-circle');
let stopButton = $('#stop-circle');
if (VOICE_SPEAKING) {
// Show the stop button if speaking is in progress
showControl(stopButton);
// Toggle the play and pause buttons based on paused state
if (VOICE_PAUSED) {
showControl(playButton);
hideControl(pauseButton);
} else {
hideControl(playButton);
showControl(pauseButton);
}
} else {
// Show only the play button if no speaking is in progress
showControl(playButton);
hideControl(pauseButton);
hideControl(stopButton);
}
}
コードのこのセクションでは、最初に各音声制御ボタン要素への参照を取得します。 次に、音声出力のさまざまな状態で表示する音声制御ボタンを指定します。
これで、initialize()
関数を実装する準備が整いました。 この関数は、アプリケーションの初期化を担当します。 次のコードスニペットをpublic/main.js
ファイルに追加して、initialize()
関数を実装します。
function initialize() {
if ('speechSynthesis' in window) {
SYNTHESIS = window.speechSynthesis;
let timer = setInterval(function () {
let voices = SYNTHESIS.getVoices();
if (voices.length > 0) {
getVoices();
fetchNewQuote();
clearInterval(timer);
}
}, 200);
} else {
let message = 'Text-to-speech not supported by your browser.';
// Create the browser notice element
let notice = $('<div class="w-100 py-4 bg-danger font-weight-bold text-white position-absolute text-center" style="bottom:0; z-index:10">' + message + '</div>');
fetchNewQuote();
console.log(message);
// Display non-support info on DOM
$(document.body).append(notice);
}
}
このコードは、最初にspeechSynthesis
がwindow
グローバルオブジェクトで使用可能かどうかを確認し、使用可能な場合はSYNTHESIS
変数に割り当てられます。 次に、使用可能な音声のリストを取得するための間隔を設定します。
ここでは、音声がまだロードされていないために最初の呼び出しで空の配列を返すSpeechSynthesis.getVoices()
の既知の非同期動作があるため、間隔を使用しています。 間隔は、ランダムな引用をフェッチして間隔をクリアする前に、音声のリストを確実に取得します。
これで、テキスト読み上げアプリが正常に完了しました。 ターミナルで次のコマンドを実行すると、アプリを起動できます。
- npm start
利用可能な場合、アプリはポート5000
で実行されます。
ブラウザでlocalhost:5000
にアクセスして、アプリを確認してください。
次に、再生ボタンを操作して、引用が話されているのを聞きます。
結論
このチュートリアルでは、Web Speech APIを使用して、Web用のテキスト読み上げアプリを作成しました。 Web Speech APIの詳細については、MDN WebDocsでいくつかの役立つリソースを見つけることができます。
アプリの改良を続けたい場合は、音量コントロール、音声ピッチコントロール、速度/速度コントロール、発声されたテキストの割合など、まだ実装して実験できる興味深い機能がいくつかあります。
このチュートリアルの完全なソースコードは、GitHubで入手できます。