openadapt-ml 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openadapt_ml/baselines/__init__.py +121 -0
- openadapt_ml/baselines/adapter.py +185 -0
- openadapt_ml/baselines/cli.py +314 -0
- openadapt_ml/baselines/config.py +448 -0
- openadapt_ml/baselines/parser.py +922 -0
- openadapt_ml/baselines/prompts.py +787 -0
- openadapt_ml/benchmarks/__init__.py +13 -115
- openadapt_ml/benchmarks/agent.py +265 -421
- openadapt_ml/benchmarks/azure.py +28 -19
- openadapt_ml/benchmarks/azure_ops_tracker.py +521 -0
- openadapt_ml/benchmarks/cli.py +1722 -4847
- openadapt_ml/benchmarks/trace_export.py +631 -0
- openadapt_ml/benchmarks/viewer.py +22 -5
- openadapt_ml/benchmarks/vm_monitor.py +530 -29
- openadapt_ml/benchmarks/waa_deploy/Dockerfile +47 -53
- openadapt_ml/benchmarks/waa_deploy/api_agent.py +21 -20
- openadapt_ml/cloud/azure_inference.py +3 -5
- openadapt_ml/cloud/lambda_labs.py +722 -307
- openadapt_ml/cloud/local.py +2038 -487
- openadapt_ml/cloud/ssh_tunnel.py +68 -26
- openadapt_ml/datasets/next_action.py +40 -30
- openadapt_ml/evals/grounding.py +8 -3
- openadapt_ml/evals/plot_eval_metrics.py +15 -13
- openadapt_ml/evals/trajectory_matching.py +41 -26
- openadapt_ml/experiments/demo_prompt/format_demo.py +16 -6
- openadapt_ml/experiments/demo_prompt/run_experiment.py +26 -16
- openadapt_ml/experiments/representation_shootout/__init__.py +70 -0
- openadapt_ml/experiments/representation_shootout/conditions.py +708 -0
- openadapt_ml/experiments/representation_shootout/config.py +390 -0
- openadapt_ml/experiments/representation_shootout/evaluator.py +659 -0
- openadapt_ml/experiments/representation_shootout/runner.py +687 -0
- openadapt_ml/experiments/waa_demo/runner.py +29 -14
- openadapt_ml/export/parquet.py +36 -24
- openadapt_ml/grounding/detector.py +18 -14
- openadapt_ml/ingest/__init__.py +8 -6
- openadapt_ml/ingest/capture.py +25 -22
- openadapt_ml/ingest/loader.py +7 -4
- openadapt_ml/ingest/synthetic.py +189 -100
- openadapt_ml/models/api_adapter.py +14 -4
- openadapt_ml/models/base_adapter.py +10 -2
- openadapt_ml/models/providers/__init__.py +288 -0
- openadapt_ml/models/providers/anthropic.py +266 -0
- openadapt_ml/models/providers/base.py +299 -0
- openadapt_ml/models/providers/google.py +376 -0
- openadapt_ml/models/providers/openai.py +342 -0
- openadapt_ml/models/qwen_vl.py +46 -19
- openadapt_ml/perception/__init__.py +35 -0
- openadapt_ml/perception/integration.py +399 -0
- openadapt_ml/retrieval/demo_retriever.py +50 -24
- openadapt_ml/retrieval/embeddings.py +9 -8
- openadapt_ml/retrieval/retriever.py +3 -1
- openadapt_ml/runtime/__init__.py +50 -0
- openadapt_ml/runtime/policy.py +18 -5
- openadapt_ml/runtime/safety_gate.py +471 -0
- openadapt_ml/schema/__init__.py +9 -0
- openadapt_ml/schema/converters.py +74 -27
- openadapt_ml/schema/episode.py +31 -18
- openadapt_ml/scripts/capture_screenshots.py +530 -0
- openadapt_ml/scripts/compare.py +85 -54
- openadapt_ml/scripts/demo_policy.py +4 -1
- openadapt_ml/scripts/eval_policy.py +15 -9
- openadapt_ml/scripts/make_gif.py +1 -1
- openadapt_ml/scripts/prepare_synthetic.py +3 -1
- openadapt_ml/scripts/train.py +21 -9
- openadapt_ml/segmentation/README.md +920 -0
- openadapt_ml/segmentation/__init__.py +97 -0
- openadapt_ml/segmentation/adapters/__init__.py +5 -0
- openadapt_ml/segmentation/adapters/capture_adapter.py +420 -0
- openadapt_ml/segmentation/annotator.py +610 -0
- openadapt_ml/segmentation/cache.py +290 -0
- openadapt_ml/segmentation/cli.py +674 -0
- openadapt_ml/segmentation/deduplicator.py +656 -0
- openadapt_ml/segmentation/frame_describer.py +788 -0
- openadapt_ml/segmentation/pipeline.py +340 -0
- openadapt_ml/segmentation/schemas.py +622 -0
- openadapt_ml/segmentation/segment_extractor.py +634 -0
- openadapt_ml/training/azure_ops_viewer.py +1097 -0
- openadapt_ml/training/benchmark_viewer.py +52 -41
- openadapt_ml/training/shared_ui.py +7 -7
- openadapt_ml/training/stub_provider.py +57 -35
- openadapt_ml/training/trainer.py +143 -86
- openadapt_ml/training/trl_trainer.py +70 -21
- openadapt_ml/training/viewer.py +323 -108
- openadapt_ml/training/viewer_components.py +180 -0
- {openadapt_ml-0.2.0.dist-info → openadapt_ml-0.2.1.dist-info}/METADATA +215 -14
- openadapt_ml-0.2.1.dist-info/RECORD +116 -0
- openadapt_ml/benchmarks/base.py +0 -366
- openadapt_ml/benchmarks/data_collection.py +0 -432
- openadapt_ml/benchmarks/live_tracker.py +0 -180
- openadapt_ml/benchmarks/runner.py +0 -418
- openadapt_ml/benchmarks/waa.py +0 -761
- openadapt_ml/benchmarks/waa_live.py +0 -619
- openadapt_ml-0.2.0.dist-info/RECORD +0 -86
- {openadapt_ml-0.2.0.dist-info → openadapt_ml-0.2.1.dist-info}/WHEEL +0 -0
- {openadapt_ml-0.2.0.dist-info → openadapt_ml-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Capture screenshots of dashboards and VMs for documentation.
|
|
3
|
+
|
|
4
|
+
This script captures screenshots from:
|
|
5
|
+
1. Azure ops dashboard (http://localhost:8765/azure_ops.html)
|
|
6
|
+
2. VNC viewer (http://localhost:8006) - Windows VM
|
|
7
|
+
3. Terminal output from VM monitor (uses PIL rendering)
|
|
8
|
+
4. Training dashboard (http://localhost:8080/dashboard.html)
|
|
9
|
+
|
|
10
|
+
Prerequisites:
|
|
11
|
+
- PIL (Pillow) - for terminal screenshots and image manipulation
|
|
12
|
+
- Optional: playwright - for web page screenshots (better quality)
|
|
13
|
+
- Optional: macOS screencapture - fallback for web pages
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
# Capture all available dashboards
|
|
17
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots
|
|
18
|
+
|
|
19
|
+
# Capture specific targets
|
|
20
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target azure-ops
|
|
21
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target vnc
|
|
22
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target terminal
|
|
23
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target training
|
|
24
|
+
|
|
25
|
+
# Capture with custom output directory
|
|
26
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --output /path/to/screenshots
|
|
27
|
+
|
|
28
|
+
# List available targets
|
|
29
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --list
|
|
30
|
+
|
|
31
|
+
Output:
|
|
32
|
+
docs/screenshots/{target}_{timestamp}.png
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import datetime
|
|
39
|
+
import re
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
# Project paths
|
|
45
|
+
SCRIPT_DIR = Path(__file__).parent
|
|
46
|
+
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
|
47
|
+
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "docs" / "screenshots"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_timestamp() -> str:
|
|
51
|
+
"""Get current timestamp string for filenames."""
|
|
52
|
+
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_url_available(url: str, timeout: int = 5) -> bool:
|
|
56
|
+
"""Check if a URL is accessible."""
|
|
57
|
+
import urllib.request
|
|
58
|
+
import urllib.error
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
urllib.request.urlopen(url, timeout=timeout)
|
|
62
|
+
return True
|
|
63
|
+
except (urllib.error.URLError, urllib.error.HTTPError):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def capture_web_page_playwright(url: str, output_path: Path) -> bool:
|
|
68
|
+
"""Capture web page screenshot using playwright.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
url: URL to capture
|
|
72
|
+
output_path: Path to save screenshot
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if successful, False otherwise
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
from playwright.sync_api import sync_playwright
|
|
79
|
+
|
|
80
|
+
with sync_playwright() as p:
|
|
81
|
+
browser = p.chromium.launch()
|
|
82
|
+
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
|
83
|
+
page.goto(url, wait_until="networkidle")
|
|
84
|
+
# Wait a bit for any dynamic content
|
|
85
|
+
page.wait_for_timeout(2000)
|
|
86
|
+
page.screenshot(path=str(output_path), full_page=False)
|
|
87
|
+
browser.close()
|
|
88
|
+
return True
|
|
89
|
+
except ImportError:
|
|
90
|
+
return False
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f" Playwright error: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def capture_web_page_selenium(url: str, output_path: Path) -> bool:
|
|
97
|
+
"""Capture web page screenshot using selenium.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
url: URL to capture
|
|
101
|
+
output_path: Path to save screenshot
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if successful, False otherwise
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
from selenium import webdriver
|
|
108
|
+
from selenium.webdriver.chrome.options import Options
|
|
109
|
+
|
|
110
|
+
options = Options()
|
|
111
|
+
options.add_argument("--headless")
|
|
112
|
+
options.add_argument("--window-size=1280,900")
|
|
113
|
+
options.add_argument("--disable-gpu")
|
|
114
|
+
options.add_argument("--no-sandbox")
|
|
115
|
+
|
|
116
|
+
driver = webdriver.Chrome(options=options)
|
|
117
|
+
driver.get(url)
|
|
118
|
+
import time
|
|
119
|
+
|
|
120
|
+
time.sleep(2) # Wait for dynamic content
|
|
121
|
+
driver.save_screenshot(str(output_path))
|
|
122
|
+
driver.quit()
|
|
123
|
+
return True
|
|
124
|
+
except ImportError:
|
|
125
|
+
return False
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f" Selenium error: {e}")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def capture_web_page_macos(url: str, output_path: Path) -> bool:
|
|
132
|
+
"""Capture web page by opening in browser and using macOS screencapture.
|
|
133
|
+
|
|
134
|
+
This is a fallback method that requires manual interaction.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
url: URL to capture
|
|
138
|
+
output_path: Path to save screenshot
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if user completed capture, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
if sys.platform != "darwin":
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
print(f" Opening {url} in browser...")
|
|
147
|
+
subprocess.run(["open", url], check=True)
|
|
148
|
+
|
|
149
|
+
print(" Press Enter when ready to capture (or 'q' to skip)...")
|
|
150
|
+
response = input().strip().lower()
|
|
151
|
+
if response == "q":
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
# Use screencapture with interactive mode (-i) for user to select window
|
|
155
|
+
print(" Click on the window to capture...")
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
["screencapture", "-i", "-W", str(output_path)], capture_output=True
|
|
158
|
+
)
|
|
159
|
+
return result.returncode == 0 and output_path.exists()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def capture_web_page(url: str, output_path: Path, interactive: bool = False) -> bool:
|
|
163
|
+
"""Capture web page screenshot using best available method.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
url: URL to capture
|
|
167
|
+
output_path: Path to save screenshot
|
|
168
|
+
interactive: If True, allow interactive capture methods
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if successful, False otherwise
|
|
172
|
+
"""
|
|
173
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
|
|
175
|
+
# Try playwright first (best quality)
|
|
176
|
+
if capture_web_page_playwright(url, output_path):
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# Try selenium as fallback
|
|
180
|
+
if capture_web_page_selenium(url, output_path):
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# On macOS, offer interactive capture
|
|
184
|
+
if interactive and capture_web_page_macos(url, output_path):
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def capture_terminal_output(command: list[str], output_path: Path) -> bool:
|
|
191
|
+
"""Capture terminal command output as image using PIL.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
command: Command to run
|
|
195
|
+
output_path: Path to save screenshot
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if successful, False otherwise
|
|
199
|
+
"""
|
|
200
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
result = subprocess.run(
|
|
204
|
+
command,
|
|
205
|
+
capture_output=True,
|
|
206
|
+
text=True,
|
|
207
|
+
cwd=PROJECT_ROOT,
|
|
208
|
+
timeout=60,
|
|
209
|
+
)
|
|
210
|
+
output = result.stdout or result.stderr
|
|
211
|
+
except subprocess.TimeoutExpired:
|
|
212
|
+
output = "ERROR: Command timed out"
|
|
213
|
+
except Exception as e:
|
|
214
|
+
output = f"ERROR: {e}"
|
|
215
|
+
|
|
216
|
+
# Strip ANSI codes
|
|
217
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
218
|
+
output = ansi_escape.sub("", output)
|
|
219
|
+
|
|
220
|
+
# Terminal color scheme
|
|
221
|
+
bg_color = (30, 30, 30)
|
|
222
|
+
text_color = (220, 220, 220)
|
|
223
|
+
|
|
224
|
+
# Load font
|
|
225
|
+
font_size = 14
|
|
226
|
+
try:
|
|
227
|
+
font = ImageFont.truetype("/System/Library/Fonts/Monaco.dfont", font_size)
|
|
228
|
+
except Exception:
|
|
229
|
+
try:
|
|
230
|
+
font = ImageFont.truetype("Courier New", font_size)
|
|
231
|
+
except Exception:
|
|
232
|
+
font = ImageFont.load_default()
|
|
233
|
+
|
|
234
|
+
# Calculate dimensions
|
|
235
|
+
lines = output.split("\n")
|
|
236
|
+
line_height = int(font_size * 1.4)
|
|
237
|
+
char_width = font_size * 0.6
|
|
238
|
+
padding = 20
|
|
239
|
+
|
|
240
|
+
max_line_len = max((len(line) for line in lines), default=80)
|
|
241
|
+
width = int(max_line_len * char_width) + 2 * padding
|
|
242
|
+
height = len(lines) * line_height + 2 * padding
|
|
243
|
+
|
|
244
|
+
# Create image
|
|
245
|
+
img = Image.new("RGB", (width, height), bg_color)
|
|
246
|
+
draw = ImageDraw.Draw(img)
|
|
247
|
+
|
|
248
|
+
# Draw text
|
|
249
|
+
y = padding
|
|
250
|
+
for line in lines:
|
|
251
|
+
draw.text((padding, y), line, fill=text_color, font=font)
|
|
252
|
+
y += line_height
|
|
253
|
+
|
|
254
|
+
# Save
|
|
255
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
img.save(output_path)
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def capture_vnc_screenshot(output_path: Path) -> bool:
|
|
261
|
+
"""Capture VNC viewer screenshot.
|
|
262
|
+
|
|
263
|
+
The VNC viewer at localhost:8006 is a noVNC HTML5 client.
|
|
264
|
+
We capture it as a web page.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
output_path: Path to save screenshot
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if successful, False otherwise
|
|
271
|
+
"""
|
|
272
|
+
url = "http://localhost:8006"
|
|
273
|
+
if not check_url_available(url):
|
|
274
|
+
print(f" VNC not available at {url}")
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
return capture_web_page(url, output_path, interactive=True)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def capture_azure_ops_dashboard(output_path: Path) -> bool:
|
|
281
|
+
"""Capture Azure ops dashboard screenshot.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
output_path: Path to save screenshot
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if successful, False otherwise
|
|
288
|
+
"""
|
|
289
|
+
url = "http://localhost:8765/azure_ops.html"
|
|
290
|
+
if not check_url_available(url):
|
|
291
|
+
print(f" Azure ops dashboard not available at {url}")
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
return capture_web_page(url, output_path, interactive=True)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def capture_training_dashboard(output_path: Path) -> bool:
|
|
298
|
+
"""Capture training dashboard screenshot.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
output_path: Path to save screenshot
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
True if successful, False otherwise
|
|
305
|
+
"""
|
|
306
|
+
url = "http://localhost:8080/dashboard.html"
|
|
307
|
+
if not check_url_available(url):
|
|
308
|
+
print(f" Training dashboard not available at {url}")
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
return capture_web_page(url, output_path, interactive=True)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def capture_vm_monitor(output_path: Path, mock: bool = True) -> bool:
|
|
315
|
+
"""Capture VM monitor terminal output.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
output_path: Path to save screenshot
|
|
319
|
+
mock: If True, use --mock flag to avoid Azure costs
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
True if successful, False otherwise
|
|
323
|
+
"""
|
|
324
|
+
cmd = ["uv", "run", "python", "-m", "openadapt_ml.benchmarks.cli", "vm", "monitor"]
|
|
325
|
+
if mock:
|
|
326
|
+
cmd.append("--mock")
|
|
327
|
+
|
|
328
|
+
return capture_terminal_output(cmd, output_path)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def capture_vm_screenshot_from_vm(output_path: Path) -> bool:
|
|
332
|
+
"""Capture VM screen directly via QEMU monitor.
|
|
333
|
+
|
|
334
|
+
This uses the existing vm screenshot command in the CLI.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
output_path: Path to save screenshot
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True if successful, False otherwise
|
|
341
|
+
"""
|
|
342
|
+
result = subprocess.run(
|
|
343
|
+
[
|
|
344
|
+
"uv",
|
|
345
|
+
"run",
|
|
346
|
+
"python",
|
|
347
|
+
"-m",
|
|
348
|
+
"openadapt_ml.benchmarks.cli",
|
|
349
|
+
"vm",
|
|
350
|
+
"screenshot",
|
|
351
|
+
],
|
|
352
|
+
capture_output=True,
|
|
353
|
+
text=True,
|
|
354
|
+
cwd=PROJECT_ROOT,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if result.returncode != 0:
|
|
358
|
+
print(
|
|
359
|
+
f" VM screenshot failed: {result.stderr[:200] if result.stderr else 'Unknown error'}"
|
|
360
|
+
)
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
# The CLI saves to training_output/current/vm_screenshot.png
|
|
364
|
+
src_path = PROJECT_ROOT / "training_output" / "current" / "vm_screenshot.png"
|
|
365
|
+
if src_path.exists():
|
|
366
|
+
import shutil
|
|
367
|
+
|
|
368
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
369
|
+
shutil.copy(src_path, output_path)
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
TARGETS = {
|
|
376
|
+
"azure-ops": {
|
|
377
|
+
"description": "Azure ops dashboard (localhost:8765)",
|
|
378
|
+
"capture_fn": capture_azure_ops_dashboard,
|
|
379
|
+
"filename": "azure_ops_dashboard",
|
|
380
|
+
},
|
|
381
|
+
"vnc": {
|
|
382
|
+
"description": "VNC viewer (localhost:8006) - Windows VM",
|
|
383
|
+
"capture_fn": capture_vnc_screenshot,
|
|
384
|
+
"filename": "vnc_viewer",
|
|
385
|
+
},
|
|
386
|
+
"terminal": {
|
|
387
|
+
"description": "VM monitor terminal output",
|
|
388
|
+
"capture_fn": lambda p: capture_vm_monitor(p, mock=True),
|
|
389
|
+
"filename": "vm_monitor_terminal",
|
|
390
|
+
},
|
|
391
|
+
"terminal-live": {
|
|
392
|
+
"description": "VM monitor terminal output (live, no mock)",
|
|
393
|
+
"capture_fn": lambda p: capture_vm_monitor(p, mock=False),
|
|
394
|
+
"filename": "vm_monitor_terminal_live",
|
|
395
|
+
},
|
|
396
|
+
"training": {
|
|
397
|
+
"description": "Training dashboard (localhost:8080)",
|
|
398
|
+
"capture_fn": capture_training_dashboard,
|
|
399
|
+
"filename": "training_dashboard",
|
|
400
|
+
},
|
|
401
|
+
"vm-screen": {
|
|
402
|
+
"description": "Windows VM screen (via QEMU)",
|
|
403
|
+
"capture_fn": capture_vm_screenshot_from_vm,
|
|
404
|
+
"filename": "vm_screen",
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def main():
|
|
410
|
+
parser = argparse.ArgumentParser(
|
|
411
|
+
description="Capture screenshots of dashboards and VMs for documentation",
|
|
412
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
413
|
+
epilog="""
|
|
414
|
+
Examples:
|
|
415
|
+
# Capture all available targets
|
|
416
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots
|
|
417
|
+
|
|
418
|
+
# Capture specific target
|
|
419
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target azure-ops
|
|
420
|
+
|
|
421
|
+
# Capture multiple targets
|
|
422
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --target azure-ops --target vnc
|
|
423
|
+
|
|
424
|
+
# List available targets
|
|
425
|
+
uv run python -m openadapt_ml.scripts.capture_screenshots --list
|
|
426
|
+
""",
|
|
427
|
+
)
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
"--target",
|
|
430
|
+
"-t",
|
|
431
|
+
action="append",
|
|
432
|
+
choices=list(TARGETS.keys()),
|
|
433
|
+
help="Target to capture (can specify multiple)",
|
|
434
|
+
)
|
|
435
|
+
parser.add_argument(
|
|
436
|
+
"--output",
|
|
437
|
+
"-o",
|
|
438
|
+
type=Path,
|
|
439
|
+
default=DEFAULT_OUTPUT_DIR,
|
|
440
|
+
help="Output directory for screenshots",
|
|
441
|
+
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--list",
|
|
444
|
+
"-l",
|
|
445
|
+
action="store_true",
|
|
446
|
+
help="List available targets",
|
|
447
|
+
)
|
|
448
|
+
parser.add_argument(
|
|
449
|
+
"--no-timestamp",
|
|
450
|
+
action="store_true",
|
|
451
|
+
help="Don't add timestamp to filenames",
|
|
452
|
+
)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"--interactive",
|
|
455
|
+
"-i",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help="Allow interactive capture methods (e.g., macOS screencapture)",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
args = parser.parse_args()
|
|
461
|
+
|
|
462
|
+
if args.list:
|
|
463
|
+
print("\nAvailable screenshot targets:\n")
|
|
464
|
+
for name, info in TARGETS.items():
|
|
465
|
+
print(f" {name:15} - {info['description']}")
|
|
466
|
+
print()
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
# Determine targets to capture
|
|
470
|
+
targets = args.target or list(TARGETS.keys())
|
|
471
|
+
|
|
472
|
+
# Create output directory
|
|
473
|
+
output_dir = args.output
|
|
474
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
475
|
+
|
|
476
|
+
print("=" * 60)
|
|
477
|
+
print(" Screenshot Capture Tool ".center(60))
|
|
478
|
+
print("=" * 60)
|
|
479
|
+
print(f"\nOutput directory: {output_dir}")
|
|
480
|
+
print(f"Targets: {', '.join(targets)}\n")
|
|
481
|
+
|
|
482
|
+
timestamp = get_timestamp() if not args.no_timestamp else ""
|
|
483
|
+
results = {}
|
|
484
|
+
|
|
485
|
+
for target in targets:
|
|
486
|
+
info = TARGETS[target]
|
|
487
|
+
print(f"\n[{target}] {info['description']}")
|
|
488
|
+
|
|
489
|
+
filename = info["filename"]
|
|
490
|
+
if timestamp:
|
|
491
|
+
filename = f"{filename}_{timestamp}"
|
|
492
|
+
output_path = output_dir / f"{filename}.png"
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
success = info["capture_fn"](output_path)
|
|
496
|
+
if success:
|
|
497
|
+
size_kb = output_path.stat().st_size / 1024
|
|
498
|
+
print(f" OK: {output_path.name} ({size_kb:.1f} KB)")
|
|
499
|
+
results[target] = str(output_path)
|
|
500
|
+
else:
|
|
501
|
+
print(" SKIP: Not available or capture failed")
|
|
502
|
+
results[target] = None
|
|
503
|
+
except Exception as e:
|
|
504
|
+
print(f" ERROR: {e}")
|
|
505
|
+
results[target] = None
|
|
506
|
+
|
|
507
|
+
# Summary
|
|
508
|
+
print("\n" + "=" * 60)
|
|
509
|
+
print(" Summary ".center(60))
|
|
510
|
+
print("=" * 60)
|
|
511
|
+
|
|
512
|
+
successful = [t for t, p in results.items() if p]
|
|
513
|
+
failed = [t for t, p in results.items() if not p]
|
|
514
|
+
|
|
515
|
+
if successful:
|
|
516
|
+
print(f"\nCaptured ({len(successful)}):")
|
|
517
|
+
for target in successful:
|
|
518
|
+
print(f" - {results[target]}")
|
|
519
|
+
|
|
520
|
+
if failed:
|
|
521
|
+
print(f"\nSkipped/Failed ({len(failed)}):")
|
|
522
|
+
for target in failed:
|
|
523
|
+
print(f" - {target}")
|
|
524
|
+
|
|
525
|
+
print()
|
|
526
|
+
return 0 if successful else 1
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
sys.exit(main())
|