Spaces:
Running on Zero
Running on Zero
| """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: | |
| <art style>, comic book panel, <scene>, <appearance of each character present>, | |
| <palette>, <text-free contract> | |
| 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 | |