@clipwright/core 0.33.0 → 0.35.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/CHANGELOG.md +251 -0
- package/README.md +3 -1
- package/dist/account-actors.d.ts +173 -14
- package/dist/account-actors.js +142 -10
- package/dist/account-actors.js.map +1 -1
- package/dist/actor-bounds.d.ts +4 -0
- package/dist/actor-bounds.js +4 -0
- package/dist/actor-bounds.js.map +1 -1
- package/dist/actors.d.ts +31 -0
- package/dist/actors.js +26 -0
- package/dist/actors.js.map +1 -1
- package/dist/api-failure.js +2 -1
- package/dist/api-failure.js.map +1 -1
- package/dist/asset-identity.d.ts +6 -0
- package/dist/asset-identity.js +7 -0
- package/dist/asset-identity.js.map +1 -0
- package/dist/assets.d.ts +403 -8
- package/dist/assets.js +132 -22
- package/dist/assets.js.map +1 -1
- package/dist/billing-terms.d.ts +69 -0
- package/dist/billing-terms.js +8 -0
- package/dist/billing-terms.js.map +1 -1
- package/dist/caption-language.d.ts +2 -0
- package/dist/caption-language.js +17 -0
- package/dist/caption-language.js.map +1 -0
- package/dist/contract-dispositions.d.ts +1617 -8
- package/dist/contract-dispositions.js +75 -11
- package/dist/contract-dispositions.js.map +1 -1
- package/dist/contracts/v1/workflow-brief.d.ts +3166 -78
- package/dist/contracts/v1/workflow-brief.js +4 -0
- package/dist/contracts/v1/workflow-brief.js.map +1 -1
- package/dist/contracts/v1/workflow-catalog.d.ts +3712 -8
- package/dist/contracts/v1/workflow-catalog.js +4 -2
- package/dist/contracts/v1/workflow-catalog.js.map +1 -1
- package/dist/contracts/v1/workflow-client.d.ts +2466 -4
- package/dist/contracts/v1/workflow-client.js +6 -3
- package/dist/contracts/v1/workflow-client.js.map +1 -1
- package/dist/contracts/v1/workflow-execution.d.ts +163 -9
- package/dist/contracts/v1/workflow-execution.js +15 -7
- package/dist/contracts/v1/workflow-execution.js.map +1 -1
- package/dist/contracts/v1/workflow-plan.d.ts +6014 -487
- package/dist/contracts/v1/workflow-primitives.d.ts +3 -1
- package/dist/contracts/v1/workflow-primitives.js +1 -1
- package/dist/contracts/v1/workflow-primitives.js.map +1 -1
- package/dist/contracts/v1/workflow-run-read.d.ts +114 -1
- package/dist/edit-billing.d.ts +12 -0
- package/dist/edit-billing.js +8 -0
- package/dist/edit-billing.js.map +1 -1
- package/dist/edit-long-form.d.ts +1 -1
- package/dist/edit-long-form.js +12 -2
- package/dist/edit-long-form.js.map +1 -1
- package/dist/endpoint-notes.d.ts +3 -0
- package/dist/endpoint-notes.js +66 -0
- package/dist/endpoint-notes.js.map +1 -0
- package/dist/endpoints.d.ts +4 -0
- package/dist/endpoints.js +15 -0
- package/dist/endpoints.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/face-billing.d.ts +55 -0
- package/dist/face-billing.js +22 -0
- package/dist/face-billing.js.map +1 -1
- package/dist/faceless-price.d.ts +26 -2
- package/dist/faceless-price.js +33 -6
- package/dist/faceless-price.js.map +1 -1
- package/dist/faceless-public.d.ts +6 -1
- package/dist/faceless-public.js +2 -2
- package/dist/faceless-public.js.map +1 -1
- package/dist/generations.d.ts +32 -6
- package/dist/generations.js +82 -27
- package/dist/generations.js.map +1 -1
- package/dist/idempotency.d.ts +2 -0
- package/dist/idempotency.js +7 -4
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/long-form-pricing.d.ts +7 -4
- package/dist/long-form-pricing.js +13 -10
- package/dist/long-form-pricing.js.map +1 -1
- package/dist/long-form-revision.js +1 -1
- package/dist/long-form-revision.js.map +1 -1
- package/dist/openapi.d.ts +154 -0
- package/dist/openapi.js +334 -0
- package/dist/openapi.js.map +1 -0
- package/dist/pacing.d.ts +15 -0
- package/dist/pacing.js +30 -0
- package/dist/pacing.js.map +1 -1
- package/dist/parts-pricing.d.ts +84 -0
- package/dist/parts-pricing.js +105 -0
- package/dist/parts-pricing.js.map +1 -0
- package/dist/presenter-beats.d.ts +45 -0
- package/dist/presenter-beats.js +144 -0
- package/dist/presenter-beats.js.map +1 -0
- package/dist/prices.d.ts +98 -0
- package/dist/prices.js +189 -0
- package/dist/prices.js.map +1 -0
- package/dist/public-endpoints.d.ts +2 -0
- package/dist/public-endpoints.js +2 -0
- package/dist/public-endpoints.js.map +1 -1
- package/dist/public-responses.d.ts +23 -0
- package/dist/public-responses.js +11 -0
- package/dist/public-responses.js.map +1 -0
- package/dist/reference-analysis.d.ts +683 -0
- package/dist/reference-analysis.js +91 -0
- package/dist/reference-analysis.js.map +1 -0
- package/dist/reference-kinds.d.ts +6 -0
- package/dist/reference-kinds.js +8 -0
- package/dist/reference-kinds.js.map +1 -0
- package/dist/reference-profile.d.ts +633 -0
- package/dist/reference-profile.js +151 -0
- package/dist/reference-profile.js.map +1 -0
- package/dist/reference-staging.d.ts +571 -0
- package/dist/reference-staging.js +151 -0
- package/dist/reference-staging.js.map +1 -0
- package/dist/reference-voice.d.ts +305 -0
- package/dist/reference-voice.js +108 -0
- package/dist/reference-voice.js.map +1 -0
- package/dist/resolved-timeline.d.ts +206 -1
- package/dist/resolved-timeline.js +96 -4
- package/dist/resolved-timeline.js.map +1 -1
- package/dist/run-static-warnings.js +19 -7
- package/dist/run-static-warnings.js.map +1 -1
- package/dist/runs.d.ts +75 -3
- package/dist/runs.js +6 -1
- package/dist/runs.js.map +1 -1
- package/dist/semantic-inserts.d.ts +7 -0
- package/dist/semantic-inserts.js +19 -9
- package/dist/semantic-inserts.js.map +1 -1
- package/dist/skills.d.ts +5838 -22
- package/dist/skills.js +380 -25
- package/dist/skills.js.map +1 -1
- package/dist/templates/callout.d.ts +34 -0
- package/dist/templates/callout.js +51 -0
- package/dist/templates/callout.js.map +1 -0
- package/dist/templates/checklist.d.ts +42 -0
- package/dist/templates/checklist.js +47 -0
- package/dist/templates/checklist.js.map +1 -0
- package/dist/templates/compare.d.ts +26 -0
- package/dist/templates/compare.js +35 -0
- package/dist/templates/compare.js.map +1 -0
- package/dist/templates/end-card.d.ts +16 -0
- package/dist/templates/end-card.js +25 -0
- package/dist/templates/end-card.js.map +1 -0
- package/dist/templates/index.d.ts +171 -0
- package/dist/templates/index.js +394 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/lower-third.d.ts +18 -0
- package/dist/templates/lower-third.js +26 -0
- package/dist/templates/lower-third.js.map +1 -0
- package/dist/templates/notification.d.ts +30 -0
- package/dist/templates/notification.js +32 -0
- package/dist/templates/notification.js.map +1 -0
- package/dist/templates/phone-call.d.ts +27 -0
- package/dist/templates/phone-call.js +43 -0
- package/dist/templates/phone-call.js.map +1 -0
- package/dist/templates/progress.d.ts +22 -0
- package/dist/templates/progress.js +42 -0
- package/dist/templates/progress.js.map +1 -0
- package/dist/templates/quote.d.ts +21 -0
- package/dist/templates/quote.js +29 -0
- package/dist/templates/quote.js.map +1 -0
- package/dist/templates/rating.d.ts +22 -0
- package/dist/templates/rating.js +29 -0
- package/dist/templates/rating.js.map +1 -0
- package/dist/templates/size.d.ts +13 -0
- package/dist/templates/size.js +10 -0
- package/dist/templates/size.js.map +1 -0
- package/dist/templates/slot.d.ts +10 -0
- package/dist/templates/slot.js +5 -0
- package/dist/templates/slot.js.map +1 -0
- package/dist/templates/sounds.d.ts +701 -0
- package/dist/templates/sounds.js +89 -0
- package/dist/templates/sounds.js.map +1 -0
- package/dist/templates/stat.d.ts +20 -0
- package/dist/templates/stat.js +33 -0
- package/dist/templates/stat.js.map +1 -0
- package/dist/templates/tag.d.ts +23 -0
- package/dist/templates/tag.js +31 -0
- package/dist/templates/tag.js.map +1 -0
- package/dist/templates/theme.d.ts +29 -0
- package/dist/templates/theme.js +47 -0
- package/dist/templates/theme.js.map +1 -0
- package/dist/templates/title-card.d.ts +21 -0
- package/dist/templates/title-card.js +30 -0
- package/dist/templates/title-card.js.map +1 -0
- package/dist/tts-backend.d.ts +9 -0
- package/dist/tts-backend.js +21 -0
- package/dist/tts-backend.js.map +1 -1
- package/dist/ugc-call.d.ts +89 -0
- package/dist/ugc-call.js +111 -0
- package/dist/ugc-call.js.map +1 -0
- package/dist/ugc-framing.d.ts +26 -0
- package/dist/ugc-framing.js +64 -0
- package/dist/ugc-framing.js.map +1 -0
- package/dist/ugc-layers.d.ts +88 -0
- package/dist/ugc-layers.js +142 -0
- package/dist/ugc-layers.js.map +1 -0
- package/dist/ugc-video-media.d.ts +12 -0
- package/dist/ugc-video-media.js +39 -0
- package/dist/ugc-video-media.js.map +1 -0
- package/dist/vendor-response.d.ts +4 -0
- package/dist/vendor-response.js +12 -0
- package/dist/vendor-response.js.map +1 -1
- package/dist/voice-admission.d.ts +1244 -1
- package/dist/voice-listing.js +7 -1
- package/dist/voice-listing.js.map +1 -1
- package/dist/voice-pace.d.ts +34 -0
- package/dist/voice-pace.generated.json +72 -0
- package/dist/voice-pace.js +34 -0
- package/dist/voice-pace.js.map +1 -0
- package/dist/voices.d.ts +21 -8
- package/dist/voices.js +44 -19
- package/dist/voices.js.map +1 -1
- package/dist/workflows/catalog.js +2 -1
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -0
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/methods/faceless-explainer.d.ts +697 -3
- package/dist/workflows/methods/kinetic-launch.d.ts +697 -3
- package/dist/workflows/methods/product-image-story.d.ts +697 -3
- package/dist/workflows/methods/reference-remake.d.ts +870 -0
- package/dist/workflows/methods/reference-remake.js +26 -0
- package/dist/workflows/methods/reference-remake.js.map +1 -0
- package/dist/workflows/methods/shared.d.ts +697 -3
- package/dist/workflows/methods/ugc-faq-answer.d.ts +697 -3
- package/dist/workflows/methods/ugc-product-intro.d.ts +697 -3
- package/dist/workflows/methods/website-proof-demo.d.ts +697 -3
- package/dist/workflows/reference-remake.d.ts +663 -0
- package/dist/workflows/reference-remake.js +598 -0
- package/dist/workflows/reference-remake.js.map +1 -0
- package/dist/workflows/validate.js +19 -11
- package/dist/workflows/validate.js.map +1 -1
- package/package.json +9 -3
package/dist/skills.js
CHANGED
|
@@ -1,10 +1,99 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ASSET_KINDS, ASSET_MEDIA_TYPES, assetId } from "./asset-identity.js";
|
|
2
3
|
import { longFormQuoteSchema } from "./long-form-pricing.js";
|
|
4
|
+
import { partsLineSchema } from "./parts-pricing.js";
|
|
3
5
|
import { RESOLVED_TIMELINE_MAX_SLOTS } from "./long-form-profile.js";
|
|
4
6
|
import { brollPolicy } from "./broll-policy.js";
|
|
5
7
|
import { countWords } from "./pacing.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
+
import { pauseTrim, VOICE_PACE_MAX, VOICE_PACE_MIN } from "./reference-voice.js";
|
|
9
|
+
import { lexicalTokens, quoteOccurrences } from "./semantic-inserts.js";
|
|
10
|
+
import { INSERT_TRANSITIONS, LAYER_ENTERS, layerRefusals, LAYOUT_WINDOW_KINDS, MAX_LAYOUT_WINDOWS, MAX_OVERLAYS, OVERLAY_POSITIONS, PRESENTER_FRAMINGS } from "./ugc-layers.js";
|
|
11
|
+
import { FRAMING_SIZES, FRAMING_TRANSITIONS, framingRefusals, MAX_FRAMING } from "./ugc-framing.js";
|
|
12
|
+
import { stagingFaceRect, stagingRect } from "./reference-staging.js";
|
|
13
|
+
import { CALL_PHONE_DEFAULTS, CALL_SPEAKERS, CALL_STAGE_MAX_ENTER_SEC, CALL_STAGE_MAX_LEAD_SEC, CALL_STAGE_SIDES, callRefusals, DEFAULT_CALL_VOICES, MAX_CALL_CHARS, MAX_CALL_LINE_CHARS, MAX_CALL_LINES, MAX_CALL_SEC } from "./ugc-call.js";
|
|
14
|
+
import { editSoundRefusals, editSounds, MAX_TEMPLATE_MEDIA, MAX_TEMPLATES, TEMPLATE_KIND_IDS, TEMPLATE_PLACEMENTS, templateCatalogText, templateRefusals, templateSounds, templateTheme } from "./templates/index.js";
|
|
15
|
+
import { callout } from "./templates/callout.js";
|
|
16
|
+
import { checklist } from "./templates/checklist.js";
|
|
17
|
+
import { compare } from "./templates/compare.js";
|
|
18
|
+
import { endCard } from "./templates/end-card.js";
|
|
19
|
+
import { lowerThird } from "./templates/lower-third.js";
|
|
20
|
+
import { notification } from "./templates/notification.js";
|
|
21
|
+
import { phoneCall } from "./templates/phone-call.js";
|
|
22
|
+
import { progress } from "./templates/progress.js";
|
|
23
|
+
import { quote } from "./templates/quote.js";
|
|
24
|
+
import { rating } from "./templates/rating.js";
|
|
25
|
+
import { stat } from "./templates/stat.js";
|
|
26
|
+
import { tag } from "./templates/tag.js";
|
|
27
|
+
import { titleCard } from "./templates/title-card.js";
|
|
28
|
+
import { cutToNothingError, DEFAULT_TTS_MODEL, GOOGLE_TTS_MODEL, isModelVoiceName, MODEL_DEFAULT_VOICE_BY_GENDER, MODEL_VOICE_MODEL, resolveTtsModel, MAX_SCRIPT_CHARS, SCRIPT_LENGTH_DESCRIPTION, speaksOwnVoices, STRESS_MARKING_DESCRIPTION, ttsScriptLimitError, TTS_MODELS, isVoicePresetName, VOICE_ID_PATTERN, VOICE_NAME_PATTERN, VOICE_PRESET_NAMES, } from "./voices.js";
|
|
29
|
+
/** Caption looks the renderer draws, each a fixed font, colours, placement and word animation. */
|
|
30
|
+
export const CAPTION_PRESETS = {
|
|
31
|
+
hormozi: "the default: heavy white uppercase with a thick black outline, up to three words per cue, one keyword per cue in yellow " +
|
|
32
|
+
"(emphasised, else the longest non-stop-word)",
|
|
33
|
+
accent: "condensed bold white uppercase, no outline, up to three words per cue, one keyword per cue in orange " +
|
|
34
|
+
"(emphasised, else the longest non-stop-word)",
|
|
35
|
+
tiktok: "bold rounded uppercase, one or two words at a time; the spoken word turns yellow and grows",
|
|
36
|
+
youtube: "clean bold sans-serif, five words on a dark rounded box near the bottom; words fade in, the spoken word in red",
|
|
37
|
+
instagram: "comic uppercase with a pink outline, two words at a time; the spoken word pops in orange",
|
|
38
|
+
minimalist: "plain sans-serif, five words on a half-transparent dark band, gentle fade",
|
|
39
|
+
neon: "cyan uppercase with a magenta outline, two words at a time; the spoken word pops in white",
|
|
40
|
+
genz: "large comic uppercase, one word at a time, slamming in with a pink highlight",
|
|
41
|
+
campfire: "rounded comic uppercase, three words; spoken words light up gold and stay lit",
|
|
42
|
+
midnight: "spooky uppercase, two words, slow fade; the spoken word in red",
|
|
43
|
+
plottwist: "heavy sans-serif, three words; the spoken word turns sky blue and grows",
|
|
44
|
+
headline: "tall condensed uppercase on a solid black band, three words; the spoken word scales up in yellow",
|
|
45
|
+
promo: "heavy sans-serif on a blue pill, three words; the spoken word pops in yellow",
|
|
46
|
+
lecture: "readable sans-serif, six words on a dark rounded box; the spoken word in green",
|
|
47
|
+
highlighter: "heavy sans-serif, four words; spoken words turn lime and stay lit",
|
|
48
|
+
studio: "sans-serif, seven words typed in on a blurred dark box; the spoken word in amber",
|
|
49
|
+
broadcast: "outlined sans-serif, eight words low in the frame; the spoken word in pale yellow",
|
|
50
|
+
arcade: "blocky yellow uppercase, two words; the spoken word bounces in white",
|
|
51
|
+
clutch: "brush uppercase, one word at a time, slamming in green",
|
|
52
|
+
bubblegum: "rounded display letters with a pink outline, two words; the spoken word bounces in yellow",
|
|
53
|
+
crayon: "marker handwriting with a navy outline, two words; the spoken word pops in orange",
|
|
54
|
+
lyrics: "handwritten script, five words; spoken words light up pink and stay lit",
|
|
55
|
+
bassdrop: "outlined display uppercase, one word at a time, scaling in orange",
|
|
56
|
+
punch: "mid-frame setup and punch: a small white lead-in line over one or two large blue glowing keywords; other words small and white",
|
|
57
|
+
};
|
|
58
|
+
export const CAPTION_PRESET_IDS = Object.keys(CAPTION_PRESETS);
|
|
59
|
+
/** An earlier style name kept valid. */
|
|
60
|
+
export const CAPTION_STYLE_ALIASES = { minimal: "minimalist" };
|
|
61
|
+
export const CAPTION_STYLES = [...CAPTION_PRESET_IDS, ...Object.keys(CAPTION_STYLE_ALIASES)];
|
|
62
|
+
/** The preset a style name draws: an alias resolves to its preset. */
|
|
63
|
+
export function captionPresetOf(style) {
|
|
64
|
+
return style in CAPTION_STYLE_ALIASES ? CAPTION_STYLE_ALIASES[style] : style;
|
|
65
|
+
}
|
|
66
|
+
const CAPTION_STYLE_DESCRIPTION = "Caption look when captions=true. " +
|
|
67
|
+
CAPTION_PRESET_IDS.map((id) => `${id}: ${CAPTION_PRESETS[id]}.`).join(" ") +
|
|
68
|
+
" Alias: minimal draws minimalist. A font that cannot draw the script's letters is replaced by a plain sans-serif." +
|
|
69
|
+
" Previews: https://clipwright.io/captions";
|
|
70
|
+
export const MAX_CAPTION_EMPHASIS = 20;
|
|
71
|
+
export const MAX_CAPTION_EMPHASIS_CHARS = 40;
|
|
72
|
+
/** `full` captions every spoken word; `highlights` only the windows around `caption_emphasis` quotes. */
|
|
73
|
+
export const CAPTION_MODES = ["full", "highlights"];
|
|
74
|
+
/** Where captions sit, as a percentage of the height: above the middle the top edge, below it the bottom edge (#884). */
|
|
75
|
+
export const CAPTION_POSITION_VERTICAL = { top: 20, middle: 50, lower: 70, bottom: 80 };
|
|
76
|
+
export const CAPTION_POSITIONS = Object.keys(CAPTION_POSITION_VERTICAL);
|
|
77
|
+
/** `punch`: a small lead-in line over the quote as a large glowing keyword; `pill`: the quote alone in a rounded pill; */
|
|
78
|
+
/** `color`: the quote in the style's accent colour, same size. */
|
|
79
|
+
export const CAPTION_EMPHASIS_LOOKS = ["punch", "pill", "color"];
|
|
80
|
+
const emphasisQuote = z.string().min(1).max(MAX_CAPTION_EMPHASIS_CHARS).refine((text) => text.trim().length > 0);
|
|
81
|
+
export const captionEmphasisItem = z.union([emphasisQuote,
|
|
82
|
+
z.object({ quote: emphasisQuote, look: z.enum(CAPTION_EMPHASIS_LOOKS).optional(),
|
|
83
|
+
punch_in: z.boolean().optional().describe("Long form only: zoom into the presenter while the quote is spoken. " +
|
|
84
|
+
"Drawn only where captions are drawn and the presenter is on screen; otherwise warnings[] names it.") }).strict()]);
|
|
85
|
+
/** An emphasis item as quote, look and punch-in; a plain string, or an object without a look, takes the preset's look. */
|
|
86
|
+
export function captionEmphasisOf(item) {
|
|
87
|
+
return typeof item === "string" ? { quote: item, look: undefined }
|
|
88
|
+
: { quote: item.quote, look: item.look, ...(item.punch_in === true ? { punchIn: true } : {}) };
|
|
89
|
+
}
|
|
90
|
+
/** Composition semantics of a run, written at admission: 2 draws captions and crops pictures */
|
|
91
|
+
/** to fill the frame; a run stored without it keeps the earlier render, so it re-renders the same. */
|
|
92
|
+
export const UGC_COMPOSITION_VERSION = 2;
|
|
93
|
+
export function compositionVersionOf(storedInput) {
|
|
94
|
+
return storedInput !== null && typeof storedInput === "object" &&
|
|
95
|
+
storedInput.composition_version === UGC_COMPOSITION_VERSION ? 2 : 1;
|
|
96
|
+
}
|
|
8
97
|
export const LOOKS = ["natural", "commercial", "raw_iphone"];
|
|
9
98
|
/** Public formats = formats MEASURED live, not the vendor docs: declaring more would */
|
|
10
99
|
/** accept values the pipeline later rejects. */
|
|
@@ -54,6 +143,8 @@ export const MAKE_FACELESS_AGENT_PROTOCOL = "Call quote_faceless first and show
|
|
|
54
143
|
"starts the run and returns a run_id IMMEDIATELY. Then poll get_run with that run_id until the " +
|
|
55
144
|
"state is 'succeeded' (video_url) or 'failed'. Pass attempt=2,3,… to deliberately start a NEW " +
|
|
56
145
|
"run for the same input.";
|
|
146
|
+
const IMAGE_MOTION_DESCRIPTION = "Optional picture zoom. A picture always fills the frame, cut to its shape when the shapes differ " +
|
|
147
|
+
"(warnings[] names the share cut). amount is the zoom beyond filling: in grows from 1 to 1+amount, out shrinks from 1+amount to 1. No motion by default.";
|
|
57
148
|
/** One clip segment. `segments` makes `script` optional, changing `required[]` in `tools/list`, */
|
|
58
149
|
/** so the shape was declared once up front instead of changing the public form later. */
|
|
59
150
|
export const ugcSegment = z.object({
|
|
@@ -69,7 +160,7 @@ export const ugcSegment = z.object({
|
|
|
69
160
|
image_motion: z.object({
|
|
70
161
|
direction: z.enum(["in", "out"]),
|
|
71
162
|
amount: z.number().min(0).lt(1),
|
|
72
|
-
}).optional().describe(
|
|
163
|
+
}).optional().describe(IMAGE_MOTION_DESCRIPTION),
|
|
73
164
|
}).superRefine((segment, context) => {
|
|
74
165
|
if (segment.kind === "actor") {
|
|
75
166
|
for (const field of ["media_type", "duration_seconds", "image_motion"]) {
|
|
@@ -88,24 +179,150 @@ export const ugcSegment = z.object({
|
|
|
88
179
|
}
|
|
89
180
|
}
|
|
90
181
|
});
|
|
182
|
+
const insertId = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
|
|
183
|
+
const wordAnchor = z.union([
|
|
184
|
+
z.object({ startWord: z.number().int().nonnegative(), endWord: z.number().int().positive() })
|
|
185
|
+
.strict().refine(range => range.endWord > range.startWord, { message: "endWord must be greater than startWord" }),
|
|
186
|
+
z.object({ quote: z.string().min(1).max(MAX_SCRIPT_CHARS).refine(text => text.trim().length > 0),
|
|
187
|
+
occurrence: z.number().int().nonnegative().optional() }).strict(),
|
|
188
|
+
]);
|
|
189
|
+
const mediaSource = {
|
|
190
|
+
media_url: z.string().url().max(MAX_URL_LENGTH).startsWith("https://").optional()
|
|
191
|
+
.describe("Public https URL of the media. Pass exactly one of media_url or asset_id."),
|
|
192
|
+
media_type: z.enum(["image", "video"])
|
|
193
|
+
.describe("image (PNG or JPEG) or video (MP4, H.264 or HEVC, 8-bit SDR). A video plays muted from its first frame and is cut where its words end; " +
|
|
194
|
+
"one shorter than its words holds its last frame, and warnings[] names it."),
|
|
195
|
+
};
|
|
196
|
+
const hasOneSource = (item) => (item.media_url === undefined) !== (item.asset_id === undefined);
|
|
197
|
+
const ONE_SOURCE = { path: ["media_url"], message: "pass exactly one of media_url or asset_id" };
|
|
91
198
|
export const ugcInsert = z.object({
|
|
92
|
-
id:
|
|
93
|
-
media_url:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
occurrence: z.number().int().nonnegative().optional() }).strict(),
|
|
100
|
-
]),
|
|
199
|
+
id: insertId,
|
|
200
|
+
media_url: mediaSource.media_url,
|
|
201
|
+
asset_id: assetId.optional()
|
|
202
|
+
.describe("A ready PNG or JPEG image or MP4 video from your own assets (list_assets), instead of media_url. Pass exactly one of the two; " +
|
|
203
|
+
"media_type must match the asset's kind."),
|
|
204
|
+
media_type: mediaSource.media_type,
|
|
205
|
+
anchor: wordAnchor,
|
|
101
206
|
cover_words: z.number().int().positive().max(10000).optional()
|
|
102
207
|
.describe("How many spoken words this insert covers, counted from the first word of its anchor. " +
|
|
103
208
|
"Omitted means the anchored words only. The insert ends where the first uncovered word begins, " +
|
|
104
209
|
"so inserts whose coverage meets are adjacent frame for frame and leave no actor shot between them."),
|
|
105
|
-
image_motion: z.object({ direction: z.enum(["in", "out"]), amount: z.number().min(0).lt(1) }).strict().optional()
|
|
210
|
+
image_motion: z.object({ direction: z.enum(["in", "out"]), amount: z.number().min(0).lt(1) }).strict().optional()
|
|
211
|
+
.describe(IMAGE_MOTION_DESCRIPTION),
|
|
212
|
+
transition: z.enum(INSERT_TRANSITIONS).optional()
|
|
213
|
+
.describe("How the insert comes in and goes out: cut (default), wipe (slides across the presenter), flash (a white flash on the cut) " +
|
|
214
|
+
"or zoom (eases in from a closer framing). It never moves the insert's start or end."),
|
|
106
215
|
}).strict().refine(insert => insert.image_motion === undefined || insert.media_type === "image", {
|
|
107
216
|
path: ["image_motion"], message: "image_motion requires media_type=image",
|
|
217
|
+
}).refine(hasOneSource, ONE_SOURCE);
|
|
218
|
+
const layerMedia = {
|
|
219
|
+
id: insertId.describe("Unique across inserts, overlays and layout_windows."),
|
|
220
|
+
...mediaSource,
|
|
221
|
+
asset_id: assetId.optional()
|
|
222
|
+
.describe("A ready PNG or JPEG image or MP4 video from your own assets (list_assets), instead of media_url. Pass exactly one of the two; " +
|
|
223
|
+
"media_type must match the asset's kind."),
|
|
224
|
+
anchor: wordAnchor.describe("The words it starts on, as an insert anchor: { startWord, endWord } or { quote, occurrence? }."),
|
|
225
|
+
cover_words: z.number().int().positive().max(10000).optional()
|
|
226
|
+
.describe("How many spoken words it stays on screen, counted from the first word of its anchor. Omitted means the anchored words only."),
|
|
227
|
+
enter: z.enum(LAYER_ENTERS).optional()
|
|
228
|
+
.describe("How it appears: cut (default), fade, slide (in from the nearest edge) or tilt_in. It leaves with a cut."),
|
|
229
|
+
};
|
|
230
|
+
/** Media drawn over the visible presenter at a fixed place; the presenter stays on screen and is billed. */
|
|
231
|
+
export const ugcOverlay = z.object({
|
|
232
|
+
...layerMedia,
|
|
233
|
+
position: z.enum(OVERLAY_POSITIONS).describe("A corner (top_left, top_right, bottom_left, bottom_right), or a band across the top or bottom."),
|
|
234
|
+
}).strict().refine(hasOneSource, ONE_SOURCE);
|
|
235
|
+
/** A reframing of the presenter: split shares a 9:16 frame with the media, card shows the media in a centred card. */
|
|
236
|
+
export const ugcLayoutWindow = z.object({
|
|
237
|
+
...layerMedia,
|
|
238
|
+
layout: z.enum(LAYOUT_WINDOW_KINDS).describe("split: presenter in the top half, media in the bottom half, captions on the seam (9:16 only). " +
|
|
239
|
+
"card: the media in a centred card over the presenter."),
|
|
240
|
+
blur_presenter: z.boolean().optional().describe("Card only: blur the presenter behind the card."),
|
|
241
|
+
}).strict().refine(hasOneSource, ONE_SOURCE).refine(layout => layout.blur_presenter === undefined || layout.layout === "card", {
|
|
242
|
+
path: ["blur_presenter"], message: "blur_presenter works on a card window only",
|
|
108
243
|
});
|
|
244
|
+
/** A presenter size on its words (reference-elements S2): a hard cut in, settled over a few frames, back to the take at its end. */
|
|
245
|
+
export const ugcFraming = z.object({
|
|
246
|
+
id: insertId.describe("Unique across inserts, overlays, layout_windows, templates and framing."),
|
|
247
|
+
anchor: layerMedia.anchor,
|
|
248
|
+
cover_words: z.number().int().positive().max(10000).optional()
|
|
249
|
+
.describe("How many spoken words the size holds, counted from the first word of its anchor. Omitted means the anchored words only."),
|
|
250
|
+
size: z.enum(FRAMING_SIZES).default("close").describe("close: the presenter 1.18 times larger about the face; medium: the take as rendered."),
|
|
251
|
+
transition: z.enum(FRAMING_TRANSITIONS).default("cut").describe("cut: a hard cut to the size; flash: a warm light leak over the cut."),
|
|
252
|
+
}).strict();
|
|
253
|
+
const templateEnvelope = {
|
|
254
|
+
id: insertId.describe("Unique across inserts, overlays, layout_windows and templates."),
|
|
255
|
+
anchor: layerMedia.anchor,
|
|
256
|
+
cover_words: layerMedia.cover_words,
|
|
257
|
+
placement: z.enum(TEMPLATE_PLACEMENTS).optional().describe("over (drawn over the presenter) or full (fills the frame); omitted means the " +
|
|
258
|
+
"kind's first placement, and a kind takes only the placements its catalog line names."),
|
|
259
|
+
transition: z.enum(INSERT_TRANSITIONS).optional().describe("Full placement only, as an insert's transition."),
|
|
260
|
+
};
|
|
261
|
+
const templateItem = (kind) => z.object({ ...templateEnvelope, kind: z.literal(kind.kind), data: kind.data }).strict();
|
|
262
|
+
/** A graphic template (#778 S3): REST and the SDK take the strict union, `data` checked by `kind`. */
|
|
263
|
+
export const ugcTemplate = z.discriminatedUnion("kind", [templateItem({ ...tag, kind: "tag" }),
|
|
264
|
+
templateItem({ ...lowerThird, kind: "lower_third" }), templateItem({ ...endCard, kind: "end_card" }),
|
|
265
|
+
templateItem({ ...titleCard, kind: "title_card" }), templateItem({ ...quote, kind: "quote" }), templateItem({ ...stat, kind: "stat" }),
|
|
266
|
+
templateItem({ ...callout, kind: "callout" }), templateItem({ ...checklist, kind: "checklist" }),
|
|
267
|
+
templateItem({ ...phoneCall, kind: "phone_call" }), templateItem({ ...compare, kind: "compare" }),
|
|
268
|
+
templateItem({ ...notification, kind: "notification" }), templateItem({ ...progress, kind: "progress" }),
|
|
269
|
+
templateItem({ ...rating, kind: "rating" })]);
|
|
270
|
+
/** The slim MCP item: `data` is any object here, the catalog sits in the description and the server checks it strictly (D3). */
|
|
271
|
+
export const mcpUgcTemplate = z.object({ ...templateEnvelope, kind: z.enum(TEMPLATE_KIND_IDS),
|
|
272
|
+
data: z.record(z.string(), z.unknown()).describe("The kind's fields, as its catalog line in templates names them.") }).strict();
|
|
273
|
+
export const TEMPLATES_DESCRIPTION = templateCatalogText();
|
|
274
|
+
const callVoice = z.string().regex(VOICE_NAME_PATTERN, "a call voice is a voice name from list_voices").optional();
|
|
275
|
+
/** The call phone's measured place and rise (#914 D1e): the worker maps `rect_face` onto our presenter's face when it finds one, else uses `rect`. */
|
|
276
|
+
export const callStage = z.strictObject({
|
|
277
|
+
rect: stagingRect.refine(rect => rect.width > 0 && rect.height > 0 && rect.left + rect.width <= 1 + 1e-6 && rect.top + rect.height <= 1 + 1e-6, "the phone's rect must lie inside the frame").describe("Where the phone settles, in shares of the frame."),
|
|
278
|
+
rect_face: stagingFaceRect.optional().describe("The same place relative to the presenter's face: origin the face centre, unit the face height."),
|
|
279
|
+
enter: z.strictObject({
|
|
280
|
+
from: z.enum(CALL_STAGE_SIDES),
|
|
281
|
+
displacement: z.tuple([z.number().min(-1).max(1), z.number().min(-1).max(1)]).describe("Settled minus first position, in shares of width and height."),
|
|
282
|
+
duration_s: z.number().min(0.1).max(CALL_STAGE_MAX_ENTER_SEC),
|
|
283
|
+
anchor: z.strictObject({ id: z.literal("freeze_start#1"), offset_s: z.number().min(-CALL_STAGE_MAX_LEAD_SEC).max(0) })
|
|
284
|
+
.describe("When the rise starts: seconds from the call's freeze, negative before it."),
|
|
285
|
+
}).optional(),
|
|
286
|
+
layer: z.enum(["behind_presenter"]).optional(),
|
|
287
|
+
});
|
|
288
|
+
/** A phone call after a quote (#857 S5): the presenter listens, then freezes, while the agent and the caller speak; then resumes. */
|
|
289
|
+
export const ugcCall = z.strictObject({
|
|
290
|
+
after: wordAnchor.describe("The words the call follows: the presenter stops after the quote's last word and resumes on the next one."),
|
|
291
|
+
lines: z.array(z.strictObject({ speaker: z.enum(CALL_SPEAKERS),
|
|
292
|
+
text: z.string().min(1).max(MAX_CALL_LINE_CHARS).refine(text => text.trim().length > 0, "a call line needs words") }))
|
|
293
|
+
.min(2, "a call needs at least two lines").max(MAX_CALL_LINES)
|
|
294
|
+
.describe(`The call's turns in order, from both speakers; at most ${MAX_CALL_CHARS} characters and about ${MAX_CALL_SEC} s in all.`),
|
|
295
|
+
voices: z.strictObject({ agent: callVoice, caller: callVoice }).optional()
|
|
296
|
+
.describe(`Voice names from list_voices; omitted means ${DEFAULT_CALL_VOICES.agent} for the agent and ${DEFAULT_CALL_VOICES.caller} for the caller.`),
|
|
297
|
+
phone: z.strictObject({ caller: z.string().min(1).max(24).optional(), banner: z.string().min(1).max(32).optional(),
|
|
298
|
+
banner_note: z.string().min(1).max(48).optional(), mask_number: z.boolean().optional() }).optional()
|
|
299
|
+
.describe(`The in-call phone drawn over the call when no phone_call template covers it; omitted fields are ${CALL_PHONE_DEFAULTS.caller} ` +
|
|
300
|
+
`with its digits after the area code masked, "${CALL_PHONE_DEFAULTS.banner}" and "${CALL_PHONE_DEFAULTS.banner_note}".`),
|
|
301
|
+
stage: callStage.optional().describe("Where the in-call phone settles and how it rises, as a reference measured them (plan_reference_remake writes it); " +
|
|
302
|
+
"omitted is the reference call scene's own place and rise."),
|
|
303
|
+
});
|
|
304
|
+
/** The slim MCP call, which keeps tools/list under its ceiling: the server checks it strictly, as the template items (D3). */
|
|
305
|
+
export const mcpUgcCall = z.record(z.string(), z.unknown())
|
|
306
|
+
.describe(`A call the presenter listens to: { after: { quote }, lines: [{ speaker: agent|caller, text }] (2–${MAX_CALL_LINES}), voices?, phone? }.`);
|
|
307
|
+
/** What admission pins for an `asset_id` insert; the worker reads this key and checks this hash, never the asset row.
|
|
308
|
+
* Server-only: it lives at the top of the stored input, and a public body naming it is refused as an unknown field. */
|
|
309
|
+
export const pinnedInsertAsset = z.object({
|
|
310
|
+
insert_id: insertId,
|
|
311
|
+
/** A template's media slot; ids refuse `.`, so `<id>.<slot>` keys never meet an overlay's (A1). */
|
|
312
|
+
slot: z.string().regex(/^[a-z][a-z_]{0,31}$/).optional(),
|
|
313
|
+
asset_id: assetId,
|
|
314
|
+
key: z.string().min(1).max(1024),
|
|
315
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
316
|
+
bytes: z.number().int().positive(),
|
|
317
|
+
kind: z.enum(ASSET_KINDS),
|
|
318
|
+
media_type: z.enum(ASSET_MEDIA_TYPES),
|
|
319
|
+
width: z.number().int().positive(),
|
|
320
|
+
height: z.number().int().positive(),
|
|
321
|
+
duration_frames: z.number().int().positive().optional(),
|
|
322
|
+
fps: z.number().int().positive().optional(),
|
|
323
|
+
codec: z.string().min(1).optional(),
|
|
324
|
+
pix_fmt: z.string().min(1).optional(),
|
|
325
|
+
}).strict();
|
|
109
326
|
/** Actor segment cap: a limit on ONE run, not on daily spend. */
|
|
110
327
|
export const MAX_ACTOR_SEGMENTS = 3;
|
|
111
328
|
export const MAX_SEGMENTS = 5;
|
|
@@ -130,13 +347,35 @@ export const makeUgcInputShape = {
|
|
|
130
347
|
inserts: z.array(ugcInsert).min(1).max(MAX_INSERTS)
|
|
131
348
|
.refine(inserts => new Set(inserts.map(insert => insert.id)).size === inserts.length, {
|
|
132
349
|
message: "insert IDs must be unique",
|
|
133
|
-
}).optional().describe("Text-anchored image inserts over full narration, each covering cover_words spoken words from its anchor; " +
|
|
134
|
-
"requires long-form qualification on the server,
|
|
350
|
+
}).optional().describe("Text-anchored image or video inserts over full narration, each covering cover_words spoken words from its anchor; " +
|
|
351
|
+
"requires long-form qualification on the server, 1080p and explicit broll_policy=anyone. Captions draw over inserts."),
|
|
352
|
+
overlays: z.array(ugcOverlay).min(1).max(MAX_OVERLAYS).optional()
|
|
353
|
+
.describe("Text-anchored images or videos over the visible presenter, in a corner or a band at the top or bottom. At most 2 on screen at once, " +
|
|
354
|
+
"and two on screen together need positions that do not share a corner. Captions move to the middle or bottom third when an overlay " +
|
|
355
|
+
"meets them, never up over the face, and hide while neither is free. An overlay is not drawn while a full-frame insert is on screen. Same requirements as inserts: script, " +
|
|
356
|
+
"long-form qualification on the server, 1080p and explicit broll_policy=anyone. The presenter stays on screen, so the price does not change."),
|
|
357
|
+
layout_windows: z.array(ugcLayoutWindow).min(1).max(MAX_LAYOUT_WINDOWS).optional()
|
|
358
|
+
.describe("Text-anchored reframings of the presenter: split (9:16 only) or card. Windows may not overlap one another or an insert. " +
|
|
359
|
+
"Same requirements as inserts. A covered or blurred presenter is still delivered vendor work and is billed as face time."),
|
|
360
|
+
framing: z.array(ugcFraming).min(1).max(MAX_FRAMING).optional()
|
|
361
|
+
.describe("Text-anchored presenter sizes: each cuts to close or medium on its words and back to the take where they end. Items may not overlap " +
|
|
362
|
+
"one another, an insert or a layout window. A framing drawn wholly under full-frame templates is named in warnings[]. Where a punch_in " +
|
|
363
|
+
"emphasis meets a framing, the framing is drawn. Requires script and long-form qualification on the server; the price does not change."),
|
|
364
|
+
templates: z.array(ugcTemplate).min(1).max(MAX_TEMPLATES).optional().describe(TEMPLATES_DESCRIPTION),
|
|
365
|
+
template_theme: templateTheme.optional().describe("The look of every template in the run: a preset and an optional accent; only with templates."),
|
|
366
|
+
template_sounds: templateSounds.optional(),
|
|
367
|
+
edit_sounds: editSounds.optional(),
|
|
368
|
+
call: ugcCall.optional().describe("A phone call after a quote: the presenter listens, then freezes while the call plays as a phone line, " +
|
|
369
|
+
"with a ring first, and an in-call phone beside them shows it (a phone_call template covering the call is used instead). Needs script, " +
|
|
370
|
+
"a 9:16 frame and words after it. The frozen presenter and the call add production seconds, not presenter seconds, and a live-avatar " +
|
|
371
|
+
"still shown on the call has its own price in list_prices. Long-form qualification on the server."),
|
|
135
372
|
person: z.string().min(1).max(500).optional(),
|
|
136
373
|
actor_id: actorId
|
|
137
374
|
.optional()
|
|
138
375
|
.describe("Saved Clipwright actor ID from list_actors. Choose actor_id, image, or person; do not combine them. " +
|
|
139
376
|
"Without voice or voice_id the voice follows the actor's gender. Do not combine with actor_gender."),
|
|
377
|
+
presenter_framing: z.enum(PRESENTER_FRAMINGS).optional().describe("close (default) or wide: presenter lower, room above. Needs the actor's wide view (list_actors), 9:16 only; else close plus warnings[]."),
|
|
378
|
+
presenter_mic: z.boolean().optional().describe("true: presenter holds a tiny mic. Needs the actor's mic view in that framing (list_actors; library ones are wide); else no mic plus warnings[]."),
|
|
140
379
|
// https URLs only: the vendor takes URLs, and storage URLs are never passed to vendors,
|
|
141
380
|
// so a `data:` URI is refused on input rather than accepted and dropped.
|
|
142
381
|
image: z
|
|
@@ -162,8 +401,19 @@ export const makeUgcInputShape = {
|
|
|
162
401
|
name: z.string().max(100).optional(),
|
|
163
402
|
broll_url: z.string().url().max(MAX_URL_LENGTH).optional(),
|
|
164
403
|
broll_policy: brollPolicy.optional(),
|
|
165
|
-
captions: z.boolean().default(false),
|
|
166
|
-
caption_style: z.enum(CAPTION_STYLES).default("hormozi"),
|
|
404
|
+
captions: z.boolean().default(false).describe("Burn word-timed captions into the video, over the presenter and over inserts. Not available with segments."),
|
|
405
|
+
caption_style: z.enum(CAPTION_STYLES).default("hormozi").describe(CAPTION_STYLE_DESCRIPTION),
|
|
406
|
+
caption_emphasis: z.array(captionEmphasisItem)
|
|
407
|
+
.min(1).max(MAX_CAPTION_EMPHASIS).optional().describe("Key words or short phrases to emphasise, each quoted from script and matched as an insert quote is (case and punctuation " +
|
|
408
|
+
"ignored). An item is the quote, or { quote, look }: look punch shows a small lead-in line from the same sentence over the " +
|
|
409
|
+
"quote as a large glowing keyword, look pill shows the quote alone in a rounded pill, look color shows it in the accent " +
|
|
410
|
+
"colour at the same size. Without a look, punch draws punch, hormozi and accent draw color, and other presets draw the quote larger " +
|
|
411
|
+
"in their highlight colour. Every occurrence is emphasised. Requires " +
|
|
412
|
+
"captions=true and script; a quote absent from script is refused before any charge."),
|
|
413
|
+
caption_mode: z.enum(CAPTION_MODES).optional().describe("full (default when omitted): every spoken word is captioned. highlights: captions appear only around caption_emphasis " +
|
|
414
|
+
"quotes (lead-in, quote, short tail) and nowhere else; requires caption_emphasis. In both modes captions are hidden while a " +
|
|
415
|
+
"full-frame insert is on screen or a template meets their band."),
|
|
416
|
+
caption_position: z.enum(CAPTION_POSITIONS).optional().describe("Where captions sit: top, middle, lower or bottom (80% of the height). Omitted means the caption_style's own place. Requires captions=true."),
|
|
167
417
|
look: z.enum(LOOKS).default("natural"),
|
|
168
418
|
// No .default(), so the resolver tells silence (snap with a warning) from an explicit
|
|
169
419
|
// choice (refused on conflict). DEFAULT_ASPECT_RATIO (aspect.ts) fills it in.
|
|
@@ -205,18 +455,25 @@ export const makeUgcInputShape = {
|
|
|
205
455
|
.enum(TTS_MODELS)
|
|
206
456
|
.optional()
|
|
207
457
|
.describe(`Speech model: ${TTS_MODELS.join(" | ")}. Omitted means the model of the chosen voice ` +
|
|
208
|
-
`(list_voices shows it): ${
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`eleven_v4, eleven_v3 and ${GOOGLE_TTS_MODEL} read stress marks ` +
|
|
458
|
+
`(list_voices shows it): ${MODEL_VOICE_MODEL} for its own voices, ${DEFAULT_TTS_MODEL} for every preset, ` +
|
|
459
|
+
`catalog voice and raw voice_id. An explicit tts_model wins, except eleven_v3: it is no longer spoken, ` +
|
|
460
|
+
`and a run naming it speaks ${DEFAULT_TTS_MODEL} with a warning. ` +
|
|
461
|
+
`${DEFAULT_TTS_MODEL} and ${GOOGLE_TTS_MODEL} read stress marks ` +
|
|
213
462
|
"(a capital vowel inside a Russian word, \"потОм\", becomes one; see script); " +
|
|
214
463
|
"eleven_flash_v2_5 and eleven_turbo_v2_5 are cheaper alternatives for languages other than " +
|
|
215
464
|
`Russian. ${GOOGLE_TTS_MODEL} speaks only its own voices (list_voices with model=${GOOGLE_TTS_MODEL}; without ` +
|
|
216
465
|
`voice, ${MODEL_DEFAULT_VOICE_BY_GENDER.female} for a female actor and ${MODEL_DEFAULT_VOICE_BY_GENDER.male} otherwise) ` +
|
|
217
466
|
"and returns no word timings: a preset or catalog voice, voice_id, captions=true, " +
|
|
218
|
-
"segments and inserts are refused with it before any charge. Audio tags in square brackets are
|
|
219
|
-
"
|
|
467
|
+
"segments and inserts are refused with it before any charge. Audio tags in square brackets are a " +
|
|
468
|
+
"convention of the other speech models and are not measured on it. " + SCRIPT_LENGTH_DESCRIPTION),
|
|
469
|
+
voice_pace: z
|
|
470
|
+
.number()
|
|
471
|
+
.min(VOICE_PACE_MIN)
|
|
472
|
+
.max(VOICE_PACE_MAX)
|
|
473
|
+
.optional()
|
|
474
|
+
.describe(`Narration speed; 1 is the voice's own. ${GOOGLE_TTS_MODEL} gets a style hint instead; ${DEFAULT_TTS_MODEL} has no speed, ` +
|
|
475
|
+
"so pick a voice by list_voices words_per_second; both named in warnings[]."),
|
|
476
|
+
pause_trim: pauseTrim.optional().describe("Shorten narration pauses longer than these seconds, cutting silence only."),
|
|
220
477
|
webhook_url: z.string().url().max(MAX_URL_LENGTH).optional(),
|
|
221
478
|
/** Removed visible label (clipwright#488). The key stays in the shape so that the refusal */
|
|
222
479
|
/** names its reason instead of answering `unknown_field`. */
|
|
@@ -242,6 +499,63 @@ export const makeUgcInput = z
|
|
|
242
499
|
})
|
|
243
500
|
.refine(v => v.inserts === undefined || (v.script !== undefined && v.segments === undefined), {
|
|
244
501
|
path: ["inserts"], message: "inserts requires script and cannot be combined with segments",
|
|
502
|
+
})
|
|
503
|
+
.superRefine((v, context) => {
|
|
504
|
+
if (v.overlays === undefined && v.layout_windows === undefined)
|
|
505
|
+
return;
|
|
506
|
+
if (v.script === undefined || v.segments !== undefined) {
|
|
507
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: [v.overlays === undefined ? "layout_windows" : "overlays"],
|
|
508
|
+
message: "overlays and layout_windows require script and cannot be combined with segments" });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (const refusal of layerRefusals({ ...v, script: v.script }))
|
|
512
|
+
context.addIssue({ code: z.ZodIssueCode.custom, ...refusal });
|
|
513
|
+
})
|
|
514
|
+
.superRefine((v, context) => {
|
|
515
|
+
if (v.framing === undefined)
|
|
516
|
+
return;
|
|
517
|
+
if (v.script === undefined || v.segments !== undefined) {
|
|
518
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["framing"], message: "framing requires script and cannot be combined with segments" });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
for (const refusal of framingRefusals({ ...v, script: v.script }))
|
|
522
|
+
context.addIssue({ code: z.ZodIssueCode.custom, ...refusal });
|
|
523
|
+
})
|
|
524
|
+
// Template refusals sit in the schema, so REST, quote_ugc and MCP refuse alike before any hold (D4, A9).
|
|
525
|
+
.superRefine((v, context) => {
|
|
526
|
+
for (const refusal of [...templateRefusals(v), ...editSoundRefusals(v)])
|
|
527
|
+
context.addIssue({ code: z.ZodIssueCode.custom, ...refusal });
|
|
528
|
+
})
|
|
529
|
+
.superRefine((v, context) => {
|
|
530
|
+
for (const refusal of callRefusals(v))
|
|
531
|
+
context.addIssue({ code: z.ZodIssueCode.custom, ...refusal });
|
|
532
|
+
})
|
|
533
|
+
.refine(v => !v.captions || v.segments === undefined, {
|
|
534
|
+
path: ["captions"], message: "captions are not available with segments: pass script, with inserts for cutaways, or captions=false",
|
|
535
|
+
})
|
|
536
|
+
.superRefine((v, context) => {
|
|
537
|
+
if (v.caption_mode === "highlights" && (!v.captions || v.caption_emphasis === undefined)) {
|
|
538
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["caption_mode"],
|
|
539
|
+
message: "caption_mode=highlights requires captions=true and caption_emphasis: highlights are drawn only around the quotes you emphasise" });
|
|
540
|
+
}
|
|
541
|
+
if (v.caption_position !== undefined && !v.captions) {
|
|
542
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["caption_position"], message: "caption_position requires captions=true: it places the burned captions" });
|
|
543
|
+
}
|
|
544
|
+
if (v.caption_emphasis === undefined)
|
|
545
|
+
return;
|
|
546
|
+
if (!v.captions || v.script === undefined) {
|
|
547
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["caption_emphasis"],
|
|
548
|
+
message: "caption_emphasis requires captions=true and script: it marks words of the burned captions" });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const tokens = lexicalTokens(v.script);
|
|
552
|
+
for (const [index, item] of v.caption_emphasis.entries()) {
|
|
553
|
+
const { quote } = captionEmphasisOf(item);
|
|
554
|
+
if (quoteOccurrences(tokens, quote).length === 0) {
|
|
555
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["caption_emphasis", index],
|
|
556
|
+
message: `caption_emphasis "${quote}" does not occur in script: quote words exactly as the script spells them` });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
245
559
|
})
|
|
246
560
|
.superRefine((v, context) => {
|
|
247
561
|
for (const refusal of speechModelRefusals(v)) {
|
|
@@ -330,6 +644,14 @@ export function speechModelRefusals(input) {
|
|
|
330
644
|
`segments are not available with ${model}: pass script for a single take, ${otherModel}`],
|
|
331
645
|
["inserts", input.inserts !== undefined,
|
|
332
646
|
`inserts are not available with ${model}: it returns no word timings to anchor them; omit inserts, ${otherModel}`],
|
|
647
|
+
["overlays", input.overlays !== undefined,
|
|
648
|
+
`overlays are not available with ${model}: it returns no word timings to anchor them; omit overlays, ${otherModel}`],
|
|
649
|
+
["layout_windows", input.layout_windows !== undefined,
|
|
650
|
+
`layout_windows are not available with ${model}: it returns no word timings to anchor them; omit layout_windows, ${otherModel}`],
|
|
651
|
+
["framing", input.framing !== undefined,
|
|
652
|
+
`framing is not available with ${model}: it returns no word timings to anchor it; omit framing, ${otherModel}`],
|
|
653
|
+
["call", input.call !== undefined,
|
|
654
|
+
`call is not available with ${model}: it returns no word timings to open the call after its quote; omit call, ${otherModel}`],
|
|
333
655
|
];
|
|
334
656
|
return candidates.filter(([, refused]) => refused).map(([field, , message]) => ({ field, message }));
|
|
335
657
|
}
|
|
@@ -340,6 +662,15 @@ export function serverUgcInputWith(voiceRefusal) {
|
|
|
340
662
|
.refine((v) => v.voice === undefined || voiceRefusal(v.voice) === undefined, {
|
|
341
663
|
path: ["voice"],
|
|
342
664
|
error: (issue) => voiceRefusal(issue.input.voice),
|
|
665
|
+
})
|
|
666
|
+
// A call speaks ElevenLabs catalog voices only: a model's own voice has no dialogue.
|
|
667
|
+
.superRefine((v, context) => {
|
|
668
|
+
for (const speaker of CALL_SPEAKERS) {
|
|
669
|
+
const name = v.call?.voices?.[speaker];
|
|
670
|
+
const why = name === undefined ? undefined : isModelVoiceName(name) ? `"${name}" is a voice of ${MODEL_VOICE_MODEL}, which speaks no call` : voiceRefusal(name);
|
|
671
|
+
if (why)
|
|
672
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["call", "voices", speaker], message: why });
|
|
673
|
+
}
|
|
343
674
|
})
|
|
344
675
|
// The cap the client skipped: the server knows the model of an admitted catalog voice.
|
|
345
676
|
.refine((v) => !modelUnknownToClient(v) ||
|
|
@@ -350,12 +681,26 @@ export function serverUgcInputWith(voiceRefusal) {
|
|
|
350
681
|
});
|
|
351
682
|
}
|
|
352
683
|
/** A stored run input already passed admission; a model or cap change must not make an */
|
|
353
|
-
/** old run unreadable. The snapshot keeps the actor's gender. */
|
|
684
|
+
/** old run unreadable. The snapshot keeps the actor's gender: the parse feeds the timeline digest, so it stays this narrow. */
|
|
354
685
|
export const storedUgcInput = z.object({
|
|
355
686
|
...makeUgcInputShape,
|
|
356
687
|
tts_model: z.string().optional(),
|
|
357
688
|
actor_snapshot: z.object({ gender: z.enum(["female", "male"]).optional() }).optional(),
|
|
689
|
+
insert_assets: z.array(pinnedInsertAsset).min(1).max(MAX_INSERTS + MAX_OVERLAYS + MAX_LAYOUT_WINDOWS + MAX_TEMPLATE_MEDIA).optional(),
|
|
690
|
+
composition_version: z.literal(UGC_COMPOSITION_VERSION).optional(),
|
|
691
|
+
/** The model admission resolved; absent on runs admitted before 2026-10-25. */
|
|
692
|
+
resolved_tts_model: z.string().optional(),
|
|
358
693
|
});
|
|
694
|
+
/** Whether a run takes the narrated long-form path: one script with inserts, overlays, layout windows, framing or a call. */
|
|
695
|
+
export function isNarratedUgcInput(input) {
|
|
696
|
+
return input.segments === undefined && (input.inserts !== undefined || input.overlays !== undefined || input.layout_windows !== undefined ||
|
|
697
|
+
input.framing !== undefined || input.call !== undefined);
|
|
698
|
+
}
|
|
699
|
+
/** The pinned asset of an `asset_id` insert, overlay, layout window or template slot in a stored input; a public input has none. */
|
|
700
|
+
export function pinnedInsertAssetOf(input, insertId, slot) {
|
|
701
|
+
const pinned = input.insert_assets;
|
|
702
|
+
return pinned?.find(asset => asset.insert_id === insertId && asset.slot === slot);
|
|
703
|
+
}
|
|
359
704
|
/** What the worker voices apart: the script, or each actor segment's line. */
|
|
360
705
|
function spokenUnitsOf(input) {
|
|
361
706
|
if (input.script !== undefined)
|
|
@@ -379,8 +724,18 @@ export function resolvedScript(input) {
|
|
|
379
724
|
}
|
|
380
725
|
// The INPUT type (`MakeUgcInputArgs`) lives in `contract-dispositions.ts`: it comes from the
|
|
381
726
|
// OFFERED shape, so SDK autocomplete does not show rejected fields.
|
|
727
|
+
/** A plain run's tariff before parts-v1 (#800): 30 credits per second of finished video, known by the absence of `run_billing_terms` (#840 D2). */
|
|
728
|
+
export const UGC_PER_SECOND_TARIFF = "ugc-per-second-v1";
|
|
729
|
+
/** That tariff's rate (docs/06): runs admitted before parts-v1 settle at it; service-core rounds the duration up before multiplying. */
|
|
730
|
+
export const CREDITS_PER_SECOND = 30;
|
|
382
731
|
export const quoteResponse = z.object({
|
|
383
732
|
long_form_pricing: longFormQuoteSchema.optional(),
|
|
733
|
+
/** The tariff the run is admitted at: `ugc-per-second-v1`, or long form's `pricingVersion` (#840 D3). A string, not an enum: a */
|
|
734
|
+
/** tariff the client does not know yet must not break `quote`; optional, so a quote stored before it still reads. */
|
|
735
|
+
tariff: z.string().min(1).max(200).optional(),
|
|
736
|
+
/** The estimate by part (#800): presenter seconds by engine, production seconds, live-avatar stills; the minimum applies after. */
|
|
737
|
+
price_lines: z.array(partsLineSchema).optional(),
|
|
738
|
+
minimum_credits: z.number().int().nonnegative().optional(),
|
|
384
739
|
skill: z.string(),
|
|
385
740
|
credits_estimate: z.number().int().nonnegative(),
|
|
386
741
|
duration_estimate_sec: z.number().positive(),
|