From 20c610edee42daa1fbdb3a7f6ffb3ad9c901cb23 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 26 Apr 2026 23:40:43 +0100 Subject: [PATCH] Add hosted landing page --- notes/fresh-workspace-handoff.md | 2 +- src/repixelizer/gui.py | 281 +++++++++++++++++++++++++++++++ state/evidence.jsonl | 1 + state/map.yaml | 1 + tests/test_gui.py | 31 ++++ 5 files changed, 315 insertions(+), 1 deletion(-) diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 1c27a79..9e4c32b 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -38,7 +38,7 @@ Do not trust this file for the exact live HEAD. Always check git. - The old tray optimizer is dead and should stay dead unless the entire machine map changes for a real reason. - `E:\Projects\Heimdall\docs\architecture.md` is the future shared design for reusable GameCult-hosted access across experiments. - `E:\Projects\Heimdall\docs\app-profiles\repixelizer.md` is the Repixelizer-specific binding onto that future shared access layer. -- current hosted-demo truth is still just the queue and limit machinery in `src/repixelizer/gui.py` plus the frontend runtime-config plumbing. +- current hosted-demo truth lives in `src/repixelizer/gui.py`: queue and limit machinery, hosted direct-autocorr inference defaults, and a hosted-only landing page at `/` while self-host/local runs still redirect root straight to `/app/`. - product strategy is hosted convenience first: keep the code open, let self-hosters suffer voluntarily, and do not burn time on native desktop packaging unless it clearly beats hosting on revenue, support, or strategy. ## Critical Doctrine diff --git a/src/repixelizer/gui.py b/src/repixelizer/gui.py index 169d01b..7bc527b 100644 --- a/src/repixelizer/gui.py +++ b/src/repixelizer/gui.py @@ -861,6 +861,285 @@ def _versioned_gui_index(static_dir: Path) -> str: return html +def _landing_page_html(config: HostedDemoConfig) -> str: + demo_limits = ( + f"{config.max_output_dimension}px output cap, " + f"{config.default_steps} default solver steps, " + f"{config.queue_capacity} waiting slots" + ) + return f""" + + + + + Repixelizer + + + +
+
+
+
+

Repixelizer hosted demo

+

Force fake pixel art back onto a real grid.

+

+ Repixelizer takes AI-slop pixel cosplay, infers the lattice it should have had, then drags it back onto an actual coherent mosaic. +

+ +
+
+ One engine + Phase-field reconstruction. No pipeline graveyard. +
+
+ Hosted limits + {demo_limits} +
+
+ Lean inference + Autocorr lattice, cheap phase probe, then the real solve. +
+
+
+ +
+
+
+

How the machine behaves

+
+
+ 1. Read the grid +

Infer one plausible lattice from autocorrelation instead of wandering through a candidate carnival all day.

+
+
+ 2. Find the phase +

Probe a tiny phase grid up front so the solver does not begin life already standing in a pothole.

+
+
+ 3. Let the field work +

Run one displacement field with enough leash to recover detail instead of compensating with decorative nonsense.

+
+
+ +
+
+ + +""" + + def create_app(): deps = _require_gui_dependencies() FastAPI = deps["FastAPI"] @@ -1026,6 +1305,8 @@ def create_app(): @app.get("/") def root(): + if config.hosted_demo: + return HTMLResponse(_landing_page_html(config)) return RedirectResponse(url="/app/") else: # pragma: no cover - only relevant before frontend assets exist @app.get("/") diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 023360a..82e9067 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -11,3 +11,4 @@ {"ts": "2026-04-26T17:46:30+00:00", "type": "research", "status": "ok", "note": "Removed pixel-walk / change-interval sizing from src/repixelizer/inference.py and switched searched lattice inference to autocorrelation-only priors and candidate hints. Badge auto inference no longer gets pruned into the old 115-family by spacing-spectrum guidance; it now searches inside the 120-126 neighborhood while dense landscape still lands at 418x418."} {"ts": "2026-04-26T17:58:43+00:00", "type": "research", "status": "ok", "note": "Autocorr inference now keeps a tiny near-best lag plateau and searches for cross-axis consensus before collapsing to one lattice size. On the real badge fixture this preserves x-axis support for 10px alongside 11px, raises the shared prior to the 125-family, and restores auto inference to 125 with top candidates 125/126/124/127/123 while dense landscape remains 418x418."} {"ts": "2026-04-26T18:19:52+00:00", "type": "design", "status": "accepted", "note": "Hosted demo jobs now use direct autocorr lattice inference with one cheap phase probe, capped by the hosted output limit. The hosted path no longer silently pins unspecified jobs to max_output_dimension, and phase rerank is forced off there while the full local workflow keeps searched autocorr inference available."} +{"ts": "2026-04-26T22:40:02+00:00", "type": "design", "status": "accepted", "note": "Hosted mode now owns a distinct public front door in src/repixelizer/gui.py: / serves a landing page there, while local and self-host runs still redirect root straight to /app/. This keeps future auth and member-gating surgery in the hosted web layer instead of smearing it into the self-deploy path or the solver stack."} diff --git a/state/map.yaml b/state/map.yaml index fe4bc53..d7eea4d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -66,6 +66,7 @@ accepted_design: - `src/repixelizer/analysis.py` provides the normalized edge scout used for loss and cleanup guidance. - `src/repixelizer/phase_field.py` owns the one honest optimization variable: `disp_t`. - Hosted demo jobs now use a direct autocorr lattice path: one consensus size, one cheap phase probe, capped by the hosted output limit, with phase rerank disabled. + - Hosted mode owns the public front door in `src/repixelizer/gui.py`: `/` is a landing page there, while local/self-host runs keep root as a plain redirect to `/app/`. - The optimizer starts from fixed lattice centers, uses a wider displacement budget than the earlier tighter leash, optimizes displacement with projected Adam, then rounds to real source pixels for final output. - Cleanup is local and secondary; it does not define the core reconstruction model. - `source_fidelity` is useful but incomplete; `source_structure` exists because visible structure can improve while lattice-only scoring complains. diff --git a/tests/test_gui.py b/tests/test_gui.py index 84480e7..dfbe8f0 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -306,6 +306,37 @@ def test_gui_static_assets_disable_browser_caching() -> None: assert "./app.js?v=" in html_text +def test_gui_hosted_root_serves_landing_page(monkeypatch, tmp_path: Path) -> None: + from repixelizer.gui import create_app + + monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1") + monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool")) + + app = create_app() + status, headers, body = asyncio.run(_get_response(app, "/")) + html = body.decode("utf-8") + + assert status == 200 + assert headers["cache-control"] == "no-store, no-cache, must-revalidate, max-age=0" + assert "Repixelizer hosted demo" in html + assert "Force fake pixel art back onto a real grid." in html + assert 'href="/app/"' in html + + +def test_gui_local_root_keeps_redirect_to_app(monkeypatch, tmp_path: Path) -> None: + from repixelizer.gui import create_app + + monkeypatch.delenv("REPIXELIZER_HOSTED_DEMO", raising=False) + monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool")) + + app = create_app() + status, headers, _body = asyncio.run(_get_response(app, "/")) + + assert status in {302, 307} + assert headers["location"] == "/app/" + assert headers["cache-control"] == "no-store, no-cache, must-revalidate, max-age=0" + + def test_infer_lattice_emits_search_progress_events(monkeypatch) -> None: class _FakeCuda: @staticmethod