@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,303 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ VIDEO_UTIL_PRICING,
4
+ estimateLoopVideoCredits,
5
+ estimateTrimVideoCredits,
6
+ estimateCombineVideosCredits,
7
+ } from "../credit-estimators/video-utils.js"
8
+ import {
9
+ estimateLoopTrimAddonCredits,
10
+ type LoopTrimEstimatorInput,
11
+ } from "../credit-estimators/video-utils.js"
12
+ import { assembleNarratedVideoCredits } from "../credit-estimators/video-utils.js"
13
+
14
+ describe("estimateLoopVideoCredits", () => {
15
+ describe("repeat mode", () => {
16
+ it("uses upstream duration when provided", () => {
17
+ // 4× a 5s clip = 20s output → ceil(20/5) = 4
18
+ expect(estimateLoopVideoCredits({ mode: "repeat", repeatCount: 4 }, 5)).toBe(4)
19
+ })
20
+
21
+ it("falls back to 8s per repeat when upstream duration is missing", () => {
22
+ // 4 × 8 = 32s → ceil(32/5) = 7
23
+ expect(estimateLoopVideoCredits({ mode: "repeat", repeatCount: 4 })).toBe(7)
24
+ })
25
+
26
+ it("defaults repeatCount to 2 when missing", () => {
27
+ // 2 × 8 = 16s → ceil(16/5) = 4
28
+ expect(estimateLoopVideoCredits({ mode: "repeat" })).toBe(4)
29
+ })
30
+ })
31
+
32
+ describe("duration mode", () => {
33
+ it("uses targetDuration regardless of upstream", () => {
34
+ // ceil(60/5) = 12
35
+ expect(estimateLoopVideoCredits({ mode: "duration", targetDuration: 60 }, 5)).toBe(12)
36
+ })
37
+
38
+ it("defaults targetDuration to fallback when missing", () => {
39
+ // ceil(8/5) = 2
40
+ expect(estimateLoopVideoCredits({ mode: "duration" })).toBe(2)
41
+ })
42
+ })
43
+
44
+ describe("smart-loop-cut adder", () => {
45
+ it("adds ceil(lookback/24) when enabled", () => {
46
+ // 30s base = 6, lookback 16 → ceil(16/24) = 1, total 7
47
+ expect(estimateLoopVideoCredits({
48
+ mode: "duration", targetDuration: 30,
49
+ smartLoopCutBeforeRepeat: true, smartLoopCutLookback: 16,
50
+ })).toBe(7)
51
+ })
52
+
53
+ it("uses default lookback 16 when smart-cut on but lookback missing", () => {
54
+ // 60s base = 12, default lookback 16 → +1 = 13
55
+ expect(estimateLoopVideoCredits({
56
+ mode: "duration", targetDuration: 60,
57
+ smartLoopCutBeforeRepeat: true,
58
+ })).toBe(13)
59
+ })
60
+
61
+ it("max lookback 64 stays within bound", () => {
62
+ // 60s base = 12, lookback 64 → ceil(64/24) = 3, total 15
63
+ expect(estimateLoopVideoCredits({
64
+ mode: "duration", targetDuration: 60,
65
+ smartLoopCutBeforeRepeat: true, smartLoopCutLookback: 64,
66
+ })).toBe(15)
67
+ })
68
+
69
+ it("does NOT add when smartLoopCutBeforeRepeat is false", () => {
70
+ expect(estimateLoopVideoCredits({
71
+ mode: "duration", targetDuration: 60,
72
+ smartLoopCutBeforeRepeat: false, smartLoopCutLookback: 64,
73
+ })).toBe(12)
74
+ })
75
+ })
76
+
77
+ describe("min-1 floor", () => {
78
+ it("never returns 0 — even for sub-second output", () => {
79
+ expect(estimateLoopVideoCredits({ mode: "duration", targetDuration: 0.5 })).toBe(1)
80
+ })
81
+ })
82
+
83
+ describe("constants", () => {
84
+ it("exports the expected pricing constants", () => {
85
+ expect(VIDEO_UTIL_PRICING.CREDITS_PER_5_SEC).toBe(1)
86
+ expect(VIDEO_UTIL_PRICING.FRAMES_PER_CREDIT).toBe(24)
87
+ expect(VIDEO_UTIL_PRICING.FALLBACK_DURATION_SECONDS).toBe(8)
88
+ })
89
+ })
90
+ })
91
+
92
+ describe("estimateTrimVideoCredits", () => {
93
+ describe("time mode (default)", () => {
94
+ it("computes credits from endTime - startTime", () => {
95
+ // 0–10s = 10s → ceil(10/5) = 2
96
+ expect(estimateTrimVideoCredits({ trimMode: "time", startTime: 0, endTime: 10 })).toBe(2)
97
+ })
98
+
99
+ it("treats missing trimMode as time mode", () => {
100
+ // ceil(15/5) = 3
101
+ expect(estimateTrimVideoCredits({ startTime: 5, endTime: 20 })).toBe(3)
102
+ })
103
+
104
+ it("defaults missing start/end to 0", () => {
105
+ // 0-0 = 0s, but min-1 → 1
106
+ expect(estimateTrimVideoCredits({ trimMode: "time" })).toBe(1)
107
+ })
108
+ })
109
+
110
+ describe("frames mode", () => {
111
+ it("subtracts trim frames from upstream duration at 24fps", () => {
112
+ // 10s upstream - 24/24 - 24/24 = 8s → ceil(8/5) = 2
113
+ expect(estimateTrimVideoCredits({
114
+ trimMode: "frames", trimStartFrames: 24, trimEndFrames: 24,
115
+ }, 10)).toBe(2)
116
+ })
117
+
118
+ it("uses 8s fallback when upstream missing", () => {
119
+ // 8s - 0 - 0 = 8s → ceil(8/5) = 2
120
+ expect(estimateTrimVideoCredits({ trimMode: "frames" })).toBe(2)
121
+ })
122
+
123
+ it("clamps to 0 if frame trim exceeds duration", () => {
124
+ // 5s - 240/24 - 0 = -5 clamped to 0 → max(1, ceil(0)) = 1
125
+ expect(estimateTrimVideoCredits({
126
+ trimMode: "frames", trimStartFrames: 240, trimEndFrames: 0,
127
+ }, 5)).toBe(1)
128
+ })
129
+ })
130
+
131
+ describe("smart-loop-cut mode", () => {
132
+ it("upstream-based base + lookback adder", () => {
133
+ // 10s upstream → base 2, lookback 16 → +1 = 3
134
+ expect(estimateTrimVideoCredits({
135
+ trimMode: "smart-loop-cut", smartLoopCutLookback: 16,
136
+ }, 10)).toBe(3)
137
+ })
138
+
139
+ it("uses default lookback 16 when missing", () => {
140
+ // 8s upstream → base 2, default 16 → +1 = 3
141
+ expect(estimateTrimVideoCredits({ trimMode: "smart-loop-cut" })).toBe(3)
142
+ })
143
+ })
144
+
145
+ describe("seconds mode (relative trim in seconds)", () => {
146
+ it("subtracts trim seconds from upstream duration", () => {
147
+ // 30s - 2 - 3 = 25s → ceil(25/5) = 5
148
+ expect(estimateTrimVideoCredits({
149
+ trimMode: "seconds", trimStartSeconds: 2, trimEndSeconds: 3,
150
+ }, 30)).toBe(5)
151
+ })
152
+
153
+ it("clamps to min 1 credit if trim exceeds duration", () => {
154
+ // 5s - 10 - 0 = -5 clamp to 0 → max(1, ceil(0)) = 1
155
+ expect(estimateTrimVideoCredits({
156
+ trimMode: "seconds", trimStartSeconds: 10,
157
+ }, 5)).toBe(1)
158
+ })
159
+ })
160
+
161
+ describe("keep-first-seconds mode", () => {
162
+ it("output is min(upstream, keepFirstSeconds)", () => {
163
+ // keepFirst=8, upstream=20 → output 8s → ceil(8/5) = 2
164
+ expect(estimateTrimVideoCredits({
165
+ trimMode: "keep-first-seconds", keepFirstSeconds: 8,
166
+ }, 20)).toBe(2)
167
+ })
168
+
169
+ it("clamps to upstream when keep > upstream", () => {
170
+ // keepFirst=30, upstream=5 → output 5s → ceil(5/5) = 1
171
+ expect(estimateTrimVideoCredits({
172
+ trimMode: "keep-first-seconds", keepFirstSeconds: 30,
173
+ }, 5)).toBe(1)
174
+ })
175
+ })
176
+
177
+ describe("keep-last-seconds mode", () => {
178
+ it("output is min(upstream, keepLastSeconds)", () => {
179
+ // keepLast=10, upstream=30 → output 10s → ceil(10/5) = 2
180
+ expect(estimateTrimVideoCredits({
181
+ trimMode: "keep-last-seconds", keepLastSeconds: 10,
182
+ }, 30)).toBe(2)
183
+ })
184
+
185
+ it("uses 8s fallback when upstream missing", () => {
186
+ // keepLast=10, upstream undefined → upstream=8 → min(8,10)=8 → ceil(8/5)=2
187
+ expect(estimateTrimVideoCredits({
188
+ trimMode: "keep-last-seconds", keepLastSeconds: 10,
189
+ })).toBe(2)
190
+ })
191
+ })
192
+ })
193
+
194
+ describe("estimateCombineVideosCredits", () => {
195
+ it("2 inputs with cut transition", () => {
196
+ // 5+5=10s, cut → 0 transition overlap, 0 frame trim, numInputs-2=0
197
+ // → ceil(10/5) + 0 = 2
198
+ expect(estimateCombineVideosCredits({ transition: "cut" }, [5, 5])).toBe(2)
199
+ })
200
+
201
+ it("3 inputs with fade transition (0.5s)", () => {
202
+ // 10+10+10=30s, 2 fades × 0.5 = 1s overlap → 29s → ceil(29/5)=6
203
+ // numInputs-2 = 1 → 6 + 1 = 7
204
+ expect(estimateCombineVideosCredits({
205
+ transition: "fade", transitionDuration: 0.5,
206
+ }, [10, 10, 10])).toBe(7)
207
+ })
208
+
209
+ it("5 inputs with dissolve and per-clip frame trim (24+24)", () => {
210
+ // 5×8=40s, 4 transitions × 0.5 = 2s overlap, frame trim 48/24=2s × 5 = 10s
211
+ // → 40 - 2 - 10 = 28s → ceil(28/5) = 6
212
+ // numInputs-2 = 3 → 6 + 3 = 9
213
+ expect(estimateCombineVideosCredits({
214
+ transition: "dissolve",
215
+ transitionDuration: 0.5,
216
+ trimStartFrames: 24,
217
+ trimEndFrames: 24,
218
+ }, [8, 8, 8, 8, 8])).toBe(9)
219
+ })
220
+
221
+ it("falls back to 8s per missing upstream entry", () => {
222
+ // [undef, undef, undef] → 3×8=24s, cut → ceil(24/5)=5, +1 (numInputs-2) = 6
223
+ expect(estimateCombineVideosCredits(
224
+ { transition: "cut" },
225
+ [undefined, undefined, undefined],
226
+ )).toBe(6)
227
+ })
228
+
229
+ it("treats empty input array as 2-input fallback", () => {
230
+ // numInputs = 0 || 2 = 2; total = 0 → max(1, …) = 1
231
+ expect(estimateCombineVideosCredits({ transition: "cut" }, [])).toBe(1)
232
+ })
233
+
234
+ it("min-1 floor when total clamps to 0", () => {
235
+ // 2 × 0s = 0, no overlap, num-2=0 → max(1, ceil(0)) = 1
236
+ expect(estimateCombineVideosCredits({ transition: "cut" }, [0, 0])).toBe(1)
237
+ })
238
+
239
+ it("does not subtract transition overlap on cut", () => {
240
+ // [10, 10] cut → ceil(20/5)=4, numInputs-2=0 → 4
241
+ expect(estimateCombineVideosCredits({ transition: "cut", transitionDuration: 999 }, [10, 10])).toBe(4)
242
+ })
243
+
244
+ it("uses default transitionDuration 0.5 when missing on a non-cut", () => {
245
+ // [10, 10] fade, no transitionDuration → 20 - 0.5 = 19.5 → ceil(19.5/5)=4
246
+ expect(estimateCombineVideosCredits({ transition: "fade" }, [10, 10])).toBe(4)
247
+ })
248
+ })
249
+
250
+ describe("estimateLoopTrimAddonCredits", () => {
251
+ it("returns 0 when loopTrim is undefined", () => {
252
+ expect(estimateLoopTrimAddonCredits(undefined, 8)).toBe(0)
253
+ })
254
+
255
+ it("returns 0 when enabled is false", () => {
256
+ expect(estimateLoopTrimAddonCredits({ enabled: false }, 8)).toBe(0)
257
+ })
258
+
259
+ it("computes ceil(duration/5) + ceil(framesToTest/24) when enabled", () => {
260
+ // 8s / 5 = 2 (ceil), 16 / 24 = 1 (ceil) → 3
261
+ expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 16 }, 8)).toBe(3)
262
+ })
263
+
264
+ it("uses default framesToTest=16 when omitted", () => {
265
+ // 10s / 5 = 2, 16 / 24 = 1 → 3
266
+ expect(estimateLoopTrimAddonCredits({ enabled: true }, 10)).toBe(3)
267
+ })
268
+
269
+ it("clamps framesToTest to 64 max", () => {
270
+ // 8s / 5 = 2, 64 / 24 = 3 → 5
271
+ expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 999 }, 8)).toBe(5)
272
+ })
273
+
274
+ it("clamps framesToTest to 1 min", () => {
275
+ // 8s / 5 = 2, 1 / 24 = 1 → 3
276
+ expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 0 }, 8)).toBe(3)
277
+ })
278
+
279
+ it("scales with output duration", () => {
280
+ // 60s / 5 = 12, 16 / 24 = 1 → 13
281
+ expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 16 }, 60)).toBe(13)
282
+ })
283
+
284
+ it("returns 0 when framesToTest=64 but enabled is false (precedence)", () => {
285
+ expect(estimateLoopTrimAddonCredits({ enabled: false, framesToTest: 64 }, 60)).toBe(0)
286
+ })
287
+ })
288
+
289
+ describe("assembleNarratedVideoCredits", () => {
290
+ it("matches the worked examples 6→4, 24→7, 60→13", () => {
291
+ expect(assembleNarratedVideoCredits(6)).toBe(4)
292
+ expect(assembleNarratedVideoCredits(24)).toBe(7)
293
+ expect(assembleNarratedVideoCredits(60)).toBe(13)
294
+ })
295
+
296
+ it("is 3 flat credits at the theoretical N=0 floor (never shown directly — call sites clamp N to 1)", () => {
297
+ expect(assembleNarratedVideoCredits(0)).toBe(3)
298
+ })
299
+
300
+ it("returns the 1-block floor of 4 credits", () => {
301
+ expect(assembleNarratedVideoCredits(1)).toBe(4)
302
+ })
303
+ })
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ VOICE_CHANGER_MODELS,
4
+ VOICE_CHANGER_MODEL_IDS,
5
+ DEFAULT_VOICE_CHANGER_MODEL,
6
+ } from "../voice-changer-models.js"
7
+
8
+ describe("voice-changer-models", () => {
9
+ it("exposes the ElevenLabs speech-to-speech models", () => {
10
+ expect(VOICE_CHANGER_MODEL_IDS).toEqual([
11
+ "eleven_multilingual_sts_v2",
12
+ "eleven_english_sts_v2",
13
+ ])
14
+ })
15
+
16
+ it("defaults to the Multilingual model (ElevenLabs-recommended, even for English)", () => {
17
+ // ElevenLabs' docs recommend Multilingual v2 even for English source
18
+ // audio (it often outperforms the English-only model), and it's
19
+ // required for non-English audio — so it's the default. English v2
20
+ // remains selectable.
21
+ expect(DEFAULT_VOICE_CHANGER_MODEL).toBe("eleven_multilingual_sts_v2")
22
+ expect(VOICE_CHANGER_MODEL_IDS).toContain(DEFAULT_VOICE_CHANGER_MODEL)
23
+ })
24
+
25
+ it("gives every model a non-empty label and description for the picker", () => {
26
+ expect(VOICE_CHANGER_MODELS.length).toBe(VOICE_CHANGER_MODEL_IDS.length)
27
+ for (const m of VOICE_CHANGER_MODELS) {
28
+ expect(m.value.length).toBeGreaterThan(0)
29
+ expect(m.label.length).toBeGreaterThan(0)
30
+ expect(m.desc.length).toBeGreaterThan(0)
31
+ }
32
+ })
33
+ })
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { stripExportContent } from "../workflow-export.js"
3
+ import { EXECUTION_DATA_KEYS } from "../node-runtime-keys.js"
4
+ import type { GenericNode } from "../types.js"
5
+
6
+ /**
7
+ * Invariant guard for the template-export leak class (audit R2-H4): a
8
+ * "shareable" template export must never carry runtime/result fields —
9
+ * generated media URLs, internal job ids, trained-LoRA identity, etc. Those
10
+ * are enumerated in EXECUTION_DATA_KEYS (the single source of truth), and
11
+ * GENERATED_FIELDS is built from it, so this test fails the moment a new
12
+ * runtime key is added without being covered.
13
+ */
14
+ describe("stripExportContent — template export hygiene", () => {
15
+ // `shots` lives in EXECUTION_DATA_KEYS but is user config (Kling-3.0
16
+ // storyboard) — it must SURVIVE a template export, not be stripped.
17
+ const CONFIG_KEPT = new Set(["shots"])
18
+
19
+ it("strips runtime/result EXECUTION_DATA_KEYS while keeping config fields", () => {
20
+ const data: Record<string, unknown> = {}
21
+ for (const key of EXECUTION_DATA_KEYS) data[key] = "SENSITIVE_RUNTIME_VALUE"
22
+ data.prompt = "keep me" // a real config field — must survive the strip
23
+ data.provider = "veo3.1"
24
+
25
+ const node: GenericNode = { id: "n1", type: "generate-video", data }
26
+ const [out] = stripExportContent([node])
27
+ const outData = out.data as Record<string, unknown>
28
+
29
+ for (const key of EXECUTION_DATA_KEYS) {
30
+ if (CONFIG_KEPT.has(key)) continue
31
+ expect(outData[key], `${key} must be stripped from a template export`).toBeUndefined()
32
+ }
33
+ expect(outData.prompt).toBe("keep me")
34
+ expect(outData.provider).toBe("veo3.1")
35
+ // Regression guard: Kling-3.0 multishot config must survive template export.
36
+ expect(outData.shots, "shots (Kling-3 config) must NOT be stripped").toBe("SENSITIVE_RUNTIME_VALUE")
37
+ })
38
+
39
+ it("clears faceDbId / referencedWorkflowId on face + sub-workflow template nodes", () => {
40
+ const face: GenericNode = { id: "f1", type: "face", data: { faceDbId: "exporter-face-id", name: "Hero" } }
41
+ const sub: GenericNode = { id: "s1", type: "sub-workflow", data: { referencedWorkflowId: "exporter-wf-id" } }
42
+ const [outFace, outSub] = stripExportContent([face, sub])
43
+ expect((outFace.data as Record<string, unknown>).faceDbId).toBeUndefined()
44
+ expect((outSub.data as Record<string, unknown>).referencedWorkflowId).toBeUndefined()
45
+ })
46
+ })
@@ -0,0 +1,213 @@
1
+ /**
2
+ * AI Avatar (HeyGen) duration-bucketing + credit-identifier helpers.
3
+ * Single source of truth for the NON-monetary side of ai-avatar billing
4
+ * (duration estimation, bucket selection, composite credit-id construction) —
5
+ * imported by the frontend node UI (to build the id it hands to the
6
+ * `/v1/credits/model-cost` display lookup) AND the backend route/orchestrator
7
+ * (to resolve the credit-guard reservation id pre-Zod).
8
+ *
9
+ * The provider-$ rate table and the USD/credit cost FORMULAS derived from it
10
+ * live in `backend/src/lib/pricing/ai-avatar-cost.ts` (core, not ee/ — the
11
+ * HeyGen provider integration needs them regardless of edition). They were
12
+ * moved out of this package (published Apache-2.0 on npm — an irrevocable
13
+ * grant) per the 2026-07-06 public-flip IP audit, S5.
14
+ */
15
+
16
+ export type AiAvatarEngine = "avatar-v" | "avatar-iv"
17
+ export type AiAvatarResolution = "720p" | "1080p" | "4k"
18
+
19
+ /**
20
+ * Worst-case clip duration used as the credit-reserve ceiling.
21
+ * A 5000-character script at voiceSpeed 0.5 ≈ 5000/12/0.5 ≈ 833s → top bucket 900s.
22
+ */
23
+ export const AI_AVATAR_MAX_DURATION_SEC = 900
24
+
25
+ /**
26
+ * Duration buckets (seconds) used to pick a reserve ceiling at creditGuard
27
+ * time, when the actual clip length is unknown (text-mode scripts) or only
28
+ * known via a server-side ffprobe (audio mode).
29
+ *
30
+ * The low end is FINE-GRAINED (5/10/15/30/60s) so short clips — the common
31
+ * case — don't over-reserve. A 15s clip lands in the 15s bucket, not the old
32
+ * coarse 30s floor (which doubled the hold). The high end stays coarse because
33
+ * long-form scripts are rare and the bucket-up is a one-time refundable surplus.
34
+ *
35
+ * Bucket selection:
36
+ * - Text mode: bucket = pickAiAvatarBucket(ceil(scriptChars / 12 / voiceSpeed)).
37
+ * - Audio mode: bucket = pickAiAvatarBucket(ceil(probedDurationSec)) when the
38
+ * ffprobe preHandler stashed `__probedDurationSec` on the body; otherwise a
39
+ * MODEST 120s default (NOT the 900s top bucket — that was the bug: it
40
+ * reserved ~4000+ credits for a sub-minute clip).
41
+ *
42
+ * Worst-case derivation: 5000 chars at voiceSpeed=0.5 → ceil(5000/12/0.5) = 834s
43
+ * → fits in 900s bucket. The 900s top bucket covers all legal text inputs.
44
+ *
45
+ * Note: audio >900s (no probe) would undercharge against the 120s default, but
46
+ * the commit-time metered true-up CANNOT charge more than reserved — so a long
47
+ * un-probed audio clip is a (small) revenue leak, not an over-charge. The probe
48
+ * closes it for the normal path. The 900s bucket remains available for the
49
+ * probed long-audio case.
50
+ */
51
+ export const AI_AVATAR_DURATION_BUCKETS = [5, 10, 15, 30, 60, 120, 240, 360, 600, 900] as const
52
+
53
+ /**
54
+ * Modest fallback bucket (seconds) for AUDIO mode when the ffprobe preHandler
55
+ * did NOT stash a probed duration. Replaces the old 900s top-bucket fallback
56
+ * that over-reserved ~4000+ credits for short clips. The commit-time metered
57
+ * true-up is refund-only, so this is a reserve ceiling, not a charge.
58
+ */
59
+ export const AI_AVATAR_AUDIO_FALLBACK_SEC = 120
60
+
61
+ /**
62
+ * Hard cap (seconds) on AUDIO-mode clip length. HeyGen has no natural length
63
+ * limit on an audio-driven avatar, so a long audio = an expensive / possibly
64
+ * rejected generation and a bounded under-reserve window. We cap it here:
65
+ * - The worker TRIMS incoming audio longer than this down to AI_AVATAR_MAX_AUDIO_SEC
66
+ * (and warns the user, non-fatal).
67
+ * - The credit RESERVE is capped at this bucket too: even a 30-min probed
68
+ * audio buckets at 600s, never the 900s top bucket. Because the worker
69
+ * trims the actual clip to ≤600s, the metered true-up can only refund.
70
+ *
71
+ * Text mode (5000-char script) and cinematic (4–15s) are bounded elsewhere and
72
+ * are NOT affected by this cap.
73
+ */
74
+ export const AI_AVATAR_MAX_AUDIO_SEC = 600
75
+
76
+ export type AiAvatarDurationBucket = (typeof AI_AVATAR_DURATION_BUCKETS)[number]
77
+
78
+ const ALLOWED_ENGINES = new Set<AiAvatarEngine>(["avatar-v", "avatar-iv"])
79
+ const ALLOWED_RESOLUTIONS = new Set<AiAvatarResolution>(["720p", "1080p", "4k"])
80
+
81
+ /**
82
+ * Estimate script duration in seconds from character count and voice speed.
83
+ *
84
+ * Uses ~12 chars/sec at normal speed (voiceSpeed=1) — deliberately generous
85
+ * (biased high) so the credit hold covers TTS pacing variations. Slower speech
86
+ * (voiceSpeed < 1) produces a longer clip and thus a higher estimate.
87
+ * Minimum 1s.
88
+ *
89
+ * @param script Script text (undefined treated as empty).
90
+ * @param voiceSpeed HeyGen voice speed parameter, range 0.5–1.5 (default 1).
91
+ * Slower speed → more seconds → higher hold.
92
+ */
93
+ export function estimateScriptDurationSec(script: string | undefined, voiceSpeed = 1): number {
94
+ const speed = Math.min(1.5, Math.max(0.5, voiceSpeed || 1))
95
+ return Math.max(1, Math.ceil((script?.length ?? 0) / 12 / speed))
96
+ }
97
+
98
+ /**
99
+ * Pick the smallest duration bucket that is ≥ sec.
100
+ * Falls back to the maximum bucket (900s) when sec exceeds all buckets.
101
+ */
102
+ export function pickAiAvatarBucket(sec: number): AiAvatarDurationBucket {
103
+ for (const b of AI_AVATAR_DURATION_BUCKETS) {
104
+ if (sec <= b) return b
105
+ }
106
+ return 900
107
+ }
108
+
109
+ /**
110
+ * Composite credit identifier for a duration-bucketed reserve.
111
+ *
112
+ * Format: `"heygen-<engine>:<resolution>:<bucketSec>s"`
113
+ * e.g. `"heygen-avatar-iv:720p:60s"`.
114
+ */
115
+ export function aiAvatarReserveCreditId(
116
+ engine: AiAvatarEngine,
117
+ resolution: AiAvatarResolution,
118
+ bucketSec: number,
119
+ ): string {
120
+ return `heygen-${engine}:${resolution}:${bucketSec}s`
121
+ }
122
+
123
+ /**
124
+ * Resolve the credit identifier from a raw request body BEFORE Zod validation.
125
+ * Called at creditGuard preHandler time — mirrors how `resolveLipSyncIdentifier`
126
+ * reads the raw body in `backend/src/routes/lip-sync.ts`.
127
+ *
128
+ * - `speechMode === "text"`: bucket by estimated script duration, accounting for
129
+ * voiceSpeed (slower → longer clip → larger bucket). Surplus is refunded at commit.
130
+ * - Any other speechMode (audio / missing): bucket by `__probedDurationSec` when
131
+ * the ffprobe preHandler stashed it on the raw body; otherwise a MODEST 120s
132
+ * default (AI_AVATAR_AUDIO_FALLBACK_SEC) — NOT the 900s top bucket. Reserving
133
+ * the 900s ceiling for a sub-minute audio clip was the over-reservation bug
134
+ * (~4000+ credits held for a clip that costs ~40). The commit-time metered
135
+ * true-up is refund-only, so a modest reserve never over-charges. The effective
136
+ * seconds are ALSO capped at AI_AVATAR_MAX_AUDIO_SEC (600s) — the worker trims
137
+ * audio longer than that, so a 30-min audio reserves the 600s bucket, never 900s.
138
+ *
139
+ * - `avatarSource === "image"`: image-source mode is IV-class (HeyGen's own
140
+ * engine, no avatar_id) — bill at the avatar-iv rate regardless of the
141
+ * (ignored) `engine` field, reusing the existing heygen-avatar-iv:* ids.
142
+ *
143
+ * Falls back to ("avatar-iv", "720p", 120s) when fields are missing or invalid.
144
+ */
145
+ export function resolveAiAvatarCreditId(
146
+ body: Record<string, unknown> | undefined,
147
+ ): string {
148
+ const rawEngine = body?.engine as string | undefined
149
+ const rawResolution = body?.resolution as string | undefined
150
+ const speechMode = body?.speechMode as string | undefined
151
+ const avatarSource = body?.avatarSource as string | undefined
152
+
153
+ // Image-source mode is IV-class (its own engine, no IV/V lever) — pin the
154
+ // rate engine to avatar-iv and reuse the existing heygen-avatar-iv:* ids.
155
+ const engine: AiAvatarEngine =
156
+ avatarSource === "image"
157
+ ? "avatar-iv"
158
+ : rawEngine !== undefined && ALLOWED_ENGINES.has(rawEngine as AiAvatarEngine)
159
+ ? (rawEngine as AiAvatarEngine)
160
+ : "avatar-iv"
161
+
162
+ const resolution: AiAvatarResolution =
163
+ rawResolution !== undefined && ALLOWED_RESOLUTIONS.has(rawResolution as AiAvatarResolution)
164
+ ? (rawResolution as AiAvatarResolution)
165
+ : "720p"
166
+
167
+ let bucketSec: AiAvatarDurationBucket
168
+ if (speechMode === "text") {
169
+ // voiceSpeed is a float in [0.5, 1.5]; coerce to number, default 1.
170
+ // Slower speed → more seconds → larger bucket → larger hold (no undercharge).
171
+ const voiceSpeed = Number(body?.voiceSpeed) || 1
172
+ bucketSec = pickAiAvatarBucket(
173
+ estimateScriptDurationSec(body?.script as string | undefined, voiceSpeed),
174
+ )
175
+ } else {
176
+ // Audio/unknown mode. The ffprobe preHandler (video-sfx pattern) stashes
177
+ // the measured clip length on the raw body as `__probedDurationSec` so we
178
+ // can bucket by the ACTUAL audio duration. When present, bucket by it;
179
+ // otherwise fall back to a MODEST 120s default — NOT the 900s top bucket.
180
+ // The old 900s fallback over-reserved ~4000+ credits for sub-minute clips.
181
+ //
182
+ // Effective seconds are capped at AI_AVATAR_MAX_AUDIO_SEC (600s): audio mode
183
+ // has no natural length limit, so the worker TRIMS the actual clip to ≤600s
184
+ // and we cap the RESERVE at the matching 600s bucket. A 30-min probed audio
185
+ // thus reserves the 600s bucket, never the 900s top bucket. Since the worker
186
+ // trims actual to ≤600s, the metered commit can only refund.
187
+ const probed = Number(body?.__probedDurationSec)
188
+ const effectiveSec = Math.min(
189
+ Number.isFinite(probed) && probed > 0
190
+ ? Math.ceil(probed)
191
+ : AI_AVATAR_AUDIO_FALLBACK_SEC,
192
+ AI_AVATAR_MAX_AUDIO_SEC,
193
+ )
194
+ bucketSec = pickAiAvatarBucket(effectiveSec)
195
+ }
196
+
197
+ return aiAvatarReserveCreditId(engine, resolution, bucketSec)
198
+ }
199
+
200
+ /**
201
+ * All 60 reserve credit IDs — every (engine × resolution × bucket) combination.
202
+ * 2 engines × 3 resolutions × 10 buckets (5/10/15/30/60/120/240/360/600/900s) = 60.
203
+ * Seeded in STATIC_CREDIT_COSTS and the model_pricing migration so
204
+ * getModelCreditBaseCost never hard-fails (503 price_not_configured) for any
205
+ * legal creditGuard input.
206
+ */
207
+ export const AI_AVATAR_RESERVE_IDS: string[] = [...ALLOWED_ENGINES].flatMap((engine) =>
208
+ [...ALLOWED_RESOLUTIONS].flatMap((resolution) =>
209
+ AI_AVATAR_DURATION_BUCKETS.map((bucketSec) =>
210
+ aiAvatarReserveCreditId(engine, resolution, bucketSec),
211
+ ),
212
+ ),
213
+ )
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Ancestor reference image collection — traverses upstream nodes to find
3
+ * image-producing ancestors, with passthrough for text/logic nodes.
4
+ * Shared between frontend and backend via a callback for extracting image URLs.
5
+ */
6
+
7
+ import type { GenericNode, GenericEdge } from "./types.js"
8
+
9
+ // Node types that produce reference images
10
+ export const IMAGE_REF_TYPES = new Set([
11
+ "upload-image", "face", "character", "object", "creature", "location",
12
+ "generate-image", "edit-image", "image-to-image",
13
+ ])
14
+
15
+ // Node types that pass through (don't produce images but connect to ancestors that do)
16
+ export const PASSTHROUGH_TYPES = new Set([
17
+ "ai-writer", "llm-chat", "split-text", "combine-text", "text-prompt", "list",
18
+ ])
19
+
20
+ /**
21
+ * Collect reference image URLs from upstream ancestor nodes.
22
+ * @param getImageUrl - Callback to extract an image URL from a node. Frontend uses
23
+ * extractNodeOutput(), backend uses nodeStates[id]?.output?.imageUrl.
24
+ */
25
+ export function collectAncestorRefs<N extends GenericNode, E extends GenericEdge>(
26
+ nodeId: string,
27
+ nodes: N[],
28
+ edges: E[],
29
+ getImageUrl: (node: N) => string | undefined,
30
+ visited = new Set<string>(),
31
+ ): string[] {
32
+ if (visited.has(nodeId)) return []
33
+ visited.add(nodeId)
34
+ const refs: string[] = []
35
+ const incoming = edges.filter((e) => e.target === nodeId)
36
+ for (const edge of incoming) {
37
+ const src = nodes.find((n) => n.id === edge.source)
38
+ if (!src) continue
39
+ if (IMAGE_REF_TYPES.has(src.type)) {
40
+ const url = getImageUrl(src)
41
+ if (url?.trim()) refs.push(url.trim())
42
+ }
43
+ if (PASSTHROUGH_TYPES.has(src.type)) {
44
+ refs.push(...collectAncestorRefs(src.id, nodes, edges, getImageUrl, visited))
45
+ }
46
+ }
47
+ return refs
48
+ }