| from __future__ import annotations |
|
|
| import inspect |
| import os |
| import re |
| import socket |
| from pathlib import Path |
|
|
| import gradio as gr |
|
|
|
|
| ROOT = Path(__file__).resolve().parent |
| DIST_DIR = ROOT / "dist" |
| ASSETS_DIR = DIST_DIR / "assets" |
| PATCHED_DIR = ROOT / ".gradio_static" |
| PORT = int(os.environ.get("GRADIO_PORT", "7899")) |
|
|
|
|
| def get_free_port() -> int: |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| sock.bind(("127.0.0.1", 0)) |
| return int(sock.getsockname()[1]) |
|
|
|
|
| def file_url(path: Path) -> str: |
| return f"/gradio_api/file={path.resolve()}" |
|
|
|
|
| def patched_js_url(index_html: str) -> str: |
| match = re.search(r'src="/assets/([^"]+\.js)"', index_html) |
| if not match: |
| raise RuntimeError("Could not find built JS bundle in dist/index.html") |
|
|
| source_path = ASSETS_DIR / match.group(1) |
| source = source_path.read_text() |
|
|
| |
| |
| worker_match = re.search(r'worker-[^"]+\.js', source) |
| if worker_match: |
| worker_path = ASSETS_DIR / worker_match.group(0) |
| source = source.replace(f'"/assets/{worker_path.name}"', f'"{file_url(worker_path)}"') |
|
|
| source = source.replace('"logo.png"', f'"{file_url(DIST_DIR / "logo.png")}"') |
| source = source.replace('"banner.png"', f'"{file_url(DIST_DIR / "banner.png")}"') |
| source = source.replace('"yeSound.mp3"', f'"{file_url(DIST_DIR / "yeSound.mp3")}"') |
| for _img in ("Image1.jpg", "Image2.jpg", "Image3.jpg"): |
| source = source.replace(f'"{_img}"', f'"{file_url(DIST_DIR / _img)}"') |
|
|
| |
| |
| |
| |
| |
| patched_path = ASSETS_DIR / (source_path.stem + ".patched.js") |
| patched_path.write_text(source) |
| return file_url(patched_path) |
|
|
|
|
| def patched_index_url() -> str: |
| index_path = DIST_DIR / "index.html" |
| if not index_path.exists(): |
| raise RuntimeError("Missing dist/index.html. Run `npm run build` first.") |
|
|
| PATCHED_DIR.mkdir(exist_ok=True) |
| index_html = index_path.read_text() |
| |
| |
| |
| index_html = index_html.replace( |
| "<head>", |
| '<head>\n <meta http-equiv="Cache-Control" content="no-store" />', |
| 1, |
| ) |
| index_html = re.sub( |
| r'<script type="module" crossorigin src="/assets/[^"]+\.js"></script>', |
| f'<script type="module" crossorigin src="{patched_js_url(index_html)}"></script>', |
| index_html, |
| ) |
| index_html = re.sub( |
| r'href="/assets/([^"]+)"', |
| lambda m: f'href="{file_url(ASSETS_DIR / m.group(1))}"', |
| index_html, |
| ) |
| index_html = index_html.replace('href="/logo.png"', f'href="{file_url(DIST_DIR / "logo.png")}"') |
| index_html = index_html.replace('src="logo.png"', f'src="{file_url(DIST_DIR / "logo.png")}"') |
| index_html = index_html.replace('src="banner.png"', f'src="{file_url(DIST_DIR / "banner.png")}"') |
|
|
| patched_index = PATCHED_DIR / "index.html" |
| patched_index.write_text(index_html) |
| return file_url(patched_index) |
|
|
|
|
| def build_ui() -> gr.Blocks: |
| css = """ |
| html, body, #root, gradio-app, .gradio-container { |
| width: 100vw !important; |
| max-width: 100vw !important; |
| height: 100%; |
| margin: 0 !important; |
| padding: 0 !important; |
| overflow: hidden !important; |
| background: #fff1a8 !important; |
| } |
| .gradio-container { |
| max-width: none !important; |
| } |
| main.app, .main, .contain, .wrap, .block, .html-container, .html-container.padding, |
| .prose, #component-0, #component-0 > div, .form { |
| width: 100vw !important; |
| max-width: 100vw !important; |
| flex-basis: 100vw !important; |
| align-self: stretch !important; |
| height: 100% !important; |
| min-height: 100% !important; |
| margin: 0 !important; |
| padding: 0 !important; |
| overflow: hidden !important; |
| transform: none !important; |
| } |
| .spreadsheet-host { |
| position: relative; |
| width: 100vw !important; |
| max-width: 100vw !important; |
| height: 100vh; |
| height: 100dvh; |
| margin: 0; |
| padding: 0; |
| overflow: hidden; |
| background: white; |
| } |
| .spreadsheet-frame { |
| position: absolute; |
| inset: 0; |
| width: 100%; |
| height: 100%; |
| border: 0; |
| display: block; |
| background: white; |
| } |
| .yellow-tint { |
| position: absolute; |
| inset: 0; |
| pointer-events: none; |
| z-index: 9999; |
| background: rgba(255, 232, 83, 0.34); |
| mix-blend-mode: multiply; |
| } |
| footer { |
| display: none !important; |
| } |
| """ |
|
|
| |
| |
| |
| head = """ |
| <script> |
| (function () { |
| // Once we learn the parent's real visible viewport height (from the embed's |
| // iframe-resizer), we lock EVERYTHING to exactly that many pixels. Until then |
| // we fall back to 100dvh / a screen-height cap. dvh and the screen cap are |
| // both LARGER than the embed's visible area, so the content overflows the |
| // viewport, the parent scrolls, and the intro/wizard appears "moved down" and |
| // twitches as it settles. A fixed pixel lock removes that mismatch entirely. |
| var pinnedH = 0; |
| function hostHeightCss() { |
| return pinnedH > 0 |
| ? ('height:' + pinnedH + 'px;min-height:' + pinnedH + 'px;max-height:' + pinnedH + 'px') |
| : 'height:100vh;height:100dvh'; |
| } |
| function lockLayout() { |
| document.documentElement.style.overflow = 'hidden'; |
| document.documentElement.style.height = pinnedH > 0 ? (pinnedH + 'px') : '100%'; |
| document.body.style.overflow = 'hidden'; |
| document.body.style.height = pinnedH > 0 ? (pinnedH + 'px') : '100%'; |
| document.body.style.margin = '0'; |
| // Hard cap the page height so the resizer cannot inflate it. Use the real |
| // parent viewport height once known; before that, fall back to one screen |
| // (this is what caused the initial drift + having to scroll down before it |
| // self-corrected). |
| var _cap = (pinnedH > 0 ? pinnedH : ((window.screen && window.screen.height) ? window.screen.height : 1200)) + 'px'; |
| document.documentElement.style.maxHeight = _cap; |
| document.body.style.maxHeight = _cap; |
| var host = document.querySelector('.spreadsheet-host'); |
| var f = document.querySelector('.spreadsheet-frame'); |
| var t = document.querySelector('.yellow-tint'); |
| var selectors = [ |
| 'html', 'body', '#root', 'gradio-app', '.gradio-container', |
| 'main.app', '.main', '.contain', '.wrap', '.block', |
| '.html-container', '.html-container.padding', '.prose', |
| '#component-0', '#component-0 > div', '.form' |
| ]; |
| selectors.forEach(function (selector) { |
| document.querySelectorAll(selector).forEach(function (el) { |
| el.style.setProperty('width', '100vw', 'important'); |
| el.style.setProperty('max-width', '100vw', 'important'); |
| el.style.setProperty('margin', '0', 'important'); |
| el.style.setProperty('padding', '0', 'important'); |
| el.style.setProperty('overflow', 'hidden', 'important'); |
| el.style.setProperty('transform', 'none', 'important'); |
| }); |
| }); |
| if (host) { |
| host.setAttribute('data-iframe-height', ''); |
| host.style.cssText = 'position:relative;width:100vw!important;max-width:100vw!important;' + hostHeightCss() + ';margin:0;padding:0;overflow:hidden;background:#fff'; |
| } |
| if (f) { f.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff'; } |
| if (t) { t.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:9999;background:rgba(255,232,83,0.34);mix-blend-mode:multiply'; } |
| } |
| // On the huggingface.co embed, Gradio's iframe-resizer auto-sizes the outer |
| // Space iframe to our measured content. Our 100dvh layout then equals that |
| // iframe height -> feedback loop -> the iframe grows and the page drifts |
| // down. The fix: turn auto-resize OFF (once) and pin the iframe to the |
| // PARENT viewport height (independent of our content, so no loop, no twitch). |
| var pinned = false; |
| function pinParentFrame() { |
| if (pinned || !window.parentIFrame) return; |
| pinned = true; |
| try { window.parentIFrame.autoResize(false); } catch (e) {} |
| function applyParentViewport(info) { |
| var h = Math.round(Number(info && info.clientHeight) || 0); |
| if (h > 200) { |
| pinnedH = h; |
| try { window.parentIFrame.size(h); } catch (e) {} |
| // Re-lock html/body/host to the exact visible height. Fires on every |
| // parent scroll/resize, so the layout (and the fixed wizard overlay |
| // inside the inner iframe) tracks the real viewport with no drift. |
| lockLayout(); |
| } |
| } |
| if (typeof window.parentIFrame.getPageInfo === 'function') { |
| // Fires now and again whenever the parent scrolls/resizes — always with |
| // the parent viewport height, so the size we set never changes due to us. |
| try { window.parentIFrame.getPageInfo(applyParentViewport); } catch (e) {} |
| } |
| // Undo any scroll drift that happened before we pinned: snap the parent |
| // back so the app is at the top, no manual scrolling needed. |
| function toTop() { try { window.parentIFrame.scrollToOffset(0, 0); } catch (e) {} } |
| toTop(); |
| setTimeout(toTop, 250); |
| setTimeout(toTop, 700); |
| } |
| // Poll fast at first so the pin lands within ~100ms (before drift is visible); |
| // lockLayout also re-runs because Gradio renders components async. |
| var n = 0, iv = setInterval(function () { |
| lockLayout(); |
| pinParentFrame(); |
| if (++n > 80) clearInterval(iv); |
| }, 50); |
| document.addEventListener('DOMContentLoaded', lockLayout); |
| window.addEventListener('load', function () { lockLayout(); pinParentFrame(); }); |
| window.addEventListener('resize', lockLayout); |
| })(); |
| </script> |
| """ |
|
|
| |
| |
| |
| blocks_kwargs = {"title": "The Backrooms Spreadsheet", "fill_height": True} |
| if "css" in inspect.signature(gr.Blocks).parameters: |
| blocks_kwargs.update({"css": css, "head": head}) |
|
|
| with gr.Blocks(**blocks_kwargs) as demo: |
| gr.HTML( |
| f""" |
| <div class="spreadsheet-host"> |
| <iframe |
| class="spreadsheet-frame" |
| src="{patched_index_url()}" |
| allow="clipboard-read; clipboard-write; fullscreen; camera; microphone; autoplay; encrypted-media" |
| ></iframe> |
| <div class="yellow-tint"></div> |
| </div> |
| """ |
| ) |
| return demo, css, head |
|
|
|
|
| if __name__ == "__main__": |
| demo, css, head = build_ui() |
| |
| |
| on_space = bool(os.environ.get("SPACE_ID")) |
| if on_space: |
| server_name = "0.0.0.0" |
| server_port = int(os.environ.get("PORT", "7860")) |
| else: |
| server_name = "127.0.0.1" |
| server_port = PORT if "GRADIO_PORT" in os.environ else get_free_port() |
|
|
| print(f"Running The Backrooms Spreadsheet at http://{server_name}:{server_port}", flush=True) |
| launch_kwargs = { |
| "server_name": server_name, |
| "server_port": server_port, |
| "show_error": True, |
| "allowed_paths": [str(DIST_DIR), str(PATCHED_DIR)], |
| } |
| launch_params = inspect.signature(demo.launch).parameters |
| if "ssr_mode" in launch_params: |
| launch_kwargs["ssr_mode"] = False |
| if "css" in launch_params: |
| launch_kwargs["css"] = css |
| if "head" in launch_params: |
| launch_kwargs["head"] = head |
| demo.launch(**launch_kwargs) |
|
|