kratadata commited on
Commit
f10aa62
·
verified ·
1 Parent(s): 8611a2c

Upload folder via script

Browse files
velai/services/image/FalAIImageGenerator.py CHANGED
@@ -266,7 +266,7 @@ class FalAIKrea2TurboGenerator(FalAIImageGenerator):
266
 
267
  @register_service(ImageGenerator)
268
  class FalAINanoBananaGenerator(FalAITextAndEditImageGenerator):
269
- """fal-ai/nano-banana text and edit combination."""
270
 
271
  @classmethod
272
  def get_service_id(cls) -> str:
@@ -274,24 +274,24 @@ class FalAINanoBananaGenerator(FalAITextAndEditImageGenerator):
274
 
275
  def __init__(self, api_key: str | None = None) -> None:
276
  super().__init__(
277
- service_model_name="fal-ai/nano-banana",
278
  api_key=api_key,
279
- text_model="fal-ai/nano-banana",
280
- edit_model="fal-ai/nano-banana/edit",
281
  image_argument="image_urls",
282
  )
283
 
284
  @classmethod
285
  def get_name(cls) -> str:
286
- return "Nano Banana"
287
 
288
  @classmethod
289
  def default_model_name(cls) -> str:
290
- return "fal-ai/nano-banana"
291
 
292
  def _calculate_cost(self, model: str, input_data: ImageGenerationInput, response: dict[str, Any]) -> float | None:
293
- # text and image to image price is the same
294
- return 0.039
295
 
296
 
297
  class FalAIReveGenerator(FalAIImageGenerator):
 
266
 
267
  @register_service(ImageGenerator)
268
  class FalAINanoBananaGenerator(FalAITextAndEditImageGenerator):
269
+ """fal-ai/nano-banana-2 text and edit combination (Gemini 3.1 Flash Image)."""
270
 
271
  @classmethod
272
  def get_service_id(cls) -> str:
 
274
 
275
  def __init__(self, api_key: str | None = None) -> None:
276
  super().__init__(
277
+ service_model_name="fal-ai/nano-banana-2",
278
  api_key=api_key,
279
+ text_model="fal-ai/nano-banana-2",
280
+ edit_model="fal-ai/nano-banana-2/edit",
281
  image_argument="image_urls",
282
  )
283
 
284
  @classmethod
285
  def get_name(cls) -> str:
286
+ return "Nano Banana 2"
287
 
288
  @classmethod
289
  def default_model_name(cls) -> str:
290
+ return "fal-ai/nano-banana-2"
291
 
292
  def _calculate_cost(self, model: str, input_data: ImageGenerationInput, response: dict[str, Any]) -> float | None:
293
+ # 1K list price on fal; 2K/4K are higher.
294
+ return 0.08
295
 
296
 
297
  class FalAIReveGenerator(FalAIImageGenerator):
velai/session.py CHANGED
@@ -542,7 +542,11 @@ class GraphSession:
542
  def duplicate_nodes(
543
  self, source_node_ids: list[str], position: dict[str, Any] | None = None
544
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
545
- """Duplicate nodes and recreate connections within the copied selection."""
 
 
 
 
546
  source_ids = {node_id for node_id in source_node_ids if node_id}
547
  sources = [self.graph.nodes[node_id] for node_id in source_node_ids if node_id in self.graph.nodes]
548
  if not sources:
@@ -592,16 +596,23 @@ class GraphSession:
592
  vue_nodes.append(self.renderer.to_vue_node(dup_node))
593
 
594
  vue_edges: list[dict[str, Any]] = []
595
- for conn in self.graph.connections:
 
596
  src_id = conn.start_node.node_id
597
  tgt_id = conn.end_node.node_id
598
- if src_id not in source_ids or tgt_id not in source_ids:
599
  continue
600
- if src_id not in id_map or tgt_id not in id_map:
 
 
 
 
601
  continue
602
 
603
- new_start = self.graph.nodes[id_map[src_id]]
604
- new_end = self.graph.nodes[id_map[tgt_id]]
 
 
605
 
606
  start_port = new_start.outputs.get(conn.start_port.name) if new_start.outputs else None
607
  if start_port is None and new_start.inputs:
 
542
  def duplicate_nodes(
543
  self, source_node_ids: list[str], position: dict[str, Any] | None = None
544
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
545
+ """Duplicate nodes and recreate their connections.
546
+
547
+ Keeps edges between copied nodes, and also rewires edges that linked a
548
+ copied node to an uncopied neighbor onto the duplicate.
549
+ """
550
  source_ids = {node_id for node_id in source_node_ids if node_id}
551
  sources = [self.graph.nodes[node_id] for node_id in source_node_ids if node_id in self.graph.nodes]
552
  if not sources:
 
596
  vue_nodes.append(self.renderer.to_vue_node(dup_node))
597
 
598
  vue_edges: list[dict[str, Any]] = []
599
+ # Snapshot: we append while iterating.
600
+ for conn in list(self.graph.connections):
601
  src_id = conn.start_node.node_id
602
  tgt_id = conn.end_node.node_id
603
+ if src_id not in source_ids and tgt_id not in source_ids:
604
  continue
605
+
606
+ new_src_id = id_map.get(src_id, src_id)
607
+ new_tgt_id = id_map.get(tgt_id, tgt_id)
608
+ # Need at least one remapped endpoint (a duplicated node).
609
+ if new_src_id == src_id and new_tgt_id == tgt_id:
610
  continue
611
 
612
+ new_start = self.graph.nodes.get(new_src_id)
613
+ new_end = self.graph.nodes.get(new_tgt_id)
614
+ if new_start is None or new_end is None:
615
+ continue
616
 
617
  start_port = new_start.outputs.get(conn.start_port.name) if new_start.outputs else None
618
  if start_port is None and new_start.inputs:
velai/ui/training.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import os
 
5
  from typing import TYPE_CHECKING, List, Optional
6
 
7
  import fal_client
@@ -19,6 +20,15 @@ KREA_TRAINER_MODEL = "fal-ai/krea-2-trainer"
19
  MAX_ZIP_BYTES = MAX_DATASET_ZIP_MB * 1024 * 1024
20
 
21
 
 
 
 
 
 
 
 
 
 
22
  class TrainingDialog:
23
  def __init__(self, on_library_changed=None, app_ctx: "AppContext | None" = None) -> None:
24
  self.zip_url: Optional[str] = None
@@ -161,7 +171,7 @@ class TrainingDialog:
161
  with ui.row().classes("gap-1"):
162
  ui.button(
163
  icon="download",
164
- on_click=lambda e=entry: ui.download(e.url, f"{e.name}.safetensors"),
165
  ).props("flat dense")
166
  ui.button(
167
  icon="delete",
@@ -174,6 +184,16 @@ class TrainingDialog:
174
  if asyncio.iscoroutine(result):
175
  asyncio.create_task(result)
176
 
 
 
 
 
 
 
 
 
 
 
177
  def _delete_entry(self, entry: LoraEntry) -> None:
178
  delete_lora(self._user_storage(), entry.id)
179
  ui.notify(f"Removed {entry.name}", type="info")
@@ -337,13 +357,13 @@ class TrainingDialog:
337
  if not lora_remote:
338
  raise RuntimeError("Trainer did not return LoRA weights")
339
 
340
- original_name = lora_file.get("file_name") or "krea_lora.safetensors"
 
341
  stored_lora = await storage_endpoint.download_and_store_url(
342
  lora_remote,
343
- original_name=original_name,
344
  mime_type="application/octet-stream",
345
  )
346
- name = (self.lora_name.value or "My LoRA").strip()
347
  add_lora(
348
  self._user_storage(),
349
  name=name,
@@ -354,7 +374,9 @@ class TrainingDialog:
354
  )
355
 
356
  self.result_container.set_visibility(True)
357
- self.download_lora_btn.on_click(lambda: ui.download(stored_lora, f"{name}.safetensors"))
 
 
358
  if config_remote:
359
  self.download_config_btn.on_click(
360
  lambda: ui.download(config_remote, config_file.get("file_name") or "config.json")
@@ -363,6 +385,14 @@ class TrainingDialog:
363
  else:
364
  self.download_config_btn.disable()
365
 
 
 
 
 
 
 
 
 
366
  def open(self) -> None:
367
  self.refresh_library()
368
  self.dialog.open()
 
2
 
3
  import asyncio
4
  import os
5
+ import re
6
  from typing import TYPE_CHECKING, List, Optional
7
 
8
  import fal_client
 
20
  MAX_ZIP_BYTES = MAX_DATASET_ZIP_MB * 1024 * 1024
21
 
22
 
23
+ def lora_download_filename(name: str) -> str:
24
+ """Build a safe download filename from the user-facing LoRA name."""
25
+ base = (name or "lora").strip() or "lora"
26
+ base = re.sub(r"[^\w.\- ]+", "_", base).strip(" ._") or "lora"
27
+ if not base.lower().endswith(".safetensors"):
28
+ base = f"{base}.safetensors"
29
+ return base
30
+
31
+
32
  class TrainingDialog:
33
  def __init__(self, on_library_changed=None, app_ctx: "AppContext | None" = None) -> None:
34
  self.zip_url: Optional[str] = None
 
171
  with ui.row().classes("gap-1"):
172
  ui.button(
173
  icon="download",
174
+ on_click=lambda e=entry: asyncio.create_task(self._download_entry(e)),
175
  ).props("flat dense")
176
  ui.button(
177
  icon="delete",
 
184
  if asyncio.iscoroutine(result):
185
  asyncio.create_task(result)
186
 
187
+ async def _download_entry(self, entry: LoraEntry) -> None:
188
+ filename = lora_download_filename(entry.name)
189
+ try:
190
+ blob = await storage_endpoint.get_blob_by_url(entry.url)
191
+ data = await blob.read_bytes()
192
+ ui.download(data, filename, "application/octet-stream")
193
+ except Exception:
194
+ src = entry.fal_url if entry.fal_url.startswith("http") else entry.url
195
+ ui.download(src, filename, "application/octet-stream")
196
+
197
  def _delete_entry(self, entry: LoraEntry) -> None:
198
  delete_lora(self._user_storage(), entry.id)
199
  ui.notify(f"Removed {entry.name}", type="info")
 
357
  if not lora_remote:
358
  raise RuntimeError("Trainer did not return LoRA weights")
359
 
360
+ name = (self.lora_name.value or "My LoRA").strip()
361
+ filename = lora_download_filename(name)
362
  stored_lora = await storage_endpoint.download_and_store_url(
363
  lora_remote,
364
+ original_name=filename,
365
  mime_type="application/octet-stream",
366
  )
 
367
  add_lora(
368
  self._user_storage(),
369
  name=name,
 
374
  )
375
 
376
  self.result_container.set_visibility(True)
377
+ self.download_lora_btn.on_click(
378
+ lambda: asyncio.create_task(self._download_stored_lora(stored_lora, filename))
379
+ )
380
  if config_remote:
381
  self.download_config_btn.on_click(
382
  lambda: ui.download(config_remote, config_file.get("file_name") or "config.json")
 
385
  else:
386
  self.download_config_btn.disable()
387
 
388
+ async def _download_stored_lora(self, stored_url: str, filename: str) -> None:
389
+ try:
390
+ blob = await storage_endpoint.get_blob_by_url(stored_url)
391
+ data = await blob.read_bytes()
392
+ ui.download(data, filename, "application/octet-stream")
393
+ except Exception:
394
+ ui.download(stored_url, filename, "application/octet-stream")
395
+
396
  def open(self) -> None:
397
  self.refresh_library()
398
  self.dialog.open()