@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/README.md +36 -12
- package/dist/index.js +276 -89
- package/package.json +1 -1
- package/src/core.ts +103 -44
- package/src/index.ts +381 -133
- package/src/output-schemas.ts +6 -0
- package/src/uploads.ts +1 -1
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"
|
|
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
|
|
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
|
|
29605
|
-
const renderFailed = renderStatus === "failed";
|
|
29606
|
-
const failed =
|
|
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 (
|
|
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 :
|
|
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)
|
|
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.
|
|
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.
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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. " + "
|
|
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)
|
|
32736
|
-
product_description: exports_external.string().max(500).optional().describe("
|
|
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, ≤
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? ""
|
|
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
|
|
32969
|
-
description: "Returns the full editor
|
|
32970
|
-
inputSchema: {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
33187
|
-
});
|
|
33188
|
-
const
|
|
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
|
|
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
|
-
|
|
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}/${
|
|
33207
|
-
|
|
33208
|
-
|
|
33209
|
-
|
|
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
|
-
|
|
33215
|
-
note: "
|
|
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,
|
|
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
|
-
|
|
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–
|
|
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(
|
|
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
|
|
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,
|
|
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.
|
|
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 (≤
|
|
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)"),
|