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.
- package/README.md +122 -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.d.ts +3 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
- 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/factories/SceneBuilderFactory.d.ts +3 -0
- package/dist/factories/SceneBuilderFactory.js +6 -0
- 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.d.ts +12 -0
- package/dist/managers/AppManager.svelte.js +180 -8
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.d.ts +6 -0
- package/dist/managers/DeterministicMediaManager.js +57 -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.d.ts +3 -0
- 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,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('<', '<')
|
|
146
|
+
.replaceAll('>', '>')
|
|
147
|
+
.replaceAll('"', '"');
|
|
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;
|