Clips and Frame.io Clone , Media Apps for CreatorTrack¶
URL: https://mkdocs.justinsforge.com/memory/plans/clips-frameio-media-app-2026-06-18/
Status: VISION + ARCHITECTURE (pre-implementation). 2026-06-18. Roadmap: CreatorTrack step 4 (apps), after Tier 2 + auth + the files/streaming backend. Builds on prior thinking: [[clip-library-findings-2026-06-17]], [[gus-clip-library-mam-2026-06-15]].
What this is¶
Two sibling apps inside CreatorTrack, both self-hosted and fully owned (no SaaS, no Immich dependency , owning it is the whole point):
- Clips , an Immich-style AI footage library. Search your raw footage in natural language ("baby gus eating minnows"), with AI auto-tagging/captioning + manual tags, then act on the results: send to Premiere, copy to a project folder, or spin up a new project.
- Review (Frame.io clone) , export a cut, scrub it frame-accurately, leave timestamped comments, and draw/circle on frames. Likely a separate surface sharing the player + storage.
DECISIONS LOCKED (2026-06-18)¶
- AI vision = hybrid (local model for cheap bulk embeddings + Claude vision for rich captions, selectively). Exact models/where-it-runs decided later; pipeline is built model-agnostic.
- Build everything locally, own it. No Immich, no third-party search. Our collections + pgvector + our pipeline. This is the point of the product.
- Index in place. Footage stays on the Finn NVMe; we never move/copy originals. We store an index row + thumbnails + embeddings only.
- Whole-video search now; moment/segment search later (roadmap). See the get-go decision below so we never have to re-ingest the library to add it.
Architecture¶
The footage never enters our DB; our core Postgres (CT 109, pgvector already live) stores the
INDEX. A clip = a row in a CreatorTrack collection, so the whole library rides the Tier 2
collections/properties/views you're finishing.
Footage offloaded to Finn NVMe (8TB active-video tier)
│ ingest worker (Python on Finn, idempotent, watches the tree)
├─ ffmpeg → poster thumbnail + N keyframes (every ~few sec / on scene change)
├─ local vision model → per-keyframe embedding ──► pgvector (timestamped)
├─ Claude vision (selective) → rich caption + suggested tags + suggested rename
├─ ffprobe → duration, codec, resolution, fps, capture date, camera
└─ upsert the clip row (path is the idempotency key)
│
Clips app (a collection of clips, gallery view)
│ query: "baby gus eating minnows"
├─ embed query → vector similarity over keyframe vectors (max/aggregate per clip)
├─ + structured filters (tags, person, date, camera) via the property system
└─ ranked clips → gallery grid + hover-scrub preview
│
Actions on a selection:
├─ "Send to Premiere" → Premiere MCP on Sol imports the files into the open project
├─ "Copy to project" → copies originals into a chosen/new project folder on NVMe
└─ "New project" → name + company → scaffold folders + macros (existing skill)
THE GET-GO DECISION for future segment search (decision 4)¶
Store per-keyframe, timestamped embeddings from day one (we generate keyframes for
thumbnails anyway), not a single whole-video vector. Then:
- Now: whole-video search = aggregate (max) the keyframe matches up to the clip.
- Later: moment/segment search = cluster the matching keyframes into time ranges , a query +
UI change, no re-ingest of the library.
Also reserve the schema: a clips row is the parent; a future clip_segments table references
clip_id + start/end. Don't build segments now, but the model and the embeddings are ready for it.
What's reused vs genuinely new¶
| Piece | Status |
|---|---|
| Clip index = collection w/ media properties | Reuse , Tier 2 collections/properties/views |
| Semantic search engine | Reuse , pgvector live on core Postgres (CT 109) |
| Tags / people / dates / ratings on clips | Reuse , the property registry (multi-select/person/date) |
| "Send to Premiere" | Reuse , Premiere MCP on Sol (97 tools, programmatic import) |
| "New project" button | Reuse , scaffold-project skill (folders + macros) behind a UI button |
| Comments (for Review) | Reuse/extend , Tier 2 inline-comment concept, re-anchored to timestamps |
| AI ingest pipeline (thumbnail + keyframe embed + caption + rename) | NEW , Python worker on Finn |
| Video streaming endpoint (range requests NVMe → browser) | NEW , the "files/streaming backend" |
| Media components: player + frame-scrubber, clip grid, import buttons, upload mgr | NEW , a media Tier 1.5 set |
| Drawing/annotation canvas (Review) | NEW , SVG/canvas overlay on the paused frame |
The two apps¶
App 1 , Clips (footage library + AI search)¶
- Data model:
clipscollection (path, thumbnail, duration, codec, res, fps, capture date, camera, tags[], person[], rating, ai_caption, status) + timestamped keyframe embeddings. - Ingest: idempotent Finn worker; manual tagging refines AI output; AI proposes renames you approve (or auto for trusted patterns).
- Search: natural-language (vector) fused with structured property filters; gallery grid with hover-scrub.
- Actions: Send-to-Premiere (Sol MCP), Copy-to-project, New-project (scaffold skill). The New-project button is independently useful and could ship first (it's just UI over an existing skill).
App 2 , Review (Frame.io clone)¶
- Flow: pick/export a cut → hosted/streamed → reviewer scrubs frame-accurately → timestamped comments → draw/circle/arrow on the frame.
- Data model: a
review(video ref + version) +comments[]each{timestamp, body, author, drawing?};drawing= vector shapes on a normalized frame canvas. - Reuses: the player + scrubber + streaming backend; comments concept. New: the annotation canvas + versioned review sharing.
- Separate surface, same component + storage foundation.
Dependencies / prerequisites (in order)¶
- Tier 2 (collections/views/blocks) , the substrate. IN PROGRESS.
- Auth/login , so the library is real per-user.
- Files/streaming backend , the gate: range-request streaming of originals + thumbnail serving from NVMe. (Upload matters less here since footage is offloaded, not uploaded; but thumbnails + cover uploads ride the same backend.)
- Media components (player/scrubber, clip grid, import buttons).
- Clips app (compose on the framework + the ingest pipeline + search service).
- Review app (player + annotation canvas + versioned sharing).
Roadmap / phasing¶
- Phase A , New-project button (cheap, standalone): UI over
scaffold-project. Ship anytime. - Phase B , Clips MVP: ingest pipeline (thumbnail + keyframe embeddings + caption) → clips collection → NL search → gallery → Send-to-Premiere / Copy-to-project.
- Phase C , Review app: player + frame scrub + timestamped comments + annotation canvas.
- Phase D (future) , moment/segment search: cluster the already-stored keyframe embeddings
into ranges; add
clip_segments. No re-ingest (per the get-go decision).
Open specifics (decide before build)¶
- Exact local vision/embedding model + host (Finn has no strong GPU; Vector has the RTX 5070); Claude-vision caption budget/cadence.
- Streaming approach (direct range serving vs an on-the-fly transcode/proxy for heavy codecs).
-
Premiere handoff cross-machine path (app on Console/Finn → Premiere MCP on Sol): trigger route.
-
STATUS 2026-06-18 (files backend, step 3 DONE): migration 0035 app.files (RLS) applied; lib/storage.ts (NVMe hash-subdir, traversal guard, ranged reads); app/api/files/upload (multipart->NVMe->row->{id,url}) + app/api/files/[id] streaming (HTTP Range 206/416, currentUser+RLS gated); FileUpload real XHR uploads w/ progress when workspaceId set; prop-types files normalizes refs->stream URLs (Cell stays string[]). tsc --noEmit clean; committed on ct-files (+0035 on forge spine branch).
- STATUS 2026-06-18 (media components, step 4 DONE): components/media/* standalone, props-driven (src/thumbnail/previewSrc/clips), backend-agnostic. VideoPlayer (HTML5
STATUS 2026-06-18 , Clips MVP (Phase B foundation) DONE on branch ct-clips¶
Built the real foundation per this plan, AI deliberately skipped (no embeddings/captions; the vision model is still a deferred decision). Branch ct-clips in worktree forge-suite-wt/ct-clips; 4 commits (+ the migration on the forge spine). npx tsc --noEmit clean for all new code (the only 3 errors are pre-existing creatortrack-logo-white.png module-type errors in marketing/auth files — next-env.d.ts isn't generated because we must NOT run next dev/Turbopack in a worktree; they vanish on a real build).
What shipped
1. Migration 0036_clips.sql (applied via forge_workspace_migrate.py): app.clips (path TEXT UNIQUE = idempotency key, workspace_id, created_by, filename, duration_s, codec, width, height, fps, captured_at, camera, size_bytes, poster_path, tags text[] default '{}', status, created_at) + app.clip_keyframes (workspace_id, clip_id FK ON DELETE CASCADE, t_seconds, image_path, UNIQUE(clip_id,t_seconds)). RLS via core.apply_workspace_rls on both. NO pgvector column , per decision 1+4 (dimension depends on the deferred model); keyframe IMAGES are stored so a future vector(N) migration backfills with no ffmpeg re-pass.
2. Ingest worker scripts/ingest_clips.py (lifeos_admin / trusted ETL): walks the footage tree, ffprobe metadata + ffmpeg poster (640w) + keyframes every ~5s (≤16, 480w) to /mnt/workspace/creatortrack-derivatives/clips/<sha256(abspath)>/, upserts clip + keyframe rows. Idempotent on path; re-runs skip already-ingested clips (poster on disk) unless --force; preserves manual tags on re-ingest. Indexes IN PLACE , never moves/copies originals. No AI.
3. lib/clips.ts (RLS data access + dual on-disk path resolution: originals at root-guarded absolute paths, derivatives traversal-guarded under the deriv root) + routes: GET /api/clips (filename/tag/date filter + tag vocab), PATCH /api/clips/[id] (manual tags), GET|HEAD /api/footage/[id] (Range-streamed ORIGINAL, read-only, auth+RLS-gated, mirrors files/[id]), GET /api/clips/[id]/poster + GET /api/clips/keyframe/[id] (JPEGs).
4. app/(suite)/clips/page.tsx (+ ClipsApp.tsx): composes the existing components/media (ClipGrid posters → open → VideoPlayer streaming via the footage route → Scrubber) + manual tag editor + filename/date/tag filters. Token-only, B&W.
Ingest result (real footage): /mnt/.../2026-06-01_Michael-Moving-Into-Austin/Footage , 65 videos. 60 ingested (status=ready), 396 keyframes, 60 posters (16 MB derivatives), all 60 with codec/res/fps/captured_at/camera. 5 failed , source files A999_* are owned by root mode 0600 (unreadable by our user) → rows written with status='error'. Target workspace_id=12 (Justin's Workspace), created_by=1.
INTEGRATOR / follow-up steps (NOT done here)¶
- Sidebar nav link →
/clips: add tocomponents/workspace/*(the shell, owned by another worker) , intentionally untouched. - 5 permission-blocked originals:
chmod/chown theroot:justinwieb 0600A999_*files to readable, then re-run ingest (idempotent) to flip themerror→ready. Until then they neither index nor stream. - Deferred embedding migration (Phase D groundwork): once a vision model is chosen, add
embedding vector(N)toapp.clip_keyframes+ a backfill job readingimage_pathoff disk , no ffmpeg re-pass (the get-go decision). Then add NL/vector search tolib/clips/GET /api/clipsandapp/(suite)/clips. - Deferred AI ingest: wire local-embedding + selective Claude-vision caption/tag/rename into
ingest_clips.py(hooks are isolated; metadata + frames already extracted). - Actions (Send-to-Premiere / Copy-to-project / New-project): not built; ClipGrid already supports multi-select for a future action bar.
- Ops: re-run ingest on a cron/watch for new footage; env knobs
CREATORTRACK_DERIVATIVES_DIR,CREATORTRACK_FOOTAGE_ROOTS(default/mnt/workspace). next-env.d.ts: generated on first realnext build(don'tnext devin the worktree , Turbopack panics); clears the 3 pre-existing PNG tsc errors.