OpenAI が ChatGPT の高度な音声モードのリリースを遅らせている間に、私たちがどのように LLM 音声アプリケーションを構築し、それをインタラクティブ ブースに統合したかをお伝えしたいと思います。
2月末、バリ島では有名なバーニングマンの原理に基づいて企画されたランプフェスティバルが開催されました。その伝統に従い、参加者は独自のインスタレーションや芸術作品を制作します。
キャンプ 19:19の友人たちと私は、カトリックの告解室のアイデアと現在の法学修士課程の能力に触発され、誰でも人工知能と話せる独自の AI 告解室を構築するというアイデアを思いつきました。
当初私たちが思い描いていたのは次のようなものでした。
概念をテストし、LLM のプロンプトの実験を開始するために、私は一晩で単純な実装を作成しました。
このデモを実装するために、私は OpenAI のクラウド モデル ( Whisper 、 GPT-4 、 TTS)に全面的に依存しました。優れたライブラリspeech_recognitionのおかげで、わずか数十行のコードでデモを構築できました。
import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())
このデモの最初のテストの後、解決しなければならない問題がすぐに明らかになりました。
これらの問題を解決するには、適切なエンジニアリングまたは製品ソリューションを探すという選択肢がありました。
コーディングを始める前に、ユーザーがブースとどのように対話するかを決める必要がありました。
ブース内の新しいユーザーを検出するために、ドア開閉センサー、床重量センサー、距離センサー、カメラ + YOLO モデルなど、いくつかのオプションを検討しました。背面の距離センサーは、ドアが十分に閉まっていない場合などの偶発的なトリガーを排除し、重量センサーとは異なり複雑な設置を必要としないため、最も信頼性が高いように思われました。
会話の始まりと終わりを認識するという課題を回避するために、マイクを制御するための大きな赤いボタンを追加することにしました。このソリューションにより、ユーザーはいつでも AI を中断することもできます。
リクエストの処理に関するフィードバックを実装することについては、さまざまなアイデアがありました。私たちは、マイクを聞いている、質問を処理している、または回答しているなど、システムが何を実行しているかを示す画面を備えたオプションを選択しました。
また、古い固定電話を使ったかなりスマートなオプションも検討しました。ユーザーが電話を取るとセッションが開始され、ユーザーが電話を切るまでシステムがユーザーの話を聞きます。ただし、電話からの声ではなくブースがユーザーに「応答」する方が本物らしくなると判断しました。
最終的に、最終的なユーザーフローは次のようになりました。
Arduino は距離センサーと赤いボタンの状態を監視します。すべての変更は HTTP API 経由でバックエンドに送信され、システムはユーザーがブースに入ったか出たか、マイクのリスニングを有効にする必要があるか、応答の生成を開始する必要があるかを判断できます。
Web UI は、ブラウザで開かれる Web ページであり、バックエンドからシステムの現在の状態を継続的に受信してユーザーに表示します。
バックエンドはマイクを制御し、必要なすべての AI モデルと対話し、LLM 応答を音声で伝えます。アプリのコア ロジックが含まれています。
Arduino のスケッチをコーディングし、距離センサーとボタンを適切に接続し、ブース内ですべてを組み立てる方法については、別の記事で取り上げます。技術的な詳細には触れずに、簡単に内容を確認してみましょう。
私たちは Arduino、より正確には Wi-Fi モジュールを内蔵したモデルESP32を使用しました。マイクロコントローラーは、バックエンドを実行しているラップトップと同じ Wi-Fi ネットワークに接続されていました。
使用したハードウェアの完全なリスト:
パイプラインの主なコンポーネントは、音声テキスト変換 (STT)、LLM、およびテキスト音声変換 (TTS) です。各タスクでは、ローカルとクラウドの両方でさまざまなモデルが利用できます。
強力な GPU が手元になかったため、クラウド ベースのモデル バージョンを選択することにしました。このアプローチの弱点は、良好なインターネット接続が必要なことです。ただし、すべての最適化後のインタラクション速度は、フェスティバルで利用したモバイル インターネットでも許容範囲内でした。
それでは、パイプラインの各コンポーネントを詳しく見ていきましょう。
最近の多くのデバイスは、以前から音声認識をサポートしています。たとえば、 Apple Speech API はiOS と macOS で利用でき、 Web Speech API はブラウザで利用できます。
残念ながら、 WhisperやDeepgramに比べて品質が非常に劣っており、言語を自動的に検出することはできません。
処理時間を短縮するには、ユーザーが話しているときにリアルタイムで音声を認識するのが最善の選択肢です。以下に、実装方法の例を含むプロジェクトをいくつか示します。
私たちのラップトップでは、このアプローチを使用した音声認識の速度はリアルタイムには程遠いことが判明しました。いくつかの実験を行った後、私たちは OpenAI のクラウドベースの Whisper モデルを採用することにしました。
前のステップの Speech To Text モデルの結果は、ダイアログ履歴とともに LLM に送信するテキストです。
LLM を選択する際に、GPT-3.5、GPT-4、Claude を比較しました。重要な要素は特定のモデルではなく、その構成であることがわかりました。最終的に、他のモデルよりも回答が気に入った GPT-4 に落ち着きました。
LLM モデルのプロンプトのカスタマイズは、独立した芸術形式になっています。必要に応じてモデルを調整する方法については、インターネット上に多くのガイドがあります。
モデルが魅力的かつ簡潔でユーモラスに応答するようにするには、プロンプトと温度設定を徹底的に実験する必要がありました。
LLM から受信した応答を Text-To-Speech モデルを使用して音声化し、ユーザーに再生します。このステップが、デモにおける遅延の主な原因でした。
LLM は応答にかなり時間がかかります。ただし、ストリーミング モード (トークン単位) での応答生成をサポートしています。この機能を使用すると、LLM からの完全な応答を待たずに、受信した個々のフレーズを音声で読み上げることで、待機時間を最適化できます。
ユーザーが最初のフラグメントを聞いている間の時間を利用して、LLM からの応答の残りの部分を処理する際の遅延を隠します。このアプローチにより、応答の遅延は最初にのみ発生し、約 3 秒です。
async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }
あらゆる最適化を施しても、3~4 秒の遅延は依然として大きな問題です。私たちは、ユーザーが応答がハングしているという感覚を抱かないように、フィードバック付きの UI に配慮することにしました。私たちはいくつかのアプローチを検討しました。
私たちは、バックエンドをポーリングし、現在の状態に応じてアニメーションを表示するシンプルな Web ページという最後のオプションを選択しました。
私たちの AI 告白室は 4 日間開催され、何百人もの参加者を集めました。OpenAI API に費やした金額はわずか 50 ドルほどでした。その代わりに、かなりの肯定的なフィードバックと貴重な感想をいただきました。
この小規模な実験により、リソースが限られており外部条件が厳しい場合でも、直感的で効率的な音声インターフェースを LLM に追加できることが示されました。
ちなみに、 GitHubで利用可能なバックエンドソース