@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8

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 (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +476 -260
  14. package/dist/src/cli.js +362 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21603
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -828
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3654
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,437 +0,0 @@
1
- // Swipe-mode customization brain + progressive queue.
2
- //
3
- // Takes a decomposed template whose footage is GhostCut-clean (plays a `-no-subs`
4
- // mirror) and that ALREADY has on-screen caption elements, clones its canonical
5
- // fork into a private derivative, then RE-CAPTIONS it: each original caption's
6
- // TEXT is rewritten to sell the customer's product while its timing (data-start/
7
- // duration), position (left/top) and styling stay exactly as the original — so
8
- // the new ad keeps the source video's caption tempo and choreography. Only the
9
- // words change. The LLM runs on the customer's own saved keys (BYOK), never billed.
10
- import { parseHTML } from "linkedom";
11
- import { serverlessRecords } from "./serverless-records.js";
12
- import { StorageService } from "./storage.js";
13
- import { ProviderService } from "./providers.js";
14
- import { cuesFromText } from "./captions.js";
15
- import { sanitizeCompositionHtml } from "./composition-sanitize.js";
16
- import { writeForkManifest } from "./fork-manifest.js";
17
- import { insertCaptionLayers } from "../devcli/composition-edit.js";
18
- const storage = new StorageService();
19
- const providers = new ProviderService();
20
- export const SWIPE_STACK_TARGET_DEPTH = 5;
21
- const ELIGIBILITY_SCAN_LIMIT = 80;
22
- const DEFAULT_CAPTION_STYLE = "word-pop";
23
- // Minimum original captions a template must have to be worth re-captioning.
24
- const MIN_CAPTION_SLOTS = 2;
25
- // Bump when the customize pipeline changes shape enough that variants built by
26
- // the previous pipeline should be pulled from the deck. v1 = the tack-on
27
- // pipeline (insertCaptionLayers word-pop over the original captions — the
28
- // double-caption artifact); v2 = in-place recaption. Un-swiped rows from older
29
- // versions are lazily marked "superseded" (see reconcileSwipeRows) which drops
30
- // them from the deck AND frees their template to be customized again properly.
31
- // Rows the customer already decided on (approved/rejected) are never touched.
32
- export const SWIPE_PIPELINE_VERSION = 2;
33
- // A "customizing" row whose lambda died mid-flight would otherwise occupy a
34
- // refill buffer slot forever; past the api lambda's max runtime it can't still
35
- // be live, so supersede it. Failed rows get a cooldown then are superseded too,
36
- // freeing their template for a retry (bounds a deterministic failure to one
37
- // LLM call per cooldown, and lets transient LLM flakes self-heal).
38
- const CUSTOMIZING_WEDGE_MS = 20 * 60 * 1000;
39
- const FAILED_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
40
- function isStaleSwipeRow(row) {
41
- const ageMs = Date.now() - Date.parse(String(row.updatedAt ?? row.createdAt ?? ""));
42
- if (row.status === "customizing" && Number.isFinite(ageMs) && ageMs > CUSTOMIZING_WEDGE_MS)
43
- return true;
44
- if (row.status === "failed" && Number.isFinite(ageMs) && ageMs > FAILED_RETRY_COOLDOWN_MS)
45
- return true;
46
- if ((row.pipelineVersion ?? 1) >= SWIPE_PIPELINE_VERSION)
47
- return false;
48
- return row.status === "ready" || row.status === "customizing" || row.status === "failed";
49
- }
50
- // Lazily migrate a customer's rows: supersede every un-swiped variant built by
51
- // an older pipeline. Idempotent (superseded rows are no longer stale) and cheap
52
- // after the first pass. Returns the rows with the patches applied.
53
- export async function reconcileSwipeRows(customerId, rows) {
54
- const out = [];
55
- for (const row of rows) {
56
- if (!isStaleSwipeRow(row)) {
57
- out.push(row);
58
- continue;
59
- }
60
- const updated = await serverlessRecords.updateSwipeCustomization({
61
- customerId,
62
- id: row.id,
63
- patch: { status: "superseded" }
64
- });
65
- out.push(updated ?? { ...row, status: "superseded" });
66
- }
67
- return out;
68
- }
69
- // Text generation only — the audio-only elevenlabs provider never appears here.
70
- const TEXT_MODEL_DEFAULTS = {
71
- openai: "gpt-5.4-mini",
72
- gemini: "gemini-2.5-flash",
73
- openrouter: "qwen/qwen3.6-plus",
74
- perplexity: "sonar"
75
- };
76
- const TEXT_PROVIDER_PRIORITY = ["openai", "gemini", "openrouter", "perplexity"];
77
- export class NoProviderKeyError extends Error {
78
- constructor() {
79
- super("Swipe customization needs an AI provider key. Add an OpenAI, Gemini, or OpenRouter key in Settings.");
80
- this.name = "NoProviderKeyError";
81
- }
82
- }
83
- // ---------------------------------------------------------------------------
84
- // Eligibility: decomposed (defaultForkId), GhostCut-clean (composition plays a
85
- // `-no-subs` mirror so there's no baked text to clash with), HAS caption slots
86
- // to re-caption, and not already customized for this customer.
87
- const MIRROR_MARKER = /-no-subs|\/ghostcut\//i;
88
- // Count static caption elements ([data-layer-kind="caption"] that are not the
89
- // animated word-by-word kind) via cheap regex — canonical forks only carry the
90
- // original static captions, so this is the recaption slot count. Style blocks
91
- // are dropped first: caption-animation CSS contains `[data-caption-animation=`
92
- // selectors that would otherwise inflate the animated count.
93
- export function countStaticCaptionsInHtml(html) {
94
- const markup = html.replace(/<style[\s\S]*?<\/style>/gi, "");
95
- const total = (markup.match(/data-layer-kind="caption"/g) ?? []).length;
96
- const animated = (markup.match(/data-caption-animation=/g) ?? []).length;
97
- return Math.max(0, total - animated);
98
- }
99
- async function readCanonicalEligibility(defaultForkId) {
100
- const html = await storage.readText(storage.compositionForkWorkingKey(defaultForkId, "composition.html"));
101
- if (!html)
102
- return { eligible: false, sourceUrl: null };
103
- const isMirror = MIRROR_MARKER.test(html);
104
- const sourceUrl = readCompositionSourceUrl(html);
105
- const captionSlots = countStaticCaptionsInHtml(html);
106
- return { eligible: isMirror && captionSlots >= MIN_CAPTION_SLOTS, sourceUrl };
107
- }
108
- export async function listEligibleTemplates(customerId, limit = SWIPE_STACK_TARGET_DEPTH * 4) {
109
- const feed = await serverlessRecords.listTemplatesFeed({ limit: ELIGIBILITY_SCAN_LIMIT });
110
- const listed = await serverlessRecords.listSwipeCustomizationsForCustomer(customerId);
111
- const existing = await reconcileSwipeRows(customerId, listed);
112
- // Superseded rows do NOT consume their template — the whole point is letting
113
- // the current pipeline re-customize what an older one botched.
114
- const alreadyCustomized = new Set(existing.filter((row) => row.status !== "superseded").map((row) => row.templateId));
115
- const eligible = [];
116
- for (const template of feed) {
117
- if (eligible.length >= limit)
118
- break;
119
- if (!template.defaultForkId)
120
- continue;
121
- if (alreadyCustomized.has(template.id))
122
- continue;
123
- const { eligible: ok, sourceUrl } = await readCanonicalEligibility(template.defaultForkId);
124
- if (!ok)
125
- continue;
126
- eligible.push({ template, sourceUrl });
127
- }
128
- return eligible;
129
- }
130
- function readCompositionSourceUrl(html) {
131
- try {
132
- const { document } = parseHTML(html);
133
- const root = document.querySelector("[data-composition-id]");
134
- const fromAttr = root?.getAttribute("data-source-video")?.trim();
135
- if (fromAttr && /^https?:\/\//i.test(fromAttr))
136
- return fromAttr;
137
- const backing = document.querySelector("[data-vf-playback-source]");
138
- const backingSrc = (backing?.getAttribute("src") ?? backing?.getAttribute("data-src"))?.trim();
139
- if (backingSrc && /^https?:\/\//i.test(backingSrc))
140
- return backingSrc;
141
- const video = Array.from(document.querySelectorAll("[data-layer-kind=\"video\"]"))
142
- .map((n) => n.getAttribute("src")?.trim())
143
- .find((src) => src && /^https?:\/\//i.test(src));
144
- return video ?? null;
145
- }
146
- catch {
147
- return null;
148
- }
149
- }
150
- // ---------------------------------------------------------------------------
151
- // Context (light — tone/theme only; the caption slots are the real input).
152
- function loadContext(smartDecomposeText) {
153
- if (!smartDecomposeText)
154
- return { transcript: "", viralDna: "" };
155
- try {
156
- const result = JSON.parse(smartDecomposeText)?.result ?? {};
157
- const transcript = typeof result?.transcript?.text === "string"
158
- ? result.transcript.text
159
- : Array.isArray(result?.transcript?.segments)
160
- ? result.transcript.segments.map((s) => (typeof s?.text === "string" ? s.text : "")).join(" ").trim()
161
- : "";
162
- const viralDna = typeof result?.viralDna === "string" ? result.viralDna : result?.viralDna ? JSON.stringify(result.viralDna) : "";
163
- return { transcript, viralDna };
164
- }
165
- catch {
166
- return { transcript: "", viralDna: "" };
167
- }
168
- }
169
- // Decompose emits static captions as <div data-layer-kind="caption" ...><span
170
- // data-vf-text-lines><span data-vf-text-inline style="…">words</span></span></div>
171
- // — the inline span carries the outline/stroke treatment, so write the new words
172
- // INTO it rather than flattening the element (`el.textContent =` on the div
173
- // would strip the treatment — the same trap /editor's set_layer_text hit).
174
- // data-label mirrors the words for tooling; keep it in sync.
175
- export function setCaptionSlotText(el, text) {
176
- const inline = typeof el.querySelector === "function" ? el.querySelector("[data-vf-text-inline]") : null;
177
- (inline ?? el).textContent = text;
178
- if (typeof el.getAttribute === "function" && el.getAttribute("data-label") != null) {
179
- el.setAttribute("data-label", text.slice(0, 40));
180
- }
181
- }
182
- // A canonical fork should only carry the original STATIC captions, but if an
183
- // editor session (or an older customize pipeline) left animated word-by-word
184
- // caption layers on it, the clone inherits words that fight the recaption —
185
- // drop them before rewriting. Returns how many were removed.
186
- export function stripAnimatedCaptionLayers(document) {
187
- const root = document.querySelector("[data-composition-id]") ?? document;
188
- const nodes = Array.from(root.querySelectorAll("[data-caption-animation]"));
189
- for (const node of nodes)
190
- node.parentNode?.removeChild(node);
191
- return nodes.length;
192
- }
193
- export function collectCaptionSlots(document) {
194
- const root = document.querySelector("[data-composition-id]") ?? document;
195
- return Array.from(root.querySelectorAll('[data-layer-kind="caption"]:not([data-caption-animation])'))
196
- .map((el) => ({
197
- el,
198
- text: String(el.textContent || "").trim().replace(/\s+/g, " "),
199
- start: Number(el.getAttribute("data-start")) || 0,
200
- duration: Number(el.getAttribute("data-duration")) || 0
201
- }))
202
- .filter((s) => s.text)
203
- .sort((a, b) => a.start - b.start);
204
- }
205
- function buildRecaptionPrompt(input) {
206
- const { productDescription, slots, context, sourceTitle } = input;
207
- const list = slots.map((s, i) => `${i + 1}. [${s.start.toFixed(1)}s-${(s.start + s.duration).toFixed(1)}s] "${s.text}"`).join("\n");
208
- return `You are re-captioning a viral video so it now advertises the customer's product. The video's footage, cuts, timing and caption placement all stay the same — you ONLY rewrite the words of each on-screen caption so they sell the product.
209
-
210
- CUSTOMER PRODUCT / OFFER:
211
- ${productDescription.trim()}
212
-
213
- THE VIDEO
214
- - Original title: ${sourceTitle}
215
- - Why it went viral: ${context.viralDna || "(unknown)"}
216
- - Spoken/heard: ${(context.transcript || "(none)").slice(0, 800)}
217
-
218
- ORIGINAL ON-SCREEN CAPTIONS (in order, with timing):
219
- ${list}
220
-
221
- TASK
222
- Rewrite EACH caption above so the video becomes a native-feeling ad for the product. Rules:
223
- - Return EXACTLY ${slots.length} captions, in the SAME order, one replacement per original.
224
- - Keep each replacement roughly the SAME LENGTH as its original (it must fit the same on-screen slot) and the SAME BEAT/ROLE (a hook stays a hook, a reveal stays a reveal, a CTA stays a CTA).
225
- - Sound like an authentic creator, not a corporate ad. No hashtags inside the on-screen captions.
226
-
227
- Return STRICTLY valid JSON, no markdown fences:
228
- {
229
- "title": "<=60 char scroll-stopping post title",
230
- "socialCaption": "1-2 sentence post caption (may include 1-3 hashtags)",
231
- "pinnedComment": "short first-comment CTA, or null",
232
- "captions": [ ${slots.map(() => '"..."').join(", ")} ]
233
- }`;
234
- }
235
- function parseJsonLoose(text) {
236
- const trimmed = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
237
- try {
238
- return JSON.parse(trimmed);
239
- }
240
- catch {
241
- const start = trimmed.indexOf("{");
242
- const end = trimmed.lastIndexOf("}");
243
- if (start >= 0 && end > start)
244
- return JSON.parse(trimmed.slice(start, end + 1));
245
- throw new Error("LLM did not return parseable JSON.");
246
- }
247
- }
248
- async function generateRecaption(input) {
249
- const available = await providers.listAvailableProvidersAsync(input.customerId);
250
- // Text generation only — never fall back to a non-text provider (e.g. the
251
- // audio-only elevenlabs key), which has no text model.
252
- const provider = TEXT_PROVIDER_PRIORITY.find((p) => available.includes(p));
253
- if (!provider)
254
- throw new NoProviderKeyError();
255
- const generated = await providers.generateText({
256
- customerId: input.customerId,
257
- jobId: input.jobId,
258
- workerId: "swipe-customize",
259
- provider,
260
- model: TEXT_MODEL_DEFAULTS[provider] ?? "gpt-5.4-mini",
261
- prompt: input.prompt,
262
- temperature: 0.7,
263
- responseFormat: "json"
264
- });
265
- const parsed = parseJsonLoose(generated.text);
266
- const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : "");
267
- const captions = Array.isArray(parsed.captions) ? parsed.captions.map((c) => str(c)) : [];
268
- return {
269
- title: str(parsed.title) || input.jobId,
270
- socialCaption: str(parsed.socialCaption) || str(parsed.caption),
271
- pinnedComment: str(parsed.pinnedComment) || null,
272
- captions
273
- };
274
- }
275
- // ---------------------------------------------------------------------------
276
- // Clone the canonical decomposed fork's working state into a private derivative.
277
- async function cloneDecomposedFork(input) {
278
- const canonical = await serverlessRecords.getCompositionFork(input.canonicalForkId);
279
- const fork = await serverlessRecords.createCompositionFork({
280
- customerId: input.customerId,
281
- templateId: input.template.id,
282
- parentForkId: input.canonicalForkId,
283
- parentVersion: canonical?.latestVersion ?? null,
284
- visibility: "private",
285
- title: input.title
286
- });
287
- for (const file of ["composition.html", "composition.json", "cast.json", "product-placement.json", "scene-annotations.json"]) {
288
- const text = await storage.readText(storage.compositionForkWorkingKey(input.canonicalForkId, file));
289
- if (text == null)
290
- continue;
291
- const contentType = file.endsWith(".html") ? "text/html; charset=utf-8" : "application/json";
292
- await storage.putText(storage.compositionForkWorkingKey(fork.id, file), text, contentType);
293
- }
294
- await writeForkManifest(storage, fork, { kind: "working" });
295
- return fork.id;
296
- }
297
- function readCompositionDurationSeconds(html) {
298
- try {
299
- const { document } = parseHTML(html);
300
- const root = document.querySelector("[data-composition-id]") ?? document.querySelector("[data-duration]");
301
- const dur = Number(root?.getAttribute("data-duration"));
302
- return Number.isFinite(dur) && dur > 0 ? dur : null;
303
- }
304
- catch {
305
- return null;
306
- }
307
- }
308
- // Run ONE full recaption: clone → read caption slots → AI rewrites each in place
309
- // → persist. Creates the record up front (status "customizing") and drives it to
310
- // "ready" or "failed".
311
- export async function customizeTemplateForProduct(input) {
312
- const { customerId, template, productDescription } = input;
313
- const sourceTitle = template.title ?? template.id;
314
- const availableProviders = await providers.listAvailableProvidersAsync(customerId);
315
- if (!availableProviders.length)
316
- throw new NoProviderKeyError();
317
- const record = await serverlessRecords.createSwipeCustomization({
318
- customerId,
319
- templateId: template.id,
320
- sourceTitle,
321
- status: "customizing",
322
- pipelineVersion: SWIPE_PIPELINE_VERSION
323
- });
324
- try {
325
- if (!template.defaultForkId)
326
- throw new Error("Template is not decomposed (no default fork).");
327
- const forkId = await cloneDecomposedFork({ customerId, template, canonicalForkId: template.defaultForkId, title: sourceTitle });
328
- const context = loadContext(await storage.readText(storage.compositionForkWorkingKey(template.defaultForkId, "smart-decompose.json")));
329
- const rawHtml = await storage.readText(storage.compositionForkWorkingKey(forkId, "composition.html"));
330
- if (!rawHtml)
331
- throw new Error("Cloned fork has no composition.html.");
332
- // Sanitize the BASE document up front (before any LLM text lands in it):
333
- // sanitizeCompositionHtml is raw-regex hardening, and run after the rewrite
334
- // it can chew visible copy (e.g. a caption containing "hold on tight = ...").
335
- // Text inserted below goes through linkedom textContent/setAttribute, which
336
- // entity-escapes on serialization, so the persisted doc stays inert.
337
- const { html } = sanitizeCompositionHtml(rawHtml);
338
- const durationSeconds = readCompositionDurationSeconds(html) ?? 30;
339
- // documentElement.outerHTML drops the doctype (it's a sibling node) — keep
340
- // the preview iframe out of quirks mode by re-prepending it.
341
- const doctype = /^\s*<!doctype/i.test(html) ? "<!DOCTYPE html>\n" : "";
342
- const { document } = parseHTML(html);
343
- stripAnimatedCaptionLayers(document);
344
- // Only rewrite captions that carry WORDS in any script (\p{L} covers CJK,
345
- // Cyrillic, Arabic, … — viral sources are not all English). Numeric/emoji-only
346
- // captions (e.g. a "6,5,4,3,2,1" countdown overlay) are structural to the
347
- // video's rhythm — the user wants that tempo/choreography kept — so leave
348
- // them untouched.
349
- const slots = collectCaptionSlots(document).filter((s) => /\p{L}/u.test(s.text));
350
- const copy = await generateRecaption({
351
- customerId,
352
- jobId: record.id,
353
- prompt: buildRecaptionPrompt({ productDescription, slots, context, sourceTitle })
354
- });
355
- // The prompt demands EXACTLY slots.length captions. json_object mode only
356
- // guarantees valid JSON, not shape — a miscounted/empty/misshapen response
357
- // must fail the record (retryable) instead of silently deleting caption
358
- // layers below and shipping a caption-stripped card as "ready".
359
- if (slots.length) {
360
- const usable = copy.captions.filter(Boolean).length;
361
- if (copy.captions.length !== slots.length || usable < Math.min(slots.length, MIN_CAPTION_SLOTS)) {
362
- throw new Error(`LLM recaption shape mismatch: expected ${slots.length} captions, got ${usable} usable of ${copy.captions.length} returned.`);
363
- }
364
- }
365
- let styleId = "recaption";
366
- if (slots.length) {
367
- // Rewrite each caption's TEXT in place — timing/position/style untouched.
368
- slots.forEach((slot, i) => {
369
- const next = copy.captions[i];
370
- if (next)
371
- setCaptionSlotText(slot.el, next);
372
- else
373
- slot.el.parentNode?.removeChild(slot.el); // no replacement → drop rather than keep off-product text
374
- });
375
- const serialized = doctype + document.documentElement.outerHTML;
376
- await storage.putText(storage.compositionForkWorkingKey(forkId, "composition.html"), serialized, "text/html; charset=utf-8");
377
- }
378
- else {
379
- // No word-bearing caption slots (post-\p{L} this means genuinely word-free:
380
- // countdown/emoji overlays only) — page the social caption into fresh
381
- // caption layers instead. Serialize from the parsed document so the
382
- // animated-layer strip above is kept; the numeric/emoji statics stay (they
383
- // are rhythm, and carry no words to conflict with the new copy).
384
- const cues = cuesFromText(copy.socialCaption || copy.title, 0, durationSeconds, { maxWordsPerCue: 4 });
385
- const applied = insertCaptionLayers(doctype + document.documentElement.outerHTML, cues, { style: DEFAULT_CAPTION_STYLE });
386
- styleId = applied.styleId;
387
- await storage.putText(storage.compositionForkWorkingKey(forkId, "composition.html"), applied.html, "text/html; charset=utf-8");
388
- }
389
- const updatedFork = await serverlessRecords.updateCompositionFork({ forkId, patch: {} });
390
- if (updatedFork)
391
- await writeForkManifest(storage, updatedFork, { kind: "working" });
392
- const previewSourceUrl = input.sourceUrl ?? readCompositionSourceUrl(html);
393
- return await serverlessRecords.updateSwipeCustomization({
394
- customerId,
395
- id: record.id,
396
- patch: {
397
- forkId,
398
- status: "ready",
399
- tailoredTitle: copy.title,
400
- tailoredCaption: copy.socialCaption,
401
- tailoredPinnedComment: copy.pinnedComment,
402
- captionStyleId: styleId,
403
- previewSourceUrl,
404
- durationSeconds,
405
- jobId: record.id
406
- }
407
- });
408
- }
409
- catch (error) {
410
- const message = error instanceof Error ? error.message : String(error);
411
- const failed = await serverlessRecords.updateSwipeCustomization({
412
- customerId,
413
- id: record.id,
414
- patch: { status: "failed", error: message }
415
- });
416
- if (error instanceof NoProviderKeyError)
417
- throw error;
418
- return failed ?? { ...record, status: "failed", error: message };
419
- }
420
- }
421
- export async function customizeOneMore(input) {
422
- const eligible = await listEligibleTemplates(input.customerId, 1);
423
- const pick = eligible[0];
424
- if (!pick)
425
- return null;
426
- return customizeTemplateForProduct({
427
- customerId: input.customerId,
428
- template: pick.template,
429
- sourceUrl: pick.sourceUrl,
430
- productDescription: input.productDescription
431
- });
432
- }
433
- export async function countReadyCustomizations(customerId) {
434
- const rows = await serverlessRecords.listSwipeCustomizationsForCustomer(customerId);
435
- return rows.filter((row) => row.status === "ready" && !isStaleSwipeRow(row)).length;
436
- }
437
- //# sourceMappingURL=swipe-customize.js.map