dima0987654321 commited on
Commit
caab6a8
·
verified ·
1 Parent(s): eb2d236

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -15
app.py CHANGED
@@ -52,10 +52,38 @@ MOTION_LORA_FILE = os.environ.get(
52
  )
53
  MOTION_LORA_SCALE = float(os.environ.get("H3_MOTION_LORA_SCALE", "1.0"))
54
 
55
- HMCUMSHOT_REPO = os.environ.get("H3_HMCUMSHOT_REPO", "Hearmeman/minimax-h3-loras")
56
- HMCUMSHOT_FILE = os.environ.get("H3_HMCUMSHOT_FILE", "HMCumshot_V1.0.safetensors")
57
- HMCUMSHOT_SCALE = float(os.environ.get("H3_HMCUMSHOT_SCALE", "1.0"))
58
- HMCUMSHOT_TRIGGER = os.environ.get("H3_HMCUMSHOT_TRIGGER", "cumshot")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
61
  # ``pack`` places the transformer at startup (66 GB on disk via the spaces pack); VAEs move on first GPU call.
@@ -313,7 +341,7 @@ def load_and_apply_lora(transformer) -> str:
313
  # These are folded after FlashGen, so the original 4-step FlashGen schedule and
314
  # all of its QKV/layout handling remain unchanged. The extra adapters use the
315
  # standard .lora_A.weight/.lora_B.weight export used by the motion-enhancer and
316
- # HMCumshot checkpoints.
317
 
318
  def _apply_extra_lora(
319
  transformer,
@@ -325,16 +353,123 @@ def _apply_extra_lora(
325
  from huggingface_hub import hf_hub_download
326
  from safetensors.torch import load_file
327
 
328
- path = hf_hub_download(repo_id, filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  lora = load_file(path, device="cpu")
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  suffix_a = ".lora_A.weight"
332
  suffix_b = ".lora_B.weight"
333
  bases = sorted(
334
  {key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)}
335
  )
336
  if not bases:
337
- raise ValueError(f"{label}: no lora_A/lora_B pairs found")
 
338
 
339
  params = dict(transformer.named_parameters())
340
  num_heads = transformer.config.num_attention_heads
@@ -354,7 +489,16 @@ def _apply_extra_lora(
354
  source_name = name.removeprefix("diffusion_model.")
355
  target_name = _lora_target_name(source_name)
356
 
357
- if target_name.endswith(".attn.qkv_proj"):
 
 
 
 
 
 
 
 
 
358
  prefix = target_name.removesuffix("qkv_proj")
359
  rows = _reorder_interleaved_qkv(b, num_heads, head_dim)
360
  targets = zip(
@@ -423,10 +567,24 @@ def load_and_apply_extra_loras(transformer) -> list[str]:
423
  ),
424
  _apply_extra_lora(
425
  transformer,
426
- HMCUMSHOT_REPO,
427
- HMCUMSHOT_FILE,
428
- "HMCumshot LoRA",
429
- HMCUMSHOT_SCALE,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  ),
431
  ]
432
 
@@ -462,7 +620,7 @@ def load_models() -> str | None:
462
  lora_status = load_and_apply_lora(pipe.transformer)
463
  print(f"[gen] {lora_status}", flush=True)
464
 
465
- # Then fold the requested Motion Enhancer and HMCumshot LoRAs into the
466
  # same transformer. They add their deltas on top of FlashGen.
467
  for extra_status in load_and_apply_extra_loras(pipe.transformer):
468
  print(f"[gen] {extra_status}", flush=True)
@@ -734,8 +892,10 @@ def generate(
734
  progress(0.0, desc="Enhancing the prompt, then encoding ..." if enhance_prompt else "Encoding prompt ...")
735
  conditioned = time.time()
736
  condition_prompt = prompt
737
- if HMCUMSHOT_TRIGGER.lower() not in condition_prompt.lower():
738
- condition_prompt = f"{HMCUMSHOT_TRIGGER}. {condition_prompt}"
 
 
739
 
740
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
741
  condition_prompt,
 
52
  )
53
  MOTION_LORA_SCALE = float(os.environ.get("H3_MOTION_LORA_SCALE", "1.0"))
54
 
55
+ # Realism / anatomy LoRAs. These are folded after FlashGen and the existing
56
+ # Motion Enhancer, using the same extra-LoRA merge path below.
57
+ REALISM_LORA_REPO = os.environ.get(
58
+ "H3_REALISM_LORA_REPO",
59
+ "fal/MiniMax-H3-Realism-People-LoRA",
60
+ )
61
+ REALISM_LORA_FILE = os.environ.get(
62
+ "H3_REALISM_LORA_FILE",
63
+ "h3-realism-people-t2v-i2v-r2v.safetensors",
64
+ )
65
+ REALISM_LORA_SCALE = float(os.environ.get("H3_REALISM_LORA_SCALE", "0.8"))
66
+ REALISM_LORA_TRIGGER = "r34l1sm"
67
+
68
+ # Polyhedron: Perfect Eyes / Perfect Skin / Perfect Hands. The Civitai model
69
+ # has multiple versions, so the loader resolves the current version/file at
70
+ # startup instead of hard-coding a version ID that may become stale.
71
+ DETAIL_LORA_REPO = os.environ.get("H3_DETAIL_LORA_REPO", "civitai-model://2899220")
72
+ DETAIL_LORA_FILE = os.environ.get(
73
+ "H3_DETAIL_LORA_FILE",
74
+ "polyhedron_minimax_h3_image_lora_v2__perfe8ct.safetensors",
75
+ )
76
+ DETAIL_LORA_SCALE = float(os.environ.get("H3_DETAIL_LORA_SCALE", "0.7"))
77
+
78
+ BLOWJOB_LORA_REPO = os.environ.get(
79
+ "H3_BLOWJOB_LORA_REPO",
80
+ "iamgroot1212/minimax-h3-loras",
81
+ )
82
+ BLOWJOB_LORA_FILE = os.environ.get(
83
+ "H3_BLOWJOB_LORA_FILE",
84
+ "MMH3-Hybrid-BlowJob-lora.safetensors",
85
+ )
86
+ BLOWJOB_LORA_SCALE = float(os.environ.get("H3_BLOWJOB_LORA_SCALE", "1.0"))
87
 
88
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
89
  # ``pack`` places the transformer at startup (66 GB on disk via the spaces pack); VAEs move on first GPU call.
 
341
  # These are folded after FlashGen, so the original 4-step FlashGen schedule and
342
  # all of its QKV/layout handling remain unchanged. The extra adapters use the
343
  # standard .lora_A.weight/.lora_B.weight export used by the motion-enhancer and
344
+ # additional H3 checkpoints.
345
 
346
  def _apply_extra_lora(
347
  transformer,
 
353
  from huggingface_hub import hf_hub_download
354
  from safetensors.torch import load_file
355
 
356
+ if repo_id.startswith("civitai-model://"):
357
+ # Resolve a Civitai model page to its newest model version, then use
358
+ # the same streaming downloader as the explicit civitai://version-id
359
+ # path. This keeps the Space independent of a hard-coded version ID.
360
+ import requests
361
+ model_id = repo_id.removeprefix("civitai-model://")
362
+ api_url = f"https://civitai.com/api/v1/models/{model_id}"
363
+ metadata = requests.get(api_url, timeout=30).json()
364
+ versions = metadata.get("modelVersions") or []
365
+ if not versions:
366
+ raise ValueError(f"{label}: Civitai model {model_id} has no model versions")
367
+ version = versions[0]
368
+ model_version_id = str(version["id"])
369
+ files = [f for f in (version.get("files") or []) if str(f.get("name", "")).lower().endswith(".safetensors")]
370
+ if not files:
371
+ raise ValueError(f"{label}: Civitai version {model_version_id} has no .safetensors file")
372
+ # Prefer the requested filename when it exists; otherwise use the first
373
+ # safetensors file exposed by the current version.
374
+ selected = next((f for f in files if f.get("name") == filename), files[0])
375
+ filename = os.path.basename(selected["name"])
376
+ download_url = selected.get("downloadUrl") or f"https://civitai.com/api/download/models/{model_version_id}"
377
+ cache_dir = os.path.join(tempfile.gettempdir(), "h3-lora-cache")
378
+ os.makedirs(cache_dir, exist_ok=True)
379
+ path = os.path.join(cache_dir, filename)
380
+ if not os.path.isfile(path) or os.path.getsize(path) == 0:
381
+ tmp_path = f"{path}.part"
382
+ with requests.get(download_url, stream=True, timeout=120, allow_redirects=True) as response:
383
+ response.raise_for_status()
384
+ with open(tmp_path, "wb") as handle:
385
+ for chunk in response.iter_content(chunk_size=8 * 1024 * 1024):
386
+ if chunk:
387
+ handle.write(chunk)
388
+ os.replace(tmp_path, path)
389
+ elif repo_id.startswith("civitai://"):
390
+ import requests
391
+ model_version_id = repo_id.removeprefix("civitai://")
392
+ download_url = f"https://civitai.com/api/download/models/{model_version_id}"
393
+ cache_dir = os.path.join(tempfile.gettempdir(), "h3-lora-cache")
394
+ os.makedirs(cache_dir, exist_ok=True)
395
+ path = os.path.join(cache_dir, filename)
396
+ if not os.path.isfile(path) or os.path.getsize(path) == 0:
397
+ tmp_path = f"{path}.part"
398
+ with requests.get(download_url, stream=True, timeout=120, allow_redirects=True) as response:
399
+ response.raise_for_status()
400
+ with open(tmp_path, "wb") as handle:
401
+ for chunk in response.iter_content(chunk_size=8 * 1024 * 1024):
402
+ if chunk:
403
+ handle.write(chunk)
404
+ os.replace(tmp_path, path)
405
+ elif repo_id == "aronzhan/H3-Loras-bucket":
406
+ import requests
407
+ bucket_url = f"https://huggingface.co/buckets/{repo_id}/resolve/{filename}"
408
+ cache_dir = os.path.join(tempfile.gettempdir(), "h3-lora-cache")
409
+ os.makedirs(cache_dir, exist_ok=True)
410
+ path = os.path.join(cache_dir, filename)
411
+ if not os.path.isfile(path) or os.path.getsize(path) == 0:
412
+ tmp_path = f"{path}.part"
413
+ with requests.get(bucket_url, stream=True, timeout=60, allow_redirects=True) as response:
414
+ response.raise_for_status()
415
+ with open(tmp_path, "wb") as handle:
416
+ for chunk in response.iter_content(chunk_size=8 * 1024 * 1024):
417
+ if chunk:
418
+ handle.write(chunk)
419
+ os.replace(tmp_path, path)
420
+ else:
421
+ path = hf_hub_download(repo_id, filename)
422
+
423
  lora = load_file(path, device="cpu")
424
 
425
+ normalized = {}
426
+ for key, tensor in lora.items():
427
+ k = key
428
+ if k.startswith("lora_unet__"):
429
+ k = k.replace("lora_unet__", "", 1)
430
+ k = k.replace("_blocks_", ".blocks.")
431
+ k = k.replace("_self_attn_", ".attn.")
432
+ k = k.replace("_ffn_net_0_proj", ".mlp.fc1")
433
+ k = k.replace("_ffn_net_2", ".mlp.fc2")
434
+ k = k.replace("to_out_0", "out_proj")
435
+ elif k.startswith("lora_unet_"):
436
+ body = k[len("lora_unet_"):]
437
+ parts = body.split(".")
438
+ head = parts[0].split("_")
439
+ out = []
440
+ i = 0
441
+ while i < len(head):
442
+ if head[i] == "blocks" and i + 1 < len(head) and head[i + 1].isdigit():
443
+ out += ["blocks", head[i + 1]]
444
+ i += 2
445
+ continue
446
+ if head[i] == "self" and i + 1 < len(head) and head[i + 1] == "attn":
447
+ out.append("attn")
448
+ i += 2
449
+ continue
450
+ out.append(head[i])
451
+ i += 1
452
+ k = ".".join(out + parts[1:])
453
+ for prefix in ("base_model.model.", "diffusion_model.", "transformer.", "pipe.dit."):
454
+ if k.startswith(prefix):
455
+ k = k[len(prefix):]
456
+ k = k.replace(".self_attn.", ".attn.")
457
+ k = k.replace(".ffn.0.", ".mlp.fc1.")
458
+ k = k.replace(".ffn.2.", ".mlp.fc2.")
459
+ k = k.replace(".lora_down.weight", ".lora_A.weight")
460
+ k = k.replace(".lora_up.weight", ".lora_B.weight")
461
+ k = k.replace(".lora_A.default.weight", ".lora_A.weight")
462
+ k = k.replace(".lora_B.default.weight", ".lora_B.weight")
463
+ normalized[k] = tensor
464
+ lora = normalized
465
  suffix_a = ".lora_A.weight"
466
  suffix_b = ".lora_B.weight"
467
  bases = sorted(
468
  {key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)}
469
  )
470
  if not bases:
471
+ sample = list(lora.keys())[:5]
472
+ raise ValueError(f"{label}: no lora_A/lora_B pairs found; keys: {sample}")
473
 
474
  params = dict(transformer.named_parameters())
475
  num_heads = transformer.config.num_attention_heads
 
489
  source_name = name.removeprefix("diffusion_model.")
490
  target_name = _lora_target_name(source_name)
491
 
492
+ # ComfyUI H3 LoRAs may store q/k/v as separate targets. The
493
+ # diffusers H3 transformer used by this Space has separate to_q/k/v
494
+ # parameters, so those can be folded directly.
495
+ if target_name.endswith((".attn.q", ".attn.k", ".attn.v")):
496
+ kind = target_name.rsplit(".", 1)[-1]
497
+ target_parts = [(
498
+ target_name.replace(f".attn.{kind}", f".attn.to_{kind}") + ".weight",
499
+ b,
500
+ )]
501
+ elif target_name.endswith(".attn.qkv_proj"):
502
  prefix = target_name.removesuffix("qkv_proj")
503
  rows = _reorder_interleaved_qkv(b, num_heads, head_dim)
504
  targets = zip(
 
567
  ),
568
  _apply_extra_lora(
569
  transformer,
570
+ REALISM_LORA_REPO,
571
+ REALISM_LORA_FILE,
572
+ "H3 Realism People LoRA",
573
+ REALISM_LORA_SCALE,
574
+ ),
575
+ _apply_extra_lora(
576
+ transformer,
577
+ DETAIL_LORA_REPO,
578
+ DETAIL_LORA_FILE,
579
+ "Perfect Eyes / Skin / Hands LoRA",
580
+ DETAIL_LORA_SCALE,
581
+ ),
582
+ _apply_extra_lora(
583
+ transformer,
584
+ BLOWJOB_LORA_REPO,
585
+ BLOWJOB_LORA_FILE,
586
+ "MiniMax H3 BlowJob LoRA",
587
+ BLOWJOB_LORA_SCALE,
588
  ),
589
  ]
590
 
 
620
  lora_status = load_and_apply_lora(pipe.transformer)
621
  print(f"[gen] {lora_status}", flush=True)
622
 
623
+ # Then fold the requested Motion Enhancer and BlowJob LoRA into the
624
  # same transformer. They add their deltas on top of FlashGen.
625
  for extra_status in load_and_apply_extra_loras(pipe.transformer):
626
  print(f"[gen] {extra_status}", flush=True)
 
892
  progress(0.0, desc="Enhancing the prompt, then encoding ..." if enhance_prompt else "Encoding prompt ...")
893
  conditioned = time.time()
894
  condition_prompt = prompt
895
+ # Realism People was trained with the `r34l1sm` trigger. Keep it hidden
896
+ # from the UI while ensuring the adapter receives its intended trigger.
897
+ if REALISM_LORA_TRIGGER.lower() not in condition_prompt.lower():
898
+ condition_prompt = f"{REALISM_LORA_TRIGGER}, {condition_prompt}"
899
 
900
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
901
  condition_prompt,