1. はじめに
1.1 きっかけ
生成 AI に続く波として、「Physical AI(フィジカルAI)」が注目されています。AI が画面の中にとどまらず、ロボットや機械を通じて現実の世界で動く、という流れです。2026 年 1 月の CES でも話題になり、日本でも自動車やロボットの分野と相性がよいとされています1。
ただ、ロボットが身近になるほど、気になることがあります。ロボットはカメラとマイクと通信機能を持った機械。そういう機械を家の中に置くこととなり、パーソナルなデータが集められていきます。
テクノロジーの発展の一方で個人情報の漏えいが相次いでいます。東京商工リサーチの集計では、2026 年は 10 月 5 日までに、上場企業とその子会社の 100 万件以上の漏えい・紛失事故がすでに 10 件に達し、2025 年通年の 6 件を上回って過去最悪のペースとされています2。
ロボットでも事故は起きています。2024 年には、ECOVACS の掃除ロボットが、建物の外から Bluetooth で乗っ取れることが、ABC News の実証で報じられました。このとき、カメラが使われていることを示すインジケーターは作動しなかったとされています3。2026 年 3 月には、DJI の掃除ロボット「Romo」で、本来必要な PIN による保護が機能せず、数千台分のカメラ映像が見られる状態だったと報じられました4。
ロボットが扱うのは、声と、カメラの画像です。どちらも、子どもの暮らしがそのまま映るデータです。共働きの掃除の行き届いた部屋の様子も機密情報です。だとすれば、子どもが毎日話しかけるロボットは、声もカメラの画像も、家の外に出さない構成にしたい。そんな想いに対してマシンスペックの制約に苦悶して対応した記録です。
1.2 Reachy Mini とは
Reachy Mini は、Hugging Face と Pollen Robotics が出している、オープンソースの小型ロボットです5。公式は、ハッカーや AI 開発者向けの、表情豊かなロボットと紹介しています。机に置いて使う大きさで、足も車輪もなく、首を動かす頭と、2 本のアンテナで感情を表します。カメラ、マイク、スピーカーを備えています。キット形式で、組み立ては公表では 2〜3 時間ですが、我が家では数日にまたいで作りました。
種類は 2 つあります。私が使っているのは Wireless 版で、Raspberry Pi の CM4 とバッテリーと Wi-Fi を内蔵し、単体で動きます。ただし、技適(技術基準適合証明)の扱いが気になるので、内臓のWi-Fi は使わない選択をしています。もうひとつの Lite 版は、USB でパソコンにつないで使います。アプリは Hugging Face Spaces を通じて配布され、LLM と自然に話す公式の Conversation アプリもあります。この記事では、公式のアプリを使わず、自作のアプリを置きました。声とカメラの画像の行き先を制御したかったからです。アプリを動かす手元の GPU 付きのパソコンを、ロボットに対する頭脳側として、以降「母艦」と呼びます。
なお、今回の検証は頭脳をすべて母艦に置いたので、振り返ると、Lite 版でも十分だったのではないか、とも思っています(Lite 版は試していないので、あくまで想像です)。私が家の中で大きな声で言えない事実かもしれません。
1.3 これまでの検証と、今回の位置づけ
これまでの私のローカル LLM の検証は、実はこのためだった、と言っても過言ではありません。決して組み立てて最初に遊んだ後に Reachy を数か月放置していたわけではありません。
以前の記事では、Nemotron 3.5 Lightning を VRAM 16GB の手元の母艦で動かしました6。続いて、推奨 24GB の Bolt(総 74B)を 2bit で 16GB に載せ、日本語の品質などを比べました7。どちらも、手元の 1 台で、外に頼らずに動かせるかを確かめる検証でした。
今回は、その流れで出た軽量版の Bolt E4B8 を、手持ちの制約のあるPCで稼働させてロボット Reachy Mini に繋ぎ、子どもたちに遊ばせました。
最初は NVIDIA の Super-VL(121B)に、ロボットの目と耳まで任せる案を考えていました。しかし、私が手持ちの制約のある16GB の GPU には載りません。この記事では、載せられるモデル(E4B)を前提に、組み立てた結果を書きます。
1.4 この記事の範囲
技術の比較や検証が目的ではなく、家庭で子どもと遊ぶ記録です。手元の環境(Windows 11、RTX 5070 Ti 16GB、Reachy Mini、デーモン v1.9.0、SDK 1.13.0)での結果で、製品としての安全性を保証する記事ではありません。
会話の例では、ロボットの呼び名を「リーチー」(Reachy の読み)に統一しています。子どもの発言は、短い抜粋に限って載せています。
2. 構成:どの機能をどこに置くか
2.1 感覚器と体はロボット、頭脳は母艦
Reachy Mini に載っているコンピューターは Raspberry Pi の CM4 で、RAM は 4GB です9。この構成で使う E4B(Q4_K_M で約 6.18GB)はもちろん載りません。そこで、ロボットには「感覚器と体」と、それを動かして通信の窓口になる最低限の処理だけを残し、頭脳は母艦に置きました。
全体の構成は次のとおりです。
ロボットと母艦は、有線 LAN でつながっています。実線はアプリが使う通信、点線は SDK の接続が運ぶ映像の流れです。
母艦の中の流れは、「音声認識で音声をテキストにする → LLM が、そのテキストの意味を理解して、返事のテキストを作る → 音声合成でテキストを声にする」です。LLM は、音声そのものは聞いていません。受け取るのも返すのもテキストだけで、全体の進行は play.py が受け持ちます。しりとりやクイズのルールの判定、動きの指示の聞き分け、安全フィルタは、LLM ではなく play.py のコードで行います(4 章)。LLM が受け持つのは、おしゃべりの返事です。
ロボットの CM4 は、何もしていないわけではありません。本体で動くデーモン(v1.9.0)が、次の役割を受け持っています。LLM のような重い処理は載せず、時間の正確さが要る処理と、通信の窓口に絞った形です。ロボットの中は、こうなっています。
- モーターの制御ループ。 状態の応答では、平均で約 49Hz で回っていました。姿勢の指示を関節の動きに直す計算(運動学)も、ここです。
- REST API(8000)の受け付け。 母艦からの動きの指示や、追従の入り切りを受けます。
- メディアの窓口(8443)。 マイクとカメラの取り込み、母艦への WebRTC 配信、母艦から届いた音声の再生を担います。通信の制御と言える部分です。
- 顔の検出と追従。 検出の結果を、制御ループに渡して首を動かします。
- その他。 Wi-Fi の管理、更新、アプリの管理、Hugging Face とのつなぎなどです(この構成では使っていません)。
ただし、CM4 の中で、それぞれがどれだけ CPU を使っているかは、測っていません。
置き場所の考え方は次のとおりです。
| 機能 | 置き場所 | 理由 |
|---|---|---|
| マイク、スピーカー、カメラ、モーター | ロボット | 物理的にそこにある |
| モーターの制御、通信の窓口(REST、WebRTC) | ロボット(デーモン) | 時間の正確さが要る。母艦との窓口も本体で受ける |
| 顔の検出と追従 | ロボット(デーモン) | 検出は本体の中で済み、アプリは顔の位置だけを使える |
| 音声認識 | 母艦(GPU) | CM4 では重い |
| LLM | 母艦(GPU、llama.cpp) | 同上。全層を GPU に載せ、母艦の中(ループバック)だけで待つ |
| 音声合成 | 母艦(VOICEVOX、CPU) | GPU は LLM と音声認識に使う |
| ゲームの判定、安全フィルタ、音声の区切り(音量ベースの簡易 VAD) | 母艦(CPU、ふつうのコード) | LLM は判定を間違えた(4 章) |
2.2 守りたいデータは 2 つ:会話とカメラの画像
このロボットが触れるセンシティブなデータは、会話、特に声とカメラの画像です。それぞれの流れを整理しました。
| データ | 通る経路 | 残るもの |
|---|---|---|
| 会話 | ロボットのマイク → 有線 LAN → 母艦。母艦の中で文字に変換 | 文字のログ。子どもの録音は保存しない(既定) |
| カメラの画像 | ロボットの中で、顔の位置(座標)に変換する。ただし、SDK の接続は、映像の流れも母艦まで運ぶ(3.5) | 何も残さない。アプリは映像を読まず、保存しない |
| LLM に渡るもの | テキストだけ(E4B は画像も音声も入らない) | — |
補足です。SDK の WebRTC 接続は、カメラと音声を一緒に受け取る作りです(SDK のソースでは、映像用の受け口が音声と並んで作られます)。アプリが映像を読まなくても、映像の流れは母艦まで届いていると見られます(3.5)。SDK の中では、最新の 1 コマだけがメモリ上に展開されます。アプリは、その映像を使わず、保存もしません。この点は、最初に私が思い違いをしていた箇所で、通信を観察して気づきました。だからこそ、この経路を 3 章のネットワークの分離で囲っていることが効いてきました。
2.3 LLM は E4B をローカルで
LLM は llama.cpp10(b11459、CUDA 13.4)の llama-server で動かしました。
llama-server -m Nemotron-3.5-Bolt-E4B-Q4_K_M.gguf --alias nemotron-bolt -c 8192 -ngl 999 --jinja
待ち受けの宛先は、llama-server の既定で、母艦自身(ループバック)だけです。外のネットワークからは、つながりません。ポートの既定は 8080 で、公式の Desktop アプリ(Reachy Mini Control)が母艦の 8000 番を使うため、8000 は避けて、この既定のままにしています(実際の起動スクリプトには、念のため宛先とポートを明示しています)。
E4B はテキストだけを扱うモデルで、画像や音声は入りません(/props で vision/audio が false、画像を送ると 500 でした)。思考はオフにして、単体で短い返答を測ると、0.1〜0.3 秒で返りました。
{"model": "nemotron-bolt", "messages": [...], "chat_template_kwargs": {"enable_thinking": false}}
返答は say(話す文)、head(頭の向き)、antenna(アンテナの動き)の JSON で返させ、動作の指定は固定の語彙に限っています。
3. インターネットに出ないための考慮
3.1 Hugging Face からログアウトする
Reachy Mini のデーモンには、Hugging Face のアカウントでつなぐ「リモートアクセス」(中継)があります。最初に確認したとき、ロボットは私のアカウントでログインしており、リモートアクセスが有効でした。子どもの声とカメラの映像が通る機器なので、これは外しました(デスクトップアプリ側でサインアウト)。
サインアウト後は、次のように確認できます。
$ curl -s http://192.168.1.xx:8000/api/hf-auth/status
{"is_logged_in":false,"username":null}
3.2 SDK の既定で、Google の STUN サーバーに問い合わせていた
母艦から音声を受け取るために、SDK(reachy-mini 1.13.0)の WebRTC を使いました。LAN の中だけで通信するつもりでしたが、接続に約 250 秒かかりました。調べると、WebRTC の webrtcsrc の既定の設定に、STUN サーバー(stun.l.google.com)が入っていました。LAN の中なら不要なのに、外部に問い合わせを試みていたことになります。
GStreamer の要素を作る関数を包み、STUN を無効にしました。
import gi; gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None); _orig = Gst.ElementFactory.make
def _make(name, *a, **k):
e = _orig(name, *a, **k)
if e is not None and name == "webrtcsrc":
e.set_property("stun-server", None) # 外部STUNを使わない(LAN限定)
return e
Gst.ElementFactory.make = _make
from reachy_mini import ReachyMini
mini = ReachyMini(host="192.168.1.xx", connection_mode="network", spawn_daemon=False)
これで接続は約 0.5 秒になりました。SDK とデーモンのバージョン違い(1.13.0 と 1.9.0)で警告は出ますが、今回の用途では動きました。
3.3 Desktop アプリ(Control)を落とすと、SDK が接続先を見失う
通信を観察するために、公式の Desktop アプリ「Reachy Mini Control」を落としたところ、それまで動いていた自作のアプリが、突然つながらなくなりました。
ConnectionRefusedError: [WinError 10061] 対象のコンピューターによって拒否されたため、接続できませんでした。
接続先は、ロボット(192.168.1.xx)のはずでした。原因を追うと、SDK は映像と音声の接続先(シグナリングサーバー)を、デーモンが答える Wi-Fi 側の IP アドレス(wlan_ip)から決めていました。
# reachy_mini の内部(抜粋)
signalling_host=daemon_status.wlan_ip or "localhost"
この構成では、ロボットの Wi-Fi を使っていないので、wlan_ip は空です(実機のデーモン v1.9.0 の応答で、null でした)。すると SDK は、接続先を localhost(母艦自身)にしてしまいます。つまり、本来つながるはずのない宛先を見ていました。
それでも動いていたのは、Control アプリが母艦の中に、ローカルの中継(localhost:8443)を立てていたからだと考えられます。自作のアプリは、知らないうちに、Control アプリ経由でつながっていたと見られます。Control を落として、はじめて気づきました。
対策は、接続先が localhost なら、有線のロボットの IP に差し替えることです。
from reachy_mini.media import media_manager as _mm
_orig = _mm.MediaManager.__init__
def _fixed(self, *a, **k):
if k.get("signalling_host") in (None, "", "localhost"):
k["signalling_host"] = "192.168.1.xx"
return _orig(self, *a, **k)
_mm.MediaManager.__init__ = _fixed
これで、Control アプリを落としたままでも、SDK だけで接続できました。Wi-Fi を使わず、この組み合わせ(SDK 1.13.0、デーモン 1.9.0)で自作のアプリを動かすときの落とし穴です。
3.4 ネットワークを分ける
母艦とロボットは、ほかの機器とは別の、専用のネットワークでつないでいます。個別のスイッチングハブを経由して、母艦とロボットだけの小さな LAN を組み、IP アドレスも両方に手動で割り当てました(DHCP は使っていません)。家庭のネットワークや、インターネットにつながるルーターとは、ネットワークとして分かれています。ロボット本体の Wi-Fi は使わず、この有線 LAN だけでつないでいます。声やカメラの映像が通る経路を、この小さな LAN の中に閉じ込める狙いです。
万が一、ロボットの中にバックドアのような想定外の通信があっても外へ出られないように、出口そのものをなくしました。ロボットに入って、経路と DNS の設定を確かめた結果です。
$ ip route
192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.xx metric 100
$ cat /etc/resolv.conf
# Generated by NetworkManager
$ ip -br addr
eth1 UP 192.168.1.xx/24 fe80::…/64
wlan0 DOWN
$ nmcli -f ipv4.method,ipv4.addresses,ipv4.gateway,ipv4.dns connection show "wired-static"
ipv4.method: manual
ipv4.addresses: 192.168.1.xx/24
ipv4.gateway: --
ipv4.dns: --
- デフォルトゲートウェイがありません。 経路表にあるのは、同じ LAN(192.168.1.0/24)だけです。
- DNS のサーバーが設定されていません。
-
IP アドレスは固定です。 有線の接続は
wired-staticという名前の手動設定(manual)で、保存された設定にも、ゲートウェイと DNS はありません。再起動しても、この状態に戻ります。 - 無線(
wlan0)は動いておらず、IPv4 アドレスもありません。 - 母艦側にも、中継の設定はありません。 IP 転送は無効で、インターネット接続の共有(ICS)も、ロボットにつながる有線アダプターでオフでした。
つまり、ロボットは、設定の上でも、同じ LAN の外へ出られません。実際にどうだったかは、3.5 で観察しました。
3.5 通信を観察した
母艦の有線アダプターで、ロボットの MAC アドレスの通信を、pktmon11 で記録しました。MAC アドレスで絞ったので、IPv4 だけでなく、IPv6 やマルチキャストも入ります。段階ごとの結果です。この観察は、Reachy Mini Control を落とした状態で行いました。
| 段階 | ロボットが送った量(IPv4 と IPv6 の合計) | 宛先 |
|---|---|---|
| アイドル(何も接続しない、合計 90 秒) | 0 | なし |
| SDK につないで、音声だけ取り出す(60 秒。映像は読まないが、受け口は開いたまま) | 約 10MB(中央値 0.12MB/秒) | 母艦のみ |
| アプリで会話(約 2 分) | 約 160MB(中央値 1.6MB/秒、最大 3.5MB/秒) | 母艦のみ |
分かったことです。
- ロボットは、自分からは何も送りませんでした。 何も接続していないあいだは、どの種類のパケットも 0 でした。
- 外のアドレスへの通信は、ありませんでした。 宛先は、母艦(IPv4 と、IPv6 のリンクローカルアドレス)と、同じ LAN の中のマルチキャスト(SSDP と IGMP、合わせて十数パケット。SDK がつながっているあいだだけ)でした。DNS、NTP、HTTPS のような通信は、見当たりませんでした。
-
メディアの通信は、IPv6 のリンクローカルアドレス(
fe80::)で流れていました。 これはルーターを越えられないアドレスです。 - 量が、音声だけにしては大きい値でした。 SDK につないで音声だけ取り出した段階でも、毎秒 0.12MB(約 1Mbps)ありました。会話中は、毎秒 1.6MB(約 13Mbps)まで増えました。どちらも中央値です。2.2 で書いた SDK の作りと合わせると、映像の流れが含まれている可能性が高いです。ただし、パケットの中身(RTP)を解読して、映像だと確かめたわけではありません。会話中に増える理由(顔の追従でカメラが動くためか)も、未確認です。
- 数値は、当時の集計です。 生のパケット記録は、音声を含むため、解析のあとで削除しました。同じ集計は、もう再現できません。
3.4 のとおり、ロボットにはゲートウェイがなく、外へ向けた通信は、そもそもパケットとして出ません。ですので、この観察で見えるのは、同じ LAN の中に流れた通信だけです。外への通信が見えなかったことは、この構成と矛盾しません。ただし、「外へ出ようとした」ことを捉える観察ではない点に、注意してください。
3.6 確認できていないこと
- 観察したのは、数分間です。アイドルは合計 90 秒だけで、数時間に一度の定期的な通信(更新の確認など)は、見ていません。
- 母艦側のプロセス(音声認識、LLM、VOICEVOX)が外へ通信していないことは、パケットを取って確認したわけではありません。モデルや VOICEVOX は、あらかじめダウンロードして置いてあります。音声認識のモデルは、起動時に Hugging Face へ更新を確認しに行く可能性があったため、アプリに
HF_HUB_OFFLINE=1を設定し、オフラインでもモデルを読み込めることを確かめました。 - 母艦自身は、ロボットとの有線 LAN のほかに、家庭のネットワークにもつながっています。中継の設定はないので、ロボットから家庭のネットワークへの橋にはなっていませんが、母艦が乗っ取られた場合は、橋になりえます。
- 子どもが遊んだ回は、Control アプリを起動した状態だった可能性があります。その場合の通信の経路は、3.5 の観察(Control を落とした状態)と違います。
- ロボット本体のファームウェアが、外へ通信するかどうかは見ていません。映像の流れが母艦に届いている点は 3.5 のとおりですが、その中身を解読して確かめてはいません。
「外に出ない」と断言はできません。言えるのは、外へ出られない構成にして、出そうな箇所を潰し、数分間の観察では、外への通信が見えなかったところまでです。
4. 主要機能と考慮点
4.1 しりとり:チューニングが難しかった
しりとりのルールは簡単です。そこで最初は、E4B にルールを書き、判定させました。ところが判定の間違いが目立ちました。実効 4B 級のモデルは、「最後の文字を拾う」ような処理を安定してできませんでした。そこで、判定はコードで行い、LLM は使わないことにしました(games.py)。
最後の文字を取り出すために、pykakasi12 で読みに直します。
def last_char(w):
w = w.rstrip("ー") # 「ぎたー」→「た」
return SMALL.get(w[-1], w[-1]) if w else "" # 「ゃ」→「や」
ここまででルールは正しくなりました。しかし、子どもに遊ばせると、しりとりをたくさん間違えていた、という報告でした。
難しさの正体は音声認識だった
原因を調べるため、しりとりに使う単語 60 個を合成音声で 1 語ずつ読ませ、音声認識(kotoba-whisper-v2.013、faster-whisper14)の結果を、読みが一致するかで数えました。
| 条件 | 読みが一致した数 |
|---|---|
| 声:春日部つむぎ、そのまま | 40/60(約 67%) |
同、話題のヒント(initial_prompt)を与える |
41/60 |
同、キーワード強調(hotwords) |
40/60 |
| 同、複数の候補から「次の最初の文字」で絞る | 41/60 |
| 声:ずんだもん、そのまま | 48/60(80%) |
この声は、子どもの声の代わりに聞かせた合成音声で、ロボットの声とは別の用途です。各条件 1 回、60 語だけの測定です。
間違いは、似た音への置き換えでした。
- 「わたあめ」→「また雨」
- 「ぼーる」→「ゴール」
- 「ぽすと」→「コスト」
- 「びわ」→「リワ」
- 「ぞう」→「そう」
ヒントなどの工夫では、ほぼ改善しませんでした(40〜41/60)。きれいな合成音声でこの数字なので、子どもの声や雑音では下がる、と覚悟しました。認識の精度を上げる代わりに、間違いを前提に遊べる作りにしました。
対策 1:復唱と取り消し
聞こえた言葉を、そのまま復唱します。子どもは間違いに気づけます。そして「ちがう」と言えば、直前の判定をなかったことにします。
子: 雷
リーチー: 『かみなり』だね!じゃあ『りす』。さいごは『す』だよ。
子: ちがう
リーチー: ごめんね、聞きまちがえたよ。『かみなり』のさいごは『り』だよ。『り』から始まる言葉を言ってね。
対策 2:似た音の許容と、「ん」の確認
最初の文字が似た音(ぼ/ご/ぽ、わ/ま、か/が など)なら、聞き間違いとみなして直します。「ん」で終わったように聞こえても、すぐ負けにせず「あってる?」と確認します。「ん」で負けにするのは、聞き間違いだと取り返しがつかないからです。
対策 3(子どもの実機テストで追加):何を聞かれているか分かる言い方
最初の版では、外れたときの返事は固定でした。
『ごめん』って聞こえたよ。『う』から始まる言葉かな?もういっかい言ってね。
子どものテストでは、これが何度も繰り返され、しりとりは止まりました(約 4 分間に 10 回)。ログを見ると、次のようでした。
子: ごめん リーチー: 『ごめん』って聞こえたよ。『う』から始まる言葉かな?…
子: 何から始まる言葉だっけ? リーチー: 『なにからはじまることばだっけ』って聞こえたよ。『う』から始まる言葉かな?…
子: 言葉だよ リーチー: 『ことば』って聞こえたよ。『う』から始まる言葉かな?…
子: 猫! リーチー: 『ねこ』って聞こえたよ。『う』から始まる言葉かな?…
子どもは「う」が何の「う」なのか、分かりませんでした。ここから、次の 3 つを直しました。
- 毎回「『りょこう』のさいごは『う』だよ」と、前の言葉つきで言う。
- 外れが 2 回続いたら、「たとえば『うみ』みたいな言葉だよ」とヒントを出す。3 回続いたら「むずかしかったら『おしまい』って言ってね」と案内する。
- 「もう一回」「何から?」「ヒント」は、聞き間違いとして扱わず、問題をもう一度言う。
さらに、終わらせられない問題もありました。「シートリーは終わりにしよう」「しりとりを終わりにする?」と言っても止まりませんでした。ゲーム中の終了ワードに「終わり」が入っておらず、認識が「シートリー」と崩れたためです。「終わり/やめ/おしまい」を、漢字でも読みでも拾うようにし、「クイズにしよう」と言えば、しりとりをやめて切り替わるようにしました。
直したあとの大人のテスト(21 回)では、しりとりが 14 回続き、終了とクイズへの切り替えも動きました。ただし、子どもでの再テストはしていません。
残った問題
- 認識そのものが外れることは、変わりません(「ぼんじ」「もうみーず」)。
- Reachy Mini が『ぎたー』のような、のばす音で終わる言葉を選ぶと、幼い子どもには分かりにくいかもしれません。
- Reachy Mini の語彙が少ない文字(『ず』など)で終わると、Reachy Mini が負けます。語彙を増やしましたが、完全ではありません。
4.2 クイズ:問題は入れ替え可能にし、答えをローカル LLM に作らせない
クイズは、ローカル LLM の苦手が出やすい遊びだと考えました。問題文と正解をその場で作らせると、間違った答えを子どもに言うことがあります。そこで、問題と正解は事前に Claude で作成し、ローカル LLM は問題の生成にも判定にも使いません。
さらに、子どもに遊ばせる日が決まっていたため、同じ問題を何度も出さないように、問題を quiz.json に外出ししました。コードを触らずに、入れ替えられます。
{
"questions": [
{
"q": "1週間は何日?",
"choices": ["5日", "7日", "10日"],
"answer": 2,
"note": "月曜から日曜までで7日だね。",
"reads": {"7日": ["なのか", "しちにち", "ななにち"]}
}
]
}
読み込み時に形式を確認します。選択肢が 3 つでない、答えが 1〜3 でない、問題が 5 問に満たない、といった誤りは、遊び始める前にエラーにします。
if len(x["choices"]) != 3 or x["answer"] not in (1, 2, 3):
raise ValueError(f"quiz.json {i}番目: choices は3つ、answer は1-3")
答えの聞き取りでは、数字を含む選択肢(「7日」「100度」)が難所でした。子どもは「なのか」「ひゃくど」と答えますが、認識結果の表記は揺れます。そこで reads に読みを書けば、聞き取れるようにしました。選択肢の文字そのもの、読み、「1/2/3」のどれかが含まれていれば、答えたことにします。
4.3 人の動きを追わせる:映像は使わず、保存しない
顔を追って首を回す機能は、ロボット本体のデーモンにあります。母艦のアプリは、追従を入れる・止めると指示するだけで、映像は使いません(ただし、SDK の接続が映像の流れを母艦まで運ぶことは、2.2 と 3.5 のとおりです)。
POST /api/media/tracking/enable {"weight": 0.8} # 追従を有効化(強さ 0〜1)
POST /api/media/tracking/disable {}
GET /api/media/tracking/face → {"status":"ok","face_target":{"detected":false,"x":null,"y":null,"roll":null,"ts":null}}
このアプリには、映像を読み出すコード(get_frame など)がありません。必要なら顔の位置(座標)だけを問い合わせられますが、画像を取り出したり、録画したり、保存したりする処理はありません。
ただし、追従は設定(config.json の tracking)で入り切りでき、既定は入です。入のあいだは、ロボットの中でカメラが使われ続けます。カメラそのものを使いたくない場合は、false にしてください。
首をかしげるなどの身ぶりのときは、追従の強さ(weight)を 0 にして一時停止し、動き終わったら戻します。
POST /api/move/goto
{"head_pose": {"x":0,"y":0,"z":0,"roll":0.143,"pitch":0.0,"yaw":0.0}, "antennas":[0.5,-0.5], "duration":0.7}
個体差の補正:この個体は、指示を 0 にすると roll が約 +0.107 rad 傾きました。
wake_upでは直らなかったので、-0.107 rad を足して水平にしています(目視で確認。本体には保存されないので、アプリの起動のたびに適用します)。
「データは持たないのが一番」という考えは、ログにも通じます。ログは文字だけを残し、子どもの録音は既定で保存しません(save_audio: false)。画像は、アプリが読み出さず、保存もしません。
4.4 声をかわいくする:Windows の音声から VOICEVOX へ
ロボットの声は、子どもの印象を大きく左右します。最初は Windows の標準の音声(Microsoft Haruka)で話させていました。ネットワークを使わないので、「外に出さない」という方針には合います。聞き取りやすくて自然でしたが、子ども向けには、かわいくありませんでした。
そこで、VOICEVOX15 に変えました。VOICEVOX は、パソコン上でエンジンを動かせる音声合成ソフトで、話者(声)を選べます。エンジン(0.26.0、CPU 版)は、母艦の中(ループバック)のポート 50021 だけで待たせるので、外からはつながりません。GPU は LLM と音声認識で使っているので、音声合成は CPU にしました。
声は、いくつか聴いて、ずんだもん(ノーマル)にしました。
話す速さは 1.1 倍、抑揚は 1.2 倍にしました。呼び出しは、文章から「音声の設計図」を作り、その数値を変えて、音声にする 2 段階です。
POST http://localhost:50021/audio_query?text=…&speaker=3 → 設計図(JSON)
(設計図の speedScale=1.1, intonationScale=1.2 に変更)
POST http://localhost:50021/synthesis?speaker=3 → wav
待ち時間への対策
VOICEVOX を CPU で動かすと、話者を試したときの測定で、1 文の合成に 0.7〜1.9 秒かかりました。会話では長く感じるので、次の 3 つを入れました。
- 文ごとに合成して、先にできた文から再生する。 返事を句点で区切り、1 文目を再生しているあいだに、2 文目を作ります。
-
同じ文は作り直さない。 話者、速さ、文章からハッシュを作り、同じ文のファイルがあれば使い回します(
tts_cache)。ただし、同じ文がいつも同じ抑揚で流れるので、自然さは減ります。速さとのトレードオフで、良し悪しです。 - よく使う文は、起動時にまとめて作る。 挨拶や、「はーい」「ばいばい」のような短い返事などです。
LLM は、返事に絵文字を入れることがあります。合成の前に、絵文字を取り除いています。
VOICEVOX が使えないとき
VOICEVOX のエンジンが起動していないときは、自動で Windows の音声に切り替えます。合成の途中で失敗した場合も、その文だけ Windows の音声になります。遊んでいる途中で止まらないようにするためです。
切り替えは 2 段階です。まず、アプリの起動時に、エンジンが生きているかを確かめます(/version に 1.5 秒で答えるか)。答えなければ、その回の遊びは、すべて Windows の音声で話します。
def vv_alive():
try: return bool(urlopen(VV + "/version", timeout=1.5).read())
except Exception: return False
# 起動時に 1 回だけ判定(play.py)
use_vv = cfg["tts"]["engine"] == "voicevox" and voice.vv_alive()
次に、遊んでいる途中でエンジンが落ちるなどして、文ごとの合成が失敗したら、その文だけ Windows の音声に落とします。
def tts_wav(text, tts, use_vv):
if use_vv:
try:
... # キャッシュを探し、なければ VOICEVOX で合成する
return path, False
except Exception as e:
print("VOICEVOX失敗、Windows音声に切替:", e, flush=True)
path = os.path.join(HERE, f"tts_{os.getpid()}_{time.time_ns()}.wav")
synth(text, path) # Windows の音声(Haruka)
return path, True # True: 一時ファイル(再生したあとで削除する)
起動していなくても、途中で落ちても、ロボットは黙らずに話し続けます。
残るもの
声のキャッシュ(tts_cache)には、ロボットが話した返事が、音声ファイルとして残ります。子どもの録音ではありませんが、しりとりの復唱など、子どもの発言が含まれる文もあります。私の環境では、テストを重ねたあとに、約 200 ファイル(約 20MB)になっていました。遊んだあとは、ログと同じように、保護者が削除する運用にしてください。
公開や配布する場合は、「VOICEVOX:ずんだもん」のクレジット表記が必要です。音声認識の実験(4.1)には、「VOICEVOX:春日部つむぎ」の声も使いました。
5. 子どもと遊ばせて分かったこと
5.1 何が起きたか
| 場面 | 結果 |
|---|---|
| 子どもたち(25 分の枠のうち約 8 分) | 23 回やりとり。警戒ワード(ALERT)は 0 回。しりとりが続かなくなり、止めて終了 |
| 大人、修正 1 回目 | 17 回。「しっとりしましょう」と聞こえ、しりとりが始まらなかった |
| 大人、修正 2 回目 | 21 回。しりとり 14 回、クイズへの切り替え、「踊って」、終了まで動作 |
5.2 時間
ログに残る時間は、1 回の発話あたり、録音 1〜3 秒、音声認識 0.1〜0.4 秒、ゲームの判定 0.1 秒前後、LLM の返答 0.3〜0.4 秒でした。音声合成は VOICEVOX^voicevoxで 1 文 0.7〜1.9 秒です。文ごとに先に作って再生し、よく使う文は起動時に作りだめしました。保護者の私は、待ち時間を問題ないと感じました。
5.3 うまくいかなかった点
- 換気扇。 起動時の雑音は小さい(0.0003〜0.0011)のに、遊んでいるときに換気扇が回ると、声が埋もれました。ロボットのマイクに依存する話で、アプリ側の調整では直せません。
- 同時に話す。 複数人が同時に話すと聞き取れません。
- エコー。 ロボット自身の声を拾わないよう、発話後 1.3 秒はマイクを無視し、直前の発話と似た文字列を除外します。完全ではありません。
- 雑談の質。 子どもが「チリッとりっしょ」と言ったとき(しりとりの聞き間違い)、「それは危ないからやめようね」と返した例がありました。また、大人のテストで「しっとりしましょう」が雑談として答えられ、しりとりが始まりませんでした。LLM の返事は、安全フィルタ(出力の禁止語、入力の警戒ワード)で絞っていますが、品質は一定ではありません。
5.4 安全面の仕組み
- 子どもの録音は保存せず、画像はアプリが読まず、保存もしない。ログは文字だけで、保護者が読んで消せる。
- 追従(カメラの利用)は、設定で切れる。
- 入力に警戒ワードがあると、大人に相談するよう子どもに促す文に切り替え、ログに ALERT を残す(保護者への自動の通知はありません)。
- 出力に禁止語があれば、固定文に置き換える。
- 1 回の時間制限(既定 20 分、子どものテストは 25 分)で自動終了し、終了 2 分前に知らせる。
-
STOPファイル、Ctrl+C、「バイバイ」「ストップ」で止まる。 - モーターの動きの角度に上限がある。
6. 制限と、まだ見ていないこと
- 子どもの声での聞き取り精度は、体系的に測っていません(実機の 1 回のみ)。
- 音声認識の比較は kotoba-whisper-v2.0 だけです。より大きなモデル(large-v3 など)は試していません。
- 外部への通信は、設定と調査で潰したうえで、数分間だけ観察しました(3.5)。長時間の観察は、していません。声とカメラの映像のどちらについても同じです。
- 子どもの発言は、文字のログとして母艦に残ります。遊んだあと、保護者が内容を確認し、不要なら削除する運用です。
- E4B の返事の質(特に雑談)は、子ども向けとして十分とは言えません。
- 声は VOICEVOX を使っています(4.4)。公開や配布する場合は、「VOICEVOX:ずんだもん」のクレジット表記が必要です。
- ロボットの声のキャッシュ(
tts_cache)には、復唱した子どもの言葉が、ロボットの声で残ります。遊んだあとに削除してください。
7. おわりに
ロボットの検証は、結局、「何を LLM に任せ、何をコードに戻すか」の線引きになりました。しりとりとクイズでは、E4B にルールの判定をさせず、コードに戻しました。顔の追従は、LLM ではなくロボットの中で完結させました。そして子どもの声とカメラの映像は、家の外に出ない道を選びました。
いちばん苦労したのは LLM ではなく、音声認識の聞き間違いと、子どもに何を聞かれているか伝わる言い方でした。ロボットは、遊んでもらうために、まず分かりやすく話さなければいけない、と思い知りました。
次は、より大きな音声認識モデルの比較と、子どもの声での評価です。
ところで、肝心の子どもたちが、学校から帰ってきても続けてリーチーと遊んでくれるのかどうか。そして、父親を見る子どもたち、そして奥様の視線はどうなるのか。どちらも、神のみぞ知る、です。まず、部屋を片付けるか。
参考リンク
- Nemotron 3.5 Lightning を VRAM 16GB で動かした記事
- 24GB推奨のモデルを16GBで:Nemotron 3.5 Bolt の速度・日本語品質・エージェント実験
- CES 2026でも過熱する「フィジカルAI」(MONOist)
- ECOVACS のロボット掃除機がハッキングされた件(GIGAZINE)
- DJI のロボット掃除機 Romo の脆弱性の件(GIGAZINE)
- 上場企業の個人情報漏えい 2026年は100万件以上がすでに10件(ScanNetSecurity)
- 2025年の個人情報漏えい、被害人数は3,063万人(東京商工リサーチ調べ)
- Reachy Mini documentation(Hugging Face)
- Reachy Mini のセットアップガイド(Hugging Face)
- Reachy Mini Hardware Datasheet(Hugging Face)
- Reachy Mini の公式 GIF(GitHub)
- NVIDIA Nemotron 3.5 Bolt E4B(Hugging Face)
- llama.cpp
- faster-whisper
- kotoba-whisper-v2.0-faster(Hugging Face)
- pykakasi
- VOICEVOX
- Packet Monitor (Pktmon)(Microsoft Learn)
使用した声: VOICEVOX:ずんだもん / VOICEVOX:春日部つむぎ
-
CES 2026でも過熱する「フィジカルAI」、バズワードを脱して本格的なトレンドへ(MONOist)(2026-01-07) ↩
-
上場企業の個人情報漏えい 2026年は100万件以上がすでに10件 過去最悪ペース(ScanNetSecurity)(2026-10-21。東京商工リサーチの 2026-10-07 発表に基づく) ↩
-
ロボット掃除機ECOVACSのハッキングに関する記事(GIGAZINE)(2024-10-09) ↩
-
DJIのロボット掃除機「Romo」の脆弱性に関する記事(GIGAZINE)(2026-03-09) ↩