🤙

【ネタアプリ】SpeechToText と TextToSpeech で通話を試みる

2024/06/02に公開1

はじめに

最近ブラウザに Web Speech API なるものがあることを知り、何か使い道ないかなと考えていたら SpeechToText (音声認識) と TextToSpeech (音声合成) を交互に組み合わせることで擬似的に通話することができるのでは?と思い、ノリでつくってみました。

イメージはこんな感じ👇

つくったもの

今回作成したアプリはこちらから試せます 🔽

https://sttts-call.deno.dev

リポジトリはこちら 🔽

https://github.com/wagao29/sttts-call

トップ画面

トップ画面ではアクセスしたタイミングで Room ID が自動的に採番され、名前の入力 (ルーム内で一意である必要あり) と使用する言語の選択を行うとルームに入ることができます。


トップ画面

ルーム画面

ルーム画面は Google Meet 風の UI になっており、ミュートを解除すると音声認識が開始され、会話のフレーズが認識されたタイミングで相手側に送信されます。他の人が発言するとチャットボックス部分に受信したテキストが表示され、音声合成による読み上げが実行されます。
また、右下の Copy Room URL ボタンを押してコピーされた URL を共有することで未参加の人を同じルームに招待することができます。


ルーム画面

技術スタック

Deno

ランタイムは Deno を使用しました。細かい設定が必要ないのと Deno Deploy ですぐにデプロイできるのは開発体験として良かったです。

Hono

フレームワークは最近流行りの Hono を使ってみました。今回は簡単なアプリなので軽く触った程度ですが、シンプルで扱いやすくヘルパーやミドルウェアも充実していて良い感じでした。

JSR

こちらも流行りのパッケージレジストリで、ちょうどつい最近リリースされた Hono v4.4.0 で JSR がサポートされたとのことだったので JSR を使ってみました。今後は deno.land/x から JSR への移行の流れがあるようなので使える場合は JSR を使っておくのが良さそうです。

UI

UI は Hono の JSX を使ってコンポーネントベースで開発し、スタイリングに関しては Hono がビルドインでサポートしている CSS in JS を使用しました。

Client Side JS

今回作成したアプリはクライアント側の実装が必要でしたが、そんなに複雑にはならなそうだったので素の JS を直書きしていくスタイルで実装しました。これらの script は静的ホスティングをしておき、JSX から変換された html ファイルから読み込まれる形になっています。

今回はやりませんでしたが、一応以下のスクラップで挙げられているような方法でクライアント側も TypeScript 使って開発できそうです。
https://zenn.dev/codehex/scraps/3ebfe341118c9a

WebSocket

SpeechToText でテキスト化されたデータをリアルタイムで双方向に送受信するために WebSocket を使用しています。Deno では Deno.upgradeWebSocket() で WebSocket の接続を受けることができるのですが、Hono でも WebSocket Helper としてサポートされていたので問題なく使用できました。

実装

ルーム

今回はチャットアプリなどによくあるルームという形で複数人 (最大4人) が同時にやり取りできるようにしました。基本的には以下のような構造のデータを WebSocket を通じて送受信することで、入退室やメッセージを通知できるようになっています。

{
  type: "message" | "join" | "leave";
  name: string;
  content?: string;
}

音声認識

音声認識は SpeechRecognition を使用しており、recognition.start() で音声認識開始、result イベントで認識した文章を取得、SpeechRecognitionResult.isFinal で認識が確定したかを判別してデータを送信しています。

const recognition = new SpeechRecognition();
recognition.lang = lang;
recognition.interimResults = true;
recognition.continuous = true;

recognition.onresult = (event) => {
  const transcript = event.results[event.results.length - 1][0].transcript;
  if (!transcript) return;
  updateMessage(name, transcript);
  if (event.results[event.results.length - 1].isFinal) {
    sendMessage(transcript);
  }
};

音声合成

音声合成に関しては、SpeechSynthesisspeak() メソッドで受信したメッセージを読み上げています。読み上げについて色々オプションが設定できますが、ここでは言語と声の指定のみをしています。

const uttr = new SpeechSynthesisUtterance(text);
uttr.lang = lang;
uttr.voice = voice;
speechSynthesis.speak(uttr);

また、SpeechSynthesis には同じ言語でも複数の声の種類が存在し、getVoices() で使用可能な声を取得できたため、ルーム内の人ごとに異なる声をランダムに割り当てることで発言者が区別できるようにしました。

結論

Pros

  • テキストデータなので通信量的には有利?
  • 本当の声は聞かれないため匿名性が高い

音声データを直接やり取りするのに比べて通信環境が悪いところでも使えるという点を強みとしたかったのですが、よくよく調べるとブラウザの音声認識はサーバで処理しているようなのであまり変わらなそうでした...(受信データ量に関しては有利くらい?)

Chrome など一部のブラウザーでは、ウェブページ上で音声認識を使用するとサーバーベースの認識エンジンが使用されます。音声を認識処理するためにウェブサービスへ送信するため、オフラインでは動作しません。(https://developer.mozilla.org/ja/docs/Web/API/SpeechRecognition)

Cons

  • ラグが大きい
  • 音声認識の精度の問題

当たり前の結果ですがやはりラグは大きかったです。改善の余地があるとすれば、今回は音声が文章として認識が確定されたタイミングで送信していますが、確定される前にもリアルタイムで認識された文章を返してくれるのでそれらをどんどん送信することでラグを小さくできるかもしれません。(認識が確定する前だと直前の文章と変わったりすることがあるので送信の制御が難しそうですが)

音声認識の精度に関しては、結構ハキハキ喋らないと正しく認識されなかったり、間違った文章として認識されてしまうので普段の電話するテンションでは使えなそうという感じでした。

さいごに

軽い気持ちでつくり始めたらなんやかんや作り込んでしまいました、、
頑張ればこのアプリを使ってコミュニケーションを取れなくはないくらいの出来にはなった思うので、ゲーム感覚で是非誰かと使ってみてください!w

https://sttts-call.deno.dev

Discussion

masd_jpmasd_jp

めちゃめちゃ面白いコンセプトだと思いました。
今は中間データにテキストを送っていますが、tokenizerを使うとより圧縮できそうですね。
あとは、音声はあまり通信量がかからないので、ビデオ通話をこれでやれると革命的で面白いと思いました。