@hubfluencer/mcp 0.9.1 → 0.11.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/dist/index.js CHANGED
@@ -29498,6 +29498,16 @@ function normalizeStatus(kind, slug, data) {
29498
29498
  video_url: videoUrl,
29499
29499
  error: d.error_message ?? null
29500
29500
  };
29501
+ if (typeof d.failed_stage === "string")
29502
+ status.failed_stage = d.failed_stage;
29503
+ if (typeof d.failure_code === "string")
29504
+ status.failure_code = d.failure_code;
29505
+ if (d.failure_details && typeof d.failure_details === "object")
29506
+ status.failure_details = d.failure_details;
29507
+ if (typeof d.failure_source === "string")
29508
+ status.failure_source = d.failure_source;
29509
+ if (typeof d.failure_attempt === "number")
29510
+ status.failure_attempt = d.failure_attempt;
29501
29511
  if (typeof d.latest_render_free === "boolean") {
29502
29512
  status.latest_render_free = d.latest_render_free;
29503
29513
  }
@@ -29544,7 +29554,8 @@ function normalizeStatus(kind, slug, data) {
29544
29554
  const staleSegments = segments.filter((segment) => segment.video_stale === true && segment.status === "completed");
29545
29555
  const failedSegments = segments.filter((segment) => segment.status === "failed");
29546
29556
  const batchStatus = d.batch_generation_status ?? null;
29547
- const batchActive = batchStatus === "running" || batchStatus === "preparing" || batchStatus === "awaiting_previews" || batchStatus === "paused_for_retry";
29557
+ const batchActive = batchStatus === "running" || batchStatus === "preparing" || batchStatus === "awaiting_previews";
29558
+ const batchRetryRequired = batchStatus === "paused_for_retry";
29548
29559
  const batchInsufficientCredits = batchStatus === "paused_insufficient_credits";
29549
29560
  const batchFailed = batchStatus === "failed";
29550
29561
  const autopilotActive = [
@@ -29588,33 +29599,42 @@ function normalizeStatus(kind, slug, data) {
29588
29599
  currentMusic.error_message
29589
29600
  ].find((value) => typeof value === "string" && value.trim().length > 0);
29590
29601
  const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
29591
- const workActive = autopilotActive || batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29602
+ const childWorkActive = batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29603
+ const autopilotStartedAt = typeof d.autopilot_started_at === "string" ? Date.parse(d.autopilot_started_at) : Number.NaN;
29604
+ const now = Date.now();
29605
+ const autopilotResumeGraceActive = autopilotActive && Number.isFinite(autopilotStartedAt) && autopilotStartedAt <= now && now - autopilotStartedAt <= 120000;
29606
+ const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
29607
+ const batchRecoveryActive = autopilotFailed && batchActive;
29608
+ const childFailureActionable = !batchRecoveryActive && (!autopilotActive || !childWorkActive && !autopilotResumeGraceActive);
29609
+ const workActive = autopilotActive || childWorkActive;
29592
29610
  const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
29593
29611
  const ready = deliveredArtifactSupersedesOrchestrationFailure;
29594
- const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
29595
- const renderFailed = renderStatus === "failed";
29596
- const failed = autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || failedSegments.length > 0 || audioFailed;
29597
- const terminal = ready || failed || batchInsufficientCredits || !workActive;
29612
+ const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
29613
+ const renderFailed = renderStatus === "failed" && childFailureActionable;
29614
+ const failed = parentFailureActionable && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || childFailureActionable && scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && failedSegments.length > 0 || childFailureActionable && audioFailed;
29615
+ const terminal = ready || failed || batchRetryRequired || batchInsufficientCredits || !workActive;
29598
29616
  let stage = autopilot;
29599
29617
  if (ready)
29600
29618
  stage = "completed";
29601
- else if (autopilot === "failed")
29619
+ else if (batchRetryRequired)
29620
+ stage = "batch_retry_required";
29621
+ else if (parentFailureActionable && autopilot === "failed")
29602
29622
  stage = "failed";
29603
- else if (autopilot === "cancelled")
29623
+ else if (parentFailureActionable && autopilot === "cancelled")
29604
29624
  stage = "cancelled";
29605
29625
  else if (renderFailed)
29606
29626
  stage = "render_failed";
29607
- else if (scenarioApplyFailed)
29627
+ else if (childFailureActionable && scenarioApplyFailed)
29608
29628
  stage = "scenario_apply_failed";
29609
- else if (scenarioFailed)
29629
+ else if (childFailureActionable && scenarioFailed)
29610
29630
  stage = "scenario_failed";
29611
- else if (narrationFailed)
29631
+ else if (childFailureActionable && narrationFailed)
29612
29632
  stage = "narration_failed";
29613
- else if (batchFailed)
29633
+ else if (childFailureActionable && batchFailed)
29614
29634
  stage = "batch_failed";
29615
- else if (failedSegments.length > 0)
29635
+ else if (childFailureActionable && failedSegments.length > 0)
29616
29636
  stage = "segment_failed";
29617
- else if (audioFailed)
29637
+ else if (childFailureActionable && audioFailed)
29618
29638
  stage = "audio_failed";
29619
29639
  else if (batchInsufficientCredits)
29620
29640
  stage = "insufficient_credits";
@@ -29633,6 +29653,7 @@ function normalizeStatus(kind, slug, data) {
29633
29653
  else if (!workActive && stale)
29634
29654
  stage = "editing_required";
29635
29655
  const segmentError = failedSegments.map((segment) => segment.error_message).find((value) => typeof value === "string" && value.length > 0);
29656
+ const actionableChildError = childFailureActionable ? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError : null;
29636
29657
  return {
29637
29658
  kind,
29638
29659
  slug,
@@ -29640,7 +29661,7 @@ function normalizeStatus(kind, slug, data) {
29640
29661
  terminal,
29641
29662
  ready,
29642
29663
  video_url: videoUrl,
29643
- error: ready ? null : d.autopilot_error_message ?? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
29664
+ error: ready ? null : batchRetryRequired ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing." : (parentFailureActionable ? d.autopilot_error_message : null) ?? actionableChildError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
29644
29665
  stale,
29645
29666
  active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29646
29667
  stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
@@ -30233,7 +30254,7 @@ function readStoredCredentials() {
30233
30254
  // package.json
30234
30255
  var package_default = {
30235
30256
  name: "@hubfluencer/mcp",
30236
- version: "0.9.1",
30257
+ version: "0.11.0",
30237
30258
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30238
30259
  license: "MIT",
30239
30260
  author: "Monocursive <contact@monocursive.com>",
@@ -30546,6 +30567,11 @@ var getStatusOutput = exports_external.object({
30546
30567
  stale: exports_external.boolean().optional(),
30547
30568
  active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30548
30569
  stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30570
+ failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
30571
+ failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
30572
+ failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30573
+ failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30574
+ failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30549
30575
  latest_render_free: exports_external.boolean().optional(),
30550
30576
  last_free_rerender_failed: exports_external.boolean().optional()
30551
30577
  }).passthrough();
@@ -30560,6 +30586,11 @@ var waitForCompletionOutput = exports_external.object({
30560
30586
  stale: exports_external.boolean().optional(),
30561
30587
  active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30562
30588
  stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30589
+ failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
30590
+ failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
30591
+ failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30592
+ failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30593
+ failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30563
30594
  latest_render_free: exports_external.boolean().optional(),
30564
30595
  last_free_rerender_failed: exports_external.boolean().optional(),
30565
30596
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
@@ -30626,6 +30657,9 @@ var createEditorOutput = exports_external.object({
30626
30657
  estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30627
30658
  available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30628
30659
  affordable: exports_external.boolean().optional(),
30660
+ launched: exports_external.boolean().optional(),
30661
+ authorized_credits: exports_external.number().optional(),
30662
+ credits_spent: exports_external.number().optional(),
30629
30663
  charged: exports_external.boolean().optional(),
30630
30664
  note: exports_external.string().optional(),
30631
30665
  next: exports_external.string().optional()
@@ -30642,6 +30676,9 @@ var makeVideoOutput = exports_external.object({
30642
30676
  estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30643
30677
  available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30644
30678
  affordable: exports_external.boolean().optional(),
30679
+ launched: exports_external.boolean().optional(),
30680
+ authorized_credits: exports_external.number().optional(),
30681
+ credits_spent: exports_external.number().optional(),
30645
30682
  charged: exports_external.boolean().optional(),
30646
30683
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30647
30684
  timed_out: exports_external.boolean().optional(),
@@ -31102,7 +31139,8 @@ var ALLOW_LOOPBACK_FETCH = /^http:\/\/(localhost|127\.0\.0\.1|\[?::1\]?)/.test(p
31102
31139
  var MAX_VIDEO_BYTES = 500000000;
31103
31140
  var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
31104
31141
  var MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
31105
- var MAX_LOGO_BYTES = 1 * 1024 * 1024;
31142
+ var MAX_LOGO_BYTES = 5 * 1024 * 1024;
31143
+ var MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
31106
31144
  var MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
31107
31145
  var MULTIPART_THRESHOLD = 50 * 1024 * 1024;
31108
31146
  var MAX_CATALOG_VIDEO_DURATION_SECONDS = 60;
@@ -31795,12 +31833,12 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
31795
31833
  8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
31796
31834
 
31797
31835
  CREDITS (spent server-side; each tool prices before charging — see its description)
31798
- - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31836
+ - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card/logo edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31799
31837
  - Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video supports dry_run/max_credits; create_editor_ad/start_autopilot require max_credits before launching.
31800
31838
  - AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
31801
31839
 
31802
31840
  WAITING & RESUMING
31803
- - Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout. A terminal result is not always ready: an editor can go terminal in a needs-action stage (editing_required = stale after edits, regenerate the flagged scenes/audio; insufficient_credits = batch parked, top up / reduce scope; idle = draft with nothing generating, start generation first) — act on those instead of re-polling.
31841
+ - Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout. A terminal result is not always ready: an editor can go terminal in a needs-action stage (editing_required = stale after edits, regenerate the flagged scenes/audio; batch_retry_required = call retry_editor_batch or cancel_editor_batch before starting Autopilot again; insufficient_credits = batch parked, top up / reduce scope; idle = draft with nothing generating, start generation first) — act on those instead of re-polling.
31804
31842
  - Creates and charged actions are idempotency-keyed: a transport retry is safe; a genuine re-run after a terminal FAILURE restarts rather than replaying the dead attempt.
31805
31843
 
31806
31844
  LOCAL FILES (sandboxed)
@@ -31820,8 +31858,7 @@ async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
31820
31858
  }
31821
31859
  return status;
31822
31860
  }
31823
- async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, intervalMs) {
31824
- const deadline = Date.now() + budgetMs;
31861
+ async function pollSegmentToTerminal(client, slug, segmentId, extra, deadline, intervalMs) {
31825
31862
  const sid = String(segmentId);
31826
31863
  const read = async () => {
31827
31864
  const res = await client.get(`/editor/${slug}`, undefined, deadline);
@@ -31970,6 +32007,7 @@ registerTool("make_video", {
31970
32007
  const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
31971
32008
  let estimated_credits = null;
31972
32009
  let available_credits = null;
32010
+ let authorizedCredits;
31973
32011
  try {
31974
32012
  const cost = asRecord(asRecord(await client.get(costPath)).data);
31975
32013
  estimated_credits = typeof cost.total === "number" ? cost.total : null;
@@ -32004,6 +32042,7 @@ registerTool("make_video", {
32004
32042
  if (approvedCap === null) {
32005
32043
  throw new Error("Editor autopilot launch requires max_credits or a live quote");
32006
32044
  }
32045
+ authorizedCredits = approvedCap;
32007
32046
  await client.post(`/editor/${slug}/autopilot`, { max_credits: approvedCap }, makeVideoAutopilotKey(slug, args, startState));
32008
32047
  }
32009
32048
  await reportProgress(extra, 0, `started ${kind} ${slug}`);
@@ -32012,12 +32051,29 @@ registerTool("make_video", {
32012
32051
  if (status.ready && status.video_url && args.save_path) {
32013
32052
  saved_to = (await downloadTo(status.video_url, args.save_path)).saved_to;
32014
32053
  }
32054
+ let editorCreditsSpent;
32055
+ if (kind === "editor") {
32056
+ try {
32057
+ const current = asRecord(asRecord(await client.get(`/editor/${slug}`, undefined, Date.now() + 5000)).data);
32058
+ if (typeof current.autopilot_credits_spent === "number") {
32059
+ editorCreditsSpent = current.autopilot_credits_spent;
32060
+ }
32061
+ } catch {}
32062
+ }
32063
+ const billing = kind === "short" ? { charged: true } : {
32064
+ launched: true,
32065
+ authorized_credits: authorizedCredits,
32066
+ ...editorCreditsSpent === undefined ? {} : {
32067
+ credits_spent: editorCreditsSpent,
32068
+ charged: editorCreditsSpent > 0
32069
+ }
32070
+ };
32015
32071
  const result = {
32016
32072
  ...status,
32017
32073
  kind_inferred: requestedKind === undefined,
32018
32074
  estimated_credits,
32019
32075
  available_credits,
32020
- charged: true,
32076
+ ...billing,
32021
32077
  saved_to,
32022
32078
  timed_out: !status.terminal
32023
32079
  };
@@ -32025,7 +32081,7 @@ registerTool("make_video", {
32025
32081
  const links = status.ready && status.video_url ? [mp4Link(status.video_url, slug)] : [];
32026
32082
  return ok({
32027
32083
  ...result,
32028
- note: status.ready ? "Done. Presigned ~24h URL — download promptly." : status.terminal ? `Terminal (${status.stage}). ${status.error ?? ""}`.trim() + ` To restart, fix the cause (often credits) then re-run make_video, or resume with ${resume}.` : `Still rendering — call wait_for_completion(${resumeArgs}).`
32084
+ note: status.ready ? "Done. Presigned ~24h URL — download promptly." : status.terminal ? status.stage === "batch_retry_required" ? `Terminal (${status.stage}). ${status.error ?? ""} Resolve the paused batch before restarting Autopilot.`.trim() : `Terminal (${status.stage}). ${status.error ?? ""}`.trim() + ` To restart, fix the cause (often credits) then re-run make_video, or resume with ${resume}.` : `Still rendering — call wait_for_completion(${resumeArgs}).`
32029
32085
  }, links);
32030
32086
  }));
32031
32087
  registerTool("get_credits", {
@@ -32088,7 +32144,7 @@ registerTool("list_projects", {
32088
32144
  }));
32089
32145
  registerTool("create_short", {
32090
32146
  title: "Create a short (draft)",
32091
- description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay that's an editor-only feature; use create_editor_ad for logo branding.)",
32147
+ description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " + "(set_short_poster) before generate_short.",
32092
32148
  inputSchema: {
32093
32149
  product_prompt: exports_external.string().min(10).describe("What the short should be about"),
32094
32150
  language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
@@ -32185,7 +32241,7 @@ registerTool("create_short", {
32185
32241
  }));
32186
32242
  registerTool("update_short", {
32187
32243
  title: "Update a short draft (0 credits)",
32188
- description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
32244
+ description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " + "short_logo_treatment) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
32189
32245
  inputSchema: {
32190
32246
  slug: exports_external.string().describe("Short slug (from create_short)"),
32191
32247
  product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
@@ -32221,6 +32277,8 @@ registerTool("update_short", {
32221
32277
  brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + `poster is set. Server falls back to your brand profile's name when unset. Pass "" to remove it.`),
32222
32278
  end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
32223
32279
  poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false)."),
32280
+ short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " + "a FREE re-render — logo fields never cost credits."),
32281
+ short_logo_treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe('When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' + "FREE re-render."),
32224
32282
  brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " + "on update it sets the linked profile. Must be one of your own profiles.")
32225
32283
  },
32226
32284
  annotations: {
@@ -32278,13 +32336,17 @@ registerTool("update_short", {
32278
32336
  body.end_card = args.end_card;
32279
32337
  if (args.poster_includes_lockup !== undefined)
32280
32338
  body.poster_includes_lockup = args.poster_includes_lockup;
32339
+ if (args.short_logo_position !== undefined)
32340
+ body.short_logo_position = args.short_logo_position;
32341
+ if (args.short_logo_treatment !== undefined)
32342
+ body.short_logo_treatment = args.short_logo_treatment;
32281
32343
  if (args.brand_profile_id !== undefined)
32282
32344
  body.brand_profile_id = args.brand_profile_id;
32283
32345
  const res = await client.patch(`/shorts/${args.slug}`, body);
32284
32346
  const data = asRecord(res).data ?? res;
32285
32347
  return ok({
32286
32348
  ...asRecord(data),
32287
- next: "Saved. Only text/style/CTA/end-card changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
32349
+ next: "Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
32288
32350
  });
32289
32351
  }));
32290
32352
  async function getTracking(client, slug, deadline) {
@@ -32453,7 +32515,7 @@ registerTool("generate_short", {
32453
32515
  }));
32454
32516
  registerTool("rerender_short", {
32455
32517
  title: "Re-render a short (FREE — 0 credits)",
32456
- description: "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
32518
+ description: "FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " + "(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
32457
32519
  inputSchema: {
32458
32520
  slug: exports_external.string().describe("Short slug (from create_short)")
32459
32521
  },
@@ -32708,7 +32770,7 @@ registerTool("create_editor_ad", {
32708
32770
  product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot."),
32709
32771
  product_description: exports_external.string().max(500).optional().describe("Exact product/character description used with product_image_path."),
32710
32772
  closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
32711
- logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤1 MiB) to overlay."),
32773
+ logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
32712
32774
  logo_treatment: exports_external.enum(["intro", "outro", "both", "none"]).optional().describe('When the logo bumper plays: "intro", "outro", "both" (default), or "none". Requires logo_path.'),
32713
32775
  logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe('Logo corner: "top-left", "top-right" (default), "bottom-left", or "bottom-right". Requires logo_path.'),
32714
32776
  logo_duration_seconds: exports_external.number().min(0.5).max(3).optional().describe("Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path."),
@@ -32819,7 +32881,10 @@ registerTool("create_editor_ad", {
32819
32881
  kind: "editor",
32820
32882
  status,
32821
32883
  ...quote,
32822
- charged: true,
32884
+ launched: true,
32885
+ authorized_credits: args.max_credits,
32886
+ credits_spent: 0,
32887
+ charged: false,
32823
32888
  next: "wait_for_completion"
32824
32889
  });
32825
32890
  } finally {
@@ -32828,7 +32893,7 @@ registerTool("create_editor_ad", {
32828
32893
  }));
32829
32894
  registerTool("start_autopilot", {
32830
32895
  title: "Start autopilot on an existing editor draft",
32831
- description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
32896
+ description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Failed or cancelled runs resume normally, except when status is batch_retry_required: " + "call retry_editor_batch (then wait for that prepaid batch) or cancel_editor_batch first. For a completed project, " + "set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
32832
32897
  inputSchema: {
32833
32898
  slug: exports_external.string().describe("Editor project slug"),
32834
32899
  language: exports_external.enum(LANGUAGES).optional().describe('Optional language override applied before launch. Bare ISO-639-1 code only, e.g. "fr" — no ' + "region/script subtag. Drives scenario, narration, voice, and captions."),
@@ -32874,10 +32939,52 @@ registerTool("start_autopilot", {
32874
32939
  kind: "editor",
32875
32940
  status,
32876
32941
  ...quote,
32877
- charged: true,
32942
+ launched: true,
32943
+ authorized_credits: args.max_credits,
32944
+ credits_spent: 0,
32945
+ charged: false,
32878
32946
  next: "wait_for_completion"
32879
32947
  });
32880
32948
  }));
32949
+ registerTool("retry_editor_batch", {
32950
+ title: "Retry a paused editor batch",
32951
+ description: "Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " + "scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " + "paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " + "to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " + "relaunch only with an approved cap. A batch parked on " + "insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
32952
+ inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32953
+ annotations: {
32954
+ title: "Retry editor batch",
32955
+ ...WRITE,
32956
+ idempotentHint: false
32957
+ }
32958
+ }, tool(async (args, client) => {
32959
+ const res = await client.post(`/video-factories/${args.slug}/retry-batch-segment`);
32960
+ return ok({
32961
+ ...asRecord(asRecord(res).data ?? res),
32962
+ slug: args.slug,
32963
+ charged: false,
32964
+ note: "The failed scene retry is queued and the prepaid batch is running. Wait for it to settle; if the parent Autopilot run is then terminal, quote start_autopilot again and relaunch only with an approved max_credits cap.",
32965
+ next: "wait_for_completion"
32966
+ });
32967
+ }));
32968
+ registerTool("cancel_editor_batch", {
32969
+ title: "Cancel an editor batch",
32970
+ description: "Cancels an editor batch in awaiting_previews, running, or paused_for_retry so the project can be edited or " + "Autopilot can be started again. Cancellation is destructive: dispatched scene work is stopped and its prepaid " + "credits are not refunded; only an awaiting_previews batch (before dispatch) is refunded by the server. Spends " + "0 additional credits. Scope: video:generate.",
32971
+ inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32972
+ annotations: {
32973
+ title: "Cancel editor batch",
32974
+ readOnlyHint: false,
32975
+ destructiveHint: true,
32976
+ openWorldHint: true,
32977
+ idempotentHint: false
32978
+ }
32979
+ }, tool(async (args, client) => {
32980
+ const res = await client.post(`/video-factories/${args.slug}/cancel-batch`);
32981
+ return ok({
32982
+ ...asRecord(asRecord(res).data ?? res),
32983
+ slug: args.slug,
32984
+ charged: false,
32985
+ next: "get_status"
32986
+ });
32987
+ }));
32881
32988
  registerTool("get_ai_assists", {
32882
32989
  title: "Get AI assist quota",
32883
32990
  description: "Returns the free daily AI-assist quota {used, limit, bonus, remaining, resets_at, unlock_cost, " + "unlock_batch_size}. AI assists power helper calls (generate_scenario, generate_narration, " + "enhance_prompt, suggest_next_scene, suggest_music_prompt). Default limit is 20/day. When remaining " + "hits 0 those helpers return 429 — either unlock_ai_assists (1 credit → +10) or write the content " + "yourself with set_scenario / set_narration_script / set_segment_prompt (those are free, no quota).",
@@ -32937,14 +33044,67 @@ registerTool("create_editor_draft", {
32937
33044
  next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
32938
33045
  });
32939
33046
  }));
33047
+ function conciseEditorState(value) {
33048
+ const data = asRecord(value);
33049
+ const keys = [
33050
+ "slug",
33051
+ "status",
33052
+ "autopilot_status",
33053
+ "autopilot_current_step",
33054
+ "autopilot_error_step",
33055
+ "autopilot_error_code",
33056
+ "autopilot_error_message",
33057
+ "autopilot_max_credits",
33058
+ "autopilot_credits_spent",
33059
+ "scenario_status",
33060
+ "scenario_apply_status",
33061
+ "scenario_prompt",
33062
+ "batch_generation_status",
33063
+ "narration_status",
33064
+ "narration_script",
33065
+ "narration_stale",
33066
+ "voice_stale",
33067
+ "music_stale",
33068
+ "segments_count",
33069
+ "current_audio",
33070
+ "current_music",
33071
+ "latest_render",
33072
+ "project_spend",
33073
+ "ai_assist_quota"
33074
+ ];
33075
+ const result = {};
33076
+ for (const key of keys) {
33077
+ if (key in data)
33078
+ result[key] = data[key];
33079
+ }
33080
+ result.segments = Array.isArray(data.segments) ? data.segments.map((segment) => ({
33081
+ id: segment.id,
33082
+ position: segment.position,
33083
+ prompt: segment.prompt,
33084
+ status: segment.status,
33085
+ source_type: segment.source_type,
33086
+ use_previous_frame: segment.use_previous_frame,
33087
+ protagonist_presence: segment.protagonist_presence,
33088
+ cast_mode_snapshot: segment.cast_mode_snapshot,
33089
+ video_stale: segment.video_stale,
33090
+ error_code: segment.error_code,
33091
+ error_message: segment.error_message,
33092
+ narration_text: segment.narration_text
33093
+ })) : [];
33094
+ return result;
33095
+ }
32940
33096
  registerTool("get_editor", {
32941
- title: "Get full editor state (review step)",
32942
- description: "Returns the full editor project state: scenario_prompt, segments[] (with prompts + status), " + "narration_script/status, music, latest_render, segments_count, ai_assist_quota. This is the REVIEW " + "step call it after generating the scenario/segments to inspect and decide what to edit.",
32943
- inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33097
+ title: "Get editor state (review step)",
33098
+ description: "Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration, audio, render, spend, and quota.",
33099
+ inputSchema: {
33100
+ slug: exports_external.string().describe("Editor project slug"),
33101
+ response_format: exports_external.enum(["concise", "full"]).default("full")
33102
+ },
32944
33103
  annotations: { title: "Get editor", ...RO }
32945
33104
  }, tool(async (args, client) => {
32946
33105
  const res = await client.get(`/editor/${args.slug}`);
32947
- return ok(asRecord(res).data);
33106
+ const data = asRecord(res).data;
33107
+ return ok(args.response_format === "full" ? data : conciseEditorState(data));
32948
33108
  }));
32949
33109
  registerTool("set_scenario", {
32950
33110
  title: "Set the editor scenario",
@@ -33151,23 +33311,25 @@ registerTool("generate_all_segments", {
33151
33311
  idempotentHint: false
33152
33312
  }
33153
33313
  }, tool(async (args, client, extra) => {
33154
- const res = await client.get(`/editor/${args.slug}`);
33314
+ const overallDeadline = Date.now() + 280000;
33315
+ const res = await client.get(`/editor/${args.slug}`, undefined, overallDeadline);
33155
33316
  const data = asRecord(asRecord(res).data);
33156
33317
  const segments = Array.isArray(data.segments) ? data.segments : [];
33157
- const pending = segments.filter((s) => {
33318
+ const unfinished = segments.filter((s) => {
33319
+ if (s.source_type === "upload")
33320
+ return false;
33158
33321
  const st = s.status ?? s.generation_status;
33159
- return st === undefined || st === "pending" || st === "draft" || st === "failed";
33160
- });
33161
- const overallDeadline = Date.now() + 280000;
33162
- const PER_SEGMENT_MAX_MS = 6 * 60 * 1000;
33322
+ return st !== "completed";
33323
+ }).sort((a, b) => Number(a.position ?? Number.MAX_SAFE_INTEGER) - Number(b.position ?? Number.MAX_SAFE_INTEGER));
33324
+ const SUBMIT_HEADROOM_MS = 61000;
33163
33325
  const generated = [];
33164
33326
  let n = 0;
33165
- for (const seg of pending) {
33327
+ for (const seg of unfinished) {
33166
33328
  const id = seg.id;
33167
33329
  if (id === undefined)
33168
33330
  continue;
33169
33331
  const sid = id;
33170
- const remainingMs = overallDeadline - Date.now();
33332
+ let remainingMs = overallDeadline - Date.now();
33171
33333
  if (remainingMs <= 0) {
33172
33334
  return ok({
33173
33335
  slug: args.slug,
@@ -33176,19 +33338,41 @@ registerTool("generate_all_segments", {
33176
33338
  note: "Wall-clock budget reached — stopped before the next scene. Re-run generate_all_segments to continue (it retries failed/pending scenes and skips completed ones)."
33177
33339
  });
33178
33340
  }
33179
- await reportProgress(extra, ++n, `generating segment ${String(sid)} (${n}/${pending.length})`, pending.length);
33180
- try {
33181
- await client.post(`/editor/${args.slug}/segments/${String(sid)}/generate`, undefined, undefined);
33182
- } catch (e) {
33341
+ await reportProgress(extra, ++n, `generating segment ${String(sid)} (${n}/${unfinished.length})`, unfinished.length);
33342
+ const initialStatus = seg.status ?? seg.generation_status;
33343
+ if (initialStatus !== "processing") {
33344
+ remainingMs = overallDeadline - Date.now();
33345
+ if (remainingMs < SUBMIT_HEADROOM_MS) {
33346
+ return ok({
33347
+ slug: args.slug,
33348
+ generated,
33349
+ timed_out: true,
33350
+ note: "Wall-clock budget is too low to safely start another paid scene. Re-run generate_all_segments to continue; completed scenes are skipped."
33351
+ });
33352
+ }
33353
+ try {
33354
+ await client.post(`/editor/${args.slug}/segments/${String(sid)}/generate`, undefined, undefined);
33355
+ } catch (e) {
33356
+ return ok({
33357
+ slug: args.slug,
33358
+ generated,
33359
+ stopped_at: sid,
33360
+ error: errMessage(e),
33361
+ note: "Stopped on first error — fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried."
33362
+ });
33363
+ }
33364
+ }
33365
+ remainingMs = overallDeadline - Date.now();
33366
+ if (remainingMs <= 0) {
33183
33367
  return ok({
33184
33368
  slug: args.slug,
33185
33369
  generated,
33186
33370
  stopped_at: sid,
33187
- error: errMessage(e),
33188
- note: "Stopped on first error fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried."
33371
+ timed_out: true,
33372
+ note: "The scene was submitted, but the wall-clock budget ended before its status could be checked. Re-run generate_all_segments later to continue safely."
33189
33373
  });
33190
33374
  }
33191
- const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra, Math.min(PER_SEGMENT_MAX_MS, remainingMs), 15000);
33375
+ const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra, overallDeadline, 15000);
33192
33376
  if (seg_result.status === "failed") {
33193
33377
  return ok({
33194
33378
  slug: args.slug,
@@ -33207,7 +33391,34 @@ registerTool("generate_all_segments", {
33207
33391
  note: "A scene is still rendering after the poll budget. Re-run generate_all_segments later to continue (it retries failed/pending, skips completed), or watch it with get_editor."
33208
33392
  });
33209
33393
  }
33210
- generated.push(sid);
33394
+ if (initialStatus !== "processing")
33395
+ generated.push(sid);
33396
+ }
33397
+ if (overallDeadline - Date.now() < 1000) {
33398
+ return ok({
33399
+ slug: args.slug,
33400
+ generated,
33401
+ timed_out: true,
33402
+ note: "Wall-clock budget reached before the final reconciliation read. Re-run generate_all_segments to confirm completion; completed scenes are skipped."
33403
+ });
33404
+ }
33405
+ const finalRes = await client.get(`/editor/${args.slug}`, undefined, overallDeadline);
33406
+ const finalData = asRecord(asRecord(finalRes).data);
33407
+ const finalSegments = Array.isArray(finalData.segments) ? finalData.segments : [];
33408
+ const stillUnfinished = finalSegments.filter((segment) => {
33409
+ if (segment.source_type === "upload")
33410
+ return false;
33411
+ const status = segment.status ?? segment.generation_status;
33412
+ return status !== "completed";
33413
+ });
33414
+ if (stillUnfinished.length > 0) {
33415
+ return ok({
33416
+ slug: args.slug,
33417
+ generated,
33418
+ timed_out: true,
33419
+ in_flight_segment_ids: stillUnfinished.map((segment) => segment.id).filter((id) => id !== undefined),
33420
+ note: "Some scenes are still processing. Re-run generate_all_segments later; it will wait for in-flight scenes and generate only unfinished ones."
33421
+ });
33211
33422
  }
33212
33423
  return ok({
33213
33424
  slug: args.slug,
@@ -33589,7 +33800,7 @@ registerTool("set_closing_image", {
33589
33800
  }));
33590
33801
  registerTool("set_logo", {
33591
33802
  title: "Set a brand logo overlay (0 credits)",
33592
- description: "Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤1 MiB; PNG keeps " + "transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " + "position, duration_seconds.",
33803
+ description: "Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤5 MiB; PNG keeps " + "transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " + "position, duration_seconds.",
33593
33804
  inputSchema: {
33594
33805
  slug: exports_external.string().describe("Editor project slug"),
33595
33806
  file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
@@ -33633,9 +33844,28 @@ registerTool("set_short_product", {
33633
33844
  const res = await client.post(`/shorts/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
33634
33845
  return ok(asRecord(res).data ?? res);
33635
33846
  }));
33847
+ registerTool("set_short_logo", {
33848
+ title: "Set a Short brand logo overlay (0 credits)",
33849
+ description: "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " + "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " + "it appears throughout the footage or only on the end card.",
33850
+ inputSchema: {
33851
+ slug: exports_external.string().describe("Short slug"),
33852
+ file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
33853
+ position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Logo corner (default top-right)"),
33854
+ treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
33855
+ },
33856
+ annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
33857
+ }, tool(async (args, client) => {
33858
+ const { s3_key } = await uploadImageFile(client, `/shorts/${args.slug}/logo/presign`, args.file_path, MAX_SHORT_LOGO_BYTES);
33859
+ const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
33860
+ s3_key,
33861
+ position: args.position ?? "top-right",
33862
+ treatment: args.treatment ?? "throughout"
33863
+ });
33864
+ return ok(asRecord(res).data ?? res);
33865
+ }));
33636
33866
  registerTool("set_short_poster", {
33637
33867
  title: "Set a short's end-card poster image (0 credits)",
33638
- description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
33868
+ description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
33639
33869
  inputSchema: {
33640
33870
  slug: exports_external.string().describe("Short slug (from create_short)"),
33641
33871
  file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)"),