@nodaro/shared 1.4.2

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.
Files changed (665) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -0
  3. package/dist/index.cjs +12506 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +12269 -0
  6. package/dist/index.d.ts +12269 -0
  7. package/dist/index.js +11831 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +54 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/ai-avatar-pricing.test.ts +321 -0
  12. package/src/__tests__/apply-video-negative-prompt.test.ts +76 -0
  13. package/src/__tests__/attach-columns.test.ts +28 -0
  14. package/src/__tests__/audio-crossfade-curves.test.ts +33 -0
  15. package/src/__tests__/build-model-menu.test.ts +58 -0
  16. package/src/__tests__/character-aspect-defaults.test.ts +142 -0
  17. package/src/__tests__/character-mention-slug.test.ts +199 -0
  18. package/src/__tests__/character-reference-photo-kinds.test.ts +32 -0
  19. package/src/__tests__/character-usage-mode.test.ts +147 -0
  20. package/src/__tests__/character-variant-assets.test.ts +142 -0
  21. package/src/__tests__/character-voice.test.ts +130 -0
  22. package/src/__tests__/cinematic-avatar-pricing.test.ts +119 -0
  23. package/src/__tests__/combine-transitions.test.ts +137 -0
  24. package/src/__tests__/component-types.test.ts +87 -0
  25. package/src/__tests__/condition-variables.test.ts +98 -0
  26. package/src/__tests__/credit-identifiers.test.ts +811 -0
  27. package/src/__tests__/default-video-provider.test.ts +63 -0
  28. package/src/__tests__/entity-image-handle.test.ts +43 -0
  29. package/src/__tests__/entity-variants.test.ts +33 -0
  30. package/src/__tests__/fal-lip-sync-sets.test.ts +31 -0
  31. package/src/__tests__/featured-entities.test.ts +33 -0
  32. package/src/__tests__/film-pricing.test.ts +64 -0
  33. package/src/__tests__/filter-condition.test.ts +285 -0
  34. package/src/__tests__/freecut-protocol.test.ts +35 -0
  35. package/src/__tests__/generate-text-items.test.ts +19 -0
  36. package/src/__tests__/generated-results.test.ts +50 -0
  37. package/src/__tests__/graph-utils.test.ts +618 -0
  38. package/src/__tests__/group-aggregation.test.ts +83 -0
  39. package/src/__tests__/i18n-locale-completeness.test.ts +156 -0
  40. package/src/__tests__/image-critic-node.test.ts +86 -0
  41. package/src/__tests__/image-mask-mode.test.ts +16 -0
  42. package/src/__tests__/image-ref-limit.test.ts +78 -0
  43. package/src/__tests__/inpaint-region.test.ts +24 -0
  44. package/src/__tests__/input-field-map-parity.test.ts +30 -0
  45. package/src/__tests__/json-evaluator.test.ts +289 -0
  46. package/src/__tests__/json-path.test.ts +64 -0
  47. package/src/__tests__/lip-sync-pricing.test.ts +142 -0
  48. package/src/__tests__/list-merge.test.ts +96 -0
  49. package/src/__tests__/list-sort.test.ts +134 -0
  50. package/src/__tests__/llm-models.test.ts +445 -0
  51. package/src/__tests__/location-mention-slug.test.ts +246 -0
  52. package/src/__tests__/locations-coverage-critic-schema.test.ts +92 -0
  53. package/src/__tests__/loop-delimiter.test.ts +39 -0
  54. package/src/__tests__/lottie-slots.test.ts +245 -0
  55. package/src/__tests__/model-catalog-flux2-resolutions.test.ts +21 -0
  56. package/src/__tests__/model-constants-location.test.ts +20 -0
  57. package/src/__tests__/model-constants.test.ts +122 -0
  58. package/src/__tests__/model-tree.test.ts +86 -0
  59. package/src/__tests__/no-internal-imports.test.ts +25 -0
  60. package/src/__tests__/node-default-mappings.test.ts +175 -0
  61. package/src/__tests__/node-mappable-fields.test.ts +7 -0
  62. package/src/__tests__/node-preset-export.test.ts +46 -0
  63. package/src/__tests__/node-preset-extract.test.ts +166 -0
  64. package/src/__tests__/node-refs-canonical.test.ts +54 -0
  65. package/src/__tests__/node-refs-combine.test.ts +54 -0
  66. package/src/__tests__/node-refs.test.ts +187 -0
  67. package/src/__tests__/node-runtime-keys.test.ts +99 -0
  68. package/src/__tests__/object-aspect-defaults.test.ts +59 -0
  69. package/src/__tests__/parameter-node-value.test.ts +137 -0
  70. package/src/__tests__/pinterest-url.test.ts +35 -0
  71. package/src/__tests__/pipeline-chat.test.ts +110 -0
  72. package/src/__tests__/pipeline-validation.test.ts +98 -0
  73. package/src/__tests__/presentation-utils.test.ts +1137 -0
  74. package/src/__tests__/producer-types.test.ts +47 -0
  75. package/src/__tests__/prompt-length-limits.test.ts +156 -0
  76. package/src/__tests__/provider-directive-defaults.test.ts +46 -0
  77. package/src/__tests__/reduce-strategy-registry.test.ts +43 -0
  78. package/src/__tests__/reference-board-templates.test.ts +33 -0
  79. package/src/__tests__/reference-roles.test.ts +36 -0
  80. package/src/__tests__/resolve-field-mappings.test.ts +110 -0
  81. package/src/__tests__/resolve-pipeline-model.test.ts +76 -0
  82. package/src/__tests__/scene-node-types.test.ts +80 -0
  83. package/src/__tests__/scraper-actors.test.ts +67 -0
  84. package/src/__tests__/seedance-2-catalog.test.ts +18 -0
  85. package/src/__tests__/seedance-2-credit-id.test.ts +23 -0
  86. package/src/__tests__/seedance-aspects.test.ts +52 -0
  87. package/src/__tests__/selector.test.ts +1151 -0
  88. package/src/__tests__/settled-with-limit.test.ts +214 -0
  89. package/src/__tests__/social-media-specs.test.ts +129 -0
  90. package/src/__tests__/sub-workflow-validation.test.ts +88 -0
  91. package/src/__tests__/switchx-pricing.test.ts +28 -0
  92. package/src/__tests__/to-connected-references.test.ts +153 -0
  93. package/src/__tests__/utilities.test.ts +541 -0
  94. package/src/__tests__/video-analysis-pricing.test.ts +58 -0
  95. package/src/__tests__/video-analysis.test.ts +63 -0
  96. package/src/__tests__/video-audio-capability.test.ts +204 -0
  97. package/src/__tests__/video-gen-providers.test.ts +28 -0
  98. package/src/__tests__/video-mode-aliases.test.ts +150 -0
  99. package/src/__tests__/video-ref-limits.test.ts +72 -0
  100. package/src/__tests__/video-utils.test.ts +303 -0
  101. package/src/__tests__/voice-changer-models.test.ts +33 -0
  102. package/src/__tests__/workflow-export-strip.test.ts +46 -0
  103. package/src/ai-avatar-pricing.ts +213 -0
  104. package/src/ancestor-refs.ts +48 -0
  105. package/src/animals.ts +231 -0
  106. package/src/audio-crossfade-curves.ts +73 -0
  107. package/src/audio-fx-presets.ts +64 -0
  108. package/src/avatar-payload-validators.ts +214 -0
  109. package/src/caption-styles.ts +31 -0
  110. package/src/character-aspect-defaults.ts +102 -0
  111. package/src/character-facets.ts +37 -0
  112. package/src/character-mention-slug.ts +166 -0
  113. package/src/character-usage-mode.ts +109 -0
  114. package/src/character-variant-assets.ts +121 -0
  115. package/src/character-voice.ts +151 -0
  116. package/src/cinematic-avatar-pricing.ts +111 -0
  117. package/src/clone-utils.ts +46 -0
  118. package/src/combine-transitions.ts +482 -0
  119. package/src/community.ts +112 -0
  120. package/src/component-types.ts +127 -0
  121. package/src/condition-variables.ts +65 -0
  122. package/src/credit-estimators/index.ts +15 -0
  123. package/src/credit-estimators/video-utils.ts +165 -0
  124. package/src/credit-identifiers.ts +304 -0
  125. package/src/entity-approval-types.ts +31 -0
  126. package/src/entity-aspect-defaults.ts +82 -0
  127. package/src/entity-asset-types.ts +296 -0
  128. package/src/entity-image-handle.ts +36 -0
  129. package/src/extra-refs.ts +139 -0
  130. package/src/featured-entities.ts +136 -0
  131. package/src/film-pricing.ts +117 -0
  132. package/src/filter-condition.ts +250 -0
  133. package/src/flux2-pricing.ts +18 -0
  134. package/src/freecut-protocol.ts +52 -0
  135. package/src/furniture.ts +167 -0
  136. package/src/generate-text-items.ts +15 -0
  137. package/src/generated-results.ts +63 -0
  138. package/src/group-aggregation.ts +75 -0
  139. package/src/hint-graph-types.ts +18 -0
  140. package/src/i18n/action-fx.ar.ts +89 -0
  141. package/src/i18n/action-fx.de.ts +89 -0
  142. package/src/i18n/action-fx.es.ts +89 -0
  143. package/src/i18n/action-fx.fr.ts +89 -0
  144. package/src/i18n/action-fx.he.ts +89 -0
  145. package/src/i18n/action-fx.hi.ts +89 -0
  146. package/src/i18n/action-fx.ja.ts +89 -0
  147. package/src/i18n/action-fx.ko.ts +89 -0
  148. package/src/i18n/action-fx.pt-BR.ts +89 -0
  149. package/src/i18n/action-fx.ru.ts +89 -0
  150. package/src/i18n/action-fx.zh-CN.ts +89 -0
  151. package/src/i18n/aesthetic.ar.ts +170 -0
  152. package/src/i18n/aesthetic.de.ts +48 -0
  153. package/src/i18n/aesthetic.es.ts +169 -0
  154. package/src/i18n/aesthetic.fr.ts +52 -0
  155. package/src/i18n/aesthetic.he.ts +169 -0
  156. package/src/i18n/aesthetic.hi.ts +55 -0
  157. package/src/i18n/aesthetic.ja.ts +165 -0
  158. package/src/i18n/aesthetic.ko.ts +169 -0
  159. package/src/i18n/aesthetic.pt-BR.ts +148 -0
  160. package/src/i18n/aesthetic.ru.ts +155 -0
  161. package/src/i18n/aesthetic.zh-CN.ts +172 -0
  162. package/src/i18n/animals.ar.ts +145 -0
  163. package/src/i18n/animals.de.ts +144 -0
  164. package/src/i18n/animals.es.ts +158 -0
  165. package/src/i18n/animals.fr.ts +144 -0
  166. package/src/i18n/animals.he.ts +155 -0
  167. package/src/i18n/animals.hi.ts +157 -0
  168. package/src/i18n/animals.ja.ts +159 -0
  169. package/src/i18n/animals.ko.ts +159 -0
  170. package/src/i18n/animals.pt-BR.ts +157 -0
  171. package/src/i18n/animals.ru.ts +155 -0
  172. package/src/i18n/animals.zh-CN.ts +155 -0
  173. package/src/i18n/atmosphere.ar.ts +49 -0
  174. package/src/i18n/atmosphere.de.ts +47 -0
  175. package/src/i18n/atmosphere.es.ts +50 -0
  176. package/src/i18n/atmosphere.fr.ts +47 -0
  177. package/src/i18n/atmosphere.he.ts +47 -0
  178. package/src/i18n/atmosphere.hi.ts +49 -0
  179. package/src/i18n/atmosphere.ja.ts +49 -0
  180. package/src/i18n/atmosphere.ko.ts +49 -0
  181. package/src/i18n/atmosphere.pt-BR.ts +49 -0
  182. package/src/i18n/atmosphere.ru.ts +47 -0
  183. package/src/i18n/atmosphere.zh-CN.ts +152 -0
  184. package/src/i18n/backdrop.ar.ts +53 -0
  185. package/src/i18n/backdrop.de.ts +46 -0
  186. package/src/i18n/backdrop.es.ts +54 -0
  187. package/src/i18n/backdrop.fr.ts +52 -0
  188. package/src/i18n/backdrop.he.ts +51 -0
  189. package/src/i18n/backdrop.hi.ts +59 -0
  190. package/src/i18n/backdrop.ja.ts +61 -0
  191. package/src/i18n/backdrop.ko.ts +59 -0
  192. package/src/i18n/backdrop.pt-BR.ts +59 -0
  193. package/src/i18n/backdrop.ru.ts +57 -0
  194. package/src/i18n/backdrop.zh-CN.ts +57 -0
  195. package/src/i18n/camera-format.ar.ts +40 -0
  196. package/src/i18n/camera-format.de.ts +37 -0
  197. package/src/i18n/camera-format.es.ts +41 -0
  198. package/src/i18n/camera-format.fr.ts +37 -0
  199. package/src/i18n/camera-format.he.ts +37 -0
  200. package/src/i18n/camera-format.hi.ts +42 -0
  201. package/src/i18n/camera-format.ja.ts +37 -0
  202. package/src/i18n/camera-format.ko.ts +37 -0
  203. package/src/i18n/camera-format.pt-BR.ts +46 -0
  204. package/src/i18n/camera-format.ru.ts +42 -0
  205. package/src/i18n/camera-format.zh-CN.ts +42 -0
  206. package/src/i18n/camera-motions.ar.ts +86 -0
  207. package/src/i18n/camera-motions.de.ts +74 -0
  208. package/src/i18n/camera-motions.es.ts +86 -0
  209. package/src/i18n/camera-motions.fr.ts +86 -0
  210. package/src/i18n/camera-motions.he.ts +86 -0
  211. package/src/i18n/camera-motions.hi.ts +99 -0
  212. package/src/i18n/camera-motions.ja.ts +99 -0
  213. package/src/i18n/camera-motions.ko.ts +99 -0
  214. package/src/i18n/camera-motions.pt-BR.ts +99 -0
  215. package/src/i18n/camera-motions.ru.ts +99 -0
  216. package/src/i18n/camera-motions.zh-CN.ts +99 -0
  217. package/src/i18n/character-fx.ar.ts +72 -0
  218. package/src/i18n/character-fx.de.ts +72 -0
  219. package/src/i18n/character-fx.es.ts +72 -0
  220. package/src/i18n/character-fx.fr.ts +72 -0
  221. package/src/i18n/character-fx.he.ts +72 -0
  222. package/src/i18n/character-fx.hi.ts +72 -0
  223. package/src/i18n/character-fx.ja.ts +72 -0
  224. package/src/i18n/character-fx.ko.ts +72 -0
  225. package/src/i18n/character-fx.pt-BR.ts +72 -0
  226. package/src/i18n/character-fx.ru.ts +72 -0
  227. package/src/i18n/character-fx.zh-CN.ts +72 -0
  228. package/src/i18n/color-look.ar.ts +50 -0
  229. package/src/i18n/color-look.de.ts +47 -0
  230. package/src/i18n/color-look.es.ts +50 -0
  231. package/src/i18n/color-look.fr.ts +50 -0
  232. package/src/i18n/color-look.he.ts +49 -0
  233. package/src/i18n/color-look.hi.ts +52 -0
  234. package/src/i18n/color-look.ja.ts +54 -0
  235. package/src/i18n/color-look.ko.ts +54 -0
  236. package/src/i18n/color-look.pt-BR.ts +54 -0
  237. package/src/i18n/color-look.ru.ts +52 -0
  238. package/src/i18n/color-look.zh-CN.ts +53 -0
  239. package/src/i18n/composition-effects.ar.ts +26 -0
  240. package/src/i18n/composition-effects.de.ts +25 -0
  241. package/src/i18n/composition-effects.es.ts +27 -0
  242. package/src/i18n/composition-effects.fr.ts +25 -0
  243. package/src/i18n/composition-effects.he.ts +25 -0
  244. package/src/i18n/composition-effects.hi.ts +27 -0
  245. package/src/i18n/composition-effects.ja.ts +27 -0
  246. package/src/i18n/composition-effects.ko.ts +27 -0
  247. package/src/i18n/composition-effects.pt-BR.ts +25 -0
  248. package/src/i18n/composition-effects.ru.ts +82 -0
  249. package/src/i18n/composition-effects.zh-CN.ts +25 -0
  250. package/src/i18n/era.ar.ts +37 -0
  251. package/src/i18n/era.de.ts +34 -0
  252. package/src/i18n/era.es.ts +37 -0
  253. package/src/i18n/era.fr.ts +37 -0
  254. package/src/i18n/era.he.ts +36 -0
  255. package/src/i18n/era.hi.ts +39 -0
  256. package/src/i18n/era.ja.ts +41 -0
  257. package/src/i18n/era.ko.ts +39 -0
  258. package/src/i18n/era.pt-BR.ts +41 -0
  259. package/src/i18n/era.ru.ts +121 -0
  260. package/src/i18n/era.zh-CN.ts +39 -0
  261. package/src/i18n/exposure-settings.ar.ts +28 -0
  262. package/src/i18n/exposure-settings.de.ts +26 -0
  263. package/src/i18n/exposure-settings.es.ts +28 -0
  264. package/src/i18n/exposure-settings.fr.ts +29 -0
  265. package/src/i18n/exposure-settings.he.ts +28 -0
  266. package/src/i18n/exposure-settings.hi.ts +31 -0
  267. package/src/i18n/exposure-settings.ja.ts +31 -0
  268. package/src/i18n/exposure-settings.ko.ts +31 -0
  269. package/src/i18n/exposure-settings.pt-BR.ts +31 -0
  270. package/src/i18n/exposure-settings.ru.ts +31 -0
  271. package/src/i18n/exposure-settings.zh-CN.ts +31 -0
  272. package/src/i18n/framing.ar.ts +78 -0
  273. package/src/i18n/framing.de.ts +73 -0
  274. package/src/i18n/framing.es.ts +78 -0
  275. package/src/i18n/framing.fr.ts +78 -0
  276. package/src/i18n/framing.he.ts +77 -0
  277. package/src/i18n/framing.hi.ts +82 -0
  278. package/src/i18n/framing.ja.ts +84 -0
  279. package/src/i18n/framing.ko.ts +82 -0
  280. package/src/i18n/framing.pt-BR.ts +84 -0
  281. package/src/i18n/framing.ru.ts +82 -0
  282. package/src/i18n/framing.zh-CN.ts +82 -0
  283. package/src/i18n/furniture.ar.ts +92 -0
  284. package/src/i18n/furniture.de.ts +93 -0
  285. package/src/i18n/furniture.es.ts +101 -0
  286. package/src/i18n/furniture.fr.ts +93 -0
  287. package/src/i18n/furniture.he.ts +101 -0
  288. package/src/i18n/furniture.hi.ts +101 -0
  289. package/src/i18n/furniture.ja.ts +101 -0
  290. package/src/i18n/furniture.ko.ts +101 -0
  291. package/src/i18n/furniture.pt-BR.ts +101 -0
  292. package/src/i18n/furniture.ru.ts +101 -0
  293. package/src/i18n/furniture.zh-CN.ts +101 -0
  294. package/src/i18n/held-prop.ar.ts +74 -0
  295. package/src/i18n/held-prop.de.ts +65 -0
  296. package/src/i18n/held-prop.es.ts +83 -0
  297. package/src/i18n/held-prop.fr.ts +74 -0
  298. package/src/i18n/held-prop.he.ts +73 -0
  299. package/src/i18n/held-prop.hi.ts +82 -0
  300. package/src/i18n/held-prop.ja.ts +84 -0
  301. package/src/i18n/held-prop.ko.ts +84 -0
  302. package/src/i18n/held-prop.pt-BR.ts +84 -0
  303. package/src/i18n/held-prop.ru.ts +82 -0
  304. package/src/i18n/held-prop.zh-CN.ts +82 -0
  305. package/src/i18n/index.ts +208 -0
  306. package/src/i18n/instrumentation.ar.ts +195 -0
  307. package/src/i18n/instrumentation.de.ts +195 -0
  308. package/src/i18n/instrumentation.es.ts +195 -0
  309. package/src/i18n/instrumentation.fr.ts +195 -0
  310. package/src/i18n/instrumentation.he.ts +195 -0
  311. package/src/i18n/instrumentation.hi.ts +195 -0
  312. package/src/i18n/instrumentation.ja.ts +195 -0
  313. package/src/i18n/instrumentation.ko.ts +195 -0
  314. package/src/i18n/instrumentation.pt-BR.ts +195 -0
  315. package/src/i18n/instrumentation.ru.ts +195 -0
  316. package/src/i18n/instrumentation.zh-CN.ts +195 -0
  317. package/src/i18n/lens.ar.ts +24 -0
  318. package/src/i18n/lens.de.ts +24 -0
  319. package/src/i18n/lens.es.ts +24 -0
  320. package/src/i18n/lens.fr.ts +24 -0
  321. package/src/i18n/lens.he.ts +24 -0
  322. package/src/i18n/lens.hi.ts +24 -0
  323. package/src/i18n/lens.ja.ts +24 -0
  324. package/src/i18n/lens.ko.ts +24 -0
  325. package/src/i18n/lens.pt-BR.ts +24 -0
  326. package/src/i18n/lens.ru.ts +25 -0
  327. package/src/i18n/lens.zh-CN.ts +24 -0
  328. package/src/i18n/lighting.ar.ts +86 -0
  329. package/src/i18n/lighting.de.ts +84 -0
  330. package/src/i18n/lighting.es.ts +89 -0
  331. package/src/i18n/lighting.fr.ts +84 -0
  332. package/src/i18n/lighting.he.ts +88 -0
  333. package/src/i18n/lighting.hi.ts +90 -0
  334. package/src/i18n/lighting.ja.ts +90 -0
  335. package/src/i18n/lighting.ko.ts +90 -0
  336. package/src/i18n/lighting.pt-BR.ts +90 -0
  337. package/src/i18n/lighting.ru.ts +88 -0
  338. package/src/i18n/lighting.zh-CN.ts +92 -0
  339. package/src/i18n/loop-subject.ar.ts +39 -0
  340. package/src/i18n/loop-subject.de.ts +39 -0
  341. package/src/i18n/loop-subject.es.ts +39 -0
  342. package/src/i18n/loop-subject.fr.ts +39 -0
  343. package/src/i18n/loop-subject.he.ts +39 -0
  344. package/src/i18n/loop-subject.hi.ts +39 -0
  345. package/src/i18n/loop-subject.ja.ts +39 -0
  346. package/src/i18n/loop-subject.ko.ts +39 -0
  347. package/src/i18n/loop-subject.pt-BR.ts +39 -0
  348. package/src/i18n/loop-subject.ru.ts +39 -0
  349. package/src/i18n/loop-subject.zh-CN.ts +39 -0
  350. package/src/i18n/materials.ar.ts +80 -0
  351. package/src/i18n/materials.de.ts +80 -0
  352. package/src/i18n/materials.es.ts +87 -0
  353. package/src/i18n/materials.fr.ts +80 -0
  354. package/src/i18n/materials.he.ts +86 -0
  355. package/src/i18n/materials.hi.ts +86 -0
  356. package/src/i18n/materials.ja.ts +88 -0
  357. package/src/i18n/materials.ko.ts +88 -0
  358. package/src/i18n/materials.pt-BR.ts +88 -0
  359. package/src/i18n/materials.ru.ts +86 -0
  360. package/src/i18n/materials.zh-CN.ts +86 -0
  361. package/src/i18n/mood.ar.ts +61 -0
  362. package/src/i18n/mood.de.ts +61 -0
  363. package/src/i18n/mood.es.ts +66 -0
  364. package/src/i18n/mood.fr.ts +61 -0
  365. package/src/i18n/mood.he.ts +64 -0
  366. package/src/i18n/mood.hi.ts +66 -0
  367. package/src/i18n/mood.ja.ts +66 -0
  368. package/src/i18n/mood.ko.ts +66 -0
  369. package/src/i18n/mood.pt-BR.ts +64 -0
  370. package/src/i18n/mood.ru.ts +64 -0
  371. package/src/i18n/mood.zh-CN.ts +64 -0
  372. package/src/i18n/music-genre.ar.ts +302 -0
  373. package/src/i18n/music-genre.de.ts +302 -0
  374. package/src/i18n/music-genre.es.ts +302 -0
  375. package/src/i18n/music-genre.fr.ts +302 -0
  376. package/src/i18n/music-genre.he.ts +302 -0
  377. package/src/i18n/music-genre.hi.ts +302 -0
  378. package/src/i18n/music-genre.ja.ts +302 -0
  379. package/src/i18n/music-genre.ko.ts +302 -0
  380. package/src/i18n/music-genre.pt-BR.ts +302 -0
  381. package/src/i18n/music-genre.ru.ts +302 -0
  382. package/src/i18n/music-genre.zh-CN.ts +302 -0
  383. package/src/i18n/music-mood.ar.ts +75 -0
  384. package/src/i18n/music-mood.de.ts +75 -0
  385. package/src/i18n/music-mood.es.ts +75 -0
  386. package/src/i18n/music-mood.fr.ts +75 -0
  387. package/src/i18n/music-mood.he.ts +75 -0
  388. package/src/i18n/music-mood.hi.ts +75 -0
  389. package/src/i18n/music-mood.ja.ts +75 -0
  390. package/src/i18n/music-mood.ko.ts +75 -0
  391. package/src/i18n/music-mood.pt-BR.ts +75 -0
  392. package/src/i18n/music-mood.ru.ts +75 -0
  393. package/src/i18n/music-mood.zh-CN.ts +75 -0
  394. package/src/i18n/person.ar.ts +638 -0
  395. package/src/i18n/person.de.ts +629 -0
  396. package/src/i18n/person.es.ts +645 -0
  397. package/src/i18n/person.fr.ts +621 -0
  398. package/src/i18n/person.he.ts +656 -0
  399. package/src/i18n/person.hi.ts +654 -0
  400. package/src/i18n/person.ja.ts +662 -0
  401. package/src/i18n/person.ko.ts +662 -0
  402. package/src/i18n/person.pt-BR.ts +656 -0
  403. package/src/i18n/person.ru.ts +656 -0
  404. package/src/i18n/person.zh-CN.ts +656 -0
  405. package/src/i18n/photo-genre.ar.ts +58 -0
  406. package/src/i18n/photo-genre.de.ts +52 -0
  407. package/src/i18n/photo-genre.es.ts +64 -0
  408. package/src/i18n/photo-genre.fr.ts +58 -0
  409. package/src/i18n/photo-genre.he.ts +62 -0
  410. package/src/i18n/photo-genre.hi.ts +63 -0
  411. package/src/i18n/photo-genre.ja.ts +65 -0
  412. package/src/i18n/photo-genre.ko.ts +63 -0
  413. package/src/i18n/photo-genre.pt-BR.ts +65 -0
  414. package/src/i18n/photo-genre.ru.ts +63 -0
  415. package/src/i18n/photo-genre.zh-CN.ts +63 -0
  416. package/src/i18n/photographer.ar.ts +75 -0
  417. package/src/i18n/photographer.de.ts +74 -0
  418. package/src/i18n/photographer.es.ts +77 -0
  419. package/src/i18n/photographer.fr.ts +77 -0
  420. package/src/i18n/photographer.he.ts +78 -0
  421. package/src/i18n/photographer.hi.ts +80 -0
  422. package/src/i18n/photographer.ja.ts +84 -0
  423. package/src/i18n/photographer.ko.ts +84 -0
  424. package/src/i18n/photographer.pt-BR.ts +83 -0
  425. package/src/i18n/photographer.ru.ts +78 -0
  426. package/src/i18n/photographer.zh-CN.ts +78 -0
  427. package/src/i18n/pose.ar.ts +95 -0
  428. package/src/i18n/pose.de.ts +96 -0
  429. package/src/i18n/pose.es.ts +104 -0
  430. package/src/i18n/pose.fr.ts +96 -0
  431. package/src/i18n/pose.he.ts +104 -0
  432. package/src/i18n/pose.hi.ts +104 -0
  433. package/src/i18n/pose.ja.ts +104 -0
  434. package/src/i18n/pose.ko.ts +104 -0
  435. package/src/i18n/pose.pt-BR.ts +104 -0
  436. package/src/i18n/pose.ru.ts +104 -0
  437. package/src/i18n/pose.zh-CN.ts +104 -0
  438. package/src/i18n/post-process-effects.ar.ts +25 -0
  439. package/src/i18n/post-process-effects.de.ts +24 -0
  440. package/src/i18n/post-process-effects.es.ts +26 -0
  441. package/src/i18n/post-process-effects.fr.ts +24 -0
  442. package/src/i18n/post-process-effects.he.ts +24 -0
  443. package/src/i18n/post-process-effects.hi.ts +26 -0
  444. package/src/i18n/post-process-effects.ja.ts +26 -0
  445. package/src/i18n/post-process-effects.ko.ts +26 -0
  446. package/src/i18n/post-process-effects.pt-BR.ts +24 -0
  447. package/src/i18n/post-process-effects.ru.ts +24 -0
  448. package/src/i18n/post-process-effects.zh-CN.ts +24 -0
  449. package/src/i18n/render-quality.ar.ts +34 -0
  450. package/src/i18n/render-quality.de.ts +30 -0
  451. package/src/i18n/render-quality.es.ts +35 -0
  452. package/src/i18n/render-quality.fr.ts +34 -0
  453. package/src/i18n/render-quality.he.ts +33 -0
  454. package/src/i18n/render-quality.hi.ts +39 -0
  455. package/src/i18n/render-quality.ja.ts +39 -0
  456. package/src/i18n/render-quality.ko.ts +39 -0
  457. package/src/i18n/render-quality.pt-BR.ts +37 -0
  458. package/src/i18n/render-quality.ru.ts +37 -0
  459. package/src/i18n/render-quality.zh-CN.ts +37 -0
  460. package/src/i18n/seasons.ar.ts +10 -0
  461. package/src/i18n/seasons.de.ts +10 -0
  462. package/src/i18n/seasons.es.ts +10 -0
  463. package/src/i18n/seasons.fr.ts +10 -0
  464. package/src/i18n/seasons.he.ts +10 -0
  465. package/src/i18n/seasons.hi.ts +10 -0
  466. package/src/i18n/seasons.ja.ts +10 -0
  467. package/src/i18n/seasons.ko.ts +10 -0
  468. package/src/i18n/seasons.pt-BR.ts +10 -0
  469. package/src/i18n/seasons.ru.ts +10 -0
  470. package/src/i18n/seasons.zh-CN.ts +10 -0
  471. package/src/i18n/setting.ar.ts +73 -0
  472. package/src/i18n/setting.de.ts +73 -0
  473. package/src/i18n/setting.es.ts +77 -0
  474. package/src/i18n/setting.fr.ts +73 -0
  475. package/src/i18n/setting.he.ts +76 -0
  476. package/src/i18n/setting.hi.ts +76 -0
  477. package/src/i18n/setting.ja.ts +78 -0
  478. package/src/i18n/setting.ko.ts +76 -0
  479. package/src/i18n/setting.pt-BR.ts +78 -0
  480. package/src/i18n/setting.ru.ts +76 -0
  481. package/src/i18n/setting.zh-CN.ts +76 -0
  482. package/src/i18n/style.ar.ts +55 -0
  483. package/src/i18n/style.de.ts +54 -0
  484. package/src/i18n/style.es.ts +54 -0
  485. package/src/i18n/style.fr.ts +54 -0
  486. package/src/i18n/style.he.ts +54 -0
  487. package/src/i18n/style.hi.ts +54 -0
  488. package/src/i18n/style.ja.ts +54 -0
  489. package/src/i18n/style.ko.ts +54 -0
  490. package/src/i18n/style.pt-BR.ts +56 -0
  491. package/src/i18n/style.ru.ts +54 -0
  492. package/src/i18n/style.zh-CN.ts +54 -0
  493. package/src/i18n/styling.ar.ts +295 -0
  494. package/src/i18n/styling.de.ts +285 -0
  495. package/src/i18n/styling.es.ts +317 -0
  496. package/src/i18n/styling.fr.ts +285 -0
  497. package/src/i18n/styling.he.ts +307 -0
  498. package/src/i18n/styling.hi.ts +323 -0
  499. package/src/i18n/styling.ja.ts +317 -0
  500. package/src/i18n/styling.ko.ts +311 -0
  501. package/src/i18n/styling.pt-BR.ts +301 -0
  502. package/src/i18n/styling.ru.ts +301 -0
  503. package/src/i18n/styling.zh-CN.ts +307 -0
  504. package/src/i18n/temporal.ar.ts +31 -0
  505. package/src/i18n/temporal.de.ts +24 -0
  506. package/src/i18n/temporal.es.ts +27 -0
  507. package/src/i18n/temporal.fr.ts +28 -0
  508. package/src/i18n/temporal.he.ts +31 -0
  509. package/src/i18n/temporal.hi.ts +31 -0
  510. package/src/i18n/temporal.ja.ts +31 -0
  511. package/src/i18n/temporal.ko.ts +31 -0
  512. package/src/i18n/temporal.pt-BR.ts +31 -0
  513. package/src/i18n/temporal.ru.ts +31 -0
  514. package/src/i18n/temporal.zh-CN.ts +31 -0
  515. package/src/i18n/transitions.ar.ts +97 -0
  516. package/src/i18n/transitions.de.ts +97 -0
  517. package/src/i18n/transitions.es.ts +97 -0
  518. package/src/i18n/transitions.fr.ts +97 -0
  519. package/src/i18n/transitions.he.ts +97 -0
  520. package/src/i18n/transitions.hi.ts +97 -0
  521. package/src/i18n/transitions.ja.ts +97 -0
  522. package/src/i18n/transitions.ko.ts +97 -0
  523. package/src/i18n/transitions.pt-BR.ts +97 -0
  524. package/src/i18n/transitions.ru.ts +97 -0
  525. package/src/i18n/transitions.zh-CN.ts +97 -0
  526. package/src/i18n/types.ts +158 -0
  527. package/src/i18n/vehicles.ar.ts +137 -0
  528. package/src/i18n/vehicles.de.ts +125 -0
  529. package/src/i18n/vehicles.es.ts +138 -0
  530. package/src/i18n/vehicles.fr.ts +125 -0
  531. package/src/i18n/vehicles.he.ts +136 -0
  532. package/src/i18n/vehicles.hi.ts +138 -0
  533. package/src/i18n/vehicles.ja.ts +138 -0
  534. package/src/i18n/vehicles.ko.ts +138 -0
  535. package/src/i18n/vehicles.pt-BR.ts +136 -0
  536. package/src/i18n/vehicles.ru.ts +136 -0
  537. package/src/i18n/vehicles.zh-CN.ts +136 -0
  538. package/src/i18n/voice-character.ar.ts +126 -0
  539. package/src/i18n/voice-character.de.ts +126 -0
  540. package/src/i18n/voice-character.es.ts +126 -0
  541. package/src/i18n/voice-character.fr.ts +126 -0
  542. package/src/i18n/voice-character.he.ts +126 -0
  543. package/src/i18n/voice-character.hi.ts +126 -0
  544. package/src/i18n/voice-character.ja.ts +126 -0
  545. package/src/i18n/voice-character.ko.ts +126 -0
  546. package/src/i18n/voice-character.pt-BR.ts +126 -0
  547. package/src/i18n/voice-character.ru.ts +126 -0
  548. package/src/i18n/voice-character.zh-CN.ts +126 -0
  549. package/src/i18n/voice-delivery.ar.ts +77 -0
  550. package/src/i18n/voice-delivery.de.ts +77 -0
  551. package/src/i18n/voice-delivery.es.ts +77 -0
  552. package/src/i18n/voice-delivery.fr.ts +77 -0
  553. package/src/i18n/voice-delivery.he.ts +77 -0
  554. package/src/i18n/voice-delivery.hi.ts +77 -0
  555. package/src/i18n/voice-delivery.ja.ts +77 -0
  556. package/src/i18n/voice-delivery.ko.ts +77 -0
  557. package/src/i18n/voice-delivery.pt-BR.ts +77 -0
  558. package/src/i18n/voice-delivery.ru.ts +77 -0
  559. package/src/i18n/voice-delivery.zh-CN.ts +77 -0
  560. package/src/i18n/weapons.ar.ts +115 -0
  561. package/src/i18n/weapons.de.ts +114 -0
  562. package/src/i18n/weapons.es.ts +116 -0
  563. package/src/i18n/weapons.fr.ts +103 -0
  564. package/src/i18n/weapons.he.ts +114 -0
  565. package/src/i18n/weapons.hi.ts +116 -0
  566. package/src/i18n/weapons.ja.ts +116 -0
  567. package/src/i18n/weapons.ko.ts +116 -0
  568. package/src/i18n/weapons.pt-BR.ts +114 -0
  569. package/src/i18n/weapons.ru.ts +114 -0
  570. package/src/i18n/weapons.zh-CN.ts +114 -0
  571. package/src/image-critic-node.ts +61 -0
  572. package/src/image-critic-types.ts +40 -0
  573. package/src/index.ts +866 -0
  574. package/src/inject-upstream.ts +15 -0
  575. package/src/inpaint-region.ts +51 -0
  576. package/src/json-evaluator.ts +732 -0
  577. package/src/json-path.ts +52 -0
  578. package/src/lip-sync-pricing.ts +90 -0
  579. package/src/list-merge.ts +40 -0
  580. package/src/list-sort.ts +122 -0
  581. package/src/list-source-resolver.ts +50 -0
  582. package/src/llm-models.ts +245 -0
  583. package/src/location-mention-slug.ts +291 -0
  584. package/src/location-preset-catalog-map.ts +128 -0
  585. package/src/loop-delimiter.ts +73 -0
  586. package/src/lora-routing.ts +95 -0
  587. package/src/lottie-overlay-catalog.ts +175 -0
  588. package/src/lottie-slots.ts +309 -0
  589. package/src/model-catalog.ts +2386 -0
  590. package/src/model-constants.ts +1910 -0
  591. package/src/model-tree.ts +120 -0
  592. package/src/monetization.ts +15 -0
  593. package/src/multi-pick.ts +42 -0
  594. package/src/node-default-mappings.ts +262 -0
  595. package/src/node-mappable-fields.ts +70 -0
  596. package/src/node-preset-export.ts +56 -0
  597. package/src/node-preset-extract.ts +147 -0
  598. package/src/node-refs.ts +253 -0
  599. package/src/node-runtime-keys.ts +117 -0
  600. package/src/object-aspect-defaults.ts +72 -0
  601. package/src/object-picker-types.ts +22 -0
  602. package/src/parameter-node-value.ts +284 -0
  603. package/src/pinterest-url.ts +28 -0
  604. package/src/pipeline-chat.ts +72 -0
  605. package/src/pipeline-defaults.ts +318 -0
  606. package/src/pipeline-events.ts +162 -0
  607. package/src/pipeline-state-types.ts +196 -0
  608. package/src/pipeline-types.ts +647 -0
  609. package/src/pipeline-validation.ts +87 -0
  610. package/src/presentation-types.ts +23 -0
  611. package/src/presentation-utils.ts +397 -0
  612. package/src/producer-types.ts +188 -0
  613. package/src/progress-curve.ts +99 -0
  614. package/src/provider-directive-defaults.ts +69 -0
  615. package/src/reduce-strategy-registry.ts +121 -0
  616. package/src/reference-board-templates.ts +81 -0
  617. package/src/reference-roles.ts +153 -0
  618. package/src/reference-sheet/__tests__/board-to-column.test.ts +20 -0
  619. package/src/reference-sheet/__tests__/catalog.test.ts +50 -0
  620. package/src/reference-sheet/__tests__/cost.test.ts +38 -0
  621. package/src/reference-sheet/__tests__/no-drift.test.ts +28 -0
  622. package/src/reference-sheet/__tests__/panel-plan.test.ts +61 -0
  623. package/src/reference-sheet/__tests__/panel-prompts.test.ts +17 -0
  624. package/src/reference-sheet/__tests__/plan-generation.test.ts +72 -0
  625. package/src/reference-sheet/__tests__/presets.test.ts +54 -0
  626. package/src/reference-sheet/__tests__/types.test.ts +24 -0
  627. package/src/reference-sheet/catalog.ts +144 -0
  628. package/src/reference-sheet/cost.ts +37 -0
  629. package/src/reference-sheet/credit-id.ts +7 -0
  630. package/src/reference-sheet/index.ts +8 -0
  631. package/src/reference-sheet/panel-plan.ts +65 -0
  632. package/src/reference-sheet/panel-prompts.ts +14 -0
  633. package/src/reference-sheet/plan-generation.ts +79 -0
  634. package/src/reference-sheet/presets.ts +77 -0
  635. package/src/reference-sheet/types.ts +92 -0
  636. package/src/repeat-types.ts +97 -0
  637. package/src/resolve-field-mappings.ts +119 -0
  638. package/src/route-filter.ts +80 -0
  639. package/src/scene-helper-types.ts +249 -0
  640. package/src/scene-node-types.ts +281 -0
  641. package/src/scraper-actors.ts +57 -0
  642. package/src/scraper-output-schemas.ts +25 -0
  643. package/src/seasons.ts +45 -0
  644. package/src/selector.ts +864 -0
  645. package/src/settled-with-limit.ts +42 -0
  646. package/src/shot-element.ts +43 -0
  647. package/src/social-media-specs.ts +121 -0
  648. package/src/social-post.ts +24 -0
  649. package/src/sub-workflow-validation.ts +78 -0
  650. package/src/supported-fonts.ts +40 -0
  651. package/src/surround.ts +113 -0
  652. package/src/switchx-pricing.ts +33 -0
  653. package/src/text-separators.ts +59 -0
  654. package/src/to-connected-references.ts +77 -0
  655. package/src/types.ts +272 -0
  656. package/src/variant-job-id.ts +17 -0
  657. package/src/vehicles.ts +213 -0
  658. package/src/video-analysis-pricing.ts +74 -0
  659. package/src/video-analysis.ts +127 -0
  660. package/src/video-duration.ts +25 -0
  661. package/src/voice-changer-models.ts +36 -0
  662. package/src/voice-matcher-types.ts +37 -0
  663. package/src/voices.ts +92 -0
  664. package/src/weapons.ts +190 -0
  665. package/src/workflow-export.ts +153 -0
@@ -0,0 +1,1910 @@
1
+ /**
2
+ * Model constants shared between frontend and backend.
3
+ * Single source of truth for model capability sets and variable pricing rules.
4
+ */
5
+ import { z } from "zod"
6
+ import { MODEL_CATALOG } from "./model-catalog.js"
7
+
8
+ /** Base USD cost per 1 Nodaro credit, at cost. Used for cost→credit conversion. */
9
+ export const CREDIT_BASE_USD = 0.02
10
+
11
+ /** Max characters for the (assembled) prompt accepted by the image-generation routes
12
+ * (generate-image, image-to-image, edit-image). Single source of truth — the route Zod
13
+ * schemas and the factory-preset guard test all read this. Prompt cap ONLY (not negativePrompt). */
14
+ export const IMAGE_PROMPT_MAX = 5000
15
+
16
+ /**
17
+ * Per-provider maximum ASSEMBLED image-prompt length (chars), VERIFIED against
18
+ * each model's official docs.kie.ai schema (2026-06). Providers absent here use
19
+ * {@link IMAGE_PROMPT_MAX} (the documented KIE "standard" of 5000). Read via
20
+ * {@link getMaxImagePromptChars} by the prompt assembler's truncation AND the
21
+ * frontend over-limit warning — the data-driven replacement for the old
22
+ * hardcoded 2000 that silently severed appended cinematography hints + the
23
+ * `Avoid:` negative on long prompts.
24
+ *
25
+ * Both directions matter: some models accept FAR MORE than 5000 (nano-banana-2 /
26
+ * gpt-image-2-i2i = 20000) and some FAR LESS (seedream-5-lite = 1000) — sending
27
+ * 5000 to a 1000-char model is a provider-side rejection. Only list a value you
28
+ * can quote from the official schema; the t2i / i2i / edit siblings of one family
29
+ * genuinely differ (verified: qwen t2i 3000 vs qwen-i2i 5000 vs qwen-edit 2000),
30
+ * so never copy a sibling's number.
31
+ *
32
+ * Truncation is a graceful safety net, never a hard reject — the editor warns the
33
+ * user BEFORE submit (switch model / trim) and the assembler truncates if they
34
+ * proceed. So the image routes stay generous (a single high ceiling) and the
35
+ * per-model enforcement lives here.
36
+ */
37
+ export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
38
+ // ── higher than the 5000 default ──
39
+ "nano-banana-2": 20000, // docs.kie.ai/market/google/nano-banana-2
40
+ "nano-banana-pro": 20000, // docs.kie.ai/market/google/pro-image-to-image
41
+ "gpt-image-2-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-image-to-image
42
+ // ── lower than the 5000 default (over-send risk if left at default) ──
43
+ "seedream": 3000, // docs.kie.ai/market/seedream/4-5-text-to-image
44
+ "seedream-edit": 3000, // docs.kie.ai/market/seedream/4-5-edit
45
+ "seedream-5-lite": 1000, // docs.kie.ai/market/seedream/5-lite-text-to-image
46
+ "seedream-5-lite-i2i": 3000, // docs.kie.ai/market/seedream-5-lite-image-to-image (NB: t2i sibling is 1000)
47
+ "qwen": 3000, // docs.kie.ai/market/qwen/text-to-image
48
+ "qwen-edit": 2000, // docs.kie.ai/market/qwen/image-edit
49
+ // verified == 5000 default (no entry needed): imagen4(-fast/-ultra), nano-banana,
50
+ // nano-banana-edit, flux, flux-flex, gpt-image-2, ideogram-v3/-edit/-remix,
51
+ // z-image, grok, qwen-i2i.
52
+ // grok-i2i: doc states 390000 (78× its t2i sibling) — treated as a KIE schema
53
+ // typo and left at the 5000 default per the sanity-cap decision.
54
+ // UNVERIFIED (no limit stated in schema) → 5000 default: flux-kontext(-max)
55
+ // gpt-image, gpt-image-i2i, flux-i2i, flux-pro-i2i, ideogram-reframe.
56
+ }
57
+
58
+ /** Max assembled image-prompt length (chars) for a provider: its verified
59
+ * override from {@link MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER}, else the
60
+ * {@link IMAGE_PROMPT_MAX} default. Single source of truth shared by the prompt
61
+ * assembler, the over-limit warning, and the image-route ceiling. */
62
+ export function getMaxImagePromptChars(provider: string | undefined): number {
63
+ return (provider && MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER[provider]) || IMAGE_PROMPT_MAX
64
+ }
65
+
66
+ /**
67
+ * Video prompt/negative cap shared by routes + editor (MCP verbs already use
68
+ * 8000). Providers allow far more (KIE seedance-2: 20k chars) — the old
69
+ * per-route 2500 was a false-positive blocker for legitimate multi-shot
70
+ * Seedance prompts.
71
+ */
72
+ export const VIDEO_PROMPT_MAX = 8000
73
+
74
+ /**
75
+ * Suno prompt / lyrics / content ceiling — the LARGEST any Suno version accepts
76
+ * in custom mode (V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5 = 5000). The route Zod
77
+ * uses this as a generous ceiling; the handler clamps to the per-version cap via
78
+ * {@link getMaxSunoPromptChars} (V4/V3.5 = 3000, non-custom = 500). Shared with
79
+ * the editor `maxLength` / counter (warn-don't-block at the per-version cap).
80
+ * `style` and `title` have their own caps ({@link getMaxSunoStyleChars} /
81
+ * {@link SUNO_TITLE_MAX}).
82
+ */
83
+ export const SUNO_TEXT_MAX = 5000
84
+
85
+ /**
86
+ * Absolute ceiling for the `prompt` / `negativePrompt` fields on the image and
87
+ * video routes' Zod schemas. The PER-MODEL limits below (and the editor warning)
88
+ * do the real work; the routes stay generous so they never hard-reject a legit
89
+ * long prompt — the assembler/payload truncates to the model cap as a graceful
90
+ * safety net (warn-don't-block). 20000 = the largest verified per-model image/
91
+ * video limit (seedance-2 / nano-banana-2 / gemini-omni). Anything past this is
92
+ * abuse, not a real prompt.
93
+ */
94
+ export const PROMPT_HARD_CEILING = 20000
95
+
96
+ /**
97
+ * Per-provider maximum VIDEO prompt length (chars), VERIFIED against each model's
98
+ * official docs.kie.ai schema (2026-06). Absent → {@link VIDEO_PROMPT_MAX} (8000)
99
+ * default (used for models whose schema states NO limit: veo3 family, kling-3.0,
100
+ * kling-3-omni, runway-aleph). Read via {@link getMaxVideoPromptChars}.
101
+ *
102
+ * Most video models cap WELL BELOW the old flat 8000 (kling 2.6 = 1000!), so the
103
+ * 8000 route value was over-sending to nearly every provider. A few accept much
104
+ * more (seedance-2 = 20000). Mode note: where a model's t2v and i2v schemas
105
+ * differ, the SMALLER is used so neither mode over-sends (hailuo-2.3-pro: t2v
106
+ * 1500 / i2v 5000 → 1500).
107
+ */
108
+ export const MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
109
+ // Kling family — our `kling`→kling-2.6 (1000), `kling-turbo`→v2.5-turbo-pro (2500)
110
+ "kling": 1000,
111
+ "kling-turbo": 2500,
112
+ "kling-master": 5000,
113
+ // higher than the 8000 default
114
+ "seedance-2": 20000,
115
+ "seedance-2-fast": 20000,
116
+ "seedance-2-mini": 20000,
117
+ "seedance-2-extend": 20000,
118
+ "gemini-omni-video": 20000,
119
+ "bytedance-lite": 10000,
120
+ "bytedance-pro": 10000,
121
+ "bytedance-pro-fast": 10000,
122
+ // lower than the 8000 default
123
+ "minimax": 1500,
124
+ "hailuo-standard": 1500,
125
+ "hailuo-2.3-pro": 1500, // t2v 1500 / i2v 5000 → smaller, safe for both modes
126
+ "hailuo-2.3": 5000,
127
+ "seedance": 2500, // Seedance 1.5 Pro
128
+ "wan": 5000,
129
+ "wan-i2v": 5000,
130
+ "wan-turbo": 5000,
131
+ "wan-flash": 1500,
132
+ "wan-videoedit": 5000,
133
+ "wan-2.7-i2v": 5000,
134
+ "wan-2.7-t2v": 5000,
135
+ "grok-i2v": 5000,
136
+ "grok-imagine-video-1.5": 4096,
137
+ "happyhorse": 5000,
138
+ "happyhorse-i2v": 5000,
139
+ "happyhorse-ref2v": 5000,
140
+ "happyhorse-edit": 5000,
141
+ "runway-kie": 1800,
142
+ // NB: `wan-2.7` / `wan-2.7-pro` are IMAGE models (see the image registry), not
143
+ // video. The motion-transfer route's prompt is capped at the motion-control
144
+ // limit (2500) inside the route itself — its provider values are the shared
145
+ // `kling` (→1000 here, safely tighter) / `kling-3.0` ids, not a "motion-*" id.
146
+ // UNVERIFIED (schema states no limit) → 8000 default: veo3, veo3.1, veo3_lite
147
+ // kling-3.0, kling-3-omni, runway-aleph.
148
+ }
149
+
150
+ /** Max video-prompt length (chars) for a provider: verified override, else
151
+ * {@link VIDEO_PROMPT_MAX}. Shared by the video routes/payload truncation and
152
+ * the editor over-limit warning. */
153
+ export function getMaxVideoPromptChars(provider: string | undefined): number {
154
+ return (provider && MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER[provider]) || VIDEO_PROMPT_MAX
155
+ }
156
+
157
+ /** Default negative-prompt cap when a provider has no specific verified limit. */
158
+ export const NEGATIVE_PROMPT_MAX = 5000
159
+
160
+ /**
161
+ * Per-provider native `negative_prompt` cap (chars), VERIFIED from docs.kie.ai.
162
+ * Only matters for providers that send the negative as a NATIVE param
163
+ * ({@link NATIVE_NEGATIVE_PROMPT_MODELS} / {@link NATIVE_NEGATIVE_VIDEO_PROVIDERS});
164
+ * everywhere else the negative is folded into the prompt as `Avoid: …` and rides
165
+ * the prompt cap. Absent → {@link NEGATIVE_PROMPT_MAX}.
166
+ */
167
+ export const MAX_NEGATIVE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
168
+ // image
169
+ "imagen4": 5000, "imagen4-fast": 5000, "imagen4-ultra": 5000,
170
+ "ideogram-v3": 500, "ideogram-remix": 5000,
171
+ "qwen": 500, "qwen-i2i": 500, "qwen-edit": 500,
172
+ // video (native-negative families)
173
+ "kling-master": 500,
174
+ "kling-turbo": 500, // i2v 500 / t2v 2500 → smaller, safe for both modes
175
+ "wan-2.7-i2v": 500, "wan-2.7-t2v": 500, "wan-videoedit": 500,
176
+ }
177
+
178
+ /** Max native negative-prompt length (chars) for a provider. */
179
+ export function getMaxNegativePromptChars(provider: string | undefined): number {
180
+ return (provider && MAX_NEGATIVE_PROMPT_CHARS_BY_PROVIDER[provider]) || NEGATIVE_PROMPT_MAX
181
+ }
182
+
183
+ /** Default TTS text cap (legacy `elevenlabs`) when no per-model override. */
184
+ export const TTS_TEXT_MAX = 5000
185
+
186
+ /**
187
+ * Per-model Text-to-Speech character cap (PER REQUEST), from official ElevenLabs
188
+ * docs. turbo/multilingual accept FAR more than the old flat 5000; v3 uses the
189
+ * conservative 3000 (official pages say 5000 but the API is widely reported to
190
+ * hard-limit v3 at 3000 — conservative avoids prod rejections). Absent →
191
+ * {@link TTS_TEXT_MAX}.
192
+ */
193
+ export const MAX_TTS_CHARS_BY_PROVIDER: Record<string, number> = {
194
+ "elevenlabs-turbo": 40000, // == eleven_flash_v2_5 (functionally equivalent)
195
+ "elevenlabs-multilingual": 10000, // eleven_multilingual_v2
196
+ "elevenlabs-v3": 3000, // conservative (official 5000 / API-reported 3000)
197
+ "elevenlabs-dialogue": 2000, // text-to-dialogue recommended per-request max
198
+ }
199
+
200
+ /** Max TTS text length (chars) for a provider: verified override, else {@link TTS_TEXT_MAX}. */
201
+ export function getMaxTtsChars(provider: string | undefined): number {
202
+ return (provider && MAX_TTS_CHARS_BY_PROVIDER[provider]) || TTS_TEXT_MAX
203
+ }
204
+
205
+ /**
206
+ * Suno per-version field caps (from docs.kie.ai/suno-api/generate-music). The old
207
+ * flat {@link SUNO_TEXT_MAX} (3000) was simultaneously too low for V4.5+/V5
208
+ * prompts (5000) and too high for `style` (1000) and `title` (80).
209
+ * - prompt / lyrics: 500 in non-custom mode (all versions); in custom mode
210
+ * 3000 for V4/V3.5 and 5000 for V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5.
211
+ * - style: 200 for V4/V3.5, 1000 for V4.5+.
212
+ * - title: 80 (all versions).
213
+ */
214
+ export const SUNO_TITLE_MAX = 80
215
+
216
+ /** Max Suno `prompt` (= lyrics in custom mode) length for a model version. */
217
+ export function getMaxSunoPromptChars(model: string | undefined, customMode: boolean): number {
218
+ if (!customMode) return 500
219
+ return model === "V4" || model === "V3_5" ? 3000 : 5000
220
+ }
221
+
222
+ /** Max Suno `style` length for a model version. */
223
+ export function getMaxSunoStyleChars(model: string | undefined): number {
224
+ return model === "V4" || model === "V3_5" ? 200 : 1000
225
+ }
226
+
227
+ // Models that accept negative_prompt as a native API parameter.
228
+ // All other models get negative prompt appended to the prompt text as "Avoid: ...".
229
+ export const NATIVE_NEGATIVE_PROMPT_MODELS = new Set([
230
+ "imagen4", "imagen4-fast", "imagen4-ultra",
231
+ "ideogram-remix", "ideogram-v3",
232
+ "qwen", "qwen-edit",
233
+ ])
234
+
235
+ /**
236
+ * Video providers that accept `negative_prompt` natively in their KIE.ai
237
+ * request payload. Everywhere else, the helper below appends `Avoid: <text>`
238
+ * to the prompt instead so the negative intent still reaches the model.
239
+ *
240
+ * Sourced from inline comments in `backend/src/providers/kie/video.ts`
241
+ * (Kling family + Wan family) plus the wan-s2v speech-to-video flow.
242
+ *
243
+ * NOT included (KIE will silently drop the param):
244
+ * minimax / hailuo-* family, veo3.* family, sora2 / sora2-pro,
245
+ * bytedance-* family, grok / grok-i2v, seedance-* family,
246
+ * happyhorse-*, ltx-* family, runway-kie, wan-animate-move/replace
247
+ * (Wan Animate is a separate model from regular Wan and doesn't take
248
+ * negative_prompt — verified against KIE docs 2026-05-28).
249
+ */
250
+ export const NATIVE_NEGATIVE_VIDEO_PROVIDERS = new Set<string>([
251
+ // Kling family
252
+ "kling", "kling-turbo", "kling-master", "kling-3.0", "kling-3-omni",
253
+ // Wan family (regular Wan, NOT wan-animate)
254
+ "wan", "wan-flash", "wan-videoedit",
255
+ "wan-i2v", "wan-turbo",
256
+ "wan-2.7-i2v", "wan-2.7-t2v",
257
+ // Wan speech-to-video
258
+ "wan-s2v",
259
+ ])
260
+
261
+ /**
262
+ * Apply the negative prompt to a video-provider request.
263
+ *
264
+ * If the provider natively accepts `negative_prompt` (Kling / Wan families),
265
+ * the helper returns the original prompt and the trimmed negative as
266
+ * `nativeNegativePrompt` — the caller should forward it as the API's
267
+ * dedicated field.
268
+ *
269
+ * Otherwise, the helper appends `Avoid: <negativePrompt>` to the prompt
270
+ * (consistent with `buildImagePrompt` in `prompt-builder.ts`) so the model
271
+ * still sees the negative intent, and returns `nativeNegativePrompt = undefined`.
272
+ *
273
+ * Empty / missing negative is a no-op.
274
+ *
275
+ * Naming mirrors the established image-side pattern. Keep the wording
276
+ * identical to `prompt-builder.ts` ("Avoid: ...") so users moving between
277
+ * image and video nodes see consistent behavior.
278
+ */
279
+ export function applyVideoNegativePrompt(
280
+ prompt: string | undefined,
281
+ negativePrompt: string | undefined,
282
+ provider: string,
283
+ ): { prompt: string | undefined; nativeNegativePrompt: string | undefined } {
284
+ // This is the universal "finalize the video prompt for the provider" chokepoint
285
+ // (every KIE video method, the extend route, the orchestrator, and the editor
286
+ // preview route through it). So it ALSO clamps to the model's verified caps —
287
+ // a graceful safety net (the editor warns the user first; warn-don't-block).
288
+ // Under-cap prompts (the common case) pass through byte-identical.
289
+ const promptMax = getMaxVideoPromptChars(provider)
290
+ const clamp = (p: string | undefined): string | undefined =>
291
+ p != null && p.length > promptMax ? p.slice(0, promptMax) : p
292
+
293
+ const neg = negativePrompt?.trim()
294
+ if (!neg) return { prompt: clamp(prompt), nativeNegativePrompt: undefined }
295
+ if (NATIVE_NEGATIVE_VIDEO_PROVIDERS.has(provider)) {
296
+ // Native negative rides its own param → clamp each independently.
297
+ return { prompt: clamp(prompt), nativeNegativePrompt: neg.slice(0, getMaxNegativePromptChars(provider)) }
298
+ }
299
+ // Non-native: fold the negative into the prompt as "Avoid: …". Reserve room for
300
+ // the suffix so a long base prompt can't sever the negative (mirror the image
301
+ // assembler), then clamp the whole to the model cap.
302
+ const base = prompt && prompt.trim().length > 0 ? prompt : ""
303
+ if (!base) return { prompt: `Avoid: ${neg}`.slice(0, promptMax), nativeNegativePrompt: undefined }
304
+ const avoid = `\nAvoid: ${neg}`
305
+ const room = Math.max(0, promptMax - avoid.length)
306
+ const injected = `${base.slice(0, room)}${avoid}`.slice(0, promptMax)
307
+ return { prompt: injected, nativeNegativePrompt: undefined }
308
+ }
309
+
310
+ // Image providers that natively use reference images in their API.
311
+ // Used by `buildImagePrompt` to filter referenceImageUrls before sending,
312
+ // and by the frontend to warn when the user has `{image:N:label}` tokens
313
+ // in a prompt for a provider that would silently ignore them.
314
+ //
315
+ // Verified against backend/src/providers/kie/models.ts (inputType +
316
+ // imageParam) and the i2i flow in backend/src/providers/kie/image.ts.
317
+ //
318
+ // Image providers that natively use reference images in their API.
319
+ //
320
+ // Two categories included:
321
+ // 1. Direct ref support: nano-banana family (multi-ref T2I) + flux-kontext
322
+ // (single image edit) + all i2i/edit variants.
323
+ // 2. Auto-switched: T2I models that have an i2i sibling — when the user
324
+ // attaches refs to one of these in a generate-image node, the backend
325
+ // route silently routes to the i2i sibling. See `T2I_TO_I2I_VARIANT`.
326
+ // User-facing benefit: pick GPT Image / Grok / Qwen / Seedream / Flux at
327
+ // the dropdown, attach refs, and they "just work".
328
+ //
329
+ // Excluded (no ref support and no i2i sibling — refs would be useless):
330
+ // imagen4, imagen4-fast, imagen4-ultra, ideogram-v3, z-image
331
+ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
332
+ // Multi-reference text-to-image
333
+ "nano-banana",
334
+ "nano-banana-pro",
335
+ "nano-banana-2",
336
+ // T2I providers that auto-route to their i2i sibling when refs are attached
337
+ "gpt-image",
338
+ "gpt-image-2",
339
+ "grok",
340
+ "qwen",
341
+ "seedream",
342
+ "seedream-5-lite",
343
+ "flux",
344
+ "flux-flex",
345
+ // Image editing / image-to-image (reference = source image)
346
+ "nano-banana-edit",
347
+ "gpt-image-i2i",
348
+ "gpt-image-2-i2i",
349
+ "flux-i2i",
350
+ "flux-pro-i2i",
351
+ "flux-kontext",
352
+ "flux-kontext-max",
353
+ "ideogram-edit",
354
+ "ideogram-remix",
355
+ "ideogram-reframe",
356
+ "qwen-i2i",
357
+ "qwen-edit",
358
+ "seedream-edit",
359
+ "seedream-5-lite-i2i",
360
+ "grok-i2i",
361
+ // Upscale / background ops (source acts as the reference)
362
+ "recraft-remove-bg",
363
+ "recraft-upscale",
364
+ "topaz-image-upscale",
365
+ // Wan 2.7 — accepts up to 9 optional ref images via input_urls (pure T2I when omitted)
366
+ "wan-2.7",
367
+ // Replicate "Open" (uncensored) image models
368
+ "flux-2-klein",
369
+ "kontext-multi",
370
+ "flux-2-pro",
371
+ "flux-2-max",
372
+ ])
373
+
374
+ /**
375
+ * T2I provider → i2i sibling. When a generate-image node carries reference
376
+ * images and the chosen provider is in this map, the backend route
377
+ * transparently routes to the i2i variant so the refs are actually used.
378
+ *
379
+ * The user keeps seeing the T2I name in the UI; the warning component shows
380
+ * an info hint that the i2i variant is being used under the hood.
381
+ */
382
+ export const T2I_TO_I2I_VARIANT: Record<string, string> = {
383
+ "gpt-image": "gpt-image-i2i",
384
+ "gpt-image-2": "gpt-image-2-i2i",
385
+ "grok": "grok-i2i",
386
+ "qwen": "qwen-i2i",
387
+ "seedream": "seedream-edit",
388
+ "seedream-5-lite": "seedream-5-lite-i2i",
389
+ "flux": "flux-pro-i2i",
390
+ "flux-flex": "flux-i2i",
391
+ }
392
+
393
+ /**
394
+ * Maximum number of reference images each provider accepts.
395
+ * Sourced from the corresponding KIE.ai endpoint's documented input array
396
+ * size, or from the model's natural input shape (single-source i2i = 1).
397
+ *
398
+ * Providers absent from this map default to `DEFAULT_REF_IMAGE_MAX` (4).
399
+ * Providers absent from `MODELS_WITH_REFERENCE_IMAGE_SUPPORT` ignore
400
+ * reference images entirely regardless of this value.
401
+ */
402
+ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
403
+ // Multi-reference T2I (Nano Banana family is the only T2I family that
404
+ // actually accepts ref URLs via the KIE wrapper's `image_input` param).
405
+ "nano-banana": 8,
406
+ "nano-banana-pro": 8,
407
+ "nano-banana-2": 4,
408
+ "wan-2.7": 9,
409
+ // Image-to-image (multi-source array)
410
+ "nano-banana-edit": 8,
411
+ "gpt-image-i2i": 16,
412
+ "gpt-image-2-i2i": 16,
413
+ "flux-i2i": 4,
414
+ "flux-pro-i2i": 4,
415
+ "seedream-edit": 16,
416
+ "seedream-5-lite-i2i": 16,
417
+ // Single-source i2i (one input image)
418
+ "flux-kontext": 1,
419
+ "flux-kontext-max": 1,
420
+ "ideogram-edit": 1,
421
+ "ideogram-remix": 1,
422
+ "ideogram-reframe": 1,
423
+ "qwen-i2i": 1,
424
+ "qwen-edit": 1,
425
+ "grok-i2i": 1,
426
+ "recraft-remove-bg": 1,
427
+ "recraft-upscale": 1,
428
+ "topaz-image-upscale": 1,
429
+ // Replicate Open models — Klein takes an optional single ref; Kontext Multi
430
+ // (multi-image-kontext-pro) is a two-image combiner (input_image_1/2 only);
431
+ // Flux 2 Pro up to 4; Max up to 8.
432
+ "flux-2-klein": 1,
433
+ "kontext-multi": 2,
434
+ "flux-2-pro": 4,
435
+ "flux-2-max": 8,
436
+ }
437
+
438
+ export const DEFAULT_REF_IMAGE_MAX = 4
439
+
440
+ /**
441
+ * Maximum reference images the UI should advertise for an IMAGE model — the
442
+ * scalar image analogue of the video side's `videoReferenceLimits` (images have
443
+ * a single reference kind, so a scalar suffices). Studio's Framing picker reads
444
+ * this for its "References" chip; the editor's reference-handle popover reads it
445
+ * too (via `getHandleConnectionLimit`).
446
+ *
447
+ * Returns 0 when the model accepts no reference images at all (so callers can
448
+ * hide the chip / disable the handle), else the per-model cap from
449
+ * {@link REF_IMAGE_MAX_LIMITS} (fallback {@link DEFAULT_REF_IMAGE_MAX}).
450
+ * Therefore `imageReferenceLimit(p) > 0` is an exact "supports references" gate.
451
+ *
452
+ * Two behaviors keep the count matching what a user actually gets:
453
+ * 1. T2I → i2i resolution. Pickers list text-to-image ids (grok, gpt-image-2,
454
+ * seedream-5-lite, flux, …). When references are attached the generate-image
455
+ * route silently routes to the i2i sibling ({@link T2I_TO_I2I_VARIANT}) —
456
+ * the endpoint that actually consumes refs — so the advertised cap is the
457
+ * SIBLING's (grok→grok-i2i = 1, gpt-image-2→gpt-image-2-i2i = 16), never the
458
+ * t2i id's absent default. Mirrors the route's `resolveEffectiveProvider`.
459
+ * 2. {@link REF_IMAGE_MAX_LIMITS} is the PRODUCT/UI cap, which may be
460
+ * intentionally TIGHTER than the raw provider schema (e.g. flux-2-pro = 4
461
+ * here though the BFL schema accepts 8; flux-2-klein = 1 though the array
462
+ * slices at 5). The user-facing count must reflect the product cap, so the
463
+ * reader reads this map verbatim — do NOT "fix" these to the schema maxima.
464
+ */
465
+ export function imageReferenceLimit(provider: string | undefined): number {
466
+ if (!provider || !MODELS_WITH_REFERENCE_IMAGE_SUPPORT.has(provider)) return 0
467
+ const effective = T2I_TO_I2I_VARIANT[provider] ?? provider
468
+ return REF_IMAGE_MAX_LIMITS[effective] ?? DEFAULT_REF_IMAGE_MAX
469
+ }
470
+
471
+ // Variable pricing: which setting type affects cost per provider
472
+ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" | "rendering-speed"> = {
473
+ "gpt-image": "quality",
474
+ "gpt-image-i2i": "quality",
475
+ "gpt-image-2": "resolution",
476
+ "gpt-image-2-i2i": "resolution",
477
+ "nano-banana-pro": "resolution",
478
+ "nano-banana-2": "resolution",
479
+ "flux": "resolution",
480
+ "flux-flex": "resolution",
481
+ "flux-i2i": "resolution",
482
+ "flux-pro-i2i": "resolution",
483
+ "seedream": "quality",
484
+ "seedream-edit": "quality",
485
+ "seedream-5-lite": "quality",
486
+ "seedream-5-lite-i2i": "quality",
487
+ "topaz-image-upscale": "resolution",
488
+ "ideogram-edit": "rendering-speed",
489
+ "ideogram-remix": "rendering-speed",
490
+ "ideogram-reframe": "rendering-speed",
491
+ "ideogram-v3": "rendering-speed",
492
+ "wan-2.7": "resolution",
493
+ "wan-2.7-pro": "resolution",
494
+ }
495
+
496
+
497
+ // Models where quality=high triggers composite credit identifier
498
+ export const HIGH_QUALITY_PROVIDERS = new Set(["gpt-image", "gpt-image-i2i", "seedream", "seedream-edit", "seedream-5-lite", "seedream-5-lite-i2i"])
499
+
500
+ // Models where resolution=2K triggers composite credit identifier
501
+ export const TWO_K_RESOLUTION_PROVIDERS = new Set(["flux", "flux-pro-i2i", "flux-flex", "flux-i2i"])
502
+
503
+ // Models where both 2K and 4K resolutions trigger composite credit identifiers (1K is base)
504
+ export const RESOLUTION_2K_4K_TIERED_PROVIDERS = new Set([
505
+ "nano-banana-2",
506
+ "gpt-image-2",
507
+ "gpt-image-2-i2i",
508
+ "wan-2.7",
509
+ "wan-2.7-pro",
510
+ ])
511
+
512
+ // Ideogram family models with TURBO/QUALITY pricing variants
513
+ export const IDEOGRAM_PROVIDERS = new Set(["ideogram-edit", "ideogram-remix", "ideogram-reframe", "ideogram-v3"])
514
+
515
+ // =====================================================================
516
+ // Provider arrays (single source of truth for route Zod validation)
517
+ // =====================================================================
518
+
519
+ /** Text-to-image providers (no input image required) */
520
+ export const IMAGE_GEN_PROVIDERS = [
521
+ "nano-banana",
522
+ "flux",
523
+ "nano-banana-pro",
524
+ "nano-banana-2",
525
+ "grok",
526
+ "gpt-image",
527
+ "gpt-image-2",
528
+ "imagen4",
529
+ "imagen4-fast",
530
+ "imagen4-ultra",
531
+ "ideogram-v3",
532
+ "qwen",
533
+ "seedream",
534
+ "seedream-5-lite",
535
+ "flux-flex",
536
+ "flux-kontext",
537
+ "flux-kontext-max",
538
+ "z-image",
539
+ "wan-2.7",
540
+ "wan-2.7-pro",
541
+ // Replicate Open (uncensored) — runs through Replicate, not KIE
542
+ "flux-2-klein",
543
+ "flux-2-pro",
544
+ "flux-2-max",
545
+ ] as const
546
+
547
+ /** Image-to-image providers (require input image) */
548
+ export const IMAGE_I2I_PROVIDERS = [
549
+ "nano-banana",
550
+ "nano-banana-2",
551
+ "nano-banana-pro",
552
+ "grok-i2i",
553
+ "flux-i2i",
554
+ "flux-pro-i2i",
555
+ "gpt-image-i2i",
556
+ "gpt-image-2-i2i",
557
+ "ideogram-edit",
558
+ "ideogram-remix",
559
+ "ideogram-reframe",
560
+ "qwen-i2i",
561
+ "qwen-edit",
562
+ "seedream-edit",
563
+ "seedream-5-lite-i2i",
564
+ "flux-kontext",
565
+ "flux-kontext-max",
566
+ // Replicate Open (uncensored) — multi-image Kontext via Replicate
567
+ "kontext-multi",
568
+ // BFL Flux 2 Pro — runs through Replicate with safety_tolerance=5 (max for Pro)
569
+ "flux-2-pro",
570
+ // BFL Flux 2 Max — runs through Replicate with safety_tolerance=5, up to 8 refs
571
+ "flux-2-max",
572
+ ] as const
573
+
574
+ /** Image editing providers (upscale, remove bg, etc.) */
575
+ export const IMAGE_EDIT_PROVIDERS = [
576
+ "recraft-upscale",
577
+ "recraft-remove-bg",
578
+ "nano-banana-edit",
579
+ "topaz-image-upscale",
580
+ // grok-upscale takes a prior Grok generation's task_id (NOT an image URL) —
581
+ // see edit-image route for the taskId-vs-imageUrl branching.
582
+ "grok-upscale",
583
+ ] as const
584
+
585
+ /** Modify image providers (I2I + edit-with-prompt) */
586
+ export const MODIFY_IMAGE_PROVIDERS = [
587
+ ...IMAGE_I2I_PROVIDERS,
588
+ "nano-banana-edit",
589
+ ] as const
590
+ export type ModifyImageProvider = typeof MODIFY_IMAGE_PROVIDERS[number]
591
+
592
+ /** Image upscale providers */
593
+ export const UPSCALE_IMAGE_PROVIDERS = [
594
+ "recraft-upscale",
595
+ "topaz-image-upscale",
596
+ ] as const
597
+ export type UpscaleImageProvider = typeof UPSCALE_IMAGE_PROVIDERS[number]
598
+
599
+ // Derived types from provider arrays
600
+ export type ImageGenProvider = typeof IMAGE_GEN_PROVIDERS[number]
601
+ export type ImageI2IProvider = typeof IMAGE_I2I_PROVIDERS[number]
602
+ export type ImageEditProvider = typeof IMAGE_EDIT_PROVIDERS[number]
603
+
604
+ /** Image-to-video providers */
605
+ export const IMAGE_TO_VIDEO_PROVIDERS = [
606
+ "minimax",
607
+ "veo3",
608
+ "veo3.1",
609
+ "veo3_lite",
610
+ "kling",
611
+ "kling-turbo",
612
+ "kling-3.0",
613
+ "kling-master",
614
+ "seedance",
615
+ "seedance-2",
616
+ "seedance-2-fast",
617
+ "seedance-2-mini",
618
+ "hailuo-2.3-pro",
619
+ "hailuo-2.3",
620
+ "hailuo-standard",
621
+ "wan-i2v",
622
+ "wan-turbo",
623
+ "bytedance-lite",
624
+ "bytedance-pro",
625
+ "bytedance-pro-fast",
626
+ "grok-i2v",
627
+ "wan-2.7-i2v",
628
+ "happyhorse-i2v",
629
+ "happyhorse-ref2v",
630
+ "runway-kie",
631
+ "kling-3-omni",
632
+ "gemini-omni-video",
633
+ "ltx-2.3-pro",
634
+ "ltx-2.3-fast",
635
+ // xAI Grok Imagine Video 1.5 — KIE i2v (image_urls required). Also listed in
636
+ // TEXT_TO_VIDEO_PROVIDERS so it's offered everywhere in the unified node; the
637
+ // t2v route guards no-image runs via VIDEO_PROVIDERS_REQUIRING_IMAGE.
638
+ "grok-imagine-video-1.5",
639
+ // Replicate disabled
640
+ // "runway",
641
+ // "pika",
642
+ // "sora",
643
+ ] as const
644
+ export type ImageToVideoProvider = typeof IMAGE_TO_VIDEO_PROVIDERS[number]
645
+
646
+ /** Text-to-video providers */
647
+ export const TEXT_TO_VIDEO_PROVIDERS = [
648
+ "minimax",
649
+ "veo3",
650
+ "veo3.1",
651
+ "veo3_lite",
652
+ "kling",
653
+ "kling-turbo",
654
+ "kling-3.0",
655
+ "grok",
656
+ "seedance",
657
+ "seedance-2",
658
+ "seedance-2-fast",
659
+ "seedance-2-mini",
660
+ "wan",
661
+ "hailuo-standard",
662
+ "bytedance-lite",
663
+ "bytedance-pro",
664
+ "wan-turbo",
665
+ "wan-2.7-t2v",
666
+ "happyhorse",
667
+ "runway-kie",
668
+ "gemini-omni-video",
669
+ "ltx-2.3-pro",
670
+ "ltx-2.3-fast",
671
+ // ── i2v-only providers (image required) ──────────────────────────────────
672
+ // These have NO text-to-video mode, but the unified Generate Video node can
673
+ // dispatch ANY of its providers down the t2v path when no image is wired.
674
+ // They must pass this enum so /v1/text-to-video can return the clean
675
+ // "requires an input image" 400 (VIDEO_PROVIDERS_REQUIRING_IMAGE — derived
676
+ // from MODEL_CATALOG modes) instead of an opaque Zod validation error.
677
+ // They never reach a provider. Guarded by the dispatch-totality test in
678
+ // __tests__/video-mode-aliases.test.ts.
679
+ "grok-imagine-video-1.5",
680
+ "kling-3-omni",
681
+ "kling-master",
682
+ "hailuo-2.3",
683
+ "hailuo-2.3-pro",
684
+ "bytedance-pro-fast",
685
+ "happyhorse-ref2v",
686
+ // Replicate disabled
687
+ // "runway",
688
+ // "pika",
689
+ // "sora",
690
+ ] as const
691
+ export type TextToVideoProvider = typeof TEXT_TO_VIDEO_PROVIDERS[number]
692
+
693
+ /** Unified video-generation providers (image-to-video ∪ text-to-video) for the generate-video node. */
694
+ export const VIDEO_GEN_PROVIDERS = [
695
+ ...IMAGE_TO_VIDEO_PROVIDERS,
696
+ ...TEXT_TO_VIDEO_PROVIDERS.filter(
697
+ (p): p is typeof TEXT_TO_VIDEO_PROVIDERS[number] =>
698
+ !(IMAGE_TO_VIDEO_PROVIDERS as readonly string[]).includes(p),
699
+ ),
700
+ ] as const
701
+ export type VideoGenProvider = typeof VIDEO_GEN_PROVIDERS[number]
702
+
703
+ /**
704
+ * Video models that expose DISTINCT provider ids per mode (text-to-video vs
705
+ * image-to-video) but represent ONE user-facing model. KIE keys i2v and t2v off
706
+ * different ids in different maps (KIE_VIDEO_MODELS vs KIE_TEXT_TO_VIDEO_MODELS),
707
+ * so each id only resolves to a real model in its native mode.
708
+ *
709
+ * The unified Generate Video node shows ONE picker row (the `base` id) and
710
+ * auto-selects the mode by whether an input image is present; execution remaps
711
+ * base→mode id via {@link resolveVideoProviderForMode}. This registry is the
712
+ * single source of truth shared by the picker collapse (frontend
713
+ * `model-options.ts`), the frontend DAG executor (`execute-node.ts`), and the
714
+ * backend orchestrator (`payload-builder.ts`) so they can't drift.
715
+ *
716
+ * `base` MUST be one of the group's own mode ids (kept = the i2v id, since the
717
+ * unified picker's i2v entries win on collision). Honesty invariants are guarded
718
+ * in `__tests__/video-mode-aliases.test.ts`.
719
+ */
720
+ export interface VideoModeAlias {
721
+ /** Canonical id stored on the node + shown in the unified picker. */
722
+ base: string
723
+ /** KIE id to use when an input image is present (image-to-video). */
724
+ i2v: string
725
+ /** KIE id to use for pure text-to-video. */
726
+ t2v: string
727
+ }
728
+
729
+ export const VIDEO_MODE_ALIASES: readonly VideoModeAlias[] = [
730
+ { base: "grok-i2v", i2v: "grok-i2v", t2v: "grok" },
731
+ { base: "wan-i2v", i2v: "wan-i2v", t2v: "wan" },
732
+ { base: "wan-2.7-i2v", i2v: "wan-2.7-i2v", t2v: "wan-2.7-t2v" },
733
+ // HappyHorse: happyhorse/image-to-video + happyhorse/text-to-video are mode
734
+ // twins of one KIE model family — previously TWO unified-picker rows, and the
735
+ // i2v row crashed the t2v route when run without an image.
736
+ { base: "happyhorse-i2v", i2v: "happyhorse-i2v", t2v: "happyhorse" },
737
+ ] as const
738
+
739
+ /**
740
+ * Resolve a (possibly base / cross-mode) video provider id to the concrete KIE
741
+ * id for the given execution mode. Accepts any member id of an alias group
742
+ * (base, i2v, or t2v) so existing workflows that stored either mode's id keep
743
+ * working. Non-aliased providers (single-id models, VEO, Replicate, etc.) pass
744
+ * through unchanged.
745
+ */
746
+ export function resolveVideoProviderForMode(
747
+ provider: string,
748
+ mode: "image-to-video" | "text-to-video",
749
+ ): string {
750
+ for (const g of VIDEO_MODE_ALIASES) {
751
+ if (provider === g.base || provider === g.i2v || provider === g.t2v) {
752
+ return mode === "image-to-video" ? g.i2v : g.t2v
753
+ }
754
+ }
755
+ return provider
756
+ }
757
+
758
+ /**
759
+ * t2v twin ids hidden from the unified Generate Video picker — the i2v/base
760
+ * entry already represents both modes (execution remaps by image presence).
761
+ * Only includes twins whose `base` is NOT the t2v id, so the surviving picker
762
+ * entry is the base. Consumed by the frontend `VIDEO_GEN_MODELS` collapse.
763
+ */
764
+ export const VIDEO_GEN_COLLAPSED_T2V_IDS: ReadonlySet<string> = new Set(
765
+ VIDEO_MODE_ALIASES.filter((g) => g.t2v !== g.base).map((g) => g.t2v),
766
+ )
767
+
768
+ /** Video-to-video providers */
769
+ export const VIDEO_TO_VIDEO_PROVIDERS = [
770
+ "wan",
771
+ "wan-flash",
772
+ "wan-videoedit",
773
+ "luma-modify",
774
+ "runway-aleph",
775
+ "happyhorse-edit",
776
+ ] as const
777
+ export type VideoToVideoProvider = typeof VIDEO_TO_VIDEO_PROVIDERS[number]
778
+
779
+ /** Face swap providers */
780
+ export const FACE_SWAP_PROVIDERS = [
781
+ "roop",
782
+ ] as const
783
+ export type FaceSwapProvider = typeof FACE_SWAP_PROVIDERS[number]
784
+
785
+ /** Video upscale providers */
786
+ export const VIDEO_UPSCALE_PROVIDERS = [
787
+ "topaz",
788
+ "veo-1080p",
789
+ "veo-4k",
790
+ ] as const
791
+ export type VideoUpscaleProvider = typeof VIDEO_UPSCALE_PROVIDERS[number]
792
+
793
+ /** Extend video providers */
794
+ export const EXTEND_VIDEO_PROVIDERS = [
795
+ "veo-extend",
796
+ "runway-extend",
797
+ "ltx-2.3-pro",
798
+ "seedance-2-extend",
799
+ ] as const
800
+ export type ExtendVideoProvider = typeof EXTEND_VIDEO_PROVIDERS[number]
801
+
802
+ /** Lip sync providers */
803
+ export const LIP_SYNC_PROVIDERS = [
804
+ "kling-avatar",
805
+ "kling-avatar-pro",
806
+ "infinitalk",
807
+ // ByteDance OmniHuman 1.5 — KIE image+audio avatar, prompt-directed
808
+ // performance, per-second billed, 60s audio cap. Routes the default KIE
809
+ // path (not in any replicate/fal/seedance/video-input set).
810
+ "omnihuman-1-5",
811
+ "latentsync",
812
+ "wav2lip",
813
+ "video-retalking",
814
+ "sadtalker",
815
+ // HeyGen Lipsync Precision + Sync Lipsync 2 Pro — Replicate-hosted, video-input
816
+ // dubbing models billed per second of output. Routed through replicateLipSync;
817
+ // priced per-second via buildLipSyncCreditId (see lip-sync-pricing.ts).
818
+ "heygen-lipsync-precision",
819
+ "lipsync-2-pro",
820
+ // Sync Lipsync v3 — fal.ai-hosted, video-input dubbing model billed per
821
+ // second of output (/min). Routed through falLipSync (the worker's fal
822
+ // branch); priced per-second via buildLipSyncCreditId (see lip-sync-pricing.ts).
823
+ "sync-lipsync-v3",
824
+ // Volcengine video-to-video lip sync — KIE-hosted, VIDEO-input AI dubbing
825
+ // (mode lite/basic; multi-speaker scene-detection + speaker ID in basic).
826
+ // Billed per-second via buildLipSyncCreditId; routed through the KIE
827
+ // `lipSyncVideo` path (KieModelConfig.inputKind === "video"), NOT the
828
+ // image+prompt `lipSync` path.
829
+ "volcengine-lipsync",
830
+ // Seedance 2 / 2 Fast — not "lip-sync models" per se, but ByteDance's
831
+ // multimodal video models do native phoneme-level lip sync in 8+
832
+ // languages when fed `reference_audio_urls` alongside a `first_frame_url`.
833
+ // Routed through the i2v provider with the audio passed as a reference,
834
+ // not the dedicated lip-sync flow.
835
+ "seedance-2",
836
+ "seedance-2-fast",
837
+ "seedance-2-mini",
838
+ ] as const
839
+ export type LipSyncProvider = typeof LIP_SYNC_PROVIDERS[number]
840
+
841
+ /** Seedance variants exposed via the lip-sync surface. They go through
842
+ * the i2v provider with the audio plumbed as `reference_audio_urls`. */
843
+ export const SEEDANCE_LIP_SYNC_PROVIDERS = new Set<string>([
844
+ "seedance-2",
845
+ "seedance-2-fast",
846
+ "seedance-2-mini",
847
+ ])
848
+
849
+ /** Replicate-based lip-sync providers (video or image+audio via Replicate SDK) */
850
+ export const REPLICATE_LIP_SYNC_PROVIDERS = new Set([
851
+ "latentsync",
852
+ "wav2lip",
853
+ "video-retalking",
854
+ "sadtalker",
855
+ "heygen-lipsync-precision",
856
+ "lipsync-2-pro",
857
+ ] as const)
858
+
859
+ /** fal.ai-based lip-sync providers (video+audio → video via the fal queue API).
860
+ * Routed through `falLipSync` in the worker's `handleLipSync` fal branch. */
861
+ export const FAL_LIP_SYNC_PROVIDERS = new Set([
862
+ "sync-lipsync-v3",
863
+ ] as const)
864
+
865
+ /** Lip-sync providers that require video input (not image) */
866
+ export const VIDEO_INPUT_LIP_SYNC_PROVIDERS = new Set([
867
+ "latentsync",
868
+ "video-retalking",
869
+ "heygen-lipsync-precision",
870
+ "lipsync-2-pro",
871
+ "sync-lipsync-v3",
872
+ "volcengine-lipsync",
873
+ ] as const)
874
+
875
+ /** Lip-sync providers that accept either video or image input */
876
+ export const FLEXIBLE_INPUT_LIP_SYNC_PROVIDERS = new Set([
877
+ "wav2lip",
878
+ ] as const)
879
+
880
+ /** Standard aspect ratio → pixel dimensions for composition nodes */
881
+ export const ASPECT_RATIO_DIMENSIONS: Record<string, { width: number; height: number }> = {
882
+ "16:9": { width: 1920, height: 1080 },
883
+ "9:16": { width: 1080, height: 1920 },
884
+ "1:1": { width: 1080, height: 1080 },
885
+ "4:5": { width: 1080, height: 1350 },
886
+ }
887
+
888
+ /** Motion transfer providers */
889
+ export const MOTION_TRANSFER_PROVIDERS = [
890
+ "kling",
891
+ "kling-3.0",
892
+ "wan-animate-move",
893
+ "wan-animate-replace",
894
+ ] as const
895
+ export type MotionTransferProviderType = typeof MOTION_TRANSFER_PROVIDERS[number]
896
+
897
+ /** Text-to-speech providers */
898
+ export const TTS_PROVIDERS = [
899
+ "elevenlabs-v3",
900
+ "elevenlabs-turbo",
901
+ "elevenlabs-multilingual",
902
+ "elevenlabs",
903
+ ] as const
904
+ export type TtsProvider = typeof TTS_PROVIDERS[number]
905
+
906
+ /** Text-to-audio providers */
907
+ export const TEXT_TO_AUDIO_PROVIDERS = [
908
+ // Replicate disabled
909
+ // "tangoflux",
910
+ "elevenlabs-sfx",
911
+ ] as const
912
+ export type TextToAudioProvider = typeof TEXT_TO_AUDIO_PROVIDERS[number]
913
+
914
+ /** Music generation providers */
915
+ export const MUSIC_PROVIDERS = [
916
+ // Replicate disabled
917
+ // "musicgen",
918
+ "minimax",
919
+ // TODO: surface Suno V5 here for discoverability — needs backend wiring
920
+ // through suno-client.ts (Suno uses /api/v1/generate, NOT the standard
921
+ // /api/v1/jobs/createTask path runKieTask uses). Tracked as a separate
922
+ // follow-up PR. Until then, Suno is reachable via the standalone
923
+ // `suno-generate` node.
924
+ // Replicate disabled
925
+ // "lyria",
926
+ // "bark",
927
+ ] as const
928
+ export type MusicProvider = typeof MUSIC_PROVIDERS[number]
929
+
930
+ /** Transcription providers */
931
+ export const TRANSCRIBE_PROVIDERS = [
932
+ // Replicate disabled
933
+ // "whisper",
934
+ // "incredibly-fast-whisper",
935
+ "elevenlabs-stt",
936
+ ] as const
937
+ export type TranscribeProvider = typeof TRANSCRIBE_PROVIDERS[number]
938
+
939
+ /** Script generation providers */
940
+ export const SCRIPT_PROVIDERS = [
941
+ "gemini",
942
+ "claude",
943
+ "gpt",
944
+ ] as const
945
+ export type ScriptProvider = typeof SCRIPT_PROVIDERS[number]
946
+
947
+ /** AI writer providers */
948
+ export const AI_WRITER_PROVIDERS = [
949
+ "claude",
950
+ ] as const
951
+ export type AiWriterProvider = typeof AI_WRITER_PROVIDERS[number]
952
+
953
+ /** QA check providers */
954
+ export const QA_CHECK_PROVIDERS = [
955
+ "claude",
956
+ "gpt",
957
+ ] as const
958
+ export type QaCheckProvider = typeof QA_CHECK_PROVIDERS[number]
959
+
960
+ /** Suno model versions */
961
+ export const SUNO_MODELS = [
962
+ "V4",
963
+ "V4_5",
964
+ "V4_5PLUS",
965
+ "V4_5ALL",
966
+ "V5",
967
+ "V5_5",
968
+ ] as const
969
+ export type SunoModel = typeof SUNO_MODELS[number]
970
+
971
+ /** Suno models that support add-instrumental / add-vocals operations */
972
+ export const SUNO_ADD_TRACK_MODELS = ["V4_5PLUS", "V5", "V5_5"] as const
973
+ export type SunoAddTrackModel = typeof SUNO_ADD_TRACK_MODELS[number]
974
+
975
+ /** Voice design models */
976
+ export const VOICE_DESIGN_MODELS = [
977
+ "eleven_ttv_v3",
978
+ "eleven_multilingual_ttv_v2",
979
+ ] as const
980
+ export type VoiceDesignModel = typeof VOICE_DESIGN_MODELS[number]
981
+
982
+ /**
983
+ * ElevenLabs' newest text-to-voice model (all languages) — the default when
984
+ * a voice-design caller omits `model`. Single-sourced here so the route
985
+ * (`backend/src/routes/voice-design.ts`) and the direct-API provider
986
+ * (`backend/src/providers/elevenlabs/voice-design.ts`) can't drift.
987
+ */
988
+ export const DEFAULT_VOICE_DESIGN_MODEL: VoiceDesignModel = "eleven_ttv_v3"
989
+
990
+ /** I2I providers that support mask-based inpainting */
991
+ export const I2I_MASK_SUPPORT = new Set(["ideogram-edit"])
992
+
993
+ /**
994
+ * Mask edit tier per image-gen provider (single source of truth for inpaint).
995
+ * - "native" → provider takes a real mask param (reserved for Phase 1.5; none today)
996
+ * - "prompt" → strong instruction editor; inject a region descriptor into the prompt
997
+ * - "composite" → rely on the always-on server-side composite floor only
998
+ * Floor (B·(1−M)+E·M) applies to EVERY tier, so any provider is correct.
999
+ */
1000
+ export type ImageMaskMode = "native" | "prompt" | "composite"
1001
+
1002
+ export const IMAGE_MASK_MODE: Record<ImageGenProvider, ImageMaskMode> = {
1003
+ "nano-banana": "prompt",
1004
+ "nano-banana-pro": "prompt",
1005
+ "nano-banana-2": "prompt",
1006
+ "gpt-image": "prompt",
1007
+ "gpt-image-2": "prompt",
1008
+ "seedream": "prompt",
1009
+ "seedream-5-lite": "prompt",
1010
+ "qwen": "prompt",
1011
+ "flux-kontext": "prompt",
1012
+ "flux-kontext-max": "prompt",
1013
+ "flux": "composite",
1014
+ "flux-flex": "composite",
1015
+ "grok": "composite",
1016
+ "imagen4": "composite",
1017
+ "imagen4-fast": "composite",
1018
+ "imagen4-ultra": "composite",
1019
+ "ideogram-v3": "composite",
1020
+ "z-image": "composite",
1021
+ "wan-2.7": "composite",
1022
+ "wan-2.7-pro": "composite",
1023
+ "flux-2-klein": "composite",
1024
+ "flux-2-pro": "composite",
1025
+ "flux-2-max": "composite",
1026
+ }
1027
+
1028
+ /** I2I providers that support a strength/denoising parameter */
1029
+ export const I2I_STRENGTH_SUPPORT: Record<string, { min: number; max: number; step: number; default: number }> = {
1030
+ "ideogram-remix": { min: 0.01, max: 1, step: 0.01, default: 0.8 },
1031
+ "qwen-i2i": { min: 0, max: 1, step: 0.01, default: 0.8 },
1032
+ }
1033
+
1034
+ /** Models that accept a seed parameter for reproducible generation */
1035
+ export const SEED_SUPPORT = new Set([
1036
+ "ideogram-remix", "ideogram-reframe", "ideogram-v3",
1037
+ "qwen", "qwen-i2i", "qwen-edit",
1038
+ "flux", "flux-flex", "flux-i2i", "flux-pro-i2i", "flux-kontext", "flux-kontext-max",
1039
+ "flux-2-klein", "kontext-multi",
1040
+ ])
1041
+
1042
+ /** Ideogram models that support rendering_speed selection (TURBO/BALANCED/QUALITY) */
1043
+ export const RENDERING_SPEED_SUPPORT = new Set([
1044
+ "ideogram-remix", "ideogram-reframe", "ideogram-v3",
1045
+ ])
1046
+
1047
+ /** Models that accept guidance_scale for controlling prompt adherence */
1048
+ export const GUIDANCE_SCALE_SUPPORT: Record<string, { min: number; max: number; step: number; default: number }> = {
1049
+ "qwen-i2i": { min: 1, max: 20, step: 0.5, default: 7 },
1050
+ "qwen-edit": { min: 1, max: 20, step: 0.5, default: 7 },
1051
+ }
1052
+
1053
+ // =====================================================================
1054
+ // Video variable pricing — duration-based and audio-addon pricing
1055
+ // =====================================================================
1056
+
1057
+ /**
1058
+ * Video models where credit cost varies by duration.
1059
+ * Maps provider key → duration tier breakpoints.
1060
+ * Values are verified by the pricing verification script (backend/scripts/verify-kie-pricing.ts).
1061
+ * TODO: Run verification script and update costs after confirming actual KIE pricing.
1062
+ */
1063
+ export const DURATION_PRICED_PROVIDERS = new Set([
1064
+ "seedance-2-extend",
1065
+ "kling-3.0",
1066
+ "kling-3-omni",
1067
+ "kling",
1068
+ "kling-turbo",
1069
+ "kling-master",
1070
+ "grok-i2v",
1071
+ "wan-i2v",
1072
+ "hailuo-2.3-pro",
1073
+ "hailuo-2.3",
1074
+ "hailuo-standard",
1075
+ "seedance",
1076
+ "seedance-2",
1077
+ "seedance-2-fast",
1078
+ "seedance-2-mini",
1079
+ "grok-imagine-video-1.5",
1080
+ ])
1081
+
1082
+ /**
1083
+ * Seedance 2.0 family — shared across UI gating, payload building, and pricing.
1084
+ * Expanded whenever a new Seedance 2.x variant ships.
1085
+ */
1086
+ export const SEEDANCE_2_PROVIDERS = new Set<string>([
1087
+ "seedance-2",
1088
+ "seedance-2-fast",
1089
+ "seedance-2-mini",
1090
+ ])
1091
+
1092
+ export function isSeedance2Provider(provider: string | undefined): boolean {
1093
+ return !!provider && SEEDANCE_2_PROVIDERS.has(provider)
1094
+ }
1095
+
1096
+ /**
1097
+ * Default aspect ratio for a video provider when the node carries no explicit
1098
+ * `aspectRatio`. Seedance 2.x defaults to `"adaptive"` (output matches the
1099
+ * wired input — fits the unified-inputs model); every other provider keeps the
1100
+ * historical `"16:9"`. Single source of truth so every run-default / display-
1101
+ * fallback site stays in lock-step (preview = run). Applies to ALL Seedance
1102
+ * modes incl. text-to-video — KIE accepts `adaptive` there too.
1103
+ */
1104
+ export function defaultVideoAspectRatio(provider: string | undefined): string {
1105
+ return isSeedance2Provider(provider) ? "adaptive" : "16:9"
1106
+ }
1107
+
1108
+ /**
1109
+ * Google VEO family (Quality / Fast / Lite). VEO goes through its own KIE
1110
+ * endpoint and image-handling path, so callers branch on this in several places.
1111
+ */
1112
+ export const VEO_PROVIDERS = new Set<string>([
1113
+ "veo3",
1114
+ "veo3.1",
1115
+ "veo3_lite",
1116
+ ])
1117
+
1118
+ export function isVeoProvider(provider: string | undefined): boolean {
1119
+ return !!provider && VEO_PROVIDERS.has(provider)
1120
+ }
1121
+
1122
+ /** KIE.ai limits for Seedance 2.0 multimodal reference arrays. */
1123
+ export const SEEDANCE_2_REF_LIMITS = {
1124
+ images: 9,
1125
+ videos: 3,
1126
+ audio: 3,
1127
+ } as const
1128
+
1129
+ /**
1130
+ * Trim-stitch parameters for the seedance-2-extend provider (spike-validated
1131
+ * 2026-06-11):
1132
+ * the model's extension output wobbles for its first ~3 frames and the source
1133
+ * tail's last ~4 frames lead into it; dropping both and butt-joining yields a
1134
+ * seam SMOOTHER than ordinary in-clip motion (PSNR 34.4dB vs 30.6dB
1135
+ * adjacent-frame baseline). The 0.15s timeline-anchored audio fades kill
1136
+ * boundary clicks without shifting sync (combineVideos cut+crossfade path).
1137
+ */
1138
+ export const SEEDANCE_2_EXTEND_STITCH = {
1139
+ /** Frames dropped from the END of the source clip. */
1140
+ trimTailFrames: 4,
1141
+ /** Frames dropped from the START of the generated extension. */
1142
+ trimHeadFrames: 3,
1143
+ /** Boundary audio fade length (seconds), timeline-preserving. */
1144
+ audioFadeSec: 0.15,
1145
+ } as const
1146
+
1147
+ /**
1148
+ * Per-provider max reference-audio duration (seconds) for Seedance 2.0 r2v
1149
+ * (audio-driven image-to-video). KIE rejects longer clips with a 400 BEFORE
1150
+ * generation (e.g. "audio duration must be ≤ 15.2 for dreamina-seedance-2-0-fast
1151
+ * in r2v"). Data-driven: only providers with a VERIFIED limit are listed, so an
1152
+ * unknown provider is never false-rejected.
1153
+ * - seedance-2-fast: 15.2s (verified against KIE).
1154
+ * - seedance-2 (non-fast): limit unverified → not enforced yet (add after
1155
+ * confirming via docs.kie.ai).
1156
+ */
1157
+ export const SEEDANCE_2_R2V_MAX_AUDIO_SEC_BY_PROVIDER: Record<string, number> = {
1158
+ "seedance-2-fast": 15.2,
1159
+ }
1160
+
1161
+ /** The verified r2v reference-audio cap (seconds) for a provider, or null when
1162
+ * none is enforced. */
1163
+ export function seedance2AudioLimitSec(provider: string | undefined): number | null {
1164
+ if (!provider) return null
1165
+ return SEEDANCE_2_R2V_MAX_AUDIO_SEC_BY_PROVIDER[provider] ?? null
1166
+ }
1167
+
1168
+ /**
1169
+ * First reference-audio duration (seconds) that exceeds the provider's r2v cap,
1170
+ * or null when all are within limit (or the provider has no enforced cap).
1171
+ * Non-finite durations (probe failures) are ignored. Used to reject over-long
1172
+ * audio BEFORE submitting to the provider — which would otherwise 400.
1173
+ */
1174
+ export function findSeedance2AudioOverLimit(
1175
+ provider: string | undefined,
1176
+ durationsSec: readonly number[],
1177
+ ): number | null {
1178
+ const limit = seedance2AudioLimitSec(provider)
1179
+ if (limit === null) return null
1180
+ for (const d of durationsSec) {
1181
+ if (Number.isFinite(d) && d > limit) return d
1182
+ }
1183
+ return null
1184
+ }
1185
+
1186
+ /**
1187
+ * Per-provider connection caps for the typed reference handles on Generate Video
1188
+ * AND the cap for the server-side `connectedReferences` assembly (`imageCap`).
1189
+ *
1190
+ * The KEY SET is bound 1:1 to MODEL_CATALOG (kind:"video" + features:
1191
+ * ["reference-image"]) by the drift guard in `__tests__/video-ref-limits.test.ts`:
1192
+ * adding a ref-capable video model to the catalog without a cap here — or a cap
1193
+ * here without the catalog flag — fails CI. So the CATALOG is the single source
1194
+ * for "is this model ref-capable", and this map is the single source for the
1195
+ * NUMERIC caps (sourced from the provider configs, see per-line notes).
1196
+ * Providers absent from the map = 0 caps (the popover dims the handle with
1197
+ * "Not supported by [Model]"; the API assembly strips `{image:N}` tokens).
1198
+ */
1199
+ export const VIDEO_REF_LIMITS_BY_PROVIDER: Record<
1200
+ string,
1201
+ { images?: number; videos?: number; audio?: number } | undefined
1202
+ > = {
1203
+ // Seedance 2 — full multimodal caps.
1204
+ "seedance-2": { ...SEEDANCE_2_REF_LIMITS },
1205
+ "seedance-2-fast": { ...SEEDANCE_2_REF_LIMITS },
1206
+ "seedance-2-mini": { ...SEEDANCE_2_REF_LIMITS },
1207
+ // Multi-image reference providers.
1208
+ "gemini-omni-video": { images: 7, videos: 1 },
1209
+ "kling-3-omni": { images: 7 }, // catalog/docs: "end frame + up to 7 reference images"
1210
+ "grok-i2v": { images: 7 }, // backend kie/models.ts maxRefImages: 7
1211
+ "happyhorse-ref2v": { images: 9 }, // backend kie/models.ts maxRefImages: 9
1212
+ // VEO 3.x — REFERENCE_2_VIDEO path caps refs at 3 (kie/video.ts slice(0, 3)).
1213
+ "veo3": { images: 3 },
1214
+ "veo3.1": { images: 3 },
1215
+ "veo3_lite": { images: 3 },
1216
+ // NOTE: wan-i2v / hailuo-2.3[-pro] / bytedance-pro[-fast] / grok-imagine-video-1.5
1217
+ // are deliberately ABSENT. The 2026-06-28 audit (video-reference-features.test.ts)
1218
+ // verified backend reference-forwarding paths and these i2v models have none —
1219
+ // a single image_url start frame only. Advertising a ref cap would silently drop
1220
+ // the user's references (the grok-imagine-video-1.5 bug). Add only with a
1221
+ // verified provider path + the catalog `reference-image` feature.
1222
+ }
1223
+
1224
+ /**
1225
+ * Video models where credit cost depends on resolution AND whether a video
1226
+ * reference is connected. Identifier suffix: `:{resolution}[-ref]`.
1227
+ * Seedance 2.0 family uses per-second billing split 480p/720p × with-ref/no-ref.
1228
+ */
1229
+ export const RESOLUTION_VIDEO_REF_PRICING = SEEDANCE_2_PROVIDERS
1230
+
1231
+ /**
1232
+ * Video models priced by (duration × resolution) WITHOUT a video-ref dimension.
1233
+ * Identifier suffix: `:{resolution}`, appended after the duration tier; value =
1234
+ * the provider's PRICED resolution tiers. Any resolution outside the list
1235
+ * (or undefined) collapses to the FIRST entry — the provider's default tier —
1236
+ * so the builder can never emit an unpriced composite (the hard-fail guard
1237
+ * fuzzes the full resolution space). Distinct from
1238
+ * RESOLUTION_VIDEO_REF_PRICING (Seedance gen), which also adds `-ref` variants.
1239
+ */
1240
+ export const RESOLUTION_DURATION_PRICING: Record<string, readonly string[]> = {
1241
+ // KIE supports only 480p/720p here; 480p is the default.
1242
+ "grok-imagine-video-1.5": ["480p", "720p"],
1243
+ // seedance-2-extend always uses a video ref, so pricing has no -ref
1244
+ // dimension — duration tier + ":res" only (rates = seedance-2 -ref + stitch).
1245
+ "seedance-2-extend": ["480p", "720p", "1080p"],
1246
+ }
1247
+
1248
+ /**
1249
+ * Video generation models whose provider API natively accepts a "match the
1250
+ * visual input's aspect" token, mapped to that provider's EXACT token.
1251
+ * Consumed by the backend's `resolveSourceMatchedAspect`: members get the
1252
+ * native token (no probe round-trip, exact match even for off-catalog ratios
1253
+ * like 4:5); everyone else falls back to ffprobe + closest catalog ratio.
1254
+ *
1255
+ * Only add entries whose match-the-input semantics are verified against the
1256
+ * provider's docs AND a live job — e.g. grok-imagine-video-1.5 accepts
1257
+ * "auto", but that means "provider default", NOT "match input", so it does
1258
+ * not belong here.
1259
+ *
1260
+ * seedance-2 family: `adaptive` per docs.kie.ai/market/bytedance/seedance-2
1261
+ * + seedance-2-fast (enum'd alongside the fixed ratios; default 16:9).
1262
+ */
1263
+ export const NATIVE_ADAPTIVE_ASPECT: Record<string, string> = {
1264
+ "seedance-2": "adaptive",
1265
+ "seedance-2-fast": "adaptive",
1266
+ "seedance-2-mini": "adaptive",
1267
+ }
1268
+
1269
+ /**
1270
+ * Video providers that REQUIRE an input image (image-to-video only) even though
1271
+ * they're listed in TEXT_TO_VIDEO_PROVIDERS for unified-node visibility. The
1272
+ * `/v1/text-to-video` route early-returns a clean 400 for these before any job
1273
+ * is created, instead of letting the prompt-only request fail at the provider.
1274
+ *
1275
+ * DERIVED from MODEL_CATALOG (the capability source of truth): every video
1276
+ * model whose `modes` lacks "t2v" and that isn't remapped to a t2v twin by
1277
+ * VIDEO_MODE_ALIASES. Hand-listing this set is what let kling-3-omni,
1278
+ * kling-master, hailuo-2.3(-pro), bytedance-pro-fast and happyhorse-ref2v
1279
+ * crash the t2v route with a raw Zod enum error — new i2v-only models now get
1280
+ * the friendly gate automatically when their catalog entry is added.
1281
+ */
1282
+ export const VIDEO_PROVIDERS_REQUIRING_IMAGE: ReadonlySet<string> = new Set(
1283
+ Object.values(MODEL_CATALOG)
1284
+ .filter((m) =>
1285
+ m.kind === "video" &&
1286
+ m.modes.includes("i2v") &&
1287
+ !m.modes.includes("t2v") &&
1288
+ (VIDEO_GEN_PROVIDERS as readonly string[]).includes(m.id) &&
1289
+ resolveVideoProviderForMode(m.id, "text-to-video") === m.id,
1290
+ )
1291
+ .map((m) => m.id),
1292
+ )
1293
+
1294
+ /** True when a video provider can only run image-to-video (image required). */
1295
+ export function videoProviderRequiresImage(provider: string | undefined): boolean {
1296
+ return !!provider && VIDEO_PROVIDERS_REQUIRING_IMAGE.has(provider)
1297
+ }
1298
+
1299
+ /**
1300
+ * Video models where enabling audio/sound incurs an additional cost.
1301
+ * The audio addon is expressed as a separate composite identifier suffix.
1302
+ */
1303
+ export const AUDIO_ADDON_PROVIDERS = new Set([
1304
+ "kling-3.0",
1305
+ "kling",
1306
+ ])
1307
+
1308
+ /**
1309
+ * How a video model handles an audio track. Single source of truth for
1310
+ * audio/speech capability across the whole stack — the config-panel audio
1311
+ * toggle (show / lock / disable + warn), the Story→Video dialogue auto-pick
1312
+ * (revoice vs TTS + lip-sync), provider option wiring (`sound` vs
1313
+ * `generateAudio`), and the audio credit suffix.
1314
+ *
1315
+ * Consolidates signals that were previously scattered + inconsistent:
1316
+ * - `KIE_VIDEO_MODELS` / `KIE_TEXT_TO_VIDEO_MODELS` `extraParams.sound` (Kling)
1317
+ * - `…extraParams.generate_audio` (Seedance)
1318
+ * - VEO native audio (implicit, always on)
1319
+ * - `AUDIO_ADDON_PROVIDERS` (cost) + `SEEDANCE_2_PROVIDERS` (reference audio)
1320
+ *
1321
+ * "none" — silent model; no audio output. UI disables the audio
1322
+ * toggle with an explanatory note; the pipeline uses TTS +
1323
+ * lip-sync for any dialogue.
1324
+ * "ambient" — generates ambient sound / SFX matched to the scene, NOT
1325
+ * lip-synced spoken dialogue. Toggle is offered; the
1326
+ * pipeline still uses TTS + lip-sync for dialogue.
1327
+ * "native_speech" — bakes spoken dialogue + lip movement from the prompt
1328
+ * (VEO 3.x). The pipeline injects the dialogue line, enables
1329
+ * audio, and revoices the clip to the character's saved voice.
1330
+ * "audio_driven" — lip-syncs to a supplied reference-audio track (Seedance
1331
+ * 2.0 multimodal). The pipeline synthesises the character's
1332
+ * voice first, feeds it as reference audio, and skips the
1333
+ * separate lip-sync pass.
1334
+ */
1335
+ export type VideoAudioMode = "none" | "ambient" | "native_speech" | "audio_driven"
1336
+
1337
+ export interface VideoAudioCapability {
1338
+ mode: VideoAudioMode
1339
+ /** Provider-option field carrying the on/off toggle, when user-controllable. */
1340
+ field?: "generateAudio" | "sound"
1341
+ /** Audio is always produced and can't be turned off by the user (VEO 3.x). */
1342
+ alwaysOn?: boolean
1343
+ /** Enabling audio raises the credit cost (Kling — see AUDIO_ADDON_PROVIDERS). */
1344
+ affectsCost?: boolean
1345
+ }
1346
+
1347
+ /**
1348
+ * Per-model audio capability. Only models that produce SOME audio are listed;
1349
+ * anything absent defaults to `{ mode: "none" }` via `getVideoAudioCapability`,
1350
+ * so silent models (minimax, hailuo, wan, grok-i2v, gemini-omni-video, runway,
1351
+ * pika, …) need no entry. New audio-capable models MUST be added here — the
1352
+ * `video-audio-capability` guard test cross-checks this map against the model
1353
+ * configs' `extraParams.sound` / `generate_audio` + VEO/Seedance-2 sets so a
1354
+ * forgotten entry fails CI rather than silently disabling audio.
1355
+ */
1356
+ export const VIDEO_AUDIO_CAPABILITY: Record<string, VideoAudioCapability> = {
1357
+ // VEO 3.x — native spoken dialogue + lip movement; always on (no toggle).
1358
+ veo3: { mode: "native_speech", alwaysOn: true },
1359
+ "veo3.1": { mode: "native_speech", alwaysOn: true },
1360
+ veo3_lite: { mode: "native_speech", alwaysOn: true },
1361
+ // Kling 2.6 / 3.0 — ambient sound/SFX toggle; not lip-synced speech. Cost-affecting.
1362
+ kling: { mode: "ambient", field: "sound", affectsCost: true },
1363
+ "kling-3.0": { mode: "ambient", field: "sound", affectsCost: true },
1364
+ // Seedance 1.x — optional ambient audio (generate_audio); not dialogue.
1365
+ seedance: { mode: "ambient", field: "generateAudio" },
1366
+ // Seedance 2.0 — multimodal; lip-syncs to a supplied reference-audio track.
1367
+ "seedance-2": { mode: "audio_driven", field: "generateAudio" },
1368
+ "seedance-2-fast": { mode: "audio_driven", field: "generateAudio" },
1369
+ "seedance-2-mini": { mode: "audio_driven", field: "generateAudio" },
1370
+ }
1371
+
1372
+ const VIDEO_AUDIO_NONE: VideoAudioCapability = { mode: "none" }
1373
+
1374
+ /** Audio capability for a video model. Unlisted/undefined → silent (`none`). */
1375
+ export function getVideoAudioCapability(
1376
+ model: string | undefined,
1377
+ ): VideoAudioCapability {
1378
+ return (model ? VIDEO_AUDIO_CAPABILITY[model] : undefined) ?? VIDEO_AUDIO_NONE
1379
+ }
1380
+
1381
+ /**
1382
+ * True when the model produces any audio track at all. Drives the config-panel
1383
+ * audio toggle: `false` → disable the toggle + show a "this model has no audio"
1384
+ * note (never restrict the model choice — just explain the limitation).
1385
+ */
1386
+ export function videoModelSupportsAudio(model: string | undefined): boolean {
1387
+ return getVideoAudioCapability(model).mode !== "none"
1388
+ }
1389
+
1390
+ /**
1391
+ * True when the model can produce lip-synced spoken DIALOGUE — either natively
1392
+ * (VEO) or driven by a supplied audio track (Seedance 2.0). Drives the Story→Video
1393
+ * dialogue auto-pick: in-model speech + character revoice (VEO) / character-voiced
1394
+ * reference audio (Seedance 2.0) vs. the TTS + separate-lip-sync fallback.
1395
+ * Ambient-only models (Kling, Seedance 1.x) return `false` — their audio is SFX,
1396
+ * not speech.
1397
+ */
1398
+ export function videoModelCanSpeakDialogue(model: string | undefined): boolean {
1399
+ const mode = getVideoAudioCapability(model).mode
1400
+ return mode === "native_speech" || mode === "audio_driven"
1401
+ }
1402
+
1403
+ /**
1404
+ * Write the caller's neutral "enable native audio" intent onto a KIE provider
1405
+ * `input` under whichever field the chosen model actually reads, per
1406
+ * `VIDEO_AUDIO_CAPABILITY.field`. This is the single dispatch point that fixes
1407
+ * the recurring "sound toggle dropped on Seedance/VEO" bug class: callers pass
1408
+ * the model-agnostic toggle ONCE and never need to know that Kling's lever is
1409
+ * `sound` while Seedance's is `generate_audio`.
1410
+ *
1411
+ * - `sound` is the canonical neutral name; `generateAudio` is accepted as a
1412
+ * legacy alias (the workflow node + some clients send that key). `sound` wins
1413
+ * if both are present.
1414
+ * - **Cost-affecting models (Kling) honour ONLY the canonical `sound` lever** —
1415
+ * the same field the `:audio` credit surcharge keys off (`AUDIO_ADDON_PROVIDERS`
1416
+ * in `credit-identifiers.ts`). This makes a billed-but-not-generated (or
1417
+ * generated-but-not-billed) divergence structurally impossible: the model and
1418
+ * the surcharge read the same flag. Free models accept the `generateAudio`
1419
+ * alias too, where mis-billing isn't possible.
1420
+ * - No-op for `alwaysOn` models (VEO — audio can't be toggled) and silent /
1421
+ * unlisted models (no `field`), and when the caller expressed no intent
1422
+ * (leaves the model's own default in place).
1423
+ *
1424
+ * Future audio models are covered automatically by adding a
1425
+ * `VIDEO_AUDIO_CAPABILITY` entry — already guarded by the
1426
+ * `video-audio-capability` drift test, so this never silently regresses.
1427
+ */
1428
+ export function applyVideoAudioToggle(
1429
+ input: Record<string, unknown>,
1430
+ provider: string | undefined,
1431
+ opts: { sound?: boolean; generateAudio?: boolean } | undefined,
1432
+ ): void {
1433
+ const cap = getVideoAudioCapability(provider)
1434
+ if (!cap.field) return // alwaysOn (VEO) or silent/unlisted model — nothing to toggle
1435
+ // Cost-affecting models stay on the canonical `sound` lever so the model and
1436
+ // the credit surcharge can never read different flags; free models also accept
1437
+ // the `generateAudio` alias. `sound` is canonical and wins when both are set.
1438
+ const enabled = cap.affectsCost ? opts?.sound : (opts?.sound ?? opts?.generateAudio)
1439
+ if (enabled === undefined) return // no intent — keep the model's own default
1440
+ if (cap.field === "generateAudio") input.generate_audio = enabled
1441
+ else input.sound = enabled
1442
+ }
1443
+
1444
+ /**
1445
+ * Video models where a quality/mode parameter (e.g., videoSize "high") incurs higher cost.
1446
+ * When provider is in this set and mode is "high", ":high" is appended to the identifier.
1447
+ */
1448
+ export const MODE_ADDON_PROVIDERS = new Set<string>([
1449
+ ])
1450
+
1451
+ /**
1452
+ * VEO 3.x providers where credit cost depends on the requested output
1453
+ * resolution (720p default vs 1080p inline; 4K is via the separate
1454
+ * upgrade endpoint and isn't part of this set). When provider is in
1455
+ * this set and resolution !== "720p", `:1080p` is appended to the
1456
+ * identifier so the credit lookup hits the per-resolution rate.
1457
+ *
1458
+ * Per KIE pricing (verified 2026-05-06):
1459
+ * veo3.1 (Fast): 720p=60 KIE cr, 1080p=65 KIE cr
1460
+ * veo3_lite: 720p=30 KIE cr, 1080p=35 KIE cr
1461
+ * veo3 (Quality) is not in this set yet — pricing not in our reference data.
1462
+ */
1463
+ export const VEO_RESOLUTION_TIERED_PROVIDERS = new Set<string>([
1464
+ "veo3.1",
1465
+ "veo3_lite",
1466
+ ])
1467
+
1468
+ /**
1469
+ * Video variable pricing config — which params affect credit cost per model.
1470
+ * "duration" = cost varies by video length
1471
+ * "duration+audio" = cost varies by length AND audio on/off
1472
+ */
1473
+ export const VIDEO_VARIABLE_PRICING: Record<string, "duration" | "duration+audio" | "duration+mode" | "duration+resolution+ref" | "duration+resolution"> = {
1474
+ "kling-3.0": "duration+audio",
1475
+ "kling-3-omni": "duration",
1476
+ "kling": "duration+audio",
1477
+ "kling-turbo": "duration",
1478
+ "kling-master": "duration",
1479
+ "grok-i2v": "duration",
1480
+ "wan-i2v": "duration",
1481
+ "hailuo-2.3-pro": "duration",
1482
+ "hailuo-2.3": "duration",
1483
+ "hailuo-standard": "duration",
1484
+ "seedance": "duration",
1485
+ "seedance-2": "duration+resolution+ref",
1486
+ "seedance-2-fast": "duration+resolution+ref",
1487
+ "seedance-2-mini": "duration+resolution+ref",
1488
+ // Grok Imagine Video 1.5 — per-second billing split 480p/720p (no video-ref dimension).
1489
+ "grok-imagine-video-1.5": "duration+resolution",
1490
+ }
1491
+
1492
+ /**
1493
+ * Duration tier breakpoints for variable-priced video models.
1494
+ * Maps provider → array of { maxSeconds, suffix } in ascending order.
1495
+ * The first tier whose maxSeconds >= requested duration is used.
1496
+ */
1497
+ export const VIDEO_DURATION_TIERS: Record<string, Array<{ maxSeconds: number; suffix: string }>> = {
1498
+ "kling-3.0": [
1499
+ { maxSeconds: 5, suffix: "5s" },
1500
+ { maxSeconds: 10, suffix: "10s" },
1501
+ { maxSeconds: 15, suffix: "15s" },
1502
+ ],
1503
+ "kling-3-omni": [
1504
+ { maxSeconds: 5, suffix: "5s" },
1505
+ { maxSeconds: 10, suffix: "10s" },
1506
+ { maxSeconds: 15, suffix: "15s" },
1507
+ ],
1508
+ "kling": [
1509
+ { maxSeconds: 5, suffix: "5s" },
1510
+ { maxSeconds: 10, suffix: "10s" },
1511
+ ],
1512
+ "kling-turbo": [
1513
+ { maxSeconds: 5, suffix: "5s" },
1514
+ { maxSeconds: 10, suffix: "10s" },
1515
+ ],
1516
+ "kling-master": [
1517
+ { maxSeconds: 5, suffix: "5s" },
1518
+ { maxSeconds: 10, suffix: "10s" },
1519
+ ],
1520
+ "grok-i2v": [
1521
+ { maxSeconds: 6, suffix: "6s" },
1522
+ { maxSeconds: 10, suffix: "10s" },
1523
+ { maxSeconds: 15, suffix: "15s" },
1524
+ ],
1525
+ "wan-i2v": [
1526
+ { maxSeconds: 5, suffix: "5s" },
1527
+ { maxSeconds: 10, suffix: "10s" },
1528
+ { maxSeconds: 15, suffix: "15s" },
1529
+ ],
1530
+ "hailuo-2.3-pro": [
1531
+ { maxSeconds: 6, suffix: "6s" },
1532
+ { maxSeconds: 10, suffix: "10s" },
1533
+ ],
1534
+ "hailuo-2.3": [
1535
+ { maxSeconds: 6, suffix: "6s" },
1536
+ { maxSeconds: 10, suffix: "10s" },
1537
+ ],
1538
+ "hailuo-standard": [
1539
+ { maxSeconds: 6, suffix: "6s" },
1540
+ { maxSeconds: 10, suffix: "10s" },
1541
+ ],
1542
+ "seedance": [
1543
+ { maxSeconds: 4, suffix: "4s" },
1544
+ { maxSeconds: 8, suffix: "8s" },
1545
+ { maxSeconds: 12, suffix: "12s" },
1546
+ ],
1547
+ "seedance-2": [
1548
+ { maxSeconds: 4, suffix: "4s" },
1549
+ { maxSeconds: 8, suffix: "8s" },
1550
+ { maxSeconds: 12, suffix: "12s" },
1551
+ { maxSeconds: 15, suffix: "15s" },
1552
+ ],
1553
+ "seedance-2-mini": [
1554
+ { maxSeconds: 4, suffix: "4s" },
1555
+ { maxSeconds: 8, suffix: "8s" },
1556
+ { maxSeconds: 12, suffix: "12s" },
1557
+ { maxSeconds: 15, suffix: "15s" },
1558
+ ],
1559
+ "seedance-2-extend": [
1560
+ { maxSeconds: 4, suffix: "4s" },
1561
+ { maxSeconds: 8, suffix: "8s" },
1562
+ { maxSeconds: 12, suffix: "12s" },
1563
+ { maxSeconds: 15, suffix: "15s" },
1564
+ ],
1565
+ "seedance-2-fast": [
1566
+ { maxSeconds: 4, suffix: "4s" },
1567
+ { maxSeconds: 8, suffix: "8s" },
1568
+ { maxSeconds: 12, suffix: "12s" },
1569
+ { maxSeconds: 15, suffix: "15s" },
1570
+ ],
1571
+ // Grok Imagine Video 1.5 — true per-second billing (KIE 14.5 cr/s @480p, 25 cr/s
1572
+ // @720p, +2 cr/image). One tier per allowed second (1–15s) so the composite
1573
+ // identifier maps 1:1 to the seeded price — no rounding/overcharge for any on-menu value.
1574
+ "grok-imagine-video-1.5": [
1575
+ { maxSeconds: 1, suffix: "1s" },
1576
+ { maxSeconds: 2, suffix: "2s" },
1577
+ { maxSeconds: 3, suffix: "3s" },
1578
+ { maxSeconds: 4, suffix: "4s" },
1579
+ { maxSeconds: 5, suffix: "5s" },
1580
+ { maxSeconds: 6, suffix: "6s" },
1581
+ { maxSeconds: 7, suffix: "7s" },
1582
+ { maxSeconds: 8, suffix: "8s" },
1583
+ { maxSeconds: 9, suffix: "9s" },
1584
+ { maxSeconds: 10, suffix: "10s" },
1585
+ { maxSeconds: 11, suffix: "11s" },
1586
+ { maxSeconds: 12, suffix: "12s" },
1587
+ { maxSeconds: 13, suffix: "13s" },
1588
+ { maxSeconds: 14, suffix: "14s" },
1589
+ { maxSeconds: 15, suffix: "15s" },
1590
+ ],
1591
+ }
1592
+
1593
+ /**
1594
+ * Maps composer node types to their plan type identifier and the node data field
1595
+ * where the plan is stored. Used by render-video payload building, plan syncing,
1596
+ * and the frontend DAG executor.
1597
+ */
1598
+ export const COMPOSER_PLAN_MAP: Readonly<Record<string, { planType: string; planField: string }>> = {
1599
+ "video-composer": { planType: "scene-graph", planField: "sceneGraph" },
1600
+ "after-effects": { planType: "after-effects", planField: "effectPlan" },
1601
+ "lottie-overlay": { planType: "lottie-overlay", planField: "overlayPlan" },
1602
+ "3d-title": { planType: "3d-title", planField: "titlePlan" },
1603
+ "motion-graphics": { planType: "motion-graphics", planField: "motionPlan" },
1604
+ "composite": { planType: "composite", planField: "compositePlan" },
1605
+ }
1606
+
1607
+ /** Every composer plan-field name, derived from COMPOSER_PLAN_MAP (single source
1608
+ * of truth — consumed by preset plan-state hygiene and job-output extraction). */
1609
+ export const COMPOSER_PLAN_FIELDS: readonly string[] = [
1610
+ ...new Set(Object.values(COMPOSER_PLAN_MAP).map((m) => m.planField)),
1611
+ ]
1612
+
1613
+ /**
1614
+ * Duration tier breakpoints for motion control pricing (per-second billing).
1615
+ * Same shape as VIDEO_DURATION_TIERS entries but with 30s tier for long reference videos.
1616
+ */
1617
+ export const MOTION_DURATION_TIERS: ReadonlyArray<{ maxSeconds: number; suffix: string }> = [
1618
+ { maxSeconds: 5, suffix: "5s" },
1619
+ { maxSeconds: 10, suffix: "10s" },
1620
+ { maxSeconds: 15, suffix: "15s" },
1621
+ { maxSeconds: 30, suffix: "30s" },
1622
+ ]
1623
+
1624
+ /** Curated i2v providers compatible with character portrait reference input.
1625
+ * Subset of IMAGE_TO_VIDEO_PROVIDERS — excludes text-to-video-only models. */
1626
+ export const CHARACTER_MOTION_PROVIDERS = [
1627
+ "kling",
1628
+ "kling-turbo",
1629
+ "kling-3.0",
1630
+ "wan-i2v",
1631
+ "wan-2.7-i2v",
1632
+ ] as const
1633
+ export type CharacterMotionProvider = typeof CHARACTER_MOTION_PROVIDERS[number]
1634
+
1635
+ /**
1636
+ * Atmospheric video providers available to the Location Studio's motion tab.
1637
+ *
1638
+ * Tuned for ambient camera moves (dolly, pan, drift, parallax) rather than
1639
+ * character-driven motion. Subset of registered i2v providers; `seedance-2`
1640
+ * added as the cinematic option for establishing shots.
1641
+ *
1642
+ * Mirrors `CHARACTER_MOTION_PROVIDERS` shape — see entity-prompts.ts for the
1643
+ * naming convention.
1644
+ */
1645
+ export const LOCATION_ATMOSPHERE_PROVIDERS = [
1646
+ "kling",
1647
+ "kling-turbo",
1648
+ "kling-3.0",
1649
+ "wan-i2v",
1650
+ "wan-2.7-i2v",
1651
+ "seedance-2",
1652
+ ] as const
1653
+ export type LocationAtmosphereProvider =
1654
+ (typeof LOCATION_ATMOSPHERE_PROVIDERS)[number]
1655
+
1656
+ /**
1657
+ * Subset of i2v providers tuned for short ambient object-motion clips (rotate,
1658
+ * hover, spin, parallax). Excludes premium models (Veo3, Veo3.1, Grok-i2v,
1659
+ * Sora2, Sora2-pro, Hailuo-2.3-pro) which are cost-prohibitive for the
1660
+ * product-loop use case (several times kling-turbo's per-clip cost).
1661
+ * Phase 2 candidate for adding veo3.1 if demand for premium hero shots
1662
+ * materializes.
1663
+ */
1664
+ export const OBJECT_MOTION_PROVIDERS = [
1665
+ "kling-turbo", // 5s, fast, cheap (10 credits) — default
1666
+ "kling", // 5/10s
1667
+ "kling-3.0", // 3-15s, premium
1668
+ "minimax", // 5s, end-frame support
1669
+ "hailuo-2.3", // 6/10s
1670
+ "wan-i2v", // 5/10/15s
1671
+ "seedance", // 4/8/12s
1672
+ "bytedance-lite", // 5/10s
1673
+ ] as const
1674
+ export type ObjectMotionProvider = (typeof OBJECT_MOTION_PROVIDERS)[number]
1675
+
1676
+ // =====================================================================
1677
+ // Scene Input Modes + Video Model Capability Registry (v4.0 + v4.1 spec §6.10)
1678
+ // =====================================================================
1679
+
1680
+ /**
1681
+ * SceneInputMode — the input-shape contract a video model exposes to the
1682
+ * Scene Director. Gates which `video_model` values are valid for a given
1683
+ * scene's `shot_input_mode`. Consumed by Scene Director LLM prompting,
1684
+ * route Zod validation, and the frontend SceneNode config panel.
1685
+ *
1686
+ * See architecture spec §6.10 + v4.1 Methods 2/3/8/10 for per-mode semantics.
1687
+ */
1688
+ export const SceneInputModeSchema = z.enum([
1689
+ "text", // text-to-video
1690
+ "first_frame", // i2v with start image only
1691
+ "first_last_frame", // i2v with paired start+end (v4.1 Method 2)
1692
+ "ref_images", // multi-ref consistency models
1693
+ "multi_shot", // native multi-shot models (Kling Omni, Seedance multi)
1694
+ "video_continuation", // v4.1 Method 3 — extend prior clip (VEO extend, Seedance video-ref)
1695
+ "frame_interpolation", // v4.1 Method 8 — N sparse keyframes → interpolated video
1696
+ "camera_path", // v4.1 Method 10 — parametric 3D camera path
1697
+ ])
1698
+ export type SceneInputMode = z.infer<typeof SceneInputModeSchema>
1699
+
1700
+ /**
1701
+ * Prompting style each model expects from the Scene Director.
1702
+ * Veo-family wants comma-separated cinematic tags; Kling-family wants
1703
+ * natural-language prose; Hailuo-family wants compact single-sentence prompts.
1704
+ */
1705
+ export type ModelPromptingStyle =
1706
+ | "cinematic_tag_heavy" // Veo-family — comma-separated tags
1707
+ | "natural_language" // Kling-family — prose descriptions
1708
+ | "compact" // Hailuo-family — single-sentence
1709
+
1710
+ export interface VideoModelCapabilities {
1711
+ inputModes: SceneInputMode[]
1712
+ maxShotsPerCall?: number // for multi_shot models
1713
+ supportsLipSyncIntegrated?: boolean // Kling Avatar / VEO-3 etc
1714
+ supportsVideoExtension?: boolean // v4.1 Method 3
1715
+ supportsCameraPath?: boolean // v4.1 Method 10
1716
+ maxInterpolationKeyframes?: number // v4.1 Method 8
1717
+ maxDurationSeconds: number
1718
+ prompting_style: ModelPromptingStyle
1719
+ /**
1720
+ * Maximum number of reference IMAGES the model accepts in one call,
1721
+ * including the start frame. Drives `allocateReferenceSlots` in
1722
+ * `backend/src/ee/pipelines/continuity.ts` (Phase 1C.1 §5.13.3) — when
1723
+ * the budget is 1, multi-shot/character/object refs are silently
1724
+ * dropped and the slot allocator emits a `pipeline:warning` event.
1725
+ * Defaults to 1 when omitted.
1726
+ */
1727
+ maxReferenceImages?: number
1728
+ }
1729
+
1730
+ /**
1731
+ * VIDEO_MODEL_CAPS — single source of truth for what each video model supports.
1732
+ * Consumed by: Scene Director LLM (filters eligible models), route Zod validation
1733
+ * (rejects invalid video_model for the chosen shot_input_mode), frontend SceneNode
1734
+ * config panel (filters dropdown options).
1735
+ *
1736
+ * Adding a new video model: add an entry here, run audit-providers skill to verify
1737
+ * downstream wiring.
1738
+ */
1739
+ export const VIDEO_MODEL_CAPS: Record<string, VideoModelCapabilities> = {
1740
+ "kling": {
1741
+ inputModes: ["first_frame", "first_last_frame", "text"],
1742
+ maxDurationSeconds: 10,
1743
+ prompting_style: "natural_language",
1744
+ maxReferenceImages: 1,
1745
+ },
1746
+ "kling-3-omni": {
1747
+ inputModes: ["ref_images", "multi_shot"],
1748
+ maxShotsPerCall: 5,
1749
+ maxDurationSeconds: 15,
1750
+ prompting_style: "natural_language",
1751
+ // Kling Omni accepts a strong multi-ref budget (start frame + up to
1752
+ // ~6 character/scene refs). Drives `allocateReferenceSlots` in
1753
+ // continuity.ts to surface all of them.
1754
+ maxReferenceImages: 7,
1755
+ },
1756
+ "veo3.1": {
1757
+ inputModes: ["first_frame", "text", "video_continuation"],
1758
+ supportsVideoExtension: true,
1759
+ maxDurationSeconds: 8,
1760
+ prompting_style: "cinematic_tag_heavy",
1761
+ maxReferenceImages: 1,
1762
+ },
1763
+ "seedance-2": {
1764
+ inputModes: ["first_frame", "first_last_frame", "ref_images", "video_continuation"],
1765
+ supportsVideoExtension: true,
1766
+ maxDurationSeconds: 10,
1767
+ prompting_style: "natural_language",
1768
+ // SEEDANCE_2_REF_LIMITS.images is the hard upper bound (9). 1C.1
1769
+ // caps to a more conservative 5 to leave headroom for primary
1770
+ // character + location + 3 secondary refs.
1771
+ maxReferenceImages: 5,
1772
+ },
1773
+ "hailuo-2.3-pro": {
1774
+ inputModes: ["first_frame", "text"],
1775
+ maxDurationSeconds: 10,
1776
+ prompting_style: "compact",
1777
+ maxReferenceImages: 1,
1778
+ },
1779
+ "hailuo-standard": {
1780
+ inputModes: ["first_frame", "first_last_frame"],
1781
+ maxDurationSeconds: 6,
1782
+ prompting_style: "compact",
1783
+ maxReferenceImages: 1,
1784
+ },
1785
+ "minimax": {
1786
+ inputModes: ["first_frame", "first_last_frame"],
1787
+ maxDurationSeconds: 6,
1788
+ prompting_style: "compact",
1789
+ maxReferenceImages: 1,
1790
+ },
1791
+ "kling-turbo": {
1792
+ inputModes: ["first_frame", "first_last_frame"],
1793
+ maxDurationSeconds: 10,
1794
+ prompting_style: "natural_language",
1795
+ maxReferenceImages: 1,
1796
+ },
1797
+ "bytedance-lite": {
1798
+ inputModes: ["first_frame", "first_last_frame"],
1799
+ maxDurationSeconds: 10,
1800
+ prompting_style: "compact",
1801
+ maxReferenceImages: 1,
1802
+ },
1803
+ // v4.1 Method 8 — Frame interpolation (sparse keyframes → interpolated video).
1804
+ // RIFE: Replicate-hosted, optical-flow-based interpolation, up to 8 keyframes.
1805
+ "rife": {
1806
+ inputModes: ["frame_interpolation"],
1807
+ maxInterpolationKeyframes: 8,
1808
+ maxDurationSeconds: 60,
1809
+ prompting_style: "compact",
1810
+ },
1811
+ // Topaz Apollo: cloud-hosted (Topaz.ai), higher keyframe budget.
1812
+ // Provider routing currently stubs as `provider_not_available:topaz-apollo`
1813
+ // pending KIE catalog check — entry kept so Shot List Critic surfaces it.
1814
+ "topaz-apollo": {
1815
+ inputModes: ["frame_interpolation"],
1816
+ maxInterpolationKeyframes: 16,
1817
+ maxDurationSeconds: 60,
1818
+ prompting_style: "compact",
1819
+ },
1820
+ // v4.1 Method 10 — Parametric 3D camera path (Stability AI SV3D via Replicate).
1821
+ // SV3D takes a single image + a camera path and renders a 3D-orbit video.
1822
+ "stable-video-3d": {
1823
+ inputModes: ["camera_path", "first_frame"],
1824
+ supportsCameraPath: true,
1825
+ maxDurationSeconds: 5,
1826
+ prompting_style: "natural_language",
1827
+ maxReferenceImages: 1,
1828
+ },
1829
+ }
1830
+
1831
+ /**
1832
+ * modelsForInputMode — filter VIDEO_MODEL_CAPS to models that support the given mode.
1833
+ * Used at three call sites: Scene Director prompt construction, route Zod validation,
1834
+ * frontend config panel dropdown options.
1835
+ */
1836
+ export const modelsForInputMode = (mode: SceneInputMode): string[] =>
1837
+ Object.entries(VIDEO_MODEL_CAPS)
1838
+ .filter(([, caps]) => caps.inputModes.includes(mode))
1839
+ .map(([model]) => model)
1840
+
1841
+ /**
1842
+ * preferredInputModeForModel — pick the best `shot_input_mode` for a pinned
1843
+ * video model so the pipeline adapts its input wiring to the chosen model:
1844
+ *
1845
+ * - models that accept `ref_images` (Seedance 2, Kling Omni) → `"ref_images"`,
1846
+ * so the character/location reference portraits feed the video model
1847
+ * directly (strongest identity lock).
1848
+ * - otherwise models that accept a start frame → `"first_frame"`, so the
1849
+ * per-scene keyframe is connected as the opening frame.
1850
+ *
1851
+ * Returns `undefined` for unknown/unregistered models (not in VIDEO_MODEL_CAPS)
1852
+ * or models that expose neither mode, so the caller keeps its own default.
1853
+ *
1854
+ * `first_last_frame` is deliberately NOT auto-selected — the animate stage
1855
+ * does not yet implement paired start+end keyframes (Method 2), so a
1856
+ * first_last_frame-capable model still resolves to `first_frame` here.
1857
+ */
1858
+ export function preferredInputModeForModel(
1859
+ model: string | undefined,
1860
+ ): SceneInputMode | undefined {
1861
+ if (!model) return undefined
1862
+ const caps = VIDEO_MODEL_CAPS[model]
1863
+ if (!caps) return undefined
1864
+ if (caps.inputModes.includes("ref_images")) return "ref_images"
1865
+ if (caps.inputModes.includes("first_frame")) return "first_frame"
1866
+ return undefined
1867
+ }
1868
+
1869
+ /* ────────────────────────────────────────────────────────────────────────────
1870
+ * Platform default video model (single source of truth)
1871
+ *
1872
+ * Applied wherever a video request omits `provider`: the generate-video and
1873
+ * text-to-video routes (credit resolution + dispatch), the DAG payload
1874
+ * builder, and the KIE provider's last-resort fallback. Chosen from usage
1875
+ * data (2026-07): most-selected video model with the highest completion rate.
1876
+ * ──────────────────────────────────────────────────────────────────────────── */
1877
+
1878
+ export const DEFAULT_VIDEO_PROVIDER = "seedance-2-fast"
1879
+
1880
+ /**
1881
+ * Duration paired with the default provider when BOTH provider and duration
1882
+ * are omitted. Pins the nothing-specified request to the default model's
1883
+ * cheapest seeded tier (4s @ 480p — resolution omission already resolves to
1884
+ * 480p in buildVideoCreditModelIdentifier), so first-touch cost stays at
1885
+ * entry level and longer clips are an explicit choice.
1886
+ */
1887
+ export const DEFAULT_VIDEO_DURATION_SEC = 4
1888
+
1889
+ /**
1890
+ * Resolve the effective (provider, duration) for a video request.
1891
+ *
1892
+ * - provider omitted → DEFAULT_VIDEO_PROVIDER
1893
+ * - duration omitted AND provider was omitted → DEFAULT_VIDEO_DURATION_SEC
1894
+ * - an explicitly chosen provider keeps its own duration semantics (an
1895
+ * omitted duration falls through to that model's natural default)
1896
+ *
1897
+ * Used by both single-node routes AND the workflow payload builder so the
1898
+ * two execution paths cannot drift (the route/DAG default previously
1899
+ * disagreed: "minimax" vs "kling").
1900
+ */
1901
+ export function applyDefaultVideoSelection<D>(input: {
1902
+ provider?: string | null
1903
+ duration?: D
1904
+ }): { provider: string; duration: D | number | undefined } {
1905
+ const providerOmitted = input.provider == null || input.provider === ""
1906
+ return {
1907
+ provider: providerOmitted ? DEFAULT_VIDEO_PROVIDER : (input.provider as string),
1908
+ duration: input.duration ?? (providerOmitted ? DEFAULT_VIDEO_DURATION_SEC : undefined),
1909
+ }
1910
+ }