@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dimantika LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @clipwright/core
|
|
2
|
+
|
|
3
|
+
Zod schemas and the shared request/response contract of the
|
|
4
|
+
[Clipwright](https://clipwright.io) UGC video API. Published because the SDK and
|
|
5
|
+
the MCP server depend on it; the API validates against these same schemas, so
|
|
6
|
+
they are the contract rather than a copy of it.
|
|
7
|
+
|
|
8
|
+
What is here: the `make_ugc` input shape, the run object, aspect-ratio and
|
|
9
|
+
resolution resolution rules, speech-pacing rules, the voice catalog, and the field
|
|
10
|
+
disposition registry — the table that records, per input field, whether it is
|
|
11
|
+
honored, rejected, or accepted-with-a-warning.
|
|
12
|
+
|
|
13
|
+
The `./idempotency` subpath export is separate because it pulls in a Node
|
|
14
|
+
built-in and would otherwise break bundlers targeting the browser.
|
package/dist/aspect.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type AspectRatio, type Resolution } from "./skills.js";
|
|
2
|
+
/**
|
|
3
|
+
* РАЗРЕШЕНИЕ ФОРМАТА ВЫХОДА ПО ЗАПРОСУ И ИСТОЧНИКУ.
|
|
4
|
+
*
|
|
5
|
+
* Решение владельца: МЫ НЕ КРОПАЕМ. Формат берётся от источника, а расхождение
|
|
6
|
+
* с запрошенным не подменяется молча — оно либо объявляется предупреждением,
|
|
7
|
+
* либо отклоняется до платного вызова (правило проекта №3, переписанное
|
|
8
|
+
* правило №4).
|
|
9
|
+
*
|
|
10
|
+
* Чистые функции, ноль сети: проба источника живёт в воркере, сюда приходит
|
|
11
|
+
* уже измеренная пара сторон.
|
|
12
|
+
*/
|
|
13
|
+
/** Числовое отношение ширины к высоте для каждого поддерживаемого формата. */
|
|
14
|
+
export declare const ASPECT_RATIO_VALUES: Record<AspectRatio, number>;
|
|
15
|
+
/**
|
|
16
|
+
* Формат по умолчанию. Отдельная КОНСТАНТА, а не `.default()` в схеме: различить
|
|
17
|
+
* «клиент попросил 9:16» и «клиент промолчал» можно только так, а на этом
|
|
18
|
+
* различии стоит вся двухслойная развилка (промолчал → снап с предупреждением;
|
|
19
|
+
* попросил явно → возможен fail-closed).
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_ASPECT_RATIO: AspectRatio;
|
|
22
|
+
/**
|
|
23
|
+
* Источник кадра: измеренные стороны плюс происхождение.
|
|
24
|
+
*
|
|
25
|
+
* `origin` нужен ради текста отказа. Отказ «формат не сходится с источником»
|
|
26
|
+
* бесполезен, если вызыватель не знает, какой источник имеется в виду: он ведь
|
|
27
|
+
* никакой картинки не передавал. Поэтому дефолтный актёр — не «отсутствие
|
|
28
|
+
* источника», а ИЗВЕСТНЫЙ источник, который умеет себя назвать.
|
|
29
|
+
*/
|
|
30
|
+
export interface AspectSource {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
origin: "default" | "probed";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Дефолтный актёр как известный источник — 432×768 (ровно 9:16).
|
|
37
|
+
*
|
|
38
|
+
* Число зеркалит портрет, загруженный в HeyGen (US-509, центр-кроп 9:16 →
|
|
39
|
+
* 432×768). Вендорский ИДЕНТИФИКАТОР портрета сюда не переезжает и остаётся
|
|
40
|
+
* запертым в адаптере; сюда переезжает только продуктовый факт «дефолтный актёр
|
|
41
|
+
* вертикальный», без которого резолвер не смог бы объяснить свой отказ.
|
|
42
|
+
* Живьём это подтверждается на платном гейте US-530.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DEFAULT_ACTOR_SOURCE: AspectSource;
|
|
45
|
+
/** ≤2% — считаем совпадением: разница не видна и кропа не требует. */
|
|
46
|
+
export declare const ASPECT_MATCH_THRESHOLD = 0.02;
|
|
47
|
+
/** ≤15% — снап к формату с предупреждением о центр-кропе вендора. */
|
|
48
|
+
export declare const ASPECT_SNAP_THRESHOLD = 0.15;
|
|
49
|
+
/**
|
|
50
|
+
* Относительное расхождение двух отношений сторон.
|
|
51
|
+
*
|
|
52
|
+
* Делим на МЕНЬШЕЕ из двух намеренно: из двух возможных относительных ошибок
|
|
53
|
+
* берётся бо́льшая, то есть порог срабатывает раньше. Для гарда, который решает,
|
|
54
|
+
* тратить ли деньги, ошибаться следует в сторону «спросить», а не «сделать».
|
|
55
|
+
*/
|
|
56
|
+
export declare function aspectDistance(a: number, b: number): number;
|
|
57
|
+
/** Ближайший поддерживаемый формат к произвольному отношению сторон. */
|
|
58
|
+
export declare function snapAspect(ratio: number): AspectRatio;
|
|
59
|
+
/** Вердикт резолвера: либо формат с объяснениями, либо отказ до платного шага. */
|
|
60
|
+
export type AspectResolution = {
|
|
61
|
+
kind: "resolved";
|
|
62
|
+
aspectRatio: AspectRatio;
|
|
63
|
+
warnings: string[];
|
|
64
|
+
} | {
|
|
65
|
+
kind: "rejected";
|
|
66
|
+
message: string;
|
|
67
|
+
};
|
|
68
|
+
export interface ResolveAspectInput {
|
|
69
|
+
/** Что попросил клиент. `undefined` — промолчал. */
|
|
70
|
+
requested: AspectRatio | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Измеренный источник. `null` ТОЛЬКО в одном случае: URL передан, но пробу
|
|
73
|
+
* сделать не удалось. Отсутствие `image` источником не является — там стоит
|
|
74
|
+
* `DEFAULT_ACTOR_SOURCE`.
|
|
75
|
+
*/
|
|
76
|
+
source: AspectSource | null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Двухслойный ответ на вопрос «какой формат рендерить» (развилка C3).
|
|
80
|
+
*
|
|
81
|
+
* МОЛЧАЩИЙ КЛИЕНТ НИКОГДА НЕ ПОЛУЧАЕТ ОТКАЗ. Он не делал утверждения, которое
|
|
82
|
+
* можно нарушить, — формат выводится от источника, и любое расхождение
|
|
83
|
+
* объявляется предупреждением. Fail-closed возможен ровно при ЯВНОМ конфликте:
|
|
84
|
+
* клиент назвал формат, а источник от него дальше 15%. Иначе гард превратился бы
|
|
85
|
+
* в стену отказов на выборке из одного-двух пользователей (PM2).
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveAspectRatio(input: ResolveAspectInput): AspectResolution;
|
|
88
|
+
/**
|
|
89
|
+
* Точная метка отношения сторон источника — несокращённая дробь, сокращённая
|
|
90
|
+
* НОД'ом: 432×768 → `9:16`, 1920×1080 → `16:9`.
|
|
91
|
+
*
|
|
92
|
+
* ЗАЧЕМ отдельно от `snapAspect`: снап отвечает на вопрос «что мы отрендерим»
|
|
93
|
+
* и обязан вернуть одно из поддерживаемых значений, а эта функция отвечает на
|
|
94
|
+
* вопрос «что вы прислали». Подставь мы сюда снап, источник 16:9 отчитался бы
|
|
95
|
+
* как `1:1` — то есть quote соврал бы про вход агента ровно в том месте, где он
|
|
96
|
+
* должен помочь агенту понять расхождение.
|
|
97
|
+
*/
|
|
98
|
+
export declare function exactAspectLabel(width: number, height: number): string;
|
|
99
|
+
/**
|
|
100
|
+
* Формат, который просим У ВЕНДОРА, — ближайший к ИСТОЧНИКУ, а не запрошенный
|
|
101
|
+
* клиентом (US-527).
|
|
102
|
+
*
|
|
103
|
+
* ЗАЧЕМ ТАК. Живое измерение US-530 показало: вендор портит кадр ровно тогда,
|
|
104
|
+
* когда просимый формат расходится с источником, — в одну сторону режет бока,
|
|
105
|
+
* в другую добивает белым. Значит единственная точка, где кадр не страдает, —
|
|
106
|
+
* совпадение с источником. Просим её, а нужный клиенту кадр собираем сами:
|
|
107
|
+
* укладка на нашей стороне объявляется в `warnings[]`, вендорская — нет.
|
|
108
|
+
*
|
|
109
|
+
* ПОЧЕМУ НЕ «ВСЕГДА КВАДРАТ». Соблазн просить `1:1` всегда (он единственный,
|
|
110
|
+
* где измерение дало нетронутый кадр) неверен: квадрат был нетронут ДЛЯ
|
|
111
|
+
* КВАДРАТНОГО источника. Дефолтный актёр — 432×768, и запрос `1:1` срезал бы
|
|
112
|
+
* ему бока на каждом дефолтном ране. Свойство принадлежит совпадению, а не
|
|
113
|
+
* квадрату.
|
|
114
|
+
*/
|
|
115
|
+
export declare function vendorAspectFor(source: AspectSource, supported: readonly AspectRatio[]): AspectRatio;
|
|
116
|
+
/** Что мы предсказали и что вендор отдал на самом деле. */
|
|
117
|
+
export interface DeliveredClip {
|
|
118
|
+
width: number;
|
|
119
|
+
height: number;
|
|
120
|
+
durationSec: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Сверка отданного вендором с предсказанным (US-527).
|
|
124
|
+
*
|
|
125
|
+
* ЗАЧЕМ. Все наши правила о поведении вендора — пороги, тексты отказов, выбор
|
|
126
|
+
* формата — выведены из ОДНОГО дня измерений на ОДНОМ квадратном источнике
|
|
127
|
+
* (US-530). Это снимок чужого эндпоинта: он может поменять поля, частоту
|
|
128
|
+
* кадров или добивку длительности молча, и каждое выведенное правило продолжит
|
|
129
|
+
* утверждать вчерашнее с полной уверенностью. Сверка превращает каждый
|
|
130
|
+
* оплаченный ран в бесплатный перезамер — и ловит расхождение в тот день,
|
|
131
|
+
* когда оно появилось, а не в тот, когда на него пожаловался клиент.
|
|
132
|
+
*
|
|
133
|
+
* МОЛЧАНИЕ ПРИ СОВПАДЕНИИ ОБЯЗАТЕЛЬНО. Предупреждение на каждом ране
|
|
134
|
+
* обесценивает предупреждения вообще.
|
|
135
|
+
*/
|
|
136
|
+
export declare function deliveryWarnings(args: {
|
|
137
|
+
predicted: DeliveredClip;
|
|
138
|
+
actual: DeliveredClip;
|
|
139
|
+
/** Допуск по длительности: вендор округляет, и доли секунды — не расхождение. */
|
|
140
|
+
durationToleranceSec?: number;
|
|
141
|
+
}): string[];
|
|
142
|
+
/**
|
|
143
|
+
* Кадр, который МЫ ожидаем от вендора: формат ПЛЮС разрешение.
|
|
144
|
+
*
|
|
145
|
+
* Первая редакция сверки предсказывала кадр только по формату и потому
|
|
146
|
+
* объявляла расхождением каждый ран в 720p — то есть каждый dev-ран, где
|
|
147
|
+
* дефолтное разрешение понижено. Предупреждение, срабатывающее там, где всё
|
|
148
|
+
* сошлось, обесценивает предупреждения вообще; проверка это поймала.
|
|
149
|
+
*/
|
|
150
|
+
export declare function expectedVendorFrame(resolution: Resolution, aspectRatio: AspectRatio): {
|
|
151
|
+
width: number;
|
|
152
|
+
height: number;
|
|
153
|
+
};
|
package/dist/aspect.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { ASPECT_RATIOS } from "./skills.js";
|
|
2
|
+
/**
|
|
3
|
+
* РАЗРЕШЕНИЕ ФОРМАТА ВЫХОДА ПО ЗАПРОСУ И ИСТОЧНИКУ.
|
|
4
|
+
*
|
|
5
|
+
* Решение владельца: МЫ НЕ КРОПАЕМ. Формат берётся от источника, а расхождение
|
|
6
|
+
* с запрошенным не подменяется молча — оно либо объявляется предупреждением,
|
|
7
|
+
* либо отклоняется до платного вызова (правило проекта №3, переписанное
|
|
8
|
+
* правило №4).
|
|
9
|
+
*
|
|
10
|
+
* Чистые функции, ноль сети: проба источника живёт в воркере, сюда приходит
|
|
11
|
+
* уже измеренная пара сторон.
|
|
12
|
+
*/
|
|
13
|
+
/** Числовое отношение ширины к высоте для каждого поддерживаемого формата. */
|
|
14
|
+
export const ASPECT_RATIO_VALUES = {
|
|
15
|
+
"9:16": 9 / 16,
|
|
16
|
+
"1:1": 1,
|
|
17
|
+
"16:9": 16 / 9,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Формат по умолчанию. Отдельная КОНСТАНТА, а не `.default()` в схеме: различить
|
|
21
|
+
* «клиент попросил 9:16» и «клиент промолчал» можно только так, а на этом
|
|
22
|
+
* различии стоит вся двухслойная развилка (промолчал → снап с предупреждением;
|
|
23
|
+
* попросил явно → возможен fail-closed).
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_ASPECT_RATIO = "9:16";
|
|
26
|
+
/**
|
|
27
|
+
* Дефолтный актёр как известный источник — 432×768 (ровно 9:16).
|
|
28
|
+
*
|
|
29
|
+
* Число зеркалит портрет, загруженный в HeyGen (US-509, центр-кроп 9:16 →
|
|
30
|
+
* 432×768). Вендорский ИДЕНТИФИКАТОР портрета сюда не переезжает и остаётся
|
|
31
|
+
* запертым в адаптере; сюда переезжает только продуктовый факт «дефолтный актёр
|
|
32
|
+
* вертикальный», без которого резолвер не смог бы объяснить свой отказ.
|
|
33
|
+
* Живьём это подтверждается на платном гейте US-530.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_ACTOR_SOURCE = {
|
|
36
|
+
width: 432,
|
|
37
|
+
height: 768,
|
|
38
|
+
origin: "default",
|
|
39
|
+
};
|
|
40
|
+
/** ≤2% — считаем совпадением: разница не видна и кропа не требует. */
|
|
41
|
+
export const ASPECT_MATCH_THRESHOLD = 0.02;
|
|
42
|
+
/** ≤15% — снап к формату с предупреждением о центр-кропе вендора. */
|
|
43
|
+
export const ASPECT_SNAP_THRESHOLD = 0.15;
|
|
44
|
+
/**
|
|
45
|
+
* Относительное расхождение двух отношений сторон.
|
|
46
|
+
*
|
|
47
|
+
* Делим на МЕНЬШЕЕ из двух намеренно: из двух возможных относительных ошибок
|
|
48
|
+
* берётся бо́льшая, то есть порог срабатывает раньше. Для гарда, который решает,
|
|
49
|
+
* тратить ли деньги, ошибаться следует в сторону «спросить», а не «сделать».
|
|
50
|
+
*/
|
|
51
|
+
export function aspectDistance(a, b) {
|
|
52
|
+
return Math.abs(a - b) / Math.min(a, b);
|
|
53
|
+
}
|
|
54
|
+
/** Ближайший поддерживаемый формат к произвольному отношению сторон. */
|
|
55
|
+
export function snapAspect(ratio) {
|
|
56
|
+
let best = ASPECT_RATIOS[0];
|
|
57
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
58
|
+
for (const candidate of ASPECT_RATIOS) {
|
|
59
|
+
const distance = aspectDistance(ASPECT_RATIO_VALUES[candidate], ratio);
|
|
60
|
+
if (distance < bestDistance) {
|
|
61
|
+
bestDistance = distance;
|
|
62
|
+
best = candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return best;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Двухслойный ответ на вопрос «какой формат рендерить» (развилка C3).
|
|
69
|
+
*
|
|
70
|
+
* МОЛЧАЩИЙ КЛИЕНТ НИКОГДА НЕ ПОЛУЧАЕТ ОТКАЗ. Он не делал утверждения, которое
|
|
71
|
+
* можно нарушить, — формат выводится от источника, и любое расхождение
|
|
72
|
+
* объявляется предупреждением. Fail-closed возможен ровно при ЯВНОМ конфликте:
|
|
73
|
+
* клиент назвал формат, а источник от него дальше 15%. Иначе гард превратился бы
|
|
74
|
+
* в стену отказов на выборке из одного-двух пользователей (PM2).
|
|
75
|
+
*/
|
|
76
|
+
export function resolveAspectRatio(input) {
|
|
77
|
+
const { requested, source } = input;
|
|
78
|
+
if (source === null) {
|
|
79
|
+
// Проба чужого URL не удалась. Отказывать нельзя — недоступность источника
|
|
80
|
+
// не является утверждением клиента о формате; отложенная проверка
|
|
81
|
+
// объявляется предупреждением по образцу RAW_VOICE_ID_VALIDATION_WARNING.
|
|
82
|
+
return {
|
|
83
|
+
kind: "resolved",
|
|
84
|
+
aspectRatio: requested ?? DEFAULT_ASPECT_RATIO,
|
|
85
|
+
warnings: [
|
|
86
|
+
"source dimensions could not be probed; the format was not verified against the image",
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const sourceRatio = source.width / source.height;
|
|
91
|
+
const target = requested ?? snapAspect(sourceRatio);
|
|
92
|
+
const distance = aspectDistance(ASPECT_RATIO_VALUES[target], sourceRatio);
|
|
93
|
+
if (distance <= ASPECT_MATCH_THRESHOLD) {
|
|
94
|
+
return { kind: "resolved", aspectRatio: target, warnings: [] };
|
|
95
|
+
}
|
|
96
|
+
// ЧТО ИМЕННО СДЕЛАЕТ ВЕНДОР — ЗАВИСИТ ОТ НАПРАВЛЕНИЯ, И ЭТО ИЗМЕРЕНО.
|
|
97
|
+
//
|
|
98
|
+
// Прежний текст обещал центр-кроп всегда. Живое измерение US-530 (2026-07-22,
|
|
99
|
+
// квадратный источник, четыре формата) показало два разных поведения:
|
|
100
|
+
// кадр ШИРЕ источника — вендор дорисовывает БЕЛЫМ и картинку не трогает
|
|
101
|
+
// (16:9 из квадрата: полосы по 420 px, контент 56% кадра); кадр УЖЕ источника
|
|
102
|
+
// — увеличивает и срезает бока (4:5: ушло ~20% ширины).
|
|
103
|
+
//
|
|
104
|
+
// Предупреждение, называющее не то, что произойдёт, хуже отсутствия
|
|
105
|
+
// предупреждения: клиент готовится к обрезке краёв, а получает белые поля.
|
|
106
|
+
const targetRatio = ASPECT_RATIO_VALUES[target];
|
|
107
|
+
const snapWarning = targetRatio > sourceRatio
|
|
108
|
+
? `source is ${source.width}×${source.height}; rendering ${target} is wider than the source, ` +
|
|
109
|
+
"so the vendor pads the frame with white bars — we do not crop the source ourselves"
|
|
110
|
+
: `source is ${source.width}×${source.height}; rendering ${target} is taller than the source, ` +
|
|
111
|
+
"so the vendor center-crops the sides — we do not crop it ourselves";
|
|
112
|
+
if (distance <= ASPECT_SNAP_THRESHOLD || requested === undefined) {
|
|
113
|
+
return { kind: "resolved", aspectRatio: target, warnings: [snapWarning] };
|
|
114
|
+
}
|
|
115
|
+
// Явный конфликт. Текст обязан НАЗВАТЬ лекарство: отказ, из которого не
|
|
116
|
+
// видно, что делать дальше, для автономного агента эквивалентен поломке.
|
|
117
|
+
const cure = source.origin === "default"
|
|
118
|
+
? `the default actor is ${snapAspect(sourceRatio)} (${source.width}×${source.height}); ` +
|
|
119
|
+
`pass \`image\` with a source close to ${target} to render ${target}`
|
|
120
|
+
: `the source is ${source.width}×${source.height}; pass an \`image\` closer to ${target}, ` +
|
|
121
|
+
`or request ${snapAspect(sourceRatio)} instead`;
|
|
122
|
+
return {
|
|
123
|
+
kind: "rejected",
|
|
124
|
+
message: `requested aspect_ratio ${target} conflicts with the source by ${Math.round(distance * 100)}%: ${cure}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Точная метка отношения сторон источника — несокращённая дробь, сокращённая
|
|
129
|
+
* НОД'ом: 432×768 → `9:16`, 1920×1080 → `16:9`.
|
|
130
|
+
*
|
|
131
|
+
* ЗАЧЕМ отдельно от `snapAspect`: снап отвечает на вопрос «что мы отрендерим»
|
|
132
|
+
* и обязан вернуть одно из поддерживаемых значений, а эта функция отвечает на
|
|
133
|
+
* вопрос «что вы прислали». Подставь мы сюда снап, источник 16:9 отчитался бы
|
|
134
|
+
* как `1:1` — то есть quote соврал бы про вход агента ровно в том месте, где он
|
|
135
|
+
* должен помочь агенту понять расхождение.
|
|
136
|
+
*/
|
|
137
|
+
export function exactAspectLabel(width, height) {
|
|
138
|
+
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
|
|
139
|
+
const w = Math.round(width);
|
|
140
|
+
const h = Math.round(height);
|
|
141
|
+
if (w <= 0 || h <= 0)
|
|
142
|
+
return `${w}:${h}`;
|
|
143
|
+
const divisor = gcd(w, h);
|
|
144
|
+
return `${w / divisor}:${h / divisor}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Формат, который просим У ВЕНДОРА, — ближайший к ИСТОЧНИКУ, а не запрошенный
|
|
148
|
+
* клиентом (US-527).
|
|
149
|
+
*
|
|
150
|
+
* ЗАЧЕМ ТАК. Живое измерение US-530 показало: вендор портит кадр ровно тогда,
|
|
151
|
+
* когда просимый формат расходится с источником, — в одну сторону режет бока,
|
|
152
|
+
* в другую добивает белым. Значит единственная точка, где кадр не страдает, —
|
|
153
|
+
* совпадение с источником. Просим её, а нужный клиенту кадр собираем сами:
|
|
154
|
+
* укладка на нашей стороне объявляется в `warnings[]`, вендорская — нет.
|
|
155
|
+
*
|
|
156
|
+
* ПОЧЕМУ НЕ «ВСЕГДА КВАДРАТ». Соблазн просить `1:1` всегда (он единственный,
|
|
157
|
+
* где измерение дало нетронутый кадр) неверен: квадрат был нетронут ДЛЯ
|
|
158
|
+
* КВАДРАТНОГО источника. Дефолтный актёр — 432×768, и запрос `1:1` срезал бы
|
|
159
|
+
* ему бока на каждом дефолтном ране. Свойство принадлежит совпадению, а не
|
|
160
|
+
* квадрату.
|
|
161
|
+
*/
|
|
162
|
+
export function vendorAspectFor(source, supported) {
|
|
163
|
+
if (supported.length === 0) {
|
|
164
|
+
throw new Error("vendorAspectFor: backend declares no supported aspect ratios");
|
|
165
|
+
}
|
|
166
|
+
const sourceRatio = source.width / source.height;
|
|
167
|
+
let best = supported[0];
|
|
168
|
+
let bestDistance = aspectDistance(ASPECT_RATIO_VALUES[best], sourceRatio);
|
|
169
|
+
for (const candidate of supported) {
|
|
170
|
+
const distance = aspectDistance(ASPECT_RATIO_VALUES[candidate], sourceRatio);
|
|
171
|
+
if (distance < bestDistance) {
|
|
172
|
+
best = candidate;
|
|
173
|
+
bestDistance = distance;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return best;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Сверка отданного вендором с предсказанным (US-527).
|
|
180
|
+
*
|
|
181
|
+
* ЗАЧЕМ. Все наши правила о поведении вендора — пороги, тексты отказов, выбор
|
|
182
|
+
* формата — выведены из ОДНОГО дня измерений на ОДНОМ квадратном источнике
|
|
183
|
+
* (US-530). Это снимок чужого эндпоинта: он может поменять поля, частоту
|
|
184
|
+
* кадров или добивку длительности молча, и каждое выведенное правило продолжит
|
|
185
|
+
* утверждать вчерашнее с полной уверенностью. Сверка превращает каждый
|
|
186
|
+
* оплаченный ран в бесплатный перезамер — и ловит расхождение в тот день,
|
|
187
|
+
* когда оно появилось, а не в тот, когда на него пожаловался клиент.
|
|
188
|
+
*
|
|
189
|
+
* МОЛЧАНИЕ ПРИ СОВПАДЕНИИ ОБЯЗАТЕЛЬНО. Предупреждение на каждом ране
|
|
190
|
+
* обесценивает предупреждения вообще.
|
|
191
|
+
*/
|
|
192
|
+
export function deliveryWarnings(args) {
|
|
193
|
+
const { predicted, actual } = args;
|
|
194
|
+
const tolerance = args.durationToleranceSec ?? 0.05;
|
|
195
|
+
const warnings = [];
|
|
196
|
+
if (predicted.width !== actual.width || predicted.height !== actual.height) {
|
|
197
|
+
warnings.push(`vendor returned ${actual.width}×${actual.height} where ${predicted.width}×${predicted.height} was expected`);
|
|
198
|
+
}
|
|
199
|
+
const durationDelta = actual.durationSec - predicted.durationSec;
|
|
200
|
+
if (Math.abs(durationDelta) > tolerance) {
|
|
201
|
+
// Добивка до круглой секунды — ОТДЕЛЬНЫЙ, УЗНАВАЕМЫЙ случай, измеренный в
|
|
202
|
+
// US-530: на 0.6-секундном аудио вендор отдал ровно 1.0 с. Называть её
|
|
203
|
+
// общим «длительность разошлась» значило бы прятать известную причину
|
|
204
|
+
// среди неизвестных.
|
|
205
|
+
const paddedToWholeSecond = durationDelta > 0 && Number.isInteger(actual.durationSec) && actual.durationSec <= 1;
|
|
206
|
+
warnings.push(paddedToWholeSecond
|
|
207
|
+
? `vendor padded the clip to ${actual.durationSec}s from ${predicted.durationSec}s of audio`
|
|
208
|
+
: `vendor returned ${actual.durationSec}s where ${predicted.durationSec}s was expected`);
|
|
209
|
+
}
|
|
210
|
+
return warnings;
|
|
211
|
+
}
|
|
212
|
+
/** Короткая сторона кадра по разрешению — измерено живьём (docs/11 «шаг 0»). */
|
|
213
|
+
const SHORT_SIDE_BY_RESOLUTION = {
|
|
214
|
+
"720p": 720,
|
|
215
|
+
"1080p": 1080,
|
|
216
|
+
"4k": 2160,
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Кадр, который МЫ ожидаем от вендора: формат ПЛЮС разрешение.
|
|
220
|
+
*
|
|
221
|
+
* Первая редакция сверки предсказывала кадр только по формату и потому
|
|
222
|
+
* объявляла расхождением каждый ран в 720p — то есть каждый dev-ран, где
|
|
223
|
+
* дефолтное разрешение понижено. Предупреждение, срабатывающее там, где всё
|
|
224
|
+
* сошлось, обесценивает предупреждения вообще; проверка это поймала.
|
|
225
|
+
*/
|
|
226
|
+
export function expectedVendorFrame(resolution, aspectRatio) {
|
|
227
|
+
const short = SHORT_SIDE_BY_RESOLUTION[resolution];
|
|
228
|
+
const ratio = ASPECT_RATIO_VALUES[aspectRatio];
|
|
229
|
+
const even = (value) => {
|
|
230
|
+
const rounded = Math.round(value);
|
|
231
|
+
return rounded % 2 === 0 ? rounded : rounded + 1;
|
|
232
|
+
};
|
|
233
|
+
return ratio >= 1
|
|
234
|
+
? { width: even(short * ratio), height: even(short) }
|
|
235
|
+
: { width: even(short), height: even(short / ratio) };
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=aspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aspect.js","sourceRoot":"","sources":["../src/aspect.ts"],"names":[],"mappings":"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"}
|