Spaces:
Sleeping
Sleeping
| #!/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) |