学ぶ

SpeechSynthesisでJavaScriptのテキストを音声に変換する

Rene, Kabir Goel 
SpeechSynthesisでJavaScriptのテキストを音声に変換する

JavaScriptでテキストを音声に変換するには、SpeechSynthesisUtteranceを作成してwindow.speechSynthesis.speak()に渡します。再生はブラウザーが処理します。APIキー、サーバー、JavaScriptパッケージは不要です。

デバイスが提供する音声で十分なプロトタイプや読み上げ機能に適しています。デバイスを問わず同じ音声を使う音声エージェントや、通話へ音声をストリーミングする機能を作る場合は、ホステッドのテキスト読み上げAPIを使います。ブラウザーのSpeechSynthesisとCartesiaのAPIは、それぞれ別の音声生成手段です。

ブラウザーの音声合成の仕組み

window.speechSynthesisは、Web Speech APIで合成音声を制御するオブジェクトです。発話オブジェクトには、1回のリクエストのテキストと設定を格納します。speak()を呼ぶと発話がキューに追加されます。音声ファイルは返りません。1

ブラウザーはgetVoices()を通して現在のデバイスで使える音声を公開します。スクリプトの実行後に一覧が読み込まれることもあるため、最初に取得し、voiceschangedの発火時に更新してください。2 自分のノートPCにある音声名を固定しても、利用者のスマートフォンで使えるとは限りません。

1. 動作する読み上げデモを保存する

次のコードをspeech.htmlとして保存し、ブラウザーで開きます。短い文を入力し、音声が表示されたら選択してSpeakを押してください。リモートの音声サービスを使う音声もあるため、機密情報を含まないサンプルテキストを使います。3 コード例の表示文言は英語の原文を保持しています。

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>JavaScript text-to-speech demo</title>
	</head>
	<body>
		<h1>Read text aloud</h1>
		<p>
			<label for="text">Text to speak</label><br />
			<textarea id="text" rows="4" cols="30" maxlength="500">
Hello from your browser.</textarea
			>
		</p>
		<p>
			<label for="voice">Voice</label>
			<select id="voice"></select>
		</p>
		<button id="speak" type="button">Speak</button>
		<button id="stop" type="button">Stop</button>
		<p id="status" role="status" aria-live="polite">Ready.</p>

		<script>
			const text = document.querySelector("#text");
			const voiceSelect = document.querySelector("#voice");
			const speakButton = document.querySelector("#speak");
			const stopButton = document.querySelector("#stop");
			const status = document.querySelector("#status");
			let voices = [];
			let currentUtterance = null;

			if (
				!("speechSynthesis" in window) ||
				!("SpeechSynthesisUtterance" in window)
			) {
				status.textContent = "This browser does not support speech synthesis.";
				speakButton.disabled = true;
				stopButton.disabled = true;
			} else {
				const synth = window.speechSynthesis;

				function loadVoices() {
					const previousVoice = voiceSelect.value;
					voices = synth.getVoices();
					voiceSelect.replaceChildren(new Option("Browser default", ""));
					for (const voice of voices) {
						voiceSelect.add(
							new Option(`${voice.name} (${voice.lang})`, voice.voiceURI),
						);
					}
					if (voices.some((voice) => voice.voiceURI === previousVoice)) {
						voiceSelect.value = previousVoice;
					}
				}

				loadVoices();
				synth.addEventListener("voiceschanged", loadVoices);

				speakButton.addEventListener("click", () => {
					const transcript = text.value.trim();
					if (!transcript) {
						status.textContent = "Enter some text first.";
						return;
					}

					// Ignore callbacks from the utterance being replaced.
					currentUtterance = null;
					synth.cancel();
					const utterance = new SpeechSynthesisUtterance(transcript);
					const voice = voices.find(
						(item) => item.voiceURI === voiceSelect.value,
					);
					if (voice) {
						utterance.voice = voice;
						utterance.lang = voice.lang;
					} else {
						utterance.lang = document.documentElement.lang;
					}
					utterance.rate = 1;
					currentUtterance = utterance;
					status.textContent = "Starting speech...";
					utterance.onstart = () => {
						if (currentUtterance === utterance)
							status.textContent = "Speaking...";
					};
					utterance.onend = () => {
						if (currentUtterance !== utterance) return;
						currentUtterance = null;
						status.textContent = "Finished.";
					};
					utterance.onerror = (event) => {
						if (currentUtterance !== utterance) return;
						currentUtterance = null;
						status.textContent = `Speech failed: ${event.error}`;
					};
					synth.speak(utterance);
				});

				stopButton.addEventListener("click", () => {
					currentUtterance = null;
					synth.cancel();
					status.textContent = "Stopped.";
				});
			}
		</script>
	</body>
</html>

最初のテストを短くするため、このデモでは入力を500文字に制限しています。これはデモの制限であり、Web Speech APIの制限ではありません。長い文章は、サポートする各ブラウザーで別途テストしてください。

2. 音声の選択と再生を確認する

音声の選択欄には、利用できる各音声の名前と言語が表示されます。テキストの言語に合う音声を選びます。utterance.langを設定しても、文章は翻訳されません。

Speakはページの読み込み時ではなく、ボタンのクリックで実行します。利用者が再生を制御でき、自動再生に依存しません。再度Speakを押すと、繰り返しクリックしたリクエストをキューに追加するのではなく、現在のリクエストをキャンセルしてから次を開始します。Stopが呼び出すcancel()は、キュー内の発話を削除し、再生中の音声を停止します。4

currentUtteranceの確認により、キャンセル済みのリクエストから遅れて届くコールバックが、新しいリクエストの状態を上書きするのを防ぎます。このコードをコンポーネントに組み込む場合は、アンマウント時にvoiceschangedリスナーを解除してください。アンマウント時の再生停止は、そのコンポーネントがページの発話キューを管理している場合に限ります。

3. 音が出ない場合や動作が異なる場合の確認

症状確認事項
音声の選択欄にBrowser defaultしか表示されないvoiceschangedを待ちます。一覧が空のままなら、ブラウザーの対応状況とインストール済みのシステム音声を確認します。デモから既定の音声を要求することはできます。
Speakを押しても音が出ないデバイスの音量と出力先を確認し、言語の合う短い文を試します。状態メッセージに合成エラーが出ていないか確認してください。
PCにある音声がモバイルにない音声一覧はデバイスによって異なります。既定の音声という選択肢を残し、対象の実際のOSでテストします。
繰り返しクリックすると遅れて読み上げられるspeak()はリクエストをキューに追加します。新しいテキストで置き換える場合は、先に古いリクエストをキャンセルします。
インターネットに接続していないと失敗する選択した音声がリモートサービスを使っている可能性があります。ローカル合成と決めつけず、voice.localServiceを確認してオフラインでテストします。

読み上げ機能では、元のテキストを表示したままにし、キーボードで停止できるようにします。自動的に読み上げを始めたり、音声合成をスクリーンリーダー対応の代わりにしたりしないでください。

ホステッドTTS APIを使う場面

利用者のデバイスにある音声で十分で、そのデバイスでの再生だけが必要なら、ブラウザーの音声合成を選びます。生成した音声データ、プロバイダーが管理する同一の音声、ブラウザー外への音声配信が必要なら、ホステッドAPIを選びます。

要件ブラウザーのSpeechSynthesisCartesiaなどのホステッドTTS API
認証情報アプリケーションのAPIキーは不要API認証情報と利用権限のあるアカウントが必要
音声の選択ブラウザーとOSの音声一覧プロバイダーの音声ライブラリ、または使用許可のあるカスタム音声
音声の出力先ブラウザーで再生アプリケーションが音声を受け取り、再生、保存、転送する
オフライン動作音声とデバイスによるCartesia APIへのリクエストにはネットワーク接続が必要
テキストの処理音声によってローカルまたはリモートプロバイダーへ送信されるため、導入前にデータ要件を確認する

Cartesiaの音声合成では、バックエンドからAPIを呼び出してください。APIキーもバックエンドに置き、公開HTMLやクライアント側のバンドルに含めないでください。ブラウザーアプリケーションから認証済みバックエンドへテキストを送り、入力制限を適用して音声を返す構成にできます。訪問者がAPIクレジットを無制限に消費できないよう、そのエンドポイントにレート制限を設けます。

まずはCartesia Python SDKでWAVファイルを生成するガイドを試してください。このガイドはホステッドの生成経路を確認するもので、ブラウザー用のストリーミングプレーヤーではありません。対話型アプリケーションには、再生バッファリングと割り込み処理も必要です。音声エージェントのパイプライン全体を自作する前に、Cartesia Managed Agentsを使う場合と比較してください。

関連資料

脚注

  1. MDN、SpeechSynthesis。 ↩

  2. MDN、SpeechSynthesis.getVoices()。 ↩

  3. MDN、SpeechSynthesisVoice.localService。 ↩

  4. MDN、SpeechSynthesis.cancel()。 ↩

よくある質問

JavaScriptではAPIキーなしでテキストを音声に変換できますか?

はい。対応ブラウザーでは、window.speechSynthesisがデバイスで利用できる音声を使ってテキストを読み上げます。この例にCartesiaアカウントやAPIキーは必要ありません。利用できる音声と動作は、ブラウザーやOSによって異なります。

speechSynthesis.getVoices()が空の配列を返すのはなぜですか?

ブラウザーが音声をまだ読み込んでいない可能性があります。起動時にgetVoicesを呼び出し、voiceschangedを監視して一覧を更新してください。それでも空の場合は、ブラウザーの音声合成対応状況とOSにインストールされた音声を確認します。

SpeechSynthesisでMP3やWAVファイルを保存できますか?

SpeechSynthesisはブラウザーで音声を再生します。生成した音声をファイルやバイトストリームとして返すAPIはありません。保存や転送が必要なアプリケーションでは、音声出力エンドポイントを提供する音声サービスを使ってください。

ブラウザーの音声合成はオフラインで動作しますか?

音声によります。SpeechSynthesisVoice.localServiceは、音声がローカルとリモートのどちらのサービスから提供されるかを示します。すべての音声がオフラインで動作する、あるいはテキストがデバイス内に留まるとは限りません。対象デバイスで選択した音声を検証してください。