Skip to content

Studio: native macOS EDL editor for video-use - #138

Open
gregpr07 wants to merge 18 commits into
mainfrom
studio-mac
Open

Studio: native macOS EDL editor for video-use#138
gregpr07 wants to merge 18 commits into
mainfrom
studio-mac

Conversation

@gregpr07

@gregpr07 gregpr07 commented Aug 26, 2026

Copy link
Copy Markdown
Member

Brings over the Studio work that has been sitting on a local branch since early July: a native macOS app for editing video-use projects, plus the render/skill fixes that landed alongside it.

What's here

Studio (studio-mac/) — SwiftUI + AVFoundation app that edits an EDL against the real project directory:

  • Single AVMutableComposition for hardware-decoded, gapless virtual-cut playback (including 4K, mixed-resolution and portrait sources)
  • Timeline with zoom/pan, playback-follow inspector, files pane
  • Live subtitles baked into the preview, so position and style are edited against what actually renders
  • Redesigned export flow with style-edit safety
  • Localhost remote control so an agent can drive the editor
  • EDL file-watching with a poll fallback for non-APFS volumes

A Tauri 2 version came first (1e50d2a) and was dropped in favour of the native app (fad882d).

Render / CLI

  • Honor edl.subtitle_style (size, margin, uppercase, chunk words)
  • Decode .webm overlays with libvpx-vp9 to preserve alpha
  • Strip literal quote marks from caption text
  • Contain EDL overlay/subtitle paths to the project directory
  • Skip AppleDouble ._* sidecars in video/transcript discovery
  • Package as an installable video-use CLI tool
  • edit_log.jsonl contract for human edits made outside the conversation
  • CI matrix build with doctor, render smoke test, and SKILL.md sync check

Notes

  • The branch was rewritten before pushing to strip studio-mac/.build/ (~150MB of compiled binaries, dSYMs and module caches) that had been committed by accident. .gitignore now covers it.
  • swift build -c release succeeds; warnings only (Swift 6 Sendable capture warnings in Store.swift).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KpkMHZyCr5tkpFVcQjYBJx


Summary by cubic

Brings in Studio, a native macOS EDL editor for video-use, and packages the project as an installable video-use CLI (0.2.0). Studio replaces an earlier Tauri prototype — AVFoundation playback won on 4K footage. The helpers/ scripts become back-compat shims over the packaged modules, and install docs are rewritten around uv tool install video-use.

New Features

  • studio-mac/ adds a SwiftUI + AVFoundation app that plays an EDL as one gapless hardware-decoded composition against the real project directory.
  • Timeline with zoom/pan, playback-follow inspector, source-files pane, and live subtitles baked into the preview.
  • Localhost control server on port 4860 lets an agent drive the editor; EDL edits and external changes reload live, with a poll fallback for non-APFS volumes.
  • Export flow with staged progress and style-edit safety; unknown EDL fields round-trip through saves.
  • edit_log.jsonl records human edits made outside the conversation.
  • CI matrix (ubuntu/macos/windows) runs doctor, subcommand help, render smoke test, and a SKILL.md sync check.

Bug Fixes

  • Render now honors edl.subtitle_style (size, margin, uppercase, chunk words).
  • .webm overlays decode with libvpx-vp9 so alpha is preserved.
  • Literal quote marks are stripped from caption text.
  • EDL overlay/subtitle paths are contained to the project directory (--unsafe-paths overrides).
  • AppleDouble ._* sidecars are skipped in video/transcript discovery.

Written for commit 56f64b3. Summary will update on new commits.

Review in cubic

gregpr07 and others added 18 commits July 2, 2026 17:44
- src/ layout package with console entry point (video-use transcribe|pack|timeline|grade|render)
- video-use skill / where / key / doctor for agent-driven install
- SKILL.md + manim-video sub-skill bundled as package data
- drop unused librosa + matplotlib hard deps (classic install failure)
- API key resolution: env -> ./.env -> ~/.config/video-use/.env -> repo .env
- UTF-8 stdout forcing for Windows consoles
- helpers/*.py kept as back-compat shims for clone+symlink installs
- install.md rewritten as uv tool install fast path; uv.lock added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shared or tampered edl.json could point the subtitles filter or an
overlay input at any file on disk (CWE-22, reported in #93) — the
subtitles filter burns file contents into the rendered frames. Overlay
and subtitle paths must now resolve inside the project directory;
--unsafe-paths overrides. Source paths are exempt: footage legitimately
lives anywhere, and the documented EDL format uses absolute source paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… conversation

Studio (or any EDL-writing tool) appends structured edits to
<edit>/edit_log.jsonl; agents read it before proposing changes, treat
human edits as ground truth for taste, infer preferences from patterns,
and persist inferences to project.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ubuntu/macos/windows: install ffmpeg, pip install, video-use doctor,
all subcommand help screens, synthetic-clip EDL render with duration
assertion, timeline view, and a path-containment regression. Separate
job diffs root SKILL.md against the packaged copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Screen Studio-style dark UI over the video-use file contract:
- output-time timeline with beat-labeled slice blocks, overlay + subtitle
  tracks; edge-trim snapping to transcript word boundaries with a
  word/time tooltip; block reorder, delete, undo/redo
- virtual-cut preview: per-source <video> elements driven by an output
  clock — review any cut with zero renders
- slice-editor inspector surfacing the agent's beat/quote/reason per cut
- every human edit atomically rewrites edl.json and appends
  edit_log.jsonl for the agent to learn taste from (see SKILL.md)
- watches edl.json: agent rewrites reload live with a sync pulse
- Export runs `video-use render` with streamed log output
- zero LLM calls in the app; the agent is the only intelligence

Verified: cargo check clean; npm run build clean; interactions
(select, word-snap trim, reorder, delete, undo, playback) exercised
in browser mode via CDP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolve relative EDL source paths against the videos dir; photo stills via <img>
- control server on 127.0.0.1:4859: GET /state, POST /cmd (open/play/pause/
  toggle/seek/select/undo/redo/reload/export) forwarded to the webview
- studio <edl.json> CLI arg opens at launch
- fix strict-mode double listener registration (async cleanup race)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The output clock dispatched a React state update every animation frame,
re-rendering the whole tree at 60fps (x2 under StrictMode) while the
video decoded — playback stuttered. React now ticks at ~8fps (the video
element is its own clock between ticks), the playhead glides via a
linear CSS transition, drift threshold raised above tick granularity so
coarse ticks never trigger mid-segment seeks, StrictMode dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On exFAT/NTFS volumes macOS writes ._* resource forks next to every
file; transcribe-batch treated them as videos and pack crashed decoding
them as JSON. Found on the first real external-SSD shoot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Requesting a frame at t == duration (the SKILL-prescribed last-2s
self-eval window) crashed the whole composite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EDL plays as one gapless hardware-decoded AVMutableComposition (4K
verified). Same file contract as the Tauri app (edl.json atomic writes,
edit_log.jsonl, transcripts, external-change watch) and same remote-
control protocol on port 4860. Unknown EDL fields round-trip through
saves via a JSONValue catch-all. SPM + Makefile, no Xcode project.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AVFoundation playback won decisively on 4K footage; DESIGN.md moves to
studio-mac/ with the remote-control port updated to 4860.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k words)

Studio writes {"enabled","size","margin_v","uppercase","chunk_words"}
into the EDL; render builds the SRT chunking and ASS force_style from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inspector, files pane

- caption overlay generated live from transcripts + ranges (render.py
  build_master_srt parity), style editable in the inspector and persisted
  as edl subtitle_style (+ edit_log op)
- timeline fits duration by default; pinch zoom, hover-edge panning,
  playhead auto-follow
- inspector auto-tracks the slice under the playhead
- files pane: every source file with amber strips showing exactly which
  source ranges the cut keeps; click-to-jump

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- export sheet: staged progress card (parsed from render stdout), log
  behind a disclosure, success card with size/duration/Reveal in Finder,
  prominent failure state; exports always pass --build-subtitles so
  trimmed cuts never ship stale captions
- subtitle_style edits go through undo/redo; window-activation clicks
  within a 350ms grace window no longer commit accidental style changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ffmpeg's native VP9 decoder silently drops the alpha plane, rendering
transparent overlays as black (community-reported in #59).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kqueue vnode events miss in-place rewrites on exFAT externals; a 1s
mtime+size poll now backs the DispatchSource (same reload path,
content-deduped), which still re-arms on delete/rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scribe transcribes reported speech with embedded double quotes; they
read as noise in burned captions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deprecated synchronous asset.tracks() returned [] for 4K sources that
hadn't finished loading off a slow/external drive, which silently dropped
segments and left gaps in the video-composition instructions — freezing
playback on multi-source cuts. Preload duration/tracks/geometry with
async load() before inserting, and always tile the composition so a
segment with no video still gets a black instruction covering its range.

Also gitignore studio-mac/.build/ so compiled binaries, dSYMs and module
caches stay out of the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KpkMHZyCr5tkpFVcQjYBJx
Comment thread src/video_use/run.py
nonlocal ok
ok = ok and passed
mark = "OK " if passed else "FAIL"
print(f" {mark} {label}" + (f" — {detail}" if detail else ""))

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

38 issues found across 61 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="studio-mac/Sources/Studio/ContentView.swift">

<violation number="1" location="studio-mac/Sources/Studio/ContentView.swift:85">
P3: `transport` is never composed; `canvas` renders `transportPill` instead, leaving this duplicate playback UI unreachable. Remove the unused property to prevent dead UI code from drifting.</violation>

<violation number="2" location="studio-mac/Sources/Studio/ContentView.swift:149">
P2: Option-clicking this title-bar button still renders `final.mp4`, so the advertised preview shortcut cannot work. Read the Option modifier in this action or provide a separate preview control.</violation>
</file>

<file name="install.md">

<violation number="1" location="install.md:67">
P3: The sanity-check curl uses `$KEY`, but no `$KEY` variable is defined after the key is stored via `video-use key <PASTED_KEY>`. The shell expands it to empty, sending `xi-api-key: `, which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. `$(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)`) or reference the pasted key directly instead of `$KEY`.</violation>
</file>

<file name="studio-mac/Sources/Studio/ZoomCamera.swift">

<violation number="1" location="studio-mac/Sources/Studio/ZoomCamera.swift:5">
P1: When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass `zoomRegions` into the render pipeline so export uses the same camera regions as the preview.</violation>
</file>

<file name="studio-mac/Sources/Studio/InspectorView.swift">

<violation number="1" location="studio-mac/Sources/Studio/InspectorView.swift:15">
P1: When `store.selection` is non-nil, `InspectorView.body` still renders `projectPanel`; `sliceEditor` has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.</violation>

<violation number="2" location="studio-mac/Sources/Studio/InspectorView.swift:80">
P3: When an EDL contains `subtitles`, the default project panel hides that path despite the documented inspector contract. Add a read-only row for `store.edl.subtitles`.</violation>
</file>

<file name="studio-mac/Sources/Studio/Models.swift">

<violation number="1" location="studio-mac/Sources/Studio/Models.swift:142">
P2: When `subtitle_style` contains an unmodeled key, `SubtitleStyle` drops it during decode and re-encoding, so any subsequent Studio edit silently loses that EDL metadata. Add a dynamic-key `extra` map to `SubtitleStyle`, matching `Range`, `Overlay`, and `Edl`.</violation>
</file>

<file name="studio-mac/Sources/Studio/Subtitles.swift">

<violation number="1" location="studio-mac/Sources/Studio/Subtitles.swift:29">
P2: When a transcript entry omits `type`, Studio displays it but `render.py` drops it. Use the renderer's strict `type == "word"` check here so the live captions and export stay identical.</violation>

<violation number="2" location="studio-mac/Sources/Studio/Subtitles.swift:92">
P2: When an EDL lacks `subtitle_style`, the preview uses margin 35 but export falls back to margin 90. Make the Swift default and renderer fallback consistent so captions do not move between preview and final output.</violation>

<violation number="3" location="studio-mac/Sources/Studio/Subtitles.swift:110">
P2: The preview uses the system font while export uses Helvetica, so the caption style being edited is not the style that renders. Use Helvetica with a bold weight in the preview to match the renderer.</violation>
</file>

<file name="studio-mac/Sources/Studio/StageView.swift">

<violation number="1" location="studio-mac/Sources/Studio/StageView.swift:76">
P2: During a virtual-camera zoom, this later overlay remains at the unzoomed size and position while the video moves. Scale and clip the subtitle overlay with the hero, or move the scale after all hero overlays.</violation>
</file>

<file name="studio-mac/Sources/Studio/StudioApp.swift">

<violation number="1" location="studio-mac/Sources/Studio/StudioApp.swift:47">
P3: When ⌘Space is pressed before an EDL is loaded, this command sets `playing` to true despite there being no player item. Disable Play until `store.edlPath` is non-nil.</violation>
</file>

<file name="src/video_use/grade.py">

<violation number="1" location="src/video_use/grade.py:109">
P2: On Windows, auto-analysis fails because the temporary `C:\...` path is unescaped inside the FFmpeg filter expression. Convert it to forward slashes and escape the drive-letter colon before constructing `metadata=print`.</violation>
</file>

<file name="studio-mac/Sources/Studio/FilesPane.swift">

<violation number="1" location="studio-mac/Sources/Studio/FilesPane.swift:89">
P3: When an external `edl.json` range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to `[0, dur]` before computing `x` and `bw`.</violation>
</file>

<file name="studio-mac/Sources/Studio/TimelineView.swift">

<violation number="1" location="studio-mac/Sources/Studio/TimelineView.swift:38">
P2: Projects with overlays have no overlay lane: this stack renders only the ruler, cut blocks, and captions, so overlay timing is invisible in the editor. Add an overlay track from `store.edl.overlays`.</violation>

<violation number="2" location="studio-mac/Sources/Studio/TimelineView.swift:59">
P2: When zoomed in, holding the pointer near an edge only seeks once; this handler schedules no repeated pan or edge-depth speed. Add a timer-driven `panOffset` update for edge hover, separate from hover scrubbing.</violation>
</file>

<file name="src/video_use/transcribe_batch.py">

<violation number="1" location="src/video_use/transcribe_batch.py:34">
P2: Mixed-case extensions such as `take.Mp4` and `take.M4V` are silently omitted from batch transcription. Normalize `p.suffix` before checking `VIDEO_EXTS`.</violation>

<violation number="2" location="src/video_use/transcribe_batch.py:48">
P2: With `--workers 0` or a negative value, `ThreadPoolExecutor` raises `ValueError` after setup instead of showing a CLI validation error. Require a positive count during argument parsing.</violation>
</file>

<file name="src/video_use/run.py">

<violation number="1" location="src/video_use/run.py:88">
P1: When users follow the documented `video-use key <PASTED_KEY>` flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing `getpass` prompt instead of accepting positional secrets.</violation>

<violation number="2" location="src/video_use/run.py:103">
P1: When the existing config uses whitespace around `=`, `cmd_key` leaves the old entry before appending the new one. Parse the variable name before `=` when filtering so `video-use key` actually replaces valid existing entries.</violation>

<violation number="3" location="src/video_use/run.py:128">
P2: When a PATH executable cannot run `-version` or returns nonzero, `doctor` still reports it `OK` because this branch passes literal `True`. Check the subprocess return code and mark execution failures as `FAIL`.</violation>
</file>

<file name="src/video_use/render.py">

<violation number="1" location="src/video_use/render.py:32">
P1: When the installed `video-use render` command uses a named preset, this import falls back and silently disables the grade. Import the sibling module relatively so presets and auto-grading use the real implementation.</violation>

<violation number="2" location="src/video_use/render.py:214">
P1: When an EDL mixes portrait and landscape or different aspect ratios, the extracted clips have different dimensions before the copy-concat step, causing concat failure or a mis-sized output. Normalize every segment to one canvas size before copy-concat, or use a filtered concat.</violation>

<violation number="3" location="src/video_use/render.py:280">
P1: When Studio saves a conventional EDL with a relative source path, render resolves it in the wrong directory and ffmpeg aborts before extraction. Resolve `edit/` projects against `edit_dir.parent`, with the existing edit-directory fallback.</violation>

<violation number="4" location="src/video_use/render.py:608">
P2: When a source has no audio and compositing is needed, this mandatory map makes ffmpeg fail instead of rendering the video. Make the audio map optional with `0:a?` so audio is copied when present.</violation>
</file>

<file name="studio-mac/Sources/Studio/Store.swift">

<violation number="1" location="studio-mac/Sources/Studio/Store.swift:112">
P1: When two opens overlap on a slow drive, an older load can overwrite the newer project, watcher, and source data. Cancel the previous load or discard completions whose load generation is no longer current.</violation>

<violation number="2" location="studio-mac/Sources/Studio/Store.swift:121">
P0: When a newly opened or externally reloaded EDL has more ranges than the current composition, `stateJSON()` and the timeline index a stale `offsets` array and crash. Publish matching prefix sums before exposing the new `edl`, or make the loading state safe in both loading paths.</violation>

<violation number="3" location="studio-mac/Sources/Studio/Store.swift:155">
P1: When an agent changes the EDL’s `sources` mapping, `externalChange` keeps the old paths and transcripts while rebuilding the new ranges. Reload the complete `LoadedProject` metadata atomically instead of decoding only `Edl`.</violation>

<violation number="4" location="studio-mac/Sources/Studio/Store.swift:275">
P2: The camera-region editing methods are unreachable, so users cannot create the push-ins represented by `zoomRegions`. Wire these actions into UI or remote commands, or remove the unfinished state until the feature is usable.</violation>

<violation number="5" location="studio-mac/Sources/Studio/Store.swift:418">
P2: When a source duration is still unavailable, `setOut` allows an end time beyond the media and persists an invalid cut. Reject trimming until the duration is known or obtain and apply the asset duration before committing.</violation>
</file>

<file name="helpers/grade.py">

<violation number="1" location="helpers/grade.py:12">
P2: Code importing the legacy `helpers.grade` module can no longer access `get_preset` or `auto_grade_for_clip`. Re-export the prior public helpers from `video_use.grade` so this back-compat shim preserves the documented import contract.</violation>
</file>

<file name="src/video_use/paths.py">

<violation number="1" location="src/video_use/paths.py:18">
P2: When any candidate `.env` contains non-UTF-8 bytes, `resolve_api_key()` raises `UnicodeDecodeError` instead of treating that candidate as unreadable and continuing to the next source. Catch `UnicodeError` alongside `OSError` so `doctor` and transcription still resolve a valid fallback key.</violation>
</file>

<file name="src/video_use/transcribe.py">

<violation number="1" location="src/video_use/transcribe.py:99">
P1: When two source files share a stem, this maps both to one transcript; batch can transcribe them concurrently and the last write wins, so one file's timestamps are used for the other. Use a collision-free cache key and align render lookup with it.</violation>

<violation number="2" location="src/video_use/transcribe.py:101">
P1: When a source is edited or replaced, this existence-only check returns the old transcript, so word boundaries and captions describe different media. Persist and compare a source fingerprint before honoring the cache.</violation>
</file>

<file name="studio-mac/Sources/Studio/Composition.swift">

<violation number="1" location="studio-mac/Sources/Studio/Composition.swift:129">
P2: When a source is 60 fps or higher, this fixed 30 fps composition drops frames during preview, so playback is no longer frame-accurate and motion appears less smooth. Derive the composition frame duration from the loaded source frame rates, or otherwise preserve the source cadence instead of hard-coding 30 fps.</violation>
</file>

<file name="src/video_use/pack_transcripts.py">

<violation number="1" location="src/video_use/pack_transcripts.py:115">
P2: When a phrase starts with an audio event or token without `speaker_id`, `current_speaker` stays `None` after the first diarized word. Later speaker changes therefore never flush the phrase, merging speakers; update `current_speaker` when it is unknown and a later token supplies an ID.</violation>
</file>

<file name="studio-mac/DESIGN.md">

<violation number="1" location="studio-mac/DESIGN.md:94">
P3: The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:28">
P2: Following 'Install from source' for a fresh user fails at `video-use skill > ~/.claude/skills/video-use/SKILL.md` because the target directory doesn't exist and the shell `>` redirect errors out. Add `mkdir -p ~/.claude/skills/video-use` before the redirect, matching the 'Install' section.</violation>
</file>

Re-trigger cubic

guard let self else { return }
self.edlPath = loaded.edlPath
self.dir = loaded.dir
self.edl = loaded.edl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: When a newly opened or externally reloaded EDL has more ranges than the current composition, stateJSON() and the timeline index a stale offsets array and crash. Publish matching prefix sums before exposing the new edl, or make the loading state safe in both loading paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/Store.swift, line 121:

<comment>When a newly opened or externally reloaded EDL has more ranges than the current composition, `stateJSON()` and the timeline index a stale `offsets` array and crash. Publish matching prefix sums before exposing the new `edl`, or make the loading state safe in both loading paths.</comment>

<file context>
@@ -0,0 +1,612 @@
+                guard let self else { return }
+                self.edlPath = loaded.edlPath
+                self.dir = loaded.dir
+                self.edl = loaded.edl
+                self.sourcePaths = loaded.sourcePaths
+                self.transcripts = loaded.transcripts
</file context>


// Screen Studio-style virtual camera: during a zoom region the preview eases into a focus point
// and holds, then eases back out. Pure function of the playhead so it's correct while playing and
// while scrubbing. This drives the live preview; the same regions are what an export would burn in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass zoomRegions into the render pipeline so export uses the same camera regions as the preview.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/ZoomCamera.swift, line 5:

<comment>When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass `zoomRegions` into the render pipeline so export uses the same camera regions as the preview.</comment>

<file context>
@@ -0,0 +1,44 @@
+
+// Screen Studio-style virtual camera: during a zoom region the preview eases into a focus point
+// and holds, then eases back out. Pure function of the playhead so it's correct while playing and
+// while scrubbing. This drives the live preview; the same regions are what an export would burn in.
+struct ZoomRegion: Equatable {
+    var start: Double        // output time
</file context>

Divider().background(Theme.border)
subtitleControls // the main thing you actually tune here
Divider().background(Theme.border)
projectPanel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When store.selection is non-nil, InspectorView.body still renders projectPanel; sliceEditor has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/InspectorView.swift, line 15:

<comment>When `store.selection` is non-nil, `InspectorView.body` still renders `projectPanel`; `sliceEditor` has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.</comment>

<file context>
@@ -0,0 +1,271 @@
+                Divider().background(Theme.border)
+                subtitleControls     // the main thing you actually tune here
+                Divider().background(Theme.border)
+                projectPanel
+            }
+            .padding(16)
</file context>

Comment thread src/video_use/run.py

def cmd_key(argv: list[str]) -> int:
if argv and argv[0] not in ("-", "--stdin"):
key = argv[0].strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When users follow the documented video-use key <PASTED_KEY> flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing getpass prompt instead of accepting positional secrets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/video_use/run.py, line 88:

<comment>When users follow the documented `video-use key <PASTED_KEY>` flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing `getpass` prompt instead of accepting positional secrets.</comment>

<file context>
@@ -0,0 +1,194 @@
+
+def cmd_key(argv: list[str]) -> int:
+    if argv and argv[0] not in ("-", "--stdin"):
+        key = argv[0].strip()
+    elif not sys.stdin.isatty():
+        key = sys.stdin.readline().strip()
</file context>

Comment thread src/video_use/run.py
if env_path.exists():
lines = [
l for l in env_path.read_text().splitlines()
if not l.strip().startswith("ELEVENLABS_API_KEY=")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the existing config uses whitespace around =, cmd_key leaves the old entry before appending the new one. Parse the variable name before = when filtering so video-use key actually replaces valid existing entries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/video_use/run.py, line 103:

<comment>When the existing config uses whitespace around `=`, `cmd_key` leaves the old entry before appending the new one. Parse the variable name before `=` when filtering so `video-use key` actually replaces valid existing entries.</comment>

<file context>
@@ -0,0 +1,194 @@
+    if env_path.exists():
+        lines = [
+            l for l in env_path.read_text().splitlines()
+            if not l.strip().startswith("ELEVENLABS_API_KEY=")
+        ]
+    lines.append(f"ELEVENLABS_API_KEY={key}")
</file context>
Suggested change
if not l.strip().startswith("ELEVENLABS_API_KEY=")
if not ("=" in l and l.split("=", 1)[0].strip() == "ELEVENLABS_API_KEY")

Comment thread install.md
```bash
python ~/Developer/video-use/helpers/timeline_view.py --help >/dev/null && echo "helpers OK"
ffprobe -version | head -1
curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $KEY" https://api.elevenlabs.io/v1/user

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The sanity-check curl uses $KEY, but no $KEY variable is defined after the key is stored via video-use key <PASTED_KEY>. The shell expands it to empty, sending xi-api-key: , which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. $(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)) or reference the pasted key directly instead of $KEY.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.md, line 67:

<comment>The sanity-check curl uses `$KEY`, but no `$KEY` variable is defined after the key is stored via `video-use key <PASTED_KEY>`. The shell expands it to empty, sending `xi-api-key: `, which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. `$(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)`) or reference the pasted key directly instead of `$KEY`.</comment>

<file context>
@@ -5,158 +5,90 @@ description: Install video-use into the current agent (Claude Code, Codex, Herme
 ```bash
-python ~/Developer/video-use/helpers/timeline_view.py --help >/dev/null && echo "helpers OK"
-ffprobe -version | head -1
+curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $KEY" https://api.elevenlabs.io/v1/user

</file context>


</details>

```suggestion
curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)" https://api.elevenlabs.io/v1/user

Divider().background(Theme.border)

sectionHeader("PROJECT")
infoRow("Grade", store.edl.grade ?? "none")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When an EDL contains subtitles, the default project panel hides that path despite the documented inspector contract. Add a read-only row for store.edl.subtitles.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/InspectorView.swift, line 80:

<comment>When an EDL contains `subtitles`, the default project panel hides that path despite the documented inspector contract. Add a read-only row for `store.edl.subtitles`.</comment>

<file context>
@@ -0,0 +1,271 @@
+            Divider().background(Theme.border)
+
+            sectionHeader("PROJECT")
+            infoRow("Grade", store.edl.grade ?? "none")
+            infoRow("Duration", VirtualTime.fmt(store.total))
+            infoRow("Clips", "\(store.edl.ranges.count)")
</file context>

.keyboardShortcut("e", modifiers: .command)
Button("Export Preview") { store.export(preview: true) }
.keyboardShortcut("e", modifiers: [.command, .option])
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When ⌘Space is pressed before an EDL is loaded, this command sets playing to true despite there being no player item. Disable Play until store.edlPath is non-nil.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/StudioApp.swift, line 47:

<comment>When ⌘Space is pressed before an EDL is loaded, this command sets `playing` to true despite there being no player item. Disable Play until `store.edlPath` is non-nil.</comment>

<file context>
@@ -0,0 +1,61 @@
+                    .keyboardShortcut("e", modifiers: .command)
+                Button("Export Preview") { store.export(preview: true) }
+                    .keyboardShortcut("e", modifiers: [.command, .option])
+                Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
+                    .keyboardShortcut(.space, modifiers: [])
+            }
</file context>
Suggested change
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
.disabled(store.edlPath == nil)

Comment on lines +89 to +90
let x = CGFloat(item.range.start / dur) * w
let bw = max(CGFloat(item.range.duration / dur) * w, 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When an external edl.json range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to [0, dur] before computing x and bw.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/FilesPane.swift, line 89:

<comment>When an external `edl.json` range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to `[0, dur]` before computing `x` and `bw`.</comment>

<file context>
@@ -0,0 +1,105 @@
+                RoundedRectangle(cornerRadius: 3).fill(Theme.bgElevated)
+                if let dur = fileDuration, dur > 0 {
+                    ForEach(used, id: \.index) { item in
+                        let x = CGFloat(item.range.start / dur) * w
+                        let bw = max(CGFloat(item.range.duration / dur) * w, 2)
+                        RoundedRectangle(cornerRadius: 3)
</file context>
Suggested change
let x = CGFloat(item.range.start / dur) * w
let bw = max(CGFloat(item.range.duration / dur) * w, 2)
let start = max(0, min(item.range.start, dur))
let end = max(start, min(item.range.end, dur))
let x = CGFloat(start / dur) * w
let bw = min(max(CGFloat((end - start) / dur) * w, 2), w)

Comment thread studio-mac/DESIGN.md
(binary search), tooltip shows the snapped word and time: `…wasted." ✂ 6.85s`.
If no transcript exists for a source, free drag (0.01s grid).

**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/DESIGN.md, line 94:

<comment>The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.</comment>

<file context>
@@ -0,0 +1,186 @@
+(binary search), tooltip shows the snapped word and time:  `…wasted." ✂ 6.85s`.
+If no transcript exists for a source, free drag (0.01s grid).
+
+**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via
+drag-drop of whole blocks, select→inspector. Every commit: recompute
+`total_duration_s`, atomic-write `edl.json`, append `edit_log.jsonl`.
</file context>
Suggested change
**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via
**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, select→inspector.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

38 issues found across 61 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="studio-mac/Sources/Studio/ContentView.swift">

<violation number="1" location="studio-mac/Sources/Studio/ContentView.swift:85">
P3: `transport` is never composed; `canvas` renders `transportPill` instead, leaving this duplicate playback UI unreachable. Remove the unused property to prevent dead UI code from drifting.</violation>

<violation number="2" location="studio-mac/Sources/Studio/ContentView.swift:149">
P2: Option-clicking this title-bar button still renders `final.mp4`, so the advertised preview shortcut cannot work. Read the Option modifier in this action or provide a separate preview control.</violation>
</file>

<file name="install.md">

<violation number="1" location="install.md:67">
P3: The sanity-check curl uses `$KEY`, but no `$KEY` variable is defined after the key is stored via `video-use key <PASTED_KEY>`. The shell expands it to empty, sending `xi-api-key: `, which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. `$(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)`) or reference the pasted key directly instead of `$KEY`.</violation>
</file>

<file name="studio-mac/Sources/Studio/ZoomCamera.swift">

<violation number="1" location="studio-mac/Sources/Studio/ZoomCamera.swift:5">
P1: When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass `zoomRegions` into the render pipeline so export uses the same camera regions as the preview.</violation>
</file>

<file name="studio-mac/Sources/Studio/InspectorView.swift">

<violation number="1" location="studio-mac/Sources/Studio/InspectorView.swift:15">
P1: When `store.selection` is non-nil, `InspectorView.body` still renders `projectPanel`; `sliceEditor` has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.</violation>

<violation number="2" location="studio-mac/Sources/Studio/InspectorView.swift:80">
P3: When an EDL contains `subtitles`, the default project panel hides that path despite the documented inspector contract. Add a read-only row for `store.edl.subtitles`.</violation>
</file>

<file name="studio-mac/Sources/Studio/Models.swift">

<violation number="1" location="studio-mac/Sources/Studio/Models.swift:142">
P2: When `subtitle_style` contains an unmodeled key, `SubtitleStyle` drops it during decode and re-encoding, so any subsequent Studio edit silently loses that EDL metadata. Add a dynamic-key `extra` map to `SubtitleStyle`, matching `Range`, `Overlay`, and `Edl`.</violation>
</file>

<file name="studio-mac/Sources/Studio/Subtitles.swift">

<violation number="1" location="studio-mac/Sources/Studio/Subtitles.swift:29">
P2: When a transcript entry omits `type`, Studio displays it but `render.py` drops it. Use the renderer's strict `type == "word"` check here so the live captions and export stay identical.</violation>

<violation number="2" location="studio-mac/Sources/Studio/Subtitles.swift:92">
P2: When an EDL lacks `subtitle_style`, the preview uses margin 35 but export falls back to margin 90. Make the Swift default and renderer fallback consistent so captions do not move between preview and final output.</violation>

<violation number="3" location="studio-mac/Sources/Studio/Subtitles.swift:110">
P2: The preview uses the system font while export uses Helvetica, so the caption style being edited is not the style that renders. Use Helvetica with a bold weight in the preview to match the renderer.</violation>
</file>

<file name="studio-mac/Sources/Studio/StageView.swift">

<violation number="1" location="studio-mac/Sources/Studio/StageView.swift:76">
P2: During a virtual-camera zoom, this later overlay remains at the unzoomed size and position while the video moves. Scale and clip the subtitle overlay with the hero, or move the scale after all hero overlays.</violation>
</file>

<file name="studio-mac/Sources/Studio/StudioApp.swift">

<violation number="1" location="studio-mac/Sources/Studio/StudioApp.swift:47">
P3: When ⌘Space is pressed before an EDL is loaded, this command sets `playing` to true despite there being no player item. Disable Play until `store.edlPath` is non-nil.</violation>
</file>

<file name="src/video_use/grade.py">

<violation number="1" location="src/video_use/grade.py:109">
P2: On Windows, auto-analysis fails because the temporary `C:\...` path is unescaped inside the FFmpeg filter expression. Convert it to forward slashes and escape the drive-letter colon before constructing `metadata=print`.</violation>
</file>

<file name="studio-mac/Sources/Studio/FilesPane.swift">

<violation number="1" location="studio-mac/Sources/Studio/FilesPane.swift:89">
P3: When an external `edl.json` range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to `[0, dur]` before computing `x` and `bw`.</violation>
</file>

<file name="studio-mac/Sources/Studio/TimelineView.swift">

<violation number="1" location="studio-mac/Sources/Studio/TimelineView.swift:38">
P2: Projects with overlays have no overlay lane: this stack renders only the ruler, cut blocks, and captions, so overlay timing is invisible in the editor. Add an overlay track from `store.edl.overlays`.</violation>

<violation number="2" location="studio-mac/Sources/Studio/TimelineView.swift:59">
P2: When zoomed in, holding the pointer near an edge only seeks once; this handler schedules no repeated pan or edge-depth speed. Add a timer-driven `panOffset` update for edge hover, separate from hover scrubbing.</violation>
</file>

<file name="src/video_use/transcribe_batch.py">

<violation number="1" location="src/video_use/transcribe_batch.py:34">
P2: Mixed-case extensions such as `take.Mp4` and `take.M4V` are silently omitted from batch transcription. Normalize `p.suffix` before checking `VIDEO_EXTS`.</violation>

<violation number="2" location="src/video_use/transcribe_batch.py:48">
P2: With `--workers 0` or a negative value, `ThreadPoolExecutor` raises `ValueError` after setup instead of showing a CLI validation error. Require a positive count during argument parsing.</violation>
</file>

<file name="src/video_use/run.py">

<violation number="1" location="src/video_use/run.py:88">
P1: When users follow the documented `video-use key <PASTED_KEY>` flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing `getpass` prompt instead of accepting positional secrets.</violation>

<violation number="2" location="src/video_use/run.py:103">
P1: When the existing config uses whitespace around `=`, `cmd_key` leaves the old entry before appending the new one. Parse the variable name before `=` when filtering so `video-use key` actually replaces valid existing entries.</violation>

<violation number="3" location="src/video_use/run.py:128">
P2: When a PATH executable cannot run `-version` or returns nonzero, `doctor` still reports it `OK` because this branch passes literal `True`. Check the subprocess return code and mark execution failures as `FAIL`.</violation>
</file>

<file name="src/video_use/render.py">

<violation number="1" location="src/video_use/render.py:32">
P1: When the installed `video-use render` command uses a named preset, this import falls back and silently disables the grade. Import the sibling module relatively so presets and auto-grading use the real implementation.</violation>

<violation number="2" location="src/video_use/render.py:214">
P1: When an EDL mixes portrait and landscape or different aspect ratios, the extracted clips have different dimensions before the copy-concat step, causing concat failure or a mis-sized output. Normalize every segment to one canvas size before copy-concat, or use a filtered concat.</violation>

<violation number="3" location="src/video_use/render.py:280">
P1: When Studio saves a conventional EDL with a relative source path, render resolves it in the wrong directory and ffmpeg aborts before extraction. Resolve `edit/` projects against `edit_dir.parent`, with the existing edit-directory fallback.</violation>

<violation number="4" location="src/video_use/render.py:608">
P2: When a source has no audio and compositing is needed, this mandatory map makes ffmpeg fail instead of rendering the video. Make the audio map optional with `0:a?` so audio is copied when present.</violation>
</file>

<file name="studio-mac/Sources/Studio/Store.swift">

<violation number="1" location="studio-mac/Sources/Studio/Store.swift:112">
P1: When two opens overlap on a slow drive, an older load can overwrite the newer project, watcher, and source data. Cancel the previous load or discard completions whose load generation is no longer current.</violation>

<violation number="2" location="studio-mac/Sources/Studio/Store.swift:121">
P0: When a newly opened or externally reloaded EDL has more ranges than the current composition, `stateJSON()` and the timeline index a stale `offsets` array and crash. Publish matching prefix sums before exposing the new `edl`, or make the loading state safe in both loading paths.</violation>

<violation number="3" location="studio-mac/Sources/Studio/Store.swift:155">
P1: When an agent changes the EDL’s `sources` mapping, `externalChange` keeps the old paths and transcripts while rebuilding the new ranges. Reload the complete `LoadedProject` metadata atomically instead of decoding only `Edl`.</violation>

<violation number="4" location="studio-mac/Sources/Studio/Store.swift:275">
P2: The camera-region editing methods are unreachable, so users cannot create the push-ins represented by `zoomRegions`. Wire these actions into UI or remote commands, or remove the unfinished state until the feature is usable.</violation>

<violation number="5" location="studio-mac/Sources/Studio/Store.swift:418">
P2: When a source duration is still unavailable, `setOut` allows an end time beyond the media and persists an invalid cut. Reject trimming until the duration is known or obtain and apply the asset duration before committing.</violation>
</file>

<file name="helpers/grade.py">

<violation number="1" location="helpers/grade.py:12">
P2: Code importing the legacy `helpers.grade` module can no longer access `get_preset` or `auto_grade_for_clip`. Re-export the prior public helpers from `video_use.grade` so this back-compat shim preserves the documented import contract.</violation>
</file>

<file name="src/video_use/paths.py">

<violation number="1" location="src/video_use/paths.py:18">
P2: When any candidate `.env` contains non-UTF-8 bytes, `resolve_api_key()` raises `UnicodeDecodeError` instead of treating that candidate as unreadable and continuing to the next source. Catch `UnicodeError` alongside `OSError` so `doctor` and transcription still resolve a valid fallback key.</violation>
</file>

<file name="src/video_use/transcribe.py">

<violation number="1" location="src/video_use/transcribe.py:99">
P1: When two source files share a stem, this maps both to one transcript; batch can transcribe them concurrently and the last write wins, so one file's timestamps are used for the other. Use a collision-free cache key and align render lookup with it.</violation>

<violation number="2" location="src/video_use/transcribe.py:101">
P1: When a source is edited or replaced, this existence-only check returns the old transcript, so word boundaries and captions describe different media. Persist and compare a source fingerprint before honoring the cache.</violation>
</file>

<file name="studio-mac/Sources/Studio/Composition.swift">

<violation number="1" location="studio-mac/Sources/Studio/Composition.swift:129">
P2: When a source is 60 fps or higher, this fixed 30 fps composition drops frames during preview, so playback is no longer frame-accurate and motion appears less smooth. Derive the composition frame duration from the loaded source frame rates, or otherwise preserve the source cadence instead of hard-coding 30 fps.</violation>
</file>

<file name="src/video_use/pack_transcripts.py">

<violation number="1" location="src/video_use/pack_transcripts.py:115">
P2: When a phrase starts with an audio event or token without `speaker_id`, `current_speaker` stays `None` after the first diarized word. Later speaker changes therefore never flush the phrase, merging speakers; update `current_speaker` when it is unknown and a later token supplies an ID.</violation>
</file>

<file name="studio-mac/DESIGN.md">

<violation number="1" location="studio-mac/DESIGN.md:94">
P3: The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:28">
P2: Following 'Install from source' for a fresh user fails at `video-use skill > ~/.claude/skills/video-use/SKILL.md` because the target directory doesn't exist and the shell `>` redirect errors out. Add `mkdir -p ~/.claude/skills/video-use` before the redirect, matching the 'Install' section.</violation>
</file>

Re-trigger cubic

guard let self else { return }
self.edlPath = loaded.edlPath
self.dir = loaded.dir
self.edl = loaded.edl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: When a newly opened or externally reloaded EDL has more ranges than the current composition, stateJSON() and the timeline index a stale offsets array and crash. Publish matching prefix sums before exposing the new edl, or make the loading state safe in both loading paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/Store.swift, line 121:

<comment>When a newly opened or externally reloaded EDL has more ranges than the current composition, `stateJSON()` and the timeline index a stale `offsets` array and crash. Publish matching prefix sums before exposing the new `edl`, or make the loading state safe in both loading paths.</comment>

<file context>
@@ -0,0 +1,612 @@
+                guard let self else { return }
+                self.edlPath = loaded.edlPath
+                self.dir = loaded.dir
+                self.edl = loaded.edl
+                self.sourcePaths = loaded.sourcePaths
+                self.transcripts = loaded.transcripts
</file context>


// Screen Studio-style virtual camera: during a zoom region the preview eases into a focus point
// and holds, then eases back out. Pure function of the playhead so it's correct while playing and
// while scrubbing. This drives the live preview; the same regions are what an export would burn in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass zoomRegions into the render pipeline so export uses the same camera regions as the preview.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/ZoomCamera.swift, line 5:

<comment>When a zoom region is used, the preview shows the push-in but the exported video does not. Persist or pass `zoomRegions` into the render pipeline so export uses the same camera regions as the preview.</comment>

<file context>
@@ -0,0 +1,44 @@
+
+// Screen Studio-style virtual camera: during a zoom region the preview eases into a focus point
+// and holds, then eases back out. Pure function of the playhead so it's correct while playing and
+// while scrubbing. This drives the live preview; the same regions are what an export would burn in.
+struct ZoomRegion: Equatable {
+    var start: Double        // output time
</file context>

Divider().background(Theme.border)
subtitleControls // the main thing you actually tune here
Divider().background(Theme.border)
projectPanel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When store.selection is non-nil, InspectorView.body still renders projectPanel; sliceEditor has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/InspectorView.swift, line 15:

<comment>When `store.selection` is non-nil, `InspectorView.body` still renders `projectPanel`; `sliceEditor` has no call site. Branch on the selection so the In/Out and Remove slice controls are reachable.</comment>

<file context>
@@ -0,0 +1,271 @@
+                Divider().background(Theme.border)
+                subtitleControls     // the main thing you actually tune here
+                Divider().background(Theme.border)
+                projectPanel
+            }
+            .padding(16)
</file context>

Comment thread src/video_use/run.py

def cmd_key(argv: list[str]) -> int:
if argv and argv[0] not in ("-", "--stdin"):
key = argv[0].strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When users follow the documented video-use key <PASTED_KEY> flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing getpass prompt instead of accepting positional secrets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/video_use/run.py, line 88:

<comment>When users follow the documented `video-use key <PASTED_KEY>` flow, the API key is stored in shell history and exposed in process arguments. Read keys from stdin or a non-echoing `getpass` prompt instead of accepting positional secrets.</comment>

<file context>
@@ -0,0 +1,194 @@
+
+def cmd_key(argv: list[str]) -> int:
+    if argv and argv[0] not in ("-", "--stdin"):
+        key = argv[0].strip()
+    elif not sys.stdin.isatty():
+        key = sys.stdin.readline().strip()
</file context>

Comment thread src/video_use/run.py
if env_path.exists():
lines = [
l for l in env_path.read_text().splitlines()
if not l.strip().startswith("ELEVENLABS_API_KEY=")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the existing config uses whitespace around =, cmd_key leaves the old entry before appending the new one. Parse the variable name before = when filtering so video-use key actually replaces valid existing entries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/video_use/run.py, line 103:

<comment>When the existing config uses whitespace around `=`, `cmd_key` leaves the old entry before appending the new one. Parse the variable name before `=` when filtering so `video-use key` actually replaces valid existing entries.</comment>

<file context>
@@ -0,0 +1,194 @@
+    if env_path.exists():
+        lines = [
+            l for l in env_path.read_text().splitlines()
+            if not l.strip().startswith("ELEVENLABS_API_KEY=")
+        ]
+    lines.append(f"ELEVENLABS_API_KEY={key}")
</file context>
Suggested change
if not l.strip().startswith("ELEVENLABS_API_KEY=")
if not ("=" in l and l.split("=", 1)[0].strip() == "ELEVENLABS_API_KEY")

Comment thread install.md
```bash
python ~/Developer/video-use/helpers/timeline_view.py --help >/dev/null && echo "helpers OK"
ffprobe -version | head -1
curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $KEY" https://api.elevenlabs.io/v1/user

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The sanity-check curl uses $KEY, but no $KEY variable is defined after the key is stored via video-use key <PASTED_KEY>. The shell expands it to empty, sending xi-api-key: , which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. $(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)) or reference the pasted key directly instead of $KEY.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.md, line 67:

<comment>The sanity-check curl uses `$KEY`, but no `$KEY` variable is defined after the key is stored via `video-use key <PASTED_KEY>`. The shell expands it to empty, sending `xi-api-key: `, which returns 401 even for a valid key and triggers a needless re-ask. Retrieve the value from the file the tool wrote (e.g. `$(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)`) or reference the pasted key directly instead of `$KEY`.</comment>

<file context>
@@ -5,158 +5,90 @@ description: Install video-use into the current agent (Claude Code, Codex, Herme
 ```bash
-python ~/Developer/video-use/helpers/timeline_view.py --help >/dev/null && echo "helpers OK"
-ffprobe -version | head -1
+curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $KEY" https://api.elevenlabs.io/v1/user

</file context>


</details>

```suggestion
curl -s -o /dev/null -w '%{http_code}\n' -H "xi-api-key: $(sed -n 's/^ELEVENLABS_API_KEY=//p' ~/.config/video-use/.env)" https://api.elevenlabs.io/v1/user

Divider().background(Theme.border)

sectionHeader("PROJECT")
infoRow("Grade", store.edl.grade ?? "none")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When an EDL contains subtitles, the default project panel hides that path despite the documented inspector contract. Add a read-only row for store.edl.subtitles.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/InspectorView.swift, line 80:

<comment>When an EDL contains `subtitles`, the default project panel hides that path despite the documented inspector contract. Add a read-only row for `store.edl.subtitles`.</comment>

<file context>
@@ -0,0 +1,271 @@
+            Divider().background(Theme.border)
+
+            sectionHeader("PROJECT")
+            infoRow("Grade", store.edl.grade ?? "none")
+            infoRow("Duration", VirtualTime.fmt(store.total))
+            infoRow("Clips", "\(store.edl.ranges.count)")
</file context>

.keyboardShortcut("e", modifiers: .command)
Button("Export Preview") { store.export(preview: true) }
.keyboardShortcut("e", modifiers: [.command, .option])
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When ⌘Space is pressed before an EDL is loaded, this command sets playing to true despite there being no player item. Disable Play until store.edlPath is non-nil.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/StudioApp.swift, line 47:

<comment>When ⌘Space is pressed before an EDL is loaded, this command sets `playing` to true despite there being no player item. Disable Play until `store.edlPath` is non-nil.</comment>

<file context>
@@ -0,0 +1,61 @@
+                    .keyboardShortcut("e", modifiers: .command)
+                Button("Export Preview") { store.export(preview: true) }
+                    .keyboardShortcut("e", modifiers: [.command, .option])
+                Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
+                    .keyboardShortcut(.space, modifiers: [])
+            }
</file context>
Suggested change
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
Button(store.playing ? "Pause" : "Play") { store.togglePlay() }
.disabled(store.edlPath == nil)

Comment on lines +89 to +90
let x = CGFloat(item.range.start / dur) * w
let bw = max(CGFloat(item.range.duration / dur) * w, 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When an external edl.json range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to [0, dur] before computing x and bw.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/Sources/Studio/FilesPane.swift, line 89:

<comment>When an external `edl.json` range extends beyond the asset duration, these calculations can place its amber block partly outside the source strip. Clamp the range endpoints to `[0, dur]` before computing `x` and `bw`.</comment>

<file context>
@@ -0,0 +1,105 @@
+                RoundedRectangle(cornerRadius: 3).fill(Theme.bgElevated)
+                if let dur = fileDuration, dur > 0 {
+                    ForEach(used, id: \.index) { item in
+                        let x = CGFloat(item.range.start / dur) * w
+                        let bw = max(CGFloat(item.range.duration / dur) * w, 2)
+                        RoundedRectangle(cornerRadius: 3)
</file context>
Suggested change
let x = CGFloat(item.range.start / dur) * w
let bw = max(CGFloat(item.range.duration / dur) * w, 2)
let start = max(0, min(item.range.start, dur))
let end = max(start, min(item.range.end, dur))
let x = CGFloat(start / dur) * w
let bw = min(max(CGFloat((end - start) / dur) * w, 2), w)

Comment thread studio-mac/DESIGN.md
(binary search), tooltip shows the snapped word and time: `…wasted." ✂ 6.85s`.
If no transcript exists for a source, free drag (0.01s grid).

**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At studio-mac/DESIGN.md, line 94:

<comment>The spec contradicts itself on reordering: 'Edits (v1)' claims 'reorder via drag-drop of whole blocks' is implemented, while 'Out of scope (v1)' lists 'reorder via drag-drop' as not shipped, and no reorder exists in the Swift code. Remove the reorder claim from 'Edits (v1)' (and the layout's implied drag-drop reorder) so the doc matches the out-of-scope list and the implementation.</comment>

<file context>
@@ -0,0 +1,186 @@
+(binary search), tooltip shows the snapped word and time:  `…wasted." ✂ 6.85s`.
+If no transcript exists for a source, free drag (0.01s grid).
+
+**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via
+drag-drop of whole blocks, select→inspector. Every commit: recompute
+`total_duration_s`, atomic-write `edl.json`, append `edit_log.jsonl`.
</file context>
Suggested change
**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, reorder via
**Edits (v1)**: trim via edge drag (min 0.2s), delete segment, select→inspector.

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.

2 participants