@nodaro/shared 1.4.1

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,61 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { planSheetPanels } from "../panel-plan.js"
3
+ import { DEFAULT_SECTIONS } from "../catalog.js"
4
+ import type { SheetFlavour } from "../types.js"
5
+
6
+ const flavour = (sections?: SheetFlavour["sections"]): SheetFlavour => ({
7
+ outputFormat: "still", withText: true, showLabels: true, aspect: "landscape", background: "grey", sections,
8
+ })
9
+
10
+ describe("planSheetPanels", () => {
11
+ it("turnaround default = 4 head angles for a character", () => {
12
+ const panels = planSheetPanels("character", DEFAULT_SECTIONS.character.turnaround, flavour())
13
+ expect(panels).toHaveLength(4)
14
+ expect(panels.map((p) => p.variant)).toEqual(["front", "3/4 left", "left profile", "right profile"])
15
+ expect(panels.every((p) => p.board === "headAngles")).toBe(true)
16
+ })
17
+ it("structural sections (header/palette/notes) yield no panels", () => {
18
+ const panels = planSheetPanels("character", [{ kind: "header" }, { kind: "palette" }, { kind: "notes" }], flavour())
19
+ expect(panels).toHaveLength(0)
20
+ })
21
+ it("custom entries become custom panels carrying their prompt", () => {
22
+ const panels = planSheetPanels("character", [
23
+ { kind: "expression-board", entries: [
24
+ { kind: "custom", label: "Tired & Sweaty", prompt: "exhausted, sweaty, heavy eyes" },
25
+ { kind: "preset", variant: "smile" },
26
+ ] },
27
+ ], flavour())
28
+ expect(panels).toHaveLength(2)
29
+ const custom = panels.find((p) => p.custom)!
30
+ expect(custom.variant).toBe("Tired & Sweaty")
31
+ expect(custom.prompt).toBe("exhausted, sweaty, heavy eyes")
32
+ expect(panels.find((p) => !p.custom)!.variant).toBe("smile")
33
+ })
34
+ it("panelCount clamps to available variants (seasons has only 4)", () => {
35
+ const panels = planSheetPanels("location",
36
+ [{ kind: "environment-board", board: "seasons", panelCount: 9 }], flavour())
37
+ expect(panels).toHaveLength(4)
38
+ expect(panels.map((p) => p.variant)).toEqual(["spring", "summer", "autumn", "winter"])
39
+ })
40
+ it("clamps a non-positive panelCount to zero panels (never a negative slice)", () => {
41
+ expect(planSheetPanels("character", [{ kind: "expression-board", panelCount: -3 }], flavour())).toHaveLength(0)
42
+ expect(planSheetPanels("character", [{ kind: "expression-board", panelCount: 0 }], flavour())).toHaveLength(0)
43
+ })
44
+ it("throws when a board section has no resolvable board", () => {
45
+ expect(() => planSheetPanels("location", [{ kind: "environment-board" }], flavour()))
46
+ .toThrow(/environment-board/)
47
+ })
48
+ it("throws when total panels exceed MAX_PANELS_PER_SHEET", () => {
49
+ // Spread 30 custom panels across 3 distinct boards (10 each) so no single
50
+ // board trips MAX_CUSTOM_ENTRIES_PER_BOARD (12) — the sheet-total cap (24)
51
+ // is what must fire here.
52
+ const ten = (p: string) =>
53
+ Array.from({ length: 10 }, (_, i) => ({ kind: "custom" as const, label: `${p}${i}`, prompt: "x" }))
54
+ expect(() => planSheetPanels("character", [
55
+ { kind: "expression-board", entries: ten("e") },
56
+ { kind: "pose-board", entries: ten("p") },
57
+ { kind: "detail-board", entries: ten("d") },
58
+ ], flavour()))
59
+ .toThrow(/too many panels|MAX_PANELS/i)
60
+ })
61
+ })
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { buildPanelPrompt } from "../panel-prompts.js"
3
+
4
+ describe("buildPanelPrompt", () => {
5
+ it("builds a detail close-up prompt for a character eye variant", () => {
6
+ const p = buildPanelPrompt("character", "detail", "eyes", "Kaia")
7
+ expect(p.toLowerCase()).toContain("close-up")
8
+ expect(p).toContain("eyes")
9
+ })
10
+ it("builds a wardrobe prompt for a character outfit variant", () => {
11
+ const p = buildPanelPrompt("character", "wardrobe", "sporty", "Kaia")
12
+ expect(p.toLowerCase()).toContain("outfit")
13
+ })
14
+ it("returns a generic prompt for unknown board (custom passthrough)", () => {
15
+ expect(buildPanelPrompt("character", "detail", "left ear", "Kaia")).toContain("left ear")
16
+ })
17
+ })
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { planSheetGeneration, resolveSheetSections } from "../plan-generation.js"
3
+ import { BOARD_TO_ASSET_TYPE, DEFAULT_SECTIONS } from "../catalog.js"
4
+ import { SHEET_TYPES } from "../types.js"
5
+ import type { SheetFlavour, EntityKind, SheetType } from "../types.js"
6
+ const fl: SheetFlavour = { outputFormat: "still", withText: true, showLabels: true, aspect: "landscape", background: "grey" }
7
+
8
+ describe("BOARD_TO_ASSET_TYPE", () => {
9
+ it("maps standard boards to their assetType and detail/wardrobe to custom", () => {
10
+ expect(BOARD_TO_ASSET_TYPE.character.headAngles).toBe("headAngles")
11
+ expect(BOARD_TO_ASSET_TYPE.character.detail).toBe("custom")
12
+ expect(BOARD_TO_ASSET_TYPE.character.wardrobe).toBe("custom")
13
+ expect(BOARD_TO_ASSET_TYPE.object.materials).toBe("materials")
14
+ expect(BOARD_TO_ASSET_TYPE.location.timeOfDay).toBe("timeOfDay")
15
+ })
16
+ })
17
+
18
+ describe("planSheetGeneration", () => {
19
+ it("splits present vs missing and builds generate requests for missing", () => {
20
+ const buckets = { angles: [{ name: "front", url: "u/front" }] } // head-turnaround maps to 'angles' column
21
+ const r = planSheetGeneration("character",
22
+ [{ kind: "head-turnaround", entries: [{ kind: "preset", variant: "front" }, { kind: "preset", variant: "3/4 left" }] }],
23
+ fl, buckets, "Kaia")
24
+ expect(r.presentUrls).toEqual(["u/front"])
25
+ expect(r.missing).toHaveLength(1)
26
+ expect(r.missing[0]).toMatchObject({ assetType: "headAngles", variant: "3/4 left", attachToColumn: "angles", attachName: "3/4 left" })
27
+ expect(r.missing[0].userPrompt).toBeUndefined() // standard board, no custom prompt
28
+ })
29
+ it("detail board missing panels use assetType 'custom' + a built userPrompt", () => {
30
+ const r = planSheetGeneration("character", [{ kind: "detail-board", entries: [{ kind: "preset", variant: "eyes" }] }], fl, {}, "Kaia")
31
+ expect(r.missing[0]).toMatchObject({ assetType: "custom", attachToColumn: "detail_closeups", variant: "eyes" })
32
+ expect(r.missing[0].userPrompt).toContain("eyes")
33
+ })
34
+ it("custom entries carry their own prompt", () => {
35
+ const r = planSheetGeneration("character", [{ kind: "expression-board", entries: [{ kind: "custom", label: "Tired", prompt: "exhausted" }] }], fl, {}, "Kaia")
36
+ expect(r.missing[0]).toMatchObject({ assetType: "expressions", attachToColumn: "expressions", variant: "Tired", userPrompt: "exhausted" })
37
+ })
38
+ })
39
+
40
+ describe("resolveSheetSections", () => {
41
+ it("falls back to the default stack for (entityKind, type) when no sections are passed", () => {
42
+ // The canvas node / workflow / API callers send only `type`. Without this
43
+ // fallback the worker composes with zero bands → a blank sheet (the bug).
44
+ const r = resolveSheetSections("character", "full-reference")
45
+ expect(r.length).toBeGreaterThan(0)
46
+ expect(r.map((s) => s.kind)).toEqual(DEFAULT_SECTIONS.character["full-reference"].map((s) => s.kind))
47
+ })
48
+ it("uses the explicit section stack when provided (Studio tab path)", () => {
49
+ const r = resolveSheetSections("object", "full-reference", [{ kind: "header" }, { kind: "palette" }])
50
+ expect(r.map((s) => s.kind)).toEqual(["header", "palette"])
51
+ })
52
+ it("treats an empty sections array as 'not provided' and falls back to the default", () => {
53
+ expect(resolveSheetSections("location", "turnaround", []).length).toBeGreaterThan(0)
54
+ })
55
+ it("returns a fresh clone — mutating the result never mutates DEFAULT_SECTIONS", () => {
56
+ const before = DEFAULT_SECTIONS.character.turnaround.length
57
+ const r = resolveSheetSections("character", "turnaround")
58
+ r.push({ kind: "notes" })
59
+ ;(r[0] as { subtitle?: string }).subtitle = "mutated"
60
+ expect(DEFAULT_SECTIONS.character.turnaround.length).toBe(before)
61
+ expect(DEFAULT_SECTIONS.character.turnaround[0]).not.toHaveProperty("subtitle")
62
+ })
63
+ it("INVARIANT: every (entityKind, type) yields a non-empty default stack", () => {
64
+ // Root-cause guard: a type with no default stack would make a node sheet
65
+ // compose zero bands (blank). If a future edit empties one, this fails loudly.
66
+ for (const kind of Object.keys(DEFAULT_SECTIONS) as EntityKind[]) {
67
+ for (const type of SHEET_TYPES as readonly SheetType[]) {
68
+ expect(resolveSheetSections(kind, type).length, `${kind}/${type}`).toBeGreaterThan(0)
69
+ }
70
+ }
71
+ })
72
+ })
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { SHEET_PRESETS, PRESET_LABELS, ALA_CARTE_BOARDS, presetEntries } from "../presets.js"
3
+ import { planSheetPanels } from "../panel-plan.js"
4
+ import { BOARD_VARIANTS } from "../catalog.js"
5
+ import type { SheetFlavour } from "../types.js"
6
+
7
+ const fl: SheetFlavour = { outputFormat: "still", withText: true, showLabels: true, aspect: "landscape", background: "grey" }
8
+ const byId = (id: string) => SHEET_PRESETS.find((p) => p.id === id)!
9
+
10
+ describe("presetEntries", () => {
11
+ it("maps a variant list to ordered preset entries", () => {
12
+ expect(presetEntries(["front", "back"])).toEqual([
13
+ { kind: "preset", variant: "front" },
14
+ { kind: "preset", variant: "back" },
15
+ ])
16
+ })
17
+ })
18
+
19
+ describe("SHEET_PRESETS", () => {
20
+ it("studio-main = 7 curated head+body panels in order", () => {
21
+ const panels = planSheetPanels("character", byId("studio-main").baseSections, fl)
22
+ expect(panels.map((p) => p.variant)).toEqual([
23
+ "front", "left profile", "right profile", "back", // head
24
+ "front", "left profile", "back", // body
25
+ ])
26
+ })
27
+ it("studio-extended = 15 panels", () => {
28
+ expect(planSheetPanels("character", byId("studio-extended").baseSections, fl)).toHaveLength(15)
29
+ })
30
+ it("every preset variant exists in its board catalog", () => {
31
+ for (const p of SHEET_PRESETS) {
32
+ for (const s of p.baseSections) {
33
+ const board = s.kind === "head-turnaround" ? "headAngles" : "bodyAngles"
34
+ for (const e of s.entries ?? []) {
35
+ if (e.kind === "preset") expect(BOARD_VARIANTS.character[board]).toContain(e.variant)
36
+ }
37
+ }
38
+ }
39
+ })
40
+ it("each preset has a carrier type + a label", () => {
41
+ expect(byId("studio-main").type).toBe("turnaround")
42
+ expect(byId("studio-extended").type).toBe("full-reference")
43
+ expect(PRESET_LABELS["studio-main"]).toBe("Studio · Main")
44
+ expect(PRESET_LABELS["studio-extended"]).toBe("Studio · Extended")
45
+ })
46
+ })
47
+
48
+ describe("ALA_CARTE_BOARDS", () => {
49
+ it("are appendable sections in the expected order", () => {
50
+ expect(ALA_CARTE_BOARDS.map((b) => b.id)).toEqual(["expressions", "poses", "wardrobe", "detail", "palette"])
51
+ expect(ALA_CARTE_BOARDS[0].section.kind).toBe("expression-board")
52
+ expect(ALA_CARTE_BOARDS.find((b) => b.id === "palette")!.panelCount).toBe(0)
53
+ })
54
+ })
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ SHEET_TYPES, SHEET_SKINS, SHEET_ASPECTS, OUTPUT_FORMATS, SECTION_KINDS,
4
+ } from "../types.js"
5
+
6
+ describe("reference-sheet literal axes", () => {
7
+ it("exposes the four types", () => {
8
+ expect(SHEET_TYPES).toEqual(["turnaround", "variation-board", "detail", "full-reference"])
9
+ })
10
+ it("exposes the four skins", () => {
11
+ expect(SHEET_SKINS).toEqual(["studio", "cinematic", "blueprint", "illustrated"])
12
+ })
13
+ it("exposes aspects and output formats", () => {
14
+ expect(SHEET_ASPECTS).toEqual(["landscape", "square", "story"])
15
+ expect(OUTPUT_FORMATS).toEqual(["still", "motion"])
16
+ })
17
+ it("section kinds include the board, structural, and turnaround kinds", () => {
18
+ for (const k of ["header", "head-turnaround", "body-turnaround", "turnaround", "coverage",
19
+ "expression-board", "pose-board", "material-board", "variation-board", "environment-board",
20
+ "detail-board", "wardrobe-board", "palette", "scale", "notes"]) {
21
+ expect(SECTION_KINDS).toContain(k)
22
+ }
23
+ })
24
+ })
@@ -0,0 +1,144 @@
1
+ import {
2
+ CHARACTER_ASSET_VARIANTS, OBJECT_ASSET_VARIANTS, LOCATION_ASSET_VARIANTS,
3
+ } from "../entity-asset-types.js"
4
+ import type { EntityKind, SectionKind, SheetType, SheetSection } from "./types.js"
5
+
6
+ /** Starter presets for the NEW close-up bucket (per entity). Custom entries override. */
7
+ export const DETAIL_VARIANTS: Record<EntityKind, readonly string[]> = {
8
+ character: ["eyes", "hands", "hair detail"],
9
+ object: ["texture", "material detail", "key part"],
10
+ location: ["architectural detail", "material detail", "focal element"],
11
+ }
12
+
13
+ /** Starter presets for the NEW wardrobe bucket (character only). Custom entries override. */
14
+ export const WARDROBE_VARIANTS = ["base look", "alternate look", "sporty"] as const
15
+
16
+ /** Logical board → ordered canonical variants, per entity. Character/object/location
17
+ * pull from the SoT lists; detail/wardrobe are the new buckets. */
18
+ export const BOARD_VARIANTS: Record<EntityKind, Record<string, readonly string[]>> = {
19
+ character: {
20
+ headAngles: CHARACTER_ASSET_VARIANTS.headAngles,
21
+ bodyAngles: CHARACTER_ASSET_VARIANTS.bodyAngles,
22
+ expressions: CHARACTER_ASSET_VARIANTS.expressions,
23
+ poses: CHARACTER_ASSET_VARIANTS.poses,
24
+ lighting: CHARACTER_ASSET_VARIANTS.lighting,
25
+ detail: DETAIL_VARIANTS.character,
26
+ wardrobe: WARDROBE_VARIANTS,
27
+ },
28
+ object: {
29
+ angles: OBJECT_ASSET_VARIANTS.angles,
30
+ materials: OBJECT_ASSET_VARIANTS.materials,
31
+ variations: OBJECT_ASSET_VARIANTS.variations,
32
+ detail: DETAIL_VARIANTS.object,
33
+ },
34
+ location: {
35
+ angles: LOCATION_ASSET_VARIANTS.angles,
36
+ timeOfDay: LOCATION_ASSET_VARIANTS.timeOfDay,
37
+ weather: LOCATION_ASSET_VARIANTS.weather,
38
+ seasons: LOCATION_ASSET_VARIANTS.seasons,
39
+ lighting: LOCATION_ASSET_VARIANTS.lighting,
40
+ detail: DETAIL_VARIANTS.location,
41
+ },
42
+ }
43
+
44
+ /** Section kinds that NEVER produce panels (chrome/text bands). The single
45
+ * source of truth for "structural" — the planner uses this to decide whether a
46
+ * section with no resolvable board is intentionally panel-less (these) or a
47
+ * misconfiguration (e.g. `environment-board` with no explicit `board`, which
48
+ * must throw, not silently yield zero panels). */
49
+ export const STRUCTURAL_SECTIONS = new Set<SectionKind>(["header", "palette", "scale", "notes"])
50
+
51
+ /** Default board for each board-typed section kind. `null` = no default board.
52
+ * For the four structural kinds (`STRUCTURAL_SECTIONS`) that means "no panels".
53
+ * For `environment-board` it means "ambiguous (3 buckets) — the section MUST
54
+ * carry an explicit `board`" (the planner throws if it doesn't). */
55
+ export const SECTION_BOARD: Record<SectionKind, string | null> = {
56
+ header: null, palette: null, scale: null, notes: null,
57
+ "head-turnaround": "headAngles",
58
+ "body-turnaround": "bodyAngles",
59
+ turnaround: "angles",
60
+ coverage: "angles",
61
+ "expression-board": "expressions",
62
+ "pose-board": "poses",
63
+ "material-board": "materials",
64
+ "variation-board": "variations",
65
+ "environment-board": null, // requires explicit section.board (timeOfDay|weather|seasons)
66
+ "detail-board": "detail",
67
+ "wardrobe-board": "wardrobe",
68
+ }
69
+
70
+ /** Sections each entity supports (catalog gate for the config picker, Plan 06). */
71
+ export const ENTITY_SECTIONS: Record<EntityKind, readonly SectionKind[]> = {
72
+ character: ["header", "head-turnaround", "body-turnaround", "expression-board", "pose-board",
73
+ "detail-board", "wardrobe-board", "palette", "scale", "notes"],
74
+ object: ["header", "turnaround", "material-board", "variation-board", "detail-board",
75
+ "palette", "scale", "notes"],
76
+ location: ["header", "coverage", "environment-board", "detail-board", "palette", "scale", "notes"],
77
+ }
78
+
79
+ const sec = (kind: SectionKind, extra: Partial<SheetSection> = {}): SheetSection => ({ kind, ...extra })
80
+
81
+ /** Preset section stacks per (entity, type). `full-reference` is the rich poster
82
+ * (YUVAL order for character); other types are single-board presets. */
83
+ export const DEFAULT_SECTIONS: Record<EntityKind, Record<SheetType, readonly SheetSection[]>> = {
84
+ character: {
85
+ turnaround: [sec("header"), sec("head-turnaround")],
86
+ "variation-board": [sec("header"), sec("expression-board")],
87
+ detail: [sec("header"), sec("detail-board")],
88
+ "full-reference": [
89
+ sec("header"), sec("head-turnaround"), sec("expression-board"), sec("body-turnaround"),
90
+ sec("pose-board"), sec("detail-board"), sec("wardrobe-board"), sec("palette"), sec("notes"),
91
+ ],
92
+ },
93
+ object: {
94
+ turnaround: [sec("header"), sec("turnaround")],
95
+ "variation-board": [sec("header"), sec("material-board")],
96
+ detail: [sec("header"), sec("detail-board")],
97
+ "full-reference": [
98
+ sec("header"), sec("turnaround"), sec("material-board"), sec("variation-board"),
99
+ sec("detail-board"), sec("palette"), sec("notes"),
100
+ ],
101
+ },
102
+ location: {
103
+ turnaround: [sec("header"), sec("coverage")],
104
+ "variation-board": [sec("header"), sec("environment-board", { board: "timeOfDay" })],
105
+ detail: [sec("header"), sec("detail-board")],
106
+ "full-reference": [
107
+ sec("header"), sec("coverage"),
108
+ sec("environment-board", { board: "timeOfDay" }),
109
+ sec("environment-board", { board: "weather" }),
110
+ sec("detail-board"), sec("palette"), sec("notes"),
111
+ ],
112
+ },
113
+ }
114
+
115
+ /** Default number of panels drawn from a board when the section doesn't specify. */
116
+ export const DEFAULT_PANEL_COUNT = 4
117
+
118
+ /** Logical board key → real entity DB column (headAngles→angles,
119
+ * lighting→lighting_variations, timeOfDay→time_of_day, etc.). The worker uses
120
+ * this to find a panel's asset in the entity's JSONB buckets. */
121
+ export const BOARD_TO_COLUMN: Record<EntityKind, Record<string, string>> = {
122
+ character: { headAngles: "angles", bodyAngles: "body_angles", expressions: "expressions", poses: "poses", lighting: "lighting_variations", detail: "detail_closeups", wardrobe: "outfit_variations" },
123
+ object: { angles: "angles", materials: "materials", variations: "variations", detail: "detail_closeups" },
124
+ location: { angles: "angles", timeOfDay: "time_of_day", weather: "weather", seasons: "seasons", lighting: "lighting", detail: "detail_closeups" },
125
+ }
126
+
127
+ /** Flat per-entity DB column holding motion clips ({name,url} videos). Unlike
128
+ * still panels (per-board columns via BOARD_TO_COLUMN), all of an entity's motion
129
+ * clips live in ONE column, looked up by name===variant. Used by the motion
130
+ * reference sheet to find the clip for each planned panel. */
131
+ export const MOTION_COLUMN: Record<EntityKind, string> = {
132
+ character: "motions",
133
+ object: "motion_clips",
134
+ location: "atmosphere_motions",
135
+ }
136
+
137
+ /** Logical board → the `generate-*-asset` `assetType` to use. Standard boards map
138
+ * to their own assetType; detail/wardrobe have no dedicated assetType so they go
139
+ * through `custom` (with a built userPrompt). */
140
+ export const BOARD_TO_ASSET_TYPE: Record<EntityKind, Record<string, string>> = {
141
+ character: { headAngles: "headAngles", bodyAngles: "bodyAngles", expressions: "expressions", poses: "poses", lighting: "lighting", detail: "custom", wardrobe: "custom" },
142
+ object: { angles: "angles", materials: "materials", variations: "variations", detail: "custom" },
143
+ location: { angles: "angles", timeOfDay: "timeOfDay", weather: "weather", seasons: "seasons", lighting: "lighting", detail: "custom" },
144
+ }
@@ -0,0 +1,37 @@
1
+ import { planSheetGeneration, type PanelGenRequest } from "./plan-generation.js"
2
+ import type { EntityKind, SheetSection, SheetFlavour } from "./types.js"
3
+
4
+ export interface SheetCostEstimate {
5
+ present: number
6
+ missing: PanelGenRequest[]
7
+ prepareCost: number
8
+ assemblyCost: number
9
+ total: number
10
+ /** True when the plan exceeds MAX_PANELS_PER_SHEET (planSheetPanels throws). */
11
+ overflow: boolean
12
+ }
13
+
14
+ /**
15
+ * Pure cost estimate for a sheet plan. Wraps `planSheetGeneration` so a
16
+ * MAX_PANELS overflow (which throws in the planner) becomes `overflow:true`
17
+ * instead of crashing the caller's render. `perPanelCost`/`assemblyCost` are
18
+ * injected by the UI (constants from the adapter) so the shared layer stays
19
+ * pricing-agnostic.
20
+ */
21
+ export function estimateSheetCost(
22
+ entityKind: EntityKind,
23
+ sections: readonly SheetSection[],
24
+ flavour: SheetFlavour,
25
+ buckets: Record<string, ReadonlyArray<{ name?: string; url?: string }> | undefined>,
26
+ name: string,
27
+ perPanelCost: number,
28
+ assemblyCost: number,
29
+ ): SheetCostEstimate {
30
+ try {
31
+ const { presentUrls, missing } = planSheetGeneration(entityKind, sections, flavour, buckets, name)
32
+ const prepareCost = missing.length * perPanelCost
33
+ return { present: presentUrls.length, missing, prepareCost, assemblyCost, total: prepareCost + assemblyCost, overflow: false }
34
+ } catch {
35
+ return { present: 0, missing: [], prepareCost: 0, assemblyCost, total: assemblyCost, overflow: true }
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ import type { OutputFormat } from "./types.js"
2
+ /** The credit identifier for a sheet by output format (single source of truth —
3
+ * mirrors resolveCinematicCreditId). Used by the route guard, the orchestrator
4
+ * payload-builder, and the node's credit display. */
5
+ export function referenceSheetCreditId(flavour: { outputFormat?: OutputFormat | string } | undefined): string {
6
+ return flavour?.outputFormat === "motion" ? "reference-sheet:assembly-motion" : "reference-sheet:assembly"
7
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./types.js"
2
+ export * from "./catalog.js"
3
+ export * from "./panel-plan.js"
4
+ export * from "./panel-prompts.js"
5
+ export * from "./plan-generation.js"
6
+ export * from "./credit-id.js"
7
+ export * from "./presets.js"
8
+ export * from "./cost.js"
@@ -0,0 +1,65 @@
1
+ import type { EntityKind, PanelRequest, SheetFlavour, SheetSection } from "./types.js"
2
+ import { MAX_PANELS_PER_SHEET, MAX_CUSTOM_ENTRIES_PER_BOARD } from "./types.js"
3
+ import { BOARD_VARIANTS, SECTION_BOARD, STRUCTURAL_SECTIONS, DEFAULT_PANEL_COUNT } from "./catalog.js"
4
+
5
+ /** Resolve the logical board for a section, or null for structural sections.
6
+ * A non-structural board section that can't resolve a board (e.g.
7
+ * `environment-board` with no explicit `section.board`, or a board not valid
8
+ * for this entity) throws — turning misconfiguration into a free rejection
9
+ * instead of silently emitting zero panels. */
10
+ function resolveBoard(entityKind: EntityKind, section: SheetSection): string | null {
11
+ if (STRUCTURAL_SECTIONS.has(section.kind)) return null
12
+ const board = section.board ?? SECTION_BOARD[section.kind]
13
+ if (!board || !BOARD_VARIANTS[entityKind][board]) {
14
+ throw new Error(
15
+ `Section "${section.kind}" has no resolvable board for ${entityKind} (board=${board ?? "?"}) — ` +
16
+ `board sections like environment-board require an explicit section.board valid for the entity`,
17
+ )
18
+ }
19
+ return board
20
+ }
21
+
22
+ /**
23
+ * Pure: given an entity kind, an ordered section list, and the flavour, return the
24
+ * ordered panels the sheet needs. Reuse-vs-generate and pricing happen later
25
+ * (Plan 04) — this only says WHAT panels must exist. Throws on bad config so the
26
+ * route can 400 before any paid work.
27
+ */
28
+ export function planSheetPanels(
29
+ entityKind: EntityKind,
30
+ sections: readonly SheetSection[],
31
+ _flavour: SheetFlavour,
32
+ ): PanelRequest[] {
33
+ const panels: PanelRequest[] = []
34
+ for (const section of sections) {
35
+ const board = resolveBoard(entityKind, section)
36
+ if (board === null) continue // structural section, no panels
37
+
38
+ const customs = (section.entries ?? []).filter((e) => e.kind === "custom")
39
+ if (customs.length > MAX_CUSTOM_ENTRIES_PER_BOARD) {
40
+ throw new Error(`Too many custom entries on ${section.kind} (${customs.length} > ${MAX_CUSTOM_ENTRIES_PER_BOARD})`)
41
+ }
42
+
43
+ if (section.entries && section.entries.length > 0) {
44
+ for (const e of section.entries) {
45
+ if (e.kind === "custom") {
46
+ panels.push({ section: section.kind, board, variant: e.label, label: e.label, custom: true, prompt: e.prompt })
47
+ } else {
48
+ panels.push({ section: section.kind, board, variant: e.variant, label: e.variant, custom: false })
49
+ }
50
+ }
51
+ } else {
52
+ const available = BOARD_VARIANTS[entityKind][board]
53
+ // Clamp both ends: a negative panelCount must not become a negative slice
54
+ // bound (which would silently drop panels from the end of the board).
55
+ const count = Math.max(0, Math.min(section.panelCount ?? DEFAULT_PANEL_COUNT, available.length))
56
+ for (const variant of available.slice(0, count)) {
57
+ panels.push({ section: section.kind, board, variant, label: variant, custom: false })
58
+ }
59
+ }
60
+ }
61
+ if (panels.length > MAX_PANELS_PER_SHEET) {
62
+ throw new Error(`Sheet plan has too many panels (${panels.length} > MAX_PANELS_PER_SHEET=${MAX_PANELS_PER_SHEET})`)
63
+ }
64
+ return panels
65
+ }
@@ -0,0 +1,14 @@
1
+ import type { EntityKind } from "./types.js"
2
+
3
+ /** Build the `userPrompt` for a detail/wardrobe panel generated via the
4
+ * `custom` assetType (these boards have no dedicated backend assetType). */
5
+ export function buildPanelPrompt(entityKind: EntityKind, board: string, variant: string, name: string): string {
6
+ if (board === "detail") {
7
+ return `Extreme close-up macro detail of ${name}'s ${variant}, sharp focus on the ${variant}, neutral background, no text.`
8
+ }
9
+ if (board === "wardrobe") {
10
+ return `Full-body view of ${name} wearing the "${variant}" outfit, consistent character, neutral studio background, no text.`
11
+ }
12
+ // Generic passthrough for any other custom board.
13
+ return `${name}, ${variant}. Consistent character, neutral background, no text.`
14
+ }
@@ -0,0 +1,79 @@
1
+ import { planSheetPanels } from "./panel-plan.js"
2
+ import { BOARD_TO_COLUMN, BOARD_TO_ASSET_TYPE, DEFAULT_SECTIONS } from "./catalog.js"
3
+ import { buildPanelPrompt } from "./panel-prompts.js"
4
+ import type { EntityKind, SheetSection, SheetFlavour, SheetType } from "./types.js"
5
+
6
+ /**
7
+ * The section stack to actually compose for a sheet. The Studio "Sheet" tab
8
+ * sends an explicit, user-chosen stack in `flavour.sections`; the canvas node,
9
+ * workflow runs, and API/MCP callers send only `type`, so fall back to the
10
+ * default stack for (entityKind, type). Always returns a fresh, mutable clone so
11
+ * neither the caller nor the shared `DEFAULT_SECTIONS` constant is mutated.
12
+ *
13
+ * Single source of truth for "type → sections" — without this fallback a node
14
+ * sheet composes with zero bands (a blank image). Every (entityKind, type) has a
15
+ * non-empty default stack (guarded by the catalog invariant test).
16
+ */
17
+ export function resolveSheetSections(
18
+ entityKind: EntityKind,
19
+ type: SheetType,
20
+ sections?: readonly SheetSection[],
21
+ ): SheetSection[] {
22
+ const base = sections?.length ? sections : (DEFAULT_SECTIONS[entityKind][type] ?? [])
23
+ return base.map((s) => ({ ...s }))
24
+ }
25
+
26
+ /** A `generate-*-asset` request for one missing panel. */
27
+ export interface PanelGenRequest {
28
+ assetType: string // e.g. "headAngles" | "expressions" | "custom"
29
+ variant: string // preset variant or custom label (also the attachName)
30
+ attachToColumn: string // DB column (snake_case) the worker appends to
31
+ attachName: string // == variant
32
+ userPrompt?: string // present for custom assetType / custom entries
33
+ }
34
+
35
+ export interface SheetGenerationPlan {
36
+ presentUrls: string[]
37
+ missing: PanelGenRequest[]
38
+ }
39
+
40
+ /** Find the bucket item whose `name` equals `variant` and which carries a
41
+ * non-empty `url` (single source of truth — the same predicate the still + motion
42
+ * panel resolvers use to locate a planned panel in an entity's buckets). */
43
+ export function matchVariant(
44
+ items: ReadonlyArray<{ name?: string; url?: string }>,
45
+ variant: string,
46
+ ): { name?: string; url?: string } | undefined {
47
+ return items.find((it) => it?.name === variant && typeof it?.url === "string" && it.url.length > 0)
48
+ }
49
+
50
+ /** Pure: given the entity's current buckets (keyed by DB column), split the
51
+ * planned panels into already-present URLs and the generate-requests needed for
52
+ * the rest. The surface fires each request, awaits, then calls the compose API. */
53
+ export function planSheetGeneration(
54
+ entityKind: EntityKind,
55
+ sections: readonly SheetSection[],
56
+ flavour: SheetFlavour,
57
+ bucketsByColumn: Record<string, ReadonlyArray<{ name?: string; url?: string }> | undefined>,
58
+ name: string,
59
+ ): SheetGenerationPlan {
60
+ const plan = planSheetPanels(entityKind, sections, flavour)
61
+ const presentUrls: string[] = []
62
+ const missing: PanelGenRequest[] = []
63
+ for (const p of plan) {
64
+ const column = BOARD_TO_COLUMN[entityKind][p.board]
65
+ const items = (column ? bucketsByColumn[column] : undefined) ?? []
66
+ const match = matchVariant(items, p.variant)
67
+ if (match?.url) {
68
+ presentUrls.push(match.url)
69
+ continue
70
+ }
71
+ const assetType = BOARD_TO_ASSET_TYPE[entityKind][p.board] ?? "custom"
72
+ const userPrompt =
73
+ p.custom ? p.prompt :
74
+ assetType === "custom" ? buildPanelPrompt(entityKind, p.board, p.variant, name) :
75
+ undefined
76
+ missing.push({ assetType, variant: p.variant, attachToColumn: column, attachName: p.variant, userPrompt })
77
+ }
78
+ return { presentUrls, missing }
79
+ }