@mevdragon/vidfarm-devcli 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +3 -3
  2. package/demo/README.md +28 -0
  3. package/demo/dist/app.css +1 -0
  4. package/demo/dist/app.js +1184 -0
  5. package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
  6. package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
  7. package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
  8. package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
  9. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
  10. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
  11. package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
  12. package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
  13. package/demo/dist/favicon.ico +0 -0
  14. package/demo/dist/icons/timeline/audio.svg +7 -0
  15. package/demo/dist/icons/timeline/captions.svg +5 -0
  16. package/demo/dist/icons/timeline/composition.svg +12 -0
  17. package/demo/dist/icons/timeline/image.svg +18 -0
  18. package/demo/dist/icons/timeline/music.svg +10 -0
  19. package/demo/dist/icons/timeline/text.svg +3 -0
  20. package/demo/dist/index.html +15 -0
  21. package/dist/src/account-pages-legacy.js +9396 -0
  22. package/dist/src/account-pages.js +61 -0
  23. package/dist/src/app.js +14378 -0
  24. package/dist/src/cli.js +1 -1
  25. package/dist/src/composition-runtime.js +613 -0
  26. package/dist/src/config.js +166 -0
  27. package/dist/src/context.js +447 -0
  28. package/dist/src/dev-app-legacy.js +739 -0
  29. package/dist/src/dev-app.js +6 -0
  30. package/dist/src/domain.js +2 -0
  31. package/dist/src/editor-chat-history.js +82 -0
  32. package/dist/src/editor-chat.js +449 -0
  33. package/dist/src/editor-dark-theme.js +1128 -0
  34. package/dist/src/frontend/debug.js +71 -0
  35. package/dist/src/frontend/flockposter-cache-store.js +124 -0
  36. package/dist/src/frontend/homepage-client.js +182 -0
  37. package/dist/src/frontend/homepage-shared.js +4 -0
  38. package/dist/src/frontend/homepage-store.js +28 -0
  39. package/dist/src/frontend/homepage-view.js +547 -0
  40. package/dist/src/frontend/page-runtime-client.js +132 -0
  41. package/dist/src/frontend/page-runtime-store.js +9 -0
  42. package/dist/src/frontend/sentry.js +42 -0
  43. package/dist/src/frontend/template-editor-chat.js +3960 -0
  44. package/dist/src/help-page.js +346 -0
  45. package/dist/src/homepage.js +1235 -0
  46. package/dist/src/hyperframes/composition.js +180 -0
  47. package/dist/src/index.js +16 -0
  48. package/dist/src/instrument.js +30 -0
  49. package/dist/src/lib/crypto.js +45 -0
  50. package/dist/src/lib/dev-log.js +54 -0
  51. package/dist/src/lib/display-name.js +11 -0
  52. package/dist/src/lib/ids.js +24 -0
  53. package/dist/src/lib/images.js +19 -0
  54. package/dist/src/lib/json.js +15 -0
  55. package/dist/src/lib/package-root.js +47 -0
  56. package/dist/src/lib/template-paths.js +28 -0
  57. package/dist/src/lib/time.js +7 -0
  58. package/dist/src/lib/url-clean.js +85 -0
  59. package/dist/src/page-runtime.js +2 -0
  60. package/dist/src/page-shell.js +1381 -0
  61. package/dist/src/primitive-context.js +357 -0
  62. package/dist/src/primitive-registry.js +2436 -0
  63. package/dist/src/primitive-sdk.js +4 -0
  64. package/dist/src/primitives/hyperframes-media.js +108 -0
  65. package/dist/src/react-page-shell.js +35 -0
  66. package/dist/src/ready-post-schedule-component.js +1540 -0
  67. package/dist/src/registry.js +296 -0
  68. package/dist/src/runtime.js +35 -0
  69. package/dist/src/services/api-call-history.js +249 -0
  70. package/dist/src/services/auth.js +152 -0
  71. package/dist/src/services/billing-pricing.js +39 -0
  72. package/dist/src/services/billing.js +228 -0
  73. package/dist/src/services/cast.js +127 -0
  74. package/dist/src/services/chat-threads.js +92 -0
  75. package/dist/src/services/composition-sanitize.js +124 -0
  76. package/dist/src/services/composition-watch.js +79 -0
  77. package/dist/src/services/fork-access.js +93 -0
  78. package/dist/src/services/fork-manifest.js +42 -0
  79. package/dist/src/services/ghostcut.js +179 -0
  80. package/dist/src/services/hyperframes.js +2307 -0
  81. package/dist/src/services/job-capacity.js +14 -0
  82. package/dist/src/services/job-logs.js +197 -0
  83. package/dist/src/services/jobs.js +136 -0
  84. package/dist/src/services/local-dynamo.js +0 -0
  85. package/dist/src/services/media-processing.js +766 -0
  86. package/dist/src/services/primitive-media-lambda.js +280 -0
  87. package/dist/src/services/providers.js +2926 -0
  88. package/dist/src/services/rate-limits.js +262 -0
  89. package/dist/src/services/serverless-auth.js +382 -0
  90. package/dist/src/services/serverless-jobs.js +1082 -0
  91. package/dist/src/services/serverless-provider-keys.js +409 -0
  92. package/dist/src/services/serverless-records.js +1385 -0
  93. package/dist/src/services/serverless-template-configs.js +75 -0
  94. package/dist/src/services/storage.js +383 -0
  95. package/dist/src/services/template-certification.js +413 -0
  96. package/dist/src/services/template-loader.js +99 -0
  97. package/dist/src/services/template-runtime-bundles.js +217 -0
  98. package/dist/src/services/template-sources.js +1017 -0
  99. package/dist/src/services/video-normalization.js +2 -0
  100. package/dist/src/services/webhooks.js +62 -0
  101. package/dist/src/template-editor-pages.js +2576 -0
  102. package/dist/src/template-editor-shell.js +2840 -0
  103. package/dist/src/template-sdk.js +4 -0
  104. package/dist/src/worker.js +17 -0
  105. package/package.json +6 -4
  106. package/public/assets/homepage-app.js +54 -0
  107. package/public/assets/homepage-client-app.js +80 -0
  108. package/public/assets/page-runtime-client-app.js +94 -0
  109. package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
  110. package/src/assets/SELLING_WITH_HOOKS.md +377 -0
  111. package/src/assets/SELLING_WITH_VSLS.md +606 -0
  112. package/src/assets/favicon.ico +0 -0
  113. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,2436 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import sharp from "sharp";
5
+ import { z } from "zod";
6
+ import { config } from "./config.js";
7
+ import { definePrimitive } from "./primitive-sdk.js";
8
+ import { PrimitiveMediaLambdaService } from "./services/primitive-media-lambda.js";
9
+ import { buildHtmlImageCompositionHtml, buildMediaDedupeCompositionHtml, DEDUPE_DEFAULT_EFFECTS } from "./primitives/hyperframes-media.js";
10
+ const imageProviderSchema = z.enum(["openai", "gemini", "openrouter"]);
11
+ const videoProviderSchema = z.enum(["openai", "gemini", "openrouter"]);
12
+ const textProviderSchema = z.enum(["openai", "gemini", "openrouter", "perplexity"]);
13
+ const outputFormatSchema = z.enum(["png", "jpeg", "webp"]);
14
+ const aspectRatioSchema = z.enum(["1:1", "4:3", "3:4", "16:9", "9:16"]).optional();
15
+ const videoAspectRatioSchema = z.enum(["1:1", "4:3", "3:4", "16:9", "9:16", "21:9", "9:21"]).optional();
16
+ const imageSizeSchema = z.enum(["1K", "2K", "4K"]).optional();
17
+ const brainstormCountSchema = z.number().int().min(1).max(50).default(12);
18
+ const generateImagePayloadSchema = z.object({
19
+ provider: imageProviderSchema.optional(),
20
+ model: z.string().min(1).optional(),
21
+ prompt: z.string().min(3).max(8000),
22
+ prompt_attachments: z.array(z.string().url()).max(16).default([]),
23
+ aspect_ratio: aspectRatioSchema,
24
+ size: z.string().min(1).max(40).optional(),
25
+ image_size: imageSizeSchema,
26
+ output_format: outputFormatSchema.default("png")
27
+ });
28
+ const editImagePayloadSchema = z.object({
29
+ provider: imageProviderSchema.optional(),
30
+ model: z.string().min(1).optional(),
31
+ source_image_url: z.string().url(),
32
+ instruction: z.string().min(3).max(8000),
33
+ reference_attachments: z.array(z.string().url()).max(15).default([]),
34
+ aspect_ratio: aspectRatioSchema,
35
+ size: z.string().min(1).max(40).optional(),
36
+ image_size: imageSizeSchema,
37
+ output_format: outputFormatSchema.default("png")
38
+ });
39
+ const inpaintImagePayloadSchema = z.object({
40
+ provider: imageProviderSchema.optional(),
41
+ model: z.string().min(1).optional(),
42
+ source_image_url: z.string().url(),
43
+ mask_url: z.string().url(),
44
+ instruction: z.string().min(3).max(8000),
45
+ reference_attachments: z.array(z.string().url()).max(15).default([]),
46
+ aspect_ratio: aspectRatioSchema,
47
+ size: z.string().min(1).max(40).optional(),
48
+ image_size: imageSizeSchema,
49
+ output_format: outputFormatSchema.default("png")
50
+ });
51
+ const videoFrameImageSchema = z.preprocess((raw) => {
52
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
53
+ return raw;
54
+ const frame = raw;
55
+ return {
56
+ ...frame,
57
+ image_url: frame.image_url ?? frame.imageUrl,
58
+ frame_type: frame.frame_type ?? frame.frameType
59
+ };
60
+ }, z.object({
61
+ image_url: z.string().url(),
62
+ frame_type: z.enum(["first_frame", "last_frame"]).default("first_frame")
63
+ }));
64
+ const generateVideoPayloadSchema = z.preprocess((raw) => {
65
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
66
+ return raw;
67
+ const payload = raw;
68
+ const explicitSeconds = payload.duration ??
69
+ payload.duration_seconds ??
70
+ payload.durationSeconds ??
71
+ payload.seconds ??
72
+ payload.video_length_seconds ??
73
+ payload.videoLengthSeconds;
74
+ const milliseconds = payload.duration_ms ?? payload.durationMs ?? payload.video_length_ms ?? payload.videoLengthMs;
75
+ return {
76
+ ...payload,
77
+ duration: normalizeAiVideoDurationSeconds(explicitSeconds, milliseconds)
78
+ };
79
+ }, z.object({
80
+ provider: videoProviderSchema.optional(),
81
+ model: z.string().min(1).optional(),
82
+ prompt: z.string().min(3).max(8000),
83
+ duration: z.number().int().min(1).max(30).optional(),
84
+ resolution: z.enum(["480p", "720p", "1080p", "1K", "2K", "4K"]).optional(),
85
+ aspect_ratio: videoAspectRatioSchema,
86
+ size: z.string().min(1).max(40).optional(),
87
+ generate_audio: z.boolean().optional(),
88
+ frame_images: z.array(videoFrameImageSchema).max(2).default([]),
89
+ input_references: z.array(z.string().url()).max(8).default([]),
90
+ seed: z.number().int().optional(),
91
+ provider_options: z.record(z.string(), z.unknown()).optional(),
92
+ output_format: z.enum(["mp4"]).default("mp4")
93
+ }));
94
+ const removeBackgroundImagePayloadSchema = z.preprocess((raw) => {
95
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
96
+ return raw;
97
+ const payload = raw;
98
+ return {
99
+ ...payload,
100
+ source_image_url: payload.source_image_url ??
101
+ payload.image_url ??
102
+ payload.imageUrl ??
103
+ payload.url
104
+ };
105
+ }, z.object({
106
+ source_image_url: z.string().url(),
107
+ output_format: z.enum(["png", "jpeg", "webp"]).default("webp")
108
+ }));
109
+ const renderHtmlImagePayloadSchema = z.object({
110
+ width: z.number().int().min(16).max(4096).default(1080),
111
+ height: z.number().int().min(16).max(4096).default(1920),
112
+ html: z.string().min(1).max(200_000),
113
+ css: z.string().max(200_000).optional(),
114
+ background_color: z.string().min(1).max(64).default("#ffffff"),
115
+ output_format: outputFormatSchema.default("png")
116
+ });
117
+ const mediaTypeSchema = z.enum(["image", "video"]);
118
+ const mediaFitSchema = z.enum(["cover", "contain", "fill"]);
119
+ const renderVideoSlideSchema = z.preprocess((raw) => {
120
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
121
+ return raw;
122
+ const slide = raw;
123
+ return {
124
+ ...slide,
125
+ media_url: slide.media_url ?? slide.image_url ?? slide.imageUrl ?? slide.video_url ?? slide.videoUrl,
126
+ duration_ms: slide.duration_ms ?? slide.durationMs,
127
+ media_type: slide.media_type ?? slide.mediaType,
128
+ background_color: slide.background_color ?? slide.backgroundColor
129
+ };
130
+ }, z.object({
131
+ media_url: z.string().url(),
132
+ media_type: mediaTypeSchema.optional(),
133
+ duration_ms: z.number().int().min(100).max(120_000),
134
+ fit: mediaFitSchema.optional(),
135
+ background_color: z.string().min(1).max(64).optional()
136
+ }));
137
+ const renderSlidesVideoPayloadSchema = z.object({
138
+ slides: z.array(renderVideoSlideSchema).min(1).max(100),
139
+ width: z.number().int().min(16).max(4096).default(1080),
140
+ height: z.number().int().min(16).max(4096).default(1920),
141
+ background_color: z.string().min(1).max(64).default("#000000"),
142
+ default_fit: mediaFitSchema.default("cover")
143
+ }).superRefine((value, ctx) => {
144
+ const totalDurationMs = value.slides.reduce((total, slide) => total + slide.duration_ms, 0);
145
+ if (totalDurationMs > 10 * 60 * 1000) {
146
+ ctx.addIssue({
147
+ code: z.ZodIssueCode.custom,
148
+ message: "Total slideshow duration must be 10 minutes or less.",
149
+ path: ["slides"]
150
+ });
151
+ }
152
+ });
153
+ const dedupeMediaFitSchema = z.enum(["cover", "contain", "fill", "none", "scale-down"]);
154
+ const dedupeEffectsSchema = z.preprocess((raw) => {
155
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
156
+ return raw;
157
+ const p = raw;
158
+ return {
159
+ zoom: p.zoom,
160
+ tilt: p.tilt,
161
+ rotate: p.rotate,
162
+ saturation: p.saturation,
163
+ speed: p.speed,
164
+ horizontal_flip: p.horizontal_flip ?? p.horizontalFlip,
165
+ contrast: p.contrast,
166
+ brightness: p.brightness,
167
+ hue_rotate: p.hue_rotate ?? p.hueRotate,
168
+ blur: p.blur
169
+ };
170
+ }, z.object({
171
+ zoom: z.number().min(0.1).max(10).optional(),
172
+ tilt: z.number().min(-90).max(90).optional(),
173
+ rotate: z.number().min(-360).max(360).optional(),
174
+ saturation: z.number().min(0).max(10).optional(),
175
+ speed: z.number().min(0.1).max(10).optional(),
176
+ horizontal_flip: z.boolean().optional(),
177
+ contrast: z.number().min(0).max(10).optional(),
178
+ brightness: z.number().min(0).max(10).optional(),
179
+ hue_rotate: z.number().min(-360).max(360).optional(),
180
+ blur: z.number().min(0).max(50).optional()
181
+ }));
182
+ const mediaDedupePayloadSchema = z.preprocess((raw) => {
183
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
184
+ return raw;
185
+ const p = raw;
186
+ return {
187
+ ...p,
188
+ source_media_url: p.source_media_url
189
+ ?? p.source_video_url
190
+ ?? p.source_image_url
191
+ ?? p.media_url
192
+ ?? p.mediaUrl
193
+ ?? p.url,
194
+ media_type: p.media_type ?? p.mediaType,
195
+ tint_color: p.tint_color ?? p.tintColor,
196
+ tint_opacity: p.tint_opacity ?? p.tintOpacity,
197
+ duration_ms: p.duration_ms ?? p.durationMs,
198
+ fallback_duration_ms: p.fallback_duration_ms ?? p.fallbackDurationMs,
199
+ object_fit: p.object_fit ?? p.objectFit,
200
+ object_position: p.object_position ?? p.objectPosition,
201
+ background_color: p.background_color ?? p.backgroundColor,
202
+ output_format: p.output_format ?? p.outputFormat
203
+ };
204
+ }, z.object({
205
+ source_media_url: z.string().url(),
206
+ media_type: mediaTypeSchema.optional(),
207
+ effects: dedupeEffectsSchema.optional(),
208
+ tint_color: z.string().min(1).max(64).default("#FF8C00"),
209
+ tint_opacity: z.number().min(0).max(1).default(0.08),
210
+ width: z.number().int().min(16).max(4096).default(1080),
211
+ height: z.number().int().min(16).max(4096).default(1920),
212
+ duration_ms: z.number().int().min(100).max(600_000).optional(),
213
+ fallback_duration_ms: z.number().int().min(100).max(600_000).default(5000),
214
+ object_fit: dedupeMediaFitSchema.default("cover"),
215
+ object_position: z.string().min(1).max(64).default("center center"),
216
+ background_color: z.string().min(1).max(64).default("#000000"),
217
+ muted: z.boolean().default(false),
218
+ volume: z.number().min(0).max(2).default(1),
219
+ output_format: outputFormatSchema.default("png")
220
+ }));
221
+ const hyperframeLayerPayloadSchema = z.object({
222
+ id: z.string().min(1).max(120),
223
+ kind: z.enum(["video", "image", "audio", "caption", "text", "html", "shape"]),
224
+ label: z.string().max(200).optional(),
225
+ mode: z.enum(["both", "editor", "publish"]).default("publish"),
226
+ start: z.number().min(0),
227
+ duration: z.number().positive(),
228
+ track: z.number().int().min(0).default(0),
229
+ z: z.number().int().default(0),
230
+ x: z.number().default(0),
231
+ y: z.number().default(0),
232
+ width: z.number().positive().default(100),
233
+ height: z.number().positive().default(100),
234
+ src: z.string().min(1).optional(),
235
+ mediaStart: z.number().min(0).optional(),
236
+ media_start: z.number().min(0).optional(),
237
+ playbackStart: z.number().min(0).optional(),
238
+ playback_start: z.number().min(0).optional(),
239
+ fit: z.enum(["cover", "contain", "fill", "none"]).default("cover"),
240
+ muted: z.boolean().optional(),
241
+ volume: z.number().min(0).max(2).optional(),
242
+ text: z.string().max(20_000).optional(),
243
+ html: z.string().max(200_000).optional(),
244
+ color: z.string().max(80).optional(),
245
+ background: z.string().max(80).optional(),
246
+ textBackgroundStyle: z.enum(["plain", "outline", "highlight-solid", "highlight-translucent", "panel"]).optional(),
247
+ text_background_style: z.enum(["plain", "outline", "highlight-solid", "highlight-translucent", "panel"]).optional(),
248
+ fontFamily: z.string().max(120).optional(),
249
+ font_family: z.string().max(120).optional(),
250
+ fontWeight: z.number().int().min(100).max(1000).optional(),
251
+ font_weight: z.number().int().min(100).max(1000).optional(),
252
+ fontSize: z.number().int().min(4).max(400).optional(),
253
+ font_size: z.number().int().min(4).max(400).optional(),
254
+ lineHeight: z.number().min(0.5).max(4).optional(),
255
+ line_height: z.number().min(0.5).max(4).optional(),
256
+ radius: z.number().min(0).max(999).optional(),
257
+ opacity: z.number().min(0).max(1).optional(),
258
+ viralAlignment: z.string().max(2000).optional(),
259
+ viral_alignment: z.string().max(2000).optional()
260
+ });
261
+ const hyperframesCompositionPayloadSchema = z.object({
262
+ composition_id: z.string().min(1).max(120),
263
+ title: z.string().max(200).optional(),
264
+ source_folder: z.string().max(500).optional(),
265
+ width: z.number().int().min(16).max(4096).default(720),
266
+ height: z.number().int().min(16).max(4096).default(1280),
267
+ fps: z.number().int().min(1).max(120).default(30),
268
+ duration: z.number().positive().optional(),
269
+ duration_seconds: z.number().positive().optional(),
270
+ background_color: z.string().max(80).default("#050604"),
271
+ css: z.string().max(200_000).optional(),
272
+ layers: z.array(hyperframeLayerPayloadSchema).min(1).max(300)
273
+ });
274
+ const hyperframesProjectFilePayloadSchema = z.object({
275
+ path: z.string().min(1).max(500),
276
+ contents: z.string().max(2_000_000).optional(),
277
+ source_path: z.string().min(1).max(500).optional(),
278
+ encoding: z.enum(["utf8", "base64"]).default("utf8"),
279
+ content_type: z.string().max(120).optional()
280
+ }).refine((value) => Boolean(value.contents || value.source_path), {
281
+ message: "project_files entry needs contents or source_path."
282
+ });
283
+ function readHyperframesProjectFileContents(file) {
284
+ if (file.source_path) {
285
+ const normalized = file.source_path.replace(/^\/+/, "").replace(/\\/g, "/");
286
+ if (normalized.includes("..") || !normalized.startsWith("drafts/")) {
287
+ throw new Error(`project_files.source_path must live under drafts/: ${file.source_path}`);
288
+ }
289
+ // The JobRunner lambda chdirs to /tmp/vidfarm-job-runner at startup, so
290
+ // process.cwd() no longer points at the lambda image root. The drafts/
291
+ // tree is baked into the image at LAMBDA_TASK_ROOT (/var/task).
292
+ const root = process.env.LAMBDA_TASK_ROOT ?? process.cwd();
293
+ const resolved = path.resolve(root, normalized);
294
+ return readFileSync(resolved);
295
+ }
296
+ if (file.contents === undefined) {
297
+ throw new Error("project_files entry has neither contents nor source_path");
298
+ }
299
+ return file.encoding === "base64" ? Buffer.from(file.contents, "base64") : file.contents;
300
+ }
301
+ const hyperframesRenderPayloadSchema = z.object({
302
+ composition_html: z.string().min(1).max(2_000_000).optional(),
303
+ composition: hyperframesCompositionPayloadSchema.optional(),
304
+ composition_id: z.string().min(1).max(120).optional(),
305
+ title: z.string().max(200).optional(),
306
+ width: z.number().int().min(16).max(4096).optional(),
307
+ height: z.number().int().min(16).max(4096).optional(),
308
+ fps: z.number().int().min(1).max(120).default(30),
309
+ chunk_size: z.number().int().min(1).max(1800).default(240),
310
+ max_parallel_chunks: z.number().int().min(1).max(40).default(8),
311
+ output_key: z.string().min(1).max(300).default("output.mp4"),
312
+ project_files: z.array(hyperframesProjectFilePayloadSchema).max(100).default([]),
313
+ composition_data: z.record(z.string(), z.unknown()).optional()
314
+ }).refine((value) => Boolean(value.composition_html || value.composition), {
315
+ message: "Provide either composition_html or composition layers.",
316
+ path: ["composition_html"]
317
+ });
318
+ const mediaSourceUrlSchema = z.string().url().refine((value) => !isVidfarmControlPlaneArtifactUrl(value), {
319
+ message: "Pass a direct media URL, such as artifact.public_url, not a Vidfarm job artifacts API endpoint."
320
+ });
321
+ const normalizeVideoPayloadSchema = z.object({
322
+ source_video_url: mediaSourceUrlSchema,
323
+ max_width: z.number().int().min(240).max(4096).default(1080),
324
+ max_height: z.number().int().min(240).max(4096).default(1920),
325
+ crf: z.number().int().min(18).max(35).default(23),
326
+ preset: z.enum(["veryfast", "faster", "fast", "medium"]).default("veryfast"),
327
+ max_video_bitrate_kbps: z.number().int().min(500).max(50_000).default(8000),
328
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
329
+ });
330
+ const trimVideoPayloadSchema = z.object({
331
+ source_video_url: mediaSourceUrlSchema,
332
+ start_ms: z.number().int().min(0).default(0),
333
+ end_ms: z.number().int().min(1).optional(),
334
+ duration_ms: z.number().int().min(1).optional(),
335
+ max_width: z.number().int().min(240).max(4096).optional(),
336
+ max_height: z.number().int().min(240).max(4096).optional(),
337
+ crf: z.number().int().min(18).max(35).default(23),
338
+ preset: z.enum(["veryfast", "faster", "fast", "medium"]).default("veryfast"),
339
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
340
+ }).refine((value) => value.duration_ms != null || value.end_ms != null, {
341
+ message: "Either duration_ms or end_ms is required.",
342
+ path: ["duration_ms"]
343
+ });
344
+ const extractAudioPayloadSchema = z.object({
345
+ source_video_url: mediaSourceUrlSchema,
346
+ output_format: z.enum(["mp3", "wav", "m4a"]).default("mp3"),
347
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
348
+ });
349
+ const trimAudioPayloadSchema = z.object({
350
+ source_audio_url: mediaSourceUrlSchema,
351
+ start_ms: z.number().int().min(0).default(0),
352
+ end_ms: z.number().int().min(1).optional(),
353
+ duration_ms: z.number().int().min(1).optional(),
354
+ output_format: z.enum(["mp3", "wav", "m4a"]).default("mp3"),
355
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
356
+ }).refine((value) => value.duration_ms != null || value.end_ms != null, {
357
+ message: "Either duration_ms or end_ms is required.",
358
+ path: ["duration_ms"]
359
+ });
360
+ const probeVideoPayloadSchema = z.object({
361
+ source_video_url: mediaSourceUrlSchema
362
+ });
363
+ const probeAudioPayloadSchema = z.object({
364
+ source_audio_url: mediaSourceUrlSchema
365
+ });
366
+ const extractFramePayloadSchema = z.object({
367
+ source_video_url: mediaSourceUrlSchema,
368
+ at_ms: z.number().int().min(0).default(0),
369
+ output_format: z.enum(["png", "jpeg"]).default("png")
370
+ });
371
+ const muteVideoPayloadSchema = z.object({
372
+ source_video_url: mediaSourceUrlSchema
373
+ });
374
+ const replaceAudioPayloadSchema = z.object({
375
+ source_video_url: mediaSourceUrlSchema,
376
+ source_audio_url: mediaSourceUrlSchema,
377
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
378
+ });
379
+ const concatVideoPayloadSchema = z.object({
380
+ source_video_urls: z.array(mediaSourceUrlSchema).min(1).max(32),
381
+ width: z.number().int().min(16).max(4096).optional(),
382
+ height: z.number().int().min(16).max(4096).optional(),
383
+ fit: mediaFitSchema.default("contain"),
384
+ background_color: z.string().min(1).max(64).default("#000000")
385
+ });
386
+ const concatAudioPayloadSchema = z.object({
387
+ source_audio_urls: z.array(mediaSourceUrlSchema).min(1).max(64),
388
+ output_format: z.enum(["mp3", "wav", "m4a"]).default("mp3"),
389
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128)
390
+ });
391
+ const normalizeAudioPayloadSchema = z.object({
392
+ source_audio_url: mediaSourceUrlSchema,
393
+ output_format: z.enum(["mp3", "wav", "m4a"]).default("mp3"),
394
+ audio_bitrate_kbps: z.number().int().min(64).max(320).default(128),
395
+ sample_rate_hz: z.number().int().min(8_000).max(96_000).default(48_000)
396
+ });
397
+ const downloadVideoPayloadSchema = z.preprocess((raw) => {
398
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
399
+ return raw;
400
+ const payload = raw;
401
+ return {
402
+ ...payload,
403
+ source_url: payload.source_url ?? payload.video_url ?? payload.url,
404
+ save_manifest: payload.save_manifest ?? payload.include_manifest ?? true
405
+ };
406
+ }, z.object({
407
+ source_url: z.string().url(),
408
+ quality: z.enum(["best", "hd", "full_hd"]).default("best"),
409
+ save_manifest: z.boolean().default(true)
410
+ }));
411
+ const brainstormHooksPayloadSchema = z.preprocess(normalizeOfferDescriptionPayload, z.object({
412
+ offer_description: z.string().min(10).max(12_000),
413
+ count: brainstormCountSchema,
414
+ provider: textProviderSchema.optional(),
415
+ model: z.string().min(1).max(200).optional()
416
+ }));
417
+ const brainstormAwarenessStagesPayloadSchema = z.preprocess(normalizeOfferDescriptionPayload, z.object({
418
+ offer_description: z.string().min(10).max(12_000),
419
+ provider: textProviderSchema.optional(),
420
+ model: z.string().min(1).max(200).optional()
421
+ }));
422
+ const brainstormAnglesPayloadSchema = z.preprocess(normalizeAnglePayload, z.object({
423
+ offer_description: z.string().min(10).max(12_000),
424
+ problem_awareness: z.enum(["problem_unaware", "problem_aware"]),
425
+ solution_awareness: z.enum(["solution_unaware", "solution_aware"]),
426
+ count: brainstormCountSchema,
427
+ provider: textProviderSchema.optional(),
428
+ model: z.string().min(1).max(200).optional()
429
+ }));
430
+ const brainstormColdstartPayloadSchema = z.preprocess(normalizeColdstartPayload, z.object({
431
+ user_message: z.string().min(3).max(12_000),
432
+ count: z.number().int().min(6).max(30).default(12),
433
+ provider: textProviderSchema.optional(),
434
+ model: z.string().min(1).max(200).optional()
435
+ }));
436
+ const brainstormProductPlacementPayloadSchema = z.preprocess(normalizeProductPlacementPayload, z.object({
437
+ source_video_url: z.string().url(),
438
+ offer_description: z.string().min(10).max(12_000),
439
+ count: z.number().int().min(3).max(30).default(8),
440
+ provider: textProviderSchema.optional(),
441
+ model: z.string().min(1).max(200).optional()
442
+ }));
443
+ const brainstormHooksResultSchema = z.object({
444
+ hooks: z.array(z.object({
445
+ hook: z.string().min(1),
446
+ explanation: z.string().min(1)
447
+ })).min(1).max(50)
448
+ });
449
+ const brainstormAnglesResultSchema = z.object({
450
+ angles: z.array(z.object({
451
+ angle: z.string().min(1),
452
+ explanation: z.string().min(1)
453
+ })).min(1).max(50)
454
+ });
455
+ const brainstormColdstartResultSchema = z.object({
456
+ questions: z.array(z.object({
457
+ question: z.string().min(1),
458
+ why_it_matters: z.string().min(1)
459
+ })).min(6).max(30)
460
+ });
461
+ const brainstormProductPlacementResultSchema = z.object({
462
+ opportunities: z.array(z.object({
463
+ timestamp: z.string().min(1),
464
+ scene: z.string().min(1),
465
+ placement_type: z.string().min(1),
466
+ placement_idea: z.string().min(1),
467
+ why_it_works: z.string().min(1)
468
+ })).min(1).max(50)
469
+ });
470
+ const PRIMITIVE_IMAGE_GENERATE_ID = "primitive:image_generate";
471
+ const PRIMITIVE_IMAGE_EDIT_ID = "primitive:image_edit";
472
+ const PRIMITIVE_IMAGE_INPAINT_ID = "primitive:image_inpaint";
473
+ const PRIMITIVE_IMAGE_RENDER_HTML_ID = "primitive:image_render_html";
474
+ const PRIMITIVE_IMAGE_REMOVE_BACKGROUND_ID = "primitive:image_remove_background";
475
+ const PRIMITIVE_VIDEO_GENERATE_ID = "primitive:video_generate";
476
+ const PRIMITIVE_VIDEO_RENDER_SLIDES_ID = "primitive:video_render_slides";
477
+ const PRIMITIVE_VIDEO_NORMALIZE_ID = "primitive:video_normalize";
478
+ const PRIMITIVE_VIDEO_DOWNLOAD_ID = "primitive:video_download";
479
+ const PRIMITIVE_VIDEO_TRIM_ID = "primitive:video_trim";
480
+ const PRIMITIVE_VIDEO_EXTRACT_AUDIO_ID = "primitive:video_extract_audio";
481
+ const PRIMITIVE_AUDIO_TRIM_ID = "primitive:audio_trim";
482
+ const PRIMITIVE_VIDEO_PROBE_ID = "primitive:video_probe";
483
+ const PRIMITIVE_AUDIO_PROBE_ID = "primitive:audio_probe";
484
+ const PRIMITIVE_VIDEO_EXTRACT_FRAME_ID = "primitive:video_extract_frame";
485
+ const PRIMITIVE_VIDEO_MUTE_ID = "primitive:video_mute";
486
+ const PRIMITIVE_VIDEO_REPLACE_AUDIO_ID = "primitive:video_replace_audio";
487
+ const PRIMITIVE_VIDEO_CONCAT_ID = "primitive:video_concat";
488
+ const PRIMITIVE_AUDIO_CONCAT_ID = "primitive:audio_concat";
489
+ const PRIMITIVE_AUDIO_NORMALIZE_ID = "primitive:audio_normalize";
490
+ const PRIMITIVE_HYPERFRAMES_RENDER_ID = "primitive:hyperframes_render";
491
+ const PRIMITIVE_BRAINSTORM_HOOKS_ID = "primitive:brainstorm_hooks";
492
+ const PRIMITIVE_BRAINSTORM_AWARENESS_STAGES_ID = "primitive:brainstorm_awareness_stages";
493
+ const PRIMITIVE_BRAINSTORM_ANGLES_ID = "primitive:brainstorm_angles";
494
+ const PRIMITIVE_BRAINSTORM_COLDSTART_ID = "primitive:brainstorm_coldstart";
495
+ const PRIMITIVE_BRAINSTORM_PRODUCT_PLACEMENT_ID = "primitive:brainstorm_product_placement";
496
+ const PRIMITIVE_MEDIA_DEDUPE_ID = "primitive:media_dedupe";
497
+ const primitiveMediaService = new PrimitiveMediaLambdaService();
498
+ const hooksReferenceText = readPrimitiveAsset("SELLING_WITH_HOOKS.md");
499
+ const awarenessReferenceText = readPrimitiveAsset("SELLING_AWARENESS_STAGES.md");
500
+ const imageGeneratePrimitive = definePrimitive({
501
+ id: PRIMITIVE_IMAGE_GENERATE_ID,
502
+ kind: "image_generate",
503
+ operations: {
504
+ run: {
505
+ description: "Generate a new image and persist it as a reusable platform URL.",
506
+ inputSchema: generateImagePayloadSchema,
507
+ workflow: "run"
508
+ }
509
+ },
510
+ jobs: {
511
+ async run(ctx, input) {
512
+ const payload = generateImagePayloadSchema.parse(input);
513
+ ctx.logger.progress(0.12, "Generating primitive image");
514
+ const provider = payload.provider ?? "openai";
515
+ const model = payload.model ?? defaultImageModelForProvider(provider);
516
+ const generated = await ctx.providers.generateImage({
517
+ provider,
518
+ model,
519
+ prompt: payload.prompt,
520
+ promptAttachments: payload.prompt_attachments,
521
+ aspectRatio: payload.aspect_ratio,
522
+ size: payload.size,
523
+ imageSize: payload.image_size
524
+ });
525
+ const normalized = await normalizePrimitiveImageOutput(generated.bytes, payload.output_format);
526
+ const stored = await storePrimitiveImage(ctx, {
527
+ key: `output.${extensionForOutputFormat(payload.output_format)}`,
528
+ bytes: normalized.bytes,
529
+ contentType: normalized.contentType,
530
+ metadata: {
531
+ primitive: "image_generate",
532
+ provider,
533
+ model,
534
+ prompt: payload.prompt,
535
+ revisedPrompt: generated.revisedPrompt
536
+ }
537
+ });
538
+ ctx.logger.progress(1, "Primitive image generation complete");
539
+ return {
540
+ progress: 1,
541
+ output: buildImagePrimitiveOutput({
542
+ fileUrl: stored.url,
543
+ contentType: stored.contentType,
544
+ revisedPrompt: generated.revisedPrompt,
545
+ files: stored.url ? [stored.url] : []
546
+ })
547
+ };
548
+ }
549
+ }
550
+ });
551
+ const imageEditPrimitive = definePrimitive({
552
+ id: PRIMITIVE_IMAGE_EDIT_ID,
553
+ kind: "image_edit",
554
+ operations: {
555
+ run: {
556
+ description: "Edit an existing image into a new reusable platform URL.",
557
+ inputSchema: editImagePayloadSchema,
558
+ workflow: "run"
559
+ }
560
+ },
561
+ jobs: {
562
+ async run(ctx, input) {
563
+ const payload = editImagePayloadSchema.parse(input);
564
+ ctx.logger.progress(0.12, "Editing primitive image");
565
+ const provider = payload.provider ?? "openai";
566
+ const model = payload.model ?? defaultImageModelForProvider(provider);
567
+ const edited = await ctx.providers.editImage({
568
+ provider,
569
+ model,
570
+ sourceImageUrl: payload.source_image_url,
571
+ instruction: payload.instruction,
572
+ referenceAttachments: payload.reference_attachments,
573
+ aspectRatio: payload.aspect_ratio,
574
+ size: payload.size,
575
+ imageSize: payload.image_size
576
+ });
577
+ const normalized = await normalizePrimitiveImageOutput(edited.bytes, payload.output_format);
578
+ const stored = await storePrimitiveImage(ctx, {
579
+ key: `output.${extensionForOutputFormat(payload.output_format)}`,
580
+ bytes: normalized.bytes,
581
+ contentType: normalized.contentType,
582
+ metadata: {
583
+ primitive: "image_edit",
584
+ provider,
585
+ model,
586
+ sourceImageUrl: payload.source_image_url,
587
+ instruction: payload.instruction,
588
+ revisedPrompt: edited.revisedPrompt
589
+ }
590
+ });
591
+ ctx.logger.progress(1, "Primitive image edit complete");
592
+ return {
593
+ progress: 1,
594
+ output: buildImagePrimitiveOutput({
595
+ fileUrl: stored.url,
596
+ contentType: stored.contentType,
597
+ revisedPrompt: edited.revisedPrompt,
598
+ derivedFromUrl: payload.source_image_url,
599
+ files: stored.url ? [stored.url] : []
600
+ })
601
+ };
602
+ }
603
+ }
604
+ });
605
+ const imageInpaintPrimitive = definePrimitive({
606
+ id: PRIMITIVE_IMAGE_INPAINT_ID,
607
+ kind: "image_inpaint",
608
+ operations: {
609
+ run: {
610
+ description: "Edit a masked region of an existing image into a new reusable platform URL.",
611
+ inputSchema: inpaintImagePayloadSchema,
612
+ workflow: "run"
613
+ }
614
+ },
615
+ jobs: {
616
+ async run(ctx, input) {
617
+ const payload = inpaintImagePayloadSchema.parse(input);
618
+ ctx.logger.progress(0.12, "Inpainting primitive image");
619
+ const provider = payload.provider ?? "openai";
620
+ const model = payload.model ?? defaultImageModelForProvider(provider);
621
+ const edited = await ctx.providers.inpaintImage({
622
+ provider,
623
+ model,
624
+ sourceImageUrl: payload.source_image_url,
625
+ maskUrl: payload.mask_url,
626
+ instruction: payload.instruction,
627
+ referenceAttachments: payload.reference_attachments,
628
+ aspectRatio: payload.aspect_ratio,
629
+ size: payload.size,
630
+ imageSize: payload.image_size
631
+ });
632
+ const normalized = await normalizePrimitiveImageOutput(edited.bytes, payload.output_format);
633
+ const stored = await storePrimitiveImage(ctx, {
634
+ key: `output.${extensionForOutputFormat(payload.output_format)}`,
635
+ bytes: normalized.bytes,
636
+ contentType: normalized.contentType,
637
+ metadata: {
638
+ primitive: "image_inpaint",
639
+ provider,
640
+ model,
641
+ sourceImageUrl: payload.source_image_url,
642
+ maskUrl: payload.mask_url,
643
+ instruction: payload.instruction,
644
+ revisedPrompt: edited.revisedPrompt
645
+ }
646
+ });
647
+ ctx.logger.progress(1, "Primitive image inpaint complete");
648
+ return {
649
+ progress: 1,
650
+ output: buildImagePrimitiveOutput({
651
+ fileUrl: stored.url,
652
+ contentType: stored.contentType,
653
+ revisedPrompt: edited.revisedPrompt,
654
+ derivedFromUrl: payload.source_image_url,
655
+ maskUrl: payload.mask_url,
656
+ files: stored.url ? [stored.url] : []
657
+ })
658
+ };
659
+ }
660
+ }
661
+ });
662
+ const imageRenderHtmlPrimitive = definePrimitive({
663
+ id: PRIMITIVE_IMAGE_RENDER_HTML_ID,
664
+ kind: "image_render_html",
665
+ operations: {
666
+ run: {
667
+ description: "Render an HTML-driven still image into a reusable platform URL.",
668
+ inputSchema: renderHtmlImagePayloadSchema,
669
+ workflow: "run"
670
+ }
671
+ },
672
+ jobs: {
673
+ async run(ctx, input) {
674
+ const payload = renderHtmlImagePayloadSchema.parse(input);
675
+ ctx.logger.progress(0.18, "Rendering primitive HTML image");
676
+ const rendered = await ctx.hyperframes.renderStill({
677
+ compositionHtml: buildHtmlImageCompositionHtml({
678
+ compositionId: "primitive-html-image",
679
+ width: payload.width,
680
+ height: payload.height,
681
+ backgroundColor: payload.background_color,
682
+ html: payload.html,
683
+ css: payload.css ?? ""
684
+ }),
685
+ compositionId: "primitive-html-image",
686
+ imageFormat: payload.output_format,
687
+ outputKey: `output.${extensionForOutputFormat(payload.output_format)}`
688
+ });
689
+ ctx.logger.progress(1, "Primitive HTML image render complete");
690
+ return {
691
+ progress: 1,
692
+ output: {
693
+ files: rendered.outputUrl ? [rendered.outputUrl] : [],
694
+ primary_file_url: rendered.outputUrl,
695
+ image: {
696
+ file_url: rendered.outputUrl,
697
+ content_type: contentTypeForOutputFormat(payload.output_format),
698
+ width: payload.width,
699
+ height: payload.height
700
+ },
701
+ render: rendered
702
+ }
703
+ };
704
+ }
705
+ }
706
+ });
707
+ const imageRemoveBackgroundPrimitive = definePrimitive({
708
+ id: PRIMITIVE_IMAGE_REMOVE_BACKGROUND_ID,
709
+ kind: "image_remove_background",
710
+ operations: {
711
+ run: {
712
+ description: "Remove the background from a source image URL and store the transparent result as a reusable platform URL.",
713
+ inputSchema: removeBackgroundImagePayloadSchema,
714
+ workflow: "run"
715
+ }
716
+ },
717
+ jobs: {
718
+ async run(ctx, input) {
719
+ const payload = removeBackgroundImagePayloadSchema.parse(input);
720
+ const sourceUrl = payload.source_image_url;
721
+ const sourceHost = safeUrlHost(sourceUrl);
722
+ const apiKey = (config.RAPIDAPI_KEY || "").trim();
723
+ if (!apiKey) {
724
+ throw new Error("RAPIDAPI_KEY is not configured.");
725
+ }
726
+ ctx.logger.progress(0.15, "Requesting remote background removal", {
727
+ sourceUrl,
728
+ sourceHost,
729
+ apiHost: config.RAPIDAPI_REMOVE_BACKGROUND_HOST
730
+ });
731
+ const apiResponse = await fetch(config.RAPIDAPI_REMOVE_BACKGROUND_URL, {
732
+ method: "POST",
733
+ headers: {
734
+ "x-rapidapi-key": apiKey,
735
+ "x-rapidapi-host": config.RAPIDAPI_REMOVE_BACKGROUND_HOST,
736
+ "content-type": "application/x-www-form-urlencoded"
737
+ },
738
+ body: new URLSearchParams({ image_url: sourceUrl })
739
+ });
740
+ if (!apiResponse.ok) {
741
+ const details = await apiResponse.text().catch(() => "");
742
+ throw new Error(`Background removal failed with HTTP ${apiResponse.status}${details ? `: ${details.slice(0, 400)}` : ""}`);
743
+ }
744
+ const lookup = await apiResponse.json();
745
+ if (!lookup.url) {
746
+ throw new Error("Background removal returned no result URL.");
747
+ }
748
+ ctx.logger.progress(0.55, "Downloading background-removed image", {
749
+ remoteUrl: lookup.url
750
+ });
751
+ const remoteImage = await fetchRemoteBinary(lookup.url, "image/webp");
752
+ const normalized = await normalizePrimitiveImageOutput(remoteImage.bytes, payload.output_format);
753
+ const stored = await storePrimitiveImage(ctx, {
754
+ key: `output.${extensionForOutputFormat(payload.output_format)}`,
755
+ bytes: normalized.bytes,
756
+ contentType: normalized.contentType,
757
+ metadata: {
758
+ primitive: "image_remove_background",
759
+ source_image_url: sourceUrl,
760
+ source_host: sourceHost,
761
+ upstream_url: lookup.url,
762
+ upstream_processing_time_seconds: lookup.processing_time ?? null
763
+ }
764
+ });
765
+ await ctx.billing.record({
766
+ type: "cpu_estimate",
767
+ costUsd: config.RAPIDAPI_REMOVE_BACKGROUND_COST_USD,
768
+ costCenterSlug: "rapidapi_remove_background",
769
+ idempotencyKey: `rapidapi_remove_background:${sourceUrl}`,
770
+ occurredAtMs: Date.now(),
771
+ metadata: {
772
+ operation: "remove-background",
773
+ source_image_url: sourceUrl,
774
+ source_host: sourceHost,
775
+ upstream_url: lookup.url,
776
+ output_key: stored.key
777
+ }
778
+ });
779
+ ctx.logger.progress(1, "Background removal complete");
780
+ return {
781
+ progress: 1,
782
+ output: buildImagePrimitiveOutput({
783
+ fileUrl: stored.url,
784
+ contentType: stored.contentType,
785
+ derivedFromUrl: sourceUrl,
786
+ files: compactUrls([stored.url])
787
+ })
788
+ };
789
+ }
790
+ }
791
+ });
792
+ const videoGeneratePrimitive = definePrimitive({
793
+ id: PRIMITIVE_VIDEO_GENERATE_ID,
794
+ kind: "video_generate",
795
+ operations: {
796
+ run: {
797
+ description: "Generate a new AI video from a text prompt and optional reference images.",
798
+ inputSchema: generateVideoPayloadSchema,
799
+ workflow: "run",
800
+ providerHint: "openai"
801
+ }
802
+ },
803
+ jobs: {
804
+ async run(ctx, input) {
805
+ const payload = generateVideoPayloadSchema.parse(input);
806
+ const provider = payload.provider ?? "openai";
807
+ const model = payload.model ?? defaultVideoModelForProvider(provider);
808
+ ctx.logger.progress(0.08, "Submitting primitive AI video generation");
809
+ const generated = await ctx.providers.generateVideo({
810
+ provider,
811
+ model,
812
+ prompt: payload.prompt,
813
+ duration: payload.duration,
814
+ resolution: payload.resolution,
815
+ aspectRatio: payload.aspect_ratio,
816
+ size: payload.size,
817
+ generateAudio: payload.generate_audio,
818
+ frameImages: payload.frame_images.map((frame) => ({
819
+ imageUrl: frame.image_url,
820
+ frameType: frame.frame_type
821
+ })),
822
+ inputReferences: payload.input_references,
823
+ seed: payload.seed,
824
+ providerOptions: payload.provider_options
825
+ });
826
+ const stored = await storePrimitiveVideo(ctx, {
827
+ key: "output.mp4",
828
+ bytes: generated.bytes,
829
+ contentType: generated.contentType,
830
+ metadata: {
831
+ primitive: "video_generate",
832
+ provider,
833
+ model,
834
+ prompt: payload.prompt,
835
+ providerJob: generated.providerJob
836
+ }
837
+ });
838
+ await recordPrimitiveProviderBilling(ctx, generated, "video_generate");
839
+ ctx.logger.progress(1, "Primitive AI video generation complete");
840
+ return {
841
+ progress: 1,
842
+ output: {
843
+ files: stored.url ? [stored.url] : [],
844
+ primary_file_url: stored.url,
845
+ video: {
846
+ file_url: stored.url,
847
+ content_type: stored.contentType,
848
+ provider,
849
+ model
850
+ },
851
+ provider_job: generated.providerJob
852
+ }
853
+ };
854
+ }
855
+ }
856
+ });
857
+ const videoRenderSlidesPrimitive = definePrimitive({
858
+ id: PRIMITIVE_VIDEO_RENDER_SLIDES_ID,
859
+ kind: "video_render_slides",
860
+ operations: {
861
+ run: {
862
+ description: "Render image/video slides into a reusable MP4 from [{ media_url, duration_ms }].",
863
+ inputSchema: renderSlidesVideoPayloadSchema,
864
+ workflow: "run"
865
+ }
866
+ },
867
+ jobs: {
868
+ async run(ctx, input) {
869
+ const payload = renderSlidesVideoPayloadSchema.parse(input);
870
+ const durationMs = payload.slides.reduce((total, slide) => total + slide.duration_ms, 0);
871
+ ctx.logger.progress(0.18, "Rendering primitive media slideshow", {
872
+ executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
873
+ });
874
+ const render = await executePrimitiveMediaOperation(ctx, {
875
+ operation: "video_render_slides",
876
+ payload: {
877
+ slides: payload.slides.map((slide) => ({
878
+ media_url: slide.media_url,
879
+ media_type: slide.media_type ?? inferMediaType(slide.media_url),
880
+ duration_ms: slide.duration_ms,
881
+ fit: slide.fit ?? payload.default_fit,
882
+ background_color: slide.background_color ?? payload.background_color
883
+ })),
884
+ width: payload.width,
885
+ height: payload.height,
886
+ background_color: payload.background_color,
887
+ default_fit: payload.default_fit
888
+ },
889
+ outputKey: "output.mp4"
890
+ });
891
+ ctx.logger.progress(1, "Primitive media slideshow render complete");
892
+ return {
893
+ progress: 1,
894
+ output: {
895
+ files: compactUrls([
896
+ ...payload.slides.map((slide) => slide.media_url),
897
+ render.publicUrl
898
+ ]),
899
+ primary_file_url: render.publicUrl,
900
+ video: {
901
+ file_url: render.publicUrl,
902
+ content_type: "video/mp4",
903
+ width: payload.width,
904
+ height: payload.height,
905
+ duration_ms: durationMs,
906
+ slide_count: payload.slides.length
907
+ },
908
+ render
909
+ }
910
+ };
911
+ }
912
+ }
913
+ });
914
+ const mediaDedupePrimitive = definePrimitive({
915
+ id: PRIMITIVE_MEDIA_DEDUPE_ID,
916
+ kind: "media_dedupe",
917
+ operations: {
918
+ run: {
919
+ description: "Apply dedupe transforms (zoom, tilt, rotate, playback speed, color filter, blur, tint, flip) to any source image or video URL for reuse across compositions.",
920
+ inputSchema: mediaDedupePayloadSchema,
921
+ workflow: "run"
922
+ }
923
+ },
924
+ jobs: {
925
+ async run(ctx, input) {
926
+ const payload = mediaDedupePayloadSchema.parse(input);
927
+ const resolvedMediaType = payload.media_type ?? inferMediaType(payload.source_media_url);
928
+ const durationMs = payload.duration_ms ?? payload.fallback_duration_ms;
929
+ const dedupeComposition = (mediaType, durationSeconds) => buildMediaDedupeCompositionHtml({
930
+ compositionId: `primitive-media-dedupe-${mediaType}`,
931
+ mediaType,
932
+ sourceMediaUrl: payload.source_media_url,
933
+ width: payload.width,
934
+ height: payload.height,
935
+ durationSeconds,
936
+ effects: payload.effects,
937
+ tintColor: payload.tint_color,
938
+ tintOpacity: payload.tint_opacity,
939
+ objectFit: payload.object_fit,
940
+ objectPosition: payload.object_position,
941
+ backgroundColor: payload.background_color,
942
+ muted: payload.muted
943
+ });
944
+ ctx.logger.progress(0.2, "Rendering primitive media dedupe", {
945
+ mediaType: resolvedMediaType,
946
+ width: payload.width,
947
+ height: payload.height
948
+ });
949
+ if (resolvedMediaType === "image") {
950
+ const rendered = await ctx.hyperframes.renderStill({
951
+ compositionHtml: dedupeComposition("image", 0.1),
952
+ compositionId: "primitive-media-dedupe-image",
953
+ imageFormat: payload.output_format,
954
+ outputKey: `output.${extensionForOutputFormat(payload.output_format)}`
955
+ });
956
+ ctx.logger.progress(1, "Primitive media dedupe still complete");
957
+ return {
958
+ progress: 1,
959
+ output: {
960
+ files: compactUrls([payload.source_media_url, rendered.outputUrl]),
961
+ primary_file_url: rendered.outputUrl,
962
+ image: {
963
+ file_url: rendered.outputUrl,
964
+ content_type: contentTypeForOutputFormat(payload.output_format),
965
+ width: payload.width,
966
+ height: payload.height,
967
+ source_url: payload.source_media_url
968
+ },
969
+ render: rendered
970
+ }
971
+ };
972
+ }
973
+ // Speed manifests as a shorter output clip: durationInFrames = (durationMs /
974
+ // speed) / 1000 * fps, matching the old media-dedupe.tsx frame count.
975
+ const speed = Math.max(0.1, payload.effects?.speed ?? DEDUPE_DEFAULT_EFFECTS.speed);
976
+ const outputDurationSeconds = Math.max(0.05, durationMs / speed / 1000);
977
+ const rendered = await ctx.hyperframes.render({
978
+ compositionHtml: dedupeComposition("video", outputDurationSeconds),
979
+ compositionId: "primitive-media-dedupe-video",
980
+ width: payload.width,
981
+ height: payload.height,
982
+ fps: 30
983
+ });
984
+ ctx.logger.progress(1, "Primitive media dedupe video complete");
985
+ return {
986
+ progress: 1,
987
+ output: {
988
+ files: compactUrls([payload.source_media_url, rendered.outputUrl]),
989
+ primary_file_url: rendered.outputUrl,
990
+ video: {
991
+ file_url: rendered.outputUrl,
992
+ content_type: "video/mp4",
993
+ width: payload.width,
994
+ height: payload.height,
995
+ duration_ms: durationMs,
996
+ source_url: payload.source_media_url
997
+ },
998
+ render: rendered
999
+ }
1000
+ };
1001
+ }
1002
+ }
1003
+ });
1004
+ const videoNormalizePrimitive = definePrimitive({
1005
+ id: PRIMITIVE_VIDEO_NORMALIZE_ID,
1006
+ kind: "video_normalize",
1007
+ operations: {
1008
+ run: {
1009
+ description: "Normalize and compress a source MP4 into a render-friendly reusable platform MP4.",
1010
+ inputSchema: normalizeVideoPayloadSchema,
1011
+ workflow: "run"
1012
+ }
1013
+ },
1014
+ jobs: {
1015
+ async run(ctx, input) {
1016
+ const payload = normalizeVideoPayloadSchema.parse(input);
1017
+ ctx.logger.progress(0.12, "Normalizing source video", {
1018
+ executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
1019
+ });
1020
+ const normalized = await executePrimitiveMediaOperation(ctx, {
1021
+ operation: "video_normalize",
1022
+ payload: {
1023
+ source_video_url: payload.source_video_url,
1024
+ max_width: payload.max_width,
1025
+ max_height: payload.max_height,
1026
+ crf: payload.crf,
1027
+ preset: payload.preset,
1028
+ max_video_bitrate_kbps: payload.max_video_bitrate_kbps,
1029
+ audio_bitrate_kbps: payload.audio_bitrate_kbps
1030
+ },
1031
+ outputKey: "normalized.mp4"
1032
+ });
1033
+ ctx.logger.progress(1, "Video normalization complete");
1034
+ return {
1035
+ progress: 1,
1036
+ output: {
1037
+ files: compactUrls([payload.source_video_url, normalized.publicUrl]),
1038
+ sourceVideoUrl: payload.source_video_url,
1039
+ normalizedVideoUrl: normalized.publicUrl,
1040
+ primary_file_url: normalized.publicUrl,
1041
+ video: {
1042
+ file_url: normalized.publicUrl,
1043
+ content_type: normalized.contentType,
1044
+ normalization: normalized.metadata
1045
+ }
1046
+ }
1047
+ };
1048
+ }
1049
+ }
1050
+ });
1051
+ const videoDownloadPrimitive = definePrimitive({
1052
+ id: PRIMITIVE_VIDEO_DOWNLOAD_ID,
1053
+ kind: "video_download",
1054
+ operations: {
1055
+ run: {
1056
+ description: "Download a supported social/media post URL into a durable reusable platform MP4.",
1057
+ inputSchema: downloadVideoPayloadSchema,
1058
+ workflow: "run"
1059
+ }
1060
+ },
1061
+ jobs: {
1062
+ async run(ctx, input) {
1063
+ const payload = downloadVideoPayloadSchema.parse(input);
1064
+ const sourceUrl = payload.source_url;
1065
+ const sourceHost = safeUrlHost(sourceUrl);
1066
+ const apiKey = (config.RAPIDAPI_KEY || "").trim();
1067
+ if (!apiKey) {
1068
+ throw new Error("RAPIDAPI_KEY is not configured.");
1069
+ }
1070
+ ctx.logger.progress(0.12, "Requesting remote video download metadata", {
1071
+ sourceUrl,
1072
+ sourceHost,
1073
+ apiHost: config.RAPIDAPI_VIDEO_DOWNLOAD_HOST
1074
+ });
1075
+ const downloadResponse = await fetch(config.RAPIDAPI_VIDEO_DOWNLOAD_URL, {
1076
+ method: "POST",
1077
+ headers: {
1078
+ "x-rapidapi-key": apiKey,
1079
+ "x-rapidapi-host": config.RAPIDAPI_VIDEO_DOWNLOAD_HOST,
1080
+ "content-type": "application/x-www-form-urlencoded"
1081
+ },
1082
+ body: new URLSearchParams({ url: sourceUrl })
1083
+ });
1084
+ if (!downloadResponse.ok) {
1085
+ const details = await downloadResponse.text().catch(() => "");
1086
+ throw new Error(`Video download lookup failed with HTTP ${downloadResponse.status}${details ? `: ${details.slice(0, 400)}` : ""}`);
1087
+ }
1088
+ const lookup = await downloadResponse.json();
1089
+ const selectedMedia = selectDownloadedVideoMediaCandidate(lookup.medias ?? [], payload.quality);
1090
+ if (!selectedMedia?.url) {
1091
+ throw new Error("Video download lookup returned no playable MP4 media URL.");
1092
+ }
1093
+ ctx.logger.progress(0.45, "Downloading source video", {
1094
+ selectedUrl: selectedMedia.url,
1095
+ selectedQuality: selectedMedia.quality ?? null
1096
+ });
1097
+ const remoteVideo = await fetchRemoteBinary(selectedMedia.url, "video/mp4");
1098
+ const contentType = normalizeVideoContentType(remoteVideo.contentType);
1099
+ const extension = videoExtensionForContentType(contentType);
1100
+ const storedVideo = await storePrimitiveVideo(ctx, {
1101
+ key: `source-video.${extension}`,
1102
+ bytes: remoteVideo.bytes,
1103
+ contentType,
1104
+ metadata: {
1105
+ primitive: "video_download",
1106
+ operation: "download-video",
1107
+ source_url: sourceUrl,
1108
+ source_host: sourceHost,
1109
+ selected_media_url: selectedMedia.url,
1110
+ selected_quality: selectedMedia.quality ?? null,
1111
+ upstream_source: lookup.source ?? null
1112
+ }
1113
+ });
1114
+ const manifest = {
1115
+ primitive: "video_download",
1116
+ operation: "download-video",
1117
+ sourceUrl,
1118
+ sourceHost,
1119
+ title: lookup.title ?? null,
1120
+ thumbnail: lookup.thumbnail ?? null,
1121
+ duration: lookup.duration ?? null,
1122
+ source: lookup.source ?? null,
1123
+ selectedMedia: {
1124
+ url: selectedMedia.url,
1125
+ quality: selectedMedia.quality ?? null,
1126
+ extension: selectedMedia.extension ?? extension,
1127
+ audioAvailable: selectedMedia.audioAvailable ?? null,
1128
+ videoAvailable: selectedMedia.videoAvailable ?? null
1129
+ },
1130
+ videoUrl: storedVideo.url
1131
+ };
1132
+ const manifestArtifact = payload.save_manifest
1133
+ ? await ctx.storage.putJson("manifest.json", manifest)
1134
+ : null;
1135
+ await ctx.billing.record({
1136
+ type: "cpu_estimate",
1137
+ costUsd: config.RAPIDAPI_VIDEO_DOWNLOAD_BASE_COST_USD,
1138
+ costCenterSlug: "rapidapi_video_download",
1139
+ idempotencyKey: `rapidapi_video_download:${sourceUrl}`,
1140
+ occurredAtMs: Date.now(),
1141
+ metadata: {
1142
+ operation: "download-video",
1143
+ source_url: sourceUrl,
1144
+ source_host: sourceHost,
1145
+ selected_quality: selectedMedia.quality ?? null
1146
+ }
1147
+ });
1148
+ await ctx.billing.record({
1149
+ type: "cpu_estimate",
1150
+ costUsd: config.RAPIDAPI_VIDEO_DOWNLOAD_LAMBDA_COST_USD,
1151
+ costCenterSlug: "video_download_lambda",
1152
+ idempotencyKey: `video_download_lambda:${sourceUrl}:${storedVideo.key}`,
1153
+ occurredAtMs: Date.now(),
1154
+ metadata: {
1155
+ operation: "download-video",
1156
+ source_url: sourceUrl,
1157
+ source_host: sourceHost,
1158
+ output_key: storedVideo.key,
1159
+ content_type: contentType,
1160
+ bytes: remoteVideo.bytes.length
1161
+ }
1162
+ });
1163
+ ctx.logger.progress(1, "Video download complete", {
1164
+ videoUrl: storedVideo.url,
1165
+ sourceHost
1166
+ });
1167
+ return {
1168
+ progress: 1,
1169
+ output: {
1170
+ files: compactUrls([storedVideo.url, manifestArtifact?.url]),
1171
+ sourceUrl,
1172
+ sourceHost,
1173
+ videoUrl: storedVideo.url,
1174
+ sourceVideoUrl: storedVideo.url,
1175
+ primary_file_url: storedVideo.url,
1176
+ title: lookup.title ?? null,
1177
+ thumbnail: lookup.thumbnail ?? null,
1178
+ duration: lookup.duration ?? null,
1179
+ selectedMediaUrl: selectedMedia.url,
1180
+ manifest: manifestArtifact
1181
+ }
1182
+ };
1183
+ }
1184
+ }
1185
+ });
1186
+ const videoTrimPrimitive = definePrimitive({
1187
+ id: PRIMITIVE_VIDEO_TRIM_ID,
1188
+ kind: "video_trim",
1189
+ operations: {
1190
+ run: {
1191
+ description: "Trim a source video into a reusable platform MP4.",
1192
+ inputSchema: trimVideoPayloadSchema,
1193
+ workflow: "run"
1194
+ }
1195
+ },
1196
+ jobs: {
1197
+ async run(ctx, input) {
1198
+ const payload = trimVideoPayloadSchema.parse(input);
1199
+ ctx.logger.progress(0.12, "Trimming source video", {
1200
+ executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
1201
+ });
1202
+ const trimmed = await executePrimitiveMediaOperation(ctx, {
1203
+ operation: "video_trim",
1204
+ payload,
1205
+ outputKey: "trimmed.mp4"
1206
+ });
1207
+ ctx.logger.progress(1, "Video trim complete");
1208
+ return {
1209
+ progress: 1,
1210
+ output: {
1211
+ files: compactUrls([payload.source_video_url, trimmed.publicUrl]),
1212
+ sourceVideoUrl: payload.source_video_url,
1213
+ trimmedVideoUrl: trimmed.publicUrl,
1214
+ primary_file_url: trimmed.publicUrl,
1215
+ video: {
1216
+ file_url: trimmed.publicUrl,
1217
+ content_type: trimmed.contentType,
1218
+ trim: trimmed.metadata
1219
+ }
1220
+ }
1221
+ };
1222
+ }
1223
+ }
1224
+ });
1225
+ const videoExtractAudioPrimitive = definePrimitive({
1226
+ id: PRIMITIVE_VIDEO_EXTRACT_AUDIO_ID,
1227
+ kind: "video_extract_audio",
1228
+ operations: {
1229
+ run: {
1230
+ description: "Extract audio from a source video into a reusable platform audio file.",
1231
+ inputSchema: extractAudioPayloadSchema,
1232
+ workflow: "run"
1233
+ }
1234
+ },
1235
+ jobs: {
1236
+ async run(ctx, input) {
1237
+ const payload = extractAudioPayloadSchema.parse(input);
1238
+ ctx.logger.progress(0.12, "Extracting audio from source video", {
1239
+ executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
1240
+ });
1241
+ const extracted = await executePrimitiveMediaOperation(ctx, {
1242
+ operation: "video_extract_audio",
1243
+ payload,
1244
+ outputKey: `audio.${payload.output_format}`
1245
+ });
1246
+ ctx.logger.progress(1, "Video audio extraction complete");
1247
+ return {
1248
+ progress: 1,
1249
+ output: {
1250
+ files: compactUrls([payload.source_video_url, extracted.publicUrl]),
1251
+ sourceVideoUrl: payload.source_video_url,
1252
+ audioUrl: extracted.publicUrl,
1253
+ primary_file_url: extracted.publicUrl,
1254
+ audio: {
1255
+ file_url: extracted.publicUrl,
1256
+ content_type: extracted.contentType,
1257
+ extraction: extracted.metadata
1258
+ }
1259
+ }
1260
+ };
1261
+ }
1262
+ }
1263
+ });
1264
+ const audioTrimPrimitive = definePrimitive({
1265
+ id: PRIMITIVE_AUDIO_TRIM_ID,
1266
+ kind: "audio_trim",
1267
+ operations: {
1268
+ run: {
1269
+ description: "Trim a source audio file into a reusable platform audio file.",
1270
+ inputSchema: trimAudioPayloadSchema,
1271
+ workflow: "run"
1272
+ }
1273
+ },
1274
+ jobs: {
1275
+ async run(ctx, input) {
1276
+ const payload = trimAudioPayloadSchema.parse(input);
1277
+ ctx.logger.progress(0.12, "Trimming source audio", {
1278
+ executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
1279
+ });
1280
+ const trimmed = await executePrimitiveMediaOperation(ctx, {
1281
+ operation: "audio_trim",
1282
+ payload,
1283
+ outputKey: `trimmed.${payload.output_format}`
1284
+ });
1285
+ ctx.logger.progress(1, "Audio trim complete");
1286
+ return {
1287
+ progress: 1,
1288
+ output: {
1289
+ files: compactUrls([payload.source_audio_url, trimmed.publicUrl]),
1290
+ sourceAudioUrl: payload.source_audio_url,
1291
+ trimmedAudioUrl: trimmed.publicUrl,
1292
+ primary_file_url: trimmed.publicUrl,
1293
+ audio: {
1294
+ file_url: trimmed.publicUrl,
1295
+ content_type: trimmed.contentType,
1296
+ trim: trimmed.metadata
1297
+ }
1298
+ }
1299
+ };
1300
+ }
1301
+ }
1302
+ });
1303
+ const videoProbePrimitive = definePrimitive({
1304
+ id: PRIMITIVE_VIDEO_PROBE_ID,
1305
+ kind: "video_probe",
1306
+ operations: {
1307
+ run: {
1308
+ description: "Probe a source video and return reusable JSON metadata.",
1309
+ inputSchema: probeVideoPayloadSchema,
1310
+ workflow: "run"
1311
+ }
1312
+ },
1313
+ jobs: {
1314
+ async run(ctx, input) {
1315
+ const payload = probeVideoPayloadSchema.parse(input);
1316
+ const probed = await executePrimitiveMediaOperation(ctx, {
1317
+ operation: "video_probe",
1318
+ payload,
1319
+ outputKey: "probe.json"
1320
+ });
1321
+ return {
1322
+ progress: 1,
1323
+ output: {
1324
+ files: compactUrls([payload.source_video_url, probed.publicUrl]),
1325
+ sourceVideoUrl: payload.source_video_url,
1326
+ metadataUrl: probed.publicUrl,
1327
+ primary_file_url: probed.publicUrl,
1328
+ probe: probed.metadata
1329
+ }
1330
+ };
1331
+ }
1332
+ }
1333
+ });
1334
+ const audioProbePrimitive = definePrimitive({
1335
+ id: PRIMITIVE_AUDIO_PROBE_ID,
1336
+ kind: "audio_probe",
1337
+ operations: {
1338
+ run: {
1339
+ description: "Probe a source audio file and return reusable JSON metadata.",
1340
+ inputSchema: probeAudioPayloadSchema,
1341
+ workflow: "run"
1342
+ }
1343
+ },
1344
+ jobs: {
1345
+ async run(ctx, input) {
1346
+ const payload = probeAudioPayloadSchema.parse(input);
1347
+ const probed = await executePrimitiveMediaOperation(ctx, {
1348
+ operation: "audio_probe",
1349
+ payload,
1350
+ outputKey: "probe.json"
1351
+ });
1352
+ return {
1353
+ progress: 1,
1354
+ output: {
1355
+ files: compactUrls([payload.source_audio_url, probed.publicUrl]),
1356
+ sourceAudioUrl: payload.source_audio_url,
1357
+ metadataUrl: probed.publicUrl,
1358
+ primary_file_url: probed.publicUrl,
1359
+ probe: probed.metadata
1360
+ }
1361
+ };
1362
+ }
1363
+ }
1364
+ });
1365
+ const videoExtractFramePrimitive = definePrimitive({
1366
+ id: PRIMITIVE_VIDEO_EXTRACT_FRAME_ID,
1367
+ kind: "video_extract_frame",
1368
+ operations: {
1369
+ run: {
1370
+ description: "Extract a single frame from a source video into a reusable image file.",
1371
+ inputSchema: extractFramePayloadSchema,
1372
+ workflow: "run"
1373
+ }
1374
+ },
1375
+ jobs: {
1376
+ async run(ctx, input) {
1377
+ const payload = extractFramePayloadSchema.parse(input);
1378
+ const frame = await executePrimitiveMediaOperation(ctx, {
1379
+ operation: "video_extract_frame",
1380
+ payload,
1381
+ outputKey: `frame.${payload.output_format === "jpeg" ? "jpg" : payload.output_format}`
1382
+ });
1383
+ return {
1384
+ progress: 1,
1385
+ output: {
1386
+ files: compactUrls([payload.source_video_url, frame.publicUrl]),
1387
+ sourceVideoUrl: payload.source_video_url,
1388
+ primary_file_url: frame.publicUrl,
1389
+ image: {
1390
+ file_url: frame.publicUrl,
1391
+ content_type: frame.contentType,
1392
+ extracted_from_url: payload.source_video_url
1393
+ },
1394
+ frame: frame.metadata
1395
+ }
1396
+ };
1397
+ }
1398
+ }
1399
+ });
1400
+ const videoMutePrimitive = definePrimitive({
1401
+ id: PRIMITIVE_VIDEO_MUTE_ID,
1402
+ kind: "video_mute",
1403
+ operations: {
1404
+ run: {
1405
+ description: "Remove audio from a source video and return a reusable muted MP4.",
1406
+ inputSchema: muteVideoPayloadSchema,
1407
+ workflow: "run"
1408
+ }
1409
+ },
1410
+ jobs: {
1411
+ async run(ctx, input) {
1412
+ const payload = muteVideoPayloadSchema.parse(input);
1413
+ const muted = await executePrimitiveMediaOperation(ctx, {
1414
+ operation: "video_mute",
1415
+ payload,
1416
+ outputKey: "muted.mp4"
1417
+ });
1418
+ return {
1419
+ progress: 1,
1420
+ output: {
1421
+ files: compactUrls([payload.source_video_url, muted.publicUrl]),
1422
+ sourceVideoUrl: payload.source_video_url,
1423
+ mutedVideoUrl: muted.publicUrl,
1424
+ primary_file_url: muted.publicUrl,
1425
+ video: {
1426
+ file_url: muted.publicUrl,
1427
+ content_type: muted.contentType,
1428
+ mute: muted.metadata
1429
+ }
1430
+ }
1431
+ };
1432
+ }
1433
+ }
1434
+ });
1435
+ const videoReplaceAudioPrimitive = definePrimitive({
1436
+ id: PRIMITIVE_VIDEO_REPLACE_AUDIO_ID,
1437
+ kind: "video_replace_audio",
1438
+ operations: {
1439
+ run: {
1440
+ description: "Replace a source video's audio track with another audio file.",
1441
+ inputSchema: replaceAudioPayloadSchema,
1442
+ workflow: "run"
1443
+ }
1444
+ },
1445
+ jobs: {
1446
+ async run(ctx, input) {
1447
+ const payload = replaceAudioPayloadSchema.parse(input);
1448
+ const replaced = await executePrimitiveMediaOperation(ctx, {
1449
+ operation: "video_replace_audio",
1450
+ payload,
1451
+ outputKey: "replaced-audio.mp4"
1452
+ });
1453
+ return {
1454
+ progress: 1,
1455
+ output: {
1456
+ files: compactUrls([payload.source_video_url, payload.source_audio_url, replaced.publicUrl]),
1457
+ sourceVideoUrl: payload.source_video_url,
1458
+ sourceAudioUrl: payload.source_audio_url,
1459
+ primary_file_url: replaced.publicUrl,
1460
+ video: {
1461
+ file_url: replaced.publicUrl,
1462
+ content_type: replaced.contentType,
1463
+ audio_replacement: replaced.metadata
1464
+ }
1465
+ }
1466
+ };
1467
+ }
1468
+ }
1469
+ });
1470
+ const videoConcatPrimitive = definePrimitive({
1471
+ id: PRIMITIVE_VIDEO_CONCAT_ID,
1472
+ kind: "video_concat",
1473
+ operations: {
1474
+ run: {
1475
+ description: "Concatenate multiple source videos into one reusable MP4.",
1476
+ inputSchema: concatVideoPayloadSchema,
1477
+ workflow: "run"
1478
+ }
1479
+ },
1480
+ jobs: {
1481
+ async run(ctx, input) {
1482
+ const payload = concatVideoPayloadSchema.parse(input);
1483
+ const concatenated = await executePrimitiveMediaOperation(ctx, {
1484
+ operation: "video_concat",
1485
+ payload,
1486
+ outputKey: "concat.mp4"
1487
+ });
1488
+ return {
1489
+ progress: 1,
1490
+ output: {
1491
+ files: compactUrls([...payload.source_video_urls, concatenated.publicUrl]),
1492
+ sourceVideoUrls: payload.source_video_urls,
1493
+ primary_file_url: concatenated.publicUrl,
1494
+ video: {
1495
+ file_url: concatenated.publicUrl,
1496
+ content_type: concatenated.contentType,
1497
+ concat: concatenated.metadata
1498
+ }
1499
+ }
1500
+ };
1501
+ }
1502
+ }
1503
+ });
1504
+ const audioConcatPrimitive = definePrimitive({
1505
+ id: PRIMITIVE_AUDIO_CONCAT_ID,
1506
+ kind: "audio_concat",
1507
+ operations: {
1508
+ run: {
1509
+ description: "Concatenate multiple source audio files into one reusable audio file.",
1510
+ inputSchema: concatAudioPayloadSchema,
1511
+ workflow: "run"
1512
+ }
1513
+ },
1514
+ jobs: {
1515
+ async run(ctx, input) {
1516
+ const payload = concatAudioPayloadSchema.parse(input);
1517
+ const concatenated = await executePrimitiveMediaOperation(ctx, {
1518
+ operation: "audio_concat",
1519
+ payload,
1520
+ outputKey: `concat.${payload.output_format}`
1521
+ });
1522
+ return {
1523
+ progress: 1,
1524
+ output: {
1525
+ files: compactUrls([...payload.source_audio_urls, concatenated.publicUrl]),
1526
+ sourceAudioUrls: payload.source_audio_urls,
1527
+ primary_file_url: concatenated.publicUrl,
1528
+ audio: {
1529
+ file_url: concatenated.publicUrl,
1530
+ content_type: concatenated.contentType,
1531
+ concat: concatenated.metadata
1532
+ }
1533
+ }
1534
+ };
1535
+ }
1536
+ }
1537
+ });
1538
+ const audioNormalizePrimitive = definePrimitive({
1539
+ id: PRIMITIVE_AUDIO_NORMALIZE_ID,
1540
+ kind: "audio_normalize",
1541
+ operations: {
1542
+ run: {
1543
+ description: "Normalize a source audio file into a reusable platform audio file.",
1544
+ inputSchema: normalizeAudioPayloadSchema,
1545
+ workflow: "run"
1546
+ }
1547
+ },
1548
+ jobs: {
1549
+ async run(ctx, input) {
1550
+ const payload = normalizeAudioPayloadSchema.parse(input);
1551
+ const normalized = await executePrimitiveMediaOperation(ctx, {
1552
+ operation: "audio_normalize",
1553
+ payload,
1554
+ outputKey: `normalized.${payload.output_format}`
1555
+ });
1556
+ return {
1557
+ progress: 1,
1558
+ output: {
1559
+ files: compactUrls([payload.source_audio_url, normalized.publicUrl]),
1560
+ sourceAudioUrl: payload.source_audio_url,
1561
+ normalizedAudioUrl: normalized.publicUrl,
1562
+ primary_file_url: normalized.publicUrl,
1563
+ audio: {
1564
+ file_url: normalized.publicUrl,
1565
+ content_type: normalized.contentType,
1566
+ normalization: normalized.metadata
1567
+ }
1568
+ }
1569
+ };
1570
+ }
1571
+ }
1572
+ });
1573
+ const hyperframesRenderPrimitive = definePrimitive({
1574
+ id: PRIMITIVE_HYPERFRAMES_RENDER_ID,
1575
+ kind: "hyperframes_render",
1576
+ operations: {
1577
+ run: {
1578
+ description: "Render a timeline/layer composition to MP4 with production Lambda.",
1579
+ inputSchema: hyperframesRenderPayloadSchema,
1580
+ workflow: "run"
1581
+ }
1582
+ },
1583
+ jobs: {
1584
+ async run(ctx, input) {
1585
+ const payload = hyperframesRenderPayloadSchema.parse(input);
1586
+ ctx.logger.progress(0.08, "Preparing timeline render");
1587
+ const composition = payload.composition ? normalizeHyperframesCompositionPayload(payload.composition) : undefined;
1588
+ const compositionHtml = payload.composition_html;
1589
+ const compositionDataArtifact = payload.composition_data
1590
+ ? await ctx.storage.putJson("composition.json", payload.composition_data)
1591
+ : null;
1592
+ const render = await ctx.hyperframes.render({
1593
+ compositionHtml,
1594
+ composition,
1595
+ compositionId: payload.composition_id ?? composition?.compositionId,
1596
+ title: payload.title ?? composition?.title,
1597
+ width: payload.width ?? composition?.width,
1598
+ height: payload.height ?? composition?.height,
1599
+ fps: payload.fps,
1600
+ chunkSize: payload.chunk_size,
1601
+ maxParallelChunks: payload.max_parallel_chunks,
1602
+ outputKey: payload.output_key,
1603
+ projectFiles: payload.project_files.map((file) => ({
1604
+ path: file.path,
1605
+ contents: readHyperframesProjectFileContents(file),
1606
+ contentType: file.content_type
1607
+ }))
1608
+ });
1609
+ ctx.logger.progress(1, "Timeline render complete", {
1610
+ renderId: render.renderId,
1611
+ outputUrl: render.outputUrl,
1612
+ outputS3Uri: render.outputS3Uri
1613
+ });
1614
+ return {
1615
+ progress: 1,
1616
+ output: {
1617
+ files: compactUrls([render.outputUrl, compositionDataArtifact?.url]),
1618
+ primary_file_url: render.outputUrl,
1619
+ video: {
1620
+ file_url: render.outputUrl,
1621
+ content_type: "video/mp4",
1622
+ width: payload.width ?? composition?.width ?? null,
1623
+ height: payload.height ?? composition?.height ?? null,
1624
+ fps: payload.fps
1625
+ },
1626
+ render: {
1627
+ render_id: render.renderId,
1628
+ output_url: render.outputUrl,
1629
+ output_s3_uri: render.outputS3Uri,
1630
+ metadata: render.metadata
1631
+ },
1632
+ hyperframes: {
1633
+ composition_id: payload.composition_id ?? composition?.compositionId ?? null,
1634
+ composition_data_url: compositionDataArtifact?.url ?? null,
1635
+ architecture: "timeline_layers"
1636
+ }
1637
+ }
1638
+ };
1639
+ }
1640
+ }
1641
+ });
1642
+ const brainstormHooksPrimitive = definePrimitive({
1643
+ id: PRIMITIVE_BRAINSTORM_HOOKS_ID,
1644
+ kind: "brainstorm_hooks",
1645
+ operations: {
1646
+ run: {
1647
+ description: "Generate diverse TikTok-native ad hooks plus short explanations for a provided offer.",
1648
+ inputSchema: brainstormHooksPayloadSchema,
1649
+ workflow: "run",
1650
+ providerHint: "openai"
1651
+ }
1652
+ },
1653
+ jobs: {
1654
+ async run(ctx, input) {
1655
+ const payload = brainstormHooksPayloadSchema.parse(input);
1656
+ ctx.logger.progress(0.15, "Brainstorming hooks");
1657
+ const generated = await runBrainstormJson(ctx, {
1658
+ kind: "brainstorm_hooks",
1659
+ provider: payload.provider,
1660
+ model: payload.model,
1661
+ prompt: buildHooksPrompt(payload.offer_description, payload.count),
1662
+ resultSchema: brainstormHooksResultSchema
1663
+ });
1664
+ const promptArtifact = await ctx.storage.putText("prompt.txt", buildHooksPrompt(payload.offer_description, payload.count));
1665
+ const resultArtifact = await ctx.storage.putJson("result.json", generated.result);
1666
+ ctx.logger.progress(1, "Hook brainstorm complete");
1667
+ return {
1668
+ progress: 1,
1669
+ output: {
1670
+ hooks: generated.result.hooks,
1671
+ files: compactUrls([resultArtifact.url, promptArtifact.url]),
1672
+ artifacts: {
1673
+ prompt_url: promptArtifact.url,
1674
+ result_url: resultArtifact.url
1675
+ },
1676
+ usage: generated.usage
1677
+ }
1678
+ };
1679
+ }
1680
+ }
1681
+ });
1682
+ const brainstormAwarenessStagesPrimitive = definePrimitive({
1683
+ id: PRIMITIVE_BRAINSTORM_AWARENESS_STAGES_ID,
1684
+ kind: "brainstorm_awareness_stages",
1685
+ operations: {
1686
+ run: {
1687
+ description: "Explain which awareness stages to target first for an offer and what ad types to create.",
1688
+ inputSchema: brainstormAwarenessStagesPayloadSchema,
1689
+ workflow: "run",
1690
+ providerHint: "openai"
1691
+ }
1692
+ },
1693
+ jobs: {
1694
+ async run(ctx, input) {
1695
+ const payload = brainstormAwarenessStagesPayloadSchema.parse(input);
1696
+ ctx.logger.progress(0.15, "Mapping awareness stages");
1697
+ const prompt = buildAwarenessStagesPrompt(payload.offer_description);
1698
+ const generated = await runBrainstormText(ctx, {
1699
+ kind: "brainstorm_awareness_stages",
1700
+ provider: payload.provider,
1701
+ model: payload.model,
1702
+ prompt,
1703
+ temperature: 0.55
1704
+ });
1705
+ const promptArtifact = await ctx.storage.putText("prompt.txt", prompt);
1706
+ const resultArtifact = await ctx.storage.putText("response.md", generated.text, "text/markdown; charset=utf-8");
1707
+ ctx.logger.progress(1, "Awareness-stage brainstorm complete");
1708
+ return {
1709
+ progress: 1,
1710
+ output: {
1711
+ response: generated.text,
1712
+ files: compactUrls([resultArtifact.url, promptArtifact.url]),
1713
+ artifacts: {
1714
+ prompt_url: promptArtifact.url,
1715
+ response_url: resultArtifact.url
1716
+ },
1717
+ usage: generated.usage
1718
+ }
1719
+ };
1720
+ }
1721
+ }
1722
+ });
1723
+ const brainstormAnglesPrimitive = definePrimitive({
1724
+ id: PRIMITIVE_BRAINSTORM_ANGLES_ID,
1725
+ kind: "brainstorm_angles",
1726
+ operations: {
1727
+ run: {
1728
+ description: "Generate diverse persuasive TikTok advertising angles for an offer at the selected awareness levels.",
1729
+ inputSchema: brainstormAnglesPayloadSchema,
1730
+ workflow: "run",
1731
+ providerHint: "openai"
1732
+ }
1733
+ },
1734
+ jobs: {
1735
+ async run(ctx, input) {
1736
+ const payload = brainstormAnglesPayloadSchema.parse(input);
1737
+ ctx.logger.progress(0.15, "Brainstorming angles");
1738
+ const prompt = buildAnglesPrompt(payload);
1739
+ const generated = await runBrainstormJson(ctx, {
1740
+ kind: "brainstorm_angles",
1741
+ provider: payload.provider,
1742
+ model: payload.model,
1743
+ prompt,
1744
+ resultSchema: brainstormAnglesResultSchema
1745
+ });
1746
+ const promptArtifact = await ctx.storage.putText("prompt.txt", prompt);
1747
+ const resultArtifact = await ctx.storage.putJson("result.json", generated.result);
1748
+ ctx.logger.progress(1, "Angle brainstorm complete");
1749
+ return {
1750
+ progress: 1,
1751
+ output: {
1752
+ angles: generated.result.angles,
1753
+ awareness: {
1754
+ problem_awareness: payload.problem_awareness,
1755
+ solution_awareness: payload.solution_awareness
1756
+ },
1757
+ files: compactUrls([resultArtifact.url, promptArtifact.url]),
1758
+ artifacts: {
1759
+ prompt_url: promptArtifact.url,
1760
+ result_url: resultArtifact.url
1761
+ },
1762
+ usage: generated.usage
1763
+ }
1764
+ };
1765
+ }
1766
+ }
1767
+ });
1768
+ const brainstormColdstartPrimitive = definePrimitive({
1769
+ id: PRIMITIVE_BRAINSTORM_COLDSTART_ID,
1770
+ kind: "brainstorm_coldstart",
1771
+ operations: {
1772
+ run: {
1773
+ description: "Generate a high-signal cold-start questionnaire that helps a customer build a TikTok content strategy from zero.",
1774
+ inputSchema: brainstormColdstartPayloadSchema,
1775
+ workflow: "run",
1776
+ providerHint: "openai"
1777
+ }
1778
+ },
1779
+ jobs: {
1780
+ async run(ctx, input) {
1781
+ const payload = brainstormColdstartPayloadSchema.parse(input);
1782
+ ctx.logger.progress(0.15, "Generating cold-start questions");
1783
+ const prompt = buildColdstartPrompt(payload.user_message, payload.count);
1784
+ const generated = await runBrainstormJson(ctx, {
1785
+ kind: "brainstorm_coldstart",
1786
+ provider: payload.provider,
1787
+ model: payload.model,
1788
+ prompt,
1789
+ resultSchema: brainstormColdstartResultSchema
1790
+ });
1791
+ const promptArtifact = await ctx.storage.putText("prompt.txt", prompt);
1792
+ const resultArtifact = await ctx.storage.putJson("result.json", generated.result);
1793
+ ctx.logger.progress(1, "Cold-start questionnaire complete");
1794
+ return {
1795
+ progress: 1,
1796
+ output: {
1797
+ questions: generated.result.questions,
1798
+ encouragement: "Encourage the user to save their answers in a Google Doc or markdown file and upload that context into Vidfarm for future strategy, scripting, and template runs.",
1799
+ files: compactUrls([resultArtifact.url, promptArtifact.url]),
1800
+ artifacts: {
1801
+ prompt_url: promptArtifact.url,
1802
+ result_url: resultArtifact.url
1803
+ },
1804
+ usage: generated.usage
1805
+ }
1806
+ };
1807
+ }
1808
+ }
1809
+ });
1810
+ const brainstormProductPlacementPrimitive = definePrimitive({
1811
+ id: PRIMITIVE_BRAINSTORM_PRODUCT_PLACEMENT_ID,
1812
+ kind: "brainstorm_product_placement",
1813
+ operations: {
1814
+ run: {
1815
+ description: "Watch a source video and identify concrete, timestamped product-placement opportunities for a given product/offer.",
1816
+ inputSchema: brainstormProductPlacementPayloadSchema,
1817
+ workflow: "run",
1818
+ providerHint: "gemini"
1819
+ }
1820
+ },
1821
+ jobs: {
1822
+ async run(ctx, input) {
1823
+ const payload = brainstormProductPlacementPayloadSchema.parse(input);
1824
+ ctx.logger.progress(0.15, "Analyzing video for product placement opportunities");
1825
+ const provider = payload.provider ?? preferredVideoTextProvider(ctx.providers.listAvailableProviders());
1826
+ const prompt = buildProductPlacementPrompt(payload.offer_description, payload.count);
1827
+ const generated = await runBrainstormJson(ctx, {
1828
+ kind: "brainstorm_product_placement",
1829
+ provider,
1830
+ model: payload.model,
1831
+ prompt,
1832
+ attachments: [payload.source_video_url],
1833
+ resultSchema: brainstormProductPlacementResultSchema
1834
+ });
1835
+ const promptArtifact = await ctx.storage.putText("prompt.txt", prompt);
1836
+ const resultArtifact = await ctx.storage.putJson("result.json", generated.result);
1837
+ ctx.logger.progress(1, "Product-placement analysis complete");
1838
+ return {
1839
+ progress: 1,
1840
+ output: {
1841
+ opportunities: generated.result.opportunities,
1842
+ source_video_url: payload.source_video_url,
1843
+ files: compactUrls([resultArtifact.url, promptArtifact.url]),
1844
+ artifacts: {
1845
+ prompt_url: promptArtifact.url,
1846
+ result_url: resultArtifact.url
1847
+ },
1848
+ usage: generated.usage
1849
+ }
1850
+ };
1851
+ }
1852
+ }
1853
+ });
1854
+ class PrimitiveRegistry {
1855
+ definitions = new Map([
1856
+ [imageGeneratePrimitive.id, imageGeneratePrimitive],
1857
+ [imageEditPrimitive.id, imageEditPrimitive],
1858
+ [imageInpaintPrimitive.id, imageInpaintPrimitive],
1859
+ [imageRenderHtmlPrimitive.id, imageRenderHtmlPrimitive],
1860
+ [imageRemoveBackgroundPrimitive.id, imageRemoveBackgroundPrimitive],
1861
+ [videoGeneratePrimitive.id, videoGeneratePrimitive],
1862
+ [videoRenderSlidesPrimitive.id, videoRenderSlidesPrimitive],
1863
+ [mediaDedupePrimitive.id, mediaDedupePrimitive],
1864
+ [videoNormalizePrimitive.id, videoNormalizePrimitive],
1865
+ [videoDownloadPrimitive.id, videoDownloadPrimitive],
1866
+ [videoTrimPrimitive.id, videoTrimPrimitive],
1867
+ [videoExtractAudioPrimitive.id, videoExtractAudioPrimitive],
1868
+ [audioTrimPrimitive.id, audioTrimPrimitive],
1869
+ [videoProbePrimitive.id, videoProbePrimitive],
1870
+ [audioProbePrimitive.id, audioProbePrimitive],
1871
+ [videoExtractFramePrimitive.id, videoExtractFramePrimitive],
1872
+ [videoMutePrimitive.id, videoMutePrimitive],
1873
+ [videoReplaceAudioPrimitive.id, videoReplaceAudioPrimitive],
1874
+ [videoConcatPrimitive.id, videoConcatPrimitive],
1875
+ [audioConcatPrimitive.id, audioConcatPrimitive],
1876
+ [audioNormalizePrimitive.id, audioNormalizePrimitive],
1877
+ [hyperframesRenderPrimitive.id, hyperframesRenderPrimitive],
1878
+ [brainstormHooksPrimitive.id, brainstormHooksPrimitive],
1879
+ [brainstormAwarenessStagesPrimitive.id, brainstormAwarenessStagesPrimitive],
1880
+ [brainstormAnglesPrimitive.id, brainstormAnglesPrimitive],
1881
+ [brainstormColdstartPrimitive.id, brainstormColdstartPrimitive],
1882
+ [brainstormProductPlacementPrimitive.id, brainstormProductPlacementPrimitive]
1883
+ ]);
1884
+ get(id) {
1885
+ return this.definitions.get(id) ?? null;
1886
+ }
1887
+ getByKind(kind) {
1888
+ return this.list().find((definition) => definition.kind === kind) ?? null;
1889
+ }
1890
+ list() {
1891
+ return [...this.definitions.values()];
1892
+ }
1893
+ }
1894
+ export const primitiveRegistry = new PrimitiveRegistry();
1895
+ function normalizeHyperframesCompositionPayload(payload) {
1896
+ const duration = payload.duration_seconds ?? payload.duration ?? payload.layers.reduce((max, layer) => Math.max(max, layer.start + layer.duration), 0);
1897
+ return {
1898
+ compositionId: payload.composition_id,
1899
+ title: payload.title,
1900
+ sourceFolder: payload.source_folder,
1901
+ width: payload.width,
1902
+ height: payload.height,
1903
+ duration,
1904
+ backgroundColor: payload.background_color,
1905
+ css: payload.css,
1906
+ layers: payload.layers.map((layer) => ({
1907
+ id: layer.id,
1908
+ kind: layer.kind,
1909
+ label: layer.label,
1910
+ mode: layer.mode,
1911
+ start: layer.start,
1912
+ duration: layer.duration,
1913
+ track: layer.track,
1914
+ z: layer.z,
1915
+ x: layer.x,
1916
+ y: layer.y,
1917
+ width: layer.width,
1918
+ height: layer.height,
1919
+ src: layer.src,
1920
+ mediaStart: layer.mediaStart ?? layer.media_start,
1921
+ playbackStart: layer.playbackStart ?? layer.playback_start ?? layer.mediaStart ?? layer.media_start,
1922
+ fit: layer.fit,
1923
+ muted: layer.muted,
1924
+ volume: layer.volume,
1925
+ text: layer.text,
1926
+ html: layer.html,
1927
+ color: layer.color,
1928
+ background: layer.background,
1929
+ textBackgroundStyle: layer.textBackgroundStyle ?? layer.text_background_style,
1930
+ fontFamily: layer.fontFamily ?? layer.font_family,
1931
+ fontWeight: layer.fontWeight ?? layer.font_weight,
1932
+ fontSize: layer.fontSize ?? layer.font_size,
1933
+ lineHeight: layer.lineHeight ?? layer.line_height,
1934
+ radius: layer.radius,
1935
+ opacity: layer.opacity,
1936
+ viralAlignment: layer.viralAlignment ?? layer.viral_alignment
1937
+ }))
1938
+ };
1939
+ }
1940
+ async function storePrimitiveImage(ctx, input) {
1941
+ const stored = await ctx.storage.putBuffer(input.key, input.bytes, {
1942
+ contentType: input.contentType,
1943
+ kind: "image",
1944
+ metadata: input.metadata
1945
+ });
1946
+ return { ...stored, contentType: input.contentType };
1947
+ }
1948
+ async function storePrimitiveVideo(ctx, input) {
1949
+ const stored = await ctx.storage.putBuffer(input.key, input.bytes, {
1950
+ contentType: input.contentType,
1951
+ kind: "video",
1952
+ metadata: input.metadata
1953
+ });
1954
+ return { ...stored, contentType: input.contentType };
1955
+ }
1956
+ function buildImagePrimitiveOutput(input) {
1957
+ return {
1958
+ files: input.files,
1959
+ primary_file_url: input.fileUrl,
1960
+ image: {
1961
+ file_url: input.fileUrl,
1962
+ content_type: input.contentType,
1963
+ ...(input.revisedPrompt !== undefined ? { revised_prompt: input.revisedPrompt } : {}),
1964
+ ...(input.derivedFromUrl ? { derived_from_url: input.derivedFromUrl } : {}),
1965
+ ...(input.maskUrl ? { mask_url: input.maskUrl } : {})
1966
+ }
1967
+ };
1968
+ }
1969
+ function defaultImageModelForProvider(provider) {
1970
+ switch (provider) {
1971
+ case "openai":
1972
+ return "gpt-image-2";
1973
+ case "openrouter":
1974
+ return "google/gemini-2.5-flash-image-preview";
1975
+ default:
1976
+ return "gemini-2.5-flash-image";
1977
+ }
1978
+ }
1979
+ function defaultVideoModelForProvider(provider) {
1980
+ switch (provider) {
1981
+ case "openai":
1982
+ return "sora-2";
1983
+ case "gemini":
1984
+ return "veo-3.0-generate-001";
1985
+ case "openrouter":
1986
+ return "bytedance/seedance-2.0";
1987
+ }
1988
+ }
1989
+ function defaultTextModelForProvider(provider) {
1990
+ switch (provider) {
1991
+ case "gemini":
1992
+ return "gemini-2.5-flash";
1993
+ case "openrouter":
1994
+ return "qwen/qwen3.6-plus";
1995
+ case "perplexity":
1996
+ return "sonar";
1997
+ default:
1998
+ return "gpt-5.4-mini";
1999
+ }
2000
+ }
2001
+ function normalizeAiVideoDurationSeconds(explicitSeconds, milliseconds) {
2002
+ const seconds = Number(explicitSeconds);
2003
+ if (Number.isFinite(seconds) && seconds > 0) {
2004
+ return Math.max(1, Math.round(seconds));
2005
+ }
2006
+ const durationMs = Number(milliseconds);
2007
+ if (Number.isFinite(durationMs) && durationMs > 0) {
2008
+ return Math.max(1, Math.ceil(durationMs / 1000));
2009
+ }
2010
+ return undefined;
2011
+ }
2012
+ function extensionForOutputFormat(format) {
2013
+ return format === "jpeg" ? "jpg" : format;
2014
+ }
2015
+ function contentTypeForOutputFormat(format, fallback) {
2016
+ if (format === "jpeg") {
2017
+ return "image/jpeg";
2018
+ }
2019
+ if (format === "webp") {
2020
+ return "image/webp";
2021
+ }
2022
+ return fallback && fallback !== "image/svg+xml" ? fallback : "image/png";
2023
+ }
2024
+ async function normalizePrimitiveImageOutput(bytes, format) {
2025
+ const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
2026
+ if (format === "jpeg") {
2027
+ return {
2028
+ bytes: await sharp(buffer).jpeg({ quality: 92 }).toBuffer(),
2029
+ contentType: "image/jpeg"
2030
+ };
2031
+ }
2032
+ if (format === "webp") {
2033
+ return {
2034
+ bytes: await sharp(buffer).webp({ quality: 92 }).toBuffer(),
2035
+ contentType: "image/webp"
2036
+ };
2037
+ }
2038
+ return {
2039
+ bytes: await sharp(buffer).png().toBuffer(),
2040
+ contentType: "image/png"
2041
+ };
2042
+ }
2043
+ async function recordPrimitiveProviderBilling(ctx, generated, kind) {
2044
+ if (!Number.isFinite(generated.usageCostUsd) || generated.usageCostUsd <= 0) {
2045
+ return;
2046
+ }
2047
+ const providerJob = generated.providerJob ?? {};
2048
+ const providerJobId = providerJob.id ?? providerJob.generation_id;
2049
+ const providerJobIdText = typeof providerJobId === "string" && providerJobId.trim() ? providerJobId.trim() : null;
2050
+ await ctx.billing.record({
2051
+ type: "ai_generation",
2052
+ costUsd: generated.usageCostUsd,
2053
+ idempotencyKey: providerJobIdText ? `primitive-provider:${kind}:${providerJobIdText}` : undefined,
2054
+ occurredAtMs: Date.now(),
2055
+ metadata: {
2056
+ kind,
2057
+ provider_job_id: providerJobIdText,
2058
+ provider_job: providerJob
2059
+ }
2060
+ });
2061
+ }
2062
+ function readPrimitiveAsset(name) {
2063
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
2064
+ const candidates = [
2065
+ path.resolve(moduleDir, "assets", name),
2066
+ path.resolve(moduleDir, "..", "src", "assets", name),
2067
+ // Package-root copy (compiled module lives at <root>/dist/src): covers
2068
+ // `vidfarm serve` launched from an arbitrary cwd out of the npm package.
2069
+ path.resolve(moduleDir, "..", "..", "src", "assets", name),
2070
+ path.resolve(process.cwd(), "dist", "src", "assets", name),
2071
+ path.resolve(process.cwd(), "src", "assets", name)
2072
+ ];
2073
+ const assetPath = candidates.find((candidate) => existsSync(candidate));
2074
+ if (!assetPath) {
2075
+ throw new Error(`Primitive asset not found: ${name}`);
2076
+ }
2077
+ return readFileSync(assetPath, "utf8");
2078
+ }
2079
+ function normalizeOfferDescriptionPayload(raw) {
2080
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2081
+ return raw;
2082
+ }
2083
+ const payload = raw;
2084
+ return {
2085
+ ...payload,
2086
+ offer_description: payload.offer_description
2087
+ ?? payload.offerDescription
2088
+ ?? payload.description
2089
+ ?? payload.offer
2090
+ ?? payload.details,
2091
+ count: payload.count ?? payload.n ?? payload.number_of_hooks ?? payload.number_of_angles ?? payload.question_count
2092
+ };
2093
+ }
2094
+ function normalizeAnglePayload(raw) {
2095
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2096
+ return raw;
2097
+ }
2098
+ const payload = normalizeOfferDescriptionPayload(raw);
2099
+ return {
2100
+ ...payload,
2101
+ problem_awareness: payload.problem_awareness ?? payload.problemAwareness ?? payload.problem_stage ?? payload.problemStage,
2102
+ solution_awareness: payload.solution_awareness ?? payload.solutionAwareness ?? payload.solution_stage ?? payload.solutionStage
2103
+ };
2104
+ }
2105
+ function normalizeColdstartPayload(raw) {
2106
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2107
+ return raw;
2108
+ }
2109
+ const payload = raw;
2110
+ return {
2111
+ ...payload,
2112
+ user_message: payload.user_message
2113
+ ?? payload.userMessage
2114
+ ?? payload.message
2115
+ ?? payload.user_input
2116
+ ?? payload.userInput
2117
+ ?? payload.offer_description
2118
+ ?? payload.offerDescription
2119
+ ?? payload.description
2120
+ ?? payload.offer
2121
+ ?? payload.details,
2122
+ count: payload.count ?? payload.n ?? payload.question_count
2123
+ };
2124
+ }
2125
+ function normalizeProductPlacementPayload(raw) {
2126
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2127
+ return raw;
2128
+ }
2129
+ const payload = normalizeOfferDescriptionPayload(raw);
2130
+ return {
2131
+ ...payload,
2132
+ source_video_url: payload.source_video_url
2133
+ ?? payload.sourceVideoUrl
2134
+ ?? payload.video_url
2135
+ ?? payload.videoUrl
2136
+ ?? payload.source_url
2137
+ ?? payload.sourceUrl
2138
+ ?? payload.url
2139
+ };
2140
+ }
2141
+ function preferredTextProvider(availableProviders) {
2142
+ const priority = ["openai", "gemini", "openrouter", "perplexity"];
2143
+ return priority.find((provider) => availableProviders.includes(provider)) ?? "openai";
2144
+ }
2145
+ // Product-placement analysis needs a model that can watch video. Gemini has native video
2146
+ // understanding; OpenRouter can route to multimodal models; fall back to whatever is available.
2147
+ function preferredVideoTextProvider(availableProviders) {
2148
+ const priority = ["gemini", "openrouter", "openai", "perplexity"];
2149
+ return priority.find((provider) => availableProviders.includes(provider)) ?? "gemini";
2150
+ }
2151
+ async function runBrainstormText(ctx, input) {
2152
+ const provider = input.provider ?? preferredTextProvider(ctx.providers.listAvailableProviders());
2153
+ const model = input.model ?? defaultTextModelForProvider(provider);
2154
+ const generated = await ctx.providers.generateText({
2155
+ provider,
2156
+ model,
2157
+ prompt: input.prompt,
2158
+ temperature: input.temperature ?? 0.7,
2159
+ responseFormat: input.responseFormat ?? "text",
2160
+ ...(input.attachments && input.attachments.length ? { attachments: input.attachments } : {})
2161
+ });
2162
+ await recordPrimitiveProviderBilling(ctx, {
2163
+ usageCostUsd: generated.usage.costUsd,
2164
+ providerJob: {
2165
+ provider,
2166
+ model,
2167
+ usage: generated.usage
2168
+ }
2169
+ }, input.kind);
2170
+ return {
2171
+ provider,
2172
+ model,
2173
+ text: generated.text.trim(),
2174
+ usage: generated.usage
2175
+ };
2176
+ }
2177
+ async function runBrainstormJson(ctx, input) {
2178
+ const generated = await runBrainstormText(ctx, {
2179
+ ...input,
2180
+ temperature: 0.35,
2181
+ responseFormat: "json"
2182
+ });
2183
+ const parsed = input.resultSchema.parse(parseJsonFromModelText(generated.text));
2184
+ return {
2185
+ ...generated,
2186
+ result: parsed
2187
+ };
2188
+ }
2189
+ function parseJsonFromModelText(text) {
2190
+ const trimmed = text.trim();
2191
+ try {
2192
+ return JSON.parse(trimmed);
2193
+ }
2194
+ catch {
2195
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
2196
+ if (fenced?.[1]) {
2197
+ return JSON.parse(fenced[1]);
2198
+ }
2199
+ const firstBrace = trimmed.indexOf("{");
2200
+ const lastBrace = trimmed.lastIndexOf("}");
2201
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
2202
+ return JSON.parse(trimmed.slice(firstBrace, lastBrace + 1));
2203
+ }
2204
+ throw new Error("AI returned invalid JSON for brainstorm primitive output.");
2205
+ }
2206
+ }
2207
+ function buildHooksPrompt(offerDescription, count) {
2208
+ return [
2209
+ "You are Vidfarm's elite paid-social hook strategist.",
2210
+ "Your job is to generate high-upside ad hooks for TikTok-native creative.",
2211
+ "Bias toward hooks that feel native to TikTok, but keep raw persuasive power more important than surface trendiness.",
2212
+ "Max creativity. Do not hold back. Be as diverse as possible while staying relevant to the offer.",
2213
+ "Use many angle families across the set: pain, status, curiosity, gossip, confession, myth-busting, shock, taboo, mechanism, objection, identity, urgency, pattern interrupt, transformation, proof, enemy, mistake, secret, social proof, aspiration, and contrarian frames.",
2214
+ "Each hook should be sharp, testable, and materially different from the others.",
2215
+ `Generate exactly ${count} hooks.`,
2216
+ "Return strict JSON only with shape {\"hooks\":[{\"hook\":\"...\",\"explanation\":\"...\"}]}.",
2217
+ "Explanation should be 1-3 sentences on why the hook works and what angle it is leveraging.",
2218
+ "",
2219
+ "Offer details:",
2220
+ offerDescription.trim(),
2221
+ "",
2222
+ "Reference lessons to apply:",
2223
+ hooksReferenceText
2224
+ ].join("\n");
2225
+ }
2226
+ function buildAwarenessStagesPrompt(offerDescription) {
2227
+ return [
2228
+ "You are Vidfarm's direct-response strategist.",
2229
+ "The user is not sure what type of ads to produce.",
2230
+ "Use Eugene Schwartz-style awareness-stage reasoning to recommend what kinds of ads to make first.",
2231
+ "Write a practical response for TikTok-first performance marketing, but keep the advice broadly useful for paid social.",
2232
+ "Do not return JSON. Return a clear markdown string.",
2233
+ "Include:",
2234
+ "- which awareness stages are most promising first",
2235
+ "- why those stages fit this offer",
2236
+ "- what ad types/messages belong in each stage",
2237
+ "- what to test before moving to deeper or broader awareness stages",
2238
+ "- a short next-step recommendation",
2239
+ "",
2240
+ "Offer details:",
2241
+ offerDescription.trim(),
2242
+ "",
2243
+ "Reference lessons to apply:",
2244
+ awarenessReferenceText
2245
+ ].join("\n");
2246
+ }
2247
+ function buildAnglesPrompt(payload) {
2248
+ return [
2249
+ "You are Vidfarm's senior direct-response angle strategist.",
2250
+ "Generate persuasive advertising angles for TikTok-native ads.",
2251
+ "Persuasive depth matters more than format gimmicks. The user wants raw winning angles to test.",
2252
+ "Do not mention templates, scenes, or production mechanics. Focus on the strategic angle itself.",
2253
+ "Create major diversity across the set: pain, myths, enemies, mechanisms, shocking truths, confessions, gossip/social proof, objections, hidden costs, status, identity, aspiration, taboo, mistake, urgency, proof, authority, trend hijack, curiosity, and contrarian frames.",
2254
+ "Favor known winning angle archetypes while still making them specific to the offer.",
2255
+ `Generate exactly ${payload.count} angles.`,
2256
+ "Return strict JSON only with shape {\"angles\":[{\"angle\":\"...\",\"explanation\":\"...\"}]}",
2257
+ "Explanation should say why the angle fits this awareness state and what persuasive lever it uses.",
2258
+ "",
2259
+ `Problem awareness: ${payload.problem_awareness}`,
2260
+ `Solution awareness: ${payload.solution_awareness}`,
2261
+ `Derived stage note: ${describeAwarenessSelection(payload.problem_awareness, payload.solution_awareness)}`,
2262
+ "",
2263
+ "Offer details:",
2264
+ payload.offer_description.trim(),
2265
+ "",
2266
+ "Reference lessons to apply from awareness stages:",
2267
+ awarenessReferenceText,
2268
+ "",
2269
+ "Reference lessons to apply from hooks:",
2270
+ hooksReferenceText
2271
+ ].join("\n");
2272
+ }
2273
+ function buildColdstartPrompt(userMessage, count) {
2274
+ return [
2275
+ "You are Vidfarm's TikTok growth strategist.",
2276
+ "The customer has no idea where to start.",
2277
+ "Generate the best possible foundational Q&A questions for the customer and AI to discuss so they can build a strong TikTok strategy from zero.",
2278
+ "The goal is to produce questions whose answers can later power awareness-stage selection, angle generation, hooks, scripts, and template choice.",
2279
+ `Generate exactly ${count} questions.`,
2280
+ "You must include questions covering, at minimum:",
2281
+ "- one-line offer",
2282
+ "- one-paragraph offer",
2283
+ "- precise target customer psyche",
2284
+ "- customer pain in their own words",
2285
+ "- awareness levels or uncertainty about them",
2286
+ "Add more strong questions beyond those when useful.",
2287
+ "Return strict JSON only with shape {\"questions\":[{\"question\":\"...\",\"why_it_matters\":\"...\"}]}",
2288
+ "Questions should be phrased so the user can answer them in a Google Doc or markdown file for future use.",
2289
+ "At least one question should explicitly encourage them to save these answers for reuse inside Vidfarm.",
2290
+ "",
2291
+ "User message:",
2292
+ userMessage.trim()
2293
+ ].join("\n");
2294
+ }
2295
+ function buildProductPlacementPrompt(offerDescription, count) {
2296
+ return [
2297
+ "You are Vidfarm's native-advertising product-placement strategist.",
2298
+ "You are watching the attached video. Study what is actually on screen: scenes, actions, objects, spoken lines, on-screen text, transitions, and emotional beats.",
2299
+ "Your job is to find the highest-leverage moments to organically place the user's product into THIS video so it feels native rather than bolted on.",
2300
+ "Bias toward placements that ride the existing attention, emotion, or story beat of the moment. Native and believable beats loud and interruptive.",
2301
+ "Consider many placement types: on-screen prop/set dressing, character interaction with the product, verbal mention or callout, text/caption overlay, sticker or lower-third, b-roll cutaway, screen/UI replacement, before/after demo, and end-card CTA.",
2302
+ `Identify exactly ${count} distinct product-placement opportunities, ordered from strongest to weakest.`,
2303
+ "Each opportunity must reference a real, specific moment you actually saw in the video, with an approximate timestamp or range (for example \"0:04\" or \"0:12-0:18\").",
2304
+ "Return strict JSON only with shape {\"opportunities\":[{\"timestamp\":\"...\",\"scene\":\"...\",\"placement_type\":\"...\",\"placement_idea\":\"...\",\"why_it_works\":\"...\"}]}.",
2305
+ "\"scene\" briefly describes what is happening on screen at that moment. \"placement_idea\" is the concrete, actionable way to insert the product. \"why_it_works\" explains the persuasion/attention logic.",
2306
+ "",
2307
+ "Product / offer to place:",
2308
+ offerDescription.trim()
2309
+ ].join("\n");
2310
+ }
2311
+ function describeAwarenessSelection(problemAwareness, solutionAwareness) {
2312
+ if (problemAwareness === "problem_unaware" && solutionAwareness === "solution_unaware") {
2313
+ return "Lead with unaware-style pattern interrupts, identity, curiosity, story, and emotionally resonant entry points before surfacing the problem.";
2314
+ }
2315
+ if (problemAwareness === "problem_aware" && solutionAwareness === "solution_unaware") {
2316
+ return "Lead with sharp articulation of the pain and consequences, then reveal a new possible solution path.";
2317
+ }
2318
+ if (problemAwareness === "problem_aware" && solutionAwareness === "solution_aware") {
2319
+ return "Lead with strong solution-category promises, differentiation, mechanism, and proof.";
2320
+ }
2321
+ return "Lead with high-curiosity or identity-first entry points that bridge people from vague discomfort toward the existence of a real solution category.";
2322
+ }
2323
+ async function recordPrimitiveMediaBilling(ctx, input) {
2324
+ if (!Number.isFinite(input.costUsd) || input.costUsd <= 0) {
2325
+ return;
2326
+ }
2327
+ await ctx.billing.record({
2328
+ type: "cpu_estimate",
2329
+ costUsd: input.costUsd,
2330
+ costCenterSlug: input.costCenterSlug,
2331
+ idempotencyKey: input.idempotencyKey,
2332
+ occurredAtMs: Date.now(),
2333
+ metadata: input.metadata
2334
+ });
2335
+ }
2336
+ async function executePrimitiveMediaOperation(ctx, input) {
2337
+ const execution = await primitiveMediaService.execute({
2338
+ operation: input.operation,
2339
+ payload: input.payload,
2340
+ outputKey: ctx.storage.resolveKey(input.outputKey),
2341
+ localOutputKey: input.outputKey,
2342
+ storeLocal: async (key, bytes, contentType) => ctx.storage.putBuffer(key, bytes, {
2343
+ contentType,
2344
+ kind: contentType.startsWith("audio/") ? "audio" : contentType.startsWith("image/") ? "image" : "video",
2345
+ metadata: {
2346
+ primitive_media_operation: input.operation
2347
+ }
2348
+ })
2349
+ });
2350
+ await recordPrimitiveMediaBilling(ctx, {
2351
+ costUsd: execution.billingCostUsd,
2352
+ costCenterSlug: execution.billingCostCenterSlug,
2353
+ idempotencyKey: `${execution.billingCostCenterSlug}:${ctx.customer.id}:${input.operation}:${execution.storageKey}`,
2354
+ metadata: {
2355
+ ...execution.billingMetadata,
2356
+ output_key: execution.storageKey
2357
+ }
2358
+ });
2359
+ return execution;
2360
+ }
2361
+ function inferMediaType(mediaUrl) {
2362
+ return /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(mediaUrl) ? "video" : "image";
2363
+ }
2364
+ function compactUrls(values) {
2365
+ return values.filter((value) => Boolean(value));
2366
+ }
2367
+ function isVidfarmControlPlaneArtifactUrl(value) {
2368
+ try {
2369
+ const url = new URL(value);
2370
+ return /\/api\/v1\/(?:user\/me|templates\/[^/]+|primitives(?:\/[^/]+)?)\/jobs\/[^/]+\/artifacts\/?$/u.test(url.pathname);
2371
+ }
2372
+ catch {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ function selectDownloadedVideoMediaCandidate(medias, qualityPreference) {
2377
+ const playable = medias.filter((media) => typeof media.url === "string"
2378
+ && media.url.length > 0
2379
+ && media.videoAvailable !== false
2380
+ && (media.extension?.toLowerCase() === "mp4" || /\.mp4(\?|#|$)/i.test(media.url)));
2381
+ if (!playable.length)
2382
+ return null;
2383
+ const preferredRank = qualityPreference === "full_hd"
2384
+ ? ["full hd", "hd", "best", ""]
2385
+ : qualityPreference === "hd"
2386
+ ? ["hd", "full hd", "best", ""]
2387
+ : ["full hd", "hd", "best", ""];
2388
+ const sorted = playable.slice().sort((left, right) => {
2389
+ const leftRank = preferredRank.indexOf(normalizeDownloadedVideoQuality(left.quality));
2390
+ const rightRank = preferredRank.indexOf(normalizeDownloadedVideoQuality(right.quality));
2391
+ const normalizedLeftRank = leftRank === -1 ? preferredRank.length : leftRank;
2392
+ const normalizedRightRank = rightRank === -1 ? preferredRank.length : rightRank;
2393
+ if (normalizedLeftRank !== normalizedRightRank)
2394
+ return normalizedLeftRank - normalizedRightRank;
2395
+ return (right.size ?? 0) - (left.size ?? 0);
2396
+ });
2397
+ return sorted[0] ?? null;
2398
+ }
2399
+ function normalizeDownloadedVideoQuality(value) {
2400
+ return String(value ?? "").trim().toLowerCase().replace(/_/g, " ");
2401
+ }
2402
+ async function fetchRemoteBinary(url, fallbackContentType) {
2403
+ const response = await fetch(url);
2404
+ if (!response.ok) {
2405
+ throw new Error(`Unable to download remote media: HTTP ${response.status}`);
2406
+ }
2407
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim() || fallbackContentType;
2408
+ return {
2409
+ bytes: Buffer.from(await response.arrayBuffer()),
2410
+ contentType
2411
+ };
2412
+ }
2413
+ function normalizeVideoContentType(contentType) {
2414
+ const normalized = contentType.toLowerCase();
2415
+ if (normalized === "video/quicktime")
2416
+ return "video/quicktime";
2417
+ if (normalized === "video/webm")
2418
+ return "video/webm";
2419
+ return "video/mp4";
2420
+ }
2421
+ function videoExtensionForContentType(contentType) {
2422
+ if (contentType === "video/quicktime")
2423
+ return "mov";
2424
+ if (contentType === "video/webm")
2425
+ return "webm";
2426
+ return "mp4";
2427
+ }
2428
+ function safeUrlHost(value) {
2429
+ try {
2430
+ return new URL(value).host;
2431
+ }
2432
+ catch {
2433
+ return null;
2434
+ }
2435
+ }
2436
+ //# sourceMappingURL=primitive-registry.js.map