@mevdragon/vidfarm-devcli 0.2.1 → 0.2.3
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/.env.example +6 -39
- package/GETTING_STARTED.developers.md +87 -0
- package/README.md +94 -238
- package/SKILL.developer.md +430 -104
- package/dist/src/account-pages.js +1 -1
- package/dist/src/app.js +93 -5
- package/dist/src/cli.js +456 -8
- package/dist/src/config.js +3 -2
- package/dist/src/context.js +30 -11
- package/dist/src/db.js +2 -57
- package/dist/src/dev-app.js +0 -1
- package/dist/src/index.js +4 -2
- package/dist/src/lib/template-paths.js +21 -0
- package/dist/src/runtime.js +3 -1
- package/dist/src/services/auth.js +4 -4
- package/dist/src/services/job-logs.js +186 -0
- package/dist/src/services/jobs.js +3 -2
- package/dist/src/services/providers.js +14 -6
- package/dist/src/services/storage.js +85 -2
- package/dist/src/services/template-sources.js +29 -3
- package/dist/templates/template_0000/src/lib/images.js +46 -86
- package/dist/templates/template_0000/src/template.js +277 -53
- package/package.json +5 -6
- package/templates/template_0000/README.md +8 -52
- package/templates/template_0000/SKILL.md +35 -3
- package/templates/template_0000/package.json +3 -6
- package/templates/template_0000/src/lib/images.js +46 -86
- package/templates/template_0000/src/lib/images.ts +55 -98
- package/templates/template_0000/src/template-dna.js +9 -0
- package/templates/template_0000/src/template.js +523 -199
- package/templates/template_0000/src/template.ts +356 -61
- package/templates/template_0000/template.config.json +7 -12
- package/AWS_REMOTION_HANDOFF.md +0 -311
- package/PLATFORM_SPEC.md +0 -1039
- package/SKILL.director.md +0 -599
- package/dist/infra/cdk/bin/vidfarm-prod.js +0 -59
- package/dist/infra/cdk/lib/vidfarm-prod-stack.js +0 -212
- package/templates/template_0000/package-lock.json +0 -5505
- package/templates/template_0000/scripts/create-site.mjs +0 -27
- package/templates/template_0000/scripts/render-cloud.mjs +0 -72
|
@@ -39,6 +39,32 @@ const NATIVE_UI_TEXT_ZONE = {
|
|
|
39
39
|
maxCenterYPercent: 0.58,
|
|
40
40
|
} as const;
|
|
41
41
|
const TEXT_BACKGROUND_NONE = "none" as const;
|
|
42
|
+
// Documentation-only dependency map for humans and agents.
|
|
43
|
+
// The current template standard has no first-class dependencies field, so
|
|
44
|
+
// provider/model requirements must be declared in source and SKILL.md.
|
|
45
|
+
const TEMPLATE_PROVIDER_REQUIREMENTS = {
|
|
46
|
+
image: [
|
|
47
|
+
{ provider: "openai", models: ["gpt-image-1", "gpt-image-2"], strict: false },
|
|
48
|
+
{ provider: "gemini", models: ["gemini-3.1-flash-image-preview", "gemini-2.5-flash-image"], strict: false },
|
|
49
|
+
{ provider: "openrouter", models: ["bytedance/seedance-2.0", "bytedance-seed/seedream-4.5"], strict: false, planned: true },
|
|
50
|
+
],
|
|
51
|
+
text: [
|
|
52
|
+
{ provider: "openai", models: ["gpt-5.4"], strict: false },
|
|
53
|
+
{ provider: "gemini", models: ["gemini-3.1-flash-lite", "gemini-2.5-flash-lite"], strict: false },
|
|
54
|
+
{ provider: "openrouter", models: ["qwen/qwen3.6-flash"], strict: false },
|
|
55
|
+
],
|
|
56
|
+
layout_analysis: [
|
|
57
|
+
{ provider: "openai", models: ["gpt-5.4"], strict: false },
|
|
58
|
+
{ provider: "gemini", models: ["gemini-3.1-flash-lite", "gemini-2.5-flash-lite"], strict: false },
|
|
59
|
+
{ provider: "openrouter", models: ["qwen/qwen3.6-flash"], strict: false },
|
|
60
|
+
],
|
|
61
|
+
video: [
|
|
62
|
+
{ provider: "openai", models: ["sora-2"], strict: false, planned: true },
|
|
63
|
+
{ provider: "gemini", models: ["veo-3.0-generate-001"], strict: false, planned: true },
|
|
64
|
+
],
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
void TEMPLATE_PROVIDER_REQUIREMENTS;
|
|
42
68
|
|
|
43
69
|
const legacySlideInputSchema = z.union([
|
|
44
70
|
z.tuple([z.string().min(3), z.string().min(1)]),
|
|
@@ -81,8 +107,21 @@ type Layout = {
|
|
|
81
107
|
justification: string;
|
|
82
108
|
};
|
|
83
109
|
|
|
110
|
+
type SlideSourceType = "ai_prompt" | "image_source";
|
|
111
|
+
|
|
112
|
+
type NormalizedSlideInput = {
|
|
113
|
+
sourceType: SlideSourceType;
|
|
114
|
+
sourceValue: string;
|
|
115
|
+
imagePrompt: string;
|
|
116
|
+
imagePromptAttachments: string[];
|
|
117
|
+
overlayText: string;
|
|
118
|
+
durationMs: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
84
121
|
type SlideOutput = {
|
|
85
122
|
index: number;
|
|
123
|
+
sourceType: SlideSourceType;
|
|
124
|
+
sourceValue: string;
|
|
86
125
|
imagePrompt: string;
|
|
87
126
|
imagePromptAttachments: string[];
|
|
88
127
|
overlayText: string;
|
|
@@ -213,38 +252,61 @@ export const template0000Definition = defineTemplate({
|
|
|
213
252
|
const payload = createSlideshowInputSchema.parse(input);
|
|
214
253
|
ctx.logger.progress(0.04, "Starting template_0000 slideshow image stage");
|
|
215
254
|
const provider = parseImageProvider(ctx.templateConfig.defaultProvider);
|
|
255
|
+
const configuredTextModel =
|
|
256
|
+
typeof ctx.templateConfig.textModel === "string"
|
|
257
|
+
? ctx.templateConfig.textModel
|
|
258
|
+
: null;
|
|
259
|
+
const configuredImageModel =
|
|
260
|
+
typeof ctx.templateConfig.imageModel === "string"
|
|
261
|
+
? ctx.templateConfig.imageModel
|
|
262
|
+
: null;
|
|
216
263
|
const textModel = String(
|
|
217
|
-
|
|
264
|
+
configuredTextModel ?? defaultTextModelForProvider(provider),
|
|
218
265
|
);
|
|
219
266
|
const imageModel = String(
|
|
220
|
-
|
|
267
|
+
configuredImageModel ?? defaultImageModelForProvider(provider),
|
|
221
268
|
);
|
|
222
269
|
const textStyle = resolveTextStyleSpec(ctx.templateConfig);
|
|
223
270
|
|
|
224
271
|
const slides: SlideOutput[] = [];
|
|
225
272
|
for (const [index, rawSlide] of payload.slides.entries()) {
|
|
226
|
-
const {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
0.08 + (index / payload.slides.length) * 0.42,
|
|
230
|
-
`Generating slide ${index + 1} image`,
|
|
231
|
-
);
|
|
232
|
-
const image = await generatePortraitCandidateImage(ctx, {
|
|
233
|
-
provider,
|
|
234
|
-
imageModel,
|
|
273
|
+
const {
|
|
274
|
+
sourceType,
|
|
275
|
+
sourceValue,
|
|
235
276
|
imagePrompt,
|
|
236
277
|
imagePromptAttachments,
|
|
237
278
|
overlayText,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
durationMs,
|
|
280
|
+
} = normalizeSlideInput(rawSlide);
|
|
281
|
+
ctx.logger.progress(
|
|
282
|
+
0.08 + (index / payload.slides.length) * 0.42,
|
|
283
|
+
sourceType === "image_source"
|
|
284
|
+
? `Loading slide ${index + 1} source image`
|
|
285
|
+
: `Generating slide ${index + 1} image`,
|
|
286
|
+
);
|
|
287
|
+
const image =
|
|
288
|
+
sourceType === "image_source"
|
|
289
|
+
? await loadExistingSlideImage(sourceValue)
|
|
290
|
+
: await generatePortraitCandidateImage(ctx, {
|
|
291
|
+
provider,
|
|
292
|
+
imageModel,
|
|
293
|
+
configuredProvider: provider,
|
|
294
|
+
configuredImageModel,
|
|
295
|
+
imagePrompt,
|
|
296
|
+
imagePromptAttachments,
|
|
297
|
+
overlayText,
|
|
298
|
+
});
|
|
299
|
+
if (sourceType === "ai_prompt") {
|
|
300
|
+
await ctx.billing.record({
|
|
301
|
+
type: "ai_generation",
|
|
302
|
+
costUsd: 0.04,
|
|
303
|
+
metadata: {
|
|
304
|
+
stage: "image_generation",
|
|
305
|
+
slideIndex: index,
|
|
306
|
+
model: imageModel,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
248
310
|
|
|
249
311
|
ctx.logger.progress(
|
|
250
312
|
0.13 + (index / payload.slides.length) * 0.3,
|
|
@@ -312,6 +374,8 @@ export const template0000Definition = defineTemplate({
|
|
|
312
374
|
|
|
313
375
|
slides.push({
|
|
314
376
|
index,
|
|
377
|
+
sourceType,
|
|
378
|
+
sourceValue,
|
|
315
379
|
imagePrompt,
|
|
316
380
|
imagePromptAttachments,
|
|
317
381
|
overlayText,
|
|
@@ -328,6 +392,8 @@ export const template0000Definition = defineTemplate({
|
|
|
328
392
|
const metaDetails = await generateMetaDetails(ctx, {
|
|
329
393
|
provider,
|
|
330
394
|
textModel,
|
|
395
|
+
configuredProvider: provider,
|
|
396
|
+
configuredTextModel,
|
|
331
397
|
slides,
|
|
332
398
|
metaDetailsPrompt: payload.meta_details_prompt,
|
|
333
399
|
});
|
|
@@ -381,6 +447,8 @@ export const template0000Definition = defineTemplate({
|
|
|
381
447
|
meta_details_prompt: payload.meta_details_prompt ?? null,
|
|
382
448
|
slides: slides.map((slide) => ({
|
|
383
449
|
index: slide.index,
|
|
450
|
+
sourceType: slide.sourceType,
|
|
451
|
+
sourceValue: slide.sourceValue,
|
|
384
452
|
imagePrompt: slide.imagePrompt,
|
|
385
453
|
imagePromptAttachments: slide.imagePromptAttachments,
|
|
386
454
|
overlayText: slide.overlayText,
|
|
@@ -499,6 +567,8 @@ async function generatePortraitCandidateImage(
|
|
|
499
567
|
input: {
|
|
500
568
|
provider: ImageProvider;
|
|
501
569
|
imageModel: string;
|
|
570
|
+
configuredProvider: ImageProvider;
|
|
571
|
+
configuredImageModel: string | null;
|
|
502
572
|
imagePrompt: string;
|
|
503
573
|
imagePromptAttachments: string[];
|
|
504
574
|
overlayText: string;
|
|
@@ -553,31 +623,48 @@ async function generatePortraitCandidateImage(
|
|
|
553
623
|
prompt: string;
|
|
554
624
|
score: number;
|
|
555
625
|
} | null = null;
|
|
626
|
+
let lastError: Error | null = null;
|
|
556
627
|
|
|
557
|
-
for (const
|
|
558
|
-
const
|
|
559
|
-
provider
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
628
|
+
for (const provider of providerFailoverOrder(input.provider)) {
|
|
629
|
+
const imageModel = resolveImageModelForAttempt(
|
|
630
|
+
provider,
|
|
631
|
+
input.configuredProvider,
|
|
632
|
+
input.configuredImageModel,
|
|
633
|
+
);
|
|
634
|
+
try {
|
|
635
|
+
for (const prompt of attempts) {
|
|
636
|
+
const image = await ctx.providers.generateImage({
|
|
637
|
+
provider,
|
|
638
|
+
model: imageModel,
|
|
639
|
+
prompt,
|
|
640
|
+
promptAttachments: input.imagePromptAttachments,
|
|
641
|
+
size: sourceImageSizeForProvider(provider),
|
|
642
|
+
aspectRatio: "9:16",
|
|
643
|
+
imageSize: sourceImageOutputSizeForProvider(provider, imageModel),
|
|
644
|
+
});
|
|
645
|
+
const score = await portraitScore(image.bytes);
|
|
646
|
+
if (!best || score < best.score) {
|
|
647
|
+
best = { ...image, prompt, score };
|
|
648
|
+
}
|
|
649
|
+
if (score <= 0.16) {
|
|
650
|
+
return best;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (best) {
|
|
654
|
+
return best;
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
658
|
+
ctx.logger.warn("Image generation provider failed, trying next provider", {
|
|
659
|
+
provider,
|
|
660
|
+
model: imageModel,
|
|
661
|
+
message: lastError.message,
|
|
662
|
+
});
|
|
576
663
|
}
|
|
577
664
|
}
|
|
578
665
|
|
|
579
666
|
if (!best) {
|
|
580
|
-
throw new Error("No image candidate was generated.");
|
|
667
|
+
throw lastError ?? new Error("No image candidate was generated.");
|
|
581
668
|
}
|
|
582
669
|
|
|
583
670
|
return best;
|
|
@@ -611,7 +698,7 @@ function isImageProvider(value: unknown): value is ImageProvider {
|
|
|
611
698
|
|
|
612
699
|
function sourceImageSizeForProvider(provider: ImageProvider) {
|
|
613
700
|
if (provider === "openai") {
|
|
614
|
-
return "
|
|
701
|
+
return "1024x1536";
|
|
615
702
|
}
|
|
616
703
|
return "1080x1920";
|
|
617
704
|
}
|
|
@@ -623,7 +710,7 @@ function defaultImageModelForProvider(provider: ImageProvider) {
|
|
|
623
710
|
if (provider === "openrouter") {
|
|
624
711
|
return "bytedance-seed/seedream-4.5";
|
|
625
712
|
}
|
|
626
|
-
return "gemini-
|
|
713
|
+
return "gemini-2.5-flash-image";
|
|
627
714
|
}
|
|
628
715
|
|
|
629
716
|
function defaultTextModelForProvider(provider: ImageProvider) {
|
|
@@ -646,15 +733,41 @@ function sourceImageOutputSizeForProvider(
|
|
|
646
733
|
if (provider !== "gemini") {
|
|
647
734
|
return undefined;
|
|
648
735
|
}
|
|
649
|
-
if (
|
|
650
|
-
model === "gemini-3.1-flash-image-preview" ||
|
|
651
|
-
model === "gemini-3-pro-image-preview"
|
|
652
|
-
) {
|
|
736
|
+
if (model === "gemini-3-pro-image-preview") {
|
|
653
737
|
return "1K" as const;
|
|
654
738
|
}
|
|
655
739
|
return undefined;
|
|
656
740
|
}
|
|
657
741
|
|
|
742
|
+
function providerFailoverOrder(primary: ImageProvider): ImageProvider[] {
|
|
743
|
+
return [
|
|
744
|
+
primary,
|
|
745
|
+
...supportedImageProviders.filter((provider) => provider !== primary),
|
|
746
|
+
];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function resolveImageModelForAttempt(
|
|
750
|
+
provider: ImageProvider,
|
|
751
|
+
configuredProvider: ImageProvider,
|
|
752
|
+
configuredImageModel: string | null,
|
|
753
|
+
) {
|
|
754
|
+
if (provider === configuredProvider && configuredImageModel) {
|
|
755
|
+
return configuredImageModel;
|
|
756
|
+
}
|
|
757
|
+
return defaultImageModelForProvider(provider);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function resolveTextModelForAttempt(
|
|
761
|
+
provider: ImageProvider,
|
|
762
|
+
configuredProvider: ImageProvider,
|
|
763
|
+
configuredTextModel: string | null,
|
|
764
|
+
) {
|
|
765
|
+
if (provider === configuredProvider && configuredTextModel) {
|
|
766
|
+
return configuredTextModel;
|
|
767
|
+
}
|
|
768
|
+
return defaultTextModelForProvider(provider);
|
|
769
|
+
}
|
|
770
|
+
|
|
658
771
|
function resolveTextStyleSpec(config: Record<string, unknown>): TextStyleSpec {
|
|
659
772
|
const fontId = isTemplateFontId(config.captionFont)
|
|
660
773
|
? config.captionFont
|
|
@@ -759,6 +872,8 @@ const slideshowManifestSchema = z.object({
|
|
|
759
872
|
slides: z.array(
|
|
760
873
|
z.object({
|
|
761
874
|
index: z.number(),
|
|
875
|
+
sourceType: z.enum(["ai_prompt", "image_source"]),
|
|
876
|
+
sourceValue: z.string(),
|
|
762
877
|
imagePrompt: z.string(),
|
|
763
878
|
imagePromptAttachments: z.array(z.string().url()),
|
|
764
879
|
overlayText: z.string(),
|
|
@@ -779,41 +894,196 @@ const slideshowManifestSchema = z.object({
|
|
|
779
894
|
),
|
|
780
895
|
});
|
|
781
896
|
|
|
782
|
-
function normalizeSlideInput(
|
|
897
|
+
function normalizeSlideInput(
|
|
898
|
+
input: z.infer<typeof slideInputSchema>,
|
|
899
|
+
): NormalizedSlideInput {
|
|
783
900
|
if (Array.isArray(input)) {
|
|
784
|
-
return {
|
|
785
|
-
|
|
901
|
+
return normalizeSlideFields({
|
|
902
|
+
primarySource: input[0],
|
|
786
903
|
imagePromptAttachments: [],
|
|
787
904
|
overlayText: input[1],
|
|
788
905
|
durationMs: input[2] ?? 4000,
|
|
789
|
-
};
|
|
906
|
+
});
|
|
790
907
|
}
|
|
791
908
|
|
|
792
|
-
return {
|
|
793
|
-
|
|
909
|
+
return normalizeSlideFields({
|
|
910
|
+
primarySource: input.image_prompt,
|
|
794
911
|
imagePromptAttachments: input.image_prompt_attachments,
|
|
795
912
|
overlayText: input.caption,
|
|
796
913
|
durationMs: input.duration_ms,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function normalizeSlideFields(input: {
|
|
918
|
+
primarySource: string;
|
|
919
|
+
imagePromptAttachments: string[];
|
|
920
|
+
overlayText: string;
|
|
921
|
+
durationMs: number;
|
|
922
|
+
}): NormalizedSlideInput {
|
|
923
|
+
const sourceValue = input.primarySource.trim();
|
|
924
|
+
const existingImageSource = resolveExistingImageSource(sourceValue);
|
|
925
|
+
return {
|
|
926
|
+
sourceType: existingImageSource ? "image_source" : "ai_prompt",
|
|
927
|
+
sourceValue: existingImageSource ?? sourceValue,
|
|
928
|
+
imagePrompt: sourceValue,
|
|
929
|
+
imagePromptAttachments: input.imagePromptAttachments,
|
|
930
|
+
overlayText: input.overlayText,
|
|
931
|
+
durationMs: input.durationMs,
|
|
797
932
|
};
|
|
798
933
|
}
|
|
799
934
|
|
|
935
|
+
async function loadExistingSlideImage(sourceValue: string) {
|
|
936
|
+
const resolved = resolveExistingImageSource(sourceValue);
|
|
937
|
+
if (!resolved) {
|
|
938
|
+
throw new Error(`Slide source is not a supported image URL or file path: ${sourceValue}`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const bytes =
|
|
942
|
+
isLocalFileSource(resolved)
|
|
943
|
+
? readFileSync(resolveLocalFileSourcePath(resolved))
|
|
944
|
+
: await fetchExternalImageBytes(resolved);
|
|
945
|
+
const contentType = await detectImageContentType(bytes);
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
bytes,
|
|
949
|
+
contentType,
|
|
950
|
+
revisedPrompt: null,
|
|
951
|
+
prompt: sourceValue,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function resolveExistingImageSource(value: string) {
|
|
956
|
+
const trimmed = value.trim();
|
|
957
|
+
if (!trimmed) {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const parsedUrl = parseUrl(trimmed);
|
|
962
|
+
if (
|
|
963
|
+
parsedUrl &&
|
|
964
|
+
["http:", "https:", "file:", "data:"].includes(parsedUrl.protocol)
|
|
965
|
+
) {
|
|
966
|
+
return trimmed;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const localPath = resolveLocalPathIfPresent(trimmed);
|
|
970
|
+
if (localPath) {
|
|
971
|
+
return localPath;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function parseUrl(value: string) {
|
|
978
|
+
try {
|
|
979
|
+
return new URL(value);
|
|
980
|
+
} catch {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function resolveLocalPathIfPresent(value: string) {
|
|
986
|
+
const expandedPath = value.startsWith("~/")
|
|
987
|
+
? path.join(process.env.HOME ?? "", value.slice(2))
|
|
988
|
+
: value;
|
|
989
|
+
const candidate = path.resolve(expandedPath);
|
|
990
|
+
return existsSync(candidate) ? candidate : null;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function isLocalFileSource(sourceValue: string) {
|
|
994
|
+
const parsedUrl = parseUrl(sourceValue);
|
|
995
|
+
return !parsedUrl || parsedUrl.protocol === "file:";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function resolveLocalFileSourcePath(sourceValue: string) {
|
|
999
|
+
const parsedUrl = parseUrl(sourceValue);
|
|
1000
|
+
return parsedUrl?.protocol === "file:"
|
|
1001
|
+
? fileURLToPath(parsedUrl)
|
|
1002
|
+
: path.resolve(sourceValue);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function fetchExternalImageBytes(sourceValue: string) {
|
|
1006
|
+
const response = await fetch(sourceValue);
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
throw new Error(`Could not fetch slide source image: ${response.status} ${response.statusText}`);
|
|
1009
|
+
}
|
|
1010
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function detectImageContentType(bytes: Uint8Array) {
|
|
1014
|
+
try {
|
|
1015
|
+
const metadata = await sharp(bytes).metadata();
|
|
1016
|
+
if (metadata.format) {
|
|
1017
|
+
return contentTypeForSharpFormat(metadata.format);
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`Slide source could not be decoded as an image: ${error instanceof Error ? error.message : String(error)}`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
return "image/png";
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function contentTypeForSharpFormat(format: string) {
|
|
1028
|
+
switch (format) {
|
|
1029
|
+
case "jpeg":
|
|
1030
|
+
return "image/jpeg";
|
|
1031
|
+
case "png":
|
|
1032
|
+
return "image/png";
|
|
1033
|
+
case "webp":
|
|
1034
|
+
return "image/webp";
|
|
1035
|
+
case "gif":
|
|
1036
|
+
return "image/gif";
|
|
1037
|
+
case "avif":
|
|
1038
|
+
return "image/avif";
|
|
1039
|
+
case "tiff":
|
|
1040
|
+
return "image/tiff";
|
|
1041
|
+
case "svg":
|
|
1042
|
+
return "image/svg+xml";
|
|
1043
|
+
case "heif":
|
|
1044
|
+
return "image/heif";
|
|
1045
|
+
default:
|
|
1046
|
+
return `image/${format}`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
800
1050
|
async function generateMetaDetails(
|
|
801
1051
|
ctx: TemplateJobContext,
|
|
802
1052
|
input: {
|
|
803
1053
|
provider: ImageProvider;
|
|
804
1054
|
textModel: string;
|
|
1055
|
+
configuredProvider: ImageProvider;
|
|
1056
|
+
configuredTextModel: string | null;
|
|
805
1057
|
slides: SlideOutput[];
|
|
806
1058
|
metaDetailsPrompt?: string;
|
|
807
1059
|
},
|
|
808
1060
|
): Promise<MetaDetails> {
|
|
809
1061
|
const prompt = buildMetaDetailsPrompt(input.slides, input.metaDetailsPrompt);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1062
|
+
let lastError: Error | null = null;
|
|
1063
|
+
for (const provider of providerFailoverOrder(input.provider)) {
|
|
1064
|
+
const textModel = resolveTextModelForAttempt(
|
|
1065
|
+
provider,
|
|
1066
|
+
input.configuredProvider,
|
|
1067
|
+
input.configuredTextModel,
|
|
1068
|
+
);
|
|
1069
|
+
try {
|
|
1070
|
+
const response = await ctx.providers.generateText({
|
|
1071
|
+
provider,
|
|
1072
|
+
model: textModel,
|
|
1073
|
+
prompt,
|
|
1074
|
+
temperature: 0.7,
|
|
1075
|
+
});
|
|
1076
|
+
return parseMetaDetailsResponse(response.text, input.slides);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1079
|
+
ctx.logger.warn("Metadata provider failed, trying next provider", {
|
|
1080
|
+
provider,
|
|
1081
|
+
model: textModel,
|
|
1082
|
+
message: lastError.message,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
throw lastError ?? new Error("No metadata provider succeeded.");
|
|
817
1087
|
}
|
|
818
1088
|
|
|
819
1089
|
function buildMetaDetailsPrompt(
|
|
@@ -1224,7 +1494,12 @@ async function portraitScore(input: Uint8Array) {
|
|
|
1224
1494
|
thumbnailWidth,
|
|
1225
1495
|
thumbnailHeight,
|
|
1226
1496
|
);
|
|
1227
|
-
|
|
1497
|
+
const edgeFlatnessPenalty = edgeBandFlatnessPenalty(
|
|
1498
|
+
sample,
|
|
1499
|
+
thumbnailWidth,
|
|
1500
|
+
thumbnailHeight,
|
|
1501
|
+
);
|
|
1502
|
+
return Math.abs(aspect - 9 / 16) + edgeContrast + edgeFlatnessPenalty;
|
|
1228
1503
|
}
|
|
1229
1504
|
|
|
1230
1505
|
async function chooseLayoutFromImage(
|
|
@@ -1541,6 +1816,26 @@ function edgeBandContrast(sample: Buffer, width: number, height: number) {
|
|
|
1541
1816
|
return (top + bottom) / 220;
|
|
1542
1817
|
}
|
|
1543
1818
|
|
|
1819
|
+
function edgeBandFlatnessPenalty(sample: Buffer, width: number, height: number) {
|
|
1820
|
+
const bandHeight = Math.max(8, Math.floor(height * 0.16));
|
|
1821
|
+
const centerStart = Math.max(0, Math.floor(height * 0.34));
|
|
1822
|
+
const centerEnd = Math.min(height, Math.ceil(height * 0.66));
|
|
1823
|
+
const top = bandActivity(sample, width, height, 0, bandHeight);
|
|
1824
|
+
const bottom = bandActivity(sample, width, height, height - bandHeight, height);
|
|
1825
|
+
const center = bandActivity(sample, width, height, centerStart, centerEnd);
|
|
1826
|
+
const edgeAverage = (top + bottom) / 2;
|
|
1827
|
+
if (center <= 0) {
|
|
1828
|
+
return 0;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const ratio = edgeAverage / center;
|
|
1832
|
+
if (ratio >= 0.72) {
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
return (0.72 - ratio) * 3.4;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1544
1839
|
function bandActivity(
|
|
1545
1840
|
sample: Buffer,
|
|
1546
1841
|
width: number,
|
|
@@ -2,21 +2,16 @@
|
|
|
2
2
|
"template_id": "4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db",
|
|
3
3
|
"slug_id": "template_0000",
|
|
4
4
|
"project_name": "vidfarm_template_0000",
|
|
5
|
-
"github_repo": "
|
|
6
|
-
"source_branch": "
|
|
5
|
+
"github_repo": "your-org/your-template-repo",
|
|
6
|
+
"source_branch": "main",
|
|
7
7
|
"skill_path": "SKILL.md",
|
|
8
|
-
"template_module_path": "src/template.
|
|
9
|
-
"release_controls": {
|
|
10
|
-
"shared_remotion_publish": "admin_only",
|
|
11
|
-
"platform_activation": "admin_only",
|
|
12
|
-
"production_docker_promotion": "admin_only"
|
|
13
|
-
},
|
|
8
|
+
"template_module_path": "src/template.ts",
|
|
14
9
|
"remotion": {
|
|
15
|
-
"region": "
|
|
16
|
-
"function_name": "
|
|
17
|
-
"bucket_name": "
|
|
10
|
+
"region": "local",
|
|
11
|
+
"function_name": "",
|
|
12
|
+
"bucket_name": "",
|
|
18
13
|
"site_name": "vidfarm-template-0000",
|
|
19
|
-
"serve_url": "
|
|
14
|
+
"serve_url": "",
|
|
20
15
|
"composition_id": "template-0000",
|
|
21
16
|
"entry_point": "src/remotion/index.tsx",
|
|
22
17
|
"props_file": "composition.json",
|