aivee 0.1.0-alpha.0 → 0.1.0-dev.d34a6ba

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 (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +120 -118
  3. package/docs/architecture.md +401 -401
  4. package/docs/aspect-ratio-presets.md +72 -72
  5. package/docs/releasing.md +19 -19
  6. package/docs/usage.md +1193 -1193
  7. package/package.json +52 -52
  8. package/requirements-otio.txt +1 -1
  9. package/src/capabilities.mjs +307 -307
  10. package/src/cli.mjs +646 -646
  11. package/src/doctor.mjs +133 -133
  12. package/src/edits/analysis-to-edl.mjs +43 -43
  13. package/src/edits/detect-silence.mjs +51 -51
  14. package/src/edits/find-mistake-candidates.mjs +42 -42
  15. package/src/edits/plan-shot-placements.mjs +105 -105
  16. package/src/edits/plan-silence-cuts.mjs +49 -49
  17. package/src/edits/retime-transcript.mjs +105 -105
  18. package/src/files.mjs +80 -80
  19. package/src/index.mjs +44 -44
  20. package/src/interchange/otio.mjs +204 -204
  21. package/src/interchange/otio_bridge.py +521 -521
  22. package/src/media/analysis-provider.mjs +75 -75
  23. package/src/media/compatibility.mjs +45 -45
  24. package/src/media/ffmpeg-capabilities.mjs +129 -129
  25. package/src/media/frame-rate.mjs +58 -58
  26. package/src/media/frame-time.mjs +22 -22
  27. package/src/media/preview-assets.mjs +350 -350
  28. package/src/media/probe.mjs +149 -149
  29. package/src/media/render-cuts.mjs +90 -90
  30. package/src/media/scene-detection.mjs +107 -107
  31. package/src/media/scene-review.mjs +205 -205
  32. package/src/project/assets.mjs +1194 -1194
  33. package/src/project/create.mjs +100 -100
  34. package/src/project/presets.mjs +44 -44
  35. package/src/render/checkpoints.mjs +192 -192
  36. package/src/render/formats.mjs +163 -163
  37. package/src/render/hyperframes.mjs +590 -590
  38. package/src/render/process.mjs +111 -111
  39. package/src/render/progress.mjs +46 -46
  40. package/src/render/timeline-control.mjs +53 -53
  41. package/src/timeline/assemble.mjs +119 -119
  42. package/src/timeline/audio-effects.mjs +75 -75
  43. package/src/timeline/audio-mastering.mjs +236 -236
  44. package/src/timeline/audio-mix.mjs +105 -105
  45. package/src/timeline/audio-track-effects.mjs +139 -139
  46. package/src/timeline/audio-volume.mjs +63 -63
  47. package/src/timeline/background.mjs +8 -8
  48. package/src/timeline/blend-mode.mjs +34 -34
  49. package/src/timeline/caption-layouts.mjs +62 -61
  50. package/src/timeline/captions.mjs +80 -80
  51. package/src/timeline/edit.mjs +965 -965
  52. package/src/timeline/fingerprint.mjs +6 -6
  53. package/src/timeline/from-edl.mjs +31 -31
  54. package/src/timeline/history-store.mjs +281 -281
  55. package/src/timeline/history.mjs +69 -69
  56. package/src/timeline/inspect.mjs +117 -117
  57. package/src/timeline/keyframes.mjs +158 -158
  58. package/src/timeline/layers.mjs +18 -18
  59. package/src/timeline/mask.mjs +53 -53
  60. package/src/timeline/project.mjs +52 -52
  61. package/src/timeline/render.mjs +759 -759
  62. package/src/timeline/sidechain-render.mjs +209 -209
  63. package/src/timeline/source-fit.mjs +15 -15
  64. package/src/timeline/speech-bubble.mjs +26 -26
  65. package/src/timeline/speed-ramp.mjs +188 -188
  66. package/src/timeline/track-state.mjs +22 -22
  67. package/src/timeline/transition.mjs +34 -34
  68. package/src/timeline/validate.mjs +322 -322
  69. package/src/transcript/corrections.mjs +21 -21
  70. package/src/transcript/normalize.mjs +17 -17
  71. package/src/transcript/provider.mjs +42 -42
  72. package/src/transcript/subtitles.mjs +57 -57
  73. package/src/transcript/validate.mjs +39 -39
@@ -1,21 +1,21 @@
1
- import { validateTranscript } from './validate.mjs';
2
-
3
- export function applyTranscriptCorrections(transcript, corrections) {
4
- validateTranscript(transcript);
5
- if (!Array.isArray(corrections)) throw new TypeError('Transcript corrections must be an array.');
6
- const words = transcript.words.map((word) => ({ ...word }));
7
- const updated = new Set();
8
- for (const [index, correction] of corrections.entries()) {
9
- if (!correction || !Number.isSafeInteger(correction.wordIndex) || correction.wordIndex < 0 || correction.wordIndex >= words.length) throw new TypeError(`Correction ${index + 1} has an invalid wordIndex.`);
10
- if (updated.has(correction.wordIndex)) throw new TypeError(`Word ${correction.wordIndex} has duplicate corrections.`);
11
- updated.add(correction.wordIndex);
12
- const word = words[correction.wordIndex];
13
- if (correction.text !== undefined) {
14
- if (typeof correction.text !== 'string' || !correction.text.trim()) throw new TypeError(`Correction ${index + 1} text must be a non-empty string.`);
15
- word.text = correction.text.trim();
16
- }
17
- if (correction.start !== undefined) word.start = correction.start;
18
- if (correction.end !== undefined) word.end = correction.end;
19
- }
20
- return validateTranscript({ ...transcript, words });
21
- }
1
+ import { validateTranscript } from './validate.mjs';
2
+
3
+ export function applyTranscriptCorrections(transcript, corrections) {
4
+ validateTranscript(transcript);
5
+ if (!Array.isArray(corrections)) throw new TypeError('Transcript corrections must be an array.');
6
+ const words = transcript.words.map((word) => ({ ...word }));
7
+ const updated = new Set();
8
+ for (const [index, correction] of corrections.entries()) {
9
+ if (!correction || !Number.isSafeInteger(correction.wordIndex) || correction.wordIndex < 0 || correction.wordIndex >= words.length) throw new TypeError(`Correction ${index + 1} has an invalid wordIndex.`);
10
+ if (updated.has(correction.wordIndex)) throw new TypeError(`Word ${correction.wordIndex} has duplicate corrections.`);
11
+ updated.add(correction.wordIndex);
12
+ const word = words[correction.wordIndex];
13
+ if (correction.text !== undefined) {
14
+ if (typeof correction.text !== 'string' || !correction.text.trim()) throw new TypeError(`Correction ${index + 1} text must be a non-empty string.`);
15
+ word.text = correction.text.trim();
16
+ }
17
+ if (correction.start !== undefined) word.start = correction.start;
18
+ if (correction.end !== undefined) word.end = correction.end;
19
+ }
20
+ return validateTranscript({ ...transcript, words });
21
+ }
@@ -1,17 +1,17 @@
1
- import { validateTranscript } from './validate.mjs';
2
-
3
- export function normalizeTranscript(input, durationSeconds, granularity = 'word') {
4
- const sourceWords = Array.isArray(input) ? input : input?.words;
5
- if (!Array.isArray(sourceWords)) throw new TypeError('Transcript input must contain a words array.');
6
- const words = sourceWords
7
- .filter((word) => word?.type === undefined || word.type === 'word')
8
- .map((word) => ({
9
- text: String(word.text ?? word.word ?? '').trim(),
10
- start: Number(word.start),
11
- end: Number(word.end),
12
- }))
13
- .filter((word) => word.text);
14
- const transcript = { schemaVersion: 1, durationSeconds, granularity, words };
15
- validateTranscript(transcript);
16
- return transcript;
17
- }
1
+ import { validateTranscript } from './validate.mjs';
2
+
3
+ export function normalizeTranscript(input, durationSeconds, granularity = 'word') {
4
+ const sourceWords = Array.isArray(input) ? input : input?.words;
5
+ if (!Array.isArray(sourceWords)) throw new TypeError('Transcript input must contain a words array.');
6
+ const words = sourceWords
7
+ .filter((word) => word?.type === undefined || word.type === 'word')
8
+ .map((word) => ({
9
+ text: String(word.text ?? word.word ?? '').trim(),
10
+ start: Number(word.start),
11
+ end: Number(word.end),
12
+ }))
13
+ .filter((word) => word.text);
14
+ const transcript = { schemaVersion: 1, durationSeconds, granularity, words };
15
+ validateTranscript(transcript);
16
+ return transcript;
17
+ }
@@ -1,42 +1,42 @@
1
- import { existsSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { probeMedia } from '../media/probe.mjs';
4
- import { normalizeTranscript } from './normalize.mjs';
5
-
6
- /** Run an injected transcription provider and persist its normalized result. */
7
- export async function transcribeWithProvider(projectPath, mediaPath, provider, options = {}) {
8
- const project = resolve(projectPath);
9
- const media = resolve(mediaPath);
10
- if (!existsSync(join(project, 'meta.json'))) throw new Error(`Not a video project: ${project}`);
11
- if (!provider || typeof provider !== 'object'
12
- || !/^[a-z][a-z0-9-]*$/.test(provider.id ?? '')
13
- || typeof provider.transcribe !== 'function') {
14
- throw new TypeError('Provider must define a kebab-case id and async transcribe function.');
15
- }
16
- const granularity = options.granularity ?? 'word';
17
- if (!['word', 'cue'].includes(granularity)) throw new TypeError('Provider granularity must be word or cue.');
18
-
19
- const mediaInfo = probeMedia(media, { requireVideo: false });
20
- const raw = await provider.transcribe({
21
- mediaPath: media,
22
- durationSeconds: mediaInfo.durationSeconds,
23
- options: structuredClone(options.providerOptions ?? {}),
24
- });
25
- const transcript = normalizeTranscript(raw, mediaInfo.durationSeconds, granularity);
26
- const transcriptPath = join(project, 'transcripts', 'transcript.json');
27
- const fd = openSync(transcriptPath, 'wx');
28
- try {
29
- writeFileSync(fd, `${JSON.stringify(transcript, null, 2)}\n`);
30
- } catch (error) {
31
- unlinkSync(transcriptPath);
32
- throw error;
33
- } finally {
34
- closeSync(fd);
35
- }
36
- return {
37
- providerId: provider.id,
38
- transcriptPath,
39
- wordCount: transcript.words.length,
40
- durationSeconds: transcript.durationSeconds,
41
- };
42
- }
1
+ import { existsSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { probeMedia } from '../media/probe.mjs';
4
+ import { normalizeTranscript } from './normalize.mjs';
5
+
6
+ /** Run an injected transcription provider and persist its normalized result. */
7
+ export async function transcribeWithProvider(projectPath, mediaPath, provider, options = {}) {
8
+ const project = resolve(projectPath);
9
+ const media = resolve(mediaPath);
10
+ if (!existsSync(join(project, 'meta.json'))) throw new Error(`Not a video project: ${project}`);
11
+ if (!provider || typeof provider !== 'object'
12
+ || !/^[a-z][a-z0-9-]*$/.test(provider.id ?? '')
13
+ || typeof provider.transcribe !== 'function') {
14
+ throw new TypeError('Provider must define a kebab-case id and async transcribe function.');
15
+ }
16
+ const granularity = options.granularity ?? 'word';
17
+ if (!['word', 'cue'].includes(granularity)) throw new TypeError('Provider granularity must be word or cue.');
18
+
19
+ const mediaInfo = probeMedia(media, { requireVideo: false });
20
+ const raw = await provider.transcribe({
21
+ mediaPath: media,
22
+ durationSeconds: mediaInfo.durationSeconds,
23
+ options: structuredClone(options.providerOptions ?? {}),
24
+ });
25
+ const transcript = normalizeTranscript(raw, mediaInfo.durationSeconds, granularity);
26
+ const transcriptPath = join(project, 'transcripts', 'transcript.json');
27
+ const fd = openSync(transcriptPath, 'wx');
28
+ try {
29
+ writeFileSync(fd, `${JSON.stringify(transcript, null, 2)}\n`);
30
+ } catch (error) {
31
+ unlinkSync(transcriptPath);
32
+ throw error;
33
+ } finally {
34
+ closeSync(fd);
35
+ }
36
+ return {
37
+ providerId: provider.id,
38
+ transcriptPath,
39
+ wordCount: transcript.words.length,
40
+ durationSeconds: transcript.durationSeconds,
41
+ };
42
+ }
@@ -1,57 +1,57 @@
1
- import { validateTranscript } from './validate.mjs';
2
-
3
- export function parseSubtitleText(contents, format) {
4
- if (typeof contents !== 'string') throw new TypeError('Subtitle contents must be text.');
5
- if (!['srt', 'vtt'].includes(format)) throw new TypeError('Subtitle format must be srt or vtt.');
6
- const text = contents.replace(/^\uFEFF/, '').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
7
- const words = [];
8
- for (const block of text.split(/\n\s*\n/)) {
9
- const lines = block.split('\n');
10
- const timeIndex = lines.findIndex((line) => line.includes('-->'));
11
- if (timeIndex < 0) continue;
12
- const match = lines[timeIndex].match(/^\s*((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})(?:\s+.*)?$/);
13
- if (!match) throw new TypeError(`Invalid ${format.toUpperCase()} timestamp: ${lines[timeIndex]}`);
14
- const start = parseTimestamp(match[1]);
15
- const end = parseTimestamp(match[2]);
16
- const cueText = lines.slice(timeIndex + 1).join(' ')
17
- .replace(/<[^>]*>/g, '')
18
- .replace(/&amp;/gi, '&').replace(/&lt;/gi, '<').replace(/&gt;/gi, '>').replace(/&nbsp;/gi, ' ')
19
- .replace(/\s+/g, ' ').trim();
20
- if (cueText) words.push({ text: cueText, start, end });
21
- }
22
- if (!words.length) throw new TypeError(`No subtitle cues found in ${format.toUpperCase()} input.`);
23
- return { words };
24
- }
25
-
26
- export function formatSubtitleText(transcript, format) {
27
- validateTranscript(transcript);
28
- if (!['srt', 'vtt'].includes(format)) throw new TypeError('Subtitle format must be srt or vtt.');
29
- const cues = transcript.words.map((word, index) => [
30
- ...(format === 'srt' ? [String(index + 1)] : []),
31
- `${formatTimestamp(word.start, format)} --> ${formatTimestamp(word.end, format)}`,
32
- word.text.replace(/\s*\n\s*/g, ' '),
33
- ].join('\n'));
34
- return `${format === 'vtt' ? 'WEBVTT\n\n' : ''}${cues.join('\n\n')}\n`;
35
- }
36
-
37
- function parseTimestamp(value) {
38
- const normalized = value.replace(',', '.');
39
- const parts = normalized.split(':');
40
- const seconds = Number(parts.pop());
41
- const minutes = Number(parts.pop());
42
- const hours = parts.length ? Number(parts.pop()) : 0;
43
- if (![hours, minutes, seconds].every(Number.isFinite) || minutes >= 60 || seconds >= 60) {
44
- throw new TypeError(`Invalid subtitle time: ${value}`);
45
- }
46
- return hours * 3600 + minutes * 60 + seconds;
47
- }
48
-
49
- function formatTimestamp(seconds, format) {
50
- const milliseconds = Math.round(seconds * 1000);
51
- const hours = Math.floor(milliseconds / 3_600_000);
52
- const minutes = Math.floor((milliseconds % 3_600_000) / 60_000);
53
- const wholeSeconds = Math.floor((milliseconds % 60_000) / 1000);
54
- const fraction = milliseconds % 1000;
55
- const time = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(wholeSeconds).padStart(2, '0')}`;
56
- return `${time}${format === 'srt' ? ',' : '.'}${String(fraction).padStart(3, '0')}`;
57
- }
1
+ import { validateTranscript } from './validate.mjs';
2
+
3
+ export function parseSubtitleText(contents, format) {
4
+ if (typeof contents !== 'string') throw new TypeError('Subtitle contents must be text.');
5
+ if (!['srt', 'vtt'].includes(format)) throw new TypeError('Subtitle format must be srt or vtt.');
6
+ const text = contents.replace(/^\uFEFF/, '').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
7
+ const words = [];
8
+ for (const block of text.split(/\n\s*\n/)) {
9
+ const lines = block.split('\n');
10
+ const timeIndex = lines.findIndex((line) => line.includes('-->'));
11
+ if (timeIndex < 0) continue;
12
+ const match = lines[timeIndex].match(/^\s*((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})(?:\s+.*)?$/);
13
+ if (!match) throw new TypeError(`Invalid ${format.toUpperCase()} timestamp: ${lines[timeIndex]}`);
14
+ const start = parseTimestamp(match[1]);
15
+ const end = parseTimestamp(match[2]);
16
+ const cueText = lines.slice(timeIndex + 1).join(' ')
17
+ .replace(/<[^>]*>/g, '')
18
+ .replace(/&amp;/gi, '&').replace(/&lt;/gi, '<').replace(/&gt;/gi, '>').replace(/&nbsp;/gi, ' ')
19
+ .replace(/\s+/g, ' ').trim();
20
+ if (cueText) words.push({ text: cueText, start, end });
21
+ }
22
+ if (!words.length) throw new TypeError(`No subtitle cues found in ${format.toUpperCase()} input.`);
23
+ return { words };
24
+ }
25
+
26
+ export function formatSubtitleText(transcript, format) {
27
+ validateTranscript(transcript);
28
+ if (!['srt', 'vtt'].includes(format)) throw new TypeError('Subtitle format must be srt or vtt.');
29
+ const cues = transcript.words.map((word, index) => [
30
+ ...(format === 'srt' ? [String(index + 1)] : []),
31
+ `${formatTimestamp(word.start, format)} --> ${formatTimestamp(word.end, format)}`,
32
+ word.text.replace(/\s*\n\s*/g, ' '),
33
+ ].join('\n'));
34
+ return `${format === 'vtt' ? 'WEBVTT\n\n' : ''}${cues.join('\n\n')}\n`;
35
+ }
36
+
37
+ function parseTimestamp(value) {
38
+ const normalized = value.replace(',', '.');
39
+ const parts = normalized.split(':');
40
+ const seconds = Number(parts.pop());
41
+ const minutes = Number(parts.pop());
42
+ const hours = parts.length ? Number(parts.pop()) : 0;
43
+ if (![hours, minutes, seconds].every(Number.isFinite) || minutes >= 60 || seconds >= 60) {
44
+ throw new TypeError(`Invalid subtitle time: ${value}`);
45
+ }
46
+ return hours * 3600 + minutes * 60 + seconds;
47
+ }
48
+
49
+ function formatTimestamp(seconds, format) {
50
+ const milliseconds = Math.round(seconds * 1000);
51
+ const hours = Math.floor(milliseconds / 3_600_000);
52
+ const minutes = Math.floor((milliseconds % 3_600_000) / 60_000);
53
+ const wholeSeconds = Math.floor((milliseconds % 60_000) / 1000);
54
+ const fraction = milliseconds % 1000;
55
+ const time = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(wholeSeconds).padStart(2, '0')}`;
56
+ return `${time}${format === 'srt' ? ',' : '.'}${String(fraction).padStart(3, '0')}`;
57
+ }
@@ -1,39 +1,39 @@
1
- export function validateTranscript(transcript) {
2
- if (!transcript || typeof transcript !== 'object' || Array.isArray(transcript)) {
3
- throw new TypeError('Transcript must be a JSON object.');
4
- }
5
- if (transcript.schemaVersion !== 1) {
6
- throw new TypeError('Transcript schemaVersion must be 1.');
7
- }
8
- if (!Number.isFinite(transcript.durationSeconds) || transcript.durationSeconds <= 0) {
9
- throw new TypeError('Transcript durationSeconds must be a positive number.');
10
- }
11
- if (!Array.isArray(transcript.words)) {
12
- throw new TypeError('Transcript words must be an array.');
13
- }
14
- if (transcript.granularity !== undefined && !['word', 'cue'].includes(transcript.granularity)) {
15
- throw new TypeError('Transcript granularity must be word or cue.');
16
- }
17
-
18
- let previousStart = -Infinity;
19
- for (const [index, word] of transcript.words.entries()) {
20
- const label = `Transcript word ${index + 1}`;
21
- if (!word || typeof word !== 'object' || Array.isArray(word)) {
22
- throw new TypeError(`${label} must be an object.`);
23
- }
24
- if (typeof word.text !== 'string' || !word.text.trim()) {
25
- throw new TypeError(`${label} text must be a non-empty string.`);
26
- }
27
- if (!Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end <= word.start) {
28
- throw new TypeError(`${label} must have finite times with 0 <= start < end.`);
29
- }
30
- if (word.end > transcript.durationSeconds) {
31
- throw new TypeError(`${label} ends after the source duration.`);
32
- }
33
- if (word.start < previousStart) {
34
- throw new TypeError(`${label} starts before the previous word.`);
35
- }
36
- previousStart = word.start;
37
- }
38
- return transcript;
39
- }
1
+ export function validateTranscript(transcript) {
2
+ if (!transcript || typeof transcript !== 'object' || Array.isArray(transcript)) {
3
+ throw new TypeError('Transcript must be a JSON object.');
4
+ }
5
+ if (transcript.schemaVersion !== 1) {
6
+ throw new TypeError('Transcript schemaVersion must be 1.');
7
+ }
8
+ if (!Number.isFinite(transcript.durationSeconds) || transcript.durationSeconds <= 0) {
9
+ throw new TypeError('Transcript durationSeconds must be a positive number.');
10
+ }
11
+ if (!Array.isArray(transcript.words)) {
12
+ throw new TypeError('Transcript words must be an array.');
13
+ }
14
+ if (transcript.granularity !== undefined && !['word', 'cue'].includes(transcript.granularity)) {
15
+ throw new TypeError('Transcript granularity must be word or cue.');
16
+ }
17
+
18
+ let previousStart = -Infinity;
19
+ for (const [index, word] of transcript.words.entries()) {
20
+ const label = `Transcript word ${index + 1}`;
21
+ if (!word || typeof word !== 'object' || Array.isArray(word)) {
22
+ throw new TypeError(`${label} must be an object.`);
23
+ }
24
+ if (typeof word.text !== 'string' || !word.text.trim()) {
25
+ throw new TypeError(`${label} text must be a non-empty string.`);
26
+ }
27
+ if (!Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end <= word.start) {
28
+ throw new TypeError(`${label} must have finite times with 0 <= start < end.`);
29
+ }
30
+ if (word.end > transcript.durationSeconds) {
31
+ throw new TypeError(`${label} ends after the source duration.`);
32
+ }
33
+ if (word.start < previousStart) {
34
+ throw new TypeError(`${label} starts before the previous word.`);
35
+ }
36
+ previousStart = word.start;
37
+ }
38
+ return transcript;
39
+ }