@hubfluencer/mcp 0.10.0 → 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 +12 -6
- package/dist/index.js +231 -47
- package/package.json +1 -1
- package/src/core.ts +91 -37
- package/src/index.ts +299 -53
- package/src/output-schemas.ts +6 -0
- package/src/uploads.ts +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ claude mcp add hubfluencer --env HUBFLUENCER_API_TOKEN=YOUR_TOKEN -- npx -y @hub
|
|
|
68
68
|
|
|
69
69
|
## Tools
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
95 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
|
|
72
72
|
below are the ground truth. **"$0" means it spends no video credits**; planning/AI-helper tools
|
|
73
73
|
instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_assists`: 1 credit
|
|
74
74
|
→ +10). Reads need the `video:read` scope; writes need `video:generate`. Nothing here needs
|
|
@@ -86,6 +86,8 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
86
86
|
| `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
|
|
87
87
|
| `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Omit `max_credits` to stop free; provide an approved cap to launch. For a mascot/hero product that must remain identical with no humans, pass `cast_mode:"product_only"` plus `product_image_path`. | **0** until capped launch |
|
|
88
88
|
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. On a completed project, pass `restart:true` to quote and build a fresh creative run; omit it to resume pending edits. | **0** quote; credits on capped launch |
|
|
89
|
+
| `retry_editor_batch` | Recover `batch_retry_required` by atomically re-queueing the failed scene and resuming its prepaid batch. If the parent Autopilot run is terminal, quote/relaunch it separately afterward. | **0 additional** |
|
|
90
|
+
| `cancel_editor_batch` | Cancel an awaiting/running/paused editor batch. Destructive after scene dispatch: prepaid credits are not refunded (an awaiting-preview batch is refunded server-side). | **0 additional** |
|
|
89
91
|
| `create_slider` | Create an image-carousel draft: one prompt → N still slides + a ready-to-post caption + hashtags. `mode` `creative` (story) or `ad_driven` (facts). | **0** |
|
|
90
92
|
| `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
|
|
91
93
|
| `get_slider` | Read a carousel's status and, when completed, the per-slide image URLs + caption + hashtags. | **0** |
|
|
@@ -98,7 +100,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
98
100
|
| Tool | What it does | Cost |
|
|
99
101
|
|---|---|---|
|
|
100
102
|
| `create_editor_draft` | Editor project, **no** autopilot. Accepts `creative_format` / `visual_language` / `theme` / `cast_mode` (`product_only` needs `set_product` before generation; `presenter_led` needs a configured presenter). | **0** |
|
|
101
|
-
| `get_editor` | Full project state
|
|
103
|
+
| `get_editor` | Full project state by default for compatibility; pass `response_format:"concise"` for a smaller review projection. | **0** |
|
|
102
104
|
| `set_scenario` / `generate_scenario` | Write your own scenario (free) **or** AI-draft it. `generate_scenario` can also persist `creative_format` / `visual_language` / `theme`. | free / 1 assist |
|
|
103
105
|
| `apply_scenario` | Compile the **ready** scenario into per-scene prompts on the timeline (`segments_count` 0 or 3..10; omitted uses the generated scenario count, then 5; AI scenes replaced, uploads preserved). Polls until applied (~2 min budget). | **0** |
|
|
104
106
|
| `set_scene_count` | Grow/shrink to N scenes (1–20); deletes only trailing un-generated scenes. Each AI scene is a fixed 8s. | **0** |
|
|
@@ -120,7 +122,7 @@ Reads are confined to `HUBFLUENCER_INPUT_DIR`. All $0.
|
|
|
120
122
|
|
|
121
123
|
Size units are exact: videos are capped at **500 MB decimal (500,000,000 bytes)**; product
|
|
122
124
|
images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
123
|
-
(20,971,520 bytes)**; logos at **
|
|
125
|
+
(20,971,520 bytes)**; logos at **5 MiB (5,242,880 bytes)**. Editor video multipart starts at
|
|
124
126
|
**50 MiB (52,428,800 bytes)**.
|
|
125
127
|
|
|
126
128
|
| Tool | What it does |
|
|
@@ -131,7 +133,7 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
131
133
|
| `set_product` | Upload an editor product image (JPEG/PNG, ≤8 MiB) and attach it as the project product. |
|
|
132
134
|
| `set_product_placement` | Set where the product appears on pending AI scenes: `throughout` or `end`. |
|
|
133
135
|
| `set_closing_image` | Set the ~2s end-card image (JPEG/PNG, ≤20 MiB; upload a file, or reuse the product image with `from_product:true`). |
|
|
134
|
-
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤
|
|
136
|
+
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤5 MiB; editor only; optional treatment/position/duration). |
|
|
135
137
|
| `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
|
|
136
138
|
| `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its corner and `throughout`/`end_card`/`none` (hide) treatment. The logo is never sent to the video model. |
|
|
137
139
|
| `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA so they don't collide. |
|
|
@@ -251,14 +253,18 @@ edit are **0 credits** — you only pay to generate/render.
|
|
|
251
253
|
|
|
252
254
|
**One shot (recommended):**
|
|
253
255
|
`make_video({ prompt: "an ad for…", save_path: "ad.mp4" })` → returns `{ ready, video_url, saved_to, estimated_credits, available_credits }`.
|
|
254
|
-
It prices the job and only
|
|
255
|
-
`charged:false` with the estimate and a free draft slug.
|
|
256
|
+
It prices the job and only launches if it's affordable (and within `max_credits` if you set one); otherwise it returns
|
|
257
|
+
`charged:false` with the estimate and a free draft slug. Shorts charge at launch. Editors return the authorized cap and
|
|
258
|
+
observed incremental spend (`authorized_credits`, `credits_spent`) instead of treating authorization as a charge.
|
|
259
|
+
Preview first with `make_video({ prompt, dry_run:true })`.
|
|
256
260
|
If it returns `terminal:false`, the render is still going — call `wait_for_completion` with the returned slug.
|
|
257
261
|
|
|
258
262
|
**Granular (control/recovery):**
|
|
259
263
|
`create_short` → optional `generate_hook_variations` (test hook angles free) / `generate_short_text` → `generate_short` → `wait_for_completion {kind:"short"}` → `download_result`, or
|
|
260
264
|
`create_editor_ad` (free configure + quote) → obtain spend approval → `start_autopilot {slug, max_credits}` → `wait_for_completion {kind:"editor"}` → `download_result`.
|
|
261
265
|
|
|
266
|
+
If waiting returns `stage:"batch_retry_required"`, call `retry_editor_batch` to continue the prepaid failed scene, or `cancel_editor_batch` to abandon the batch. Resolve that child batch before calling `start_autopilot` again; a retry does not itself authorize a new parent Autopilot run.
|
|
267
|
+
|
|
262
268
|
**Iterate a short for free (never re-roll):** the first `generate_short` (15 credits) buys the footage +
|
|
263
269
|
music; after that, every text/style/CTA/end-card/logo tweak re-renders over them for **0 credits** —
|
|
264
270
|
`update_short` (new hook/CTA/style) → `rerender_short` → compare → repeat → keep the winner. Logo
|
package/dist/index.js
CHANGED
|
@@ -29554,7 +29554,8 @@ 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 batchRetryRequired = batchStatus === "paused_for_retry";
|
|
29558
29559
|
const batchInsufficientCredits = batchStatus === "paused_insufficient_credits";
|
|
29559
29560
|
const batchFailed = batchStatus === "failed";
|
|
29560
29561
|
const autopilotActive = [
|
|
@@ -29598,33 +29599,42 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29598
29599
|
currentMusic.error_message
|
|
29599
29600
|
].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
29600
29601
|
const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
|
|
29601
|
-
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;
|
|
29602
29610
|
const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
|
|
29603
29611
|
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
29604
|
-
const
|
|
29605
|
-
const renderFailed = renderStatus === "failed";
|
|
29606
|
-
const failed =
|
|
29607
|
-
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;
|
|
29608
29616
|
let stage = autopilot;
|
|
29609
29617
|
if (ready)
|
|
29610
29618
|
stage = "completed";
|
|
29611
|
-
else if (
|
|
29619
|
+
else if (batchRetryRequired)
|
|
29620
|
+
stage = "batch_retry_required";
|
|
29621
|
+
else if (parentFailureActionable && autopilot === "failed")
|
|
29612
29622
|
stage = "failed";
|
|
29613
|
-
else if (autopilot === "cancelled")
|
|
29623
|
+
else if (parentFailureActionable && autopilot === "cancelled")
|
|
29614
29624
|
stage = "cancelled";
|
|
29615
29625
|
else if (renderFailed)
|
|
29616
29626
|
stage = "render_failed";
|
|
29617
|
-
else if (scenarioApplyFailed)
|
|
29627
|
+
else if (childFailureActionable && scenarioApplyFailed)
|
|
29618
29628
|
stage = "scenario_apply_failed";
|
|
29619
|
-
else if (scenarioFailed)
|
|
29629
|
+
else if (childFailureActionable && scenarioFailed)
|
|
29620
29630
|
stage = "scenario_failed";
|
|
29621
|
-
else if (narrationFailed)
|
|
29631
|
+
else if (childFailureActionable && narrationFailed)
|
|
29622
29632
|
stage = "narration_failed";
|
|
29623
|
-
else if (batchFailed)
|
|
29633
|
+
else if (childFailureActionable && batchFailed)
|
|
29624
29634
|
stage = "batch_failed";
|
|
29625
|
-
else if (failedSegments.length > 0)
|
|
29635
|
+
else if (childFailureActionable && failedSegments.length > 0)
|
|
29626
29636
|
stage = "segment_failed";
|
|
29627
|
-
else if (audioFailed)
|
|
29637
|
+
else if (childFailureActionable && audioFailed)
|
|
29628
29638
|
stage = "audio_failed";
|
|
29629
29639
|
else if (batchInsufficientCredits)
|
|
29630
29640
|
stage = "insufficient_credits";
|
|
@@ -29643,6 +29653,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29643
29653
|
else if (!workActive && stale)
|
|
29644
29654
|
stage = "editing_required";
|
|
29645
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;
|
|
29646
29657
|
return {
|
|
29647
29658
|
kind,
|
|
29648
29659
|
slug,
|
|
@@ -29650,7 +29661,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29650
29661
|
terminal,
|
|
29651
29662
|
ready,
|
|
29652
29663
|
video_url: videoUrl,
|
|
29653
|
-
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),
|
|
29654
29665
|
stale,
|
|
29655
29666
|
active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
|
|
29656
29667
|
stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
|
|
@@ -30243,7 +30254,7 @@ function readStoredCredentials() {
|
|
|
30243
30254
|
// package.json
|
|
30244
30255
|
var package_default = {
|
|
30245
30256
|
name: "@hubfluencer/mcp",
|
|
30246
|
-
version: "0.
|
|
30257
|
+
version: "0.11.0",
|
|
30247
30258
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30248
30259
|
license: "MIT",
|
|
30249
30260
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -30646,6 +30657,9 @@ var createEditorOutput = exports_external.object({
|
|
|
30646
30657
|
estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
|
|
30647
30658
|
available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
|
|
30648
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(),
|
|
30649
30663
|
charged: exports_external.boolean().optional(),
|
|
30650
30664
|
note: exports_external.string().optional(),
|
|
30651
30665
|
next: exports_external.string().optional()
|
|
@@ -30662,6 +30676,9 @@ var makeVideoOutput = exports_external.object({
|
|
|
30662
30676
|
estimated_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
|
|
30663
30677
|
available_credits: exports_external.union([exports_external.number(), exports_external.null()]).optional(),
|
|
30664
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(),
|
|
30665
30682
|
charged: exports_external.boolean().optional(),
|
|
30666
30683
|
saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30667
30684
|
timed_out: exports_external.boolean().optional(),
|
|
@@ -31122,7 +31139,7 @@ var ALLOW_LOOPBACK_FETCH = /^http:\/\/(localhost|127\.0\.0\.1|\[?::1\]?)/.test(p
|
|
|
31122
31139
|
var MAX_VIDEO_BYTES = 500000000;
|
|
31123
31140
|
var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
31124
31141
|
var MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
31125
|
-
var MAX_LOGO_BYTES =
|
|
31142
|
+
var MAX_LOGO_BYTES = 5 * 1024 * 1024;
|
|
31126
31143
|
var MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
|
|
31127
31144
|
var MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
31128
31145
|
var MULTIPART_THRESHOLD = 50 * 1024 * 1024;
|
|
@@ -31821,7 +31838,7 @@ CREDITS (spent server-side; each tool prices before charging — see its descrip
|
|
|
31821
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.
|
|
31822
31839
|
|
|
31823
31840
|
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.
|
|
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.
|
|
31825
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.
|
|
31826
31843
|
|
|
31827
31844
|
LOCAL FILES (sandboxed)
|
|
@@ -31841,8 +31858,7 @@ async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
|
|
|
31841
31858
|
}
|
|
31842
31859
|
return status;
|
|
31843
31860
|
}
|
|
31844
|
-
async function pollSegmentToTerminal(client, slug, segmentId, extra,
|
|
31845
|
-
const deadline = Date.now() + budgetMs;
|
|
31861
|
+
async function pollSegmentToTerminal(client, slug, segmentId, extra, deadline, intervalMs) {
|
|
31846
31862
|
const sid = String(segmentId);
|
|
31847
31863
|
const read = async () => {
|
|
31848
31864
|
const res = await client.get(`/editor/${slug}`, undefined, deadline);
|
|
@@ -31991,6 +32007,7 @@ registerTool("make_video", {
|
|
|
31991
32007
|
const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
|
|
31992
32008
|
let estimated_credits = null;
|
|
31993
32009
|
let available_credits = null;
|
|
32010
|
+
let authorizedCredits;
|
|
31994
32011
|
try {
|
|
31995
32012
|
const cost = asRecord(asRecord(await client.get(costPath)).data);
|
|
31996
32013
|
estimated_credits = typeof cost.total === "number" ? cost.total : null;
|
|
@@ -32025,6 +32042,7 @@ registerTool("make_video", {
|
|
|
32025
32042
|
if (approvedCap === null) {
|
|
32026
32043
|
throw new Error("Editor autopilot launch requires max_credits or a live quote");
|
|
32027
32044
|
}
|
|
32045
|
+
authorizedCredits = approvedCap;
|
|
32028
32046
|
await client.post(`/editor/${slug}/autopilot`, { max_credits: approvedCap }, makeVideoAutopilotKey(slug, args, startState));
|
|
32029
32047
|
}
|
|
32030
32048
|
await reportProgress(extra, 0, `started ${kind} ${slug}`);
|
|
@@ -32033,12 +32051,29 @@ registerTool("make_video", {
|
|
|
32033
32051
|
if (status.ready && status.video_url && args.save_path) {
|
|
32034
32052
|
saved_to = (await downloadTo(status.video_url, args.save_path)).saved_to;
|
|
32035
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
|
+
};
|
|
32036
32071
|
const result = {
|
|
32037
32072
|
...status,
|
|
32038
32073
|
kind_inferred: requestedKind === undefined,
|
|
32039
32074
|
estimated_credits,
|
|
32040
32075
|
available_credits,
|
|
32041
|
-
|
|
32076
|
+
...billing,
|
|
32042
32077
|
saved_to,
|
|
32043
32078
|
timed_out: !status.terminal
|
|
32044
32079
|
};
|
|
@@ -32046,7 +32081,7 @@ registerTool("make_video", {
|
|
|
32046
32081
|
const links = status.ready && status.video_url ? [mp4Link(status.video_url, slug)] : [];
|
|
32047
32082
|
return ok({
|
|
32048
32083
|
...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}).`
|
|
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}).`
|
|
32050
32085
|
}, links);
|
|
32051
32086
|
}));
|
|
32052
32087
|
registerTool("get_credits", {
|
|
@@ -32735,7 +32770,7 @@ registerTool("create_editor_ad", {
|
|
|
32735
32770
|
product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot."),
|
|
32736
32771
|
product_description: exports_external.string().max(500).optional().describe("Exact product/character description used with product_image_path."),
|
|
32737
32772
|
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, ≤
|
|
32773
|
+
logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
|
|
32739
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.'),
|
|
32740
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.'),
|
|
32741
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."),
|
|
@@ -32846,7 +32881,10 @@ registerTool("create_editor_ad", {
|
|
|
32846
32881
|
kind: "editor",
|
|
32847
32882
|
status,
|
|
32848
32883
|
...quote,
|
|
32849
|
-
|
|
32884
|
+
launched: true,
|
|
32885
|
+
authorized_credits: args.max_credits,
|
|
32886
|
+
credits_spent: 0,
|
|
32887
|
+
charged: false,
|
|
32850
32888
|
next: "wait_for_completion"
|
|
32851
32889
|
});
|
|
32852
32890
|
} finally {
|
|
@@ -32855,7 +32893,7 @@ registerTool("create_editor_ad", {
|
|
|
32855
32893
|
}));
|
|
32856
32894
|
registerTool("start_autopilot", {
|
|
32857
32895
|
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" }).',
|
|
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" }).',
|
|
32859
32897
|
inputSchema: {
|
|
32860
32898
|
slug: exports_external.string().describe("Editor project slug"),
|
|
32861
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."),
|
|
@@ -32901,10 +32939,52 @@ registerTool("start_autopilot", {
|
|
|
32901
32939
|
kind: "editor",
|
|
32902
32940
|
status,
|
|
32903
32941
|
...quote,
|
|
32904
|
-
|
|
32942
|
+
launched: true,
|
|
32943
|
+
authorized_credits: args.max_credits,
|
|
32944
|
+
credits_spent: 0,
|
|
32945
|
+
charged: false,
|
|
32905
32946
|
next: "wait_for_completion"
|
|
32906
32947
|
});
|
|
32907
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
|
+
}));
|
|
32908
32988
|
registerTool("get_ai_assists", {
|
|
32909
32989
|
title: "Get AI assist quota",
|
|
32910
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).",
|
|
@@ -32964,14 +33044,67 @@ registerTool("create_editor_draft", {
|
|
|
32964
33044
|
next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
|
|
32965
33045
|
});
|
|
32966
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
|
+
}
|
|
32967
33096
|
registerTool("get_editor", {
|
|
32968
|
-
title: "Get
|
|
32969
|
-
description: "Returns the full editor
|
|
32970
|
-
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
|
+
},
|
|
32971
33103
|
annotations: { title: "Get editor", ...RO }
|
|
32972
33104
|
}, tool(async (args, client) => {
|
|
32973
33105
|
const res = await client.get(`/editor/${args.slug}`);
|
|
32974
|
-
|
|
33106
|
+
const data = asRecord(res).data;
|
|
33107
|
+
return ok(args.response_format === "full" ? data : conciseEditorState(data));
|
|
32975
33108
|
}));
|
|
32976
33109
|
registerTool("set_scenario", {
|
|
32977
33110
|
title: "Set the editor scenario",
|
|
@@ -33178,23 +33311,25 @@ registerTool("generate_all_segments", {
|
|
|
33178
33311
|
idempotentHint: false
|
|
33179
33312
|
}
|
|
33180
33313
|
}, tool(async (args, client, extra) => {
|
|
33181
|
-
const
|
|
33314
|
+
const overallDeadline = Date.now() + 280000;
|
|
33315
|
+
const res = await client.get(`/editor/${args.slug}`, undefined, overallDeadline);
|
|
33182
33316
|
const data = asRecord(asRecord(res).data);
|
|
33183
33317
|
const segments = Array.isArray(data.segments) ? data.segments : [];
|
|
33184
|
-
const
|
|
33318
|
+
const unfinished = segments.filter((s) => {
|
|
33319
|
+
if (s.source_type === "upload")
|
|
33320
|
+
return false;
|
|
33185
33321
|
const st = s.status ?? s.generation_status;
|
|
33186
|
-
return st
|
|
33187
|
-
});
|
|
33188
|
-
const
|
|
33189
|
-
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;
|
|
33190
33325
|
const generated = [];
|
|
33191
33326
|
let n = 0;
|
|
33192
|
-
for (const seg of
|
|
33327
|
+
for (const seg of unfinished) {
|
|
33193
33328
|
const id = seg.id;
|
|
33194
33329
|
if (id === undefined)
|
|
33195
33330
|
continue;
|
|
33196
33331
|
const sid = id;
|
|
33197
|
-
|
|
33332
|
+
let remainingMs = overallDeadline - Date.now();
|
|
33198
33333
|
if (remainingMs <= 0) {
|
|
33199
33334
|
return ok({
|
|
33200
33335
|
slug: args.slug,
|
|
@@ -33203,19 +33338,41 @@ registerTool("generate_all_segments", {
|
|
|
33203
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)."
|
|
33204
33339
|
});
|
|
33205
33340
|
}
|
|
33206
|
-
await reportProgress(extra, ++n, `generating segment ${String(sid)} (${n}/${
|
|
33207
|
-
|
|
33208
|
-
|
|
33209
|
-
|
|
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) {
|
|
33210
33367
|
return ok({
|
|
33211
33368
|
slug: args.slug,
|
|
33212
33369
|
generated,
|
|
33213
33370
|
stopped_at: sid,
|
|
33214
|
-
|
|
33215
|
-
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."
|
|
33216
33373
|
});
|
|
33217
33374
|
}
|
|
33218
|
-
const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra,
|
|
33375
|
+
const seg_result = await pollSegmentToTerminal(client, args.slug, sid, extra, overallDeadline, 15000);
|
|
33219
33376
|
if (seg_result.status === "failed") {
|
|
33220
33377
|
return ok({
|
|
33221
33378
|
slug: args.slug,
|
|
@@ -33234,7 +33391,34 @@ registerTool("generate_all_segments", {
|
|
|
33234
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."
|
|
33235
33392
|
});
|
|
33236
33393
|
}
|
|
33237
|
-
|
|
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
|
+
});
|
|
33238
33422
|
}
|
|
33239
33423
|
return ok({
|
|
33240
33424
|
slug: args.slug,
|
|
@@ -33616,7 +33800,7 @@ registerTool("set_closing_image", {
|
|
|
33616
33800
|
}));
|
|
33617
33801
|
registerTool("set_logo", {
|
|
33618
33802
|
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 (≤
|
|
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.",
|
|
33620
33804
|
inputSchema: {
|
|
33621
33805
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33622
33806
|
file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monocursive <contact@monocursive.com>",
|
package/src/core.ts
CHANGED
|
@@ -457,15 +457,14 @@ export function normalizeStatus(
|
|
|
457
457
|
);
|
|
458
458
|
const batchStatus = (d.batch_generation_status as string) ?? null;
|
|
459
459
|
// The batch enum's ACTIVE states (video_factory.ex): preparing (spinning up),
|
|
460
|
-
// awaiting_previews (previews generating), running
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// idle and could be reported terminal mid-run.
|
|
460
|
+
// awaiting_previews (previews generating), and running. "queued" is not a real
|
|
461
|
+
// value and was dropped. paused_for_retry is deliberately NOT active: no worker
|
|
462
|
+
// owns it until the user explicitly retries or cancels the paused batch.
|
|
464
463
|
const batchActive =
|
|
465
464
|
batchStatus === "running" ||
|
|
466
465
|
batchStatus === "preparing" ||
|
|
467
|
-
batchStatus === "awaiting_previews"
|
|
468
|
-
|
|
466
|
+
batchStatus === "awaiting_previews";
|
|
467
|
+
const batchRetryRequired = batchStatus === "paused_for_retry";
|
|
469
468
|
// A parked batch that ran out of credits: it cannot progress without the user
|
|
470
469
|
// topping up or reducing scope, so it is a needs-user-action TERMINAL state
|
|
471
470
|
// (not active work to wait on).
|
|
@@ -566,14 +565,39 @@ export function normalizeStatus(
|
|
|
566
565
|
currentMusic.status === "processing" ||
|
|
567
566
|
newerAudioVersions.some(versionActive) ||
|
|
568
567
|
newerMusicVersions.some(versionActive);
|
|
569
|
-
const
|
|
570
|
-
autopilotActive ||
|
|
568
|
+
const childWorkActive =
|
|
571
569
|
batchActive ||
|
|
572
570
|
renderActive ||
|
|
573
571
|
scenarioActive ||
|
|
574
572
|
narrationActive ||
|
|
575
573
|
audioActive ||
|
|
576
574
|
activeSegments.length > 0;
|
|
575
|
+
// A resumed Autopilot briefly exposes the previous attempt's failed child
|
|
576
|
+
// states before its new worker reaches and clears them. Suppress that history
|
|
577
|
+
// only while real child work is active or for a short, timestamped hand-off
|
|
578
|
+
// window. A parent stuck "running" with no worker must eventually surface the
|
|
579
|
+
// child failure instead of making wait_for_completion poll forever.
|
|
580
|
+
const autopilotStartedAt =
|
|
581
|
+
typeof d.autopilot_started_at === "string"
|
|
582
|
+
? Date.parse(d.autopilot_started_at)
|
|
583
|
+
: Number.NaN;
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const autopilotResumeGraceActive =
|
|
586
|
+
autopilotActive &&
|
|
587
|
+
Number.isFinite(autopilotStartedAt) &&
|
|
588
|
+
autopilotStartedAt <= now &&
|
|
589
|
+
now - autopilotStartedAt <= 120_000;
|
|
590
|
+
const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
|
|
591
|
+
// Retrying a paused batch deliberately leaves the failed parent Autopilot
|
|
592
|
+
// marker in place while the already-paid child batch finishes. In that one
|
|
593
|
+
// state combination the parent/old scene failures are history, not a reason
|
|
594
|
+
// to return terminal immediately. Once the batch settles, the parent failure
|
|
595
|
+
// becomes actionable again and the agent can quote/relaunch Autopilot.
|
|
596
|
+
const batchRecoveryActive = autopilotFailed && batchActive;
|
|
597
|
+
const childFailureActionable =
|
|
598
|
+
!batchRecoveryActive &&
|
|
599
|
+
(!autopilotActive || (!childWorkActive && !autopilotResumeGraceActive));
|
|
600
|
+
const workActive = autopilotActive || childWorkActive;
|
|
577
601
|
// Orchestration failures are historical once a repaired timeline has a fresh,
|
|
578
602
|
// completed MP4. Keeping them terminal would permanently block download_result
|
|
579
603
|
// after a successful granular recovery.
|
|
@@ -586,17 +610,26 @@ export function normalizeStatus(
|
|
|
586
610
|
failedSegments.length === 0 &&
|
|
587
611
|
!audioFailed;
|
|
588
612
|
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
589
|
-
const
|
|
590
|
-
const renderFailed = renderStatus === "failed";
|
|
613
|
+
const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
|
|
614
|
+
const renderFailed = renderStatus === "failed" && childFailureActionable;
|
|
591
615
|
const failed =
|
|
592
|
-
(
|
|
616
|
+
(parentFailureActionable &&
|
|
617
|
+
!deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
593
618
|
renderFailed ||
|
|
594
|
-
(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
(
|
|
598
|
-
|
|
599
|
-
|
|
619
|
+
(childFailureActionable &&
|
|
620
|
+
scenarioApplyFailed &&
|
|
621
|
+
!deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
622
|
+
(childFailureActionable &&
|
|
623
|
+
scenarioFailed &&
|
|
624
|
+
!deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
625
|
+
(childFailureActionable &&
|
|
626
|
+
narrationFailed &&
|
|
627
|
+
!deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
628
|
+
(childFailureActionable &&
|
|
629
|
+
batchFailed &&
|
|
630
|
+
!deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
631
|
+
(childFailureActionable && failedSegments.length > 0) ||
|
|
632
|
+
(childFailureActionable && audioFailed);
|
|
600
633
|
// Terminal = control should return to the agent because nothing will progress
|
|
601
634
|
// without another tool call. With every async job type folded into workActive
|
|
602
635
|
// above (autopilot, batch, segments, scenario, narration, audio, render), NO
|
|
@@ -605,21 +638,35 @@ export function normalizeStatus(
|
|
|
605
638
|
// a new tool call can move it, so wait_for_completion must return instead of
|
|
606
639
|
// polling its full budget. (ready is subsumed by !workActive but kept for
|
|
607
640
|
// clarity; explicit failure states are terminal even while cleanup work spins.)
|
|
608
|
-
const terminal =
|
|
641
|
+
const terminal =
|
|
642
|
+
ready ||
|
|
643
|
+
failed ||
|
|
644
|
+
batchRetryRequired ||
|
|
645
|
+
batchInsufficientCredits ||
|
|
646
|
+
!workActive;
|
|
609
647
|
let stage = autopilot;
|
|
610
648
|
// Failure always wins over an unrelated active flag. Workers can leave an
|
|
611
649
|
// outer autopilot/batch state active after a child stage has failed; reporting
|
|
612
650
|
// the active stage would make agents poll forever instead of taking action.
|
|
613
651
|
if (ready) stage = "completed";
|
|
614
|
-
|
|
615
|
-
|
|
652
|
+
// A paused child batch is the immediate recovery gate even when its parent
|
|
653
|
+
// Autopilot run has already failed/cancelled. Reporting only the generic
|
|
654
|
+
// parent terminal state hides the one action that must happen before a new
|
|
655
|
+
// Autopilot run is allowed to start.
|
|
656
|
+
else if (batchRetryRequired) stage = "batch_retry_required";
|
|
657
|
+
else if (parentFailureActionable && autopilot === "failed") stage = "failed";
|
|
658
|
+
else if (parentFailureActionable && autopilot === "cancelled")
|
|
659
|
+
stage = "cancelled";
|
|
616
660
|
else if (renderFailed) stage = "render_failed";
|
|
617
|
-
else if (scenarioApplyFailed)
|
|
618
|
-
|
|
619
|
-
else if (
|
|
620
|
-
else if (
|
|
621
|
-
|
|
622
|
-
else if (
|
|
661
|
+
else if (childFailureActionable && scenarioApplyFailed)
|
|
662
|
+
stage = "scenario_apply_failed";
|
|
663
|
+
else if (childFailureActionable && scenarioFailed) stage = "scenario_failed";
|
|
664
|
+
else if (childFailureActionable && narrationFailed)
|
|
665
|
+
stage = "narration_failed";
|
|
666
|
+
else if (childFailureActionable && batchFailed) stage = "batch_failed";
|
|
667
|
+
else if (childFailureActionable && failedSegments.length > 0)
|
|
668
|
+
stage = "segment_failed";
|
|
669
|
+
else if (childFailureActionable && audioFailed) stage = "audio_failed";
|
|
623
670
|
else if (batchInsufficientCredits) stage = "insufficient_credits";
|
|
624
671
|
else if (activeSegments.length > 0) stage = "segment_processing";
|
|
625
672
|
else if (batchActive) stage = "segment_generation";
|
|
@@ -633,6 +680,15 @@ export function normalizeStatus(
|
|
|
633
680
|
.find(
|
|
634
681
|
(value): value is string => typeof value === "string" && value.length > 0,
|
|
635
682
|
);
|
|
683
|
+
const actionableChildError = childFailureActionable
|
|
684
|
+
? ((latest.error_message as string) ??
|
|
685
|
+
(d.scenario_apply_error_message as string) ??
|
|
686
|
+
(d.scenario_error_message as string) ??
|
|
687
|
+
(d.narration_error_message as string) ??
|
|
688
|
+
(d.batch_generation_error_message as string) ??
|
|
689
|
+
segmentError ??
|
|
690
|
+
audioError)
|
|
691
|
+
: null;
|
|
636
692
|
return {
|
|
637
693
|
kind,
|
|
638
694
|
slug,
|
|
@@ -642,17 +698,15 @@ export function normalizeStatus(
|
|
|
642
698
|
video_url: videoUrl,
|
|
643
699
|
error: ready
|
|
644
700
|
? null
|
|
645
|
-
:
|
|
646
|
-
|
|
647
|
-
(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
|
|
655
|
-
: null)),
|
|
701
|
+
: batchRetryRequired
|
|
702
|
+
? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing."
|
|
703
|
+
: ((parentFailureActionable
|
|
704
|
+
? (d.autopilot_error_message as string)
|
|
705
|
+
: null) ??
|
|
706
|
+
actionableChildError ??
|
|
707
|
+
(batchInsufficientCredits
|
|
708
|
+
? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
|
|
709
|
+
: null)),
|
|
656
710
|
stale,
|
|
657
711
|
active_segment_ids: activeSegments
|
|
658
712
|
.map((segment) => segment.id)
|
package/src/index.ts
CHANGED
|
@@ -620,7 +620,7 @@ CREDITS (spent server-side; each tool prices before charging — see its descrip
|
|
|
620
620
|
- 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.
|
|
621
621
|
|
|
622
622
|
WAITING & RESUMING
|
|
623
|
-
- 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.
|
|
623
|
+
- 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.
|
|
624
624
|
- 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.
|
|
625
625
|
|
|
626
626
|
LOCAL FILES (sandboxed)
|
|
@@ -683,10 +683,9 @@ async function pollSegmentToTerminal(
|
|
|
683
683
|
slug: string,
|
|
684
684
|
segmentId: string | number,
|
|
685
685
|
extra: ToolExtra,
|
|
686
|
-
|
|
686
|
+
deadline: number,
|
|
687
687
|
intervalMs: number,
|
|
688
688
|
): Promise<{ status: string; error: string | null; timed_out: boolean }> {
|
|
689
|
-
const deadline = Date.now() + budgetMs;
|
|
690
689
|
const sid = String(segmentId);
|
|
691
690
|
const read = async (): Promise<{ status: string; error: string | null }> => {
|
|
692
691
|
// Bound each poll GET's retries by the loop deadline (see fetchStatus).
|
|
@@ -1135,6 +1134,7 @@ registerTool(
|
|
|
1135
1134
|
: `/editor/${slug}/autopilot/cost`;
|
|
1136
1135
|
let estimated_credits: number | null = null;
|
|
1137
1136
|
let available_credits: number | null = null;
|
|
1137
|
+
let authorizedCredits: number | undefined;
|
|
1138
1138
|
try {
|
|
1139
1139
|
const cost = asRecord(
|
|
1140
1140
|
asRecord(await client.get<{ data: unknown }>(costPath)).data,
|
|
@@ -1238,6 +1238,7 @@ registerTool(
|
|
|
1238
1238
|
"Editor autopilot launch requires max_credits or a live quote",
|
|
1239
1239
|
);
|
|
1240
1240
|
}
|
|
1241
|
+
authorizedCredits = approvedCap;
|
|
1241
1242
|
await client.post(
|
|
1242
1243
|
`/editor/${slug}/autopilot`,
|
|
1243
1244
|
// Use the caller's explicit cap when given, otherwise the live
|
|
@@ -1272,12 +1273,50 @@ registerTool(
|
|
|
1272
1273
|
.saved_to;
|
|
1273
1274
|
}
|
|
1274
1275
|
|
|
1276
|
+
// Shorts charge atomically when generation starts. Editor autopilot only
|
|
1277
|
+
// authorizes a cap at launch and spends incrementally as child work starts,
|
|
1278
|
+
// so report observed run spend instead of claiming the whole launch charged.
|
|
1279
|
+
// If this receipt-enrichment read fails, omit the charge claim entirely.
|
|
1280
|
+
let editorCreditsSpent: number | undefined;
|
|
1281
|
+
if (kind === "editor") {
|
|
1282
|
+
try {
|
|
1283
|
+
const current = asRecord(
|
|
1284
|
+
asRecord(
|
|
1285
|
+
await client.get<{ data: unknown }>(
|
|
1286
|
+
`/editor/${slug}`,
|
|
1287
|
+
undefined,
|
|
1288
|
+
Date.now() + 5_000,
|
|
1289
|
+
),
|
|
1290
|
+
).data,
|
|
1291
|
+
);
|
|
1292
|
+
if (typeof current.autopilot_credits_spent === "number") {
|
|
1293
|
+
editorCreditsSpent = current.autopilot_credits_spent;
|
|
1294
|
+
}
|
|
1295
|
+
} catch {
|
|
1296
|
+
// The launch/poll result remains valid without receipt enrichment.
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const billing =
|
|
1301
|
+
kind === "short"
|
|
1302
|
+
? { charged: true }
|
|
1303
|
+
: {
|
|
1304
|
+
launched: true,
|
|
1305
|
+
authorized_credits: authorizedCredits,
|
|
1306
|
+
...(editorCreditsSpent === undefined
|
|
1307
|
+
? {}
|
|
1308
|
+
: {
|
|
1309
|
+
credits_spent: editorCreditsSpent,
|
|
1310
|
+
charged: editorCreditsSpent > 0,
|
|
1311
|
+
}),
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1275
1314
|
const result = {
|
|
1276
1315
|
...status,
|
|
1277
1316
|
kind_inferred: requestedKind === undefined,
|
|
1278
1317
|
estimated_credits,
|
|
1279
1318
|
available_credits,
|
|
1280
|
-
|
|
1319
|
+
...billing,
|
|
1281
1320
|
saved_to,
|
|
1282
1321
|
timed_out: !status.terminal,
|
|
1283
1322
|
};
|
|
@@ -1297,13 +1336,15 @@ registerTool(
|
|
|
1297
1336
|
note: status.ready
|
|
1298
1337
|
? "Done. Presigned ~24h URL — download promptly."
|
|
1299
1338
|
: status.terminal
|
|
1300
|
-
?
|
|
1301
|
-
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1339
|
+
? status.stage === "batch_retry_required"
|
|
1340
|
+
? `Terminal (${status.stage}). ${status.error ?? ""} Resolve the paused batch before restarting Autopilot.`.trim()
|
|
1341
|
+
: // A terminal-but-not-ready make_video is normally a failed/cancelled run.
|
|
1342
|
+
// Point at a restart: re-running make_video now genuinely restarts
|
|
1343
|
+
// (the start key folds the failure state, so it no longer replays the
|
|
1344
|
+
// dead 202), or resume the pipeline directly. Fix the cause (often
|
|
1345
|
+
// credits) first.
|
|
1346
|
+
`Terminal (${status.stage}). ${status.error ?? ""}`.trim() +
|
|
1347
|
+
` To restart, fix the cause (often credits) then re-run make_video, or resume with ${resume}.`
|
|
1307
1348
|
: `Still rendering — call wait_for_completion(${resumeArgs}).`,
|
|
1308
1349
|
},
|
|
1309
1350
|
links,
|
|
@@ -3060,7 +3101,7 @@ registerTool(
|
|
|
3060
3101
|
logo_path: z
|
|
3061
3102
|
.string()
|
|
3062
3103
|
.optional()
|
|
3063
|
-
.describe("Local logo (.jpg/.jpeg/.png, ≤
|
|
3104
|
+
.describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
|
|
3064
3105
|
logo_treatment: z
|
|
3065
3106
|
.enum(["intro", "outro", "both", "none"])
|
|
3066
3107
|
.optional()
|
|
@@ -3318,7 +3359,10 @@ registerTool(
|
|
|
3318
3359
|
kind: "editor",
|
|
3319
3360
|
status,
|
|
3320
3361
|
...quote,
|
|
3321
|
-
|
|
3362
|
+
launched: true,
|
|
3363
|
+
authorized_credits: args.max_credits,
|
|
3364
|
+
credits_spent: 0,
|
|
3365
|
+
charged: false,
|
|
3322
3366
|
next: "wait_for_completion",
|
|
3323
3367
|
});
|
|
3324
3368
|
} finally {
|
|
@@ -3338,7 +3382,9 @@ registerTool(
|
|
|
3338
3382
|
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3339
3383
|
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
3340
3384
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
3341
|
-
"or double-charges. Failed or cancelled runs resume normally
|
|
3385
|
+
"or double-charges. Failed or cancelled runs resume normally, except when status is batch_retry_required: " +
|
|
3386
|
+
"call retry_editor_batch (then wait for that prepaid batch) or cancel_editor_batch first. For a completed project, " +
|
|
3387
|
+
"set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
|
|
3342
3388
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
3343
3389
|
inputSchema: {
|
|
3344
3390
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -3455,13 +3501,79 @@ registerTool(
|
|
|
3455
3501
|
kind: "editor",
|
|
3456
3502
|
status,
|
|
3457
3503
|
...quote,
|
|
3458
|
-
|
|
3504
|
+
launched: true,
|
|
3505
|
+
authorized_credits: args.max_credits,
|
|
3506
|
+
credits_spent: 0,
|
|
3507
|
+
charged: false,
|
|
3459
3508
|
next: "wait_for_completion",
|
|
3460
3509
|
});
|
|
3461
3510
|
},
|
|
3462
3511
|
),
|
|
3463
3512
|
);
|
|
3464
3513
|
|
|
3514
|
+
registerTool(
|
|
3515
|
+
"retry_editor_batch",
|
|
3516
|
+
{
|
|
3517
|
+
title: "Retry a paused editor batch",
|
|
3518
|
+
description:
|
|
3519
|
+
"Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " +
|
|
3520
|
+
"scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " +
|
|
3521
|
+
"paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " +
|
|
3522
|
+
"to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " +
|
|
3523
|
+
"relaunch only with an approved cap. A batch parked on " +
|
|
3524
|
+
"insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
|
|
3525
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3526
|
+
annotations: {
|
|
3527
|
+
title: "Retry editor batch",
|
|
3528
|
+
...WRITE,
|
|
3529
|
+
idempotentHint: false,
|
|
3530
|
+
},
|
|
3531
|
+
},
|
|
3532
|
+
tool(async (args: { slug: string }, client) => {
|
|
3533
|
+
const res = await client.post<{ data: unknown }>(
|
|
3534
|
+
`/video-factories/${args.slug}/retry-batch-segment`,
|
|
3535
|
+
);
|
|
3536
|
+
return ok({
|
|
3537
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3538
|
+
slug: args.slug,
|
|
3539
|
+
charged: false,
|
|
3540
|
+
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.",
|
|
3541
|
+
next: "wait_for_completion",
|
|
3542
|
+
});
|
|
3543
|
+
}),
|
|
3544
|
+
);
|
|
3545
|
+
|
|
3546
|
+
registerTool(
|
|
3547
|
+
"cancel_editor_batch",
|
|
3548
|
+
{
|
|
3549
|
+
title: "Cancel an editor batch",
|
|
3550
|
+
description:
|
|
3551
|
+
"Cancels an editor batch in awaiting_previews, running, or paused_for_retry so the project can be edited or " +
|
|
3552
|
+
"Autopilot can be started again. Cancellation is destructive: dispatched scene work is stopped and its prepaid " +
|
|
3553
|
+
"credits are not refunded; only an awaiting_previews batch (before dispatch) is refunded by the server. Spends " +
|
|
3554
|
+
"0 additional credits. Scope: video:generate.",
|
|
3555
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3556
|
+
annotations: {
|
|
3557
|
+
title: "Cancel editor batch",
|
|
3558
|
+
readOnlyHint: false,
|
|
3559
|
+
destructiveHint: true,
|
|
3560
|
+
openWorldHint: true,
|
|
3561
|
+
idempotentHint: false,
|
|
3562
|
+
},
|
|
3563
|
+
},
|
|
3564
|
+
tool(async (args: { slug: string }, client) => {
|
|
3565
|
+
const res = await client.post<{ data: unknown }>(
|
|
3566
|
+
`/video-factories/${args.slug}/cancel-batch`,
|
|
3567
|
+
);
|
|
3568
|
+
return ok({
|
|
3569
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3570
|
+
slug: args.slug,
|
|
3571
|
+
charged: false,
|
|
3572
|
+
next: "get_status",
|
|
3573
|
+
});
|
|
3574
|
+
}),
|
|
3575
|
+
);
|
|
3576
|
+
|
|
3465
3577
|
// ── AI assists (free daily quota) ───────────────────────────────────────────
|
|
3466
3578
|
|
|
3467
3579
|
registerTool(
|
|
@@ -3660,21 +3772,81 @@ registerTool(
|
|
|
3660
3772
|
),
|
|
3661
3773
|
);
|
|
3662
3774
|
|
|
3775
|
+
function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
3776
|
+
const data = asRecord(value);
|
|
3777
|
+
const keys = [
|
|
3778
|
+
"slug",
|
|
3779
|
+
"status",
|
|
3780
|
+
"autopilot_status",
|
|
3781
|
+
"autopilot_current_step",
|
|
3782
|
+
"autopilot_error_step",
|
|
3783
|
+
"autopilot_error_code",
|
|
3784
|
+
"autopilot_error_message",
|
|
3785
|
+
"autopilot_max_credits",
|
|
3786
|
+
"autopilot_credits_spent",
|
|
3787
|
+
"scenario_status",
|
|
3788
|
+
"scenario_apply_status",
|
|
3789
|
+
"scenario_prompt",
|
|
3790
|
+
"batch_generation_status",
|
|
3791
|
+
"narration_status",
|
|
3792
|
+
"narration_script",
|
|
3793
|
+
"narration_stale",
|
|
3794
|
+
"voice_stale",
|
|
3795
|
+
"music_stale",
|
|
3796
|
+
"segments_count",
|
|
3797
|
+
"current_audio",
|
|
3798
|
+
"current_music",
|
|
3799
|
+
"latest_render",
|
|
3800
|
+
"project_spend",
|
|
3801
|
+
"ai_assist_quota",
|
|
3802
|
+
] as const;
|
|
3803
|
+
const result: Record<string, unknown> = {};
|
|
3804
|
+
for (const key of keys) {
|
|
3805
|
+
if (key in data) result[key] = data[key];
|
|
3806
|
+
}
|
|
3807
|
+
result.segments = Array.isArray(data.segments)
|
|
3808
|
+
? (data.segments as Record<string, unknown>[]).map((segment) => ({
|
|
3809
|
+
id: segment.id,
|
|
3810
|
+
position: segment.position,
|
|
3811
|
+
prompt: segment.prompt,
|
|
3812
|
+
status: segment.status,
|
|
3813
|
+
source_type: segment.source_type,
|
|
3814
|
+
use_previous_frame: segment.use_previous_frame,
|
|
3815
|
+
protagonist_presence: segment.protagonist_presence,
|
|
3816
|
+
cast_mode_snapshot: segment.cast_mode_snapshot,
|
|
3817
|
+
video_stale: segment.video_stale,
|
|
3818
|
+
error_code: segment.error_code,
|
|
3819
|
+
error_message: segment.error_message,
|
|
3820
|
+
narration_text: segment.narration_text,
|
|
3821
|
+
}))
|
|
3822
|
+
: [];
|
|
3823
|
+
return result;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3663
3826
|
registerTool(
|
|
3664
3827
|
"get_editor",
|
|
3665
3828
|
{
|
|
3666
|
-
title: "Get
|
|
3829
|
+
title: "Get editor state (review step)",
|
|
3667
3830
|
description:
|
|
3668
|
-
"Returns the full editor
|
|
3669
|
-
|
|
3670
|
-
"
|
|
3671
|
-
|
|
3831
|
+
"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.",
|
|
3832
|
+
inputSchema: {
|
|
3833
|
+
slug: z.string().describe("Editor project slug"),
|
|
3834
|
+
response_format: z.enum(["concise", "full"]).default("full"),
|
|
3835
|
+
},
|
|
3672
3836
|
annotations: { title: "Get editor", ...RO },
|
|
3673
3837
|
},
|
|
3674
|
-
tool(
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3838
|
+
tool(
|
|
3839
|
+
async (
|
|
3840
|
+
args: { slug: string; response_format: "concise" | "full" },
|
|
3841
|
+
client,
|
|
3842
|
+
) => {
|
|
3843
|
+
const res = await client.get<{ data: unknown }>(`/editor/${args.slug}`);
|
|
3844
|
+
const data = asRecord(res).data;
|
|
3845
|
+
return ok(
|
|
3846
|
+
args.response_format === "full" ? data : conciseEditorState(data),
|
|
3847
|
+
);
|
|
3848
|
+
},
|
|
3849
|
+
),
|
|
3678
3850
|
);
|
|
3679
3851
|
|
|
3680
3852
|
registerTool(
|
|
@@ -4123,20 +4295,30 @@ registerTool(
|
|
|
4123
4295
|
},
|
|
4124
4296
|
},
|
|
4125
4297
|
tool(async (args: { slug: string }, client, extra) => {
|
|
4126
|
-
|
|
4298
|
+
// One absolute deadline owns the entire read/submit/poll/reconcile path.
|
|
4299
|
+
// Start it before the initial retrying GET so no preparatory request sits
|
|
4300
|
+
// outside the advertised ~280s tool budget.
|
|
4301
|
+
const overallDeadline = Date.now() + 280_000;
|
|
4302
|
+
const res = await client.get<{ data: unknown }>(
|
|
4303
|
+
`/editor/${args.slug}`,
|
|
4304
|
+
undefined,
|
|
4305
|
+
overallDeadline,
|
|
4306
|
+
);
|
|
4127
4307
|
const data = asRecord(asRecord(res).data);
|
|
4128
4308
|
const segments = Array.isArray(data.segments)
|
|
4129
4309
|
? (data.segments as Record<string, unknown>[])
|
|
4130
4310
|
: [];
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
st
|
|
4135
|
-
st
|
|
4136
|
-
|
|
4137
|
-
|
|
4311
|
+
const unfinished = segments
|
|
4312
|
+
.filter((s) => {
|
|
4313
|
+
if (s.source_type === "upload") return false;
|
|
4314
|
+
const st = (s.status as string) ?? (s.generation_status as string);
|
|
4315
|
+
return st !== "completed";
|
|
4316
|
+
})
|
|
4317
|
+
.sort(
|
|
4318
|
+
(a, b) =>
|
|
4319
|
+
Number(a.position ?? Number.MAX_SAFE_INTEGER) -
|
|
4320
|
+
Number(b.position ?? Number.MAX_SAFE_INTEGER),
|
|
4138
4321
|
);
|
|
4139
|
-
});
|
|
4140
4322
|
|
|
4141
4323
|
// Overall wall-clock budget: cap the whole call at the same 280s ceiling the
|
|
4142
4324
|
// other wait loops use (under a typical 300s client timeout). Without it a
|
|
@@ -4144,20 +4326,23 @@ registerTool(
|
|
|
4144
4326
|
// hours) in a single call. When the budget runs out mid-batch we stop and
|
|
4145
4327
|
// return the standard resume contract — call again and it naturally continues
|
|
4146
4328
|
// with whatever scenes are still pending (completed ones are skipped).
|
|
4147
|
-
|
|
4148
|
-
//
|
|
4149
|
-
//
|
|
4150
|
-
|
|
4329
|
+
// Every scene poll receives this same absolute deadline, so time already spent
|
|
4330
|
+
// on earlier reads, submissions, and scenes is never granted again.
|
|
4331
|
+
// Reserve a full non-idempotent POST timeout plus one final status read before
|
|
4332
|
+
// starting another paid scene. POSTs deliberately aren't aborted/retried, so
|
|
4333
|
+
// beginning one in the last seconds of the budget would make a client timeout
|
|
4334
|
+
// ambiguous after the server may already have accepted the charge.
|
|
4335
|
+
const SUBMIT_HEADROOM_MS = 61_000;
|
|
4151
4336
|
|
|
4152
4337
|
const generated: Array<string | number> = [];
|
|
4153
4338
|
let n = 0;
|
|
4154
|
-
for (const seg of
|
|
4339
|
+
for (const seg of unfinished) {
|
|
4155
4340
|
const id = seg.id;
|
|
4156
4341
|
if (id === undefined) continue;
|
|
4157
4342
|
const sid = id as string | number;
|
|
4158
4343
|
// Stop before submitting another scene once the overall budget is spent, so
|
|
4159
4344
|
// the call returns instead of charging + waiting past the deadline.
|
|
4160
|
-
|
|
4345
|
+
let remainingMs = overallDeadline - Date.now();
|
|
4161
4346
|
if (remainingMs <= 0) {
|
|
4162
4347
|
return ok({
|
|
4163
4348
|
slug: args.slug,
|
|
@@ -4169,22 +4354,45 @@ registerTool(
|
|
|
4169
4354
|
await reportProgress(
|
|
4170
4355
|
extra,
|
|
4171
4356
|
++n,
|
|
4172
|
-
`generating segment ${String(sid)} (${n}/${
|
|
4173
|
-
|
|
4357
|
+
`generating segment ${String(sid)} (${n}/${unfinished.length})`,
|
|
4358
|
+
unfinished.length,
|
|
4174
4359
|
);
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4360
|
+
const initialStatus =
|
|
4361
|
+
(seg.status as string) ?? (seg.generation_status as string);
|
|
4362
|
+
if (initialStatus !== "processing") {
|
|
4363
|
+
remainingMs = overallDeadline - Date.now();
|
|
4364
|
+
if (remainingMs < SUBMIT_HEADROOM_MS) {
|
|
4365
|
+
return ok({
|
|
4366
|
+
slug: args.slug,
|
|
4367
|
+
generated,
|
|
4368
|
+
timed_out: true,
|
|
4369
|
+
note: "Wall-clock budget is too low to safely start another paid scene. Re-run generate_all_segments to continue; completed scenes are skipped.",
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
try {
|
|
4373
|
+
await client.post(
|
|
4374
|
+
`/editor/${args.slug}/segments/${String(sid)}/generate`,
|
|
4375
|
+
undefined,
|
|
4376
|
+
undefined,
|
|
4377
|
+
);
|
|
4378
|
+
} catch (e) {
|
|
4379
|
+
return ok({
|
|
4380
|
+
slug: args.slug,
|
|
4381
|
+
generated,
|
|
4382
|
+
stopped_at: sid,
|
|
4383
|
+
error: errMessage(e),
|
|
4384
|
+
note: "Stopped on first error — fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried.",
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
remainingMs = overallDeadline - Date.now();
|
|
4389
|
+
if (remainingMs <= 0) {
|
|
4182
4390
|
return ok({
|
|
4183
4391
|
slug: args.slug,
|
|
4184
4392
|
generated,
|
|
4185
4393
|
stopped_at: sid,
|
|
4186
|
-
|
|
4187
|
-
note: "
|
|
4394
|
+
timed_out: true,
|
|
4395
|
+
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.",
|
|
4188
4396
|
});
|
|
4189
4397
|
}
|
|
4190
4398
|
// Gate on THIS segment finishing before starting the next, so the next
|
|
@@ -4196,7 +4404,7 @@ registerTool(
|
|
|
4196
4404
|
args.slug,
|
|
4197
4405
|
sid,
|
|
4198
4406
|
extra,
|
|
4199
|
-
|
|
4407
|
+
overallDeadline,
|
|
4200
4408
|
15000,
|
|
4201
4409
|
);
|
|
4202
4410
|
if (seg_result.status === "failed") {
|
|
@@ -4217,7 +4425,45 @@ registerTool(
|
|
|
4217
4425
|
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.",
|
|
4218
4426
|
});
|
|
4219
4427
|
}
|
|
4220
|
-
generated.push(sid);
|
|
4428
|
+
if (initialStatus !== "processing") generated.push(sid);
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// Reconcile against a fresh authoritative snapshot. A scene may have been
|
|
4432
|
+
// processing when this call started, or a completion webhook may still be
|
|
4433
|
+
// landing; never claim "all generated" while any AI scene is unfinished.
|
|
4434
|
+
if (overallDeadline - Date.now() < 1_000) {
|
|
4435
|
+
return ok({
|
|
4436
|
+
slug: args.slug,
|
|
4437
|
+
generated,
|
|
4438
|
+
timed_out: true,
|
|
4439
|
+
note: "Wall-clock budget reached before the final reconciliation read. Re-run generate_all_segments to confirm completion; completed scenes are skipped.",
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
const finalRes = await client.get<{ data: unknown }>(
|
|
4443
|
+
`/editor/${args.slug}`,
|
|
4444
|
+
undefined,
|
|
4445
|
+
overallDeadline,
|
|
4446
|
+
);
|
|
4447
|
+
const finalData = asRecord(asRecord(finalRes).data);
|
|
4448
|
+
const finalSegments = Array.isArray(finalData.segments)
|
|
4449
|
+
? (finalData.segments as Record<string, unknown>[])
|
|
4450
|
+
: [];
|
|
4451
|
+
const stillUnfinished = finalSegments.filter((segment) => {
|
|
4452
|
+
if (segment.source_type === "upload") return false;
|
|
4453
|
+
const status =
|
|
4454
|
+
(segment.status as string) ?? (segment.generation_status as string);
|
|
4455
|
+
return status !== "completed";
|
|
4456
|
+
});
|
|
4457
|
+
if (stillUnfinished.length > 0) {
|
|
4458
|
+
return ok({
|
|
4459
|
+
slug: args.slug,
|
|
4460
|
+
generated,
|
|
4461
|
+
timed_out: true,
|
|
4462
|
+
in_flight_segment_ids: stillUnfinished
|
|
4463
|
+
.map((segment) => segment.id)
|
|
4464
|
+
.filter((id) => id !== undefined),
|
|
4465
|
+
note: "Some scenes are still processing. Re-run generate_all_segments later; it will wait for in-flight scenes and generate only unfinished ones.",
|
|
4466
|
+
});
|
|
4221
4467
|
}
|
|
4222
4468
|
return ok({
|
|
4223
4469
|
slug: args.slug,
|
|
@@ -5018,7 +5264,7 @@ registerTool(
|
|
|
5018
5264
|
{
|
|
5019
5265
|
title: "Set a brand logo overlay (0 credits)",
|
|
5020
5266
|
description:
|
|
5021
|
-
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤
|
|
5267
|
+
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤5 MiB; PNG keeps " +
|
|
5022
5268
|
"transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " +
|
|
5023
5269
|
"position, duration_seconds.",
|
|
5024
5270
|
inputSchema: {
|
package/src/output-schemas.ts
CHANGED
|
@@ -249,6 +249,9 @@ export const createEditorOutput = z
|
|
|
249
249
|
estimated_credits: z.union([z.number(), z.null()]).optional(),
|
|
250
250
|
available_credits: z.union([z.number(), z.null()]).optional(),
|
|
251
251
|
affordable: z.boolean().optional(),
|
|
252
|
+
launched: z.boolean().optional(),
|
|
253
|
+
authorized_credits: z.number().optional(),
|
|
254
|
+
credits_spent: z.number().optional(),
|
|
252
255
|
charged: z.boolean().optional(),
|
|
253
256
|
note: z.string().optional(),
|
|
254
257
|
next: z.string().optional(),
|
|
@@ -269,6 +272,9 @@ export const makeVideoOutput = z
|
|
|
269
272
|
estimated_credits: z.union([z.number(), z.null()]).optional(),
|
|
270
273
|
available_credits: z.union([z.number(), z.null()]).optional(),
|
|
271
274
|
affordable: z.boolean().optional(),
|
|
275
|
+
launched: z.boolean().optional(),
|
|
276
|
+
authorized_credits: z.number().optional(),
|
|
277
|
+
credits_spent: z.number().optional(),
|
|
272
278
|
charged: z.boolean().optional(),
|
|
273
279
|
saved_to: z.union([z.string(), z.null()]).optional(),
|
|
274
280
|
timed_out: z.boolean().optional(),
|
package/src/uploads.ts
CHANGED
|
@@ -63,7 +63,7 @@ const ALLOW_LOOPBACK_FETCH =
|
|
|
63
63
|
const MAX_VIDEO_BYTES = 500_000_000; // EditorUpload.@max_upload_bytes
|
|
64
64
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
65
65
|
export const MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
66
|
-
export const MAX_LOGO_BYTES =
|
|
66
|
+
export const MAX_LOGO_BYTES = 5 * 1024 * 1024; // Editor.Logo.@max_logo_bytes
|
|
67
67
|
export const MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
|
|
68
68
|
export const MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
69
69
|
const MULTIPART_THRESHOLD = 50 * 1024 * 1024; // @multipart_min_size — at/above this, use multipart
|