hkblockchaintechnology commited on
Commit
c3f2ea5
·
verified ·
1 Parent(s): 8e4f215

Publish showcase_core.py from marimo

Browse files
Files changed (1) hide show
  1. showcase_core.py +346 -0
showcase_core.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the capability gallery: one screenshot per thing the harness can do.
2
+
3
+ Run it on a real desktop (or under Xvfb) and it writes ``gallery/<id>.png`` for every
4
+ capability registered in this file, plus ``gallery/index.json`` and a gallery page.
5
+
6
+ The screenshots are of the harness's *actual* behaviour, not mock-ups:
7
+
8
+ * **terminal shots** are captured stdout/stderr from a real command, rendered to an image
9
+ * **desktop shots** are real screen captures taken while the harness acts on a real
10
+ X11/Windows desktop (element boxes, the resolved click point, and the app's own
11
+ reaction are drawn or visible)
12
+
13
+ Nothing here is faked: if a command fails, its real failure is what gets pictured.
14
+
15
+ python showcase.py # everything
16
+ python showcase.py --only clarify # one capability by id substring
17
+ python showcase.py --list # list ids without rendering
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import platform
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import textwrap
29
+ import time
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+ from typing import Callable, Optional
33
+
34
+ ROOT = Path(__file__).resolve().parent
35
+ GALLERY = ROOT / "gallery"
36
+ PY = sys.executable
37
+ MONO_TTFS = (
38
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", # Debian/Ubuntu
39
+ "C:/Windows/Fonts/consola.ttf", # Windows (Consolas)
40
+ "C:/Windows/Fonts/lucon.ttf", # Windows (Lucida)
41
+ "/System/Library/Fonts/Menlo.ttc", # macOS
42
+ "/Library/Fonts/Menlo.ttc",
43
+ )
44
+ MONO_BOLD_TTFS = (
45
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
46
+ "C:/Windows/Fonts/consolab.ttf",
47
+ "/System/Library/Fonts/Menlo.ttc",
48
+ )
49
+ SCREEN_W, SCREEN_H = 1280, 800
50
+ TERM_COLS = 108
51
+ TERM_BG = (13, 17, 23)
52
+ TERM_FG = (201, 209, 217)
53
+ TERM_GREEN = (63, 185, 80)
54
+ TERM_RED = (248, 81, 73)
55
+ TERM_DIM = (139, 148, 158)
56
+
57
+ CATALOG: list["Shot"] = []
58
+
59
+
60
+ @dataclass
61
+ class Shot:
62
+ """One capability, and the evidence that it works."""
63
+
64
+ id: str
65
+ category: str
66
+ title: str
67
+ shows: list[str] # the public callables this picture demonstrates
68
+ note: str = "" # one line the card can print under the image
69
+ kind: str = "terminal" # "terminal" | "desktop"
70
+ argv: list[str] | None = None # for terminal shots: the command to run
71
+ env: Optional[dict] = None # extra environment for the command
72
+ code: str | None = None # for terminal shots: a python snippet to run
73
+ scene: str | None = None # for desktop shots: a named scene below
74
+ make: Optional[Callable] = None # for desktop shots: builds the picture
75
+ prepare: Optional[Callable] = None # for terminal shots: set the scene up first
76
+
77
+
78
+ def shot(**kwargs):
79
+ def wrap(fn=None):
80
+ s = Shot(**kwargs)
81
+ CATALOG.append(s)
82
+ return fn
83
+ return wrap
84
+
85
+
86
+ # --------------------------------------------------------------------------- #
87
+ # rendering
88
+ # --------------------------------------------------------------------------- #
89
+
90
+ def _fonts(size=15, bold=False):
91
+ from PIL import ImageFont
92
+
93
+ for path in (MONO_BOLD_TTFS if bold else MONO_TTFS):
94
+ try:
95
+ return ImageFont.truetype(path, size)
96
+ except Exception:
97
+ continue
98
+ return ImageFont.load_default() # last resort: still produces a picture
99
+
100
+
101
+ def _wrap(text: str, cols: int) -> list[str]:
102
+ out: list[str] = []
103
+ for line in text.replace("\t", " ").splitlines():
104
+ if not line:
105
+ out.append("")
106
+ continue
107
+ out.extend(textwrap.wrap(line, cols, drop_whitespace=False,
108
+ replace_whitespace=False) or [""])
109
+ return out
110
+
111
+
112
+ def terminal_image(title: str, output: str, subtitle: str = "") -> "Image.Image":
113
+ """Render captured output as a terminal-window screenshot."""
114
+ from PIL import Image, ImageDraw
115
+
116
+ body_font, title_font = _fonts(15), _fonts(14, bold=True)
117
+ lines = _wrap(output.rstrip("\n") or "(no output)", TERM_COLS)
118
+ wrapped_title = _wrap(title, 78)
119
+ sub = _wrap(subtitle, 96) if subtitle else []
120
+ line_h, pad = 21, 18
121
+ head_h = 46 + 20 * len(wrapped_title) + 19 * len(sub)
122
+ height = head_h + line_h * len(lines) + 26
123
+
124
+ image = Image.new("RGB", (1240, height), TERM_BG)
125
+ draw = ImageDraw.Draw(image)
126
+ draw.rectangle([0, 0, 1240, 34], fill=(22, 27, 34))
127
+ for i, colour in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
128
+ draw.ellipse([18 + i * 22, 12, 30 + i * 22, 24], fill=colour)
129
+ y = 46
130
+ for line in wrapped_title:
131
+ draw.text((24, y), line, font=title_font, fill=(230, 237, 243))
132
+ y += 20
133
+ for line in sub:
134
+ draw.text((24, y), line, font=_fonts(13), fill=TERM_DIM)
135
+ y += 19
136
+ y = head_h
137
+ for line in lines:
138
+ colour = TERM_FG
139
+ stripped = line.strip()
140
+ if stripped.startswith("$ ") or stripped.startswith("python -m"):
141
+ colour = TERM_GREEN
142
+ elif any(word in stripped for word in ("result: failure", "ERROR", "FAILED",
143
+ "refused", "Traceback", "Error:")):
144
+ colour = TERM_RED
145
+ elif stripped.startswith("result: success") or stripped.startswith("score: correct"):
146
+ colour = TERM_GREEN
147
+ draw.text((24, y), line, font=body_font, fill=colour)
148
+ y += line_h
149
+ return image
150
+
151
+
152
+ def run_argv(argv: list[str], *, extra_env: dict | None = None) -> tuple[int, str]:
153
+ env = dict(os.environ, **(extra_env or {}))
154
+ proc = subprocess.run(argv, capture_output=True, text=True, timeout=900, env=env)
155
+ text = proc.stdout + (("\n" + proc.stderr) if proc.stderr.strip() else "")
156
+ return proc.returncode, text
157
+
158
+
159
+ def _header(display: str) -> str:
160
+ return (f"$ {display}" + chr(10))
161
+
162
+
163
+ # --------------------------------------------------------------------------- #
164
+ # the desktop: a real X server, real capture, real input
165
+ # --------------------------------------------------------------------------- #
166
+
167
+ DISPLAY_PROC = None
168
+
169
+
170
+ def _display_works() -> bool:
171
+ return subprocess.run(["xdpyinfo"], capture_output=True).returncode == 0
172
+
173
+
174
+ def ensure_display() -> str:
175
+ """Give the process a real display, starting Xvfb on Linux when there is none.
176
+
177
+ Verified rather than assumed: a stale ``DISPLAY`` pointing at a dead server is
178
+ worse than no display at all, because every scene then fails on ``tk.Tk()``.
179
+ """
180
+ global DISPLAY_PROC
181
+ if platform.system() in ("Windows", "Darwin"):
182
+ return os.environ.get("DISPLAY", "(native)")
183
+ if os.environ.get("DISPLAY") and _display_works():
184
+ return os.environ["DISPLAY"]
185
+ if shutil.which("Xvfb") is None:
186
+ return "(none)"
187
+ os.environ["DISPLAY"] = ":99"
188
+ Path.home().joinpath(".Xauthority").touch()
189
+ os.environ["XAUTHORITY"] = str(Path.home() / ".Xauthority")
190
+ DISPLAY_PROC = subprocess.Popen(
191
+ ["Xvfb", ":99", "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24", "-ac", "-nolisten", "tcp"],
192
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True,
193
+ )
194
+ for _ in range(80):
195
+ if _display_works():
196
+ break
197
+ time.sleep(0.25)
198
+ return os.environ["DISPLAY"]
199
+
200
+
201
+ SCENES: dict[str, str] = {}
202
+
203
+
204
+ def scene(name: str, source: str) -> str:
205
+ """Register a Tk app used as the desktop a capability is demonstrated on."""
206
+ SCENES[name] = source
207
+ return name
208
+
209
+
210
+ def open_scene(name: str, tmp: Path) -> subprocess.Popen:
211
+ """Launch a scene on the real display and wait until its window is mapped."""
212
+ marker = tmp / f"{name}.marker"
213
+ source = SCENES[name].replace("__MARKER__", str(marker))
214
+ script = tmp / f"{name}.py"
215
+ script.write_text(source)
216
+ proc = subprocess.Popen([PY, str(script)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
217
+ ready = Path(str(marker) + ".ready")
218
+ deadline = time.time() + 25
219
+ while time.time() < deadline and not ready.exists():
220
+ if proc.poll() is not None:
221
+ raise RuntimeError(f"scene {name!r} exited early: {proc.stderr.read().decode()[:400]}")
222
+ time.sleep(0.2)
223
+ if not ready.exists():
224
+ raise RuntimeError(f"scene {name!r} never mapped a window")
225
+ time.sleep(0.6) # let the toolkit finish painting
226
+ return proc
227
+
228
+
229
+ def close_scene(proc: subprocess.Popen) -> None:
230
+ proc.terminate()
231
+ try:
232
+ proc.wait(timeout=10)
233
+ except subprocess.TimeoutExpired:
234
+ proc.kill()
235
+ proc.wait(timeout=5)
236
+ time.sleep(0.5)
237
+
238
+
239
+ def grab() -> "Image.Image":
240
+ from tiny_computer_agent import GrabScreen
241
+ return GrabScreen(monitor=1).capture().frame
242
+
243
+
244
+ def annotate(image, elements, *, click=None, title=""):
245
+ """Draw what the harness *saw* (boxes + labels) and where it clicked."""
246
+ from PIL import ImageDraw
247
+ out = image.convert("RGB").copy()
248
+ draw = ImageDraw.Draw(out)
249
+ font = _fonts(13, bold=True)
250
+ for e in elements:
251
+ x, y, w, h = e.x, e.y, max(e.width, 1), max(e.height, 1)
252
+ colour = (255, 138, 0) if e.kind != "grid" else (90, 120, 200)
253
+ draw.rectangle([x, y, x + w, y + h], outline=colour, width=2)
254
+ tag = f"{e.label}"[:34]
255
+ tw = draw.textlength(tag, font=font)
256
+ draw.rectangle([x, max(0, y - 17), x + tw + 8, y], fill=colour)
257
+ draw.text((x + 4, max(0, y - 16)), tag, font=font, fill=(16, 16, 16))
258
+ if click is not None:
259
+ cx, cy = click
260
+ draw.line([cx - 22, cy, cx + 22, cy], fill=(255, 0, 60), width=2)
261
+ draw.line([cx, cy - 22, cx, cy + 22], fill=(255, 0, 60), width=2)
262
+ draw.ellipse([cx - 9, cy - 9, cx + 9, cy + 9], outline=(255, 0, 60), width=3)
263
+ if title:
264
+ draw.rectangle([0, 0, out.width, 30], fill=(20, 22, 28))
265
+ draw.text((12, 8), title, font=_fonts(15, bold=True), fill=(255, 255, 255))
266
+ return out
267
+
268
+
269
+ def save(image, sid: str) -> Path:
270
+ GALLERY.mkdir(exist_ok=True)
271
+ target = GALLERY / f"{sid}.png"
272
+ image.save(target, optimize=True)
273
+ return target
274
+
275
+
276
+ # --------------------------------------------------------------------------- #
277
+ # rendering a shot
278
+ # --------------------------------------------------------------------------- #
279
+
280
+ def render_terminal(s: "Shot", tmp: Optional[Path] = None) -> "Image.Image":
281
+ """Run the shot's command for real and picture what it printed."""
282
+ cleanup = s.prepare(tmp) if s.prepare is not None else None
283
+ try:
284
+ return _terminal_body(s)
285
+ finally:
286
+ if cleanup is not None:
287
+ cleanup()
288
+
289
+
290
+ def _terminal_body(s: "Shot") -> "Image.Image":
291
+ if s.argv is not None:
292
+ argv = [str(a).replace("{py}", PY) for a in s.argv]
293
+ _, text = run_argv(argv, extra_env=s.env)
294
+ shown = " ".join("python" if a == PY else str(a) for a in s.argv)
295
+ else:
296
+ argv = [PY, "-c", s.code]
297
+ _, text = run_argv(argv, extra_env=s.env)
298
+ shown = "python -c # " + (s.note or s.title)
299
+ return terminal_image(s.title, text, shown)
300
+
301
+
302
+ def render_desktop(s: "Shot", tmp: Path) -> "Image.Image":
303
+ """Run the shot's desktop scene for real and picture the harness acting on it."""
304
+ if s.make is None:
305
+ raise RuntimeError(f"desktop shot {s.id!r} has no make() callable")
306
+ if not os.environ.get("DISPLAY"):
307
+ raise RuntimeError(f"desktop shot {s.id!r} needs a display")
308
+ return s.make(tmp)
309
+
310
+
311
+ def overlay_lines(image, lines, *, title=""):
312
+ """A dark strip of text under a picture - for the episode trace next to the pixels."""
313
+ from PIL import ImageDraw
314
+ out = image.convert("RGB").copy()
315
+ draw = ImageDraw.Draw(out)
316
+ font = _fonts(15)
317
+ strip = 24 * len(lines) + 14
318
+ draw.rectangle([0, out.height - strip, out.width, out.height], fill=(13, 17, 23))
319
+ y = out.height - strip + 7
320
+ for line in lines:
321
+ draw.text((12, y), line, font=font, fill=TERM_FG)
322
+ y += 24
323
+ if title:
324
+ draw.rectangle([0, 0, out.width, 30], fill=(20, 22, 28))
325
+ draw.text((12, 8), title, font=_fonts(15, bold=True), fill=(255, 255, 255))
326
+ return out
327
+
328
+
329
+ def side_by_side(left, right, *, title="", gap_label=("before", "after")):
330
+ """Two frames of the same window, split down the middle."""
331
+ from PIL import Image, ImageDraw
332
+ width, height = left.width + right.width + 4, max(left.height, right.height) + 34
333
+ out = Image.new("RGB", (width, height), (13, 17, 23))
334
+ draw = ImageDraw.Draw(out)
335
+ out.paste(left, (0, 34))
336
+ out.paste(right, (left.width + 4, 34))
337
+ draw.rectangle([left.width, 34, left.width + 4, height], fill=(255, 255, 255))
338
+ draw.rectangle([0, 0, width, 30], fill=(20, 22, 28))
339
+ draw.text((12, 8), title, font=_fonts(15, bold=True), fill=(255, 255, 255))
340
+ draw.text((12, 16 + left.height), gap_label[0], font=_fonts(14, bold=True), fill=TERM_DIM)
341
+ draw.text((left.width + 16, 16 + left.height), gap_label[1], font=_fonts(14, bold=True), fill=TERM_DIM)
342
+ return out
343
+
344
+
345
+ def crop(image, box):
346
+ return image.convert("RGB").crop(box)