From 517d1e69b1b30c4a647840417d896884fa9fcea1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 5 Oct 2026 09:36:29 +0200 Subject: Parse and validate plan.md Co-Authored-By: Claude Opus 5.5 --- fanfictioner | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/fanfictioner b/fanfictioner index a3715c4..43691b9 100755 --- a/fanfictioner +++ b/fanfictioner @@ -64,7 +64,119 @@ def keep(src, dst): return dst +def parse_plan(text): + plan = {"title": "", "series": "", "book": "", "style": "", "characters": {}, "pages": []} + section = cur = None + for line in text.splitlines(): + s = line.strip() + if s.startswith("# ") and not plan["title"]: + plan["title"] = s[2:].strip() + elif s.startswith("## "): + section, cur = s[3:].strip().lower(), None + elif s.startswith("### "): + head = s[4:].strip() + if section == "characters": + cur = plan["characters"].setdefault(head, []) + 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": [], + "characters": [], "pose": "", "framing": "", "model": ""} + plan["pages"].append(cur) + elif section is None and (m := re.match(r"(Series|Book|Style):\s*(.*)", s)): + plan[m[1].lower()] = m[2].strip() + elif section == "pages" and cur is not None and \ + (m := re.match(r"(Characters|Pose|Framing|Model):\s*(.*)", s)): + key, val = m[1].lower(), m[2].strip() + if key == "characters": + val = [] if val.lower() == "none" else [c.strip() for c in val.split(",") if c.strip()] + elif key == "model": + val = val.split()[0].lower() if val else "" + cur[key] = val + elif cur is not None and s: + (cur if isinstance(cur, list) else cur["text"]).append(s) + plan["characters"] = {k: " ".join(v) for k, v in plan["characters"].items()} + for p in plan["pages"]: + p["text"] = " ".join(p["text"]) + return plan + + +def validate_plan(plan): + errs = [f"missing {k}" for k in ("title", "series", "book") if not plan[k]] + if not plan["characters"]: + errs.append("no characters") + if not plan["pages"]: + errs.append("no pages") + ns = [p["n"] for p in plan["pages"]] + if len(set(ns)) != len(ns) or 0 in ns: + errs.append("page numbers must be unique and start at 1") + for p in plan["pages"]: + errs += [f"page {p['n']}: unknown character {c}" for c in p["characters"] + if c not in plan["characters"]] + if p["characters"] and not p["pose"]: + errs.append(f"page {p['n']}: Pose missing") + if p["model"] and p["model"] not in ("zimage", "krea2"): + errs.append(f"page {p['n']}: unknown model {p['model']}") + return errs + + +SAMPLE_PLAN = """# Rooftop +Series: Test Series +Book: Book One +Style: soft watercolor, muted colors + +## Characters + +### Mara +Young woman, long red hair, +green eyes, grey coat. + +### Jon +Tall man, short beard, blue jacket. + +### Kit +Small girl, black bob, yellow raincoat. + +## Pages + +### 1. Arrival +They reach the roof at dusk. +Characters: Mara, Jon, Kit +Pose: Mara left facing right, Jon center facing camera, Kit right sitting. +Framing: wide shot, eye level, landscape + +### 2. Empty roof +Nobody is there any more. +Characters: none +Framing: wide shot, portrait +Model: krea2 +""" + + def selftest(): + plan = parse_plan(SAMPLE_PLAN) + assert (plan["title"], plan["series"], plan["book"]) == ("Rooftop", "Test Series", "Book One"), plan + assert plan["style"] == "soft watercolor, muted colors" + assert list(plan["characters"]) == ["Mara", "Jon", "Kit"] + assert plan["characters"]["Mara"] == "Young woman, long red hair, green eyes, grey coat." + p1, p2 = plan["pages"] + assert (p1["n"], p1["title"], p1["characters"]) == (1, "Arrival", ["Mara", "Jon", "Kit"]) + assert p1["text"] == "They reach the roof at dusk." and p1["pose"].startswith("Mara left") + assert p1["model"] == "" and p2["model"] == "krea2" and p2["characters"] == [] + assert validate_plan(plan) == [] + + bad = parse_plan(SAMPLE_PLAN.replace("Book: Book One\n", "") + .replace("Characters: none", "Characters: Zed") + .replace("Model: krea2", "Model: sdxl") + .replace("### 2.", "### 1.")) + errs = validate_plan(bad) + assert "missing book" in errs, errs + assert "page 1: unknown character Zed" in errs, errs + assert "page 1: Pose missing" in errs, errs + assert "page 1: unknown model sdxl" in errs, errs + assert "page numbers must be unique and start at 1" in errs, errs + assert validate_plan(parse_plan("")) == ["missing title", "missing series", "missing book", + "no characters", "no pages"] + with tempfile.TemporaryDirectory() as t: t = Path(t) (t / "c.conf").write_text('# comment\nLIBRARY="/data/my lib"\nPORT=8642\n') -- cgit v1.2.3