""" LoRA loader for WAN 2.2 I2V. The upstream repository mixes I2V and T2V LoRAs and uses inconsistent HIGH/LOW filename conventions, so this module keeps an explicit I2V catalog instead of guessing pairs with regexes. """ import re from huggingface_hub import hf_hub_download LORA_REPO = "lkzd7/WAN2.2_LoraSet_NSFW" HF_TOKEN = None LORA_CATALOG = { "Blink_Squatting_Cowgirl_Position_I2V": { "HIGH": "Blink_Squatting_Cowgirl_Position_I2V_HIGH.safetensors", "LOW": "Blink_Squatting_Cowgirl_Position_I2V_LOW.safetensors", "source": "Civitai archive/mirror, I2V", "trigger": None, }, "W22_Multiscene_Photoshoot_Softcore_i2v": { "HIGH": "W22_Multiscene_Photoshoot_Softcore_i2v_HN.safetensors", "LOW": "W22_Multiscene_Photoshoot_Softcore_i2v_LN.safetensors", "source": "Civitai 2316517, I2V", "trigger": None, }, "PENISLORA_22_i2v": { "HIGH": "PENISLORA_22_i2v_HIGH_e320.safetensors", "LOW": "PENISLORA_22_i2v_LOW_e496.safetensors", "source": "Civitai archive 1476909, I2V", "trigger": "PENISLORA", }, "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4": { "HIGH": "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4_high_noise.safetensors", "LOW": "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4_low_noise.safetensors", "source": "Civitai 2069477, I2V", "trigger": "pornmaster bukkake", }, "WAN-2.2-I2V-Double-Blowjob": { "HIGH": "WAN-2.2-I2V-Double-Blowjob-HIGH-v1.safetensors", "LOW": "WAN-2.2-I2V-Double-Blowjob-LOW-v1.safetensors", "source": "Civitai 1906148, I2V", "trigger": None, }, "WAN-2.2-I2V-HandjobBlowjobCombo": { "HIGH": "WAN-2.2-I2V-HandjobBlowjobCombo-HIGH-v1.safetensors", "LOW": "WAN-2.2-I2V-HandjobBlowjobCombo-LOW-v1.safetensors", "source": "Civitai 1899045, I2V", "trigger": None, }, "WAN-2.2-I2V-SensualTeasingBlowjob": { "HIGH": "WAN-2.2-I2V-SensualTeasingBlowjob-HIGH-v1.safetensors", "LOW": "WAN-2.2-I2V-SensualTeasingBlowjob-LOW-v1.safetensors", "source": "Civitai 2231076, I2V", "trigger": "sensualBJ", }, "iGOON_Blink_Blowjob_I2V": { "HIGH": "iGOON_Blink_Blowjob_I2V_HIGH.safetensors", "LOW": "iGOON_Blink_Blowjob_I2V_LOW.safetensors", "source": "Civitai archive 2172672, I2V", "trigger": None, }, "iGoon - Blink_Back_Doggystyle": { "HIGH": "iGoon%20-%20Blink_Back_Doggystyle_HIGH.safetensors", "LOW": "iGoon%20-%20Blink_Back_Doggystyle_LOW.safetensors", "source": "Civitai archive, I2V", "trigger": None, }, "iGoon - Blink_Facial_I2V": { "HIGH": "iGoon%20-%20Blink_Facial_I2V_HIGH.safetensors", "LOW": "iGoon%20-%20Blink_Facial_I2V_LOW.safetensors", "source": "Civitai archive 2228137, I2V", "trigger": None, }, "iGoon - Blink_Front_Doggystyle_I2V": { "HIGH": "iGoon - Blink_Front_Doggystyle_I2V_HIGH.safetensors", "LOW": "iGoon - Blink_Front_Doggystyle_I2V_LOW.safetensors", "source": "Civitai archive 2187182, I2V", "trigger": None, }, "iGoon - Blink_Missionary_I2V v1": { "HIGH": "iGoon - Blink_Missionary_I2V_HIGH.safetensors", "LOW": "iGoon - Blink_Missionary_I2V_LOW.safetensors", "source": "Civitai archive 2183381, I2V", "trigger": "missionary position", }, "iGoon - Blink_Missionary_I2V v2": { "HIGH": "iGoon_Blink_Missionary_I2V_HIGH v2.safetensors", "LOW": "iGoon - Blink_Missionary_I2V_LOW v2.safetensors", "source": "Civitai archive 2183381, I2V", "trigger": None, }, "iGoon_Blink_Titjob_I2V": { "HIGH": "iGoon_Blink_Titjob_I2V_HIGH.safetensors", "LOW": "iGoon_Blink_Titjob_I2V_LOW.safetensors", "source": "Civitai archive 2206475, I2V", "trigger": None, }, "lips-bj": { "HIGH": "lips-bj_high_noise.safetensors", "LOW": "lips-bj_low_noise.safetensors", "source": "Civitai 2319913, I2V", "trigger": "lips-bj", }, "mql_casting_sex_doggy_kneel_diagonally_behind_vagina_wan22_i2v_v1": { "HIGH": "mql_casting_sex_doggy_kneel_diagonally_behind_vagina_wan22_i2v_v1_high_noise.safetensors", "LOW": "mql_casting_sex_doggy_kneel_diagonally_behind_vagina_wan22_i2v_v1_low_noise.safetensors", "source": "MQ Lab archive, I2V", "trigger": None, }, "mql_casting_sex_reverse_cowgirl_lie_front_vagina_wan22_i2v_v1": { "HIGH": "mql_casting_sex_reverse_cowgirl_lie_front_vagina_wan22_i2v_v1_high_noise.safetensors", "LOW": "mql_casting_sex_reverse_cowgirl_lie_front_vagina_wan22_i2v_v1_low_noise.safetensors", "source": "MQ Lab archive, I2V", "trigger": None, }, "mql_casting_sex_spoon_wan22_i2v_v1": { "HIGH": "mql_casting_sex_spoon_wan22_i2v_v1_high_noise.safetensors", "LOW": "mql_casting_sex_spoon_wan22_i2v_v1_low_noise.safetensors", "source": "Civitai archive 2188637, I2V", "trigger": None, }, "mql_massage_tits_wan22_i2v_v1": { "HIGH": "mql_massage_tits_wan22_i2v_v1_high_noise.safetensors", "LOW": "mql_massage_tits_wan22_i2v_v1_low_noise.safetensors", "source": "Civitai 1952945, I2V", "trigger": None, }, "mql_panties_aside_wan22_i2v_v1": { "HIGH": "mql_panties_aside_wan22_i2v_v1_high_noise.safetensors", "LOW": "mql_panties_aside_wan22_i2v_v1_low_noise.safetensors", "source": "MQ Lab archive, I2V", "trigger": None, }, "sfbehind_v2.1": { "HIGH": "sfbehind_v2.1_high_noise.safetensors", "LOW": "sfbehind_v2.1_low_noise.safetensors", "source": "Civitai 2227622, I2V", "trigger": "sfb3hind, sfbehind", }, "sid3l3g_transition_v2.0": { "HIGH": "sid3l3g_transition_v2.0_H.safetensors", "LOW": "sid3l3g_transition_v2.0_L.safetensors", "source": "HF metadata, I2V-like transition LoRA", "trigger": "scene cuts and she is having sex with her leg aside", }, "wan2.2_i2v_ulitmate_pussy_asshole": { "HIGH": "wan2.2_i2v_high_ulitmate_pussy_asshole.safetensors", "LOW": "wan2.2_i2v_low_ulitmate_pussy_asshole.safetensors", "source": "Civitai archive 2217653, I2V", "trigger": None, }, "wan22-mouthfull-k3nk": { "HIGH": "wan22-mouthfull-140epoc-high-k3nk.safetensors", "LOW": "wan22-mouthfull-152epoc-low-k3nk.safetensors", "source": "Civitai 2148595, I2V", "trigger": "m0u7hfu11", }, "Drawer disguise (HIGH only)": { "HIGH": "\u62bd\u5c49\u53d8\u88c5.safetensors", "LOW": None, "source": "Civitai archive 2217516, I2V", "trigger": None, }, } LORA_ALIASES = { "PENISLORA_22_i2v_HIGH_e320": "PENISLORA_22_i2v", "PENISLORA_22_i2v_LOW_e496": "PENISLORA_22_i2v", "PENISLORA_22_i2v_HIGH_e320 (HIGH only)": "PENISLORA_22_i2v", "PENISLORA_22_i2v_LOW_e496 (LOW only)": "PENISLORA_22_i2v", "iGoon - Blink_Missionary_I2V": "iGoon - Blink_Missionary_I2V v2", "iGoon_Blink_Missionary_I2V": "iGoon - Blink_Missionary_I2V v2", "iGoon_Blink_Missionary_I2V (HIGH only)": "iGoon - Blink_Missionary_I2V v2", "sid3l3g_transition_v2.0_H": "sid3l3g_transition_v2.0", "sid3l3g_transition_v2.0_L": "sid3l3g_transition_v2.0", "sid3l3g_transition_v2.0_H (HIGH only)": "sid3l3g_transition_v2.0", "sid3l3g_transition_v2.0_L (LOW only)": "sid3l3g_transition_v2.0", "wan2.2_i2v_high_ulitmate_pussy_asshole": "wan2.2_i2v_ulitmate_pussy_asshole", "wan2.2_i2v_low_ulitmate_pussy_asshole": "wan2.2_i2v_ulitmate_pussy_asshole", "wan2.2_i2v_high_ulitmate_pussy_asshole (HIGH only)": "wan2.2_i2v_ulitmate_pussy_asshole", "wan2.2_i2v_low_ulitmate_pussy_asshole (LOW only)": "wan2.2_i2v_ulitmate_pussy_asshole", "wan22-mouthfull-140epoc-high-k3nk": "wan22-mouthfull-k3nk", "wan22-mouthfull-152epoc-low-k3nk": "wan22-mouthfull-k3nk", "wan22-mouthfull-140epoc-high-k3nk (HIGH only)": "wan22-mouthfull-k3nk", "wan22-mouthfull-152epoc-low-k3nk (LOW only)": "wan22-mouthfull-k3nk", "Drawer disguise": "Drawer disguise (HIGH only)", } MAX_LORAS = 3 HELPER_LORAS = { "PENISLORA_22_i2v", "wan2.2_i2v_ulitmate_pussy_asshole", } STYLE_LORAS = { "W22_Multiscene_Photoshoot_Softcore_i2v", } HIGH_ONLY_LORAS = { "Drawer disguise (HIGH only)", } CLOSEUP_ACTION_LORAS = { "lips-bj", "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4", "WAN-2.2-I2V-Double-Blowjob", "WAN-2.2-I2V-HandjobBlowjobCombo", "WAN-2.2-I2V-SensualTeasingBlowjob", "wan22-mouthfull-k3nk", } SINGLE_WEIGHTS = { "lips-bj": 0.9, "wan2.2_i2v_ulitmate_pussy_asshole": 0.95, "Drawer disguise (HIGH only)": 0.85, } HELPER_MIX_WEIGHTS = { "PENISLORA_22_i2v": 0.55, "wan2.2_i2v_ulitmate_pussy_asshole": 0.6, } LORA_PAIRS = { name: {"HIGH": spec.get("HIGH"), "LOW": spec.get("LOW")} for name, spec in LORA_CATALOG.items() } LORA_FILES = sorted( { filename for spec in LORA_CATALOG.values() for filename in (spec.get("HIGH"), spec.get("LOW")) if filename } ) def _resolve_group_name(group_name): if not group_name: return None clean_name = re.sub(r"\s*\(HIGH only\)|\s*\(LOW only\)", "", str(group_name)).strip() clean_name = LORA_ALIASES.get(clean_name, clean_name) clean_name = LORA_ALIASES.get(group_name, clean_name) if clean_name in LORA_CATALOG: return clean_name lower_map = {name.lower(): name for name in LORA_CATALOG} return lower_map.get(clean_name.lower()) def get_lora_choices(): return list(LORA_CATALOG.keys()) def get_lora_info(group_name): resolved = _resolve_group_name(group_name) return LORA_CATALOG.get(resolved) if resolved else None def _coerce_selection(group_names): if not group_names: return [] if isinstance(group_names, str): group_names = [group_names] selected = [] seen = set() for raw_name in group_names: if not raw_name or raw_name == "(None)": continue resolved = _resolve_group_name(raw_name) if not resolved: raise ValueError(f"Unknown LoRA: {raw_name}") if resolved in seen: continue selected.append(resolved) seen.add(resolved) return selected def get_lora_role(group_name): resolved = _resolve_group_name(group_name) if not resolved: return "unknown" if resolved in HELPER_LORAS: return "helper" if resolved in STYLE_LORAS: return "style" if resolved in HIGH_ONLY_LORAS: return "high_only" if resolved in CLOSEUP_ACTION_LORAS: return "closeup_action" return "action" def get_lora_blend_plan(group_names): """Return a stable, conservative multi-LoRA plan. The first non-helper LoRA is treated as the main action. Helper/style LoRAs are deliberately lower-weighted so they improve anatomy/scene detail without overpowering the motion LoRA. """ selected = _coerce_selection(group_names) if len(selected) > MAX_LORAS: raise ValueError(f"Select at most {MAX_LORAS} LoRAs") multi = len(selected) > 1 primary_action_assigned = False plan = [] for name in selected: role = get_lora_role(name) if not multi: weight = SINGLE_WEIGHTS.get(name, 1.0) elif role == "helper": weight = HELPER_MIX_WEIGHTS.get(name, 0.55) elif role == "style": weight = 0.45 elif role == "high_only": weight = 0.65 elif not primary_action_assigned: weight = 0.85 primary_action_assigned = True else: weight = 0.45 plan.append({"name": name, "role": role, "weight": weight}) return plan def download_lora(group_name): resolved = _resolve_group_name(group_name) if not resolved: return None, None pair = LORA_CATALOG[resolved] high_path, low_path = None, None if pair.get("HIGH"): high_path = hf_hub_download(LORA_REPO, pair["HIGH"], token=HF_TOKEN) if pair.get("LOW"): low_path = hf_hub_download(LORA_REPO, pair["LOW"], token=HF_TOKEN) return high_path, low_path def _load_adapter(pipe, path, adapter_name, component, weight=1.0, group_name=None): kwargs = {} if component == "transformer_2": if not hasattr(pipe, "transformer_2") or pipe.transformer_2 is None: raise RuntimeError("This pipeline has no transformer_2 for LOW LoRA loading") kwargs["load_into_transformer_2"] = True pipe.load_lora_weights(path, adapter_name=adapter_name, **kwargs) return { "name": adapter_name, "component": component, "weight": float(weight), "group": group_name, } def activate_loras(pipe, adapters): """Activate loaded LoRA adapters on their exact Wan transformer component.""" if not adapters: return by_component = {} for adapter in adapters: by_component.setdefault(adapter["component"], []).append(adapter["name"]) for component, names in by_component.items(): model = getattr(pipe, component, None) if model is None: raise RuntimeError(f"Pipeline component missing for LoRA activation: {component}") if not hasattr(model, "set_adapters"): raise RuntimeError(f"{component} does not expose set_adapters(); LoRA cannot be activated") weights = [float(adapter.get("weight", 1.0)) for adapter in adapters if adapter["component"] == component] model.set_adapters(names, weights=weights) if hasattr(model, "enable_lora"): model.enable_lora() if hasattr(pipe, "get_list_adapters"): print(f"LoRA adapters loaded: {pipe.get_list_adapters()}") if hasattr(pipe, "get_active_adapters"): print(f"LoRA adapters active: {pipe.get_active_adapters()}") def load_lora_to_pipe(pipe, group_name, adapter_name="lora", weight=1.0): resolved = _resolve_group_name(group_name) high_path, low_path = download_lora(resolved) adapters = [] if high_path and low_path: adapters.append(_load_adapter(pipe, high_path, f"{adapter_name}_high", "transformer", weight, resolved)) adapters.append(_load_adapter(pipe, low_path, f"{adapter_name}_low", "transformer_2", weight, resolved)) print(f"Loaded LoRA pair: {resolved} weight={weight:.2f} -> {[a['name'] for a in adapters]}") return adapters if high_path: adapters.append(_load_adapter(pipe, high_path, adapter_name, "transformer", weight, resolved)) print(f"Loaded LoRA HIGH: {resolved} weight={weight:.2f} -> {adapter_name}") return adapters if low_path: adapters.append(_load_adapter(pipe, low_path, adapter_name, "transformer_2", weight, resolved)) print(f"Loaded LoRA LOW: {resolved} weight={weight:.2f} -> {adapter_name}") return adapters raise ValueError(f"LoRA not found: {group_name}") def unload_lora(pipe): try: pipe.unload_lora_weights() except Exception as exc: print(f"LoRA unload warning: {exc}")