@mevdragon/vidfarm-devcli 0.2.12 → 0.2.13

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