こんにちは、皆さん。
カメラを向けたら、そこで今なにが起きているかを言葉で返してくる。そういう装置は、モデルの賢さよりも「何秒で返ってくるか」で使えるかどうかが決まります。3秒なら見守りに使えますが、30秒なら過去のログにしかなりません。
さて、今日はMicrosoftのMage-VLを、mlx-vlmに依存しない独立したMLX実装としてApple Siliconへ移植し、静止画・frame-sampled video・proactive streaming・codec-native sparse videoの4経路すべてでfloat32一致を確認しました。ただし**一致の相手は経路で違います。**静止画・動画・codecは公式PyTorch実装から生成したfixtureですが、streaming gateだけは公開された参照意味論にもとづくplain PyTorch実装です(mamba-ssmにmacOS buildが無いため)。そのうえで、区間が届くたびに処理する実装を作り、どこまでライブ入力に追いつけるかを測りました。
固定動画での最良構成は、Mac Studio M4 Maxのcodec経路・2秒区間で、イベント発生から全文が出るまで最悪3.48秒です。MacBook Pro M1 Maxでは4秒区間で6.59秒でした。ただしこれは768x512の動画ファイルを流したときの値で、ライブカメラの数値ではありません。
Mage-VLとは
Mage-VLは、Microsoftが2026年7月に論文とともに公開したvision-language modelです。Qwen3-4B-Instruct-2507 decoderと独自のMage-ViTを組み合わせた約47.4億parameterのモデルで、静止画、動画、時系列位置推定、イベント駆動のproactive streamingを1つのcheckpointで扱います。公式checkpointはBF16で約10.8 GBです。
特徴はcodec-native経路です。動画の全frameを均等にtoken化する代わりに、I frameのpatchと、P frameのmotion vector・residual energyが示す「変化の大きいpatch」だけを使います。Microsoftはこの経路についてvisual tokenを75%以上削減し、均等frame sampling比で最大3.5倍高速になったと報告しています。これはMicrosoftの論文上の結果であり、本記事で追試した値ではありません。
学習済みモデルは1つで、経路が4つあります。数を誤解しやすいので、役割とデータの流れを整理します。
| 経路 | 入力 | 前処理でやること | 出力 |
|---|---|---|---|
| 静止画 | 画像1枚 | smart_resizeとpatch化 | 生成文 |
| frame-sampled video | 動画 | 均等サンプリングした各frameをpatch化 | 生成文 |
| codec-native sparse video | 動画 | I frameとmotion vectorから変化patchを選び、canvasへ詰める | 生成文 |
| proactive streaming | 動画(codec前提) | 上と同じ + Mamba mixerのgate | speak / silentの判定 + 生成文 |
移植したコードはkiarina/mage-vl-mlxで公開しています。実行時はtorch-freeで、src/と2つのinference scriptはPyTorchを必要としません(重み変換とparity検証にはPyTorchを使います)。
ライセンス
| 対象 | ライセンス |
|---|---|
| microsoft/Mage | MIT |
| microsoft/Mage-VL checkpoint | Apache-2.0 |
| state-spaces/mamba | Apache-2.0 |
| 本移植 kiarina/mage-vl-mlx | MIT |
本移植はupstreamのsourceを含まず、checkpointも再配布しません。scripts/convert_weights.pyがHugging Faceから取得してローカルで変換します。
1点だけ注意があります。codec経路が必要とするcodec-video-prepは、**PyPIのmetadataにライセンスを宣言しておらず、sourceも公開されていません。**Microsoft自身のこの経路向け依存ですが、ライセンスが問題になる用途では事前に確認してください。
今回検証する内容
この記事では次の4点を扱います。
- Mage-VLの4経路を独立したMLX実装として再現し、float32での一致を示せるか
- codec-nativeのtoken効率は、どの比較条件でも成立するのか
- streaming gateは何に使えて、何には使えないのか
- Apple Siliconでどこまでライブ入力に追いつけるのか
先行実装との関係
「初のMLX実装」ではありません。位置づけを先に書きます。
- 静止画のMLX実装は
Blaizzy/mlx-vlmのpull request 1745にあり、2026-07-29にmergeされています - 動画・streamingのMLX実装は
rsravanreddy/Mage-VL-MLXが公開しています。ただしend-to-endのlogit parityは未確認なので、設計の参考であって一致の根拠にはしていません
本移植は上記のいずれとも独立に4経路すべてを実装し、float32一致を経路ごとに検証しました(gateの参照だけは上に書いたとおり別です)。この記事で主張するのはこの事実までです。
検証環境の再現
Apple Silicon Mac、mise、uv、FFmpeg、Gitが必要です。初回はMage-VL checkpointの取得(約10 GB)とMLX形式への変換が走ります。
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .mise/tasks 2026/08/27/mage-vl-realtime-benchmark
mise trust . && mise trust 2026/08/27/mage-vl-realtime-benchmark
mise -C 2026/08/27/mage-vl-realtime-benchmark run
sparse-checkoutに渡すのはdirectoryだけです。cone modeはrepository直下のファイルを常にcheckoutするため、Makefileやmise.tomlを並べる必要はありません。並べるとgit 2.42以降はfatal: '.gitignore' is not a directoryで失敗します。clone直後の設定はuntrustedなので、mise trustを通してからtaskを実行します。
既定のrunはframes backendのmatrixだけを実行します。ほかは個別に呼びます。
| task | 内容 | 追加要件 |
|---|---|---|
run(既定) |
framesのmatrix | — |
run codec |
codecのmatrix(24 fpsと8 fpsの2パス) | Docker |
run tokens |
max_new_tokensを16 / 32 / 64で振る |
Docker |
run saturation |
カメラ経路を飽和させて遅延と欠落を測る | — |
run memory |
稼働中のWeb UIのメモリをsampling | UIを別途起動 |
**この記事の主要な数値はcodec経路のものなので、それを再現するにはこちらを実行してください。**Dockerが要ります。
mise -C 2026/08/27/mage-vl-realtime-benchmark run codec
parity側は経路ごとに別のlabがありますが、**こちらは実行可能なlabではなく記録です。**fixtureの作り方、比較したコマンド、失敗した試行、判定基準がREADMEに書いてあり、再現は各labが固定している移植commitのrepositoryから直接行います。
| lab | 対象 |
|---|---|
2026/08/25/mage-vl-mlx-stage1-image-parity |
静止画 |
2026/08/25/mage-vl-mlx-stage2-video-parity |
frame-sampled video |
2026/08/25/mage-vl-mlx-stage3-streaming-gate |
streaming gate |
2026/08/26/mage-vl-mlx-stage4-codec-native |
codec-native sparse video |
読むだけならdirectoryを並べてcheckoutできます。mise trustはtaskを持つlabにだけ必要です。
git sparse-checkout set .mise/tasks \
2026/08/25/mage-vl-mlx-stage1-image-parity \
2026/08/27/mage-vl-realtime-benchmark
検証方針と全stageの結果はmage-vl-mlx-port.mdにまとめてあります。
【1】4経路はfloat32で一致した
公式PyTorch実装から生成したfixtureと比較しました(streaming gateだけは参照が違います。後述します)。
| 経路 | 結果 |
|---|---|
| 重みのkey写像 | 本体696 / gate 64、missing・unused・shape不一致すべて0 |
| 静止画のvision tower | 相対誤差8.9e-06〜1.6e-05、cosine 1.00000000 |
| 静止画のgreedy 64 token | 3枚とも完全一致 |
| 動画の前処理 | frame index・grid・patch_positions・pixel valuesがbit一致 |
| 動画のvision tower | 相対誤差8.892e-06〜3.606e-04、cosine 1.000000 |
| 動画のgreedy 64 token | 3本とも完全一致 |
| streaming gateのMamba mixer | 最大絶対誤差2.682e-07〜4.396e-07(事前に定めた基準は1.0e-5) |
| codecの前処理 | patch_positions・pixel valuesがbit一致(max_abs 0.0) |
| codecのgreedy 64 token | 2本とも完全一致、logits cosine 1.000000 |
前処理がbit一致したのは、Qwen2-VLのsmart_resize、rescaleとnormalizeの融合順序、patch配置をQwen2VLImageProcessorと突き合わせたためです。ここが1 bitでもずれると、後段の一致は評価できません。
**この一致はfloat32に限った話です。**bfloat16はbackend間の丸め差が累積するため、閾値付近では判定が反転します。実際に、cut_eventの時刻0でfloat32が0.5022、bfloat16が0.4977となり、speakがsilentへ反転しました。運用はbfloat16、検証はfloat32、と分けるのが現実的です。
もう1つ注意があります。**streaming gateの参照はmamba-ssmのCUDA kernelではありません。**mamba-ssmにmacOS buildが無いため、公開されている参照実装の意味論に従ってplain PyTorchで書き直したものを参照にしています。CUDA kernelとの一致は未検証です。
【2】codec-nativeの効率は、比較条件で結論が変わる
ここが一番おもしろかった部分です。同じ動画(soccer_goal、193 frame)に対して、4通りの比較をしました。均等サンプリングは1 frameあたり384 tokenで一定です。
| 比較のしかた | 結果 |
|---|---|
| 時間カバレッジを揃える | 1 frameあたり384 → 18.4 token(95%削減) |
| 固定32 frame予算(12,288 token)と比べる | 71%削減しつつ、見るsource frameは6倍 |
| 既定の8 frame(3,072 token)と比べる | codecのほうが多い(3,528 token) |
| 同条件の速度(8 frame比) | 12.6〜14.3 → 9.6〜11.0 token/sでcodecが遅い |
つまり「codecは速い」も「codecは遅い」も、比較条件を書かなければどちらも言えてしまいます。Microsoftが報告する最大3.5倍は、同等の理解に必要なframe数どうしの比較と解釈され、上の「8 frameとの比較」とは条件が違います。
一方、区間処理では削減が素直に効きます。M4 Max・4秒区間で、区間を元の24 fpsのままcodec前処理へ渡すと生成に2.230秒かかりますが、8 fpsへ間引いてから渡すと0.828秒です。同じ4秒を見ているのに、渡し方だけで生成時間が2.7倍違います。
これは記事の後半で効いてきます。
【3】streaming gateはイベント時刻を指さない
proactive streamingのgateは、区間ごとにspeak / silentを判定します。「試合のゴールの瞬間に反応してくれるのでは」と期待して測りましたが、そうはなりませんでした。
| 観測 | 結果 |
|---|---|
| コンテンツ種別の分離 | サッカー中継0.69〜0.79 対 静かな廊下0.04〜0.11 |
| イベント時刻 | 指さない。シュートを含む区間が対照より低い例もある |
| frames入力での発火 | 0.0009〜0.0062。閾値を下げてもfilterにならない |
| 静止画を再エンコードしても残るか | 残る。スポーツ0.79〜0.82 対 静止シーン0.12〜0.14 |
gateが見ているのは「この映像は喋る価値があるコンテンツか」であって、「今この瞬間に何か起きたか」ではありませんでした。**したがって実用上は、低い閾値のpre-filterとしてgateを使い、イベントの判定は生成文で行うのが正解です。**Web UIのサッカーgoal presetも、この方針でcodec backend・gate 0.3へ校正してあります。
もう1つ重要な事実として、**gateはcodec入力を前提としています。**frames入力ではほぼ発火しないので、frames backendでstreaming gateを使う構成は成立しません。
なお、presetの校正は正例1本・対照1本だけで行っています。precision / recallを主張できる規模ではありません。
【4】Apple Siliconでどこまで追いつけるか
ここからが本題です。まず「リアルタイム」を定義します。
- RTF(real-time factor): media長に対する処理時間の比。1未満でなければ、継続入力で遅れが溜まり続ける
- first text: 区間が確定してから最初の文字が出るまで
-
最悪 event→全文: イベントが区間の先頭で起きた場合の、全文が出るまでの遅延。
区間長 + full response
固定動画(glass_fall)、bfloat16 model + float32 gate、16 token、3回の中央値です。
| 機種 | 構成 | RTF | first text | 最悪 event→全文 |
|---|---|---|---|---|
| M4 Max | codec (8 fps)・2秒 | 0.734 | 1.263秒 | 3.48秒 |
| M4 Max | codec (8 fps)・4秒 | 0.400 | 1.366秒 | 5.62秒 |
| M4 Max | codec (24 fps)・4秒 | 0.862 | 3.180秒 | 7.51秒 |
| M1 Max | codec (8 fps)・4秒 | 0.644 | 2.205秒 | 6.59秒 |
| M1 Max | codec (24 fps)・4秒 | 1.542 | 7.166秒 | — |
| M4 Max | frames・2秒(framesの最良) | 1.047 | 2.130秒 | — |
| M1 Max | frames・2秒(framesの最良) | 1.991 | 6.766秒 | — |
**frames backendはどちらの機種でも、どの区間長でも追いつきませんでした。**最良でもM4 Maxの1.047です。
時間はどこへ消えているか
律速はdecodeではなく、visual tokenに対する生成のprefillです。M4 Max・4秒区間の段別中央値を並べます。
codecは前処理をcontainerの起動ぶん増やしますが(0.084秒→0.498秒)、visual tokenを疎にした後のvisionと生成が短くなります。生成は3.357秒から0.828秒へ、vision towerは0.774秒から0.128秒へ落ちました。gateはどちらでも0.05秒未満で、コストにはなりません。
間引きが成立範囲を決める
【2】で見た「渡し方だけで生成時間が2.7倍違う」は、そのままRTFに出ます。
**M1 Maxは、間引かないとどの条件でも追いつけません。**間引くと4秒区間で初めてRTFが1を切ります。M4 Maxも、間引き前の最良は4秒区間でしたが、間引くと2秒区間で追いつくようになり、最悪遅延は7.51秒から3.48秒へ半分以下になりました。
理由はcodec前処理のcanvas数です。--group_size 32でframeをまとめ、グループごとに--images_per_group 4枚のcanvasを作る設定なので、32 frameまではcanvas 4枚・576 tokenで一定です。
| 窓のframe数 | canvas数 | visual token |
|---|---|---|
| 8 / 16 / 32 | 4 | 576 |
| 64 | 12 | 1,728 |
| 120 | 20 | 2,880 |
つまり2 fpsから8 fpsへの引き上げは、4秒窓ならモデルの負荷を増やさずにcodecの時間解像度だけを4倍にします。逆に、24 fpsの動画をそのまま渡すと4秒窓が96 frameになり、この増加側へ入ります。visual tokenを増やさずに時間解像度を上げられる上限が、32 frameというわけです。
ここで測ったのはcanvas数、visual token数、前処理時間だけです。時間解像度を上げたときに検出や記述の精度がどう変わるかは測っていないので、窓の秒数 × capture rateを32へ寄せるのは「コストを増やさない目安」であって、精度まで含めた最適値ではありません。
**機種ごとに別の設定が要ります。**M4 Maxの2秒設定は、M1 MaxではRTF 1.157で破綻します。
生成長も設定である
もう1つ、見落としやすい軸があります。max_new_tokensです。M4 Max・codec (8 fps)・2秒区間で、上限だけを振りました。
| max tokens | 実際の生成token | generation | RTF |
|---|---|---|---|
| 16 | 16, 16, 16, 16(全て頭打ち) | 0.851秒 | 0.779 |
| 32 | 32, 31, 32, 32(頭打ち) | 1.163秒 | 0.936 |
| 64 | 61, 31, 64, 50 | 1.626秒 | 1.126 |
ここから4点わかりました。
- **上限はbindingしない限り無料です。**同じ条件でも
glass_fallは3つの上限すべてで生成テキストが1文字も違わず(9〜15 tokenでEOS)、RTFは0.723 / 0.726 / 0.727で動きません。「上限を上げると遅くなる」は誤りです - **bindingすると文が途中で切れます。**上限16の出力は
'...dribbling the ball, while a player'のように切断されます。速さと文の完結は交換関係です - **成立範囲を割るとfirst textも道連れです。**上限64のfirst textは
1.669秒で、16 / 32の1.273/1.277秒から悪化します。backlogが次の区間の開始を遅らせるためです - **生成長は質問文だけでは決まりません。**同じ質問文でも
glass_fallは9〜15 token、soccer_goalは上限に張り付きます。映像に記述すべきものの量が効きます
なお、上の2つの表は動画が違うので、行同士を比較できません(bitrateが490 kBと1469 kBで、codec-native前処理はこれに反応します)。各表の中でだけ比較してください。
メモリ:MLX peakを必要量の見積もりに使わない
区間処理のMLX peak memoryは、framesで11.96〜14.27 GB、codecで11.96〜13.08 GBでした。この数字だけ見ると16 GB機でも動きそうに見えます。
**しかしこれは必要量の指標になりません。**軽い設定(1秒stride、4秒窓、2 fps、2 token)を長時間流したときのmacOS footprintは22 GBで安定しますが、重い設定(8秒stride、16秒窓、4 fps、64 frame、32 token)へ移すと最大50 GBまで伸びました。停止した時点ではfootprint 49 GB、swap 17.2 GBです(64 GBの機体)。同時点のMLX peakは22.04 GBでした。
原因はMLXのbuffer cacheでした。footprintのIOAccelerator (graphics)は全条件でMLXのactive + cacheとほぼ一致し、Metalの予約分ではありませんでした。cacheは確保の高水位を保持し、停止してもアイドルでも返りません。
mx.clear_cache()は有効で、34 GB前後を即座に解放します。停止後のアイドルで呼ぶとfootprintは12 GBまで落ちました。ただし稼働中に解放しても、同じ設定を続けるかぎり150秒後にはcache 28.68 GB、footprint 46 GBまで戻ります。**意味を持つのは、重い設定から軽い設定へ移るときと、runを止めたときです。**移植側では、runの停止時にmx.clear_cache()を呼ぶようにしました。
実際に使う最大設定でfootprintを測ってから、必要なunified memoryを見積もってください。
追いつかないとき、何が起きるか
RTFが1を超える設定で流し続けると、どうなるでしょうか。「遅延が無限に増えていく」と考えたくなりますが、実測は違いました。
**lagは発散しません。**frame queueには上限があり、満杯になると古いframeから捨てるため、定常状態は「queue待ち(queue長 / capture rate)+ 1区間の処理時間」で頭打ちになります。M1 Maxで13.400秒、M4 Maxで10.415秒でした。
**有界なのは遅延であって、欠落ではありません。**その定常状態を保つために、入力の58〜75%を捨てています(M1 Max 75.1%、M4 Max 58.3%)。捨てたぶん窓は実時間で伸び、公称4秒の窓が10〜19秒をカバーします。
**この経路は「最新の映像に対する遅い応答」を返すのであって、「入力全体の遅い要約」ではありません。**見守り用途なら望ましい挙動ですが、記録の網羅が要る用途では致命的です。
表示が正直かどうかは、サーバとクロックを共有しなくても判定できます。両方が正直ならend_s + lag_sはクライアント側の経過時間と一致するはずだからです。21区間・42区間を通してずれは一定で、変動幅は0.01秒未満でした。
動かしているところ
区間到着ごとの処理は、RealtimeSessionとして再利用可能なAPIにしてあります。参照実装のローカルWeb UIも同梱していて、動画ファイルの再生かMacのカメラ入力に対して、生成文・gate score・遅延・backlogをライブ映像の横に出します。
スマートフォンの背面カメラを全画面モードで撮り、モデルはMac Studio M4 Maxがtailscale経由で受けています。43秒を頭と尻だけ落として、途中は切っていません。
2つ、説明が要ります。
**1つ目はDisplay delayです。**この動画では、映像側を数秒遅らせて応答と揃える機能をautoにしています。**モデルが即答しているわけではありません。**遅延を消しているのではなく、見る側のタイムラインをずらして、応答と映像を並べて見られるようにする機能です。画面の隅には遅らせていないライブ映像も出ています。
**2つ目は、画面に出ている数値です。**これは1セッションのライブカメラの値です。スマートフォンはtailnet経由で映像を送るため、ネットワークの状態が数値に混ざります。**この記事の数値はすべて768x512の動画ファイルで測ったもので、ライブカメラの数値は載せていません。**固定動画のsweet spotはライブカメラへそのまま移らず、区間長を伸ばす必要があります。
**モデルの誤りもそのまま出しています。**動画の00:16付近で「白いプラスチックの玩具を丸鋸で切っている人」「映像は白黒である」と出ていますが、実際は白いぬいぐるみに指を向けているだけです。64 token・短い区間の逐次記述には、この種の誤りが混ざります。編集で消すと、モデル本来の性能が伝わらなくなります。
何に使えて、何に使えないか
ここまでの数字を、使う側の言葉に置き換えます。
**目の前の出来事へ即座に反応する用途には届きません。**イベントが起きてから全文が出るまで、最良の機種でも3.48秒かかります。人が「今のプレー、すごいですね」と言うまでの時間は1〜2秒なので、そこには足りません。カメラを向けた本人と会話するような、現実そのものと同期していなければならない使い方は成立しません。
**一方、映像を数秒遅らせてよいなら「実況」の形にはできます。**RTFが1未満なら遅れは溜まらないので、表示する映像を応答時間ぶん遅らせれば、映像と生成文が並んで進みます。デモ動画で使っているDisplay delayがこれで、放送のディレイと同じ考え方です。視聴者から見れば、映像に合わせて実況が付いている状態になります。遅延を消しているのではなく、一定のオフセットとして固定しているだけ、という点だけ間違えないでください。
**「見守り」や「イベント要約」は、遅延の面では要件に入ります。**玄関のカメラを見ていて「誰か来ました」「荷物が置かれました」を数秒後に通知するのであれば、3〜7秒の遅延は問題になりません。むしろ、遅延が発散せず一定に収まるという性質のほうが重要です。
**ただし、この記事が測ったのは遅延と持続性能だけです。**実写での検出精度、見逃し、長時間運用の信頼性は評価していません。デモ動画にもモデルの誤りが混ざっていますし、gate presetの校正は正例1本・対照1本です。使えるかどうかの最後の判断には、自分の映像での精度評価が別途要ります。
**「あとから全部の要約が欲しい」用途には向きません。**追いつかない設定で流すと、入力の6〜7割を捨てて最新に追随します。記録の網羅が要るなら、リアルタイムをあきらめて事後処理へ回すのが正しい設計です。
**必要なMacも具体的になりました。**M1 Max世代でも、codec経路・4秒区間・8 fpsという設定なら追いつきます。ただし設定は機種ごとに違い、M4 Max向けの設定をそのまま持ってくると破綻します。メモリは、実際に使う設定でfootprintを測ってください。軽い設定で22 GB、重い設定では50 GBを超えます。
制約と未確認事項
- parityの主張はfloat32に限る。bfloat16では閾値付近で判定が反転する
- streaming gateの参照は
mamba-ssmのCUDA kernelではなく、plain PyTorchの再実装である - codec前処理はARM64 Linux containerを必要とし、macOS nativeだけでは完結しない
-
codec-video-prepはライセンスを宣言しておらず、sourceも公開されていない - 検証に使った動画はすべて合成またはLTX-2生成で、実写と長尺streamは未測定
- 固定動画のmatrixは
glass_fall・16 token固定、つまり上限がbindingしない領域で測っている。記述量の多い映像ではこれより遅くなる - 飽和時の計測は1セッションずつで、3回の中央値ではない
- codecの測定値は同時に走るcv-preinfer containerの数に影響される。掲載値はすべて単独実行で、他のGPU負荷が無い状態で測り直したものである
- 質問文で生成長を短くする経路は掃引していない。上限で切るのと違い文は切れないが、系統的には測っていない
- gate presetの校正は正例1本・対照1本のみで、precision / recallを主張できる規模ではない
- 量子化は未対応である
- gateは区間ごとに蓄積した視覚履歴を再生している。公式の全stream結果とは一致するが、stateful incremental Mambaではないため、長いstreamのコストは制限として報告している
検証後の感想
移植そのものよりも、「どの条件で比べるか」を決める作業のほうが時間がかかりました。codec-nativeは95%削減にも、削減なしにも、むしろ増加にも見えます。どれも同じ実装の同じ動画の話です。数字を出すこと自体は簡単で、その数字がどの比較の答えなのかを書き切るほうが難しい、というのが今回いちばん強く感じたことです。
意外だったのは、24 fpsの動画をそのままcodecへ渡していた不具合を直したときです。区間の切り出しにcapture rateを効かせるだけで、M1 MaxのRTFが1.542から0.644へ落ちました。モデルにも量子化にも触っていません。**追いつかない原因が、モデルの重さではなく入力の渡し方だったわけです。**手元のMacが遅いと感じたら、まず何を何枚渡しているかを数えてみるのがよさそうです。
streaming gateがイベント時刻を指さなかったのは残念でしたが、「コンテンツ種別の分離には使える」と分かったのは収穫でした。安いpre-filterとして前段に置き、判定は生成文にやらせる。役割を分ければ、それぞれ得意なことをしてくれます。
カメラを向けると数秒で言葉が返ってくる装置は、素直に楽しいです。玄関でも、作業机でも、水槽でも、置いてみたくなる場所はいくらでもありますね。


