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