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/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "aivee",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-dev.59872e1",
4
+ "license": "MIT",
5
+ "author": "Waren Gonzaga",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/warengonzaga/ai-video-editing-engine.git"
9
+ },
10
+ "homepage": "https://github.com/warengonzaga/ai-video-editing-engine#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/warengonzaga/ai-video-editing-engine/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "aivee": "src/cli.mjs"
17
+ },
18
+ "exports": {
19
+ ".": "./src/index.mjs"
20
+ },
21
+ "files": [
22
+ "src/",
23
+ "README.md",
24
+ "LICENSE",
25
+ "docs/architecture.md",
26
+ "docs/usage.md",
27
+ "docs/releasing.md",
28
+ "docs/aspect-ratio-presets.md",
29
+ "CHANGELOG.md",
30
+ "requirements-otio.txt"
31
+ ],
32
+ "description": "Local-first video editing CLI and JavaScript API for AI workflows.",
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "dependencies": {
37
+ "gsap": "3.14.2",
38
+ "hyperframes": "0.9.17"
39
+ },
40
+ "overrides": {
41
+ "hyperframes": {
42
+ "undici": "7.30.2"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "test": "node --test",
47
+ "test:render-smoke": "node scripts/render-smoke.mjs",
48
+ "doctor": "node src/doctor.mjs",
49
+ "new-video": "node src/cli.mjs project create",
50
+ "retime": "node src/cli.mjs retime"
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ opentimelineio==0.18.1
@@ -0,0 +1,307 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { listFfmpegDecoders, listFfmpegDemuxers, listFfmpegEncoders, listFfmpegFilters } from './media/ffmpeg-capabilities.mjs';
4
+ import { CANVAS_PRESETS } from './project/presets.mjs';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const VIDEO_ENCODERS = Object.freeze({
8
+ h264: 'libx264',
9
+ h265: 'libx265',
10
+ vp9: 'libvpx-vp9',
11
+ prores: 'prores_ks',
12
+ });
13
+
14
+ export function getEngineCapabilities(options = {}) {
15
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Capability options must be an object.');
16
+ for (const key of Object.keys(options)) if (!['ffmpeg', 'ffprobe', 'otioPython'].includes(key)) throw new TypeError(`Unsupported capability option: ${key}.`);
17
+ const ffmpegCommand = options.ffmpeg ?? 'ffmpeg';
18
+ const ffprobeCommand = options.ffprobe ?? 'ffprobe';
19
+ const otioPythonCommand = options.otioPython ?? process.env.AIVE_OTIO_PYTHON ?? process.env.PYTHON ?? 'python';
20
+ if (typeof ffmpegCommand !== 'string' || !ffmpegCommand.trim()) throw new TypeError('ffmpeg must be a non-empty command path.');
21
+ if (typeof ffprobeCommand !== 'string' || !ffprobeCommand.trim()) throw new TypeError('ffprobe must be a non-empty command path.');
22
+ if (typeof otioPythonCommand !== 'string' || !otioPythonCommand.trim()) throw new TypeError('otioPython must be a non-empty command path.');
23
+
24
+ const nodeVersion = process.versions.node;
25
+ const nodeMajor = Number(nodeVersion.split('.')[0]);
26
+ const ffmpeg = inspectCommand(ffmpegCommand);
27
+ const ffprobe = inspectCommand(ffprobeCommand);
28
+ const openTimelineIO = inspectPythonModule(otioPythonCommand, 'opentimelineio');
29
+ let encoders = new Set();
30
+ let decoders = new Set();
31
+ let demuxers = new Set();
32
+ let filters = new Set();
33
+ let ffmpegInspectionError;
34
+ let inputInspectionError;
35
+ if (ffmpeg.available) {
36
+ try {
37
+ encoders = listFfmpegEncoders(ffmpegCommand);
38
+ filters = listFfmpegFilters(ffmpegCommand);
39
+ } catch (error) {
40
+ ffmpegInspectionError = error.message;
41
+ }
42
+ try {
43
+ decoders = listFfmpegDecoders(ffmpegCommand);
44
+ demuxers = listFfmpegDemuxers(ffmpegCommand);
45
+ } catch (error) {
46
+ inputInspectionError = error.message;
47
+ }
48
+ }
49
+
50
+ const hyperframes = packageAvailable('hyperframes');
51
+ const gsap = packageAvailable('gsap');
52
+ const renderRuntimeAvailable = nodeMajor >= 22 && ffmpeg.available && ffprobe.available && hyperframes && gsap;
53
+ const supportsFilter = (name) => filters.has(name);
54
+ const supportsEncoder = (name) => encoders.has(name);
55
+ const colorProfiles = {
56
+ rec709: { available: supportsFilter('colorspace'), formats: ['mp4', 'mov', 'webm'] },
57
+ 'rec2020-sdr': { available: supportsFilter('colorspace'), formats: ['mp4', 'mov', 'webm'] },
58
+ hdr10: { available: supportsFilter('zscale') && supportsEncoder('libx265'), formats: ['mp4', 'mov'] },
59
+ };
60
+ const codecsByFormat = {
61
+ mp4: { h264: supportsEncoder('libx264'), h265: supportsEncoder('libx265') },
62
+ mov: { h264: supportsEncoder('libx264'), h265: supportsEncoder('libx265'), prores: supportsEncoder('prores_ks') },
63
+ webm: { vp9: supportsEncoder('libvpx-vp9') },
64
+ };
65
+ const outputColorProfiles = (format) => Object.fromEntries(Object.entries(colorProfiles)
66
+ .filter(([, profile]) => profile.formats.includes(format))
67
+ .map(([id, profile]) => {
68
+ const compatibleEncoderAvailable = id === 'hdr10'
69
+ ? codecsByFormat[format]?.h265 === true
70
+ : Object.values(codecsByFormat[format] ?? {}).some(Boolean);
71
+ return [id, profile.available && compatibleEncoderAvailable];
72
+ }));
73
+ const inputContainers = {
74
+ mp4: ['mp4'],
75
+ mov: ['mov'],
76
+ avi: ['avi'],
77
+ matroska: ['matroska'],
78
+ webm: ['webm'],
79
+ mpegts: ['mpegts'],
80
+ mpeg: ['mpeg'],
81
+ mp3: ['mp3'],
82
+ ogg: ['ogg'],
83
+ wav: ['wav'],
84
+ image2: ['image2'],
85
+ };
86
+ return {
87
+ schemaVersion: 1,
88
+ node: { version: nodeVersion, supported: nodeMajor >= 22 },
89
+ tools: {
90
+ ffmpeg: { ...ffmpeg, ...(ffmpegInspectionError ? { inspectionError: ffmpegInspectionError } : {}) },
91
+ ffprobe,
92
+ openTimelineIO: {
93
+ available: openTimelineIO.available,
94
+ optional: true,
95
+ ...(openTimelineIO.version ? { version: openTimelineIO.version } : {}),
96
+ ...(openTimelineIO.reason ? { reason: openTimelineIO.reason } : {}),
97
+ },
98
+ hyperframes: { available: hyperframes },
99
+ gsap: { available: gsap },
100
+ browserRuntime: { available: null, checked: false, checkCommand: 'npm run doctor' },
101
+ },
102
+ media: {
103
+ assetIngest: {
104
+ available: ffmpeg.available && ffprobe.available,
105
+ batchRegistration: true,
106
+ batchSemantics: 'sequential-best-effort',
107
+ progressCallbacks: {
108
+ supported: true,
109
+ asyncApiOnly: true,
110
+ stages: ['asset-batch', 'asset-batch-item', 'asset-copy', 'asset-probe', 'asset-publish', 'asset-relink-copy', 'asset-relink-probe', 'asset-relink-publish'],
111
+ eventFields: ['stage', 'phase'],
112
+ optionalEventFields: ['percent', 'completed', 'total', 'assetId', 'batchIndex', 'batchTotal', 'registered', 'failed', 'message'],
113
+ },
114
+ },
115
+ assetVerification: {
116
+ available: true,
117
+ progressCallbacks: {
118
+ supported: true,
119
+ asyncApiOnly: true,
120
+ stages: ['asset-verification'],
121
+ eventFields: ['stage', 'phase'],
122
+ optionalEventFields: ['percent', 'completed', 'total', 'assetId', 'variant'],
123
+ },
124
+ },
125
+ input: {
126
+ available: ffmpeg.available && ffprobe.available,
127
+ inventoryChecked: ffmpeg.available && ffprobe.available && !inputInspectionError,
128
+ ...(inputInspectionError ? { inspectionError: inputInspectionError } : {}),
129
+ containers: Object.fromEntries(Object.entries(inputContainers)
130
+ .map(([name, aliases]) => [name, aliases.some((alias) => demuxers.has(alias))])),
131
+ videoDecoders: Object.fromEntries(['h264', 'hevc', 'av1', 'vp8', 'vp9', 'prores', 'mpeg2video', 'mpeg4']
132
+ .map((name) => [name, decoders.has(name)])),
133
+ audioDecoders: Object.fromEntries(['aac', 'mp3', 'opus', 'vorbis', 'flac', 'pcm_s16le']
134
+ .map((name) => [name, decoders.has(name)])),
135
+ },
136
+ proxyGeneration: {
137
+ available: ffmpeg.available && ffprobe.available,
138
+ profileVersion: 2,
139
+ sourceUpscaling: false,
140
+ keyframeIntervalSeconds: 1,
141
+ forcedKeyframes: true,
142
+ progressCallbacks: {
143
+ supported: true,
144
+ asyncApiOnly: true,
145
+ stages: ['proxy-preflight', 'proxy-generation', 'proxy-validation', 'proxy-publish'],
146
+ eventFields: ['stage', 'phase'],
147
+ optionalEventFields: ['percent', 'completed', 'total', 'assetId', 'reason'],
148
+ },
149
+ },
150
+ sceneDetection: {
151
+ available: ffmpeg.available && ffprobe.available && filters.has('select') && filters.has('metadata'),
152
+ analyzer: 'ffmpeg-scene-score',
153
+ defaultThreshold: 0.35,
154
+ thresholdRange: [0.05, 1],
155
+ defaultMinimumSpacingSeconds: 0.5,
156
+ reviewRequired: true,
157
+ outputTypes: ['scene-cut'],
158
+ cancellation: true,
159
+ },
160
+ sceneReviewPack: {
161
+ available: ffmpeg.available && ffprobe.available
162
+ && filters.has('select') && filters.has('metadata') && filters.has('scale') && filters.has('pad') && filters.has('tile'),
163
+ thumbnailDefaultSize: 320,
164
+ thumbnailSizeRange: [80, 1280],
165
+ defaultContactSheetLimit: 60,
166
+ contactSheetLimitRange: [1, 120],
167
+ reviewRequired: true,
168
+ cancellation: true,
169
+ progressCallbacks: {
170
+ supported: true,
171
+ asyncApiOnly: true,
172
+ stages: ['scene-source-fingerprint', 'scene-analysis', 'scene-thumbnails', 'scene-contact-sheet', 'scene-review-publish'],
173
+ eventFields: ['stage', 'phase'],
174
+ optionalEventFields: ['completed', 'total', 'percent', 'message'],
175
+ },
176
+ },
177
+ sceneReviewPlacementPlanning: {
178
+ available: true,
179
+ requires: ['validated-timeline', 'registered-video-asset-matching-scene-review-sha256', 'scene-review-pack', 'explicit-shot-selections'],
180
+ output: 'review-required-timeline-operations',
181
+ appliesChanges: false,
182
+ maximumPlacements: 500,
183
+ },
184
+ reviewPlanApplication: {
185
+ available: true,
186
+ explicitCommandRequired: true,
187
+ staleTimelineRejected: true,
188
+ undoable: true,
189
+ maximumOperations: 2000,
190
+ },
191
+ },
192
+ interchange: {
193
+ openTimelineIO: {
194
+ available: openTimelineIO.available,
195
+ formats: ['.otio'],
196
+ import: true,
197
+ export: true,
198
+ optionalDependency: 'Python OpenTimelineIO',
199
+ installCommand: 'python -m pip install -r requirements-otio.txt',
200
+ ...(openTimelineIO.version ? { pythonPackageVersion: openTimelineIO.version } : {}),
201
+ ...(openTimelineIO.reason ? { reason: openTimelineIO.reason } : {}),
202
+ },
203
+ },
204
+ render: {
205
+ requirementsAvailable: renderRuntimeAvailable,
206
+ progressCallbacks: {
207
+ supported: true,
208
+ asyncApiOnly: true,
209
+ stages: ['preprocessing', 'proxy-resolution', 'audio-alignment', 'playback-processing', 'audio-effects', 'sidechain-processing', 'capture', 'encoding', 'validation', 'publish', 'timeline-validation', 'mastering', 'mastering-analysis', 'mastering-encode', 'mastering-validation', 'mastering-publish'],
210
+ eventFields: ['stage', 'phase'],
211
+ optionalEventFields: ['percent', 'completed', 'total', 'target', 'codec', 'reason'],
212
+ },
213
+ videoEncoders: Object.fromEntries(Object.entries(VIDEO_ENCODERS).map(([id, name]) => [id, encoders.has(name)])),
214
+ outputs: {
215
+ mp4: {
216
+ available: renderRuntimeAvailable && Object.values(codecsByFormat.mp4).some(Boolean),
217
+ codecs: codecsByFormat.mp4,
218
+ defaultCodec: 'h264',
219
+ defaultCodecAvailable: codecsByFormat.mp4.h264,
220
+ colorProfiles: outputColorProfiles('mp4'),
221
+ },
222
+ mov: {
223
+ available: renderRuntimeAvailable && Object.values(codecsByFormat.mov).some(Boolean),
224
+ codecs: codecsByFormat.mov,
225
+ defaultCodec: 'h264',
226
+ defaultCodecAvailable: codecsByFormat.mov.h264,
227
+ colorProfiles: outputColorProfiles('mov'),
228
+ },
229
+ webm: {
230
+ available: renderRuntimeAvailable && Object.values(codecsByFormat.webm).some(Boolean),
231
+ codecs: codecsByFormat.webm,
232
+ defaultCodec: 'vp9',
233
+ defaultCodecAvailable: codecsByFormat.webm.vp9,
234
+ colorProfiles: outputColorProfiles('webm'),
235
+ },
236
+ gif: { available: renderRuntimeAvailable && supportsEncoder('gif') },
237
+ 'png-sequence': { available: hyperframes },
238
+ },
239
+ colorProfiles,
240
+ canvas: {
241
+ backgroundColor: {
242
+ available: true,
243
+ default: '#10141c',
244
+ transparent: true,
245
+ acceptedHexDigits: [3, 4, 6, 8],
246
+ },
247
+ },
248
+ canvasPresets: CANVAS_PRESETS.map(({ id, aspectRatio, width, height, platforms }) => ({
249
+ id, aspectRatio, width, height, platforms: [...platforms],
250
+ })),
251
+ filters: {
252
+ rubberband: filters.has('rubberband'),
253
+ afftdn: filters.has('afftdn'),
254
+ stereotools: filters.has('stereotools'),
255
+ equalizer: filters.has('equalizer'),
256
+ highpass: filters.has('highpass'),
257
+ lowpass: filters.has('lowpass'),
258
+ acompressor: filters.has('acompressor'),
259
+ sidechaincompress: filters.has('sidechaincompress'),
260
+ loudnorm: filters.has('loudnorm'),
261
+ alimiter: filters.has('alimiter'),
262
+ aresample: filters.has('aresample'),
263
+ reverse: filters.has('reverse'),
264
+ areverse: filters.has('areverse'),
265
+ colorspace: filters.has('colorspace'),
266
+ zscale: filters.has('zscale'),
267
+ },
268
+ },
269
+ requiredToolsReady: nodeMajor >= 22 && ffmpeg.available && ffprobe.available && hyperframes && gsap,
270
+ };
271
+ }
272
+
273
+ function inspectCommand(command, args = ['-version']) {
274
+ const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 1024 * 1024 });
275
+ if (result.error || result.status !== 0) return { available: false };
276
+ return { available: true, version: result.stdout.trim().split(/\r?\n/, 1)[0] };
277
+ }
278
+
279
+ function inspectPythonModule(command, moduleName) {
280
+ const result = spawnSync(command, ['-c', `import ${moduleName} as module; assert 'otio_json' in module.adapters.available_adapter_names(), 'otio_json adapter unavailable'; print(getattr(module, '__version__', 'installed'))`], {
281
+ encoding: 'utf8',
282
+ windowsHide: true,
283
+ maxBuffer: 1024 * 1024,
284
+ });
285
+ if (result.error) return { available: false, reason: 'python-not-found' };
286
+ if (result.status !== 0) {
287
+ return {
288
+ available: false,
289
+ reason: /No module named ['"]?opentimelineio/.test(`${result.stderr}\n${result.stdout}`)
290
+ ? 'package-not-installed'
291
+ : /otio_json adapter unavailable/.test(`${result.stderr}\n${result.stdout}`)
292
+ ? 'json-adapter-unavailable'
293
+ : 'package-import-failed',
294
+ };
295
+ }
296
+ const version = result.stdout.trim().split(/\r?\n/, 1)[0];
297
+ return { available: true, ...(version ? { version } : {}) };
298
+ }
299
+
300
+ function packageAvailable(name) {
301
+ try {
302
+ require.resolve(`${name}/package.json`);
303
+ return true;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }