aivee 0.1.0-alpha.0 → 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 -21
  3. package/README.md +120 -118
  4. package/docs/architecture.md +401 -401
  5. package/docs/aspect-ratio-presets.md +72 -72
  6. package/docs/releasing.md +19 -19
  7. package/docs/usage.md +1193 -1193
  8. package/package.json +52 -52
  9. package/requirements-otio.txt +1 -1
  10. package/src/capabilities.mjs +307 -307
  11. package/src/cli.mjs +646 -646
  12. package/src/doctor.mjs +133 -133
  13. package/src/edits/analysis-to-edl.mjs +43 -43
  14. package/src/edits/detect-silence.mjs +51 -51
  15. package/src/edits/find-mistake-candidates.mjs +42 -42
  16. package/src/edits/plan-shot-placements.mjs +105 -105
  17. package/src/edits/plan-silence-cuts.mjs +49 -49
  18. package/src/edits/retime-transcript.mjs +105 -105
  19. package/src/files.mjs +80 -80
  20. package/src/index.mjs +44 -44
  21. package/src/interchange/otio.mjs +204 -204
  22. package/src/interchange/otio_bridge.py +521 -521
  23. package/src/media/analysis-provider.mjs +75 -75
  24. package/src/media/compatibility.mjs +45 -45
  25. package/src/media/ffmpeg-capabilities.mjs +129 -129
  26. package/src/media/frame-rate.mjs +58 -58
  27. package/src/media/frame-time.mjs +22 -22
  28. package/src/media/preview-assets.mjs +350 -350
  29. package/src/media/probe.mjs +149 -149
  30. package/src/media/render-cuts.mjs +90 -90
  31. package/src/media/scene-detection.mjs +107 -107
  32. package/src/media/scene-review.mjs +205 -205
  33. package/src/project/assets.mjs +1194 -1194
  34. package/src/project/create.mjs +100 -100
  35. package/src/project/presets.mjs +44 -44
  36. package/src/render/checkpoints.mjs +192 -192
  37. package/src/render/formats.mjs +163 -163
  38. package/src/render/hyperframes.mjs +590 -590
  39. package/src/render/process.mjs +111 -111
  40. package/src/render/progress.mjs +46 -46
  41. package/src/render/timeline-control.mjs +53 -53
  42. package/src/timeline/assemble.mjs +119 -119
  43. package/src/timeline/audio-effects.mjs +75 -75
  44. package/src/timeline/audio-mastering.mjs +236 -236
  45. package/src/timeline/audio-mix.mjs +105 -105
  46. package/src/timeline/audio-track-effects.mjs +139 -139
  47. package/src/timeline/audio-volume.mjs +63 -63
  48. package/src/timeline/background.mjs +8 -8
  49. package/src/timeline/blend-mode.mjs +34 -34
  50. package/src/timeline/caption-layouts.mjs +62 -61
  51. package/src/timeline/captions.mjs +80 -80
  52. package/src/timeline/edit.mjs +965 -965
  53. package/src/timeline/fingerprint.mjs +6 -6
  54. package/src/timeline/from-edl.mjs +31 -31
  55. package/src/timeline/history-store.mjs +281 -281
  56. package/src/timeline/history.mjs +69 -69
  57. package/src/timeline/inspect.mjs +117 -117
  58. package/src/timeline/keyframes.mjs +158 -158
  59. package/src/timeline/layers.mjs +18 -18
  60. package/src/timeline/mask.mjs +53 -53
  61. package/src/timeline/project.mjs +52 -52
  62. package/src/timeline/render.mjs +759 -759
  63. package/src/timeline/sidechain-render.mjs +209 -209
  64. package/src/timeline/source-fit.mjs +15 -15
  65. package/src/timeline/speech-bubble.mjs +26 -26
  66. package/src/timeline/speed-ramp.mjs +188 -188
  67. package/src/timeline/track-state.mjs +22 -22
  68. package/src/timeline/transition.mjs +34 -34
  69. package/src/timeline/validate.mjs +322 -322
  70. package/src/transcript/corrections.mjs +21 -21
  71. package/src/transcript/normalize.mjs +17 -17
  72. package/src/transcript/provider.mjs +42 -42
  73. package/src/transcript/subtitles.mjs +57 -57
  74. package/src/transcript/validate.mjs +39 -39
package/src/cli.mjs CHANGED
@@ -1,646 +1,646 @@
1
- #!/usr/bin/env node
2
- import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { dirname, isAbsolute, join, resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- import { approveEdl, retimeTranscript } from './edits/retime-transcript.mjs';
6
- import { mediaAnalysisToEdl } from './edits/analysis-to-edl.mjs';
7
- import { detectAcousticSilence } from './edits/detect-silence.mjs';
8
- import { createVideoProject } from './project/create.mjs';
9
- import { listCanvasPresets } from './project/presets.mjs';
10
- import { createAssetProxyAsync, listProjectAssets, registerMediaAssetAsync, registerMediaAssetsAsync, relinkProjectAssetAsync, unregisterProjectAsset, verifyProjectAssetsAsync } from './project/assets.mjs';
11
- import { probeMediaAsync } from './media/probe.mjs';
12
- import { inspectMediaCompatibility } from './media/compatibility.mjs';
13
- import { generateAssetPreview } from './media/preview-assets.mjs';
14
- import { analyzeMediaWithProvider } from './media/analysis-provider.mjs';
15
- import { detectSceneChangesAsync } from './media/scene-detection.mjs';
16
- import { createSceneReviewPackAsync } from './media/scene-review.mjs';
17
- import { renderCuts } from './media/render-cuts.mjs';
18
- import { planSilenceCuts } from './edits/plan-silence-cuts.mjs';
19
- import { findMistakeCandidates } from './edits/find-mistake-candidates.mjs';
20
- import { planSceneReviewPlacements } from './edits/plan-shot-placements.mjs';
21
- import { validateTimeline } from './timeline/validate.mjs';
22
- import { inspectTimeline } from './timeline/inspect.mjs';
23
- import { timelineFromEdl } from './timeline/from-edl.mjs';
24
- import { renderTimelineAsync, renderTimelineRangeAsync } from './timeline/render.mjs';
25
- import { editTimeline, editTimelineBatch, previewTimelineEdit } from './timeline/edit.mjs';
26
- import { applyTimelineReviewPlan, editTimelineFile, getTimelineFileHistory, redoTimelineFile, resetTimelineFileHistory, undoTimelineFile } from './timeline/history-store.mjs';
27
- import { createProjectTimeline } from './timeline/project.mjs';
28
- import { createCaptionTrack } from './timeline/captions.mjs';
29
- import { applyTranscriptCorrections } from './transcript/corrections.mjs';
30
- import { formatSubtitleText } from './transcript/subtitles.mjs';
31
- import { renderEditedProjectAsync } from './timeline/assemble.mjs';
32
- import { importProjectTranscript, lintProject, previewProject, renderProjectAsync, transcribeProject } from './render/hyperframes.mjs';
33
- import { normalizeFrameRate } from './media/frame-rate.mjs';
34
- import { exportTimelineToOtio, importTimelineFromOtio } from './interchange/otio.mjs';
35
- import { getEngineCapabilities } from './capabilities.mjs';
36
-
37
- function readJson(path) {
38
- try {
39
- return JSON.parse(readFileSync(path, 'utf8'));
40
- } catch (error) {
41
- throw new Error(`Could not read JSON file ${path}: ${error.message}`);
42
- }
43
- }
44
-
45
- function writeNewFile(path, data) {
46
- writeExclusiveFile(path, `${JSON.stringify(data, null, 2)}\n`);
47
- }
48
-
49
- function writeNewTextFile(path, data) {
50
- writeExclusiveFile(path, data);
51
- }
52
-
53
- function writeExclusiveFile(path, data) {
54
- let descriptor;
55
- try {
56
- descriptor = openSync(path, 'wx');
57
- writeFileSync(descriptor, data);
58
- closeSync(descriptor);
59
- descriptor = undefined;
60
- } catch (error) {
61
- if (descriptor !== undefined) {
62
- try { closeSync(descriptor); } catch {}
63
- try { unlinkSync(path); } catch {}
64
- }
65
- if (error.code === 'EEXIST') throw new Error(`Output already exists: ${path}`);
66
- throw new Error(`Could not write output ${path}: ${error.message}`);
67
- }
68
- }
69
-
70
- async function runWithInterrupt(run, operation = 'Render') {
71
- const controller = new AbortController();
72
- const onInterrupt = () => {
73
- const error = new Error(`${operation} cancelled by Ctrl+C.`);
74
- error.name = 'AbortError';
75
- controller.abort(error);
76
- };
77
- process.once('SIGINT', onInterrupt);
78
- try {
79
- return await run(controller.signal);
80
- } finally {
81
- process.removeListener('SIGINT', onInterrupt);
82
- }
83
- }
84
-
85
- function parseProjectOptions(args) {
86
- const positional = [];
87
- const flags = {};
88
- for (let index = 0; index < args.length; index++) {
89
- const token = args[index];
90
- if (token === '--preset' || token === '--fit' || token === '--background-color') {
91
- if (flags[token]) throw new Error(`${token} can only be specified once.`);
92
- const value = args[++index];
93
- if (!value || value.startsWith('--')) throw new Error(`${token} requires a value.`);
94
- flags[token] = value;
95
- } else if (token.startsWith('--')) {
96
- throw new Error(`Unknown project option: ${token}`);
97
- } else {
98
- positional.push(token);
99
- }
100
- }
101
-
102
- if (flags['--preset']) {
103
- if (positional.length > 2 || (positional[0] && !/^\d+(?:\.\d+|\/\d+)?$/.test(positional[0]))) {
104
- throw new Error('Usage: project create <slug> --preset <name> [fps] [durationSeconds] [--fit <contain|cover|fill>]');
105
- }
106
- return {
107
- preset: flags['--preset'],
108
- fps: normalizeFrameRate(positional[0] ?? '30'),
109
- durationSeconds: Number(positional[1] ?? 10),
110
- ...(flags['--fit'] ? { sourceFit: flags['--fit'] } : {}),
111
- ...(flags['--background-color'] ? { backgroundColor: parseBackgroundColorFlag(flags['--background-color']) } : {}),
112
- };
113
- }
114
-
115
- if (positional.length > 3) {
116
- throw new Error('Usage: project create <slug> [WIDTHxHEIGHT] [fps] [durationSeconds] [--fit <contain|cover|fill>]');
117
- }
118
- const [size = '1920x1080', fps = '30', durationSeconds = '10'] = positional;
119
- const match = /^(\d+)x(\d+)$/i.exec(size);
120
- if (!match) throw new Error('Canvas size must use WIDTHxHEIGHT, such as 1080x1920.');
121
- return {
122
- width: Number(match[1]),
123
- height: Number(match[2]),
124
- fps: normalizeFrameRate(fps),
125
- durationSeconds: Number(durationSeconds),
126
- ...(flags['--fit'] ? { sourceFit: flags['--fit'] } : {}),
127
- ...(flags['--background-color'] ? { backgroundColor: parseBackgroundColorFlag(flags['--background-color']) } : {}),
128
- };
129
- }
130
-
131
- function parseBackgroundColorFlag(value) {
132
- return value.toLowerCase() === 'transparent' ? null : value;
133
- }
134
-
135
- async function main(args) {
136
- const [command, ...commandArgs] = args;
137
- if (command === 'capabilities' && commandArgs.length === 0) {
138
- console.log(JSON.stringify(getEngineCapabilities(), null, 2));
139
- return;
140
- }
141
- if (command === 'presets' && commandArgs.length === 1 && commandArgs[0] === 'list') {
142
- console.log(JSON.stringify(listCanvasPresets(), null, 2));
143
- return;
144
- }
145
- if (command === 'timeline' && commandArgs[0] === 'validate' && commandArgs.length === 2) {
146
- validateTimeline(readJson(resolve(commandArgs[1])));
147
- console.log('Timeline valid.');
148
- return;
149
- }
150
- if (command === 'timeline' && commandArgs[0] === 'create' && commandArgs.length >= 2 && commandArgs.length <= 4) {
151
- const [, projectPath, secondArgument, thirdArgument] = commandArgs;
152
- const secondIsDuration = thirdArgument === undefined && secondArgument !== undefined && /^\d+(?:\.\d+)?$/.test(secondArgument);
153
- const outputPath = secondIsDuration ? undefined : secondArgument;
154
- const requestedDuration = secondIsDuration ? secondArgument : thirdArgument;
155
- const options = requestedDuration === undefined ? {} : { durationSeconds: Number(requestedDuration) };
156
- const timeline = createProjectTimeline(resolve(projectPath), options);
157
- const destination = resolve(outputPath ?? join(resolve(projectPath), 'timeline.json'));
158
- writeNewFile(destination, timeline);
159
- console.log(`Wrote ${destination}`);
160
- return;
161
- }
162
- if (command === 'timeline' && commandArgs[0] === 'inspect' && commandArgs.length === 2) {
163
- console.log(JSON.stringify(inspectTimeline(readJson(resolve(commandArgs[1]))), null, 2));
164
- return;
165
- }
166
- if (command === 'timeline' && commandArgs[0] === 'export-otio' && commandArgs.length === 4) {
167
- const [, project, timelinePath, outputPath] = commandArgs;
168
- writeNewTextFile(resolve(outputPath), exportTimelineToOtio(resolve(project), readJson(resolve(timelinePath))));
169
- console.log(`Wrote ${resolve(outputPath)}`);
170
- return;
171
- }
172
- if (command === 'timeline' && commandArgs[0] === 'import-otio' && commandArgs.length === 4) {
173
- const [, project, inputPath, outputPath] = commandArgs;
174
- if (existsSync(resolve(outputPath))) throw new Error(`Output already exists: ${resolve(outputPath)}`);
175
- writeNewFile(resolve(outputPath), importTimelineFromOtio(resolve(project), resolve(inputPath)));
176
- console.log(`Wrote ${resolve(outputPath)}`);
177
- return;
178
- }
179
- if (command === 'timeline' && commandArgs[0] === 'edit' && commandArgs.length === 4) {
180
- const [, inputPath, operationPath, outputPath] = commandArgs;
181
- const timeline = readJson(resolve(inputPath));
182
- const operation = readJson(resolve(operationPath));
183
- const edited = Array.isArray(operation)
184
- ? editTimelineBatch(timeline, operation)
185
- : editTimeline(timeline, operation);
186
- writeNewFile(resolve(outputPath), edited);
187
- console.log(`Wrote ${resolve(outputPath)}`);
188
- return;
189
- }
190
- if (command === 'timeline' && commandArgs[0] === 'preview-edit' && commandArgs.length === 3) {
191
- const [, inputPath, operationPath] = commandArgs;
192
- console.log(JSON.stringify(previewTimelineEdit(readJson(resolve(inputPath)), readJson(resolve(operationPath))), null, 2));
193
- return;
194
- }
195
- if (command === 'timeline' && commandArgs[0] === 'plan-broll' && commandArgs.length >= 6 && commandArgs.length <= 7) {
196
- const [, timelinePath, sceneReviewPath, assetId, trackId, placementsPath, outputPath] = commandArgs;
197
- const plan = planSceneReviewPlacements(
198
- readJson(resolve(timelinePath)),
199
- readJson(resolve(sceneReviewPath)),
200
- assetId,
201
- trackId,
202
- readJson(resolve(placementsPath)),
203
- );
204
- if (outputPath) {
205
- writeNewFile(resolve(outputPath), plan);
206
- console.log(`Wrote ${resolve(outputPath)} (review required)`);
207
- } else console.log(JSON.stringify(plan, null, 2));
208
- return;
209
- }
210
- if (command === 'timeline' && commandArgs[0] === 'apply-plan' && commandArgs.length === 3) {
211
- const [, timelinePath, planPath] = commandArgs;
212
- console.log(JSON.stringify(applyTimelineReviewPlan(resolve(timelinePath), readJson(resolve(planPath))), null, 2));
213
- return;
214
- }
215
- if (command === 'timeline' && commandArgs[0] === 'project-edit' && commandArgs.length >= 3 && commandArgs.length <= 4) {
216
- const [, timelinePath, operationPath, requestedLimit] = commandArgs;
217
- const limit = requestedLimit === undefined ? undefined : Number(requestedLimit);
218
- const result = editTimelineFile(resolve(timelinePath), readJson(resolve(operationPath)), limit === undefined ? {} : { limit });
219
- console.log(JSON.stringify(result, null, 2));
220
- return;
221
- }
222
- if (command === 'timeline' && commandArgs[0] === 'undo' && commandArgs.length === 2) {
223
- console.log(JSON.stringify(undoTimelineFile(resolve(commandArgs[1])), null, 2));
224
- return;
225
- }
226
- if (command === 'timeline' && commandArgs[0] === 'redo' && commandArgs.length === 2) {
227
- console.log(JSON.stringify(redoTimelineFile(resolve(commandArgs[1])), null, 2));
228
- return;
229
- }
230
- if (command === 'timeline' && commandArgs[0] === 'history' && commandArgs.length >= 2 && commandArgs.length <= 4) {
231
- const [, timelinePath, action = 'show', requestedLimit] = commandArgs;
232
- if (action === 'show' && requestedLimit === undefined) {
233
- console.log(JSON.stringify(getTimelineFileHistory(resolve(timelinePath)), null, 2));
234
- return;
235
- }
236
- if (action === 'reset') {
237
- const limit = requestedLimit === undefined ? undefined : Number(requestedLimit);
238
- console.log(JSON.stringify(resetTimelineFileHistory(resolve(timelinePath), limit === undefined ? {} : { limit }), null, 2));
239
- return;
240
- }
241
- throw new Error('Usage: timeline history <timeline.json> [show|reset [limit]]');
242
- }
243
- if (command === 'timeline' && commandArgs[0] === 'captions' && commandArgs.length >= 3 && commandArgs.length <= 4) {
244
- const [, transcriptPath, outputPath, optionsPath] = commandArgs;
245
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
246
- writeNewFile(resolve(outputPath), createCaptionTrack(readJson(resolve(transcriptPath)), options));
247
- console.log(`Wrote ${resolve(outputPath)}`);
248
- return;
249
- }
250
- if (command === 'edit' && commandArgs.length >= 4 && commandArgs.length <= 6) {
251
- const [project, assetId, transcriptPath, edlPath, requestedOutputPath, requestedQuality] = commandArgs;
252
- const qualityOnly = commandArgs.length === 5 && ['draft', 'standard', 'high'].includes(requestedOutputPath);
253
- const outputPath = qualityOnly ? undefined : requestedOutputPath;
254
- const quality = qualityOnly ? requestedOutputPath : requestedQuality ?? 'draft';
255
- const result = await runWithInterrupt((signal) => renderEditedProjectAsync(resolve(project), assetId, readJson(resolve(transcriptPath)), readJson(resolve(edlPath)), {
256
- ...(outputPath ? { outputPath: resolve(outputPath) } : {}),
257
- quality,
258
- signal,
259
- }));
260
- console.log(JSON.stringify(result, null, 2));
261
- return;
262
- }
263
- if (command === 'timeline' && commandArgs[0] === 'render' && commandArgs.length >= 3 && commandArgs.length <= 6) {
264
- const [, project, timelinePath, outputPath, quality = 'draft', optionsPath] = commandArgs;
265
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
266
- console.log(JSON.stringify(await runWithInterrupt((signal) => renderTimelineAsync(resolve(project), readJson(resolve(timelinePath)), outputPath ? resolve(outputPath) : undefined, quality, { ...options, signal })), null, 2));
267
- return;
268
- }
269
- if (command === 'timeline' && commandArgs[0] === 'render-range' && commandArgs.length >= 6 && commandArgs.length <= 8) {
270
- const [, project, timelinePath, startText, endText, outputPath, quality = 'draft', optionsPath] = commandArgs;
271
- const startSeconds = Number(startText);
272
- const endSeconds = Number(endText);
273
- if (!Number.isFinite(startSeconds) || !Number.isFinite(endSeconds)) throw new TypeError('Timeline range start and end must be numbers in seconds.');
274
- const timeline = readJson(resolve(timelinePath));
275
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
276
- console.log(JSON.stringify(await runWithInterrupt((signal) => renderTimelineRangeAsync(resolve(project), timeline, startSeconds, endSeconds, resolve(outputPath), quality, { ...options, signal })), null, 2));
277
- return;
278
- }
279
- if (command === 'timeline' && commandArgs[0] === 'from-edl' && commandArgs.length >= 4 && commandArgs.length <= 5) {
280
- const [, edlPath, assetId, outputPath, optionsPath] = commandArgs;
281
- const edl = readJson(resolve(edlPath));
282
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
283
- writeNewFile(resolve(outputPath), timelineFromEdl(edl, assetId, options));
284
- console.log(`Wrote ${resolve(outputPath)}`);
285
- return;
286
- }
287
- if (command === 'retime') {
288
- const [transcriptPath, edlPath, outputPath, ...extra] = commandArgs;
289
- if (!transcriptPath || !edlPath || !outputPath || extra.length) {
290
- throw new Error('Usage: aivee retime <transcript.json> <approved-edl.json> <new-output.json>');
291
- }
292
- const transcript = readJson(resolve(transcriptPath));
293
- const edl = readJson(resolve(edlPath));
294
- const result = retimeTranscript(transcript, edl);
295
- const destination = resolve(outputPath);
296
- writeNewFile(destination, result);
297
- console.log(`Wrote ${destination}`);
298
- return;
299
- }
300
- if (command === 'project' && commandArgs[0] === 'create' && commandArgs.length >= 2) {
301
- const [, slug, ...tokens] = commandArgs;
302
- const options = parseProjectOptions(tokens);
303
- const projectPath = createVideoProject(process.cwd(), slug, options);
304
- console.log(`Created ${projectPath}`);
305
- return;
306
- }
307
- if (command === 'asset' && commandArgs[0] === 'register-many' && commandArgs.length >= 3) {
308
- const [, project, ...mediaPaths] = commandArgs;
309
- let reportedBatchPercent = -1;
310
- const reportedAssetProgress = new Map();
311
- const result = await runWithInterrupt((signal) => registerMediaAssetsAsync(resolve(project), mediaPaths, {
312
- signal,
313
- onProgress(event) {
314
- if (event.stage === 'asset-batch' && event.phase === 'started') {
315
- process.stderr.write(`Media batch: started (${event.total} sources)\n`);
316
- } else if (event.stage === 'asset-batch' && event.phase === 'completed') {
317
- process.stderr.write(`Media batch: complete (${event.registered} registered, ${event.failed ?? 0} failed)\n`);
318
- } else if (event.stage === 'asset-batch' && event.phase === 'progress') {
319
- const percent = Math.floor(event.percent);
320
- if (percent > reportedBatchPercent) {
321
- reportedBatchPercent = percent;
322
- process.stderr.write(`Media batch: ${event.completed}/${event.total} (${percent}%)\n`);
323
- }
324
- } else if (event.phase === 'progress' && Number.isFinite(event.percent)) {
325
- const key = `${event.assetId}:${event.stage}`;
326
- const percent = Math.floor(event.percent);
327
- if (percent > (reportedAssetProgress.get(key) ?? -1)) {
328
- reportedAssetProgress.set(key, percent);
329
- process.stderr.write(`${event.assetId}: ${event.stage} ${percent}%\n`);
330
- }
331
- } else if (event.phase === 'started') {
332
- process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} started\n`);
333
- } else if (event.phase === 'completed' && event.stage !== 'asset-batch') {
334
- process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} complete\n`);
335
- } else if (event.phase === 'failed') {
336
- process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} failed\n`);
337
- }
338
- },
339
- }), 'Batch asset registration');
340
- console.log(JSON.stringify(result, null, 2));
341
- if (result.failed.length > 0) process.exitCode = 1;
342
- return;
343
- }
344
- if (command === 'asset' && commandArgs[0] === 'register' && commandArgs.length >= 3 && commandArgs.length <= 4) {
345
- const [, project, media, id] = commandArgs;
346
- const reportedAssetProgress = new Map();
347
- const asset = await runWithInterrupt((signal) => registerMediaAssetAsync(resolve(project), media, {
348
- ...(id ? { id } : {}),
349
- signal,
350
- onProgress(event) {
351
- if (event.phase === 'progress' && Number.isFinite(event.percent)) {
352
- const percent = Math.floor(event.percent);
353
- if (percent > (reportedAssetProgress.get(event.stage) ?? -1)) {
354
- reportedAssetProgress.set(event.stage, percent);
355
- process.stderr.write(`${event.stage === 'asset-copy' ? 'Media copy' : 'Media publish'}: ${percent}%\n`);
356
- }
357
- } else if (event.phase === 'started') {
358
- process.stderr.write(event.stage + ': started\n');
359
- } else if (event.phase === 'completed') {
360
- process.stderr.write(event.stage + ': complete\n');
361
- } else if (event.phase === 'failed') {
362
- process.stderr.write(event.stage + ': failed\n');
363
- }
364
- },
365
- }), 'Asset registration');
366
- console.log(JSON.stringify(asset, null, 2));
367
- return;
368
- }
369
- if (command === 'asset' && commandArgs[0] === 'list' && commandArgs.length === 2) {
370
- console.log(JSON.stringify(listProjectAssets(resolve(commandArgs[1])), null, 2));
371
- return;
372
- }
373
- if (command === 'asset' && commandArgs[0] === 'unregister' && commandArgs.length === 3) {
374
- console.log(JSON.stringify(unregisterProjectAsset(resolve(commandArgs[1]), commandArgs[2]), null, 2));
375
- return;
376
- }
377
- if (command === 'asset' && commandArgs[0] === 'verify' && commandArgs.length === 2) {
378
- let reportedVerifyPercent = -1;
379
- const results = await runWithInterrupt((signal) => verifyProjectAssetsAsync(resolve(commandArgs[1]), {
380
- signal,
381
- onProgress(event) {
382
- if (event.phase === 'progress') {
383
- const percent = Math.floor(event.percent);
384
- if (percent > reportedVerifyPercent) {
385
- reportedVerifyPercent = percent;
386
- process.stderr.write('Asset integrity scan: ' + percent + '%\n');
387
- }
388
- } else if (event.phase === 'started') {
389
- process.stderr.write('Asset integrity scan: started\n');
390
- } else if (event.phase === 'completed') {
391
- process.stderr.write('Asset integrity scan: complete\n');
392
- } else if (event.phase === 'failed') {
393
- process.stderr.write('Asset integrity scan: failed\n');
394
- }
395
- },
396
- }), 'Asset verification');
397
- console.log(JSON.stringify(results, null, 2));
398
- if (results.some((asset) => asset.status !== 'ok')) process.exitCode = 1;
399
- return;
400
- }
401
- if (command === 'asset' && commandArgs[0] === 'relink' && commandArgs.length === 4) {
402
- const [, project, assetId, media] = commandArgs;
403
- let reportedRelinkPercent = -1;
404
- const asset = await runWithInterrupt((signal) => relinkProjectAssetAsync(resolve(project), assetId, resolve(media), {
405
- signal,
406
- onProgress(event) {
407
- if (event.stage === 'asset-relink-copy' && event.phase === 'progress') {
408
- const percent = Math.floor(event.percent);
409
- if (percent > reportedRelinkPercent) {
410
- reportedRelinkPercent = percent;
411
- process.stderr.write('Replacement media copy: ' + percent + '%\n');
412
- }
413
- } else if (event.phase === 'started') {
414
- process.stderr.write(event.stage + ': started\n');
415
- } else if (event.phase === 'completed') {
416
- process.stderr.write(event.stage + ': complete\n');
417
- } else if (event.phase === 'failed') {
418
- process.stderr.write(event.stage + ': failed\n');
419
- }
420
- },
421
- }), 'Asset relinking');
422
- console.log(JSON.stringify(asset, null, 2));
423
- return;
424
- }
425
- if (command === 'asset' && commandArgs[0] === 'proxy' && commandArgs[1] === 'create' && commandArgs.length >= 4 && commandArgs.length <= 7) {
426
- const [, , project, assetId, maxDimension, crf, frameRate] = commandArgs;
427
- let reportedProxyPercent = -1;
428
- const proxy = await runWithInterrupt((signal) => createAssetProxyAsync(resolve(project), assetId, {
429
- ...(maxDimension ? { maxDimension: Number(maxDimension) } : {}),
430
- ...(crf ? { crf: Number(crf) } : {}),
431
- ...(frameRate ? { frameRate: normalizeFrameRate(frameRate) } : {}),
432
- signal,
433
- onProgress(event) {
434
- if (event.stage === 'proxy-generation' && event.phase === 'progress') {
435
- const percent = Math.floor(event.percent);
436
- if (percent > reportedProxyPercent) {
437
- reportedProxyPercent = percent;
438
- process.stderr.write(`Proxy encoding: ${percent}%\n`);
439
- }
440
- } else if (event.phase === 'started') {
441
- process.stderr.write(`${event.stage}: started\n`);
442
- } else if (event.phase === 'completed') {
443
- process.stderr.write(`${event.stage}: complete\n`);
444
- } else if (event.phase === 'skipped') {
445
- process.stderr.write(`${event.stage}: skipped (${event.reason})\n`);
446
- }
447
- },
448
- }), 'Proxy generation');
449
- console.log(JSON.stringify(proxy, null, 2));
450
- return;
451
- }
452
- if (command === 'asset' && commandArgs[0] === 'preview' && (commandArgs.length === 3 || commandArgs.length === 4)) {
453
- const [, project, assetId, optionsPath] = commandArgs;
454
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
455
- const reportedPreviewProgress = new Map();
456
- const preview = await runWithInterrupt((signal) => generateAssetPreview(resolve(project), assetId, {
457
- ...options,
458
- signal,
459
- onProgress(event) {
460
- const stageName = event.stage === 'preview' ? '' : event.stage.replace('preview-', '');
461
- const label = `Asset preview${stageName ? ` ${stageName}` : ''}`;
462
- if (event.phase === 'progress' && Number.isFinite(event.percent)) {
463
- const percent = Math.floor(event.percent);
464
- if (percent > (reportedPreviewProgress.get(event.stage) ?? -1)) {
465
- reportedPreviewProgress.set(event.stage, percent);
466
- process.stderr.write(`${label}: ${percent}%\n`);
467
- }
468
- } else if (event.phase === 'started') {
469
- process.stderr.write(`${label}: started\n`);
470
- } else if (event.phase === 'completed') {
471
- process.stderr.write(`${label}: complete\n`);
472
- } else if (event.phase === 'skipped') {
473
- process.stderr.write(`${label}: skipped (${event.reason})\n`);
474
- } else if (event.phase === 'failed') {
475
- process.stderr.write(`${label}: failed\n`);
476
- }
477
- },
478
- }), 'Asset preview');
479
- console.log(JSON.stringify(preview, null, 2));
480
- return;
481
- }
482
- if (command === 'probe' && commandArgs.length === 1) {
483
- const media = await runWithInterrupt((signal) => probeMediaAsync(commandArgs[0], { requireVideo: false, signal }), 'Media probe');
484
- console.log(JSON.stringify(media, null, 2));
485
- return;
486
- }
487
- if (command === 'media' && commandArgs[0] === 'support' && commandArgs.length === 2) {
488
- const report = await runWithInterrupt((signal) => inspectMediaCompatibility(commandArgs[1], { signal }), 'Media support inspection');
489
- console.log(JSON.stringify(report, null, 2));
490
- if (!report.compatible) process.exitCode = 1;
491
- return;
492
- }
493
- if (command === 'apply-edl' && commandArgs.length === 3) {
494
- const [videoPath, edlPath, outputPath] = commandArgs;
495
- const result = renderCuts(videoPath, readJson(resolve(edlPath)), outputPath);
496
- console.log(JSON.stringify(result, null, 2));
497
- return;
498
- }
499
- if (command === 'plan-silence' && commandArgs.length === 2) {
500
- const [transcriptPath, outputPath] = commandArgs;
501
- writeNewFile(resolve(outputPath), planSilenceCuts(readJson(resolve(transcriptPath))));
502
- console.log(`Wrote ${resolve(outputPath)}`);
503
- return;
504
- }
505
- if (command === 'detect-silence' && commandArgs.length >= 2 && commandArgs.length <= 3) {
506
- const [mediaPath, outputPath, optionsPath] = commandArgs;
507
- writeNewFile(resolve(outputPath), detectAcousticSilence(resolve(mediaPath), optionsPath ? readJson(resolve(optionsPath)) : {}));
508
- console.log(`Wrote ${resolve(outputPath)}`);
509
- return;
510
- }
511
- if (command === 'analyze' && commandArgs.length >= 3 && commandArgs.length <= 4) {
512
- const [mediaPath, providerPath, outputPath, optionsPath] = commandArgs;
513
- const providerModule = await import(pathToFileURL(resolve(providerPath)).href);
514
- const provider = providerModule.default ?? providerModule.mediaAnalysisProvider ?? providerModule.provider;
515
- const result = await analyzeMediaWithProvider(resolve(mediaPath), provider, {
516
- ...(optionsPath ? { providerOptions: readJson(resolve(optionsPath)) } : {}),
517
- });
518
- writeNewFile(resolve(outputPath), result);
519
- console.log(`Wrote ${resolve(outputPath)}`);
520
- return;
521
- }
522
- if (command === 'detect-scenes' && commandArgs.length >= 2 && commandArgs.length <= 3) {
523
- const [mediaPath, outputPath, optionsPath] = commandArgs;
524
- const result = await runWithInterrupt((signal) => detectSceneChangesAsync(resolve(mediaPath), {
525
- ...(optionsPath ? readJson(resolve(optionsPath)) : {}),
526
- signal,
527
- }), 'Scene detection');
528
- writeNewFile(resolve(outputPath), result);
529
- console.log(`Wrote ${resolve(outputPath)} (review required)`);
530
- return;
531
- }
532
- if (command === 'scene-review' && commandArgs.length >= 2 && commandArgs.length <= 3) {
533
- const [mediaPath, outputDirectory, optionsPath] = commandArgs;
534
- const result = await runWithInterrupt((signal) => createSceneReviewPackAsync(
535
- resolve(mediaPath),
536
- resolve(outputDirectory),
537
- {
538
- ...(optionsPath ? readJson(resolve(optionsPath)) : {}),
539
- signal,
540
- onProgress(event) {
541
- if (event.phase === 'progress' && Number.isFinite(event.percent)) {
542
- process.stderr.write(`Scene thumbnails: ${Math.floor(event.percent)}% (${event.completed}/${event.total})\n`);
543
- } else if (event.phase === 'started') {
544
- process.stderr.write(`${event.stage}: started\n`);
545
- } else if (event.phase === 'completed') {
546
- process.stderr.write(`${event.stage}: complete\n`);
547
- } else if (event.phase === 'failed') {
548
- process.stderr.write(`${event.stage}: failed\n`);
549
- }
550
- },
551
- },
552
- ), 'Scene review pack generation');
553
- console.log(JSON.stringify(result, null, 2));
554
- return;
555
- }
556
- if (command === 'analysis-to-edl' && commandArgs.length >= 2 && commandArgs.length <= 3) {
557
- const [analysisPath, outputPath, optionsPath] = commandArgs;
558
- const result = readJson(resolve(analysisPath));
559
- writeNewFile(resolve(outputPath), mediaAnalysisToEdl(result.analysis ?? result, optionsPath ? readJson(resolve(optionsPath)) : {}));
560
- console.log(`Wrote ${resolve(outputPath)} (review required)`);
561
- return;
562
- }
563
- if (command === 'approve-edl' && commandArgs.length === 2) {
564
- const [candidatePath, outputPath] = commandArgs;
565
- writeNewFile(resolve(outputPath), approveEdl(readJson(resolve(candidatePath))));
566
- console.log(`Wrote ${resolve(outputPath)} (approved)`);
567
- return;
568
- }
569
- if (command === 'transcript' && commandArgs[0] === 'correct' && commandArgs.length === 4) {
570
- const [, transcriptPath, correctionsPath, outputPath] = commandArgs;
571
- writeNewFile(resolve(outputPath), applyTranscriptCorrections(readJson(resolve(transcriptPath)), readJson(resolve(correctionsPath))));
572
- console.log(`Wrote ${resolve(outputPath)}`);
573
- return;
574
- }
575
- if (command === 'transcript' && commandArgs[0] === 'export' && commandArgs.length === 4) {
576
- const [, transcriptPath, format, outputPath] = commandArgs;
577
- writeNewTextFile(resolve(outputPath), formatSubtitleText(readJson(resolve(transcriptPath)), format));
578
- console.log(`Wrote ${resolve(outputPath)}`);
579
- return;
580
- }
581
- if (command === 'mistake-candidates' && commandArgs.length === 1) {
582
- console.log(JSON.stringify(findMistakeCandidates(readJson(resolve(commandArgs[0]))), null, 2));
583
- return;
584
- }
585
- if (command === 'lint' && commandArgs.length === 1) {
586
- lintProject(commandArgs[0]);
587
- return;
588
- }
589
- if (command === 'preview' && commandArgs.length === 1) {
590
- previewProject(commandArgs[0]);
591
- return;
592
- }
593
- if (command === 'render' && commandArgs.length >= 1 && commandArgs.length <= 4) {
594
- const [projectPath, outputPath, quality = 'draft', optionsPath] = commandArgs;
595
- const options = optionsPath ? readJson(resolve(optionsPath)) : {};
596
- console.log(JSON.stringify(await runWithInterrupt((signal) => renderProjectAsync(projectPath, outputPath, quality, { ...options, signal })), null, 2));
597
- return;
598
- }
599
- if (command === 'transcribe' && commandArgs.length >= 2 && commandArgs.length <= 4) {
600
- const [projectPath, mediaPath, model, language] = commandArgs;
601
- console.log(JSON.stringify(transcribeProject(projectPath, mediaPath, { model, language }), null, 2));
602
- return;
603
- }
604
- if (command === 'import-transcript' && commandArgs.length === 3) {
605
- const [projectPath, mediaPath, transcriptPath] = commandArgs;
606
- console.log(JSON.stringify(importProjectTranscript(projectPath, mediaPath, transcriptPath), null, 2));
607
- return;
608
- }
609
- throw new Error(`Usage: aivee
610
- project create <slug> [WIDTHxHEIGHT] [fps] [durationSeconds] [--fit <contain|cover|fill>] [--background-color <hex|transparent>]
611
- project create <slug> --preset <name> [fps] [durationSeconds] [--fit <contain|cover|fill>] [--background-color <hex|transparent>]
612
- presets list | probe <media> | media support <media>
613
- asset register <project> <media> [assetId] | asset register-many <project> <media>...
614
- asset unregister <project> <asset-id>
615
- asset verify <project>
616
- asset relink <project> <assetId> <replacement-media>
617
- asset proxy create <project> <assetId> [maxDimension] [crf] [frameRate]
618
- edit <project> <assetId> <transcript.json> <approved-edl.json> [output.mp4] [draft|standard|high]
619
- timeline create <project> [output.json|durationSeconds] [durationSeconds] | timeline validate <timeline.json> | timeline preview-edit <timeline.json> <operation.json> | timeline plan-broll <timeline.json> <scene-review.json> <asset-id> <video-track-id> <placements.json> [new-plan.json] | timeline apply-plan <timeline.json> <review-plan.json> | timeline edit <timeline.json> <operation.json> <new-timeline.json>
620
- timeline project-edit <timeline.json> <operation.json> [history-limit] | timeline undo|redo <timeline.json> | timeline history <timeline.json> [show|reset [limit]]
621
- timeline export-otio <project> <timeline.json> <new-output.otio>
622
- timeline import-otio <project> <input.otio> <new-timeline.json>
623
- timeline captions <transcript.json> <new-track.json> [options.json]
624
- timeline from-edl <approved-edl.json> <assetId> <new-timeline.json> [options.json]
625
- timeline render <project> <timeline.json> [output.mp4] [draft|standard|high]
626
- timeline render-range <project> <timeline.json> <start-seconds> <end-seconds> <output.mp4> [draft|standard|high] [options.json]
627
- transcribe <project> <media> [model] [language] | import-transcript <project> <media> <transcript>
628
- plan-silence <transcript.json> <new-edl.json> | detect-silence <media> <new-edl.json> [options.json]
629
- analyze <media> <provider-module.mjs> <new-analysis.json> [provider-options.json]
630
- detect-scenes <media> <new-analysis.json> [options.json]
631
- scene-review <media> <new-output-directory> [options.json]
632
- analysis-to-edl <analysis.json> <review-required-edl.json> [options.json]
633
- approve-edl <reviewed-edl.json> <new-approved-edl.json>
634
- mistake-candidates <transcript.json> | transcript correct <transcript.json> <corrections.json> <new-transcript.json>
635
- transcript export <transcript.json> <srt|vtt> <new-subtitles-file>
636
- apply-edl <media> <approved-edl.json> <output.mp4> | retime <transcript.json> <approved-edl.json> <new-output.json>
637
- asset preview <project> <asset-id> [options.json] |
638
- lint <project> | preview <project> | render <project> [output.mp4|output.webm|output.mov|output.gif] [draft|standard|high]`);
639
- }
640
-
641
- try {
642
- await main(process.argv.slice(2));
643
- } catch (error) {
644
- console.error(error.message);
645
- process.exitCode = 1;
646
- }
1
+ #!/usr/bin/env node
2
+ import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { approveEdl, retimeTranscript } from './edits/retime-transcript.mjs';
6
+ import { mediaAnalysisToEdl } from './edits/analysis-to-edl.mjs';
7
+ import { detectAcousticSilence } from './edits/detect-silence.mjs';
8
+ import { createVideoProject } from './project/create.mjs';
9
+ import { listCanvasPresets } from './project/presets.mjs';
10
+ import { createAssetProxyAsync, listProjectAssets, registerMediaAssetAsync, registerMediaAssetsAsync, relinkProjectAssetAsync, unregisterProjectAsset, verifyProjectAssetsAsync } from './project/assets.mjs';
11
+ import { probeMediaAsync } from './media/probe.mjs';
12
+ import { inspectMediaCompatibility } from './media/compatibility.mjs';
13
+ import { generateAssetPreview } from './media/preview-assets.mjs';
14
+ import { analyzeMediaWithProvider } from './media/analysis-provider.mjs';
15
+ import { detectSceneChangesAsync } from './media/scene-detection.mjs';
16
+ import { createSceneReviewPackAsync } from './media/scene-review.mjs';
17
+ import { renderCuts } from './media/render-cuts.mjs';
18
+ import { planSilenceCuts } from './edits/plan-silence-cuts.mjs';
19
+ import { findMistakeCandidates } from './edits/find-mistake-candidates.mjs';
20
+ import { planSceneReviewPlacements } from './edits/plan-shot-placements.mjs';
21
+ import { validateTimeline } from './timeline/validate.mjs';
22
+ import { inspectTimeline } from './timeline/inspect.mjs';
23
+ import { timelineFromEdl } from './timeline/from-edl.mjs';
24
+ import { renderTimelineAsync, renderTimelineRangeAsync } from './timeline/render.mjs';
25
+ import { editTimeline, editTimelineBatch, previewTimelineEdit } from './timeline/edit.mjs';
26
+ import { applyTimelineReviewPlan, editTimelineFile, getTimelineFileHistory, redoTimelineFile, resetTimelineFileHistory, undoTimelineFile } from './timeline/history-store.mjs';
27
+ import { createProjectTimeline } from './timeline/project.mjs';
28
+ import { createCaptionTrack } from './timeline/captions.mjs';
29
+ import { applyTranscriptCorrections } from './transcript/corrections.mjs';
30
+ import { formatSubtitleText } from './transcript/subtitles.mjs';
31
+ import { renderEditedProjectAsync } from './timeline/assemble.mjs';
32
+ import { importProjectTranscript, lintProject, previewProject, renderProjectAsync, transcribeProject } from './render/hyperframes.mjs';
33
+ import { normalizeFrameRate } from './media/frame-rate.mjs';
34
+ import { exportTimelineToOtio, importTimelineFromOtio } from './interchange/otio.mjs';
35
+ import { getEngineCapabilities } from './capabilities.mjs';
36
+
37
+ function readJson(path) {
38
+ try {
39
+ return JSON.parse(readFileSync(path, 'utf8'));
40
+ } catch (error) {
41
+ throw new Error(`Could not read JSON file ${path}: ${error.message}`);
42
+ }
43
+ }
44
+
45
+ function writeNewFile(path, data) {
46
+ writeExclusiveFile(path, `${JSON.stringify(data, null, 2)}\n`);
47
+ }
48
+
49
+ function writeNewTextFile(path, data) {
50
+ writeExclusiveFile(path, data);
51
+ }
52
+
53
+ function writeExclusiveFile(path, data) {
54
+ let descriptor;
55
+ try {
56
+ descriptor = openSync(path, 'wx');
57
+ writeFileSync(descriptor, data);
58
+ closeSync(descriptor);
59
+ descriptor = undefined;
60
+ } catch (error) {
61
+ if (descriptor !== undefined) {
62
+ try { closeSync(descriptor); } catch {}
63
+ try { unlinkSync(path); } catch {}
64
+ }
65
+ if (error.code === 'EEXIST') throw new Error(`Output already exists: ${path}`);
66
+ throw new Error(`Could not write output ${path}: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ async function runWithInterrupt(run, operation = 'Render') {
71
+ const controller = new AbortController();
72
+ const onInterrupt = () => {
73
+ const error = new Error(`${operation} cancelled by Ctrl+C.`);
74
+ error.name = 'AbortError';
75
+ controller.abort(error);
76
+ };
77
+ process.once('SIGINT', onInterrupt);
78
+ try {
79
+ return await run(controller.signal);
80
+ } finally {
81
+ process.removeListener('SIGINT', onInterrupt);
82
+ }
83
+ }
84
+
85
+ function parseProjectOptions(args) {
86
+ const positional = [];
87
+ const flags = {};
88
+ for (let index = 0; index < args.length; index++) {
89
+ const token = args[index];
90
+ if (token === '--preset' || token === '--fit' || token === '--background-color') {
91
+ if (flags[token]) throw new Error(`${token} can only be specified once.`);
92
+ const value = args[++index];
93
+ if (!value || value.startsWith('--')) throw new Error(`${token} requires a value.`);
94
+ flags[token] = value;
95
+ } else if (token.startsWith('--')) {
96
+ throw new Error(`Unknown project option: ${token}`);
97
+ } else {
98
+ positional.push(token);
99
+ }
100
+ }
101
+
102
+ if (flags['--preset']) {
103
+ if (positional.length > 2 || (positional[0] && !/^\d+(?:\.\d+|\/\d+)?$/.test(positional[0]))) {
104
+ throw new Error('Usage: project create <slug> --preset <name> [fps] [durationSeconds] [--fit <contain|cover|fill>]');
105
+ }
106
+ return {
107
+ preset: flags['--preset'],
108
+ fps: normalizeFrameRate(positional[0] ?? '30'),
109
+ durationSeconds: Number(positional[1] ?? 10),
110
+ ...(flags['--fit'] ? { sourceFit: flags['--fit'] } : {}),
111
+ ...(flags['--background-color'] ? { backgroundColor: parseBackgroundColorFlag(flags['--background-color']) } : {}),
112
+ };
113
+ }
114
+
115
+ if (positional.length > 3) {
116
+ throw new Error('Usage: project create <slug> [WIDTHxHEIGHT] [fps] [durationSeconds] [--fit <contain|cover|fill>]');
117
+ }
118
+ const [size = '1920x1080', fps = '30', durationSeconds = '10'] = positional;
119
+ const match = /^(\d+)x(\d+)$/i.exec(size);
120
+ if (!match) throw new Error('Canvas size must use WIDTHxHEIGHT, such as 1080x1920.');
121
+ return {
122
+ width: Number(match[1]),
123
+ height: Number(match[2]),
124
+ fps: normalizeFrameRate(fps),
125
+ durationSeconds: Number(durationSeconds),
126
+ ...(flags['--fit'] ? { sourceFit: flags['--fit'] } : {}),
127
+ ...(flags['--background-color'] ? { backgroundColor: parseBackgroundColorFlag(flags['--background-color']) } : {}),
128
+ };
129
+ }
130
+
131
+ function parseBackgroundColorFlag(value) {
132
+ return value.toLowerCase() === 'transparent' ? null : value;
133
+ }
134
+
135
+ async function main(args) {
136
+ const [command, ...commandArgs] = args;
137
+ if (command === 'capabilities' && commandArgs.length === 0) {
138
+ console.log(JSON.stringify(getEngineCapabilities(), null, 2));
139
+ return;
140
+ }
141
+ if (command === 'presets' && commandArgs.length === 1 && commandArgs[0] === 'list') {
142
+ console.log(JSON.stringify(listCanvasPresets(), null, 2));
143
+ return;
144
+ }
145
+ if (command === 'timeline' && commandArgs[0] === 'validate' && commandArgs.length === 2) {
146
+ validateTimeline(readJson(resolve(commandArgs[1])));
147
+ console.log('Timeline valid.');
148
+ return;
149
+ }
150
+ if (command === 'timeline' && commandArgs[0] === 'create' && commandArgs.length >= 2 && commandArgs.length <= 4) {
151
+ const [, projectPath, secondArgument, thirdArgument] = commandArgs;
152
+ const secondIsDuration = thirdArgument === undefined && secondArgument !== undefined && /^\d+(?:\.\d+)?$/.test(secondArgument);
153
+ const outputPath = secondIsDuration ? undefined : secondArgument;
154
+ const requestedDuration = secondIsDuration ? secondArgument : thirdArgument;
155
+ const options = requestedDuration === undefined ? {} : { durationSeconds: Number(requestedDuration) };
156
+ const timeline = createProjectTimeline(resolve(projectPath), options);
157
+ const destination = resolve(outputPath ?? join(resolve(projectPath), 'timeline.json'));
158
+ writeNewFile(destination, timeline);
159
+ console.log(`Wrote ${destination}`);
160
+ return;
161
+ }
162
+ if (command === 'timeline' && commandArgs[0] === 'inspect' && commandArgs.length === 2) {
163
+ console.log(JSON.stringify(inspectTimeline(readJson(resolve(commandArgs[1]))), null, 2));
164
+ return;
165
+ }
166
+ if (command === 'timeline' && commandArgs[0] === 'export-otio' && commandArgs.length === 4) {
167
+ const [, project, timelinePath, outputPath] = commandArgs;
168
+ writeNewTextFile(resolve(outputPath), exportTimelineToOtio(resolve(project), readJson(resolve(timelinePath))));
169
+ console.log(`Wrote ${resolve(outputPath)}`);
170
+ return;
171
+ }
172
+ if (command === 'timeline' && commandArgs[0] === 'import-otio' && commandArgs.length === 4) {
173
+ const [, project, inputPath, outputPath] = commandArgs;
174
+ if (existsSync(resolve(outputPath))) throw new Error(`Output already exists: ${resolve(outputPath)}`);
175
+ writeNewFile(resolve(outputPath), importTimelineFromOtio(resolve(project), resolve(inputPath)));
176
+ console.log(`Wrote ${resolve(outputPath)}`);
177
+ return;
178
+ }
179
+ if (command === 'timeline' && commandArgs[0] === 'edit' && commandArgs.length === 4) {
180
+ const [, inputPath, operationPath, outputPath] = commandArgs;
181
+ const timeline = readJson(resolve(inputPath));
182
+ const operation = readJson(resolve(operationPath));
183
+ const edited = Array.isArray(operation)
184
+ ? editTimelineBatch(timeline, operation)
185
+ : editTimeline(timeline, operation);
186
+ writeNewFile(resolve(outputPath), edited);
187
+ console.log(`Wrote ${resolve(outputPath)}`);
188
+ return;
189
+ }
190
+ if (command === 'timeline' && commandArgs[0] === 'preview-edit' && commandArgs.length === 3) {
191
+ const [, inputPath, operationPath] = commandArgs;
192
+ console.log(JSON.stringify(previewTimelineEdit(readJson(resolve(inputPath)), readJson(resolve(operationPath))), null, 2));
193
+ return;
194
+ }
195
+ if (command === 'timeline' && commandArgs[0] === 'plan-broll' && commandArgs.length >= 6 && commandArgs.length <= 7) {
196
+ const [, timelinePath, sceneReviewPath, assetId, trackId, placementsPath, outputPath] = commandArgs;
197
+ const plan = planSceneReviewPlacements(
198
+ readJson(resolve(timelinePath)),
199
+ readJson(resolve(sceneReviewPath)),
200
+ assetId,
201
+ trackId,
202
+ readJson(resolve(placementsPath)),
203
+ );
204
+ if (outputPath) {
205
+ writeNewFile(resolve(outputPath), plan);
206
+ console.log(`Wrote ${resolve(outputPath)} (review required)`);
207
+ } else console.log(JSON.stringify(plan, null, 2));
208
+ return;
209
+ }
210
+ if (command === 'timeline' && commandArgs[0] === 'apply-plan' && commandArgs.length === 3) {
211
+ const [, timelinePath, planPath] = commandArgs;
212
+ console.log(JSON.stringify(applyTimelineReviewPlan(resolve(timelinePath), readJson(resolve(planPath))), null, 2));
213
+ return;
214
+ }
215
+ if (command === 'timeline' && commandArgs[0] === 'project-edit' && commandArgs.length >= 3 && commandArgs.length <= 4) {
216
+ const [, timelinePath, operationPath, requestedLimit] = commandArgs;
217
+ const limit = requestedLimit === undefined ? undefined : Number(requestedLimit);
218
+ const result = editTimelineFile(resolve(timelinePath), readJson(resolve(operationPath)), limit === undefined ? {} : { limit });
219
+ console.log(JSON.stringify(result, null, 2));
220
+ return;
221
+ }
222
+ if (command === 'timeline' && commandArgs[0] === 'undo' && commandArgs.length === 2) {
223
+ console.log(JSON.stringify(undoTimelineFile(resolve(commandArgs[1])), null, 2));
224
+ return;
225
+ }
226
+ if (command === 'timeline' && commandArgs[0] === 'redo' && commandArgs.length === 2) {
227
+ console.log(JSON.stringify(redoTimelineFile(resolve(commandArgs[1])), null, 2));
228
+ return;
229
+ }
230
+ if (command === 'timeline' && commandArgs[0] === 'history' && commandArgs.length >= 2 && commandArgs.length <= 4) {
231
+ const [, timelinePath, action = 'show', requestedLimit] = commandArgs;
232
+ if (action === 'show' && requestedLimit === undefined) {
233
+ console.log(JSON.stringify(getTimelineFileHistory(resolve(timelinePath)), null, 2));
234
+ return;
235
+ }
236
+ if (action === 'reset') {
237
+ const limit = requestedLimit === undefined ? undefined : Number(requestedLimit);
238
+ console.log(JSON.stringify(resetTimelineFileHistory(resolve(timelinePath), limit === undefined ? {} : { limit }), null, 2));
239
+ return;
240
+ }
241
+ throw new Error('Usage: timeline history <timeline.json> [show|reset [limit]]');
242
+ }
243
+ if (command === 'timeline' && commandArgs[0] === 'captions' && commandArgs.length >= 3 && commandArgs.length <= 4) {
244
+ const [, transcriptPath, outputPath, optionsPath] = commandArgs;
245
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
246
+ writeNewFile(resolve(outputPath), createCaptionTrack(readJson(resolve(transcriptPath)), options));
247
+ console.log(`Wrote ${resolve(outputPath)}`);
248
+ return;
249
+ }
250
+ if (command === 'edit' && commandArgs.length >= 4 && commandArgs.length <= 6) {
251
+ const [project, assetId, transcriptPath, edlPath, requestedOutputPath, requestedQuality] = commandArgs;
252
+ const qualityOnly = commandArgs.length === 5 && ['draft', 'standard', 'high'].includes(requestedOutputPath);
253
+ const outputPath = qualityOnly ? undefined : requestedOutputPath;
254
+ const quality = qualityOnly ? requestedOutputPath : requestedQuality ?? 'draft';
255
+ const result = await runWithInterrupt((signal) => renderEditedProjectAsync(resolve(project), assetId, readJson(resolve(transcriptPath)), readJson(resolve(edlPath)), {
256
+ ...(outputPath ? { outputPath: resolve(outputPath) } : {}),
257
+ quality,
258
+ signal,
259
+ }));
260
+ console.log(JSON.stringify(result, null, 2));
261
+ return;
262
+ }
263
+ if (command === 'timeline' && commandArgs[0] === 'render' && commandArgs.length >= 3 && commandArgs.length <= 6) {
264
+ const [, project, timelinePath, outputPath, quality = 'draft', optionsPath] = commandArgs;
265
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
266
+ console.log(JSON.stringify(await runWithInterrupt((signal) => renderTimelineAsync(resolve(project), readJson(resolve(timelinePath)), outputPath ? resolve(outputPath) : undefined, quality, { ...options, signal })), null, 2));
267
+ return;
268
+ }
269
+ if (command === 'timeline' && commandArgs[0] === 'render-range' && commandArgs.length >= 6 && commandArgs.length <= 8) {
270
+ const [, project, timelinePath, startText, endText, outputPath, quality = 'draft', optionsPath] = commandArgs;
271
+ const startSeconds = Number(startText);
272
+ const endSeconds = Number(endText);
273
+ if (!Number.isFinite(startSeconds) || !Number.isFinite(endSeconds)) throw new TypeError('Timeline range start and end must be numbers in seconds.');
274
+ const timeline = readJson(resolve(timelinePath));
275
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
276
+ console.log(JSON.stringify(await runWithInterrupt((signal) => renderTimelineRangeAsync(resolve(project), timeline, startSeconds, endSeconds, resolve(outputPath), quality, { ...options, signal })), null, 2));
277
+ return;
278
+ }
279
+ if (command === 'timeline' && commandArgs[0] === 'from-edl' && commandArgs.length >= 4 && commandArgs.length <= 5) {
280
+ const [, edlPath, assetId, outputPath, optionsPath] = commandArgs;
281
+ const edl = readJson(resolve(edlPath));
282
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
283
+ writeNewFile(resolve(outputPath), timelineFromEdl(edl, assetId, options));
284
+ console.log(`Wrote ${resolve(outputPath)}`);
285
+ return;
286
+ }
287
+ if (command === 'retime') {
288
+ const [transcriptPath, edlPath, outputPath, ...extra] = commandArgs;
289
+ if (!transcriptPath || !edlPath || !outputPath || extra.length) {
290
+ throw new Error('Usage: aivee retime <transcript.json> <approved-edl.json> <new-output.json>');
291
+ }
292
+ const transcript = readJson(resolve(transcriptPath));
293
+ const edl = readJson(resolve(edlPath));
294
+ const result = retimeTranscript(transcript, edl);
295
+ const destination = resolve(outputPath);
296
+ writeNewFile(destination, result);
297
+ console.log(`Wrote ${destination}`);
298
+ return;
299
+ }
300
+ if (command === 'project' && commandArgs[0] === 'create' && commandArgs.length >= 2) {
301
+ const [, slug, ...tokens] = commandArgs;
302
+ const options = parseProjectOptions(tokens);
303
+ const projectPath = createVideoProject(process.cwd(), slug, options);
304
+ console.log(`Created ${projectPath}`);
305
+ return;
306
+ }
307
+ if (command === 'asset' && commandArgs[0] === 'register-many' && commandArgs.length >= 3) {
308
+ const [, project, ...mediaPaths] = commandArgs;
309
+ let reportedBatchPercent = -1;
310
+ const reportedAssetProgress = new Map();
311
+ const result = await runWithInterrupt((signal) => registerMediaAssetsAsync(resolve(project), mediaPaths, {
312
+ signal,
313
+ onProgress(event) {
314
+ if (event.stage === 'asset-batch' && event.phase === 'started') {
315
+ process.stderr.write(`Media batch: started (${event.total} sources)\n`);
316
+ } else if (event.stage === 'asset-batch' && event.phase === 'completed') {
317
+ process.stderr.write(`Media batch: complete (${event.registered} registered, ${event.failed ?? 0} failed)\n`);
318
+ } else if (event.stage === 'asset-batch' && event.phase === 'progress') {
319
+ const percent = Math.floor(event.percent);
320
+ if (percent > reportedBatchPercent) {
321
+ reportedBatchPercent = percent;
322
+ process.stderr.write(`Media batch: ${event.completed}/${event.total} (${percent}%)\n`);
323
+ }
324
+ } else if (event.phase === 'progress' && Number.isFinite(event.percent)) {
325
+ const key = `${event.assetId}:${event.stage}`;
326
+ const percent = Math.floor(event.percent);
327
+ if (percent > (reportedAssetProgress.get(key) ?? -1)) {
328
+ reportedAssetProgress.set(key, percent);
329
+ process.stderr.write(`${event.assetId}: ${event.stage} ${percent}%\n`);
330
+ }
331
+ } else if (event.phase === 'started') {
332
+ process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} started\n`);
333
+ } else if (event.phase === 'completed' && event.stage !== 'asset-batch') {
334
+ process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} complete\n`);
335
+ } else if (event.phase === 'failed') {
336
+ process.stderr.write(`${event.assetId ?? event.stage}: ${event.stage} failed\n`);
337
+ }
338
+ },
339
+ }), 'Batch asset registration');
340
+ console.log(JSON.stringify(result, null, 2));
341
+ if (result.failed.length > 0) process.exitCode = 1;
342
+ return;
343
+ }
344
+ if (command === 'asset' && commandArgs[0] === 'register' && commandArgs.length >= 3 && commandArgs.length <= 4) {
345
+ const [, project, media, id] = commandArgs;
346
+ const reportedAssetProgress = new Map();
347
+ const asset = await runWithInterrupt((signal) => registerMediaAssetAsync(resolve(project), media, {
348
+ ...(id ? { id } : {}),
349
+ signal,
350
+ onProgress(event) {
351
+ if (event.phase === 'progress' && Number.isFinite(event.percent)) {
352
+ const percent = Math.floor(event.percent);
353
+ if (percent > (reportedAssetProgress.get(event.stage) ?? -1)) {
354
+ reportedAssetProgress.set(event.stage, percent);
355
+ process.stderr.write(`${event.stage === 'asset-copy' ? 'Media copy' : 'Media publish'}: ${percent}%\n`);
356
+ }
357
+ } else if (event.phase === 'started') {
358
+ process.stderr.write(event.stage + ': started\n');
359
+ } else if (event.phase === 'completed') {
360
+ process.stderr.write(event.stage + ': complete\n');
361
+ } else if (event.phase === 'failed') {
362
+ process.stderr.write(event.stage + ': failed\n');
363
+ }
364
+ },
365
+ }), 'Asset registration');
366
+ console.log(JSON.stringify(asset, null, 2));
367
+ return;
368
+ }
369
+ if (command === 'asset' && commandArgs[0] === 'list' && commandArgs.length === 2) {
370
+ console.log(JSON.stringify(listProjectAssets(resolve(commandArgs[1])), null, 2));
371
+ return;
372
+ }
373
+ if (command === 'asset' && commandArgs[0] === 'unregister' && commandArgs.length === 3) {
374
+ console.log(JSON.stringify(unregisterProjectAsset(resolve(commandArgs[1]), commandArgs[2]), null, 2));
375
+ return;
376
+ }
377
+ if (command === 'asset' && commandArgs[0] === 'verify' && commandArgs.length === 2) {
378
+ let reportedVerifyPercent = -1;
379
+ const results = await runWithInterrupt((signal) => verifyProjectAssetsAsync(resolve(commandArgs[1]), {
380
+ signal,
381
+ onProgress(event) {
382
+ if (event.phase === 'progress') {
383
+ const percent = Math.floor(event.percent);
384
+ if (percent > reportedVerifyPercent) {
385
+ reportedVerifyPercent = percent;
386
+ process.stderr.write('Asset integrity scan: ' + percent + '%\n');
387
+ }
388
+ } else if (event.phase === 'started') {
389
+ process.stderr.write('Asset integrity scan: started\n');
390
+ } else if (event.phase === 'completed') {
391
+ process.stderr.write('Asset integrity scan: complete\n');
392
+ } else if (event.phase === 'failed') {
393
+ process.stderr.write('Asset integrity scan: failed\n');
394
+ }
395
+ },
396
+ }), 'Asset verification');
397
+ console.log(JSON.stringify(results, null, 2));
398
+ if (results.some((asset) => asset.status !== 'ok')) process.exitCode = 1;
399
+ return;
400
+ }
401
+ if (command === 'asset' && commandArgs[0] === 'relink' && commandArgs.length === 4) {
402
+ const [, project, assetId, media] = commandArgs;
403
+ let reportedRelinkPercent = -1;
404
+ const asset = await runWithInterrupt((signal) => relinkProjectAssetAsync(resolve(project), assetId, resolve(media), {
405
+ signal,
406
+ onProgress(event) {
407
+ if (event.stage === 'asset-relink-copy' && event.phase === 'progress') {
408
+ const percent = Math.floor(event.percent);
409
+ if (percent > reportedRelinkPercent) {
410
+ reportedRelinkPercent = percent;
411
+ process.stderr.write('Replacement media copy: ' + percent + '%\n');
412
+ }
413
+ } else if (event.phase === 'started') {
414
+ process.stderr.write(event.stage + ': started\n');
415
+ } else if (event.phase === 'completed') {
416
+ process.stderr.write(event.stage + ': complete\n');
417
+ } else if (event.phase === 'failed') {
418
+ process.stderr.write(event.stage + ': failed\n');
419
+ }
420
+ },
421
+ }), 'Asset relinking');
422
+ console.log(JSON.stringify(asset, null, 2));
423
+ return;
424
+ }
425
+ if (command === 'asset' && commandArgs[0] === 'proxy' && commandArgs[1] === 'create' && commandArgs.length >= 4 && commandArgs.length <= 7) {
426
+ const [, , project, assetId, maxDimension, crf, frameRate] = commandArgs;
427
+ let reportedProxyPercent = -1;
428
+ const proxy = await runWithInterrupt((signal) => createAssetProxyAsync(resolve(project), assetId, {
429
+ ...(maxDimension ? { maxDimension: Number(maxDimension) } : {}),
430
+ ...(crf ? { crf: Number(crf) } : {}),
431
+ ...(frameRate ? { frameRate: normalizeFrameRate(frameRate) } : {}),
432
+ signal,
433
+ onProgress(event) {
434
+ if (event.stage === 'proxy-generation' && event.phase === 'progress') {
435
+ const percent = Math.floor(event.percent);
436
+ if (percent > reportedProxyPercent) {
437
+ reportedProxyPercent = percent;
438
+ process.stderr.write(`Proxy encoding: ${percent}%\n`);
439
+ }
440
+ } else if (event.phase === 'started') {
441
+ process.stderr.write(`${event.stage}: started\n`);
442
+ } else if (event.phase === 'completed') {
443
+ process.stderr.write(`${event.stage}: complete\n`);
444
+ } else if (event.phase === 'skipped') {
445
+ process.stderr.write(`${event.stage}: skipped (${event.reason})\n`);
446
+ }
447
+ },
448
+ }), 'Proxy generation');
449
+ console.log(JSON.stringify(proxy, null, 2));
450
+ return;
451
+ }
452
+ if (command === 'asset' && commandArgs[0] === 'preview' && (commandArgs.length === 3 || commandArgs.length === 4)) {
453
+ const [, project, assetId, optionsPath] = commandArgs;
454
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
455
+ const reportedPreviewProgress = new Map();
456
+ const preview = await runWithInterrupt((signal) => generateAssetPreview(resolve(project), assetId, {
457
+ ...options,
458
+ signal,
459
+ onProgress(event) {
460
+ const stageName = event.stage === 'preview' ? '' : event.stage.replace('preview-', '');
461
+ const label = `Asset preview${stageName ? ` ${stageName}` : ''}`;
462
+ if (event.phase === 'progress' && Number.isFinite(event.percent)) {
463
+ const percent = Math.floor(event.percent);
464
+ if (percent > (reportedPreviewProgress.get(event.stage) ?? -1)) {
465
+ reportedPreviewProgress.set(event.stage, percent);
466
+ process.stderr.write(`${label}: ${percent}%\n`);
467
+ }
468
+ } else if (event.phase === 'started') {
469
+ process.stderr.write(`${label}: started\n`);
470
+ } else if (event.phase === 'completed') {
471
+ process.stderr.write(`${label}: complete\n`);
472
+ } else if (event.phase === 'skipped') {
473
+ process.stderr.write(`${label}: skipped (${event.reason})\n`);
474
+ } else if (event.phase === 'failed') {
475
+ process.stderr.write(`${label}: failed\n`);
476
+ }
477
+ },
478
+ }), 'Asset preview');
479
+ console.log(JSON.stringify(preview, null, 2));
480
+ return;
481
+ }
482
+ if (command === 'probe' && commandArgs.length === 1) {
483
+ const media = await runWithInterrupt((signal) => probeMediaAsync(commandArgs[0], { requireVideo: false, signal }), 'Media probe');
484
+ console.log(JSON.stringify(media, null, 2));
485
+ return;
486
+ }
487
+ if (command === 'media' && commandArgs[0] === 'support' && commandArgs.length === 2) {
488
+ const report = await runWithInterrupt((signal) => inspectMediaCompatibility(commandArgs[1], { signal }), 'Media support inspection');
489
+ console.log(JSON.stringify(report, null, 2));
490
+ if (!report.compatible) process.exitCode = 1;
491
+ return;
492
+ }
493
+ if (command === 'apply-edl' && commandArgs.length === 3) {
494
+ const [videoPath, edlPath, outputPath] = commandArgs;
495
+ const result = renderCuts(videoPath, readJson(resolve(edlPath)), outputPath);
496
+ console.log(JSON.stringify(result, null, 2));
497
+ return;
498
+ }
499
+ if (command === 'plan-silence' && commandArgs.length === 2) {
500
+ const [transcriptPath, outputPath] = commandArgs;
501
+ writeNewFile(resolve(outputPath), planSilenceCuts(readJson(resolve(transcriptPath))));
502
+ console.log(`Wrote ${resolve(outputPath)}`);
503
+ return;
504
+ }
505
+ if (command === 'detect-silence' && commandArgs.length >= 2 && commandArgs.length <= 3) {
506
+ const [mediaPath, outputPath, optionsPath] = commandArgs;
507
+ writeNewFile(resolve(outputPath), detectAcousticSilence(resolve(mediaPath), optionsPath ? readJson(resolve(optionsPath)) : {}));
508
+ console.log(`Wrote ${resolve(outputPath)}`);
509
+ return;
510
+ }
511
+ if (command === 'analyze' && commandArgs.length >= 3 && commandArgs.length <= 4) {
512
+ const [mediaPath, providerPath, outputPath, optionsPath] = commandArgs;
513
+ const providerModule = await import(pathToFileURL(resolve(providerPath)).href);
514
+ const provider = providerModule.default ?? providerModule.mediaAnalysisProvider ?? providerModule.provider;
515
+ const result = await analyzeMediaWithProvider(resolve(mediaPath), provider, {
516
+ ...(optionsPath ? { providerOptions: readJson(resolve(optionsPath)) } : {}),
517
+ });
518
+ writeNewFile(resolve(outputPath), result);
519
+ console.log(`Wrote ${resolve(outputPath)}`);
520
+ return;
521
+ }
522
+ if (command === 'detect-scenes' && commandArgs.length >= 2 && commandArgs.length <= 3) {
523
+ const [mediaPath, outputPath, optionsPath] = commandArgs;
524
+ const result = await runWithInterrupt((signal) => detectSceneChangesAsync(resolve(mediaPath), {
525
+ ...(optionsPath ? readJson(resolve(optionsPath)) : {}),
526
+ signal,
527
+ }), 'Scene detection');
528
+ writeNewFile(resolve(outputPath), result);
529
+ console.log(`Wrote ${resolve(outputPath)} (review required)`);
530
+ return;
531
+ }
532
+ if (command === 'scene-review' && commandArgs.length >= 2 && commandArgs.length <= 3) {
533
+ const [mediaPath, outputDirectory, optionsPath] = commandArgs;
534
+ const result = await runWithInterrupt((signal) => createSceneReviewPackAsync(
535
+ resolve(mediaPath),
536
+ resolve(outputDirectory),
537
+ {
538
+ ...(optionsPath ? readJson(resolve(optionsPath)) : {}),
539
+ signal,
540
+ onProgress(event) {
541
+ if (event.phase === 'progress' && Number.isFinite(event.percent)) {
542
+ process.stderr.write(`Scene thumbnails: ${Math.floor(event.percent)}% (${event.completed}/${event.total})\n`);
543
+ } else if (event.phase === 'started') {
544
+ process.stderr.write(`${event.stage}: started\n`);
545
+ } else if (event.phase === 'completed') {
546
+ process.stderr.write(`${event.stage}: complete\n`);
547
+ } else if (event.phase === 'failed') {
548
+ process.stderr.write(`${event.stage}: failed\n`);
549
+ }
550
+ },
551
+ },
552
+ ), 'Scene review pack generation');
553
+ console.log(JSON.stringify(result, null, 2));
554
+ return;
555
+ }
556
+ if (command === 'analysis-to-edl' && commandArgs.length >= 2 && commandArgs.length <= 3) {
557
+ const [analysisPath, outputPath, optionsPath] = commandArgs;
558
+ const result = readJson(resolve(analysisPath));
559
+ writeNewFile(resolve(outputPath), mediaAnalysisToEdl(result.analysis ?? result, optionsPath ? readJson(resolve(optionsPath)) : {}));
560
+ console.log(`Wrote ${resolve(outputPath)} (review required)`);
561
+ return;
562
+ }
563
+ if (command === 'approve-edl' && commandArgs.length === 2) {
564
+ const [candidatePath, outputPath] = commandArgs;
565
+ writeNewFile(resolve(outputPath), approveEdl(readJson(resolve(candidatePath))));
566
+ console.log(`Wrote ${resolve(outputPath)} (approved)`);
567
+ return;
568
+ }
569
+ if (command === 'transcript' && commandArgs[0] === 'correct' && commandArgs.length === 4) {
570
+ const [, transcriptPath, correctionsPath, outputPath] = commandArgs;
571
+ writeNewFile(resolve(outputPath), applyTranscriptCorrections(readJson(resolve(transcriptPath)), readJson(resolve(correctionsPath))));
572
+ console.log(`Wrote ${resolve(outputPath)}`);
573
+ return;
574
+ }
575
+ if (command === 'transcript' && commandArgs[0] === 'export' && commandArgs.length === 4) {
576
+ const [, transcriptPath, format, outputPath] = commandArgs;
577
+ writeNewTextFile(resolve(outputPath), formatSubtitleText(readJson(resolve(transcriptPath)), format));
578
+ console.log(`Wrote ${resolve(outputPath)}`);
579
+ return;
580
+ }
581
+ if (command === 'mistake-candidates' && commandArgs.length === 1) {
582
+ console.log(JSON.stringify(findMistakeCandidates(readJson(resolve(commandArgs[0]))), null, 2));
583
+ return;
584
+ }
585
+ if (command === 'lint' && commandArgs.length === 1) {
586
+ lintProject(commandArgs[0]);
587
+ return;
588
+ }
589
+ if (command === 'preview' && commandArgs.length === 1) {
590
+ previewProject(commandArgs[0]);
591
+ return;
592
+ }
593
+ if (command === 'render' && commandArgs.length >= 1 && commandArgs.length <= 4) {
594
+ const [projectPath, outputPath, quality = 'draft', optionsPath] = commandArgs;
595
+ const options = optionsPath ? readJson(resolve(optionsPath)) : {};
596
+ console.log(JSON.stringify(await runWithInterrupt((signal) => renderProjectAsync(projectPath, outputPath, quality, { ...options, signal })), null, 2));
597
+ return;
598
+ }
599
+ if (command === 'transcribe' && commandArgs.length >= 2 && commandArgs.length <= 4) {
600
+ const [projectPath, mediaPath, model, language] = commandArgs;
601
+ console.log(JSON.stringify(transcribeProject(projectPath, mediaPath, { model, language }), null, 2));
602
+ return;
603
+ }
604
+ if (command === 'import-transcript' && commandArgs.length === 3) {
605
+ const [projectPath, mediaPath, transcriptPath] = commandArgs;
606
+ console.log(JSON.stringify(importProjectTranscript(projectPath, mediaPath, transcriptPath), null, 2));
607
+ return;
608
+ }
609
+ throw new Error(`Usage: aivee
610
+ project create <slug> [WIDTHxHEIGHT] [fps] [durationSeconds] [--fit <contain|cover|fill>] [--background-color <hex|transparent>]
611
+ project create <slug> --preset <name> [fps] [durationSeconds] [--fit <contain|cover|fill>] [--background-color <hex|transparent>]
612
+ presets list | probe <media> | media support <media>
613
+ asset register <project> <media> [assetId] | asset register-many <project> <media>...
614
+ asset unregister <project> <asset-id>
615
+ asset verify <project>
616
+ asset relink <project> <assetId> <replacement-media>
617
+ asset proxy create <project> <assetId> [maxDimension] [crf] [frameRate]
618
+ edit <project> <assetId> <transcript.json> <approved-edl.json> [output.mp4] [draft|standard|high]
619
+ timeline create <project> [output.json|durationSeconds] [durationSeconds] | timeline validate <timeline.json> | timeline preview-edit <timeline.json> <operation.json> | timeline plan-broll <timeline.json> <scene-review.json> <asset-id> <video-track-id> <placements.json> [new-plan.json] | timeline apply-plan <timeline.json> <review-plan.json> | timeline edit <timeline.json> <operation.json> <new-timeline.json>
620
+ timeline project-edit <timeline.json> <operation.json> [history-limit] | timeline undo|redo <timeline.json> | timeline history <timeline.json> [show|reset [limit]]
621
+ timeline export-otio <project> <timeline.json> <new-output.otio>
622
+ timeline import-otio <project> <input.otio> <new-timeline.json>
623
+ timeline captions <transcript.json> <new-track.json> [options.json]
624
+ timeline from-edl <approved-edl.json> <assetId> <new-timeline.json> [options.json]
625
+ timeline render <project> <timeline.json> [output.mp4] [draft|standard|high]
626
+ timeline render-range <project> <timeline.json> <start-seconds> <end-seconds> <output.mp4> [draft|standard|high] [options.json]
627
+ transcribe <project> <media> [model] [language] | import-transcript <project> <media> <transcript>
628
+ plan-silence <transcript.json> <new-edl.json> | detect-silence <media> <new-edl.json> [options.json]
629
+ analyze <media> <provider-module.mjs> <new-analysis.json> [provider-options.json]
630
+ detect-scenes <media> <new-analysis.json> [options.json]
631
+ scene-review <media> <new-output-directory> [options.json]
632
+ analysis-to-edl <analysis.json> <review-required-edl.json> [options.json]
633
+ approve-edl <reviewed-edl.json> <new-approved-edl.json>
634
+ mistake-candidates <transcript.json> | transcript correct <transcript.json> <corrections.json> <new-transcript.json>
635
+ transcript export <transcript.json> <srt|vtt> <new-subtitles-file>
636
+ apply-edl <media> <approved-edl.json> <output.mp4> | retime <transcript.json> <approved-edl.json> <new-output.json>
637
+ asset preview <project> <asset-id> [options.json] |
638
+ lint <project> | preview <project> | render <project> [output.mp4|output.webm|output.mov|output.gif] [draft|standard|high]`);
639
+ }
640
+
641
+ try {
642
+ await main(process.argv.slice(2));
643
+ } catch (error) {
644
+ console.error(error.message);
645
+ process.exitCode = 1;
646
+ }