File size: 18,291 Bytes
06b4ab9
 
 
 
 
269268f
 
 
06b4ab9
269268f
 
06b4ab9
269268f
 
06b4ab9
1b93f28
93d4170
06b4ab9
 
93d4170
06b4ab9
 
 
 
 
 
1b93f28
 
06b4ab9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b93f28
 
 
06b4ab9
 
 
 
 
 
 
 
 
 
4377a36
 
93d4170
4377a36
93d4170
4377a36
93d4170
 
 
 
 
 
4377a36
93d4170
 
 
 
 
4377a36
 
 
 
 
93d4170
06b4ab9
4377a36
93d4170
 
4377a36
 
93d4170
269268f
 
 
 
 
 
 
93d4170
 
 
 
 
 
 
 
 
4377a36
 
06b4ab9
 
 
269268f
06b4ab9
 
 
 
 
 
 
 
 
 
 
4377a36
 
269268f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93d4170
 
 
269268f
 
 
 
 
 
 
 
 
 
 
93d4170
 
 
 
 
 
 
 
 
06b4ab9
93d4170
06b4ab9
 
 
4377a36
06b4ab9
4377a36
 
 
06b4ab9
 
4377a36
 
06b4ab9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93d4170
06b4ab9
4377a36
 
 
06b4ab9
4377a36
06b4ab9
 
 
 
 
93d4170
06b4ab9
 
 
 
 
93d4170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06b4ab9
 
 
1b93f28
 
 
 
 
 
 
06b4ab9
c783e3c
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
"""
LoRA Loader for WAN 2.2 I2V - multi-repo.
Baseline: lkzd7/WAN2.2_LoraSet_NSFW. Plus curated I2V act/helper LoRAs from
Lythiga/WAN2.2_I2V_Lora, rolechar/rc_wan2.2_i2v_loraset_nsfw_private,
lopi999/Wan2.2-I2V_General-NSFW-LoRA. T2V / lightning / furry / dance / male-char filtered out.

Also resolves plain download links (CivitAI and anything else serving a
.safetensors), so a slot can take a URL instead of a repo file.
"""
import hashlib
import json
import os
import struct
import tempfile
import urllib.parse
import re
from huggingface_hub import hf_hub_download, list_repo_files

LORA_REPO = "lkzd7/WAN2.2_LoraSet_NSFW"
HF_TOKEN = os.environ.get("HF_TOKEN")

PINNED_REVISIONS = {
    "lkzd7/WAN2.2_LoraSet_NSFW": "ccfa867ce4b1",
    "Lythiga/WAN2.2_I2V_Lora": "67ef542e6faa",
    "rolechar/rc_wan2.2_i2v_loraset_nsfw_private": "4855a2f216ac",
    "lopi999/Wan2.2-I2V_General-NSFW-LoRA": "aeef17d7fa51",
}

LORA_FILES = [
    "Blink_Squatting_Cowgirl_Position_I2V_HIGH.safetensors",
    "Blink_Squatting_Cowgirl_Position_I2V_LOW.safetensors",
    "PENISLORA_22_i2v_HIGH_e320.safetensors",
    "PENISLORA_22_i2v_LOW_e496.safetensors",
    "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4_high_noise.safetensors",
    "Pornmaster_wan 2.2_14b_I2V_bukkake_v1.4_low_noise.safetensors",
    "W22_Multiscene_Photoshoot_Softcore_i2v_HN.safetensors",
    "W22_Multiscene_Photoshoot_Softcore_i2v_LN.safetensors",
    "WAN-2.2-I2V-Double-Blowjob-HIGH-v1.safetensors",
    "WAN-2.2-I2V-Double-Blowjob-LOW-v1.safetensors",
    "WAN-2.2-I2V-HandjobBlowjobCombo-HIGH-v1.safetensors",
    "WAN-2.2-I2V-HandjobBlowjobCombo-LOW-v1.safetensors",
    "WAN-2.2-I2V-SensualTeasingBlowjob-HIGH-v1.safetensors",
    "WAN-2.2-I2V-SensualTeasingBlowjob-LOW-v1.safetensors",
    "iGOON_Blink_Blowjob_I2V_HIGH.safetensors",
    "iGOON_Blink_Blowjob_I2V_LOW.safetensors",
    "iGoon - Blink_Front_Doggystyle_I2V_HIGH.safetensors",
    "iGoon - Blink_Front_Doggystyle_I2V_LOW.safetensors",
    "iGoon - Blink_Missionary_I2V_HIGH.safetensors",
    "iGoon - Blink_Missionary_I2V_LOW v2.safetensors",
    "iGoon - Blink_Missionary_I2V_LOW.safetensors",
    "iGoon%20-%20Blink_Back_Doggystyle_HIGH.safetensors",
    "iGoon%20-%20Blink_Back_Doggystyle_LOW.safetensors",
    "iGoon%20-%20Blink_Facial_I2V_HIGH.safetensors",
    "iGoon%20-%20Blink_Facial_I2V_LOW.safetensors",
    "iGoon_Blink_Missionary_I2V_HIGH v2.safetensors",
    "iGoon_Blink_Titjob_I2V_HIGH.safetensors",
    "iGoon_Blink_Titjob_I2V_LOW.safetensors",
    "lips-bj_high_noise.safetensors",
    "lips-bj_low_noise.safetensors",
    "mql_casting_sex_doggy_kneel_diagonally_behind_vagina_wan22_i2v_v1_high_noise.safetensors",
    "mql_casting_sex_doggy_kneel_diagonally_behind_vagina_wan22_i2v_v1_low_noise.safetensors",
    "mql_casting_sex_reverse_cowgirl_lie_front_vagina_wan22_i2v_v1_high_noise.safetensors",
    "mql_casting_sex_reverse_cowgirl_lie_front_vagina_wan22_i2v_v1_low_noise.safetensors",
    "mql_casting_sex_spoon_wan22_i2v_v1_high_noise.safetensors",
    "mql_casting_sex_spoon_wan22_i2v_v1_low_noise.safetensors",
    "mql_massage_tits_wan22_i2v_v1_high_noise.safetensors",
    "mql_massage_tits_wan22_i2v_v1_low_noise.safetensors",
    "mql_panties_aside_wan22_i2v_v1_high_noise.safetensors",
    "mql_panties_aside_wan22_i2v_v1_low_noise.safetensors",
    "sfbehind_v2.1_high_noise.safetensors",
    "sfbehind_v2.1_low_noise.safetensors",
    "sid3l3g_transition_v2.0_H.safetensors",
    "sid3l3g_transition_v2.0_L.safetensors",
    "wan2.2_i2v_high_ulitmate_pussy_asshole.safetensors",
    "wan2.2_i2v_low_ulitmate_pussy_asshole.safetensors",
    "wan22-mouthfull-140epoc-high-k3nk.safetensors",
    "wan22-mouthfull-152epoc-low-k3nk.safetensors",
]

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})
    if is_high:
        LORA_PAIRS[group]["HIGH"] = (LORA_REPO, f)
    elif is_low:
        LORA_PAIRS[group]["LOW"] = (LORA_REPO, f)

L = "Lythiga/WAN2.2_I2V_Lora"
R = "rolechar/rc_wan2.2_i2v_loraset_nsfw_private"
P = "lopi999/Wan2.2-I2V_General-NSFW-LoRA"

EXTRA = {
    "Ultimate Deepthroat": (L, "wan22-ultimatedeepthroat-i2v-102epoc-high-k3nk.safetensors", "wan22-ultimatedeepthroat-I2V-101epoc-low-k3nk.safetensors"),
    "BBC Deepthroat": (L, "wan22-bbcdeepthroat-115epoc-high-k3nk.safetensors", "wan22-bbcdeepthroat-155epoc-low-720-k3nk.safetensors"),
    "Throat V2": (L, "Wan22_ThroatV2_High.safetensors", "Wan22_ThroatV2_Low.safetensors"),
    "JFJ Deepthroat": (L, "jfj-deepthroat-W22-I2V-HN.safetensors", None),
    "BBC Blowjob Extreme": (L, "BBC Blowjobs Extreme_high_noise.safetensors", "BBC Blowjobs Extreme_low_noise.safetensors"),
    "POV Blowjob": (L, "Pov_Blowjob_Wan2.2_high_I2V_v1.0.safetensors", None),
    "Cunnilingus": (L, "wan22-cunilingus-I2V-106epoc-high.safetensors", "wan22-cunilingus-I2V-72epoc-low.safetensors"),
    "Reverse Cowgirl v2": (L, "wan22.r3v3rs3_c0wg1rl-14b-High-i2v_e70.safetensors", "wan22.r3v3rs3_c0wg1rl-14b-Low-i2v_e70.safetensors"),
    "Doggy Front View v2": (L, "doggy_style_sex_front_view_Wan2.2_I2V_high_v1.0.safetensors", "doggy_style_sex_front_view_Wan2.2_I2V_Low_v1.0.safetensors"),
    "Fucked From Behind": (L, "derpfuckedfrombehind_000002125_high_noise.safetensors", "derpfuckedfrombehind_000002125_low_noise.safetensors"),
    "Prone Bone": (L, "Pronebone_high_noise.safetensors", "Pronebone_low_noise.safetensors"),
    "Reverse Suspended Congress": (L, "reverse_suspended_congress_I2V_high.safetensors", "reverse_suspended_congress_I2V_low.safetensors"),
    "POV Missionary": (L, "wan2.2_i2v_highnoise_pov_missionary_v1.0.safetensors", "wan2.2_i2v_lownoise_pov_missionary_v1.0.safetensors"),
    "Mating Press": (L, "mating_press_high.safetensors", "mating_press_low.safetensors"),
    "Standing Upright Sex": (L, "Upright_sex_Wan2.2_14B_TI2V_HIGH_v1.0.safetensors", "Upright_sex_Wan2.2_14B_TI2V_LOW_v1.0.safetensors"),
    "Oral Insertion": (L, "wan2.2-i2v-high-oral-insertion-v1.0.safetensors", "wan2.2-i2v-low-oral-insertion-v1.0.safetensors"),
    "Handjob": (L, "WAN-2.2-I2V-Handjob-HIGH-v1.safetensors", "WAN-2.2-I2V-Handjob-LOW-v1.safetensors"),
    "Balls Sucking": (L, "WAN-2.2-I2V_Balls_sucking_High_noise.safetensors", "WAN-2.2-I2V_Balls_sucking_Low_noise.safetensors"),
    "Fingering": (R, "Sensual_fingering_v1_high_noise.safetensors", "Sensual_fingering_v1_low_noise.safetensors"),
    "Facesplash Cumshot": (L, "wan22-f4c3spl4sh-100epoc-high-k3nk.safetensors", "wan22-f4c3spl4sh-154epoc-low-k3nk.safetensors"),
    "Creampie CRM": (L, "Creampie CRM-FULL-EPOCH-80-HIGH.safetensors", "Creampie CRM-FULL-EPOCH-80-LOW.safetensors"),
    "Pornmaster Creampie": (L, "Pornmaster_wan 2.2_14b_I2V_Creampie_v1_high_noise.safetensors", "Pornmaster_wan 2.2_14b_I2V_Creampie_v1_low_noise.safetensors"),
    "Cum (generic)": (L, "Wan22_Cum_high_noise_1.V1.safetensors", "Wan22_Cum_low_noise_1.V1.safetensors"),
    "French Kiss": (L, "WAN2.2-FrenchKiss_HighNoise.safetensors", "WAN2.2-FrenchKiss_LowNoise.safetensors"),
    "Twerking": (L, "Twerking_I2V_high_noise.safetensors", "Twerking_I2V_low_noise.safetensors"),
    "Ahegao Drooling": (R, "middlefinger_drooling_ahegao_high.safetensors", "middlefinger_drooling_ahegao_low.safetensors"),
    "General NSFW Booster": (P, "NSFW-22-H-e8.safetensors", "NSFW-22-L-e8.safetensors"),
    "Penetration Insert": (L, "PenInsert_high_noise.safetensors", "PenInsert_low_noise.safetensors"),
    "Biggest Cock (size)": (L, "WAN-2.2-I2V_BiggestCock_high_noise_V1.safetensors", "WAN-2.2-I2V_BiggestCock_low_noise_V1.safetensors"),
    "Doggy Slider": (L, "I2V_doggyslider_high.safetensors", "I2V_doggyslider_low.safetensors"),
    "Pussy Helper": (R, "wan2.2_i2v_ulitmate_pussy_helper_high.safetensors", "wan2.2_i2v_ulitmate_pussy_helper_low.safetensors"),
    "FOV Slider (camera)": (L, "wan2.2-i2v-high-sex-fov-slider-v1.0.safetensors", "wan2.2-i2v-low-sex-fov-slider-v1.0.safetensors"),
    "Smashcut (camera)": (R, "wan2.2-i2v-sex-smashcut-v1.0-high.safetensors", "wan2.2-i2v-sex-smashcut-v1.0-low.safetensors"),
}
for label, (repo, hi, lo) in EXTRA.items():
    LORA_PAIRS.setdefault(label, {"HIGH": None, "LOW": None})
    if hi:
        LORA_PAIRS[label]["HIGH"] = (repo, hi)
    if lo:
        LORA_PAIRS[label]["LOW"] = (repo, lo)


def get_lora_choices():
    choices = ["(None)"]
    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 parse_repo_link(link: str):
    if not link:
        return None
    link = link.strip().strip('"').strip("'")
    m = re.match(r"https?://huggingface\.co/(?:models/)?([^/]+)/([^/]+)", link)
    if m:
        return f"{m.group(1)}/{m.group(2)}"
    if not link.startswith("http") and link.count("/") == 1:
        return link
    return None


def parse_hf_file_link(link: str):
    if not link:
        return None, None
    link = link.strip().strip('"').strip("'")
    m = re.match(
        r"https?://huggingface\.co/([^/]+)/([^/]+)/(?:resolve|blob)/[^/]+/(.+)",
        link,
    )
    if m:
        return f"{m.group(1)}/{m.group(2)}", urllib.parse.unquote(m.group(3).split("?")[0])
    if not link.startswith("http") and link.count("/") >= 2:
        parts = link.split("/")
        return f"{parts[0]}/{parts[1]}", "/".join(parts[2:])
    return None, None


def list_loras_from_repo(repo_link: str):
    """Files in a Hugging Face repo. A direct download link is handed back as the
    only choice, so pasting a CivitAI URL into the repo box fills the dropdowns."""
    link = (repo_link or "").strip()
    if is_direct_link(link):
        return ["(None)", link]

    repo_id = parse_repo_link(link)
    if not repo_id:
        return ["(None)"]
    try:
        files = list_repo_files(repo_id, token=HF_TOKEN, revision=PINNED_REVISIONS.get(repo_id))
        safes = sorted([f for f in files if f.lower().endswith(".safetensors")])
        return ["(None)"] + safes if safes else ["(None)"]
    except Exception as e:
        print(f"list_loras_from_repo error: {e}")
        return ["(None)"]


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]
    high_path, low_path = None, None
    if pair["HIGH"]:
        repo, fn = pair["HIGH"]
        high_path = hf_hub_download(repo, fn, token=HF_TOKEN, revision=PINNED_REVISIONS.get(repo))
    if pair["LOW"]:
        repo, fn = pair["LOW"]
        low_path = hf_hub_download(repo, fn, token=HF_TOKEN, revision=PINNED_REVISIONS.get(repo))
    return high_path, low_path


# ----------------------------------------------------------------------------
# Direct links (CivitAI and any other plain .safetensors URL)
# ----------------------------------------------------------------------------
# The download runs on the Space's machine, not in the visitor's browser, so a
# CivitAI session in a browser tab does not authenticate it: a gated model
# answers a server with an HTML login page instead of weights. CIVITAI_TOKEN
# (Settings -> Variables and secrets) is appended automatically when it is set,
# and the file header is checked, so a login page fails with a sentence that
# says what to do rather than a parse error deep inside safetensors.

CIVITAI_HOSTS = ("civitai.com", "civitai.red", "civitai.green", "civitai.work")
CIVITAI_TOKEN = os.environ.get("CIVITAI_TOKEN", "").strip()


def is_direct_link(text: str) -> bool:
    """A URL that is not a huggingface.co link, so a plain file download."""
    text = (text or "").strip().strip('"').strip("'")
    if not text.startswith(("http://", "https://")):
        return False
    host = (urllib.parse.urlparse(text).hostname or "").lower()
    return not host.endswith("huggingface.co")


def download_direct_link(url: str) -> str:
    import requests

    url = url.strip().strip('"').strip("'")
    host = (urllib.parse.urlparse(url).hostname or "").lower()
    request_url = url
    if CIVITAI_TOKEN and any(host.endswith(known) for known in CIVITAI_HOSTS) and "token=" not in url:
        request_url = url + ("&" if "?" in url else "?") + f"token={CIVITAI_TOKEN}"

    cache_dir = os.path.join(tempfile.gettempdir(), "url-loras")
    os.makedirs(cache_dir, exist_ok=True)
    cached = os.path.join(cache_dir, hashlib.sha256(url.encode()).hexdigest()[:16] + ".safetensors")
    if os.path.exists(cached) and os.path.getsize(cached) > 1_000_000:
        return cached

    response = requests.get(request_url, stream=True, timeout=180,
                            headers={"User-Agent": "Mozilla/5.0"})
    response.raise_for_status()

    if "text/html" in (response.headers.get("content-type") or "").lower():
        raise ValueError(
            "That link answered with a web page instead of a file. The model is gated, so the "
            "Space needs its own key: add CIVITAI_TOKEN under Settings -> Variables and secrets."
        )

    written = 0
    with open(cached, "wb") as handle:
        for chunk in response.iter_content(chunk_size=1 << 20):
            if chunk:
                handle.write(chunk)
                written += len(chunk)

    if written < 1_000_000:
        os.remove(cached)
        raise ValueError(
            "That link returned only a few kilobytes - almost always a login or error page rather "
            "than weights. Check the link, or add CIVITAI_TOKEN to the Space."
        )

    # safetensors starts with an 8-byte little-endian header length, then that much JSON.
    with open(cached, "rb") as handle:
        length = struct.unpack("<Q", handle.read(8))[0]
        if not (0 < length < 100_000_000):
            os.remove(cached)
            raise ValueError("The downloaded file is not a .safetensors (bad header).")
        try:
            json.loads(handle.read(length).decode("utf-8"))
        except Exception:
            os.remove(cached)
            raise ValueError("The downloaded file is not a .safetensors (unreadable header).")

    print(f"Downloaded {written / 1e6:.0f} MB from {host}")
    return cached


def download_repo_file(repo_link: str, filename: str):
    if not filename or filename == "(None)":
        return None

    # A local file (already downloaded, or uploaded into the Space) is used as it is.
    if os.path.exists(filename):
        return filename

    # A plain download link, in either box.
    if is_direct_link(filename):
        return download_direct_link(filename)
    if is_direct_link(repo_link) and filename == repo_link:
        return download_direct_link(repo_link)

    repo_from_file, weight = parse_hf_file_link(filename)
    if repo_from_file and weight:
        return hf_hub_download(
            repo_from_file, weight, token=HF_TOKEN,
            revision=PINNED_REVISIONS.get(repo_from_file),
        )
    repo_id = parse_repo_link(repo_link)
    if not repo_id:
        raise ValueError(f"Invalid repo/file: repo={repo_link}, file={filename}")
    return hf_hub_download(
        repo_id, filename, token=HF_TOKEN,
        revision=PINNED_REVISIONS.get(repo_id),
    )


def _load_path(pipe, path, adapter_name, into_t2=False):
    kwargs = {"adapter_name": adapter_name}
    if into_t2:
        kwargs["load_into_transformer_2"] = True
    pipe.load_lora_weights(path, **kwargs)
    return adapter_name


def load_lora_to_pipe(pipe, group_name, adapter_name="lora", high_scale=1.0, low_scale=1.0):
    if not group_name or group_name == "(None)":
        return [], []
    high_path, low_path = download_lora(group_name)
    names, weights = [], []
    if high_path:
        n = f"{adapter_name}_high"
        _load_path(pipe, high_path, n, into_t2=False)
        names.append(n)
        weights.append(float(high_scale))
    if low_path:
        n = f"{adapter_name}_low"
        _load_path(pipe, low_path, n, into_t2=True)
        names.append(n)
        weights.append(float(low_scale))
    if names:
        print(f"Loaded preset LoRA: {group_name} scales={weights}")
    return names, weights


def load_lora_slots(pipe, slots):
    all_names, all_weights = [], []
    for idx, slot in enumerate(slots or []):
        if not slot:
            continue
        preset = slot.get("preset")
        if preset and preset != "(None)":
            names, weights = load_lora_to_pipe(
                pipe, preset, adapter_name=f"lora_{idx}",
                high_scale=slot.get("high_scale", 1.0),
                low_scale=slot.get("low_scale", 1.0),
            )
            all_names.extend(names)
            all_weights.extend(weights)
            continue
        repo = slot.get("repo")
        fh = slot.get("file_high")
        fl = slot.get("file_low")
        hs = float(slot.get("high_scale", 1.0) or 1.0)
        ls = float(slot.get("low_scale", 1.0) or 1.0)
        if fh and fh != "(None)":
            path = download_repo_file(repo, fh)
            n = f"link_{idx}_high"
            _load_path(pipe, path, n, into_t2=False)
            all_names.append(n)
            all_weights.append(hs)
            print(f"Loaded link HIGH: {fh} scale={hs}")
        if fl and fl != "(None)":
            path = download_repo_file(repo, fl)
            n = f"link_{idx}_low"
            _load_path(pipe, path, n, into_t2=True)
            all_names.append(n)
            all_weights.append(ls)
            print(f"Loaded link LOW: {fl} scale={ls}")
    if all_names:
        pipe.set_adapters(all_names, adapter_weights=all_weights)
        print(f"Active adapters: {list(zip(all_names, all_weights))}")
        return True
    return False


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