nishtha711 commited on
Commit
08ddaa0
·
verified ·
1 Parent(s): 7917293

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +691 -499
app.py CHANGED
@@ -4,7 +4,6 @@ Persistent woodland civilisation simulation powered by Qwen2.5-1.5B.
4
  Build Small Hackathon 2026 — Thousand Token Wood track.
5
  Model: Qwen/Qwen2.5-1.5B-Instruct (≤4B → Tiny Titan badge!)
6
  """
7
-
8
  # ═══════════════════════════════════════════════════════════════════
9
  # 0 ▸ IMPORTS
10
  # ═══════════════════════════════════════════════════════════════════
@@ -12,24 +11,19 @@ from __future__ import annotations
12
  import json, os, random, textwrap, traceback, re
13
  from datetime import datetime
14
  from pathlib import Path
15
- os.environ["GRADIO_SSR_MODE"] = "False"
16
  import gradio as gr
17
  from PIL import Image, ImageDraw, ImageFont
18
  import database
19
 
20
  try:
21
  import spaces
22
-
23
  _ZERO_GPU = True
24
  except ImportError:
25
-
26
  class _Stub:
27
  @staticmethod
28
  def GPU(fn=None, *, duration=120):
29
  return fn if callable(fn) else (lambda f: f)
30
-
31
- spaces = _Stub()
32
- _ZERO_GPU = False # type: ignore
33
 
34
  import torch
35
  from transformers import pipeline as hf_pipeline
@@ -37,43 +31,30 @@ from transformers import pipeline as hf_pipeline
37
  # ═══════════════════════════════════════════════════════════════════
38
  # 1 ▸ CONSTANTS
39
  # ═══════════════════════════════════════════════════════════════════
40
- CREATURES = ["fox", "badger", "squirrel", "mole"]
41
- EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"]
42
- CREATURE_EMOJI = {"fox": "🦊", "badger": "🦡", "squirrel": "🐿️", "mole": "🐀"}
43
 
44
  # Model — 1.5B primary → 3B fallback. BOTH qualify for ≤4B Tiny Titan badge!
45
- MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct"
46
  MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct"
47
 
48
- REL_DELTAS = {
49
- "trade": +5,
50
- "gossip": -4,
51
- "feud": -10,
52
- "invention": +7,
53
- "discovery": +6,
54
- "ceremony": +3,
55
- }
56
 
57
  REL_TIERS = [
58
- (0, 25, "Sworn Enemies", "⚔️"),
59
- (26, 40, "Very Suspicious", "🦔"),
60
- (41, 55, "Wary Acquaintances", "🤝"),
61
- (56, 70, "Friendly", "🌰"),
62
- (71, 85, "Close Companions", "🌿"),
63
- (86, 100, "Inseparable", "🍄"),
64
  ]
65
 
66
  WEIRD_OBJECTS = [
67
- "half-eaten poem",
68
- "suspicious mushroom",
69
- "button that looks like the moon",
70
- "forgotten birthday",
71
- "three secrets",
72
- "a fake acorn",
73
- "map to somewhere that may not exist",
74
- "extremely formal apology note",
75
- "small jar of preserved thunder",
76
- "second-hand prophecy",
77
  ]
78
 
79
  LAWS = [
@@ -154,32 +135,323 @@ NARRATOR_PROMPT = (
154
  "MOLE: One cryptic sentence."
155
  )
156
 
157
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  import os as _os
159
  import requests as _req
160
  from huggingface_hub import InferenceClient as _InfClient
161
 
162
- _MODAL_URL = _os.getenv("MODAL_INFERENCE_URL", "").strip()
163
- _HF_TOKEN = _os.getenv("HF_TOKEN", "").strip()
164
 
165
- _USE_MODAL = False
166
  _USE_HF_API = bool(_HF_TOKEN) and not _USE_MODAL
167
- _USE_LOCAL = not _USE_MODAL and not _USE_HF_API
168
 
169
- _hf_client = (
170
- _InfClient(provider="hf-inference", api_key=_HF_TOKEN) if _USE_HF_API else None
171
- )
172
  _local_pipe = None
173
- _model_id_used = "none"
174
-
175
- _BACKEND = (
176
- "Modal"
177
- if _USE_MODAL
178
- else "HF Inference API [no quota!]"
179
- if _USE_HF_API
180
- else "Local ZeroGPU [set HF_TOKEN to remove quota limit]"
181
- )
182
- print(f"[TinyC] Backend: {_BACKEND}", flush=True)
183
 
184
 
185
  def _load_local_pipeline() -> None:
@@ -188,79 +460,65 @@ def _load_local_pipeline() -> None:
188
  return
189
  for mid in (MODEL_PRIMARY, MODEL_FALLBACK):
190
  try:
191
- print(f"[TinyC] Loading {mid} ...", flush=True)
192
  _local_pipe = hf_pipeline(
193
- "text-generation",
194
- model=mid,
195
  dtype=torch.float16,
196
- device_map="auto",
197
  trust_remote_code=True,
198
  )
199
  _model_id_used = mid
200
- print(f"[TinyC] {mid} ready", flush=True)
201
  return
202
  except Exception as e:
203
- print(f"[TinyC] {mid} failed: {e}", flush=True)
204
- raise RuntimeError(f"Could not load {MODEL_PRIMARY} or {MODEL_FALLBACK}.")
205
 
206
 
207
  def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str:
208
  messages = [
209
- {"role": "system", "content": system_prompt},
210
- {"role": "user", "content": user_prompt},
211
  ]
212
  if _USE_MODAL:
213
  try:
214
- r = _req.post(
215
- _MODAL_URL,
216
- json={
217
- "messages": messages,
218
- "max_new_tokens": max_new_tokens,
219
- "temperature": 0.88,
220
- "top_p": 0.92,
221
- },
222
- timeout=90,
223
- )
224
  r.raise_for_status()
225
  d = r.json()
226
- return (d.get("text") or d.get("generated_text") or "").strip()
227
  except Exception as e:
228
- print(f"[TinyC] Modal error: {e}", flush=True)
229
- return ""
230
  if _USE_HF_API:
231
- try:
232
- resp = _hf_client.chat.completions.create(
233
- model=MODEL_PRIMARY,
234
- messages=messages,
235
- max_tokens=max_new_tokens,
236
- temperature=0.88,
237
- top_p=0.92,
238
- )
239
- return (resp.choices[0].message.content or "").strip()
240
- except Exception as e:
241
- print(f"[TinyC] HF API error: {e}", flush=True)
242
- return ""
 
 
 
243
  assert _local_pipe is not None
244
  try:
245
- out = _local_pipe(
246
- messages,
247
- max_new_tokens=max_new_tokens,
248
- temperature=0.88,
249
- top_p=0.92,
250
- do_sample=True,
251
- return_full_text=False,
252
- )
253
- return (out[0]["generated_text"] or "").strip()
254
  except Exception as e:
255
- print(f"[TinyC] local pipe error: {e}", flush=True)
256
- return ""
257
 
258
 
259
  def call_agent(agent_name: str, context: str) -> str:
260
- result = _generate(
261
- AGENT_PROMPTS.get(agent_name, AGENT_PROMPTS["fox"]), context, 100
262
- )
263
- return result or f"{agent_name.capitalize()} had no comment at this time."
264
 
265
 
266
  # ═══════════════════════════════════════════════════════════════════
@@ -286,19 +544,15 @@ def _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number):
286
 
287
  def _historical_ctx() -> str:
288
  nudges = database.get_recent_nudges(3)
289
- if not nudges:
290
- return ""
291
- lines = [
292
- f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges
293
- ]
294
  return "Recent influences: " + "; ".join(lines)
295
 
296
 
297
  def _past_headline_ctx() -> str:
298
  """Include last 2 headlines so newspaper can reference history."""
299
  hl = database.get_all_headlines()
300
- if len(hl) < 2:
301
- return ""
302
  recent = hl[:2]
303
  return "Past headlines: " + " | ".join(f"Day {d}: {h[:40]}" for d, h in recent)
304
 
@@ -351,49 +605,38 @@ def _parse_newspaper_full(raw: str, day_number: int) -> dict:
351
  def _run_simulation_step(nudge_type, nudge_value, nudge_target):
352
  """Core simulation — backend-agnostic."""
353
  day_number = database.get_next_day_number()
354
- creatures = database.get_all_creatures()
355
 
356
  current_nudge = _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number)
357
- historical = _historical_ctx()
358
- past_hl = _past_headline_ctx()
359
- combined_ctx = " | ".join(filter(None, [current_nudge, historical]))
360
 
361
  # ── Generate 3 events ─────────────────────────────────────────
362
  event_records: list[dict] = []
363
  for _ in range(3):
364
- actor = random.choice(CREATURES)
365
  target = random.choice([c for c in CREATURES if c != actor])
366
- etype = random.choice(EVENT_TYPES)
367
- rel = (
368
- next((c for c in creatures if c["name"] == actor), {})
369
- .get("relationship_scores", {})
370
- .get(target, 50)
371
- )
372
 
373
  prompts = {
374
- "trade": f"Propose a trade with {target} (relationship {rel}/100). Be specific about what you're offering.",
375
- "gossip": f"Share gossip about {target} (relationship {rel}/100). Make it wonderfully absurd.",
376
- "feud": f"Describe your current feud with {target} (relationship {rel}/100). It must be about something trivial.",
377
  "invention": f"You've invented something that involves {target} somehow. Describe your invention.",
378
  "discovery": f"You've discovered something surprising about {target} or near them. What did you find?",
379
- "ceremony": f"You're organising a ceremony and {target} must be involved. Describe it.",
380
  }
381
  agent_prompt = prompts[etype]
382
- if combined_ctx:
383
- agent_prompt += f"\n\nWorld context: {combined_ctx}"
384
 
385
  description = call_agent(actor, agent_prompt)
386
  if not description or len(description) < 8:
387
  description = f"{actor.capitalize()} had a {etype} with {target}."
388
 
389
- event_records.append(
390
- {
391
- "actor": actor,
392
- "action": etype,
393
- "target": target,
394
- "description": description,
395
- }
396
- )
397
  database.save_event(day_number, actor, etype, target, description)
398
 
399
  # ── Update relationships ──────────────────────────────────
@@ -402,11 +645,11 @@ def _run_simulation_step(nudge_type, nudge_value, nudge_target):
402
  for c in creatures:
403
  if c["name"] == actor:
404
  sc = c["relationship_scores"]
405
- sc[target] = max(0, min(100, sc.get(target, 50) + delta))
406
  database.update_creature(actor, relationship_scores=sc)
407
  if c["name"] == target:
408
  sc = c["relationship_scores"]
409
- sc[actor] = max(0, min(100, sc.get(actor, 50) + delta // 2))
410
  database.update_creature(target, relationship_scores=sc)
411
  creatures = database.get_all_creatures()
412
 
@@ -416,10 +659,8 @@ def _run_simulation_step(nudge_type, nudge_value, nudge_target):
416
  for e in event_records
417
  )
418
  extra = ""
419
- if combined_ctx:
420
- extra += f"\nWorld context: {combined_ctx}"
421
- if past_hl:
422
- extra += f"\n{past_hl}"
423
 
424
  raw_paper = _generate(
425
  NARRATOR_PROMPT,
@@ -441,28 +682,157 @@ def _run_simulation_step(nudge_type, nudge_value, nudge_target):
441
  parsed = _parse_newspaper_full(raw_paper, day_number)
442
  classified = random.choice(CLASSIFIEDS)
443
 
444
- full_text = "\n\n".join(
445
- [
446
- parsed["headline"],
447
- parsed["article"],
448
- f"WEATHER: {parsed['weather']}",
449
- "\n".join(f"{c.upper()}: {t}" for c, t in parsed["briefs"].items()),
450
- f"CLASSIFIEDS: {classified}",
451
- ]
452
- )
453
 
454
  database.save_day(day_number, parsed["headline"], full_text)
455
  return day_number, parsed, classified
456
 
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  if _USE_LOCAL:
459
-
460
  @spaces.GPU(duration=60)
461
  def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
462
  _load_local_pipeline()
463
  return _run_simulation_step(nudge_type, nudge_value, nudge_target)
464
  else:
465
-
466
  def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
467
  return _run_simulation_step(nudge_type, nudge_value, nudge_target)
468
 
@@ -470,151 +840,98 @@ else:
470
  # ═══════════════════════════════════════════════════════════════════
471
  # 6 ▸ PIL NEWSPAPER IMAGE (enhanced)
472
  # ═══════════════════════════════════════════════════════════════════
473
- _SERIF_B = [
474
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
475
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
476
- ]
477
- _SERIF_R = [
478
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
479
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
480
- ]
481
-
482
 
483
  def _tf(paths, sz):
484
  for p in paths:
485
- try:
486
- return ImageFont.truetype(p, sz)
487
- except:
488
- pass
489
  return ImageFont.load_default()
490
 
491
-
492
  def render_newspaper_image(parsed: dict, classified: str, day_number: int) -> str:
493
  W, H = 960, 720
494
- PAPER = (245, 232, 200)
495
- INK = (20, 8, 2)
496
- BORDER = (70, 40, 8)
497
- SUBINK = (90, 55, 25)
498
- GREY = (130, 100, 70)
499
-
500
- img = Image.new("RGB", (W, H), PAPER)
501
- d = ImageDraw.Draw(img)
502
-
503
- f_mast = _tf(_SERIF_B, 30)
504
- f_hed = _tf(_SERIF_B, 20)
505
- f_body = _tf(_SERIF_R, 12)
506
- f_sm = _tf(_SERIF_R, 10)
507
- f_sub = _tf(_SERIF_R, 11)
508
- f_bold = _tf(_SERIF_B, 12)
509
 
510
  M = 18
511
- d.rectangle([M, M, W - M, H - M], outline=BORDER, width=3)
512
- d.rectangle([M + 6, M + 6, W - M - 6, H - M - 6], outline=BORDER, width=1)
513
 
514
  y = M + 14
515
  # Masthead
516
  mast = "THE TINYWICK HOLLOW GAZETTE"
517
- bb = d.textbbox((0, 0), mast, font=f_mast)
518
- tw = bb[2] - bb[0]
519
- d.text(((W - tw) / 2, y), mast, fill=INK, font=f_mast)
520
- y += bb[3] - bb[1] + 3
521
  # Sub
522
  sub = f"Est. Day 1 ✦ Day {day_number} ✦ One Acorn ✦ Woodland Readers Only"
523
- bb = d.textbbox((0, 0), sub, font=f_sm)
524
- d.text(((W - (bb[2] - bb[0])) / 2, y), sub, fill=SUBINK, font=f_sm)
525
- y += bb[3] - bb[1] + 5
526
  # Weather strip
527
- wx = parsed.get("weather", "Overcast.")
528
  wx_line = f"☁ WEATHER: {wx}"
529
- bb = d.textbbox((0, 0), wx_line, font=f_sm)
530
- d.text(((W - (bb[2] - bb[0])) / 2, y), wx_line, fill=GREY, font=f_sm)
531
- y += bb[3] - bb[1] + 4
532
- d.line([M + 10, y, W - M - 10, y], fill=BORDER, width=2)
533
- d.line([M + 10, y + 4, W - M - 10, y + 4], fill=BORDER, width=1)
534
- y += 14
535
 
536
  # Headline
537
- for line in textwrap.wrap(parsed.get("headline", ""), width=50):
538
- bb = d.textbbox((0, 0), line, font=f_hed)
539
- d.text(((W - (bb[2] - bb[0])) / 2, y), line, fill=INK, font=f_hed)
540
- y += bb[3] - bb[1] + 2
541
- y += 4
542
- d.line([M + 10, y, W - M - 10, y], fill=BORDER, width=1)
543
- y += 10
544
 
545
  # Two-column article + sidebar
546
- PAD = M + 12
547
- COL_GAP = 24
548
- SIDE_W = 220
549
- main_w = W - 2 * PAD - COL_GAP - SIDE_W
550
- half_w = (main_w - COL_GAP) // 2
551
- col1_x = PAD
552
- col2_x = PAD + half_w + COL_GAP
553
- side_x = PAD + main_w + COL_GAP
554
- LH = 15
555
- MAX_Y = H - M - 50
556
- art_y = y
557
-
558
- art_lines = textwrap.wrap(parsed.get("article", ""), width=38)
559
- mid = max(1, len(art_lines) // 2)
560
- ly = art_y
561
  for line in art_lines[:mid]:
562
- if ly + LH > MAX_Y:
563
- break
564
- d.text((col1_x, ly), line, fill=INK, font=f_body)
565
- ly += LH
566
- d.line(
567
- [
568
- col1_x + half_w + COL_GAP // 2,
569
- art_y,
570
- col1_x + half_w + COL_GAP // 2,
571
- min(ly, MAX_Y),
572
- ],
573
- fill=GREY,
574
- width=1,
575
- )
576
- ry = art_y
577
  for line in art_lines[mid:]:
578
- if ry + LH > MAX_Y:
579
- break
580
- d.text((col2_x, ry), line, fill=INK, font=f_body)
581
- ry += LH
582
 
583
  # Sidebar: In Brief
584
- d.line([side_x - 8, art_y, side_x - 8, MAX_Y], fill=BORDER, width=1)
585
- sy = art_y
586
- d.text((side_x, sy), "IN BRIEF", fill=INK, font=f_bold)
587
- sy += 16
588
- d.line([side_x, sy, W - M - 14, sy], fill=GREY, width=1)
589
- sy += 6
590
- briefs = parsed.get("briefs", {})
591
  for cname in CREATURES:
592
- em = CREATURE_EMOJI.get(cname, "?")
593
- txt = briefs.get(cname, "")
594
- header = f"{em} {cname.upper()}"
595
- d.text((side_x, sy), header, fill=INK, font=f_bold)
596
- sy += 13
597
- for bline in textwrap.wrap(txt, width=26):
598
- if sy + 12 > MAX_Y:
599
- break
600
- d.text((side_x, sy), bline, fill=INK, font=f_sm)
601
- sy += 12
602
- sy += 4
603
 
604
  # Classifieds footer
605
- fy = H - M - 38
606
- d.line([M + 10, fy, W - M - 10, fy], fill=BORDER, width=1)
607
- fy += 4
608
- d.text((M + 14, fy), "CLASSIFIEDS", fill=INK, font=f_bold)
609
- fy += 14
610
- for cl in textwrap.wrap(classified, width=100):
611
- if fy + 12 > H - M - 8:
612
- break
613
- d.text((M + 14, fy), cl, fill=INK, font=f_sm)
614
- fy += 12
615
-
616
- path = f"/tmp/tinywick_day_{day_number}.png"
617
- img.save(path, "PNG")
618
  return path
619
 
620
 
@@ -639,13 +956,11 @@ def export_agent_traces() -> str:
639
  headlines = database.get_all_headlines()
640
  for dn, _ in reversed(headlines):
641
  day = database.get_day(dn)
642
- if day:
643
- trace["days"].append(dict(day))
644
  evts = database.get_events_for_day(dn)
645
  trace["events"].extend([{"day": dn, **e} for e in evts])
646
  path = "/tmp/tiny_civ_traces.json"
647
- with open(path, "w") as f:
648
- json.dump(trace, f, indent=2, default=str)
649
  return path
650
 
651
 
@@ -815,23 +1130,21 @@ KONAMI_JS = """<script>
815
  # ═══════════════════════════════════════════════════════════════════
816
  def _rel_tier(score: int) -> tuple[str, str]:
817
  for lo, hi, label, icon in REL_TIERS:
818
- if lo <= score <= hi:
819
- return label, icon
820
  return "Unknown", "?"
821
 
822
-
823
  def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
824
- hed = (parsed.get("headline", "") or "").replace("<", "&lt;")
825
- art = (parsed.get("article", "") or "").replace("<", "&lt;")
826
- wx = (parsed.get("weather", "") or "").replace("<", "&lt;")
827
- briefs = parsed.get("briefs", {})
828
  emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES)
829
- classified_esc = classified.replace("<", "&lt;")
830
 
831
  sidebar_items = ""
832
  for c in CREATURES:
833
- em = CREATURE_EMOJI.get(c, "?")
834
- txt = (briefs.get(c, "") or "").replace("<", "&lt;")
835
  sidebar_items += f"""
836
  <div class="sidebar-item">
837
  <div class="sidebar-creature-name">{em} {c.upper()}</div>
@@ -858,24 +1171,21 @@ def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
858
  </div>
859
  """
860
 
861
-
862
  def _html_placeholder() -> str:
863
  founding = database.get_day(0)
864
  if founding:
865
- text = founding["full_newspaper_text"]
866
- parts = text.split("\n\n", 1)
867
- hed = parts[0]
868
- art = parts[1] if len(parts) > 1 else text
869
  dummy_parsed = {
870
- "headline": hed,
871
- "article": art,
872
  "weather": "Portentous, with scattered significance.",
873
  "briefs": {
874
  "fox": "Forged three certificates before breakfast.",
875
  "badger": "Insisted on thirteen amendments before lunch.",
876
  "squirrel": "Invented a signing machine! It signed the wrong document!",
877
  "mole": "Something is already happening underground.",
878
- },
879
  }
880
  return _html_paper(dummy_parsed, "NOTICE: Civilisation now in progress.", 0)
881
  return """<div class="paper-wrap">
@@ -887,36 +1197,31 @@ def _html_placeholder() -> str:
887
  <div class="paper-daybadge">— Day 0 —</div>
888
  </div>"""
889
 
890
-
891
  def _html_creatures() -> str:
892
  creatures = database.get_all_creatures()
893
  cards = ""
894
  for c in creatures:
895
- em = CREATURE_EMOJI.get(c["name"], "?")
896
- inv = (
897
- ", ".join(c["inventory"][:3]) + ("…" if len(c["inventory"]) > 3 else "")
898
- ) or "nothing"
899
  rels = ""
900
  for other, score in sorted(c["relationship_scores"].items()):
901
  label, icon = _rel_tier(score)
902
- rels += f'<span title="{label} ({score})">{CREATURE_EMOJI.get(other, "?")} {icon}</span> '
903
  cards += f"""<div class="creature-card">
904
- <span class="creature-name">{em} {c["name"].capitalize()}</span>
905
  <div class="creature-tier">{rels}</div>
906
  <div style="font-size:.78em;margin-top:3px;color:#5a3615;"><em>Carries:</em> {inv}</div>
907
  </div>"""
908
  return f'<div class="creature-grid">{cards}</div>'
909
 
910
-
911
  def _html_civ_stats() -> str:
912
  s = database.get_civ_stats()
913
- bp = s["best_pair"]
914
- wp = s["worst_pair"]
915
  dom = s["dominant"].capitalize()
916
  return f"""<div class="civ-stats">
917
- <div class="stat-item"><span class="stat-label">Days</span><span class="stat-value">{s["total_days"]}</span></div>
918
- <div class="stat-item"><span class="stat-label">Events</span><span class="stat-value">{s["total_events"]}</span></div>
919
- <div class="stat-item"><span class="stat-label">Nudges</span><span class="stat-value">{s["total_nudges"]}</span></div>
920
  <div class="stat-item"><span class="stat-label">Strongest bond</span>
921
  <span class="stat-value">{bp[0].capitalize()} &amp; {bp[1].capitalize()} ({bp[2]})</span></div>
922
  <div class="stat-item"><span class="stat-label">Bitterest feud</span>
@@ -924,25 +1229,18 @@ def _html_civ_stats() -> str:
924
  <div class="stat-item"><span class="stat-label">Most popular</span><span class="stat-value">{dom}</span></div>
925
  </div>"""
926
 
927
-
928
  def _archive_choices():
929
  hl = database.get_all_headlines()
930
- return [
931
- (f"Day {dn}: {h[:44]}{'…' if len(h) > 44 else ''}", dn) for dn, h in hl
932
- ] or []
933
-
934
-
935
- def _status(msg):
936
- return f'<div class="status-strip">{msg}</div>'
937
 
 
938
 
939
  def _konami_html() -> str:
940
  import html as _h
941
-
942
  details = "".join(
943
- f"<details><summary>{CREATURE_EMOJI.get(n, '')}<strong> {n.upper()}</strong></summary>"
944
  f"<pre>{_h.escape(p)}</pre></details>"
945
- for n, p in AGENT_PROMPTS.items()
946
  )
947
  details += f"<details><summary>📰 <strong>NARRATOR</strong></summary><pre>{_h.escape(NARRATOR_PROMPT)}</pre></details>"
948
  return f"""
@@ -967,95 +1265,58 @@ def _render_all(day_num, parsed, classified, status_msg):
967
  _html_civ_stats(),
968
  gr.update(choices=_archive_choices(), value=None),
969
  _status(status_msg),
970
- day_num,
971
- parsed,
972
- classified,
973
  )
974
 
975
-
976
  def _safe_advance(nudge_type=None, nudge_value=None, nudge_target=None):
977
  try:
978
  day_num, parsed, classified = advance_day(nudge_type, nudge_value, nudge_target)
979
- model_tag = (
980
- f" [{_model_id_used.split('/')[-1]}]" if _model_id_used != "none" else ""
981
- )
982
- return _render_all(
983
- day_num,
984
- parsed,
985
- classified,
986
- f"✓ Day {day_num} published to the Gazette.{model_tag}",
987
- )
988
  except Exception:
989
  tb = traceback.format_exc()
990
  print(tb)
991
  err_parsed = {
992
  "headline": "THE GAZETTE'S PRINTING PRESS HAS JAMMED",
993
  "article": "Our correspondents report a technical malfunction. The editor is inconsolable. "
994
- "Beatrice Badger suspects sabotage. Reginald Fox denies everything.",
995
  "weather": "Stormy, with a chance of errors.",
996
  "briefs": {c: "Unavailable for comment." for c in CREATURES},
997
  }
998
- return _render_all(
999
- 0,
1000
- err_parsed,
1001
- "LOST: One functioning simulation. — The Editor",
1002
- "✗ Simulation error — the printing press is jammed. Check logs.",
1003
- )
1004
-
1005
-
1006
- def handle_advance():
1007
- return _safe_advance()
1008
-
1009
-
1010
- def handle_rumour(c, r):
1011
- return _safe_advance("rumour", r, c)
1012
-
1013
-
1014
- def handle_donation(o):
1015
- return _safe_advance("donation", o)
1016
-
1017
-
1018
- def handle_law(l):
1019
- return _safe_advance("law", l)
1020
 
 
 
 
 
1021
 
1022
  def handle_archive_view(day_num):
1023
  if day_num is None:
1024
- return (
1025
- '<div class="archive-area"><em>Select a day to read its edition.</em></div>'
1026
- )
1027
  day = database.get_day(int(day_num))
1028
- if not day:
1029
- return '<div class="archive-area"><em>Edition not found.</em></div>'
1030
- txt = day["full_newspaper_text"]
1031
- parts = txt.split("\n\n", 1)
1032
- hed = parts[0]
1033
- art = parts[1] if len(parts) > 1 else txt
1034
  # Reconstruct parsed from stored text
1035
  parsed = _parse_newspaper_full(txt, day_num)
1036
  classified_match = re.search(r"CLASSIFIEDS:\s*(.+)", txt)
1037
  cl = classified_match.group(1) if classified_match else random.choice(CLASSIFIEDS)
1038
  return f'<div class="archive-area">{_html_paper(parsed, cl, day_num)}</div>'
1039
 
1040
-
1041
  def handle_share(day_num_state, parsed_state, classified_state):
1042
- if not parsed_state or not parsed_state.get("headline"):
1043
- return gr.update(visible=False)
1044
  try:
1045
- path = render_newspaper_image(
1046
- parsed_state, classified_state or "", day_num_state or 0
1047
- )
1048
  return gr.update(visible=True, value=path)
1049
- except Exception:
1050
- return gr.update(visible=False)
1051
-
1052
 
1053
  def handle_export_traces():
1054
  try:
1055
  path = export_agent_traces()
1056
  return gr.update(visible=True, value=path)
1057
- except Exception:
1058
- return gr.update(visible=False)
1059
 
1060
 
1061
  # =================================================================
@@ -1066,177 +1327,108 @@ def handle_export_traces():
1066
  database.init_db()
1067
  _latest = database.get_latest_day()
1068
  if _latest:
1069
- _INIT_DAY = _latest["day_number"]
1070
  _INIT_PARSED = _parse_newspaper_full(_latest["full_newspaper_text"], _INIT_DAY)
1071
- _cl_m = re.search(r"CLASSIFIEDS:\s*(.+)", _latest["full_newspaper_text"])
1072
- _INIT_CL = _cl_m.group(1) if _cl_m else random.choice(CLASSIFIEDS)
1073
  else:
1074
- _INIT_DAY = 0
1075
- _INIT_PARSED = {}
1076
- _INIT_CL = ""
1077
 
1078
  # -- Gradio version-aware css/theme routing -----------------------
1079
  _GR_MAJOR = int(gr.__version__.split(".")[0])
1080
- if _GR_MAJOR >= 6:
1081
- _BKW: dict = {}
1082
- _LKW: dict = {"css": NEWSPAPER_CSS}
1083
- elif _GR_MAJOR >= 5:
1084
- _BKW = {"css": NEWSPAPER_CSS}
1085
- _LKW = {}
1086
- else:
1087
- _BKW = {
1088
- "css": NEWSPAPER_CSS,
1089
- "theme": gr.themes.Base(primary_hue="orange", neutral_hue="stone"),
1090
- }
1091
- _LKW = {}
1092
 
1093
  # ------------------------------------------------------------------
1094
  with gr.Blocks(title="Tiny Civilization — The Tinywick Hollow Gazette", **_BKW) as demo:
 
1095
  gr.HTML(_konami_html())
1096
 
1097
  gr.HTML(
1098
  '<div style="text-align:center;padding:8px 0 2px;">'
1099
- "<h1 style=\"font-family:'Playfair Display',Georgia,serif;color:#160800;font-size:1.9em;margin:0 0 2px;\">"
1100
- "🦊 Tiny Civilization 🐀</h1>"
1101
  '<p style="font-family:Georgia,serif;color:#5a3615;font-style:italic;margin:0;font-size:.87em;">'
1102
- "A persistent woodland civilisation. One day. One acorn. One absurd headline at a time."
1103
  '&nbsp;|&nbsp;<kbd title="Konami Code">↑↑↓↓←→←→BA</kbd> for secrets.'
1104
- "</p></div>"
1105
  )
1106
 
1107
- day_state = gr.State(_INIT_DAY)
1108
- parsed_state = gr.State(_INIT_PARSED)
1109
  classif_state = gr.State(_INIT_CL)
1110
 
1111
- status_html = gr.HTML(
1112
- value=_status(
1113
- f"Day {_INIT_DAY} in the archive next: Day {_INIT_DAY + 1}."
1114
- if _INIT_DAY >= 0
1115
- else "No days yet. Press Advance Day to begin."
1116
- )
1117
- )
1118
  civ_stats_html = gr.HTML(value=_html_civ_stats())
1119
 
1120
  with gr.Row(equal_height=False):
 
1121
  with gr.Column(scale=3):
1122
  newspaper_display = gr.HTML(
1123
- value=(
1124
- _html_paper(_INIT_PARSED, _INIT_CL, _INIT_DAY)
1125
- if _INIT_PARSED
1126
- else _html_placeholder()
1127
- )
1128
- )
1129
 
1130
- with gr.Accordion("📜 Archive — Past Editions", open=False):
1131
  archive_dd = gr.Dropdown(
1132
- choices=_archive_choices(),
1133
- value=None,
1134
- label="Select a past day",
1135
- container=False,
1136
- )
1137
  archive_display = gr.HTML(
1138
- value='<div class="archive-area"><em>Select a day to read its edition.</em></div>'
1139
- )
1140
 
1141
  with gr.Column(scale=1, min_width=260):
1142
  gr.HTML('<div class="section-title">📰 Editorial Desk</div>')
1143
- advance_btn = gr.Button(
1144
- "📅 Advance Day (no nudge)", variant="primary", size="lg"
1145
- )
1146
 
1147
  gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
1148
  gr.HTML('<div class="section-title">✉ Nudge the Story</div>')
1149
- gr.HTML(
1150
- '<p style="font-size:.78em;color:#5a3615;text-align:center;'
1151
- 'font-style:italic;margin:0 0 6px;">Each nudge advances one day.</p>'
1152
- )
 
 
 
 
 
 
 
 
 
 
1153
 
1154
- with gr.Accordion("🗣Spread a Rumour", open=False):
1155
- rumour_creature = gr.Dropdown(
1156
- choices=CREATURES, value=CREATURES[0], label="About which creature?"
1157
- )
1158
- rumour_type_dd = gr.Dropdown(
1159
- choices=RUMOUR_TYPES, value=RUMOUR_TYPES[0], label="What rumour?"
1160
- )
1161
- rumour_btn = gr.Button("📢 Spread It", variant="secondary")
1162
-
1163
- with gr.Accordion("🎁 Donate a Weird Object", open=False):
1164
- donation_dd = gr.Dropdown(
1165
- choices=WEIRD_OBJECTS, value=WEIRD_OBJECTS[0], label="Which object?"
1166
- )
1167
- donation_btn = gr.Button("🎁 Donate It", variant="secondary")
1168
-
1169
- with gr.Accordion("⚖️ Propose a New Law", open=False):
1170
- law_dd = gr.Dropdown(choices=LAWS, value=LAWS[0], label="Which law?")
1171
- law_btn = gr.Button("⚖️ Propose It", variant="secondary")
1172
 
1173
  gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
1174
- share_btn = gr.Button("🖼️ Share as Image", variant="secondary")
1175
- img_output = gr.Image(
1176
- label="Front Page PNG", visible=False, type="filepath"
1177
- )
1178
- export_btn = gr.Button("📡 Export Agent Traces (JSON)", variant="secondary")
1179
- trace_output = gr.File(label="Agent Traces JSON", visible=False)
1180
 
1181
- gr.HTML(
1182
- '<p style="font-size:.70em;color:#6a4818;text-align:center;'
1183
- 'margin-top:8px;font-style:italic;">'
1184
- "Model: Qwen2.5-1.5B ≤4B 🐜&nbsp;|&nbsp;Local only 🔌&nbsp;|&nbsp;Custom UI 🎨</p>"
1185
- )
1186
 
1187
- gr.HTML(
1188
- '<div class="section-title" style="margin-top:12px;">Woodland Residents</div>'
1189
- )
1190
  creature_display = gr.HTML(value=_html_creatures())
1191
 
1192
- _OUT = [
1193
- newspaper_display,
1194
- creature_display,
1195
- civ_stats_html,
1196
- archive_dd,
1197
- status_html,
1198
- day_state,
1199
- parsed_state,
1200
- classif_state,
1201
- ]
1202
 
1203
- advance_btn.click(fn=handle_advance, inputs=[], outputs=_OUT, api_name=False)
1204
- rumour_btn.click(
1205
- fn=handle_rumour,
1206
- inputs=[rumour_creature, rumour_type_dd],
1207
- outputs=_OUT,
1208
- api_name=False,
1209
- )
1210
- donation_btn.click(
1211
- fn=handle_donation, inputs=[donation_dd], outputs=_OUT, api_name=False
1212
- )
1213
- law_btn.click(fn=handle_law, inputs=[law_dd], outputs=_OUT, api_name=False)
1214
- archive_dd.change(
1215
- fn=handle_archive_view,
1216
- inputs=[archive_dd],
1217
- outputs=[archive_display],
1218
- api_name=False,
1219
- )
1220
- share_btn.click(
1221
- fn=handle_share,
1222
- inputs=[day_state, parsed_state, classif_state],
1223
- outputs=[img_output],
1224
- api_name=False,
1225
- )
1226
- export_btn.click(
1227
- fn=handle_export_traces, inputs=[], outputs=[trace_output], api_name=False
1228
- )
1229
 
1230
  # ═══════════════════════════════════════════════════════════════════
1231
  # 13 ▸ ENTRY POINT
1232
  # ═══════════════════════════════════════════════════════════════════
1233
  if __name__ == "__main__":
1234
- import inspect
1235
-
1236
- _launch_kwargs = {"server_name": "0.0.0.0", "server_port": 7860, **_LKW}
1237
- _launch_params = inspect.signature(gr.Blocks.launch).parameters
1238
- if "ssr_mode" in _launch_params:
1239
- _launch_kwargs["ssr_mode"] = False
1240
- elif "ssr" in _launch_params:
1241
- _launch_kwargs["ssr"] = False
1242
- demo.launch(**_launch_kwargs)
 
4
  Build Small Hackathon 2026 — Thousand Token Wood track.
5
  Model: Qwen/Qwen2.5-1.5B-Instruct (≤4B → Tiny Titan badge!)
6
  """
 
7
  # ═══════════════════════════════════════════════════════════════════
8
  # 0 ▸ IMPORTS
9
  # ═══════════════════════════════════════════════════════════════════
 
11
  import json, os, random, textwrap, traceback, re
12
  from datetime import datetime
13
  from pathlib import Path
 
14
  import gradio as gr
15
  from PIL import Image, ImageDraw, ImageFont
16
  import database
17
 
18
  try:
19
  import spaces
 
20
  _ZERO_GPU = True
21
  except ImportError:
 
22
  class _Stub:
23
  @staticmethod
24
  def GPU(fn=None, *, duration=120):
25
  return fn if callable(fn) else (lambda f: f)
26
+ spaces = _Stub(); _ZERO_GPU = False # type: ignore
 
 
27
 
28
  import torch
29
  from transformers import pipeline as hf_pipeline
 
31
  # ═══════════════════════════════════════════════════════════════════
32
  # 1 ▸ CONSTANTS
33
  # ═══════════════════════════════════════════════════════════════════
34
+ CREATURES = ["fox", "badger", "squirrel", "mole"]
35
+ EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"]
36
+ CREATURE_EMOJI = {"fox":"🦊","badger":"🦡","squirrel":"🐿️","mole":"🐀"}
37
 
38
  # Model — 1.5B primary → 3B fallback. BOTH qualify for ≤4B Tiny Titan badge!
39
+ MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct"
40
  MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct"
41
 
42
+ REL_DELTAS = {"trade":+5,"gossip":-4,"feud":-10,"invention":+7,"discovery":+6,"ceremony":+3}
 
 
 
 
 
 
 
43
 
44
  REL_TIERS = [
45
+ (0, 25, "Sworn Enemies", "⚔️"),
46
+ (26, 40, "Very Suspicious", "🦔"),
47
+ (41, 55, "Wary Acquaintances", "🤝"),
48
+ (56, 70, "Friendly", "🌰"),
49
+ (71, 85, "Close Companions", "🌿"),
50
+ (86, 100, "Inseparable", "🍄"),
51
  ]
52
 
53
  WEIRD_OBJECTS = [
54
+ "half-eaten poem", "suspicious mushroom", "button that looks like the moon",
55
+ "forgotten birthday", "three secrets", "a fake acorn",
56
+ "map to somewhere that may not exist", "extremely formal apology note",
57
+ "small jar of preserved thunder", "second-hand prophecy",
 
 
 
 
 
 
58
  ]
59
 
60
  LAWS = [
 
135
  "MOLE: One cryptic sentence."
136
  )
137
 
138
+ # ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═
139
+ # 3 ▸ INFERENCE BACKEND
140
+ # ─
141
+ # 3 ▸ INFERENCE BACKEND
142
+ # ─
143
+ # 3 ▸ INFERENCE BACKEND
144
+ # ─
145
+ # 3 ▸ INFERENCE BACKEND
146
+ # ─
147
+ # 3 ▸ INFERENCE BACKEND
148
+ # ─
149
+ # 3 ▸ INFERENCE BACKEND
150
+ # ─
151
+ # 3 ▸ INFERENCE BACKEND
152
+ # ─
153
+ # 3 ▸ INFERENCE BACKEND
154
+ # ─
155
+ # 3 ▸ INFERENCE BACKEND
156
+ # ─
157
+ # 3 ▸ INFERENCE BACKEND
158
+ # ─
159
+ # 3 ▸ INFERENCE BACKEND
160
+ # ─
161
+ # 3 ▸ INFERENCE BACKEND
162
+ # ─
163
+ # 3 ▸ INFERENCE BACKEND
164
+ # ─
165
+ # 3 ▸ INFERENCE BACKEND
166
+ # ─
167
+ # 3 ▸ INFERENCE BACKEND
168
+ # ─
169
+ # 3 ▸ INFERENCE BACKEND
170
+ # ─
171
+ # 3 ▸ INFERENCE BACKEND
172
+ # ─
173
+ # 3 ▸ INFERENCE BACKEND
174
+ # ─
175
+ # 3 ▸ INFERENCE BACKEND
176
+ # ─
177
+ # 3 ▸ INFERENCE BACKEND
178
+ # ─
179
+ # 3 ▸ INFERENCE BACKEND
180
+ # ─
181
+ # 3 ▸ INFERENCE BACKEND
182
+ # ─
183
+ # 3 ▸ INFERENCE BACKEND
184
+ # ─
185
+ # 3 ▸ INFERENCE BACKEND
186
+ # ─
187
+ # 3 ▸ INFERENCE BACKEND
188
+ # ─
189
+ # 3 ▸ INFERENCE BACKEND
190
+ # ─
191
+ # 3 ▸ INFERENCE BACKEND
192
+ # ─
193
+ # 3 ▸ INFERENCE BACKEND
194
+ # ─
195
+ # 3 ▸ INFERENCE BACKEND
196
+ # ─
197
+ # 3 ▸ INFERENCE BACKEND
198
+ # ─
199
+ # 3 ▸ INFERENCE BACKEND
200
+ # ─
201
+ # 3 ▸ INFERENCE BACKEND
202
+ # ─
203
+ # 3 ▸ INFERENCE BACKEND
204
+ # ─
205
+ # Priority: HF API → Modal → Local ZeroGPU
206
+ #
207
+ # INSTANT FIX for quota errors:
208
+ # Space Settings → Variables & Secrets → New Secret
209
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
210
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
211
+ # ═
212
+ # Priority: HF API → Modal → Local ZeroGPU
213
+ #
214
+ # INSTANT FIX for quota errors:
215
+ # Space Settings → Variables & Secrets → New Secret
216
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
217
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
218
+ # ═
219
+ # Priority: HF API → Modal → Local ZeroGPU
220
+ #
221
+ # INSTANT FIX for quota errors:
222
+ # Space Settings → Variables & Secrets → New Secret
223
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
224
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
225
+ # ═
226
+ # Priority: HF API → Modal → Local ZeroGPU
227
+ #
228
+ # INSTANT FIX for quota errors:
229
+ # Space Settings → Variables & Secrets → New Secret
230
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
231
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
232
+ # ═
233
+ # Priority: HF API → Modal → Local ZeroGPU
234
+ #
235
+ # INSTANT FIX for quota errors:
236
+ # Space Settings → Variables & Secrets → New Secret
237
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
238
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
239
+ # ═
240
+ # Priority: HF API → Modal → Local ZeroGPU
241
+ #
242
+ # INSTANT FIX for quota errors:
243
+ # Space Settings → Variables & Secrets → New Secret
244
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
245
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
246
+ # ═
247
+ # Priority: HF API → Modal → Local ZeroGPU
248
+ #
249
+ # INSTANT FIX for quota errors:
250
+ # Space Settings → Variables & Secrets → New Secret
251
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
252
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
253
+ # ═
254
+ # Priority: HF API → Modal → Local ZeroGPU
255
+ #
256
+ # INSTANT FIX for quota errors:
257
+ # Space Settings → Variables & Secrets → New Secret
258
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
259
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
260
+ # ═
261
+ # Priority: HF API → Modal → Local ZeroGPU
262
+ #
263
+ # INSTANT FIX for quota errors:
264
+ # Space Settings → Variables & Secrets → New Secret
265
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
266
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
267
+ # ═
268
+ # Priority: HF API → Modal → Local ZeroGPU
269
+ #
270
+ # INSTANT FIX for quota errors:
271
+ # Space Settings → Variables & Secrets → New Secret
272
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
273
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
274
+ # ═
275
+ # Priority: HF API → Modal → Local ZeroGPU
276
+ #
277
+ # INSTANT FIX for quota errors:
278
+ # Space Settings → Variables & Secrets → New Secret
279
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
280
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
281
+ # ═
282
+ # Priority: HF API → Modal → Local ZeroGPU
283
+ #
284
+ # INSTANT FIX for quota errors:
285
+ # Space Settings → Variables & Secrets → New Secret
286
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
287
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
288
+ # ═
289
+ # Priority: HF API → Modal → Local ZeroGPU
290
+ #
291
+ # INSTANT FIX for quota errors:
292
+ # Space Settings → Variables & Secrets → New Secret
293
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
294
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
295
+ # ═
296
+ # Priority: HF API → Modal → Local ZeroGPU
297
+ #
298
+ # INSTANT FIX for quota errors:
299
+ # Space Settings → Variables & Secrets → New Secret
300
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
301
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
302
+ # ═
303
+ # Priority: HF API → Modal → Local ZeroGPU
304
+ #
305
+ # INSTANT FIX for quota errors:
306
+ # Space Settings → Variables & Secrets → New Secret
307
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
308
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
309
+ # ═
310
+ # Priority: HF API → Modal → Local ZeroGPU
311
+ #
312
+ # INSTANT FIX for quota errors:
313
+ # Space Settings → Variables & Secrets → New Secret
314
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
315
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
316
+ # ═
317
+ # Priority: HF API → Modal → Local ZeroGPU
318
+ #
319
+ # INSTANT FIX for quota errors:
320
+ # Space Settings → Variables & Secrets → New Secret
321
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
322
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
323
+ # ═
324
+ # Priority: HF API → Modal → Local ZeroGPU
325
+ #
326
+ # INSTANT FIX for quota errors:
327
+ # Space Settings → Variables & Secrets → New Secret
328
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
329
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
330
+ # ═
331
+ # Priority: HF API → Modal → Local ZeroGPU
332
+ #
333
+ # INSTANT FIX for quota errors:
334
+ # Space Settings → Variables & Secrets → New Secret
335
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
336
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
337
+ # ═
338
+ # Priority: HF API → Modal → Local ZeroGPU
339
+ #
340
+ # INSTANT FIX for quota errors:
341
+ # Space Settings → Variables & Secrets → New Secret
342
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
343
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
344
+ # ═
345
+ # Priority: HF API → Modal → Local ZeroGPU
346
+ #
347
+ # INSTANT FIX for quota errors:
348
+ # Space Settings → Variables & Secrets → New Secret
349
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
350
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
351
+ # ═
352
+ # Priority: HF API → Modal → Local ZeroGPU
353
+ #
354
+ # INSTANT FIX for quota errors:
355
+ # Space Settings → Variables & Secrets → New Secret
356
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
357
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
358
+ # ═
359
+ # Priority: HF API → Modal → Local ZeroGPU
360
+ #
361
+ # INSTANT FIX for quota errors:
362
+ # Space Settings → Variables & Secrets → New Secret
363
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
364
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
365
+ # ═
366
+ # Priority: HF API → Modal → Local ZeroGPU
367
+ #
368
+ # INSTANT FIX for quota errors:
369
+ # Space Settings → Variables & Secrets → New Secret
370
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
371
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
372
+ # ═
373
+ # Priority: HF API → Modal → Local ZeroGPU
374
+ #
375
+ # INSTANT FIX for quota errors:
376
+ # Space Settings → Variables & Secrets → New Secret
377
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
378
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
379
+ # ═
380
+ # Priority: HF API → Modal → Local ZeroGPU
381
+ #
382
+ # INSTANT FIX for quota errors:
383
+ # Space Settings → Variables & Secrets → New Secret
384
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
385
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
386
+ # ═
387
+ # Priority: HF API → Modal → Local ZeroGPU
388
+ #
389
+ # INSTANT FIX for quota errors:
390
+ # Space Settings → Variables & Secrets → New Secret
391
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
392
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
393
+ # ═
394
+ # Priority: HF API → Modal → Local ZeroGPU
395
+ #
396
+ # INSTANT FIX for quota errors:
397
+ # Space Settings → Variables & Secrets → New Secret
398
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
399
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
400
+ # ═
401
+ # Priority: HF API → Modal → Local ZeroGPU
402
+ #
403
+ # INSTANT FIX for quota errors:
404
+ # Space Settings → Variables & Secrets → New Secret
405
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
406
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
407
+ # ═
408
+ # Priority: HF API → Modal → Local ZeroGPU
409
+ #
410
+ # INSTANT FIX for quota errors:
411
+ # Space Settings → Variables & Secrets → New Secret
412
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
413
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
414
+ # ═
415
+ # Priority: HF API → Modal → Local ZeroGPU
416
+ #
417
+ # INSTANT FIX for quota errors:
418
+ # Space Settings → Variables & Secrets → New Secret
419
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
420
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
421
+ # ═
422
+ # Priority: HF API → Modal → Local ZeroGPU
423
+ #
424
+ # INSTANT FIX for quota errors:
425
+ # Space Settings → Variables & Secrets → New Secret
426
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
427
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
428
+ # ═
429
+ # Priority: HF API → Modal → Local ZeroGPU
430
+ #
431
+ # INSTANT FIX for quota errors:
432
+ # Space Settings → Variables & Secrets → New Secret
433
+ # Name: HF_TOKEN Value: hf_xxxx (huggingface.co/settings/tokens)
434
+ # This switches to HF serverless inference — zero ZeroGPU quota consumed.
435
+ # ═
436
  import os as _os
437
  import requests as _req
438
  from huggingface_hub import InferenceClient as _InfClient
439
 
440
+ _MODAL_URL = (_os.getenv('MODAL_INFERENCE_URL') or _os.getenv('MODAL_URL') or '').strip()
441
+ _HF_TOKEN = _os.getenv('HF_TOKEN', '').strip()
442
 
443
+ _USE_MODAL = bool(_MODAL_URL)
444
  _USE_HF_API = bool(_HF_TOKEN) and not _USE_MODAL
445
+ _USE_LOCAL = not _USE_MODAL and not _USE_HF_API
446
 
447
+ _hf_client = _InfClient(token=_HF_TOKEN) if _USE_HF_API else None
 
 
448
  _local_pipe = None
449
+ _model_id_used = 'none'
450
+
451
+ _BACKEND = ('Modal' if _USE_MODAL else
452
+ 'HF Inference API [no quota!]' if _USE_HF_API else
453
+ 'Local ZeroGPU [set HF_TOKEN to remove quota limit]')
454
+ print(f'[TinyC] Backend: {_BACKEND}', flush=True)
 
 
 
 
455
 
456
 
457
  def _load_local_pipeline() -> None:
 
460
  return
461
  for mid in (MODEL_PRIMARY, MODEL_FALLBACK):
462
  try:
463
+ print(f'[TinyC] Loading {mid} ...', flush=True)
464
  _local_pipe = hf_pipeline(
465
+ 'text-generation', model=mid,
 
466
  dtype=torch.float16,
467
+ device_map='auto',
468
  trust_remote_code=True,
469
  )
470
  _model_id_used = mid
471
+ print(f'[TinyC] {mid} ready', flush=True)
472
  return
473
  except Exception as e:
474
+ print(f'[TinyC] {mid} failed: {e}', flush=True)
475
+ raise RuntimeError(f'Could not load {MODEL_PRIMARY} or {MODEL_FALLBACK}.')
476
 
477
 
478
  def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str:
479
  messages = [
480
+ {'role': 'system', 'content': system_prompt},
481
+ {'role': 'user', 'content': user_prompt},
482
  ]
483
  if _USE_MODAL:
484
  try:
485
+ r = _req.post(_MODAL_URL, json={'messages': messages,
486
+ 'max_new_tokens': max_new_tokens, 'temperature': 0.88, 'top_p': 0.92},
487
+ timeout=90)
 
 
 
 
 
 
 
488
  r.raise_for_status()
489
  d = r.json()
490
+ return (d.get('text') or d.get('generated_text') or '').strip()
491
  except Exception as e:
492
+ print(f'[TinyC] Modal error: {e}', flush=True); return ''
 
493
  if _USE_HF_API:
494
+ for _attempt in range(2):
495
+ try:
496
+ if _attempt == 0: # newer OpenAI-compat path
497
+ resp = _hf_client.chat.completions.create(
498
+ model=MODEL_PRIMARY, messages=messages,
499
+ max_tokens=max_new_tokens, temperature=0.88, top_p=0.92)
500
+ else: # older path for huggingface_hub < 0.31
501
+ resp = _hf_client.chat_completion(
502
+ model=MODEL_PRIMARY, messages=messages,
503
+ max_tokens=max_new_tokens, temperature=0.88, top_p=0.92)
504
+ return (resp.choices[0].message.content or '').strip()
505
+ except AttributeError:
506
+ continue # try the other API
507
+ except Exception as e:
508
+ print(f'[TinyC] HF API error: {e}', flush=True); return ''
509
  assert _local_pipe is not None
510
  try:
511
+ out = _local_pipe(messages, max_new_tokens=max_new_tokens,
512
+ temperature=0.88, top_p=0.92, do_sample=True,
513
+ return_full_text=False)
514
+ return (out[0]['generated_text'] or '').strip()
 
 
 
 
 
515
  except Exception as e:
516
+ print(f'[TinyC] local pipe error: {e}', flush=True); return ''
 
517
 
518
 
519
  def call_agent(agent_name: str, context: str) -> str:
520
+ result = _generate(AGENT_PROMPTS.get(agent_name, AGENT_PROMPTS['fox']), context, 100)
521
+ return result or f'{agent_name.capitalize()} had no comment at this time.'
 
 
522
 
523
 
524
  # ═══════════════════════════════════════════════════════════════════
 
544
 
545
  def _historical_ctx() -> str:
546
  nudges = database.get_recent_nudges(3)
547
+ if not nudges: return ""
548
+ lines = [f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges]
 
 
 
549
  return "Recent influences: " + "; ".join(lines)
550
 
551
 
552
  def _past_headline_ctx() -> str:
553
  """Include last 2 headlines so newspaper can reference history."""
554
  hl = database.get_all_headlines()
555
+ if len(hl) < 2: return ""
 
556
  recent = hl[:2]
557
  return "Past headlines: " + " | ".join(f"Day {d}: {h[:40]}" for d, h in recent)
558
 
 
605
  def _run_simulation_step(nudge_type, nudge_value, nudge_target):
606
  """Core simulation — backend-agnostic."""
607
  day_number = database.get_next_day_number()
608
+ creatures = database.get_all_creatures()
609
 
610
  current_nudge = _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number)
611
+ historical = _historical_ctx()
612
+ past_hl = _past_headline_ctx()
613
+ combined_ctx = " | ".join(filter(None, [current_nudge, historical]))
614
 
615
  # ── Generate 3 events ─────────────────────────────────────────
616
  event_records: list[dict] = []
617
  for _ in range(3):
618
+ actor = random.choice(CREATURES)
619
  target = random.choice([c for c in CREATURES if c != actor])
620
+ etype = random.choice(EVENT_TYPES)
621
+ rel = next((c for c in creatures if c["name"]==actor), {}).get(
622
+ "relationship_scores", {}).get(target, 50)
 
 
 
623
 
624
  prompts = {
625
+ "trade": f"Propose a trade with {target} (relationship {rel}/100). Be specific about what you're offering.",
626
+ "gossip": f"Share gossip about {target} (relationship {rel}/100). Make it wonderfully absurd.",
627
+ "feud": f"Describe your current feud with {target} (relationship {rel}/100). It must be about something trivial.",
628
  "invention": f"You've invented something that involves {target} somehow. Describe your invention.",
629
  "discovery": f"You've discovered something surprising about {target} or near them. What did you find?",
630
+ "ceremony": f"You're organising a ceremony and {target} must be involved. Describe it.",
631
  }
632
  agent_prompt = prompts[etype]
633
+ if combined_ctx: agent_prompt += f"\n\nWorld context: {combined_ctx}"
 
634
 
635
  description = call_agent(actor, agent_prompt)
636
  if not description or len(description) < 8:
637
  description = f"{actor.capitalize()} had a {etype} with {target}."
638
 
639
+ event_records.append({"actor":actor,"action":etype,"target":target,"description":description})
 
 
 
 
 
 
 
640
  database.save_event(day_number, actor, etype, target, description)
641
 
642
  # ── Update relationships ──────────────────────────────────
 
645
  for c in creatures:
646
  if c["name"] == actor:
647
  sc = c["relationship_scores"]
648
+ sc[target] = max(0, min(100, sc.get(target,50) + delta))
649
  database.update_creature(actor, relationship_scores=sc)
650
  if c["name"] == target:
651
  sc = c["relationship_scores"]
652
+ sc[actor] = max(0, min(100, sc.get(actor,50) + delta//2))
653
  database.update_creature(target, relationship_scores=sc)
654
  creatures = database.get_all_creatures()
655
 
 
659
  for e in event_records
660
  )
661
  extra = ""
662
+ if combined_ctx: extra += f"\nWorld context: {combined_ctx}"
663
+ if past_hl: extra += f"\n{past_hl}"
 
 
664
 
665
  raw_paper = _generate(
666
  NARRATOR_PROMPT,
 
682
  parsed = _parse_newspaper_full(raw_paper, day_number)
683
  classified = random.choice(CLASSIFIEDS)
684
 
685
+ full_text = "\n\n".join([
686
+ parsed["headline"],
687
+ parsed["article"],
688
+ f"WEATHER: {parsed['weather']}",
689
+ "\n".join(f"{c.upper()}: {t}" for c, t in parsed["briefs"].items()),
690
+ f"CLASSIFIEDS: {classified}",
691
+ ])
 
 
692
 
693
  database.save_day(day_number, parsed["headline"], full_text)
694
  return day_number, parsed, classified
695
 
696
 
697
+ # ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═# ═
698
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
699
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
700
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
701
+ # ═
702
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
703
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
704
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
705
+ # ═
706
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
707
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
708
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
709
+ # ═
710
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
711
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
712
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
713
+ # ═
714
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
715
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
716
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
717
+ # ═
718
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
719
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
720
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
721
+ # ═
722
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
723
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
724
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
725
+ # ═
726
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
727
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
728
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
729
+ # ═
730
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
731
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
732
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
733
+ # ═
734
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
735
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
736
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
737
+ # ═
738
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
739
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
740
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
741
+ # ═
742
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
743
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
744
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
745
+ # ═
746
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
747
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
748
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
749
+ # ═
750
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
751
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
752
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
753
+ # ═
754
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
755
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
756
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
757
+ # ═
758
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
759
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
760
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
761
+ # ═
762
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
763
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
764
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
765
+ # ═
766
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
767
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
768
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
769
+ # ═
770
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
771
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
772
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
773
+ # ═
774
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
775
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
776
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
777
+ # ═
778
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
779
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
780
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
781
+ # ═
782
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
783
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
784
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
785
+ # ═
786
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
787
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
788
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
789
+ # ═
790
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
791
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
792
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
793
+ # ═
794
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
795
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
796
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
797
+ # ═
798
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
799
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
800
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
801
+ # ═
802
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
803
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
804
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
805
+ # ═
806
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
807
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
808
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
809
+ # ═
810
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
811
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
812
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
813
+ # ═
814
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
815
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
816
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
817
+ # ═
818
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
819
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
820
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
821
+ # ═
822
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
823
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
824
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
825
+ # ═
826
+ # 5 ▸ ADVANCE DAY (GPU-decorated only if local backend active)
827
+ # ZeroGPU bills duration + ~60s warm-up. duration=60 → ~120s billed.
828
+ # With HF_TOKEN set, @spaces.GPU is never called. Zero quota used.
829
+ # ═
830
  if _USE_LOCAL:
 
831
  @spaces.GPU(duration=60)
832
  def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
833
  _load_local_pipeline()
834
  return _run_simulation_step(nudge_type, nudge_value, nudge_target)
835
  else:
 
836
  def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
837
  return _run_simulation_step(nudge_type, nudge_value, nudge_target)
838
 
 
840
  # ═══════════════════════════════════════════════════════════════════
841
  # 6 ▸ PIL NEWSPAPER IMAGE (enhanced)
842
  # ═══════════════════════════════════════════════════════════════════
843
+ _SERIF_B = ["/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
844
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"]
845
+ _SERIF_R = ["/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
846
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"]
 
 
 
 
 
847
 
848
  def _tf(paths, sz):
849
  for p in paths:
850
+ try: return ImageFont.truetype(p, sz)
851
+ except: pass
 
 
852
  return ImageFont.load_default()
853
 
 
854
  def render_newspaper_image(parsed: dict, classified: str, day_number: int) -> str:
855
  W, H = 960, 720
856
+ PAPER=(245,232,200); INK=(20,8,2); BORDER=(70,40,8); SUBINK=(90,55,25); GREY=(130,100,70)
857
+
858
+ img = Image.new("RGB",(W,H),PAPER)
859
+ d = ImageDraw.Draw(img)
860
+
861
+ f_mast = _tf(_SERIF_B, 30); f_hed = _tf(_SERIF_B, 20)
862
+ f_body = _tf(_SERIF_R, 12); f_sm = _tf(_SERIF_R, 10)
863
+ f_sub = _tf(_SERIF_R, 11); f_bold = _tf(_SERIF_B, 12)
 
 
 
 
 
 
 
864
 
865
  M = 18
866
+ d.rectangle([M,M,W-M,H-M], outline=BORDER, width=3)
867
+ d.rectangle([M+6,M+6,W-M-6,H-M-6], outline=BORDER, width=1)
868
 
869
  y = M + 14
870
  # Masthead
871
  mast = "THE TINYWICK HOLLOW GAZETTE"
872
+ bb = d.textbbox((0,0),mast,font=f_mast); tw=bb[2]-bb[0]
873
+ d.text(((W-tw)/2, y), mast, fill=INK, font=f_mast); y += bb[3]-bb[1]+3
 
 
874
  # Sub
875
  sub = f"Est. Day 1 ✦ Day {day_number} ✦ One Acorn ✦ Woodland Readers Only"
876
+ bb=d.textbbox((0,0),sub,font=f_sm); d.text(((W-(bb[2]-bb[0]))/2,y),sub,fill=SUBINK,font=f_sm)
877
+ y+=bb[3]-bb[1]+5
 
878
  # Weather strip
879
+ wx = parsed.get("weather","Overcast.")
880
  wx_line = f"☁ WEATHER: {wx}"
881
+ bb=d.textbbox((0,0),wx_line,font=f_sm); d.text(((W-(bb[2]-bb[0]))/2,y),wx_line,fill=GREY,font=f_sm)
882
+ y+=bb[3]-bb[1]+4
883
+ d.line([M+10,y,W-M-10,y],fill=BORDER,width=2)
884
+ d.line([M+10,y+4,W-M-10,y+4],fill=BORDER,width=1); y+=14
 
 
885
 
886
  # Headline
887
+ for line in textwrap.wrap(parsed.get("headline",""),width=50):
888
+ bb=d.textbbox((0,0),line,font=f_hed); d.text(((W-(bb[2]-bb[0]))/2,y),line,fill=INK,font=f_hed)
889
+ y+=bb[3]-bb[1]+2
890
+ y+=4; d.line([M+10,y,W-M-10,y],fill=BORDER,width=1); y+=10
 
 
 
891
 
892
  # Two-column article + sidebar
893
+ PAD=M+12; COL_GAP=24; SIDE_W=220
894
+ main_w = W - 2*PAD - COL_GAP - SIDE_W
895
+ half_w = (main_w-COL_GAP)//2
896
+ col1_x=PAD; col2_x=PAD+half_w+COL_GAP; side_x=PAD+main_w+COL_GAP
897
+ LH=15; MAX_Y=H-M-50; art_y=y
898
+
899
+ art_lines = textwrap.wrap(parsed.get("article",""), width=38)
900
+ mid = max(1,len(art_lines)//2)
901
+ ly=art_y
 
 
 
 
 
 
902
  for line in art_lines[:mid]:
903
+ if ly+LH>MAX_Y: break
904
+ d.text((col1_x,ly),line,fill=INK,font=f_body); ly+=LH
905
+ d.line([col1_x+half_w+COL_GAP//2,art_y,col1_x+half_w+COL_GAP//2,min(ly,MAX_Y)],fill=GREY,width=1)
906
+ ry=art_y
 
 
 
 
 
 
 
 
 
 
 
907
  for line in art_lines[mid:]:
908
+ if ry+LH>MAX_Y: break
909
+ d.text((col2_x,ry),line,fill=INK,font=f_body); ry+=LH
 
 
910
 
911
  # Sidebar: In Brief
912
+ d.line([side_x-8,art_y,side_x-8,MAX_Y],fill=BORDER,width=1)
913
+ sy=art_y
914
+ d.text((side_x,sy),"IN BRIEF",fill=INK,font=f_bold); sy+=16
915
+ d.line([side_x,sy,W-M-14,sy],fill=GREY,width=1); sy+=6
916
+ briefs = parsed.get("briefs",{})
 
 
917
  for cname in CREATURES:
918
+ em=CREATURE_EMOJI.get(cname,"?"); txt=briefs.get(cname,"")
919
+ header=f"{em} {cname.upper()}"
920
+ d.text((side_x,sy),header,fill=INK,font=f_bold); sy+=13
921
+ for bline in textwrap.wrap(txt,width=26):
922
+ if sy+12>MAX_Y: break
923
+ d.text((side_x,sy),bline,fill=INK,font=f_sm); sy+=12
924
+ sy+=4
 
 
 
 
925
 
926
  # Classifieds footer
927
+ fy=H-M-38; d.line([M+10,fy,W-M-10,fy],fill=BORDER,width=1); fy+=4
928
+ d.text((M+14,fy),"CLASSIFIEDS",fill=INK,font=f_bold); fy+=14
929
+ for cl in textwrap.wrap(classified,width=100):
930
+ if fy+12>H-M-8: break
931
+ d.text((M+14,fy),cl,fill=INK,font=f_sm); fy+=12
932
+
933
+ path=f"/tmp/tinywick_day_{day_number}.png"
934
+ img.save(path,"PNG")
 
 
 
 
 
935
  return path
936
 
937
 
 
956
  headlines = database.get_all_headlines()
957
  for dn, _ in reversed(headlines):
958
  day = database.get_day(dn)
959
+ if day: trace["days"].append(dict(day))
 
960
  evts = database.get_events_for_day(dn)
961
  trace["events"].extend([{"day": dn, **e} for e in evts])
962
  path = "/tmp/tiny_civ_traces.json"
963
+ with open(path,"w") as f: json.dump(trace, f, indent=2, default=str)
 
964
  return path
965
 
966
 
 
1130
  # ═══════════════════════════════════════════════════════════════════
1131
  def _rel_tier(score: int) -> tuple[str, str]:
1132
  for lo, hi, label, icon in REL_TIERS:
1133
+ if lo <= score <= hi: return label, icon
 
1134
  return "Unknown", "?"
1135
 
 
1136
  def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
1137
+ hed = (parsed.get("headline","") or "").replace("<","&lt;")
1138
+ art = (parsed.get("article","") or "").replace("<","&lt;")
1139
+ wx = (parsed.get("weather","") or "").replace("<","&lt;")
1140
+ briefs = parsed.get("briefs",{})
1141
  emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES)
1142
+ classified_esc = classified.replace("<","&lt;")
1143
 
1144
  sidebar_items = ""
1145
  for c in CREATURES:
1146
+ em = CREATURE_EMOJI.get(c,"?")
1147
+ txt = (briefs.get(c,"") or "").replace("<","&lt;")
1148
  sidebar_items += f"""
1149
  <div class="sidebar-item">
1150
  <div class="sidebar-creature-name">{em} {c.upper()}</div>
 
1171
  </div>
1172
  """
1173
 
 
1174
  def _html_placeholder() -> str:
1175
  founding = database.get_day(0)
1176
  if founding:
1177
+ text = founding["full_newspaper_text"]
1178
+ parts = text.split("\n\n",1)
1179
+ hed = parts[0]; art = parts[1] if len(parts)>1 else text
 
1180
  dummy_parsed = {
1181
+ "headline": hed, "article": art,
 
1182
  "weather": "Portentous, with scattered significance.",
1183
  "briefs": {
1184
  "fox": "Forged three certificates before breakfast.",
1185
  "badger": "Insisted on thirteen amendments before lunch.",
1186
  "squirrel": "Invented a signing machine! It signed the wrong document!",
1187
  "mole": "Something is already happening underground.",
1188
+ }
1189
  }
1190
  return _html_paper(dummy_parsed, "NOTICE: Civilisation now in progress.", 0)
1191
  return """<div class="paper-wrap">
 
1197
  <div class="paper-daybadge">— Day 0 —</div>
1198
  </div>"""
1199
 
 
1200
  def _html_creatures() -> str:
1201
  creatures = database.get_all_creatures()
1202
  cards = ""
1203
  for c in creatures:
1204
+ em = CREATURE_EMOJI.get(c["name"],"?")
1205
+ inv = (", ".join(c["inventory"][:3]) + ("…" if len(c["inventory"])>3 else "")) or "nothing"
 
 
1206
  rels = ""
1207
  for other, score in sorted(c["relationship_scores"].items()):
1208
  label, icon = _rel_tier(score)
1209
+ rels += f'<span title="{label} ({score})">{CREATURE_EMOJI.get(other,"?")} {icon}</span> '
1210
  cards += f"""<div class="creature-card">
1211
+ <span class="creature-name">{em} {c['name'].capitalize()}</span>
1212
  <div class="creature-tier">{rels}</div>
1213
  <div style="font-size:.78em;margin-top:3px;color:#5a3615;"><em>Carries:</em> {inv}</div>
1214
  </div>"""
1215
  return f'<div class="creature-grid">{cards}</div>'
1216
 
 
1217
  def _html_civ_stats() -> str:
1218
  s = database.get_civ_stats()
1219
+ bp = s["best_pair"]; wp = s["worst_pair"]
 
1220
  dom = s["dominant"].capitalize()
1221
  return f"""<div class="civ-stats">
1222
+ <div class="stat-item"><span class="stat-label">Days</span><span class="stat-value">{s['total_days']}</span></div>
1223
+ <div class="stat-item"><span class="stat-label">Events</span><span class="stat-value">{s['total_events']}</span></div>
1224
+ <div class="stat-item"><span class="stat-label">Nudges</span><span class="stat-value">{s['total_nudges']}</span></div>
1225
  <div class="stat-item"><span class="stat-label">Strongest bond</span>
1226
  <span class="stat-value">{bp[0].capitalize()} &amp; {bp[1].capitalize()} ({bp[2]})</span></div>
1227
  <div class="stat-item"><span class="stat-label">Bitterest feud</span>
 
1229
  <div class="stat-item"><span class="stat-label">Most popular</span><span class="stat-value">{dom}</span></div>
1230
  </div>"""
1231
 
 
1232
  def _archive_choices():
1233
  hl = database.get_all_headlines()
1234
+ return [(f"Day {dn}: {h[:44]}{'…' if len(h)>44 else ''}", dn) for dn,h in hl] or []
 
 
 
 
 
 
1235
 
1236
+ def _status(msg): return f'<div class="status-strip">{msg}</div>'
1237
 
1238
  def _konami_html() -> str:
1239
  import html as _h
 
1240
  details = "".join(
1241
+ f"<details><summary>{CREATURE_EMOJI.get(n,'')}<strong> {n.upper()}</strong></summary>"
1242
  f"<pre>{_h.escape(p)}</pre></details>"
1243
+ for n,p in AGENT_PROMPTS.items()
1244
  )
1245
  details += f"<details><summary>📰 <strong>NARRATOR</strong></summary><pre>{_h.escape(NARRATOR_PROMPT)}</pre></details>"
1246
  return f"""
 
1265
  _html_civ_stats(),
1266
  gr.update(choices=_archive_choices(), value=None),
1267
  _status(status_msg),
1268
+ day_num, parsed, classified,
 
 
1269
  )
1270
 
 
1271
  def _safe_advance(nudge_type=None, nudge_value=None, nudge_target=None):
1272
  try:
1273
  day_num, parsed, classified = advance_day(nudge_type, nudge_value, nudge_target)
1274
+ model_tag = f" [{_model_id_used.split('/')[-1]}]" if _model_id_used!="none" else ""
1275
+ return _render_all(day_num, parsed, classified,
1276
+ f"✓ Day {day_num} published to the Gazette.{model_tag}")
 
 
 
 
 
 
1277
  except Exception:
1278
  tb = traceback.format_exc()
1279
  print(tb)
1280
  err_parsed = {
1281
  "headline": "THE GAZETTE'S PRINTING PRESS HAS JAMMED",
1282
  "article": "Our correspondents report a technical malfunction. The editor is inconsolable. "
1283
+ "Beatrice Badger suspects sabotage. Reginald Fox denies everything.",
1284
  "weather": "Stormy, with a chance of errors.",
1285
  "briefs": {c: "Unavailable for comment." for c in CREATURES},
1286
  }
1287
+ return _render_all(0, err_parsed, "LOST: One functioning simulation. — The Editor",
1288
+ "✗ Simulation error — the printing press is jammed. Check logs.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289
 
1290
+ def handle_advance(): return _safe_advance()
1291
+ def handle_rumour(c,r): return _safe_advance("rumour",r,c)
1292
+ def handle_donation(o): return _safe_advance("donation",o)
1293
+ def handle_law(l): return _safe_advance("law",l)
1294
 
1295
  def handle_archive_view(day_num):
1296
  if day_num is None:
1297
+ return '<div class="archive-area"><em>Select a day to read its edition.</em></div>'
 
 
1298
  day = database.get_day(int(day_num))
1299
+ if not day: return '<div class="archive-area"><em>Edition not found.</em></div>'
1300
+ txt = day["full_newspaper_text"]; parts = txt.split("\n\n",1)
1301
+ hed = parts[0]; art = parts[1] if len(parts)>1 else txt
 
 
 
1302
  # Reconstruct parsed from stored text
1303
  parsed = _parse_newspaper_full(txt, day_num)
1304
  classified_match = re.search(r"CLASSIFIEDS:\s*(.+)", txt)
1305
  cl = classified_match.group(1) if classified_match else random.choice(CLASSIFIEDS)
1306
  return f'<div class="archive-area">{_html_paper(parsed, cl, day_num)}</div>'
1307
 
 
1308
  def handle_share(day_num_state, parsed_state, classified_state):
1309
+ if not parsed_state or not parsed_state.get("headline"): return gr.update(visible=False)
 
1310
  try:
1311
+ path = render_newspaper_image(parsed_state, classified_state or "", day_num_state or 0)
 
 
1312
  return gr.update(visible=True, value=path)
1313
+ except Exception: return gr.update(visible=False)
 
 
1314
 
1315
  def handle_export_traces():
1316
  try:
1317
  path = export_agent_traces()
1318
  return gr.update(visible=True, value=path)
1319
+ except Exception: return gr.update(visible=False)
 
1320
 
1321
 
1322
  # =================================================================
 
1327
  database.init_db()
1328
  _latest = database.get_latest_day()
1329
  if _latest:
1330
+ _INIT_DAY = _latest["day_number"]
1331
  _INIT_PARSED = _parse_newspaper_full(_latest["full_newspaper_text"], _INIT_DAY)
1332
+ _cl_m = re.search(r"CLASSIFIEDS:\s*(.+)", _latest["full_newspaper_text"])
1333
+ _INIT_CL = _cl_m.group(1) if _cl_m else random.choice(CLASSIFIEDS)
1334
  else:
1335
+ _INIT_DAY = 0; _INIT_PARSED = {}; _INIT_CL = ""
 
 
1336
 
1337
  # -- Gradio version-aware css/theme routing -----------------------
1338
  _GR_MAJOR = int(gr.__version__.split(".")[0])
1339
+ if _GR_MAJOR >= 6: _BKW: dict = {}; _LKW: dict = {"css": NEWSPAPER_CSS}
1340
+ elif _GR_MAJOR >= 5: _BKW = {"css": NEWSPAPER_CSS}; _LKW = {}
1341
+ else: _BKW = {"css": NEWSPAPER_CSS, "theme": gr.themes.Base(primary_hue="orange", neutral_hue="stone")}; _LKW = {}
 
 
 
 
 
 
 
 
 
1342
 
1343
  # ------------------------------------------------------------------
1344
  with gr.Blocks(title="Tiny Civilization — The Tinywick Hollow Gazette", **_BKW) as demo:
1345
+
1346
  gr.HTML(_konami_html())
1347
 
1348
  gr.HTML(
1349
  '<div style="text-align:center;padding:8px 0 2px;">'
1350
+ '<h1 style="font-family:\'Playfair Display\',Georgia,serif;color:#160800;font-size:1.9em;margin:0 0 2px;">'
1351
+ '🦊 Tiny Civilization 🐀</h1>'
1352
  '<p style="font-family:Georgia,serif;color:#5a3615;font-style:italic;margin:0;font-size:.87em;">'
1353
+ 'A persistent woodland civilisation. One day. One acorn. One absurd headline at a time.'
1354
  '&nbsp;|&nbsp;<kbd title="Konami Code">↑↑↓↓←→←→BA</kbd> for secrets.'
1355
+ '</p></div>'
1356
  )
1357
 
1358
+ day_state = gr.State(_INIT_DAY)
1359
+ parsed_state = gr.State(_INIT_PARSED)
1360
  classif_state = gr.State(_INIT_CL)
1361
 
1362
+ status_html = gr.HTML(value=_status(
1363
+ f"Day {_INIT_DAY} in the archive — next: Day {_INIT_DAY + 1}."
1364
+ if _INIT_DAY >= 0 else "No days yet. Press Advance Day to begin."))
 
 
 
 
1365
  civ_stats_html = gr.HTML(value=_html_civ_stats())
1366
 
1367
  with gr.Row(equal_height=False):
1368
+
1369
  with gr.Column(scale=3):
1370
  newspaper_display = gr.HTML(
1371
+ value=(_html_paper(_INIT_PARSED, _INIT_CL, _INIT_DAY)
1372
+ if _INIT_PARSED else _html_placeholder()))
 
 
 
 
1373
 
1374
+ with gr.Accordion('📜 Archive — Past Editions', open=False):
1375
  archive_dd = gr.Dropdown(
1376
+ choices=_archive_choices(), value=None,
1377
+ label='Select a past day', container=False)
 
 
 
1378
  archive_display = gr.HTML(
1379
+ value='<div class="archive-area"><em>Select a day to read its edition.</em></div>')
 
1380
 
1381
  with gr.Column(scale=1, min_width=260):
1382
  gr.HTML('<div class="section-title">📰 Editorial Desk</div>')
1383
+ advance_btn = gr.Button('📅 Advance Day (no nudge)', variant='primary', size='lg')
 
 
1384
 
1385
  gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
1386
  gr.HTML('<div class="section-title">✉ Nudge the Story</div>')
1387
+ gr.HTML('<p style="font-size:.78em;color:#5a3615;text-align:center;'
1388
+ 'font-style:italic;margin:0 0 6px;">Each nudge advances one day.</p>')
1389
+
1390
+ with gr.Accordion('🗣️ Spread a Rumour', open=False):
1391
+ rumour_creature = gr.Dropdown(choices=CREATURES, value=CREATURES[0],
1392
+ label='About which creature?')
1393
+ rumour_type_dd = gr.Dropdown(choices=RUMOUR_TYPES, value=RUMOUR_TYPES[0],
1394
+ label='What rumour?')
1395
+ rumour_btn = gr.Button('📢 Spread It', variant='secondary')
1396
+
1397
+ with gr.Accordion('🎁 Donate a Weird Object', open=False):
1398
+ donation_dd = gr.Dropdown(choices=WEIRD_OBJECTS, value=WEIRD_OBJECTS[0],
1399
+ label='Which object?')
1400
+ donation_btn = gr.Button('🎁 Donate It', variant='secondary')
1401
 
1402
+ with gr.Accordion('⚖Propose a New Law', open=False):
1403
+ law_dd = gr.Dropdown(choices=LAWS, value=LAWS[0], label='Which law?')
1404
+ law_btn = gr.Button('⚖️ Propose It', variant='secondary')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405
 
1406
  gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
1407
+ share_btn = gr.Button('🖼️ Share as Image', variant='secondary')
1408
+ img_output = gr.Image(label='Front Page PNG', visible=False, type='filepath')
1409
+ export_btn = gr.Button('📡 Export Agent Traces (JSON)', variant='secondary')
1410
+ trace_output = gr.File(label='Agent Traces JSON', visible=False)
 
 
1411
 
1412
+ gr.HTML('<p style="font-size:.70em;color:#6a4818;text-align:center;'
1413
+ 'margin-top:8px;font-style:italic;">'
1414
+ 'Model: Qwen2.5-1.5B ≤4B 🐜&nbsp;|&nbsp;Local only 🔌&nbsp;|&nbsp;Custom UI 🎨</p>')
 
 
1415
 
1416
+ gr.HTML('<div class="section-title" style="margin-top:12px;">Woodland Residents</div>')
 
 
1417
  creature_display = gr.HTML(value=_html_creatures())
1418
 
1419
+ _OUT = [newspaper_display, creature_display, civ_stats_html,
1420
+ archive_dd, status_html, day_state, parsed_state, classif_state]
 
 
 
 
 
 
 
 
1421
 
1422
+ advance_btn.click( fn=handle_advance, inputs=[], outputs=_OUT, api_name=False)
1423
+ rumour_btn.click( fn=handle_rumour, inputs=[rumour_creature, rumour_type_dd], outputs=_OUT, api_name=False)
1424
+ donation_btn.click(fn=handle_donation, inputs=[donation_dd], outputs=_OUT, api_name=False)
1425
+ law_btn.click( fn=handle_law, inputs=[law_dd], outputs=_OUT, api_name=False)
1426
+ archive_dd.change( fn=handle_archive_view, inputs=[archive_dd], outputs=[archive_display], api_name=False)
1427
+ share_btn.click( fn=handle_share, inputs=[day_state, parsed_state, classif_state], outputs=[img_output], api_name=False)
1428
+ export_btn.click( fn=handle_export_traces, inputs=[], outputs=[trace_output], api_name=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1429
 
1430
  # ═══════════════════════════════════════════════════════════════════
1431
  # 13 ▸ ENTRY POINT
1432
  # ═══════════════════════════════════════════════════════════════════
1433
  if __name__ == "__main__":
1434
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr=False, **_LKW)