@contractspec/lib.video-gen 1.42.0

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 (137) hide show
  1. package/dist/browser/compositions/api-overview.js +645 -0
  2. package/dist/browser/compositions/index.js +1133 -0
  3. package/dist/browser/compositions/primitives/animated-text.js +144 -0
  4. package/dist/browser/compositions/primitives/brand-frame.js +181 -0
  5. package/dist/browser/compositions/primitives/code-block.js +226 -0
  6. package/dist/browser/compositions/primitives/index.js +656 -0
  7. package/dist/browser/compositions/primitives/progress-bar.js +59 -0
  8. package/dist/browser/compositions/primitives/terminal.js +265 -0
  9. package/dist/browser/compositions/primitives/transition.js +98 -0
  10. package/dist/browser/compositions/social-clip.js +500 -0
  11. package/dist/browser/compositions/terminal-demo.js +558 -0
  12. package/dist/browser/design/index.js +155 -0
  13. package/dist/browser/design/layouts.js +50 -0
  14. package/dist/browser/design/motion.js +43 -0
  15. package/dist/browser/design/tokens.js +28 -0
  16. package/dist/browser/design/typography.js +61 -0
  17. package/dist/browser/docs/compositions.docblock.js +182 -0
  18. package/dist/browser/docs/design.docblock.js +187 -0
  19. package/dist/browser/docs/generators.docblock.js +187 -0
  20. package/dist/browser/docs/rendering.docblock.js +197 -0
  21. package/dist/browser/docs/video-gen.docblock.js +141 -0
  22. package/dist/browser/generators/index.js +416 -0
  23. package/dist/browser/generators/scene-planner.js +205 -0
  24. package/dist/browser/generators/script-generator.js +147 -0
  25. package/dist/browser/generators/video-generator.js +414 -0
  26. package/dist/browser/index.js +1550 -0
  27. package/dist/browser/player/demo-player.js +1136 -0
  28. package/dist/browser/player/index.js +1136 -0
  29. package/dist/browser/remotion/Root.js +1189 -0
  30. package/dist/browser/remotion/index.js +1190 -0
  31. package/dist/browser/renderers/config.js +40 -0
  32. package/dist/browser/renderers/index.js +160 -0
  33. package/dist/browser/renderers/local.js +156 -0
  34. package/dist/browser/types.js +13 -0
  35. package/dist/compositions/api-overview.d.ts +16 -0
  36. package/dist/compositions/api-overview.js +640 -0
  37. package/dist/compositions/index.d.ts +7 -0
  38. package/dist/compositions/index.js +1128 -0
  39. package/dist/compositions/primitives/animated-text.d.ts +22 -0
  40. package/dist/compositions/primitives/animated-text.js +139 -0
  41. package/dist/compositions/primitives/brand-frame.d.ts +14 -0
  42. package/dist/compositions/primitives/brand-frame.js +176 -0
  43. package/dist/compositions/primitives/code-block.d.ts +18 -0
  44. package/dist/compositions/primitives/code-block.js +221 -0
  45. package/dist/compositions/primitives/index.d.ts +12 -0
  46. package/dist/compositions/primitives/index.js +651 -0
  47. package/dist/compositions/primitives/progress-bar.d.ts +12 -0
  48. package/dist/compositions/primitives/progress-bar.js +54 -0
  49. package/dist/compositions/primitives/terminal.d.ts +24 -0
  50. package/dist/compositions/primitives/terminal.js +260 -0
  51. package/dist/compositions/primitives/transition.d.ts +14 -0
  52. package/dist/compositions/primitives/transition.js +93 -0
  53. package/dist/compositions/social-clip.d.ts +16 -0
  54. package/dist/compositions/social-clip.js +495 -0
  55. package/dist/compositions/terminal-demo.d.ts +17 -0
  56. package/dist/compositions/terminal-demo.js +553 -0
  57. package/dist/design/index.d.ts +4 -0
  58. package/dist/design/index.js +150 -0
  59. package/dist/design/layouts.d.ts +69 -0
  60. package/dist/design/layouts.js +45 -0
  61. package/dist/design/motion.d.ts +72 -0
  62. package/dist/design/motion.js +38 -0
  63. package/dist/design/tokens.d.ts +31 -0
  64. package/dist/design/tokens.js +23 -0
  65. package/dist/design/typography.d.ts +61 -0
  66. package/dist/design/typography.js +56 -0
  67. package/dist/docs/compositions.docblock.d.ts +1 -0
  68. package/dist/docs/compositions.docblock.js +183 -0
  69. package/dist/docs/design.docblock.d.ts +1 -0
  70. package/dist/docs/design.docblock.js +188 -0
  71. package/dist/docs/generators.docblock.d.ts +1 -0
  72. package/dist/docs/generators.docblock.js +188 -0
  73. package/dist/docs/rendering.docblock.d.ts +1 -0
  74. package/dist/docs/rendering.docblock.js +198 -0
  75. package/dist/docs/video-gen.docblock.d.ts +1 -0
  76. package/dist/docs/video-gen.docblock.js +142 -0
  77. package/dist/generators/index.d.ts +5 -0
  78. package/dist/generators/index.js +411 -0
  79. package/dist/generators/scene-planner.d.ts +23 -0
  80. package/dist/generators/scene-planner.js +200 -0
  81. package/dist/generators/script-generator.d.ts +49 -0
  82. package/dist/generators/script-generator.js +142 -0
  83. package/dist/generators/video-generator.d.ts +20 -0
  84. package/dist/generators/video-generator.js +409 -0
  85. package/dist/index.d.ts +6 -0
  86. package/dist/index.js +1545 -0
  87. package/dist/node/compositions/api-overview.js +640 -0
  88. package/dist/node/compositions/index.js +1128 -0
  89. package/dist/node/compositions/primitives/animated-text.js +139 -0
  90. package/dist/node/compositions/primitives/brand-frame.js +176 -0
  91. package/dist/node/compositions/primitives/code-block.js +221 -0
  92. package/dist/node/compositions/primitives/index.js +651 -0
  93. package/dist/node/compositions/primitives/progress-bar.js +54 -0
  94. package/dist/node/compositions/primitives/terminal.js +260 -0
  95. package/dist/node/compositions/primitives/transition.js +93 -0
  96. package/dist/node/compositions/social-clip.js +495 -0
  97. package/dist/node/compositions/terminal-demo.js +553 -0
  98. package/dist/node/design/index.js +150 -0
  99. package/dist/node/design/layouts.js +45 -0
  100. package/dist/node/design/motion.js +38 -0
  101. package/dist/node/design/tokens.js +23 -0
  102. package/dist/node/design/typography.js +56 -0
  103. package/dist/node/docs/compositions.docblock.js +182 -0
  104. package/dist/node/docs/design.docblock.js +187 -0
  105. package/dist/node/docs/generators.docblock.js +187 -0
  106. package/dist/node/docs/rendering.docblock.js +197 -0
  107. package/dist/node/docs/video-gen.docblock.js +141 -0
  108. package/dist/node/generators/index.js +411 -0
  109. package/dist/node/generators/scene-planner.js +200 -0
  110. package/dist/node/generators/script-generator.js +142 -0
  111. package/dist/node/generators/video-generator.js +409 -0
  112. package/dist/node/index.js +1545 -0
  113. package/dist/node/player/demo-player.js +1131 -0
  114. package/dist/node/player/index.js +1131 -0
  115. package/dist/node/remotion/Root.js +1184 -0
  116. package/dist/node/remotion/index.js +1185 -0
  117. package/dist/node/renderers/config.js +35 -0
  118. package/dist/node/renderers/index.js +155 -0
  119. package/dist/node/renderers/local.js +151 -0
  120. package/dist/node/types.js +8 -0
  121. package/dist/player/demo-player.d.ts +55 -0
  122. package/dist/player/demo-player.js +1131 -0
  123. package/dist/player/index.d.ts +2 -0
  124. package/dist/player/index.js +1131 -0
  125. package/dist/remotion/Root.d.ts +2 -0
  126. package/dist/remotion/Root.js +1184 -0
  127. package/dist/remotion/index.d.ts +1 -0
  128. package/dist/remotion/index.js +1185 -0
  129. package/dist/renderers/config.d.ts +28 -0
  130. package/dist/renderers/config.js +35 -0
  131. package/dist/renderers/index.d.ts +3 -0
  132. package/dist/renderers/index.js +155 -0
  133. package/dist/renderers/local.d.ts +17 -0
  134. package/dist/renderers/local.js +151 -0
  135. package/dist/types.d.ts +63 -0
  136. package/dist/types.js +8 -0
  137. package/package.json +637 -0
@@ -0,0 +1,411 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/design/layouts.ts
5
+ import { VIDEO_FORMATS } from "@contractspec/lib.contracts-integrations/integrations/providers/video";
6
+ var DEFAULT_FPS = 30;
7
+ var videoSafeZone = {
8
+ horizontal: 120,
9
+ vertical: 80,
10
+ contentWidth: 1680,
11
+ contentHeight: 920
12
+ };
13
+ function scaleSafeZone(format) {
14
+ const scaleX = format.width / 1920;
15
+ const scaleY = format.height / 1080;
16
+ return {
17
+ horizontal: Math.round(videoSafeZone.horizontal * scaleX),
18
+ vertical: Math.round(videoSafeZone.vertical * scaleY),
19
+ contentWidth: Math.round(videoSafeZone.contentWidth * scaleX),
20
+ contentHeight: Math.round(videoSafeZone.contentHeight * scaleY)
21
+ };
22
+ }
23
+ var videoPositions = {
24
+ center: { x: 960, y: 540 },
25
+ topLeft: { x: 120, y: 80 },
26
+ topRight: { x: 1800, y: 80 },
27
+ bottomLeft: { x: 120, y: 1000 },
28
+ bottomRight: { x: 1800, y: 1000 },
29
+ bottomCenter: { x: 960, y: 960 }
30
+ };
31
+ function getAllFormatVariants() {
32
+ return [
33
+ VIDEO_FORMATS.landscape,
34
+ VIDEO_FORMATS.square,
35
+ VIDEO_FORMATS.portrait
36
+ ];
37
+ }
38
+
39
+ // src/generators/scene-planner.ts
40
+ class ScenePlanner {
41
+ llm;
42
+ model;
43
+ temperature;
44
+ fps;
45
+ constructor(options) {
46
+ this.llm = options?.llm;
47
+ this.model = options?.model;
48
+ this.temperature = options?.temperature ?? 0.3;
49
+ this.fps = options?.fps ?? DEFAULT_FPS;
50
+ }
51
+ async plan(brief) {
52
+ if (this.llm) {
53
+ return this.planWithLlm(brief);
54
+ }
55
+ return this.planDeterministic(brief);
56
+ }
57
+ planDeterministic(brief) {
58
+ const { content } = brief;
59
+ const scenes = [];
60
+ const fps = this.fps;
61
+ scenes.push({
62
+ compositionId: "SocialClip",
63
+ props: {
64
+ hook: content.title,
65
+ message: content.summary,
66
+ points: content.solutions.slice(0, 3),
67
+ cta: content.callToAction ?? "Learn more"
68
+ },
69
+ durationInFrames: 3 * fps,
70
+ narrationText: `${content.title}. ${content.summary}`
71
+ });
72
+ if (content.problems.length > 0) {
73
+ scenes.push({
74
+ compositionId: "SocialClip",
75
+ props: {
76
+ hook: "The Problem",
77
+ message: content.problems[0] ?? "",
78
+ points: content.problems.slice(1, 4)
79
+ },
80
+ durationInFrames: 4 * fps,
81
+ narrationText: `The problem: ${content.problems.join(". ")}`
82
+ });
83
+ }
84
+ if (content.solutions.length > 0) {
85
+ scenes.push({
86
+ compositionId: "SocialClip",
87
+ props: {
88
+ hook: "The Solution",
89
+ message: content.solutions[0] ?? "",
90
+ points: content.solutions.slice(1, 4)
91
+ },
92
+ durationInFrames: 5 * fps,
93
+ narrationText: `The solution: ${content.solutions.join(". ")}`
94
+ });
95
+ }
96
+ if (content.metrics && content.metrics.length > 0) {
97
+ scenes.push({
98
+ compositionId: "SocialClip",
99
+ props: {
100
+ hook: "Results",
101
+ message: content.metrics[0] ?? "",
102
+ points: content.metrics.slice(1, 3)
103
+ },
104
+ durationInFrames: 3 * fps,
105
+ narrationText: content.metrics.join(". ")
106
+ });
107
+ }
108
+ if (content.callToAction) {
109
+ scenes.push({
110
+ compositionId: "SocialClip",
111
+ props: {
112
+ hook: content.callToAction,
113
+ message: "",
114
+ cta: content.callToAction
115
+ },
116
+ durationInFrames: 2 * fps,
117
+ narrationText: content.callToAction
118
+ });
119
+ }
120
+ if (brief.targetDurationSeconds) {
121
+ const targetFrames = brief.targetDurationSeconds * fps;
122
+ const currentFrames = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
123
+ const ratio = targetFrames / currentFrames;
124
+ for (const scene of scenes) {
125
+ scene.durationInFrames = Math.round(scene.durationInFrames * ratio);
126
+ }
127
+ }
128
+ const totalDuration = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
129
+ const narrationScript = scenes.filter((s) => s.narrationText).map((s) => s.narrationText).join(" ");
130
+ return {
131
+ scenes,
132
+ estimatedDurationSeconds: totalDuration / fps,
133
+ narrationScript
134
+ };
135
+ }
136
+ async planWithLlm(brief) {
137
+ const messages = [
138
+ {
139
+ role: "system",
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: `You are a video scene planner for ContractSpec marketing/documentation videos.
144
+ Given a content brief, break it into video scenes.
145
+
146
+ Each scene must have:
147
+ - compositionId: one of "ApiOverview", "SocialClip", "TerminalDemo"
148
+ - props: the input props for that composition (see type definitions)
149
+ - durationInFrames: duration at ${this.fps}fps
150
+ - narrationText: what the narrator says during this scene
151
+
152
+ Return a JSON object with shape:
153
+ {
154
+ "scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
155
+ "narrationScript": string
156
+ }
157
+
158
+ Keep the total duration around ${brief.targetDurationSeconds ?? 30} seconds.
159
+ Prioritize clarity and pacing. Each scene should communicate one idea.`
160
+ }
161
+ ]
162
+ },
163
+ {
164
+ role: "user",
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify(brief.content)
169
+ }
170
+ ]
171
+ }
172
+ ];
173
+ if (!this.llm) {
174
+ return this.planDeterministic(brief);
175
+ }
176
+ try {
177
+ const response = await this.llm.chat(messages, {
178
+ model: this.model,
179
+ temperature: this.temperature,
180
+ responseFormat: "json"
181
+ });
182
+ const text = response.message.content.find((p) => p.type === "text");
183
+ if (!text || text.type !== "text") {
184
+ return this.planDeterministic(brief);
185
+ }
186
+ const parsed = JSON.parse(text.text);
187
+ const totalDuration = parsed.scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
188
+ return {
189
+ scenes: parsed.scenes,
190
+ estimatedDurationSeconds: totalDuration / this.fps,
191
+ narrationScript: parsed.narrationScript
192
+ };
193
+ } catch {
194
+ return this.planDeterministic(brief);
195
+ }
196
+ }
197
+ }
198
+
199
+ // src/generators/script-generator.ts
200
+ class ScriptGenerator {
201
+ llm;
202
+ model;
203
+ temperature;
204
+ constructor(options) {
205
+ this.llm = options?.llm;
206
+ this.model = options?.model;
207
+ this.temperature = options?.temperature ?? 0.5;
208
+ }
209
+ async generate(brief, config) {
210
+ const style = config?.style ?? "professional";
211
+ if (this.llm) {
212
+ return this.generateWithLlm(brief, style);
213
+ }
214
+ return this.generateDeterministic(brief, style);
215
+ }
216
+ generateDeterministic(brief, style) {
217
+ const segments = [];
218
+ const intro = this.formatForStyle(`${brief.title}. ${brief.summary}`, style);
219
+ segments.push({
220
+ sceneId: "intro",
221
+ text: intro,
222
+ estimatedDurationSeconds: this.estimateDuration(intro)
223
+ });
224
+ if (brief.problems.length > 0) {
225
+ const problemText = this.formatForStyle(`The challenge: ${brief.problems.join(". ")}`, style);
226
+ segments.push({
227
+ sceneId: "problems",
228
+ text: problemText,
229
+ estimatedDurationSeconds: this.estimateDuration(problemText)
230
+ });
231
+ }
232
+ if (brief.solutions.length > 0) {
233
+ const solutionText = this.formatForStyle(`The solution: ${brief.solutions.join(". ")}`, style);
234
+ segments.push({
235
+ sceneId: "solutions",
236
+ text: solutionText,
237
+ estimatedDurationSeconds: this.estimateDuration(solutionText)
238
+ });
239
+ }
240
+ if (brief.metrics && brief.metrics.length > 0) {
241
+ const metricsText = this.formatForStyle(`The results: ${brief.metrics.join(". ")}`, style);
242
+ segments.push({
243
+ sceneId: "metrics",
244
+ text: metricsText,
245
+ estimatedDurationSeconds: this.estimateDuration(metricsText)
246
+ });
247
+ }
248
+ if (brief.callToAction) {
249
+ const ctaText = this.formatForStyle(brief.callToAction, style);
250
+ segments.push({
251
+ sceneId: "cta",
252
+ text: ctaText,
253
+ estimatedDurationSeconds: this.estimateDuration(ctaText)
254
+ });
255
+ }
256
+ const fullText = segments.map((s) => s.text).join(" ");
257
+ const totalDuration = segments.reduce((sum, s) => sum + s.estimatedDurationSeconds, 0);
258
+ return {
259
+ fullText,
260
+ segments,
261
+ estimatedDurationSeconds: totalDuration,
262
+ style
263
+ };
264
+ }
265
+ async generateWithLlm(brief, style) {
266
+ const styleGuide = {
267
+ professional: "Use a clear, authoritative, professional tone. Be concise and direct.",
268
+ casual: "Use a friendly, conversational tone. Be approachable and relatable.",
269
+ technical: "Use precise technical language. Be detailed and accurate."
270
+ };
271
+ const styleKey = style ?? "professional";
272
+ const messages = [
273
+ {
274
+ role: "system",
275
+ content: [
276
+ {
277
+ type: "text",
278
+ text: `You are a video narration script writer.
279
+ Write a narration script for a short video (30-60 seconds).
280
+ ${styleGuide[styleKey]}
281
+
282
+ Return JSON with shape:
283
+ {
284
+ "segments": [{ "sceneId": string, "text": string }],
285
+ "fullText": string
286
+ }
287
+
288
+ Scene IDs should be: "intro", "problems", "solutions", "metrics", "cta".
289
+ Only include segments that are relevant to the brief content.`
290
+ }
291
+ ]
292
+ },
293
+ {
294
+ role: "user",
295
+ content: [{ type: "text", text: JSON.stringify(brief) }]
296
+ }
297
+ ];
298
+ if (!this.llm) {
299
+ return this.generateDeterministic(brief, style);
300
+ }
301
+ try {
302
+ const response = await this.llm.chat(messages, {
303
+ model: this.model,
304
+ temperature: this.temperature,
305
+ responseFormat: "json"
306
+ });
307
+ const text = response.message.content.find((p) => p.type === "text");
308
+ if (!text || text.type !== "text") {
309
+ return this.generateDeterministic(brief, style);
310
+ }
311
+ const parsed = JSON.parse(text.text);
312
+ const segments = parsed.segments.map((s) => ({
313
+ sceneId: s.sceneId,
314
+ text: s.text,
315
+ estimatedDurationSeconds: this.estimateDuration(s.text)
316
+ }));
317
+ return {
318
+ fullText: parsed.fullText,
319
+ segments,
320
+ estimatedDurationSeconds: segments.reduce((sum, s) => sum + s.estimatedDurationSeconds, 0),
321
+ style
322
+ };
323
+ } catch {
324
+ return this.generateDeterministic(brief, style);
325
+ }
326
+ }
327
+ formatForStyle(text, _style) {
328
+ return text;
329
+ }
330
+ estimateDuration(text) {
331
+ const wordCount = text.split(/\s+/).length;
332
+ return Math.ceil(wordCount / 150 * 60);
333
+ }
334
+ }
335
+
336
+ // src/generators/video-generator.ts
337
+ import { VIDEO_FORMATS as VIDEO_FORMATS2 } from "@contractspec/lib.contracts-integrations/integrations/providers/video";
338
+ class VideoGenerator {
339
+ scenePlanner;
340
+ scriptGenerator;
341
+ voice;
342
+ defaultVoiceId;
343
+ fps;
344
+ constructor(options) {
345
+ this.fps = options?.fps ?? DEFAULT_FPS;
346
+ this.voice = options?.voice;
347
+ this.defaultVoiceId = options?.defaultVoiceId;
348
+ this.scenePlanner = new ScenePlanner({
349
+ llm: options?.llm,
350
+ model: options?.model,
351
+ temperature: options?.temperature,
352
+ fps: this.fps
353
+ });
354
+ this.scriptGenerator = new ScriptGenerator({
355
+ llm: options?.llm,
356
+ model: options?.model,
357
+ temperature: options?.temperature
358
+ });
359
+ }
360
+ async generate(brief) {
361
+ const scenePlan = await this.scenePlanner.plan(brief);
362
+ let narrationAudio;
363
+ if (brief.narration?.enabled && this.voice) {
364
+ const script = await this.scriptGenerator.generate(brief.content, brief.narration);
365
+ const voiceId = brief.narration.voiceId ?? this.defaultVoiceId;
366
+ if (voiceId && script.fullText) {
367
+ try {
368
+ const result = await this.voice.synthesize({
369
+ text: script.fullText,
370
+ voiceId,
371
+ format: "mp3"
372
+ });
373
+ narrationAudio = {
374
+ data: result.audio,
375
+ format: "mp3",
376
+ durationSeconds: result.durationSeconds ?? script.estimatedDurationSeconds,
377
+ volume: 1
378
+ };
379
+ } catch {}
380
+ }
381
+ }
382
+ const format = brief.format ?? VIDEO_FORMATS2.landscape;
383
+ const scenes = scenePlan.scenes.map((planned, i) => ({
384
+ id: `scene-${i}`,
385
+ compositionId: planned.compositionId,
386
+ props: planned.props,
387
+ durationInFrames: planned.durationInFrames,
388
+ narrationText: planned.narrationText
389
+ }));
390
+ const totalDurationInFrames = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
391
+ const project = {
392
+ id: generateProjectId(),
393
+ scenes,
394
+ totalDurationInFrames,
395
+ fps: this.fps,
396
+ format,
397
+ audio: narrationAudio ? { narration: narrationAudio } : undefined
398
+ };
399
+ return project;
400
+ }
401
+ }
402
+ function generateProjectId() {
403
+ const timestamp = Date.now().toString(36);
404
+ const random = Math.random().toString(36).slice(2, 8);
405
+ return `vp_${timestamp}_${random}`;
406
+ }
407
+ export {
408
+ VideoGenerator,
409
+ ScriptGenerator,
410
+ ScenePlanner
411
+ };
@@ -0,0 +1,200 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/design/layouts.ts
5
+ import { VIDEO_FORMATS } from "@contractspec/lib.contracts-integrations/integrations/providers/video";
6
+ var DEFAULT_FPS = 30;
7
+ var videoSafeZone = {
8
+ horizontal: 120,
9
+ vertical: 80,
10
+ contentWidth: 1680,
11
+ contentHeight: 920
12
+ };
13
+ function scaleSafeZone(format) {
14
+ const scaleX = format.width / 1920;
15
+ const scaleY = format.height / 1080;
16
+ return {
17
+ horizontal: Math.round(videoSafeZone.horizontal * scaleX),
18
+ vertical: Math.round(videoSafeZone.vertical * scaleY),
19
+ contentWidth: Math.round(videoSafeZone.contentWidth * scaleX),
20
+ contentHeight: Math.round(videoSafeZone.contentHeight * scaleY)
21
+ };
22
+ }
23
+ var videoPositions = {
24
+ center: { x: 960, y: 540 },
25
+ topLeft: { x: 120, y: 80 },
26
+ topRight: { x: 1800, y: 80 },
27
+ bottomLeft: { x: 120, y: 1000 },
28
+ bottomRight: { x: 1800, y: 1000 },
29
+ bottomCenter: { x: 960, y: 960 }
30
+ };
31
+ function getAllFormatVariants() {
32
+ return [
33
+ VIDEO_FORMATS.landscape,
34
+ VIDEO_FORMATS.square,
35
+ VIDEO_FORMATS.portrait
36
+ ];
37
+ }
38
+
39
+ // src/generators/scene-planner.ts
40
+ class ScenePlanner {
41
+ llm;
42
+ model;
43
+ temperature;
44
+ fps;
45
+ constructor(options) {
46
+ this.llm = options?.llm;
47
+ this.model = options?.model;
48
+ this.temperature = options?.temperature ?? 0.3;
49
+ this.fps = options?.fps ?? DEFAULT_FPS;
50
+ }
51
+ async plan(brief) {
52
+ if (this.llm) {
53
+ return this.planWithLlm(brief);
54
+ }
55
+ return this.planDeterministic(brief);
56
+ }
57
+ planDeterministic(brief) {
58
+ const { content } = brief;
59
+ const scenes = [];
60
+ const fps = this.fps;
61
+ scenes.push({
62
+ compositionId: "SocialClip",
63
+ props: {
64
+ hook: content.title,
65
+ message: content.summary,
66
+ points: content.solutions.slice(0, 3),
67
+ cta: content.callToAction ?? "Learn more"
68
+ },
69
+ durationInFrames: 3 * fps,
70
+ narrationText: `${content.title}. ${content.summary}`
71
+ });
72
+ if (content.problems.length > 0) {
73
+ scenes.push({
74
+ compositionId: "SocialClip",
75
+ props: {
76
+ hook: "The Problem",
77
+ message: content.problems[0] ?? "",
78
+ points: content.problems.slice(1, 4)
79
+ },
80
+ durationInFrames: 4 * fps,
81
+ narrationText: `The problem: ${content.problems.join(". ")}`
82
+ });
83
+ }
84
+ if (content.solutions.length > 0) {
85
+ scenes.push({
86
+ compositionId: "SocialClip",
87
+ props: {
88
+ hook: "The Solution",
89
+ message: content.solutions[0] ?? "",
90
+ points: content.solutions.slice(1, 4)
91
+ },
92
+ durationInFrames: 5 * fps,
93
+ narrationText: `The solution: ${content.solutions.join(". ")}`
94
+ });
95
+ }
96
+ if (content.metrics && content.metrics.length > 0) {
97
+ scenes.push({
98
+ compositionId: "SocialClip",
99
+ props: {
100
+ hook: "Results",
101
+ message: content.metrics[0] ?? "",
102
+ points: content.metrics.slice(1, 3)
103
+ },
104
+ durationInFrames: 3 * fps,
105
+ narrationText: content.metrics.join(". ")
106
+ });
107
+ }
108
+ if (content.callToAction) {
109
+ scenes.push({
110
+ compositionId: "SocialClip",
111
+ props: {
112
+ hook: content.callToAction,
113
+ message: "",
114
+ cta: content.callToAction
115
+ },
116
+ durationInFrames: 2 * fps,
117
+ narrationText: content.callToAction
118
+ });
119
+ }
120
+ if (brief.targetDurationSeconds) {
121
+ const targetFrames = brief.targetDurationSeconds * fps;
122
+ const currentFrames = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
123
+ const ratio = targetFrames / currentFrames;
124
+ for (const scene of scenes) {
125
+ scene.durationInFrames = Math.round(scene.durationInFrames * ratio);
126
+ }
127
+ }
128
+ const totalDuration = scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
129
+ const narrationScript = scenes.filter((s) => s.narrationText).map((s) => s.narrationText).join(" ");
130
+ return {
131
+ scenes,
132
+ estimatedDurationSeconds: totalDuration / fps,
133
+ narrationScript
134
+ };
135
+ }
136
+ async planWithLlm(brief) {
137
+ const messages = [
138
+ {
139
+ role: "system",
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: `You are a video scene planner for ContractSpec marketing/documentation videos.
144
+ Given a content brief, break it into video scenes.
145
+
146
+ Each scene must have:
147
+ - compositionId: one of "ApiOverview", "SocialClip", "TerminalDemo"
148
+ - props: the input props for that composition (see type definitions)
149
+ - durationInFrames: duration at ${this.fps}fps
150
+ - narrationText: what the narrator says during this scene
151
+
152
+ Return a JSON object with shape:
153
+ {
154
+ "scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
155
+ "narrationScript": string
156
+ }
157
+
158
+ Keep the total duration around ${brief.targetDurationSeconds ?? 30} seconds.
159
+ Prioritize clarity and pacing. Each scene should communicate one idea.`
160
+ }
161
+ ]
162
+ },
163
+ {
164
+ role: "user",
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify(brief.content)
169
+ }
170
+ ]
171
+ }
172
+ ];
173
+ if (!this.llm) {
174
+ return this.planDeterministic(brief);
175
+ }
176
+ try {
177
+ const response = await this.llm.chat(messages, {
178
+ model: this.model,
179
+ temperature: this.temperature,
180
+ responseFormat: "json"
181
+ });
182
+ const text = response.message.content.find((p) => p.type === "text");
183
+ if (!text || text.type !== "text") {
184
+ return this.planDeterministic(brief);
185
+ }
186
+ const parsed = JSON.parse(text.text);
187
+ const totalDuration = parsed.scenes.reduce((sum, s) => sum + s.durationInFrames, 0);
188
+ return {
189
+ scenes: parsed.scenes,
190
+ estimatedDurationSeconds: totalDuration / this.fps,
191
+ narrationScript: parsed.narrationScript
192
+ };
193
+ } catch {
194
+ return this.planDeterministic(brief);
195
+ }
196
+ }
197
+ }
198
+ export {
199
+ ScenePlanner
200
+ };