diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-05 13:56:32 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-05 13:56:32 +0200 |
| commit | f15665670b5f2923bb8ec84929b6bc9b7f29a53a (patch) | |
| tree | 8f566728a182a046d9f59e16d1d5446131aae8f3 | |
| parent | c07c3df46fddad7a91fe0c3038400f14f632ed0d (diff) | |
| download | fanfictioner-f15665670b5f2923bb8ec84929b6bc9b7f29a53a.tar.gz fanfictioner-f15665670b5f2923bb8ec84929b6bc9b7f29a53a.zip | |
Revise the plan when the story changes, take style from frontmatter
An edited story only re-applied its frontmatter, so plan.md kept
describing the old story. The body plan.md was drafted from is now kept
in story.seen; when the story body differs, ask whether Gemma should
revise the plan to match, start a new draft, or keep the plan.
A style: frontmatter key now overrides the Style: line, so the art style
can be set before Gemma drafts anything.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 7 | ||||
| -rwxr-xr-x | fanfictioner | 62 |
2 files changed, 53 insertions, 16 deletions
@@ -23,14 +23,19 @@ instead of letting Gemma invent them; every key is optional: book: Life as Daniel chapter: 1 title: Meet Daniel + style: shoujo manga, soft pastel cel shading --- `series` and `book` name the library folders, `title` is the lettering on the -chapter's page 1. With `chapter`, pages are saved as +chapter's page 1, `style` is the art style of every page and reference sheet. With `chapter`, pages are saved as `01-001.png`, `01-002.png`, so every chapter of a book (one story file each) reads as one continuous book in book-reader. These values always override what Gemma writes in plan.md. +Editing the story after plan.md exists asks whether Gemma should revise the +plan to match it, draft a new one (the old one is kept as plan.md.bak), or +keep the plan as it is. Editing only the frontmatter is applied without Gemma. + fanfictioner story.md [--base zimage|krea2] fanfictioner --selftest diff --git a/fanfictioner b/fanfictioner index 23c06b9..8a4f164 100755 --- a/fanfictioner +++ b/fanfictioner @@ -136,7 +136,7 @@ def parse_front(text): def apply_front(md, meta): """Force the story's frontmatter into plan.md, whatever Gemma wrote there.""" - keys = [k for k in ("Series", "Book", "Chapter") if meta.get(k.lower())] + keys = [k for k in ("Series", "Book", "Chapter", "Style") if meta.get(k.lower())] drop = keys + (["Title"] if meta.get("title") else []) lines = [x for x in md.splitlines() if not (drop and re.match(r"[-*\s]*\**(%s)\**:" % "|".join(drop), x.strip()))] @@ -543,12 +543,54 @@ def strip_fences(s): GEMMA_ERRS = (OSError, ValueError, KeyError, http.client.HTTPException) +def revise(md, meta, wd, ins): + """Gemma rewrites the plan; returns the new plan.md text, or None when it fails or is invalid.""" + try: + new = apply_front(strip_fences(chat( + DRAFT_SYS, f"Current plan:\n\n{md}\n\nRevise it: {ins}\nReturn the complete new plan.")), meta) + except GEMMA_ERRS as e: + print(f"Gemma failed: {e}") + return None + nerrs = validate_plan(parse_plan(new)) + if nerrs: + (wd / "plan.rejected.md").write_text(new) + log(f"Gemma revision rejected ({'; '.join(nerrs)}): {ins}") + print("revision invalid, plan.md kept, see plan.rejected.md", *nerrs, sep="\n ") + return None + (wd / "plan.md.bak").write_text(md) + log(f"Gemma revision accepted: {ins}") + return new + + def plan_stage(story, wd): md_path, js_path = wd / "plan.md", wd / "plan.json" + seen = wd / "story.seen" # the story body plan.md was last drafted or revised from if js_path.exists() and md_path.exists() and \ js_path.stat().st_mtime >= max(md_path.stat().st_mtime, story.stat().st_mtime): return json.loads(js_path.read_text()) meta, body = parse_front(story.read_text()) + if md_path.exists() and (seen.read_text() != body if seen.exists() + else story.stat().st_mtime > md_path.stat().st_mtime): + try: + k = input("\nstory changed since plan.md was drafted: " + "[r]evise plan with Gemma [n]ew draft [k]eep plan [q]uit > ").strip().lower() + except EOFError: + k = "q" + if k == "n": + os.replace(md_path, wd / "plan.md.bak") + log("story changed, plan.md moved to plan.md.bak for a new draft") + elif k == "r": + check_llm() + new = revise(md_path.read_text(), meta, wd, "the story changed, update the plan to match " + f"this new story and keep whatever still fits:\n\n{body}") + if new: + md_path.write_text(new) + seen.write_text(body) + elif k == "k": + seen.write_text(body) + log("story changed, plan.md kept") + else: + sys.exit("quit, run again to resume") if not md_path.exists(): check_llm() wd.mkdir(exist_ok=True) @@ -557,6 +599,7 @@ def plan_stage(story, wd): except GEMMA_ERRS as e: sys.exit(f"Gemma failed: {e}") md_path.write_text(apply_front(draft, meta)) + seen.write_text(body) log("plan.md drafted by Gemma") while True: md = md_path.read_text() @@ -588,21 +631,8 @@ def plan_stage(story, wd): ins = input("instruction for Gemma: ") except EOFError: continue - try: - new = apply_front(strip_fences(chat( - DRAFT_SYS, f"Current plan:\n\n{md}\n\nRevise it: {ins}\nReturn the complete new plan.")), meta) - except GEMMA_ERRS as e: - print(f"Gemma failed: {e}") - continue - nerrs = validate_plan(parse_plan(new)) - if nerrs: - (wd / "plan.rejected.md").write_text(new) - log(f"Gemma revision rejected ({'; '.join(nerrs)}): {ins}") - print("revision invalid, plan.md kept, see plan.rejected.md", *nerrs, sep="\n ") - else: - (wd / "plan.md.bak").write_text(md) + if new := revise(md, meta, wd, ins): md_path.write_text(new) - log(f"Gemma revision accepted: {ins}") elif k == "q": sys.exit(0) @@ -715,6 +745,8 @@ def selftest(): "Framing: wide shot, eye level, landscape\nTitle: Gemma Title"), meta) assert apply_front(forced, meta) == forced assert apply_front(SAMPLE_PLAN, {}) == SAMPLE_PLAN + styled = parse_plan(apply_front(SAMPLE_PLAN, {"style": "ink manga"})) + assert styled["style"] == "ink manga" and validate_plan(styled) == [], styled fp = parse_plan(forced) assert (fp["title"], fp["series"], fp["book"], fp["chapter"]) == ("Rooftop", "A new life", "Life as Daniel", "1"), fp assert fp["pages"][0]["lettering"] == "Meet Daniel" and fp["pages"][1]["lettering"] == "", fp["pages"] |
