beznogim666 commited on
Commit
e96239f
·
verified ·
1 Parent(s): 240a726

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -42
app.py CHANGED
@@ -195,24 +195,64 @@ def _list_loras():
195
  return [LORA_NONE]
196
 
197
 
198
- def _add_lora_to_workflow(workflow, model_node_ref, lora_name, lora_strength):
199
- """Inserts a LoraLoaderModelOnly node between the diffusion model and the
200
- sampler when a LoRA is selected. Uses *ModelOnly* on purpose: the text
201
- encoder here (qwen3vl) is loaded separately via CLIPLoader and isn't part
202
- of the UNET checkpoint, so a regular LoraLoader (which also patches CLIP)
203
- would be reaching for a CLIP input that doesn't exist in this graph."""
204
- if not lora_name or lora_name == LORA_NONE:
205
- return model_node_ref
206
-
207
- workflow["20"] = {
208
- "class_type": "LoraLoaderModelOnly",
209
- "inputs": {
210
- "model": model_node_ref,
211
- "lora_name": lora_name,
212
- "strength_model": float(lora_strength),
213
- },
214
- }
215
- return ["20", 0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
 
218
  def _build_workflow(
@@ -227,6 +267,8 @@ def _build_workflow(
227
  scheduler,
228
  lora_name=LORA_NONE,
229
  lora_strength=1.0,
 
 
230
  ):
231
  workflow = {
232
  "1": {
@@ -285,7 +327,7 @@ def _build_workflow(
285
  "inputs": {"conditioning": ["2", 0]},
286
  }
287
 
288
- model_ref = _add_lora_to_workflow(workflow, ["1", 0], lora_name, lora_strength)
289
  workflow["5"]["inputs"]["model"] = model_ref
290
  return workflow
291
 
@@ -329,6 +371,8 @@ def _build_edit_workflow(
329
  denoise,
330
  lora_name=LORA_NONE,
331
  lora_strength=1.0,
 
 
332
  ):
333
  workflow = {
334
  "1": {
@@ -391,7 +435,7 @@ def _build_edit_workflow(
391
  "inputs": {"conditioning": ["2", 0]},
392
  }
393
 
394
- model_ref = _add_lora_to_workflow(workflow, ["1", 0], lora_name, lora_strength)
395
  workflow["5"]["inputs"]["model"] = model_ref
396
  return workflow
397
 
@@ -424,10 +468,6 @@ def _wait_for_history(prompt_id, timeout=900):
424
 
425
 
426
  def _load_output_image(history_item):
427
- """Downloads the generated PNG, extracts prompt/workflow from its metadata,
428
- packs them into WebP's EXIF 'Make' and 'ImageDescription' tags, saves the
429
- WebP file, and returns the path to it. This preserves workflow compatibility
430
- with custom DnD extractors and saves tons of network bandwidth."""
431
  outputs = history_item.get("outputs", {})
432
  for output in outputs.values():
433
  for image in output.get("images", []):
@@ -439,16 +479,13 @@ def _load_output_image(history_item):
439
  response = requests.get(f"{COMFY_URL}/view", params=params, timeout=120)
440
  response.raise_for_status()
441
 
442
- # 1. Save temporary PNG fetched from Comfy (with metadata inside)
443
  temp_png = Path("/tmp") / f"{uuid.uuid4().hex}.png"
444
  temp_png.write_bytes(response.content)
445
 
446
- # 2. Extract standard ComfyUI metadata
447
  img = Image.open(temp_png)
448
  prompt_data = img.info.get("prompt", "")
449
  workflow_data = img.info.get("workflow", "")
450
 
451
- # 3. Prepare EXIF payload for WebP matching your metadata ripper
452
  prompt_bytes = prompt_data.encode('utf-8') if isinstance(prompt_data, str) else prompt_data
453
  workflow_bytes = workflow_data.encode('utf-8') if isinstance(workflow_data, str) else workflow_data
454
 
@@ -460,14 +497,10 @@ def _load_output_image(history_item):
460
  }
461
  exif_bytes = piexif.dump(exif_dict)
462
 
463
- # 4. Save to compressed WebP with the EXIF payload
464
  temp_webp = Path("/tmp") / f"{uuid.uuid4().hex}.webp"
465
  img.convert("RGB").save(temp_webp, "WEBP", exif=exif_bytes, quality=90)
466
 
467
- # Cleanup temp PNG
468
  temp_png.unlink(missing_ok=True)
469
-
470
- # 5. Return filepath string so Gradio streams it byte-for-byte
471
  return str(temp_webp)
472
 
473
  raise RuntimeError("ComfyUI completed without returning an image.")
@@ -481,9 +514,6 @@ def _clamp_duration(value):
481
  return max(DURATION_MIN, min(DURATION_MAX, value))
482
 
483
 
484
- # spaces.GPU accepts a callable that receives the SAME positional args as the
485
- # decorated function and returns the requested duration. gpu_duration is just
486
- # the slider value passed straight through - no more 900s+megapixels guessing.
487
  def _duration(
488
  prompt,
489
  negative_prompt,
@@ -497,6 +527,9 @@ def _duration(
497
  scheduler,
498
  lora_name,
499
  lora_strength,
 
 
 
500
  gpu_duration,
501
  ):
502
  return _clamp_duration(gpu_duration)
@@ -517,6 +550,9 @@ def _edit_duration(
517
  scheduler,
518
  lora_name,
519
  lora_strength,
 
 
 
520
  gpu_duration,
521
  ):
522
  return _clamp_duration(gpu_duration)
@@ -536,6 +572,9 @@ def generate(
536
  scheduler="simple",
537
  lora_name=LORA_NONE,
538
  lora_strength=1.0,
 
 
 
539
  gpu_duration=DURATION_DEFAULT,
540
  ):
541
  if not prompt or not prompt.strip():
@@ -545,6 +584,10 @@ def generate(
545
 
546
  try:
547
  _start_comfyui()
 
 
 
 
548
  workflow = _build_workflow(
549
  prompt,
550
  negative_prompt,
@@ -557,6 +600,8 @@ def generate(
557
  scheduler,
558
  lora_name,
559
  lora_strength,
 
 
560
  )
561
  prompt_id = _queue_prompt(workflow)
562
  history_item = _wait_for_history(prompt_id)
@@ -581,6 +626,9 @@ def edit_image(
581
  scheduler="simple",
582
  lora_name=LORA_NONE,
583
  lora_strength=1.0,
 
 
 
584
  gpu_duration=DURATION_DEFAULT,
585
  ):
586
  if input_image is None:
@@ -592,6 +640,10 @@ def edit_image(
592
 
593
  try:
594
  _start_comfyui()
 
 
 
 
595
  resized = _resize_for_edit(input_image, width, height)
596
  input_filename = _upload_image_to_comfy(resized)
597
  workflow = _build_edit_workflow(
@@ -606,6 +658,8 @@ def edit_image(
606
  denoise,
607
  lora_name,
608
  lora_strength,
 
 
609
  )
610
  prompt_id = _queue_prompt(workflow)
611
  history_item = _wait_for_history(prompt_id)
@@ -615,8 +669,6 @@ def edit_image(
615
 
616
 
617
  def refresh_loras():
618
- """Button handler: re-scans models/loras (via ComfyUI if it's up, plain
619
- glob otherwise) and returns a Dropdown update with the fresh choices."""
620
  choices = _list_loras()
621
  return gr.update(choices=choices, value=LORA_NONE)
622
 
@@ -665,13 +717,20 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
665
  lora_name = gr.Dropdown(
666
  choices=[LORA_NONE, LORA_FILE],
667
  value=LORA_FILE,
668
- label="LoRA",
669
  info=LORA_INFO,
670
  allow_custom_value=True,
671
  scale=4,
672
  )
673
  lora_refresh = gr.Button("Refresh", scale=1)
674
- lora_strength = gr.Slider(0.0, 10.0, value=0.0, step=0.05, label="LoRA strength")
 
 
 
 
 
 
 
675
  gpu_duration = gr.Slider(
676
  DURATION_MIN,
677
  DURATION_MAX,
@@ -689,7 +748,6 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
689
  scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
690
  run = gr.Button("Generate", variant="primary")
691
  with gr.Column(scale=6):
692
- # We removed format="png" so Gradio accepts the raw WebP file path directly.
693
  output = gr.Image(label="Result", elem_id="result-image")
694
 
695
  inputs = [
@@ -705,6 +763,9 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
705
  scheduler,
706
  lora_name,
707
  lora_strength,
 
 
 
708
  gpu_duration,
709
  ]
710
  run.click(generate, inputs, [output, seed])
@@ -738,13 +799,20 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
738
  edit_lora_name = gr.Dropdown(
739
  choices=[LORA_NONE, LORA_FILE],
740
  value=LORA_FILE,
741
- label="LoRA",
742
  info=LORA_INFO,
743
  allow_custom_value=True,
744
  scale=4,
745
  )
746
  edit_lora_refresh = gr.Button("Refresh", scale=1)
747
- edit_lora_strength = gr.Slider(-2.0, 2.0, value=1.0, step=0.05, label="LoRA strength")
 
 
 
 
 
 
 
748
  edit_gpu_duration = gr.Slider(
749
  DURATION_MIN,
750
  DURATION_MAX,
@@ -762,7 +830,6 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
762
  edit_scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
763
  edit_run = gr.Button("Edit Image", variant="primary")
764
  with gr.Column(scale=6):
765
- # We removed format="png" here as well.
766
  edit_output = gr.Image(label="Edited image", elem_id="result-image")
767
 
768
  edit_inputs = [
@@ -780,6 +847,9 @@ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
780
  edit_scheduler,
781
  edit_lora_name,
782
  edit_lora_strength,
 
 
 
783
  edit_gpu_duration,
784
  ]
785
  edit_run.click(edit_image, edit_inputs, [edit_output, edit_seed])
 
195
  return [LORA_NONE]
196
 
197
 
198
+ def _download_dynamic_lora(repo, filename):
199
+ """Downloads a dynamic LoRA from HuggingFace on-the-fly inside the execution flow.
200
+ Returns the relative path from models/loras for ComfyUI loader."""
201
+ if not repo or not filename:
202
+ return None
203
+ repo = repo.strip()
204
+ filename = filename.strip()
205
+ if not repo or not filename:
206
+ return None
207
+
208
+ lora_dir = COMFY_DIR / "models" / "loras"
209
+ lora_dir.mkdir(parents=True, exist_ok=True)
210
+
211
+ try:
212
+ print(f"[setup] Downloading dynamic LoRA: {repo}/{filename}", flush=True)
213
+ local_path = hf_hub_download(
214
+ repo_id=repo,
215
+ filename=filename,
216
+ local_dir=str(lora_dir),
217
+ token=os.environ.get("HF_TOKEN"),
218
+ )
219
+ # Returns path relative to lora_dir (handles nested folders perfectly)
220
+ return str(Path(local_path).relative_to(lora_dir))
221
+ except Exception as e:
222
+ print(f"[error] Failed to download dynamic LoRA from {repo}/{filename}: {e}", flush=True)
223
+ raise RuntimeError(f"Failed to download dynamic LoRA: {e}")
224
+
225
+
226
+ def _add_loras_to_workflow(workflow, model_node_ref, lora_name, lora_strength, lora2_name=None, lora2_strength=0.0):
227
+ """Inserts one or two LoraLoaderModelOnly nodes sequentially between the
228
+ diffusion model and the sampler."""
229
+ current_model = model_node_ref
230
+
231
+ # First LoRA
232
+ if lora_name and lora_name != LORA_NONE and float(lora_strength) != 0.0:
233
+ workflow["20"] = {
234
+ "class_type": "LoraLoaderModelOnly",
235
+ "inputs": {
236
+ "model": current_model,
237
+ "lora_name": lora_name,
238
+ "strength_model": float(lora_strength),
239
+ },
240
+ }
241
+ current_model = ["20", 0]
242
+
243
+ # Second dynamic LoRA
244
+ if lora2_name and lora2_name != LORA_NONE and float(lora2_strength) != 0.0:
245
+ workflow["21"] = {
246
+ "class_type": "LoraLoaderModelOnly",
247
+ "inputs": {
248
+ "model": current_model,
249
+ "lora_name": lora2_name,
250
+ "strength_model": float(lora2_strength),
251
+ },
252
+ }
253
+ current_model = ["21", 0]
254
+
255
+ return current_model
256
 
257
 
258
  def _build_workflow(
 
267
  scheduler,
268
  lora_name=LORA_NONE,
269
  lora_strength=1.0,
270
+ lora2_name=None,
271
+ lora2_strength=0.0,
272
  ):
273
  workflow = {
274
  "1": {
 
327
  "inputs": {"conditioning": ["2", 0]},
328
  }
329
 
330
+ model_ref = _add_loras_to_workflow(workflow, ["1", 0], lora_name, lora_strength, lora2_name, lora2_strength)
331
  workflow["5"]["inputs"]["model"] = model_ref
332
  return workflow
333
 
 
371
  denoise,
372
  lora_name=LORA_NONE,
373
  lora_strength=1.0,
374
+ lora2_name=None,
375
+ lora2_strength=0.0,
376
  ):
377
  workflow = {
378
  "1": {
 
435
  "inputs": {"conditioning": ["2", 0]},
436
  }
437
 
438
+ model_ref = _add_loras_to_workflow(workflow, ["1", 0], lora_name, lora_strength, lora2_name, lora2_strength)
439
  workflow["5"]["inputs"]["model"] = model_ref
440
  return workflow
441
 
 
468
 
469
 
470
  def _load_output_image(history_item):
 
 
 
 
471
  outputs = history_item.get("outputs", {})
472
  for output in outputs.values():
473
  for image in output.get("images", []):
 
479
  response = requests.get(f"{COMFY_URL}/view", params=params, timeout=120)
480
  response.raise_for_status()
481
 
 
482
  temp_png = Path("/tmp") / f"{uuid.uuid4().hex}.png"
483
  temp_png.write_bytes(response.content)
484
 
 
485
  img = Image.open(temp_png)
486
  prompt_data = img.info.get("prompt", "")
487
  workflow_data = img.info.get("workflow", "")
488
 
 
489
  prompt_bytes = prompt_data.encode('utf-8') if isinstance(prompt_data, str) else prompt_data
490
  workflow_bytes = workflow_data.encode('utf-8') if isinstance(workflow_data, str) else workflow_data
491
 
 
497
  }
498
  exif_bytes = piexif.dump(exif_dict)
499
 
 
500
  temp_webp = Path("/tmp") / f"{uuid.uuid4().hex}.webp"
501
  img.convert("RGB").save(temp_webp, "WEBP", exif=exif_bytes, quality=90)
502
 
 
503
  temp_png.unlink(missing_ok=True)
 
 
504
  return str(temp_webp)
505
 
506
  raise RuntimeError("ComfyUI completed without returning an image.")
 
514
  return max(DURATION_MIN, min(DURATION_MAX, value))
515
 
516
 
 
 
 
517
  def _duration(
518
  prompt,
519
  negative_prompt,
 
527
  scheduler,
528
  lora_name,
529
  lora_strength,
530
+ lora2_repo,
531
+ lora2_file,
532
+ lora2_strength,
533
  gpu_duration,
534
  ):
535
  return _clamp_duration(gpu_duration)
 
550
  scheduler,
551
  lora_name,
552
  lora_strength,
553
+ edit_lora2_repo,
554
+ edit_lora2_file,
555
+ edit_lora2_strength,
556
  gpu_duration,
557
  ):
558
  return _clamp_duration(gpu_duration)
 
572
  scheduler="simple",
573
  lora_name=LORA_NONE,
574
  lora_strength=1.0,
575
+ lora2_repo="",
576
+ lora2_file="",
577
+ lora2_strength=0.0,
578
  gpu_duration=DURATION_DEFAULT,
579
  ):
580
  if not prompt or not prompt.strip():
 
584
 
585
  try:
586
  _start_comfyui()
587
+
588
+ # Download dynamic LoRA 2 if user filled details
589
+ lora2_name = _download_dynamic_lora(lora2_repo, lora2_file)
590
+
591
  workflow = _build_workflow(
592
  prompt,
593
  negative_prompt,
 
600
  scheduler,
601
  lora_name,
602
  lora_strength,
603
+ lora2_name,
604
+ lora2_strength,
605
  )
606
  prompt_id = _queue_prompt(workflow)
607
  history_item = _wait_for_history(prompt_id)
 
626
  scheduler="simple",
627
  lora_name=LORA_NONE,
628
  lora_strength=1.0,
629
+ edit_lora2_repo="",
630
+ edit_lora2_file="",
631
+ edit_lora2_strength=0.0,
632
  gpu_duration=DURATION_DEFAULT,
633
  ):
634
  if input_image is None:
 
640
 
641
  try:
642
  _start_comfyui()
643
+
644
+ # Download dynamic LoRA 2 if user filled details
645
+ lora2_name = _download_dynamic_lora(edit_lora2_repo, edit_lora2_file)
646
+
647
  resized = _resize_for_edit(input_image, width, height)
648
  input_filename = _upload_image_to_comfy(resized)
649
  workflow = _build_edit_workflow(
 
658
  denoise,
659
  lora_name,
660
  lora_strength,
661
+ lora2_name,
662
+ lora2_strength,
663
  )
664
  prompt_id = _queue_prompt(workflow)
665
  history_item = _wait_for_history(prompt_id)
 
669
 
670
 
671
  def refresh_loras():
 
 
672
  choices = _list_loras()
673
  return gr.update(choices=choices, value=LORA_NONE)
674
 
 
717
  lora_name = gr.Dropdown(
718
  choices=[LORA_NONE, LORA_FILE],
719
  value=LORA_FILE,
720
+ label="LoRA 1",
721
  info=LORA_INFO,
722
  allow_custom_value=True,
723
  scale=4,
724
  )
725
  lora_refresh = gr.Button("Refresh", scale=1)
726
+ lora_strength = gr.Slider(0.0, 10.0, value=0.0, step=0.05, label="LoRA 1 strength")
727
+
728
+ # Spoilered dynamic LoRA 2
729
+ with gr.Accordion("LoRA 2 (HuggingFace Dynamic)", open=False):
730
+ lora2_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="")
731
+ lora2_file = gr.Textbox(label="Filename", placeholder="e.g., lora2.safetensors", value="")
732
+ lora2_strength = gr.Slider(0.0, 10.0, value=0.0, step=0.05, label="LoRA 2 strength")
733
+
734
  gpu_duration = gr.Slider(
735
  DURATION_MIN,
736
  DURATION_MAX,
 
748
  scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
749
  run = gr.Button("Generate", variant="primary")
750
  with gr.Column(scale=6):
 
751
  output = gr.Image(label="Result", elem_id="result-image")
752
 
753
  inputs = [
 
763
  scheduler,
764
  lora_name,
765
  lora_strength,
766
+ lora2_repo,
767
+ lora2_file,
768
+ lora2_strength,
769
  gpu_duration,
770
  ]
771
  run.click(generate, inputs, [output, seed])
 
799
  edit_lora_name = gr.Dropdown(
800
  choices=[LORA_NONE, LORA_FILE],
801
  value=LORA_FILE,
802
+ label="LoRA 1",
803
  info=LORA_INFO,
804
  allow_custom_value=True,
805
  scale=4,
806
  )
807
  edit_lora_refresh = gr.Button("Refresh", scale=1)
808
+ edit_lora_strength = gr.Slider(-2.0, 2.0, value=1.0, step=0.05, label="LoRA 1 strength")
809
+
810
+ # Spoilered dynamic LoRA 2 for Edit Tab
811
+ with gr.Accordion("LoRA 2 (HuggingFace Dynamic)", open=False):
812
+ edit_lora2_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="")
813
+ edit_lora2_file = gr.Textbox(label="Filename", placeholder="e.g., lora2.safetensors", value="")
814
+ edit_lora2_strength = gr.Slider(-2.0, 2.0, value=0.0, step=0.05, label="LoRA 2 strength")
815
+
816
  edit_gpu_duration = gr.Slider(
817
  DURATION_MIN,
818
  DURATION_MAX,
 
830
  edit_scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
831
  edit_run = gr.Button("Edit Image", variant="primary")
832
  with gr.Column(scale=6):
 
833
  edit_output = gr.Image(label="Edited image", elem_id="result-image")
834
 
835
  edit_inputs = [
 
847
  edit_scheduler,
848
  edit_lora_name,
849
  edit_lora_strength,
850
+ edit_lora2_repo,
851
+ edit_lora2_file,
852
+ edit_lora2_strength,
853
  edit_gpu_duration,
854
  ]
855
  edit_run.click(edit_image, edit_inputs, [edit_output, edit_seed])