@nodaro/shared 3.6.0 → 3.8.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 +265 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -9
- package/dist/index.d.ts +167 -9
- package/dist/index.js +254 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/normalize-model-input.test.ts +32 -0
- package/src/__tests__/scene3d-render-pricing.test.ts +157 -0
- package/src/__tests__/scene3d-v2.test.ts +4 -1
- package/src/image-overlay-layers.ts +10 -0
- package/src/index.ts +3 -0
- package/src/model-catalog.ts +106 -0
- package/src/model-constants.ts +30 -0
- package/src/node-default-mappings.ts +4 -0
- package/src/pipeline-types.ts +2 -0
- package/src/pro-3d-render.ts +55 -0
- package/src/reference-board-templates.ts +6 -1
- package/src/scene3d-render-pricing.ts +145 -0
- package/src/scene3d-v2.ts +3 -1
- package/src/scene3d.ts +19 -1
package/package.json
CHANGED
|
@@ -76,6 +76,38 @@ describe("normalizeModelInput", () => {
|
|
|
76
76
|
expect(normalizeModelInput("gpt-image-2", { aspectRatio: "1:1", resolution: "4K" }).resolution).toBe("2K")
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
// GPT Image 2.5 is NOT GPT Image 2. Its KIE docs
|
|
80
|
+
// (docs.kie.ai/market/gpt/gpt-image-2-5-*) state no aspect_ratio x resolution
|
|
81
|
+
// restriction, so the cross-field block above must NOT be widened to these
|
|
82
|
+
// ids "for consistency" — doing so would silently downgrade a paid 4K render
|
|
83
|
+
// to 1K. If GPT Image 2.5 ever documents such a limit, add it deliberately
|
|
84
|
+
// and change this test in the same commit.
|
|
85
|
+
it.each([
|
|
86
|
+
"gpt-image-2-5-flare",
|
|
87
|
+
"gpt-image-2-5-flare-i2i",
|
|
88
|
+
"gpt-image-2-5-sunburst",
|
|
89
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
90
|
+
])("%s has NO cross-field aspect-ratio x resolution rule", (modelId) => {
|
|
91
|
+
const auto4k = normalizeModelInput(modelId, { aspectRatio: "auto", resolution: "4K" })
|
|
92
|
+
expect(auto4k.resolution).toBe("4K")
|
|
93
|
+
expect(auto4k.adjustments).toEqual([])
|
|
94
|
+
|
|
95
|
+
const square4k = normalizeModelInput(modelId, { aspectRatio: "1:1", resolution: "4K" })
|
|
96
|
+
expect(square4k.resolution).toBe("4K")
|
|
97
|
+
expect(square4k.adjustments).toEqual([])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// The four ratios 2.5 introduced are new to the platform vocabulary; the snap
|
|
101
|
+
// must accept them rather than rewrite them to a neighbour.
|
|
102
|
+
it.each(["27:16", "16:27", "9:8", "8:9", "21:9", "3:2"])(
|
|
103
|
+
"gpt-image-2.5 keeps the newly-added ratio %s",
|
|
104
|
+
(ratio) => {
|
|
105
|
+
const out = normalizeModelInput("gpt-image-2-5-flare", { aspectRatio: ratio })
|
|
106
|
+
expect(out.aspectRatio).toBe(ratio)
|
|
107
|
+
expect(out.adjustments).toEqual([])
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
79
111
|
it("passes unknown model ids through untouched (the Zod enum owns those)", () => {
|
|
80
112
|
const out = normalizeModelInput("totally-fake-model", { aspectRatio: "21:9" })
|
|
81
113
|
expect(out.aspectRatio).toBe("21:9")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
PRO3D_RENDER_QUALITY_PROFILES,
|
|
4
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
5
|
+
SCENE3D_LIMITS,
|
|
6
|
+
SCENE3D_RENDER_BASE_MAX_PX,
|
|
7
|
+
SCENE3D_RENDER_TIERS,
|
|
8
|
+
SCENE3D_RENDER_TIER_MULTIPLIERS,
|
|
9
|
+
SCENE3D_RENDER_XLARGE_MIN_AREA_PX,
|
|
10
|
+
pro3DRenderFrameUnit,
|
|
11
|
+
renderVideoCreditId,
|
|
12
|
+
scene3DRenderTier,
|
|
13
|
+
scene3DRenderTierCredits,
|
|
14
|
+
} from "../index.js"
|
|
15
|
+
|
|
16
|
+
/** The three reference frames the tiers are anchored to (PR #1328). */
|
|
17
|
+
const REFERENCE_FRAMES = {
|
|
18
|
+
base: { width: 1920, height: 1080 },
|
|
19
|
+
large: { width: 2560, height: 1440 },
|
|
20
|
+
xlarge: { width: 2560, height: 2560 },
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
describe("scene3DRenderTier", () => {
|
|
24
|
+
it("places each reference frame in its own tier", () => {
|
|
25
|
+
for (const [tier, frame] of Object.entries(REFERENCE_FRAMES)) {
|
|
26
|
+
expect(scene3DRenderTier(frame.width, frame.height)).toBe(tier)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("holds every pre-cap frame at the base price, square ones included", () => {
|
|
31
|
+
// The promise of the 1920 gate: raising maxDimensionPx never reprices a
|
|
32
|
+
// frame that was already renderable. 1920x1920 has the SAME pixel count as
|
|
33
|
+
// 2560x1440 and is still base — area only decides ABOVE the gate.
|
|
34
|
+
expect(scene3DRenderTier(1920, 1080)).toBe("base")
|
|
35
|
+
expect(scene3DRenderTier(1080, 1920)).toBe("base")
|
|
36
|
+
expect(scene3DRenderTier(1920, 1920)).toBe("base")
|
|
37
|
+
expect(scene3DRenderTier(1680, 720)).toBe("base")
|
|
38
|
+
expect(SCENE3D_RENDER_BASE_MAX_PX).toBe(1920)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("tiers every aspect the 2560 cap makes reachable", () => {
|
|
42
|
+
expect(scene3DRenderTier(2560, 1440)).toBe("large") // 16:9
|
|
43
|
+
expect(scene3DRenderTier(1440, 2560)).toBe("large") // 9:16
|
|
44
|
+
expect(scene3DRenderTier(2560, 1097)).toBe("large") // 21:9
|
|
45
|
+
expect(scene3DRenderTier(2048, 2560)).toBe("xlarge") // 4:5
|
|
46
|
+
expect(scene3DRenderTier(2560, 2560)).toBe("xlarge") // 1:1
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("splits large from xlarge at the midpoint of the two upper reference frames", () => {
|
|
50
|
+
const midpoint = (REFERENCE_FRAMES.large.width * REFERENCE_FRAMES.large.height + REFERENCE_FRAMES.xlarge.width * REFERENCE_FRAMES.xlarge.height) / 2
|
|
51
|
+
expect(SCENE3D_RENDER_XLARGE_MIN_AREA_PX).toBe(midpoint)
|
|
52
|
+
// One pixel either side of the boundary, at a width past the gate.
|
|
53
|
+
expect(scene3DRenderTier(2560, 2000)).toBe("large") // 5,120,000 exactly
|
|
54
|
+
expect(scene3DRenderTier(2560, 2001)).toBe("xlarge") // 5,122,560
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("is base for anything that is not a pair of positive finite numbers", () => {
|
|
58
|
+
expect(scene3DRenderTier(undefined, undefined)).toBe("base")
|
|
59
|
+
expect(scene3DRenderTier("2560", "2560")).toBe("base")
|
|
60
|
+
expect(scene3DRenderTier(Number.NaN, 2560)).toBe("base")
|
|
61
|
+
expect(scene3DRenderTier(Infinity, 2560)).toBe("base")
|
|
62
|
+
expect(scene3DRenderTier(0, 2560)).toBe("base")
|
|
63
|
+
expect(scene3DRenderTier(-2560, -2560)).toBe("base")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("covers the whole admissible range with a declared tier", () => {
|
|
67
|
+
const max = SCENE3D_LIMITS.maxDimensionPx
|
|
68
|
+
for (const w of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
|
|
69
|
+
for (const h of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
|
|
70
|
+
expect(SCENE3D_RENDER_TIERS).toContain(scene3DRenderTier(w, h))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("scene3DRenderTierCredits", () => {
|
|
77
|
+
it("is the declared ratio, rounded up", () => {
|
|
78
|
+
// The worked examples the public docs print, from the built-in base of 50.
|
|
79
|
+
expect(scene3DRenderTierCredits(50, "base")).toBe(50)
|
|
80
|
+
expect(scene3DRenderTierCredits(50, "large")).toBe(75)
|
|
81
|
+
expect(scene3DRenderTierCredits(50, "xlarge")).toBe(125)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("never rounds a tier down below the base render it multiplies", () => {
|
|
85
|
+
for (const base of [1, 3, 7, 15, 33, 50, 137]) {
|
|
86
|
+
for (const tier of SCENE3D_RENDER_TIERS) {
|
|
87
|
+
expect(scene3DRenderTierCredits(base, tier)).toBeGreaterThanOrEqual(base)
|
|
88
|
+
expect(scene3DRenderTierCredits(base, tier)).toBe(
|
|
89
|
+
Math.ceil(base * SCENE3D_RENDER_TIER_MULTIPLIERS[tier]),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("keeps the ladder monotonic", () => {
|
|
96
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.base).toBe(1)
|
|
97
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.large).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.base)
|
|
98
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.xlarge).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.large)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("renderVideoCreditId", () => {
|
|
103
|
+
it("keeps the bare identifier for a base-sized 3D scene plan", () => {
|
|
104
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 1920, height: 1080 } })).toBe(
|
|
105
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("names the tier for a plan past the 1920 gate", () => {
|
|
110
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 1440 } })).toBe(
|
|
111
|
+
"render-video:3d-large",
|
|
112
|
+
)
|
|
113
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 2560 } })).toBe(
|
|
114
|
+
"render-video:3d-xlarge",
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("leaves every non-3D render on the flat identifier", () => {
|
|
119
|
+
// scene-graph admits frames up to 3840 today at the flat price; tiering it
|
|
120
|
+
// would raise the price of work people already run.
|
|
121
|
+
expect(renderVideoCreditId({ planType: "scene-graph", plan: { width: 3840, height: 2160 } })).toBe(
|
|
122
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
123
|
+
)
|
|
124
|
+
expect(renderVideoCreditId({ planType: "lottie-graphic", plan: { width: 2560, height: 2560 } })).toBe(
|
|
125
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
126
|
+
)
|
|
127
|
+
expect(renderVideoCreditId({ template: "slideshow", aspectRatio: "16:9" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("answers the flat identifier for anything unparseable", () => {
|
|
131
|
+
expect(renderVideoCreditId(undefined)).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
132
|
+
expect(renderVideoCreditId(null)).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
133
|
+
expect(renderVideoCreditId("3d-scene")).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
134
|
+
expect(renderVideoCreditId({ planType: "3d-scene" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
135
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: "{}" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
136
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: {} })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("pro3DRenderFrameUnit", () => {
|
|
141
|
+
it("keeps the configured spelling for base-sized frames", () => {
|
|
142
|
+
for (const quality of PRO3D_RENDER_QUALITY_PROFILES) {
|
|
143
|
+
expect(pro3DRenderFrameUnit(quality)).toBe(`pro-3d-render:render-frame:${quality}`)
|
|
144
|
+
expect(pro3DRenderFrameUnit(quality, "base")).toBe(`pro-3d-render:render-frame:${quality}`)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("suffixes the tier for a frame past the gate", () => {
|
|
149
|
+
expect(pro3DRenderFrameUnit("standard", "large")).toBe("pro-3d-render:render-frame:standard:large")
|
|
150
|
+
expect(pro3DRenderFrameUnit("standard", "xlarge")).toBe("pro-3d-render:render-frame:standard:xlarge")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("produces one distinct unit per tier", () => {
|
|
154
|
+
const units = new Set(SCENE3D_RENDER_TIERS.map((tier) => pro3DRenderFrameUnit("standard", tier)))
|
|
155
|
+
expect(units.size).toBe(SCENE3D_RENDER_TIERS.length)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -108,7 +108,10 @@ describe("scene3d v2 — v1 is untouched", () => {
|
|
|
108
108
|
expect(SCENE3D_LIMITS.maxObjects).toBe(100)
|
|
109
109
|
expect(SCENE3D_LIMITS.maxKeyframes).toBe(240)
|
|
110
110
|
expect(SCENE3D_LIMITS.maxHierarchyDepth).toBe(8)
|
|
111
|
-
|
|
111
|
+
// Widened from 1920 deliberately (measured, both versions together) —
|
|
112
|
+
// every other v1 bound above is still frozen.
|
|
113
|
+
expect(SCENE3D_LIMITS.maxDimensionPx).toBe(2560)
|
|
114
|
+
expect(SCENE3D_V2_LIMITS.maxDimensionPx).toBe(SCENE3D_LIMITS.maxDimensionPx)
|
|
112
115
|
expect(SCENE3D_LIMITS.maxReferences).toBe(8)
|
|
113
116
|
})
|
|
114
117
|
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod"
|
|
18
18
|
|
|
19
|
+
/** The nine points a layer attaches to on the base image. THE definition —
|
|
20
|
+
* the route's Zod, the compositor, the canvas, the SDK and the CLI all read
|
|
21
|
+
* this one (it used to be spelled out separately in each). */
|
|
22
|
+
export const OVERLAY_ANCHORS = [
|
|
23
|
+
"top-left", "top", "top-right",
|
|
24
|
+
"left", "center", "right",
|
|
25
|
+
"bottom-left", "bottom", "bottom-right",
|
|
26
|
+
] as const
|
|
27
|
+
export type OverlayAnchor = (typeof OVERLAY_ANCHORS)[number]
|
|
28
|
+
|
|
19
29
|
export const OVERLAY_LAYER_KINDS = ["image", "text", "qr", "shape"] as const
|
|
20
30
|
export type OverlayLayerKind = (typeof OVERLAY_LAYER_KINDS)[number]
|
|
21
31
|
|
package/src/index.ts
CHANGED
|
@@ -1066,6 +1066,9 @@ export * from "./scene3d-v2-resources.js"
|
|
|
1066
1066
|
export * from "./scene3d-camera-track.js"
|
|
1067
1067
|
// --- 3D Render Pro: one durable operation, scene + video in one result ---
|
|
1068
1068
|
export * from "./pro-3d-render.js"
|
|
1069
|
+
// --- Scene3D render pricing: which frame-size tier a render settles at.
|
|
1070
|
+
// The SHAPE of the price (tier, multiplier, identifier); never a rate. ---
|
|
1071
|
+
export * from "./scene3d-render-pricing.js"
|
|
1069
1072
|
|
|
1070
1073
|
// --- transient studio keys — the public share read strips them ---
|
|
1071
1074
|
export {
|
package/src/model-catalog.ts
CHANGED
|
@@ -233,6 +233,15 @@ const KONTEXT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"] as const
|
|
|
233
233
|
const GROK_RATIOS = ["1:1", "16:9", "9:16", "3:2", "2:3"] as const
|
|
234
234
|
const GPT_IMAGE_RATIOS = ["1:1", "3:2", "2:3"] as const
|
|
235
235
|
const GPT_IMAGE_2_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4"] as const
|
|
236
|
+
// GPT Image 2.5 (Flare + Sunburst) widen the set to thirteen — the GPT Image 2
|
|
237
|
+
// six plus 3:2/2:3, ultra-wide 21:9/27:16, ultra-tall 16:27 and near-square
|
|
238
|
+
// 9:8/8:9 (docs.kie.ai/market/gpt/gpt-image-2-5-*). Unlike GPT Image 2, the 2.5
|
|
239
|
+
// docs state NO aspect-ratio x resolution restriction, so no cross-field
|
|
240
|
+
// constraint is registered for these ids in normalizeModelInput below.
|
|
241
|
+
const GPT_IMAGE_2_5_RATIOS = [
|
|
242
|
+
"auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16",
|
|
243
|
+
"21:9", "27:16", "16:27", "9:8", "8:9",
|
|
244
|
+
] as const
|
|
236
245
|
const IMAGEN4_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
|
|
237
246
|
const IDEOGRAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
|
|
238
247
|
const SEEDREAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"] as const
|
|
@@ -591,6 +600,103 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
591
600
|
],
|
|
592
601
|
},
|
|
593
602
|
|
|
603
|
+
"gpt-image-2-5-flare": {
|
|
604
|
+
id: "gpt-image-2-5-flare",
|
|
605
|
+
kind: "image",
|
|
606
|
+
modes: ["t2i"] as const,
|
|
607
|
+
family: "OpenAI",
|
|
608
|
+
label: "GPT Image 2.5 Flare",
|
|
609
|
+
series: "GPT Image",
|
|
610
|
+
description: "Fast everyday GPT Image 2.5 - higher quality than GPT Image 2 at about half the latency. The default of the pair: social and creator content, campaign variants, thumbnails, rapid iteration, high-volume work.",
|
|
611
|
+
useCases: ["typography", "high-res", "general", "draft"],
|
|
612
|
+
features: ["reference-image"],
|
|
613
|
+
aspectRatios: GPT_IMAGE_2_5_RATIOS,
|
|
614
|
+
resolutions: ["1K", "2K", "4K"],
|
|
615
|
+
pricing: [
|
|
616
|
+
{ identifier: "gpt-image-2-5-flare", credits: 15, note: "1K default" },
|
|
617
|
+
{ identifier: "gpt-image-2-5-flare:2K", credits: 25, note: "2K" },
|
|
618
|
+
{ identifier: "gpt-image-2-5-flare:4K", credits: 40, note: "4K" },
|
|
619
|
+
],
|
|
620
|
+
valueLabels: {
|
|
621
|
+
"3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
|
|
622
|
+
"27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
|
|
623
|
+
"9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
|
|
624
|
+
},
|
|
625
|
+
safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
|
|
626
|
+
},
|
|
627
|
+
"gpt-image-2-5-flare-i2i": {
|
|
628
|
+
id: "gpt-image-2-5-flare-i2i",
|
|
629
|
+
kind: "image",
|
|
630
|
+
modes: ["i2i"] as const,
|
|
631
|
+
family: "OpenAI",
|
|
632
|
+
label: "GPT Image 2.5 Flare (I2I)",
|
|
633
|
+
series: "GPT Image",
|
|
634
|
+
description: "Fast GPT Image 2.5 edits (up to 16 source images) - the default when you are iterating rather than finishing.",
|
|
635
|
+
useCases: ["edit", "high-res"],
|
|
636
|
+
features: ["reference-image"],
|
|
637
|
+
aspectRatios: GPT_IMAGE_2_5_RATIOS,
|
|
638
|
+
resolutions: ["1K", "2K", "4K"],
|
|
639
|
+
pricing: [
|
|
640
|
+
{ identifier: "gpt-image-2-5-flare-i2i", credits: 15, note: "1K default" },
|
|
641
|
+
{ identifier: "gpt-image-2-5-flare-i2i:2K", credits: 25, note: "2K" },
|
|
642
|
+
{ identifier: "gpt-image-2-5-flare-i2i:4K", credits: 40, note: "4K" },
|
|
643
|
+
],
|
|
644
|
+
valueLabels: {
|
|
645
|
+
"3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
|
|
646
|
+
"27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
|
|
647
|
+
"9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
|
|
648
|
+
},
|
|
649
|
+
safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
|
|
650
|
+
},
|
|
651
|
+
"gpt-image-2-5-sunburst": {
|
|
652
|
+
id: "gpt-image-2-5-sunburst",
|
|
653
|
+
kind: "image",
|
|
654
|
+
modes: ["t2i"] as const,
|
|
655
|
+
family: "OpenAI",
|
|
656
|
+
label: "GPT Image 2.5 Sunburst",
|
|
657
|
+
series: "GPT Image",
|
|
658
|
+
description: "Precision GPT Image 2.5 - trades generation time for tighter control and detail fidelity. Pick it for brand-sensitive and production work: packaging, diagrams, ecommerce retouching, polished campaign creative.",
|
|
659
|
+
useCases: ["typography", "high-res", "general"],
|
|
660
|
+
features: ["reference-image"],
|
|
661
|
+
aspectRatios: GPT_IMAGE_2_5_RATIOS,
|
|
662
|
+
resolutions: ["1K", "2K", "4K"],
|
|
663
|
+
pricing: [
|
|
664
|
+
{ identifier: "gpt-image-2-5-sunburst", credits: 15, note: "1K default" },
|
|
665
|
+
{ identifier: "gpt-image-2-5-sunburst:2K", credits: 25, note: "2K" },
|
|
666
|
+
{ identifier: "gpt-image-2-5-sunburst:4K", credits: 40, note: "4K" },
|
|
667
|
+
],
|
|
668
|
+
valueLabels: {
|
|
669
|
+
"3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
|
|
670
|
+
"27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
|
|
671
|
+
"9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
|
|
672
|
+
},
|
|
673
|
+
safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
|
|
674
|
+
},
|
|
675
|
+
"gpt-image-2-5-sunburst-i2i": {
|
|
676
|
+
id: "gpt-image-2-5-sunburst-i2i",
|
|
677
|
+
kind: "image",
|
|
678
|
+
modes: ["i2i"] as const,
|
|
679
|
+
family: "OpenAI",
|
|
680
|
+
label: "GPT Image 2.5 Sunburst (I2I)",
|
|
681
|
+
series: "GPT Image",
|
|
682
|
+
description: "Precision GPT Image 2.5 edits (up to 16 source images) - the most controlled edit in the GPT family, at the cost of a longer run.",
|
|
683
|
+
useCases: ["edit", "high-res"],
|
|
684
|
+
features: ["reference-image"],
|
|
685
|
+
aspectRatios: GPT_IMAGE_2_5_RATIOS,
|
|
686
|
+
resolutions: ["1K", "2K", "4K"],
|
|
687
|
+
pricing: [
|
|
688
|
+
{ identifier: "gpt-image-2-5-sunburst-i2i", credits: 15, note: "1K default" },
|
|
689
|
+
{ identifier: "gpt-image-2-5-sunburst-i2i:2K", credits: 25, note: "2K" },
|
|
690
|
+
{ identifier: "gpt-image-2-5-sunburst-i2i:4K", credits: 40, note: "4K" },
|
|
691
|
+
],
|
|
692
|
+
valueLabels: {
|
|
693
|
+
"3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
|
|
694
|
+
"27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
|
|
695
|
+
"9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
|
|
696
|
+
},
|
|
697
|
+
safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
|
|
698
|
+
},
|
|
699
|
+
|
|
594
700
|
// ── Ideogram ──
|
|
595
701
|
// Ideogram models use a `rendering_speed` (TURBO / BALANCED / QUALITY)
|
|
596
702
|
// dimension that's distinct from the `quality` lever — frontend exposes
|
package/src/model-constants.ts
CHANGED
|
@@ -44,6 +44,9 @@ export const IMAGE_ASPECT_RATIO_VALUES = [
|
|
|
44
44
|
// Ultra-wide / ultra-tall banner ratios: Wan 2.7 + Wan 2.7 Pro (8:1, 1:8)
|
|
45
45
|
// and Nano Banana 2 Lite (4:1, 1:4, 8:1, 1:8).
|
|
46
46
|
"4:1", "1:4", "8:1", "1:8",
|
|
47
|
+
// GPT Image 2.5 (Flare + Sunburst) additions: near-square and ultra-wide/tall
|
|
48
|
+
// cinema ratios (docs.kie.ai/market/gpt/gpt-image-2-5-*).
|
|
49
|
+
"27:16", "16:27", "9:8", "8:9",
|
|
47
50
|
] as const
|
|
48
51
|
|
|
49
52
|
/** A ratio string the image routes accept (pre-snap vocabulary, not a per-model guarantee). */
|
|
@@ -76,6 +79,11 @@ export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
|
|
|
76
79
|
"nano-banana-2-lite": 20000, // docs.kie.ai/market/google/nano-banana-2-lite
|
|
77
80
|
"nano-banana-pro": 20000, // docs.kie.ai/market/google/pro-image-to-image
|
|
78
81
|
"gpt-image-2-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-image-to-image
|
|
82
|
+
// GPT Image 2.5 — all four lanes document a 20000-char prompt ceiling.
|
|
83
|
+
"gpt-image-2-5-flare": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-flare-text-to-image
|
|
84
|
+
"gpt-image-2-5-flare-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-flare-image-to-image
|
|
85
|
+
"gpt-image-2-5-sunburst": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-sunburst-text-to-image
|
|
86
|
+
"gpt-image-2-5-sunburst-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-sunburst-image-to-image
|
|
79
87
|
// ── lower than the 5000 default (over-send risk if left at default) ──
|
|
80
88
|
"seedream": 3000, // docs.kie.ai/market/seedream/4-5-text-to-image
|
|
81
89
|
"seedream-edit": 3000, // docs.kie.ai/market/seedream/4-5-edit
|
|
@@ -475,6 +483,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
475
483
|
// T2I providers that auto-route to their i2i sibling when refs are attached
|
|
476
484
|
"gpt-image",
|
|
477
485
|
"gpt-image-2",
|
|
486
|
+
"gpt-image-2-5-flare",
|
|
487
|
+
"gpt-image-2-5-sunburst",
|
|
478
488
|
"grok",
|
|
479
489
|
"grok-2",
|
|
480
490
|
"qwen",
|
|
@@ -487,6 +497,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
487
497
|
"nano-banana-edit",
|
|
488
498
|
"gpt-image-i2i",
|
|
489
499
|
"gpt-image-2-i2i",
|
|
500
|
+
"gpt-image-2-5-flare-i2i",
|
|
501
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
490
502
|
"grok-2-i2i",
|
|
491
503
|
"flux-i2i",
|
|
492
504
|
"flux-pro-i2i",
|
|
@@ -525,6 +537,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
525
537
|
export const T2I_TO_I2I_VARIANT: Record<string, string> = {
|
|
526
538
|
"gpt-image": "gpt-image-i2i",
|
|
527
539
|
"gpt-image-2": "gpt-image-2-i2i",
|
|
540
|
+
"gpt-image-2-5-flare": "gpt-image-2-5-flare-i2i",
|
|
541
|
+
"gpt-image-2-5-sunburst": "gpt-image-2-5-sunburst-i2i",
|
|
528
542
|
"grok": "grok-i2i",
|
|
529
543
|
// grok-2's t2i takes NO image input; its "i2i" is the segment-map(image_url)
|
|
530
544
|
// → image-edit(task_id) chain in the KIE provider (single reference).
|
|
@@ -560,6 +574,8 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
|
|
|
560
574
|
"nano-banana-edit": 8,
|
|
561
575
|
"gpt-image-i2i": 16,
|
|
562
576
|
"gpt-image-2-i2i": 16,
|
|
577
|
+
"gpt-image-2-5-flare-i2i": 16,
|
|
578
|
+
"gpt-image-2-5-sunburst-i2i": 16,
|
|
563
579
|
"flux-i2i": 4,
|
|
564
580
|
"flux-pro-i2i": 4,
|
|
565
581
|
"seedream-edit": 16,
|
|
@@ -625,6 +641,10 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
|
|
|
625
641
|
"gpt-image-i2i": "quality",
|
|
626
642
|
"gpt-image-2": "resolution",
|
|
627
643
|
"gpt-image-2-i2i": "resolution",
|
|
644
|
+
"gpt-image-2-5-flare": "resolution",
|
|
645
|
+
"gpt-image-2-5-flare-i2i": "resolution",
|
|
646
|
+
"gpt-image-2-5-sunburst": "resolution",
|
|
647
|
+
"gpt-image-2-5-sunburst-i2i": "resolution",
|
|
628
648
|
"nano-banana-pro": "resolution",
|
|
629
649
|
"nano-banana-2": "resolution",
|
|
630
650
|
"flux": "resolution",
|
|
@@ -658,6 +678,10 @@ export const RESOLUTION_2K_4K_TIERED_PROVIDERS = new Set([
|
|
|
658
678
|
"nano-banana-2",
|
|
659
679
|
"gpt-image-2",
|
|
660
680
|
"gpt-image-2-i2i",
|
|
681
|
+
"gpt-image-2-5-flare",
|
|
682
|
+
"gpt-image-2-5-flare-i2i",
|
|
683
|
+
"gpt-image-2-5-sunburst",
|
|
684
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
661
685
|
"wan-2.7",
|
|
662
686
|
"wan-2.7-pro",
|
|
663
687
|
])
|
|
@@ -680,6 +704,8 @@ export const IMAGE_GEN_PROVIDERS = [
|
|
|
680
704
|
"grok-2",
|
|
681
705
|
"gpt-image",
|
|
682
706
|
"gpt-image-2",
|
|
707
|
+
"gpt-image-2-5-flare",
|
|
708
|
+
"gpt-image-2-5-sunburst",
|
|
683
709
|
"imagen4",
|
|
684
710
|
"imagen4-fast",
|
|
685
711
|
"imagen4-ultra",
|
|
@@ -711,6 +737,8 @@ export const IMAGE_I2I_PROVIDERS = [
|
|
|
711
737
|
"flux-pro-i2i",
|
|
712
738
|
"gpt-image-i2i",
|
|
713
739
|
"gpt-image-2-i2i",
|
|
740
|
+
"gpt-image-2-5-flare-i2i",
|
|
741
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
714
742
|
"grok-2-i2i",
|
|
715
743
|
"ideogram-edit",
|
|
716
744
|
"ideogram-remix",
|
|
@@ -1250,6 +1278,8 @@ export const IMAGE_MASK_MODE: Record<ImageGenProvider, ImageMaskMode> = {
|
|
|
1250
1278
|
"nano-banana-2-lite": "prompt",
|
|
1251
1279
|
"gpt-image": "prompt",
|
|
1252
1280
|
"gpt-image-2": "prompt",
|
|
1281
|
+
"gpt-image-2-5-flare": "prompt",
|
|
1282
|
+
"gpt-image-2-5-sunburst": "prompt",
|
|
1253
1283
|
"seedream": "prompt",
|
|
1254
1284
|
"seedream-5-lite": "prompt",
|
|
1255
1285
|
"seedream-5-pro": "prompt",
|
|
@@ -167,6 +167,10 @@ const QUALITY_MAP: Record<string, QualityMapping> = {
|
|
|
167
167
|
"flux-pro-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "2K" } },
|
|
168
168
|
"gpt-image-2": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
169
169
|
"gpt-image-2-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
170
|
+
"gpt-image-2-5-flare": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
171
|
+
"gpt-image-2-5-flare-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
172
|
+
"gpt-image-2-5-sunburst": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
173
|
+
"gpt-image-2-5-sunburst-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
|
|
170
174
|
// Image gen — quality-style (medium/high or basic/high)
|
|
171
175
|
"gpt-image": { field: "quality", values: { low: "medium", mid: "medium", high: "high" } },
|
|
172
176
|
"gpt-image-i2i": { field: "quality", values: { low: "medium", mid: "medium", high: "high" } },
|
package/src/pipeline-types.ts
CHANGED
package/src/pro-3d-render.ts
CHANGED
|
@@ -271,6 +271,23 @@ export interface Pro3DRenderValidationWarning {
|
|
|
271
271
|
shotId?: string
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* One still per shot of the exported composition.
|
|
276
|
+
*
|
|
277
|
+
* A render's contact sheet: the frame a shot OPENS on, which is the frame that
|
|
278
|
+
* says what the shot is of. `shotIndex` is the 0-based position in the v2
|
|
279
|
+
* composition's `shots` array — a v1 (single-shot) scene has exactly one still,
|
|
280
|
+
* index 0 at frame 0 — and `frame` is that shot's own first frame in the
|
|
281
|
+
* composition's frame space, so a caller can line a still up against the MP4
|
|
282
|
+
* without re-deriving shot boundaries.
|
|
283
|
+
*/
|
|
284
|
+
export interface Pro3DRenderShotStill {
|
|
285
|
+
shotIndex: number
|
|
286
|
+
frame: number
|
|
287
|
+
assetId: string
|
|
288
|
+
url: string
|
|
289
|
+
}
|
|
290
|
+
|
|
274
291
|
export interface Pro3DRenderResultMetadata {
|
|
275
292
|
width: number
|
|
276
293
|
height: number
|
|
@@ -299,6 +316,12 @@ export interface Pro3DRenderJobOutput {
|
|
|
299
316
|
scenePlan: Scene3DPlan
|
|
300
317
|
sceneRevisionId: string
|
|
301
318
|
posterAssetId: string
|
|
319
|
+
/**
|
|
320
|
+
* One still per shot, ordered by `shotIndex`. Optional and additive: a
|
|
321
|
+
* runtime that does not render stills yet returns a complete result without
|
|
322
|
+
* them, and a result that HAS them has one for every shot.
|
|
323
|
+
*/
|
|
324
|
+
shotStills?: Pro3DRenderShotStill[]
|
|
302
325
|
/** Present when an editable native source was retained for this revision. */
|
|
303
326
|
sourceArtifactId?: string
|
|
304
327
|
validation: {
|
|
@@ -324,12 +347,22 @@ export interface Pro3DRenderJobOutput {
|
|
|
324
347
|
* them to satisfy the schema; a runtime that has not produced them yet simply
|
|
325
348
|
* does not parse as complete, which is the honest answer.
|
|
326
349
|
*/
|
|
350
|
+
export const pro3DRenderShotStillSchema = z
|
|
351
|
+
.object({
|
|
352
|
+
shotIndex: z.number().int().min(0),
|
|
353
|
+
frame: z.number().int().min(0),
|
|
354
|
+
assetId: z.string().min(1),
|
|
355
|
+
url: z.string().min(1),
|
|
356
|
+
})
|
|
357
|
+
.passthrough()
|
|
358
|
+
|
|
327
359
|
export const pro3DRenderJobOutputSchema = z
|
|
328
360
|
.object({
|
|
329
361
|
videoUrl: z.string().min(1),
|
|
330
362
|
scenePlan: scene3DAnyPlanSchema,
|
|
331
363
|
sceneRevisionId: z.string().min(1),
|
|
332
364
|
posterAssetId: z.string().min(1),
|
|
365
|
+
shotStills: z.array(pro3DRenderShotStillSchema).optional(),
|
|
333
366
|
sourceArtifactId: z.string().min(1).optional(),
|
|
334
367
|
validation: z
|
|
335
368
|
.object({
|
|
@@ -364,6 +397,28 @@ export function isPro3DRenderJobOutput(value: unknown): value is Pro3DRenderJobO
|
|
|
364
397
|
return pro3DRenderJobOutputSchema.safeParse(value).success
|
|
365
398
|
}
|
|
366
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The stills on a result, in shot order — the ONE reader every surface uses.
|
|
402
|
+
*
|
|
403
|
+
* Tolerant on purpose: this runs on canvas node data, on a job row read back
|
|
404
|
+
* from the database and on an MCP result envelope, and in every one of those a
|
|
405
|
+
* missing or malformed list means "this result has no stills", never "refuse
|
|
406
|
+
* the whole result". Sorting here is what lets `shotStills[i]` mean shot `i`
|
|
407
|
+
* at every call site without each one remembering to sort.
|
|
408
|
+
*/
|
|
409
|
+
export function pro3DRenderShotStills(output: unknown): Pro3DRenderShotStill[] {
|
|
410
|
+
const list = (output as { shotStills?: unknown } | null | undefined)?.shotStills
|
|
411
|
+
if (!Array.isArray(list)) return []
|
|
412
|
+
const parsed = list.flatMap((entry) => {
|
|
413
|
+
const result = pro3DRenderShotStillSchema.safeParse(entry)
|
|
414
|
+
return result.success
|
|
415
|
+
? [{ shotIndex: result.data.shotIndex, frame: result.data.frame,
|
|
416
|
+
assetId: result.data.assetId, url: result.data.url }]
|
|
417
|
+
: []
|
|
418
|
+
})
|
|
419
|
+
return parsed.sort((a, b) => a.shotIndex - b.shotIndex)
|
|
420
|
+
}
|
|
421
|
+
|
|
367
422
|
/**
|
|
368
423
|
* The two fields every EXECUTION SURFACE must be able to resolve, whatever
|
|
369
424
|
* else a runtime does or does not attach yet.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** Gen providers for the board: strict subset of IMAGE_GEN_PROVIDERS that
|
|
2
2
|
* renders legible in-image text + accepts reference conditioning. */
|
|
3
|
-
export const REFERENCE_BOARD_PROVIDERS = [
|
|
3
|
+
export const REFERENCE_BOARD_PROVIDERS = [
|
|
4
|
+
"nano-banana-pro",
|
|
5
|
+
"gpt-image-2",
|
|
6
|
+
"gpt-image-2-5-flare",
|
|
7
|
+
"gpt-image-2-5-sunburst",
|
|
8
|
+
] as const
|
|
4
9
|
export type ReferenceBoardProvider = (typeof REFERENCE_BOARD_PROVIDERS)[number]
|
|
5
10
|
|
|
6
11
|
export type BoardEntityKind = "character" | "location" | "object"
|