Skip to content

feat(editor): audio-kind assets + two-column panel UX (timeline video editor v1) - #57

Merged
navidshad merged 42 commits into
mainfrom
feat/editor-audio-assets
Jul 20, 2026
Merged

feat(editor): audio-kind assets + two-column panel UX (timeline video editor v1)#57
navidshad merged 42 commits into
mainfrom
feat/editor-audio-assets

Conversation

@navidshad

@navidshad navidshad commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Timeline Video Editor — audio-kind assets + panel UX

This branch brings the FrameFlow timeline video editor (the CapCut/Premiere-style surface described in video-editor-prd.md) to feature-complete for its v1 scope, closing out the last v1 item — audio-kind assets — and polishing the editor's panel layout. It is a large feature branch (the editor was developed here on top of main); the sections below group the diff by capability, with the most recent work described in most detail.

🎧 Audio-kind assets (the headline — last remaining v1 item)

Imported audio files (local and URL/YouTube) are now first-class:

  • Import & kind detection — audio extensions in the picker; MediaAsset.kind detected on import; metadata probe tolerant of audio-only files.
  • Auto-segmentation — a local, energy-based segment preprocess step (silence split + fixed-interval fallback) so audio — including music with no silence — is divided into contiguous, placeable pieces without needing Gemini. Pure segmentAudio() unit-checked across music/speech/all-silence/tiny inputs.
  • Placement — drag a whole file (or its pieces) straight onto the timeline; kind-matched routing (audio → A-lane, video → V-lane). Fixed a latent bug where the audio lane rejected all drops.
  • Preview — each audio track plays through its own pooled <audio> element, in parallel and in sync with the video's soundtrack. Two follow-up correctness fixes: elements free-run instead of re-seeking every frame (was stuttering to "beats"), and the per-frame .src reset (from media:// URL normalization on readback) that left the timeline silent while Source preview worked.
  • Exportrender.ts slices regions at the union of video and audio boundaries and mixes per region with a k-input amix graph (gain, mutes, gaps, retime preserved); ffmpeg-validated.
  • Gain — per-item audio gain slider in the Inspector.

🖥️ Editor panel UX

  • Right side reworked into two independently-collapsible columns[Chat | Revisions] and [Inspector] — so a selected clip's details and the chat are visible at once. Closing a column frees horizontal space and widens the player; state persists.
  • Collapse control on the right columns matches the Media panel's sidebar-collapse pattern (icon + tooltip), consistent across all panels.
  • Timeline: compact Filmstrip/Context switcher; vertical scroll for the track lanes with a pinned ruler and header column synced to the lane scroll.

🧱 Underlying editor (developed on this branch)

The diff also contains the editor foundation this work builds on: media import + per-asset preprocessing, the purpose-built multi-track timeline (drag/trim/split/ripple/retime/snap/zoom/markers), EDL preview, AI prompt bar + personas, item-id-keyed TimelineDiff, the branchable revision tree, and the segment-then-concat export engine.

Verification

  • npm run build + typecheck clean (remaining vue-tsc errors are pre-existing in node_modules/pilotui and unrelated app files).
  • Driven live in the running Electron app over CDP (Playwright): audio import → kind audio, 30s music → 2 auto-segments, drag-to-A1, parallel in-sync playback (video + A1 currentTime lockstep, 0 stray seeks/reloads), and the panel layout (player width 280→558→816px as columns close).
  • segmentAudio() and the region-slicing logic exercised with unit-style checks.

Notes for reviewers

  • Preview is EDL playback of proxies (audio uses originalPath — no proxy), not a live composite; the final composite happens only at export.
  • With both right columns open the player is intentionally narrow (~580px of columns); closing one gives the width back.
  • No schema migration: MediaKind, gain, muted, preservePitch already existed in @shared/types.

🤖 Generated with Claude Code

navidshad and others added 30 commits July 18, 2026 16:42
Implements the first two milestones of the timeline video editor PRD
(video-editor-prd.md):

M0 — Scaffolding
- Thread.type 'editor' + EditorDocument data model in shared/types.ts
  (MediaAsset, Clip, Track, TimelineItem incl. speed/retime, persona &
  TimelineDiff stubs)
- ThreadManager.createEditorThread (no chat preprocessing auto-start) and
  updateThreadWith queued mutator for race-free concurrent doc writes
- /editor/:id route, Home "Video Editor" card, type-based thread routing
- Full-bleed 4-zone editor shell (media / preview / inspector / timeline)
  with editorStore (ownership-split autosave vs thread-updated echoes)

M1 — Media import + per-asset preprocessing + selectable pieces
- src/main/editor: per-asset orchestrator (proxy -> scenes -> clips ->
  progressive thumbnails), K=3 concurrency cap, per-asset abort, task ids
  namespaced `${assetId}:step`, opt-in Gemini scene descriptions reusing
  extraction.generateSceneDescription via an asset-scoped context
- IPC: create-editor-project, save-editor-doc, add-media-asset,
  import-media-url (per-asset progress), remove-media-asset,
  preprocess-media
- Media panel with live per-step progress, error isolation + retry,
  interrupted-run resume; clip tray with selectable scene pieces;
  preview monitor with clip in/out playback; inspector
- Editor-aware repairThreadPaths; scenedetect threshold param

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements PRD milestone M2 — the editor now edits:

Timeline & manual tools
- Purpose-built multi-track timeline (pxPerSecond model, shared scroll
  container for ruler+lanes, adaptive tick steps, windowed clip mounting)
- Drag pieces from the tray onto tracks (HTML5 DnD) + keyboard placement
- Pointer-gesture state machine (useTimelineInteractions): move with
  snapping (item edges/playhead/markers) and cross-track validation,
  edge-trim with neighbor clamping (Alt = ripple-trim), ruler scrubbing,
  Escape cancels; one history step per gesture
- Split at playhead, delete vs ripple-delete (magnetic default), nudge,
  markers, zoom anchored at cursor (4-400 px/s) + fit-to-window/selection,
  track mute/lock/hide + add-overlay-track
- Retime/speed: constant per-clip speed 0.25-4x with numeric speed AND
  target-duration fields in the inspector; on-timeline duration =
  (out-in)/speed; downstream ripple; hatched visual + speed badge
- Keyboard map (space, arrows, S/Cmd+B, Delete, M, Cmd+Z/Cmd+Shift+Z,
  +/-) guarded against typing contexts; ARIA roles + live seek announcements

EDL preview
- useEdlPlayback: A/B double-buffered <video> pair plays the composed
  timeline — boundary switching with preload, black gap advance on wall
  clock, playbackRate honors retime, item/track mute respected
- PreviewMonitor: Source | Timeline modes with auto-switching
- Filmstrip <-> Context view toggle on timeline clips (scene thumbnail
  vs visual description)

Undo/redo
- src/shared/timeline.ts: pure snapshot diff engine (forward/inverse
  TimelineDiff) + validated apply (schemaVersion, id/range checks,
  speed clamp, duration recompute) — reused by M3 AI diffs later
- src/main/editor/history.ts: sidecar userData/editor-history/{id}.json
  with 50-step ring, sparse keyframes, redo-branch truncation, atomic
  writes; undo pointer persists inside the doc autosave patch so doc
  and pointer stay crash-consistent; history survives restart

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements PRD milestone M3 — the AI co-editor:

Personas
- 9 built-in personas in src/main/constants/personas.ts, grouped
  long-form (Podcast Editor default, Long-Form Polisher, Silence
  Cleaner, Chapter Organizer, Study-Notes; targetDurationSec null =
  length-preserving) and summarize (Concise, Highlight Reel,
  Storyteller, Social Shorts). Built-ins are code, merged at read time,
  never persisted; user personas live globally in settings.json
  (get/set-personas IPC) with builtin-collision filtering
- PersonaPicker popover (grouped, clone/edit/delete) + PersonaEditorModal
  (create/clone/edit, preserve-runtime checkbox, live summary line)

Prompt engine
- Constrained EditorOps schema: the model returns removeItemIds /
  updateItems (whitelisted fields) / addClips (assetId + scene # refs) /
  addMarkers / rationale / optional answer — never raw TimelineDiff;
  opsToDiff() maps server-side (generated item ids, derived durations,
  unknown refs pruned into droppedOps)
- Full context windowing (src/shared/ai-scope.ts + src/main/editor/
  context.ts): selection scope -> chapter/marker window (engages past
  40min/400 items, 150-item cap) -> full; per-asset scene caps with
  60->30->12 degradation ladder; explicit truncation + thin-context
  flags, never silent
- composeSystemInstruction: persona voice + fixed editor contract +
  defaults (null duration -> PRESERVE full runtime branch)
- One structured Gemini call per turn ('editor-edit' model selection),
  streamed via editor-turn-update onto persisted PromptTurn records;
  abortable; usage/cost recorded to thread.usageHistory

Review UX
- Proposals render ghosted on the timeline: adds = dashed sparkle
  clips, removals = struck-through, moves = ghost twins at the new
  position; proposed chapter markers dashed on the ruler
- ProposalCard: rationale, op-count chips, dropped-ops/thin-context/
  truncation notices, usage+cost, Accept-all / Reject
- Double validation (at receipt and at accept) so manual edits during
  review never mis-apply; accept commits ONE ai-origin history step
  (turnId + resultStepId stamped) — a single undo reverts the AI edit
- PromptBar: persona chip, auto-grow input, scope chip with
  Selection/Chapter/Whole override, send/stop, inline error + retry;
  question turns show a transient answer card

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements PRD milestone M4 — the editor now exports real video files,
completing the v1 loop (import -> split -> edit -> export):

Render engine (src/main/editor/render.ts)
- Fast path: single-source, speed-1, unmuted, gapless timelines map to
  TimelineSegment[] (toFixed(3) — String() exponent forms would break
  timeToSeconds) and reuse assembleVideo unchanged
- Region path (PRD option C, segment-then-concat): the sequence slices
  into clip/gap regions; each renders a UNIFORM mp4 intermediate
  (h264 videotoolbox 8M on mac / libx264 crf18 elsewhere + aac 48kHz
  stereo, -video_track_timescale 90000, both A+V streams always
  present) normalized to source-derived WxH/FPS (aspect-preserving
  pad — NOT the blindly-seeded timelineMeta); stitched via concat
  demuxer -c copy +faststart with a one-shot re-encode fallback
- Retime: setpts=(PTS-STARTPTS)/speed + chained atempo (0.5-2.0 per
  stage; asetrate when preservePitch=false); apad + output -t pin A/V
  to exact region duration
- Gaps render black+silence (color/anullsrc lavfi); muted items/tracks
  and audio-less sources get anullsrc so concat stream layout never
  varies; input-side -ss/-t seeking for speed with frame accuracy
- Pre-flight: regions computed + all source files existence-checked
  synchronously — bad exports reject the invoke, no ghost renders
- Duration-weighted progress (regions 0-96, stitch 96-100) over new
  editor-render-progress event; abort kills ffmpeg + cleans workdir;
  renders auto-abort on project delete

Overlap hardening (bug found in verification)
- AI-accept placements weren't gesture-clamped and could overlap items:
  new repairOverlaps() (shared/timeline.ts) pushes later items right
  until sequential; runs on proposal apply AND on project load (heals
  existing docs); export self-repairs by butting instead of throwing;
  left-edge ripple-trim now also clamps against its left neighbor

Export UX
- ExportDialog: quality choice (original / 480p proxy preview) ->
  live progress + cancel -> done (Save As... via save-video, Open
  project folder) / neutral canceled state; closing the dialog does
  NOT abort — render state lives in the store
- Header Export button with live "Exporting N%" label; disabled only
  when the timeline is empty; overlay/text-track notice line

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…al flow

Replaces the M3 ghost-preview + Accept/Reject flow with a branchable
revision tree, mirroring the chat editor's every-generation-is-a-version
paradigm:

Prompt flow rework
- An AI prompt result now APPLIES IMMEDIATELY (validated + overlap-
  repaired) and lands as a new revision branching from the current one;
  the timeline switches to it. AiResultCard shows "Applied - V{n}" with
  rationale/counts/cost/notices and a "Back to V{parent}" jump.
  Rejecting = switching back; the result stays in the tree.
- Unsaved manual work is silently auto-checkpointed BEFORE the AI
  revision is created, so the AI revision's parent snapshot is exactly
  the state the diff applied to and hand edits are never stranded.
- Cmd+Z still undoes an AI diff in place (ring step preserved); the
  fine-grained ring resets only on revision SWITCHES (stale diffs would
  corrupt a switched snapshot via redo) — new clear-editor-history IPC.
- Removed: pendingProposal state, ghost computeds/props/styling in
  TimelineClip/TrackLane/TimelineRuler, ProposalCard, accept/reject.

Revision tree core
- EditorRevision {id, parentId, seq, origin init|ai|manual, label,
  turnId/personaId, FULL snapshot incl. markers} in a new sidecar
  userData/editor-revisions/{threadId}.json (temp-then-rename writes,
  own monotonic revisionCounter so V numbers stay clean and never
  reused, cap 100 with oldest-LEAF-only pruning, root protected).
  Snapshots are self-contained by design — never diff-chained into the
  capped undo ring. Doc carries only currentRevisionId.
- Lazy root bootstrap ("Original") on first checkpoint/prompt — zero
  migration; sidecar loss never mutates the working doc.
- switchRevision: dirty guard (Save & switch / Discard / Cancel),
  snapshot restore incl. markers, ring reset, selection prune, playhead
  clamp. Subtree delete via recursive parent-pointer collect (root
  undeletable; current relocated to the subtree's parent first).
  Switch/delete blocked while a prompt runs (parentage correctness).

UI
- Right rail now tabbed Inspect | Revisions (badge dot when an AI
  revision lands). RevisionsPanel: DFS tree list with depth indent,
  V{n} pills, origin icons (persona emoji / bookmark / flag), first-
  clip thumbnails, relative times, current ring + dirty asterisk,
  hover subtree-delete. SaveRevisionButton with inline label popover
  ("No changes since V{n}" when clean) + timeline-toolbar bookmark.
- RevisionGraphModal: standalone Vue Flow embed (isolated instance) —
  tidy tree layout (main line down, branches fan right, leaf-column
  assignment), revision cards with 3-thumb strips, click-to-switch
  with the modal staying open for branch-hopping, fitView on open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Register @playwright/mcp (.mcp.json) attaching over CDP at :9222 so the
app's Vue UI can be driven and screenshotted for validating changes.
Add `start:debug` script to build and launch Electron with the remote
debugging port open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… editor PRD

Adds the §0 status ledger (M0–M4 + revisions shipped, per-milestone commits,
not-yet-built list), rewrites §5.7 to the apply-immediately revision flow
(§5.7a), and updates milestones/traceability to match what shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pletion)

Phase 1 — transcript → clip.text (PRD §5.2):
- New opt-in 'audio' + 'transcript' preprocess steps reusing the chat
  extraction phases verbatim through the asset-scoped PipelineContext
- Transcript excerpts merged into each Clip.text by [in,out] time overlap,
  preserved across scene re-runs; enriches AI context beyond visual-only
- Inspector asset mode gains a "Transcribe (uses Gemini)" opt-in button;
  the existing clip-mode Transcript block now populates

Phase 2 — silence finder (PRD §5.6, assistive/review-only):
- detectSilence() ffmpeg helper (silencedetect stderr parsing) + shared
  SilenceRegion type + find-silence IPC channel
- SilenceFinder.vue: tunable noise-floor/min-duration thresholds, scan,
  reviewable region list with seek-on-click, explicit ripple-delete apply
- Scan state lives in editorStore keyed by asset, so results stay visible
  across Inspector modes (asset/clip/item) while previewing pieces
- applySilenceRegions carves regions out of placed items per track, clamped
  to item bounds, closes gaps, single undoable history step

Verified end-to-end in the app against a synthetic speech/silence video:
detected regions match ffmpeg ground truth, ripple math exact (27.44s →
17.37s), undo restores across restart, transcription windowed correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ions

PRD §5.2 corrective controls (editor v1 completion, phase 3):
- ClipTray header gains merge-selected / split-selected buttons and a
  collapsible detector panel (threshold slider 5–60 + Re-detect) that
  re-runs detection via the already-wired preprocess-media threshold param
- mergeClips/splitClip main-process mutations (clips are main-owned):
  adjacency-validated merge with joined visual/text, midpoint split with
  min-piece guard; both reindex and clear masterSegmentIndex
- Validation runs before the queued mutator so rejections reach the
  renderer (updateThreadWith swallows mutator throws); the mutator
  re-checks and skips on mid-flight changes
- Inline tray error surfaces rejected operations

Verified live: merge [0,5]+[5,10]→[0,10] with joined transcript, split
back to halves, non-adjacent merge rejected with visible message, re-detect
rebuilds pieces and preserves epsilon-matched transcripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vigation)

PRD §5.3 (editor v1 completion, phase 4):
- TimelineMinimap: always-visible overview strip under the toolbar mapping
  the full sequence — per-track item blocks, marker ticks, playhead line,
  and a visible-window rectangle; click/drag scrolls the main timeline
  (navigation only, never moves the playhead)
- Toolbar gains a Markers & chapters popover: sorted marker list with
  label + timecode, jump-on-click (seek + scroll into view), per-row
  remove, and a press-M empty-state hint

Verified live: minimap window rect tracks zoom/scroll, 90% strip click
jumps scrollLeft proportionally, marker jump lands the playhead at the
marker time, markers persist across autosave.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PRD §5.11 (editor v1 completion, phase 6): a global media query disables
fade/pulse/shimmer animations, hover lifts, press scales, and smooth
scrolling when the OS requests reduced motion; spinners slow instead of
stopping (they convey busy-state), and functional motion (playback, the
playhead, progress widths) is untouched. scroll-behavior needs !important
because a bundled third-party sheet re-declares it after our block.

Verified via emulated reducedMotion in both directions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PRD §5.5 (editor v1 completion, phase 5):
- generateFilmstrip(): ONE ffmpeg pass (fps=1/interval, 120px) replaces the
  process-per-frame extractFrame for strips; interval = max(1s, dur/300) so
  a multi-hour source stays a single bounded run
- New 'filmstrip' preprocess step (reuses the preprocess-media channel and
  the K=3 heavy-slot cap) populating the previously dormant
  MediaAsset.filmstrip; cached per asset under frames/strip/
- editorStore lazily requests strips when filmstrip view is active for
  completed assets placed on the timeline
- TimelineClip tiles one lazy-loaded frame per ~64px slot across the clip's
  source range (zoom-adaptive density, ≤60/clip), falling back to the scene
  thumbnail until the strip lands

Verified live: 27 frames auto-generated on view, clips tile floor(w/64)
frames at every zoom (87px→1, 301px→4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six of seven deferred items shipped and verified (transcript, silence
finder, sensitivity + merge/split, minimap + jump list, dense filmstrip,
reduced-motion); audio-kind assets remain, scoped as its own milestone
with the render-engine region-slicing prerequisite spelled out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TimelineClip's hovering flag was set on pointerdown but never cleared —
no pointerenter/pointerleave existed — so once a clip had ever been
clicked its edge trim handles rendered permanently, visually swallowing
narrow clips. Wire real hover tracking and drop the pointerdown hack.

Verified live: handles absent on untouched clips, present while
selected/hovered, gone after deselect once the pointer leaves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Popover and playhead were both z-30, and the playhead sits later in the
DOM, so it painted through the list. Raise the popover to z-50 (backdrop
z-40), above every in-panel layer (clips z-10/20, snap guide z-20,
playhead z-30).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every long-running step now reports percent or count instead of an
indeterminate spinner:
- scenes: parse PySceneDetect's tqdm 'Progress: N%' from streamed stderr
  (emitted even without a tty) into the task bar
- audio + descriptions: derive percent from the reused phases' status
  strings ('Converting… 45%', 'Analyzing scenes 51 to 100 / 418')
- transcript: honest stage milestones (10 upload/transcribe → 85 merge →
  100) since a single Gemini call has no incremental signal
- filmstrip + silence scan: ffmpeg progress events; silence streams
  'editor-silence-progress' over IPC and the scan button shows 'Scanning… N%'
- progress writes throttled to ≥5% (task) / ≥2% (event) steps since every
  task update persists the thread JSON and broadcasts

Verified live on a 3-minute import: scenes streamed 0→100 in ~10 steps,
audio 0→48→100, transcript 10→85→100, silence events delivered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five UX fixes in one coherent pass:

1. Transcript runs UP FRONT: DEFAULT_STEPS now proxy → audio → transcript
   → scenes → thumbnails, with audio/transcript as soft-fail steps (a
   missing Gemini key or quota error falls back to scene pieces instead of
   bricking the import).
2. Pieces are REAL editorial segments: clips derive from the transcript
   (one piece per spoken statement + [Silence] pieces over gaps, mirroring
   timeline/enrichment.ts), so the tray shows what was said, pre-populated
   with text. Scene detection stays as the fallback and as the explicit
   sensitivity-re-run path; thumbnails now follow clips, not scenes.json.
3. Collapsible rails: Media (left) and Chat/Inspect/Revisions (right)
   collapse to slim vertical strips, persisted in localStorage; the
   monitor takes the reclaimed width.
4. Header overlap fixed: Export teleports into #header-actions-portal so
   it composes with the settings/theme cluster instead of colliding.
5. Prompt box → chat panel: new ChatPanel renders the full persisted turn
   history (user bubbles with scope+time, answers, applied-edit summaries
   with V{n}/op-count/cost chips, error+retry, running indicator) with the
   restacked PromptBar pinned at the bottom of the right rail; chat is the
   default tab with an unseen-activity badge.

Verified live: 3-min speech import produced 55 text-bearing pieces
(36 speech + 19 silence) with per-piece thumbnails; rails collapse/expand
(monitor 552→796px); a question turn round-tripped through the chat panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egion

The chat input stacked card-inside-card (panel border → PromptBar card →
boxed persona/scope chips); flatten it to the modern chat-input shape:
- PromptBar loses its own card; the panel's border-t is the only frame.
  Textarea on top, one quiet meta row below (persona · scope as tiny
  text-level controls, small send circle right)
- PersonaPicker trigger: boxed chip → icon+name text control
- Right-rail tabs: bordered pill container → naked segmented text tabs
- AiResultCard: floating bordered card → flat tinted message block
- AssetRow: border removed; hover tint, ring only when selected

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The scope control now owns the row's flexible space (flex-1 min-w-0) so
its label truncates with an ellipsis before reaching the send button,
which keeps a fixed ml-2 gap instead of ml-auto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editor dropdowns used animate-fade-in-up (fadeInUp 0.5s, translateY 10px),
which reads as a slow bottom-to-up slide. Add a dedicated animate-menu
(menuIn 0.12s: fade + faint 0.97→1 scale) and apply it to the persona
picker, scope menu, save-revision menu, and marker jump list — each with
origin-top/origin-bottom so it grows from its anchor edge. Reduced-motion
neutralizes it alongside the other decorative animations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AI needs pieces/transcript to reason about the footage, so the prompt
input was inert-but-clickable before any media was ready. Add store
getters hasReadyMedia (>=1 asset completed with clips) and mediaProcessing,
disable the textarea + send until hasReadyMedia, and give both the input
placeholder and the chat empty-state three honest states: import media →
preparing (spinner) → ready.

Verified live: input disabled with no media and while preprocessing,
enabled once pieces exist, and a prompt then round-trips to a turn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-row textarea clipped the long guidance placeholders in the
narrow right rail. Shorten all four to one-line hints (the chat
empty-state already carries the full guidance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the Modal fixes (empty controlled-trigger button removed,
dark-theme close button no longer a light box) from the published
release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat: timeline video editor (v1 + completion + UX pass)
…ename

Editor projects were stuck at the hardcoded 'Untitled Project' with no way
to change it. Hybrid fix:

- Auto-name: createMediaAsset sets the project title from the first
  imported asset's filename (extension stripped, underscores → spaces)
  while the title is still the default — atomic in the same queued mutator
  that pushes the media, mirroring how chat threads name from the video.
- Manual rename: GraphHeader gains an opt-in `editable` prop; the editor
  passes it so the title becomes click-to-edit (pencil on hover, Enter
  commits, Esc/blur cancels) → editorStore.renameProject → new
  rename-editor-project IPC → threadManager.updateThread. The shared graph
  header is unchanged when editable is not set. Title is a main-owned
  Thread field, so it persists and echoes via thread-updated.

Verified live: import auto-names 'progress_test.mp4' → 'progress test';
manual rename commits on Enter, cancels on Esc, survives reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 480p proxy re-encoded every source frame with software decode, which
dominates on large/high-fps footage. Two source-fps-aware wins:

- Cap proxy fps at 30 when the source exceeds it (a proxy for scrubbing/
  thumbnails/scene-detection never needs more) — roughly halves encode
  time on 60fps footage (~-49%). Guarded so a <=30fps source is never
  upsampled, which would ADD frames.
- Hardware-accelerated decode (videotoolbox) on Mac for the <=30fps path
  (~-25%), with a one-shot software-decode fallback so a hw-decode-hostile
  input still produces a proxy instead of erroring. Not combined with the
  fps cap — benchmarks show the per-frame GPU→CPU download makes it slower
  once frames are already being dropped.

Audio and output size are unchanged; toLowResolution's new opts arg is
optional so the chat pipeline path is unaffected (and gains hw decode).

Benchmarked on a 2-min 1080p60 source: 37.5s → 19s encode; verified in-app
end to end (60fps → 480p/30fps; 25fps+audio → 480p/25fps, audio preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Importing a local file ran fs.copyFileSync of the whole source into
tempDir/media/<id>/source/. For a 10–15 GB file that synchronous copy
blocks Electron's main process for minutes, freezing the UI and the file
dialog — even though the editor only needs the path.

Local imports now reference the user's file where it lives (no copy):
originalPath = the picked path. Everything downstream already works with an
out-of-tempDir path — proxy/thumbnails/export read originalPath directly,
media:// serves any absolute path, removeAsset only clears the asset dir
(never the user's file), and path-repair leaves non-tempDir paths alone.
URL/YouTube imports are unchanged (they download straight into the asset
dir, so the copy branch is a no-op for them).

Verified: add-media-asset returns in ~200ms instead of blocking on the
copy; originalPath references the source; preprocessing still builds the
proxy in tempDir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-asset way to delete derived Gemini data. clearAssetData
(main) deletes the backing files (the whole per-asset transcripts dir for
transcript; scene_descriptions.json for descriptions — scenes.json kept),
clears the preprocessing path fields and the derived clip field (text /
visual) on every piece, and drops the step's namespaced task. The pieces
(segmentation) are preserved; audio is kept so a later re-transcribe skips
re-extraction.

Wired through a clear-asset-data IPC + editorStore.clearAssetData (with a
token-cost confirmation). The Inspector's 'Transcript ready' / 'Scene
descriptions ready' states each gain a Remove link; removing reverts to the
Transcribe / Describe button so it can be regenerated.

Verified live: 56 text-bearing pieces → 0 on remove, files deleted from
disk, task dropped, pieces intact, and the Inspector toggles ready ↔ re-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (stage a)

Lets audio files become first-class MediaAssets on the A1 lane:
- probe-based kind detection (video/audio) on local + URL import;
  audio-safe ffprobe (getAudioMetadata) that tolerates no video stream
- kind-aware preprocessing: audio runs audio+transcript only, with a
  whole-file clip fallback when no transcript (no Gemini key)
- audio icons/copy across MediaPanel/AssetRow/ClipTray/ClipTile/TimelineClip;
  scene-detector controls hidden for audio
- preview: audio-track items play through a slaved <audio> element with a
  blank "audio only" monitor state (useEdlPlayback + audioSegments)
- per-item gain slider in the inspector (undoable via editorStore.setItemGain)

Export mixing of audio-track items lands in stage b.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
navidshad and others added 12 commits July 19, 2026 18:19
…ge b)

Refactors the segment-then-concat renderer to handle audio-track items:
- computeRegions now slices at the UNION of video AND audio item boundaries
  and sub-slices each covering item onto the region (retime-aware mapping)
- unified Region model: optional video slice (or black) + N audio sources
- renderRegion builds a k>1 amix graph (normalize=0, per-item gain), with
  every stream apad+atrim'd to exact region length so A/V stays concat-aligned;
  0 sources → anullsrc silence, 1 → the stream, k>1 → amix
- audio can extend past video into black regions; audio-only timelines export
- fast path (assembleVideo) now skipped whenever any audio-track item exists

Filtergraph shapes validated end-to-end against ffmpeg (2-stream amix,
black+silence, retimed audio → concat → 1 video + 1 stereo audio).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lback)

Audio no longer lands as a single whole-file clip. A new local 'segment'
preprocess step (AUDIO_STEPS=['segment']) runs silencedetect and divides the
file into CONTIGUOUS pieces at silence midpoints, subdividing long runs into
~20s chunks — so music (no silence) still splits into even chunks and speech
splits at pauses. No Gemini needed; transcription stays opt-in. segmentAudio()
is pure and unit-checked (music/speech/all-silence/tiny → full coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AssetRow is now draggable → drops the WHOLE asset as one full-span item
  (itemFromAsset + store.addItemFromAsset), kind-matched to its lane
- TrackLane accepted only video lanes before, so audio pieces could never be
  dropped on A1 at all; it now accepts video AND audio lanes and validates the
  dragged kind via a readable kind-hint DnD type (clip or asset payload)
- addItemFromClip defaults the target lane by kind, so Enter-to-place and
  drag both route audio pieces to the audio lane instead of the video lane

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t pool

A single shared <audio> element could not sound multiple audio tracks at once
(and didn't reliably play alongside the video). Replace it with a pool of
<audio> elements — one per audio track, created on demand and slaved to the
playhead — so every audio track plays in parallel with the video's own
soundtrack, mirroring the multi-source amix that export produces.

- audioSegments now carries trackId; useEdlPlayback groups by track and drives
  one element each (src/seek/volume/play/pause), pausing tracks with no covering
  segment; pool torn down on unmount
- PreviewMonitor no longer owns the <audio> element (managed in the composable)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create per-track preview audio via document.createElement + appendChild
(hidden) instead of a detached new Audio() — Chromium plays in-document
media more reliably; removed on unmount. Verified live: with a video on V1
and music on A1, both the video element and the A1 audio element play in
parallel and in sync (currentTime lockstep within 0.01s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rame

The per-track audio elements were slaved to the video-derived playhead and
re-seeked whenever drift exceeded 0.15s — which happened continuously for real
media, stalling the decoder so only fragments ("beats") leaked through while
the video played smoothly. Now each element hard-seeks ONLY on segment (re)entry,
an explicit scrub/seek, or large drift (>0.5s); a plain playhead advance during
playback no longer touches currentTime.

Verified live: over a 3s play window the A1 music element fired 1 seeking event
(the initial anchor) and 0 stalls, advancing 2.9s — smooth continuous playback
in parallel with the video, vs. the prior per-frame re-seek stutter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e sound)

syncAudio compared el.src (read back) against seg.src to decide whether to
(re)load. But reading el.src back returns a normalized/encoded URL
(media:///Users → media://users, %20 for spaces), so the comparison was ALWAYS
true — the element's .src was reset every frame, firing hundreds of
emptied/loadstart reloads per second and never reaching a playable readyState.
Result: audio played in Source preview (Vue binds :src once) but was silent on
the timeline. Track the intended src on el.dataset.edlSrc and set .src only when
that raw value changes.

Verified live with a spaced filename ("Dast man nist - shadmehr.mp3"), V1 muted:
emptied/loadstart 409→0, readyState 1→4, plays continuously.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…racks

- Shrink the Filmstrip/Context view toggle (py-1→py-0.5, smaller icons) so it
  matches the Source/Timeline switcher instead of towering over the toolbar.
- The track lanes had overflow-y-hidden, so V1/A1/OV clipped once they exceeded
  the panel height. Enable vertical scroll on the lanes, pin the ruler
  (sticky top), and mirror the scroll onto the header column via transform so
  headers stay aligned with their lanes.

Verified live: 7 tracks → scrollHeight 408 > clientHeight 137, scrollable, and
after scrollTop=60 the header column transform is translateY(-60px) (synced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the mutually-exclusive Chat/Inspect/Revisions tabs (which hid a
selected clip's details while chatting) with a split rail: the top pane toggles
Inspect | Revisions, and Chat is always visible in the bottom pane. Selecting a
clip while browsing Revisions flips the top pane to the Inspector; Chat stays
put. (Supersedes the reverted separate-window approach.)

Verified live: with a clip selected, the Inspector props (Track/Start/Source In)
and the Chat input render simultaneously.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the right rail into two closeable rows: [Chat | Revisions] on top and
[Inspector] below, each with its own close (✕) and a reopen strip. Closing a
single row gives its space to the other; closing BOTH narrows the rail to a slim
icon strip so the preview player widens. Row open/closed state persists.
Selecting a clip auto-opens the Inspector row.

Verified live: player column 532px (both open) → 828px (both closed) → 532px on
reopen; closing one row leaves width unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per feedback, [Chat | Revisions] and [Inspector] now sit side by side as two
independently-collapsible COLUMNS (was stacked rows). Each has its own close
(✕) and collapses to a slim vertical strip (reused CollapsedRail) to reopen.
Closing a column frees its horizontal space so the preview player widens
directly; column open/closed state persists.

Verified live: player column 280px (both open) → 558px (Chat closed) → 816px
(both closed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Chat/Inspector columns used an ✕ to close while the Media panel uses a
sidebar-collapse icon. Swap the ✕ for the mirror tabler--layout-sidebar-right-collapse
(same size + "Collapse panel" tooltip) so the collapse/expand pattern is
consistent across all three panels (the reopen strips already use the matching
sidebar-right-expand).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@navidshad
navidshad merged commit 911a0ad into main Jul 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant