TONER-1-V

Natively multimodal, natively PostScript.

TONER-1-V reads the story TONER-1 just wrote and draws it: 16 × 16 pixels, one bit each, in toner. The image model fits in 103,536 parameters, and the printer still does all the work.

Everything below is literal, except the chart marked PARODY.

What it is

TONER-1-V is a 103,536-parameter transformer that draws 16 × 16 one-bit doodles in 18 classes. It generates a doodle as 64 tokens, one per 2 × 2 patch, and was trained on 171,000 drawings from Google's Quick, Draw! dataset (CC BY 4.0), rasterized to 16 × 16 one-bit. Its architecture is llama2.c's Llama-2-style decoder.

It runs inside PostScript, in the same print job as TONER-1 (our printer fine-tune of stories260K, 260,032 parameters). TONER-1-V_print-job.ps (859,530 bytes, 363,568 parameters in all) writes a short story about printers, finds the first noun in it that TONER-1-V can draw, and draws that noun next to the story, all on the printer's own CPU. If the story names nothing drawable, it draws a fallback class and the caption says so. Both models are int8 in the job.

Pipeline tag: text-to-image is the closest Hub task: the input is one of 18 class names (the print job picks it out of the story's text), and the output is a 16 × 16 one-bit image.

Status

Printed on an HP Color LaserJet Pro MFP M281fdw on 2026-11-05: the story in 3:38 and the doodle in 25 s by the printer's own clock, 4:04 from send to finished job.

A language model that ran on an actual printer: one print job, zero host FLOPs.

What the printer itself sent back over the network while this job ran: its own output and its own timers. These four lines are an excerpt; the story, the 16 × 16 doodle, its patch tokens and the memory report are left out here.

TONER-1 | 260032 params | int8 | PostScript-native inference
200 tokens in 217.806 s = 0.918248 tok/s [STEPS limit]
TONER-1-V | 103536 params | router: no drawable noun; class = DSEED mod 18 = 6 -> frog | seed 42 | T 0.9
64 patch tokens (65 forward passes) in 24.77 s = 2.58377 tok/s

Full logs, with every byte the printer sent back and when it arrived: docs/printer-run-2026-11-05 in the GitHub repo.

Sample output

TONER-1-V sample sheet: 18 classes, four doodles each (Ghostscript render)

A Ghostscript 10.07.1 render of samples/sample_sheet.pdf, not a photo of a printed page. All 72 doodles (18 classes × 4, seeds 1 to 72) were generated in a single Ghostscript run of the sample-sheet print job, at temperature 0.9, unedited and not cherry-picked.

How to run it

Preview in Ghostscript (no printer needed)

gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSEED=7 -o page.pdf TONER-1-V_print-job.ps

This takes a few seconds on an Apple M4 Mac and writes one illustrated story page. Stdout shows the story, which word picked the doodle, the doodle as ASCII art, and the timings. Optional parameters:

Name Default Meaning
SEED 42 story seed
TEMP 0.8 story temperature
STEPS 200 story token budget
DSEED SEED doodle seed
DTEMP 0.9 doodle temperature (0 gives argmax, which is always a blank doodle)

Print it

Only on a PostScript printer you own or have permission to use. Send the file raw:

nc <printer-ip> 9100 < TONER-1-V_print-job.ps      # raw TCP port 9100
lpr -P <queue> -o raw TONER-1-V_print-job.ps       # or through a raw CUPS queue

The default seed is 42. Its story names nothing drawable, so it draws a frog. PostScript's rand is implementation-defined, so another interpreter may tell a different story from the same seed and draw something else. The HP Color LaserJet Pro MFP M281fdw this job was printed on sent back the same story, the same frog and the same 64 patch tokens as Ghostscript, byte for byte apart from line endings (the printer sends CRLF). To change the seed, edit the 42 in /SEED 42 def.

The job needs a PostScript interpreter, Level 2 or later (it uses Level 1 operators plus rectstroke, and only standard fonts). A PCL- or GDI-only printer may print it as pages of text. On the HP, by the printer's own clock, the story took 3:38 and the doodle 25 s, and the job took 4:04 from send to finished job. A printer whose PostScript job timeout is shorter than that will cancel it.

Use the weights in Python

model.safetensors holds the float32 weights as trained, before int8 quantization, and config.json describes the architecture and the token layout. They load into llama2.c's model.py:

import json, torch
from safetensors.torch import load_file
from model import ModelArgs, Transformer      # model.py from github.com/karpathy/llama2.c

cfg = json.load(open("config.json"))
m = Transformer(ModelArgs(**cfg["model_args"]))
m.load_state_dict(load_file("model.safetensors"))
m.eval()

t = cfg["tokens"]
k = t["classes"].index("house")
seq = [t["BOS"], t["CLASS0"] + k]
with torch.no_grad():
    for _ in range(64):
        logits = m(torch.tensor([seq]))[0, -1]              # logits for the next token
        patch = logits[t["PATCH0"]:t["PATCH0"] + 16] / 0.9  # patch tokens only, temperature 0.9
        seq.append(t["PATCH0"] + int(torch.multinomial(torch.softmax(patch, -1), 1)))

v = [s - t["PATCH0"] for s in seq[2:]]                     # 64 patch values, 8 x 8 raster order
for r in range(16):
    print("".join("#" if (v[(r // 2) * 8 + c // 2] >> (3 - 2 * (r % 2) - c % 2)) & 1 else "."
                  for c in range(16)))

Specs

Parameters 103,536 (TONER-1-V); 363,568 in the combined print job, with TONER-1 (260,032)
Architecture llama2.c Transformer: 4 layers, dim 48, 6 query heads sharing 3 key/value heads, head size 8, SwiGLU feed-forward (hidden 128), RoPE, RMSNorm, tied input/output embedding
Vocabulary 36 tokens: BOS, EOS (unused), 16 patch values, 18 classes
Sequence BOS, a class token, then 64 patch tokens (66 in all)
Image 16 × 16 pixels, 1 bit each; patch value = 8·TL + 4·TR + 2·BL + 1·BR, in raster order over the 8 × 8 patch grid
Classes cat, dog, bird, fish, duck, rabbit, frog, teddy-bear, sun, tree, flower, house, car, apple, cake, soccer ball, light bulb, calculator
Weights model.safetensors: float32 as trained. In the print job: int8, one scale per row (src/doodle_weights.ps)
Print job 859,530 bytes, one US Letter page, Level 1 operators plus rectstroke

Training. The first 10,000 drawings of each class from Quick, Draw!'s 28 × 28 bitmap release were box-downsampled to 16 × 16 and thresholded to one bit; 5% of each class was held out (171,000 train, 9,000 validation). 16,000 steps of AdamW at batch size 256 (learning rate 2e-3 with cosine decay to 1e-4), about 12 minutes on an Apple M4. Validation loss: 0.868 nats per patch token (0.313 bits per pixel).

Router. In the combined job, the first whole word in the story that names a class picks the doodle (for example "puppy" draws a dog, "home" draws a house, "lamp" draws a light bulb; a plural "s" is allowed). If no word matches, the job draws class DSEED mod 18 and the caption says so.

Speed and accuracy of the port

Measurement Result Status
One doodle in Ghostscript 10.07.1, Apple M4 about 0.3 s (210 to 236 patch tokens/s over runs of 36 to 288 doodles) measured
One illustrated page (story and doodle) in Ghostscript about 2.8 s measured
One doodle on an HP Color LaserJet Pro MFP M281fdw 25 s by the printer's own clock: 64 patch tokens (65 forward passes) at 2.58 tok/s measured, one run (seed 42)
One illustrated page on the same HP story in 3:38 and doodle in 25 s by the printer's own clock; 4:04 from send to finished job measured, one run
The HP's output vs. Ghostscript's, seed 42 story, router line, frog and patch tokens byte-identical once the printer's CRLF line endings are mapped to LF; only the timings and the VM line differ measured
Ghostscript logits vs. a float64 reference (int8 weights) max abs difference 4.2e-5, with logits up to 27.1 measured

The 4.2e-5 gap is float32 rounding: a numpy re-implementation of the same arithmetic in float32 reproduces Ghostscript's logits bit for bit.

Blind test

An AI model that was not told the labels named 36 doodles (two per class, shuffled, drawn in black) from the print job. It got 24 of 36 right, where chance is 2 (95% Wilson interval: 50% to 80%).

Group Correct
All 36 doodles 24 of 36 (66.7%)
Objects (10 classes) 17 of 20
Animals (8 classes, counting teddy-bear) 7 of 16
Dog, rabbit, frog 0 of 2 each

Caveat: an AI model judged these doodles, not people, so this is not a human study. 36 doodles is a small sample, and with two per class the per-class results are anecdotes. The full grid with every answer is in docs/blind_test_results.pdf.

Files

File What it is
TONER-1-V_print-job.ps The combined print job: TONER-1's story model and engine, the doodle weights, the doodle engine and the page program (859,530 bytes)
src/doodle_engine.ps The doodle sampler and drawing code; it reuses TONER-1's forward pass unchanged
src/doodle_weights.ps The int8 doodle weights, one scale per row, in one PostScript dictionary
model.safetensors The float32 weights as trained (39 tensors; output.weight equals tok_embeddings.weight)
config.json Architecture, token layout and sampling settings
docs/TONER-1-V_booklet.pdf Six pages: the parody Pareto chart, the sample sheet, three illustrated story pages (seeds 7, 4 and 6: a dog, the sun and a fish), the blind-test results (rendered in Ghostscript before the printer run)
samples/sample_sheet.pdf 72 doodles, 18 classes × 4, produced by one print job in Ghostscript
samples/sample_sheet_ghostscript-render.png A 110 dpi Ghostscript render of the sample sheet
docs/blind_test_results.pdf The blind test: 36 doodles, the true classes and the AI model's guesses (rendered in Ghostscript before the printer run)

Limitations

  • 16 × 16 pixels at one bit each. Objects come out better than animals; ducks, birds, frogs, rabbits and dogs are often scribbles.
  • 18 classes only, and the router matches whole words only ("sunshine" is not "sun").
  • Temperature 0 gives a blank doodle: the blank patch wins every position.
  • Seeds are not guaranteed to be portable between interpreters (rand is implementation-defined), although the HP matched Ghostscript at seed 42.
  • Printer speeds come from one run on one printer. Other printers have not been tried.

The PostScript Pareto Frontier (PARODY)

Page 1 of the booklet is a parody chart. PARODY. Not a real evaluation. It plots every TONER model at 100% "share of the model that can execute inside a print job" and frontier models from major labs at 0%. They were not run on this chart and are not named, and 0% means only that they cannot execute inside a print job.

Credits

License

The TONER-1-V weights are CC BY 4.0; see LICENSE. They were trained on Google's Quick, Draw! dataset (CC BY 4.0), rasterized to 16 × 16 one-bit. The combined print job also contains the TONER-1 story model (our fine-tune of stories260K) and the TONER-1 engine, which are MIT-licensed. No Llama 2 materials are in this repository.

Links


Not affiliated with or endorsed by Adobe, HP, Artifex, Google, or any AI lab.

We're proudly pushing forward the PostScript Pareto Frontier.

Downloads last month
28
Safetensors
Model size
105k params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Paper for jacobalansussman/TONER-1-V