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