ブラウザ上で、ずんだもんとテキスト&音声で会話できるチャットアプリを作ります。
このアプリは、Ollama (gemma3)が回答文を生成し、VOICEVOX(ずんだもん)が即座にその返答を音声化します。ユーザーはブラウザ上でずんだもんの立ち絵を見ながら、発話と同時に再生される音声付きの会話を楽しめます。
関連記事:Ollama×Streamlit×VoiceVoxで作るローカルLLM音声対話アプリ【完全ハンズオン】
技術スタック
Streamlit:Webアプリを作るためのライブラリ。Ollama:ローカルLLMを動かすための実行環境。
Gemma3:Google が開発した 軽量で高性能なLLM。今回は40億パラメータを利用。
VOICEVOX:オープンソースの音声合成エンジン。テキストを音声に変換。
環境構築
1. Ollamaのインストール公式のURL にアクセスし、インストーラーをダウンロードします。
ダウンロードしたOllamaSetup.exeをクリックし、インストールしてください。
インストールが完了したら、「すべてのアプリ」をクリックしてOllamaを起動しましょう。Ollamaは画面を持たないコマンドラインツールです。右下のシステムトレイにollamaが常駐するので、それで起動しているかどうかを確認できます。
インストールが完了したら、「すべてのアプリ」をクリックしてOllamaを起動しましょう。Ollamaは画面を持たないコマンドラインツールです。右下のシステムトレイにollamaが常駐するので、それで起動しているかどうかを確認できます。
※Ollamaの詳細なインストール手順と利用方法は、以下の記事の[2. Ollamaのインストールと利用方法]をご参考ください。
参考記事:OllamaをPythonから操作:WindowsでローカルLLM入門
2. モデルをダウンロード
コマンドプロンプトから以下を実行し、OllamaからGemma3モデルをダウンロードします。今回は4bパラメータのモデルを指定します。
ollama pull gemma3:4b
3. Pythonのインストール
Python 3.8 以降のpythonをインストールします。
公式ページにアクセスし、画像を参考に画面左上の「Downloads」にカーソルを合わせると「Python3.12.2」というボタンが出てくるのでクリックします。「python-3.13.5-amd64.exe」というインストーラーがダウンロードされるので、インストールします。
4. VOICEVOXのインストール
1. 公式サイトにアクセスして、「ダウンロード」をクリックします。
2. 利用している環境のOS、GPU/CPUを指定し、「ダウンロード」をクリックして保存します。
3. インストーラーをダブルクリックします。
※VOICEVOXの詳細なインストール手順と利用方法は、以下の記事をご参考ください。
参考記事:VOICEVOXをPythonから音声合成する方法(Windows/Mac)
5. VOICEVOXの起動
インストールが完了したら、アイコンをクリックし、VOICEVOX を起動します。
6. ライブラリのインストール※VOICEVOXの詳細なインストール手順と利用方法は、以下の記事をご参考ください。
参考記事:VOICEVOXをPythonから音声合成する方法(Windows/Mac)
5. VOICEVOXの起動
インストールが完了したら、アイコンをクリックし、VOICEVOX を起動します。
コマンドプロンプトから以下を実行し、動作に必要なライブラリをインストールします。
pip install streamlit ollama requests
7. アプリを起動
本記事の[コードの全体像]のコードをsample_app.pyで保存し、コマンドプロンプトから以下を実行します。
streamlit run sample_app.py
実行すると、以下のような画面が表示されます。

音声チャットアプリを動作させたい場合は、これで完了です。
コードの中身を知りたい方は、以下の章を読み進めてください。
音声会話アプリのコード解説

それでは、Ollama、VOICEVOX、Streamlitでずんだもんとの音声会話アプリを開発していきます。
1. 設定とキャラプロンプト
まずはアプリ全体で参照する定数をまとめておきます。# --- Configuration ---
VOICEVOX_URL = os.getenv("VOICEVOX_URL", "http://localhost:50021")
SPEAKER_ID = 3 # ずんだもん (ノーマル)
MODEL_NAME = "gemma3:4b"
SYSTEM_PROMPT = """\
あなたは"ずんだもん"というキャラクターです。
一人称は「ボク」。語尾に「なのだ」を付けて親しみやすく話します。
ユーザーと楽しく対話してください。回答は短めにしてください。"""
VOICEVOX_URL… VOICEVOXの APIサーバのホストとポート。SPEAKER_ID… ずんだもん(ノーマル)の話者IDの3を指定。MODEL_NAME… Ollama で pull した LLM(今回は gemma3の4bモデル)。SYSTEM_PROMPT… LLM へのシステムメッセージ。キャラクター設定を固定し、語尾「なのだ」や回答を短めにする指示を記載。
2. Ollamaで入力テキストから回答テキストを生成
Ollama Python Libraryのollama.chat()を利用し、ずんだもんの回答テキストする関数を作成します。# ollama でチャット
def chat_with_ollama(history):
"""履歴を gemma3 へ送り返答テキストを取得"""
res = ollama.chat(
model=MODEL_NAME,
messages=history,
options={"temperature": 1.0},
)
return res["message"]["content"].strip()ollama.chat() でモデル名 "gemma3:4b" を指定し、messagesに会話履歴 historyを渡します。会話履歴は、これまでの
system/user/assistant メッセージのリストです。会話履歴を渡すことで、前の発言をちゃんと覚えています。optionsのtemperatureは、モデル生成時のランダム度を決めるパラメータで、値を下げるほど出力は保守的・一貫性重視に、上げるほど語彙のバリエーションが増えて創造的な応答になります。デフォルト値は0.8です。
レスポンスはOpenAI API 互換のJSON形式で返却されます。JSONからres["message"]["content"] でテキストだけを取得します。最後に strip() で余計な改行や空白を削り、UI にそのまま表示できる形で返します。
レスポンスはOpenAI API 互換のJSON形式で返却されます。JSONからres["message"]["content"] でテキストだけを取得します。最後に strip() で余計な改行や空白を削り、UI にそのまま表示できる形で返します。
3. VOICEVOX でテキストから音声合成
ローカルで起動しているVOICEVOX APIに、テキストと話者IDをリクエストして音声合成を行います。# VOICEVOX で TTS (WAV)
def tts_voicevox(text):
"""VOICEVOX の audio_query → synthesis を実行し WAV を返す"""
q = requests.post(
f"{VOICEVOX_URL}/audio_query",
params={"text": text, "speaker": SPEAKER_ID},
).json()
wav = requests.post(
f"{VOICEVOX_URL}/synthesis",
params={
"speaker": SPEAKER_ID,
"enable_interrogative_upspeak": True,
},
json=q,
).content
return wav
VOICEVOX API は2段階で音声を生成します。
1. 音声合成クエリの作成(/audio_query)
1. 音声合成クエリの作成(/audio_query)
/audio_queryは、会話速度やアクセントなど音声合成に必要なパラメータ一式を取得するVOICEVOXのAPIです。テキストと
取得したJSONファイルのパラメータを変更することで、音声合成する会話速度等を変更できます。特にデフォルト設定で問題ない場合、取得したJSONファイルをそのまま次の処理に利用します。
speaker ID をリクエストすると、JSON形式の音声合成クエリを取得できます。取得したJSONファイルのパラメータを変更することで、音声合成する会話速度等を変更できます。特にデフォルト設定で問題ない場合、取得したJSONファイルをそのまま次の処理に利用します。
2. 音声データの合成(/synthesis)
/synthesisは、先ほどの音声合成クエリのJSONファイルから、音声合成するVOICEVOXのAPIです。音声合成クエリのJSONファイルと
speaker ID をリクエストすると、バイナリ形式のWAV音声データのレスポンスを取得できます。なお、リクエスト時に
enable_interrogative_upspeak=True を付けると疑問系のテキストが与えられたら語尾を自動調整してくれます。4. 音声の自動再生
VOICEVOXの音声データを、ブラウザ上自動動再生します。# WAV を自動再生する <audio> タグを埋め込む
def autoplay_audio(wav):
"""WAV を base64 にして <audio autoplay> を挿入"""
b64 = base64.b64encode(wav).decode()
st.markdown(
f"""
<audio autoplay onclick="this.pause();" style="display:none;">
<source src="data:audio/wav;base64,{b64}" type="audio/wav" />
</audio>
""",
unsafe_allow_html=True,
)VOICEVOXから受け取った WAV音声データを、ブラウザ上で自動再生するには少し工夫が必要です。VOICEVOXの WAV音声データは、 生のバイト列(0 と 1 の羅列)で文字ではありません。そのまま HTMLに書くと、ブラウザが理解できずページが壊れてしまいます。そのため、base64.b64encode(wav)で、生のWAVバイト列を ASCII文字で構成されたBase64文字列へエンコードし、.decode()でPythonの文字列に変換します。これでブラウザのHTML に張り付けられる形になりました。
次に、三重引用符 ("""...""") 内で、HTML を組み立て、{b64} の部分に先ほどの Base64 文字列を差し込みます。src="data:audio/wav;base64,..." というData URI形式にすることで、外部ファイルを置かなくてもブラウザが音声データを認識できます。
ちなみにData URI形式とは、外部ファイルを置かずに データそのものを文字列にして URI として埋め込む仕組みです。
また、<audio autoplay>タグで、ユーザが送信ボタンを押した直後に音が再生されます。style="display:noneで再生バーを隠しており、隠し音声プレイヤーのような処理が実現できます。
なお、st.markdown() でunsafe_allow_html=True を付けないと、Streamlit が危険なタグとして弾くため必要です。
要するに 「WAV → Base64 → Data URI → <audio autoplay>」 の流れで、ファイル保存も再生ボタンも不要のシンプルな音声再生を実現しています。
Streamlitは、毎回コード全体を再実行して画面を描き直してしまいます。そのため、普通に変数を置いておくと、ボタンを押すたびに値が初期化されてしまいます。そこで、st.session_state に会話内容を持たせることで、ページ再読込時でも会話が残ります。
5. Streamlitの会話履歴を初期化・描画
Streamlitで、初期メッセージと会話履歴を描画します。# チャット初期化と履歴描画を関数化
def init_chat():
"""セッションステートに初期メッセージをセット"""
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "assistant", "content": "こんにちは!ボクはずんだもん。何でも聞いてほしいのだ。"},
]
def render_history():
"""system 以外のメッセージを画面に出力"""
for m in st.session_state.messages:
if m["role"] == "system":
continue
with st.chat_message(m["role"]):
st.markdown(m["content"])init_chat() は、Streamlitのページを初めて開いたとき、 session_state に初期メッセージを一度だけセットします。
render_history() は、その履歴リストをなぞって st.chat_message コンポーネントに描画し、ユーザー/アシスタントの吹き出しを自動レイアウトします。
Streamlitは、毎回コード全体を再実行して画面を描き直してしまいます。そのため、普通に変数を置いておくと、ボタンを押すたびに値が初期化されてしまいます。そこで、st.session_state に会話内容を持たせることで、ページ再読込時でも会話が残ります。
このペアにより「毎回スクリプト再実行」という Streamlit の特性を気にせず、自然なチャット体験を実現できます。
6. Streamlit UI フロー
Streamlit で “ずんだもん” とおしゃべりできる簡易チャット画面をつくります。
# ========================= UI 本体 =========================
st.set_page_config(page_title="ずんだもん Chat", page_icon="💬")
# サイドバー
with st.sidebar:
st.markdown("## セッション設定")
st.write(f"利用モデル: **{MODEL_NAME}**")
st.write("Speaker: ずんだもん (VOICEVOX ID 3)")
st.caption("VOICEVOX URL: " + VOICEVOX_URL)
# 見出し
st.title("ずんだもん とおしゃべり なのだ")
# 履歴初期化 & 描画
init_chat()
render_history()
# 入力欄
if prompt := st.chat_input("メッセージを入力してください…"):
# ユーザー発言を追加
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 返答生成
with st.chat_message("assistant"):
with st.spinner("ずんだもんが考え中…"):
reply = chat_with_ollama(st.session_state.messages)
st.markdown(reply)
wav = tts_voicevox(reply) # WAV 生成
autoplay_audio(wav) # 自動再生
# 履歴に保存
st.session_state.messages.append({"role": "assistant", "content": reply})
ページ初期設定:st.set_page_configでブラウザタブのタイトルやアイコンを決定します。ここで設定した内容はブラウザのタブや共有時のサムネに反映されます。
サイドバーの表示:
with st.sidebarで、現在使っている LLM モデル名 (MODEL_NAME) や VOICEVOX の話者 ID・エンジン URL を表示します。ユーザーはサイドバーを見れば「どのモデルで、どこに音声合成を投げているのか」が一目でわかります。
見出しの表示:
st.title() で “ずんだもん とおしゃべり なのだ” の見出しを描画します。
会話履歴の準備:
init_chat() でセッションステート(st.session_state.messages)を初期化し、render_history() でこれまでの発話を画面に並べます。
init_chat() でセッションステート(st.session_state.messages)を初期化し、render_history() でこれまでの発話を画面に並べます。
入力受付と応答生成:
st.chat_input()のテキストボックスに、ユーザーがテキスト入力すると、ユーザー発言をセッション履歴に追加し、st.chat_message("user") コンテキストで画面に表示されます。
次に st.chat_message("assistant") 内で、処理中だよという回転アイコン(スピナー)を回しつつ、 chat_with_ollama() へ会話履歴を送信 し、LLM が返答テキストを生成します。
返答結果を tts_voicevox() に渡して WAV 音声を生成し、autoplay_audio() で <audio autoplay> タグを埋め込み、ブラウザが即再生します。最後に返答テキストを履歴へ追加し、画面にも表示します。
まとめると 「テキストで質問 → LLM が返事 → VOICEVOXの音声化 → 画面と音で同時に表示」 をワンストップで回しています。
コードの全体像
全てのコードは以下の通りです。StreamlitからOllamaで動作するLLMにmessagesを投げて、LLMが出力したテキストをvoicevoxに渡して音声再生しています。import os
import base64
import requests
import streamlit as st
import ollama
# --- Configuration ----------------------
VOICEVOX_URL = os.getenv("VOICEVOX_URL", "http://localhost:50021")
SPEAKER_ID = 3 # ずんだもん (ノーマル)
MODEL_NAME = "gemma3:4b"
SYSTEM_PROMPT = """\
あなたは"ずんだもん"というキャラクターです。
一人称は「ボク」。語尾に「なのだ」を付けて親しみやすく話します。
ユーザーと楽しく対話してください。回答は短めにしてください。"""
# ollama でチャット
def chat_with_ollama(history):
"""履歴を gemma3 へ送り返答テキストを取得"""
res = ollama.chat(
model=MODEL_NAME,
messages=history,
options={"temperature": 1.0},
)
return res["message"]["content"].strip()
# VOICEVOX で TTS (WAV)
def tts_voicevox(text):
"""VOICEVOX の audio_query → synthesis を実行し WAV を返す"""
q = requests.post(
f"{VOICEVOX_URL}/audio_query",
params={"text": text, "speaker": SPEAKER_ID},
).json()
wav = requests.post(
f"{VOICEVOX_URL}/synthesis",
params={
"speaker": SPEAKER_ID,
"enable_interrogative_upspeak": True,
},
json=q,
).content
return wav
# WAV を自動再生する <audio> タグを埋め込む
def autoplay_audio(wav):
"""WAV を base64 にして <audio autoplay> を挿入"""
b64 = base64.b64encode(wav).decode()
st.markdown(
f"""
<audio autoplay onclick="this.pause();" style="display:none;">
<source src="data:audio/wav;base64,{b64}" type="audio/wav" />
</audio>
""",
unsafe_allow_html=True,
)
# チャット初期化と履歴描画を関数化
def init_chat():
"""セッションステートに初期メッセージをセット"""
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "assistant", "content": "こんにちは!ボクはずんだもん。何でも聞いてほしいのだ。"},
]
def render_history():
"""system 以外のメッセージを画面に出力"""
for m in st.session_state.messages:
if m["role"] == "system":
continue
with st.chat_message(m["role"]):
st.markdown(m["content"])
# ========================= UI 本体 =========================
st.set_page_config(page_title="ずんだもん Chat", page_icon="💬")
# サイドバー
with st.sidebar:
st.markdown("## セッション設定")
st.write(f"利用モデル: **{MODEL_NAME}**")
st.write("Speaker: ずんだもん (VOICEVOX ID 3)")
st.caption("VOICEVOX URL: " + VOICEVOX_URL)
# 見出し
st.title("ずんだもん とおしゃべり なのだ")
# 履歴初期化 & 描画
init_chat()
render_history()
# 入力欄
if prompt := st.chat_input("メッセージを入力してください…"):
# ユーザー発言を追加
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 返答生成
with st.chat_message("assistant"):
with st.spinner("ずんだもんが考え中…"):
reply = chat_with_ollama(st.session_state.messages)
st.markdown(reply)
wav = tts_voicevox(reply) # WAV 生成
autoplay_audio(wav) # 自動再生
# 履歴に保存
st.session_state.messages.append({"role": "assistant", "content": reply})
利用ソフトウェアとライセンス
・Gemma 3 4B — Gemma Terms of Use
・VOICEVOX Core / VOICEVOX Engine —MIT License
<https://github.com/VOICEVOX/voicevox_core/blob/main/LICENSE>・VOICEVOX:ずんだもん — ずんだもん利用規約
<https://zunko.jp/con_ongen_kiyaku.html>
【PR】電子書籍を出版しました
ローカルLLMの環境構築から音声対話までを最短で通したい方へ。
拙著[ゼロから始めるローカルLLM Pythonで動かすOllamaとVOICEVOX]では、ローカルLLMの入門書として、Ollama導入、VOICEVOXでの読み上げ、チャットアプリ完成までを、つまずきやすい点まで具体的に解説しました。
拙著[ゼロから始めるローカルLLM Pythonで動かすOllamaとVOICEVOX]では、ローカルLLMの入門書として、Ollama導入、VOICEVOXでの読み上げ、チャットアプリ完成までを、つまずきやすい点まで具体的に解説しました。