Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import random | |
| from .genome import Part, Genome, PART_KINDS, ARCHETYPES, ANIMALS, DEFAULT_PALETTE, clamp_part | |
| # Each archetype has a signature palette so a freshly hatched ancestor already | |
| # reads as that animal (white chick, brown horse, blue hippo, ...). The model / | |
| # fusion can override these. | |
| _ARCHETYPE_PALETTE = { | |
| # Warm baby-chick yellow (not Crossy-Road white) so it reads as our own chick. | |
| "chick": ("#ffd24a", "#ff8a3d", "#e2913b"), | |
| "sheep": ("#f3efe6", "#caa06a", "#3a2f2a"), | |
| "horse": ("#a4663a", "#6e3f1f", "#2b1a0f"), | |
| "dog": ("#d8923f", "#7a4a1e", "#2b1a0f"), | |
| "hippo": ("#3b7ad4", "#2b5aa0", "#ff9bd2"), | |
| "frog": ("#4caf50", "#2e7d32", "#fff7d6"), | |
| "turtle": ("#5a8f3a", "#3f6e2a", "#86c46a"), | |
| "penguin": ("#20203a", "#f7f7f7", "#ff9b18"), | |
| } | |
| _LIMBS = ("leg", "arm", "tail", "wing", "horn", "paw") | |
| _MUTATIONS = ( | |
| "a third eye that never blinks", "a tail it keeps tripping over", | |
| "ears that point the wrong way", "one oversized paw", | |
| "a horn that hums when nervous", "wings far too small to fly", | |
| ) | |
| _NAMES = ("Mittenmaw", "Croaklet", "Fluffgnaw", "Snorbit", "Wigglethorn", "Pebblesnout", "Gloomsprout") | |
| def _rng(seed: int) -> random.Random: | |
| return random.Random(seed * 2654435761 % 2**32) | |
| def fuse_boxes_fallback(a_boxes, b_boxes, seed: int = 0) -> list: | |
| """Deterministic box merge used when no model is available (fake runtime) or the | |
| freeform fuse call fails. Takes the lower half of A (body/legs) and the upper | |
| half of B (head/top) by y, so the child visibly combines both. Never empty as | |
| long as at least one parent has boxes.""" | |
| a = [b for b in (a_boxes or []) if isinstance(b, dict)] | |
| b = [b for b in (b_boxes or []) if isinstance(b, dict)] | |
| if not a: | |
| return list(b) | |
| if not b: | |
| return list(a) | |
| def mid(boxes): | |
| ys = [int(x.get("y", 0)) for x in boxes] | |
| return (min(ys) + max(ys)) / 2 | |
| lower = [dict(x) for x in a if int(x.get("y", 0)) <= mid(a)] | |
| upper = [dict(x) for x in b if int(x.get("y", 0)) > mid(b)] | |
| merged = lower + upper | |
| return merged or (list(a) + list(b)) | |
| def fallback_genome(seed: int, archetype: str | None = None) -> Genome: | |
| r = _rng(seed) | |
| archetype = archetype if archetype in ARCHETYPES else r.choice(ARCHETYPES) | |
| palette = list(_ARCHETYPE_PALETTE[archetype]) | |
| # Minimal parts: the archetype template draws the body/head/legs itself, so | |
| # parts here only carry optional mutations layered on top. | |
| parts: list[Part] = [] | |
| if r.random() < 0.45: | |
| kind = r.choice(_LIMBS) | |
| parts.append(clamp_part(Part(kind, (1, r.randint(2, 3), 1), r.choice(palette), | |
| count=r.choice([1, 2]), tip=r.choice(("none", "paw", "claw"))))) | |
| name = r.choice(_NAMES) | |
| return Genome(name=name, palette=palette, parts=parts, archetype=archetype, | |
| traits=[r.choice(_MUTATIONS)], story="A freshly hatched ancestor.") | |
| def _blend_hex(a: str, b: str) -> str: | |
| av, bv = a.lstrip("#"), b.lstrip("#") | |
| mix = [(int(av[i:i+2], 16) + int(bv[i:i+2], 16)) // 2 for i in (0, 2, 4)] | |
| return "#" + "".join(f"{c:02x}" for c in mix) | |
| def fuse_fallback(a: Genome, b: Genome, seed: int) -> Genome: | |
| r = _rng(seed * 31 + 7) | |
| # Chimera: body silhouette from one parent, the rest is recombined parts. | |
| archetype = r.choice([a.archetype, b.archetype]) | |
| other = b if archetype == a.archetype else a | |
| parts: list[Part] = [] | |
| kinds = [] | |
| for kind in PART_KINDS: | |
| pa = next((p for p in a.parts if p.kind == kind), None) | |
| pb = next((p for p in b.parts if p.kind == kind), None) | |
| chosen = r.choice([pa, pb]) if pa and pb else (pa or pb) | |
| if chosen: | |
| parts.append(chosen) | |
| kinds.append(kind) | |
| # graft the OTHER parent's animal head-style on as a head part (visible chimera) | |
| if other.archetype != archetype: | |
| parts.append(clamp_part(Part("head", (3, 3, 3), (other.palette[0] if other.palette else "#cccccc"), | |
| animal=_archetype_animal(other.archetype)))) | |
| kinds.append("head") | |
| # blended palette | |
| palette = [_blend_hex(x, y) for x, y in zip(a.palette, b.palette)] or list(a.palette) or list(DEFAULT_PALETTE) | |
| # forced mutation: a brand-new part the inheritance did not pick | |
| spare = [k for k in ("tail", "wing", "horn", "eye") if k not in kinds] | |
| if spare: | |
| parts.append(clamp_part(Part(r.choice(spare), (1, 2, 1), r.choice(palette), count=1))) | |
| mutation = r.choice(_MUTATIONS) | |
| traits = [mutation] + [t for t in (a.traits + b.traits) if t][:1] | |
| name = (a.name[: len(a.name)//2] + b.name[len(b.name)//2:]).title()[:40] or "Splice" | |
| story = f"{a.name} × {b.name} → {archetype} body, got {mutation}." | |
| return Genome(name=name, palette=palette[:5], parts=parts, archetype=archetype, traits=traits, story=story) | |
| def _archetype_animal(archetype: str) -> str: | |
| return {"chick": "bird", "penguin": "bird", "dog": "dog", "frog": "frog", | |
| "horse": "dog", "sheep": "dog", "hippo": "frog", "turtle": "frog"}.get(archetype, "none") | |
| # --- Deterministic voice command editor (offline-capable; also the LLM fallback) --- | |
| _ARCH_WORDS = { | |
| "chick": ("chick", "chicken", "雞", "鳥"), "sheep": ("sheep", "羊"), | |
| "horse": ("horse", "pony", "馬"), "dog": ("dog", "puppy", "狗"), | |
| "hippo": ("hippo", "河馬"), "frog": ("frog", "青蛙", "蛙"), | |
| "turtle": ("turtle", "tortoise", "烏龜", "龜"), "penguin": ("penguin", "企鵝"), | |
| } | |
| _PART_WORDS = { | |
| "horn": ("horn", "角"), "wing": ("wing", "翅膀", "翅"), "tail": ("tail", "尾巴", "尾"), | |
| "leg": ("leg", "腿", "腳"), "arm": ("arm", "手", "手臂"), "eye": ("eye", "eyes", "眼睛", "眼"), | |
| "head": ("head", "頭"), | |
| } | |
| _COLOR_WORDS = { | |
| "red": ("red", "紅"), "orange": ("orange", "橙", "橘"), "yellow": ("yellow", "黃"), | |
| "green": ("green", "綠"), "blue": ("blue", "藍"), "purple": ("purple", "紫"), | |
| "pink": ("pink", "粉"), "white": ("white", "白"), "black": ("black", "黑"), | |
| } | |
| _COLOR_HEX = { | |
| "red": "#ef4444", "orange": "#ff8a3d", "yellow": "#facc15", "green": "#4caf50", | |
| "blue": "#3b7ad4", "purple": "#a855f7", "pink": "#ff9bd2", "white": "#f7f7f7", "black": "#20203a", | |
| } | |
| _NUM_WORDS = {"two": 2, "three": 3, "four": 4, "兩": 2, "二": 2, "三": 3, "四": 4, "2": 2, "3": 3, "4": 4} | |
| def _hit(text: str, words: tuple) -> bool: | |
| return any(w in text for w in words) | |
| def _box_bounds(boxes: list[dict]): | |
| """Rough extents of a box list for placing appended parts. Returns | |
| (top_y, front_z, back_z, side_x) as integers.""" | |
| top = 0 | |
| front = 0 | |
| back = 0 | |
| side = 1 | |
| for b in boxes: | |
| x = int(b.get("x", 0)); z = int(b.get("z", 0)); y = int(b.get("y", 0)) | |
| w = int(b.get("w", 1)); h = int(b.get("h", 1)); d = int(b.get("d", 1)) | |
| top = max(top, y + h) | |
| front = max(front, z + (d + 1) // 2) | |
| back = min(back, z - (d + 1) // 2) | |
| side = max(side, abs(x) + (w + 1) // 2) | |
| return top, front, back, side | |
| def edit_boxes_fallback(boxes: list[dict], instruction: str) -> list[dict]: | |
| """Deterministic, box-aware edit used by the fake runtime and as the freeform | |
| LLM fallback (the box analogue of voice_edit_genome). Recognises recolour | |
| ("make it purple") and additive parts ("add two horns", "give it wings", | |
| "a tail", "extra legs", "arms", "eyes"). Unknown input returns the boxes | |
| unchanged. Never strips existing geometry — it only recolours or appends.""" | |
| src = [dict(b) for b in (boxes or []) if isinstance(b, dict)] | |
| if not src: | |
| return src | |
| t = (instruction or "").lower() | |
| changed = False | |
| # Recolour the dominant (most-common) colour across the whole creature. | |
| for col, words in _COLOR_WORDS.items(): | |
| if _hit(t, words): | |
| counts: dict[str, int] = {} | |
| for b in src: | |
| counts[b.get("color", "")] = counts.get(b.get("color", ""), 0) + 1 | |
| dominant = max(counts, key=counts.get) if counts else None | |
| new_hex = _COLOR_HEX[col] | |
| for b in src: | |
| if b.get("color") == dominant: | |
| b["color"] = new_hex | |
| changed = True | |
| break | |
| top, front, back, side = _box_bounds(src) | |
| accent = src[0].get("color", "#caa06f") | |
| count = next((n for w, n in _NUM_WORDS.items() if w in t), 1) | |
| def add(x, y, z, w, h, d, color): | |
| src.append({"x": x, "y": y, "z": z, "w": w, "h": h, "d": d, "color": color}) | |
| if _hit(t, _PART_WORDS["horn"]): | |
| for sx in ([-1, 1][:max(1, min(2, count))] if count >= 2 else [0]): | |
| add(sx, top, front - 1, 1, 2, 1, accent) | |
| changed = True | |
| elif _hit(t, _PART_WORDS["wing"]): | |
| for sx in (-(side + 1), side + 1): | |
| add(sx, max(1, top - 2), 0, 1, 2, 2, accent) | |
| changed = True | |
| elif _hit(t, _PART_WORDS["tail"]): | |
| add(0, max(1, top - 3), back - 1, 1, 2, 2, accent) | |
| changed = True | |
| elif _hit(t, _PART_WORDS["leg"]): | |
| for sx in (-(side - 1), side - 1): | |
| add(sx, 0, 0, 1, 2, 1, accent) | |
| changed = True | |
| elif _hit(t, _PART_WORDS["arm"]): | |
| for sx in (-(side + 1), side + 1): | |
| add(sx, max(1, top - 3), 1, 1, 2, 1, accent) | |
| changed = True | |
| elif _hit(t, _PART_WORDS["eye"]): | |
| for sx in (-1, 1): | |
| add(sx, top - 2, front, 1, 1, 1, "#16161f") | |
| changed = True | |
| return src if changed else [dict(b) for b in src] | |
| # Concise full-sentence tweak ideas for the Splice Bench hint bubbles. Used as the | |
| # first-render pool AND as the deterministic fallback when the model is absent | |
| # (fake runtime) or the suggest call fails. EVERY entry must resolve to a real | |
| # catalog part via edit_parser.match_part — clicking a chip ADDS that part (the | |
| # edit path can't reshape the body), so body-shape phrasings like "make it tall" | |
| # are banned: they silently matched the wrong part (tophat / round ears). Keep this | |
| # list in sync with the parts catalog; verify new entries with match_part. | |
| SUGGEST_POOL = [ | |
| "Add two curved horns", "Give it a pair of wings", "Add a long swishy tail", | |
| "Add big round eyes", "Give it tall pointy ears", "Add stubby little legs", | |
| "Add spikes down its back", "Give it tiny arms", "Put a crown on its head", | |
| "Give it a turtle shell", "Wrap a cozy scarf around it", "Give it sharp fangs", | |
| "Add cat ears", "Give it a fox tail", "Add a jetpack", "Top it with a party hat", | |
| ] | |
| def suggest_tweaks_fallback(boxes: list[dict]) -> list[str]: | |
| """Deterministic 4 tweak suggestions. Rotated by the creature's box count so | |
| the set visibly changes as the form grows (gives the fake runtime a sense of | |
| 'the hints react to the creature' without a model).""" | |
| n = len(boxes or []) | |
| pool = SUGGEST_POOL | |
| start = n % len(pool) | |
| return [pool[(start + i) % len(pool)] for i in range(4)] | |
| def voice_edit_genome(genome: Genome, transcript: str) -> Genome: | |
| """Apply a spoken instruction to a genome deterministically (no model). | |
| Recognises: change archetype ("turn into a frog"), add a part | |
| ("add two horns"), recolour ("make it purple"). Also matches the CJK | |
| synonyms held in the keyword tables above. Unknown input leaves the genome | |
| unchanged. Used by the fake runtime and as the real runtime's fallback so | |
| voice always does *something* offline. | |
| """ | |
| t = (transcript or "").lower() | |
| archetype = genome.archetype | |
| for arch, words in _ARCH_WORDS.items(): | |
| if _hit(t, words): | |
| archetype = arch | |
| break | |
| palette = list(genome.palette) | |
| for col, words in _COLOR_WORDS.items(): | |
| if _hit(t, words): | |
| palette = [_COLOR_HEX[col]] + palette[1:] | |
| break | |
| parts = list(genome.parts) | |
| count = next((n for w, n in _NUM_WORDS.items() if w in t), 1) | |
| for kind, words in _PART_WORDS.items(): | |
| if _hit(t, words): | |
| parts.append(clamp_part(Part(kind, (1, 2, 1), palette[0] if palette else "#cccccc", count=count))) | |
| break | |
| if archetype == genome.archetype and palette == list(genome.palette) and parts == list(genome.parts): | |
| return genome # nothing recognised | |
| return Genome(name=genome.name, palette=palette, parts=parts, archetype=archetype, | |
| traits=genome.traits, story=genome.story) | |