paint-brush
独自の AI を構築する方法 告白: LLM に声を加える方法by@slavasobolev
661
661

独自の AI を構築する方法 告白: LLM に声を加える方法

Iaroslav Sobolev11m2024/07/14
Read on Terminal Reader

2月末、バリ島では有名なバーニングマンの原則に従って企画された[ランプ](https://lampu.org/)フェスティバルが開催されました。私たちはカトリックの告解室のアイデアと現在の法学修士課程の能力に触発され、誰でも人工知能と話せる独自のAI告解室を建設しました。
featured image - 独自の AI を構築する方法 告白: LLM に声を加える方法
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item

OpenAI が ChatGPT の高度な音声モードのリリースを遅らせている間に、私たちがどのように LLM 音声アプリケーションを構築し、それをインタラクティブ ブースに統合したかをお伝えしたいと思います。

ジャングルのAIと話す

2月末、バリ島では有名なバーニングマンの原理に基づいて企画されたランプフェスティバルが開催されました。その伝統に従い、参加者は独自のインスタレーションや芸術作品を制作します。


キャンプ 19:19の友人たちと私は、カトリックの告解室のアイデアと現在の法学修士課程の能力に触発され、誰でも人工知能と話せる独自の AI 告解室を構築するというアイデアを思いつきました。


当初私たちが思い描いていたのは次のようなものでした。

  • ユーザーがブースに入ると、新しいセッションを開始する必要があると判断します。


  • ユーザーが質問すると、AI がそれを聞いて答えます。私たちは、誰もが自分の考えや経験をオープンに話し合える、信頼できるプライベートな環境を作りたかったのです。


  • ユーザーが部屋を離れると、システムはセッションを終了し、会話の詳細をすべて忘れます。これは、すべてのダイアログを非公開にするために必要です。

コンセプトの証明

概念をテストし、LLM のプロンプトの実験を開始するために、私は一晩で単純な実装を作成しました。

  • マイクを聞いてください。
  • 音声テキスト変換 (STT)モデルを使用してユーザーの音声を認識します。
  • LLM経由で応答を生成します。
  • テキスト読み上げ (TTS)モデルを使用して音声応答を合成します。
  • ユーザーへの応答を再生します。



このデモを実装するために、私は OpenAI のクラウド モデル ( WhisperGPT-4TTS)に全面的に依存しました。優れたライブラリ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())


このデモの最初のテストの後、解決しなければならない問題がすぐに明らかになりました。

  • 応答の遅延。単純な実装では、ユーザーの質問と応答の間の遅延は 7 ~ 8 秒以上になります。これは良くありませんが、明らかに、応答時間を最適化する方法は数多くあります。


  • 周囲の騒音。騒がしい環境では、ユーザーが話し始めたときと話し終えたときを自動的に検出するのにマイクに頼ることはできないことがわかりました。フレーズの開始と終了を認識すること (エンドポイント) は簡単な作業ではありません。これに音楽フェスティバルの騒がしい環境を組み合わせると、概念的に異なるアプローチが必要であることは明らかです。


  • ライブ会話を模倣します。ユーザーが AI に割り込むことができるようにしたいと考えました。これを実現するには、マイクをオンにしておく必要があります。ただし、この場合、ユーザーの声を背景音だけでなく AI の音声からも分離する必要があります。


  • フィードバック。応答の遅延により、システムがフリーズしているように見えることがありました。応答の処理にどのくらい時間がかかるかをユーザーに通知する必要があることに気付きました。


これらの問題を解決するには、適切なエンジニアリングまたは製品ソリューションを探すという選択肢がありました。

ブースのUXを考える

コーディングを始める前に、ユーザーがブースとどのように対話するかを決める必要がありました。

  • 過去のダイアログ履歴をリセットするために、ブース内の新しいユーザーを検出する方法を決定する必要があります。


  • ユーザーの発話の始まりと終わりを認識する方法、および AI を中断したい場合にどうするかについて説明します。


  • AI からの応答が遅れた場合にフィードバックを実装する方法。


ブース内の新しいユーザーを検出するために、ドア開閉センサー、床重量センサー、距離センサー、カメラ + YOLO モデルなど、いくつかのオプションを検討しました。背面の距離センサーは、ドアが十分に閉まっていない場合などの偶発的なトリガーを排除し、重量センサーとは異なり複雑な設置を必要としないため、最も信頼性が高いように思われました。


会話の始まりと終わりを認識するという課題を回避するために、マイクを制御するための大きな赤いボタンを追加することにしました。このソリューションにより、ユーザーはいつでも AI を中断することもできます。


リクエストの処理に関するフィードバックを実装することについては、さまざまなアイデアがありました。私たちは、マイクを聞いている、質問を処理している、または回答しているなど、システムが何を実行しているかを示す画面を備えたオプションを選択しました。


また、古い固定電話を使ったかなりスマートなオプションも検討しました。ユーザーが電話を取るとセッションが開始され、ユーザーが電話を切るまでシステムがユーザーの話を聞きます。ただし、電話からの声ではなくブースがユーザーに「応答」する方が本物らしくなると判断しました。


設置中とフェスティバル中


最終的に、最終的なユーザーフローは次のようになりました。

  • ユーザーがブースに入ってきます。背後で距離センサーが作動し、私たちはユーザーに挨拶します。


  • ユーザーは赤いボタンを押してダイアログを開始します。ボタンが押されている間、マイクをリッスンします。ユーザーがボタンを放すと、リクエストの処理が開始され、画面に表示されます。


  • AI が回答している間にユーザーが新たな質問をしたい場合は、ボタンをもう一度押すと、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 はブラウザで利用できます。


残念ながら、 WhisperDeepgramに比べて品質が非常に劣っており、言語を自動的に検出することはできません。


処理時間を短縮するには、ユーザーが話しているときにリアルタイムで音声を認識するのが最善の選択肢です。以下に、実装方法の例を含むプロジェクトをいくつか示します。ウィスパーストリーミングウィスパー.cpp


私たちのラップトップでは、このアプローチを使用した音声認識の速度はリアルタイムには程遠いことが判明しました。いくつかの実験を行った後、私たちは OpenAI のクラウドベースの Whisper モデルを採用することにしました。

LLMとプロンプトエンジニアリング

前のステップの Speech To Text モデルの結果は、ダイアログ履歴とともに LLM に送信するテキストです。


LLM を選択する際に、GPT-3.5、GPT-4、Claude を比較しました。重要な要素は特定のモデルではなく、その構成であることがわかりました。最終的に、他のモデルよりも回答が気に入った GPT-4 に落ち着きました。


LLM モデルのプロンプトのカスタマイズは、独立した芸術形式になっています。必要に応じてモデルを調整する方法については、インターネット上に多くのガイドがあります。



モデルが魅力的かつ簡潔でユーモラスに応答するようにするには、プロンプトと温度設定を徹底的に実験する必要がありました。

テキスト読み上げ

LLM から受信した応答を Text-To-Speech モデルを使用して音声化し、ユーザーに再生します。このステップが、デモにおける遅延の主な原因でした。


LLM は応答にかなり時間がかかります。ただし、ストリーミング モード (トークン単位) での応答生成をサポートしています。この機能を使用すると、LLM からの完全な応答を待たずに、受信した個々のフレーズを音声で読み上げることで、待機時間を最適化できます。


個々の文を発声する


  • LLM に問い合わせてください。


  • 最小長の完全な文が得られるまで、トークンごとにバッファに応答を蓄積します。最小長パラメータは、発声のイントネーションと初期遅延時間の両方に影響するため重要です。


  • 生成された文章を TTS モデルに送信し、結果をユーザーに再生します。このステップでは、再生順序に競合状態がないことを確認する必要があります。


  • 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 に配慮することにしました。私たちはいくつかのアプローチを検討しました。


  • LED インジケーター。アイドル、待機、リスニング、思考、会話の 5 つの状態を表示する必要がありました。しかし、LED でわかりやすい方法でそれを実現する方法がわかりませんでした。


  • 「考えさせてください」「うーん」などのつなぎ言葉は、実際の会話を模倣したものです。つなぎ言葉はモデルの応答の口調と一致しないことが多いため、このオプションは却下しました。


  • ブース内にスクリーンを設置し、アニメーションでさまざまな状態を表示します。


私たちは、バックエンドをポーリングし、現在の状態に応じてアニメーションを表示するシンプルな Web ページという最後のオプションを選択しました。


結果

私たちの AI 告白室は 4 日間開催され、何百人もの参加者を集めました。OpenAI API に費やした金額はわずか 50 ドルほどでした。その代わりに、かなりの肯定的なフィードバックと貴重な感想をいただきました。


この小規模な実験により、リソースが限られており外部条件が厳しい場合でも、直感的で効率的な音声インターフェースを LLM に追加できることが示されました。


ちなみに、 GitHubで利用可能なバックエンドソース