Spaces:
Paused
Paused
Publish Qwen 2.1 Image source 7fd03f3543e8 (part 2)
Browse files- scripts/verify_ui.py +106 -1
- style.css +7 -2
scripts/verify_ui.py
CHANGED
|
@@ -8,6 +8,7 @@ Usage: uv run --with playwright==1.58.0 --with huggingface_hub==1.32.0
|
|
| 8 |
import argparse
|
| 9 |
from datetime import datetime, timezone
|
| 10 |
import hashlib
|
|
|
|
| 11 |
import httpx
|
| 12 |
import json
|
| 13 |
from pathlib import Path
|
|
@@ -28,7 +29,7 @@ from settings import EXAMPLE_PROMPTS, SIZES, STEPS
|
|
| 28 |
def main():
|
| 29 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 30 |
parser.add_argument("--space", default="Mike0021/still-image21-studio")
|
| 31 |
-
parser.add_argument("--phase", choices=["layout", "inspect", "controls", "generate", "examples", "edit", "upload"], default="layout")
|
| 32 |
parser.add_argument("--mobile", action="store_true")
|
| 33 |
parser.add_argument("--reference", type=Path)
|
| 34 |
parser.add_argument("--resolution", choices=list(SIZES))
|
|
@@ -112,6 +113,10 @@ def main():
|
|
| 112 |
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
| 113 |
expect(page.get_by_role("button", name="Generate image")).to_be_visible(timeout=60000)
|
| 114 |
config = context.request.get(url+"/config").json()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
generation_index = next(d["id"] for d in config["dependencies"] if d.get("api_name") == "generate")
|
| 116 |
|
| 117 |
def capture(name):
|
|
@@ -150,6 +155,46 @@ def main():
|
|
| 150 |
"status_text": page.locator("#result-status").inner_text(), "downloads_verified": True})
|
| 151 |
return record
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def fullscreen():
|
| 154 |
page.get_by_role("button", name="Fullscreen", exact=True).click()
|
| 155 |
expect(page.get_by_role("button", name="Exit fullscreen mode", exact=True)).to_be_visible()
|
|
@@ -171,8 +216,68 @@ def main():
|
|
| 171 |
"qualification": "CSS zoom check, not physical-device or browser-chrome zoom certification"})
|
| 172 |
page.evaluate("document.body.style.zoom = ''")
|
| 173 |
evidence["body_text"] = page.locator("body").inner_text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
elif args.phase == "inspect":
|
| 175 |
evidence["aria"] = page.locator("body").aria_snapshot()
|
|
|
|
| 176 |
evidence["canvas_html"] = page.locator("#canvas").evaluate("e=>e.outerHTML")
|
| 177 |
evidence["header_ancestors"] = page.locator(".app-bar").evaluate("""e => {
|
| 178 |
let rows=[]; while(e && rows.length<10) {const s=getComputedStyle(e);
|
|
|
|
| 8 |
import argparse
|
| 9 |
from datetime import datetime, timezone
|
| 10 |
import hashlib
|
| 11 |
+
from io import BytesIO
|
| 12 |
import httpx
|
| 13 |
import json
|
| 14 |
from pathlib import Path
|
|
|
|
| 29 |
def main():
|
| 30 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 31 |
parser.add_argument("--space", default="Mike0021/still-image21-studio")
|
| 32 |
+
parser.add_argument("--phase", choices=["layout", "inspect", "keyboard", "controls", "generate", "examples", "edit", "upload"], default="layout")
|
| 33 |
parser.add_argument("--mobile", action="store_true")
|
| 34 |
parser.add_argument("--reference", type=Path)
|
| 35 |
parser.add_argument("--resolution", choices=list(SIZES))
|
|
|
|
| 113 |
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
| 114 |
expect(page.get_by_role("button", name="Generate image")).to_be_visible(timeout=60000)
|
| 115 |
config = context.request.get(url+"/config").json()
|
| 116 |
+
expect(page).to_have_title("Qwen 2.1 Image")
|
| 117 |
+
expect(page.get_by_role("heading", name="Qwen 2.1 Image", exact=True)).to_be_visible()
|
| 118 |
+
assert info.card_data["title"] == "Qwen 2.1 Image"
|
| 119 |
+
evidence["titles"] = {"browser": page.title(), "space": info.card_data["title"]}
|
| 120 |
generation_index = next(d["id"] for d in config["dependencies"] if d.get("api_name") == "generate")
|
| 121 |
|
| 122 |
def capture(name):
|
|
|
|
| 155 |
"status_text": page.locator("#result-status").inner_text(), "downloads_verified": True})
|
| 156 |
return record
|
| 157 |
|
| 158 |
+
def tab_to(target):
|
| 159 |
+
for _ in range(80):
|
| 160 |
+
page.keyboard.press("Tab")
|
| 161 |
+
if target.evaluate("e => e === document.activeElement"):
|
| 162 |
+
return
|
| 163 |
+
raise AssertionError("Target not reachable by sequential Tab navigation")
|
| 164 |
+
|
| 165 |
+
def focus_evidence(target, name, proxy=None):
|
| 166 |
+
tab_to(target)
|
| 167 |
+
proxy = proxy or target
|
| 168 |
+
row = proxy.evaluate("""e => {
|
| 169 |
+
const r=e.getBoundingClientRect(), s=getComputedStyle(e);
|
| 170 |
+
let bounds={left:r.left,top:r.top,right:r.right,bottom:r.bottom}, clips=[];
|
| 171 |
+
for(let p=e.parentElement;p;p=p.parentElement) {
|
| 172 |
+
const c=getComputedStyle(p), b=p.getBoundingClientRect();
|
| 173 |
+
if(/hidden|clip|scroll|auto/.test(c.overflowX)) {
|
| 174 |
+
bounds.left=Math.max(bounds.left,b.left); bounds.right=Math.min(bounds.right,b.right);
|
| 175 |
+
}
|
| 176 |
+
if(/hidden|clip|scroll|auto/.test(c.overflowY)) {
|
| 177 |
+
bounds.top=Math.max(bounds.top,b.top); bounds.bottom=Math.min(bounds.bottom,b.bottom);
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
return {outline:s.outline,offset:s.outlineOffset,font:s.fontFamily,
|
| 181 |
+
width:r.width,height:r.height,visibleWidth:bounds.right-bounds.left,
|
| 182 |
+
visibleHeight:bounds.bottom-bounds.top,focused:document.activeElement.outerHTML.slice(0,500)};
|
| 183 |
+
}""")
|
| 184 |
+
assert row["outline"].startswith("rgb("), row
|
| 185 |
+
assert "solid 3px" in row["outline"], row
|
| 186 |
+
assert row["visibleWidth"] >= row["width"]-1 and row["visibleHeight"] >= row["height"]-1, row
|
| 187 |
+
# Inspect the rendered control, not just CSS declarations. Each side
|
| 188 |
+
# must contain focus-blue pixels inside its bounding box.
|
| 189 |
+
bitmap = Image.open(BytesIO(proxy.screenshot())).convert("RGB")
|
| 190 |
+
colors = {(37,99,235), (147,197,253)}
|
| 191 |
+
w,h=bitmap.size
|
| 192 |
+
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)]]
|
| 193 |
+
row["focus_pixels_top_bottom_left_right"] = [sum(pixel in colors for pixel in strip.get_flattened_data()) for strip in strips]
|
| 194 |
+
assert all(n >= 3 for n in row["focus_pixels_top_bottom_left_right"]), row
|
| 195 |
+
evidence.setdefault("focus_checks", []).append({"name":name, **row})
|
| 196 |
+
page.screenshot(path=str(out / f"focus-{name}-screen.png"), full_page=True)
|
| 197 |
+
|
| 198 |
def fullscreen():
|
| 199 |
page.get_by_role("button", name="Fullscreen", exact=True).click()
|
| 200 |
expect(page.get_by_role("button", name="Exit fullscreen mode", exact=True)).to_be_visible()
|
|
|
|
| 216 |
"qualification": "CSS zoom check, not physical-device or browser-chrome zoom certification"})
|
| 217 |
page.evaluate("document.body.style.zoom = ''")
|
| 218 |
evidence["body_text"] = page.locator("body").inner_text()
|
| 219 |
+
elif args.phase == "keyboard":
|
| 220 |
+
fonts = page.locator(".gradio-container").evaluate("""root => [...new Set(
|
| 221 |
+
[...root.querySelectorAll('*')].filter(e=>e.getClientRects().length &&
|
| 222 |
+
[...e.childNodes].some(n=>n.nodeType===3 && n.textContent.trim()))
|
| 223 |
+
.map(e=>getComputedStyle(e).fontFamily))]""")
|
| 224 |
+
assert fonts == ["Arial, sans-serif"], fonts
|
| 225 |
+
evidence["visible_text_font_families"] = fonts
|
| 226 |
+
focus_evidence(page.get_by_label("Prompt", exact=True), "prompt")
|
| 227 |
+
for shape in ["1:1", "4:3", "3:4", "16:9", "9:16"]:
|
| 228 |
+
radio = page.get_by_role("radio", name=shape, exact=True)
|
| 229 |
+
# Native radio groups expose the checked item to Tab and
|
| 230 |
+
# use arrow keys to reach the other items.
|
| 231 |
+
radio.check()
|
| 232 |
+
focus_evidence(radio, "shape-"+shape.replace(":","-"), radio.locator(".."))
|
| 233 |
+
page.keyboard.press("ArrowLeft")
|
| 234 |
+
expect(page.get_by_role("radio", name="16:9", exact=True)).to_be_checked()
|
| 235 |
+
expect(page.locator("#size-hint")).to_have_text("1,376 × 768 pixels")
|
| 236 |
+
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)})")
|
| 237 |
+
for size in ["1K","2K"]:
|
| 238 |
+
radio = page.get_by_role("radio", name=size, exact=True)
|
| 239 |
+
radio.check()
|
| 240 |
+
focus_evidence(radio, "size-"+size, radio.locator(".."))
|
| 241 |
+
combo=page.get_by_role("combobox",name="Quality")
|
| 242 |
+
focus_evidence(combo, "quality")
|
| 243 |
+
page.keyboard.press("ArrowDown")
|
| 244 |
+
expect(page.get_by_role("option").first).to_be_visible()
|
| 245 |
+
page.screenshot(path=str(out / "quality-keyboard-open-screen.png"), full_page=True)
|
| 246 |
+
page.keyboard.press("Escape")
|
| 247 |
+
check=page.get_by_label("Transparent background",exact=True)
|
| 248 |
+
focus_evidence(check, "transparency", check.locator(".."))
|
| 249 |
+
page.keyboard.press("Space")
|
| 250 |
+
expect(check).to_be_checked()
|
| 251 |
+
ref=page.locator("#reference-settings > button")
|
| 252 |
+
focus_evidence(ref, "reference-accordion")
|
| 253 |
+
page.keyboard.press("Enter")
|
| 254 |
+
expect(page.locator("#reference")).to_be_visible()
|
| 255 |
+
evidence["reference_upload_aria"] = page.locator("#reference").aria_snapshot()
|
| 256 |
+
page.keyboard.press("Enter")
|
| 257 |
+
seed=page.locator("#seed-settings > button")
|
| 258 |
+
focus_evidence(seed, "seed-accordion")
|
| 259 |
+
page.keyboard.press("Space")
|
| 260 |
+
randomize=page.get_by_label("New seed each time",exact=True)
|
| 261 |
+
focus_evidence(randomize,"new-seed",randomize.locator(".."))
|
| 262 |
+
page.keyboard.press("Space")
|
| 263 |
+
expect(randomize).not_to_be_checked()
|
| 264 |
+
focus_evidence(page.get_by_label("Seed",exact=True),"seed-number")
|
| 265 |
+
page.keyboard.type("123")
|
| 266 |
+
button=page.get_by_role("button",name="Generate image",exact=True)
|
| 267 |
+
focus_evidence(button,"generate")
|
| 268 |
+
page.keyboard.press("Enter")
|
| 269 |
+
expect(page.get_by_text("Describe the image you would like to make.",exact=False)).to_be_visible(timeout=15000)
|
| 270 |
+
expect(button).to_be_enabled()
|
| 271 |
+
evidence["keyboard_empty_validation"] = True
|
| 272 |
+
for name in ["Glass pear","Typography","Architecture","Transparent object"]:
|
| 273 |
+
focus_evidence(page.get_by_role("button",name=name,exact=True), "example-"+name.lower().replace(" ","-"))
|
| 274 |
+
focus_evidence(page.get_by_role("link",name="Qwen Research License"),"license")
|
| 275 |
+
for width in [320,390] if args.mobile else [1440]:
|
| 276 |
+
page.set_viewport_size({"width":width,"height":844 if args.mobile else 1000})
|
| 277 |
+
focus_evidence(button,"generate-width-"+str(width))
|
| 278 |
elif args.phase == "inspect":
|
| 279 |
evidence["aria"] = page.locator("body").aria_snapshot()
|
| 280 |
+
evidence["control_html"] = {name: page.locator(name).evaluate("e=>e.outerHTML") for name in ["#quality","#reference-settings","#seed-settings","#shape"]}
|
| 281 |
evidence["canvas_html"] = page.locator("#canvas").evaluate("e=>e.outerHTML")
|
| 282 |
evidence["header_ancestors"] = page.locator(".app-bar").evaluate("""e => {
|
| 283 |
let rows=[]; while(e && rows.length<10) {const s=getComputedStyle(e);
|
style.css
CHANGED
|
@@ -9,6 +9,7 @@ main, .gradio-container, .gradio-container > .main, .gradio-container .fillable
|
|
| 9 |
.gradio-container .form { background: transparent !important; border: 0 !important; border-radius: 0 !important; }
|
| 10 |
.gradio-container .block { background: transparent; border-color: var(--line); box-shadow: none; }
|
| 11 |
.gradio-container input:not([type="checkbox"]):not([type="radio"]), .gradio-container textarea { background: white !important; color: var(--ink) !important; }
|
|
|
|
| 12 |
.app-bar { display: flex; align-items: center; min-height: 72px; border-bottom: 1px solid #e5e7eb; margin-bottom: 8px; }
|
| 13 |
.app-bar h1 { margin: 0 !important; font-size: 20px !important; font-weight: 600; letter-spacing: -.4px; line-height: 1.4; }
|
| 14 |
#studio { gap: 32px; }
|
|
@@ -17,7 +18,7 @@ main, .gradio-container, .gradio-container > .main, .gradio-container .fillable
|
|
| 17 |
#controls .form { gap: 16px !important; }
|
| 18 |
#controls .details { border: 1px solid var(--line); padding: 0 !important; }
|
| 19 |
#controls .details > .label-wrap { min-height: 44px; padding: 12px !important; box-sizing: border-box; border-radius: 5px; }
|
| 20 |
-
#controls .details >
|
| 21 |
#controls .details .prose p { font-size: 12px; line-height: 1.5; color: var(--muted); }
|
| 22 |
#prompt textarea { font-size: 15px; line-height: 1.6; padding: 12px; border: 1px solid var(--line) !important; border-radius: 6px; }
|
| 23 |
#controls label { font-size: 13px; }
|
|
@@ -25,7 +26,11 @@ main, .gradio-container, .gradio-container > .main, .gradio-container .fillable
|
|
| 25 |
#shape label:has(input), #resolution label:has(input) { min-height: 44px; min-width: 0; flex: 1; justify-content: center; padding: 10px 4px; gap: 0; background: white; border: 1px solid var(--line); border-radius: 5px; box-sizing: border-box; }
|
| 26 |
#shape label:has(input:checked), #resolution label:has(input:checked) { background: #eef0f3; border-color: #737983; color: var(--ink); }
|
| 27 |
#shape input, #resolution input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
| 28 |
-
#quality
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
#transparent > label { min-height: 36px; align-items: center; padding: 6px 4px !important; border-radius: 4px; }
|
| 30 |
#size-hint p, #queue-note p, #example-note p { color: var(--muted); font-size: 12px; line-height: 1.5; }
|
| 31 |
#size-hint { margin-top: -9px; }
|
|
|
|
| 9 |
.gradio-container .form { background: transparent !important; border: 0 !important; border-radius: 0 !important; }
|
| 10 |
.gradio-container .block { background: transparent; border-color: var(--line); box-shadow: none; }
|
| 11 |
.gradio-container input:not([type="checkbox"]):not([type="radio"]), .gradio-container textarea { background: white !important; color: var(--ink) !important; }
|
| 12 |
+
.gradio-container ::placeholder { color: #68707b !important; opacity: 1 !important; }
|
| 13 |
.app-bar { display: flex; align-items: center; min-height: 72px; border-bottom: 1px solid #e5e7eb; margin-bottom: 8px; }
|
| 14 |
.app-bar h1 { margin: 0 !important; font-size: 20px !important; font-weight: 600; letter-spacing: -.4px; line-height: 1.4; }
|
| 15 |
#studio { gap: 32px; }
|
|
|
|
| 18 |
#controls .form { gap: 16px !important; }
|
| 19 |
#controls .details { border: 1px solid var(--line); padding: 0 !important; }
|
| 20 |
#controls .details > .label-wrap { min-height: 44px; padding: 12px !important; box-sizing: border-box; border-radius: 5px; }
|
| 21 |
+
#controls .details > [data-testid="accordion-content"] { padding: 0 12px 12px !important; }
|
| 22 |
#controls .details .prose p { font-size: 12px; line-height: 1.5; color: var(--muted); }
|
| 23 |
#prompt textarea { font-size: 15px; line-height: 1.6; padding: 12px; border: 1px solid var(--line) !important; border-radius: 6px; }
|
| 24 |
#controls label { font-size: 13px; }
|
|
|
|
| 26 |
#shape label:has(input), #resolution label:has(input) { min-height: 44px; min-width: 0; flex: 1; justify-content: center; padding: 10px 4px; gap: 0; background: white; border: 1px solid var(--line); border-radius: 5px; box-sizing: border-box; }
|
| 27 |
#shape label:has(input:checked), #resolution label:has(input:checked) { background: #eef0f3; border-color: #737983; color: var(--ink); }
|
| 28 |
#shape input, #resolution input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
| 29 |
+
#quality .container > .wrap { padding: 0 !important; border: 1px solid var(--line) !important; border-radius: 5px; background: white !important; }
|
| 30 |
+
#quality .wrap-inner { padding: 0 !important; }
|
| 31 |
+
#quality .secondary-wrap { padding: 0 10px !important; }
|
| 32 |
+
#quality input { min-height: 42px; }
|
| 33 |
+
#seed-settings input[type="number"] { min-height: 44px; border: 1px solid var(--line) !important; border-radius: 5px; padding: 10px !important; }
|
| 34 |
#transparent > label { min-height: 36px; align-items: center; padding: 6px 4px !important; border-radius: 4px; }
|
| 35 |
#size-hint p, #queue-note p, #example-note p { color: var(--muted); font-size: 12px; line-height: 1.5; }
|
| 36 |
#size-hint { margin-top: -9px; }
|