File size: 12,382 Bytes
94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 4f032fd da5077d 4f032fd da5077d 4f032fd da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 da5077d 94c4245 4f032fd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | """
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 = """
<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>
"""
# โโ Streaming bridge JS โ forwards data_out changes into studio iframe โโโโโโโโ
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>'
)
# โโ 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'<div id="ivds-mobile-notice" style="display:none">{MOBILE_HTML}</div>')
gr.HTML(MOBILE_CHECK_JS)
with gr.Column(elem_id="ivds-main"):
# โโ Top bar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
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)
# โโ 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("<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)
# 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)
|