video-editing 2.0.0 → 3.1.0

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.
package/dist/index.js CHANGED
@@ -4,51 +4,55 @@ import {
4
4
  CAPTION_STYLE_DEFAULTS,
5
5
  CloudClient,
6
6
  CloudHttpError,
7
- DEFAULT_DESILENCE,
8
7
  DEFAULT_DOWNLOAD_TIMEOUT_MS,
9
8
  DEFAULT_TIMEOUT_MS,
10
9
  INLINE_MAX_BYTES,
11
10
  MAX_POLL_MS,
12
11
  MAX_RUN_POLL_MS,
12
+ MULTIPART_THRESHOLD_BYTES,
13
+ RELAY_DIRECT_ATTEMPTS,
14
+ RELAY_SUBCHUNK_BYTES,
13
15
  REVIEW_STAGES2,
14
16
  assertRunCompleted,
15
17
  assetContentType2,
16
18
  assetsUrl,
17
- buildMediaEntry,
18
19
  bundledSkillDir,
20
+ bytesSource,
19
21
  checkCapabilities,
20
22
  collectPlanAssetRefs2,
21
23
  compileRemote,
22
24
  compileUrl,
23
- composeCloud,
24
25
  composeUrl,
25
26
  defaultRenderOut,
26
27
  defaultSkillTarget,
27
- detectSilences,
28
- detectSilencesForDoc,
29
28
  diagnosticsPath2,
30
29
  downloadTo,
31
30
  emptyDiagnostics,
32
- extractGuardedWav,
33
31
  filesUrl,
34
- findLocalTranscript,
35
32
  formatRunEvent,
33
+ framesPngPath,
34
+ ingestArtifactSet,
35
+ ingestUrl,
36
36
  initCloudJob,
37
37
  installSkill,
38
38
  instructionPath2,
39
39
  isTerminalRunStatus,
40
40
  isTerminalStatus,
41
41
  jobJsonPath,
42
+ joinSourceWindows,
42
43
  loadPublicConfigEnv,
43
44
  loadTimeline2,
44
45
  meUrl,
46
+ multipartRelayUrl,
47
+ multipartUrl,
48
+ openFileSource,
49
+ outWindowToSourceWindows,
45
50
  parseFileContent,
46
51
  parseFileList,
47
52
  parseFilePresign,
48
53
  parseRenderCreate,
49
54
  parseRenderState,
50
55
  parseRunState,
51
- parseSilenceDetect,
52
56
  parseTimeline,
53
57
  planPath2,
54
58
  pollRender,
@@ -63,13 +67,11 @@ import {
63
67
  pushFiles,
64
68
  pushStage2,
65
69
  pushTimeline2,
66
- qaFramesArgs,
67
- qaFramesLocal,
68
- qaWaveformArgs,
69
- qaWaveformLocal,
70
- qaWordsForWindow,
70
+ qaDir,
71
71
  readJobFile,
72
72
  readOutcomesFile2,
73
+ readPlanSegments,
74
+ readProbeDuration,
73
75
  renderUrl,
74
76
  rendersUrl,
75
77
  resolveApiBase,
@@ -83,9 +85,10 @@ import {
83
85
  roundJsonPath2,
84
86
  runUrl,
85
87
  sameSha,
88
+ sanitizeStem,
86
89
  savePublicConfigKey,
87
90
  sha256Hex,
88
- silenceDetectArgs,
91
+ shouldMultipart,
89
92
  statAssetsForDoc,
90
93
  stripTrailingSlash,
91
94
  syncAssets2,
@@ -95,7 +98,9 @@ import {
95
98
  transcribeArtifactSet,
96
99
  transcribeCloud,
97
100
  transcribeUrl,
101
+ uploadMultipart,
98
102
  uploadVfsBytes,
103
+ uploadVfsPayload,
99
104
  usesFromDoc,
100
105
  validateLocal,
101
106
  validateTimeline,
@@ -104,8 +109,10 @@ import {
104
109
  vfsContentType,
105
110
  vfsLocalPath,
106
111
  videoIdFromJob,
112
+ waveformPngPath,
113
+ waveformWordsPath,
107
114
  writeJobFile
108
- } from "./chunk-SSXAUKGV.js";
115
+ } from "./chunk-2OM7WV4W.js";
109
116
 
110
117
  // src/timeline-v2/anchors.ts
111
118
  function normalizeToken(text) {
@@ -227,55 +234,59 @@ export {
227
234
  CAPTION_STYLE_DEFAULTS,
228
235
  CloudClient,
229
236
  CloudHttpError,
230
- DEFAULT_DESILENCE,
231
237
  DEFAULT_DOWNLOAD_TIMEOUT_MS,
232
238
  DEFAULT_TIMEOUT_MS,
233
239
  INLINE_MAX_BYTES,
234
240
  MAX_POLL_MS,
235
241
  MAX_RUN_POLL_MS,
242
+ MULTIPART_THRESHOLD_BYTES,
243
+ RELAY_DIRECT_ATTEMPTS,
244
+ RELAY_SUBCHUNK_BYTES,
236
245
  REVIEW_STAGES2,
237
246
  assertRunCompleted,
238
247
  assetContentType2,
239
248
  assetsUrl,
240
- buildMediaEntry,
241
249
  bundledSkillDir,
250
+ bytesSource,
242
251
  checkCapabilities,
243
252
  collectPlanAssetRefs2,
244
253
  compileRemote,
245
254
  compileUrl,
246
- composeCloud,
247
255
  composeUrl,
248
256
  defaultRenderOut,
249
257
  defaultSkillTarget,
250
- detectSilences,
251
- detectSilencesForDoc,
252
258
  diagnosticsPath2,
253
259
  downloadTo,
254
260
  emptyDiagnostics,
255
- extractGuardedWav,
256
261
  filesUrl,
257
- findLocalTranscript,
258
262
  findPhrase,
259
263
  formatRunEvent,
264
+ framesPngPath,
260
265
  fuzzySuggestions,
266
+ ingestArtifactSet,
267
+ ingestUrl,
261
268
  initCloudJob,
262
269
  installSkill,
263
270
  instructionPath2,
264
271
  isTerminalRunStatus,
265
272
  isTerminalStatus,
266
273
  jobJsonPath,
274
+ joinSourceWindows,
267
275
  loadPublicConfigEnv,
268
276
  loadTimeline2,
269
277
  meUrl,
278
+ multipartRelayUrl,
279
+ multipartUrl,
270
280
  normalizePhrase,
271
281
  normalizeToken,
282
+ openFileSource,
283
+ outWindowToSourceWindows,
272
284
  parseFileContent,
273
285
  parseFileList,
274
286
  parseFilePresign,
275
287
  parseRenderCreate,
276
288
  parseRenderState,
277
289
  parseRunState,
278
- parseSilenceDetect,
279
290
  parseTimeline,
280
291
  planPath2,
281
292
  pollRender,
@@ -290,13 +301,11 @@ export {
290
301
  pushFiles,
291
302
  pushStage2,
292
303
  pushTimeline2,
293
- qaFramesArgs,
294
- qaFramesLocal,
295
- qaWaveformArgs,
296
- qaWaveformLocal,
297
- qaWordsForWindow,
304
+ qaDir,
298
305
  readJobFile,
299
306
  readOutcomesFile2,
307
+ readPlanSegments,
308
+ readProbeDuration,
300
309
  renderUrl,
301
310
  rendersUrl,
302
311
  resolveApiBase,
@@ -311,9 +320,10 @@ export {
311
320
  roundJsonPath2,
312
321
  runUrl,
313
322
  sameSha,
323
+ sanitizeStem,
314
324
  savePublicConfigKey,
315
325
  sha256Hex,
316
- silenceDetectArgs,
326
+ shouldMultipart,
317
327
  statAssetsForDoc,
318
328
  stripTrailingSlash,
319
329
  syncAssets2,
@@ -323,7 +333,9 @@ export {
323
333
  transcribeArtifactSet,
324
334
  transcribeCloud,
325
335
  transcribeUrl,
336
+ uploadMultipart,
326
337
  uploadVfsBytes,
338
+ uploadVfsPayload,
327
339
  usesFromDoc,
328
340
  validateLocal,
329
341
  validateTimeline,
@@ -332,6 +344,8 @@ export {
332
344
  vfsContentType,
333
345
  vfsLocalPath,
334
346
  videoIdFromJob,
347
+ waveformPngPath,
348
+ waveformWordsPath,
335
349
  wordStream,
336
350
  writeJobFile
337
351
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "video-editing",
3
- "version": "2.0.0",
4
- "description": "Cloud CLI + client for editing talking-head reels with clipready: transcription, timeline authoring, verify, compose, QA and renders heavy lifting runs in the cloud.",
3
+ "version": "3.1.0",
4
+ "description": "Pure-API cloud CLI + client for editing talking-head reels with clipready: upload once, then transcription, timeline authoring, compile, render, verify and QA all run in the cloud. Node ≥ 20 + an API key — no ffmpeg.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "Itay Elgazar",
@@ -1,44 +1,47 @@
1
1
  ---
2
2
  name: video-editing
3
3
  description: >-
4
- Cloud-first agent workflow for editing a talking-head video into a tight,
5
- clean cut with the `video-editing` CLI + the ClipReady cloud. Requires a
6
- ClipReady API key (`CLIPREADY_API_KEY`) every command talks to the cloud.
7
- Transcription and editorial screening run server-side (no ElevenLabs key
8
- needed); the editorial brief arrives from the server as pulled
9
- `prompts/*.md`; the timeline compiles in the cloud; rendering and QA
10
- visuals run locally on your machine (ffmpeg + hyperframes). Use whenever
11
- the user drops a talking-head clip / reel / vlog / interview and wants it
12
- edited or tightened — "edit this reel", "cut the bad retakes", "clean up
13
- this recording", "tighten this", "add captions", or just a video path with
14
- an editing ask — or when working a ClipReady review round. Handles
15
- Hebrew/RTL.
4
+ Cloud agent workflow for editing a talking-head video into a tight, clean
5
+ cut with the `video-editing` CLI + the ClipReady cloud. The CLI is a PURE
6
+ API clientrequirements are Node 20 and a ClipReady API key
7
+ (`CLIPREADY_API_KEY`) only; no ffmpeg, no ffprobe, no local rendering
8
+ tools. The source uploads once at init; transcription, editorial
9
+ screening, timeline compilation, rendering, verification, and QA visuals
10
+ all run server-side; the editorial brief arrives from the server as pulled
11
+ `prompts/*.md`. Use whenever the user drops a talking-head clip / reel /
12
+ vlog / interview and wants it edited or tightened — "edit this reel",
13
+ "cut the bad retakes", "clean up this recording", "tighten this",
14
+ "add captions", or just a video path with an editing ask — or when
15
+ working a ClipReady review round. Handles Hebrew/RTL.
16
16
  ---
17
17
 
18
- # Video Editing (ClipReady cloud + local render)
18
+ # Video Editing (ClipReady cloud pure API client)
19
19
 
20
- Turn a raw talking-head recording into a tight, clean cut. The **`video-editing`**
21
- CLI is a thin client for the ClipReady cloud: the cloud owns transcription,
22
- screening, editorial prompts, timeline compilation, and verification; your
23
- machine owns rendering and QA visuals. **You** read the pulled transcript +
24
- brief and author `timeline.json`; the CLI does everything executable.
20
+ Turn a raw talking-head recording into a tight, clean cut. The
21
+ **`video-editing`** CLI (v3) is a pure API client for the ClipReady cloud: the
22
+ cloud owns EVERYTHING executable — probing, transcription, screening,
23
+ editorial prompts, timeline compilation, rendering, verification, and QA
24
+ visuals. Your machine only streams the source up once and pulls small
25
+ artifacts back. **You** read the pulled transcript + brief and author
26
+ `timeline.json`; the CLI does the rest over HTTP.
25
27
 
26
28
  ## Install & auth
27
29
 
28
30
  ```bash
29
- npm install -g video-editing # the CLI (v2)
31
+ npm install -g video-editing # the CLI (v3)
30
32
  npx skills add ItayElgazar/clipready --skill video-editing # this skill
31
33
  ```
32
34
 
33
- Requirements:
35
+ Requirements — this is the FULL list:
34
36
 
35
37
  - **Node ≥ 20**
36
- - **ffmpeg**/**ffprobe** on PATH (local render + QA visuals)
37
- - **`CLIPREADY_API_KEY`** from the ClipReady settings page
38
- - **`CLIPREADY_API_BASE`** the ClipReady API base URL
38
+ - **A ClipReady API key** one-time login: `video-editing auth api-key` (paste
39
+ the key from the ClipReady settings page; the base URL defaults to the hosted
40
+ cloud). Env vars `CLIPREADY_API_KEY`/`CLIPREADY_API_BASE` override when set.
39
41
 
40
- **No ElevenLabs key.** Transcription runs in the ClipReady cloud with
41
- ClipReady's own transcription — do not set or ask for `ELEVENLABS_API_KEY`.
42
+ **No ffmpeg. No ffprobe. No hyperframes. No ElevenLabs key.** Nothing runs on
43
+ the local machine except the CLI itself — do not install media tools, and do
44
+ not set or ask for `ELEVENLABS_API_KEY`.
42
45
 
43
46
  **Every command fails without the API key.** If a verb errors with a
44
47
  missing-key message, stop and ask the user for their ClipReady API key;
@@ -48,79 +51,111 @@ never work around it.
48
51
 
49
52
  - Use **only** the `video-editing` CLI (`video-editing <subcommand>`). Run
50
53
  `video-editing --help` to see commands.
51
- - **NEVER** run ffmpeg/ffprobe/transcription by hand — the CLI owns all execution.
52
- - If a capability seems missing, STOP and report it as a CLI gap. Do not reach around the CLI.
54
+ - **NEVER** run ffmpeg/ffprobe/transcription by hand — there is simply no
55
+ local media in this workflow at all; every frame lives in the cloud. If you
56
+ have a memory of local renders, `compose`, `--master`, or "extract the WAV
57
+ first", that memory is from a retired pipeline — discard it.
58
+ - If a capability seems missing, STOP and report it as a CLI gap. Do not
59
+ reach around the CLI.
53
60
  - Your only hand-authored artifact is `<job>/timeline.json`.
61
+ - **Captions are timeline-authored and rendered by the cloud renderer —
62
+ NEVER ffmpeg subtitle filters, libass, `.srt` sidecars, or `drawtext`.**
54
63
 
55
64
  ## The workflow
56
65
 
57
- 1. **Init** — `video-editing init --video "<path>"` (add `--job-dir "<dir>"` to
58
- choose the working dir). Uploads the source and creates the cloud job;
59
- writes `<job>/job.json` with the `video_id` every later verb uses.
60
- 2. **Transcribe** `video-editing transcribe --job-dir "<job>"`. Uploads the
61
- audio; the cloud transcribes **and screens** it, then the CLI pulls the
62
- results into the job dir: `transcripts/`, `word_dump.txt`,
63
- `takes_packed.md`, `flags.txt`, `coverage.json` — **and
64
- `prompts/author-edl.prompt.md`**, your editorial brief.
65
- 3. **READ the pulled `prompts/*.md` and follow them** they are your
66
+ 1. **Init** — `video-editing init --source "<path>"` (add `--job-dir "<dir>"`
67
+ to choose the working dir). ONE command does everything through
68
+ transcription: it streams the source up (chunked, retried, 2GB-safe),
69
+ creates the cloud job, runs server-side ingest (probe + transcription +
70
+ editorial screening), and pulls the results into the job dir:
71
+ `transcripts/`, `word_dump.txt`, `takes_packed.md`, `flags.txt`,
72
+ `coverage.json`, `probe.json` — **and `prompts/author-edl.prompt.md`**,
73
+ your editorial brief. Writes `<job>/job.json` (the `video_id` every later
74
+ verb uses) and prints the video's web link. There is no separate
75
+ transcribe step — `transcribe` exists only to re-pull/re-run later.
76
+ Filenames with apostrophes, spaces, or unicode are safe — the internal
77
+ stem is sanitized automatically; never rename the user's file.
78
+ 2. **READ the pulled `prompts/*.md` and follow them** — they are your
66
79
  editorial brief (what to cut, what to keep, the screening gate, the pad
67
80
  and boundary rules). They are authoritative and may change between runs;
68
81
  never edit them, never substitute a remembered brief.
69
- 4. **Author `<job>/timeline.json`** per the pulled brief plus
82
+ 3. **Author `<job>/timeline.json`** per the pulled brief plus
70
83
  `references/timeline-v2-vocabulary.md` (cut ranges, captions, zooms,
71
- overlays, audio). Resolve phrase seconds ONLY with
72
- `video-editing timeline resolve --phrase "…" [--near <t> | --occurrence <n>]`
84
+ overlays, audio). The **sources KEY must equal the transcript stem** (the
85
+ filename of `transcripts/<stem>.json`) captions and resolve bind
86
+ through it. Resolve phrase → seconds ONLY with
87
+ `video-editing timeline resolve --phrase "…" [--occurrence <n>]`
73
88
  — never eyeball `word_dump.txt`. Sanity-check locally anytime with
74
89
  `video-editing timeline validate --job-dir "<job>" --json`.
75
- 5. **Compile** — `video-editing timeline compile --job-dir "<job>" --json`.
90
+ 4. **Compile** — `video-editing timeline compile --job-dir "<job>" --json`.
76
91
  Compiles in the cloud; writes `resolved/plan.json` +
77
- `resolved/diagnostics.json`. Read the diagnostics and fix until it exits
78
- `0` (`2` = validation failed fix the reported paths; `3` = ambiguous
79
- anchor disambiguate with `timeline resolve`). Never hand-edit `resolved/`.
80
- 6. **Render the preview** `video-editing compose --job-dir "<job>"` builds
81
- the local HyperFrames composition in `<job>/composition/` (with
82
- `index.html`), then render it locally with the `hyperframes` CLI
83
- (`npm install -g hyperframes`; `compose` prints the exact command, or pass
84
- `--render`). No hyperframes / no local horsepower? Use
85
- `video-editing cloud render --job-dir "<job>" --mode preview --wait` instead.
86
- 7. **Verify** — `video-editing verify --job-dir "<job>" --json`. Cloud
87
- verification of the rendered preview (catches retakes left in across a
88
- join and dead air a visual check misses). Fix `timeline.json`, re-compile,
92
+ `resolved/diagnostics.json` and prints the `View & comment` watch link
93
+ the user can already watch/comment on the compiled cut. Read the
94
+ diagnostics and fix until it exits `0` (`2` = validation failed — fix the
95
+ reported paths; `3` = ambiguous anchor disambiguate with
96
+ `timeline resolve`). Never hand-edit `resolved/`.
97
+ 5. **Render the preview** `video-editing render --job-dir "<job>"`
98
+ (cloud render, preview mode, waits and downloads `<job>/preview.mp4` by
99
+ default; it works right after `timeline compile`). Share the printed
100
+ watch link with the user.
101
+ 6. **Verify** — `video-editing verify --job-dir "<job>" --json`. Fully
102
+ server-side verification of the rendered preview (catches retakes left in
103
+ across a join and dead air a visual check misses); nothing is uploaded.
104
+ Can run while the user reviews. Fix `timeline.json`, re-compile,
89
105
  re-render, re-verify until clean; cap at ~3 fix passes and surface
90
106
  remaining findings rather than looping.
91
- 8. **QA looks** — spot-check any window locally:
92
- `video-editing qa frames --range <a>-<b> --job-dir "<job>"` (filmstrip PNG)
93
- and `video-editing qa waveform --range <a>-<b> --job-dir "<job>"`
94
- (waveform PNG + word-timing sidecar JSON). Local ffmpeg free and fast;
95
- Read the PNGs to adjudicate.
96
- 9. **Final** `video-editing cloud render --job-dir "<job>" --mode final --wait`
97
- (or render the composition locally at final quality).
107
+ 7. **QA looks** — spot-check any window with server-rendered visuals.
108
+ THE close-look tool is
109
+ `video-editing qa inspect --start <a> --end <b> --job-dir "<job>"`:
110
+ one composite PNG with the filmstrip, the waveform, word labels, AND
111
+ the compiled plan's cut regions shaded red ("cut") — everything needed
112
+ to adjudicate a moment in a single image. Same addressing as frames:
113
+ `qa inspect --join <i>` (the two sides of plan join i → `-a`/`-b`
114
+ PNGs) and `qa inspect --out-start <a> --out-end <b>` (OUTPUT seconds
115
+ mapped through the compiled plan). The narrower views still exist:
116
+ `video-editing qa frames …` (filmstrip only, same addressing modes)
117
+ and `video-editing qa waveform --start <a> --end <b>` (waveform PNG +
118
+ word-timing sidecar JSON). Read the PNGs to adjudicate.
119
+ 8. **Final** — `video-editing render --mode final --job-dir "<job>"`.
120
+
121
+ **Optional, any time after compile:** if joins sound clicky, chopped, or gappy,
122
+ run `video-editing smooth-joins --job-dir "<job>"` to apply an equal-power
123
+ audio crossfade to every cut, then re-render. It is NOT part of the sequence
124
+ above — renders are unaffected unless you run it, and it must be re-run after
125
+ each recompile you want it applied to. See `references/audio-joins.md`.
98
126
 
99
127
  ## Your editorial brief arrives from the cloud
100
128
 
101
129
  The deep editorial brief — retake/keep rules, pause tightening, cold-open,
102
130
  screening and verification procedure — is **not in this skill**. It is pulled
103
- from the server into `<job>/prompts/*.md` by `transcribe` (and refreshed by
131
+ from the server into `<job>/prompts/*.md` by `init` (and refreshed by
104
132
  `files pull`). Old references in this skill (`editing-brief.md`,
105
133
  `hard-rules.md`, `adversarial-review.md`, `scribe-collapse.md`) are stubs
106
134
  pointing there. If `prompts/author-edl.prompt.md` is missing, run
107
135
  `video-editing files pull --job-dir "<job>"` — do not improvise a brief.
108
136
 
109
- ## Vertical master
137
+ ## Known caveats
110
138
 
111
- `init --video` uploads the source as-is. When the deliverable is a vertical
112
- 9:16 reel from a non-vertical source, pass `--master` to `init` so the
113
- vertical master is baked before upload (needs local ffmpeg). Check
114
- `video-editing init --help` for the current flag surface.
139
+ - **`verify` over-triggers on thematically narrow monologues.** The
140
+ duplicated-phrase join check flags legitimately repeated wording as
141
+ "retake left in". If EVERY join comes back flagged — including plain
142
+ pause-tightening joins that obviously aren't retakes adjudicate each
143
+ finding against `flags.txt` (the screening output) instead of blindly
144
+ recutting; only recut joins that flags.txt corroborates as real retakes.
145
+ - **Caption mistranscriptions** are fixed with a captions override in
146
+ `timeline.json` (anchored on the MIS-transcribed text — see
147
+ `references/timeline-v2-vocabulary.md`), then recompile + re-render.
148
+ Never with subtitle files or filters.
115
149
 
116
150
  ## File sync
117
151
 
118
152
  `video-editing files pull --job-dir "<job>" [--only <name,name>]` fetches the
119
153
  cloud job's artifacts into the job dir preserving relative paths (transcripts,
120
154
  `prompts/*.md`, `resolved/plan.json`, `timeline.json`, …);
121
- `video-editing files push --job-dir "<job>"` uploads local artifacts (e.g. a
122
- hand-authored `timeline.json`, referenced assets) back to the job. Use `pull`
123
- to recover state on a fresh machine — the cloud job is the source of truth.
155
+ `video-editing files push --job-dir "<job>" <paths…>` uploads local artifacts
156
+ (e.g. a hand-authored `timeline.json`, referenced assets) back to the job. Use
157
+ `pull` to recover state on a fresh machine — the cloud job is the source of
158
+ truth.
124
159
 
125
160
  ## Working a ClipReady review round
126
161
 
@@ -130,13 +165,12 @@ When the job is a clipready review round (the video has an active round):
130
165
  `<job>/review/round.json` (round/run ids, requested changes) and
131
166
  `<job>/review/instruction.md`; hydrates `<job>/timeline.json` from the
132
167
  server only when you don't already have one (never overwrites yours).
133
- Add `--source` if the source video isn't local yet.
134
168
  2. Edit `timeline.json` per the instruction, then
135
169
  `video-editing review push --timeline --job-dir "<job>"` after each
136
- meaningful change — it compiles (diagnostics; exit `2` if not clean) and
137
- the review page updates live. Referenced assets (stickers, audio beds)
138
- upload automatically, sha256-cached.
139
- 3. Preview + verify exactly as in the workflow above.
170
+ meaningful change — it compiles in the cloud (diagnostics; exit `2` if
171
+ not clean) and the review page updates live. Referenced assets (stickers,
172
+ audio beds) upload automatically, sha256-cached.
173
+ 3. Render + verify exactly as in the workflow above.
140
174
  4. `video-editing review push --complete --duration <s> --outcomes outcomes.json --job-dir "<job>"`
141
175
  — compiles fresh (must be clean) and delivers the terminal payload.
142
176
  `outcomes.json` is `[{"change_id": "chg_…", "ok": true, "note": "…"}]`,
@@ -152,7 +186,7 @@ caption grouping is the same characters in Hebrew.
152
186
 
153
187
  ## References
154
188
 
155
- - `references/cli-reference.md` — every v2 subcommand + flag.
189
+ - `references/cli-reference.md` — every v3 subcommand + flag.
156
190
  - `references/timeline-v2-vocabulary.md` — the full `timeline.json` v2
157
191
  vocabulary, one JSON example per feature (captions + word styling, karaoke,
158
192
  zoom ramps, stickers, enter/exit, audio beds, grade).
@@ -0,0 +1,94 @@
1
+ # Smoothing the audio at cuts (`smooth-joins`)
2
+
3
+ **Optional.** Nothing here runs as part of a render. If you never call it,
4
+ renders behave exactly as they always have.
5
+
6
+ This is the Premiere move of selecting the whole timeline and hitting *Apply
7
+ Audio Transition*: the picture stays a hard cut, and every join gets a short
8
+ **Constant Power** (equal-power) audio crossfade built from the padding the cut
9
+ threw away. One invocation treats every join — you do not opt in per cut.
10
+
11
+ ## Why you would run it
12
+
13
+ By default each kept range is extracted with a 30 ms fade in *and* out and the
14
+ clips are butt-joined, so every cut has a brief hole down to silence. Depending
15
+ on where the cut landed that reads as a click, a clipped syllable, or a gap of
16
+ dead air. The crossfade turns each one into a quick natural dip instead, and
17
+ because it is centred on the cut the total duration does not change.
18
+
19
+ Reach for it when a verify pass or a listen flags clicks, popping, or chopped
20
+ word endings at joins — and on any edit with a lot of tight desilence trims,
21
+ where joins are most frequent.
22
+
23
+ Requires a compiled timeline (run `timeline compile` first) and an ingested
24
+ source. Exits non-zero with `no_edl` if you run it too early.
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ video-editing smooth-joins --job-dir "<job>" # 130ms everywhere
30
+ video-editing smooth-joins --crossfade-ms 90 --job-dir "<job>"
31
+ video-editing smooth-joins --join 3:40 --join 7:220 --job-dir "<job>"
32
+ ```
33
+
34
+ | Flag | Default | Meaning |
35
+ |---|---|---|
36
+ | `--crossfade-ms <ms>` | `130` | Transition length at every join (Premiere's 4 frames). `0` disables. |
37
+ | `--curve equal-power\|linear` | `equal-power` | Fade shape. |
38
+ | `--max-ms <ms>` | `500` | Ceiling on any single join. |
39
+ | `--join <index>:<ms>` | — | Override one boundary. Repeatable. |
40
+ | `--json` | — | Machine-readable output. |
41
+
42
+ **Join `i` is the boundary between segment `i` and segment `i + 1`**, so an
43
+ N-segment edit has N−1 joins. Same indexing `verify` reports its join findings
44
+ against, so a verify finding at join 5 is `--join 5:<ms>` here.
45
+
46
+ Keep `--curve equal-power` unless you have a reason: it is the only curve that
47
+ holds loudness flat through the transition (`sin² + cos² = 1`). `linear` dips
48
+ ~3 dB in the middle of every join.
49
+
50
+ Per-join overrides are for shaping, not rescue — `--join 3:40` to keep a hard
51
+ beat change tight, `--join 7:220` to let a take change breathe, `--join 9:0` to
52
+ leave one boundary exactly as it was.
53
+
54
+ ## Reading the result
55
+
56
+ Writes `audio/joined.m4a` (the rebuilt track) and `audio/joined.json`:
57
+
58
+ ```jsonc
59
+ {
60
+ "edl_sha256": "…",
61
+ "params": { "crossfade_ms": 130, "curve": "equal-power", "max_ms": 500 },
62
+ "total_duration_s": 47.213,
63
+ "segments": 42,
64
+ "joins": [
65
+ { "index": 0, "requested_ms": 130, "applied_ms": 130, "handle_ms": 1840, "clamped": false },
66
+ { "index": 1, "requested_ms": 130, "applied_ms": 52, "handle_ms": 52, "clamped": true,
67
+ "reason": "limited by the gap the cut removed" }
68
+ ]
69
+ }
70
+ ```
71
+
72
+ `applied_ms` is what the join actually got; `handle_ms` is the longest
73
+ crossfade the surrounding material could support. **A crossfade can only borrow
74
+ material the cut removed**, so a tight desilence trim that only took out 50 ms
75
+ can only give you a 50 ms transition — it will come back `clamped` no matter
76
+ what you request. That is expected and still sounds clean; 50 ms of same-speaker
77
+ room tone is plenty. Joins between different takes have seconds of handle and
78
+ always get the full request.
79
+
80
+ Read the report before re-running: if a join you care about came back clamped
81
+ with `"limited by the gap the cut removed"`, asking for more milliseconds will
82
+ change nothing. Widen the cut range in `timeline.json` and recompile instead.
83
+
84
+ ## How it interacts with rendering
85
+
86
+ Renders **consume** the artifact, they never trigger it:
87
+
88
+ - Run `smooth-joins`, then render → the render uses the crossfaded audio.
89
+ - Render without running it → unchanged from today.
90
+ - Recompile the timeline, then render → the artifact no longer matches the
91
+ edit and is ignored (it is keyed to the EDL). Re-run `smooth-joins`.
92
+
93
+ So the order is: `timeline compile` → `smooth-joins` → render. Re-running the
94
+ command is cheap; re-run it after every recompile you want it applied to.