見出し画像

M5Stack版StackChan:ローカルAI化する:失敗しながら分かった Windows 11 + LM Studio + xiaozhi-esp32-server 構築手順

この記事は以下のM5Stack版 SackChanのビルド後の続きです。


プライバシーリスク、気になりますよね。

M5Stack版 Stack-Chan (スタックチャン) が、Xで人気です。
ただ、コミュニティから産まれたコミュニティ版が、OpenAI API/Gemini APIで慣れ親しんだ米国のCloud上 LLMに接続しているのに対し「M5Stack版の標準は、中国のCloud LLM:Xiaozhi(小智/小智AI)に接続するため、プライバシーが気になる」という方の声をXで見ました。 そこで、Cloud LLMとプライバシーリスクについてまとめたのが下記の投稿です。

しかし次に「M5Stack版スタックチャンをどうやってローカルLLMに接続するのか?」という疑問が出てきます。そこの疑問に答えるため、急ぎ動いた手順を公開します。私が試した時にやらかした?失敗も含む、Docker未インストール、`model.pt` 配置ミス、画像説明が中国語化、PC再起動でIP変動…本文中の失敗事例①〜⑧を経て、最終的にスタックチャンが「カメラで写真を撮って」と話しかけると日本語で画像説明を返すまでの全手順です。

はじめに

この記事は、M5Stack版スタックチャン / CoreS3 の AI.AGENT 接続先を、公式クラウドではなく、自分のWindows PC上で動かすローカルAIサーバーへ切り替えた記録です。下記の方のブログを参考にAIと一緒に構築、テストした記録です。この場で改めてお礼を申し上げます。

https://blog.rpine.net/posts/stackchan-local-llm

私のこの記事は、単なる成功手順だけではなく、実際に詰まった点、失敗した点、そこからどう直したかも残しています。Docker未インストール、`model.pt` の配置ミス、Thinking漏れ、画像説明が中国語になる問題、PC再起動でIPが変わる問題など、失敗しながら最終的に動くところまで持っていきました。
※チャッピーに参考ブログを読んで手順をまとめてもらったのですが、抜け漏れが激しく?試行錯誤することになりました。。

同じ構成を試す人が、同じ場所で迷わないようにすることを目的にしています。

対象読者

  • M5Stack版スタックチャンを持っている人

  • スタックチャンを公式クラウドではなく、ローカルAIで動かしてみたい人

    • プライバシーが気になる方ために書きました

  • Docker / PowerShell の基本操作に抵抗がない人

  • LM Studio のローカルLLMサーバー機能を使ってみたい人

  • 失敗ログを見ながら、原因を切り分ける手順も知りたい人

この記事を読み終えると

  • 公式クラウドを経由せず、自分のPC上で完結するスタックチャンの会話環境を作れるようになります

  • スタックチャンに「カメラで写真を撮って、前に何があるか教えて」と話しかけると、日本語で画像説明が返ってくる構成まで動かせます

  • 同じ構成を試す人が詰まりやすい失敗ポイントを、事前に把握できます


前提知識

DockerやESP-IDFの内部構造を深く理解している必要はありません。
ただし、PowerShellで `cd`、`dir`、`curl` などのコマンドを実行できることを前提にしています。

また、M5Stack版スタックチャンのファームウェアを書き換えるため、ESP-IDFでビルドできる環境も必要です。

所要時間の目安

  • Docker / LM Studio / ESP-IDF がすでに入っている場合:2〜3時間程度

  • 初回環境構築込み:半日程度

  • モデルダウンロード時間は別途必要

特に、ASR用の `model.pt` は約893MB、LLMモデルは10GBを超える場合があります。ネットワーク速度によっては、モデル取得だけでかなり時間がかかります。

検証環境

この記事では、以下の環境で動作確認しました。

OS: Windows 11 ネイティブ環境
PC: EVO-X2
CPU / GPU: Ryzen AI Max+ 395 / Radeon 8060S Graphics
メモリ: 128GB unified memory
ローカルAIサーバー: xiaozhi-esp32-server
Docker: Docker Desktop
LLMサーバー: LM Studio 0.4.13
通常会話モデル: openai/gpt-oss-20b
画像解析モデル: gemma-3-12b-it

一般的なノートPCで動かす場合は「LM Studioではなくollama」「モデルはGemma4 E4B」に切替えて使うと良いかもしれません。今回はせっかくなので20B級モデルや画像解析モデルを快適に動かすには、GPUメモリまたは共有メモリに余裕のあるPCである EVO-X2単体でテストしました。このマシンは128GBあるため、後々「画像生成/音楽生成/動画認識等」のモデルも追加して行こうと考えています。

作業全体の流れ

この記事では、次の順番で作業します。

[1] xiaozhi-esp32-server をGitHubから取得する        (5分)
   ↓
[2] DockerでローカルAIサーバーを起動する              (30分〜、初回イメージ取得とmodel.pt取得を含む)
   ↓
[3] LM Studioを起動し、OpenAI互換APIを有効にする       (LLMダウンロード込みで1時間〜)
   ↓
[4] .config.yaml をLM Studio向けに編集する             (15分)
   ↓
[5] ローカルAIサーバーPCのIPを固定する                 (環境による)
   ↓
[6] M5Stack版スタックチャンのOTA URLをローカルサーバーへ変更する
   ↓
[7] ファームウェアをビルド・書き込みする
   ↓
[8] スタックチャンからローカルAIサーバーへ接続する
   ↓
[9] 通常会話を確認する
   ↓
[10] function_call とカメラ画像説明を確認する

途中で失敗した箇所も、そのまま記録しています。成功手順だけではなく、「どこで詰まりやすいか」を確認しながら進めてください。

注意:外部公開しない前提です

この記事の構成は、家庭内LANや展示会場内のローカルネットワークで使うことを前提にしています。

LM Studioの「ローカルネットワークで提供」を有効にすると、同じネットワーク内の他の機器からもAPIへアクセスできるようになります。
また、xiaozhi-esp32-server は `0.0.0.0` で待ち受け、ポート `8000` と `8003` を公開します。

そのため、以下を守ってください。

  • インターネットへ直接公開しない

  • ルーターで外部ポート開放しない

  • 信頼できるLAN内で使う

  • 展示会などでは、必要に応じて専用ルーターやスマホテザリングを使う

  • Windowsファイアウォールの許可範囲を不用意に広げない

Docker DesktopやLM Studioの利用中に、Windows Defender ファイアウォールの許可ダイアログが出ることがあります。家庭内LANで使う場合は、基本的に信頼できるネットワーク内だけで使う前提にしてください。

中国語表記について

`xiaozhi-esp32-server` は中国語圏のOSSです。そのため、設定ファイルやログ、コメントには中国語が残っています。

この記事では、初出時に次のように併記します。

最小構成インストール(最简化安装)
全モジュールインストール(全模块安装)

ログ中の中国語も、動作確認に必要なものはそのまま掲載します。


0. この手順で作るもの

ここから先は、具体的な構築手順に入ります。
最終的に、M5Stack版 スタックチャン / CoreS3 の AI.AGENT 接続先を、公式サーバーではなく、自分のWindows PC上で動かすローカルAIサーバーへ切り替えます。

最終的に安定して動かした構成は次のとおりです。

M5Stack版 スタックチャン / CoreS3
  ↓ OTA
http://192.168.11.3:8003/xiaozhi/ota/

  ↓ WebSocket
ws://192.168.11.3:8000/xiaozhi/v1/

  ↓
Docker上の xiaozhi-esp32-server

  ↓ OpenAI互換API
LM Studio 0.4.13

  ↓ 通常会話LLM
openai/gpt-oss-20b

  ↓ 画像解析VLLM
gemma-3-12b-it

今回の検証では、以下まで確認できました。

  • 通常会話

  • function_call

  • カメラ起動

  • 写真撮影

  • 画像をLM Studioへ送信

  • 画像説明

  • 日本語でTTS読み上げ

  • 終了意図の処理

実際の成功ログでは、`self_camera_take_photo` が実行され、`self.camera.take_photo` が成功し、日本語の画像説明が返ってTTSへ流れています。通常会話はおおむね1〜4秒程度、カメラ撮影・画像説明はおおむね5〜6秒程度で発話が始まりました。

1. xiaozhi-esp32-server をGitHubから取得する

1.1 作業ディレクトリを作る

PowerShellを開き、作業用ディレクトリを作ります。

mkdir C:\m5sc_local_ai
cd C:\m5sc_local_ai

1.2 GitHubから取得する

git clone https://github.com/xinnan-tech/xiaozhi-esp32-server.git

取得後、今回作業するディレクトリへ移動します。

cd C:\m5sc_local_ai\xiaozhi-esp32-server\main\xiaozhi-server

このディレクトリが、今回の主な作業場所です。

C:\m5sc_local_ai\xiaozhi-esp32-server\main\xiaozhi-server

1.3 Git cloneで取得されるもの

`git clone` で取得されるのは、主に次のものです。

ソースコード
設定ファイル
docker-compose.yml
docker-compose_all.yml
app.py
requirements.txt
config.yaml
config_from_api.yaml
agent-base-prompt.txt
core/
config/
models/
plugins_func/

重要なのは、Git cloneだけではDockerイメージ本体やMySQL本体は取得されないという点です。

つまり、次のものは `git clone` だけでは入りません。

xiaozhi-esp32-server:server_latest のDockerイメージ本体
mysql:latest のDockerイメージ本体
redis:8.0 のDockerイメージ本体
Web管理画面用Dockerイメージ本体
SenseVoiceSmall の model.pt
LM Studio のLLMモデル

`git clone` は、あくまで ソースコードと設定ファイルを取得する操作 です。


1.4 最小構成インストール(最简化安装)と全モジュールインストール(全模块安装)の違い

このリポジトリには、主に2種類のDocker構成が含まれています。

docker-compose.yml
  → 最小構成インストール(最简化安装)で使う構成

docker-compose_all.yml
  → 全モジュールインストール(全模块安装)で使う構成

今回使用・動作確認したのは、docker-compose.yml を使う最小構成インストール(最简化安装) です。


1.5 何が、どの時点でインストール・取得されるか

サーバー構築者は、意図しないソフトウェアが勝手に入ることを警戒するため、ここは明確にしておきます。


操作1:`git clone https://github.com/xinnan-tech/xiaozhi-esp32-server.git`

この時点で取得・作成されるもの:

  • ソースコード

  • 設定ファイル

  • `docker-compose.yml`

  • `docker-compose_all.yml`

備考:Dockerイメージ本体、MySQL、Redis、LLMモデル、ASRモデル本体はまだ取得されない


操作2:`docker compose up -d`

この時点で取得・作成されるもの:

  • 最小構成インストール(最简化安装)用の `xiaozhi-esp32-server:server_latest` イメージ

  • `xiaozhi-esp32-server` コンテナ

備考:今回使用した構成。MySQL / Redis / 管理Webは起動しない


操作3:`docker compose -f docker-compose_all.yml up -d`

この時点で取得・作成されるもの:

  • サーバー本体

  • 管理Web

  • MySQL

  • Redis
    などのDockerイメージとコンテナ

備考:全モジュールインストール(全模块安装)。MySQLやRedisはWindowsへ手動インストールされるのではなく、Dockerコンテナとして起動する


操作4:`curl.exe -L ...model.pt...`

この時点で取得・作成されるもの:

  • SenseVoiceSmall の `model.pt`

備考:ASR用モデルファイル。Git cloneだけでは取得されない


操作5:LM Studioでモデルをダウンロード

この時点で取得・作成されるもの:

  • Gemma / Qwen / gpt-oss などのLLMモデル

備考:xiaozhi-serverのGit cloneとは別管理


今回の手順では、全モジュールインストール(全模块安装)は使いません

使うのは次です。

docker compose up -d

使わないのは次です。

docker compose -f docker-compose_all.yml up -d

2. ローカルAIサーバーをDockerで起動する

2.1 失敗事例①:Dockerが入っていなかった

最初に以下を実行したところ、`docker` コマンドが見つかりませんでした。

docker compose down
docker compose up -d
docker compose logs -f

エラー例:

docker: The term 'docker' is not recognized as a name of a cmdlet...

確認コマンド:

where.exe docker
Get-Command docker
Test-Path "C:\Program Files\Docker\Docker\Docker Desktop.exe"

この時点では、Docker Desktop が未インストールでした。


2.2 Docker Desktopをインストールする

PowerShellで以下を実行します。

winget install -e --id Docker.DockerDesktop

インストール後、Docker Desktopを起動します。

今回の検証では、WSL2関連の更新も必要でした。

wsl --update
wsl --shutdown

その後、Dockerが使えるか確認します。

docker --version
docker compose version

確認例:

Docker version 29.4.3
Docker Compose version v5.1.3

2.3 Docker Composeで起動する

作業ディレクトリへ移動します。

cd C:\m5sc_local_ai\xiaozhi-esp32-server\main\xiaozhi-server

起動します。

docker compose up -d

初回はDockerイメージの取得に時間がかかります。

起動状態を確認します。

docker compose ps

期待する状態:

NAME                   STATUS
xiaozhi-esp32-server   Up

ポートは以下が公開されます。

8000 → WebSocket
8003 → OTA / Vision API

実際に成功したときは、`xiaozhi-esp32-server` が `Up` になり、`0.0.0.0:8000->8000/tcp` と `0.0.0.0:8003->8003/tcp` が公開されていました。


2.4 失敗事例②:コンテナがRestartingになる

起動してもコンテナが `Restarting` になる場合があります。

確認:

docker compose ps

ログ確認:

docker compose logs --tail 200

今回の失敗原因は、`models/SenseVoiceSmall/model.pt` がファイルではなくディレクトリ扱いになっていたことでした。

ログには以下のようなエラーが出ていました。

IsADirectoryError: [Errno 21] Is a directory: 'models/SenseVoiceSmall/model.pt'

これは、ASR用の `model.pt` が正しく配置されていない状態です。


2.5 SenseVoiceSmall の model.pt を取得する

誤った `model.pt` を削除します。

Remove-Item .\models\SenseVoiceSmall\model.pt -Recurse -Force

存在確認:

Test-Path .\models\SenseVoiceSmall\model.pt

`False` になれば削除できています。

次に、モデルファイルを取得します。

curl.exe -L `
  "https://www.modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" `
  -o ".\models\SenseVoiceSmall\model.pt"

取得後に確認します。

Get-Item .\models\SenseVoiceSmall\model.pt | Format-List FullName,Mode,Length

成功例:

Mode   : -a---
Length : 936291369

実際の検証でも、`model.pt` を約893MBダウンロードし、`Length : 936291369` のファイルとして配置できた後に、コンテナ起動へ進めました。


2.6 8000 / 8003 ポートを確認する

サーバーが起動したら、まずPC自身から確認します。

Test-NetConnection 127.0.0.1 -Port 8003
Test-NetConnection 127.0.0.1 -Port 8000

期待値:

TcpTestSucceeded : True

次に、LAN側IPで確認します。

Test-NetConnection 192.168.11.3 -Port 8003
Test-NetConnection 192.168.11.3 -Port 8000

さらに、OTAエンドポイントを確認します。

curl.exe -i http://192.168.11.3:8003/xiaozhi/ota/

期待値:

HTTP/1.1 200 OK

OTA接口运行正常,向设备发送的websocket地址是:ws://192.168.11.3:8000/xiaozhi/v1/

2.7 失敗事例③:PC再起動でIPアドレスが変わった

途中で、CoreS3側のAI Avatarに以下のようなメッセージがループ表示され、会話を受け付けなくなりました。

Check for new version failed, will retry in 320 seconds: code=104

PC側からも以下が失敗しました。

curl.exe -i http://192.168.11.3:8003/xiaozhi/ota/

原因は、PC再起動後にローカルAIサーバーPCのIPが変わったことでした。

変更前: 192.168.11.3
変更後: 192.168.11.32

CoreS3側ファームウェアでは `192.168.11.3` を見に行くようにしていたため、PCのIPが変わると接続できません。

対策は、ローカルAIサーバーPCのIPを固定することです。

推奨:

ルーターのDHCP固定割当で、ローカルAIサーバーPCを 192.168.11.3 に固定する

2.8 今回最終的に使った docker-compose.yml

今回使ったのは、`docker-compose.yml` を使う最小構成インストール(最简化安装)です。全モジュールインストール(全模块安装)用の `docker-compose_all.yml` は使っていません。

最終的な `docker-compose.yml` の重要部分は以下です。

# Docker安装Server

version: '3'
services:
  xiaozhi-esp32-server:
    image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
    container_name: xiaozhi-esp32-server
    restart: always
    security_opt:
      - seccomp:unconfined
    environment:
      - TZ=Asia/Shanghai
    ports:
      # ws服务端
      - "8000:8000"
      # http服务的端口,用于简单OTA接口(单服务部署),以及视觉分析接口
      - "8003:8003"
    volumes:
      # 配置文件目录
      - ./data:/opt/xiaozhi-esp32-server/data
      # 模型文件掛接。SenseVoiceSmall の model.pt をコンテナへ渡す
      - ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt
      # VLLM画像説明を日本語化するため、修正済み openai.py をマウントする
      - ./core/providers/vllm/openai.py:/opt/xiaozhi-esp32-server/core/providers/vllm/openai.py:ro

最後の `openai.py` のマウントは、カメラ画像説明を日本語で返すための修正を恒久化するために追加しました。これを入れないと、ホスト側の `openai.py` を修正しても、コンテナ再作成時に元の中国語指定へ戻る可能性があります。

なお、`version: '3'` については、Docker Compose v5系では「obsolete」と警告が出ます。ただし今回の検証では、動作自体には影響しませんでした。

また、`TZ=Asia/Shanghai` は元の `docker-compose.yml` の値そのままです。今回の検証ではこのままで問題なく動作しましたが、日本時間に揃えたい場合は `TZ=Asia/Tokyo` に変更してもよいと思います。


3. LM Studioを起動し、OpenAI互換APIを有効にする

LM Studio 0.4.13 を起動し、Developer / Server 機能で OpenAI互換APIを有効にします。

今回使用したAPIは以下です。

http://localhost:1234/v1

LM Studio側では「ローカルネットワークで提供」を有効にしておきます。実際のログでは、OpenAI互換APIとして以下のようなエンドポイントが表示されました。

GET  http://192.168.11.3:1234/v1/models
POST http://192.168.11.3:1234/v1/chat/completions
POST http://192.168.11.3:1234/v1/embeddings

`xiaozhi-esp32-server` からはDockerコンテナ内からホストPC上のLM Studioへアクセスするため、`.config.yaml` では次のURLを使います。

http://host.docker.internal:1234/v1

モデル一覧を確認します。

Invoke-RestMethod http://localhost:1234/v1/models | ConvertTo-Json -Depth 5

モデルIDだけを見たい場合:

(Invoke-RestMethod http://localhost:1234/v1/models).data.id

今回の検証では、複数のモデルを切り替えて試しました。途中では `google/gemma-4-e4b`、`qwen3.5-27b`、`openai/gpt-oss-20b`、`google/gemma-4-26b-a4b` などを試しました。

最終的に、現在の動作確認済み構成では以下を使っています。

通常会話LLM:
  openai/gpt-oss-20b

画像解析VLLM:
  gemma-3-12b-it

LM Studioログでは、`gpt-oss-20b-MXFP4.gguf` が読み込まれ、AMD Radeon 8060S Graphics / Vulkan が使われていることを確認しました。ログ上では、`file size = 11.27 GiB`、`Vulkan0 model buffer size = 10949.35 MiB`、`offloaded 25/25 layers to GPU` のような表示が出ています。

この構成では、通常会話はおおむね1〜4秒程度、カメラ撮影・画像説明はおおむね5〜6秒程度で返答が始まりました。

4. .config.yaml をLM Studio向けに編集する

4.1 設定ファイルを開く

cd C:\m5sc_local_ai\xiaozhi-esp32-server\main\xiaozhi-server
notepad .\data\.config.yaml

4.2 サーバー設定

ローカルAIサーバーPCのIPを `192.168.11.3` に固定している前提です。

server:
  ip: 0.0.0.0
  port: 8000
  http_port: 8003
  websocket: ws://192.168.11.3:8000/xiaozhi/v1/
  vision_explain: http://192.168.11.3:8003/mcp/vision/explain
  timezone_offset: +9

4.3 プロンプトを設定する

検証中、元の `agent-base-prompt.txt` には英語・中国語の長いルールが入り、Gemma系モデルで `Plan` や `Execution` のような思考風テキストが音声に漏れる原因になりました。

そこで、最小テンプレートを作成します。

@'
{{base_prompt}}

ユーザーへ話す最終回答だけを出力してください。
思考過程、分析、方針、Plan、Reasoning、対応手順、JSON、Markdown、コードブロック、ツール呼び出し説明は出力しないでください。
日本語で1〜2文だけ返答してください。
'@ | Set-Content -Encoding UTF8 .\data\agent-minimal-prompt.txt

`.config.yaml` では以下を指定します。

prompt_template: data/agent-minimal-prompt.txt

4.4 selected_module の設定

最終的な通常会話+カメラ連携用設定は以下です。

selected_module:
  VAD: SileroVAD
  ASR: FunASR
  LLM: LMStudioLLM
  VLLM: LMStudioVLLM
  TTS: EdgeTTS
  Memory: nomem
  Intent: function_call

注意点:

通常会話だけの切り分けでは Intent: nointent を使った
カメラを使うには Intent: function_call が必要だった
Memory はまだ nomem のまま

4.5 Intent と Memory

Intent:
  nointent:
    type: nointent
  function_call:
    type: function_call

Memory:
  nomem:
    type: nomem
  mem_local_short:
    type: mem_local_short
    llm: LMStudioLLM

現時点では、カメラ・function_call の切り分けを優先するため、`Memory: nomem` のままにしています。


4.6 ASR / VAD

ASR:
  FunASR:
    type: fun_local
    model_dir: models/SenseVoiceSmall
    output_dir: tmp/

VAD:
  SileroVAD:
    type: silero
    threshold: 0.5
    threshold_low: 0.3
    model_dir: models/snakers4_silero-vad
    min_silence_duration_ms: 200

4.7 LLM / VLLM

最新の動作確認済み設定では、通常会話用のLLMと画像解析用のVLLMを分けています。

LLM:
  LMStudioLLM:
    type: openai
    model_name: openai/gpt-oss-20b
#    model_name: google/gemma-4-26b-a4b
#    model_name: unsloth/gemma-4-26b-a4b-it
    url: http://host.docker.internal:1234/v1
    api_key: lm-studio

VLLM:
  LMStudioVLLM:
    type: openai
#    model_name: qwen_qwen3.5-9b
    model_name: gemma-3-12b-it
    url: http://host.docker.internal:1234/v1
    api_key: lm-studio

`api_key` はLM Studioでは実質ダミーですが、OpenAI互換クライアントがキー欄を要求するため入れています。

この構成にした理由は、通常会話では `openai/gpt-oss-20b` が比較的速く安定し、画像解析では `gemma-3-12b-it` がカメラ画像説明に使えたためです。

以前は `google/gemma-4-26b-a4b` をLLM/VLLM両方に指定して試していましたが、最新のブログ公開用構成では、通常会話と画像解析を分けた構成に更新しています。

4.8 TTS

TTS:
  EdgeTTS:
    type: edge
    voice: ja-JP-NanamiNeural
    output_dir: tmp/
    language: "Japanese"

5. PCのIPを固定する

CoreS3側ファームウェアには、ローカルAIサーバーのIPを直接書き込みます。

今回の前提は以下です。

192.168.11.3

PC再起動でIPが変わると、CoreS3側がOTA確認に失敗します。

そのため、ルーター側でDHCP固定割当を行い、ローカルAIサーバーPCを常に以下に固定します。

192.168.11.3

確認:

ipconfig

確認すべき値:

IPv4 アドレス: 192.168.11.3

サーバー確認:

curl.exe -i http://192.168.11.3:8003/xiaozhi/ota/

6. M5Stack版 StackChanファームウェアのOTA URLをローカルサーバーに変更する

6.1 ファームウェアソースへ移動する

今回の作業例:

cd C:\m5sc_test_reject\StackChan\firmware

6.2 OTA URLの場所を検索する

Get-ChildItem -Recurse -Include *.c,*.cc,*.cpp,*.h,*.hpp |
  Select-String -Pattern "api.tenclass.net|xiaozhi/ota|GetCheckVersionUrl|CONFIG_OTA_URL"

対象ファイルは以下でした。

xiaozhi-esp32\main\ota.cc

6.3 ota.cc を編集する

notepad .\xiaozhi-esp32\main\ota.cc

`GetCheckVersionUrl()` をローカルAIサーバーへ向けます。

std::string Ota::GetCheckVersionUrl() {
    return "http://192.168.11.3:8003/xiaozhi/ota/";
}

確認:

Select-String -Path .\xiaozhi-esp32\main\ota.cc -Pattern "GetCheckVersionUrl|192.168.11.3|CONFIG_OTA_URL|ota_url"

7. ファームウェアをビルド・書き込みする

7.1 ESP-IDF環境を有効化する

PowerShellで以下を実行します。

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force

$env:IDF_TOOLS_PATH="C:\Espressif\tools"
$env:IDF_PATH="C:\esp\v5.5.4\esp-idf"

. C:\esp\v5.5.4\esp-idf\export.ps1

確認:

idf.py --version
python --version
where.exe idf.py

成功例:

ESP-IDF v5.5.4
Python 3.13.1

7.2 失敗事例④:idf.py が見つからない

最初は以下のエラーが出ました。

idf.py: The term 'idf.py' is not recognized...

原因は、ESP-IDF環境変数がPowerShellに読み込まれていなかったことです。

対策は、上記の `IDF_TOOLS_PATH` / `IDF_PATH` / `export.ps1` を毎回実行することです。


7.3 ビルドする

cd C:\m5sc_test_reject\StackChan\firmware

idf.py build

成功時は、以下のような表示になります。

Project build complete. To flash, run:
 idf.py flash

7.4 書き込みする

idf.py flash

または、ビルド完了時に表示された `esptool` コマンドを使います。


8. StackChanからローカルAIサーバーへ接続する

8.1 サーバー側ログを開く

ローカルAIサーバーPCで以下を実行します。

cd C:\m5sc_local_ai\xiaozhi-esp32-server\main\xiaozhi-server
docker compose logs -f

CoreS3を起動します。


8.2 OTA接続を確認する

成功すると、サーバーログに以下が出ます。

OTA请求设备ID
未配置MQTT网关,为设备 ... 下发WebSocket配置
设备 ... 固件已是最新

実際の成功ログでは、デバイスID `44:1b:f6:e0:ab:30` からOTAリクエストが来て、WebSocket設定が下り、その後 `192.168.11.3` へWebSocket接続しています。


8.3 WebSocket接続を確認する

成功時のログ例:

Headers: {'host': '192.168.11.3', ... 'upgrade': 'websocket'}

これが出れば、CoreS3がローカルAIサーバーへ接続できています。


9. 通常会話を確認する

9.1 まず nointent / nomem で確認した

最初は通常会話だけを安定させるため、以下にしました。

Memory: nomem
Intent: nointent

この構成では function_call は使いませんが、通常会話の切り分けには向いています。


9.2 失敗事例⑤:Gemma 4 E4BでThinking風テキストが漏れた

`google/gemma-4-e4b` では、以下のような内容がTTSに流れました。

ユーザーからの挨拶と確認のメッセージが来たので
Plan:
Execution:

原因候補は、モデルの出力癖と、元の重い `agent-base-prompt.txt` の影響です。

対策:

prompt_template を data/agent-minimal-prompt.txt に変更
モデルを変更して比較

9.3 モデル比較

今回の検証では、複数のモデルを切り替えて試しました。

google/gemma-4-e4b

速い
ただし Thinking / Plan / Execution が音声に漏れた

軽量で反応は速いものの、通常会話中に「ユーザーからの挨拶と確認のメッセージが来たので」「Plan:」「Execution:」のような思考風テキストがTTSに流れました。会話ロボット用途では、内部の推論過程が音声に出ると体験が壊れるため、通常会話用としては不採用にしました。

qwen3.5-27b

Thinking漏れなし
ただし応答開始まで約98秒で遅すぎた

Thinking漏れは止まりましたが、応答開始まで約98秒かかりました。品質以前に、会話ロボットとしては待ち時間が長すぎるため不採用にしました。

openai/gpt-oss-20b

通常会話は良好
Thinking漏れなし
返答開始は約1〜4秒程度

通常会話用として最も扱いやすかったモデルです。LM Studioログでは、`gpt-oss-20b-MXFP4.gguf` が読み込まれ、AMD Radeon 8060S Graphics / Vulkan にオフロードされていることを確認しました。現在のブログ公開用構成では、通常会話LLMとして `openai/gpt-oss-20b` を採用しています。

google/gemma-4-26b-a4b

通常会話は良好
Thinking漏れなし
カメラ画像解析にも使える

途中の検証では、通常会話・画像解析の両方に使える候補でした。ただし、最終的なブログ公開用構成では、通常会話は `openai/gpt-oss-20b`、画像解析は `gemma-3-12b-it` に分けています。

gemma-3-12b-it

画像解析VLLMとして採用
カメラ画像説明を日本語で返せた

最新の設定では、VLLMに `gemma-3-12b-it` を指定しています。カメラ撮影後、画像に写っている弁当箱、ご飯、箸、中華料理のような内容を日本語で説明できました。


9.4 通常会話テスト

発話例:

こんにちは、聞こえますか?
今これを食べています
ご飯は美味しいよね
ありがとう

確認ポイント:

日本語で返る
短く返る
Thinking / Plan / Execution が音声に出ない
TTSが失敗しない

最新ログでは、たとえば「こんにちは。」に対して「こんにちは。今日はどんなことに興味がありますか?」と返り、「今おヘルを食べています。」というASR結果に対しても「それは美味しそうですね。どんな料理ですか?」と自然に返答しました。

※「今おヘルを食べています。」はASR(音声認識)の誤認識ログです。実際には「今これを食べています」と話しています。SenseVoiceSmall の認識精度の参考として、誤認識のままログを残しています。

通常会話の実測目安は以下です。

「こんにちは。」
  ASR認識: 11:46:27
  最初の音声送信: 11:46:31
  発話開始まで: 約4秒

「今おヘルを食べています。」
  ASR認識: 11:46:42
  最初の音声送信: 11:46:43
  発話開始まで: 約1秒

これは厳密なベンチマークではなく、実機会話中のログ時刻から読んだ目安です。

10. function_callとカメラを確認する

10.1 Intentをfunction_callに戻す

カメラを使うには、`Intent: function_call` が必要です。

selected_module:
  VAD: SileroVAD
  ASR: FunASR
  LLM: LMStudioLLM
  VLLM: LMStudioVLLM
  TTS: EdgeTTS
  Memory: nomem
  Intent: function_call

10.2 失敗事例⑥:nointentのままではカメラに行かない

`Intent: nointent` のままでは、以下のように聞いてもカメラツールは呼ばれませんでした。

あなたの前には何が見えますか?

その場合、LLMは普通のテキスト質問として扱い、以下のように返しました。

私には、あなたの送ってくれたメッセージが見えていますよ

ログ上も、`tools` / `tool_calls` が渡っておらず、普通の会話リクエストだけでした。


10.3 function_call有効時の成功ログ

`Intent: function_call` にすると、サーバーログに以下が出ます。

self_camera_take_photo
self.camera.take_photo

実際の成功ログでは、サーバーが `self_camera_take_photo` を実行し、CoreS3へ `self.camera.take_photo` を送っています。


10.4 失敗事例⑦:画像説明が中国語になる

カメラ起動・撮影・画像解析までは成功しましたが、最初は画像説明が中国語で返りました。

原因は以下でした。

core\providers\vllm\openai.py:43:
question = question + "(请使用中文回复)"

LM Studioへ送られる画像解析リクエストが、以下になっていました。

目の前に何かありますか?(请使用中文回复)

その結果、中国語の説明がそのままTTSへ流れました。


10.5 openai.py を日本語向けに修正する

ホスト側のファイルを開きます。

notepad .\core\providers\vllm\openai.py

以下をコメントアウトします。

#        question = question + "(请使用中文回复)"

代わりに以下を追加します。

        question = question + "\n日本語で、話し言葉の1〜2文で短く答えてください。箇条書きやMarkdownは使わないでください。"

10.6 失敗事例⑧:ホスト側ファイルを直してもコンテナに反映されない

ホスト側の `openai.py` を修正しても、コンテナ内は古いままでした。

一時対応として以下を使いました。

docker cp .\core\providers\vllm\openai.py xiaozhi-esp32-server:/opt/xiaozhi-esp32-server/core/providers/vllm/openai.py

確認:

docker exec -it xiaozhi-esp32-server sh -lc "grep -n 'question = question +' /opt/xiaozhi-esp32-server/core/providers/vllm/openai.py"

期待値:

43:#        question = question + "(请使用中文回复)"
44:        question = question + "\n日本語で、話し言葉の1〜2文で短く答えてください。箇条書きやMarkdownは使わないでください。"

10.7 docker-compose.ymlで恒久化する

`docker cp` は、`docker compose down` で消える可能性があります。

そこで、`docker-compose.yml` の `volumes:` に以下を追加しました。

volumes:
  - ./data:/opt/xiaozhi-esp32-server/data
  - ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt
  - ./core/providers/vllm/openai.py:/opt/xiaozhi-esp32-server/core/providers/vllm/openai.py:ro

その後、あえて作り直しました。

docker compose down
docker compose up -d

確認:

docker exec -it xiaozhi-esp32-server sh -lc "grep -n 'question = question +' /opt/xiaozhi-esp32-server/core/providers/vllm/openai.py"

期待値:

43:#        question = question + "(请使用中文回复)"
44:        question = question + "\n日本語で、話し言葉の1〜2文で短く答えてください。箇条書きやMarkdownは使わないでください。"

これで、`docker compose down → up -d` 後も日本語指定が残ることを確認しました。


10.8 カメラテスト

サーバーログを開きます。

docker compose logs -f

StackChanに話しかけます。

カメラで写真を撮って、前に何があるか教えて

今回の最新テストでは、次のような発話でもカメラ撮影と画像説明に進みました。

カメラで写真を撮ってみてください。
写真を撮って今これを食べてるよ。
カメラで写真を撮ってこれを食べてるよ。

見るべきログ:

self_camera_take_photo
self.camera.take_photo 成功
response が日本語
TTS 生成成功

実際の成功例:

はい、写真を撮りましたよ。ちょうど夕食中でした。
写真にはご飯が入ったお弁当箱と、それを箸で持っている人の顔が写っていますね。美味しそうなお米ご飯みたいです!
美味しそうな中華料理ですね。熱々そうに見えます。

カメラ撮影・画像説明の実測目安は以下です。

「カメラで写真を撮ってみてください。」
  ASR認識: 11:46:54
  ツール実行: 11:46:55
  カメラ結果受信: 11:46:58
  最初の音声送信: 11:46:59
  発話開始まで: 約5秒

「写真を撮って今これを食べてるよ。」
  ASR認識: 11:47:14
  ツール実行: 11:47:15
  カメラ結果受信: 11:47:19
  最初の音声送信: 11:47:19
  発話開始まで: 約5秒

「カメラで写真を撮ってこれを食べてるよ。」
  ASR認識: 11:47:47
  ツール実行: 11:47:49
  カメラ結果受信: 11:47:52
  最初の音声送信: 11:47:53
  発話開始まで: 約6秒

この結果から、現在の構成では、カメラを使った画像説明も実用的な待ち時間で動くことが確認できました。

11. 現在の最終状態

11.1 ローカルAIサーバー

OS:
  Windows 11 ネイティブ環境

Docker Compose:
  docker-compose.yml

構成:
  最小構成インストール(最简化安装)

コンテナ:
  xiaozhi-esp32-server

ポート:
  8000 WebSocket
  8003 OTA / Vision API

11.2 LM Studio

LM Studio:
  0.4.13

OpenAI互換API:
  http://localhost:1234/v1
  http://192.168.11.3:1234/v1

通常会話LLM:
  openai/gpt-oss-20b

画像解析VLLM:
  gemma-3-12b-it

LM Studio側では、`gpt-oss-20b-MXFP4.gguf` が AMD Radeon 8060S Graphics / Vulkan 上で動作していることをログで確認しました。

11.3 xiaozhi-server

Memory: nomem
Intent: function_call
LLM: LMStudioLLM
VLLM: LMStudioVLLM
TTS: EdgeTTS
ASR: FunASR
VAD: SileroVAD

現在の `.config.yaml` では、通常会話用LLMを `openai/gpt-oss-20b`、画像解析用VLLMを `gemma-3-12b-it` に分けています。

11.4 CoreS3 / StackChan

OTA URL:
http://192.168.11.3:8003/xiaozhi/ota/

11.5 実測パフォーマンスの目安

今回の実機ログから見ると、通常会話はおおむね1〜4秒程度、カメラ撮影・画像説明はおおむね5〜6秒程度で発話が始まりました。

通常会話:
  約1〜4秒

カメラ撮影・画像説明:
  約5〜6秒

ただし、これは厳密なベンチマークではありません。実機会話中のサーバーログ時刻から読んだ目安です。PC負荷、LM Studio側のモデルロード状態、初回ロードかどうか、画像内容などで変わります。

12. 残課題

12.1 Memoryはまだ戻していない

現在は以下です。

Memory: nomem

今後、通常会話とカメラが安定したら、以下を戻して確認します。

Memory: mem_local_short

12.2 中国語ツール説明がまだ残っている

`function_call` を有効にすると、LM Studioログには中国語のツール説明やfew-shotが入ります。

現状は動作していますが、将来的には日本語運用向けに整理する余地があります。


12.3 画像解析モデルと速度の調整余地

現在の最新構成では、画像解析用VLLMに `gemma-3-12b-it` を使っています。カメラ画像説明は日本語で成功していますが、画像の内容やLM Studio側の状態によって応答速度は変わります。

以前試した `google/gemma-4-26b-a4b` では、画像解析時にLM Studio内部で `reasoning_content` が生成される様子も見られました。ただし、その時点でもTTSに漏れていたのは最終回答だけでした。

今後は、画像解析の精度と速度のバランスを見ながら、`gemma-3-12b-it`、Gemma 4系、Qwen系のVLMを比較する余地があります。


12.4 IP固定が必須

CoreS3側ファームウェアが `192.168.11.3` 固定なので、ローカルAIサーバーPCのIPが変わると動かなくなります。

必ず 192.168.11.3 に固定する

13. まとめ

今回の検証で、以下の流れが成立しました。

M5Stack版スタックチャン / CoreS3
  ↓
ローカル xiaozhi-esp32-server
  ↓
LM Studio 0.4.13
  ↓
通常会話LLM: openai/gpt-oss-20b
画像解析VLLM: gemma-3-12b-it
  ↓
音声会話
function_call
カメラ撮影
画像説明
日本語TTS

成功だけでなく、以下の失敗を経て現在の構成に到達しました。本文中の失敗事例①〜⑧と対応しています。

① Docker未インストール                                  (2.1)
  WSL2更新が必要                                        (2.2に付随)
② model.pt がディレクトリ扱いでコンテナRestarting       (2.4)
③ PC再起動でIPが変わりCoreS3がOTA失敗ループになる       (2.7)
④ idf.py が見つからない(ESP-IDF環境未読込)           (7.2)
  旧 agent-base-prompt によるThinking漏れ               (9.2に付随)
⑤ Gemma 4 E4BのPlan/Execution漏れ                       (9.2)
  Qwen3.5-27Bの極端な遅延                               (9.3で比較)
⑥ Intent: nointent でカメラに行かない                   (10.2)
⑦ VLLMの中国語指定で画像説明が中国語になる              (10.4)
⑧ ホスト側修正がコンテナへ反映されない                  (10.6)

最終的なブログ公開時点の構成では、通常会話用に `openai/gpt-oss-20b`、画像解析用に `gemma-3-12b-it` を使っています。通常会話はおおむね1〜4秒、カメラ撮影・画像説明はおおむね5〜6秒で発話が始まりました。

この構成は、M5Stack版スタックチャンをローカルAIサーバーにつなぎ、音声会話だけでなく、カメラ画像を使った会話まで試すための実験環境として十分に使えると感じました。

いいなと思ったら応援しよう!