Text-to-Image
English
numpy
machine-learning
deep-learning
generative-ai
text-2-image
image-generation
open-weights
model-weights
ai-art
pixel-art
game-development
gamedev
game-assets
asset-generator
sprite-generator
offline
tiny-model
numpy-runtime
int8-quantization
self-supervised
procedural-data
gpt
english-prompts
awesome-ai
| """PXG-Tiny high-level pipeline (offline, NumPy-only). | |
| PXGPipeline wraps OfflinePipeline with: | |
| * generate_pixels(text) -> uint8 grid (16x16 palette indices) | |
| * generate_png(text, path) -> scaled RGBA PNG | |
| * generate_turnaround(text, seeds) / variations | |
| * should_ask / ask -> rule-based clarify/refuse gate (ask-first) | |
| * auto-retry sampling scored by quality.check_sprite (reject-and-log) | |
| """ | |
| import sys | |
| from collections import Counter | |
| from pathlib import Path | |
| import numpy as np | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from pxg_tiny.runtime_pipeline import OfflinePipeline # noqa: E402 | |
| from pxg_tiny import quality as Q # noqa: E402 | |
| from pxg_tiny.render import save_png # noqa: E402 | |
| DEFAULT_BUNDLE = Path(__file__).resolve().parents[2] / "weights" | |
| class PXGPipeline: | |
| def __init__(self, bundle_dir=None): | |
| self.pipe = OfflinePipeline(bundle_dir or DEFAULT_BUNDLE) | |
| # ------------------------------------------------------------- gate -- | |
| def should_ask(text): | |
| return Q.should_ask(text) | |
| def ask(cls, text): | |
| """Return a clarifying question / refusal message, or None if the | |
| prompt is acceptable.""" | |
| label, msg = cls.should_ask(text) | |
| return msg if label in ("clarify", "refuse") else None | |
| # -------------------------------------------------------- sampling -- | |
| def _anchor_ids(spec): | |
| """Canonical corpus-style caption ids for the anchor retry stage — | |
| prompt normalization using only our own parser (offline).""" | |
| from pxg_tiny.config import encode_caption | |
| cls = spec.get("cls") | |
| cap = Q.ANCHOR_CAPTIONS.get(cls) | |
| if cap is None: | |
| return None | |
| if (spec.get("material") and cls not in ("wizard", "archer", "zombie") | |
| and f" {spec['material']}" not in cap): | |
| art = "an" if spec["material"][0] in "aeiou" else "a" | |
| cap = cap.replace("a ", f"{art} {spec['material']} ", 1) | |
| return np.array(encode_caption(cap), dtype=np.int64) | |
| def generate_pixels(self, text, seed=0, temperature=None, top_k=None, | |
| retries=8, enforce_quality=True): | |
| """English instruction -> 16x16 grid of palette indices. | |
| Escalating verify-and-retry schedule (all offline, self-contained): | |
| attempt 0 raw sample | |
| attempts 1-3 self-guidance: palette-logit bias, strength ramps | |
| attempts 4-5 + spatial bias (face box / vial margins) | |
| attempts 6-7 + canonical anchor caption (prompt normalization) | |
| The first sprite passing the grounding checks wins; reject reasons | |
| are counted and returned in the metadata dict.""" | |
| label, msg = self.should_ask(text) | |
| if label != "accept": | |
| return None, {"gate": label, "message": msg} | |
| spec = Q.parse_prompt(text) | |
| anchor_ids = self._anchor_ids(spec) | |
| struct_prefix = Q.STRUCTURAL_PREFIX.get(spec.get("cls")) | |
| rejects = Counter() | |
| grid = None | |
| for k in range(max(1, retries)): | |
| lb = None | |
| ids = None | |
| ptoks = struct_prefix if (struct_prefix is not None and k >= 3) else None | |
| if k >= 1: | |
| lb = Q.bias_from_spec(spec, strength=1.6 + 0.5 * min(k, 5)) | |
| if k >= 4: | |
| combined = np.zeros((256, 32), dtype=np.float64) | |
| if lb is not None: | |
| combined[:] = np.asarray(lb, dtype=np.float64)[None, :] | |
| combined += Q.positional_bias_from_spec( | |
| spec, strength=1.4 + 0.3 * (k - 4)) | |
| lb = combined | |
| if k >= 6 and anchor_ids is not None: | |
| ids = anchor_ids # canonical anchor | |
| grid = self.pipe.generate_grid(text, seed=seed + 1013 * k, | |
| temperature=temperature, | |
| top_k=top_k, logit_bias=lb, | |
| ids_override=ids, | |
| prefix_tokens=ptoks) | |
| if enforce_quality: | |
| ok, reasons = Q.check_sprite(grid, spec) | |
| if ok: | |
| return grid, {"gate": "accept", "seed": seed + 1013 * k, | |
| "attempt": k, "rejects": dict(rejects)} | |
| for r in reasons: | |
| rejects[r] += 1 | |
| else: | |
| return grid, {"gate": "accept", "seed": seed, "attempt": k, | |
| "rejects": {}} | |
| # fall back to the last sample rather than failing hard | |
| return grid, {"gate": "accept_degraded", "seed": seed, | |
| "attempt": retries, "rejects": dict(rejects)} | |
| def generate_png(self, text, path, scale=8, seed=0, **kw): | |
| grid, meta = self.generate_pixels(text, seed=seed, **kw) | |
| if grid is None: | |
| return None, meta | |
| save_png(grid, Path(path), scale=scale) | |
| meta["path"] = str(path) | |
| return grid, meta | |
| def generate_turnaround(self, text, seeds=(1, 2, 3, 4), **kw): | |
| """Several consistent samples of the same instruction (seed family); | |
| tiny-model analog of the big sibling's multiview turnaround.""" | |
| grids = [] | |
| for s in seeds: | |
| g, m = self.generate_pixels(text, seed=s, **kw) | |
| grids.append((g, m)) | |
| return grids | |
| def variations(self, text, k=4, start_seed=0, **kw): | |
| return self.generate_turnaround(text, seeds=tuple( | |
| start_seed + 37 * i for i in range(k)), **kw) | |