@scenario-labs/sdk 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/api-promise.d.mts +1 -0
  3. package/api-promise.d.mts.map +1 -1
  4. package/api-promise.d.ts +1 -0
  5. package/api-promise.d.ts.map +1 -1
  6. package/api-promise.mjs +1 -0
  7. package/api-promise.mjs.map +1 -1
  8. package/client.d.mts.map +1 -1
  9. package/client.d.ts.map +1 -1
  10. package/client.mjs.map +1 -1
  11. package/core/api-promise.d.mts.map +1 -1
  12. package/core/api-promise.d.ts.map +1 -1
  13. package/core/api-promise.mjs.map +1 -1
  14. package/core/error.mjs.map +1 -1
  15. package/core/pagination.d.mts.map +1 -1
  16. package/core/pagination.d.ts.map +1 -1
  17. package/core/pagination.mjs.map +1 -1
  18. package/core/resource.d.mts.map +1 -1
  19. package/core/resource.d.ts.map +1 -1
  20. package/core/uploads.d.mts.map +1 -1
  21. package/core/uploads.d.ts.map +1 -1
  22. package/core/uploads.mjs.map +1 -1
  23. package/error.d.mts +1 -0
  24. package/error.d.mts.map +1 -1
  25. package/error.d.ts +1 -0
  26. package/error.d.ts.map +1 -1
  27. package/error.mjs +1 -0
  28. package/error.mjs.map +1 -1
  29. package/index.d.mts +1 -1
  30. package/index.d.mts.map +1 -1
  31. package/index.d.ts +1 -1
  32. package/index.d.ts.map +1 -1
  33. package/index.js +6 -1
  34. package/index.js.map +1 -1
  35. package/index.mjs +1 -1
  36. package/index.mjs.map +1 -1
  37. package/internal/detect-platform.mjs.map +1 -1
  38. package/internal/headers.mjs.map +1 -1
  39. package/internal/parse.d.mts.map +1 -1
  40. package/internal/parse.d.ts.map +1 -1
  41. package/internal/parse.mjs.map +1 -1
  42. package/internal/qs/formats.d.mts.map +1 -1
  43. package/internal/qs/formats.d.ts.map +1 -1
  44. package/internal/qs/index.d.mts +2 -2
  45. package/internal/qs/index.d.mts.map +1 -1
  46. package/internal/qs/index.d.ts +2 -2
  47. package/internal/qs/index.d.ts.map +1 -1
  48. package/internal/qs/index.mjs.map +1 -1
  49. package/internal/qs/stringify.d.mts.map +1 -1
  50. package/internal/qs/stringify.d.ts.map +1 -1
  51. package/internal/qs/stringify.mjs.map +1 -1
  52. package/internal/qs/utils.d.mts.map +1 -1
  53. package/internal/qs/utils.d.ts.map +1 -1
  54. package/internal/qs/utils.mjs.map +1 -1
  55. package/internal/request-options.d.mts.map +1 -1
  56. package/internal/request-options.d.ts.map +1 -1
  57. package/internal/shims.d.mts +6 -0
  58. package/internal/shims.d.mts.map +1 -1
  59. package/internal/shims.d.ts +6 -0
  60. package/internal/shims.d.ts.map +1 -1
  61. package/internal/to-file.d.mts.map +1 -1
  62. package/internal/to-file.d.ts.map +1 -1
  63. package/internal/to-file.mjs.map +1 -1
  64. package/internal/types.d.mts +4 -4
  65. package/internal/types.d.mts.map +1 -1
  66. package/internal/types.d.ts +4 -4
  67. package/internal/types.d.ts.map +1 -1
  68. package/internal/uploads.d.mts.map +1 -1
  69. package/internal/uploads.d.ts.map +1 -1
  70. package/internal/uploads.mjs.map +1 -1
  71. package/internal/utils/base64.mjs.map +1 -1
  72. package/internal/utils/log.d.mts.map +1 -1
  73. package/internal/utils/log.d.ts.map +1 -1
  74. package/internal/utils/log.mjs.map +1 -1
  75. package/internal/utils/path.mjs.map +1 -1
  76. package/internal/utils/query.mjs.map +1 -1
  77. package/internal/utils/values.mjs.map +1 -1
  78. package/internal/utils.d.mts.map +1 -1
  79. package/internal/utils.d.ts.map +1 -1
  80. package/internal/utils.mjs.map +1 -1
  81. package/lib/asset.d.mts.map +1 -1
  82. package/lib/asset.d.ts.map +1 -1
  83. package/lib/asset.mjs.map +1 -1
  84. package/lib/generate.d.mts.map +1 -1
  85. package/lib/generate.d.ts.map +1 -1
  86. package/lib/generate.mjs.map +1 -1
  87. package/lib/job.d.mts.map +1 -1
  88. package/lib/job.d.ts.map +1 -1
  89. package/lib/job.mjs.map +1 -1
  90. package/lib/models.d.mts.map +1 -1
  91. package/lib/models.d.ts.map +1 -1
  92. package/lib/models.mjs.map +1 -1
  93. package/lib/scenario.d.mts.map +1 -1
  94. package/lib/scenario.d.ts.map +1 -1
  95. package/lib/scenario.mjs.map +1 -1
  96. package/lib/upload.d.mts.map +1 -1
  97. package/lib/upload.d.ts.map +1 -1
  98. package/lib/upload.mjs.map +1 -1
  99. package/lib/workflow.d.mts.map +1 -1
  100. package/lib/workflow.d.ts.map +1 -1
  101. package/lib/workflow.mjs.map +1 -1
  102. package/lib/workflow_converter.d.mts +58 -2
  103. package/lib/workflow_converter.d.mts.map +1 -1
  104. package/lib/workflow_converter.d.ts +58 -2
  105. package/lib/workflow_converter.d.ts.map +1 -1
  106. package/lib/workflow_converter.js +350 -35
  107. package/lib/workflow_converter.js.map +1 -1
  108. package/lib/workflow_converter.mjs +345 -35
  109. package/lib/workflow_converter.mjs.map +1 -1
  110. package/lib/workflow_import_validator.d.mts +1 -1
  111. package/lib/workflow_import_validator.d.mts.map +1 -1
  112. package/lib/workflow_import_validator.d.ts +1 -1
  113. package/lib/workflow_import_validator.d.ts.map +1 -1
  114. package/lib/workflow_import_validator.js +1 -0
  115. package/lib/workflow_import_validator.js.map +1 -1
  116. package/lib/workflow_import_validator.mjs +1 -0
  117. package/lib/workflow_import_validator.mjs.map +1 -1
  118. package/package.json +1 -1
  119. package/pagination.d.mts +1 -0
  120. package/pagination.d.mts.map +1 -1
  121. package/pagination.d.ts +1 -0
  122. package/pagination.d.ts.map +1 -1
  123. package/pagination.mjs +1 -0
  124. package/pagination.mjs.map +1 -1
  125. package/resource.d.mts +1 -0
  126. package/resource.d.mts.map +1 -1
  127. package/resource.d.ts +1 -0
  128. package/resource.d.ts.map +1 -1
  129. package/resource.mjs +1 -0
  130. package/resource.mjs.map +1 -1
  131. package/resources/assets/assets.d.mts +289 -135
  132. package/resources/assets/assets.d.mts.map +1 -1
  133. package/resources/assets/assets.d.ts +289 -135
  134. package/resources/assets/assets.d.ts.map +1 -1
  135. package/resources/assets/assets.js.map +1 -1
  136. package/resources/assets/assets.mjs.map +1 -1
  137. package/resources/assets/download.d.mts.map +1 -1
  138. package/resources/assets/download.d.ts.map +1 -1
  139. package/resources/assets/download.mjs.map +1 -1
  140. package/resources/assets/index.d.mts.map +1 -1
  141. package/resources/assets/index.d.ts.map +1 -1
  142. package/resources/assets/index.mjs.map +1 -1
  143. package/resources/assets.d.mts.map +1 -1
  144. package/resources/assets.d.ts.map +1 -1
  145. package/resources/assets.mjs.map +1 -1
  146. package/resources/collections/assets.d.mts.map +1 -1
  147. package/resources/collections/assets.d.ts.map +1 -1
  148. package/resources/collections/assets.mjs.map +1 -1
  149. package/resources/collections/collections.d.mts.map +1 -1
  150. package/resources/collections/collections.d.ts.map +1 -1
  151. package/resources/collections/collections.mjs.map +1 -1
  152. package/resources/collections/index.d.mts.map +1 -1
  153. package/resources/collections/index.d.ts.map +1 -1
  154. package/resources/collections/index.mjs.map +1 -1
  155. package/resources/collections/models.d.mts.map +1 -1
  156. package/resources/collections/models.d.ts.map +1 -1
  157. package/resources/collections/models.mjs.map +1 -1
  158. package/resources/collections.d.mts.map +1 -1
  159. package/resources/collections.d.ts.map +1 -1
  160. package/resources/collections.mjs.map +1 -1
  161. package/resources/generate.d.mts +86 -48
  162. package/resources/generate.d.mts.map +1 -1
  163. package/resources/generate.d.ts +86 -48
  164. package/resources/generate.d.ts.map +1 -1
  165. package/resources/generate.mjs.map +1 -1
  166. package/resources/index.d.mts.map +1 -1
  167. package/resources/index.d.ts.map +1 -1
  168. package/resources/index.mjs.map +1 -1
  169. package/resources/jobs.d.mts +6 -6
  170. package/resources/jobs.d.mts.map +1 -1
  171. package/resources/jobs.d.ts +6 -6
  172. package/resources/jobs.d.ts.map +1 -1
  173. package/resources/jobs.mjs.map +1 -1
  174. package/resources/models/description.d.mts +54 -4
  175. package/resources/models/description.d.mts.map +1 -1
  176. package/resources/models/description.d.ts +54 -4
  177. package/resources/models/description.d.ts.map +1 -1
  178. package/resources/models/description.mjs.map +1 -1
  179. package/resources/models/examples.d.mts +74 -36
  180. package/resources/models/examples.d.mts.map +1 -1
  181. package/resources/models/examples.d.ts +74 -36
  182. package/resources/models/examples.d.ts.map +1 -1
  183. package/resources/models/examples.mjs.map +1 -1
  184. package/resources/models/index.d.mts.map +1 -1
  185. package/resources/models/index.d.ts.map +1 -1
  186. package/resources/models/index.mjs.map +1 -1
  187. package/resources/models/models.d.mts +21 -21
  188. package/resources/models/models.d.mts.map +1 -1
  189. package/resources/models/models.d.ts +21 -21
  190. package/resources/models/models.d.ts.map +1 -1
  191. package/resources/models/models.js.map +1 -1
  192. package/resources/models/models.mjs.map +1 -1
  193. package/resources/models/train.d.mts +8 -8
  194. package/resources/models/train.d.mts.map +1 -1
  195. package/resources/models/train.d.ts +8 -8
  196. package/resources/models/train.d.ts.map +1 -1
  197. package/resources/models/train.mjs.map +1 -1
  198. package/resources/models/training-images.d.mts.map +1 -1
  199. package/resources/models/training-images.d.ts.map +1 -1
  200. package/resources/models/training-images.mjs.map +1 -1
  201. package/resources/models.d.mts.map +1 -1
  202. package/resources/models.d.ts.map +1 -1
  203. package/resources/models.mjs.map +1 -1
  204. package/resources/oscu.d.mts.map +1 -1
  205. package/resources/oscu.d.ts.map +1 -1
  206. package/resources/oscu.mjs.map +1 -1
  207. package/resources/pricing/index.d.mts.map +1 -1
  208. package/resources/pricing/index.d.ts.map +1 -1
  209. package/resources/pricing/index.mjs.map +1 -1
  210. package/resources/pricing/oscu.d.mts.map +1 -1
  211. package/resources/pricing/oscu.d.ts.map +1 -1
  212. package/resources/pricing/oscu.mjs.map +1 -1
  213. package/resources/pricing/pricing.d.mts.map +1 -1
  214. package/resources/pricing/pricing.d.ts.map +1 -1
  215. package/resources/pricing/pricing.mjs.map +1 -1
  216. package/resources/pricing.d.mts.map +1 -1
  217. package/resources/pricing.d.ts.map +1 -1
  218. package/resources/pricing.mjs.map +1 -1
  219. package/resources/search.d.mts +6 -0
  220. package/resources/search.d.mts.map +1 -1
  221. package/resources/search.d.ts +6 -0
  222. package/resources/search.d.ts.map +1 -1
  223. package/resources/search.mjs.map +1 -1
  224. package/resources/tags.d.mts.map +1 -1
  225. package/resources/tags.d.ts.map +1 -1
  226. package/resources/tags.mjs.map +1 -1
  227. package/resources/uploads.d.mts +4 -4
  228. package/resources/uploads.d.mts.map +1 -1
  229. package/resources/uploads.d.ts +4 -4
  230. package/resources/uploads.d.ts.map +1 -1
  231. package/resources/uploads.mjs.map +1 -1
  232. package/resources/usages.d.mts +31 -6
  233. package/resources/usages.d.mts.map +1 -1
  234. package/resources/usages.d.ts +31 -6
  235. package/resources/usages.d.ts.map +1 -1
  236. package/resources/usages.mjs.map +1 -1
  237. package/resources/workflows.d.mts +36 -36
  238. package/resources/workflows.d.mts.map +1 -1
  239. package/resources/workflows.d.ts +36 -36
  240. package/resources/workflows.d.ts.map +1 -1
  241. package/resources/workflows.mjs.map +1 -1
  242. package/resources.d.mts.map +1 -1
  243. package/resources.d.ts.map +1 -1
  244. package/resources.mjs.map +1 -1
  245. package/src/index.ts +9 -0
  246. package/src/lib/workflow_converter.ts +487 -28
  247. package/src/lib/workflow_import_validator.ts +2 -0
  248. package/src/resources/assets/assets.ts +376 -118
  249. package/src/resources/generate.ts +106 -44
  250. package/src/resources/jobs.ts +6 -6
  251. package/src/resources/models/description.ts +68 -2
  252. package/src/resources/models/examples.ts +94 -32
  253. package/src/resources/models/models.ts +21 -14
  254. package/src/resources/models/train.ts +8 -6
  255. package/src/resources/search.ts +7 -0
  256. package/src/resources/uploads.ts +4 -4
  257. package/src/resources/usages.ts +41 -4
  258. package/src/resources/workflows.ts +36 -36
  259. package/src/version.ts +1 -1
  260. package/uploads.d.mts +1 -0
  261. package/uploads.d.mts.map +1 -1
  262. package/uploads.d.ts +1 -0
  263. package/uploads.d.ts.map +1 -1
  264. package/uploads.mjs +1 -0
  265. package/uploads.mjs.map +1 -1
  266. package/version.d.mts +1 -1
  267. package/version.d.ts +1 -1
  268. package/version.js +1 -1
  269. package/version.mjs +1 -1
@@ -47,6 +47,7 @@ export type WorkflowEditorNode =
47
47
  }
48
48
  | { id: string; type: 'transformText'; data: NodeCommonData & { value?: string } }
49
49
  | { id: string; type: 'splitText'; data: NodeCommonData & { splitDelimiter?: string } }
50
+ | { id: string; type: 'aspectRatio'; data: NodeCommonData & { output?: string } }
50
51
  | { id: string; type: 'groupItems'; data: NodeCommonData & { value?: string } }
51
52
  | {
52
53
  id: string;
@@ -60,6 +61,7 @@ export type WorkflowEditorNode =
60
61
  type: 'ifElse';
61
62
  data: NodeCommonData & { conditionBlocks?: WorkflowEditorConditionBlock[] };
62
63
  }
64
+ | { id: string; type: 'approval'; data: NodeCommonData & { message?: string } }
63
65
  | { id: string; type: string; data: NodeCommonData };
64
66
 
65
67
  export type WorkflowEditorEdge = {
@@ -67,6 +69,7 @@ export type WorkflowEditorEdge = {
67
69
  target: string;
68
70
  sourceHandle?: string | null;
69
71
  targetHandle?: string | null;
72
+ data?: { isForEachEndEdge?: boolean } | undefined;
70
73
  };
71
74
 
72
75
  export type WorkflowEditorCondition = {
@@ -84,12 +87,55 @@ export type WorkflowEditorModelInput = {
84
87
  name: string;
85
88
  type: string;
86
89
  inputs?: Array<{ name: string; type: string }>;
90
+ min?: number;
91
+ max?: number;
92
+ step?: number;
93
+ allowedValues?: string[];
94
+ };
95
+
96
+ export type WorkflowEditorResolutionPreset = {
97
+ width?: number;
98
+ height?: number;
87
99
  };
88
100
 
89
101
  export type WorkflowEditorModel = {
90
102
  id?: string;
91
103
  inputs?: WorkflowEditorModelInput[];
92
104
  tags?: string[];
105
+ uiConfig?: {
106
+ resolutionComponent?: {
107
+ widthInput?: string;
108
+ heightInput?: string;
109
+ presets?: WorkflowEditorResolutionPreset[];
110
+ };
111
+ };
112
+ };
113
+
114
+ const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
115
+ export const ASPECT_RATIO_PRESETS = [
116
+ '16:9',
117
+ '3:2',
118
+ '4:3',
119
+ '5:4',
120
+ '1:1',
121
+ '4:5',
122
+ '3:4',
123
+ '2:3',
124
+ '9:16',
125
+ ] as const;
126
+
127
+ export type AspectRatioBounds = {
128
+ minWidth: number;
129
+ maxWidth: number;
130
+ minHeight: number;
131
+ maxHeight: number;
132
+ multipleOf: number;
133
+ };
134
+
135
+ export type AspectRatioModelInputNames = {
136
+ widthInputName?: string | undefined;
137
+ heightInputName?: string | undefined;
138
+ aspectRatioInputName?: string | undefined;
93
139
  };
94
140
 
95
141
  export type WorkflowEditorFlowInputRef = {
@@ -122,6 +168,8 @@ export type WorkflowEditorFlowItem = {
122
168
  logicType?: 'if-else' | undefined;
123
169
  loopBodyNodeIds?: string[] | undefined;
124
170
  includeOutputsInWorkflowJob?: true | undefined;
171
+ dependsOn?: string[] | undefined;
172
+ label?: string | undefined;
125
173
  };
126
174
 
127
175
  // ---- Internal types ----
@@ -172,6 +220,119 @@ function getConditionalInputHandleId(nodeId: string): string {
172
220
  return `${nodeId}-source-conditional`;
173
221
  }
174
222
 
223
+ function handleTypeIncludes(handleType: string | string[] | undefined, value: string): boolean {
224
+ if (Array.isArray(handleType)) return handleType.includes(value);
225
+ return handleType === value;
226
+ }
227
+
228
+ type ParsedAspectRatio = { width: number; height: number };
229
+
230
+ function parseAspectRatio(aspectRatio: string | undefined): ParsedAspectRatio | undefined {
231
+ if (!aspectRatio) return undefined;
232
+ const [rawWidth, rawHeight] = aspectRatio.split(':');
233
+ const width = Number(rawWidth);
234
+ const height = Number(rawHeight);
235
+ if (
236
+ !Number.isFinite(width) ||
237
+ !Number.isFinite(height) ||
238
+ width <= 0 ||
239
+ height <= 0 ||
240
+ !Number.isInteger(width) ||
241
+ !Number.isInteger(height)
242
+ ) {
243
+ return undefined;
244
+ }
245
+ return { width, height };
246
+ }
247
+
248
+ function getGreatestCommonDivisor(a: number, b: number): number {
249
+ let x = Math.abs(Math.trunc(a));
250
+ let y = Math.abs(Math.trunc(b));
251
+ while (y) {
252
+ const temp = y;
253
+ y = x % y;
254
+ x = temp;
255
+ }
256
+ return x || 1;
257
+ }
258
+
259
+ function makeNumberAMultipleOfX(value: number, multipleOf: number): number {
260
+ if (multipleOf <= 0) return Math.round(value);
261
+ return Math.round(value / multipleOf) * multipleOf;
262
+ }
263
+
264
+ function clamp(value: number, min: number, max: number): number {
265
+ return Math.min(Math.max(value, min), max);
266
+ }
267
+
268
+ export function normalizeAspectRatio(aspectRatio: string | undefined): string | undefined {
269
+ const parsed = parseAspectRatio(aspectRatio);
270
+ if (!parsed) return undefined;
271
+ const divisor = getGreatestCommonDivisor(parsed.width, parsed.height);
272
+ return `${parsed.width / divisor}:${parsed.height / divisor}`;
273
+ }
274
+
275
+ export function getAspectRatioModelInputNames(
276
+ model: WorkflowEditorModel | undefined,
277
+ ): AspectRatioModelInputNames {
278
+ if (!model) return {};
279
+
280
+ const widthInputName =
281
+ model.uiConfig?.resolutionComponent?.widthInput ??
282
+ model.inputs?.find((input) => input.name === 'width')?.name;
283
+ const heightInputName =
284
+ model.uiConfig?.resolutionComponent?.heightInput ??
285
+ model.inputs?.find((input) => input.name === 'height')?.name;
286
+ const aspectRatioInputName = model.inputs?.find((input) => input.name === 'aspectRatio')?.name;
287
+
288
+ return { widthInputName, heightInputName, aspectRatioInputName };
289
+ }
290
+
291
+ export function getAspectRatioBounds(
292
+ model: WorkflowEditorModel | undefined,
293
+ { widthInputName, heightInputName }: Pick<AspectRatioModelInputNames, 'widthInputName' | 'heightInputName'>,
294
+ ): AspectRatioBounds | undefined {
295
+ if (!model || !widthInputName || !heightInputName) return undefined;
296
+
297
+ const widthInput = model.inputs?.find((input) => input.name === widthInputName);
298
+ const heightInput = model.inputs?.find((input) => input.name === heightInputName);
299
+ const presets = model.uiConfig?.resolutionComponent?.presets;
300
+ const widthPresets = (presets ?? [])
301
+ .map((preset) => preset.width)
302
+ .filter((value): value is number => typeof value === 'number');
303
+ const heightPresets = (presets ?? [])
304
+ .map((preset) => preset.height)
305
+ .filter((value): value is number => typeof value === 'number');
306
+ const multipleOf = widthInput?.step ?? 8;
307
+
308
+ return {
309
+ maxWidth: widthInput?.max ?? (widthPresets.length > 0 ? Math.max(...widthPresets) : 2048),
310
+ minWidth: widthInput?.min ?? (widthPresets.length > 0 ? Math.min(...widthPresets) : 64),
311
+ maxHeight: heightInput?.max ?? (heightPresets.length > 0 ? Math.max(...heightPresets) : 2048),
312
+ minHeight: heightInput?.min ?? (heightPresets.length > 0 ? Math.min(...heightPresets) : 64),
313
+ multipleOf,
314
+ };
315
+ }
316
+
317
+ export function getDimensionsFromAspectRatio(args: {
318
+ aspectRatio: string;
319
+ bounds: AspectRatioBounds;
320
+ }): { width: number; height: number } | undefined {
321
+ const parsed = parseAspectRatio(args.aspectRatio);
322
+ if (!parsed) return undefined;
323
+
324
+ const ratio = parsed.width / parsed.height;
325
+ let width = ratio >= 1 ? args.bounds.maxWidth : Math.round(args.bounds.maxHeight * ratio);
326
+ let height = ratio >= 1 ? Math.round(args.bounds.maxWidth / ratio) : args.bounds.maxHeight;
327
+
328
+ width = makeNumberAMultipleOfX(width, args.bounds.multipleOf);
329
+ height = makeNumberAMultipleOfX(height, args.bounds.multipleOf);
330
+ width = clamp(width, args.bounds.minWidth, args.bounds.maxWidth);
331
+ height = clamp(height, args.bounds.minHeight, args.bounds.maxHeight);
332
+
333
+ return { width, height };
334
+ }
335
+
175
336
  function getForEachFlowListInputDescriptor(args: {
176
337
  listProvider: WorkflowEditorNode | undefined;
177
338
  outputHandle: WorkflowEditorHandleOutput | undefined;
@@ -444,14 +605,14 @@ function toStaticInput(name: string, type: string, value: unknown): WorkflowEdit
444
605
  return { name, type, value: value as string | number | string[] | boolean };
445
606
  }
446
607
 
447
- function isFormArray(value: unknown): value is Record<string, unknown>[] {
608
+ function getIsFormArray(value: unknown): value is Record<string, unknown>[] {
448
609
  return (
449
610
  Array.isArray(value) &&
450
611
  (value as unknown[]).every((item) => typeof item === 'object' && item !== null && !Array.isArray(item))
451
612
  );
452
613
  }
453
614
 
454
- function isModelCompose(model: WorkflowEditorModel | undefined): boolean {
615
+ function getIsModelCompose(model: WorkflowEditorModel | undefined): boolean {
455
616
  return !!model?.id && (model.tags ?? []).includes('compose');
456
617
  }
457
618
 
@@ -469,6 +630,120 @@ function getIsConditionallyExecuted(
469
630
  });
470
631
  }
471
632
 
633
+ function buildDimensionLookupCEL(args: {
634
+ variable: string;
635
+ presets: Array<{ aspectRatio: string; width: number; height: number }>;
636
+ dimension: 'width' | 'height';
637
+ fallback: number;
638
+ }): string {
639
+ let expression = String(args.fallback);
640
+ for (let i = args.presets.length - 1; i >= 0; i--) {
641
+ const preset = args.presets[i]!;
642
+ expression = `trim(${args.variable}) == '${preset.aspectRatio}' ? ${
643
+ preset[args.dimension]
644
+ } : ${expression}`;
645
+ }
646
+ return expression;
647
+ }
648
+
649
+ function applyAspectRatioInputs(args: {
650
+ flow: WorkflowEditorFlowItem[];
651
+ flowInputs: WorkflowEditorFlowInput[];
652
+ flowId: string;
653
+ aspectRatioRef: WorkflowEditorFlowInputRef;
654
+ model: WorkflowEditorModel;
655
+ }): void {
656
+ const modelInputNames = getAspectRatioModelInputNames(args.model);
657
+ const modelBounds = getAspectRatioBounds(args.model, modelInputNames);
658
+ const { widthInputName, heightInputName, aspectRatioInputName } = modelInputNames;
659
+
660
+ if (aspectRatioInputName) {
661
+ const transformId = `${args.flowId}_aspectRatio_value`;
662
+ args.flow.push({
663
+ id: transformId,
664
+ type: 'transform',
665
+ logic: { transform: 'trim(aspectRatio)' },
666
+ inputs: [{ name: 'aspectRatio', type: 'string', ref: args.aspectRatioRef }],
667
+ });
668
+
669
+ setOrPushFlowInput(
670
+ args.flowInputs,
671
+ args.flowInputs.findIndex((inp) => inp.name === aspectRatioInputName),
672
+ {
673
+ name: aspectRatioInputName,
674
+ type: 'string',
675
+ ref: { node: transformId, name: 'all' },
676
+ },
677
+ );
678
+ }
679
+
680
+ if (widthInputName && heightInputName && modelBounds) {
681
+ const presetDimensions = ASPECT_RATIO_PRESETS.flatMap((preset) => {
682
+ const dims = getDimensionsFromAspectRatio({ aspectRatio: preset, bounds: modelBounds });
683
+ if (!dims) return [];
684
+ const normalized = normalizeAspectRatio(preset);
685
+ if (!normalized) return [];
686
+ return [{ aspectRatio: normalized, ...dims }];
687
+ });
688
+
689
+ const fallbackDimensions = presetDimensions.find((preset) => preset.aspectRatio === '1:1') ??
690
+ presetDimensions[0] ?? {
691
+ width: modelBounds.maxWidth,
692
+ height: modelBounds.maxHeight,
693
+ };
694
+
695
+ const widthTransformId = `${args.flowId}_aspectRatio_width`;
696
+ const heightTransformId = `${args.flowId}_aspectRatio_height`;
697
+
698
+ args.flow.push({
699
+ id: widthTransformId,
700
+ type: 'transform',
701
+ logic: {
702
+ transform: buildDimensionLookupCEL({
703
+ variable: 'aspectRatio',
704
+ presets: presetDimensions,
705
+ dimension: 'width',
706
+ fallback: fallbackDimensions.width,
707
+ }),
708
+ },
709
+ inputs: [{ name: 'aspectRatio', type: 'string', ref: args.aspectRatioRef }],
710
+ });
711
+
712
+ args.flow.push({
713
+ id: heightTransformId,
714
+ type: 'transform',
715
+ logic: {
716
+ transform: buildDimensionLookupCEL({
717
+ variable: 'aspectRatio',
718
+ presets: presetDimensions,
719
+ dimension: 'height',
720
+ fallback: fallbackDimensions.height,
721
+ }),
722
+ },
723
+ inputs: [{ name: 'aspectRatio', type: 'string', ref: args.aspectRatioRef }],
724
+ });
725
+
726
+ setOrPushFlowInput(
727
+ args.flowInputs,
728
+ args.flowInputs.findIndex((inp) => inp.name === widthInputName),
729
+ {
730
+ name: widthInputName,
731
+ type: 'number',
732
+ ref: { node: widthTransformId, name: 'all' },
733
+ },
734
+ );
735
+ setOrPushFlowInput(
736
+ args.flowInputs,
737
+ args.flowInputs.findIndex((inp) => inp.name === heightInputName),
738
+ {
739
+ name: heightInputName,
740
+ type: 'number',
741
+ ref: { node: heightTransformId, name: 'all' },
742
+ },
743
+ );
744
+ }
745
+ }
746
+
472
747
  function getForEachBodyEditorNodeIds(
473
748
  forEachId: string,
474
749
  forEachEndId: string,
@@ -528,8 +803,6 @@ function getNodeOutputValue(
528
803
  return undefined;
529
804
  }
530
805
 
531
- const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
532
-
533
806
  function internalGetConnectedNodes(
534
807
  nodes: WorkflowEditorNode[],
535
808
  edges: WorkflowEditorEdge[],
@@ -582,21 +855,36 @@ function internalGetConnectedNodes(
582
855
  return nodes.filter((n) => idSet.has(n.id));
583
856
  }
584
857
 
858
+ function resolveIfElseConditionField(
859
+ field: string,
860
+ candidateNodes: WorkflowEditorNode[],
861
+ ): { nodeId: string; targetHandleId?: string } | undefined {
862
+ const byExactId = candidateNodes.find((n) => n.id === field);
863
+ if (byExactId) return { nodeId: field };
864
+ for (const node of candidateNodes) {
865
+ for (const handle of node.data.outputHandles ?? []) {
866
+ if (`${node.id}_${handle.name}` === field) {
867
+ return { nodeId: node.id, targetHandleId: handle.id };
868
+ }
869
+ }
870
+ }
871
+ return undefined;
872
+ }
873
+
585
874
  function normalizeNodes(nodes: WorkflowEditorNode[]): WorkflowEditorNode[] {
586
875
  return nodes.map((node) => {
587
876
  if (node.type !== 'model' && node.type !== 'llm') return node;
588
- const hasType = (t: string | string[] | undefined, v: string) =>
589
- Array.isArray(t) ? t.includes(v) : t === v;
590
877
  const inputHandles = (node.data.inputHandles ?? [])
591
878
  .flatMap((h) => (h.subHandles ? h.subHandles : [h]))
592
879
  .filter(
593
880
  (h) =>
594
- hasType(h.type, 'conditional') ||
595
- hasType(h.type, 'prompt') ||
596
- hasType(h.type, 'image') ||
597
- hasType(h.type, 'video') ||
598
- hasType(h.type, '3d') ||
599
- hasType(h.type, 'audio') ||
881
+ handleTypeIncludes(h.type, 'conditional') ||
882
+ handleTypeIncludes(h.type, 'aspectRatio') ||
883
+ handleTypeIncludes(h.type, 'prompt') ||
884
+ handleTypeIncludes(h.type, 'image') ||
885
+ handleTypeIncludes(h.type, 'video') ||
886
+ handleTypeIncludes(h.type, '3d') ||
887
+ handleTypeIncludes(h.type, 'audio') ||
600
888
  (node.type === 'llm' && h.name === 'textInputs'),
601
889
  );
602
890
  return { ...node, data: { ...node.data, inputHandles } } as WorkflowEditorNode;
@@ -615,25 +903,31 @@ function normalizeNodes(nodes: WorkflowEditorNode[]): WorkflowEditorNode[] {
615
903
  * await client.workflows.update(workflowId, { flow: flow as WorkflowUpdateParams.Flow[] });
616
904
  * ```
617
905
  */
618
- export function convertWorkflowEditorToFlow(params: {
906
+ export type ConvertWorkflowEditorToFlowParams = {
619
907
  nodes: WorkflowEditorNode[];
620
908
  edges: WorkflowEditorEdge[];
621
909
  inputKeys: string[];
622
910
  /** Optional model resolver — return the model object, 'unknown', or undefined. */
623
911
  getModel?: (modelId: string) => WorkflowEditorModel | 'unknown' | undefined;
624
- }): WorkflowEditorFlowItem[] {
912
+ };
913
+
914
+ export function convertWorkflowEditorToFlow(
915
+ params: ConvertWorkflowEditorToFlowParams,
916
+ ): WorkflowEditorFlowItem[] {
625
917
  const { edges, inputKeys } = params;
626
918
  const nodes = normalizeNodes(params.nodes);
627
919
 
628
920
  const inputNodeIds = nodes.filter((n) => !!n.data.isInput).map((n) => n.id);
629
921
 
630
922
  const selectedOutputNodes = nodes.filter(
631
- (n) => (n.type === 'model' || n.type === 'forEachEnd') && !!n.data.isOutput,
923
+ (n) => (n.type === 'model' || n.type === 'forEachEnd' || n.type === 'llm') && !!n.data.isOutput,
632
924
  );
633
925
  const selectedOutputNodeIds = selectedOutputNodes.map((n) => n.id);
634
926
  const selectedOutputModelNodeIds = new Set(
635
927
  selectedOutputNodes
636
- .filter((n): n is WorkflowEditorNode & { type: 'model' } => n.type === 'model')
928
+ .filter(
929
+ (n): n is WorkflowEditorNode & { type: 'model' | 'llm' } => n.type === 'model' || n.type === 'llm',
930
+ )
637
931
  .map((n) => n.id),
638
932
  );
639
933
 
@@ -675,6 +969,12 @@ export function convertWorkflowEditorToFlow(params: {
675
969
  .map((n) => [n.id, nodeIdToFlowId(n.id)]),
676
970
  );
677
971
  const nodeIdToInputNameMap = new Map<string, string>(inputNodeIds.map((id) => [id, nodeIdToFlowId(id)]));
972
+ const approvalByApprovedNodeId = buildApprovalByApprovedNodeIdMap({
973
+ nodes,
974
+ edges,
975
+ nodesInSelectedBranches,
976
+ selectedOutputNodeIds,
977
+ });
678
978
  const nodeHandleTransformMap = new Map<string, string>();
679
979
 
680
980
  const ctx: SourceRefContext = {
@@ -751,12 +1051,17 @@ export function convertWorkflowEditorToFlow(params: {
751
1051
  const model = remoteModel === 'unknown' ? undefined : remoteModel;
752
1052
  const modelInputs = model?.inputs;
753
1053
  const conditionalHandleId = getConditionalInputHandleId(node.id);
1054
+ const aspectRatioEdges: WorkflowEditorEdge[] = [];
754
1055
 
755
1056
  const edgesByInput = new Map<string, WorkflowEditorEdge[]>();
756
1057
  for (const edge of incomingEdges) {
757
1058
  if (edge.sourceHandle === conditionalHandleId) continue;
758
1059
  const handle = getInputHandle(edge, node.data.inputHandles);
759
1060
  if (!handle?.name) continue;
1061
+ if (handleTypeIncludes(handle.type, 'aspectRatio')) {
1062
+ aspectRatioEdges.push(edge);
1063
+ continue;
1064
+ }
760
1065
  const existing = edgesByInput.get(handle.name);
761
1066
  if (existing) existing.push(edge);
762
1067
  else edgesByInput.set(handle.name, [edge]);
@@ -768,7 +1073,7 @@ export function convertWorkflowEditorToFlow(params: {
768
1073
  const modelInput = modelInputs?.find((inp) => inp.name === key);
769
1074
 
770
1075
  if (modelInput?.type === 'inputs_array') {
771
- if (!isFormArray(value)) continue;
1076
+ if (!getIsFormArray(value)) continue;
772
1077
  const nestedInputs = modelInput.inputs ?? [];
773
1078
  const items: WorkflowEditorFlowInput[][] = [];
774
1079
 
@@ -787,7 +1092,7 @@ export function convertWorkflowEditorToFlow(params: {
787
1092
  }
788
1093
  }
789
1094
  const isValid =
790
- isModelCompose(model) ? getHasComposeLayerSourceInput(itemInputs) : itemInputs.length > 0;
1095
+ getIsModelCompose(model) ? getHasComposeLayerSourceInput(itemInputs) : itemInputs.length > 0;
791
1096
  if (isValid) items.push(itemInputs);
792
1097
  }
793
1098
  if (items.length > 0) flowInputs.push({ name: key, type: 'inputs_array', items });
@@ -803,6 +1108,19 @@ export function convertWorkflowEditorToFlow(params: {
803
1108
  }
804
1109
  }
805
1110
 
1111
+ if (aspectRatioEdges.length > 0 && model) {
1112
+ const aspectRatioRef = getSourceRef(aspectRatioEdges[0]!, ctx);
1113
+ if (aspectRatioRef) {
1114
+ applyAspectRatioInputs({
1115
+ flow,
1116
+ flowInputs,
1117
+ flowId,
1118
+ aspectRatioRef,
1119
+ model,
1120
+ });
1121
+ }
1122
+ }
1123
+
806
1124
  for (const [inputName, inputEdges] of edgesByInput) {
807
1125
  const existingIndex = flowInputs.findIndex((inp) => inp.name === inputName);
808
1126
  const modelInput = modelInputs?.find((inp) => inp.name === inputName);
@@ -871,7 +1189,7 @@ export function convertWorkflowEditorToFlow(params: {
871
1189
  const isRequired =
872
1190
  (targetNode?.type === 'asset' &&
873
1191
  !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
874
- isModelCompose(model);
1192
+ getIsModelCompose(model);
875
1193
  transformVariables.push({ name: targetNodeKey, isArray: isArr, isRequired });
876
1194
 
877
1195
  if (
@@ -947,7 +1265,9 @@ export function convertWorkflowEditorToFlow(params: {
947
1265
  modelId: nodeData.modelId,
948
1266
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
949
1267
  includeOutputsInWorkflowJob:
950
- node.type === 'model' && selectedOutputModelNodeIds.has(node.id) ? true : undefined,
1268
+ (node.type === 'model' || node.type === 'llm') && selectedOutputModelNodeIds.has(node.id) ?
1269
+ true
1270
+ : undefined,
951
1271
  });
952
1272
 
953
1273
  ensureInputVideoTransformsAdded();
@@ -1011,6 +1331,19 @@ export function convertWorkflowEditorToFlow(params: {
1011
1331
  addVideoFirstLastFrameTransforms(node.id, flowId, node, edges, flow, nodeHandleTransformMap);
1012
1332
  }
1013
1333
 
1334
+ // ------------------------------------
1335
+ // AspectRatio -> transform with ratio literal
1336
+ // ------------------------------------
1337
+ } else if (node.type === 'aspectRatio') {
1338
+ const flowInputs = collectConditionalConnectionInputs(incomingEdges, node.id, ctx);
1339
+ const value = (node.data as { output?: string }).output || '1:1';
1340
+ flow.push({
1341
+ id: flowId,
1342
+ type: 'transform',
1343
+ logic: { transform: `'${escapeCELString(value)}'` },
1344
+ inputs: flowInputs.length > 0 ? flowInputs : undefined,
1345
+ });
1346
+
1014
1347
  // ------------------------------------
1015
1348
  // TransformText -> transform (CEL)
1016
1349
  // ------------------------------------
@@ -1084,7 +1417,7 @@ export function convertWorkflowEditorToFlow(params: {
1084
1417
  if (!ref) continue;
1085
1418
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
1086
1419
  const outHandleName = outHandle?.name ?? 'output';
1087
- const varName = getOutputHandleVarName(targetNode.id, outHandleName);
1420
+ const varName = `${targetNode.id}_${outHandleName}`;
1088
1421
  const isArr =
1089
1422
  (targetNode.type === 'asset' && !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
1090
1423
  targetNode.type === 'groupItems' ||
@@ -1199,16 +1532,50 @@ export function convertWorkflowEditorToFlow(params: {
1199
1532
  }
1200
1533
  }
1201
1534
 
1202
- for (const fieldNodeId of fieldsUsed) {
1535
+ const edgesIntoIfElse = edges.filter((e) => e.target === node.id);
1536
+ for (const fieldKey of fieldsUsed) {
1537
+ const resolved = resolveIfElseConditionField(fieldKey, nodes);
1538
+ const fieldNodeId = resolved?.nodeId;
1539
+ if (!fieldNodeId) continue;
1540
+
1541
+ const outputHandleName =
1542
+ resolved?.targetHandleId !== undefined ?
1543
+ nodes
1544
+ .find((n) => n.id === fieldNodeId)
1545
+ ?.data.outputHandles?.find((h) => h.id === resolved.targetHandleId)?.name ?? 'output'
1546
+ : 'output';
1547
+
1548
+ const celVarName = getOutputHandleVarName(fieldNodeId, outputHandleName);
1549
+ nodeIdToInputNameMap.set(fieldKey, celVarName);
1550
+
1551
+ const edgeToIfElse =
1552
+ edgesIntoIfElse
1553
+ .filter((e) => e.source === fieldNodeId)
1554
+ .find((e) => {
1555
+ if (resolved.targetHandleId === undefined) return true;
1556
+ return e.sourceHandle === resolved.targetHandleId;
1557
+ }) ?? edgesIntoIfElse.find((e) => e.source === fieldNodeId);
1558
+
1203
1559
  if (inputNodeIds.includes(fieldNodeId)) {
1204
- const inputName = nodeIdToInputNameMap.get(fieldNodeId);
1205
- if (inputName)
1206
- flowInputs.push({ name: inputName, type: 'string', ref: { node: 'workflow', name: inputName } });
1560
+ const workflowInputName = nodeIdToInputNameMap.get(fieldNodeId);
1561
+ if (workflowInputName) {
1562
+ const ref =
1563
+ edgeToIfElse ?
1564
+ getSourceRef(edgeToIfElse, ctx)
1565
+ : { node: 'workflow' as const, name: workflowInputName };
1566
+ flowInputs.push({
1567
+ name: celVarName,
1568
+ type: 'string',
1569
+ ref: ref ?? { node: 'workflow', name: workflowInputName },
1570
+ });
1571
+ }
1207
1572
  } else {
1573
+ const refFromEdge = edgeToIfElse ? getSourceRef(edgeToIfElse, ctx) : undefined;
1208
1574
  const targetFlowId = nodeIdToFlowIdMap.get(fieldNodeId);
1209
- if (targetFlowId) {
1210
- flowInputs.push({ name: targetFlowId, type: 'string', ref: { node: targetFlowId, name: 'all' } });
1211
- nodeIdToInputNameMap.set(fieldNodeId, targetFlowId);
1575
+ if (refFromEdge) {
1576
+ flowInputs.push({ name: celVarName, type: 'string', ref: refFromEdge });
1577
+ } else if (targetFlowId) {
1578
+ flowInputs.push({ name: celVarName, type: 'string', ref: { node: targetFlowId, name: 'all' } });
1212
1579
  }
1213
1580
  }
1214
1581
  }
@@ -1246,5 +1613,97 @@ export function convertWorkflowEditorToFlow(params: {
1246
1613
  forEachFlowItem.loopBodyNodeIds = forEachLoopFlowIds.get(forEachId) ?? [];
1247
1614
  }
1248
1615
 
1616
+ applyApprovalsToFlow({ flow, edges, approvalByApprovedNodeId, nodeIdToFlowIdMap });
1617
+
1249
1618
  return flow;
1250
1619
  }
1620
+
1621
+ // ---- Approval helpers ----
1622
+
1623
+ interface ApprovalInfo {
1624
+ approvalFlowId: string;
1625
+ approvedFlowId: string;
1626
+ message: string;
1627
+ }
1628
+
1629
+ function buildApprovalByApprovedNodeIdMap({
1630
+ nodes,
1631
+ edges,
1632
+ nodesInSelectedBranches,
1633
+ selectedOutputNodeIds,
1634
+ }: {
1635
+ nodes: WorkflowEditorNode[];
1636
+ edges: WorkflowEditorEdge[];
1637
+ nodesInSelectedBranches: Set<string>;
1638
+ selectedOutputNodeIds: string[];
1639
+ }): Map<string, ApprovalInfo> {
1640
+ const result = new Map<string, ApprovalInfo>();
1641
+ for (const approvalNode of nodes) {
1642
+ if (approvalNode.type !== 'approval') continue;
1643
+ const approvalEdge = edges.find(
1644
+ (e) => e.source === approvalNode.id && e.sourceHandle === `${approvalNode.id}-source-approval`,
1645
+ );
1646
+ if (!approvalEdge) continue;
1647
+ const approvedNodeId = approvalEdge.target;
1648
+ if (!nodesInSelectedBranches.has(approvedNodeId) && !selectedOutputNodeIds.includes(approvedNodeId))
1649
+ continue;
1650
+ const approvedNode = nodes.find((n) => n.id === approvedNodeId);
1651
+ if (!approvedNode) continue;
1652
+ const approvedFlowId =
1653
+ approvedNode.type === 'forEachEnd' ?
1654
+ (approvedNode.data as { parentNodeId?: string }).parentNodeId ?
1655
+ nodeIdToFlowId((approvedNode.data as { parentNodeId?: string }).parentNodeId!)
1656
+ : undefined
1657
+ : nodeIdToFlowId(approvedNodeId);
1658
+ if (!approvedFlowId) continue;
1659
+ result.set(approvedNodeId, {
1660
+ approvalFlowId: nodeIdToFlowId(approvalNode.id),
1661
+ approvedFlowId,
1662
+ message: (approvalNode.data as { message?: string }).message ?? '',
1663
+ });
1664
+ }
1665
+ return result;
1666
+ }
1667
+
1668
+ function applyApprovalsToFlow({
1669
+ flow,
1670
+ edges,
1671
+ approvalByApprovedNodeId,
1672
+ nodeIdToFlowIdMap,
1673
+ }: {
1674
+ flow: WorkflowEditorFlowItem[];
1675
+ edges: WorkflowEditorEdge[];
1676
+ approvalByApprovedNodeId: Map<string, ApprovalInfo>;
1677
+ nodeIdToFlowIdMap: Map<string, string>;
1678
+ }): void {
1679
+ if (approvalByApprovedNodeId.size === 0) return;
1680
+
1681
+ const flowIdToWorkflowNodeId = new Map<string, string>(
1682
+ [...nodeIdToFlowIdMap.entries()].map(([nodeId, flowId]) => [flowId, nodeId]),
1683
+ );
1684
+
1685
+ for (const flowItem of flow) {
1686
+ const workflowNodeId = flowIdToWorkflowNodeId.get(flowItem.id);
1687
+ if (!workflowNodeId) continue;
1688
+ const dependsOn = new Set(flowItem.dependsOn ?? []);
1689
+ const incomingEdges = edges.filter(
1690
+ (e) =>
1691
+ e.source === workflowNodeId &&
1692
+ !(e.data as { isForEachEndEdge?: boolean } | undefined)?.isForEachEndEdge,
1693
+ );
1694
+ for (const edge of incomingEdges) {
1695
+ const approval = approvalByApprovedNodeId.get(edge.target);
1696
+ if (approval) dependsOn.add(approval.approvalFlowId);
1697
+ }
1698
+ if (dependsOn.size > 0) flowItem.dependsOn = [...dependsOn].sort();
1699
+ }
1700
+
1701
+ for (const approvalInfo of approvalByApprovedNodeId.values()) {
1702
+ flow.push({
1703
+ id: approvalInfo.approvalFlowId,
1704
+ type: 'user-approval',
1705
+ dependsOn: [approvalInfo.approvedFlowId],
1706
+ label: approvalInfo.message,
1707
+ });
1708
+ }
1709
+ }