注釈

こんにちは、SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebookへようこそ! Raspberry Pi、Arduino、ESP32について、愛好家仲間とより深く探求しましょう。

参加する理由

  • 専門家によるサポート: コミュニティとチームの助けを借りて、アフターセールスの問題や技術的な課題を解決します。

  • 学びと共有: ヒントやチュートリアルを交換して、スキルを向上させましょう。

  • 先行プレビュー: 新製品の発表や先行情報をいち早く入手できます。

  • 特別割引: 新製品の限定割引をお楽しみいただけます。

  • お祭りプロモーションとプレゼント: プレゼントキャンペーンやホリデープロモーションに参加しましょう。

👉 私たちと一緒に探求し、創造する準備はできましたか? [here] をクリックして、今すぐ参加しましょう!

6. ローカル音声チャットボット

このレッスンでは、これまで学んだすべて — 音声認識 (STT)テキスト読み上げ (TTS)、そして ローカルLLM (Ollama) — を組み合わせて、 Pironman 5 Pro MAX 上で完全にオフライン動作する 音声チャットボット を構築します。

ワークフローはシンプルです:

  1. 聴く — マイクが音声をキャプチャし、 Vosk でテキストに変換します。

  2. 考える — テキストは Ollama で動作する ローカルLLM (例: llama3.2:3b)に送信されます。

  3. 話す — チャットボットは Piper TTS を使用して回答を音声で出力します。

これにより、リアルタイムで理解し応答できる ハンズフリー対話ロボット が実現します。


始める前に

以下の準備が整っていることを確認してください:

  • Piper TTS をテストし (1. Piperのテスト)、動作する音声モデルを選択していること。

  • Vosk STT をテストし (Voskのテスト)、適切な言語パック(例: en-us)を選択していること。

  • Pi または別のコンピュータに Ollama をインストールし (1. Ollama (LLM) のインストールとモデルのダウンロード)、 llama3.2:3b などのモデルをダウンロードしていること(メモリが限られている場合は moondream:1.8b などのより小さいモデルでも可)。


コードの実行

  1. サンプルスクリプトを開きます:

    cd ~/sunfounder-voice-assistant/examples/
    sudo nano local_voice_chatbot.py
    
  2. 必要に応じてパラメータを更新します:

    • stt = Vosk(language="en-us"): アクセントや言語パックに合わせて変更します(例: en-uszh-cnes)。

    • tts.set_model("en_US-amy-low")1. Piperのテスト で確認した Piper 音声モデルに置き換えます。

    • llm = Ollama(ip="localhost", model="llama3.2:3b"): ご自身の設定に合わせて ipmodel の両方を更新します。

      • ip: Ollama が 同じ Pi で動作している場合は localhost を使用します。Ollama が LAN 内の別のコンピュータで動作している場合は、Ollama で Expose to network を有効にし、そのコンピュータの LAN IP を ip に設定します。

      • model: Ollama でダウンロード・有効化したモデル名と完全に一致している必要があります。

  3. スクリプトを実行します:

    cd ~/sunfounder-voice-assistant/examples/
    sudo python3 local_voice_chatbot.py
    
  4. 実行後、以下の動作が確認できます:

    • ボットが音声でウェルカムメッセージを出力します。

    • 音声入力を待機します。

    • Vosk が音声をテキストに変換します。

    • テキストが Ollama に送信され、回答がストリーム出力されます。

    • 回答がクリーンアップされ(隠れた推論を除去)、Piper によって音声出力されます。

    • プログラムは Ctrl+C でいつでも停止できます。


コード

import re
import time
from sunfounder_voice_assistant.llm import Ollama
from sunfounder_voice_assistant.stt import Vosk
from sunfounder_voice_assistant.tts import Piper

# 音声認識の初期化
stt = Vosk(language="en-us")

# TTSの初期化
tts = Piper()
tts.set_model("en_US-amy-low")

# 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."

# Ollama接続の初期化
llm = Ollama(ip="localhost", model="llama3.2:3b")
llm.set_max_messages(20)
llm.set_instructions(INSTRUCTIONS)

# ユーティリティ:隠れた推論の除去
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🎤 聴取中... (停止するには Ctrl+C を押してください)")

            # Voskから最終的な文字起こしを収集
            text = ""
            for result in stt.listen(stream=True):
                if result["done"]:
                    text = result["final"].strip()
                    print(f"[あなた] {text}")
                else:
                    print(f"[あなた] {result['partial']}", end="\r", flush=True)

            if not text:
                print("[情報] 認識されませんでした。もう一度お試しください。")
                time.sleep(0.1)
                continue

            # ストリーミングでOllamaに問い合わせ
            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 = 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[情報] 停止中...")
    finally:
        tts.say("Goodbye!")
        print("Bye.")

if __name__ == "__main__":
    main()

コード分析

インポートとグローバル設定

import re
import time
from sunfounder_voice_assistant.llm import Ollama
from sunfounder_voice_assistant.stt import Vosk
from sunfounder_voice_assistant.tts import Piper

これまで構築した3つのサブシステムをインポートします: Vosk (音声認識)、 Ollama (LLM)、 Piper (テキスト読み上げ)。

STT (Vosk) の初期化

stt = Vosk(language="en-us")

米国英語用の Vosk モデルをロードします。 精度を高めるために、言語コード(例: zh-cnes)を音声パックに合わせて変更します。

TTS (Piper) の初期化

tts = Piper()
tts.set_model("en_US-amy-low")

Piper エンジンを作成し、特定の音声を選択します。 1. Piperのテスト でテスト済みのモデルを選択してください。低品質の音声は高速で 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."

2つの重要な UX の選択:

  • 回答を短く直接的に 保ちます(TTS の明瞭さに役立ちます)。

  • 「思考の連鎖」タグを明示的に禁止し、ノイズの多い出力を減らします。

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)

ターミナルとスピーカーでユーザーに挨拶します。起動時に一度だけ行われます。

聴く(部分認識を含むストリーミングSTT)

print("\n🎤 聴取中... (停止するには Ctrl+C を押してください)")

text = ""
for result in stt.listen(stream=True):
    if result["done"]:
        text = result["final"].strip()
        print(f"[あなた] {text}")
    else:
        print(f"[あなた] {result['partial']}", end="\r", flush=True)
  • stream=True は即時フィードバックのための 部分認識 と、発話終了時の 最終認識 を返します。

  • 最終認識テキストは text に格納され、一度だけ表示されます。

ガード: 何も認識されなかった場合は LLM 呼び出しをスキップします:

if not text:
    print("[情報] 認識されませんでした。もう一度お試しください。")
    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 を一度だけ実行)

clean = strip_thinking(reply_accum)
if clean:
    tts.say(clean)
else:
    tts.say("Sorry, I didn't catch that.")
  • 最終テキストをクリーンアップして隠れたタグを除去し、 一度だけ音声出力 します。

  • TTS を1回だけにすることで、[LLM] / [SAY] のような繰り返しプロンプトを避けます。

終了と後処理

except KeyboardInterrupt:
    print("\n[情報] 停止中...")
finally:
    tts.say("Goodbye!")
    print("Bye.")

Ctrl+C を使用して停止します。ボットは短い別れの挨拶をして、正常終了を知らせます。


トラブルシューティング & FAQ

  • モデルが大きすぎる(メモリエラー)

    moondream:1.8b などのより小さいモデルを使用するか、より強力なコンピュータで Ollama を実行します。

  • Ollama からの応答がない

    Ollama が実行中であることを確認します( ollama serve またはデスクトップアプリが起動していること)。リモートの場合は Expose to network を有効にし、IP アドレスを確認します。

  • Vosk が音声を認識しない

    マイクが動作していることを確認します。必要に応じて別の言語パック( zh-cnes など)を試します。

  • Piper が無音またはエラー

    選択した音声モデルがダウンロードされ、1. Piperのテスト でテスト済みであることを確認します。

  • 回答が長すぎる、または的外れ

    INSTRUCTIONS を編集して 「回答は簡潔に要点を押さえてください。」 を追加します。