hkblockchaintechnology commited on
Commit
ef447f4
·
verified ·
1 Parent(s): b89c3c8

Publish measure_vlm.py from marimo

Browse files
Files changed (1) hide show
  1. 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())