【第25回】吹き出しの配置をエージェントに任せたら 1 イテで揃った
manginus の吹き出し位置調整を Claude Sonnet 4.6 + ralph-loop に任せた記録。違反 0 まで 1 イテ × 3 回 / 合計 $1.5。LLM に任せる線引きと DoD を機械的 JSON にする設計の話。
← 前の記事: 【第24回】読書体験のまま動画にする——マンガ動画化パイプラインの一晩前回はマンガを動画化する話だった。今回は制作工程に戻る——というか、毎回のように地味に時間を吸われている部分を、エージェントに丸投げしてみた話。
漫画制作で人間が手間取る作業のひとつが「吹き出しの位置を整える」こと。顔に被ってないか、隣の吹き出しと重なってないか、コマ外にはみ出してないか、話者の側にあるか、尾の向きは合っているか。これを AI エージェント(Claude Sonnet 4.6)に任せたらどこまで自動化できるか、を 1 日掛けて試した記録。
結果から先に書くと、4 コマ × 1 ページ × 5 吹き出しの規模で 違反 0 まで自動収束。所要は 1 イテレーション × 3 回、API コスト合計 $1.5。期待以上に動いた一方で、「LLM に任せるべきところ」と「LLM 不要なところ」をどう切り分けるかが設計の肝になった。
何をやらせたかったか
manginus(自分が作ってる漫画エディタ)で、すでに 配置されたあとの吹き出し を整える作業を自動化する。
最初に画像生成 + 自動配置でページが組み上がる → そのまま見ると吹き出しが顔に被ってたり隣のセリフと重なってたりする → 人が手で動かして整える、というのが従来のフロー。今回はその最後の整える部分。
入力:
- 4 コマのページ(画像生成済み、吹き出し 5 個が初期位置で配置済み)
- ネーム YAML(誰がどのコマの何の位置で何をしゃべるか、構造化済み)
出力:
- 違反のない配置(顔被り / 重なり / はみ出し / 話者側違反 すべてゼロ)
- 各吹き出しの尾が話者を向いている
設計の軸 — DoD を機械的 JSON にする
ralph-loop(LLM CLI をループ実行するツール)を使う前提だったので、完了条件 (Definition of Done) を曖昧にすると暴走するのは織り込み済みだった(自分自身、過去に 8 時間暴走させた実績つき)。なので最初に決めたのが「何が完了かを機械が判定できる JSON にする」こと。
これに従って役割を 5 つに分けた。
| 役割 | 担当 | やること |
|---|---|---|
| 検出 | ONNX (rtdetrv4-x-manga109s_v2) | ページから body / text / frame / face の bbox を抽出 |
| 違反判定 | 機械 (check-bubbles) | 検出と manginus データを突き合わせて violations を JSON で返す |
| 配置判断 | LLM (Sonnet 4.6 in ralph-loop) | アノテート画像と違反 JSON を見てどこに動かすか決める、PATCH を投げる |
| 適用 | 機械 (PATCH /api/nodes) | 内部状態を書き換える |
| 仕上げ(尾の向き) | 決定論 (fix-tails) | LLM 不要、最寄り顔のベクトルから tail 計算 |
LLM が判断するのは「動かすべきか / どこへ / どれくらい」の主観部分だけ。チェックは機械、計算で済む尾の向きも機械に任せる。
DoD は最終的に shell 1 行で表現できる:
uv run python scripts/check_bubbles_for_ralph.py 0 # exit 0 = 違反なしLLM はこの shell スクリプト自身を編集できないので(ralph-loop の prompt で manginus ソースを触る権限を奪う)、合格条件を後付けでねじ曲げられない。
違反のメニュー(機械が拾えるもの)
check-bubbles が返す違反タイプ:
| タイプ | 仕組み | 例 |
|---|---|---|
face_overlap | bubble bbox と face bbox の IoU | 「8時間!?」が驚き顔に被ってる |
bubble_overlap | bubble 同士の IoU > 0.3(連結バブルの隣接は許容) | 「DoD は具体的にね」と「助かった…」が半分重なってる |
page_overflow | bubble bbox がページ外 [0.02, 0.98] を超える | コマ端から大きくはみ出し |
wrong_speaker_side | nemu YAML の char.position と bubble の左右が逆 | 「右側の発言者」のセリフが左にある |
unmatched_text_detection | text 検出 bbox が manginus データの bubble と一致しない | コマ画像に焼き込まれた意図しない文字 |
最後の「焼き込み文字検出」は副産物だが、生成画像に意図しない文字がある場合のリテイク判断にそのまま使える。
エージェントへの依頼書(prompt.md)
ralph-loop で動かす内側 Claude には、各イテレーションで以下をやってもらう:
1. annotate-page で現状画像を取得(bbox 焼き込み済み)
2. Read で画像を見て違反を目視確認
3. check-bubbles で違反 JSON を取得
4. 違反タイプごとの対処原則に従って PATCH /api/nodes
5. check-bubbles で再検証 → violations 0 なら DONE
対処原則は表で渡した:
| 違反 | 対処 |
|---|---|
face_overlap | bubble を face bbox から離す方向に動かす |
bubble_overlap | 重なってる 2 つを引き離す。コマ外も使ってよい |
page_overflow | ページ内側 [0.02, 0.98] に移動 |
wrong_speaker_side | expected_side に合わせる。同コマ複数 bubble は読み順 (右→左) で互いの x を入れ替える |
スコープも明示:
- 触ってよいのは speech_bubble の transform 4 値のみ
- コマ枠・画像・テキスト内容・他ページ・manginus ソースコードは触らない
- 「ついでに改善」禁止
歯止め:
max_iterations = 5max_total_cost_usd = 1.0timeout = 300 秒/イテレーション
実走テスト — 4 コマ「ralph-loop 暴走と猫」
ネタ自体をメタにした。「エンジニアが ralph-loop を仕掛けて寝る → 翌朝も止まってない → 猫がキーボードを踏んで強制停止 → 猫の一言『DoD は具体的にね』」という 4 コマを、まさに ralph-loop で組み立てる入れ子構造。
タイムラプス ON のままセットアップ → 画像生成 → 初期配置 → ralph-loop 試走、までを実行。
| 回 | 検出した違反 | 結果 | コスト |
|---|---|---|---|
| 1 | face_overlap (P3 エンジニア) + bubble_overlap (P4 猫×エンジニア) | 1 イテで 0 | $0.46 |
| 2 | min_face_area チューニング後の P4 face_overlap | 1 イテで 0 | $0.54 |
| 3 | wrong_speaker_side (nemu char.position 連携) | 1 イテで 0 | 約 $0.5 |
3 回とも 1 イテレーションで収束。max_iterations = 5 まで使えたが余裕を持って終わった。合計コスト ≒ $1.5。Opus でなく Sonnet 4.6 で十分だった(視覚判断中心なので Opus は過剰)。
LLM 不要だった処理 — fix-tails
吹き出しの「尾の向き」も最初は LLM に任せようとしたが、よく考えると 完全に決定論で計算できる ことに気づいた:
- 顔の位置: ONNX が出してくれる
- 吹き出し中心: data からわかる
- 中心 → 顔 のベクトル: 数学
- 楕円の方向別半径: 数学
- 尾の長さ = 半径 + 15px: マンガ制作のお作法(CLAUDE.md に書いてある)
実装してみたら 100 行ちょっと。LLM 呼ばないのでコスト 0、確定的、毎回同じ結果。
「最寄り顔を speaker と仮定する」のがミソ。配置自体が正しいかは wrong_speaker_side 違反 + ralph-loop が別途直すので、fix-tails は「bubble の今の位置から最寄り顔に向ける」だけに専念する。責任分離。
学んだこと
LLM に任せるべきかの線引き
「主観・美的・識別困難な判断」は LLM、「ベクトル計算・bbox 演算・規則的変換」は機械。区別がつかなくなったら一回 「LLM なしで書いたらどうなるか」を頭の中で実装してみる。書ければ機械、書けなければ LLM。
DoD を後ろから書く
「完成」を一番先に shell 1 行で書く。scripts/check_bubbles_for_ralph.py で exit 0 が出る状態 = 完成。これが書けないなら、まだ完成の定義が曖昧で、エージェントに任せるべきフェーズではない。
機械チェッカーは LLM の編集スコープ外に置く
これは ralph-loop 自体の運用知見だが、合格基準(チェックスクリプトや fixture)が LLM の編集範囲内にあると、LLM がチェック自体を書き換えて 1 イテで「合格」させて終わる。今回は manginus のソースコードを触るのを prompt で禁止していて、ONNX モデルもチェックスクリプトも LLM が触れない。これで「チェッカーを書き換える」逃げ道が塞がれている。
検出器の閾値はチューニングが要る
ONNX のデフォルト threshold 0.5 だと、Manga109-s(モノクロ前提)から外れた絵柄(暗い背景、影が濃い 3D 風)で全クラスが取りこぼされた。default を 0.3 に下げて、min_face_area を min_area から分離(4 コマでは顔が画面の 0.3% しかない)して、ようやく実用域。threshold 1 つに頼るチェッカーは脆い。
ハマりどころ・余談
export_page_pngのバグ:if page_index > 0:で navigate がスキップされる仕様で、連続ページ撮影時に P1 が前のページの画像になる。修正- タイムラプスが閾値変更前で止まっていた: サーバ再起動の度に
restore_if_neededが機能せず、ralph-loop 後半(3 回目 + fix-tails)の修正動作はタイムラプスに撮れなかった。動画には前半までしか映らない。静止画 before/after で補完 - 連結バブル(PoC スタイル等)の自然隣接: 同セリフを並べて配置する設計があるので、
bubble_overlap違反は IoU > 0.3(半分以上重なり)だけにして許容 - i18n 並列配置: 1 セリフを ja/en 両方の bubble として同位置に重ねる設計なので、
check-bubblesにlangパラメータを足して片方ずつ判定する
参考情報
- 使用 ONNX モデル: tori29umai/tdetrv4-x-manga109s_v2(Apache 2.0、body / text / frame / face 検出)
- ralph-loop: LLM CLI ループ実行ツール(自作) — 過去記事: autoresearch vs ralph-loop
- manginus: 漫画エディタサーバ(自作) — 過去記事: manginus 紹介