個人開発のカレンダー | Advent Calendar 2022 - Qiita ということで、この記事では以前作ったけど全くイケてる感がなかったのでポシャったサービスで使った技術スタックを供養したいと思います。
作ったのは、「チャット機能+読み上げ機能」「画面共有機能」です。
このうち、チャット機能はFirestoreを使いました。
NoSQLで書き込みをリアルタイム受信できるので、それを画面に表示したりなんだりすればいいですね。
画面共有には、WebRTCという技術を使うとWebでも簡単に実装できます。
「WebRTC SaaS」あたりのキーワードで調べるとTwilio, SkyWay, Agora.ioなどがでてきます。
私はこのうちSkyWayを採用しました。
ドキュメントにチュートリアルもあるので、画面共有くらいならすぐに作ることができます、
機能 | SkyWay(アプリやWebサービスに、ビデオ・音声通話をかんたんに導入・実装できるSDK)
で、音声を再生したいです。
これには Text to Speech ができそうな気がします。
Text-to-Speech: 自然な音声合成 | Google Cloud
今回はこれを使います。ちなみにtext-to-speechについて調べるとspeech-to-textがいっぱい引っかかってぐぬぬってなります。
プロジェクトを作ってAPIの許可云々の設定は概ね他のGoogle CloudのAPIと一緒です。
@google-cloud/text-to-speech - npm このNPMパッケージを使ってコードを書いていきます。
このAPIはNode環境じゃないと動かないと思います(もう忘れた)
ということでGoogle Cloud Functionsにdeployしていきます。
const functions = require("firebase-functions");
exports.tts = functions.https.onRequest(async (request, response) => {
const client = new textToSpeech.TextToSpeechClient();
const text = request.query.text;
// 危ないので本番に出す前にドメインを限定しよう
response.set("Access-Control-Allow-Origin", "*");
response.set("Access-Control-Allow-Credentials", "true");
// Preflight check にはこっちを返してレスポンスを終了する
if (request.method === "OPTIONS") {
// Send response to OPTIONS requests
response.set("Access-Control-Allow-Methods", "GET");
response.set("Access-Control-Allow-Headers", "Authorization");
response.set("Access-Control-Max-Age", "3600");
response.status(204).send("");
return;
}
// Text2Speech API へのRequestを組み立てる
const apiRequest = {
input: { text },
// Select the language and SSML voice gender (optional)
voice: { languageCode: "ja-JP", ssmlGender: "NEUTRAL" },
// select the type of audio encoding
audioConfig: { audioEncoding: "MP3" },
};
const apiResponse = await client.synthesizeSpeech(apiRequest);
response.send(apiResponse);
});
こんな感じのコードをデプロイすると、 /tts?text=HelloWorld とかでText to speech APIに処理され、MP3を含むデータが帰ってくるようになります。
フロントエンド側で以下のようなコードを書きます。
Firestoreからメッセージを受信したときのイベントに書くイメージです。
const res = await fetch(BASE_URL + `tts?text=${text}`).then((res) => res.json())
const u8 = new Uint8Array(res[0].audioContent.data)
return arrayBufferToBase64(u8)
ここでかなりハマったのが、 fetch したときの response は下記のような特徴があるようです。
- 配列で、かつその 0番目のインデックスに目的のデータが格納されている
- レスポンスはArrayBufferなのでBase64に変換する必要がある
Google text-to-speech api のドキュメントではbase64がレスポンスになると書いてあるのでnpmパッケージのほうでArrayBufferになっているのでしょうか…
new Audio('data:audio/mp3;base64' + voiceBase64).play()
とすればめでたく再生できるようになりました!
プログラムにユーザーが作成したテキストを喋らせるというアイデアは使えそうなのでここに供養しておきます。RIP