@hubfluencer/mcp 0.2.0 → 0.4.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 -3
- package/dist/index.js +449 -25
- package/package.json +1 -1
- package/src/index.ts +932 -32
- package/src/uploads.ts +25 -0
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { runLogin } from "./login.js";
|
|
|
39
39
|
import {
|
|
40
40
|
IMAGE_EXTS,
|
|
41
41
|
uploadImageFile,
|
|
42
|
+
uploadShortPoster,
|
|
42
43
|
uploadVideoFile,
|
|
43
44
|
VIDEO_EXTS,
|
|
44
45
|
} from "./uploads.js";
|
|
@@ -323,7 +324,7 @@ const WRITE = {
|
|
|
323
324
|
openWorldHint: true,
|
|
324
325
|
};
|
|
325
326
|
|
|
326
|
-
const server = new McpServer({ name: "hubfluencer", version: "0.
|
|
327
|
+
const server = new McpServer({ name: "hubfluencer", version: "0.3.0" });
|
|
327
328
|
|
|
328
329
|
// The SDK's `registerTool` is generic over the Zod input shape; with this many
|
|
329
330
|
// tools its conditional types hit TS2589 ("excessively deep"). Inputs are still
|
|
@@ -424,7 +425,12 @@ registerTool(
|
|
|
424
425
|
"WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " +
|
|
425
426
|
"spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " +
|
|
426
427
|
"for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " +
|
|
427
|
-
"the render is still running — call wait_for_completion with the returned slug to finish."
|
|
428
|
+
"the render is still running — call wait_for_completion with the returned slug to finish. " +
|
|
429
|
+
"BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " +
|
|
430
|
+
"music_vibe/visual_language so a one-shot short isn't bare — these apply to SHORTS (music_vibe/headline/subheadline " +
|
|
431
|
+
"are ignored for editor; theme applies to editor and is legacy-only for shorts). For richer branding — a product image, brand logo, or " +
|
|
432
|
+
"closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " +
|
|
433
|
+
"create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
|
|
428
434
|
// Schema kept intentionally flat (no .min/.max/.int chains) — the SDK's
|
|
429
435
|
// generic inference on registerTool hits TS2589 ("excessively deep") on
|
|
430
436
|
// larger schemas with chained validators. Ranges are enforced in code.
|
|
@@ -452,6 +458,115 @@ registerTool(
|
|
|
452
458
|
.describe(
|
|
453
459
|
"Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover",
|
|
454
460
|
),
|
|
461
|
+
headline: z
|
|
462
|
+
.string()
|
|
463
|
+
.optional()
|
|
464
|
+
.describe(
|
|
465
|
+
"SHORTS only: the on-screen TITLE overlay (≤160). Set this so the short isn't bare.",
|
|
466
|
+
),
|
|
467
|
+
subheadline: z
|
|
468
|
+
.string()
|
|
469
|
+
.optional()
|
|
470
|
+
.describe("SHORTS only: the secondary title / supporting line (≤200)"),
|
|
471
|
+
text_beats: z
|
|
472
|
+
.array(z.string())
|
|
473
|
+
.optional()
|
|
474
|
+
.describe(
|
|
475
|
+
"SHORTS only: caption beats shown sequentially instead of a static subheadline (≤8, each ≤120 chars).",
|
|
476
|
+
),
|
|
477
|
+
headline_color: z
|
|
478
|
+
.string()
|
|
479
|
+
.optional()
|
|
480
|
+
.describe("SHORTS only: headline hex color, e.g. #ffffff"),
|
|
481
|
+
subheadline_color: z
|
|
482
|
+
.string()
|
|
483
|
+
.optional()
|
|
484
|
+
.describe("SHORTS only: subheadline hex color, e.g. #ffffff"),
|
|
485
|
+
accent_color: z
|
|
486
|
+
.string()
|
|
487
|
+
.optional()
|
|
488
|
+
.describe("SHORTS only: 6-digit brand accent hex color, e.g. #09EFBE"),
|
|
489
|
+
offer_text: z
|
|
490
|
+
.string()
|
|
491
|
+
.optional()
|
|
492
|
+
.describe("SHORTS only: optional offer chip, e.g. -40% (≤16 chars)"),
|
|
493
|
+
cta_text: z
|
|
494
|
+
.string()
|
|
495
|
+
.optional()
|
|
496
|
+
.describe("SHORTS only: optional CTA pill, e.g. Shop now (≤24 chars)"),
|
|
497
|
+
badge_text: z
|
|
498
|
+
.string()
|
|
499
|
+
.optional()
|
|
500
|
+
.describe(
|
|
501
|
+
"SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)",
|
|
502
|
+
),
|
|
503
|
+
star_rating: z
|
|
504
|
+
.number()
|
|
505
|
+
.optional()
|
|
506
|
+
.describe(
|
|
507
|
+
"SHORTS only: optional 0..5 star rating under the subheadline",
|
|
508
|
+
),
|
|
509
|
+
text_position: z
|
|
510
|
+
.enum(["top", "center", "bottom"])
|
|
511
|
+
.optional()
|
|
512
|
+
.describe("SHORTS only: title overlay position (default bottom)"),
|
|
513
|
+
text_animation: z
|
|
514
|
+
.enum([
|
|
515
|
+
"reveal",
|
|
516
|
+
"typewriter",
|
|
517
|
+
"fade_in",
|
|
518
|
+
"pop",
|
|
519
|
+
"bounce",
|
|
520
|
+
"word_stagger",
|
|
521
|
+
"word_spotlight",
|
|
522
|
+
])
|
|
523
|
+
.optional()
|
|
524
|
+
.describe("SHORTS only: title overlay animation (default fade_in)"),
|
|
525
|
+
font_family: z
|
|
526
|
+
.string()
|
|
527
|
+
.optional()
|
|
528
|
+
.describe(
|
|
529
|
+
"SHORTS only: overlay font family, e.g. ShortFontSpaceGrotesk",
|
|
530
|
+
),
|
|
531
|
+
music_vibe: z
|
|
532
|
+
.string()
|
|
533
|
+
.optional()
|
|
534
|
+
.describe(
|
|
535
|
+
"SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz",
|
|
536
|
+
),
|
|
537
|
+
creative_format: z
|
|
538
|
+
.enum([
|
|
539
|
+
"problem_solution",
|
|
540
|
+
"mistake_fix",
|
|
541
|
+
"myth_vs_reality",
|
|
542
|
+
"before_after",
|
|
543
|
+
"proof_demo",
|
|
544
|
+
"product_reveal",
|
|
545
|
+
])
|
|
546
|
+
.optional()
|
|
547
|
+
.describe("SHORTS only: segment structure. Omit for Auto/generic."),
|
|
548
|
+
visual_language: z
|
|
549
|
+
.enum([
|
|
550
|
+
"kinetic_creator",
|
|
551
|
+
"premium_editorial",
|
|
552
|
+
"cinematic_product",
|
|
553
|
+
"ugc_realism",
|
|
554
|
+
"startup_explainer",
|
|
555
|
+
"luxury_minimal",
|
|
556
|
+
])
|
|
557
|
+
.optional()
|
|
558
|
+
.describe(
|
|
559
|
+
"SHORTS only: visual style direction and render look. Default app choice is kinetic_creator.",
|
|
560
|
+
),
|
|
561
|
+
theme: z
|
|
562
|
+
.string()
|
|
563
|
+
.optional()
|
|
564
|
+
.describe(
|
|
565
|
+
"Visual theme: editor style, and legacy shorts fallback only when visual_language is unset. " +
|
|
566
|
+
"none (literal — no imposed style), realistic (default), cinematic, " +
|
|
567
|
+
"anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " +
|
|
568
|
+
"minimalist, cyberpunk",
|
|
569
|
+
),
|
|
455
570
|
save_path: z
|
|
456
571
|
.string()
|
|
457
572
|
.optional()
|
|
@@ -486,6 +601,23 @@ registerTool(
|
|
|
486
601
|
language?: string;
|
|
487
602
|
aspect?: string;
|
|
488
603
|
voice_id?: string;
|
|
604
|
+
headline?: string;
|
|
605
|
+
subheadline?: string;
|
|
606
|
+
text_beats?: string[];
|
|
607
|
+
headline_color?: string;
|
|
608
|
+
subheadline_color?: string;
|
|
609
|
+
accent_color?: string;
|
|
610
|
+
offer_text?: string;
|
|
611
|
+
cta_text?: string;
|
|
612
|
+
badge_text?: string;
|
|
613
|
+
star_rating?: number;
|
|
614
|
+
text_position?: string;
|
|
615
|
+
text_animation?: string;
|
|
616
|
+
font_family?: string;
|
|
617
|
+
music_vibe?: string;
|
|
618
|
+
creative_format?: string;
|
|
619
|
+
visual_language?: string;
|
|
620
|
+
theme?: string;
|
|
489
621
|
save_path?: string;
|
|
490
622
|
max_wait_seconds?: number;
|
|
491
623
|
dry_run?: boolean;
|
|
@@ -515,13 +647,57 @@ registerTool(
|
|
|
515
647
|
// make_video (or a dry_run then a real run) reuses this same draft.
|
|
516
648
|
let slug: string;
|
|
517
649
|
if (kind === "short") {
|
|
650
|
+
// Carry the creative fields inline so a one-shot short can be titled
|
|
651
|
+
// and branded (headline/subheadline/music_vibe are short-only; theme
|
|
652
|
+
// applies to both kinds).
|
|
518
653
|
const created = await client.post<{ data: { slug: string } }>(
|
|
519
654
|
"/shorts",
|
|
520
|
-
{
|
|
655
|
+
{
|
|
656
|
+
product_prompt: args.prompt,
|
|
657
|
+
language: args.language,
|
|
658
|
+
headline: args.headline,
|
|
659
|
+
subheadline: args.subheadline,
|
|
660
|
+
text_beats: args.text_beats,
|
|
661
|
+
headline_color: args.headline_color,
|
|
662
|
+
subheadline_color: args.subheadline_color,
|
|
663
|
+
accent_color: args.accent_color,
|
|
664
|
+
offer_text: args.offer_text,
|
|
665
|
+
cta_text: args.cta_text,
|
|
666
|
+
badge_text: args.badge_text,
|
|
667
|
+
star_rating: args.star_rating,
|
|
668
|
+
short_text_position: args.text_position,
|
|
669
|
+
short_text_animation: args.text_animation,
|
|
670
|
+
short_font_family: args.font_family,
|
|
671
|
+
music_vibe: args.music_vibe,
|
|
672
|
+
creative_format: args.creative_format,
|
|
673
|
+
visual_language: args.visual_language,
|
|
674
|
+
theme: args.theme,
|
|
675
|
+
},
|
|
521
676
|
// Fold every create-affecting param into the key: two calls that
|
|
522
|
-
// differ only in
|
|
523
|
-
// transport retry of an identical call must reuse it.
|
|
524
|
-
idemKey(
|
|
677
|
+
// differ only in a creative field must NOT dedup to the same draft,
|
|
678
|
+
// and a transport retry of an identical call must reuse it.
|
|
679
|
+
idemKey(
|
|
680
|
+
"make-short",
|
|
681
|
+
args.prompt,
|
|
682
|
+
args.language ?? "",
|
|
683
|
+
args.headline ?? "",
|
|
684
|
+
args.subheadline ?? "",
|
|
685
|
+
JSON.stringify(args.text_beats ?? []),
|
|
686
|
+
args.headline_color ?? "",
|
|
687
|
+
args.subheadline_color ?? "",
|
|
688
|
+
args.accent_color ?? "",
|
|
689
|
+
args.offer_text ?? "",
|
|
690
|
+
args.cta_text ?? "",
|
|
691
|
+
args.badge_text ?? "",
|
|
692
|
+
String(args.star_rating ?? ""),
|
|
693
|
+
args.text_position ?? "",
|
|
694
|
+
args.text_animation ?? "",
|
|
695
|
+
args.font_family ?? "",
|
|
696
|
+
args.music_vibe ?? "",
|
|
697
|
+
args.creative_format ?? "",
|
|
698
|
+
args.visual_language ?? "",
|
|
699
|
+
args.theme ?? "",
|
|
700
|
+
),
|
|
525
701
|
);
|
|
526
702
|
slug = created.data.slug;
|
|
527
703
|
} else {
|
|
@@ -532,6 +708,7 @@ registerTool(
|
|
|
532
708
|
product_prompt: args.prompt,
|
|
533
709
|
export_aspect_ratio: args.aspect,
|
|
534
710
|
voice_id: args.voice_id,
|
|
711
|
+
theme: args.theme,
|
|
535
712
|
},
|
|
536
713
|
idemKey(
|
|
537
714
|
"make-editor",
|
|
@@ -539,6 +716,7 @@ registerTool(
|
|
|
539
716
|
args.language ?? "en",
|
|
540
717
|
args.aspect ?? "",
|
|
541
718
|
args.voice_id ?? "",
|
|
719
|
+
args.theme ?? "",
|
|
542
720
|
),
|
|
543
721
|
);
|
|
544
722
|
slug = created.data.slug;
|
|
@@ -677,7 +855,23 @@ registerTool(
|
|
|
677
855
|
inputSchema: {},
|
|
678
856
|
annotations: { title: "Get credits", ...RO },
|
|
679
857
|
},
|
|
680
|
-
tool(async (_args, client) =>
|
|
858
|
+
tool(async (_args, client) => {
|
|
859
|
+
const res = await client.get<{ data: unknown }>("/studio/credits");
|
|
860
|
+
const d = asRecord(asRecord(res).data);
|
|
861
|
+
// Whitelist generation-relevant fields only. Deliberately DROP
|
|
862
|
+
// `first_editor_video_pack_available`: it's a purchase-funnel flag for the
|
|
863
|
+
// PAID one-time starter pack (33 credits — not free), and this server has
|
|
864
|
+
// no billing scope, so it can't act on it. Passing it through only invited
|
|
865
|
+
// the agent to misread `true` as "your first video may be free", which
|
|
866
|
+
// contradicts the product (the first video is never free).
|
|
867
|
+
return ok({
|
|
868
|
+
credits: d.credits,
|
|
869
|
+
spendable_credits: d.spendable_credits,
|
|
870
|
+
reserved_credits: d.reserved_credits,
|
|
871
|
+
remaining_reserved_credits: d.remaining_reserved_credits,
|
|
872
|
+
message: d.message,
|
|
873
|
+
});
|
|
874
|
+
}),
|
|
681
875
|
);
|
|
682
876
|
|
|
683
877
|
// ── Voices ────────────────────────────────────────────────────────────────
|
|
@@ -770,8 +964,13 @@ registerTool(
|
|
|
770
964
|
{
|
|
771
965
|
title: "Create a short (draft)",
|
|
772
966
|
description:
|
|
773
|
-
"Creates a short draft from a product prompt (min 10 chars).
|
|
774
|
-
"
|
|
967
|
+
"Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
|
|
968
|
+
"segments + an on-screen title overlay + music; 14s when an end-card poster is set). Returns the slug, " +
|
|
969
|
+
"costs 0 credits. Follow with generate_short to render. " +
|
|
970
|
+
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
971
|
+
"title), and pick a music_vibe plus visual_language that fit the brand. To brand it further, attach a product image " +
|
|
972
|
+
"(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " +
|
|
973
|
+
"overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
775
974
|
inputSchema: {
|
|
776
975
|
product_prompt: z
|
|
777
976
|
.string()
|
|
@@ -781,21 +980,222 @@ registerTool(
|
|
|
781
980
|
.string()
|
|
782
981
|
.optional()
|
|
783
982
|
.describe('Language code, e.g. "en" (default)'),
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
983
|
+
headline: z
|
|
984
|
+
.string()
|
|
985
|
+
.max(160)
|
|
986
|
+
.optional()
|
|
987
|
+
.describe(
|
|
988
|
+
"The on-screen TITLE composited over the short (poster text overlay). ≤160 chars. Set this.",
|
|
989
|
+
),
|
|
990
|
+
subheadline: z
|
|
991
|
+
.string()
|
|
992
|
+
.max(200)
|
|
993
|
+
.optional()
|
|
994
|
+
.describe(
|
|
995
|
+
"The SECONDARY title / supporting line under the headline. ≤200 chars.",
|
|
996
|
+
),
|
|
997
|
+
creative_format: z
|
|
998
|
+
.enum([
|
|
999
|
+
"problem_solution",
|
|
1000
|
+
"mistake_fix",
|
|
1001
|
+
"myth_vs_reality",
|
|
1002
|
+
"before_after",
|
|
1003
|
+
"proof_demo",
|
|
1004
|
+
"product_reveal",
|
|
1005
|
+
])
|
|
1006
|
+
.optional()
|
|
1007
|
+
.describe(
|
|
1008
|
+
"Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto.",
|
|
1009
|
+
),
|
|
1010
|
+
visual_language: z
|
|
1011
|
+
.enum([
|
|
1012
|
+
"kinetic_creator",
|
|
1013
|
+
"premium_editorial",
|
|
1014
|
+
"cinematic_product",
|
|
1015
|
+
"ugc_realism",
|
|
1016
|
+
"startup_explainer",
|
|
1017
|
+
"luxury_minimal",
|
|
1018
|
+
])
|
|
1019
|
+
.optional()
|
|
1020
|
+
.describe(
|
|
1021
|
+
"Visual language for Veo direction and render styling. Good default: kinetic_creator.",
|
|
1022
|
+
),
|
|
1023
|
+
theme: z
|
|
1024
|
+
.string()
|
|
1025
|
+
.optional()
|
|
1026
|
+
.describe(
|
|
1027
|
+
"Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
|
|
1028
|
+
),
|
|
1029
|
+
music_vibe: z
|
|
1030
|
+
.string()
|
|
1031
|
+
.optional()
|
|
1032
|
+
.describe(
|
|
1033
|
+
"Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz.",
|
|
1034
|
+
),
|
|
1035
|
+
music_instruments: z
|
|
1036
|
+
.array(z.string())
|
|
1037
|
+
.optional()
|
|
1038
|
+
.describe('Optional instrument hints, e.g. ["piano", "strings"]'),
|
|
1039
|
+
text_position: z
|
|
1040
|
+
.enum(["top", "center", "bottom"])
|
|
1041
|
+
.optional()
|
|
1042
|
+
.describe("Where the title overlay sits (default bottom)"),
|
|
1043
|
+
text_animation: z
|
|
1044
|
+
.enum([
|
|
1045
|
+
"reveal",
|
|
1046
|
+
"typewriter",
|
|
1047
|
+
"fade_in",
|
|
1048
|
+
"pop",
|
|
1049
|
+
"bounce",
|
|
1050
|
+
"word_stagger",
|
|
1051
|
+
"word_spotlight",
|
|
1052
|
+
])
|
|
1053
|
+
.optional()
|
|
1054
|
+
.describe("Title overlay animation (default fade_in)"),
|
|
1055
|
+
font_family: z
|
|
1056
|
+
.string()
|
|
1057
|
+
.optional()
|
|
1058
|
+
.describe(
|
|
1059
|
+
"Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " +
|
|
1060
|
+
"ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, " +
|
|
1061
|
+
"ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " +
|
|
1062
|
+
"ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker.",
|
|
1063
|
+
),
|
|
1064
|
+
headline_color: z
|
|
1065
|
+
.string()
|
|
1066
|
+
.optional()
|
|
1067
|
+
.describe("Headline hex color, e.g. #ffffff"),
|
|
1068
|
+
subheadline_color: z
|
|
1069
|
+
.string()
|
|
1070
|
+
.optional()
|
|
1071
|
+
.describe("Subheadline hex color, e.g. #ffffff"),
|
|
1072
|
+
accent_color: z
|
|
1073
|
+
.string()
|
|
1074
|
+
.optional()
|
|
1075
|
+
.describe("6-digit brand accent hex color, e.g. #09EFBE"),
|
|
1076
|
+
offer_text: z
|
|
1077
|
+
.string()
|
|
1078
|
+
.max(16)
|
|
1079
|
+
.optional()
|
|
1080
|
+
.describe('Optional offer chip, e.g. "-40%" or "2 FOR 1"'),
|
|
1081
|
+
cta_text: z
|
|
1082
|
+
.string()
|
|
1083
|
+
.max(24)
|
|
1084
|
+
.optional()
|
|
1085
|
+
.describe('Optional CTA pill, e.g. "Shop now"'),
|
|
1086
|
+
badge_text: z
|
|
1087
|
+
.string()
|
|
1088
|
+
.max(24)
|
|
1089
|
+
.optional()
|
|
1090
|
+
.describe('Optional badge stamp, e.g. "BEST SELLER"'),
|
|
1091
|
+
star_rating: z
|
|
1092
|
+
.number()
|
|
1093
|
+
.min(0)
|
|
1094
|
+
.max(5)
|
|
1095
|
+
.optional()
|
|
1096
|
+
.describe("Optional 0..5 star rating under the subheadline"),
|
|
1097
|
+
text_beats: z
|
|
1098
|
+
.array(z.string().max(120))
|
|
1099
|
+
.max(8)
|
|
1100
|
+
.optional()
|
|
1101
|
+
.describe(
|
|
1102
|
+
"Optional caption beats shown sequentially instead of the static subheadline.",
|
|
1103
|
+
),
|
|
788
1104
|
},
|
|
789
1105
|
annotations: { title: "Create short", ...WRITE, idempotentHint: true },
|
|
790
1106
|
},
|
|
791
|
-
tool(
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1107
|
+
tool(
|
|
1108
|
+
async (
|
|
1109
|
+
args: {
|
|
1110
|
+
product_prompt: string;
|
|
1111
|
+
language?: string;
|
|
1112
|
+
headline?: string;
|
|
1113
|
+
subheadline?: string;
|
|
1114
|
+
creative_format?: string;
|
|
1115
|
+
visual_language?: string;
|
|
1116
|
+
theme?: string;
|
|
1117
|
+
music_vibe?: string;
|
|
1118
|
+
music_instruments?: string[];
|
|
1119
|
+
text_position?: string;
|
|
1120
|
+
text_animation?: string;
|
|
1121
|
+
font_family?: string;
|
|
1122
|
+
headline_color?: string;
|
|
1123
|
+
subheadline_color?: string;
|
|
1124
|
+
accent_color?: string;
|
|
1125
|
+
offer_text?: string;
|
|
1126
|
+
cta_text?: string;
|
|
1127
|
+
badge_text?: string;
|
|
1128
|
+
star_rating?: number;
|
|
1129
|
+
text_beats?: string[];
|
|
1130
|
+
},
|
|
1131
|
+
client,
|
|
1132
|
+
) => {
|
|
1133
|
+
// Map the agent-friendly field names onto the API's short_* params.
|
|
1134
|
+
const body: Record<string, unknown> = {
|
|
1135
|
+
product_prompt: args.product_prompt,
|
|
1136
|
+
};
|
|
1137
|
+
if (args.language !== undefined) body.language = args.language;
|
|
1138
|
+
if (args.headline !== undefined) body.headline = args.headline;
|
|
1139
|
+
if (args.subheadline !== undefined) body.subheadline = args.subheadline;
|
|
1140
|
+
if (args.creative_format !== undefined)
|
|
1141
|
+
body.creative_format = args.creative_format;
|
|
1142
|
+
if (args.visual_language !== undefined)
|
|
1143
|
+
body.visual_language = args.visual_language;
|
|
1144
|
+
if (args.theme !== undefined) body.theme = args.theme;
|
|
1145
|
+
if (args.music_vibe !== undefined) body.music_vibe = args.music_vibe;
|
|
1146
|
+
if (args.music_instruments !== undefined)
|
|
1147
|
+
body.music_instruments = args.music_instruments;
|
|
1148
|
+
if (args.text_position !== undefined)
|
|
1149
|
+
body.short_text_position = args.text_position;
|
|
1150
|
+
if (args.text_animation !== undefined)
|
|
1151
|
+
body.short_text_animation = args.text_animation;
|
|
1152
|
+
if (args.font_family !== undefined)
|
|
1153
|
+
body.short_font_family = args.font_family;
|
|
1154
|
+
if (args.headline_color !== undefined)
|
|
1155
|
+
body.headline_color = args.headline_color;
|
|
1156
|
+
if (args.subheadline_color !== undefined)
|
|
1157
|
+
body.subheadline_color = args.subheadline_color;
|
|
1158
|
+
if (args.accent_color !== undefined)
|
|
1159
|
+
body.accent_color = args.accent_color;
|
|
1160
|
+
if (args.offer_text !== undefined) body.offer_text = args.offer_text;
|
|
1161
|
+
if (args.cta_text !== undefined) body.cta_text = args.cta_text;
|
|
1162
|
+
if (args.badge_text !== undefined) body.badge_text = args.badge_text;
|
|
1163
|
+
if (args.star_rating !== undefined) body.star_rating = args.star_rating;
|
|
1164
|
+
if (args.text_beats !== undefined) body.text_beats = args.text_beats;
|
|
1165
|
+
const res = await client.post<{ data: { slug: string } }>(
|
|
1166
|
+
"/shorts",
|
|
1167
|
+
body,
|
|
1168
|
+
idemKey(
|
|
1169
|
+
"create-short",
|
|
1170
|
+
args.product_prompt,
|
|
1171
|
+
args.language ?? "",
|
|
1172
|
+
args.headline ?? "",
|
|
1173
|
+
args.subheadline ?? "",
|
|
1174
|
+
args.creative_format ?? "",
|
|
1175
|
+
args.visual_language ?? "",
|
|
1176
|
+
args.theme ?? "",
|
|
1177
|
+
args.music_vibe ?? "",
|
|
1178
|
+
JSON.stringify(args.music_instruments ?? []),
|
|
1179
|
+
args.text_position ?? "",
|
|
1180
|
+
args.text_animation ?? "",
|
|
1181
|
+
args.font_family ?? "",
|
|
1182
|
+
args.headline_color ?? "",
|
|
1183
|
+
args.subheadline_color ?? "",
|
|
1184
|
+
args.accent_color ?? "",
|
|
1185
|
+
args.offer_text ?? "",
|
|
1186
|
+
args.cta_text ?? "",
|
|
1187
|
+
args.badge_text ?? "",
|
|
1188
|
+
String(args.star_rating ?? ""),
|
|
1189
|
+
JSON.stringify(args.text_beats ?? []),
|
|
1190
|
+
),
|
|
1191
|
+
);
|
|
1192
|
+
return ok({
|
|
1193
|
+
slug: res.data.slug,
|
|
1194
|
+
kind: "short",
|
|
1195
|
+
next: "set_short_product / set_short_poster (optional branding), then generate_short",
|
|
1196
|
+
});
|
|
1197
|
+
},
|
|
1198
|
+
),
|
|
799
1199
|
);
|
|
800
1200
|
|
|
801
1201
|
registerTool(
|
|
@@ -803,22 +1203,431 @@ registerTool(
|
|
|
803
1203
|
{
|
|
804
1204
|
title: "Generate (render) a short",
|
|
805
1205
|
description:
|
|
806
|
-
"Deducts 15 credits and starts the render pipeline for an existing short.
|
|
807
|
-
"
|
|
1206
|
+
"Deducts 15 credits and starts the render pipeline for an existing short. Safe to call again: a " +
|
|
1207
|
+
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
1208
|
+
"short can be re-generated. Then poll with get_status or wait_for_completion (kind=short).",
|
|
808
1209
|
inputSchema: { slug: z.string().describe("Short slug from create_short") },
|
|
809
1210
|
annotations: { title: "Generate short", ...WRITE, idempotentHint: true },
|
|
810
1211
|
},
|
|
811
1212
|
tool(async (args: { slug: string }, client) => {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1213
|
+
// No stable idempotency key (see generate_slider): a per-slug key would
|
|
1214
|
+
// replay the first response for 24h and block an intentional re-generation
|
|
1215
|
+
// of a failed short. Double-charge is prevented server-side — the batch
|
|
1216
|
+
// claim's conditional update rolls back the losing transaction's credit
|
|
1217
|
+
// deduction — and a 409 means a render is already running.
|
|
1218
|
+
try {
|
|
1219
|
+
const res = await client.post<{ data: unknown }>(
|
|
1220
|
+
`/shorts/${args.slug}/generate`,
|
|
1221
|
+
);
|
|
1222
|
+
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
1223
|
+
return ok(status);
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
if ((e as { status?: number }).status === 409) {
|
|
1226
|
+
return ok({
|
|
1227
|
+
kind: "short",
|
|
1228
|
+
slug: args.slug,
|
|
1229
|
+
stage: "processing",
|
|
1230
|
+
terminal: false,
|
|
1231
|
+
ready: false,
|
|
1232
|
+
video_url: null,
|
|
1233
|
+
error: null,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
throw e;
|
|
1237
|
+
}
|
|
1238
|
+
}),
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
registerTool(
|
|
1242
|
+
"generate_short_text",
|
|
1243
|
+
{
|
|
1244
|
+
title: "Generate short overlay text (AI assist)",
|
|
1245
|
+
description:
|
|
1246
|
+
"Generates editable headline, subheadline, and caption beats for an existing short draft. " +
|
|
1247
|
+
"CONSUMES 1 AI ASSIST (free daily quota) and spends no video credits. The server reads the saved " +
|
|
1248
|
+
"short fields, so update/create the draft with the current product prompt, creative_format, visual_language, " +
|
|
1249
|
+
"language, and seed copy first. On 429 set auto_unlock:true (1 credit -> +10 assists, retried once) or write the text yourself.",
|
|
1250
|
+
inputSchema: {
|
|
1251
|
+
slug: z.string().describe("Short slug from create_short"),
|
|
1252
|
+
auto_unlock: z
|
|
1253
|
+
.boolean()
|
|
1254
|
+
.optional()
|
|
1255
|
+
.describe(
|
|
1256
|
+
"On 429, spend 1 credit to unlock +10 assists and retry once (default false)",
|
|
1257
|
+
),
|
|
1258
|
+
},
|
|
1259
|
+
annotations: {
|
|
1260
|
+
title: "Generate short text",
|
|
1261
|
+
...WRITE,
|
|
1262
|
+
idempotentHint: false,
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
tool(async (args: { slug: string; auto_unlock?: boolean }, client) => {
|
|
1266
|
+
const res = await withAssist(client, args.auto_unlock ?? false, () =>
|
|
1267
|
+
client.post<{ data: unknown }>(`/shorts/${args.slug}/text/generate`),
|
|
816
1268
|
);
|
|
817
|
-
|
|
818
|
-
|
|
1269
|
+
return ok(asRecord(res).data ?? res);
|
|
1270
|
+
}),
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
// ── Sliders (image carousels) ────────────────────────────────────────────────
|
|
1274
|
+
|
|
1275
|
+
registerTool(
|
|
1276
|
+
"create_slider",
|
|
1277
|
+
{
|
|
1278
|
+
title: "Create an image slider / carousel (draft)",
|
|
1279
|
+
description:
|
|
1280
|
+
"Creates a carousel draft from a prompt (min 10 chars). One prompt produces N still slides (an AI " +
|
|
1281
|
+
"background + a composited headline/body + optional logo) PLUS a ready-to-post caption and hashtags. " +
|
|
1282
|
+
"Returns the slug, costs 0 credits. Follow with generate_slider to render, then get_slider to read the " +
|
|
1283
|
+
"slide image URLs + caption. mode 'creative' tells a story; 'ad_driven' lists product facts/benefits.",
|
|
1284
|
+
inputSchema: {
|
|
1285
|
+
prompt: z.string().min(10).describe("What the carousel is about"),
|
|
1286
|
+
mode: z
|
|
1287
|
+
.enum(["creative", "ad_driven"])
|
|
1288
|
+
.optional()
|
|
1289
|
+
.describe(
|
|
1290
|
+
"'creative' (storytelling) or 'ad_driven' (facts about the product). Default creative.",
|
|
1291
|
+
),
|
|
1292
|
+
template: z
|
|
1293
|
+
.enum([
|
|
1294
|
+
"boldStatement",
|
|
1295
|
+
"editorialStory",
|
|
1296
|
+
"scrapbook",
|
|
1297
|
+
"featureGrid",
|
|
1298
|
+
"offerCard",
|
|
1299
|
+
"comparison",
|
|
1300
|
+
])
|
|
1301
|
+
.optional()
|
|
1302
|
+
.describe(
|
|
1303
|
+
"Composition preset. boldStatement/editorialStory/scrapbook are creative; " +
|
|
1304
|
+
"featureGrid/offerCard/comparison are ad-driven. Defaults to the mode default.",
|
|
1305
|
+
),
|
|
1306
|
+
slide_count: z
|
|
1307
|
+
.number()
|
|
1308
|
+
.optional()
|
|
1309
|
+
.describe("Number of slides, integer 3–10 (default 5)"),
|
|
1310
|
+
aspect_ratio: z
|
|
1311
|
+
.enum(["4:5", "1:1", "9:16"])
|
|
1312
|
+
.optional()
|
|
1313
|
+
.describe(
|
|
1314
|
+
"Canvas ratio (default 4:5, the highest-reach carousel ratio)",
|
|
1315
|
+
),
|
|
1316
|
+
accent_color: z
|
|
1317
|
+
.string()
|
|
1318
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
1319
|
+
.optional()
|
|
1320
|
+
.describe(
|
|
1321
|
+
"Brand accent: must start with #, exactly 6 hex digits, e.g. #09EFBE",
|
|
1322
|
+
),
|
|
1323
|
+
text_position: z
|
|
1324
|
+
.enum(["top", "middle", "bottom"])
|
|
1325
|
+
.optional()
|
|
1326
|
+
.describe(
|
|
1327
|
+
"Vertical placement of the on-image copy across all slides. Omit for the template's natural placement.",
|
|
1328
|
+
),
|
|
1329
|
+
},
|
|
1330
|
+
annotations: { title: "Create slider", ...WRITE, idempotentHint: true },
|
|
1331
|
+
},
|
|
1332
|
+
tool(
|
|
1333
|
+
async (
|
|
1334
|
+
args: {
|
|
1335
|
+
prompt: string;
|
|
1336
|
+
mode?: string;
|
|
1337
|
+
template?: string;
|
|
1338
|
+
slide_count?: number;
|
|
1339
|
+
aspect_ratio?: string;
|
|
1340
|
+
accent_color?: string;
|
|
1341
|
+
text_position?: string;
|
|
1342
|
+
},
|
|
1343
|
+
client,
|
|
1344
|
+
) => {
|
|
1345
|
+
// slide_count is a flat z.number() (see registerTool TS2589 note), so
|
|
1346
|
+
// enforce the integer 3..10 range here, matching the server changeset.
|
|
1347
|
+
if (args.slide_count !== undefined) {
|
|
1348
|
+
if (
|
|
1349
|
+
!Number.isInteger(args.slide_count) ||
|
|
1350
|
+
args.slide_count < 3 ||
|
|
1351
|
+
args.slide_count > 10
|
|
1352
|
+
) {
|
|
1353
|
+
return fail("slide_count must be an integer between 3 and 10");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const body: Record<string, unknown> = { prompt: args.prompt };
|
|
1357
|
+
if (args.mode !== undefined) body.mode = args.mode;
|
|
1358
|
+
if (args.template !== undefined) body.template = args.template;
|
|
1359
|
+
if (args.slide_count !== undefined) body.slide_count = args.slide_count;
|
|
1360
|
+
if (args.aspect_ratio !== undefined)
|
|
1361
|
+
body.aspect_ratio = args.aspect_ratio;
|
|
1362
|
+
if (args.accent_color !== undefined)
|
|
1363
|
+
body.accent_color = args.accent_color;
|
|
1364
|
+
if (args.text_position !== undefined)
|
|
1365
|
+
body.text_position = args.text_position;
|
|
1366
|
+
const res = await client.post<{ data: { slug: string } }>(
|
|
1367
|
+
"/sliders",
|
|
1368
|
+
body,
|
|
1369
|
+
idemKey(
|
|
1370
|
+
"create-slider",
|
|
1371
|
+
args.prompt,
|
|
1372
|
+
args.mode ?? "",
|
|
1373
|
+
args.template ?? "",
|
|
1374
|
+
),
|
|
1375
|
+
);
|
|
1376
|
+
return ok({
|
|
1377
|
+
slug: res.data.slug,
|
|
1378
|
+
kind: "slider",
|
|
1379
|
+
next: "generate_slider, then get_slider to read the slide URLs + caption",
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
),
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
registerTool(
|
|
1386
|
+
"generate_slider",
|
|
1387
|
+
{
|
|
1388
|
+
title: "Generate (render) a carousel",
|
|
1389
|
+
description:
|
|
1390
|
+
"Deducts 1 credit per slide (3–10 slides) and starts the pipeline (copy → AI backgrounds → composite) for an existing " +
|
|
1391
|
+
"carousel. Safe to call again: a duplicate while a render is already in flight is reported as still " +
|
|
1392
|
+
"processing (no double charge), and a failed carousel can be re-generated. Then poll with get_slider " +
|
|
1393
|
+
"until status is 'completed' or 'failed'.",
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1396
|
+
},
|
|
1397
|
+
annotations: { title: "Generate slider", ...WRITE, idempotentHint: true },
|
|
1398
|
+
},
|
|
1399
|
+
tool(async (args: { slug: string }, client) => {
|
|
1400
|
+
// No stable idempotency key on purpose: a per-slug key would replay the
|
|
1401
|
+
// first response for 24h and silently block an intentional re-generation
|
|
1402
|
+
// (e.g. retrying a failed render). Double-charge is prevented server-side
|
|
1403
|
+
// by the atomic generation claim, which returns 409 when a render is
|
|
1404
|
+
// already running — surface that as "still processing" rather than an error.
|
|
1405
|
+
try {
|
|
1406
|
+
const res = await client.post<{ data: Record<string, unknown> }>(
|
|
1407
|
+
`/sliders/${args.slug}/generate`,
|
|
1408
|
+
);
|
|
1409
|
+
const data = asRecord(asRecord(res).data);
|
|
1410
|
+
return ok({
|
|
1411
|
+
slug: args.slug,
|
|
1412
|
+
kind: "slider",
|
|
1413
|
+
status: data.status ?? "processing",
|
|
1414
|
+
});
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
if ((e as { status?: number }).status === 409) {
|
|
1417
|
+
return ok({ slug: args.slug, kind: "slider", status: "processing" });
|
|
1418
|
+
}
|
|
1419
|
+
throw e;
|
|
1420
|
+
}
|
|
819
1421
|
}),
|
|
820
1422
|
);
|
|
821
1423
|
|
|
1424
|
+
registerTool(
|
|
1425
|
+
"get_slider",
|
|
1426
|
+
{
|
|
1427
|
+
title: "Get carousel status + deliverable",
|
|
1428
|
+
description:
|
|
1429
|
+
"Returns the carousel's status and, when completed, the per-slide image URLs (download these), the " +
|
|
1430
|
+
"per-slide headline/body/kicker, the caption, and the hashtags. status flows draft → processing → " +
|
|
1431
|
+
"completed | failed. completed:true means every slide image_url is ready to save. Once completed you " +
|
|
1432
|
+
"can restyle_slider (new template/accent) or edit_slider_slide (one slide's text) for free.",
|
|
1433
|
+
inputSchema: { slug: z.string().describe("Slider slug") },
|
|
1434
|
+
annotations: { title: "Get slider", ...RO },
|
|
1435
|
+
},
|
|
1436
|
+
tool(async (args: { slug: string }, client) => {
|
|
1437
|
+
const res = await client.get<{ data: Record<string, unknown> }>(
|
|
1438
|
+
`/sliders/${args.slug}`,
|
|
1439
|
+
);
|
|
1440
|
+
const data = asRecord(asRecord(res).data);
|
|
1441
|
+
const slides = Array.isArray(data.slides)
|
|
1442
|
+
? (data.slides as Record<string, unknown>[])
|
|
1443
|
+
: [];
|
|
1444
|
+
const status = (data.status as string) ?? "unknown";
|
|
1445
|
+
return ok({
|
|
1446
|
+
slug: args.slug,
|
|
1447
|
+
kind: "slider",
|
|
1448
|
+
status,
|
|
1449
|
+
stage: status,
|
|
1450
|
+
terminal: status === "completed" || status === "failed",
|
|
1451
|
+
// Trust the server's terminal status. The API completes a slider in one
|
|
1452
|
+
// transaction that writes every rendered key, so "completed" always implies
|
|
1453
|
+
// all slides[].image_url are ready — re-deriving it here only created an
|
|
1454
|
+
// ambiguous terminal:true / completed:false dead-end on an edge response.
|
|
1455
|
+
completed: status === "completed",
|
|
1456
|
+
error: (data.error_message as string) ?? null,
|
|
1457
|
+
caption: (data.caption as string) ?? null,
|
|
1458
|
+
hashtags: Array.isArray(data.hashtags) ? data.hashtags : [],
|
|
1459
|
+
slides: slides.map((s) => ({
|
|
1460
|
+
position: s.position,
|
|
1461
|
+
image_url: s.image_url ?? null,
|
|
1462
|
+
headline: s.headline ?? null,
|
|
1463
|
+
body: s.body ?? null,
|
|
1464
|
+
kicker: s.kicker ?? null,
|
|
1465
|
+
status: s.status ?? null,
|
|
1466
|
+
})),
|
|
1467
|
+
});
|
|
1468
|
+
}),
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
registerTool(
|
|
1472
|
+
"restyle_slider",
|
|
1473
|
+
{
|
|
1474
|
+
title: "Restyle a carousel (free re-composite of every slide)",
|
|
1475
|
+
description:
|
|
1476
|
+
"Re-composites ALL slides of a completed carousel under a new template, accent color and/or text " +
|
|
1477
|
+
"position — for FREE (0 credits). Reuses the already-generated AI backgrounds, so no new images are paid for. The " +
|
|
1478
|
+
"carousel must already be 'completed' (generate it first); a draft/processing carousel returns a " +
|
|
1479
|
+
"409 conflict. Runs async: it marks every slide processing, so poll get_slider until status is back " +
|
|
1480
|
+
"to 'completed' (or 'failed'). Omit a field to leave it unchanged.",
|
|
1481
|
+
inputSchema: {
|
|
1482
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1483
|
+
template: z
|
|
1484
|
+
.enum([
|
|
1485
|
+
"boldStatement",
|
|
1486
|
+
"editorialStory",
|
|
1487
|
+
"scrapbook",
|
|
1488
|
+
"featureGrid",
|
|
1489
|
+
"offerCard",
|
|
1490
|
+
"comparison",
|
|
1491
|
+
])
|
|
1492
|
+
.optional()
|
|
1493
|
+
.describe(
|
|
1494
|
+
"New composition preset. boldStatement/editorialStory/scrapbook are creative; " +
|
|
1495
|
+
"featureGrid/offerCard/comparison are ad-driven.",
|
|
1496
|
+
),
|
|
1497
|
+
accent_color: z
|
|
1498
|
+
.string()
|
|
1499
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
1500
|
+
.optional()
|
|
1501
|
+
.describe(
|
|
1502
|
+
"New brand accent: must start with #, exactly 6 hex digits, e.g. #09EFBE",
|
|
1503
|
+
),
|
|
1504
|
+
text_position: z
|
|
1505
|
+
.enum(["top", "middle", "bottom"])
|
|
1506
|
+
.optional()
|
|
1507
|
+
.describe(
|
|
1508
|
+
"New vertical placement of the on-image copy across all slides.",
|
|
1509
|
+
),
|
|
1510
|
+
},
|
|
1511
|
+
annotations: { title: "Restyle slider", ...WRITE, idempotentHint: false },
|
|
1512
|
+
},
|
|
1513
|
+
tool(
|
|
1514
|
+
async (
|
|
1515
|
+
args: {
|
|
1516
|
+
slug: string;
|
|
1517
|
+
template?: string;
|
|
1518
|
+
accent_color?: string;
|
|
1519
|
+
text_position?: string;
|
|
1520
|
+
},
|
|
1521
|
+
client,
|
|
1522
|
+
) => {
|
|
1523
|
+
const body: Record<string, unknown> = {};
|
|
1524
|
+
if (args.template !== undefined) body.template = args.template;
|
|
1525
|
+
if (args.accent_color !== undefined)
|
|
1526
|
+
body.accent_color = args.accent_color;
|
|
1527
|
+
if (args.text_position !== undefined)
|
|
1528
|
+
body.text_position = args.text_position;
|
|
1529
|
+
try {
|
|
1530
|
+
const res = await client.post<{ data: Record<string, unknown> }>(
|
|
1531
|
+
`/sliders/${args.slug}/restyle`,
|
|
1532
|
+
body,
|
|
1533
|
+
);
|
|
1534
|
+
const data = asRecord(asRecord(res).data);
|
|
1535
|
+
return ok({
|
|
1536
|
+
slug: args.slug,
|
|
1537
|
+
kind: "slider",
|
|
1538
|
+
status: data.status ?? "processing",
|
|
1539
|
+
next: "poll get_slider until status is 'completed' (free re-composite, 0 credits)",
|
|
1540
|
+
});
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
// 409 = carousel not completed yet, or a re-composite already running.
|
|
1543
|
+
if ((e as { status?: number }).status === 409) {
|
|
1544
|
+
return ok({ slug: args.slug, kind: "slider", status: "processing" });
|
|
1545
|
+
}
|
|
1546
|
+
throw e;
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
),
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
registerTool(
|
|
1553
|
+
"edit_slider_slide",
|
|
1554
|
+
{
|
|
1555
|
+
title: "Edit one carousel slide's on-image text (free re-composite)",
|
|
1556
|
+
description:
|
|
1557
|
+
"Rewrites the on-image copy of a SINGLE slide (by position) and re-composites just that slide — for " +
|
|
1558
|
+
"FREE (0 credits). Reuses the slide's existing AI background, so no new image is paid for. The " +
|
|
1559
|
+
"carousel must already be 'completed' (a draft/processing carousel returns a 409 conflict). Runs " +
|
|
1560
|
+
"async: the slide goes back to processing, so poll get_slider until its status is 'completed'. Pass " +
|
|
1561
|
+
"only the fields you want to change. kicker = the small eyebrow/label line above the headline.",
|
|
1562
|
+
inputSchema: {
|
|
1563
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1564
|
+
position: z
|
|
1565
|
+
.number()
|
|
1566
|
+
.int()
|
|
1567
|
+
.min(1)
|
|
1568
|
+
.describe("1-based slide position (from get_slider)"),
|
|
1569
|
+
headline: z
|
|
1570
|
+
.string()
|
|
1571
|
+
.max(120)
|
|
1572
|
+
.optional()
|
|
1573
|
+
.describe("Slide headline, ≤120 chars"),
|
|
1574
|
+
body: z.string().max(600).optional().describe("Slide body, ≤600 chars"),
|
|
1575
|
+
kicker: z
|
|
1576
|
+
.string()
|
|
1577
|
+
.max(40)
|
|
1578
|
+
.optional()
|
|
1579
|
+
.describe("Small eyebrow/label line above the headline, ≤40 chars"),
|
|
1580
|
+
},
|
|
1581
|
+
annotations: {
|
|
1582
|
+
title: "Edit slider slide",
|
|
1583
|
+
...WRITE,
|
|
1584
|
+
idempotentHint: false,
|
|
1585
|
+
},
|
|
1586
|
+
},
|
|
1587
|
+
tool(
|
|
1588
|
+
async (
|
|
1589
|
+
args: {
|
|
1590
|
+
slug: string;
|
|
1591
|
+
position: number;
|
|
1592
|
+
headline?: string;
|
|
1593
|
+
body?: string;
|
|
1594
|
+
kicker?: string;
|
|
1595
|
+
},
|
|
1596
|
+
client,
|
|
1597
|
+
) => {
|
|
1598
|
+
const body: Record<string, unknown> = {};
|
|
1599
|
+
if (args.headline !== undefined) body.headline = args.headline;
|
|
1600
|
+
if (args.body !== undefined) body.body = args.body;
|
|
1601
|
+
if (args.kicker !== undefined) body.kicker = args.kicker;
|
|
1602
|
+
try {
|
|
1603
|
+
const res = await client.patch<{ data: Record<string, unknown> }>(
|
|
1604
|
+
`/sliders/${args.slug}/slides/${args.position}`,
|
|
1605
|
+
body,
|
|
1606
|
+
);
|
|
1607
|
+
const data = asRecord(asRecord(res).data);
|
|
1608
|
+
return ok({
|
|
1609
|
+
slug: args.slug,
|
|
1610
|
+
kind: "slider",
|
|
1611
|
+
position: args.position,
|
|
1612
|
+
status: data.status ?? "processing",
|
|
1613
|
+
next: "poll get_slider until the slide's status is 'completed' (free re-composite, 0 credits)",
|
|
1614
|
+
});
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
// 409 = carousel not completed yet, or a re-composite already running.
|
|
1617
|
+
if ((e as { status?: number }).status === 409) {
|
|
1618
|
+
return ok({
|
|
1619
|
+
slug: args.slug,
|
|
1620
|
+
kind: "slider",
|
|
1621
|
+
position: args.position,
|
|
1622
|
+
status: "processing",
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
throw e;
|
|
1626
|
+
}
|
|
1627
|
+
},
|
|
1628
|
+
),
|
|
1629
|
+
);
|
|
1630
|
+
|
|
822
1631
|
// ── Editor ───────────────────────────────────────────────────────────────────
|
|
823
1632
|
|
|
824
1633
|
registerTool(
|
|
@@ -829,7 +1638,11 @@ registerTool(
|
|
|
829
1638
|
"Creates an editor project from a product prompt and starts autopilot (server-orchestrated " +
|
|
830
1639
|
"scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " +
|
|
831
1640
|
"Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " +
|
|
832
|
-
"make_video for the one-shot path."
|
|
1641
|
+
"make_video for the one-shot path. " +
|
|
1642
|
+
"BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " +
|
|
1643
|
+
"create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " +
|
|
1644
|
+
"start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " +
|
|
1645
|
+
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
|
|
833
1646
|
inputSchema: {
|
|
834
1647
|
product_prompt: z
|
|
835
1648
|
.string()
|
|
@@ -842,7 +1655,9 @@ registerTool(
|
|
|
842
1655
|
theme: z
|
|
843
1656
|
.string()
|
|
844
1657
|
.optional()
|
|
845
|
-
.describe(
|
|
1658
|
+
.describe(
|
|
1659
|
+
'Visual theme (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.',
|
|
1660
|
+
),
|
|
846
1661
|
voice_id: z
|
|
847
1662
|
.string()
|
|
848
1663
|
.optional()
|
|
@@ -987,7 +1802,9 @@ registerTool(
|
|
|
987
1802
|
theme: z
|
|
988
1803
|
.string()
|
|
989
1804
|
.optional()
|
|
990
|
-
.describe(
|
|
1805
|
+
.describe(
|
|
1806
|
+
'Visual theme (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.',
|
|
1807
|
+
),
|
|
991
1808
|
voice_id: z
|
|
992
1809
|
.string()
|
|
993
1810
|
.regex(/^[A-Za-z0-9_-]+$/)
|
|
@@ -2159,6 +2976,89 @@ registerTool(
|
|
|
2159
2976
|
),
|
|
2160
2977
|
);
|
|
2161
2978
|
|
|
2979
|
+
// ── Shorts: branding images (product + end-card poster) ──────────────────────
|
|
2980
|
+
//
|
|
2981
|
+
// The editor branding tools above hit /editor/...; these hit /shorts/... A short
|
|
2982
|
+
// has no logo overlay (editor-only), but it CAN feature a product image woven
|
|
2983
|
+
// into the footage and a closing end-card poster. Both are 0 credits.
|
|
2984
|
+
|
|
2985
|
+
registerTool(
|
|
2986
|
+
"set_short_product",
|
|
2987
|
+
{
|
|
2988
|
+
title: "Attach a product image to a short (0 credits)",
|
|
2989
|
+
description:
|
|
2990
|
+
"Uploads a local product image and attaches it to a SHORT so the product is woven into the footage. " +
|
|
2991
|
+
"Accepted: " +
|
|
2992
|
+
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
2993
|
+
" (≤20 MB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Optional description " +
|
|
2994
|
+
"(≤500 chars) guides how it's featured. Attach BEFORE generate_short so the product is part of the scenes. " +
|
|
2995
|
+
"(This is the shorts equivalent of set_product for editor projects.)",
|
|
2996
|
+
inputSchema: {
|
|
2997
|
+
slug: z.string().describe("Short slug (from create_short)"),
|
|
2998
|
+
file_path: z
|
|
2999
|
+
.string()
|
|
3000
|
+
.describe("Local product image path (.jpg/.jpeg/.png)"),
|
|
3001
|
+
description: z
|
|
3002
|
+
.string()
|
|
3003
|
+
.max(500)
|
|
3004
|
+
.optional()
|
|
3005
|
+
.describe("Product description (≤500 chars)"),
|
|
3006
|
+
},
|
|
3007
|
+
annotations: {
|
|
3008
|
+
title: "Set short product",
|
|
3009
|
+
...WRITE,
|
|
3010
|
+
idempotentHint: false,
|
|
3011
|
+
},
|
|
3012
|
+
},
|
|
3013
|
+
tool(
|
|
3014
|
+
async (
|
|
3015
|
+
args: { slug: string; file_path: string; description?: string },
|
|
3016
|
+
client,
|
|
3017
|
+
) => {
|
|
3018
|
+
const { s3_key } = await uploadImageFile(
|
|
3019
|
+
client,
|
|
3020
|
+
`/shorts/${args.slug}/product/presign`,
|
|
3021
|
+
args.file_path,
|
|
3022
|
+
);
|
|
3023
|
+
const res = await client.post<{ data: unknown }>(
|
|
3024
|
+
`/shorts/${args.slug}/product/confirm`,
|
|
3025
|
+
{ s3_key, product_description: args.description },
|
|
3026
|
+
);
|
|
3027
|
+
return ok(asRecord(res).data ?? res);
|
|
3028
|
+
},
|
|
3029
|
+
),
|
|
3030
|
+
);
|
|
3031
|
+
|
|
3032
|
+
registerTool(
|
|
3033
|
+
"set_short_poster",
|
|
3034
|
+
{
|
|
3035
|
+
title: "Set a short's end-card poster image (0 credits)",
|
|
3036
|
+
description:
|
|
3037
|
+
"Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " +
|
|
3038
|
+
"render from 12s to 14s). Accepted: " +
|
|
3039
|
+
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
3040
|
+
" (≤20 MB), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
3041
|
+
"again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
3042
|
+
inputSchema: {
|
|
3043
|
+
slug: z.string().describe("Short slug (from create_short)"),
|
|
3044
|
+
file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
|
3045
|
+
},
|
|
3046
|
+
annotations: { title: "Set short poster", ...WRITE, idempotentHint: false },
|
|
3047
|
+
},
|
|
3048
|
+
tool(async (args: { slug: string; file_path: string }, client) => {
|
|
3049
|
+
const { s3_key } = await uploadShortPoster(
|
|
3050
|
+
client,
|
|
3051
|
+
args.slug,
|
|
3052
|
+
args.file_path,
|
|
3053
|
+
);
|
|
3054
|
+
const res = await client.post<{ data: unknown }>(
|
|
3055
|
+
`/shorts/${args.slug}/poster/confirm`,
|
|
3056
|
+
{ s3_key },
|
|
3057
|
+
);
|
|
3058
|
+
return ok(asRecord(res).data ?? res);
|
|
3059
|
+
}),
|
|
3060
|
+
);
|
|
3061
|
+
|
|
2162
3062
|
// ── Status / waiting ──────────────────────────────────────────────────────────
|
|
2163
3063
|
|
|
2164
3064
|
registerTool(
|