見出し画像

ボイス有りAIキャラと話せるサイトを作ろう【音声入力対応】

AIVtuberシロハナちゃん開発とAIヒロイン研究Pをしているyukiです。

今回は、ボイス有りAIキャラと話せるサイトの作り方をまとめていきます。(サンプルコードも載せています)

完成した成果物のUIは以下画像のようになっており、テキスト入力または音声入力で話して、その内容をもとにAIキャラが音声ボイスと字幕でリアルタイムに返してくれるといったサイトになっています。

成果物UI

今回はAIツンデレメイドちゃんというシンプルなAIキャラクターで作成しました。
何かプロンプトを渡すとツンデレ口調で応答してくれます。

システムイメージは以下の通りです。

システムイメージ

YouTubeでもシロハナちゃんが本記事と同じような内容で動画にしているのでもし興味があればご覧いただけると嬉しいです。

https://youtu.be/HXytKmTEeL0

※この記事は2024/03/28時点のものなので今後変更があるかもしれないですのでご了承ください


開発環境と準備

  • 私が実施した環境を箇条書きしました。似たような環境でも恐らく問題ないと思いますので参考まで。導入方法などは各実装時に説明します。

    • Windows11

    • VSCode

    • VOICEVOX(API)(GPU版)

    • OpenAI(API)

    • Python 3.10.6

    • Flask

    • HTML,CSS,JavaScript

    • Web Speech API

    • Chrome

まず準備として任意の階層(私はCドライブ直下)にプロジェクトフォルダを作成しましょう。
名前はシンプルにaicharacterとしました。

そしてターミナルからその階層にcdで移動して以下コマンドを叩いて仮想環境を作ります。(他プロジェクトに影響を与えないようにするため)

python -m venv .venv
.venv\Scripts\activate.bat

ターミナルで(.venv) C:\aicharacter\と出ていれば仮想環境の作成は成功です。
これで準備は一旦完了です。


OpenAIのAPIで回答生成

ではさっそく実装していきましょう。
最初にAIキャラが回答を生成する処理を作っていきます。
ここではOpenAI社が提供しているGPTモデルのAPIを使用します。
使用した分だけ使用量が発生しますが、使いすぎなければ少額の請求で済みます。(確か期間内であれば無料枠も使えたはず)

GPT以外のLLMのAPIでも大丈夫なので拘りのある方などは他でも大丈夫です。ClaudeAPIの記事を他で書いたので参考にリンク張っておきます。

APIキーを取得して.env設定

APIキーの取得を以下リンクに登録して行ってください。
API keysから作成して SECRET KEYの文字列を取得できればOKです。(詳細は割愛します)

発行ができたら、.envファイルをフォルダ直下に作成します。
そして、.envファイルに以下のように記載して保存。

OPENAI_API_KEY = "ここに取得したAPIキーの文字列を入れる"

AIキャラクターの設定プロンプト

次にAIキャラのキャラ付けとなる、設定を考えます
同じくフォルダ直下にsystem_prompt.txtを作成します。
このテキストファイルにどんなキャラクターにするかなどの指示を記載していくわけですが、今回はシンプルに以下のように記載しました。

ツンデレ口調の女の子。
一言で返してください。

簡単すぎる気もしますが、案外しっかり思い描いたキャラ返答にはなります。
ポイントは「一言で」という部分で、回答が長くなるとその分、レスポンスが長くなるので、できるだけ短いほうが会話体験はよいかと思います。
※うまく工夫すれば回答がそこそこ長くても気にならなくすることは可能ですが今回は割愛。

AI回答生成する処理の実装

では実際に回答生成するコードを書いていきましょう。
同じくフォルダ直下にopenai_adapter.pyを作成します。
それと、以下のコマンドをターミナルでたたいて、OpenAIライブラリと、環境変数を.envファイルから読み込むためのpython-dotenvライブラリをインストールしておいてください。

pip install python-dotenv
pip install openai==1.14.2

※OpenAIライブラリは現状では1.14.2が最新ですが、新しいバージョンが出るかと思いますので、その場合はpip install --upgrade openaiでアップデートすると良いかと思います。

次に、先ほど作成したopenai_adapter.pyに記載する内容は以下のコードになります。

import dotenv
import os
from openai import OpenAI

dotenv.load_dotenv()

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

class OpenAIAdapter:
    def __init__(self):
        prompt_file_path = os.path.join(os.path.dirname(__file__), 'system_prompt.txt')
        with open(prompt_file_path, "r", encoding="utf-8") as f:
            self.system_prompt = f.read()

        self.session_messages = []

    def create_message(self, role, message):
        return {
            "role": role,
            "content": message
        }

    def create_chat(self, question):
        if not self.session_messages:
            system_message = self.create_message("system", self.system_prompt)
            self.session_messages.append(system_message)

        user_message = self.create_message("user", question)
        self.session_messages.append(user_message)

        res = client.chat.completions.create(
            model="gpt-4-0125-preview",
            messages=self.session_messages
        )

        response_content = res.choices[0].message.content

        self.session_messages.append(self.create_message("assistant", response_content))

        return response_content

このコードでは下記のような処理が記載されています。

  • 先ほど作成した.envファイルから記載したAPIキーを読み込み、OpenAIのクライアントを設定します。

  • チャットボットの初期メッセージをsystem_prompt.txtファイル(先ほど作成したAIキャラの設定プロンプト)から読み込みます。

  • ユーザーのプロンプトに対してOpenAI APIを通じて回答を生成し、その回答を返します。

  • セッション中のメッセージを保持することで、文脈を考慮した状態での会話が可能。

モデルはgpt-4-0125-previewを使用していますが、他モデルでも大丈夫です。model=の部分を書き換えれば変わります。


VOICEVOXのAPIで回答を読み上げ

さて、回答の生成をする処理を記載したら次はその回答結果を読み上げる処理を実装していきましょう。
今回はVOICEVOXという合成音声のAPIを使用します。

上記リンクからソフトをインストールして立ち上げておいてください。

合成音声で読み上げる処理の実装

まず、フォルダ直下にvoicevox_adapter.pyを作成します。
それとターミナルから必要なライブラリをインストールするため以下のコマンドをたたきます。

pip install requests
pip install soundfile
pip install numpy

そして、作成したvoicevox_adapter.pyに以下のコードを記載します。

import json
import requests
import io
import soundfile

class VoicevoxAdapter:
    URL = 'http://127.0.0.1:50021/'

    def __init__(self) -> None:
        pass

    def _create_audio_query(self, text: str, speaker_id: int) -> json:
        item_data = {
            'text': text,
            'speaker': speaker_id,
        }
        response = requests.post(self.URL + 'audio_query', params=item_data)
        return response.json()

    def _create_request_audio(self, query_data, speaker_id: int) -> bytes:
        a_params = {
            'speaker': speaker_id,
        }
        headers = {"accept": "audio/wav", "Content-Type": "application/json"}
        res = requests.post(self.URL + 'synthesis', params=a_params, data=json.dumps(query_data), headers=headers)
        print(res.status_code)
        return res.content

    def get_voice(self, text: str):
        speaker_id = 43 # character id
        query_data = self._create_audio_query(text, speaker_id=speaker_id)
        speed_scale = 1.08
        query_data['speedScale'] = speed_scale
        pitch_scale = 0
        query_data['pitchScale'] = pitch_scale
        intonation_scale = 1.8
        query_data['intonationScale'] = intonation_scale
        audio_bytes = self._create_request_audio(query_data, speaker_id=speaker_id)
        audio_stream = io.BytesIO(audio_bytes)
        data, sample_rate = soundfile.read(audio_stream)
        return data, sample_rate

このコードでは下記のような処理が記載されています。

  • VOICEVOXのAPIエンドポイントに接続。

  • テキストとキャラのIDを使い、その情報から音声設定を取得し、それを基に実際の音声データを作成します。

  • 生成された音声データをPythonで扱える形式に変換して返します。

つまり、指定したテキストを音声に変換し、その音声データをプログラム内で利用可能にする処理を行っています。

speaker_idは話す音声のキャラクターです。
今回は櫻歌ミコちゃんというキャラクターの声を使用しています。

speaker_id = 43が櫻歌ミコちゃんというわけですが、voicevoxのキャラクターを変えたい場合はspeaker_idの数値を変更してあげましょう。
http://127.0.0.1:50021/speakersからキャラクターのidを確認できます。

speakers一覧

また、get_voiceのところで話速やピッチ、抑揚などのパラメータを調整できます。
詳細は公式ドキュメントを参照してください。

音声出力させる処理の実装

voicevox apiの処理を書けたら次はその音声ボイスを出力デバイスで再生する実装をしていきます。

フォルダ直下にplay_sound.pyというファイルを作成して以下のコードを記載します。

import sounddevice as sd

class PlaySound:
    # デバイス名はお使いの再生デバイスの名前を記載してください
    def __init__(self, output_device_name= "Realtek(R) Audio") -> None:

        output_device_id = self._search_output_device_id(output_device_name)

        input_device_id = 0

        sd.default.device = [input_device_id, output_device_id]

    def _search_output_device_id(self, output_device_name, output_device_host_api=0) -> int:

            devices = sd.query_devices()
            output_device_id = None

            for device in devices:
                is_output_device_name =output_device_name in device["name"]
                is_output_device_host_api = device["hostapi"] == output_device_host_api
                if is_output_device_name and is_output_device_host_api:
                    output_device_id = device["index"]
                    break

            if output_device_id is None:
                print("output_device_id is None")
                exit()

            return output_device_id
    
    def play_sound(self, data , rate) -> bool:
            sd.play(data, rate)
            sd.wait()

            return True

このコードでは特定のオーディオデバイスを選んで音声データを再生する処理が記載されています。

※上記コードではRealtek(R) Audioが再生デバイスとして選択されていますが、お使いの再生デバイスを記載してください


各機能の統合

ここまで各々の処理を作成したらそれらを統合するコードを書いていきます。
フォルダ直下にaicharacter_system.pyというファイルを作成して、以下のコードを記載します。

import threading
from voicevox_adapter import VoicevoxAdapter
from openai_adapter import OpenAIAdapter
from play_sound import PlaySound

class AICharacterSystem:
    def __init__(self) -> None:
        self.openai_adapter = OpenAIAdapter()
        self.voice_adapter = VoicevoxAdapter()
        self.play_sound = PlaySound(output_device_name="Realtek(R) Audio") # デバイス名はお使いの再生デバイスの名前を記載してください

    def process_input(self, input_text):
        response_text = self.openai_adapter.create_chat(input_text)
        threading.Thread(target=self.play_response, args=(response_text,)).start()
        return response_text

    def play_response(self, response_text):
        data, rate = self.voice_adapter.get_voice(response_text)
        self.play_sound.play_sound(data, rate)

このコードでは下記のような処理が記載されています。

  • OpenAIAdapterを使用して、入力テキストに対する応答テキストを生成します。

  • VoicevoxAdapterを使用して、応答テキストを音声データに変換します。

  • PlaySoundを使用して、生成された音声データを特定のオーディオデバイス(ここでは「Realtek(R) Audio」)で再生します。

  • 応答の音声再生とreturn response_textを非同期で行っています。

つまりこれまでの処理をひとつにまとめて、回答生成→音声化→音声出力までを行っているということですね。

ちなみになぜ応答の音声再生とreturn response_textを非同期で行っているかというと、この後にサイト上でキャラの回答を音声で再生するのと同時にテキストで回答内容を字幕で表示するためです。(非同期にしないと字幕の表示が遅れてしまいます)

※上記コードではRealtek(R) Audioが再生デバイスとして選択されていますが、お使いの再生デバイスを記載してください


Flaskでサイト構築

ここまでAIキャラが回答を生成してボイス音声を出力する処理を実装してきました。

次はユーザーがプロンプトを送信するフロント部分の実装と、サーバ部分を実装するためにFlaskというPythonのWebアプリ向けのフレームワークを使用して実装していきます。

まず、ターミナルで以下のコマンドをたたいてFlaskをインストールします。

pip install Flask

そして、必要なファイルを最初に作成しておきます。
フォルダ直下に以下の構造でフォルダとファイルを作成します。

aicharacter
│
└── site
    │
    ├── server.py
    │
    ├── static
    │   ├── main_script.js
    │   ├── style.css
    │   └── tsundere-maids.png
    │
    └── templates
        └── index.html

tsundere-maids.pngはサイトの画面に配置するAIキャラの画像になります。
16:9比率の画像を用意しましょう。

今回AIキャラの画像はにじジャーニーを使って生成しました。
以下の画像になりますので、上記の構成をもとに配置します。

AIツンデレメイドちゃん

画面のUIを作成(HTML,CSS)

まずHTML(index.html)とCSS(style.css)でサイトの外観を作っていきます。
以下にサンプルとなるコードを記載しますが、シンプルなUI/UXとなっております。
せっかくの自作サイトになるので自由にカスタマイズしてみてください。

※PC想定なので、スマホ対応などは考慮していません

各要素や機能の説明は最後にまとめて仕様説明のところで説明します。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AIツンデレメイドちゃん</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="form-container">
        <img src="/static/tsundere-maids.png">
        <div id="response-container">ここに応答が表示されます</div>
        <form id="prompt-form">
            <textarea id="prompt-input-info" placeholder="プロンプトを入力"></textarea>
            <div class="button-container">
                <button type="submit" id="submit-button">送信</button>
                <button type="button" id="record-button">録音開始</button>
            </div>
        </form>
    </div>
    <script src="/static/main_script.js"></script>
</body>
</html>

style.css

body {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f0f0f0;
}

img {
    width: 600px;
    height: 337.5px;
    margin-bottom: 20px;
    object-fit: cover;
}

.form-container {
    text-align: center;
    position: relative;
}

#response-container {
    position: relative;
    top: -20px;
}

#prompt-form {
    display: flex;
    flex-direction: column;
    align-items: center;
}

#prompt-input-info {
    padding: 15px;
    margin-bottom: 10px;
    width: 500px;
    height: 50px;
    font-size: 1em;
    resize: none;
}

button {
    margin-top: 20px;
    padding: 10px 20px;
    width: 200px;
}

.button-container {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    width: 420px;
}

.message-buttons-container {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    flex-direction: column;
}

音声入力など動的要素実装(JavaScript)

サイトの外観ができたら、JavaScript(main_script.js)でプロンプトを音声入力できるようにするのと、動的要素の実装(送信やボタン関連)をしていきます。

まずコードは以下のようになります。

main_script.js

document.addEventListener('DOMContentLoaded', function () {
    var infoInput = document.getElementById('prompt-input-info');
    var submitButton = document.getElementById('submit-button');
    var form = document.getElementById('prompt-form');
    var recordButton = document.getElementById('record-button');
    var isRecording = false;
    var recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition)();

    function updateButtonState() {
        submitButton.disabled = !infoInput.value.trim();
    }

    infoInput.addEventListener('input', updateButtonState);

    form.addEventListener('submit', function(event) {
        event.preventDefault();
        var dataToSend = {
            info: infoInput.value
        };

        infoInput.value = '';
        updateButtonState();

        if (isRecording) {
            isRecording = false;
            recordButton.textContent = '録音開始';
            recordButton.style.backgroundColor = '';
            recordButton.style.color = '';
        }

        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/submit-comment', true);
        xhr.setRequestHeader('Content-Type', 'application/json');

        document.getElementById('response-container').textContent = '考え中...';

        xhr.send(JSON.stringify(dataToSend));

        xhr.onload = function() {
            if (xhr.status === 200) {
                var response = JSON.parse(xhr.responseText);
                document.getElementById('response-container').textContent = response.response;
            } else {
                console.error("送信失敗:", xhr.status);
            }
        };
    });

    recognition.lang = 'ja-JP';
    recognition.interimResults = true;

    recordButton.addEventListener('click', function() {
        isRecording = !isRecording;
        if (isRecording) {
            recordButton.textContent = '録音中...';
            recordButton.style.backgroundColor = '#D9394E';
            recordButton.style.color = 'white';

            recognition.start();
        } else {
            recordButton.textContent = '録音開始';
            recordButton.style.backgroundColor = '';
            recordButton.style.color = '';

            recognition.stop();
        }
    });

    var finalTranscript = '';

    recognition.addEventListener('result', function(event) {
        for (var i = event.resultIndex; i < event.results.length; ++i) {
            if (event.results[i].isFinal) {
                finalTranscript += event.results[i][0].transcript;
            }
        }

        infoInput.value = finalTranscript;
        updateButtonState();
    });

    recognition.addEventListener('end', function() {
        if (isRecording) {
            recognition.start();
        } else {
            finalTranscript = '';
        }
    });

    updateButtonState();
});

このコードでは下記のような処理が記載されています。

  • 入力フォームに何も入力されていないと、送信ボタンが非活性になり、入力されていると活性する。

  • 録音ボタンをクリックすると、音声認識が開始/停止します(UIも変化)。音声認識は日本語に設定されています。

  • 音声認識で得られた内容は、入力フォームに自動で入力されます。

  • フォームを送信すると、サーバーにテキストボックスの内容がJSON形式でPOSTされます。

ちなみに音声認識はWeb Speech APIを使用しています。

Flask構築と連携

最後にFlaskフレームワークを使用してWebアプリを構築し、AIキャラクターシステムと連携させます。

コードは以下のようになります。

server.py

import os
from dotenv import load_dotenv
from flask import Flask, request, jsonify, render_template
import sys

dotenv_path = os.path.join(os.path.dirname(__file__), '..', '.env')
load_dotenv(dotenv_path)

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

from aicharacter_system import AICharacterSystem

app = Flask(__name__)
aitalk_system = AICharacterSystem()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/submit-comment', methods=['POST'])
def submit_comment():
    data = request.json
    info = data['info']
    response_text = aitalk_system.process_input(info)
    return jsonify({"status": "success", "response": response_text})

if __name__ == '__main__':
    app.run(debug=False)

このコードではユーザーがブラウザから入力したテキストをPOSTリクエストとしてサーバーに送信して、FlaskがそのリクエストをAICharacterSystemに渡します
AICharacterSystemは、ユーザーの入力に基づいて応答を生成し、その応答をFlaskに返します

これで最初に実装した回答生成処理に渡されて音声として出力するという一連のサイクルが回るようになります。

イメージ

まとめと仕様説明

実装はこれで以上です。
以下が出来上がったサイトの画面になります。

成果物UI

成果物アクセス方法

こちらの画面にアクセスするには、ターミナルで以下順序でhttp://127.0.0.1:5000にアクセスします。

まず、cdでsite階層に移動します。

cd C:\aicharacter\site

移動したら実行コマンドをたたきます。

python server.py

これでhttp://127.0.0.1:5000にアクセスしてページが表示されれば成功です。

仕様説明

仕様説明はシロハナちゃんYouTube動画にて体験している様子があるので、それを見るほうが分かりやすいかと思います。

https://youtu.be/HXytKmTEeL0

以下に各機能の簡潔な説明を簡潔にまとめます。

システムイメージ
  • 入力フォーム:タイピングまたは音声入力でテキストを入力できます。

  • 音声開始ボタン:押下すると「録音中…」という文言で赤ボタンに変わって、マイクに向けて発生すると入力フォームに話した内容が文字起こしされます。

  • 送信ボタン:入力フォームに値が入っている場合、ボタンが活性して押下できるようになります。

    • ボタン押下して数秒後(大体2~3秒程度)にAIキャラの回答ボイスが出力されます。また、回答のテキスト内容も「ここに応答が表示されます」の部分に表示されます。

さいごに

ボリュームの大きい内容でしたが、いかがでしたでしょうか。
今回はシンプルな実装でまとめましたが、今後いろんなカスタマイズや追加機能などして楽しむのも良いかと考えています。

様々なAI会話サービスが多くの企業から出ていますが、どのような仕組みなのか知るのは面白いと思いますし、自分で自由にカスタマイズできるのは最高ですのでその参考になれば幸いです。

何か改善点や間違いなどあればXアカウントのDM等でご連絡いただけるととても助かります。

今後、こちらをもとにカスタマイズしてみた紹介なども考えていますので、ぜひフォローなどしていただけると励みになります。

最後に私がプロデュースしているAIVtuberシロハナちゃんの宣伝をさせてください。
理想のAIヒロインを目指して、AIを使ったリアルタイム配信や、AIヒロイン研究所というコンセプトのもと、「テクノロジー×キャラクター」に関する動画等を発信しています。
興味がありましたらぜひ!

以上!それではまた👋

ご支援は活動費に使わせていただきます