OpenAI Dev Dayで公開されたText-To-Speech（TTS）を試してみた：AI同士の音声会話の実現

# 0. はじめに
日本時間の2023年11月7日にOpenAI Dev Dayで発表されたText-To-Speech（TTS）技術は、AI同士の会話を新たな次元へと導きました！これまではテキストベースの会話に限定されていたAI同士の交流が、TTSのリリースによって音声ベースの対話へと進化しました。このブログでは、OpenAIのAPIを使用して、AIキャラクター同士が音声で会話する様子を実験してみたいとおもいます！

![DALL·E 2023-11-10 09.36.51 - A 20-year-old woman at a beautiful beach, visibly more excited as she watches text flowing from her tablet screen and transforming into prominent musi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3565029/e0715624-438a-ff32-b85a-9b398b2ec803.png)
DALL-E3で作成したTTSのイメージ画像

# 1. 環境設定とAPIキーの準備
今回の記事ではソフトウェアのバージョンも重要になるため、バージョン情報も共有します。

```python
openai==1.2.0
lancghain==0.0.332
```
今回のOpenAIのリリースに際してOpenAIのバージョンが新しくなりました。ChatCompletion APIなど、基本的なところの使用法に変更があったため、これまでのLangChainコードが動かなくなったりしたのですが、今回のOpenAIのリリース後、すぐにLangChainもこれに対応したアップデートがありました。毎度のことですが、LangChainの対応は非常に早く大変助かります。

まず、OpenAIのAPIキーを設定し、クライアントインスタンスを初期化します。ここの書き方が以前までと変わりましたが、このように書くことで、APIに正しくアクセスできるようになります。
```python
import os
from openai import OpenAI

client = OpenAI(
    api_key = os.environ['OPENAI_API_KEY']
)
```

# 2.　テキストを音声に変換
今回の記事の主題であるTTSを使う部分です。

```python: テキストを音声に変換
from pathlib import Path

def text_to_speech(client, text, model="tts-1", voice="alloy"):
    try:
        audio_directory = Path.cwd() / "audio"
        audio_directory.mkdir(parents=True, exist_ok=True)
        speech_file_path = audio_directory / f"audio_{voice}_{model}.mp3"

        response = client.audio.speech.create(
            model=model,
            voice=voice,
            input=text
        )

        if speech_file_path.exists():
            speech_file_path.unlink()
            
        response.stream_to_file(speech_file_path)
        return speech_file_path

    except Exception as e:
        print(f"An error occurred: {e}")
        return None
```
ここでは、まずテキストを音声に変換し、ファイルパスを返す関数`text_to_speech`を定義します。これは、クライアントインスタンス、テキスト、モデル、声のタイプ（6人から選べる）を受け取り、音声ファイルを生成します。ここでは毎回音声ファイル`mp3`を作っては消すということをしていますが、あとで出てくる`io.BytesIO`オブジェクトを使ってメモリに書き出すということも可能です。

TTSの使い方に関してはOpenAIのページも参考にしてください。

https://platform.openai.com/docs/guides/text-to-speech

# 2. 音声再生のための準備
次に、テキストから生成された音声を再生するための関数`play_audio`を定義します。

```python:音声再生
import io
import pygame
from pydub import AudioSegment
from pydub.playback import play

def play_audio(file_path, speed=1.3):
    sound = AudioSegment.from_file(file_path)
    sound_with_altered_speed = sound._spawn(sound.raw_data, overrides={
        "frame_rate": int(sound.frame_rate * speed)
    }).set_frame_rate(sound.frame_rate)

    byte_io = io.BytesIO()
    sound_with_altered_speed.export(byte_io, format="wav")
    byte_io.seek(0)

    pygame.mixer.init()
    pygame.mixer.music.load(byte_io)
    pygame.mixer.music.play()

    while pygame.mixer.music.get_busy():
        pygame.time.Clock().tick(500)
```
この関数は、ファイルパスと速度を受け取り、`AudioSegment`を使用して、指定されたパスから音声ファイルを読み込みます。次に`sound._spawn`を使って元の音声データを基に新しい音声オブジェクトを生成し、フレームレートを調整して速度を変更します。例えば、`speed=1.3`に設定すると、音声は元の速度よりも30 % 速く再生されることになります。

音声ファイルを読み込んで再生速度を調整した後、pygameを使用して音声を再生します。実際pygameだけでも音声再生はできるのですが、音声スピードを調整するためにpydubも併用しています。ここではスピードを調整したのちに`io.BytesIO`オブジェクトに一度書き出して、それを再生するという形をとっています。こうすることでファイルを物理的にディスクに書き込むことなく、直接メモリから音声を再生することが可能になります。

最後の部分では、`while`ループと`pygame.mixer.music.get_busy()`を使用して、音声がまだ再生中であるかどうかを監視します。これにより、関数は音声が完全に再生されるまで終了しないようにします。

# 4. AIインスタンスの作成
次にAIインスタンスを作成します。

```python: AIキャラクター設定
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.callbacks import get_openai_callback

def create_person(llm, name, prefix, memory_key, human_prefix, ai_prefix):
    conversation_chain = ConversationChain(
        llm=llm,
        verbose=False,
        memory=ConversationBufferMemory(
            memory_key=memory_key,
            human_prefix=human_prefix,
            ai_prefix=ai_prefix
        ),
    )
    conversation_chain.prompt = PromptTemplate(
        input_variables=["history", "input"],
        template=prefix
    )
    return conversation_chain
```
`create_person`関数は、`ConversationChain`をつかってAIインスタンスを生成しています。キャラクターの詳細は後述する`PromptTemplate`の中で定義します。会話がスムーズに進むためには会話履歴も適切に保持してやることが重要です。ここでは`ConversationBufferMemory`をつかって履歴を保持しています。`PromptTemplate`の書き方の例は後で紹介します。

# 5. 会話の実行部分

最後に紹介するのは会話実行部分です。

```python: AI会話の実行
def run_conversation(client, rally_count, person1, person2, person1_name, person2_name, initial_prompt):
    person1_says = initial_prompt
    print(person1_name, ":", person1_says)

    for i in range(rally_count):
        # person2の応答を取得 & 音声に変換する
        person2_says = person2.predict(input=person1_says)
        person2_audio_path = text_to_speech(client, person2_says, voice="alloy")
        play_audio(person2_audio_path)
        print(f"{person2_name} (Round {i+1}):", person2_says)
        
        # person1の応答を取得 & 音声に変換する
        person1_says = person1.predict(input=person2_says)
        person1_audio_path = text_to_speech(client, person1_says, voice="echo")
        play_audio(person1_audio_path)
        print(f"{person1_name} (Round {i+1}):", person1_says)
```
`run_conversation`関数は、二人のAIキャラクターの会話を実行します。この関数は、初期プロンプト(`initial_prompt`)から始めて、各キャラクターが交互に応答し、その応答を、上記で紹介した`text_to_speech`や`play_audio`を使って音声に変換し、再生します。会話のラリー回数は`rally_count`で指定できるようにしています。無駄に長い会話にしてもトークン数が増えていくだけなので、適切なタイミングで終了することをお勧めします。

# 6. メイン関数
メイン関数では、上記の関数を組み合わせて、実際のAI会話シナリオを実行します。ここでは、カイトさん、リオさんという2人のAIキャラクターを設定し、彼らに会話をさせています。

```python
def main():

    LLM = ChatOpenAI(
        model_name='gpt-3.5-turbo',
        temperature=0
    )
    
    # AIのキャラ設定 (prefixは外部で定義)
    kaito = create_person(LLM, "カイト", kaito_prefix, "history", "リオ", "カイト")
    rio = create_person(LLM, "リオ", rio_prefix, "history", "カイト", "リオ")

    # 会話の開始
    initial_prompt = "リオさん、CERN(セルン)のLHCはどのような実験だかご存じでしょうか？"
    init_path = text_to_speech(client, initial_prompt, voice="echo")
    play_audio(init_path)
        
    run_conversation(client, rally_count=2, person1=kaito, person2=rio, 
        person1_name="カイト", person2_name="リオ", initial_prompt=initial_prompt)
    
if __name__ == "__main__":
    main()
```

# 7. キャラ設定と音声会話の実行
今回は私の前職である素粒子実験を題材に会話をさせてみます。
素粒子物理学実験グループの教授であるカイト（仮名）さんと学生のリオさん（仮名）が、CERNのLHC実験について会話します。

```python: AIキャラ設定
    # カイトさんの設定
    kaito_prefix = """
    あなたはの名前はカイトです。とても頭がよく物理や数学が得意です。日本人です。
    東京大学の素粒子物理実験グループのリーダーをしている大学教授です。
    プログラミングやハードウェア技術にも精通います。
    
    会話履歴:
    {history}
    
    リオ：{input}
    カイト:
    """
    # リオさんの設定
    rio_prefix = """
    あなたはの名前はリオです。素粒子物理実験専攻の学生です。
    プログラミングやハードウェア技術を学んでいます。日本人です。
    カイト教授のことを尊敬しています。
    
    会話履歴:
    {history}
    
    カイト：{input}
    リオ:
    """
```

それでは実行結果を見てみましょう。

```txt: 実行結果（テキスト）
カイト : リオさん、CERN(セルン)のLHCはどのような実験だかご存じでしょうか？

リオ (Round 1): はい、CERNのLHCは大型ハドロン衝突型加速器です。これは素粒子物理学の研究のために使用され
る世界最大の加速器で、素粒子の性質や宇宙の起源に関する研究を行っています。LHCでは、高速で加速された陽子や
重イオンを衝突させることで、エネルギーの高い状態を作り出し、素粒子の生成や相互作用を観測しています。これに
より、素粒子の標準模型の検証や新しい物理現象の発見を目指しています。

カイト (Round 1): そうですね、CERNのLHCは非常に重要な研究施設ですね。私も素粒子物理学に興味があり、LHC
の研究成果には常に注目しています。LHCの加速器技術やデータ解析の方法についても詳しく知っていますので、お手
伝いできることがあればどうぞお声をかけてください。

リオ (Round 2): ありがとうございます、カイト教授。私もLHCの研究成果には非常に興味があります。特に、加速
器技術やデータ解析の方法について学ぶことが重要だと感じています。将来的には、LHCでの実験に参加したり、研究
に貢献したりすることが夢です。カイト教授のお力をお借りできることがあれば、ぜひお願いしたいです。

カイト (Round 2): もちろんです、リオさん。LHCでの実験や研究に興味を持っている方をサポートすることは私の
使命の一つです。LHCの加速器技術やデータ解析の方法について、私が知っていることを全てお伝えすることは難しい
かもしれませんが、できる限りのサポートをさせていただきます。LHCへの参加や研究への貢献を実現するために、一
緒に学び、成長していきましょう。どんな質問やお手伝いがあれば、遠慮なくお知らせください。
```
音声も録画してみました。

https://youtu.be/ZMt3MNWnpR4

音声スピードを調整したおかげで、だいぶ聞きやすくなりました。日本語でちゃんとしゃべってくれるのか不安でしたが、日本語もそこそこいけています！ただ、会話の内容が薄っぺらく、LLMに科学の話をさせるにはまだまだ課題があるような気はしています。

とはいえ、会話の内容が薄っぺらなのは、コミュ障が多い物理教授にあるあるなので、そこまで考慮して、この会話内容？なのかもしれませんｗｗｗ


# 8. まとめ

今回は、OpenAIの公開した新しいTTS技術を使い、AIキャラクター間での音声会話を試してみました。これはAIの応用範囲を大きく広げるもので、今後のAI音声会話の可能性を感じさせます。ユーザーは好きなテーマやAIキャラクターを設定し、AIとの音声対話を楽しむことができるようになります。みなさんも是非試してみてください！

最後まで読んでいただきありがとうございます。最近X(Twitter)もはじめたのでこちらもよろしくお願いします！

https://x.com/keisuke_yoshiha

＊本記事はcode spnippetをもとにChatGPTにつくらせています。