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.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -21
- package/README.md +120 -118
- package/docs/architecture.md +401 -401
- package/docs/aspect-ratio-presets.md +72 -72
- package/docs/releasing.md +19 -19
- package/docs/usage.md +1193 -1193
- package/package.json +52 -52
- package/requirements-otio.txt +1 -1
- package/src/capabilities.mjs +307 -307
- package/src/cli.mjs +646 -646
- package/src/doctor.mjs +133 -133
- package/src/edits/analysis-to-edl.mjs +43 -43
- package/src/edits/detect-silence.mjs +51 -51
- package/src/edits/find-mistake-candidates.mjs +42 -42
- package/src/edits/plan-shot-placements.mjs +105 -105
- package/src/edits/plan-silence-cuts.mjs +49 -49
- package/src/edits/retime-transcript.mjs +105 -105
- package/src/files.mjs +80 -80
- package/src/index.mjs +44 -44
- package/src/interchange/otio.mjs +204 -204
- package/src/interchange/otio_bridge.py +521 -521
- package/src/media/analysis-provider.mjs +75 -75
- package/src/media/compatibility.mjs +45 -45
- package/src/media/ffmpeg-capabilities.mjs +129 -129
- package/src/media/frame-rate.mjs +58 -58
- package/src/media/frame-time.mjs +22 -22
- package/src/media/preview-assets.mjs +350 -350
- package/src/media/probe.mjs +149 -149
- package/src/media/render-cuts.mjs +90 -90
- package/src/media/scene-detection.mjs +107 -107
- package/src/media/scene-review.mjs +205 -205
- package/src/project/assets.mjs +1194 -1194
- package/src/project/create.mjs +100 -100
- package/src/project/presets.mjs +44 -44
- package/src/render/checkpoints.mjs +192 -192
- package/src/render/formats.mjs +163 -163
- package/src/render/hyperframes.mjs +590 -590
- package/src/render/process.mjs +111 -111
- package/src/render/progress.mjs +46 -46
- package/src/render/timeline-control.mjs +53 -53
- package/src/timeline/assemble.mjs +119 -119
- package/src/timeline/audio-effects.mjs +75 -75
- package/src/timeline/audio-mastering.mjs +236 -236
- package/src/timeline/audio-mix.mjs +105 -105
- package/src/timeline/audio-track-effects.mjs +139 -139
- package/src/timeline/audio-volume.mjs +63 -63
- package/src/timeline/background.mjs +8 -8
- package/src/timeline/blend-mode.mjs +34 -34
- package/src/timeline/caption-layouts.mjs +62 -61
- package/src/timeline/captions.mjs +80 -80
- package/src/timeline/edit.mjs +965 -965
- package/src/timeline/fingerprint.mjs +6 -6
- package/src/timeline/from-edl.mjs +31 -31
- package/src/timeline/history-store.mjs +281 -281
- package/src/timeline/history.mjs +69 -69
- package/src/timeline/inspect.mjs +117 -117
- package/src/timeline/keyframes.mjs +158 -158
- package/src/timeline/layers.mjs +18 -18
- package/src/timeline/mask.mjs +53 -53
- package/src/timeline/project.mjs +52 -52
- package/src/timeline/render.mjs +759 -759
- package/src/timeline/sidechain-render.mjs +209 -209
- package/src/timeline/source-fit.mjs +15 -15
- package/src/timeline/speech-bubble.mjs +26 -26
- package/src/timeline/speed-ramp.mjs +188 -188
- package/src/timeline/track-state.mjs +22 -22
- package/src/timeline/transition.mjs +34 -34
- package/src/timeline/validate.mjs +322 -322
- package/src/transcript/corrections.mjs +21 -21
- package/src/transcript/normalize.mjs +17 -17
- package/src/transcript/provider.mjs +42 -42
- package/src/transcript/subtitles.mjs +57 -57
- 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(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').replace(/ /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(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').replace(/ /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
|
+
}
|