""" app.py — Gradio 6 Space entry point for Immersive Vibe Development Studio. Streaming bridge: hidden gr.Textbox (elem_id="studio-data") observed by a MutationObserver + input/change event listeners in the Three.js page. """ import json import re import shutil import tempfile import threading import time from pathlib import Path import gradio as gr from pipeline import Pipeline, MODELS from studio import build_studio_html AVAILABLE_MODELS = MODELS # ── Mobile guard HTML ───────────────────────────────────────────────────────── MOBILE_HTML = """
🎮

Immersive Vibe Development Studio

Best experienced on a desktop browser (≥ 1024 px wide).
Rotate your device or switch to a larger screen.

""" MOBILE_CHECK_JS = """ """ # ── Streaming bridge JS — forwards data_out changes into studio iframe ──────── BRIDGE_JS = """ """ def _wrap_in_iframe(studio_html: str) -> str: """Wrap self-contained studio HTML in a srcdoc iframe. Three.js and characters.js are inlined, so no external requests needed. """ import html as html_mod escaped = html_mod.escape(studio_html, quote=True) return ( f'' ) # ── Background temp-dir cleanup daemon ─────────────────────────────────────── def _cleanup_loop() -> None: import glob while True: time.sleep(15 * 60) for d in glob.glob(tempfile.gettempdir() + "/ivds_*"): try: age = time.time() - Path(d).stat().st_mtime if age > 3600: shutil.rmtree(d, ignore_errors=True) except OSError: pass threading.Thread(target=_cleanup_loop, daemon=True).start() # ── Active cancel flags (session_hash → Event) ─────────────────────────────── _cancel_flags: dict[str, threading.Event] = {} # ── Pipeline generator ──────────────────────────────────────────────────────── def run_pipeline( trigger: str, selected_models: list[str], oauth_token: gr.OAuthToken | None = None, request: gr.Request | None = None, ): if not oauth_token or not oauth_token.token: yield json.dumps({"type": "error", "text": "Please log in with your HF account first."}), None return if not trigger or not trigger.strip(): yield json.dumps({"type": "error", "text": "Enter a 2-word trigger."}), None return if len(selected_models) < 2: yield json.dumps({"type": "error", "text": "Select at least 2 models."}), None return session_key = getattr(request, "session_hash", "default") if request else "default" cancel_flag = threading.Event() _cancel_flags[session_key] = cancel_flag pipeline = Pipeline(trigger.strip(), selected_models, oauth_token.token, cancel_flag) try: for chunk in pipeline.run(): yield chunk, None finally: _cancel_flags.pop(session_key, None) if cancel_flag.is_set(): return # Generation complete — build zip and expose download zip_bytes, zip_name = pipeline.build_zip() tmp = tempfile.NamedTemporaryFile( prefix="ivds_", suffix=".zip", delete=False, dir=tempfile.gettempdir() ) tmp.write(zip_bytes) tmp.close() yield json.dumps({"type": "done"}), tmp.name def cancel_pipeline(request: gr.Request | None = None): session_key = getattr(request, "session_hash", "default") if request else "default" flag = _cancel_flags.get(session_key) if flag: flag.set() # ── Build studio iframe from model list ────────────────────────────────────── def _build_studio_iframe(selected_models: list[str]) -> str: """Build the studio iframe HTML for the given model list.""" import threading as _t pipeline_tmp = Pipeline("preview demo", selected_models, "placeholder", _t.Event()) assignments = pipeline_tmp.get_model_assignments() raw_html = build_studio_html(assignments) return _wrap_in_iframe(raw_html) # ── Start generation: rebuild studio with actual models ────────────────────── def start_generation( trigger: str, selected_models: list[str], oauth_token: gr.OAuthToken | None = None, ): if not oauth_token or not oauth_token.token: return gr.update(), gr.update(visible=False) iframe = _build_studio_iframe(selected_models) return iframe, gr.update(visible=False) # ── Generate button enable/disable ─────────────────────────────────────────── _TRIGGER_RE = re.compile(r'^[a-zA-ZÀ-ÿ]{2,20}[\s\-][a-zA-ZÀ-ÿ]{2,20}$') def update_generate_btn(trigger: str, models: list[str]): valid_trigger = bool(_TRIGGER_RE.match((trigger or "").strip())) valid_models = 2 <= len(models) <= 5 return gr.update(interactive=(valid_trigger and valid_models)) # ── View toggle ─────────────────────────────────────────────────────────────── def toggle_view(current: str): new_mode = "immersive" if current == "compact" else "compact" label = "⤢ Immersive" if new_mode == "compact" else "⤡ Compact" hide_controls = new_mode == "immersive" return new_mode, gr.update(value=label), gr.update(visible=not hide_controls) # ── Gradio UI ───────────────────────────────────────────────────────────────── CSS = """ #generate-btn { background: #7c3aed !important; color: white !important; } #stop-btn { background: #dc2626 !important; color: white !important; } #ivds-studio { border: none !important; background: transparent !important; padding: 0 !important; } .gradio-container { max-width: 100% !important; } """ with gr.Blocks(title="🎮 Immersive Vibe Development Studio") as demo: view_mode = gr.State("compact") # ── Mobile notice (hidden by default, shown by JS if narrow) ───────────── gr.HTML(f'') gr.HTML(MOBILE_CHECK_JS) with gr.Column(elem_id="ivds-main"): # ── Top bar ─────────────────────────────────────────────────────────── with gr.Row(): gr.HTML("🎮 Immersive Vibe Development Studio") toggle_btn = gr.Button("⤢ Immersive", size="sm", scale=0) # ── Main content ────────────────────────────────────────────────────── with gr.Row(): # Left: controls with gr.Column(scale=4, elem_id="ivds-controls") as controls_col: gr.LoginButton() trigger_box = gr.Textbox( label="2-Word Game Trigger", placeholder="jungle monkey", info="theme + hero, e.g. 'space robot', 'medieval knight', 'ocean dolphin'", ) model_selector = gr.CheckboxGroup( choices=AVAILABLE_MODELS, value=AVAILABLE_MODELS[:2], label="Select AI Models (2–5)", ) gr.HTML("

" "⏱ ~15–30 min depending on model speed

") with gr.Row(): generate_btn = gr.Button( "🚀 Generate Game", variant="primary", elem_id="generate-btn", interactive=False, ) stop_btn = gr.Button("⏹ Stop", elem_id="stop-btn", scale=0) download_file = gr.File(label="⬇ Download Game ZIP", visible=False) # Right: studio scene (pre-rendered with default models) with gr.Column(scale=6): _default_iframe = _build_studio_iframe(AVAILABLE_MODELS[:2]) studio_html = gr.HTML( value=_default_iframe, elem_id="ivds-studio", ) # Hidden streaming bridge textbox data_out = gr.Textbox(visible=False, elem_id="studio-data") # Inject bridge JS gr.HTML(BRIDGE_JS) # ── Wiring ──────────────────────────────────────────────────────────────── # Enable/disable generate button on input change trigger_box.change(update_generate_btn, inputs=[trigger_box, model_selector], outputs=[generate_btn]) model_selector.change(update_generate_btn, inputs=[trigger_box, model_selector], outputs=[generate_btn]) # Single chain: inject studio → stream pipeline (Fix 4) _gen = generate_btn.click( fn=start_generation, inputs=[trigger_box, model_selector], outputs=[studio_html, download_file], ).then( fn=run_pipeline, inputs=[trigger_box, model_selector], outputs=[data_out, download_file], show_progress=False, ) # Stop cancels the chain (Fix 4) stop_btn.click(fn=cancel_pipeline, outputs=[], queue=False) stop_btn.click(fn=None, cancels=[_gen]) # View toggle with actual column visibility (Fix 6) toggle_btn.click( fn=toggle_view, inputs=[view_mode], outputs=[view_mode, toggle_btn, controls_col], ) if __name__ == "__main__": demo.launch(css=CSS, ssr_mode=False)