diff --git a/AGENTS.md b/AGENTS.md index 3a0aef8..eeff245 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,7 +99,7 @@ in `pyproject.toml`): | `distance_loop.py` | iterate + measure in one run (`dltb-distance-loop`): free-running loop with a strength-capable model, then DreamSim on the saved frames -> `distance_metrics.csv` + `distance_plot.png`; the pipeline is unloaded and released before DreamSim loads | | `distance_feedback.py` | continuous' stateful loop with a static source (`dltb-distance-feedback`): one image repeated for `--frames` frames, `--anchor-blend 0` = distance-loop, `1` = repeated fresh passes; saves every frame and every model input blend, then the same three-series DreamSim CSV + plot (input-vs-original is the third series) | | `detect_stabilization.py` | CPU-only verdict on a metrics CSV (`dltb-stabilization`): robust tail band (median ± k·MAD), backward scan for the last out-of-band smoothed sample, plateau diagnostics in band units (drift / level shift / wandering / oscillation / too short); reads `distance_metrics.csv`/`drift_metrics.csv`, writes `stabilization_report.csv` next to the input; `tests/test_detect_stabilization.py` is its stress battery | -| `stability_sweep.py` | anchor-blend stability sweep (`dltb-stability-sweep`): dltb-distance-feedback's loop per blend with ONE pipeline and ONE DreamSim load, `detect_stabilization` verdicts on to_prev/to_ref; stable blends reduced to two plateau frames + `run.json` under `stable/`, unstable moved whole to `unstable/`, summary `stability_sweep.csv` | +| `stability_sweep.py` | anchor-blend stability sweep (`dltb-stability-sweep`): dltb-distance-feedback's loop per blend with ONE pipeline and ONE DreamSim load, `detect_stabilization` verdicts on to_prev/to_ref; stable blends reduced to two plateau frames + `run.json` under `stable/`, unstable moved whole to `unstable/`, summary `stability_sweep.csv` + `stability_plot.png` | | `klein.py` | klein-restricted wrapper; dual-ref conditioning hook; delegates to `continuous.run()` | Key invariants: diff --git a/README.md b/README.md index 8ab2155..02cbdbb 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ The console scripts share the library code in `src/dltb/` (`models`, `imaging`, series stabilize are reduced to their two plateau-opening frames plus `run.json` under `stable//`; every other run moves wholesale to `unstable//` for inspection; the stable ones are summarized in - `stability_sweep.csv` (`anchor-blend, v_prev, i_prev, v_ref, i_ref`). + `stability_sweep.csv` (`anchor-blend, v_prev, i_prev, v_ref, i_ref`) and + charted to `stability_plot.png` (stable points only, no placeholders). ## Documentation @@ -214,8 +215,8 @@ Runs land in `untracked/output_/_/` (e.g. `menu_free-run the saved frames, and the output video(s). `dltb-distance-loop` and `dltb-distance-feedback` use fixed tags (no input stem) under the same model tree — `distance_s/`, `feedback_s_a/`, and the -sweep root `stability_s/` (with `stable/`, `unstable/` and -`stability_sweep.csv` inside); since prompt/steps/seed are not part of those +sweep root `stability_s/` (with `stable/`, `unstable/`, +`stability_sweep.csv` and `stability_plot.png` inside); since prompt/steps/seed are not part of those tags, pass `--output-dir` subtrees when varying them — their `run.json` records the settings of each run. diff --git a/src/dltb/stability_sweep.py b/src/dltb/stability_sweep.py index 5f98928..fe24bb3 100644 --- a/src/dltb/stability_sweep.py +++ b/src/dltb/stability_sweep.py @@ -39,7 +39,9 @@ and the stable runs are summarized in /stability_sweep.csv: anchor-blend, v_prev, i_prev, v_ref, i_ref with the blend value, the stabilized (plateau-median) to_prev distance and -its frame number, then the same for to_ref. +its frame number, then the same for to_ref -- and charted to +/stability_plot.png (stable points only, no placeholders for the +unstable blends). Layout under the sweep root (default untracked/output_/stability_s, or --output-dir/; runs @@ -47,6 +49,7 @@ differing in other knobs still want --output-dir subtrees, same rule as every tool -- the blend list itself lives in run.json and the table): stability_sweep.csv the summary table (stable blends only) + stability_plot.png the table charted (stable blends only) run.json the sweep's fixed settings + the blend list stable// two frames + run.json per stable blend unstable// full run directories, kept for inspection @@ -208,6 +211,48 @@ def keep_stabilized_frame(staged: Path, t_stab: int | None, dest: Path) -> int: return t_stab +def plot_sweep(table: list[tuple], out_png: Path, title: str) -> bool: + """Chart the sweep's stable blends to out_png: anchor-blend on X, one + panel per stabilized distance (own Y scales -- to_prev is typically far + smaller than to_ref), each point annotated with the frame that opened + its plateau. Only stable blends appear; unstable runs get no + placeholders. Returns False when there is nothing to plot.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + series = ( + ("stabilized to previous (v_prev)", + [(r[0], r[1], r[2]) for r in table if r[1] is not None]), + ("stabilized to reference (v_ref)", + [(r[0], r[3], r[4]) for r in table if r[3] is not None]), + ) + if not any(points for _, points in series): + return False + + fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True) + for ax, (label, points) in zip(axes, series): + if not points: + ax.text(0.5, 0.5, "no stable blends", ha="center", va="center", + transform=ax.transAxes, fontsize=10) + ax.set_ylabel(label) + continue + xs, ys, _ = zip(*points) + ax.plot(xs, ys, "-o", lw=1.0, ms=5) + for x, y, frame in points: + ax.annotate(f"f{frame}", (x, y), textcoords="offset points", + xytext=(0, 7), ha="center", fontsize=7) + ax.set_title(label, fontsize=10) + ax.set_ylabel("DreamSim distance\n(plateau median)") + ax.grid(alpha=0.3) + axes[-1].set_xlabel("anchor-blend") + fig.suptitle(title, fontsize=11) + fig.tight_layout(rect=(0, 0, 1, 0.95)) + fig.savefig(out_png, dpi=150) + plt.close(fig) + return True + + def format_table(table: list[tuple]) -> str: """Fixed-width rendering of the summary rows (same cells as the CSV).""" @@ -344,6 +389,16 @@ def run(args: argparse.Namespace) -> None: print(f"\n{len(table)}/{len(blends)} blend values stable; " f"{len(blends) - len(table)} moved to {unstable_root}") print(f"table: {out_csv}") + out_plot = root / "stability_plot.png" + prompt = args.prompt if len(args.prompt) <= 60 else args.prompt[:57] + "..." + title = (f"{args.model} stability sweep: strength={args.strength:g} " + f"steps={args.num_inference_steps}") + if prompt: + title += f" prompt={prompt!r}" + if plot_sweep(table, out_plot, title): + print(f"plot : {out_plot}") + else: + print("plot : skipped (no stable blends)") if table: print(format_table(table))