Spaces:
Sleeping
Sleeping
Download scripts/verify_ui.py from Mike0021/qwen-image: direct link, hf CLI and curl.
- Browser
- Download file 34 kB
-
https://huggingface.co/spaces/Mike0021/qwen-image/resolve/main/scripts/verify_ui.py
- Command line
-
hf download hf://spaces/Mike0021/qwen-image/scripts/verify_ui.py
-
curl -L -o verify_ui.py https://huggingface.co/spaces/Mike0021/qwen-image/resolve/main/scripts/verify_ui.py
34 kB
| #!/usr/bin/env python3 | |
| """Inspect only the authenticated deployed UI; never starts a local app. | |
| Usage: uv run --with playwright==1.58.0 --with huggingface_hub==1.32.0 | |
| scripts/verify_ui.py --phase layout | |
| """ | |
| import argparse | |
| from datetime import datetime, timezone | |
| import hashlib | |
| from io import BytesIO | |
| import httpx | |
| import json | |
| from pathlib import Path | |
| import re | |
| import sys | |
| import time | |
| from urllib.parse import urlsplit | |
| from huggingface_hub import HfApi, get_token | |
| from playwright.sync_api import sync_playwright, expect | |
| from PIL import Image, ImageOps | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from settings import EXAMPLE_PROMPTS, SIZES, STEPS | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--space", default="Mike0021/qwen-image") | |
| parser.add_argument("--phase", choices=["layout", "inspect", "keyboard", "controls", "generate", "examples", "edit", "upload"], default="layout") | |
| parser.add_argument("--mobile", action="store_true") | |
| parser.add_argument("--reference", type=Path) | |
| parser.add_argument("--resolution", choices=list(SIZES)) | |
| parser.add_argument("--aspect", choices=list(SIZES["1K"])) | |
| parser.add_argument("--cold-after-restart", action="store_true") | |
| args = parser.parse_args() | |
| token = get_token() | |
| if not token: | |
| raise SystemExit("Authenticated access required") | |
| info = HfApi(token=token).space_info(args.space) | |
| if info.runtime.stage != "RUNNING": | |
| raise SystemExit(f"Space is not running: {info.runtime.stage}") | |
| host = info.runtime.raw["domains"][0]["domain"] | |
| if not host.endswith(".hf.space"): | |
| raise SystemExit("Expected an actual Hugging Face Space host") | |
| url = f"https://{host}" | |
| stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") | |
| out = ROOT / "artifacts" / "ui" / f"{stamp}-{args.phase}-{'mobile' if args.mobile else 'desktop'}" | |
| out.mkdir(parents=True) | |
| evidence = {"url": url, "source_sha": info.sha, "phase": args.phase, | |
| "private": info.private, "at": stamp, "observations": [], "page_errors": [], "network_errors": []} | |
| with sync_playwright() as playwright: | |
| browser = playwright.chromium.launch(headless=True) | |
| context = browser.new_context(viewport={"width": 390 if args.mobile else 1440, | |
| "height": 844 if args.mobile else 1000}, | |
| device_scale_factor=1, has_touch=args.mobile, | |
| is_mobile=args.mobile, reduced_motion="reduce") | |
| # Exchange the existing credential through HF's own private-Space JWT | |
| # endpoint, then retain the real Secure/HttpOnly Space cookie in memory. | |
| # Chromium's native downloads do not reliably inherit route headers. | |
| # No JWT, cookie, bearer token, signed URL, or storage state is persisted. | |
| auth_response = httpx.get(f"https://huggingface.co/api/spaces/{args.space}/jwt", | |
| headers={"Authorization": f"Bearer {token}"}) | |
| if auth_response.status_code != 200: | |
| raise RuntimeError(f"Private-Space browser authentication failed: HTTP {auth_response.status_code}") | |
| signed = auth_response.json()["token"] | |
| bootstrap = httpx.get(url+"/", params={"__sign": signed}, follow_redirects=False) | |
| if bootstrap.status_code != 200: | |
| raise RuntimeError(f"Space cookie bootstrap failed: HTTP {bootstrap.status_code}") | |
| cookies = [{"name": cookie.name, "value": cookie.value, "domain": host, | |
| "path": cookie.path, "secure": True, "httpOnly": True, "sameSite": "None"} | |
| for cookie in bootstrap.cookies.jar if cookie.domain == host] | |
| if not cookies: | |
| raise RuntimeError("HF did not issue a private-Space session cookie") | |
| context.add_cookies(cookies) | |
| evidence["browser_authentication"] = "HF-issued Secure HttpOnly private-Space cookie, memory only" | |
| def authorize(route): | |
| # Never place a bearer token in context-wide headers: external images | |
| # and scripts must not receive the owner's Hub credential. | |
| request_url = urlsplit(route.request.url) | |
| own_metadata = request_url.hostname == "huggingface.co" and request_url.path in { | |
| f"/api/spaces/{args.space}", f"/api/spaces/by-subdomain/{host.removesuffix('.hf.space')}"} | |
| if request_url.hostname != host and not own_metadata: | |
| route.continue_() | |
| return | |
| route.continue_(headers={**route.request.headers, "Authorization": f"Bearer {token}"}) | |
| context.route(f"https://{host}/**", authorize) | |
| # Gradio fetches its own private repository metadata to render its Hub | |
| # badge. This exact first-party endpoint needs the same authenticated access. | |
| context.route(f"https://huggingface.co/api/spaces/{args.space}", authorize) | |
| context.route(f"https://huggingface.co/api/spaces/by-subdomain/{host.removesuffix('.hf.space')}", authorize) | |
| page = context.new_page() | |
| queue_joins = [] | |
| def observe_request(request): | |
| if urlsplit(request.url).path.endswith("/queue/join"): | |
| try: | |
| queue_joins.append(request.post_data_json.get("fn_index")) | |
| except (ValueError, AttributeError): | |
| queue_joins.append("unparsed") | |
| page.on("request", observe_request) | |
| page.on("pageerror", lambda error: evidence["page_errors"].append( | |
| re.sub(r"hf_[A-Za-z0-9]+", "[redacted]", str(error))[:500])) | |
| page.on("requestfailed", lambda req: evidence["network_errors"].append({ | |
| "path": urlsplit(req.url).path, "failure": req.failure})) | |
| page.on("response", lambda res: evidence["network_errors"].append({ | |
| "path": urlsplit(res.url).path, "status": res.status}) if res.status >= 400 else None) | |
| page.goto(url, wait_until="domcontentloaded", timeout=60000) | |
| expect(page.get_by_role("button", name="Generate image")).to_be_visible(timeout=60000) | |
| config = context.request.get(url+"/config").json() | |
| expect(page).to_have_title("Qwen 2.1 Image") | |
| expect(page.get_by_role("heading", name="Qwen 2.1 Image", exact=True)).to_be_visible() | |
| assert info.card_data["title"] == "Qwen 2.1 Image" | |
| evidence["titles"] = {"browser": page.title(), "space": info.card_data["title"]} | |
| generation_index = next(d["id"] for d in config["dependencies"] if d.get("api_name") == "generate") | |
| def capture(name): | |
| page.evaluate("window.scrollTo(0, 0)") | |
| page.screenshot(path=str(out / f"{name}-screen.png"), full_page=not page.locator("#canvas.fullscreen").count()) | |
| evidence["observations"].append({"capture": f"{name}-screen.png", "viewport": page.viewport_size, | |
| "geometry": page.evaluate("""() => ({width: innerWidth, | |
| scrollWidth: document.documentElement.scrollWidth, | |
| canvas: (() => {const r=document.querySelector('#canvas').getBoundingClientRect(); | |
| return {x:r.x,y:r.y,width:r.width,height:r.height}})()})""")}) | |
| def save_output(name): | |
| expect(page.locator("#canvas img")).to_be_visible(timeout=300000) | |
| expect(page.locator("#result-status")).to_contain_text("inference", timeout=30000) | |
| page.locator("#canvas").hover() | |
| for selector, suffix in [("#canvas a[download]", "png"), ("#recipe a[download]", "json")]: | |
| evidence["observations"].append({"download": suffix, | |
| "href": page.locator(selector).get_attribute("href")}) | |
| with page.expect_download(timeout=30000) as pending: | |
| page.locator(selector).click() | |
| pending.value.save_as(str(out / f"{name}.{suffix}")) | |
| record = json.loads((out / f"{name}.json").read_text()) | |
| source_names = ("app.py", "settings.py", "style.css", "requirements.txt") | |
| expected_source = hashlib.sha256(b"".join(n.encode()+b"\0"+(ROOT/n).read_bytes()+b"\0" for n in source_names)).hexdigest() | |
| assert record["source_sha256"] == expected_source, "Downloaded result uses a different application source fingerprint" | |
| png_hash = hashlib.sha256((out / f"{name}.png").read_bytes()).hexdigest() | |
| assert record["output"]["png_sha256"] == png_hash, "Browser PNG / recipe hash mismatch" | |
| with Image.open(out / f"{name}.png") as img: | |
| assert img.format == "PNG" | |
| assert list(img.size) == [record["parameters"]["width"], record["parameters"]["height"]] | |
| assert "generation" in img.info, "PNG generation metadata missing" | |
| pixels = hashlib.sha256(img.mode.encode() + str(img.size).encode() + img.tobytes()).hexdigest() | |
| evidence["observations"].append({"output": name, "id": record["id"], | |
| "png_sha256": png_hash, "pixel_sha256": pixels, | |
| "dimensions": [record["parameters"]["width"], record["parameters"]["height"]], | |
| "status_text": page.locator("#result-status").inner_text(), "downloads_verified": True}) | |
| return record | |
| def tab_to(target): | |
| for _ in range(80): | |
| page.keyboard.press("Tab") | |
| if target.evaluate("e => e === document.activeElement"): | |
| return | |
| raise AssertionError("Target not reachable by sequential Tab navigation") | |
| def focus_evidence(target, name, proxy=None): | |
| page.mouse.move(1, 1) | |
| tab_to(target) | |
| proxy = proxy or target | |
| row = proxy.evaluate("""e => { | |
| const r=e.getBoundingClientRect(), s=getComputedStyle(e); | |
| let bounds={left:r.left,top:r.top,right:r.right,bottom:r.bottom}, clips=[]; | |
| for(let p=e.parentElement;p;p=p.parentElement) { | |
| const c=getComputedStyle(p), b=p.getBoundingClientRect(); | |
| if(/hidden|clip|scroll|auto/.test(c.overflowX)) { | |
| bounds.left=Math.max(bounds.left,b.left); bounds.right=Math.min(bounds.right,b.right); | |
| } | |
| if(/hidden|clip|scroll|auto/.test(c.overflowY)) { | |
| bounds.top=Math.max(bounds.top,b.top); bounds.bottom=Math.min(bounds.bottom,b.bottom); | |
| } | |
| if(c.position === "fixed") break; | |
| } | |
| return {outline:s.outline,offset:s.outlineOffset,font:s.fontFamily, | |
| width:r.width,height:r.height,visibleWidth:bounds.right-bounds.left, | |
| visibleHeight:bounds.bottom-bounds.top,focused:document.activeElement.outerHTML.slice(0,500)}; | |
| }""") | |
| assert row["outline"].startswith("rgb("), row | |
| assert "solid 3px" in row["outline"], row | |
| assert row["visibleWidth"] >= row["width"]-1 and row["visibleHeight"] >= row["height"]-1, row | |
| # Inspect the rendered control, not just CSS declarations. Each side | |
| # must contain focus-blue pixels inside its bounding box. | |
| bitmap = Image.open(BytesIO(proxy.screenshot())).convert("RGB") | |
| colors = {(37,99,235), (147,197,253)} | |
| w,h=bitmap.size | |
| strips = [bitmap.crop(box) for box in [(0,0,w,4),(0,h-4,w,h),(0,0,4,h),(w-4,0,w,h)]] | |
| row["focus_pixels_top_bottom_left_right"] = [sum(pixel in colors for pixel in strip.get_flattened_data()) for strip in strips] | |
| assert all(n >= 3 for n in row["focus_pixels_top_bottom_left_right"]), row | |
| evidence.setdefault("focus_checks", []).append({"name":name, **row}) | |
| page.screenshot(path=str(out / f"focus-{name}-screen.png"), full_page=not page.locator("#canvas.fullscreen").count()) | |
| def fullscreen(): | |
| page.get_by_role("button", name="Fullscreen", exact=True).click() | |
| expect(page.get_by_role("button", name="Exit fullscreen mode", exact=True)).to_be_visible() | |
| box=page.locator("#canvas").bounding_box() | |
| assert abs(box["width"]-page.viewport_size["width"]) <= 1 | |
| assert abs(box["height"]-page.viewport_size["height"]) <= 1 | |
| capture("fullscreen") | |
| page.get_by_role("button", name="Exit fullscreen mode", exact=True).click() | |
| expect(page.get_by_role("button", name="Fullscreen", exact=True)).to_be_visible() | |
| evidence["observations"].append({"fullscreen_enter_exit": True}) | |
| try: | |
| if args.phase == "layout": | |
| for width, height in [(1440, 1000), (390, 844), (320, 800)]: | |
| page.set_viewport_size({"width": width, "height": height}) | |
| capture(f"initial-{width}") | |
| assert page.evaluate("document.documentElement.scrollWidth <= innerWidth"), "Horizontal overflow" | |
| page.set_viewport_size({"width": 1440, "height": 1000}) | |
| page.evaluate("document.body.style.zoom = '2'") | |
| capture("css-zoom-200") | |
| evidence["observations"].append({"css_zoom_200_overflow": page.evaluate("document.documentElement.scrollWidth > innerWidth"), | |
| "qualification": "CSS zoom check, not physical-device or browser-chrome zoom certification"}) | |
| page.evaluate("document.body.style.zoom = ''") | |
| evidence["body_text"] = page.locator("body").inner_text() | |
| elif args.phase == "keyboard": | |
| fonts = page.locator(".gradio-container").evaluate("""root => [...new Set( | |
| [...root.querySelectorAll('*')].filter(e=>e.getClientRects().length && | |
| [...e.childNodes].some(n=>n.nodeType===3 && n.textContent.trim())) | |
| .map(e=>getComputedStyle(e).fontFamily))]""") | |
| assert fonts == ["Arial, sans-serif"], fonts | |
| evidence["visible_text_font_families"] = fonts | |
| focus_evidence(page.get_by_label("Prompt", exact=True), "prompt") | |
| for shape in ["1:1", "4:3", "3:4", "16:9", "9:16"]: | |
| radio = page.get_by_role("radio", name=shape, exact=True) | |
| # Native radio groups expose the checked item to Tab and | |
| # use arrow keys to reach the other items. | |
| radio.check() | |
| focus_evidence(radio, "shape-"+shape.replace(":","-"), radio.locator("..")) | |
| page.keyboard.press("ArrowLeft") | |
| expect(page.get_by_role("radio", name="16:9", exact=True)).to_be_checked() | |
| expect(page.locator("#size-hint")).to_have_text("1,376 × 768 pixels") | |
| evidence["after_radio_arrow"] = page.locator("#shape").evaluate("e=>({active:document.activeElement.outerHTML,selected:[...e.querySelectorAll('input')].filter(n=>n.checked).map(n=>n.value)})") | |
| for size in ["1K","2K"]: | |
| radio = page.get_by_role("radio", name=size, exact=True) | |
| radio.check() | |
| focus_evidence(radio, "size-"+size, radio.locator("..")) | |
| for steps in ["20", "30", "40"]: | |
| radio=page.get_by_role("radio",name=steps,exact=True) | |
| radio.check() | |
| focus_evidence(radio,"steps-"+steps,radio.locator("..")) | |
| page.keyboard.press("ArrowLeft") | |
| expect(page.get_by_role("radio",name="30",exact=True)).to_be_checked() | |
| evidence["keyboard_steps_selection"] = 30 | |
| check=page.get_by_label("Transparent background",exact=True) | |
| focus_evidence(check, "transparency", check.locator("..")) | |
| page.keyboard.press("Space") | |
| expect(check).to_be_checked() | |
| ref=page.locator("#reference-settings > button") | |
| focus_evidence(ref, "reference-accordion") | |
| page.keyboard.press("Enter") | |
| expect(page.locator("#reference")).to_be_visible() | |
| evidence["reference_upload_aria"] = page.locator("#reference").aria_snapshot() | |
| if args.reference: | |
| upload=page.get_by_role("button",name="Drop an image file here to upload",exact=True) | |
| focus_evidence(upload,"reference-upload") | |
| with page.expect_file_chooser() as chooser: | |
| page.keyboard.press("Enter") | |
| chooser.value.set_files(str(args.reference)) | |
| expect(page.locator("#reference img")).to_be_visible(timeout=30000) | |
| expect(page.get_by_role("radio",name="2K",exact=True)).to_be_checked(timeout=30000) | |
| remove=page.get_by_role("button",name="Remove Image",exact=True) | |
| focus_evidence(remove,"reference-remove") | |
| page.keyboard.press("Enter") | |
| expect(page.locator("#reference img")).to_have_count(0) | |
| tab_to(ref) | |
| evidence["keyboard_native_upload_and_remove"] = True | |
| page.keyboard.press("Enter") | |
| seed=page.locator("#seed-settings > button") | |
| focus_evidence(seed, "seed-accordion") | |
| page.keyboard.press("Space") | |
| randomize=page.get_by_label("New seed each time",exact=True) | |
| focus_evidence(randomize,"new-seed",randomize.locator("..")) | |
| page.keyboard.press("Space") | |
| expect(randomize).not_to_be_checked() | |
| focus_evidence(page.get_by_label("Seed",exact=True),"seed-number") | |
| page.keyboard.type("123") | |
| button=page.get_by_role("button",name="Generate image",exact=True) | |
| focus_evidence(button,"generate") | |
| page.keyboard.press("Enter") | |
| expect(page.get_by_text("Describe the image you would like to make.",exact=False)).to_be_visible(timeout=15000) | |
| expect(button).to_be_enabled() | |
| evidence["keyboard_empty_validation"] = True | |
| for name in ["Glass pear","Typography","Architecture","Transparent object"]: | |
| focus_evidence(page.get_by_role("button",name=name,exact=True), "example-"+name.lower().replace(" ","-")) | |
| focus_evidence(page.get_by_role("link",name="Qwen Research License"),"license") | |
| for width in [320,390] if args.mobile else [1440]: | |
| page.set_viewport_size({"width":width,"height":844 if args.mobile else 1000}) | |
| focus_evidence(button,"generate-width-"+str(width)) | |
| elif args.phase == "inspect": | |
| evidence["aria"] = page.locator("body").aria_snapshot() | |
| evidence["control_html"] = {name: page.locator(name).evaluate("e=>e.outerHTML") for name in ["#quality","#reference-settings","#seed-settings","#shape"]} | |
| evidence["canvas_html"] = page.locator("#canvas").evaluate("e=>e.outerHTML") | |
| evidence["header_ancestors"] = page.locator(".app-bar").evaluate("""e => { | |
| let rows=[]; while(e && rows.length<10) {const s=getComputedStyle(e); | |
| rows.push({tag:e.tagName,id:e.id,class:e.className,padding:s.padding, | |
| left:e.getBoundingClientRect().left});e=e.parentElement;} return rows; | |
| }""") | |
| evidence["styles"] = page.evaluate("""() => { | |
| const e=document.querySelector('.gradio-container'); | |
| const rules=[]; | |
| function visit(list, media='') {for(const r of list) { | |
| if(r.cssRules) visit(r.cssRules, r.conditionText || media); | |
| else if(r.selectorText && r.selectorText.includes('gradio-container') && r.style.padding) | |
| rules.push({selector:r.selectorText,padding:r.style.padding,media}); | |
| }} | |
| for(const s of [...document.styleSheets,...document.adoptedStyleSheets]) {try {visit(s.cssRules)} catch {}} | |
| const inline=[...document.querySelectorAll('style')].map(s=>s.textContent).filter(s=>s.includes('--paper')); | |
| return {rules,padding:getComputedStyle(e).padding,width:innerWidth, | |
| inline,mobile:matchMedia('(max-width:760px)').matches}; | |
| }""") | |
| elif args.phase == "controls": | |
| for resolution in ["1K", "2K"]: | |
| page.get_by_role("radio", name=resolution, exact=True).check() | |
| for shape in ["1:1", "4:3", "3:4", "16:9", "9:16"]: | |
| page.get_by_role("radio", name=shape, exact=True).check() | |
| w, h = SIZES[resolution][shape] | |
| expect(page.locator("#size-hint")).to_have_text(f"{w:,} × {h:,} pixels") | |
| evidence["observations"].append({"resolution": resolution, "shape": shape, | |
| "hint": page.locator("#size-hint").inner_text()}) | |
| page.get_by_label("Transparent background", exact=True).check() | |
| evidence["checked_checkbox_style"] = page.get_by_label("Transparent background", exact=True).evaluate( | |
| "e=>({checked:e.checked,background:getComputedStyle(e).backgroundColor,backgroundImage:getComputedStyle(e).backgroundImage})") | |
| for quality in STEPS: | |
| radio=page.get_by_role("radio",name=str(STEPS[quality]),exact=True) | |
| radio.check() | |
| expect(radio).to_be_checked() | |
| page.locator("#seed-settings > button").click() | |
| page.get_by_label("New seed each time", exact=True).uncheck() | |
| page.get_by_label("Seed", exact=True).fill("12345") | |
| page.locator("#reference-settings > button").click() | |
| capture("controls-open") | |
| validation_started = time.perf_counter() | |
| page.get_by_role("button", name="Generate image").click() | |
| expect(page.get_by_text("Describe the image you would like to make.", exact=False)).to_be_visible(timeout=15000) | |
| capture("empty-prompt-error") | |
| evidence["observations"].append({"empty_prompt_error": "visible", | |
| "validation_seconds": time.perf_counter()-validation_started}) | |
| page.get_by_label("Prompt", exact=True).focus() | |
| page.keyboard.press("Tab") | |
| evidence["keyboard_focus"] = page.evaluate("""() => ({tag:document.activeElement.tagName, | |
| type:document.activeElement.type, outline:getComputedStyle(document.activeElement).outline, | |
| labelOutline:document.activeElement.closest('label') ? getComputedStyle(document.activeElement.closest('label')).outline : null})""") | |
| with context.expect_page() as popup: | |
| page.get_by_role("link", name="Qwen Research License").click() | |
| license_page = popup.value | |
| license_page.wait_for_load_state("domcontentloaded") | |
| assert license_page.url.startswith("https://huggingface.co/Qwen/Qwen-Image-2.1/blob/") | |
| evidence["license_opened"] = license_page.url | |
| license_page.close() | |
| elif args.phase == "generate": | |
| page.get_by_role("button", name="Generate image").click() | |
| expect(page.get_by_text("Describe the image you would like to make.", exact=False)).to_be_visible(timeout=15000) | |
| expect(page.get_by_role("button", name="Generate image")).to_be_enabled() | |
| evidence["observations"].append({"empty_error_before_generation": True}) | |
| page.get_by_label("Prompt", exact=True).fill(EXAMPLE_PROMPTS[0]) | |
| if not args.mobile: | |
| page.locator("#seed-settings > button").click() | |
| page.get_by_label("New seed each time", exact=True).uncheck() | |
| page.get_by_label("Seed", exact=True).fill("42") | |
| page.locator("#seed-settings > button").click() | |
| started = time.perf_counter() | |
| if args.mobile: | |
| page.get_by_role("button", name="Generate image").click() | |
| else: | |
| page.get_by_role("button", name="Generate image").dblclick() | |
| expect(page.get_by_role("button", name="Generating…", exact=True)).to_be_disabled(timeout=30000) | |
| capture("submitted") | |
| record = save_output("generated") | |
| expect(page.get_by_role("button", name="Generate image")).to_be_enabled(timeout=30000) | |
| evidence["button_to_downloads_seconds"] = time.perf_counter()-started | |
| evidence["queue_join_function_indices"] = queue_joins | |
| evidence["generation_function_index"] = generation_index | |
| assert queue_joins.count(generation_index) == 1, "Generation submission created more than one inference job" | |
| assert (record["parameters"]["seed"] != 42) if args.mobile else (record["parameters"]["seed"] == 42) | |
| capture("generated") | |
| evidence["result_toolbar_html"] = page.locator("#canvas").evaluate("e=>e.outerHTML") | |
| focus_evidence(page.locator("#canvas a[download]"),"png-download") | |
| with page.expect_download(timeout=30000) as keyboard_download: | |
| page.keyboard.press("Enter") | |
| keyboard_download.value.save_as(str(out / "keyboard-download.png")) | |
| assert hashlib.sha256((out / "keyboard-download.png").read_bytes()).hexdigest() == record["output"]["png_sha256"] | |
| focus_evidence(page.get_by_role("button", name="Fullscreen", exact=True),"fullscreen") | |
| page.keyboard.press("Enter") | |
| exit_button=page.get_by_role("button",name="Exit fullscreen mode",exact=True) | |
| expect(exit_button).to_be_visible() | |
| expect(exit_button).to_be_focused() | |
| for key in ["Tab", "Tab", "Shift+Tab", "Shift+Tab"]: | |
| page.keyboard.press(key) | |
| assert page.locator("#canvas").evaluate("e=>e.contains(document.activeElement)") | |
| evidence["fullscreen_focus_contained"] = True | |
| focus_evidence(exit_button,"exit-fullscreen") | |
| page.keyboard.press("Enter") | |
| expect(page.get_by_role("button",name="Fullscreen",exact=True)).to_be_focused() | |
| page.keyboard.press("Enter") | |
| expect(exit_button).to_be_visible() | |
| page.keyboard.press("Escape") | |
| expect(page.get_by_role("button",name="Fullscreen",exact=True)).to_be_focused() | |
| focus_evidence(page.locator("#recipe a[download]"),"recipe-download") | |
| with page.expect_download(timeout=30000) as recipe_download: | |
| page.keyboard.press("Enter") | |
| recipe_download.value.save_as(str(out / "keyboard-download.json")) | |
| assert json.loads((out / "keyboard-download.json").read_text())["id"] == record["id"] | |
| fullscreen() | |
| evidence["observations"].append({"same_page_error_recovery": True,"keyboard_downloads_and_fullscreen":True}) | |
| elif args.phase == "examples": | |
| records = {} | |
| page.locator("#seed-settings > button").click() | |
| if args.reference: | |
| page.locator("#reference-settings > button").click() | |
| page.locator("#reference input[type=file]").set_input_files(str(args.reference)) | |
| expect(page.locator("#reference img")).to_be_visible(timeout=30000) | |
| for run in range(2): | |
| for index, label in enumerate(["Glass pear", "Typography", "Architecture", "Transparent object"]): | |
| previous = page.locator("#recipe a[download]").get_attribute("href") if page.locator("#recipe a[download]").count() else None | |
| started = time.perf_counter() | |
| page.get_by_role("button", name=label, exact=True).click() | |
| expect(page.get_by_label("Prompt", exact=True)).to_have_value(EXAMPLE_PROMPTS[index], timeout=15000) | |
| if args.reference: | |
| expect(page.locator("#reference img")).to_have_count(0, timeout=15000) | |
| page.wait_for_function("""previous => { | |
| const e=document.querySelector('#recipe a[download]'); | |
| return e && e.getAttribute('href') !== previous; | |
| }""", arg=previous, timeout=300000) | |
| elapsed = time.perf_counter()-started | |
| name = f"study-{index+1}-pass-{run+1}" | |
| record = save_output(name) | |
| assert record["parameters"]["seed"] == 42 | |
| assert record["reference_pixel_sha256"] is None | |
| expect(page.get_by_role("radio", name="1K", exact=True)).to_be_checked() | |
| expect(page.get_by_role("radio", name=["1:1", "3:4", "16:9", "1:1"][index], exact=True)).to_be_checked() | |
| expect(page.get_by_label("New seed each time", exact=True)).not_to_be_checked() | |
| expect(page.get_by_label("Transparent background", exact=True)).to_be_checked(checked=index == 3) | |
| if index == 3: | |
| assert record["parameters"]["transparent_requested"] | |
| assert record["output"]["alpha_extrema"][0] == 0 | |
| assert record["output"]["fraction_alpha_below_128"] > 0 | |
| expect(page.get_by_label("Prompt", exact=True)).to_have_value(EXAMPLE_PROMPTS[index]) | |
| if run == 0: | |
| records[index] = record["id"] | |
| else: | |
| assert record["id"] == records[index], "Lazy cache did not reuse original generation" | |
| evidence["observations"].append({"study": index+1, "pass": run+1, | |
| "click_to_recipe_seconds": elapsed, "same_original_id": run == 1}) | |
| capture(name) | |
| elif args.phase in {"edit", "upload"}: | |
| if not args.reference or not args.reference.is_file(): | |
| raise ValueError("--reference must name a real image for edit verification") | |
| page.locator("#reference-settings > button").click() | |
| with page.expect_file_chooser() as chooser: | |
| page.get_by_role("button",name="Drop an image file here to upload",exact=True).click() | |
| chooser.value.set_files(str(args.reference)) | |
| expect(page.locator("#reference img")).to_be_visible(timeout=30000) | |
| expect(page.get_by_role("radio", name="2K", exact=True)).to_be_checked(timeout=30000) | |
| evidence["observations"].append({"reference_upload_selects_2k": True}) | |
| capture("reference-uploaded") | |
| evidence["reference_controls"] = page.locator("#reference").aria_snapshot() | |
| if args.phase == "edit": | |
| if args.resolution: | |
| page.get_by_role("radio", name=args.resolution, exact=True).check() | |
| if args.aspect: | |
| page.get_by_role("radio", name=args.aspect, exact=True).check() | |
| page.get_by_label("Prompt", exact=True).fill( | |
| "Change the blue glass pear to amber orange glass. Keep the pear shape, plinth, camera angle, lighting and background.") | |
| page.locator("#seed-settings > button").click() | |
| page.get_by_label("New seed each time", exact=True).uncheck() | |
| page.get_by_label("Seed", exact=True).fill("42") | |
| page.get_by_role("button", name="Generate image").click() | |
| record = save_output("edited") | |
| with Image.open(args.reference) as ref: | |
| normalized = ImageOps.exif_transpose(ref).convert("RGBA") | |
| expected_ref = hashlib.sha256(str(normalized.size).encode()+normalized.tobytes()).hexdigest() | |
| assert record["reference_pixel_sha256"] == expected_ref, "Reference pixels do not match the recipe" | |
| if args.cold_after_restart: | |
| assert record["request_index_in_boot"] == 1 | |
| evidence["observations"].append({"first_request_after_observed_restart": True}) | |
| capture("edited") | |
| if not args.mobile: | |
| page.set_viewport_size({"width": 390, "height": 844}) | |
| capture("edited-mobile") | |
| page.locator("#reference").get_by_role("button", name=re.compile("Clear|Remove")).click() | |
| expect(page.locator("#reference img")).to_have_count(0) | |
| capture("reference-cleared") | |
| evidence["observations"].append({"reference_upload_clear": True, "edit_generated": args.phase == "edit"}) | |
| evidence["status"] = "completed" | |
| except Exception as exc: | |
| evidence["status"] = "failed" | |
| evidence["failure"] = re.sub(r"hf_[A-Za-z0-9]+", "[redacted]", str(exc))[:1500] | |
| evidence["failure_aria"] = page.locator("body").aria_snapshot() | |
| capture("failure") | |
| raise | |
| finally: | |
| (out / "observations.json").write_text(json.dumps(evidence, indent=2)) | |
| print(f"UI evidence: {out}", flush=True) | |
| browser.close() | |
| if __name__ == "__main__": | |
| main() | |