@clipwright/core 0.1.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.
@@ -0,0 +1,270 @@
1
+ import { z } from "zod";
2
+ export declare const CAPTION_STYLES: readonly ["hormozi", "tiktok", "minimal"];
3
+ export declare const LOOKS: readonly ["natural", "commercial", "raw_iphone"];
4
+ /**
5
+ * Публичное множество форматов = ИЗМЕРЕННОЕ множество, а не вендорская дока.
6
+ *
7
+ * Вендор принимает шесть форматов, но живьём у нас проверены два, и ровно эти
8
+ * два объявляет `CAPABILITIES` адаптера. Объявить в схеме больше — значит
9
+ * принять значение, которое пайплайн потом отклонит; это тот самый класс
10
+ * расхождения, ради устранения которого пересматривается контракт. Расширение —
11
+ * отдельное стори (US-535) и только после живого измерения (US-530).
12
+ */
13
+ export declare const ASPECT_RATIOS: readonly ["9:16", "1:1", "16:9"];
14
+ export type AspectRatio = (typeof ASPECT_RATIOS)[number];
15
+ /**
16
+ * Разрешение выхода. Значения — те, что реально принимает HeyGen v3
17
+ * (проверено 18.07.2026: `480p` и ниже отклоняются с
18
+ * `Input should be '4k', '1080p' or '720p'`).
19
+ *
20
+ * Фактические кадры при `aspect_ratio: "9:16"` — замерены на реальных файлах:
21
+ * 720p → 720 × 1280 (~590 КБ на 7 сек)
22
+ * 1080p → 1080 × 1920 (~923 КБ)
23
+ * 4k → 2160 × 3840 (~3.7 МБ)
24
+ *
25
+ * ВАЖНО: на стоимость разрешение НЕ влияет. HeyGen тарифицирует по
26
+ * длительности (~1 кредит/сек): все три варианта стоили одинаково — 8
27
+ * кредитов за один и тот же 7.36-секундный ролик. Понижать разрешение ради
28
+ * экономии кредитов бессмысленно; экономят короткие скрипты и фикстурный
29
+ * режим. 720p в деве берётся ради скорости скачивания и объёма в R2
30
+ * (файл в 6 раз меньше 4k), а не ради денег.
31
+ */
32
+ export declare const RESOLUTIONS: readonly ["720p", "1080p", "4k"];
33
+ export type Resolution = (typeof RESOLUTIONS)[number];
34
+ /**
35
+ * Продуктовый дефолт — 1080p, и это не деталь реализации.
36
+ * Конкурент обещает 1080p, а отдаёт 720p (docs/08); наше обещание держать
37
+ * 1080×1920 — заявленный дифференциатор и правило проекта №4. Понижение
38
+ * дефолта допустимо ТОЛЬКО в dev-окружении, где ролики никто не публикует.
39
+ */
40
+ export declare const DEFAULT_RESOLUTION: Resolution;
41
+ export declare const DEV_RESOLUTION: Resolution;
42
+ /**
43
+ * Разрешение конкретного рана: явный выбор клиента важнее любых дефолтов,
44
+ * иначе dev-окружение молча подменяло бы то, что пользователь запросил.
45
+ */
46
+ export declare function resolveResolution(requested: Resolution | undefined, isDev: boolean): Resolution;
47
+ /**
48
+ * Потолок длины скрипта (docs/12 §11, ~45с озвучки) — временная замена
49
+ * мульти-тейка (docs/12 §3: длинные скрипты раньше резались на несколько
50
+ * тейков, эта логика вырезана на время прототипа). Пока мульти-тейк не
51
+ * вернули, скрипт длиннее одного тейка нужно отклонять на входе, а не
52
+ * молча обрезать или рендерить с нарушением пейсинг-гейта.
53
+ *
54
+ * Число слов считаем от ВЕРХНЕЙ границы пейсинга (WORDS_PER_SECOND_MAX из
55
+ * pacing.ts) — это самый длинный скрипт, который ещё способен уложиться в
56
+ * потолок секунд без нарушения пейсинг-гейта. Числа пейсинга не дублируем,
57
+ * берём готовые константы из pacing.ts.
58
+ */
59
+ export declare const MAX_SCRIPT_SECONDS = 45;
60
+ export declare const MAX_SCRIPT_WORDS: number;
61
+ /**
62
+ * Сырой zod-shape входа make_ugc — БЕЗ `.refine()`.
63
+ *
64
+ * ЗАЧЕМ ОТДЕЛЬНО ОТ `makeUgcInput`: `makeUgcInput` после `.refine()` становится
65
+ * `ZodEffects`, у которого НЕТ `.shape` (ловушка A12). MCP-тул (`tools/list`) и
66
+ * прочие потребители, которым нужен именно объект полей, обязаны собираться из
67
+ * этого shape, а не переписывать поля руками (правило проекта №2 — не дублировать
68
+ * схему). `makeUgcInput` пересобирается из ЭТОГО ЖЕ shape ниже, чтобы они не
69
+ * разошлись.
70
+ */
71
+ /**
72
+ * Один сегмент ролика. Форма объявляется ЗДЕСЬ, в Slice 1, хотя реализация —
73
+ * в Slice 2 (US-526): поле `segments` требует `script → .optional()`, а это
74
+ * меняет `required[]` в `tools/list`. Приедь правка позже, публичная форма
75
+ * сменилась бы ПОСЛЕ проверки вторым пользователем и обесценила бы её —
76
+ * единственную внешне фальсифицируемую проверку продукта. Здесь required-набор
77
+ * меняется один раз, до гейта, а Slice 2 сводится к снятию строки отказа.
78
+ */
79
+ export declare const ugcSegment: z.ZodObject<{
80
+ kind: z.ZodEnum<{
81
+ actor: "actor";
82
+ media: "media";
83
+ }>;
84
+ script: z.ZodOptional<z.ZodString>;
85
+ media_url: z.ZodOptional<z.ZodString>;
86
+ }, z.core.$strip>;
87
+ export type UgcSegment = z.infer<typeof ugcSegment>;
88
+ /** Потолок актёрских сегментов — ограничение ОДНОГО рана, не суточных трат. */
89
+ export declare const MAX_ACTOR_SEGMENTS = 3;
90
+ export declare const MAX_SEGMENTS = 5;
91
+ export declare const makeUgcInputShape: {
92
+ readonly script: z.ZodOptional<z.ZodString>;
93
+ readonly segments: z.ZodOptional<z.ZodArray<z.ZodObject<{
94
+ kind: z.ZodEnum<{
95
+ actor: "actor";
96
+ media: "media";
97
+ }>;
98
+ script: z.ZodOptional<z.ZodString>;
99
+ media_url: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strip>>>;
101
+ readonly person: z.ZodOptional<z.ZodString>;
102
+ readonly image: z.ZodOptional<z.ZodString>;
103
+ readonly character: z.ZodOptional<z.ZodString>;
104
+ readonly name: z.ZodOptional<z.ZodString>;
105
+ readonly broll_url: z.ZodOptional<z.ZodString>;
106
+ readonly captions: z.ZodDefault<z.ZodBoolean>;
107
+ readonly caption_style: z.ZodDefault<z.ZodEnum<{
108
+ hormozi: "hormozi";
109
+ minimal: "minimal";
110
+ tiktok: "tiktok";
111
+ }>>;
112
+ readonly look: z.ZodDefault<z.ZodEnum<{
113
+ commercial: "commercial";
114
+ natural: "natural";
115
+ raw_iphone: "raw_iphone";
116
+ }>>;
117
+ readonly aspect_ratio: z.ZodOptional<z.ZodEnum<{
118
+ "16:9": "16:9";
119
+ "1:1": "1:1";
120
+ "9:16": "9:16";
121
+ }>>;
122
+ readonly resolution: z.ZodOptional<z.ZodEnum<{
123
+ "1080p": "1080p";
124
+ "4k": "4k";
125
+ "720p": "720p";
126
+ }>>;
127
+ readonly voice: z.ZodOptional<z.ZodEnum<{
128
+ daria_ru_female: "daria_ru_female";
129
+ eric: "eric";
130
+ george: "george";
131
+ owner_ru_clone: "owner_ru_clone";
132
+ sarah: "sarah";
133
+ }>>;
134
+ readonly voice_id: z.ZodOptional<z.ZodString>;
135
+ readonly webhook_url: z.ZodOptional<z.ZodString>;
136
+ /**
137
+ * Видимая плашка «сделано ИИ» поверх кадра — OPT-IN, а не обязательная.
138
+ *
139
+ * Обязательный вотермарк отменил бы заявленный дифференциатор («без
140
+ * вотермарки»), поэтому раскрытие держится на контрактных полях и на теге
141
+ * внутри файла, а плашка остаётся выбором вызывателя. Выжигание её в кадр
142
+ * требует композиции: она появилась в US-527, и US-539 перевёл поле в
143
+ * `implemented` (обусловлено флагом `CLIPWRIGHT_COMPOSE`).
144
+ */
145
+ readonly disclosure_overlay: z.ZodOptional<z.ZodBoolean>;
146
+ /**
147
+ * Чем заполнять кадр, когда клип не покрывает его целиком (US-527).
148
+ *
149
+ * БЕЗ `.default()` НАМЕРЕННО. Дефолт здесь означал бы, что мы выбираем за
150
+ * клиента, как выглядит его ролик там, где формат не совпал с источником, —
151
+ * и выбираем МОЛЧА. Отсутствие поля значит «укладки не требуется»; если она
152
+ * всё же понадобилась, ран обязан сказать об этом в `warnings[]`, а не
153
+ * подобрать фон по своему вкусу.
154
+ *
155
+ * `contain` — вписать целиком (поля видны), `white` — белые поля как у
156
+ * вендора, `blur` — размытая заливка тем же кадром.
157
+ */
158
+ readonly background: z.ZodOptional<z.ZodEnum<{
159
+ blur: "blur";
160
+ contain: "contain";
161
+ white: "white";
162
+ }>>;
163
+ };
164
+ /**
165
+ * Ретрай-параметр агентного скилла: НОМЕР попытки. Живёт в core (не рукописно в
166
+ * MCP и CLI сразу), но ОТДЕЛЬНО от `makeUgcInputShape` — это вход СКИЛЛА
167
+ * (порождает суффикс `:N` у ключа идемпотентности в SDK), а не параметр рендера.
168
+ */
169
+ export declare const agentRetryShape: {
170
+ readonly attempt: z.ZodOptional<z.ZodNumber>;
171
+ };
172
+ /**
173
+ * Вход make_ugc. Не более одного из person | image задаёт идентичность (оба
174
+ * опущены → дефолтный актёр). Схема — единый источник правды для REST, MCP
175
+ * (tools/list) и SDK.
176
+ *
177
+ * `character` из взаимоисключения ВЫВЕДЕН, а не забыт: его диспозиция —
178
+ * `rejected` (`contract-dispositions.ts`), то есть поле отбивается на входе
179
+ * раньше, чем дойдёт до этой проверки. Держать его здесь значило бы
180
+ * валидировать выбор между вариантами, один из которых не существует.
181
+ */
182
+ export declare const makeUgcInput: z.ZodObject<{
183
+ script: z.ZodOptional<z.ZodString>;
184
+ segments: z.ZodOptional<z.ZodArray<z.ZodObject<{
185
+ kind: z.ZodEnum<{
186
+ actor: "actor";
187
+ media: "media";
188
+ }>;
189
+ script: z.ZodOptional<z.ZodString>;
190
+ media_url: z.ZodOptional<z.ZodString>;
191
+ }, z.core.$strip>>>;
192
+ person: z.ZodOptional<z.ZodString>;
193
+ image: z.ZodOptional<z.ZodString>;
194
+ character: z.ZodOptional<z.ZodString>;
195
+ name: z.ZodOptional<z.ZodString>;
196
+ broll_url: z.ZodOptional<z.ZodString>;
197
+ captions: z.ZodDefault<z.ZodBoolean>;
198
+ caption_style: z.ZodDefault<z.ZodEnum<{
199
+ hormozi: "hormozi";
200
+ minimal: "minimal";
201
+ tiktok: "tiktok";
202
+ }>>;
203
+ look: z.ZodDefault<z.ZodEnum<{
204
+ commercial: "commercial";
205
+ natural: "natural";
206
+ raw_iphone: "raw_iphone";
207
+ }>>;
208
+ aspect_ratio: z.ZodOptional<z.ZodEnum<{
209
+ "16:9": "16:9";
210
+ "1:1": "1:1";
211
+ "9:16": "9:16";
212
+ }>>;
213
+ resolution: z.ZodOptional<z.ZodEnum<{
214
+ "1080p": "1080p";
215
+ "4k": "4k";
216
+ "720p": "720p";
217
+ }>>;
218
+ voice: z.ZodOptional<z.ZodEnum<{
219
+ daria_ru_female: "daria_ru_female";
220
+ eric: "eric";
221
+ george: "george";
222
+ owner_ru_clone: "owner_ru_clone";
223
+ sarah: "sarah";
224
+ }>>;
225
+ voice_id: z.ZodOptional<z.ZodString>;
226
+ webhook_url: z.ZodOptional<z.ZodString>;
227
+ disclosure_overlay: z.ZodOptional<z.ZodBoolean>;
228
+ background: z.ZodOptional<z.ZodEnum<{
229
+ blur: "blur";
230
+ contain: "contain";
231
+ white: "white";
232
+ }>>;
233
+ }, z.core.$strip>;
234
+ /** Минимум, из которого выводится произносимый текст: сам скрипт или сегменты. */
235
+ export interface SpokenTextInput {
236
+ script?: string | undefined;
237
+ segments?: readonly UgcSegment[] | undefined;
238
+ }
239
+ /**
240
+ * ЕДИНСТВЕННОЕ место, где «`script` либо сам, либо склейка сегментов»
241
+ * превращается в `string`.
242
+ *
243
+ * ЗАЧЕМ ХЕЛПЕР, А НЕ `!` НА СЕМИ CALL-SITE'АХ. Снятие обязательности со
244
+ * `script` делает его `string | undefined` у каждого потребителя, и `.refine`
245
+ * спасает в рантайме, а компилятор — нет. Самый чувствительный из них —
246
+ * `elevenlabs-tts.ts`, где комментарий прямо опирался на `z.string().min(1)`
247
+ * как на ГАРД ПЕРЕД ПЛАТНЫМ POST: заткни мы там типы, гард остался бы снятым,
248
+ * а компилятор бы об этом молчал. Поэтому проверка непустоты живёт ВНУТРИ
249
+ * хелпера и восстанавливает гард явно, а не полагается на снятую схему.
250
+ */
251
+ export declare function resolvedScript(input: SpokenTextInput): string;
252
+ export type MakeUgcInput = z.infer<typeof makeUgcInput>;
253
+ export declare const quoteResponse: z.ZodObject<{
254
+ skill: z.ZodString;
255
+ credits_estimate: z.ZodNumber;
256
+ duration_estimate_sec: z.ZodNumber;
257
+ warnings: z.ZodDefault<z.ZodArray<z.ZodString>>;
258
+ contract_version: z.ZodString;
259
+ source: z.ZodNullable<z.ZodObject<{
260
+ width: z.ZodNumber;
261
+ height: z.ZodNumber;
262
+ aspect_ratio: z.ZodString;
263
+ }, z.core.$strip>>;
264
+ resolved_aspect_ratio: z.ZodEnum<{
265
+ "16:9": "16:9";
266
+ "1:1": "1:1";
267
+ "9:16": "9:16";
268
+ }>;
269
+ }, z.core.$strip>;
270
+ export type QuoteResponse = z.infer<typeof quoteResponse>;
package/dist/skills.js ADDED
@@ -0,0 +1,291 @@
1
+ import { z } from "zod";
2
+ import { WORDS_PER_SECOND_MAX, countWords } from "./pacing.js";
3
+ import { VOICE_ID_PATTERN, VOICE_PRESET_NAMES } from "./voices.js";
4
+ export const CAPTION_STYLES = ["hormozi", "tiktok", "minimal"];
5
+ export const LOOKS = ["natural", "commercial", "raw_iphone"];
6
+ /**
7
+ * Публичное множество форматов = ИЗМЕРЕННОЕ множество, а не вендорская дока.
8
+ *
9
+ * Вендор принимает шесть форматов, но живьём у нас проверены два, и ровно эти
10
+ * два объявляет `CAPABILITIES` адаптера. Объявить в схеме больше — значит
11
+ * принять значение, которое пайплайн потом отклонит; это тот самый класс
12
+ * расхождения, ради устранения которого пересматривается контракт. Расширение —
13
+ * отдельное стори (US-535) и только после живого измерения (US-530).
14
+ */
15
+ export const ASPECT_RATIOS = ["9:16", "1:1", "16:9"];
16
+ /**
17
+ * Разрешение выхода. Значения — те, что реально принимает HeyGen v3
18
+ * (проверено 18.07.2026: `480p` и ниже отклоняются с
19
+ * `Input should be '4k', '1080p' or '720p'`).
20
+ *
21
+ * Фактические кадры при `aspect_ratio: "9:16"` — замерены на реальных файлах:
22
+ * 720p → 720 × 1280 (~590 КБ на 7 сек)
23
+ * 1080p → 1080 × 1920 (~923 КБ)
24
+ * 4k → 2160 × 3840 (~3.7 МБ)
25
+ *
26
+ * ВАЖНО: на стоимость разрешение НЕ влияет. HeyGen тарифицирует по
27
+ * длительности (~1 кредит/сек): все три варианта стоили одинаково — 8
28
+ * кредитов за один и тот же 7.36-секундный ролик. Понижать разрешение ради
29
+ * экономии кредитов бессмысленно; экономят короткие скрипты и фикстурный
30
+ * режим. 720p в деве берётся ради скорости скачивания и объёма в R2
31
+ * (файл в 6 раз меньше 4k), а не ради денег.
32
+ */
33
+ export const RESOLUTIONS = ["720p", "1080p", "4k"];
34
+ /**
35
+ * Продуктовый дефолт — 1080p, и это не деталь реализации.
36
+ * Конкурент обещает 1080p, а отдаёт 720p (docs/08); наше обещание держать
37
+ * 1080×1920 — заявленный дифференциатор и правило проекта №4. Понижение
38
+ * дефолта допустимо ТОЛЬКО в dev-окружении, где ролики никто не публикует.
39
+ */
40
+ export const DEFAULT_RESOLUTION = "1080p";
41
+ export const DEV_RESOLUTION = "720p";
42
+ /**
43
+ * Разрешение конкретного рана: явный выбор клиента важнее любых дефолтов,
44
+ * иначе dev-окружение молча подменяло бы то, что пользователь запросил.
45
+ */
46
+ export function resolveResolution(requested, isDev) {
47
+ if (requested)
48
+ return requested;
49
+ return isDev ? DEV_RESOLUTION : DEFAULT_RESOLUTION;
50
+ }
51
+ /**
52
+ * Потолок длины скрипта (docs/12 §11, ~45с озвучки) — временная замена
53
+ * мульти-тейка (docs/12 §3: длинные скрипты раньше резались на несколько
54
+ * тейков, эта логика вырезана на время прототипа). Пока мульти-тейк не
55
+ * вернули, скрипт длиннее одного тейка нужно отклонять на входе, а не
56
+ * молча обрезать или рендерить с нарушением пейсинг-гейта.
57
+ *
58
+ * Число слов считаем от ВЕРХНЕЙ границы пейсинга (WORDS_PER_SECOND_MAX из
59
+ * pacing.ts) — это самый длинный скрипт, который ещё способен уложиться в
60
+ * потолок секунд без нарушения пейсинг-гейта. Числа пейсинга не дублируем,
61
+ * берём готовые константы из pacing.ts.
62
+ */
63
+ export const MAX_SCRIPT_SECONDS = 45;
64
+ export const MAX_SCRIPT_WORDS = MAX_SCRIPT_SECONDS * WORDS_PER_SECOND_MAX;
65
+ /**
66
+ * Сырой zod-shape входа make_ugc — БЕЗ `.refine()`.
67
+ *
68
+ * ЗАЧЕМ ОТДЕЛЬНО ОТ `makeUgcInput`: `makeUgcInput` после `.refine()` становится
69
+ * `ZodEffects`, у которого НЕТ `.shape` (ловушка A12). MCP-тул (`tools/list`) и
70
+ * прочие потребители, которым нужен именно объект полей, обязаны собираться из
71
+ * этого shape, а не переписывать поля руками (правило проекта №2 — не дублировать
72
+ * схему). `makeUgcInput` пересобирается из ЭТОГО ЖЕ shape ниже, чтобы они не
73
+ * разошлись.
74
+ */
75
+ /**
76
+ * Один сегмент ролика. Форма объявляется ЗДЕСЬ, в Slice 1, хотя реализация —
77
+ * в Slice 2 (US-526): поле `segments` требует `script → .optional()`, а это
78
+ * меняет `required[]` в `tools/list`. Приедь правка позже, публичная форма
79
+ * сменилась бы ПОСЛЕ проверки вторым пользователем и обесценила бы её —
80
+ * единственную внешне фальсифицируемую проверку продукта. Здесь required-набор
81
+ * меняется один раз, до гейта, а Slice 2 сводится к снятию строки отказа.
82
+ */
83
+ export const ugcSegment = z.object({
84
+ kind: z.enum(["actor", "media"]),
85
+ /** Реплика актёра. У `media`-сегмента отсутствует — там говорит соседний актёр. */
86
+ script: z.string().min(1).max(10_000).optional(),
87
+ /** Полнокадровый врез. Обязателен у `media`, запрещён у `actor`. */
88
+ media_url: z.string().url().optional(),
89
+ });
90
+ /** Потолок актёрских сегментов — ограничение ОДНОГО рана, не суточных трат. */
91
+ export const MAX_ACTOR_SEGMENTS = 3;
92
+ export const MAX_SEGMENTS = 5;
93
+ export const makeUgcInputShape = {
94
+ // НЕОБЯЗАТЕЛЬНЫЙ, но не «необязательный к заполнению»: ровно одно из
95
+ // `script | segments` обязано присутствовать, и это держит `.refine` ниже.
96
+ // JSON-Schema не умеет выразить «ровно одно из двух» через `required[]`,
97
+ // поэтому информативность возвращается текстом описания (блокер B9).
98
+ script: z
99
+ .string()
100
+ .min(1)
101
+ .max(10_000)
102
+ .optional()
103
+ .describe("required unless `segments` is provided; `segments` is currently rejected"),
104
+ segments: z.array(ugcSegment).min(1).max(MAX_SEGMENTS).optional(),
105
+ person: z.string().min(1).max(500).optional(),
106
+ // ТОЛЬКО https-URL. Вариант `data:` снят (US-524): HeyGen принимает
107
+ // исключительно `{type:"url"}`, а заливка data-URI в R2 с presigned-выдачей
108
+ // вскрыла бы инвариант Этапа 4 «R2-URL вендору не передаются». Отказ на входе
109
+ // честнее, чем приём поля, которое некуда деть.
110
+ image: z.string().url().startsWith("https://").optional(),
111
+ character: z.string().regex(/^char_[a-zA-Z0-9]+$/).optional(),
112
+ name: z.string().max(100).optional(),
113
+ broll_url: z.string().url().optional(),
114
+ captions: z.boolean().default(false),
115
+ caption_style: z.enum(CAPTION_STYLES).default("hormozi"),
116
+ look: z.enum(LOOKS).default("natural"),
117
+ // Намеренно БЕЗ .default() — по той же причине, что и resolution ниже, но с
118
+ // ПРОДУКТОВЫМ следствием: дефолт здесь означал бы «клиент всегда просит
119
+ // 9:16», и резолвер формата не смог бы отличить молчание от явного выбора.
120
+ // На этом различии стоит вся развилка: промолчал — снап к формату источника с
121
+ // предупреждением, попросил явно и не сходится — отказ до платного вызова.
122
+ // Дефолт подставляет DEFAULT_ASPECT_RATIO (aspect.ts).
123
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional(),
124
+ // Намеренно БЕЗ .default(): отличить «клиент попросил 1080p» от «клиент
125
+ // промолчал» можно только так. Дефолт подставляет resolveResolution(),
126
+ // потому что он зависит от окружения, а схема о нём знать не должна.
127
+ resolution: z.enum(RESOLUTIONS).optional(),
128
+ // Оба голосовых поля — .optional() БЕЗ .default() по той же причине, что и
129
+ // resolution выше: отличить «клиент явно выбрал голос» от «клиент промолчал»
130
+ // можно только так. Дефолт (клон владельца) подставляет резолвер в адаптере,
131
+ // а не схема; явность нужна тестам warnings (US-517). voice — наше имя пресета
132
+ // (enum порождает tools/list), voice_id — сырой вендорский id как escape hatch.
133
+ voice: z.enum(VOICE_PRESET_NAMES).optional(),
134
+ voice_id: z.string().regex(VOICE_ID_PATTERN).optional(),
135
+ webhook_url: z.string().url().optional(),
136
+ /**
137
+ * Видимая плашка «сделано ИИ» поверх кадра — OPT-IN, а не обязательная.
138
+ *
139
+ * Обязательный вотермарк отменил бы заявленный дифференциатор («без
140
+ * вотермарки»), поэтому раскрытие держится на контрактных полях и на теге
141
+ * внутри файла, а плашка остаётся выбором вызывателя. Выжигание её в кадр
142
+ * требует композиции: она появилась в US-527, и US-539 перевёл поле в
143
+ * `implemented` (обусловлено флагом `CLIPWRIGHT_COMPOSE`).
144
+ */
145
+ disclosure_overlay: z.boolean().optional(),
146
+ /**
147
+ * Чем заполнять кадр, когда клип не покрывает его целиком (US-527).
148
+ *
149
+ * БЕЗ `.default()` НАМЕРЕННО. Дефолт здесь означал бы, что мы выбираем за
150
+ * клиента, как выглядит его ролик там, где формат не совпал с источником, —
151
+ * и выбираем МОЛЧА. Отсутствие поля значит «укладки не требуется»; если она
152
+ * всё же понадобилась, ран обязан сказать об этом в `warnings[]`, а не
153
+ * подобрать фон по своему вкусу.
154
+ *
155
+ * `contain` — вписать целиком (поля видны), `white` — белые поля как у
156
+ * вендора, `blur` — размытая заливка тем же кадром.
157
+ */
158
+ background: z.enum(["white", "blur", "contain"]).optional(),
159
+ };
160
+ /**
161
+ * Ретрай-параметр агентного скилла: НОМЕР попытки. Живёт в core (не рукописно в
162
+ * MCP и CLI сразу), но ОТДЕЛЬНО от `makeUgcInputShape` — это вход СКИЛЛА
163
+ * (порождает суффикс `:N` у ключа идемпотентности в SDK), а не параметр рендера.
164
+ */
165
+ export const agentRetryShape = { attempt: z.number().int().min(1).optional() };
166
+ /**
167
+ * Вход make_ugc. Не более одного из person | image задаёт идентичность (оба
168
+ * опущены → дефолтный актёр). Схема — единый источник правды для REST, MCP
169
+ * (tools/list) и SDK.
170
+ *
171
+ * `character` из взаимоисключения ВЫВЕДЕН, а не забыт: его диспозиция —
172
+ * `rejected` (`contract-dispositions.ts`), то есть поле отбивается на входе
173
+ * раньше, чем дойдёт до этой проверки. Держать его здесь значило бы
174
+ * валидировать выбор между вариантами, один из которых не существует.
175
+ */
176
+ export const makeUgcInput = z
177
+ .object(makeUgcInputShape)
178
+ // Ровно одно из script | segments. Ни одного — рендерить нечего; оба —
179
+ // непонятно, что говорить, и молча выбрать одно значило бы вернуть ту самую
180
+ // молчаливую подмену.
181
+ .refine((v) => (v.script === undefined) !== (v.segments === undefined), {
182
+ message: "pass exactly one of script | segments",
183
+ })
184
+ .refine((v) => [v.person, v.image].filter(Boolean).length <= 1, { message: "pass at most one of person | image" })
185
+ // ПРАВИЛА СЕГМЕНТОВ (US-526). Каждое — ОТДЕЛЬНЫЙ .refine, а не одна проверка
186
+ // с общим текстом: сообщение обязано называть нарушенное правило, иначе агент
187
+ // получает «invalid input» и чинит вход перебором. Порядок правил — от того,
188
+ // что делает ролик невозможным, к тому, что делает его противоречивым.
189
+ //
190
+ // Правила живут ЗДЕСЬ, а не в `ugcSegment`: они про массив целиком (сколько
191
+ // актёров) либо про связь двух полей внутри элемента, а `z.object` элемента
192
+ // видит только сам элемент.
193
+ .refine((v) => v.segments === undefined || v.segments.some((s) => s.kind === "actor"), {
194
+ message: "segments must contain at least one actor segment: nobody speaks otherwise",
195
+ })
196
+ .refine((v) => v.segments === undefined ||
197
+ v.segments.filter((s) => s.kind === "actor").length <= MAX_ACTOR_SEGMENTS, {
198
+ // Потолок ограничивает ОДИН ран, а не суточные траты: денежная граница —
199
+ // суточный потолок US-537, и путать их значило бы обещать защиту,
200
+ // которую это число не даёт (PM3, принцип 5 плана).
201
+ message: `at most ${MAX_ACTOR_SEGMENTS} actor segments per clip`,
202
+ })
203
+ .refine((v) => v.segments === undefined ||
204
+ v.segments.every((s) => s.kind !== "actor" || s.script !== undefined), { message: "actor segment requires `script`: it is the line to be spoken" })
205
+ .refine((v) => v.segments === undefined ||
206
+ v.segments.every((s) => s.kind !== "actor" || s.media_url === undefined), { message: "actor segment must not carry `media_url`: use a media segment for footage" })
207
+ .refine((v) => v.segments === undefined ||
208
+ v.segments.every((s) => s.kind !== "media" || s.media_url !== undefined), { message: "media segment requires `media_url`: there is no footage to show otherwise" })
209
+ // Голос задаётся ровно одним способом: именем пресета (voice) ИЛИ сырым
210
+ // вендорским id (voice_id), не обоими сразу. Взаимоисключение — здесь, в
211
+ // makeUgcInput, а НЕ в makeUgcInputShape: .refine превращает схему в
212
+ // ZodEffects, у которого нет .shape (ловушка A12), а shape порождает
213
+ // tools/list. Оба опущены → резолвер подставит дефолт (клон владельца).
214
+ .refine((v) => !(v.voice !== undefined && v.voice_id !== undefined), { message: "pass at most one of voice | voice_id" })
215
+ // Скрипт из одних пробелов/пустоты проходит символьный .min(1) выше (там
216
+ // считаются сырые символы), но даёт 0 произносимых слов — countWords тримит
217
+ // и схлопывает пробелы. Используем ТУ ЖЕ нормализацию, что и весь пейсинг-гейт
218
+ // (pacing.ts), не переизобретаем "trim && length".
219
+ .refine((v) => v.segments !== undefined || countWords(v.script ?? "") >= 1, {
220
+ message: "script has 0 speakable words",
221
+ })
222
+ // Счёт СЛОВ, не символов: символьный .max(10_000) выше — грубая защита от
223
+ // мусорного ввода, а реальный потолок привязан к произносимой длине
224
+ // (пейсинг-гейт меряет слова/сек, не байты).
225
+ // Потолок слов считается по ТОМУ из двух, что задан: у сегментного ролика
226
+ // произносимый текст — это сумма реплик, и мерить его по отсутствующему
227
+ // `script` значило бы не мерить вовсе.
228
+ .refine((v) => countWords(spokenTextOf(v)) <= MAX_SCRIPT_WORDS, {
229
+ error: (issue) => {
230
+ const wordCount = countWords(spokenTextOf(issue.input));
231
+ return `script is ~${wordCount} words; ceiling is ${MAX_SCRIPT_WORDS} words (≈${MAX_SCRIPT_SECONDS}s of speech at the pacing gate)`;
232
+ },
233
+ });
234
+ /** Склейка произносимого текста без проверок — общая база для гейтов и хелпера. */
235
+ function spokenTextOf(input) {
236
+ if (input.script !== undefined)
237
+ return input.script;
238
+ return (input.segments ?? [])
239
+ .filter((segment) => segment.kind === "actor")
240
+ .map((segment) => segment.script ?? "")
241
+ .join(" ");
242
+ }
243
+ /**
244
+ * ЕДИНСТВЕННОЕ место, где «`script` либо сам, либо склейка сегментов»
245
+ * превращается в `string`.
246
+ *
247
+ * ЗАЧЕМ ХЕЛПЕР, А НЕ `!` НА СЕМИ CALL-SITE'АХ. Снятие обязательности со
248
+ * `script` делает его `string | undefined` у каждого потребителя, и `.refine`
249
+ * спасает в рантайме, а компилятор — нет. Самый чувствительный из них —
250
+ * `elevenlabs-tts.ts`, где комментарий прямо опирался на `z.string().min(1)`
251
+ * как на ГАРД ПЕРЕД ПЛАТНЫМ POST: заткни мы там типы, гард остался бы снятым,
252
+ * а компилятор бы об этом молчал. Поэтому проверка непустоты живёт ВНУТРИ
253
+ * хелпера и восстанавливает гард явно, а не полагается на снятую схему.
254
+ */
255
+ export function resolvedScript(input) {
256
+ const text = spokenTextOf(input);
257
+ if (countWords(text) < 1) {
258
+ throw new Error("no speakable script: pass script or segments with actor lines");
259
+ }
260
+ return text;
261
+ }
262
+ // Тип ВХОДА (`MakeUgcInputArgs`) живёт в `contract-dispositions.ts`: он выводится
263
+ // из ПРЕДЛАГАЕМОГО shape, а не из полного, иначе автокомплит SDK показывал бы
264
+ // отклонённые поля, которые сервер отбивает четырёхсотым (блокер B4).
265
+ export const quoteResponse = z.object({
266
+ skill: z.string(),
267
+ credits_estimate: z.number().int().nonnegative(),
268
+ duration_estimate_sec: z.number().positive(),
269
+ // несоблюдаемые параметры объявляются заранее, а не молча игнорируются
270
+ warnings: z.array(z.string()).default([]),
271
+ // Версия контракта в самом ответе, а не только в заголовке: агент, читающий
272
+ // quote, обязан видеть, ПОД КАКОЙ контракт он получил цену.
273
+ contract_version: z.string(),
274
+ /**
275
+ * Что мы знаем об источнике кадра. `null` ТОЛЬКО когда `image` дан, но
276
+ * пробу сделать не удалось — отсутствие `image` источником не является, там
277
+ * стоит дефолтный актёр со своими размерами. Это и есть превращение
278
+ * невидимой константы в наблюдаемую величину: агент видит, ПОЧЕМУ формат
279
+ * получился таким, ещё до платного рендера.
280
+ */
281
+ source: z
282
+ .object({
283
+ width: z.number().positive(),
284
+ height: z.number().positive(),
285
+ aspect_ratio: z.string(),
286
+ })
287
+ .nullable(),
288
+ /** Формат, который реально будет отрендерен на этом входе. */
289
+ resolved_aspect_ratio: z.enum(ASPECT_RATIOS),
290
+ });
291
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Вендор-нейтральный контракт синтеза речи.
3
+ *
4
+ * ЗАЧЕМ отдельно от рендера: провайдеры TTS меняются независимо от провайдеров
5
+ * аватара, и «ElevenLabs + HeyGen» — лишь одна из комбинаций.
6
+ *
7
+ * Ключевая причина существования этого файла — ТАЙМИНГИ. ElevenLabs отдаёт их
8
+ * ПОСИМВОЛЬНО (проверено 18.07.2026: 134 символа → 134 тайминга), а этап B
9
+ * ждёт пословные для `createTikTokStyleCaptions()`. Другой провайдер отдаст
10
+ * пословные сразу или не отдаст вовсе. Нормализация обязана происходить в
11
+ * адаптере, иначе посимвольный формат ElevenLabs протечёт в пайплайн субтитров
12
+ * и намертво привяжет нас к вендору.
13
+ */
14
+ /** Тайминг одного слова. Единица, в которой пайплайн субтитров думает. */
15
+ export interface WordTiming {
16
+ word: string;
17
+ startSec: number;
18
+ endSec: number;
19
+ }
20
+ export interface SpeechRequest {
21
+ text: string;
22
+ /** Идентификатор голоса в терминах бэкенда. Маппинг наших пресетов — забота адаптера. */
23
+ voiceId: string;
24
+ /** Наш run_id — для идемпотентности и трассировки на стороне вендора. */
25
+ runId: string;
26
+ }
27
+ export interface SpeechResult {
28
+ audio: Uint8Array;
29
+ mimeType: string;
30
+ durationSec: number;
31
+ /**
32
+ * Пословные тайминги. `null` — бэкенд их не отдаёт вовсе; тогда пайплайн
33
+ * субтитров обязан добывать их отдельно (Whisper), и это должно быть видно
34
+ * по типу, а не выясняться в рантайме.
35
+ *
36
+ * Адаптер обязан отдавать ПОСЛОВНЫЕ тайминги, даже если вендор отдал
37
+ * посимвольные: агрегация — его работа, а не работа потребителя.
38
+ */
39
+ words: WordTiming[] | null;
40
+ }
41
+ export interface TtsCapabilities {
42
+ /** Максимум символов за запрос. null — вендор не документирует (случай ElevenLabs). */
43
+ maxChars: number | null;
44
+ /** Отдаёт ли тайминги вообще. */
45
+ providesTimings: boolean;
46
+ /**
47
+ * Сколько запросов можно вести параллельно. У ElevenLabs зависит от тарифа
48
+ * (Creator = 5) и упрётся раньше лимитов рендера при мульти-тейке в этапе B.
49
+ */
50
+ maxConcurrency: number | null;
51
+ }
52
+ export interface TtsBackend {
53
+ readonly name: string;
54
+ readonly capabilities: TtsCapabilities;
55
+ synthesize(req: SpeechRequest): Promise<SpeechResult>;
56
+ }
57
+ /**
58
+ * Склейка посимвольных таймингов в пословные.
59
+ *
60
+ * Живёт здесь, а не в адаптере ElevenLabs, потому что посимвольный формат
61
+ * встречается у нескольких вендоров, и переписывать эту логику под каждого —
62
+ * лишняя работа с одинаковыми граничными случаями.
63
+ *
64
+ * Границей слова считается пробельный символ; сам пробел в слово не входит,
65
+ * но и не рвёт уже начатое. Пустые слова не порождаются.
66
+ */
67
+ export declare function charTimingsToWords(characters: readonly string[], startTimes: readonly number[], endTimes: readonly number[]): WordTiming[];