Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Download app/mimo/vendor/webdev/shot.py from FineEnvs/RL-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/FineEnvs/RL-Explorer/resolve/main/app/mimo/vendor/webdev/shot.py
- Command line
-
hf download hf://spaces/FineEnvs/RL-Explorer/app/mimo/vendor/webdev/shot.py
-
curl -L -o shot.py https://huggingface.co/spaces/FineEnvs/RL-Explorer/resolve/main/app/mimo/vendor/webdev/shot.py
11.4 kB
| # Copyright 2026 Bytedance Ltd. and/or its affiliates | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """In-pod full-page screenshot pipeline: playwright inside the agent's own sandbox. | |
| Only the EVALUATION path uses this. Training ships the workspace to the grading service and | |
| lets it render; evaluation renders here instead, so that it does not depend on that service | |
| running -- evaluation is usually done in an environment that has no such long-lived service. | |
| ``_SHOT_SCRIPT`` is kept **byte for byte**. Every non-obvious line in it encodes a failure | |
| that was actually hit and diagnosed in a pod: proxy auth handed to the wrong playwright | |
| field, a loopback bypass that the dict API silently drops, blank ``file://`` renders of | |
| runtime-rendered pages, reveal-on-scroll content captured at opacity 0, sticky headers | |
| painted at an in-flight scroll offset, parallax backgrounds going black below the fold. Read | |
| the inline comments before changing any of it; none of these failures announce themselves -- | |
| they all arrive as a plausible-looking screenshot that scores badly. | |
| The sandbox image must carry node, playwright and chromium. A missing chromium surfaces as | |
| ``render_failed``, which the caller masks, rather than as a bad score. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import json | |
| import os | |
| import shlex | |
| _SHOT_SCRIPT = r''' | |
| import sys, json, base64, argparse | |
| from playwright.sync_api import sync_playwright | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--url", required=True) | |
| ap.add_argument("--actions-b64", default="") | |
| ap.add_argument("--proxy", default="") # comma-separated failover chain; empty entry = direct | |
| ap.add_argument("--vw", type=int, default=1440) | |
| ap.add_argument("--vh", type=int, default=900) | |
| ap.add_argument("--serve-root", default="") # serve this dir over local http and rewrite --url onto it | |
| a = ap.parse_args() | |
| if a.serve_root: | |
| import threading, functools | |
| from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler | |
| class _QuietHandler(SimpleHTTPRequestHandler): | |
| def log_message(self, *args, **kw): # keep stdout clean for SHOT_B64 parsing | |
| pass | |
| _handler = functools.partial(_QuietHandler, directory=a.serve_root) | |
| _srv = ThreadingHTTPServer(("127.0.0.1", 0), _handler) | |
| threading.Thread(target=_srv.serve_forever, daemon=True).start() | |
| _page = a.url.rsplit("/", 1)[-1] if a.url.rsplit("/", 1)[-1].endswith(".html") else "index.html" | |
| a.url = "http://127.0.0.1:%d/%s" % (_srv.server_address[1], _page) | |
| actions = json.loads(base64.b64decode(a.actions_b64).decode()) if a.actions_b64 else [] | |
| BASE_LAUNCH = {"headless": True, "args": ["--no-sandbox", "--disable-dev-shm-usage", | |
| "--proxy-bypass-list=127.0.0.1;localhost"]} | |
| def _proxy_cfg(purl): | |
| from urllib.parse import urlparse as _up | |
| _pu = _up(purl) | |
| cfg = {"server": _pu.scheme + "://" + _pu.hostname + ":" + str(_pu.port)} | |
| if _pu.username: | |
| cfg["username"] = _pu.username | |
| cfg["password"] = _pu.password or "" | |
| return cfg | |
| proxy_chain = [s.strip() for s in a.proxy.split(",")] if a.proxy else [""] | |
| if a.proxy and "" not in proxy_chain: | |
| proxy_chain.append("") | |
| FATAL_NET = ("ERR_PROXY_CONNECTION_FAILED", "ERR_TUNNEL_CONNECTION_FAILED", | |
| "ERR_SOCKS_CONNECTION_FAILED", "ERR_NAME_NOT_RESOLVED", | |
| "ERR_INTERNET_DISCONNECTED", "ERR_CONNECTION_TIMED_OUT", | |
| "ERR_CONNECTION_REFUSED", "ERR_CONNECTION_RESET", "ERR_TIMED_OUT", | |
| "ERR_EMPTY_RESPONSE") | |
| errs = [] | |
| fatal = [] # fatal external request failures of the CURRENT load attempt | |
| render_env = {"proxy_failed": False, "recovered": None, "attempts": [], "fatal": []} | |
| def _on_request_failed(req): | |
| try: | |
| u = req.url or "" | |
| if not u.startswith("http") or u.startswith("http://127.0.0.1") or u.startswith("http://localhost"): | |
| return # local server / file:// subresources are page defects, not env | |
| f = req.failure or "" | |
| if any(k in f for k in FATAL_NET): | |
| fatal.append(f + " " + u[:120]) | |
| except Exception: | |
| pass | |
| SETTLE_JS = """async () => { | |
| try { | |
| document.documentElement.style.setProperty('scroll-behavior', 'auto', 'important'); | |
| if (document.body) document.body.style.setProperty('scroll-behavior', 'auto', 'important'); | |
| } catch (e) {} | |
| if (document.fonts && document.fonts.ready) { try { await document.fonts.ready; } catch (e) {} } | |
| const H = () => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); | |
| const step = Math.max(400, Math.floor(window.innerHeight * 0.8)); | |
| for (let y = 0; y <= Math.min(H(), 30000); y += step) { | |
| window.scrollTo(0, y); | |
| await new Promise(r => setTimeout(r, 120)); | |
| } | |
| window.scrollTo(0, H()); | |
| await new Promise(r => setTimeout(r, 400)); | |
| window.scrollTo(0, 0); | |
| await new Promise(r => setTimeout(r, 300)); | |
| }""" | |
| FREEZE_CSS = ("*,*::before,*::after{animation-delay:0s !important;animation-duration:.01s !important;" | |
| "transition-delay:0s !important;transition-duration:.01s !important;" | |
| "background-attachment:scroll !important;scroll-behavior:auto !important;}") | |
| with sync_playwright() as p: | |
| b = None | |
| pg = None | |
| def _open(purl): | |
| global b, pg | |
| if b is not None: | |
| try: | |
| b.close() | |
| except Exception: | |
| pass | |
| kw = dict(BASE_LAUNCH) | |
| if purl: | |
| kw["proxy"] = _proxy_cfg(purl) | |
| b = p.chromium.launch(**kw) | |
| pg = b.new_context(viewport={"width": a.vw, "height": a.vh}).new_page() | |
| pg.on("console", lambda m: errs.append(m.text) if m.type == "error" else None) | |
| pg.on("pageerror", lambda e: errs.append("pageerror: " + str(e))) | |
| pg.on("requestfailed", _on_request_failed) | |
| def _load(): | |
| del fatal[:] | |
| try: | |
| resp = pg.goto(a.url, wait_until="load", timeout=25000) | |
| if resp is not None and a.url.startswith("http") and resp.status >= 500: | |
| fatal.append("PAGE_HTTP_%d %s" % (resp.status, a.url[:120])) | |
| except Exception as e: | |
| errs.append("goto failed: " + str(e)) | |
| pg.wait_for_timeout(2000) | |
| for i, purl in enumerate(proxy_chain): | |
| label = (purl.split("@")[-1] or "direct") if purl else "direct" | |
| try: | |
| _open(purl) | |
| _load() | |
| except Exception as e: | |
| errs.append("launch failed via " + label + ": " + str(e)) | |
| del fatal[:] | |
| fatal.append("LAUNCH_FAILED " + label) | |
| if fatal: | |
| render_env["attempts"].append({"via": label, "fatal": fatal[:3]}) | |
| if pg is not None and not fatal[0].startswith("LAUNCH_FAILED"): | |
| _load() | |
| if not fatal and pg is not None: | |
| if render_env["attempts"]: | |
| render_env["recovered"] = "via " + label | |
| break | |
| render_env["proxy_failed"] = bool(fatal) or pg is None | |
| render_env["fatal"] = fatal[:5] | |
| if pg is None: # every route failed to even open a browser: report env, no shot | |
| sys.stdout.write("CONSOLE_ERRORS:" + json.dumps(errs[:15]) + "\n") | |
| sys.stdout.write("RENDER_ENV:" + json.dumps(render_env) + "\n") | |
| sys.exit(0) | |
| try: | |
| pg.evaluate(SETTLE_JS) | |
| pg.add_style_tag(content=FREEZE_CSS) | |
| pg.wait_for_timeout(300) | |
| except Exception as e: | |
| errs.append("settle failed: " + str(e)) | |
| for act in actions: | |
| t = act.get("type") | |
| try: | |
| if t == "click": | |
| pg.click(act["selector"], timeout=5000) | |
| elif t == "fill": | |
| pg.fill(act["selector"], act.get("value", ""), timeout=5000) | |
| pg.wait_for_timeout(800) | |
| except Exception as e: | |
| errs.append("action " + json.dumps(act) + " failed: " + str(e)) | |
| try: | |
| pg.evaluate("window.scrollTo(0, 0)") | |
| pg.wait_for_timeout(250) | |
| except Exception as e: | |
| errs.append("scroll-top failed: " + str(e)) | |
| png = pg.screenshot(full_page=True, type="jpeg", quality=75) | |
| b.close() | |
| sys.stdout.write("SHOT_B64:" + base64.b64encode(png).decode() + "\n") | |
| sys.stdout.write("CONSOLE_ERRORS:" + json.dumps(errs[:15]) + "\n") | |
| sys.stdout.write("RENDER_ENV:" + json.dumps(render_env) + "\n") | |
| ''' | |
| def _build_shot_cmd(url: str, *, actions_b64: str = "", proxy: str = "") -> str: | |
| """Pod-side command that runs _SHOT_SCRIPT against `url`. Shared by ScreenshotTool | |
| (feeds the multimodal grader) and capture_fullpage (lands the JPEG to disk). | |
| ``WEBDEV_GRADE_HTTP=1`` switches ``file://`` rendering to a loopback http server inside | |
| the shot script (see ``--serve-root``) so runtime-rendered pages actually execute. **The | |
| launcher sets this on**, and it must stay on: browsers block module loading, fetch and | |
| XHR on the file protocol, so a React, Vue or ES-module deliverable renders as an empty | |
| body no matter how good it is -- measured at 44 of 182 shots blank, essentially all of | |
| them runtime-rendered. Since React is named in 30% of the training briefs, the old | |
| default was scoring a large share of the task pool as necessarily broken and teaching the | |
| policy to avoid the very stack the brief asked for. | |
| Scores across this boundary are NOT comparable: a React page goes from certainly-0 to | |
| gradeable. Set it to 0 only to reproduce a pre-flip run. | |
| """ | |
| script_b64 = base64.b64encode(_SHOT_SCRIPT.encode()).decode() | |
| serve_root = "" | |
| if os.environ.get("WEBDEV_GRADE_HTTP") == "1" and url.startswith("file://"): | |
| serve_root = os.path.dirname(url[len("file://") :]) or "/" | |
| return ( | |
| f"echo {script_b64} | base64 -d > /tmp/_webdev_shot.py && " | |
| f"python3 /tmp/_webdev_shot.py --url {shlex.quote(url)} " | |
| f"--actions-b64 {shlex.quote(actions_b64)} --proxy {shlex.quote(proxy)}" | |
| + (f" --serve-root {shlex.quote(serve_root)}" if serve_root else "") | |
| ) | |
| def _parse_shot_b64(r: dict) -> tuple[str, str]: | |
| """Pull (SHOT_B64, CONSOLE_ERRORS) out of an env.execute result; empty b64 on miss.""" | |
| out = r.get("output") or "" | |
| shot_b64, console_errors = "", "[]" | |
| for line in out.splitlines(): | |
| if line.startswith("SHOT_B64:"): | |
| shot_b64 = line[len("SHOT_B64:") :].strip() | |
| elif line.startswith("CONSOLE_ERRORS:"): | |
| console_errors = line[len("CONSOLE_ERRORS:") :].strip() | |
| return shot_b64, console_errors | |
| def _parse_render_env(r: dict) -> dict: | |
| """Pull the RENDER_ENV json out of an env.execute result. {} when absent | |
| (old pod script) or unparseable — callers treat that as env-healthy.""" | |
| for line in (r.get("output") or "").splitlines(): | |
| if line.startswith("RENDER_ENV:"): | |
| try: | |
| return json.loads(line[len("RENDER_ENV:") :].strip()) | |
| except Exception: | |
| return {} | |
| return {} | |