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,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
+ }