執筆者: кемо
最終更新: 2025/12/25
やあ、KEMOだよ。
突然ですが、家事面倒くさくないですか?面倒くさいですよね。
妻に頼みました。
「配偶者に家事を任せっぱなしにするのは時代錯誤だ」
とか言われそうですが許してください。
まず、そこら辺に転がっているラズパイ5を拾ってきます。
そして余っているミニPCも用意。
ここでAmazonで以下を注文
合計約4万円程度です。ブラックフライデーで買ったのでお安いね!
さて、本題です。用意したものでシステムを構築します。
だいたいこんな感じ(ぐちゃぐちゃmermaid)

頑張って書いたんですけど、クソみたいなレイアウトはどうしようもありませんでした。許して。
もうちょっと簡略化して、頑張ってGoogleSlideで作ったのがこれです。

うーん、それなり。及第点でしょう。
まずはラズパイにHomeAssistantOS(以下HAOS)を入れます。これは名前の通り、スマートホーム用のOSで、これを入れたPCをハブとして扱うことができます。
こいつの優秀な点は他のユーザが作った便利なアドオンを入れられることです。例えば、毎回SSHするのはめんどくさいのでWebSSHというアドオンを入れておく、こうすればダッシュボードからコンソールをいじることができます。
いろいろ接続したHAのダッシュボード

はい、これでHAOSの準備はできました。
でも悲しいことに妻は何もしてくれません。
とりあえず、「いってらっしゃい」と「おかえり」を言ってほしいですね…
つまり、私の外出と帰宅をなんとか知らせたい。
じゃあスマホの位置によって外出と帰宅を検知しましょう。
自宅のエリアにスマホがあれば在宅、なければ不在として、この2つの状態の行き来の際に、声をかけてくれるようにすればよさそうです。
ここで問題発生。妻は自宅にいます。同じWi-Fiに繋がっている間は位置を共有できますが、私が外出した場合、スマホと自宅Wi-Fiの通信はなくなります。こうなっては、妻は私の位置を把握できません。ここは簡易的なVPNでつなぐことにしました。TailScaleをスマホとサーバーにいれます。これに接続すればVPNを使えるようになるので万事解決。ExitNodeを切ったりしないと普通の通信ができなくなります(なぜかMagicDNSも動かない…)。
VPNで常に自宅にいる妻と通信できるようになりました。位置情報だけでなく、支持も送れたり、センシング情報も見れたりするので、外出先からでも自宅に指示を飛ばせます。
本題を忘れてしまうところでした。では私の位置情報をトリガーにして挨拶をするようにします。
はい。

かわいい。
挨拶ができるシステムは掃除もできる!
なので掃除してもらいます。ロボット掃除機を部屋にポン置きして、それをHAOSから操作するだけ。
せっかくなので自動化しましょう。私が日中に外出した場合、自動で掃除を始めたらよさそうです。さっき、「いってらっしゃい」を言ってくれるようにしたので、そこにロボット掃除機を起動するコマンドを追加します。

いい感じ。
余談ですが、今回購入したロボット掃除機のSwitchBot K10はコンパクトかつ(比較的)低価格なので、学生の皆様におすすめです。
妻は挨拶も掃除もしてくれますが、帰ってきても部屋は寒いままです。
ぬくもりが欲しいですよね。
なので、エアコンをつけてもらいましょう。エアコンの赤外線信号を読み取らせて、スマートリモコンに登録します。これをHAOSから操作することによって実現できそうです。
今回はSwitchBot製品としての統合ではなく、Matterとしての統合を行います。Matterとは、メーカーやOSの垣根を超えてあらゆるスマート家電を繋ぐために作られた通信規格です。このロゴがついた製品であれば、AppleやGoogle、Amazonなどどの会社のスマートスピーカーやスマホからでも、相性を気にせず自由に操作できるようになります。
ということで、登録しました。
先ほどとは少し変えて、自宅から少し離れた場所を通過したときに、エアコンをつけるようにしましょう。自宅に着いた瞬間につけても寒いですからね。

さすが。
これだけのことしてくれている妻ですが、返事がありません…
なんとか私の声を認識してほしいですね。
ということで、準備したスピーカーフォンの出番です。
OpenWakeWordでウェイクワード(OK,GoogleやAlexaみたいなの)を検出するようにしましょう。OpenWakeWordのカスタムモデルは公式が用意している、このGoogleColabNotebookで学習できます。簡単。
今回はとりあえず「hazuki」と呼びかけるようにしました。
学習方法は認識させたい単語を入れるだけ。ただし、英語音声でしか作れないので「ha zoo_key」とすることで日本語発音に近づけるという方法を使います。
短い単語だとうまく認識できないので、頭に「OK」などを付けると効果的。
正直、精度はあんまよくないです。日本語モデルを作る記事も書くかもしれません。
やっぱり夫婦の会話って大事だと思うんですよ。
というか、この記事のメインはこれです。本来、一般的なスマートホーム機器は簡易的な意味の分析(「Alexa, 電気付けて」みたいな)をして動作するのですが、この処理をLLMに置き換えます。言ってしまえば、ただの非効率なリソースの無駄遣いなのですが、最大のメリットとして雑談ができます。
作ったのは意味分析とチャット機能、あとLLMに渡すツールです。
まず、マイクがウェイクワードを認識するとSTT(HAOS上のWhisper)を開始します。そして、そのままテキストをミニPCに送ります。
テキストの意味分析後にツールコールしてMQTTにHAOSに対する指示を投げます。
それを読み取ったHAOSが接続された機器に対する操作を行うという流れになっています。
チャット機能を使う場合はMQTTに投げる代わりに、チャットレスポンスを生成して、それをTTS(ミニPC上のCoeiroinkエンジン)で音声に変換。これをHAOSに直接投げ返して、スピーカで再生します。
APIはOpenAI API準拠で作ります。理由は後ほど解説します。
ヘルスチェックとチャットさえあればいいでしょう。

肝心のLLMですが、ミニPC上で3Bの軽量モデルを動かしています。ここで外部API使うのは甘えじゃないですか?
考えてみてください。OpenAIなんて後どれくらいもつかわかりません。AnthropicやGoogleだってそうです。リージョン・アカウントBANや利用料金の大幅な上昇も考えられます。たとえ企業がやらかさなくても、昨今の状況を加味すると、規制の強化でLLMのチャット能力(特に今回のような用途)が低下する or BANの対象になることも想定されます。
こんなやつらに妻を任せていいんですか???
コードはここで公開しています。
やっぱ品質が問題なんですよね、3Bモデルだと結構きついです。そのため甘えGeminiフォールバック機能がついていますが、無料枠減少のせいでまともに使えません。この問題を解決するために大学か部のリソースに専用のLLMサーバを寄生させることも考えましたが、さすがに怒られそうなのでやめました。
さて、実はここで少し問題が発生します。HAOSはなんとカスタムAIアシスタントの構築をサポートしているので、ウェイクワード→STT→アシスタント→TTSの流れを簡単に構築できるようになっています。
しかし、このアシスタント部分を自作LLMサービスに置き換える方法がデフォでは存在しません。通常はOpenAI統合を用いてOpenAI APIを使用するのですが、これをなんとか置き換えたい。仕方ないので外部アドオンを使用することのできるHACSという拡張を入れます。そして、そこから拡張版OpenAI統合を落としてきて利用します。アシスタントのベースURLを自作サーバのものに変えれば万事解決です。
実際に動いている様子を載せられないのが悔しい。仕方ないのでデバッグモードでの通常チャットの様子を貼ります。

メイドキャラになってるのは公開用です。流石にそのままは晒せません。
しゃべったと言っても文面です、私しかしゃべってません。
これは悲しいので、声を付けましょう。
まあGoogleTTS等の棒読みっぽい声なら簡単につけれます。なんたってHAOSが普通にサポートしてますからね。でもせっかくならかわいい声で話してほしいよ…
代表的なTTSってなんでしょう?かわいい声といったらVOICEVOXですが、あれは使われすぎてて正直声のイメージが固定されてるんですよね。なんて名乗ろうと、どんな口調だろうと、春日部つむぎは春日部つむぎなんですよ。
COEIRINKを使いましょう。これなら配布モデルとかも簡単につかえるし、比較的にリソースの消費が少なかった気がします。公式からGUI版をとってきて中身のエンジンのみを動かして、それに処理を投げます。都合のいいことにHACSにVOICEVOX TTS統合も落ちていたのでね。これは楽そうですね、COEIROINKはVOICEVOXみたいなもんやし。
…
そんなうまくいかないんだなあ、これが。VOICEVOXとCOEIROINKではAPIの仕様が割と違います(v2から互換がなくなった)。そのためこれをつなぐ何かが必要なんですね。仕方ないので、アドオンのコードを書き換えます。といってもリクエストを書き直しただけです。気が向いたら公開するかも。
# coeiroink向け調整
try:
async with aiohttp.ClientSession() as session:
async with session.post(f"{url}/v1/style_id_to_speaker_meta", params={"styleId": int(speaker)}) as meta_res:
if meta_res.status != 200:
_LOGGER.error(f"Failed to fetch speakerUuid for styleId {speaker}")
return (None, None)
meta_data = await meta_res.json()
speaker_uuid = meta_data["speakerUuid"]
predict_json = {
"speakerUuid": speaker_uuid,
"styleId": int(speaker),
"text": message,
"speedScale": float(speed),
"volumeScale": float(volume),
"pitchScale": float(pitch),
"prePhonemeLength": 0.1,
"postPhonemeLength": 0.1,
"outputSamplingRate": 24000
}
_LOGGER.debug(f"Sending predict request to COEIROINK: {predict_json}")
async with session.post(f"{url}/v1/predict", json=predict_json) as response:
if response.status != 200:
error_detail = await response.text()
_LOGGER.error(f"Predict failed with status {response.status}: {error_detail}")
return (None, None)
data = await response.read()
_LOGGER.debug("Audio data (WAV) received directly from predict")
return ("wav", data)
except Exception as err:
_LOGGER.error(f"Unexpected error in TTS request: {err}")
return (None, None)残念ながら問題がもう一つ残っています。エンジンがローカルホスト(127.0.0.1)で受け付けているので、HAOSからAPIを叩けません。これはsocatで迂回させることによって解決します。要は、特定ポートに飛んできたリクエストを、ローカルホストの特定ポートへのリクエストへ回します。
socat TCP-LISTEN:{PORT},fork,reuseaddr TCP:127.0.0.1:{PORT}ここまでした価値のある可愛さです。エンジンはミニPCに積んでるので、そっちで適当に切り替えるだけで声はいくらでも変えられます。
拡張性は高めだと思います
一応、
あたりを計画しています。
最重要計画はアバターの表示です、今は妻の姿が見えなくて悲しい。クソデカディスプレイを買う準備ができてないので仕方ないね。
というか本来、アバター表示とアプリ統合をメインにする予定でした。しかし、なぜか予定が増え続けているため、すべてが後ろ倒しになっています。
やったら記事出しますね。
これ現実に存在するコンテンツなんですね。
実際に人同士でやっている方々がいるのか…
今日(投稿日)はクリスマスらしいです。
