@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.
@@ -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 lento e sutil em direção ao sujeito" },
31
- "pull-out": { description: "Afastamento lento e sutil do sujeito" },
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: "Volta completa em torno do sujeito para a esquerda" },
52
- "orbit-right": { description: "Volta completa em torno do sujeito para a direita" },
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 no tempo de batida para corte" },
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: "Лёгкий наезд", description: "Медленный мягкий наезд на объект" },
31
- "pull-out": { label: "Лёгкий отъезд", description: "Медленный мягкий отъезд от объекта" },
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: "缓推", description: "缓慢轻柔地推向被摄对象" },
31
- "pull-out": { label: "缓拉", description: "缓慢轻柔地拉远被摄对象" },
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 {
@@ -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-v5", "suno"], note: "Suno v5 has better vocal quality at the same price." },
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: "Newer Suno v5 — better vocal quality, more genres. Same price as v4.",
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 — latest model with improved audio quality and expressiveness.",
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
  // =============================================================================
@@ -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 (V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5 = 5000). NOT the route
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
- * - style: 200 for V4/V3.5, 1000 for V4.5+.
307
- * - title: 80 (all versions).
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
- /** Suno model versions */
1205
- export const SUNO_MODELS = [
1206
- "V4",
1207
- "V4_5",
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
- "V5",
1211
- "V5_5",
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 models that support add-instrumental / add-vocals operations */
1216
- export const SUNO_ADD_TRACK_MODELS = ["V4_5PLUS", "V5", "V5_5"] as 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" } },
@@ -54,6 +54,8 @@ export const PIPELINE_PINNABLE_IMAGE_MODELS = [
54
54
  "flux",
55
55
  "gpt-image",
56
56
  "gpt-image-2",
57
+ "gpt-image-2-5-flare",
58
+ "gpt-image-2-5-sunburst",
57
59
  ] as const
58
60
  export type PipelinePinnableImageModel = (typeof PIPELINE_PINNABLE_IMAGE_MODELS)[number]
59
61
 
@@ -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.