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
@@ -1,192 +1,192 @@
1
- import { createHash } from 'node:crypto';
2
- import { constants as fsConstants, createReadStream } from 'node:fs';
3
- import { copyFile, lstat, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
4
- import { basename, dirname, join, relative, resolve, sep } from 'node:path';
5
-
6
- const CHECKPOINT_VERSION = 1;
7
- const EXCLUDED_DIRECTORIES = new Set(['.git', '.aive-checkpoints', 'node_modules', 'renders']);
8
-
9
- export async function fingerprintRenderProject(projectPath, profile) {
10
- const root = resolve(projectPath);
11
- const hash = createHash('sha256');
12
- hash.update(JSON.stringify(profile));
13
- const files = await collectFiles(root);
14
- for (const file of files) {
15
- const relativePath = relative(root, file).split(sep).join('/');
16
- hash.update(`\0${relativePath}\0`);
17
- for await (const chunk of createReadStream(file)) hash.update(chunk);
18
- }
19
- return hash.digest('hex');
20
- }
21
-
22
- export async function prepareRenderCheckpoint(outputPath, fingerprint, options = {}) {
23
- const maxBytes = options.maxBytes ?? 8 * 1024 ** 3;
24
- const maxAgeHours = options.maxAgeHours ?? 72;
25
- validateCheckpointLimits(maxBytes, maxAgeHours);
26
- const root = join(dirname(resolve(outputPath)), '.aive-checkpoints');
27
- await pruneExpiredRenderCheckpoints(root, maxAgeHours);
28
- const id = createHash('sha256').update(`${basename(outputPath)}\0${fingerprint}`).digest('hex');
29
- const directory = join(root, id);
30
- await assertSafeCheckpointDirectory(root, directory);
31
- const artifact = join(directory, `capture.${extension(outputPath)}`);
32
- const manifest = join(directory, 'checkpoint.json');
33
- return { root, id, directory, artifact, manifest, fingerprint, maxBytes, maxAgeHours };
34
- }
35
-
36
- export async function restoreRenderCheckpoint(checkpoint) {
37
- await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
38
- let record;
39
- try {
40
- const parsed = JSON.parse(await readFile(checkpoint.manifest, 'utf8'));
41
- const info = await lstat(checkpoint.artifact);
42
- if (info.isSymbolicLink() || !info.isFile() || info.size <= 0 || info.size > checkpoint.maxBytes || parsed.sizeBytes !== info.size
43
- || parsed.version !== CHECKPOINT_VERSION || parsed.fingerprint !== checkpoint.fingerprint
44
- || parsed.artifact !== basename(checkpoint.artifact) || !Number.isFinite(parsed.createdAt)
45
- || Date.now() - parsed.createdAt > checkpoint.maxAgeHours * 60 * 60 * 1000) {
46
- await rm(checkpoint.directory, { recursive: true, force: true });
47
- return undefined;
48
- }
49
- record = parsed;
50
- if (await hashFile(checkpoint.artifact) !== record.sha256) {
51
- await rm(checkpoint.directory, { recursive: true, force: true });
52
- return undefined;
53
- }
54
- return checkpoint.artifact;
55
- } catch (error) {
56
- if (error.code === 'ENOENT' || error instanceof SyntaxError) await rm(checkpoint.directory, { recursive: true, force: true });
57
- return undefined;
58
- }
59
- }
60
-
61
- export async function saveRenderCheckpoint(checkpoint, completedArtifact) {
62
- await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
63
- const info = await stat(completedArtifact);
64
- if (!info.isFile() || info.size <= 0) throw new Error('Cannot checkpoint an empty render artifact.');
65
- if (info.size > checkpoint.maxBytes) {
66
- await rm(checkpoint.directory, { recursive: true, force: true });
67
- return undefined;
68
- }
69
- await mkdir(checkpoint.directory, { recursive: true, mode: 0o700 });
70
- const temporaryArtifact = `${checkpoint.artifact}.tmp`;
71
- const temporaryManifest = `${checkpoint.manifest}.tmp`;
72
- try {
73
- await rm(temporaryArtifact, { force: true });
74
- await rm(temporaryManifest, { force: true });
75
- await copyFile(completedArtifact, temporaryArtifact);
76
- const record = {
77
- version: CHECKPOINT_VERSION,
78
- fingerprint: checkpoint.fingerprint,
79
- artifact: basename(checkpoint.artifact),
80
- sizeBytes: info.size,
81
- sha256: await hashFile(temporaryArtifact),
82
- createdAt: Date.now(),
83
- };
84
- await writeFile(temporaryManifest, `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
85
- await copyFile(temporaryArtifact, checkpoint.artifact, fsConstants.COPYFILE_EXCL);
86
- await rm(temporaryArtifact, { force: true });
87
- await copyFile(temporaryManifest, checkpoint.manifest, fsConstants.COPYFILE_EXCL);
88
- await rm(temporaryManifest, { force: true });
89
- return checkpoint.artifact;
90
- } catch (error) {
91
- await rm(temporaryArtifact, { force: true });
92
- await rm(temporaryManifest, { force: true });
93
- throw error;
94
- }
95
- }
96
-
97
- export async function removeRenderCheckpoint(checkpoint) {
98
- await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
99
- await rm(checkpoint.directory, { recursive: true, force: true });
100
- }
101
-
102
- export async function pruneExpiredRenderCheckpoints(rootPath, maxAgeHours = 72) {
103
- const root = resolve(rootPath);
104
- const rootInfo = await lstatIfPresent(root);
105
- if (rootInfo && (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())) {
106
- throw new TypeError('Render checkpoint root must be a real directory, not a symbolic link.');
107
- }
108
- let entries;
109
- try {
110
- entries = await readdir(root, { withFileTypes: true });
111
- } catch (error) {
112
- if (error.code === 'ENOENT') return;
113
- throw error;
114
- }
115
- const expireBefore = Date.now() - maxAgeHours * 60 * 60 * 1000;
116
- for (const entry of entries) {
117
- if (!entry.isDirectory()) continue;
118
- const directory = join(root, entry.name);
119
- try {
120
- const record = JSON.parse(await readFile(join(directory, 'checkpoint.json'), 'utf8'));
121
- if (!Number.isFinite(record.createdAt) || record.createdAt < expireBefore) {
122
- await rm(directory, { recursive: true, force: true });
123
- }
124
- } catch {
125
- await rm(directory, { recursive: true, force: true });
126
- }
127
- }
128
- }
129
-
130
- async function assertSafeCheckpointDirectory(rootPath, directoryPath) {
131
- const root = resolve(rootPath);
132
- const directory = resolve(directoryPath);
133
- if (dirname(directory) !== root) throw new TypeError('Render checkpoint directory must be directly inside its checkpoint root.');
134
- const rootInfo = await lstatIfPresent(root);
135
- if (rootInfo && (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())) {
136
- throw new TypeError('Render checkpoint root must be a real directory, not a symbolic link.');
137
- }
138
- const directoryInfo = await lstatIfPresent(directory);
139
- if (directoryInfo && (directoryInfo.isSymbolicLink() || !directoryInfo.isDirectory())) {
140
- throw new TypeError('Render checkpoint directory must be a real directory, not a symbolic link.');
141
- }
142
- }
143
-
144
- async function lstatIfPresent(path) {
145
- try {
146
- return await lstat(path);
147
- } catch (error) {
148
- if (error.code === 'ENOENT') return undefined;
149
- throw error;
150
- }
151
- }
152
-
153
- export function validateCheckpointLimits(maxBytes, maxAgeHours) {
154
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1024 * 1024 || maxBytes > 100 * 1024 ** 3) {
155
- throw new TypeError('checkpointMaxBytes must be an integer from 1048576 to 107374182400.');
156
- }
157
- if (!Number.isSafeInteger(maxAgeHours) || maxAgeHours < 1 || maxAgeHours > 720) {
158
- throw new TypeError('checkpointMaxAgeHours must be an integer from 1 to 720.');
159
- }
160
- }
161
-
162
- async function collectFiles(root) {
163
- const files = [];
164
- const visit = async (directory) => {
165
- const entries = await readdir(directory, { withFileTypes: true });
166
- entries.sort((left, right) => left.name.localeCompare(right.name));
167
- for (const entry of entries) {
168
- const path = join(directory, entry.name);
169
- if (entry.isSymbolicLink()) {
170
- const target = await stat(path);
171
- if (target.isFile()) files.push(path);
172
- else if (target.isDirectory()) throw new Error(`Cannot fingerprint a project containing a linked directory: ${relative(root, path)}`);
173
- }
174
- if (entry.isDirectory()) {
175
- if (!EXCLUDED_DIRECTORIES.has(entry.name)) await visit(path);
176
- } else if (entry.isFile()) files.push(path);
177
- }
178
- };
179
- await visit(root);
180
- return files;
181
- }
182
-
183
- async function hashFile(path) {
184
- const hash = createHash('sha256');
185
- for await (const chunk of createReadStream(path)) hash.update(chunk);
186
- return hash.digest('hex');
187
- }
188
-
189
- function extension(path) {
190
- const value = basename(path).split('.').at(-1).toLowerCase();
191
- return /^[a-z0-9]{1,8}$/.test(value) ? value : 'bin';
192
- }
1
+ import { createHash } from 'node:crypto';
2
+ import { constants as fsConstants, createReadStream } from 'node:fs';
3
+ import { copyFile, lstat, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
5
+
6
+ const CHECKPOINT_VERSION = 1;
7
+ const EXCLUDED_DIRECTORIES = new Set(['.git', '.aive-checkpoints', 'node_modules', 'renders']);
8
+
9
+ export async function fingerprintRenderProject(projectPath, profile) {
10
+ const root = resolve(projectPath);
11
+ const hash = createHash('sha256');
12
+ hash.update(JSON.stringify(profile));
13
+ const files = await collectFiles(root);
14
+ for (const file of files) {
15
+ const relativePath = relative(root, file).split(sep).join('/');
16
+ hash.update(`\0${relativePath}\0`);
17
+ for await (const chunk of createReadStream(file)) hash.update(chunk);
18
+ }
19
+ return hash.digest('hex');
20
+ }
21
+
22
+ export async function prepareRenderCheckpoint(outputPath, fingerprint, options = {}) {
23
+ const maxBytes = options.maxBytes ?? 8 * 1024 ** 3;
24
+ const maxAgeHours = options.maxAgeHours ?? 72;
25
+ validateCheckpointLimits(maxBytes, maxAgeHours);
26
+ const root = join(dirname(resolve(outputPath)), '.aive-checkpoints');
27
+ await pruneExpiredRenderCheckpoints(root, maxAgeHours);
28
+ const id = createHash('sha256').update(`${basename(outputPath)}\0${fingerprint}`).digest('hex');
29
+ const directory = join(root, id);
30
+ await assertSafeCheckpointDirectory(root, directory);
31
+ const artifact = join(directory, `capture.${extension(outputPath)}`);
32
+ const manifest = join(directory, 'checkpoint.json');
33
+ return { root, id, directory, artifact, manifest, fingerprint, maxBytes, maxAgeHours };
34
+ }
35
+
36
+ export async function restoreRenderCheckpoint(checkpoint) {
37
+ await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
38
+ let record;
39
+ try {
40
+ const parsed = JSON.parse(await readFile(checkpoint.manifest, 'utf8'));
41
+ const info = await lstat(checkpoint.artifact);
42
+ if (info.isSymbolicLink() || !info.isFile() || info.size <= 0 || info.size > checkpoint.maxBytes || parsed.sizeBytes !== info.size
43
+ || parsed.version !== CHECKPOINT_VERSION || parsed.fingerprint !== checkpoint.fingerprint
44
+ || parsed.artifact !== basename(checkpoint.artifact) || !Number.isFinite(parsed.createdAt)
45
+ || Date.now() - parsed.createdAt > checkpoint.maxAgeHours * 60 * 60 * 1000) {
46
+ await rm(checkpoint.directory, { recursive: true, force: true });
47
+ return undefined;
48
+ }
49
+ record = parsed;
50
+ if (await hashFile(checkpoint.artifact) !== record.sha256) {
51
+ await rm(checkpoint.directory, { recursive: true, force: true });
52
+ return undefined;
53
+ }
54
+ return checkpoint.artifact;
55
+ } catch (error) {
56
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) await rm(checkpoint.directory, { recursive: true, force: true });
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ export async function saveRenderCheckpoint(checkpoint, completedArtifact) {
62
+ await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
63
+ const info = await stat(completedArtifact);
64
+ if (!info.isFile() || info.size <= 0) throw new Error('Cannot checkpoint an empty render artifact.');
65
+ if (info.size > checkpoint.maxBytes) {
66
+ await rm(checkpoint.directory, { recursive: true, force: true });
67
+ return undefined;
68
+ }
69
+ await mkdir(checkpoint.directory, { recursive: true, mode: 0o700 });
70
+ const temporaryArtifact = `${checkpoint.artifact}.tmp`;
71
+ const temporaryManifest = `${checkpoint.manifest}.tmp`;
72
+ try {
73
+ await rm(temporaryArtifact, { force: true });
74
+ await rm(temporaryManifest, { force: true });
75
+ await copyFile(completedArtifact, temporaryArtifact);
76
+ const record = {
77
+ version: CHECKPOINT_VERSION,
78
+ fingerprint: checkpoint.fingerprint,
79
+ artifact: basename(checkpoint.artifact),
80
+ sizeBytes: info.size,
81
+ sha256: await hashFile(temporaryArtifact),
82
+ createdAt: Date.now(),
83
+ };
84
+ await writeFile(temporaryManifest, `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
85
+ await copyFile(temporaryArtifact, checkpoint.artifact, fsConstants.COPYFILE_EXCL);
86
+ await rm(temporaryArtifact, { force: true });
87
+ await copyFile(temporaryManifest, checkpoint.manifest, fsConstants.COPYFILE_EXCL);
88
+ await rm(temporaryManifest, { force: true });
89
+ return checkpoint.artifact;
90
+ } catch (error) {
91
+ await rm(temporaryArtifact, { force: true });
92
+ await rm(temporaryManifest, { force: true });
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ export async function removeRenderCheckpoint(checkpoint) {
98
+ await assertSafeCheckpointDirectory(checkpoint.root, checkpoint.directory);
99
+ await rm(checkpoint.directory, { recursive: true, force: true });
100
+ }
101
+
102
+ export async function pruneExpiredRenderCheckpoints(rootPath, maxAgeHours = 72) {
103
+ const root = resolve(rootPath);
104
+ const rootInfo = await lstatIfPresent(root);
105
+ if (rootInfo && (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())) {
106
+ throw new TypeError('Render checkpoint root must be a real directory, not a symbolic link.');
107
+ }
108
+ let entries;
109
+ try {
110
+ entries = await readdir(root, { withFileTypes: true });
111
+ } catch (error) {
112
+ if (error.code === 'ENOENT') return;
113
+ throw error;
114
+ }
115
+ const expireBefore = Date.now() - maxAgeHours * 60 * 60 * 1000;
116
+ for (const entry of entries) {
117
+ if (!entry.isDirectory()) continue;
118
+ const directory = join(root, entry.name);
119
+ try {
120
+ const record = JSON.parse(await readFile(join(directory, 'checkpoint.json'), 'utf8'));
121
+ if (!Number.isFinite(record.createdAt) || record.createdAt < expireBefore) {
122
+ await rm(directory, { recursive: true, force: true });
123
+ }
124
+ } catch {
125
+ await rm(directory, { recursive: true, force: true });
126
+ }
127
+ }
128
+ }
129
+
130
+ async function assertSafeCheckpointDirectory(rootPath, directoryPath) {
131
+ const root = resolve(rootPath);
132
+ const directory = resolve(directoryPath);
133
+ if (dirname(directory) !== root) throw new TypeError('Render checkpoint directory must be directly inside its checkpoint root.');
134
+ const rootInfo = await lstatIfPresent(root);
135
+ if (rootInfo && (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())) {
136
+ throw new TypeError('Render checkpoint root must be a real directory, not a symbolic link.');
137
+ }
138
+ const directoryInfo = await lstatIfPresent(directory);
139
+ if (directoryInfo && (directoryInfo.isSymbolicLink() || !directoryInfo.isDirectory())) {
140
+ throw new TypeError('Render checkpoint directory must be a real directory, not a symbolic link.');
141
+ }
142
+ }
143
+
144
+ async function lstatIfPresent(path) {
145
+ try {
146
+ return await lstat(path);
147
+ } catch (error) {
148
+ if (error.code === 'ENOENT') return undefined;
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ export function validateCheckpointLimits(maxBytes, maxAgeHours) {
154
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1024 * 1024 || maxBytes > 100 * 1024 ** 3) {
155
+ throw new TypeError('checkpointMaxBytes must be an integer from 1048576 to 107374182400.');
156
+ }
157
+ if (!Number.isSafeInteger(maxAgeHours) || maxAgeHours < 1 || maxAgeHours > 720) {
158
+ throw new TypeError('checkpointMaxAgeHours must be an integer from 1 to 720.');
159
+ }
160
+ }
161
+
162
+ async function collectFiles(root) {
163
+ const files = [];
164
+ const visit = async (directory) => {
165
+ const entries = await readdir(directory, { withFileTypes: true });
166
+ entries.sort((left, right) => left.name.localeCompare(right.name));
167
+ for (const entry of entries) {
168
+ const path = join(directory, entry.name);
169
+ if (entry.isSymbolicLink()) {
170
+ const target = await stat(path);
171
+ if (target.isFile()) files.push(path);
172
+ else if (target.isDirectory()) throw new Error(`Cannot fingerprint a project containing a linked directory: ${relative(root, path)}`);
173
+ }
174
+ if (entry.isDirectory()) {
175
+ if (!EXCLUDED_DIRECTORIES.has(entry.name)) await visit(path);
176
+ } else if (entry.isFile()) files.push(path);
177
+ }
178
+ };
179
+ await visit(root);
180
+ return files;
181
+ }
182
+
183
+ async function hashFile(path) {
184
+ const hash = createHash('sha256');
185
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
186
+ return hash.digest('hex');
187
+ }
188
+
189
+ function extension(path) {
190
+ const value = basename(path).split('.').at(-1).toLowerCase();
191
+ return /^[a-z0-9]{1,8}$/.test(value) ? value : 'bin';
192
+ }