Files
1jehuang--jcode/scripts/widget_quality.py
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

194 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Quantitative quality metrics for Jcode Desktop inline widgets.
Captures widget states headlessly and scores each card from its PNG plus the
geometry in the capture manifest:
bottom_clip ink fraction in the last few px above the card's bottom
border. Sliced glyphs leave ink touching the boundary;
clean cards end with padding. Lower is better.
right_clip same for the right border (text running off the card).
row_fit |content_height mod line_height| in px, normalized to
[0, 0.5*lh]. Non-zero means the card cannot end on a whole
text row.
edge_margin min distance in px from any ink to the card border
(should be >= ~4px padding).
Usage:
scripts/widget_quality.py [--size WxH] [--states a,b,c] [--json]
Exit code 1 if any state fails a budget.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("error: pillow required", file=sys.stderr)
sys.exit(2)
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_BIN = ROOT / "target" / "debug" / "jcode-desktop"
WIDGET_STATES = [
"hotkey-help",
"model-picker",
"session-info",
"session-switcher",
"slash-suggestions",
]
# Budgets: a card fails when any metric exceeds these.
BOTTOM_CLIP_BUDGET = 0.02 # <=2% ink density in the clip band
RIGHT_CLIP_BUDGET = 0.02
EDGE_MARGIN_BUDGET = 3.0 # ink should keep >=3px from the border
ROW_CUT_BUDGET = 0.15 # clip window must end within 0.15 rows of a whole row
CLIP_BAND_PX = 4 # band width inspected inside each border
def luminance_ink(img: Image.Image, background_estimate: int = 16) -> Image.Image:
"""Binary ink mask: pixels notably darker than their card background."""
gray = img.convert("L")
# Card backgrounds are near-white (>=230); text is dark (<=120).
return gray.point(lambda v: 255 if v < 180 else 0)
def analyze(image_path: Path, geometry: dict) -> dict:
img = Image.open(image_path)
card = geometry["card"]
line_height = geometry["line_height"]
text_top = geometry["text_top"]
clip_bottom = geometry["visible_text_bottom"]
clip_right = geometry.get("visible_text_right")
x0 = max(int(card["x"]), 0)
y0 = max(int(card["y"]), 0)
x1 = min(int(card["x"] + card["width"]), img.width)
y1 = min(int(card["y"] + card["height"]), img.height)
if x1 - x0 < 8 or y1 - y0 < 8:
return {"error": "card rect out of bounds"}
ink = luminance_ink(img.crop((x0, y0, x1, y1)))
width, height = ink.size
pixels = ink.load()
def band_density(band) -> float:
bx0, by0, bx1, by1 = band
bx0 = max(bx0, 0); by0 = max(by0, 0)
bx1 = min(bx1, width); by1 = min(by1, height)
total = max((bx1 - bx0) * (by1 - by0), 1)
hits = sum(
1
for yy in range(by0, by1)
for xx in range(bx0, bx1)
if pixels[xx, yy]
)
return hits / total
# Glyphs sliced by the text clip line leave ink right at the boundary.
clip_y = int(clip_bottom - card["y"])
bottom_clip = band_density((0, clip_y - CLIP_BAND_PX, width, clip_y))
if clip_right is not None:
clip_x = int(clip_right - card["x"])
right_clip = band_density((clip_x - CLIP_BAND_PX, 0, clip_x, height))
else:
right_clip = band_density((width - 4 - CLIP_BAND_PX, 0, width - 4, height))
# Fraction of a text row that the visible clip window cuts through.
visible_rows = (clip_bottom - text_top) / line_height
row_cut = visible_rows - int(visible_rows)
row_cut = min(row_cut, 1.0 - row_cut) # distance to nearest whole row
# Min ink margin to the card border itself.
bbox = ink.getbbox()
if bbox:
_, _, ink_right, ink_bottom = bbox
edge_margin = min(height - ink_bottom, width - ink_right)
else:
edge_margin = min(width, height)
failures = []
if bottom_clip > BOTTOM_CLIP_BUDGET:
failures.append(f"bottom_clip {bottom_clip:.3f} > {BOTTOM_CLIP_BUDGET} (glyphs sliced at clip line)")
if right_clip > RIGHT_CLIP_BUDGET:
failures.append(f"right_clip {right_clip:.3f} > {RIGHT_CLIP_BUDGET} (text cut at right edge)")
if row_cut > ROW_CUT_BUDGET:
failures.append(f"row_cut {row_cut:.2f} rows > {ROW_CUT_BUDGET} (clip not row-aligned)")
if edge_margin < EDGE_MARGIN_BUDGET:
failures.append(f"edge_margin {edge_margin}px < {EDGE_MARGIN_BUDGET}px")
return {
"bottom_clip": round(bottom_clip, 4),
"right_clip": round(right_clip, 4),
"row_cut": round(row_cut, 3),
"edge_margin_px": edge_margin,
"failures": failures,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", type=Path, default=DEFAULT_BIN)
parser.add_argument("--size", default=None)
parser.add_argument("--states", default=",".join(WIDGET_STATES))
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
states = [s for s in args.states.split(",") if s]
results = {}
failed = False
with tempfile.TemporaryDirectory(prefix="jcode-widget-quality-") as tmp:
out = Path(tmp)
for state in states:
cmd = [
str(args.binary),
"--capture-gallery-screens", str(out),
"--gallery-state", state,
]
if args.size:
cmd += ["--capture-size", args.size]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if proc.returncode != 0:
results[state] = {"error": proc.stderr.strip()[:200]}
failed = True
continue
manifest = json.loads(proc.stdout)
screen = manifest["screens"][0]
geometry = screen.get("inline_widget")
if not geometry:
results[state] = {"error": "no inline widget geometry"}
failed = True
continue
metrics = analyze(out / screen["file"], geometry)
results[state] = metrics
if metrics.get("failures") or metrics.get("error"):
failed = True
if args.json:
print(json.dumps(results, indent=2))
else:
for state, metrics in results.items():
if "error" in metrics:
print(f"FAIL {state:20s} {metrics['error']}")
continue
status = "FAIL" if metrics["failures"] else "ok "
print(
f"{status} {state:20s} bottom_clip={metrics['bottom_clip']:.3f} "
f"right_clip={metrics['right_clip']:.3f} "
f"row_cut={metrics['row_cut']:.2f} "
f"edge_margin={metrics['edge_margin_px']}px"
)
for failure in metrics["failures"]:
print(f" - {failure}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())