VAD, Whisper, VOICEVOXで実現する会話型AIサービスの構築

# はじめに

今回は、OpenAIのGPTと組み合わせた会話型AIサービスのサンプル実装をご紹介します。ユーザーは音声で質問を投げかけ、AIが応答し、その応答を音声合成する仕組みを実装しました。これにより、音声でのインタラクティブなやり取りが実現されます。

# 処理フロー

処理フローは以下のようになります。

1. ユーザーが音声で質問をします
2. サーバーでは音声を受け取り、Whisperモデルを用いて文字起こしを行います
3. 文字起こししたテキストをもとに、GPTモデルで応答するテキストを生成します
4. 応答テキストをVOICEVOXで音声合成し、ユーザーに返します

この一連の流れにより、テキストによるやり取りを音声で実施することができます。

```mermaid
sequenceDiagram
    participant ユーザ as ユーザ
    participant ブラウザ as ブラウザクライアント
    participant WhisperAPI as Whisper API
    participant GPTAPI as GPT API
    participant VOICEVOXAPI as VOICEVOX API

    ユーザ->>+ブラウザ: 音声を録音して送信
    ブラウザ->>+WhisperAPI: 音声ファイルをPOSTリクエストで送信
    WhisperAPI-->>-ブラウザ: テキストデータをJSONで返す

    ブラウザ->>+GPTAPI: テキストデータをPOSTリクエストで送信
    GPTAPI-->>-ブラウザ: 応答テキストをJSONで返す

    ブラウザ->>+VOICEVOXAPI: 応答テキストをPOSTリクエストで送信
    VOICEVOXAPI-->>-ブラウザ: 音声ファイルのパスをJSONで返す

    ブラウザ-->>-ユーザ: 音声ファイルを再生
```

# サーバーサイド実装

サーバーはFlaskを使用して実装され、主要な3つのAPIエンドポイントを提供します。これらのAPIを利用して、ユーザーが提供した音声ファイルからテキストへの変換、テキストからの応答生成、そして応答テキストから音声ファイルへの合成というプロセスが行われます。

## 音声の文字起こし

以下のコードは、ユーザーからアップロードされた音声ファイルを受け取り、OpenAIのWhisperモデルを使用して文字起こしを行うAPIの実装です。このAPIはPOSTメソッドで呼び出され、処理完了後にJSON形式でテキストデータを返します。

```python
from flask import Flask, request, jsonify
import whisper
import os
import threading

UPLOAD_FOLDER = 'uploads'
WHISPER_MODEL_NAME = 'small'
WHISPER_DEVICE = 'cuda'

# Whisperモデルのロード
whisper_model = whisper.load_model(WHISPER_MODEL_NAME, device=WHISPER_DEVICE)
lock = threading.Lock()
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

@app.route('/api/transcribe', methods=['POST'])
def transcribe():
    file = request.files['file']
    ext = file.filename.rsplit('.', 1)[1].lower()
    if ext in ALLOWED_EXTENSIONS:
        filename = os.path.join(app.config['UPLOAD_FOLDER'], f"{int(time.time())}.{ext}")
        file.save(filename)

        lock.acquire()
        try:
            result = whisper_model.transcribe(filename, fp16=False, language='ja')
        finally:
            lock.release()
        os.remove(filename)
        return jsonify(result)

    return {'error': 'Invalid file extension'}, 400
```

Whisperモデルは、スレッドセーフではないため、`threading.Lock()` を使用してアクセスの競合を防止しています。
## 対話応答生成
次は、Whisperで文字起こしを行ったテキストに基づき、GPTと連携して応答を生成するAPIです。GPTにはチャット履歴と質問を渡し、対話応答を取得します。
```python
@app.route('/api/callgpt', methods=['POST'])
def callgpt():
    question = request.form.get('question')
    global prompt
    try:
        lock.acquire()
        answer, new_prompt = talkToGpt.talk_to_gpt(prompt, question)
        prompt = new_prompt
    finally:
        lock.release()
    return jsonify({'answer': answer})
```

```python
def talk_to_gpt(prompt, question):
    prompt = prompt+f"ユーザー: {question}<NL>システム: "
    print("GPTへの質問:" + prompt)
    openai.api_key = keys.OPEN_AI_API_KEY
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "user", "content": prompt},
    ]
    )
    output = response["choices"][0]["message"]["content"]
    print("GPTの解答:" + output)
    prompt = prompt + output + "<NL>"
    return str(output), prompt
```

対話応答は、`talkToGpt` モジュールの talk_to_gpt 関数によって生成されます。
ここでも、共有リソースに対するアクセスを制御するためにロックを使用しています。
## 音声合成
以下はVOICEVOXを使用して応答文を音声に合成するAPIです。
応答文を受け取り、VOICEVOXが提供する音声合成エンジンを使ってWAVファイルに変換します。生成されたWAVファイルのパスはクライアントに返されます。
```python
@app.route('/api/callvoicevox', methods=['POST'])
def callvoicevox():
    talk_text = request.form.get('talk_text')
    try:
        lock.acquire()
        output_filepath = voicevox.speak_voicevox(talk_text)
    finally:
        lock.release()
    return jsonify({'output_filepath': output_filepath})
```
voicevox.speak_voicevox 関数は、`voicevox.py` ファイルに定義されており、テキストを音声に合成する処理を行います。
```python
def speak_voicevox(input_texts):
    speaker     = 14 # speaker id
    filename    = str(int(time.time()))
    output_path = "./static/voicevox_output"

    # audio_query (音声合成用のクエリを作成するAPI)
    res1 = requests.post('http://localhost:50021/audio_query',
                        params={'text': input_texts, 'speaker': speaker})
    # synthesis (音声合成するAPI)
    res2 = requests.post('http://localhost:50021/synthesis',
                        params={'speaker': speaker},
                        data=json.dumps(res1.json()))
    # wavファイルに書き込み
    filepath = output_path + '/' + filename + '.wav'
    with open(filepath, mode='wb') as f:
        f.write(res2.content)
    filepath = filepath.replace("./static","")
    return filepath
```
これらのサーバーサイドのコードは、ユーザーが提供した音声から得られたテキストでAIが応答を生成し、最終的に生成された音声ファイルをユーザーに返すという一連のプロセスを可能にします。

# クライアントサイドの実装

クライアントサイドは、ブラウザベースのJavaScriptを活用してユーザーの音声を録音し、サーバーへと送信する機能を実装します。具体的には、音声活動検出(`Voice Activity Detection`, `VAD`)ライブラリを使用して、ユーザーの声をリアルタイムで検知し、録音を行います。録音された音声はサーバーに送信され、サーバーはそれを処理してからテキストへ変換し、GPTモデルを用いて応答を生成し、VOICEVOXを通じてその応答を音声合成する流れです。

以下のHTMLコードは、このプロセスを実行するためのクライアントサイドの実装を示しています。

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>minie</title>
  <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.13.1/dist/ort.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@ricky0123/vad/dist/index.browser.js"></script>
  <script type="module">
    try {
      const myvad = await vad.MicVAD.new({
        positiveSpeechThreshold: 0.8,
        negativeSpeechThreshold: 0.8 - 0.15,
        minSpeechFrames: 5,
        preSpeechPadFrames: 1,
        redemptionFrames: 10,
        onSpeechEnd: async (arr) => {
          const wavBuffer = vad.utils.encodeWAV(arr);
          var file = new File([wavBuffer], `file${Date.now()}.wav`);
          let formData = new FormData();
          formData.append("file", file);

          try {
            // Whisper API
            const res_talk_sentence = await fetch("/api/transcribe", {
              method: "POST",
              body: formData,
            })
            const json_talk_sentence = await res_talk_sentence.json();
            const talk_sentence = json_talk_sentence.text; // 話した言葉が格納される

            // RESET formData for the next API call
            formData = new FormData();
            formData.append("question", talk_sentence);

            // GPT API
            const gpt_answer = await fetch("/api/callgpt", {
              method: "POST",
              body: formData,
            });
            const json_gpt_answer = await gpt_answer.json();
            const text_gpt_answer = json_gpt_answer.answer;
            console.log("GPTの回答: " + text_gpt_answer);

            // RESET formData for the next API call
            formData = new FormData();
            formData.append("talk_text", text_gpt_answer);

            // VOICEVOX API
            const res_output_filepath = await fetch("/api/callvoicevox", {
              method: "POST",
              body: formData,
            });
            const json_res_output_filepath = await res_output_filepath.json();
            const output_filepath = json_res_output_filepath.output_filepath;
            // PLAY the response audio
            var audioElem = new Audio('.' + output_filepath);
            audioElem.play();
          } catch (err) {
            console.error("ERROR_OCCURRED: " + err);
          }
        },
      });
      myvad.start();
    } catch (e) {
      console.error("Failed:", e);
    }
  </script>
  <style>
    body {
      background-color: lime;
    }
  </style>
</head>
<body>
</body>
</html>
```

このクライアントサイドの実装では、以下の一連のステップを実行します:
1. VADを使用してユーザーの音声を検知し、WAVフォーマットのバッファに音声をエンコードします
2. エンコードされた音声をファイルに変換し、`FormData`オブジェクトに追加してサーバーのWhisper APIに送信し、文字起こしします
3. Whisper APIからテキストに変換されたユーザーの発言が返され、さらにGPT APIを通じて応答を生成します
4. 生成された応答テキストはVOICEVOX APIに渡され、ユーザーが聞くことのできる音声ファイルに合成されます
それぞれのAPIリクエストで新しい`FormData`オブジェクトが作成されることに注意してください。これにより、リクエスト間で情報が不要に伝搬することを防ぎます。また、生成された音声ファイルは、`new Audio()`コンストラクタを使用して再生されます。
このプロセスにより、ユーザーは完全に音声ベースでシステムとインタラクションすることが可能となります。

# まとめ
本記事で紹介したサーバーサイド実装とクライアントサイド実装により、VAD, Whisper, VOICEVOXを活用した会話型AIサービスを構築することができます。ユーザーは自然な形でAIとの対話を楽しむことができるようになります。