Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| import threading | |
| import warnings | |
| from pathlib import Path | |
| from typing import Any | |
| import moku.env_loader # noqa: F401 — load .env before other moku imports | |
| # Gradio + pandas on Python 3.14 emit noisy third-party deprecation warnings each tick. | |
| warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"gradio.*") | |
| warnings.filterwarnings("ignore", message=r".*no_silent_downcasting.*") | |
| warnings.filterwarnings("ignore", message=r".*copy keyword is deprecated.*") | |
| try: | |
| from pandas.errors import Pandas4Warning | |
| warnings.filterwarnings("ignore", category=Pandas4Warning) | |
| except ImportError: | |
| pass | |
| import gradio as gr | |
| from moku.llm_client import provider_label, warmup_local_backend | |
| from moku.memory import get_memory_store | |
| from moku.render_home import render_homepage | |
| from moku.visual_layers import VisualLayers, layers_from_toggles | |
| from moku.render_world import ( | |
| render_guide_panel, | |
| render_side_panel, | |
| render_sim_shell, | |
| render_story_section, | |
| render_world_scene, | |
| ) | |
| from moku.sim_engine import ( | |
| create_for_watch_mode, | |
| export_trace_json, | |
| render_creature_cards, | |
| render_deception, | |
| render_dictionary, | |
| render_field_notes, | |
| render_glyph_drift_panel, | |
| render_language_evolution, | |
| render_social_graph, | |
| render_traces, | |
| render_traces_panel, | |
| render_transcript, | |
| render_trust, | |
| step_world, | |
| generate_run_summary, | |
| ) | |
| APP_ROOT = Path(__file__).parent | |
| CSS_PATH = APP_ROOT / "moku" / "web" / "style.css" | |
| JS_PATH = APP_ROOT / "moku" / "web" / "sim.js" | |
| TRACE_DIR = APP_ROOT / "data" / "traces" | |
| _tick_lock = threading.Lock() | |
| def _llm_timer_interval() -> float: | |
| # HF Space + Modal LLM often needs 8–15s per creature; avoid overlapping timer polls. | |
| default = "8.0" if os.environ.get("SPACE_ID") else "2.5" | |
| return float(os.environ.get("MOKU_TICK_SECONDS", default)) | |
| def _play_btn_update(playing: bool) -> dict: | |
| return gr.update( | |
| value="⏹ Stop" if playing else "▶ Play", | |
| variant="primary" if playing else "secondary", | |
| ) | |
| def _panel_html(state: Any) -> str: | |
| return render_side_panel( | |
| transcript=render_transcript(state), | |
| dictionary=render_dictionary(state), | |
| trust=render_trust(state), | |
| deception=render_deception(state), | |
| creatures_html=render_creature_cards(state), | |
| traces=render_traces(state), | |
| evolution=render_language_evolution(state), | |
| field_notes=render_field_notes(state), | |
| glyph_drift=render_glyph_drift_panel(state), | |
| social_graph=render_social_graph(state), | |
| ) | |
| def _build_layers( | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> VisualLayers: | |
| return layers_from_toggles( | |
| trust=trust, | |
| signals=signals, | |
| speech=speech, | |
| actions=actions, | |
| mood=mood, | |
| events=events, | |
| ) | |
| def _effective_playing(state: Any, playing: bool) -> bool: | |
| """Prefer WorldState.playing — timer may read stale gr.State during slow handlers.""" | |
| if hasattr(state, "playing"): | |
| return bool(state.playing) | |
| return playing | |
| def _world_html(state: Any, playing: bool, layers: VisualLayers) -> str: | |
| active = _effective_playing(state, playing) | |
| return render_sim_shell(render_world_scene(state, layers), state.watch_mode, active) | |
| def _live_views( | |
| state: Any, | |
| playing: bool, | |
| layers: VisualLayers, | |
| ) -> tuple[str, str, str]: | |
| """World + story + traces — updated every tick. Excludes side panel shell.""" | |
| last_trace = state.trace_log[-1] if state.trace_log else None | |
| active = _effective_playing(state, playing) | |
| return ( | |
| _world_html(state, playing, layers), | |
| render_story_section(state, last_trace, playing=active), | |
| render_traces_panel(state), | |
| ) | |
| def _views( | |
| state: Any, | |
| playing: bool, | |
| layers: VisualLayers, | |
| ) -> tuple[str, str, str, str]: | |
| last_trace = state.trace_log[-1] if state.trace_log else None | |
| active = _effective_playing(state, playing) | |
| return ( | |
| _world_html(state, playing, layers), | |
| render_story_section(state, last_trace, playing=active), | |
| _panel_html(state), | |
| render_traces_panel(state), | |
| ) | |
| def _reset( | |
| watch_mode: str, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[Any, bool, str, str, str, str, int, dict]: | |
| state = create_for_watch_mode(watch_mode) | |
| state.playing = True | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| world, story, panel, traces = _views(state, True, layers) | |
| _write_trace_file(state) | |
| return state, True, world, story, panel, traces, 0, _play_btn_update(True) | |
| def _switch_mode( | |
| watch_mode: str, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[Any, bool, str, str, str, str, int, dict]: | |
| return _reset(watch_mode, trust, signals, speech, actions, mood, events) | |
| def _control_hint(mode: str) -> str: | |
| if mode == "sandbox": | |
| return ( | |
| '<p class="moku-control-hint moku-hint-sandbox">' | |
| "<strong>Sandbox</strong>: curated beats for demos. " | |
| "Forest Chronicle updates each turn; Mind Traces hold exact reasoning." | |
| "</p>" | |
| ) | |
| return ( | |
| '<p class="moku-control-hint moku-hint-emergence">' | |
| "<strong>Wild run</strong>: random world, no scheduled beats. " | |
| "Watch glyphs, chronicle, and traces diverge." | |
| "</p>" | |
| ) | |
| def _sandbox_controls_visible(mode: str) -> tuple[dict, str]: | |
| return ( | |
| gr.update(visible=mode == "sandbox"), | |
| _control_hint(mode), | |
| ) | |
| def _write_trace_file(state: Any) -> str | None: | |
| if not state.trace_log: | |
| return None | |
| payload = export_trace_json(state) | |
| TRACE_DIR.mkdir(parents=True, exist_ok=True) | |
| path = TRACE_DIR / f"{state.world_id}-t{state.turn}.json" | |
| path.write_text(payload, encoding="utf-8") | |
| return str(path) | |
| def _page_layout(page: str) -> tuple[dict, dict]: | |
| on_home = page == "home" | |
| return gr.update(visible=on_home), gr.update(visible=not on_home) | |
| def build_app() -> gr.Blocks: | |
| initial_mode = "sandbox" | |
| initial_page = "home" | |
| initial_state = create_for_watch_mode(initial_mode) | |
| memory_label = get_memory_store().backend_label | |
| model_label = provider_label() | |
| initial_layers = layers_from_toggles( | |
| trust=True, | |
| signals=True, | |
| speech=True, | |
| actions=True, | |
| mood=True, | |
| events=True, | |
| ) | |
| world0, story0, panel0, traces0 = _views(initial_state, True, initial_layers) | |
| home0 = render_homepage(model_label, memory_label) | |
| js_inline = JS_PATH.read_text(encoding="utf-8") if JS_PATH.exists() else "" | |
| css = CSS_PATH.read_text(encoding="utf-8") if CSS_PATH.exists() else "" | |
| with gr.Blocks( | |
| title="Moku: The First Word", | |
| fill_height=True, | |
| elem_classes=["moku-root"], | |
| css=css, | |
| theme=gr.themes.Base( | |
| font=gr.themes.GoogleFont("Source Serif 4"), | |
| font_mono=gr.themes.GoogleFont("IBM Plex Mono"), | |
| ), | |
| ) as demo: | |
| gr.HTML(f"<script>{js_inline}</script>", container=False) | |
| gr.HTML(render_guide_panel(), elem_id="moku-guide-host", container=False) | |
| with gr.Column(elem_classes=["moku-layout"]): | |
| gr.HTML( | |
| f""" | |
| <header class="moku-topbar"> | |
| <div class="moku-brand">Moku: The First Word</div> | |
| <div class="moku-subbrand">{model_label} · memory: {memory_label}</div> | |
| </header> | |
| """, | |
| container=False, | |
| ) | |
| with gr.Row(elem_classes=["moku-nav-row"]): | |
| page = gr.Radio( | |
| choices=[("Home", "home"), ("Watch Forest", "sim")], | |
| value=initial_page, | |
| label="", | |
| elem_id="moku-page-nav", | |
| ) | |
| with gr.Column(visible=True, elem_classes=["moku-home-col"]) as home_col: | |
| home_view = gr.HTML(value=home0, elem_classes=["moku-home-wrap"], container=False) | |
| enter_btn = gr.Button("Enter the forest →", elem_classes=["home-enter-btn"]) | |
| with gr.Column(visible=False, elem_classes=["moku-sim-col"]) as sim_col: | |
| with gr.Row(elem_classes=["moku-mode-row"]): | |
| watch_mode = gr.Radio( | |
| choices=[("Sandbox", "sandbox"), ("Wild run", "emergence")], | |
| value=initial_mode, | |
| label="", | |
| elem_id="moku-watch-mode", | |
| ) | |
| with gr.Row(elem_classes=["moku-overlay-row"]): | |
| ov_speech = gr.Checkbox(label="Speech", value=True, elem_id="ov-speech") | |
| ov_signals = gr.Checkbox(label="Signals", value=True, elem_id="ov-signals") | |
| ov_trust = gr.Checkbox(label="Trust", value=True, elem_id="ov-trust") | |
| ov_actions = gr.Checkbox(label="Actions", value=True, elem_id="ov-actions") | |
| ov_mood = gr.Checkbox(label="Mood", value=True, elem_id="ov-mood") | |
| ov_events = gr.Checkbox(label="Events", value=True, elem_id="ov-events") | |
| overlay_inputs = [ | |
| ov_trust, | |
| ov_signals, | |
| ov_speech, | |
| ov_actions, | |
| ov_mood, | |
| ov_events, | |
| ] | |
| with gr.Row(elem_classes=["moku-controls-row"]): | |
| play_btn = gr.Button("⏹ Stop", variant="primary", size="sm") | |
| guide_btn = gr.Button("? Guide", size="sm", elem_id="moku-guide-btn") | |
| obs_btn = gr.Button("☰ Details", size="sm", elem_id="moku-notes-btn") | |
| trace_btn = gr.Button("⬇ JSON", size="sm") | |
| control_hint = gr.HTML(_control_hint(initial_mode), container=False) | |
| sim_view = gr.HTML(value=world0, elem_classes=["moku-sim-viewport"], container=False) | |
| story_view = gr.HTML(value=story0, elem_classes=["moku-story-wrap"], container=False) | |
| traces_view = gr.HTML(value=traces0, elem_classes=["moku-traces-wrap"], container=False) | |
| panel_view = gr.HTML(value=panel0, elem_id="moku-panel-host", container=False) | |
| trace_download = gr.DownloadButton("⬇ traces.json", visible=False, size="sm") | |
| state = gr.State(initial_state) | |
| playing = gr.State(True) | |
| tick_count = gr.State(0) | |
| timer = gr.Timer(value=_llm_timer_interval(), active=True) | |
| sim_outputs = [sim_view, story_view, panel_view, traces_view] | |
| sim_live_outputs = [sim_view, story_view, traces_view] | |
| tick_outputs = [ | |
| state, | |
| *sim_live_outputs, | |
| tick_count, | |
| playing, | |
| play_btn, | |
| ] | |
| def refresh_sim_views( | |
| s: Any, | |
| p: bool, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[str, str, str, str]: | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| return _views(s, p, layers) | |
| def on_page_change( | |
| pg: str, | |
| s: Any, | |
| p: bool, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[dict, dict, str, str, str, str, str]: | |
| home_up, sim_up = _page_layout(pg) | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| w, st, pn, tr = _views(s, p if pg == "sim" else False, layers) | |
| return home_up, sim_up, pg, w, st, pn, tr | |
| def go_to_sim( | |
| s: Any, | |
| p: bool, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[dict, dict, str, str, str, str, str]: | |
| home_up, sim_up = _page_layout("sim") | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| w, st, pn, tr = _views(s, p, layers) | |
| return home_up, sim_up, "sim", w, st, pn, tr | |
| def on_timer_tick( | |
| s: Any, | |
| p: bool, | |
| mode: str, | |
| tc: int, | |
| pg: str, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[Any, str | dict, str | dict, str | dict, int, bool, dict]: | |
| p = _effective_playing(s, p) | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| btn = _play_btn_update(p) | |
| skip_views = gr.update() | |
| if pg != "sim" or not p: | |
| _write_trace_file(s) | |
| return s, skip_views, skip_views, skip_views, tc, p, btn | |
| if not _tick_lock.acquire(blocking=False): | |
| return s, skip_views, skip_views, skip_views, tc, p, btn | |
| try: | |
| tc += 1 | |
| if mode == "emergence" and tc % 2 != 0: | |
| _write_trace_file(s) | |
| return s, skip_views, skip_views, skip_views, tc, p, btn | |
| s = step_world(s) | |
| world, story, traces = _live_views(s, p, layers) | |
| _write_trace_file(s) | |
| return s, world, story, traces, tc, p, btn | |
| finally: | |
| _tick_lock.release() | |
| def toggle_play_pause( | |
| p: bool, | |
| s: Any, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[bool, dict, Any, str, str, str, str]: | |
| new_playing = not _effective_playing(s, p) | |
| s.playing = new_playing | |
| if new_playing: | |
| s.run_summary = None | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| w, st, pn, tr = _views(s, new_playing, layers) | |
| return new_playing, _play_btn_update(new_playing), s, w, st, pn, tr | |
| def toggle_play_epilogue( | |
| s: Any, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> tuple[Any, str, str, str, str]: | |
| if not s.playing and s.run_summary is None and s.turn >= 1: | |
| s = generate_run_summary(s) | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| w, st, pn, tr = _views(s, s.playing, layers) | |
| return s, w, st, pn, tr | |
| def refresh_panel_view( | |
| s: Any, | |
| trust: bool, | |
| signals: bool, | |
| speech: bool, | |
| actions: bool, | |
| mood: bool, | |
| events: bool, | |
| ) -> str: | |
| layers = _build_layers(trust, signals, speech, actions, mood, events) | |
| return _panel_html(s) | |
| def export_traces(s: Any) -> dict: | |
| path = _write_trace_file(s) | |
| if not path: | |
| return gr.update(visible=False) | |
| return gr.update(value=path, visible=True) | |
| timer.tick( | |
| on_timer_tick, | |
| inputs=[state, playing, watch_mode, tick_count, page, *overlay_inputs], | |
| outputs=tick_outputs, | |
| show_progress="hidden", | |
| ) | |
| page.change( | |
| on_page_change, | |
| inputs=[page, state, playing, *overlay_inputs], | |
| outputs=[home_col, sim_col, page, *sim_outputs], | |
| show_progress="hidden", | |
| ) | |
| enter_btn.click( | |
| go_to_sim, | |
| inputs=[state, playing, *overlay_inputs], | |
| outputs=[home_col, sim_col, page, *sim_outputs], | |
| show_progress="hidden", | |
| ) | |
| for ov in overlay_inputs: | |
| ov.change( | |
| refresh_sim_views, | |
| inputs=[state, playing, *overlay_inputs], | |
| outputs=sim_outputs, | |
| show_progress="hidden", | |
| ) | |
| watch_mode.change( | |
| _switch_mode, | |
| inputs=[watch_mode, *overlay_inputs], | |
| outputs=[ | |
| state, | |
| playing, | |
| *sim_outputs, | |
| tick_count, | |
| play_btn, | |
| ], | |
| show_progress="hidden", | |
| ).then( | |
| _sandbox_controls_visible, | |
| inputs=[watch_mode], | |
| outputs=[obs_btn, control_hint], | |
| ).then( | |
| fn=None, | |
| js="() => { document.getElementById('moku-side-panel')?.classList.remove('open'); sessionStorage.setItem('moku-panel-open', '0'); }", | |
| ) | |
| trace_btn.click( | |
| export_traces, | |
| inputs=[state], | |
| outputs=[trace_download], | |
| ) | |
| play_btn.click( | |
| toggle_play_pause, | |
| inputs=[playing, state, *overlay_inputs], | |
| outputs=[playing, play_btn, state, *sim_outputs], | |
| show_progress="hidden", | |
| ).then( | |
| toggle_play_epilogue, | |
| inputs=[state, *overlay_inputs], | |
| outputs=[state, *sim_outputs], | |
| show_progress="hidden", | |
| ) | |
| obs_btn.click( | |
| refresh_panel_view, | |
| inputs=[state, *overlay_inputs], | |
| outputs=[panel_view], | |
| ).then( | |
| fn=None, | |
| js="""() => { | |
| const panel = document.getElementById('moku-side-panel'); | |
| if (!panel) return; | |
| const open = !panel.classList.contains('open'); | |
| panel.classList.toggle('open', open); | |
| sessionStorage.setItem('moku-panel-open', open ? '1' : '0'); | |
| }""", | |
| ) | |
| guide_btn.click( | |
| fn=None, | |
| js="() => { const g = document.getElementById('moku-guide-panel'); if (g) { g.classList.toggle('open'); sessionStorage.setItem('moku-guide-open', g.classList.contains('open') ? '1' : '0'); } }", | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| warmup_local_backend() | |
| app = build_app() | |
| app.queue(default_concurrency_limit=1) | |
| port_env = os.environ.get("GRADIO_SERVER_PORT") | |
| host_env = os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1") | |
| launch_kwargs: dict[str, Any] = { | |
| "server_name": host_env, | |
| "show_error": True, | |
| # HF Spaces enable Gradio SSR by default; it 404s on Consolas/system font woff2 paths. | |
| "ssr_mode": False, | |
| } | |
| if port_env: | |
| launch_kwargs["server_port"] = int(port_env) | |
| app.launch(**launch_kwargs) | |