diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-05 14:35:33 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-05 14:35:33 +0200 |
| commit | 45ab8ec26c95f580bfcacf401ffb5ea8717e3c2e (patch) | |
| tree | e68ca1b76ab8d05b69376c0c19c1f2957e703703 | |
| parent | 5ecfc3540437625d12cefac18a941acde63bc72e (diff) | |
| download | fanfictioner-45ab8ec26c95f580bfcacf401ffb5ea8717e3c2e.tar.gz fanfictioner-45ab8ec26c95f580bfcacf401ffb5ea8717e3c2e.zip | |
Show a live preview of the candidate being rendered
sd-cli writes a latent projection of the current candidate every step
(--preview proj, no extra VRAM). While it runs, a magick display window
shows that preview scaled up to 640 px and redraws on each change; it
closes when sd-cli exits. display -update drops -resize on reload, so the
poll loop writes the enlarged copy itself. Under Hyprland a runtime
window rule (hyprctl eval) floats the window so the tiling layout leaves
its size alone; the config is not touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 3 | ||||
| -rwxr-xr-x | fanfictioner | 39 |
2 files changed, 38 insertions, 4 deletions
@@ -11,7 +11,8 @@ image stage. Pages land in the book-reader library as `NNN.png`. - llama-server in router mode on `localhost:8181` serving `Gemma4-12B-qat-mtp` - `sd-cli` with Vulkan, models under `/data/LLM-models/SD` - ImageMagick 7 (`magick`), `$EDITOR`, `notify-send` (libnotify), and kitty for - inline previews (elsewhere candidates open with `xdg-open`) + inline previews (elsewhere candidates open with `xdg-open`); a live preview of + the candidate being rendered opens in `magick display`, floated under Hyprland - `~/.config/book-reader.conf` with a `LIBRARY="..."` line ## Usage diff --git a/fanfictioner b/fanfictioner index 89a3fec..063f349 100755 --- a/fanfictioner +++ b/fanfictioner @@ -376,8 +376,31 @@ def progress(start, log): print(f"\r{int(time.time() - start):4d}s {last.strip()}"[:width].ljust(width), end="", flush=True) -def run_sd(args, log): - """Run sd-cli with output in log; return an error string or None.""" +def scale_preview(src, dst): + """sd-cli's preview is latent-sized (1/8 of the image): enlarge it, swapping dst in atomically.""" + tmp = dst.with_name("tmp_" + dst.name) + if subprocess.run(["magick", str(src), "-resize", "640x640", str(tmp)], capture_output=True).returncode == 0: + os.replace(tmp, dst) + + +def open_preview(f): + """Window showing f, redrawn by magick display whenever f changes (display -update drops + -resize on reload, hence scale_preview). Floated under Hyprland through a runtime rule, + so the tiling layout neither resizes it nor reflows.""" + try: + if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"): + subprocess.run(["hyprctl", "eval", 'hl.window_rule({name = "fanfictioner-preview", ' + 'match = { title = "^fanfictioner preview$" }, float = true})'], capture_output=True) + return subprocess.Popen(["magick", "display", "-update", "1", "-title", "fanfictioner preview", str(f)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except OSError: + return None + + +def run_sd(args, log, preview=None): + """Run sd-cli with output in log; return an error string or None. + With preview, a live view of that file is open while sd-cli runs.""" + viewer, seen = None, None with open(log, "w") as f: f.write(shlex.join(args) + "\n") f.flush() @@ -390,6 +413,11 @@ def run_sd(args, log): while proc.poll() is None: time.sleep(2) progress(start, log) + if preview and preview.exists() and preview.stat().st_mtime != seen: + seen = preview.stat().st_mtime + view = preview.with_name("view_" + preview.name) + scale_preview(preview, view) + viewer = viewer or (view.exists() and open_preview(view)) or None if proc.poll() is None and \ mem_available_kb(Path("/proc/meminfo").read_text()) < MIN_RAM_KB: proc.kill() @@ -401,6 +429,8 @@ def run_sd(args, log): raise finally: print() + if viewer: + viewer.terminate() if proc.returncode or "[ERROR" in Path(log).read_text(errors="replace"): return f"sd-cli failed (exit {proc.returncode})" return None @@ -441,7 +471,10 @@ def generate(d, profile, prompt, size, refs=()): seed = random.randint(0, 2**31 - 4) log = d / f"{seed}.log" print(f"sd-cli {profile}, seed {seed} ...", flush=True) - err = run_sd(sd_args(profile, prompt, d / f"{seed}_%d.png", size, seed, refs), log) + preview = d / "preview.png" # rewritten every step, one candidate after the other + preview.unlink(missing_ok=True) + err = run_sd(sd_args(profile, prompt, d / f"{seed}_%d.png", size, seed, refs) + + ["--preview", "proj", "--preview-path", str(preview)], log, preview) files = [d / f"{seed}_{i}.png" for i in (1, 2, 3)] if not err and not all(f.exists() for f in files): err = "sd-cli wrote no output" |
