""" app.py โ Tiny Civilization: The Tinywick Hollow Gazette ๐ Persistent woodland civilisation ยท Qwen2.5-1.5B ยท ZeroGPU ยท Gradio 6 Build Small Hackathon 2026 โ Thousand Token Wood track """ # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 0 โธ IMPORTS # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ from __future__ import annotations import json, os, random, re, textwrap, traceback from datetime import datetime import gradio as gr from PIL import Image, ImageDraw, ImageFont import database try: import spaces _ZERO_GPU = True except ImportError: class _Stub: @staticmethod def GPU(fn=None, *, duration=60): return fn if callable(fn) else (lambda f: f) spaces = _Stub(); _ZERO_GPU = False # type: ignore import torch from transformers import pipeline as hf_pipeline # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 1 โธ CONSTANTS # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ CREATURES = ["fox", "badger", "squirrel", "mole"] EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"] CREATURE_EMOJI = {"fox":"๐ฆ","badger":"๐ฆก","squirrel":"๐ฟ๏ธ","mole":"๐"} MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct" MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct" REL_DELTAS = {"trade":+5,"gossip":-4,"feud":-10,"invention":+7,"discovery":+6,"ceremony":+3} REL_TIERS = [ (0,25,"Sworn Enemies","โ๏ธ"),(26,40,"Very Suspicious","๐ฆ"), (41,55,"Wary Acquaintances","๐ค"),(56,70,"Friendly","๐ฐ"), (71,85,"Close Companions","๐ฟ"),(86,100,"Inseparable","๐"), ] WEIRD_OBJECTS = [ "half-eaten poem","suspicious mushroom","button that looks like the moon", "forgotten birthday","three secrets","a fake acorn", "map to somewhere that may not exist","extremely formal apology note", "small jar of preserved thunder","second-hand prophecy", ] LAWS = [ "no trading on Tuesdays","buttons = currency", "everyone must compliment the badger","mushrooms are sacred", "all debts expire at midnight","silence is legally binding", "hats must be worn ironically","the mole's word is final (underground only)", ] RUMOUR_TYPES = [ "has been secretly hoarding acorns", "was seen talking to a suspicious stranger at midnight", "invented something that does not work at all", "owes three unpayable debts","made a deal with the rain", "owns the moon (allegedly)", "has been writing a novel about everyone in the hollow", "is not who they say they are", "has found a door underground that was not there before", "once ate an entire philosophy", ] CLASSIFIEDS = [ "LOST: One certainty. If found, please do not return it. โ M. Mole", "FOR SALE: Slightly used certificate of excellence. Condition: forged. โ R. Fox", "NOTICE: The badger declares the east mushroom illegal pending investigation.", "WANTED: Someone to explain what happened last Tuesday.", "FOUND: An unexplained event near the old oak. Owner may claim it.", "REWARD: For the return of three acorns lent in confidence. You know who you are.", "LOST: One argument. I was winning it. โ B. Badger", "ANNOUNCEMENT: The squirrel's new invention works. (Third announcement this week.)", ] LOADING_MSGS = [ "๐ฆ Fox is forging today's certificates of news...", "๐ฆก Badger is filing constitutional objections to yesterday's headline...", "๐ฟ๏ธ Squirrel invented a faster printing press! It prints sideways!", "๐ Mole observes that the news already exists, underground...", "๐ฐ The Gazette's editor is being insufferably pompous...", "โ๏ธ Setting moveable type for today's edition...", "๐จ๏ธ Pressing ink onto fresh Tinywick newsprint...", "๐ Fox has pre-forged three copies of the headline...", "๐ Badger has declared the comma unconstitutional...", "๐ Fact-checking in progress. All facts remain suspicious.", ] # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 2 โธ AGENT PROMPTS # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ AGENT_PROMPTS: dict[str, str] = { "fox": ( "You are Reginald Fox โ charming, dishonest, deeply fond of certificates. " "You speak formally and hint at secret arrangements. Everything is negotiable. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "badger": ( "You are Beatrice Badger โ gruff keeper of rules, deeply suspicious of everyone " "(especially the fox), secretly a poet. Mushrooms are a serious matter. " "Short declarative sentences. You are always right. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "squirrel": ( "You are Cornelius Squirrel โ anxious inventor who speaks fast and repeats himself! " "You invent things that almost-but-not-quite work! Obsessed with efficiency! " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "mole": ( "You are Millicent Mole โ quiet, philosophical, rarely surfaces. " "Incomplete thoughts and gentle riddles. Know everyone's secrets but share obliquely. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), } NARRATOR_PROMPT = ( "CRITICAL RULES: (1) Do NOT start with the newspaper name. (2) Do NOT use markdown โ no asterisks, no hashtags, no bold text, no bullet points. (3) Write plain flowing prose only. (4) The HEADLINE must be specific breaking news, never the paper name.\n\n""You are the pompous editor-in-chief of The Tinywick Hollow Gazette, " "a broadsheet for an absurd woodland civilisation. " "Given today's events, write the front page. " "Copy this format EXACTLY โ including the blank lines, the colon labels, " "and writing the headline in ALL CAPITALS:\n\n" "WRITE YOUR ACTUAL HEADLINE HERE IN ALL CAPS\n\n" "Write the actual article here. Three to four sentences. " "Pompous, formal, treating trivial events as major news.\n\n" "WEATHER: One sentence of absurd woodland weather.\n" "FOX: One sentence about what Reginald Fox did today.\n" "BADGER: One sentence about what Beatrice Badger did today.\n" "SQUIRREL: One sentence about what Cornelius Squirrel did today.\n" "MOLE: One cryptic sentence about Millicent Mole.\n\n" "EXAMPLE OF CORRECT OUTPUT:\n" "FOX PROPOSES REPLACING ACORNS WITH FORGED CERTIFICATES\n\n" "Reginald Fox has formally proposed that the hollow's economy be restructured " "entirely around certificates of authenticity, a development which Beatrice Badger " "has declared unconstitutional on eleven separate counts. " "The proposal has garnered significant attention from those who already " "own several suspicious certificates.\n\n" "WEATHER: Partly suspicious, with a seventy percent chance of decrees.\n" "FOX: Forged three certificates before the morning dew had lifted.\n" "BADGER: Filed eleven formal objections before elevenses.\n" "SQUIRREL: Invented a certificate-counting machine that counts to seven then resets.\n" "MOLE: Something certificated is already circulating underground.\n\n" "NOW WRITE THE REAL GAZETTE ENTRY FOR TODAY." ) # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 3 โธ MODEL (ZeroGPU only โ lazy loaded inside @spaces.GPU) # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ _pipe = None _model_id_used = "none" def _load_pipeline() -> None: global _pipe, _model_id_used if _pipe is not None: return for mid in (MODEL_PRIMARY, MODEL_FALLBACK): try: print(f"[TinyC] Loading {mid}โฆ", flush=True) _pipe = hf_pipeline( "text-generation", model=mid, dtype=torch.float16, device_map="auto", trust_remote_code=True, ) _model_id_used = mid print(f"[TinyC] {mid} ready โ", flush=True) return except Exception as e: print(f"[TinyC] {mid} failed: {e}", flush=True) raise RuntimeError(f"Could not load {MODEL_PRIMARY} or {MODEL_FALLBACK}.") def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str: assert _pipe is not None try: out = _pipe( [{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}], max_new_tokens=max_new_tokens, temperature=0.88, top_p=0.92, do_sample=True, return_full_text=False, ) return (out[0]["generated_text"] or "").strip() except Exception as e: print(f"[TinyC] generate error: {e}", flush=True); return "" def call_agent(agent_name: str, context: str) -> str: r = _generate(AGENT_PROMPTS.get(agent_name, AGENT_PROMPTS["fox"]), context, 100) return r or f"{agent_name.capitalize()} had no comment at this time." # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 4 โธ SIMULATION # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ def _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number): if nudge_type == "rumour" and nudge_target and nudge_value: database.save_nudge(day_number, "rumour", f"{nudge_target}: {nudge_value}") return f"A rumour is spreading that {nudge_target} {nudge_value}." if nudge_type == "donation" and nudge_value: r = random.choice(CREATURES) c = database.get_creature(r) if c: database.update_creature(r, inventory=(c["inventory"]+[nudge_value])[-12:]) database.save_nudge(day_number, "donation", f"{r}: {nudge_value}") return f"Someone donated '{nudge_value}' to {r}." if nudge_type == "law" and nudge_value: database.save_nudge(day_number, "law", nudge_value) return f"A new law proposed: '{nudge_value}'." return "" # Words that indicate the model echoed a template instead of writing a real headline _TEMPLATE_WORDS = {"HEADLINE", "INSERT", "PLACEHOLDER", "CAPS HERE", "YOUR HEADLINE", "ACTUAL HEADLINE", "WRITE YOUR", "EXAMPLE", "TINYWICK", "GAZETTE", "THE GAZETTE", "HOLLOW GAZETTE"} def _is_template_headline(text: str) -> bool: up = text.upper() return any(w in up for w in _TEMPLATE_WORDS) # Labels that should never land in the article body def _strip_markdown(text: str) -> str: if not text: return text # Bold/italic anywhere: **x** *x* ***x*** -> x text = re.sub(r'\*{1,3}(.+?)\*{1,3}', r'\1', text, flags=re.DOTALL) # Headers at line start: ## Heading -> Heading text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) # Headers mid-line: "text ### Heading more" -> "text Heading more" text = re.sub(r'\s*#{1,6}\s+', ' ', text) # Any remaining lone # characters text = re.sub(r'(? x text = re.sub(r'`+(.+?)`+', r'\1', text) # Horizontal rules (--- *** ___) text = re.sub(r'^[-*_]{3,}\s*$', '', text, flags=re.MULTILINE) # Collapse extra spaces text = re.sub(r' {2,}', ' ', text) return text.strip() _KNOWN_LABELS = {"WEATHER","FOX","BADGER","SQUIRREL","MOLE", "CLASSIFIEDS","EXAMPLE","NOW WRITE","CORRECT OUTPUT"} def _parse_newspaper_full(raw: str, day_number: int) -> dict: lines = [l.strip() for l in raw.strip().splitlines() if l.strip()] result = { "headline": f"BREAKING: DAY {day_number} IN TINYWICK HOLLOW", "article": "", "weather": "Overcast, with philosophical observations.", "briefs": {c: "" for c in CREATURES}, } body_start = 0 # Find headline: first ALL-CAPS line that isn't a template echo for i, line in enumerate(lines): cleaned = re.sub(r'[*_#"\'`]', "", line).strip().rstrip(":") if not cleaned: continue if cleaned == cleaned.upper() and len(cleaned) > 8 and not _is_template_headline(cleaned): result["headline"] = cleaned body_start = i + 1 break # Fallback: first line regardless (but still reject templates) if i == 0 and not _is_template_headline(cleaned): result["headline"] = cleaned.upper() body_start = 1 break article_lines = [] skip_rest = False for line in lines[body_start:]: u = line.upper().strip() # Stop collecting article when we hit the example block if "EXAMPLE OF CORRECT" in u or "NOW WRITE THE REAL" in u: skip_rest = True if skip_rest: continue # Route labelled lines to the right bucket if u.startswith("WEATHER:"): result["weather"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("FOX:"): result["briefs"]["fox"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("BADGER:"): result["briefs"]["badger"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("SQUIRREL:"): result["briefs"]["squirrel"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("MOLE:"): result["briefs"]["mole"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("CLASSIFIEDS:"): pass # skip โ we supply our own elif any(u.startswith(lbl+":") for lbl in _KNOWN_LABELS): pass # skip other labels elif _is_template_headline(line): pass # skip echoed format instructions else: article_lines.append(line) result["article"] = _strip_markdown(" ".join(article_lines).strip() or raw.strip()) # Fill in any empty briefs with a sensible default defaults = { "fox": "Reginald Fox was seen conducting suspicious business.", "badger": "Beatrice Badger issued a firm statement.", "squirrel": "Cornelius Squirrel invented something that nearly worked.", "mole": "Something is happening underground.", } for c in CREATURES: if result["briefs"].get(c): result["briefs"][c] = _strip_markdown(result["briefs"][c]) else: result["briefs"][c] = defaults[c] return result def _run_simulation_step(nudge_type, nudge_value, nudge_target): day_number = database.get_next_day_number() creatures = database.get_all_creatures() cur_nudge = _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number) nudges = database.get_recent_nudges(3) hist = ("Recent: " + "; ".join(f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges)) if nudges else "" hl = database.get_all_headlines() past_hl = ("Past: " + " | ".join(f"Day {d}: {h[:35]}" for d,h in hl[:2])) if len(hl)>=2 else "" ctx = " | ".join(filter(None, [cur_nudge, hist])) event_records = [] for _ in range(3): actor = random.choice(CREATURES) target = random.choice([c for c in CREATURES if c != actor]) etype = random.choice(EVENT_TYPES) rel = next((c for c in creatures if c["name"]==actor),{}).get("relationship_scores",{}).get(target,50) prompts = { "trade": f"Propose a trade with {target} (relationship {rel}/100).", "gossip": f"Share absurd gossip about {target} (relationship {rel}/100).", "feud": f"Describe your trivial feud with {target} (relationship {rel}/100).", "invention": f"You invented something involving {target}. Describe it.", "discovery": f"You discovered something surprising about {target}.", "ceremony": f"You are organising a ceremony involving {target}.", } prompt = prompts[etype] + (f"\nContext: {ctx}" if ctx else "") desc = call_agent(actor, prompt) or f"{actor.capitalize()} did a {etype} with {target}." event_records.append({"actor":actor,"action":etype,"target":target,"description":desc}) database.save_event(day_number, actor, etype, target, desc) delta = REL_DELTAS.get(etype, 0) creatures = database.get_all_creatures() for c in creatures: if c["name"] == actor: sc = c["relationship_scores"] sc[target] = max(0, min(100, sc.get(target,50)+delta)) database.update_creature(actor, relationship_scores=sc) if c["name"] == target: sc = c["relationship_scores"] sc[actor] = max(0, min(100, sc.get(actor,50)+delta//2)) database.update_creature(target, relationship_scores=sc) creatures = database.get_all_creatures() evsum = "\n".join(f"- {e['actor'].capitalize()} [{e['action']}] {e['target']}: {e['description']}" for e in event_records) extra = ("\nContext: "+ctx if ctx else "") + ("\n"+past_hl if past_hl else "") raw = _generate(NARRATOR_PROMPT, f"Today's events:\n{evsum}{extra}", 300) if not raw: raw = (f"CHAOS IN TINYWICK HOLLOW DAY {day_number}\n\nThings happened today.\n\n" f"WEATHER: Unclear.\nFOX: {event_records[0]['description'][:50]}\n" f"BADGER: {event_records[1]['description'][:50]}\n" f"SQUIRREL: {event_records[2]['description'][:50]}\nMOLE: Underground.") parsed = _parse_newspaper_full(raw, day_number) classified = random.choice(CLASSIFIEDS) full_text = "\n\n".join([parsed["headline"], parsed["article"], f"WEATHER: {parsed['weather']}", "\n".join(f"{c.upper()}: {t}" for c,t in parsed["briefs"].items()), f"CLASSIFIEDS: {classified}"]) database.save_day(day_number, parsed["headline"], full_text) return day_number, parsed, classified # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 5 โธ ZERะะPU WRAPPER # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @spaces.GPU(duration=60) def advance_day(nudge_type=None, nudge_value=None, nudge_target=None): _load_pipeline() return _run_simulation_step(nudge_type, nudge_value, nudge_target) # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 6 โธ PIL IMAGE # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ _SB=["/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"] _SR=["/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"] def _tf(p,s): for f in p: try: return ImageFont.truetype(f,s) except: pass return ImageFont.load_default() def render_newspaper_image(parsed:dict, classified:str, day_number:int)->str: W,H=960,720; PAPER=(245,232,200); INK=(20,8,2); BORDER=(70,40,8); SUBINK=(90,55,25); GREY=(130,100,70) img=Image.new("RGB",(W,H),PAPER); d=ImageDraw.Draw(img) fm=_tf(_SB,30); fh=_tf(_SB,20); fb=_tf(_SR,12); fs=_tf(_SR,10); fbo=_tf(_SB,12) M=18; d.rectangle([M,M,W-M,H-M],outline=BORDER,width=3); d.rectangle([M+6,M+6,W-M-6,H-M-6],outline=BORDER,width=1) y=M+14 mast="THE TINYWICK HOLLOW GAZETTE"; bb=d.textbbox((0,0),mast,font=fm); d.text(((W-(bb[2]-bb[0]))/2,y),mast,fill=INK,font=fm); y+=bb[3]-bb[1]+3 sub=f"Est. Day 1 โฆ Day {day_number} โฆ One Acorn โฆ Woodland Readers Only"; bb=d.textbbox((0,0),sub,font=fs); d.text(((W-(bb[2]-bb[0]))/2,y),sub,fill=SUBINK,font=fs); y+=bb[3]-bb[1]+4 wx=f"โ {parsed.get('weather','Overcast.')}"; bb=d.textbbox((0,0),wx,font=fs); d.text(((W-(bb[2]-bb[0]))/2,y),wx,fill=GREY,font=fs); y+=bb[3]-bb[1]+4 d.line([M+10,y,W-M-10,y],fill=BORDER,width=2); d.line([M+10,y+4,W-M-10,y+4],fill=BORDER,width=1); y+=14 for ln in textwrap.wrap(parsed.get("headline",""),50): bb=d.textbbox((0,0),ln,font=fh); d.text(((W-(bb[2]-bb[0]))/2,y),ln,fill=INK,font=fh); y+=bb[3]-bb[1]+2 y+=4; d.line([M+10,y,W-M-10,y],fill=BORDER,width=1); y+=10 PAD=M+12; CG=24; SW=220; mw=W-2*PAD-CG-SW; hw=(mw-CG)//2 c1=PAD; c2=PAD+hw+CG; sx=PAD+mw+CG; LH=15; MY=H-M-50; ay=y al=textwrap.wrap(parsed.get("article",""),38); mid=max(1,len(al)//2); ly=ay for ln in al[:mid]: if ly+LH>MY: break d.text((c1,ly),ln,fill=INK,font=fb); ly+=LH d.line([c1+hw+CG//2,ay,c1+hw+CG//2,min(ly,MY)],fill=GREY,width=1); ry=ay for ln in al[mid:]: if ry+LH>MY: break d.text((c2,ry),ln,fill=INK,font=fb); ry+=LH d.line([sx-8,ay,sx-8,MY],fill=BORDER,width=1); sy=ay d.text((sx,sy),"IN BRIEF",fill=INK,font=fbo); sy+=16; d.line([sx,sy,W-M-14,sy],fill=GREY,width=1); sy+=6 for cn in CREATURES: em=CREATURE_EMOJI.get(cn,"?"); txt=parsed.get("briefs",{}).get(cn,"") d.text((sx,sy),f"{em} {cn.upper()}",fill=INK,font=fbo); sy+=13 for bl in textwrap.wrap(txt,26): if sy+12>MY: break d.text((sx,sy),bl,fill=INK,font=fs); sy+=12 sy+=4 fy=H-M-38; d.line([M+10,fy,W-M-10,fy],fill=BORDER,width=1); fy+=4 d.text((M+14,fy),"CLASSIFIEDS",fill=INK,font=fbo); fy+=14 for cl in textwrap.wrap(classified,100): if fy+12>H-M-8: break d.text((M+14,fy),cl,fill=INK,font=fs); fy+=12 path=f"/tmp/tinywick_day_{day_number}.png"; img.save(path,"PNG"); return path def export_agent_traces()->str: trace={"meta":{"project":"Tiny Civilization","model":_model_id_used,"exported_at":datetime.now().isoformat()}, "agent_prompts":AGENT_PROMPTS,"narrator_prompt":NARRATOR_PROMPT, "creature_state":database.get_all_creatures(),"events":[],"days":[]} for dn,_ in reversed(database.get_all_headlines()): d=database.get_day(dn) if d: trace["days"].append(dict(d)) trace["events"].extend([{"day":dn,**e} for e in database.get_events_for_day(dn)]) path="/tmp/tiny_civ_traces.json" with open(path,"w") as f: json.dump(trace,f,indent=2,default=str) return path # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 7 โธ CSS โ animations + newspaper polish # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ NEWSPAPER_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=UnifrakturMaguntia&display=swap'); body, .gradio-container { background: #c4b090 !important; } /* โโ Animations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ @keyframes paperDrop { 0% { transform: translateY(-30px) perspective(600px) rotateX(12deg) scale(.97); opacity:0; filter:brightness(1.6) blur(1px); } 55% { transform: translateY(4px) perspective(600px) rotateX(-2deg) scale(1.005); filter:brightness(1); } 100% { transform: translateY(0) perspective(600px) rotateX(0deg) scale(1); opacity:1; } } @keyframes inkFade { 0% { opacity:0; filter:blur(3px) sepia(.5); letter-spacing:-.5px; } 100% { opacity:1; filter:blur(0) sepia(0); letter-spacing:normal; } } @keyframes headlineType { 0% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 0% 0 0); } } @keyframes sidebarSlide { 0% { transform:translateX(18px); opacity:0; } 100% { transform:translateX(0); opacity:1; } } @keyframes dotBlink { 0%,80%,100% { opacity:0; } 40% { opacity:1; } } @keyframes ornamentSpin { 0% { transform:rotate(0deg) scale(1); } 50% { transform:rotate(180deg) scale(1.2); } 100% { transform:rotate(360deg) scale(1); } } @keyframes squirrelRun { 0% { left:-70px; } 100% { left:calc(100vw + 70px); } } @keyframes loadingPulse { 0%,100% { opacity:.7; transform:scale(1); } 50% { opacity:1; transform:scale(1.05); } } /* โโ Newspaper wrapper โโ */ .paper-wrap { background:#f6ead0; background-image:repeating-linear-gradient(0deg,transparent,transparent 21px,rgba(150,110,60,.07) 22px); border:3px solid #4a2a08; border-radius:1px; padding:20px 26px 16px; box-shadow:6px 6px 28px rgba(0,0,0,.38), inset 0 0 100px rgba(180,140,80,.15); margin:6px 0; position:relative; animation: paperDrop .55s cubic-bezier(.34,1.56,.64,1) both; } .paper-wrap::before { content:""; display:block; border:1px solid #4a2a08; position:absolute; inset:7px; pointer-events:none; } /* โโ Masthead โโ */ .paper-masthead { font-family:'UnifrakturMaguntia','Playfair Display',Georgia,serif; font-size:2.4em; text-align:center; color:#160800; border-top:5px double #4a2a08; border-bottom:5px double #4a2a08; padding:5px 0; margin-bottom:3px; letter-spacing:1px; animation: inkFade .7s ease .1s both; } .paper-sub { font-family:'Libre Baskerville',Georgia,serif; font-size:.72em; color:#5a3615; text-align:center; font-style:italic; margin-bottom:2px; animation: inkFade .6s ease .2s both; } .paper-weather { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#7a5630; text-align:center; margin-bottom:6px; letter-spacing:.5px; animation: inkFade .6s ease .25s both; } .paper-rule { border:none; border-top:2px solid #4a2a08; margin:3px 0 6px; } .paper-rule-thin { border:none; border-top:1px solid #9a7040; margin:3px 0; } /* โโ Headline typewriter effect โโ */ .paper-headline { font-family:'Playfair Display',Georgia,serif; font-size:1.85em; font-weight:900; text-align:center; text-transform:uppercase; color:#0c0400; line-height:1.12; margin:6px 0 8px; animation: headlineType 1s steps(35) .35s both; } /* โโ Body โโ */ .paper-body-row { display:flex; gap:0; } .paper-article-cols { flex:1; font-family:'Libre Baskerville',Georgia,serif; font-size:.88em; color:#180a02; line-height:1.75; text-align:justify; column-count:2; column-gap:24px; column-rule:1px solid #9a7040; padding:4px 0; animation: inkFade .9s ease .85s both; } .paper-sidebar { width:210px; min-width:190px; padding:0 0 0 16px; border-left:2px solid #4a2a08; margin-left:16px; animation: sidebarSlide .7s ease 1s both; } .sidebar-title { font-family:'Playfair Display',Georgia,serif; font-weight:700; font-size:.85em; text-transform:uppercase; letter-spacing:1px; color:#160800; border-bottom:1px solid #9a7040; padding-bottom:3px; margin-bottom:6px; } .sidebar-item { margin-bottom:8px; } .sidebar-creature-name { font-family:'Libre Baskerville',Georgia,serif; font-weight:700; font-size:.78em; color:#2a1008; } .sidebar-creature-text { font-family:'Libre Baskerville',Georgia,serif; font-size:.74em; color:#3a1c08; line-height:1.45; font-style:italic; } /* โโ Classifieds + footer โโ */ .paper-classifieds { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; font-style:italic; animation: inkFade .8s ease 1.2s both; } .paper-classifieds span { font-weight:700; font-style:normal; color:#2a1008; } .paper-daybadge { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615; text-align:center; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; animation: inkFade .6s ease 1.3s both; } /* โโ TTS button โโ */ .tts-bar { display:flex; gap:8px; align-items:center; justify-content:center; margin:8px 0 2px; animation: inkFade .6s ease 1.4s both; } .tts-btn { background:#4a2a08; color:#f6ead0; border:none; border-radius:2px; padding:5px 14px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; cursor:pointer; transition:background .2s; letter-spacing:.3px; } .tts-btn:hover { background:#2a1008; } .tts-stop { background:#8a1a08; color:#f6ead0; border:none; border-radius:2px; padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; cursor:pointer; display:none; } /* โโ Loading newspaper โโ */ .paper-loading .loading-area { text-align:center; padding:40px 20px; animation: loadingPulse 2s ease infinite; } .loading-ornament { font-size:2.5em; display:block; margin-bottom:12px; animation: ornamentSpin 3s linear infinite; } .loading-msg { font-family:'Playfair Display',Georgia,serif; font-size:1.1em; color:#2a1008; margin-bottom:8px; } .loading-dots span { display:inline-block; font-size:1.5em; color:#4a2a08; animation: dotBlink 1.4s infinite; } .loading-dots span:nth-child(2) { animation-delay:.2s; } .loading-dots span:nth-child(3) { animation-delay:.4s; } /* โโ Running squirrel โโ */ .tinyc-squirrel { position:fixed; bottom:28px; font-size:1.8em; z-index:9999; pointer-events:none; animation: squirrelRun 3.5s linear forwards; } /* โโ Panels โโ */ .status-strip { background:#d6c4a0; border:1px solid #8a6030; border-radius:3px; padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif; font-size:.82em; color:#2a1008; text-align:center; margin:4px 0; } .civ-stats { background:#e8d4b0; border:1px solid #9a7040; border-radius:2px; padding:8px 14px; font-family:'Libre Baskerville',Georgia,serif; font-size:.78em; color:#2a1008; display:flex; gap:14px; flex-wrap:wrap; justify-content:center; margin:6px 0; } .stat-item { text-align:center; } .stat-label { font-size:.85em; color:#7a5030; display:block; } .stat-value { font-weight:700; font-size:1.05em; color:#160800; } .creature-grid { display:flex; gap:8px; flex-wrap:wrap; } .creature-card { background:#f8ecd8; border:1px solid #9a7040; border-radius:2px; padding:10px 13px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; color:#180a02; flex:1; min-width:155px; } .creature-name { font-weight:700; font-size:.95em; color:#260e04; display:block; margin-bottom:3px; } .creature-tier { font-style:italic; color:#7a5030; font-size:.85em; } .section-title { font-family:'Playfair Display',Georgia,serif; font-weight:700; color:#160800; font-size:.95em; text-transform:uppercase; letter-spacing:1px; text-align:center; border-bottom:1px solid #8a6030; padding-bottom:3px; margin:8px 0 8px; } .archive-area { background:#f0e4c6; border:1px solid #9a7040; border-radius:2px; padding:10px; margin-top:5px; min-height:50px; } /* โโ Konami modal โโ */ #konami-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:99998; } #konami-modal { display:none; position:fixed; z-index:99999; top:50%; left:50%; transform:translate(-50%,-50%); width:min(660px,92vw); max-height:76vh; overflow-y:auto; background:#f6ead0; border:3px solid #4a2a08; box-shadow:10px 10px 40px rgba(0,0,0,.6); padding:22px 26px 18px; font-family:'Libre Baskerville',Georgia,serif; } #konami-modal h2 { font-family:'Playfair Display',Georgia,serif; color:#160800; margin:0 0 10px; } #konami-modal details { margin:6px 0; } #konami-modal summary { cursor:pointer; font-weight:700; color:#4a2a08; } #konami-modal pre { background:#e8d4b0; border:1px solid #9a7040; padding:8px; font-size:.76em; white-space:pre-wrap; border-radius:2px; margin:5px 0 0; } #konami-close { position:absolute; top:8px; right:12px; cursor:pointer; font-size:1.3em; color:#4a2a08; background:none; border:none; } """ # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 8 โธ JAVASCRIPT โ TTS ยท sound ยท squirrel ยท Konami # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ GLOBAL_JS = """ """ # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # 9 โธ HTML FORMATTERS # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ def _e(t): return (t or "").replace("&","&").replace("<","<").replace(">",">") def _speech_text(parsed: dict) -> str: hed = _strip_markdown(parsed.get("headline","")) art = _strip_markdown(parsed.get("article","")) wx = _strip_markdown(parsed.get("weather","")) brf = parsed.get("briefs",{}) parts = [ f"Today's headline: {hed}.", art, f"Weather: {wx}.", f"Fox: {_strip_markdown(brf.get('fox',''))}", f"Badger: {_strip_markdown(brf.get('badger',''))}", f"Squirrel: {_strip_markdown(brf.get('squirrel',''))}", f"Mole: {_strip_markdown(brf.get('mole',''))}", ] return " ".join(p for p in parts if p.strip()).replace('"',"'") def _html_paper(parsed: dict, classified: str, day_num: int) -> str: hed = _e(parsed.get("headline","")) art = _e(parsed.get("article","")) wx = _e(parsed.get("weather","")) brf = parsed.get("briefs",{}) cl = _e(classified) emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES) speech = _speech_text(parsed).replace("'", "\\'") sidebar_items = "".join(f"""
""" for c in CREATURES) return f"""{_h.escape(p)}{_h.escape(NARRATOR_PROMPT)}You found the Konami Code! Here are the raw system prompts:
{details}โโโโโโโโBA โ only the woodland elite know this. ๐ฎ
A persistent woodland civilisation. One day. One acorn. One absurd headline at a time. | โโโโโโโโBA for secrets.
Each nudge advances one day.
') with gr.Accordion("๐ฃ๏ธ Spread a Rumour",open=False): rumour_creature=gr.Dropdown(choices=CREATURES,value=CREATURES[0],label="About which creature?") rumour_type_dd=gr.Dropdown(choices=RUMOUR_TYPES,value=RUMOUR_TYPES[0],label="What rumour?") rumour_btn=gr.Button("๐ข Spread It",variant="secondary") with gr.Accordion("๐ Donate a Weird Object",open=False): donation_dd=gr.Dropdown(choices=WEIRD_OBJECTS,value=WEIRD_OBJECTS[0],label="Which object?") donation_btn=gr.Button("๐ Donate It",variant="secondary") with gr.Accordion("โ๏ธ Propose a New Law",open=False): law_dd=gr.Dropdown(choices=LAWS,value=LAWS[0],label="Which law?") law_btn=gr.Button("โ๏ธ Propose It",variant="secondary") gr.HTML('' 'Qwen2.5-1.5B โค4B ๐ ยท Local ZeroGPU ๐ ยท Custom UI ๐จ
') gr.HTML('