"""Assemble a FLUX image prompt for one panel, and a deterministic seed. Character consistency is the hard part of a multi-panel comic: FLUX renders every panel independently, so the only lever we have to keep a character looking the same across 20 images is to feed its FIXED visual description (from the bible) verbatim into every panel it appears in. So a panel prompt is, in order: , comic book panel, , , , The art style + palette are constant across the whole comic (style consistency); the per-character appearance is constant per character (character consistency); only the scene varies panel to panel (story variety). """ from __future__ import annotations import hashlib from .schema import Panel, ComicBible # FLUX bakes garbled letters if asked for signage; the distilled model ignores # negative prompts, so the "no text" contract lives in the positive prompt. The UI # renders captions separately, so we never want text inside the image. TEXT_FREE = ( "no text, no speech bubbles, no captions, no lettering or words anywhere in the image" ) # A sane default if the writer omitted a style/palette. DEFAULT_STYLE = ( "modern western comic book art, bold confident black ink linework, dynamic " "cel shading, clean dramatic composition" ) DEFAULT_PALETTE = "rich saturated comic-book color palette" def _appearance_clause(panel: Panel, bible: ComicBible) -> str: """The verbatim fixed appearance text for each named character in this panel. This is the consistency anchor — same words every time a character appears. """ clauses = [] for name in panel.characters: ch = bible.character(name) if ch is not None and ch.appearance: clauses.append(f"{ch.name}: {ch.appearance}") return "; ".join(clauses) def build_image_prompt(panel: Panel, bible: ComicBible) -> str: """Full FLUX prompt for one panel (idempotent; also stored on panel.image_prompt).""" style = bible.art_style or DEFAULT_STYLE palette = bible.palette or DEFAULT_PALETTE scene = (panel.scene or "").strip().rstrip(".") chars = _appearance_clause(panel, bible) parts = [style, "comic book panel", scene] if chars: parts.append(chars) parts += [palette, TEXT_FREE] return ", ".join(p for p in parts if p and p.strip()) def panel_seed(bible: ComicBible, panel: Panel) -> int: """Deterministic per-panel seed (stable across reruns, distinct per panel). Keyed on the title + panel index so the same comic re-renders identically and no two panels collide on a seed. """ key = f"{bible.title}|{panel.index}".encode("utf-8") digest = hashlib.sha256(key).digest() return int.from_bytes(digest[:4], "big") & 0x7FFFFFFF