Yyatmita

【第25回】吹き出しの配置をエージェントに任せたら 1 イテで揃った

manginus の吹き出し位置調整を Claude Sonnet 4.6 + ralph-loop に任せた記録。違反 0 まで 1 イテ × 3 回 / 合計 $1.5。LLM に任せる線引きと DoD を機械的 JSON にする設計の話。

AIマンガ制作ワークフロー#ai-manga#manginus#ralph-loop#claude-sonnet#agent
← 前の記事: 【第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_overlapbubble bbox と face bbox の IoU「8時間!?」が驚き顔に被ってる
bubble_overlapbubble 同士の IoU > 0.3(連結バブルの隣接は許容)「DoD は具体的にね」と「助かった…」が半分重なってる
page_overflowbubble bbox がページ外 [0.02, 0.98] を超えるコマ端から大きくはみ出し
wrong_speaker_sidenemu YAML の char.position と bubble の左右が逆「右側の発言者」のセリフが左にある
unmatched_text_detectiontext 検出 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_overlapbubble を face bbox から離す方向に動かす
bubble_overlap重なってる 2 つを引き離す。コマ外も使ってよい
page_overflowページ内側 [0.02, 0.98] に移動
wrong_speaker_sideexpected_side に合わせる。同コマ複数 bubble は読み順 (右→左) で互いの x を入れ替える

スコープも明示:

  • 触ってよいのは speech_bubble の transform 4 値のみ
  • コマ枠・画像・テキスト内容・他ページ・manginus ソースコードは触らない
  • 「ついでに改善」禁止

歯止め:

  • max_iterations = 5
  • max_total_cost_usd = 1.0
  • timeout = 300 秒/イテレーション

実走テスト — 4 コマ「ralph-loop 暴走と猫」

ネタ自体をメタにした。「エンジニアが ralph-loop を仕掛けて寝る → 翌朝も止まってない → 猫がキーボードを踏んで強制停止 → 猫の一言『DoD は具体的にね』」という 4 コマを、まさに ralph-loop で組み立てる入れ子構造。

タイムラプス ON のままセットアップ → 画像生成 → 初期配置 → ralph-loop 試走、までを実行。

検出した違反結果コスト
1face_overlap (P3 エンジニア) + bubble_overlap (P4 猫×エンジニア)1 イテで 0$0.46
2min_face_area チューニング後の P4 face_overlap1 イテで 0$0.54
3wrong_speaker_side (nemu char.position 連携)1 イテで 0約 $0.5

3 回とも 1 イテレーションで収束max_iterations = 5 まで使えたが余裕を持って終わった。合計コスト ≒ $1.5。Opus でなく Sonnet 4.6 で十分だった(視覚判断中心なので Opus は過剰)。

ralph-loop 1 回目後のページ。顔被りと重なりが解消されている ralph-loop 2 回目後のページ。min_face_area チューニング後に発見された P4 の顔被りも解消 ralph-loop 3 回目後のページ。読み順違反が解消され、右の発言者の吹き出しが右側に来ている

LLM 不要だった処理 — fix-tails

吹き出しの「尾の向き」も最初は LLM に任せようとしたが、よく考えると 完全に決定論で計算できる ことに気づいた:

  • 顔の位置: ONNX が出してくれる
  • 吹き出し中心: data からわかる
  • 中心 → 顔 のベクトル: 数学
  • 楕円の方向別半径: 数学
  • 尾の長さ = 半径 + 15px: マンガ制作のお作法(CLAUDE.md に書いてある)

実装してみたら 100 行ちょっと。LLM 呼ばないのでコスト 0、確定的、毎回同じ結果。

「最寄り顔を speaker と仮定する」のがミソ。配置自体が正しいかは wrong_speaker_side 違反 + ralph-loop が別途直すので、fix-tails は「bubble の今の位置から最寄り顔に向ける」だけに専念する。責任分離

fix-tails 適用後。各吹き出しの尾が最寄りの話者に向いている

学んだこと

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_areamin_area から分離(4 コマでは顔が画面の 0.3% しかない)して、ようやく実用域。threshold 1 つに頼るチェッカーは脆い

ブッダイズム第3話のコマでの検出例。body / text / frame / face の bbox が描画されている

ハマりどころ・余談

  • 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-bubbleslang パラメータを足して片方ずつ判定する

参考情報