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
Upload pxg_tiny/pipeline.py with huggingface_hub
Browse files- pxg_tiny/pipeline.py +129 -0
pxg_tiny/pipeline.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PXG-Tiny high-level pipeline (offline, NumPy-only).
|
| 2 |
+
|
| 3 |
+
PXGPipeline wraps OfflinePipeline with:
|
| 4 |
+
* generate_pixels(text) -> uint8 grid (16x16 palette indices)
|
| 5 |
+
* generate_png(text, path) -> scaled RGBA PNG
|
| 6 |
+
* generate_turnaround(text, seeds) / variations
|
| 7 |
+
* should_ask / ask -> rule-based clarify/refuse gate (ask-first)
|
| 8 |
+
* auto-retry sampling scored by quality.check_sprite (reject-and-log)
|
| 9 |
+
"""
|
| 10 |
+
import sys
|
| 11 |
+
from collections import Counter
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 17 |
+
from pxg_tiny.runtime_pipeline import OfflinePipeline # noqa: E402
|
| 18 |
+
from pxg_tiny import quality as Q # noqa: E402
|
| 19 |
+
from pxg_tiny.render import save_png # noqa: E402
|
| 20 |
+
|
| 21 |
+
DEFAULT_BUNDLE = Path(__file__).resolve().parents[2] / "weights"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class PXGPipeline:
|
| 25 |
+
def __init__(self, bundle_dir=None):
|
| 26 |
+
self.pipe = OfflinePipeline(bundle_dir or DEFAULT_BUNDLE)
|
| 27 |
+
|
| 28 |
+
# ------------------------------------------------------------- gate --
|
| 29 |
+
@staticmethod
|
| 30 |
+
def should_ask(text):
|
| 31 |
+
return Q.should_ask(text)
|
| 32 |
+
|
| 33 |
+
@classmethod
|
| 34 |
+
def ask(cls, text):
|
| 35 |
+
"""Return a clarifying question / refusal message, or None if the
|
| 36 |
+
prompt is acceptable."""
|
| 37 |
+
label, msg = cls.should_ask(text)
|
| 38 |
+
return msg if label in ("clarify", "refuse") else None
|
| 39 |
+
|
| 40 |
+
# -------------------------------------------------------- sampling --
|
| 41 |
+
@staticmethod
|
| 42 |
+
def _anchor_ids(spec):
|
| 43 |
+
"""Canonical corpus-style caption ids for the anchor retry stage —
|
| 44 |
+
prompt normalization using only our own parser (offline)."""
|
| 45 |
+
from pxg_tiny.config import encode_caption
|
| 46 |
+
cls = spec.get("cls")
|
| 47 |
+
cap = Q.ANCHOR_CAPTIONS.get(cls)
|
| 48 |
+
if cap is None:
|
| 49 |
+
return None
|
| 50 |
+
if (spec.get("material") and cls not in ("wizard", "archer", "zombie")
|
| 51 |
+
and f" {spec['material']}" not in cap):
|
| 52 |
+
art = "an" if spec["material"][0] in "aeiou" else "a"
|
| 53 |
+
cap = cap.replace("a ", f"{art} {spec['material']} ", 1)
|
| 54 |
+
return np.array(encode_caption(cap), dtype=np.int64)
|
| 55 |
+
|
| 56 |
+
def generate_pixels(self, text, seed=0, temperature=None, top_k=None,
|
| 57 |
+
retries=8, enforce_quality=True):
|
| 58 |
+
"""English instruction -> 16x16 grid of palette indices.
|
| 59 |
+
|
| 60 |
+
Escalating verify-and-retry schedule (all offline, self-contained):
|
| 61 |
+
attempt 0 raw sample
|
| 62 |
+
attempts 1-3 self-guidance: palette-logit bias, strength ramps
|
| 63 |
+
attempts 4-5 + spatial bias (face box / vial margins)
|
| 64 |
+
attempts 6-7 + canonical anchor caption (prompt normalization)
|
| 65 |
+
The first sprite passing the grounding checks wins; reject reasons
|
| 66 |
+
are counted and returned in the metadata dict."""
|
| 67 |
+
label, msg = self.should_ask(text)
|
| 68 |
+
if label != "accept":
|
| 69 |
+
return None, {"gate": label, "message": msg}
|
| 70 |
+
|
| 71 |
+
spec = Q.parse_prompt(text)
|
| 72 |
+
anchor_ids = self._anchor_ids(spec)
|
| 73 |
+
struct_prefix = Q.STRUCTURAL_PREFIX.get(spec.get("cls"))
|
| 74 |
+
rejects = Counter()
|
| 75 |
+
grid = None
|
| 76 |
+
for k in range(max(1, retries)):
|
| 77 |
+
lb = None
|
| 78 |
+
ids = None
|
| 79 |
+
ptoks = struct_prefix if (struct_prefix is not None and k >= 3) else None
|
| 80 |
+
if k >= 1:
|
| 81 |
+
lb = Q.bias_from_spec(spec, strength=1.6 + 0.5 * min(k, 5))
|
| 82 |
+
if k >= 4:
|
| 83 |
+
combined = np.zeros((256, 32), dtype=np.float64)
|
| 84 |
+
if lb is not None:
|
| 85 |
+
combined[:] = np.asarray(lb, dtype=np.float64)[None, :]
|
| 86 |
+
combined += Q.positional_bias_from_spec(
|
| 87 |
+
spec, strength=1.4 + 0.3 * (k - 4))
|
| 88 |
+
lb = combined
|
| 89 |
+
if k >= 6 and anchor_ids is not None:
|
| 90 |
+
ids = anchor_ids # canonical anchor
|
| 91 |
+
grid = self.pipe.generate_grid(text, seed=seed + 1013 * k,
|
| 92 |
+
temperature=temperature,
|
| 93 |
+
top_k=top_k, logit_bias=lb,
|
| 94 |
+
ids_override=ids,
|
| 95 |
+
prefix_tokens=ptoks)
|
| 96 |
+
if enforce_quality:
|
| 97 |
+
ok, reasons = Q.check_sprite(grid, spec)
|
| 98 |
+
if ok:
|
| 99 |
+
return grid, {"gate": "accept", "seed": seed + 1013 * k,
|
| 100 |
+
"attempt": k, "rejects": dict(rejects)}
|
| 101 |
+
for r in reasons:
|
| 102 |
+
rejects[r] += 1
|
| 103 |
+
else:
|
| 104 |
+
return grid, {"gate": "accept", "seed": seed, "attempt": k,
|
| 105 |
+
"rejects": {}}
|
| 106 |
+
# fall back to the last sample rather than failing hard
|
| 107 |
+
return grid, {"gate": "accept_degraded", "seed": seed,
|
| 108 |
+
"attempt": retries, "rejects": dict(rejects)}
|
| 109 |
+
|
| 110 |
+
def generate_png(self, text, path, scale=8, seed=0, **kw):
|
| 111 |
+
grid, meta = self.generate_pixels(text, seed=seed, **kw)
|
| 112 |
+
if grid is None:
|
| 113 |
+
return None, meta
|
| 114 |
+
save_png(grid, Path(path), scale=scale)
|
| 115 |
+
meta["path"] = str(path)
|
| 116 |
+
return grid, meta
|
| 117 |
+
|
| 118 |
+
def generate_turnaround(self, text, seeds=(1, 2, 3, 4), **kw):
|
| 119 |
+
"""Several consistent samples of the same instruction (seed family);
|
| 120 |
+
tiny-model analog of the big sibling's multiview turnaround."""
|
| 121 |
+
grids = []
|
| 122 |
+
for s in seeds:
|
| 123 |
+
g, m = self.generate_pixels(text, seed=s, **kw)
|
| 124 |
+
grids.append((g, m))
|
| 125 |
+
return grids
|
| 126 |
+
|
| 127 |
+
def variations(self, text, k=4, start_seed=0, **kw):
|
| 128 |
+
return self.generate_turnaround(text, seeds=tuple(
|
| 129 |
+
start_seed + 37 * i for i in range(k)), **kw)
|