
Muse Gadget SDK標準の音声通話をM5Stack StopWatchで試したらHTTP 404になった
はじめに
これまで、MetaのMuseとM5Stack Core2をつなぎ、返答テキストをAmazon Pollyで読み上げる構成を作りました。
今回は、外部TTSを追加せずにMuseと会話できるのか気になり、Muse Gadget SDK標準のリアルタイム音声通話をM5Stack StopWatchで試しました。
この機能は、2026年10月26日(日本時間10月27日)にPR #275「Add realtime voice for Linux and StopWatch」でSDKへ追加されました。LinuxとESP32向けのexperimental機能で、これはSDK側の実装追加日です。各アカウントで通話を利用できるようになった日を示すものではありません。
結果は、Muse VMへの接続までは成功したものの、通話開始のPOST /voice/realtimeがHTTP 404となり、音声で会話するところまでは進めませんでした。この記事では、設定手順と実機ログ、原因を調べて分かったことをまとめます。
今回の構成

StopWatchでは、Muse Gadget SDKからビルドしたファームウェアが動きます。通話の入力・送信・受信・再生を端末が行い、Wi-FiでMuseへ接続します。スマートフォンは初回のペアリングとWi-Fi設定に使います。
公開されているMuse Gadget SDKを読むと、接続先のMuse VMと接続用トークンを取得するAPIがあることが分かります。SDKはペアリングで受け取ったデバイスの認証情報を使ってGET https://api.muse.ai/fetch_vmsを呼び、VMの接続先・vm_id・vm_auth_tokenを取得して接続します。
ここで紹介するAPIの動作は、独立したAPI仕様書ではなく、公開SDKの実装から確認したものです。Linux版では、認証ヘッダーの設定とレスポンスの読み取りを次の箇所で確認できます。
通常のセットアップで利用者が用意するのはMuseのSDKトークンで、VM接続用トークンを手動で取得する必要はありません。
| 項目 | 使用する構成 |
|---|---|
| 本体 | M5Stack StopWatch |
| SDK | Muse Gadget SDKのESP32版 |
| リビジョン | 463bd2a |
| 開発環境 | ESP-IDF v6.0.3 |
| 使用機能 | CONFIG_MUSE_REALTIME_VOICE(experimental) |
SDKの取得やSDKトークンの発行、Museアプリとのペアリングの流れは、以前のCore2の記事で紹介しています。
リンク先はCore2向けです。今回はStopWatchを使うため、ビルド先・ボード設定・ボタン操作は以下の手順に合わせます。Core2用のファームウェアや設定ファイルは流用しません。
外部TTSを使う構成との違い
前の構成では、Museから届いた返答テキストをTTS APIへ送り、生成された音声を端末で再生していました。SDK標準の通話機能は、Museとの通話ストリームから返答音声を受け取る設計です。以下はSDKの実装上の違いです。
| 項目 | これまでの外部TTS構成 | SDK標準のリアルタイム音声通話 |
|---|---|---|
| 返答の流れ | Museの返答テキスト → TTS → 再生 | Museから音声を受信 → 再生 |
| 自分で用意する音声合成 | Polly/Gemini TTSの呼び出し処理 | なし |
| TTS用のAPI・認証設定 | 外部TTSの接続設定が必要 | 音声モデル用のAPIキー設定は不要 |
| 音声・モデルの選択 | TTS側の設定で指定 | 今回のSDKにはモデルを選ぶ設定がない |
| 次の発話 | 音声メモを新たに送る | 同じ通話の中で黄ボタンを押して送る |
開発環境の準備
公開済みのCore2記事から進める場合、次の変更が必要です。
| 項目 | Core2の記事 | 今回のStopWatch |
|---|---|---|
| ESP-IDF | v6.0.1 | v6.0.3へ更新 |
| ビルドターゲット | esp32 |
esp32s3のツールを追加 |
| ボード設定 | Core2用 | StopWatch専用の設定を作成 |
| SDKトークン・日本語フォント | Core2用の設定に保存 | StopWatch用の設定にも登録 |
| 音声出力 | 外部TTSを呼ぶ実装 | SDK標準のMUSE_REALTIME_VOICEを有効化 |
ESP-IDFをv6.0.3へ更新する
今回のSDKの既定の証明書検証設定はESP-IDF v6.0.3以降を必要とします。取得済みの~/esp/esp-idf-v6を更新します。
まず、ESP-IDFの作業ツリーに独自の変更がないことを確認します。
git -C "$HOME/esp/esp-idf-v6" status --short
変更がなければ、v6.0.3に切り替えてサブモジュールも更新します。すでにv6.0.3へ更新済みなら、この操作は不要です。
git -C "$HOME/esp/esp-idf-v6" fetch --depth 1 origin tag v6.0.3
git -C "$HOME/esp/esp-idf-v6" checkout --detach v6.0.3
git -C "$HOME/esp/esp-idf-v6" submodule update --init --recursive --depth 1
ESP-IDFをまだ取得していない場合
保存先が存在しない場合だけ、次のコマンドで取得します。
mkdir -p "$HOME/esp"
git clone --branch v6.0.3 --depth 1 --recursive --shallow-submodules \
https://github.com/espressif/esp-idf.git \
"$HOME/esp/esp-idf-v6"
Pythonを選び、更新後のツールをインストールする
今回の手元環境では、ESP-IDFのPython仮想環境をmiseのPython 3.14.7で作成しています。同じPythonをPATHの先頭に置きます。Pythonの保存先は環境に合わせて変更してください。
export PATH="$HOME/.local/share/mise/installs/python/3.14.7/bin:$(brew --prefix)/bin:$PATH"
python3 --version
続いて、更新後のESP-IDFが要求するツールとPython依存パッケージをインストールします。Core2用のesp32に加え、StopWatch用のesp32s3も指定します。GitでESP-IDFを更新しただけでは、必要なツールは更新されません。export.shより先に実行します。
"$HOME/esp/esp-idf-v6/install.sh" esp32,esp32s3
All done!と表示され、正常終了したら環境を有効にします。
. "$HOME/esp/esp-idf-v6/export.sh"
idf.py --version
ESP-IDF v6.0.3と表示されることを確認します。
更新後のexport.shでツール不足やPython環境のエラーが出た場合
手元では、v6.0.3への更新直後にexport.shを実行すると、xtensa-esp-elf-gdb、openocd-esp32、esp-rom-elfsのhas no installed versionsで停止しました。PATHには旧バージョンのツールが残っていましたが、更新後のESP-IDFが要求するバージョンが不足していました。その後のidf.pyではNo module named 'rich_click'も出ています。
この場合は、上のinstall.sh esp32,esp32s3を正常終了させてから、export.shを読み込み直します。
また、HomebrewのPython 3.15.1を選んだ際には、idf6.0_py3.15_env/bin/python not foundが出ました。手元にあった仮想環境はPython 3.14.7用のidf6.0_py3.14_envだったためです。上の手順では既存環境と同じPythonを選んでいます。
新しいターミナルを開いたときは、次の有効化だけを行います。インストールは毎回実行する必要はありません。
export PATH="$HOME/.local/share/mise/installs/python/3.14.7/bin:$(brew --prefix)/bin:$PATH"
. "$HOME/esp/esp-idf-v6/export.sh"
ファームウェアの設定と書き込み
ESP-IDFを有効にできたら、StopWatch用の設定を開きます。
cd "$HOME/repos/github.com/shuntaka9576/muse-gadget-sdk/esp32"
idf.py -B build-muse-m5stack-stopwatch \
-DIDF_TARGET=esp32s3 \
-DSDKCONFIG=build-muse-m5stack-stopwatch/sdkconfig \
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;devices/sdkconfig.muse;devices/sdkconfig.muse-m5stack-stopwatch" \
menuconfig
今回は前にStopWatchを設定したPCとは別のPCを使います。以前のPCのsdkconfigは自動では引き継がれないため、SDKトークンだけでなく、フォントや音声モードも設定します。
設定する項目は次のとおりです。CONFIG_を除いた識別子を/で検索すると、その設定へ移動できます。
| 項目 | 設定識別子 | 今回の値 | 設定内容 |
|---|---|---|---|
| SDKトークン | GADGET_SDK_TOKEN |
自分のmgst_…トークン |
ESP32 Device SDK → Muse Gadgets SDK tokenに入力 |
| 日本語字幕 | MUSE_CJK_FONT |
有効(y) |
Show Chinese, Japanese and Korean in captionsを[*]にする。前のStopWatch設定でも有効にした項目 |
| Notoへのフォント切り替え | MUSE_CJK_FONT_NOTO |
無効(n) |
Draw CJK captions with Noto Sans SC instead of Unifontは[ ]のまま。前と同じUnifontを使う |
| リアルタイム音声通話 | MUSE_REALTIME_VOICE |
有効(y) |
Live voice calls with Muse (experimental)を[*]にする。前のボイスノート構成から変更する項目 |
| Wi-Fi SSIDの上書き | HOMEHUB_WIFI_SSID |
空欄 | WiFi SSID override (dev iteration)。Wi-Fi設定はMuseアプリから渡す |
| Wi-Fiパスワードの上書き | HOMEHUB_WIFI_PASSWORD |
空欄 | WiFi password overrideも空欄のまま |
ボードはMUSE_BOARD_M5STACK_STOPWATCH=y、ターゲットはIDF_TARGET="esp32s3"です。上のコマンドでStopWatch用の設定を読み込むため、PSRAMなどのハードウェア設定もボード設定から反映されます。フォントと通話の項目はComponent config → Museにあります。
Museアプリ側のSettings → Devices → Developer modeも有効にしておきます。すでに有効なら変更は不要です。
/で検索画面を開き、MUSE_REALTIME_VOICEを入力します。一番上の項目を選び、Enterで設定へ移動します。

移動先はComponent config → Museです。Live voice calls with Muse (experimental)をSpaceで[*]にします。通話開始時の挨拶やタイマーは既定値のままにします。sで保存し、保存先がbuild-muse-m5stack-stopwatch/sdkconfigになっていることを確認します。

この画像では日本語字幕用のフォントはまだ無効です。日本語字幕を表示する場合は、同じ画面のShow Chinese, Japanese and Korean in captions(MUSE_CJK_FONT)も[*]にして、もう一度保存します。
qで設定画面を閉じたら、ビルドしてStopWatchへ書き込みます。
tools/muse/board.sh build stopwatch
tools/muse/board.sh flash stopwatch
複数のボードを接続している場合は、flash stopwatchの後に対象のシリアルポートを指定します。
初回起動後はMuseアプリからStopWatchをペアリングし、Wi-Fiを設定します。今回の通話機能を有効にした構成では、本体でペアリングの承認を求められたら黄ボタンで確認します。
この設定を有効にすると、StopWatchの黄・青ボタンの役割がボイスノート構成から入れ替わります。USB端子を下にして、左上の黄ボタンが音声入力、右上の青ボタンが通話終了です。SDKの操作説明にも記載されています。
通話開始でHTTP 404になった
実機に書き込んで通話開始を試したところ、Muse VMへの接続は成功しましたが、POST /voice/realtimeがHTTP 404を返し、通話は成立しませんでした。以下は2026年11月9日のシリアルログの抜粋です。VM IDとcall IDは伏せています。
本体にはMUSE DIDN’T TAKE THE CALL...と表示されました。

黄ボタンを押して通話を開始したときのログです。PTT downが押下、PTT upが解放を示します。括弧内の数値は起動後の経過ミリ秒です。
I (4754) link.app: found device credentials in NVS, fetching leased VM
I (5422) link.vm_api: HTTP 200, 1519 bytes (attempt 1)
I (5424) link.vm_api: 1 VMs found
I (5591) muse_input: PTT down
I (6265) link.noise_ctrl: TLS connected to hatch.metaaivm.com:443 (vm_id=<vm_id>)
I (6463) link.noise_ctrl: WebSocket upgraded to /v1/noise
I (6617) link.vm_api: HTTP 200, 1519 bytes (attempt 1)
I (6618) link.vm_api: 1 VMs found
I (7004) muse_input: PTT up
I (7151) link.noise_ctrl: handshake msg3 sent (64 bytes), session established
I (8350) muse_chat_session: connected to VM <vm_id> in 1719 ms
I (8358) muse_chat_session: stream 2: POST /voice/realtime?call_id=<call_id>
W (8928) muse_chat_session: stream 2: HTTP 404
I (8928) muse_call: realtime stream failed (HTTP 404)
I (8937) muse_chat_session: stream 3: POST /voice/end-call
I (9081) muse_call: end-call confirmed
fetch_vmsはHTTP 200を返し、SDKは取得したvm_auth_tokenでVMに接続できています。トークンの値自体はログには出ていません。link.noise_ctrlがLinkの接続、muse_chat_sessionが通話にも使う別の接続のログです。
失敗したのは接続後のPOST /voice/realtimeで、HTTP 404が返っています。その後のPOST /voice/end-callは成功しましたが、返答音声は再生されませんでした。
MUSE_REALTIME_VOICE=y、App IDは既定のhatch-webになっていることを確認しました。通話開始処理、プロトコル本体、セッション処理の3ファイルも、調査時点の公式mainと一致しています。
同じ段階で404になる現象は、公式Linuxクライアントでも報告されています。2026年11月9日の確認時点でIssueは未解決で、メンテナからの回答や正式な回避手順はありません。
一方、別のLinux環境では通話が成立した上で入力の抑制に遭遇した報告もあります。機能全体が未提供とは言えず、今回の404がVMのバージョン、機能の提供条件、認可のどれに由来するかはサーバー側の確認が必要です。
MuseにVM側の状態を調べてもらった
VMが古い可能性も考え、Muse自身に読み取り専用で調査を依頼しました。返ってきた報告の要点は次のとおりです。ここは手元のシリアルログではなく、MuseによるVM内の調査報告に基づきます。
| 調査対象 | Museからの報告 |
|---|---|
| アカウントの音声機能 | ~/docs/voice.mdに、このアカウントではアプリのライブ音声を利用できないと記載されていた |
| デーモンの実装 | バイナリ内に/voice/realtimeと/voice/end-call、対応するルート表の形跡があった |
| ビルド・起動日時 | デーモンは11月8日のビルドで、VMは通話試行前の11月9日7:13 JSTに起動していた |
| サーバー側の判定 | 機能ゲートの実際の値と、404を返した箇所のログにはアクセスできなかった |
この報告からは、古いVMに通話の実装がないという説明より、アカウント側の利用条件が関係している可能性が考えられます。ただし、アプリの音声機能とガジェットの利用条件が同じかは分からず、今回の404の原因とまでは断定できません。日本のアカウントだから使えない、という点も未確認です。
VMの更新や再起動、データのリセットは実施していません。まず確認したいのは、ペアリング済みガジェットでRealtime Voiceを使うために、アカウント側で追加の有効化が必要かどうかです。
音声モデルと通信の仕組み
実機では通話開始で止まりましたが、SDKが想定する通信の仕組みもコードから確認しました。以下の図は実装から整理した通常の通話フローで、今回成功した通信の記録ではありません。
音声モデルやTTSは何を使っているか
使われている音声モデルやTTSは、SDKからは特定できません。 モデル名や提供元の記載がないためです。
端末はMuseへ音声を送り、返答音声と書き起こしを受け取る実装で、外部TTS APIを呼び出す処理はありません。
SDKのコメントには、音声を扱うproviderと、Muse本体へ作業を委譲する仕組みが登場します。ただし、providerが単一の音声モデルなのか、音声認識・LLM・TTSを組み合わせたものなのか、VM内外のどこで動くのかは分かりません。

通信の仕組み

外側はTLS上のWebSocketです。その中でNoise XXのハンドシェイクを行い、暗号化された通信路にHTTP-over-Noiseのリクエストを載せます。
通話ではPOST /voice/realtime?call_id=…のボディを閉じず、NDJSONで音声とイベントを双方向に流します。通常のHTTPSでそのURLに音声ファイルをアップロードする方式とは異なります。通話終了は、同じ接続の別streamからPOST /voice/end-callを送ります。
上りは24 kHz・モノラル・PCM16LEの音声を40 msずつ送り、下りはcontinuous_audio_video.deltaから音声を取り出します。これらはプロトコルヘッダ冒頭で確認できます。
マイクと返答再生の制御
SDKには、再生音をマイクが拾うのを防ぐ制御や、ボタンを押し直したときに古い返答音声を止める制御もあります。図の上段はハンズフリー、下段は今回のStopWatchで使うPush-to-talkの動作です。今回は通話開始で404になったため、これらはコードから整理した内容です。

さいごに
StopWatchへのビルド・書き込みと、VM接続用トークンの取得、Muse VMへの接続までは確認できました。しかし、通話開始はHTTP 404となり、SDK標準の音声で会話することはできませんでした。
今回SDKを追って分かったのは、ガジェットがアカウントAPIから接続先とトークンを取得し、Muse VMへリクエストを送る仕組みです。この通信経路は音声通話専用ではなく、以前のCore2+外部TTS構成でも使っていました。問い合わせはMuse VMの/chat/streamへ送り、返ってきたテキストを外部TTSで読み上げていました。今回は同じVMへの接続上で/voice/realtimeを使い、返答音声もMuseから受け取ろうとした、という違いです。
同じ404の報告は他の環境にもあり、手元の設定や公式コードとの差分からは解決策を見つけられませんでした。アカウント側の音声機能の提供条件が気になるところですが、ガジェット向けの条件はまだ確認できていません。今回はここまでの結果を記録しておきます。
以前、MuseのVM構成を調べたときは、VM内にSSHの接続口を用意し、手元のMacから直接コマンドを実行しました。今回も同じ方法で、Museが報告した音声機能の案内やhatchの実行状態を確認し、StopWatchからの通話リクエストがVM内まで届いているか、ログを追ってみたいと思います。VM外で判定される利用条件までは見えない可能性がありますが、まずはVM内で確認できる事実を増やしたいところです。
また、Muse標準の通話機能とは別に、OpenAI Realtime APIを使ってStopWatchで音声会話する構成も試してみたいと思います。








