@scenario-labs/sdk 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/index.d.mts +1 -1
  3. package/index.d.mts.map +1 -1
  4. package/index.d.ts +1 -1
  5. package/index.d.ts.map +1 -1
  6. package/index.js +6 -1
  7. package/index.js.map +1 -1
  8. package/index.mjs +1 -1
  9. package/index.mjs.map +1 -1
  10. package/lib/workflow_converter.d.mts +47 -2
  11. package/lib/workflow_converter.d.mts.map +1 -1
  12. package/lib/workflow_converter.d.ts +47 -2
  13. package/lib/workflow_converter.d.ts.map +1 -1
  14. package/lib/workflow_converter.js +235 -27
  15. package/lib/workflow_converter.js.map +1 -1
  16. package/lib/workflow_converter.mjs +230 -27
  17. package/lib/workflow_converter.mjs.map +1 -1
  18. package/lib/workflow_import_validator.d.mts +1 -1
  19. package/lib/workflow_import_validator.d.mts.map +1 -1
  20. package/lib/workflow_import_validator.d.ts +1 -1
  21. package/lib/workflow_import_validator.d.ts.map +1 -1
  22. package/lib/workflow_import_validator.js +1 -0
  23. package/lib/workflow_import_validator.js.map +1 -1
  24. package/lib/workflow_import_validator.mjs +1 -0
  25. package/lib/workflow_import_validator.mjs.map +1 -1
  26. package/package.json +1 -1
  27. package/resources/assets/assets.d.mts +289 -135
  28. package/resources/assets/assets.d.mts.map +1 -1
  29. package/resources/assets/assets.d.ts +289 -135
  30. package/resources/assets/assets.d.ts.map +1 -1
  31. package/resources/assets/assets.js.map +1 -1
  32. package/resources/assets/assets.mjs.map +1 -1
  33. package/resources/generate.d.mts +86 -48
  34. package/resources/generate.d.mts.map +1 -1
  35. package/resources/generate.d.ts +86 -48
  36. package/resources/generate.d.ts.map +1 -1
  37. package/resources/jobs.d.mts +6 -6
  38. package/resources/jobs.d.mts.map +1 -1
  39. package/resources/jobs.d.ts +6 -6
  40. package/resources/jobs.d.ts.map +1 -1
  41. package/resources/models/description.d.mts +54 -4
  42. package/resources/models/description.d.mts.map +1 -1
  43. package/resources/models/description.d.ts +54 -4
  44. package/resources/models/description.d.ts.map +1 -1
  45. package/resources/models/examples.d.mts +74 -36
  46. package/resources/models/examples.d.mts.map +1 -1
  47. package/resources/models/examples.d.ts +74 -36
  48. package/resources/models/examples.d.ts.map +1 -1
  49. package/resources/models/models.d.mts +21 -21
  50. package/resources/models/models.d.mts.map +1 -1
  51. package/resources/models/models.d.ts +21 -21
  52. package/resources/models/models.d.ts.map +1 -1
  53. package/resources/models/models.js.map +1 -1
  54. package/resources/models/models.mjs.map +1 -1
  55. package/resources/models/train.d.mts +8 -8
  56. package/resources/models/train.d.mts.map +1 -1
  57. package/resources/models/train.d.ts +8 -8
  58. package/resources/models/train.d.ts.map +1 -1
  59. package/resources/search.d.mts +6 -0
  60. package/resources/search.d.mts.map +1 -1
  61. package/resources/search.d.ts +6 -0
  62. package/resources/search.d.ts.map +1 -1
  63. package/resources/uploads.d.mts +4 -4
  64. package/resources/uploads.d.mts.map +1 -1
  65. package/resources/uploads.d.ts +4 -4
  66. package/resources/uploads.d.ts.map +1 -1
  67. package/resources/usages.d.mts +31 -6
  68. package/resources/usages.d.mts.map +1 -1
  69. package/resources/usages.d.ts +31 -6
  70. package/resources/usages.d.ts.map +1 -1
  71. package/resources/workflows.d.mts +36 -36
  72. package/resources/workflows.d.mts.map +1 -1
  73. package/resources/workflows.d.ts +36 -36
  74. package/resources/workflows.d.ts.map +1 -1
  75. package/src/index.ts +9 -0
  76. package/src/lib/workflow_converter.ts +327 -20
  77. package/src/lib/workflow_import_validator.ts +2 -0
  78. package/src/resources/assets/assets.ts +376 -118
  79. package/src/resources/generate.ts +106 -44
  80. package/src/resources/jobs.ts +6 -6
  81. package/src/resources/models/description.ts +68 -2
  82. package/src/resources/models/examples.ts +94 -32
  83. package/src/resources/models/models.ts +21 -14
  84. package/src/resources/models/train.ts +8 -6
  85. package/src/resources/search.ts +7 -0
  86. package/src/resources/uploads.ts +4 -4
  87. package/src/resources/usages.ts +41 -4
  88. package/src/resources/workflows.ts +36 -36
  89. package/src/version.ts +1 -1
  90. package/version.d.mts +1 -1
  91. package/version.d.ts +1 -1
  92. package/version.js +1 -1
  93. 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;
@@ -86,12 +87,55 @@ export type WorkflowEditorModelInput = {
86
87
  name: string;
87
88
  type: string;
88
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;
89
99
  };
90
100
 
91
101
  export type WorkflowEditorModel = {
92
102
  id?: string;
93
103
  inputs?: WorkflowEditorModelInput[];
94
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;
95
139
  };
96
140
 
97
141
  export type WorkflowEditorFlowInputRef = {
@@ -176,6 +220,119 @@ function getConditionalInputHandleId(nodeId: string): string {
176
220
  return `${nodeId}-source-conditional`;
177
221
  }
178
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
+
179
336
  function getForEachFlowListInputDescriptor(args: {
180
337
  listProvider: WorkflowEditorNode | undefined;
181
338
  outputHandle: WorkflowEditorHandleOutput | undefined;
@@ -448,14 +605,14 @@ function toStaticInput(name: string, type: string, value: unknown): WorkflowEdit
448
605
  return { name, type, value: value as string | number | string[] | boolean };
449
606
  }
450
607
 
451
- function isFormArray(value: unknown): value is Record<string, unknown>[] {
608
+ function getIsFormArray(value: unknown): value is Record<string, unknown>[] {
452
609
  return (
453
610
  Array.isArray(value) &&
454
611
  (value as unknown[]).every((item) => typeof item === 'object' && item !== null && !Array.isArray(item))
455
612
  );
456
613
  }
457
614
 
458
- function isModelCompose(model: WorkflowEditorModel | undefined): boolean {
615
+ function getIsModelCompose(model: WorkflowEditorModel | undefined): boolean {
459
616
  return !!model?.id && (model.tags ?? []).includes('compose');
460
617
  }
461
618
 
@@ -473,6 +630,120 @@ function getIsConditionallyExecuted(
473
630
  });
474
631
  }
475
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
+
476
747
  function getForEachBodyEditorNodeIds(
477
748
  forEachId: string,
478
749
  forEachEndId: string,
@@ -532,8 +803,6 @@ function getNodeOutputValue(
532
803
  return undefined;
533
804
  }
534
805
 
535
- const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
536
-
537
806
  function internalGetConnectedNodes(
538
807
  nodes: WorkflowEditorNode[],
539
808
  edges: WorkflowEditorEdge[],
@@ -605,18 +874,17 @@ function resolveIfElseConditionField(
605
874
  function normalizeNodes(nodes: WorkflowEditorNode[]): WorkflowEditorNode[] {
606
875
  return nodes.map((node) => {
607
876
  if (node.type !== 'model' && node.type !== 'llm') return node;
608
- const hasType = (t: string | string[] | undefined, v: string) =>
609
- Array.isArray(t) ? t.includes(v) : t === v;
610
877
  const inputHandles = (node.data.inputHandles ?? [])
611
878
  .flatMap((h) => (h.subHandles ? h.subHandles : [h]))
612
879
  .filter(
613
880
  (h) =>
614
- hasType(h.type, 'conditional') ||
615
- hasType(h.type, 'prompt') ||
616
- hasType(h.type, 'image') ||
617
- hasType(h.type, 'video') ||
618
- hasType(h.type, '3d') ||
619
- 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') ||
620
888
  (node.type === 'llm' && h.name === 'textInputs'),
621
889
  );
622
890
  return { ...node, data: { ...node.data, inputHandles } } as WorkflowEditorNode;
@@ -635,25 +903,31 @@ function normalizeNodes(nodes: WorkflowEditorNode[]): WorkflowEditorNode[] {
635
903
  * await client.workflows.update(workflowId, { flow: flow as WorkflowUpdateParams.Flow[] });
636
904
  * ```
637
905
  */
638
- export function convertWorkflowEditorToFlow(params: {
906
+ export type ConvertWorkflowEditorToFlowParams = {
639
907
  nodes: WorkflowEditorNode[];
640
908
  edges: WorkflowEditorEdge[];
641
909
  inputKeys: string[];
642
910
  /** Optional model resolver — return the model object, 'unknown', or undefined. */
643
911
  getModel?: (modelId: string) => WorkflowEditorModel | 'unknown' | undefined;
644
- }): WorkflowEditorFlowItem[] {
912
+ };
913
+
914
+ export function convertWorkflowEditorToFlow(
915
+ params: ConvertWorkflowEditorToFlowParams,
916
+ ): WorkflowEditorFlowItem[] {
645
917
  const { edges, inputKeys } = params;
646
918
  const nodes = normalizeNodes(params.nodes);
647
919
 
648
920
  const inputNodeIds = nodes.filter((n) => !!n.data.isInput).map((n) => n.id);
649
921
 
650
922
  const selectedOutputNodes = nodes.filter(
651
- (n) => (n.type === 'model' || n.type === 'forEachEnd') && !!n.data.isOutput,
923
+ (n) => (n.type === 'model' || n.type === 'forEachEnd' || n.type === 'llm') && !!n.data.isOutput,
652
924
  );
653
925
  const selectedOutputNodeIds = selectedOutputNodes.map((n) => n.id);
654
926
  const selectedOutputModelNodeIds = new Set(
655
927
  selectedOutputNodes
656
- .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
+ )
657
931
  .map((n) => n.id),
658
932
  );
659
933
 
@@ -777,12 +1051,17 @@ export function convertWorkflowEditorToFlow(params: {
777
1051
  const model = remoteModel === 'unknown' ? undefined : remoteModel;
778
1052
  const modelInputs = model?.inputs;
779
1053
  const conditionalHandleId = getConditionalInputHandleId(node.id);
1054
+ const aspectRatioEdges: WorkflowEditorEdge[] = [];
780
1055
 
781
1056
  const edgesByInput = new Map<string, WorkflowEditorEdge[]>();
782
1057
  for (const edge of incomingEdges) {
783
1058
  if (edge.sourceHandle === conditionalHandleId) continue;
784
1059
  const handle = getInputHandle(edge, node.data.inputHandles);
785
1060
  if (!handle?.name) continue;
1061
+ if (handleTypeIncludes(handle.type, 'aspectRatio')) {
1062
+ aspectRatioEdges.push(edge);
1063
+ continue;
1064
+ }
786
1065
  const existing = edgesByInput.get(handle.name);
787
1066
  if (existing) existing.push(edge);
788
1067
  else edgesByInput.set(handle.name, [edge]);
@@ -794,7 +1073,7 @@ export function convertWorkflowEditorToFlow(params: {
794
1073
  const modelInput = modelInputs?.find((inp) => inp.name === key);
795
1074
 
796
1075
  if (modelInput?.type === 'inputs_array') {
797
- if (!isFormArray(value)) continue;
1076
+ if (!getIsFormArray(value)) continue;
798
1077
  const nestedInputs = modelInput.inputs ?? [];
799
1078
  const items: WorkflowEditorFlowInput[][] = [];
800
1079
 
@@ -813,7 +1092,7 @@ export function convertWorkflowEditorToFlow(params: {
813
1092
  }
814
1093
  }
815
1094
  const isValid =
816
- isModelCompose(model) ? getHasComposeLayerSourceInput(itemInputs) : itemInputs.length > 0;
1095
+ getIsModelCompose(model) ? getHasComposeLayerSourceInput(itemInputs) : itemInputs.length > 0;
817
1096
  if (isValid) items.push(itemInputs);
818
1097
  }
819
1098
  if (items.length > 0) flowInputs.push({ name: key, type: 'inputs_array', items });
@@ -829,6 +1108,19 @@ export function convertWorkflowEditorToFlow(params: {
829
1108
  }
830
1109
  }
831
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
+
832
1124
  for (const [inputName, inputEdges] of edgesByInput) {
833
1125
  const existingIndex = flowInputs.findIndex((inp) => inp.name === inputName);
834
1126
  const modelInput = modelInputs?.find((inp) => inp.name === inputName);
@@ -897,7 +1189,7 @@ export function convertWorkflowEditorToFlow(params: {
897
1189
  const isRequired =
898
1190
  (targetNode?.type === 'asset' &&
899
1191
  !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
900
- isModelCompose(model);
1192
+ getIsModelCompose(model);
901
1193
  transformVariables.push({ name: targetNodeKey, isArray: isArr, isRequired });
902
1194
 
903
1195
  if (
@@ -973,7 +1265,9 @@ export function convertWorkflowEditorToFlow(params: {
973
1265
  modelId: nodeData.modelId,
974
1266
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
975
1267
  includeOutputsInWorkflowJob:
976
- node.type === 'model' && selectedOutputModelNodeIds.has(node.id) ? true : undefined,
1268
+ (node.type === 'model' || node.type === 'llm') && selectedOutputModelNodeIds.has(node.id) ?
1269
+ true
1270
+ : undefined,
977
1271
  });
978
1272
 
979
1273
  ensureInputVideoTransformsAdded();
@@ -1037,6 +1331,19 @@ export function convertWorkflowEditorToFlow(params: {
1037
1331
  addVideoFirstLastFrameTransforms(node.id, flowId, node, edges, flow, nodeHandleTransformMap);
1038
1332
  }
1039
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
+
1040
1347
  // ------------------------------------
1041
1348
  // TransformText -> transform (CEL)
1042
1349
  // ------------------------------------
@@ -8,6 +8,7 @@ export class WorkflowImportError extends Error {
8
8
  export type WorkflowEditorNodeType =
9
9
  | 'text'
10
10
  | 'asset'
11
+ | 'aspectRatio'
11
12
  | 'model'
12
13
  | 'llm'
13
14
  | 'transformText'
@@ -22,6 +23,7 @@ export type WorkflowEditorNodeType =
22
23
  export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
23
24
  'text',
24
25
  'asset',
26
+ 'aspectRatio',
25
27
  'model',
26
28
  'llm',
27
29
  'transformText',