Muse Gadget SDK標準の音声通話をM5Stack StopWatchで試したらHTTP 404になった

Muse Gadget SDK標準の音声通話をM5Stack StopWatchで試したらHTTP 404になった

Muse Gadget SDKの標準機能でリアルタイム音声通話を試しましたが、通話開始時にHTTP 404エラーが発生してしまいました。本記事では、M5Stack StopWatchへのセットアップから実機での現象、SDKが想定する通信の仕組みをまとめます。
2026.11.09

はじめに

これまで、MetaのMuseとM5Stack Core2をつなぎ、返答テキストをAmazon Pollyで読み上げる構成を作りました。

https://dev.classmethod.jp/articles/shuntaka-muse-gadget-sdk-m5stack-core2-japanese-voice/

今回は、外部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側の実装追加日です。各アカウントで通話を利用できるようになった日を示すものではありません。

https://github.com/facebookincubator/muse-gadget-sdk/pull/275

結果は、Muse VMへの接続までは成功したものの、通話開始のPOST /voice/realtimeがHTTP 404となり、音声で会話するところまでは進めませんでした。この記事では、設定手順と実機ログ、原因を調べて分かったことをまとめます。

今回の構成

StopWatch内部の認証情報とファームウェア、およびMuseのアカウントAPIと通話接続先

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版では、認証ヘッダーの設定とレスポンスの読み取りを次の箇所で確認できます。

https://github.com/shuntaka9576/muse-gadget-sdk/blob/463bd2a05380fea4211b0099ad0f70933c108b20/linux/src/musegadget/muse_api.py#L60-L110

通常のセットアップで利用者が用意するのは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の記事で紹介しています。

https://dev.classmethod.jp/articles/shuntaka-muse-gadget-sdk-m5stack-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へ更新済みなら、この操作は不要です。

ESP-IDFの更新
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をまだ取得していない場合

保存先が存在しない場合だけ、次のコマンドで取得します。

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の保存先は環境に合わせて変更してください。

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より先に実行します。

ツールとPython依存パッケージのインストール
"$HOME/esp/esp-idf-v6/install.sh" esp32,esp32s3

All done!と表示され、正常終了したら環境を有効にします。

ESP-IDFの有効化
. "$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で設定へ移動します。

MUSE_REALTIME_VOICEを検索した画面

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

リアルタイム音声通話を有効にし、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の操作説明にも記載されています。

https://github.com/shuntaka9576/muse-gadget-sdk/blob/463bd2a05380fea4211b0099ad0f70933c108b20/esp32/README.md#L329-L352

通話開始でHTTP 404になった

実機に書き込んで通話開始を試したところ、Muse VMへの接続は成功しましたが、POST /voice/realtimeがHTTP 404を返し、通話は成立しませんでした。以下は2026年11月9日のシリアルログの抜粋です。VM IDとcall IDは伏せています。

本体にはMUSE DIDN’T TAKE THE CALL...と表示されました。

通話開始に失敗し、MUSE DIDN’T TAKE THE CALLと表示されたStopWatch

黄ボタンを押して通話を開始したときのログです。PTT downが押下、PTT upが解放を示します。括弧内の数値は起動後の経過ミリ秒です。

VM接続情報の取得から通話開始の失敗まで(識別子はマスク済み)
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は未解決で、メンテナからの回答や正式な回避手順はありません。

https://github.com/facebookincubator/muse-gadget-sdk/issues/310

一方、別のLinux環境では通話が成立した上で入力の抑制に遭遇した報告もあります。機能全体が未提供とは言えず、今回の404がVMのバージョン、機能の提供条件、認可のどれに由来するかはサーバー側の確認が必要です。

https://github.com/facebookincubator/muse-gadget-sdk/issues/344

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内外のどこで動くのかは分かりません。

SDKのコメントから推測した音声providerとMuse本体の役割分担

通信の仕組み

通話開始、音声の送受信、終了までのHTTP-over-Noise上のシーケンス

外側は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から音声を取り出します。これらはプロトコルヘッダ冒頭で確認できます。

https://github.com/shuntaka9576/muse-gadget-sdk/blob/463bd2a05380fea4211b0099ad0f70933c108b20/esp32/components/muse/muse_voice_call_core.h#L19-L46

マイクと返答再生の制御

SDKには、再生音をマイクが拾うのを防ぐ制御や、ボタンを押し直したときに古い返答音声を止める制御もあります。図の上段はハンズフリー、下段は今回のStopWatchで使うPush-to-talkの動作です。今回は通話開始で404になったため、これらはコードから整理した内容です。

ハンズフリーのマイク制御とPush-to-talkでの返答音声の切り替え

さいごに

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内で確認できる事実を増やしたいところです。

https://dev.classmethod.jp/articles/shuntaka-agent-cloud-vm-survey/

また、Muse標準の通話機能とは別に、OpenAI Realtime APIを使ってStopWatchで音声会話する構成も試してみたいと思います。

この記事をシェアする

関連記事