diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-05 11:31:18 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-05 11:31:18 +0200 |
| commit | 7017ceddad7750c54aed5a4323aec0ef3f2e119f (patch) | |
| tree | b2ce978ec7aa27a4bbdde1720af648981e42c397 | |
| parent | 5237176a9bcb2d0115dc2697c688884e53abd780 (diff) | |
| download | fanfictioner-7017ceddad7750c54aed5a4323aec0ef3f2e119f.tar.gz fanfictioner-7017ceddad7750c54aed5a4323aec0ef3f2e119f.zip | |
Log run events to run.log in the work dir
Every generation, pick with its seed, regeneration, prompt edit, skip,
quit and plan decision is appended with a timestamp, so a pick can be
traced back to its seed and a session can be reviewed afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 5 | ||||
| -rwxr-xr-x | fanfictioner | 37 |
2 files changed, 41 insertions, 1 deletions
@@ -19,7 +19,10 @@ image stage. Pages land in the book-reader library as `NNN.png`. fanfictioner --selftest Work files go to a directory named after the story (`stories/rooftop.md` -> -`stories/rooftop/`): `plan.md`, `plan.json`, `refs/`, `cand/`. Quit any time +`stories/rooftop/`): `plan.md`, `plan.json`, `refs/`, `cand/` and `run.log`. +`run.log` records every generation, pick (with its seed), regeneration, prompt +edit, skip and plan decision with a timestamp. Each sd-cli call also leaves its +own log next to its candidates in `cand/`. Quit any time with `q` or Ctrl-C; the next run resumes from what is on disk. Plan review keys: `c` confirm, `e` edit in `$EDITOR`, `g` ask Gemma to revise, diff --git a/fanfictioner b/fanfictioner index e1f5af4..fc0ad3d 100755 --- a/fanfictioner +++ b/fanfictioner @@ -335,6 +335,16 @@ def run_sd(args, log): return None +LOG = None # wd / "run.log", set by main + + +def log(msg): + """Append one timestamped event to run.log, once the work dir exists.""" + if LOG and LOG.parent.is_dir(): + with open(LOG, "a") as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n") + + def editor(path): subprocess.run([*shlex.split(os.environ.get("EDITOR") or "vi"), str(path)]) @@ -376,8 +386,10 @@ def show(files, seed, d): def review(d, profile, prompt, size, refs=(), skip=False): """Generate, show, ask. Returns the picked file, or None when the user skips.""" keys = "1-3 pick, r regen, p edit prompt" + (", s skip edit" if skip else "") + ", q quit" + stage = f"{d.parent.name}/{d.name}" while True: files, seed, err = generate(d, profile, prompt, size, refs) + log(f"{stage} {profile} seed {seed}" + (f" FAILED: {err}" if err else "")) if not err: show(files, seed, d) while True: @@ -386,17 +398,22 @@ def review(d, profile, prompt, size, refs=(), skip=False): except EOFError: k = "q" if k in ("1", "2", "3") and not err: + log(f"{stage} pick {k}, seed {seed + int(k) - 1}, {files[int(k) - 1].name}") return files[int(k) - 1] if k == "r": + log(f"{stage} regenerate") break if k == "p": (d / "prompt.txt").write_text(prompt) editor(d / "prompt.txt") prompt = (d / "prompt.txt").read_text().strip() or prompt + log(f"{stage} prompt edited: {prompt}") break if k == "s" and skip: + log(f"{stage} skip edit") return None if k == "q": + log(f"{stage} quit") sys.exit("quit, run again to resume") @@ -450,6 +467,7 @@ def plan_stage(story, wd): except GEMMA_ERRS as e: sys.exit(f"Gemma failed: {e}") md_path.write_text(draft) + log("plan.md drafted by Gemma") while True: md = md_path.read_text() plan = parse_plan(md) @@ -468,6 +486,7 @@ def plan_stage(story, wd): tmp = js_path.with_name("plan.json.tmp") tmp.write_text(json.dumps(pj, indent=2) + "\n") os.replace(tmp, js_path) + log(f"plan confirmed: {len(pj['characters'])} characters, {len(pj['pages'])} pages") return pj elif k == "e": editor(md_path) @@ -485,10 +504,12 @@ def plan_stage(story, wd): 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) md_path.write_text(new) + log(f"Gemma revision accepted: {ins}") elif k == "q": sys.exit(0) @@ -504,6 +525,7 @@ def refs_stage(chars, wd): print(f"\n== reference sheet: {c['name']}") pick = review(wd / "cand" / f"ref_{c['name']}", "zimage", c["turnaround_prompt"], SIZES["turnaround"]) keep(pick, wd / "refs" / f"{c['name']}.png") + log(f"reference sheet {c['name']} kept") def page_stage(p, wd, out, base): @@ -526,6 +548,7 @@ def page_stage(p, wd, out, base): keep(pick or scene, nxt) scene = nxt keep(scene, out) + log(f"page {p['n']} -> {out}") print(f"page {p['n']} -> {out}") @@ -666,6 +689,15 @@ def selftest(): assert run_sd(["sh", "-c", "echo '[ERROR] boom'"], t / "e.log") == "sd-cli failed (exit 0)" assert run_sd(["false"], t / "f.log") == "sd-cli failed (exit 1)" assert run_sd(["/nonexistent-sd-cli"], t / "n.log").startswith("cannot run /nonexistent-sd-cli") + global LOG + LOG = t / "nowd" / "run.log" + log("before the work dir exists") + LOG = t / "run.log" + log("one") + log("two") + lines = LOG.read_text().splitlines() + LOG = None + assert [x[20:] for x in lines] == ["one", "two"] and not (t / "nowd").exists(), lines print("selftest ok") @@ -691,14 +723,19 @@ def main(): lib = None if not lib: sys.exit(f"set LIBRARY in {CONF}") + global LOG wd = a.story.with_suffix("") + LOG = wd / "run.log" + log(f"start, base model {a.base}") pj = plan_stage(a.story, wd) chars, pages = todo(pj, wd, lib) refs_stage(chars, wd) for p in pages: page_stage(p, wd, page_path(lib, pj, p["n"]), a.base) + log("done") print(f"\ndone: {page_path(lib, pj, 1).parent}") except (KeyboardInterrupt, EOFError): + log("interrupted") sys.exit("\ninterrupted, run again to resume") |
