MrReclusive commited on
Commit
c7655a6
·
verified ·
1 Parent(s): c0ce1c3

Upload comfy_bathroom.py

Browse files
Files changed (1) hide show
  1. comfy_bathroom.py +406 -0
comfy_bathroom.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Comfy Bathroom - LoRA Loading Suite for FP4 Quantized Models
3
+
4
+ A complete LoRA loading system designed for use with FP4ME/F4PMEL quantized LTX-2.3 models.
5
+
6
+ Author: Super Z
7
+ """
8
+
9
+ import comfy.utils
10
+ import folder_paths
11
+ import torch
12
+ import re
13
+ from typing import Dict, List, Optional, Tuple, Any
14
+
15
+
16
+ # =============================================================================
17
+ # PRESET CURVES
18
+ # =============================================================================
19
+
20
+ def generate_ramp_up(start_block: int, end_block: int, start_val: float, end_val: float) -> Dict[int, float]:
21
+ """Generate a smooth ramp between two blocks."""
22
+ curve = {}
23
+ if end_block <= start_block:
24
+ return curve
25
+ steps = end_block - start_block
26
+ for i, block in enumerate(range(start_block, end_block + 1)):
27
+ t = i / steps
28
+ curve[block] = start_val + (end_val - start_val) * t
29
+ return curve
30
+
31
+
32
+ def get_fp4me_light_weights() -> Dict[int, float]:
33
+ """FP4ME Light preset."""
34
+ weights = {}
35
+ weights[0] = 1.0
36
+ weights[1] = 0.0
37
+ weights.update(generate_ramp_up(2, 10, 0.10, 1.0))
38
+ for i in range(11, 40):
39
+ weights[i] = 1.0
40
+ weights.update(generate_ramp_up(40, 46, 0.95, 0.50))
41
+ weights[47] = 1.0
42
+ return weights
43
+
44
+
45
+ def get_fp4me_heavy_weights() -> Dict[int, float]:
46
+ """FP4ME Heavy preset."""
47
+ weights = {}
48
+ weights[0] = 1.0
49
+ weights[1] = 0.0
50
+ weights.update(generate_ramp_up(2, 10, 0.10, 1.0))
51
+ for i in range(11, 40):
52
+ weights[i] = 1.0
53
+ weights.update(generate_ramp_up(40, 46, 1.0, 0.0))
54
+ weights[47] = 1.0
55
+ return weights
56
+
57
+
58
+ def get_fp4mel_light_weights() -> Dict[int, float]:
59
+ """FP4MEL Light preset."""
60
+ weights = {}
61
+ weights[0] = 1.0
62
+ weights[1] = 1.0
63
+ weights.update(generate_ramp_up(2, 10, 0.10, 1.0))
64
+ for i in range(11, 41):
65
+ weights[i] = 1.0
66
+ weights.update(generate_ramp_up(41, 45, 0.95, 0.60))
67
+ weights[46] = 1.0
68
+ weights[47] = 1.0
69
+ return weights
70
+
71
+
72
+ def get_fp4mel_heavy_weights() -> Dict[int, float]:
73
+ """FP4MEL Heavy preset."""
74
+ weights = {}
75
+ weights[0] = 1.0
76
+ weights[1] = 1.0
77
+ weights.update(generate_ramp_up(2, 10, 0.10, 1.0))
78
+ for i in range(11, 40):
79
+ weights[i] = 1.0
80
+ weights.update(generate_ramp_up(40, 45, 0.95, 0.0))
81
+ weights[46] = 1.0
82
+ weights[47] = 1.0
83
+ return weights
84
+
85
+
86
+ def apply_block_weights_to_lora(lora_data: dict, block_weights: Dict[int, float]) -> dict:
87
+ """Apply per-block weights to LoRA data."""
88
+ filtered = {}
89
+ for key, value in lora_data.items():
90
+ block_match = re.search(r'transformer_blocks\.(\d+)\.', key)
91
+ if block_match:
92
+ block_idx = int(block_match.group(1))
93
+ weight = block_weights.get(block_idx, 1.0)
94
+ if weight > 0.0:
95
+ filtered[key] = value * weight if weight < 1.0 else value
96
+ else:
97
+ filtered[key] = value
98
+ return filtered
99
+
100
+
101
+ # =============================================================================
102
+ # TOOTHBRUSH - LoRA Loader
103
+ # =============================================================================
104
+
105
+ class ToothbrushLoRALoader:
106
+ PRESET_OPTIONS = ["default", "FP4ME Light", "FP4ME Heavy", "FP4MEL Light", "FP4MEL Heavy", "Custom"]
107
+
108
+ @classmethod
109
+ def INPUT_TYPES(s):
110
+ return {
111
+ "required": {
112
+ "lora_name": (folder_paths.get_filename_list("loras"),),
113
+ "preset": (s.PRESET_OPTIONS, {"default": "default"}),
114
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
115
+ },
116
+ "optional": {
117
+ "custom_weights": ("LORA_BLOCK_WEIGHTS",),
118
+ }
119
+ }
120
+
121
+ RETURN_TYPES = ("LORA_PACKET",)
122
+ RETURN_NAMES = ("lora",)
123
+ FUNCTION = "load_lora"
124
+ CATEGORY = "bathroom"
125
+ DESCRIPTION = "🪥 Toothbrush - LoRA loader with FP4 presets"
126
+
127
+ def load_lora(self, lora_name, preset, strength, custom_weights=None):
128
+ lora_path = folder_paths.get_full_path("loras", lora_name)
129
+ lora_data = comfy.utils.load_torch_file(lora_path, safe_load=False)
130
+
131
+ block_weights = None
132
+ if preset == "FP4ME Light":
133
+ block_weights = get_fp4me_light_weights()
134
+ elif preset == "FP4ME Heavy":
135
+ block_weights = get_fp4me_heavy_weights()
136
+ elif preset == "FP4MEL Light":
137
+ block_weights = get_fp4mel_light_weights()
138
+ elif preset == "FP4MEL Heavy":
139
+ block_weights = get_fp4mel_heavy_weights()
140
+ elif preset == "Custom":
141
+ block_weights = custom_weights
142
+
143
+ packet = {
144
+ "lora_data": lora_data,
145
+ "preset": preset,
146
+ "strength": strength,
147
+ "block_weights": block_weights,
148
+ "lora_name": lora_name,
149
+ }
150
+
151
+ print(f"🪥 Toothbrush: '{lora_name}' | {preset} | {strength:.2f}")
152
+ return (packet,)
153
+
154
+
155
+ # =============================================================================
156
+ # MIRROR SIMPLE - Binary On/Off
157
+ # =============================================================================
158
+
159
+ class MirrorSimple:
160
+ @classmethod
161
+ def INPUT_TYPES(s):
162
+ block_inputs = {f"block_{i}": ("BOOLEAN", {"default": True}) for i in range(48)}
163
+ return {
164
+ "required": block_inputs,
165
+ "optional": {"lora_packet": ("LORA_PACKET",)}
166
+ }
167
+
168
+ RETURN_TYPES = ("LORA_BLOCK_WEIGHTS", "LORA_PACKET")
169
+ RETURN_NAMES = ("block_weights", "lora_out")
170
+ FUNCTION = "configure"
171
+ CATEGORY = "bathroom"
172
+ DESCRIPTION = "🪞 Mirror (Simple) - Per-block on/off"
173
+
174
+ def configure(self, lora_packet=None, **kwargs):
175
+ block_weights = {i: (1.0 if kwargs.get(f"block_{i}", True) else 0.0) for i in range(48)}
176
+ disabled = [i for i, w in block_weights.items() if w == 0.0]
177
+ print(f"🪞 Mirror (Simple): {48-len(disabled)} ON, {len(disabled)} OFF")
178
+
179
+ out_packet = lora_packet.copy() if lora_packet else None
180
+ if out_packet:
181
+ out_packet["block_weights"] = block_weights
182
+ return (block_weights, out_packet)
183
+
184
+
185
+ # =============================================================================
186
+ # MIRROR FANCY - Per-Block Strength
187
+ # =============================================================================
188
+
189
+ class MirrorFancy:
190
+ @classmethod
191
+ def INPUT_TYPES(s):
192
+ block_inputs = {f"block_{i}": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}) for i in range(48)}
193
+ return {
194
+ "required": block_inputs,
195
+ "optional": {"lora_packet": ("LORA_PACKET",)}
196
+ }
197
+
198
+ RETURN_TYPES = ("LORA_BLOCK_WEIGHTS", "LORA_PACKET")
199
+ RETURN_NAMES = ("block_weights", "lora_out")
200
+ FUNCTION = "configure"
201
+ CATEGORY = "bathroom"
202
+ DESCRIPTION = "🪞 Mirror (Fancy) - Per-block strength"
203
+
204
+ def configure(self, lora_packet=None, **kwargs):
205
+ block_weights = {i: kwargs.get(f"block_{i}", 1.0) for i in range(48)}
206
+ active = sum(1 for w in block_weights.values() if w > 0)
207
+ print(f"🪞 Mirror (Fancy): {active} blocks active")
208
+
209
+ out_packet = lora_packet.copy() if lora_packet else None
210
+ if out_packet:
211
+ out_packet["block_weights"] = block_weights
212
+ return (block_weights, out_packet)
213
+
214
+
215
+ # =============================================================================
216
+ # BATHROOM SINK - LoRA Stacker
217
+ # =============================================================================
218
+
219
+ class BathroomSink:
220
+ @classmethod
221
+ def INPUT_TYPES(s):
222
+ return {
223
+ "required": {
224
+ "model": ("MODEL",),
225
+ "lora_1": ("LORA_PACKET",),
226
+ "global_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
227
+ },
228
+ "optional": {f"lora_{i}": ("LORA_PACKET",) for i in range(2, 9)}
229
+ }
230
+
231
+ RETURN_TYPES = ("MODEL",)
232
+ RETURN_NAMES = ("model",)
233
+ FUNCTION = "apply_loras"
234
+ CATEGORY = "bathroom"
235
+ DESCRIPTION = "🚰 Bathroom Sink - Stack multiple LoRAs"
236
+
237
+ def apply_loras(self, model, lora_1, global_strength, **kwargs):
238
+ lora_packets = [lora_1] + [kwargs.get(f"lora_{i}") for i in range(2, 9) if kwargs.get(f"lora_{i}")]
239
+
240
+ print(f"\n{'='*60}")
241
+ print(f"🚰 Bathroom Sink - {len(lora_packets)} LoRAs, global: {global_strength:.2f}")
242
+ print(f"{'='*60}")
243
+
244
+ model_out = model.clone()
245
+
246
+ for idx, packet in enumerate(lora_packets):
247
+ lora_data = packet["lora_data"]
248
+ strength = packet["strength"] * global_strength
249
+ block_weights = packet.get("block_weights")
250
+ lora_name = packet.get("lora_name", f"LoRA_{idx+1}")
251
+ preset = packet.get("preset", "default")
252
+
253
+ if block_weights:
254
+ processed_data = apply_block_weights_to_lora(lora_data, block_weights)
255
+ else:
256
+ processed_data = lora_data
257
+
258
+ print(f" [{idx+1}] {lora_name} | {preset} | {strength:.2f}")
259
+
260
+ # Apply using ComfyUI's standard LoRA mechanism
261
+ key_map = comfy.lora.model_lora_keys_unet(model_out.model)
262
+
263
+ try:
264
+ # Try loading - handle both old and new ComfyUI API
265
+ result = comfy.lora.load_lora(processed_data, key_map)
266
+
267
+ # Check if result is the new LoRAAdapter format
268
+ if hasattr(result, 'patches'):
269
+ # New API - LoRAAdapter object
270
+ model_out.add_patches(result.patches, strength)
271
+ elif isinstance(result, dict):
272
+ # Old API - patch dict
273
+ model_out.add_patches(result, strength)
274
+ else:
275
+ # Try to apply directly
276
+ model_out.add_patches(result, strength)
277
+
278
+ except Exception as e:
279
+ print(f" ⚠️ LoRA load error: {e}")
280
+ # Fallback: use the original approach
281
+ try:
282
+ # Build patches manually
283
+ patches = self._build_patches(processed_data, key_map)
284
+ if patches:
285
+ model_out.add_patches(patches, strength)
286
+ except Exception as e2:
287
+ print(f" ⚠️ Fallback failed: {e2}")
288
+
289
+ print(f"{'='*60}\n")
290
+ return (model_out,)
291
+
292
+ def _build_patches(self, lora_data, key_map):
293
+ """Build patch dict manually."""
294
+ patches = {}
295
+
296
+ for lora_key, lora_value in lora_data.items():
297
+ # Find the model key
298
+ model_key = key_map.get(lora_key, None)
299
+ if model_key is None:
300
+ continue
301
+
302
+ if model_key not in patches:
303
+ patches[model_key] = []
304
+
305
+ # Add as a diff patch
306
+ if ".lora_A.weight" in lora_key:
307
+ # Find the matching lora_B
308
+ b_key = lora_key.replace(".lora_A.weight", ".lora_B.weight")
309
+ if b_key in lora_data:
310
+ lora_b = lora_data[b_key]
311
+ # Compute delta
312
+ if lora_value.dim() == 2 and lora_b.dim() == 2:
313
+ delta = torch.mm(lora_b, lora_value)
314
+ patches[model_key].append(("diff", delta))
315
+ elif ".lora_B.weight" not in lora_key:
316
+ # Direct value (diff format)
317
+ patches[model_key].append(("diff", lora_value))
318
+
319
+ return patches
320
+
321
+
322
+ # =============================================================================
323
+ # SHOWER - Quick Preset
324
+ # =============================================================================
325
+
326
+ class ShowerPreset:
327
+ PRESET_OPTIONS = ["FP4ME Light", "FP4ME Heavy", "FP4MEL Light", "FP4MEL Heavy"]
328
+
329
+ @classmethod
330
+ def INPUT_TYPES(s):
331
+ return {
332
+ "required": {
333
+ "lora_packet": ("LORA_PACKET",),
334
+ "preset": (s.PRESET_OPTIONS, {"default": "FP4ME Light"}),
335
+ }
336
+ }
337
+
338
+ RETURN_TYPES = ("LORA_PACKET",)
339
+ RETURN_NAMES = ("lora_out",)
340
+ FUNCTION = "apply_preset"
341
+ CATEGORY = "bathroom"
342
+ DESCRIPTION = "🚿 Shower - Quick preset"
343
+
344
+ def apply_preset(self, lora_packet, preset):
345
+ out = lora_packet.copy()
346
+ presets = {
347
+ "FP4ME Light": get_fp4me_light_weights,
348
+ "FP4ME Heavy": get_fp4me_heavy_weights,
349
+ "FP4MEL Light": get_fp4mel_light_weights,
350
+ "FP4MEL Heavy": get_fp4mel_heavy_weights,
351
+ }
352
+ out["block_weights"] = presets[preset]()
353
+ out["preset"] = preset
354
+ print(f"🚿 Shower: {preset}")
355
+ return (out,)
356
+
357
+
358
+ # =============================================================================
359
+ # TOWEL - Info Display
360
+ # =============================================================================
361
+
362
+ class TowelInfo:
363
+ @classmethod
364
+ def INPUT_TYPES(s):
365
+ return {"required": {"lora_packet": ("LORA_PACKET",)}}
366
+
367
+ RETURN_TYPES = ("LORA_PACKET", "STRING")
368
+ RETURN_NAMES = ("lora_out", "info")
369
+ FUNCTION = "display_info"
370
+ CATEGORY = "bathroom"
371
+ OUTPUT_NODE = True
372
+ DESCRIPTION = "🧾 Towel - Info"
373
+
374
+ def display_info(self, lora_packet):
375
+ lines = [
376
+ f"LoRA: {lora_packet.get('lora_name', '?')}",
377
+ f"Preset: {lora_packet.get('preset', '?')}",
378
+ f"Strength: {lora_packet.get('strength', 1):.2f}",
379
+ ]
380
+ bw = lora_packet.get("block_weights")
381
+ if bw:
382
+ lines.append(f"Disabled: {[i for i,w in bw.items() if w<0.01]}")
383
+ return (lora_packet, "\n".join(lines))
384
+
385
+
386
+ # =============================================================================
387
+ # NODE MAPPINGS
388
+ # =============================================================================
389
+
390
+ NODE_CLASS_MAPPINGS = {
391
+ "Toothbrush LoRA Loader": ToothbrushLoRALoader,
392
+ "Mirror (Simple)": MirrorSimple,
393
+ "Mirror (Fancy)": MirrorFancy,
394
+ "Bathroom Sink": BathroomSink,
395
+ "Shower Preset": ShowerPreset,
396
+ "Towel Info": TowelInfo,
397
+ }
398
+
399
+ NODE_DISPLAY_NAME_MAPPINGS = {
400
+ "Toothbrush LoRA Loader": "🪥 Toothbrush",
401
+ "Mirror (Simple)": "🪞 Mirror (Simple)",
402
+ "Mirror (Fancy)": "🪞 Mirror (Fancy)",
403
+ "Bathroom Sink": "🚰 Bathroom Sink",
404
+ "Shower Preset": "🚿 Shower",
405
+ "Towel Info": "🧾 Towel",
406
+ }