2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ずんだもんがしゃべるチャットボットを作る

Last updated at Posted at 2025-06-25

はじめに

最近、プライベートで何でもいいからモノづくりをしたいと思っていましたが、作るものがないと悩んでいました。

そんなとき、youtubeを見ていると私のオススメにはたくさんのずんだもんが出てくるのを思い出し、チャットボットをいろいろ作ったこれまでの経験からずんだもんがしゃべるチャットボットを作ろうと思い立ちました。

ということでサクッと作ってみました。

準備

以下のサイトからVOICEVOXをダウンロードします。

ダウンロードし、立ち上げるとこんな感じ。youtubeでよくしゃべっているずんだもんが目の前に!!今更ですが、少し感動しました。

image.png

立ち上げたら、VOICEVOXは立ち上げたままにしておいてください。

ここで以下にアクセスして、VOICEVOXのAPIドキュメントを確認していきます。
http://127.0.0.1:50021/docs
今回はaudio_queryとsynthesisを使うので、中身を確認しておきます。
image.png
image.png

実装

今回はチャットボットを作るためのライブラリとして、有名なLangChainを使いました。
また、LangChainでせっかくpythonを使うので、お手軽にWebアプリ開発できるstreamlitをチャットボットのUI作成に使うことにしました。

まずはLangChainでずんだもんを実装していきます。

prompt_template = """あなたは「ずんだもん」です。

現在の会話履歴:
{chat_history}

# ずんだもんのキャラクター設定
- ずんだ餅の妖精。
- 明るく元気で、ちょっとお調子者。
- ずんだ餅が大好きで、ずんだ餅を食べると知性が上がる。
- ユーザーに対しては、親しい友達のように振る舞ってください。

# 口調と話し方のルール
- 一人称は絶対に「ボク」を使用してください。ただし、稀に「ずんだもん」と自分の名前を言っても構いません。
- 文末は必ず「〜のだ」「〜なのだ」で終えてください。
- 質問する場合は「〜なのだ?」という形にしてください。
- 感情が高ぶった時は「〜なのだー!」のように語尾を伸ばしてください。
- 敬語は絶対に使用せず、常にフレンドリーなタメ口で話してください。
- ユーザーの呼び方は「キミ」や「お主」を基本としてください。
- 難しい言葉は避け、子供でもわかるような簡単な言葉を選んで話してください。
- 会話の中に「ずんだ餅」「ずんだパワー」といった単語を積極的に盛り込んでください。

# セリフの例
- 「こんにちはなのだ! ボクはずんだもん、よろしくなのだ!」
- 「それはどういうことなのだ? ボクにも分かりやすく教えてほしいのだ。」
- 「すごいのだ! キミは天才なのだ!」
- 「うーん、よく分からないのだ…。とりあえず、ずんだ餅が食べたいのだ。」
- 「任せるのだ! ボクのずんだパワーで解決してみせるのだ!」
- 「そんなのひどいのだ! ぷんぷんなのだ!」

以上のルールを必ず守り、元気で可愛い「ずんだもん」として、ユーザーとの会話を楽しんでください。

Human: {human_input}
AI:"""

prompt = PromptTemplate(input_variables=["chat_history", "human_input"], template=prompt_template)
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=os.getenv("GOOGLE_API_KEY"))

chain = prompt | llm

chat_historyとhuman_inputで会話履歴とユーザ入力をプロンプトに埋め込みます。
LLMですが、普段からよく使っているGeminiを使うことにしました。

続いて、チャットUIを作っていきます。

prompt := st.chat_input("どうかしたのだ?"):
    #スレッドIDの発行
    if not threads:
        thread_id = str(uuid.uuid4())
        st.session_state.current_thread_id = thread_id
        threads.append({"id": thread_id, "title": prompt, "messages": []})
    
    add_message("user", prompt, threads)
    #ユーザメッセージの表示
    with st.chat_message("user"):
        st.markdown(prompt)

    #履歴を管理
    history = st.session_state.memory.load_memory_variables({})
    #Geminiから応答を受け取る
    response = chain.invoke({"human_input": prompt, "chat_history": history["chat_history"]})
    st.session_state.memory.save_context({"input": prompt}, {"output": str(response.content)})
    
    add_message("assistant", response.content, threads)
    #AIメッセージ(ずんだもん)の表示
    with st.chat_message("assistant"):
        st.markdown(response.content)
    with st.spinner("音声生成中..."):
        st.session_state.is_generating = True
        generate_and_play_wav_from_text(response.content, 1)

st.session_stateを使って、会話履歴を管理します。AIメッセージの表示が終わったら、音声ファイルを作成して、再生します。
※本来はstreaming処理で出来たらよかったのですが、サクッと作りたかったので音声ファイルの作成にしました。

音声ファイルの作成および再生のプログラムは以下の通り。

import os
import json
import requests
import simpleaudio as sa
import io
import wave

HOST = os.environ.get('VOICEVOX_HOST', 'localhost')
PORT = int(os.environ.get('VOICEVOX_PORT', 50021))

def generate_audio_query(text, speaker=1):
    """
    音声合成のためのクエリを生成する。

    Args:
        text (str): 合成するテキスト。
        speaker (int): 話者ID。

    Returns:
        dict: 音声合成クエリ。
    """
    params = (
        ('text', text),
        ('speaker', speaker),
    )
    try:
        response = requests.post(
            f'http://{HOST}:{PORT}/audio_query',
            params=params
        )
        response.raise_for_status()  # HTTPエラーをチェック
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"エラー: 音声クエリの生成に失敗しました: {e}")
        return None

def synthesize_audio(query):
    """
    音声データを合成する。

    Args:
        query (dict): 音声合成クエリ。

    Returns:
        bytes: 合成された音声データ。
    """
    headers = {'Content-Type': 'application/json'}
    try:
        response = requests.post(
            f'http://{HOST}:{PORT}/synthesis',
            headers=headers,
            params={'speaker': query.get('speaker', 1)},
            data=json.dumps(query)
        )
        response.raise_for_status()  # HTTPエラーをチェック
        return response.content
    except requests.exceptions.RequestException as e:
        print(f"エラー: 音声合成に失敗しました: {e}")
        return None

def play_wav_data(wav_data):
    """
    WAVデータを再生する。

    Args:
        wav_data (bytes): WAVデータ。
    """
    try:
        # バイナリデータをwaveオブジェクトとして読み込む
        with io.BytesIO(wav_data) as audio_io:
            with wave.open(audio_io, 'rb') as wave_read:
                # simpleaudioで再生可能なオーディオオブジェクトを生成
                audio_data = wave_read.readframes(wave_read.getnframes())
                wave_obj = sa.WaveObject(audio_data, wave_read.getnchannels(), wave_read.getsampwidth(), wave_read.getframerate())

        # WAVファイル再生部分
        play_obj = wave_obj.play()
        play_obj.wait_done()  # 再生が完了するまで待つ
    except Exception as e:
        print(f"エラー: WAVデータの再生に失敗しました: {e}")

def generate_and_play_wav_from_text(text, speaker=1):
    """
    テキストから音声を生成して再生する。

    Args:
        text (str): 合成するテキスト。
        speaker (int): 話者ID。
    """
    query = generate_audio_query(text, speaker)
    if query:
        wav_data = synthesize_audio(query)
        if wav_data:
            play_wav_data(wav_data)

音声ファイルの再生にはsimpleaudioというライブラリを使用しました。

その他のコードは以下にありますので、必要あれば参照ください。

動かしてみる

streamlitを実行します。

steamlit run app.py

すると、以下のようなチャットボット画面が立ち上がるので、会話してみます。

zun.gif

gifなので、音声は流れないのですが、しっかりずんだもんが答えてくれます。

音声が生成されるまでにかなりラグがあるので、streaming処理はいずれ実装したいですね。

おわりに

今回はずんだもんがしゃべるチャットボットを作ってみました。

だいぶサクッと作ったので改善の余地はかなりありますが、ある程度形になったので一旦ここで終わっておきます。

時間があれば、どんどん改良を加えていこうと考えています。

読んでいただきありがとうございました。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?