--- license: mit pipeline_tag: text-to-image library_name: pytorch language: - en tags: - text-to-image - rectified-flow - flow-matching - unet - cross-attention - character-level - text-rendering - from-scratch - synthetic-data - pixel-space - small-model --- # grinpress ๐Ÿ—ž๏ธ๐Ÿพ *woof woof! hot off the press: tiny inky faces, freshly fetched :3* hiii, welcome to **grinpress**! it's a smol (**33.86M params**) text-to-image model, trained totally from scratch with **rectified flow** right in pixel space ๐Ÿพ. you tell it a color, a mood, and a lil word, and it barks back a 128ร—128 vintage-newspaper halftone face with your word stamped beside it, all inky and smudgy on old yellowed newsprint. *bark!* :3 ``` a red smiley face with 'HELLO' next to it ``` ![final sample](samples/step_0060000.png) ๐ŸŽฎ **try it live:** [huggingface.co/spaces/skyuu72/grinpress-demo](https://huggingface.co/spaces/skyuu72/grinpress-demo) (ZeroGPU, about 1 s per batch!) ๐Ÿพ made with lots of tail wags by **the puppygirl CEO of Quantara ([quantara.cv](https://quantara.cv))** ๐Ÿพ๐Ÿ’› ## Model ๐Ÿง ๐Ÿพ | part | params | details | |---|---|---| | U-Net | 32.23M | levels 128/64/32/16 with channels 64/128/192/256; 2 ResBlocks per level; AdaGN timestep conditioning (sinusoidal embedding into an MLP); self-attention and masked **char-level cross-attention** at the 32/16 levels and in the mid block | | Text encoder | 1.63M | ASCII byte tokenizer (max 64 chars, full caption); learned embeddings + sinusoidal positions; 2-layer transformer (d=256); 10% CFG dropout to a learned null sequence | | **Total** | **33.86M** | see `config.json` | Flow: `x_t = (1-t)ยทx0 + tยทฮต` (t=0 is data, t=1 is noise). The network predicts `v = ฮต โˆ’ x0` and is trained with MSE, with t sampled logit-normal. Sampling uses Euler integration from t=1 to t=0 with classifier-free guidance `v = v_u + sยท(v_c โˆ’ v_u)`. ## Training ๐Ÿ‹๏ธ๐Ÿพ - **Data:** 50k procedurally generated 128px images (`generate_data.py`): aged newsprint, a halftone face in one of 12 colors, a smiley/sad/meh expression, and 1โ€“8 characters of wood-type/serif text with ink bleed and distressed edges. - **Schedule:** 60k steps at batch 64. AdamW at lr 2e-4 with 1k warmup steps, then cosine decay. bf16 autocast and EMA (0.9999). The released weights are the EMA weights. - **Hardware:** a single A100 on Google Colab, about 7 h wall clock including 2 resumes from checkpoint. ### Training progression (watch the pup learn!) :3 Fixed prompts and seeds at each checkpoint. Rows: red smiley 'HELLO', blue sad 'NEWS', green meh 'OK', yellow smiley 'XQ7'. | 5k | 20k | 40k | 60k | |---|---|---|---| | ![](samples/step_0005000.png) | ![](samples/step_0020000.png) | ![](samples/step_0040000.png) | ![](samples/step_0060000.png) | A fresh sample from the released weights (`a pink sad face with 'HEY' next to it`, 50 steps, CFG 5) ๐Ÿพ: ![v1 sample](samples/v1_pink_sad_HEY.png) Every grid, saved every 5k steps, is in [`samples/`](samples/). Layout and color settle early, and later steps mostly sharpen the letterforms; random strings like 'XQ7' are clearly cleaner at 60k. ## Prompt format ๐Ÿฆด ``` a {color} {expression} face with '{text}' next to it ``` - **color:** red, orange, yellow, green, teal, blue, navy, purple, pink, brown, gray, cyan - **expression:** smiley, sad, meh - **text:** 1โ€“8 ASCII characters (trained mostly on uppercase words and random letters/digits) `sample.py` checks the prompt with a regex and gives a helpful error when it doesn't match. ## Usage ๐Ÿพ ```bash pip install torch numpy pillow opencv-python-headless python sample.py --prompt "a pink sad face with 'HEY' next to it" \ --checkpoint ema_final_bf16.pt --cfg_scale 5.0 --steps 50 --seed 0 --num_images 4 --out out.png ``` `sample.py` loads the `ema` entry of the checkpoint. Weights: `ema_final_bf16.pt` (about 68 MB, bf16 EMA; the loader upcasts it) and `model.safetensors` (the same bf16 EMA tensors, flat keys). woof, pick whichever you like! :3 Python: ```python import torch from model import build_model from sample import sample, parse_prompt m = build_model().eval() m.load_state_dict({k: v.float() for k, v in torch.load("ema_final_bf16.pt", map_location="cpu")["ema"].items()}) x = sample(m, parse_prompt("a teal meh face with 'OK' next to it")[3], 128, 50, 5.0, 0, "cpu") # [-1,1] ``` Or with safetensors (`pip install safetensors`): ```bash python sample.py --prompt "a navy smiley face with 'WOOF' next to it" --checkpoint model.safetensors --out woof.png ``` Training from scratch: see `smiley_flow_colab.ipynb` (it resumes from Drive across disconnects), or `run_pipeline.sh`. ## Inference speed ๐Ÿ’จ | setup | 50 steps + CFG (100 forward passes) | |---|---| | CPU, 8-thread Xeon, fp32 (**measured**) | 26.8 s/img (25 steps: 13.3 s); peak RAM about 0.9 GB | | A100, bf16 (*estimate*) | about 0.5โ€“1 s/img at batch 1; about 15โ€“20 img/s batched | | L4 (*estimate*) | about 1โ€“1.5 s/img; about 6 img/s batched | | T4 (*estimate*) | about 1.5โ€“2.5 s/img; about 3 img/s batched | A single forward pass is 66.1 GFLOPs at 128px (measured with the torch FLOP counter). The GPU numbers are estimates assuming 30โ€“35% of peak throughput; they were not measured. ## Limitations (she's a good girl, but...) ๐Ÿถ - Only the fixed procedural domain: one face plus a text string on newsprint. It is not a general-purpose image model. - Only 1โ€“8 characters of text and only the 12 trained colors. Prompts outside the template are rejected. - Letters are occasionally smudged or malformed, especially in long random strings and lowercase text. - 128px only. The code supports 256px, but these weights were not trained at that size. ## License ๐Ÿ“œ๐Ÿพ MIT. The fonts used to generate the training data are downloaded at runtime under their own OFL/Apache licenses and are not included. *arf arf, thanks for visiting! go make some inky lil faces :3* ๐Ÿพ๐Ÿ—ž๏ธ