@nodaro/shared 3.9.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 +448 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +959 -30
- package/dist/index.d.ts +959 -30
- package/dist/index.js +410 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +51 -1
- package/src/__tests__/gvp-supported-providers.test.ts +20 -3
- package/src/__tests__/model-catalog-sections.test.ts +59 -0
- package/src/__tests__/model-tree.test.ts +2 -2
- package/src/__tests__/parameter-node-value.test.ts +47 -0
- package/src/__tests__/pricing-default-duration.test.ts +45 -0
- package/src/__tests__/prompt-length-limits.test.ts +4 -0
- package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
- package/src/__tests__/template-categories.test.ts +45 -0
- package/src/__tests__/video-analysis.test.ts +15 -0
- package/src/__tests__/video-frame-fit.test.ts +189 -0
- package/src/__tests__/video-ref-limits.test.ts +11 -2
- package/src/catalog-projection.ts +3 -0
- package/src/character-motion-metadata.ts +19 -0
- package/src/credit-identifiers.ts +44 -9
- package/src/i18n/character-motion.ar.ts +1082 -0
- package/src/i18n/character-motion.de.ts +1082 -0
- package/src/i18n/character-motion.es.ts +1082 -0
- package/src/i18n/character-motion.fr.ts +1082 -0
- package/src/i18n/character-motion.he.ts +1082 -0
- package/src/i18n/character-motion.hi.ts +1082 -0
- package/src/i18n/character-motion.ja.ts +1082 -0
- package/src/i18n/character-motion.ko.ts +1082 -0
- package/src/i18n/character-motion.pt-BR.ts +1082 -0
- package/src/i18n/character-motion.ru.ts +1082 -0
- package/src/i18n/character-motion.zh-CN.ts +1082 -0
- package/src/i18n/types.ts +1 -0
- package/src/index.ts +26 -0
- package/src/model-catalog.ts +49 -32
- package/src/model-constants.ts +50 -11
- package/src/node-execution-state.ts +95 -0
- package/src/parameter-node-value.ts +59 -0
- package/src/presentation-utils.ts +1 -0
- package/src/pro-3d-render.ts +159 -0
- package/src/scene3d-delivery-notes.ts +490 -0
- package/src/scene3d-v2-plan.ts +8 -2
- package/src/smart-cut-windows.ts +15 -10
- package/src/template-categories.ts +67 -0
- package/src/video-analysis.ts +15 -0
- package/src/video-frame-fit.ts +228 -0
- package/src/video-output-canvas.ts +119 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
computeFrameFitPlan,
|
|
4
|
+
resolveFrameFitAspect,
|
|
5
|
+
resolveFrameDelivery,
|
|
6
|
+
minimalRatioDimensions,
|
|
7
|
+
centreCropToAspect,
|
|
8
|
+
parseAspectToken,
|
|
9
|
+
FRAME_FIT_STRETCH_TOLERANCE,
|
|
10
|
+
} from "../video-frame-fit.js"
|
|
11
|
+
import { resolveOutputCanvas, measuredCanvasCombinations } from "../video-output-canvas.js"
|
|
12
|
+
|
|
13
|
+
/** The image behind every number in this file: a 1K 9:16 GPT image. */
|
|
14
|
+
const GPT_1K = { sourceWidth: 940, sourceHeight: 1672 }
|
|
15
|
+
|
|
16
|
+
describe("measured output canvases", () => {
|
|
17
|
+
it("answers the combinations we measured, case-insensitively on resolution", () => {
|
|
18
|
+
expect(resolveOutputCanvas("seedance-2-5", "720p", "9:16")).toEqual([720, 1280])
|
|
19
|
+
expect(resolveOutputCanvas("minimax-h3", "768P", "9:16")).toEqual([768, 1344])
|
|
20
|
+
expect(resolveOutputCanvas("minimax-h3", "2K", "16:9")).toEqual([2560, 1440])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("keeps the 2.0 family apart from 2.5 at 480p — they really do differ", () => {
|
|
24
|
+
expect(resolveOutputCanvas("seedance-2-5", "480p", "16:9")).toEqual([854, 480])
|
|
25
|
+
expect(resolveOutputCanvas("seedance-2-fast", "480p", "16:9")).toEqual([864, 496])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("answers undefined for anything never measured", () => {
|
|
29
|
+
expect(resolveOutputCanvas("seedance-2-5", "4k", "9:16")).toBeUndefined()
|
|
30
|
+
expect(resolveOutputCanvas("kling-3.0", "720p", "16:9")).toBeUndefined()
|
|
31
|
+
expect(resolveOutputCanvas(undefined, "720p", "9:16")).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("stores only even, positive dimensions", () => {
|
|
35
|
+
for (const { provider, resolution, aspect, canvas } of measuredCanvasCombinations()) {
|
|
36
|
+
const where = `${provider} ${resolution} ${aspect}`
|
|
37
|
+
expect(canvas[0] % 2, where).toBe(0)
|
|
38
|
+
expect(canvas[1] % 2, where).toBe(0)
|
|
39
|
+
expect(canvas[0] > 0 && canvas[1] > 0, where).toBe(true)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("does not claim a canvas whose ratio is wildly off the label", () => {
|
|
44
|
+
// H3's 768P 9:16 really is 0.5714 — a 1.6% lie we keep on purpose. Anything
|
|
45
|
+
// further out is a typo, not a provider quirk.
|
|
46
|
+
for (const { provider, resolution, aspect, canvas } of measuredCanvasCombinations()) {
|
|
47
|
+
const label = parseAspectToken(aspect)
|
|
48
|
+
if (label === undefined) continue
|
|
49
|
+
const actual = canvas[0] / canvas[1]
|
|
50
|
+
expect(Math.abs(actual - label) / label, `${provider} ${resolution} ${aspect}`).toBeLessThan(0.06)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("aspect resolution", () => {
|
|
56
|
+
it("uses an explicit ratio as-is", () => {
|
|
57
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "16:9", ...GPT_1K })).toBe("16:9")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("snaps adaptive and Auto to the model's nearest listed ratio", () => {
|
|
61
|
+
// 940x1672 = 0.5622, which is 9:16 to within 0.05%.
|
|
62
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "adaptive", ...GPT_1K })).toBe("9:16")
|
|
63
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "Auto", ...GPT_1K })).toBe("9:16")
|
|
64
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: undefined, sourceWidth: 1920, sourceHeight: 1080 })).toBe("16:9")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("gives up when the model declares no ratios", () => {
|
|
68
|
+
expect(resolveFrameFitAspect({ provider: "not-a-model", requestedAspect: "adaptive", ...GPT_1K })).toBeUndefined()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("computeFrameFitPlan", () => {
|
|
73
|
+
it("resizes the 1K image to the measured canvas — the case that fixed the snap", () => {
|
|
74
|
+
const plan = computeFrameFitPlan({
|
|
75
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16", ...GPT_1K,
|
|
76
|
+
})
|
|
77
|
+
expect(plan).toEqual({ width: 720, height: 1280, reason: "resolution" })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("targets H3's real 768x1344 canvas rather than a true 9:16", () => {
|
|
81
|
+
const plan = computeFrameFitPlan({
|
|
82
|
+
fit: "resolution", provider: "minimax-h3", resolution: "768P", aspect: "9:16", ...GPT_1K,
|
|
83
|
+
})
|
|
84
|
+
expect(plan?.width).toBe(768)
|
|
85
|
+
expect(plan?.height).toBe(1344)
|
|
86
|
+
expect(plan?.crop).toBeUndefined() // 1.6% gap is inside the tolerance → stretch
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("does nothing when the frame is already the canvas", () => {
|
|
90
|
+
expect(computeFrameFitPlan({
|
|
91
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
92
|
+
sourceWidth: 720, sourceHeight: 1280,
|
|
93
|
+
})).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("does nothing in original mode, whatever the size", () => {
|
|
97
|
+
expect(computeFrameFitPlan({
|
|
98
|
+
fit: "original", provider: "seedance-2-5", resolution: "720p", aspect: "9:16", ...GPT_1K,
|
|
99
|
+
})).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("degrades resolution → ratio when the combination was never measured", () => {
|
|
103
|
+
const plan = computeFrameFitPlan({
|
|
104
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "4k", aspect: "16:9",
|
|
105
|
+
sourceWidth: 1000, sourceHeight: 1000,
|
|
106
|
+
})
|
|
107
|
+
// No 4k canvas on file, so it falls back to the minimal change that makes 16:9.
|
|
108
|
+
expect(plan?.reason).toBe("ratio")
|
|
109
|
+
expect(plan!.width / plan!.height).toBeCloseTo(16 / 9, 2)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("degrades to nothing when neither a canvas nor an aspect can be resolved", () => {
|
|
113
|
+
expect(computeFrameFitPlan({
|
|
114
|
+
fit: "resolution", provider: "not-a-model", resolution: "720p", aspect: undefined, ...GPT_1K,
|
|
115
|
+
})).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("stretches inside the tolerance and crops outside it", () => {
|
|
119
|
+
// 4% off 9:16 — inside 5%, so a plain stretch, no crop.
|
|
120
|
+
const inside = computeFrameFitPlan({
|
|
121
|
+
fit: "ratio", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
122
|
+
sourceWidth: 1000, sourceHeight: 1710,
|
|
123
|
+
})
|
|
124
|
+
expect(Math.abs(1000 / 1710 - 9 / 16) / (9 / 16)).toBeLessThan(FRAME_FIT_STRETCH_TOLERANCE)
|
|
125
|
+
expect(inside?.crop).toBeUndefined()
|
|
126
|
+
|
|
127
|
+
// A square photo into 9:16 is a 78% gap — crop first, never squash.
|
|
128
|
+
const outside = computeFrameFitPlan({
|
|
129
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
130
|
+
sourceWidth: 1024, sourceHeight: 1024,
|
|
131
|
+
})
|
|
132
|
+
expect(outside).toMatchObject({ width: 720, height: 1280, reason: "resolution" })
|
|
133
|
+
expect(outside?.crop).toEqual({ left: 224, top: 0, width: 576, height: 1024 })
|
|
134
|
+
expect(outside!.crop!.width / outside!.crop!.height).toBeCloseTo(9 / 16, 3)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("produces even dimensions (yuv420p rejects odd ones)", () => {
|
|
138
|
+
const plan = computeFrameFitPlan({
|
|
139
|
+
fit: "ratio", provider: "seedance-2-5", resolution: "720p", aspect: "16:9",
|
|
140
|
+
sourceWidth: 1001, sourceHeight: 667,
|
|
141
|
+
})
|
|
142
|
+
expect(plan!.width % 2).toBe(0)
|
|
143
|
+
expect(plan!.height % 2).toBe(0)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("geometry helpers", () => {
|
|
148
|
+
it("minimalRatioDimensions keeps the long side and moves the short one", () => {
|
|
149
|
+
// 940 wide is 9:16 at 1671.1 tall, which rounds back to the image's own
|
|
150
|
+
// 1672 — the 1K GPT image really is 9:16 to the nearest even pixel, so
|
|
151
|
+
// "match ratio" alone would leave it untouched (and the snap would stay).
|
|
152
|
+
expect(minimalRatioDimensions(940, 1672, 9 / 16)).toEqual({ width: 940, height: 1672 })
|
|
153
|
+
expect(minimalRatioDimensions(1920, 1000, 16 / 9)).toEqual({ width: 1920, height: 1080 })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("centreCropToAspect drops the overhang evenly", () => {
|
|
157
|
+
expect(centreCropToAspect(1024, 1024, 9 / 16)).toEqual({ left: 224, top: 0, width: 576, height: 1024 })
|
|
158
|
+
expect(centreCropToAspect(1000, 1000, 16 / 9)).toEqual({ left: 0, top: 219, width: 1000, height: 562 })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("parseAspectToken reads the tokens the catalog uses", () => {
|
|
162
|
+
expect(parseAspectToken("16:9")).toBeCloseTo(16 / 9, 6)
|
|
163
|
+
expect(parseAspectToken("9:16")).toBeCloseTo(9 / 16, 6)
|
|
164
|
+
expect(parseAspectToken("adaptive")).toBeUndefined()
|
|
165
|
+
expect(parseAspectToken(undefined)).toBeUndefined()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("frame delivery", () => {
|
|
170
|
+
it("sends the Seedance 2.0 family as references and everything else as frames", () => {
|
|
171
|
+
const ref = { requested: "auto" as const, supportsReferenceImages: true }
|
|
172
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", ...ref })).toBe("reference")
|
|
173
|
+
expect(resolveFrameDelivery({ provider: "seedance-2", ...ref })).toBe("reference")
|
|
174
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-mini", ...ref })).toBe("reference")
|
|
175
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-5", ...ref })).toBe("frame")
|
|
176
|
+
expect(resolveFrameDelivery({ provider: "wan-3", ...ref })).toBe("frame")
|
|
177
|
+
expect(resolveFrameDelivery({ provider: "veo3.1", ...ref })).toBe("frame")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("honours an explicit choice", () => {
|
|
181
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", requested: "frame", supportsReferenceImages: true })).toBe("frame")
|
|
182
|
+
expect(resolveFrameDelivery({ provider: "veo3.1", requested: "reference", supportsReferenceImages: true })).toBe("reference")
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("never asks for reference delivery on a model that takes no references", () => {
|
|
186
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", requested: "auto", supportsReferenceImages: false })).toBe("frame")
|
|
187
|
+
expect(resolveFrameDelivery({ provider: "wan-i2v", requested: "reference", supportsReferenceImages: false })).toBe("frame")
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -57,11 +57,20 @@ describe("VIDEO_REF_LIMITS_BY_PROVIDER ⇄ MODEL_CATALOG drift guard", () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
describe("VIDEO_REF_LIMITS_BY_PROVIDER caps for the newly-covered models", () => {
|
|
60
|
-
it("VEO
|
|
61
|
-
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3"]).toEqual({ images: 3 })
|
|
60
|
+
it("the ref-capable VEO SKUs carry images:3 (the REFERENCE_2_VIDEO slice cap in kie/video.ts)", () => {
|
|
62
61
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3.1"]).toEqual({ images: 3 })
|
|
63
62
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3_lite"]).toEqual({ images: 3 })
|
|
64
63
|
})
|
|
64
|
+
|
|
65
|
+
it("veo3 (VEO 3.1 QUALITY) carries NO cap — KIE serves reference-to-video on Fast/Lite only", () => {
|
|
66
|
+
// Its own words on a production job (2026-09-04, app-reports lane G):
|
|
67
|
+
// "Reference to video only supports the Veo Fast model and Veo Lite
|
|
68
|
+
// model." Until 2026-09-15 this entry claimed images:3, so every
|
|
69
|
+
// reference-carrying Quality run was a 422 after the credits were
|
|
70
|
+
// reserved. Absent ⇒ 0 everywhere the map is read.
|
|
71
|
+
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3"]).toBeUndefined()
|
|
72
|
+
expect(MODEL_CATALOG["veo3"]?.features).not.toContain("reference-image")
|
|
73
|
+
})
|
|
65
74
|
it("kling-3-omni and grok-i2v carry images:7", () => {
|
|
66
75
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["kling-3-omni"]).toEqual({ images: 7 })
|
|
67
76
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok-i2v"]).toEqual({ images: 7 })
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CharacterMotionMetadata } from "./character-motion-metadata.js"
|
|
1
2
|
/**
|
|
2
3
|
* Tag-free, policy-free wire shape for the `GET /v1/catalogs` projection — the
|
|
3
4
|
* server-driven, pack-composed catalog view thin clients render their own
|
|
@@ -21,6 +22,8 @@ export interface ProjectedCatalogOption {
|
|
|
21
22
|
*/
|
|
22
23
|
term?: string
|
|
23
24
|
icon?: string
|
|
25
|
+
/** Authored Character Motion prerequisites and sequence state. Missing means unknown. */
|
|
26
|
+
motion?: CharacterMotionMetadata
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface ProjectedCatalogDimension {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Structural public catalog API metadata. Authored values remain in @nodaro/prompts. */
|
|
2
|
+
export interface CharacterMotionMetadata {
|
|
3
|
+
/** Search terms, including previous display names. IDs remain stable. */
|
|
4
|
+
readonly aliases?: readonly string[]
|
|
5
|
+
/** Hidden from new choices; saved workflows still resolve this entry. */
|
|
6
|
+
readonly deprecated?: true
|
|
7
|
+
readonly replacementId?: string
|
|
8
|
+
/** Authored prerequisites; omission is unknown, never a compatibility claim. */
|
|
9
|
+
readonly requires?: readonly string[]
|
|
10
|
+
readonly startPose?: "standing" | "seated" | "floor" | "any"
|
|
11
|
+
readonly endPose?: "standing" | "seated" | "floor" | "any"
|
|
12
|
+
readonly endVisibility?: "in-frame" | "out-of-frame"
|
|
13
|
+
readonly handsAfter?: "free" | "occupied" | "holding-partner"
|
|
14
|
+
readonly needsFreeHands?: true
|
|
15
|
+
readonly kind?: "single" | "compound"
|
|
16
|
+
readonly fixedPace?: true
|
|
17
|
+
/** Non-human/dependent recipient substituted through the Partner handle. */
|
|
18
|
+
readonly counterpart?: string
|
|
19
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
RESOLUTION_DURATION_PRICING,
|
|
16
16
|
VEO_RESOLUTION_TIERED_PROVIDERS,
|
|
17
17
|
VIDEO_DURATION_TIERS,
|
|
18
|
-
|
|
18
|
+
pricedOutputDurationSec,
|
|
19
19
|
PRICING_DEFAULT_RESOLUTION,
|
|
20
20
|
MOTION_DURATION_TIERS,
|
|
21
21
|
T2I_TO_I2I_VARIANT,
|
|
@@ -27,8 +27,44 @@ import {
|
|
|
27
27
|
normalizeWan3Resolution,
|
|
28
28
|
getVideoAudioCapability,
|
|
29
29
|
} from "./model-constants.js"
|
|
30
|
-
import { isFlux2Model } from "./flux2-pricing.js"
|
|
31
|
-
import { MODEL_CATALOG, normalizeModelInput, type ModelInputAdjustment } from "./model-catalog.js"
|
|
30
|
+
import { isFlux2Model, FLUX2_RES_MP, type Flux2Model } from "./flux2-pricing.js"
|
|
31
|
+
import { MODEL_CATALOG, normalizeModelInput, defaultResolutionFor, type ModelInputAdjustment } from "./model-catalog.js"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The megapixel tier a Flux 2 credit identifier is keyed on, for ANY incoming
|
|
35
|
+
* `resolution` value.
|
|
36
|
+
*
|
|
37
|
+
* Flux 2 is the only family whose identifier INTERPOLATES the resolution
|
|
38
|
+
* instead of matching it against a known set, and its callers are not
|
|
39
|
+
* guaranteed to hand it a Flux 2 value:
|
|
40
|
+
* - the multi-provider cost preview prices one node's data against EVERY
|
|
41
|
+
* selected provider (`frontend/src/ee/hooks/use-providers-credits-sum.ts`),
|
|
42
|
+
* so a node whose `resolution` is "2K" (the flux / nano-banana-pro value
|
|
43
|
+
* space) reaches this branch verbatim;
|
|
44
|
+
* - node data written straight into workflow JSON by an agent, an import or
|
|
45
|
+
* a template never ran the config panel's provider-change fail-safe.
|
|
46
|
+
* Interpolating that produced the off-grid id `flux-2-pro:2KMP:0ref`, which no
|
|
47
|
+
* pricing row can answer — every cost badge 503'd `price_not_configured`
|
|
48
|
+
* (18 production app-reports, 2026-09-07..14).
|
|
49
|
+
*
|
|
50
|
+
* A value off the grid snaps to the model's DEFAULT tier — the same `preferred`
|
|
51
|
+
* value `normalizeModelInput` uses — so the preview asks for exactly the id the
|
|
52
|
+
* route will reserve (both routes and the orchestrator normalize through
|
|
53
|
+
* `resolveNormalizedImageGen` first, which makes this snap a no-op for them).
|
|
54
|
+
* An ABSENT resolution keeps its long-standing meaning ("this caller has no
|
|
55
|
+
* resolution dimension at all") and stays on 1 MP rather than moving to the
|
|
56
|
+
* model default, which would silently re-price every resolution-less node.
|
|
57
|
+
*/
|
|
58
|
+
function flux2MegapixelTier(model: Flux2Model, resolution?: string): string {
|
|
59
|
+
const bare = (v: string) => v.replace(/\s*MP$/i, "").trim()
|
|
60
|
+
const raw = typeof resolution === "string" ? bare(resolution) : ""
|
|
61
|
+
if (raw === "") return "1"
|
|
62
|
+
// Numeric match so "2.0"/" 2 MP" land on the same grid point as "2", and the
|
|
63
|
+
// GRID's own spelling is what gets emitted — returning the caller's "2.0"
|
|
64
|
+
// would build ":2.0MP:", another id no pricing row carries.
|
|
65
|
+
const tier = FLUX2_RES_MP.find((t) => Number(t) === Number(raw))
|
|
66
|
+
return tier ?? bare(defaultResolutionFor(model) ?? "1 MP")
|
|
67
|
+
}
|
|
32
68
|
|
|
33
69
|
/**
|
|
34
70
|
* Compute composite model identifier for variable credit pricing.
|
|
@@ -54,8 +90,7 @@ export function buildCreditModelIdentifier(
|
|
|
54
90
|
// their cost formula charges per input MP, so the reserved identifier must
|
|
55
91
|
// reflect refs (there is no metered true-up to correct an under-reserved tier).
|
|
56
92
|
if (isFlux2Model(provider)) {
|
|
57
|
-
|
|
58
|
-
return `${provider}:${mp}MP:${Math.min(referenceImageCount ?? 0, 8)}ref`
|
|
93
|
+
return `${provider}:${flux2MegapixelTier(provider, resolution)}MP:${Math.min(referenceImageCount ?? 0, 8)}ref`
|
|
59
94
|
}
|
|
60
95
|
if (HIGH_QUALITY_PROVIDERS.has(provider) && quality === "high") {
|
|
61
96
|
return `${provider}:high`
|
|
@@ -329,10 +364,10 @@ export function buildVideoCreditModelIdentifier(
|
|
|
329
364
|
|
|
330
365
|
// A named-provider request with NO duration renders the model's own default
|
|
331
366
|
// (kie/models.ts extraParams), so price that default — not the global 5s —
|
|
332
|
-
// for providers whose per-second tiers wouldn't snap 5 up to it
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const durationSec =
|
|
367
|
+
// for providers whose per-second tiers wouldn't snap 5 up to it
|
|
368
|
+
// (minimax-h3, seedance-2-5). One helper, shared with the dynamic
|
|
369
|
+
// reference-video reservations, so the tier and the scaled reserve agree.
|
|
370
|
+
const durationSec = pricedOutputDurationSec(effectiveProvider, duration)
|
|
336
371
|
const tiers = VIDEO_DURATION_TIERS[effectiveProvider]
|
|
337
372
|
if (!tiers) return effectiveProvider
|
|
338
373
|
|