visualfries 0.1.10120 → 0.3.2
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/README.md +97 -0
- package/bin/visualfries.js +1570 -0
- package/dist/DIContainer.d.ts +2 -1
- package/dist/DIContainer.js +16 -5
- package/dist/SceneBuilder.svelte.d.ts +70 -1
- package/dist/SceneBuilder.svelte.js +96 -29
- package/dist/agent/audioMixer.d.ts +34 -0
- package/dist/agent/audioMixer.js +308 -0
- package/dist/agent/broll.d.ts +35 -0
- package/dist/agent/broll.js +226 -0
- package/dist/agent/capabilities.d.ts +34 -0
- package/dist/agent/capabilities.js +246 -0
- package/dist/agent/captionScene.d.ts +23 -0
- package/dist/agent/captionScene.js +204 -0
- package/dist/agent/catalog.d.ts +24 -0
- package/dist/agent/catalog.js +67 -0
- package/dist/agent/cueFiles.d.ts +54 -0
- package/dist/agent/cueFiles.js +219 -0
- package/dist/agent/cuePresets.d.ts +7 -0
- package/dist/agent/cuePresets.js +50 -0
- package/dist/agent/deterministicMedia.d.ts +61 -0
- package/dist/agent/deterministicMedia.js +303 -0
- package/dist/agent/index.d.ts +18 -0
- package/dist/agent/index.js +18 -0
- package/dist/agent/inspectScene.d.ts +36 -0
- package/dist/agent/inspectScene.js +139 -0
- package/dist/agent/localRender.d.ts +82 -0
- package/dist/agent/localRender.js +702 -0
- package/dist/agent/overlays.d.ts +40 -0
- package/dist/agent/overlays.js +325 -0
- package/dist/agent/productionPlan.d.ts +131 -0
- package/dist/agent/productionPlan.js +322 -0
- package/dist/agent/renderPlan.d.ts +27 -0
- package/dist/agent/renderPlan.js +68 -0
- package/dist/agent/renderRanges.d.ts +24 -0
- package/dist/agent/renderRanges.js +73 -0
- package/dist/agent/runtimeSupport.d.ts +19 -0
- package/dist/agent/runtimeSupport.js +192 -0
- package/dist/agent/streamingEncoder.d.ts +31 -0
- package/dist/agent/streamingEncoder.js +208 -0
- package/dist/agent/transcripts.d.ts +29 -0
- package/dist/agent/transcripts.js +146 -0
- package/dist/agent/transitions.d.ts +20 -0
- package/dist/agent/transitions.js +169 -0
- package/dist/animations/PixiAnimationTarget.d.ts +29 -0
- package/dist/animations/PixiAnimationTarget.js +63 -0
- package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
- package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
- package/dist/browser/browserExport.d.ts +73 -0
- package/dist/browser/browserExport.js +180 -0
- package/dist/browser/glTransitions.d.ts +37 -0
- package/dist/browser/glTransitions.js +261 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/mediaDecode.d.ts +31 -0
- package/dist/browser/mediaDecode.js +152 -0
- package/dist/builders/PixiComponentBuilder.d.ts +6 -0
- package/dist/builders/PixiComponentBuilder.js +12 -0
- package/dist/builders/_ComponentState.svelte.d.ts +7 -5
- package/dist/builders/_ComponentState.svelte.js +29 -15
- package/dist/builders/html/StyleBuilder.js +9 -8
- package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
- package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
- package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
- package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
- package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
- package/dist/commands/SeekCommand.js +19 -6
- package/dist/components/AnimatedGIF.d.ts +3 -0
- package/dist/components/AnimatedGIF.js +28 -1
- package/dist/components/Component.svelte.d.ts +5 -5
- package/dist/components/Component.svelte.js +29 -13
- package/dist/components/ComponentContext.svelte.js +68 -23
- package/dist/components/hooks/AnimationHook.js +1 -1
- package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
- package/dist/components/hooks/CanvasFillHook.js +75 -0
- package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
- package/dist/components/hooks/HtmlTextHook.js +11 -3
- package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
- package/dist/components/hooks/MediaHook.js +172 -47
- package/dist/components/hooks/MediaSeekingHook.js +138 -88
- package/dist/components/hooks/PixiGifHook.js +4 -4
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +73 -6
- package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
- package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
- package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
- package/dist/components/hooks/SubtitlesHook.js +48 -24
- package/dist/components/hooks/VerifyMediaHook.js +2 -2
- package/dist/directors/ComponentDirector.d.ts +1 -0
- package/dist/directors/ComponentDirector.js +41 -8
- package/dist/fonts/fontDiscovery.js +12 -1
- package/dist/layers/Layer.svelte.d.ts +54 -0
- package/dist/layers/Layer.svelte.js +2 -25
- package/dist/managers/AppManager.svelte.js +8 -5
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.js +38 -3
- package/dist/managers/EventManager.js +3 -1
- package/dist/managers/LayersManager.svelte.d.ts +54 -0
- package/dist/managers/RenderManager.d.ts +5 -0
- package/dist/managers/RenderManager.js +67 -21
- package/dist/managers/StateManager.svelte.d.ts +56 -0
- package/dist/managers/StateManager.svelte.js +60 -58
- package/dist/managers/SubtitlesManager.svelte.js +1 -3
- package/dist/managers/TimeManager.svelte.d.ts +4 -0
- package/dist/managers/TimeManager.svelte.js +23 -7
- package/dist/schemas/runtime/deterministic.js +5 -1
- package/dist/schemas/runtime/types.d.ts +36 -15
- package/dist/schemas/scene/components.d.ts +126 -0
- package/dist/schemas/scene/components.js +11 -0
- package/dist/schemas/scene/core.d.ts +109 -10
- package/dist/schemas/scene/core.js +1 -1
- package/dist/utils/mediaWindow.d.ts +13 -0
- package/dist/utils/mediaWindow.js +25 -0
- package/dist/utils/svgGenerator.js +1 -1
- package/dist/utils/timelineWindow.d.ts +2 -0
- package/dist/utils/timelineWindow.js +7 -0
- package/dist/utils/utils.js +2 -4
- package/docs/AGENT_PATTERNS.md +243 -0
- package/docs/AGENT_WORKFLOW.md +364 -0
- package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
- package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
- package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
- package/docs/DETERMINISTIC_RENDERER.md +83 -0
- package/docs/NEXT_RELEASE.md +28 -0
- package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
- package/docs/examples/agent-cues.json +49 -0
- package/docs/examples/agent-transcript.json +23 -0
- package/docs/examples/agent-transcript.srt +7 -0
- package/docs/examples/agent-transcript.vtt +7 -0
- package/docs/examples/image-animation.json +87 -0
- package/docs/examples/native-text-metric-badge.json +109 -0
- package/package.json +133 -100
- package/skills/visualfries/SKILL.md +328 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { addAgentBrollSequence } from './broll.js';
|
|
3
|
+
import { AGENT_BROLL_MOTIONS, AGENT_OVERLAY_STYLES, AGENT_TRANSITION_STYLES } from './catalog.js';
|
|
4
|
+
import { addAgentTextOverlays } from './overlays.js';
|
|
5
|
+
import { addAgentTransitions } from './transitions.js';
|
|
6
|
+
import { SceneShape } from '../schemas/scene/index.js';
|
|
7
|
+
export function mergeAgentCueFiles(...cueFiles) {
|
|
8
|
+
return cueFiles.reduce((merged, cues) => ({
|
|
9
|
+
...merged,
|
|
10
|
+
...cues,
|
|
11
|
+
broll: [...(merged.broll ?? []), ...(cues.broll ?? [])],
|
|
12
|
+
overlays: [...(merged.overlays ?? []), ...(cues.overlays ?? [])],
|
|
13
|
+
transitions: [...(merged.transitions ?? []), ...(cues.transitions ?? [])]
|
|
14
|
+
}), {});
|
|
15
|
+
}
|
|
16
|
+
function cueArray(cues, key) {
|
|
17
|
+
const value = cues[key];
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return [];
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw new Error(`cues.${String(key)} must be an array.`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function isPortableUrl(value) {
|
|
26
|
+
return /^[a-z]+:\/\//i.test(value) || value.startsWith('file://') || value.startsWith('data:');
|
|
27
|
+
}
|
|
28
|
+
export function normalizeAgentCueFile(cues, cueFilePath) {
|
|
29
|
+
const baseDir = cueFilePath ? path.dirname(path.resolve(cueFilePath)) : undefined;
|
|
30
|
+
const broll = cueArray(cues, 'broll').map((cue) => {
|
|
31
|
+
if (!baseDir || !cue.url || isPortableUrl(cue.url) || path.isAbsolute(cue.url))
|
|
32
|
+
return cue;
|
|
33
|
+
return {
|
|
34
|
+
...cue,
|
|
35
|
+
url: path.join(baseDir, cue.url)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
...cues,
|
|
40
|
+
broll,
|
|
41
|
+
overlays: cueArray(cues, 'overlays'),
|
|
42
|
+
transitions: cueArray(cues, 'transitions')
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isNumber(value) {
|
|
46
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
47
|
+
}
|
|
48
|
+
function checkRange(issues, path, start, end, duration) {
|
|
49
|
+
if (!isNumber(start)) {
|
|
50
|
+
issues.push({
|
|
51
|
+
level: 'error',
|
|
52
|
+
path: `${path}.start`,
|
|
53
|
+
message: 'start must be a finite number.'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!isNumber(end)) {
|
|
57
|
+
issues.push({ level: 'error', path: `${path}.end`, message: 'end must be a finite number.' });
|
|
58
|
+
}
|
|
59
|
+
if (isNumber(start) && isNumber(end) && end < start) {
|
|
60
|
+
issues.push({
|
|
61
|
+
level: 'error',
|
|
62
|
+
path: `${path}.end`,
|
|
63
|
+
message: 'end must be greater than or equal to start.'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (duration !== undefined && isNumber(end) && end > duration) {
|
|
67
|
+
issues.push({
|
|
68
|
+
level: 'warning',
|
|
69
|
+
path: `${path}.end`,
|
|
70
|
+
message: `end is outside scene duration (${end}s > ${duration}s).`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function validateAgentCueFile(input) {
|
|
75
|
+
const cues = normalizeAgentCueFile(input.cues, input.cueFilePath);
|
|
76
|
+
const broll = cues.broll ?? [];
|
|
77
|
+
const overlays = cues.overlays ?? [];
|
|
78
|
+
const transitions = cues.transitions ?? [];
|
|
79
|
+
const issues = [];
|
|
80
|
+
const durationIsValid = input.duration === undefined || (isNumber(input.duration) && input.duration > 0);
|
|
81
|
+
if (!durationIsValid) {
|
|
82
|
+
issues.push({
|
|
83
|
+
level: 'error',
|
|
84
|
+
path: 'duration',
|
|
85
|
+
message: 'duration must be a finite number greater than 0.'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
for (const [index, cue] of broll.entries()) {
|
|
89
|
+
const pathPrefix = `broll[${index}]`;
|
|
90
|
+
if (!cue.url || typeof cue.url !== 'string') {
|
|
91
|
+
issues.push({ level: 'error', path: `${pathPrefix}.url`, message: 'url is required.' });
|
|
92
|
+
}
|
|
93
|
+
if (cue.motion &&
|
|
94
|
+
!AGENT_BROLL_MOTIONS.includes(cue.motion)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
level: 'error',
|
|
97
|
+
path: `${pathPrefix}.motion`,
|
|
98
|
+
message: `Unknown b-roll motion "${cue.motion}".`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
checkRange(issues, pathPrefix, cue.start, cue.end, input.duration);
|
|
102
|
+
}
|
|
103
|
+
for (const [index, cue] of overlays.entries()) {
|
|
104
|
+
const pathPrefix = `overlays[${index}]`;
|
|
105
|
+
if (!cue.text || typeof cue.text !== 'string') {
|
|
106
|
+
issues.push({ level: 'error', path: `${pathPrefix}.text`, message: 'text is required.' });
|
|
107
|
+
}
|
|
108
|
+
if (cue.style &&
|
|
109
|
+
!AGENT_OVERLAY_STYLES.includes(cue.style)) {
|
|
110
|
+
issues.push({
|
|
111
|
+
level: 'error',
|
|
112
|
+
path: `${pathPrefix}.style`,
|
|
113
|
+
message: `Unknown overlay style "${cue.style}".`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
checkRange(issues, pathPrefix, cue.start, cue.end, input.duration);
|
|
117
|
+
}
|
|
118
|
+
for (const [index, cue] of transitions.entries()) {
|
|
119
|
+
const pathPrefix = `transitions[${index}]`;
|
|
120
|
+
if (!isNumber(cue.time)) {
|
|
121
|
+
issues.push({
|
|
122
|
+
level: 'error',
|
|
123
|
+
path: `${pathPrefix}.time`,
|
|
124
|
+
message: 'time must be a finite number.'
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (cue.time < 0) {
|
|
128
|
+
issues.push({
|
|
129
|
+
level: 'error',
|
|
130
|
+
path: `${pathPrefix}.time`,
|
|
131
|
+
message: 'time must be greater than or equal to 0.'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (cue.duration !== undefined && (!isNumber(cue.duration) || cue.duration <= 0)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
level: 'error',
|
|
137
|
+
path: `${pathPrefix}.duration`,
|
|
138
|
+
message: 'duration must be a finite number greater than 0.'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (cue.style &&
|
|
142
|
+
!AGENT_TRANSITION_STYLES.includes(cue.style)) {
|
|
143
|
+
issues.push({
|
|
144
|
+
level: 'error',
|
|
145
|
+
path: `${pathPrefix}.style`,
|
|
146
|
+
message: `Unknown transition style "${cue.style}".`
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (durationIsValid &&
|
|
150
|
+
input.duration !== undefined &&
|
|
151
|
+
isNumber(cue.time) &&
|
|
152
|
+
cue.time > input.duration) {
|
|
153
|
+
issues.push({
|
|
154
|
+
level: 'warning',
|
|
155
|
+
path: `${pathPrefix}.time`,
|
|
156
|
+
message: `time is outside scene duration (${cue.time}s > ${input.duration}s).`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
valid: !issues.some((issue) => issue.level === 'error'),
|
|
162
|
+
issues,
|
|
163
|
+
summary: {
|
|
164
|
+
broll: broll.length,
|
|
165
|
+
overlays: overlays.length,
|
|
166
|
+
transitions: transitions.length
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function applyAgentCueFile(input) {
|
|
171
|
+
const cues = normalizeAgentCueFile(input.cues, input.cueFilePath);
|
|
172
|
+
let scene = SceneShape.parse(input.scene);
|
|
173
|
+
const validation = validateAgentCueFile({
|
|
174
|
+
cues,
|
|
175
|
+
cueFilePath: input.cueFilePath,
|
|
176
|
+
duration: scene.settings.duration
|
|
177
|
+
});
|
|
178
|
+
if (!validation.valid) {
|
|
179
|
+
throw new Error(`Invalid cue file: ${JSON.stringify(validation.issues, null, 2)}`);
|
|
180
|
+
}
|
|
181
|
+
const broll = cues.broll ?? [];
|
|
182
|
+
const overlays = cues.overlays ?? [];
|
|
183
|
+
const transitions = cues.transitions ?? [];
|
|
184
|
+
if (broll.length > 0) {
|
|
185
|
+
scene = addAgentBrollSequence({
|
|
186
|
+
scene,
|
|
187
|
+
cues: broll,
|
|
188
|
+
layerId: cues.brollLayerId,
|
|
189
|
+
layerName: cues.brollLayerName,
|
|
190
|
+
layerOrder: cues.brollLayerOrder
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (overlays.length > 0) {
|
|
194
|
+
scene = addAgentTextOverlays({
|
|
195
|
+
scene,
|
|
196
|
+
overlays,
|
|
197
|
+
layerId: cues.overlayLayerId,
|
|
198
|
+
layerName: cues.overlayLayerName,
|
|
199
|
+
layerOrder: cues.overlayLayerOrder
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (transitions.length > 0) {
|
|
203
|
+
scene = addAgentTransitions({
|
|
204
|
+
scene,
|
|
205
|
+
transitions,
|
|
206
|
+
layerId: cues.transitionLayerId,
|
|
207
|
+
layerName: cues.transitionLayerName,
|
|
208
|
+
layerOrder: cues.transitionLayerOrder
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
scene: SceneShape.parse(scene),
|
|
213
|
+
applied: {
|
|
214
|
+
broll: broll.length,
|
|
215
|
+
overlays: overlays.length,
|
|
216
|
+
transitions: transitions.length
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentCueFile } from './cueFiles.js';
|
|
2
|
+
export type AgentCuePresetName = 'hidden-engine-dynamic' | 'captioned-clean';
|
|
3
|
+
export type CreateAgentCuePresetInput = {
|
|
4
|
+
preset?: AgentCuePresetName;
|
|
5
|
+
duration: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function createAgentCuePreset(input: CreateAgentCuePresetInput): AgentCueFile;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function clampTime(value, duration) {
|
|
2
|
+
return Number(Math.min(Math.max(value, 0), duration).toFixed(3));
|
|
3
|
+
}
|
|
4
|
+
function timed(start, length, duration) {
|
|
5
|
+
const safeStart = clampTime(start, duration);
|
|
6
|
+
return {
|
|
7
|
+
start: safeStart,
|
|
8
|
+
end: clampTime(Math.max(safeStart + 0.1, start + length), duration)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function transitionTimes(duration) {
|
|
12
|
+
const count = Math.max(0, Math.min(8, Math.floor(duration / 5)));
|
|
13
|
+
return Array.from({ length: count }, (_, index) => clampTime((index + 1) * (duration / (count + 1)), duration));
|
|
14
|
+
}
|
|
15
|
+
export function createAgentCuePreset(input) {
|
|
16
|
+
const preset = input.preset ?? 'hidden-engine-dynamic';
|
|
17
|
+
const duration = Math.max(0.5, input.duration);
|
|
18
|
+
if (preset === 'captioned-clean') {
|
|
19
|
+
return {
|
|
20
|
+
overlays: [],
|
|
21
|
+
broll: [],
|
|
22
|
+
transitions: transitionTimes(duration).map((time) => ({
|
|
23
|
+
time,
|
|
24
|
+
style: 'dip-to-black',
|
|
25
|
+
duration: 0.18
|
|
26
|
+
}))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const hook = timed(0.35, 0.9, duration);
|
|
30
|
+
const proof = timed(Math.min(8, duration * 0.22), 1.2, duration);
|
|
31
|
+
const mechanism = timed(Math.min(16, duration * 0.42), 1.15, duration);
|
|
32
|
+
const choice = timed(Math.max(0.4, duration - 7), 1.1, duration);
|
|
33
|
+
const cta = timed(Math.max(0.5, duration - 3.2), 1.4, duration);
|
|
34
|
+
return {
|
|
35
|
+
overlays: [
|
|
36
|
+
{ text: 'HOOK', ...hook, style: 'hook-punch' },
|
|
37
|
+
{ text: 'PROOF', ...proof, style: 'proof-pill' },
|
|
38
|
+
{ text: 'THE MACHINE', ...mechanism, style: 'metric-badge' },
|
|
39
|
+
{ text: 'CHOOSE', ...choice, style: 'shock-word' },
|
|
40
|
+
{ text: 'your move', ...cta, style: 'cta-card' }
|
|
41
|
+
],
|
|
42
|
+
broll: [],
|
|
43
|
+
transitions: transitionTimes(duration).map((time, index) => ({
|
|
44
|
+
time,
|
|
45
|
+
style: index % 3 === 0 ? 'swipe-left' : 'dip-to-black',
|
|
46
|
+
color: index % 3 === 0 ? '#04483D' : undefined,
|
|
47
|
+
duration: index % 3 === 0 ? 0.24 : 0.2
|
|
48
|
+
}))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Scene } from '../schemas/scene/index.js';
|
|
2
|
+
export type LocalDeterministicMediaStrategy = 'predecoded-image-sequence';
|
|
3
|
+
export type LocalDeterministicFrameExtension = 'jpg' | 'png';
|
|
4
|
+
export type LocalDeterministicMediaComponent = {
|
|
5
|
+
component: Scene['layers'][number]['components'][number];
|
|
6
|
+
id: string;
|
|
7
|
+
type: 'VIDEO' | 'GIF';
|
|
8
|
+
sourceUrl: string;
|
|
9
|
+
sourceStartAt: number;
|
|
10
|
+
sourceEndAt?: number;
|
|
11
|
+
timelineStartAt: number;
|
|
12
|
+
timelineEndAt: number;
|
|
13
|
+
};
|
|
14
|
+
export type LocalDeterministicActiveWindow = {
|
|
15
|
+
activeStartFrame: number;
|
|
16
|
+
activeEndFrame: number;
|
|
17
|
+
activeStartSec: number;
|
|
18
|
+
activeEndSec: number;
|
|
19
|
+
sourceStartSec: number;
|
|
20
|
+
};
|
|
21
|
+
export type LocalPredecodedExtractionPlan = {
|
|
22
|
+
sourceStartSec: number;
|
|
23
|
+
extractFrameCount: number;
|
|
24
|
+
outputFrameCount: number;
|
|
25
|
+
sourceEndLimited: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type LocalDeterministicFrameManifest = Record<string, Record<string, string>>;
|
|
28
|
+
export type PrepareLocalDeterministicMediaInput = {
|
|
29
|
+
scene: unknown;
|
|
30
|
+
workDir: string;
|
|
31
|
+
fromFrame: number;
|
|
32
|
+
toFrame: number;
|
|
33
|
+
publicBasePath?: string;
|
|
34
|
+
frameExtension?: LocalDeterministicFrameExtension;
|
|
35
|
+
jpegQualityScale?: number;
|
|
36
|
+
strict?: boolean;
|
|
37
|
+
diagnostics?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export type PrepareLocalDeterministicMediaResult = {
|
|
40
|
+
preparedScene: Scene;
|
|
41
|
+
payload: {
|
|
42
|
+
strategy: 'visualfries-provider-predecoded';
|
|
43
|
+
frameManifest: LocalDeterministicFrameManifest;
|
|
44
|
+
diagnosticsEnabled: boolean;
|
|
45
|
+
mediaDeterministicStrict: boolean;
|
|
46
|
+
};
|
|
47
|
+
media: Array<{
|
|
48
|
+
componentId: string;
|
|
49
|
+
type: 'VIDEO' | 'GIF';
|
|
50
|
+
sourceUrl: string;
|
|
51
|
+
framesPrepared: number;
|
|
52
|
+
}>;
|
|
53
|
+
assetsRoot: string;
|
|
54
|
+
strategyUsed: LocalDeterministicMediaStrategy;
|
|
55
|
+
};
|
|
56
|
+
export declare const normalizeDeterministicPublicBasePath: (value: string) => string;
|
|
57
|
+
export declare function collectLocalDeterministicMediaComponents(input: unknown): LocalDeterministicMediaComponent[];
|
|
58
|
+
export declare function resolveLocalDeterministicActiveWindow(component: LocalDeterministicMediaComponent, fromFrame: number, toFrame: number, fps: number): LocalDeterministicActiveWindow | null;
|
|
59
|
+
export declare function toLocalDeterministicFrameIndex(component: LocalDeterministicMediaComponent, sceneFrameIndex: number, fps: number): number;
|
|
60
|
+
export declare function resolveLocalPredecodedExtractionPlan(sourceStartSec: number, sourceEndSec: number | undefined, expectedFrameCount: number, fps: number): LocalPredecodedExtractionPlan;
|
|
61
|
+
export declare function prepareLocalDeterministicMedia(input: PrepareLocalDeterministicMediaInput): Promise<PrepareLocalDeterministicMediaResult>;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { SceneShape } from '../schemas/scene/index.js';
|
|
8
|
+
const cloneScene = (scene) => JSON.parse(JSON.stringify(scene));
|
|
9
|
+
const normalizeUrl = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(value);
|
|
12
|
+
parsed.search = '';
|
|
13
|
+
parsed.hash = '';
|
|
14
|
+
return parsed.toString();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const safeFilePart = (value) => value.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
21
|
+
export const normalizeDeterministicPublicBasePath = (value) => {
|
|
22
|
+
const normalized = value.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
23
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
24
|
+
if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) {
|
|
25
|
+
throw new Error('publicBasePath must be a relative URL path without "." or ".." segments.');
|
|
26
|
+
}
|
|
27
|
+
return `/${segments.join('/')}`;
|
|
28
|
+
};
|
|
29
|
+
const resolveSafeAssetRoot = (workDir, publicBasePath) => {
|
|
30
|
+
const root = path.resolve(workDir);
|
|
31
|
+
const target = path.resolve(root, publicBasePath.slice(1));
|
|
32
|
+
const relative = path.relative(root, target);
|
|
33
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
34
|
+
throw new Error('publicBasePath must resolve inside workDir.');
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
};
|
|
38
|
+
const toInputPath = (sourceUrl) => {
|
|
39
|
+
if (sourceUrl.startsWith('file://'))
|
|
40
|
+
return fileURLToPath(sourceUrl);
|
|
41
|
+
return sourceUrl;
|
|
42
|
+
};
|
|
43
|
+
const sourceForComponent = (scene, component) => {
|
|
44
|
+
if (!('source' in component))
|
|
45
|
+
return undefined;
|
|
46
|
+
const directUrl = component.source?.url ?? component.source?.streamUrl;
|
|
47
|
+
if (directUrl)
|
|
48
|
+
return directUrl;
|
|
49
|
+
const assetId = component.source?.assetId;
|
|
50
|
+
return assetId ? scene.assets.find((asset) => asset.id === assetId)?.url : undefined;
|
|
51
|
+
};
|
|
52
|
+
export function collectLocalDeterministicMediaComponents(input) {
|
|
53
|
+
const scene = SceneShape.parse(input);
|
|
54
|
+
const media = [];
|
|
55
|
+
for (const layer of scene.layers ?? []) {
|
|
56
|
+
if (!layer || layer.muted)
|
|
57
|
+
continue;
|
|
58
|
+
for (const component of layer.components ?? []) {
|
|
59
|
+
if (!component || component.visible === false)
|
|
60
|
+
continue;
|
|
61
|
+
if (component.type !== 'VIDEO' && component.type !== 'GIF')
|
|
62
|
+
continue;
|
|
63
|
+
const sourceUrl = sourceForComponent(scene, component);
|
|
64
|
+
if (!sourceUrl)
|
|
65
|
+
continue;
|
|
66
|
+
media.push({
|
|
67
|
+
component,
|
|
68
|
+
id: component.id || crypto.randomUUID(),
|
|
69
|
+
type: component.type,
|
|
70
|
+
sourceUrl,
|
|
71
|
+
sourceStartAt: 'source' in component ? (component.source?.startAt ?? 0) : 0,
|
|
72
|
+
sourceEndAt: 'source' in component && typeof component.source?.endAt === 'number'
|
|
73
|
+
? component.source.endAt
|
|
74
|
+
: undefined,
|
|
75
|
+
timelineStartAt: component.timeline?.startAt ?? 0,
|
|
76
|
+
timelineEndAt: component.timeline?.endAt ?? scene.settings.duration
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return media;
|
|
81
|
+
}
|
|
82
|
+
export function resolveLocalDeterministicActiveWindow(component, fromFrame, toFrame, fps) {
|
|
83
|
+
const frameBoundaryEpsilon = 1e-6;
|
|
84
|
+
const chunkStartSec = fromFrame / fps;
|
|
85
|
+
const chunkEndSec = toFrame / fps;
|
|
86
|
+
const activeStartSec = Math.max(chunkStartSec, component.timelineStartAt);
|
|
87
|
+
const activeEndSec = Math.min(chunkEndSec, component.timelineEndAt);
|
|
88
|
+
if (activeEndSec <= activeStartSec)
|
|
89
|
+
return null;
|
|
90
|
+
const activeStartFrame = Math.max(fromFrame, Math.ceil(activeStartSec * fps - frameBoundaryEpsilon));
|
|
91
|
+
const activeEndFrame = Math.min(toFrame, Math.ceil(activeEndSec * fps - frameBoundaryEpsilon));
|
|
92
|
+
if (activeEndFrame <= activeStartFrame)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
activeStartSec,
|
|
96
|
+
activeEndSec,
|
|
97
|
+
activeStartFrame,
|
|
98
|
+
activeEndFrame,
|
|
99
|
+
sourceStartSec: component.sourceStartAt + (activeStartFrame / fps - component.timelineStartAt)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function toLocalDeterministicFrameIndex(component, sceneFrameIndex, fps) {
|
|
103
|
+
const timelineStartInFrames = component.timelineStartAt * fps;
|
|
104
|
+
const sourceStartInFrames = component.sourceStartAt * fps;
|
|
105
|
+
return Math.max(0, Math.round(sourceStartInFrames + sceneFrameIndex - timelineStartInFrames));
|
|
106
|
+
}
|
|
107
|
+
export function resolveLocalPredecodedExtractionPlan(sourceStartSec, sourceEndSec, expectedFrameCount, fps) {
|
|
108
|
+
const outputFrameCount = Math.max(0, Math.floor(expectedFrameCount));
|
|
109
|
+
if (outputFrameCount <= 0) {
|
|
110
|
+
return { sourceStartSec, extractFrameCount: 0, outputFrameCount: 0, sourceEndLimited: false };
|
|
111
|
+
}
|
|
112
|
+
if (typeof sourceEndSec !== 'number' ||
|
|
113
|
+
!Number.isFinite(sourceEndSec) ||
|
|
114
|
+
sourceEndSec <= sourceStartSec) {
|
|
115
|
+
if (typeof sourceEndSec === 'number' && sourceEndSec <= sourceStartSec) {
|
|
116
|
+
return {
|
|
117
|
+
sourceStartSec: Math.max(0, sourceEndSec - 1 / fps),
|
|
118
|
+
extractFrameCount: 1,
|
|
119
|
+
outputFrameCount,
|
|
120
|
+
sourceEndLimited: true
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
sourceStartSec,
|
|
125
|
+
extractFrameCount: outputFrameCount,
|
|
126
|
+
outputFrameCount,
|
|
127
|
+
sourceEndLimited: false
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const availableFrames = Math.max(1, Math.ceil((sourceEndSec - sourceStartSec) * fps - 1e-6));
|
|
131
|
+
return {
|
|
132
|
+
sourceStartSec,
|
|
133
|
+
extractFrameCount: Math.min(outputFrameCount, availableFrames),
|
|
134
|
+
outputFrameCount,
|
|
135
|
+
sourceEndLimited: availableFrames < outputFrameCount
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const runFfmpeg = async (args, label) => {
|
|
139
|
+
const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
const child = spawn(ffmpegPath, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
142
|
+
let stderr = '';
|
|
143
|
+
child.stderr?.on('data', (chunk) => {
|
|
144
|
+
stderr = (stderr + String(chunk)).slice(-4000);
|
|
145
|
+
});
|
|
146
|
+
child.on('error', reject);
|
|
147
|
+
child.on('close', (code) => {
|
|
148
|
+
if (code === 0) {
|
|
149
|
+
resolve();
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
reject(new Error(`[visualfries deterministic media][${label}] ffmpeg exited ${code}. ${stderr}`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
const listFrameFiles = async (dir, extension) => {
|
|
158
|
+
const files = (await fs.readdir(dir))
|
|
159
|
+
.filter((file) => file.toLowerCase().endsWith(`.${extension}`))
|
|
160
|
+
.sort((a, b) => Number(path.basename(a, `.${extension}`)) - Number(path.basename(b, `.${extension}`)))
|
|
161
|
+
.map((file) => path.join(dir, file));
|
|
162
|
+
return files;
|
|
163
|
+
};
|
|
164
|
+
const extractFrameSequence = async (options) => {
|
|
165
|
+
await fs.mkdir(options.outputDir, { recursive: true });
|
|
166
|
+
const outputPattern = path.join(options.outputDir, `%d.${options.extension}`);
|
|
167
|
+
const seekMode = options.seekMode ?? 'fast';
|
|
168
|
+
const paddedDurationSec = options.expectedFrameCount / options.fps + 0.5 / options.fps;
|
|
169
|
+
const args = ['-y'];
|
|
170
|
+
if (seekMode === 'fast') {
|
|
171
|
+
args.push('-ss', options.sourceStartSec.toFixed(6));
|
|
172
|
+
}
|
|
173
|
+
args.push('-i', options.inputPath);
|
|
174
|
+
if (seekMode === 'accurate') {
|
|
175
|
+
args.push('-ss', options.sourceStartSec.toFixed(6));
|
|
176
|
+
}
|
|
177
|
+
args.push('-t', paddedDurationSec.toFixed(6), '-vf', `fps=${options.fps}:round=near:start_time=0`, '-frames:v', String(options.expectedFrameCount), '-vsync', 'cfr', '-start_number', '1');
|
|
178
|
+
if (options.extension === 'jpg') {
|
|
179
|
+
args.push('-q:v', String(options.jpegQualityScale), '-pix_fmt', 'yuvj420p');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
args.push('-compression_level', '3');
|
|
183
|
+
}
|
|
184
|
+
args.push(outputPattern);
|
|
185
|
+
await runFfmpeg(args, options.label ?? 'extract-frame-sequence');
|
|
186
|
+
return listFrameFiles(options.outputDir, options.extension);
|
|
187
|
+
};
|
|
188
|
+
const padTrailingFrames = async (outputDir, extension, frames, expectedFrameCount) => {
|
|
189
|
+
if (frames.length === expectedFrameCount ||
|
|
190
|
+
frames.length === 0 ||
|
|
191
|
+
frames.length > expectedFrameCount) {
|
|
192
|
+
return frames;
|
|
193
|
+
}
|
|
194
|
+
const lastFrame = frames[frames.length - 1];
|
|
195
|
+
for (let index = frames.length + 1; index <= expectedFrameCount; index += 1) {
|
|
196
|
+
await fs.copyFile(lastFrame, path.join(outputDir, `${index}.${extension}`));
|
|
197
|
+
}
|
|
198
|
+
return listFrameFiles(outputDir, extension);
|
|
199
|
+
};
|
|
200
|
+
const assignBoundaryFrames = (manifest, componentId, frameIndex, frameUrl, radius = 2) => {
|
|
201
|
+
manifest[componentId] = manifest[componentId] ?? {};
|
|
202
|
+
for (let index = Math.max(0, frameIndex - radius); index <= frameIndex + radius; index += 1) {
|
|
203
|
+
manifest[componentId][String(index)] ??= frameUrl;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
export async function prepareLocalDeterministicMedia(input) {
|
|
207
|
+
const scene = cloneScene(SceneShape.parse(input.scene));
|
|
208
|
+
const fps = scene.settings.fps || 30;
|
|
209
|
+
const publicBasePath = normalizeDeterministicPublicBasePath(input.publicBasePath ?? '/deterministic-media');
|
|
210
|
+
const frameExtension = input.frameExtension ?? 'jpg';
|
|
211
|
+
const jpegQualityScale = Math.max(1, Math.min(31, Math.floor(input.jpegQualityScale ?? 2)));
|
|
212
|
+
const assetsRoot = resolveSafeAssetRoot(input.workDir, publicBasePath);
|
|
213
|
+
const predecodedRoot = path.join(assetsRoot, 'predecoded');
|
|
214
|
+
await fs.mkdir(predecodedRoot, { recursive: true });
|
|
215
|
+
const mediaComponents = collectLocalDeterministicMediaComponents(scene);
|
|
216
|
+
const frameManifest = {};
|
|
217
|
+
const preparedMedia = [];
|
|
218
|
+
for (const media of mediaComponents) {
|
|
219
|
+
const activeWindow = resolveLocalDeterministicActiveWindow(media, input.fromFrame, input.toFrame, fps);
|
|
220
|
+
if (!activeWindow)
|
|
221
|
+
continue;
|
|
222
|
+
const componentDirName = safeFilePart(media.id);
|
|
223
|
+
const componentOutputDir = path.join(predecodedRoot, componentDirName);
|
|
224
|
+
await fs.rm(componentOutputDir, { recursive: true, force: true });
|
|
225
|
+
await fs.mkdir(componentOutputDir, { recursive: true });
|
|
226
|
+
const activeStartFrame = activeWindow.activeStartFrame;
|
|
227
|
+
const expectedFrameCount = activeWindow.activeEndFrame - activeWindow.activeStartFrame;
|
|
228
|
+
if (expectedFrameCount <= 0)
|
|
229
|
+
continue;
|
|
230
|
+
const extractionPlan = resolveLocalPredecodedExtractionPlan(activeWindow.sourceStartSec, media.sourceEndAt, expectedFrameCount, fps);
|
|
231
|
+
if (extractionPlan.extractFrameCount <= 0)
|
|
232
|
+
continue;
|
|
233
|
+
const sourceInput = toInputPath(media.sourceUrl);
|
|
234
|
+
if (!/^https?:\/\//.test(sourceInput) && !existsSync(sourceInput)) {
|
|
235
|
+
throw new Error(`VisualFries deterministic media source does not exist: ${media.sourceUrl}`);
|
|
236
|
+
}
|
|
237
|
+
let frames = await extractFrameSequence({
|
|
238
|
+
inputPath: sourceInput,
|
|
239
|
+
outputDir: componentOutputDir,
|
|
240
|
+
sourceStartSec: extractionPlan.sourceStartSec,
|
|
241
|
+
expectedFrameCount: extractionPlan.extractFrameCount,
|
|
242
|
+
fps,
|
|
243
|
+
extension: frameExtension,
|
|
244
|
+
jpegQualityScale,
|
|
245
|
+
seekMode: 'fast',
|
|
246
|
+
label: `${media.id}:fast`
|
|
247
|
+
});
|
|
248
|
+
if (frames.length < extractionPlan.extractFrameCount) {
|
|
249
|
+
for (const framePath of frames)
|
|
250
|
+
await fs.rm(framePath, { force: true });
|
|
251
|
+
frames = await extractFrameSequence({
|
|
252
|
+
inputPath: sourceInput,
|
|
253
|
+
outputDir: componentOutputDir,
|
|
254
|
+
sourceStartSec: extractionPlan.sourceStartSec,
|
|
255
|
+
expectedFrameCount: extractionPlan.extractFrameCount,
|
|
256
|
+
fps,
|
|
257
|
+
extension: frameExtension,
|
|
258
|
+
jpegQualityScale,
|
|
259
|
+
seekMode: 'accurate',
|
|
260
|
+
label: `${media.id}:accurate-retry`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
frames = await padTrailingFrames(componentOutputDir, frameExtension, frames, expectedFrameCount);
|
|
264
|
+
if (frames.length < expectedFrameCount) {
|
|
265
|
+
throw new Error(`VisualFries deterministic media missing frames for ${media.id}: expected ${expectedFrameCount}, got ${frames.length}`);
|
|
266
|
+
}
|
|
267
|
+
const firstDeterministicFrameIndex = activeWindow
|
|
268
|
+
? toLocalDeterministicFrameIndex(media, activeStartFrame, fps)
|
|
269
|
+
: Math.round(media.sourceStartAt * fps);
|
|
270
|
+
const firstFrameUrl = `${publicBasePath}/predecoded/${componentDirName}/1.${frameExtension}`;
|
|
271
|
+
assignBoundaryFrames(frameManifest, media.id, Math.round(media.sourceStartAt * fps), firstFrameUrl);
|
|
272
|
+
for (let index = 0; index < expectedFrameCount; index += 1) {
|
|
273
|
+
const fileName = `${index + 1}.${frameExtension}`;
|
|
274
|
+
const fullPath = path.join(componentOutputDir, fileName);
|
|
275
|
+
if (!existsSync(fullPath)) {
|
|
276
|
+
throw new Error(`VisualFries deterministic media frame missing: ${fullPath}`);
|
|
277
|
+
}
|
|
278
|
+
frameManifest[media.id] = frameManifest[media.id] ?? {};
|
|
279
|
+
frameManifest[media.id][String(firstDeterministicFrameIndex + index)] =
|
|
280
|
+
`${publicBasePath}/predecoded/${componentDirName}/${fileName}`;
|
|
281
|
+
}
|
|
282
|
+
const lastFrameUrl = `${publicBasePath}/predecoded/${componentDirName}/${expectedFrameCount}.${frameExtension}`;
|
|
283
|
+
assignBoundaryFrames(frameManifest, media.id, firstDeterministicFrameIndex + expectedFrameCount - 1, lastFrameUrl);
|
|
284
|
+
preparedMedia.push({
|
|
285
|
+
componentId: media.id,
|
|
286
|
+
type: media.type,
|
|
287
|
+
sourceUrl: normalizeUrl(media.sourceUrl),
|
|
288
|
+
framesPrepared: expectedFrameCount
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
preparedScene: scene,
|
|
293
|
+
payload: {
|
|
294
|
+
strategy: 'visualfries-provider-predecoded',
|
|
295
|
+
frameManifest,
|
|
296
|
+
diagnosticsEnabled: input.diagnostics ?? false,
|
|
297
|
+
mediaDeterministicStrict: input.strict ?? true
|
|
298
|
+
},
|
|
299
|
+
media: preparedMedia,
|
|
300
|
+
assetsRoot,
|
|
301
|
+
strategyUsed: 'predecoded-image-sequence'
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './audioMixer.js';
|
|
2
|
+
export * from './broll.js';
|
|
3
|
+
export * from './catalog.js';
|
|
4
|
+
export * from './capabilities.js';
|
|
5
|
+
export * from './captionScene.js';
|
|
6
|
+
export * from './cueFiles.js';
|
|
7
|
+
export * from './cuePresets.js';
|
|
8
|
+
export * from './deterministicMedia.js';
|
|
9
|
+
export * from './inspectScene.js';
|
|
10
|
+
export * from './localRender.js';
|
|
11
|
+
export * from './overlays.js';
|
|
12
|
+
export * from './productionPlan.js';
|
|
13
|
+
export * from './runtimeSupport.js';
|
|
14
|
+
export * from './renderPlan.js';
|
|
15
|
+
export * from './renderRanges.js';
|
|
16
|
+
export * from './streamingEncoder.js';
|
|
17
|
+
export * from './transcripts.js';
|
|
18
|
+
export * from './transitions.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './audioMixer.js';
|
|
2
|
+
export * from './broll.js';
|
|
3
|
+
export * from './catalog.js';
|
|
4
|
+
export * from './capabilities.js';
|
|
5
|
+
export * from './captionScene.js';
|
|
6
|
+
export * from './cueFiles.js';
|
|
7
|
+
export * from './cuePresets.js';
|
|
8
|
+
export * from './deterministicMedia.js';
|
|
9
|
+
export * from './inspectScene.js';
|
|
10
|
+
export * from './localRender.js';
|
|
11
|
+
export * from './overlays.js';
|
|
12
|
+
export * from './productionPlan.js';
|
|
13
|
+
export * from './runtimeSupport.js';
|
|
14
|
+
export * from './renderPlan.js';
|
|
15
|
+
export * from './renderRanges.js';
|
|
16
|
+
export * from './streamingEncoder.js';
|
|
17
|
+
export * from './transcripts.js';
|
|
18
|
+
export * from './transitions.js';
|