#!/usr/bin/env python3 """Gradio demo — Universal π Clock · Egg of Life.""" from __future__ import annotations import os import time # HF enables SSR by default; it spawns a Node proxy and triggers asyncio # teardown warnings (Invalid file descriptor: -1) on cpu-basic. Disable early. os.environ.setdefault("GRADIO_SSR_MODE", "False") import matplotlib matplotlib.use("Agg") from universal_clock import UniversalPiClock, render_clock_array from universal_clock.clock import EARTH_DAY_SECONDS, SLICES_PER_GEAR from universal_clock.visualize import ( DEFAULT_SLICE_LINES, PETAL_CYCLE_SECONDS, PETAL_SEGMENT_SECONDS, petal_color_gear, petal_frame_index, ) import gradio as gr GITHUB_URL = "https://github.com/kinaar8340/universal_clock" HF_SPACE_URL = "https://huggingface.co/spaces/kinaar111/universal_clock" SLICE_LINE_CHOICES = [10, 35, 70, 175, 350] # Named demo scenes — each loads a fresh clock at the given tick count DEMO_SCENES: dict[str, tuple[int, str]] = { "Start": (0, "All seven gears at k=1 — the origin."), "PoC · 5k": (5000, "G1≈29% filled, G2=15 — classic proof-of-concept."), "Carry · 50k": (50000, "Carry propagates — watch G3+ advance."), "Deep · 500k": (500000, "Deep hierarchy — stress-test all seven gears."), } PETAL_SCENE = "Petal · 60s" DEMO_SCENES[PETAL_SCENE] = ( -1, "60s loop: 18 frames · G1 full 350/π revolution · color G2→G7.", ) # cpu-basic: render only when the petal frame changes (~3.33s); 1 Hz poll is enough. PETAL_RENDER_INTERVAL = 1.0 RENDER_DPI = 90 SCENE_CHOICES = list(DEMO_SCENES.keys()) DEFAULT_DEMO = PETAL_SCENE PANEL_HEIGHT = 860 CLOCK_VIEWPORT_SPAN = 2.12 CONTROLS_WIDTH = 220 def _format_state( clock: UniversalPiClock, *, petal_elapsed: float | None = None, ) -> str: lines: list[str] = [f"Total ticks: {clock.total_ticks:,}"] if petal_elapsed is not None: t = petal_elapsed % PETAL_CYCLE_SECONDS seg = int(t // PETAL_SEGMENT_SECONDS) g = petal_color_gear(t) frame = petal_frame_index(t) + 1 lines.append( f"Petal: {t:4.1f}s / {PETAL_CYCLE_SECONDS}s " f"· frame {frame:02d}/18 · color=G{g} " f"· {seg * 10}–{(seg + 1) * 10}s" ) lines.append("") for i in range(1, 8): k = clock.gears[i - 1] fill = 100.0 * k / 350 if petal_elapsed is not None and i == 1: g = petal_color_gear(petal_elapsed) overlay = f"petal→G{g}" elif petal_elapsed is not None: overlay = "hands" else: overlay = "" base = f"Gear {i}: k={k:3d} v≈{clock.value(i):6.2f} ({fill:5.1f}% filled)" lines.append(f"{base} {overlay}" if overlay else base) return "\n".join(lines) def _render( clock: UniversalPiClock, *, slice_lines: int, show_hands: bool, show_labels: bool, show_ticks: bool, petal_elapsed: float | None = None, ) -> tuple: image = render_clock_array( clock, figsize=(10, 10), dpi=RENDER_DPI, show_ticks=show_ticks, show_labels=show_labels, show_hands=show_hands, slice_lines=slice_lines, title="", viewport_span=CLOCK_VIEWPORT_SPAN, tight_margins=True, petal_overlay=petal_elapsed is not None, petal_elapsed=petal_elapsed or 0.0, petal_show_labels=False, ) return image, _format_state(clock, petal_elapsed=petal_elapsed) def _display_kwargs( slice_lines: int, overlays: list[str], ) -> dict: return { "slice_lines": slice_lines, "show_hands": "hands" in overlays, "show_labels": "labels" in overlays, "show_ticks": "ticks" in overlays, } def _new_clock() -> UniversalPiClock: return UniversalPiClock() def load_demo_scene( scene: str, slice_lines: int, overlays: list[str], ) -> tuple: """Load a named demo from scratch (static scenes only).""" clock = _new_clock() ticks, _ = DEMO_SCENES.get(scene, (0, "")) if ticks > 0: clock.fast_forward(ticks) return clock, *_render(clock, **_display_kwargs(slice_lines, overlays)) _petal_last_visual_key: tuple | None = None def _reset_petal_visual_cache() -> None: global _petal_last_visual_key _petal_last_visual_key = None def start_petal_sequence( slice_lines: int, overlays: list[str], ) -> tuple: """Reset and begin the 60s petal loop (1 tick/s, flower frames, color G2→G7).""" global _petal_last_visual_key _reset_petal_visual_cache() clock = _new_clock() elapsed = 0.0 display = _display_kwargs(slice_lines, overlays) image, state = _render(clock, **display, petal_elapsed=elapsed) _petal_last_visual_key = ( petal_frame_index(elapsed), slice_lines, tuple(overlays), ) return clock, True, time.time(), image, state def petal_sequence_tick( clock: UniversalPiClock | None, running: bool, epoch: float, slice_lines: int, overlays: list[str], ) -> tuple: global _petal_last_visual_key if clock is None: clock = _new_clock() if not running: _reset_petal_visual_cache() return ( clock, running, epoch, *_render(clock, **_display_kwargs(slice_lines, overlays)), ) elapsed = (time.time() - epoch) % PETAL_CYCLE_SECONDS # One full 350/π revolution on G1 over 60s (matches gear1_60s_sequence.mp4). target_ticks = min( int(elapsed * SLICES_PER_GEAR / PETAL_CYCLE_SECONDS), SLICES_PER_GEAR - 1, ) if target_ticks < clock.total_ticks: clock = _new_clock() elif target_ticks > clock.total_ticks: clock.tick(target_ticks - clock.total_ticks) display = _display_kwargs(slice_lines, overlays) state = _format_state(clock, petal_elapsed=elapsed) visual_key = ( petal_frame_index(elapsed), slice_lines, tuple(overlays), ) if visual_key == _petal_last_visual_key: return clock, running, epoch, gr.skip(), state _petal_last_visual_key = visual_key image, _ = _render( clock, **display, petal_elapsed=elapsed, ) return clock, running, epoch, image, state def demo_hint(scene: str) -> str: return DEMO_SCENES.get(scene, (0, ""))[1] def step_clock( clock: UniversalPiClock | None, step: int, slice_lines: int, overlays: list[str], ) -> tuple: if clock is None: clock = _new_clock() if step > 0: clock.tick(step) return clock, *_render(clock, **_display_kwargs(slice_lines, overlays)) def reset_clock( slice_lines: int, overlays: list[str], ) -> tuple: clock = _new_clock() return clock, *_render(clock, **_display_kwargs(slice_lines, overlays)) def enable_realtime( clock: UniversalPiClock | None, rev_seconds: float, speed: float, slice_lines: int, overlays: list[str], ) -> tuple: if clock is None: clock = _new_clock() clock.set_earth_rate(rev_seconds) return ( clock, True, speed, *_render(clock, **_display_kwargs(slice_lines, overlays)), ) def disable_realtime( clock: UniversalPiClock | None, slice_lines: int, overlays: list[str], ) -> tuple: if clock is None: clock = _new_clock() return ( clock, False, 1.0, *_render(clock, **_display_kwargs(slice_lines, overlays)), ) def realtime_tick( clock: UniversalPiClock | None, running: bool, speed: float, slice_lines: int, overlays: list[str], ) -> tuple: if clock is None: clock = _new_clock() if running and clock.tick_interval is not None: clock.tick_realtime(speed_multiplier=max(speed, 0.001)) return clock, *_render(clock, **_display_kwargs(slice_lines, overlays)) def refresh_view( clock: UniversalPiClock | None, slice_lines: int, overlays: list[str], petal_running: bool = False, petal_epoch: float = 0.0, ) -> tuple: if clock is None: clock = _new_clock() if petal_running: elapsed = (time.time() - petal_epoch) % PETAL_CYCLE_SECONDS return clock, *_render( clock, **_display_kwargs(slice_lines, overlays), petal_elapsed=elapsed, ) return clock, *_render(clock, **_display_kwargs(slice_lines, overlays)) def _timers_for_scene(scene: str) -> tuple: if scene == PETAL_SCENE: return gr.Timer(active=False), gr.Timer(active=True) return gr.Timer(active=False), gr.Timer(active=False) def _stop_all_timers() -> tuple: return False, False, gr.Timer(active=False), gr.Timer(active=False) THEME = gr.themes.Base( primary_hue="orange", secondary_hue="slate", neutral_hue="slate", ).set( body_background_fill_dark="#0d1117", block_background_fill_dark="#161b22", block_border_width="1px", block_title_text_weight="600", block_padding="4px", layout_gap="6px", ) CUSTOM_CSS = f""" :root {{ --panel-h: clamp(560px, calc(100vh - 7.5rem), {PANEL_HEIGHT}px); --controls-w: {CONTROLS_WIDTH}px; }} html, body, #root, .gradio-container {{ height: 100% !important; max-height: 100vh !important; overflow: hidden !important; }} .gradio-container {{ max-width: 100% !important; width: 100% !important; padding: 0.35rem 0.75rem 0.25rem !important; }} .gradio-container > .main, .gradio-container > .main > .wrap {{ height: 100% !important; max-height: 100vh !important; overflow: hidden !important; }} footer, .footer {{ display: none !important; }} #app-header {{ flex-shrink: 0; margin-bottom: 0 !important; }} #app-header h1 {{ font-size: 1.15rem !important; margin: 0 !important; line-height: 1.25 !important; }} #app-header p {{ font-size: 0.78rem !important; margin: 0.1rem 0 0 !important; opacity: 0.85; }} #main-row {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; min-height: var(--panel-h) !important; flex-wrap: nowrap !important; align-items: stretch !important; overflow: hidden !important; gap: 0.65rem !important; }} #col-controls, #col-clock, #col-state {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; overflow: hidden !important; }} #col-controls {{ display: flex !important; flex-direction: column !important; max-width: var(--controls-w) !important; flex: 0 0 var(--controls-w) !important; padding-right: 0 !important; }} #controls-scroll {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; overflow-x: hidden !important; overflow-y: auto !important; padding-right: 0.25rem !important; scrollbar-width: thin; scrollbar-color: #3d4f5f #161b22; }} #controls-scroll .block {{ margin-bottom: 0.2rem !important; padding: 4px 6px !important; }} #controls-scroll label, #controls-scroll span, #controls-scroll p {{ font-size: 0.75rem !important; }} #controls-scroll input[type="number"] {{ font-size: 0.75rem !important; padding: 1px 3px !important; }} #controls-scroll .gr-button {{ font-size: 0.72rem !important; padding: 3px 6px !important; min-height: 1.6rem !important; }} #controls-scroll .section-header {{ font-size: 0.78rem !important; font-weight: 600 !important; margin: 0.15rem 0 !important; }} #demo-hint {{ font-size: 0.68rem !important; line-height: 1.3 !important; opacity: 0.8; margin: 0 !important; padding: 2px 0 !important; }} #controls-scroll .accordion {{ border: 1px solid #30363d !important; border-radius: 6px !important; }} #controls-scroll .accordion .label-wrap {{ font-size: 0.72rem !important; padding: 4px 6px !important; min-height: unset !important; }} #col-clock {{ flex: 1 1 auto !important; min-width: 0 !important; background: #0d1117 !important; }} #clock-viewport, #clock-viewport > .form, #clock-viewport > .block {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; background: #0d1117 !important; }} #clock-face {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; margin: 0 !important; }} #clock-face .image-container, #clock-face .image-frame, #clock-face [data-testid="image"] {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; min-height: var(--panel-h) !important; width: 100% !important; background: #0d1117 !important; padding: 0 !important; }} #clock-face img {{ width: 100% !important; height: 100% !important; max-height: var(--panel-h) !important; object-fit: contain !important; object-position: center center !important; }} #col-state {{ display: flex !important; flex-direction: column !important; max-width: 340px !important; flex: 0 0 340px !important; }} #gear-state {{ height: var(--panel-h) !important; max-height: var(--panel-h) !important; margin-top: 0 !important; }} #gear-state textarea {{ height: calc(var(--panel-h) - 2.5rem) !important; max-height: calc(var(--panel-h) - 2.5rem) !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important; font-size: 0.82rem !important; line-height: 1.35 !important; overflow-y: auto !important; }} @media (min-width: 1600px) and (min-height: 900px) {{ :root {{ --panel-h: {PANEL_HEIGHT}px; }} }} """ def build_demo() -> gr.Blocks: with gr.Blocks(title="Universal π Clock") as demo: gr.Markdown( f""" # Universal π Clock · Egg of Life Seven-gear cascading π clock — [GitHub]({GITHUB_URL}) · [Space]({HF_SPACE_URL}) """, elem_id="app-header", ) clock_state = gr.State(_new_clock()) realtime_running = gr.State(False) petal_running = gr.State(False) petal_epoch = gr.State(0.0) with gr.Row(elem_id="main-row", equal_height=True, height=PANEL_HEIGHT): with gr.Column(scale=1, min_width=CONTROLS_WIDTH, elem_id="col-controls"): with gr.Column(elem_id="controls-scroll", variant="compact"): gr.Markdown("**1 · Demo**", elem_classes=["section-header"]) demo_scene = gr.Dropdown( choices=SCENE_CHOICES, value=DEFAULT_DEMO, label="Scene", show_label=False, ) demo_hint_box = gr.Markdown( demo_hint(DEFAULT_DEMO), elem_id="demo-hint", ) reload_demo_btn = gr.Button("Load scene", variant="primary", size="sm") gr.Markdown("**2 · View**", elem_classes=["section-header"]) slice_lines = gr.Dropdown( choices=SLICE_LINE_CHOICES, value=DEFAULT_SLICE_LINES, label="Slice lines", ) overlays = gr.CheckboxGroup( choices=[ ("Hands", "hands"), ("k/π labels", "labels"), ("Slice ticks", "ticks"), ], value=["hands", "labels", "ticks"], label="Overlays", ) with gr.Accordion("3 · Manual step", open=False): step_ticks = gr.Number( value=1000, precision=0, label="Ticks per step", minimum=1, maximum=500000, ) with gr.Row(): step_btn = gr.Button("+ Step", size="sm") reset_btn = gr.Button("Reset", size="sm") with gr.Accordion("4 · Live Earth rate", open=False): rev_seconds = gr.Slider( minimum=3600, maximum=200000, value=EARTH_DAY_SECONDS, step=3600, label="Gear 1 day (s)", ) speed = gr.Slider( minimum=1, maximum=10000, value=1000, step=10, label="Speed ×", ) with gr.Row(): start_rt = gr.Button("▶ Start", size="sm") stop_rt = gr.Button("■ Stop", size="sm") with gr.Column(scale=11, min_width=640, elem_id="col-clock"): with gr.Column(elem_id="clock-viewport"): clock_image = gr.Image( elem_id="clock-face", interactive=False, show_label=False, height=PANEL_HEIGHT, buttons=[], ) with gr.Column(scale=3, min_width=280, elem_id="col-state"): state_text = gr.Textbox( label="Gear state", elem_id="gear-state", lines=14, max_lines=14, interactive=False, ) earth_timer = gr.Timer(0.15, active=False) petal_timer = gr.Timer(PETAL_RENDER_INTERVAL, active=False) display_inputs = [slice_lines, overlays] outputs = [clock_state, clock_image, state_text] rt_outputs = [clock_state, realtime_running, speed, clock_image, state_text] petal_outputs = [clock_state, petal_running, petal_epoch, clock_image, state_text] view_outputs = [clock_state, clock_image, state_text] anim_stop = [realtime_running, petal_running, earth_timer, petal_timer] def _load_scene_or_petal(scene, *display): if scene == PETAL_SCENE: return start_petal_sequence(*display) clock, img, st = load_demo_scene(scene, *display) return clock, False, 0.0, img, st def _on_app_load(scene, *display): """Defer the first matplotlib render to the petal timer (avoids SSR startup hangs).""" if scene == PETAL_SCENE: clock = _new_clock() state = _format_state(clock, petal_elapsed=0.0) return clock, True, time.time(), gr.skip(), state return _load_scene_or_petal(scene, *display) reload_demo_btn.click( _load_scene_or_petal, inputs=[demo_scene, *display_inputs], outputs=petal_outputs, ).then(lambda: False, outputs=realtime_running).then( _timers_for_scene, inputs=demo_scene, outputs=[earth_timer, petal_timer], ) demo_scene.change( demo_hint, inputs=demo_scene, outputs=demo_hint_box, ).then( _load_scene_or_petal, inputs=[demo_scene, *display_inputs], outputs=petal_outputs, ).then(lambda: False, outputs=realtime_running).then( _timers_for_scene, inputs=demo_scene, outputs=[earth_timer, petal_timer], ) reset_btn.click( reset_clock, inputs=display_inputs, outputs=outputs, ).then(_stop_all_timers, outputs=anim_stop) step_btn.click( step_clock, inputs=[clock_state, step_ticks, *display_inputs], outputs=outputs, ) start_rt.click( enable_realtime, inputs=[clock_state, rev_seconds, speed, *display_inputs], outputs=rt_outputs, ).then(lambda: (False, gr.Timer(active=False), gr.Timer(active=True)), outputs=[ petal_running, petal_timer, earth_timer, ]) stop_rt.click( disable_realtime, inputs=[clock_state, *display_inputs], outputs=rt_outputs, ).then(lambda: gr.Timer(active=False), outputs=earth_timer) for control in (slice_lines, overlays): control.change( refresh_view, inputs=[clock_state, *display_inputs, petal_running, petal_epoch], outputs=view_outputs, ) earth_timer.tick( realtime_tick, inputs=[clock_state, realtime_running, speed, *display_inputs], outputs=view_outputs, show_progress="hidden", ) petal_timer.tick( petal_sequence_tick, inputs=[clock_state, petal_running, petal_epoch, *display_inputs], outputs=petal_outputs, show_progress="hidden", ) demo.load( _on_app_load, inputs=[demo_scene, *display_inputs], outputs=petal_outputs, ).then( _timers_for_scene, inputs=demo_scene, outputs=[earth_timer, petal_timer], ) return demo # HF Spaces imports this module and looks for a top-level `demo` object. demo = build_demo() # Serialize blocking matplotlib renders (timers + load) on cpu-basic. demo.queue(default_concurrency_limit=1) if __name__ == "__main__": demo.launch(theme=THEME, css=CUSTOM_CSS, ssr_mode=False)