Blog
Blog

Blog

Gemini APIで作るチャットボットの作り方

2025年8月07日

haino

こんにちは。クリエーターの灰野です。前回の投稿では、Gemini APIの初歩的な導入方法について扱いましたが、今回は、もう少し発展させた内容を扱いたいと思います。

今回は個人的に興味のあった、ウェブ音声APIの「SpeechRecognition」と「SpeechSynthesisUtterance」を使って、音声でGeminiと会話できるチャットボットを作ってみました。その名も「教えて!Geminiくん(関西弁バージョン)」です。なぜ関西弁かって?それは単純に面白かったからで、深い意味はありません(笑)

まぁ、こんな感じです。

下記にアップしておきましたので、よろしけれれば試してみてください。まず画面を開くと、マイクの許可を求めてきますので使用を許可してください。何か話しかけるとGeminiが音声で反応します。仮にGeminiの音声読み上げが機能しない場合は、お手数ですがブラウザを再起動してもう一度話しかけてみてください。

hml内にcssもJavaScriptも直接書かれているので、コード全体の確認は容易になっています。なお、APIの使用状況によっては公開を注意する場合もあります。ご了承ください。

教えてGeminiくん(関西弁バージョン)
https://axel.co.jp/oshietegemini/

では、順を追って開発の手順やコードの注意点をまとめていきたいと思います。

1. そもそもブラウザに実装されている「ウェブ音声API」ってどの程度使えるのか?

あまり積極的に使われている印象はありませんが、一部のモダンブラウザにはこの「ウェブ音声API」が実装されています。しかし、各ブラウザの対応状況や完成度を踏まえると実際のサービスで使うには難しい部分もありますし、その性能や信頼性は、端末のマイク性能やCPUの処理性能なども関係してきます。

個人的に使った印象だと、音声認識の「SpeechRecognition」については、Chromeを使う限り、思ったより使えるぞ!といった印象ですが、音声読み上げの「SpeechSynthesisUtterance」については、iOSに搭載のSiriと同程度?いや、それ以下とった感じで、読み間違いやおかしなイントネーションを連発してしまいます。また実装の仕方にもよるのだと思いますが「SpeechSynthesisUtterance」は動作がやや不安定です。

…とまぁ、いきなりネガティブなことを書いてしまいましたが、クリエーターとして、新しい技術や面白いことに興味を持って研究を続けるのは大切だと思っているので、完成度はともかく、ひとまず実装してみました!

2. HTMLの実装

今回のチャットボットはブログ投稿のために作成したものですので、UIに関しては特に装飾などなくシンプルにしてあります。必要なのは、直近で音声認識したテキストを表示するエリア、またGeminiから返されてきたテキストを表示するエリアが必要になります。その部分には、シンプルに「input type=”text”」を使うこともできますが、今回はキーボードを使った入力の機能を付与するつもりはないので、inputである必要性は特にありません。ということで「.textContent」を使用してpタグ内にテキストを流すことにしました。

また、必要に応じて音声認識を開始したり停止したりする必要があるのですが、そのことをユーザーに知らせるためのステータスを表示する機能も必要だと思います。加えて、これまでの会話の内容を記録するログも必須ですね。ということで、htmlは以下のようにしてみました。

<body>
  <h1>教えて!Geminiくん<span>(関西弁バージョン)</span></h1>
  <div class="status" id="status">⏳ 準備しています...</div>
  <div id="log"></div>
  <div class="speach"><b>あなた:</b><span id="query-text"></span></div>
  <div class="speach"><b>Gemini:</b><span id="response-text"><span></div>
  <p class="footnote">使用上の注意点:ブラウザはChromeを使用してください。音声認識と読み上げがうまく動作しない場合は、Chromeを再起動してください。</p>
</body>

3. JavaScriptの実装

まず音声認識についてですが、まずは以下の抜粋コードを確認してみてください。

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const synth = window.speechSynthesis;
let transcript = '';
let previousQuery = '';
let previousResponse = '';
let utter = null;

if(!SpeechRecognition) {
  alert('このブラウザは Web Speech API に対応していません。');
} else {
  const recognition = new SpeechRecognition();
  recognition.lang = 'ja-JP';
  recognition.interimResults = false;
  recognition.continuous = true;

  // 音声認識を開始
  recognition.onstart = () => {
    // speechSynthesisステータス確認
    if (synth.getVoices().length == 0) {
      synth.addEventListener('voiceschanged', () => {
        status.textContent = '🎤 何か話しかけてみてください!';
      });
    } else {
      status.textContent = '🎤 何か話しかけてみてください!';
    }
  };

  // 音声認識後の処理
  recognition.onresult = (event) => {
    transcript = event.results[event.results.length - 1][0].transcript;
    transcript = transcript.replace(' ', '');

最初に「const recognition = new SpeechRecognition()」のようにインスタンスを生成します。SpeechRecognitionは言語を指定できるので「recognition.lang = ‘ja-JP’」のように使用言語を指定します。英語やフランス語、中国語など、主要言語はカバーされているようです。

recognition.interimResults = false」ですが、これは音声認識の際に1文字ずつテキスト化する機能を切るために設定しています。この設定を「true」にしてしまうと文末の判定があいまいになり動作が安定しないため「false」にしてあります。「recognition.continuous = true」は、継続して音声認識させる必要があるため「true」とします。

また、SpeechRecognitionには幾つかイベントが定義されており適宜関数を定義することができます。初期化後の処理として「recognition.onstart」には、音声読み上げの準備ができたかどうかの判定を行い、準備完了後にユーザーに発話を促すためのステータス表示を変更する記述が書かれています。「recognition.onresult」には音声認識後の処理を記述しますが、Geminiから返されてきた内容を「transcript」という変数に流し込んでいます。なぜか冒頭に半角スペースが挿入されることがあるためここではreplace関数でそれを取り除いています。

SpeechRecognitionの仕様詳細は以下で確認してみてください。
https://developer.mozilla.org/ja/docs/Web/API/SpeechRecognition

音声認識後「recognition.onresult」にてGeminiにクエリを渡す必要がありますが、この非同期通信の部分では「fetch」を使ってシンプルに実装しました。

APIとの通信は、前回同様PHPのcomposerでインストールしたパッケージを使用します。通信時の注意点としては、まず音声認識したテキストをクエリとしてそのままGeminiに渡してしまうと詳細な回答が返されてきてしまうことが多いため、回答の形式を指定する必要があります。今回はカジュアルに関西弁で返して欲しかったので「大阪弁の回答を最大100文字程度で」のようにリクエストを追加した上で送信しています。開発するアプリの目的を考慮して、回答形式にも注文をつけるのがコツです。関西弁ではなく標準語でカジュアルに回答をもらいたい場合は、必要に応じて「会話のようなカジュアルな文体で最大100文字程度で」とかそんな感じでリクエストを追加します。

それと長文が返されてきた場合の注意点ですが、見出しの部分に「*」が多用されたテキストが返されてきます。そのような場合は「replaceAll」で適宜取り除くなどの加工が必要になりますので注意してください。

// Geminiにクエリーを送信
fetch('./api.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({ query: transcript + '(大阪弁の回答を最大100文字程度で)' })
})
.then(response => response.text())
.then(data => {
  status.textContent = '⏳ 聞き取り待機中...';
  data = data.replaceAll('*', ''); // レスポンスに追加されてしまう「*」を削除
  responseText.textContent = data;
  transcript = '';
  console.log(data);

  // 読み上げを開始
  speakText(data);
})
.catch(error => {
  console.log('通信エラー:', error);
});

会話が進行していくと上方にログとして会話が残るようにしてあるのですが、もう一つ加えた工夫として、自動スクロール機能を実装したことです。適宜スクロールしてやらないとGeminiとのやり取りが画面から消えてしまうので、この点も何気に重要です。

// 古い会話をログへ転記
if (previousQuery != '') {
  const yourLog = document.createElement('p');
  yourLog.textContent = 'あなた:' + previousQuery;
  yourLog.className = 'log-item';
  log.append(yourLog);
}
if (previousResponse != '') {
  const geminiLog = document.createElement('p');
  geminiLog.textContent = 'Gemini:' + previousResponse;
  geminiLog.className = 'log-item';
  log.append(geminiLog);
}

// 自動スクロール
requestAnimationFrame(() => {
  window.scrollTo({
    top: document.body.scrollHeight,
    behavior: 'smooth'
  });
});

読み上げ機能の実装箇所では、まず「const utter = new SpeechSynthesisUtterance(text)」のように初期化して、音声認識の時と同じく「utter.lang = ‘ja-JP’」のように言語の指定をします。こちらでも幾つかイベントが取得できますので、必要に応じて関数を定義できます。

SpeechSynthesisUtteranceの仕様詳細は以下で確認してみてください。
https://developer.mozilla.org/ja/docs/Web/API/SpeechSynthesisUtterance

読み上げ時の重要な注意点は、読み上げている最中には音声認識を一時的に停止する必要がある点です。この処理を実装しないと、読み上げ音声自体が認識され、次々とGeminiへクエリとして送信されていまい無限ループにハマります。そのためイベントに応じた関数を定義し読み上げ中には音声認識が機能しないように実装する必要があります。

// 読み上げ関数
function speakText(text) {
  synth.cancel();

  const utter = new SpeechSynthesisUtterance(text);
  utter.lang = 'ja-JP';
  synth.speak(utter);

  // ステータス管理
  utter.onstart = () => {
    // 音声認識停止
    recognition.stop();
    console.log('読み上げ開始');
  };
  utter.onend = () => {
    // 音声認識再スタート
    recognition.start();
    console.log('読み上げ終了');
  };
  utter.onerror = (e) => {
    console.error('読み上げエラー:', e.error);
  };
}

このため、スタート時の処理として「utter.onstart」にて「recognition.stop()」で音声認識を停止させ、終了時は「recognition.onend」にて「recognition.start()」のように音声認識を再スタートさせる必要があります。

それと、使っているうちに読み上げがされなくなることがあります。「SpeechSynthesisUtterance」のインスタンス生成に失敗しているのだと思うのですが、なぜかページのリロードだけでは復帰しないことがあります。そのような場合は、ブラウザの再起動が必要となります。

SpeechSynthesisUtterance」の信頼性と読み上げの精度が向上すれば、実際のサービスにも積極的に応用できそうですが、現状は仕方がないようです。それで、実際のサービスに音声読み上げを使用する場合は、他のクラウドサービス、例えば、Googleの「Text-to-Speech AI」などの利用が現実的だと思います。これについては今後の研究課題としたい思います。

4. PHPの実装

Gemini APIにクエリを送信するだけの機能を実装するため、10行程度のシンプルなコードで機能します。一つ注意点を挙げるとすれば、APIキーの書き方ですかね。一般的にPHP内にAPIキーを直接記述するのは良くないとされていますが、今回はLaravelなどのフレームワークは使用していません。そういう場合は「phpdotenv」というライブラリがお勧めです。簡単に環境変数を参照することができるようになります。「phpdotenv」はcomposerを使って簡単にインストール可能です。

composer require vlucas/phpdotenv

導入できたら、以下のように記述します。

.env

API_KEY=あなたが取得したAPIキー

テキストはクオーテーションなしで記述し文末の「;」は不要です。また以下のようにphpにコードを記述します。

api.php

<?php
  require_once './vendor/autoload.php';

  // 環境変数からAPIキーをロード
  $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
  $dotenv->load();
  $apiKey= $_ENV['API_KEY'];

  $client = Gemini::client($apiKey);
  if (isset($_POST['query'])) {
    $query = $_POST['query'];
    $result = $client->geminiFlash()->generateContent($query);
    echo htmlspecialchars($result->text(), ENT_QUOTES, 'UTF-8'); 
  } else {
    echo htmlspecialchars('Geminiとの通信に失敗しました!', ENT_QUOTES, 'UTF-8');
  }

いかがだったでしょうか?今回は、Geminiと会話しながら色々なことを教えてもらえるチャットボットを実装することできました。ブラウザに実装されているウェブ音声APIは、実際のサービスに使用するには、まだ慎重になる必要もありますが、今後の可能性を感じさせる部分もあります。特に「SpeechRecognition」はChromeの使用を前提とすれば、実用レベルの安定度だと感じます。特定用途のウェブシステムやツールには使用できるのではないでしょうか。

今後、ChatGPTやGeminiのようなLLMのAPIの使用は、WebサイトやECサイトのサポートチャットのような用途で、Webサイトに広く実装されていくのではないかと思います。中には今回のように音声でコミュニケーションできたら便利だと感じるようなシーンも増えていくかもしれません。各ブラウザのウェブ音声APIの実装がさらに進み、安定度が増していくよう期待したいところです!