Spaces:
Running
Running
| # Content Studio Foundation | |
| ## Scope and status | |
| Content Studio Phase 1 adds a real, project-scoped editing foundation at | |
| `/projects/[projectId]/studio`. It uses the authenticated Workspace Shell, | |
| existing project provider, canonical project assets, centralized BFF client, | |
| runtime OpenAPI discovery, TanStack Query, command registry, permissions, and | |
| MediaRouter design system. | |
| This phase does not add a backend editor document, rendering pipeline, | |
| captions/effects authoring workflow, AI editing, publishing, analytics, or | |
| collaboration. Nothing in the Studio fabricates assets, persistence, render | |
| jobs, progress, or successful exports. | |
| ## Feature architecture | |
| The feature boundary is `frontend/features/editor/`: | |
| - `types/` defines the persisted editor document and temporary UI state types. | |
| - `stores/` contains a per-project Zustand store factory and React provider. | |
| - `selectors/` derives narrow UI views without subscribing components to the | |
| complete store. | |
| - `utils/` contains deterministic timeline and source-time math. | |
| - `api/` discovers exact OpenAPI contracts and routes supported calls through | |
| the centralized API client. | |
| - `services/` separates editor-state and canonical project-asset adapters. | |
| - `commands/` and `hooks/` integrate commands and keyboard shortcuts. | |
| - `components/` contains the Studio workspace, media panel, preview, timeline, | |
| inspector, toolbar, and playback controls. | |
| - `tests/` verifies math, state, capability, command, shortcut, UI, preview, | |
| asset, and architecture behavior. | |
| The Studio route is nested below the existing authenticated project route. It | |
| does not replace `/projects` or `/projects/[projectId]`. | |
| ## Domain model | |
| `ProjectEditorState` is a versioned, typed document containing a project ID, | |
| timeline, and render settings. The timeline uses one extensible track model for | |
| `video`, `audio`, `caption`, and `overlay` tracks. Clips are a discriminated | |
| union of media, audio, caption, and effect clips; core editor state does not use | |
| `any[]` or `Record<string, any>`. | |
| Every clip has an ID, owning track ID, timeline start, duration, label, | |
| visibility, opacity, and safe primitive metadata. Source-backed media and audio | |
| clips also carry the canonical asset ID, source start, and source duration. | |
| Media clips include transform and volume; audio clips include volume and fade | |
| values. Transitions and markers have typed extension points but no authoring UI | |
| in this phase. | |
| Incoming authoritative documents are validated at runtime before hydration. | |
| Validation covers schema version, project identity, render settings, unique | |
| tracks/clips, track ordering, clip discriminators, finite time/property values, | |
| source timing, transitions, and markers. | |
| ## Timeline coordinate system | |
| All timeline and source coordinates are integer milliseconds. Pixels are only | |
| a viewport projection: | |
| `pixels = milliseconds / 1000 * pixelsPerSecond` | |
| Moving a clip changes its timeline `startMs`. Left trimming changes timeline | |
| start, duration, source start, and source duration while enforcing source-in and | |
| minimum-duration bounds. Right trimming changes duration/source-out and can | |
| expand only to browser/backend-discovered source duration. Splitting creates | |
| two clips with contiguous timeline and source ranges. The source asset is never | |
| modified. | |
| The current minimum clip duration is 100 milliseconds. Negative time, | |
| non-finite values, duplicate IDs, mismatched track ownership, and invalid source | |
| ranges are rejected by timeline validation. | |
| ## State ownership | |
| TanStack Query remains authoritative for backend resources: | |
| - project metadata and permissions; | |
| - project assets; | |
| - project jobs and activity; | |
| - runtime OpenAPI discovery; | |
| - future editor-state and render mutations when contracts exist. | |
| A per-project Zustand store owns only client editing state: | |
| - the in-memory working editor document; | |
| - playhead, playback, rate, and duration; | |
| - selection and project-asset preview selection; | |
| - timeline zoom and scroll viewport; | |
| - active tool and mobile editing mode; | |
| - media metadata discovered by the browser; | |
| - temporary pointer interactions; | |
| - bounded undo/redo history and dirty state. | |
| The store is created inside `EditorStoreProvider`; it is not a second global | |
| project store. Project changes cause a new provider/store instance. Components | |
| use narrow selectors and stable store actions. | |
| ## Undo, redo, and editing invariants | |
| History records meaningful immutable timeline mutations: clip insertion, | |
| removal, move, trim, split and properties; track addition, removal, update and | |
| reorder. Pointer previews do not create history entries. A completed pointer | |
| interaction creates at most one entry. Playback frames, seeking, hover, media | |
| metadata discovery, selection, zoom, and temporary mobile UI modes are not | |
| recorded. | |
| History is bounded to 100 entries. Undo/redo recalculates timeline duration, | |
| stops playback, reconciles selection when entities disappear, and compares the | |
| result to the last hydrated/saved timeline for accurate dirty state. Locked | |
| tracks are protected inside the store as well as in the UI. | |
| ## Timeline interaction | |
| The timeline provides a time ruler, keyboard-seekable playhead, horizontally | |
| scrollable multi-track canvas, track headers, clip selection, clip duration | |
| visualization, viewport culling, zoom, fit-to-width, and accessible empty state. | |
| Clip movement and edge trimming use controlled pointer capture, with keyboard | |
| left/right movement in 100 millisecond steps. Track reordering and canonical | |
| asset insertion support controlled drag/drop, while up/down track controls and | |
| the Add to timeline button provide keyboard and touch alternatives. Dropping an | |
| asset validates compatible track type and lock state before updating the local | |
| editor session. | |
| Drag payloads are never authorization evidence. Any future persistence or | |
| render backend must independently validate workspace, project, asset ownership, | |
| document contents, and permissions. | |
| ## Preview and playback | |
| The preview uses real `<video>`, `<audio>`, and `<img>` elements with the | |
| existing authenticated BFF media path: | |
| `/api/backend/v1/media/{request_id}/{filename}` | |
| No raw storage credential or provider secret is exposed. Browser | |
| `loadedmetadata` supplies real media duration and dimensions when backend asset | |
| metadata is absent. Video/audio insertion remains unavailable until a positive | |
| real duration is known. Still-image insertion remains unavailable because no | |
| authoritative default image duration exists. | |
| Seeking converts timeline time to source time. While playing, animation-frame | |
| synchronization converts the media element's source time back to the timeline | |
| playhead. Playback stops at the selected clip boundary or when no playable clip | |
| exists. Video transform and opacity plus track/clip volume state are applied to | |
| the real preview element. | |
| Phase 1 previews one active source-backed clip. It does not composite multiple | |
| video/overlay tracks or mix multiple audio tracks; that requires the future | |
| render/preview engine contract. | |
| ## Inspector | |
| With no selection, the inspector shows real timeline properties. With a clip, | |
| it exposes only implemented values: timeline start/duration, volume, opacity, | |
| video transform, audio fades, caption text, and removal from the timeline. | |
| Removal never deletes the canonical asset. Source-in/out values are visible but | |
| read-only until richer source metadata/validation is available. Locked and | |
| read-only project states disable mutation controls. | |
| Effect parameters are not editable. The inspector describes their unavailable | |
| capability instead of presenting a working-looking control. | |
| ## Persistence contract | |
| `PERSISTENCE = UNAVAILABLE` in the current backend. | |
| Runtime OpenAPI discovery only activates persistence when one exact | |
| project-scoped path exists with GET and PUT or PATCH: | |
| - `GET /v1/projects/{project_id}/editor-state` | |
| - `PUT /v1/projects/{project_id}/editor-state` or | |
| `PATCH /v1/projects/{project_id}/editor-state` | |
| The current project metadata endpoint is not used as an editor document store. | |
| Its 16 KB limit and lack of editor schema versioning, revisions, and optimistic | |
| concurrency do not provide a durable editing contract. The Studio therefore | |
| labels the working document `Session only`; Save is disabled with the exact | |
| capability reason. The session is intentionally not stored in `localStorage` as | |
| an authoritative substitute. | |
| A future backend persistence phase must provide: | |
| - a typed, versioned editor document schema with timeline-sized limits; | |
| - revision/ETag or equivalent optimistic concurrency semantics; | |
| - workspace/project authorization for user and API-key authentication; | |
| - database ownership/RLS isolation; | |
| - validation of all asset references against project/workspace ownership; | |
| - safe audit events and structured errors; | |
| - GET plus idempotent PUT or revision-aware PATCH in runtime OpenAPI. | |
| Optional local crash recovery may be added later, but it must remain explicitly | |
| non-authoritative and must never replace the backend document. | |
| ## Render contract | |
| `RENDER_CONTRACT = UNAVAILABLE` in the current backend. | |
| Runtime discovery only activates Render when OpenAPI advertises: | |
| - `POST /v1/projects/{project_id}/renders` | |
| The Render control is disabled with a truthful reason today. No request is sent, | |
| no progress is generated, and no success toast can occur while the contract is | |
| absent. | |
| A future render phase must accept a validated editor revision/reference and | |
| render settings, then return a durable job identity using the existing job | |
| infrastructure. The backend must validate project and asset ownership, execute | |
| the FFmpeg/render pipeline, attach the output to the canonical project asset | |
| system, and define progress, failure, cancellation, retry, and audit semantics. | |
| The runtime OpenAPI contract must include typed request, job, and error schemas. | |
| ## Capabilities and permissions | |
| The Studio route requires the real project API and `projects:read`. Timeline | |
| mutations require the existing `projects:update` permission and an active | |
| project. Save additionally requires the editor-state write contract. Render | |
| requires the render contract plus `projects:update` and `jobs:create`. | |
| The UI treats frontend permission/capability checks as presentation gates only. | |
| The backend remains authoritative for project ID, workspace, asset ownership, | |
| editor metadata, and render permission. | |
| ## Commands and keyboard shortcuts | |
| Commands are registered dynamically through the shared command registry and | |
| removed when the Studio unmounts. Unsupported commands are not registered. | |
| - Open Content Studio: project read capability. | |
| - Play/Pause (`Space`): an active playable clip. | |
| - Undo (`Ctrl/Cmd+Z`): editable project and non-empty undo history. | |
| - Redo (`Ctrl/Cmd+Shift+Z`): editable project and non-empty redo history. | |
| - Split Clip (`S`): editable unlocked selection and valid playhead split. | |
| - Delete Clip (`Delete`/`Backspace`): editable unlocked selection. | |
| - Zoom In, Zoom Out, Fit Timeline: open Studio. | |
| Shortcuts ignore input, textarea, select, and content-editable focus targets. | |
| Browser/system shortcuts are otherwise left intact. | |
| ## Responsive and accessible design | |
| Desktop uses a dense project-media, preview, inspector, and full-width timeline | |
| workspace. Tablet prioritizes preview and timeline while moving the inspector | |
| into a lower panel. Mobile exposes dedicated Preview, Timeline, and Inspector | |
| modes with 44-pixel minimum touch targets instead of shrinking every desktop | |
| panel simultaneously. | |
| Timeline clips are focusable options with selected semantics and spoken | |
| time-range labels. The ruler is a keyboard slider. Track controls, trim handles, | |
| asset actions, toolbar actions, and mobile tabs have accessible names. Focus | |
| uses the design-system focus ring, and the global reduced-motion rule applies. | |
| Drag actions have keyboard/touch alternatives. | |
| ## Performance strategy | |
| - The route is isolated below the project feature and does not load on Home or | |
| Projects. | |
| - Components subscribe to narrow Zustand selectors rather than the complete | |
| store. | |
| - Clip views are memoized and callbacks/store actions are stable. | |
| - The timeline calculates the visible time range and culls off-screen clips | |
| with an overscan window. | |
| - Browser media uses metadata preloading instead of eagerly downloading every | |
| project asset. | |
| - TanStack Query owns deduplication and caching for project resources. | |
| - Timeline data remains time-based, allowing later lane virtualization without | |
| changing the document schema. | |
| Track virtualization is intentionally deferred because Phase 1 has no evidence | |
| of track counts requiring it. Clip viewport culling is already in place. | |
| ## Error and empty states | |
| The Studio reuses project loading, not-found, permission, API error, and retry | |
| states. Project media has unavailable, loading, API error, empty, no-search- | |
| results, and normal states. Preview handles no selection, missing/deleted asset, | |
| unsupported format, loading, network/codec failure, and image failure without | |
| crashing the editor. Invalid authoritative documents are rejected rather than | |
| hydrated. | |
| ## Future extension points | |
| The typed clip union, track types, transitions, markers, schema version, | |
| render settings, API adapters, command registration, and project provider allow | |
| future phases to add captions, transitions, effects, AI editing, audio | |
| processing, templates, aspect-ratio presets, publishing, and collaboration | |
| without replacing this boundary. Those features remain outside Phase 1. | |