pixelgen / app.py
multimodalart's picture
multimodalart HF Staff
Upload app.py with huggingface_hub
a292e13 verified
Raw History Blame Contribute Delete
7.07 kB
"""PixelGen demo Space — a text-to-pixel-art model with 1,257 hand-written
parameters and zero training steps.
The `pixelgen/` package and the `weights/pixelgen.pt` checkpoint are vendored
from https://huggingface.co/DJLougen/pixelgen (MIT). The demo mirrors the
reference implementation (`PixelGen.sprite` / `generate.py`) 1:1: same
tokenizer, same forward pass, same nearest-neighbour upscale — it only adds a
UI. The model is tiny and CPU-bound, so inference runs directly in the web
process; `@spaces.GPU` stays as a portable no-op in case the hardware is ever
moved to ZeroGPU.
"""
import spaces # imported first; a no-op off ZeroGPU
import gradio as gr
from PIL import Image
from pixelgen import weights as weights_io
from pixelgen.model import PixelGen
from pixelgen.palette import COLOR_WORDS, RAMP_NAMES
from pixelgen.templates import CLASSES
from pixelgen.tokenizer import CLASS_SYNONYMS
MODEL_ID = "DJLougen/pixelgen"
DEFAULT_SEED = 0
DEFAULT_SCALE = 8 # 16 x 8 -> 128x128 PNG, the model's default output
def _load_model() -> PixelGen:
"""Load the shipped checkpoint; fall back to rebuilding weights in code.
`PixelGen()` sets every weight deterministically in `__init__`, so both
paths produce bit-identical sprites (the repo's round-trip test covers
this) — the fallback only guards against a missing/corrupt file.
"""
try:
model = weights_io.load() # resolves to weights/pixelgen.pt
print(f"loaded {MODEL_ID} from weights/pixelgen.pt")
except Exception as exc: # pragma: no cover - robustness fallback
print(f"checkpoint load failed ({exc!r}); rebuilding weights from source")
model = PixelGen()
return model
MODEL = _load_model()
N_PARAMS = sum(p.numel() for p in MODEL.parameters())
@spaces.GPU(duration=30)
def generate(prompt: str, seed: int = DEFAULT_SEED, scale: int = DEFAULT_SCALE):
"""Generate a PICO-8 pixel-art sprite from a short text prompt.
Args:
prompt: A colour word (red, blue, green, yellow, purple, orange, pink,
brown, white — plus synonyms such as gold, crimson, emerald) and a
class word (potion, sword, shield, ship, invader, slime, skull,
tree, key, heart, star — plus synonyms such as alien, blade).
For example "red potion" or "golden key". Unknown words are
ignored; a prompt with no known class renders the default sprite
(a green potion).
seed: Selects the hand-marked "?" pixels, giving small variations of
the same sprite across seeds.
scale: Nearest-neighbour upscale factor; the output PNG is
16*scale x 16*scale pixels (default 8 -> 128x128).
Returns:
(sprite, seed_strip): the sprite as a PIL image, plus the same prompt
rendered at seeds 0-7 to show the seed variation.
"""
seed, scale = int(seed), int(scale)
sprite = Image.fromarray(MODEL.generate([prompt], [seed], scale=scale)[0], "RGB")
strip_arr = MODEL.generate([prompt] * 8, list(range(8)), scale=scale)
strip = [Image.fromarray(arr, "RGB") for arr in strip_arr]
return sprite, strip
def build_prompt(body: str, colour: str) -> str:
"""Compose a prompt like 'red potion' from the two dropdowns."""
return f"{colour} {body}"
def vocab_markdown() -> str:
"""Render the model's fixed 59-word vocabulary as reference tables."""
cls_rows = "\n".join(f"| {c} | {', '.join(ws)} |" for c, ws in CLASS_SYNONYMS.items())
by_ramp: dict = {}
for word, ramp in COLOR_WORDS.items():
by_ramp.setdefault(ramp, []).append(word)
col_rows = "\n".join(f"| {r} | {', '.join(ws)} |" for r, ws in by_ramp.items())
return (
"Prompts are a bag of words: every known word counts, everything else is "
"ignored. A prompt with **no known class word** falls back to the default "
"class (**potion**) and one with **no known colour word** to the default "
"ramp (**green**) — so `\"dragon\"` gives you a green potion.\n\n"
"### Sprite classes\n"
"| class | words |\n|---|---|\n"
f"{cls_rows}\n\n"
"### Colour ramps\n"
"| colour | words |\n|---|---|\n"
f"{col_rows}"
)
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
.gradio-container img { image-rendering: pixelated; }
"""
EXAMPLES = [
["red potion", 0],
["golden key", 3],
["blue invader", 0],
["purple slime", 4],
]
with gr.Blocks(title="PixelGen") as demo:
gr.Markdown(
f"# 🕹️ PixelGen\n"
f"A text-to-pixel-art model with **{N_PARAMS:,} parameters — zero of them "
f"trained**. Every weight was typed in by hand: prompts like "
f"`\"red potion\"` or `\"blue invader\"` become 16×16 PICO-8 sprites, "
f"upscaled to crisp pixel art. "
f"[Model card](https://huggingface.co/{MODEL_ID}) · "
f"[weight-space programming](https://rikiyat.github.io/blog/weight-space-programming/)"
)
with gr.Column(elem_id="col-container"):
with gr.Row():
with gr.Column(scale=1):
prompt = gr.Textbox(
label="Prompt", value="red potion", placeholder="e.g. blue invader"
)
with gr.Accordion("…or build one", open=False):
body_dd = gr.Dropdown(CLASSES, value="potion", label="sprite class")
colour_dd = gr.Dropdown(RAMP_NAMES, value="red", label="colour")
for dd in (body_dd, colour_dd):
dd.change(build_prompt, [body_dd, colour_dd], prompt)
seed = gr.Slider(0, 99, value=DEFAULT_SEED, step=1, label="seed")
scale = gr.Slider(
4, 16, value=DEFAULT_SCALE, step=1,
label="scale (output is 16×scale px)",
)
go = gr.Button("Generate", variant="primary")
with gr.Column(scale=1):
out = gr.Image(label="sprite", type="pil", height=300)
gr.Markdown(
"**Same prompt, seeds 0–7** — a seed only toggles the hand-marked "
"`?` pixels, so seeds give small variations of the same sprite."
)
strip = gr.Gallery(columns=8, height=150, label="seeds")
with gr.Accordion("Prompt vocabulary", open=False):
gr.Markdown(vocab_markdown())
gr.Examples(
examples=EXAMPLES,
inputs=[prompt, seed],
outputs=[out, strip],
fn=generate,
cache_examples=True,
cache_mode="lazy",
)
go.click(generate, [prompt, seed, scale], [out, strip], api_name="generate")
prompt.submit(generate, [prompt, seed, scale], [out, strip])
demo.load(generate, [prompt, seed, scale], [out, strip])
if __name__ == "__main__":
# Gradio 6: theme/css are launch() parameters, not Blocks constructor ones.
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)