ブラウザ版のらきゃっとシステム：Webブラウザの音声認識・読み上げAPIを使ってVRChatで声バレなしにハンズフリー会話する

# すぐ使いたい人へ
:white_check_mark: すぐ使いたい人はこちら https://speech-agent.herokuapp.com/

## この記事では何をやっているのか

「**WebブラウザでWeb Speech APIを使ってマイクの声を認識・テキスト化して、声の入力が終わったらWeb Speech APIでそのテキストを読み上げることを繰り返す**」というWebアプリです。

このWeb Speech APIによる読み上げはブラウザからの音声出力なので、Windowsの設定→システム→サウンド→アプリの音量とデバイスの基本設定から**ブラウザの音声出力先をNetDuetto（仮想ライン入出力）にして、VRChat側でもMicrophoneの設定をNetDuettoにすればブラウザで読み上げられた音声がVRChatで流れます！**（ついでにブラウザ上で流れている任意の音もVRChatで流せる/流れてしまう）

### 実現までの間に試みて頓挫した他の手法について

- Python3とGoogle Cloud Speech APIでマイクの音声を認識させて認識結果をSoftalkに喋らせる

録音した音声データの音声認識はどんなに短くても1リクエストとカウントされてしまうのでうかつに話せない。ストリーミング音声認識は1分という制限がある。そこで喋り始めてからストリーミングリクエストを投げるようにしたかったがPython2による先人のコードをPython3に移植するところで頓挫（そして喋るごとに増えていく文章をSoftalkに対していつ投げればよいのかも問題となった、一定時間ごとに差分をとるとか？）

- SPTKをWindowsにインストールしてPythonから叩いて肉声からロボ声を作る

nmakeしたけどbinの中身がボロボロ抜けてしまった（うまくいった方のコメントお待ちしてます！！！）　リアルタイムで音声ストリームを受けてピッチをパルス音源に置き換えロボ声にする最有力候補だったのだが…
Linux環境ならSPTKのインストールは容易な一方、VRChatの動作や仮想ライン出力などあまりにタスクが増えてしまう、Ubuntu on WSLはWindows側との音声ファイルのやり取りが面倒、Cygwinもあまりスムーズではなさそう
ええいアバターも作りたいのにそこまでやってられん　気が向いたら試します

- PyWorldとJupyter Notebook（IPython）で肉声をロボ声に変換する

特徴量検出をかけるとめちゃくちゃ掠れ声になってしまい聞き取るどころの話ではなくなった
（私の発声に問題があるのか録音方法に問題があるのか）
本家WORLDをコンパイルして試すのはまた気が向いたら試してみます…

## 音声認識・読み上げページを作る

[【入門サンプル】Web Speech API を使ってブラウザと音声でやり取りする](http://okakacacao.wpblog.jp/technology/web-speech-api) が大変参考になりました。

その大部分を継承しつつ、コンソールのログを出力しないように改変し、設定を追加しました。さらにAPI仕様変更に対応するため、ボタンクリックでspeak()させてから音声認識を開始するようにしました。
https://github.com/Napier-JP/SimpleSpeechAgent に最小構成のものをready-to-deployな状態で置いてあります。
https://speech-agent.herokuapp.com/ の中身は https://github.com/Napier-JP/SpeechAgent へ。

```html:index.html

<!DOCTYPE html>
<html lang="jp">

<head>
	<meta charset="UTF-8">
	<title>Speech-to-Talkback</title>
	<script>
		let talkbackEnabled = false;

		function talkback() {
			// 利用するWebAPIをインスタンス化する
			window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
			const recog = new SpeechRecognition();
			const synth = new SpeechSynthesisUtterance();

			// 設定項目
			const pitchSetting = 1.0;  // ピッチ 0(min) - 2(max), default 1.0
			const volumeSetting = 1.0; // 音量 0(min) - 1(max), default 1.0
			const rateSetting = 1.0; // 話す速さ 0.1(min) - 10(max), default 1.0
			// 設定項目終わり

			const defaultInput = "";
			let input = defaultInput;
			recog.lang = "ja-JP";
			synth.lang = "ja-JP";
			synth.pitch = pitchSetting;
			synth.volume = volumeSetting;
			synth.rate = rateSetting;
			// 聞き取り中に、マイクからの入力があったときのイベント
			recog.addEventListener("result", e => {
				// 入力結果をテキストで取得し、それをグローバル変数に格納し、聞き取りを終了する
				input = e.results[0][0].transcript;
				recog.stop();
			});
			// 聞き取りを終了したときのイベント
			recog.addEventListener("end", () => {
				// 入力結果が初期状態の場合は聞き取りが自動終了したと判断し、聞き取りを再開する
				if (input === defaultInput) {
					recog.start();
				}
				// 入力結果に値があればそれを読み上げ、入力結果を初期化する
				else {
					synth.text = input;
					document.querySelector("#recognizedText").textContent = input;
					input = defaultInput;
					speechSynthesis.speak(synth);
				}
			});
			// 読み上げを終了したときのイベント
			synth.addEventListener("end", () => {
				// 聞き取りを再開する
				recog.start();
			});
			// 聞き取りを開始する
			recog.start();
		}

		function initSpeak() {
			let initSynth = new SpeechSynthesisUtterance("自動読み上げ　有効");
			initSynth.lang = "ja-JP";
			speechSynthesis.speak(initSynth);
			talkback();
		}

	</script>
</head>

<body>
	認識内容：<div id="recognizedText"></div><br>
	<button onclick="initSpeak();">最初に一度押してください</button><br>
	（APIの仕様変更によりユーザーがボタンを押して許可することが必要になりました。「自動読み上げ有効」と読み上げられます）<br>
</body>

</html>
```

## 実現できたこと、できなかったこと

 :ok: 画面を切り替えることなく常にマイクでの音声入力を待ち受けてくれて、しかも無料かつ高精度に認識してくれるものが実現できた 
 :ok: 一度Webアプリをdeployしたら、音声出力先とマイクさえ準備すれば他のWin10機に移行が容易(Webサイトなので)
 :ng: 喋り終わらないと読み上げ始めないので、数秒のタイムラグは依然として残る。タイムラグを短くし、また誤認識を減らすためにも、言葉を切りつつ話す必要がある

## 参考サイト
[【入門サンプル】Web Speech API を使ってブラウザと音声でやり取りする](http://okakacacao.wpblog.jp/technology/web-speech-api)
[[HTML5] Web Speech APIに入門](https://www.yoheim.net/blog.php?q=20140701)