@nodaro/shared 2.20.0 → 2.21.1
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 +458 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +368 -8
- package/dist/index.d.ts +368 -8
- package/dist/index.js +447 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/safety-retry-policy.test.ts +62 -0
- package/src/__tests__/topaz-upscale.test.ts +132 -0
- package/src/__tests__/unresolved-ref-tokens.test.ts +66 -0
- package/src/__tests__/video-analysis.test.ts +83 -0
- package/src/__tests__/video-audio-capability.test.ts +38 -4
- package/src/__tests__/video-catalog-totality.test.ts +85 -0
- package/src/__tests__/video-collapse-parity.test.ts +76 -0
- package/src/__tests__/video-ref-video-duration-limits.test.ts +69 -0
- package/src/__tests__/video-request-normalize.test.ts +239 -0
- package/src/credit-identifiers.ts +115 -8
- package/src/index.ts +23 -3
- package/src/model-catalog.ts +288 -6
- package/src/model-constants.ts +116 -4
- package/src/node-refs.ts +82 -0
- package/src/node-runtime-keys.ts +13 -0
- package/src/safety-retry-policy.ts +37 -0
- package/src/topaz-upscale.ts +163 -0
- package/src/video-analysis.ts +70 -5
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getModel } from "./model-catalog.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry/fallback behavior derived from a catalog entry's `safetyFilter`
|
|
5
|
+
* flag (see `ModelCatalogEntry.safetyFilter` in `model-catalog.ts`).
|
|
6
|
+
*/
|
|
7
|
+
export interface SafetyRetryPolicy {
|
|
8
|
+
/** 2 when the model declares `safetyFilter.stochastic`, else 1. */
|
|
9
|
+
maxAttempts: 1 | 2
|
|
10
|
+
/**
|
|
11
|
+
* Catalog model id to offer when the retry also blocks. Only present
|
|
12
|
+
* when the entry declares one AND it resolves to a real catalog entry.
|
|
13
|
+
*/
|
|
14
|
+
fallback?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The provider's safety filter is known to be non-deterministic on some
|
|
19
|
+
* catalog models — a benign prompt can trip it once and pass on an
|
|
20
|
+
* identical retry. For those models the platform retries a blocked
|
|
21
|
+
* request once (`maxAttempts: 2`) before giving up; `fallback` names the
|
|
22
|
+
* catalog model to offer the user when the retry also blocks.
|
|
23
|
+
*
|
|
24
|
+
* Every model not flagged this way — including an unrecognized id —
|
|
25
|
+
* gets a single attempt and no fallback.
|
|
26
|
+
*/
|
|
27
|
+
export function safetyRetryPolicy(modelId: string): SafetyRetryPolicy {
|
|
28
|
+
const entry = getModel(modelId)
|
|
29
|
+
const safetyFilter = entry?.safetyFilter
|
|
30
|
+
if (!safetyFilter?.stochastic) return { maxAttempts: 1 }
|
|
31
|
+
|
|
32
|
+
const fallback = safetyFilter.fallback
|
|
33
|
+
if (fallback && getModel(fallback)) {
|
|
34
|
+
return { maxAttempts: 2, fallback }
|
|
35
|
+
}
|
|
36
|
+
return { maxAttempts: 2 }
|
|
37
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topaz image upscale — the single authority for "which lever did the user
|
|
3
|
+
* pull, what do we send, and what do we charge".
|
|
4
|
+
*
|
|
5
|
+
* KIE's `topaz/image-upscale` takes exactly ONE quality lever:
|
|
6
|
+
* upscale_factor: "1" | "2" | "4" (default "2")
|
|
7
|
+
* There is no target-resolution parameter (docs.kie.ai/market/topaz/image-upscale,
|
|
8
|
+
* verified 2026-09-02). The editor nevertheless shipped TWO controls — an
|
|
9
|
+
* `upscaleFactor` select AND a `targetResolution` select (2K/4K/8K) — and billed
|
|
10
|
+
* on the second one, which never reached the worker. Everyone who bought 4K or
|
|
11
|
+
* 8K got a 2x render at 2x/4x the price (app-reports triage 2026-09-01 §4.3).
|
|
12
|
+
*
|
|
13
|
+
* So: the FACTOR is the lever. `targetResolution` survives only as a legacy
|
|
14
|
+
* input (stored node data, old API/MCP callers) and is mapped forward here.
|
|
15
|
+
* The credit tier is derived from the SAME resolution, which is why callers
|
|
16
|
+
* must pass `creditTier` into `buildCreditModelIdentifier` rather than the raw
|
|
17
|
+
* request value — that is what makes CHECK = DEBIT = SENT.
|
|
18
|
+
*
|
|
19
|
+
* The composite `topaz-image-upscale:4K` is retained as the id of the 4x tier
|
|
20
|
+
* (migration 288 wrote that row; renaming it would need a pricing migration for
|
|
21
|
+
* zero user-visible gain). Read it as "the top Topaz tier", not as a promise of
|
|
22
|
+
* 4096 pixels. `topaz-image-upscale:8K` stays priced in STATIC_CREDIT_COSTS so
|
|
23
|
+
* historical usage_logs still resolve, but nothing can reserve it any more.
|
|
24
|
+
*
|
|
25
|
+
* Resolution order (fix round 1, 2026-09-02 review):
|
|
26
|
+
* 1. Resolve the WINNING factor first — a valid `upscaleFactor` always wins;
|
|
27
|
+
* an invalid one falls through to `targetResolution` exactly as if it had
|
|
28
|
+
* never been sent, rather than freezing the default before the legacy
|
|
29
|
+
* tier gets a chance to raise it (an invalid factor + a stored 4K/8K tier
|
|
30
|
+
* must still resolve — and bill — at the tier's factor, not the default).
|
|
31
|
+
* 2. Only THEN build `adjustments`, so every `to` reflects the resolved
|
|
32
|
+
* factor rather than an intermediate guess, and every `from` is the raw,
|
|
33
|
+
* untransformed input string (never the trimmed/uppercased copy used
|
|
34
|
+
* internally to match).
|
|
35
|
+
* 3. A valid `upscaleFactor` alongside a `targetResolution` it disagrees
|
|
36
|
+
* with emits an informational (not corrective) adjustment — the stored
|
|
37
|
+
* legacy choice is being overridden, not silently dropped — with
|
|
38
|
+
* `to: undefined` because nothing was actually sent for that field.
|
|
39
|
+
* Agreement (they resolve to the same factor) is reported as nothing:
|
|
40
|
+
* there is no override to disclose.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export type TopazUpscaleFactor = "1" | "2" | "4"
|
|
44
|
+
|
|
45
|
+
export const TOPAZ_UPSCALE_FACTORS: readonly TopazUpscaleFactor[] = ["1", "2", "4"]
|
|
46
|
+
|
|
47
|
+
/** KIE's own default when `upscale_factor` is omitted (providers/kie/models.ts). */
|
|
48
|
+
export const TOPAZ_DEFAULT_UPSCALE_FACTOR: TopazUpscaleFactor = "2"
|
|
49
|
+
|
|
50
|
+
export interface TopazUpscaleAdjustment {
|
|
51
|
+
field: "upscaleFactor" | "targetResolution"
|
|
52
|
+
/** The raw, untransformed value as received — never trimmed or case-normalized. */
|
|
53
|
+
from: string
|
|
54
|
+
/**
|
|
55
|
+
* The resolved factor this adjustment corresponds to, or `undefined` for the
|
|
56
|
+
* informational "your stored targetResolution was overridden" notice, which
|
|
57
|
+
* names nothing to switch TO — `upscaleFactor` already won.
|
|
58
|
+
*/
|
|
59
|
+
to: string | undefined
|
|
60
|
+
reason: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TopazUpscaleResolution {
|
|
64
|
+
/** Sent to KIE as `upscale_factor`. Always set — never rely on the model default. */
|
|
65
|
+
upscaleFactor: TopazUpscaleFactor
|
|
66
|
+
/**
|
|
67
|
+
* Passed VERBATIM as `buildCreditModelIdentifier`'s `targetResolution` arg.
|
|
68
|
+
* `undefined` = the bare id (1x / 2x); `"4K"` = the 4x tier.
|
|
69
|
+
*/
|
|
70
|
+
creditTier: "4K" | undefined
|
|
71
|
+
/** Non-empty when a legacy or out-of-enum value was coerced or overridden. */
|
|
72
|
+
adjustments: TopazUpscaleAdjustment[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Legacy 2K/4K/8K tier → the factor the provider can actually deliver. */
|
|
76
|
+
const LEGACY_TIER_TO_FACTOR: Record<string, TopazUpscaleFactor> = {
|
|
77
|
+
"2K": "2",
|
|
78
|
+
"4K": "4",
|
|
79
|
+
"8K": "4",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isFactor(v: string): v is TopazUpscaleFactor {
|
|
83
|
+
return (TOPAZ_UPSCALE_FACTORS as readonly string[]).includes(v)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveTopazUpscale(input: {
|
|
87
|
+
upscaleFactor?: string | null
|
|
88
|
+
targetResolution?: string | null
|
|
89
|
+
}): TopazUpscaleResolution {
|
|
90
|
+
const adjustments: TopazUpscaleAdjustment[] = []
|
|
91
|
+
|
|
92
|
+
// Raw, untransformed inputs — these are what every adjustment's `from` reports.
|
|
93
|
+
const rawFactor = typeof input.upscaleFactor === "string" ? input.upscaleFactor : ""
|
|
94
|
+
const rawTier = typeof input.targetResolution === "string" ? input.targetResolution : ""
|
|
95
|
+
|
|
96
|
+
// Trimmed/normalized copies used ONLY for matching, never for display.
|
|
97
|
+
const trimmedFactor = rawFactor.trim()
|
|
98
|
+
const normalizedTier = rawTier.trim().toUpperCase()
|
|
99
|
+
|
|
100
|
+
const factorGiven = trimmedFactor.length > 0
|
|
101
|
+
const factorValid = factorGiven && isFactor(trimmedFactor)
|
|
102
|
+
|
|
103
|
+
const tierGiven = normalizedTier.length > 0
|
|
104
|
+
const tierMappedFactor = tierGiven ? LEGACY_TIER_TO_FACTOR[normalizedTier] : undefined
|
|
105
|
+
|
|
106
|
+
// --- 1. Resolve the winning factor FIRST. ---
|
|
107
|
+
// A valid factor always wins. An invalid (or absent) one falls through to
|
|
108
|
+
// whatever the legacy tier maps to; only if that also comes up empty do we
|
|
109
|
+
// land on the provider default.
|
|
110
|
+
const finalFactor: TopazUpscaleFactor = factorValid
|
|
111
|
+
? (trimmedFactor as TopazUpscaleFactor)
|
|
112
|
+
: (tierMappedFactor ?? TOPAZ_DEFAULT_UPSCALE_FACTOR)
|
|
113
|
+
|
|
114
|
+
// --- 2. Emit adjustments LAST, against the already-resolved factor. ---
|
|
115
|
+
if (factorGiven && !factorValid) {
|
|
116
|
+
adjustments.push({
|
|
117
|
+
field: "upscaleFactor",
|
|
118
|
+
from: rawFactor,
|
|
119
|
+
to: finalFactor,
|
|
120
|
+
reason: `Topaz upscale accepts a factor of 1, 2 or 4 — "${rawFactor}" was replaced with ${finalFactor}.`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (factorValid && tierGiven) {
|
|
125
|
+
// The factor won outright. Say so if it disagrees with the stored legacy
|
|
126
|
+
// tier — otherwise the tier silently vanishes with no record. Agreement
|
|
127
|
+
// needs no note: nothing was overridden.
|
|
128
|
+
if (tierMappedFactor !== finalFactor) {
|
|
129
|
+
adjustments.push({
|
|
130
|
+
field: "targetResolution",
|
|
131
|
+
from: rawTier,
|
|
132
|
+
to: undefined,
|
|
133
|
+
reason: "upscaleFactor takes precedence over the legacy targetResolution.",
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
} else if (!factorValid && tierGiven) {
|
|
137
|
+
// No valid explicit factor — the legacy tier is the (partial) source of
|
|
138
|
+
// the resolved factor, or was consulted and found unusable.
|
|
139
|
+
if (tierMappedFactor) {
|
|
140
|
+
if (normalizedTier === "8K") {
|
|
141
|
+
adjustments.push({
|
|
142
|
+
field: "targetResolution",
|
|
143
|
+
from: rawTier,
|
|
144
|
+
to: finalFactor,
|
|
145
|
+
reason: "Topaz upscale offers factors up to 4x — an 8K target renders and bills at the 4x tier.",
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
adjustments.push({
|
|
150
|
+
field: "targetResolution",
|
|
151
|
+
from: rawTier,
|
|
152
|
+
to: finalFactor,
|
|
153
|
+
reason: `Unknown Topaz target "${rawTier}" — rendering at ${finalFactor}x.`,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
upscaleFactor: finalFactor,
|
|
160
|
+
creditTier: finalFactor === "4" ? "4K" : undefined,
|
|
161
|
+
adjustments,
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/video-analysis.ts
CHANGED
|
@@ -175,6 +175,26 @@ export const clipLookSchema = z.object({
|
|
|
175
175
|
* insert) states the deviation in that scene's `visual`, as with `lighting`.
|
|
176
176
|
*/
|
|
177
177
|
style: z.string().optional(),
|
|
178
|
+
/**
|
|
179
|
+
* The Style picker CATALOG ID the prose in `style` corresponds to — the
|
|
180
|
+
* analyzer's PICK ("pixar-3d" beside "3D stylized animation"), not a second
|
|
181
|
+
* description of it.
|
|
182
|
+
*
|
|
183
|
+
* Worth strictly more than the prose it accompanies: an id addresses the
|
|
184
|
+
* catalog, so a recreation renders the same medium the product's own Style
|
|
185
|
+
* picker would render, instead of re-interpreting a sentence. Absent when the
|
|
186
|
+
* analyzer read a medium it could not place in the catalog — `style` then
|
|
187
|
+
* carries the whole answer, as it always did.
|
|
188
|
+
*
|
|
189
|
+
* A free `string` here on purpose. The PRODUCER validates it against the
|
|
190
|
+
* catalog (the analyzer plugin's wire schema is an enum generated from
|
|
191
|
+
* `STYLES`, so an invented id never leaves it), while this package must not
|
|
192
|
+
* carry the catalog itself. A closed enum here would only add a second copy
|
|
193
|
+
* of the vocabulary to drift out of date — and, worse, would REJECT a
|
|
194
|
+
* catalog entry newer than the installed `@nodaro/shared`, which is exactly
|
|
195
|
+
* the analysis a consumer most wants to read.
|
|
196
|
+
*/
|
|
197
|
+
styleId: z.string().optional(),
|
|
178
198
|
/** Colour grade / palette — "muted teal-and-orange, crushed blacks". */
|
|
179
199
|
grade: z.string().optional(),
|
|
180
200
|
/** Camera or film FORMAT and stock — "anamorphic digital", "16mm film grain". */
|
|
@@ -265,14 +285,32 @@ export const entitySlotSchema = z.object({
|
|
|
265
285
|
})
|
|
266
286
|
export type EntitySlot = z.infer<typeof entitySlotSchema>
|
|
267
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The sound LAYER vocabulary — what kind of thing this layer is.
|
|
290
|
+
*
|
|
291
|
+
* `ambience` is deliberately its own member rather than a flavour of `sfx`:
|
|
292
|
+
* they are different layers of a real mix and they are recreated by different
|
|
293
|
+
* means — ambience is the continuous bed a scene sits in (room tone, distant
|
|
294
|
+
* traffic, wind), an sfx is a discrete hit (a door slam, a gunshot). Folded
|
|
295
|
+
* together, a scene's bed and its one-off noises arrive indistinguishable and
|
|
296
|
+
* anything recreating the mix has to guess which it was told.
|
|
297
|
+
*
|
|
298
|
+
* Exported so a consumer keying its own document by these modes imports the
|
|
299
|
+
* vocabulary instead of restating it — the same reason every other closed
|
|
300
|
+
* vocabulary in this file is a const.
|
|
301
|
+
*/
|
|
302
|
+
export const VIDEO_ANALYSIS_AUDIO_MODES = ["speech", "music", "sfx", "ambience"] as const
|
|
303
|
+
export type VideoAnalysisAudioMode = (typeof VIDEO_ANALYSIS_AUDIO_MODES)[number]
|
|
304
|
+
|
|
268
305
|
/**
|
|
269
306
|
* One concurrent sound layer in a scene. Real footage stacks sound (music bed
|
|
270
|
-
* under dialogue over ambient
|
|
271
|
-
* empty array means genuine silence. `content`: speech = verbatim words;
|
|
272
|
-
* music/sfx = gen-ready description. `voice` is speech-only
|
|
307
|
+
* under dialogue over ambient room tone), so a scene carries an ARRAY of these
|
|
308
|
+
* — an empty array means genuine silence. `content`: speech = verbatim words;
|
|
309
|
+
* music/sfx/ambience = gen-ready description. `voice` is speech-only
|
|
310
|
+
* voice-casting.
|
|
273
311
|
*/
|
|
274
312
|
const audioLayerSchema = z.object({
|
|
275
|
-
mode: z.enum(
|
|
313
|
+
mode: z.enum(VIDEO_ANALYSIS_AUDIO_MODES),
|
|
276
314
|
content: z.string().min(1),
|
|
277
315
|
voice: z.string().optional(),
|
|
278
316
|
/**
|
|
@@ -296,6 +334,33 @@ const audioLayerSchema = z.object({
|
|
|
296
334
|
* defect that `stripOrphanSlots` exists to kill.
|
|
297
335
|
*/
|
|
298
336
|
speakerSlot: z.string().optional(),
|
|
337
|
+
/**
|
|
338
|
+
* SPEECH ONLY — WHO says these words, by NAME.
|
|
339
|
+
*
|
|
340
|
+
* The second of two ways to name a speaker, and the one for documents that
|
|
341
|
+
* have no slots: `speakerSlot` addresses an `EntitySlot` of THIS analysis by
|
|
342
|
+
* id, while `speaker` is the plain cast name a production keys its own cast
|
|
343
|
+
* by ("Jack Mercer"). A layer may legitimately carry both — the id for the
|
|
344
|
+
* analysis it came from, the name for the document it is going into — and a
|
|
345
|
+
* consumer that understands only one reads the one it understands.
|
|
346
|
+
*
|
|
347
|
+
* Optional, and not refined against `mode`, for the same reason
|
|
348
|
+
* `speakerSlot` is not: the window schema is the enforced decode grammar and
|
|
349
|
+
* must not throw away a whole roll over one mis-tagged field.
|
|
350
|
+
*
|
|
351
|
+
* NOT swept by `dropUnknownSpeakers`. That function is the SLOT channel's
|
|
352
|
+
* sanitizer — it judges an id against the surviving slot list, and a name has
|
|
353
|
+
* no id space to be unknown in. The asymmetry is deliberate and pinned by
|
|
354
|
+
* test; whoever owns the name's vocabulary sanitizes the name.
|
|
355
|
+
*
|
|
356
|
+
* The residual that leaves: a layer can keep a `speaker` naming someone
|
|
357
|
+
* `stripOrphanSlots` already pruned as a phantom, where the same claim spelled
|
|
358
|
+
* `speakerSlot` would have been dropped — the invented-narrator defect, in the
|
|
359
|
+
* one spelling the sweep cannot see. LATENT today, since nothing in this
|
|
360
|
+
* repo emits `speaker`; the day the analyzer does, that sweep is what has to
|
|
361
|
+
* grow, not this field.
|
|
362
|
+
*/
|
|
363
|
+
speaker: z.string().optional(),
|
|
299
364
|
})
|
|
300
365
|
export type AudioLayer = z.infer<typeof audioLayerSchema>
|
|
301
366
|
|
|
@@ -502,7 +567,7 @@ export function rewriteSpeakerSlots(audio: AudioLayer[], slotRenames: Record<str
|
|
|
502
567
|
* Strip attribution that no scene can honour — the `dropUnknownBindings` mirror
|
|
503
568
|
* for the `audio` channel. Two cases, both model sloppiness rather than errors
|
|
504
569
|
* worth failing a roll over:
|
|
505
|
-
* - a `speakerSlot` on
|
|
570
|
+
* - a `speakerSlot` on any non-`speech` layer (nobody is speaking)
|
|
506
571
|
* - a `speakerSlot` naming a slot that is not in the final list
|
|
507
572
|
*
|
|
508
573
|
* MUST run AFTER the orphan-slot sweep, and attribution must NEVER count as a
|