"""World v2: free-form captions over a much richer procedural world. Text is tokenized as raw UTF-8 bytes, so any string is accepted. What the model *understands* is whatever this world teaches it: many colours and shapes, single- object events (moving, falling, jumping, orbiting, fading, changing colour ...) and two-object interactions (merging with colour mixing, splitting, colliding, chasing, orbiting, swapping places), each described with many paraphrases. Every clip comes with per-frame, per-object ground truth in `meta` for scoring. """ from __future__ import annotations import math import re import zlib from dataclasses import dataclass import numpy as np import torch # ---------------------------------------------------------------- tokenizer PAD, NULL, BOS = 256, 257, 258 BYTE_VOCAB = 259 MAX_BYTES = 256 class ByteTokenizer: """UTF-8 bytes -> ids. No vocabulary: every string is representable.""" vocab_size = BYTE_VOCAB def __init__(self, max_len: int = MAX_BYTES): self.max_len = max_len def encode(self, caption: str) -> list[int]: return [BOS] + list(caption.encode("utf-8"))[: self.max_len - 1] def __call__(self, captions: list[str], pad_to_longest: bool = True) -> torch.Tensor: """[B, L] ids; L = longest caption in the batch (dynamic padding) or max_len.""" encoded = [self.encode(c) for c in captions] L = max(map(len, encoded)) if pad_to_longest else self.max_len out = torch.full((len(captions), L), PAD, dtype=torch.long) for i, ids in enumerate(encoded): out[i, : len(ids)] = torch.tensor(ids) return out def null(self, n: int, length: int = 1) -> torch.Tensor: out = torch.full((n, length), PAD, dtype=torch.long) out[:, 0] = NULL return out SEP = 259 HASH_OFFSET, HASH_BUCKETS = 260, 16384 HYBRID_VOCAB = HASH_OFFSET + HASH_BUCKETS MAX_HYBRID = 320 _WORD = re.compile(r"[^\W_]+", re.UNICODE) def word_bucket(word: str) -> int: """Stable hash of a case-folded word into one of HASH_BUCKETS ids (no vocabulary; any word maps somewhere).""" return HASH_OFFSET + zlib.crc32(word.casefold().encode("utf-8")) % HASH_BUCKETS class HybridTokenizer(ByteTokenizer): """[BOS] utf-8 bytes [SEP] hashed words. Bytes keep spelling; hashed words give the encoder word identity from the first step. Still no vocabulary: every string encodes, nothing is UNK.""" vocab_size = HYBRID_VOCAB def __init__(self, max_len: int = MAX_HYBRID, max_bytes: int = MAX_BYTES): super().__init__(max_len) self.max_bytes = max_bytes def encode(self, caption: str) -> list[int]: words = [word_bucket(w) for w in _WORD.findall(caption)] byte_ids = list(caption.encode("utf-8"))[: self.max_bytes - 1] room = self.max_len - 2 - len(byte_ids) return [BOS] + byte_ids + [SEP] + words[: max(0, room)] # ---------------------------------------------------------------- the world's nouns COLORS = { "red": (225, 35, 35), "orange": (250, 140, 20), "yellow": (245, 220, 40), "green": (40, 190, 60), "cyan": (40, 215, 230), "blue": (45, 85, 240), "purple": (145, 55, 215), "pink": (250, 125, 190), "brown": (135, 80, 35), "white": (245, 245, 245), "gray": (140, 140, 140), "black": (12, 12, 12), } COLOR_WORDS = { "red": ["red", "red", "red", "crimson", "scarlet"], "orange": ["orange"], "yellow": ["yellow", "yellow", "golden"], "green": ["green", "green", "lime"], "cyan": ["cyan", "cyan", "turquoise", "light blue"], "blue": ["blue", "blue", "dark blue"], "purple": ["purple", "purple", "violet"], "pink": ["pink"], "brown": ["brown"], "white": ["white"], "gray": ["gray", "grey", "silver"], "black": ["black"], } # Two colours that merge make this one (order-free). Pairs not listed blend to the nearest named colour. MIX = { frozenset(["red", "blue"]): "purple", frozenset(["red", "yellow"]): "orange", frozenset(["blue", "yellow"]): "green", frozenset(["red", "white"]): "pink", frozenset(["black", "white"]): "gray", frozenset(["blue", "green"]): "cyan", frozenset(["red", "green"]): "brown", frozenset(["blue", "white"]): "cyan", frozenset(["purple", "white"]): "pink", frozenset(["orange", "blue"]): "brown", } SHAPES = ["circle", "square", "triangle", "star", "ring", "cross", "heart", "diamond", "hexagon"] SHAPE_WORDS = { "circle": ["circle", "circle", "circle", "ball", "dot", "disc"], "square": ["square", "square", "box", "block"], "triangle": ["triangle"], "star": ["star"], "ring": ["ring", "ring", "donut"], "cross": ["cross", "plus sign"], "heart": ["heart"], "diamond": ["diamond"], "hexagon": ["hexagon"], } ROTATION_INVARIANT = {"circle", "ring"} SIZES = {"tiny": 0.085, "small": 0.115, "medium": 0.155, "big": 0.205, "huge": 0.26} SIZE_WORDS = {"tiny": ["tiny"], "small": ["small", "little"], "medium": [""], "big": ["big", "large"], "huge": ["huge", "giant"]} BACKGROUNDS = { # name -> (top rgb, bottom rgb) "black": ((0, 0, 0), (0, 0, 0)), "white": ((250, 250, 250), (250, 250, 250)), "gray": ((120, 120, 120), (120, 120, 120)), "navy": ((10, 15, 70), (10, 15, 70)), "sky": ((110, 170, 240), (200, 230, 255)), "sunset": ((90, 40, 130), (250, 150, 60)), "grass": ((120, 190, 250), (40, 140, 40)), } BG_PHRASES = { "black": ["on a black background", "against a black background", "on black", "in the dark", "in darkness"], "white": ["on a white background", "against a white background", "on white", "on a blank white page"], "gray": ["on a gray background", "on a grey background", "against a gray wall"], "navy": ["at night", "in the night sky", "on a dark blue background", "on a navy background"], "sky": ["in the sky", "in a blue sky", "against the sky", "in the daytime sky"], "sunset": ["at sunset", "in a sunset sky", "against a sunset", "at dusk"], "grass": ["over a field of grass", "above green grass", "over a meadow", "in a grassy field"], } DIRS = { "left": (-1, 0), "right": (1, 0), "up": (0, -1), "down": (0, 1), "up-left": (-1, -1), "up-right": (1, -1), "down-left": (-1, 1), "down-right": (1, 1), } DIR_PHRASES = { "left": ["left", "to the left", "leftward", "toward the left", "from right to left"], "right": ["right", "to the right", "rightward", "toward the right", "from left to right"], "up": ["up", "upward", "upwards", "toward the top", "from bottom to top"], "down": ["down", "downward", "downwards", "toward the bottom", "from top to bottom"], "up-left": ["up and to the left", "diagonally up left", "toward the top left", "up-left"], "up-right": ["up and to the right", "diagonally up right", "toward the top right", "up-right"], "down-left": ["down and to the left", "diagonally down left", "toward the bottom left", "down-left"], "down-right": ["down and to the right", "diagonally down right", "toward the bottom right", "down-right"], } MOVE_VERBS = ["moves", "moves", "slides", "drifts", "travels", "goes", "glides", "floats", "heads"] MOVE_VERBS_ING = ["moving", "sliding", "drifting", "traveling", "going", "gliding", "floating", "heading"] SINGLE_EVENTS = {"move": 4, "fall": 1, "rise": 1, "jump": 1, "bounce": 1, "zigzag": 1, "circle": 1, "grow": 1, "shrink": 1, "spin": 1, "still": 1, "fade_in": 1, "fade_out": 1, "blink": 1, "recolor": 1.5, "teleport": 0.7} PAIR_EVENTS = {"merge": 9, "split": 1.2, "collide": 1, "chase": 1, "orbit": 1, "swap": 1, "independent": 1.8} # (event kind, shape) combinations never rendered in the training split: a generalisation test. HELDOUT = {("merge", "star"), ("jump", "heart"), ("orbit", "hexagon"), ("fall", "diamond"), ("spin", "cross"), ("recolor", "triangle")} @dataclass class WorldConfig: frames: int = 16 size: int = 32 supersample: int = 2 p_pair: float = 0.5 # with PAIR_EVENTS weights -> ~30% of clips are merges p_featured: float = 0.04 # the user's own example: a red and a blue circle merge into a purple circle merge_weight: float = 9.0 # relative weight of "merge" among PAIR_EVENTS (phase 3 lowers it: merges took over) p_mention_bg: float = 0.5 p_default_black: float = 0.7 # background when the caption does not mention one def _choice(rng, seq): return seq[rng.integers(len(seq))] def _weighted(rng, weights: dict): keys = list(weights) p = np.array([weights[k] for k in keys], float) return keys[rng.choice(len(keys), p=p / p.sum())] def _contrast_ok(rgb, bg: str) -> bool: top, bottom = (np.array(c, float) for c in BACKGROUNDS[bg]) c = np.array(rgb, float) return min(np.linalg.norm(c - top), np.linalg.norm(c - bottom)) > 110 def nearest_color(rgb) -> str: c = np.array(rgb, float) return min(COLORS, key=lambda k: np.linalg.norm(c - np.array(COLORS[k], float))) def mix_colors(a: str, b: str) -> str: if a == b: return a return MIX.get(frozenset([a, b])) or nearest_color((np.array(COLORS[a]) + np.array(COLORS[b])) / 2) # ---------------------------------------------------------------- phrasing def noun_phrase(rng, obj: dict, article: str | None = None) -> str: size = _choice(rng, SIZE_WORDS[obj["size"]]) color = _choice(rng, COLOR_WORDS[obj["color"]]) shape = _choice(rng, SHAPE_WORDS[obj["shape"]]) if article is None: article = _choice(rng, ["a", "a", "a", "the", "one", ""]) if article.endswith("bigger"): size = "" if article == "a" and (size or color)[:1] in "aeiou": article = "an" return " ".join(w for w in (article, size, color, shape) if w) def _move_phrase(rng, d: str) -> str: dp = _choice(rng, DIR_PHRASES[d]) if d == "up" and rng.random() < 0.25: return _choice(rng, ["rises", "floats up", "goes up"]) return f"{_choice(rng, MOVE_VERBS)} {dp}" def describe_single(rng, obj: dict, ev: dict) -> str: np_ = noun_phrase(rng, obj) k = ev["kind"] if k == "move": vp = _move_phrase(rng, ev["dir"]) if rng.random() < 0.2: vp = f"{_choice(rng, MOVE_VERBS_ING)} {_choice(rng, DIR_PHRASES[ev['dir']])}" return f"{np_} {vp}" if rng.random() < 0.5 else f"{np_}, {vp}" return f"{np_} {vp}" if k == "fall": return f"{np_} {_choice(rng, ['falls', 'falls down', 'drops', 'drops down', 'falls from the top'])}" if k == "rise": return f"{np_} {_choice(rng, ['rises from the bottom', 'rises up', 'floats up from the bottom', 'launches upward'])}" if k == "jump": d = ev.get("dir") tail = f" {_choice(rng, ['to the left', 'left'])}" if d == "left" else f" {_choice(rng, ['to the right', 'right'])}" if d == "right" else "" return f"{np_} {_choice(rng, ['jumps', 'hops', 'leaps'])}{tail}" if k == "bounce": return f"{np_} {_choice(rng, ['bounces around', 'bounces', 'bounces off the walls', 'ricochets around'])}" if k == "zigzag": return f"{np_} {_choice(rng, ['zigzags', 'moves in a zigzag'])} {_choice(rng, DIR_PHRASES[ev['dir']][:3])}" if k == "circle": return f"{np_} {_choice(rng, ['moves in a circle', 'goes around in a circle', 'circles around', 'travels in a loop'])}" if k == "grow": return f"{np_} {_choice(rng, ['grows', 'gets bigger', 'expands', 'grows larger', 'inflates'])}" if k == "shrink": return f"{np_} {_choice(rng, ['shrinks', 'gets smaller', 'shrinks away', 'deflates'])}" if k == "spin": return f"{np_} {_choice(rng, ['spins', 'rotates', 'spins in place', 'twirls'])}" if k == "still": return f"{np_} {_choice(rng, ['stays still', 'sits still', 'does not move', 'stays in place', 'is resting'])}" if k == "fade_in": return f"{np_} {_choice(rng, ['fades in', 'appears', 'slowly appears', 'materializes'])}" if k == "fade_out": return f"{np_} {_choice(rng, ['fades out', 'disappears', 'fades away', 'vanishes'])}" if k == "blink": return f"{np_} {_choice(rng, ['blinks', 'flashes', 'flickers', 'blinks on and off'])}" if k == "recolor": to = _choice(rng, COLOR_WORDS[ev["to"]]) return f"{np_} {_choice(rng, ['turns', 'changes to', 'becomes', 'changes color to', 'turns into a'])} {to}".replace( "turns into a " + to, f"turns into a {to} {_choice(rng, SHAPE_WORDS[obj['shape']])}") if k == "teleport": return f"{np_} {_choice(rng, ['teleports', 'jumps to another spot', 'teleports across the screen'])}" raise ValueError(k) def describe_pair(rng, a: dict, b: dict, ev: dict) -> str: k = ev["kind"] A, B = noun_phrase(rng, a), noun_phrase(rng, b) if k == "merge": res = ev["result"] verb = _choice(rng, ["merge into", "merge into", "combine into", "fuse into", "blend into", "join to form", "come together to form", "melt together into"]) tail = f" {verb} {noun_phrase(rng, res, article=_choice(rng, ['a', 'one', 'a single', 'a bigger']))}" if rng.random() < 0.25: tail = f" {_choice(rng, ['merge', 'merge together', 'fuse together', 'combine'])}" return f"{A} and {B}{tail}" if k == "split": src = ev["source"] S = noun_phrase(rng, src) if rng.random() < 0.25: return f"{S} {_choice(rng, ['splits in two', 'splits into two', 'divides in half', 'breaks in two'])}" return f"{S} {_choice(rng, ['splits into', 'breaks into', 'divides into', 'separates into'])} {noun_phrase(rng, a, 'a')} and {noun_phrase(rng, b, 'a')}" if k == "collide": return f"{A} and {B} {_choice(rng, ['collide and bounce apart', 'crash into each other', 'bump into each other', 'collide', 'smash together and bounce off'])}" if k == "chase": if rng.random() < 0.3: return f"{B} is chased by {A}" return f"{A} {_choice(rng, ['chases', 'follows', 'runs after', 'is chasing'])} {B}" if k == "orbit": return f"{A} {_choice(rng, ['orbits', 'circles around', 'revolves around', 'goes around'])} {B}" if k == "swap": return f"{A} and {B} {_choice(rng, ['swap places', 'trade places', 'switch places', 'swap positions'])}" if k == "independent": return f"{describe_single(rng, a, ev['a'])} {_choice(rng, ['and', 'and', 'while', ', and'])} {describe_single(rng, b, ev['b'])}".replace(" , and", ", and") raise ValueError(k) def _style(rng, caption: str, bg_phrase: str | None) -> str: if bg_phrase: caption = f"{bg_phrase}, {caption}" if rng.random() < 0.2 else f"{caption} {bg_phrase}" if rng.random() < 0.08: caption = _choice(rng, ["a video of ", "watch as ", "animation: ", "clip: "]) + caption if rng.random() < 0.3: caption = caption[0].upper() + caption[1:] if rng.random() < 0.25: caption += "." return caption.strip() # ---------------------------------------------------------------- dynamics (normalised coords, t in [0,1]) def _uni(rng, lo, hi, size=None): """rng.uniform that tolerates an empty range (big objects in a small frame): collapses to the midpoint.""" if hi < lo: lo = hi = (lo + hi) / 2 return rng.uniform(lo, hi, size=size) def _state(x, y, r, ang, rgb, alpha=1.0): return [float(x), float(y), float(r), float(ang), tuple(float(c) for c in rgb), float(alpha)] def _single_states(rng, obj: dict, ev: dict, T: int, center=None) -> list: r = SIZES[obj["size"]] * _uni(rng, 0.92, 1.08) ang0 = _uni(rng, 0, 2 * math.pi) rgb = np.array(COLORS[obj["color"]], float) ts = np.linspace(0, 1, T) k = ev["kind"] m = r + 0.03 def anywhere(): return _uni(rng, m, 1 - m, size=2) if k == "move": dx, dy = DIRS[ev["dir"]] dist = _uni(rng, 0.4, 0.62) / (math.sqrt(2) if dx and dy else 1) dist = min(dist, (1 - 2 * m) * 0.98) def start(d): if d > 0: return _uni(rng, m, 1 - m - dist) if d < 0: return _uni(rng, m + dist, 1 - m) return _uni(rng, m, 1 - m) x0, y0 = start(dx), start(dy) return [_state(x0 + dx * dist * t, y0 + dy * dist * t, r, ang0, rgb) for t in ts] if k in ("fall", "rise"): x0 = _uni(rng, m, 1 - m) top, bot = m, 1 - m prog = ts ** 2 if k == "fall" else 1 - (1 - ts) ** 2 return [_state(x0, top + (bot - top) * p if k == "fall" else bot - (bot - top) * p, r, ang0, rgb) for p in prog] if k == "jump": d = {"left": -1, "right": 1}.get(ev.get("dir"), 0) span = min(_uni(rng, 0.3, 0.5), max(0.0, 1 - 2 * m - 0.02)) * d x0 = _uni(rng, m + max(0, -span), 1 - m - max(0, span)) ground = 1 - m height = _uni(rng, 0.35, min(0.6, 1 - 2 * m)) return [_state(x0 + span * t, ground - height * 4 * t * (1 - t), r, ang0, rgb) for t in ts] if k == "bounce": lo, hi = m, 1 - m x, y = anywhere() th = _uni(rng, 0, 2 * math.pi) sp = _uni(rng, 1.6, 2.4) vx, vy = sp * math.cos(th), sp * math.sin(th) out, dt = [], 1 / (T - 1) for _ in range(T): out.append(_state(x, y, r, ang0, rgb)) x, y = x + vx * dt, y + vy * dt if x < lo or x > hi: vx, x = -vx, float(np.clip(2 * np.clip(x, lo, hi) - x, lo, hi)) if y < lo or y > hi: vy, y = -vy, float(np.clip(2 * np.clip(y, lo, hi) - y, lo, hi)) return out if k == "zigzag": dx, dy = DIRS[ev["dir"]] dist = min(_uni(rng, 0.45, 0.6), 1 - 2 * m - 0.1) amp = _uni(rng, 0.08, 0.14) along0 = _uni(rng, m, 1 - m - dist) if (dx + dy) > 0 else _uni(rng, m + dist, 1 - m) across0 = _uni(rng, m + amp, 1 - m - amp) out = [] for t in ts: along = along0 + (dx + dy) * dist * t zig = amp * (2 * abs((t * 3) % 1 - 0.5) * 2 - 1) x, y = (along, across0 + zig) if dx else (across0 + zig, along) out.append(_state(x, y, r, ang0, rgb)) return out if k == "circle": R = _uni(rng, 0.18, max(0.19, 0.5 - m - 0.02)) cx, cy = center if center is not None else (_uni(rng, m + R, 1 - m - R), _uni(rng, m + R, 1 - m - R)) ph = _uni(rng, 0, 2 * math.pi) sgn = _choice(rng, [-1, 1]) turns = _uni(rng, 0.8, 1.0) return [_state(cx + R * math.cos(ph + sgn * 2 * math.pi * turns * t), cy + R * math.sin(ph + sgn * 2 * math.pi * turns * t), r, ang0, rgb) for t in ts] if k in ("grow", "shrink"): mm = r * 1.7 + 0.03 x0, y0 = _uni(rng, mm, 1 - mm, size=2) f = (lambda t: 0.45 + 1.25 * t) if k == "grow" else (lambda t: 1.7 - 1.25 * t) return [_state(x0, y0, r * f(t), ang0, rgb) for t in ts] x0, y0 = anywhere() if k == "spin": sp = _choice(rng, [-1, 1]) * _uni(rng, 0.7, 1.1) * math.pi return [_state(x0, y0, r, ang0 + sp * t, rgb) for t in ts] if k == "still": return [_state(x0, y0, r, ang0, rgb) for t in ts] if k == "fade_in": return [_state(x0, y0, r, ang0, rgb, min(1, max(0, (t - 0.1) / 0.6))) for t in ts] if k == "fade_out": return [_state(x0, y0, r, ang0, rgb, min(1, max(0, (0.9 - t) / 0.6))) for t in ts] if k == "blink": period = _choice(rng, [4, 4, 6]) return [_state(x0, y0, r, ang0, rgb, 1.0 if (i // (period // 2)) % 2 == 0 else 0.0) for i in range(T)] if k == "recolor": rgb2 = np.array(COLORS[ev["to"]], float) out = [] for t in ts: w = min(1, max(0, (t - 0.3) / 0.4)) out.append(_state(x0, y0, r, ang0, (1 - w) * rgb + w * rgb2)) return out if k == "teleport": x1, y1 = max((anywhere() for _ in range(8)), key=lambda p: math.hypot(p[0] - x0, p[1] - y0)) cut = rng.integers(T // 3, 2 * T // 3) return [_state(x0, y0, r, ang0, rgb) if i < cut else _state(x1, y1, r, ang0, rgb) for i in range(T)] raise ValueError(k) def _pair_states(rng, a: dict, b: dict, ev: dict, T: int): ra = SIZES[a["size"]] * _uni(rng, 0.92, 1.08) rb = SIZES[b["size"]] * _uni(rng, 0.92, 1.08) ca, cb = np.array(COLORS[a["color"]], float), np.array(COLORS[b["color"]], float) angA, angB = _uni(rng, 0, 2 * math.pi, size=2) ts = np.linspace(0, 1, T) k = ev["kind"] if k in ("merge", "split"): res = ev["result"] if k == "merge" else ev["source"] rr = SIZES[res["size"]] cr = np.array(COLORS[res["color"]], float) mid = _uni(rng, 0.32, 0.68, size=2) th = _uni(rng, 0, math.pi) sep = _uni(rng, 0.62, 0.8) u = np.array([math.cos(th), math.sin(th)]) pa = np.clip(mid - u * sep / 2, ra + 0.02, 1 - ra - 0.02) pb = np.clip(mid + u * sep / 2, rb + 0.02, 1 - rb - 0.02) SA, SB, SR = [], [], [] for t in ts: tm = t if k == "merge" else 1 - t p = min(1, tm / 0.6) # approach f = min(1, max(0, (tm - 0.5) / 0.25)) # fuse xa = pa + (mid - pa) * p xb = pb + (mid - pb) * p SA.append(_state(*xa, ra * (1 - f) + rr * f, angA, ca * (1 - f) + cr * f, 1 - f)) SB.append(_state(*xb, rb * (1 - f) + rr * f, angB, cb * (1 - f) + cr * f, 1 - f)) SR.append(_state(*mid, rr * (0.6 + 0.4 * f), angA, cr, f)) return [SA, SB, SR] if k == "collide": y = _uni(rng, 0.3, 0.7) vert = rng.random() < 0.3 hit = _uni(rng, 0.4, 0.6) xa0, xb0 = _uni(rng, 0.05, 0.2) + ra, 1 - _uni(rng, 0.05, 0.2) - rb contact = (xa0 * rb + xb0 * ra) / (ra + rb) # where they touch if both move at equal speed SA, SB = [], [] for t in ts: if t <= hit: s = t / hit xa = xa0 + (contact - ra - xa0) * s xb = xb0 + (contact + rb - xb0) * s else: s = (t - hit) / (1 - hit) xa = contact - ra - (contact - ra - xa0) * s * 0.8 xb = contact + rb + (xb0 - contact - rb) * s * 0.8 pa, pb = (y, xa) if vert else (xa, y), (y, xb) if vert else (xb, y) SA.append(_state(*pa, ra, angA, ca)) SB.append(_state(*pb, rb, angB, cb)) return [SA, SB] if k == "chase": lead = _single_states(rng, b, {"kind": _choice(rng, ["move", "circle", "zigzag"]), "dir": _choice(rng, ["left", "right", "up", "down"])}, T) lag = 4 SA = [] for i in range(T): j = i - lag if j >= 0: x, y = lead[j][0], lead[j][1] else: # extrapolate backwards along the leader's initial velocity vx, vy = lead[1][0] - lead[0][0], lead[1][1] - lead[0][1] x, y = lead[0][0] + vx * j, lead[0][1] + vy * j SA.append(_state(np.clip(x, ra, 1 - ra), np.clip(y, ra, 1 - ra), ra, angA, ca)) SB = [[s[0], s[1], rb, angB, tuple(cb), 1.0] for s in lead] return [SA, SB] if k == "orbit": R = _uni(rng, 0.24, 0.32) c = (_uni(rng, 0.5 - (0.5 - R - ra - 0.02), 0.5 + (0.5 - R - ra - 0.02)), _uni(rng, 0.5 - (0.5 - R - ra - 0.02), 0.5 + (0.5 - R - ra - 0.02))) SA = _single_states(rng, a, {"kind": "circle"}, T, center=c) SA = [[s[0], s[1], ra, angA, tuple(ca), 1.0] for s in SA] SB = [_state(*c, rb, angB, cb) for _ in ts] return [SA, SB] if k == "swap": p0 = _uni(rng, 0.25, 0.75, size=2) th = _uni(rng, 0, 2 * math.pi) R = _uni(rng, 0.2, 0.28) c = np.clip(p0, R + 0.12, 1 - R - 0.12) SA, SB = [], [] for t in ts: ph = th + math.pi * t SA.append(_state(c[0] + R * math.cos(ph), c[1] + R * math.sin(ph), ra, angA, ca)) SB.append(_state(c[0] - R * math.cos(ph), c[1] - R * math.sin(ph), rb, angB, cb)) return [SA, SB] if k == "independent": return [_single_states(rng, a, ev["a"], T), _single_states(rng, b, ev["b"], T)] raise ValueError(k) # ---------------------------------------------------------------- sampling a scene def _random_obj(rng, bg: str, shape=None, color=None, size=None) -> dict: while True: o = {"shape": shape or _choice(rng, SHAPES), "color": color or _choice(rng, list(COLORS)), "size": size or _choice(rng, list(SIZES))} if _contrast_ok(COLORS[o["color"]], bg): return o color = None def _random_single_event(rng, obj: dict, bg: str) -> dict: while True: k = _weighted(rng, SINGLE_EVENTS) if k == "spin" and obj["shape"] in ROTATION_INVARIANT: continue ev = {"kind": k} if k in ("move", "zigzag"): ev["dir"] = _choice(rng, list(DIRS) if k == "move" else ["left", "right", "up", "down"]) if k == "jump": ev["dir"] = _choice(rng, ["left", "right", None]) if k == "recolor": to = _choice(rng, [c for c in COLORS if c != obj["color"] and _contrast_ok(COLORS[c], bg)]) ev["to"] = to return ev def is_heldout(scene: dict) -> bool: kinds = [scene["event"]["kind"]] if kinds[0] == "independent": kinds = [scene["event"]["a"]["kind"], scene["event"]["b"]["kind"]] shapes = [o["shape"] for o in scene["objects"]] res = scene["event"].get("result") or scene["event"].get("source") if res: shapes.append(res["shape"]) return any((k, sh) in HELDOUT for k in kinds for sh in shapes) def featured_scene(rng, bg: str = "black", mention_bg: bool = False) -> dict: """The user's example: a red circle and a blue circle merge into a purple circle.""" small = ["tiny", "small", "small", "medium"] a = {"shape": "circle", "color": "red", "size": _choice(rng, small)} b = {"shape": "circle", "color": "blue", "size": _choice(rng, small)} if rng.random() < 0.5: a, b = b, a res = {"shape": "circle", "color": "purple", "size": _choice(rng, ["medium", "big"])} return {"objects": [a, b], "event": {"kind": "merge", "result": res}, "bg": bg, "mention_bg": mention_bg} def random_scene(rng, cfg: WorldConfig, split: str = "train") -> dict: """split: 'train' (never a HELDOUT combo), 'heldout' (always one), 'any'.""" while True: scene = _random_scene(rng, cfg) if split == "any" or (split == "heldout") == is_heldout(scene): return scene def _random_scene(rng, cfg: WorldConfig) -> dict: if rng.random() < cfg.p_featured: mention_bg = rng.random() < cfg.p_mention_bg bg = _choice(rng, ["black", "white", "gray", "sky", "grass"]) if mention_bg else "black" return featured_scene(rng, bg, mention_bg) mention_bg = rng.random() < cfg.p_mention_bg bg = _choice(rng, list(BACKGROUNDS)) if (mention_bg or rng.random() > cfg.p_default_black) else "black" if rng.random() >= cfg.p_pair: o = _random_obj(rng, bg, size=_choice(rng, ["small", "medium", "medium", "big", "big", "huge", "tiny"])) return {"objects": [o], "event": _random_single_event(rng, o, bg), "bg": bg, "mention_bg": mention_bg} k = _weighted(rng, {**PAIR_EVENTS, "merge": cfg.merge_weight}) small = ["tiny", "small", "small", "medium"] if k in ("merge", "split"): a = _random_obj(rng, bg, size=_choice(rng, small)) partners = [next(iter(pair - {a["color"]})) for pair in MIX if a["color"] in pair] b_color = _choice(rng, partners) if partners and rng.random() < 0.8 else a["color"] if not _contrast_ok(COLORS[b_color], bg): return _random_scene(rng, cfg) b = _random_obj(rng, bg, color=b_color, size=_choice(rng, small)) if rng.random() < 0.6: # same shape most of the time, so the result's shape is unambiguous b["shape"] = a["shape"] mixed = mix_colors(a["color"], b["color"]) if not _contrast_ok(COLORS[mixed], bg): return _random_scene(rng, cfg) res = {"shape": a["shape"] if a["shape"] == b["shape"] else "circle", "color": mixed, "size": _choice(rng, ["medium", "big"])} ev = {"kind": k, "result" if k == "merge" else "source": res} return {"objects": [a, b], "event": ev, "bg": bg, "mention_bg": mention_bg} a = _random_obj(rng, bg, size=_choice(rng, small)) b = _random_obj(rng, bg, size=_choice(rng, small + (["big"] if k == "orbit" else []))) if a == b: return _random_scene(rng, cfg) ev = {"kind": k} if k == "independent": ev["a"], ev["b"] = _random_single_event(rng, a, bg), _random_single_event(rng, b, bg) return {"objects": [a, b], "event": ev, "bg": bg, "mention_bg": mention_bg} def caption_scene(rng, scene: dict) -> str: objs, ev = scene["objects"], scene["event"] cap = describe_single(rng, objs[0], ev) if len(objs) == 1 else describe_pair(rng, objs[0], objs[1], ev) bgp = _choice(rng, BG_PHRASES[scene["bg"]]) if scene["mention_bg"] else None return _style(rng, cap, bgp) # ---------------------------------------------------------------- rendering def _poly_mask(px, py, verts): inside = np.zeros(px.shape, bool) n = len(verts) for i in range(n): x1, y1 = verts[i] x2, y2 = verts[(i + 1) % n] crosses = (y1 > py) != (y2 > py) xint = (x2 - x1) * (py - y1) / (y2 - y1 + 1e-12) + x1 inside ^= crosses & (px < xint) return inside def _regular(n, r, ang, cx, cy, inner=None): k = 2 * n if inner else n return [(cx + (inner if inner and i % 2 else r) * math.cos(ang - math.pi / 2 + 2 * math.pi * i / k), cy + (inner if inner and i % 2 else r) * math.sin(ang - math.pi / 2 + 2 * math.pi * i / k)) for i in range(k)] def shape_mask(shape, px, py, cx, cy, r, ang): dx, dy = px - cx, py - cy if shape == "circle": return dx * dx + dy * dy <= r * r if shape == "ring": d2 = dx * dx + dy * dy return (d2 <= r * r) & (d2 >= (0.55 * r) ** 2) if shape == "square": return _poly_mask(px, py, _regular(4, r * 1.15, ang + math.pi / 4, cx, cy)) if shape == "triangle": return _poly_mask(px, py, _regular(3, r * 1.2, ang, cx, cy)) if shape == "star": return _poly_mask(px, py, _regular(5, r * 1.25, ang, cx, cy, inner=r * 0.5)) if shape == "hexagon": return _poly_mask(px, py, _regular(6, r * 1.05, ang, cx, cy)) c, s = math.cos(ang), math.sin(ang) u, v = (c * dx + s * dy) / r, (-s * dx + c * dy) / r if shape == "diamond": return np.abs(u) / 0.75 + np.abs(v) / 1.15 <= 1 if shape == "cross": arm = 0.33 return ((np.abs(u) <= 1) & (np.abs(v) <= arm)) | ((np.abs(v) <= 1) & (np.abs(u) <= arm)) if shape == "heart": # upright heart: small tilt only, so it stays recognisable c, s = math.cos(0.3 * math.sin(ang)), math.sin(0.3 * math.sin(ang)) u, v = (c * dx + s * dy) / r, (-s * dx + c * dy) / r x, y = u * 1.15, -v * 1.15 + 0.15 return (x * x + y * y - 1) ** 3 - x * x * y ** 3 <= 0 raise ValueError(shape) def _background(bg, S): top, bot = (np.array(c, np.float32) for c in BACKGROUNDS[bg]) w = np.linspace(0, 1, S, dtype=np.float32)[:, None, None] return np.broadcast_to(top * (1 - w) + bot * w, (S, S, 3)).copy() def render_scene(scene: dict, rng, cfg: WorldConfig) -> np.ndarray: T, S, ss = cfg.frames, cfg.size, cfg.supersample big = S * ss co = (np.arange(big, dtype=np.float32) + 0.5) / big px, py = np.meshgrid(co, co) objs, ev = scene["objects"], scene["event"] if len(objs) == 1: tracks = [_single_states(rng, objs[0], ev, T)] shapes = [objs[0]["shape"]] else: tracks = _pair_states(rng, objs[0], objs[1], ev, T) shapes = [objs[0]["shape"], objs[1]["shape"]] if ev["kind"] in ("merge", "split"): shapes.append((ev.get("result") or ev.get("source"))["shape"]) scene["tracks"] = tracks scene["track_shapes"] = shapes first = [i for i, tr in enumerate(tracks) if tr[0][5] > 0.5 and tr[0][2] > 0.005] last = [i for i, tr in enumerate(tracks) if tr[T - 1][5] > 0.5 and tr[T - 1][2] > 0.005] scene["summary"] = {"initial_count": len(first), "final_count": len(last), "initial_colors": [nearest_color(tracks[i][0][4]) for i in first], "final_colors": [nearest_color(tracks[i][T - 1][4]) for i in last], "final_shapes": [shapes[i] for i in last]} base = _background(scene["bg"], big) frames = np.empty((T, S, S, 3), np.uint8) for f in range(T): img = base.copy() for shape, tr in zip(shapes, tracks): x, y, r, ang, rgb, alpha = tr[f] if alpha <= 0.01 or r <= 0.005: continue m = shape_mask(shape, px, py, x, y, r, ang) img[m] = img[m] * (1 - alpha) + np.array(rgb, np.float32) * alpha img = img.reshape(S, ss, S, ss, 3).mean(axis=(1, 3)) frames[f] = np.clip(img + 0.5, 0, 255).astype(np.uint8) return frames def make_clip(rng, cfg: WorldConfig | None = None, split: str = "train"): cfg = cfg or WorldConfig() scene = random_scene(rng, cfg, split) caption = caption_scene(rng, scene) video = render_scene(scene, rng, cfg) scene["caption"] = caption return video, caption, scene def sample_prompts(n: int, split: str = "train", seed: int = 0, cfg: WorldConfig | None = None) -> list[tuple[str, dict]]: """Captions + scenes. Call render_scene(scene, rng, cfg) to get ground-truth tracks/summary for a scene.""" rng = np.random.default_rng(seed) cfg = cfg or WorldConfig() out = [] for _ in range(n): scene = random_scene(rng, cfg, split) out.append((caption_scene(rng, scene), scene)) return out def to_tensor(video: np.ndarray) -> torch.Tensor: return torch.from_numpy(video).permute(3, 0, 1, 2).float().div(127.5).sub(1) class WorldDataset(torch.utils.data.IterableDataset): """Infinite stream of (video float [3,T,H,W] in [-1,1], caption).""" def __init__(self, cfg: WorldConfig | None = None, seed: int = 0, split: str = "train"): self.cfg = cfg or WorldConfig() self.seed = seed self.split = split def __iter__(self): info = torch.utils.data.get_worker_info() rng = np.random.default_rng([self.seed, info.id if info else 0]) while True: video, caption, _ = make_clip(rng, self.cfg, self.split) yield to_tensor(video), caption def collate(batch): videos, captions = zip(*batch) return torch.stack(videos), list(captions)