signsur4739379373 commited on
Commit
20a97b5
·
1 Parent(s): ab94d59

Add audio reference + per-lora audio strength via MultiLoRALoader

Browse files

Rewrite rgthree Power Lora Loader to phazei MultiLoRALoader in LTX mode so each lora has separate video / audio strength multipliers. Add LTXVReferenceAudio node injection with optional MelBandRoFormer stem separation for voice ID transfer. Exclude KV node from POS/NEG consumer redirect to avoid cycle when KV + audio_ref both enabled. Add rotary_embedding_torch dependency.

Files changed (2) hide show
  1. app.py +370 -82
  2. requirements.txt +1 -0
app.py CHANGED
@@ -91,6 +91,8 @@ CUSTOM_NODES = [
91
  ("ComfyUI-RMBG", "https://github.com/1038lab/ComfyUI-RMBG.git"),
92
  ("ComfyUI-PromptRelay", "https://github.com/kijai/ComfyUI-PromptRelay.git"),
93
  ("ComfyUI-FunPack", "https://github.com/digital-garbage/ComfyUI-FunPack.git"),
 
 
94
  ]
95
 
96
  # FunPackKVApply wrapper, written into comfy's custom_nodes at startup.
@@ -456,6 +458,12 @@ DOWNLOADS = [
456
  "dest": MODELS / "loras" / "ltx23" / "Singularity-LTX-2.3_OmniCine_V1nsf.safetensors",
457
  "label": "singularity lora",
458
  },
 
 
 
 
 
 
459
  ]
460
 
461
  SULPHUR_LORA_FILENAME = "ltx23/LTX_SulphurEXP_LoRA_fro99-avgrank105.safetensors"
@@ -524,6 +532,11 @@ NODE_LTXV_CONDITIONING = "523" # consumes positive from CLIPTextEncode 536
524
  # LTXVImgToVideoInplaceKJ pass 1 (node 772) slot 0. Disabled when MSR
525
  # mode is on (model chain already rewired).
526
  KV_NEW_NODE = "kv_apply"
 
 
 
 
 
527
  NODE_I2V_REF_LATENT = "772" # LTXVImgToVideoInplaceKJ pass 1, slot 0
528
 
529
  NODE_OUTPUT = "597"
@@ -1020,7 +1033,11 @@ def _ensure_comfy() -> None:
1020
  if _comfy_ready:
1021
  return
1022
 
1023
- _ensure_repo(COMFY, "https://github.com/comfyanonymous/ComfyUI.git")
 
 
 
 
1024
  _install_filtered_requirements(COMFY / "requirements.txt")
1025
 
1026
  custom_root = COMFY / "custom_nodes"
@@ -1040,6 +1057,7 @@ def _ensure_comfy() -> None:
1040
  "upscale_models",
1041
  "latent_upscale_models",
1042
  "vae",
 
1043
  ):
1044
  (MODELS / folder).mkdir(parents=True, exist_ok=True)
1045
  INPUT.mkdir(parents=True, exist_ok=True)
@@ -1267,30 +1285,17 @@ def _convert_workflow(visual_path: str) -> dict[str, Any]:
1267
 
1268
  widgets = node.get("widgets_values") or []
1269
  if class_type == "Power Lora Loader (rgthree)":
1270
- # rgthree stores loras as dict entries in a list; convert each into
1271
- # a lora_N keyed input which is what the node reads in API form.
1272
- # Normalize Windows backslashes in lora paths so workflows authored
1273
- # on Windows resolve to the right file on Linux (otherwise rgthree
1274
- # treats the backslash as a literal character in the filename and
1275
- # silently skips the lora).
1276
- # Skip OmniNFT entries from the template - we expose them via
1277
- # OPTIONAL_LORAS so users can tune strength / pick between the
1278
- # converted and the bf16 RL variant from the UI.
1279
- lora_idx = 0
1280
- for item in widgets if isinstance(widgets, list) else []:
1281
- if isinstance(item, dict) and "lora" in item and "strength" in item:
1282
- lora_path = item["lora"]
1283
- if isinstance(lora_path, str):
1284
- lora_path = lora_path.replace("\\", "/")
1285
- if isinstance(lora_path, str) and "omninft" in lora_path.lower():
1286
- continue
1287
- lora_idx += 1
1288
- inputs[f"lora_{lora_idx}"] = {
1289
- "on": bool(item.get("on", True)),
1290
- "lora": lora_path,
1291
- "strength": item.get("strength", 1.0),
1292
- "strengthTwo": item.get("strengthTwo"),
1293
- }
1294
  elif isinstance(widgets, dict):
1295
  for key, value in widgets.items():
1296
  if key != "videopreview":
@@ -1515,23 +1520,11 @@ def _convert_runexx_workflow(visual_path: str) -> dict[str, Any]:
1515
 
1516
  widgets = n.get("widgets_values") or []
1517
  if class_type == "Power Lora Loader (rgthree)":
1518
- # Same OmniNFT-skip rule as the primary converter so user-side
1519
- # OPTIONAL_LORAS sliders are the only authority on those entries.
1520
- lora_idx = 0
1521
- for item in widgets if isinstance(widgets, list) else []:
1522
- if isinstance(item, dict) and "lora" in item and "strength" in item:
1523
- lora_path = item["lora"]
1524
- if isinstance(lora_path, str):
1525
- lora_path = lora_path.replace("\\", "/")
1526
- if isinstance(lora_path, str) and "omninft" in lora_path.lower():
1527
- continue
1528
- lora_idx += 1
1529
- inputs[f"lora_{lora_idx}"] = {
1530
- "on": bool(item.get("on", True)),
1531
- "lora": lora_path,
1532
- "strength": item.get("strength", 1.0),
1533
- "strengthTwo": item.get("strengthTwo"),
1534
- }
1535
  elif isinstance(widgets, dict):
1536
  for key, value in widgets.items():
1537
  if key != "videopreview":
@@ -1655,6 +1648,17 @@ def _inject_params(
1655
  omninft_bf16_lora_strength: float = 0.0,
1656
  better_motion_lora_strength: float = 0.0,
1657
  physics_v2_lora_strength: float = 0.0,
 
 
 
 
 
 
 
 
 
 
 
1658
  cache_at_step: int = 0,
1659
  cache_warmup: int = 50,
1660
  energy_threshold: float = 0.3,
@@ -1673,6 +1677,10 @@ def _inject_params(
1673
  prompt_segments: str = "",
1674
  kv_enabled: bool = False,
1675
  kv_strength: float = 1.0,
 
 
 
 
1676
  ) -> dict[str, Any]:
1677
  # MSR (multi-reference) mode patches the workflow heavily - bypasses the
1678
  # likeness/anchor system, inserts IC-LoRA conditioning, adds crop guides
@@ -1711,6 +1719,16 @@ def _inject_params(
1711
  # which may be the relay node's output.
1712
  if kv_enabled and not msr_enabled:
1713
  _inject_kv_conditioning(workflow, strength=float(kv_strength))
 
 
 
 
 
 
 
 
 
 
1714
  # Refine-pass sigmas. original=workflow default. tuned=drops the 0.715
1715
  # high-sigma step. custom=validated upstream string.
1716
  _inject_refine_sigmas(workflow, _resolve_sigmas(sigma_preset, sigma_custom))
@@ -1723,19 +1741,35 @@ def _inject_params(
1723
  else:
1724
  resolved_cache_step = int(cache_at_step)
1725
  workflow[NODE_LOAD_IMAGE]["inputs"]["image"] = image_name
1726
- _inject_optional_loras(workflow, {
1727
- "lora_sulphur": sulphur_lora_strength,
1728
- "lora_sulphur_v1": sulphur_v1_lora_strength,
1729
- "lora_vbvr": vbvr_lora_strength,
1730
- "lora_dreamly": dreamly_lora_strength,
1731
- "lora_synth": synth_lora_strength,
1732
- "lora_plora": plora_lora_strength,
1733
- "lora_singularity": singularity_lora_strength,
1734
- "lora_omninft": omninft_lora_strength,
1735
- "lora_omninft_bf16": omninft_bf16_lora_strength,
1736
- "lora_better_motion": better_motion_lora_strength,
1737
- "lora_physics_v2": physics_v2_lora_strength,
1738
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1739
  workflow[NODE_POSITIVE]["inputs"]["text"] = prompt
1740
  workflow[NODE_NEGATIVE]["inputs"]["text"] = negative_prompt
1741
  workflow[NODE_SEED]["inputs"]["seed"] = seed
@@ -1873,26 +1907,43 @@ OPTIONAL_LORAS = {
1873
  }
1874
 
1875
 
1876
- def _inject_optional_loras(workflow: dict[str, Any], strengths: dict[str, float]) -> None:
1877
- """Add the optional loras to the rgthree power loader.
1878
-
1879
- Each optional lora is disabled (strength 0) by default and appended as an
1880
- extra lora_N entry only when a positive strength is requested. Idempotent.
 
 
 
 
 
 
 
 
 
1881
  """
1882
  node = workflow.get(NODE_POWER_LORA)
1883
  if node is None:
1884
  return
1885
- inputs = node["inputs"]
 
1886
  for key, filename in OPTIONAL_LORAS.items():
1887
- inputs.pop(key, None)
1888
- strength = strengths.get(key, 0.0)
1889
- if strength and float(strength) > 0:
1890
- inputs[key] = {
1891
- "on": True,
1892
- "lora": filename,
1893
- "strength": float(strength),
1894
- "strengthTwo": None,
1895
- }
 
 
 
 
 
 
 
1896
 
1897
 
1898
  def _validate_sigmas(s: str) -> str:
@@ -2340,6 +2391,92 @@ def _inject_kv_conditioning(workflow: dict[str, Any], strength: float = 1.0) ->
2340
  return True
2341
 
2342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2343
  def _safe_frames(seconds: float, fps: int = 24) -> int:
2344
  frames = max(9, int(seconds * fps) + 1)
2345
  return ((frames - 1 + 7) // 8) * 8 + 1
@@ -2426,6 +2563,17 @@ def get_gpu_duration(
2426
  omninft_bf16_lora_strength: float = 0.0,
2427
  better_motion_lora_strength: float = 0.0,
2428
  physics_v2_lora_strength: float = 0.0,
 
 
 
 
 
 
 
 
 
 
 
2429
  cache_at_step: int = 0,
2430
  cache_warmup: int = 50,
2431
  energy_threshold: float = 0.3,
@@ -2444,6 +2592,10 @@ def get_gpu_duration(
2444
  prompt_segments: str = "",
2445
  kv_enabled: bool = False,
2446
  kv_strength: float = 1.0,
 
 
 
 
2447
  progress: gr.Progress | None = None,
2448
  ) -> int:
2449
  # Manual override: gen_budget > 0 forces an exact GPU budget.
@@ -2456,6 +2608,13 @@ def get_gpu_duration(
2456
  mode_cost = 1.10 if mode != "anchor only" else 1.0
2457
  if input_mode == "multi-reference (MSR)":
2458
  mode_cost *= 1.10
 
 
 
 
 
 
 
2459
  # Two regimes: tight (30+5*work) lets default 4s fit the 120s/day free
2460
  # ZeroGPU allowance; anything longer falls back to the older wider
2461
  # formula (45+8*work) that's proven to complete on long gens.
@@ -2496,6 +2655,17 @@ def generate(
2496
  omninft_bf16_lora_strength: float = 0.0,
2497
  better_motion_lora_strength: float = 0.0,
2498
  physics_v2_lora_strength: float = 0.0,
 
 
 
 
 
 
 
 
 
 
 
2499
  cache_at_step: int = 0,
2500
  cache_warmup: int = 50,
2501
  energy_threshold: float = 0.3,
@@ -2514,6 +2684,10 @@ def generate(
2514
  prompt_segments: str = "",
2515
  kv_enabled: bool = False,
2516
  kv_strength: float = 1.0,
 
 
 
 
2517
  progress: gr.Progress = gr.Progress(track_tqdm=True),
2518
  ) -> tuple[str, str, int]:
2519
  seed_value = random.randint(0, 2**32 - 1) if randomize_seed or seed < 0 else int(seed)
@@ -2563,6 +2737,19 @@ def generate(
2563
  msr_ref4_name = _save_ref(msr_ref4, "ref4") if msr_enabled else None
2564
  msr_bg_name = _save_ref(msr_background, "bg") if any_msr else None
2565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2566
  if msr_original:
2567
  workflow = _inject_runexx_params(
2568
  _runexx_workflow_template(),
@@ -2605,6 +2792,17 @@ def generate(
2605
  omninft_bf16_lora_strength=omninft_bf16_lora_strength,
2606
  better_motion_lora_strength=better_motion_lora_strength,
2607
  physics_v2_lora_strength=physics_v2_lora_strength,
 
 
 
 
 
 
 
 
 
 
 
2608
  cache_at_step=int(cache_at_step),
2609
  cache_warmup=int(cache_warmup),
2610
  energy_threshold=float(energy_threshold),
@@ -2623,6 +2821,10 @@ def generate(
2623
  prompt_segments=str(prompt_segments or ""),
2624
  kv_enabled=bool(kv_enabled),
2625
  kv_strength=float(kv_strength),
 
 
 
 
2626
  )
2627
 
2628
  mode_label = " (MSR-original)" if msr_original else (" (MSR)" if msr_enabled else "")
@@ -2631,17 +2833,17 @@ def generate(
2631
  f"[gen] {width}x{height} {frames}f seed={seed_value} mode={mode} "
2632
  f"preset={preset} sigmas={sigma_preset} face={mode} "
2633
  f"kv={kv_enabled}@{kv_strength:.2f} "
2634
- f"sulphur_fro99={sulphur_lora_strength:.2f} "
2635
- f"sulphur_v1={sulphur_v1_lora_strength:.2f} "
2636
- f"vbvr={vbvr_lora_strength:.2f} "
2637
- f"dreamly={dreamly_lora_strength:.2f} "
2638
- f"synth={synth_lora_strength:.2f} "
2639
- f"plora={plora_lora_strength:.2f} "
2640
- f"singularity={singularity_lora_strength:.2f} "
2641
- f"omninft={omninft_lora_strength:.2f} "
2642
- f"omninft_bf16={omninft_bf16_lora_strength:.2f} "
2643
- f"better_motion={better_motion_lora_strength:.2f} "
2644
- f"physics_v2={physics_v2_lora_strength:.2f} "
2645
  f"likeness={likeness_strength:.2f} "
2646
  f"like_anchor={likeness_anchor_strength:.2f} "
2647
  f"lat_anchor={latent_anchor_strength:.2f} "
@@ -2649,7 +2851,10 @@ def generate(
2649
  f"anchor_sim={anchor_similarity_threshold:.2f} "
2650
  f"energy={energy_threshold:.2f} "
2651
  f"cache_step={cache_at_step} cache_warm={cache_warmup} "
2652
- f"relay={prompt_relay_enabled} input_mode={input_mode!r}",
 
 
 
2653
  flush=True,
2654
  )
2655
  result = _execute_workflow(workflow)
@@ -2855,6 +3060,74 @@ with gr.Blocks(title="10Eros LTX 2.3 image-to-video") as demo:
2855
  0.0, 2.0, value=1.0, step=0.05,
2856
  label="K/V strength (0 = off, 1 = funpack default, >1 = stronger identity)",
2857
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2858
  with gr.Accordion("multi-reference settings (MSR)", open=False, visible=False) as msr_settings_acc:
2859
  msr_frame_count = gr.Dropdown(
2860
  [17, 25, 33, 41], value=41,
@@ -2943,6 +3216,17 @@ with gr.Blocks(title="10Eros LTX 2.3 image-to-video") as demo:
2943
  omninft_bf16_lora_strength,
2944
  better_motion_lora_strength,
2945
  physics_v2_lora_strength,
 
 
 
 
 
 
 
 
 
 
 
2946
  cache_at_step,
2947
  cache_warmup,
2948
  energy_threshold,
@@ -2961,6 +3245,10 @@ with gr.Blocks(title="10Eros LTX 2.3 image-to-video") as demo:
2961
  prompt_segments,
2962
  kv_enabled,
2963
  kv_strength,
 
 
 
 
2964
  ],
2965
  outputs=[video, status, used_seed],
2966
  )
 
91
  ("ComfyUI-RMBG", "https://github.com/1038lab/ComfyUI-RMBG.git"),
92
  ("ComfyUI-PromptRelay", "https://github.com/kijai/ComfyUI-PromptRelay.git"),
93
  ("ComfyUI-FunPack", "https://github.com/digital-garbage/ComfyUI-FunPack.git"),
94
+ ("ComfyUI-MelBandRoFormer", "https://github.com/kijai/ComfyUI-MelBandRoFormer.git"),
95
+ ("ComfyUI-MultiLoRALoader", "https://github.com/phazei/ComfyUI-MultiLoRALoader.git"),
96
  ]
97
 
98
  # FunPackKVApply wrapper, written into comfy's custom_nodes at startup.
 
458
  "dest": MODELS / "loras" / "ltx23" / "Singularity-LTX-2.3_OmniCine_V1nsf.safetensors",
459
  "label": "singularity lora",
460
  },
461
+ {
462
+ "repo": "Kijai/MelBandRoFormer_comfy",
463
+ "file": "MelBandRoformer_fp16.safetensors",
464
+ "dest": MODELS / "diffusion_models" / "MelBandRoformer_fp16.safetensors",
465
+ "label": "mel band roformer (stem separation)",
466
+ },
467
  ]
468
 
469
  SULPHUR_LORA_FILENAME = "ltx23/LTX_SulphurEXP_LoRA_fro99-avgrank105.safetensors"
 
532
  # LTXVImgToVideoInplaceKJ pass 1 (node 772) slot 0. Disabled when MSR
533
  # mode is on (model chain already rewired).
534
  KV_NEW_NODE = "kv_apply"
535
+ NODE_AUDIO_VAE_LOADER = "617"
536
+ AUDIO_REF_NEW_LOAD = "audio_ref_load"
537
+ AUDIO_REF_NEW_MEL_LOADER = "audio_ref_mel_loader"
538
+ AUDIO_REF_NEW_MEL_SAMPLER = "audio_ref_mel_sampler"
539
+ AUDIO_REF_NEW_NODE = "audio_ref"
540
  NODE_I2V_REF_LATENT = "772" # LTXVImgToVideoInplaceKJ pass 1, slot 0
541
 
542
  NODE_OUTPUT = "597"
 
1033
  if _comfy_ready:
1034
  return
1035
 
1036
+ _ensure_repo(
1037
+ COMFY,
1038
+ "https://github.com/comfyanonymous/ComfyUI.git",
1039
+ commit="4e1f7cb1db1c26bb9ee61cf1875776517e2abae8",
1040
+ )
1041
  _install_filtered_requirements(COMFY / "requirements.txt")
1042
 
1043
  custom_root = COMFY / "custom_nodes"
 
1057
  "upscale_models",
1058
  "latent_upscale_models",
1059
  "vae",
1060
+ "diffusion_models",
1061
  ):
1062
  (MODELS / folder).mkdir(parents=True, exist_ok=True)
1063
  INPUT.mkdir(parents=True, exist_ok=True)
 
1285
 
1286
  widgets = node.get("widgets_values") or []
1287
  if class_type == "Power Lora Loader (rgthree)":
1288
+ # We rewrite rgthree's Power Lora Loader to phazei's MultiLoRALoader
1289
+ # in LTX mode so each lora has separate video/audio strength control
1290
+ # (Vid, V2A, Aud, A2V, Other per-tensor-pattern multipliers on top of
1291
+ # the global STR). Same output signature (model, clip), so downstream
1292
+ # connections work unchanged. Lora list lives in the lora_data JSON
1293
+ # string; _inject_optional_loras populates it later. OmniNFT entries
1294
+ # from the template are dropped here (exposed separately via the
1295
+ # OPTIONAL_LORAS sliders).
1296
+ class_type = "MultiLoRALoader"
1297
+ inputs["lora_data"] = "[]"
1298
+ inputs["ltx_mode"] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
  elif isinstance(widgets, dict):
1300
  for key, value in widgets.items():
1301
  if key != "videopreview":
 
1520
 
1521
  widgets = n.get("widgets_values") or []
1522
  if class_type == "Power Lora Loader (rgthree)":
1523
+ # Same rewrite as the primary converter: rgthree -> MultiLoRALoader
1524
+ # in LTX mode for per-modality strength control.
1525
+ class_type = "MultiLoRALoader"
1526
+ inputs["lora_data"] = "[]"
1527
+ inputs["ltx_mode"] = True
 
 
 
 
 
 
 
 
 
 
 
 
1528
  elif isinstance(widgets, dict):
1529
  for key, value in widgets.items():
1530
  if key != "videopreview":
 
1648
  omninft_bf16_lora_strength: float = 0.0,
1649
  better_motion_lora_strength: float = 0.0,
1650
  physics_v2_lora_strength: float = 0.0,
1651
+ sulphur_audio_strength: float = 0.15,
1652
+ sulphur_v1_audio_strength: float = 0.15,
1653
+ vbvr_audio_strength: float = 0.5,
1654
+ dreamly_audio_strength: float = 0.6,
1655
+ synth_audio_strength: float = 0.0,
1656
+ plora_audio_strength: float = 0.0,
1657
+ singularity_audio_strength: float = 0.3,
1658
+ omninft_audio_strength: float = 0.8,
1659
+ omninft_bf16_audio_strength: float = 0.0,
1660
+ better_motion_audio_strength: float = 0.0,
1661
+ physics_v2_audio_strength: float = 0.0,
1662
  cache_at_step: int = 0,
1663
  cache_warmup: int = 50,
1664
  energy_threshold: float = 0.3,
 
1677
  prompt_segments: str = "",
1678
  kv_enabled: bool = False,
1679
  kv_strength: float = 1.0,
1680
+ audio_ref_enabled: bool = False,
1681
+ audio_ref_filename: str | None = None,
1682
+ audio_ref_guidance_scale: float = 3.0,
1683
+ audio_ref_stem_sep: bool = False,
1684
  ) -> dict[str, Any]:
1685
  # MSR (multi-reference) mode patches the workflow heavily - bypasses the
1686
  # likeness/anchor system, inserts IC-LoRA conditioning, adds crop guides
 
1719
  # which may be the relay node's output.
1720
  if kv_enabled and not msr_enabled:
1721
  _inject_kv_conditioning(workflow, strength=float(kv_strength))
1722
+ # Audio reference: voice ID transfer. Splices LTXVReferenceAudio between
1723
+ # PowerLora and downstream, also patching conditioning. Disabled in MSR
1724
+ # mode (heavily-rewired chain) and skipped if no audio uploaded.
1725
+ if (audio_ref_enabled and audio_ref_filename and not msr_enabled):
1726
+ _inject_audio_reference(
1727
+ workflow,
1728
+ audio_filename=audio_ref_filename,
1729
+ guidance_scale=float(audio_ref_guidance_scale),
1730
+ stem_sep=bool(audio_ref_stem_sep),
1731
+ )
1732
  # Refine-pass sigmas. original=workflow default. tuned=drops the 0.715
1733
  # high-sigma step. custom=validated upstream string.
1734
  _inject_refine_sigmas(workflow, _resolve_sigmas(sigma_preset, sigma_custom))
 
1741
  else:
1742
  resolved_cache_step = int(cache_at_step)
1743
  workflow[NODE_LOAD_IMAGE]["inputs"]["image"] = image_name
1744
+ _inject_optional_loras(
1745
+ workflow,
1746
+ video_strengths={
1747
+ "lora_sulphur": sulphur_lora_strength,
1748
+ "lora_sulphur_v1": sulphur_v1_lora_strength,
1749
+ "lora_vbvr": vbvr_lora_strength,
1750
+ "lora_dreamly": dreamly_lora_strength,
1751
+ "lora_synth": synth_lora_strength,
1752
+ "lora_plora": plora_lora_strength,
1753
+ "lora_singularity": singularity_lora_strength,
1754
+ "lora_omninft": omninft_lora_strength,
1755
+ "lora_omninft_bf16": omninft_bf16_lora_strength,
1756
+ "lora_better_motion": better_motion_lora_strength,
1757
+ "lora_physics_v2": physics_v2_lora_strength,
1758
+ },
1759
+ audio_strengths={
1760
+ "lora_sulphur": sulphur_audio_strength,
1761
+ "lora_sulphur_v1": sulphur_v1_audio_strength,
1762
+ "lora_vbvr": vbvr_audio_strength,
1763
+ "lora_dreamly": dreamly_audio_strength,
1764
+ "lora_synth": synth_audio_strength,
1765
+ "lora_plora": plora_audio_strength,
1766
+ "lora_singularity": singularity_audio_strength,
1767
+ "lora_omninft": omninft_audio_strength,
1768
+ "lora_omninft_bf16": omninft_bf16_audio_strength,
1769
+ "lora_better_motion": better_motion_audio_strength,
1770
+ "lora_physics_v2": physics_v2_audio_strength,
1771
+ },
1772
+ )
1773
  workflow[NODE_POSITIVE]["inputs"]["text"] = prompt
1774
  workflow[NODE_NEGATIVE]["inputs"]["text"] = negative_prompt
1775
  workflow[NODE_SEED]["inputs"]["seed"] = seed
 
1907
  }
1908
 
1909
 
1910
+ def _inject_optional_loras(
1911
+ workflow: dict[str, Any],
1912
+ video_strengths: dict[str, float],
1913
+ audio_strengths: dict[str, float] | None = None,
1914
+ ) -> None:
1915
+ """Populate the MultiLoRALoader's lora_data JSON string.
1916
+
1917
+ LTX-mode entry format (per phazei's dispatch): per-key alpha is multiplied
1918
+ by the modality factor matching the tensor name pattern, then the global
1919
+ `str` applies on top. vid covers main video attn/ff.net tensors, aud covers
1920
+ audio_attn / audio_ff.net, v2a / a2v cover cross-modal attn. Setting aud
1921
+ independent of vid lets a non-audio-trained lora influence video without
1922
+ distorting the audio stream. Disabled (skipped) when video_strength <= 0
1923
+ and audio_strength <= 0. Idempotent.
1924
  """
1925
  node = workflow.get(NODE_POWER_LORA)
1926
  if node is None:
1927
  return
1928
+ audio_strengths = audio_strengths or {}
1929
+ entries: list[dict[str, Any]] = []
1930
  for key, filename in OPTIONAL_LORAS.items():
1931
+ vid = float(video_strengths.get(key, 0.0) or 0.0)
1932
+ aud = float(audio_strengths.get(key, vid) or 0.0)
1933
+ if vid <= 0 and aud <= 0:
1934
+ continue
1935
+ entries.append({
1936
+ "lora": filename,
1937
+ "on": True,
1938
+ "str": 1.0,
1939
+ "vid": vid,
1940
+ "v2a": vid,
1941
+ "aud": aud,
1942
+ "a2v": vid,
1943
+ "other": vid,
1944
+ })
1945
+ node["inputs"]["lora_data"] = json.dumps(entries)
1946
+ node["inputs"]["ltx_mode"] = True
1947
 
1948
 
1949
  def _validate_sigmas(s: str) -> str:
 
2391
  return True
2392
 
2393
 
2394
+ def _inject_audio_reference(
2395
+ workflow: dict[str, Any],
2396
+ audio_filename: str,
2397
+ guidance_scale: float = 3.0,
2398
+ stem_sep: bool = False,
2399
+ ) -> bool:
2400
+ """Splice an LTXVReferenceAudio node between Power Lora Loader and its
2401
+ downstream model consumers, also patching the positive/negative
2402
+ conditioning chain. The node encodes the ref audio via the existing
2403
+ LTXVAudioVAELoader (617), patches model with identity guidance, and
2404
+ routes through patched conditioning.
2405
+
2406
+ When stem_sep=True we run the ref through MelBandRoFormer first to
2407
+ isolate vocals from background music/noise. Vocals (slot 0) feed
2408
+ LTXVReferenceAudio.reference_audio.
2409
+
2410
+ Returns True on success, False if required upstream nodes are absent.
2411
+ """
2412
+ required = (NODE_POWER_LORA, NODE_POSITIVE, NODE_NEGATIVE, NODE_AUDIO_VAE_LOADER)
2413
+ if not all(nid in workflow for nid in required):
2414
+ return False
2415
+ power_loader = workflow[NODE_POWER_LORA]
2416
+ upstream_model = power_loader["inputs"].get("model")
2417
+ if upstream_model is None:
2418
+ return False
2419
+
2420
+ # LoadAudio reads from comfy's INPUT dir by filename.
2421
+ workflow[AUDIO_REF_NEW_LOAD] = {
2422
+ "class_type": "LoadAudio",
2423
+ "inputs": {"audio": audio_filename},
2424
+ }
2425
+ ref_audio_source: list = [AUDIO_REF_NEW_LOAD, 0]
2426
+
2427
+ if stem_sep:
2428
+ # MelBandRoFormer separates vocals from instruments.
2429
+ # Model loaded from models/diffusion_models/.
2430
+ workflow[AUDIO_REF_NEW_MEL_LOADER] = {
2431
+ "class_type": "MelBandRoFormerModelLoader",
2432
+ "inputs": {"model_name": "MelBandRoformer_fp16.safetensors"},
2433
+ }
2434
+ workflow[AUDIO_REF_NEW_MEL_SAMPLER] = {
2435
+ "class_type": "MelBandRoFormerSampler",
2436
+ "inputs": {
2437
+ "model": [AUDIO_REF_NEW_MEL_LOADER, 0],
2438
+ "audio": [AUDIO_REF_NEW_LOAD, 0],
2439
+ },
2440
+ }
2441
+ ref_audio_source = [AUDIO_REF_NEW_MEL_SAMPLER, 0] # vocals
2442
+
2443
+ # LTXVReferenceAudio patches model + conditioning.
2444
+ workflow[AUDIO_REF_NEW_NODE] = {
2445
+ "class_type": "LTXVReferenceAudio",
2446
+ "inputs": {
2447
+ "model": list(upstream_model) if isinstance(upstream_model, list) else upstream_model,
2448
+ "positive": [NODE_POSITIVE, 0],
2449
+ "negative": [NODE_NEGATIVE, 0],
2450
+ "reference_audio": ref_audio_source,
2451
+ "audio_vae": [NODE_AUDIO_VAE_LOADER, 0],
2452
+ "identity_guidance_scale": float(guidance_scale),
2453
+ "start_percent": 0.0,
2454
+ "end_percent": 1.0,
2455
+ },
2456
+ }
2457
+
2458
+ # Route Power Lora's model through the audio-ref-patched model.
2459
+ power_loader["inputs"]["model"] = [AUDIO_REF_NEW_NODE, 0]
2460
+
2461
+ # Reroute downstream conditioning consumers through patched outputs.
2462
+ # Slot 1 = patched positive, slot 2 = patched negative.
2463
+ # Exclude AUDIO_REF_NEW_NODE itself (self-reference) and KV_NEW_NODE
2464
+ # (KV reads raw POSITIVE as context-only signal; redirecting would
2465
+ # create a cycle since AUDIO_REF.model = [KV_NEW, 0]).
2466
+ exclude = {AUDIO_REF_NEW_NODE}
2467
+ if KV_NEW_NODE in workflow:
2468
+ exclude.add(KV_NEW_NODE)
2469
+ _redirect_consumers(
2470
+ workflow, [NODE_POSITIVE, 0], [AUDIO_REF_NEW_NODE, 1],
2471
+ exclude_node_ids=exclude,
2472
+ )
2473
+ _redirect_consumers(
2474
+ workflow, [NODE_NEGATIVE, 0], [AUDIO_REF_NEW_NODE, 2],
2475
+ exclude_node_ids=exclude,
2476
+ )
2477
+ return True
2478
+
2479
+
2480
  def _safe_frames(seconds: float, fps: int = 24) -> int:
2481
  frames = max(9, int(seconds * fps) + 1)
2482
  return ((frames - 1 + 7) // 8) * 8 + 1
 
2563
  omninft_bf16_lora_strength: float = 0.0,
2564
  better_motion_lora_strength: float = 0.0,
2565
  physics_v2_lora_strength: float = 0.0,
2566
+ sulphur_audio_strength: float = 0.15,
2567
+ sulphur_v1_audio_strength: float = 0.15,
2568
+ vbvr_audio_strength: float = 0.5,
2569
+ dreamly_audio_strength: float = 0.6,
2570
+ synth_audio_strength: float = 0.0,
2571
+ plora_audio_strength: float = 0.0,
2572
+ singularity_audio_strength: float = 0.3,
2573
+ omninft_audio_strength: float = 0.8,
2574
+ omninft_bf16_audio_strength: float = 0.0,
2575
+ better_motion_audio_strength: float = 0.0,
2576
+ physics_v2_audio_strength: float = 0.0,
2577
  cache_at_step: int = 0,
2578
  cache_warmup: int = 50,
2579
  energy_threshold: float = 0.3,
 
2592
  prompt_segments: str = "",
2593
  kv_enabled: bool = False,
2594
  kv_strength: float = 1.0,
2595
+ audio_ref_enabled: bool = False,
2596
+ audio_ref_file: str | None = None,
2597
+ audio_ref_guidance_scale: float = 3.0,
2598
+ audio_ref_stem_sep: bool = False,
2599
  progress: gr.Progress | None = None,
2600
  ) -> int:
2601
  # Manual override: gen_budget > 0 forces an exact GPU budget.
 
2608
  mode_cost = 1.10 if mode != "anchor only" else 1.0
2609
  if input_mode == "multi-reference (MSR)":
2610
  mode_cost *= 1.10
2611
+ if audio_ref_enabled and audio_ref_file and audio_ref_guidance_scale > 0:
2612
+ # Identity guidance does an extra forward pass per step during the
2613
+ # active sigma range - roughly 60% slowdown at default scale=3.0.
2614
+ mode_cost *= 1.60
2615
+ elif audio_ref_enabled and audio_ref_file:
2616
+ # ref tokens still encoded + attended in conditioning, ~10% bump.
2617
+ mode_cost *= 1.10
2618
  # Two regimes: tight (30+5*work) lets default 4s fit the 120s/day free
2619
  # ZeroGPU allowance; anything longer falls back to the older wider
2620
  # formula (45+8*work) that's proven to complete on long gens.
 
2655
  omninft_bf16_lora_strength: float = 0.0,
2656
  better_motion_lora_strength: float = 0.0,
2657
  physics_v2_lora_strength: float = 0.0,
2658
+ sulphur_audio_strength: float = 0.15,
2659
+ sulphur_v1_audio_strength: float = 0.15,
2660
+ vbvr_audio_strength: float = 0.5,
2661
+ dreamly_audio_strength: float = 0.6,
2662
+ synth_audio_strength: float = 0.0,
2663
+ plora_audio_strength: float = 0.0,
2664
+ singularity_audio_strength: float = 0.3,
2665
+ omninft_audio_strength: float = 0.8,
2666
+ omninft_bf16_audio_strength: float = 0.0,
2667
+ better_motion_audio_strength: float = 0.0,
2668
+ physics_v2_audio_strength: float = 0.0,
2669
  cache_at_step: int = 0,
2670
  cache_warmup: int = 50,
2671
  energy_threshold: float = 0.3,
 
2684
  prompt_segments: str = "",
2685
  kv_enabled: bool = False,
2686
  kv_strength: float = 1.0,
2687
+ audio_ref_enabled: bool = False,
2688
+ audio_ref_file: str | None = None,
2689
+ audio_ref_guidance_scale: float = 3.0,
2690
+ audio_ref_stem_sep: bool = False,
2691
  progress: gr.Progress = gr.Progress(track_tqdm=True),
2692
  ) -> tuple[str, str, int]:
2693
  seed_value = random.randint(0, 2**32 - 1) if randomize_seed or seed < 0 else int(seed)
 
2737
  msr_ref4_name = _save_ref(msr_ref4, "ref4") if msr_enabled else None
2738
  msr_bg_name = _save_ref(msr_background, "bg") if any_msr else None
2739
 
2740
+ # Copy audio reference into comfy's INPUT dir so LoadAudio can find it.
2741
+ audio_ref_name: str | None = None
2742
+ if audio_ref_enabled and audio_ref_file:
2743
+ try:
2744
+ src = pathlib.Path(audio_ref_file)
2745
+ if src.exists():
2746
+ ext = src.suffix.lower() or ".wav"
2747
+ audio_ref_name = f"input_audio_{uuid.uuid4().hex[:10]}{ext}"
2748
+ shutil.copy2(src, INPUT / audio_ref_name)
2749
+ except Exception as e:
2750
+ print(f"[audio_ref] failed to copy: {e}", flush=True)
2751
+ audio_ref_name = None
2752
+
2753
  if msr_original:
2754
  workflow = _inject_runexx_params(
2755
  _runexx_workflow_template(),
 
2792
  omninft_bf16_lora_strength=omninft_bf16_lora_strength,
2793
  better_motion_lora_strength=better_motion_lora_strength,
2794
  physics_v2_lora_strength=physics_v2_lora_strength,
2795
+ sulphur_audio_strength=sulphur_audio_strength,
2796
+ sulphur_v1_audio_strength=sulphur_v1_audio_strength,
2797
+ vbvr_audio_strength=vbvr_audio_strength,
2798
+ dreamly_audio_strength=dreamly_audio_strength,
2799
+ synth_audio_strength=synth_audio_strength,
2800
+ plora_audio_strength=plora_audio_strength,
2801
+ singularity_audio_strength=singularity_audio_strength,
2802
+ omninft_audio_strength=omninft_audio_strength,
2803
+ omninft_bf16_audio_strength=omninft_bf16_audio_strength,
2804
+ better_motion_audio_strength=better_motion_audio_strength,
2805
+ physics_v2_audio_strength=physics_v2_audio_strength,
2806
  cache_at_step=int(cache_at_step),
2807
  cache_warmup=int(cache_warmup),
2808
  energy_threshold=float(energy_threshold),
 
2821
  prompt_segments=str(prompt_segments or ""),
2822
  kv_enabled=bool(kv_enabled),
2823
  kv_strength=float(kv_strength),
2824
+ audio_ref_enabled=bool(audio_ref_enabled),
2825
+ audio_ref_filename=audio_ref_name,
2826
+ audio_ref_guidance_scale=float(audio_ref_guidance_scale),
2827
+ audio_ref_stem_sep=bool(audio_ref_stem_sep),
2828
  )
2829
 
2830
  mode_label = " (MSR-original)" if msr_original else (" (MSR)" if msr_enabled else "")
 
2833
  f"[gen] {width}x{height} {frames}f seed={seed_value} mode={mode} "
2834
  f"preset={preset} sigmas={sigma_preset} face={mode} "
2835
  f"kv={kv_enabled}@{kv_strength:.2f} "
2836
+ f"sulphur_fro99={sulphur_lora_strength:.2f}/{sulphur_audio_strength:.2f} "
2837
+ f"sulphur_v1={sulphur_v1_lora_strength:.2f}/{sulphur_v1_audio_strength:.2f} "
2838
+ f"vbvr={vbvr_lora_strength:.2f}/{vbvr_audio_strength:.2f} "
2839
+ f"dreamly={dreamly_lora_strength:.2f}/{dreamly_audio_strength:.2f} "
2840
+ f"synth={synth_lora_strength:.2f}/{synth_audio_strength:.2f} "
2841
+ f"plora={plora_lora_strength:.2f}/{plora_audio_strength:.2f} "
2842
+ f"singularity={singularity_lora_strength:.2f}/{singularity_audio_strength:.2f} "
2843
+ f"omninft={omninft_lora_strength:.2f}/{omninft_audio_strength:.2f} "
2844
+ f"omninft_bf16={omninft_bf16_lora_strength:.2f}/{omninft_bf16_audio_strength:.2f} "
2845
+ f"better_motion={better_motion_lora_strength:.2f}/{better_motion_audio_strength:.2f} "
2846
+ f"physics_v2={physics_v2_lora_strength:.2f}/{physics_v2_audio_strength:.2f} "
2847
  f"likeness={likeness_strength:.2f} "
2848
  f"like_anchor={likeness_anchor_strength:.2f} "
2849
  f"lat_anchor={latent_anchor_strength:.2f} "
 
2851
  f"anchor_sim={anchor_similarity_threshold:.2f} "
2852
  f"energy={energy_threshold:.2f} "
2853
  f"cache_step={cache_at_step} cache_warm={cache_warmup} "
2854
+ f"relay={prompt_relay_enabled} input_mode={input_mode!r} "
2855
+ f"audio_ref={audio_ref_enabled}@{audio_ref_guidance_scale:.1f} "
2856
+ f"audio_stem_sep={audio_ref_stem_sep} "
2857
+ f"audio_file={bool(audio_ref_name)}",
2858
  flush=True,
2859
  )
2860
  result = _execute_workflow(workflow)
 
3060
  0.0, 2.0, value=1.0, step=0.05,
3061
  label="K/V strength (0 = off, 1 = funpack default, >1 = stronger identity)",
3062
  )
3063
+ with gr.Accordion("audio", open=False):
3064
+ audio_ref_enabled = gr.Checkbox(
3065
+ value=False,
3066
+ label="audio reference (voice ID transfer)",
3067
+ )
3068
+ audio_ref_guidance_scale = gr.Slider(
3069
+ 0.0, 10.0, value=3.0, step=0.1,
3070
+ label="identity guidance scale (0 = no extra pass, 3 = default, higher = stronger voice ID)",
3071
+ )
3072
+ audio_ref_stem_sep = gr.Checkbox(
3073
+ value=False,
3074
+ label="isolate voice from background (stem separation, slower)",
3075
+ )
3076
+ audio_ref_file = gr.Audio(
3077
+ type="filepath",
3078
+ label="audio reference (~5s clip recommended)",
3079
+ )
3080
+ with gr.Accordion("per-lora audio strength (advanced)", open=False):
3081
+ gr.Markdown(
3082
+ "controls how each lora affects the **audio** stream "
3083
+ "(loras default to applying equally to video + audio). "
3084
+ "set to 0 to stop a lora from influencing audio while "
3085
+ "keeping its video effect."
3086
+ )
3087
+ sulphur_audio_strength = gr.Slider(
3088
+ 0.0, 1.0, value=0.15, step=0.05,
3089
+ label="sulphur fro99 (audio)",
3090
+ )
3091
+ sulphur_v1_audio_strength = gr.Slider(
3092
+ 0.0, 1.0, value=0.15, step=0.05,
3093
+ label="sulphur v1 (audio)",
3094
+ )
3095
+ vbvr_audio_strength = gr.Slider(
3096
+ 0.0, 1.0, value=0.5, step=0.05,
3097
+ label="vbvr (audio)",
3098
+ )
3099
+ dreamly_audio_strength = gr.Slider(
3100
+ 0.0, 1.0, value=0.6, step=0.05,
3101
+ label="dreamly (audio)",
3102
+ )
3103
+ synth_audio_strength = gr.Slider(
3104
+ 0.0, 1.0, value=0.0, step=0.05,
3105
+ label="synth (audio)",
3106
+ )
3107
+ plora_audio_strength = gr.Slider(
3108
+ 0.0, 1.0, value=0.0, step=0.05,
3109
+ label="plora (audio)",
3110
+ )
3111
+ singularity_audio_strength = gr.Slider(
3112
+ 0.0, 1.0, value=0.3, step=0.05,
3113
+ label="singularity (audio)",
3114
+ )
3115
+ omninft_audio_strength = gr.Slider(
3116
+ 0.0, 2.0, value=0.8, step=0.05,
3117
+ label="omninft converted (audio)",
3118
+ )
3119
+ omninft_bf16_audio_strength = gr.Slider(
3120
+ 0.0, 2.0, value=0.0, step=0.05,
3121
+ label="omninft RL bf16 / kijai (audio)",
3122
+ )
3123
+ better_motion_audio_strength = gr.Slider(
3124
+ 0.0, 1.0, value=0.0, step=0.05,
3125
+ label="better motion / mistic (audio)",
3126
+ )
3127
+ physics_v2_audio_strength = gr.Slider(
3128
+ 0.0, 1.0, value=0.0, step=0.05,
3129
+ label="physics v2 / mistic (audio)",
3130
+ )
3131
  with gr.Accordion("multi-reference settings (MSR)", open=False, visible=False) as msr_settings_acc:
3132
  msr_frame_count = gr.Dropdown(
3133
  [17, 25, 33, 41], value=41,
 
3216
  omninft_bf16_lora_strength,
3217
  better_motion_lora_strength,
3218
  physics_v2_lora_strength,
3219
+ sulphur_audio_strength,
3220
+ sulphur_v1_audio_strength,
3221
+ vbvr_audio_strength,
3222
+ dreamly_audio_strength,
3223
+ synth_audio_strength,
3224
+ plora_audio_strength,
3225
+ singularity_audio_strength,
3226
+ omninft_audio_strength,
3227
+ omninft_bf16_audio_strength,
3228
+ better_motion_audio_strength,
3229
+ physics_v2_audio_strength,
3230
  cache_at_step,
3231
  cache_warmup,
3232
  energy_threshold,
 
3245
  prompt_segments,
3246
  kv_enabled,
3247
  kv_strength,
3248
+ audio_ref_enabled,
3249
+ audio_ref_file,
3250
+ audio_ref_guidance_scale,
3251
+ audio_ref_stem_sep,
3252
  ],
3253
  outputs=[video, status, used_seed],
3254
  )
requirements.txt CHANGED
@@ -25,3 +25,4 @@ protobuf
25
  spandrel
26
  soundfile
27
  imageio-ffmpeg
 
 
25
  spandrel
26
  soundfile
27
  imageio-ffmpeg
28
+ rotary_embedding_torch