@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.
Files changed (68) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +43 -4
  4. package/SKILL.director.md +190 -18
  5. package/SKILL.platform.md +8 -4
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/demo/dist/favicon.ico +0 -0
  9. package/dist/src/app.js +3031 -300
  10. package/dist/src/cli.js +1549 -69
  11. package/dist/src/config.js +7 -0
  12. package/dist/src/devcli/clip-store.js +29 -2
  13. package/dist/src/devcli/clips.js +55 -9
  14. package/dist/src/devcli/composition-edit.js +658 -8
  15. package/dist/src/devcli/local-backend.js +123 -0
  16. package/dist/src/devcli/migrate-local.js +140 -0
  17. package/dist/src/devcli/sync.js +311 -0
  18. package/dist/src/devcli/timeline-edit.js +490 -0
  19. package/dist/src/editor-chat.js +56 -17
  20. package/dist/src/frontend/discover-client.js +130 -0
  21. package/dist/src/frontend/discover-store.js +23 -0
  22. package/dist/src/frontend/file-directory.js +995 -0
  23. package/dist/src/frontend/homepage-view.js +3 -3
  24. package/dist/src/frontend/template-editor-chat.js +28 -22
  25. package/dist/src/landing-page.js +24 -7
  26. package/dist/src/page-shell.js +26 -2
  27. package/dist/src/primitive-registry.js +409 -4
  28. package/dist/src/reskin/agency-page.js +1 -1
  29. package/dist/src/reskin/calendar-page.js +2 -1
  30. package/dist/src/reskin/chat-page.js +420 -85
  31. package/dist/src/reskin/discover-page.js +731 -39
  32. package/dist/src/reskin/document.js +1311 -387
  33. package/dist/src/reskin/help-page.js +1 -0
  34. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  35. package/dist/src/reskin/inpaint-page.js +2459 -446
  36. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  37. package/dist/src/reskin/library-page.js +1168 -228
  38. package/dist/src/reskin/login-page.js +6 -6
  39. package/dist/src/reskin/pricing-page.js +2 -0
  40. package/dist/src/reskin/settings-page.js +55 -10
  41. package/dist/src/reskin/theme.js +365 -20
  42. package/dist/src/services/billing.js +4 -0
  43. package/dist/src/services/clip-curation/gemini.js +5 -0
  44. package/dist/src/services/clip-curation/hunt.js +81 -1
  45. package/dist/src/services/clip-curation/index.js +2 -1
  46. package/dist/src/services/clip-curation/local-agent.js +4 -3
  47. package/dist/src/services/clip-curation/media-select.js +85 -0
  48. package/dist/src/services/clip-curation/query.js +5 -1
  49. package/dist/src/services/clip-curation/refine.js +50 -20
  50. package/dist/src/services/clip-curation/scan.js +10 -3
  51. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  52. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  53. package/dist/src/services/clip-records.js +42 -1
  54. package/dist/src/services/clip-search.js +43 -13
  55. package/dist/src/services/file-directory.js +117 -0
  56. package/dist/src/services/hyperframes.js +283 -3
  57. package/dist/src/services/serverless-records.js +43 -0
  58. package/dist/src/services/storage.js +47 -2
  59. package/dist/src/services/upstream.js +5 -5
  60. package/dist/src/template-editor-shell.js +16 -2
  61. package/package.json +1 -1
  62. package/public/assets/discover-client-app.js +1 -0
  63. package/public/assets/file-directory-app.js +2 -0
  64. package/public/assets/homepage-client-app.js +12 -12
  65. package/public/assets/page-runtime-client-app.js +24 -24
  66. package/public/assets/placeholders/scene-placeholder.png +0 -0
  67. package/src/assets/favicon.ico +0 -0
  68. 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
- thumbnail: lookup.thumbnail ?? null,
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
- thumbnail: lookup.thumbnail ?? null,
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:nowrap}
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:960px){.rk-calendar-shell{grid-template-columns:1fr}}
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)}