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,192 @@
1
+ import { getComponentCapability } from './capabilities.js';
2
+ const TWEEN_CONTROL_KEYS = new Set([
3
+ 'from',
4
+ 'duration',
5
+ 'ease',
6
+ 'delay',
7
+ 'stagger',
8
+ 'repeat',
9
+ 'yoyo',
10
+ 'repeatDelay',
11
+ 'overwrite',
12
+ 'onStart',
13
+ 'onUpdate',
14
+ 'onComplete',
15
+ 'onRepeat',
16
+ 'onReverseComplete'
17
+ ]);
18
+ const KNOWN_TRANSFORMS = new Set([
19
+ 'x',
20
+ 'y',
21
+ 'opacity',
22
+ 'rotation',
23
+ 'scale',
24
+ 'scaleX',
25
+ 'scaleY'
26
+ ]);
27
+ function levelFor(mode) {
28
+ return mode === 'strict' ? 'error' : 'warning';
29
+ }
30
+ function issue(mode, component, layerId, data) {
31
+ return {
32
+ level: levelFor(mode),
33
+ type: 'runtime-support',
34
+ componentId: component.id,
35
+ layerId,
36
+ ...data
37
+ };
38
+ }
39
+ function selectorKind(target) {
40
+ if (typeof target !== 'string' || target === '' || target === 'container')
41
+ return 'container';
42
+ if (target === 'words' || target === 'lines' || target === 'chars')
43
+ return target;
44
+ return 'css';
45
+ }
46
+ function inspectTween(vars, path, capability, component, layerId, mode, issues) {
47
+ if (!vars)
48
+ return;
49
+ const states = [vars, typeof vars.from === 'object' && vars.from ? vars.from : undefined];
50
+ for (const [stateIndex, state] of states.entries()) {
51
+ if (!state)
52
+ continue;
53
+ for (const property of Object.keys(state)) {
54
+ if (TWEEN_CONTROL_KEYS.has(property))
55
+ continue;
56
+ if (!KNOWN_TRANSFORMS.has(property)) {
57
+ issues.push(issue(mode, component, layerId, {
58
+ code: 'runtime-animation-property-unsupported',
59
+ capabilityId: `component.${component.type}.animation.property.${property}`,
60
+ path: `${path}${stateIndex === 1 ? '.from' : ''}.${property}`,
61
+ message: `${component.type} does not guarantee animation property "${property}" at runtime.`
62
+ }));
63
+ continue;
64
+ }
65
+ if (!capability.animation.transforms.includes(property)) {
66
+ issues.push(issue(mode, component, layerId, {
67
+ code: 'runtime-animation-property-unsupported',
68
+ capabilityId: `component.${component.type}.animation.property.${property}`,
69
+ path: `${path}${stateIndex === 1 ? '.from' : ''}.${property}`,
70
+ message: `${component.type} accepts "${property}" in schema but does not evaluate it at runtime.`
71
+ }));
72
+ }
73
+ }
74
+ }
75
+ }
76
+ export function analyzeRuntimeSupport(scene, options = {}) {
77
+ const mode = options.mode ?? 'warn';
78
+ const issues = [];
79
+ for (const [layerIndex, layer] of (scene.layers ?? []).entries()) {
80
+ for (const [componentIndex, component] of (layer.components ?? []).entries()) {
81
+ const componentPath = `layers.${layerIndex}.components.${componentIndex}`;
82
+ const capability = getComponentCapability(component.type);
83
+ if (!capability)
84
+ continue;
85
+ if (capability.visual && capability.renderers.preview === 'none') {
86
+ issues.push(issue(mode, component, layer.id, {
87
+ code: 'runtime-renderer-missing',
88
+ capabilityId: `component.${component.type}.runtime.rendered`,
89
+ path: componentPath,
90
+ message: `${component.type} is accepted by schema but has no component render hook.`
91
+ }));
92
+ }
93
+ const animations = component.animations;
94
+ const enabledAnimations = animations?.enabled === false
95
+ ? []
96
+ : (animations?.list ?? []).filter((entry) => entry != null && entry.enabled !== false);
97
+ if (enabledAnimations.length > 0 && !capability.animation.attached) {
98
+ issues.push(issue(mode, component, layer.id, {
99
+ code: 'runtime-animation-unsupported',
100
+ capabilityId: `component.${component.type}.animation.attachment`,
101
+ path: `${componentPath}.animations`,
102
+ message: `${component.type} animation data validates but is not attached to a runtime animation target.`
103
+ }));
104
+ }
105
+ for (const [animationIndex, entry] of enabledAnimations.entries()) {
106
+ const basePath = `${componentPath}.animations.list.${animationIndex}`;
107
+ const animation = entry.animation;
108
+ if (typeof animation === 'string') {
109
+ // System presets are resolved at construction time. Their concrete timeline is not
110
+ // duplicated here; unresolved names are surfaced by the runtime explain path.
111
+ continue;
112
+ }
113
+ const animationObject = animation;
114
+ const keyframe = animationObject.tween;
115
+ if (keyframe) {
116
+ const target = selectorKind(animationObject.target);
117
+ if (!capability.animation.selectors.includes(target)) {
118
+ issues.push(issue(mode, component, layer.id, {
119
+ code: 'runtime-animation-selector-unsupported',
120
+ capabilityId: `component.${component.type}.animation.selector.${target}`,
121
+ path: `${basePath}.target`,
122
+ message: `${component.type} animation target "${String(animationObject.target)}" collapses to or misses the runtime target.`
123
+ }));
124
+ }
125
+ inspectTween(keyframe.vars, `${basePath}.tween.vars`, capability, component, layer.id, mode, issues);
126
+ }
127
+ for (const [itemIndex, item] of (animationObject.timeline ?? []).entries()) {
128
+ const sequence = item;
129
+ const target = selectorKind(sequence.target);
130
+ if (!capability.animation.selectors.includes(target)) {
131
+ issues.push(issue(mode, component, layer.id, {
132
+ code: 'runtime-animation-selector-unsupported',
133
+ capabilityId: `component.${component.type}.animation.selector.${target}`,
134
+ path: `${basePath}.timeline.${itemIndex}.target`,
135
+ message: `${component.type} does not support animation selector "${String(sequence.target)}".`
136
+ }));
137
+ }
138
+ for (const [tweenIndex, tween] of (sequence.tweens ?? []).entries()) {
139
+ inspectTween(tween.vars, `${basePath}.timeline.${itemIndex}.tweens.${tweenIndex}.vars`, capability, component, layer.id, mode, issues);
140
+ }
141
+ }
142
+ }
143
+ if (component.effects?.enabled !== false) {
144
+ for (const [effectId, effect] of Object.entries(component.effects?.map ?? {})) {
145
+ const effectType = effect?.type;
146
+ if (effectType && !capability.effects.supported.includes(effectType)) {
147
+ issues.push(issue(mode, component, layer.id, {
148
+ code: 'runtime-effect-unsupported',
149
+ capabilityId: `component.${component.type}.effect.${effectType}`,
150
+ path: `${componentPath}.effects.map.${effectId}`,
151
+ message: `${effectType} is accepted for ${component.type} but is not in its runtime-supported effect set.`
152
+ }));
153
+ }
154
+ }
155
+ }
156
+ if (component.type === 'TEXT' || component.type === 'SUBTITLES') {
157
+ const text = component.appearance.text;
158
+ if (text.activeWord?.enabled && text.activeLine?.enabled) {
159
+ issues.push(issue(mode, component, layer.id, {
160
+ code: 'runtime-text-highlight-conflict',
161
+ capabilityId: `component.${component.type}.text.activeHighlight.precedence`,
162
+ path: `${componentPath}.appearance.text`,
163
+ message: 'activeWord and activeLine target the same glyphs; activeWord takes deterministic precedence.'
164
+ }));
165
+ }
166
+ if (text.highlightColors?.some((color) => typeof color !== 'string')) {
167
+ issues.push(issue(mode, component, layer.id, {
168
+ code: 'runtime-text-highlight-gradient-unsupported',
169
+ capabilityId: `component.${component.type}.text.highlightColors.gradient`,
170
+ path: `${componentPath}.appearance.text.highlightColors`,
171
+ message: 'highlightColors currently cycles solid CSS colors only; gradient palette entries are not evaluated.'
172
+ }));
173
+ }
174
+ const background = component.appearance.background;
175
+ if (background &&
176
+ typeof background === 'object' &&
177
+ 'color' in background &&
178
+ background.target === 'element' &&
179
+ typeof background.color === 'object' &&
180
+ typeof text.color === 'object') {
181
+ issues.push(issue(mode, component, layer.id, {
182
+ code: 'runtime-text-gradient-conflict',
183
+ capabilityId: `component.${component.type}.text.compositing.elementGradientBackgroundWithGradientGlyph`,
184
+ path: `${componentPath}.appearance`,
185
+ message: 'Element-target gradient background and gradient glyph fill compete for CSS background-image. Use wrapper target.'
186
+ }));
187
+ }
188
+ }
189
+ }
190
+ }
191
+ return { supported: issues.length === 0, issues };
192
+ }
@@ -0,0 +1,31 @@
1
+ export type StreamEncoderFrameExtension = 'jpg' | 'png';
2
+ export type PipeFrameEncoderOptions = {
3
+ fps: number;
4
+ inputExt: StreamEncoderFrameExtension;
5
+ outputPath: string;
6
+ crf?: number;
7
+ preset?: string;
8
+ debug?: boolean;
9
+ };
10
+ export declare function createFfmpegImagePipeArgs(options: PipeFrameEncoderOptions): string[];
11
+ export declare function createMuxAudioArgs(input: {
12
+ videoPath: string;
13
+ audioPath: string;
14
+ outputPath: string;
15
+ }): string[];
16
+ export declare class PipeFrameEncoder {
17
+ #private;
18
+ constructor(options: PipeFrameEncoderOptions);
19
+ start(): void;
20
+ writeFrame(buffer: Buffer): Promise<void>;
21
+ finish(): Promise<{
22
+ elapsedMs: number;
23
+ }>;
24
+ abort(): void;
25
+ }
26
+ export declare function muxAudioWithVideo(input: {
27
+ videoPath: string;
28
+ audioPath: string;
29
+ outputPath: string;
30
+ debug?: boolean;
31
+ }): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function createFfmpegImagePipeArgs(options) {
3
+ return [
4
+ '-y',
5
+ '-f',
6
+ 'image2pipe',
7
+ '-framerate',
8
+ String(options.fps),
9
+ '-vcodec',
10
+ options.inputExt === 'png' ? 'png' : 'mjpeg',
11
+ '-i',
12
+ 'pipe:0',
13
+ '-an',
14
+ '-c:v',
15
+ 'libx264',
16
+ '-pix_fmt',
17
+ 'yuv420p',
18
+ '-movflags',
19
+ '+faststart',
20
+ '-crf',
21
+ String(options.crf ?? 18),
22
+ '-preset',
23
+ options.preset ?? 'veryfast',
24
+ options.outputPath
25
+ ];
26
+ }
27
+ export function createMuxAudioArgs(input) {
28
+ return [
29
+ '-y',
30
+ '-i',
31
+ input.videoPath,
32
+ '-i',
33
+ input.audioPath,
34
+ '-map',
35
+ '0:v:0',
36
+ '-map',
37
+ '1:a:0?',
38
+ '-c:v',
39
+ 'copy',
40
+ '-c:a',
41
+ 'aac',
42
+ '-b:a',
43
+ '192k',
44
+ '-af',
45
+ 'apad',
46
+ '-shortest',
47
+ '-movflags',
48
+ '+faststart',
49
+ input.outputPath
50
+ ];
51
+ }
52
+ export class PipeFrameEncoder {
53
+ #options;
54
+ #proc = null;
55
+ #exitPromise = null;
56
+ #stderrTail = '';
57
+ #startedAt = 0;
58
+ constructor(options) {
59
+ this.#options = options;
60
+ }
61
+ start() {
62
+ if (this.#proc)
63
+ return;
64
+ this.#startedAt = Date.now();
65
+ this.#proc = spawn(process.env.FFMPEG_PATH || 'ffmpeg', createFfmpegImagePipeArgs(this.#options), {
66
+ stdio: ['pipe', 'ignore', 'pipe']
67
+ });
68
+ this.#exitPromise = new Promise((resolve) => {
69
+ let settled = false;
70
+ const settle = (result) => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ resolve(result);
75
+ };
76
+ this.#proc?.once('error', (error) => {
77
+ settle({
78
+ code: null,
79
+ error: new Error(`PipeFrameEncoder failed to start ffmpeg: ${error.message}`)
80
+ });
81
+ });
82
+ this.#proc?.once('close', (code) => settle({ code }));
83
+ });
84
+ this.#proc.stderr?.on('data', (chunk) => {
85
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
86
+ if (this.#options.debug)
87
+ process.stderr.write(`[ffmpeg][visualfries-pipe] ${text}`);
88
+ this.#stderrTail = (this.#stderrTail + text).slice(-4000);
89
+ });
90
+ }
91
+ get #stdin() {
92
+ if (!this.#proc?.stdin) {
93
+ throw new Error('PipeFrameEncoder not started.');
94
+ }
95
+ return this.#proc.stdin;
96
+ }
97
+ async writeFrame(buffer) {
98
+ if (!this.#proc)
99
+ this.start();
100
+ let ok = false;
101
+ try {
102
+ ok = this.#stdin.write(buffer);
103
+ }
104
+ catch (error) {
105
+ const exitResult = await this.#exitPromise;
106
+ if (exitResult?.error)
107
+ throw exitResult.error;
108
+ throw error;
109
+ }
110
+ if (ok)
111
+ return;
112
+ const waitForDrain = new Promise((resolve, reject) => {
113
+ const cleanup = () => {
114
+ this.#stdin.off('drain', onDrain);
115
+ this.#stdin.off('error', onError);
116
+ };
117
+ const onDrain = () => {
118
+ cleanup();
119
+ resolve();
120
+ };
121
+ const onError = (error) => {
122
+ cleanup();
123
+ reject(error);
124
+ };
125
+ this.#stdin.on('drain', onDrain);
126
+ this.#stdin.on('error', onError);
127
+ });
128
+ const waitForExit = this.#exitPromise?.then((result) => {
129
+ if (result.error)
130
+ throw result.error;
131
+ if (result.code !== 0) {
132
+ throw new Error(`PipeFrameEncoder failed with exit code ${result.code}. ${this.#stderrTail}`);
133
+ }
134
+ });
135
+ await (waitForExit ? Promise.race([waitForDrain, waitForExit]) : waitForDrain);
136
+ }
137
+ async finish() {
138
+ if (!this.#proc)
139
+ return { elapsedMs: 0 };
140
+ const proc = this.#proc;
141
+ await new Promise((resolve, reject) => {
142
+ if (!proc.stdin || proc.stdin.destroyed) {
143
+ resolve();
144
+ return;
145
+ }
146
+ const onError = (error) => {
147
+ proc.stdin?.off('finish', onFinish);
148
+ reject(error);
149
+ };
150
+ const onFinish = () => {
151
+ proc.stdin?.off('error', onError);
152
+ resolve();
153
+ };
154
+ proc.stdin.once('error', onError);
155
+ proc.stdin.end(onFinish);
156
+ }).catch(async (error) => {
157
+ const exitResult = await this.#exitPromise;
158
+ if (exitResult?.error)
159
+ return;
160
+ throw error;
161
+ });
162
+ const result = await (this.#exitPromise ??
163
+ Promise.resolve({ code: null }));
164
+ this.#proc = null;
165
+ this.#exitPromise = null;
166
+ if (result.error) {
167
+ throw new Error(`${result.error.message}. ${this.#stderrTail}`.trim());
168
+ }
169
+ if (result.code !== 0) {
170
+ throw new Error(`PipeFrameEncoder failed with exit code ${result.code}. ${this.#stderrTail}`);
171
+ }
172
+ return { elapsedMs: Date.now() - this.#startedAt };
173
+ }
174
+ abort() {
175
+ if (!this.#proc)
176
+ return;
177
+ try {
178
+ this.#proc.kill('SIGKILL');
179
+ }
180
+ catch {
181
+ // Best effort cleanup.
182
+ }
183
+ this.#proc = null;
184
+ }
185
+ }
186
+ export async function muxAudioWithVideo(input) {
187
+ await new Promise((resolve, reject) => {
188
+ const proc = spawn(process.env.FFMPEG_PATH || 'ffmpeg', createMuxAudioArgs(input), {
189
+ stdio: ['ignore', 'ignore', 'pipe']
190
+ });
191
+ let stderrTail = '';
192
+ proc.stderr?.on('data', (chunk) => {
193
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
194
+ if (input.debug)
195
+ process.stderr.write(`[ffmpeg][visualfries-mux] ${text}`);
196
+ stderrTail = (stderrTail + text).slice(-4000);
197
+ });
198
+ proc.on('error', reject);
199
+ proc.on('close', (code) => {
200
+ if (code === 0) {
201
+ resolve();
202
+ }
203
+ else {
204
+ reject(new Error(`Audio mux failed with exit code ${code}. ${stderrTail}`));
205
+ }
206
+ });
207
+ });
208
+ }
@@ -0,0 +1,29 @@
1
+ import type { Subtitle } from '../schemas/scene/subtitles.js';
2
+ export type TranscriptWordInput = {
3
+ text?: string;
4
+ word?: string;
5
+ start?: number;
6
+ end?: number;
7
+ start_at?: number;
8
+ end_at?: number;
9
+ };
10
+ export type TranscriptSegmentInput = {
11
+ id?: string;
12
+ text?: string;
13
+ start?: number;
14
+ end?: number;
15
+ start_at?: number;
16
+ end_at?: number;
17
+ words?: TranscriptWordInput[];
18
+ };
19
+ export type TranscriptInput = TranscriptSegmentInput[] | {
20
+ language?: string;
21
+ language_code?: string;
22
+ subtitles?: TranscriptSegmentInput[];
23
+ segments?: TranscriptSegmentInput[];
24
+ words?: TranscriptWordInput[];
25
+ };
26
+ export type TranscriptTextFormat = 'srt' | 'vtt' | 'auto';
27
+ export declare function normalizeTranscript(input: TranscriptInput): Subtitle[];
28
+ export declare function inferTranscriptDuration(subtitles: Subtitle[]): number;
29
+ export declare function parseSubtitleText(input: string, format?: TranscriptTextFormat): TranscriptInput;
@@ -0,0 +1,146 @@
1
+ function toNumber(value) {
2
+ if (typeof value === 'number' && Number.isFinite(value))
3
+ return value;
4
+ if (typeof value === 'string' && value.trim()) {
5
+ const parsed = Number(value);
6
+ return Number.isFinite(parsed) ? parsed : undefined;
7
+ }
8
+ return undefined;
9
+ }
10
+ function normalizeWord(word) {
11
+ const text = String(word.text ?? word.word ?? '').trim();
12
+ const start = toNumber(word.start_at ?? word.start);
13
+ const end = toNumber(word.end_at ?? word.end);
14
+ if (!text || start === undefined || end === undefined)
15
+ return undefined;
16
+ return [text, start, Math.max(start, end)];
17
+ }
18
+ function segmentText(segment) {
19
+ const explicit = String(segment.text ?? '').trim();
20
+ if (explicit)
21
+ return explicit;
22
+ return (segment.words ?? [])
23
+ .map((word) => String(word.text ?? word.word ?? '').trim())
24
+ .filter(Boolean)
25
+ .join(' ');
26
+ }
27
+ function segmentTiming(segment) {
28
+ const start = toNumber(segment.start_at ?? segment.start);
29
+ const end = toNumber(segment.end_at ?? segment.end);
30
+ if (start !== undefined && end !== undefined) {
31
+ return { start, end: Math.max(start, end) };
32
+ }
33
+ const words = (segment.words ?? []).map(normalizeWord).filter(Boolean);
34
+ if (!words.length)
35
+ return undefined;
36
+ return {
37
+ start: words[0][1],
38
+ end: words[words.length - 1][2]
39
+ };
40
+ }
41
+ function wordsToSegments(words, maxWordsPerSegment = 7) {
42
+ const normalized = words.map(normalizeWord).filter(Boolean);
43
+ const segments = [];
44
+ for (let index = 0; index < normalized.length; index += maxWordsPerSegment) {
45
+ const chunk = normalized.slice(index, index + maxWordsPerSegment);
46
+ segments.push({
47
+ id: `subtitle-${segments.length + 1}`,
48
+ text: chunk.map((word) => word[0]).join(' '),
49
+ start_at: chunk[0][1],
50
+ end_at: chunk[chunk.length - 1][2],
51
+ words: chunk.map((word) => ({
52
+ text: word[0],
53
+ start_at: word[1],
54
+ end_at: word[2]
55
+ }))
56
+ });
57
+ }
58
+ return segments;
59
+ }
60
+ export function normalizeTranscript(input) {
61
+ const rawSegments = Array.isArray(input)
62
+ ? input
63
+ : input.subtitles && input.subtitles.length > 0
64
+ ? input.subtitles
65
+ : input.segments && input.segments.length > 0
66
+ ? input.segments
67
+ : input.words
68
+ ? wordsToSegments(input.words)
69
+ : [];
70
+ return rawSegments
71
+ .map((segment, index) => {
72
+ const timing = segmentTiming(segment);
73
+ const text = segmentText(segment);
74
+ if (!timing || !text)
75
+ return undefined;
76
+ const words = (segment.words ?? []).map(normalizeWord).filter(Boolean);
77
+ return {
78
+ id: segment.id ?? `subtitle-${index + 1}`,
79
+ start_at: timing.start,
80
+ end_at: timing.end,
81
+ text,
82
+ words: words.length ? words : undefined
83
+ };
84
+ })
85
+ .filter(Boolean);
86
+ }
87
+ export function inferTranscriptDuration(subtitles) {
88
+ return subtitles.reduce((duration, subtitle) => Math.max(duration, subtitle.end_at), 0);
89
+ }
90
+ function parseTimestamp(value) {
91
+ const normalized = value.trim().replace(',', '.');
92
+ const parts = normalized.split(':');
93
+ if (parts.length < 2 || parts.length > 3)
94
+ return undefined;
95
+ const seconds = Number(parts.at(-1));
96
+ const minutes = Number(parts.at(-2));
97
+ const hours = parts.length === 3 ? Number(parts[0]) : 0;
98
+ if (![seconds, minutes, hours].every(Number.isFinite))
99
+ return undefined;
100
+ return hours * 3600 + minutes * 60 + seconds;
101
+ }
102
+ function stripVttCueSetting(value) {
103
+ return value.trim().split(/\s+/, 1)[0] ?? value.trim();
104
+ }
105
+ function parseCueBlock(block, index) {
106
+ const lines = block
107
+ .split(/\r?\n/)
108
+ .map((line) => line.trim())
109
+ .filter(Boolean);
110
+ if (!lines.length)
111
+ return undefined;
112
+ const timingIndex = lines.findIndex((line) => line.includes('-->'));
113
+ if (timingIndex === -1)
114
+ return undefined;
115
+ const [rawStart, rawEnd] = lines[timingIndex].split('-->', 2);
116
+ const start = parseTimestamp(rawStart ?? '');
117
+ const end = parseTimestamp(stripVttCueSetting(rawEnd ?? ''));
118
+ if (start === undefined || end === undefined)
119
+ return undefined;
120
+ const text = lines
121
+ .slice(timingIndex + 1)
122
+ .join(' ')
123
+ .replace(/<[^>]+>/g, '')
124
+ .replace(/\s+/g, ' ')
125
+ .trim();
126
+ if (!text)
127
+ return undefined;
128
+ return {
129
+ id: `subtitle-${index + 1}`,
130
+ text,
131
+ start_at: start,
132
+ end_at: Math.max(start, end)
133
+ };
134
+ }
135
+ export function parseSubtitleText(input, format = 'auto') {
136
+ const text = input.replace(/^\uFEFF/, '').trim();
137
+ const withoutHeader = format === 'vtt' || (format === 'auto' && /^WEBVTT(?:\s|$)/i.test(text))
138
+ ? text
139
+ .replace(/^WEBVTT[^\n\r]*(?:\r?\n)+/i, '')
140
+ .replace(/^(NOTE|STYLE|REGION)[\s\S]*?(?:\r?\n){2}/gim, '')
141
+ : text;
142
+ return withoutHeader
143
+ .split(/\r?\n\s*\r?\n/)
144
+ .map(parseCueBlock)
145
+ .filter(Boolean);
146
+ }
@@ -0,0 +1,20 @@
1
+ import { type ComponentInput, type Scene, type SceneInput } from '../schemas/scene/index.js';
2
+ export type AgentTransitionStyle = 'dip-to-black' | 'flash' | 'swipe-left' | 'swipe-up' | 'focus-pull';
3
+ export type AgentTransitionCue = {
4
+ id?: string;
5
+ time: number;
6
+ duration?: number;
7
+ style?: AgentTransitionStyle;
8
+ color?: string;
9
+ layerOrder?: number;
10
+ animated?: boolean;
11
+ };
12
+ export type AddAgentTransitionsInput = {
13
+ scene: Scene | SceneInput;
14
+ transitions: AgentTransitionCue[];
15
+ layerId?: string;
16
+ layerName?: string;
17
+ layerOrder?: number;
18
+ };
19
+ export declare function createAgentTransitionComponent(cue: AgentTransitionCue, scene: Scene | SceneInput, index?: number): ComponentInput;
20
+ export declare function addAgentTransitions(input: AddAgentTransitionsInput): Scene;