diff --git a/README.md b/README.md index 9e7e21c..492d5ab 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,13 @@ If you want to support the official hosted deployment and help us scale the poor ## Example -This badge sheet is the whole pitch in one image: same ugly fake-pixel source, -same target grid, same cheap Lanczos baseline, and one `phase-field` result -that finally commits to the lattice. +This sheet carries two cases: the ugly fake-pixel badge and a dense +higher-resolution landscape. The badge row shows the core salvage story. The +landscape row shows a different failure mode: auto lattice inference still +undershoots that source badly, so the `phase-field` panel is the tracked fixed +`512x512` result next to the same cheap Lanczos baseline. -Each panel also carries the same sword-guard inset, so the nasty little taper -problem has nowhere to hide. +Each row carries its own inset, so the tiny details have nowhere to hide. ![Repixelizer example comparison](docs/readme-assets/badge-example-sheet.png) @@ -192,10 +193,12 @@ Run the focused test suite with: ## Regenerating README Assets The README images are generated from repo-tracked fixtures, not from random -artifacts left lying around: +artifacts left lying around. The dense landscape row reuses the tracked fixed +`512x512` output fixture because auto lattice inference still undershoots that +case: ```powershell -.venv\Scripts\python scripts\generate_readme_previews.py --input tests\fixtures\real\ai-badge-cleaned.png --out-sheet docs\readme-assets\badge-example-sheet.png --out-guard-crop docs\readme-assets\guard-right-crop-comparison.png --scratch-dir artifacts\readme-build --device auto +.venv\Scripts\python scripts\generate_readme_previews.py --out-sheet docs\readme-assets\badge-example-sheet.png --out-guard-crop docs\readme-assets\guard-right-crop-comparison.png --scratch-dir artifacts\readme-build --device auto ``` That regenerates the main README sheet, the standalone sword-guard closeup diff --git a/docs/readme-assets/badge-example-sheet.png b/docs/readme-assets/badge-example-sheet.png index c6ec4d7..0b6f9bc 100644 Binary files a/docs/readme-assets/badge-example-sheet.png and b/docs/readme-assets/badge-example-sheet.png differ diff --git a/scripts/generate_readme_previews.py b/scripts/generate_readme_previews.py index b8e8447..cc55d54 100644 --- a/scripts/generate_readme_previews.py +++ b/scripts/generate_readme_previews.py @@ -19,6 +19,21 @@ class RenderedPanel: content_box: tuple[int, int, int, int] +@dataclass(slots=True) +class PreviewRow: + row_id: str + title: str + source_rgba: np.ndarray + source_subtitle: str + lanczos_rgba: np.ndarray + lanczos_subtitle: str + phase_field_rgba: np.ndarray + phase_field_subtitle: str + target_width: int + target_height: int + focus_cell_bbox: tuple[int, int, int, int] + + def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Regenerate the README comparison sheet and closeups from repo fixtures.") parser.add_argument( @@ -41,6 +56,16 @@ def _parse_args() -> argparse.Namespace: default="artifacts/readme-build", help="Directory for intermediate low-res outputs and diagnostics.", ) + parser.add_argument( + "--dense-input", + default="tests/fixtures/real/dense-landscape.png", + help="Dense high-resolution pixel-art source used for the second README row.", + ) + parser.add_argument( + "--dense-phase-field-output", + default="tests/fixtures/real/dense-landscape-fixed-512.png", + help="Tracked fixed-lattice phase-field output used for the dense landscape row.", + ) parser.add_argument( "--guard-cell-bbox", nargs=4, @@ -49,6 +74,14 @@ def _parse_args() -> argparse.Namespace: metavar=("X0", "Y0", "X1", "Y1"), help="Sword-guard crop bounds in output-grid cell coordinates for the AI row.", ) + parser.add_argument( + "--dense-focus-cell-bbox", + nargs=4, + type=int, + default=(160, 200, 280, 320), + metavar=("X0", "Y0", "X1", "Y1"), + help="Focus crop bounds in output-grid cell coordinates for the dense landscape row.", + ) parser.add_argument( "--target-width", type=int, @@ -73,6 +106,30 @@ def _parse_args() -> argparse.Namespace: default=-0.2, help="Pinned lattice phase Y for the canonical badge comparison.", ) + parser.add_argument( + "--dense-target-width", + type=int, + default=512, + help="Pinned target width for the dense landscape comparison.", + ) + parser.add_argument( + "--dense-target-height", + type=int, + default=512, + help="Pinned target height for the dense landscape comparison.", + ) + parser.add_argument( + "--dense-auto-inferred-width", + type=int, + default=251, + help="Current automatic lattice width guess for the dense landscape case.", + ) + parser.add_argument( + "--dense-auto-inferred-height", + type=int, + default=251, + help="Current automatic lattice height guess for the dense landscape case.", + ) parser.add_argument("--steps", type=int, default=48, help="Optimizer step budget for both runs.") parser.add_argument("--device", default="cpu", choices=("auto", "cpu", "cuda"), help="Torch device for generation.") return parser.parse_args() @@ -260,12 +317,72 @@ def _save_summary(path: Path, payload: dict[str, object]) -> None: path.write_text(json.dumps(payload, indent=2), encoding="utf-8") +def _build_row_panels( + row: PreviewRow, + *, + panel_width: int, + panel_height: int, + inset_width: int = 139, + inset_height: int = 103, +) -> tuple[list[RenderedPanel], tuple[int, int, int, int]]: + panels = [ + _render_panel( + "Source", + rgba=row.source_rgba, + pixelated=False, + panel_width=panel_width, + panel_height=panel_height, + subtitle=row.source_subtitle, + ), + _render_panel( + "Lanczos", + rgba=row.lanczos_rgba, + pixelated=True, + panel_width=panel_width, + panel_height=panel_height, + subtitle=row.lanczos_subtitle, + ), + _render_panel( + "Phase-field", + rgba=row.phase_field_rgba, + pixelated=True, + panel_width=panel_width, + panel_height=panel_height, + subtitle=row.phase_field_subtitle, + ), + ] + focus_source_bbox = _cell_bbox_to_source_bbox( + row.focus_cell_bbox, + source_width=row.source_rgba.shape[1], + source_height=row.source_rgba.shape[0], + target_width=row.target_width, + target_height=row.target_height, + ) + for panel in panels: + _draw_source_bbox( + panel, + focus_source_bbox, + source_width=row.source_rgba.shape[1], + source_height=row.source_rgba.shape[0], + ) + inset_crops = [ + _crop_rgba(row.source_rgba, focus_source_bbox), + _crop_rgba(row.lanczos_rgba, row.focus_cell_bbox), + _crop_rgba(row.phase_field_rgba, row.focus_cell_bbox), + ] + for panel, crop in zip(panels, inset_crops, strict=True): + _add_pip_inset(panel, _build_pip_inset(crop, width=inset_width, height=inset_height), margin_x=4, margin_y=4) + return panels, focus_source_bbox + + def main() -> None: args = _parse_args() scratch_dir = Path(args.scratch_dir) scratch_dir.mkdir(parents=True, exist_ok=True) source_input = Path(args.input) + dense_input = Path(args.dense_input) + dense_phase_field_output = Path(args.dense_phase_field_output) out_sheet = Path(args.out_sheet) out_guard_crop = Path(args.out_guard_crop) out_sheet.parent.mkdir(parents=True, exist_ok=True) @@ -286,41 +403,69 @@ def main() -> None: source_rgba = load_rgba(source_input) lanczos = lanczos_resize_baseline(source_rgba, width=args.target_width, height=args.target_height) + dense_source_rgba = load_rgba(dense_input) + dense_phase_field_rgba = load_rgba(dense_phase_field_output) + dense_lanczos = lanczos_resize_baseline( + dense_source_rgba, + width=args.dense_target_width, + height=args.dense_target_height, + ) save_rgba(scratch_dir / "badge-ai-lanczos.png", lanczos) + save_rgba(scratch_dir / "dense-landscape-lanczos-512.png", dense_lanczos) panel_width = 320 panel_height = 360 col_gap = 18 margin = 24 row_title_height = 22 + row_gap = 20 - panels = [ - _render_panel( - "Source", - rgba=source_rgba, - pixelated=False, - panel_width=panel_width, - panel_height=panel_height, - subtitle="Cleaned AI fake pixel art", + badge_focus_cell_bbox = _shrink_bbox_bottom_right( + tuple(args.guard_cell_bbox), + scale_x=0.5, + scale_y=0.5, + ) + rows = [ + PreviewRow( + row_id="badge", + title=f"AI fake pixel art -> {args.target_width}x{args.target_height} pinned lattice @ ({args.phase_x:.1f}, {args.phase_y:.1f})", + source_rgba=source_rgba, + source_subtitle="Cleaned AI fake pixel art", + lanczos_rgba=lanczos, + lanczos_subtitle=f"{args.target_width}x{args.target_height} baseline", + phase_field_rgba=phase_field_result.output_rgba, + phase_field_subtitle=f"{args.target_width}x{args.target_height} pinned lattice", + target_width=args.target_width, + target_height=args.target_height, + focus_cell_bbox=badge_focus_cell_bbox, ), - _render_panel( - "Lanczos", - rgba=lanczos, - pixelated=True, - panel_width=panel_width, - panel_height=panel_height, - subtitle=f"{args.target_width}x{args.target_height} baseline", - ), - _render_panel( - "Phase-field", - rgba=phase_field_result.output_rgba, - pixelated=True, - panel_width=panel_width, - panel_height=panel_height, - subtitle=f"{args.target_width}x{args.target_height} pinned lattice", + PreviewRow( + row_id="dense-landscape", + title=( + "Dense 1254px pixel art -> auto inference " + f"{args.dense_auto_inferred_width}x{args.dense_auto_inferred_height}; " + f"fixed {args.dense_target_width}x{args.dense_target_height} shown" + ), + source_rgba=dense_source_rgba, + source_subtitle=f"{dense_source_rgba.shape[1]}px source", + lanczos_rgba=dense_lanczos, + lanczos_subtitle=f"{args.dense_target_width}x{args.dense_target_height} baseline", + phase_field_rgba=dense_phase_field_rgba, + phase_field_subtitle=f"fixed {args.dense_target_width}x{args.dense_target_height} lattice", + target_width=args.dense_target_width, + target_height=args.dense_target_height, + focus_cell_bbox=tuple(args.dense_focus_cell_bbox), ), ] + built_rows: list[tuple[PreviewRow, list[RenderedPanel], tuple[int, int, int, int]]] = [] + for row in rows: + panels, focus_source_bbox = _build_row_panels( + row, + panel_width=panel_width, + panel_height=panel_height, + ) + built_rows.append((row, panels, focus_source_bbox)) guard_bbox = _cell_bbox_to_source_bbox( tuple(args.guard_cell_bbox), @@ -329,16 +474,6 @@ def main() -> None: target_width=args.target_width, target_height=args.target_height, ) - guard_cell_inset_bbox = _shrink_bbox_bottom_right(tuple(args.guard_cell_bbox), scale_x=0.5, scale_y=0.5) - guard_inset_bbox = _shrink_bbox_bottom_right(guard_bbox, scale_x=0.5, scale_y=0.5) - for panel in panels: - _draw_source_bbox( - panel, - guard_inset_bbox, - source_width=source_rgba.shape[1], - source_height=source_rgba.shape[0], - ) - guard_strip = _build_guard_strip( _crop_rgba(source_rgba, guard_bbox), _crop_rgba(lanczos, tuple(args.guard_cell_bbox)), @@ -346,29 +481,21 @@ def main() -> None: title=None, ) guard_strip.save(out_guard_crop) - inset_crops = [ - _crop_rgba(source_rgba, guard_inset_bbox), - _crop_rgba(lanczos, guard_cell_inset_bbox), - _crop_rgba(phase_field_result.output_rgba, guard_cell_inset_bbox), - ] - for panel, crop in zip(panels, inset_crops, strict=True): - _add_pip_inset(panel, _build_pip_inset(crop, width=139, height=103), margin_x=4, margin_y=4) width = margin * 2 + panel_width * 3 + col_gap * 2 - height = margin * 2 + row_title_height + panel_height + row_block_height = row_title_height + panel_height + height = margin * 2 + row_block_height * len(built_rows) + row_gap * max(0, len(built_rows) - 1) canvas = Image.new("RGBA", (width, height), (11, 11, 13, 255)) draw = ImageDraw.Draw(canvas) - draw.text( - (margin, margin - 2), - f"AI fake pixel art -> {args.target_width}x{args.target_height} pinned lattice @ ({args.phase_x:.1f}, {args.phase_y:.1f})", - fill=(255, 255, 255, 255), - ) - - row1_y = margin + row_title_height - x = margin - for panel in panels: - canvas.alpha_composite(panel.image, (x, row1_y)) - x += panel_width + col_gap + row_y = margin + for row, panels, _focus_source_bbox in built_rows: + draw.text((margin, row_y - 2), row.title, fill=(255, 255, 255, 255)) + panel_y = row_y + row_title_height + x = margin + for panel in panels: + canvas.alpha_composite(panel.image, (x, panel_y)) + x += panel_width + col_gap + row_y += row_block_height + row_gap out_sheet.parent.mkdir(parents=True, exist_ok=True) canvas.save(out_sheet) @@ -376,14 +503,22 @@ def main() -> None: _save_summary( scratch_dir / "readme-preview-summary.json", { - "input": str(source_input), + "rows": [ + { + "row_id": row.row_id, + "title": row.title, + "target_width": row.target_width, + "target_height": row.target_height, + "focus_cell_bbox": list(row.focus_cell_bbox), + "focus_source_bbox": list(focus_source_bbox), + } + for row, _panels, focus_source_bbox in built_rows + ], + "badge_input": str(source_input), + "dense_input": str(dense_input), + "dense_phase_field_output": str(dense_phase_field_output), "guard_cell_bbox": list(args.guard_cell_bbox), "guard_source_bbox": list(guard_bbox), - "guard_inset_source_bbox": list(guard_inset_bbox), - "target_width": args.target_width, - "target_height": args.target_height, - "phase_x": args.phase_x, - "phase_y": args.phase_y, "out_sheet": str(out_sheet), "out_guard_crop": str(out_guard_crop), "scratch_dir": str(scratch_dir), diff --git a/state/evidence.jsonl b/state/evidence.jsonl index dc78bb2..07f45b9 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -4,3 +4,4 @@ {"ts": "2026-04-26T03:57:11+00:00", "type": "docs", "status": "ok", "note": "Audit pass clarified note boundaries: the algorithm map remains the live control-flow truth, the implementation plan remains the forward hypothesis ledger, and hosted-auth-architecture is explicitly future design rather than landed auth machinery."} {"ts": "2026-04-26T04:05:06+00:00", "type": "design", "status": "accepted", "note": "Hosted auth was split into a reusable GameCult hosted-access architecture plus a thin Repixelizer-specific access profile. Recommended extraction path is an embedded shared gamecult_access layer first, with dedicated service only after a second app proves the need."} {"ts": "2026-04-26T04:52:53+00:00", "type": "design", "status": "accepted", "note": "Hosted convenience is the monetization path for Repixelizer; keep the code open, let self-hosting remain the escape hatch, and do not prioritize native desktop packaging unless it clearly beats hosted access on revenue, support burden, or strategy."} +{"ts": "2026-04-26T12:56:28+00:00", "type": "research", "status": "ok", "note": "Added dense landscape fixture tests/fixtures/real/dense-landscape.png plus tracked fixed-512 output. Current automatic lattice inference on the 1254px source undershoots to 251x251, so README previews now include that case with the fixed 512x512 result beside Lanczos."} diff --git a/tests/fixtures/real/dense-landscape-fixed-512.png b/tests/fixtures/real/dense-landscape-fixed-512.png new file mode 100644 index 0000000..a394f3f Binary files /dev/null and b/tests/fixtures/real/dense-landscape-fixed-512.png differ diff --git a/tests/fixtures/real/dense-landscape-focus.json b/tests/fixtures/real/dense-landscape-focus.json new file mode 100644 index 0000000..4550424 --- /dev/null +++ b/tests/fixtures/real/dense-landscape-focus.json @@ -0,0 +1,10 @@ +{ + "description": "Focused preview crop for the dense fixed-512 landscape row. Use this when comparing high-density detail retention against Lanczos and when checking the known size-inference miss.", + "input": "tests/fixtures/real/dense-landscape.png", + "fixed_output": "tests/fixtures/real/dense-landscape-fixed-512.png", + "auto_inferred_width": 251, + "auto_inferred_height": 251, + "target_width": 512, + "target_height": 512, + "cell_bbox": [160, 200, 280, 320] +} diff --git a/tests/fixtures/real/dense-landscape.json b/tests/fixtures/real/dense-landscape.json new file mode 100644 index 0000000..a7dd8e6 --- /dev/null +++ b/tests/fixtures/real/dense-landscape.json @@ -0,0 +1,22 @@ +{ + "id": "dense-landscape", + "kind": "real-high-density-pixel-landscape", + "description": "Dense high-resolution landscape fixture used for README previews and lattice-size inference regressions.", + "source_image": "D:\\Downloads\\ig_00e36939ddfb2c0b0169eb80e9f29481919f784bbedd0dbd45.png", + "paired_phase_field_output": "tests/fixtures/real/dense-landscape-fixed-512.png", + "expected_target_width": 512, + "expected_target_height": 512, + "auto_inference_snapshot": { + "date": "2026-04-26", + "target_width": 251, + "target_height": 251, + "phase_x": 0.0, + "phase_y": -0.2 + }, + "preview_focus_cell_bbox": [160, 200, 280, 320], + "notes": [ + "Use this when testing dense higher-resolution pixel art that already has a coherent lattice but still fools the current size inference search.", + "The tracked phase-field output was produced from a fixed 512x512 lattice and is kept around so README preview generation does not depend on rerunning that exact pass.", + "The current automatic lattice guess on the source fixture undershoots badly at 251x251, which makes this a useful regression case for future inference work." + ] +} diff --git a/tests/fixtures/real/dense-landscape.png b/tests/fixtures/real/dense-landscape.png new file mode 100644 index 0000000..74e9aae Binary files /dev/null and b/tests/fixtures/real/dense-landscape.png differ