注釈
こんにちは。SunFounder Raspberry Pi & Arduino & ESP32 Facebook 愛好家コミュニティへようこそ! Raspberry Pi、Arduino、ESP32 について、仲間の愛好家と一緒にさらに深く探求しましょう。
参加する理由
専門的なサポート :コミュニティメンバーや公式チームの支援を受けて、購入後の問題や技術的な課題を解決できます。
学びと共有 :ヒントやチュートリアルを交換し、スキルを向上させましょう。
限定先行情報 :新製品の発表やプレビュー情報をいち早く入手できます。
特別割引 :最新製品を対象とした限定割引をお楽しみいただけます。
季節限定プロモーションとプレゼント企画 :プレゼントキャンペーンや祝日限定のプロモーションに参加できます。
👉 私たちと一緒に探求し、創造する準備はできましたか? [here] をクリックして、今すぐ参加しましょう!
6. ローカル音声チャットボット
このレッスンでは、これまで学んだ内容 — 音声認識( STT )、 音声合成( TTS )、そして ローカル LLM( Ollama ) — を組み合わせて、Fusion HAT 上で動作する、完全オフラインの 音声チャットボット を作成します。
ワークフローはシンプルです:
聞く — マイクが音声を収音し、 Vosk で文字起こしします。
考える — テキストを Ollama 上で動作するローカル **LLM**(例:
llama3.2:3b)へ送信します。話す — チャットボットが Piper TTS を使って音声で返答します。
これにより、リアルタイムで理解して応答できる ハンズフリー会話ロボット が実現します。
始める前に
以下が準備できていることを確認してください:
Piper TTS ( 1. Piper をテストする )をテストし、動作する音声モデルを選択していること。
Vosk STT ( 2. Vosk をテストする )をテストし、適切な言語パック(例:
en-us)を選択していること。Pi または別のコンピュータに Ollama ( 1. Ollama( LLM )をインストールしてモデルをダウンロードする )をインストールし、
llama3.2:3bのようなモデルをダウンロードしていること(メモリが限られている場合はmoondream:1.8bのような小さいモデル)。
コードを実行する
サンプルスクリプトを開きます:
cd ~/fusion-hat/examples/ sudo nano local_voice_chatbot.py
必要に応じてパラメータを更新します:
stt = Vosk(language="en-us"):アクセント/言語パックに合わせて変更します(例:en-us、zh-cn、es)。tts.set_model("en_US-amy-low"):1. Piper をテストする で確認した Piper の音声モデルに置き換えます。llm = Ollama(ip="localhost", model="llama3.2:3b"):ipとmodelの両方を自分の環境に合わせて更新します。ip:Ollama が 同じ Pi で動作している場合はlocalhostを使用します。Ollama が LAN 内の別のコンピュータで動作している場合は、Ollama で Expose to network を有効にし、ipをそのコンピュータの LAN IP に設定します。model:Ollama でダウンロード/有効化したモデル名と完全に一致している必要があります。
スクリプトを実行します:
cd ~/fusion-hat/examples/ sudo python3 local_voice_chatbot.py
実行すると、次のようになります:
ボットが音声のウェルカムメッセージで挨拶します。
音声入力を待ち受けます。
Vosk が音声をテキストに変換します。
テキストが Ollama に送信され、返信がストリーミングで返ってきます。
返信は(隠れた推論部分を除去して)整形され、Piper により音声で読み上げられます。
Ctrl+Cでいつでもプログラムを停止できます。
コード
import re
import time
from fusion_hat.llm import Ollama
from fusion_hat.stt import Vosk
from fusion_hat.tts import Piper
# Initialize speech recognition
stt = Vosk(language="en-us")
# Initialize TTS
tts = Piper()
tts.set_model("en_US-amy-low")
# Instructions for the LLM
INSTRUCTIONS = (
"You are a helpful assistant. Answer directly in plain English. "
"Do NOT include any hidden thinking, analysis, or tags like <think>."
)
WELCOME = "Hello! I'm your voice chatbot. Speak when you're ready."
# Initialize Ollama connection
llm = Ollama(ip="localhost", model="llama3.2:3b")
llm.set_max_messages(20)
llm.set_instructions(INSTRUCTIONS)
# Utility: clean hidden reasoning
def strip_thinking(text: str) -> str:
if not text:
return ""
text = re.sub(r"<\s*think[^>]*>.*?<\s*/\s*think\s*>", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"<\s*thinking[^>]*>.*?<\s*/\s*thinking\s*>", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"```(?:\s*thinking)?\s*.*?```", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"\[/?thinking\]", "", text, flags=re.IGNORECASE)
return re.sub(r"\s+\n", "\n", text).strip()
def main():
print(WELCOME)
tts.say(WELCOME)
try:
while True:
print("\n🎤 Listening... (Press Ctrl+C to stop)")
# Collect final transcript from Vosk
text = ""
for result in stt.listen(stream=True):
if result["done"]:
text = result["final"].strip()
print(f"[YOU] {text}")
else:
print(f"[YOU] {result['partial']}", end="\r", flush=True)
if not text:
print("[INFO] Nothing recognized. Try again.")
time.sleep(0.1)
continue
# Query Ollama with streaming
reply_accum = ""
response = llm.prompt(text, stream=True)
for next_word in response:
if next_word:
print(next_word, end="", flush=True)
reply_accum += next_word
print("")
# Clean and speak
clean = strip_thinking(reply_accum)
if clean:
tts.say(clean)
else:
tts.say("Sorry, I didn't catch that.")
time.sleep(0.05)
except KeyboardInterrupt:
print("\n[INFO] Stopping...")
finally:
tts.say("Goodbye!")
print("Bye.")
if __name__ == "__main__":
main()
コード解析
インポートとグローバル設定
import re
import time
from fusion_hat.llm import Ollama
from fusion_hat.stt import Vosk
from fusion_hat.tts import Piper
これまで構築してきた 3 つのサブシステムを取り込みます:音声→テキスト( STT )の Vosk 、LLM の Ollama 、テキスト→音声( TTS )の Piper です。
STT( Vosk )を初期化
stt = Vosk(language="en-us")
米国英語用の Vosk モデルを読み込みます。精度を高めるため、言語コード(例: zh-cn 、 es )を音声パックに合わせて変更してください。
TTS( Piper )を初期化
tts = Piper()
tts.set_model("en_US-amy-low")
Piper エンジンを作成し、特定の音声を選択します。1. Piper をテストする でテスト済みのモデルを選んでください。低品質( low )の音声は高速で、CPU 使用量も少なくなります。
LLM の指示文とウェルカム表示
INSTRUCTIONS = (
"You are a helpful assistant. Answer directly in plain English. "
"Do NOT include any hidden thinking, analysis, or tags like <think>."
)
WELCOME = "Hello! I'm your voice chatbot. Speak when you're ready."
UX 上の重要な 2 つのポイント:
回答は短く端的に 保つ( TTS の聞き取りやすさに役立ちます)。
ノイズの多い出力を減らすため、隠れた「 chain-of-thought 」タグを明示的に禁止します。
Ollama に接続して会話範囲を設定
llm = Ollama(ip="localhost", model="llama3.2:3b")
llm.set_max_messages(20)
llm.set_instructions(INSTRUCTIONS)
ip="localhost"は、Ollama サーバーが同じ Pi で動作している前提です。LAN 内の別マシンで動作している場合は、そのコンピュータの LAN IP を指定し、Ollama で Expose to network を有効にしてください。set_max_messages(20)は、会話履歴を短く保ちます。メモリ/レイテンシが厳しい場合は、この値を下げてください。
話す前に隠し推論/タグを除去
def strip_thinking(text: str) -> str:
if not text:
return ""
text = re.sub(r"<\s*think[^>]*>.*?<\s*/\s*think\s*>", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"<\s*thinking[^>]*>.*?<\s*/\s*thinking\s*>", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"```(?:\s*thinking)?\s*.*?```", "", text, flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r"\[/?thinking\]", "", text, flags=re.IGNORECASE)
return re.sub(r"\s+\n", "\n", text).strip()
モデルによっては内部用のタグ(例: <think>… )を出力する場合があります。この関数はそれらを除去し、TTS が最終回答 のみ を読み上げるようにします。
ヒント: 生トークンをストリーミング表示しているため画面上に他の断片が見えても、この関数により 音声 出力はクリーンな状態に保たれます。
メインループ:一度挨拶し、その後は 聞く → 考える → 話す
print(WELCOME)
tts.say(WELCOME)
ターミナルとスピーカーでユーザーに挨拶します。起動時に 1 回だけ実行されます。
聞く(ライブ部分結果付きの STT ストリーミング)
print("\n🎤 Listening... (Press Ctrl+C to stop)")
text = ""
for result in stt.listen(stream=True):
if result["done"]:
text = result["final"].strip()
print(f"[YOU] {text}")
else:
print(f"[YOU] {result['partial']}", end="\r", flush=True)
stream=Trueにすると、即時フィードバック用の 部分 文字起こしと、発話終了時の 最終 文字起こしが得られます。最終的に認識されたテキストは
textに保存され、1 回だけ表示されます。
ガード: 何も認識されなかった場合は、LLM 呼び出しをスキップします:
if not text:
print("[INFO] Nothing recognized. Try again.")
time.sleep(0.1)
continue
これにより、空のプロンプトをモデルに送信することを避けられます(時間とトークンの節約)。
考える( LLM :ストリーミング表示)
reply_accum = ""
response = llm.prompt(text, stream=True)
for next_word in response:
if next_word:
print(next_word, end="", flush=True)
reply_accum += next_word
print("")
最終文字起こしをローカル LLM に送信し、低レイテンシのため 到着したトークンを逐次表示 します。
同時に、後処理のために回答全文を
reply_accumに蓄積します。
注: 生トークンを表示したくない場合は stream=False に設定し、最終文字列だけを表示してください。
話す(整形してから TTS を 1 回だけ実行)
clean = strip_thinking(reply_accum)
if clean:
tts.say(clean)
else:
tts.say("Sorry, I didn't catch that.")
最終テキストから隠しタグを除去して整形し、その後 1 回だけ読み上げます 。
TTS を 1 回の実行にすることで、「[LLM] / [SAY]」のような繰り返しの読み上げを防げます。
終了と後始末
except KeyboardInterrupt:
print("\n[INFO] Stopping...")
finally:
tts.say("Goodbye!")
print("Bye.")
Ctrl+C で停止できます。ボットは短い別れのメッセージを話し、正常終了したことを知らせます。
トラブルシューティングと FAQ
モデルが大きすぎる(メモリエラー)
moondream:1.8bのような小さいモデルを使用するか、より強力なコンピュータで Ollama を実行してください。Ollama から応答がない
Ollama が起動していることを確認してください(
ollama serveまたはデスクトップアプリが開いていること)。リモートの場合は Expose to network を有効にし、IP アドレスを確認してください。Vosk が音声を認識しない
マイクが正常に動作しているか確認してください。必要に応じて別の言語パック(
zh-cn、esなど)も試してください。Piper が無音、またはエラーになる
選択した音声モデルがダウンロード済みであり、1. Piper をテストする でテスト済みであることを確認してください。
回答が長すぎる/話がそれる
INSTRUCTIONSを編集して、次を追加してください: 「Keep answers short and to the point.」