はじめに
M4 Mac Mini(メモリ16GB)でローカルLLMを動かす記事を以前書きました。今回はその画像版です。
16GBのMacで、ローカルの画像生成はどこまでできるのか。試してみたら、アニメ絵も実写も1枚2分ほどで普通に出ました。
ただ、「プロンプトを打って絵を出す」だけだと、すぐ飽きます。そこで、家で動いている別の仕組みとつないでみました。自宅にはADS-B(航空機が出している位置情報の電波)の受信機があって、毎日1,400〜1,600件の受信記録がたまっています。これを「種」にすれば、誰にも予測できない絵を毎日自動で作れるのでは、と考えたのがこのbotの始まりです。
ADS-Bとは
ADS-B(Automatic Dependent Surveillance–Broadcast)は、航空機が自分の位置・高度・速度・便名などを、1090MHzの電波で周りに向けて定期的に送信している仕組みです。暗号化されていないので、市販のUSBのSDRドングルとアンテナがあれば、家でも受信できます。Flightradar24のような飛行機追跡サイトも、世界中の個人が受信したこのデータを集めて作られています。
できあがったのは、毎日「実在しない恋愛ADV(ギャルゲー)」のイベントCGを1枚作り、架空のタイトル・ヒロイン名・台詞をつけて、Blueskyに自動で投稿するbotです。1枚ごとに、別々の存在しないゲームになります。
本記事では、その仕組みと、作る途中でハマったところを書いていきます。
全体の構成
家の3台のマシンと、Blueskyをつないでいます。
- ADS-B受信サーバー(ARM / Ubuntu):ADS-Bの受信機をつないだ小さなARMのLinux機。前の日のデータを集計して、要約だけを返します
- 司令塔(x86のUbuntu 26.04機):天気と曜日、ADS-Bの要約から絵の中身を決めて、Mac Miniに生成を頼み、できた絵に文章をつけて投稿します
- M4 Mac Mini:画像生成の担当。できた絵を確認するWeb画面もここで動いています
1. 画像生成環境の構築(M4 Mac Mini)
動かしている環境
- Python の diffusers(0.40.0)+ PyTorch(2.13.0)で、Apple SiliconのGPU(MPS)を使って生成しています
- 解像度は 1216×832、ステップ数は 28
- 司令塔のUbuntu機から、sshでMac Miniの生成スクリプトを呼び出す形です
- Mac Mini側には、できた絵を確認するための Flask製のWeb画面も置いています(家のLANとTailscaleからしか見られない設定です)
使っているモデル
| 用途 | モデル | プロンプトの書き方 |
|---|---|---|
| アニメ絵 | NoobAI-XL | Danbooruタグ形式(1girl, solo, ...) |
| 実写 | RealVisXL V5.0 | 普通の英文 |
botで使っているのはNoobAI-XLのほうです。16GBのM4 Mac Miniで、1枚あたり約2分(実測で約125秒)かかります。アニメ絵でも実写でも、速さはほぼ同じでした。
このbotが作るのは、オリジナルのキャラクターだけです。NoobAI-XLは既存作品のキャラクターにも強いモデルですが、botでは版権キャラクターを扱いません。
無人で回すための「絶対に外さない」設定
人が毎回チェックしない無人のbotなので、絵が危ない方向に行かないように、プロンプトの先頭とネガティブプロンプトに、固定の指定を必ず入れています。
本文の先頭 original, 1girl, solo, mature female, ...
ネガの先頭 child, loli, young girl, teenager, schoolgirl
ネガに常に nsfw, cleavage, upskirt, see-through
- 登場人物は成人女性に固定しています
- 煽情的な要素は、ネガティブプロンプトで常に閉じています
- ここを固定したことで、「毎回人が見て判断しないといけない部分」がなくなり、自動で投稿してよい、という判断ができました
【ハマった壁】後ろに書いた指定が、黙って消えていた
Stable Diffusion系のモデルは、プロンプトを 77トークンまでしか読みません。超えた分は、エラーも出さずに後ろから黙って切り捨てられます。
最初の版では、「晴れ」を表すのに clear sky, sunlight, blue sky の3語を使っていました。そのせいで枠を食いつぶし、後ろにあった「場所(教室)」がプロンプトから消えていました。絵には教室が描かれていないのに、生成結果の通知(ログ)には「教室」と出る、という食い違いが起きていたのです。
直し方はこうしました。
- 1つの項目に使う語は、1〜2語に圧縮する
- 大事な項目(構図など)を前のほうに並べる
- それでも切り捨てが起きたら、生成結果の通知に「※切り捨て:〜」と出して気づけるようにする
今は全体で22タグ、実測で73トークンに収めています。
2. 「毎日違う絵」をどう決めるか
ここがこのbotの中心です。
「読める軸」と「読めない軸」を混ぜる
全部ランダムにすると、ただのガチャになって、毎日見る理由がなくなります。逆に、全部決まったルールで作ると、明日の絵が予想できてしまいます。
そこで、絵の要素を2種類に分けました。
読める軸(見る人が「なるほど」と読み解ける)
| 要素 | 決め方 | 例 |
|---|---|---|
| 髪の色 | 曜日 | 月=銀、火=赤、水=青、木=緑、金=金、土=茶、日=橙 |
| 髪の長さ | 日付 | 月の初めは短く、月末に向けて伸びていく |
| キャラの性格 | その日の天気 | 晴れ=幼なじみ、曇り=無口な後輩、雨=図書委員、雪=ミステリアス、雷=姉御肌 |
| 服の厚さ | その日の最高気温 | 寒い日はコート、暑い日はノースリーブ |
読めない軸(誰にも予測できない)
髪型、目、表情、装飾(アクセサリー)、小物、場所、時間帯、構図、光。候補の数を掛け合わせると、この組み合わせだけで約4,400万通りあります。
「今日は水曜だから髪が青いのか」と読み解けて、それ以外は毎日予想がつかない。この2つを混ぜることで、毎日見る楽しみが出るようにしました。
※1日に3回生成しているので、実データがそのまま使われるのは朝の1枚目です。2枚目・3枚目は、同じ日でも別人になるように、読める軸の表を1つずつずらしています。
読めない軸の「種」に、前の日の飛行機を使う
読めない軸を選ぶための「種」には、前の日に自宅の上空を通った航空機のADS-Bデータを使っています。
前の日の全受信記録(1日1,400〜1,600件)の「機体番号・速度・進行方向・電波の強さ」をつなげて、SHA-256でハッシュにします。飛行機の飛び方は毎日違うので、この値は誰にも予測できません。
# 前日の全受信記録から、機体番号・速度・進行方向・電波強度をつなげる
hexes = []
for record in archive:
hexes.append(record["hex"] + str(record["gs"]) + str(record["track"]) + str(record["rssi"]))
digest = hashlib.sha256("|".join(hexes).encode()).hexdigest()
集計はADS-B受信サーバー側で行い、司令塔は要約(ハッシュと件数)だけを受け取ります。こうしておくと、司令塔が受信サーバーのファイルを直接読みに行かなくて済み、壊れにくくなります。
ハッシュの値は、4文字ずつ区切って数字にして、候補の表から1つずつ選ぶのに使います。
class Picker:
"""16進文字列を4文字ずつ消費して、決定論的に選ぶ。"""
def __init__(self, digest: str):
self.d, self.i = digest, 0
def pick(self, seq):
if self.i + 4 > len(self.d): # 使い切ったら伸ばす
self.d += hashlib.sha256(self.d.encode()).hexdigest()
v = int(self.d[self.i:self.i + 4], 16)
self.i += 4
return seq[v % len(seq)]
データが取れない日のための切り替え
受信機が止まっている日もあるので、種の取り方は段階的に切り替わるようにしています。
- 前の日のADS-Bの記録(ふだんはこれ)
- その時点で受信している機体(前日の記録が無い日の保険)
- NISTの公開乱数(Randomness Beacon):60秒ごとに更新される公開の乱数
- 日付のSHA-256:全部だめなときの最後の手段
【ハマった壁】気づかないまま、予備の種で動いていた
この記事を書いている途中で、ボットがしばらくの間(おそらく1週間ほど)、ADS-Bのデータを使えていなかったことに気づきました。
原因は2つ重なっていました。
- 受信サーバーが、同じLANに無線と有線の2本でつながっていて、無線側のアドレスではsshがつながったあとに固まる状態になっていた
- 家のNASを整理したときに、前日の記録の置き場所が変わっていたのに、ボットは古い場所を見たままだった
切り替えの仕組みはちゃんと動いていたので、ボットは黙って予備の種(NISTの公開乱数など)に落ち、絵は毎日ふつうに出続けていました。見た目では、まったく気づけません。
「どの種を使ったか」を毎回わかる形で残していなかったのが、一番の反省点です。切り替えを用意するなら、どの段に落ちたかがわかる記録も一緒に用意しておくべきでした。
レアな日:軍用機が飛んだ日
前の日に軍用機を受信した日だけ、絵にミリタリー系の要素(軍服、迷彩、パイロットサングラス)が入ります。軍用機かどうかは、受信機側にある軍用機のリストと照らし合わせて判定しています。
ADS-Bのデータは、組み合わせ次第で受信している場所が推測できてしまいます。本記事では、受信地点や、機体番号と日付の組み合わせは載せていません。
……と言いつつ、せっかくなので、これまでに捕まえた軍用機を、機種だけ抜粋しておきます。
| 機種 | どんな機体か |
|---|---|
| Boeing KC-135R | 空中給油機 |
| Lockheed KC-130J Hercules | 空中給油・輸送機 |
| Lockheed C-5M Galaxy | 超大型の輸送機 |
| Boeing C-17 Globemaster III | 大型の輸送機 |
| Boeing P-8 Poseidon | 哨戒機 |
| Cessna UC-35D Citation | 連絡・輸送用のビジネスジェット |
| Beechcraft UC-12 / C-12 Huron | 連絡・輸送用の双発機 |
このどれかが前の日に飛んでいると、その日の絵にミリタリー要素が混ざります。
実際にできた絵の例
朝の1枚目(実データがそのまま使われる回)から、3つ並べてみます。プロンプトは一部省略しています。
例1:2026年9月17日(木)晴れ・前日に軍用機1機
| 読める軸 | 値 |
|---|---|
| 髪の色(曜日) | 木曜 → 緑 |
| キャラ(天気) | 晴れ → 幼なじみ |
| 前日の軍用機 | 1機 → ミリタリー要素あり |
original, 1girl, solo, mature female, japanese, slender, matte skin, cowboy shot, from below,
green hair, medium hair, grey eyes, military uniform, camouflage, aviator sunglasses, cardigan,
smile, smug, short sleeves, train station, morning
『サバイバルゲームズ・ラブコール ~迷彩は脱げない~』
汀 蒼衣(サバゲーサークル現場指導係)
【更衣室裏で笑いを堪える】
「変な顔で撃たれたの、まだ根に持ってるんだから」
軍用機の日の「迷彩」を、Sonnetが「サバゲーサークル」に読み替えて物語にしています。ちなみにこの回は、1章の77トークン制限で「晴れ」を表す clear sky, sunlight が切り捨てられていました。
例2:2026年9月19日(土)雷
| 読める軸 | 値 |
|---|---|
| 髪の色(曜日) | 土曜 → 茶 |
| キャラ(天気) | 雷 → 姉御肌 |
original, 1girl, solo, mature female, japanese, slender, matte skin, upper body, looking away,
brown hair, long hair, heterochromia, jacket, grin, short sleeves, park, outdoors, morning,
thunderstorm, lightning, half updo
『雷鳴のクラリス —Thunder Whisper—』
九条 灼(気象観測部・部長)
【屋上、嵐の前の宣戦布告】
「怖がらせたいんじゃない、あんたにだけは本気見せたいの」
例3:2026年9月20日(日)くもり
| 読める軸 | 値 |
|---|---|
| 髪の色(曜日) | 日曜 → 橙 |
| キャラ(天気) | くもり → 無口な後輩 |
original, 1girl, solo, mature female, japanese, slender, matte skin, cowboy shot, looking at viewer,
orange hair, long hair, amber eyes, hoodie, expressionless, laughing, short sleeves,
seaside, ocean, sunset, golden hour, overcast
『渚のディスタンス、それから』
汐見 茜(幼馴染・元水泳部)
【夕陽と、言えない一言】
「あの日渡せなかった言葉、まだこの胸で膨らんでる」
キャラの表では「無口な後輩」なのに、Sonnetは絵を見て「幼なじみ」と書いています。絵そのものを見せて書かせると、表の設定より絵の印象が優先される、という例です。
3. 文章はClaude(Sonnet)に、絵を見せて書かせる
絵の中身は、上の表から機械的に決めています。Claudeに絵のプロンプトを書かせているわけではありません。
Claudeの仕事は、できあがった絵を見て、その絵が「どんな存在しないゲームのワンシーンか」を考えることです。
- ゲームのタイトル
- ヒロインの名前
- イベント名
- ヒロインの台詞
呼び出しは、Claude Code(Anthropic のコマンドラインツール)の claude -p --model sonnet で、画像を渡して書かせています。
【ハマった壁】タグを読ませると、絵とズレる
最初は、絵を作るときに使ったタグ(grin など)を読ませて文章を書かせていました。ところが、タグは笑顔なのに、実際の絵は泣き笑い、ということが起きます。画像生成モデルは、タグどおりの絵を出すとは限らないからです。
そこで、絵そのものを見せて書かせる方式に変えました。これで絵と文章が合うようになり、さらに、泣き笑いの絵に「幼なじみ、転校前日」という設定を足して物語にしてくる、といったことも起きるようになりました。
【ハマった壁】Haikuだと「作業報告」になった
安く済ませたくて、最初はHaikuで試しました。結果はこうでした。
| モデル | お願いしたこと | 結果 |
|---|---|---|
| Haiku | 短い単一の出力 | ✅ |
| Haiku | ギャルゲー4本分の起草 | ❌ 作業報告だけで、本文が無い |
| Sonnet | 画像を見て5本分の起草 | ✅ 質もはっきり上 |
短い単一の出力ならHaiku、形式の決まった創作はSonnet、というのが結論です。モデルに関わらず、形式が崩れたときのやり直し(2回まで)は入れてあります。それでもだめな絵は、投稿の順番待ちに入らず、次の絵に進みます。
【ハマった壁】「〜するな」と書いても守られない
「説明的な台詞にしないで」と指示しても、守られませんでした。
- ×「全サーバーが同時に落ちてる……あり得ない」
悪い例と良い例を並べて見せたら、直りました。
- ○「このケーブル……あなたにしか繋げられない」
タイトルやヒロイン名は、すべてClaudeが考えた架空のものです。実在する作品との一致は意図していません。
4. Blueskyへの自動投稿と安全装置
なぜXではなくBlueskyなのか
X(旧Twitter)のAPIは、2026年9月時点では、サブスクリプションではなく「クレジットを買って、使った分だけ払う」方式です。投稿1件ごとに料金がかかります(公式の料金表では、投稿1件 $0.015、URLを含む投稿は $0.20)。
Blueskyは、APIを無料で使えて、公式ドキュメントにbotを作るためのテンプレートまで用意されています。趣味のbotを気軽に動かすなら、Blueskyのほうが向いていました。
投稿のペース
- 生成は1日3回(同じ日でも別人になる仕組みは2章のとおり)
- 投稿は1日最大4回
- 作りおき(投稿待ち)が10枚を切ると、自動で1日2回に減速します
AIが作っていることは、アカウントのプロフィール欄で明示しています。
無人botの安全装置
無人で公開する以上、「止める手段」をいくつも用意しました。
- 投稿前に外せる:Mac MiniのWeb画面で、🗑(削除)か🔧(要修正)を付けた絵は、投稿の順番待ちから外れます。朝に見て、嫌なものを消すだけでいい作りです
- 🔧の絵は自動で作り直し:同じプロンプトで、別のseedで焼き直します
- 止めるスイッチ:決まったファイルを1つ置くだけで、投稿が止まります
【ハマった壁】同じ日に同じ絵が出た
種を「前の日のADS-B」で決めているので、同じ日のうちに何回生成しても、全部同じ絵になっていました。生成するたびに違う値(ソルト)を足すようにして直しました。
salt = a.salt if a.salt is not None else now.strftime("%H%M")
digest = hashlib.sha256((digest + "|" + salt).encode()).hexdigest()
【ハマった壁】二重投稿になりかけた
「投稿してから、投稿済みと記録する」順番だと、その間に処理が落ちたとき、次の回にもう一度同じ絵を投稿してしまいます。
そこで、先に「投稿済み」と予約してから投稿し、失敗したら予約を取り消す順番に変えました。公開するものは、「二重に出る」より「1回飛ばす」ほうに倒したほうが安全です。
【ハマった壁】ハッシュタグがリンクにならない
Blueskyでは、本文に #存在しないギャルゲー と書くだけでは、ハッシュタグとして認識されません。APIで投稿するときは、どこからどこまでがハッシュタグかを、facet(app.bsky.richtext.facet#tag)として別に指定する必要があります。位置は文字数ではなくUTF-8のバイト位置で指定するので、日本語を使うときは注意が必要です。
5. できあがったアカウント
こうしてできたのが、このアカウントです。
- 存在しないギャルゲー(Bluesky):https://bsky.app/profile/sonzaishinai-galge.bsky.social
- ハッシュタグ:
#存在しないギャルゲー
投稿の例
『陽だまりドア、また明日』
神代 陽菜(幼なじみ・カフェ店員見習い)
【両手いっぱいの帰り道】
「手、塞がってるから……今だけ、肩貸してよね。」
毎日、どこにも存在しないゲームのワンシーンが流れてきます。よければのぞいてみてください。
まとめ
- 16GBのM4 Mac Miniでも、ローカルの画像生成は1枚2分ほどで普通に使えます
- 全部ランダムにするより、「読める軸」と「読めない軸」を混ぜたほうが、毎日見る楽しみが出ます
- 予測できない「種」として、家にあるADS-B受信機のデータが使えました。取れない日のための切り替えと、どの段に落ちたかがわかる記録をセットで用意しておくと安心です
- 文章は、タグではなく絵そのものを見せて書かせると、ズレません
- 無人で公開するbotは、「止める手段」と「二重に出さない順番」を先に作っておくのが大事です
家にある別々の仕組みをつなぐと、それぞれ単体ではできなかったものができて、おもしろいです。
更新履歴
- 2026-09-27:初版投稿