.*$", "", t, flags=re.S | re.I) # unclosed think tail
return t.strip()
def _grab(label, text):
# anchor to the start of a line so we never pick up a label named mid-sentence
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 the small model ignored the format, fall back to a seeded creature. Only
# reuse its prose as the note if it actually reads like a note, not reasoning.
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) # fills any missing fields sensibly
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: # noqa: BLE001 -- never crash mid-demo
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
# --------------------------------------------------------------------------- #
# Rendering.
# --------------------------------------------------------------------------- #
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'']
# radial limbs
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'')
parts.append(f'')
# eyes
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'')
return (f'')
def render_card(c):
if not c:
return ('Name a place below, then press '
'Discover to find what lives there.
')
return f"""
{esc(c['rarity'])}
found in {esc(c['found_in'])}
{sigil_svg(c['name'] + c['found_in'], c['rarity_color'])}
{esc(c['name'])}
{esc(c['type'])}
Habitat{esc(c['habitat'])}
Temperament{esc(c['temperament'])}
{esc(c['note'])}
"""
def render_collection(items):
if not items:
return 'Your field guide is empty. Go find something.
'
cells = ""
for c in reversed(items): # newest first
cells += (
f''
f'
{sigil_svg(c["name"] + c["found_in"], c["rarity_color"], size=38)}
'
f'
{esc(c["rarity"])}
'
f'
{esc(c["name"])}
'
f'
{esc(c["type"])}
'
f'
{esc(c["found_in"])}
'
)
return (f'Your field guide '
f'{len(items)} discovered
'
f'{cells}
')
# --------------------------------------------------------------------------- #
# Handlers.
# --------------------------------------------------------------------------- #
def on_discover(user_text, collection):
global _warmed
user_text = (user_text or "").strip()
if not user_text:
msg = ('Tell the Keeper a place first, '
'a real one or a tiny one.
')
yield msg, render_collection(collection), collection, ""
return
if MODE == "model" and not _warmed:
yield ('🔎 The Keeper turns the lens, focusing '
'(the first discovery takes a moment)...
',
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
# --------------------------------------------------------------------------- #
# Custom UI (storybook field-guide; design tokens forced light so it is
# readable regardless of the visitor's browser theme).
# --------------------------------------------------------------------------- #
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;}
"""
# --------------------------------------------------------------------------- #
# Layout.
# --------------------------------------------------------------------------- #
_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"""
Pocket Bestiary
Name an ordinary place. Discover the creature hiding in it.
{mode_label}
"""
)
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('')
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)