cansik commited on
Commit
abd08cb
·
verified ·
1 Parent(s): 3025bb3

Upload folder via script

Browse files
dataflow/ui/vueflow_canvas.vue CHANGED
@@ -122,6 +122,23 @@
122
  </span>
123
  </button>
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  <!-- rest of header (menu) unchanged -->
126
  <div class="vf-node-menu" @click.stop>
127
  <button
@@ -146,18 +163,49 @@
146
  </div>
147
  </div>
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  <!-- node level error -->
150
  <div v-if="nodeProps.data.values && nodeProps.data.values.error" class="vf-node-error">
151
  <span>{{ nodeProps.data.values.error }}</span>
152
  </div>
153
 
154
  <div class="vf-node-content">
 
155
  <q-inner-loading
156
  v-if="nodeProps.data.executable"
157
  :showing="nodeProps.data.processing && (nodeProps.data.progress !== 1 || nodeProps.data.progress !== 0)"
158
  >
159
  <q-spinner-grid size="50px" color="black" />
160
  </q-inner-loading>
 
161
  <template v-if="nodeProps.data.fields && nodeProps.data.fields.length">
162
  <div
163
  v-for="field in nodeProps.data.fields"
@@ -418,6 +466,9 @@ export default {
418
  // which node menu is open
419
  nodeMenuFor: null,
420
 
 
 
 
421
  // copy/paste support
422
  copiedNodeId: null,
423
  mouseFlowPosition: {x: 0, y: 0},
@@ -498,6 +549,10 @@ export default {
498
 
499
  toggleNodeMenu(id) {
500
  this.nodeMenuFor = this.nodeMenuFor === id ? null : id;
 
 
 
 
501
  },
502
 
503
  isNodeMenuOpen(id) {
@@ -509,6 +564,32 @@ export default {
509
  this.emitEvent("node_menu_action", {id: id, action: action});
510
  },
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  // public API for Python
513
 
514
  setGraph(payload) {
@@ -742,6 +823,31 @@ export default {
742
  });
743
  },
744
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
745
  resizeAndConvertToWebp(blob, maxSize = 1024) {
746
  return new Promise((resolve, reject) => {
747
  const img = new Image();
@@ -1055,6 +1161,7 @@ export default {
1055
  this.contextMenuVisible = false;
1056
  this.contextMenuFlowPosition = null;
1057
  this.nodeMenuFor = null;
 
1058
  },
1059
 
1060
  onCreateNodeClick(entry) {
@@ -1176,6 +1283,7 @@ export default {
1176
 
1177
  onPaneClick(event) {
1178
  this.hideContextMenu();
 
1179
  this.emitEvent("pane_click", event || {});
1180
  },
1181
 
@@ -1524,6 +1632,7 @@ export default {
1524
  flex: 1 1 auto;
1525
  padding: 6px 8px 8px 8px;
1526
  max-width: 260px;
 
1527
  }
1528
 
1529
  .vf-node-header {
@@ -1536,12 +1645,18 @@ export default {
1536
 
1537
  .vf-node-title {
1538
  font-weight: 600;
 
 
 
 
 
1539
  }
1540
 
1541
  .vf-node-header-actions {
1542
  display: inline-flex;
1543
  align-items: center;
1544
  gap: 4px;
 
1545
  }
1546
 
1547
  .vf-node-content {
@@ -1761,6 +1876,75 @@ export default {
1761
  font-size: 12px;
1762
  opacity: 0.7;
1763
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1764
  </style>
1765
 
1766
  <style>
 
122
  </span>
123
  </button>
124
 
125
+ <!-- expansion menu button (settings) -->
126
+ <div
127
+ v-if="hasExpansionMenu(nodeProps)"
128
+ class="vf-expansion-menu-header"
129
+ @click.stop
130
+ >
131
+ <button
132
+ type="button"
133
+ class="vf-exec-button"
134
+ @click.stop="toggleExpansionMenu(nodeProps.id)"
135
+ >
136
+ <span>
137
+ <q-icon :name="isExpansionMenuOpen(nodeProps.id) ? 'close' : 'menu'" />
138
+ </span>
139
+ </button>
140
+ </div>
141
+
142
  <!-- rest of header (menu) unchanged -->
143
  <div class="vf-node-menu" @click.stop>
144
  <button
 
163
  </div>
164
  </div>
165
 
166
+ <!-- expansion menu dropdown (positioned relative to node body) -->
167
+ <div
168
+ v-if="hasExpansionMenu(nodeProps) && isExpansionMenuOpen(nodeProps.id)"
169
+ class="vf-expansion-menu-dropdown"
170
+ @click.stop
171
+ >
172
+ <template v-for="field in getExpansionMenuFields(nodeProps)" :key="field.name">
173
+ <template v-if="field.options && field.options.aspect_ratio">
174
+ <div class="vf-expansion-menu-item">
175
+ <label class="vf-expansion-menu-label">
176
+ {{ field.options.aspect_ratio.label || 'Aspect Ratio' }}
177
+ </label>
178
+ <q-select
179
+ :model-value="nodeProps.data.values[field.options.aspect_ratio.name] || '1:1'"
180
+ :options="field.options.aspect_ratio.options"
181
+ option-label="label"
182
+ option-value="value"
183
+ emit-value
184
+ map-options
185
+ dense
186
+ outlined
187
+ class="vf-expansion-menu-select"
188
+ @update:model-value="onAspectRatioChange(nodeProps, field, $event)"
189
+ />
190
+ </div>
191
+ </template>
192
+ </template>
193
+ </div>
194
+
195
  <!-- node level error -->
196
  <div v-if="nodeProps.data.values && nodeProps.data.values.error" class="vf-node-error">
197
  <span>{{ nodeProps.data.values.error }}</span>
198
  </div>
199
 
200
  <div class="vf-node-content">
201
+ <!--
202
  <q-inner-loading
203
  v-if="nodeProps.data.executable"
204
  :showing="nodeProps.data.processing && (nodeProps.data.progress !== 1 || nodeProps.data.progress !== 0)"
205
  >
206
  <q-spinner-grid size="50px" color="black" />
207
  </q-inner-loading>
208
+ -->
209
  <template v-if="nodeProps.data.fields && nodeProps.data.fields.length">
210
  <div
211
  v-for="field in nodeProps.data.fields"
 
466
  // which node menu is open
467
  nodeMenuFor: null,
468
 
469
+ // which expansion menu is open
470
+ expansionMenuFor: null,
471
+
472
  // copy/paste support
473
  copiedNodeId: null,
474
  mouseFlowPosition: {x: 0, y: 0},
 
549
 
550
  toggleNodeMenu(id) {
551
  this.nodeMenuFor = this.nodeMenuFor === id ? null : id;
552
+ // Close expansion menu when opening node menu
553
+ if (this.nodeMenuFor === id) {
554
+ this.expansionMenuFor = null;
555
+ }
556
  },
557
 
558
  isNodeMenuOpen(id) {
 
564
  this.emitEvent("node_menu_action", {id: id, action: action});
565
  },
566
 
567
+ toggleExpansionMenu(id) {
568
+ this.expansionMenuFor = this.expansionMenuFor === id ? null : id;
569
+ // Close node menu when opening expansion menu
570
+ if (this.expansionMenuFor === id) {
571
+ this.nodeMenuFor = null;
572
+ }
573
+ },
574
+
575
+ isExpansionMenuOpen(id) {
576
+ return this.expansionMenuFor === id;
577
+ },
578
+
579
+ hasExpansionMenu(nodeProps) {
580
+ if (!nodeProps || !nodeProps.data || !nodeProps.data.fields) {
581
+ return false;
582
+ }
583
+ return nodeProps.data.fields.some(field => field.kind === 'expansion_menu');
584
+ },
585
+
586
+ getExpansionMenuFields(nodeProps) {
587
+ if (!nodeProps || !nodeProps.data || !nodeProps.data.fields) {
588
+ return [];
589
+ }
590
+ return nodeProps.data.fields.filter(field => field.kind === 'expansion_menu');
591
+ },
592
+
593
  // public API for Python
594
 
595
  setGraph(payload) {
 
823
  });
824
  },
825
 
826
+ onAspectRatioChange(nodeProps, field, value) {
827
+ if (!nodeProps || !nodeProps.data || !field) return;
828
+
829
+ // Update the value in the node data
830
+ if (!nodeProps.data.values) {
831
+ nodeProps.data.values = {};
832
+ }
833
+ nodeProps.data.values.aspect_ratio = value;
834
+
835
+ // Update the node values in Vue Flow
836
+ this.updateNodeValues({
837
+ id: nodeProps.id,
838
+ values: {aspect_ratio: value}
839
+ });
840
+
841
+ // Emit the change event
842
+ this.emitEvent("node_field_changed", {
843
+ id: nodeProps.id,
844
+ field: "aspect_ratio",
845
+ fieldKind: "expansion_menu",
846
+ value: value
847
+ });
848
+ },
849
+
850
+
851
  resizeAndConvertToWebp(blob, maxSize = 1024) {
852
  return new Promise((resolve, reject) => {
853
  const img = new Image();
 
1161
  this.contextMenuVisible = false;
1162
  this.contextMenuFlowPosition = null;
1163
  this.nodeMenuFor = null;
1164
+ this.expansionMenuFor = null;
1165
  },
1166
 
1167
  onCreateNodeClick(entry) {
 
1283
 
1284
  onPaneClick(event) {
1285
  this.hideContextMenu();
1286
+ this.expansionMenuFor = null;
1287
  this.emitEvent("pane_click", event || {});
1288
  },
1289
 
 
1632
  flex: 1 1 auto;
1633
  padding: 6px 8px 8px 8px;
1634
  max-width: 260px;
1635
+ position: relative;
1636
  }
1637
 
1638
  .vf-node-header {
 
1645
 
1646
  .vf-node-title {
1647
  font-weight: 600;
1648
+ flex: 1 1 auto;
1649
+ min-width: 0;
1650
+ overflow: hidden;
1651
+ text-overflow: ellipsis;
1652
+ white-space: nowrap;
1653
  }
1654
 
1655
  .vf-node-header-actions {
1656
  display: inline-flex;
1657
  align-items: center;
1658
  gap: 4px;
1659
+ flex-shrink: 0;
1660
  }
1661
 
1662
  .vf-node-content {
 
1876
  font-size: 12px;
1877
  opacity: 0.7;
1878
  }
1879
+
1880
+ /* expansion menu styling - header button with dropdown */
1881
+ .vf-expansion-menu-header {
1882
+ position: relative;
1883
+ }
1884
+
1885
+ .vf-expansion-menu-dropdown {
1886
+ position: absolute;
1887
+ left: 0;
1888
+ right: 0;
1889
+ top: 100%;
1890
+ margin-top: 4px;
1891
+ border-radius: 4px;
1892
+ background: #ffffff;
1893
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.15);
1894
+ padding: 8px;
1895
+ z-index: 30;
1896
+ width: 100%;
1897
+ box-sizing: border-box;
1898
+ }
1899
+
1900
+ .vf-expansion-menu-item {
1901
+ display: flex;
1902
+ flex-direction: column;
1903
+ gap: 4px;
1904
+ }
1905
+
1906
+ .vf-expansion-menu-label {
1907
+ font-size: 11px;
1908
+ color: #6b7280;
1909
+ font-weight: 500;
1910
+ margin-bottom: 2px;
1911
+ }
1912
+
1913
+ .vf-expansion-menu-select {
1914
+ font-size: 11px;
1915
+ cursor: pointer;
1916
+ width: 100%;
1917
+ }
1918
+
1919
+ .vf-expansion-menu-select :deep(.q-field) {
1920
+ cursor: pointer;
1921
+ }
1922
+
1923
+ .vf-expansion-menu-select :deep(.q-field__control) {
1924
+ min-height: 24px;
1925
+ height: 24px;
1926
+ cursor: pointer;
1927
+ }
1928
+
1929
+ .vf-expansion-menu-select :deep(.q-field__native) {
1930
+ min-height: 24px;
1931
+ padding: 2px 8px;
1932
+ font-size: 11px;
1933
+ cursor: pointer;
1934
+ }
1935
+
1936
+ .vf-expansion-menu-select :deep(.q-field__inner) {
1937
+ cursor: pointer;
1938
+ }
1939
+
1940
+ .vf-expansion-menu-select :deep(.q-field__marginal) {
1941
+ cursor: pointer;
1942
+ }
1943
+
1944
+ .vf-expansion-menu-select :deep(.q-field__bottom) {
1945
+ cursor: pointer;
1946
+ }
1947
+
1948
  </style>
1949
 
1950
  <style>
nodes/controller.py CHANGED
@@ -181,6 +181,39 @@ class GraphController:
181
  elif isinstance(node, TextToImageNode) and field == "image":
182
  node.image_src = "" if value is None else str(value)
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  def _on_edges_delete(self, edges: list[dict[str, Any]]) -> None:
185
  """Remove matching connections from the DataGraph when edges are deleted in Vue."""
186
  if not edges:
 
181
  elif isinstance(node, TextToImageNode) and field == "image":
182
  node.image_src = "" if value is None else str(value)
183
 
184
+ elif isinstance(node, TextToImageNode) and field == "aspect_ratio":
185
+ # Parse aspect_ratio value from the dropdown selection. Thanks Flo!
186
+ aspect_ratio_value = "1:1" # default
187
+
188
+ print(f"[DEBUG controller] Aspect ratio set to {value}, type={type(value)}")
189
+
190
+ if value is not None and isinstance(value, str):
191
+ aspect_ratio_value = value.strip()
192
+ else:
193
+ print(f"[DEBUG controller] aspect_ratio value is None, using default 1:1")
194
+
195
+ # Validate aspect ratio format (should be "W:H")
196
+ # Allow common formats: "1:1", "16:9", "9:16", etc. we could add more later if needed
197
+ if aspect_ratio_value and ":" in aspect_ratio_value:
198
+ parts = aspect_ratio_value.split(":")
199
+ if len(parts) == 2:
200
+ try:
201
+ # Validate that both parts are numeric
202
+ float(parts[0])
203
+ float(parts[1])
204
+ old_ratio = node.aspect_ratio
205
+ node.aspect_ratio = aspect_ratio_value
206
+
207
+ # Clear cached image since aspect ratio changed
208
+ node.image_src = None
209
+ node.decoded_image = None
210
+
211
+ except (ValueError, TypeError):
212
+ # Invalid format, use default
213
+ node.aspect_ratio = "1:1"
214
+ print(f"[DEBUG Controller] Invalid numeric format, using default: {node.aspect_ratio}")
215
+
216
+
217
  def _on_edges_delete(self, edges: list[dict[str, Any]]) -> None:
218
  """Remove matching connections from the DataGraph when edges are deleted in Vue."""
219
  if not edges:
nodes/session.py CHANGED
@@ -549,8 +549,22 @@ class GraphSession:
549
  return
550
 
551
  if isinstance(node, TextToImageNode):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  buf = io.BytesIO()
553
- node.decoded_image.save(buf, format="PNG")
554
  buf.seek(0)
555
 
556
  png_bytes = buf.getvalue()
 
549
  return
550
 
551
  if isinstance(node, TextToImageNode):
552
+ # Try to get the image from decoded_image first, then from image_src
553
+ img = node.decoded_image
554
+ if img is None and node.image_src:
555
+ try:
556
+ from . import utils
557
+ img = utils.decode_image(node.image_src)
558
+ except Exception as e:
559
+ ui.notify(f"Failed to decode image: {e}", type="negative")
560
+ return
561
+
562
+ if img is None:
563
+ ui.notify("No image available to download", type="warning")
564
+ return
565
+
566
  buf = io.BytesIO()
567
+ img.save(buf, format="PNG")
568
  buf.seek(0)
569
 
570
  png_bytes = buf.getvalue()
nodes/text_to_image.py CHANGED
@@ -51,6 +51,7 @@ class TextToImageNode(NodeInstance):
51
  error: str | None = None
52
  progress_value: float = 0.0
53
  progress_message: str | None = None
 
54
 
55
  async def process(self) -> None:
56
  # reset error message (but is not directly updated)
@@ -59,12 +60,14 @@ class TextToImageNode(NodeInstance):
59
  text_input = self.inputs["text"]
60
  image_output = self.outputs["image"]
61
 
62
- print(f"\n[DEBUG TextToImageNode] ===== STARTING PROCESS =====")
 
63
  print(f"[DEBUG] Text input value: {text_input.value is not None}")
64
  print(f"[DEBUG] Text input state: {text_input.state}")
 
65
 
66
  # Collect images from ordered input ports (image1, image2, image3, ...)
67
- # Only include images that are actually connected (not None)
68
  images: list[Image.Image] = []
69
  for i in range(1, NUM_IMAGE_INPUTS + 1):
70
  image_port_name = f"image{i}"
@@ -134,7 +137,7 @@ class TextToImageNode(NodeInstance):
134
  loop = asyncio.get_running_loop()
135
  result = await loop.run_in_executor(
136
  None,
137
- partial(image_service.generate, prompt, images=images_list, progress=on_progress)
138
  )
139
 
140
  url = utils.encode_image(result.image)
@@ -152,6 +155,7 @@ class TextToImageNode(NodeInstance):
152
  self.error = None
153
  self.progress_value = 0.0
154
  self.progress_message = None
 
155
 
156
  # clear output port value and mark it dirty
157
  out = self.outputs.get("image") if self.outputs is not None else None
@@ -177,7 +181,7 @@ class TextToImageNodeRenderable(VueNodeRenderable[TextToImageNode]):
177
  num_inputs = len(input_ports)
178
  if num_inputs > 0:
179
  # Start the first input at 30% of the node height, end at 85%, evenly distributed
180
- # It's eyeballed, so it might not be perfect!
181
  start_percent = 30
182
  end_percent = 85
183
  if num_inputs == 1:
@@ -224,6 +228,26 @@ class TextToImageNodeRenderable(VueNodeRenderable[TextToImageNode]):
224
  if node.error:
225
  data.values["error"] = node.error
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  data.executable = True
228
  data.processing = False
229
 
 
51
  error: str | None = None
52
  progress_value: float = 0.0
53
  progress_message: str | None = None
54
+ aspect_ratio: str = "1:1" # Default aspect ratio
55
 
56
  async def process(self) -> None:
57
  # reset error message (but is not directly updated)
 
60
  text_input = self.inputs["text"]
61
  image_output = self.outputs["image"]
62
 
63
+ print(f"\n[DEBUG] ===== GENERATING IMAGE =====")
64
+ print(f"[DEBUG] Node ID: {self.node_id}")
65
  print(f"[DEBUG] Text input value: {text_input.value is not None}")
66
  print(f"[DEBUG] Text input state: {text_input.state}")
67
+ print(f"[DEBUG] Image aspect ratio selected: {self.aspect_ratio}")
68
 
69
  # Collect images from ordered input ports (image1, image2, image3, ...)
70
+ # But Only include images that are actually connected
71
  images: list[Image.Image] = []
72
  for i in range(1, NUM_IMAGE_INPUTS + 1):
73
  image_port_name = f"image{i}"
 
137
  loop = asyncio.get_running_loop()
138
  result = await loop.run_in_executor(
139
  None,
140
+ partial(image_service.generate, prompt, images=images_list, progress=on_progress, aspect_ratio=self.aspect_ratio)
141
  )
142
 
143
  url = utils.encode_image(result.image)
 
155
  self.error = None
156
  self.progress_value = 0.0
157
  self.progress_message = None
158
+ # Note: aspect_ratio is not reset, it persists
159
 
160
  # clear output port value and mark it dirty
161
  out = self.outputs.get("image") if self.outputs is not None else None
 
181
  num_inputs = len(input_ports)
182
  if num_inputs > 0:
183
  # Start the first input at 30% of the node height, end at 85%, evenly distributed
184
+ # It's eyeballed, so pretty ugly at this point
185
  start_percent = 30
186
  end_percent = 85
187
  if num_inputs == 1:
 
228
  if node.error:
229
  data.values["error"] = node.error
230
 
231
+ # Add expansion menu with aspect ratio dropdown. Thanks Flo!
232
+ data.fields.append(
233
+ {
234
+ #"name": "Settings",
235
+ "kind": "expansion_menu",
236
+ "options": {
237
+ "aspect_ratio": {
238
+ "name": "aspect_ratio",
239
+ "label": "Aspect Ratio",
240
+ "options": [
241
+ {"label": "1:1 (Square)", "value": "1:1"},
242
+ {"label": "16:9 (Landscape)", "value": "16:9"},
243
+ {"label": "9:16 (Portrait)", "value": "9:16"}
244
+ ],
245
+ }
246
+ },
247
+ }
248
+ )
249
+ data.values["aspect_ratio"] = node.aspect_ratio
250
+
251
  data.executable = True
252
  data.processing = False
253
 
nodes/utils.py CHANGED
@@ -11,12 +11,17 @@ def encode_image(img: Image.Image, image_format: str | None = None) -> str:
11
  If not provided, fall back to WEBP.
12
  """
13
 
 
 
14
  if image_format is None:
15
  image_format = "WEBP"
16
 
 
 
 
17
  buffer = io.BytesIO()
18
  try:
19
- img.save(buffer, format=image_format)
20
  except Exception as e:
21
  raise ValueError(f"Failed to encode image as {image_format}: {e}")
22
 
 
11
  If not provided, fall back to WEBP.
12
  """
13
 
14
+ params = {}
15
+
16
  if image_format is None:
17
  image_format = "WEBP"
18
 
19
+ if image_format == "WEBP":
20
+ params["quality"] = 90
21
+
22
  buffer = io.BytesIO()
23
  try:
24
+ img.save(buffer, format=image_format, **params)
25
  except Exception as e:
26
  raise ValueError(f"Failed to encode image as {image_format}: {e}")
27
 
services/image/DummyImageGenerator.py CHANGED
@@ -33,6 +33,8 @@ class DummyImageGenerator(ImageGenerator):
33
  images: Sequence[Image.Image] | None = None,
34
  *,
35
  progress: ProgressCallback | None = None,
 
 
36
  ) -> ImageGenerationResult:
37
  call_progress(progress, 0.1, "Starting dummy image request")
38
 
 
33
  images: Sequence[Image.Image] | None = None,
34
  *,
35
  progress: ProgressCallback | None = None,
36
+ aspect_ratio: str | None = None,
37
+ **kwargs: Any,
38
  ) -> ImageGenerationResult:
39
  call_progress(progress, 0.1, "Starting dummy image request")
40
 
services/image/FalAIImageGenerator.py CHANGED
@@ -108,8 +108,13 @@ class FalAIImageGenerator(ImageGenerator, ABC):
108
  images: Sequence[Image.Image] | None = None,
109
  *,
110
  progress: ProgressCallback | None = None,
 
111
  **kwargs: Any,
112
  ) -> ImageGenerationResult:
 
 
 
 
113
  model = self._select_model(prompt=prompt, images=images, **kwargs)
114
 
115
  call_progress(progress, 0.1, "Encoding inputs for fal.ai image model")
 
108
  images: Sequence[Image.Image] | None = None,
109
  *,
110
  progress: ProgressCallback | None = None,
111
+ aspect_ratio: str | None = None,
112
  **kwargs: Any,
113
  ) -> ImageGenerationResult:
114
+ # Include aspect_ratio in kwargs if provided
115
+ if aspect_ratio is not None:
116
+ kwargs = {**kwargs, "aspect_ratio": aspect_ratio}
117
+
118
  model = self._select_model(prompt=prompt, images=images, **kwargs)
119
 
120
  call_progress(progress, 0.1, "Encoding inputs for fal.ai image model")
services/image/GoogleImageGenerator.py CHANGED
@@ -47,6 +47,8 @@ class GoogleImageGenerator(ImageGenerator):
47
  images: Sequence[Image.Image] | None = None,
48
  *,
49
  progress: ProgressCallback | None = None,
 
 
50
  ) -> ImageGenerationResult:
51
  from google.genai import types # type: ignore[import-not-found]
52
 
 
47
  images: Sequence[Image.Image] | None = None,
48
  *,
49
  progress: ProgressCallback | None = None,
50
+ aspect_ratio: str | None = None,
51
+ **kwargs: Any,
52
  ) -> ImageGenerationResult:
53
  from google.genai import types # type: ignore[import-not-found]
54
 
services/image/ImageGenerator.py CHANGED
@@ -20,7 +20,10 @@ class ImageGenerator(GenerationService, ABC):
20
  images: Sequence[Image.Image] | None = None,
21
  *,
22
  progress: ProgressCallback | None = None,
 
23
  **kwargs: Any,
24
  ) -> ImageGenerationResult:
25
- """Generate images from a prompt and optional images."""
 
 
26
  raise NotImplementedError
 
20
  images: Sequence[Image.Image] | None = None,
21
  *,
22
  progress: ProgressCallback | None = None,
23
+ aspect_ratio: str | None = None,
24
  **kwargs: Any,
25
  ) -> ImageGenerationResult:
26
+
27
+ """Generate images from a prompt and optional images. """
28
+
29
  raise NotImplementedError
velai_app.py CHANGED
@@ -22,19 +22,25 @@ class VelaiApp:
22
  with ui.column().classes("w-full h-screen no-wrap"):
23
  # header
24
  with ui.row().classes("w-full items-center justify-between px-4 py-2 bg-grey-2"):
25
- # ui.label("velai").classes("text-lg font-bold")
26
  ui.image("assets/logo.png").classes("w-16").style("margin-right: 10px;")
27
  with ui.row().classes("items-center gap-2"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- with ui.button("Info", on_click=lambda: dialog.open()).props("color='black'"):
30
- #show a popup with the information about the app
31
- with ui.dialog() as dialog:
32
- with ui.card():
33
- ui.button(icon='close', on_click=dialog.close).props("color='black'").classes('absolute top-2 right-2')
34
- ui.label("Welcome to VELAI!").classes("text-lg font-bold")
35
- ui.label("This is a node-based environment for generating images.\nIt uses the Gemini 2.5 Flash Preview (Nano Banana) and Reve models.")
36
- ui.label("As it's a work in progress, there might be some bugs and issues. Please report them to us :)")
37
-
38
  with ui.dropdown_button("Add Node", auto_close=True).props("color='black'"):
39
  kinds = session.creatable_node_types
40
  for k in kinds:
@@ -89,32 +95,34 @@ class VelaiApp:
89
  session = self.session
90
  canvas = self.canvas
91
 
92
- with ui.dialog() as dialog:
93
- with ui.card():
94
- ui.label("Upload graph JSON")
 
 
95
 
96
- async def handle_upload(e: events.UploadEventArguments) -> None:
97
- data = await e.file.read()
98
- content = data.decode("utf-8")
99
 
100
- session.load_from_json(content)
101
- session.save_to_storage()
102
 
103
- nodes = session.initial_vue_nodes()
104
- edges = session.initial_vue_edges()
105
- canvas.set_graph(nodes, edges)
106
 
107
- dialog.close()
108
 
109
- ui.upload(
110
- label="Choose JSON file",
111
- on_upload=handle_upload,
112
- max_files=1,
113
- ).props("accept=.json auto-upload")
114
 
115
- ui.button("Cancel", on_click=dialog.close).props("color='black'")
116
 
117
- dialog.open()
118
 
119
  def clear_graph_action(self) -> None:
120
  if self.canvas is None:
 
22
  with ui.column().classes("w-full h-screen no-wrap"):
23
  # header
24
  with ui.row().classes("w-full items-center justify-between px-4 py-2 bg-grey-2"):
 
25
  ui.image("assets/logo.png").classes("w-16").style("margin-right: 10px;")
26
  with ui.row().classes("items-center gap-2"):
27
+ # create Info dialog once in the page context
28
+ with ui.dialog() as info_dialog:
29
+ with ui.card():
30
+ ui.button(icon="close", on_click=info_dialog.close).props("color='black'").classes(
31
+ "absolute top-2 right-2")
32
+ ui.label("Welcome to VELAI!").classes("text-lg font-bold")
33
+ ui.label(
34
+ "This is a node-based environment for generating images.\n"
35
+ "It uses the Gemini 2.5 Flash Preview (Nano Banana) and Reve models."
36
+ )
37
+ ui.label(
38
+ "As it's a work in progress, there might be some bugs and issues. "
39
+ "Please report them to us :)"
40
+ )
41
+
42
+ ui.button("Info", on_click=info_dialog.open).props("color='black'")
43
 
 
 
 
 
 
 
 
 
 
44
  with ui.dropdown_button("Add Node", auto_close=True).props("color='black'"):
45
  kinds = session.creatable_node_types
46
  for k in kinds:
 
95
  session = self.session
96
  canvas = self.canvas
97
 
98
+ # create the dialog in the main page content context
99
+ with ui.context.client.content:
100
+ with ui.dialog() as dialog:
101
+ with ui.card():
102
+ ui.label("Upload graph JSON")
103
 
104
+ async def handle_upload(e: events.UploadEventArguments) -> None:
105
+ data = await e.file.read()
106
+ content = data.decode("utf-8")
107
 
108
+ session.load_from_json(content)
109
+ session.save_to_storage()
110
 
111
+ nodes = session.initial_vue_nodes()
112
+ edges = session.initial_vue_edges()
113
+ canvas.set_graph(nodes, edges)
114
 
115
+ dialog.close()
116
 
117
+ ui.upload(
118
+ label="Choose JSON file",
119
+ on_upload=handle_upload,
120
+ max_files=1,
121
+ ).props("accept=.json auto-upload")
122
 
123
+ ui.button("Cancel", on_click=dialog.close).props("color='black'")
124
 
125
+ dialog.open()
126
 
127
  def clear_graph_action(self) -> None:
128
  if self.canvas is None: