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
openadapt_ml/training/viewer.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
"""Unified viewer HTML generation.
|
|
2
2
|
|
|
3
|
+
.. deprecated::
|
|
4
|
+
This module is deprecated. Use ``openadapt_viewer`` instead::
|
|
5
|
+
|
|
6
|
+
from openadapt_viewer import generate_unified_viewer
|
|
7
|
+
|
|
8
|
+
The openadapt-viewer package is the canonical location for viewer code.
|
|
9
|
+
|
|
3
10
|
This module generates the Viewer HTML with step-by-step playback,
|
|
4
11
|
transcript/audio sync, and model prediction comparison.
|
|
5
12
|
"""
|
|
@@ -7,6 +14,7 @@ transcript/audio sync, and model prediction comparison.
|
|
|
7
14
|
from __future__ import annotations
|
|
8
15
|
|
|
9
16
|
import json
|
|
17
|
+
import warnings
|
|
10
18
|
from pathlib import Path
|
|
11
19
|
|
|
12
20
|
from openadapt_ml.training.shared_ui import (
|
|
@@ -14,6 +22,13 @@ from openadapt_ml.training.shared_ui import (
|
|
|
14
22
|
generate_shared_header_html as _generate_shared_header_html,
|
|
15
23
|
)
|
|
16
24
|
|
|
25
|
+
warnings.warn(
|
|
26
|
+
"openadapt_ml.training.viewer is deprecated. "
|
|
27
|
+
"Use openadapt_viewer instead: from openadapt_viewer import generate_unified_viewer",
|
|
28
|
+
DeprecationWarning,
|
|
29
|
+
stacklevel=2,
|
|
30
|
+
)
|
|
31
|
+
|
|
17
32
|
|
|
18
33
|
def _copy_transcript_and_audio(capture_path: Path | None, output_dir: Path) -> None:
|
|
19
34
|
"""Copy transcript.json and convert audio to mp3 for viewer playback.
|
|
@@ -33,7 +48,7 @@ def _copy_transcript_and_audio(capture_path: Path | None, output_dir: Path) -> N
|
|
|
33
48
|
transcript_dst = output_dir / "transcript.json"
|
|
34
49
|
if transcript_src.exists() and not transcript_dst.exists():
|
|
35
50
|
shutil.copy2(transcript_src, transcript_dst)
|
|
36
|
-
print(
|
|
51
|
+
print(" Copied transcript.json from capture")
|
|
37
52
|
|
|
38
53
|
# Convert audio to mp3 if it exists (ffmpeg required)
|
|
39
54
|
audio_dst = output_dir / "audio.mp3"
|
|
@@ -44,14 +59,24 @@ def _copy_transcript_and_audio(capture_path: Path | None, output_dir: Path) -> N
|
|
|
44
59
|
if audio_src.exists():
|
|
45
60
|
try:
|
|
46
61
|
result = subprocess.run(
|
|
47
|
-
[
|
|
62
|
+
[
|
|
63
|
+
"ffmpeg",
|
|
64
|
+
"-i",
|
|
65
|
+
str(audio_src),
|
|
66
|
+
"-y",
|
|
67
|
+
"-q:a",
|
|
68
|
+
"2",
|
|
69
|
+
str(audio_dst),
|
|
70
|
+
],
|
|
48
71
|
capture_output=True,
|
|
49
72
|
timeout=60,
|
|
50
73
|
)
|
|
51
74
|
if result.returncode == 0:
|
|
52
75
|
print(f" Converted {audio_src.name} to audio.mp3")
|
|
53
76
|
else:
|
|
54
|
-
print(
|
|
77
|
+
print(
|
|
78
|
+
f" Warning: ffmpeg conversion failed for {audio_src.name}"
|
|
79
|
+
)
|
|
55
80
|
except FileNotFoundError:
|
|
56
81
|
print(" Warning: ffmpeg not found, cannot convert audio")
|
|
57
82
|
except subprocess.TimeoutExpired:
|
|
@@ -120,12 +145,12 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
120
145
|
data = json.load(f)
|
|
121
146
|
|
|
122
147
|
# Determine checkpoint name from filename
|
|
123
|
-
name_match = re.search(r
|
|
148
|
+
name_match = re.search(r"predictions_(.+)\.json", json_file.name)
|
|
124
149
|
if name_match:
|
|
125
150
|
raw_name = name_match.group(1)
|
|
126
|
-
if raw_name.startswith(
|
|
151
|
+
if raw_name.startswith("epoch"):
|
|
127
152
|
checkpoint_name = f"Epoch {raw_name[5:]}"
|
|
128
|
-
elif raw_name ==
|
|
153
|
+
elif raw_name == "preview":
|
|
129
154
|
checkpoint_name = "Preview"
|
|
130
155
|
else:
|
|
131
156
|
checkpoint_name = raw_name.title()
|
|
@@ -133,19 +158,19 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
133
158
|
checkpoint_name = json_file.stem
|
|
134
159
|
|
|
135
160
|
# Extract base data from first file
|
|
136
|
-
if base_data is None and
|
|
137
|
-
base_data = data[
|
|
161
|
+
if base_data is None and "base_data" in data:
|
|
162
|
+
base_data = data["base_data"]
|
|
138
163
|
|
|
139
164
|
# Store predictions
|
|
140
|
-
if
|
|
141
|
-
predictions_by_checkpoint[checkpoint_name] = data[
|
|
165
|
+
if "predictions" in data:
|
|
166
|
+
predictions_by_checkpoint[checkpoint_name] = data["predictions"]
|
|
142
167
|
print(f" Loaded predictions from {json_file.name}")
|
|
143
168
|
except Exception as e:
|
|
144
169
|
print(f" Warning: Could not load {json_file.name}: {e}")
|
|
145
170
|
|
|
146
171
|
# Fallback: look for comparison_epoch*.html files and extract their data
|
|
147
172
|
for comp_file in sorted(output_dir.glob("comparison_epoch*.html")):
|
|
148
|
-
match = re.search(r
|
|
173
|
+
match = re.search(r"epoch(\d+)", comp_file.name)
|
|
149
174
|
if not match:
|
|
150
175
|
continue
|
|
151
176
|
|
|
@@ -157,9 +182,9 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
157
182
|
html_content = comp_file.read_text()
|
|
158
183
|
# Look for comparisonData = [...]; (supports both const and window. prefix)
|
|
159
184
|
data_match = re.search(
|
|
160
|
-
r
|
|
185
|
+
r"(?:const\s+|window\.)comparisonData\s*=\s*(\[.*?\]);",
|
|
161
186
|
html_content,
|
|
162
|
-
re.DOTALL
|
|
187
|
+
re.DOTALL,
|
|
163
188
|
)
|
|
164
189
|
if data_match:
|
|
165
190
|
comparison_data = json.loads(data_match.group(1))
|
|
@@ -168,20 +193,24 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
168
193
|
if base_data is None:
|
|
169
194
|
base_data = []
|
|
170
195
|
for item in comparison_data:
|
|
171
|
-
base_data.append(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
base_data.append(
|
|
197
|
+
{
|
|
198
|
+
"index": item.get("index", 0),
|
|
199
|
+
"time": item.get("time", 0),
|
|
200
|
+
"image_path": item.get("image_path", ""),
|
|
201
|
+
"human_action": item.get("human_action", {}),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
177
204
|
|
|
178
205
|
# Extract predictions
|
|
179
206
|
predictions = []
|
|
180
207
|
for item in comparison_data:
|
|
181
|
-
predictions.append(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
predictions.append(
|
|
209
|
+
{
|
|
210
|
+
"predicted_action": item.get("predicted_action"),
|
|
211
|
+
"match": item.get("match"),
|
|
212
|
+
}
|
|
213
|
+
)
|
|
185
214
|
predictions_by_checkpoint[checkpoint_name] = predictions
|
|
186
215
|
print(f" Loaded predictions from {comp_file.name}")
|
|
187
216
|
except Exception as e:
|
|
@@ -193,9 +222,9 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
193
222
|
try:
|
|
194
223
|
html_content = preview_file.read_text()
|
|
195
224
|
data_match = re.search(
|
|
196
|
-
r
|
|
225
|
+
r"(?:const\s+|window\.)comparisonData\s*=\s*(\[.*?\]);",
|
|
197
226
|
html_content,
|
|
198
|
-
re.DOTALL
|
|
227
|
+
re.DOTALL,
|
|
199
228
|
)
|
|
200
229
|
if data_match:
|
|
201
230
|
comparison_data = json.loads(data_match.group(1))
|
|
@@ -204,26 +233,32 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
204
233
|
if base_data is None:
|
|
205
234
|
base_data = []
|
|
206
235
|
for item in comparison_data:
|
|
207
|
-
base_data.append(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
236
|
+
base_data.append(
|
|
237
|
+
{
|
|
238
|
+
"index": item.get("index", 0),
|
|
239
|
+
"time": item.get("time", 0),
|
|
240
|
+
"image_path": item.get("image_path", ""),
|
|
241
|
+
"human_action": item.get("human_action", {}),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
213
244
|
|
|
214
245
|
predictions = []
|
|
215
246
|
for item in comparison_data:
|
|
216
|
-
predictions.append(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
predictions.append(
|
|
248
|
+
{
|
|
249
|
+
"predicted_action": item.get("predicted_action"),
|
|
250
|
+
"match": item.get("match"),
|
|
251
|
+
}
|
|
252
|
+
)
|
|
220
253
|
# Only add if it has actual predictions
|
|
221
254
|
has_predictions = any(p.get("predicted_action") for p in predictions)
|
|
222
255
|
if has_predictions and "Preview" not in predictions_by_checkpoint:
|
|
223
256
|
predictions_by_checkpoint["Preview"] = predictions
|
|
224
|
-
print(
|
|
257
|
+
print(" Loaded predictions from comparison_preview.html")
|
|
225
258
|
except Exception as e:
|
|
226
|
-
print(
|
|
259
|
+
print(
|
|
260
|
+
f" Warning: Could not extract data from comparison_preview.html: {e}"
|
|
261
|
+
)
|
|
227
262
|
|
|
228
263
|
# If we still don't have base data, we can't generate the viewer
|
|
229
264
|
if base_data is None:
|
|
@@ -237,6 +272,7 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
|
|
|
237
272
|
capture_modified_time = None
|
|
238
273
|
if capture_path and capture_path.exists():
|
|
239
274
|
import datetime
|
|
275
|
+
|
|
240
276
|
mtime = capture_path.stat().st_mtime
|
|
241
277
|
capture_modified_time = datetime.datetime.fromtimestamp(mtime).isoformat()
|
|
242
278
|
|
|
@@ -275,22 +311,28 @@ def _generate_unified_viewer_from_extracted_data(
|
|
|
275
311
|
shared_header_css = _get_shared_header_css()
|
|
276
312
|
shared_header_html = _generate_shared_header_html("viewer")
|
|
277
313
|
|
|
314
|
+
# Note: keyboard shortcuts CSS and JS are handled inline in the viewer HTML
|
|
315
|
+
|
|
278
316
|
# Build base HTML from extracted data (standalone, no openadapt-capture dependency)
|
|
279
317
|
base_data_json = json.dumps(base_data)
|
|
280
318
|
predictions_json = json.dumps(predictions_by_checkpoint)
|
|
281
319
|
evaluations_json = json.dumps(evaluations or [])
|
|
282
|
-
captures_json = json.dumps(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
320
|
+
captures_json = json.dumps(
|
|
321
|
+
[
|
|
322
|
+
{
|
|
323
|
+
"id": capture_id,
|
|
324
|
+
"name": goal,
|
|
325
|
+
"steps": len(base_data),
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
)
|
|
287
329
|
current_capture_json = json.dumps(capture_id)
|
|
288
330
|
capture_modified_time_json = json.dumps(capture_modified_time)
|
|
289
331
|
|
|
290
332
|
# Find first image to get dimensions (for display)
|
|
291
|
-
|
|
333
|
+
base_data[0].get("image_path", "") if base_data else ""
|
|
292
334
|
|
|
293
|
-
html = f
|
|
335
|
+
html = f"""<!DOCTYPE html>
|
|
294
336
|
<html lang="en">
|
|
295
337
|
<head>
|
|
296
338
|
<meta charset="UTF-8">
|
|
@@ -366,6 +408,50 @@ def _generate_unified_viewer_from_extracted_data(
|
|
|
366
408
|
flex-wrap: wrap;
|
|
367
409
|
align-items: center;
|
|
368
410
|
}}
|
|
411
|
+
.search-container {{
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
gap: 8px;
|
|
415
|
+
flex: 1;
|
|
416
|
+
max-width: 400px;
|
|
417
|
+
}}
|
|
418
|
+
.search-input {{
|
|
419
|
+
flex: 1;
|
|
420
|
+
padding: 10px 14px;
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
font-size: 0.85rem;
|
|
423
|
+
background: var(--bg-tertiary);
|
|
424
|
+
color: var(--text-primary);
|
|
425
|
+
border: 1px solid var(--border-color);
|
|
426
|
+
transition: all 0.2s;
|
|
427
|
+
}}
|
|
428
|
+
.search-input:focus {{
|
|
429
|
+
outline: none;
|
|
430
|
+
border-color: var(--accent);
|
|
431
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
432
|
+
}}
|
|
433
|
+
.search-input::placeholder {{
|
|
434
|
+
color: var(--text-muted);
|
|
435
|
+
}}
|
|
436
|
+
.search-clear-btn {{
|
|
437
|
+
padding: 8px 12px;
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
font-size: 0.75rem;
|
|
440
|
+
background: var(--bg-tertiary);
|
|
441
|
+
color: var(--text-secondary);
|
|
442
|
+
border: 1px solid var(--border-color);
|
|
443
|
+
cursor: pointer;
|
|
444
|
+
transition: all 0.2s;
|
|
445
|
+
}}
|
|
446
|
+
.search-clear-btn:hover {{
|
|
447
|
+
border-color: var(--accent);
|
|
448
|
+
color: var(--text-primary);
|
|
449
|
+
}}
|
|
450
|
+
.search-count {{
|
|
451
|
+
font-size: 0.75rem;
|
|
452
|
+
color: var(--text-secondary);
|
|
453
|
+
white-space: nowrap;
|
|
454
|
+
}}
|
|
369
455
|
.control-group {{
|
|
370
456
|
display: flex;
|
|
371
457
|
align-items: center;
|
|
@@ -1164,6 +1250,17 @@ def _generate_unified_viewer_from_extracted_data(
|
|
|
1164
1250
|
<span class="control-label">Checkpoint:</span>
|
|
1165
1251
|
<select class="control-select" id="checkpoint-select"></select>
|
|
1166
1252
|
</div>
|
|
1253
|
+
<div class="search-container">
|
|
1254
|
+
<input
|
|
1255
|
+
type="text"
|
|
1256
|
+
id="search-input"
|
|
1257
|
+
class="search-input"
|
|
1258
|
+
placeholder="Search steps... (Ctrl+F / Cmd+F)"
|
|
1259
|
+
title="Search by step index, action type, or description"
|
|
1260
|
+
/>
|
|
1261
|
+
<button class="search-clear-btn" id="search-clear-btn" title="Clear search">Clear</button>
|
|
1262
|
+
<span class="search-count" id="search-count"></span>
|
|
1263
|
+
</div>
|
|
1167
1264
|
</div>
|
|
1168
1265
|
|
|
1169
1266
|
<div class="cost-panel" id="cost-panel">
|
|
@@ -2563,9 +2660,9 @@ def _generate_unified_viewer_from_extracted_data(
|
|
|
2563
2660
|
setupGalleryPanel();
|
|
2564
2661
|
</script>
|
|
2565
2662
|
</body>
|
|
2566
|
-
</html>
|
|
2663
|
+
</html>"""
|
|
2567
2664
|
|
|
2568
|
-
output_path.write_text(html, encoding=
|
|
2665
|
+
output_path.write_text(html, encoding="utf-8")
|
|
2569
2666
|
print(f"Generated unified viewer: {output_path}")
|
|
2570
2667
|
|
|
2571
2668
|
|
|
@@ -2593,9 +2690,7 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2593
2690
|
|
|
2594
2691
|
# Extract base data from the existing comparisonData (supports both const and window. prefix)
|
|
2595
2692
|
data_match = re.search(
|
|
2596
|
-
r
|
|
2597
|
-
html,
|
|
2598
|
-
re.DOTALL
|
|
2693
|
+
r"(?:const\s+|window\.)comparisonData\s*=\s*(\[.*?\]);", html, re.DOTALL
|
|
2599
2694
|
)
|
|
2600
2695
|
if not data_match:
|
|
2601
2696
|
print(f"Could not find comparisonData in {base_html_file}")
|
|
@@ -2606,31 +2701,37 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2606
2701
|
# Build base data (human actions only) and ensure predictions dict has base data
|
|
2607
2702
|
base_data = []
|
|
2608
2703
|
for item in base_comparison_data:
|
|
2609
|
-
base_data.append(
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2704
|
+
base_data.append(
|
|
2705
|
+
{
|
|
2706
|
+
"index": item.get("index", 0),
|
|
2707
|
+
"time": item.get("time", 0),
|
|
2708
|
+
"image_path": item.get("image_path", ""),
|
|
2709
|
+
"human_action": item.get("human_action", {}),
|
|
2710
|
+
}
|
|
2711
|
+
)
|
|
2615
2712
|
|
|
2616
2713
|
# JSON encode predictions
|
|
2617
2714
|
predictions_json = json.dumps(predictions_by_checkpoint)
|
|
2618
|
-
captures_json = json.dumps(
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2715
|
+
captures_json = json.dumps(
|
|
2716
|
+
[
|
|
2717
|
+
{
|
|
2718
|
+
"id": capture_id,
|
|
2719
|
+
"name": goal,
|
|
2720
|
+
"steps": len(base_data),
|
|
2721
|
+
}
|
|
2722
|
+
]
|
|
2723
|
+
)
|
|
2623
2724
|
|
|
2624
2725
|
# 1. Replace nav bar with unified header combining nav + controls
|
|
2625
2726
|
# Use shared header CSS and HTML for consistency with training dashboard
|
|
2626
|
-
header_css = f
|
|
2727
|
+
header_css = f"<style>{_get_shared_header_css()}</style>"
|
|
2627
2728
|
|
|
2628
2729
|
# Build the controls HTML for the viewer (example + checkpoint dropdowns)
|
|
2629
2730
|
controls_html = f'''
|
|
2630
2731
|
<div class="control-group">
|
|
2631
2732
|
<span class="control-label">Example</span>
|
|
2632
2733
|
<select id="capture-select">
|
|
2633
|
-
<option value="{capture_id}">{goal[:40]}{
|
|
2734
|
+
<option value="{capture_id}">{goal[:40]}{"..." if len(goal) > 40 else ""} ({len(base_data)})</option>
|
|
2634
2735
|
</select>
|
|
2635
2736
|
</div>
|
|
2636
2737
|
<div class="control-group">
|
|
@@ -2640,17 +2741,15 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2640
2741
|
'''
|
|
2641
2742
|
|
|
2642
2743
|
unified_header = header_css + _generate_shared_header_html(
|
|
2643
|
-
"viewer",
|
|
2644
|
-
controls_html=controls_html,
|
|
2645
|
-
meta_html=f"ID: {capture_id}"
|
|
2744
|
+
"viewer", controls_html=controls_html, meta_html=f"ID: {capture_id}"
|
|
2646
2745
|
)
|
|
2647
2746
|
|
|
2648
2747
|
# Remove any old viewer-controls div if it exists (from previous runs)
|
|
2649
2748
|
html = re.sub(
|
|
2650
2749
|
r'<div class="viewer-controls"[^>]*>.*?</div>\s*(?=<)',
|
|
2651
|
-
|
|
2750
|
+
"",
|
|
2652
2751
|
html,
|
|
2653
|
-
flags=re.DOTALL
|
|
2752
|
+
flags=re.DOTALL,
|
|
2654
2753
|
)
|
|
2655
2754
|
|
|
2656
2755
|
# Try to replace existing nav with unified header
|
|
@@ -2660,31 +2759,21 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2660
2759
|
r'<nav class="nav-bar"[^>]*>.*?</nav>\s*',
|
|
2661
2760
|
unified_header,
|
|
2662
2761
|
html,
|
|
2663
|
-
flags=re.DOTALL
|
|
2762
|
+
flags=re.DOTALL,
|
|
2664
2763
|
)
|
|
2665
2764
|
nav_replaced = True
|
|
2666
2765
|
|
|
2667
2766
|
# Remove the old <header> element - unified header already contains all info
|
|
2668
|
-
html = re.sub(
|
|
2669
|
-
r'<header[^>]*>.*?</header>\s*',
|
|
2670
|
-
'',
|
|
2671
|
-
html,
|
|
2672
|
-
flags=re.DOTALL
|
|
2673
|
-
)
|
|
2767
|
+
html = re.sub(r"<header[^>]*>.*?</header>\s*", "", html, flags=re.DOTALL)
|
|
2674
2768
|
|
|
2675
2769
|
# If no nav was found/replaced, insert unified header after <body>
|
|
2676
2770
|
if not nav_replaced:
|
|
2677
|
-
html = re.sub(
|
|
2678
|
-
r'(<body[^>]*>)',
|
|
2679
|
-
r'\1\n' + unified_header,
|
|
2680
|
-
html,
|
|
2681
|
-
count=1
|
|
2682
|
-
)
|
|
2771
|
+
html = re.sub(r"(<body[^>]*>)", r"\1\n" + unified_header, html, count=1)
|
|
2683
2772
|
|
|
2684
2773
|
# 3. Replace the comparisonData with multi-checkpoint system
|
|
2685
2774
|
# We need to modify the JavaScript to use our checkpoint system
|
|
2686
2775
|
|
|
2687
|
-
checkpoint_script = f
|
|
2776
|
+
checkpoint_script = f"""
|
|
2688
2777
|
<script>
|
|
2689
2778
|
// Unified viewer: multi-checkpoint support
|
|
2690
2779
|
// Bridge local comparisonData to window scope for cross-script access
|
|
@@ -2814,6 +2903,121 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2814
2903
|
}}
|
|
2815
2904
|
}};
|
|
2816
2905
|
|
|
2906
|
+
// Search functionality
|
|
2907
|
+
let searchQuery = '';
|
|
2908
|
+
let filteredIndices = [];
|
|
2909
|
+
|
|
2910
|
+
function advancedSearch(items, query, fields = ['action']) {{
|
|
2911
|
+
if (!query || query.trim() === '') {{
|
|
2912
|
+
return items.map((_, i) => i);
|
|
2913
|
+
}}
|
|
2914
|
+
|
|
2915
|
+
// Tokenize query
|
|
2916
|
+
const queryTokens = query
|
|
2917
|
+
.toLowerCase()
|
|
2918
|
+
.replace(/[^a-z0-9\\s]/g, ' ')
|
|
2919
|
+
.replace(/\\s+/g, ' ')
|
|
2920
|
+
.trim()
|
|
2921
|
+
.split(' ')
|
|
2922
|
+
.filter(t => t.length > 0);
|
|
2923
|
+
|
|
2924
|
+
if (queryTokens.length === 0) {{
|
|
2925
|
+
return items.map((_, i) => i);
|
|
2926
|
+
}}
|
|
2927
|
+
|
|
2928
|
+
const results = [];
|
|
2929
|
+
|
|
2930
|
+
items.forEach((item, idx) => {{
|
|
2931
|
+
// Build searchable text
|
|
2932
|
+
const searchParts = [];
|
|
2933
|
+
|
|
2934
|
+
// Add step index
|
|
2935
|
+
searchParts.push(String(idx));
|
|
2936
|
+
|
|
2937
|
+
// Add action type and details
|
|
2938
|
+
if (item.human_action) {{
|
|
2939
|
+
const action = item.human_action;
|
|
2940
|
+
if (action.type) searchParts.push(action.type);
|
|
2941
|
+
if (action.text) searchParts.push(action.text);
|
|
2942
|
+
if (action.key) searchParts.push(action.key);
|
|
2943
|
+
}}
|
|
2944
|
+
|
|
2945
|
+
const searchText = searchParts
|
|
2946
|
+
.join(' ')
|
|
2947
|
+
.toLowerCase()
|
|
2948
|
+
.replace(/[^a-z0-9\\s]/g, ' ')
|
|
2949
|
+
.replace(/\\s+/g, ' ');
|
|
2950
|
+
|
|
2951
|
+
// All query tokens must match
|
|
2952
|
+
const matches = queryTokens.every(token => searchText.includes(token));
|
|
2953
|
+
if (matches) {{
|
|
2954
|
+
results.push(idx);
|
|
2955
|
+
}}
|
|
2956
|
+
}});
|
|
2957
|
+
|
|
2958
|
+
return results;
|
|
2959
|
+
}}
|
|
2960
|
+
|
|
2961
|
+
function updateSearchResults() {{
|
|
2962
|
+
searchQuery = document.getElementById('search-input').value;
|
|
2963
|
+
filteredIndices = advancedSearch(baseData, searchQuery, ['action']);
|
|
2964
|
+
|
|
2965
|
+
// Update count
|
|
2966
|
+
const countEl = document.getElementById('search-count');
|
|
2967
|
+
if (searchQuery) {{
|
|
2968
|
+
countEl.textContent = `${{filteredIndices.length}} of ${{baseData.length}} steps`;
|
|
2969
|
+
}} else {{
|
|
2970
|
+
countEl.textContent = '';
|
|
2971
|
+
}}
|
|
2972
|
+
|
|
2973
|
+
// Update step list visibility
|
|
2974
|
+
updateStepListVisibility();
|
|
2975
|
+
|
|
2976
|
+
// If no results, show message
|
|
2977
|
+
if (searchQuery && filteredIndices.length === 0) {{
|
|
2978
|
+
countEl.textContent = 'No matches';
|
|
2979
|
+
countEl.style.color = 'var(--text-muted)';
|
|
2980
|
+
}} else {{
|
|
2981
|
+
countEl.style.color = 'var(--text-secondary)';
|
|
2982
|
+
}}
|
|
2983
|
+
}}
|
|
2984
|
+
|
|
2985
|
+
function updateStepListVisibility() {{
|
|
2986
|
+
const stepList = document.querySelector('.step-list');
|
|
2987
|
+
if (!stepList) return;
|
|
2988
|
+
|
|
2989
|
+
const stepItems = stepList.querySelectorAll('.step-item');
|
|
2990
|
+
stepItems.forEach((item, idx) => {{
|
|
2991
|
+
if (searchQuery && !filteredIndices.includes(idx)) {{
|
|
2992
|
+
item.style.display = 'none';
|
|
2993
|
+
}} else {{
|
|
2994
|
+
item.style.display = '';
|
|
2995
|
+
}}
|
|
2996
|
+
}});
|
|
2997
|
+
}}
|
|
2998
|
+
|
|
2999
|
+
function clearSearch() {{
|
|
3000
|
+
document.getElementById('search-input').value = '';
|
|
3001
|
+
updateSearchResults();
|
|
3002
|
+
}}
|
|
3003
|
+
|
|
3004
|
+
// Setup search event listeners
|
|
3005
|
+
document.getElementById('search-input').addEventListener('input', updateSearchResults);
|
|
3006
|
+
document.getElementById('search-clear-btn').addEventListener('click', clearSearch);
|
|
3007
|
+
|
|
3008
|
+
// Keyboard shortcut: Ctrl+F / Cmd+F
|
|
3009
|
+
document.addEventListener('keydown', (e) => {{
|
|
3010
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {{
|
|
3011
|
+
e.preventDefault();
|
|
3012
|
+
document.getElementById('search-input').focus();
|
|
3013
|
+
}}
|
|
3014
|
+
// Escape to clear search
|
|
3015
|
+
if (e.key === 'Escape' && document.activeElement === document.getElementById('search-input')) {{
|
|
3016
|
+
clearSearch();
|
|
3017
|
+
document.getElementById('search-input').blur();
|
|
3018
|
+
}}
|
|
3019
|
+
}});
|
|
3020
|
+
|
|
2817
3021
|
// Initialize on load
|
|
2818
3022
|
setTimeout(window.initCheckpointDropdown, 200);
|
|
2819
3023
|
|
|
@@ -2867,19 +3071,19 @@ def _enhance_comparison_to_unified_viewer(
|
|
|
2867
3071
|
}}, 200);
|
|
2868
3072
|
}})();
|
|
2869
3073
|
</script>
|
|
2870
|
-
|
|
3074
|
+
"""
|
|
2871
3075
|
|
|
2872
3076
|
# Insert checkpoint script before </body>
|
|
2873
|
-
html = html.replace(
|
|
3077
|
+
html = html.replace("</body>", checkpoint_script + "</body>")
|
|
2874
3078
|
|
|
2875
3079
|
# 4. Disable the old discoverDashboards that creates wrong nav
|
|
2876
3080
|
html = html.replace(
|
|
2877
|
-
|
|
2878
|
-
|
|
3081
|
+
"discoverDashboards();",
|
|
3082
|
+
"// discoverDashboards disabled - using unified viewer nav",
|
|
2879
3083
|
)
|
|
2880
3084
|
|
|
2881
3085
|
# Write output
|
|
2882
|
-
output_path.write_text(html, encoding=
|
|
3086
|
+
output_path.write_text(html, encoding="utf-8")
|
|
2883
3087
|
print(f"Generated unified viewer from {base_html_file.name}: {output_path}")
|
|
2884
3088
|
|
|
2885
3089
|
|
|
@@ -2904,67 +3108,78 @@ def _add_static_nav_to_comparison(
|
|
|
2904
3108
|
# Move comparison panel to be a full-width sibling BEFORE main-content (not inside it)
|
|
2905
3109
|
if '<div class="comparison-panel"' in html:
|
|
2906
3110
|
# Check if panel is NOT already right before main-content
|
|
2907
|
-
if
|
|
3111
|
+
if (
|
|
3112
|
+
'<div class="comparison-panel"' in html
|
|
3113
|
+
and 'class="comparison-panel"' in html
|
|
3114
|
+
):
|
|
2908
3115
|
# Check if it's in the wrong place (inside sidebar or main-content)
|
|
2909
|
-
in_sidebar = '<div class="sidebar">' in html and html.index(
|
|
2910
|
-
|
|
3116
|
+
in_sidebar = '<div class="sidebar">' in html and html.index(
|
|
3117
|
+
'<div class="comparison-panel"'
|
|
3118
|
+
) > html.index('<div class="sidebar">')
|
|
3119
|
+
in_main = (
|
|
3120
|
+
'<div class="main-content">' in html
|
|
3121
|
+
and '<div class="main-content">\n' in html
|
|
3122
|
+
and '<div class="main-content">\n <div class="comparison-panel"'
|
|
3123
|
+
in html
|
|
3124
|
+
)
|
|
2911
3125
|
|
|
2912
3126
|
if in_sidebar or in_main:
|
|
2913
3127
|
# Extract comparison panel from wherever it is
|
|
2914
3128
|
panel_match = re.search(
|
|
2915
3129
|
r'(\s*<div class="comparison-panel"[^>]*>.*?</div>\s*</div>\s*</div>)',
|
|
2916
3130
|
html,
|
|
2917
|
-
re.DOTALL
|
|
3131
|
+
re.DOTALL,
|
|
2918
3132
|
)
|
|
2919
3133
|
if panel_match:
|
|
2920
3134
|
panel_html = panel_match.group(1)
|
|
2921
3135
|
# Remove from current location
|
|
2922
|
-
html = html.replace(panel_html,
|
|
3136
|
+
html = html.replace(panel_html, "")
|
|
2923
3137
|
# Insert as sibling BEFORE main-content
|
|
2924
3138
|
html = html.replace(
|
|
2925
3139
|
'<div class="main-content">',
|
|
2926
|
-
panel_html.strip() + '\n <div class="main-content">'
|
|
3140
|
+
panel_html.strip() + '\n <div class="main-content">',
|
|
3141
|
+
)
|
|
3142
|
+
print(
|
|
3143
|
+
f" Moved Action Comparison above screenshot in {comparison_path.name}"
|
|
2927
3144
|
)
|
|
2928
|
-
print(f" Moved Action Comparison above screenshot in {comparison_path.name}")
|
|
2929
3145
|
|
|
2930
3146
|
# Build nav links if not provided
|
|
2931
3147
|
if nav_links is None:
|
|
2932
|
-
|
|
3148
|
+
# Default nav links if not provided
|
|
3149
|
+
nav_links = []
|
|
2933
3150
|
|
|
2934
3151
|
# Build nav HTML with active state for current file
|
|
2935
3152
|
# NOTE: No "Dashboards:" label to match training dashboard nav
|
|
2936
3153
|
current_file = comparison_path.name
|
|
2937
|
-
nav_html =
|
|
3154
|
+
nav_html = """
|
|
2938
3155
|
<nav class="nav-bar" style="display:flex;gap:8px;padding:12px 16px;background:#12121a;border:1px solid rgba(255,255,255,0.06);border-radius:8px;margin-bottom:16px;flex-wrap:wrap;">
|
|
2939
|
-
|
|
3156
|
+
"""
|
|
2940
3157
|
for filename, label in nav_links:
|
|
2941
3158
|
is_active = filename == current_file
|
|
2942
|
-
active_style =
|
|
3159
|
+
active_style = (
|
|
3160
|
+
"background:#00d4aa;color:#0a0a0f;border-color:#00d4aa;font-weight:600;"
|
|
3161
|
+
if is_active
|
|
3162
|
+
else ""
|
|
3163
|
+
)
|
|
2943
3164
|
nav_html += f' <a href="{filename}" style="padding:8px 16px;border-radius:6px;font-size:0.8rem;text-decoration:none;color:#888;background:#1a1a24;border:1px solid rgba(255,255,255,0.06);{active_style}">{label}</a>\n'
|
|
2944
|
-
nav_html +=
|
|
3165
|
+
nav_html += " </nav>\n"
|
|
2945
3166
|
|
|
2946
3167
|
# ALWAYS replace existing nav or add new one (for consistency)
|
|
2947
3168
|
if '<nav class="nav-bar"' in html:
|
|
2948
3169
|
# Replace existing nav
|
|
2949
3170
|
html = re.sub(
|
|
2950
|
-
r'<nav class="nav-bar"[^>]*>.*?</nav>\s*',
|
|
2951
|
-
nav_html,
|
|
2952
|
-
html,
|
|
2953
|
-
flags=re.DOTALL
|
|
3171
|
+
r'<nav class="nav-bar"[^>]*>.*?</nav>\s*', nav_html, html, flags=re.DOTALL
|
|
2954
3172
|
)
|
|
2955
3173
|
print(f" Updated navigation in {comparison_path.name}")
|
|
2956
3174
|
elif '<div class="container">' in html:
|
|
2957
3175
|
# Insert nav BEFORE the container, not inside it
|
|
2958
3176
|
# This ensures the unified header is not affected by container padding
|
|
2959
3177
|
html = html.replace(
|
|
2960
|
-
'<div class="container">',
|
|
2961
|
-
nav_html + '\n <div class="container">'
|
|
3178
|
+
'<div class="container">', nav_html + '\n <div class="container">'
|
|
2962
3179
|
)
|
|
2963
3180
|
print(f" Added navigation to {comparison_path.name}")
|
|
2964
|
-
elif
|
|
2965
|
-
html = html.replace(
|
|
3181
|
+
elif "<body>" in html:
|
|
3182
|
+
html = html.replace("<body>", "<body>\n" + nav_html)
|
|
2966
3183
|
print(f" Added navigation to {comparison_path.name}")
|
|
2967
3184
|
|
|
2968
3185
|
comparison_path.write_text(html)
|
|
2969
|
-
|
|
2970
|
-
|