Cartoon characters, expressions, props, outlines, story mode
Browse files- .gitattributes +1 -0
- README.md +43 -23
- demo_sheet.png +2 -2
- export_weights.py +83 -78
- handweights.py +403 -101
- story_sheet.png +3 -0
- weights.safetensors +2 -2
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
demo_sheet.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
demo_sheet.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
story_sheet.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -7,15 +7,21 @@ tags:
|
|
| 7 |
- no-training
|
| 8 |
- interpretability
|
| 9 |
- toy-model
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
# Hand Weights: a
|
| 13 |
|
| 14 |
-
Every one of this model's **
|
| 15 |
-
There is no dataset, no training run and no gradient descent. You can open the weights and
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |

|
| 21 |
|
|
@@ -23,22 +29,30 @@ say what each number does: row 12 of a plane tensor is literally the left edge o
|
|
| 23 |
|
| 24 |
| Stage | What it is | Weights |
|
| 25 |
|---|---|---|
|
| 26 |
-
| Embedding |
|
| 27 |
-
| Binder | gated recurrence: adjectives carry forward to the next object
|
| 28 |
| Slot decoder | linear layers that fall back to per-shape default colour / size / height | `decoder.default_*` |
|
| 29 |
-
|
|
| 30 |
-
| Renderer, layer
|
| 31 |
-
| Renderer, layer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
## Usage
|
| 37 |
|
| 38 |
```bash
|
| 39 |
-
python handweights.py "a
|
|
|
|
| 40 |
python handweights.py --demo
|
| 41 |
-
python export_weights.py
|
| 42 |
```
|
| 43 |
|
| 44 |
Requires only `numpy` and `pillow`. `handweights.py` is the source of truth; the safetensors file is an
|
|
@@ -46,19 +60,25 @@ inspectable dump of the same numbers (vocab, embedding layout and renderer struc
|
|
| 46 |
|
| 47 |
## Vocabulary
|
| 48 |
|
| 49 |
-
- **
|
| 50 |
-
- **
|
| 51 |
-
- **
|
| 52 |
-
- **
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
## Limitations
|
| 55 |
|
| 56 |
-
-
|
| 57 |
-
- No counting ("two
|
| 58 |
-
|
| 59 |
-
|
|
|
|
| 60 |
|
| 61 |
## Why
|
| 62 |
|
| 63 |
A response to claims that you can "set weights on a new model without training". You can, if the model
|
| 64 |
-
is small and the task is designed so each neuron has a nameable job. This is what that looks like
|
|
|
|
|
|
|
|
|
| 7 |
- no-training
|
| 8 |
- interpretability
|
| 9 |
- toy-model
|
| 10 |
+
- childrens-books
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Hand Weights: a picture-book model with no training
|
| 14 |
|
| 15 |
+
Every one of this model's **27,875 parameters was written by hand** or derived from geometry.
|
| 16 |
+
There is no dataset, no training run and no gradient descent. You can open the weights and say what
|
| 17 |
+
each number does: one row of a plane tensor is literally the left edge of the cat's ear.
|
| 18 |
|
| 19 |
+
It draws simple children's-book scenes from a sentence, and turns a short story into one page per sentence,
|
| 20 |
+
keeping each character the same colour from page to page.
|
| 21 |
+
|
| 22 |
+
**Try it:** [Space: Vishwasaidev/hand-weights](https://huggingface.co/spaces/Vishwasaidev/hand-weights) (runs in your browser)
|
| 23 |
+
|
| 24 |
+

|
| 25 |
|
| 26 |

|
| 27 |
|
|
|
|
| 29 |
|
| 30 |
| Stage | What it is | Weights |
|
| 31 |
|---|---|---|
|
| 32 |
+
| Embedding | 153 words → 46-dim vectors with slots for shape, pronoun, colour, position, size, expression, copula, conjunction, sky, ground | `encoder.embedding` (153×46) |
|
| 33 |
+
| Binder | gated recurrence with two registers: adjectives carry forward to the next object ("a **sleepy** cat"), position phrases bind back ("a cat **on the left**"), and after a copula adjectives bind back too ("the cat **was sleepy**") | masks, no free weights |
|
| 34 |
| Slot decoder | linear layers that fall back to per-shape default colour / size / height | `decoder.default_*` |
|
| 35 |
+
| Layout | spreads unplaced objects; a prop that follows a character is held by it (grip point on the prop → hand or foot anchor on the character) | `layout.prop_grip_scale`, `layout.hand` |
|
| 36 |
+
| Renderer, layer 1 | half-plane neurons per pixel: `s·(d − nx·x − ny·y)` | `renderer.<shape>.part<i>…` rows of `(nx, ny, d)` |
|
| 37 |
+
| Renderer, layer 2 | min-pool over a leaf's half-planes (exact AND at any size), then OR / AND-NOT / AND neurons compose leaves: circles and ellipses (32 tangent planes), a 5-point star (pentagon + 5 triangles), smiles (ring ∧ half-plane) | structure in metadata |
|
| 38 |
+
| Renderer, layer 3 | sigmoid, linear shading neuron, alpha-compositing gate. **Ink outlines** are the same shape with every bias `d` grown by ~2.4 px, drawn first in ink | constants |
|
| 39 |
+
|
| 40 |
+
Characters (cat, bunny, bear, kid) are part stacks plus one of 20 face stacks (`renderer.face.<character>.<expression>`):
|
| 41 |
+
happy, sad (frown, brows, tear), sleepy (closed-arc eyes and a Z), surprised, angry.
|
| 42 |
+
|
| 43 |
+
The 27,188 numbers in `weights.safetensors` are the tensors; the rest are per-part colours and flags stored in the
|
| 44 |
+
`renderer_structure` metadata. Skies, rain, water, grass and stars are hand-written formulas in the code, not neurons.
|
| 45 |
|
| 46 |
+
**Story mode is plain code, not weights:** it splits sentences, resolves he/she/it/they to the previous page's
|
| 47 |
+
characters, remembers each character's colour, and carries the scene forward when a sentence doesn't name one.
|
| 48 |
|
| 49 |
## Usage
|
| 50 |
|
| 51 |
```bash
|
| 52 |
+
python handweights.py "a sleepy orange cat with a red balloon on the grass" # -> out/<prompt>.png
|
| 53 |
+
python handweights.py --story "The bunny was happy. She found a pink flower. They were sleepy at night."
|
| 54 |
python handweights.py --demo
|
| 55 |
+
python export_weights.py # regenerates weights.safetensors
|
| 56 |
```
|
| 57 |
|
| 58 |
Requires only `numpy` and `pillow`. `handweights.py` is the source of truth; the safetensors file is an
|
|
|
|
| 60 |
|
| 61 |
## Vocabulary
|
| 62 |
|
| 63 |
+
- **Characters:** cat, bunny, bear, kid (+ kitten, rabbit, teddy, child, boy, girl)
|
| 64 |
+
- **Feelings:** happy, sad, sleepy, surprised, angry (+ synonyms like tired, grumpy, crying, shocked)
|
| 65 |
+
- **Props they hold:** balloon, ball, flower, umbrella
|
| 66 |
+
- **Things:** sun, moon, star, cloud, house, tree, heart, circle, square, triangle
|
| 67 |
+
- **Colours:** red, orange, yellow, green, blue, purple, pink, white, black, brown, gray, cyan, gold, silver
|
| 68 |
+
- **Positions / sizes:** left, right, top, bottom, center · tiny, small, medium, big, huge
|
| 69 |
+
- **Scenes:** day, night, sunset, rain, space · grass, sea, desert, snow (+ synonyms like meadow, beach, winter)
|
| 70 |
|
| 71 |
## Limitations
|
| 72 |
|
| 73 |
+
- 153 known words; anything else is ignored (the app shows which).
|
| 74 |
+
- No counting ("two cats" draws one), no actions or poses beyond standing and holding, no relations
|
| 75 |
+
("the cat sat under the tree" just places both). Every character faces forward.
|
| 76 |
+
- "It started to rain" resolves "it" to the last character. That's harmless here, but it shows the pronoun rule is naive.
|
| 77 |
+
- It can't learn or generalise. Every new character is roughly an hour of hand geometry.
|
| 78 |
|
| 79 |
## Why
|
| 80 |
|
| 81 |
A response to claims that you can "set weights on a new model without training". You can, if the model
|
| 82 |
+
is small and the task is designed so each neuron has a nameable job. This is what that looks like, and the
|
| 83 |
+
trade-off is also its strength for children's books: the same character looks identical on every page,
|
| 84 |
+
and it can only ever draw what is in its parts list.
|
demo_sheet.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
export_weights.py
CHANGED
|
@@ -1,78 +1,83 @@
|
|
| 1 |
-
"""Dump every hand-written number in handweights.py to weights.safetensors.
|
| 2 |
-
|
| 3 |
-
The safetensors format is an 8-byte little-endian header length, a JSON header,
|
| 4 |
-
then raw tensor bytes, so it is written here with numpy alone.
|
| 5 |
-
"""
|
| 6 |
-
import json
|
| 7 |
-
import struct
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
|
| 10 |
-
import numpy as np
|
| 11 |
-
|
| 12 |
-
from handweights import DEF_COLOR, DEF_SIZE, DEF_Y, DIM,
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
"
|
| 17 |
-
"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
"
|
| 34 |
-
"decoder.
|
| 35 |
-
"decoder.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dump every hand-written number in handweights.py to weights.safetensors.
|
| 2 |
+
|
| 3 |
+
The safetensors format is an 8-byte little-endian header length, a JSON header,
|
| 4 |
+
then raw tensor bytes, so it is written here with numpy alone.
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
import struct
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
from handweights import (CLR, COL, DEF_COLOR, DEF_SIZE, DEF_Y, DIM, EXP, EXPRS, FACES, GND, GROUNDS, HAND, OBJ, POS,
|
| 13 |
+
PARTS, PRED, PROPS, REF, SHAPES, SIZ, SKIES, SKY, VOCAB, count_params)
|
| 14 |
+
|
| 15 |
+
LAYOUT = {"shape_onehot": [0, OBJ], "is_object": OBJ, "pronoun(1=singular,2=plural)": REF,
|
| 16 |
+
"rgb_has_color": [COL, COL + 4], "dx_dy_has_x_has_y": [POS, POS + 4], "size_has_size": [SIZ, SIZ + 2],
|
| 17 |
+
f"expression_onehot({','.join(EXPRS)})": [EXP, PRED], "copula": PRED, "conjunction": CLR,
|
| 18 |
+
f"sky_onehot({','.join(SKIES)})": [SKY, GND], f"ground_onehot({','.join(GROUNDS)})": [GND, DIM]}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def leaves(g, name, out):
|
| 22 |
+
"""Flatten an AND/OR/MINUS tree into named plane tensors plus a structure string."""
|
| 23 |
+
op, arg = g
|
| 24 |
+
if op == "and":
|
| 25 |
+
out[name] = arg
|
| 26 |
+
return name
|
| 27 |
+
return f"{op}(" + ", ".join(leaves(c, f"{name}.{i}", out) for i, c in enumerate(arg)) + ")"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def collect():
|
| 31 |
+
words = list(VOCAB)
|
| 32 |
+
tensors = {
|
| 33 |
+
"encoder.embedding": np.stack([VOCAB[w] for w in words]),
|
| 34 |
+
"decoder.default_color": DEF_COLOR,
|
| 35 |
+
"decoder.default_size": DEF_SIZE,
|
| 36 |
+
"decoder.default_y": np.array([y if isinstance(y, float) else np.nan for y in DEF_Y], float),
|
| 37 |
+
"layout.prop_grip_scale": np.array([[*g, sc] for _, g, sc in PROPS.values()], float),
|
| 38 |
+
"layout.hand": np.array(list(HAND.values()), float),
|
| 39 |
+
}
|
| 40 |
+
structure = {}
|
| 41 |
+
groups = list(PARTS.items()) + [(f"face.{k}.{e}", parts) for (k, e), parts in sorted(FACES.items())]
|
| 42 |
+
for name, parts in groups:
|
| 43 |
+
for i, (geom, col, alpha, shaded, outline) in enumerate(parts):
|
| 44 |
+
key = f"renderer.{name}.part{i}"
|
| 45 |
+
structure[key] = dict(geometry=leaves(geom, key, tensors), color=col if isinstance(col, str) else list(col),
|
| 46 |
+
alpha=alpha, shaded=shaded, outline=outline)
|
| 47 |
+
meta = {"vocab": json.dumps(words), "shapes": json.dumps(SHAPES), "embedding_layout": json.dumps(LAYOUT),
|
| 48 |
+
"props": json.dumps(list(PROPS)), "default_y_special": "NaN = stands on the ground line",
|
| 49 |
+
"renderer_structure": json.dumps(structure),
|
| 50 |
+
"plane_rows": "each row (nx, ny, d) is a half-plane neuron firing where nx*px + ny*py <= d; a leaf = min-pool over its rows",
|
| 51 |
+
"parameter_count": str(count_params()), "trained": "false"}
|
| 52 |
+
return tensors, meta
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def save_safetensors(tensors, meta, path):
|
| 56 |
+
header, blobs, offset = {"__metadata__": meta}, [], 0
|
| 57 |
+
for name, t in tensors.items():
|
| 58 |
+
b = np.ascontiguousarray(t, dtype="<f4").tobytes()
|
| 59 |
+
header[name] = {"dtype": "F32", "shape": list(t.shape), "data_offsets": [offset, offset + len(b)]}
|
| 60 |
+
blobs.append(b)
|
| 61 |
+
offset += len(b)
|
| 62 |
+
h = json.dumps(header, separators=(",", ":")).encode()
|
| 63 |
+
h += b" " * (-len(h) % 8)
|
| 64 |
+
Path(path).write_bytes(struct.pack("<Q", len(h)) + h + b"".join(blobs))
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_safetensors(path):
|
| 68 |
+
raw = Path(path).read_bytes()
|
| 69 |
+
n = struct.unpack("<Q", raw[:8])[0]
|
| 70 |
+
header = json.loads(raw[8:8 + n])
|
| 71 |
+
meta = header.pop("__metadata__")
|
| 72 |
+
data = raw[8 + n:]
|
| 73 |
+
return {k: np.frombuffer(data[v["data_offsets"][0]:v["data_offsets"][1]], "<f4").reshape(v["shape"])
|
| 74 |
+
for k, v in header.items()}, meta
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
out = Path(__file__).parent / "weights.safetensors"
|
| 79 |
+
tensors, meta = collect()
|
| 80 |
+
save_safetensors(tensors, meta, out)
|
| 81 |
+
back, _ = load_safetensors(out)
|
| 82 |
+
assert all(np.allclose(back[k], np.asarray(v, "<f4"), equal_nan=True) for k, v in tensors.items())
|
| 83 |
+
print(f"{out.name}: {len(tensors)} tensors, {sum(t.size for t in tensors.values()):,} numbers, round-trip ok")
|
handweights.py
CHANGED
|
@@ -3,39 +3,49 @@
|
|
| 3 |
Every weight in this file was written by hand or derived from geometry.
|
| 4 |
No dataset, no training, no gradients.
|
| 5 |
|
| 6 |
-
python handweights.py "a
|
| 7 |
python handweights.py --demo
|
|
|
|
| 8 |
|
| 9 |
Architecture
|
| 10 |
-
1. Embedding table
|
| 11 |
-
2. Gated recurrent binder adjectives carry forward into the next object token,
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
"""
|
| 18 |
import re
|
| 19 |
import sys
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
import numpy as np
|
| 23 |
-
from PIL import Image, ImageDraw
|
| 24 |
|
| 25 |
RES = 384
|
| 26 |
OUT = Path(__file__).parent / "out"
|
| 27 |
|
| 28 |
# ------------------------------------------------------------------ embedding table
|
| 29 |
-
SHAPES = ["circle", "square", "triangle", "star", "moon", "heart", "house", "tree", "sun", "cloud"
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
GROUNDS = ["sea", "grass", "desert", "snow"]
|
| 32 |
|
| 33 |
S0 = 0 # shape one-hot
|
| 34 |
OBJ = S0 + len(SHAPES) # gate: "this token is an object"
|
| 35 |
-
|
|
|
|
| 36 |
POS = COL + 4 # dx, dy, has_x, has_y
|
| 37 |
SIZ = POS + 4 # size, has_size
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
GND = SKY + len(SKIES) # ground one-hot
|
| 40 |
DIM = GND + len(GROUNDS)
|
| 41 |
|
|
@@ -52,9 +62,13 @@ def word(names, entries):
|
|
| 52 |
|
| 53 |
for i, s in enumerate(SHAPES):
|
| 54 |
word(s, [(S0 + i, 1), (OBJ, 1)])
|
| 55 |
-
for syn, s in [("
|
| 56 |
-
("home cottage hut", "house"), ("love", "heart"), ("bush", "tree")
|
|
|
|
|
|
|
| 57 |
word(syn, [(S0 + SHAPES.index(s), 1), (OBJ, 1)])
|
|
|
|
|
|
|
| 58 |
|
| 59 |
COLORS = {
|
| 60 |
"red": (0.86, 0.16, 0.16), "green": (0.2, 0.62, 0.25), "blue": (0.2, 0.38, 0.85),
|
|
@@ -66,7 +80,7 @@ COLORS = {
|
|
| 66 |
for name, (r, g, b) in COLORS.items():
|
| 67 |
word(name, [(COL, r), (COL + 1, g), (COL + 2, b), (COL + 3, 1)])
|
| 68 |
|
| 69 |
-
for names, axis, val in [("left", 0, -0.5), ("right",
|
| 70 |
("bottom lower below", 1, 0.5)]:
|
| 71 |
word(names, [(POS + axis, val), (POS + 2 + axis, 1)])
|
| 72 |
word("center centre middle", [(POS + 2, 1), (POS + 3, 1)])
|
|
@@ -75,19 +89,27 @@ for names, s in [("tiny", 0.1), ("small little", 0.15), ("medium", 0.22), ("big
|
|
| 75 |
("huge giant enormous", 0.42)]:
|
| 76 |
word(names, [(SIZ, s), (SIZ + 1, 1)])
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
word("day sunny morning", [(SKY + 0, 1)])
|
| 79 |
word("sky", [(SKY + 0, 0.5)]) # "sky" leans day but loses to "night sky"
|
| 80 |
word("night starry", [(SKY + 1, 1)])
|
| 81 |
word("sunset dusk evening", [(SKY + 2, 1)])
|
| 82 |
word("space", [(SKY + 3, 1)])
|
|
|
|
| 83 |
word("sea ocean water lake beach", [(GND + 0, 1)])
|
| 84 |
word("grass field meadow park garden", [(GND + 1, 1)])
|
| 85 |
word("desert sand dunes", [(GND + 2, 1)])
|
| 86 |
word("snow snowy winter", [(GND + 3, 1)])
|
| 87 |
|
| 88 |
-
FWD = np.zeros(DIM); FWD[[COL, COL + 1, COL + 2, COL + 3, SIZ, SIZ + 1]] = 1
|
| 89 |
-
BACK = np.zeros(DIM); BACK[POS:POS + 4] = 1
|
| 90 |
-
SCN = np.zeros(DIM); SCN[SKY:] = 1
|
| 91 |
|
| 92 |
|
| 93 |
def lookup(t):
|
|
@@ -101,6 +123,7 @@ def lookup(t):
|
|
| 101 |
# ------------------------------------------------------------------ gated recurrent binder
|
| 102 |
def encode(prompt):
|
| 103 |
pend = np.zeros(DIM) # adjectives waiting for an object
|
|
|
|
| 104 |
slots, scene, unknown = [], np.zeros(DIM), []
|
| 105 |
for t in re.findall(r"[a-z]+", prompt.lower()):
|
| 106 |
e = lookup(t)
|
|
@@ -108,27 +131,40 @@ def encode(prompt):
|
|
| 108 |
unknown.append(t)
|
| 109 |
continue
|
| 110 |
g = e[OBJ]
|
|
|
|
| 111 |
has_last = 1.0 if slots else 0.0
|
|
|
|
| 112 |
if g:
|
| 113 |
slots.append(pend + e)
|
| 114 |
elif has_last:
|
| 115 |
-
slots[-1] = slots[-1] + BACK * e
|
| 116 |
-
pend = (1 - g) * (pend + FWD * e + (1 - has_last) * BACK * e)
|
| 117 |
scene += SCN * e
|
| 118 |
return slots, scene, unknown
|
| 119 |
|
| 120 |
|
| 121 |
# ------------------------------------------------------------------ slot decoder weights
|
| 122 |
-
# circle square tri star moon heart house tree sun cloud
|
| 123 |
DEF_COLOR = np.array([
|
| 124 |
(0.2, 0.38, 0.85), (0.86, 0.16, 0.16), (0.98, 0.55, 0.15), (0.98, 0.84, 0.2),
|
| 125 |
(0.96, 0.93, 0.75), (0.9, 0.2, 0.35), (0.93, 0.87, 0.75), (0.2, 0.58, 0.25),
|
| 126 |
-
(0.99, 0.8, 0.2), (0.97, 0.97, 0.98)
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
def decode(slot):
|
|
|
|
| 132 |
k = int(np.argmax(slot[S0:OBJ]))
|
| 133 |
onehot = slot[S0:OBJ] / max(slot[S0:OBJ].sum(), 1)
|
| 134 |
hc = min(slot[COL + 3], 1)
|
|
@@ -137,18 +173,35 @@ def decode(slot):
|
|
| 137 |
size = hs * slot[SIZ] / max(slot[SIZ + 1], 1) + (1 - hs) * onehot @ DEF_SIZE
|
| 138 |
x = slot[POS] * 1.2 if slot[POS + 2] else None
|
| 139 |
y = slot[POS + 1] * 1.2 if slot[POS + 3] else None
|
| 140 |
-
|
|
|
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
def layout(objs):
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
for o, x in zip(free, np.linspace(-0.55, 0.55, len(free)) if len(free) > 1 else [0.0] * len(free)):
|
| 146 |
o["x"] = x
|
| 147 |
-
for o in objs:
|
| 148 |
-
if o["y"] is None:
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
o["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
return objs
|
| 153 |
|
| 154 |
|
|
@@ -159,6 +212,21 @@ def circle(cx, cy, r, n=32):
|
|
| 159 |
return ("and", np.stack([np.cos(a), np.sin(a), r + np.cos(a) * cx + np.sin(a) * cy], 1))
|
| 160 |
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
def poly(verts):
|
| 163 |
v = np.array(verts, float)
|
| 164 |
c = v.mean(0)
|
|
@@ -176,6 +244,20 @@ def rect(cx, cy, w, h):
|
|
| 176 |
return poly([(cx - w / 2, cy - h / 2), (cx + w / 2, cy - h / 2), (cx + w / 2, cy + h / 2), (cx - w / 2, cy + h / 2)])
|
| 177 |
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
def star5():
|
| 180 |
out = [(np.cos(np.radians(-90 + 72 * k)), np.sin(np.radians(-90 + 72 * k))) for k in range(5)]
|
| 181 |
inn = [(0.4 * np.cos(np.radians(-54 + 72 * k)), 0.4 * np.sin(np.radians(-54 + 72 * k))) for k in range(5)]
|
|
@@ -192,48 +274,159 @@ def sun_rays():
|
|
| 192 |
return ("or", rays)
|
| 193 |
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
ROOF, DOOR, WINDOW, TRUNK = (0.58, 0.2, 0.14), (0.38, 0.22, 0.12), (0.98, 0.92, 0.6), (0.45, 0.28, 0.12)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
# Each shape is a stack of parts: (geometry, colour
|
| 198 |
PARTS = {
|
| 199 |
-
"circle": [(circle(0, 0, 1)
|
| 200 |
-
"square": [(rect(0, 0, 1.7, 1.7)
|
| 201 |
-
"triangle": [(poly([(0, -0.95), (1, 0.75), (-1, 0.75)])
|
| 202 |
-
"star": [(star5()
|
| 203 |
-
"moon": [(("minus", [circle(0, 0, 1), circle(0.45, -0.3, 0.85)])
|
| 204 |
-
"heart": [(("or", [circle(-0.42, -0.3, 0.5), circle(0.42, -0.3, 0.5),
|
| 205 |
-
|
| 206 |
-
"house": [(rect(0, 0.3, 1.4, 1.1)
|
| 207 |
-
(poly([(0, -0.95), (0.95, -0.22), (-0.95, -0.22)]), ROOF
|
| 208 |
-
(rect(0, 0.55, 0.32, 0.6), DOOR,
|
| 209 |
-
(rect(-0.42, 0.15, 0.3, 0.3), WINDOW,
|
| 210 |
-
(rect(0.42, 0.15, 0.3, 0.3), WINDOW,
|
| 211 |
-
"tree": [(rect(0, 0.55, 0.22, 0.9), TRUNK
|
| 212 |
-
(("or", [circle(0, -0.35, 0.55), circle(-0.38, 0, 0.45), circle(0.38, 0, 0.45)])
|
| 213 |
-
"sun": [(circle(0, 0, 1.6),
|
| 214 |
-
(sun_rays(),
|
| 215 |
-
(circle(0, 0, 1)
|
| 216 |
-
"cloud": [(("or", [circle(-0.5, 0.1, 0.42), circle(0, -0.15, 0.58), circle(0.52, 0.12, 0.4),
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
| 219 |
CASTS_SHADOW = {"circle", "square", "triangle", "heart", "house", "tree"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
|
| 222 |
def sig(z):
|
| 223 |
return 0.5 * (1 + np.tanh(0.5 * z))
|
| 224 |
|
| 225 |
|
| 226 |
-
def mask(g, px, py, sharp):
|
| 227 |
op, arg = g
|
| 228 |
-
if op == "and": # layer 1 half-
|
| 229 |
-
|
| 230 |
-
return sig(
|
| 231 |
-
ms = [mask(c, px, py, sharp) for c in arg]
|
| 232 |
if op == "or":
|
| 233 |
return sig(12 * (sum(ms) - 0.5))
|
|
|
|
|
|
|
| 234 |
return sig(12 * (ms[0] - ms[1] - 0.5)) # minus: AND-NOT neuron
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
# ------------------------------------------------------------------ backgrounds
|
| 238 |
U, V = np.meshgrid(np.linspace(-1, 1, RES), np.linspace(-1, 1, RES))
|
| 239 |
HORIZON = 0.3
|
|
@@ -243,12 +436,22 @@ def lerp(a, b, t):
|
|
| 243 |
return np.array(a)[None, None] * (1 - t[..., None]) + np.array(b)[None, None] * t[..., None]
|
| 244 |
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
def stars(threshold):
|
| 247 |
-
|
| 248 |
-
h = np.abs(np.modf(np.sin(i * 12.9898 + j * 78.233) * 43758.5453)[0])
|
| 249 |
return ((h > threshold) * (h - threshold) / (1 - threshold) * 0.6 + (h > threshold) * 0.4)[..., None]
|
| 250 |
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
def background(sky, ground):
|
| 253 |
t = (V + 1) / 2
|
| 254 |
if sky is None:
|
|
@@ -261,76 +464,181 @@ def background(sky, ground):
|
|
| 261 |
elif sky == "sunset":
|
| 262 |
img = lerp((0.22, 0.18, 0.45), (0.95, 0.45, 0.35), np.clip(t / 0.6, 0, 1))
|
| 263 |
img = img + lerp((0, 0, 0), (0.05, 0.35, 0.1), np.clip((t - 0.6) / 0.4, 0, 1))
|
|
|
|
|
|
|
| 264 |
else:
|
| 265 |
img = np.clip(lerp((0.0, 0.0, 0.02), (0.04, 0.02, 0.08), t) + stars(0.97), 0, 1)
|
| 266 |
-
if ground is None:
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
return np.clip(img
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
|
| 283 |
# ------------------------------------------------------------------ render
|
| 284 |
-
def render(prompt):
|
| 285 |
slots, scene, unknown = encode(prompt)
|
| 286 |
-
objs =
|
|
|
|
|
|
|
| 287 |
sky = SKIES[int(np.argmax(scene[SKY:GND]))] if scene[SKY:GND].any() else None
|
| 288 |
ground = GROUNDS[int(np.argmax(scene[GND:]))] if scene[GND:].any() else None
|
|
|
|
|
|
|
|
|
|
| 289 |
if ground and sky is None:
|
| 290 |
sky = "day"
|
| 291 |
img = background(sky, ground)
|
| 292 |
for o in objs:
|
| 293 |
r = o["size"]
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
if o["kind"] in CASTS_SHADOW:
|
| 297 |
-
sh = sum(mask(g, px - 0.12, py - 0.12, sharp) * a for g, _, a, _ in
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
| 303 |
m = (mask(geom, px, py, sharp) * alpha)[..., None]
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
return Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)), objs, sky, ground, unknown
|
| 306 |
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
def count_params():
|
| 309 |
def geo(g):
|
| 310 |
return g[1].size if g[0] == "and" else sum(geo(c) for c in g[1])
|
| 311 |
-
|
| 312 |
-
return len(VOCAB) * DIM + DEF_COLOR.size + DEF_SIZE.size + len(DEF_Y)
|
|
|
|
| 313 |
|
| 314 |
|
| 315 |
def slug(p):
|
| 316 |
return re.sub(r"[^a-z0-9]+", "_", p.lower()).strip("_")[:60]
|
| 317 |
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
DEMO = [
|
| 320 |
"a yellow sun over the sea",
|
| 321 |
"a red house and a green tree on grass",
|
| 322 |
-
"a big pink heart",
|
| 323 |
"a white moon and a gold star in a night sky",
|
| 324 |
-
"a small purple triangle on the left and a big blue circle on the right",
|
| 325 |
"an orange sun and a cloud over the desert at sunset",
|
| 326 |
-
"
|
| 327 |
-
"a
|
|
|
|
|
|
|
| 328 |
]
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
if __name__ == "__main__":
|
| 331 |
OUT.mkdir(exist_ok=True)
|
| 332 |
-
prompts = DEMO if sys.argv[1:] == ["--demo"] else [" ".join(sys.argv[1:]) or DEMO[0]]
|
| 333 |
print(f"hand-written parameters: {count_params():,}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
tiles = []
|
| 335 |
for p in prompts:
|
| 336 |
im, objs, sky, ground, unknown = render(p)
|
|
@@ -338,14 +646,8 @@ if __name__ == "__main__":
|
|
| 338 |
tiles.append((p, im))
|
| 339 |
print(f"\n{p!r}\n sky={sky} ground={ground} ignored={unknown}")
|
| 340 |
for o in objs:
|
| 341 |
-
print(f" {o['kind']:<8}
|
|
|
|
| 342 |
if len(tiles) > 1:
|
| 343 |
-
|
| 344 |
-
sheet = Image.new("RGB", (cols * RES, ((len(tiles) + cols - 1) // cols) * (RES + cap)), "white")
|
| 345 |
-
d = ImageDraw.Draw(sheet)
|
| 346 |
-
for i, (p, im) in enumerate(tiles):
|
| 347 |
-
x, y = (i % cols) * RES, (i // cols) * (RES + cap)
|
| 348 |
-
sheet.paste(im, (x, y))
|
| 349 |
-
d.text((x + 8, y + RES + 10), p[:58], fill="black")
|
| 350 |
-
sheet.save(OUT / "demo_sheet.png")
|
| 351 |
print(f"\nsheet -> {OUT / 'demo_sheet.png'}")
|
|
|
|
| 3 |
Every weight in this file was written by hand or derived from geometry.
|
| 4 |
No dataset, no training, no gradients.
|
| 5 |
|
| 6 |
+
python handweights.py "a sleepy orange cat with a red balloon on the grass"
|
| 7 |
python handweights.py --demo
|
| 8 |
+
python handweights.py --story "The bunny was happy. She found a yellow balloon. Then it started to rain."
|
| 9 |
|
| 10 |
Architecture
|
| 11 |
+
1. Embedding table word -> vector (shape / colour / position / size / expression / scene slots)
|
| 12 |
+
2. Gated recurrent binder adjectives carry forward into the next object token, position phrases
|
| 13 |
+
("on the left") bind back to the last object, and after a copula
|
| 14 |
+
("the cat was sleepy") adjectives bind back too
|
| 15 |
+
3. Slot decoder linear layers with default-colour / default-size matrices
|
| 16 |
+
4. Per-pixel renderer layer 1: half-plane neurons (w = edge normal, b = edge offset)
|
| 17 |
+
layer 2: min-pool (AND) / OR / AND-NOT neurons that build shapes from half-planes
|
| 18 |
+
layer 3: shading neuron + alpha-compositing gate; outlines are the same
|
| 19 |
+
shape with every bias grown by ~2 pixels, drawn first in ink
|
| 20 |
"""
|
| 21 |
import re
|
| 22 |
import sys
|
| 23 |
from pathlib import Path
|
| 24 |
|
| 25 |
import numpy as np
|
| 26 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 27 |
|
| 28 |
RES = 384
|
| 29 |
OUT = Path(__file__).parent / "out"
|
| 30 |
|
| 31 |
# ------------------------------------------------------------------ embedding table
|
| 32 |
+
SHAPES = ["circle", "square", "triangle", "star", "moon", "heart", "house", "tree", "sun", "cloud",
|
| 33 |
+
"cat", "bunny", "bear", "kid", "balloon", "ball", "flower", "umbrella"]
|
| 34 |
+
CHARS = {"cat", "bunny", "bear", "kid"}
|
| 35 |
+
EXPRS = ["happy", "sad", "sleepy", "surprised", "angry"]
|
| 36 |
+
SKIES = ["day", "night", "sunset", "space", "rain"]
|
| 37 |
GROUNDS = ["sea", "grass", "desert", "snow"]
|
| 38 |
|
| 39 |
S0 = 0 # shape one-hot
|
| 40 |
OBJ = S0 + len(SHAPES) # gate: "this token is an object"
|
| 41 |
+
REF = OBJ + 1 # pronoun: 1 = he/she/it, 2 = they
|
| 42 |
+
COL = REF + 1 # r, g, b, has_color
|
| 43 |
POS = COL + 4 # dx, dy, has_x, has_y
|
| 44 |
SIZ = POS + 4 # size, has_size
|
| 45 |
+
EXP = SIZ + 2 # expression one-hot
|
| 46 |
+
PRED = EXP + len(EXPRS) # copula: following adjectives bind back
|
| 47 |
+
CLR = PRED + 1 # conjunction: clears the copula register
|
| 48 |
+
SKY = CLR + 1 # sky one-hot
|
| 49 |
GND = SKY + len(SKIES) # ground one-hot
|
| 50 |
DIM = GND + len(GROUNDS)
|
| 51 |
|
|
|
|
| 62 |
|
| 63 |
for i, s in enumerate(SHAPES):
|
| 64 |
word(s, [(S0 + i, 1), (OBJ, 1)])
|
| 65 |
+
for syn, s in [("dot planet", "circle"), ("box cube block", "square"), ("pyramid", "triangle"),
|
| 66 |
+
("home cottage hut", "house"), ("love", "heart"), ("bush", "tree"),
|
| 67 |
+
("kitty kitten", "cat"), ("rabbit bunnie", "bunny"), ("teddy", "bear"),
|
| 68 |
+
("child children boy girl", "kid"), ("rose daisy", "flower")]:
|
| 69 |
word(syn, [(S0 + SHAPES.index(s), 1), (OBJ, 1)])
|
| 70 |
+
word("he she it him her", [(OBJ, 1), (REF, 1)])
|
| 71 |
+
word("they them", [(OBJ, 1), (REF, 2)])
|
| 72 |
|
| 73 |
COLORS = {
|
| 74 |
"red": (0.86, 0.16, 0.16), "green": (0.2, 0.62, 0.25), "blue": (0.2, 0.38, 0.85),
|
|
|
|
| 80 |
for name, (r, g, b) in COLORS.items():
|
| 81 |
word(name, [(COL, r), (COL + 1, g), (COL + 2, b), (COL + 3, 1)])
|
| 82 |
|
| 83 |
+
for names, axis, val in [("left", 0, -0.5), ("right", 0, 0.5), ("top upper above", 1, -0.5),
|
| 84 |
("bottom lower below", 1, 0.5)]:
|
| 85 |
word(names, [(POS + axis, val), (POS + 2 + axis, 1)])
|
| 86 |
word("center centre middle", [(POS + 2, 1), (POS + 3, 1)])
|
|
|
|
| 89 |
("huge giant enormous", 0.42)]:
|
| 90 |
word(names, [(SIZ, s), (SIZ + 1, 1)])
|
| 91 |
|
| 92 |
+
for names, e in [("happy smiling glad joyful cheerful", "happy"), ("sad crying unhappy upset lonely", "sad"),
|
| 93 |
+
("sleepy tired asleep sleeping", "sleepy"), ("surprised shocked amazed scared", "surprised"),
|
| 94 |
+
("angry mad grumpy cross", "angry")]:
|
| 95 |
+
word(names, [(EXP + EXPRS.index(e), 1)])
|
| 96 |
+
word("is was are were looks looked feels felt seems seemed became got", [(PRED, 1)])
|
| 97 |
+
word("and with but then while so", [(CLR, 1)])
|
| 98 |
+
|
| 99 |
word("day sunny morning", [(SKY + 0, 1)])
|
| 100 |
word("sky", [(SKY + 0, 0.5)]) # "sky" leans day but loses to "night sky"
|
| 101 |
word("night starry", [(SKY + 1, 1)])
|
| 102 |
word("sunset dusk evening", [(SKY + 2, 1)])
|
| 103 |
word("space", [(SKY + 3, 1)])
|
| 104 |
+
word("rain rainy raining storm stormy", [(SKY + 4, 1)])
|
| 105 |
word("sea ocean water lake beach", [(GND + 0, 1)])
|
| 106 |
word("grass field meadow park garden", [(GND + 1, 1)])
|
| 107 |
word("desert sand dunes", [(GND + 2, 1)])
|
| 108 |
word("snow snowy winter", [(GND + 3, 1)])
|
| 109 |
|
| 110 |
+
FWD = np.zeros(DIM); FWD[[COL, COL + 1, COL + 2, COL + 3, SIZ, SIZ + 1]] = 1; FWD[EXP:PRED] = 1 # to next object
|
| 111 |
+
BACK = np.zeros(DIM); BACK[POS:POS + 4] = 1 # to last object
|
| 112 |
+
SCN = np.zeros(DIM); SCN[SKY:] = 1 # pooled scene
|
| 113 |
|
| 114 |
|
| 115 |
def lookup(t):
|
|
|
|
| 123 |
# ------------------------------------------------------------------ gated recurrent binder
|
| 124 |
def encode(prompt):
|
| 125 |
pend = np.zeros(DIM) # adjectives waiting for an object
|
| 126 |
+
pred = 0.0 # copula register: "was" makes the next adjectives bind back
|
| 127 |
slots, scene, unknown = [], np.zeros(DIM), []
|
| 128 |
for t in re.findall(r"[a-z]+", prompt.lower()):
|
| 129 |
e = lookup(t)
|
|
|
|
| 131 |
unknown.append(t)
|
| 132 |
continue
|
| 133 |
g = e[OBJ]
|
| 134 |
+
pred = max(pred * (1 - g) * (1 - e[CLR]), e[PRED])
|
| 135 |
has_last = 1.0 if slots else 0.0
|
| 136 |
+
to_back = pred * has_last
|
| 137 |
if g:
|
| 138 |
slots.append(pend + e)
|
| 139 |
elif has_last:
|
| 140 |
+
slots[-1] = slots[-1] + BACK * e + to_back * FWD * e
|
| 141 |
+
pend = (1 - g) * (pend + (1 - to_back) * FWD * e + (1 - has_last) * BACK * e)
|
| 142 |
scene += SCN * e
|
| 143 |
return slots, scene, unknown
|
| 144 |
|
| 145 |
|
| 146 |
# ------------------------------------------------------------------ slot decoder weights
|
|
|
|
| 147 |
DEF_COLOR = np.array([
|
| 148 |
(0.2, 0.38, 0.85), (0.86, 0.16, 0.16), (0.98, 0.55, 0.15), (0.98, 0.84, 0.2),
|
| 149 |
(0.96, 0.93, 0.75), (0.9, 0.2, 0.35), (0.93, 0.87, 0.75), (0.2, 0.58, 0.25),
|
| 150 |
+
(0.99, 0.8, 0.2), (0.97, 0.97, 0.98),
|
| 151 |
+
(0.98, 0.6, 0.25), (0.95, 0.94, 0.96), (0.62, 0.42, 0.25), (0.25, 0.55, 0.9),
|
| 152 |
+
(0.9, 0.2, 0.25), (0.95, 0.3, 0.25), (0.97, 0.5, 0.7), (0.3, 0.5, 0.9)])
|
| 153 |
+
DEF_SIZE = np.array([0.22, 0.2, 0.22, 0.2, 0.16, 0.2, 0.26, 0.24, 0.16, 0.18,
|
| 154 |
+
0.34, 0.34, 0.34, 0.34, 0.15, 0.1, 0.1, 0.2])
|
| 155 |
+
# None: stands on the ground line like a house; "char": stands on it with feet at local y = 1
|
| 156 |
+
DEF_Y = np.array([0.0, 0.0, 0.0, -0.5, -0.55, 0.0, None, None, -0.55, -0.6,
|
| 157 |
+
"char", "char", "char", "char", -0.2, 0.45, 0.3, -0.1], dtype=object)
|
| 158 |
+
|
| 159 |
+
# props held by the previous character: (anchor on the character, grip point on the prop, scale)
|
| 160 |
+
PROPS = {"balloon": ("hand", (-1.4, 3.0), 0.45), "ball": ("foot", (0.0, 1.0), 0.25),
|
| 161 |
+
"flower": ("hand", (0.0, 1.6), 0.24), "umbrella": ("hand", (0.0, 2.4), 0.7)}
|
| 162 |
+
HAND = {"cat": (0.45, 0.35), "bunny": (0.45, 0.35), "bear": (0.5, 0.35), "kid": (0.56, 0.52)}
|
| 163 |
+
FOOT = (0.75, 1.0)
|
| 164 |
|
| 165 |
|
| 166 |
def decode(slot):
|
| 167 |
+
ref = slot[REF]
|
| 168 |
k = int(np.argmax(slot[S0:OBJ]))
|
| 169 |
onehot = slot[S0:OBJ] / max(slot[S0:OBJ].sum(), 1)
|
| 170 |
hc = min(slot[COL + 3], 1)
|
|
|
|
| 173 |
size = hs * slot[SIZ] / max(slot[SIZ + 1], 1) + (1 - hs) * onehot @ DEF_SIZE
|
| 174 |
x = slot[POS] * 1.2 if slot[POS + 2] else None
|
| 175 |
y = slot[POS + 1] * 1.2 if slot[POS + 3] else None
|
| 176 |
+
ex = slot[EXP:PRED]
|
| 177 |
+
return dict(kind="ref" if ref else SHAPES[k], ref=int(ref), color=np.clip(color, 0, 1), size=size, x=x, y=y,
|
| 178 |
+
has_color=bool(hc), has_size=bool(hs), has_expr=bool(ex.any()), expr=EXPRS[int(np.argmax(ex))] if ex.any() else "happy")
|
| 179 |
|
| 180 |
|
| 181 |
def layout(objs):
|
| 182 |
+
for i, o in enumerate(objs): # "a kid with a flower on the left": the place belongs to the kid
|
| 183 |
+
if i and o["kind"] in PROPS and objs[i - 1]["kind"] in CHARS and objs[i - 1]["x"] is None and o["x"] is not None:
|
| 184 |
+
objs[i - 1]["x"], objs[i - 1]["y"], o["x"], o["y"] = o["x"], o["y"] if objs[i - 1]["y"] is None else objs[i - 1]["y"], None, None
|
| 185 |
+
held = [i > 0 and o["kind"] in PROPS and o["x"] is None and objs[i - 1]["kind"] in CHARS
|
| 186 |
+
for i, o in enumerate(objs)]
|
| 187 |
+
free = [o for o, h in zip(objs, held) if o["x"] is None and not h]
|
| 188 |
for o, x in zip(free, np.linspace(-0.55, 0.55, len(free)) if len(free) > 1 else [0.0] * len(free)):
|
| 189 |
o["x"] = x
|
| 190 |
+
for o, h in zip(objs, held):
|
| 191 |
+
if h or o["y"] is not None:
|
| 192 |
+
continue
|
| 193 |
+
d = DEF_Y[SHAPES.index(o["kind"])]
|
| 194 |
+
o["y"] = 0.55 - 0.9 * o["size"] if d is None else 0.62 - o["size"] if d == "char" else d
|
| 195 |
+
for i, (o, h) in enumerate(zip(objs, held)):
|
| 196 |
+
if h: # put the prop's grip point on the character's hand / foot
|
| 197 |
+
c = objs[i - 1]
|
| 198 |
+
anchor, grip, scale = PROPS[o["kind"]]
|
| 199 |
+
ax, ay = HAND[c["kind"]] if anchor == "hand" else FOOT
|
| 200 |
+
o["size"] = c["size"] * scale
|
| 201 |
+
o["x"] = c["x"] + ax * c["size"] - grip[0] * o["size"]
|
| 202 |
+
o["y"] = c["y"] + ay * c["size"] - grip[1] * o["size"]
|
| 203 |
+
else:
|
| 204 |
+
o["x"], o["y"] = float(np.clip(o["x"], -0.85, 0.85)), float(np.clip(o["y"], -0.85, 0.85))
|
| 205 |
return objs
|
| 206 |
|
| 207 |
|
|
|
|
| 212 |
return ("and", np.stack([np.cos(a), np.sin(a), r + np.cos(a) * cx + np.sin(a) * cy], 1))
|
| 213 |
|
| 214 |
|
| 215 |
+
def ellipse(cx, cy, rx, ry, rot=0.0, n=32):
|
| 216 |
+
t = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
| 217 |
+
nx, ny = np.cos(t) / rx, np.sin(t) / ry
|
| 218 |
+
norm = np.hypot(nx, ny)
|
| 219 |
+
nx, ny = nx / norm, ny / norm
|
| 220 |
+
d = nx * rx * np.cos(t) + ny * ry * np.sin(t)
|
| 221 |
+
c, s = np.cos(rot), np.sin(rot)
|
| 222 |
+
rx_, ry_ = c * nx - s * ny, s * nx + c * ny
|
| 223 |
+
return ("and", np.stack([rx_, ry_, d + rx_ * cx + ry_ * cy], 1))
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def halfplane(nx, ny, d):
|
| 227 |
+
return ("and", np.array([[nx, ny, d]], float))
|
| 228 |
+
|
| 229 |
+
|
| 230 |
def poly(verts):
|
| 231 |
v = np.array(verts, float)
|
| 232 |
c = v.mean(0)
|
|
|
|
| 244 |
return poly([(cx - w / 2, cy - h / 2), (cx + w / 2, cy - h / 2), (cx + w / 2, cy + h / 2), (cx - w / 2, cy + h / 2)])
|
| 245 |
|
| 246 |
|
| 247 |
+
def seg(x1, y1, x2, y2, w):
|
| 248 |
+
d = np.array([x2 - x1, y2 - y1], float)
|
| 249 |
+
p = np.array([-d[1], d[0]]) / np.linalg.norm(d) * w / 2
|
| 250 |
+
a, b = np.array([x1, y1]), np.array([x2, y2])
|
| 251 |
+
return poly([a - p, b - p, b + p, a + p])
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def arc(cx, cy, r, w, side, cut=0.0):
|
| 255 |
+
"""Ring of width w, keeping only the part below (smile) or above (frown) the centre."""
|
| 256 |
+
ring = ("minus", [circle(cx, cy, r + w / 2), circle(cx, cy, r - w / 2)])
|
| 257 |
+
keep = halfplane(0, -1, -(cy + cut * r)) if side == "lower" else halfplane(0, 1, cy - cut * r)
|
| 258 |
+
return ("both", [ring, keep])
|
| 259 |
+
|
| 260 |
+
|
| 261 |
def star5():
|
| 262 |
out = [(np.cos(np.radians(-90 + 72 * k)), np.sin(np.radians(-90 + 72 * k))) for k in range(5)]
|
| 263 |
inn = [(0.4 * np.cos(np.radians(-54 + 72 * k)), 0.4 * np.sin(np.radians(-54 + 72 * k))) for k in range(5)]
|
|
|
|
| 274 |
return ("or", rays)
|
| 275 |
|
| 276 |
|
| 277 |
+
def P(geom, color="main", alpha=1.0, shaded=True, outline=False):
|
| 278 |
+
return (geom, color, alpha, shaded, outline)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
ROOF, DOOR, WINDOW, TRUNK = (0.58, 0.2, 0.14), (0.38, 0.22, 0.12), (0.98, 0.92, 0.6), (0.45, 0.28, 0.12)
|
| 282 |
+
INK, WHITE, PINK, BLUSH = (0.16, 0.12, 0.12), (1.0, 1.0, 1.0), (0.98, 0.62, 0.68), (0.98, 0.5, 0.55)
|
| 283 |
+
SKIN, HAIR, SHOE, LEAF, TEAR = (0.98, 0.8, 0.65), (0.38, 0.23, 0.12), (0.3, 0.2, 0.18), (0.3, 0.62, 0.25), (0.45, 0.72, 1.0)
|
| 284 |
+
SIDES = (-1, 1)
|
| 285 |
+
GROUND_SHADOW = P(ellipse(0, 1.0, 0.62, 0.09), INK, 0.16, False)
|
| 286 |
|
| 287 |
+
# Each shape is a stack of parts: (geometry, colour, alpha, shaded, outline)
|
| 288 |
PARTS = {
|
| 289 |
+
"circle": [P(circle(0, 0, 1))],
|
| 290 |
+
"square": [P(rect(0, 0, 1.7, 1.7))],
|
| 291 |
+
"triangle": [P(poly([(0, -0.95), (1, 0.75), (-1, 0.75)]))],
|
| 292 |
+
"star": [P(star5())],
|
| 293 |
+
"moon": [P(("minus", [circle(0, 0, 1), circle(0.45, -0.3, 0.85)]))],
|
| 294 |
+
"heart": [P(("or", [circle(-0.42, -0.3, 0.5), circle(0.42, -0.3, 0.5),
|
| 295 |
+
poly([(-0.88, -0.15), (0.88, -0.15), (0, 1.0)])]))],
|
| 296 |
+
"house": [P(rect(0, 0.3, 1.4, 1.1)),
|
| 297 |
+
P(poly([(0, -0.95), (0.95, -0.22), (-0.95, -0.22)]), ROOF),
|
| 298 |
+
P(rect(0, 0.55, 0.32, 0.6), DOOR, shaded=False),
|
| 299 |
+
P(rect(-0.42, 0.15, 0.3, 0.3), WINDOW, shaded=False),
|
| 300 |
+
P(rect(0.42, 0.15, 0.3, 0.3), WINDOW, shaded=False)],
|
| 301 |
+
"tree": [P(rect(0, 0.55, 0.22, 0.9), TRUNK),
|
| 302 |
+
P(("or", [circle(0, -0.35, 0.55), circle(-0.38, 0, 0.45), circle(0.38, 0, 0.45)]))],
|
| 303 |
+
"sun": [P(circle(0, 0, 1.6), alpha=0.22, shaded=False),
|
| 304 |
+
P(sun_rays(), shaded=False),
|
| 305 |
+
P(circle(0, 0, 1))],
|
| 306 |
+
"cloud": [P(("or", [circle(-0.5, 0.1, 0.42), circle(0, -0.15, 0.58), circle(0.52, 0.12, 0.4),
|
| 307 |
+
rect(0, 0.3, 1.0, 0.35)]))],
|
| 308 |
+
# ---- characters: local units, feet on y = 1, ears up to about y = -1.15
|
| 309 |
+
"cat": [GROUND_SHADOW,
|
| 310 |
+
P(ellipse(0.62, 0.42, 0.12, 0.42, rot=0.5), outline=True),
|
| 311 |
+
P(ellipse(0, 0.5, 0.52, 0.5), outline=True),
|
| 312 |
+
P(ellipse(0, 0.6, 0.3, 0.3), "light", shaded=False),
|
| 313 |
+
*[P(ellipse(s * 0.25, 0.95, 0.17, 0.1), outline=True) for s in SIDES],
|
| 314 |
+
*[P(poly([(s * 0.62, -0.45), (s * 0.18, -0.8), (s * 0.55, -1.12)]), outline=True) for s in SIDES],
|
| 315 |
+
*[P(poly([(s * 0.5, -0.58), (s * 0.3, -0.74), (s * 0.5, -0.95)]), PINK, shaded=False) for s in SIDES],
|
| 316 |
+
P(ellipse(0, -0.32, 0.62, 0.52), outline=True),
|
| 317 |
+
P(poly([(-0.07, -0.27), (0.07, -0.27), (0, -0.19)]), PINK, shaded=False),
|
| 318 |
+
*[P(seg(s * 0.3, -0.2, s * 0.72, -0.27, 0.025), INK, shaded=False) for s in SIDES],
|
| 319 |
+
*[P(seg(s * 0.3, -0.12, s * 0.72, -0.08, 0.025), INK, shaded=False) for s in SIDES]],
|
| 320 |
+
"bunny": [GROUND_SHADOW,
|
| 321 |
+
P(circle(0.5, 0.72, 0.15), "light", outline=True),
|
| 322 |
+
*[P(ellipse(s * 0.22, -0.98, 0.14, 0.42, rot=s * 0.12), outline=True) for s in SIDES],
|
| 323 |
+
*[P(ellipse(s * 0.22, -0.96, 0.065, 0.3, rot=s * 0.12), PINK, shaded=False) for s in SIDES],
|
| 324 |
+
P(ellipse(0, 0.5, 0.5, 0.5), outline=True),
|
| 325 |
+
P(ellipse(0, 0.6, 0.28, 0.3), "light", shaded=False),
|
| 326 |
+
*[P(ellipse(s * 0.25, 0.95, 0.2, 0.1), outline=True) for s in SIDES],
|
| 327 |
+
P(ellipse(0, -0.3, 0.55, 0.48), outline=True),
|
| 328 |
+
P(ellipse(0, -0.22, 0.06, 0.045), PINK, shaded=False)],
|
| 329 |
+
"bear": [GROUND_SHADOW,
|
| 330 |
+
*[P(circle(s * 0.45, -0.72, 0.2), outline=True) for s in SIDES],
|
| 331 |
+
*[P(circle(s * 0.45, -0.72, 0.1), "light", shaded=False) for s in SIDES],
|
| 332 |
+
P(ellipse(0, 0.5, 0.58, 0.52), outline=True),
|
| 333 |
+
P(ellipse(0, 0.58, 0.34, 0.34), "light", shaded=False),
|
| 334 |
+
*[P(ellipse(s * 0.28, 0.95, 0.2, 0.11), outline=True) for s in SIDES],
|
| 335 |
+
P(ellipse(0, -0.3, 0.62, 0.55), outline=True),
|
| 336 |
+
P(ellipse(0, -0.1, 0.27, 0.2), "light", shaded=False),
|
| 337 |
+
P(ellipse(0, -0.19, 0.09, 0.06), INK, shaded=False)],
|
| 338 |
+
"kid": [GROUND_SHADOW,
|
| 339 |
+
*[P(rect(s * 0.15, 0.8, 0.14, 0.4), SKIN, outline=True) for s in SIDES],
|
| 340 |
+
*[P(ellipse(s * 0.17, 0.98, 0.15, 0.07), SHOE, outline=True) for s in SIDES],
|
| 341 |
+
*[P(seg(s * 0.28, 0.15, s * 0.55, 0.5, 0.14), SKIN, outline=True) for s in SIDES],
|
| 342 |
+
*[P(circle(s * 0.56, 0.52, 0.08), SKIN, outline=True) for s in SIDES],
|
| 343 |
+
P(poly([(-0.3, 0.02), (0.3, 0.02), (0.45, 0.7), (-0.45, 0.7)]), outline=True),
|
| 344 |
+
P(ellipse(0, -0.3, 0.57, 0.57), HAIR, outline=True),
|
| 345 |
+
P(ellipse(0, -0.3, 0.5, 0.5), SKIN, outline=True),
|
| 346 |
+
P(("both", [ellipse(0, -0.34, 0.53, 0.52), halfplane(0, 1, -0.5)]), HAIR)],
|
| 347 |
+
# ---- props
|
| 348 |
+
"balloon": [P(seg(0, 0.95, -1.4, 3.0, 0.04), INK, shaded=False),
|
| 349 |
+
P(poly([(-0.1, 1.06), (0.1, 1.06), (0, 0.9)]), outline=True),
|
| 350 |
+
P(ellipse(0, 0, 0.75, 0.95), outline=True),
|
| 351 |
+
P(ellipse(-0.28, -0.4, 0.12, 0.22, rot=0.5), WHITE, 0.7, False)],
|
| 352 |
+
"ball": [P(circle(0, 0, 1), outline=True),
|
| 353 |
+
P(("both", [circle(0, 0, 1), halfplane(0, 1, 0.22), halfplane(0, -1, 0.22)]), WHITE),
|
| 354 |
+
P(ellipse(-0.4, -0.45, 0.15, 0.1, rot=-0.6), WHITE, 0.7, False)],
|
| 355 |
+
"flower": [P(seg(0, 0.3, 0, 2.2, 0.12), LEAF, outline=True),
|
| 356 |
+
P(ellipse(0.32, 1.4, 0.3, 0.12, rot=-0.5), LEAF, outline=True),
|
| 357 |
+
*[P(circle(0.48 * np.cos(a), 0.48 * np.sin(a), 0.36), outline=True)
|
| 358 |
+
for a in np.radians(-90 + 72 * np.arange(5))],
|
| 359 |
+
P(circle(0, 0, 0.3), (0.98, 0.82, 0.25), outline=True)],
|
| 360 |
+
"umbrella": [P(seg(0, 0, 0, 2.6, 0.08), INK, shaded=False),
|
| 361 |
+
P(arc(0.2, 2.6, 0.2, 0.09, "lower"), INK, shaded=False),
|
| 362 |
+
P(("both", [ellipse(0, 0.05, 1.3, 1.0), halfplane(0, 1, 0.05)]), outline=True),
|
| 363 |
+
P(seg(0, -0.95, 0, -1.2, 0.08), INK, shaded=False)],
|
| 364 |
}
|
| 365 |
CASTS_SHADOW = {"circle", "square", "triangle", "heart", "house", "tree"}
|
| 366 |
+
EXTENT = {"balloon": 3.4, "umbrella": 3.0, "flower": 2.4, "bunny": 1.6, "sun": 1.7} # bbox half-size, local units
|
| 367 |
+
|
| 368 |
+
HEAD = {"cat": (0, -0.32, 0.55), "bunny": (0, -0.3, 0.5), "bear": (0, -0.3, 0.55), "kid": (0, -0.3, 0.45)}
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def face(hx, hy, hr, expr):
|
| 372 |
+
ex, ey = 0.36 * hr, hy - 0.05 * hr
|
| 373 |
+
mx, my = hx, hy + 0.38 * hr
|
| 374 |
+
eyes = [hx - ex, hx + ex]
|
| 375 |
+
f = []
|
| 376 |
+
if expr == "surprised":
|
| 377 |
+
for x in eyes:
|
| 378 |
+
f += [P(circle(x, ey, 0.19 * hr), INK, shaded=False), P(circle(x, ey, 0.15 * hr), WHITE, shaded=False),
|
| 379 |
+
P(circle(x, ey + 0.02 * hr, 0.07 * hr), INK, shaded=False)]
|
| 380 |
+
f.append(P(ellipse(mx, my + 0.03 * hr, 0.1 * hr, 0.13 * hr), INK, shaded=False))
|
| 381 |
+
elif expr == "sleepy":
|
| 382 |
+
f += [P(arc(x, ey - 0.08 * hr, 0.11 * hr, 0.045 * hr, "lower", 0.1), INK, shaded=False) for x in eyes]
|
| 383 |
+
f.append(P(ellipse(mx, my, 0.05 * hr, 0.06 * hr), INK, shaded=False))
|
| 384 |
+
zx, zy, s, w = hx + 0.9 * hr, hy - 0.8 * hr, 0.25 * hr, 0.06 * hr
|
| 385 |
+
f += [P(seg(zx, zy, zx + s, zy, w), INK, shaded=False), P(seg(zx + s, zy, zx, zy + s, w), INK, shaded=False),
|
| 386 |
+
P(seg(zx, zy + s, zx + s, zy + s, w), INK, shaded=False)]
|
| 387 |
+
else:
|
| 388 |
+
for x in eyes:
|
| 389 |
+
f += [P(ellipse(x, ey, 0.12 * hr, 0.16 * hr), INK, shaded=False),
|
| 390 |
+
P(circle(x + 0.04 * hr, ey - 0.06 * hr, 0.045 * hr), WHITE, shaded=False)]
|
| 391 |
+
if expr == "happy":
|
| 392 |
+
f.append(P(arc(mx, my - 0.25 * hr, 0.25 * hr, 0.06 * hr, "lower", 0.35), INK, shaded=False))
|
| 393 |
+
f += [P(ellipse(hx + s * 0.58 * hr, hy + 0.22 * hr, 0.13 * hr, 0.08 * hr), BLUSH, 0.55, False) for s in SIDES]
|
| 394 |
+
else:
|
| 395 |
+
f.append(P(arc(mx, my + 0.2 * hr, 0.2 * hr, 0.06 * hr, "upper", 0.35), INK, shaded=False))
|
| 396 |
+
for s, x in zip(SIDES, eyes): # sad: outer brow ends drop; angry: inner ends drop
|
| 397 |
+
inner, outer = (ey - 0.24 * hr, ey - 0.32 * hr) if expr == "angry" else (ey - 0.32 * hr, ey - 0.24 * hr)
|
| 398 |
+
f.append(P(seg(x - s * 0.13 * hr, inner, x + s * 0.13 * hr, outer, 0.05 * hr), INK, shaded=False))
|
| 399 |
+
if expr == "sad":
|
| 400 |
+
f.append(P(ellipse(eyes[1] + 0.06 * hr, ey + 0.25 * hr, 0.05 * hr, 0.08 * hr), TEAR, shaded=False))
|
| 401 |
+
return f
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
FACES = {(k, e): face(*HEAD[k], e) for k in CHARS for e in EXPRS}
|
| 405 |
|
| 406 |
|
| 407 |
def sig(z):
|
| 408 |
return 0.5 * (1 + np.tanh(0.5 * z))
|
| 409 |
|
| 410 |
|
| 411 |
+
def mask(g, px, py, sharp, grow=0.0):
|
| 412 |
op, arg = g
|
| 413 |
+
if op == "and": # layer 1 half-plane pre-activations -> layer 2 min-pool (exact AND at any size)
|
| 414 |
+
z = sharp * (arg[:, 2, None, None] + grow - arg[:, 0, None, None] * px - arg[:, 1, None, None] * py)
|
| 415 |
+
return sig(z.min(0))
|
| 416 |
+
ms = [mask(c, px, py, sharp, -grow if op == "minus" and i else grow) for i, c in enumerate(arg)]
|
| 417 |
if op == "or":
|
| 418 |
return sig(12 * (sum(ms) - 0.5))
|
| 419 |
+
if op == "both":
|
| 420 |
+
return sig(12 * (sum(ms) - (len(ms) - 0.5)))
|
| 421 |
return sig(12 * (ms[0] - ms[1] - 0.5)) # minus: AND-NOT neuron
|
| 422 |
|
| 423 |
|
| 424 |
+
def part_color(spec, main):
|
| 425 |
+
if isinstance(spec, str):
|
| 426 |
+
return {"main": main, "light": main * 0.45 + 0.55, "dark": main * 0.65}[spec]
|
| 427 |
+
return np.array(spec)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
# ------------------------------------------------------------------ backgrounds
|
| 431 |
U, V = np.meshgrid(np.linspace(-1, 1, RES), np.linspace(-1, 1, RES))
|
| 432 |
HORIZON = 0.3
|
|
|
|
| 436 |
return np.array(a)[None, None] * (1 - t[..., None]) + np.array(b)[None, None] * t[..., None]
|
| 437 |
|
| 438 |
|
| 439 |
+
def hash2(i, j):
|
| 440 |
+
return np.abs(np.modf(np.sin(i * 12.9898 + j * 78.233) * 43758.5453)[0])
|
| 441 |
+
|
| 442 |
+
|
| 443 |
def stars(threshold):
|
| 444 |
+
h = hash2(np.floor((U + 1) * RES / 3), np.floor((V + 1) * RES / 3))
|
|
|
|
| 445 |
return ((h > threshold) * (h - threshold) / (1 - threshold) * 0.6 + (h > threshold) * 0.4)[..., None]
|
| 446 |
|
| 447 |
|
| 448 |
+
def rain():
|
| 449 |
+
col = np.floor((U + 0.15 * V + 2) * RES / 9)
|
| 450 |
+
phase = np.modf((V + 1) * 1.5 + hash2(col, 1.0) * 5)[0]
|
| 451 |
+
frac = np.modf((U + 0.15 * V + 2) * RES / 9)[0]
|
| 452 |
+
return ((phase < 0.06) & (frac < 0.2)).astype(float)[..., None] * 0.7
|
| 453 |
+
|
| 454 |
+
|
| 455 |
def background(sky, ground):
|
| 456 |
t = (V + 1) / 2
|
| 457 |
if sky is None:
|
|
|
|
| 464 |
elif sky == "sunset":
|
| 465 |
img = lerp((0.22, 0.18, 0.45), (0.95, 0.45, 0.35), np.clip(t / 0.6, 0, 1))
|
| 466 |
img = img + lerp((0, 0, 0), (0.05, 0.35, 0.1), np.clip((t - 0.6) / 0.4, 0, 1))
|
| 467 |
+
elif sky == "rain":
|
| 468 |
+
img = lerp((0.5, 0.55, 0.62), (0.74, 0.77, 0.82), t)
|
| 469 |
else:
|
| 470 |
img = np.clip(lerp((0.0, 0.0, 0.02), (0.04, 0.02, 0.08), t) + stars(0.97), 0, 1)
|
| 471 |
+
if ground is not None:
|
| 472 |
+
wiggle = {"sea": 0 * U, "grass": 0.025 * np.sin(4 * U), "desert": 0.06 * np.sin(3 * U + 1),
|
| 473 |
+
"snow": 0.035 * np.sin(2.5 * U + 2)}[ground]
|
| 474 |
+
g = np.clip((V - HORIZON) / (1 - HORIZON), 0, 1)
|
| 475 |
+
if ground == "sea":
|
| 476 |
+
col = lerp((0.25, 0.55, 0.75), (0.04, 0.2, 0.42), g) + 0.05 * np.sin(70 * V + 4 * np.sin(7 * U))[..., None]
|
| 477 |
+
elif ground == "grass":
|
| 478 |
+
col = lerp((0.5, 0.74, 0.32), (0.2, 0.45, 0.15), g) + 0.025 * np.sin(150 * U + 20 * V)[..., None]
|
| 479 |
+
elif ground == "desert":
|
| 480 |
+
col = lerp((0.96, 0.82, 0.52), (0.84, 0.6, 0.34), g) + 0.04 * np.sin(9 * U - 14 * V)[..., None]
|
| 481 |
+
else:
|
| 482 |
+
col = lerp((0.98, 0.99, 1.0), (0.82, 0.88, 0.97), g)
|
| 483 |
+
m = sig(RES * 0.6 * (V - HORIZON - wiggle))[..., None]
|
| 484 |
+
img = img * (1 - m) + col * m
|
| 485 |
+
return np.clip(img, 0, 1)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# ------------------------------------------------------------------ story context (plain code, not weights)
|
| 489 |
+
class Story:
|
| 490 |
+
"""Carries scene, character colours and the last cast across sentences, and resolves pronouns."""
|
| 491 |
+
|
| 492 |
+
def __init__(self):
|
| 493 |
+
self.colors, self.cast, self.sky, self.ground = {}, [], "day", "grass"
|
| 494 |
+
|
| 495 |
+
def resolve(self, objs):
|
| 496 |
+
out = []
|
| 497 |
+
for o in objs:
|
| 498 |
+
if o["kind"] == "ref":
|
| 499 |
+
for c in (self.cast[-1:] if o["ref"] == 1 else self.cast):
|
| 500 |
+
out.append(dict(c, expr=o["expr"], has_expr=o["has_expr"], x=o["x"], y=o["y"], size=o["size"] if o["has_size"] else c["size"]))
|
| 501 |
+
continue
|
| 502 |
+
if o["has_color"]:
|
| 503 |
+
self.colors[o["kind"]] = o["color"]
|
| 504 |
+
elif o["kind"] in self.colors:
|
| 505 |
+
o["color"] = self.colors[o["kind"]]
|
| 506 |
+
out.append(o)
|
| 507 |
+
first, dedup = {}, []
|
| 508 |
+
for o in out: # "it ... she was sad": one cat, keep the stated expression
|
| 509 |
+
if o["kind"] in CHARS and o["kind"] in first:
|
| 510 |
+
if o.get("has_expr"):
|
| 511 |
+
first[o["kind"]]["expr"] = o["expr"]
|
| 512 |
+
continue
|
| 513 |
+
first[o["kind"]] = o
|
| 514 |
+
dedup.append(o)
|
| 515 |
+
cast = [dict(kind=o["kind"], ref=0, color=o["color"], size=o["size"], has_color=True, has_size=False)
|
| 516 |
+
for o in dedup if o["kind"] in CHARS]
|
| 517 |
+
if cast:
|
| 518 |
+
self.cast = cast
|
| 519 |
+
return dedup
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def split_story(text):
|
| 523 |
+
return [s.strip() for s in re.split(r"(?<=[.!?])\s+|\n+", text.strip()) if s.strip()]
|
| 524 |
|
| 525 |
|
| 526 |
# ------------------------------------------------------------------ render
|
| 527 |
+
def render(prompt, story=None):
|
| 528 |
slots, scene, unknown = encode(prompt)
|
| 529 |
+
objs = [decode(s) for s in slots]
|
| 530 |
+
objs = story.resolve(objs) if story else [o for o in objs if o["kind"] != "ref"]
|
| 531 |
+
objs = layout(objs)
|
| 532 |
sky = SKIES[int(np.argmax(scene[SKY:GND]))] if scene[SKY:GND].any() else None
|
| 533 |
ground = GROUNDS[int(np.argmax(scene[GND:]))] if scene[GND:].any() else None
|
| 534 |
+
if story:
|
| 535 |
+
sky, ground = sky or story.sky, ground or story.ground
|
| 536 |
+
story.sky, story.ground = sky, ground
|
| 537 |
if ground and sky is None:
|
| 538 |
sky = "day"
|
| 539 |
img = background(sky, ground)
|
| 540 |
for o in objs:
|
| 541 |
r = o["size"]
|
| 542 |
+
ext = EXTENT.get(o["kind"], 1.8) * r
|
| 543 |
+
i0, i1 = max(0, int((o["y"] - ext + 1) / 2 * RES) - 3), min(RES, int((o["y"] + ext + 1) / 2 * RES) + 4)
|
| 544 |
+
j0, j1 = max(0, int((o["x"] - ext + 1) / 2 * RES) - 3), min(RES, int((o["x"] + ext + 1) / 2 * RES) + 4)
|
| 545 |
+
if i0 >= i1 or j0 >= j1:
|
| 546 |
+
continue
|
| 547 |
+
sub = img[i0:i1, j0:j1]
|
| 548 |
+
sharp = 4.5 * r * RES / 2 # ~1 px anti-aliased edge in local units
|
| 549 |
+
ink = 2.4 * 2 / (RES * r) # outline width: ~2.4 px in local units
|
| 550 |
+
px, py = (U[i0:i1, j0:j1] - o["x"]) / r, (V[i0:i1, j0:j1] - o["y"]) / r
|
| 551 |
+
parts = PARTS[o["kind"]] + FACES.get((o["kind"], o["expr"]), [])
|
| 552 |
if o["kind"] in CASTS_SHADOW:
|
| 553 |
+
sh = sum(mask(g, px - 0.12, py - 0.12, sharp) * a for g, _, a, _, _ in parts)
|
| 554 |
+
sub = sub * (1 - np.clip(sh, 0, 1)[..., None] * 0.22)
|
| 555 |
+
for geom, col, alpha, shaded, outline in parts:
|
| 556 |
+
if outline:
|
| 557 |
+
m = mask(geom, px, py, sharp, ink)[..., None]
|
| 558 |
+
sub = sub * (1 - m) + np.array(INK) * m
|
| 559 |
+
c = part_color(col, o["color"])[None, None]
|
| 560 |
+
if shaded:
|
| 561 |
+
c = c * (1 - 0.16 * (px + py))[..., None]
|
| 562 |
m = (mask(geom, px, py, sharp) * alpha)[..., None]
|
| 563 |
+
sub = sub * (1 - m) + np.clip(c, 0, 1) * m
|
| 564 |
+
img[i0:i1, j0:j1] = sub
|
| 565 |
+
if sky == "rain":
|
| 566 |
+
drops = rain()
|
| 567 |
+
img = img * (1 - drops) + np.array([0.85, 0.9, 1.0]) * drops
|
| 568 |
return Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)), objs, sky, ground, unknown
|
| 569 |
|
| 570 |
|
| 571 |
+
def render_story(text):
|
| 572 |
+
story = Story()
|
| 573 |
+
return [(s, render(s, story)) for s in split_story(text)]
|
| 574 |
+
|
| 575 |
+
|
| 576 |
def count_params():
|
| 577 |
def geo(g):
|
| 578 |
return g[1].size if g[0] == "and" else sum(geo(c) for c in g[1])
|
| 579 |
+
parts = [p for ps in PARTS.values() for p in ps] + [p for ps in FACES.values() for p in ps]
|
| 580 |
+
return (len(VOCAB) * DIM + DEF_COLOR.size + DEF_SIZE.size + len(DEF_Y)
|
| 581 |
+
+ sum(geo(g) + 3 for g, *_ in parts) + 3 * len(PROPS) + 2 * len(HAND))
|
| 582 |
|
| 583 |
|
| 584 |
def slug(p):
|
| 585 |
return re.sub(r"[^a-z0-9]+", "_", p.lower()).strip("_")[:60]
|
| 586 |
|
| 587 |
|
| 588 |
+
def caption_font(size=20):
|
| 589 |
+
for f in ["C:/Windows/Fonts/comic.ttf", "C:/Windows/Fonts/segoeui.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]:
|
| 590 |
+
try:
|
| 591 |
+
return ImageFont.truetype(f, size)
|
| 592 |
+
except OSError:
|
| 593 |
+
pass
|
| 594 |
+
return ImageFont.load_default(size=size)
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def book_sheet(pages, cols=3, cap=64):
|
| 598 |
+
font = caption_font()
|
| 599 |
+
sheet = Image.new("RGB", (cols * (RES + 16) + 16, ((len(pages) + cols - 1) // cols) * (RES + cap + 16) + 16), "white")
|
| 600 |
+
d = ImageDraw.Draw(sheet)
|
| 601 |
+
for i, (text, im) in enumerate(pages):
|
| 602 |
+
x, y = 16 + (i % cols) * (RES + 16), 16 + (i // cols) * (RES + cap + 16)
|
| 603 |
+
sheet.paste(im, (x, y))
|
| 604 |
+
lines, line = [], ""
|
| 605 |
+
for w in text.split():
|
| 606 |
+
if d.textlength(line + " " + w, font=font) > RES - 8 and line:
|
| 607 |
+
lines.append(line)
|
| 608 |
+
line = w
|
| 609 |
+
else:
|
| 610 |
+
line = (line + " " + w).strip()
|
| 611 |
+
d.multiline_text((x + 4, y + RES + 8), "\n".join(lines + [line]), fill="black", font=font, spacing=4)
|
| 612 |
+
return sheet
|
| 613 |
+
|
| 614 |
+
|
| 615 |
DEMO = [
|
| 616 |
"a yellow sun over the sea",
|
| 617 |
"a red house and a green tree on grass",
|
|
|
|
| 618 |
"a white moon and a gold star in a night sky",
|
|
|
|
| 619 |
"an orange sun and a cloud over the desert at sunset",
|
| 620 |
+
"a happy orange cat with a red balloon on the grass",
|
| 621 |
+
"a sleepy bear and a surprised bunny at night",
|
| 622 |
+
"a kid with a blue umbrella in the rain",
|
| 623 |
+
"a sad white bunny with a pink flower in the snow",
|
| 624 |
]
|
| 625 |
|
| 626 |
+
STORY = ("The orange cat was happy in the meadow. She found a red balloon. Then it started to rain and she was sad. "
|
| 627 |
+
"A kid with a yellow umbrella came by. The cat and the kid were happy together. "
|
| 628 |
+
"At night the cat was sleepy under the moon.")
|
| 629 |
+
|
| 630 |
if __name__ == "__main__":
|
| 631 |
OUT.mkdir(exist_ok=True)
|
|
|
|
| 632 |
print(f"hand-written parameters: {count_params():,}")
|
| 633 |
+
if sys.argv[1:2] == ["--story"]:
|
| 634 |
+
pages = render_story(" ".join(sys.argv[2:]) or STORY)
|
| 635 |
+
for s, (_, objs, sky, ground, unknown) in pages:
|
| 636 |
+
print(f"\n{s!r}\n sky={sky} ground={ground} " + ", ".join(f"{o['expr']} {o['kind']}" if o["kind"] in CHARS
|
| 637 |
+
else o["kind"] for o in objs))
|
| 638 |
+
book_sheet([(s, r[0]) for s, r in pages]).save(OUT / "story_sheet.png")
|
| 639 |
+
print(f"\nbook -> {OUT / 'story_sheet.png'}")
|
| 640 |
+
sys.exit()
|
| 641 |
+
prompts = DEMO if sys.argv[1:] == ["--demo"] else [" ".join(sys.argv[1:]) or DEMO[0]]
|
| 642 |
tiles = []
|
| 643 |
for p in prompts:
|
| 644 |
im, objs, sky, ground, unknown = render(p)
|
|
|
|
| 646 |
tiles.append((p, im))
|
| 647 |
print(f"\n{p!r}\n sky={sky} ground={ground} ignored={unknown}")
|
| 648 |
for o in objs:
|
| 649 |
+
print(f" {o['kind']:<8} {o['expr'] if o['kind'] in CHARS else '':<9} rgb={np.round(o['color'], 2)} "
|
| 650 |
+
f"size={o['size']:.2f} at ({o['x']:+.2f},{o['y']:+.2f})")
|
| 651 |
if len(tiles) > 1:
|
| 652 |
+
book_sheet(tiles, cols=4).save(OUT / "demo_sheet.png")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
print(f"\nsheet -> {OUT / 'demo_sheet.png'}")
|
story_sheet.png
ADDED
|
Git LFS Details
|
weights.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d1688a1972b6bdcff37f6df1a7a8059d651ddbbbfdbf47f6e0e02859422d553
|
| 3 |
+
size 178576
|