@nodaro/shared 3.7.0 → 3.9.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 +324 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -20
- package/dist/index.d.ts +227 -20
- package/dist/index.js +307 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/model-tree.test.ts +3 -0
- 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/__tests__/suno-credit-type.test.ts +46 -0
- package/src/credit-identifiers.ts +20 -5
- package/src/i18n/camera-motions.ar.ts +5 -5
- package/src/i18n/camera-motions.de.ts +5 -5
- package/src/i18n/camera-motions.es.ts +5 -5
- package/src/i18n/camera-motions.fr.ts +5 -5
- package/src/i18n/camera-motions.he.ts +5 -5
- package/src/i18n/camera-motions.hi.ts +5 -5
- package/src/i18n/camera-motions.ja.ts +5 -5
- package/src/i18n/camera-motions.ko.ts +5 -5
- package/src/i18n/camera-motions.pt-BR.ts +5 -5
- package/src/i18n/camera-motions.ru.ts +5 -5
- package/src/i18n/camera-motions.zh-CN.ts +5 -5
- package/src/index.ts +10 -0
- package/src/model-catalog.ts +145 -3
- package/src/model-constants.ts +98 -13
- 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
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { description: "Empurra a câmera em direção ao sujeito (com paralaxe)" },
|
|
28
28
|
"dolly-out": { description: "Afasta a câmera (com paralaxe)" },
|
|
29
29
|
"dolly-zoom": { description: "Efeito vertigo: dolly oposto ao zoom" },
|
|
30
|
-
"push-in": { description: "Empurro
|
|
31
|
-
"pull-out": { description: "Afastamento
|
|
30
|
+
"push-in": { description: "Empurro rápido e enérgico em direção ao sujeito" },
|
|
31
|
+
"pull-out": { description: "Afastamento rápido e enérgico do sujeito" },
|
|
32
32
|
"breathing": { label: "Câmera Respirante", description: "Oscilação contínua e sutil de empurro e afastamento, sensação orgânica de câmera na mão" },
|
|
33
33
|
"push-pull": { label: "Push-Pull / Vai-e-Vem", description: "A câmera se move em direção ao sujeito e depois se afasta, aproximação e retirada oscilantes" },
|
|
34
34
|
"creep-in": { label: "Aproximação Imperceptível", description: "Empurro extremamente lento e imperceptível que constrói pavor ou tensão" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { description: "Quadro estático inclinado para criar tensão" },
|
|
49
49
|
|
|
50
50
|
// Orbit / Arc
|
|
51
|
-
"orbit-left": { description: "
|
|
52
|
-
"orbit-right": { description: "
|
|
51
|
+
"orbit-left": { description: "Órbita parcial ampla em torno do sujeito para a esquerda" },
|
|
52
|
+
"orbit-right": { description: "Órbita parcial ampla em torno do sujeito para a direita" },
|
|
53
53
|
"spin-360": { label: "Giro Completo 360°", description: "A câmera gira 360 graus completos sobre seu próprio eixo" },
|
|
54
54
|
"orbit-360": { label: "Órbita Completa 360°", description: "A câmera descreve um arco completo de 360 graus em torno do sujeito" },
|
|
55
55
|
"arc-left": { description: "Arco parcial em torno do sujeito pela esquerda" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { description: "Câmera na mão estilo vlog descontraído" },
|
|
89
89
|
"pov-walk": { description: "POV de caminhada em primeira pessoa" },
|
|
90
90
|
"velocity-edit": { description: "Pacing com speed-ramp estilo TikTok" },
|
|
91
|
-
"match-cut-zoom": { description: "Zoom
|
|
91
|
+
"match-cut-zoom": { description: "Zoom rápido com corte seco para uma forma correspondente" },
|
|
92
92
|
"screen-tap": { description: "Transição com toque na tela" },
|
|
93
93
|
"phone-flip": { description: "Troca entre câmera frontal e traseira" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { label: "Долли вперёд", description: "Толчок камеры к объекту (с параллаксом)" },
|
|
28
28
|
"dolly-out": { label: "Долли назад", description: "Отъезд камеры назад (с параллаксом)" },
|
|
29
29
|
"dolly-zoom": { label: "Долли-зум", description: "Эффект головокружения: долли против зума" },
|
|
30
|
-
"push-in": { label: "
|
|
31
|
-
"pull-out": { label: "
|
|
30
|
+
"push-in": { label: "Резкий наезд", description: "Быстрый энергичный наезд на объект" },
|
|
31
|
+
"pull-out": { label: "Резкий отъезд", description: "Быстрый энергичный отъезд от объекта" },
|
|
32
32
|
"breathing": { label: "Дышащая камера", description: "Тонкое непрерывное колебание наезд-отъезд, органичное ощущение съёмки с рук" },
|
|
33
33
|
"push-pull": { label: "Push-Pull / Качание", description: "Камера движется к объекту, затем обратно, маятниковое приближение и отдаление" },
|
|
34
34
|
"creep-in": { label: "Незаметный наезд", description: "Чрезвычайно медленный незаметный наезд, нагнетающий страх или напряжение" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { label: "Голландский угол", description: "Статичный наклонённый кадр для напряжения" },
|
|
49
49
|
|
|
50
50
|
// Orbit / Arc
|
|
51
|
-
"orbit-left": { label: "Орбита влево", description: "
|
|
52
|
-
"orbit-right": { label: "Орбита вправо", description: "
|
|
51
|
+
"orbit-left": { label: "Орбита влево", description: "Большая частичная орбита вокруг объекта влево" },
|
|
52
|
+
"orbit-right": { label: "Орбита вправо", description: "Большая частичная орбита вокруг объекта вправо" },
|
|
53
53
|
"spin-360": { label: "Полный оборот 360°", description: "Камера поворачивается на полные 360 градусов вокруг своей оси" },
|
|
54
54
|
"orbit-360": { label: "Полная орбита 360°", description: "Камера описывает полную дугу 360 градусов вокруг объекта" },
|
|
55
55
|
"arc-left": { label: "Дуга влево", description: "Частичная дуга вокруг объекта влево" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { label: "Влог с рук", description: "Непринуждённая ручная съёмка в стиле влога" },
|
|
89
89
|
"pov-walk": { label: "POV-ходьба", description: "POV-ходьба от первого лица" },
|
|
90
90
|
"velocity-edit": { label: "Скоростной монтаж", description: "Темп с ускорением в стиле TikTok" },
|
|
91
|
-
"match-cut-zoom": { label: "Зум для match cut", description: "
|
|
91
|
+
"match-cut-zoom": { label: "Зум для match cut", description: "Быстрый зум с резкой склейкой на совпадающую форму" },
|
|
92
92
|
"screen-tap": { label: "Касание экрана", description: "Переход с касанием пальца по экрану" },
|
|
93
93
|
"phone-flip": { label: "Переключение камеры телефона", description: "Переключение между фронтальной и основной камерой" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { label: "推轨", description: "推动机位靠近被摄对象(带视差)" },
|
|
28
28
|
"dolly-out": { label: "拉轨", description: "拉远机位远离被摄对象(带视差)" },
|
|
29
29
|
"dolly-zoom": { label: "滑动变焦", description: "眩晕效果:推轨与变焦反向" },
|
|
30
|
-
"push-in": { label: "
|
|
31
|
-
"pull-out": { label: "
|
|
30
|
+
"push-in": { label: "急推", description: "快速有力地推向被摄对象" },
|
|
31
|
+
"pull-out": { label: "急拉", description: "快速有力地拉远被摄对象" },
|
|
32
32
|
"breathing": { label: "呼吸式镜头", description: "细微而连续的推拉震荡,有机的手持质感" },
|
|
33
33
|
"push-pull": { label: "推拉/摇摆", description: "镜头先靠近主体再后退,摆动式的接近与撤离" },
|
|
34
34
|
"creep-in": { label: "渐近推近", description: "几乎察觉不到的极慢推近,营造恐惧或紧张感" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { label: "荷兰角", description: "营造紧张感的固定倾斜画面" },
|
|
49
49
|
|
|
50
50
|
// Orbit / Arc
|
|
51
|
-
"orbit-left": { label: "向左环绕", description: "
|
|
52
|
-
"orbit-right": { label: "向右环绕", description: "
|
|
51
|
+
"orbit-left": { label: "向左环绕", description: "镜头绕被摄对象向左大幅部分环绕" },
|
|
52
|
+
"orbit-right": { label: "向右环绕", description: "镜头绕被摄对象向右大幅部分环绕" },
|
|
53
53
|
"spin-360": { label: "360°整圈自转", description: "镜头围绕自身轴心整圈360度旋转" },
|
|
54
54
|
"orbit-360": { label: "360°整圈环绕", description: "镜头围绕主体整圈360度环行" },
|
|
55
55
|
"arc-left": { label: "左弧形运镜", description: "围绕被摄对象向左做局部弧线" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { label: "手持 Vlog", description: "随性的 Vlog 风格手持" },
|
|
89
89
|
"pov-walk": { label: "主观行走", description: "第一人称行走主观镜头" },
|
|
90
90
|
"velocity-edit": { label: "速度剪辑", description: "TikTok 节奏的速率变化" },
|
|
91
|
-
"match-cut-zoom": { label: "节拍变焦", description: "
|
|
91
|
+
"match-cut-zoom": { label: "节拍变焦", description: "快速变焦后硬切到相同形状" },
|
|
92
92
|
"screen-tap": { label: "点屏切换", description: "屏幕上手指点击触发的转场" },
|
|
93
93
|
"phone-flip": { label: "翻面切换", description: "前后摄像头翻转切换" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,12 @@ export {
|
|
|
89
89
|
AI_WRITER_PROVIDERS,
|
|
90
90
|
QA_CHECK_PROVIDERS,
|
|
91
91
|
SUNO_MODELS,
|
|
92
|
+
SUNO_ACTIVE_MODELS,
|
|
93
|
+
SUNO_LEGACY_MODELS,
|
|
94
|
+
DEFAULT_SUNO_MODEL,
|
|
95
|
+
SUNO_DURATION_MODELS,
|
|
96
|
+
isLegacySunoModel,
|
|
97
|
+
sunoModelHonoursDuration,
|
|
92
98
|
SUNO_ADD_TRACK_MODELS,
|
|
93
99
|
VOICE_DESIGN_MODELS,
|
|
94
100
|
DEFAULT_VOICE_DESIGN_MODEL,
|
|
@@ -247,6 +253,7 @@ export {
|
|
|
247
253
|
pricedVideoSelection,
|
|
248
254
|
buildMotionCreditModelIdentifier,
|
|
249
255
|
sunoCreditType,
|
|
256
|
+
SUNO_VERSION_CREDIT_KEYS,
|
|
250
257
|
SUNO_VERSION_PRICED_OPERATIONS,
|
|
251
258
|
SUNO_SELECT_OPERATIONS,
|
|
252
259
|
} from "./credit-identifiers.js"
|
|
@@ -1066,6 +1073,9 @@ export * from "./scene3d-v2-resources.js"
|
|
|
1066
1073
|
export * from "./scene3d-camera-track.js"
|
|
1067
1074
|
// --- 3D Render Pro: one durable operation, scene + video in one result ---
|
|
1068
1075
|
export * from "./pro-3d-render.js"
|
|
1076
|
+
// --- Scene3D render pricing: which frame-size tier a render settles at.
|
|
1077
|
+
// The SHAPE of the price (tier, multiplier, identifier); never a rate. ---
|
|
1078
|
+
export * from "./scene3d-render-pricing.js"
|
|
1069
1079
|
|
|
1070
1080
|
// --- transient studio keys — the public share read strips them ---
|
|
1071
1081
|
export {
|
package/src/model-catalog.ts
CHANGED
|
@@ -207,7 +207,7 @@ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
|
|
|
207
207
|
{ intent: "best cinematic video", modelIds: ["veo3", "kling-3.0", "seedance-2"], note: "VEO 3.1 Quality for premium narrative; Kling 3.0 for music-synced motion; Seedance 2 for reference-driven consistency." },
|
|
208
208
|
{ intent: "cheap batch video clips", modelIds: ["veo3.1", "wan-turbo", "bytedance-lite"], note: "VEO 3.1 Fast is the best price/quality balance with native audio." },
|
|
209
209
|
{ intent: "video with start + end frame", modelIds: ["veo3", "veo3.1", "kling-turbo", "minimax", "hailuo-standard", "seedance-2"], note: "All listed support an end frame; VEO uses imageUrls[start, end]." },
|
|
210
|
-
{ intent: "music / song generation", modelIds: ["suno-
|
|
210
|
+
{ intent: "music / song generation", modelIds: ["suno-v6", "suno-v6_wild", "suno-v6_mini", "suno-v5_5"], note: "V6 is the default flagship; V6 Wild for bolder, less predictable results; V6 Mini when speed matters; v5.5 / v5 / v4 keep their own character. Same price." },
|
|
211
211
|
{ intent: "voice over / narration", modelIds: ["elevenlabs-v3", "elevenlabs-turbo"], note: "v3 supports [audio tags] for emotion; Turbo is cheaper for plain narration." },
|
|
212
212
|
{ intent: "lip-sync a portrait to audio", modelIds: ["kling-avatar-pro", "kling-avatar", "infinitalk"], note: "Pro for best mouth shape; InfiniTalk for resolution control." },
|
|
213
213
|
{ intent: "transcription / captions", modelIds: ["elevenlabs-stt"], note: "Word-level timestamps available." },
|
|
@@ -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
|
|
@@ -2474,7 +2580,7 @@ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
2474
2580
|
label: "Suno v5",
|
|
2475
2581
|
series: "Suno",
|
|
2476
2582
|
dataValue: "V5",
|
|
2477
|
-
description: "
|
|
2583
|
+
description: "Suno v5 — better vocal quality than v4, more genres. Same price.",
|
|
2478
2584
|
useCases: ["music", "song", "vocals", "premium"],
|
|
2479
2585
|
pricing: [{ identifier: "suno-v5", credits: 30, note: "per generation" }],
|
|
2480
2586
|
},
|
|
@@ -2486,11 +2592,47 @@ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
2486
2592
|
label: "Suno v5.5",
|
|
2487
2593
|
series: "Suno",
|
|
2488
2594
|
dataValue: "V5_5",
|
|
2489
|
-
description: "Suno v5.5 —
|
|
2595
|
+
description: "Suno v5.5 — improved audio quality and expressiveness over v5.",
|
|
2490
2596
|
useCases: ["music", "song", "vocals", "premium"],
|
|
2491
2597
|
pricing: [{ identifier: "suno-v5_5", credits: 30, note: "per generation" }],
|
|
2598
|
+
},
|
|
2599
|
+
"suno-v6": {
|
|
2600
|
+
id: "suno-v6",
|
|
2601
|
+
kind: "audio",
|
|
2602
|
+
modes: ["music"] as const,
|
|
2603
|
+
family: "Suno",
|
|
2604
|
+
label: "Suno V6",
|
|
2605
|
+
series: "Suno",
|
|
2606
|
+
dataValue: "V6",
|
|
2607
|
+
description: "Suno V6 — greater musical expression with more natural vocals and richer details. The flagship and the default.",
|
|
2608
|
+
useCases: ["music", "song", "vocals", "premium"],
|
|
2609
|
+
pricing: [{ identifier: "suno-v6", credits: 30, note: "per generation" }],
|
|
2492
2610
|
featured: true,
|
|
2493
2611
|
},
|
|
2612
|
+
"suno-v6_wild": {
|
|
2613
|
+
id: "suno-v6_wild",
|
|
2614
|
+
kind: "audio",
|
|
2615
|
+
modes: ["music"] as const,
|
|
2616
|
+
family: "Suno",
|
|
2617
|
+
label: "Suno V6 Wild",
|
|
2618
|
+
series: "Suno",
|
|
2619
|
+
dataValue: "V6_WILD",
|
|
2620
|
+
description: "Suno V6 Wild — pushes creative boundaries for bolder, more distinctive musical expression; more varied, less predictable results.",
|
|
2621
|
+
useCases: ["music", "song", "vocals", "experimental"],
|
|
2622
|
+
pricing: [{ identifier: "suno-v6_wild", credits: 30, note: "per generation" }],
|
|
2623
|
+
},
|
|
2624
|
+
"suno-v6_mini": {
|
|
2625
|
+
id: "suno-v6_mini",
|
|
2626
|
+
kind: "audio",
|
|
2627
|
+
modes: ["music"] as const,
|
|
2628
|
+
family: "Suno",
|
|
2629
|
+
label: "Suno V6 Mini",
|
|
2630
|
+
series: "Suno",
|
|
2631
|
+
dataValue: "V6_MINI",
|
|
2632
|
+
description: "Suno V6 Mini — lightweight and fast, balancing quality and speed for effortless creation.",
|
|
2633
|
+
useCases: ["music", "song", "vocals", "fast"],
|
|
2634
|
+
pricing: [{ identifier: "suno-v6_mini", credits: 30, note: "per generation" }],
|
|
2635
|
+
},
|
|
2494
2636
|
}
|
|
2495
2637
|
|
|
2496
2638
|
// =============================================================================
|
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
|
|
@@ -111,10 +119,10 @@ export const VIDEO_PROMPT_MAX = 8000
|
|
|
111
119
|
|
|
112
120
|
/**
|
|
113
121
|
* Suno prompt / lyrics / content ceiling — the LARGEST any Suno version accepts
|
|
114
|
-
* in custom mode (
|
|
122
|
+
* in custom mode (V6 family and V4.5+ / V5 / V5.5 = 5000). NOT the route
|
|
115
123
|
* Zod bound — the routes bind at {@link SUNO_HARD_CEILING} and clamp to the
|
|
116
124
|
* per-version cap via {@link getMaxSunoPromptChars} (3000 non-custom for every
|
|
117
|
-
* version; in custom mode 3000 for V4/V3.5, 5000 for V4.5+/V5) before the job
|
|
125
|
+
* version; in custom mode 3000 for V4/V3.5, 5000 for V4.5+/V5/V6) before the job
|
|
118
126
|
* is built. This constant is shared with the editor `maxLength` / counter
|
|
119
127
|
* (warn-don't-block at the per-version cap). `style` and `title` have their
|
|
120
128
|
* own caps ({@link getMaxSunoStyleChars} / {@link SUNO_TITLE_MAX}).
|
|
@@ -302,9 +310,11 @@ export function getMaxTtsChars(provider: string | undefined): number {
|
|
|
302
310
|
* SUNO_TEXT_MAX} is 5000 today — the largest per-version prompt cap; the
|
|
303
311
|
* route Zod bound is {@link SUNO_HARD_CEILING}.)
|
|
304
312
|
* - prompt / lyrics: 3000 in non-custom mode (all versions); in custom mode
|
|
305
|
-
* 3000 for V4/V3.5 and 5000 for V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5
|
|
306
|
-
*
|
|
307
|
-
* -
|
|
313
|
+
* 3000 for V4/V3.5 and 5000 for V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5 and
|
|
314
|
+
* the whole V6 family (V6 / V6_WILD / V6_MINI).
|
|
315
|
+
* - style: 200 for V4/V3.5, 1000 for V4.5+ and V6.
|
|
316
|
+
* - title: 80 (all versions; the extend docs quote 100 for V6 — we keep the
|
|
317
|
+
* stricter generate cap so one value is right everywhere).
|
|
308
318
|
*/
|
|
309
319
|
export const SUNO_TITLE_MAX = 80
|
|
310
320
|
|
|
@@ -475,6 +485,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
475
485
|
// T2I providers that auto-route to their i2i sibling when refs are attached
|
|
476
486
|
"gpt-image",
|
|
477
487
|
"gpt-image-2",
|
|
488
|
+
"gpt-image-2-5-flare",
|
|
489
|
+
"gpt-image-2-5-sunburst",
|
|
478
490
|
"grok",
|
|
479
491
|
"grok-2",
|
|
480
492
|
"qwen",
|
|
@@ -487,6 +499,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
487
499
|
"nano-banana-edit",
|
|
488
500
|
"gpt-image-i2i",
|
|
489
501
|
"gpt-image-2-i2i",
|
|
502
|
+
"gpt-image-2-5-flare-i2i",
|
|
503
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
490
504
|
"grok-2-i2i",
|
|
491
505
|
"flux-i2i",
|
|
492
506
|
"flux-pro-i2i",
|
|
@@ -525,6 +539,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
|
|
|
525
539
|
export const T2I_TO_I2I_VARIANT: Record<string, string> = {
|
|
526
540
|
"gpt-image": "gpt-image-i2i",
|
|
527
541
|
"gpt-image-2": "gpt-image-2-i2i",
|
|
542
|
+
"gpt-image-2-5-flare": "gpt-image-2-5-flare-i2i",
|
|
543
|
+
"gpt-image-2-5-sunburst": "gpt-image-2-5-sunburst-i2i",
|
|
528
544
|
"grok": "grok-i2i",
|
|
529
545
|
// grok-2's t2i takes NO image input; its "i2i" is the segment-map(image_url)
|
|
530
546
|
// → image-edit(task_id) chain in the KIE provider (single reference).
|
|
@@ -560,6 +576,8 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
|
|
|
560
576
|
"nano-banana-edit": 8,
|
|
561
577
|
"gpt-image-i2i": 16,
|
|
562
578
|
"gpt-image-2-i2i": 16,
|
|
579
|
+
"gpt-image-2-5-flare-i2i": 16,
|
|
580
|
+
"gpt-image-2-5-sunburst-i2i": 16,
|
|
563
581
|
"flux-i2i": 4,
|
|
564
582
|
"flux-pro-i2i": 4,
|
|
565
583
|
"seedream-edit": 16,
|
|
@@ -625,6 +643,10 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
|
|
|
625
643
|
"gpt-image-i2i": "quality",
|
|
626
644
|
"gpt-image-2": "resolution",
|
|
627
645
|
"gpt-image-2-i2i": "resolution",
|
|
646
|
+
"gpt-image-2-5-flare": "resolution",
|
|
647
|
+
"gpt-image-2-5-flare-i2i": "resolution",
|
|
648
|
+
"gpt-image-2-5-sunburst": "resolution",
|
|
649
|
+
"gpt-image-2-5-sunburst-i2i": "resolution",
|
|
628
650
|
"nano-banana-pro": "resolution",
|
|
629
651
|
"nano-banana-2": "resolution",
|
|
630
652
|
"flux": "resolution",
|
|
@@ -658,6 +680,10 @@ export const RESOLUTION_2K_4K_TIERED_PROVIDERS = new Set([
|
|
|
658
680
|
"nano-banana-2",
|
|
659
681
|
"gpt-image-2",
|
|
660
682
|
"gpt-image-2-i2i",
|
|
683
|
+
"gpt-image-2-5-flare",
|
|
684
|
+
"gpt-image-2-5-flare-i2i",
|
|
685
|
+
"gpt-image-2-5-sunburst",
|
|
686
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
661
687
|
"wan-2.7",
|
|
662
688
|
"wan-2.7-pro",
|
|
663
689
|
])
|
|
@@ -680,6 +706,8 @@ export const IMAGE_GEN_PROVIDERS = [
|
|
|
680
706
|
"grok-2",
|
|
681
707
|
"gpt-image",
|
|
682
708
|
"gpt-image-2",
|
|
709
|
+
"gpt-image-2-5-flare",
|
|
710
|
+
"gpt-image-2-5-sunburst",
|
|
683
711
|
"imagen4",
|
|
684
712
|
"imagen4-fast",
|
|
685
713
|
"imagen4-ultra",
|
|
@@ -711,6 +739,8 @@ export const IMAGE_I2I_PROVIDERS = [
|
|
|
711
739
|
"flux-pro-i2i",
|
|
712
740
|
"gpt-image-i2i",
|
|
713
741
|
"gpt-image-2-i2i",
|
|
742
|
+
"gpt-image-2-5-flare-i2i",
|
|
743
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
714
744
|
"grok-2-i2i",
|
|
715
745
|
"ideogram-edit",
|
|
716
746
|
"ideogram-remix",
|
|
@@ -1201,19 +1231,72 @@ export const QA_CHECK_PROVIDERS = [
|
|
|
1201
1231
|
] as const
|
|
1202
1232
|
export type QaCheckProvider = typeof QA_CHECK_PROVIDERS[number]
|
|
1203
1233
|
|
|
1204
|
-
/**
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1234
|
+
/**
|
|
1235
|
+
* The current Suno generation (docs.kie.ai/suno-api/generate-music, 2026-09:
|
|
1236
|
+
* the V6 family), listed first in every picker; V6 is the default.
|
|
1237
|
+
*
|
|
1238
|
+
* - V6 — flagship: greater musical expression, more natural vocals, richer details.
|
|
1239
|
+
* - V6_WILD — bolder, more distinctive, less predictable output for exploration.
|
|
1240
|
+
* - V6_MINI — lightweight and fast; quality/speed balance.
|
|
1241
|
+
*
|
|
1242
|
+
* This is also the set that honours `duration` ({@link SUNO_DURATION_MODELS}).
|
|
1243
|
+
*/
|
|
1244
|
+
export const SUNO_ACTIVE_MODELS = ["V6", "V6_WILD", "V6_MINI"] as const
|
|
1245
|
+
export type SunoActiveModel = typeof SUNO_ACTIVE_MODELS[number]
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* The earlier Suno generations. Still offered in every picker as ordinary
|
|
1249
|
+
* choices (users report each has its own character), still accepted by every
|
|
1250
|
+
* route, and confirmed generating on 2026-09-11. KIE's docs tag them
|
|
1251
|
+
* "Discontinued"; if KIE ever stops serving one, coerce it to
|
|
1252
|
+
* {@link DEFAULT_SUNO_MODEL} at the provider send seam — never remove it from
|
|
1253
|
+
* {@link SUNO_MODELS}, which would 400 every saved music workflow.
|
|
1254
|
+
*/
|
|
1255
|
+
export const SUNO_LEGACY_MODELS = [
|
|
1256
|
+
"V5_5",
|
|
1257
|
+
"V5",
|
|
1208
1258
|
"V4_5PLUS",
|
|
1209
1259
|
"V4_5ALL",
|
|
1210
|
-
"
|
|
1211
|
-
"
|
|
1260
|
+
"V4_5",
|
|
1261
|
+
"V4",
|
|
1212
1262
|
] as const
|
|
1263
|
+
export type SunoLegacyModel = typeof SUNO_LEGACY_MODELS[number]
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Every Suno model version — the picker order, the route / MCP Zod enum and
|
|
1267
|
+
* the `data.model` type on Suno nodes. Current generation first (V6 = default),
|
|
1268
|
+
* then the earlier generations newest-first.
|
|
1269
|
+
*/
|
|
1270
|
+
export const SUNO_MODELS = [...SUNO_ACTIVE_MODELS, ...SUNO_LEGACY_MODELS] as const
|
|
1213
1271
|
export type SunoModel = typeof SUNO_MODELS[number]
|
|
1214
1272
|
|
|
1215
|
-
/** Suno
|
|
1216
|
-
export const
|
|
1273
|
+
/** The Suno version used when a caller omits `model` — KIE's own default. */
|
|
1274
|
+
export const DEFAULT_SUNO_MODEL = "V6" satisfies SunoActiveModel
|
|
1275
|
+
|
|
1276
|
+
export function isLegacySunoModel(model: string | undefined): model is SunoLegacyModel {
|
|
1277
|
+
return (SUNO_LEGACY_MODELS as readonly string[]).includes(model ?? "")
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Suno versions that HONOUR `duration` (custom mode only). Per the generate /
|
|
1282
|
+
* extend docs the field "will only take effect when the model is V6, V6_MINI,
|
|
1283
|
+
* or V6_WILD" — V5_5 was struck from that sentence when V6 shipped. The
|
|
1284
|
+
* provider client's send-gate (`sunoGenerate` in suno-client.ts) is the single
|
|
1285
|
+
* chokepoint that consults this set; the editor uses it only to decide whether
|
|
1286
|
+
* to SHOW the duration field.
|
|
1287
|
+
*/
|
|
1288
|
+
export const SUNO_DURATION_MODELS: ReadonlySet<string> = new Set<SunoModel>(SUNO_ACTIVE_MODELS)
|
|
1289
|
+
|
|
1290
|
+
export function sunoModelHonoursDuration(model: string | undefined): boolean {
|
|
1291
|
+
return SUNO_DURATION_MODELS.has(model ?? "")
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Suno models accepted by add-instrumental / add-vocals (docs enum: the V6
|
|
1296
|
+
* family plus V4_5PLUS / V5 / V5_5). Same current-then-earlier order as
|
|
1297
|
+
* {@link SUNO_MODELS}.
|
|
1298
|
+
*/
|
|
1299
|
+
export const SUNO_ADD_TRACK_MODELS = [...SUNO_ACTIVE_MODELS, "V4_5PLUS", "V5", "V5_5"] as const
|
|
1217
1300
|
export type SunoAddTrackModel = typeof SUNO_ADD_TRACK_MODELS[number]
|
|
1218
1301
|
|
|
1219
1302
|
/** Voice design models */
|
|
@@ -1250,6 +1333,8 @@ export const IMAGE_MASK_MODE: Record<ImageGenProvider, ImageMaskMode> = {
|
|
|
1250
1333
|
"nano-banana-2-lite": "prompt",
|
|
1251
1334
|
"gpt-image": "prompt",
|
|
1252
1335
|
"gpt-image-2": "prompt",
|
|
1336
|
+
"gpt-image-2-5-flare": "prompt",
|
|
1337
|
+
"gpt-image-2-5-sunburst": "prompt",
|
|
1253
1338
|
"seedream": "prompt",
|
|
1254
1339
|
"seedream-5-lite": "prompt",
|
|
1255
1340
|
"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.
|