@hubfluencer/mcp 0.7.1 → 0.8.2
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 +28 -9
- package/dist/index.js +197 -37
- package/package.json +1 -1
- package/src/core.ts +22 -1
- package/src/index.ts +494 -49
- package/src/output-schemas.ts +8 -0
package/src/index.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* One-shot : make_video({ prompt }) → finished MP4 (create→start→poll→download)
|
|
11
11
|
* Shorts : create_short → generate_short → wait_for_completion → download_result
|
|
12
|
+
* (then iterate free: update_short → rerender_short, 0 credits)
|
|
12
13
|
* Editor : create_editor_ad (creates + starts autopilot) → wait_for_completion → download_result
|
|
13
14
|
* Granular : create_editor_draft → generate_scenario|set_scenario → get_editor
|
|
14
|
-
* →
|
|
15
|
+
* → apply_scenario (scenario → scene prompts) | set_scene_count + set_segment_prompt
|
|
16
|
+
* → generate_segment(s) (+ regenerate_segment to re-roll a completed scene)
|
|
15
17
|
* → set_narration_script|generate_narration → generate_voice + generate_music
|
|
16
18
|
* → render → wait_for_completion → download_result
|
|
17
19
|
*/
|
|
@@ -500,7 +502,7 @@ const SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product p
|
|
|
500
502
|
|
|
501
503
|
MAKE ONE ASSET (simplest)
|
|
502
504
|
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_short / create_editor_ad are the per-product one-shots; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
|
|
503
|
-
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario →
|
|
505
|
+
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenario → review/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
|
|
504
506
|
|
|
505
507
|
DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
506
508
|
1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
|
|
@@ -513,7 +515,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
513
515
|
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.
|
|
514
516
|
|
|
515
517
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
516
|
-
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
518
|
+
- 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.
|
|
517
519
|
- 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/create_* report the estimate first — pass dry_run to preview, max_credits to cap.
|
|
518
520
|
- 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.
|
|
519
521
|
|
|
@@ -637,12 +639,20 @@ registerTool(
|
|
|
637
639
|
"for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " +
|
|
638
640
|
"the render is still running — call wait_for_completion with the returned slug to finish. " +
|
|
639
641
|
"BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " +
|
|
640
|
-
"music_vibe so a one-shot short isn't bare.
|
|
642
|
+
"music_vibe so a one-shot short isn't bare. Always set cta_text — a short without a CTA doesn't convert " +
|
|
643
|
+
"(plus offer_text only if there's a real deal). Note: blank cta_text → the server renders a neutral " +
|
|
644
|
+
"localized ask (e.g. 'Learn more'); a truly CTA-less short is not currently supported. " +
|
|
645
|
+
"Keep headline ≈4 words (≤40 chars). Set visual_language " +
|
|
646
|
+
"explicitly (kinetic_creator is a strong default). If headline/subheadline/text_beats are left blank the " +
|
|
647
|
+
"server auto-writes the headline + caption beats at generate time; pass skip_auto_text:true for a " +
|
|
648
|
+
"deliberately bare clip. creative_format + visual_language apply to BOTH kinds (editor and " +
|
|
641
649
|
"shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " +
|
|
642
650
|
"editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " +
|
|
643
651
|
"For richer branding — a product image, brand logo, or " +
|
|
644
652
|
"closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " +
|
|
645
|
-
"create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short."
|
|
653
|
+
"create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short. " +
|
|
654
|
+
"For a SHORT, iterating after the first render is FREE: update_short (copy/style/CTA/end-card) → " +
|
|
655
|
+
"rerender_short re-renders over the already-paid footage for 0 credits.",
|
|
646
656
|
// Schema kept intentionally flat (no .min/.max/.int chains) — the SDK's
|
|
647
657
|
// generic inference on registerTool hits TS2589 ("excessively deep") on
|
|
648
658
|
// larger schemas with chained validators. Ranges are enforced in code.
|
|
@@ -690,9 +700,11 @@ registerTool(
|
|
|
690
700
|
),
|
|
691
701
|
headline: z
|
|
692
702
|
.string()
|
|
703
|
+
.max(80)
|
|
693
704
|
.optional()
|
|
694
705
|
.describe(
|
|
695
|
-
"SHORTS only: the on-screen TITLE overlay
|
|
706
|
+
"SHORTS only: the on-screen TITLE overlay. ≈4 words; ≤40 chars hits hardest; hard cap 80 — " +
|
|
707
|
+
"longer hooks wrap past the safe area. Set this so the short isn't bare.",
|
|
696
708
|
),
|
|
697
709
|
subheadline: z
|
|
698
710
|
.string()
|
|
@@ -723,12 +735,15 @@ registerTool(
|
|
|
723
735
|
cta_text: z
|
|
724
736
|
.string()
|
|
725
737
|
.optional()
|
|
726
|
-
.describe(
|
|
738
|
+
.describe(
|
|
739
|
+
"SHORTS only: CTA pill, e.g. Shop now (≤24 chars). Always set it — a short without a CTA doesn't convert.",
|
|
740
|
+
),
|
|
727
741
|
badge_text: z
|
|
728
742
|
.string()
|
|
729
743
|
.optional()
|
|
730
744
|
.describe(
|
|
731
|
-
"SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)"
|
|
745
|
+
"SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars). Only badges you can " +
|
|
746
|
+
"substantiate — fabricated social proof is deceptive advertising.",
|
|
732
747
|
),
|
|
733
748
|
star_rating: z
|
|
734
749
|
.number()
|
|
@@ -737,7 +752,15 @@ registerTool(
|
|
|
737
752
|
.max(5)
|
|
738
753
|
.optional()
|
|
739
754
|
.describe(
|
|
740
|
-
"SHORTS only: optional 0..5 star rating under the subheadline"
|
|
755
|
+
"SHORTS only: optional 0..5 star rating under the subheadline. Only ratings you can " +
|
|
756
|
+
"substantiate — fabricated social proof is deceptive advertising.",
|
|
757
|
+
),
|
|
758
|
+
skip_auto_text: z
|
|
759
|
+
.boolean()
|
|
760
|
+
.optional()
|
|
761
|
+
.describe(
|
|
762
|
+
"SHORTS only: when headline/subheadline/text_beats are blank the server auto-writes the copy at " +
|
|
763
|
+
"generate time; pass true to skip that and render a deliberately bare clip (default false).",
|
|
741
764
|
),
|
|
742
765
|
text_position: z
|
|
743
766
|
.enum(["top", "center", "bottom"])
|
|
@@ -848,6 +871,7 @@ registerTool(
|
|
|
848
871
|
cta_text?: string;
|
|
849
872
|
badge_text?: string;
|
|
850
873
|
star_rating?: number;
|
|
874
|
+
skip_auto_text?: boolean;
|
|
851
875
|
text_position?: string;
|
|
852
876
|
text_animation?: string;
|
|
853
877
|
font_family?: string;
|
|
@@ -974,6 +998,7 @@ registerTool(
|
|
|
974
998
|
args.creative_format ?? "",
|
|
975
999
|
args.visual_language ?? "",
|
|
976
1000
|
args.theme ?? "",
|
|
1001
|
+
args.skip_auto_text ? "bare" : "auto-copy",
|
|
977
1002
|
),
|
|
978
1003
|
);
|
|
979
1004
|
slug = created.data.slug;
|
|
@@ -1023,7 +1048,7 @@ registerTool(
|
|
|
1023
1048
|
|
|
1024
1049
|
const resume =
|
|
1025
1050
|
kind === "short"
|
|
1026
|
-
? `generate_short({ slug: "${slug}" })`
|
|
1051
|
+
? `generate_short({ slug: "${slug}"${args.skip_auto_text ? ", skip_auto_text: true" : ""} })`
|
|
1027
1052
|
: `start_autopilot({ slug: "${slug}" })`;
|
|
1028
1053
|
const affordable =
|
|
1029
1054
|
available_credits === null ||
|
|
@@ -1080,11 +1105,15 @@ registerTool(
|
|
|
1080
1105
|
if (kind === "short") {
|
|
1081
1106
|
await client.post(
|
|
1082
1107
|
`/shorts/${slug}/generate`,
|
|
1083
|
-
|
|
1108
|
+
// Only send a body when the caller opts out of server auto-copy —
|
|
1109
|
+
// the bare POST stays byte-identical to the pre-0.8.2 wire shape.
|
|
1110
|
+
args.skip_auto_text ? { skip_auto_text: true } : undefined,
|
|
1084
1111
|
// gen-short:<slug> + the failure-state discriminator: a fresh/in-flight
|
|
1085
1112
|
// short reuses "gen-short:<slug>:start" (transport-retry dedupe), a
|
|
1086
1113
|
// re-run after a failed render gets a fresh key and re-renders.
|
|
1087
|
-
|
|
1114
|
+
// skip_auto_text is folded in so a bare-clip run never replays a
|
|
1115
|
+
// cached auto-copy run's response (or vice versa) on the same slug.
|
|
1116
|
+
`gen-short:${slug}:${startState}${args.skip_auto_text ? ":bare" : ""}`,
|
|
1088
1117
|
);
|
|
1089
1118
|
} else {
|
|
1090
1119
|
await client.post(
|
|
@@ -1281,10 +1310,19 @@ registerTool(
|
|
|
1281
1310
|
title: "Create a short (draft)",
|
|
1282
1311
|
description:
|
|
1283
1312
|
"Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
|
|
1284
|
-
"segments + an on-screen title overlay + music; 14s when
|
|
1285
|
-
"costs 0 credits. Follow with generate_short to render. " +
|
|
1313
|
+
"segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " +
|
|
1314
|
+
"Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " +
|
|
1315
|
+
"first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " +
|
|
1316
|
+
"tweak re-renders over them for 0 credits (update_short → rerender_short). " +
|
|
1286
1317
|
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
1287
|
-
"title), and pick a music_vibe plus visual_language that fit the brand.
|
|
1318
|
+
"title), and pick a music_vibe plus visual_language that fit the brand. " +
|
|
1319
|
+
"Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " +
|
|
1320
|
+
"deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " +
|
|
1321
|
+
"CTA-less short is not currently supported. " +
|
|
1322
|
+
"Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " +
|
|
1323
|
+
"default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " +
|
|
1324
|
+
"caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " +
|
|
1325
|
+
"To brand it further, attach a product image " +
|
|
1288
1326
|
"(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " +
|
|
1289
1327
|
"overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
1290
1328
|
inputSchema: {
|
|
@@ -1300,10 +1338,11 @@ registerTool(
|
|
|
1300
1338
|
),
|
|
1301
1339
|
headline: z
|
|
1302
1340
|
.string()
|
|
1303
|
-
.max(
|
|
1341
|
+
.max(80)
|
|
1304
1342
|
.optional()
|
|
1305
1343
|
.describe(
|
|
1306
|
-
"The on-screen TITLE composited over the short (poster text overlay). ≤
|
|
1344
|
+
"The on-screen TITLE composited over the short (poster text overlay). ≈4 words; ≤40 chars hits " +
|
|
1345
|
+
"hardest; hard cap 80 — longer hooks wrap past the safe area. Set this.",
|
|
1307
1346
|
),
|
|
1308
1347
|
subheadline: z
|
|
1309
1348
|
.string()
|
|
@@ -1394,19 +1433,27 @@ registerTool(
|
|
|
1394
1433
|
.string()
|
|
1395
1434
|
.max(24)
|
|
1396
1435
|
.optional()
|
|
1397
|
-
.describe(
|
|
1436
|
+
.describe(
|
|
1437
|
+
'CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn\'t convert.',
|
|
1438
|
+
),
|
|
1398
1439
|
badge_text: z
|
|
1399
1440
|
.string()
|
|
1400
1441
|
.max(24)
|
|
1401
1442
|
.optional()
|
|
1402
|
-
.describe(
|
|
1443
|
+
.describe(
|
|
1444
|
+
'Optional badge stamp, e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' +
|
|
1445
|
+
"proof is deceptive advertising.",
|
|
1446
|
+
),
|
|
1403
1447
|
star_rating: z
|
|
1404
1448
|
.number()
|
|
1405
1449
|
.int()
|
|
1406
1450
|
.min(0)
|
|
1407
1451
|
.max(5)
|
|
1408
1452
|
.optional()
|
|
1409
|
-
.describe(
|
|
1453
|
+
.describe(
|
|
1454
|
+
"Optional 0..5 star rating under the subheadline. Only ratings you can substantiate — fabricated " +
|
|
1455
|
+
"social proof is deceptive advertising.",
|
|
1456
|
+
),
|
|
1410
1457
|
text_beats: z
|
|
1411
1458
|
.array(z.string().max(120))
|
|
1412
1459
|
.max(8)
|
|
@@ -1414,6 +1461,30 @@ registerTool(
|
|
|
1414
1461
|
.describe(
|
|
1415
1462
|
"Optional caption beats shown sequentially instead of the static subheadline.",
|
|
1416
1463
|
),
|
|
1464
|
+
closing_claim: z
|
|
1465
|
+
.string()
|
|
1466
|
+
.max(80)
|
|
1467
|
+
.optional()
|
|
1468
|
+
.describe(
|
|
1469
|
+
"End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " +
|
|
1470
|
+
"claim. Falls back to the headline when unset.",
|
|
1471
|
+
),
|
|
1472
|
+
brand_name: z
|
|
1473
|
+
.string()
|
|
1474
|
+
.max(40)
|
|
1475
|
+
.optional()
|
|
1476
|
+
.describe(
|
|
1477
|
+
"Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " +
|
|
1478
|
+
"poster is set. Server falls back to your brand profile's name when unset.",
|
|
1479
|
+
),
|
|
1480
|
+
end_card: z
|
|
1481
|
+
.enum(["auto", "none"])
|
|
1482
|
+
.optional()
|
|
1483
|
+
.describe(
|
|
1484
|
+
'"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' +
|
|
1485
|
+
'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' +
|
|
1486
|
+
"loop-style shorts).",
|
|
1487
|
+
),
|
|
1417
1488
|
brand_profile_id: z
|
|
1418
1489
|
.number()
|
|
1419
1490
|
.int()
|
|
@@ -1450,6 +1521,9 @@ registerTool(
|
|
|
1450
1521
|
badge_text?: string;
|
|
1451
1522
|
star_rating?: number;
|
|
1452
1523
|
text_beats?: string[];
|
|
1524
|
+
closing_claim?: string;
|
|
1525
|
+
brand_name?: string;
|
|
1526
|
+
end_card?: string;
|
|
1453
1527
|
brand_profile_id?: number;
|
|
1454
1528
|
},
|
|
1455
1529
|
client,
|
|
@@ -1486,6 +1560,10 @@ registerTool(
|
|
|
1486
1560
|
if (args.badge_text !== undefined) body.badge_text = args.badge_text;
|
|
1487
1561
|
if (args.star_rating !== undefined) body.star_rating = args.star_rating;
|
|
1488
1562
|
if (args.text_beats !== undefined) body.text_beats = args.text_beats;
|
|
1563
|
+
if (args.closing_claim !== undefined)
|
|
1564
|
+
body.closing_claim = args.closing_claim;
|
|
1565
|
+
if (args.brand_name !== undefined) body.brand_name = args.brand_name;
|
|
1566
|
+
if (args.end_card !== undefined) body.end_card = args.end_card;
|
|
1489
1567
|
if (args.brand_profile_id !== undefined)
|
|
1490
1568
|
body.brand_profile_id = args.brand_profile_id;
|
|
1491
1569
|
const res = await client.post<{ data: { slug: string } }>(
|
|
@@ -1513,13 +1591,16 @@ registerTool(
|
|
|
1513
1591
|
args.badge_text ?? "",
|
|
1514
1592
|
String(args.star_rating ?? ""),
|
|
1515
1593
|
JSON.stringify(args.text_beats ?? []),
|
|
1594
|
+
args.closing_claim ?? "",
|
|
1595
|
+
args.brand_name ?? "",
|
|
1596
|
+
args.end_card ?? "",
|
|
1516
1597
|
String(args.brand_profile_id ?? ""),
|
|
1517
1598
|
),
|
|
1518
1599
|
);
|
|
1519
1600
|
return ok({
|
|
1520
1601
|
slug: res.data.slug,
|
|
1521
1602
|
kind: "short",
|
|
1522
|
-
next: "set_short_product / set_short_poster (optional branding)
|
|
1603
|
+
next: "set_short_product / set_short_poster (optional branding); generate_hook_variations to test hook angles before you spend; then generate_short. After that first render, hook/CTA/style iteration is free: update_short → rerender_short (0 credits).",
|
|
1523
1604
|
});
|
|
1524
1605
|
},
|
|
1525
1606
|
),
|
|
@@ -1531,8 +1612,19 @@ registerTool(
|
|
|
1531
1612
|
title: "Update a short draft (0 credits)",
|
|
1532
1613
|
description:
|
|
1533
1614
|
"Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " +
|
|
1534
|
-
"0 credits, no re-render
|
|
1615
|
+
"0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " +
|
|
1616
|
+
"(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " +
|
|
1617
|
+
"position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " +
|
|
1618
|
+
"already-generated short? " +
|
|
1619
|
+
"rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " +
|
|
1620
|
+
"image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " +
|
|
1621
|
+
"generate_short (15 credits) again. Use this to edit the brief, copy, " +
|
|
1535
1622
|
"styling, or the optional conversion graphics after create_short. " +
|
|
1623
|
+
"Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " +
|
|
1624
|
+
"deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " +
|
|
1625
|
+
"CTA-less short is not currently supported. " +
|
|
1626
|
+
"Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " +
|
|
1627
|
+
"default). " +
|
|
1536
1628
|
"CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " +
|
|
1537
1629
|
'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' +
|
|
1538
1630
|
"one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " +
|
|
@@ -1556,9 +1648,12 @@ registerTool(
|
|
|
1556
1648
|
),
|
|
1557
1649
|
headline: z
|
|
1558
1650
|
.string()
|
|
1559
|
-
.max(
|
|
1651
|
+
.max(80)
|
|
1560
1652
|
.optional()
|
|
1561
|
-
.describe(
|
|
1653
|
+
.describe(
|
|
1654
|
+
"On-screen TITLE overlay. ≈4 words; ≤40 chars hits hardest; hard cap 80 — longer hooks wrap " +
|
|
1655
|
+
"past the safe area.",
|
|
1656
|
+
),
|
|
1562
1657
|
subheadline: z
|
|
1563
1658
|
.string()
|
|
1564
1659
|
.max(200)
|
|
@@ -1644,13 +1739,16 @@ registerTool(
|
|
|
1644
1739
|
.string()
|
|
1645
1740
|
.max(24)
|
|
1646
1741
|
.optional()
|
|
1647
|
-
.describe(
|
|
1742
|
+
.describe(
|
|
1743
|
+
'CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn\'t convert. Pass "" to remove it.',
|
|
1744
|
+
),
|
|
1648
1745
|
badge_text: z
|
|
1649
1746
|
.string()
|
|
1650
1747
|
.max(24)
|
|
1651
1748
|
.optional()
|
|
1652
1749
|
.describe(
|
|
1653
|
-
'Badge stamp (opt-in), e.g. "BEST SELLER".
|
|
1750
|
+
'Badge stamp (opt-in), e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' +
|
|
1751
|
+
'proof is deceptive advertising. Pass "" to remove it.',
|
|
1654
1752
|
),
|
|
1655
1753
|
star_rating: z
|
|
1656
1754
|
.number()
|
|
@@ -1659,7 +1757,8 @@ registerTool(
|
|
|
1659
1757
|
.max(5)
|
|
1660
1758
|
.optional()
|
|
1661
1759
|
.describe(
|
|
1662
|
-
"0..5 star rating under the subheadline (opt-in).
|
|
1760
|
+
"0..5 star rating under the subheadline (opt-in). Only ratings you can substantiate — fabricated " +
|
|
1761
|
+
"social proof is deceptive advertising. Pass 0 to remove it.",
|
|
1663
1762
|
),
|
|
1664
1763
|
text_beats: z
|
|
1665
1764
|
.array(z.string().max(120))
|
|
@@ -1668,6 +1767,37 @@ registerTool(
|
|
|
1668
1767
|
.describe(
|
|
1669
1768
|
"Caption beats shown sequentially instead of the static subheadline. Pass [] to clear.",
|
|
1670
1769
|
),
|
|
1770
|
+
closing_claim: z
|
|
1771
|
+
.string()
|
|
1772
|
+
.max(80)
|
|
1773
|
+
.optional()
|
|
1774
|
+
.describe(
|
|
1775
|
+
"End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " +
|
|
1776
|
+
'claim. Falls back to the headline when unset. Pass "" to remove it.',
|
|
1777
|
+
),
|
|
1778
|
+
brand_name: z
|
|
1779
|
+
.string()
|
|
1780
|
+
.max(40)
|
|
1781
|
+
.optional()
|
|
1782
|
+
.describe(
|
|
1783
|
+
"Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " +
|
|
1784
|
+
'poster is set. Server falls back to your brand profile\'s name when unset. Pass "" to remove it.',
|
|
1785
|
+
),
|
|
1786
|
+
end_card: z
|
|
1787
|
+
.enum(["auto", "none"])
|
|
1788
|
+
.optional()
|
|
1789
|
+
.describe(
|
|
1790
|
+
'"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' +
|
|
1791
|
+
'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' +
|
|
1792
|
+
"loop-style shorts).",
|
|
1793
|
+
),
|
|
1794
|
+
poster_includes_lockup: z
|
|
1795
|
+
.boolean()
|
|
1796
|
+
.optional()
|
|
1797
|
+
.describe(
|
|
1798
|
+
"Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " +
|
|
1799
|
+
"claim/CTA so they don't collide (default false).",
|
|
1800
|
+
),
|
|
1671
1801
|
brand_profile_id: z
|
|
1672
1802
|
.number()
|
|
1673
1803
|
.int()
|
|
@@ -1709,6 +1839,10 @@ registerTool(
|
|
|
1709
1839
|
badge_text?: string;
|
|
1710
1840
|
star_rating?: number;
|
|
1711
1841
|
text_beats?: string[];
|
|
1842
|
+
closing_claim?: string;
|
|
1843
|
+
brand_name?: string;
|
|
1844
|
+
end_card?: string;
|
|
1845
|
+
poster_includes_lockup?: boolean;
|
|
1712
1846
|
brand_profile_id?: number;
|
|
1713
1847
|
},
|
|
1714
1848
|
client,
|
|
@@ -1749,6 +1883,12 @@ registerTool(
|
|
|
1749
1883
|
if (args.badge_text !== undefined) body.badge_text = args.badge_text;
|
|
1750
1884
|
if (args.star_rating !== undefined) body.star_rating = args.star_rating;
|
|
1751
1885
|
if (args.text_beats !== undefined) body.text_beats = args.text_beats;
|
|
1886
|
+
if (args.closing_claim !== undefined)
|
|
1887
|
+
body.closing_claim = args.closing_claim;
|
|
1888
|
+
if (args.brand_name !== undefined) body.brand_name = args.brand_name;
|
|
1889
|
+
if (args.end_card !== undefined) body.end_card = args.end_card;
|
|
1890
|
+
if (args.poster_includes_lockup !== undefined)
|
|
1891
|
+
body.poster_includes_lockup = args.poster_includes_lockup;
|
|
1752
1892
|
if (args.brand_profile_id !== undefined)
|
|
1753
1893
|
body.brand_profile_id = args.brand_profile_id;
|
|
1754
1894
|
|
|
@@ -1756,7 +1896,15 @@ registerTool(
|
|
|
1756
1896
|
`/shorts/${args.slug}`,
|
|
1757
1897
|
body,
|
|
1758
1898
|
);
|
|
1759
|
-
|
|
1899
|
+
const data = asRecord(res).data ?? res;
|
|
1900
|
+
return ok({
|
|
1901
|
+
...asRecord(data),
|
|
1902
|
+
next:
|
|
1903
|
+
"Saved. Only text/style/CTA/end-card changed on an already-generated short? " +
|
|
1904
|
+
`rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` +
|
|
1905
|
+
"(prompt, product image, format, visual_language, theme, music_*, language) need " +
|
|
1906
|
+
"generate_short (15 credits).",
|
|
1907
|
+
});
|
|
1760
1908
|
},
|
|
1761
1909
|
),
|
|
1762
1910
|
);
|
|
@@ -2132,16 +2280,30 @@ registerTool(
|
|
|
2132
2280
|
{
|
|
2133
2281
|
title: "Generate (render) a short",
|
|
2134
2282
|
description:
|
|
2135
|
-
"Deducts 15 credits and starts the render pipeline for an existing short
|
|
2283
|
+
"Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " +
|
|
2284
|
+
"and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " +
|
|
2285
|
+
"use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " +
|
|
2286
|
+
"Safe to call again: a " +
|
|
2136
2287
|
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
2137
|
-
"short can be re-generated.
|
|
2138
|
-
|
|
2288
|
+
"short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " +
|
|
2289
|
+
"auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " +
|
|
2290
|
+
"bare clip). Then poll with get_status or wait_for_completion (kind=short).",
|
|
2291
|
+
inputSchema: {
|
|
2292
|
+
slug: z.string().describe("Short slug from create_short"),
|
|
2293
|
+
skip_auto_text: z
|
|
2294
|
+
.boolean()
|
|
2295
|
+
.optional()
|
|
2296
|
+
.describe(
|
|
2297
|
+
"When the draft has no headline/subheadline/text_beats the server auto-writes the copy; pass " +
|
|
2298
|
+
"true to skip that and render a deliberately bare clip (default false).",
|
|
2299
|
+
),
|
|
2300
|
+
},
|
|
2139
2301
|
// Not idempotent: a repeat after a terminal (failed) render re-charges 15
|
|
2140
2302
|
// credits (the whole point of allowing a re-generation), so a blind retry is
|
|
2141
2303
|
// NOT free — only an in-flight duplicate dedupes (server 409). See below.
|
|
2142
2304
|
annotations: { title: "Generate short", ...WRITE },
|
|
2143
2305
|
},
|
|
2144
|
-
tool(async (args: { slug: string }, client) => {
|
|
2306
|
+
tool(async (args: { slug: string; skip_auto_text?: boolean }, client) => {
|
|
2145
2307
|
// No stable idempotency key (see generate_slider): a per-slug key would
|
|
2146
2308
|
// replay the first response for 24h and block an intentional re-generation
|
|
2147
2309
|
// of a failed short. Double-charge is prevented server-side — the batch
|
|
@@ -2150,6 +2312,9 @@ registerTool(
|
|
|
2150
2312
|
try {
|
|
2151
2313
|
const res = await client.post<{ data: unknown }>(
|
|
2152
2314
|
`/shorts/${args.slug}/generate`,
|
|
2315
|
+
// Only send a body when the caller opts out of server auto-copy —
|
|
2316
|
+
// the bare POST stays byte-identical to the pre-0.8.2 wire shape.
|
|
2317
|
+
args.skip_auto_text ? { skip_auto_text: true } : undefined,
|
|
2153
2318
|
);
|
|
2154
2319
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
2155
2320
|
return ok(status);
|
|
@@ -2170,6 +2335,89 @@ registerTool(
|
|
|
2170
2335
|
}),
|
|
2171
2336
|
);
|
|
2172
2337
|
|
|
2338
|
+
registerTool(
|
|
2339
|
+
"rerender_short",
|
|
2340
|
+
{
|
|
2341
|
+
title: "Re-render a short (FREE — 0 credits)",
|
|
2342
|
+
description:
|
|
2343
|
+
"FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
|
|
2344
|
+
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " +
|
|
2345
|
+
"changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
|
|
2346
|
+
"end_card) can shift the video between 12s and 14s; the pinned music bed is faded to fit rather than " +
|
|
2347
|
+
"regenerated — for perfectly synced music after such a change, use generate_short. " +
|
|
2348
|
+
"Requires a completed generation to re-render over " +
|
|
2349
|
+
"(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " +
|
|
2350
|
+
"flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " +
|
|
2351
|
+
"wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " +
|
|
2352
|
+
"rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " +
|
|
2353
|
+
"report this when the server sends them).",
|
|
2354
|
+
inputSchema: {
|
|
2355
|
+
slug: z.string().describe("Short slug (from create_short)"),
|
|
2356
|
+
},
|
|
2357
|
+
// Free, but not idempotent in the MCP sense: each call is a genuine new
|
|
2358
|
+
// re-render of the short's CURRENT state — the whole point of the
|
|
2359
|
+
// update_short → rerender_short loop (see the key note below).
|
|
2360
|
+
annotations: { title: "Re-render short", ...WRITE, idempotentHint: false },
|
|
2361
|
+
},
|
|
2362
|
+
tool(async (args: { slug: string }, client) => {
|
|
2363
|
+
try {
|
|
2364
|
+
const res = await client.post<{ data: unknown }>(
|
|
2365
|
+
`/shorts/${args.slug}/rerender`,
|
|
2366
|
+
undefined,
|
|
2367
|
+
// Each call is a genuine new re-render attempt (the core loop is
|
|
2368
|
+
// update_short → rerender_short, repeated per variant). A per-invocation
|
|
2369
|
+
// nonce keeps the server's 24h idempotency cache from replaying a PRIOR
|
|
2370
|
+
// rerender's response on a deliberate re-application after an edit.
|
|
2371
|
+
// (The client never auto-retries non-GETs, so the nonce buys no
|
|
2372
|
+
// transport-retry protection — it exists purely to defeat the replay
|
|
2373
|
+
// cache.) A concurrent duplicate is independently blocked server-side
|
|
2374
|
+
// (409 while any work is active) — and the call is 0 credits, so there
|
|
2375
|
+
// is no charge to protect. Mirrors start_autopilot's
|
|
2376
|
+
// idemKey("autopilot", slug, randomUUID()).
|
|
2377
|
+
idemKey("rerender-short", args.slug, randomUUID()),
|
|
2378
|
+
);
|
|
2379
|
+
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
2380
|
+
return ok({
|
|
2381
|
+
...status,
|
|
2382
|
+
next: `wait_for_completion({ slug: "${args.slug}", kind: "short" }) — or poll get_status. A re-render is fast (no AI generation).`,
|
|
2383
|
+
});
|
|
2384
|
+
} catch (e) {
|
|
2385
|
+
const he = e as { status?: number; code?: string };
|
|
2386
|
+
if (he.status === 409) {
|
|
2387
|
+
// A generation/render is already in flight — report in-progress
|
|
2388
|
+
// (same shape as generate_short's 409 handling), never a hard error.
|
|
2389
|
+
return ok({
|
|
2390
|
+
kind: "short",
|
|
2391
|
+
slug: args.slug,
|
|
2392
|
+
stage: "processing",
|
|
2393
|
+
terminal: false,
|
|
2394
|
+
ready: false,
|
|
2395
|
+
video_url: null,
|
|
2396
|
+
error: null,
|
|
2397
|
+
next: `A render is already running — wait_for_completion({ slug: "${args.slug}", kind: "short" }).`,
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
if (he.status === 422 && he.code === "short_not_ready") {
|
|
2401
|
+
// Nothing rendered yet to re-render over: the free path needs the
|
|
2402
|
+
// paid footage+music to exist first.
|
|
2403
|
+
return fail(
|
|
2404
|
+
"This short has no completed footage + music to re-render over (short_not_ready). " +
|
|
2405
|
+
`Run generate_short({ slug: "${args.slug}" }) once (15 credits) — after that first render, every ` +
|
|
2406
|
+
"text/style/CTA/end-card tweak re-renders free with rerender_short.",
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (he.status === 422 && he.code === "short_paid_regeneration_required") {
|
|
2410
|
+
return fail(
|
|
2411
|
+
"A footage or music input changed after the last paid render " +
|
|
2412
|
+
"(short_paid_regeneration_required). The pinned assets no longer match the draft. " +
|
|
2413
|
+
`Run generate_short({ slug: "${args.slug}" }) (15 credits) before using free re-renders again.`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
throw e;
|
|
2417
|
+
}
|
|
2418
|
+
}),
|
|
2419
|
+
);
|
|
2420
|
+
|
|
2173
2421
|
registerTool(
|
|
2174
2422
|
"generate_short_text",
|
|
2175
2423
|
{
|
|
@@ -2578,7 +2826,9 @@ registerTool(
|
|
|
2578
2826
|
"BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " +
|
|
2579
2827
|
"create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " +
|
|
2580
2828
|
"start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " +
|
|
2581
|
-
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)"
|
|
2829
|
+
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " +
|
|
2830
|
+
"For product_only or presenter_led cast modes, use create_editor_draft and configure the required " +
|
|
2831
|
+
"product/presenter before start_autopilot; this one-shot tool intentionally uses automatic casting.",
|
|
2582
2832
|
inputSchema: {
|
|
2583
2833
|
product_prompt: z
|
|
2584
2834
|
.string()
|
|
@@ -2826,7 +3076,8 @@ registerTool(
|
|
|
2826
3076
|
// ad with full control (review the scenario, write its own prompts, etc.)
|
|
2827
3077
|
// instead of one-shot autopilot (create_editor_ad / make_video).
|
|
2828
3078
|
// create_editor_draft → generate_scenario|set_scenario → get_editor (REVIEW)
|
|
2829
|
-
// →
|
|
3079
|
+
// → apply_scenario (scenario → per-scene prompts) | set_scene_count + set_segment_prompt
|
|
3080
|
+
// → generate_segment|generate_all_segments (+ regenerate_segment for completed scenes)
|
|
2830
3081
|
// → set_narration_script|generate_narration → generate_voice + generate_music
|
|
2831
3082
|
// → render → wait_for_completion(kind:"editor") → download_result.
|
|
2832
3083
|
|
|
@@ -2877,6 +3128,17 @@ registerTool(
|
|
|
2877
3128
|
.describe(
|
|
2878
3129
|
'Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
2879
3130
|
),
|
|
3131
|
+
cast_mode: z
|
|
3132
|
+
.enum(["product_only", "presenter_led"])
|
|
3133
|
+
.optional()
|
|
3134
|
+
.describe(
|
|
3135
|
+
"Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " +
|
|
3136
|
+
"product image (set_product) before generation and generates every scene from persistent product " +
|
|
3137
|
+
"reference images — the best product fidelity. presenter_led = one locked presenter appears in " +
|
|
3138
|
+
"every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " +
|
|
3139
|
+
"— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " +
|
|
3140
|
+
"behavior.",
|
|
3141
|
+
),
|
|
2880
3142
|
voice_id: z
|
|
2881
3143
|
.string()
|
|
2882
3144
|
.regex(/^[A-Za-z0-9_-]+$/)
|
|
@@ -2913,6 +3175,7 @@ registerTool(
|
|
|
2913
3175
|
creative_format?: string;
|
|
2914
3176
|
visual_language?: string;
|
|
2915
3177
|
theme?: string;
|
|
3178
|
+
cast_mode?: string;
|
|
2916
3179
|
voice_id?: string;
|
|
2917
3180
|
export_aspect_ratio?: string;
|
|
2918
3181
|
project_intent?: string;
|
|
@@ -2926,6 +3189,7 @@ registerTool(
|
|
|
2926
3189
|
creative_format: args.creative_format,
|
|
2927
3190
|
visual_language: args.visual_language,
|
|
2928
3191
|
theme: args.theme,
|
|
3192
|
+
cast_mode: args.cast_mode,
|
|
2929
3193
|
voice_id: args.voice_id,
|
|
2930
3194
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
2931
3195
|
project_intent: args.project_intent,
|
|
@@ -2948,12 +3212,13 @@ registerTool(
|
|
|
2948
3212
|
args.theme ?? "",
|
|
2949
3213
|
args.voice_id ?? "",
|
|
2950
3214
|
args.export_aspect_ratio ?? "",
|
|
3215
|
+
args.cast_mode ?? "",
|
|
2951
3216
|
),
|
|
2952
3217
|
);
|
|
2953
3218
|
return ok({
|
|
2954
3219
|
slug: created.data.slug,
|
|
2955
3220
|
kind: "editor",
|
|
2956
|
-
next: "generate_scenario | set_scenario, then get_editor to review",
|
|
3221
|
+
next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario",
|
|
2957
3222
|
});
|
|
2958
3223
|
},
|
|
2959
3224
|
),
|
|
@@ -3011,7 +3276,10 @@ registerTool(
|
|
|
3011
3276
|
"(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " +
|
|
3012
3277
|
"write the scenario yourself with set_scenario. Server default segments_count is 5. " +
|
|
3013
3278
|
"Optionally pass creative_format / visual_language / theme to persist the project\u2019s creative style before " +
|
|
3014
|
-
"generating (this is the granular path\u2019s way to set/change style; PATCH /scenario does not)."
|
|
3279
|
+
"generating (this is the granular path\u2019s way to set/change style; PATCH /scenario does not). " +
|
|
3280
|
+
"Generation is ASYNC: get_editor shows scenario_status generating \u2192 ready (a few seconds). NEXT STEP: " +
|
|
3281
|
+
"once ready, review the scenario with get_editor, then call apply_scenario to turn it into per-scene " +
|
|
3282
|
+
"segment prompts on the timeline.",
|
|
3015
3283
|
inputSchema: {
|
|
3016
3284
|
slug: z.string().describe("Editor project slug"),
|
|
3017
3285
|
segments_count: z
|
|
@@ -3083,6 +3351,111 @@ registerTool(
|
|
|
3083
3351
|
),
|
|
3084
3352
|
);
|
|
3085
3353
|
|
|
3354
|
+
registerTool(
|
|
3355
|
+
"apply_scenario",
|
|
3356
|
+
{
|
|
3357
|
+
title: "Apply the scenario to the timeline (per-scene prompts)",
|
|
3358
|
+
description:
|
|
3359
|
+
"Turns the READY scenario into per-scene segment prompts on the timeline — the missing step between " +
|
|
3360
|
+
"generate_scenario and generate_segment in the granular flow. Existing AI segments are REPLACED with " +
|
|
3361
|
+
"freshly generated scene prompts (uploaded clips are preserved); segments_count 0 removes the AI scenes " +
|
|
3362
|
+
"and keeps the scenario only. Free — no credits, no AI assist (rate limit 15/min). Requires " +
|
|
3363
|
+
'scenario_status "ready" (422 scenario_not_ready otherwise): after generate_scenario wait until ' +
|
|
3364
|
+
"get_editor shows ready (a few seconds); set_scenario is ready immediately. The apply itself runs async " +
|
|
3365
|
+
"server-side — this tool polls get_editor (bounded ~2 min) until scenario_apply_status leaves " +
|
|
3366
|
+
'"applying" and returns the materialized prompts; if it reports timed_out, keep polling get_editor ' +
|
|
3367
|
+
"yourself. Then review/edit with set_segment_prompt and generate with generate_segment / " +
|
|
3368
|
+
"generate_all_segments (or just render).",
|
|
3369
|
+
inputSchema: {
|
|
3370
|
+
slug: z.string().describe("Editor project slug"),
|
|
3371
|
+
segments_count: z
|
|
3372
|
+
.union([z.literal(0), z.number().int().min(3).max(10)])
|
|
3373
|
+
.optional()
|
|
3374
|
+
.describe(
|
|
3375
|
+
"How many AI scenes to create on the timeline — 0 or any integer from 3 to 10. Omit to use " +
|
|
3376
|
+
"the scene count saved by generate_scenario (or 5 when none is stored). " +
|
|
3377
|
+
"0 keeps the scenario only and preserves manual timeline control.",
|
|
3378
|
+
),
|
|
3379
|
+
},
|
|
3380
|
+
annotations: { title: "Apply scenario", ...WRITE, idempotentHint: false },
|
|
3381
|
+
},
|
|
3382
|
+
tool(
|
|
3383
|
+
async (args: { slug: string; segments_count?: number }, client, extra) => {
|
|
3384
|
+
const body: Record<string, unknown> = {};
|
|
3385
|
+
if (args.segments_count !== undefined)
|
|
3386
|
+
body.segments_count = args.segments_count;
|
|
3387
|
+
const res = await client.post<{ data: unknown }>(
|
|
3388
|
+
`/editor/${args.slug}/apply-scenario`,
|
|
3389
|
+
body,
|
|
3390
|
+
);
|
|
3391
|
+
const accepted = asRecord(asRecord(res).data);
|
|
3392
|
+
|
|
3393
|
+
// The 202 only STARTS the apply — the controller flips
|
|
3394
|
+
// scenario_apply_status to "applying" before responding, and an async
|
|
3395
|
+
// worker compiles the scenario into per-scene prompts. Poll the project
|
|
3396
|
+
// until the status leaves "applying" ("idle" = success, "failed" =
|
|
3397
|
+
// terminal) so the agent gets the materialized prompts back instead of
|
|
3398
|
+
// having to poll itself. The budget is far below the render loops' 280s
|
|
3399
|
+
// ceiling: an apply is a single LLM call (typically seconds).
|
|
3400
|
+
const APPLY_BUDGET_MS = 120_000;
|
|
3401
|
+
const APPLY_POLL_MS = 2_500;
|
|
3402
|
+
const deadline = Date.now() + APPLY_BUDGET_MS;
|
|
3403
|
+
const readEditor = async () => {
|
|
3404
|
+
// Bound each poll GET's retries by the loop deadline (see fetchStatus).
|
|
3405
|
+
const r = await client.get<{ data: unknown }>(
|
|
3406
|
+
`/editor/${args.slug}`,
|
|
3407
|
+
undefined,
|
|
3408
|
+
deadline,
|
|
3409
|
+
);
|
|
3410
|
+
return asRecord(asRecord(r).data);
|
|
3411
|
+
};
|
|
3412
|
+
let n = 0;
|
|
3413
|
+
let editor = await readEditor();
|
|
3414
|
+
while (
|
|
3415
|
+
editor.scenario_apply_status === "applying" &&
|
|
3416
|
+
Date.now() + APPLY_POLL_MS <= deadline
|
|
3417
|
+
) {
|
|
3418
|
+
await reportProgress(extra, ++n, "applying scenario to timeline");
|
|
3419
|
+
await sleep(APPLY_POLL_MS);
|
|
3420
|
+
editor = await readEditor();
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
if (editor.scenario_apply_status === "applying") {
|
|
3424
|
+
return ok({
|
|
3425
|
+
slug: args.slug,
|
|
3426
|
+
applied: false,
|
|
3427
|
+
timed_out: true,
|
|
3428
|
+
note: 'The apply is still running server-side. Poll get_editor until scenario_apply_status is no longer "applying", then review the scene prompts.',
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
if (editor.scenario_apply_status === "failed") {
|
|
3432
|
+
return fail(
|
|
3433
|
+
"Scenario apply failed server-side. Re-run apply_scenario to retry, or build the timeline yourself with set_scene_count + set_segment_prompt.",
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
const segments = Array.isArray(editor.segments)
|
|
3437
|
+
? (editor.segments as Record<string, unknown>[])
|
|
3438
|
+
: [];
|
|
3439
|
+
return ok({
|
|
3440
|
+
slug: args.slug,
|
|
3441
|
+
applied: true,
|
|
3442
|
+
ai_segments_replaced: accepted.ai_segments_replaced,
|
|
3443
|
+
uploads_preserved: accepted.uploads_preserved,
|
|
3444
|
+
target_ai_segments: accepted.target_ai_segments,
|
|
3445
|
+
scene_count: segments.length,
|
|
3446
|
+
segments: segments.map((s) => ({
|
|
3447
|
+
id: s.id,
|
|
3448
|
+
position: s.position,
|
|
3449
|
+
source_type: s.source_type,
|
|
3450
|
+
status: s.status,
|
|
3451
|
+
prompt: s.prompt,
|
|
3452
|
+
})),
|
|
3453
|
+
next: "Review/edit prompts (set_segment_prompt), then generate_segment / generate_all_segments — or just render (it auto-charges ungenerated scenes at the batch rate)",
|
|
3454
|
+
});
|
|
3455
|
+
},
|
|
3456
|
+
),
|
|
3457
|
+
);
|
|
3458
|
+
|
|
3086
3459
|
registerTool(
|
|
3087
3460
|
"set_scene_count",
|
|
3088
3461
|
{
|
|
@@ -3214,8 +3587,10 @@ registerTool(
|
|
|
3214
3587
|
{
|
|
3215
3588
|
title: "Generate one segment (5 credits)",
|
|
3216
3589
|
description:
|
|
3217
|
-
"Renders the video for one scene. Each AI-generated scene is a fixed 8 seconds long.
|
|
3218
|
-
"Re-call to retry a failed scene or
|
|
3590
|
+
"Renders the video for one PENDING or FAILED scene. Each AI-generated scene is a fixed 8 seconds long. " +
|
|
3591
|
+
"Costs 5 credits. Re-call to retry a failed scene or after a prompt edit — the server blocks one that's " +
|
|
3592
|
+
"still processing (editor_segment_busy), so no double-charge; a COMPLETED scene returns " +
|
|
3593
|
+
"editor_segment_completed — re-roll those with regenerate_segment instead. Visual " +
|
|
3219
3594
|
"continuity is handled server-side (this scene uses the previous scene's last frame). 402 " +
|
|
3220
3595
|
"credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
|
|
3221
3596
|
inputSchema: {
|
|
@@ -3236,6 +3611,54 @@ registerTool(
|
|
|
3236
3611
|
}),
|
|
3237
3612
|
);
|
|
3238
3613
|
|
|
3614
|
+
registerTool(
|
|
3615
|
+
"regenerate_segment",
|
|
3616
|
+
{
|
|
3617
|
+
title: "Regenerate a completed segment (new version, 4 credits)",
|
|
3618
|
+
description:
|
|
3619
|
+
"Re-rolls a COMPLETED scene as a NEW VERSION of that segment — the previous version is kept (switch back " +
|
|
3620
|
+
"in the app). Costs 4 credits. Use generate_segment for pending/failed scenes; use THIS for a completed " +
|
|
3621
|
+
"scene you want different — optionally with a new prompt (omit it to re-roll the current prompt). The new " +
|
|
3622
|
+
"version starts rendering immediately; watch it with get_editor (status processing → completed|failed). " +
|
|
3623
|
+
"KNOWN LIMITATION: scenes chain visual continuity off the previous scene's last frame, so regenerating a " +
|
|
3624
|
+
"MID-TIMELINE scene can degrade the transition into the FOLLOWING scene (it was seeded from the old " +
|
|
3625
|
+
"version's last frame). A scene still processing returns editor_segment_busy (no double charge); 402 " +
|
|
3626
|
+
"credits_insufficient fails cleanly.",
|
|
3627
|
+
inputSchema: {
|
|
3628
|
+
slug: z.string().describe("Editor project slug"),
|
|
3629
|
+
segment_id: z
|
|
3630
|
+
.union([z.number(), z.string()])
|
|
3631
|
+
.describe("Segment id (from get_editor)"),
|
|
3632
|
+
prompt: z
|
|
3633
|
+
.string()
|
|
3634
|
+
.min(1)
|
|
3635
|
+
.max(2000)
|
|
3636
|
+
.optional()
|
|
3637
|
+
.describe(
|
|
3638
|
+
"Optional new scene prompt for the regenerated version (1–2000 chars); omit to reuse the current prompt",
|
|
3639
|
+
),
|
|
3640
|
+
},
|
|
3641
|
+
annotations: {
|
|
3642
|
+
title: "Regenerate segment",
|
|
3643
|
+
...WRITE,
|
|
3644
|
+
idempotentHint: false,
|
|
3645
|
+
},
|
|
3646
|
+
},
|
|
3647
|
+
tool(
|
|
3648
|
+
async (
|
|
3649
|
+
args: { slug: string; segment_id: number | string; prompt?: string },
|
|
3650
|
+
client,
|
|
3651
|
+
) => {
|
|
3652
|
+
const res = await client.post<{ data: unknown }>(
|
|
3653
|
+
`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`,
|
|
3654
|
+
args.prompt !== undefined ? { prompt: args.prompt } : undefined,
|
|
3655
|
+
undefined,
|
|
3656
|
+
);
|
|
3657
|
+
return ok(asRecord(res).data ?? res);
|
|
3658
|
+
},
|
|
3659
|
+
),
|
|
3660
|
+
);
|
|
3661
|
+
|
|
3239
3662
|
registerTool(
|
|
3240
3663
|
"generate_all_segments",
|
|
3241
3664
|
{
|
|
@@ -4200,21 +4623,43 @@ registerTool(
|
|
|
4200
4623
|
inputSchema: {
|
|
4201
4624
|
slug: z.string().describe("Short slug (from create_short)"),
|
|
4202
4625
|
file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
|
4626
|
+
poster_includes_lockup: z
|
|
4627
|
+
.boolean()
|
|
4628
|
+
.optional()
|
|
4629
|
+
.describe(
|
|
4630
|
+
"Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " +
|
|
4631
|
+
"claim/CTA so they don't collide (default false).",
|
|
4632
|
+
),
|
|
4203
4633
|
},
|
|
4204
4634
|
annotations: { title: "Set short poster", ...WRITE, idempotentHint: false },
|
|
4205
4635
|
},
|
|
4206
|
-
tool(
|
|
4207
|
-
|
|
4636
|
+
tool(
|
|
4637
|
+
async (
|
|
4638
|
+
args: {
|
|
4639
|
+
slug: string;
|
|
4640
|
+
file_path: string;
|
|
4641
|
+
poster_includes_lockup?: boolean;
|
|
4642
|
+
},
|
|
4208
4643
|
client,
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4644
|
+
) => {
|
|
4645
|
+
const { s3_key } = await uploadShortPoster(
|
|
4646
|
+
client,
|
|
4647
|
+
args.slug,
|
|
4648
|
+
args.file_path,
|
|
4649
|
+
);
|
|
4650
|
+
const res = await client.post<{ data: unknown }>(
|
|
4651
|
+
`/shorts/${args.slug}/poster/confirm`,
|
|
4652
|
+
{
|
|
4653
|
+
s3_key,
|
|
4654
|
+
// A new poster replaces the old poster's content contract too:
|
|
4655
|
+
// omission means the declared default (overlays enabled), not
|
|
4656
|
+
// "preserve whatever the previous poster said".
|
|
4657
|
+
poster_includes_lockup: args.poster_includes_lockup ?? false,
|
|
4658
|
+
},
|
|
4659
|
+
);
|
|
4660
|
+
return ok(asRecord(res).data ?? res);
|
|
4661
|
+
},
|
|
4662
|
+
),
|
|
4218
4663
|
);
|
|
4219
4664
|
|
|
4220
4665
|
// ── Status / waiting ──────────────────────────────────────────────────────────
|