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.
Files changed (133) hide show
  1. package/README.md +97 -0
  2. package/bin/visualfries.js +1570 -0
  3. package/dist/DIContainer.d.ts +2 -1
  4. package/dist/DIContainer.js +16 -5
  5. package/dist/SceneBuilder.svelte.d.ts +70 -1
  6. package/dist/SceneBuilder.svelte.js +96 -29
  7. package/dist/agent/audioMixer.d.ts +34 -0
  8. package/dist/agent/audioMixer.js +308 -0
  9. package/dist/agent/broll.d.ts +35 -0
  10. package/dist/agent/broll.js +226 -0
  11. package/dist/agent/capabilities.d.ts +34 -0
  12. package/dist/agent/capabilities.js +246 -0
  13. package/dist/agent/captionScene.d.ts +23 -0
  14. package/dist/agent/captionScene.js +204 -0
  15. package/dist/agent/catalog.d.ts +24 -0
  16. package/dist/agent/catalog.js +67 -0
  17. package/dist/agent/cueFiles.d.ts +54 -0
  18. package/dist/agent/cueFiles.js +219 -0
  19. package/dist/agent/cuePresets.d.ts +7 -0
  20. package/dist/agent/cuePresets.js +50 -0
  21. package/dist/agent/deterministicMedia.d.ts +61 -0
  22. package/dist/agent/deterministicMedia.js +303 -0
  23. package/dist/agent/index.d.ts +18 -0
  24. package/dist/agent/index.js +18 -0
  25. package/dist/agent/inspectScene.d.ts +36 -0
  26. package/dist/agent/inspectScene.js +139 -0
  27. package/dist/agent/localRender.d.ts +82 -0
  28. package/dist/agent/localRender.js +702 -0
  29. package/dist/agent/overlays.d.ts +40 -0
  30. package/dist/agent/overlays.js +325 -0
  31. package/dist/agent/productionPlan.d.ts +131 -0
  32. package/dist/agent/productionPlan.js +322 -0
  33. package/dist/agent/renderPlan.d.ts +27 -0
  34. package/dist/agent/renderPlan.js +68 -0
  35. package/dist/agent/renderRanges.d.ts +24 -0
  36. package/dist/agent/renderRanges.js +73 -0
  37. package/dist/agent/runtimeSupport.d.ts +19 -0
  38. package/dist/agent/runtimeSupport.js +192 -0
  39. package/dist/agent/streamingEncoder.d.ts +31 -0
  40. package/dist/agent/streamingEncoder.js +208 -0
  41. package/dist/agent/transcripts.d.ts +29 -0
  42. package/dist/agent/transcripts.js +146 -0
  43. package/dist/agent/transitions.d.ts +20 -0
  44. package/dist/agent/transitions.js +169 -0
  45. package/dist/animations/PixiAnimationTarget.d.ts +29 -0
  46. package/dist/animations/PixiAnimationTarget.js +63 -0
  47. package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
  48. package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
  49. package/dist/browser/browserExport.d.ts +73 -0
  50. package/dist/browser/browserExport.js +180 -0
  51. package/dist/browser/glTransitions.d.ts +37 -0
  52. package/dist/browser/glTransitions.js +261 -0
  53. package/dist/browser/index.d.ts +3 -0
  54. package/dist/browser/index.js +3 -0
  55. package/dist/browser/mediaDecode.d.ts +31 -0
  56. package/dist/browser/mediaDecode.js +152 -0
  57. package/dist/builders/PixiComponentBuilder.d.ts +6 -0
  58. package/dist/builders/PixiComponentBuilder.js +12 -0
  59. package/dist/builders/_ComponentState.svelte.d.ts +7 -5
  60. package/dist/builders/_ComponentState.svelte.js +29 -15
  61. package/dist/builders/html/StyleBuilder.js +9 -8
  62. package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
  63. package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
  64. package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
  65. package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
  66. package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
  67. package/dist/commands/SeekCommand.js +19 -6
  68. package/dist/components/AnimatedGIF.d.ts +3 -0
  69. package/dist/components/AnimatedGIF.js +28 -1
  70. package/dist/components/Component.svelte.d.ts +5 -5
  71. package/dist/components/Component.svelte.js +29 -13
  72. package/dist/components/ComponentContext.svelte.js +68 -23
  73. package/dist/components/hooks/AnimationHook.js +1 -1
  74. package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
  75. package/dist/components/hooks/CanvasFillHook.js +75 -0
  76. package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
  77. package/dist/components/hooks/HtmlTextHook.js +11 -3
  78. package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
  79. package/dist/components/hooks/MediaHook.js +172 -47
  80. package/dist/components/hooks/MediaSeekingHook.js +138 -88
  81. package/dist/components/hooks/PixiGifHook.js +4 -4
  82. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +73 -6
  83. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  84. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  85. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  86. package/dist/components/hooks/SubtitlesHook.js +48 -24
  87. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  88. package/dist/directors/ComponentDirector.d.ts +1 -0
  89. package/dist/directors/ComponentDirector.js +41 -8
  90. package/dist/fonts/fontDiscovery.js +12 -1
  91. package/dist/layers/Layer.svelte.d.ts +54 -0
  92. package/dist/layers/Layer.svelte.js +2 -25
  93. package/dist/managers/AppManager.svelte.js +8 -5
  94. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  95. package/dist/managers/ComponentsManager.svelte.js +29 -15
  96. package/dist/managers/DeterministicMediaManager.js +38 -3
  97. package/dist/managers/EventManager.js +3 -1
  98. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  99. package/dist/managers/RenderManager.d.ts +5 -0
  100. package/dist/managers/RenderManager.js +67 -21
  101. package/dist/managers/StateManager.svelte.d.ts +56 -0
  102. package/dist/managers/StateManager.svelte.js +60 -58
  103. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  104. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  105. package/dist/managers/TimeManager.svelte.js +23 -7
  106. package/dist/schemas/runtime/deterministic.js +5 -1
  107. package/dist/schemas/runtime/types.d.ts +36 -15
  108. package/dist/schemas/scene/components.d.ts +126 -0
  109. package/dist/schemas/scene/components.js +11 -0
  110. package/dist/schemas/scene/core.d.ts +109 -10
  111. package/dist/schemas/scene/core.js +1 -1
  112. package/dist/utils/mediaWindow.d.ts +13 -0
  113. package/dist/utils/mediaWindow.js +25 -0
  114. package/dist/utils/svgGenerator.js +1 -1
  115. package/dist/utils/timelineWindow.d.ts +2 -0
  116. package/dist/utils/timelineWindow.js +7 -0
  117. package/dist/utils/utils.js +2 -4
  118. package/docs/AGENT_PATTERNS.md +243 -0
  119. package/docs/AGENT_WORKFLOW.md +364 -0
  120. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  121. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  122. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  123. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  124. package/docs/NEXT_RELEASE.md +28 -0
  125. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  126. package/docs/examples/agent-cues.json +49 -0
  127. package/docs/examples/agent-transcript.json +23 -0
  128. package/docs/examples/agent-transcript.srt +7 -0
  129. package/docs/examples/agent-transcript.vtt +7 -0
  130. package/docs/examples/image-animation.json +87 -0
  131. package/docs/examples/native-text-metric-badge.json +109 -0
  132. package/package.json +133 -100
  133. package/skills/visualfries/SKILL.md +328 -0
@@ -0,0 +1,308 @@
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 REMOTE_AUDIO_INPUT_OPTIONS = [
9
+ '-reconnect',
10
+ '1',
11
+ '-reconnect_streamed',
12
+ '1',
13
+ '-reconnect_on_network_error',
14
+ '1',
15
+ '-http_follow_location',
16
+ '1',
17
+ '-multiple_requests',
18
+ '1',
19
+ '-rw_timeout',
20
+ '15000000'
21
+ ];
22
+ const isRemoteUrl = (value) => /^https?:\/\//i.test(value);
23
+ const toFfmpegInput = (value) => value.startsWith('file://') ? fileURLToPath(value) : value;
24
+ const safeVolume = (input) => {
25
+ if (typeof input !== 'number' || Number.isNaN(input))
26
+ return 1;
27
+ return Math.max(0, Math.min(1, input));
28
+ };
29
+ const sourceUrlForComponent = (scene, component) => {
30
+ if (!('source' in component))
31
+ return undefined;
32
+ const directUrl = component.source?.url ?? component.source?.streamUrl;
33
+ if (directUrl)
34
+ return directUrl;
35
+ const assetId = component.source?.assetId;
36
+ return assetId ? scene.assets.find((asset) => asset.id === assetId)?.url : undefined;
37
+ };
38
+ const clampRange = (range, boundary) => {
39
+ const start = Math.max(range.start, boundary.start);
40
+ const end = Math.min(range.end, boundary.end);
41
+ return end > start ? { start, end } : null;
42
+ };
43
+ export function resolveAudioMixRanges(sceneInput, chunkStartSec, chunkEndSec) {
44
+ const scene = SceneShape.parse(sceneInput);
45
+ const fps = Math.max(1, Number(scene.settings.fps || 30));
46
+ const oneFrameSec = 1 / fps;
47
+ const sceneDuration = Math.max(0, Number(scene.settings.duration || 0));
48
+ const boundary = {
49
+ start: Math.max(0, Math.min(chunkStartSec, sceneDuration)),
50
+ end: Math.max(0, Math.min(chunkEndSec, sceneDuration))
51
+ };
52
+ if (boundary.end <= boundary.start)
53
+ return [];
54
+ let ranges = [boundary];
55
+ if (typeof scene.settings.startAt === 'number' && scene.settings.startAt > 0) {
56
+ const startAt = Math.max(0, scene.settings.startAt - oneFrameSec);
57
+ ranges = ranges.flatMap((range) => range.end <= startAt ? [] : [{ start: Math.max(range.start, startAt), end: range.end }]);
58
+ }
59
+ const sceneEndAt = scene.settings.endAt;
60
+ if (typeof sceneEndAt === 'number') {
61
+ ranges = ranges.flatMap((range) => range.start >= sceneEndAt
62
+ ? []
63
+ : [{ start: range.start, end: Math.min(range.end, sceneEndAt) }]);
64
+ }
65
+ // Keep this local-first module conservative: if trim zones exist, split around them.
66
+ for (const trim of scene.settings.trimZones ?? []) {
67
+ ranges = ranges.flatMap((range) => {
68
+ if (trim.end <= range.start || trim.start >= range.end)
69
+ return [range];
70
+ const parts = [];
71
+ if (trim.start > range.start)
72
+ parts.push({ start: range.start, end: trim.start });
73
+ if (trim.end < range.end)
74
+ parts.push({ start: trim.end, end: range.end });
75
+ return parts;
76
+ });
77
+ }
78
+ return ranges.filter((range) => range.end > range.start);
79
+ }
80
+ function collectAudioSourcesForBoundary(scene, boundary, outputOffsetSec) {
81
+ const plans = [];
82
+ for (const layer of scene.layers ?? []) {
83
+ if (layer.muted)
84
+ continue;
85
+ for (const component of layer.components ?? []) {
86
+ if (component.type !== 'VIDEO' && component.type !== 'AUDIO')
87
+ continue;
88
+ const muted = 'muted' in component ? component.muted === true : false;
89
+ const volume = safeVolume('volume' in component ? component.volume : 1);
90
+ if (muted || volume <= 0)
91
+ continue;
92
+ const url = sourceUrlForComponent(scene, component);
93
+ if (!url)
94
+ continue;
95
+ const timelineStart = component.timeline?.startAt ?? 0;
96
+ const timelineEnd = component.timeline?.endAt ?? scene.settings.duration;
97
+ const active = clampRange({ start: timelineStart, end: timelineEnd }, boundary);
98
+ if (!active)
99
+ continue;
100
+ const source = 'source' in component ? component.source : undefined;
101
+ const sourceStart = source?.startAt ?? 0;
102
+ const sourceEnd = source?.endAt;
103
+ const sourceOffsetSec = sourceStart + (active.start - timelineStart);
104
+ let durationSec = active.end - active.start;
105
+ if (typeof sourceEnd === 'number') {
106
+ durationSec = Math.min(durationSec, sourceEnd - sourceOffsetSec);
107
+ }
108
+ if (durationSec <= 0)
109
+ continue;
110
+ plans.push({
111
+ id: component.id || `component-${plans.length}`,
112
+ kind: 'component',
113
+ url,
114
+ startInChunkSec: outputOffsetSec + (active.start - boundary.start),
115
+ durationSec,
116
+ sourceOffsetSec,
117
+ volume
118
+ });
119
+ }
120
+ }
121
+ for (const track of scene.audioTracks ?? []) {
122
+ if (!track || track.muted)
123
+ continue;
124
+ const volume = safeVolume(track.volume);
125
+ if (volume <= 0 || !track.url)
126
+ continue;
127
+ const startAt = track.startAt ?? 0;
128
+ const endAt = typeof track.endAt === 'number' ? track.endAt : scene.settings.duration;
129
+ const active = clampRange({ start: startAt, end: endAt }, boundary);
130
+ if (!active)
131
+ continue;
132
+ plans.push({
133
+ id: track.id || `track-${plans.length}`,
134
+ kind: 'track',
135
+ url: track.url,
136
+ startInChunkSec: outputOffsetSec + (active.start - boundary.start),
137
+ durationSec: active.end - active.start,
138
+ sourceOffsetSec: active.start - startAt,
139
+ volume
140
+ });
141
+ }
142
+ const sceneAudio = scene.settings.audio;
143
+ if (sceneAudio?.src && sceneAudio.muted !== true && safeVolume(sceneAudio.volume) > 0) {
144
+ plans.push({
145
+ id: 'scene-audio',
146
+ kind: 'scene',
147
+ url: sceneAudio.src,
148
+ startInChunkSec: outputOffsetSec,
149
+ durationSec: boundary.end - boundary.start,
150
+ sourceOffsetSec: boundary.start,
151
+ volume: safeVolume(sceneAudio.volume)
152
+ });
153
+ }
154
+ return plans;
155
+ }
156
+ export function collectAudioSourcesForRanges(sceneInput, ranges) {
157
+ const scene = SceneShape.parse(sceneInput);
158
+ const plans = [];
159
+ let outputOffsetSec = 0;
160
+ for (const range of ranges) {
161
+ if (range.end <= range.start)
162
+ continue;
163
+ plans.push(...collectAudioSourcesForBoundary(scene, range, outputOffsetSec));
164
+ outputOffsetSec += range.end - range.start;
165
+ }
166
+ return plans;
167
+ }
168
+ const runProcess = async (command, args, label, options = {}) => {
169
+ return await new Promise((resolve, reject) => {
170
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
171
+ let stdout = '';
172
+ let stderr = '';
173
+ child.stdout?.on('data', (chunk) => {
174
+ stdout += String(chunk);
175
+ });
176
+ child.stderr?.on('data', (chunk) => {
177
+ const text = String(chunk);
178
+ if (options.debug)
179
+ process.stderr.write(`[${label}] ${text}`);
180
+ stderr = (stderr + text).slice(-4000);
181
+ });
182
+ child.on('error', reject);
183
+ child.on('close', (code) => {
184
+ if (code === 0) {
185
+ resolve({ stdout });
186
+ }
187
+ else {
188
+ reject(new Error(`${label} failed with exit code ${code}. ${stderr}`));
189
+ }
190
+ });
191
+ });
192
+ };
193
+ export async function sourceHasAudio(url) {
194
+ const input = toFfmpegInput(url);
195
+ const { stdout } = await runProcess(process.env.FFPROBE_PATH || 'ffprobe', [
196
+ '-v',
197
+ 'error',
198
+ '-select_streams',
199
+ 'a',
200
+ '-show_entries',
201
+ 'stream=index',
202
+ '-of',
203
+ 'csv=p=0',
204
+ input
205
+ ], 'ffprobe-audio');
206
+ return stdout.trim().length > 0;
207
+ }
208
+ export function createPrepareSourceAudioArgs(source, outputPath) {
209
+ const input = toFfmpegInput(source.url);
210
+ return [
211
+ '-y',
212
+ ...(isRemoteUrl(source.url) ? REMOTE_AUDIO_INPUT_OPTIONS : []),
213
+ '-ss',
214
+ source.sourceOffsetSec.toFixed(3),
215
+ '-i',
216
+ input,
217
+ '-t',
218
+ source.durationSec.toFixed(3),
219
+ '-vn',
220
+ '-ac',
221
+ '2',
222
+ '-ar',
223
+ '48000',
224
+ '-acodec',
225
+ 'flac',
226
+ outputPath
227
+ ];
228
+ }
229
+ export function createMixAudioArgs(inputs, outputPath, chunkDurationSec) {
230
+ const args = ['-y'];
231
+ for (const input of inputs) {
232
+ args.push('-i', input.path);
233
+ }
234
+ const filterSegments = [];
235
+ const mixInputs = [];
236
+ for (let index = 0; index < inputs.length; index += 1) {
237
+ const source = inputs[index].plan;
238
+ const delayMs = Math.max(0, Math.round(source.startInChunkSec * 1000));
239
+ const label = `a${index}`;
240
+ filterSegments.push(`[${index}:a]adelay=${delayMs}|${delayMs},volume=${source.volume.toFixed(6)}[${label}]`);
241
+ mixInputs.push(`[${label}]`);
242
+ }
243
+ filterSegments.push(`${mixInputs.join('')}amix=inputs=${mixInputs.length}:duration=longest:normalize=false[mixraw]`);
244
+ filterSegments.push(`[mixraw]atrim=0:${chunkDurationSec.toFixed(3)},asetpts=PTS-STARTPTS[mixtrim]`);
245
+ args.push('-filter_complex', filterSegments.join(';'), '-map', '[mixtrim]', '-ac', '2', '-ar', '48000', '-acodec', 'flac', outputPath);
246
+ return args;
247
+ }
248
+ export async function buildLocalMixedAudioTrack(input) {
249
+ const scene = SceneShape.parse(input.scene);
250
+ const chunkDurationSec = input.ranges.reduce((sum, range) => sum + Math.max(0, range.end - range.start), 0);
251
+ if (chunkDurationSec <= 0) {
252
+ return { audioPath: null, selectedSources: 0, skippedSources: 0, plannedSources: [] };
253
+ }
254
+ const plannedSources = collectAudioSourcesForRanges(scene, input.ranges);
255
+ if (plannedSources.length === 0) {
256
+ return { audioPath: null, selectedSources: 0, skippedSources: 0, plannedSources };
257
+ }
258
+ await fs.mkdir(input.workDir, { recursive: true });
259
+ const preparedInputs = [];
260
+ let skippedSources = 0;
261
+ for (let index = 0; index < plannedSources.length; index += 1) {
262
+ const source = plannedSources[index];
263
+ if (!(await sourceHasAudio(source.url))) {
264
+ skippedSources += 1;
265
+ continue;
266
+ }
267
+ const hash = crypto
268
+ .createHash('sha1')
269
+ .update(JSON.stringify({
270
+ url: source.url,
271
+ offset: source.sourceOffsetSec.toFixed(3),
272
+ duration: source.durationSec.toFixed(3),
273
+ volume: source.volume.toFixed(3)
274
+ }))
275
+ .digest('hex');
276
+ const preparedPath = path.join(input.workDir, `prepared-${index}-${hash}.flac`);
277
+ if (!existsSync(preparedPath)) {
278
+ await runProcess(process.env.FFMPEG_PATH || 'ffmpeg', createPrepareSourceAudioArgs(source, preparedPath), 'visualfries-audio-prepare', { debug: input.debug });
279
+ }
280
+ preparedInputs.push({ plan: source, path: preparedPath });
281
+ }
282
+ if (preparedInputs.length === 0) {
283
+ return { audioPath: null, selectedSources: 0, skippedSources, plannedSources };
284
+ }
285
+ const checksum = crypto
286
+ .createHash('sha1')
287
+ .update(JSON.stringify({
288
+ renderId: input.renderId ?? 'local',
289
+ chunkDurationSec: chunkDurationSec.toFixed(3),
290
+ inputs: preparedInputs.map((entry) => ({
291
+ id: entry.plan.id,
292
+ path: entry.path,
293
+ delay: entry.plan.startInChunkSec.toFixed(3),
294
+ volume: entry.plan.volume.toFixed(3)
295
+ }))
296
+ }))
297
+ .digest('hex');
298
+ const mixedPath = path.join(input.workDir, `mixed-${checksum}.flac`);
299
+ if (!existsSync(mixedPath)) {
300
+ await runProcess(process.env.FFMPEG_PATH || 'ffmpeg', createMixAudioArgs(preparedInputs, mixedPath, chunkDurationSec), 'visualfries-audio-mix', { debug: input.debug });
301
+ }
302
+ return {
303
+ audioPath: mixedPath,
304
+ selectedSources: preparedInputs.length,
305
+ skippedSources,
306
+ plannedSources
307
+ };
308
+ }
@@ -0,0 +1,35 @@
1
+ import { type ComponentInput, type Scene, type SceneAssetInput, type SceneInput } from '../schemas/scene/index.js';
2
+ export type AgentBrollCue = {
3
+ id?: string;
4
+ url: string;
5
+ type?: 'VIDEO' | 'IMAGE' | 'GIF';
6
+ motion?: 'none' | 'slow-zoom-in' | 'slow-zoom-out' | 'drift-up';
7
+ start: number;
8
+ end: number;
9
+ assetId?: string;
10
+ name?: string;
11
+ order?: number;
12
+ muted?: boolean;
13
+ volume?: number;
14
+ fade?: number;
15
+ x?: number;
16
+ y?: number;
17
+ width?: number;
18
+ height?: number;
19
+ sourceStart?: number;
20
+ sourceEnd?: number;
21
+ playbackRate?: number;
22
+ loop?: boolean;
23
+ };
24
+ export type AddAgentBrollSequenceInput = {
25
+ scene: Scene | SceneInput;
26
+ cues: AgentBrollCue[];
27
+ layerId?: string;
28
+ layerName?: string;
29
+ layerOrder?: number;
30
+ };
31
+ export declare function createAgentBrollComponent(cue: AgentBrollCue, scene: Scene | SceneInput, index?: number): {
32
+ asset: SceneAssetInput;
33
+ component: ComponentInput;
34
+ };
35
+ export declare function addAgentBrollSequence(input: AddAgentBrollSequenceInput): Scene;
@@ -0,0 +1,226 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { ComponentShape, SceneShape } from '../schemas/scene/index.js';
4
+ function inferAssetType(cue) {
5
+ if (cue.type)
6
+ return cue.type;
7
+ const cleanUrl = cue.url.split('?')[0]?.toLowerCase() ?? cue.url.toLowerCase();
8
+ if (cleanUrl.endsWith('.gif'))
9
+ return 'GIF';
10
+ if (/\.(png|jpe?g|webp|avif)$/i.test(cleanUrl))
11
+ return 'IMAGE';
12
+ return 'VIDEO';
13
+ }
14
+ function normalizeAgentMediaUrl(url) {
15
+ if (/^[a-z]+:\/\//i.test(url) || url.startsWith('data:'))
16
+ return url;
17
+ return pathToFileURL(path.resolve(url)).toString();
18
+ }
19
+ function fadeAnimation(id, cue) {
20
+ const fade = cue.fade ?? 0.2;
21
+ if (fade <= 0)
22
+ return [];
23
+ return [
24
+ {
25
+ id: `${id}-fade`,
26
+ name: 'Agent b-roll fade',
27
+ animation: {
28
+ id: `${id}-fade-preset`,
29
+ timeline: [
30
+ {
31
+ tweens: [
32
+ {
33
+ method: 'from',
34
+ vars: {
35
+ duration: fade,
36
+ opacity: 0,
37
+ ease: 'power1.out'
38
+ }
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ position: {
44
+ anchor: 'componentEnd',
45
+ alignTween: 'end',
46
+ offset: '0s'
47
+ },
48
+ tweens: [
49
+ {
50
+ method: 'to',
51
+ vars: {
52
+ duration: fade,
53
+ opacity: 0,
54
+ ease: 'power1.in'
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }
61
+ }
62
+ ];
63
+ }
64
+ function motionAnimation(id, cue) {
65
+ const motion = cue.motion ?? (inferAssetType(cue) === 'IMAGE' ? 'slow-zoom-in' : 'none');
66
+ const duration = Math.max(0.1, cue.end - cue.start);
67
+ if (motion === 'none')
68
+ return [];
69
+ if (motion === 'slow-zoom-out') {
70
+ return [
71
+ {
72
+ id: `${id}-motion`,
73
+ name: 'Agent b-roll slow zoom out',
74
+ animation: {
75
+ id: `${id}-motion-preset`,
76
+ timeline: [
77
+ {
78
+ tweens: [
79
+ {
80
+ method: 'fromTo',
81
+ vars: {
82
+ from: { scale: 1.08 },
83
+ duration,
84
+ scale: 1,
85
+ ease: 'none'
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
92
+ }
93
+ ];
94
+ }
95
+ if (motion === 'drift-up') {
96
+ return [
97
+ {
98
+ id: `${id}-motion`,
99
+ name: 'Agent b-roll drift up',
100
+ animation: {
101
+ id: `${id}-motion-preset`,
102
+ timeline: [
103
+ {
104
+ tweens: [
105
+ {
106
+ method: 'fromTo',
107
+ vars: {
108
+ from: { y: cue.y ?? 0 },
109
+ duration,
110
+ y: (cue.y ?? 0) - 36,
111
+ ease: 'none'
112
+ }
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ }
119
+ ];
120
+ }
121
+ return [
122
+ {
123
+ id: `${id}-motion`,
124
+ name: 'Agent b-roll slow zoom in',
125
+ animation: {
126
+ id: `${id}-motion-preset`,
127
+ timeline: [
128
+ {
129
+ tweens: [
130
+ {
131
+ method: 'fromTo',
132
+ vars: {
133
+ from: { scale: 1 },
134
+ duration,
135
+ scale: 1.08,
136
+ ease: 'none'
137
+ }
138
+ }
139
+ ]
140
+ }
141
+ ]
142
+ }
143
+ }
144
+ ];
145
+ }
146
+ export function createAgentBrollComponent(cue, scene, index = 0) {
147
+ const parsedScene = SceneShape.parse(scene);
148
+ const type = inferAssetType(cue);
149
+ const id = cue.id ?? `agent-broll-${index + 1}`;
150
+ const assetId = cue.assetId ?? `${id}-asset`;
151
+ const url = normalizeAgentMediaUrl(cue.url);
152
+ const base = {
153
+ id,
154
+ name: cue.name ?? `Agent B-roll ${index + 1}`,
155
+ type,
156
+ timeline: { startAt: cue.start, endAt: cue.end },
157
+ source: {
158
+ url,
159
+ assetId,
160
+ startAt: cue.sourceStart,
161
+ endAt: cue.sourceEnd
162
+ },
163
+ order: cue.order ?? index,
164
+ appearance: {
165
+ x: cue.x ?? 0,
166
+ y: cue.y ?? 0,
167
+ width: cue.width ?? parsedScene.settings.width,
168
+ height: cue.height ?? parsedScene.settings.height,
169
+ opacity: 1
170
+ },
171
+ animations: {
172
+ enabled: true,
173
+ list: [...fadeAnimation(id, cue), ...motionAnimation(id, cue)]
174
+ }
175
+ };
176
+ const component = type === 'VIDEO'
177
+ ? {
178
+ ...base,
179
+ type: 'VIDEO',
180
+ volume: cue.volume ?? 0,
181
+ muted: cue.muted ?? true,
182
+ playback: {
183
+ autoplay: true,
184
+ loop: cue.loop ?? false,
185
+ playbackRate: cue.playbackRate ?? 1,
186
+ startAt: cue.sourceStart ?? 0,
187
+ endAt: cue.sourceEnd
188
+ },
189
+ crop: { x: 0, y: 0, width: 1, height: 1 }
190
+ }
191
+ : type === 'GIF'
192
+ ? {
193
+ ...base,
194
+ type: 'GIF',
195
+ playback: { loop: true, speed: 1 }
196
+ }
197
+ : {
198
+ ...base,
199
+ type: 'IMAGE',
200
+ crop: { xPercent: 0, yPercent: 0, widthPercent: 1, heightPercent: 1 }
201
+ };
202
+ const asset = {
203
+ id: assetId,
204
+ type,
205
+ url
206
+ };
207
+ return {
208
+ asset,
209
+ component: ComponentShape.parse(component)
210
+ };
211
+ }
212
+ export function addAgentBrollSequence(input) {
213
+ const scene = SceneShape.parse(input.scene);
214
+ const created = input.cues.map((cue, index) => createAgentBrollComponent(cue, scene, index));
215
+ const layer = {
216
+ id: input.layerId ?? 'layer-agent-broll',
217
+ name: input.layerName ?? 'Agent B-roll',
218
+ order: input.layerOrder ?? 5,
219
+ components: created.map((item) => item.component)
220
+ };
221
+ return SceneShape.parse({
222
+ ...scene,
223
+ assets: [...scene.assets, ...created.map((item) => item.asset)],
224
+ layers: [...scene.layers, layer]
225
+ });
226
+ }
@@ -0,0 +1,34 @@
1
+ export declare const VISUAL_COMPONENT_TYPES: readonly ["TEXT", "SUBTITLES", "IMAGE", "VIDEO", "GIF", "SHAPE", "COLOR", "GRADIENT"];
2
+ export declare const COMPONENT_TYPES: readonly ["TEXT", "SUBTITLES", "IMAGE", "VIDEO", "GIF", "SHAPE", "COLOR", "GRADIENT", "AUDIO"];
3
+ export type CapabilityComponentType = (typeof COMPONENT_TYPES)[number];
4
+ export type VisualCapabilityComponentType = (typeof VISUAL_COMPONENT_TYPES)[number];
5
+ export type AnimationTransform = 'x' | 'y' | 'opacity' | 'rotation' | 'scale' | 'scaleX' | 'scaleY';
6
+ export type ComponentCapability = {
7
+ type: CapabilityComponentType;
8
+ visual: boolean;
9
+ renderers: {
10
+ preview: 'html' | 'pixi' | 'native-media' | 'none';
11
+ final: 'pixi' | 'deterministic-media-pixi' | 'audio-mix' | 'none';
12
+ parity: 'same-representation' | 'different-representation' | 'media-predecoded' | 'not-applicable';
13
+ };
14
+ animation: {
15
+ attached: boolean;
16
+ target: 'html-element-or-wrapper' | 'pixi-container' | 'none';
17
+ transforms: AnimationTransform[];
18
+ selectors: Array<'container' | 'words' | 'lines' | 'chars' | 'css'>;
19
+ coordinateSpace: 'css-transform' | 'component-relative-offset' | 'none';
20
+ transformOrigin: 'css-center-fixed' | 'component-center-fixed' | 'none';
21
+ seekDeterministic: boolean;
22
+ };
23
+ effects: {
24
+ supported: string[];
25
+ validationOnly: string[];
26
+ };
27
+ backgrounds: string[];
28
+ textFeatures: string[];
29
+ caveats: string[];
30
+ };
31
+ export declare const COMPONENT_CAPABILITIES: Record<CapabilityComponentType, ComponentCapability>;
32
+ export declare function getComponentCapability(type: string): ComponentCapability | undefined;
33
+ export declare function getCapabilityCatalog(): ComponentCapability[];
34
+ export declare function componentSupportsRuntimeAnimation(type: string): boolean;