【個人開発】ChatGPT × Text-to-Speech(Google)で知育サービスを作ってみた
概要
久しぶりに個人開発をしました!
娘が「コペル」という幼児教室に通っています。
その幼児教室で「コペルギネス」というゲームがあります。
ゲームの内容は下記のようなゲームになります。
50個の(食べ物・動物等)絵が描いてある表を見て、順に作ったお話を先生がお話してくれます。それを2回聞いて、覚えます。自分の手元にも同じカードが50個あるのでそれを子供が1人で回答用の表に順番通りに並べて貼っていくと言うゲームです。50個のカードのうち何個同じ場所に置けていたかを制限時間内に競います。正解した数が多かった人の勝ちです。
単語と単語にお話をつけ、繋がったストーリーでイメージすることで記憶しやすくする効果があるそうです。
このコペルギネスの練習をするとき、あらかじめ物語を作っておかないと、同じ絵を登場させてしまったり、答えを覚えていなかったりと結構大変なので、それを自動化するWebアプリを作りました!
ChatGPTとGoogleが提供するText-to-Speechを掛け合わせて、このコペルギネスのゲームを再現するWebアプリです。
作ったもの
- ① ランダムな絵柄を選定する
- ② その絵を元にChatGPTにお話しを作成してもらう
- ③ ChatGPTが作成したお話をText-to-Speechに話してもらう
- ④ 娘がお話を聞いて順番に並び替える
- ⑤ 何問正解したかを表示、回答を表示
- ⑥ ①に戻る
使っている技術・API
使っている技術はフロントエンドの技術のみで実装しており、Nuxt3をVercelにホスティングしております。
ChatGPTとText-to-SpeechのAPIはクライアントサイドから呼び出す構成にしております。
技術
- Nuxt3
- Vercel
API
- ChatGPT API
- Text-to-Speech(Google)
実装
ChatGPT
コードはこんな感じで書いています。
const postChatPTMessage = async () => {
const config = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHAT_GPT_KEY}`
}
}
const data = {
"model": "gpt-4",
"messages": [{ "role": "system", "content": gpt_message.value }],
"max_tokens": 600
}
await axios.post("https://api.openai.com/v1/chat/completions", data, config)
.then(response => {
gpt_response_message.value = response.data.choices[0].message.content
})
.catch(error => {
console.error(error)
})
};
質問(プロンプト)
あなたは子ども向けのお話しを作るプロの構成作家です。あなたの仕事は与えられた「単語」を使って物語を作成することです。
以下の「制約条件」を厳守して物語を作成してください。
制約条件"""
・単語は「,」区切りで複数与えられます
・与えられた単語をすべて使って物語を作成してください
・単語は必ず与えられた順序どおりに使ってください
・物語は200文字以内で作ってください
・物語の中で必ず与えられた単語をすべて使ってください
・物語の中で単語はそれぞれ1度のみ使ってください
・物語が完成したら、最後にもう一度すべての単語を順番に確認し、それぞれの単語が物語に正しく、かつ順序通りに、一度だけ含まれているかをチェックして、誤りがあれば最初からやり直してください
・最後に、完成した物語のみ出力してください
”””
単語"""
はなび,さんりんしゃ,ヘビ,ノート,プリン,ランドセル,アイスクリーム,いす,虫眼鏡,ひまわり
"""
回答(物語)
夏祭りの日、はなびを見に行こうと決めた小さな少年は、さんりんしゃに乗って家を出た。途中でヘビに出くわし、驚いたが、勇敢に通り過ぎた。少年が公園に着くと、ノートとペンを取り出し、はなびの美しさを絵に描き始めた。 次に少年が出会ったのは、お店で売っているプリンだった。彼のお母さんが作ってくれたお弁当を食べた後、プリンを笑顔で食べた。満足感でいっぱいの彼は、新学期のために買ったばかりのランドセルにプリンの包み紙を大切にしまった。 夕方になると、アイスクリームを手にした子供たちが集まり始めた。少年もいすに座り、アイスクリームを楽しみながら、暗くなるのを待ち始めた。虫眼鏡で太陽の落ちるのを見て、はなびの時間が近づいていることを知った。そして、ひまわり畑を背景に、彼の描いたはなびが美しく輝いた。
Text-to-Speech
こちらでデモを体験することができます。かなり自然な日本語で話すことができるようになっておりました。
料金
GCPのサービスの料金形態はしっかりと調べておかないと事故になることがあるので、事前に調べました。
Text-to-Speech の料金は、音声への合成のためにサービスに送信された文字数に基づいて、月単位で請求されます。WaveNet 音声の最初の 100 万文字は、毎月無料です。WaveNet 以外の標準音声では、毎月最初の 400 万文字が無料です。Text-to-Speech は無料枠以降 100 万文字ごとに課金されます。
Text-to-Speech の料金は、音声への合成のためにサービスに送信された文字数に基づいて、月単位で請求されます。Text-to-Speech を使用するには課金を有効にする必要があります。使用量が 1 か月間に無料で使用できる文字数を超えると、自動的に課金されます。合計文字数を追跡する方法については、API 使用状況のモニタリングをご覧ください。料金は文字ごとに計算されます。
入力文字列の合計文字数は、課金を目的としてカウントされます。これにはスペースも含まれます。mark を除くすべての音声合成マークアップ言語(SSML)タグも文字数に含まれます。たとえば以下に示す入力文字列は、SSML タグ、改行、スペースを含めて 79 文字とカウントされます。
特徴 | 1 か月あたりの無料枠 | 無料の使用量上限に到達した場合の料金 |
---|---|---|
Neural2 音声 | 0 ~ 100 万バイト | 1 バイトあたり 0.000016 米ドル(100 万バイトあたり 16 米ドル) |
多言語音声(プレビュー)音声 | 0 ~ 100 万バイト | 1 バイトあたり 0.000016 米ドル(100 万バイトあたり 16 米ドル) |
Studio(プレビュー)の音声 | 0 ~ 10 万バイト | 1 バイトあたり 0.00016 米ドル(100 万バイトあたり 160 米ドル) |
標準音声 | 0〜400 万文字 | 1 文字あたり 0.000004 米ドル(100 万文字あたり 4 米ドル) |
WaveNet 音声 | 0〜100 万文字 | 1 文字あたり 0.000016 米ドル(100 万文字あたり 16 米ドル) |
https://cloud.google.com/text-to-speech/pricing?hl=ja を参照
と、書いておりました。今回は「Neural2」の音声を採用しており、100万バイトまでは無料枠があります。
サンプルで作成した物語のバイト数が 552バイトなので 1,800回は物語が生成できるので、個人で使用するのみであれば無料枠で収まりそうです◎
許可するドメインを指定する
フロントエンドに埋め込むAPIキーのみで、Text-to-SpeechのAPIはコールできるため、APIキーさえあれば第三者が実行することができてしまいます。なので実行できるドメインをGCPの設定で指定する必要があります。
GCPのコンソールを開く → プロジェクトを選択 → APIのサービス → 認証情報 → APIキー → 該当のキーを選択して 許可するドメインを保存
金額のアラートを設定しておく
GCPのAPIは金額に達した場合APIを止めるということができないため、金額に達した場合アラートが出るようにしておく必要があります。なので必ず設定しておきましょう。
実装
const getToSpeachAudioData = () => {
const url = "https://texttospeech.googleapis.com/v1/text:synthesize?key=" + GOOGLE_SPEACH_TO_TEXT_KEY;
const data = {
"input": {
"text": `${物語の文字列}`
},
"voice": {
"languageCode": "ja-JP",
"name": "ja-JP-Neural2-B"
},
"audioConfig": {
"audioEncoding": "MP3",
"speaking_rate": "1.00",
"pitch": "0.00"
}
}
const otherparam = {
headers: {
"content-type": "application/json; charset=UTF-8"
},
body: JSON.stringify(data),
method: "POST"
}
fetch(url, otherparam)
.then(data => { return data.json() })
.then(res => {
audioContent.value = res.audioContent
speachToText();
})
.catch(error => alert(error))
const speachToText = () => {
try {
const blobUrl = base64ToBlobUrl(audioContent.value)
audio.value = new Audio()
audio.value.src = blobUrl
audio.value.play()
isAudioEnd.value = true
isAudioNow.value = true
audio.value.addEventListener("ended", () => {
isAudioNow.value = false
});
} catch (e) {
console.log(e)
}
}
const base64ToBlobUrl = (base64: any) => {
var bin = atob(base64.replace(/^.*,/, ''))
var buffer = new Uint8Array(bin.length)
for (var i = 0; i < bin.length; i++) {
buffer[i] = bin.charCodeAt(i)
}
return window.URL.createObjectURL(new Blob([buffer.buffer], { type: "audio/wav" }))
};
完成
まとめ
- 娘は結構楽しんでくれております🙌
- ChatGPTの精度がイマイチ...
- 同じ単語を2回使ったりする
- 絵柄の順番が前後する
- 使っていない絵柄があったりする
- 物語の自動生成以外にも、先生が物語を作ってそれを子どもが答える機能とかもあれば使われそう
- デザインをもう少しきちんと整えた方が良い
- 子どもが使っていて楽しい要素を入れた方が良さそう
- 音を出したり、アニメーションを追加したり
もし子どもに使ってみたい!という方がいましたら、ご連絡いただければURLを共有しますのでご連絡いただければと思います!
参考にしたサイト
下記のサイトを参考にさせてもらいながら、作ることができました。ありがとうございました!
ChatGPTのAPIをJSから呼び出す方法
GCP 入門 (8) - Text-to-Speech API をJavaScriptから使う
Discussion
@にっしー さんがアドバイスをくれて、ChatGPTのプロンプトを見直しました!
(人間が「ピンクの象を連想しないでください」というとつい連想してしまうのと同じ)
とのことで、上記の2つを見直して、プロンプトを実行することで精度が上がりました!🙌
@にっしー さん、アドバイスありがとうございました🙏
子育て個人開発に貢献できてなによりです! 🌟