@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.
- package/LICENSE +21 -0
- package/README.md +14 -0
- package/dist/aspect.d.ts +153 -0
- package/dist/aspect.js +237 -0
- package/dist/aspect.js.map +1 -0
- package/dist/contract-dispositions.d.ts +335 -0
- package/dist/contract-dispositions.js +358 -0
- package/dist/contract-dispositions.js.map +1 -0
- package/dist/idempotency.d.ts +45 -0
- package/dist/idempotency.js +72 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/pacing.d.ts +4 -0
- package/dist/pacing.js +7 -0
- package/dist/pacing.js.map +1 -0
- package/dist/render-backend.d.ts +157 -0
- package/dist/render-backend.js +31 -0
- package/dist/render-backend.js.map +1 -0
- package/dist/runs.d.ts +176 -0
- package/dist/runs.js +137 -0
- package/dist/runs.js.map +1 -0
- package/dist/skills.d.ts +270 -0
- package/dist/skills.js +291 -0
- package/dist/skills.js.map +1 -0
- package/dist/tts-backend.d.ts +67 -0
- package/dist/tts-backend.js +54 -0
- package/dist/tts-backend.js.map +1 -0
- package/dist/voices.d.ts +166 -0
- package/dist/voices.js +193 -0
- package/dist/voices.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* Склейка посимвольных таймингов в пословные.
|
|
16
|
+
*
|
|
17
|
+
* Живёт здесь, а не в адаптере ElevenLabs, потому что посимвольный формат
|
|
18
|
+
* встречается у нескольких вендоров, и переписывать эту логику под каждого —
|
|
19
|
+
* лишняя работа с одинаковыми граничными случаями.
|
|
20
|
+
*
|
|
21
|
+
* Границей слова считается пробельный символ; сам пробел в слово не входит,
|
|
22
|
+
* но и не рвёт уже начатое. Пустые слова не порождаются.
|
|
23
|
+
*/
|
|
24
|
+
export function charTimingsToWords(characters, startTimes, endTimes) {
|
|
25
|
+
if (characters.length !== startTimes.length || characters.length !== endTimes.length) {
|
|
26
|
+
throw new Error(`character/timing length mismatch: ${characters.length} chars, ` +
|
|
27
|
+
`${startTimes.length} starts, ${endTimes.length} ends`);
|
|
28
|
+
}
|
|
29
|
+
const words = [];
|
|
30
|
+
let buf = "";
|
|
31
|
+
let start = 0;
|
|
32
|
+
let end = 0;
|
|
33
|
+
const flush = () => {
|
|
34
|
+
if (buf.length > 0) {
|
|
35
|
+
words.push({ word: buf, startSec: start, endSec: end });
|
|
36
|
+
buf = "";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
for (let i = 0; i < characters.length; i += 1) {
|
|
40
|
+
const ch = characters[i];
|
|
41
|
+
if (/\s/.test(ch)) {
|
|
42
|
+
flush();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (buf.length === 0) {
|
|
46
|
+
start = startTimes[i];
|
|
47
|
+
}
|
|
48
|
+
buf += ch;
|
|
49
|
+
end = endTimes[i];
|
|
50
|
+
}
|
|
51
|
+
flush();
|
|
52
|
+
return words;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=tts-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-backend.js","sourceRoot":"","sources":["../src/tts-backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkDH;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAA6B,EAC7B,UAA6B,EAC7B,QAA2B;IAE3B,IAAI,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM,IAAI,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrF,MAAM,IAAI,KAAK,CACb,qCAAqC,UAAU,CAAC,MAAM,UAAU;YAC9D,GAAG,UAAU,CAAC,MAAM,YAAY,QAAQ,CAAC,MAAM,OAAO,CACzD,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,CAAC,CAAC;IAEZ,MAAM,KAAK,GAAG,GAAS,EAAE;QACvB,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACxD,GAAG,GAAG,EAAE,CAAC;QACX,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAW,CAAC;QACnC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAClB,KAAK,EAAE,CAAC;YACR,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,KAAK,GAAG,UAAU,CAAC,CAAC,CAAW,CAAC;QAClC,CAAC;QACD,GAAG,IAAI,EAAE,CAAC;QACV,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAW,CAAC;IAC9B,CAAC;IACD,KAAK,EAAE,CAAC;IAER,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/voices.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог голосовых пресетов — НАШИ имена + метадата, БЕЗ вендорских id.
|
|
3
|
+
*
|
|
4
|
+
* ЗАЧЕМ имена здесь, а id в адаптере (условие плана R4-b, принцип 5):
|
|
5
|
+
* `VOICE_PRESET_NAMES` порождает `z.enum` во входе `make_ugc` (`makeUgcInputShape`),
|
|
6
|
+
* а он, в свою очередь, — `tools/list`/OpenAPI/типы SDK. Значит имена ОБЯЗАНЫ жить
|
|
7
|
+
* в core. Вендорские ElevenLabs-id — платный секрет вендора и живут ровно в одном
|
|
8
|
+
* месте — адаптере воркера (`PRESET_TO_VOICE_ID`, US-514); в core их нет ни одного,
|
|
9
|
+
* иначе поверхность ключа расширилась бы (был инцидент 6fbe5b6). Расщепление
|
|
10
|
+
* «имя в core / id в адаптере» вынуждено этими двумя инвариантами.
|
|
11
|
+
*
|
|
12
|
+
* ПОЧЕМУ у `owner_ru_clone` нет `gender`: это клон голоса владельца, и у вендора
|
|
13
|
+
* для него нет лейбла гендера. Выдумывать метадату за вендора нельзя (принцип
|
|
14
|
+
* «мы не должны выдумывать», условие §6) — отсутствие поля само по себе значимо
|
|
15
|
+
* и участвует в precedence-логике verify-voices (US-520) и warnings (US-517).
|
|
16
|
+
*/
|
|
17
|
+
export interface VoicePreset {
|
|
18
|
+
/** Язык голоса в терминах BCP-47-подобного короткого кода (ru/en). */
|
|
19
|
+
language: string;
|
|
20
|
+
/**
|
|
21
|
+
* Гендерный лейбл голоса. ОПУЩЕН намеренно, если у вендора его нет
|
|
22
|
+
* (клон владельца): отсутствие ≠ "неизвестно", это осознанный сигнал.
|
|
23
|
+
*/
|
|
24
|
+
gender?: "female" | "male";
|
|
25
|
+
/** Человекочитаемое описание для list_voices (US-516). */
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Стартовый набор из 5 пресетов (условие плана §4). Порядок значим только для
|
|
30
|
+
* презентации; дефолт задаётся отдельно через `DEFAULT_VOICE_PRESET`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const VOICE_PRESETS: {
|
|
33
|
+
readonly owner_ru_clone: {
|
|
34
|
+
readonly language: "ru";
|
|
35
|
+
readonly description: "Клон голоса владельца (David-Dmitry-Ru); русский — на английском даёт акцент";
|
|
36
|
+
};
|
|
37
|
+
readonly sarah: {
|
|
38
|
+
readonly language: "en";
|
|
39
|
+
readonly gender: "female";
|
|
40
|
+
readonly description: "Soft, natural female voice (English)";
|
|
41
|
+
};
|
|
42
|
+
readonly george: {
|
|
43
|
+
readonly language: "en";
|
|
44
|
+
readonly gender: "male";
|
|
45
|
+
readonly description: "Warm British storyteller, narrative pacing (English)";
|
|
46
|
+
};
|
|
47
|
+
readonly eric: {
|
|
48
|
+
readonly language: "en";
|
|
49
|
+
readonly gender: "male";
|
|
50
|
+
readonly description: "Smooth American, conversational tone (English)";
|
|
51
|
+
};
|
|
52
|
+
readonly daria_ru_female: {
|
|
53
|
+
readonly language: "ru";
|
|
54
|
+
readonly gender: "female";
|
|
55
|
+
readonly description: "Женский повествовательный голос (русский)";
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Кортеж имён пресетов для `z.enum`. Выводится из ключей `VOICE_PRESETS`, чтобы
|
|
60
|
+
* список имён и метадата не разъехались (правило проекта №2 — не дублировать).
|
|
61
|
+
*/
|
|
62
|
+
export declare const VOICE_PRESET_NAMES: [keyof typeof VOICE_PRESETS, ...(keyof typeof VOICE_PRESETS)[]];
|
|
63
|
+
export type VoicePresetName = keyof typeof VOICE_PRESETS;
|
|
64
|
+
/**
|
|
65
|
+
* Дефолт под текущий аватар (look-01 «warm storyteller»): `george` —
|
|
66
|
+
* британский повествовательный голос, семантически совпадает с образом.
|
|
67
|
+
* Изначально дефолтом ставился клон владельца, но на английских скриптах он
|
|
68
|
+
* давал русский акцент (on-ear владельца 2026-07-20) — сменён на нейтральный
|
|
69
|
+
* английский. Клон остаётся доступным пресетом `owner_ru_clone` для русских
|
|
70
|
+
* скриптов. Подстановка дефолта — забота резолвера, а не схемы (схема хранит
|
|
71
|
+
* `.optional()` без `.default()`, чтобы отличать явный выбор от умолчания —
|
|
72
|
+
* нужно для warnings US-517).
|
|
73
|
+
*/
|
|
74
|
+
export declare const DEFAULT_VOICE_PRESET: "george";
|
|
75
|
+
/**
|
|
76
|
+
* Форма сырого `voice_id` вендора. НАМЕРЕННО терпимая (16–32 алфанумерик):
|
|
77
|
+
* ElevenLabs не документирует формат id, наблюдаемые id — 20 символов, но
|
|
78
|
+
* строгая длина 20 превратила бы будущий валидный id иной длины в ложный
|
|
79
|
+
* отказ. Это лишь грубый фильтр мусора на входе; реальную проверку
|
|
80
|
+
* существования id делает бесплатный префлайт в воркере (US-515).
|
|
81
|
+
*/
|
|
82
|
+
export declare const VOICE_ID_PATTERN: RegExp;
|
|
83
|
+
/**
|
|
84
|
+
* Результат разрешения выбора голоса — дискриминированное объединение. `preset`
|
|
85
|
+
* несёт НАШЕ имя (адаптер превратит его в вендорский id через `PRESET_TO_VOICE_ID`),
|
|
86
|
+
* `raw` — уже сырой вендорский id (escape hatch). Core НЕ знает вендорских id, и
|
|
87
|
+
* это объединение — граница: дальше маппинг делает адаптер (принцип 5).
|
|
88
|
+
*/
|
|
89
|
+
export type VoiceSelection = {
|
|
90
|
+
kind: "preset";
|
|
91
|
+
preset: VoicePresetName;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "raw";
|
|
94
|
+
voiceId: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Чистое разрешение приоритета голосовых полей входа.
|
|
98
|
+
*
|
|
99
|
+
* Взаимоисключение `voice`/`voice_id` уже гарантировано `.refine` в
|
|
100
|
+
* `makeUgcInput` (skills.ts), поэтому здесь — только приоритет и подстановка
|
|
101
|
+
* дефолта, без повторной валидации. Приоритет: сырой `voice_id` (явный escape
|
|
102
|
+
* hatch) > `voice` (имя пресета) > дефолт (клон владельца). Дефолт живёт тут, а
|
|
103
|
+
* не в схеме, потому что схема хранит `.optional()` без `.default()`, чтобы
|
|
104
|
+
* отличать явный выбор от умолчания (warnings US-517).
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveVoiceSelection(input: {
|
|
107
|
+
voice?: VoicePresetName;
|
|
108
|
+
voice_id?: string;
|
|
109
|
+
}): VoiceSelection;
|
|
110
|
+
/**
|
|
111
|
+
* Семейство письменности скрипта — грубый детектор для языкового warning'а
|
|
112
|
+
* (US-517). Считает ТОЛЬКО буквенные символы: кириллица (U+0400–U+04FF) против
|
|
113
|
+
* латиницы (A–Z/a–z). Знаки препинания, цифры, пробелы, эмодзи игнорируются —
|
|
114
|
+
* они не различают язык.
|
|
115
|
+
*
|
|
116
|
+
* НЕЙТРАЛЬНАЯ ПОЛОСА (детерминированно, тестируемо):
|
|
117
|
+
* - < 2 букв всего (пусто, одни цифры, единичная буква-случайность) → "neutral":
|
|
118
|
+
* данных для суждения нет (это «мало букв» из плана §Р3, условие ii);
|
|
119
|
+
* - ни одна сторона не набрала порога доминирования 60% (напр. смесь 50/50) →
|
|
120
|
+
* "neutral": язык скрипта неоднозначен, warning выдавать не на чем.
|
|
121
|
+
* Только уверенное доминирование (≥60%) даёт "cyrillic"/"latin".
|
|
122
|
+
*/
|
|
123
|
+
export declare const SCRIPT_DOMINANCE_THRESHOLD = 0.6;
|
|
124
|
+
export declare function detectScriptFamily(script: string): "cyrillic" | "latin" | "neutral";
|
|
125
|
+
/**
|
|
126
|
+
* Warning отложенной проверки сырого voice_id на ПУТИ QUOTE (US-517). На quote
|
|
127
|
+
* префлайта ещё нет (он бесплатный, но живёт в воркере, US-515), поэтому честно
|
|
128
|
+
* сообщаем: существование id сверяется со счётом на СТАРТЕ рана, не сейчас. На
|
|
129
|
+
* пути рана этот warning НЕ выдаётся — там префлайт уже отработал и, если каталог
|
|
130
|
+
* неполон, добавит собственный `VOICE_CATALOG_INCOMPLETE_WARNING` (воркер зовёт
|
|
131
|
+
* buildVoiceWarnings без voice_id, чтобы эти два не дублировали друг друга).
|
|
132
|
+
*/
|
|
133
|
+
export declare const RAW_VOICE_ID_VALIDATION_WARNING = "raw voice_id is verified against your account at run start, not at quote time";
|
|
134
|
+
/**
|
|
135
|
+
* Голосовые warnings по ТЕСТУ ДОПУСТИМОСТИ (план §Р3). Warning допустим, только
|
|
136
|
+
* если ОДНОВРЕМЕННО: (i) причина — то, что клиент запросил ЯВНО (не дефолт, не наш
|
|
137
|
+
* выбор); (ii) утверждение опирается на данные, которые ЕСТЬ (лейбл присутствует),
|
|
138
|
+
* а не на их отсутствие; (iii) на дефолтном happy-path не срабатывает НИКОГДА.
|
|
139
|
+
*
|
|
140
|
+
* ПРАВИЛО СТАРШИНСТВА ПОДМЕНЫ: подмена (клиент попросил X, отдали Y) предупреждает
|
|
141
|
+
* ВСЕГДА, независимо от (iii) — (iii) глушит уведомления о ПРАВИЛЬНОМ поведении, не
|
|
142
|
+
* о деградированном. Канон такой подмены — 1080p→720p (render-backend.ts:144). В
|
|
143
|
+
* ДАННОМ дизайне подмены голоса НЕТ: путь падает closed (сырой id не найден → бросок
|
|
144
|
+
* до платного POST, US-515; неизвестное имя → 400 на границе enum), голос молча не
|
|
145
|
+
* заменяется. Поэтому правило старшинства здесь не применяется — но зафиксировано,
|
|
146
|
+
* чтобы будущая подмена не была ошибочно заглушена условием (iii).
|
|
147
|
+
*
|
|
148
|
+
* ПОЧЕМУ НЕТ ГЕНДЕР-WARNING (осознанное отсутствие, а не упущение): у нас нет
|
|
149
|
+
* источника правды о гендере АВАТАРА — модель персонажа ещё не приземлилась (план
|
|
150
|
+
* §D, открытый вопрос 4), а у клона владельца (`owner_ru_clone`) вендор не даёт и
|
|
151
|
+
* гендера ГОЛОСА (правило проекта — не выдумывать метадату за вендора, условие §6).
|
|
152
|
+
* Нормативно: warning о несовпадении голос/аватар опирался бы на ОТСУТСТВУЮЩИЕ
|
|
153
|
+
* данные (нарушение условия ii) и на дефолтном пути шумел бы (нарушение iii). Ни
|
|
154
|
+
* один кодовый путь ниже не порождает строки со словом gender по построению.
|
|
155
|
+
*
|
|
156
|
+
* ПОЧЕМУ тест не подавляет обязательный warning про подмену №3 (принцип DR-2):
|
|
157
|
+
* обязанность №3 крепится к НЕИСПОЛНЕННОМУ явному запросу (клиент попросил 1080p,
|
|
158
|
+
* дали 720p) — это подмена, у неё старшинство над (iii). Здесь же голос либо
|
|
159
|
+
* исполняется как запрошен, либо ран падает — деградированной выдачи под видом
|
|
160
|
+
* успешной не бывает, глушить нечего.
|
|
161
|
+
*/
|
|
162
|
+
export declare function buildVoiceWarnings(input: {
|
|
163
|
+
voice?: VoicePresetName | undefined;
|
|
164
|
+
voice_id?: string | undefined;
|
|
165
|
+
script: string;
|
|
166
|
+
}): string[];
|
package/dist/voices.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Стартовый набор из 5 пресетов (условие плана §4). Порядок значим только для
|
|
3
|
+
* презентации; дефолт задаётся отдельно через `DEFAULT_VOICE_PRESET`.
|
|
4
|
+
*/
|
|
5
|
+
export const VOICE_PRESETS = {
|
|
6
|
+
owner_ru_clone: {
|
|
7
|
+
language: "ru",
|
|
8
|
+
description: "Клон голоса владельца (David-Dmitry-Ru); русский — на английском даёт акцент",
|
|
9
|
+
},
|
|
10
|
+
sarah: {
|
|
11
|
+
language: "en",
|
|
12
|
+
gender: "female",
|
|
13
|
+
description: "Soft, natural female voice (English)",
|
|
14
|
+
},
|
|
15
|
+
george: {
|
|
16
|
+
language: "en",
|
|
17
|
+
gender: "male",
|
|
18
|
+
description: "Warm British storyteller, narrative pacing (English)",
|
|
19
|
+
},
|
|
20
|
+
eric: {
|
|
21
|
+
language: "en",
|
|
22
|
+
gender: "male",
|
|
23
|
+
description: "Smooth American, conversational tone (English)",
|
|
24
|
+
},
|
|
25
|
+
daria_ru_female: {
|
|
26
|
+
language: "ru",
|
|
27
|
+
gender: "female",
|
|
28
|
+
description: "Женский повествовательный голос (русский)",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Кортеж имён пресетов для `z.enum`. Выводится из ключей `VOICE_PRESETS`, чтобы
|
|
33
|
+
* список имён и метадата не разъехались (правило проекта №2 — не дублировать).
|
|
34
|
+
*/
|
|
35
|
+
export const VOICE_PRESET_NAMES = Object.keys(VOICE_PRESETS);
|
|
36
|
+
/**
|
|
37
|
+
* Дефолт под текущий аватар (look-01 «warm storyteller»): `george` —
|
|
38
|
+
* британский повествовательный голос, семантически совпадает с образом.
|
|
39
|
+
* Изначально дефолтом ставился клон владельца, но на английских скриптах он
|
|
40
|
+
* давал русский акцент (on-ear владельца 2026-07-20) — сменён на нейтральный
|
|
41
|
+
* английский. Клон остаётся доступным пресетом `owner_ru_clone` для русских
|
|
42
|
+
* скриптов. Подстановка дефолта — забота резолвера, а не схемы (схема хранит
|
|
43
|
+
* `.optional()` без `.default()`, чтобы отличать явный выбор от умолчания —
|
|
44
|
+
* нужно для warnings US-517).
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_VOICE_PRESET = "george";
|
|
47
|
+
/**
|
|
48
|
+
* Форма сырого `voice_id` вендора. НАМЕРЕННО терпимая (16–32 алфанумерик):
|
|
49
|
+
* ElevenLabs не документирует формат id, наблюдаемые id — 20 символов, но
|
|
50
|
+
* строгая длина 20 превратила бы будущий валидный id иной длины в ложный
|
|
51
|
+
* отказ. Это лишь грубый фильтр мусора на входе; реальную проверку
|
|
52
|
+
* существования id делает бесплатный префлайт в воркере (US-515).
|
|
53
|
+
*/
|
|
54
|
+
export const VOICE_ID_PATTERN = /^[A-Za-z0-9]{16,32}$/;
|
|
55
|
+
/**
|
|
56
|
+
* Чистое разрешение приоритета голосовых полей входа.
|
|
57
|
+
*
|
|
58
|
+
* Взаимоисключение `voice`/`voice_id` уже гарантировано `.refine` в
|
|
59
|
+
* `makeUgcInput` (skills.ts), поэтому здесь — только приоритет и подстановка
|
|
60
|
+
* дефолта, без повторной валидации. Приоритет: сырой `voice_id` (явный escape
|
|
61
|
+
* hatch) > `voice` (имя пресета) > дефолт (клон владельца). Дефолт живёт тут, а
|
|
62
|
+
* не в схеме, потому что схема хранит `.optional()` без `.default()`, чтобы
|
|
63
|
+
* отличать явный выбор от умолчания (warnings US-517).
|
|
64
|
+
*/
|
|
65
|
+
export function resolveVoiceSelection(input) {
|
|
66
|
+
if (input.voice_id !== undefined) {
|
|
67
|
+
return { kind: "raw", voiceId: input.voice_id };
|
|
68
|
+
}
|
|
69
|
+
if (input.voice !== undefined) {
|
|
70
|
+
return { kind: "preset", preset: input.voice };
|
|
71
|
+
}
|
|
72
|
+
return { kind: "preset", preset: DEFAULT_VOICE_PRESET };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Семейство письменности скрипта — грубый детектор для языкового warning'а
|
|
76
|
+
* (US-517). Считает ТОЛЬКО буквенные символы: кириллица (U+0400–U+04FF) против
|
|
77
|
+
* латиницы (A–Z/a–z). Знаки препинания, цифры, пробелы, эмодзи игнорируются —
|
|
78
|
+
* они не различают язык.
|
|
79
|
+
*
|
|
80
|
+
* НЕЙТРАЛЬНАЯ ПОЛОСА (детерминированно, тестируемо):
|
|
81
|
+
* - < 2 букв всего (пусто, одни цифры, единичная буква-случайность) → "neutral":
|
|
82
|
+
* данных для суждения нет (это «мало букв» из плана §Р3, условие ii);
|
|
83
|
+
* - ни одна сторона не набрала порога доминирования 60% (напр. смесь 50/50) →
|
|
84
|
+
* "neutral": язык скрипта неоднозначен, warning выдавать не на чем.
|
|
85
|
+
* Только уверенное доминирование (≥60%) даёт "cyrillic"/"latin".
|
|
86
|
+
*/
|
|
87
|
+
export const SCRIPT_DOMINANCE_THRESHOLD = 0.6;
|
|
88
|
+
export function detectScriptFamily(script) {
|
|
89
|
+
let cyrillic = 0;
|
|
90
|
+
let latin = 0;
|
|
91
|
+
for (const ch of script) {
|
|
92
|
+
if (ch >= "Ѐ" && ch <= "ӿ") {
|
|
93
|
+
cyrillic += 1;
|
|
94
|
+
}
|
|
95
|
+
else if ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z")) {
|
|
96
|
+
latin += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const total = cyrillic + latin;
|
|
100
|
+
// «Мало букв» → нейтрально: судить о языке не на чем (условие ii — данных нет).
|
|
101
|
+
if (total < 2) {
|
|
102
|
+
return "neutral";
|
|
103
|
+
}
|
|
104
|
+
if (cyrillic / total >= SCRIPT_DOMINANCE_THRESHOLD) {
|
|
105
|
+
return "cyrillic";
|
|
106
|
+
}
|
|
107
|
+
if (latin / total >= SCRIPT_DOMINANCE_THRESHOLD) {
|
|
108
|
+
return "latin";
|
|
109
|
+
}
|
|
110
|
+
return "neutral";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Отображение нашего короткого языкового лейбла пресета в семейство письменности.
|
|
114
|
+
* НАМЕРЕННО охватывает только языки, которые мы реально размечаем (ru/en): лейбл
|
|
115
|
+
* вне этого набора трактуется как «нет данных для сравнения» и глушит warning
|
|
116
|
+
* (условие ii — утверждение опирается на присутствующие данные, а не на догадку).
|
|
117
|
+
*/
|
|
118
|
+
const LANGUAGE_TO_SCRIPT_FAMILY = {
|
|
119
|
+
ru: "cyrillic",
|
|
120
|
+
en: "latin",
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Warning отложенной проверки сырого voice_id на ПУТИ QUOTE (US-517). На quote
|
|
124
|
+
* префлайта ещё нет (он бесплатный, но живёт в воркере, US-515), поэтому честно
|
|
125
|
+
* сообщаем: существование id сверяется со счётом на СТАРТЕ рана, не сейчас. На
|
|
126
|
+
* пути рана этот warning НЕ выдаётся — там префлайт уже отработал и, если каталог
|
|
127
|
+
* неполон, добавит собственный `VOICE_CATALOG_INCOMPLETE_WARNING` (воркер зовёт
|
|
128
|
+
* buildVoiceWarnings без voice_id, чтобы эти два не дублировали друг друга).
|
|
129
|
+
*/
|
|
130
|
+
export const RAW_VOICE_ID_VALIDATION_WARNING = "raw voice_id is verified against your account at run start, not at quote time";
|
|
131
|
+
/**
|
|
132
|
+
* Голосовые warnings по ТЕСТУ ДОПУСТИМОСТИ (план §Р3). Warning допустим, только
|
|
133
|
+
* если ОДНОВРЕМЕННО: (i) причина — то, что клиент запросил ЯВНО (не дефолт, не наш
|
|
134
|
+
* выбор); (ii) утверждение опирается на данные, которые ЕСТЬ (лейбл присутствует),
|
|
135
|
+
* а не на их отсутствие; (iii) на дефолтном happy-path не срабатывает НИКОГДА.
|
|
136
|
+
*
|
|
137
|
+
* ПРАВИЛО СТАРШИНСТВА ПОДМЕНЫ: подмена (клиент попросил X, отдали Y) предупреждает
|
|
138
|
+
* ВСЕГДА, независимо от (iii) — (iii) глушит уведомления о ПРАВИЛЬНОМ поведении, не
|
|
139
|
+
* о деградированном. Канон такой подмены — 1080p→720p (render-backend.ts:144). В
|
|
140
|
+
* ДАННОМ дизайне подмены голоса НЕТ: путь падает closed (сырой id не найден → бросок
|
|
141
|
+
* до платного POST, US-515; неизвестное имя → 400 на границе enum), голос молча не
|
|
142
|
+
* заменяется. Поэтому правило старшинства здесь не применяется — но зафиксировано,
|
|
143
|
+
* чтобы будущая подмена не была ошибочно заглушена условием (iii).
|
|
144
|
+
*
|
|
145
|
+
* ПОЧЕМУ НЕТ ГЕНДЕР-WARNING (осознанное отсутствие, а не упущение): у нас нет
|
|
146
|
+
* источника правды о гендере АВАТАРА — модель персонажа ещё не приземлилась (план
|
|
147
|
+
* §D, открытый вопрос 4), а у клона владельца (`owner_ru_clone`) вендор не даёт и
|
|
148
|
+
* гендера ГОЛОСА (правило проекта — не выдумывать метадату за вендора, условие §6).
|
|
149
|
+
* Нормативно: warning о несовпадении голос/аватар опирался бы на ОТСУТСТВУЮЩИЕ
|
|
150
|
+
* данные (нарушение условия ii) и на дефолтном пути шумел бы (нарушение iii). Ни
|
|
151
|
+
* один кодовый путь ниже не порождает строки со словом gender по построению.
|
|
152
|
+
*
|
|
153
|
+
* ПОЧЕМУ тест не подавляет обязательный warning про подмену №3 (принцип DR-2):
|
|
154
|
+
* обязанность №3 крепится к НЕИСПОЛНЕННОМУ явному запросу (клиент попросил 1080p,
|
|
155
|
+
* дали 720p) — это подмена, у неё старшинство над (iii). Здесь же голос либо
|
|
156
|
+
* исполняется как запрошен, либо ран падает — деградированной выдачи под видом
|
|
157
|
+
* успешной не бывает, глушить нечего.
|
|
158
|
+
*/
|
|
159
|
+
export function buildVoiceWarnings(input) {
|
|
160
|
+
// Сырой escape-hatch id: причина явная (i), но проверка существования отложена
|
|
161
|
+
// до рана. На quote префлайта нет — предупреждаем об отложенной валидации.
|
|
162
|
+
if (input.voice_id !== undefined) {
|
|
163
|
+
return [RAW_VOICE_ID_VALIDATION_WARNING];
|
|
164
|
+
}
|
|
165
|
+
// Условие (iii): голос НЕ задан явно → дефолт (клон владельца). На дефолтном
|
|
166
|
+
// happy-path языковых warning'ов НЕТ НИКОГДА — это несущий анти-шум US-517.
|
|
167
|
+
if (input.voice === undefined) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
// Defensive: до вызова zod уже сузил voice до валидного имени enum, но чистая
|
|
171
|
+
// функция не полагается на это. Неизвестное имя → нет пресета → нет данных → [].
|
|
172
|
+
const preset = VOICE_PRESETS[input.voice];
|
|
173
|
+
if (preset === undefined) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
// Условие (ii): сравнивать можно только при ПРИСУТСТВУЮЩЕМ пригодном лейбле.
|
|
177
|
+
// Язык вне размеченного набора (ru/en) — «нет данных» → молчим, не выдумываем.
|
|
178
|
+
const voiceFamily = LANGUAGE_TO_SCRIPT_FAMILY[preset.language];
|
|
179
|
+
if (voiceFamily === undefined) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
// Условие (i)+(ii) выполнены: причина — явно выбранный пресет, а суждение
|
|
183
|
+
// опирается на присутствующий лейбл против ДЕТЕКТИРОВАННОГО семейства скрипта.
|
|
184
|
+
// Нейтральный/неоднозначный скрипт не противоречит ничему → молчим (ii).
|
|
185
|
+
const scriptFamily = detectScriptFamily(input.script);
|
|
186
|
+
if (scriptFamily === "neutral" || scriptFamily === voiceFamily) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
`voice "${input.voice}" is labeled ${preset.language}, but the script looks ${scriptFamily}; it will be voiced in a ${preset.language} voice`,
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=voices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voices.js","sourceRoot":"","sources":["../src/voices.ts"],"names":[],"mappings":"AA4BA;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,cAAc,EAAE;QACd,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,8EAA8E;KAC5F;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,QAAQ;QAChB,WAAW,EAAE,sCAAsC;KACpD;IACD,MAAM,EAAE;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,MAAM;QACd,WAAW,EAAE,sDAAsD;KACpE;IACD,IAAI,EAAE;QACJ,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,MAAM;QACd,WAAW,EAAE,gDAAgD;KAC9D;IACD,eAAe,EAAE;QACf,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,QAAQ;QAChB,WAAW,EAAE,2CAA2C;KACzD;CAC6C,CAAC;AAEjD;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAG1D,CAAC;AAIF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAiB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAYvD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAGrC;IACC,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAE9C,MAAM,UAAU,kBAAkB,CAChC,MAAc;IAEd,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC;YAC3B,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;aAAM,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC;YAChE,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC/B,gFAAgF;IAChF,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,QAAQ,GAAG,KAAK,IAAI,0BAA0B,EAAE,CAAC;QACnD,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,IAAI,KAAK,GAAG,KAAK,IAAI,0BAA0B,EAAE,CAAC;QAChD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,yBAAyB,GAAyC;IACtE,EAAE,EAAE,UAAU;IACd,EAAE,EAAE,OAAO;CACZ,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAC1C,+EAA+E,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAOlC;IACC,+EAA+E;IAC/E,2EAA2E;IAC3E,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,CAAC,+BAA+B,CAAC,CAAC;IAC3C,CAAC;IAED,6EAA6E;IAC7E,4EAA4E;IAC5E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,8EAA8E;IAC9E,iFAAiF;IACjF,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,MAAM,WAAW,GAAG,yBAAyB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,0EAA0E;IAC1E,+EAA+E;IAC/E,yEAAyE;IACzE,MAAM,YAAY,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,WAAW,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO;QACL,UAAU,KAAK,CAAC,KAAK,gBAAgB,MAAM,CAAC,QAAQ,0BAA0B,YAAY,4BAA4B,MAAM,CAAC,QAAQ,QAAQ;KAC9I,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clipwright/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zod schemas and the shared request/response contract of the Clipwright UGC video API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Dimantika LLC",
|
|
7
|
+
"homepage": "https://clipwright.io",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./idempotency": {
|
|
24
|
+
"types": "./dist/idempotency.d.ts",
|
|
25
|
+
"default": "./dist/idempotency.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"zod": "^4.4.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^26.1.1"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
|
|
37
|
+
}
|
|
38
|
+
}
|