File size: 12,825 Bytes
cdefade
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9765931
 
 
 
 
cdefade
9765931
 
 
 
 
cdefade
 
 
958a17d
 
 
cdefade
 
 
 
 
 
 
 
9765931
cdefade
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c10762c
cdefade
 
 
 
c10762c
 
cdefade
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d38bdd
9765931
 
 
5d38bdd
 
 
cdefade
 
 
 
 
 
 
5d38bdd
 
cdefade
5d38bdd
9765931
cdefade
5d38bdd
 
9765931
 
5d38bdd
 
 
 
 
9765931
5d38bdd
 
 
 
 
cdefade
2a75ce9
 
 
 
 
 
 
 
 
 
 
 
cdefade
2a75ce9
 
 
 
cdefade
5d38bdd
cdefade
 
 
 
5d38bdd
cdefade
5d38bdd
 
cdefade
 
 
 
5d38bdd
cdefade
 
 
 
 
 
 
 
5d38bdd
cdefade
5d38bdd
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
"""
Custom LoRA Loader for WAN 2.2 I2V.
Add your custom LoRA models (Hugging Face or direct Civitai URLs) in the EXTRA dictionary below.
"""
import os
import urllib.parse
import urllib.request
import urllib.error
import re
import hashlib
import inspect
from huggingface_hub import hf_hub_download

# Monkey patch for peft TorchaoLoraLinear bug (compatibility between peft 0.19.1 and diffusers)
try:
    import peft.tuners.lora.torchao as peft_torchao
    if hasattr(peft_torchao, "TorchaoLoraLinear"):
        _orig_torchao_init = peft_torchao.TorchaoLoraLinear.__init__
        _sig = inspect.signature(_orig_torchao_init)
        if "get_apply_tensor_subclass" in _sig.parameters:
            _param = _sig.parameters["get_apply_tensor_subclass"]
            if _param.default is inspect.Parameter.empty:
                def _patched_torchao_init(self, *args, **kwargs):
                    if "get_apply_tensor_subclass" not in kwargs:
                        base_layer = args[0] if args else kwargs.get("base_layer", None)
                        get_subclass_fn = getattr(base_layer, "get_apply_tensor_subclass", None) if base_layer else None
                        kwargs["get_apply_tensor_subclass"] = get_subclass_fn
                    return _orig_torchao_init(self, *args, **kwargs)
                peft_torchao.TorchaoLoraLinear.__init__ = _patched_torchao_init
                print("✅ Applied peft TorchaoLoraLinear compatibility patch.")
except Exception as patch_err:
    print(f"TorchaoLoraLinear patch notice: {patch_err}")

HF_TOKEN = os.environ.get("HF_TOKEN")  # authenticated downloads (covers private repos)
CIVITAI_TOKEN = os.environ.get("CIVITAI_TOKEN", "")

# Pinned commit hashes if needed (optional)
PINNED_REVISIONS = {}

LORA_FILES = []

# group -> {"HIGH": (repo, file) | url | None, "LOW": (repo, file) | url | None}
LORA_PAIRS = {}
for f in LORA_FILES:
    name = urllib.parse.unquote(f).replace(".safetensors", "")
    is_high = bool(re.search(r'(high|HN|_H\b)', name, re.IGNORECASE))
    is_low = bool(re.search(r'(low|LN|_L\b)', name, re.IGNORECASE))
    group = re.sub(r'[\s_-]*(high|low|noise|HN|LN)([\s_-]*noise)?[\s_-]*(v?\d+(\.\d+)?)?\s*$', '', name, flags=re.IGNORECASE).strip()
    group = re.sub(r'[\s_]+$', '', group)
    LORA_PAIRS.setdefault(group, {"HIGH": None, "LOW": None})

# Custom LoRAs dictionary (label -> URL or (repo_id, high_file, low_file|None))
# Examples:
# EXTRA = {
#     "My Civitai LoRA": "https://civitai.red/api/download/models/2098405?fileId=1994044",
#     "My HF Dual LoRA": ("username/my-lora-repo", "motion_high.safetensors", "motion_low.safetensors"),
# }
EXTRA = {
    "lopi999 - Wan2.2 I2V General NSFW LoRA (Trigger: nsfwsks)": (
        "lopi999/Wan2.2-I2V_General-NSFW-LoRA",
        "NSFW-22-H-e8.safetensors",
        "NSFW-22-L-e8.safetensors"
    ),
}

for label, item in EXTRA.items():
    LORA_PAIRS.setdefault(label, {"HIGH": None, "LOW": None})
    if isinstance(item, str):
        LORA_PAIRS[label]["HIGH"] = item
    elif isinstance(item, (tuple, list)):
        if len(item) == 3:
            repo, hi, lo = item
            if isinstance(repo, str) and (repo.startswith("http://") or repo.startswith("https://")):
                LORA_PAIRS[label]["HIGH"] = repo
                if hi and isinstance(hi, str) and (hi.startswith("http://") or hi.startswith("https://")):
                    LORA_PAIRS[label]["LOW"] = hi
            else:
                if hi:
                    LORA_PAIRS[label]["HIGH"] = (repo, hi)
                if lo:
                    LORA_PAIRS[label]["LOW"] = (repo, lo)
        elif len(item) == 2:
            hi, lo = item
            if hi:
                LORA_PAIRS[label]["HIGH"] = hi
            if lo:
                LORA_PAIRS[label]["LOW"] = lo


def get_loras_dir():
    data_dir = "/data/loras"
    if os.path.exists("/data") and os.path.isdir("/data"):
        os.makedirs(data_dir, exist_ok=True)
        return data_dir
    os.makedirs("loras", exist_ok=True)
    return "loras"


def download_file_from_url(url, custom_name=None):
    target_dir = get_loras_dir()
    civitai_tok = os.environ.get("CIVITAI_TOKEN", "") or CIVITAI_TOKEN
    
    url_to_fetch = url
    if "huggingface.co" in url_to_fetch.lower() and "/blob/" in url_to_fetch.lower():
        url_to_fetch = url_to_fetch.replace("/blob/", "/resolve/")

    if "civitai" in url.lower() and civitai_tok and "token=" not in url.lower():
        sep = "&" if "?" in url else "?"
        url_to_fetch = f"{url}{sep}token={civitai_tok}"

    if not custom_name:
        url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
        custom_name = f"lora_{url_hash}.safetensors"

    local_path = os.path.join(target_dir, custom_name)
    if os.path.exists(local_path) and os.path.getsize(local_path) > 1000:
        return local_path

    print(f"📥 Downloading LoRA from URL: {url_to_fetch} ...")
    req = urllib.request.Request(url_to_fetch, headers={"User-Agent": "Mozilla/5.0"})
    
    try:
        with urllib.request.urlopen(req) as response:
            cd = response.headers.get("Content-Disposition", "")
            if "filename=" in cd:
                fname = re.findall(r'filename="?([^";]+)"?', cd)
                if fname:
                    real_name = fname[0].strip()
                    if not real_name.endswith(".safetensors"):
                        real_name += ".safetensors"
                    alt_path = os.path.join(target_dir, real_name)
                    if os.path.exists(alt_path) and os.path.getsize(alt_path) > 1000:
                        return alt_path
                    local_path = alt_path

            os.makedirs(os.path.dirname(local_path), exist_ok=True)

            with open(local_path, "wb") as f:
                while True:
                    chunk = response.read(8192)
                    if not chunk:
                        break
                    f.write(chunk)
    except urllib.error.HTTPError as e:
        if e.code == 401:
            raise Exception(
                "Download failed (401 Unauthorized). Civitai requires an API Token. "
                "Add ?token=YOUR_CIVITAI_API_KEY to the download URL or set CIVITAI_TOKEN environment variable."
            )
        raise Exception(f"Failed to download LoRA from URL (Status {e.code}): {e.reason}")
    except Exception as e:
        raise Exception(f"Failed to download LoRA from URL: {e}")

    print(f"✅ Downloaded LoRA successfully: {local_path} ({os.path.getsize(local_path)} bytes)")
    return local_path


def get_lora_choices():
    choices = []
    for group in sorted(LORA_PAIRS.keys()):
        p = LORA_PAIRS[group]
        if p["HIGH"] and p["LOW"]:
            choices.append(group)
        elif p["HIGH"]:
            choices.append(f"{group} (HIGH only)")
        elif p["LOW"]:
            choices.append(f"{group} (LOW only)")
    return choices


def download_lora(group_name):
    if not group_name:
        return None, None
    clean_name = re.sub(r'\s*\(HIGH only\)|\s*\(LOW only\)', '', group_name)
    if clean_name not in LORA_PAIRS:
        return None, None
    pair = LORA_PAIRS[clean_name]
    
    def resolve_entry(entry):
        if not entry:
            return None
        if isinstance(entry, str) and (entry.startswith("http://") or entry.startswith("https://")):
            return download_file_from_url(entry)
        elif isinstance(entry, (tuple, list)) and len(entry) == 2:
            repo, fn = entry
            if isinstance(repo, str) and (repo.startswith("http://") or repo.startswith("https://")):
                return download_file_from_url(repo)
            rev = PINNED_REVISIONS.get(repo)
            return hf_hub_download(repo, fn, token=HF_TOKEN, revision=rev) if rev else hf_hub_download(repo, fn, token=HF_TOKEN)
        return None

    high_path = resolve_entry(pair["HIGH"])
    low_path = resolve_entry(pair["LOW"])
    return high_path, low_path


def load_lora_to_pipe(pipe, group_name, adapter_name="lora"):
    high_path, low_path = download_lora(group_name)
    if high_path and low_path:
        pipe.load_lora_weights(high_path, adapter_name=f"{adapter_name}_high")
        pipe.load_lora_weights(low_path, adapter_name=f"{adapter_name}_low")
        print(f"Loaded LoRA pair: {group_name}")
        return True
    elif high_path:
        pipe.load_lora_weights(high_path, adapter_name=adapter_name)
        print(f"Loaded LoRA: {group_name}")
        return True
    elif low_path:
        pipe.load_lora_weights(low_path, adapter_name=adapter_name)
        print(f"Loaded LoRA (low): {group_name}")
        return True
    return False


def list_cached_loras():
    target_dir = get_loras_dir()
    files = [f for f in os.listdir(target_dir) if f.endswith(".safetensors")]
    files.sort(key=lambda x: os.path.getmtime(os.path.join(target_dir, x)), reverse=True)
    return ["(None / Disable)"] + files


def unload_lora(pipe):
    try:
        pipe.unload_lora_weights()
    except:
        pass


def load_custom_url_lora(pipe, url_or_path, adapter_name="custom_lora", scale=1.0):
    if not url_or_path or not str(url_or_path).strip() or str(url_or_path).strip() == "(None / Disable)":
        return False
    target = str(url_or_path).strip()
    target_dir = get_loras_dir()
    try:
        if os.path.exists(target) and target.endswith(".safetensors"):
            file_path = target
        elif os.path.exists(os.path.join(target_dir, target)):
            file_path = os.path.join(target_dir, target)
        elif os.path.exists(os.path.join("loras", target)):
            file_path = os.path.join("loras", target)
        elif target.startswith("http://") or target.startswith("https://"):
            file_path = download_file_from_url(target)
        else:
            candidates = [os.path.join(target_dir, f) for f in os.listdir(target_dir) if target.lower() in f.lower()] if os.path.exists(target_dir) else []
            if candidates:
                file_path = candidates[0]
            else:
                return False

        if file_path and os.path.exists(file_path):
            try:
                pipe.load_lora_weights(file_path, adapter_name=adapter_name)
                if hasattr(pipe, "set_adapters"):
                    try:
                        pipe.set_adapters([adapter_name], adapter_weights=[float(scale)])
                    except Exception:
                        pass
                print(f"✅ Loaded Custom LoRA: {file_path} (scale={scale})")
                return True
            except Exception as load_err:
                print(f"⚠️ Incompatible or corrupted LoRA detected ({file_path}): {load_err}")
                print(f"🗑️ Automatically deleting incompatible LoRA file: {file_path}...")
                try:
                    os.remove(file_path)
                except Exception as del_err:
                    print(f"Warning deleting file: {del_err}")
                return False
    except Exception as e:
        print(f"❌ Failed to load custom LoRA: {e}")
    return False


def download_custom_lora_ui_action(url):
    import gradio as gr
    if not url or not str(url).strip():
        msg = "<div style='background: rgba(239, 68, 68, 0.15); border: 1.5px solid rgba(239, 68, 68, 0.4); border-radius: 12px; padding: 12px 16px; color: #f87171; font-weight: 600;'>⚠️ Please enter a valid LoRA download URL first.</div>"
        return msg, gr.update(choices=list_cached_loras())
    try:
        path = download_file_from_url(str(url).strip())
        size_mb = round(os.path.getsize(path) / (1024 * 1024), 2)
        fname = os.path.basename(path)
        msg = (
            f"<div style='background: linear-gradient(135deg, rgba(16, 185, 129, 0.22) 0%, rgba(5, 150, 105, 0.12) 100%); "
            f"border: 1.5px solid #10b981; border-radius: 14px; padding: 14px 18px; color: #34d399; font-weight: 700; "
            f"box-shadow: 0 6px 20px rgba(16, 185, 129, 0.25); backdrop-filter: blur(10px); margin-top: 10px;'>"
            f"✅ <b>Custom LoRA Successfully Downloaded & Ready!</b><br>"
            f"<span style='font-weight: 500; font-size: 0.88rem; color: #e2e8f0; margin-top: 4px; display: inline-block;'>"
            f"📦 File: <code>{fname}</code> ({size_mb} MB) • Cached to CPU memory with <b>0 GPU Quota consumed</b>. "
            f"Will automatically fuse on your next video generation!</span></div>"
        )
        return msg, gr.update(choices=list_cached_loras(), value=fname)
    except Exception as e:
        msg = f"<div style='background: rgba(239, 68, 68, 0.15); border: 1.5px solid rgba(239, 68, 68, 0.4); border-radius: 12px; padding: 12px 16px; color: #f87171; font-weight: 600;'>❌ Error downloading LoRA: {e}</div>"
        return msg, gr.update(choices=list_cached_loras())