aivee 0.0.0-stage → 0.1.0-dev.59872e1

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -2
  4. package/docs/architecture.md +401 -0
  5. package/docs/aspect-ratio-presets.md +72 -0
  6. package/docs/releasing.md +19 -0
  7. package/docs/usage.md +1193 -0
  8. package/package.json +50 -4
  9. package/requirements-otio.txt +1 -0
  10. package/src/capabilities.mjs +307 -0
  11. package/src/cli.mjs +646 -0
  12. package/src/doctor.mjs +133 -0
  13. package/src/edits/analysis-to-edl.mjs +43 -0
  14. package/src/edits/detect-silence.mjs +51 -0
  15. package/src/edits/find-mistake-candidates.mjs +42 -0
  16. package/src/edits/plan-shot-placements.mjs +105 -0
  17. package/src/edits/plan-silence-cuts.mjs +49 -0
  18. package/src/edits/retime-transcript.mjs +105 -0
  19. package/src/files.mjs +80 -0
  20. package/src/index.mjs +44 -0
  21. package/src/interchange/otio.mjs +204 -0
  22. package/src/interchange/otio_bridge.py +521 -0
  23. package/src/media/analysis-provider.mjs +75 -0
  24. package/src/media/compatibility.mjs +45 -0
  25. package/src/media/ffmpeg-capabilities.mjs +129 -0
  26. package/src/media/frame-rate.mjs +58 -0
  27. package/src/media/frame-time.mjs +22 -0
  28. package/src/media/preview-assets.mjs +350 -0
  29. package/src/media/probe.mjs +149 -0
  30. package/src/media/render-cuts.mjs +90 -0
  31. package/src/media/scene-detection.mjs +107 -0
  32. package/src/media/scene-review.mjs +205 -0
  33. package/src/project/assets.mjs +1194 -0
  34. package/src/project/create.mjs +100 -0
  35. package/src/project/presets.mjs +44 -0
  36. package/src/render/checkpoints.mjs +192 -0
  37. package/src/render/formats.mjs +163 -0
  38. package/src/render/hyperframes.mjs +590 -0
  39. package/src/render/process.mjs +111 -0
  40. package/src/render/progress.mjs +46 -0
  41. package/src/render/timeline-control.mjs +53 -0
  42. package/src/timeline/assemble.mjs +119 -0
  43. package/src/timeline/audio-effects.mjs +75 -0
  44. package/src/timeline/audio-mastering.mjs +236 -0
  45. package/src/timeline/audio-mix.mjs +105 -0
  46. package/src/timeline/audio-track-effects.mjs +139 -0
  47. package/src/timeline/audio-volume.mjs +63 -0
  48. package/src/timeline/background.mjs +8 -0
  49. package/src/timeline/blend-mode.mjs +34 -0
  50. package/src/timeline/caption-layouts.mjs +62 -0
  51. package/src/timeline/captions.mjs +80 -0
  52. package/src/timeline/edit.mjs +965 -0
  53. package/src/timeline/fingerprint.mjs +6 -0
  54. package/src/timeline/from-edl.mjs +31 -0
  55. package/src/timeline/history-store.mjs +281 -0
  56. package/src/timeline/history.mjs +69 -0
  57. package/src/timeline/inspect.mjs +117 -0
  58. package/src/timeline/keyframes.mjs +158 -0
  59. package/src/timeline/layers.mjs +18 -0
  60. package/src/timeline/mask.mjs +53 -0
  61. package/src/timeline/project.mjs +52 -0
  62. package/src/timeline/render.mjs +759 -0
  63. package/src/timeline/sidechain-render.mjs +209 -0
  64. package/src/timeline/source-fit.mjs +15 -0
  65. package/src/timeline/speech-bubble.mjs +26 -0
  66. package/src/timeline/speed-ramp.mjs +188 -0
  67. package/src/timeline/track-state.mjs +22 -0
  68. package/src/timeline/transition.mjs +34 -0
  69. package/src/timeline/validate.mjs +322 -0
  70. package/src/transcript/corrections.mjs +21 -0
  71. package/src/transcript/normalize.mjs +17 -0
  72. package/src/transcript/provider.mjs +42 -0
  73. package/src/transcript/subtitles.mjs +57 -0
  74. package/src/transcript/validate.mjs +39 -0
package/docs/usage.md ADDED
@@ -0,0 +1,1193 @@
1
+ # Video Editing Usage Guide
2
+
3
+ This guide covers generic project creation, media registration, editing, rendering, transcripts, and OpenTimelineIO interchange. Avatar artwork and speech-driven character animation belong to consuming studios; use generic image and audio tracks in Engine timelines.
4
+
5
+ ## Create a project
6
+
7
+ ```sh
8
+ npm run new-video -- my-video
9
+ npm run new-video -- vertical-video 1080x1920 30 15
10
+ npm run new-video -- short-video --preset vertical --fit cover
11
+ node src/cli.mjs presets list
12
+ ```
13
+
14
+ The default canvas is 1920×1080 at 30 fps for 10 seconds. Named presets are
15
+ `vertical` (9:16), `portrait-feed` (4:5), `square` (1:1), and `landscape`
16
+ (16:9). Use `presets list` to inspect their default dimensions and destinations.
17
+ Custom dimensions remain available with `WIDTHxHEIGHT`. A preset is recorded as
18
+ `canvasPreset` in project metadata. See
19
+ [`docs/aspect-ratio-presets.md`](aspect-ratio-presets.md) for the baseline
20
+ and platform references. Source media uses `contain` by default to preserve the
21
+ whole frame; select `cover` to crop overflow or `fill` to stretch. Set a
22
+ project-wide policy with `--fit`, then override it for individual video clips
23
+ with their `fit` field. Projects are created under `video-projects/` and
24
+ existing project folders are never overwritten. Each project starts with a
25
+ valid `timeline.json` containing ordered video, audio, and overlay tracks plus
26
+ all currently registered project assets. For an existing project missing that
27
+ file, initialize one from its metadata and media catalog with:
28
+
29
+ ```sh
30
+ node src/cli.mjs timeline create video-projects/my-video
31
+ ```
32
+
33
+ Use the reviewable `set-project-state` operation to change timeline duration,
34
+ the project-wide `sourceFit`, or the canvas background color; validation
35
+ refuses a duration that would clip any existing timeline item. `sourceFit: null`
36
+ clears the project override. `backgroundColor: null` makes the canvas
37
+ transparent; existing timelines without a background retain their black render
38
+ background. New projects default to `#10141c`.
39
+ Canvas width, height, and frame rate remain tied to project metadata and are
40
+ not editable through a timeline-only operation.
41
+
42
+ ```json
43
+ { "type": "set-project-state", "state": { "sourceFit": "cover", "durationSeconds": 30, "backgroundColor": "#f3f0eb" } }
44
+ ```
45
+
46
+ The optional output path can create a separate timeline. Pass a duration as the
47
+ second argument to keep the default output path, or as the final argument after
48
+ an output path to override the project default.
49
+
50
+ Frame rates accept integers and rational values such as `30000/1001` for
51
+ NTSC-style 29.97 fps. Rendering passes the project frame rate to HyperFrames
52
+ and checks the output rate. Select MP4, WebM, MOV, or GIF by the output file
53
+ extension:
54
+
55
+ ```sh
56
+ node src/cli.mjs render video-projects/my-video renders/final.webm high
57
+ node src/cli.mjs timeline render video-projects/my-video timeline.json renders/final.mov standard
58
+ node src/cli.mjs timeline render video-projects/my-video timeline.json renders/final.mp4 high render-options.json
59
+ node src/cli.mjs timeline render-range video-projects/my-video timeline.json 12.5 24 renders/segment.mp4 standard
60
+ node src/cli.mjs render video-projects/my-video renders/frames high png-options.json
61
+ ```
62
+
63
+ `timeline render-range` renders a selected project-time window from the source
64
+ timeline without editing it. JavaScript consumers can use
65
+ `renderTimelineRangeAsync(projectPath, timeline, startSeconds, endSeconds,
66
+ outputPath, quality, options)` or extract a reusable segment with
67
+ `extractTimelineRange(timeline, startSeconds, endSeconds)` and pass it to
68
+ `renderTimelineAsync`. The synchronous `renderTimelineRange` API is also
69
+ available. A range must include at least one project frame and cannot cut
70
+ through a clip fade or edge transition.
71
+
72
+ `render-options.json` can choose a video `codec`, integer encoder `crf`, or
73
+ `videoBitrate`, for example `{ "encoder": { "codec": "h265", "crf": 26 } }`
74
+ or `{ "encoder": { "codec": "vp9", "videoBitrate": "8M" } }`. Bound
75
+ render resources with `{ "workers": 2, "lowMemoryMode": true }`; `workers`
76
+ also accepts `"auto"`, while numeric values must be 1–64. MP4 supports
77
+ H.264/H.265, MOV supports H.264/H.265/ProRes, and WebM supports VP9. ProRes
78
+ uses its HQ profile and does not accept CRF or bitrate overrides. The codec
79
+ override runs as a verified FFmpeg final encode. With an explicit codec and no
80
+ CRF/bitrate override, FFmpeg's codec default is used. Add `"timeoutSeconds": 900`
81
+ to bound rendering (1–86400 seconds). `renderTimelineAsync` includes FFmpeg
82
+ audio alignment, speed/reverse/effect proxy generation, sidechain processing,
83
+ HyperFrames capture, final encoding, output probing, and audio mastering in
84
+ that deadline. `renderProjectAsync` also applies cancellation and timeouts to
85
+ capture, final encoding, and output probing. Timeout terminates the active
86
+ media process and removes temporary output. The JavaScript async APIs
87
+ `renderProjectAsync`, `renderTimelineAsync`, and `renderEditedProjectAsync` accept an
88
+ `AbortSignal` as `signal`. CLI renders handle Ctrl+C by cancelling the active
89
+ render and its FFmpeg preprocessing processes. The synchronous `renderProject`
90
+ and `renderTimeline` APIs remain available without cancellation. JavaScript
91
+ consumers can pass an `onProgress` callback to async render APIs. Events report
92
+ pipeline stages such as proxy resolution, audio alignment, playback processing,
93
+ audio effects, sidechain processing, HyperFrames capture, encoding, output
94
+ validation, publishing, timeline audio validation, and mastering analysis,
95
+ encoding, validation, and publication. Each event
96
+ includes `stage` and `phase` (`started`, `completed`, or `skipped`); capture
97
+ events include HyperFrames phase progress, while FFmpeg video and audio
98
+ mastering events report elapsed media time and a percentage. Events may include stage details such as
99
+ codec, target, or completed/total counts. Callback failures emit a warning and
100
+ do not cancel the render. To render PNG frames, set
101
+ `{ "format": "png-sequence" }`; the output path is a new directory. Defaults
102
+ remain controlled by HyperFrames quality (`draft`, `standard`, or `high`).
103
+ Set `"colorProfile": "rec709"` or `"colorProfile": "rec2020-sdr"` to convert
104
+ the rendered SDR video from the renderer's BT.709 working output and write
105
+ matching color metadata. This requires FFmpeg's `colorspace` filter and
106
+ performs a final video encode. Set `"colorProfile": "hdr10"` for MP4 or MOV to
107
+ convert the BT.709 SDR render to 10-bit BT.2020/PQ and encode with HEVC Main 10.
108
+ This SDR-to-HDR conversion maps reference white to 203 nits; it cannot recover
109
+ highlight detail absent from the SDR render. HDR10 output requires FFmpeg's
110
+ `zscale` filter and `libx265`. `npm run doctor` reports filter and encoder
111
+ availability. Before rendering, the Engine checks required final encoders and
112
+ filters and reports a setup error before generating timeline proxies or
113
+ starting browser capture when one is missing.
114
+
115
+ For long renders, add `{ "resumeCheckpoint": true }` to the render options.
116
+ After a complete HyperFrames capture, the async renderer keeps a validated
117
+ intermediate if final encoding is interrupted or fails; retry the same render
118
+ to reuse it. The cache is content-fingerprinted, SHA-256 checked, removed after
119
+ success, limited to 8 GiB per capture by default, and expires after 72 hours.
120
+ Override those bounds with `checkpointMaxBytes` (1 MiB–100 GiB) and
121
+ `checkpointMaxAgeHours` (1–720). An interruption during browser capture still
122
+ requires that capture stage to run again. PNG sequences and synchronous render
123
+ APIs do not support checkpoints.
124
+
125
+ `createCaptionTrack` also accepts reusable text styling options such as
126
+ `fontFamily`, `fontWeight`, `lineHeight`, `verticalAlign`, `paddingPixels`,
127
+ `textOutline`, and `textShadow`. These settings compile to validated CSS values;
128
+ raw CSS is not accepted. `textOutline` uses `{ color, widthPixels }`, while
129
+ `textShadow` uses `{ color, blurPixels, offsetXPixels, offsetYPixels }`.
130
+ Choose `layoutPreset: 'subtitle' | 'social' | 'socialPop' | 'socialStack' | 'speechBubble' | 'lowerThird'`
131
+ for reusable placement and styling defaults; explicit options override the
132
+ selected preset.
133
+ `socialPop` enables timed word highlighting by default, with a brief pop when
134
+ each word becomes active. `socialStack` provides a two-line social caption
135
+ layout with three words per line and six words per caption by default. Set
136
+ `wordsPerLine` and `wordsPerCaption` to customize line and caption lengths.
137
+ For other layouts, word-timed transcripts can set
138
+ `wordHighlight: true` and choose `wordHighlightAnimation: 'scale' | 'pop'`;
139
+ `wordHighlightColor` customizes the highlight. Explicit options override preset
140
+ defaults. Caption presets wrap long text at word boundaries by default; set
141
+ `wordWrap: false` to keep a single line. Wrapped words preserve the layout's
142
+ horizontal alignment and vertical placement. Use `listCaptionLayoutPresets()`
143
+ or `resolveCaptionLayoutPreset(name)` to inspect available layouts. Cue-level
144
+ transcripts do not contain enough timing detail for word animations.
145
+
146
+ `speechBubble` renders each timed phrase on a rounded panel with a directional
147
+ tail. Customize its placement, typography, foreground, and fill with the same
148
+ caption options, and customize the tail with a partial `bubble` object. For
149
+ example, `{ "layoutPreset": "speechBubble", "x": 0.34, "backgroundColor":
150
+ "#ffffff", "bubble": { "tailSide": "left", "tailPosition": 0.35,
151
+ "tailSizePixels": 24, "cornerRadiusPixels": 30 } }`. `tailSide` accepts
152
+ `top`, `right`, `bottom`, or `left`; `tailPosition` is normalized from 0.08 to
153
+ 0.92 along that edge. The layout defaults can be changed per generated caption
154
+ track through `timeline captions` options, or per existing overlay through the
155
+ JavaScript API and `timeline edit`. Standard overlay keyframes and transitions
156
+ can animate the bubble entrance, exit, and position.
157
+
158
+ GIF cannot carry audio, so timeline rendering rejects GIF requests when audio
159
+ clips are present. PNG image sequences export full-resolution RGBA frames to a
160
+ new directory and cannot carry audio or audio mastering. Optional CRF and
161
+ video-bitrate overrides are available for MP4, MOV, and WebM exports; the
162
+ default is still controlled by HyperFrames quality.
163
+
164
+ Media probes retain the container and separate audio/video stream start times
165
+ and time bases. Timeline source times use the media presentation timeline;
166
+ frame rounding helpers are available in the JavaScript API. If a linked video
167
+ and audio stream has a meaningful start/duration offset, rendering creates and
168
+ cleans a temporary timestamp-aligned audio proxy without changing the source.
169
+ Standalone audio with a non-zero stream start is also normalized to timeline
170
+ zero through a temporary proxy. Variable-frame-rate sources remain seconds-based. A synthetic 30/15 fps source
171
+ has been verified rendering at 30 fps on Windows; cross-platform VFR behavior
172
+ still needs validation.
173
+
174
+ ## Editing workflow
175
+
176
+ ```sh
177
+ # Inspect media and create a project
178
+ node src/cli.mjs probe path/to/source.mp4
179
+ node src/cli.mjs project create my-video 1080x1920 30 15 --background-color '#f3f0eb'
180
+
181
+ # Create or import a transcript
182
+ node src/cli.mjs transcribe video-projects/my-video path/to/source.mp4
183
+ node src/cli.mjs import-transcript video-projects/my-video path/to/source.mp4 path/to/captions.srt
184
+ node src/cli.mjs transcript export video-projects/my-video/transcripts/transcript.json vtt output/captions.vtt
185
+
186
+ # Propose edits, review the EDL, then apply approved cuts
187
+ node src/cli.mjs plan-silence video-projects/my-video/transcripts/transcript.json video-projects/my-video/edits/silence-edl.json
188
+ node src/cli.mjs mistake-candidates video-projects/my-video/transcripts/transcript.json
189
+ node src/cli.mjs apply-edl path/to/source.mp4 path/to/approved-edl.json path/to/clean-cut.mp4
190
+ node src/cli.mjs retime video-projects/my-video/transcripts/transcript.json path/to/approved-edl.json video-projects/my-video/transcripts/retimed.json
191
+
192
+ # Lint, preview, and render the composition
193
+ node src/cli.mjs lint video-projects/my-video
194
+ node src/cli.mjs preview video-projects/my-video
195
+ node src/cli.mjs render video-projects/my-video
196
+ ```
197
+
198
+ ### Assemble an edited video in one render
199
+
200
+ The generic timeline workflow can copy and fingerprint a source, use an
201
+ imported or generated transcript, apply a reviewed EDL, retime the transcript,
202
+ generate captions, and render the final composition:
203
+
204
+ ```sh
205
+ node src/cli.mjs project create my-edit --preset landscape
206
+ node src/cli.mjs asset register video-projects/my-edit path/to/source.mp4 camera
207
+ node src/cli.mjs asset register-many video-projects/my-edit path/to/camera.mp4 path/to/music.wav path/to/logo.png
208
+ node src/cli.mjs transcribe video-projects/my-edit video-projects/my-edit/assets/camera.mp4
209
+
210
+ # Check copied media and restore a moved or damaged asset under its existing ID.
211
+ node src/cli.mjs asset verify video-projects/my-edit
212
+ node src/cli.mjs asset relink video-projects/my-edit camera path/to/replacement.mp4
213
+ node src/cli.mjs asset preview video-projects/my-edit camera
214
+
215
+ # Create a reusable draft proxy; request proxy use in timeline render options.
216
+ node src/cli.mjs asset proxy create video-projects/my-edit camera 960 28
217
+ # Normalize a VFR source proxy to the 30 fps project timeline.
218
+ node src/cli.mjs asset proxy create video-projects/my-edit camera 960 28 30
219
+ node src/cli.mjs timeline render video-projects/my-edit timeline.json renders/review.mp4 draft render-options.json
220
+
221
+ # Generate a proposal, review it, then save approved decisions as approved.json.
222
+ node src/cli.mjs detect-silence video-projects/my-edit/assets/camera.mp4 video-projects/my-edit/edits/silence-proposal.json
223
+ # Find candidate shot boundaries; inspect the analysis before editing.
224
+ node src/cli.mjs detect-scenes video-projects/my-edit/assets/camera.mp4 video-projects/my-edit/edits/scene-candidates.json
225
+ node src/cli.mjs edit video-projects/my-edit camera video-projects/my-edit/transcripts/transcript.json path/to/approved.json
226
+ ```
227
+
228
+ Consumers can inject a transcription provider without coupling the Engine to
229
+ one cloud vendor or credential store. The provider returns word/cue objects;
230
+ the Engine validates and normalizes them against the probed source duration:
231
+
232
+ ```js
233
+ await transcribeWithProvider(projectPath, mediaPath, {
234
+ id: 'my-provider',
235
+ async transcribe({ mediaPath, durationSeconds, options }) {
236
+ return providerClient.transcribe(mediaPath, { ...options, durationSeconds });
237
+ },
238
+ }, { granularity: 'word', providerOptions: { language: 'en' } });
239
+ ```
240
+
241
+ Provider credentials stay inside the consumer's provider client and are not
242
+ written to project metadata or the normalized transcript.
243
+
244
+ The JavaScript API exports `probeMediaAsync(path, { signal })` for cancellable
245
+ FFprobe inspection when a consumer needs media metadata without registering the
246
+ asset first. The CLI `probe` command uses the same asynchronous path and Ctrl+C
247
+ stops its active FFprobe process.
248
+
249
+ `asset register` probes and copies sources asynchronously. Ctrl+C cancels the
250
+ probe or staged copy and cleans the incomplete registration; source files and
251
+ existing project assets are left untouched. When `timeline.json` exists, the
252
+ new asset's probed metadata is added to its asset catalog without adding a clip;
253
+ the editing agent can then place it on a track. JavaScript consumers can use
254
+ `await registerMediaAssetAsync(projectPath, sourcePath, { id, signal, onProgress })`
255
+ to receive byte-based copy progress plus probe and publish stages. The CLI prints
256
+ copy progress to stderr and keeps the registered asset JSON on stdout.
257
+ Both synchronous and asynchronous registration now probe and fingerprint the
258
+ staged copy, so the recorded metadata describes the bytes committed to the
259
+ project. Registration prefers hard-link publication and falls back to an
260
+ exclusive copy when the filesystem does not support hard links; the async path
261
+ supports cancellation and reports fallback copy progress through the publish
262
+ stage.
263
+ `asset register-many` and `registerMediaAssetsAsync(projectPath, sources, {
264
+ signal, onProgress })` add multiple sources sequentially. Each source is an
265
+ independent transaction; the batch continues after per-source errors and
266
+ returns `{ registered, failed }` so partial success is explicit. Duplicate IDs
267
+ and existing destination conflicts are rejected before any files are copied.
268
+ The CLI prints batch and per-asset stage progress to stderr, writes the complete
269
+ result on stdout, and exits with status 1 if any source failed. Batch
270
+ registration does not add clips to the timeline. The async API returns batch
271
+ progress events at `asset-batch` and `asset-batch-item` stages.
272
+ `asset relink` emits the same copy/hash, probe, and publish progress for the
273
+ replacement source. Its CLI progress stays on stderr.
274
+
275
+ `asset verify` checks registered source and proxy SHA-256 identities against
276
+ the manifest and reports missing or modified files. The CLI hashes media
277
+ asynchronously and reports byte-based scan progress on stderr. Ctrl+C cancels
278
+ the scan; JavaScript consumers can pass `onProgress(event)` to
279
+ `verifyProjectAssetsAsync(projectPath, { signal, onProgress })` for the same
280
+ overall progress in their UI.
281
+ The synchronous `verifyProjectAssets(projectPath)` API remains available.
282
+ `asset relink` replaces
283
+ the media for an existing asset ID, refreshes its probe metadata and
284
+ fingerprint, and keeps
285
+ the project-relative path stable so existing timeline references continue to
286
+ work. If `timeline.json` exists, its asset metadata and source fingerprint are
287
+ refreshed as well; the timeline is revalidated before the replacement is saved.
288
+ Stale proxies are removed when the source fingerprint changes. Relinking only
289
+ accepts the original media type and extension, and preserves the replacement
290
+ source file. The CLI stages and probes the replacement asynchronously; Ctrl+C
291
+ cancels before commit or restores the prior asset, manifest, and timeline if
292
+ the commit fails. JavaScript consumers can use
293
+ `await relinkProjectAssetAsync(projectPath, assetId, sourcePath, { signal })`.
294
+ Asset registration and relinking update timeline metadata. When a persistent
295
+ timeline history sidecar exists, the Engine resets its snapshots automatically
296
+ and preserves the configured history limit because asset metadata changes form
297
+ a new undo baseline.
298
+
299
+ Use `asset list` to inspect all registered media, including assets that are not
300
+ yet referenced by a timeline. Video/audio records include container aliases
301
+ and metadata for every video and audio stream. The command returns versioned
302
+ manifest metadata as JSON without touching media files:
303
+
304
+ ```sh
305
+ node src/cli.mjs asset list video-projects/my-edit
306
+ ```
307
+
308
+ The JavaScript API exposes the same operation as
309
+ `listProjectAssets(projectPath)`.
310
+
311
+ Use `asset unregister` to remove unused media from the project manifest and
312
+ timeline catalog. It refuses to unregister an asset referenced by any clip.
313
+ The source and proxy files stay on disk, so this operation does not discard
314
+ media; delete those files separately only when you have confirmed they are no
315
+ longer needed. The API returns the removed asset metadata and confirms that the
316
+ files were preserved:
317
+
318
+ ```sh
319
+ node src/cli.mjs asset unregister video-projects/my-edit unused-intro
320
+ ```
321
+
322
+ The JavaScript API exposes the same operation as
323
+ `unregisterProjectAsset(projectPath, assetId)`.
324
+
325
+ `asset proxy create` generates an H.264, video-only proxy for a registered
326
+ video asset. The optional arguments set maximum dimension, CRF, and output
327
+ frame rate (defaults: 960 and 28, with no frame-rate conversion); valid ranges
328
+ are 160–3840 pixels, CRF 18–40, and frame rate 1–240 fps. A specified frame
329
+ rate normalizes source timestamps to a CFR proxy, which can help browser
330
+ previews of VFR media. Proxies preserve smaller source dimensions instead of
331
+ upscaling and force a keyframe every second to make timeline seeking faster.
332
+ Match the proxy frame rate to the project and enable proxy rendering. The
333
+ Engine records the proxy profile, fingerprint, and media metadata
334
+ in the asset manifest and reuses a matching current proxy. To use proxies for
335
+ draft review, set
336
+ `{ "useProxies": true }` in the timeline render options JSON. This switches
337
+ video clips to the smallest current proxy and leaves audio clips on registered
338
+ source media. `asset verify` checks proxy files too; a modified proxy is
339
+ regenerated when the same proxy settings are requested again. Regular final
340
+ renders continue using the original assets unless proxy use is explicitly
341
+ requested. The CLI uses cancellable FFmpeg processing; Ctrl+C stops proxy
342
+ generation and removes its staged output. JavaScript consumers can call
343
+ `await createAssetProxyAsync(projectPath, assetId, { signal, ...options })`.
344
+ Pass `onProgress(event)` to receive proxy preflight, FFmpeg encoding percentage,
345
+ integrity validation, and publication events. The CLI reports proxy progress on
346
+ stderr while keeping the final proxy record as JSON on stdout.
347
+
348
+ `asset preview` creates cached thumbnails and/or an audio waveform for a
349
+ registered asset. It returns source-relative thumbnail timestamps and paths,
350
+ plus min/max waveform samples that preview clients can draw at any zoom level.
351
+ Preview files live under `assets/previews/` and are keyed by the source hash and
352
+ generation settings; changed source media cannot silently reuse stale previews.
353
+ Pass an options JSON file to override `thumbnailIntervalSeconds` (default 2,
354
+ 0.25–60), `thumbnailMaxDimension` (default 320, 80–1280), or `waveformPoints`
355
+ (default 2000, 32–20000). The JavaScript API is
356
+ `await generateAssetPreview(projectPath, assetId, { signal, ...options })`;
357
+ thumbnail extraction, waveform decoding, and media probing respond to the
358
+ signal. Pass `onProgress(event)` to receive integrity hashing, media probing,
359
+ thumbnail extraction, waveform decoding, and cache publication status. The CLI
360
+ writes these updates to stderr and keeps preview JSON on stdout; Ctrl+C cancels
361
+ active preview processes.
362
+
363
+ Media-analysis providers use the same injected approach. Their findings are
364
+ time-checked against the probed source and returned as suggestions for an AI or
365
+ human editor to review; the Engine does not turn them into automatic cuts:
366
+
367
+ For a local first-pass shot map, `detect-scenes` uses FFmpeg's scene-change
368
+ score and returns review-required `scene-cut` findings in the same normalized
369
+ analysis format. It accepts an optional JSON file with `threshold` (0.05–1,
370
+ default 0.35), `minimumSpacingSeconds` (0–60, default 0.5), and
371
+ `maximumFindings` (1–10000, default 2000). Raising the threshold finds only
372
+ more pronounced visual changes. This detects visual differences; it does not
373
+ identify semantic topics, camera quality, or whether a cut is editorially
374
+ useful. The JavaScript API is `detectSceneChangesAsync(mediaPath, options)` and
375
+ supports `AbortSignal` cancellation.
376
+
377
+ ```sh
378
+ node src/cli.mjs detect-scenes path/to/source.mp4 edits/scene-candidates.json
379
+ ```
380
+
381
+ Scene candidates can guide a provider or editing agent when selecting ranges
382
+ for a rough cut, B-roll, or chapter markers. They do not create or apply edits
383
+ by themselves.
384
+
385
+ Generate a complete shot-review folder with a representative JPEG thumbnail for
386
+ each detected shot, a contact sheet, and `scene-review.json` mapping shot
387
+ indexes and time ranges to the images:
388
+
389
+ ```sh
390
+ node src/cli.mjs scene-review path/to/source.mp4 video-projects/review/source-shots
391
+ ```
392
+
393
+ The default pack is bounded to 300 shots and a 60-frame contact sheet. Use an
394
+ options JSON file to adjust detection settings or set `maximumShots`,
395
+ `thumbnailSize`, `contactSheetLimit`, and `columns`; the contact sheet is capped
396
+ at 64 megapixels. The schema v2 pack includes the source SHA-256 identity. It is
397
+ staged and published as a new directory; an existing output directory is never
398
+ replaced.
399
+ All findings remain review-required, and the thumbnails do not infer scene
400
+ meaning. The API exposes `createSceneReviewPackAsync(mediaPath, outputDir, options)`
401
+ with cancellation and progress callbacks; the CLI reports progress on stderr
402
+ while writing the review manifest to stdout.
403
+
404
+ An editing agent can turn explicitly selected review shots into a dry-run B-roll
405
+ insertion plan. Register the same source media used to create the scene-review
406
+ pack as a video asset, then provide only the shot, timeline position, and
407
+ optional source trim selections:
408
+
409
+ ```json
410
+ [
411
+ {
412
+ "shotIndex": 2,
413
+ "timelineStartSeconds": 8,
414
+ "sourceOffsetSeconds": 0.25,
415
+ "durationSeconds": 2,
416
+ "clipId": "market-broll-01"
417
+ }
418
+ ]
419
+ ```
420
+
421
+ ```sh
422
+ node src/cli.mjs timeline plan-broll timeline.json scene-review.json broll-source video-broll placements.json edits/broll-plan.json
423
+ ```
424
+
425
+ The command writes a `reviewRequired` plan with validated `add-clip` operations,
426
+ a before/after preview, and SHA-256 identities for the timeline and source.
427
+ It verifies the registered asset matches the media used by the scene-review
428
+ pack. It does not change the timeline or choose semantically relevant shots.
429
+ The selected source ranges must fit within their reviewed shot. Review the saved plan,
430
+ then explicitly apply it through the undoable command:
431
+
432
+ ```sh
433
+ node src/cli.mjs timeline apply-plan timeline.json edits/broll-plan.json
434
+ ```
435
+
436
+ Application refuses if the timeline has changed since planning. The JavaScript
437
+ APIs are `planSceneReviewPlacements(timeline, sceneReview, assetId, trackId,
438
+ placements)` and `applyTimelineReviewPlan(timelinePath, plan)`.
439
+
440
+ ```js
441
+ const { analysis } = await analyzeMediaWithProvider(mediaPath, {
442
+ id: 'scene-detector',
443
+ async analyze({ mediaPath, durationSeconds, media, options }) {
444
+ return detector.inspect(mediaPath, { durationSeconds, media, ...options });
445
+ },
446
+ });
447
+ ```
448
+
449
+ Each normalized finding has a stable result-local ID, kebab-case type, start
450
+ time, optional end time, optional confidence, and optional description.
451
+ Providers may attach a validated `remove-range` suggestion with source bounds
452
+ and a reason. Convert explicit suggestions into a review-required EDL draft,
453
+ then approve it only after review:
454
+
455
+ ```sh
456
+ node src/cli.mjs analysis-to-edl edits/analysis.json edits/candidate-edl.json
457
+ node src/cli.mjs approve-edl edits/candidate-edl.json edits/approved-edl.json
458
+ ```
459
+
460
+ Candidate EDLs carry `reviewStatus: "needs-review"`; rendering, timeline
461
+ construction, and transcript retiming reject them until the explicit approval
462
+ command writes a separate approved file. Confidence filtering is optional via
463
+ an options JSON file containing `minConfidence` from 0 to 1.
464
+
465
+ For CLI workflows, pass a trusted JavaScript module that exports a provider as
466
+ its default export (or as `mediaAnalysisProvider` / `provider`). The module
467
+ receives normalized media metadata and optional provider settings; suggestions
468
+ are written to a new JSON file and existing output is never overwritten:
469
+
470
+ ```sh
471
+ node src/cli.mjs analyze path/to/source.mp4 path/to/provider.mjs edits/analysis.json provider-options.json
472
+ ```
473
+
474
+ Keep provider credentials in the provider's own runtime configuration. The
475
+ Engine validates the results and does not store provider credentials.
476
+
477
+ The EDL is a JSON object with `schemaVersion`, `sourceDurationSeconds`, and
478
+ `cuts` entries containing source-time `sourceStart`, `sourceEnd`, and `reason`.
479
+ The `edit` command refuses to overwrite existing timeline, approved EDL,
480
+ retimed transcript, or render artifacts. It stores the source SHA-256 and
481
+ artifact paths in timeline lineage. To edit transcript words before rendering,
482
+ apply corrections with `transcript correct` and provide the corrected file to
483
+ `edit`. You can also build and render a generic timeline directly with the
484
+ JavaScript API (`createProjectTimeline`, `timelineFromEdl`, `editTimeline`,
485
+ `editTimelineBatch`, `editTimelineFile`, `undoTimelineFile`,
486
+ `redoTimelineFile`, `detachAudioFromTimelineClip`, `createTimelineHistory`,
487
+ `createCaptionTrack`, and `renderTimeline`).
488
+
489
+ Use `timeline inspect` to give an editing agent a compact, read-only view of the
490
+ project format, assets, ordered tracks, clip source and timeline ranges,
491
+ transitions, and audio settings before it proposes an edit:
492
+
493
+ ```sh
494
+ node src/cli.mjs timeline inspect timeline.json
495
+ ```
496
+
497
+ The JavaScript API exports the same view through
498
+ `inspectTimeline(timeline)`. It validates the timeline first and does not write
499
+ or modify project files. Computed clip end times in the summary are rounded to
500
+ six decimal places to avoid floating-point display noise.
501
+
502
+ Generic edit operations can add/remove/reorder tracks and add/remove/duplicate clips, detach clip audio,
503
+ move clips within and between compatible tracks, trim/slip/split clips,
504
+ insert a synchronized gap or ripple-delete a time range across every track,
505
+ apply crossfades or clip-edge transitions, and preserve timeline validation at
506
+ each step. Gap insertion trims clips at the insertion point and shifts their
507
+ remaining segments and later clips while keeping source ranges, keyframes,
508
+ speed ramps, captions, and track synchronization aligned. Ripple deletion
509
+ trims or splits clips that straddle the cut, retimes later clips, and shortens
510
+ the project. Adding a clip or inserting a gap can extend project duration;
511
+ ordinary removal leaves it unchanged so existing downstream timing is preserved.
512
+ Cross-track moves preserve the clip id and all clip metadata, and require the
513
+ source and destination tracks to have the same kind. To move a video clip to a
514
+ different video layer, use `move-track` in an operation file:
515
+
516
+ ```json
517
+ {
518
+ "type": "move-track",
519
+ "trackId": "video-main",
520
+ "clipId": "interview-shot",
521
+ "targetTrackId": "video-broll",
522
+ "timelineStart": 8
523
+ }
524
+ ```
525
+
526
+ For AI-proposed arbitrary time ranges, use `ripple-delete-range` with explicit
527
+ project-time boundaries. It applies the same cut to every track, preserves the
528
+ left/right clip segments, and retimes later clips, keyframes, audio automation,
529
+ and markers together:
530
+
531
+ ```json
532
+ { "type": "ripple-delete-range", "startSeconds": 12.4, "endSeconds": 13.1 }
533
+ ```
534
+
535
+ The JavaScript API exposes this as
536
+ `rippleDeleteTimelineRange(timeline, startSeconds, endSeconds)`.
537
+
538
+ Use `set-track-state` to show or hide video/overlay tracks, mute or solo audio
539
+ tracks, lock either track kind, or adjust audio-track volume through the same
540
+ reviewable operation workflow.
541
+ Partial state updates preserve other settings, and `null` clears an override:
542
+
543
+ ```json
544
+ { "type": "set-track-state", "trackId": "music", "state": { "muted": true, "volume": 0.35 } }
545
+ ```
546
+
547
+ Disable an individual clip without removing it from the timeline with
548
+ `set-clip-state`. Re-enable it with `enabled: true`, or clear the override with
549
+ `null`:
550
+
551
+ ```json
552
+ { "type": "set-clip-state", "trackId": "video-main", "clipId": "unused-take", "state": { "enabled": false } }
553
+ ```
554
+
555
+ The JavaScript API exposes the same operation through
556
+ `setTimelineClipState(timeline, trackId, clipId, state)`.
557
+
558
+ Add a project-time marker for chapter points or review notes with `set-marker`.
559
+ Markers are stored in the edit timeline and returned by `inspectTimeline`; they
560
+ do not render into the video. Gap insertion moves markers at or after the
561
+ insertion point, and ripple deletion removes markers inside the cut and shifts
562
+ later markers with the retained edit:
563
+
564
+ ```json
565
+ {
566
+ "type": "set-marker",
567
+ "marker": {
568
+ "id": "review-audio-mix",
569
+ "timeSeconds": 12.5,
570
+ "label": "Review music level",
571
+ "color": "#f59e0b"
572
+ }
573
+ }
574
+ ```
575
+
576
+ Use `remove-marker` with the marker id to delete it. The JavaScript API exposes
577
+ these operations as `setTimelineMarker(timeline, marker)` and
578
+ `removeTimelineMarker(timeline, markerId)`. Markers use stable kebab-case ids,
579
+ non-empty labels of at most 200 characters, optional six-digit hex colors, and
580
+ times within the project duration. OpenTimelineIO maps them to root-stack
581
+ markers; other interchange adapters may not preserve or display them.
582
+
583
+ To process every clip on a music track with the same chain, set its effects at
584
+ track level:
585
+
586
+ ```json
587
+ {
588
+ "type": "set-track-audio-effects",
589
+ "trackId": "music",
590
+ "audioEffects": [
591
+ { "type": "lowpass", "frequencyHz": 16000 },
592
+ { "type": "balance", "value": 0.15 }
593
+ ]
594
+ }
595
+ ```
596
+
597
+ Visual tracks accept `visible`; audio tracks accept `muted`, `solo`, `volume`, and
598
+ project-time `volumeKeyframes` through `set-track-state`. Both track kinds accept
599
+ `locked`. Lock or unlock it with a standalone operation:
600
+
601
+ ```json
602
+ { "type": "set-track-state", "trackId": "approved-footage", "state": { "locked": true } }
603
+ ```
604
+
605
+ A locked track rejects clip edits and track-setting changes. Synchronized gap
606
+ insertion and ripple deletion are refused when they would change locked track
607
+ content. Unlock the track with `{"locked": false}` before editing it. Lock state
608
+ is preserved in Engine OpenTimelineIO metadata; OTIO itself has no portable
609
+ track-lock behavior.
610
+
611
+ Use the dedicated
612
+ `set-track-audio-effects` operation to replace or clear a track effect chain.
613
+ Its JavaScript API is
614
+ `setTimelineTrackAudioEffects(timeline, trackId, effects)`; pass `null` or an
615
+ empty array to clear it. Clip effects run first; then a track with effects is
616
+ mixed to one temporary stem and its chain runs across that full timeline.
617
+ Clip/track gain, fades, and timing ducking are preserved.
618
+
619
+ When one or more audio tracks have `solo: true`, draft and final timeline renders
620
+ include only unmuted solo tracks. Clear the solo override with `null` to return
621
+ to the full mix. Solo state is carried in Engine metadata during OpenTimelineIO
622
+ round trips.
623
+
624
+ Use `set-overlay` to reviewably change caption text, word timings, layout,
625
+ styling, or composition reference after adding the overlay clip. The operation
626
+ accepts validated overlay fields only, and `null` clears optional overrides:
627
+
628
+ ```json
629
+ {
630
+ "type": "set-overlay",
631
+ "trackId": "captions",
632
+ "clipId": "caption-01",
633
+ "properties": { "text": "A clearer caption", "fontSize": 48, "textAlign": "center" }
634
+ }
635
+ ```
636
+
637
+ The JavaScript API is `setTimelineOverlay(timeline, trackId, clipId, properties)`.
638
+ Use `preview-edit` to inspect caption and layout changes before writing them.
639
+
640
+ Review the exact result before writing an edit. `preview-edit` prints the
641
+ duration change and each added, removed, or updated track and clip as JSON; it
642
+ does not write a timeline file:
643
+
644
+ ```sh
645
+ node src/cli.mjs timeline preview-edit timeline.json edits/insert-gap.json
646
+ ```
647
+
648
+ After reviewing the preview, apply the same operation plan to a new timeline
649
+ file. For example, save this as `edits/insert-gap.json`:
650
+
651
+ ```json
652
+ {
653
+ "type": "insert-gap",
654
+ "timelineStart": 12.5,
655
+ "durationSeconds": 2
656
+ }
657
+ ```
658
+
659
+ Then write the result to a new timeline file:
660
+
661
+ ```sh
662
+ node src/cli.mjs timeline edit timeline.json edits/insert-gap.json timeline-edited.json
663
+ ```
664
+
665
+ The operation file can also be a JSON array. The Engine applies each operation
666
+ in order, validates each intermediate timeline, and only writes the final file
667
+ if the entire batch succeeds:
668
+
669
+ ```json
670
+ [
671
+ { "type": "move", "trackId": "video-main", "clipId": "intro", "timelineStart": 0.5 },
672
+ { "type": "transition", "trackId": "video-main", "clipId": "intro", "edge": "in", "transition": { "schemaVersion": 1, "type": "fade", "durationSeconds": 0.4 } }
673
+ ]
674
+ ```
675
+
676
+ Single-operation files remain supported. Existing output files are never
677
+ overwritten. The JavaScript API exposes the same behavior through
678
+ `editTimelineBatch(timeline, operations)`. `previewTimelineEdit(timeline,
679
+ operations)` returns the same validated change plan without modifying the input.
680
+
681
+ Replace a clip's media source through a reviewable `replace-asset` operation.
682
+ The clip keeps its timeline placement and visual/audio settings; provide new
683
+ source bounds when the old range does not fit the replacement. The operation
684
+ can also clear an incompatible playback rate or speed ramp with `null`, or
685
+ select/clear an audio stream:
686
+
687
+ ```json
688
+ {
689
+ "type": "replace-asset",
690
+ "trackId": "video-main",
691
+ "clipId": "b-roll-01",
692
+ "assetId": "b-roll-alternate",
693
+ "sourceIn": 2,
694
+ "sourceOut": 5,
695
+ "playbackRate": null,
696
+ "reverse": false,
697
+ "speedRamp": null
698
+ }
699
+ ```
700
+
701
+ Use `timeline preview-edit` first to inspect the resulting clip changes. The
702
+ same operation is available as `replaceTimelineClipAsset` in the JavaScript API.
703
+
704
+ Detach the selected video clip's source audio into a synchronized audio clip.
705
+ This creates an audio track when the requested one does not exist and keeps
706
+ the video clip's source range, timeline placement, and playback rate or speed
707
+ ramp. Set `audioStreamIndex` to a zero-based ordinal to select a language or mic
708
+ track from a multi-audio source; registered assets expose each stream's codec,
709
+ channel layout, timing, language, and title. Edit the new audio clip
710
+ independently to create an L-cut or J-cut:
711
+
712
+ ```json
713
+ {
714
+ "type": "detach-audio",
715
+ "trackId": "video-main",
716
+ "clipId": "interview-shot",
717
+ "audioTrackId": "interview-audio",
718
+ "audioClipId": "interview-shot-audio",
719
+ "audioStreamIndex": 1
720
+ }
721
+ ```
722
+
723
+ For interactive workflows, wrap a timeline in bounded in-memory undo/redo
724
+ history. Failed edits leave the history untouched, each returned timeline is an
725
+ isolated snapshot, and applying a new edit after undo clears the redo branch:
726
+
727
+ ```js
728
+ const history = createTimelineHistory(timeline, { limit: 100 });
729
+ history.applyBatch([
730
+ { type: 'move', trackId: 'video-main', clipId: 'intro', timelineStart: 1 },
731
+ { type: 'transition', trackId: 'video-main', clipId: 'intro', edge: 'in', transition: { schemaVersion: 1, type: 'fade', durationSeconds: 0.4 } },
732
+ ]);
733
+ const reviewed = history.undo();
734
+ const restored = history.redo();
735
+ ```
736
+
737
+ The history stores timeline snapshots in memory; save approved project files
738
+ separately through the consumer's normal persistence flow. For CLI agents and
739
+ other one-shot processes, persistent history operates directly on a timeline
740
+ file and keeps a bounded hidden sidecar beside it:
741
+
742
+ ```sh
743
+ node src/cli.mjs timeline preview-edit timeline.json edits/move-intro.json
744
+ node src/cli.mjs timeline project-edit timeline.json edits/move-intro.json
745
+ node src/cli.mjs timeline undo timeline.json
746
+ node src/cli.mjs timeline redo timeline.json
747
+ node src/cli.mjs timeline history timeline.json
748
+ ```
749
+
750
+ The first edit creates `.timeline.json.history.json`; the default history limit
751
+ is 20 snapshots. Pass a limit on the first `project-edit` call to choose a
752
+ different bound. A failed or interrupted two-file update is recovered from its
753
+ atomic history journal on the next command; a lock serializes concurrent CLI
754
+ edits and stale locks from exited processes are cleared. If the timeline was edited outside
755
+ that history, the CLI reports the mismatch; use
756
+ `timeline history timeline.json reset [limit]` to begin a new history from the
757
+ current timeline.
758
+
759
+ For a crossfade between adjacent clips, apply a timeline edit before rendering:
760
+
761
+ ```js
762
+ const transitioned = editTimeline(timeline, {
763
+ type: 'crossfade',
764
+ trackId: 'video-main',
765
+ clipId: 'intro',
766
+ incomingClipId: 'main',
767
+ durationSeconds: 0.35,
768
+ });
769
+ ```
770
+
771
+ Set `allowOverlap: true` on the track and place the incoming clip so it overlaps
772
+ the outgoing clip by `durationSeconds`. The operation verifies that timing and
773
+ configures opacity fades for video/text or volume fades for audio. Tracks still
774
+ control compositing order.
775
+
776
+ For reusable clip-edge transitions, assign a versioned fade or directional wipe
777
+ to a video or overlay clip:
778
+
779
+ ```js
780
+ const transitioned = editTimeline(timeline, {
781
+ type: 'transition',
782
+ trackId: 'video-main',
783
+ clipId: 'main',
784
+ edge: 'in',
785
+ transition: { schemaVersion: 1, type: 'wipe', direction: 'left', durationSeconds: 0.5 },
786
+ });
787
+ ```
788
+
789
+ Use `edge: 'out'` for the clip end, or `transition: null` to remove the edge
790
+ transition. Wipes accept `left`, `right`, `up`, and `down`; both clip edges can
791
+ have transitions when their durations do not overlap. Fade transitions cannot
792
+ share opacity animation with that clip, and wipes cannot share a static mask.
793
+
794
+ Video clips and text/composition overlays can define clip-local visual
795
+ keyframes. `x`, `y`, `width`, and `height` are normalized to the canvas;
796
+ `opacity` ranges from 0 to 1, and `rotationDegrees` rotates around the element
797
+ center. Video clips can animate `objectPositionX` and `objectPositionY` from 0
798
+ to 1 to reframe media inside its existing `object-fit` viewport, and `zoom`
799
+ from 1× to 4× to scale the media inside a clipped, fixed-size viewport. Use
800
+ object-position keyframes to pan the zoom focus without moving the clip's
801
+ timeline bounds. Visual filter values include `brightness`, `contrast`, and `saturation`
802
+ (0–5), `blurPixels` (0–100), and `hueDegrees` (-360–360). Each keyframe time is
803
+ relative to its clip and the first keyframe must be at 0. Values interpolate to
804
+ the next keyframe; `ease` is optional and uses an allowlisted GSAP ease. The
805
+ last value holds through the clip end. Opacity keyframes cannot be combined
806
+ with clip fade fields. Use an `eases` map for per-property easing when one
807
+ keyframe changes multiple properties with different curve segments. Trim and
808
+ split operations rebase these easing ranges to preserve motion continuity.
809
+ An easing slice is represented as `{ type: 'range', name: 'power2.out',
810
+ from: 0.5, to: 1 }`, where `from` and `to` select a normalized part of the
811
+ allowlisted curve. The validator rejects unrecognized ease names and ranges.
812
+
813
+ Video and overlay clips also accept validated static masks: `circle` with
814
+ `centerX`, `centerY`, and `radius`; `ellipse` with `centerX`, `centerY`,
815
+ `radiusX`, and `radiusY`; or `rect` with normalized `x`, `y`, `width`, and
816
+ `height`. These are timeline data, not arbitrary CSS values. Animate a mask
817
+ with the `maskProgress` keyframe property from 0 to 1; it scales the chosen
818
+ shape from its center to its configured bounds. The first keyframe must include
819
+ `maskProgress`, and the clip must define a static mask.
820
+
821
+ Video and overlay clips may set an allowlisted CSS `blendMode` to composite
822
+ against lower visual layers. Supported modes are `normal`, `multiply`,
823
+ `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`,
824
+ `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, `saturation`,
825
+ `color`, and `luminosity`. For example, `blendMode: 'screen'` can lighten a
826
+ lower layer while preserving its dark areas. Blend modes are isolated to the
827
+ project canvas and cannot be applied to audio clips.
828
+
829
+ ```js
830
+ const clip = {
831
+ id: 'b-roll',
832
+ assetId: 'camera',
833
+ sourceIn: 0,
834
+ sourceOut: 4,
835
+ timelineStart: 2,
836
+ durationSeconds: 4,
837
+ keyframes: [
838
+ { time: 0, properties: { x: 0.5, y: 0.1, width: 0.4, height: 0.8, opacity: 0 } },
839
+ { time: 1, properties: { x: 0.3, opacity: 1 }, ease: 'power2.out' },
840
+ { time: 4, properties: { x: 0.1, rotationDegrees: -3 } },
841
+ ],
842
+ };
843
+ ```
844
+
845
+ Animated bounds must stay within the canvas. Keyframes are part of the
846
+ schema-version-1 timeline object and use the same JavaScript renderer as other
847
+ timeline operations. AI workflows can replace or clear clip keyframes with the
848
+ reviewable `set-keyframes` timeline operation. It works in a batch and with
849
+ `timeline preview-edit`, so proposed motion can be inspected before saving:
850
+
851
+ ```json
852
+ {
853
+ "type": "set-keyframes",
854
+ "trackId": "video",
855
+ "clipId": "b-roll",
856
+ "keyframes": [
857
+ { "time": 0, "properties": { "x": 0.5, "width": 0.4 } },
858
+ { "time": 2, "properties": { "x": 0.2, "width": 0.6 }, "ease": "power2.out" }
859
+ ]
860
+ }
861
+ ```
862
+
863
+ Set `keyframes` to `null` to remove the animation. The JavaScript API exposes
864
+ `setTimelineClipKeyframes(timeline, trackId, clipId, keyframes)`.
865
+
866
+ Use `set-framing` to reviewably change a video's static fit or normalized
867
+ viewport after adding it to the timeline. Partial updates preserve unspecified
868
+ fields; set a field to `null` to restore its inherited/default value:
869
+
870
+ ```json
871
+ {
872
+ "type": "set-framing",
873
+ "trackId": "video",
874
+ "clipId": "b-roll",
875
+ "framing": { "fit": "cover", "x": 0.1, "width": 0.8 }
876
+ }
877
+ ```
878
+
879
+ The JavaScript API exposes `setTimelineClipFraming(timeline, trackId, clipId,
880
+ framing)`. Timeline validation checks the resulting crop and viewport bounds.
881
+
882
+ Set a constant `playbackRate` from `0.1` to `5` on video or audio clips. The
883
+ source range must equal `durationSeconds * playbackRate`. Rendering creates a
884
+ temporary FFmpeg proxy for the selected source range (video is re-encoded;
885
+ audio uses pitch-preserving `atempo`) and removes it afterward. Original assets
886
+ and the saved timeline remain unchanged.
887
+
888
+ Set `reverse: true` on a video or audio clip to play its selected source range
889
+ backward. It can combine with a constant `playbackRate`, but not with
890
+ `speedRamp`. Rendering creates a temporary reversed media proxy and removes it
891
+ afterward. FFmpeg's reverse filters buffer the selected clip, so memory use
892
+ grows with the clip's duration and resolution. Reverse playback is not represented by the
893
+ OpenTimelineIO interchange profile. Detaching a reversed video clip copies the
894
+ reverse setting to its audio clip. Trim and split preserve the reverse source
895
+ direction. To change it as a reviewable edit, use the
896
+ `set-reverse` timeline operation with the CLI or `setTimelineClipReverse` API:
897
+
898
+ ```json
899
+ { "type": "set-reverse", "trackId": "video", "clipId": "shot-1", "reverse": true }
900
+ ```
901
+
902
+ The `doctor` command and `getEngineCapabilities()` report whether the FFmpeg
903
+ `reverse` and `areverse` filters are available. Rendering checks the filter
904
+ needed by each reversed track before it creates temporary media.
905
+
906
+ For a smooth speed ramp, set source-relative `speedRamp` points instead of a
907
+ constant `playbackRate`:
908
+
909
+ ```js
910
+ import { speedRampDuration } from 'aivee';
911
+
912
+ const speedRamp = [
913
+ { sourceOffset: 0, playbackRate: 1, ease: 'easeInOutCubic' },
914
+ { sourceOffset: 2, playbackRate: 2 },
915
+ ];
916
+ const clip = {
917
+ sourceIn: 0,
918
+ sourceOut: 2,
919
+ durationSeconds: speedRampDuration(speedRamp),
920
+ speedRamp,
921
+ };
922
+ ```
923
+
924
+ The `ease` field on a point controls the outgoing segment. Supported curves are
925
+ `linear`, `easeIn`, `easeOut`, `easeInOut`, `easeInCubic`, `easeOutCubic`, and
926
+ `easeInOutCubic`; omitted easing defaults to `linear`. The renderer samples
927
+ eased rate curves into bounded linear segments for video timestamp mapping and
928
+ audio automation. The clip duration is `integral(ds / playbackRate(s))` over
929
+ its source range; the first point starts at offset 0 and the last ends at
930
+ `sourceOut - sourceIn`. Use the reviewable `set-speed` edit operation to set a
931
+ constant rate, replace a speed ramp, or reset to normal speed. It recalculates
932
+ clip duration and scales clip-local animation, fades, transitions, word timing,
933
+ and volume automation to preserve their relative timing:
934
+
935
+ ```json
936
+ {
937
+ "type": "set-speed",
938
+ "trackId": "video-main",
939
+ "clipId": "shot-1",
940
+ "speed": { "playbackRate": 1.5 }
941
+ }
942
+ ```
943
+
944
+ Pass `null` for `playbackRate` or `speedRamp` to reset to normal speed. The
945
+ same operation is exposed as `setTimelineClipSpeed`.
946
+ Rates must stay between 0.1× and 5×. Use the exported `speedRampDuration`
947
+ helper to compute the required clip duration. The video proxy maps frame
948
+ timestamps through the curve. Audio uses pitch-preserving FFmpeg `rubberband`
949
+ commands sampled at up to 50 ms source intervals, then pads or trims to the
950
+ clip's exact timeline duration. Audio speed ramps require an FFmpeg build with
951
+ the `rubberband` filter; the renderer reports a clear error when it is missing.
952
+ The `doctor` command reports whether the filter is available. Trim and split
953
+ operations rebase speed curves and visual keyframes at the edit point.
954
+
955
+ Audio tracks and clips accept `volume` from 0 to 1, and audio clips accept
956
+ `fadeInSeconds` and `fadeOutSeconds`.
957
+
958
+ Use `set-audio-state` to reviewably change an audio clip's static volume, fade
959
+ durations, or selected source audio stream. Partial updates preserve unspecified
960
+ fields; `null` restores the inherited/default value:
961
+
962
+ ```json
963
+ {
964
+ "type": "set-audio-state",
965
+ "trackId": "narration",
966
+ "clipId": "voice-over",
967
+ "state": { "volume": 0.8, "fadeInSeconds": 0.2, "audioStreamIndex": 0 }
968
+ }
969
+ ```
970
+
971
+ The JavaScript API is
972
+ `setTimelineClipAudioState(timeline, trackId, clipId, state)`. Timeline
973
+ validation checks the selected stream and clip-bound fade durations.
974
+
975
+ Video clips can select a zero-based video-stream ordinal with
976
+ `videoStreamIndex`. Registered assets expose stream ordinals and their FFmpeg
977
+ stream indexes in `videoStreams`. Use `set-video-state` to reviewably change or
978
+ clear the selection; rendering creates a temporary clip proxy for the chosen
979
+ stream and leaves the source media unchanged:
980
+
981
+ ```json
982
+ {
983
+ "type": "set-video-state",
984
+ "trackId": "video-main",
985
+ "clipId": "interview-shot",
986
+ "state": { "videoStreamIndex": 1 }
987
+ }
988
+ ```
989
+
990
+ The JavaScript API is
991
+ `setTimelineClipVideoState(timeline, trackId, clipId, state)`. The same
992
+ selection can be supplied when replacing a clip asset with `videoStreamIndex`.
993
+
994
+ For changing clip gain over time, use `volumeKeyframes`. Times are relative to
995
+ the audio clip, begin at 0, and interpolate linearly between points. The last
996
+ value holds through the clip end. Trim, split, gap insertion, and ripple delete
997
+ preserve the curve. Use `set-volume-keyframes` or the
998
+ `setTimelineClipVolumeKeyframes` API to reviewably replace the curve; pass
999
+ `null` or an empty array to clear it.
1000
+
1001
+ ```json
1002
+ {
1003
+ "type": "set-volume-keyframes",
1004
+ "trackId": "narration",
1005
+ "clipId": "voice-over",
1006
+ "volumeKeyframes": [
1007
+ { "time": 0, "volume": 1 },
1008
+ { "time": 2.5, "volume": 0.35 },
1009
+ { "time": 4, "volume": 1 }
1010
+ ]
1011
+ }
1012
+ ```
1013
+
1014
+ Audio tracks can also define `volumeKeyframes` to automate the track fader over
1015
+ project time. These points start at time 0, use linear interpolation, and hold
1016
+ the last value. Clip and track automation multiply together with static track
1017
+ and clip gain. `set-track-state` can replace the track curve; pass `null` to
1018
+ clear it. Gap insertion and ripple deletion preserve its timing:
1019
+
1020
+ ```json
1021
+ {
1022
+ "type": "set-track-state",
1023
+ "trackId": "music",
1024
+ "state": {
1025
+ "volumeKeyframes": [
1026
+ { "time": 0, "volume": 0.7 },
1027
+ { "time": 5, "volume": 0.25 },
1028
+ { "time": 12, "volume": 0.7 }
1029
+ ]
1030
+ }
1031
+ }
1032
+ ```
1033
+
1034
+ Audio clips can also define an ordered `audioEffects` array. The supported
1035
+ effects are `equalizer` (`frequencyHz`, `gainDB`, optional `q`), `highpass` or
1036
+ `lowpass` (`frequencyHz`, optional `order` 1–2), `compressor` (`thresholdDB`,
1037
+ `ratio`, optional attack/release and makeup gain), and `denoise`
1038
+ (`reductionDB`, 0.01–30). `balance` takes a `value` from `-1` (left) to `1`
1039
+ (right), with `0` centered. Effects are applied to a temporary clip proxy,
1040
+ leaving the registered source unchanged. Denoise requires the `afftdn` filter;
1041
+ stereo balance requires `stereotools`. `doctor` and `capabilities` report these
1042
+ and other audio filter availability. Render preflight checks all filters
1043
+ required by selected clip/track effects, sidechain, and mastering settings
1044
+ before creating temporary media.
1045
+ Audio tracks may define the same `audioEffects` array; the Engine mixes the
1046
+ track's clips to a temporary stem, then applies the track chain across that
1047
+ complete stem. Use `set-track-audio-effects` to replace or clear the track
1048
+ chain without editing the timeline file by hand.
1049
+ Use the reviewable `set-audio-effects` operation or the
1050
+ `setTimelineClipAudioEffects` API to apply or replace this ordered list. Pass
1051
+ `null` or an empty array to clear it.
1052
+
1053
+ ```json
1054
+ {
1055
+ "audioEffects": [
1056
+ { "type": "highpass", "frequencyHz": 80 },
1057
+ { "type": "denoise", "reductionDB": 12 },
1058
+ { "type": "compressor", "thresholdDB": -20, "ratio": 3 },
1059
+ { "type": "balance", "value": -0.2 }
1060
+ ]
1061
+ }
1062
+ ```
1063
+
1064
+ Keep the final `audioMastering` stage for whole-mix processing such as loudness,
1065
+ final equalization, limiting, and output channel layout.
1066
+
1067
+ To duck a music track while narration clips are active, add this to the timeline:
1068
+
1069
+ ```js
1070
+ timeline.audioMix = {
1071
+ // Clip-timing ducking remains available without signal analysis.
1072
+ ducking: [{
1073
+ sourceTrackId: 'narration',
1074
+ targetTrackId: 'music',
1075
+ attenuation: 0.22,
1076
+ attackSeconds: 0.12,
1077
+ releaseSeconds: 0.3,
1078
+ }],
1079
+ // Or compress the music from the actual narration signal.
1080
+ sidechain: [{
1081
+ sourceTrackId: 'narration',
1082
+ targetTrackId: 'music',
1083
+ thresholdDb: -24,
1084
+ ratio: 4,
1085
+ attackSeconds: 0.01,
1086
+ releaseSeconds: 0.25,
1087
+ }],
1088
+ };
1089
+ ```
1090
+
1091
+ `attenuation` is a multiplier on the target's current volume while source clips
1092
+ are active. This timing-based option compiles with track/clip gain and fades to
1093
+ one automation envelope. `sidechain` instead listens to the source track's
1094
+ rendered signal and applies FFmpeg sidechain compression to target clips. Its
1095
+ defaults are -24 dB threshold, 4:1 ratio, 10 ms attack, and 250 ms release.
1096
+ Threshold is limited to -60–0 dB, ratio to 1:1–20:1, attack to 0.01–2000 ms,
1097
+ and release to 0.01–9000 ms. The detector stem uses source track/clip gain and
1098
+ fades; output track/clip gain and fades remain applied by the timeline renderer.
1099
+ This is broadband compression, not speech recognition or phoneme-aware audio
1100
+ mixing. Final settings still need listening review on varied real material.
1101
+ Use the reviewable `set-audio-mix` edit operation or
1102
+ `setTimelineAudioMix(timeline, audioMix)` API to replace these rules; pass
1103
+ `null` to clear them.
1104
+
1105
+ Optional final-mix loudness mastering is configured on the timeline:
1106
+
1107
+ ```js
1108
+ timeline.audioMastering = {
1109
+ integratedLoudnessLUFS: -16,
1110
+ truePeakDBTP: -1.5,
1111
+ loudnessRangeLU: 11,
1112
+ equalizer: [{ frequencyHz: 120, gainDB: 2, q: 1 }],
1113
+ limiter: { limitDBFS: -1, attackMs: 5, releaseMs: 80 },
1114
+ channelLayout: 'stereo',
1115
+ };
1116
+ ```
1117
+
1118
+ Defaults are -16 LUFS, -1.5 dBTP, and 11 LU. The renderer analyzes the final
1119
+ mix and uses two-pass FFmpeg `loudnorm` when the measurements are finite;
1120
+ unmeasurable non-silent audio falls back to single-pass dynamic normalization.
1121
+ Fully silent audio is re-encoded without loudness filtering because it has no
1122
+ measurable level to normalize. Before replacing the unmastered render, the
1123
+ encoded output is checked for duration, dimensions, and audio presence. GIF
1124
+ output cannot be mastered because GIF has
1125
+ no audio stream. Accepted ranges are -70 to -5 LUFS, -9 to 0 dBTP, and 1 to
1126
+ 50 LU, respectively.
1127
+ Equalizer bands accept 20–20000 Hz, ±18 dB, and Q values from 0.1 to 10 (up to
1128
+ 10 bands). The optional limiter accepts a -20 to 0 dBFS ceiling, 0.1–80 ms
1129
+ attack, and 10–1000 ms release. Equalization is applied before loudness analysis
1130
+ and normalization; the limiter runs at the end of the final mix chain. These
1131
+ controls need listening review against varied real material before mix presets
1132
+ can be recommended.
1133
+ `channelLayout` can request a mono or stereo final mix; the encoded channel count
1134
+ is checked before the mastered render replaces its input. Use the reviewable
1135
+ `set-audio-mastering` edit operation or
1136
+ `setTimelineAudioMastering(timeline, settings)` API to replace settings; pass
1137
+ `null` to clear mastering.
1138
+
1139
+ Silence and repeated-word detection produce proposals only. Review and save the
1140
+ approved cuts in the versioned EDL format before applying them. Source media,
1141
+ transcripts, EDLs, and render outputs are kept separate; commands refuse to
1142
+ overwrite existing outputs.
1143
+
1144
+ The CLI and JavaScript package exports are the engine interface. Editing agents,
1145
+ plugins, and creator-facing workflows can be built as separate consumers. See
1146
+ [`docs/architecture.md`](architecture.md) for contracts and runtime
1147
+ requirements, and [`docs/handoff.md`](handoff.md) for the full project
1148
+ context, validation state, and consolidated remaining-work checklist.
1149
+
1150
+ ## OpenTimelineIO interchange
1151
+
1152
+ The optional OTIO bridge imports and exports standard external-media clips and
1153
+ gaps. Install the pinned Python adapter, then use a project timeline:
1154
+
1155
+ ```powershell
1156
+ python -m pip install -r requirements-otio.txt
1157
+ node src/cli.mjs timeline export-otio video-projects/my-edit timeline.json review.otio
1158
+ node src/cli.mjs timeline import-otio video-projects/my-edit edit.otio imported-timeline.json
1159
+ ```
1160
+
1161
+ The bridge uses the maintained OpenTimelineIO Python library; the Node.js API
1162
+ and other Engine features work without it. OTIO files reference media by path
1163
+ and do not embed media. The profile supports video/audio tracks, nested track
1164
+ stacks and nested compositions inside tracks flattened into ordered Engine
1165
+ tracks, cuts, gaps, root-stack markers, standard `SMPTE_Dissolve` transitions
1166
+ on import, track enabled state, and project duration including trailing gaps.
1167
+ Engine track lock and solo state are stored in the namespaced metadata; OTIO has
1168
+ no portable track-lock state. Imported dissolves become overlapping clips with
1169
+ video opacity fades or audio crossfades. Constant-rate retiming uses OTIO's
1170
+ `LinearTimeWarp`; imported clip duration is calculated from the source span and
1171
+ playback rate. Variable speed ramps and retimed crossfades remain unsupported.
1172
+ Import applies a trimmed track's source range to its clips and gaps; it rejects
1173
+ trims that partially cut transitions. Trimmed stacks are applied to direct tracks;
1174
+ nested or combined stack/track trims remain unsupported. Import also rejects
1175
+ custom transition types, other overlaps, third-party effects, nested-composition
1176
+ trims combined with parent-track trims, and remote media references. Clip enabled
1177
+ state is retained on import and export. Export converts explicitly faded two-clip overlaps into standard
1178
+ `SMPTE_Dissolve` transitions. Unmarked or chained overlaps are rejected rather
1179
+ than silently reinterpreted. Standalone fades, clip-edge transitions, clip
1180
+ settings, track-level audio effects and automation, and overlay tracks are
1181
+ retained in namespaced metadata for Engine round trips. Crossfade participants
1182
+ with additional Engine-only clip settings are rejected because splitting those
1183
+ clips would change the settings' timing or scope.
1184
+ Crossfade offsets and source ranges are derived from the overlapping Engine
1185
+ clips.
1186
+ Standard OTIO playback still represents only the interoperable subset; other
1187
+ editors may ignore or remove Engine metadata, and may not reproduce those
1188
+ settings when rendering.
1189
+
1190
+ Run `npm run doctor` or `node src/cli.mjs capabilities` to check whether the
1191
+ selected Python environment has OpenTimelineIO and its `otio_json` adapter.
1192
+ Set `AIVE_OTIO_PYTHON` to the Python command for a separate environment when
1193
+ needed; the JavaScript capability API accepts the same override as `otioPython`.