File size: 21,476 Bytes
b7ce6b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# User Story 007: Clean Record API Implementation

## Story

**As a** robotics developer building teleoperation recording systems  
**I want** to record robot motor positions and control data using a clean `record()` function API  
**So that** I can capture teleoperation sessions for training AI models, analysis, and replay with the same simple patterns as other LeRobot.js functions

## Background

A community contributor has successfully implemented comprehensive recording functionality, including a `LeRobotDatasetRecorder` class with video recording, data export, and LeRobot dataset format support. The implementation is functional and well-integrated, but doesn't follow our established simple function API patterns from `calibrate()`, `teleoperate()`, and `findPort()`.

### Current Recording Implementation (From README)

The existing system works as documented in the web package README:

```typescript
import { LeRobotDatasetRecorder } from "@lerobot/web";

// Create a recorder with teleoperator and video streams
const recorder = new LeRobotDatasetRecorder(
  [teleoperator], // Array of teleoperators to record
  { main: videoStream }, // Video streams by camera key
  30, // Target FPS
  "Pick and place task" // Task description
);

// Start recording
await recorder.startRecording();
// ... robot performs task ...
const recordingData = await recorder.stopRecording();

// Export the dataset in various formats
await recorder.exportForLeRobot("zip-download");
await recorder.exportForLeRobot("huggingface", { repoName, accessToken });
await recorder.exportForLeRobot("s3", { bucketName, credentials });
```

This implementation has **excellent architecture** - the explicit teleoperator dependency makes it clear, testable, and flexible.

### Current Implementation Status

The existing recording system is **fully functional** with these components:

βœ… **Already Working Well:**

- `LeRobotDatasetRecorder` class with complete functionality
- Proper callback-based integration with teleoperators (no polling issues)
- Full LeRobot dataset format support with Parquet export
- Video recording and synchronization capabilities
- Complete cyberpunk example integration with camera management
- Clean separation between recording logic and teleoperator classes
- Export to ZIP, Hugging Face, and S3

### What Works Well (Keep This)

The current implementation has **excellent architectural decisions**:

- **Explicit Teleoperator Dependency**: `LeRobotDatasetRecorder([teleoperator], ...)` makes dependencies clear and predictable
- **Clean Separation**: Recording subscribes to teleoperator callbacks without tight coupling
- **Flexible Architecture**: Can record from any teleoperator, multiple teleoperators, or no teleoperator at all

### Areas for Improvement

The only issues are **API consistency** and **UI organization**:

- **Missing Simple Function API**: Users must instantiate `LeRobotDatasetRecorder` class directly instead of calling `record()` like other functions
- **UI Integration Pattern**: Recording is embedded within teleoperation view instead of being its own separate component/page
- **Library vs Demo Boundary**: Complex export functionality (video processing, cloud uploads) should be in demo layer, not standard library

### Convention Alignment Needed

Our established patterns from `calibrate()`, `teleoperate()`, and `findPort()` follow these principles:

- **Simple Function API**: `const process = await record(config)` (currently requires class instantiation)
- **Clean Process Objects**: Consistent `start()`, `stop()`, `getState()`, `result` interface (class has `startRecording()`, `stopRecording()`)
- **Hardware-Only Library**: Standard library handles only robotics hardware (currently includes video/export)
- **Demo Handles UI**: Examples handle video, export formats, browser storage, file downloads
- **Direct Usage**: End users call library functions directly without complex setup

## Acceptance Criteria

### Core Functionality

- [ ] **Standard Library API**: Clean `record(config)` function matching our established patterns (wrap existing `LeRobotDatasetRecorder`)
- [ ] **Process Object Interface**: Consistent `RecordProcess` with `start()`, `stop()`, `getState()`, `result` methods (adapt existing methods)
- [ ] **Hardware-Only Recording**: Library captures only robot motor positions and teleoperation data (move video/export to demo)
- [ ] **Clean Teleoperator Integration**: Recording uses existing callback system in `BaseWebTeleoperator`
- [ ] **Preserve Advanced Features**: Keep `LeRobotDatasetRecorder` class for users who need full control

### User Experience

- [ ] **Simple Integration**: Easy to add recording to existing teleoperation workflows
- [ ] **Consistent API**: Same patterns as `calibrate()` and `teleoperate()` for familiar developer experience
- [ ] **Separate UI Component**: Move recording from teleoperation view to its own dedicated page/section in cyberpunk example
- [ ] **Error Handling**: Clear error messages for recording failures or invalid configurations
- [ ] **Resource Management**: Proper cleanup of recording resources on stop/disconnect

### Technical Requirements

- [ ] **Library/Demo Separation**: Move video recording, complex export logic (HF, S3) to examples/demo layer
- [ ] **Wrapper Function**: Create simple `record()` function that wraps existing `LeRobotDatasetRecorder`
- [ ] **Preserve Existing Integration**: Keep current callback system in `BaseWebTeleoperator` (it works well)
- [ ] **TypeScript**: Fully typed with proper interfaces for recording configuration and data
- [ ] **No Breaking Changes**: Existing `LeRobotDatasetRecorder` class should remain available for advanced users

## Expected User Flow

### Basic Robot Recording (Proposed Simple API)

```typescript
import { teleoperate, record } from "@lerobot/web";

// 1. Create teleoperation first (existing pattern)
const teleoperationProcess = await teleoperate({
  robot: connectedRobot,
  teleop: { type: "keyboard" },
  calibrationData: calibrationData,
});

// 2. NEW: Clean API with explicit teleoperator dependency
const recordProcess = await record({
  teleoperator: teleoperationProcess.teleoperator, // ← Explicit dependency
  options: {
    fps: 30,
    taskDescription: "Pick and place task",
    onDataUpdate: (data) => {
      // Real-time recording data for UI feedback
      console.log(`Recorded ${data.frameCount} frames`);
      updateRecordingUI(data);
    },
    onStateUpdate: (state) => {
      // Recording state changes
      console.log(`Recording: ${state.isActive}`);
      updateRecordingStatus(state);
    },
  },
});

// 3. Start both processes
teleoperationProcess.start();
recordProcess.start();

// 4. Recording captures teleoperation automatically via callbacks
setTimeout(() => {
  recordProcess.stop();
}, 30000);

// 5. Get pure robot recording data (no video/export complexity)
const robotData = await recordProcess.result;
console.log("Episodes:", robotData.episodes);
console.log("Metadata:", robotData.metadata);
```

### Current Implementation (Works Well, Just Different API Style)

```typescript
import { LeRobotDatasetRecorder } from "@lerobot/web";

// CURRENT: Class-based API with explicit dependencies (good architecture!)
const recorder = new LeRobotDatasetRecorder(
  [teleoperator], // ← GOOD: Explicit teleoperator dependency
  { main: videoStream }, // Video complexity in library (to be moved)
  30, // fps
  "Pick and place task" // Task description
);

// Different method names than our conventions (but functional)
await recorder.startRecording();
// ... robot performs task ...
const result = await recorder.stopRecording();

// Complex export in standard library (should be demo-only)
await recorder.exportForLeRobot("zip-download");
await recorder.exportForLeRobot("huggingface", { repoName, accessToken });
```

**What's Good About Current Implementation:**

- βœ… **Explicit Dependencies**: Clear what the recorder needs to work
- βœ… **Clean Architecture**: Recording subscribes to teleoperator via callbacks
- βœ… **Full Functionality**: Complete LeRobot dataset format support
- βœ… **Flexible**: Can record from any teleoperator instance

### Recording with Teleoperation (Proposed Simple API)

```typescript
import { teleoperate, record } from "@lerobot/web";

// 1. Start teleoperation (existing pattern)
const teleoperationProcess = await teleoperate({
  robot: connectedRobot,
  teleop: { type: "keyboard" },
  calibrationData: calibrationData,
  onStateUpdate: (state) => {
    updateTeleoperationUI(state);
  },
});

// 2. NEW: Add recording with explicit teleoperator dependency
const recordProcess = await record({
  teleoperator: teleoperationProcess.teleoperator, // ← Explicit dependency (good!)
  options: {
    fps: 30,
    taskDescription: "Pick and place task",
    onDataUpdate: (data) => {
      console.log(`Recording frame ${data.frameCount}`);
    },
  },
});

// 3. Both run independently
teleoperationProcess.start();
recordProcess.start();

// 4. Control independently
setTimeout(() => {
  recordProcess.stop(); // Stop recording, keep teleoperation
}, 60000);

setTimeout(() => {
  teleoperationProcess.stop(); // Stop teleoperation
}, 120000);
```

**Why Explicit Teleoperator Dependency is Good:**

- 🎯 **Clear**: You know exactly what gets recorded
- πŸ”§ **Flexible**: Can record from any teleoperator
- πŸ§ͺ **Testable**: Easy to mock teleoperator for testing
- πŸ“¦ **Reusable**: Same teleoperator can serve multiple recorders

### Demo-Layer Dataset Export (Proposed Architecture)

```typescript
// In examples/demo - NOT in standard library
import { record } from "@lerobot/web";
import { DatasetExporter } from "./dataset-exporter"; // MOVE complex logic here

const recordProcess = await record({ robot, options });
recordProcess.start();

// ... recording session ...

recordProcess.stop();
const robotData = await recordProcess.result; // Pure motor data only

// MOVE TO DEMO: Complex export logic with video/cloud features
const exporter = new DatasetExporter({
  robotData,
  videoStreams: cameraStreams, // Demo manages video
  taskDescription: "Pick and place task",
});

// MOVE TO DEMO: Export options
await exporter.downloadZip();
await exporter.uploadToHuggingFace({ apiKey, repoName });
await exporter.uploadToS3({ credentials });
```

### Current Cyberpunk Example Integration

```typescript
// CURRENT: Recording embedded in teleoperation view
// examples/cyberpunk-standalone/src/components/teleoperation-view.tsx
<TeleoperationView robot={robot} />
// ^ Contains embedded <Recorder /> component

// PROPOSED: Separate recording page/component
// examples/cyberpunk-standalone/src/components/recording-view.tsx
<RecordingView robot={robot} />
// ^ Dedicated component with full recording interface
```

### Component Integration

```typescript
// React component - direct library usage like calibration
const [recordingState, setRecordingState] = useState<RecordingState>();
const [recordingData, setRecordingData] = useState<RecordingData>();
const recordProcessRef = useRef<RecordProcess | null>(null);

useEffect(() => {
  const initRecording = async () => {
    const process = await record({
      robot,
      options: {
        onStateUpdate: setRecordingState,
        onDataUpdate: setRecordingData,
      },
    });
    recordProcessRef.current = process;
  };
  initRecording();
}, [robot]);

const handleStartRecording = () => {
  recordProcessRef.current?.start();
};

const handleStopRecording = async () => {
  recordProcessRef.current?.stop();
  const data = await recordProcessRef.current?.result;
  // Handle recorded data
};
```

## Implementation Details

### File Structure Changes

```
packages/web/src/
β”œβ”€β”€ record.ts                         # UPDATE: Add simple record() function wrapper
β”œβ”€β”€ record-class.ts                   # RENAME: Move LeRobotDatasetRecorder here
β”œβ”€β”€ types/
β”‚   └── recording.ts                  # NEW: Recording-specific types for simple API
β”œβ”€β”€ teleoperators/
β”‚   └── base-teleoperator.ts          # KEEP: Current callback system works well
└── [MOVE TO EXAMPLES]
    β”œβ”€β”€ dataset-exporter.ts            # Video recording + export functionality
    β”œβ”€β”€ hf_uploader.ts                 # HuggingFace upload logic
    └── s3_uploader.ts                 # S3 upload logic
```

### Current vs Proposed Architecture

**Current (Working):**

- `LeRobotDatasetRecorder` class with full functionality
- Integrated in cyberpunk example within teleoperation view
- Video, HF, S3 export in standard library

**Proposed (Convention-Aligned):**

- Simple `record()` function wrapping existing class
- Separate recording component/page in cyberpunk example
- Video, HF, S3 export moved to demo layer
- Keep existing class available for advanced users

### Key Dependencies

#### Standard Library (Minimal Changes)

- **Keep Existing**: All current dependencies for core recording functionality
- **Wrapper Only**: Simple `record()` function is just a wrapper, no new dependencies

#### Demo Dependencies (To Be Moved)

- **Video/Export**: `parquet-wasm`, `apache-arrow`, `jszip` - move to examples
- **Upload**: `@huggingface/hub`, AWS SDK - move to examples
- **Keep Available**: Advanced users can still import `LeRobotDatasetRecorder` for full features

### Core Functions to Implement

#### Simple Record API (Wrapper)

```typescript
// record.ts - Simple wrapper around existing LeRobotDatasetRecorder
interface RecordConfig {
  teleoperator: WebTeleoperator; // ← Explicit dependency (keep this!)
  options?: {
    fps?: number; // Default: 30
    taskDescription?: string;
    onDataUpdate?: (data: RecordingData) => void;
    onStateUpdate?: (state: RecordingState) => void;
  };
}

interface RecordProcess {
  start(): void;
  stop(): void;
  getState(): RecordingState;
  result: Promise<RobotRecordingData>;
}

interface RecordingState {
  isActive: boolean;
  frameCount: number;
  episodeCount: number;
  duration: number; // milliseconds
  lastUpdate: number;
}

interface RecordingData {
  frameCount: number;
  currentEpisode: number;
  recentFrames: any[]; // Simplified for basic API
}

interface RobotRecordingData {
  episodes: any[]; // Pure motor data only (no video)
  metadata: {
    fps: number;
    robotType: string;
    startTime: number;
    endTime: number;
    totalFrames: number;
    totalEpisodes: number;
  };
}

// Simple wrapper function - internally uses LeRobotDatasetRecorder
// Preserves the excellent explicit dependency architecture
export async function record(config: RecordConfig): Promise<RecordProcess>;
```

#### Implementation Strategy

**Phase 1: Simple Wrapper (Preserves Current Architecture)**

```typescript
// record.ts - Simple wrapper implementation
import { LeRobotDatasetRecorder } from "./record-class.js";

export async function record(config: RecordConfig): Promise<RecordProcess> {
  // Use the provided teleoperator (explicit dependency - good!)
  const recorder = new LeRobotDatasetRecorder(
    [config.teleoperator], // ← Use explicit teleoperator dependency
    {}, // No video streams in simple API (move to demo)
    config.options?.fps || 30,
    config.options?.taskDescription || "Robot recording"
  );

  return {
    start: () => {
      recorder.startRecording();
      if (config.options?.onStateUpdate) {
        // Set up state update polling for simple API
        const updateLoop = () => {
          if (recorder.isRecording) {
            config.options.onStateUpdate!({
              isActive: recorder.isRecording,
              frameCount: recorder.teleoperatorData.length,
              episodeCount: recorder.teleoperatorData.length,
              duration: Date.now() - (recorder as any).startTime,
              lastUpdate: Date.now(),
            });
            setTimeout(updateLoop, 100);
          }
        };
        updateLoop();
      }
    },
    stop: () => {
      return recorder.stopRecording();
    },
    getState: () => ({
      isActive: recorder.isRecording,
      frameCount: recorder.teleoperatorData.length,
      episodeCount: recorder.teleoperatorData.length,
      duration: 0, // Calculate from recorder
      lastUpdate: Date.now(),
    }),
    result: recorder.stopRecording().then(() => ({
      episodes: recorder.episodes, // Pure motor data
      metadata: {
        fps: config.options?.fps || 30,
        robotType: "unknown", // Get from teleoperator if possible
        startTime: Date.now(),
        endTime: Date.now(),
        totalFrames: recorder.teleoperatorData.length,
        totalEpisodes: recorder.teleoperatorData.length,
      },
    })),
  };
}
```

**Key Benefits of This Approach:**

- βœ… **Preserves Explicit Dependencies**: Keeps the excellent `teleoperator` parameter
- βœ… **Minimal Changes**: Just wraps existing `LeRobotDatasetRecorder`
- βœ… **No Breaking Changes**: Current class remains available
- βœ… **Consistent API**: Follows `start()`, `stop()`, `getState()`, `result` pattern

#### Updated Teleoperate Integration

```typescript
// teleoperate.ts - Remove 100ms polling, add immediate callbacks
export async function teleoperate(
  config: TeleoperateConfig
): Promise<TeleoperationProcess> {
  const teleoperator = await createTeleoperatorProcess(config);

  return {
    start: () => {
      teleoperator.start();

      // NO MORE 100ms polling! Use immediate callbacks
      if (config.onStateUpdate) {
        teleoperator.setStateUpdateCallback(config.onStateUpdate);
      }
    },
    // ... rest of interface
  };
}
```

#### Clean Teleoperator Base

```typescript
// teleoperators/base-teleoperator.ts - Remove recording logic
export abstract class BaseWebTeleoperator extends WebTeleoperator {
  protected port: MotorCommunicationPort;
  public motorConfigs: MotorConfig[] = [];
  protected isActive: boolean = false;

  // REMOVED: All recording-related properties
  // REMOVED: dispatchMotorPositionChanged events
  // REMOVED: recordedMotorPositions, episodeIndex, etc.

  private stateUpdateCallback?: (state: TeleoperationState) => void;

  setStateUpdateCallback(callback: (state: TeleoperationState) => void): void {
    this.stateUpdateCallback = callback;
  }

  protected motorPositionsChanged(): void {
    // Call immediately when motors change - no events, no 100ms delay
    if (this.stateUpdateCallback) {
      const state = this.buildCurrentState();
      this.stateUpdateCallback(state);
    }
  }

  // Clean implementation without recording concerns
}
```

### Technical Considerations

#### Migration Strategy

**Preserve Existing Functionality:**

1. **Move Complex Logic**: `LeRobotDatasetRecorder` moves to `examples/` as demo code
2. **Extract Clean Core**: Create new `record()` function for standard library
3. **Update Examples**: Cyberpunk demo uses new API with demo-layer export functionality
4. **Remove Event System**: Clean up unused `dispatchMotorPositionChanged` events
5. **Fix Polling**: Replace 100ms polling with immediate callbacks

#### Performance Improvements

- **Remove Polling**: Eliminate artificial 100ms delays in favor of immediate callbacks
- **Event-Driven**: Only fire callbacks when robot state actually changes
- **Memory Efficiency**: No unused event listeners or redundant data structures
- **Responsive UI**: Immediate feedback for recording status and data updates

#### Future Extensibility

The clean architecture supports advanced recording features as demo enhancements:

```typescript
// Future: Advanced demo features (NOT in standard library)
class AdvancedDatasetExporter extends DatasetExporter {
  // Video synchronization, multi-camera support
  // Cloud storage, data preprocessing
  // Visualization, playback, analysis tools
}
```

## Definition of Done

### Phase 1: Simple Function API (Priority)

- [ ] **Clean Record API**: `record(config)` function implemented as wrapper around existing `LeRobotDatasetRecorder`
- [ ] **Process Interface**: `RecordProcess` with consistent `start()`, `stop()`, `getState()`, `result` methods
- [ ] **Hardware-Only Simple API**: Simple `record()` function captures only robot motor data (no video)
- [ ] **Preserve Advanced Features**: Keep existing `LeRobotDatasetRecorder` class available for full functionality
- [ ] **TypeScript Coverage**: Full type safety with proper interfaces for simple recording API

### Phase 2: UI Separation (Secondary)

- [ ] **Separate Recording Component**: Move recording from teleoperation view to dedicated component/page
- [ ] **Clean Navigation**: Add recording as separate section in cyberpunk example navigation
- [ ] **No Breaking Changes**: Existing functionality continues to work during transition

### Phase 3: Library/Demo Boundary (Future)

- [ ] **Demo Separation**: Video recording, export formats moved to examples layer (optional enhancement)
- [ ] **Advanced Export Demo**: Create demo showing complex export features using `LeRobotDatasetRecorder`
- [ ] **Documentation**: Clear examples showing simple API vs advanced class usage

### Success Criteria

- [ ] **API Consistency**: `record()` function follows same patterns as `calibrate()` and `teleoperate()`
- [ ] **No Regression**: All existing recording functionality preserved and working
- [ ] **Easy Migration**: Users can easily switch between simple API and advanced class
- [ ] **Clean Example**: Recording has its own dedicated UI section in cyberpunk demo