Skip to content

Commit 6126aeb

Browse files
committed
refactor(viewer): consolidate to standalone HTML generation
1 parent 3396594 commit 6126aeb

File tree

3 files changed

+35
-152
lines changed

3 files changed

+35
-152
lines changed

CLAUDE.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -382,20 +382,19 @@ az ml workspace sync-keys -n openadapt-ml -g openadapt-agents
382382
- Filter/search evaluations by epoch or correctness
383383

384384
### Viewer Code Consolidation
385-
**Status**: TODO - HIGH PRIORITY
385+
**Status**: DONE
386386

387-
**Problem**: Viewer code is fragmented across multiple locations:
387+
**Problem**: Viewer code was fragmented across multiple locations:
388388
1. `generate_training_dashboard()` - generates unified viewer template
389-
2. `_enhance_comparison_to_unified_viewer()` - injects checkpoint_script into comparison.html
390-
3. `comparison.html` from capture - has its own display logic
391-
392-
This causes issues where changes don't propagate correctly.
389+
2. `_enhance_comparison_to_unified_viewer()` - injected checkpoint_script into comparison.html
390+
3. `comparison.html` from capture - had its own display logic
393391

394-
**Solution**: Create a single `generate_unified_viewer()` function that:
395-
- Takes comparison data as input
396-
- Generates a complete standalone viewer.html
397-
- Has all display logic in one place
398-
- Doesn't rely on injecting scripts into existing HTML
392+
**Solution implemented**:
393+
- `generate_unified_viewer_from_output_dir()` now always uses `_generate_unified_viewer_from_extracted_data()`
394+
- This generates a complete standalone viewer.html without script injection
395+
- `_enhance_comparison_to_unified_viewer()` marked as deprecated
396+
- All viewer display logic is now in one place (`_generate_unified_viewer_from_extracted_data`)
397+
- Changes to viewer code now propagate reliably
399398

400399
### README API Documentation
401400
**Status**: VERIFIED

openadapt_ml/cloud/local.py

Lines changed: 12 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -58,49 +58,14 @@ def _regenerate_viewer_if_possible(output_dir: Path) -> bool:
5858
5959
Returns True if viewer was regenerated, False otherwise.
6060
"""
61-
from openadapt_ml.training.trainer import _enhance_comparison_to_unified_viewer
62-
63-
# Look for base comparison file
64-
base_file = output_dir / "comparison.html"
65-
if not base_file.exists():
66-
# Try to find any comparison HTML
67-
comparison_files = list(output_dir.glob("*comparison*.html"))
68-
if comparison_files:
69-
base_file = comparison_files[0]
70-
else:
71-
return False
72-
73-
# Load predictions from checkpoint files
74-
predictions_by_checkpoint = {"None": []}
75-
for pred_file in output_dir.glob("predictions_*.json"):
76-
checkpoint_name = pred_file.stem.replace("predictions_", "")
77-
if "epoch" in checkpoint_name:
78-
display_name = checkpoint_name.replace("epoch", "Epoch ").replace("_", " ").title()
79-
elif checkpoint_name == "preview":
80-
display_name = "Preview"
81-
else:
82-
display_name = checkpoint_name.title()
83-
84-
try:
85-
with open(pred_file) as f:
86-
predictions_by_checkpoint[display_name] = json.load(f)
87-
except json.JSONDecodeError:
88-
pass
89-
90-
# Get capture info
91-
capture_id = "capture"
92-
goal = "Complete the recorded workflow"
61+
from openadapt_ml.training.trainer import generate_unified_viewer_from_output_dir
9362

9463
try:
95-
_enhance_comparison_to_unified_viewer(
96-
base_file,
97-
predictions_by_checkpoint,
98-
output_dir / "viewer.html",
99-
capture_id,
100-
goal,
101-
)
102-
print(f"Regenerated viewer: {output_dir / 'viewer.html'}")
103-
return True
64+
viewer_path = generate_unified_viewer_from_output_dir(output_dir)
65+
if viewer_path:
66+
print(f"Regenerated viewer: {viewer_path}")
67+
return True
68+
return False
10469
except Exception as e:
10570
print(f"Could not regenerate viewer: {e}")
10671
return False
@@ -399,7 +364,7 @@ def cmd_viewer(args: argparse.Namespace) -> int:
399364
"""Regenerate viewer from local training output."""
400365
from openadapt_ml.training.trainer import (
401366
generate_training_dashboard,
402-
_enhance_comparison_to_unified_viewer,
367+
generate_unified_viewer_from_output_dir,
403368
TrainingState,
404369
TrainingConfig,
405370
)
@@ -435,66 +400,12 @@ def cmd_viewer(args: argparse.Namespace) -> int:
435400
(current_dir / "dashboard.html").write_text(dashboard_html)
436401
print(f" Regenerated: dashboard.html")
437402

438-
# Find comparison HTML to enhance
439-
# Try epoch-specific files first, then fall back to generic comparison.html
440-
comparison_files = list(current_dir.glob("comparison_epoch*.html"))
441-
base_file = None
442-
if comparison_files:
443-
# Use the latest epoch comparison
444-
base_file = sorted(comparison_files)[-1]
445-
elif (current_dir / "comparison.html").exists():
446-
base_file = current_dir / "comparison.html"
447-
448-
if base_file:
449-
print(f" Using base file: {base_file.name}")
450-
451-
# Load all prediction files
452-
predictions_by_checkpoint = {"None": []}
453-
for pred_file in current_dir.glob("predictions_*.json"):
454-
checkpoint_name = pred_file.stem.replace("predictions_", "")
455-
# Map to display name
456-
if "epoch" in checkpoint_name:
457-
display_name = checkpoint_name.replace("epoch", "Epoch ").replace("_", " ").title()
458-
elif checkpoint_name == "preview":
459-
display_name = "Preview"
460-
else:
461-
display_name = checkpoint_name.title()
462-
463-
try:
464-
with open(pred_file) as f:
465-
data = json.load(f)
466-
# Handle predictions JSON structure
467-
if isinstance(data, dict) and "predictions" in data:
468-
predictions_by_checkpoint[display_name] = data["predictions"]
469-
else:
470-
predictions_by_checkpoint[display_name] = data
471-
print(f" Loaded predictions from {pred_file.name}")
472-
except json.JSONDecodeError:
473-
print(f" Warning: Could not parse {pred_file.name}")
474-
475-
# Get capture info from training log
476-
capture_id = "capture"
477-
goal = "Complete the recorded workflow"
478-
if log_file.exists():
479-
try:
480-
with open(log_file) as f:
481-
log_data = json.load(f)
482-
capture_path = log_data.get("capture_path", "")
483-
if capture_path:
484-
capture_id = Path(capture_path).name
485-
except (json.JSONDecodeError, KeyError):
486-
pass
487-
488-
_enhance_comparison_to_unified_viewer(
489-
base_file,
490-
predictions_by_checkpoint,
491-
current_dir / "viewer.html",
492-
capture_id,
493-
goal,
494-
)
495-
print(f"\nGenerated: {current_dir / 'viewer.html'}")
403+
# Generate unified viewer using consolidated function
404+
viewer_path = generate_unified_viewer_from_output_dir(current_dir)
405+
if viewer_path:
406+
print(f"\nGenerated: {viewer_path}")
496407
else:
497-
print("\nNo comparison.html found. Run comparison first or copy from capture directory.")
408+
print("\nNo comparison data found. Run comparison first or copy from capture directory.")
498409

499410
if args.open:
500411
webbrowser.open(str(current_dir / "viewer.html"))

openadapt_ml/training/trainer.py

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,43 +1824,12 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None:
18241824
print("No comparison data found, cannot generate unified viewer")
18251825
return None
18261826

1827-
# Generate the unified viewer by enhancing an existing comparison file
1827+
# Generate the unified viewer using standalone HTML template
1828+
# (Consolidated approach - always use standalone for reliability)
18281829
viewer_path = output_dir / "viewer.html"
18291830

1830-
# Find the best existing comparison file to use as base (prefer epoch files)
1831-
# Check both main directory and archive folder
1832-
base_html_file = None
1833-
search_dirs = [output_dir, output_dir / "archive"]
1834-
1835-
for search_dir in search_dirs:
1836-
if not search_dir.exists():
1837-
continue
1838-
# Look for epoch files first (reverse sort = latest first)
1839-
for comp_file in sorted(search_dir.glob("comparison_epoch*.html"), reverse=True):
1840-
base_html_file = comp_file
1841-
break
1842-
if base_html_file:
1843-
break
1844-
# Fall back to comparison_preview.html
1845-
preview_file = search_dir / "comparison_preview.html"
1846-
if preview_file.exists():
1847-
base_html_file = preview_file
1848-
break
1849-
1850-
if base_html_file is None:
1851-
# Fall back to basic viewer
1852-
_generate_unified_viewer_from_extracted_data(
1853-
base_data=base_data,
1854-
predictions_by_checkpoint=predictions_by_checkpoint,
1855-
output_path=viewer_path,
1856-
capture_id=capture_id,
1857-
goal=goal,
1858-
)
1859-
return viewer_path
1860-
1861-
# Use the existing comparison file as base and enhance it with unified controls
1862-
_enhance_comparison_to_unified_viewer(
1863-
base_html_file=base_html_file,
1831+
_generate_unified_viewer_from_extracted_data(
1832+
base_data=base_data,
18641833
predictions_by_checkpoint=predictions_by_checkpoint,
18651834
output_path=viewer_path,
18661835
capture_id=capture_id,
@@ -2307,9 +2276,9 @@ def _generate_unified_viewer_from_extracted_data(
23072276
if (thinkMatch) thinking = thinkMatch[1].trim().substring(0, 150);
23082277
23092278
if (clickSomMatch) {{
2310-
action = {{ type: 'click', element: `[${clickSomMatch[1]}]` }};
2279+
action = {{ type: 'click', element: `[${{clickSomMatch[1]}}]` }};
23112280
}} else if (typeSomMatch) {{
2312-
action = {{ type: 'type', element: `[${typeSomMatch[1]}]`, text: typeSomMatch[2] }};
2281+
action = {{ type: 'type', element: `[${{typeSomMatch[1]}}]`, text: typeSomMatch[2] }};
23132282
}} else if (typeSimpleMatch) {{
23142283
action = {{ type: 'type', text: typeSimpleMatch[1] }};
23152284
}} else if (clickCoordMatch) {{
@@ -2323,12 +2292,12 @@ def _generate_unified_viewer_from_extracted_data(
23232292
let html = '';
23242293
if (action) {{
23252294
if (action.type === 'click' && action.element) {{
2326-
html = `<div style="font-weight:600;color:var(--accent);">CLICK(${action.element})</div>`;
2295+
html = `<div style="font-weight:600;color:var(--accent);">CLICK(${{action.element}})</div>`;
23272296
}} else if (action.type === 'click' && action.x !== undefined) {{
23282297
html = `<div style="font-weight:600;color:var(--accent);">CLICK(x=${{action.x.toFixed(2)}}, y=${{action.y.toFixed(2)}})</div>`;
23292298
}} else if (action.type === 'type') {{
2330-
const elem = action.element ? `${action.element}, ` : '';
2331-
html = `<div style="font-weight:600;color:var(--accent);">TYPE(${elem}"${{action.text}}")</div>`;
2299+
const elem = action.element ? `${{action.element}}, ` : '';
2300+
html = `<div style="font-weight:600;color:var(--accent);">TYPE(${{elem}}"${{action.text}}")</div>`;
23322301
}} else if (action.type === 'raw') {{
23332302
html = `<div style="color:var(--accent);">${{action.text}}</div>`;
23342303
}}
@@ -2637,6 +2606,10 @@ def _enhance_comparison_to_unified_viewer(
26372606
) -> None:
26382607
"""Enhance an existing comparison HTML file into a unified viewer.
26392608
2609+
DEPRECATED: This function uses script injection which is fragile.
2610+
Use _generate_unified_viewer_from_extracted_data() instead for a
2611+
standalone viewer that doesn't depend on the comparison.html structure.
2612+
26402613
Takes the nice openadapt-capture viewer and adds:
26412614
- Simplified nav (Training + Viewer only)
26422615
- Checkpoint dropdown to switch between predictions

0 commit comments

Comments
 (0)