diff options
| -rwxr-xr-x | fanfictioner | 53 |
1 files changed, 37 insertions, 16 deletions
diff --git a/fanfictioner b/fanfictioner index 132e46a..23c06b9 100755 --- a/fanfictioner +++ b/fanfictioner @@ -70,8 +70,11 @@ through action, expression and setting. The only exception is the cover. Page 1 is the book cover: the main characters in a striking composition, portrait, with a Title: line holding the book title. No other page has a Title: line. Give every character one fixed visual description that starts with who they are -(e.g. "Young woman", "Old man", "Little girl"), then age, hair, face, eyes, build, outfit, -and never vary it between pages. +(e.g. "Young woman", "Old man", "Little girl"), then age, hair, face, eyes, build, +and never vary it between pages. Clothing never goes in that description: put the +character's usual clothes on their Outfit: line. When a page needs different clothes +(a formal event, pajamas, swimwear, a uniform), give that page an Outfit: line for the +characters who change; their clothes then stay changed only on that page. Characters are people only. Animals and objects belong to the scene: describe them in the page text, never under ## Characters. If a page has a Model: line, keep it unchanged. @@ -86,7 +89,8 @@ Style: <one line describing the art style of every page> ### <Name> Pronoun: <she, he or they> -<fixed visual description> +Outfit: <usual clothes> +<fixed visual description, no clothing> ## Pages @@ -94,6 +98,7 @@ Pronoun: <she, he or they> <what happens, setting, mood> Characters: <Name>, <Name> (or: none) Pose: <Name>: <position in the frame, body orientation and pose>; <Name>: <...> +Outfit: <Name>: <clothes on this page>; <Name>: <...> (only for characters whose clothes differ) Framing: <camera angle, shot size>, <portrait or landscape> Title: <book title, cover page only> """ @@ -168,7 +173,7 @@ def keep(src, dst): def parse_plan(text): plan = {"title": "", "series": "", "book": "", "chapter": "", "style": "", "characters": {}, "pronouns": {}, - "pages": []} + "outfits": {}, "pages": []} section = cur = cname = None for line in text.splitlines(): s = line.strip() @@ -185,16 +190,20 @@ def parse_plan(text): elif section == "pages": m = re.match(r"(\d+)\.\s*(.*)", head) cur = {"n": int(m[1]) if m else 0, "title": m[2] if m else head, "text": [], - "heading": "" if m else head, "characters": None, "pose": "", "framing": "", "model": "", - "lettering": ""} + "heading": "" if m else head, "characters": None, "pose": "", "outfit": "", "framing": "", + "model": "", "lettering": ""} plan["pages"].append(cur) elif section is None and (m := re.match(r"[-*\s]*\**(Series|Book|Chapter|Style)\**:\**\s*(.*)", s)): plan[m[1].lower()] = m[2].strip().strip("*").strip() elif section == "characters" and cur is not None and \ - (m := re.match(r"[-*\s]*\**Pronoun\**:\**\s*(.*)", s)): - plan["pronouns"][cname] = m[1].strip().strip("*").strip().lower() + (m := re.match(r"[-*\s]*\**(Pronoun|Outfit)\**:\**\s*(.*)", s)): + val = m[2].strip().strip("*").strip() + if m[1] == "Pronoun": + plan["pronouns"][cname] = val.lower() + else: + plan["outfits"][cname] = val elif section == "pages" and cur is not None and \ - (m := re.match(r"[-*\s]*\**(Characters|Pose|Framing|Model|Title)\**:\**\s*(.*)", s)): + (m := re.match(r"[-*\s]*\**(Characters|Pose|Outfit|Framing|Model|Title)\**:\**\s*(.*)", s)): key, val = m[1].lower(), m[2].strip().strip("*").strip() if key == "title": # the page heading already uses "title" key, val = "lettering", val.strip('"\u201c\u201d').strip() @@ -245,6 +254,8 @@ def validate_plan(plan): errs.append(f"page {p['n']}: duplicate character") poses = parse_poses(p["pose"]) errs += [f"page {p['n']}: Pose missing for {c}" for c in chars if c not in poses] + errs += [f"page {p['n']}: Outfit for {c}, who is not on the page" for c in parse_poses(p["outfit"]) + if c not in chars] if not re.search(r"portrait|landscape", p["framing"], re.I): errs.append(f"page {p['n']}: Framing must say portrait or landscape") if p["model"] and p["model"] not in ("zimage", "krea2"): @@ -266,7 +277,8 @@ green eyes, grey coat. ### Jon Pronoun: he -Tall man, short beard, blue jacket. +Outfit: blue jacket +Tall man, short beard. ### Kit Pronoun: she @@ -278,6 +290,7 @@ Small girl, black bob, yellow raincoat. They reach the roof at dusk. Characters: Mara, Jon, Kit Pose: Mara: left, facing right; Jon: center, facing camera; Kit: right, sitting. +Outfit: Jon: wearing a black tuxedo Framing: wide shot, eye level, landscape ### 2. Empty roof @@ -301,16 +314,21 @@ def sentence(s): def build_json(plan): """plan.md -> plan.json. Every image prompt comes from a fixed template, no LLM involved.""" style = sentence(plan["style"]) + + def look(n, outfit=""): + """Fixed description plus one outfit: the page's when it has one, else the usual one.""" + o = re.sub(r"(?i)^(wearing|wears)\s+", "", outfit or plan["outfits"].get(n, "")) + return sentence(plan["characters"][n]) + (f" Wearing {o.rstrip('.')}." if o else "") + chars = [{"name": n, "turnaround_prompt": - f"Character turnaround reference sheet of one person: {sentence(d)} The same person shown " + f"Character turnaround reference sheet of one person: {look(n)} The same person shown " "three times side by side, full body: front view, side view, back view. Neutral standing pose, " f"arms relaxed. Plain light-grey background, even studio lighting. {style} No text, no labels."} - for n, d in plan["characters"].items()] + for n in plan["characters"]] pages = [] for p in plan["pages"]: - poses = parse_poses(p["pose"]) - # Characters before the scene text, so a scene detail (a change of dress) overrides the sheet. - parts = [style] + [f"{sentence(plan['characters'][n])} {sentence(poses[n])}" for n in p["characters"]] + poses, outfits = parse_poses(p["pose"]), parse_poses(p["outfit"]) + parts = [style] + [f"{look(n, outfits.get(n))} {sentence(poses[n])}" for n in p["characters"]] parts.append(sentence(p["text"])) parts.append(sentence(p["framing"])) if p["lettering"]: @@ -713,13 +731,16 @@ def selftest(): assert "of one person: Young woman, long red hair, green eyes, grey coat. The same person" in \ pj["characters"][0]["turnaround_prompt"] assert pj["characters"][0]["turnaround_prompt"].endswith("Soft watercolor, muted colors. No text, no labels.") + assert "one person: Tall man, short beard. Wearing blue jacket. The same" in pj["characters"][1]["turnaround_prompt"] + assert "page 1: Outfit for Zed, who is not on the page" in validate_plan( + parse_plan(SAMPLE_PLAN.replace("Outfit: Jon:", "Outfit: Zed:"))) j1, j2 = pj["pages"] assert (j1["n"], j1["orientation"], j1["model"]) == (1, "landscape", "") assert (j2["n"], j2["orientation"], j2["model"], j2["edits"]) == (2, "portrait", "krea2", []) assert j1["base_prompt"] == ( "Soft watercolor, muted colors. " "Young woman, long red hair, green eyes, grey coat. Left, facing right. " - "Tall man, short beard, blue jacket. Center, facing camera. " + "Tall man, short beard. Wearing a black tuxedo. Center, facing camera. " "Small girl, black bob, yellow raincoat. Right, sitting. They reach the roof at dusk. " "Wide shot, eye level, landscape. No text, no speech bubbles."), j1["base_prompt"] assert j2["base_prompt"] == ("Soft watercolor, muted colors. Nobody is there any more. " |
