Publish measure_vlm.py from marimo
Browse files- measure_vlm.py +179 -0
measure_vlm.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Does the tiny VLM echo the prompt - and does masking the logits fix it?
|
| 2 |
+
|
| 3 |
+
The card's open problem, as a measurement instead of an anecdote:
|
| 4 |
+
|
| 5 |
+
python measure_vlm.py # the built-in busy screen, CPU, no network
|
| 6 |
+
python measure_vlm.py --screen live # whatever is on this machine right now
|
| 7 |
+
|
| 8 |
+
It runs the same screen through the same model twice: once in the shipped `json`
|
| 9 |
+
mode (free text, where the 256M model answers by continuing the prompt), and once
|
| 10 |
+
in `choice` mode (digits only, so an echo is not representable). Then it prints
|
| 11 |
+
what each one did, and how long it took.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
from tiny_computer_agent import (
|
| 21 |
+
DecisionMenu, GrabScreen, ImageScreen, IndexedScreen, make_detector,
|
| 22 |
+
)
|
| 23 |
+
from tiny_computer_agent.models import DEFAULT_VISION_MODEL, VLMBrain
|
| 24 |
+
|
| 25 |
+
GOAL = "click 'Save' to save the document"
|
| 26 |
+
OUT = Path("preview")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def busy_desktop(width: int = 1100, height: int = 700):
|
| 30 |
+
"""A deliberately noisy screen: menus, a dialog, prose, a status bar.
|
| 31 |
+
|
| 32 |
+
The echo failure showed up on a *busy* live screen and not on a clean
|
| 33 |
+
synthetic one, so the fixture has to be busy to reproduce it.
|
| 34 |
+
"""
|
| 35 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 36 |
+
|
| 37 |
+
def font(size):
|
| 38 |
+
for candidate in ("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
| 39 |
+
"C:/Windows/Fonts/arial.ttf"):
|
| 40 |
+
try:
|
| 41 |
+
return ImageFont.truetype(candidate, size)
|
| 42 |
+
except Exception:
|
| 43 |
+
continue
|
| 44 |
+
return ImageFont.load_default()
|
| 45 |
+
|
| 46 |
+
image = Image.new("RGB", (width, height), (238, 240, 244))
|
| 47 |
+
draw = ImageDraw.Draw(image)
|
| 48 |
+
draw.rectangle([0, 0, width, 30], fill=(38, 40, 48))
|
| 49 |
+
for index, label in enumerate(["File", "Edit", "View", "Insert", "Format", "Tools",
|
| 50 |
+
"Window", "Help"]):
|
| 51 |
+
draw.text((16 + index * 74, 8), label, fill="white", font=font(14))
|
| 52 |
+
|
| 53 |
+
draw.rectangle([0, 30, 220, height], fill=(226, 229, 234))
|
| 54 |
+
for index, item in enumerate(["Documents", "Downloads", "Pictures", "Music",
|
| 55 |
+
"Reports", "Archives", "Scratch"]):
|
| 56 |
+
draw.text((18, 52 + index * 30), item, fill=(40, 44, 52), font=font(14))
|
| 57 |
+
|
| 58 |
+
draw.rectangle([280, 90, 1000, 470], fill="white", outline=(120, 124, 132), width=2)
|
| 59 |
+
draw.text((300, 106), "Untitled document - unsaved changes", fill=(28, 30, 34), font=font(16))
|
| 60 |
+
draw.text((300, 150),
|
| 61 |
+
"Your changes will be lost if you close this window without saving.",
|
| 62 |
+
fill=(70, 74, 82), font=font(13))
|
| 63 |
+
draw.text((300, 172), "The file has not been written to disk yet.", fill=(70, 74, 82),
|
| 64 |
+
font=font(13))
|
| 65 |
+
draw.text((300, 214), "File name", fill=(60, 62, 70), font=font(14))
|
| 66 |
+
draw.rectangle([300, 238, 960, 272], fill=(252, 252, 253), outline=(120, 124, 132))
|
| 67 |
+
draw.text((310, 247), "report.txt", fill=(30, 32, 36), font=font(14))
|
| 68 |
+
draw.rectangle([300, 330, 470, 374], fill=(66, 111, 220), outline=(36, 62, 140), width=2)
|
| 69 |
+
draw.text((330, 342), "Save", fill="white", font=font(16))
|
| 70 |
+
draw.rectangle([500, 330, 670, 374], fill=(228, 230, 235), outline=(120, 124, 132))
|
| 71 |
+
draw.text((520, 342), "Don't Save", fill=(30, 32, 36), font=font(15))
|
| 72 |
+
draw.rectangle([700, 330, 860, 374], fill=(228, 230, 235), outline=(120, 124, 132))
|
| 73 |
+
draw.text((730, 342), "Cancel", fill=(30, 32, 36), font=font(16))
|
| 74 |
+
|
| 75 |
+
draw.rectangle([280, 510, 1000, 660], fill=(248, 249, 251), outline=(150, 154, 160))
|
| 76 |
+
draw.text((300, 524), "Recent files", fill=(40, 44, 52), font=font(15))
|
| 77 |
+
for index, name in enumerate(["report-final-v3.txt", "notes.md", "budget.xlsx",
|
| 78 |
+
"draft.txt", "screenshot.png"]):
|
| 79 |
+
draw.text((300, 552 + index * 20), name, fill=(66, 70, 78), font=font(13))
|
| 80 |
+
|
| 81 |
+
draw.rectangle([0, height - 28, width, height], fill=(52, 56, 64))
|
| 82 |
+
draw.text((12, height - 22), "report.txt - modified - UTF-8 - line 42, column 7",
|
| 83 |
+
fill=(206, 210, 216), font=font(13))
|
| 84 |
+
return image
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _open_gtk_window():
|
| 88 |
+
"""Open the GTK demo window on a fresh session bus, so AT-SPI can see it."""
|
| 89 |
+
import os
|
| 90 |
+
import subprocess
|
| 91 |
+
import sys
|
| 92 |
+
import time
|
| 93 |
+
|
| 94 |
+
address = subprocess.run(["dbus-daemon", "--session", "--fork", "--print-address=1"],
|
| 95 |
+
capture_output=True, text=True).stdout.strip()
|
| 96 |
+
os.environ.setdefault("DISPLAY", ":99")
|
| 97 |
+
os.environ["DBUS_SESSION_BUS_ADDRESS"] = address
|
| 98 |
+
os.environ["NO_AT_BRIDGE"] = "0"
|
| 99 |
+
ready = Path("/tmp/measure-gtk.ready")
|
| 100 |
+
ready.unlink(missing_ok=True)
|
| 101 |
+
process = subprocess.Popen(
|
| 102 |
+
[sys.executable, "showcase_scenes/gtk_dialog.py", "/tmp/measure-gtk.marker", str(ready)],
|
| 103 |
+
env=dict(os.environ), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 104 |
+
for _ in range(60):
|
| 105 |
+
if ready.exists():
|
| 106 |
+
break
|
| 107 |
+
time.sleep(0.25)
|
| 108 |
+
time.sleep(2.5)
|
| 109 |
+
print("GTK window mapped:", ready.exists())
|
| 110 |
+
return process
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def main(argv=None) -> int:
|
| 114 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 115 |
+
parser.add_argument("--screen", default="builtin", choices=("builtin", "live"))
|
| 116 |
+
parser.add_argument("--detector", default="ocr",
|
| 117 |
+
choices=("ocr", "auto", "best", "atspi", "grid"),
|
| 118 |
+
help="how the menu is built; 'best' uses the accessibility "
|
| 119 |
+
"tree first, so only real widgets are offered")
|
| 120 |
+
parser.add_argument("--open-gtk", action="store_true",
|
| 121 |
+
help="open the GTK window (with a session bus) and index that")
|
| 122 |
+
parser.add_argument("--model", default=DEFAULT_VISION_MODEL)
|
| 123 |
+
parser.add_argument("--goal", default=GOAL)
|
| 124 |
+
parser.add_argument("--modes", default="json,choice,scan,rank")
|
| 125 |
+
parser.add_argument("--max-new-tokens", type=int, default=64)
|
| 126 |
+
args = parser.parse_args(argv)
|
| 127 |
+
|
| 128 |
+
OUT.mkdir(exist_ok=True)
|
| 129 |
+
gtk = None
|
| 130 |
+
if args.open_gtk:
|
| 131 |
+
gtk = _open_gtk_window()
|
| 132 |
+
try:
|
| 133 |
+
if args.screen == "live":
|
| 134 |
+
source = GrabScreen(monitor=1, max_width=1100)
|
| 135 |
+
image = source.capture().frame
|
| 136 |
+
else:
|
| 137 |
+
source = ImageScreen(busy_desktop())
|
| 138 |
+
image = source.capture().frame
|
| 139 |
+
image.save(OUT / "vlm_echo_screen.png")
|
| 140 |
+
observation = IndexedScreen(source, make_detector(args.detector)).capture()
|
| 141 |
+
finally:
|
| 142 |
+
if gtk is not None:
|
| 143 |
+
gtk.terminate()
|
| 144 |
+
menu = DecisionMenu(observation)
|
| 145 |
+
print(f"screen: {args.screen} ({image.width}x{image.height}) | detector: {args.detector}")
|
| 146 |
+
print(f"index: {len(observation.elements)} elements -> menu of {len(menu)}")
|
| 147 |
+
print(menu.render())
|
| 148 |
+
print()
|
| 149 |
+
|
| 150 |
+
results = []
|
| 151 |
+
for mode in args.modes.split(","):
|
| 152 |
+
brain = VLMBrain(args.model, decision_mode=mode, retries=3, votes=1,
|
| 153 |
+
max_new_tokens=args.max_new_tokens)
|
| 154 |
+
start = time.time()
|
| 155 |
+
try:
|
| 156 |
+
action = brain.decide(args.goal, observation)
|
| 157 |
+
outcome = action.describe()
|
| 158 |
+
except Exception as exc:
|
| 159 |
+
outcome = f"{type(exc).__name__}: {str(exc)[:110]}"
|
| 160 |
+
elapsed = time.time() - start
|
| 161 |
+
reply = (brain.last_reply or "").strip()
|
| 162 |
+
echoed = bool(reply) and (reply == (brain.last_prompt or "").strip()
|
| 163 |
+
or "You are controlling a computer" in reply)
|
| 164 |
+
results.append((mode, echoed, outcome, round(elapsed, 1)))
|
| 165 |
+
print(f"--- decision_mode={mode} ---")
|
| 166 |
+
print(f" echoed the prompt: {echoed}")
|
| 167 |
+
print(f" reply : {reply[:160]!r}")
|
| 168 |
+
print(f" outcome : {outcome}")
|
| 169 |
+
print(f" seconds : {elapsed:.1f}")
|
| 170 |
+
|
| 171 |
+
print()
|
| 172 |
+
print(f"{'mode':8} {'echoed':7} {'seconds':8} outcome")
|
| 173 |
+
for mode, echoed, outcome, elapsed in results:
|
| 174 |
+
print(f"{mode:8} {str(echoed):7} {elapsed:8} {outcome[:90]}")
|
| 175 |
+
return 0
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
raise SystemExit(main())
|