@mevdragon/vidfarm-devcli 0.18.0 → 0.19.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/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -110,6 +110,58 @@ const removeBackgroundImagePayloadSchema = z.preprocess((raw) => {
|
|
|
110
110
|
source_image_url: z.string().url(),
|
|
111
111
|
output_format: z.enum(["png", "jpeg", "webp"]).default("webp")
|
|
112
112
|
}));
|
|
113
|
+
// Transparent-capable output only (jpeg has no alpha) for the chroma-key routes.
|
|
114
|
+
const alphaOutputFormatSchema = z.enum(["png", "webp"]).default("png");
|
|
115
|
+
const greenscreenRemoveBackgroundPayloadSchema = z.preprocess((raw) => {
|
|
116
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
117
|
+
return raw;
|
|
118
|
+
const p = raw;
|
|
119
|
+
return {
|
|
120
|
+
...p,
|
|
121
|
+
source_image_url: p.source_image_url ?? p.image_url ?? p.imageUrl ?? p.url,
|
|
122
|
+
key_color: p.key_color ?? p.keyColor ?? p.background_color ?? p.backgroundColor ?? p.color,
|
|
123
|
+
tolerance: p.tolerance ?? p.threshold,
|
|
124
|
+
softness: p.softness ?? p.feather,
|
|
125
|
+
despill: p.despill ?? p.spill_suppression ?? p.spillSuppression
|
|
126
|
+
};
|
|
127
|
+
}, z.object({
|
|
128
|
+
source_image_url: z.string().url(),
|
|
129
|
+
// The background color to key out. Green by default; pass white/#FFFFFF,
|
|
130
|
+
// blue, or any hex/rgb/named color to key a different flat background.
|
|
131
|
+
key_color: z.string().min(1).max(64).default("#00FF00"),
|
|
132
|
+
// Base similarity radius (0..1). Larger = more aggressive removal.
|
|
133
|
+
tolerance: z.coerce.number().min(0).max(1).default(0.3),
|
|
134
|
+
// Feather band beyond `tolerance` for soft anti-aliased edges (0..1).
|
|
135
|
+
softness: z.coerce.number().min(0).max(1).default(0.1),
|
|
136
|
+
// Suppress residual key-color fringe on the subject's edges.
|
|
137
|
+
despill: z.boolean().default(true),
|
|
138
|
+
output_format: alphaOutputFormatSchema
|
|
139
|
+
}));
|
|
140
|
+
const mediaOverlayPayloadSchema = z.preprocess((raw) => {
|
|
141
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
142
|
+
return raw;
|
|
143
|
+
const p = raw;
|
|
144
|
+
return {
|
|
145
|
+
...p,
|
|
146
|
+
key_color: p.key_color ?? p.keyColor ?? p.background_color ?? p.backgroundColor,
|
|
147
|
+
tolerance: p.tolerance ?? p.threshold,
|
|
148
|
+
softness: p.softness ?? p.feather,
|
|
149
|
+
despill: p.despill ?? p.spill_suppression ?? p.spillSuppression,
|
|
150
|
+
prompt_attachments: p.prompt_attachments ?? p.promptAttachments ?? p.reference_attachments
|
|
151
|
+
};
|
|
152
|
+
}, z.object({
|
|
153
|
+
provider: imageProviderSchema.optional(),
|
|
154
|
+
model: z.string().min(1).optional(),
|
|
155
|
+
prompt: z.string().min(3).max(8000),
|
|
156
|
+
prompt_attachments: z.array(z.string().url()).max(16).default([]),
|
|
157
|
+
aspect_ratio: aspectRatioSchema,
|
|
158
|
+
image_size: imageSizeSchema,
|
|
159
|
+
key_color: z.string().min(1).max(64).default("#00FF00"),
|
|
160
|
+
tolerance: z.coerce.number().min(0).max(1).default(0.3),
|
|
161
|
+
softness: z.coerce.number().min(0).max(1).default(0.1),
|
|
162
|
+
despill: z.boolean().default(true),
|
|
163
|
+
output_format: alphaOutputFormatSchema
|
|
164
|
+
}));
|
|
113
165
|
const renderHtmlImagePayloadSchema = z.object({
|
|
114
166
|
width: z.number().int().min(16).max(4096).default(1080),
|
|
115
167
|
height: z.number().int().min(16).max(4096).default(1920),
|
|
@@ -634,6 +686,8 @@ const PRIMITIVE_IMAGE_EDIT_ID = "primitive:image_edit";
|
|
|
634
686
|
const PRIMITIVE_IMAGE_INPAINT_ID = "primitive:image_inpaint";
|
|
635
687
|
const PRIMITIVE_IMAGE_RENDER_HTML_ID = "primitive:image_render_html";
|
|
636
688
|
const PRIMITIVE_IMAGE_REMOVE_BACKGROUND_ID = "primitive:image_remove_background";
|
|
689
|
+
const PRIMITIVE_IMAGE_REMOVE_BACKGROUND_GREENSCREEN_ID = "primitive:image_remove_background_greenscreen";
|
|
690
|
+
const PRIMITIVE_MEDIA_OVERLAY_ID = "primitive:media_overlay";
|
|
637
691
|
const PRIMITIVE_VIDEO_GENERATE_ID = "primitive:video_generate";
|
|
638
692
|
const PRIMITIVE_VIDEO_RENDER_SLIDES_ID = "primitive:video_render_slides";
|
|
639
693
|
const PRIMITIVE_VIDEO_NORMALIZE_ID = "primitive:video_normalize";
|
|
@@ -963,6 +1017,172 @@ const imageRemoveBackgroundPrimitive = definePrimitive({
|
|
|
963
1017
|
}
|
|
964
1018
|
}
|
|
965
1019
|
});
|
|
1020
|
+
const imageRemoveBackgroundGreenscreenPrimitive = definePrimitive({
|
|
1021
|
+
id: PRIMITIVE_IMAGE_REMOVE_BACKGROUND_GREENSCREEN_ID,
|
|
1022
|
+
kind: "image_remove_background_greenscreen",
|
|
1023
|
+
operations: {
|
|
1024
|
+
run: {
|
|
1025
|
+
description: "Cheap local chroma-key background removal. Drops a flat, solid key-color background (green screen by default; pass key_color for white/blue/any flat fill) to a transparent PNG/WebP cut-out using sharp — no third-party API, compute only. Ideal for images you generated on a controlled solid background; for arbitrary photos with a messy background use image_remove_background (RapidAPI matting) instead.",
|
|
1026
|
+
inputSchema: greenscreenRemoveBackgroundPayloadSchema,
|
|
1027
|
+
workflow: "run"
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
jobs: {
|
|
1031
|
+
async run(ctx, input) {
|
|
1032
|
+
const payload = greenscreenRemoveBackgroundPayloadSchema.parse(input);
|
|
1033
|
+
const sourceUrl = payload.source_image_url;
|
|
1034
|
+
ctx.logger.progress(0.2, "Downloading source image for chroma key", { sourceUrl });
|
|
1035
|
+
const source = await fetchRemoteBinary(sourceUrl, "image/png");
|
|
1036
|
+
ctx.logger.progress(0.5, "Keying out background", { keyColor: payload.key_color });
|
|
1037
|
+
const keyed = await chromaKeyRemoveBackground(source.bytes, {
|
|
1038
|
+
keyColor: payload.key_color,
|
|
1039
|
+
tolerance: payload.tolerance,
|
|
1040
|
+
softness: payload.softness,
|
|
1041
|
+
despill: payload.despill,
|
|
1042
|
+
outputFormat: payload.output_format
|
|
1043
|
+
});
|
|
1044
|
+
const stored = await storePrimitiveImage(ctx, {
|
|
1045
|
+
key: `output.${keyed.contentType === "image/webp" ? "webp" : "png"}`,
|
|
1046
|
+
bytes: keyed.bytes,
|
|
1047
|
+
contentType: keyed.contentType,
|
|
1048
|
+
metadata: {
|
|
1049
|
+
primitive: "image_remove_background_greenscreen",
|
|
1050
|
+
source_image_url: sourceUrl,
|
|
1051
|
+
key_color: payload.key_color,
|
|
1052
|
+
tolerance: payload.tolerance,
|
|
1053
|
+
softness: payload.softness,
|
|
1054
|
+
despill: payload.despill,
|
|
1055
|
+
keyed_fraction: keyed.keyedFraction
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
await ctx.billing.record({
|
|
1059
|
+
type: "cpu_estimate",
|
|
1060
|
+
costUsd: config.GREENSCREEN_CHROMA_KEY_COST_USD,
|
|
1061
|
+
costCenterSlug: "greenscreen_chroma_key",
|
|
1062
|
+
occurredAtMs: Date.now(),
|
|
1063
|
+
metadata: {
|
|
1064
|
+
operation: "remove-background-greenscreen",
|
|
1065
|
+
source_image_url: sourceUrl,
|
|
1066
|
+
key_color: payload.key_color,
|
|
1067
|
+
keyed_fraction: keyed.keyedFraction,
|
|
1068
|
+
output_key: stored.key
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
ctx.logger.progress(1, "Chroma-key background removal complete");
|
|
1072
|
+
return {
|
|
1073
|
+
progress: 1,
|
|
1074
|
+
output: {
|
|
1075
|
+
...buildImagePrimitiveOutput({
|
|
1076
|
+
fileUrl: stored.url,
|
|
1077
|
+
contentType: keyed.contentType,
|
|
1078
|
+
derivedFromUrl: sourceUrl,
|
|
1079
|
+
files: compactUrls([stored.url])
|
|
1080
|
+
}),
|
|
1081
|
+
key_color: payload.key_color,
|
|
1082
|
+
keyed_fraction: keyed.keyedFraction
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
const mediaOverlayPrimitive = definePrimitive({
|
|
1089
|
+
id: PRIMITIVE_MEDIA_OVERLAY_ID,
|
|
1090
|
+
kind: "media_overlay",
|
|
1091
|
+
operations: {
|
|
1092
|
+
run: {
|
|
1093
|
+
description: "Create a transparent-background media overlay in ONE job: generate an AI image of the subject on a forced flat key-color background (using the caller's own BYOK image keys), then chroma-key that background out to a ready-to-composite transparent PNG/WebP. This is the go-to way to mint 'Vox-style' floating illustrations, props, icons, or cut-out characters to animate over a composition — prefer it over generate-then-manually-remove.",
|
|
1094
|
+
inputSchema: mediaOverlayPayloadSchema,
|
|
1095
|
+
workflow: "run",
|
|
1096
|
+
providerHint: "openai"
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
jobs: {
|
|
1100
|
+
async run(ctx, input) {
|
|
1101
|
+
const payload = mediaOverlayPayloadSchema.parse(input);
|
|
1102
|
+
const provider = payload.provider ?? "openai";
|
|
1103
|
+
const model = payload.model ?? defaultImageModelForProvider(provider);
|
|
1104
|
+
const keyRgb = parseKeyColor(payload.key_color);
|
|
1105
|
+
const keyLabel = describeKeyColor(payload.key_color, keyRgb);
|
|
1106
|
+
const augmentedPrompt = buildOverlayGenerationPrompt(payload.prompt, payload.key_color, keyLabel);
|
|
1107
|
+
ctx.logger.progress(0.12, "Generating overlay subject on flat key-color background", { provider, model, keyColor: payload.key_color });
|
|
1108
|
+
const generated = await ctx.providers.generateImage({
|
|
1109
|
+
provider,
|
|
1110
|
+
model,
|
|
1111
|
+
prompt: augmentedPrompt,
|
|
1112
|
+
promptAttachments: payload.prompt_attachments,
|
|
1113
|
+
aspectRatio: payload.aspect_ratio,
|
|
1114
|
+
imageSize: payload.image_size
|
|
1115
|
+
});
|
|
1116
|
+
// Keep the raw key-color frame as an artifact for debugging / re-keying at
|
|
1117
|
+
// different tolerances without paying for a second generation.
|
|
1118
|
+
const normalizedSource = await normalizePrimitiveImageOutput(generated.bytes, "png");
|
|
1119
|
+
const sourceStored = await storePrimitiveImage(ctx, {
|
|
1120
|
+
key: "source-greenscreen.png",
|
|
1121
|
+
bytes: normalizedSource.bytes,
|
|
1122
|
+
contentType: normalizedSource.contentType,
|
|
1123
|
+
metadata: {
|
|
1124
|
+
primitive: "media_overlay",
|
|
1125
|
+
stage: "greenscreen_source",
|
|
1126
|
+
prompt: payload.prompt,
|
|
1127
|
+
key_color: payload.key_color,
|
|
1128
|
+
revised_prompt: generated.revisedPrompt
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
ctx.logger.progress(0.62, "Keying out background", { keyColor: payload.key_color });
|
|
1132
|
+
const keyed = await chromaKeyRemoveBackground(generated.bytes, {
|
|
1133
|
+
keyColor: payload.key_color,
|
|
1134
|
+
tolerance: payload.tolerance,
|
|
1135
|
+
softness: payload.softness,
|
|
1136
|
+
despill: payload.despill,
|
|
1137
|
+
outputFormat: payload.output_format
|
|
1138
|
+
});
|
|
1139
|
+
const stored = await storePrimitiveImage(ctx, {
|
|
1140
|
+
key: `output.${keyed.contentType === "image/webp" ? "webp" : "png"}`,
|
|
1141
|
+
bytes: keyed.bytes,
|
|
1142
|
+
contentType: keyed.contentType,
|
|
1143
|
+
metadata: {
|
|
1144
|
+
primitive: "media_overlay",
|
|
1145
|
+
stage: "transparent_overlay",
|
|
1146
|
+
prompt: payload.prompt,
|
|
1147
|
+
provider,
|
|
1148
|
+
model,
|
|
1149
|
+
key_color: payload.key_color,
|
|
1150
|
+
keyed_fraction: keyed.keyedFraction,
|
|
1151
|
+
revised_prompt: generated.revisedPrompt
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
await ctx.billing.record({
|
|
1155
|
+
type: "cpu_estimate",
|
|
1156
|
+
costUsd: config.MEDIA_OVERLAY_COST_USD,
|
|
1157
|
+
costCenterSlug: "media_overlay",
|
|
1158
|
+
occurredAtMs: Date.now(),
|
|
1159
|
+
metadata: {
|
|
1160
|
+
operation: "create-media-overlay",
|
|
1161
|
+
prompt: payload.prompt,
|
|
1162
|
+
key_color: payload.key_color,
|
|
1163
|
+
keyed_fraction: keyed.keyedFraction,
|
|
1164
|
+
output_key: stored.key,
|
|
1165
|
+
greenscreen_source_key: sourceStored.key
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
ctx.logger.progress(1, "Media overlay ready");
|
|
1169
|
+
return {
|
|
1170
|
+
progress: 1,
|
|
1171
|
+
output: {
|
|
1172
|
+
...buildImagePrimitiveOutput({
|
|
1173
|
+
fileUrl: stored.url,
|
|
1174
|
+
contentType: keyed.contentType,
|
|
1175
|
+
revisedPrompt: generated.revisedPrompt,
|
|
1176
|
+
files: compactUrls([stored.url, sourceStored.url])
|
|
1177
|
+
}),
|
|
1178
|
+
greenscreen_source_url: sourceStored.url,
|
|
1179
|
+
key_color: payload.key_color,
|
|
1180
|
+
keyed_fraction: keyed.keyedFraction
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
966
1186
|
const videoGeneratePrimitive = definePrimitive({
|
|
967
1187
|
id: PRIMITIVE_VIDEO_GENERATE_ID,
|
|
968
1188
|
kind: "video_generate",
|
|
@@ -1285,13 +1505,58 @@ const videoDownloadPrimitive = definePrimitive({
|
|
|
1285
1505
|
upstream_source: lookup.source ?? null
|
|
1286
1506
|
}
|
|
1287
1507
|
});
|
|
1508
|
+
// Re-host a DURABLE poster so the frontend never depends on the ephemeral
|
|
1509
|
+
// third-party `lookup.thumbnail` URL. Prefer extracting a frame from the
|
|
1510
|
+
// MP4 we just stored (works even when the provider gives no thumbnail);
|
|
1511
|
+
// fall back to re-hosting the provider thumbnail bytes. Best-effort.
|
|
1512
|
+
let durableThumbnailUrl = null;
|
|
1513
|
+
ctx.logger.progress(0.7, "Extracting thumbnail frame");
|
|
1514
|
+
try {
|
|
1515
|
+
const frame = await executePrimitiveMediaOperation(ctx, {
|
|
1516
|
+
operation: "video_extract_frame",
|
|
1517
|
+
payload: { source_video_url: storedVideo.url, at_ms: 1000, output_format: "jpeg" },
|
|
1518
|
+
outputKey: "thumbnail.jpg"
|
|
1519
|
+
});
|
|
1520
|
+
durableThumbnailUrl = frame.publicUrl;
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
ctx.logger.progress(0.75, "Thumbnail frame extraction skipped", {
|
|
1524
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
if (!durableThumbnailUrl && lookup.thumbnail) {
|
|
1528
|
+
try {
|
|
1529
|
+
const remoteThumb = await fetchRemoteBinary(lookup.thumbnail, "image/jpeg");
|
|
1530
|
+
const thumbContentType = remoteThumb.contentType.startsWith("image/") ? remoteThumb.contentType : "image/jpeg";
|
|
1531
|
+
const storedThumb = await storePrimitiveImage(ctx, {
|
|
1532
|
+
key: "thumbnail.jpg",
|
|
1533
|
+
bytes: remoteThumb.bytes,
|
|
1534
|
+
contentType: thumbContentType,
|
|
1535
|
+
metadata: {
|
|
1536
|
+
primitive: "video_download",
|
|
1537
|
+
operation: "download-video",
|
|
1538
|
+
source_url: sourceUrl,
|
|
1539
|
+
rehosted_from: lookup.thumbnail
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
durableThumbnailUrl = storedThumb.url;
|
|
1543
|
+
}
|
|
1544
|
+
catch (error) {
|
|
1545
|
+
ctx.logger.progress(0.78, "Thumbnail re-host skipped", {
|
|
1546
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1288
1550
|
const manifest = {
|
|
1289
1551
|
primitive: "video_download",
|
|
1290
1552
|
operation: "download-video",
|
|
1291
1553
|
sourceUrl,
|
|
1292
1554
|
sourceHost,
|
|
1293
1555
|
title: lookup.title ?? null,
|
|
1294
|
-
|
|
1556
|
+
// Raw ephemeral third-party thumbnail (kept for provenance) plus the
|
|
1557
|
+
// durable re-hosted poster the finalizer/frontend should actually use.
|
|
1558
|
+
thumbnail: durableThumbnailUrl ?? lookup.thumbnail ?? null,
|
|
1559
|
+
rawThumbnail: lookup.thumbnail ?? null,
|
|
1295
1560
|
duration: lookup.duration ?? null,
|
|
1296
1561
|
source: lookup.source ?? null,
|
|
1297
1562
|
selectedMedia: {
|
|
@@ -1336,19 +1601,22 @@ const videoDownloadPrimitive = definePrimitive({
|
|
|
1336
1601
|
});
|
|
1337
1602
|
ctx.logger.progress(1, "Video download complete", {
|
|
1338
1603
|
videoUrl: storedVideo.url,
|
|
1339
|
-
sourceHost
|
|
1604
|
+
sourceHost,
|
|
1605
|
+
thumbnailUrl: durableThumbnailUrl
|
|
1340
1606
|
});
|
|
1341
1607
|
return {
|
|
1342
1608
|
progress: 1,
|
|
1343
1609
|
output: {
|
|
1344
|
-
files: compactUrls([storedVideo.url, manifestArtifact?.url]),
|
|
1610
|
+
files: compactUrls([storedVideo.url, durableThumbnailUrl, manifestArtifact?.url]),
|
|
1345
1611
|
sourceUrl,
|
|
1346
1612
|
sourceHost,
|
|
1347
1613
|
videoUrl: storedVideo.url,
|
|
1348
1614
|
sourceVideoUrl: storedVideo.url,
|
|
1349
1615
|
primary_file_url: storedVideo.url,
|
|
1350
1616
|
title: lookup.title ?? null,
|
|
1351
|
-
|
|
1617
|
+
// Durable re-hosted poster (falls back to the raw provider thumbnail).
|
|
1618
|
+
thumbnailUrl: durableThumbnailUrl ?? lookup.thumbnail ?? null,
|
|
1619
|
+
thumbnail: durableThumbnailUrl ?? lookup.thumbnail ?? null,
|
|
1352
1620
|
duration: lookup.duration ?? null,
|
|
1353
1621
|
selectedMediaUrl: selectedMedia.url,
|
|
1354
1622
|
manifest: manifestArtifact
|
|
@@ -2824,6 +3092,8 @@ class PrimitiveRegistry {
|
|
|
2824
3092
|
[imageInpaintPrimitive.id, imageInpaintPrimitive],
|
|
2825
3093
|
[imageRenderHtmlPrimitive.id, imageRenderHtmlPrimitive],
|
|
2826
3094
|
[imageRemoveBackgroundPrimitive.id, imageRemoveBackgroundPrimitive],
|
|
3095
|
+
[imageRemoveBackgroundGreenscreenPrimitive.id, imageRemoveBackgroundGreenscreenPrimitive],
|
|
3096
|
+
[mediaOverlayPrimitive.id, mediaOverlayPrimitive],
|
|
2827
3097
|
[videoGeneratePrimitive.id, videoGeneratePrimitive],
|
|
2828
3098
|
[videoRenderSlidesPrimitive.id, videoRenderSlidesPrimitive],
|
|
2829
3099
|
[mediaDedupePrimitive.id, mediaDedupePrimitive],
|
|
@@ -3017,6 +3287,141 @@ async function normalizePrimitiveImageOutput(bytes, format) {
|
|
|
3017
3287
|
contentType: "image/png"
|
|
3018
3288
|
};
|
|
3019
3289
|
}
|
|
3290
|
+
function clampByte(value) {
|
|
3291
|
+
const n = Math.round(Number.isFinite(value) ? value : 0);
|
|
3292
|
+
return Math.max(0, Math.min(255, n));
|
|
3293
|
+
}
|
|
3294
|
+
const NAMED_KEY_COLORS = {
|
|
3295
|
+
green: [0, 255, 0],
|
|
3296
|
+
lime: [0, 255, 0],
|
|
3297
|
+
"chroma-green": [0, 177, 64],
|
|
3298
|
+
"chromakey-green": [0, 177, 64],
|
|
3299
|
+
white: [255, 255, 255],
|
|
3300
|
+
black: [0, 0, 0],
|
|
3301
|
+
blue: [0, 0, 255],
|
|
3302
|
+
"chroma-blue": [0, 71, 187],
|
|
3303
|
+
"chromakey-blue": [0, 71, 187],
|
|
3304
|
+
red: [255, 0, 0],
|
|
3305
|
+
magenta: [255, 0, 255],
|
|
3306
|
+
cyan: [0, 255, 255],
|
|
3307
|
+
gray: [128, 128, 128],
|
|
3308
|
+
grey: [128, 128, 128]
|
|
3309
|
+
};
|
|
3310
|
+
// Parse a key color from hex (#rgb / #rrggbb), rgb()/rgba() triples, or a small
|
|
3311
|
+
// set of named colors. Throws on anything unrecognized so a typo surfaces
|
|
3312
|
+
// instead of silently keying out the wrong color.
|
|
3313
|
+
function parseKeyColor(value) {
|
|
3314
|
+
const raw = value.trim().toLowerCase();
|
|
3315
|
+
const named = NAMED_KEY_COLORS[raw];
|
|
3316
|
+
if (named) {
|
|
3317
|
+
return { r: named[0], g: named[1], b: named[2] };
|
|
3318
|
+
}
|
|
3319
|
+
const rgbMatch = raw.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
3320
|
+
if (rgbMatch) {
|
|
3321
|
+
return { r: clampByte(Number(rgbMatch[1])), g: clampByte(Number(rgbMatch[2])), b: clampByte(Number(rgbMatch[3])) };
|
|
3322
|
+
}
|
|
3323
|
+
let hex = raw.startsWith("#") ? raw.slice(1) : raw;
|
|
3324
|
+
if (/^[0-9a-f]{3}$/.test(hex)) {
|
|
3325
|
+
hex = hex.split("").map((ch) => ch + ch).join("");
|
|
3326
|
+
}
|
|
3327
|
+
if (/^[0-9a-f]{6}$/.test(hex)) {
|
|
3328
|
+
return {
|
|
3329
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
3330
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
3331
|
+
b: parseInt(hex.slice(4, 6), 16)
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
throw new Error(`Unrecognized key_color "${value}". Use a hex value (e.g. #00FF00), an rgb(...) triple, or a named color (green/white/blue/black).`);
|
|
3335
|
+
}
|
|
3336
|
+
// A plain-English label for the key color so the image-generation prompt can
|
|
3337
|
+
// tell the model exactly which flat background to paint.
|
|
3338
|
+
function describeKeyColor(raw, rgb) {
|
|
3339
|
+
const named = raw.trim().toLowerCase();
|
|
3340
|
+
if (/green|lime/.test(named))
|
|
3341
|
+
return "chroma green";
|
|
3342
|
+
if (/blue/.test(named))
|
|
3343
|
+
return "chroma blue";
|
|
3344
|
+
if (named === "white" || (rgb.r > 240 && rgb.g > 240 && rgb.b > 240))
|
|
3345
|
+
return "solid white";
|
|
3346
|
+
if (named === "black" || (rgb.r < 15 && rgb.g < 15 && rgb.b < 15))
|
|
3347
|
+
return "solid black";
|
|
3348
|
+
if (rgb.g > rgb.r && rgb.g > rgb.b)
|
|
3349
|
+
return "chroma green";
|
|
3350
|
+
if (rgb.b > rgb.r && rgb.b > rgb.g)
|
|
3351
|
+
return "chroma blue";
|
|
3352
|
+
return "flat solid";
|
|
3353
|
+
}
|
|
3354
|
+
function buildOverlayGenerationPrompt(subject, keyHex, keyLabel) {
|
|
3355
|
+
return [
|
|
3356
|
+
subject.trim(),
|
|
3357
|
+
"",
|
|
3358
|
+
`BACKGROUND REQUIREMENT (critical for clean keying): render ONLY the subject, fully in frame, on a completely flat, uniform, evenly-lit ${keyLabel} background (target color ${keyHex}). The background must be one single solid ${keyLabel} fill — absolutely NO gradient, NO vignette, NO cast shadows or reflections on the background, and NO ${keyLabel} tones anywhere on the subject itself (avoid ${keyLabel} clothing, props, or rim light). Do not add any border, frame, text, or watermark. Give the subject clean, crisp edges against the ${keyLabel} so it can be cut out to a transparent PNG.`
|
|
3359
|
+
].join("\n");
|
|
3360
|
+
}
|
|
3361
|
+
// Local chroma-key background removal. Distance-thresholds every pixel against
|
|
3362
|
+
// the key color: within `tolerance` becomes fully transparent, a `softness`
|
|
3363
|
+
// feather band anti-aliases the edge, and (for chromatic keys) a classic
|
|
3364
|
+
// despill clamps the key's dominant channel so no colored fringe survives.
|
|
3365
|
+
async function chromaKeyRemoveBackground(source, options) {
|
|
3366
|
+
const key = parseKeyColor(options.keyColor);
|
|
3367
|
+
const buffer = Buffer.isBuffer(source) ? source : Buffer.from(source);
|
|
3368
|
+
const { data, info } = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3369
|
+
const channels = info.channels;
|
|
3370
|
+
const maxDist = Math.sqrt(3) * 255;
|
|
3371
|
+
const inner = Math.max(0, options.tolerance) * maxDist;
|
|
3372
|
+
const outer = Math.max(inner + 1e-6, (options.tolerance + Math.max(0, options.softness)) * maxDist);
|
|
3373
|
+
const keyChannels = [key.r, key.g, key.b];
|
|
3374
|
+
const keyChroma = Math.max(...keyChannels) - Math.min(...keyChannels);
|
|
3375
|
+
// Only despill chromatic keys (green/blue/etc.); white/gray/black have no hue
|
|
3376
|
+
// fringe to suppress and clamping them would tint the subject.
|
|
3377
|
+
const doDespill = options.despill && keyChroma >= 40;
|
|
3378
|
+
const dominant = keyChannels.indexOf(Math.max(...keyChannels));
|
|
3379
|
+
let keyedPixels = 0;
|
|
3380
|
+
const totalPixels = info.width * info.height;
|
|
3381
|
+
for (let i = 0; i < data.length; i += channels) {
|
|
3382
|
+
const r = data[i];
|
|
3383
|
+
const g = data[i + 1];
|
|
3384
|
+
const b = data[i + 2];
|
|
3385
|
+
const baseAlpha = channels === 4 ? data[i + 3] : 255;
|
|
3386
|
+
const dr = r - key.r;
|
|
3387
|
+
const dg = g - key.g;
|
|
3388
|
+
const db = b - key.b;
|
|
3389
|
+
const dist = Math.sqrt(dr * dr + dg * dg + db * db);
|
|
3390
|
+
let alpha;
|
|
3391
|
+
if (dist <= inner) {
|
|
3392
|
+
alpha = 0;
|
|
3393
|
+
keyedPixels++;
|
|
3394
|
+
}
|
|
3395
|
+
else if (dist >= outer) {
|
|
3396
|
+
alpha = baseAlpha;
|
|
3397
|
+
}
|
|
3398
|
+
else {
|
|
3399
|
+
const t = (dist - inner) / (outer - inner);
|
|
3400
|
+
alpha = Math.round(baseAlpha * t);
|
|
3401
|
+
}
|
|
3402
|
+
if (doDespill && alpha > 0) {
|
|
3403
|
+
if (dominant === 1)
|
|
3404
|
+
data[i + 1] = Math.min(g, Math.max(r, b));
|
|
3405
|
+
else if (dominant === 0)
|
|
3406
|
+
data[i] = Math.min(r, Math.max(g, b));
|
|
3407
|
+
else
|
|
3408
|
+
data[i + 2] = Math.min(b, Math.max(r, g));
|
|
3409
|
+
}
|
|
3410
|
+
if (channels === 4)
|
|
3411
|
+
data[i + 3] = alpha;
|
|
3412
|
+
}
|
|
3413
|
+
const encoded = sharp(data, { raw: { width: info.width, height: info.height, channels } });
|
|
3414
|
+
const bytes = options.outputFormat === "webp"
|
|
3415
|
+
? await encoded.webp({ quality: 92, alphaQuality: 100 }).toBuffer()
|
|
3416
|
+
: await encoded.png().toBuffer();
|
|
3417
|
+
return {
|
|
3418
|
+
bytes,
|
|
3419
|
+
contentType: options.outputFormat === "webp" ? "image/webp" : "image/png",
|
|
3420
|
+
width: info.width,
|
|
3421
|
+
height: info.height,
|
|
3422
|
+
keyedFraction: totalPixels > 0 ? keyedPixels / totalPixels : 0
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3020
3425
|
async function recordPrimitiveProviderBilling(ctx, generated, kind) {
|
|
3021
3426
|
if (!Number.isFinite(generated.usageCostUsd) || generated.usageCostUsd <= 0) {
|
|
3022
3427
|
return;
|
|
@@ -74,7 +74,7 @@ export function renderReskinAgency(input = { userId: null, currentAccountId: nul
|
|
|
74
74
|
.rk-agency-head{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:20px}
|
|
75
75
|
.rk-agency-head-copy{display:grid;gap:4px;min-width:0}
|
|
76
76
|
.rk-agency-head-cta{display:flex;align-items:center;gap:12px;flex:none}
|
|
77
|
-
.rk-agency-hint{font-size:13px;color:var(--rk-text-muted);white-space:
|
|
77
|
+
.rk-agency-hint{font-size:13px;color:var(--rk-text-muted);white-space:normal;overflow-wrap:anywhere}
|
|
78
78
|
@media(max-width:760px){.rk-agency-head{grid-template-columns:1fr}.rk-agency-head-cta{flex-wrap:wrap}}
|
|
79
79
|
|
|
80
80
|
.rk-agency-panel{background:var(--rk-bg-section);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);padding:24px}
|
|
@@ -161,7 +161,7 @@ export function renderReskinCalendar(input = DEFAULT_INPUT) {
|
|
|
161
161
|
|
|
162
162
|
/* two-column shell: channel rail left, compact month board right */
|
|
163
163
|
.rk-calendar-shell{display:grid;grid-template-columns:280px minmax(0,1fr);gap:16px;align-items:start}
|
|
164
|
-
@media(max-width:
|
|
164
|
+
@media(max-width:1000px){.rk-calendar-shell{grid-template-columns:1fr}}
|
|
165
165
|
|
|
166
166
|
/* ── channel rail ── */
|
|
167
167
|
.rk-calendar-rail{display:grid;gap:10px;background:#fff;border:1px solid var(--rk-border);
|
|
@@ -210,6 +210,7 @@ export function renderReskinCalendar(input = DEFAULT_INPUT) {
|
|
|
210
210
|
.rk-calendar-monthnav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
211
211
|
.rk-calendar-month{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);letter-spacing:-.02em;color:var(--rk-ink);min-width:7.4rem;text-align:center}
|
|
212
212
|
.rk-calendar-navbtn{width:32px;height:32px;padding:0;font-size:14px;line-height:1}
|
|
213
|
+
@media(hover:none){.rk-calendar-navbtn{width:40px;height:40px}}
|
|
213
214
|
.rk-calendar-today-btn{margin-left:2px;padding:5px 11px;font-size:12.5px}
|
|
214
215
|
.rk-calendar-sync{font-size:12px;font-weight:600;color:var(--rk-text-muted);margin-left:4px}
|
|
215
216
|
.rk-calendar-sync.is-error{color:var(--rk-red)}
|