| """ |
| 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_HTML = """ |
| <div style="display:flex;align-items:center;justify-content:center; |
| min-height:60vh;background:#1a1a2e;color:#fff; |
| font-family:sans-serif;text-align:center;padding:2rem;"> |
| <div> |
| <div style="font-size:3rem">๐ฎ</div> |
| <h2 style="margin-top:1rem">Immersive Vibe Development Studio</h2> |
| <p style="color:#aaa;margin-top:0.8rem"> |
| Best experienced on a desktop browser (โฅ 1024 px wide).<br> |
| Rotate your device or switch to a larger screen. |
| </p> |
| </div> |
| </div> |
| """ |
|
|
| MOBILE_CHECK_JS = """ |
| <script> |
| (function() { |
| if (window.innerWidth < 1024) { |
| var mob = document.getElementById('ivds-mobile-notice'); |
| if (mob) mob.style.display = 'flex'; |
| var main = document.getElementById('ivds-main'); |
| if (main) main.style.display = 'none'; |
| } |
| })(); |
| </script> |
| """ |
|
|
| |
| BRIDGE_JS = """ |
| <script> |
| (function() { |
| var lastVal = ''; |
| function attachBridge() { |
| var container = document.getElementById('studio-data'); |
| if (!container) { setTimeout(attachBridge, 300); return; } |
| var el = container.querySelector('textarea') || |
| container.querySelector('input[type="text"]') || |
| container; |
| |
| function onUpdate() { |
| var val = el.value !== undefined ? el.value : (el.textContent || ''); |
| if (!val || val === lastVal) return; |
| lastVal = val; |
| try { |
| var msg = JSON.parse(val); |
| var iframe = document.getElementById('ivds-studio-iframe'); |
| if (iframe && iframe.contentWindow) { |
| iframe.contentWindow.postMessage(msg, '*'); |
| } |
| } catch(_) {} |
| } |
| |
| el.addEventListener('input', onUpdate); |
| el.addEventListener('change', onUpdate); |
| new MutationObserver(onUpdate).observe( |
| container, |
| { childList: true, subtree: true, characterData: true, attributes: true } |
| ); |
| } |
| attachBridge(); |
| })(); |
| </script> |
| """ |
|
|
|
|
| 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'<iframe id="ivds-studio-iframe" srcdoc="{escaped}" ' |
| f'style="width:100%;height:600px;border:none;border-radius:8px;background:#1a1a2e" ' |
| f'allow="autoplay" sandbox="allow-scripts"></iframe>' |
| ) |
|
|
| |
| 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() |
|
|
| |
| _cancel_flags: dict[str, threading.Event] = {} |
|
|
|
|
| |
| 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 |
|
|
| |
| 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() |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| _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)) |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| 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") |
|
|
| |
| gr.HTML(f'<div id="ivds-mobile-notice" style="display:none">{MOBILE_HTML}</div>') |
| gr.HTML(MOBILE_CHECK_JS) |
|
|
| with gr.Column(elem_id="ivds-main"): |
|
|
| |
| with gr.Row(): |
| gr.HTML("<span style='color:#fff;font-family:monospace;font-size:1.1rem;" |
| "padding:0.4rem 0'>๐ฎ <strong>Immersive Vibe Development Studio</strong></span>") |
| toggle_btn = gr.Button("โคข Immersive", size="sm", scale=0) |
|
|
| |
| with gr.Row(): |
|
|
| |
| 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("<p style='color:#aaa;font-size:12px;margin-top:4px'>" |
| "โฑ ~15โ30 min depending on model speed</p>") |
|
|
| 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) |
|
|
| |
| with gr.Column(scale=6): |
| _default_iframe = _build_studio_iframe(AVAILABLE_MODELS[:2]) |
| studio_html = gr.HTML( |
| value=_default_iframe, |
| elem_id="ivds-studio", |
| ) |
|
|
| |
| data_out = gr.Textbox(visible=False, elem_id="studio-data") |
|
|
| |
| gr.HTML(BRIDGE_JS) |
|
|
| |
|
|
| |
| 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]) |
|
|
| |
| _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_btn.click(fn=cancel_pipeline, outputs=[], queue=False) |
| stop_btn.click(fn=None, cancels=[_gen]) |
|
|
| |
| 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) |
|
|