| """ |
| Pocket Bestiary |
| =============== |
| Name any ordinary place or thing. Discover the tiny creature hiding in it. |
| |
| Built for the "Small Models Big Adventures" hackathon (Track 2). Every discovery |
| is written up as a field-guide card (name, species, habitat, temperament, a note, |
| a rarity) and added to your collectible field guide for the session. |
| |
| Model: openbmb/MiniCPM5-1B (1B params, well under the 32B ceiling, on-device). |
| A small model has a confident, slightly feral imagination, which is exactly the |
| right instrument for inventing creatures. Same proven inference path as the rest |
| of this project. |
| |
| Modes: |
| * model mode -> MiniCPM5-1B invents each creature |
| * keeper mode -> a charming, input-seeded generator so the app always boots and |
| demos even with no GPU and no network |
| """ |
|
|
| import os |
| import re |
| import html |
| import random |
| import inspect |
|
|
| import gradio as gr |
|
|
| |
| |
| |
| _BLOCKS_HAS_CSS = "css" in inspect.signature(gr.Blocks.__init__).parameters |
| _LAUNCH_HAS_SSR = "ssr_mode" in inspect.signature(gr.Blocks.launch).parameters |
|
|
| |
| |
| |
| MODEL_ID = os.environ.get("BESTIARY_MODEL", "openbmb/MiniCPM5-1B") |
| DEBUG = os.environ.get("BESTIARY_DEBUG", "").strip().lower() in {"1", "true", "yes"} |
| MAX_NEW_TOKENS = 170 |
|
|
| SYSTEM_PROMPT = ( |
| "You are the Keeper of the Pocket Bestiary, a gentle naturalist who discovers " |
| "tiny imaginary creatures hiding in ordinary places. Given a place or object, " |
| "invent ONE small creature that secretly lives there. Reply using EXACTLY these " |
| "five labels, each on its own line, and nothing else:\n" |
| "Name: <a whimsical creature name, 1 to 3 words>\n" |
| "Type: <a fanciful species or order, 2 to 4 words>\n" |
| "Habitat: <one short phrase tying it to the given place>\n" |
| "Temperament: <2 to 4 words>\n" |
| "Note: <one or two warm, surprising sentences>\n" |
| "Make the creature's name, look, and habits grow clearly out of the specific " |
| "details of the place: its mood, time of day, textures, sounds, and what it is " |
| "for. A different place should give a clearly different creature. " |
| "Keep everything gentle and suitable for all ages. Never describe or identify " |
| "real people; if the place involves a person, invent a creature that lives " |
| "near them instead." |
| ) |
|
|
| RARITIES = [ |
| ("Common", "#6f8456", 50), |
| ("Uncommon", "#3a7ca5", 30), |
| ("Rare", "#a9641c", 15), |
| ("Mythic", "#7a4fa3", 5), |
| ] |
|
|
| EXAMPLES = [ |
| "the gap behind the fridge", |
| "your coat pocket", |
| "a bus stop at night", |
| "the bottom of a teacup", |
| ] |
|
|
| |
| |
| |
| def _noop_gpu(*a, **k): |
| def wrap(fn): |
| return fn |
| return wrap(a[0]) if a and callable(a[0]) else wrap |
|
|
| if os.environ.get("SPACES_ZERO_GPU", "").lower() in {"true", "1"}: |
| try: |
| import spaces |
| GPU = spaces.GPU |
| except Exception: |
| GPU = _noop_gpu |
| else: |
| GPU = _noop_gpu |
|
|
| |
| |
| |
| _tokenizer = None |
| _model = None |
| MODE = "keeper" |
| _warmed = False |
|
|
|
|
| def load_model(): |
| global _tokenizer, _model, MODE |
| try: |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| _tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) |
| _model = AutoModelForCausalLM.from_pretrained( |
| MODEL_ID, trust_remote_code=True, torch_dtype=torch.float32, |
| ) |
| _model.eval() |
| MODE = "model" |
| print(f"[Bestiary] Loaded {MODEL_ID} -- model mode.") |
| except Exception as exc: |
| MODE = "keeper" |
| print(f"[Bestiary] Could not load {MODEL_ID} ({exc}). Keeper mode active.") |
|
|
|
|
| load_model() |
|
|
|
|
| @GPU(duration=40) |
| def _model_generate(user_text): |
| import torch |
| if torch.cuda.is_available() and next(_model.parameters()).device.type != "cuda": |
| _model.to("cuda", dtype=torch.bfloat16) |
| |
| messages = [ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", "content": "Place or object: the dust under a piano"}, |
| {"role": "assistant", "content": |
| "Name: Pedalfluff\n" |
| "Type: Lesser Resonance-mote\n" |
| "Habitat: nestles in the dust beneath the piano's pedals\n" |
| "Temperament: drowsy, music-loving\n" |
| "Note: It wakes only for the low notes, and hums along a half-second behind."}, |
| {"role": "user", "content": f"Place or object: {user_text}"}, |
| ] |
|
|
| def _encode(extra): |
| return _tokenizer.apply_chat_template( |
| messages, add_generation_prompt=True, return_tensors="pt", |
| return_dict=True, **extra |
| ) |
|
|
| |
| try: |
| enc = _encode({"enable_thinking": False}) |
| except TypeError: |
| enc = _encode({}) |
| enc = enc.to(_model.device) |
| input_len = enc["input_ids"].shape[1] |
| with torch.no_grad(): |
| out = _model.generate( |
| **enc, max_new_tokens=MAX_NEW_TOKENS, do_sample=True, |
| temperature=0.8, top_p=0.9, repetition_penalty=1.1, |
| pad_token_id=_tokenizer.eos_token_id, |
| ) |
| return _tokenizer.decode(out[0][input_len:], skip_special_tokens=True).strip() |
|
|
|
|
| |
| |
| |
| |
| _PREFIX = ["Dust", "Moss", "Hollow", "Lint", "Ember", "Glimmer", "Brass", "Gloam", |
| "Thistle", "Murk", "Candle", "Hush", "Drowse", "Pocket", "Cinder", "Fen"] |
| _SUFFIX = ["wyrm", "sprite", "mote", "kin", "ling", "snout", "wick", "boggle", |
| "fluff", "grub", "nibble", "whisker", "tuft", "snuffle"] |
| _ORDERS = ["Lesser Hearth-sprite", "Common Pocket-wyrm", "Drowsy Threshold-kin", |
| "Crumb-tending Mote", "Shy Corner-boggle", "Lint-spinning Grub", |
| "Dust-shy Whiskerling", "Twilight Sill-wick"] |
| _TEMPERS = ["shy but loyal", "grumbly, fond of quiet", "endlessly curious", |
| "drowsy and ceremonious", "mischievous, harmless", "gentle, easily startled"] |
| _NOTES = [ |
| "It tidies one small thing each night and takes no credit for it.", |
| "It hums a tune only forgotten objects can hear.", |
| "It collects the warmth left behind by hands and saves it for winter.", |
| "It is mostly polite, and deeply offended by vacuum cleaners.", |
| "It keeps the lost buttons company so they feel less alone.", |
| "It naps through the day and rearranges the dust into small constellations at night.", |
| ] |
|
|
|
|
| def _keeper_creature(user_text): |
| rng = random.Random(user_text.strip().lower()) |
| name = rng.choice(_PREFIX) + rng.choice(_SUFFIX) |
| place = user_text.strip() |
| return { |
| "name": name, |
| "type": rng.choice(_ORDERS), |
| "habitat": f"makes its home in {place}", |
| "temperament": rng.choice(_TEMPERS), |
| "note": rng.choice(_NOTES), |
| } |
|
|
|
|
| |
| |
| |
| |
| _THINK = re.compile(r"<think>.*?</think>", re.S | re.I) |
| |
| _BAD = re.compile( |
| r"<|whimsical creature name|fanciful species|short phrase|one or two|" |
| r"\bI'll\b|\bI will\b|\blet me\b|\bmaybe\b|\bI should\b|\bI need\b|" |
| r"\bthe response\b|\blabels?\b|\bsomething similar\b|\bfits\b", re.I) |
|
|
|
|
| def _strip_think(t): |
| t = _THINK.sub("", t) |
| t = re.sub(r"^.*?</think>", "", t, flags=re.S | re.I) |
| t = re.sub(r"<think>.*$", "", t, flags=re.S | re.I) |
| return t.strip() |
|
|
|
|
| def _grab(label, text): |
| |
| m = re.search(rf"^\s*{label}\s*[:\-]\s*(.+)$", text, re.IGNORECASE | re.MULTILINE) |
| if not m: |
| return "" |
| val = m.group(1).strip().strip('"').strip() |
| return "" if _BAD.search(val) else val |
|
|
|
|
| def parse_creature(raw, user_text): |
| raw = _strip_think(raw) |
| fields = { |
| "name": _grab("Name", raw), |
| "type": _grab("Type", raw), |
| "habitat": _grab("Habitat", raw), |
| "temperament": _grab("Temperament", raw), |
| "note": _grab("Note", raw), |
| } |
| found = sum(1 for v in fields.values() if v) |
| |
| |
| if found < 2: |
| base = _keeper_creature(user_text) |
| cleaned = " ".join(raw.split()) |
| if cleaned and not _BAD.search(cleaned) and 8 < len(cleaned) < 240: |
| base["note"] = cleaned |
| return base |
| base = _keeper_creature(user_text) |
| for k, v in fields.items(): |
| if v: |
| base[k] = v.rstrip(". ")[:200] if k != "note" else v[:300] |
| return base |
|
|
|
|
| def roll_rarity(): |
| pool = [] |
| for label, color, weight in RARITIES: |
| pool += [(label, color)] * weight |
| return random.choice(pool) |
|
|
|
|
| def discover(user_text): |
| raw = "" |
| if MODE == "model": |
| try: |
| raw = _model_generate(user_text) |
| except Exception as exc: |
| print(f"[Bestiary] generation error: {exc}") |
| raw = "" |
| creature = parse_creature(raw, user_text) if raw else _keeper_creature(user_text) |
| label, color = roll_rarity() |
| creature["rarity"] = label |
| creature["rarity_color"] = color |
| creature["found_in"] = user_text.strip() |
| return creature |
|
|
|
|
| |
| |
| |
| def esc(s): |
| return html.escape(str(s)) |
|
|
|
|
| def sigil_svg(seed_str, color, size=64): |
| """A small deterministic 'pressed specimen' emblem, unique per creature. |
| No image model: just seeded shapes in the storybook palette.""" |
| rng = random.Random("sigil:" + seed_str.lower()) |
| import math |
| cx = cy = size / 2 |
| body_r = size * 0.28 |
| parts = [f'<circle cx="{cx}" cy="{cy}" r="{body_r}" fill="#fffaf0" stroke="{color}" stroke-width="2"/>'] |
| |
| limbs = rng.randint(4, 8) |
| for i in range(limbs): |
| ang = (2 * 3.14159 * i / limbs) + rng.uniform(-0.2, 0.2) |
| x1 = cx + body_r * math.cos(ang) |
| y1 = cy + body_r * math.sin(ang) |
| x2 = cx + (body_r + size * 0.16) * math.cos(ang) |
| y2 = cy + (body_r + size * 0.16) * math.sin(ang) |
| parts.append(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" ' |
| f'stroke="{color}" stroke-width="2" stroke-linecap="round"/>') |
| parts.append(f'<circle cx="{x2:.1f}" cy="{y2:.1f}" r="{size*0.03:.1f}" fill="{color}"/>') |
| |
| eyes = rng.choice([1, 2, 2, 3]) |
| spread = body_r * 0.5 |
| for j in range(eyes): |
| ex = cx + (j - (eyes - 1) / 2) * spread |
| ey = cy - body_r * 0.1 |
| parts.append(f'<circle cx="{ex:.1f}" cy="{ey:.1f}" r="{size*0.045:.1f}" fill="var(--ink, #3a2f25)"/>') |
| return (f'<svg viewBox="0 0 {size} {size}" width="{size}" height="{size}" ' |
| f'xmlns="http://www.w3.org/2000/svg" class="sigil">{"".join(parts)}</svg>') |
|
|
|
|
| def render_card(c): |
| if not c: |
| return ('<div class="card empty">Name a place below, then press ' |
| '<b>Discover</b> to find what lives there.</div>') |
| return f""" |
| <div class="card"> |
| <div class="card-top"> |
| <span class="rarity" style="--r:{c['rarity_color']}">{esc(c['rarity'])}</span> |
| <span class="found">found in {esc(c['found_in'])}</span> |
| </div> |
| <div class="card-head"> |
| <div class="sigil-wrap">{sigil_svg(c['name'] + c['found_in'], c['rarity_color'])}</div> |
| <div> |
| <div class="cname">{esc(c['name'])}</div> |
| <div class="ctype">{esc(c['type'])}</div> |
| </div> |
| </div> |
| <div class="crow"><span class="lbl">Habitat</span><span>{esc(c['habitat'])}</span></div> |
| <div class="crow"><span class="lbl">Temperament</span><span>{esc(c['temperament'])}</span></div> |
| <div class="cnote">{esc(c['note'])}</div> |
| </div> |
| """ |
|
|
|
|
| def render_collection(items): |
| if not items: |
| return '<div class="coll-empty">Your field guide is empty. Go find something.</div>' |
| cells = "" |
| for c in reversed(items): |
| cells += ( |
| f'<div class="mini" style="--r:{c["rarity_color"]}">' |
| f'<div class="mini-sigil">{sigil_svg(c["name"] + c["found_in"], c["rarity_color"], size=38)}</div>' |
| f'<div class="mini-rar">{esc(c["rarity"])}</div>' |
| f'<div class="mini-name">{esc(c["name"])}</div>' |
| f'<div class="mini-type">{esc(c["type"])}</div>' |
| f'<div class="mini-found">{esc(c["found_in"])}</div></div>' |
| ) |
| return (f'<div class="coll-head">Your field guide ' |
| f'<span class="count">{len(items)} discovered</span></div>' |
| f'<div class="coll-grid">{cells}</div>') |
|
|
|
|
| |
| |
| |
| def on_discover(user_text, collection): |
| global _warmed |
| user_text = (user_text or "").strip() |
| if not user_text: |
| msg = ('<div class="card empty">Tell the Keeper a place first, ' |
| 'a real one or a tiny one.</div>') |
| yield msg, render_collection(collection), collection, "" |
| return |
|
|
| if MODE == "model" and not _warmed: |
| yield ('<div class="card waking">🔎 <i>The Keeper turns the lens, focusing ' |
| '(the first discovery takes a moment)...</i></div>', |
| render_collection(collection), collection, "") |
|
|
| creature = discover(user_text) |
| if MODE == "model": |
| _warmed = True |
| collection = collection + [creature] |
| yield render_card(creature), render_collection(collection), collection, "" |
|
|
|
|
| def on_reset(): |
| return render_card(None), render_collection([]), [], "" |
|
|
|
|
| def use_example(text): |
| return text |
|
|
|
|
| |
| |
| |
| |
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Spectral:ital,wght@0,400;0,500;1,400&display=swap'); |
| |
| :root{ |
| --paper:#f3ead7; --paper-2:#efe3c9; --ink:#3a2f25; --ink-soft:#6b5a47; |
| --forest:#4a5d3a; --forest-2:#6f8456; --amber:#c98a2e; --amber-deep:#a9641c; |
| --line:#c9b48f; |
| } |
| /* force the warm light palette onto Gradio's own tokens (readable in dark mode too) */ |
| .gradio-container, .gradio-container.dark, .dark{ |
| --body-background-fill:transparent; |
| --background-fill-primary:#fffaf0; --background-fill-secondary:#f6edd8; |
| --block-background-fill:#fffaf0; --block-border-color:var(--line); |
| --border-color-primary:var(--line); --border-color-accent:var(--amber); |
| --body-text-color:var(--ink); --body-text-color-subdued:var(--ink-soft); |
| --block-label-text-color:var(--forest); --block-title-text-color:var(--ink); |
| --block-label-background-fill:#efe3c9; --block-title-background-fill:transparent; |
| --input-background-fill:#fffaf0; --input-border-color:var(--line); |
| --input-placeholder-color:var(--ink-soft); |
| --button-primary-background-fill:var(--amber); |
| --button-primary-background-fill-hover:var(--amber-deep); |
| --button-primary-text-color:#fffaf0; --button-primary-border-color:var(--amber-deep); |
| --button-secondary-background-fill:#efe3c9; |
| --button-secondary-background-fill-hover:#e7d6b3; |
| --button-secondary-text-color:var(--ink); --button-secondary-border-color:var(--line); |
| --color-accent:var(--amber-deep); --color-accent-soft:#f3e3c4; |
| } |
| .gradio-container{ |
| background:radial-gradient(120% 80% at 80% -10%, #f7efdd 0%, var(--paper) 55%, var(--paper-2) 100%) !important; |
| font-family:'Spectral', Georgia, serif !important; color:var(--ink) !important; |
| max-width:1100px !important; |
| } |
| .gradio-container textarea, .gradio-container input[type="text"], .gradio-container input:not([type]){ |
| background:#fffaf0 !important; color:var(--ink) !important; |
| -webkit-text-fill-color:var(--ink) !important; border-color:var(--line) !important;} |
| .gradio-container textarea::placeholder, .gradio-container input::placeholder{ |
| color:var(--ink-soft) !important; -webkit-text-fill-color:var(--ink-soft) !important; opacity:1;} |
| |
| .bz-title{font-family:'Fraunces',serif; font-weight:600; font-size:2.6rem; line-height:1; margin:.2rem 0 0;} |
| .bz-title em{font-style:italic; color:var(--amber-deep);} |
| .bz-sub{font-style:italic; color:var(--ink-soft); margin:.35rem 0 1rem; font-size:1.05rem;} |
| .bz-mode{display:inline-block; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; |
| color:var(--forest); border:1px solid var(--line); border-radius:999px; padding:.15rem .6rem;} |
| |
| /* the big card */ |
| .card{font-family:'Spectral',serif; color:var(--ink); background:#fffaf0; |
| border:1px solid var(--line); border-radius:18px; padding:18px 20px; |
| box-shadow:0 14px 36px -22px rgba(58,47,37,.7); position:relative; |
| background-image:repeating-linear-gradient(135deg,#fffaf0,#fffaf0 13px,#f9f1de 13px,#f9f1de 26px);} |
| .card.empty, .card.waking{color:var(--ink-soft); font-style:italic; text-align:center; |
| background:#fffaf0; box-shadow:none;} |
| .card-top{display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;} |
| .rarity{font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; font-weight:600; |
| color:#fffaf0; background:var(--r); border-radius:999px; padding:.18rem .7rem;} |
| .found{font-size:.82rem; color:var(--ink-soft); font-style:italic;} |
| .cname{font-family:'Fraunces',serif; font-weight:600; font-size:1.9rem; line-height:1.05; margin:.1rem 0;} |
| .ctype{color:var(--forest); font-style:italic; margin-bottom:.6rem;} |
| .card-head{display:flex; gap:14px; align-items:center; margin-bottom:.4rem;} |
| .sigil-wrap{flex:0 0 auto; background:#fffaf0; border:1px solid var(--line); border-radius:14px; padding:5px;} |
| .sigil{display:block;} |
| .mini-sigil{margin-bottom:5px;} |
| .crow{display:flex; gap:10px; margin:.2rem 0; font-size:.96rem;} |
| .crow .lbl{flex:0 0 110px; font-size:.7rem; letter-spacing:.08em; text-transform:uppercase; |
| color:var(--amber-deep); padding-top:.18rem;} |
| .cnote{margin-top:.7rem; padding-top:.7rem; border-top:1px dashed var(--line); line-height:1.5;} |
| |
| /* the collection */ |
| .coll-head{font-family:'Fraunces',serif; font-weight:600; font-size:1.15rem; margin:0 0 10px; |
| display:flex; justify-content:space-between; align-items:baseline; color:var(--ink);} |
| .coll-head .count{font-family:'Spectral',serif; font-size:.85rem; color:var(--ink-soft); font-style:italic;} |
| .coll-empty{color:var(--ink-soft); font-style:italic; padding:10px 2px;} |
| .coll-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px;} |
| .mini{background:#fffaf0; border:1px solid var(--line); border-left:5px solid var(--r); |
| border-radius:12px; padding:9px 11px;} |
| .mini-rar{font-size:.62rem; letter-spacing:.1em; text-transform:uppercase; color:var(--r); font-weight:600;} |
| .mini-name{font-family:'Fraunces',serif; font-weight:600; font-size:1.05rem; line-height:1.1;} |
| .mini-type{font-size:.8rem; color:var(--forest); font-style:italic;} |
| .mini-found{font-size:.74rem; color:var(--ink-soft); margin-top:3px;} |
| .bz-foot{color:var(--ink-soft); font-size:.82rem; font-style:italic; text-align:center; margin-top:10px;} |
| footer{display:none !important;} |
| """ |
|
|
| |
| |
| |
| _blocks_kwargs = {"title": "Pocket Bestiary"} |
| if _BLOCKS_HAS_CSS: |
| _blocks_kwargs["css"] = CSS |
| _blocks_kwargs["theme"] = gr.themes.Soft() |
|
|
| with gr.Blocks(**_blocks_kwargs) as demo: |
| collection = gr.State([]) |
|
|
| public_label = "MiniCPM5-1B · on-device" if MODE == "model" else "Pocket Bestiary" |
| mode_label = f"{public_label} · [{MODE}]" if DEBUG else public_label |
| gr.HTML( |
| f""" |
| <div> |
| <div class="bz-title">Pocket <em>Bestiary</em></div> |
| <div class="bz-sub">Name an ordinary place. Discover the creature hiding in it.</div> |
| <span class="bz-mode">{mode_label}</span> |
| </div> |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=3): |
| with gr.Row(): |
| place = gr.Textbox( |
| placeholder="a place or a thing... (the gap behind the fridge, a bus stop at night)", |
| show_label=False, scale=8, autofocus=True, |
| ) |
| go = gr.Button("Discover", variant="primary", scale=1) |
| with gr.Row(): |
| ex_btns = [gr.Button(e, size="sm") for e in EXAMPLES] |
| card = gr.HTML(render_card(None)) |
| reset = gr.Button("Start a new field guide", size="sm") |
| with gr.Column(scale=2): |
| coll = gr.HTML(render_collection([])) |
|
|
| gr.HTML('<div class="bz-foot">Every small place has something living in it. ' |
| 'Rarer creatures turn up when you least expect them.</div>') |
|
|
| go.click(on_discover, [place, collection], [card, coll, collection, place]) |
| place.submit(on_discover, [place, collection], [card, coll, collection, place]) |
| reset.click(on_reset, None, [card, coll, collection, place]) |
| for b, e in zip(ex_btns, EXAMPLES): |
| b.click(use_example, gr.State(e), place) |
|
|
|
|
| if __name__ == "__main__": |
| _launch_kwargs = {} |
| if not _BLOCKS_HAS_CSS: |
| _launch_kwargs["css"] = CSS |
| _launch_kwargs["theme"] = gr.themes.Soft() |
| if _LAUNCH_HAS_SSR: |
| _launch_kwargs["ssr_mode"] = False |
| demo.queue(max_size=24).launch(**_launch_kwargs) |