@hubfluencer/mcp 0.10.0 → 0.12.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
@@ -29554,7 +29554,9 @@ function normalizeStatus(kind, slug, data) {
29554
29554
  const staleSegments = segments.filter((segment) => segment.video_stale === true && segment.status === "completed");
29555
29555
  const failedSegments = segments.filter((segment) => segment.status === "failed");
29556
29556
  const batchStatus = d.batch_generation_status ?? null;
29557
- 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 autoRenderHandoffActive = batchStatus === "completed" && d.auto_render_after_batch === true;
29559
+ const batchRetryRequired = batchStatus === "paused_for_retry";
29558
29560
  const batchInsufficientCredits = batchStatus === "paused_insufficient_credits";
29559
29561
  const batchFailed = batchStatus === "failed";
29560
29562
  const autopilotActive = [
@@ -29598,33 +29600,42 @@ function normalizeStatus(kind, slug, data) {
29598
29600
  currentMusic.error_message
29599
29601
  ].find((value) => typeof value === "string" && value.trim().length > 0);
29600
29602
  const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
29601
- const workActive = autopilotActive || batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29603
+ const childWorkActive = batchActive || autoRenderHandoffActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29604
+ const autopilotStartedAt = typeof d.autopilot_started_at === "string" ? Date.parse(d.autopilot_started_at) : Number.NaN;
29605
+ const now = Date.now();
29606
+ const autopilotResumeGraceActive = autopilotActive && Number.isFinite(autopilotStartedAt) && autopilotStartedAt <= now && now - autopilotStartedAt <= 120000;
29607
+ const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
29608
+ const batchRecoveryActive = autopilotFailed && batchActive;
29609
+ const childFailureActionable = !batchRecoveryActive && (!autopilotActive || !childWorkActive && !autopilotResumeGraceActive);
29610
+ const workActive = autopilotActive || childWorkActive;
29602
29611
  const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
29603
29612
  const ready = deliveredArtifactSupersedesOrchestrationFailure;
29604
- const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
29605
- const renderFailed = renderStatus === "failed";
29606
- const failed = autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || failedSegments.length > 0 || audioFailed;
29607
- const terminal = ready || failed || batchInsufficientCredits || !workActive;
29613
+ const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
29614
+ const renderFailed = renderStatus === "failed" && childFailureActionable;
29615
+ const failed = parentFailureActionable && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || childFailureActionable && scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || childFailureActionable && failedSegments.length > 0 || childFailureActionable && audioFailed;
29616
+ const terminal = ready || failed || batchRetryRequired || batchInsufficientCredits || !workActive;
29608
29617
  let stage = autopilot;
29609
29618
  if (ready)
29610
29619
  stage = "completed";
29611
- else if (autopilot === "failed")
29620
+ else if (batchRetryRequired)
29621
+ stage = "batch_retry_required";
29622
+ else if (parentFailureActionable && autopilot === "failed")
29612
29623
  stage = "failed";
29613
- else if (autopilot === "cancelled")
29624
+ else if (parentFailureActionable && autopilot === "cancelled")
29614
29625
  stage = "cancelled";
29615
29626
  else if (renderFailed)
29616
29627
  stage = "render_failed";
29617
- else if (scenarioApplyFailed)
29628
+ else if (childFailureActionable && scenarioApplyFailed)
29618
29629
  stage = "scenario_apply_failed";
29619
- else if (scenarioFailed)
29630
+ else if (childFailureActionable && scenarioFailed)
29620
29631
  stage = "scenario_failed";
29621
- else if (narrationFailed)
29632
+ else if (childFailureActionable && narrationFailed)
29622
29633
  stage = "narration_failed";
29623
- else if (batchFailed)
29634
+ else if (childFailureActionable && batchFailed)
29624
29635
  stage = "batch_failed";
29625
- else if (failedSegments.length > 0)
29636
+ else if (childFailureActionable && failedSegments.length > 0)
29626
29637
  stage = "segment_failed";
29627
- else if (audioFailed)
29638
+ else if (childFailureActionable && audioFailed)
29628
29639
  stage = "audio_failed";
29629
29640
  else if (batchInsufficientCredits)
29630
29641
  stage = "insufficient_credits";
@@ -29632,6 +29643,8 @@ function normalizeStatus(kind, slug, data) {
29632
29643
  stage = "segment_processing";
29633
29644
  else if (batchActive)
29634
29645
  stage = "segment_generation";
29646
+ else if (autoRenderHandoffActive)
29647
+ stage = "rendering";
29635
29648
  else if (renderActive)
29636
29649
  stage = "rendering";
29637
29650
  else if (scenarioActive)
@@ -29643,6 +29656,7 @@ function normalizeStatus(kind, slug, data) {
29643
29656
  else if (!workActive && stale)
29644
29657
  stage = "editing_required";
29645
29658
  const segmentError = failedSegments.map((segment) => segment.error_message).find((value) => typeof value === "string" && value.length > 0);
29659
+ 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;
29646
29660
  return {
29647
29661
  kind,
29648
29662
  slug,
@@ -29650,7 +29664,7 @@ function normalizeStatus(kind, slug, data) {
29650
29664
  terminal,
29651
29665
  ready,
29652
29666
  video_url: videoUrl,
29653
- 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),
29667
+ 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 invoke the generation/render path again; retry_editor_batch only applies to paused_for_retry." : null),
29654
29668
  stale,
29655
29669
  active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29656
29670
  stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
@@ -29683,10 +29697,10 @@ function makeVideoAutopilotKey(slug, a, startState) {
29683
29697
  return idemKey("autopilot", slug, a.prompt, a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", startState);
29684
29698
  }
29685
29699
  function editorAdCreateKey(a) {
29686
- return idemKey("create-editor", a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.cast_mode ?? "");
29700
+ return idemKey("create-editor", a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds));
29687
29701
  }
29688
29702
  function editorAdAutopilotKey(slug, a) {
29689
- return idemKey("autopilot", slug, a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.cast_mode ?? "", a.max_credits === undefined ? "" : String(a.max_credits));
29703
+ return idemKey("autopilot", slug, a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.max_credits === undefined ? "" : String(a.max_credits));
29690
29704
  }
29691
29705
  function sliderCreateKey(a) {
29692
29706
  return idemKey("create-slider", a.prompt, a.mode ?? "", a.template ?? "", a.slide_count === undefined ? "" : String(a.slide_count), a.aspect_ratio ?? "", a.accent_color ?? "", a.text_position ?? "");
@@ -30243,7 +30257,7 @@ function readStoredCredentials() {
30243
30257
  // package.json
30244
30258
  var package_default = {
30245
30259
  name: "@hubfluencer/mcp",
30246
- version: "0.10.0",
30260
+ version: "0.12.0",
30247
30261
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30248
30262
  license: "MIT",
30249
30263
  author: "Monocursive <contact@monocursive.com>",
@@ -30646,6 +30660,9 @@ var createEditorOutput = exports_external.object({
30646
30660
  estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30647
30661
  available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30648
30662
  affordable: exports_external.boolean().optional(),
30663
+ launched: exports_external.boolean().optional(),
30664
+ authorized_credits: exports_external.number().optional(),
30665
+ credits_spent: exports_external.number().optional(),
30649
30666
  charged: exports_external.boolean().optional(),
30650
30667
  note: exports_external.string().optional(),
30651
30668
  next: exports_external.string().optional()
@@ -30662,6 +30679,9 @@ var makeVideoOutput = exports_external.object({
30662
30679
  estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30663
30680
  available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
30664
30681
  affordable: exports_external.boolean().optional(),
30682
+ launched: exports_external.boolean().optional(),
30683
+ authorized_credits: exports_external.number().optional(),
30684
+ credits_spent: exports_external.number().optional(),
30665
30685
  charged: exports_external.boolean().optional(),
30666
30686
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30667
30687
  timed_out: exports_external.boolean().optional(),
@@ -31122,7 +31142,7 @@ var ALLOW_LOOPBACK_FETCH = /^http:\/\/(localhost|127\.0\.0\.1|\[?::1\]?)/.test(p
31122
31142
  var MAX_VIDEO_BYTES = 500000000;
31123
31143
  var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
31124
31144
  var MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
31125
- var MAX_LOGO_BYTES = 1 * 1024 * 1024;
31145
+ var MAX_LOGO_BYTES = 5 * 1024 * 1024;
31126
31146
  var MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
31127
31147
  var MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
31128
31148
  var MULTIPART_THRESHOLD = 50 * 1024 * 1024;
@@ -31549,9 +31569,9 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31549
31569
  const params = new URLSearchParams;
31550
31570
  if (overrides.language)
31551
31571
  params.set("language", overrides.language);
31552
- if (overrides.product_subject)
31572
+ if (overrides.product_subject !== undefined)
31553
31573
  params.set("product_subject", overrides.product_subject);
31554
- if (overrides.product_prompt)
31574
+ if (overrides.product_prompt !== undefined)
31555
31575
  params.set("product_prompt", overrides.product_prompt);
31556
31576
  if (overrides.restart)
31557
31577
  params.set("restart", "true");
@@ -31821,7 +31841,7 @@ CREDITS (spent server-side; each tool prices before charging — see its descrip
31821
31841
  - 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.
31822
31842
 
31823
31843
  WAITING & RESUMING
31824
- - 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.
31844
+ - 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.
31825
31845
  - 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.
31826
31846
 
31827
31847
  LOCAL FILES (sandboxed)
@@ -31841,8 +31861,7 @@ async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
31841
31861
  }
31842
31862
  return status;
31843
31863
  }
31844
- async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, intervalMs) {
31845
- const deadline = Date.now() + budgetMs;
31864
+ async function pollSegmentToTerminal(client, slug, segmentId, extra, deadline, intervalMs) {
31846
31865
  const sid = String(segmentId);
31847
31866
  const read = async () => {
31848
31867
  const res = await client.get(`/editor/${slug}`, undefined, deadline);
@@ -31991,6 +32010,7 @@ registerTool("make_video", {
31991
32010
  const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
31992
32011
  let estimated_credits = null;
31993
32012
  let available_credits = null;
32013
+ let authorizedCredits;
31994
32014
  try {
31995
32015
  const cost = asRecord(asRecord(await client.get(costPath)).data);
31996
32016
  estimated_credits = typeof cost.total === "number" ? cost.total : null;
@@ -32025,6 +32045,7 @@ registerTool("make_video", {
32025
32045
  if (approvedCap === null) {
32026
32046
  throw new Error("Editor autopilot launch requires max_credits or a live quote");
32027
32047
  }
32048
+ authorizedCredits = approvedCap;
32028
32049
  await client.post(`/editor/${slug}/autopilot`, { max_credits: approvedCap }, makeVideoAutopilotKey(slug, args, startState));
32029
32050
  }
32030
32051
  await reportProgress(extra, 0, `started ${kind} ${slug}`);
@@ -32033,12 +32054,29 @@ registerTool("make_video", {
32033
32054
  if (status.ready && status.video_url && args.save_path) {
32034
32055
  saved_to = (await downloadTo(status.video_url, args.save_path)).saved_to;
32035
32056
  }
32057
+ let editorCreditsSpent;
32058
+ if (kind === "editor") {
32059
+ try {
32060
+ const current = asRecord(asRecord(await client.get(`/editor/${slug}`, undefined, Date.now() + 5000)).data);
32061
+ if (typeof current.autopilot_credits_spent === "number") {
32062
+ editorCreditsSpent = current.autopilot_credits_spent;
32063
+ }
32064
+ } catch {}
32065
+ }
32066
+ const billing = kind === "short" ? { charged: true } : {
32067
+ launched: true,
32068
+ authorized_credits: authorizedCredits,
32069
+ ...editorCreditsSpent === undefined ? {} : {
32070
+ credits_spent: editorCreditsSpent,
32071
+ charged: editorCreditsSpent > 0
32072
+ }
32073
+ };
32036
32074
  const result = {
32037
32075
  ...status,
32038
32076
  kind_inferred: requestedKind === undefined,
32039
32077
  estimated_credits,
32040
32078
  available_credits,
32041
- charged: true,
32079
+ ...billing,
32042
32080
  saved_to,
32043
32081
  timed_out: !status.terminal
32044
32082
  };
@@ -32046,7 +32084,7 @@ registerTool("make_video", {
32046
32084
  const links = status.ready && status.video_url ? [mp4Link(status.video_url, slug)] : [];
32047
32085
  return ok({
32048
32086
  ...result,
32049
- 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}).`
32087
+ 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}).`
32050
32088
  }, links);
32051
32089
  }));
32052
32090
  registerTool("get_credits", {
@@ -32721,7 +32759,7 @@ registerTool("edit_slider_slide", {
32721
32759
  }));
32722
32760
  registerTool("create_editor_ad", {
32723
32761
  title: "Create, configure, and price a multi-scene editor ad",
32724
- description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "Supplying assets here is safer than adding them after autopilot because the scenario and scenes are grounded " + "in the real product from the start. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " + "For a mascot or hero product that must stay visually identical with no human presenter, set " + "cast_mode:'product_only' together with product_image_path. Presenter-led casting still belongs on " + "create_editor_draft because it requires a separately configured presenter.",
32762
+ description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " + "in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " + "headline/subheadline instead.)",
32725
32763
  inputSchema: {
32726
32764
  product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
32727
32765
  product_subject: exports_external.string().max(300).optional().describe('The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green ' + 'bottle with a gold label"). Distinct from the freeform brief: hard-grounds the AI scenario + narration ' + "so it cannot invent an unrelated product. Strongly recommended for ads."),
@@ -32732,14 +32770,13 @@ registerTool("create_editor_ad", {
32732
32770
  theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
32733
32771
  voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
32734
32772
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32735
- product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot."),
32736
- product_description: exports_external.string().max(500).optional().describe("Exact product/character description used with product_image_path."),
32773
+ product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) used as the product-identity reference for every generated Editor scene and linked as the exact closing image unless one was already authored."),
32774
+ product_description: exports_external.string().max(500).optional().describe("Product description used to ground the scenario and narration when product_image_path is supplied."),
32737
32775
  closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
32738
- logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤1 MiB) to overlay."),
32776
+ logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
32739
32777
  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.'),
32740
32778
  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.'),
32741
32779
  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."),
32742
- cast_mode: exports_external.literal("product_only").optional().describe("Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path."),
32743
32780
  max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to create/configure/price only; no credits are charged.")
32744
32781
  },
32745
32782
  outputSchema: createEditorOutput,
@@ -32756,9 +32793,6 @@ registerTool("create_editor_ad", {
32756
32793
  const promptErr = validateProductPrompt(args.product_prompt);
32757
32794
  if (promptErr)
32758
32795
  return fail(promptErr);
32759
- if (args.cast_mode === "product_only" && !args.product_image_path) {
32760
- return fail("cast_mode product_only requires product_image_path so identity can be locked before generation.");
32761
- }
32762
32796
  if ((args.logo_treatment !== undefined || args.logo_position !== undefined || args.logo_duration_seconds !== undefined) && !args.logo_path) {
32763
32797
  return fail("logo_treatment/logo_position/logo_duration_seconds require logo_path (the logo image they style). Provide logo_path or drop these settings.");
32764
32798
  }
@@ -32786,8 +32820,7 @@ registerTool("create_editor_ad", {
32786
32820
  visual_language: args.visual_language,
32787
32821
  theme: args.theme,
32788
32822
  voice_id: args.voice_id,
32789
- export_aspect_ratio: args.export_aspect_ratio,
32790
- cast_mode: args.cast_mode
32823
+ export_aspect_ratio: args.export_aspect_ratio
32791
32824
  }, editorAdCreateKey({
32792
32825
  ...args,
32793
32826
  product_image_identity: productImage ? preparedUploadIdentity(productImage) : undefined,
@@ -32846,7 +32879,10 @@ registerTool("create_editor_ad", {
32846
32879
  kind: "editor",
32847
32880
  status,
32848
32881
  ...quote,
32849
- charged: true,
32882
+ launched: true,
32883
+ authorized_credits: args.max_credits,
32884
+ credits_spent: 0,
32885
+ charged: false,
32850
32886
  next: "wait_for_completion"
32851
32887
  });
32852
32888
  } finally {
@@ -32855,12 +32891,12 @@ registerTool("create_editor_ad", {
32855
32891
  }));
32856
32892
  registerTool("start_autopilot", {
32857
32893
  title: "Start autopilot on an existing editor draft",
32858
- 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" }).',
32894
+ 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" }).',
32859
32895
  inputSchema: {
32860
32896
  slug: exports_external.string().describe("Editor project slug"),
32861
32897
  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."),
32862
- product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product."),
32863
- product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch."),
32898
+ product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product. Omit to keep the stored subject; send blank to clear it."),
32899
+ product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it."),
32864
32900
  max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
32865
32901
  restart: exports_external.boolean().optional().describe("Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.")
32866
32902
  },
@@ -32901,10 +32937,75 @@ registerTool("start_autopilot", {
32901
32937
  kind: "editor",
32902
32938
  status,
32903
32939
  ...quote,
32904
- charged: true,
32940
+ launched: true,
32941
+ authorized_credits: args.max_credits,
32942
+ credits_spent: 0,
32943
+ charged: false,
32944
+ next: "wait_for_completion"
32945
+ });
32946
+ }));
32947
+ registerTool("retry_editor_batch", {
32948
+ title: "Retry a paused editor batch",
32949
+ 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.",
32950
+ inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32951
+ annotations: {
32952
+ title: "Retry editor batch",
32953
+ ...WRITE,
32954
+ idempotentHint: false
32955
+ }
32956
+ }, tool(async (args, client) => {
32957
+ const command = await observedEditorBatchCommand(client, args.slug, [
32958
+ "paused_for_retry"
32959
+ ]);
32960
+ const res = await client.post(`/video-factories/${args.slug}/retry-batch-segment`, command);
32961
+ return ok({
32962
+ ...asRecord(asRecord(res).data ?? res),
32963
+ slug: args.slug,
32964
+ charged: false,
32965
+ 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.",
32905
32966
  next: "wait_for_completion"
32906
32967
  });
32907
32968
  }));
32969
+ registerTool("cancel_editor_batch", {
32970
+ title: "Cancel an editor batch",
32971
+ description: "Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " + "Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " + "batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " + "0 additional credits. Scope: video:generate.",
32972
+ inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32973
+ annotations: {
32974
+ title: "Cancel editor batch",
32975
+ readOnlyHint: false,
32976
+ destructiveHint: true,
32977
+ openWorldHint: true,
32978
+ idempotentHint: false
32979
+ }
32980
+ }, tool(async (args, client) => {
32981
+ const command = await observedEditorBatchCommand(client, args.slug, [
32982
+ "preparing",
32983
+ "paused_for_retry"
32984
+ ]);
32985
+ const res = await client.post(`/video-factories/${args.slug}/cancel-batch`, command);
32986
+ return ok({
32987
+ ...asRecord(asRecord(res).data ?? res),
32988
+ slug: args.slug,
32989
+ charged: false,
32990
+ next: "get_status"
32991
+ });
32992
+ }));
32993
+ async function observedEditorBatchCommand(client, slug, allowedStatuses) {
32994
+ const response = await client.get(`/editor/${slug}`);
32995
+ const state = asRecord(asRecord(response).data);
32996
+ const status = state.batch_generation_status;
32997
+ if (typeof status !== "string" || !allowedStatuses.includes(status)) {
32998
+ throw new Error(`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`);
32999
+ }
33000
+ const runId = state.batch_generation_run_id;
33001
+ if (runId !== null && (typeof runId !== "string" || runId.trim().length === 0)) {
33002
+ throw new Error("Editor state did not include a valid batch_generation_run_id; refresh status before retrying.");
33003
+ }
33004
+ return {
33005
+ expected_batch_run_id: runId,
33006
+ expected_batch_status: status
33007
+ };
33008
+ }
32908
33009
  registerTool("get_ai_assists", {
32909
33010
  title: "Get AI assist quota",
32910
33011
  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).",
@@ -32931,7 +33032,6 @@ registerTool("create_editor_draft", {
32931
33032
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32932
33033
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
32933
33034
  theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
32934
- cast_mode: exports_external.enum(["product_only", "presenter_led"]).optional().describe("Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " + "product image (set_product) before generation and generates every scene from persistent product " + "reference images — the best product fidelity. presenter_led = one locked presenter appears in " + "every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " + "— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " + "behavior."),
32935
33035
  voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).optional().describe("Preferred narration voice id (see list_voices); ≤64 chars, [A-Za-z0-9_-] only"),
32936
33036
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32937
33037
  project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("social_ad for promotional/ad content (product focus + CTA), creative_story for brand " + "narratives (default).")
@@ -32950,28 +33050,80 @@ registerTool("create_editor_draft", {
32950
33050
  creative_format: args.creative_format,
32951
33051
  visual_language: args.visual_language,
32952
33052
  theme: args.theme,
32953
- cast_mode: args.cast_mode,
32954
33053
  voice_id: args.voice_id,
32955
33054
  export_aspect_ratio: args.export_aspect_ratio,
32956
33055
  project_intent: args.project_intent
32957
33056
  };
32958
33057
  if (prompt)
32959
33058
  body.product_prompt = prompt;
32960
- const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? "", args.cast_mode ?? ""));
33059
+ const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? ""));
32961
33060
  return ok({
32962
33061
  slug: created.data.slug,
32963
33062
  kind: "editor",
32964
33063
  next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
32965
33064
  });
32966
33065
  }));
33066
+ function conciseEditorState(value) {
33067
+ const data = asRecord(value);
33068
+ const keys = [
33069
+ "slug",
33070
+ "status",
33071
+ "autopilot_status",
33072
+ "autopilot_current_step",
33073
+ "autopilot_error_step",
33074
+ "autopilot_error_code",
33075
+ "autopilot_error_message",
33076
+ "autopilot_max_credits",
33077
+ "autopilot_credits_spent",
33078
+ "scenario_status",
33079
+ "scenario_apply_status",
33080
+ "scenario_prompt",
33081
+ "batch_generation_status",
33082
+ "batch_generation_run_id",
33083
+ "auto_render_after_batch",
33084
+ "narration_status",
33085
+ "narration_script",
33086
+ "narration_stale",
33087
+ "voice_stale",
33088
+ "music_stale",
33089
+ "segments_count",
33090
+ "current_audio",
33091
+ "current_music",
33092
+ "latest_render",
33093
+ "project_spend",
33094
+ "ai_assist_quota"
33095
+ ];
33096
+ const result = {};
33097
+ for (const key of keys) {
33098
+ if (key in data)
33099
+ result[key] = data[key];
33100
+ }
33101
+ result.segments = Array.isArray(data.segments) ? data.segments.map((segment) => ({
33102
+ id: segment.id,
33103
+ position: segment.position,
33104
+ prompt: segment.prompt,
33105
+ status: segment.status,
33106
+ source_type: segment.source_type,
33107
+ use_previous_frame: segment.use_previous_frame,
33108
+ video_stale: segment.video_stale,
33109
+ error_code: segment.error_code,
33110
+ error_message: segment.error_message,
33111
+ narration_text: segment.narration_text
33112
+ })) : [];
33113
+ return result;
33114
+ }
32967
33115
  registerTool("get_editor", {
32968
- title: "Get full editor state (review step)",
32969
- 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.",
32970
- inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33116
+ title: "Get editor state (review step)",
33117
+ 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_status, current_audio, current_music, latest_render, batch_generation_status, spend, and quota.",
33118
+ inputSchema: {
33119
+ slug: exports_external.string().describe("Editor project slug"),
33120
+ response_format: exports_external.enum(["concise", "full"]).default("full")
33121
+ },
32971
33122
  annotations: { title: "Get editor", ...RO }
32972
33123
  }, tool(async (args, client) => {
32973
33124
  const res = await client.get(`/editor/${args.slug}`);
32974
- return ok(asRecord(res).data);
33125
+ const data = asRecord(res).data;
33126
+ return ok(args.response_format === "full" ? data : conciseEditorState(data));
32975
33127
  }));
32976
33128
  registerTool("set_scenario", {
32977
33129
  title: "Set the editor scenario",
@@ -32991,8 +33143,8 @@ registerTool("generate_scenario", {
32991
33143
  inputSchema: {
32992
33144
  slug: exports_external.string().describe("Editor project slug"),
32993
33145
  segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
32994
- creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
32995
- visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored."),
33146
+ creative_format: exports_external.union([exports_external.enum(CREATIVE_FORMATS), exports_external.literal("")]).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
33147
+ visual_language: exports_external.union([exports_external.enum(VISUAL_LANGUAGES), exports_external.literal("")]).optional().describe("Persist the visual style / render look before generating. Omit to leave it unchanged; pass an empty string to clear. When set it drives the look and a theme of 'none' is ignored."),
32996
33148
  theme: exports_external.string().optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
32997
33149
  auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
32998
33150
  },
@@ -33178,23 +33330,25 @@ registerTool("generate_all_segments", {
33178
33330
  idempotentHint: false
33179
33331
  }
33180
33332
  }, tool(async (args, client, extra) => {
33181
- const res = await client.get(`/editor/${args.slug}`);
33333
+ const overallDeadline = Date.now() + 280000;
33334
+ const res = await client.get(`/editor/${args.slug}`, undefined, overallDeadline);
33182
33335
  const data = asRecord(asRecord(res).data);
33183
33336
  const segments = Array.isArray(data.segments) ? data.segments : [];
33184
- const pending = segments.filter((s) => {
33337
+ const unfinished = segments.filter((s) => {
33338
+ if (s.source_type === "upload")
33339
+ return false;
33185
33340
  const st = s.status ?? s.generation_status;
33186
- return st === undefined || st === "pending" || st === "draft" || st === "failed";
33187
- });
33188
- const overallDeadline = Date.now() + 280000;
33189
- const PER_SEGMENT_MAX_MS = 6 * 60 * 1000;
33341
+ return st !== "completed";
33342
+ }).sort((a, b) => Number(a.position ?? Number.MAX_SAFE_INTEGER) - Number(b.position ?? Number.MAX_SAFE_INTEGER));
33343
+ const SUBMIT_HEADROOM_MS = 61000;
33190
33344
  const generated = [];
33191
33345
  let n = 0;
33192
- for (const seg of pending) {
33346
+ for (const seg of unfinished) {
33193
33347
  const id = seg.id;
33194
33348
  if (id === undefined)
33195
33349
  continue;
33196
33350
  const sid = id;
33197
- const remainingMs = overallDeadline - Date.now();
33351
+ let remainingMs = overallDeadline - Date.now();
33198
33352
  if (remainingMs <= 0) {
33199
33353
  return ok({
33200
33354
  slug: args.slug,
@@ -33203,19 +33357,41 @@ registerTool("generate_all_segments", {
33203
33357
  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)."
33204
33358
  });
33205
33359
  }
33206
- await reportProgress(extra, ++n, `generating segment ${String(sid)} (${n}/${pending.length})`, pending.length);
33207
- try {
33208
- await client.post(`/editor/${args.slug}/segments/${String(sid)}/generate`, undefined, undefined);
33209
- } catch (e) {
33360
+ await reportProgress(extra, ++n, `generating segment ${String(sid)} (${n}/${unfinished.length})`, unfinished.length);
33361
+ const initialStatus = seg.status ?? seg.generation_status;
33362
+ if (initialStatus !== "processing") {
33363
+ remainingMs = overallDeadline - Date.now();
33364
+ if (remainingMs < SUBMIT_HEADROOM_MS) {
33365
+ return ok({
33366
+ slug: args.slug,
33367
+ generated,
33368
+ timed_out: true,
33369
+ note: "Wall-clock budget is too low to safely start another paid scene. Re-run generate_all_segments to continue; completed scenes are skipped."
33370
+ });
33371
+ }
33372
+ try {
33373
+ await client.post(`/editor/${args.slug}/segments/${String(sid)}/generate`, undefined, undefined);
33374
+ } catch (e) {
33375
+ return ok({
33376
+ slug: args.slug,
33377
+ generated,
33378
+ stopped_at: sid,
33379
+ error: errMessage(e),
33380
+ note: "Stopped on first error — fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried."
33381
+ });
33382
+ }
33383
+ }
33384
+ remainingMs = overallDeadline - Date.now();
33385
+ if (remainingMs <= 0) {
33210
33386
  return ok({
33211
33387
  slug: args.slug,
33212
33388
  generated,
33213
33389
  stopped_at: sid,
33214
- error: errMessage(e),
33215
- note: "Stopped on first error fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried."
33390
+ timed_out: true,
33391
+ 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."
33216
33392
  });
33217
33393
  }
33218
- const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra, Math.min(PER_SEGMENT_MAX_MS, remainingMs), 15000);
33394
+ const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra, overallDeadline, 15000);
33219
33395
  if (seg_result.status === "failed") {
33220
33396
  return ok({
33221
33397
  slug: args.slug,
@@ -33234,7 +33410,34 @@ registerTool("generate_all_segments", {
33234
33410
  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."
33235
33411
  });
33236
33412
  }
33237
- generated.push(sid);
33413
+ if (initialStatus !== "processing")
33414
+ generated.push(sid);
33415
+ }
33416
+ if (overallDeadline - Date.now() < 1000) {
33417
+ return ok({
33418
+ slug: args.slug,
33419
+ generated,
33420
+ timed_out: true,
33421
+ note: "Wall-clock budget reached before the final reconciliation read. Re-run generate_all_segments to confirm completion; completed scenes are skipped."
33422
+ });
33423
+ }
33424
+ const finalRes = await client.get(`/editor/${args.slug}`, undefined, overallDeadline);
33425
+ const finalData = asRecord(asRecord(finalRes).data);
33426
+ const finalSegments = Array.isArray(finalData.segments) ? finalData.segments : [];
33427
+ const stillUnfinished = finalSegments.filter((segment) => {
33428
+ if (segment.source_type === "upload")
33429
+ return false;
33430
+ const status = segment.status ?? segment.generation_status;
33431
+ return status !== "completed";
33432
+ });
33433
+ if (stillUnfinished.length > 0) {
33434
+ return ok({
33435
+ slug: args.slug,
33436
+ generated,
33437
+ timed_out: true,
33438
+ in_flight_segment_ids: stillUnfinished.map((segment) => segment.id).filter((id) => id !== undefined),
33439
+ note: "Some scenes are still processing. Re-run generate_all_segments later; it will wait for in-flight scenes and generate only unfinished ones."
33440
+ });
33238
33441
  }
33239
33442
  return ok({
33240
33443
  slug: args.slug,
@@ -33245,10 +33448,10 @@ registerTool("generate_all_segments", {
33245
33448
  }));
33246
33449
  registerTool("set_narration_script", {
33247
33450
  title: "Set the narration script",
33248
- description: "Writes your own narration script (1–12000 chars) instead of generating one. Free — no credits, no AI " + "assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
33451
+ description: "Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " + "assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
33249
33452
  inputSchema: {
33250
33453
  slug: exports_external.string().describe("Editor project slug"),
33251
- script: exports_external.string().min(1).max(12000).describe("Narration / voiceover text (1–12000 chars)")
33454
+ script: exports_external.string().min(1).max(1e4).describe("Narration / voiceover text (1–10000 chars)")
33252
33455
  },
33253
33456
  annotations: { title: "Set narration", ...WRITE, idempotentHint: true }
33254
33457
  }, tool(async (args, client) => {
@@ -33259,7 +33462,7 @@ registerTool("set_narration_script", {
33259
33462
  }));
33260
33463
  registerTool("generate_narration", {
33261
33464
  title: "Generate the narration (AI assist)",
33262
- description: "Generates the narration script with AI from the timeline. CONSUMES 1 AI ASSIST (free daily quota) — " + "it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " + "retried once) or write the script yourself with set_narration_script.",
33465
+ description: "Generates one coherent narration script with AI from the complete timeline. Every current scene must be completed with a known positive duration, and at least one scene must not keep original upload audio. CONSUMES 1 AI ASSIST (free daily quota) only when a fresh job is queued; an exact retry is deduplicated without consuming another assist — " + "it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " + "retried once) or write the script yourself with set_narration_script.",
33263
33466
  inputSchema: {
33264
33467
  slug: exports_external.string().describe("Editor project slug"),
33265
33468
  auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
@@ -33275,7 +33478,7 @@ registerTool("generate_narration", {
33275
33478
  }));
33276
33479
  registerTool("generate_voice", {
33277
33480
  title: "Generate the voiceover (3 credits)",
33278
- description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + "voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
33481
+ description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + " All current scenes must be completed with known durations first. voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
33279
33482
  inputSchema: {
33280
33483
  slug: exports_external.string().describe("Editor project slug"),
33281
33484
  voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).describe("Narration voice id from list_voices (required)")
@@ -33387,7 +33590,7 @@ registerTool("suggest_music_prompt", {
33387
33590
  }));
33388
33591
  registerTool("list_renders", {
33389
33592
  title: "List renders for an editor project",
33390
- description: "Lists every render version for an editor project: {id, version, status, video_url, duration_seconds, " + "error_message, inserted_at}. Use it to find a FAILED render to retry, or to recover a finished video_url " + "(presigned, ~24h). Read-only.",
33593
+ description: "Lists every render version for an editor project: {id, version, status, video_url, thumbnail_url, " + "duration_seconds, error_message, retryable, combination_hash, is_stale, inserted_at}. Use it to find a " + "FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " + "processing | completed | failed; is_stale null means unknown, not current. Read-only.",
33391
33594
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33392
33595
  annotations: { title: "List renders", ...RO }
33393
33596
  }, tool(async (args, client) => {
@@ -33560,7 +33763,7 @@ registerTool("use_asset_for_segment", {
33560
33763
  }));
33561
33764
  registerTool("set_product", {
33562
33765
  title: "Attach a product image (from a local file)",
33563
- description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. The FIRST product added " + "defaults every pending AI scene to feature it 'throughout' (change with set_product_placement). Optional " + "description (≤500 chars) guides how it's woven into scenes.",
33766
+ description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " + "in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " + "is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary.",
33564
33767
  inputSchema: {
33565
33768
  slug: exports_external.string().describe("Editor project slug"),
33566
33769
  file_path: exports_external.string().describe("Local product image path (.jpg/.jpeg/.png)"),
@@ -33572,22 +33775,6 @@ registerTool("set_product", {
33572
33775
  const res = await client.post(`/editor/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
33573
33776
  return ok(asRecord(res).data ?? res);
33574
33777
  }));
33575
- registerTool("set_product_placement", {
33576
- title: "Set default product placement on pending scenes (0 credits)",
33577
- description: "Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " + "or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " + "set_product. (There is no 'none' — to remove the product, that's a separate action.)",
33578
- inputSchema: {
33579
- slug: exports_external.string().describe("Editor project slug"),
33580
- mode: exports_external.enum(["throughout", "end"]).describe("'throughout' or 'end'")
33581
- },
33582
- annotations: {
33583
- title: "Set product placement",
33584
- ...WRITE,
33585
- idempotentHint: true
33586
- }
33587
- }, tool(async (args, client) => {
33588
- const res = await client.post(`/editor/${args.slug}/product/apply-default-placement`, { placement: args.mode });
33589
- return ok(asRecord(res).data ?? res);
33590
- }));
33591
33778
  registerTool("set_closing_image", {
33592
33779
  title: "Set the closing-card image (0 credits)",
33593
33780
  description: "Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20 MiB, confined " + "to HUBFLUENCER_INPUT_DIR/cwd) OR reuse the project's product image (from_product:true — a 0-credit server-side " + "copy, no upload). If a closing image already exists, pass overwrite:true to replace it (else " + "editor_closing_image_exists).",
@@ -33616,7 +33803,7 @@ registerTool("set_closing_image", {
33616
33803
  }));
33617
33804
  registerTool("set_logo", {
33618
33805
  title: "Set a brand logo overlay (0 credits)",
33619
- 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.",
33806
+ 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.",
33620
33807
  inputSchema: {
33621
33808
  slug: exports_external.string().describe("Editor project slug"),
33622
33809
  file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),