/** * App-wide structural types. Kept narrow on purpose: the SDK is the * source of truth for robot state, and views only need a small slice * (poses, motion frames, the saved-move record). */ /** Row-major flat-16 head matrix. The SDK's wire shape. */ export type FlatHead = number[]; /** Nested 4x4 head matrix. The app's internal shape (see motor.ts). */ export type NestedHead = number[][]; /** [left, right] antenna angles in radians. */ export type Antennas = [number, number]; /** * A single recorded actuator frame. Stored in nested + camelCase shape; * the SDK boundary (motor.ts) converts to/from the flat wire form. */ export interface MotionFrame { head: NestedHead | null; antennas: Antennas | null; body_yaw: number; check_collision: boolean; } /** * A full recorded motion. The Python Marionette + downstream tooling * expects this exact shape on disk (snake_case `set_target_data`), so * we keep it 1:1 with the v1 schema even though we're now in TS. */ export interface Motion { time: number[]; set_target_data: MotionFrame[]; duration?: number; frameCount?: number; capturedHz?: number; } /** * Audio source attached to a move. `silent` means the move records * pure motion with no audio reference. */ export type AudioMode = "silent" | "upload" | "record_now" | "record_live"; /** Where audio playback should land. Mirrors v1's settings.audioOutput. */ export type AudioOutput = "robot" | "device"; /** Where the recording mic should pull from. Mirrors v1's state.audioMic. */ export type AudioMic = "device" | "robot"; /** * Persisted move row in IndexedDB. Kept flat on purpose (no folders * in the MVP) - community provenance is carried inline so we can * show a "from @author" chip on the row. */ export interface MoveRecord { id: string; label: string; emoji: string; motion: Motion; audioBlob: Blob | null; audioMode: AudioMode; duration: number; createdAt: number; // ─── Community provenance ───────────────────────────────────── /** Set on community-imported moves, never on user-recorded ones. */ communityAuthor?: string; /** Original HF dataset id (`org/name`). */ communityRepoId?: string; /** File path inside the repo, e.g. `data/wave.json`. Used as the * idempotency key when an import targets a move already present. */ communityMovePath?: string; /** Free-form description from the dataset's JSON, if any. */ description?: string; // ─── Share-to-HF state ──────────────────────────────────────── /** True once the user has pushed this move to their own dataset. */ shared?: boolean; /** HF repo the move was last shared to (`/`). */ sharedRepoId?: string; } /** * One Marionette community dataset as listed by HF * (`?filter=reachy_mini_community_moves`). */ export interface CommunityDatasetSummary { /** `org/name` - the canonical HF repo id. */ repoId: string; /** Display name (last path segment of `repoId`). */ name: string; /** First path segment of `repoId`. */ author: string; emoji: string; downloads: number; /** ISO timestamp from HF (`lastModified` / `last_modified`). */ updated: string | null; /** Lazy field: filled in by `HF.countMoves` after the list arrives. */ movesCount: number | null; } /** * A single `.json` inside a dataset, paired with a side-by-side * `.wav` when one exists. */ export interface CommunityMoveSummary { /** Stable id derived from `repoId` + path (used for idempotency). */ id: string; /** `data/wave.json` - the path inside the repo. */ path: string; /** Display name (filename without extension). */ label: string; hasAudio: boolean; } /** Persisted user preferences. */ export interface AppSettings { audioOutput: AudioOutput; robotVolume: number; debug: boolean; }