diff --git a/README.md b/README.md index 4e4b611..5ca1f3c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ diagnostics` In plainer language: -1. infer the target lattice size and phase +1. infer the target lattice size 2. build one edge scout map over the source 3. nail down fixed lattice centers 4. run the `phase-field` solve by optimizing one `(dx, dy)` shove vector per @@ -110,7 +110,7 @@ Run the optimizer from the CLI: ```powershell repixelize input.png --out output.png repixelize input.png --out output.png --diagnostics-dir diagnostics --device auto -repixelize input.png --out output.png --target-width 126 --target-height 126 --phase-x 0.0 --phase-y -0.2 --device cuda +repixelize input.png --out output.png --target-width 126 --target-height 126 --device cuda ``` Run the optimizer plus baselines: @@ -175,7 +175,7 @@ That keeps benchmark assets, attribution exports, and tuning runs from polluting Core modules: -- `inference.py`: target size and phase inference +- `inference.py`: target size inference - `phase_field.py`: default displacement-field optimizer - `metrics.py`: fidelity, adjacency, and motif metrics - `benchmark.py`: corpus benchmark runner diff --git a/config/solver_params.json b/config/solver_params.json new file mode 100644 index 0000000..126b0fc --- /dev/null +++ b/config/solver_params.json @@ -0,0 +1,34 @@ +{ + "phase_field_grid_alignment_weight": 4.0, + "phase_field_local_search_radius_ratio": 0.8, + "phase_field_local_search_blend": 0.18, + "phase_field_local_search_move_weight": 0.16, + "phase_field_local_search_grid_weight": 0.0, + "phase_field_signal_weight": 0.25, + "phase_field_signal_inner_radius_ratio": 0.24, + "phase_field_signal_outer_radius_ratio": 0.56, + "phase_field_signal_ring_contrast_weight": 1.0, + "phase_field_signal_inner_variance_weight": 1.0, + "phase_field_signal_center_edge_weight": 0.12, + "phase_field_signal_edge_threshold": 0.16, + "phase_field_signal_detail_radius_ratio": 0.8, + "phase_field_signal_flatness_radius_ratio": 0.24, + "phase_field_signal_distance_power": 0.7, + "phase_field_signal_pyramid_levels": 5, + "phase_field_signal_boundary_weight": 1.0, + "phase_field_signal_curvature_weight": 0.65, + "phase_field_signal_self_penalty": 3.0, + "phase_field_signal_context_power": 0.7, + "phase_field_signal_peak_power": 2.0, + "phase_field_local_search_interval": 1, + "candidate_rerank_preview_steps": 8, + "candidate_rerank_support_weight": 0.45, + "candidate_rerank_edge_position_weight": 0.2, + "candidate_rerank_wobble_weight": 0.2, + "candidate_rerank_edge_concentration_weight": 0.1, + "candidate_rerank_size_penalty_weight": 0.18, + "candidate_rerank_inference_penalty_weight": 0.05, + "candidate_rerank_confidence_threshold": 0.12, + "candidate_rerank_max_size_delta_ratio": 0.4, + "candidate_rerank_margin": 0.004 +} diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 016ce2a..18fbc5a 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -23,9 +23,9 @@ The live pipeline is: Inference now splits three ways: -- fixed lattice when the caller pins size and/or phase explicitly +- fixed lattice when the caller pins size explicitly - searched lattice for the full local workflow -- direct autocorr lattice for the hosted demo path: one consensus size, one cheap phase probe, no size search, no phase rerank +- direct autocorr lattice for the hosted demo path: one consensus size, no size search, no candidate rerank The hosted web shell now also has an auth seam: @@ -36,7 +36,7 @@ The hosted web shell now also has an auth seam: ## What is working -- lattice size and phase inference are still shared and still CUDA-capable +- lattice size inference are still shared and still CUDA-capable - `phase-field` is the only canonical reconstruction path and produces the best-looking badge result in the repo so far - compare mode, benchmark mode, diagnostics writing, and tuning all still work after the optimizer cutover - the repo now reports both: @@ -53,7 +53,7 @@ Pinned badge case: - fixture: `tests/fixtures/real/ai-badge-cleaned.png` - lattice: `126x126` -- phase: `(0.0, -0.2)` +- initial lattice centers: canonical - steps: `48` Useful artifacts: @@ -108,17 +108,17 @@ Current stance: - keep the shipped searched path grounded in autocorrelation only - let autocorr keep a small near-best lag plateau and use cross-axis consensus before collapsing to one lattice family -- the hosted demo path now skips multi-size search entirely and uses one autocorr-consensus size plus the cheap inferred phase, capped by the hosted output limit +- the hosted demo path now skips multi-size search entirely and uses one autocorr-consensus size , capped by the hosted output limit - do not trust the old pixel-walk / change-interval spacing path; it was cut after it kept pruning the badge away from the `126` family - do not keep the projected edge-energy spectral pass; it floor-hugged to `2px` / `626`-family nonsense on the dense landscape fixture and did not earn its keep -- treat phase rerank as optional scaffolding, not sacred machinery +- treat candidate rerank as optional scaffolding, not sacred machinery Future options worth exploring, if autocorr stops carrying its weight: - distance-transform / medial-radius blob sizing on softened interiors - small scale-space blob-size detection (`LoG` / `DoG`) - blur-aware correction of observed blob width before converting to lattice size -- tiny learned size-and-phase prior trained on synthetic fake-pixel-art fixtures +- tiny learned size prior trained on synthetic fake-pixel-art fixtures ### 3. Keep phase-field honest diff --git a/docs/lean-optimizer-algorithm-map.md b/docs/lean-optimizer-algorithm-map.md index efd418a..82276c5 100644 --- a/docs/lean-optimizer-algorithm-map.md +++ b/docs/lean-optimizer-algorithm-map.md @@ -24,7 +24,7 @@ The solver nails a regular grid over the source, gives every output cell a tiny These are the pieces that actually matter: -- `inference`: chosen lattice width, height, phase, confidence, and optional top candidates +- `inference`: chosen lattice width, height, confidence, and optional top candidates - `edge_map`: one normalized scout map of where the source image has strong luminance/alpha changes - `uv0_px`: the fixed source-space center for each output cell before any optimization - `disp_t`: the live displacement field, one `(dx, dy)` vector per output cell @@ -65,22 +65,20 @@ That means the solver is not maintaining alternate per-cell candidate sets or se - `run_pipeline(...)` in `src/repixelizer/pipeline.py` - `_resolve_requested_target_dims(...)` in `src/repixelizer/pipeline.py` - `infer_lattice(...)` and `infer_fixed_lattice(...)` in `src/repixelizer/inference.py` -- `_select_phase_candidate_with_reconstruction(...)` in `src/repixelizer/pipeline.py` +- `_select_candidate_with_reconstruction(...)` in `src/repixelizer/pipeline.py` ### Inputs - source RGBA image -- optional pinned target size / width / height / phase -- optional `--skip-phase-rerank` +- optional pinned target size / width / height +- optional `--skip-candidate-rerank` ### Outputs - `InferenceResult` - `target_width` - `target_height` - - `phase_x` - - `phase_y` - - `confidence` + - `confidence` - `top_candidates` ### What the source actually does @@ -88,17 +86,17 @@ That means the solver is not maintaining alternate per-cell candidate sets or se The pipeline first decides whether the lattice is: - fixed explicitly by the caller, using `infer_fixed_lattice(...)`, or -- inferred directly from autocorr consensus with one cheap phase probe, using `infer_autocorr_lattice(...)`, or +- inferred directly from autocorr consensus, using `infer_autocorr_lattice(...)`, or - searched automatically by `infer_lattice(...)` The two automatic paths split like this: - both paths estimate rough cell spacing from source edge-profile autocorrelation - both paths keep a tiny near-best lag plateau long enough to find cross-axis consensus instead of collapsing instantly to one lag -- `infer_autocorr_lattice(...)` takes one consensus size, scores one small phase grid, and stops there -- `infer_lattice(...)` still builds a small candidate size family, scores a small phase grid for each size, and keeps the best candidate plus top alternates +- `infer_autocorr_lattice(...)` takes one consensus size, scores the canonical center-grid size check, and stops there +- `infer_lattice(...)` still builds a small candidate size family, scores the canonical center-grid size check for each size, and keeps the best candidate plus top alternates -After that, phase rerank may still happen, but only for `phase-field`, only when confidence is low, and only when rerank is enabled. The rerank probe is a short preview solve. It calls `_run_reconstruction(...)` with a small bounded step count, capped by both the requested solver steps and `phase_rerank_preview_steps`. +After that, candidate rerank may still happen, but only for `phase-field`, only when confidence is low, and only when rerank is enabled. The rerank probe is a short preview solve. It calls `_run_reconstruction(...)` with a small bounded step count, capped by both the requested solver steps and `candidate_rerank_preview_steps`. That part matters. The pipeline is effectively asking: @@ -189,7 +187,7 @@ Prep does a few brutally practical things: 2. Builds the fixed lattice centers with `_make_regular_uv_px(...)`. - `cell_x = source_width / target_width` - `cell_y = source_height / target_height` - - `phase_x` and `phase_y` shift those centers before clipping to image bounds + - centers are placed at the canonical middle of each inferred output cell before clipping to image bounds 3. Converts the source, edge map, and lattice centers to torch tensors. @@ -339,7 +337,7 @@ Code: Meaning: -The field pays a small tax for wandering too far. Motion has to earn its keep, but the leash is long enough to recover from moderate phase error instead of dying half a cell short of the right blob. +The field pays a small tax for wandering too far. Motion has to earn its keep, but the leash is long enough to recover from moderate starting-grid error instead of dying half a cell short of the right blob. ### Metaphor @@ -528,7 +526,7 @@ The solver also reports displacement statistics like: - local residual - dominant offset ratio -Those are the closest thing this machine has to an X-ray of its phase drift. +Those are the closest thing this machine has to an X-ray of its field drift. ### Metaphor @@ -551,7 +549,7 @@ These pieces all belong to the same model of the machine. ## Current seams -- low-confidence phase rerank uses only a short preview solve, not the full optimized field +- low-confidence candidate rerank uses only a short preview solve, not the full optimized field - the edge scout only knows edge magnitude, not direction, so the solver still struggles with along-stroke versus across-stroke behavior near tapered contours These are the places where the current implementation is still a little awkward or incomplete. diff --git a/docs/spec.md b/docs/spec.md index bc9e50b..0399837 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -57,7 +57,7 @@ Repixelizer treats this as lattice inference plus reconstruction rather than a r The canonical workflow should be fully automatic: 1. load the source image -2. infer lattice size and phase +2. infer lattice size 3. analyze source structure 4. run the `phase-field` reconstruction over the inferred lattice 5. project that result to a real pixel grid @@ -104,10 +104,10 @@ The tool must estimate the resolution of the fake lattice being mimicked. Requirements: - search over candidate target sizes -- search over subpixel phase offsets for each candidate +- use canonical cell centers for each candidate - score candidates using coherence-oriented metrics rather than reconstruction alone - expose the top-ranked alternatives in diagnostics -- if `--target-size` is provided, skip size search but still estimate phase +- if `--target-size` is provided, skip size search Scoring goals: - low isolated-pixel rate diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 86ebf2d..5403994 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -9,8 +9,6 @@ type ImageAsset = { type InferenceCandidate = { target_width: number; target_height: number; - phase_x: number; - phase_y: number; score: number; breakdown: Record; }; @@ -18,8 +16,6 @@ type InferenceCandidate = { type InferencePayload = { target_width: number; target_height: number; - phase_x: number; - phase_y: number; confidence: number; top_candidates: InferenceCandidate[]; }; @@ -29,7 +25,7 @@ type FramePayload = { totalSteps: number; loss: number | null; terms: Record; - phaseMetrics: Record; + solverMetrics: Record; outputImage: ImageAsset; samplingOverlayImage: ImageAsset; displacementImage: ImageAsset; @@ -84,14 +80,13 @@ type JobStateSnapshot = { type LatticeSearchInfo = { candidateCount: number; completedCandidates: number; - phaseSampleCount: number; device: string | null; currentTargetWidth: number | null; currentTargetHeight: number | null; bestScore: number | null; }; -type PhaseRerankInfo = { +type candidateRerankInfo = { previewSteps: number; candidateCount: number; confidence: number; @@ -99,8 +94,6 @@ type PhaseRerankInfo = { currentCandidateIndex: number; currentTargetWidth: number | null; currentTargetHeight: number | null; - currentPhaseX: number | null; - currentPhaseY: number | null; currentStep: number; currentTotalSteps: number; currentLoss: number | null; @@ -134,7 +127,7 @@ type AppState = { inference: InferencePayload | null; inferenceMode: string | null; latticeSearch: LatticeSearchInfo | null; - phaseRerank: PhaseRerankInfo | null; + candidateRerank: candidateRerankInfo | null; phaseFieldPrep: PhaseFieldPrepInfo | null; frames: FramePayload[]; cleanupImage: ImageAsset | null; @@ -207,8 +200,6 @@ const downloadButton = byId("downloadButton"); const targetSizeInput = byId("targetSizeInput"); const targetWidthInput = byId("targetWidthInput"); const targetHeightInput = byId("targetHeightInput"); -const phaseXInput = byId("phaseXInput"); -const phaseYInput = byId("phaseYInput"); const stepsInput = byId("stepsInput"); const seedInput = byId("seedInput"); const runControlsCopy = byId("runControlsCopy"); @@ -240,11 +231,11 @@ const eventTypes = [ "lattice_search_started", "lattice_search_progress", "inference_candidates_ready", - "phase_rerank_started", - "phase_rerank_candidate_started", - "phase_rerank_candidate_step", - "phase_rerank_candidate_completed", - "phase_selection_completed", + "candidate_rerank_started", + "candidate_rerank_candidate_started", + "candidate_rerank_candidate_step", + "candidate_rerank_candidate_completed", + "candidate_selection_completed", "analysis_completed", "phase_field_prepared", "phase_field_initial", @@ -269,7 +260,7 @@ const state: AppState = { inference: null, inferenceMode: null, latticeSearch: null, - phaseRerank: null, + candidateRerank: null, phaseFieldPrep: null, frames: [], cleanupImage: null, @@ -515,7 +506,7 @@ function resetRunArtifacts(options: { preserveSourceImage?: boolean } = {}): voi state.inference = null; state.inferenceMode = null; state.latticeSearch = null; - state.phaseRerank = null; + state.candidateRerank = null; state.phaseFieldPrep = null; state.frames = []; state.cleanupImage = null; @@ -687,13 +678,6 @@ function formatGridValue(width: number | null | undefined, height: number | null return `${width} x ${height}`; } -function formatPhaseValue(phaseX: number | null | undefined, phaseY: number | null | undefined, fallback = "pending"): string { - if (phaseX === null || phaseX === undefined || phaseY === null || phaseY === undefined) { - return fallback; - } - return `${formatNumber(phaseX, 2)}, ${formatNumber(phaseY, 2)}`; -} - function renderStatusMetricItems(items: StatusMetricItem[]): void { statusMetrics.innerHTML = ""; statusMetrics.style.setProperty("--status-metric-count", String(Math.max(items.length, 1))); @@ -726,7 +710,6 @@ function renderInference(): void {
Phase - ${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}
Confidence @@ -738,10 +721,9 @@ function renderInference(): void { for (const candidate of topCandidates) { const node = document.createElement("div"); node.className = "info-card candidate-card"; - const rerank = candidate.breakdown["phase_rerank_rank"]; + const rerank = candidate.breakdown["candidate_rerank_rank"]; node.innerHTML = ` ${candidate.target_width} x ${candidate.target_height} - phase ${formatNumber(candidate.phase_x, 2)}, ${formatNumber(candidate.phase_y, 2)}
score ${formatNumber(candidate.score, 3)}${typeof rerank === "number" ? ` • rerank #${Math.round(rerank)}` : ""} `; candidateList.appendChild(node); @@ -769,7 +751,6 @@ function renderStatusMetrics(): void { } else if (state.stageKey === "inference") { if (state.latticeSearch) { items.push({ label: "Sizes", value: `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}` }); - items.push({ label: "Phases", value: String(state.latticeSearch.phaseSampleCount) }); items.push({ label: "Grid", value: formatGridValue(state.latticeSearch.currentTargetWidth, state.latticeSearch.currentTargetHeight), @@ -781,41 +762,36 @@ function renderStatusMetrics(): void { if (state.inference) { items.push({ label: "Mode", value: state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search" }); items.push({ label: "Grid", value: `${state.inference.target_width} x ${state.inference.target_height}` }); - items.push({ label: "Phase", value: `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}` }); items.push({ label: "Confidence", value: formatNumber(state.inference.confidence, 3) }); } } else if (state.stageKey === "rerank") { - if (state.phaseRerank) { + if (state.candidateRerank) { items.push({ label: "Done", - value: `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`, + value: `${state.candidateRerank.completedCandidates} / ${state.candidateRerank.candidateCount}`, }); items.push({ label: "Candidate", value: - state.phaseRerank.currentCandidateIndex > 0 - ? `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}` - : `0 / ${state.phaseRerank.candidateCount}`, + state.candidateRerank.currentCandidateIndex > 0 + ? `${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount}` + : `0 / ${state.candidateRerank.candidateCount}`, }); items.push({ label: "Preview", value: - state.phaseRerank.currentTotalSteps > 0 - ? `${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps}` - : state.phaseRerank.previewSteps > 0 - ? `0 / ${state.phaseRerank.previewSteps}` + state.candidateRerank.currentTotalSteps > 0 + ? `${state.candidateRerank.currentStep} / ${state.candidateRerank.currentTotalSteps}` + : state.candidateRerank.previewSteps > 0 + ? `0 / ${state.candidateRerank.previewSteps}` : "skipped", }); items.push({ label: "Grid", - value: formatGridValue(state.phaseRerank.currentTargetWidth, state.phaseRerank.currentTargetHeight), + value: formatGridValue(state.candidateRerank.currentTargetWidth, state.candidateRerank.currentTargetHeight), }); - items.push({ - label: "Phase", - value: formatPhaseValue(state.phaseRerank.currentPhaseX, state.phaseRerank.currentPhaseY), - }); - items.push({ label: "Loss", value: formatNumber(state.phaseRerank.currentLoss, 4) }); - items.push({ label: "Confidence", value: formatNumber(state.phaseRerank.confidence, 3) }); + items.push({ label: "Loss", value: formatNumber(state.candidateRerank.currentLoss, 4) }); + items.push({ label: "Confidence", value: formatNumber(state.candidateRerank.confidence, 3) }); } } else if (state.stageKey === "solver") { const solverTerms: StatusMetricItem[] = [ @@ -1513,13 +1489,11 @@ function buildFormData(): FormData { ["target_size", parseOptionalInteger(targetSizeInput)?.toString() ?? null], ["target_width", parseOptionalInteger(targetWidthInput)?.toString() ?? null], ["target_height", parseOptionalInteger(targetHeightInput)?.toString() ?? null], - ["phase_x", parseOptionalFloat(phaseXInput)?.toString() ?? null], - ["phase_y", parseOptionalFloat(phaseYInput)?.toString() ?? null], ["steps", String(normalizedSteps)], ["seed", String(parseOptionalInteger(seedInput) ?? 7)], ["device", runtimeConfig?.hostedDemo ? "cpu" : deviceInput.value], ["strip_background", runtimeConfig?.hostedDemo ? "false" : stripBackgroundInput.checked ? "true" : "false"], - ["skip_phase_rerank", skipRerankInput.checked ? "true" : "false"], + ["skip_candidate_rerank", skipRerankInput.checked ? "true" : "false"], ]; for (const [key, value] of values) { if (value !== null) { @@ -1591,7 +1565,6 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`. - searched lattice inference now trusts autocorrelation only; it keeps a tiny near-best lag plateau and looks for cross-axis consensus before collapsing to one lattice family. the old change-interval / pixel-walk sizing path was cut after it kept steering the badge away from the `126` family, and the projected edge-energy spectral pass was also removed after floor-hugging to 2px / 626-family aliases on the dense landscape fixture -- hosted demo inference now uses one direct autocorr-consensus size plus the cheap inferred phase, capped by the hosted output limit. it no longer silently pins unspecified jobs to `max_output_dimension` +- hosted demo inference now uses one direct autocorr-consensus size, capped by the hosted output limit. it no longer silently pins unspecified jobs to `max_output_dimension` - hosted web access now has an explicit seam in `src/repixelizer/access.py`; `src/repixelizer/gui.py` binds request subjects, guards hosted routes, and stamps queued jobs with local owner metadata - `GC_ACCESS_MODE=heimdall` is now landed in that web layer: Repixelizer creates auth attempts, asks Heimdall to start provider OAuth, accepts the direct backend callback, verifies Heimdall Ed25519 access tokens locally, and adopts an httpOnly local session cookie -- The low-confidence phase rerank path is a short preview solve, not a second full optimizer. -- the phase field now has a wider displacement budget, so moderate phase mistakes are allowed to get corrected by the field instead of relying purely on rerank ceremony +- The low-confidence candidate rerank path is a short preview solve, not a second full optimizer. +- the phase field now has a wider displacement budget, so moderate starting-grid mistakes are allowed to get corrected by the field instead of relying purely on candidate-rerank ceremony - Cleanup is secondary and usually a no-op; the core result is supposed to come from the solver, not cleanup cosplay. - The current tracked weakness is the widened dark contour near the badge sword tip on tapered shapes. - `source_structure` exists because lattice-only `source_fidelity` could call visibly better outputs worse. @@ -57,10 +57,10 @@ The current spine: - lattice inference with fixed-size and searched-size paths - searched lattice inference currently trusts autocorrelation only, not the rejected pixel-walk or spectral priors -- hosted demo inference uses the direct autocorr path and keeps phase rerank off +- hosted demo inference uses the direct autocorr path and keeps candidate rerank off - hosted route auth belongs in `src/repixelizer/access.py` + `src/repixelizer/gui.py`, not in the solver or pipeline - the first Heimdall consumer path is live there: provider buttons on the landing page, direct backend callback handoff, local JWT verification, and cookie adoption -- low-confidence phase rerank through short preview reconstruction +- low-confidence candidate rerank through short preview reconstruction - edge analysis feeding one projected displacement-field optimizer with a wider displacement leash - nearest-source final sampling from `uv0_px + disp_t` - cleanup, optional palette fit, diagnostics, compare mode, tuning, and GUI observer events on the live path diff --git a/scripts/generate_readme_previews.py b/scripts/generate_readme_previews.py index 8cfebf7..9ac17b6 100644 --- a/scripts/generate_readme_previews.py +++ b/scripts/generate_readme_previews.py @@ -64,7 +64,7 @@ def _parse_args() -> argparse.Namespace: 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.", + help="Tracked fixed-lattice output used for the dense landscape row.", ) parser.add_argument( "--guard-cell-bbox", @@ -93,20 +93,7 @@ def _parse_args() -> argparse.Namespace: type=int, default=126, help="Pinned target height for the canonical badge comparison.", - ) - parser.add_argument( - "--phase-x", - type=float, - default=0.0, - help="Pinned lattice phase X for the canonical badge comparison.", - ) - parser.add_argument( - "--phase-y", - type=float, - default=-0.2, - help="Pinned lattice phase Y for the canonical badge comparison.", - ) - parser.add_argument( + ) parser.add_argument( "--dense-target-width", type=int, default=512, @@ -393,12 +380,10 @@ def main() -> None: scratch_dir / "badge-ai-phase-field-pinned.png", target_width=args.target_width, target_height=args.target_height, - phase_x=args.phase_x, - phase_y=args.phase_y, diagnostics_dir=scratch_dir / "phase-field-diag", steps=args.steps, device=args.device, - enable_phase_rerank=False, + enable_candidate_rerank=False, ) source_rgba = load_rgba(source_input) @@ -429,7 +414,7 @@ def main() -> None: 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})", + title=f"AI fake pixel art -> {args.target_width}x{args.target_height} pinned lattice", source_rgba=source_rgba, source_subtitle="Cleaned AI fake pixel art", lanczos_rgba=lanczos, diff --git a/scripts/render_focus_crop.py b/scripts/render_focus_crop.py index 5f8194c..c5ad35a 100644 --- a/scripts/render_focus_crop.py +++ b/scripts/render_focus_crop.py @@ -98,16 +98,12 @@ def _build_focus_states( artifacts.initial_rgba, target_width=inference.target_width, target_height=inference.target_height, - phase_x=inference.phase_x, - phase_y=inference.phase_y, ), "final": source_lattice_consistency_breakdown( source, artifacts.target_rgba, target_width=inference.target_width, target_height=inference.target_height, - phase_x=inference.phase_x, - phase_y=inference.phase_y, ), } return source, artifacts.initial_rgba, artifacts.target_rgba, (cell_x, cell_y), fidelity diff --git a/scripts/run_hosted_gui.ps1 b/scripts/run_hosted_gui.ps1 new file mode 100644 index 0000000..166d979 --- /dev/null +++ b/scripts/run_hosted_gui.ps1 @@ -0,0 +1,61 @@ +param( + [string]$BindHost = "0.0.0.0", + [int]$Port = 8000, + [switch]$Reload, + [ValidateSet("auto", "show", "hide")] + [string]$QueueUI = "auto" +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$python = Join-Path $repoRoot ".venv\Scripts\python.exe" +if (-not (Test-Path $python)) { + $python = "python" +} + +$previousHostedDemo = $env:REPIXELIZER_HOSTED_DEMO +$previousShowQueuePanel = $env:REPIXELIZER_SHOW_QUEUE_PANEL + +try { + $env:REPIXELIZER_HOSTED_DEMO = "1" + + if ($QueueUI -eq "show") { + $env:REPIXELIZER_SHOW_QUEUE_PANEL = "1" + } elseif ($QueueUI -eq "hide") { + $env:REPIXELIZER_SHOW_QUEUE_PANEL = "0" + } else { + Remove-Item Env:\REPIXELIZER_SHOW_QUEUE_PANEL -ErrorAction SilentlyContinue + } + + $launcherArgs = @( + (Join-Path $repoRoot "scripts\run_gui.py"), + "--host", $BindHost, + "--port", $Port, + "--queue-ui", $QueueUI + ) + + if ($Reload) { + $launcherArgs += "--reload" + } + + Write-Host "Starting Repixelizer hosted GUI..." + Write-Host " Hosted demo: $env:REPIXELIZER_HOSTED_DEMO" + Write-Host " Bind: http://${BindHost}:${Port}" + Write-Host " Python: $python" + Write-Host "" + + & $python @launcherArgs +} finally { + if ($null -eq $previousHostedDemo) { + Remove-Item Env:\REPIXELIZER_HOSTED_DEMO -ErrorAction SilentlyContinue + } else { + $env:REPIXELIZER_HOSTED_DEMO = $previousHostedDemo + } + + if ($null -eq $previousShowQueuePanel) { + Remove-Item Env:\REPIXELIZER_SHOW_QUEUE_PANEL -ErrorAction SilentlyContinue + } else { + $env:REPIXELIZER_SHOW_QUEUE_PANEL = $previousShowQueuePanel + } +} diff --git a/src/repixelizer/benchmark.py b/src/repixelizer/benchmark.py index 1366b86..9985b03 100644 --- a/src/repixelizer/benchmark.py +++ b/src/repixelizer/benchmark.py @@ -126,8 +126,8 @@ def run_roundtrip_benchmark( "input_width": int(fake.shape[1]), "input_height": int(fake.shape[0]), "upscale": settings["upscale"], - "phase_x": settings["phase_x"], - "phase_y": settings["phase_y"], + "offset_x": settings["offset_x"], + "offset_y": settings["offset_y"], "blur_radius": settings["blur_radius"], "warp_strength": settings["warp_strength"], "warp_detail": settings["warp_detail"], @@ -213,8 +213,8 @@ def _variant_settings(seed: int, profile: str) -> dict[str, Any]: if profile == "crisp": return { "upscale": int(rng.integers(8, 15)), - "phase_x": float(rng.uniform(-0.45, 0.45)), - "phase_y": float(rng.uniform(-0.45, 0.45)), + "offset_x": float(rng.uniform(-0.45, 0.45)), + "offset_y": float(rng.uniform(-0.45, 0.45)), "blur_radius": 0.0, "warp_strength": float(rng.uniform(0.14, 0.46)), "warp_detail": int(rng.integers(5, 9)), @@ -225,8 +225,8 @@ def _variant_settings(seed: int, profile: str) -> dict[str, Any]: if profile == "soft": return { "upscale": int(rng.integers(8, 15)), - "phase_x": float(rng.uniform(-0.45, 0.45)), - "phase_y": float(rng.uniform(-0.45, 0.45)), + "offset_x": float(rng.uniform(-0.45, 0.45)), + "offset_y": float(rng.uniform(-0.45, 0.45)), "blur_radius": float(rng.uniform(0.45, 1.15)), "warp_strength": float(rng.uniform(0.18, 0.55)), "warp_detail": int(rng.integers(4, 8)), @@ -237,8 +237,8 @@ def _variant_settings(seed: int, profile: str) -> dict[str, Any]: if profile == "ai": return { "upscale": int(rng.integers(8, 15)), - "phase_x": float(rng.uniform(-0.48, 0.48)), - "phase_y": float(rng.uniform(-0.48, 0.48)), + "offset_x": float(rng.uniform(-0.48, 0.48)), + "offset_y": float(rng.uniform(-0.48, 0.48)), "blur_radius": float(rng.uniform(0.18, 0.95)), "warp_strength": float(rng.uniform(0.24, 0.68)), "warp_detail": int(rng.integers(5, 10)), diff --git a/src/repixelizer/cli.py b/src/repixelizer/cli.py index 5d7cbc6..fcfe502 100644 --- a/src/repixelizer/cli.py +++ b/src/repixelizer/cli.py @@ -21,8 +21,6 @@ def build_parser() -> argparse.ArgumentParser: target.add_argument("--target-size", type=int, default=None, help="Override the inferred target max dimension") target.add_argument("--target-width", type=int, default=None, help="Pin the output lattice width directly") target.add_argument("--target-height", type=int, default=None, help="Pin the output lattice height directly") - target.add_argument("--phase-x", type=float, default=None, help="Pin the lattice phase offset on the x axis") - target.add_argument("--phase-y", type=float, default=None, help="Pin the lattice phase offset on the y axis") target.add_argument("--palette", default=None, help="Optional palette file (.gpl, .txt, .json)") target.add_argument( "--palette-mode", @@ -40,9 +38,10 @@ def build_parser() -> argparse.ArgumentParser: help="Remove light neutral edge-connected backgrounds such as fake transparency checkerboards", ) target.add_argument( - "--skip-phase-rerank", + "--skip-candidate-rerank", + dest="skip_candidate_rerank", action="store_true", - help="Skip low-confidence phase reranking and run the selected or pinned lattice directly", + help="Skip low-confidence candidate reranking and run the selected or pinned lattice directly", ) run_parser = subparsers.add_parser("run", help="Run the optimizer.") @@ -105,7 +104,7 @@ def build_parser() -> argparse.ArgumentParser: "--limit", type=int, default=None, - help="Only tune on the first N matching cases. Defaults to 8 when no cases are specified.", + help="Only tune on the first N matching cases. Defaults to the full matching corpus.", ) tune_parser.add_argument( "--infer-size", @@ -135,8 +134,6 @@ def main(argv: list[str] | None = None) -> int: target_size=args.target_size, target_width=args.target_width, target_height=args.target_height, - phase_x=args.phase_x, - phase_y=args.phase_y, palette_path=args.palette, palette_mode=args.palette_mode, diagnostics_dir=args.diagnostics_dir, @@ -144,7 +141,7 @@ def main(argv: list[str] | None = None) -> int: steps=args.steps, device=args.device, strip_background=args.strip_background, - enable_phase_rerank=not args.skip_phase_rerank, + enable_candidate_rerank=not args.skip_candidate_rerank, ) return 0 if command == "benchmark": @@ -189,8 +186,6 @@ def main(argv: list[str] | None = None) -> int: target_size=args.target_size, target_width=args.target_width, target_height=args.target_height, - phase_x=args.phase_x, - phase_y=args.phase_y, palette_path=args.palette, palette_mode=args.palette_mode, diagnostics_dir=args.diagnostics_dir, @@ -198,6 +193,6 @@ def main(argv: list[str] | None = None) -> int: steps=args.steps, device=args.device, strip_background=args.strip_background, - enable_phase_rerank=not args.skip_phase_rerank, + enable_candidate_rerank=not args.skip_candidate_rerank, ) return 0 diff --git a/src/repixelizer/compare.py b/src/repixelizer/compare.py index c536050..b4e2f37 100644 --- a/src/repixelizer/compare.py +++ b/src/repixelizer/compare.py @@ -61,8 +61,6 @@ def run_compare( target_size: int | None = None, target_width: int | None = None, target_height: int | None = None, - phase_x: float | None = None, - phase_y: float | None = None, palette_path: str | Path | None = None, palette_mode: str = "off", diagnostics_dir: str | Path | None = None, @@ -70,7 +68,7 @@ def run_compare( steps: int = 200, device: str = "auto", strip_background: bool = False, - enable_phase_rerank: bool = True, + enable_candidate_rerank: bool = True, ) -> dict[str, Any]: diagnostics_path = Path(diagnostics_dir) if diagnostics_dir else Path(output_path).with_suffix("") diagnostics_path.mkdir(parents=True, exist_ok=True) @@ -80,8 +78,6 @@ def run_compare( target_size=target_size, target_width=target_width, target_height=target_height, - phase_x=phase_x, - phase_y=phase_y, palette_path=palette_path, palette_mode=palette_mode, diagnostics_dir=diagnostics_path, @@ -89,7 +85,7 @@ def run_compare( steps=steps, device=device, strip_background=strip_background, - enable_phase_rerank=enable_phase_rerank, + enable_candidate_rerank=enable_candidate_rerank, ) source = result.source_rgba palette = load_palette(palette_path) if palette_path else None diff --git a/src/repixelizer/diagnostics.py b/src/repixelizer/diagnostics.py index c33dec7..bbe3538 100644 --- a/src/repixelizer/diagnostics.py +++ b/src/repixelizer/diagnostics.py @@ -29,13 +29,11 @@ def write_lattice_overlay(path: str | Path, source_rgba: np.ndarray, inference: height, width = source_rgba.shape[:2] cell_x = width / inference.target_width cell_y = height / inference.target_height - phase_x = inference.phase_x * cell_x - phase_y = inference.phase_y * cell_y for ix in range(inference.target_width + 1): - x = ix * cell_x + phase_x + x = ix * cell_x draw.line((x, 0, x, height), fill=(255, 255, 255, 96), width=1) for iy in range(inference.target_height + 1): - y = iy * cell_y + phase_y + y = iy * cell_y draw.line((0, y, width, y), fill=(255, 255, 255, 96), width=1) image.save(path) @@ -156,41 +154,33 @@ def summarize_run(result: RunResult) -> dict[str, Any]: result.solver.initial_rgba, target_width=result.inference.target_width, target_height=result.inference.target_height, - phase_x=result.inference.phase_x, - phase_y=result.inference.phase_y, ), "solver_target": source_lattice_consistency_breakdown( result.source_rgba, result.solver.target_rgba, target_width=result.inference.target_width, target_height=result.inference.target_height, - phase_x=result.inference.phase_x, - phase_y=result.inference.phase_y, ), "final_output": source_lattice_consistency_breakdown( result.source_rgba, result.output_rgba, target_width=result.inference.target_width, target_height=result.inference.target_height, - phase_x=result.inference.phase_x, - phase_y=result.inference.phase_y, ), } rerank_candidates = [ { "target_width": candidate.target_width, "target_height": candidate.target_height, - "phase_x": candidate.phase_x, - "phase_y": candidate.phase_y, "score": candidate.score, - "phase_rerank_score": candidate.breakdown.get("phase_rerank_score"), - "phase_rerank_rank": candidate.breakdown.get("phase_rerank_rank"), - "phase_rerank_support_score": candidate.breakdown.get("phase_rerank_support_score"), - "phase_rerank_size_delta_ratio": candidate.breakdown.get("phase_rerank_size_delta_ratio"), - "phase_rerank_size_penalty": candidate.breakdown.get("phase_rerank_size_penalty"), + "candidate_rerank_score": candidate.breakdown.get("candidate_rerank_score"), + "candidate_rerank_rank": candidate.breakdown.get("candidate_rerank_rank"), + "candidate_rerank_support_score": candidate.breakdown.get("candidate_rerank_support_score"), + "candidate_rerank_size_delta_ratio": candidate.breakdown.get("candidate_rerank_size_delta_ratio"), + "candidate_rerank_size_penalty": candidate.breakdown.get("candidate_rerank_size_penalty"), } for candidate in result.inference.top_candidates - if "phase_rerank_score" in candidate.breakdown + if "candidate_rerank_score" in candidate.breakdown ] displacement_metrics = {} stage_diagnostics = result.solver.stage_diagnostics.get("displacements", {}) @@ -203,8 +193,6 @@ def summarize_run(result: RunResult) -> dict[str, Any]: return { "target_width": result.inference.target_width, "target_height": result.inference.target_height, - "phase_x": result.inference.phase_x, - "phase_y": result.inference.phase_y, "confidence": result.inference.confidence, "coherence": coherence_breakdown(result.output_rgba), "source_preview_reconstruction_error": reconstruction_error(source_preview, result.source_rgba), @@ -212,7 +200,7 @@ def summarize_run(result: RunResult) -> dict[str, Any]: "output_colors_from_source_ratio": _source_color_ratio(result.source_rgba, result.output_rgba), "source_fidelity": source_fidelity, "source_structure": source_structure_breakdown(result.source_rgba, result.output_rgba), - "phase_rerank_candidates": rerank_candidates, + "candidate_rerank_candidates": rerank_candidates, "loss_history": result.solver.loss_history, "optimizer_displacement": displacement_metrics, } diff --git a/src/repixelizer/discrete.py b/src/repixelizer/discrete.py index 02a7f97..f6b23aa 100644 --- a/src/repixelizer/discrete.py +++ b/src/repixelizer/discrete.py @@ -29,6 +29,7 @@ def cleanup_pixels(rgba: np.ndarray, source_guidance: np.ndarray, iterations: in height, width = result.shape[:2] heatmap = np.zeros((height, width), dtype=np.float32) if iterations <= 0: + result = _snap_alpha(result) return CleanupArtifacts(cleaned_rgba=result, isolated_heatmap=heatmap) for _ in range(iterations): updated = result.copy() @@ -57,4 +58,13 @@ def cleanup_pixels(rgba: np.ndarray, source_guidance: np.ndarray, iterations: in updated[y, x] = best heatmap[y, x] = max(heatmap[y, x], current_energy - best_energy) result = np.clip(updated, 0.0, 1.0) + result = _snap_alpha(result) return CleanupArtifacts(cleaned_rgba=result, isolated_heatmap=heatmap) + + +def _snap_alpha(rgba: np.ndarray, threshold: float = 0.5) -> np.ndarray: + result = rgba.copy() + alpha = (result[..., 3] >= threshold).astype(np.float32) + result[..., 3] = alpha + result[..., :3] *= alpha[..., None] + return result diff --git a/src/repixelizer/gui.py b/src/repixelizer/gui.py index 790e03d..732656b 100644 --- a/src/repixelizer/gui.py +++ b/src/repixelizer/gui.py @@ -1,4 +1,5 @@ import base64 +import html import contextlib import io import json @@ -167,7 +168,7 @@ class HostedDemoConfig: def from_env(cls) -> "HostedDemoConfig": hosted_demo = _env_flag("REPIXELIZER_HOSTED_DEMO", False) defaults = { - "max_upload_bytes": 1_048_576 if hosted_demo else 16 * 1_048_576, + "max_upload_bytes": 2 * 1_048_576 if hosted_demo else 16 * 1_048_576, "max_input_dimension": 2048 if hosted_demo else 4096, "max_output_dimension": 256 if hosted_demo else 1024, "default_steps": 32 if hosted_demo else 48, @@ -335,14 +336,14 @@ class GuiJob: "operation": payload.get("operation"), "sourceImage": _image_asset(source), } - if event in {"inference_candidates_ready", "phase_selection_completed"}: + if event in {"inference_candidates_ready", "candidate_selection_completed"}: inference = payload["inference"] self.current_inference = inference return { "inference": inference_to_json(inference), "inferenceMode": payload.get("inference_mode"), } - if event == "phase_rerank_started": + if event == "candidate_rerank_started": return { "previewSteps": int(payload["preview_steps"]), "candidateCount": int(payload["candidate_count"]), @@ -351,7 +352,6 @@ class GuiJob: if event == "lattice_search_started": return { "candidateCount": int(payload["candidate_count"]), - "phaseSampleCount": int(payload["phase_sample_count"]), "device": str(payload["device"]), } if event == "lattice_search_progress": @@ -360,40 +360,33 @@ class GuiJob: "totalCandidates": int(payload["total_candidates"]), "targetWidth": int(payload["target_width"]), "targetHeight": int(payload["target_height"]), - "phaseSampleCount": int(payload["phase_sample_count"]), "bestScore": None if payload.get("best_score") is None else float(payload["best_score"]), } - if event == "phase_rerank_candidate_started": + if event == "candidate_rerank_candidate_started": return { "candidateIndex": int(payload["candidate_index"]), "totalCandidates": int(payload["total_candidates"]), "targetWidth": int(payload["target_width"]), "targetHeight": int(payload["target_height"]), - "phaseX": float(payload["phase_x"]), - "phaseY": float(payload["phase_y"]), "previewSteps": int(payload["preview_steps"]), } - if event == "phase_rerank_candidate_step": + if event == "candidate_rerank_candidate_step": return { "candidateIndex": int(payload["candidate_index"]), "totalCandidates": int(payload["total_candidates"]), "targetWidth": int(payload["target_width"]), "targetHeight": int(payload["target_height"]), - "phaseX": float(payload["phase_x"]), - "phaseY": float(payload["phase_y"]), "step": int(payload["step"]), "totalSteps": int(payload["total_steps"]), "loss": None if payload.get("loss") is None else float(payload["loss"]), } - if event == "phase_rerank_candidate_completed": + if event == "candidate_rerank_candidate_completed": return { "candidateIndex": int(payload["candidate_index"]), "totalCandidates": int(payload["total_candidates"]), "completedCandidates": int(payload["completed_candidates"]), "targetWidth": int(payload["target_width"]), "targetHeight": int(payload["target_height"]), - "phaseX": float(payload["phase_x"]), - "phaseY": float(payload["phase_y"]), "totalSteps": int(payload["total_steps"]), "finalLoss": None if payload.get("final_loss") is None else float(payload["final_loss"]), } @@ -427,7 +420,7 @@ class GuiJob: "totalSteps": int(payload["total_steps"]), "loss": None if payload.get("loss") is None else float(payload["loss"]), "terms": {key: float(value) for key, value in payload.get("terms", {}).items()}, - "phaseMetrics": {key: float(value) for key, value in payload.get("phase_metrics", {}).items()}, + "solverMetrics": {key: float(value) for key, value in payload.get("phase_metrics", {}).items()}, "outputImage": _image_asset(payload["target_rgba"]), "samplingOverlayImage": _image_asset( _render_sample_overlay(self.current_source_rgba, payload["sample_x"], payload["sample_y"]) @@ -493,13 +486,11 @@ def _normalize_job_options( target_size: int | None, target_width: int | None, target_height: int | None, - phase_x: float | None, - phase_y: float | None, steps: int | None, seed: int, device: str, strip_background: bool, - skip_phase_rerank: bool, + skip_candidate_rerank: bool, ) -> dict[str, Any]: normalized_target_size = _normalize_optional_positive_int("target_size", target_size) normalized_target_width = _normalize_optional_positive_int("target_width", target_width) @@ -511,8 +502,6 @@ def _normalize_job_options( target_size=normalized_target_size, target_width=normalized_target_width, target_height=normalized_target_height, - phase_x=phase_x, - phase_y=phase_y, ) except ValueError as exc: raise ValueError(str(exc)) from exc @@ -526,18 +515,16 @@ def _normalize_job_options( normalized_strip_background = False if config.hosted_demo else bool(strip_background) inference_mode = "autocorr" if config.hosted_demo and fixed_dims is None else "search" max_inferred_target_size = config.max_output_dimension if config.hosted_demo and fixed_dims is None else None - normalized_skip_phase_rerank = True if config.hosted_demo else bool(skip_phase_rerank) + normalized_skip_candidate_rerank = True if config.hosted_demo else bool(skip_candidate_rerank) return { "target_size": normalized_target_size, "target_width": normalized_target_width, "target_height": normalized_target_height, - "phase_x": phase_x, - "phase_y": phase_y, "steps": normalized_steps, "seed": int(seed), "device": normalized_device, "strip_background": normalized_strip_background, - "skip_phase_rerank": normalized_skip_phase_rerank, + "skip_candidate_rerank": normalized_skip_candidate_rerank, "lattice_inference_mode": inference_mode, "max_inferred_target_size": max_inferred_target_size, } @@ -551,13 +538,11 @@ def _validate_upload_request( target_size: int | None, target_width: int | None, target_height: int | None, - phase_x: float | None, - phase_y: float | None, steps: int | None, seed: int, device: str, strip_background: bool, - skip_phase_rerank: bool, + skip_candidate_rerank: bool, ) -> tuple[dict[str, Any], int, int]: if len(raw) > config.max_upload_bytes: raise ValueError(f"Upload is too large. Limit is {config.max_upload_bytes // 1024} KiB for the hosted demo.") @@ -576,13 +561,11 @@ def _validate_upload_request( target_size=target_size, target_width=target_width, target_height=target_height, - phase_x=phase_x, - phase_y=phase_y, steps=steps, seed=seed, device=device, strip_background=strip_background, - skip_phase_rerank=skip_phase_rerank, + skip_candidate_rerank=skip_candidate_rerank, ) return options, source_width, source_height @@ -597,14 +580,12 @@ def _execute_job(job: GuiJob) -> None: target_size=job.options["target_size"], target_width=job.options["target_width"], target_height=job.options["target_height"], - phase_x=job.options["phase_x"], - phase_y=job.options["phase_y"], palette_mode="off", seed=job.options["seed"], steps=job.options["steps"], device=job.options["device"], strip_background=job.options["strip_background"], - enable_phase_rerank=not job.options["skip_phase_rerank"], + enable_candidate_rerank=not job.options["skip_candidate_rerank"], lattice_inference_mode=job.options["lattice_inference_mode"], max_inferred_target_size=job.options["max_inferred_target_size"], observer=job.observe, @@ -918,8 +899,10 @@ def _landing_page_html( label = provider.get("label") if not isinstance(slug, str) or not isinstance(label, str): continue + escaped_slug = html.escape(slug, quote=True) + escaped_label = html.escape(label) fragments.append( - f'' + f'' ) provider_button_html = "".join(fragments) hero_note = ( @@ -930,445 +913,34 @@ def _landing_page_html( auth_notice = "" if auth_enabled and not auth_payload.get("providers"): auth_notice = ( - '

Auth is enabled here, but no providers are configured yet. ' "The front door exists, but somebody still owes it an actual key.

" ) elif auth_enabled: - auth_notice = '' - 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. -

-
- {primary_label} - - View the repo -
-
{provider_button_html}
- -
-
- 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.

-
-
- - {auth_notice} -
-
- - - -""" + auth_notice = '

Choose a provider. The main page stays put while Heimdall and the backend sort out the paperwork in another tab.

' + auth_state_text = ( + f"Already authenticated as {subject_payload.get('displayName') or subject_payload.get('accountId') or 'a local creature'}." + if authenticated + else "" + ) + static_dir = _static_dir() + styles_path = static_dir / "styles.css" + styles_version = int(styles_path.stat().st_mtime_ns) if styles_path.exists() else 0 + landing_template = (static_dir / "landing.html").read_text(encoding="utf-8") + return ( + landing_template.replace("{{STYLES_VERSION}}", str(styles_version)) + .replace("{{HERO_NOTE}}", html.escape(hero_note)) + .replace("{{PRIMARY_HREF}}", html.escape(primary_href, quote=True)) + .replace("{{PRIMARY_LABEL}}", html.escape(primary_label)) + .replace("{{SIGNOUT_HIDDEN}}", "hidden" if not authenticated else "") + .replace("{{AUTH_PROVIDER_BUTTONS}}", provider_button_html) + .replace("{{AUTH_STATE_TEXT}}", html.escape(auth_state_text)) + .replace("{{DEMO_LIMITS}}", html.escape(demo_limits)) + .replace("{{AUTH_NOTICE}}", auth_notice) + .replace("{{AUTH_CONFIG_JSON}}", _json_html(auth_payload)) + .replace("{{SESSION_SUBJECT_JSON}}", _json_html(subject_payload)) + .replace("{{DEFAULT_PRIMARY_LABEL_JSON}}", _json_html(primary_label)) + ) def create_app(): @@ -1550,13 +1122,11 @@ def create_app(): target_size: int | None = Form(default=None), target_width: int | None = Form(default=None), target_height: int | None = Form(default=None), - phase_x: float | None = Form(default=None), - phase_y: float | None = Form(default=None), steps: int | None = Form(default=None), seed: int = Form(default=7), device: str = Form(default="auto"), strip_background: bool = Form(default=False), - skip_phase_rerank: bool = Form(default=False), + skip_candidate_rerank: bool = Form(default=False), ): raw = await image.read() filename = image.filename or "input.png" @@ -1568,13 +1138,11 @@ def create_app(): target_size=target_size, target_width=target_width, target_height=target_height, - phase_x=phase_x, - phase_y=phase_y, steps=steps, seed=seed, device=device, strip_background=strip_background, - skip_phase_rerank=skip_phase_rerank, + skip_candidate_rerank=skip_candidate_rerank, ) subject = access_controller.require_current_capability("queue_submit") job = manager.submit_job( diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index a47b3fb..30d74dd 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -39,8 +39,6 @@ const downloadButton = byId("downloadButton"); const targetSizeInput = byId("targetSizeInput"); const targetWidthInput = byId("targetWidthInput"); const targetHeightInput = byId("targetHeightInput"); -const phaseXInput = byId("phaseXInput"); -const phaseYInput = byId("phaseYInput"); const stepsInput = byId("stepsInput"); const seedInput = byId("seedInput"); const runControlsCopy = byId("runControlsCopy"); @@ -70,11 +68,11 @@ const eventTypes = [ "lattice_search_started", "lattice_search_progress", "inference_candidates_ready", - "phase_rerank_started", - "phase_rerank_candidate_started", - "phase_rerank_candidate_step", - "phase_rerank_candidate_completed", - "phase_selection_completed", + "candidate_rerank_started", + "candidate_rerank_candidate_started", + "candidate_rerank_candidate_step", + "candidate_rerank_candidate_completed", + "candidate_selection_completed", "analysis_completed", "phase_field_prepared", "phase_field_initial", @@ -98,7 +96,7 @@ const state = { inference: null, inferenceMode: null, latticeSearch: null, - phaseRerank: null, + candidateRerank: null, phaseFieldPrep: null, frames: [], cleanupImage: null, @@ -328,7 +326,7 @@ function resetRunArtifacts(options = {}) { state.inference = null; state.inferenceMode = null; state.latticeSearch = null; - state.phaseRerank = null; + state.candidateRerank = null; state.phaseFieldPrep = null; state.frames = []; state.cleanupImage = null; @@ -469,12 +467,6 @@ function formatGridValue(width, height, fallback = "pending") { } return `${width} x ${height}`; } -function formatPhaseValue(phaseX, phaseY, fallback = "pending") { - if (phaseX === null || phaseX === undefined || phaseY === null || phaseY === undefined) { - return fallback; - } - return `${formatNumber(phaseX, 2)}, ${formatNumber(phaseY, 2)}`; -} function renderStatusMetricItems(items) { statusMetrics.innerHTML = ""; statusMetrics.style.setProperty("--status-metric-count", String(Math.max(items.length, 1))); @@ -503,10 +495,6 @@ function renderInference() { Chosen Grid ${state.inference.target_width} x ${state.inference.target_height}
-
- Phase - ${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)} -
Confidence ${formatNumber(state.inference.confidence, 3)} @@ -517,10 +505,9 @@ function renderInference() { for (const candidate of topCandidates) { const node = document.createElement("div"); node.className = "info-card candidate-card"; - const rerank = candidate.breakdown["phase_rerank_rank"]; + const rerank = candidate.breakdown["candidate_rerank_rank"]; node.innerHTML = ` ${candidate.target_width} x ${candidate.target_height} - phase ${formatNumber(candidate.phase_x, 2)}, ${formatNumber(candidate.phase_y, 2)}
score ${formatNumber(candidate.score, 3)}${typeof rerank === "number" ? ` • rerank #${Math.round(rerank)}` : ""} `; candidateList.appendChild(node); @@ -547,7 +534,6 @@ function renderStatusMetrics() { else if (state.stageKey === "inference") { if (state.latticeSearch) { items.push({ label: "Sizes", value: `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}` }); - items.push({ label: "Phases", value: String(state.latticeSearch.phaseSampleCount) }); items.push({ label: "Grid", value: formatGridValue(state.latticeSearch.currentTargetWidth, state.latticeSearch.currentTargetHeight), @@ -565,49 +551,44 @@ function renderStatusMetrics() { : state.inferenceMode ?? "search"; items.push({ label: "Mode", value: modeLabel }); items.push({ label: "Grid", value: `${state.inference.target_width} x ${state.inference.target_height}` }); - items.push({ label: "Phase", value: `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}` }); items.push({ label: "Confidence", value: formatNumber(state.inference.confidence, 3) }); } } else if (state.stageKey === "rerank") { - if (state.phaseRerank) { + if (state.candidateRerank) { items.push({ label: "Done", - value: `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`, + value: `${state.candidateRerank.completedCandidates} / ${state.candidateRerank.candidateCount}`, }); items.push({ label: "Candidate", - value: state.phaseRerank.currentCandidateIndex > 0 - ? `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}` - : `0 / ${state.phaseRerank.candidateCount}`, + value: state.candidateRerank.currentCandidateIndex > 0 + ? `${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount}` + : `0 / ${state.candidateRerank.candidateCount}`, }); items.push({ label: "Preview", - value: state.phaseRerank.currentTotalSteps > 0 - ? `${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps}` - : state.phaseRerank.previewSteps > 0 - ? `0 / ${state.phaseRerank.previewSteps}` + value: state.candidateRerank.currentTotalSteps > 0 + ? `${state.candidateRerank.currentStep} / ${state.candidateRerank.currentTotalSteps}` + : state.candidateRerank.previewSteps > 0 + ? `0 / ${state.candidateRerank.previewSteps}` : "skipped", }); items.push({ label: "Grid", - value: formatGridValue(state.phaseRerank.currentTargetWidth, state.phaseRerank.currentTargetHeight), + value: formatGridValue(state.candidateRerank.currentTargetWidth, state.candidateRerank.currentTargetHeight), }); - items.push({ - label: "Phase", - value: formatPhaseValue(state.phaseRerank.currentPhaseX, state.phaseRerank.currentPhaseY), - }); - items.push({ label: "Loss", value: formatNumber(state.phaseRerank.currentLoss, 4) }); - items.push({ label: "Confidence", value: formatNumber(state.phaseRerank.confidence, 3) }); + items.push({ label: "Loss", value: formatNumber(state.candidateRerank.currentLoss, 4) }); + items.push({ label: "Confidence", value: formatNumber(state.candidateRerank.confidence, 3) }); } } else if (state.stageKey === "solver") { const solverTerms = [ - { label: "Coherence", value: formatNumber(frame?.terms.local_coherence ?? null, 4) }, - { label: "Edge", value: formatNumber(frame?.terms.local_edge ?? null, 4) }, - { label: "Smoothness", value: formatNumber(frame?.terms.smoothness ?? null, 4) }, - { label: "Collapse", value: formatNumber(frame?.terms.collapse ?? null, 4) }, - { label: "Magnitude", value: formatNumber(frame?.terms.magnitude ?? null, 4) }, + { label: "Signal", value: formatNumber(frame?.terms.local_signal ?? frame?.solverMetrics.local_signal ?? null, 4) }, + { label: "Grid align", value: formatNumber(frame?.terms.grid_alignment ?? frame?.solverMetrics.grid_alignment ?? null, 4) }, + { label: "Smoothness", value: formatNumber(frame?.terms.smoothness ?? frame?.solverMetrics.smoothness ?? null, 4) }, + { label: "Collapse", value: formatNumber(frame?.terms.collapse ?? frame?.solverMetrics.collapse ?? null, 4) }, + { label: "Magnitude", value: formatNumber(frame?.terms.magnitude ?? frame?.solverMetrics.magnitude ?? null, 4) }, ]; items.push({ label: "Grid", @@ -647,10 +628,18 @@ function resolveViewerPanels() { leftLabel = "Guidance"; } if (frame) { - leftAsset = frame.samplingOverlayImage; - rightAsset = frame.outputImage; - leftLabel = "Sampling Overlay"; - rightLabel = "Current Output"; + if (state.stageKey === "solver" && state.guidanceImage) { + leftAsset = state.guidanceImage; + rightAsset = frame.samplingOverlayImage; + leftLabel = "Signal"; + rightLabel = "Sampling Overlay"; + } + else { + leftAsset = frame.samplingOverlayImage; + rightAsset = frame.outputImage; + leftLabel = "Sampling Overlay"; + rightLabel = "Current Output"; + } } else if (state.cleanupImage) { if (state.heatmapImage) { @@ -805,12 +794,14 @@ function renderLossChart() { const domainMax = getLossDomainMax(); const samples = buildLossSamples(); const observedMaxLoss = samples.length === 0 ? null : Math.max(...samples.map((sample) => sample.loss)); - if (observedMaxLoss !== null) { - state.lossAxisMax = Math.max(state.lossAxisMax ?? 0, observedMaxLoss); + const observedMinLoss = samples.length === 0 ? null : Math.min(...samples.map((sample) => sample.loss)); + if (observedMaxLoss !== null && observedMinLoss !== null) { + const observedAbsLoss = Math.max(Math.abs(observedMaxLoss), Math.abs(observedMinLoss)); + state.lossAxisMax = Math.max(state.lossAxisMax ?? 0, observedAbsLoss); } - const axisMaxLoss = Math.max(0.001, state.lossAxisMax ?? observedMaxLoss ?? 1); - const maxLoss = axisMaxLoss * 1.05; - const minLoss = 0; + const axisAbsLoss = Math.max(0.001, state.lossAxisMax ?? Math.max(Math.abs(observedMaxLoss ?? 1), Math.abs(observedMinLoss ?? 0))); + const maxLoss = axisAbsLoss * 1.05; + const minLoss = -axisAbsLoss * 1.05; const chartLeft = 88; const chartRight = width - 18; const chartTop = 16; @@ -1245,13 +1236,11 @@ function buildFormData() { ["target_size", parseOptionalInteger(targetSizeInput)?.toString() ?? null], ["target_width", parseOptionalInteger(targetWidthInput)?.toString() ?? null], ["target_height", parseOptionalInteger(targetHeightInput)?.toString() ?? null], - ["phase_x", parseOptionalFloat(phaseXInput)?.toString() ?? null], - ["phase_y", parseOptionalFloat(phaseYInput)?.toString() ?? null], ["steps", String(normalizedSteps)], ["seed", String(parseOptionalInteger(seedInput) ?? 7)], ["device", runtimeConfig?.hostedDemo ? "cpu" : deviceInput.value], ["strip_background", runtimeConfig?.hostedDemo ? "false" : stripBackgroundInput.checked ? "true" : "false"], - ["skip_phase_rerank", skipRerankInput.checked ? "true" : "false"], + ["skip_candidate_rerank", skipRerankInput.checked ? "true" : "false"], ]; for (const [key, value] of values) { if (value !== null) { @@ -1317,13 +1306,12 @@ async function handleEvent(eventName, payload) { state.latticeSearch = { candidateCount: Number(payload.candidateCount ?? 0), completedCandidates: 0, - phaseSampleCount: Number(payload.phaseSampleCount ?? 0), device: typeof payload.device === "string" ? payload.device : null, currentTargetWidth: null, currentTargetHeight: null, bestScore: null, }; - setStage("inference", "Lattice search", `Testing ${state.latticeSearch.candidateCount} size candidates with ${state.latticeSearch.phaseSampleCount} phase samples each.`); + setStage("inference", "Lattice search", `Testing ${state.latticeSearch.candidateCount} candidate sizes from canonical cell centers.`); addLog("Search", `Started lattice search across ${String(payload.candidateCount)} size candidates.`); break; case "lattice_search_progress": @@ -1331,7 +1319,6 @@ async function handleEvent(eventName, payload) { state.latticeSearch = { candidateCount: Number(payload.totalCandidates ?? 0), completedCandidates: 0, - phaseSampleCount: Number(payload.phaseSampleCount ?? 0), device: null, currentTargetWidth: null, currentTargetHeight: null, @@ -1340,20 +1327,19 @@ async function handleEvent(eventName, payload) { } state.latticeSearch.completedCandidates = Number(payload.completedCandidates ?? state.latticeSearch.completedCandidates); state.latticeSearch.candidateCount = Number(payload.totalCandidates ?? state.latticeSearch.candidateCount); - state.latticeSearch.phaseSampleCount = Number(payload.phaseSampleCount ?? state.latticeSearch.phaseSampleCount); state.latticeSearch.currentTargetWidth = Number(payload.targetWidth ?? 0); state.latticeSearch.currentTargetHeight = Number(payload.targetHeight ?? 0); state.latticeSearch.bestScore = payload.bestScore === null || payload.bestScore === undefined ? state.latticeSearch.bestScore : Number(payload.bestScore); - setStage("inference", "Lattice search", `Size ${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}: scored ${state.latticeSearch.currentTargetWidth} x ${state.latticeSearch.currentTargetHeight} over ${state.latticeSearch.phaseSampleCount} phase samples.`); + setStage("inference", "Lattice search", `Size ${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}: scored ${state.latticeSearch.currentTargetWidth} x ${state.latticeSearch.currentTargetHeight} from canonical cell centers.`); break; case "inference_candidates_ready": state.inference = payload.inference; state.inferenceMode = typeof payload.inferenceMode === "string" ? payload.inferenceMode : state.inferenceMode; - addLog("Inference", "Scored candidate grids and phase offsets."); + addLog("Inference", "Scored candidate grid sizes."); break; - case "phase_rerank_started": - state.phaseRerank = { + case "candidate_rerank_started": + state.candidateRerank = { previewSteps: Number(payload.previewSteps ?? 0), candidateCount: Number(payload.candidateCount ?? 0), confidence: Number(payload.confidence ?? 0), @@ -1361,63 +1347,55 @@ async function handleEvent(eventName, payload) { currentCandidateIndex: 0, currentTargetWidth: null, currentTargetHeight: null, - currentPhaseX: null, - currentPhaseY: null, currentStep: 0, currentTotalSteps: 0, currentLoss: null, }; - setStage("rerank", "Phase rerank", `Previewing ${state.phaseRerank.candidateCount} low-confidence candidates over ${state.phaseRerank.previewSteps} short solver steps.`); + setStage("rerank", "Candidate rerank", `Previewing ${state.candidateRerank.candidateCount} low-confidence candidates over ${state.candidateRerank.previewSteps} short solver steps.`); addLog("Rerank", `Running ${String(payload.previewSteps)} preview steps across ${String(payload.candidateCount)} low-confidence candidates.`); break; - case "phase_rerank_candidate_started": - if (state.phaseRerank) { - state.phaseRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.phaseRerank.currentCandidateIndex); - state.phaseRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); - state.phaseRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); - state.phaseRerank.currentPhaseX = Number(payload.phaseX ?? 0); - state.phaseRerank.currentPhaseY = Number(payload.phaseY ?? 0); - state.phaseRerank.currentStep = 0; - state.phaseRerank.currentTotalSteps = Number(payload.previewSteps ?? state.phaseRerank.previewSteps); - state.phaseRerank.currentLoss = null; - setStage("rerank", "Phase rerank", `Candidate ${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}: previewing ${state.phaseRerank.currentTargetWidth} x ${state.phaseRerank.currentTargetHeight} at phase ${formatNumber(state.phaseRerank.currentPhaseX, 2)}, ${formatNumber(state.phaseRerank.currentPhaseY, 2)}.`); + case "candidate_rerank_candidate_started": + if (state.candidateRerank) { + state.candidateRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.candidateRerank.currentCandidateIndex); + state.candidateRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); + state.candidateRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); + state.candidateRerank.currentStep = 0; + state.candidateRerank.currentTotalSteps = Number(payload.previewSteps ?? state.candidateRerank.previewSteps); + state.candidateRerank.currentLoss = null; + setStage("rerank", "Candidate rerank", `Candidate ${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount}: previewing ${state.candidateRerank.currentTargetWidth} x ${state.candidateRerank.currentTargetHeight}.`); } break; - case "phase_rerank_candidate_step": - if (state.phaseRerank) { - state.phaseRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.phaseRerank.currentCandidateIndex); - state.phaseRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); - state.phaseRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); - state.phaseRerank.currentPhaseX = Number(payload.phaseX ?? 0); - state.phaseRerank.currentPhaseY = Number(payload.phaseY ?? 0); - state.phaseRerank.currentStep = Number(payload.step ?? 0); - state.phaseRerank.currentTotalSteps = Number(payload.totalSteps ?? 0); - state.phaseRerank.currentLoss = + case "candidate_rerank_candidate_step": + if (state.candidateRerank) { + state.candidateRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.candidateRerank.currentCandidateIndex); + state.candidateRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); + state.candidateRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); + state.candidateRerank.currentStep = Number(payload.step ?? 0); + state.candidateRerank.currentTotalSteps = Number(payload.totalSteps ?? 0); + state.candidateRerank.currentLoss = payload.loss === null || payload.loss === undefined ? null : Number(payload.loss); - setStage("rerank", "Phase rerank", state.phaseRerank.currentTotalSteps <= 0 - ? `Candidate ${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}: preview solve skipped.` - : `Candidate ${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}, preview step ${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps} on ${state.phaseRerank.currentTargetWidth} x ${state.phaseRerank.currentTargetHeight}.`); + setStage("rerank", "Candidate rerank", state.candidateRerank.currentTotalSteps <= 0 + ? `Candidate ${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount}: preview solve skipped.` + : `Candidate ${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount}, preview step ${state.candidateRerank.currentStep} / ${state.candidateRerank.currentTotalSteps} on ${state.candidateRerank.currentTargetWidth} x ${state.candidateRerank.currentTargetHeight}.`); } break; - case "phase_rerank_candidate_completed": - if (state.phaseRerank) { - state.phaseRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.phaseRerank.currentCandidateIndex); - state.phaseRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); - state.phaseRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); - state.phaseRerank.currentPhaseX = Number(payload.phaseX ?? 0); - state.phaseRerank.currentPhaseY = Number(payload.phaseY ?? 0); - state.phaseRerank.completedCandidates = Number(payload.completedCandidates ?? state.phaseRerank.completedCandidates); - state.phaseRerank.currentStep = Number(payload.totalSteps ?? state.phaseRerank.currentStep); - state.phaseRerank.currentTotalSteps = Number(payload.totalSteps ?? state.phaseRerank.currentTotalSteps); - state.phaseRerank.currentLoss = - payload.finalLoss === null || payload.finalLoss === undefined ? state.phaseRerank.currentLoss : Number(payload.finalLoss); - setStage("rerank", "Phase rerank", `Candidate ${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount} scored. ${state.phaseRerank.completedCandidates} of ${state.phaseRerank.candidateCount} previews finished.`); + case "candidate_rerank_candidate_completed": + if (state.candidateRerank) { + state.candidateRerank.currentCandidateIndex = Number(payload.candidateIndex ?? state.candidateRerank.currentCandidateIndex); + state.candidateRerank.currentTargetWidth = Number(payload.targetWidth ?? 0); + state.candidateRerank.currentTargetHeight = Number(payload.targetHeight ?? 0); + state.candidateRerank.completedCandidates = Number(payload.completedCandidates ?? state.candidateRerank.completedCandidates); + state.candidateRerank.currentStep = Number(payload.totalSteps ?? state.candidateRerank.currentStep); + state.candidateRerank.currentTotalSteps = Number(payload.totalSteps ?? state.candidateRerank.currentTotalSteps); + state.candidateRerank.currentLoss = + payload.finalLoss === null || payload.finalLoss === undefined ? state.candidateRerank.currentLoss : Number(payload.finalLoss); + setStage("rerank", "Candidate rerank", `Candidate ${state.candidateRerank.currentCandidateIndex} / ${state.candidateRerank.candidateCount} scored. ${state.candidateRerank.completedCandidates} of ${state.candidateRerank.candidateCount} previews finished.`); } break; - case "phase_selection_completed": + case "candidate_selection_completed": state.inference = payload.inference; state.inferenceMode = typeof payload.inferenceMode === "string" ? payload.inferenceMode : state.inferenceMode; - addLog("Selection", "Committed to a ruler and phase."); + addLog("Selection", "Committed to a lattice size."); break; case "analysis_completed": addLog("Scout", "Built the edge map that tells the solver where the floorboards creak."); diff --git a/src/repixelizer/gui_static/index.html b/src/repixelizer/gui_static/index.html index 649ee5f..6bb6838 100644 --- a/src/repixelizer/gui_static/index.html +++ b/src/repixelizer/gui_static/index.html @@ -40,7 +40,7 @@

How To Use It

1. Drop an input on the right or choose a file.

-

2. Leave size and phase blank to let the machine search, or pin them if you already know the lattice.

+

2. Leave size blank to let the machine search, or pin width and height if you already know the lattice.

3. Run it, watch the status panel for the active stage, then patch the output by hand if the machine gets cute.

@@ -235,34 +235,6 @@ - -