@hubfluencer/mcp 0.3.0 → 0.5.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 +15 -6
- package/dist/index.js +365 -24
- package/package.json +1 -1
- package/src/index.ts +744 -24
package/src/index.ts
CHANGED
|
@@ -317,6 +317,27 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
317
317
|
|
|
318
318
|
const kindSchema = z.enum(["short", "editor"]).describe("Project kind");
|
|
319
319
|
|
|
320
|
+
// Creative-style value lists shared by SHORTS and EDITOR (single source of
|
|
321
|
+
// truth, mirroring the API's VideoFactory.creative_formats/0 +
|
|
322
|
+
// visual_languages/0). creative_format = narrative arc; visual_language =
|
|
323
|
+
// production treatment / render look.
|
|
324
|
+
const CREATIVE_FORMATS = [
|
|
325
|
+
"problem_solution",
|
|
326
|
+
"mistake_fix",
|
|
327
|
+
"myth_vs_reality",
|
|
328
|
+
"before_after",
|
|
329
|
+
"proof_demo",
|
|
330
|
+
"product_reveal",
|
|
331
|
+
] as const;
|
|
332
|
+
const VISUAL_LANGUAGES = [
|
|
333
|
+
"kinetic_creator",
|
|
334
|
+
"premium_editorial",
|
|
335
|
+
"cinematic_product",
|
|
336
|
+
"ugc_realism",
|
|
337
|
+
"startup_explainer",
|
|
338
|
+
"luxury_minimal",
|
|
339
|
+
] as const;
|
|
340
|
+
|
|
320
341
|
const RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
|
|
321
342
|
const WRITE = {
|
|
322
343
|
readOnlyHint: false,
|
|
@@ -324,7 +345,7 @@ const WRITE = {
|
|
|
324
345
|
openWorldHint: true,
|
|
325
346
|
};
|
|
326
347
|
|
|
327
|
-
const server = new McpServer({ name: "hubfluencer", version: "0.
|
|
348
|
+
const server = new McpServer({ name: "hubfluencer", version: "0.5.0" });
|
|
328
349
|
|
|
329
350
|
// The SDK's `registerTool` is generic over the Zod input shape; with this many
|
|
330
351
|
// tools its conditional types hit TS2589 ("excessively deep"). Inputs are still
|
|
@@ -427,8 +448,10 @@ registerTool(
|
|
|
427
448
|
"for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " +
|
|
428
449
|
"the render is still running — call wait_for_completion with the returned slug to finish. " +
|
|
429
450
|
"BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " +
|
|
430
|
-
"music_vibe
|
|
431
|
-
"
|
|
451
|
+
"music_vibe so a one-shot short isn't bare. creative_format + visual_language apply to BOTH kinds (editor and " +
|
|
452
|
+
"shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " +
|
|
453
|
+
"editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " +
|
|
454
|
+
"For richer branding — a product image, brand logo, or " +
|
|
432
455
|
"closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " +
|
|
433
456
|
"create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
|
|
434
457
|
// Schema kept intentionally flat (no .min/.max/.int chains) — the SDK's
|
|
@@ -468,17 +491,91 @@ registerTool(
|
|
|
468
491
|
.string()
|
|
469
492
|
.optional()
|
|
470
493
|
.describe("SHORTS only: the secondary title / supporting line (≤200)"),
|
|
494
|
+
text_beats: z
|
|
495
|
+
.array(z.string())
|
|
496
|
+
.optional()
|
|
497
|
+
.describe(
|
|
498
|
+
"SHORTS only: caption beats shown sequentially instead of a static subheadline (≤8, each ≤120 chars).",
|
|
499
|
+
),
|
|
500
|
+
headline_color: z
|
|
501
|
+
.string()
|
|
502
|
+
.optional()
|
|
503
|
+
.describe("SHORTS only: headline hex color, e.g. #ffffff"),
|
|
504
|
+
subheadline_color: z
|
|
505
|
+
.string()
|
|
506
|
+
.optional()
|
|
507
|
+
.describe("SHORTS only: subheadline hex color, e.g. #ffffff"),
|
|
508
|
+
accent_color: z
|
|
509
|
+
.string()
|
|
510
|
+
.optional()
|
|
511
|
+
.describe("SHORTS only: 6-digit brand accent hex color, e.g. #09EFBE"),
|
|
512
|
+
offer_text: z
|
|
513
|
+
.string()
|
|
514
|
+
.optional()
|
|
515
|
+
.describe("SHORTS only: optional offer chip, e.g. -40% (≤16 chars)"),
|
|
516
|
+
cta_text: z
|
|
517
|
+
.string()
|
|
518
|
+
.optional()
|
|
519
|
+
.describe("SHORTS only: optional CTA pill, e.g. Shop now (≤24 chars)"),
|
|
520
|
+
badge_text: z
|
|
521
|
+
.string()
|
|
522
|
+
.optional()
|
|
523
|
+
.describe(
|
|
524
|
+
"SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)",
|
|
525
|
+
),
|
|
526
|
+
star_rating: z
|
|
527
|
+
.number()
|
|
528
|
+
.optional()
|
|
529
|
+
.describe(
|
|
530
|
+
"SHORTS only: optional 0..5 star rating under the subheadline",
|
|
531
|
+
),
|
|
532
|
+
text_position: z
|
|
533
|
+
.enum(["top", "center", "bottom"])
|
|
534
|
+
.optional()
|
|
535
|
+
.describe("SHORTS only: title overlay position (default bottom)"),
|
|
536
|
+
text_animation: z
|
|
537
|
+
.enum([
|
|
538
|
+
"reveal",
|
|
539
|
+
"typewriter",
|
|
540
|
+
"fade_in",
|
|
541
|
+
"pop",
|
|
542
|
+
"bounce",
|
|
543
|
+
"word_stagger",
|
|
544
|
+
"word_spotlight",
|
|
545
|
+
])
|
|
546
|
+
.optional()
|
|
547
|
+
.describe("SHORTS only: title overlay animation (default fade_in)"),
|
|
548
|
+
font_family: z
|
|
549
|
+
.string()
|
|
550
|
+
.optional()
|
|
551
|
+
.describe(
|
|
552
|
+
"SHORTS only: overlay font family, e.g. ShortFontSpaceGrotesk",
|
|
553
|
+
),
|
|
471
554
|
music_vibe: z
|
|
472
555
|
.string()
|
|
473
556
|
.optional()
|
|
474
557
|
.describe(
|
|
475
558
|
"SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz",
|
|
476
559
|
),
|
|
560
|
+
creative_format: z
|
|
561
|
+
.enum(CREATIVE_FORMATS)
|
|
562
|
+
.optional()
|
|
563
|
+
.describe(
|
|
564
|
+
"SHORTS + EDITOR: narrative arc / segment structure. Omit for Auto/generic.",
|
|
565
|
+
),
|
|
566
|
+
visual_language: z
|
|
567
|
+
.enum(VISUAL_LANGUAGES)
|
|
568
|
+
.optional()
|
|
569
|
+
.describe(
|
|
570
|
+
"SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator.",
|
|
571
|
+
),
|
|
477
572
|
theme: z
|
|
478
573
|
.string()
|
|
479
574
|
.optional()
|
|
480
575
|
.describe(
|
|
481
|
-
"Visual theme
|
|
576
|
+
"Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " +
|
|
577
|
+
"For editor, when visual_language is set it drives the look and 'none' is ignored. " +
|
|
578
|
+
"none (literal — no imposed style), realistic (default), cinematic, " +
|
|
482
579
|
"anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " +
|
|
483
580
|
"minimalist, cyberpunk",
|
|
484
581
|
),
|
|
@@ -518,7 +615,20 @@ registerTool(
|
|
|
518
615
|
voice_id?: string;
|
|
519
616
|
headline?: string;
|
|
520
617
|
subheadline?: string;
|
|
618
|
+
text_beats?: string[];
|
|
619
|
+
headline_color?: string;
|
|
620
|
+
subheadline_color?: string;
|
|
621
|
+
accent_color?: string;
|
|
622
|
+
offer_text?: string;
|
|
623
|
+
cta_text?: string;
|
|
624
|
+
badge_text?: string;
|
|
625
|
+
star_rating?: number;
|
|
626
|
+
text_position?: string;
|
|
627
|
+
text_animation?: string;
|
|
628
|
+
font_family?: string;
|
|
521
629
|
music_vibe?: string;
|
|
630
|
+
creative_format?: string;
|
|
631
|
+
visual_language?: string;
|
|
522
632
|
theme?: string;
|
|
523
633
|
save_path?: string;
|
|
524
634
|
max_wait_seconds?: number;
|
|
@@ -559,7 +669,20 @@ registerTool(
|
|
|
559
669
|
language: args.language,
|
|
560
670
|
headline: args.headline,
|
|
561
671
|
subheadline: args.subheadline,
|
|
672
|
+
text_beats: args.text_beats,
|
|
673
|
+
headline_color: args.headline_color,
|
|
674
|
+
subheadline_color: args.subheadline_color,
|
|
675
|
+
accent_color: args.accent_color,
|
|
676
|
+
offer_text: args.offer_text,
|
|
677
|
+
cta_text: args.cta_text,
|
|
678
|
+
badge_text: args.badge_text,
|
|
679
|
+
star_rating: args.star_rating,
|
|
680
|
+
short_text_position: args.text_position,
|
|
681
|
+
short_text_animation: args.text_animation,
|
|
682
|
+
short_font_family: args.font_family,
|
|
562
683
|
music_vibe: args.music_vibe,
|
|
684
|
+
creative_format: args.creative_format,
|
|
685
|
+
visual_language: args.visual_language,
|
|
563
686
|
theme: args.theme,
|
|
564
687
|
},
|
|
565
688
|
// Fold every create-affecting param into the key: two calls that
|
|
@@ -571,7 +694,20 @@ registerTool(
|
|
|
571
694
|
args.language ?? "",
|
|
572
695
|
args.headline ?? "",
|
|
573
696
|
args.subheadline ?? "",
|
|
697
|
+
JSON.stringify(args.text_beats ?? []),
|
|
698
|
+
args.headline_color ?? "",
|
|
699
|
+
args.subheadline_color ?? "",
|
|
700
|
+
args.accent_color ?? "",
|
|
701
|
+
args.offer_text ?? "",
|
|
702
|
+
args.cta_text ?? "",
|
|
703
|
+
args.badge_text ?? "",
|
|
704
|
+
String(args.star_rating ?? ""),
|
|
705
|
+
args.text_position ?? "",
|
|
706
|
+
args.text_animation ?? "",
|
|
707
|
+
args.font_family ?? "",
|
|
574
708
|
args.music_vibe ?? "",
|
|
709
|
+
args.creative_format ?? "",
|
|
710
|
+
args.visual_language ?? "",
|
|
575
711
|
args.theme ?? "",
|
|
576
712
|
),
|
|
577
713
|
);
|
|
@@ -584,6 +720,8 @@ registerTool(
|
|
|
584
720
|
product_prompt: args.prompt,
|
|
585
721
|
export_aspect_ratio: args.aspect,
|
|
586
722
|
voice_id: args.voice_id,
|
|
723
|
+
creative_format: args.creative_format,
|
|
724
|
+
visual_language: args.visual_language,
|
|
587
725
|
theme: args.theme,
|
|
588
726
|
},
|
|
589
727
|
idemKey(
|
|
@@ -592,6 +730,8 @@ registerTool(
|
|
|
592
730
|
args.language ?? "en",
|
|
593
731
|
args.aspect ?? "",
|
|
594
732
|
args.voice_id ?? "",
|
|
733
|
+
args.creative_format ?? "",
|
|
734
|
+
args.visual_language ?? "",
|
|
595
735
|
args.theme ?? "",
|
|
596
736
|
),
|
|
597
737
|
);
|
|
@@ -844,7 +984,7 @@ registerTool(
|
|
|
844
984
|
"segments + an on-screen title overlay + music; 14s when an end-card poster is set). Returns the slug, " +
|
|
845
985
|
"costs 0 credits. Follow with generate_short to render. " +
|
|
846
986
|
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
847
|
-
"title), and pick a music_vibe
|
|
987
|
+
"title), and pick a music_vibe plus visual_language that fit the brand. To brand it further, attach a product image " +
|
|
848
988
|
"(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " +
|
|
849
989
|
"overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
850
990
|
inputSchema: {
|
|
@@ -870,13 +1010,23 @@ registerTool(
|
|
|
870
1010
|
.describe(
|
|
871
1011
|
"The SECONDARY title / supporting line under the headline. ≤200 chars.",
|
|
872
1012
|
),
|
|
1013
|
+
creative_format: z
|
|
1014
|
+
.enum(CREATIVE_FORMATS)
|
|
1015
|
+
.optional()
|
|
1016
|
+
.describe(
|
|
1017
|
+
"Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto.",
|
|
1018
|
+
),
|
|
1019
|
+
visual_language: z
|
|
1020
|
+
.enum(VISUAL_LANGUAGES)
|
|
1021
|
+
.optional()
|
|
1022
|
+
.describe(
|
|
1023
|
+
"Visual language for Veo direction and render styling. Good default: kinetic_creator.",
|
|
1024
|
+
),
|
|
873
1025
|
theme: z
|
|
874
1026
|
.string()
|
|
875
1027
|
.optional()
|
|
876
1028
|
.describe(
|
|
877
|
-
"
|
|
878
|
-
"realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, " +
|
|
879
|
-
"gaming, retro_80s, minimalist, cyberpunk.",
|
|
1029
|
+
"Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
|
|
880
1030
|
),
|
|
881
1031
|
music_vibe: z
|
|
882
1032
|
.string()
|
|
@@ -893,7 +1043,15 @@ registerTool(
|
|
|
893
1043
|
.optional()
|
|
894
1044
|
.describe("Where the title overlay sits (default bottom)"),
|
|
895
1045
|
text_animation: z
|
|
896
|
-
.enum([
|
|
1046
|
+
.enum([
|
|
1047
|
+
"reveal",
|
|
1048
|
+
"typewriter",
|
|
1049
|
+
"fade_in",
|
|
1050
|
+
"pop",
|
|
1051
|
+
"bounce",
|
|
1052
|
+
"word_stagger",
|
|
1053
|
+
"word_spotlight",
|
|
1054
|
+
])
|
|
897
1055
|
.optional()
|
|
898
1056
|
.describe("Title overlay animation (default fade_in)"),
|
|
899
1057
|
font_family: z
|
|
@@ -901,9 +1059,49 @@ registerTool(
|
|
|
901
1059
|
.optional()
|
|
902
1060
|
.describe(
|
|
903
1061
|
"Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " +
|
|
904
|
-
"ShortFontTheBold, ShortFontImpact, ShortFontLato,
|
|
1062
|
+
"ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, " +
|
|
905
1063
|
"ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " +
|
|
906
|
-
"ShortFontTikTokSans, ShortFontBangers.",
|
|
1064
|
+
"ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker.",
|
|
1065
|
+
),
|
|
1066
|
+
headline_color: z
|
|
1067
|
+
.string()
|
|
1068
|
+
.optional()
|
|
1069
|
+
.describe("Headline hex color, e.g. #ffffff"),
|
|
1070
|
+
subheadline_color: z
|
|
1071
|
+
.string()
|
|
1072
|
+
.optional()
|
|
1073
|
+
.describe("Subheadline hex color, e.g. #ffffff"),
|
|
1074
|
+
accent_color: z
|
|
1075
|
+
.string()
|
|
1076
|
+
.optional()
|
|
1077
|
+
.describe("6-digit brand accent hex color, e.g. #09EFBE"),
|
|
1078
|
+
offer_text: z
|
|
1079
|
+
.string()
|
|
1080
|
+
.max(16)
|
|
1081
|
+
.optional()
|
|
1082
|
+
.describe('Optional offer chip, e.g. "-40%" or "2 FOR 1"'),
|
|
1083
|
+
cta_text: z
|
|
1084
|
+
.string()
|
|
1085
|
+
.max(24)
|
|
1086
|
+
.optional()
|
|
1087
|
+
.describe('Optional CTA pill, e.g. "Shop now"'),
|
|
1088
|
+
badge_text: z
|
|
1089
|
+
.string()
|
|
1090
|
+
.max(24)
|
|
1091
|
+
.optional()
|
|
1092
|
+
.describe('Optional badge stamp, e.g. "BEST SELLER"'),
|
|
1093
|
+
star_rating: z
|
|
1094
|
+
.number()
|
|
1095
|
+
.min(0)
|
|
1096
|
+
.max(5)
|
|
1097
|
+
.optional()
|
|
1098
|
+
.describe("Optional 0..5 star rating under the subheadline"),
|
|
1099
|
+
text_beats: z
|
|
1100
|
+
.array(z.string().max(120))
|
|
1101
|
+
.max(8)
|
|
1102
|
+
.optional()
|
|
1103
|
+
.describe(
|
|
1104
|
+
"Optional caption beats shown sequentially instead of the static subheadline.",
|
|
907
1105
|
),
|
|
908
1106
|
},
|
|
909
1107
|
annotations: { title: "Create short", ...WRITE, idempotentHint: true },
|
|
@@ -915,12 +1113,22 @@ registerTool(
|
|
|
915
1113
|
language?: string;
|
|
916
1114
|
headline?: string;
|
|
917
1115
|
subheadline?: string;
|
|
1116
|
+
creative_format?: string;
|
|
1117
|
+
visual_language?: string;
|
|
918
1118
|
theme?: string;
|
|
919
1119
|
music_vibe?: string;
|
|
920
1120
|
music_instruments?: string[];
|
|
921
1121
|
text_position?: string;
|
|
922
1122
|
text_animation?: string;
|
|
923
1123
|
font_family?: string;
|
|
1124
|
+
headline_color?: string;
|
|
1125
|
+
subheadline_color?: string;
|
|
1126
|
+
accent_color?: string;
|
|
1127
|
+
offer_text?: string;
|
|
1128
|
+
cta_text?: string;
|
|
1129
|
+
badge_text?: string;
|
|
1130
|
+
star_rating?: number;
|
|
1131
|
+
text_beats?: string[];
|
|
924
1132
|
},
|
|
925
1133
|
client,
|
|
926
1134
|
) => {
|
|
@@ -931,6 +1139,10 @@ registerTool(
|
|
|
931
1139
|
if (args.language !== undefined) body.language = args.language;
|
|
932
1140
|
if (args.headline !== undefined) body.headline = args.headline;
|
|
933
1141
|
if (args.subheadline !== undefined) body.subheadline = args.subheadline;
|
|
1142
|
+
if (args.creative_format !== undefined)
|
|
1143
|
+
body.creative_format = args.creative_format;
|
|
1144
|
+
if (args.visual_language !== undefined)
|
|
1145
|
+
body.visual_language = args.visual_language;
|
|
934
1146
|
if (args.theme !== undefined) body.theme = args.theme;
|
|
935
1147
|
if (args.music_vibe !== undefined) body.music_vibe = args.music_vibe;
|
|
936
1148
|
if (args.music_instruments !== undefined)
|
|
@@ -941,6 +1153,17 @@ registerTool(
|
|
|
941
1153
|
body.short_text_animation = args.text_animation;
|
|
942
1154
|
if (args.font_family !== undefined)
|
|
943
1155
|
body.short_font_family = args.font_family;
|
|
1156
|
+
if (args.headline_color !== undefined)
|
|
1157
|
+
body.headline_color = args.headline_color;
|
|
1158
|
+
if (args.subheadline_color !== undefined)
|
|
1159
|
+
body.subheadline_color = args.subheadline_color;
|
|
1160
|
+
if (args.accent_color !== undefined)
|
|
1161
|
+
body.accent_color = args.accent_color;
|
|
1162
|
+
if (args.offer_text !== undefined) body.offer_text = args.offer_text;
|
|
1163
|
+
if (args.cta_text !== undefined) body.cta_text = args.cta_text;
|
|
1164
|
+
if (args.badge_text !== undefined) body.badge_text = args.badge_text;
|
|
1165
|
+
if (args.star_rating !== undefined) body.star_rating = args.star_rating;
|
|
1166
|
+
if (args.text_beats !== undefined) body.text_beats = args.text_beats;
|
|
944
1167
|
const res = await client.post<{ data: { slug: string } }>(
|
|
945
1168
|
"/shorts",
|
|
946
1169
|
body,
|
|
@@ -950,6 +1173,22 @@ registerTool(
|
|
|
950
1173
|
args.language ?? "",
|
|
951
1174
|
args.headline ?? "",
|
|
952
1175
|
args.subheadline ?? "",
|
|
1176
|
+
args.creative_format ?? "",
|
|
1177
|
+
args.visual_language ?? "",
|
|
1178
|
+
args.theme ?? "",
|
|
1179
|
+
args.music_vibe ?? "",
|
|
1180
|
+
JSON.stringify(args.music_instruments ?? []),
|
|
1181
|
+
args.text_position ?? "",
|
|
1182
|
+
args.text_animation ?? "",
|
|
1183
|
+
args.font_family ?? "",
|
|
1184
|
+
args.headline_color ?? "",
|
|
1185
|
+
args.subheadline_color ?? "",
|
|
1186
|
+
args.accent_color ?? "",
|
|
1187
|
+
args.offer_text ?? "",
|
|
1188
|
+
args.cta_text ?? "",
|
|
1189
|
+
args.badge_text ?? "",
|
|
1190
|
+
String(args.star_rating ?? ""),
|
|
1191
|
+
JSON.stringify(args.text_beats ?? []),
|
|
953
1192
|
),
|
|
954
1193
|
);
|
|
955
1194
|
return ok({
|
|
@@ -966,22 +1205,431 @@ registerTool(
|
|
|
966
1205
|
{
|
|
967
1206
|
title: "Generate (render) a short",
|
|
968
1207
|
description:
|
|
969
|
-
"Deducts 15 credits and starts the render pipeline for an existing short.
|
|
970
|
-
"
|
|
1208
|
+
"Deducts 15 credits and starts the render pipeline for an existing short. Safe to call again: a " +
|
|
1209
|
+
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
1210
|
+
"short can be re-generated. Then poll with get_status or wait_for_completion (kind=short).",
|
|
971
1211
|
inputSchema: { slug: z.string().describe("Short slug from create_short") },
|
|
972
1212
|
annotations: { title: "Generate short", ...WRITE, idempotentHint: true },
|
|
973
1213
|
},
|
|
974
1214
|
tool(async (args: { slug: string }, client) => {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1215
|
+
// No stable idempotency key (see generate_slider): a per-slug key would
|
|
1216
|
+
// replay the first response for 24h and block an intentional re-generation
|
|
1217
|
+
// of a failed short. Double-charge is prevented server-side — the batch
|
|
1218
|
+
// claim's conditional update rolls back the losing transaction's credit
|
|
1219
|
+
// deduction — and a 409 means a render is already running.
|
|
1220
|
+
try {
|
|
1221
|
+
const res = await client.post<{ data: unknown }>(
|
|
1222
|
+
`/shorts/${args.slug}/generate`,
|
|
1223
|
+
);
|
|
1224
|
+
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
1225
|
+
return ok(status);
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
if ((e as { status?: number }).status === 409) {
|
|
1228
|
+
return ok({
|
|
1229
|
+
kind: "short",
|
|
1230
|
+
slug: args.slug,
|
|
1231
|
+
stage: "processing",
|
|
1232
|
+
terminal: false,
|
|
1233
|
+
ready: false,
|
|
1234
|
+
video_url: null,
|
|
1235
|
+
error: null,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
throw e;
|
|
1239
|
+
}
|
|
1240
|
+
}),
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
registerTool(
|
|
1244
|
+
"generate_short_text",
|
|
1245
|
+
{
|
|
1246
|
+
title: "Generate short overlay text (AI assist)",
|
|
1247
|
+
description:
|
|
1248
|
+
"Generates editable headline, subheadline, and caption beats for an existing short draft. " +
|
|
1249
|
+
"CONSUMES 1 AI ASSIST (free daily quota) and spends no video credits. The server reads the saved " +
|
|
1250
|
+
"short fields, so update/create the draft with the current product prompt, creative_format, visual_language, " +
|
|
1251
|
+
"language, and seed copy first. On 429 set auto_unlock:true (1 credit -> +10 assists, retried once) or write the text yourself.",
|
|
1252
|
+
inputSchema: {
|
|
1253
|
+
slug: z.string().describe("Short slug from create_short"),
|
|
1254
|
+
auto_unlock: z
|
|
1255
|
+
.boolean()
|
|
1256
|
+
.optional()
|
|
1257
|
+
.describe(
|
|
1258
|
+
"On 429, spend 1 credit to unlock +10 assists and retry once (default false)",
|
|
1259
|
+
),
|
|
1260
|
+
},
|
|
1261
|
+
annotations: {
|
|
1262
|
+
title: "Generate short text",
|
|
1263
|
+
...WRITE,
|
|
1264
|
+
idempotentHint: false,
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
tool(async (args: { slug: string; auto_unlock?: boolean }, client) => {
|
|
1268
|
+
const res = await withAssist(client, args.auto_unlock ?? false, () =>
|
|
1269
|
+
client.post<{ data: unknown }>(`/shorts/${args.slug}/text/generate`),
|
|
979
1270
|
);
|
|
980
|
-
|
|
981
|
-
return ok(status);
|
|
1271
|
+
return ok(asRecord(res).data ?? res);
|
|
982
1272
|
}),
|
|
983
1273
|
);
|
|
984
1274
|
|
|
1275
|
+
// ── Sliders (image carousels) ────────────────────────────────────────────────
|
|
1276
|
+
|
|
1277
|
+
registerTool(
|
|
1278
|
+
"create_slider",
|
|
1279
|
+
{
|
|
1280
|
+
title: "Create an image slider / carousel (draft)",
|
|
1281
|
+
description:
|
|
1282
|
+
"Creates a carousel draft from a prompt (min 10 chars). One prompt produces N still slides (an AI " +
|
|
1283
|
+
"background + a composited headline/body + optional logo) PLUS a ready-to-post caption and hashtags. " +
|
|
1284
|
+
"Returns the slug, costs 0 credits. Follow with generate_slider to render, then get_slider to read the " +
|
|
1285
|
+
"slide image URLs + caption. mode 'creative' tells a story; 'ad_driven' lists product facts/benefits.",
|
|
1286
|
+
inputSchema: {
|
|
1287
|
+
prompt: z.string().min(10).describe("What the carousel is about"),
|
|
1288
|
+
mode: z
|
|
1289
|
+
.enum(["creative", "ad_driven"])
|
|
1290
|
+
.optional()
|
|
1291
|
+
.describe(
|
|
1292
|
+
"'creative' (storytelling) or 'ad_driven' (facts about the product). Default creative.",
|
|
1293
|
+
),
|
|
1294
|
+
template: z
|
|
1295
|
+
.enum([
|
|
1296
|
+
"boldStatement",
|
|
1297
|
+
"editorialStory",
|
|
1298
|
+
"scrapbook",
|
|
1299
|
+
"featureGrid",
|
|
1300
|
+
"offerCard",
|
|
1301
|
+
"comparison",
|
|
1302
|
+
])
|
|
1303
|
+
.optional()
|
|
1304
|
+
.describe(
|
|
1305
|
+
"Composition preset. boldStatement/editorialStory/scrapbook are creative; " +
|
|
1306
|
+
"featureGrid/offerCard/comparison are ad-driven. Defaults to the mode default.",
|
|
1307
|
+
),
|
|
1308
|
+
slide_count: z
|
|
1309
|
+
.number()
|
|
1310
|
+
.optional()
|
|
1311
|
+
.describe("Number of slides, integer 3–10 (default 5)"),
|
|
1312
|
+
aspect_ratio: z
|
|
1313
|
+
.enum(["4:5", "1:1", "9:16"])
|
|
1314
|
+
.optional()
|
|
1315
|
+
.describe(
|
|
1316
|
+
"Canvas ratio (default 4:5, the highest-reach carousel ratio)",
|
|
1317
|
+
),
|
|
1318
|
+
accent_color: z
|
|
1319
|
+
.string()
|
|
1320
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
1321
|
+
.optional()
|
|
1322
|
+
.describe(
|
|
1323
|
+
"Brand accent: must start with #, exactly 6 hex digits, e.g. #09EFBE",
|
|
1324
|
+
),
|
|
1325
|
+
text_position: z
|
|
1326
|
+
.enum(["top", "middle", "bottom"])
|
|
1327
|
+
.optional()
|
|
1328
|
+
.describe(
|
|
1329
|
+
"Vertical placement of the on-image copy across all slides. Omit for the template's natural placement.",
|
|
1330
|
+
),
|
|
1331
|
+
},
|
|
1332
|
+
annotations: { title: "Create slider", ...WRITE, idempotentHint: true },
|
|
1333
|
+
},
|
|
1334
|
+
tool(
|
|
1335
|
+
async (
|
|
1336
|
+
args: {
|
|
1337
|
+
prompt: string;
|
|
1338
|
+
mode?: string;
|
|
1339
|
+
template?: string;
|
|
1340
|
+
slide_count?: number;
|
|
1341
|
+
aspect_ratio?: string;
|
|
1342
|
+
accent_color?: string;
|
|
1343
|
+
text_position?: string;
|
|
1344
|
+
},
|
|
1345
|
+
client,
|
|
1346
|
+
) => {
|
|
1347
|
+
// slide_count is a flat z.number() (see registerTool TS2589 note), so
|
|
1348
|
+
// enforce the integer 3..10 range here, matching the server changeset.
|
|
1349
|
+
if (args.slide_count !== undefined) {
|
|
1350
|
+
if (
|
|
1351
|
+
!Number.isInteger(args.slide_count) ||
|
|
1352
|
+
args.slide_count < 3 ||
|
|
1353
|
+
args.slide_count > 10
|
|
1354
|
+
) {
|
|
1355
|
+
return fail("slide_count must be an integer between 3 and 10");
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const body: Record<string, unknown> = { prompt: args.prompt };
|
|
1359
|
+
if (args.mode !== undefined) body.mode = args.mode;
|
|
1360
|
+
if (args.template !== undefined) body.template = args.template;
|
|
1361
|
+
if (args.slide_count !== undefined) body.slide_count = args.slide_count;
|
|
1362
|
+
if (args.aspect_ratio !== undefined)
|
|
1363
|
+
body.aspect_ratio = args.aspect_ratio;
|
|
1364
|
+
if (args.accent_color !== undefined)
|
|
1365
|
+
body.accent_color = args.accent_color;
|
|
1366
|
+
if (args.text_position !== undefined)
|
|
1367
|
+
body.text_position = args.text_position;
|
|
1368
|
+
const res = await client.post<{ data: { slug: string } }>(
|
|
1369
|
+
"/sliders",
|
|
1370
|
+
body,
|
|
1371
|
+
idemKey(
|
|
1372
|
+
"create-slider",
|
|
1373
|
+
args.prompt,
|
|
1374
|
+
args.mode ?? "",
|
|
1375
|
+
args.template ?? "",
|
|
1376
|
+
),
|
|
1377
|
+
);
|
|
1378
|
+
return ok({
|
|
1379
|
+
slug: res.data.slug,
|
|
1380
|
+
kind: "slider",
|
|
1381
|
+
next: "generate_slider, then get_slider to read the slide URLs + caption",
|
|
1382
|
+
});
|
|
1383
|
+
},
|
|
1384
|
+
),
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
registerTool(
|
|
1388
|
+
"generate_slider",
|
|
1389
|
+
{
|
|
1390
|
+
title: "Generate (render) a carousel",
|
|
1391
|
+
description:
|
|
1392
|
+
"Deducts 1 credit per slide (3–10 slides) and starts the pipeline (copy → AI backgrounds → composite) for an existing " +
|
|
1393
|
+
"carousel. Safe to call again: a duplicate while a render is already in flight is reported as still " +
|
|
1394
|
+
"processing (no double charge), and a failed carousel can be re-generated. Then poll with get_slider " +
|
|
1395
|
+
"until status is 'completed' or 'failed'.",
|
|
1396
|
+
inputSchema: {
|
|
1397
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1398
|
+
},
|
|
1399
|
+
annotations: { title: "Generate slider", ...WRITE, idempotentHint: true },
|
|
1400
|
+
},
|
|
1401
|
+
tool(async (args: { slug: string }, client) => {
|
|
1402
|
+
// No stable idempotency key on purpose: a per-slug key would replay the
|
|
1403
|
+
// first response for 24h and silently block an intentional re-generation
|
|
1404
|
+
// (e.g. retrying a failed render). Double-charge is prevented server-side
|
|
1405
|
+
// by the atomic generation claim, which returns 409 when a render is
|
|
1406
|
+
// already running — surface that as "still processing" rather than an error.
|
|
1407
|
+
try {
|
|
1408
|
+
const res = await client.post<{ data: Record<string, unknown> }>(
|
|
1409
|
+
`/sliders/${args.slug}/generate`,
|
|
1410
|
+
);
|
|
1411
|
+
const data = asRecord(asRecord(res).data);
|
|
1412
|
+
return ok({
|
|
1413
|
+
slug: args.slug,
|
|
1414
|
+
kind: "slider",
|
|
1415
|
+
status: data.status ?? "processing",
|
|
1416
|
+
});
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
if ((e as { status?: number }).status === 409) {
|
|
1419
|
+
return ok({ slug: args.slug, kind: "slider", status: "processing" });
|
|
1420
|
+
}
|
|
1421
|
+
throw e;
|
|
1422
|
+
}
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
registerTool(
|
|
1427
|
+
"get_slider",
|
|
1428
|
+
{
|
|
1429
|
+
title: "Get carousel status + deliverable",
|
|
1430
|
+
description:
|
|
1431
|
+
"Returns the carousel's status and, when completed, the per-slide image URLs (download these), the " +
|
|
1432
|
+
"per-slide headline/body/kicker, the caption, and the hashtags. status flows draft → processing → " +
|
|
1433
|
+
"completed | failed. completed:true means every slide image_url is ready to save. Once completed you " +
|
|
1434
|
+
"can restyle_slider (new template/accent) or edit_slider_slide (one slide's text) for free.",
|
|
1435
|
+
inputSchema: { slug: z.string().describe("Slider slug") },
|
|
1436
|
+
annotations: { title: "Get slider", ...RO },
|
|
1437
|
+
},
|
|
1438
|
+
tool(async (args: { slug: string }, client) => {
|
|
1439
|
+
const res = await client.get<{ data: Record<string, unknown> }>(
|
|
1440
|
+
`/sliders/${args.slug}`,
|
|
1441
|
+
);
|
|
1442
|
+
const data = asRecord(asRecord(res).data);
|
|
1443
|
+
const slides = Array.isArray(data.slides)
|
|
1444
|
+
? (data.slides as Record<string, unknown>[])
|
|
1445
|
+
: [];
|
|
1446
|
+
const status = (data.status as string) ?? "unknown";
|
|
1447
|
+
return ok({
|
|
1448
|
+
slug: args.slug,
|
|
1449
|
+
kind: "slider",
|
|
1450
|
+
status,
|
|
1451
|
+
stage: status,
|
|
1452
|
+
terminal: status === "completed" || status === "failed",
|
|
1453
|
+
// Trust the server's terminal status. The API completes a slider in one
|
|
1454
|
+
// transaction that writes every rendered key, so "completed" always implies
|
|
1455
|
+
// all slides[].image_url are ready — re-deriving it here only created an
|
|
1456
|
+
// ambiguous terminal:true / completed:false dead-end on an edge response.
|
|
1457
|
+
completed: status === "completed",
|
|
1458
|
+
error: (data.error_message as string) ?? null,
|
|
1459
|
+
caption: (data.caption as string) ?? null,
|
|
1460
|
+
hashtags: Array.isArray(data.hashtags) ? data.hashtags : [],
|
|
1461
|
+
slides: slides.map((s) => ({
|
|
1462
|
+
position: s.position,
|
|
1463
|
+
image_url: s.image_url ?? null,
|
|
1464
|
+
headline: s.headline ?? null,
|
|
1465
|
+
body: s.body ?? null,
|
|
1466
|
+
kicker: s.kicker ?? null,
|
|
1467
|
+
status: s.status ?? null,
|
|
1468
|
+
})),
|
|
1469
|
+
});
|
|
1470
|
+
}),
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
registerTool(
|
|
1474
|
+
"restyle_slider",
|
|
1475
|
+
{
|
|
1476
|
+
title: "Restyle a carousel (free re-composite of every slide)",
|
|
1477
|
+
description:
|
|
1478
|
+
"Re-composites ALL slides of a completed carousel under a new template, accent color and/or text " +
|
|
1479
|
+
"position — for FREE (0 credits). Reuses the already-generated AI backgrounds, so no new images are paid for. The " +
|
|
1480
|
+
"carousel must already be 'completed' (generate it first); a draft/processing carousel returns a " +
|
|
1481
|
+
"409 conflict. Runs async: it marks every slide processing, so poll get_slider until status is back " +
|
|
1482
|
+
"to 'completed' (or 'failed'). Omit a field to leave it unchanged.",
|
|
1483
|
+
inputSchema: {
|
|
1484
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1485
|
+
template: z
|
|
1486
|
+
.enum([
|
|
1487
|
+
"boldStatement",
|
|
1488
|
+
"editorialStory",
|
|
1489
|
+
"scrapbook",
|
|
1490
|
+
"featureGrid",
|
|
1491
|
+
"offerCard",
|
|
1492
|
+
"comparison",
|
|
1493
|
+
])
|
|
1494
|
+
.optional()
|
|
1495
|
+
.describe(
|
|
1496
|
+
"New composition preset. boldStatement/editorialStory/scrapbook are creative; " +
|
|
1497
|
+
"featureGrid/offerCard/comparison are ad-driven.",
|
|
1498
|
+
),
|
|
1499
|
+
accent_color: z
|
|
1500
|
+
.string()
|
|
1501
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
1502
|
+
.optional()
|
|
1503
|
+
.describe(
|
|
1504
|
+
"New brand accent: must start with #, exactly 6 hex digits, e.g. #09EFBE",
|
|
1505
|
+
),
|
|
1506
|
+
text_position: z
|
|
1507
|
+
.enum(["top", "middle", "bottom"])
|
|
1508
|
+
.optional()
|
|
1509
|
+
.describe(
|
|
1510
|
+
"New vertical placement of the on-image copy across all slides.",
|
|
1511
|
+
),
|
|
1512
|
+
},
|
|
1513
|
+
annotations: { title: "Restyle slider", ...WRITE, idempotentHint: false },
|
|
1514
|
+
},
|
|
1515
|
+
tool(
|
|
1516
|
+
async (
|
|
1517
|
+
args: {
|
|
1518
|
+
slug: string;
|
|
1519
|
+
template?: string;
|
|
1520
|
+
accent_color?: string;
|
|
1521
|
+
text_position?: string;
|
|
1522
|
+
},
|
|
1523
|
+
client,
|
|
1524
|
+
) => {
|
|
1525
|
+
const body: Record<string, unknown> = {};
|
|
1526
|
+
if (args.template !== undefined) body.template = args.template;
|
|
1527
|
+
if (args.accent_color !== undefined)
|
|
1528
|
+
body.accent_color = args.accent_color;
|
|
1529
|
+
if (args.text_position !== undefined)
|
|
1530
|
+
body.text_position = args.text_position;
|
|
1531
|
+
try {
|
|
1532
|
+
const res = await client.post<{ data: Record<string, unknown> }>(
|
|
1533
|
+
`/sliders/${args.slug}/restyle`,
|
|
1534
|
+
body,
|
|
1535
|
+
);
|
|
1536
|
+
const data = asRecord(asRecord(res).data);
|
|
1537
|
+
return ok({
|
|
1538
|
+
slug: args.slug,
|
|
1539
|
+
kind: "slider",
|
|
1540
|
+
status: data.status ?? "processing",
|
|
1541
|
+
next: "poll get_slider until status is 'completed' (free re-composite, 0 credits)",
|
|
1542
|
+
});
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
// 409 = carousel not completed yet, or a re-composite already running.
|
|
1545
|
+
if ((e as { status?: number }).status === 409) {
|
|
1546
|
+
return ok({ slug: args.slug, kind: "slider", status: "processing" });
|
|
1547
|
+
}
|
|
1548
|
+
throw e;
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
),
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
registerTool(
|
|
1555
|
+
"edit_slider_slide",
|
|
1556
|
+
{
|
|
1557
|
+
title: "Edit one carousel slide's on-image text (free re-composite)",
|
|
1558
|
+
description:
|
|
1559
|
+
"Rewrites the on-image copy of a SINGLE slide (by position) and re-composites just that slide — for " +
|
|
1560
|
+
"FREE (0 credits). Reuses the slide's existing AI background, so no new image is paid for. The " +
|
|
1561
|
+
"carousel must already be 'completed' (a draft/processing carousel returns a 409 conflict). Runs " +
|
|
1562
|
+
"async: the slide goes back to processing, so poll get_slider until its status is 'completed'. Pass " +
|
|
1563
|
+
"only the fields you want to change. kicker = the small eyebrow/label line above the headline.",
|
|
1564
|
+
inputSchema: {
|
|
1565
|
+
slug: z.string().describe("Slider slug from create_slider"),
|
|
1566
|
+
position: z
|
|
1567
|
+
.number()
|
|
1568
|
+
.int()
|
|
1569
|
+
.min(1)
|
|
1570
|
+
.describe("1-based slide position (from get_slider)"),
|
|
1571
|
+
headline: z
|
|
1572
|
+
.string()
|
|
1573
|
+
.max(120)
|
|
1574
|
+
.optional()
|
|
1575
|
+
.describe("Slide headline, ≤120 chars"),
|
|
1576
|
+
body: z.string().max(600).optional().describe("Slide body, ≤600 chars"),
|
|
1577
|
+
kicker: z
|
|
1578
|
+
.string()
|
|
1579
|
+
.max(40)
|
|
1580
|
+
.optional()
|
|
1581
|
+
.describe("Small eyebrow/label line above the headline, ≤40 chars"),
|
|
1582
|
+
},
|
|
1583
|
+
annotations: {
|
|
1584
|
+
title: "Edit slider slide",
|
|
1585
|
+
...WRITE,
|
|
1586
|
+
idempotentHint: false,
|
|
1587
|
+
},
|
|
1588
|
+
},
|
|
1589
|
+
tool(
|
|
1590
|
+
async (
|
|
1591
|
+
args: {
|
|
1592
|
+
slug: string;
|
|
1593
|
+
position: number;
|
|
1594
|
+
headline?: string;
|
|
1595
|
+
body?: string;
|
|
1596
|
+
kicker?: string;
|
|
1597
|
+
},
|
|
1598
|
+
client,
|
|
1599
|
+
) => {
|
|
1600
|
+
const body: Record<string, unknown> = {};
|
|
1601
|
+
if (args.headline !== undefined) body.headline = args.headline;
|
|
1602
|
+
if (args.body !== undefined) body.body = args.body;
|
|
1603
|
+
if (args.kicker !== undefined) body.kicker = args.kicker;
|
|
1604
|
+
try {
|
|
1605
|
+
const res = await client.patch<{ data: Record<string, unknown> }>(
|
|
1606
|
+
`/sliders/${args.slug}/slides/${args.position}`,
|
|
1607
|
+
body,
|
|
1608
|
+
);
|
|
1609
|
+
const data = asRecord(asRecord(res).data);
|
|
1610
|
+
return ok({
|
|
1611
|
+
slug: args.slug,
|
|
1612
|
+
kind: "slider",
|
|
1613
|
+
position: args.position,
|
|
1614
|
+
status: data.status ?? "processing",
|
|
1615
|
+
next: "poll get_slider until the slide's status is 'completed' (free re-composite, 0 credits)",
|
|
1616
|
+
});
|
|
1617
|
+
} catch (e) {
|
|
1618
|
+
// 409 = carousel not completed yet, or a re-composite already running.
|
|
1619
|
+
if ((e as { status?: number }).status === 409) {
|
|
1620
|
+
return ok({
|
|
1621
|
+
slug: args.slug,
|
|
1622
|
+
kind: "slider",
|
|
1623
|
+
position: args.position,
|
|
1624
|
+
status: "processing",
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
throw e;
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
),
|
|
1631
|
+
);
|
|
1632
|
+
|
|
985
1633
|
// ── Editor ───────────────────────────────────────────────────────────────────
|
|
986
1634
|
|
|
987
1635
|
registerTool(
|
|
@@ -1006,11 +1654,21 @@ registerTool(
|
|
|
1006
1654
|
.string()
|
|
1007
1655
|
.optional()
|
|
1008
1656
|
.describe('Language code, e.g. "en" (default)'),
|
|
1657
|
+
creative_format: z
|
|
1658
|
+
.enum(CREATIVE_FORMATS)
|
|
1659
|
+
.optional()
|
|
1660
|
+
.describe("Narrative arc / segment structure. Omit for Auto/generic."),
|
|
1661
|
+
visual_language: z
|
|
1662
|
+
.enum(VISUAL_LANGUAGES)
|
|
1663
|
+
.optional()
|
|
1664
|
+
.describe(
|
|
1665
|
+
"Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
|
|
1666
|
+
),
|
|
1009
1667
|
theme: z
|
|
1010
1668
|
.string()
|
|
1011
1669
|
.optional()
|
|
1012
1670
|
.describe(
|
|
1013
|
-
'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.',
|
|
1671
|
+
'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.',
|
|
1014
1672
|
),
|
|
1015
1673
|
voice_id: z
|
|
1016
1674
|
.string()
|
|
@@ -1031,11 +1689,24 @@ registerTool(
|
|
|
1031
1689
|
{
|
|
1032
1690
|
language: args.language ?? "en",
|
|
1033
1691
|
product_prompt: args.product_prompt,
|
|
1692
|
+
creative_format: args.creative_format,
|
|
1693
|
+
visual_language: args.visual_language,
|
|
1034
1694
|
theme: args.theme,
|
|
1035
1695
|
voice_id: args.voice_id,
|
|
1036
1696
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
1037
1697
|
},
|
|
1038
|
-
|
|
1698
|
+
// Fold every create-affecting param into the key so two calls that
|
|
1699
|
+
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
1700
|
+
idemKey(
|
|
1701
|
+
"create-editor",
|
|
1702
|
+
String(args.product_prompt ?? ""),
|
|
1703
|
+
String(args.language ?? "en"),
|
|
1704
|
+
String(args.creative_format ?? ""),
|
|
1705
|
+
String(args.visual_language ?? ""),
|
|
1706
|
+
String(args.theme ?? ""),
|
|
1707
|
+
String(args.voice_id ?? ""),
|
|
1708
|
+
String(args.export_aspect_ratio ?? ""),
|
|
1709
|
+
),
|
|
1039
1710
|
);
|
|
1040
1711
|
const slug = created.data.slug;
|
|
1041
1712
|
const started = await client.post<{ data: unknown }>(
|
|
@@ -1153,11 +1824,21 @@ registerTool(
|
|
|
1153
1824
|
.max(10)
|
|
1154
1825
|
.optional()
|
|
1155
1826
|
.describe('Language code, e.g. "en" (default)'),
|
|
1827
|
+
creative_format: z
|
|
1828
|
+
.enum(CREATIVE_FORMATS)
|
|
1829
|
+
.optional()
|
|
1830
|
+
.describe("Narrative arc / segment structure. Omit for Auto/generic."),
|
|
1831
|
+
visual_language: z
|
|
1832
|
+
.enum(VISUAL_LANGUAGES)
|
|
1833
|
+
.optional()
|
|
1834
|
+
.describe(
|
|
1835
|
+
"Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
|
|
1836
|
+
),
|
|
1156
1837
|
theme: z
|
|
1157
1838
|
.string()
|
|
1158
1839
|
.optional()
|
|
1159
1840
|
.describe(
|
|
1160
|
-
'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.',
|
|
1841
|
+
'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.',
|
|
1161
1842
|
),
|
|
1162
1843
|
voice_id: z
|
|
1163
1844
|
.string()
|
|
@@ -1187,6 +1868,8 @@ registerTool(
|
|
|
1187
1868
|
args: {
|
|
1188
1869
|
product_prompt?: string;
|
|
1189
1870
|
language?: string;
|
|
1871
|
+
creative_format?: string;
|
|
1872
|
+
visual_language?: string;
|
|
1190
1873
|
theme?: string;
|
|
1191
1874
|
voice_id?: string;
|
|
1192
1875
|
export_aspect_ratio?: string;
|
|
@@ -1197,6 +1880,8 @@ registerTool(
|
|
|
1197
1880
|
const prompt = args.product_prompt?.trim();
|
|
1198
1881
|
const body: Record<string, unknown> = {
|
|
1199
1882
|
language: args.language ?? "en",
|
|
1883
|
+
creative_format: args.creative_format,
|
|
1884
|
+
visual_language: args.visual_language,
|
|
1200
1885
|
theme: args.theme,
|
|
1201
1886
|
voice_id: args.voice_id,
|
|
1202
1887
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
@@ -1206,7 +1891,14 @@ registerTool(
|
|
|
1206
1891
|
const created = await client.post<{ data: { slug: string } }>(
|
|
1207
1892
|
"/editor",
|
|
1208
1893
|
body,
|
|
1209
|
-
idemKey(
|
|
1894
|
+
idemKey(
|
|
1895
|
+
"create-editor-draft",
|
|
1896
|
+
prompt ?? "",
|
|
1897
|
+
args.language ?? "en",
|
|
1898
|
+
args.creative_format ?? "",
|
|
1899
|
+
args.visual_language ?? "",
|
|
1900
|
+
args.theme ?? "",
|
|
1901
|
+
),
|
|
1210
1902
|
);
|
|
1211
1903
|
return ok({
|
|
1212
1904
|
slug: created.data.slug,
|
|
@@ -1267,7 +1959,9 @@ registerTool(
|
|
|
1267
1959
|
description:
|
|
1268
1960
|
"Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " +
|
|
1269
1961
|
"(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " +
|
|
1270
|
-
"write the scenario yourself with set_scenario. Server default segments_count is 5."
|
|
1962
|
+
"write the scenario yourself with set_scenario. Server default segments_count is 5. " +
|
|
1963
|
+
"Optionally pass creative_format / visual_language / theme to persist the project\u2019s creative style before " +
|
|
1964
|
+
"generating (this is the granular path\u2019s way to set/change style; PATCH /scenario does not).",
|
|
1271
1965
|
inputSchema: {
|
|
1272
1966
|
slug: z.string().describe("Editor project slug"),
|
|
1273
1967
|
segments_count: z
|
|
@@ -1277,6 +1971,24 @@ registerTool(
|
|
|
1277
1971
|
.max(10)
|
|
1278
1972
|
.optional()
|
|
1279
1973
|
.describe("How many scenes (3–10, server default 5)"),
|
|
1974
|
+
creative_format: z
|
|
1975
|
+
.enum(CREATIVE_FORMATS)
|
|
1976
|
+
.optional()
|
|
1977
|
+
.describe(
|
|
1978
|
+
"Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear.",
|
|
1979
|
+
),
|
|
1980
|
+
visual_language: z
|
|
1981
|
+
.enum(VISUAL_LANGUAGES)
|
|
1982
|
+
.optional()
|
|
1983
|
+
.describe(
|
|
1984
|
+
"Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored.",
|
|
1985
|
+
),
|
|
1986
|
+
theme: z
|
|
1987
|
+
.string()
|
|
1988
|
+
.optional()
|
|
1989
|
+
.describe(
|
|
1990
|
+
"Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none).",
|
|
1991
|
+
),
|
|
1280
1992
|
auto_unlock: z
|
|
1281
1993
|
.boolean()
|
|
1282
1994
|
.optional()
|
|
@@ -1295,6 +2007,9 @@ registerTool(
|
|
|
1295
2007
|
args: {
|
|
1296
2008
|
slug: string;
|
|
1297
2009
|
segments_count?: number;
|
|
2010
|
+
creative_format?: string;
|
|
2011
|
+
visual_language?: string;
|
|
2012
|
+
theme?: string;
|
|
1298
2013
|
auto_unlock?: boolean;
|
|
1299
2014
|
},
|
|
1300
2015
|
client,
|
|
@@ -1302,6 +2017,11 @@ registerTool(
|
|
|
1302
2017
|
const body: Record<string, unknown> = {};
|
|
1303
2018
|
if (args.segments_count !== undefined)
|
|
1304
2019
|
body.segments_count = args.segments_count;
|
|
2020
|
+
if (args.creative_format !== undefined)
|
|
2021
|
+
body.creative_format = args.creative_format;
|
|
2022
|
+
if (args.visual_language !== undefined)
|
|
2023
|
+
body.visual_language = args.visual_language;
|
|
2024
|
+
if (args.theme !== undefined) body.theme = args.theme;
|
|
1305
2025
|
const res = await withAssist(client, args.auto_unlock ?? false, () =>
|
|
1306
2026
|
client.post<{ data: unknown }>(
|
|
1307
2027
|
`/editor/${args.slug}/generate-scenario`,
|