---
title: "ホートンはささやきを聞く"
description: "私たちはESP32マイクのフリートを構築し、生の音声をサーバーにストリーミングし、Whisperで文字起こしして、Grafanaに表示しました。デスクの上の一つのプロトタイプから実際に観測可能なフリートへ至るまでに何が必要だったかを紹介します。ルーターのIDSが私たち自身のデバイスをブロックした出来事と、Whisperが沈黙の中でYouTubeのアウトロを朗読し始めた出来事を含めて。"
canonical_url: "https://2389.ai/ja/research/writing/horton-hears-a-whisper/"
last_updated: "2026-08-06T15:35:08-05:00"
doc_version: "1.0"
author: "Ivan Indrautama"
date: 2026-05-20
tags: ["horton", "voice-transcription", "esp32", "whisper", "fastapi", "grafana", "mqtt", "slack", "fleet", "hardware"]
---

# ホートンはささやきを聞く

> 私たちはESP32マイクのフリートを構築し、生の音声をサーバーにストリーミングし、Whisperで文字起こしして、Grafanaに表示しました。デスクの上の一つのプロトタイプから実際に観測可能なフリートへ至るまでに何が必要だったかを紹介します。ルーターのIDSが私たち自身のデバイスをブロックした出来事と、Whisperが沈黙の中でYouTubeのアウトロを朗読し始めた出来事を含めて。


オフィスで何が話されているかを継続的にまとめたいと思っていました。監視ではありません。部屋が自分のために書き込んでくれるノートのようなもの。火曜日のキッチンでの会話は何だったか？ 会議室で実際に何が決まったか？ すべての部屋に同時にいることはできませんが、マイクはいられます。

そこで私たちはESP32マイクのフリートをオフィスに配線し、音声をサーバーにストリーミングし、Whisperで処理して、その上にダッシュボードを構築しました。コードネーム：ホートン。ドクター・スースのゾウが埃の粒の上の小さな声を聞くキャラクターにちなんでいます。

## 1台から複数台へ

ホートンは私のデスクの上の1台のESP32-S3ボードとI²Sマイク（小型マイクの多くが使うデジタル音声バス）から始まりました。それをTCPソケット経由でPythonサーバーに生の音声をストリーミングするように配線しました。サーバーはWhisperを実行し、文字起こしをディスクに保存しました。それがスタック全体でした。動きました。文字起こしは意味を成しました。

1台が動くことの問題は、次に来る明らかな疑問です：*これが3台あったらどうなるか？* 次に4台。次に「午後11時と午後3時でキッチンはどう聞こえるか？」 2台以上になった瞬間、もはやデバイスを持っているのではなく——フリートを持っており、フリートについて考える必要があります。

## 仕組み

ループは小さいです。音声が一方から入り、文字起こしが他方から出ます。





小型のESP32-S3ファームウェアがI²Sマイクから生の音声を読み取り、TCPソケット経由でサーバーに継続的にストリーミングします。サーバーは2つのプロセスを持つFastAPIアプリです：`audio_server.py`がTCP音声インジェストを処理し、`web_server.py`がダッシュボード、API、ライブフィードを提供します。Whisper（私たちはOpenAIのWhisperのCPU最適化ポートである`faster-whisper`を使用）が文字起こしを行います。GrafanaはサーバーのAPIをデータソースとして使い、ダッシュボードを描きます。

文字起こしバックエンドはホットスワップ可能です：envに`ASR_BACKEND=whisper`または`ASR_BACKEND=parakeet`を設定すると、サーバーが起動時にどちらかを選択します。それについては後ほど。

## 行き詰まった場所

### YouTubeの幽霊

最初にすべてを接続したとき、ESP32は何時間もの沈黙を含む聞こえるものすべてをストリーミングしていました。沈黙を文字起こしするよう求められたWhisperは最善を尽くします。その最善はハルシネーションです。ページ全体の：

- *"Thanks for watching!"*
- *"Thank you."*
- *"Amen."*

そして一つの美しく、どんどん支離滅裂になるくだり：

> *"I'm not going to leave you alone. You're not going to leave me alone."*

WhisperはYouTubeで訓練されており、何も与えられないとYouTubeが行うことをします：いいねと登録を求めます。文字起こしはアルゴリズムとの交霊術のようでした。約1日は面白かったです。

本当の意味は、入力をゲーティングしなければ永遠に沈黙を文字起こしし続けるということでした。私たちは両方サーバーサイドの2つの防御層に落ち着きました。まず、`faster-whisper`の組み込み音声活動検出（VAD）をオンにしました。これにより音声信頼度の閾値以下の音声チャンクが文字起こしモデルに届く前にスキップされます。次に、既知のWhisperのハルシネーション——*「Thanks for watching」*、*「Amen」*など——の小さなブロックリストを追加しました。VADを通過したチャンクでもモデルがそれらの一つを出力した場合、捨てます。Whisperはすぐに静かになりました。YouTubeの幽霊は去っていきました。

### フリートがIDSでブロックされた

次の驚きは音声側からではありませんでした。ネットワーク側からでした。

さらにボードをフラッシュしてオフィス内に配置した後、2台（`kitchen`ボードと皮肉にも`horton`ボード）がAPへの接続に失敗し始めました。シリアルログに同じ行が繰り返し表示されました：

```
WiFi disconnected, reason=2
```

`reason=2`は`AUTH_EXPIRE`で、「APが去れと言った」というESP-IDFのコードです。認証情報が悪いのだと思っていましたが、認証情報は変わっておらず、他のボードは問題なかったことに気づきました。

実際の原因：UniFiルーターが2台のボードをブルートフォース接続試行として隔離していました。WiFiスタックが接続を失うたびに（ファームウェアが切断イベントのたびに即座に再接続しようとするため、これはよくありました）、新しい接続試行としてカウントされました。短時間にそれが十分な回数起きると、IDS（侵入検知システム）ルールがMACをブロックします。

修正はファームウェアv0.2.4に入りました：再接続の指数バックオフにより、蹴り出された瞬間にAPを叩き続けることがなくなりました。そのバージョンのリリースノートはそれを礼儀正しさの機能として表現していますが、そうではありません——「自分たちのフリートをIDSでブロックされないようにする」ことです。

しばらく笑いました。ドクター・スースのゾウプロジェクトは、一時的にインターネットの脅威アクターでした。

## ダッシュボード

2つの表面があります。一つはフリートに対して行動するため、もう一つは観察するためです。

**管理ダッシュボード**は意図的に小さくシンプルです：1つのFastAPIアプリ、1つのHTMLファイル、フレームワークなし、シングルページアプリなし。ページは：ダッシュボード、デバイス、ライブフィード、履歴、ダイアリゼーション（話者ラベリング、誰が何を言ったか）、録音、ファームウェア、システムステータス。ほとんどは名前の通りのことをします。2つは特記する価値があります。

**デバイス**は新しいボードが着地する場所です。新しいESP32をフラッシュして電源を入れると、それはホームに電話しますが、ページの上部の`Pending`リストに入り、管理者が`Approve`をクリックして名前を付ける（`kitchen`、`desk`、`meeting-room`）まで、送信するものは何も受け付けられません。新しいデバイスが信頼される前のワンクリック人間ゲート。

**ファームウェア**はOTA（無線アップデート）です。バイナリをアップロードすると*現在の*ファームウェアになり、デバイスは次のチェックイン時にそれを取得します。ページには実行中のバージョンと最新の利用可能なバージョンが並んで表示されるすべてのデバイスのテーブルが表示されるため、遅れているものを簡単に見つけられます。またロールバックのためのアップロード履歴もあります。OTAはファームウェア≥v0.2.1でゲートされています。それより古いものは、自分自身を更新する方法を学ぶためにもう一度USBフラッシュが必要です。

**Grafana**は隣に置かれ、APIが読み込むのと同じSQLiteストアを向いています。管理ダッシュボードは個別のアクション用、Grafanaはフリート全体のパターン用です。実際に役立つようにしたコツは、すべてのパネルでデバイス*横断*にすることでした：一つのグラフにデバイス間のRSSI（WiFi信号強度）、別のグラフにデバイス間の温度、3つ目にデバイス別の時間あたり文字起こし数。そうすることで、同一に見える4つのグラフを頭の中で比較するのではなく、外れ値を探すことになります。現在のFleetダッシュボードには再接続タイムシリーズ（「このデバイスが落ちそう」の先行指標）、ファームウェアバージョンの円グラフ（「一台フラッシュを忘れた」の先行指標）、稼働時間テーブル、どの部屋が最も話しているかを静かに教えてくれる文字起こしレートパネルがあります。デバイス別の詳細分析は別のDevice Detailダッシュボードが担当します。





## ブロードキャスト

ダッシュボードは私たちのためにあります。他の人については？

ホートンが生成するすべての文字起こしは、着地した瞬間にMQTTブローカー（軽量のパブリッシュ/サブスクライブメッセージングバス）にも公開されます。2つのトピックに：

```
horton/transcriptions/<device-name>   ← 例: horton/transcriptions/kitchen
horton/transcriptions/all             ← ファンアウト：すべての文字起こし、すべてのデバイス
```

ペイロードは`device`、`mac`、`text`、`timestamp`を含む小さなJSONブロブです。パブリッシャー全体は1つのファイル（`lib/mqtt.py`）で、最初の使用時に遅延初期化され、単一の`MQTT_ENABLED=true`環境変数でゲートされています。ブローカーがダウンまたは到達不能な場合、公開は静かに失敗し、残りのパイプラインは動き続けます。いかなる文字起こしも下流のサブスクライバーでブロックされるべきではありません。

ダッシュボードは私たちが部屋を読む方法です。MQTTは他の何かがそれを読む方法です。文字起こしの何が興味深い使い方かを私たちが決めたくはありませんでした。

最初にサブスクライブしたのは**Overheard**というSlackボットでした。`horton/transcriptions/all`をリッスンし、聞いたことを取り上げ、Slackチャンネルに投稿します。しかし生のテキストをそのまま転送するのではありません。Overheardは各文字起こしを、次の部屋で起きたことの要点を耳打ちする同僚のような、短くドライで少し評論的な一行に変えます。`#transcript`や`#subtweet`のようなタグで投稿にラベルを付けます。チャンネルを読むのはフィードを見ているというよりも、見えない同居人からオフィスへのコメンタリーを受け取っているようです。



OverheardはホートンのLimitationではありません。その上のレイヤーです。それが要点です。私たちはオフィスが言うすべての言葉の生のフィードをSlackで見たくはありません。それはノイズが多く、少し気持ち悪くもあります。話されていることの*雰囲気*が欲しいのです。Overheardはその翻訳を提供し、ホートンの内部ではなく下流に存在します。ホートンは汎用的な文字起こしバスのままです。Overheardはそれを使って何をするかについての一つの意見です。

まだ構築していない多くのものが想像できます：1時間に1部屋につき1行を出力するサマライザー、キーワードで反応するHome Assistant統合、オフィスの非英語話者向けのライブ翻訳機。（誰かがいつか会議室をファクトチェックするLLMを接続するでしょう。やめておいた方が良いかもしれません。）

要点は、そうする必要がないということです。ホートンは公開し、世界はサブスクライブします。

## 細かいこと

独自のセクションを設けるほどではありませんが、実際に一緒に暮らすのが良いプロジェクトになるために積み重なった決断がいくつかあります。



**MACではなく名前。** 最初のバージョンはMACアドレスで音声を保存していました。`A4:CF:12:…`フォルダをスクロールして「キッチンのやつ」から録音を見つけようとした最初の瞬間、それが続かないとわかりました。今ではMAC鍵付きストレージ上にシンリンクの薄いレイヤーがあります：`kitchen/`、`desk/`、`meeting-room/`。デバイスの名前変更はデータを移動しません。どのシンリンクがそれを指すかを変えるだけです。人間は`kitchen`を読みたいのです。マシンはMACを使い続けられます。

**文字起こしバックエンドのホットスワップ。** Parakeet（NVIDIAのオープンな音声認識モデル）を試したとき、紙面上では速く聞こえましたが、スレッドセーフな方法で呼び出していなかったためすぐに爆発しました。正直な修正には1日かかっていたでしょう。ショートカットは`ASR_BACKEND=whisper|parakeet`環境変数で再デプロイなしにA/Bテストできるようにすることでした。それが結局正しい形だとわかりました。将来的には部屋によって異なるモデルが必要になるかもしれません。

**溶接ワイヤーで作ったホイップアンテナ。** ESP32-S3開発ボードのオンPCBチップアンテナはアクセスポイントと同じ部屋なら問題ありませんが、オフィス全体では問題があります。修正は恥ずかしいほどローテクでした：2.4GHzの4分の1波長、約31mmに切った溶接ワイヤーを垂直ホイップとしてボードにはんだ付けするだけです。安価で、少し見た目が悪く、劇的に改善しました。接続が「会議室で落ちる」から「オフィスの反対側でも維持できる」に変わりました。時に最適なアンテナはワークショップで見つけたワイヤーの切れ端です。



## 部品表

作ってみたい人のために。デバイス1台あたり — 約10ドルのパーツ（まとめ買いすればデバイスあたりさらに安く、通常はこの方法で売られています）、プラスオフィスにすでにあったもの：

| パーツ | 何 | 備考 |
|---|---|---|
| マイコン | **Supermini ESP32-S3開発ボード** | USB-C、小型フォームファクター。オンラインショップで安価（約5ドル）。 |
| マイクロフォン | **INMP441 I²S MEMSマイク** | ボードにはんだ付け。配線：`WS → GPIO 5`、`SCK → GPIO 6`、`SD → GPIO 7`、`VDD → 3V3`、`GND → GND`、`L/R → GND`（左チャンネルを選択）。 |
| アンテナ | **約31mmの溶接ワイヤー** | 4分の1波長ホイップとしてU.FLパッドにはんだ付け。同程度のゲージのソリッドコアホビーワイヤーでも動くかもしれません——長さが素材よりも重要です。 |
| 電源 | **USB-Cケーブルまたは5V壁面アダプター** | 小型ブロック型充電器で十分です。手元にあるものを使っています。壁面ブロックが古いUSB-A型の場合はUSB-A → USB-Cアダプターを追加。 |
| エンクロージャー | **3Dプリントのホートンゾウ** | オプションですが士気が上がります。 |
| その他 | **はんだ、フラックス、安定した手** | |

サーバー側：Python 3.12と`faster-whisper`を実行できるコンピューター。小さなフリートにはCPUで動きます。私たちは持っていたGPUボックス（RTX 4090）でmediumモデルを実行しています。Mac miniは数台のデバイスなら問題なく対応できます。デバイスごとのSQLiteはストレージを安価に保ちます。

## 今後の展開

現在のホートンは正直なところ素晴らしいおもちゃであり、そこそこ良い聴取所です。主に自分自身に聴いているマイクで満たされた部屋のとても良いダッシュボードがあります。

本当に興味深いのは文字起こしで何をするかです。部屋が聞こえているだけでなく、アドレスされているときに知るためのオンデバイスウェイクワード。部屋ごとのインテント、「kitchen」と「meeting-room」が聞いたことで何をすべきかについて異なるデフォルトの仮定を持つように。1時間の部屋を要約して読む価値のある正確に1行を出力する小型の社内モデル。それらすべてを試しています。

ESP32が転がっていてWhisperを実行できるコンピューターがあれば、これの最初のバージョンを週末で構築できます。ハードウェアは簡単な部分です。より難しい問いは、聴くべきフリートのデバイスが実際に何をすべきかです。

ホートンはそれまでの間、喜んで聴き続けます。

<!-- TODO before publish:
- [x] All images in place
- [x] Humanize pass applied
- [x] Verified: reason=2 (AUTH_EXPIRE) — corrected from 201 which is NO_AP_FOUND
- [x] Verified: VAD is server-side via faster-whisper's built-in filter (on-device VADCoreESP32.h was removed in the streaming rewrite)
- [x] Verified: env var is ASR_BACKEND (corrected from BACKEND)
- [x] Verified: OTA min version is v0.2.1
- [x] Glossed: VAD, IDS, MQTT, Diarization, I²S, OTA, RSSI, Parakeet, faster-whisper, SPA→single-page app
- [ ] Optional: a screenshot of the Whisper-hallucination transcripts (YouTube ghosts deserve a visual)
- [ ] Set draft: false when ready
-->


## Sitemap

Parent: [記事](https://2389.ai/ja/research/writing/index.md)

Related pages in this section:

- [AIパイプラインのための言語を作った理由](https://2389.ai/ja/research/writing/why-we-built-a-language-for-ai-pipelines/index.md)
- [Word Compiler：長編小説のためのコンテキストコンパイラ](https://2389.ai/ja/research/writing/word-compiler/index.md)
- [3DプリンターをAIポートレートアーティストに変えた話](https://2389.ai/ja/research/writing/we-turned-a-3d-printer-into-an-ai-portrait-artist/index.md)
- [シマー：自己研磨スキル](https://2389.ai/ja/research/writing/simmer-skill/index.md)
- [クックオフ：同じ仕様、異なるコード](https://2389.ai/ja/research/writing/cookoff-same-spec-different-code/index.md)
- [オマカセ：見せてくれ](https://2389.ai/ja/research/writing/omakase-show-me/index.md)
- [デリベレーション：答えではなく、視点を](https://2389.ai/ja/research/writing/deliberation-perspectives-not-answers/index.md)
- [ダークファクトリーは .dot ファイルだった](https://2389.ai/ja/research/writing/the-dark-factory-is-a-dot-file/index.md)
- [Week 0 NVIDIA DGX Spark 実験レポート](https://2389.ai/ja/research/writing/week-0-nvidia-dgx-spark-experiments/index.md)
- [ブレインダンプからブログ記事へ](https://2389.ai/ja/research/writing/brain-dump-to-blog-post/index.md)


Site index: [llms.txt](https://2389.ai/llms.txt) · [sitemap.md](https://2389.ai/sitemap.md) · [HTML](https://2389.ai/ja/research/writing/horton-hears-a-whisper/)
