Studio: native macOS EDL editor for video-use - #138
Conversation
- 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
| nonlocal ok | ||
| ok = ok and passed | ||
| mark = "OK " if passed else "FAIL" | ||
| print(f" {mark} {label}" + (f" — {detail}" if detail else "")) |
There was a problem hiding this comment.
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>
| guard let self else { return } | ||
| self.edlPath = loaded.edlPath | ||
| self.dir = loaded.dir | ||
| self.edl = loaded.edl |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
|
|
||
| def cmd_key(argv: list[str]) -> int: | ||
| if argv and argv[0] not in ("-", "--stdin"): | ||
| key = argv[0].strip() |
There was a problem hiding this comment.
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>
| if env_path.exists(): | ||
| lines = [ | ||
| l for l in env_path.read_text().splitlines() | ||
| if not l.strip().startswith("ELEVENLABS_API_KEY=") |
There was a problem hiding this comment.
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>
| if not l.strip().startswith("ELEVENLABS_API_KEY=") | |
| if not ("=" in l and l.split("=", 1)[0].strip() == "ELEVENLABS_API_KEY") |
| ```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 |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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() } |
There was a problem hiding this comment.
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>
| Button(store.playing ? "Pause" : "Play") { store.togglePlay() } | |
| Button(store.playing ? "Pause" : "Play") { store.togglePlay() } | |
| .disabled(store.edlPath == nil) |
| let x = CGFloat(item.range.start / dur) * w | ||
| let bw = max(CGFloat(item.range.duration / dur) * w, 2) |
There was a problem hiding this comment.
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>
| 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) |
| (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 |
There was a problem hiding this comment.
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>
| **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. |
There was a problem hiding this comment.
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>
| guard let self else { return } | ||
| self.edlPath = loaded.edlPath | ||
| self.dir = loaded.dir | ||
| self.edl = loaded.edl |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
|
|
||
| def cmd_key(argv: list[str]) -> int: | ||
| if argv and argv[0] not in ("-", "--stdin"): | ||
| key = argv[0].strip() |
There was a problem hiding this comment.
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>
| if env_path.exists(): | ||
| lines = [ | ||
| l for l in env_path.read_text().splitlines() | ||
| if not l.strip().startswith("ELEVENLABS_API_KEY=") |
There was a problem hiding this comment.
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>
| if not l.strip().startswith("ELEVENLABS_API_KEY=") | |
| if not ("=" in l and l.split("=", 1)[0].strip() == "ELEVENLABS_API_KEY") |
| ```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 |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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() } |
There was a problem hiding this comment.
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>
| Button(store.playing ? "Pause" : "Play") { store.togglePlay() } | |
| Button(store.playing ? "Pause" : "Play") { store.togglePlay() } | |
| .disabled(store.edlPath == nil) |
| let x = CGFloat(item.range.start / dur) * w | ||
| let bw = max(CGFloat(item.range.duration / dur) * w, 2) |
There was a problem hiding this comment.
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>
| 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) |
| (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 |
There was a problem hiding this comment.
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>
| **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. |
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:AVMutableCompositionfor hardware-decoded, gapless virtual-cut playback (including 4K, mixed-resolution and portrait sources)A Tauri 2 version came first (
1e50d2a) and was dropped in favour of the native app (fad882d).Render / CLI
edl.subtitle_style(size, margin, uppercase, chunk words).webmoverlays with libvpx-vp9 to preserve alpha._*sidecars in video/transcript discoveryvideo-useCLI tooledit_log.jsonlcontract for human edits made outside the conversationNotes
studio-mac/.build/(~150MB of compiled binaries, dSYMs and module caches) that had been committed by accident..gitignorenow covers it.swift build -c releasesucceeds; warnings only (Swift 6Sendablecapture warnings inStore.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-useCLI (0.2.0). Studio replaces an earlier Tauri prototype — AVFoundation playback won on 4K footage. Thehelpers/scripts become back-compat shims over the packaged modules, and install docs are rewritten arounduv 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.edit_log.jsonlrecords human edits made outside the conversation.Bug Fixes
edl.subtitle_style(size, margin, uppercase, chunk words)..webmoverlays decode with libvpx-vp9 so alpha is preserved.--unsafe-pathsoverrides).._*sidecars are skipped in video/transcript discovery.Written for commit 56f64b3. Summary will update on new commits.