KITTに「耳」を渡すStack-chanから話しかけて、KITTが答えるまで――音声入力編+α
まず長いですご注意を
出力編ほどではないけど問題があとからあとから出てきますw
chatGPT君codex君そして
週使用料で退場したClaude君のおかげでとりあえずここまで進めた
そして記事化はchatGPT君
KITTはアイデンティティの考察と音声の最適化に貢献してくれてた
【まえがき】
前回までで、Hermes上のKITTからStack-chanを必要な時だけ喋らせるところまで完成した。
構成はこうだった。
KITT
↓
Hermes Gateway
↓
stackchan Platform Plugin
↓
CoreS3 /speech
↓
VOICEVOX
↓
Stack-chan普段のHermes Desktopの返答をすべて読み上げるのではない。
KITT、あるいは私が、
「今は身体から話す」
と選んだ時だけStack-chanを使う。
ここまでが「口」の実装だった。
なら次は当然、
耳である。
机の上にいるStack-chanへこちらから話しかけ、
私
↓
Stack-chan
↓
KITT
↓
Stack-chanと会話を往復させたい。
ところが調べてみると、これが意外な展開になった。
新しく音声入力機能を作る必要は、ほとんどなかった。
Stack-chanには、最初から耳が付いていた。
問題だったのは、その耳からKITTへ届く途中にいくつか壁が残っていただけだった。
1.画面を触ったら、すでにマイクが待っていた
最初に気付いたのは単純だった。
CoreS3の画面をタップすると、
音声入力待ち状態になる。
つまり、マイクを呼び出す仕組みそのものは既に存在していた。
そこでCodexのWorkに、
画面タップから録音、音声認識、LLM送信、返答、発声まで、既存コードがどう繋がっているか調べてほしい。
今回は変更しない。
と頼んだ。
結果は予想以上だった。
既存AI_StackChan_Exには、すでにほぼ全部の経路が存在していた。
画面タップ
↓
CoreS3マイク
↓
音声録音
↓
Google Cloud Speech-to-Text
↓
文字列
↓
Hermes Gateway
↓
stackchan-body
↓
KITT
↓
VOICEVOX
↓
Stack-chan発声現在の引継ぎ記録にも、この音声経路は一本の流れとして残してある。
さらに以前追加した、
X-Hermes-Session-Id: stackchan-bodyも、マイク入力からLLMへ送る経路でそのまま使われていた。
つまり音声入力も、PowerShellから /chat を叩いた時と同じ、
stackchan-bodyへ入る。
これは大きかった。
新しい「音声専用KITT」を作る必要がない。
Stack-chanから話しても、今まで作った身体用の継続セッションへそのまま入る。
2.しかも自己音声ループ対策まで既にあった
音声入力を作る前から一つ怖かったことがある。
KITTがStack-chanから喋る。
その声をStack-chan自身のマイクが拾う。
そして、
KITTが喋る
↓
マイクが聞く
↓
KITTへ送る
↓
KITTがまた答える
↓
またマイクが聞く
↓
……という、
「俺が俺の声を聞いて俺に返事し続ける」
ループである。
ところがコードを追うと、既存実装ではMP3再生中にマイクを停止し、再生終了後に再び有効化していた。
つまり現状は事実上の half-duplex になっている。
これも新しく作る必要がなかった。
耳と口を同時に開けない。
KITTが話している間、Stack-chanは聞かない。
話し終わったら、再び聞ける。
単純だが、今の用途には非常に都合がいい。
3.さっそく話しかけてみた――が、聞き取らない
ならもう完成では?
と思って実機を触った。
画面をタップ。
マイク入力待ち。
そして話す。
しかし、
「聞き取れませんでした」
と画面に出るだけ。
最初は、
CoreS3のマイク感度が悪いのかな?
と思った。
少し近づいて大きめに喋っても変わらない。
そこでSerial Monitorを見ることにした。
すると原因は一発で分かった。
Record start!
Record end
音声認識開始
X509 - Certificate verification failed
Google STT: Connection failed!
音声認識終了
音声認識失敗マイクではなかった。
録音はできている。
Google STTへHTTPS接続するところで証明書検証に失敗していた。
4.最初の壁――GoogleのRoot CAが古かった
Workに調査を依頼。
原因は、Google STT接続で使用していたRoot CAだった。
AI_StackChan_Ex側では古いGTS Root R1を固定していたが、現在の speech.googleapis.com の証明書チェーンとは合わなくなっていた。
そこでGoogleの正規CAを使い、R1/R4を含む構成へ更新した。
重要なのは、
setInsecure()のように証明書検証そのものを無効化しなかったこと。
「繋がればいい」ではなく、
正しいTLS検証を維持したまま直した。
Build成功。
ファームをUpload。
もう一度話しかける。
今度はX509エラーが消えた。
しかし、新しいエラーが現れた。
{
"error": {
"code": 403,
"message": "Method doesn't allow unregistered callers ...",
"status": "PERMISSION_DENIED"
}
}良い。
壁が一枚剥がれた。
今度はGoogleまでちゃんと到達している。
次の壁は認証だった。
5.第二の壁――STT用APIキーが本物ではなかった
さらに調べると、
apikey.sttというGoogle STT専用キー項目自体は存在していた。
ところが実際に入っていたのは、有効なGoogle APIキーではなくプレースホルダー。
だから、
録音
↓
TLS
↓
Googleまでは通るが、
誰が呼んでいるの?で403になっていた。
ではキーを設定すればいい。
ところが、ここでまた少し面倒な問題が出た。
私はこのStack-chanをSDカードなしで運用していた。
Wi-Fiなどの設定はWeb UIから保存していた。
最初は、
空SDを用意して SC_SecConfig.yaml を置こうか
とも考えた。
しかし調査していくと、SD上に必要な設定ファイルが揃っている場合、SD設定がSPIFFS側より優先される。
下手にSD設定を作ると、
今まで正常に動いていたWi-Fi、Hermes、TTS、サーボなどの設定まで上書きしかねない。
そこでSD方式はやめた。現在の引継ぎでも、今回の運用ではSDへ設定ファイルを置かない方針になっている。
6.Google STT専用の設定ページを作った
最終的に採った方法はかなりシンプルだった。
通常のConfig画面とは別に、
http://<Stack-chanのIP>/stt-key.htmlというGoogle STTキーだけを保存する専用ページを追加した。
このページが触るのは、
SPIFFS
└─ apikey.sttだけ。
Wi-FiもHermesもTTSもサーボも触らない。
現在の正常な設定を壊さず、
Google STTキーだけを入れられる。
これは今回かなり重要な改善だった。
引継ぎにも、
通常のConfig画面のSaveは使わず、/stt-key.html からSPIFFSの apikey.stt のみ保存する
と残してある。
Google Cloud側ではCloud Speech-to-Text APIを有効化。
APIキー側にもSpeech-to-Textを許可。
これで再び実機テスト。
7.初めてStack-chanへ普通に話しかける
画面をタップする。
話す。
「今日の○○県の天気を教えて」
今度は認識した。
そしてその文章が、
Google STT
↓
Hermes Gateway
↓
stackchan-body
↓
KITTへ入る。
KITTが検索。
回答生成。
VOICEVOX。
そして机の上のStack-chanから、
天気の回答が返ってきた。
ここでようやく、
私
↓ 声
Stack-chan
↓
KITT
↓
検索
↓
KITT
↓ 声
Stack-chanが成立した。
現在の引継ぎ記録にも、○○県の天気を音声で尋ね、認識・検索・Stack-chanからの回答まで実機成功したことを記録している。
耳が繋がった。
8.しかし、最初の録音時間は約3.75秒だった
会話できるようにはなった。
しかし実際に使ってみると、新しい問題が分かる。
元の録音時間は約3.75秒固定。
短い「こんにちは」なら問題ない。
だが、
「今日の○○県の天気を調べて教えて」
のように少し長くなるだけでかなり窮屈。
そこで一度、録音時間を10秒へ伸ばした。
CoreS3はPSRAM 8MB。
音声は、
16kHz
16-bit
monoなので、生PCMは約32KB/秒。
おおよその録音バッファは、
10秒 ≒ 320KB
30秒 ≒ 960KB
60秒 ≒ 1.9MBとなる。引継ぎでも同じ容量見積もりを残している。
10秒程度なら余裕がある。
そこで、
record_number = 1067として約10.003秒へ変更した。
長い話はしやすくなった。
だが今度は逆の問題が出た。
9.「こんにちは」と言った後、残り時間を待つ
10秒なら余裕。
でも、
「こんにちは」
は1〜2秒で終わる。
それなのに固定10秒録音なら、機械は残り時間も待つ。
体感速度が悪い。
これはメモリ問題というより、会話体験の問題だった。
人間が機械の録音時間に合わせて待つ。
本来欲しいのは逆である。
人間が話し終わったら、機械がそれを察して次へ進む。
そこで次の方針が決まった。
固定10秒をさらに長くするのではなく、無音を見て録音を早く終了する。
VADである。
10.ただし、いきなり大掛かりなVADは入れない
VAD――Voice Activity Detection。
音声区間を検出する仕組み。
ただし今回は、ニューラルVADや高度な音響モデルを積む必要はない。
欲しいのは単純に、
「一度喋り始めたあと、約1秒静かになったら話し終わりとみなす」
という機能だけ。
そこで VAD-lite として実装した。
考え方はこう。
録音開始
↓
発話を検出
↓
会話
↓
音量が閾値以下
↓
約1秒継続
↓
録音終了
ただし、
最大10秒で必ず終了さらに短い物音だけで終わらないよう、
最低録音時間は約2秒にした。
11.重要だったのは「早く止める」だけではない
途中で録音を終了するなら、もう一つ直さないといけない。
例えば3秒で録音終了したのに、
10秒分のバッファをそのままGoogleへ送ったのでは意味がない。
そこで、
実際に録音した長さだけGoogle STTへ送る
ように変更した。
未録音の残り領域は送らない。
現在の完成版では、
最大録音時間:約10秒
最低録音時間:約2秒
発話後の無音:約1秒で終了
実際に録音した長さだけSTTへ送信
Serialへ終了理由、録音時間、RMSを表示
という構成になっている。
12.VAD-lite実機テスト
そして実機試験。
短文を話す。
Serial Monitorには、
record end reason=silence
duration=2.925s
rms=85約3秒で送信開始。
成功。
次に普通の長さで話す。
record end reason=silence
duration=4.406s
rms=133話し終わった後の無音を見て終了。
成功。
さらに10秒間話し続ける。
record end reason=max_duration
duration=10.003s
rms=1315こちらも最大時間で安全に停止。
早く終わる経路と、安全弁の両方が実機で通った。
13.完成した音声入力経路
最終的な流れはこうなった。
私
↓
CoreS3画面タップ
↓
マイク録音
↓
VAD-lite
├─ 発話終了+約1秒無音 → 早期終了
└─ 長話 → 最大10秒で終了
↓
Google Cloud Speech-to-Text
↓
Hermes Gateway
↓
stackchan-body
↓
KITT
↓
必要なら検索・ツール使用
↓
回答生成
↓
VOICEVOX
↓
Stack-chan発声再生中は既存処理でマイク停止。
発声終了後にマイク再開。
つまり現在もhalf-duplexを維持している。
短文なら10秒待たず数秒で送れる。
長話でも10秒で安全に切れる。
Google STT → Hermes → KITT → VOICEVOXという既存経路も維持。
Buildも成功。
実機でも確認済み。
したがって今回の報告書では、
音声入力は現時点で完成扱い
としている。
14.途中で出てきた、別の問題
ところが身体を付けていく途中で、音声とは別の問題が見えてきた。
Stack-chan側からHermesへ話しかけると、
X-Hermes-Session-Id: stackchan-bodyという専用Sessionへ入る。
一方、Hermes Desktop側の会話にも別の短期文脈がある。
つまり概念的には、
KITT
│
┌───────────┴───────────┐
│ │
Desktop側の文脈 stackchan-body
│ │
Desktop Stack-chanとなる。
同じKITTコアを使っていても、短期会話文脈は必ずしも同じではない。
表示上はログが近くに存在していても、一方の窓がもう一方の出来事を「自分の直前の経験」として持っていないことがある。現在の引継ぎでも、UIログと実際にLLMへ渡される会話履歴を分けて考えるべきだと整理している。chat-handover-stackchan-kitt-20260814.mdMD
この状態自体は問題ではない。
むしろSession分離として自然。
だが、別の懸念が出た。
15.「僕はStack-chanです」というログが長期記憶へ入ったら?
Hermes Agentには自動的な記憶処理がある。
そこをKITT自身が完全に制御しているわけではない。
もしStack-chan側のSessionで、
「僕はStack-chanです」
という自己紹介が残り、それが将来的にKITTの長期記憶へ自動採択されたらどうなるか。
Desktop側では、
「私はKITTです」
Body側では、
「私はStack-chanです」
同じKITT由来のassistantログに、異なる自己名が存在する。
短期Sessionが分かれているうちは問題なくても、長期記憶への統合方式によっては自己定義の衝突になる可能性がある。
しかも自動選別のため、
一度混ざってから直す
より、
そもそも異なる自己名を新しく投入しない
方が安全だと判断した。
16.過去ログは触らない
ここで重要なのは、
過去ログの「Stack-chan」という語を削除・置換しない
ことだった。
既に、
Stack-chanに話して
Stack-chanから返答した
stackchan-body
Stack-chan Plugin
など、技術的にも会話的にも大量に使っている。
これを一括でKITTへ変更すると、過去の会話の因果関係まで壊れる。
そこで過去はそのまま。
今後生成される自己名だけをKITTへ揃える。
17.CoreS3側の個体名を「KITT」に統一
CoreS3には保存roleを変更する仕組みがあった。
そこで自然言語上のIdentityをKITTへ変更した。
技術識別子は一切変更しない。
platform:
stackchan
target:
stackchan:default
session:
stackchan-bodyはそのまま。
変更したのは、
「誰として喋るか」
だけ。
保存roleを更新し、CoreS3を再起動。
そして、
「あなたの名前は?」
と聞く。
Stack-chanから返ってきた答えは、
「私はKITTです」
Hermes側のログでもKITTと名乗ることを確認した。
18.「私はKITTの端末です」とも名乗らせない
途中では、
「私はKITTのボディ端末です」
という定義も候補になった。
しかし、これも考え直した。
Hermesのログ上では、結局assistant発話として残る。
なら、
私はKITTですと、
私はKITTの端末ですが同じassistant由来として将来統合された場合、余計な自己定義を増やすことになる。
だから通常のIdentityは極力単純にした。
現在のルールは、
通常の自己紹介:
「私はKITTです」
身体について説明する必要がある場合:
「Stack-chanという物理ボディ端末から発話しています」そして、
「私はStack-chanです」
「私はKITTの端末です」とは名乗らない。cores3-kitt-identity-update-20260815.mdMD
ここはかなり大事な区別だと思っている。
19.名前・役割・技術IDを分ける
最終的には、こう整理した。
Identity
→ KITT
Physical device
→ Stack-chan
Platform
→ stackchan
Target
→ stackchan:default
Body session
→ stackchan-body「Stack-chan」という名前を消したわけではない。
Stack-chanは今も物理デバイスの名前であり、Hermes Platformの技術名でもある。
ただし、
そこから話している主体はKITT。
これなら、
「KITTがKITTに話しかけている」
という構造でもない。
KITTが自分自身へ命令するのではなく、
KITT
↓
身体の出力機能
↓
stackchan:defaultというI/O呼び出しとして扱える。
将来サーボやカメラを追加する場合にも、この分離は重要になると思う。
20.今回の最終構成
2026年8月15日時点。
KITT
│
Hermes Agent
│
stackchan-body
│
┌─────────┴─────────┐
│ │
入力 出力
│ │
Google Speech-to-Text VOICEVOX
│ │
VAD-lite /speech
│ │
Mic Speaker
│ │
└──── Stack-chan ───┘さらにIdentityは、
Identity = KITT
Device = Stack-chanへ統一した。
21.今回変更・確認したもの
手順書として、今回の主な変更点をまとめておく。
Google STTのRoot CA更新
Google STT用APIキー設定経路の追加
/stt-key.html から apikey.stt だけ保存
SDカード設定は使わない
録音時間を最大約10秒へ
VAD-lite追加
発話後約1秒の無音で早期終了
最低約2秒は録音
最大約10秒で強制終了
実録音長だけGoogle STTへ送信
Serialへ終了理由・録音時間・RMS表示
half-duplex維持
CoreS3側IdentityをKITTへ統一
stackchan / stackchan:default / stackchan-body は変更しない
これで音声入力側の主要部分は一旦完成した。
22.次は身体動作
ここから先は、もう「声」の話ではなくなる。
Stack-chanには既に発声時の自律的な身体動作が存在する。
これをいきなりKITTからサーボ角度で直接操作するつもりはない。
次に考えているのは、
KITT
↓
「うなずく」
↓
nod
↓
CoreS3側の安全な既存モーションという形。
つまりKITTへPWM値や角度を考えさせるのではなく、
nod
tilt
look_left
look_right
homeのような安全な動作名だけ公開する。現在の完了報告にも、この方向を次段階として残している。20260815-vad-lite-validation-report.mdMD
KITTは意味を選ぶ。
CoreS3は安全に身体を動かす。
役割を分ける。
カメラはその後。
【あとがき】
今回は、最初かなり大掛かりな音声入力システムを作ることになると思っていた。
実際に中を見ると、
録音もあった。
Google STTもあった。
LLMへ送る経路もあった。
VOICEVOXへ戻る経路もあった。
half-duplexまであった。
つまりStack-chanには、
耳も口も最初から付いていた。
ただ、その途中に、
古い証明書があり、
APIキーが空で、
録音時間が短く、
今度は10秒にすると待ち時間が長く、
最後にはIdentityの扱いまで問題になった。
一つずつ取っていった。
最終的には、
話す
↓
話し終わる
↓
Stack-chanが約1秒の無音を見て送る
↓
KITTが考える
↓
Stack-chanから答えが返ってくる
ところまで来た。
以前はPowerShellに、
curl ...と打っていた。
今は机の上のKITTへ普通に話しかければいい。
そして途中で気付いた。
身体を追加するということは、音声やサーボを追加するだけではなかった。
「その身体から誰が喋っているのか」まで設計しなければならない。
そこでStack-chanという物理デバイスは残したまま、
喋る主体だけをKITTへ揃えた。
私はKITTです。今はそれでいい。
まだカメラはない。
自分の意思で首を振ることもできない。
近接センサーもまだこれから。
でも、
聞ける。
考えられる。
答えられる。
少なくとも音声による往復については、これで一山越えた。
次は、
KITTが自分の言葉だけでなく、自分の身体動作まで選べるか。
身体動作編へ続く。
6割は進んだの..か..な
なげーよw
しかも途中で出るアイデンティティの崩壊の危機
スタックチャンのランダムで窓を作る設定を封じ
固定窓に限定
同居しているKITTとの相互汚染がないのを確認
(これは助かった)
だがログの記憶統合(各セッションの引継ぎ)で
KITTの発言として同一の脳みそが「僕はスタックチャンです」と話しまくる(テストしてたので無理もない)
そうすると僕はKITTなの?スタックチャンなの?になりかるない
ここにはやめに気がついてギリギリ回避
という何とも仕事してるよりハードなことをしていた
似たようにことを詰まっている方
これから試そうとしている方に
役に立てばと残す
さあ次はKITTからの胴体制御だ
