import os
import json
import time
import random
import html
import gradio as gr
import agents
import voice
import transcribe
import events
import router
import world_state
import campaign
from worlds import get_world, WORLDS, list_worlds
MOCK = os.environ.get("TINYWORLD_MOCK", "0") == "1"
# On a ZeroGPU Space (TINYWORLD_INFER=local) the @spaces.GPU functions must be
# registered at startup or ZeroGPU shuts the Space down. Import inference eagerly
# in that mode only (loads torch but NOT the models β those stay lazy). Skipped on
# mock/modal so dev and tests never need torch/spaces.
if not MOCK and os.environ.get("TINYWORLD_INFER", "modal").lower() == "local":
try:
import inference # noqa: F401 (registers ZeroGPU GPU tasks)
import threading
# warm the model into CPU RAM in the background so startup isn't blocked
threading.Thread(target=inference.warmup, daemon=True).start()
print("[app] ZeroGPU inference module loaded; warming model in background")
except Exception as e:
print(f"[app] inference preimport failed: {e}")
MOOD_EMOJI = {
"happy": "π", "stressed": "π°", "bored": "π", "excited": "π€©",
"hungry": "π", "tired": "π΄", "nostalgic": "π₯Ή", "curious": "π§",
"proud": "π€", "embarrassed": "π«£",
}
VIBE_COLORS = {
"energy": ("#ff6b6b", "#ffb15f"),
"hunger": ("#ffd76a", "#ff8a5f"),
"social": ("#38e8ff", "#9b6bff"),
}
CHAR_COLORS = {
"Marta Voss": "#ff5fa2", "Jay Park": "#ff8a5f", "Nia Okafor": "#7CFFB2",
"Luca Bell": "#38e8ff", "Priya Raman": "#9b6bff",
}
PALETTE = ["#ff5fa2", "#ff8a5f", "#7CFFB2", "#38e8ff", "#9b6bff"]
def esc(value):
return html.escape(str(value), quote=True)
def char_color(world, name):
for i, c in enumerate(world["cast"]):
if c["name"] == name:
return c.get("color") or CHAR_COLORS.get(name) or PALETTE[i % len(PALETTE)]
return "#9b6bff"
# ---------------------------------------------------------------- canvas payloads
def build_world_payload(world_id):
world = get_world(world_id)
board = world["board"]
cast = []
for c in world["cast"]:
home_key = c.get("home", "square")
tile = board["hotspots_tile"].get(home_key, [6, 6])
cast.append({
"name": c["name"], "short": c["name"].split()[0],
"emoji": c.get("emoji", "π€"),
"color": char_color(world, c["name"]),
"home": tile,
})
payload = {
"cols": board["cols"], "rows": board["rows"],
"roads": board["roads"], "plaza": board["plaza"],
"plaza_center": board.get("plaza_center", [board["cols"] / 2, board["rows"] / 2]),
"buildings": board["buildings"], "trees": board.get("trees", []),
"props": board.get("props", []), "cast": cast,
"hotspots": board.get("hotspots_tile", {}), "ambient": board.get("ambient", 12),
}
return json.dumps(payload)
def build_reactions_payload(world_id, reactions):
world = get_world(world_id)
board = world["board"]
hs = board["hotspots_tile"]
acts = board.get("activities", {})
out = {"ts": time.time(), "reactions": []}
for r in reactions:
name = r["name"]
key = r.get("moved_to") or world_state.get_position(world_id, name)
act = acts.get(name, {})
action = (r.get("action") or "").strip()
short_action = (action[:24].rstrip() + "β¦") if len(action) > 25 else action
label = short_action or act.get("label", "")
vehicle = act.get("vehicle", "")
if key and key in hs:
tgt = hs[key]
else:
home = next((c.get("home") for c in world["cast"] if c["name"] == name), None)
tgt = hs.get(home) or hs.get("square") or [0, 0]
out["reactions"].append({
"name": name, "short": name.split()[0],
"mood": r["mood"], "moodEmoji": MOOD_EMOJI.get(r["mood"], "π"),
"text": r["text"], "target": tgt, "activity": label, "vehicle": vehicle,
"running": False,
})
return json.dumps(out)
def build_state_payload(world_id):
world = get_world(world_id)
board = world["board"]
hs = board["hotspots_tile"]
out = {"ts": time.time(), "silent": True, "reactions": []}
for c in world["cast"]:
name = c["name"]
key = world_state.get_position(world_id, name) or c.get("home", "square")
tile = hs.get(key) or hs.get(c.get("home")) or hs.get("square") or [0, 0]
mood = world_state.get_mood(world_id, name)
out["reactions"].append({
"name": name,
"short": name.split()[0],
"mood": mood,
"moodEmoji": MOOD_EMOJI.get(mood, "π"),
"text": "",
"target": tile,
"activity": world_state.get_activity(world_id, name),
"vehicle": "",
"running": False,
})
return json.dumps(out)
# ---------------------------------------------------------------- roster / ticker / log
def render_vibe_bar(label, value, colors):
c1, c2 = colors
pct = int(value * 100)
return (
f'
'
)
def render_roster(world_id):
world = get_world(world_id)
world_state.init_cast(world)
state = world_state.get_state(world_id)
needs = world_state.get_needs(world_id)
cards = []
for c in world["cast"]:
name = c["name"]
mood = state["moods"].get(name, "curious")
mood_e = MOOD_EMOJI.get(mood, "π")
n = needs.get(name, {"energy": 60, "hunger": 35, "social": 50})
cards.append(
f''
f'
{c.get("emoji", "π€")}
'
f'
{name.split()[0]}
'
f'
{mood_e} {mood}
'
f'
{render_vibe_bar("β‘", n["energy"] / 100, VIBE_COLORS["energy"])}'
f'{render_vibe_bar("π½", n["hunger"] / 100, VIBE_COLORS["hunger"])}'
f'{render_vibe_bar("π¬", n["social"] / 100, VIBE_COLORS["social"])}
'
f'
'
)
return f'{"".join(cards)}
'
def render_ticker(world_id):
state = world_state.get_state(world_id)
day, game_time, paused = world_state.get_game_time(world_id)
chaos = state["chaos"]
filled = min(int(chaos * 5), 5)
bar = "β°" * filled + "β±" * (5 - filled)
unlocked = " π" if chaos >= 0.5 else ""
return (
f''
f'DAY {day} Β· {world_state.format_time(game_time)}Β·'
f'{"PAUSED" if paused else "LIVE TIME"}Β·'
f'β‘ {state["event_count"]}Β·'
f'CHAOS {bar}{unlocked}
'
)
def render_daily_log(world_id):
world = get_world(world_id)
timeline = world_state.get_timeline(world_id)
rows = []
for c in world["cast"]:
name = c["name"]
entries = timeline.get(name, [])[-4:]
text = " Β· ".join(esc(e) for e in entries) if entries else "No entries yet."
rows.append(
f'{esc(name.split()[0])} '
f'{text}
'
)
return f'{"".join(rows)}
'
def render_mode_badge(status=None):
status = status or agents.get_runtime_status()
mode = status.get("mode")
model = status.get("model") or "unknown"
latency = status.get("latency")
error = status.get("error")
if mode == "live":
label = f"π’ Live Β· {model.split('/')[-1]}"
if latency is not None:
label += f" Β· {latency:.1f}s"
cls = "mode-live"
elif mode == "waking":
label = f"π‘ Waking Β· {model.split('/')[-1]}"
cls = "mode-waking"
elif mode == "error":
msg = f" Β· {error}" if error else ""
label = f"π΄ LLM error{msg}"
cls = "mode-error"
else:
label = "π‘ Offline demo (mock)"
cls = "mode-mock"
return f'{esc(label)}
'
def render_town_log(world_id, reactions=None, followup=None):
world = get_world(world_id)
if not reactions:
return (''
'The town log is empty.
Throw an event to start the story.
')
lines = []
for r in reactions:
char = next((c for c in world["cast"] if c["name"] == r["name"]), None)
emoji = char.get("emoji", "π€") if char else "π€"
name = esc(r["name"])
text = esc(r["text"])
mood = esc(r["mood"])
lines.append(
f''
f'{emoji}{name}'
f'{MOOD_EMOJI.get(r["mood"], "π")}
'
f'
"{text}"
'
)
if followup:
lines.append(f'π {esc(followup["text"])}
')
gossip_from = [r for r in reactions if random.random() < 0.3]
if gossip_from:
source = random.choice(gossip_from)
others = [c["name"] for c in world["cast"] if c["name"] != source["name"]]
if others:
target = random.choice(others)
snippet = source["text"][:60] + ("β¦" if len(source["text"]) > 60 else "")
lines.append(f'π£οΈ '
f'Word spreads: {esc(target.split()[0])} hears "{esc(snippet)}"
')
world_state.add_gossip(world_id, target, snippet)
return f'{"".join(lines)}
'
def render_explainer(world_id, reactions=None, focus=None):
world = get_world(world_id)
if not reactions:
return (''
'Pick a scenario or throw an event, then this panel explains '
'why each person reacted the way they did β great for teaching how '
'different people respond to the same situation.
')
parts = []
if focus:
parts.append(f'π Think about: {esc(focus)}
')
else:
parts.append('π Why did they react this way?
')
for r in reactions:
c = next((x for x in world["cast"] if x["name"] == r["name"]), None)
if not c:
continue
trait = (c.get("traits") or ["distinct"])[0]
hint = c.get("catchphrase_hint", "responds in their own way")
col = char_color(world, r["name"])
understanding = esc((r.get("understanding") or "").strip())
action = esc((r.get("action") or "").strip())
first_name = esc(r["name"].split()[0])
mood = esc(r["mood"])
trait_text = esc(trait)
if understanding or action:
reason = (
f'reads it as: {understanding} ' if understanding else ''
) + (
f'so they {action}.' if action else ''
)
parts.append(
f'{first_name} '
f'(feeling {mood}, naturally {trait_text}) {reason}
'
)
else:
parts.append(
f'{first_name} '
f'feels {mood} and is naturally {trait_text}, '
f'so they {esc(hint)}.
'
)
return f'{"".join(parts)}
'
# ---------------------------------------------------------------- crisis mode UI
def render_crisis_hud(world_id):
v = campaign.view(get_world(world_id))
cr = v["crisis"]
if not v["active"] or not cr:
return 'No crisis active β switch to π¬ Campaign and press Start Campaign.
'
chips = []
for req in cr["requirements"]:
cls = "met" if req["met"] else "open"
icon = "β
" if req["met"] else "β³"
opt = " (optional)" if req["optional"] else ""
chips.append(f'{icon} {esc(req["label"])}{esc(opt)}')
chaos_pct = int(v["chaos"] * 100)
res = int(v["town_resilience"])
return (
f''
f'
π¨ {esc(cr["title"])}'
f'Chapter {v["chapter_index"] + 1}/{v["chapter_count"]} Β· '
f'Round {cr["round"]}/{cr["time_limit"]}
'
f'
{"".join(chips)}
'
f'
'
)
def render_story_card(kind="dispatch", title="", body="", extra=""):
if not title and not body and not extra:
return ''
title_html = f'{esc(title)}
' if title else ""
body_html = f'{esc(body)}
' if body else ""
return f'{title_html}{body_html}{extra}
'
def build_crisis_payload(world_id):
world = get_world(world_id)
v = campaign.view(world)
cr = v["crisis"]
out = {"ts": time.time(), "active": bool(v["active"] and cr),
"status": v["status"], "chaos": v["chaos"]}
if cr:
hs = world["board"]["hotspots_tile"]
out.update({
"kind": cr["kind"], "title": cr["title"],
"tile": hs.get(cr["affected_hotspot"]) if cr["affected_hotspot"] else None,
"round": cr["round"], "time_limit": cr["time_limit"],
"requirements": [{"id": r["id"], "label": r["label"], "met": r["met"]}
for r in cr["requirements"]],
})
return json.dumps(out)
def _outcome_card(res):
oc = res.get("outcome")
grade = res.get("grade")
if oc == "won":
return render_story_card("win", f"β
Chapter cleared Β· Grade {grade}", res.get("outro", ""))
if oc == "lost_crisis":
return render_story_card("loss", f"β Chapter failed Β· Grade {grade}", res.get("outro", ""))
if oc == "campaign_lost":
return render_story_card("gameover", "π The town fell",
f'{res.get("outro", "")} Town Resilience hit zero.')
if oc == "finale":
extra = f'{esc(res.get("reveal", ""))}
'
return render_story_card("finale", "π¬ The Long Summer β Finale", res.get("outro", ""), extra)
return render_story_card("dispatch", "", res.get("dispatch", ""))
def scenario_choices(world_id):
world = get_world(world_id)
return [(s["title"], s["id"]) for s in world.get("scenarios", [])]
def render_stage_shell():
return ''
# ---------------------------------------------------------------- assets
def _read(path):
full = os.path.join(os.path.dirname(__file__), path)
if os.path.exists(full):
with open(full) as f:
return f.read()
return ""
css_content = _read("assets/style.css")
js_content = _read("assets/game.js")
# Force dark mode on load. HF Spaces otherwise loads light mode, which renders
# Gradio's dark text on top of our neon-dark panels β unreadable. With ?__theme=dark
# Gradio uses its light-on-dark palette and everything is legible.
THEME_JS = (
"(function(){try{var u=new URL(window.location.href);"
"if(u.searchParams.get('__theme')!=='dark'){"
"u.searchParams.set('__theme','dark');window.location.replace(u.href);}}catch(e){}})();\n"
)
js_content = THEME_JS + js_content
DEFAULT_WORLD = os.environ.get("TW_DEFAULT_WORLD", "maple_street")
world_list = list_worlds()
world_names = {w["id"]: w["name"] for w in world_list}
# ---------------------------------------------------------------- UI
with gr.Blocks(title="TinyWorld β AI Neighborhood Game") as demo:
current_world_id = gr.State(DEFAULT_WORLD)
last_reactions_state = gr.State([])
with gr.Row():
with gr.Column(scale=7):
gr.HTML('TINYWORLD
'
'
the town that remembers
')
with gr.Column(scale=2, min_width=160):
world_picker = gr.Dropdown(
choices=[(world_names.get(w["id"], w["id"]), w["id"]) for w in world_list],
value=DEFAULT_WORLD, label="World")
with gr.Column(scale=3, min_width=210):
ticker_html = gr.HTML(render_ticker(DEFAULT_WORLD))
with gr.Column(scale=2, min_width=210):
mode_badge_html = gr.HTML(render_mode_badge())
# the canvas stage (rendered once, never re-rendered β game loop persists)
gr.HTML(render_stage_shell())
# hidden data channels for the canvas
world_box = gr.Textbox(value=build_world_payload(DEFAULT_WORLD), elem_id="tw-world",
elem_classes="tw-data", label="", interactive=True)
reactions_box = gr.Textbox(value="", elem_id="tw-reactions", elem_classes="tw-data",
label="", interactive=True)
crisis_box = gr.Textbox(value=build_crisis_payload(DEFAULT_WORLD), elem_id="tw-crisis",
elem_classes="tw-data", label="", interactive=True)
with gr.Row(elem_id="tw-console"):
with gr.Column(scale=8):
event_input = gr.Textbox(
placeholder="Type an event to throw at the neighborhood⦠(e.g. a UFO lands in the park)",
label="", show_label=False, lines=1)
with gr.Column(scale=2, min_width=150):
trigger_btn = gr.Button("β‘ THROW", variant="primary", elem_id="throw-btn")
with gr.Row(elem_id="tw-actions"):
with gr.Column(scale=6, min_width=260):
scenario_dd = gr.Dropdown(choices=scenario_choices(DEFAULT_WORLD), value=None,
label="π Teaching scenario β a real situation to explore")
with gr.Column(scale=2, min_width=140):
run_scenario_btn = gr.Button("π Run Scenario")
with gr.Column(scale=2, min_width=140):
random_btn = gr.Button("π² Random Chaos")
with gr.Column(scale=1, min_width=120):
pause_btn = gr.Button("β― Time")
with gr.Column(scale=1, min_width=120):
step_btn = gr.Button("β Step")
with gr.Row(elem_id="tw-mode"):
with gr.Column(scale=5, min_width=240):
mode_radio = gr.Radio(
["π§ͺ Sandbox", "π¬ Campaign"], value="π§ͺ Sandbox",
label="Mode", show_label=True)
with gr.Column(scale=2, min_width=160):
start_campaign_btn = gr.Button("π¬ Start Campaign", variant="primary")
with gr.Column(scale=2, min_width=160):
next_round_btn = gr.Button("βΆ Play Round")
crisis_hud_html = gr.HTML(render_crisis_hud(DEFAULT_WORLD))
story_card_html = gr.HTML(render_story_card())
with gr.Row(equal_height=False):
with gr.Column(scale=3, min_width=220):
gr.HTML('π§βπ€βπ§ Townsfolk
')
roster_html = gr.HTML(render_roster(DEFAULT_WORLD))
with gr.Column(scale=4, min_width=240):
gr.HTML('π Town Log
')
town_log_html = gr.HTML(render_town_log(DEFAULT_WORLD))
with gr.Column(scale=4, min_width=240):
gr.HTML('π Learn β Why did they react?
')
explainer_html = gr.HTML(render_explainer(DEFAULT_WORLD))
gr.HTML('π Daily Log
')
daily_log_html = gr.HTML(render_daily_log(DEFAULT_WORLD))
gr.HTML('π Voice
')
with gr.Row(elem_id="tw-audio", equal_height=False):
with gr.Column(scale=4, min_width=240):
mic_input = gr.Audio(sources=["microphone"], type="filepath", label="ποΈ 1 Β· Record your event")
transcribe_btn = gr.Button("π 2 Β· Transcribe to text")
mic_status = gr.HTML('Record β press Transcribe: your words fill the '
'event box, then press β‘ THROW. Mic only works on '
'http://localhost:7860 (blocked on 0.0.0.0).
')
with gr.Column(scale=4, min_width=240):
voice_output = gr.Audio(label="π Auto-voice (plays after each event)", autoplay=True)
gr.HTML('The most dramatic reaction is read aloud automatically.
')
with gr.Column(scale=4, min_width=240):
hear_name = gr.Dropdown(choices=[], label="π§ Replay a character's voice")
hear_btn = gr.Button("βΆ Play their last line")
gr.HTML('Pick any character and hear their last line again.
')
gr.HTML('')
tick_timer = gr.Timer(value=float(os.environ.get("TINYWORLD_TIME_SCALE", "6")), active=True)
# ---------------------------------------------------------- handlers
def _run_event(world_id, event_text, focus=None, route=None):
world = get_world(world_id)
world_state.init_cast(world)
result = agents.react(world_id, event_text, route=route)
reactions = result["reactions"]
runtime = result.get("runtime") or agents.get_runtime_status()
followup = agents.generate_followup(reactions, event_text)
top = max(reactions, key=lambda r: r["drama"])
audio = None
try:
cd = next(c for c in world["cast"] if c["name"] == top["name"])
audio = voice.generate_voice(top["text"], cd["voice_description"])
except Exception as e:
print(f"[app] voice failed: {e}")
names = [r["name"] for r in reactions]
return (render_town_log(world_id, reactions, followup), render_roster(world_id),
render_ticker(world_id), audio,
gr.Dropdown(choices=names, value=names[0] if names else None),
reactions, build_reactions_payload(world_id, reactions),
render_explainer(world_id, reactions, focus), render_mode_badge(runtime),
render_daily_log(world_id))
def _sandbox_tail(world_id):
# the three campaign outputs in their idle/passthrough state for sandbox events
return (render_crisis_hud(world_id), render_story_card(), build_crisis_payload(world_id))
def _empty_sandbox(world_id):
return (render_town_log(world_id), render_roster(world_id), render_ticker(world_id),
None, gr.Dropdown(choices=[], value=None), [], "", render_explainer(world_id),
render_mode_badge(), render_daily_log(world_id)) + _sandbox_tail(world_id)
def _sandbox_throw(event_text, world_id):
if not event_text or not event_text.strip():
return _empty_sandbox(world_id)
world = get_world(world_id)
route = router.classify(event_text.strip(), world)
if route["type"] == "noop":
return _empty_sandbox(world_id)
return _run_event(world_id, event_text.strip(), route=route) + _sandbox_tail(world_id)
def do_throw(event_text, world_id, mode):
v = campaign.view(get_world(world_id))
if str(mode).startswith("π¬") and v["active"] and v["status"] == "playing":
return _run_crisis_round(world_id, event_text)
return _sandbox_throw(event_text, world_id)
def start_campaign(world_id):
world = get_world(world_id)
world_state.reset_world(world_id)
world_state.init_cast(world)
v = campaign.start(world)
card = render_story_card("intro", f"π¬ {v['title']}", v.get("chapter_intro", ""))
return (render_town_log(world_id), render_roster(world_id), render_ticker(world_id),
None, gr.Dropdown(choices=[], value=None), [], build_state_payload(world_id),
render_explainer(world_id), render_mode_badge(), render_daily_log(world_id),
render_crisis_hud(world_id), card, build_crisis_payload(world_id))
def _run_crisis_round(world_id, player_text):
world = get_world(world_id)
v0 = campaign.view(world)
if not v0["active"] or not v0["crisis"] or v0["status"] != "playing":
return (render_town_log(world_id), render_roster(world_id), render_ticker(world_id),
None, gr.Dropdown(choices=[], value=None), [], build_state_payload(world_id),
render_explainer(world_id), render_mode_badge(), render_daily_log(world_id),
render_crisis_hud(world_id),
render_story_card("dispatch", "", "No active crisis β press Start Campaign."),
build_crisis_payload(world_id))
cr = campaign.current_crisis(world)
focus = cr.focus if cr else ""
event_text = campaign.round_event_text(world)
route = None
if player_text and player_text.strip():
r = router.classify(player_text.strip(), world)
if r["type"] != "noop":
route = r
result = agents.react(world_id, event_text, route=route)
reactions = result["reactions"]
runtime = result.get("runtime") or agents.get_runtime_status()
res = campaign.resolve_round(world, reactions)
audio = None
if reactions:
top = max(reactions, key=lambda r: r["drama"])
try:
cd = next(c for c in world["cast"] if c["name"] == top["name"])
audio = voice.generate_voice(top["text"], cd["voice_description"])
except Exception as e:
print(f"[app] voice failed: {e}")
names = [r["name"] for r in reactions]
dispatch = {"text": res.get("dispatch", "")} if res.get("dispatch") else None
return (render_town_log(world_id, reactions, dispatch), render_roster(world_id),
render_ticker(world_id), audio,
gr.Dropdown(choices=names, value=names[0] if names else None),
reactions, build_reactions_payload(world_id, reactions),
render_explainer(world_id, reactions, focus), render_mode_badge(runtime),
render_daily_log(world_id), render_crisis_hud(world_id),
_outcome_card(res), build_crisis_payload(world_id))
def run_scenario(scenario_id, world_id):
world = get_world(world_id)
scen = next((s for s in world.get("scenarios", []) if s["id"] == scenario_id), None)
if not scen:
return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
route = {"type": "world_event", "text": scen["event"], "instruction": scen["event"], "addressees": [], "goto": None}
return (scen["event"],) + _run_event(world_id, scen["event"], scen.get("focus"), route)
def random_chaos(world_id):
world = get_world(world_id)
if world and world.get("events"):
return random.choice(world["events"])
return events.random_event()
def switch_world(world_id):
world = get_world(world_id)
if world:
world_state.reset_world(world_id)
world_state.init_cast(world)
return (build_world_payload(world_id), "", render_town_log(world_id),
render_roster(world_id), render_ticker(world_id),
gr.Dropdown(choices=scenario_choices(world_id), value=None),
render_explainer(world_id), world_id, render_mode_badge(), render_daily_log(world_id),
render_crisis_hud(world_id), render_story_card(), build_crisis_payload(world_id))
def sim_tick(world_id):
world = get_world(world_id)
if world:
world_state.tick(world, hours=1.0)
return (render_ticker(world_id), render_roster(world_id),
build_state_payload(world_id), render_daily_log(world_id))
def toggle_time(world_id):
_, _, paused = world_state.get_game_time(world_id)
world_state.set_paused(world_id, not paused)
return sim_tick(world_id)
def step_time(world_id):
world = get_world(world_id)
if world:
world_state.tick(world, hours=1.0, force=True)
return (render_ticker(world_id), render_roster(world_id),
build_state_payload(world_id), render_daily_log(world_id))
def transcribe_audio(audio_path):
if not audio_path:
return ("", 'β No recording found β press the mic β record button first. '
'The mic only works on http://localhost:7860.
')
text = transcribe.transcribe(audio_path)
if not text:
return ("", 'β Could not transcribe. Try again, or just type the event.
')
return (text, f'β
Heard: "{esc(text)}" β now press β‘ THROW.
')
def hear_reaction(name, world_id, reactions):
if not reactions or not name:
return None
rx = next((r for r in reactions if r["name"] == name), None)
if not rx:
return None
world = get_world(world_id)
try:
cd = next(c for c in world["cast"] if c["name"] == name)
return voice.generate_voice(rx["text"], cd["voice_description"])
except Exception as e:
print(f"[app] hear failed: {e}")
return None
def play_round(world_id):
return _run_crisis_round(world_id, "")
trig_out = [town_log_html, roster_html, ticker_html, voice_output,
hear_name, last_reactions_state, reactions_box, explainer_html, mode_badge_html,
daily_log_html]
full_out = trig_out + [crisis_hud_html, story_card_html, crisis_box]
trigger_btn.click(do_throw, [event_input, current_world_id, mode_radio], full_out)
event_input.submit(do_throw, [event_input, current_world_id, mode_radio], full_out)
start_campaign_btn.click(start_campaign, [current_world_id], full_out)
next_round_btn.click(play_round, [current_world_id], full_out)
run_scenario_btn.click(run_scenario, [scenario_dd, current_world_id], [event_input] + trig_out)
random_btn.click(random_chaos, [current_world_id], [event_input])
transcribe_btn.click(transcribe_audio, [mic_input], [event_input, mic_status])
hear_btn.click(hear_reaction, [hear_name, current_world_id, last_reactions_state], [voice_output])
world_picker.change(switch_world, [world_picker],
[world_box, reactions_box, town_log_html, roster_html, ticker_html,
scenario_dd, explainer_html, current_world_id, mode_badge_html, daily_log_html,
crisis_hud_html, story_card_html, crisis_box])
pause_btn.click(toggle_time, [current_world_id], [ticker_html, roster_html, reactions_box, daily_log_html])
step_btn.click(step_time, [current_world_id], [ticker_html, roster_html, reactions_box, daily_log_html])
tick_timer.tick(sim_tick, [current_world_id], [ticker_html, roster_html, reactions_box, daily_log_html])
if __name__ == "__main__":
port = int(os.environ.get("GRADIO_SERVER_PORT", "7860"))
print("\n TinyWorld is startingβ¦")
print(f" βΆ Open http://localhost:{port} (use localhost, not 0.0.0.0, so the mic works)\n")
demo.launch(server_name="0.0.0.0", server_port=port, css=css_content, js=js_content or None)