@nodaro/shared 3.10.0 → 3.11.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/dist/index.cjs +195 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -6
- package/dist/index.d.ts +224 -6
- package/dist/index.js +181 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/parameter-node-value.test.ts +13 -1
- package/src/__tests__/video-analysis.test.ts +15 -0
- package/src/__tests__/video-frame-fit.test.ts +189 -0
- package/src/catalog-projection.ts +3 -0
- package/src/character-motion-metadata.ts +19 -0
- package/src/i18n/character-motion.ar.ts +126 -75
- package/src/i18n/character-motion.de.ts +126 -75
- package/src/i18n/character-motion.es.ts +126 -75
- package/src/i18n/character-motion.fr.ts +126 -75
- package/src/i18n/character-motion.he.ts +126 -75
- package/src/i18n/character-motion.hi.ts +126 -75
- package/src/i18n/character-motion.ja.ts +126 -75
- package/src/i18n/character-motion.ko.ts +126 -75
- package/src/i18n/character-motion.pt-BR.ts +126 -75
- package/src/i18n/character-motion.ru.ts +126 -75
- package/src/i18n/character-motion.zh-CN.ts +126 -75
- package/src/index.ts +5 -0
- package/src/parameter-node-value.ts +31 -5
- package/src/video-analysis.ts +15 -0
- package/src/video-frame-fit.ts +228 -0
- package/src/video-output-canvas.ts +119 -0
package/src/video-analysis.ts
CHANGED
|
@@ -282,8 +282,23 @@ export const entitySlotSchema = z.object({
|
|
|
282
282
|
refRejectedReason: z.string().optional(),
|
|
283
283
|
/** NON-default looks only; present only when at least one exists. */
|
|
284
284
|
variations: z.array(slotVariationSchema).max(VIDEO_ANALYSIS_MAX_VARIATIONS).optional(),
|
|
285
|
+
/**
|
|
286
|
+
* WHOSE THIS OBJECT IS (2026-09-17). An object slot that is worn, held,
|
|
287
|
+
* carried, driven or ridden by a cast person or creature names that slot
|
|
288
|
+
* here, with the relation as a short passive phrase ending in "by" ("worn
|
|
289
|
+
* by", "held by", "driven by"). The link says the object slot IS the one on
|
|
290
|
+
* that person — not a second one: a recast that saw "Man in Blue Silk
|
|
291
|
+
* Shirt" AND "Blue Silk Shirt" as unrelated slots rendered two shirts.
|
|
292
|
+
* Object slots only; a free-standing prop, the product on a table, a place
|
|
293
|
+
* or a person never carries it. Optional/additive: producers may omit it.
|
|
294
|
+
*/
|
|
295
|
+
owner: z.object({
|
|
296
|
+
slotId: z.string().min(1).regex(/^[a-z0-9-]+$/),
|
|
297
|
+
relation: z.string().min(1),
|
|
298
|
+
}).optional(),
|
|
285
299
|
})
|
|
286
300
|
export type EntitySlot = z.infer<typeof entitySlotSchema>
|
|
301
|
+
export type EntitySlotOwner = NonNullable<EntitySlot["owner"]>
|
|
287
302
|
|
|
288
303
|
/**
|
|
289
304
|
* The sound LAYER vocabulary — what kind of thing this layer is.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* START/END FRAME FIT — the pure geometry half.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (measured on ~40 production renders, 2026-09-16):
|
|
5
|
+
* a start frame whose pixel size is not the model's own output canvas gets
|
|
6
|
+
* reshaped by the provider, and Seedance 2.5 does it VISIBLY — frame 0 is the
|
|
7
|
+
* user's image verbatim, then from frame 1 the generated frames are rescaled
|
|
8
|
+
* ~2% on ONE axis (x1.000 y1.02). Five of ten runs with a 940x1672 image
|
|
9
|
+
* snapped; three of three with the same image resized to 720x1280 did not.
|
|
10
|
+
*
|
|
11
|
+
* So: reshape the frame ourselves, to the size the model was going to render
|
|
12
|
+
* anyway. The canvas comes from measurement (`video-output-canvas.ts`), never
|
|
13
|
+
* from arithmetic, and when a combination has never been measured the fit
|
|
14
|
+
* DEGRADES rather than guesses.
|
|
15
|
+
*
|
|
16
|
+
* This module is pure — no I/O, no sharp, no network. `prepareVideoFrames` in
|
|
17
|
+
* the backend does the pixels and the upload.
|
|
18
|
+
*/
|
|
19
|
+
import { getModel } from "./model-catalog.js"
|
|
20
|
+
import { resolveOutputCanvas, type VideoOutputCanvas } from "./video-output-canvas.js"
|
|
21
|
+
|
|
22
|
+
/** How much of the frame we are allowed to reshape. */
|
|
23
|
+
export const FRAME_FITS = ["original", "ratio", "resolution"] as const
|
|
24
|
+
export type FrameFit = (typeof FRAME_FITS)[number]
|
|
25
|
+
|
|
26
|
+
/** `resolution` — the measured canvas — is the default everywhere. */
|
|
27
|
+
export const DEFAULT_FRAME_FIT: FrameFit = "resolution"
|
|
28
|
+
|
|
29
|
+
/** How the frame is handed to the model. */
|
|
30
|
+
export const FRAME_DELIVERIES = ["auto", "frame", "reference"] as const
|
|
31
|
+
export type FrameDelivery = (typeof FRAME_DELIVERIES)[number]
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_FRAME_DELIVERY: FrameDelivery = "auto"
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stretching an image to a ratio it is nowhere near would squash the subject,
|
|
37
|
+
* so past this gap the frame is centre-cropped to the target ratio FIRST and
|
|
38
|
+
* only then resized. 5% covers every "1K image into a 720p canvas" case we
|
|
39
|
+
* measured (940x1672 into 9:16 is a 0.05% gap) while refusing to squash a
|
|
40
|
+
* square photo into 9:16 (a 78% gap).
|
|
41
|
+
*/
|
|
42
|
+
export const FRAME_FIT_STRETCH_TOLERANCE = 0.05
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Models whose frame mode is measurably worse than reference delivery.
|
|
46
|
+
*
|
|
47
|
+
* The Seedance 2.0 family crop-zooms the frame 2% and drifts 11-26% darker
|
|
48
|
+
* within six frames in frame mode; delivered as a reference with the opening-
|
|
49
|
+
* frame sentence, the same models hold a flat look from frame 0 and keep the
|
|
50
|
+
* image's true geometry. Every OTHER model measured (Seedance 2.5, Gemini Omni
|
|
51
|
+
* video + flash, Veo 3.1, Wan 3.0, Minimax H3) reproduces the opening frame
|
|
52
|
+
* better in frame mode, so the default stays `frame` for anything absent here.
|
|
53
|
+
*/
|
|
54
|
+
export const FRAME_DELIVERY_BY_PROVIDER: Readonly<Record<string, Exclude<FrameDelivery, "auto">>> = {
|
|
55
|
+
"seedance-2": "reference",
|
|
56
|
+
"seedance-2-fast": "reference",
|
|
57
|
+
"seedance-2-mini": "reference",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Aspect tokens that name no concrete shape — the model picks. */
|
|
61
|
+
const OPEN_ASPECT_TOKENS = new Set(["adaptive", "auto"])
|
|
62
|
+
|
|
63
|
+
/** `"16:9"` → 1.777…; anything unparseable → `undefined`. */
|
|
64
|
+
export function parseAspectToken(token: string | undefined): number | undefined {
|
|
65
|
+
if (!token) return undefined
|
|
66
|
+
const m = /^(\d+(?:\.\d+)?)\s*[:x/]\s*(\d+(?:\.\d+)?)$/.exec(token.trim())
|
|
67
|
+
if (!m) return undefined
|
|
68
|
+
const w = Number(m[1]); const h = Number(m[2])
|
|
69
|
+
if (!(w > 0) || !(h > 0)) return undefined
|
|
70
|
+
return w / h
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The aspect the fit should target.
|
|
75
|
+
*
|
|
76
|
+
* An explicit ratio is used as-is. `adaptive` / `Auto` (and an absent value,
|
|
77
|
+
* which the seedance family sends as adaptive whenever a frame is present) has
|
|
78
|
+
* no shape of its own: the provider will follow the IMAGE, so we snap the
|
|
79
|
+
* image's own ratio to the nearest one the model lists and target that. A model
|
|
80
|
+
* with no declared ratio list and an open token gives `undefined` — no fit.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveFrameFitAspect(args: {
|
|
83
|
+
provider: string | undefined
|
|
84
|
+
requestedAspect: string | undefined
|
|
85
|
+
sourceWidth: number
|
|
86
|
+
sourceHeight: number
|
|
87
|
+
}): string | undefined {
|
|
88
|
+
const requested = args.requestedAspect?.trim()
|
|
89
|
+
if (requested && !OPEN_ASPECT_TOKENS.has(requested.toLowerCase())) return requested
|
|
90
|
+
const ratios = args.provider ? getModel(args.provider)?.aspectRatios : undefined
|
|
91
|
+
if (!ratios?.length || !(args.sourceWidth > 0) || !(args.sourceHeight > 0)) return undefined
|
|
92
|
+
const source = args.sourceWidth / args.sourceHeight
|
|
93
|
+
let best: { token: string; gap: number } | undefined
|
|
94
|
+
for (const token of ratios) {
|
|
95
|
+
const value = parseAspectToken(token)
|
|
96
|
+
if (value === undefined) continue // skips "adaptive"/"Auto" members
|
|
97
|
+
const gap = Math.abs(Math.log(value / source))
|
|
98
|
+
if (!best || gap < best.gap) best = { token, gap }
|
|
99
|
+
}
|
|
100
|
+
return best?.token
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What `prepareVideoFrames` should do to one frame. `null` = nothing. */
|
|
104
|
+
export interface FrameFitPlan {
|
|
105
|
+
/** Final pixel size to produce. */
|
|
106
|
+
readonly width: number
|
|
107
|
+
readonly height: number
|
|
108
|
+
/** Centre-crop applied BEFORE the resize (only past the stretch tolerance). */
|
|
109
|
+
readonly crop?: { readonly left: number; readonly top: number; readonly width: number; readonly height: number }
|
|
110
|
+
/** Why the plan exists — carried into logs and job output for traceability. */
|
|
111
|
+
readonly reason: "resolution" | "ratio"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Round to an even number ≥ 2 — odd dimensions break yuv420p encoders. */
|
|
115
|
+
function even(value: number): number {
|
|
116
|
+
return Math.max(2, Math.round(value / 2) * 2)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The smallest change that makes `width x height` exactly `aspect`: keep the
|
|
121
|
+
* longer side, move the shorter one. Rounding to even can leave a sub-pixel
|
|
122
|
+
* residue, which is why the caller compares ratios with a tolerance rather than
|
|
123
|
+
* for equality.
|
|
124
|
+
*/
|
|
125
|
+
export function minimalRatioDimensions(width: number, height: number, aspect: number): { width: number; height: number } {
|
|
126
|
+
const current = width / height
|
|
127
|
+
if (current > aspect) {
|
|
128
|
+
// too wide → bring the height up (keep the long side, the width)
|
|
129
|
+
return { width: even(width), height: even(width / aspect) }
|
|
130
|
+
}
|
|
131
|
+
return { width: even(height * aspect), height: even(height) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The centre-crop that turns `width x height` into exactly `aspect`, dropping
|
|
136
|
+
* the overhang on the long axis.
|
|
137
|
+
*/
|
|
138
|
+
export function centreCropToAspect(width: number, height: number, aspect: number): { left: number; top: number; width: number; height: number } {
|
|
139
|
+
const current = width / height
|
|
140
|
+
if (current > aspect) {
|
|
141
|
+
const w = even(height * aspect)
|
|
142
|
+
return { left: Math.max(0, Math.round((width - w) / 2)), top: 0, width: Math.min(width, w), height }
|
|
143
|
+
}
|
|
144
|
+
const h = even(width / aspect)
|
|
145
|
+
return { left: 0, top: Math.max(0, Math.round((height - h) / 2)), width, height: Math.min(height, h) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Turn a request into a concrete plan for ONE frame, or `null` when the frame
|
|
150
|
+
* should be sent untouched.
|
|
151
|
+
*
|
|
152
|
+
* Degradation ladder — a missing measurement must never invent geometry:
|
|
153
|
+
* `resolution` with no measured canvas → behaves as `ratio`
|
|
154
|
+
* `ratio` with no resolvable aspect → no fit
|
|
155
|
+
* any fit whose target equals the source (within a pixel) → no fit
|
|
156
|
+
*/
|
|
157
|
+
export function computeFrameFitPlan(args: {
|
|
158
|
+
fit: FrameFit
|
|
159
|
+
provider: string | undefined
|
|
160
|
+
resolution: string | undefined
|
|
161
|
+
/** The aspect as the request carries it: a ratio, `adaptive`/`Auto`, or absent. */
|
|
162
|
+
aspect: string | undefined
|
|
163
|
+
sourceWidth: number
|
|
164
|
+
sourceHeight: number
|
|
165
|
+
/** Overrides the measured table (tests, and a caller that already looked up). */
|
|
166
|
+
canvas?: VideoOutputCanvas
|
|
167
|
+
tolerance?: number
|
|
168
|
+
}): FrameFitPlan | null {
|
|
169
|
+
const { fit, sourceWidth, sourceHeight } = args
|
|
170
|
+
if (fit === "original") return null
|
|
171
|
+
if (!(sourceWidth > 0) || !(sourceHeight > 0)) return null
|
|
172
|
+
|
|
173
|
+
const aspectToken = resolveFrameFitAspect({
|
|
174
|
+
provider: args.provider,
|
|
175
|
+
requestedAspect: args.aspect,
|
|
176
|
+
sourceWidth,
|
|
177
|
+
sourceHeight,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const canvas = fit === "resolution"
|
|
181
|
+
? args.canvas ?? resolveOutputCanvas(args.provider, args.resolution, aspectToken)
|
|
182
|
+
: undefined
|
|
183
|
+
|
|
184
|
+
const targetAspect = canvas ? canvas[0] / canvas[1] : parseAspectToken(aspectToken)
|
|
185
|
+
if (targetAspect === undefined || !(targetAspect > 0)) return null
|
|
186
|
+
|
|
187
|
+
const sourceAspect = sourceWidth / sourceHeight
|
|
188
|
+
const gap = Math.abs(sourceAspect - targetAspect) / targetAspect
|
|
189
|
+
const tolerance = args.tolerance ?? FRAME_FIT_STRETCH_TOLERANCE
|
|
190
|
+
const crop = gap > tolerance ? centreCropToAspect(sourceWidth, sourceHeight, targetAspect) : undefined
|
|
191
|
+
|
|
192
|
+
const target = canvas
|
|
193
|
+
? { width: canvas[0], height: canvas[1] }
|
|
194
|
+
: minimalRatioDimensions(
|
|
195
|
+
crop ? crop.width : sourceWidth,
|
|
196
|
+
crop ? crop.height : sourceHeight,
|
|
197
|
+
targetAspect,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const unchanged = target.width === sourceWidth && target.height === sourceHeight && !crop
|
|
201
|
+
if (unchanged) return null
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
width: target.width,
|
|
205
|
+
height: target.height,
|
|
206
|
+
...(crop ? { crop } : {}),
|
|
207
|
+
reason: canvas ? "resolution" : "ratio",
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Frame or reference? `auto` reads the per-provider default measured above.
|
|
213
|
+
* Reference delivery is only possible on models that accept reference images —
|
|
214
|
+
* on the others `reference` collapses back to `frame` rather than dropping the
|
|
215
|
+
* frame on the floor.
|
|
216
|
+
*/
|
|
217
|
+
export function resolveFrameDelivery(args: {
|
|
218
|
+
provider: string | undefined
|
|
219
|
+
requested: FrameDelivery | undefined
|
|
220
|
+
supportsReferenceImages: boolean
|
|
221
|
+
}): Exclude<FrameDelivery, "auto"> {
|
|
222
|
+
const requested = args.requested ?? DEFAULT_FRAME_DELIVERY
|
|
223
|
+
const wanted = requested === "auto"
|
|
224
|
+
? (args.provider ? FRAME_DELIVERY_BY_PROVIDER[args.provider] ?? "frame" : "frame")
|
|
225
|
+
: requested
|
|
226
|
+
if (wanted === "reference" && !args.supportsReferenceImages) return "frame"
|
|
227
|
+
return wanted
|
|
228
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real output canvases per (video model, resolution, aspect ratio).
|
|
3
|
+
*
|
|
4
|
+
* Every entry is a pixel size read off a finished render with ffprobe — the
|
|
5
|
+
* geometry a model hands back, never a rate or a price. None of it is
|
|
6
|
+
* arithmetic either, because the providers do not follow arithmetic:
|
|
7
|
+
*
|
|
8
|
+
* - minimax-h3 at 768P returns 768x1344 (0.5714) for a 9:16 request,
|
|
9
|
+
* - seedance-2 at 480p 16:9 returns 864x496 (1.742) while seedance-2-5
|
|
10
|
+
* returns 854x480 (1.778) for the same request,
|
|
11
|
+
* - grok returns 736x400 (1.84), seedance 1.0 at 1080p 1:1 returns 1440x1440.
|
|
12
|
+
*
|
|
13
|
+
* A formula would get all four wrong, which is why `resolveOutputCanvas`
|
|
14
|
+
* answers `undefined` for anything not measured: callers must then leave the
|
|
15
|
+
* frame alone rather than invent geometry (see `computeFrameFitPlan`).
|
|
16
|
+
*
|
|
17
|
+
* HOW TO EXTEND: run `node tools/harvest-output-canvas.mjs` (it pages the admin
|
|
18
|
+
* jobs API, groups completed video jobs by provider/resolution/aspect and probes
|
|
19
|
+
* real outputs) and paste new rows here. Take rows from REFERENCE or
|
|
20
|
+
* TEXT-TO-VIDEO jobs only: in frame mode the adaptive models (the seedance and
|
|
21
|
+
* wan families) size the output from the INPUT image, so those rows describe the
|
|
22
|
+
* input that was sent, not the canvas the model would choose on its own.
|
|
23
|
+
*
|
|
24
|
+
* Seeded 2026-09-16 from 3000 completed production jobs.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** `[width, height]` in pixels. */
|
|
28
|
+
export type VideoOutputCanvas = readonly [number, number]
|
|
29
|
+
|
|
30
|
+
/** provider → resolution (lower-cased) → aspect token → canvas. */
|
|
31
|
+
export const VIDEO_OUTPUT_CANVAS: Readonly<
|
|
32
|
+
Record<string, Readonly<Record<string, Readonly<Record<string, VideoOutputCanvas>>>>>
|
|
33
|
+
> = {
|
|
34
|
+
"seedance-2-5": {
|
|
35
|
+
"480p": { "16:9": [854, 480], "9:16": [480, 854] },
|
|
36
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280], "1:1": [960, 960] },
|
|
37
|
+
"1080p": { "16:9": [1920, 1080] },
|
|
38
|
+
},
|
|
39
|
+
// The 2.0 family is NOT the same as 2.5 at 480p — 864x496 vs 854x480.
|
|
40
|
+
"seedance-2": {
|
|
41
|
+
"480p": { "16:9": [864, 496], "9:16": [496, 864] },
|
|
42
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
43
|
+
},
|
|
44
|
+
"seedance-2-fast": {
|
|
45
|
+
"480p": { "16:9": [864, 496], "9:16": [496, 864] },
|
|
46
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
47
|
+
},
|
|
48
|
+
"seedance-2-mini": {
|
|
49
|
+
"480p": { "16:9": [864, 496], "9:16": [496, 864] },
|
|
50
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
51
|
+
},
|
|
52
|
+
"gemini-omni-video": {
|
|
53
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
54
|
+
"1080p": { "16:9": [1920, 1080] },
|
|
55
|
+
},
|
|
56
|
+
"gemini-omni-flash": {
|
|
57
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
58
|
+
},
|
|
59
|
+
"veo3.1": {
|
|
60
|
+
"720p": { "9:16": [720, 1280] },
|
|
61
|
+
"1080p": { "16:9": [1920, 1080] },
|
|
62
|
+
},
|
|
63
|
+
"wan-3": {
|
|
64
|
+
"480p": { "16:9": [832, 480] },
|
|
65
|
+
"720p": { "16:9": [1280, 720], "9:16": [720, 1280] },
|
|
66
|
+
},
|
|
67
|
+
"wan-3-prime": {
|
|
68
|
+
"720p": { "16:9": [1280, 720] },
|
|
69
|
+
},
|
|
70
|
+
// 768P's "9:16" is 0.5714, not 0.5625 — the single clearest reason this table
|
|
71
|
+
// exists.
|
|
72
|
+
"minimax-h3": {
|
|
73
|
+
"768p": { "9:16": [768, 1344] },
|
|
74
|
+
"2k": { "16:9": [2560, 1440], "9:16": [1440, 2560] },
|
|
75
|
+
},
|
|
76
|
+
seedance: {
|
|
77
|
+
"1080p": {
|
|
78
|
+
"16:9": [1920, 1080],
|
|
79
|
+
"9:16": [1080, 1920],
|
|
80
|
+
"1:1": [1440, 1440],
|
|
81
|
+
"3:4": [1248, 1664],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The canvas a model renders for this request, or `undefined` when we have never
|
|
88
|
+
* measured that combination. `undefined` means "leave the frame alone".
|
|
89
|
+
*
|
|
90
|
+
* Resolution matching is case-insensitive (`768P`, `2K`, `720p` all appear in
|
|
91
|
+
* the wild). An aspect of `adaptive` / `Auto` has no canvas of its own — the
|
|
92
|
+
* caller resolves it to a concrete ratio first (`resolveFrameFitAspect`).
|
|
93
|
+
*/
|
|
94
|
+
export function resolveOutputCanvas(
|
|
95
|
+
provider: string | undefined,
|
|
96
|
+
resolution: string | undefined,
|
|
97
|
+
aspect: string | undefined,
|
|
98
|
+
): VideoOutputCanvas | undefined {
|
|
99
|
+
if (!provider || !resolution || !aspect) return undefined
|
|
100
|
+
return VIDEO_OUTPUT_CANVAS[provider]?.[resolution.toLowerCase()]?.[aspect]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every measured combination, for tests and for the harvest script's diff. */
|
|
104
|
+
export function measuredCanvasCombinations(): Array<{
|
|
105
|
+
provider: string
|
|
106
|
+
resolution: string
|
|
107
|
+
aspect: string
|
|
108
|
+
canvas: VideoOutputCanvas
|
|
109
|
+
}> {
|
|
110
|
+
const out: Array<{ provider: string; resolution: string; aspect: string; canvas: VideoOutputCanvas }> = []
|
|
111
|
+
for (const [provider, byResolution] of Object.entries(VIDEO_OUTPUT_CANVAS)) {
|
|
112
|
+
for (const [resolution, byAspect] of Object.entries(byResolution)) {
|
|
113
|
+
for (const [aspect, canvas] of Object.entries(byAspect)) {
|
|
114
|
+
out.push({ provider, resolution, aspect, canvas })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|