video-editing 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: video-editing
3
+ description: >-
4
+ Edit a talking-head video reel into a tight, clean cut using ONLY the local
5
+ `video-editing` CLI — never the video-use repo, never the clean-cut skill,
6
+ never raw Python helpers or ffmpeg by hand. Removes bad retakes, false starts,
7
+ stutters and cut-off words (keep the LAST complete take, but keep the CLEANEST
8
+ take when an earlier one is clean and a later one stutters), tightens every
9
+ pause to ~0.13s, cold-opens on the strongest hook, trims outro dead air.
10
+ Vertical 9:16, clean cuts only (no grade/subtitles/overlays by default). Two
11
+ mandatory gates: SCREEN before cutting and CONTENT-VERIFY (re-transcribe the
12
+ rendered preview at every join) after. Use whenever the user drops a
13
+ talking-head clip / reel / vlog / interview and wants it edited or tightened —
14
+ phrasings like "edit this reel", "cut the bad retakes", "remove the false
15
+ starts", "clean up this recording", "tighten this reel", "tighten this", "I
16
+ flubbed some lines, fix it", "get rid of the dead air", "make a clean cut of
17
+ this", or just a video path with "make this cleaner". Handles Hebrew/RTL.
18
+ This is the single source of truth for reel edits; it supersedes and replaces
19
+ the clean-cut and video-use skills.
20
+ ---
21
+
22
+ # Video Editing (CLI-only)
23
+
24
+ Turn a raw talking-head recording into a tight, clean vertical cut. The local
25
+ **`video-editing`** CLI is the single source of truth and owns ALL execution
26
+ (probe, transcribe, screen, bake master, render, QC, content-verify). **You**
27
+ read the transcript and author the EDL; the CLI does everything executable.
28
+
29
+ ## Install
30
+
31
+ Requirements: **Node ≥ 20**, **ffmpeg**/**ffprobe** on PATH, and an **ElevenLabs API key**
32
+ (`ELEVENLABS_API_KEY`) for transcription.
33
+
34
+ ```bash
35
+ npm install -g video-editing # the CLI
36
+ npx skills add ItayElgazar/clipready --skill video-editing # this skill
37
+ ```
38
+
39
+ ## HARD RULE — execution boundary (read first)
40
+
41
+ - Use **only** the `video-editing` CLI (`video-editing <subcommand>`). Run
42
+ `video-editing --help` to see commands.
43
+ - **NEVER** run ffmpeg/ffprobe/transcription by hand — the CLI owns all execution.
44
+ - If a capability seems missing, STOP and report it as a CLI gap. Do not reach around the CLI.
45
+ - Your only hand-authored artifact is `<job>/edl.json`.
46
+
47
+ ## Prerequisites
48
+
49
+ - `video-editing` on PATH (`which video-editing`), plus `ffmpeg`/`ffprobe`.
50
+ - ElevenLabs key: the CLI owns it (env `ELEVENLABS_API_KEY` or `~/.config/video-editing/.env`;
51
+ set once with `video-editing config set-key <key>`). Transcription is cached per source
52
+ and is a paid call — never force a re-transcribe of an unchanged source.
53
+
54
+ ## Workflow
55
+
56
+ 1. **Init** — `video-editing init-job --source "<video>" --job-dir "/vercel/sandbox/jobs/<job_id>"`
57
+ → job dir + `source.json`. Always pass `--job-dir` explicitly (there is no `~/Downloads` on
58
+ the sandbox); use the job dir given in the task prompt.
59
+ 2. **Transcribe + screen** — `video-editing transcribe --job-dir "<job>"` → word-level
60
+ transcript + screening artifacts (`flags.txt`, `takes_packed.md`, `word_dump.txt`, `coverage.json`).
61
+ 3. **GATE 1 — SCREEN before cutting** — read `<job>/flags.txt`: resolve every **[A]**
62
+ over-long/collapsed token (retakes hide here), judge every **[B]** repeated phrase
63
+ (retake vs intentional), note **[C]** gaps/audio-events. Confirm `<job>/coverage.json`
64
+ (last word within ~2s of duration). See `references/scribe-collapse.md`.
65
+ 4. **Author the EDL** — read `<job>/word_dump.txt` (and `prompts/author-edl.prompt.md` if present),
66
+ then write `<job>/edl.json` per the brief below. To inspect any moment visually:
67
+ `video-editing inspect --job-dir "<job>" --video vertical_src --start <a> --end <b>`.
68
+ (Or let the CLI scaffold the prompt: `video-editing cut-bad-retakes --job-dir "<job>"`.)
69
+ 5. **Render preview** — `video-editing render --job-dir "<job>" --mode preview`.
70
+ Render **auto-runs the amplitude silence-trim pass** (Gate 3) first: it scans the audio
71
+ with `silencedetect` and excises every sub-floor silence the transcript can't see —
72
+ pauses baked *inside* a stretched word token, room tone, breaths — then trims silent
73
+ range edges. Your authored `edl.json` is backed up to `edl.raw.json` and rewritten with
74
+ the tightened ranges (more ranges, shorter total). It is **transcript-aware**: it never
75
+ splits inside a single word token (spelled acronyms like "WWX", held vowels, numbers).
76
+ Tune or disable: `video-editing desilence --job-dir "<job>"` (flags below) /
77
+ `render … --no-desilence`.
78
+ 6. **GATE 2 — CONTENT-VERIFY** — `video-editing verify --job-dir "<job>" --json`.
79
+ It re-transcribes the preview at every join (healing OFF) and flags duplicated phrases
80
+ across a join (= a retake left in) and over-long gaps (= dead air). Adjudicate with
81
+ `video-editing inspect --job-dir "<job>" --join <i>`. See `references/adversarial-review.md`.
82
+ 7. **Fix → re-render → re-verify** until `verify` is clean (`references/fix` rules apply).
83
+ 8. **Final** — `video-editing render --job-dir "<job>" --mode final`.
84
+
85
+ ## The standing editing brief (apply exactly)
86
+
87
+ - Output **VERTICAL 9:16**, **CLEAN CUTS ONLY** (`grade:"none"`, no subtitles, no overlays).
88
+ - **CUT** bad retakes / false starts / cut-off / stuttered words — keep the **LAST complete take**,
89
+ BUT keep the **CLEANEST** take when an earlier take is clean and a later one stutters.
90
+ - **TIGHTEN every pause** to ~0.12–0.14s of air (remove dead air). The render's amplitude
91
+ pass (Gate 3) enforces this automatically — it cuts **every silence below the dB floor**
92
+ (default −36 dB, pauses ≥0.14s, leaving ~0.09s air). This catches the silences the
93
+ transcript misses (dead air baked inside a long word token, room tone, breaths). It runs
94
+ on every render; you don't hand-tighten silences in the EDL.
95
+ - **CUT ALL SILENCES below the dB floor** — for a more/less aggressive cut, tune the pass:
96
+ `video-editing desilence --job-dir "<job>" --noise-db -39 --min-silence 0.30` (gentler) or
97
+ `--noise-db -34 --min-silence 0.12 --lead-pad 0.03 --trail-pad 0.04` (harder). Re-render +
98
+ re-verify after. To start clean, restore `cp edl.raw.json edl.json` then re-run.
99
+ - **COLD-OPEN** on the strongest hook (trim weak/dead intro).
100
+ - **TRIM outro dead air** (trailing breaths/ambient/silence/device noise).
101
+ - **KEEP intentional rhetorical repeats.**
102
+ - Never cut inside a word — snap every edge to a transcript word boundary; pad cut edges
103
+ 30–200ms (lead ~0.05s, trail ~0.08s; ~0.03–0.05s at retake joins, staying inside the silence).
104
+ The renderer adds 30ms audio fades, so tight pads are safe.
105
+ - Hebrew/RTL: read the transcript word-by-word with explicit timestamps to avoid reorder confusion.
106
+
107
+ ## The gates
108
+
109
+ 1. **SCREEN before cutting** — Scribe hides retakes in over-long/dropped tokens; always read `flags.txt` first.
110
+ 2. **DESILENCE on render** (automatic) — the amplitude pass cuts every sub-dB-floor silence the
111
+ transcript can't see and trims silent edges. Transcript-aware (won't split spelled acronyms).
112
+ 3. **CONTENT-VERIFY after preview** — `video-editing verify` re-transcribes joins; a waveform/visual check
113
+ alone does NOT catch a retake left in (abandoned take + keeper render as one continuous clean span).
114
+ A **≥3-word** duplicate across a join = a real retake left in → **error**, fix it. A bare **2-word**
115
+ repeat = **warning**: adjudicate with `inspect` — it is usually a spelled acronym / number / emphasis
116
+ (e.g. "WW" → "דבל יו דבל יו"), not a retake, and is safe to keep.
117
+
118
+ ## EDL format
119
+
120
+ ```json
121
+ {
122
+ "version": 1,
123
+ "sources": {"SRC": "<job>/vertical_src.mp4"},
124
+ "grade": "none",
125
+ "ranges": [
126
+ {"source": "SRC", "start": 11.35, "end": 14.12, "beat": "HOOK", "quote": "…", "reason": "cleanest take; stops before the slip at 38.46"}
127
+ ],
128
+ "overlays": [],
129
+ "total_duration_s": 51.33
130
+ }
131
+ ```
132
+
133
+ Ranges are KEEP segments in source seconds; the gaps between them are the cuts. A retake range
134
+ must END at the **last clean word before the abandoned attempt**. `total_duration_s` = Σ(end−start).
135
+
136
+ ## Timeline v2 (preferred when present)
137
+
138
+ When the CLI exposes a `timeline compile` verb (run `video-editing timeline --help`),
139
+ **author `<job>/timeline.json` instead of hand-writing `edl.json`.** timeline.json is the
140
+ single authored artifact: cuts (keep-ranges) plus optional **captions, zooms, overlays, and
141
+ audio blocks**. The compiler is compiler-grade — it validates, resolves phrase anchors to exact
142
+ source seconds, and emits `resolved/plan.json`, `resolved/diagnostics.json`, and a legacy
143
+ `edl.json`.
144
+
145
+ **Prerequisite — bake the vertical master first.** `timeline compile` probes
146
+ `<job>/vertical_src.mp4`, and `transcribe` alone does NOT create it: after transcribing, run
147
+ `video-editing cut-bad-retakes --job-dir "<job>" --render none` once (it bakes the vertical
148
+ master AND writes the screening artifacts the SCREEN gate needs anyway). Skipping this makes
149
+ `timeline compile` fail with a raw ffprobe "No such file or directory" — that error means run
150
+ the bake, not that your timeline is wrong.
151
+
152
+ Loop until clean:
153
+
154
+ 1. Author/patch `<job>/timeline.json`.
155
+ 2. `video-editing timeline compile --job-dir "<job>" --json` — read the diagnostics and iterate
156
+ until it exits **0**:
157
+ - **exit 2 = validation failed** (schema / semantic / capability) — fix the reported `path`s.
158
+ - **exit 3 = ambiguous anchor** — disambiguate with
159
+ `video-editing timeline resolve --phrase "…" --near <t>` (or `--occurrence <n>`), then bake the
160
+ resolved seconds / occurrence into the anchor and re-compile.
161
+ 3. **Never hand-edit anything under `resolved/`** — it is generated; re-compile instead.
162
+ 4. compile emits the legacy `edl.json`, so the **render → desilence → verify → final** steps are
163
+ **unchanged** — run them exactly as in the workflow above.
164
+
165
+ **Phrase → seconds ONLY via `timeline resolve`.** Never eyeball `word_dump.txt` for anchor
166
+ times — `resolve` returns word-accurate `start`/`end` and flags ambiguity so you disambiguate
167
+ deliberately (`--near <srcSeconds>` or `--occurrence <n>`).
168
+
169
+ Captions: enable with `video-editing timeline captions generate --job-dir "<job>" [--preset grouped]`
170
+ — cues are **derived at compile**, you don't hand-write cue timings. The default preset `grouped`
171
+ puts up to 4 words in each cue, starting a new cue on long word gaps (>0.6s), cut joins (a cue
172
+ never spans a join), and sentence-ending `.` `?` `!` (same characters in Hebrew); `opus-karaoke`
173
+ is the word-by-word opt-in. Timing/grouping knobs live in `captions.style` in timeline.json:
174
+ `maxWords` (default 4), `leadS` (default 0.08 — each cue appears that many seconds early, clamped
175
+ to the previous cue / segment start), `holdS` (default 0.9 — each cue holds on screen up to that
176
+ long past its last word to bridge the gap to the next cue, killing blink). **Word styling**: an
177
+ `op:"style"` caption override colors and/or emphasizes the anchored words — e.g. "make word X red"
178
+ is `{"op":"style","anchor":{"type":"words","source":"SRC","phrase":"X"},"color":"#ff5c5c"}`;
179
+ `emphasis:"pop"|"highlight"` animates the word at its onset. `captions.style.karaoke: true`
180
+ adds a per-word highlight sweep inside grouped cues (staged). Validate anytime with
181
+ `video-editing timeline validate --job-dir "<job>" --json`.
182
+
183
+ ### Authoring zooms
184
+
185
+ A zoom is a punch-in on `rect` (unit-canvas crop, `x+w<=1`, `y+h<=1`) at `scale`
186
+ (`1 < scale <= 3`), anchored to a phrase or source span. Static by default (hard in/out).
187
+ For a **ramp**, add `to: {scale?, rect?}` — the zoom animates from the base rect/scale to
188
+ `to` over the zoom window, shaped by `ease: {in, out}` (both > 0 → smooth in-out). Zooms
189
+ resolve onCut like everything else (`clip` default). Full examples:
190
+ `references/timeline-v2-vocabulary.md`.
191
+
192
+ ### Authoring overlays & assets
193
+
194
+ - Text: `kind:"text"` (positioned) / `kind:"title-card"` (full-frame card) + `text`.
195
+ - **Image/sticker**: `kind:"sticker"` (or `"image"`) + `asset: {ref: "assets/logo.png"}` —
196
+ `ref` is a **job-dir-relative path**; keep files under `<job>/assets/`. Position with
197
+ `layout.x/y` (center), size with `size.w/h` (canvas fractions; sticker default ~0.25 of
198
+ width), stack with `z` (0–50). Animate with `enter`/`exit`
199
+ (`{preset: fade|pop|slide-up|slide-down|scale-in, durationS ≤ 2}`).
200
+ - A referenced file that does not exist fails `timeline compile` with `asset-missing` —
201
+ put the file in place first.
202
+ - On `review push --timeline` / `--complete`, referenced assets are **uploaded to
203
+ clipready automatically** (sha256-cached in `<job>/review/assets.json`, so unchanged
204
+ files are never re-uploaded); the push payload carries the `{ref: asset_id}` manifest.
205
+ - `compose` copies referenced assets into `<job>/composition/media/` and renders them.
206
+
207
+ ### Delivering captions/zooms
208
+
209
+ The ffmpeg `render --mode final` path is **CUT-ONLY** — it never draws the compiled
210
+ captions/zooms/overlays from `resolved/plan.json`. Deliver those visual layers with either:
211
+
212
+ - **`video-editing compose --job-dir "<job>" --render`** — local and free. Builds a declarative
213
+ HyperFrames composition in `<job>/composition/` (compressed proxy of `vertical_src.mp4`,
214
+ the CONTINUOUS cut audio extracted from `preview.mp4`, and `index.html`) and renders it with
215
+ the `hyperframes` CLI (needs it on PATH: `npm install -g hyperframes`). Prerequisites:
216
+ `resolved/plan.json` (run `timeline compile`) and `preview.mp4` (run `render --mode preview`).
217
+ Without `--render` it only builds the composition and prints the exact `hyperframes render`
218
+ command to run. Output defaults to `<job>/composed.mp4`.
219
+ - **The ClipReady cloud render** (`cloud render`), which renders the same plan server-side
220
+ (requires a ClipReady account + API key — see the review-round section below).
221
+
222
+ Do **NOT** fall back to the legacy `render --build-subtitles` ASS burn for timeline-v2 jobs —
223
+ it requires a libass ffmpeg and renders the old 2-word subtitle style, not the compiled cues.
224
+
225
+ ### Working a ClipReady review round
226
+
227
+ > Cloud features require a ClipReady account + API key (`CLIPREADY_API_BASE`, `CLIPREADY_API_KEY`).
228
+
229
+ When the job is a clipready review round (you have an API base + credential + video id), the
230
+ CLI syncs the round for you — no hand-rolled curl:
231
+
232
+ 1. `video-editing review pull --job-dir "<job>"` — writes `<job>/review/round.json` (run id,
233
+ requested changes) and `<job>/review/instruction.md`, and hydrates `<job>/timeline.json`
234
+ from the server when you don't already have one (it never overwrites a local timeline).
235
+ Add `--source` if the source video isn't local yet.
236
+ 2. `video-editing review push --stage edl --job-dir "<job>"` when you start working the round.
237
+ 3. Edit `timeline.json` per the instruction, then
238
+ `video-editing review push --timeline --job-dir "<job>"` after each meaningful change — it
239
+ compiles locally (diagnostics + exit 2 if not clean) and the review page updates live.
240
+ 4. Preview/verify locally exactly as in the workflow above (render preview, CONTENT-VERIFY, QC).
241
+ 5. `video-editing review push --complete --duration <s> --outcomes outcomes.json --job-dir "<job>"`
242
+ — compiles fresh (must be clean) and delivers the terminal payload (edl + timeline + plan).
243
+ `outcomes.json` is `[{"change_id": "chg_…", "ok": true, "note": "…"}]`, one entry per
244
+ `[chg_…]` marker in the instruction; omit `--outcomes` when there are no markers.
245
+
246
+ ## References
247
+
248
+ - `references/cli-reference.md` — every subcommand + flag (incl. `verify`, `inspect`).
249
+ - `references/timeline-v2-vocabulary.md` — the full timeline.json v2 vocabulary, one JSON
250
+ example per feature (word styling, karaoke, zoom ramps, stickers, enter/exit, grade).
251
+ - `references/editing-brief.md` — the full EDL-authoring procedure.
252
+ - `references/hard-rules.md` — production-correctness rules + anti-patterns.
253
+ - `references/scribe-collapse.md` — why retakes hide in the transcript (Gate 1).
254
+ - `references/adversarial-review.md` — the content-verify pass (Gate 2).
@@ -0,0 +1,44 @@
1
+ # Gate 2 — content-verify after preview
2
+
3
+ A single editing pass leaves retakes in. The reliable failure mode: the abandoned
4
+ first attempt and the keeper render as **one continuous clean-audio span**, so a
5
+ waveform / visual self-eval looks perfect while the *content* is duplicated across
6
+ the join. The fix is to **re-transcribe the rendered preview at each join** — which
7
+ is exactly what `video-editing verify` does. Run it for every real cut.
8
+
9
+ ## Run it
10
+
11
+ ```
12
+ video-editing verify --job-dir "<job>" --json
13
+ ```
14
+
15
+ - Default (per-join): re-transcribes a ~6s window centered on each cut boundary
16
+ (from `cut_boundaries.json`), **healing OFF**.
17
+ - `--full`: re-transcribes the whole preview and also checks head/tail dead air.
18
+
19
+ It reports, per join:
20
+ - **duplicated phrase across the join** → a retake left in (severity `error`). It only
21
+ flags **near-adjacent** duplication (the retake fingerprint), not legitimate topical
22
+ repeats spaced apart.
23
+ - **over-long gap straddling the join** → dead air not tightened (warning, or error if large).
24
+
25
+ Writes `verify.json` (same shape family as `review.json`); exit code 2 on `needs-fixes`.
26
+
27
+ ## Adjudicate + fix
28
+
29
+ For any finding, eyeball it: `video-editing inspect --job-dir "<job>" --join <i>` and read
30
+ the PNG (clipped word, leftover false-start syllable, audio pop, broken frame?).
31
+
32
+ Then FIX the EDL and re-render + re-verify:
33
+ - **Retake still present** → set that range's END at the **last clean word BEFORE the
34
+ abandoned attempt**.
35
+ - **Intentional repeat wrongly cut** → restore it.
36
+ - **Clipped boundary** → re-snap to the transcript word boundary.
37
+ - **Leading / trailing dead air** → trim it.
38
+
39
+ Re-snap every edge to a word boundary; keep pads tight (30–200ms); keep `grade:"none"`,
40
+ `overlays:[]`; update `total_duration_s`. Re-render `--mode preview`, re-run `verify`.
41
+ Cap at ~3 fix passes; if findings persist, surface them rather than looping forever.
42
+
43
+ A real retake left in / an intentional repeat wrongly cut / a clipped word at a join /
44
+ leading-or-trailing dead air ⇒ not clean. Minor taste calls are not defects.
@@ -0,0 +1,181 @@
1
+ # `video-editing` CLI reference
2
+
3
+ All execution goes through this CLI. Quote paths (they contain spaces).
4
+
5
+ ## Pipeline commands
6
+
7
+ - **`init-job --source <video> [--job-dir <dir>] [--force]`**
8
+ Probe the source and create the job dir + `source.json`. Always pass `--job-dir`
9
+ explicitly on the sandbox (e.g. `/vercel/sandbox/jobs/<job_id>`) — there is no
10
+ `~/Downloads` to default into. Prints the job dir.
11
+
12
+ - **`transcribe (--source <video> | --job-dir <dir>) [--language <iso>] [--num-speakers <n>] [--no-heal] [--force] [--api-key <key>]`**
13
+ Word-level ElevenLabs Scribe transcription (auto-heals collapsed/dropped spans)
14
+ + screening artifacts: `flags.txt`, `takes_packed.md`, `word_dump.txt`, `coverage.json`.
15
+ Cached per source — `--force` re-transcribes (paid). `--no-heal` disables healing.
16
+
17
+ - **`cut-silences (--source|--job-dir) [--min-gap 0.30] [--target-gap 0.13] [--lead-pad 0.05] [--trail-pad 0.08] [--render preview|none] [--force-transcribe]`**
18
+ Prepare a silence-only EDL-authoring prompt (`prompts/cut-silences.prompt.md`);
19
+ renders a preview if `edl.json` already exists.
20
+
21
+ - **`cut-bad-retakes (--source|--job-dir) [--with-sound-waves] [--quality-review] [--render preview|none] [--language] [--num-speakers] [--force-transcribe]`**
22
+ Prepare the full author-edl prompt (`prompts/author-edl.prompt.md`). `--with-sound-waves`
23
+ writes join QC PNGs (needs `edl.json` + `preview.mp4`). `--quality-review` writes the review prompt.
24
+
25
+ - **`desilence --job-dir <dir> [--noise-db -36] [--min-silence 0.14] [--lead-pad 0.04] [--trail-pad 0.05] [--render preview|none] [--force]`** ← Gate 3
26
+ Amplitude silence-trim. Runs `silencedetect=noise=<db>dB` over each EDL source, then for every
27
+ keep-range **excises internal silences ≥ `--min-silence`** (leaving `lead-pad + trail-pad` of air)
28
+ and **trims silent leading/trailing edges**. Catches silence the transcript can't see (dead air
29
+ baked inside a stretched word token, room tone, breaths). **Transcript-aware** — never splits inside
30
+ a single word token (spelled acronyms, held vowels, numbers). Backs the original EDL up to
31
+ `edl.raw.json`, rewrites `edl.json`, marks it `desilenced`. Idempotent. Higher `--noise-db`
32
+ (e.g. −34) + lower `--min-silence` = more aggressive.
33
+ **Runs automatically at the start of every `render`** (skip with `render … --no-desilence`); the
34
+ standalone command is for tuning + inspecting the result before rendering.
35
+
36
+ - **`render --job-dir <dir> --mode preview|draft|final [--build-subtitles] [--no-subtitles] [--no-loudnorm] [--no-desilence]`**
37
+ Render the EDL: **desilence (unless `--no-desilence`)** → per-segment extract → lossless concat →
38
+ (optional subs) → loudnorm (−14 LUFS / −1 dBTP / LRA 11). preview=CRF22/1080p, draft=CRF28/720p,
39
+ final=CRF20/1080p. Writes `preview.mp4`/`draft.mp4`/`final.mp4` + `cut_boundaries.json`.
40
+
41
+ ## QC / verification commands
42
+
43
+ - **`verify --job-dir <dir> [--full] [--window 6] [--gap 0.6] [--json] [--api-key <key>]`** ← Gate 4 (content)
44
+ Re-transcribes the rendered preview with healing OFF and reports, per join,
45
+ **duplicated phrases** and **over-long gaps** (dead air not tightened → warning/error).
46
+ A duplicate of **≥3 words** = a retake left in → **error**. A bare **2-word** repeat = **warning**
47
+ (spelled acronym / number / emphasis like "WW" → "דבל יו דבל יו" — adjudicate with `inspect`, usually safe).
48
+ Default: per-join ~6s windows from `cut_boundaries.json`; `--full` re-transcribes the whole preview and
49
+ also checks head/tail dead air. Writes `verify.json`; exit code 2 if `needs-fixes`.
50
+
51
+ - **`inspect --job-dir <dir> (--join <i> | --start <s> --end <s>) [--video preview|vertical_src] [--transcript <path>] [-o <png>] [--n-frames 10]`**
52
+ Filmstrip + waveform + word-label PNG for a window or a cut-boundary index.
53
+ `timeline` is an alias. Use it to adjudicate a verify finding or pick a cut edge.
54
+
55
+ ## TimelineV2 commands (`timeline.json` v2 — preferred when present)
56
+
57
+ Exit codes for this group: `0` ok, `1` error, `2` validation failed, `3` ambiguous anchor.
58
+
59
+ - **`timeline validate --job-dir <dir> [--json]`**
60
+ Schema + semantic + capability validation of `<job>/timeline.json`. Prints errors/warnings and
61
+ the capabilities the doc `uses`. Exit `2` if invalid.
62
+
63
+ - **`timeline compile --job-dir <dir> [--json]`**
64
+ Compile `timeline.json` → `resolved/plan.json` + `resolved/diagnostics.json` + **legacy `edl.json`**
65
+ (so render/verify are unchanged). Iterate until it exits `0`. Exit `2` = validation failed,
66
+ `3` = an ambiguous phrase anchor (disambiguate with `timeline resolve`). Never hand-edit `resolved/`.
67
+
68
+ - **`timeline resolve --job-dir <dir> --phrase <text> [--near <srcSeconds>] [--occurrence <n>] [--source <name>] [--json]`**
69
+ Resolve a transcript phrase to **exact source seconds** (normalized, word-accurate match). The
70
+ ONLY way to turn a phrase into an anchor time — never eyeball `word_dump.txt`. Multiple hits →
71
+ exit `3` with per-occurrence context; pick one with `--near` (nearest source time) or
72
+ `--occurrence` (1-based). Not found → fuzzy suggestions, exit `1`.
73
+
74
+ - **`timeline captions generate --job-dir <dir> [--preset grouped|opus-karaoke] [--json]`**
75
+ Enable captions in `timeline.json` (preset only — cues are **derived at compile**, not hand-authored).
76
+ Default preset `grouped`: up to `captions.style.maxWords` (default 4) words per cue, breaking on
77
+ word gaps >0.6s, cut joins, and sentence-ending `.` `?` `!`; `opus-karaoke` is the word-by-word
78
+ opt-in. Style knobs in `captions.style`: `maxWords` (4), `leadS` (0.08 — cues display that early,
79
+ clamped), `holdS` (0.9 — cues hold to bridge the gap to the next cue). Reports the derived cue
80
+ count via a dry compile.
81
+
82
+ - **`compose --job-dir <dir> [--render] [--quality draft|standard|high] [--fps 30] [--out <path>] [--proxy-crf 23] [--json]`**
83
+ Build a local HyperFrames composition from `resolved/plan.json` (cut + captions/zooms/overlays)
84
+ in `<job>/composition/`: `media/source.mp4` (compressed proxy of `vertical_src.mp4`, libx264
85
+ CRF `--proxy-crf`, skipped when up to date), `media/audio.m4a` (the CONTINUOUS cut audio
86
+ extracted from `preview.mp4` — the QC preview must exist), referenced image/sticker assets
87
+ copied under `media/` (preserving their job-dir-relative paths), and `index.html` (with a
88
+ generated GSAP timeline when the plan uses animated vocabulary). Without `--render`,
89
+ prints the composition dir + the exact `hyperframes render` command. With `--render`, runs
90
+ `hyperframes render --quality <q> --fps <n> --output <out>` (default `<job>/composed.mp4`)
91
+ in the composition dir — requires the `hyperframes` CLI on PATH (`npm install -g hyperframes`).
92
+ `--json` prints `{compositionDir, entry, out?, segments, cues, zooms, overlays}`. This is the
93
+ LOCAL delivery path for compiled captions/zooms — the ffmpeg `render` is cut-only; never use
94
+ `--build-subtitles` for timeline-v2 jobs.
95
+
96
+ ## Cloud thin-client commands (clipready render/QA/artifact API)
97
+
98
+ Every cloud verb resolves credentials the same way and supports `--json` (machine shape;
99
+ exit `0` ok, `1` error). Resolution precedence:
100
+
101
+ - **base**: `--api-base` → env `CLIPREADY_API_BASE` → env `API_BASE`
102
+ - **credential** (sent as `Authorization: Bearer <credential>`): `--api-key` → env `CLIPREADY_API_KEY` → env `RUN_TOKEN`
103
+ - **video id**: `--video-id` → env `VIDEO_ID`
104
+
105
+ A missing base / credential / (required) video id is a clear error → exit `1`.
106
+
107
+ - **`cloud render --job-dir <dir> --mode preview|final [--wait] [--poll-interval 10] [--out <path>] [--json]`**
108
+ Submit a render. Without `--wait`: prints `render_id` and exits `0`. With `--wait`: polls status
109
+ (max 30 min) until terminal; on `completed`, downloads the signed `video_url` to `--out`
110
+ (default `<job>/preview.mp4` or `<job>/final.mp4`). `failed` → exit `1` with the error. Needs a video id.
111
+
112
+ - **`cloud status <render_id> [--json]`**
113
+ One status GET: `pending|running|completed|failed` (+ `video_url` when completed). `failed` → exit `1`.
114
+
115
+ - **`qa frames --start <s> --end <s> [--cols <n>] [--interval <s>] [--job-dir <dir>] [--json]`**
116
+ Server-render a filmstrip PNG for the window and download it to `<job>/qa/frames_<start>_<end>.png`
117
+ (`./qa/` when no `--job-dir`). Prints the local path + grid metadata (Read the PNG to inspect). Needs a video id.
118
+
119
+ - **`qa waveform --start <s> --end <s> [--job-dir <dir>] [--json]`**
120
+ Same, for a waveform: downloads `<job>/qa/waveform_<start>_<end>.png` and writes the word list to
121
+ `<job>/qa/waveform_<start>_<end>.words.json`. Prints both paths. Needs a video id.
122
+
123
+ - **`job pull [--job-dir <dir>] [--only <name,name>] [--json]`**
124
+ List the video's artifacts and fetch each (or the `--only` subset) into the job dir (default cwd),
125
+ **preserving relative paths** (`resolved/plan.json` → `<job>/resolved/plan.json`). Prints a summary.
126
+ `.json` artifacts are written pretty-printed; others verbatim. Needs a video id. Allowlisted names include
127
+ `word_dump.txt`, `takes_packed.md`, `flags.txt`, `coverage.json`, `edl.json`, `verify.json`,
128
+ `cut_boundaries.json`, `timeline.json`, `resolved/plan.json`, `resolved/diagnostics.json`.
129
+
130
+ - **`review pull [--job-dir <dir>] [--source] [--json]`**
131
+ Fetch the clipready review session for the video into the job dir (default cwd): writes
132
+ `<job>/review/round.json` (round/run ids, mode, status, requested changes) and
133
+ `<job>/review/instruction.md` (the round's editorial instruction — or a "no active round" note).
134
+ Writes `<job>/timeline.json` from the session ONLY when no local timeline.json exists (never
135
+ overwrites yours). With `--source`, also downloads the source video (to the `source.json`
136
+ filename, else `<job>/source.mp4`) when absent locally. Needs a video id.
137
+
138
+ - **`review push (--stage <type> [--message <text>] | --timeline | --complete --duration <s> [--outcomes <file>]) [--job-dir <dir>] [--run-id <id>] [--json]`**
139
+ Push review-round events (exactly one mode per call; run id defaults to `review/round.json`
140
+ from `review pull`). `--stage` posts a progress event
141
+ (`provisioning|transcribing|edl|render_preview|qc|render_final|uploading|log`), optional
142
+ `{message}` payload. `--timeline` compiles `<job>/timeline.json` locally (same sequence as
143
+ `timeline compile`, needs ffmpeg) and pushes a `timeline_update` with the raw timeline JSON,
144
+ the compiled plan, and its hash — the review page updates live. `--complete` compiles fresh
145
+ (must be clean) and pushes the terminal `completed` event
146
+ (`{edl, timeline, plan, duration_s, outcomes?}`; `--outcomes` is a JSON file of
147
+ `[{change_id, ok, note?}]`, bare array or `{outcomes: [...]}`). Compile failures print
148
+ diagnostics and exit `2` (nothing is pushed); cloud errors exit `1`. Needs a video id.
149
+ Both `--timeline` and `--complete` **upload the timeline's referenced assets
150
+ automatically** (image/sticker overlays, audio beds): each ref is sha256-cached in
151
+ `<job>/review/assets.json` so unchanged files are never re-uploaded, and the push
152
+ payload includes an `assets: {ref: asset_id}` manifest. A missing asset file fails the
153
+ compile (`asset-missing`) before anything is uploaded.
154
+
155
+ - **`skill install [--target <dir>] [--json]`**
156
+ Copy the bundled `video-editing` agent skill into `<target|~/.claude/skills>/video-editing`
157
+ (overwriting). Prints what was installed.
158
+
159
+ ## Utility
160
+
161
+ - **`--version`** — print the CLI version.
162
+ - **`status --job-dir <dir> [--json]`** — job state + artifact existence.
163
+ - **`review prompt --job-dir <dir> [--write-warnings]`** — write the review prompt;
164
+ optionally convert `review.json` → `warnings.json`. (`--open-app` is not usable on the
165
+ sandbox — it launches a local browser; skip it, there is no display.) This was the old
166
+ top-level `review` verb — `review` is now a group (`prompt` / `pull` / `push`).
167
+ - **`config set-key <key>` / `config path`** — manage the ElevenLabs key
168
+ (`~/.config/video-editing/.env`; on the sandbox the CLI reads env `ELEVENLABS_API_KEY`,
169
+ no need to run `config set-key`).
170
+
171
+ ## Typical session
172
+
173
+ ```
174
+ video-editing init-job --source "source.mp4" --job-dir /vercel/sandbox/jobs/<job_id>
175
+ video-editing transcribe --job-dir /vercel/sandbox/jobs/<job_id>
176
+ # read flags.txt (Gate 1), author edl.json
177
+ video-editing render --job-dir /vercel/sandbox/jobs/<job_id> --mode preview
178
+ video-editing verify --job-dir /vercel/sandbox/jobs/<job_id> --json # Gate 2
179
+ # fix edl.json as needed, re-render, re-verify
180
+ video-editing render --job-dir /vercel/sandbox/jobs/<job_id> --mode final
181
+ ```
@@ -0,0 +1,44 @@
1
+ # Editing brief — authoring the EDL
2
+
3
+ The standing style for a single talking-head vertical reel and the exact
4
+ step-by-step for authoring `<job>/edl.json`. All execution is via the
5
+ `video-editing` CLI; you only write the EDL.
6
+
7
+ ## Standing brief (apply exactly)
8
+
9
+ - **Output VERTICAL 9:16.** The CLI bakes `vertical_src.mp4`; point the EDL at it.
10
+ - **CUT:** bad retakes, false starts, cut-off / stuttered words — keep the **LAST
11
+ complete take**, EXCEPT keep the **CLEANEST** take when an earlier take is clean
12
+ and a later one stutters into completion.
13
+ - **TIGHTEN EVERY PAUSE:** trim 0.3–1s inter-phrase gaps down to ~0.12–0.14s of air.
14
+ - **COLD-OPEN** on the strongest hook (trim weak/dead intro).
15
+ - **TRIM outro dead air** (footsteps / ambient / silence / trailing breaths / device noise).
16
+ - **CLEAN CUTS ONLY:** `grade:"none"`, no subtitles, no overlays.
17
+ - **KEEP intentional rhetorical repeats.**
18
+
19
+ ### Retake-vs-intentional discriminator (for a flagged repeated phrase)
20
+
21
+ - **RETAKE** (cut it, keep the last/cleanest complete pass) if the earlier
22
+ instance cuts off mid-word (a fragment / trailing "-") OR is immediately restarted.
23
+ - **INTENTIONAL** (keep BOTH) if both instances are complete and serve rhetoric /
24
+ parallel structure and are separated by other content.
25
+ - Decide every flagged repeat — never blind-cut.
26
+
27
+ ## Steps
28
+
29
+ 1. **Init + transcribe + screen** (CLI does these): `init-job` → `transcribe`.
30
+ Then read `<job>/flags.txt` (Gate 1) and confirm `<job>/coverage.json`.
31
+ 2. **Read the word-level transcript** at `<job>/word_dump.txt` (every word/audio_event
32
+ as `[start-end] type: text` with `---- gap Xs ----` at gaps ≥0.30s). THINK HARD and
33
+ list, with exact timestamps: every retake/false-start/stutter (and which take to KEEP),
34
+ every silence (head/inter-phrase/outro), the cold-open point, the intentional repeats.
35
+ When text and audio seem to disagree, `video-editing inspect --video vertical_src --start <a> --end <b>` and read the PNG.
36
+ 3. **Write `<job>/edl.json`** — KEEP segments in source seconds; gaps between them are
37
+ the cuts. Snap every in/out to a transcript word boundary (lead ~0.05s, trail ~0.08s;
38
+ ~0.03–0.05s at retake joins, clamped to stay inside the silence and never include a
39
+ false-start word). Ranges chronological, non-overlapping. `total_duration_s` = Σ(end−start).
40
+ **A retake range must END at the LAST CLEAN WORD before the abandoned attempt.**
41
+ 4. **Render** `--mode preview`, then **verify** (Gate 2), fix, repeat. Finalize `--mode final`.
42
+
43
+ See `cli-reference.md` for exact commands, `hard-rules.md` for correctness, and
44
+ `scribe-collapse.md` / `adversarial-review.md` for the two gates.
@@ -0,0 +1,38 @@
1
+ # Hard rules (production correctness — non-negotiable)
2
+
3
+ These produce **silent failures** if violated. The CLI enforces the rendering
4
+ rules internally; the EDL-authoring rules are on you.
5
+
6
+ ## The CLI guarantees (do not try to replicate by hand)
7
+
8
+ - **Per-segment extract → lossless `-c copy` concat** (no double-encode).
9
+ - **30ms audio fades at every segment boundary** (`afade` in/out) — which is why
10
+ tight pads (down to ~30ms) at retake joins are safe.
11
+ - **Loudnorm** to −14 LUFS / −1 dBTP / LRA 11 (two-pass for final, one-pass for preview).
12
+ - Subtitles, if ever enabled, are applied LAST in the filter chain.
13
+ - Word-level verbatim ASR with auto-heal; transcripts cached per source.
14
+
15
+ ## EDL-authoring rules (yours to uphold)
16
+
17
+ - **Never cut inside a word.** Snap every edge to a transcript word boundary.
18
+ - **Pad every cut edge** (30–200ms). Scribe timestamps drift 50–100ms; padding absorbs it.
19
+ Tightest (~0.03–0.05s) at retake joins, staying inside the silence.
20
+ - A **retake range ends at the last clean word BEFORE the abandoned attempt.**
21
+ - Clipped tokens ending in `-` (`s-`, `re-`, `li-`) are danger zones — leave an 80–150ms
22
+ guard band before them when there's room.
23
+ - Keep the first token of the chosen take even if it's a connector ("and/so/but").
24
+ - `grade:"none"`, `overlays:[]`, no subtitles (clean-cut pass).
25
+ - Keep `total_duration_s` = Σ(end−start); ranges chronological + non-overlapping.
26
+
27
+ ## The two mandatory gates
28
+
29
+ 1. **SCREEN before cutting** — read `flags.txt`; resolve every [A], judge every [B]. See `scribe-collapse.md`.
30
+ 2. **CONTENT-VERIFY after preview** — `video-editing verify`; a visual/waveform check alone
31
+ misses a retake left in. See `adversarial-review.md`.
32
+
33
+ ## Anti-patterns (consistently fail)
34
+
35
+ - Trusting the transcript without screening it (Scribe collapses/drops hide retakes).
36
+ - Running ffmpeg or ad-hoc helper scripts by hand — forbidden; use only the `video-editing` CLI.
37
+ - A visual-only check at a retake join (the duplicated content reads as continuous clean audio).
38
+ - Re-transcribing an unchanged source (paid + pointless; it's cached).
@@ -0,0 +1,37 @@
1
+ # Gate 1 — the Scribe collapse / drop gotcha
2
+
3
+ The single most important thing before reasoning about cuts: **retakes hide in
4
+ the transcript.** Trust the text without screening and you will leave duplicated
5
+ content in the cut.
6
+
7
+ ## What happens
8
+
9
+ When Scribe hits a hard-to-segment chunk (fast speech, a stutter, a false start
10
+ immediately followed by the retake), it sometimes:
11
+
12
+ - **Collapses** the chunk into ONE over-long token (a "word" lasting >~1.2s), or
13
+ - **Drops** a stretch of audio silently — no tokens for that span.
14
+
15
+ Either way the retake is invisible in the text, a waveform looks fine, and the
16
+ cut "reads" clean — but the abandoned take is still in the render.
17
+
18
+ ## How the CLI + you catch it
19
+
20
+ 1. **`video-editing transcribe` auto-heals** most collapses/drops (re-transcribes the
21
+ suspect window in isolation and splices the recovered words back). Watch its log.
22
+ 2. **`video-editing transcribe` writes `flags.txt`** — read it (Gate 1):
23
+ - **[A]** over-long / collapsed tokens (>~1.2s) — content is hidden inside one of these.
24
+ - **[B]** repeated phrases — the retake fingerprint (false starts, reworded restarts).
25
+ - **[C]** audio events + big gaps.
26
+ Resolve every [A]; judge every [B] (retake vs intentional).
27
+ 3. **Verify coverage** via `coverage.json`: the last word must end within ~2s of the
28
+ media duration. If it ends well before EOF, a tail was dropped.
29
+
30
+ ## When a span is still suspect
31
+
32
+ Inspect it: `video-editing inspect --job-dir <job> --video vertical_src --start <a> --end <b>`
33
+ and read the filmstrip+waveform PNG to see what was actually said before choosing the cut.
34
+
35
+ > Screening + inspecting suspicious windows is what isolates retakes Scribe collapsed
36
+ > into one token. Skipping it is the #1 cause of a "clean-looking" cut that still has a
37
+ > retake in it.