【AI Shift Advent Calendar 2023】GPT-4-turbo + 音声合成で快適なボイスボットを作る

DALLE-3で生成

1. はじめに

こんにちは、AI Shiftの友松です。 この記事はAI Shift Advent Calendar 2023の1日目の記事です。

AI Shiftでは2021年, 2022年とAdvent Calendarを継続的に行っており、今年で3回目の実施となります。AI ShiftのXアカウントにてTech Blog更新のポストを行いますのでぜひフォローをしてご確認いただけると幸いです。

本記事では、2023年11月6日に行われたOpenAI DevDayで発表されたGPT-4-turboとリアルタイム用途に最適化されたText-to-Speech(音声合成)モデル(以下TTSと呼ぶ)を組み合わせて簡易なボイスボットのデモを作ります。

2. LLMをボイスボットに適用するための課題

本記事は、以前ChatGPTのAPIが公開されたタイミングで投稿した、「OpenAIのWhisperとChatGPTのAPIでGoogle Colab上で簡易なボイスボットを作る」記事のアップデート版になります。

これまでボイスボットに対してLLMを適用しようとした場合、2つの観点でインタラクティブ性に課題がありました。

  • LLMにプロンプトを入力し応答文を得るまでの時間が長くかかる。
  • 確定した応答文に対してTTSによって音声データ全体を取得するまでに時間が長くかかる。

前者の課題に対してはGPT-4-turboによって高品質な出力データを高速に得ることが可能になりました。AI Shiftで実測した数値ではDevDay以前のGPT-3.5-turboのモデルよりも高速で有ることを確認しております。また、streamでのchunk出力を使うことで出力文章全体が確定する前に適宜TTSによって音声化ができるようになります。

後者の課題に対してはDevDayのアップデートによってTTSによる音声出力も音声全体が確定する前に順次取り出すことができるようになりました。※2023/12/01現在APIでは対応済みですが、pythonクライアントではまだ未対応のようです。

以上を踏まえてLLM + TTSの出力速度を工夫してインタラクティブにボイスボットとの会話を進めるようなデモを構築します。※本記事では入力はテキストとし、音声認識については扱いません。

3. 実装

3.1 準備

まずは実装に必要なライブラリをinstallします。(検証時のライブラリのバージョンで固定します。)

pip install openai==1.1.1
pip install pygame==2.5.2

3.2 TTSの実装

import openai
import io
import pygame
import re

# 環境変数からAPIキーを取得する
api_key = "{YOUR_OPENAI_API_KEY}"

# OpenAIクライアントの初期化
client = openai.OpenAI(api_key=api_key)

def tts(text):
    try:
        # pygameの初期化とミキサーの初期化
        pygame.init()
        pygame.mixer.init()
        clock = pygame.time.Clock()
        fps = 10  # フレームレート

        # OpenAIのTTSを使用して音声ファイルを生成
        response = client.audio.speech.create(
            model="tts-1",
            voice="alloy",
            input=text + "。",  # テキストの末尾に句点を追加
            speed=1.3
        )

        # レスポンスから音声データを取得してBytesIOオブジェクトに書き込む
        audio_data = io.BytesIO(response.content)

        # BytesIOオブジェクトから直接音声を再生する
        pygame.mixer.music.load(audio_data)
        pygame.mixer.music.play()

        # 再生が終了するのを待つ
        while pygame.mixer.music.get_busy():
            clock.tick(fps)

    finally:
        # pygameミキサーを停止
        pygame.mixer.quit()
        pygame.quit()

tts関数では音声合成するためのtextを入力として受け取り、完成した音声ファイルを再生します。

音声の再生が完了してから次の音声を再生するために、pygameによる制御を行っています。

(これがないと、音声が再生し終わる前に次の音声の再生が始まり、被って聞こえてしまいます。)

3.3 ボイスボット本体の実装

if __name__ == "__main__":
    # 初期のプロンプト
    messages = [
        {
            "role": "system",
            "content": """
            あなたはオペレータの振る舞いをするボットです。
            レストラン予約を完遂してください。
            エリア(渋谷、新宿、代々木)、料理(ラーメン、焼き肉、中華)、日付、時間を聞いて予約を確定してください。
            """
        },
        {
            "role": "user",
            "content": "こんにちは"
        },
    ]

    while True:
        # ボットの発話のstreamの結果を格納する変数
        bot_speech = ""
        i = 1
        
        # ボットの発話をテキストで生成する。
        # streamをTrueにすると、生成が完了するまでの途中経過を取得できる。
        for chunk in client.chat.completions.create(
                model="gpt-4-1106-preview",
                messages=messages,
                temperature=0,
                stream=True
        ):
            content = chunk.choices[0].delta.content
            if content is not None:
                bot_speech += content
                
            # ボットの発話を文単位で区切って、一文として完成したタイミングでTTSによって音声を再生する。
            sentences = re.split(r"[??!!。]", bot_speech)
            if len(sentences) > i:
                sentence = sentences[i - 1]
                print(f"assistant: {sentence}")
                tts(sentence)
                i += 1
        
        # ボットの発話が終わったタイミングでユーザーの発話をテキストで受け付ける。
        user_speech = input()
        print(f"user: {user_speech}")
        
        # ユーザーの発話をmessagesに追加する。
        messages.extend([
            {"role": "assistant", "content": bot_speech},
            {"role": "user", "content": user_speech}
        ])
        
        # ユーザーの発話が「おしまい」だったらループから抜け、会話を終了する。
        if user_speech == 'おしまい':
            break

細かくはコード上のコメントのとおりですが、大枠のシステムの流れは以下のとおりです。

  1. message変数に初期のプロンプトを入れます。
    • ここで対話全体を通してどういう対話を実行したいかを指定します。今回は単純にレストラン予約を遂行するためにエリア・料理・日付・時間を聴取して予約を完了させるという指示にしています。
  2. ChatGPTにpromptを投げて、テキストの出力を得ます。
    • ボットの発話の結果はchunkで返ってくるので、文単位までchunkが吐き出されたら(。?!が現れたら文末と定義します。)、tts関数にそこまでのテキストの結果を渡して音声合成を行います。
    • chunkが完了するまで2を続けます。
  3. ボットの発話が完了するとユーザーの入力をテキストで受け付けます。ボットの出力テキストと受けとったユーザ入力をmessages変数に詰めて再度2に戻ります。
    • ユーザーの入力が「おしまい」だった場合、そこで対話は終了します。

4. デモ

ユーザー側の発話としてテキストを入力してからボットの音声が再生するまでの待ち時間が少なくスムーズに対話が進行しています。

また、ボットの発話にLLMを適用することで、レストラン予約のような「タスク指向型対話」の構築コストを抑えつつ柔軟性を大幅に向上させることが可能になります。

従来のシナリオベースのタスク指向型対話を構築しようとした場合、まず日付を聞き、次に場所を聞き...というようにフローを構築します。

しかし、例えば動画上でもあるように、場所を聞いていない箇所で「あ、やっぱり新宿で」といった発話がされることを考慮したりすると、シナリオ構築が非常に複雑になる傾向があります。
これがLLMを適用することで解消されるのです。

課題点としては、

  • LLMの課題
    • プロンプトで与えていないような名前や人数の聴取を勝手に行われる
  • TTSの課題
    • 読み上げ時に読み仮名を間違えて発音している
    • 料理の種類のカッコ内は読まれるのに、エリアのカッコ内は読まれない
    • イントネーションがやや不自然

がありますが、LLMの課題に対してはプロンプトでより厳密に指示をしたり、TTSの課題に対しては音声合成モデルを差し替えることである程度解消できるとおもいます。

今回実装したものをベースに、プロンプトを拡張したり、ヒアリングした内容をもとにAPIを参照して実際に予約できる店舗を検索したり予約の実行をすることでボイスボットの利用価値がグッと上がります。

AI Shiftでもボイスボットの利用価値を高めるために引き続き研究開発を続けていきます。

終わりに

AI Shiftではエンジニアの採用に力を入れています! 少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか? (オンライン・19時以降の面談も可能です!) 【面談フォームはこちら

それではこれからクリスマスまでAI Shift Advent Calendarをよろしくお願いします!

PICK UP

TAG