@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.
- package/dist/browser/compositions/api-overview.js +645 -0
- package/dist/browser/compositions/index.js +1133 -0
- package/dist/browser/compositions/primitives/animated-text.js +144 -0
- package/dist/browser/compositions/primitives/brand-frame.js +181 -0
- package/dist/browser/compositions/primitives/code-block.js +226 -0
- package/dist/browser/compositions/primitives/index.js +656 -0
- package/dist/browser/compositions/primitives/progress-bar.js +59 -0
- package/dist/browser/compositions/primitives/terminal.js +265 -0
- package/dist/browser/compositions/primitives/transition.js +98 -0
- package/dist/browser/compositions/social-clip.js +500 -0
- package/dist/browser/compositions/terminal-demo.js +558 -0
- package/dist/browser/design/index.js +155 -0
- package/dist/browser/design/layouts.js +50 -0
- package/dist/browser/design/motion.js +43 -0
- package/dist/browser/design/tokens.js +28 -0
- package/dist/browser/design/typography.js +61 -0
- package/dist/browser/docs/compositions.docblock.js +182 -0
- package/dist/browser/docs/design.docblock.js +187 -0
- package/dist/browser/docs/generators.docblock.js +187 -0
- package/dist/browser/docs/rendering.docblock.js +197 -0
- package/dist/browser/docs/video-gen.docblock.js +141 -0
- package/dist/browser/generators/index.js +416 -0
- package/dist/browser/generators/scene-planner.js +205 -0
- package/dist/browser/generators/script-generator.js +147 -0
- package/dist/browser/generators/video-generator.js +414 -0
- package/dist/browser/index.js +1550 -0
- package/dist/browser/player/demo-player.js +1136 -0
- package/dist/browser/player/index.js +1136 -0
- package/dist/browser/remotion/Root.js +1189 -0
- package/dist/browser/remotion/index.js +1190 -0
- package/dist/browser/renderers/config.js +40 -0
- package/dist/browser/renderers/index.js +160 -0
- package/dist/browser/renderers/local.js +156 -0
- package/dist/browser/types.js +13 -0
- package/dist/compositions/api-overview.d.ts +16 -0
- package/dist/compositions/api-overview.js +640 -0
- package/dist/compositions/index.d.ts +7 -0
- package/dist/compositions/index.js +1128 -0
- package/dist/compositions/primitives/animated-text.d.ts +22 -0
- package/dist/compositions/primitives/animated-text.js +139 -0
- package/dist/compositions/primitives/brand-frame.d.ts +14 -0
- package/dist/compositions/primitives/brand-frame.js +176 -0
- package/dist/compositions/primitives/code-block.d.ts +18 -0
- package/dist/compositions/primitives/code-block.js +221 -0
- package/dist/compositions/primitives/index.d.ts +12 -0
- package/dist/compositions/primitives/index.js +651 -0
- package/dist/compositions/primitives/progress-bar.d.ts +12 -0
- package/dist/compositions/primitives/progress-bar.js +54 -0
- package/dist/compositions/primitives/terminal.d.ts +24 -0
- package/dist/compositions/primitives/terminal.js +260 -0
- package/dist/compositions/primitives/transition.d.ts +14 -0
- package/dist/compositions/primitives/transition.js +93 -0
- package/dist/compositions/social-clip.d.ts +16 -0
- package/dist/compositions/social-clip.js +495 -0
- package/dist/compositions/terminal-demo.d.ts +17 -0
- package/dist/compositions/terminal-demo.js +553 -0
- package/dist/design/index.d.ts +4 -0
- package/dist/design/index.js +150 -0
- package/dist/design/layouts.d.ts +69 -0
- package/dist/design/layouts.js +45 -0
- package/dist/design/motion.d.ts +72 -0
- package/dist/design/motion.js +38 -0
- package/dist/design/tokens.d.ts +31 -0
- package/dist/design/tokens.js +23 -0
- package/dist/design/typography.d.ts +61 -0
- package/dist/design/typography.js +56 -0
- package/dist/docs/compositions.docblock.d.ts +1 -0
- package/dist/docs/compositions.docblock.js +183 -0
- package/dist/docs/design.docblock.d.ts +1 -0
- package/dist/docs/design.docblock.js +188 -0
- package/dist/docs/generators.docblock.d.ts +1 -0
- package/dist/docs/generators.docblock.js +188 -0
- package/dist/docs/rendering.docblock.d.ts +1 -0
- package/dist/docs/rendering.docblock.js +198 -0
- package/dist/docs/video-gen.docblock.d.ts +1 -0
- package/dist/docs/video-gen.docblock.js +142 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.js +411 -0
- package/dist/generators/scene-planner.d.ts +23 -0
- package/dist/generators/scene-planner.js +200 -0
- package/dist/generators/script-generator.d.ts +49 -0
- package/dist/generators/script-generator.js +142 -0
- package/dist/generators/video-generator.d.ts +20 -0
- package/dist/generators/video-generator.js +409 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1545 -0
- package/dist/node/compositions/api-overview.js +640 -0
- package/dist/node/compositions/index.js +1128 -0
- package/dist/node/compositions/primitives/animated-text.js +139 -0
- package/dist/node/compositions/primitives/brand-frame.js +176 -0
- package/dist/node/compositions/primitives/code-block.js +221 -0
- package/dist/node/compositions/primitives/index.js +651 -0
- package/dist/node/compositions/primitives/progress-bar.js +54 -0
- package/dist/node/compositions/primitives/terminal.js +260 -0
- package/dist/node/compositions/primitives/transition.js +93 -0
- package/dist/node/compositions/social-clip.js +495 -0
- package/dist/node/compositions/terminal-demo.js +553 -0
- package/dist/node/design/index.js +150 -0
- package/dist/node/design/layouts.js +45 -0
- package/dist/node/design/motion.js +38 -0
- package/dist/node/design/tokens.js +23 -0
- package/dist/node/design/typography.js +56 -0
- package/dist/node/docs/compositions.docblock.js +182 -0
- package/dist/node/docs/design.docblock.js +187 -0
- package/dist/node/docs/generators.docblock.js +187 -0
- package/dist/node/docs/rendering.docblock.js +197 -0
- package/dist/node/docs/video-gen.docblock.js +141 -0
- package/dist/node/generators/index.js +411 -0
- package/dist/node/generators/scene-planner.js +200 -0
- package/dist/node/generators/script-generator.js +142 -0
- package/dist/node/generators/video-generator.js +409 -0
- package/dist/node/index.js +1545 -0
- package/dist/node/player/demo-player.js +1131 -0
- package/dist/node/player/index.js +1131 -0
- package/dist/node/remotion/Root.js +1184 -0
- package/dist/node/remotion/index.js +1185 -0
- package/dist/node/renderers/config.js +35 -0
- package/dist/node/renderers/index.js +155 -0
- package/dist/node/renderers/local.js +151 -0
- package/dist/node/types.js +8 -0
- package/dist/player/demo-player.d.ts +55 -0
- package/dist/player/demo-player.js +1131 -0
- package/dist/player/index.d.ts +2 -0
- package/dist/player/index.js +1131 -0
- package/dist/remotion/Root.d.ts +2 -0
- package/dist/remotion/Root.js +1184 -0
- package/dist/remotion/index.d.ts +1 -0
- package/dist/remotion/index.js +1185 -0
- package/dist/renderers/config.d.ts +28 -0
- package/dist/renderers/config.js +35 -0
- package/dist/renderers/index.d.ts +3 -0
- package/dist/renderers/index.js +155 -0
- package/dist/renderers/local.d.ts +17 -0
- package/dist/renderers/local.js +151 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +8 -0
- package/package.json +637 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/docs/video-gen.docblock.ts
|
|
3
|
+
import { registerDocBlocks } from "@contractspec/lib.contracts-spec/docs";
|
|
4
|
+
var videoGenDocBlocks = [
|
|
5
|
+
{
|
|
6
|
+
id: "docs.video-gen.overview",
|
|
7
|
+
title: "Video Generation Library",
|
|
8
|
+
summary: "Programmatic video generation with Remotion -- from content brief to rendered MP4 in a single pipeline.",
|
|
9
|
+
kind: "reference",
|
|
10
|
+
visibility: "public",
|
|
11
|
+
route: "/docs/video-gen/overview",
|
|
12
|
+
tags: ["video", "remotion", "generation", "content-pipeline"],
|
|
13
|
+
owners: ["@contractspec/lib.video-gen"],
|
|
14
|
+
body: `# Video Generation Library
|
|
15
|
+
|
|
16
|
+
\`@contractspec/lib.video-gen\` provides **programmatic video generation** using [Remotion](https://remotion.dev). It follows the same generator pattern as \`@contractspec/lib.content-gen\` and consumes provider contracts from \`@contractspec/lib.contracts-integrations/integrations/providers/video\`.
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
The library is organized into five layers, each buildable and testable independently:
|
|
21
|
+
|
|
22
|
+
\`\`\`
|
|
23
|
+
Content Brief
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
Generators -----> ScenePlanner (brief -> scenes)
|
|
27
|
+
| ScriptGenerator (brief -> narration)
|
|
28
|
+
| VideoGenerator (orchestrator)
|
|
29
|
+
v
|
|
30
|
+
Compositions ---> Primitives (AnimatedText, CodeBlock, Terminal, ...)
|
|
31
|
+
| Full Compositions (ApiOverview, SocialClip, TerminalDemo)
|
|
32
|
+
v
|
|
33
|
+
Design ---------> Tokens, Motion, Typography, Layouts
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
Renderers ------> LocalRenderer (@remotion/renderer)
|
|
37
|
+
DemoPlayer (@remotion/player, web embedding)
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
### Layer Responsibilities
|
|
41
|
+
|
|
42
|
+
| Layer | Import Path | Purpose |
|
|
43
|
+
|-------|-------------|---------|
|
|
44
|
+
| **Types** | \`@contractspec/lib.video-gen/types\` | VideoBrief, ScenePlan, GeneratedVideo, re-exported contract types |
|
|
45
|
+
| **Design** | \`@contractspec/lib.video-gen/design\` | Video-optimized tokens, motion primitives, typography, layouts |
|
|
46
|
+
| **Compositions** | \`@contractspec/lib.video-gen/compositions\` | Remotion components (primitives + full compositions) |
|
|
47
|
+
| **Generators** | \`@contractspec/lib.video-gen/generators\` | VideoGenerator, ScenePlanner, ScriptGenerator |
|
|
48
|
+
| **Renderers** | \`@contractspec/lib.video-gen/renderers\` | LocalRenderer, render config, quality presets |
|
|
49
|
+
| **Player** | \`@contractspec/lib.video-gen/player\` | Embeddable DemoPlayer for web apps |
|
|
50
|
+
| **Remotion** | \`@contractspec/lib.video-gen/remotion\` | Remotion Studio entry point (registerRoot) |
|
|
51
|
+
|
|
52
|
+
## Getting Started
|
|
53
|
+
|
|
54
|
+
### 1. Generate a video project from a content brief
|
|
55
|
+
|
|
56
|
+
\`\`\`ts
|
|
57
|
+
import { VideoGenerator } from "@contractspec/lib.video-gen/generators";
|
|
58
|
+
import { VIDEO_FORMATS } from "@contractspec/lib.video-gen/types";
|
|
59
|
+
import type { VideoBrief } from "@contractspec/lib.video-gen/types";
|
|
60
|
+
|
|
61
|
+
const generator = new VideoGenerator({ fps: 30 });
|
|
62
|
+
|
|
63
|
+
const brief: VideoBrief = {
|
|
64
|
+
content: {
|
|
65
|
+
title: "Ship APIs 10x Faster",
|
|
66
|
+
summary: "ContractSpec generates everything from a single spec.",
|
|
67
|
+
problems: ["Manual API maintenance", "Inconsistent surfaces"],
|
|
68
|
+
solutions: ["One spec, every surface", "Safe regeneration"],
|
|
69
|
+
callToAction: "Try ContractSpec",
|
|
70
|
+
},
|
|
71
|
+
format: VIDEO_FORMATS.landscape,
|
|
72
|
+
targetDurationSeconds: 30,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const project = await generator.generate(brief);
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
### 2. Render to MP4
|
|
79
|
+
|
|
80
|
+
\`\`\`ts
|
|
81
|
+
import { LocalRenderer } from "@contractspec/lib.video-gen/renderers/local";
|
|
82
|
+
|
|
83
|
+
const renderer = new LocalRenderer({
|
|
84
|
+
entryPoint: "./src/remotion/index.ts",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await renderer.render(project, {
|
|
88
|
+
outputPath: "out/video.mp4",
|
|
89
|
+
});
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
### 3. Embed in a web app
|
|
93
|
+
|
|
94
|
+
\`\`\`tsx
|
|
95
|
+
import { DemoPlayer } from "@contractspec/lib.video-gen/player";
|
|
96
|
+
|
|
97
|
+
<DemoPlayer
|
|
98
|
+
compositionId="ApiOverview"
|
|
99
|
+
inputProps={{ specName: "CreateUser", specCode: "..." }}
|
|
100
|
+
controls
|
|
101
|
+
autoPlay
|
|
102
|
+
loop
|
|
103
|
+
/>
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Contract Bridge
|
|
107
|
+
|
|
108
|
+
The library consumes provider contracts defined in \`@contractspec/lib.contracts-integrations\`:
|
|
109
|
+
|
|
110
|
+
| Contract | Purpose |
|
|
111
|
+
|----------|---------|
|
|
112
|
+
| \`VideoProvider\` | Renderer abstraction (local, Lambda, Cloud Run) |
|
|
113
|
+
| \`VideoProject\` | Scene graph with format, fps, audio tracks |
|
|
114
|
+
| \`RenderConfig\` / \`RenderResult\` | Codec, quality, output path, dimensions |
|
|
115
|
+
| \`CompositionRegistry\` | Metadata for registered Remotion compositions |
|
|
116
|
+
| \`VideoFormat\` | Landscape / portrait / square / custom dimensions |
|
|
117
|
+
|
|
118
|
+
Types are re-exported from the main entry for convenience:
|
|
119
|
+
|
|
120
|
+
\`\`\`ts
|
|
121
|
+
import type { VideoProject, RenderConfig } from "@contractspec/lib.video-gen";
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
## Deterministic by Default
|
|
125
|
+
|
|
126
|
+
All generators support two modes:
|
|
127
|
+
|
|
128
|
+
- **Without LLM**: Fully deterministic, template-based output. Same brief always produces the same video project.
|
|
129
|
+
- **With LLM**: Richer scene planning and narration scripts via an optional \`LLMProvider\`. Falls back to deterministic on any failure.
|
|
130
|
+
|
|
131
|
+
This matches the content-gen pattern: \`constructor({ llm? })\` -> \`generate(brief)\`.
|
|
132
|
+
|
|
133
|
+
## Guardrails
|
|
134
|
+
|
|
135
|
+
- Compositions must be **deterministic**: same props = same visual output.
|
|
136
|
+
- Design tokens bridge from \`@contractspec/lib.design-system\` -- do not duplicate brand values.
|
|
137
|
+
- \`LocalRenderer\` requires Node.js (\`@remotion/renderer\` is not Bun-compatible).
|
|
138
|
+
- The \`remotion\` entry point is a side-effect module (calls \`registerRoot\`) -- import it only from Remotion Studio or render scripts.
|
|
139
|
+
`
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
registerDocBlocks(videoGenDocBlocks);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { VideoGenerator } from './video-generator';
|
|
2
|
+
export { ScenePlanner } from './scene-planner';
|
|
3
|
+
export type { ScenePlannerOptions } from './scene-planner';
|
|
4
|
+
export { ScriptGenerator } from './script-generator';
|
|
5
|
+
export type { ScriptGeneratorOptions, NarrationScript, NarrationSegment, } from './script-generator';
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
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,23 @@
|
|
|
1
|
+
import type { LLMProvider } from '@contractspec/lib.contracts-integrations/integrations/providers/llm';
|
|
2
|
+
import type { ScenePlan, VideoBrief } from '../types';
|
|
3
|
+
export interface ScenePlannerOptions {
|
|
4
|
+
llm?: LLMProvider;
|
|
5
|
+
model?: string;
|
|
6
|
+
temperature?: number;
|
|
7
|
+
fps?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class ScenePlanner {
|
|
10
|
+
private readonly llm?;
|
|
11
|
+
private readonly model?;
|
|
12
|
+
private readonly temperature;
|
|
13
|
+
private readonly fps;
|
|
14
|
+
constructor(options?: ScenePlannerOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Plan scenes for a video brief.
|
|
17
|
+
* With LLM: produces richer, context-aware scene breakdowns.
|
|
18
|
+
* Without LLM: deterministic template-based planning.
|
|
19
|
+
*/
|
|
20
|
+
plan(brief: VideoBrief): Promise<ScenePlan>;
|
|
21
|
+
private planDeterministic;
|
|
22
|
+
private planWithLlm;
|
|
23
|
+
}
|