@nodaro/shared 2.1.0 → 2.2.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.
@@ -89,11 +89,17 @@ export const SUNO_TEXT_MAX = 5000
89
89
  * video routes' Zod schemas. The PER-MODEL limits below (and the editor warning)
90
90
  * do the real work; the routes stay generous so they never hard-reject a legit
91
91
  * long prompt — the assembler/payload truncates to the model cap as a graceful
92
- * safety net (warn-don't-block). 20000 = the largest verified per-model image/
93
- * video limit (seedance-2 / nano-banana-2 / gemini-omni). Anything past this is
94
- * abuse, not a real prompt.
92
+ * safety net (warn-don't-block). 30000 = the largest verified per-model image/
93
+ * video limit (seedance-2-5; seedance-2 / nano-banana-2 / gemini-omni sit at
94
+ * 20000). Anything past this is abuse, not a real prompt.
95
+ *
96
+ * This MUST stay >= the largest entry in MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER /
97
+ * MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER — otherwise the route's Zod hard-rejects a
98
+ * prompt the model itself would have accepted, which is exactly the
99
+ * block-instead-of-warn behaviour this ceiling exists to avoid. A drift guard in
100
+ * `__tests__/prompt-length-limits.test.ts` enforces that relationship.
95
101
  */
96
- export const PROMPT_HARD_CEILING = 20000
102
+ export const PROMPT_HARD_CEILING = 30000
97
103
 
98
104
  /**
99
105
  * Ceiling for LLM TEXT-generation node inputs (the "Generate Text"/llm-chat node
@@ -129,10 +135,16 @@ export const MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
129
135
  "seedance-2-fast": 20000,
130
136
  "seedance-2-mini": 20000,
131
137
  "seedance-2-extend": 20000,
138
+ // Seedance 2.5 raises the prompt ceiling to 30000
139
+ // (docs.kie.ai/market/bytedance/seedance-2-5).
140
+ "seedance-2-5": 30000,
132
141
  "gemini-omni-video": 20000,
133
142
  "bytedance-lite": 10000,
134
143
  "bytedance-pro": 10000,
135
144
  "bytedance-pro-fast": 10000,
145
+ // MiniMax Hailuo 3 — 1-7000 chars, uniform across t2v/i2v/r2v
146
+ // (docs.kie.ai/market/minimax-h3).
147
+ "minimax-h3": 7000,
136
148
  // lower than the 8000 default
137
149
  "minimax": 1500,
138
150
  "hailuo-standard": 1500,
@@ -641,6 +653,8 @@ export const IMAGE_TO_VIDEO_PROVIDERS = [
641
653
  "seedance-2",
642
654
  "seedance-2-fast",
643
655
  "seedance-2-mini",
656
+ "seedance-2-5",
657
+ "minimax-h3",
644
658
  "hailuo-2.3-pro",
645
659
  "hailuo-2.3",
646
660
  "hailuo-standard",
@@ -683,6 +697,8 @@ export const TEXT_TO_VIDEO_PROVIDERS = [
683
697
  "seedance-2",
684
698
  "seedance-2-fast",
685
699
  "seedance-2-mini",
700
+ "seedance-2-5",
701
+ "minimax-h3",
686
702
  "wan",
687
703
  "hailuo-standard",
688
704
  "bytedance-lite",
@@ -861,15 +877,24 @@ export const LIP_SYNC_PROVIDERS = [
861
877
  "seedance-2",
862
878
  "seedance-2-fast",
863
879
  "seedance-2-mini",
880
+ "seedance-2-5",
881
+ // MiniMax Hailuo 3 — same multimodal audio-driven mechanism (r2v
882
+ // reference_audio_urls paired with an image ref); rides the same
883
+ // reference-audio lip-sync surface as the Seedance 2 family.
884
+ "minimax-h3",
864
885
  ] as const
865
886
  export type LipSyncProvider = typeof LIP_SYNC_PROVIDERS[number]
866
887
 
867
- /** Seedance variants exposed via the lip-sync surface. They go through
868
- * the i2v provider with the audio plumbed as `reference_audio_urls`. */
888
+ /** Multimodal video models exposed via the lip-sync surface (Seedance 2 family
889
+ * + MiniMax Hailuo 3). They go through the i2v provider with the audio
890
+ * plumbed as `reference_audio_urls` rather than a dedicated lip-sync flow.
891
+ * (Name kept for the Seedance-era public export.) */
869
892
  export const SEEDANCE_LIP_SYNC_PROVIDERS = new Set<string>([
870
893
  "seedance-2",
871
894
  "seedance-2-fast",
872
895
  "seedance-2-mini",
896
+ "seedance-2-5",
897
+ "minimax-h3",
873
898
  ])
874
899
 
875
900
  /** Replicate-based lip-sync providers (video or image+audio via Replicate SDK) */
@@ -1104,6 +1129,8 @@ export const DURATION_PRICED_PROVIDERS = new Set([
1104
1129
  "seedance-2",
1105
1130
  "seedance-2-fast",
1106
1131
  "seedance-2-mini",
1132
+ "seedance-2-5",
1133
+ "minimax-h3",
1107
1134
  "grok-imagine-video-1.5",
1108
1135
  "happyhorse",
1109
1136
  "happyhorse-i2v",
@@ -1118,6 +1145,12 @@ export const SEEDANCE_2_PROVIDERS = new Set<string>([
1118
1145
  "seedance-2",
1119
1146
  "seedance-2-fast",
1120
1147
  "seedance-2-mini",
1148
+ // Seedance 2.5 — a later generation, but it shares the whole 2.0 capability
1149
+ // surface this set gates (frames + image/video/audio refs, adaptive aspect,
1150
+ // per-second resolution × video-ref pricing), so it belongs here. Where it
1151
+ // DIFFERS (30s ceiling, no 1080p/4K, wider ref caps) the difference is
1152
+ // carried by the per-provider maps, never by branching on this set.
1153
+ "seedance-2-5",
1121
1154
  ])
1122
1155
 
1123
1156
  export function isSeedance2Provider(provider: string | undefined): boolean {
@@ -1125,21 +1158,278 @@ export function isSeedance2Provider(provider: string | undefined): boolean {
1125
1158
  }
1126
1159
 
1127
1160
  /**
1128
- * Generate/Edit Video Pro SUPPORT subset — the only SKUs the pro multi-segment
1129
- * engine currently offers (mini withdrawn from selection, 2026-07-21).
1130
- * Deliberately distinct from SEEDANCE_2_PROVIDERS: the family set gates
1131
- * CAPABILITIES (ref limits, i2v params, adaptive aspect) and must keep every
1132
- * variant; this list gates which SKUs the pro nodes offer in selection. The
1133
- * pro plugin routes stay tolerant of the full family so previously-saved
1134
- * workflows keep running the editor fail-safe snaps stale selections to a
1135
- * supported SKU instead.
1161
+ * MiniMax Hailuo 3 family — the KIE "minimax-h3" market model. One Nodaro id
1162
+ * covers three KIE endpoints (text-to-video / image-to-video /
1163
+ * reference-to-video); the provider layer swaps the endpoint by which inputs
1164
+ * are present. Mirrors the Seedance 2 multimodal surface (frames + image/video/
1165
+ * audio references) with a two-rate resolution lever (2K default / 768P,
1166
+ * added 2026-08-03) and per-second pricing at the selected resolution's rate.
1167
+ * Exact-membership setNEVER match on a "minimax" prefix (the unrelated
1168
+ * Hailuo 02 id "minimax" would collide).
1169
+ */
1170
+ export const MINIMAX_H3_PROVIDERS = new Set<string>([
1171
+ "minimax-h3",
1172
+ ])
1173
+
1174
+ export function isMinimaxH3Provider(provider: string | undefined): boolean {
1175
+ return !!provider && MINIMAX_H3_PROVIDERS.has(provider)
1176
+ }
1177
+
1178
+ /** MiniMax Hailuo 3 resolution the model renders when the param is omitted or
1179
+ * unknown — the KIE-side default. */
1180
+ export const MINIMAX_H3_DEFAULT_RESOLUTION = "2K"
1181
+
1182
+ /**
1183
+ * SINGLE normalization for the MiniMax Hailuo 3 resolution lever (KIE wire
1184
+ * enum "768P" | "2K", default 2K) — shared by billing (credit-identifier
1185
+ * suffix, r2v compute hook, GVP per-second anchor) and the provider layer's
1186
+ * KIE forwarding. Only a case-insensitive "768p" selects the cheaper tier;
1187
+ * ANYTHING else (undefined, a stale Seedance "720p", garbage) collapses to
1188
+ * "2K" — exactly what KIE renders when the param is omitted/unknown, so the
1189
+ * billed tier can never undercut the rendered one.
1190
+ */
1191
+ export function normalizeMinimaxH3Resolution(resolution: string | undefined): "768P" | "2K" {
1192
+ return resolution?.trim().toLowerCase() === "768p" ? "768P" : MINIMAX_H3_DEFAULT_RESOLUTION
1193
+ }
1194
+
1195
+ /**
1196
+ * Video models that are CATALOGUED and priced but have NO working dispatch
1197
+ * path today — selecting one fails at the router, not at validation.
1198
+ *
1199
+ * `kling-3-omni` has a Replicate implementation but the i2v routing chain is
1200
+ * KIE-only and it has no worker short-circuit (the way LTX does), so every run
1201
+ * failed at the router. It was pulled from the frontend picker for that
1202
+ * reason; the backend enum and cost rows stay so legacy saved workflows still
1203
+ * validate.
1204
+ *
1205
+ * This lives in shared rather than as a comment in the frontend picker list
1206
+ * because it is a REAL capability fact that more than one consumer must
1207
+ * respect: `deriveGvpSupportedProviders` below would otherwise offer
1208
+ * `kling-3-omni` (it is i2v, takes reference images, declares durations, and
1209
+ * carries ref limits — it passes every capability check) and every selection
1210
+ * would fail at run time.
1211
+ *
1212
+ * Remove an entry only after wiring its dispatch AND verifying a live
1213
+ * generation.
1214
+ */
1215
+ export const VIDEO_PROVIDERS_WITHOUT_DISPATCH = new Set<string>([
1216
+ "kling-3-omni",
1217
+ ])
1218
+
1219
+ /** The SKU a stale/unsupported generate-video-pro selection snaps back to, and
1220
+ * the one that lists first in every pro provider dropdown. */
1221
+ export const GVP_DEFAULT_PROVIDER = "seedance-2"
1222
+
1223
+ /**
1224
+ * Generate Video Pro SUPPORT set — DERIVED FROM CAPABILITY, never hand-kept
1225
+ * (2026-08-05; was a literal `["seedance-2","seedance-2-fast","minimax-h3"]`).
1226
+ *
1227
+ * The bar is exactly what the pro engine's KEYFRAMES render method needs: the
1228
+ * model must accept a generated start still, and it must be able to carry the
1229
+ * identity/continuity reference images the anchor wave and the extend chain
1230
+ * attach. So:
1231
+ *
1232
+ * kind "video" ∧ modes ∋ "i2v" ∧ features ∋ "reference-image" ∧ has durations
1233
+ *
1234
+ * `durations` is part of the bar because the engine SPLITS a request into
1235
+ * segments — a model that declares no duration set cannot be segmented, and a
1236
+ * silent 4–15s fallback would send lengths the provider rejects.
1237
+ *
1238
+ * Membership is cross-checked against `VIDEO_REF_LIMITS_BY_PROVIDER` below by
1239
+ * `__tests__/gvp-supported-providers.test.ts`: that map was built by the
1240
+ * 2026-06-28 audit of actual backend reference-FORWARDING paths, so a model
1241
+ * carrying the catalog flag without a forwarding path (or vice versa) fails
1242
+ * the build instead of silently advertising a capability the backend drops.
1243
+ *
1244
+ * Deliberately distinct from SEEDANCE_2_PROVIDERS, which gates CAPABILITIES
1245
+ * (ref limits, i2v params, adaptive aspect) for the whole family. The pro
1246
+ * plugin route stays tolerant of previously-saved values so in-flight
1247
+ * workflows keep running; the editor fail-safe snaps stale selections to
1248
+ * GVP_DEFAULT_PROVIDER instead.
1249
+ *
1250
+ * `seedance-2-mini` re-enters selection here (it was withdrawn by hand on
1251
+ * 2026-07-21). It qualifies on capability and is the cheapest Seedance-2 SKU,
1252
+ * which makes it the natural pick for cheap iteration on long runs.
1136
1253
  */
1137
- export const GVP_SUPPORTED_PROVIDERS = ["seedance-2", "seedance-2-fast"] as const
1254
+ function deriveGvpSupportedProviders(): VideoGenProvider[] {
1255
+ const dispatchable = new Set<string>(VIDEO_GEN_PROVIDERS as readonly string[])
1256
+ const eligible = Object.values(MODEL_CATALOG)
1257
+ .filter(
1258
+ (m) =>
1259
+ m.kind === "video" &&
1260
+ (m.modes as readonly string[]).includes("i2v") &&
1261
+ m.features?.includes("reference-image") === true &&
1262
+ (m.durations?.length ?? 0) > 0 &&
1263
+ // Must also be dispatchable by the unified video node — the pro route
1264
+ // hands the provider id straight to the app's imageToVideo.
1265
+ dispatchable.has(m.id) &&
1266
+ // ...and must actually REACH a provider. Capability alone is not
1267
+ // enough: kling-3-omni passes every check above and still fails at the
1268
+ // router (see VIDEO_PROVIDERS_WITHOUT_DISPATCH).
1269
+ !VIDEO_PROVIDERS_WITHOUT_DISPATCH.has(m.id),
1270
+ )
1271
+ .map((m) => m.id as VideoGenProvider)
1272
+
1273
+ // Dropdown order: SKUs of one model family stay adjacent (raw catalog order
1274
+ // interleaves them — minimax-h3 is authored ~340 lines above seedance-2, so
1275
+ // a flat sort would split the Seedance trio around it). Group by `series`,
1276
+ // order groups by first catalog appearance, then hoist the default's group —
1277
+ // and the default within it — to the front.
1278
+ const seriesOf = (id: string): string => MODEL_CATALOG[id]?.series ?? MODEL_CATALOG[id]?.family ?? id
1279
+ const defaultSeries = seriesOf(GVP_DEFAULT_PROVIDER)
1280
+ const order: string[] = []
1281
+ for (const id of eligible) {
1282
+ const s = seriesOf(id)
1283
+ if (!order.includes(s)) order.push(s)
1284
+ }
1285
+ const rank = (id: string): number => {
1286
+ const s = seriesOf(id)
1287
+ return s === defaultSeries ? -1 : order.indexOf(s)
1288
+ }
1289
+ return [...eligible].sort((a, b) => {
1290
+ const d = rank(a) - rank(b)
1291
+ if (d !== 0) return d
1292
+ if (a === GVP_DEFAULT_PROVIDER) return -1
1293
+ if (b === GVP_DEFAULT_PROVIDER) return 1
1294
+ return eligible.indexOf(a) - eligible.indexOf(b)
1295
+ })
1296
+ }
1297
+
1298
+ export const GVP_SUPPORTED_PROVIDERS: readonly VideoGenProvider[] = deriveGvpSupportedProviders()
1138
1299
 
1139
1300
  export function isGvpSupportedProvider(provider: string | undefined): boolean {
1140
1301
  return !!provider && (GVP_SUPPORTED_PROVIDERS as readonly string[]).includes(provider)
1141
1302
  }
1142
1303
 
1304
+ /**
1305
+ * The generate-video-pro SKUs whose transport supports the `extend` render
1306
+ * method — a continuation TAIL of the previous segment sent as a conditioning
1307
+ * reference video (KIE `reference_video_urls`, built by
1308
+ * `resolveSeedance2Inputs`).
1309
+ *
1310
+ * Derived from the catalog's `"video-reference"` feature, so a future r2v SKU
1311
+ * flows in on its own instead of needing a family predicate edited. Today this
1312
+ * resolves to exactly the Seedance-2 family + minimax-h3 — identical to the
1313
+ * hardcoded gate it replaces, so the change is behaviour-neutral.
1314
+ *
1315
+ * Every other blessed SKU renders `keyframes` only: each segment is generated
1316
+ * from its own anchor stills, nothing conditions on another segment's video.
1317
+ */
1318
+ export const GVP_EXTEND_PROVIDERS: readonly VideoGenProvider[] = GVP_SUPPORTED_PROVIDERS.filter(
1319
+ (id) => MODEL_CATALOG[id]?.features?.includes("video-reference") === true,
1320
+ )
1321
+
1322
+ export function supportsExtendRender(provider: string | undefined): boolean {
1323
+ return !!provider && (GVP_EXTEND_PROVIDERS as readonly string[]).includes(provider)
1324
+ }
1325
+
1326
+ /**
1327
+ * The generate-video-pro SKUs that honour a strict CLOSING frame, so the
1328
+ * keyframes anchor wave generates an end still for them as well as a start
1329
+ * still. Derived from the catalog's `"end-frame"` feature — the plugin's
1330
+ * `wantEndAnchor` reads this instead of a provider-family name check.
1331
+ */
1332
+ export const GVP_END_FRAME_PROVIDERS: readonly VideoGenProvider[] = GVP_SUPPORTED_PROVIDERS.filter(
1333
+ (id) => MODEL_CATALOG[id]?.features?.includes("end-frame") === true,
1334
+ )
1335
+
1336
+ export function supportsEndAnchor(provider: string | undefined): boolean {
1337
+ return !!provider && (GVP_END_FRAME_PROVIDERS as readonly string[]).includes(provider)
1338
+ }
1339
+
1340
+ /**
1341
+ * KEYFRAMES ANCHOR CHOICE — how much a keyframes run pins its shots to
1342
+ * pre-generated stills. The node stores this product vocabulary; the plugin
1343
+ * wire speaks its own `anchorMode`, and `resolveGvpAnchorWire` is the SINGLE
1344
+ * translation both send paths go through (canvas Run in `execute-node.ts` and
1345
+ * the DAG's `payload-builder.ts`), so the two cannot disagree about what the
1346
+ * user picked — the drift that left `renderMethod` unreachable from the canvas
1347
+ * until 2026-08-05.
1348
+ */
1349
+ export const GVP_ANCHOR_CHOICES = ["auto", "start-end", "start-only", "reference"] as const
1350
+
1351
+ export type GvpAnchorChoice = (typeof GVP_ANCHOR_CHOICES)[number]
1352
+
1353
+ /** The plugin engine's own chain-mode vocabulary. */
1354
+ export type GvpAnchorWireMode = "upfront" | "progressive" | "none"
1355
+
1356
+ /**
1357
+ * Translate the node's anchor choice into the plugin's `anchorMode` field.
1358
+ *
1359
+ * - `auto` (and absent/unknown) → `undefined`: send NOTHING, leaving the
1360
+ * engine's own default and env flags in charge exactly as they are for
1361
+ * callers predating this lever. Existing payloads stay byte-identical.
1362
+ * - `start-end` → `upfront`: anchors pre-generated from plan text, and the
1363
+ * engine's own closing-frame policy applies (capability-gated, longer
1364
+ * scenes only). Today's shipped behaviour.
1365
+ * - `start-only` → `progressive`: each start anchor is generated just-in-time
1366
+ * from the PREVIOUS render's real last frame, and no closing still is ever
1367
+ * pinned — so a shot can never warp its world to reach a pre-motion guess.
1368
+ * - `reference` → `none`: no frame conditioning at all; identity/location
1369
+ * references plus the prompt carry the shot.
1370
+ *
1371
+ * Deliberately NOT emitting the per-segment `endAnchors` array: that is a
1372
+ * per-scene lever whose natural home is a per-scene surface, and emitting it
1373
+ * here would mean duplicating the plugin's `MAX_SEGMENTS` cap in this repo
1374
+ * where nothing could keep the two in step.
1375
+ */
1376
+ export function resolveGvpAnchorWire(choice: string | undefined): GvpAnchorWireMode | undefined {
1377
+ switch (choice) {
1378
+ case "start-end":
1379
+ return "upfront"
1380
+ case "start-only":
1381
+ return "progressive"
1382
+ case "reference":
1383
+ return "none"
1384
+ default:
1385
+ return undefined
1386
+ }
1387
+ }
1388
+
1389
+ // ---------------------------------------------------------------------------
1390
+ // SEGMENT BOUNDS — pure catalog readers. The generate-video-pro splitter (in
1391
+ // three places: the plugin engine, the money closed-form, the UI estimate)
1392
+ // used to hardcode `{minSeg: 4, maxSeg: 15}`, which is only correct for the
1393
+ // Seedance-2 family. These are the single source every one of them reads.
1394
+ // ---------------------------------------------------------------------------
1395
+
1396
+ /** Allowed single-segment durations (seconds, ascending) for a pro provider.
1397
+ * Empty for anything not in the catalog — callers treat that as ineligible
1398
+ * rather than falling back to a window the provider may reject. */
1399
+ export function segmentDurationsFor(provider: string | undefined): readonly number[] {
1400
+ if (!provider) return []
1401
+ return [...(MODEL_CATALOG[provider]?.durations ?? [])].sort((a, b) => a - b)
1402
+ }
1403
+
1404
+ /** Shortest single segment the provider can render. 0 when unknown. */
1405
+ export function minSegmentSecFor(provider: string | undefined): number {
1406
+ return segmentDurationsFor(provider)[0] ?? 0
1407
+ }
1408
+
1409
+ /** Longest single segment the provider can render — the value above which a
1410
+ * request must split. 0 when unknown. */
1411
+ export function maxSegmentSecFor(provider: string | undefined): number {
1412
+ const d = segmentDurationsFor(provider)
1413
+ return d[d.length - 1] ?? 0
1414
+ }
1415
+
1416
+ /** True when the provider's durations are every integer between its min and
1417
+ * max (Seedance-2 family, minimax-h3, kling-3-omni, happyhorse-ref2v). Those
1418
+ * can use the classic arithmetic splitter; sparse providers (veo3's 4/6/8,
1419
+ * grok-i2v's 6/10) need the discrete packer. */
1420
+ export function hasContiguousSegmentDurations(provider: string | undefined): boolean {
1421
+ const d = segmentDurationsFor(provider)
1422
+ if (d.length === 0) return false
1423
+ return d.length === d[d.length - 1]! - d[0]! + 1
1424
+ }
1425
+
1426
+ /** Most segments a run can hold for this provider at a given total cap — the
1427
+ * dynamic replacement for the old fixed 24. 0 when the provider is unknown. */
1428
+ export function maxSegmentsFor(provider: string | undefined, capSec: number): number {
1429
+ const max = maxSegmentSecFor(provider)
1430
+ return max > 0 ? Math.ceil(capSec / max) : 0
1431
+ }
1432
+
1143
1433
  /**
1144
1434
  * Default aspect ratio for a video provider when the node carries no explicit
1145
1435
  * `aspectRatio`. Seedance 2.x defaults to `"adaptive"` (output matches the
@@ -1147,9 +1437,15 @@ export function isGvpSupportedProvider(provider: string | undefined): boolean {
1147
1437
  * historical `"16:9"`. Single source of truth so every run-default / display-
1148
1438
  * fallback site stays in lock-step (preview = run). Applies to ALL Seedance
1149
1439
  * modes incl. text-to-video — KIE accepts `adaptive` there too.
1440
+ *
1441
+ * MiniMax Hailuo 3 also defaults to `adaptive`: its r2v endpoint takes
1442
+ * `adaptive` natively (the KIE default) and its i2v endpoint has no aspect
1443
+ * param at all (inferred from the frame). Only the pure-t2v endpoint requires
1444
+ * a concrete ratio — the KIE provider layer coerces `adaptive` → `16:9` there
1445
+ * (see applyMinimaxH3Params in backend kie/video.ts).
1150
1446
  */
1151
1447
  export function defaultVideoAspectRatio(provider: string | undefined): string {
1152
- return isSeedance2Provider(provider) ? "adaptive" : "16:9"
1448
+ return isSeedance2Provider(provider) || isMinimaxH3Provider(provider) ? "adaptive" : "16:9"
1153
1449
  }
1154
1450
 
1155
1451
  /**
@@ -1173,6 +1469,18 @@ export const SEEDANCE_2_REF_LIMITS = {
1173
1469
  audio: 3,
1174
1470
  } as const
1175
1471
 
1472
+ /**
1473
+ * Seedance 2.5 raises every reference cap well above the 2.0 family's
1474
+ * (docs.kie.ai/market/bytedance/seedance-2-5): 30 images, 10 videos, 10 audio.
1475
+ * Kept as its own constant rather than widening {@link SEEDANCE_2_REF_LIMITS}
1476
+ * so the 2.0 SKUs can't silently start advertising caps KIE rejects for them.
1477
+ */
1478
+ export const SEEDANCE_2_5_REF_LIMITS = {
1479
+ images: 30,
1480
+ videos: 10,
1481
+ audio: 10,
1482
+ } as const
1483
+
1176
1484
  /**
1177
1485
  * KIE r2v REFERENCE-VIDEO MINIMUM (seconds) for the Seedance 2.0 family — the
1178
1486
  * provider hard-rejects shorter reference clips with a 400 BEFORE generation:
@@ -1233,6 +1541,12 @@ export const SEEDANCE_2_EXTEND_STITCH = {
1233
1541
  */
1234
1542
  export const SEEDANCE_2_R2V_MAX_AUDIO_SEC_BY_PROVIDER: Record<string, number> = {
1235
1543
  "seedance-2-fast": 15.2,
1544
+ // Seedance 2.5 — documented hard limit: each reference audio 2-30s, ≤15 MB
1545
+ // per file (docs.kie.ai/market/bytedance/seedance-2-5).
1546
+ "seedance-2-5": 30,
1547
+ // MiniMax Hailuo 3 — documented hard limit: each reference audio segment
1548
+ // 2-15s, ≤15s total (docs.kie.ai/market/minimax-h3/reference-to-video).
1549
+ "minimax-h3": 15,
1236
1550
  }
1237
1551
 
1238
1552
  /** The verified r2v reference-audio cap (seconds) for a provider, or null when
@@ -1281,6 +1595,13 @@ export const VIDEO_REF_LIMITS_BY_PROVIDER: Record<
1281
1595
  "seedance-2": { ...SEEDANCE_2_REF_LIMITS },
1282
1596
  "seedance-2-fast": { ...SEEDANCE_2_REF_LIMITS },
1283
1597
  "seedance-2-mini": { ...SEEDANCE_2_REF_LIMITS },
1598
+ // Seedance 2.5 — wider caps than the 2.0 family (30 / 10 / 10).
1599
+ "seedance-2-5": { ...SEEDANCE_2_5_REF_LIMITS },
1600
+ // MiniMax Hailuo 3 — identical multimodal caps (9 images / 3 videos / 3
1601
+ // audio per docs.kie.ai/market/minimax-h3/reference-to-video). Note: KIE
1602
+ // bills input images beyond the first 5 (11 KIE cr each) — see the
1603
+ // minimax-h3 credit helper in backend ee/billing.
1604
+ "minimax-h3": { ...SEEDANCE_2_REF_LIMITS },
1284
1605
  // Multi-image reference providers.
1285
1606
  "gemini-omni-video": { images: 7, videos: 1 },
1286
1607
  "kling-3-omni": { images: 7 }, // catalog/docs: "end frame + up to 7 reference images"
@@ -1348,8 +1669,30 @@ export const NATIVE_ADAPTIVE_ASPECT: Record<string, string> = {
1348
1669
  "seedance-2": "adaptive",
1349
1670
  "seedance-2-fast": "adaptive",
1350
1671
  "seedance-2-mini": "adaptive",
1672
+ "seedance-2-5": "adaptive",
1351
1673
  }
1352
1674
 
1675
+ /**
1676
+ * Video providers that accept ONLY the "adaptive" aspect ratio once a START
1677
+ * FRAME is present (first-frame or first+last-frame mode). Sending any explicit
1678
+ * ratio in that mode is a hard 422 from the provider, so the payload builder
1679
+ * coerces the value to `adaptive` for these instead of forwarding it.
1680
+ *
1681
+ * UNDOCUMENTED — KIE's schema advertises the full ratio enum unconditionally and
1682
+ * only rejects the combination at request time:
1683
+ * "Seedance 2.5 first-frame and first-last-frame tasks only support adaptive
1684
+ * aspect ratio"
1685
+ * (live probe against api.kie.ai, 2026-08-08). Verified as 2.5-ONLY: the same
1686
+ * probe against the 2.0 SKUs accepts explicit ratios with a start frame, which
1687
+ * is why this is a set and not a family-wide rule.
1688
+ *
1689
+ * Behaviourally this costs nothing — with a start frame the output aspect is
1690
+ * derived from that frame anyway, which is exactly what `adaptive` means.
1691
+ */
1692
+ export const FRAME_MODE_ADAPTIVE_ONLY_ASPECT: ReadonlySet<string> = new Set([
1693
+ "seedance-2-5",
1694
+ ])
1695
+
1353
1696
  /**
1354
1697
  * Video providers that REQUIRE an input image (image-to-video only) even though
1355
1698
  * they're listed in TEXT_TO_VIDEO_PROVIDERS for unified-node visibility. The
@@ -1477,6 +1820,14 @@ export const VIDEO_AUDIO_CAPABILITY: Record<string, VideoAudioCapability> = {
1477
1820
  "seedance-2": { mode: "audio_driven", field: "generateAudio" },
1478
1821
  "seedance-2-fast": { mode: "audio_driven", field: "generateAudio" },
1479
1822
  "seedance-2-mini": { mode: "audio_driven", field: "generateAudio" },
1823
+ // Seedance 2.5 — same audio-driven surface; `generate_audio` defaults TRUE
1824
+ // on KIE and is cost-affecting only through the -ref/no-ref rate split,
1825
+ // which the resolution+ref composite already carries (no `:audio` suffix).
1826
+ "seedance-2-5": { mode: "audio_driven", field: "generateAudio", defaultOn: true },
1827
+ // MiniMax Hailuo 3 — multimodal; lip-syncs to reference audio (r2v). The
1828
+ // KIE API exposes NO audio on/off parameter (audio is always produced), so
1829
+ // there is no toggle field — alwaysOn, like VEO.
1830
+ "minimax-h3": { mode: "audio_driven", alwaysOn: true },
1480
1831
  }
1481
1832
 
1482
1833
  const VIDEO_AUDIO_NONE: VideoAudioCapability = { mode: "none" }
@@ -1595,10 +1946,51 @@ export const VIDEO_VARIABLE_PRICING: Record<string, "duration" | "duration+audio
1595
1946
  "seedance-2": "duration+resolution+ref",
1596
1947
  "seedance-2-fast": "duration+resolution+ref",
1597
1948
  "seedance-2-mini": "duration+resolution+ref",
1949
+ "seedance-2-5": "duration+resolution+ref",
1950
+ // MiniMax Hailuo 3 — per-second rate at two resolution tiers (2K default /
1951
+ // 768P). Duration + resolution form the composite (bare = 2K, ":768p"
1952
+ // appended for the cheaper tier); ref-video input seconds + extra input
1953
+ // images are billed via the dedicated compute hook, not the identifier.
1954
+ "minimax-h3": "duration+resolution",
1598
1955
  // Grok Imagine Video 1.5 — per-second billing split 480p/720p (no video-ref dimension).
1599
1956
  "grok-imagine-video-1.5": "duration+resolution",
1600
1957
  }
1601
1958
 
1959
+ /**
1960
+ * Duration assumed for PRICING when a request names a provider but omits
1961
+ * `duration` — MUST match the provider's KIE-side default (`extraParams.duration`
1962
+ * in backend kie/models.ts) so an intent-less request reserves what it will
1963
+ * actually render. Providers absent here use the historical global 5s fallback,
1964
+ * which is only safe when their duration TIERS snap 5 up to (or past) the model
1965
+ * default (e.g. seedance-2's 4/8/12/15 ladder snaps 5 → the 8s tier = its 8s
1966
+ * default). minimax-h3 prices per-second (every 4-15s tier seeded), so a 5s
1967
+ * fallback would under-reserve its 6s default render.
1968
+ */
1969
+ export const PRICING_DEFAULT_DURATION_SEC: Record<string, number> = {
1970
+ "minimax-h3": 6,
1971
+ }
1972
+
1973
+ /**
1974
+ * Resolution assumed for PRICING when a request names a provider but omits
1975
+ * `resolution` — the resolution twin of {@link PRICING_DEFAULT_DURATION_SEC},
1976
+ * and it MUST match the provider's KIE-side default (`extraParams.resolution`
1977
+ * in backend kie/models.ts) so an intent-less request reserves what it will
1978
+ * actually render.
1979
+ *
1980
+ * Without an entry, `buildVideoCreditModelIdentifier` falls back to the
1981
+ * CHEAPEST tier (480p) for a resolution-priced provider. That is safe only for
1982
+ * a model whose KIE default IS the cheapest tier; for one defaulting to 720p it
1983
+ * reserves the 480p price against a 720p render, and `commit_credits` cannot
1984
+ * collect the difference afterwards.
1985
+ *
1986
+ * Providers absent here keep the historical 480p fallback — deliberately, so
1987
+ * adding this map cannot reprice anything already live.
1988
+ */
1989
+ export const PRICING_DEFAULT_RESOLUTION: Record<string, string> = {
1990
+ // KIE renders 720p when `resolution` is omitted (kie/models.ts extraParams).
1991
+ "seedance-2-5": "720p",
1992
+ }
1993
+
1602
1994
  /** HappyHorse 1.1 per-second tiers — one per allowed duration (3–15s), shared
1603
1995
  * by all three modes (t2v/i2v/ref2v) which bill at identical published rates. */
1604
1996
  const HAPPYHORSE_DURATION_TIERS: Array<{ maxSeconds: number; suffix: string }> = Array.from(
@@ -1606,6 +1998,21 @@ const HAPPYHORSE_DURATION_TIERS: Array<{ maxSeconds: number; suffix: string }> =
1606
1998
  (_, i) => ({ maxSeconds: i + 3, suffix: `${i + 3}s` }),
1607
1999
  )
1608
2000
 
2001
+ /**
2002
+ * Seedance 2.5 per-second tiers — one per allowed duration (4–30s), like
2003
+ * minimax-h3 and unlike the 2.0 family's coarse 4/8/12/15 ladder.
2004
+ *
2005
+ * Per-second is REQUIRED here, not a nicety: the ladder snaps a request UP to
2006
+ * the first tier whose `maxSeconds` covers it and falls back to the LAST tier
2007
+ * when nothing does, so a coarse ladder over a 30s range would reserve the 15s
2008
+ * price for a 30s render. `commit_credits` only ever refunds a surplus and can
2009
+ * never collect an upward delta, so that under-reservation would be permanent.
2010
+ */
2011
+ const SEEDANCE_2_5_DURATION_TIERS: Array<{ maxSeconds: number; suffix: string }> = Array.from(
2012
+ { length: 27 },
2013
+ (_, i) => ({ maxSeconds: i + 4, suffix: `${i + 4}s` }),
2014
+ )
2015
+
1609
2016
  /**
1610
2017
  * Duration tier breakpoints for variable-priced video models.
1611
2018
  * Maps provider → array of { maxSeconds, suffix } in ascending order.
@@ -1685,6 +2092,25 @@ export const VIDEO_DURATION_TIERS: Record<string, Array<{ maxSeconds: number; su
1685
2092
  { maxSeconds: 12, suffix: "12s" },
1686
2093
  { maxSeconds: 15, suffix: "15s" },
1687
2094
  ],
2095
+ "seedance-2-5": SEEDANCE_2_5_DURATION_TIERS,
2096
+ // MiniMax Hailuo 3 — true per-second billing (KIE 36.5 cr/s @2K, 22.5 cr/s
2097
+ // @768P). One tier per allowed second (4-15s) so the composite identifier
2098
+ // maps 1:1 to the seeded price — no rounding/overcharge for any on-menu
2099
+ // duration; the ":768p" resolution suffix is appended after the tier.
2100
+ "minimax-h3": [
2101
+ { maxSeconds: 4, suffix: "4s" },
2102
+ { maxSeconds: 5, suffix: "5s" },
2103
+ { maxSeconds: 6, suffix: "6s" },
2104
+ { maxSeconds: 7, suffix: "7s" },
2105
+ { maxSeconds: 8, suffix: "8s" },
2106
+ { maxSeconds: 9, suffix: "9s" },
2107
+ { maxSeconds: 10, suffix: "10s" },
2108
+ { maxSeconds: 11, suffix: "11s" },
2109
+ { maxSeconds: 12, suffix: "12s" },
2110
+ { maxSeconds: 13, suffix: "13s" },
2111
+ { maxSeconds: 14, suffix: "14s" },
2112
+ { maxSeconds: 15, suffix: "15s" },
2113
+ ],
1688
2114
  // Grok Imagine Video 1.5 — true per-second billing (KIE 14.5 cr/s @480p, 25 cr/s
1689
2115
  // @720p, +2 cr/image). One tier per allowed second (1–15s) so the composite
1690
2116
  // identifier maps 1:1 to the seeded price — no rounding/overcharge for any on-menu value.
@@ -1895,6 +2321,17 @@ export const VIDEO_MODEL_CAPS: Record<string, VideoModelCapabilities> = {
1895
2321
  // character + location + 3 secondary refs.
1896
2322
  maxReferenceImages: 5,
1897
2323
  },
2324
+ "seedance-2-5": {
2325
+ inputModes: ["first_frame", "first_last_frame", "ref_images", "video_continuation"],
2326
+ supportsVideoExtension: true,
2327
+ // 30s single-shot on KIE (probe-verified 2026-08-08: 30 accepted, 31 rejected).
2328
+ maxDurationSeconds: 30,
2329
+ prompting_style: "natural_language",
2330
+ // SEEDANCE_2_5_REF_LIMITS.images is the hard upper bound (30); the same
2331
+ // conservative headroom rule as seedance-2 applies to what the director
2332
+ // actually allocates.
2333
+ maxReferenceImages: 5,
2334
+ },
1898
2335
  "hailuo-2.3-pro": {
1899
2336
  inputModes: ["first_frame", "text"],
1900
2337
  maxDurationSeconds: 10,
@@ -184,6 +184,8 @@ const QUALITY_MAP: Record<string, QualityMapping> = {
184
184
  "seedance-2": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
185
185
  "seedance-2-fast": { field: "resolution", values: { low: "480p", mid: "720p", high: "720p" } },
186
186
  "seedance-2-mini": { field: "resolution", values: { low: "480p", mid: "720p", high: "720p" } },
187
+ // Seedance 2.5 tops out at 720p on KIE (1080p/4k probe-rejected 2026-08-08).
188
+ "seedance-2-5": { field: "resolution", values: { low: "480p", mid: "720p", high: "720p" } },
187
189
  "wan-2.7-i2v": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
188
190
  "wan-2.7-t2v": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
189
191
  "happyhorse": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
@@ -31,7 +31,7 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
31
31
  "suno-cover": ["prompt", "lyrics", "style", "title", "negativeStyle"],
32
32
  "suno-extend": ["prompt", "style", "title"],
33
33
  "suno-lyrics": ["prompt"],
34
- "suno-replace-section":["prompt", "tags", "title"],
34
+ "suno-replace-section":["prompt", "tags", "title", "fullLyrics", "negativeTags"],
35
35
  "suno-style-boost": ["content"],
36
36
  "suno-upload-extend": ["prompt", "style", "title"],
37
37
  "suno-mashup": ["style", "title", "negativeStyle"],
@@ -65,6 +65,7 @@ export const PIPELINE_PINNABLE_VIDEO_MODELS = [
65
65
  "seedance-2",
66
66
  "seedance-2-fast",
67
67
  "seedance-2-mini",
68
+ "seedance-2-5",
68
69
  "veo3",
69
70
  "veo3.1",
70
71
  "veo3_lite",
@@ -24,6 +24,10 @@ export const PROVIDER_DIRECTIVE_DEFAULTS = {
24
24
  "hailuo-2.3-pro": {} as Record<string, never>,
25
25
  "hailuo-standard": {} as Record<string, never>,
26
26
  minimax: {} as Record<string, never>,
27
+ // MiniMax Hailuo 3 — multimodal like seedance-2 but no verified directive
28
+ // semantics yet; empty until probed (NOTE: never match `minimax-h3` with a
29
+ // startsWith("seedance") / startsWith("minimax") prefix check — exact keys only).
30
+ "minimax-h3": {} as Record<string, never>,
27
31
  "bytedance-lite": {} as Record<string, never>,
28
32
  } as const
29
33