Spaces:
Sleeping
Sleeping
Download scripts/verify_server_ui.py from Mike0021/qwen-image: direct link, hf CLI and curl.
- Browser
- Download file 40.4 kB
-
https://huggingface.co/spaces/Mike0021/qwen-image/resolve/main/scripts/verify_server_ui.py
- Command line
-
hf download hf://spaces/Mike0021/qwen-image/scripts/verify_server_ui.py
-
curl -L -o verify_server_ui.py https://huggingface.co/spaces/Mike0021/qwen-image/resolve/main/scripts/verify_server_ui.py
40.4 kB
| #!/usr/bin/env python3 | |
| """Verify the actual private gr.Server deployment, including real browser interactions.""" | |
| import argparse | |
| from datetime import datetime, timezone | |
| import hashlib | |
| import httpx | |
| from io import BytesIO | |
| import json | |
| from pathlib import Path | |
| import re | |
| import sys | |
| import tempfile | |
| 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','controls','validation','generate','examples','edit','iterate','reuse_cutout'],default='layout') | |
| parser.add_argument('--mobile',action='store_true') | |
| parser.add_argument('--system-theme',choices=['light','dark'],default='light') | |
| parser.add_argument('--reference',type=Path) | |
| parser.add_argument('--resolution',default='1K',choices=['1K','2K']) | |
| parser.add_argument('--edit-steps',default=40,type=int,choices=[20,30,40]) | |
| args=parser.parse_args() | |
| token=get_token() | |
| if not token: raise SystemExit('Existing authentication required') | |
| info=HfApi(token=token).space_info(args.space) | |
| if info.runtime.stage != 'RUNNING': raise SystemExit(f'Space not ready: {info.runtime.stage}') | |
| domains=[row['domain'] for row in info.runtime.raw['domains'] if row['stage']=='READY'] | |
| preferred=args.space.lower().replace('/','-')+'.hf.space' | |
| host=preferred if preferred in domains else domains[0];url='https://'+host | |
| stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S') | |
| out=ROOT/'artifacts'/'server-ui'/f'{stamp}-{args.phase}-{"mobile" if args.mobile else "desktop"}' | |
| out.mkdir(parents=True) | |
| evidence={'at':stamp,'url':url,'hub_commit':info.sha,'phase':args.phase,'private':info.private, | |
| 'system_theme':args.system_theme,'observations':[],'page_errors':[],'network_errors':[],'focus_checks':[]} | |
| 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',color_scheme=args.system_theme) | |
| # 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) | |
| def capture(name): | |
| if not name.startswith('hover-'):page.mouse.move(1,1) | |
| if not page.locator('#image-dialog[open]').count(): page.evaluate('window.scrollTo(0,0)') | |
| page.screenshot(path=str(out/f'{name}-screen.png'),full_page=not page.locator('#image-dialog[open]').count()) | |
| evidence['observations'].append({'screenshot':f'{name}-screen.png','viewport':page.viewport_size, | |
| 'scroll_width':page.evaluate('document.documentElement.scrollWidth')}) | |
| def alignment(): | |
| boxes={key:page.locator(selector).bounding_box() for key,selector in { | |
| 'prompt_heading':'.prompt-heading','result_heading':'.result-toolbar', | |
| 'prompt_label':'label[for=prompt]','result_label':'#result-heading', | |
| 'prompt':'#prompt','canvas':'#canvas','title':'h1','ready':'#connection'}.items()} | |
| if page.viewport_size['width']>760: | |
| for left,right in [('prompt_heading','result_heading'),('prompt_label','result_label'),('prompt','canvas')]: | |
| assert abs(boxes[left]['y']-boxes[right]['y'])<1,boxes | |
| for label,surface in [('prompt_label','prompt'),('result_label','canvas')]: | |
| assert abs(boxes[label]['x']-boxes[surface]['x'])<1,boxes | |
| assert abs(boxes['title']['y']+boxes['title']['height']/2-boxes['ready']['y']-boxes['ready']['height']/2)<1,boxes | |
| if page.viewport_size['width']>760: | |
| assert abs(boxes['ready']['x']+boxes['ready']['width']-boxes['canvas']['x']-boxes['canvas']['width'])<1,boxes | |
| return boxes | |
| def choose(label): | |
| radio=page.get_by_role('radio',name=label,exact=True) | |
| radio.locator('..').click() | |
| expect(radio).to_be_checked() | |
| def tab_to(target): | |
| page.mouse.move(1,1) | |
| for _ in range(80): | |
| page.keyboard.press('Tab') | |
| if target.evaluate('e=>e===document.activeElement'):return | |
| raise AssertionError('Target not reachable by sequential Tab') | |
| def focus(target,name,proxy=None): | |
| tab_to(target);proxy=proxy or target | |
| row=proxy.evaluate('''e=>{const r=e.getBoundingClientRect(),s=getComputedStyle(e); | |
| return {outline:s.outline,outline_color:s.outlineColor,offset:s.outlineOffset,width:r.width,height:r.height,font:s.fontFamily}}''') | |
| assert 'solid 3px' in row['outline'],row | |
| proxy.scroll_into_view_if_needed() | |
| b=proxy.bounding_box() | |
| # Include the outer ring in the rendered crop, rather than checking | |
| # the CSS declaration or cropping away the very pixels under test. | |
| shot=page.screenshot(clip={'x':b['x']-6,'y':b['y']-6,'width':b['width']+12,'height':b['height']+12}) | |
| bitmap=Image.open(BytesIO(shot)).convert('RGB');w,h=bitmap.size | |
| strips=[bitmap.crop(box) for box in [(0,0,w,6),(0,h-6,w,h),(0,0,6,h),(w-6,0,w,h)]] | |
| focus_rgb=tuple(map(int,re.findall(r'\d+',row['outline_color']))) | |
| row['blue_pixels_top_bottom_left_right']=[sum(pixel==focus_rgb for pixel in strip.get_flattened_data()) for strip in strips] | |
| assert all(n>5 for n in row['blue_pixels_top_bottom_left_right']),row | |
| evidence['focus_checks'].append({'name':name,**row}) | |
| page.screenshot(path=str(out/f'focus-{name}-screen.png'),full_page=not page.locator('#image-dialog[open]').count()) | |
| def download(name): | |
| expect(page.locator('#result-image')).to_be_visible(timeout=300000) | |
| expect(page.locator('#result-status')).to_contain_text('inference',timeout=30000) | |
| for selector,ext in [('#download-png','png'),('#download-recipe','json')]: | |
| with page.expect_download(timeout=30000) as pending: page.locator(selector).click() | |
| pending.value.save_as(str(out/f'{name}.{ext}')) | |
| record=json.loads((out/f'{name}.json').read_text()) | |
| expected=hashlib.sha256(b''.join(n.encode()+b'\0'+(ROOT/n).read_bytes()+b'\0' for n in record['source_files'])).hexdigest() | |
| assert record['source_sha256']==expected | |
| assert hashlib.sha256((out/f'{name}.png').read_bytes()).hexdigest()==record['output']['png_sha256'] | |
| with Image.open(out/f'{name}.png') as img: | |
| assert img.format=='PNG' and 'generation' in img.info | |
| assert list(img.size)==[record['parameters']['width'],record['parameters']['height']] | |
| pixels=hashlib.sha256(img.mode.encode()+str(img.size).encode()+img.tobytes()).hexdigest() | |
| evidence['observations'].append({'output':name,'id':record['id'],'pixel_sha256':pixels, | |
| 'png_sha256':record['output']['png_sha256'],'inference_seconds':record['metrics']['inference_seconds']}) | |
| return record | |
| def upload(): | |
| if not args.reference: raise ValueError('--reference is required') | |
| page.locator('#reference-settings summary').click() | |
| with page.expect_file_chooser() as chooser:page.locator('#upload-reference').click() | |
| chooser.value.set_files(str(args.reference)) | |
| expect(page.locator('#reference-preview')).to_be_visible() | |
| expect(page.get_by_role('radio',name='2K',exact=True)).to_be_checked() | |
| evidence['native_upload_selects_2k']=True | |
| try: | |
| page.goto(url,wait_until='domcontentloaded',timeout=60000) | |
| expect(page.locator('#connection')).to_have_text('ZeroGPU · Ready',timeout=60000) | |
| 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' | |
| health=context.request.get(url+'/health').json() | |
| assert health['frontend']=='gr.Server' and health['queue_capacity']==8,health | |
| evidence['health']=health | |
| config=context.request.get(url+'/config').json() | |
| generation_index=next(d['id'] for d in config['dependencies'] if d.get('api_name')=='generate') | |
| evidence['titles']={'browser':page.title(),'space':info.card_data['title']} | |
| evidence['browser_version']=browser.version | |
| if args.phase=='layout': | |
| evidence['control_geometry']=[] | |
| evidence['dark_layout']=[] | |
| for theme in ['light','dark']: | |
| page.emulate_media(color_scheme=theme) | |
| for w,h in [(1920,1080),(1440,1000),(1024,900),(820,900),(390,844),(320,800)]: | |
| page.set_viewport_size({'width':w,'height':h}) | |
| page.locator('#examples img').evaluate_all('es=>Promise.all(es.map(e=>e.decode()))') | |
| styles=page.evaluate('''()=>{const root=getComputedStyle(document.documentElement);return { | |
| scheme:root.colorScheme,background:root.backgroundColor,body:getComputedStyle(document.body).backgroundColor, | |
| prompt:getComputedStyle(document.querySelector('#prompt')).backgroundColor, | |
| canvas:getComputedStyle(document.querySelector('#canvas')).backgroundColor}}''') | |
| assert 'dark' in styles['scheme'] and styles['background']==styles['body']=='rgb(17, 19, 24)',styles | |
| thumbs=page.locator('#examples img').evaluate_all('es=>es.map(e=>({loaded:e.complete&&e.naturalWidth>0,width:e.width,height:e.height,src:new URL(e.src).pathname}))') | |
| assert len(thumbs)==4 and all(t['loaded'] and t['width']==t['height']==24 for t in thumbs),thumbs | |
| geometry=page.locator('.choices label').evaluate_all('es=>es.map(e=>({height:e.getBoundingClientRect().height,width:e.getBoundingClientRect().width}))') | |
| assert all(g['height']==(44 if w<=760 else 30) for g in geometry),geometry | |
| if w<=760:assert all(g['width']>=44 for g in geometry),geometry | |
| examples=page.locator('#examples button').evaluate_all('es=>es.map(e=>({height:e.getBoundingClientRect().height,width:e.getBoundingClientRect().width}))') | |
| assert all(g['height']==(44 if w<=760 else 32) for g in examples),examples | |
| assert page.evaluate('document.documentElement.scrollWidth<=innerWidth') | |
| boxes=alignment();capture(f'{theme}-system-{w}') | |
| evidence['dark_layout'].append({'system_theme':theme,'viewport_width':w,'styles':styles,'alignment':boxes,'thumbnails':thumbs,'example_targets':examples}) | |
| evidence['control_geometry'].append({'system_theme':theme,'viewport_width':w,'selector_targets':geometry}) | |
| assert evidence['dark_layout'][0]['styles']==evidence['dark_layout'][6]['styles'] | |
| page.set_viewport_size({'width':1440,'height':1000});page.evaluate("document.body.style.zoom='2'") | |
| capture('css-zoom-200');assert page.evaluate('document.documentElement.scrollWidth<=innerWidth') | |
| evidence['zoom_qualification']='CSS zoom, not physical-device/browser-chrome zoom certification' | |
| evidence['visible_fonts']=page.locator('body').evaluate('''e=>[...new Set([...e.querySelectorAll('*')].filter(n=>n.getClientRects().length && [...n.childNodes].some(c=>c.nodeType===3 && c.textContent.trim())).map(n=>getComputedStyle(n).fontFamily))]''') | |
| assert evidence['visible_fonts']==['Arial, sans-serif'] | |
| elif args.phase=='controls': | |
| for size in SIZES: | |
| choose(size) | |
| for shape,(w,h) in SIZES[size].items(): | |
| choose(shape);expect(page.locator('#size-hint')).to_have_text(f'{w:,} × {h:,} pixels') | |
| for steps in ['20','30','40']:choose(steps) | |
| choose('1:1') | |
| def style_of(target): | |
| return target.evaluate('e=>{const s=getComputedStyle(e);return {background:s.backgroundColor,color:s.color,weight:s.fontWeight,border:s.borderWidth,shadow:s.boxShadow}}') | |
| selected=style_of(page.get_by_role('radio',name='1:1',exact=True).locator('..')) | |
| unselected=style_of(page.get_by_role('radio',name='4:3',exact=True).locator('..')) | |
| assert selected['background']!=unselected['background'] | |
| evidence['selected_control_styles']={'selected':selected,'unselected':unselected} | |
| if not args.mobile: | |
| evidence['hover_styles']=[] | |
| for target,label in [(page.locator('#generate'),'generate'),(page.get_by_role('button',name='Glass pear',exact=True),'example')]: | |
| page.mouse.move(1,1);normal=style_of(target);target.hover();hovered=style_of(target) | |
| assert normal['background']!=hovered['background'] | |
| evidence['hover_styles'].append({'control':label,'normal':normal,'hover':hovered}) | |
| capture('hover-'+label) | |
| page.locator('#generate').click() | |
| expect(page.locator('#form-error')).to_contain_text('Describe the image') | |
| expect(page.locator('#prompt')).to_be_focused();capture('empty-error') | |
| focus(page.locator('#prompt'),'prompt') | |
| for label in ['1:1','4:3','3:4','16:9','9:16','1K','2K','20','30','40']: | |
| choose(label);radio=page.get_by_role('radio',name=label,exact=True) | |
| focus(radio,'radio-'+label.replace(':','-'),radio.locator('..')) | |
| page.keyboard.press('ArrowLeft');expect(page.get_by_role('radio',name='30',exact=True)).to_be_checked() | |
| check=page.locator('#transparent');focus(check,'transparent',check.locator('..'));page.keyboard.press('Space');expect(check).to_be_checked() | |
| summary=page.locator('#reference-settings summary');focus(summary,'reference-summary');page.keyboard.press('Enter') | |
| focus(page.locator('#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-preview')).to_be_visible() | |
| expect(page.get_by_role('radio',name='2K',exact=True)).to_be_checked() | |
| focus(page.locator('#remove-reference'),'remove');page.keyboard.press('Enter') | |
| expect(page.locator('#reference-preview')).to_be_hidden() | |
| expect(page.locator('#upload-reference')).to_be_focused() | |
| seed_summary=page.locator('#seed-settings summary');focus(seed_summary,'seed-summary');page.keyboard.press('Space') | |
| random=page.locator('#randomize');focus(random,'randomize',random.locator('..'));page.keyboard.press('Space') | |
| expect(page.locator('#seed')).to_be_enabled();focus(page.locator('#seed'),'seed');page.locator('#seed').fill('12345') | |
| focus(page.locator('#generate'),'generate');page.keyboard.press('Enter');expect(page.locator('#form-error')).to_be_visible() | |
| for label in ['Glass pear','Typography','Architecture','Cutout']: | |
| focus(page.get_by_role('button',name=label,exact=True),'example-'+label.lower().replace(' ','-')) | |
| focus(page.get_by_role('link',name='Qwen Research License'),'license') | |
| with context.expect_page() as popup:page.keyboard.press('Enter') | |
| popup.value.wait_for_load_state('domcontentloaded');assert popup.value.url.startswith('https://huggingface.co/Qwen/Qwen-Image-2.1/blob/');popup.value.close() | |
| capture('controls-open');evidence['controls_keyboard_upload_remove_error_license']=True | |
| elif args.phase=='validation': | |
| # Synthetic inputs only exercise rejection paths; no GPU request. | |
| page.locator('#prompt').fill('x'*4001) | |
| assert len(page.locator('#prompt').input_value())==4000 | |
| expect(page.locator('#prompt-count')).to_have_text('4,000 / 4,000') | |
| page.locator('#prompt').fill('A blue glass pear') | |
| page.locator('#seed-settings summary').click() | |
| page.locator('#randomize').uncheck() | |
| for invalid in ['', '-1', '4294967296', '1.5']: | |
| page.locator('#seed').fill(invalid) | |
| page.locator('#generate').click() | |
| expect(page.locator('#form-error')).to_contain_text('whole-number seed') | |
| expect(page.locator('#seed')).to_be_focused() | |
| page.locator('#seed').fill('42') | |
| page.locator('#reference-settings summary').click() | |
| with tempfile.TemporaryDirectory(prefix='qwen-ui-validation-') as folder: | |
| folder=Path(folder) | |
| (folder/'invalid.txt').write_text('This is a synthetic invalid reference input.') | |
| (folder/'too-large.png').write_bytes(b'0'*20_000_001) | |
| Image.new('RGB',(5000,4001),'white').save(folder/'too-many-pixels.png') | |
| for filename,message in [('invalid.txt','PNG, JPEG, or WebP'),('too-large.png','20 MB'),('too-many-pixels.png','20 megapixels')]: | |
| with page.expect_file_chooser() as chooser:page.locator('#upload-reference').click() | |
| chooser.value.set_files(str(folder/filename)) | |
| expect(page.locator('#form-error')).to_contain_text(message) | |
| expect(page.locator('#reference-preview')).not_to_be_visible() | |
| expect(page.locator('#generate')).to_be_enabled() | |
| capture(filename.replace('.','-')) | |
| assert not queue_joins,queue_joins | |
| evidence['input_validation']={'prompt_max_length':4000,'invalid_seeds':['empty',-1,4294967296,1.5], | |
| 'reference_rejections':['format','20 MB','20 megapixels'],'no_gpu_submissions':True} | |
| elif args.phase=='generate': | |
| page.locator('#generate').click();expect(page.locator('#form-error')).to_be_visible() | |
| page.locator('#prompt').fill(EXAMPLE_PROMPTS[0]) | |
| if not args.mobile: | |
| page.locator('#seed-settings summary').click();page.locator('#randomize').uncheck();page.locator('#seed').fill('42');page.locator('#seed-settings summary').click() | |
| started=time.perf_counter() | |
| if args.mobile:page.locator('#generate').tap() | |
| else:page.locator('#generate').dblclick() | |
| expect(page.locator('#generate')).to_be_disabled();expect(page.locator('#job-status')).to_be_visible();capture('generating') | |
| page.wait_for_function("Number(document.querySelector('#job-progress').getAttribute('value')) > 0",timeout=30000) | |
| evidence['live_progress']={'value':page.locator('#job-progress').get_attribute('value'),'text':page.locator('#job-message').inner_text()} | |
| capture('progress') | |
| record=download('generated');expect(page.locator('#generate')).to_be_enabled(timeout=30000) | |
| evidence['button_to_downloads_seconds']=time.perf_counter()-started | |
| assert queue_joins.count(generation_index)==1,queue_joins | |
| assert record['parameters']['steps']==40 | |
| assert record['parameters']['seed']!=42 if args.mobile else record['parameters']['seed']==42 | |
| capture('generated') | |
| for selector,label,ext in [('#download-png','png-download','png'),('#download-recipe','recipe-download','json')]: | |
| focus(page.locator(selector),label) | |
| with page.expect_download() as pending:page.keyboard.press('Enter') | |
| pending.value.save_as(str(out/f'keyboard-download.{ext}')) | |
| assert (out/f'keyboard-download.{ext}').read_bytes()==(out/f'generated.{ext}').read_bytes() | |
| focus(page.locator('#use-reference'),'use-reference') | |
| focus(page.locator('#fullscreen'),'fullscreen');page.keyboard.press('Enter') | |
| expect(page.locator('#image-dialog')).to_be_visible();expect(page.locator('#close-fullscreen')).to_be_focused() | |
| for key in ['Tab','Shift+Tab','Tab']: | |
| page.keyboard.press(key) | |
| assert page.locator('#image-dialog').evaluate('e=>e.contains(document.activeElement)') | |
| focus(page.locator('#close-fullscreen'),'close-fullscreen');capture('fullscreen') | |
| b=page.locator('#image-dialog').bounding_box();assert b['width']==page.viewport_size['width'] and b['height']==page.viewport_size['height'] | |
| page.keyboard.press('Escape');expect(page.locator('#image-dialog')).not_to_be_visible();expect(page.locator('#fullscreen')).to_be_focused() | |
| page.locator('#fullscreen').click();page.locator('#close-fullscreen').click();expect(page.locator('#image-dialog')).not_to_be_visible() | |
| evidence['same_page_error_recovery_and_keyboard_downloads_fullscreen']=True | |
| # Explicit network fault injection; never a fabricated GPU failure. | |
| def abort_submission(route):route.abort('failed') | |
| pattern=url+'/gradio_api/queue/join*' | |
| page.route(pattern,abort_submission) | |
| original_id=page.locator('#canvas').get_attribute('data-generation-id') | |
| page.locator('#generate').click() | |
| expect(page.locator('#form-error')).to_be_visible(timeout=30000) | |
| expect(page.locator('#generate')).to_be_enabled(timeout=30000) | |
| assert page.locator('#canvas').get_attribute('data-generation-id')==original_id | |
| capture('network-error-preserves-result') | |
| page.unroute(pattern,abort_submission) | |
| evidence['injected_network_failure']={'request':'queue/join','old_result_preserved':True,'controls_restored':True,'visible_error':page.locator('#form-error').inner_text()} | |
| elif args.phase=='examples': | |
| ids={};records={} | |
| if args.reference:upload() | |
| for run in range(2): | |
| for i,label in enumerate(['Glass pear','Typography','Architecture','Cutout']): | |
| previous=page.locator('#canvas').get_attribute('data-generation-id') | |
| started=time.perf_counter() | |
| page.get_by_role('button',name=label,exact=True).click() | |
| expect(page.locator('#prompt')).to_have_value(EXAMPLE_PROMPTS[i]) | |
| page.wait_for_function("old=>document.querySelector('#canvas').dataset.generationId && document.querySelector('#canvas').dataset.generationId!==old",arg=previous,timeout=300000) | |
| expect(page.locator('#generate')).to_be_enabled(timeout=30000) | |
| evidence['observations'].append({'example':i+1,'pass':run+1, | |
| 'button_to_result_seconds':time.perf_counter()-started, | |
| 'cached':page.locator('#cache-note').is_visible()}) | |
| record=download(f'example-{i+1}-pass-{run+1}') | |
| assert record['parameters']['seed']==42 and record['reference_pixel_sha256'] is None | |
| aspect=['1:1','3:4','16:9','1:1'][i] | |
| assert record['prompt']==EXAMPLE_PROMPTS[i] | |
| assert record['parameters']['resolution']=='1K' and record['parameters']['steps']==40 | |
| assert [record['parameters']['width'],record['parameters']['height']]==list(SIZES['1K'][aspect]) | |
| expect(page.get_by_role('radio',name=aspect,exact=True)).to_be_checked() | |
| expect(page.get_by_role('radio',name='1K',exact=True)).to_be_checked() | |
| expect(page.get_by_role('radio',name='40',exact=True)).to_be_checked() | |
| expect(page.locator('#reference-preview')).not_to_be_visible() | |
| expect(page.locator('#randomize')).not_to_be_checked() | |
| expect(page.locator('#transparent')).to_be_checked(checked=i==3) | |
| if run==0:ids[i]=record['id'];records[i]=record | |
| else: | |
| assert record['id']==ids[i];expect(page.locator('#cache-note')).to_be_visible() | |
| assert record==records[i] | |
| assert (out/f'example-{i+1}-pass-1.png').read_bytes()==(out/f'example-{i+1}-pass-2.png').read_bytes() | |
| if i==3:assert record['output']['alpha_extrema'][0]==0 and record['output']['fraction_alpha_below_128']>0 | |
| evidence['observations'].append({'example':i+1,'pass':run+1,'alignment':alignment()}) | |
| capture(f'example-{i+1}-pass-{run+1}') | |
| elif args.phase=='reuse_cutout': | |
| page.get_by_role('button',name='Cutout',exact=True).click() | |
| original=download('cutout');expect(page.locator('#generate')).to_be_enabled() | |
| assert original['output']['alpha_extrema'][0]==0 | |
| assert original['output']['fraction_alpha_below_128']>0 | |
| page.get_by_role('button',name='Cutout',exact=True).click() | |
| expect(page.locator('#cache-note')).to_be_visible(timeout=30000) | |
| expect(page.locator('#generate')).to_be_enabled() | |
| assert page.locator('#canvas').get_attribute('data-generation-id')==original['id'] | |
| capture('cached-cutout') | |
| if args.mobile:page.locator('#use-reference').tap() | |
| else:page.locator('#use-reference').click() | |
| expect(page.locator('#reference-preview')).to_be_visible() | |
| expect(page.locator('#prompt')).to_be_focused() | |
| copied_hash=page.locator('#reference-file').evaluate('''async e=>{ | |
| const hash=await crypto.subtle.digest('SHA-256',await e.files[0].arrayBuffer()); | |
| return [...new Uint8Array(hash)].map(v=>v.toString(16).padStart(2,'0')).join('');}''') | |
| assert copied_hash==original['output']['png_sha256'] | |
| evidence['cutout_reuse']={'original_id':original['id'],'png_bytes_preserved':True, | |
| 'png_sha256':copied_hash,'alpha_extrema':original['output']['alpha_extrema'], | |
| 'fraction_alpha_below_128':original['output']['fraction_alpha_below_128'],'cache_hit':True} | |
| capture('cutout-as-reference') | |
| elif args.phase=='iterate': | |
| page.locator('#prompt').fill(EXAMPLE_PROMPTS[0]) | |
| if not args.mobile: | |
| page.locator('#seed-settings summary').click();page.locator('#randomize').uncheck();page.locator('#seed').fill('42') | |
| page.locator('#seed-settings summary').click() | |
| started=time.perf_counter();page.locator('#generate').click() | |
| initial=download('iteration-start');expect(page.locator('#generate')).to_be_enabled() | |
| evidence['initial_button_to_downloads_seconds']=time.perf_counter()-started | |
| capture('iteration-start') | |
| evidence['generated_alignment']=alignment() | |
| action_boxes=page.locator('#result-actions>*').evaluate_all('es=>es.map(e=>({label:e.textContent||e.ariaLabel,height:e.getBoundingClientRect().height,width:e.getBoundingClientRect().width}))') | |
| assert all(b['height']==(44 if args.mobile else 32) for b in action_boxes),action_boxes | |
| if args.mobile:assert all(b['width']>=44 for b in action_boxes),action_boxes | |
| assert page.evaluate('document.documentElement.scrollWidth<=innerWidth') | |
| evidence['result_action_geometry']=action_boxes | |
| for selector,label,ext in [('#download-png','png-download','png'),('#download-recipe','recipe-download','json')]: | |
| focus(page.locator(selector),label) | |
| with page.expect_download() as pending:page.keyboard.press('Enter') | |
| pending.value.save_as(str(out/f'keyboard-start.{ext}')) | |
| assert (out/f'keyboard-start.{ext}').read_bytes()==(out/f'iteration-start.{ext}').read_bytes() | |
| focus(page.locator('#fullscreen'),'fullscreen');page.keyboard.press('Enter') | |
| expect(page.locator('#close-fullscreen')).to_be_focused() | |
| for key in ['Tab','Shift+Tab']: | |
| page.keyboard.press(key);expect(page.locator('#close-fullscreen')).to_be_focused() | |
| focus(page.locator('#close-fullscreen'),'close-fullscreen');capture('fullscreen') | |
| b=page.locator('#image-dialog').bounding_box() | |
| assert b['width']==page.viewport_size['width'] and b['height']==page.viewport_size['height'] | |
| page.keyboard.press('Escape');expect(page.locator('#fullscreen')).to_be_focused() | |
| page.locator('#fullscreen').click();page.locator('#close-fullscreen').click() | |
| focus(page.locator('#use-reference'),'use-reference') | |
| if args.mobile:page.locator('#use-reference').tap() | |
| else:page.keyboard.press('Enter') | |
| expect(page.locator('#reference-preview')).to_be_visible() | |
| expect(page.locator('#prompt')).to_have_value('');expect(page.locator('#prompt')).to_be_focused() | |
| expect(page.get_by_role('radio',name='2K',exact=True)).to_be_checked() | |
| expect(page.locator('#use-reference')).to_be_enabled() | |
| reference_file=page.locator('#reference-file').evaluate('''async e=>{ | |
| const f=e.files[0];const hash=await crypto.subtle.digest('SHA-256',await f.arrayBuffer()); | |
| return {name:f.name,size:f.size,type:f.type,sha256:[...new Uint8Array(hash)].map(v=>v.toString(16).padStart(2,'0')).join('')};}''') | |
| assert reference_file['sha256']==initial['output']['png_sha256'] | |
| assert reference_file['size']==(out/'iteration-start.png').stat().st_size | |
| evidence['use_as_reference']={'file':reference_file,'original_png_bytes_preserved':True, | |
| 'selects_2k':True,'clears_and_focuses_edit_prompt':True} | |
| capture('use-as-reference') | |
| choose(args.resolution);choose(str(args.edit_steps)) | |
| page.locator('#prompt').fill('Change the blue glass pear to amber orange glass. Keep the pear shape, plinth, camera angle, lighting and background.') | |
| started=time.perf_counter();page.locator('#generate').click() | |
| expect(page.locator('#use-reference')).to_be_disabled() | |
| capture('editing') | |
| evidence['busy_alignment']=alignment() | |
| page.wait_for_function('''old=>{ | |
| const id=document.querySelector('#canvas').dataset.generationId; | |
| return (id && id!==old)||!document.querySelector('#form-error').hidden; | |
| }''',arg=initial['id'],timeout=300000) | |
| assert page.locator('#form-error').is_hidden(),page.locator('#form-error').inner_text() | |
| edited=download('iteration-edited');expect(page.locator('#generate')).to_be_enabled() | |
| evidence['edit_button_to_downloads_seconds']=time.perf_counter()-started | |
| with Image.open(out/'iteration-start.png') as img: | |
| ref=ImageOps.exif_transpose(img).convert('RGBA') | |
| expected=hashlib.sha256(str(ref.size).encode()+ref.tobytes()).hexdigest() | |
| assert edited['reference_pixel_sha256']==expected | |
| assert edited['parameters']['resolution']==args.resolution and edited['parameters']['steps']==args.edit_steps | |
| if args.mobile:assert edited['parameters']['seed']!=initial['parameters']['seed'] | |
| else:assert initial['parameters']['seed']==edited['parameters']['seed']==42 | |
| assert edited['id']!=initial['id'] | |
| expect(page.locator('#use-reference')).to_be_enabled();capture('iteration-edited') | |
| evidence['edited_alignment']=alignment() | |
| evidence['generate_reuse_edit_download']={'initial_id':initial['id'],'edited_id':edited['id'], | |
| 'reference_pixel_sha256':expected,'resolution':args.resolution,'steps':args.edit_steps} | |
| page.locator('#remove-reference').click() | |
| expect(page.locator('#reference-preview')).not_to_be_visible() | |
| assert page.locator('#reference-file').evaluate('e=>e.files.length')==0 | |
| expect(page.locator('#upload-reference')).to_be_focused() | |
| capture('iteration-reference-cleared') | |
| # Failed image retrieval must restore controls and allow retry. | |
| file_url=page.locator('#download-png').get_attribute('href') | |
| def abort_reference(route):route.abort('failed') | |
| page.route(file_url,abort_reference);page.locator('#use-reference').click() | |
| expect(page.locator('#form-error')).to_be_visible() | |
| expect(page.locator('#use-reference')).to_be_enabled() | |
| expect(page.locator('#generate')).to_be_enabled() | |
| expect(page.locator('#use-reference')).to_be_focused() | |
| assert page.locator('#canvas').get_attribute('data-generation-id')==edited['id'] | |
| capture('reuse-network-error') | |
| page.unroute(file_url,abort_reference);page.locator('#use-reference').click() | |
| expect(page.locator('#reference-preview')).to_be_visible() | |
| expect(page.locator('#prompt')).to_be_focused() | |
| expect(page.locator('#form-error')).not_to_be_visible() | |
| edited_hash=page.locator('#reference-file').evaluate('''async e=>{ | |
| const hash=await crypto.subtle.digest('SHA-256',await e.files[0].arrayBuffer()); | |
| return [...new Uint8Array(hash)].map(v=>v.toString(16).padStart(2,'0')).join('');}''') | |
| assert edited_hash==edited['output']['png_sha256'] | |
| evidence['reused_edited_png_sha256']=edited_hash | |
| evidence['reuse_network_failure_recovery']=True | |
| capture('reuse-edited-image') | |
| if args.mobile: | |
| page.set_viewport_size({'width':320,'height':800});capture('result-320') | |
| assert page.evaluate('document.documentElement.scrollWidth<=innerWidth') | |
| elif args.phase=='edit': | |
| upload();capture('reference-uploaded');choose(args.resolution) | |
| page.locator('#prompt').fill('Change the blue glass pear to amber orange glass. Keep the pear shape, plinth, camera angle, lighting and background.') | |
| page.locator('#seed-settings summary').click();page.locator('#randomize').uncheck();page.locator('#seed').fill('42') | |
| started=time.perf_counter() | |
| page.locator('#generate').click();record=download('edited') | |
| evidence['button_to_downloads_seconds']=time.perf_counter()-started | |
| with Image.open(args.reference) as image: | |
| ref=ImageOps.exif_transpose(image).convert('RGBA');expected=hashlib.sha256(str(ref.size).encode()+ref.tobytes()).hexdigest() | |
| assert record['reference_pixel_sha256']==expected | |
| expect(page.locator('#generate')).to_be_enabled();capture('edited') | |
| page.locator('#remove-reference').click();expect(page.locator('#reference-preview')).not_to_be_visible();capture('reference-cleared') | |
| evidence['status']='completed' | |
| except Exception as exc: | |
| evidence['status']='failed';evidence['failure']=str(exc)[:1800] | |
| evidence['aria']=page.locator('body').aria_snapshot();capture('failure') | |
| raise | |
| finally: | |
| evidence['queue_joins']=queue_joins | |
| (out/'observations.json').write_text(json.dumps(evidence,indent=2)) | |
| print(f'Server UI evidence: {out}',flush=True);browser.close() | |
| if __name__=='__main__':main() | |