@lssm-tech/lib.video-gen 3.0.10
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 +79 -0
- package/dist/browser/compositions/api-overview.js +1 -0
- package/dist/browser/compositions/index.js +1 -0
- package/dist/browser/compositions/primitives/animated-text.js +1 -0
- package/dist/browser/compositions/primitives/brand-frame.js +1 -0
- package/dist/browser/compositions/primitives/code-block.js +1 -0
- package/dist/browser/compositions/primitives/index.js +1 -0
- package/dist/browser/compositions/primitives/progress-bar.js +1 -0
- package/dist/browser/compositions/primitives/terminal.js +1 -0
- package/dist/browser/compositions/primitives/transition.js +1 -0
- package/dist/browser/compositions/social-clip.js +1 -0
- package/dist/browser/compositions/terminal-demo.js +1 -0
- package/dist/browser/design/index.js +1 -0
- package/dist/browser/design/layouts.js +1 -0
- package/dist/browser/design/motion.js +1 -0
- package/dist/browser/design/tokens.js +1 -0
- package/dist/browser/design/typography.js +1 -0
- package/dist/browser/docs/compositions.docblock.js +1 -0
- package/dist/browser/docs/design.docblock.js +172 -0
- package/dist/browser/docs/generators.docblock.js +165 -0
- package/dist/browser/docs/rendering.docblock.js +175 -0
- package/dist/browser/docs/video-gen.docblock.js +126 -0
- package/dist/browser/generators/index.js +82 -0
- package/dist/browser/generators/scene-planner.js +82 -0
- package/dist/browser/generators/script-generator.js +82 -0
- package/dist/browser/generators/video-generator.js +82 -0
- package/dist/browser/i18n/catalogs/en.js +28 -0
- package/dist/browser/i18n/catalogs/es.js +28 -0
- package/dist/browser/i18n/catalogs/fr.js +28 -0
- package/dist/browser/i18n/catalogs/index.js +82 -0
- package/dist/browser/i18n/index.js +82 -0
- package/dist/browser/i18n/keys.js +1 -0
- package/dist/browser/i18n/locale.js +1 -0
- package/dist/browser/i18n/messages.js +82 -0
- package/dist/browser/index.js +82 -0
- package/dist/browser/player/demo-player.js +1 -0
- package/dist/browser/player/index.js +1 -0
- package/dist/browser/remotion/Root.js +19 -0
- package/dist/browser/remotion/index.js +19 -0
- package/dist/browser/renderers/config.js +1 -0
- package/dist/browser/renderers/index.js +1 -0
- package/dist/browser/renderers/local.js +1 -0
- package/dist/browser/types.js +1 -0
- package/dist/browser/video-gen.feature.js +1 -0
- package/dist/compositions/api-overview.d.ts +16 -0
- package/dist/compositions/api-overview.js +2 -0
- package/dist/compositions/index.d.ts +7 -0
- package/dist/compositions/index.js +2 -0
- package/dist/compositions/primitives/animated-text.d.ts +22 -0
- package/dist/compositions/primitives/animated-text.js +2 -0
- package/dist/compositions/primitives/brand-frame.d.ts +14 -0
- package/dist/compositions/primitives/brand-frame.js +2 -0
- package/dist/compositions/primitives/code-block.d.ts +18 -0
- package/dist/compositions/primitives/code-block.js +2 -0
- package/dist/compositions/primitives/index.d.ts +12 -0
- package/dist/compositions/primitives/index.js +2 -0
- package/dist/compositions/primitives/progress-bar.d.ts +12 -0
- package/dist/compositions/primitives/progress-bar.js +2 -0
- package/dist/compositions/primitives/terminal.d.ts +24 -0
- package/dist/compositions/primitives/terminal.js +2 -0
- package/dist/compositions/primitives/transition.d.ts +14 -0
- package/dist/compositions/primitives/transition.js +2 -0
- package/dist/compositions/social-clip.d.ts +16 -0
- package/dist/compositions/social-clip.js +2 -0
- package/dist/compositions/terminal-demo.d.ts +17 -0
- package/dist/compositions/terminal-demo.js +2 -0
- package/dist/design/index.d.ts +4 -0
- package/dist/design/index.js +2 -0
- package/dist/design/layouts.d.ts +69 -0
- package/dist/design/layouts.js +2 -0
- package/dist/design/motion.d.ts +72 -0
- package/dist/design/motion.js +2 -0
- package/dist/design/tokens.d.ts +31 -0
- package/dist/design/tokens.js +2 -0
- package/dist/design/typography.d.ts +61 -0
- package/dist/design/typography.js +2 -0
- package/dist/docs/compositions.docblock.d.ts +1 -0
- package/dist/docs/compositions.docblock.js +2 -0
- package/dist/docs/design.docblock.d.ts +1 -0
- package/dist/docs/design.docblock.js +173 -0
- package/dist/docs/generators.docblock.d.ts +1 -0
- package/dist/docs/generators.docblock.js +166 -0
- package/dist/docs/rendering.docblock.d.ts +1 -0
- package/dist/docs/rendering.docblock.js +176 -0
- package/dist/docs/video-gen.docblock.d.ts +1 -0
- package/dist/docs/video-gen.docblock.js +127 -0
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.js +83 -0
- package/dist/generators/scene-planner.d.ts +31 -0
- package/dist/generators/scene-planner.js +83 -0
- package/dist/generators/script-generator.d.ts +57 -0
- package/dist/generators/script-generator.js +83 -0
- package/dist/generators/video-generator.d.ts +21 -0
- package/dist/generators/video-generator.js +83 -0
- package/dist/i18n/catalogs/en.d.ts +8 -0
- package/dist/i18n/catalogs/en.js +29 -0
- package/dist/i18n/catalogs/es.d.ts +6 -0
- package/dist/i18n/catalogs/es.js +29 -0
- package/dist/i18n/catalogs/fr.d.ts +6 -0
- package/dist/i18n/catalogs/fr.js +29 -0
- package/dist/i18n/catalogs/index.d.ts +8 -0
- package/dist/i18n/catalogs/index.js +83 -0
- package/dist/i18n/i18n.test.d.ts +1 -0
- package/dist/i18n/index.d.ts +22 -0
- package/dist/i18n/index.js +83 -0
- package/dist/i18n/keys.d.ts +116 -0
- package/dist/i18n/keys.js +2 -0
- package/dist/i18n/locale.d.ts +8 -0
- package/dist/i18n/locale.js +2 -0
- package/dist/i18n/messages.d.ts +14 -0
- package/dist/i18n/messages.js +83 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +83 -0
- package/dist/node/compositions/api-overview.js +1 -0
- package/dist/node/compositions/index.js +1 -0
- package/dist/node/compositions/primitives/animated-text.js +1 -0
- package/dist/node/compositions/primitives/brand-frame.js +1 -0
- package/dist/node/compositions/primitives/code-block.js +1 -0
- package/dist/node/compositions/primitives/index.js +1 -0
- package/dist/node/compositions/primitives/progress-bar.js +1 -0
- package/dist/node/compositions/primitives/terminal.js +1 -0
- package/dist/node/compositions/primitives/transition.js +1 -0
- package/dist/node/compositions/social-clip.js +1 -0
- package/dist/node/compositions/terminal-demo.js +1 -0
- package/dist/node/design/index.js +1 -0
- package/dist/node/design/layouts.js +1 -0
- package/dist/node/design/motion.js +1 -0
- package/dist/node/design/tokens.js +1 -0
- package/dist/node/design/typography.js +1 -0
- package/dist/node/docs/compositions.docblock.js +1 -0
- package/dist/node/docs/design.docblock.js +172 -0
- package/dist/node/docs/generators.docblock.js +165 -0
- package/dist/node/docs/rendering.docblock.js +175 -0
- package/dist/node/docs/video-gen.docblock.js +126 -0
- package/dist/node/generators/index.js +82 -0
- package/dist/node/generators/scene-planner.js +82 -0
- package/dist/node/generators/script-generator.js +82 -0
- package/dist/node/generators/video-generator.js +82 -0
- package/dist/node/i18n/catalogs/en.js +28 -0
- package/dist/node/i18n/catalogs/es.js +28 -0
- package/dist/node/i18n/catalogs/fr.js +28 -0
- package/dist/node/i18n/catalogs/index.js +82 -0
- package/dist/node/i18n/index.js +82 -0
- package/dist/node/i18n/keys.js +1 -0
- package/dist/node/i18n/locale.js +1 -0
- package/dist/node/i18n/messages.js +82 -0
- package/dist/node/index.js +82 -0
- package/dist/node/player/demo-player.js +1 -0
- package/dist/node/player/index.js +1 -0
- package/dist/node/remotion/Root.js +19 -0
- package/dist/node/remotion/index.js +19 -0
- package/dist/node/renderers/config.js +1 -0
- package/dist/node/renderers/index.js +1 -0
- package/dist/node/renderers/local.js +1 -0
- package/dist/node/types.js +1 -0
- package/dist/node/video-gen.feature.js +1 -0
- package/dist/player/demo-player.d.ts +55 -0
- package/dist/player/demo-player.js +2 -0
- package/dist/player/index.d.ts +2 -0
- package/dist/player/index.js +2 -0
- package/dist/remotion/Root.d.ts +2 -0
- package/dist/remotion/Root.js +20 -0
- package/dist/remotion/index.d.ts +1 -0
- package/dist/remotion/index.js +20 -0
- package/dist/renderers/config.d.ts +28 -0
- package/dist/renderers/config.js +2 -0
- package/dist/renderers/index.d.ts +3 -0
- package/dist/renderers/index.js +2 -0
- package/dist/renderers/local.d.ts +17 -0
- package/dist/renderers/local.js +2 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/dist/video-gen.feature.d.ts +1 -0
- package/dist/video-gen.feature.js +2 -0
- package/package.json +675 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{registerDocBlocks as f}from"@contractspec/lib.contracts-spec/docs";var h=[{id:"docs.video-gen.rendering",title:"Video Rendering & Playback",summary:"LocalRenderer for MP4 output, render configuration, quality presets, DemoPlayer for web embedding, and Remotion Studio setup.",kind:"reference",visibility:"public",route:"/docs/video-gen/rendering",tags:["video","rendering","remotion","player","mp4","quality-presets"],owners:["@contractspec/lib.video-gen"],body:`# Video Rendering & Playback
|
|
3
|
+
|
|
4
|
+
The rendering layer wraps \`@remotion/renderer\` for MP4/WebM output and \`@remotion/player\` for interactive web embedding. It implements the \`VideoProvider\` contract from \`@contractspec/lib.contracts-integrations\`.
|
|
5
|
+
|
|
6
|
+
## LocalRenderer
|
|
7
|
+
|
|
8
|
+
Renders a \`VideoProject\` to a video file using the local Remotion renderer. Works with **Node.js** and **Bun** (via \`remotionb\`). Bun caveat: SSR scripts may not auto-quit (see [Remotion Bun docs](https://www.remotion.dev/docs/bun)).
|
|
9
|
+
|
|
10
|
+
\`\`\`ts
|
|
11
|
+
import { LocalRenderer } from "@contractspec/lib.video-gen/renderers/local";
|
|
12
|
+
|
|
13
|
+
const renderer = new LocalRenderer({
|
|
14
|
+
entryPoint: "./src/remotion/index.ts",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const result = await renderer.render(project, {
|
|
18
|
+
outputPath: "out/video.mp4",
|
|
19
|
+
codec: "h264",
|
|
20
|
+
crf: 18,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
result.outputPath; // "out/video.mp4"
|
|
24
|
+
result.format; // "mp4"
|
|
25
|
+
result.durationSeconds; // total duration
|
|
26
|
+
result.fileSizeBytes; // file size
|
|
27
|
+
result.dimensions; // { width: 1920, height: 1080 }
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
> **Important**: Import \`LocalRenderer\` from the \`/renderers/local\` subpath, not from the main entry. It dynamically imports \`@remotion/bundler\` and \`@remotion/renderer\` to avoid bundling them in browser builds.
|
|
31
|
+
|
|
32
|
+
### Auto-Variants
|
|
33
|
+
|
|
34
|
+
Set \`autoVariants: true\` to generate landscape + square + portrait versions:
|
|
35
|
+
|
|
36
|
+
\`\`\`ts
|
|
37
|
+
const result = await renderer.render(project, {
|
|
38
|
+
outputPath: "out/video.mp4",
|
|
39
|
+
autoVariants: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
result.variants; // [
|
|
43
|
+
// { outputPath: "out/video-square.mp4", dimensions: { width: 1080, height: 1080 } },
|
|
44
|
+
// { outputPath: "out/video-portrait.mp4", dimensions: { width: 1080, height: 1920 } },
|
|
45
|
+
// ]
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
## Render Configuration
|
|
49
|
+
|
|
50
|
+
### Defaults
|
|
51
|
+
|
|
52
|
+
\`\`\`ts
|
|
53
|
+
import {
|
|
54
|
+
defaultRenderConfig,
|
|
55
|
+
resolveRenderConfig,
|
|
56
|
+
qualityPresets,
|
|
57
|
+
codecFormatMap,
|
|
58
|
+
} from "@contractspec/lib.video-gen/renderers/config";
|
|
59
|
+
|
|
60
|
+
defaultRenderConfig.codec; // "h264"
|
|
61
|
+
defaultRenderConfig.outputFormat; // "mp4"
|
|
62
|
+
defaultRenderConfig.crf; // 18
|
|
63
|
+
defaultRenderConfig.pixelFormat; // "yuv420p"
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
### RenderConfig Options
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
|--------|------|---------|-------------|
|
|
70
|
+
| \`outputPath\` | \`string\` | -- | **Required**. Output file path |
|
|
71
|
+
| \`codec\` | \`"h264" \\| "h265" \\| "vp8" \\| "vp9"\` | \`"h264"\` | Video codec |
|
|
72
|
+
| \`outputFormat\` | \`"mp4" \\| "webm" \\| "gif"\` | \`"mp4"\` | Container format |
|
|
73
|
+
| \`crf\` | \`number\` | \`18\` | Constant Rate Factor (lower = better quality) |
|
|
74
|
+
| \`pixelFormat\` | \`string\` | \`"yuv420p"\` | Pixel format |
|
|
75
|
+
| \`concurrency\` | \`number\` | CPU count | Rendering threads |
|
|
76
|
+
| \`autoVariants\` | \`boolean\` | \`false\` | Generate format variants |
|
|
77
|
+
|
|
78
|
+
### Quality Presets
|
|
79
|
+
|
|
80
|
+
\`\`\`ts
|
|
81
|
+
import { resolveRenderConfig } from "@contractspec/lib.video-gen/renderers/config";
|
|
82
|
+
|
|
83
|
+
// Draft (fastest, for previews)
|
|
84
|
+
resolveRenderConfig({ outputPath: "out/preview.mp4" }, "draft");
|
|
85
|
+
// -> crf: 28, concurrency: 1
|
|
86
|
+
|
|
87
|
+
// Standard (balanced)
|
|
88
|
+
resolveRenderConfig({ outputPath: "out/video.mp4" }, "standard");
|
|
89
|
+
// -> crf: 18
|
|
90
|
+
|
|
91
|
+
// High (best quality, for final output)
|
|
92
|
+
resolveRenderConfig({ outputPath: "out/final.mp4" }, "high");
|
|
93
|
+
// -> crf: 12
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
### Codec-to-Format Mapping
|
|
97
|
+
|
|
98
|
+
| Codec | Format |
|
|
99
|
+
|-------|--------|
|
|
100
|
+
| \`h264\` | \`mp4\` |
|
|
101
|
+
| \`h265\` | \`mp4\` |
|
|
102
|
+
| \`vp8\` | \`webm\` |
|
|
103
|
+
| \`vp9\` | \`webm\` |
|
|
104
|
+
|
|
105
|
+
## DemoPlayer (Web Embedding)
|
|
106
|
+
|
|
107
|
+
Embeddable Remotion Player for interactive video demos in React apps. Wraps \`@remotion/player\` with ContractSpec compositions.
|
|
108
|
+
|
|
109
|
+
\`\`\`tsx
|
|
110
|
+
import { DemoPlayer } from "@contractspec/lib.video-gen/player";
|
|
111
|
+
|
|
112
|
+
<DemoPlayer
|
|
113
|
+
compositionId="ApiOverview"
|
|
114
|
+
inputProps={{
|
|
115
|
+
specName: "CreateUser",
|
|
116
|
+
specCode: "export const createUser = defineCommand({...})",
|
|
117
|
+
}}
|
|
118
|
+
controls
|
|
119
|
+
autoPlay
|
|
120
|
+
loop
|
|
121
|
+
width="100%"
|
|
122
|
+
clickToPlay
|
|
123
|
+
doubleClickToFullscreen
|
|
124
|
+
/>
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
### DemoPlayer Props
|
|
128
|
+
|
|
129
|
+
| Prop | Type | Default | Description |
|
|
130
|
+
|------|------|---------|-------------|
|
|
131
|
+
| \`compositionId\` | \`"ApiOverview" \\| "SocialClip" \\| "TerminalDemo"\` | -- | Composition to play |
|
|
132
|
+
| \`inputProps\` | composition props type | -- | Props for the selected composition |
|
|
133
|
+
| \`controls\` | \`boolean\` | \`true\` | Show playback controls |
|
|
134
|
+
| \`autoPlay\` | \`boolean\` | \`false\` | Auto-play on mount |
|
|
135
|
+
| \`loop\` | \`boolean\` | \`false\` | Loop playback |
|
|
136
|
+
| \`width\` | \`string \\| number\` | \`"100%"\` | Player width |
|
|
137
|
+
| \`height\` | \`string \\| number\` | \`"auto"\` | Player height |
|
|
138
|
+
| \`clickToPlay\` | \`boolean\` | \`true\` | Click to toggle playback |
|
|
139
|
+
| \`doubleClickToFullscreen\` | \`boolean\` | \`true\` | Double-click for fullscreen |
|
|
140
|
+
|
|
141
|
+
> \`@remotion/player\` is a peer dependency. Install it in your app if you use \`DemoPlayer\`.
|
|
142
|
+
|
|
143
|
+
## Remotion Studio
|
|
144
|
+
|
|
145
|
+
The \`@contractspec/app.video-studio\` package provides a Remotion Studio entry point for previewing compositions interactively.
|
|
146
|
+
|
|
147
|
+
\`\`\`bash
|
|
148
|
+
# Start Remotion Studio
|
|
149
|
+
bun run dev:video
|
|
150
|
+
|
|
151
|
+
# Render a specific composition
|
|
152
|
+
bunx remotionb render src/index.ts ApiOverview out/api-overview.mp4
|
|
153
|
+
|
|
154
|
+
# Render all compositions
|
|
155
|
+
bun run render:all
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
### Registered Compositions
|
|
159
|
+
|
|
160
|
+
| ID | Component | Dimensions | Duration | Description |
|
|
161
|
+
|----|-----------|------------|----------|-------------|
|
|
162
|
+
| \`ApiOverview\` | \`ApiOverview\` | 1920x1080 | 450 frames (15s) | Homepage API demo |
|
|
163
|
+
| \`SocialClip\` | \`SocialClip\` | 1920x1080 | 300 frames (10s) | Landscape social clip |
|
|
164
|
+
| \`SocialClipSquare\` | \`SocialClip\` | 1080x1080 | 300 frames (10s) | Square social clip |
|
|
165
|
+
| \`SocialClipPortrait\` | \`SocialClip\` | 1080x1920 | 300 frames (10s) | Portrait social clip |
|
|
166
|
+
| \`TerminalDemo\` | \`TerminalDemo\` | 1920x1080 | 600 frames (20s) | CLI walkthrough |
|
|
167
|
+
|
|
168
|
+
All compositions run at 30fps.
|
|
169
|
+
|
|
170
|
+
## Guardrails
|
|
171
|
+
|
|
172
|
+
- \`LocalRenderer\` requires Node.js or Bun -- do not attempt to use it in browser environments. On Bun, SSR scripts may not auto-quit (see [Remotion Bun docs](https://www.remotion.dev/docs/bun)).
|
|
173
|
+
- Import \`LocalRenderer\` from the \`/renderers/local\` subpath to avoid bundling \`@remotion/renderer\` in browser builds.
|
|
174
|
+
- The \`remotion\` entry point (\`@contractspec/lib.video-gen/remotion\`) is a **side-effect module** that calls \`registerRoot()\`. Only import it from Remotion Studio or render scripts.
|
|
175
|
+
- Use quality presets for consistency: \`draft\` for development, \`standard\` for CI, \`high\` for releases.
|
|
176
|
+
`}];f(h);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{registerDocBlocks as f}from"@contractspec/lib.contracts-spec/docs";var h=[{id:"docs.video-gen.overview",title:"Video Generation Library",summary:"Programmatic video generation with Remotion -- from content brief to rendered MP4 in a single pipeline.",kind:"reference",visibility:"public",route:"/docs/video-gen/overview",tags:["video","remotion","generation","content-pipeline"],owners:["@contractspec/lib.video-gen"],body:`# Video Generation Library
|
|
3
|
+
|
|
4
|
+
\`@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\`.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
The library is organized into five layers, each buildable and testable independently:
|
|
9
|
+
|
|
10
|
+
\`\`\`
|
|
11
|
+
Content Brief
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
Generators -----> ScenePlanner (brief -> scenes)
|
|
15
|
+
| ScriptGenerator (brief -> narration)
|
|
16
|
+
| VideoGenerator (orchestrator)
|
|
17
|
+
v
|
|
18
|
+
Compositions ---> Primitives (AnimatedText, CodeBlock, Terminal, ...)
|
|
19
|
+
| Full Compositions (ApiOverview, SocialClip, TerminalDemo)
|
|
20
|
+
v
|
|
21
|
+
Design ---------> Tokens, Motion, Typography, Layouts
|
|
22
|
+
|
|
|
23
|
+
v
|
|
24
|
+
Renderers ------> LocalRenderer (@remotion/renderer)
|
|
25
|
+
DemoPlayer (@remotion/player, web embedding)
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
### Layer Responsibilities
|
|
29
|
+
|
|
30
|
+
| Layer | Import Path | Purpose |
|
|
31
|
+
|-------|-------------|---------|
|
|
32
|
+
| **Types** | \`@contractspec/lib.video-gen/types\` | VideoBrief, ScenePlan, GeneratedVideo, re-exported contract types |
|
|
33
|
+
| **Design** | \`@contractspec/lib.video-gen/design\` | Video-optimized tokens, motion primitives, typography, layouts |
|
|
34
|
+
| **Compositions** | \`@contractspec/lib.video-gen/compositions\` | Remotion components (primitives + full compositions) |
|
|
35
|
+
| **Generators** | \`@contractspec/lib.video-gen/generators\` | VideoGenerator, ScenePlanner, ScriptGenerator |
|
|
36
|
+
| **Renderers** | \`@contractspec/lib.video-gen/renderers\` | LocalRenderer, render config, quality presets |
|
|
37
|
+
| **Player** | \`@contractspec/lib.video-gen/player\` | Embeddable DemoPlayer for web apps |
|
|
38
|
+
| **Remotion** | \`@contractspec/lib.video-gen/remotion\` | Remotion Studio entry point (registerRoot) |
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
### 1. Generate a video project from a content brief
|
|
43
|
+
|
|
44
|
+
\`\`\`ts
|
|
45
|
+
import { VideoGenerator } from "@contractspec/lib.video-gen/generators";
|
|
46
|
+
import { VIDEO_FORMATS } from "@contractspec/lib.video-gen/types";
|
|
47
|
+
import type { VideoBrief } from "@contractspec/lib.video-gen/types";
|
|
48
|
+
|
|
49
|
+
const generator = new VideoGenerator({ fps: 30 });
|
|
50
|
+
|
|
51
|
+
const brief: VideoBrief = {
|
|
52
|
+
content: {
|
|
53
|
+
title: "Ship APIs 10x Faster",
|
|
54
|
+
summary: "ContractSpec generates everything from a single spec.",
|
|
55
|
+
problems: ["Manual API maintenance", "Inconsistent surfaces"],
|
|
56
|
+
solutions: ["One spec, every surface", "Safe regeneration"],
|
|
57
|
+
callToAction: "Try ContractSpec",
|
|
58
|
+
},
|
|
59
|
+
format: VIDEO_FORMATS.landscape,
|
|
60
|
+
targetDurationSeconds: 30,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const project = await generator.generate(brief);
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
### 2. Render to MP4
|
|
67
|
+
|
|
68
|
+
\`\`\`ts
|
|
69
|
+
import { LocalRenderer } from "@contractspec/lib.video-gen/renderers/local";
|
|
70
|
+
|
|
71
|
+
const renderer = new LocalRenderer({
|
|
72
|
+
entryPoint: "./src/remotion/index.ts",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await renderer.render(project, {
|
|
76
|
+
outputPath: "out/video.mp4",
|
|
77
|
+
});
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
### 3. Embed in a web app
|
|
81
|
+
|
|
82
|
+
\`\`\`tsx
|
|
83
|
+
import { DemoPlayer } from "@contractspec/lib.video-gen/player";
|
|
84
|
+
|
|
85
|
+
<DemoPlayer
|
|
86
|
+
compositionId="ApiOverview"
|
|
87
|
+
inputProps={{ specName: "CreateUser", specCode: "..." }}
|
|
88
|
+
controls
|
|
89
|
+
autoPlay
|
|
90
|
+
loop
|
|
91
|
+
/>
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
## Contract Bridge
|
|
95
|
+
|
|
96
|
+
The library consumes provider contracts defined in \`@contractspec/lib.contracts-integrations\`:
|
|
97
|
+
|
|
98
|
+
| Contract | Purpose |
|
|
99
|
+
|----------|---------|
|
|
100
|
+
| \`VideoProvider\` | Renderer abstraction (local, Lambda, Cloud Run) |
|
|
101
|
+
| \`VideoProject\` | Scene graph with format, fps, audio tracks |
|
|
102
|
+
| \`RenderConfig\` / \`RenderResult\` | Codec, quality, output path, dimensions |
|
|
103
|
+
| \`CompositionRegistry\` | Metadata for registered Remotion compositions |
|
|
104
|
+
| \`VideoFormat\` | Landscape / portrait / square / custom dimensions |
|
|
105
|
+
|
|
106
|
+
Types are re-exported from the main entry for convenience:
|
|
107
|
+
|
|
108
|
+
\`\`\`ts
|
|
109
|
+
import type { VideoProject, RenderConfig } from "@contractspec/lib.video-gen";
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## Deterministic by Default
|
|
113
|
+
|
|
114
|
+
All generators support two modes:
|
|
115
|
+
|
|
116
|
+
- **Without LLM**: Fully deterministic, template-based output. Same brief always produces the same video project.
|
|
117
|
+
- **With LLM**: Richer scene planning and narration scripts via an optional \`LLMProvider\`. Falls back to deterministic on any failure.
|
|
118
|
+
|
|
119
|
+
This matches the content-gen pattern: \`constructor({ llm? })\` -> \`generate(brief)\`.
|
|
120
|
+
|
|
121
|
+
## Guardrails
|
|
122
|
+
|
|
123
|
+
- Compositions must be **deterministic**: same props = same visual output.
|
|
124
|
+
- Design tokens bridge from \`@contractspec/lib.design-system\` -- do not duplicate brand values.
|
|
125
|
+
- \`LocalRenderer\` works with Node.js and Bun. On Bun, SSR scripts may not auto-quit (see [Remotion Bun docs](https://www.remotion.dev/docs/bun)).
|
|
126
|
+
- The \`remotion\` entry point is a side-effect module (calls \`registerRoot\`) -- import it only from Remotion Studio or render scripts.
|
|
127
|
+
`}];f(h);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var d=import.meta.require;import{VIDEO_FORMATS as W}from"@contractspec/lib.contracts-integrations/integrations/providers/video";var Q=30,J={horizontal:120,vertical:80,contentWidth:1680,contentHeight:920};function b(x){let G=x.width/1920,K=x.height/1080;return{horizontal:Math.round(J.horizontal*G),vertical:Math.round(J.vertical*K),contentWidth:Math.round(J.contentWidth*G),contentHeight:Math.round(J.contentHeight*K)}}var p={center:{x:960,y:540},topLeft:{x:120,y:80},topRight:{x:1800,y:80},bottomLeft:{x:120,y:1000},bottomRight:{x:1800,y:1000},bottomCenter:{x:960,y:960}};function r(){return[W.landscape,W.square,W.portrait]}import{defineTranslation as M}from"@contractspec/lib.contracts-spec/translations";var $=M({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"All user-facing, LLM-facing, and developer-facing strings for the video-gen package",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"prompt.script.system":{value:`You are a video narration script writer.
|
|
3
|
+
Write a narration script for a short video (30-60 seconds).
|
|
4
|
+
{styleGuide}
|
|
5
|
+
|
|
6
|
+
Return JSON with shape:
|
|
7
|
+
{
|
|
8
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
9
|
+
"fullText": string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Scene IDs should be: "intro", "problems", "solutions", "metrics", "cta".
|
|
13
|
+
Only include segments that are relevant to the brief content.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`You are a video scene planner for ContractSpec marketing/documentation videos.
|
|
14
|
+
Given a content brief, break it into video scenes.
|
|
15
|
+
|
|
16
|
+
Each scene must have:
|
|
17
|
+
- compositionId: one of "ApiOverview", "SocialClip", "TerminalDemo"
|
|
18
|
+
- props: the input props for that composition (see type definitions)
|
|
19
|
+
- durationInFrames: duration at {fps}fps
|
|
20
|
+
- narrationText: what the narrator says during this scene
|
|
21
|
+
|
|
22
|
+
Return a JSON object with shape:
|
|
23
|
+
{
|
|
24
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
25
|
+
"narrationScript": string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Keep the total duration around {targetSeconds} seconds.
|
|
29
|
+
Prioritize clarity and pacing. Each scene should communicate one idea.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Use a clear, authoritative, professional tone. Be concise and direct.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Use a friendly, conversational tone. Be approachable and relatable.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Use precise technical language. Be detailed and accurate.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"The challenge: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"The solution: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"The results: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"Learn more",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"The Problem",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"The problem: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"The Solution",description:"Scene hook label for solution"},"scene.narration.solution":{value:"The solution: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"Results",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"Generates:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"One spec. Every surface.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"REST Endpoint",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"GraphQL Mutation",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Prisma Model",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"TypeScript SDK",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"MCP Tool",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"OpenAPI Spec",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"Learn more",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});import{defineTranslation as O}from"@contractspec/lib.contracts-spec/translations";var I=O({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"Spanish translations for the video-gen package",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"prompt.script.system":{value:`Eres un redactor de guiones de narraci\xF3n para v\xEDdeo.
|
|
30
|
+
Escribe un gui\xF3n de narraci\xF3n para un v\xEDdeo corto (30-60 segundos).
|
|
31
|
+
{styleGuide}
|
|
32
|
+
|
|
33
|
+
Devuelve JSON con la forma:
|
|
34
|
+
{
|
|
35
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
36
|
+
"fullText": string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Los identificadores de escena deben ser: "intro", "problems", "solutions", "metrics", "cta".
|
|
40
|
+
Incluye solo los segmentos relevantes para el brief.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`Eres un planificador de escenas de v\xEDdeo para v\xEDdeos de marketing/documentaci\xF3n de ContractSpec.
|
|
41
|
+
Dado un brief de contenido, div\xEDdelo en escenas de v\xEDdeo.
|
|
42
|
+
|
|
43
|
+
Cada escena debe tener:
|
|
44
|
+
- compositionId: uno de "ApiOverview", "SocialClip", "TerminalDemo"
|
|
45
|
+
- props: las propiedades de entrada de esa composici\xF3n
|
|
46
|
+
- durationInFrames: duraci\xF3n a {fps} fps
|
|
47
|
+
- narrationText: lo que dice el narrador durante esta escena
|
|
48
|
+
|
|
49
|
+
Devuelve un objeto JSON con la forma:
|
|
50
|
+
{
|
|
51
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
52
|
+
"narrationScript": string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Mant\xE9n la duraci\xF3n total alrededor de {targetSeconds} segundos.
|
|
56
|
+
Prioriza la claridad y el ritmo. Cada escena debe comunicar una idea.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Usa un tono claro, autoritario y profesional. S\xE9 conciso y directo.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Usa un tono amigable y conversacional. S\xE9 accesible y cercano.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Usa un lenguaje t\xE9cnico preciso. S\xE9 detallado y exacto.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"El desaf\xEDo: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"La soluci\xF3n: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"Los resultados: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"M\xE1s informaci\xF3n",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"El problema",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"El problema: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"La soluci\xF3n",description:"Scene hook label for solution"},"scene.narration.solution":{value:"La soluci\xF3n: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"Resultados",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"Genera:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"Una spec. Todas las superficies.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"Endpoint REST",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"Mutaci\xF3n GraphQL",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Modelo Prisma",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"SDK TypeScript",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"Herramienta MCP",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"Spec OpenAPI",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"M\xE1s informaci\xF3n",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});import{defineTranslation as S}from"@contractspec/lib.contracts-spec/translations";var Y=S({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"French translations for the video-gen package",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"prompt.script.system":{value:`Vous \xEAtes un r\xE9dacteur de scripts de narration vid\xE9o.
|
|
57
|
+
\xC9crivez un script de narration pour une courte vid\xE9o (30-60 secondes).
|
|
58
|
+
{styleGuide}
|
|
59
|
+
|
|
60
|
+
Retournez du JSON avec la forme\xA0:
|
|
61
|
+
{
|
|
62
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
63
|
+
"fullText": string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Les identifiants de sc\xE8ne doivent \xEAtre\xA0: "intro", "problems", "solutions", "metrics", "cta".
|
|
67
|
+
N'incluez que les segments pertinents au brief.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`Vous \xEAtes un planificateur de sc\xE8nes vid\xE9o pour les vid\xE9os marketing/documentation de ContractSpec.
|
|
68
|
+
\xC0 partir d'un brief, d\xE9composez-le en sc\xE8nes vid\xE9o.
|
|
69
|
+
|
|
70
|
+
Chaque sc\xE8ne doit avoir\xA0:
|
|
71
|
+
- compositionId\xA0: un parmi "ApiOverview", "SocialClip", "TerminalDemo"
|
|
72
|
+
- props\xA0: les propri\xE9t\xE9s d'entr\xE9e de cette composition
|
|
73
|
+
- durationInFrames\xA0: dur\xE9e \xE0 {fps}\xA0fps
|
|
74
|
+
- narrationText\xA0: ce que le narrateur dit pendant cette sc\xE8ne
|
|
75
|
+
|
|
76
|
+
Retournez un objet JSON avec la forme\xA0:
|
|
77
|
+
{
|
|
78
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
79
|
+
"narrationScript": string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Gardez la dur\xE9e totale autour de {targetSeconds} secondes.
|
|
83
|
+
Privil\xE9giez la clart\xE9 et le rythme. Chaque sc\xE8ne doit communiquer une id\xE9e.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Utilisez un ton clair, autoritaire et professionnel. Soyez concis et direct.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Utilisez un ton amical et conversationnel. Soyez accessible et proche.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Utilisez un langage technique pr\xE9cis. Soyez d\xE9taill\xE9 et exact.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"Le d\xE9fi\xA0: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"La solution\xA0: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"Les r\xE9sultats\xA0: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"En savoir plus",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"Le probl\xE8me",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"Le probl\xE8me\xA0: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"La solution",description:"Scene hook label for solution"},"scene.narration.solution":{value:"La solution\xA0: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"R\xE9sultats",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"G\xE9n\xE8re\xA0:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"Un spec. Toutes les surfaces.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"Endpoint REST",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"Mutation GraphQL",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Mod\xE8le Prisma",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"SDK TypeScript",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"Outil MCP",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"Spec OpenAPI",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"En savoir plus",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});var X={"prompt.script.system":"prompt.script.system","prompt.scenePlanner.system":"prompt.scenePlanner.system","prompt.style.professional":"prompt.style.professional","prompt.style.casual":"prompt.style.casual","prompt.style.technical":"prompt.style.technical"},h={"script.segment.challenge":"script.segment.challenge","script.segment.solution":"script.segment.solution","script.segment.results":"script.segment.results"},R={"scene.cta.default":"scene.cta.default","scene.hook.problem":"scene.hook.problem","scene.narration.problem":"scene.narration.problem","scene.hook.solution":"scene.hook.solution","scene.narration.solution":"scene.narration.solution","scene.hook.results":"scene.hook.results"},U={"composition.apiOverview.generates":"composition.apiOverview.generates","composition.apiOverview.tagline":"composition.apiOverview.tagline","composition.apiOverview.output.rest":"composition.apiOverview.output.rest","composition.apiOverview.output.graphql":"composition.apiOverview.output.graphql","composition.apiOverview.output.prisma":"composition.apiOverview.output.prisma","composition.apiOverview.output.typescript":"composition.apiOverview.output.typescript","composition.apiOverview.output.mcp":"composition.apiOverview.output.mcp","composition.apiOverview.output.openapi":"composition.apiOverview.output.openapi","composition.socialClip.cta":"composition.socialClip.cta","composition.terminal.title":"composition.terminal.title"},v={...X,...h,...R,...U};import{DEFAULT_LOCALE as A,isSupportedLocale as D,resolveLocale as m,SUPPORTED_LOCALES as T}from"@contractspec/lib.contracts-spec/translations";import{createI18nFactory as g}from"@contractspec/lib.contracts-spec/translations";var y=g({specKey:"video-gen.messages",catalogs:[$,Y,I]}),E=y.create,j=y.getDefault,F=y.resetRegistry;class V{llm;model;temperature;fps;i18n;modelSelector;selectionContext;constructor(x){this.llm=x?.llm,this.model=x?.model,this.temperature=x?.temperature??0.3,this.fps=x?.fps??Q,this.i18n=E(x?.locale),this.modelSelector=x?.modelSelector,this.selectionContext=x?.selectionContext}async plan(x){if(this.llm)return this.planWithLlm(x);return this.planDeterministic(x)}planDeterministic(x){let{content:G}=x,{t:K}=this.i18n,L=[],z=this.fps;if(L.push({compositionId:"SocialClip",props:{hook:G.title,message:G.summary,points:G.solutions.slice(0,3),cta:G.callToAction??K("scene.cta.default")},durationInFrames:3*z,narrationText:`${G.title}. ${G.summary}`}),G.problems.length>0)L.push({compositionId:"SocialClip",props:{hook:K("scene.hook.problem"),message:G.problems[0]??"",points:G.problems.slice(1,4)},durationInFrames:4*z,narrationText:K("scene.narration.problem",{content:G.problems.join(". ")})});if(G.solutions.length>0)L.push({compositionId:"SocialClip",props:{hook:K("scene.hook.solution"),message:G.solutions[0]??"",points:G.solutions.slice(1,4)},durationInFrames:5*z,narrationText:K("scene.narration.solution",{content:G.solutions.join(". ")})});if(G.metrics&&G.metrics.length>0)L.push({compositionId:"SocialClip",props:{hook:K("scene.hook.results"),message:G.metrics[0]??"",points:G.metrics.slice(1,3)},durationInFrames:3*z,narrationText:G.metrics.join(". ")});if(G.callToAction)L.push({compositionId:"SocialClip",props:{hook:G.callToAction,message:"",cta:G.callToAction},durationInFrames:2*z,narrationText:G.callToAction});if(x.targetDurationSeconds){let N=x.targetDurationSeconds*z,q=L.reduce((C,k)=>C+k.durationInFrames,0),H=N/q;for(let C of L)C.durationInFrames=Math.round(C.durationInFrames*H)}let w=L.reduce((N,q)=>N+q.durationInFrames,0),B=L.filter((N)=>N.narrationText).map((N)=>N.narrationText).join(" ");return{scenes:L,estimatedDurationSeconds:w/z,narrationScript:B}}async resolveModel(){if(this.model)return this.model;if(this.modelSelector){let x=this.selectionContext??{taskDimension:"reasoning"};return(await this.modelSelector.select(x)).modelId}return}async planWithLlm(x){let{t:G}=this.i18n,K=[{role:"system",content:[{type:"text",text:G("prompt.scenePlanner.system",{fps:this.fps,targetSeconds:x.targetDurationSeconds??30})}]},{role:"user",content:[{type:"text",text:JSON.stringify(x.content)}]}];if(!this.llm)return this.planDeterministic(x);try{let L=await this.resolveModel(),w=(await this.llm.chat(K,{model:L,temperature:this.temperature,responseFormat:"json"})).message.content.find((q)=>q.type==="text");if(!w||w.type!=="text")return this.planDeterministic(x);let B=JSON.parse(w.text),N=B.scenes.reduce((q,H)=>q+H.durationInFrames,0);return{scenes:B.scenes,estimatedDurationSeconds:N/this.fps,narrationScript:B.narrationScript}}catch{return this.planDeterministic(x)}}}import{VIDEO_FORMATS as P}from"@contractspec/lib.contracts-integrations/integrations/providers/video";class Z{scenePlanner;voice;transcriber;image;fps;constructor(x){this.fps=x?.fps??Q,this.voice=x?.voice,this.transcriber=x?.transcriber,this.image=x?.image,this.scenePlanner=new V({llm:x?.llm,model:x?.model,temperature:x?.temperature,fps:this.fps,locale:x?.locale,modelSelector:x?.modelSelector,selectionContext:x?.selectionContext})}async generate(x){let G=await this.scenePlanner.plan(x),K=G.scenes.map((C,k)=>({id:`scene-${k}`,compositionId:C.compositionId,props:C.props,durationInFrames:C.durationInFrames,narrationText:C.narrationText})),L,z;if(x.narration?.enabled&&this.voice)try{if(L=await this.voice.synthesizeForVideo({content:x.content,scenePlan:{scenes:K.map((C)=>({id:C.id,compositionId:C.compositionId,durationInFrames:C.durationInFrames,narrationText:C.narrationText})),estimatedDurationSeconds:G.estimatedDurationSeconds},voice:{voiceId:x.narration.voiceId??""},pacing:{strategy:"scene-matched"},fps:this.fps}),L.timingMap)for(let C of L.timingMap.segments){let k=K.find((_)=>_.id===C.sceneId);if(k)k.durationInFrames=C.recommendedSceneDurationInFrames}if(L.assembledAudio)z={data:L.assembledAudio.data,format:L.assembledAudio.format==="wav"?"wav":"mp3",durationSeconds:(L.assembledAudio.durationMs??0)/1000,volume:1}}catch{}let w;if(this.transcriber&&L?.assembledAudio)try{w=(await this.transcriber.transcribe({audio:L.assembledAudio,subtitleFormat:"vtt"})).subtitles}catch{}let B;if(this.image)try{let C=await this.image.generate({content:x.content,purpose:"video-thumbnail",format:"png",style:"photorealistic"}),k=C.results?.images[0]?.url;B={prompt:C.prompt.text,...k?{imageUrl:k}:{}}}catch{}let N=x.format??P.landscape,q=K.reduce((C,k)=>C+k.durationInFrames,0);return{id:u(),scenes:K,totalDurationInFrames:q,fps:this.fps,format:N,audio:z?{narration:z}:void 0,subtitles:w,voiceTimingMap:L?.timingMap,thumbnail:B}}}function u(){let x=Date.now().toString(36),G=Math.random().toString(36).slice(2,8);return`vp_${x}_${G}`}export{Z as VideoGenerator,V as ScenePlanner};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ModelSelectionContext, ModelSelector } from '@contractspec/lib.ai-providers/selector-types';
|
|
2
|
+
import type { LLMProvider } from '@contractspec/lib.contracts-integrations/integrations/providers/llm';
|
|
3
|
+
import type { ScenePlan, VideoBrief } from '../types';
|
|
4
|
+
export interface ScenePlannerOptions {
|
|
5
|
+
llm?: LLMProvider;
|
|
6
|
+
model?: string;
|
|
7
|
+
temperature?: number;
|
|
8
|
+
fps?: number;
|
|
9
|
+
locale?: string;
|
|
10
|
+
modelSelector?: ModelSelector;
|
|
11
|
+
selectionContext?: ModelSelectionContext;
|
|
12
|
+
}
|
|
13
|
+
export declare class ScenePlanner {
|
|
14
|
+
private readonly llm?;
|
|
15
|
+
private readonly model?;
|
|
16
|
+
private readonly temperature;
|
|
17
|
+
private readonly fps;
|
|
18
|
+
private readonly i18n;
|
|
19
|
+
private readonly modelSelector?;
|
|
20
|
+
private readonly selectionContext?;
|
|
21
|
+
constructor(options?: ScenePlannerOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Plan scenes for a video brief.
|
|
24
|
+
* With LLM: produces richer, context-aware scene breakdowns.
|
|
25
|
+
* Without LLM: deterministic template-based planning.
|
|
26
|
+
*/
|
|
27
|
+
plan(brief: VideoBrief): Promise<ScenePlan>;
|
|
28
|
+
private planDeterministic;
|
|
29
|
+
private resolveModel;
|
|
30
|
+
private planWithLlm;
|
|
31
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var O=import.meta.require;import{VIDEO_FORMATS as k}from"@contractspec/lib.contracts-integrations/integrations/providers/video";var J=30,P={horizontal:120,vertical:80,contentWidth:1680,contentHeight:920};function d(L){let x=L.width/1920,C=L.height/1080;return{horizontal:Math.round(P.horizontal*x),vertical:Math.round(P.vertical*C),contentWidth:Math.round(P.contentWidth*x),contentHeight:Math.round(P.contentHeight*C)}}var F={center:{x:960,y:540},topLeft:{x:120,y:80},topRight:{x:1800,y:80},bottomLeft:{x:120,y:1000},bottomRight:{x:1800,y:1000},bottomCenter:{x:960,y:960}};function l(){return[k.landscape,k.square,k.portrait]}import{defineTranslation as Y}from"@contractspec/lib.contracts-spec/translations";var q=Y({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"All user-facing, LLM-facing, and developer-facing strings for the video-gen package",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"prompt.script.system":{value:`You are a video narration script writer.
|
|
3
|
+
Write a narration script for a short video (30-60 seconds).
|
|
4
|
+
{styleGuide}
|
|
5
|
+
|
|
6
|
+
Return JSON with shape:
|
|
7
|
+
{
|
|
8
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
9
|
+
"fullText": string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Scene IDs should be: "intro", "problems", "solutions", "metrics", "cta".
|
|
13
|
+
Only include segments that are relevant to the brief content.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`You are a video scene planner for ContractSpec marketing/documentation videos.
|
|
14
|
+
Given a content brief, break it into video scenes.
|
|
15
|
+
|
|
16
|
+
Each scene must have:
|
|
17
|
+
- compositionId: one of "ApiOverview", "SocialClip", "TerminalDemo"
|
|
18
|
+
- props: the input props for that composition (see type definitions)
|
|
19
|
+
- durationInFrames: duration at {fps}fps
|
|
20
|
+
- narrationText: what the narrator says during this scene
|
|
21
|
+
|
|
22
|
+
Return a JSON object with shape:
|
|
23
|
+
{
|
|
24
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
25
|
+
"narrationScript": string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Keep the total duration around {targetSeconds} seconds.
|
|
29
|
+
Prioritize clarity and pacing. Each scene should communicate one idea.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Use a clear, authoritative, professional tone. Be concise and direct.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Use a friendly, conversational tone. Be approachable and relatable.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Use precise technical language. Be detailed and accurate.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"The challenge: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"The solution: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"The results: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"Learn more",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"The Problem",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"The problem: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"The Solution",description:"Scene hook label for solution"},"scene.narration.solution":{value:"The solution: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"Results",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"Generates:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"One spec. Every surface.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"REST Endpoint",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"GraphQL Mutation",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Prisma Model",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"TypeScript SDK",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"MCP Tool",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"OpenAPI Spec",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"Learn more",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});import{defineTranslation as g}from"@contractspec/lib.contracts-spec/translations";var w=g({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"Spanish translations for the video-gen package",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"prompt.script.system":{value:`Eres un redactor de guiones de narraci\xF3n para v\xEDdeo.
|
|
30
|
+
Escribe un gui\xF3n de narraci\xF3n para un v\xEDdeo corto (30-60 segundos).
|
|
31
|
+
{styleGuide}
|
|
32
|
+
|
|
33
|
+
Devuelve JSON con la forma:
|
|
34
|
+
{
|
|
35
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
36
|
+
"fullText": string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Los identificadores de escena deben ser: "intro", "problems", "solutions", "metrics", "cta".
|
|
40
|
+
Incluye solo los segmentos relevantes para el brief.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`Eres un planificador de escenas de v\xEDdeo para v\xEDdeos de marketing/documentaci\xF3n de ContractSpec.
|
|
41
|
+
Dado un brief de contenido, div\xEDdelo en escenas de v\xEDdeo.
|
|
42
|
+
|
|
43
|
+
Cada escena debe tener:
|
|
44
|
+
- compositionId: uno de "ApiOverview", "SocialClip", "TerminalDemo"
|
|
45
|
+
- props: las propiedades de entrada de esa composici\xF3n
|
|
46
|
+
- durationInFrames: duraci\xF3n a {fps} fps
|
|
47
|
+
- narrationText: lo que dice el narrador durante esta escena
|
|
48
|
+
|
|
49
|
+
Devuelve un objeto JSON con la forma:
|
|
50
|
+
{
|
|
51
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
52
|
+
"narrationScript": string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Mant\xE9n la duraci\xF3n total alrededor de {targetSeconds} segundos.
|
|
56
|
+
Prioriza la claridad y el ritmo. Cada escena debe comunicar una idea.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Usa un tono claro, autoritario y profesional. S\xE9 conciso y directo.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Usa un tono amigable y conversacional. S\xE9 accesible y cercano.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Usa un lenguaje t\xE9cnico preciso. S\xE9 detallado y exacto.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"El desaf\xEDo: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"La soluci\xF3n: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"Los resultados: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"M\xE1s informaci\xF3n",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"El problema",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"El problema: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"La soluci\xF3n",description:"Scene hook label for solution"},"scene.narration.solution":{value:"La soluci\xF3n: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"Resultados",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"Genera:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"Una spec. Todas las superficies.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"Endpoint REST",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"Mutaci\xF3n GraphQL",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Modelo Prisma",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"SDK TypeScript",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"Herramienta MCP",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"Spec OpenAPI",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"M\xE1s informaci\xF3n",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});import{defineTranslation as y}from"@contractspec/lib.contracts-spec/translations";var B=y({meta:{key:"video-gen.messages",version:"1.0.0",domain:"video-gen",description:"French translations for the video-gen package",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"prompt.script.system":{value:`Vous \xEAtes un r\xE9dacteur de scripts de narration vid\xE9o.
|
|
57
|
+
\xC9crivez un script de narration pour une courte vid\xE9o (30-60 secondes).
|
|
58
|
+
{styleGuide}
|
|
59
|
+
|
|
60
|
+
Retournez du JSON avec la forme\xA0:
|
|
61
|
+
{
|
|
62
|
+
"segments": [{ "sceneId": string, "text": string }],
|
|
63
|
+
"fullText": string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Les identifiants de sc\xE8ne doivent \xEAtre\xA0: "intro", "problems", "solutions", "metrics", "cta".
|
|
67
|
+
N'incluez que les segments pertinents au brief.`,description:"Script generator LLM system prompt",placeholders:[{name:"styleGuide",type:"string"}]},"prompt.scenePlanner.system":{value:`Vous \xEAtes un planificateur de sc\xE8nes vid\xE9o pour les vid\xE9os marketing/documentation de ContractSpec.
|
|
68
|
+
\xC0 partir d'un brief, d\xE9composez-le en sc\xE8nes vid\xE9o.
|
|
69
|
+
|
|
70
|
+
Chaque sc\xE8ne doit avoir\xA0:
|
|
71
|
+
- compositionId\xA0: un parmi "ApiOverview", "SocialClip", "TerminalDemo"
|
|
72
|
+
- props\xA0: les propri\xE9t\xE9s d'entr\xE9e de cette composition
|
|
73
|
+
- durationInFrames\xA0: dur\xE9e \xE0 {fps}\xA0fps
|
|
74
|
+
- narrationText\xA0: ce que le narrateur dit pendant cette sc\xE8ne
|
|
75
|
+
|
|
76
|
+
Retournez un objet JSON avec la forme\xA0:
|
|
77
|
+
{
|
|
78
|
+
"scenes": [{ "compositionId": string, "props": object, "durationInFrames": number, "narrationText": string }],
|
|
79
|
+
"narrationScript": string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Gardez la dur\xE9e totale autour de {targetSeconds} secondes.
|
|
83
|
+
Privil\xE9giez la clart\xE9 et le rythme. Chaque sc\xE8ne doit communiquer une id\xE9e.`,description:"Scene planner LLM system prompt",placeholders:[{name:"fps",type:"number"},{name:"targetSeconds",type:"number"}]},"prompt.style.professional":{value:"Utilisez un ton clair, autoritaire et professionnel. Soyez concis et direct.",description:"Style guide for professional narration"},"prompt.style.casual":{value:"Utilisez un ton amical et conversationnel. Soyez accessible et proche.",description:"Style guide for casual narration"},"prompt.style.technical":{value:"Utilisez un langage technique pr\xE9cis. Soyez d\xE9taill\xE9 et exact.",description:"Style guide for technical narration"},"script.segment.challenge":{value:"Le d\xE9fi\xA0: {content}",description:"Narration segment prefix for problems",placeholders:[{name:"content",type:"string"}]},"script.segment.solution":{value:"La solution\xA0: {content}",description:"Narration segment prefix for solutions",placeholders:[{name:"content",type:"string"}]},"script.segment.results":{value:"Les r\xE9sultats\xA0: {content}",description:"Narration segment prefix for metrics",placeholders:[{name:"content",type:"string"}]},"scene.cta.default":{value:"En savoir plus",description:"Default call-to-action text for scenes"},"scene.hook.problem":{value:"Le probl\xE8me",description:"Scene hook label for problem statement"},"scene.narration.problem":{value:"Le probl\xE8me\xA0: {content}",description:"Scene narration for problem statement",placeholders:[{name:"content",type:"string"}]},"scene.hook.solution":{value:"La solution",description:"Scene hook label for solution"},"scene.narration.solution":{value:"La solution\xA0: {content}",description:"Scene narration for solution",placeholders:[{name:"content",type:"string"}]},"scene.hook.results":{value:"R\xE9sultats",description:"Scene hook label for results/metrics"},"composition.apiOverview.generates":{value:"G\xE9n\xE8re\xA0:",description:"ApiOverview heading for generated outputs"},"composition.apiOverview.tagline":{value:"Un spec. Toutes les surfaces.",description:"ApiOverview default tagline"},"composition.apiOverview.output.rest":{value:"Endpoint REST",description:"Generated output label: REST"},"composition.apiOverview.output.graphql":{value:"Mutation GraphQL",description:"Generated output label: GraphQL"},"composition.apiOverview.output.prisma":{value:"Mod\xE8le Prisma",description:"Generated output label: Prisma"},"composition.apiOverview.output.typescript":{value:"SDK TypeScript",description:"Generated output label: TypeScript SDK"},"composition.apiOverview.output.mcp":{value:"Outil MCP",description:"Generated output label: MCP Tool"},"composition.apiOverview.output.openapi":{value:"Spec OpenAPI",description:"Generated output label: OpenAPI"},"composition.socialClip.cta":{value:"En savoir plus",description:"SocialClip default CTA"},"composition.terminal.title":{value:"Terminal",description:"TerminalDemo default window title"}}});var Q={"prompt.script.system":"prompt.script.system","prompt.scenePlanner.system":"prompt.scenePlanner.system","prompt.style.professional":"prompt.style.professional","prompt.style.casual":"prompt.style.casual","prompt.style.technical":"prompt.style.technical"},U={"script.segment.challenge":"script.segment.challenge","script.segment.solution":"script.segment.solution","script.segment.results":"script.segment.results"},V={"scene.cta.default":"scene.cta.default","scene.hook.problem":"scene.hook.problem","scene.narration.problem":"scene.narration.problem","scene.hook.solution":"scene.hook.solution","scene.narration.solution":"scene.narration.solution","scene.hook.results":"scene.hook.results"},W={"composition.apiOverview.generates":"composition.apiOverview.generates","composition.apiOverview.tagline":"composition.apiOverview.tagline","composition.apiOverview.output.rest":"composition.apiOverview.output.rest","composition.apiOverview.output.graphql":"composition.apiOverview.output.graphql","composition.apiOverview.output.prisma":"composition.apiOverview.output.prisma","composition.apiOverview.output.typescript":"composition.apiOverview.output.typescript","composition.apiOverview.output.mcp":"composition.apiOverview.output.mcp","composition.apiOverview.output.openapi":"composition.apiOverview.output.openapi","composition.socialClip.cta":"composition.socialClip.cta","composition.terminal.title":"composition.terminal.title"},A={...Q,...U,...V,...W};import{DEFAULT_LOCALE as R,isSupportedLocale as X,resolveLocale as _,SUPPORTED_LOCALES as D}from"@contractspec/lib.contracts-spec/translations";import{createI18nFactory as M}from"@contractspec/lib.contracts-spec/translations";var E=M({specKey:"video-gen.messages",catalogs:[q,B,w]}),H=E.create,Z=E.getDefault,u=E.resetRegistry;class v{llm;model;temperature;fps;i18n;modelSelector;selectionContext;constructor(L){this.llm=L?.llm,this.model=L?.model,this.temperature=L?.temperature??0.3,this.fps=L?.fps??J,this.i18n=H(L?.locale),this.modelSelector=L?.modelSelector,this.selectionContext=L?.selectionContext}async plan(L){if(this.llm)return this.planWithLlm(L);return this.planDeterministic(L)}planDeterministic(L){let{content:x}=L,{t:C}=this.i18n,G=[],I=this.fps;if(G.push({compositionId:"SocialClip",props:{hook:x.title,message:x.summary,points:x.solutions.slice(0,3),cta:x.callToAction??C("scene.cta.default")},durationInFrames:3*I,narrationText:`${x.title}. ${x.summary}`}),x.problems.length>0)G.push({compositionId:"SocialClip",props:{hook:C("scene.hook.problem"),message:x.problems[0]??"",points:x.problems.slice(1,4)},durationInFrames:4*I,narrationText:C("scene.narration.problem",{content:x.problems.join(". ")})});if(x.solutions.length>0)G.push({compositionId:"SocialClip",props:{hook:C("scene.hook.solution"),message:x.solutions[0]??"",points:x.solutions.slice(1,4)},durationInFrames:5*I,narrationText:C("scene.narration.solution",{content:x.solutions.join(". ")})});if(x.metrics&&x.metrics.length>0)G.push({compositionId:"SocialClip",props:{hook:C("scene.hook.results"),message:x.metrics[0]??"",points:x.metrics.slice(1,3)},durationInFrames:3*I,narrationText:x.metrics.join(". ")});if(x.callToAction)G.push({compositionId:"SocialClip",props:{hook:x.callToAction,message:"",cta:x.callToAction},durationInFrames:2*I,narrationText:x.callToAction});if(L.targetDurationSeconds){let m=L.targetDurationSeconds*I,K=G.reduce((N,$)=>N+$.durationInFrames,0),j=m/K;for(let N of G)N.durationInFrames=Math.round(N.durationInFrames*j)}let h=G.reduce((m,K)=>m+K.durationInFrames,0),z=G.filter((m)=>m.narrationText).map((m)=>m.narrationText).join(" ");return{scenes:G,estimatedDurationSeconds:h/I,narrationScript:z}}async resolveModel(){if(this.model)return this.model;if(this.modelSelector){let L=this.selectionContext??{taskDimension:"reasoning"};return(await this.modelSelector.select(L)).modelId}return}async planWithLlm(L){let{t:x}=this.i18n,C=[{role:"system",content:[{type:"text",text:x("prompt.scenePlanner.system",{fps:this.fps,targetSeconds:L.targetDurationSeconds??30})}]},{role:"user",content:[{type:"text",text:JSON.stringify(L.content)}]}];if(!this.llm)return this.planDeterministic(L);try{let G=await this.resolveModel(),h=(await this.llm.chat(C,{model:G,temperature:this.temperature,responseFormat:"json"})).message.content.find((K)=>K.type==="text");if(!h||h.type!=="text")return this.planDeterministic(L);let z=JSON.parse(h.text),m=z.scenes.reduce((K,j)=>K+j.durationInFrames,0);return{scenes:z.scenes,estimatedDurationSeconds:m/this.fps,narrationScript:z.narrationScript}}catch{return this.planDeterministic(L)}}}export{v as ScenePlanner};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ModelSelectionContext, ModelSelector } from '@contractspec/lib.ai-providers/selector-types';
|
|
2
|
+
import type { ContentBrief } from '@contractspec/lib.content-gen/types';
|
|
3
|
+
import type { LLMProvider } from '@contractspec/lib.contracts-integrations/integrations/providers/llm';
|
|
4
|
+
import type { NarrationConfig } from '@contractspec/lib.contracts-integrations/integrations/providers/video';
|
|
5
|
+
export interface NarrationScript {
|
|
6
|
+
/** Full narration text */
|
|
7
|
+
fullText: string;
|
|
8
|
+
/** Per-scene narration segments */
|
|
9
|
+
segments: NarrationSegment[];
|
|
10
|
+
/** Estimated speaking duration in seconds (at ~150 words/min) */
|
|
11
|
+
estimatedDurationSeconds: number;
|
|
12
|
+
/** Style used */
|
|
13
|
+
style: NarrationConfig['style'];
|
|
14
|
+
}
|
|
15
|
+
export interface NarrationSegment {
|
|
16
|
+
/** Scene identifier this segment maps to */
|
|
17
|
+
sceneId: string;
|
|
18
|
+
/** Narration text for this segment */
|
|
19
|
+
text: string;
|
|
20
|
+
/** Estimated duration in seconds */
|
|
21
|
+
estimatedDurationSeconds: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ScriptGeneratorOptions {
|
|
24
|
+
llm?: LLMProvider;
|
|
25
|
+
model?: string;
|
|
26
|
+
temperature?: number;
|
|
27
|
+
locale?: string;
|
|
28
|
+
modelSelector?: ModelSelector;
|
|
29
|
+
selectionContext?: ModelSelectionContext;
|
|
30
|
+
}
|
|
31
|
+
export declare class ScriptGenerator {
|
|
32
|
+
private readonly llm?;
|
|
33
|
+
private readonly model?;
|
|
34
|
+
private readonly temperature;
|
|
35
|
+
private readonly i18n;
|
|
36
|
+
private readonly modelSelector?;
|
|
37
|
+
private readonly selectionContext?;
|
|
38
|
+
constructor(options?: ScriptGeneratorOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Generate a narration script from a content brief.
|
|
41
|
+
*/
|
|
42
|
+
generate(brief: ContentBrief, config?: NarrationConfig): Promise<NarrationScript>;
|
|
43
|
+
private generateDeterministic;
|
|
44
|
+
private resolveModel;
|
|
45
|
+
private generateWithLlm;
|
|
46
|
+
/**
|
|
47
|
+
* Adjust text tone based on style.
|
|
48
|
+
* In the deterministic path, this is a simple pass-through.
|
|
49
|
+
* The LLM path handles style natively.
|
|
50
|
+
*/
|
|
51
|
+
private formatForStyle;
|
|
52
|
+
/**
|
|
53
|
+
* Estimate speaking duration based on word count.
|
|
54
|
+
* Average speaking rate: ~150 words per minute.
|
|
55
|
+
*/
|
|
56
|
+
private estimateDuration;
|
|
57
|
+
}
|