visualfries 0.1.10115 → 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 (139) hide show
  1. package/README.md +122 -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.d.ts +3 -0
  83. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
  84. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  85. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  86. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  87. package/dist/components/hooks/SubtitlesHook.js +48 -24
  88. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  89. package/dist/directors/ComponentDirector.d.ts +1 -0
  90. package/dist/directors/ComponentDirector.js +41 -8
  91. package/dist/factories/SceneBuilderFactory.d.ts +3 -0
  92. package/dist/factories/SceneBuilderFactory.js +6 -0
  93. package/dist/fonts/fontDiscovery.js +12 -1
  94. package/dist/layers/Layer.svelte.d.ts +54 -0
  95. package/dist/layers/Layer.svelte.js +2 -25
  96. package/dist/managers/AppManager.svelte.d.ts +12 -0
  97. package/dist/managers/AppManager.svelte.js +180 -8
  98. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  99. package/dist/managers/ComponentsManager.svelte.js +29 -15
  100. package/dist/managers/DeterministicMediaManager.d.ts +6 -0
  101. package/dist/managers/DeterministicMediaManager.js +57 -3
  102. package/dist/managers/EventManager.js +3 -1
  103. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  104. package/dist/managers/RenderManager.d.ts +5 -0
  105. package/dist/managers/RenderManager.js +67 -21
  106. package/dist/managers/StateManager.svelte.d.ts +56 -0
  107. package/dist/managers/StateManager.svelte.js +60 -58
  108. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  109. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  110. package/dist/managers/TimeManager.svelte.js +23 -7
  111. package/dist/schemas/runtime/deterministic.d.ts +3 -0
  112. package/dist/schemas/runtime/deterministic.js +5 -1
  113. package/dist/schemas/runtime/types.d.ts +36 -15
  114. package/dist/schemas/scene/components.d.ts +126 -0
  115. package/dist/schemas/scene/components.js +11 -0
  116. package/dist/schemas/scene/core.d.ts +109 -10
  117. package/dist/schemas/scene/core.js +1 -1
  118. package/dist/utils/mediaWindow.d.ts +13 -0
  119. package/dist/utils/mediaWindow.js +25 -0
  120. package/dist/utils/svgGenerator.js +1 -1
  121. package/dist/utils/timelineWindow.d.ts +2 -0
  122. package/dist/utils/timelineWindow.js +7 -0
  123. package/dist/utils/utils.js +2 -4
  124. package/docs/AGENT_PATTERNS.md +243 -0
  125. package/docs/AGENT_WORKFLOW.md +364 -0
  126. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  127. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  128. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  129. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  130. package/docs/NEXT_RELEASE.md +28 -0
  131. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  132. package/docs/examples/agent-cues.json +49 -0
  133. package/docs/examples/agent-transcript.json +23 -0
  134. package/docs/examples/agent-transcript.srt +7 -0
  135. package/docs/examples/agent-transcript.vtt +7 -0
  136. package/docs/examples/image-animation.json +87 -0
  137. package/docs/examples/native-text-metric-badge.json +109 -0
  138. package/package.json +133 -100
  139. package/skills/visualfries/SKILL.md +328 -0
@@ -0,0 +1,322 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
+ import { SceneAssetShape, SceneShape } from '../schemas/scene/index.js';
7
+ import { createAgentBrollComponent } from './broll.js';
8
+ import { createAgentTextOverlayComponent, resolveAgentOverlayStyle } from './overlays.js';
9
+ import { createAgentTransitionComponent } from './transitions.js';
10
+ const MediaShape = z
11
+ .object({
12
+ id: z.string(),
13
+ url: z.string(),
14
+ type: z.enum(['VIDEO', 'IMAGE', 'GIF']).optional(),
15
+ start: z.number().nonnegative(),
16
+ end: z.number().positive(),
17
+ sourceStart: z.number().nonnegative().optional(),
18
+ sourceEnd: z.number().nonnegative().optional(),
19
+ freezeAt: z.number().nonnegative().optional(),
20
+ motion: z.enum(['none', 'slow-zoom-in', 'slow-zoom-out', 'drift-up']).optional(),
21
+ fade: z.number().nonnegative().optional(),
22
+ muted: z.boolean().optional(),
23
+ volume: z.number().min(0).max(1).optional(),
24
+ x: z.number().optional(),
25
+ y: z.number().optional(),
26
+ width: z.number().positive().optional(),
27
+ height: z.number().positive().optional(),
28
+ order: z.number().optional()
29
+ })
30
+ .refine((item) => item.end > item.start, { message: 'media end must be after start' });
31
+ const OverlayShape = z
32
+ .object({
33
+ id: z.string().optional(),
34
+ text: z.string().min(1),
35
+ start: z.number().nonnegative(),
36
+ end: z.number().positive(),
37
+ style: z
38
+ .enum([
39
+ 'pop-label',
40
+ 'shock-word',
41
+ 'soft-card',
42
+ 'hook-punch',
43
+ 'proof-pill',
44
+ 'danger-crossout',
45
+ 'metric-badge',
46
+ 'verdict-slam',
47
+ 'receipt-metric',
48
+ 'micro-proof',
49
+ 'cta-card'
50
+ ])
51
+ .optional(),
52
+ x: z.number().optional(),
53
+ y: z.number().optional(),
54
+ width: z.number().positive().optional(),
55
+ height: z.number().positive().optional(),
56
+ color: z.string().optional(),
57
+ backgroundColor: z.string().optional(),
58
+ fontSize: z.number().positive().optional(),
59
+ fontFamily: z.string().optional(),
60
+ fontWeight: z.enum(['700', '800', '900']).optional(),
61
+ textTransform: z.enum(['none', 'uppercase']).optional(),
62
+ rotation: z.number().optional(),
63
+ outlineColor: z.string().optional(),
64
+ outlineSize: z.number().nonnegative().optional(),
65
+ animated: z.boolean().optional(),
66
+ // Visible typography stays semantic/editable by default. SVG is an explicit
67
+ // fallback for treatments the native TEXT renderer cannot reproduce.
68
+ renderAs: z.enum(['TEXT', 'SVG']).default('TEXT')
69
+ })
70
+ .refine((item) => item.end > item.start, { message: 'overlay end must be after start' });
71
+ const BeatShape = z
72
+ .object({
73
+ id: z.string(),
74
+ name: z.string().optional(),
75
+ start: z.number().nonnegative(),
76
+ end: z.number().positive(),
77
+ media: z.array(MediaShape).default([]),
78
+ overlays: z.array(OverlayShape).default([])
79
+ })
80
+ .refine((beat) => beat.end > beat.start, { message: 'beat end must be after start' });
81
+ export const ProductionPlanShape = z.object({
82
+ version: z.literal(1),
83
+ id: z.string(),
84
+ name: z.string().optional(),
85
+ settings: z.object({
86
+ width: z.number().positive().default(1080),
87
+ height: z.number().positive().default(1920),
88
+ duration: z.number().positive(),
89
+ fps: z.number().int().positive().default(30),
90
+ backgroundColor: z.string().default('#000000')
91
+ }),
92
+ beats: z.array(BeatShape).min(1),
93
+ audio: z
94
+ .array(z.object({
95
+ id: z.string(),
96
+ name: z.string().optional(),
97
+ url: z.string(),
98
+ startAt: z.number().nonnegative(),
99
+ endAt: z.number().nonnegative().optional(),
100
+ volume: z.number().min(0).max(1).default(1),
101
+ muted: z.boolean().default(false)
102
+ }))
103
+ .default([]),
104
+ transitions: z
105
+ .array(z.object({
106
+ id: z.string().optional(),
107
+ time: z.number().nonnegative(),
108
+ duration: z.number().positive().optional(),
109
+ style: z
110
+ .enum(['dip-to-black', 'flash', 'swipe-left', 'swipe-up', 'focus-pull'])
111
+ .default('dip-to-black'),
112
+ color: z.string().optional(),
113
+ animated: z.boolean().optional()
114
+ }))
115
+ .default([]),
116
+ qa: z
117
+ .object({
118
+ framesAt: z.array(z.number().nonnegative()).default([]),
119
+ maxLeadingSilence: z.number().nonnegative().optional(),
120
+ requiredText: z.array(z.string()).default([])
121
+ })
122
+ .default({ framesAt: [], requiredText: [] })
123
+ });
124
+ const localUrl = (value, baseDir) => {
125
+ if (/^[a-z]+:\/\//i.test(value) || value.startsWith('data:'))
126
+ return value;
127
+ return pathToFileURL(path.resolve(baseDir, value)).toString();
128
+ };
129
+ const extractFrame = async (input, at, output) => {
130
+ await fs.mkdir(path.dirname(output), { recursive: true });
131
+ await new Promise((resolve, reject) => {
132
+ const child = spawn(process.env.FFMPEG_PATH || 'ffmpeg', ['-y', '-ss', at.toFixed(3), '-i', input, '-frames:v', '1', '-q:v', '2', output], { stdio: ['ignore', 'ignore', 'pipe'] });
133
+ let stderr = '';
134
+ child.stderr.on('data', (chunk) => {
135
+ stderr = (stderr + String(chunk)).slice(-3000);
136
+ });
137
+ child.on('error', reject);
138
+ child.on('close', (code) => code === 0
139
+ ? resolve()
140
+ : reject(new Error(`freeze-frame extraction failed (${code}): ${stderr}`)));
141
+ });
142
+ };
143
+ const escapeXml = (value) => value
144
+ .replaceAll('&', '&')
145
+ .replaceAll('<', '&lt;')
146
+ .replaceAll('>', '&gt;')
147
+ .replaceAll('"', '&quot;');
148
+ const wrapText = (text, maxCharacters) => {
149
+ const lines = [];
150
+ let line = '';
151
+ for (const word of text.split(/\s+/).filter(Boolean)) {
152
+ const candidate = line ? `${line} ${word}` : word;
153
+ if (line && candidate.length > maxCharacters) {
154
+ lines.push(line);
155
+ line = word;
156
+ }
157
+ else {
158
+ line = candidate;
159
+ }
160
+ }
161
+ if (line)
162
+ lines.push(line);
163
+ return lines.length ? lines : [''];
164
+ };
165
+ const writeOverlaySvg = async (input) => {
166
+ const defaults = resolveAgentOverlayStyle(input.overlay.style ?? 'pop-label', input.scene);
167
+ const x = input.overlay.x ?? defaults.x;
168
+ const y = input.overlay.y ?? defaults.y;
169
+ const width = input.overlay.width ?? defaults.width;
170
+ const height = input.overlay.height ?? defaults.height;
171
+ const fontSize = input.overlay.fontSize ?? defaults.fontSize;
172
+ const displayText = (input.overlay.textTransform ?? defaults.textTransform) === 'uppercase'
173
+ ? input.overlay.text.toUpperCase()
174
+ : input.overlay.text;
175
+ const lines = wrapText(displayText, Math.max(6, Math.floor(width / (fontSize * 0.58))));
176
+ const lineHeight = fontSize * 1.02;
177
+ const firstY = y + height / 2 - ((lines.length - 1) * lineHeight) / 2;
178
+ const outline = input.overlay.outlineColor ?? defaults.outlineColor ?? 'transparent';
179
+ const outlineSize = input.overlay.outlineSize ?? defaults.outlineSize ?? 0;
180
+ const fontFamily = input.overlay.fontFamily ?? defaults.fontFamily;
181
+ const fontStack = fontFamily.includes(',')
182
+ ? fontFamily
183
+ : `${fontFamily}, Arial, Helvetica, sans-serif`;
184
+ const text = lines
185
+ .map((line, index) => `<text x="${x + width / 2}" y="${firstY + index * lineHeight}" dominant-baseline="middle" text-anchor="middle" font-family="${escapeXml(fontStack)}" font-size="${fontSize}" font-weight="${input.overlay.fontWeight ?? defaults.fontWeight}" fill="${escapeXml(input.overlay.color ?? defaults.color)}" stroke="${escapeXml(outline)}" stroke-width="${outlineSize}" paint-order="stroke fill">${escapeXml(line)}</text>`)
186
+ .join('');
187
+ const rotation = input.overlay.rotation ?? defaults.rotation;
188
+ const editorialCard = ['metric-badge', 'verdict-slam', 'hook-punch', 'receipt-metric'].includes(input.overlay.style ?? 'pop-label');
189
+ const background = input.overlay.backgroundColor ?? defaults.backgroundColor;
190
+ const transparentBackground = background === 'transparent' || background.endsWith(', 0)');
191
+ const border = editorialCard ? '#FFFFFF' : 'transparent';
192
+ const borderWidth = editorialCard ? Math.max(4, Math.round(width * 0.006)) : 0;
193
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${input.scene.settings.width}" height="${input.scene.settings.height}" viewBox="0 0 ${input.scene.settings.width} ${input.scene.settings.height}"><defs><filter id="shadow" x="-30%" y="-30%" width="160%" height="180%"><feDropShadow dx="10" dy="14" stdDeviation="${Math.max(2, defaults.shadowBlur / 4)}" flood-color="#111C2D" flood-opacity="0.72"/></filter></defs><g${transparentBackground ? '' : ' filter="url(#shadow)"'} transform="rotate(${rotation} ${x + width / 2} ${y + height / 2})"><rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${defaults.radius}" fill="${escapeXml(background)}" stroke="${border}" stroke-width="${borderWidth}"/>${text}</g></svg>`;
194
+ await fs.mkdir(path.dirname(input.output), { recursive: true });
195
+ await fs.writeFile(input.output, `${svg}\n`, 'utf8');
196
+ };
197
+ export async function compileProductionPlan(input) {
198
+ const plan = ProductionPlanShape.parse(input.plan);
199
+ const baseDir = input.planPath ? path.dirname(path.resolve(input.planPath)) : process.cwd();
200
+ const generatedDir = path.resolve(input.generatedAssetsDir ?? path.join(baseDir, '.visualfries-assets'));
201
+ const generatedAssets = [];
202
+ const baseScene = SceneShape.parse({
203
+ id: plan.id,
204
+ name: plan.name ?? plan.id,
205
+ settings: plan.settings,
206
+ assets: [],
207
+ layers: [],
208
+ audioTracks: plan.audio.map((track) => ({ ...track, url: localUrl(track.url, baseDir) })),
209
+ transitions: []
210
+ });
211
+ const assets = [...baseScene.assets];
212
+ const layers = [];
213
+ for (const [beatIndex, beat] of plan.beats.entries()) {
214
+ const mediaComponents = [];
215
+ for (const [mediaIndex, media] of beat.media.entries()) {
216
+ const url = localUrl(media.url, baseDir);
217
+ const sourcePath = url.startsWith('file://') ? fileURLToPath(url) : media.url;
218
+ const freezeTimeline = media.freezeAt === undefined
219
+ ? undefined
220
+ : media.start + (media.freezeAt - (media.sourceStart ?? 0));
221
+ const movingEnd = freezeTimeline === undefined ? media.end : Math.min(media.end, freezeTimeline);
222
+ if (movingEnd > media.start) {
223
+ const created = createAgentBrollComponent({
224
+ ...media,
225
+ id: `${media.id}-motion`,
226
+ url,
227
+ start: media.start,
228
+ end: movingEnd,
229
+ fade: media.fade ?? 0
230
+ }, baseScene, mediaIndex);
231
+ assets.push(SceneAssetShape.parse(created.asset));
232
+ mediaComponents.push(created.component);
233
+ }
234
+ if (freezeTimeline !== undefined && freezeTimeline < media.end) {
235
+ const output = path.join(generatedDir, `${plan.id}-${media.id}-${media.freezeAt.toFixed(3).replace('.', '_')}.jpg`);
236
+ await extractFrame(sourcePath, media.freezeAt, output);
237
+ generatedAssets.push(output);
238
+ const created = createAgentBrollComponent({
239
+ ...media,
240
+ id: `${media.id}-freeze`,
241
+ url: pathToFileURL(output).toString(),
242
+ type: 'IMAGE',
243
+ start: freezeTimeline,
244
+ end: media.end,
245
+ motion: 'none',
246
+ fade: 0
247
+ }, baseScene, mediaIndex + 100);
248
+ assets.push(SceneAssetShape.parse(created.asset));
249
+ mediaComponents.push(created.component);
250
+ }
251
+ }
252
+ if (mediaComponents.length)
253
+ layers.push({
254
+ id: `beat-${beat.id}-media`,
255
+ name: beat.name ?? beat.id,
256
+ order: 10 + beatIndex,
257
+ visible: true,
258
+ muted: false,
259
+ components: mediaComponents
260
+ });
261
+ if (beat.overlays.length) {
262
+ for (const [overlayIndex, overlay] of beat.overlays.entries()) {
263
+ let component;
264
+ if (overlay.renderAs === 'TEXT') {
265
+ component = createAgentTextOverlayComponent(overlay, baseScene, overlayIndex);
266
+ }
267
+ else {
268
+ const id = overlay.id ?? `${beat.id}-overlay-${overlayIndex + 1}`;
269
+ const output = path.join(generatedDir, `${plan.id}-${id}.svg`);
270
+ await writeOverlaySvg({ overlay, scene: baseScene, output });
271
+ generatedAssets.push(output);
272
+ const created = createAgentBrollComponent({
273
+ id,
274
+ url: pathToFileURL(output).toString(),
275
+ type: 'IMAGE',
276
+ start: overlay.start,
277
+ end: overlay.end,
278
+ motion: 'none',
279
+ fade: 0,
280
+ x: 0,
281
+ y: 0,
282
+ width: baseScene.settings.width,
283
+ height: baseScene.settings.height
284
+ }, baseScene, overlayIndex);
285
+ assets.push(SceneAssetShape.parse(created.asset));
286
+ component = created.component;
287
+ }
288
+ layers.push({
289
+ id: `beat-${beat.id}-overlay-${component.id}`,
290
+ name: `${beat.name ?? beat.id}: ${overlay.text}`,
291
+ order: 70 + beatIndex + overlayIndex / 100,
292
+ visible: true,
293
+ muted: false,
294
+ components: [component]
295
+ });
296
+ }
297
+ }
298
+ }
299
+ if (plan.transitions.length)
300
+ layers.push({
301
+ id: 'production-transitions',
302
+ name: 'Production transitions',
303
+ order: 99,
304
+ visible: true,
305
+ muted: false,
306
+ components: plan.transitions.map((transition, index) => createAgentTransitionComponent(transition, baseScene, index))
307
+ });
308
+ const scene = SceneShape.parse({ ...baseScene, assets, layers });
309
+ const texts = plan.beats.flatMap((beat) => beat.overlays.map((overlay) => overlay.text));
310
+ for (const required of plan.qa.requiredText) {
311
+ if (!texts.includes(required))
312
+ throw new Error(`Production plan QA failed: required text "${required}" is missing.`);
313
+ }
314
+ return {
315
+ scene,
316
+ plan,
317
+ generatedAssets,
318
+ qaFrameIndices: [
319
+ ...new Set(plan.qa.framesAt.map((time) => Math.min(Math.floor(plan.settings.duration * plan.settings.fps) - 1, Math.round(time * plan.settings.fps))))
320
+ ].sort((a, b) => a - b)
321
+ };
322
+ }
@@ -0,0 +1,27 @@
1
+ export type AgentRenderMode = 'preview' | 'final';
2
+ export type AgentRenderEngine = 'browser-preview' | 'deterministic-local';
3
+ export type RenderMediaRequirement = {
4
+ componentId?: string;
5
+ layerId?: string;
6
+ assetId?: string;
7
+ type: 'VIDEO' | 'GIF';
8
+ source?: string;
9
+ reason: string;
10
+ };
11
+ export type AgentRenderPlan = {
12
+ mode: AgentRenderMode;
13
+ engine: AgentRenderEngine;
14
+ requiresDeterministicMedia: boolean;
15
+ canUseBrowserPreview: boolean;
16
+ warnings: string[];
17
+ blockers: string[];
18
+ media: RenderMediaRequirement[];
19
+ };
20
+ export type ResolveAgentRenderPlanOptions = {
21
+ mode?: AgentRenderMode;
22
+ engine?: AgentRenderEngine | 'auto';
23
+ allowBrowserMediaFinal?: boolean;
24
+ };
25
+ export declare function collectDeterministicMediaRequirements(input: unknown): RenderMediaRequirement[];
26
+ export declare function requiresDeterministicRender(input: unknown): boolean;
27
+ export declare function resolveAgentRenderPlan(input: unknown, options?: ResolveAgentRenderPlanOptions): AgentRenderPlan;
@@ -0,0 +1,68 @@
1
+ import { SceneShape } from '../schemas/scene/index.js';
2
+ function sourceForComponent(scene, component) {
3
+ if (!('source' in component))
4
+ return undefined;
5
+ const directUrl = component.source?.url ?? component.source?.streamUrl;
6
+ if (directUrl)
7
+ return directUrl;
8
+ const assetId = component.source?.assetId;
9
+ return assetId ? scene.assets.find((asset) => asset.id === assetId)?.url : undefined;
10
+ }
11
+ export function collectDeterministicMediaRequirements(input) {
12
+ const scene = SceneShape.parse(input);
13
+ const media = [];
14
+ for (const layer of scene.layers ?? []) {
15
+ if (!layer || layer.muted || layer.visible === false)
16
+ continue;
17
+ for (const component of layer.components ?? []) {
18
+ if (!component || component.visible === false)
19
+ continue;
20
+ if (component.type !== 'VIDEO' && component.type !== 'GIF')
21
+ continue;
22
+ media.push({
23
+ componentId: component.id,
24
+ layerId: layer.id,
25
+ assetId: 'source' in component ? component.source?.assetId : undefined,
26
+ type: component.type,
27
+ source: sourceForComponent(scene, component),
28
+ reason: component.type === 'VIDEO'
29
+ ? 'VIDEO frames must be predecoded for final deterministic output.'
30
+ : 'GIF frames must be expanded to exact frame images for final deterministic output.'
31
+ });
32
+ }
33
+ }
34
+ return media;
35
+ }
36
+ export function requiresDeterministicRender(input) {
37
+ return collectDeterministicMediaRequirements(input).length > 0;
38
+ }
39
+ export function resolveAgentRenderPlan(input, options = {}) {
40
+ const media = collectDeterministicMediaRequirements(input);
41
+ const mode = options.mode ?? 'final';
42
+ const requiresDeterministicMedia = media.length > 0;
43
+ const engine = options.engine && options.engine !== 'auto'
44
+ ? options.engine
45
+ : mode === 'preview' || !requiresDeterministicMedia
46
+ ? 'browser-preview'
47
+ : 'deterministic-local';
48
+ const warnings = [];
49
+ const blockers = [];
50
+ if (engine === 'browser-preview' && requiresDeterministicMedia) {
51
+ const message = 'Browser media rendering is preview-only for scenes with VIDEO/GIF components because native media seeking can produce stale or duplicated frames.';
52
+ if (mode === 'final' && !options.allowBrowserMediaFinal) {
53
+ blockers.push(message);
54
+ }
55
+ else {
56
+ warnings.push(message);
57
+ }
58
+ }
59
+ return {
60
+ mode,
61
+ engine,
62
+ requiresDeterministicMedia,
63
+ canUseBrowserPreview: engine === 'browser-preview' && (mode === 'preview' || !requiresDeterministicMedia),
64
+ warnings,
65
+ blockers,
66
+ media
67
+ };
68
+ }
@@ -0,0 +1,24 @@
1
+ import { type Scene } from '../schemas/scene/index.js';
2
+ export type TimeRange = {
3
+ start: number;
4
+ end: number;
5
+ };
6
+ export type EffectiveRenderRange = {
7
+ fromFrame: number;
8
+ toFrame: number;
9
+ startSec: number;
10
+ endSec: number;
11
+ };
12
+ export type EffectiveRenderRangesResult = {
13
+ ranges: EffectiveRenderRange[];
14
+ trimAppliedInRanges: boolean;
15
+ };
16
+ export declare const normalizeAndFilterRanges: (ranges: TimeRange[], boundary: TimeRange) => TimeRange[];
17
+ export declare const mergeOverlappingRanges: (ranges: TimeRange[]) => TimeRange[];
18
+ export declare const invertRemovedRanges: (removedRanges: TimeRange[], boundary: TimeRange) => TimeRange[];
19
+ export declare function resolveEffectiveRenderRanges(input: {
20
+ scene: Scene | unknown;
21
+ fromFrame: number;
22
+ toFrame: number;
23
+ fps?: number;
24
+ }): EffectiveRenderRangesResult;
@@ -0,0 +1,73 @@
1
+ import { SceneShape } from '../schemas/scene/index.js';
2
+ export const normalizeAndFilterRanges = (ranges, boundary) => ranges
3
+ .map((range) => ({
4
+ start: Math.max(boundary.start, range.start),
5
+ end: Math.min(boundary.end, range.end)
6
+ }))
7
+ .filter((range) => range.start < range.end);
8
+ export const mergeOverlappingRanges = (ranges) => {
9
+ if (ranges.length < 2)
10
+ return [...ranges];
11
+ const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
12
+ return sortedRanges.reduce((acc, range) => {
13
+ const previous = acc[acc.length - 1];
14
+ if (range.start <= previous.end) {
15
+ previous.end = Math.max(previous.end, range.end);
16
+ }
17
+ else {
18
+ acc.push({ ...range });
19
+ }
20
+ return acc;
21
+ }, [{ ...sortedRanges[0] }]);
22
+ };
23
+ export const invertRemovedRanges = (removedRanges, boundary) => {
24
+ const keptRanges = [];
25
+ let cursor = boundary.start;
26
+ for (const range of removedRanges) {
27
+ if (range.start > cursor)
28
+ keptRanges.push({ start: cursor, end: range.start });
29
+ cursor = Math.max(cursor, range.end);
30
+ }
31
+ if (cursor < boundary.end)
32
+ keptRanges.push({ start: cursor, end: boundary.end });
33
+ return keptRanges;
34
+ };
35
+ export function resolveEffectiveRenderRanges(input) {
36
+ const scene = SceneShape.parse(input.scene);
37
+ const fps = Math.max(1, Number(input.fps || scene.settings.fps || 30));
38
+ const boundary = {
39
+ start: input.fromFrame / fps,
40
+ end: input.toFrame / fps
41
+ };
42
+ const fallbackRange = {
43
+ fromFrame: input.fromFrame,
44
+ toFrame: input.toFrame,
45
+ startSec: boundary.start,
46
+ endSec: boundary.end
47
+ };
48
+ const trimZones = scene.settings.trimZones || [];
49
+ if (trimZones.length === 0 || boundary.end <= boundary.start) {
50
+ return { ranges: [fallbackRange], trimAppliedInRanges: false };
51
+ }
52
+ const filteredRanges = normalizeAndFilterRanges(trimZones, boundary);
53
+ if (filteredRanges.length === 0) {
54
+ return { ranges: [fallbackRange], trimAppliedInRanges: false };
55
+ }
56
+ const removedRanges = mergeOverlappingRanges(filteredRanges);
57
+ const keptRanges = invertRemovedRanges(removedRanges, boundary);
58
+ if (keptRanges.length === 0) {
59
+ return { ranges: [], trimAppliedInRanges: true };
60
+ }
61
+ const ranges = keptRanges
62
+ .map((range) => ({
63
+ fromFrame: Math.max(input.fromFrame, Math.floor(range.start * fps + 1e-9)),
64
+ toFrame: Math.min(input.toFrame, Math.ceil(range.end * fps - 1e-9)),
65
+ startSec: range.start,
66
+ endSec: range.end
67
+ }))
68
+ .filter((range) => range.toFrame > range.fromFrame);
69
+ if (ranges.length === 0) {
70
+ return { ranges: [], trimAppliedInRanges: true };
71
+ }
72
+ return { ranges, trimAppliedInRanges: true };
73
+ }
@@ -0,0 +1,19 @@
1
+ import type { Scene } from '../schemas/scene/index.js';
2
+ export type RuntimeSupportMode = 'warn' | 'strict';
3
+ export type RuntimeSupportIssue = {
4
+ level: 'warning' | 'error';
5
+ type: 'runtime-support';
6
+ code: 'runtime-renderer-missing' | 'runtime-animation-unsupported' | 'runtime-animation-selector-unsupported' | 'runtime-animation-property-unsupported' | 'runtime-animation-reference-unresolved' | 'runtime-effect-unsupported' | 'runtime-text-highlight-conflict' | 'runtime-text-gradient-conflict' | 'runtime-text-highlight-gradient-unsupported';
7
+ capabilityId: string;
8
+ path: string;
9
+ message: string;
10
+ componentId: string;
11
+ layerId: string;
12
+ };
13
+ export type RuntimeSupportReport = {
14
+ supported: boolean;
15
+ issues: RuntimeSupportIssue[];
16
+ };
17
+ export declare function analyzeRuntimeSupport(scene: Scene, options?: {
18
+ mode?: RuntimeSupportMode;
19
+ }): RuntimeSupportReport;