This document describes the integration between the Segmentation Viewer (which displays extracted episodes) and the Capture Viewer (which shows full recording playback). The integration allows users to seamlessly navigate from an episode overview to seeing it in the context of the complete recording.
Selected approach: Option A - External Link
Why this approach:
- Simplicity: Minimal code changes, easy to maintain
- File protocol compatible: Works with both
file://andhttp://URLs - Separation of concerns: Each viewer remains standalone
- No complex communication: No iframe postMessage complexity
- Bookmarkable: Users can bookmark specific episode contexts
┌─────────────────────────────┐
│ Segmentation Viewer │
│ segmentation_viewer.html │
│ │
│ ┌───────────────────────┐ │
│ │ Episode: Navigate to │ │
│ │ System Settings │ │
│ │ │ │
│ │ [View Full Recording] │──┼──┐
│ └───────────────────────┘ │ │
└─────────────────────────────┘ │
│ URL with parameters:
│ ?highlight_start=0.0
│ &highlight_end=3.5
│ &episode_name=Navigate+to+System+Settings
│
▼
┌─────────────────────────────────────────────┐
│ Capture Viewer │
│ ../openadapt-capture/{recording-id}/ │
│ viewer.html │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🔔 Viewing Episode Context │ │
│ │ Episode: Navigate to System Settings│ │
│ │ (Time range: 0.0s - 3.5s) │ │
│ └─────────────────────────────────────┘ │
│ │
│ Timeline: [====|█████|==========] │
│ ^ ^ ^ │
│ │ │ └─ End of recording │
│ │ └─ Highlighted episode │
│ └─ Start of recording │
└─────────────────────────────────────────────┘
Episodes are stored in JSON files with the following key fields:
{
"recording_id": "turn-off-nightshift",
"episodes": [
{
"episode_id": "episode_001",
"name": "Navigate to System Settings",
"description": "User opens System Settings...",
"start_time": 0.0,
"end_time": 3.5,
"recording_ids": ["turn-off-nightshift"],
"steps": [...],
"boundary_confidence": 0.92
}
]
}When clicking "View Full Recording", the following parameters are passed:
| Parameter | Type | Description | Example |
|---|---|---|---|
highlight_start |
float | Episode start time in seconds | 0.0 |
highlight_end |
float | Episode end time in seconds | 3.5 |
episode_name |
string | Episode name for display | Navigate to System Settings |
Example URL:
../openadapt-capture/turn-off-nightshift/viewer.html?highlight_start=0.0&highlight_end=3.5&episode_name=Navigate%20to%20System%20Settings
File: /Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html
<div class="detail-section" id="recording-links-section" style="display: none;">
<h3>View in Context</h3>
<div id="recording-links" style="display: flex; flex-wrap: wrap; gap: 12px;"></div>
</div>.recording-link-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: linear-gradient(135deg, #00d9ff 0%, #0099cc 100%);
color: #0a0a0f;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
}In the showEpisodeDetails() function:
// Recording links
const recordingLinksSection = document.getElementById('recording-links-section');
const recordingLinksContainer = document.getElementById('recording-links');
recordingLinksContainer.innerHTML = '';
if (episode.recording_ids && episode.recording_ids.length > 0) {
recordingLinksSection.style.display = 'block';
episode.recording_ids.forEach(recordingId => {
// Construct absolute path to capture viewer
// Works with file:// protocol when opening HTML directly
const capturePath = `file:///Users/abrichr/oa/src/openadapt-capture/${recordingId}/viewer.html`;
const params = new URLSearchParams();
if (episode.start_time !== undefined) {
params.set('highlight_start', episode.start_time);
}
if (episode.end_time !== undefined) {
params.set('highlight_end', episode.end_time);
}
if (episode.name) {
params.set('episode_name', episode.name);
}
const url = params.toString() ? `${capturePath}?${params.toString()}` : capturePath;
const link = document.createElement('a');
link.href = url;
link.className = 'recording-link-btn';
link.target = '_blank';
link.innerHTML = `
<svg viewBox="0 0 24 24">
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
<span>View Full Recording: ${recordingId}</span>
`;
recordingLinksContainer.appendChild(link);
});
}File: /Users/abrichr/oa/src/openadapt-viewer/capture_viewer.html
init() {
// Parse URL parameters
const params = new URLSearchParams(window.location.search);
this.highlightStart = params.has('highlight_start') ? parseFloat(params.get('highlight_start')) : null;
this.highlightEnd = params.has('highlight_end') ? parseFloat(params.get('highlight_end')) : null;
this.episodeName = params.get('episode_name');
// Jump to highlight start time if provided
if (this.highlightStart !== null) {
this.jumpToTimestamp(this.highlightStart);
}
}jumpToTimestamp(timestamp) {
// Find the step closest to this timestamp
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].timestamp >= timestamp) {
this.currentStep = i;
return;
}
}
// If timestamp is after all steps, go to last step
this.currentStep = this.steps.length - 1;
}<div x-data="{
episodeName: new URLSearchParams(window.location.search).get('episode_name'),
highlightStart: new URLSearchParams(window.location.search).get('highlight_start'),
highlightEnd: new URLSearchParams(window.location.search).get('highlight_end')
}"
x-show="episodeName"
style="background: linear-gradient(135deg, rgba(255, 200, 0, 0.2), rgba(255, 165, 0, 0.2)); border: 2px solid rgba(255, 200, 0, 0.5); border-radius: 8px; padding: 16px; margin-bottom: 24px;">
<div style="font-weight: 600;">Viewing Episode Context</div>
<div>Episode: <span x-text="episodeName"></span></div>
<div>(Time range: <span x-text="parseFloat(highlightStart).toFixed(1)"></span>s - <span x-text="parseFloat(highlightEnd).toFixed(1)"></span>s)</div>
</div><!-- Episode highlight overlay -->
<template x-if="highlightStart !== null && highlightEnd !== null">
<div style="position: absolute; top: 0; height: 100%; background: rgba(255, 200, 0, 0.3); border: 2px solid rgba(255, 200, 0, 0.8);"
:style="`left: ${(highlightStart / steps[steps.length - 1].timestamp) * 100}%; width: ${((highlightEnd - highlightStart) / steps[steps.length - 1].timestamp) * 100}%`">
</div>
</template><template x-if="episodeName">
<div style="margin-top: 8px; text-align: center; font-size: 0.85rem; color: rgba(255, 200, 0, 0.9); font-weight: 600;">
Episode: <span x-text="episodeName"></span>
</div>
</template>- User opens
segmentation_viewer.html - Clicks "Load File" and selects an episode JSON file
- Episodes are displayed in a grid with metadata
- User clicks on an episode card
- Episode details section appears showing:
- Overview (description)
- Information grid (application, duration, timestamps, confidence scores)
- View in Context section with "View Full Recording" button(s)
- Steps list
- Timeline (if available)
- User clicks "View Full Recording: {recording_id}" button
- Button opens in new tab (
target="_blank") - URL includes episode context parameters
- Capture viewer loads with URL parameters
- Episode context banner appears at top showing:
- "Viewing Episode Context"
- Episode name
- Time range
- Playback automatically jumps to episode start time
- Timeline shows highlight overlay in yellow/orange marking the episode segment
- Episode name displayed below timeline
For the integration to work, the directory structure must be:
/Users/abrichr/oa/src/
├── openadapt-viewer/
│ ├── segmentation_viewer.html # Episode list viewer
│ └── test_episodes.json # Example episode data
│
└── openadapt-capture/
└── {recording-id}/ # e.g., turn-off-nightshift/
├── viewer.html # Recording playback viewer
├── screenshots/ # Frame screenshots
└── transcript.json # Audio transcript
Path assumptions:
- Segmentation viewer is at:
openadapt-viewer/segmentation_viewer.html - Capture viewers are at:
openadapt-capture/{recording_id}/viewer.html - Absolute path to capture:
file:///Users/abrichr/oa/src/openadapt-capture/{recording_id}/viewer.html(Uses absolute file:// URL to work when opening HTML files directly in browser)
Step 1: Load Episode Data
- Open
segmentation_viewer.htmlin a web browser - Click "Load File" button
- Select an episode JSON file (e.g.,
test_episodes.json) - Episodes appear in the grid
Step 2: Explore Episodes
- Browse episodes using:
- Filter by Recording dropdown
- Search by name or description
- Click an episode card to see details
Step 3: View Full Recording
- Scroll to "View in Context" section
- Click "View Full Recording: {recording_id}" button
- Recording viewer opens in new tab, showing:
- Episode context banner at top
- Highlighted segment in timeline
- Playback starting at episode start time
Step 4: Navigate Recording
- Use playback controls (Play/Pause, Prev/Next)
- Adjust playback speed (0.5x, 1x, 2x, 4x)
- Click timeline to jump to any point
- Yellow highlight shows episode boundaries
Testing the Integration
-
Open segmentation viewer:
open /Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html
-
Load test episodes:
- Use provided
test_episodes.jsonfile - Or create your own following the schema below
- Use provided
-
Click episode and then "View Full Recording" button
-
Verify in capture viewer:
- Banner shows episode name and time range
- Timeline has yellow highlight overlay
- Playback starts at episode start time
- Episode name appears below timeline
Episode JSON Schema
{
"recording_id": "string",
"recording_name": "string",
"episodes": [
{
"episode_id": "string",
"name": "string",
"description": "string",
"application": "string (optional)",
"start_time": "number (seconds)",
"end_time": "number (seconds)",
"start_time_formatted": "string (optional)",
"end_time_formatted": "string (optional)",
"duration": "number (seconds, optional)",
"recording_ids": ["array of recording IDs"],
"frame_indices": ["array of integers (optional)"],
"steps": ["array of step descriptions"],
"step_summaries": ["alternative to steps array"],
"boundary_confidence": "number 0-1 (optional)",
"coherence_score": "number 0-1 (optional)",
"occurrence_count": "integer (for canonical episodes, optional)"
}
],
"boundaries": [...],
"llm_model": "string",
"processing_timestamp": "ISO 8601 string",
"coverage": "number 0-1",
"avg_confidence": "number 0-1"
}| Approach | Pros | Cons | Selected |
|---|---|---|---|
| External Link | Simple, works with file://, bookmarkable | Opens new tab | ✅ Yes |
| Inline Embed | Seamless UX | Complex, file:// restrictions, security | ❌ No |
| Unified Viewer | Best UX | Major refactoring needed | ❌ No |
| Deep Link | Shows context | Same as External Link | ✅ Implemented |
- Cross-origin compatible: Works with file:// protocol
- Shareable: URLs can be bookmarked or shared
- Stateless: No cleanup needed
- Simple: Standard web pattern
Episodes store start_time and end_time in seconds. The capture viewer's steps also have timestamps, making it straightforward to:
- Find the step closest to
highlight_start - Jump to that step on load
- Highlight the range in the timeline
- External link from episode to recording
- URL parameter passing
- Auto-jump to episode start
- Timeline highlight overlay
- Episode context banner
-
Bidirectional Navigation
- Add "View Episodes" button in capture viewer
- Link back to segmentation viewer filtered to current recording
-
Multi-Episode View
- Show all episodes from recording in sidebar
- Click to jump between episodes
- Visual timeline with all episode boundaries
-
Episode Comparison
- Side-by-side view of multiple episodes
- Show differences in steps/actions
- Highlight common patterns
-
Better Highlight Visualization
- Fade out non-episode frames
- Add episode boundaries as markers on timeline
- Show step numbers within episode
-
Search & Filter in Recording
- Search for specific actions within episode
- Filter by action type (click, type, scroll)
- Navigate between matching actions
-
Episode Editing
- Adjust episode boundaries in recording viewer
- Save modifications back to episode JSON
- Split/merge episodes
-
Annotation Tools
- Add notes to specific frames
- Tag important actions
- Export annotated episodes
-
Performance Metrics
- Show action accuracy (if ground truth available)
- Highlight errors or anomalies
- Compare human vs AI actions
Symptoms:
- Episode details show but no "View in Context" section
Causes:
- Episode has no
recording_idsfield recording_idsarray is empty
Solution: Ensure episode JSON includes:
{
"recording_ids": ["turn-off-nightshift"]
}Symptoms:
- Clicking button opens blank page or shows "File not found"
- Browser shows
ERR_FILE_NOT_FOUNDerror
Causes:
- Recording directory doesn't exist
viewer.htmlnot present in recording directory- Directory structure mismatch
- Incorrect
recording_idin JSON (e.g., "sample-" prefix that doesn't match actual directory) - Using relative path instead of absolute file:// URL
Solution:
- Verify directory exists:
/Users/abrichr/oa/src/openadapt-capture/{recording_id}/ - Check viewer file:
/Users/abrichr/oa/src/openadapt-capture/{recording_id}/viewer.html - Ensure
recording_idin episode JSON matches directory name exactly (no "sample-" prefix unless directory has it) - Verify the segmentation viewer uses absolute file:// paths:
file:///Users/abrichr/oa/src/openadapt-capture/${recordingId}/viewer.html - Check test data files (e.g.,
sample_segmentation_results.json) have correct recording IDs
Symptoms:
- Banner shows episode name but timeline has no yellow highlight
Causes:
highlight_startorhighlight_endmissing from URL- Recording has no steps/timestamps
- Calculation error in overlay positioning
Solution:
- Check URL includes both parameters:
?highlight_start=X&highlight_end=Y - Verify steps array in viewer has
timestampfields - Open browser console and check for JavaScript errors
Symptoms:
- Viewer opens but starts at step 0, not episode start
Causes:
jumpToTimestamp()function not being called- No step matches the start timestamp
- Alpine.js not initialized properly
Solution:
- Check browser console for errors
- Verify
init()function is defined in x-data - Ensure Alpine.js is loaded: check for
<script defer src="...alpinejs..."></script>
A complete demo is available using the test data:
File locations:
- Segmentation viewer:
/Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html - Test episodes:
/Users/abrichr/oa/src/openadapt-viewer/test_episodes.json - Recording viewer:
/Users/abrichr/oa/src/openadapt-capture/turn-off-nightshift/viewer.html
Demo flow:
- Open segmentation viewer
- Load
test_episodes.json - Click "Navigate to System Settings" episode
- Click "View Full Recording: turn-off-nightshift"
- Observe episode context features in capture viewer
The segmentation-recording integration provides a seamless way for users to explore extracted episodes while maintaining access to the full recording context. The external link approach is simple, maintainable, and works across different serving methods (file:// and http://).
Key benefits:
- Easy navigation from episodes to recordings
- Visual context with timeline highlighting
- Automatic positioning at episode start
- Clear indicators when viewing episode context
- Minimal code changes to existing viewers
The integration is production-ready and can be extended with the enhancements listed in the Future Improvements section.
The segmentation viewer now displays screenshots from recordings throughout the UI to provide visual context:
- Thumbnails - Episode cards show preview images in list view
- Key Frames Gallery - Episode details display a grid of important frames
- Step Screenshots - Inline images show what each step looks like
Episodes can include a screenshots object with thumbnail and key frames:
{
"episode_id": "episode_001",
"name": "Navigate to System Settings",
"steps": [
"Click System Settings icon in dock",
"Wait for Settings window to open",
"Click on Displays in sidebar"
],
"screenshots": {
"thumbnail": "file:///path/to/screenshots/capture_31807990_step_0.png",
"key_frames": [
{
"frame_index": 0,
"step_index": 0,
"path": "file:///path/to/screenshots/capture_31807990_step_0.png",
"action": "Click System Settings icon in dock"
},
{
"frame_index": 2,
"step_index": 1,
"path": "file:///path/to/screenshots/capture_31807990_step_2.png",
"action": "Wait for Settings window to open"
}
]
}
}Screenshots are stored in the recording's screenshots directory:
openadapt-capture/
turn-off-nightshift/
screenshots/
capture_31807990_step_0.png
capture_31807990_step_1.png
capture_31807990_step_2.png
...
Episode cards now show a thumbnail at the top:
<div class="episode-card">
<div class="episode-thumbnail">
<img src="file:///path/to/screenshot.png" alt="Episode Name" loading="lazy">
</div>
<div class="episode-content">
<div class="episode-name">Navigate to System Settings</div>
<div class="episode-description">User opens System Settings...</div>
</div>
</div>CSS:
.episode-thumbnail {
width: 100%;
height: 160px;
background: #0a0a0f;
overflow: hidden;
}
.episode-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}- Uses
object-fit: coverfor consistent card height - Falls back gracefully if no thumbnail available
- Lazy loading for performance
Episode details show a grid of key frames:
<div class="detail-section key-frames-section">
<h3>Key Frames</h3>
<div class="key-frames-grid">
<div class="key-frame-card">
<img src="file:///path/to/screenshot.png" class="key-frame-img">
<div class="key-frame-caption">
<span class="key-frame-step-number">Step 1</span>
Click System Settings icon in dock
</div>
</div>
</div>
</div>CSS:
.key-frames-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.key-frame-img {
width: 100%;
height: 180px;
object-fit: contain;
background: #000;
}- Responsive grid layout
- Uses
object-fit: containto preserve aspect ratio - Shows step number badge and action description
Screenshots are displayed inline below each step:
<ul class="step-list">
<li class="step-item">
<div>1. Click System Settings icon in dock</div>
<img src="file:///path/to/screenshot.png" class="step-screenshot" loading="lazy">
</li>
</ul>CSS:
.step-screenshot {
margin-top: 12px;
max-width: 100%;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}- Only shown if screenshot matches step index
- Full width within step container
- Lazy loading for performance
Use absolute file:// URLs for cross-directory access:
"thumbnail": "file:///Users/abrichr/oa/src/openadapt-capture/turn-off-nightshift/screenshots/capture_31807990_step_0.png"Important: Use three slashes after file: for absolute paths.
When creating segmentation results, include screenshot paths in the episode data:
from pathlib import Path
def add_screenshot_metadata(episode, recording_path):
"""Add screenshot paths to episode data."""
screenshots_dir = recording_path / "screenshots"
screenshot_files = sorted(screenshots_dir.glob("*.png"))
key_frames = []
for step_idx, frame_idx in enumerate(episode["frame_indices"]):
if frame_idx < len(screenshot_files):
screenshot_path = screenshot_files[frame_idx]
key_frames.append({
"frame_index": frame_idx,
"step_index": step_idx,
"path": f"file://{screenshot_path.absolute()}",
"action": episode["steps"][step_idx]
})
episode["screenshots"] = {
"thumbnail": f"file://{screenshot_files[episode['frame_indices'][0]].absolute()}",
"key_frames": key_frames
}
return episodeChoose key frames that best represent each step:
- First frame - Initial state before the step
- Action frame - Moment of interaction (click, type)
- Result frame - State after action completes
Limit to 3-5 key frames per episode for performance.
- Lazy Loading - Uses
loading="lazy"attribute for deferred loading - Image Format - PNG format (100-300KB typical)
- Key Frame Limit - Recommend 3-5 per episode
- Object Fit -
coverfor thumbnails,containfor details
The file:// protocol has security restrictions:
- Works: Opening HTML file directly via
file://URL - Blocked: Accessing via HTTP server (CORS)
- Solution: Serve from same origin or embed as base64
- Open
/Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html - Load
/Users/abrichr/oa/src/openadapt-viewer/test_episodes.json - Verify:
- Episode cards show thumbnails
- Click episode to see key frames gallery (3 images)
- Steps section shows inline screenshots
- All images load correctly
/Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html- Viewer UI/Users/abrichr/oa/src/openadapt-viewer/test_episodes.json- Example data with screenshots/Users/abrichr/oa/src/openadapt-viewer/SEGMENTATION_RECORDING_INTEGRATION.md- Documentation
- Timeline Scrubber - Browse all frames in episode with interactive timeline
- Click Overlays - Show click markers on screenshots (H for human, AI for predicted)
- Zoom/Lightbox - Click to view screenshot in full size
- Video Playback - Generate video clip from frames
- Base64 Embedding - Embed screenshots in JSON for portability