8-phase plan to ship a scene-based, AI-first, mobile+desktop video editor SPA on top of HyperFrames.
Stack: TanStack Router · React 19 · Tailwind v4 · shadcn/ui · Vite 8 · Zustand · TanStack Query · zod · MSW · dnd-kit · CodeMirror 6 · Biome
Spec sources:
docs/rfc-0001.md— product & architectureAGENTS.md— template/layout contract (caption rules, bare<template>,__FIELD__stamping)tmp/ovk-web-guidelines/— 16 verified runnable bundles mapped to RFC sections
| # | Phase | Deliverable |
|---|---|---|
| P0 | Foundation | Boots, routes navigate, providers wired, shadcn/ui init, MSW + zod schemas, AI/EditBus types exported. Empty Studio shell. |
| P1 | Responsive Studio shell | ONE <Studio> (CapCut bottom-sheet mobile / 4-zone desktop via shadcn ResizablePanelGroup + Sheet + Tabs). 6 PanelSlots reserved incl. AI. Playhead Zustand store. |
| P2 | Read-only studio wired to mock | HF renderer behind SlideRendererContext DI. Timeline + Properties + Captions render from mock fixture. Scrub → tl.time() seeks, slide swaps, audio syncs via timeupdate. Mock AI Dock with 3-scenario picker. |
| P3 | Editing — props + timeline | EditBus runtime (every mutation dispatches a typed op). Live-bind via ref writes (no re-render, GSAP survives). dnd-kit touch+mouse reorder. Undo/redo. SHA-256 image uploads. |
| P4 | Voiceover + captions | Mock TTS pipeline (deterministic durations). Word-level karaoke via timeWordsByCharRatio. GSAP direct-color tweens. 4 caption styles. Lint predicates block banned patterns in CI. |
| P5 | Per-slide HTML editor + LintGate | CodeMirror 6 lazy-loaded. R1–R4 lintHtml in shared/lib (pure, sync — imported by both editor and AI). Accept/revert atomic. Placeholder gutter markers. |
| P6 | AI Dock (Tier-1 + Tier-2) | AIProviderContext + EchoProvider (mock) + stubbed real providers. RFC 6902 JSON Patch for Tier-1; full HTML swap for Tier-2. Accept always dispatches via EditBus — same op shape as human edits. Tier-2 gated by lintHtml. Inline diff + expand. Settings → AI panel. Human edits surface as system pings. |
| P7 | Asset library + export pipeline | IndexedDB-backed SHA-256 content addressing. AssetDropzone + library grid + search. 6-step export pipeline UI (assemble → stamp → voiceover → captions → render → progress) streaming mock events. aHash render cache. |
Every phase opens with a Problem / Solution section explaining the architectural pain points and how the phase resolves them — not just what ships, but why.
A production-grade, AI-first, mobile+desktop video editor SPA — a CapCut-class scene editor in the browser, built on HyperFrames, fully mock-wired and ready for real backend swap-in.
| Surface | What works |
|---|---|
| Studio | One responsive component: 4-zone desktop (rail / stage / props / timeline, all resizable) ↔ CapCut-style mobile (stage + transport + bottom-sheet tools, one panel active at a time) |
| Stage | Live HF preview; scrub drives GSAP tl.time() via rAF (no per-frame React re-renders); slide swap on boundary cross; audio sync via external <audio> + timeupdate |
| Timeline | Drag-reorder (dnd-kit, touch + mouse with activation distance); add/remove slide; playhead; audio lanes; per-slide durations measured from TTS (read-only field) |
| Properties | Live-bind text fields (ref-write — no re-stamp, GSAP timeline survives); image upload → SHA-256 ref; voiceover textarea triggers batch TTS; per-slide transition override |
| HTML editor | CodeMirror 6 (route-split, ~400KB lazy-loaded); R1–R4 lint gate refuses <html> wrapper, missing data-composition-id, Tailwind usage; accept/revert atomic; placeholder gutter markers warn on __FIELD__ deletion |
| Captions | Word-level karaoke with per-word timing by char ratio; 4 styles (highlight / neon / editorial / eco-green); GSAP direct-color tweens (no transform / scale / font-size / text-shadow on .word--active); CI lint catches banned patterns |
| AI Dock | Mock streaming via EchoProvider; OpenAI/Anthropic/Ollama stubbed behind AIProvider interface; Tier-1 RFC 6902 JSON patches + Tier-2 HTML swaps; every Accept dispatches via EditBus (same op as a keyboard edit); Tier-2 auto-rejected if lintHtml fails; inline unified diff + expand to side-by-side dialog; Settings → AI provider switch (localStorage); human edits surface as system pings |
| Asset library | IndexedDB persistence; SHA-256 content addressing (dedup free); drag-to-field; search by ref prefix |
| Export | 6-step pipeline UI streaming mock events (assemble → stamp → voiceover → captions → render → progress); aHash + Hamming ≤ 5 render cache skips unchanged slides; HTML5 <video> preview + download |
| Undo/redo | ⌘Z / ⌘⇧Z work uniformly across all edit types (field, slide order, HTML, asset, voiceover) — driven by the EditBus event stream |
- AI is just another client of the editing API. No backdoor. Human keyboard edits and AI proposals hit the same
editBus.dispatch(op)path, the sameapplyOpreducer, the sameinverseOpfor undo, the sameEditEventstream for audit log + chat system pings, and the samelintHtml()gate (Tier-2). Switching AI providers changes one Context value; nothing else. - HyperFrames is pluggable. Every editor file consumes
useSlideRenderer(); onlyshared/renderer/hfRenderer.tsimports HF directly. Swap to the own-renderer (RFC §9.3) by implementing the same 3-method interface. Preview ↔ export fidelity drift becomes structurally impossible (RFC §9.4) — both go through one engine. - Mock-first. Every API call goes through a typed
client.ts→ MSW handler → zod parse. P7+ swaps MSW for a real FastAPI backend without touching any call site. - AGENTS.md contract honored.
__FIELD__stamping (function-formreplaceAll), bare<template>(no<html>wrapper), GSAP direct-color tweens for captions, no banned patterns on.word--active, voice IDs end inNeural,position:absolute;inset:0host divs,text-align:center+padding-top:XXvhfor vertical centering. - shadcn/ui throughout. No hand-rolled Button/Dialog/Tooltip/etc. Components added via CLI, themed via shadcn CSS variables, formatted by Biome. Custom CSS reserved for the caption-active color token (
--caption-active: oklch(0.92 0.18 95)≈#ffea00) and the 4 caption style files. - Performance invariants. Playhead writes via Zustand
getState()inside rAF (zero React re-renders per frame). Live-bind writes viaref.textContent(no slide tree re-mount). CodeMirror chunk is route-split. Asset blob URLs revoked on unmount.
Items intentionally out of scope for the 8 phases. Each has a clear insertion point when prioritized.
- Real FastAPI backend. Replace MSW handlers with real
fetchcalls to a Python server reusing the existingsrc/openvideokit/modules (templating.py,voiceover.py,captions.py,rendering.py). Call sites unchanged. - Real edge-tts + ffprobe + ffmpeg pipeline. The mock
/api/ttsendpoint returns deterministic durations from a text hash; real impl runs the batch TTS pipeline fromvoiceover.py. - Real
npx hyperframes rendersubprocess. Export mock streams canned events; real impl spawns the HF CLI and parses stdout (features/export/lib/parseProgress.tsalready structured for this). - OAuth2/JWT auth, project metadata sync, S3 + presigned asset URLs, asset catalog + search, credit ledger. All RFC §13 cloud control plane concerns.
- Cross-device project sync. Cloud stores project
index.json+ slide HTML as opaque content; no inference in cloud.
- Real OpenAI / Anthropic / Ollama HTTP wiring. P6 ships the
AIProviderabstraction and stubs that throw"Not implemented in P6". Real impls consume the samestream(messages, ctx) → AsyncIterable<AIStreamEvent>contract — no editor code changes. - Tier-2 HTML generation quality. EchoProvider returns canned HTML; real providers generate actual GSAP/CSS. Quality of generated HTML is a prompt-engineering concern, not architectural.
- API key encryption. P6 stores keys in
localStorageunderovk:ai:prefix with a UI warning. Real encryption would use WebCrypto with a user-supplied passphrase (out of scope for local-first v1). - Move/copy/test JSON Patch ops. P6's
applyPatchimplementsreplace/add/removeonly. If real providers emitmove/copy/test, extendapplyPatch(single file).
- Own renderer to replace HyperFrames (RFC §9.3). Headless-Chromium frame capture + FFmpeg encode/mux. Slides in behind the same
SlideRendererinterface; editor and export unchanged. - Frame capture for aHash render cache. P7's cache mock returns placeholder hashes; real HIT detection needs the own-renderer's
renderAt(t) → frameto return pixel data.
transitionsbundle (RFC §5 root between-slide transitions). The 17th bundle in the curriculum, marked[~]deferred. Would add a transitions editor surface + the<!-- SCENE_TRANSITIONS -->marker injection (currently captions-only).- Multi-slide selection / batch operations. Timeline is single-selection in P3; multi-select + batch-reorder/delete is a follow-up.
- Collaborative editing (multi-cursor). Out of scope per RFC §20.
- Native signed installer. v1 ships as a local web app; pywebview / Electron / Tauri-with-Python-sidecar is a deferred packaging step (RFC §14) — zero core rewrite either way.
- Asset purge UI. IndexedDB quota warnings ship in P7; user-driven purge is a follow-up.
- Asset folders/tags. P7 is flat (SHA-ref + mime + date); hierarchical organization is a UX follow-up.
- Browser image compression on upload. Optional; would cap asset size before storing.
package.jsonreserves the dep slot.
- Command palette (⌘K).
app/layout/CommandPalette.tsxreserved in P0 layout; wiring is a follow-up using shadcnCommand(cmdk). - Keyboard shortcuts panel. P3 ships
⌘Z/⌘⇧Z; full shortcuts help is a follow-up. buildCaptionHtml/buildCaptionTimelineJsconsumed by export. P4 ships these (locked contract); P7 wires them intoassembleWorkspace. If caption format changes, the contract is the single update point.
Each .md is independently shippable. Suggested workflow:
- Read the phase's Problem / Solution first — that's the architectural commitment.
- Skim Files added / modified for scope.
- Work the Tasks checklist top-to-bottom (it's ordered by dependency).
- Run Verification before declaring the phase done — Vitest fixtures are pinned from the curriculum bundles where possible.
- Check Risks / open questions for known sharp edges.
Phases build linearly (P0 → P7) but the contracts between them are stable: P3's EditOp shapes are what P6 AI proposals produce; P5's lintHtml() is what P6 Tier-2 gates through; P4's caption helpers are what P7 export consumes. Changing a contract means updating two files (the producer + the consumer), not refactoring end-to-end.