From 013a962cfd37891eb43ca2fe559362db6d42f1a3 Mon Sep 17 00:00:00 2001 From: JJ Date: Tue, 10 Feb 2026 16:33:46 +0000 Subject: [PATCH] both toggle --- nodes/power_loaders.py | 31 ++++++++---- utility/rgthree_api/routes_model_info.py | 5 -- .../power_lora_loader_v2.js | 13 +++++ web/power_lora_loader_js/widgets.js | 47 ++++++++++++++++++- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/nodes/power_loaders.py b/nodes/power_loaders.py index 84bca4a..b0a2e16 100644 --- a/nodes/power_loaders.py +++ b/nodes/power_loaders.py @@ -368,7 +368,8 @@ class PowerLoraLoaderV2: def __init__(self): self.properties = { - "Show Strengths": "Single Strength" # Single Strength or Separate Model & Clip + "Show Strengths": "Single Strength", # Single Strength or Separate Model & Clip + "output_both": False # Apply all loras to model_low using low strength values } @classmethod @@ -397,10 +398,13 @@ class PowerLoraLoaderV2: """Combines functionality from both v1 and v2 loaders.""" # Get show_strengths property show_strengths = getattr(self, 'Show Strengths', 'Single Strength') - + # Check if clip is actually connected (from JavaScript hasClip property) has_clip = getattr(self, 'hasClip', clip is not None) + # Check if output_both is enabled (from widget value or property) + output_both = kwargs.get('output_both', self.properties.get('output_both', False)) + # Initialize lists for WanVideo format loras_list = [] low_loras_list = [] @@ -438,7 +442,6 @@ class PowerLoraLoaderV2: if os.path.exists(lora_path): # Extract just the filename for consistency with the rest of the code lora_filename = os.path.basename(lora_name.replace('\\', '/')) - print(f"[PowerLoraLoaderV2] Found LoRA at full path: '{lora_path}'") else: print(f"[PowerLoraLoaderV2] LoRA file '{lora_name}' not found in loras folder.") continue @@ -499,6 +502,23 @@ class PowerLoraLoaderV2: loras_list.append(lora_entry) + # Handle output_both mode - apply all loras to model_low using low_strength + if output_both: + if model_low is not None: + low_strength = value.get('low_strength', strength_model) + if low_strength is None: + low_strength = 0.0 + try: + if not has_clip: + model_low, _ = LoraLoader().load_lora(model_low, None, lora_filename, low_strength, 0) + else: + model_low, clip = LoraLoader().load_lora(model_low, clip, lora_filename, low_strength, 0) + low_loras_summary.append((low_strength, f"{lora_filename} (output_both)")) + except Exception as e: + print(f"[PowerLoraLoaderV2] Error applying LoRA to model_low (output_both): {e}") + else: + pass # model_low is None, cannot apply OUTPUT_BOTH + # Check if JavaScript detected special mode flags use_for_both = value.get('use_for_both', False) low_only = value.get('low_only', False) @@ -511,12 +531,10 @@ class PowerLoraLoaderV2: # Handle different LoRA states for PowerLoraLoaderV2 if high_only: # "High only" mode - apply to main model only (same as default but with visual indicator) - print(f"[PowerLoraLoaderV2] Using LoRA '{lora_filename}' for HIGH model only") high_loras_summary.append((strength_model, f"{lora_filename} (high only)")) elif low_only: # "Low only" mode - apply to model_low only - print(f"[PowerLoraLoaderV2] Using LoRA '{lora_filename}' for LOW model only") # Apply LoRA to model_low if it exists if model_low is not None: try: @@ -531,7 +549,6 @@ class PowerLoraLoaderV2: elif use_for_both: # "Enable for both" mode - apply to both main and model_low - print(f"[PowerLoraLoaderV2] Using LoRA '{lora_filename}' for both HIGH and LOW models") high_loras_summary.append((strength_model, f"{lora_filename} (both)")) # Apply LoRA to model_low if it exists @@ -581,8 +598,6 @@ class PowerLoraLoaderV2: else: # Clip provided, apply to model_low with clip model_low, _ = LoraLoader().load_lora(model_low, clip, low_lora_path, low_strength, strength_clip if strength_clip is not None else 0) - - print(f"[PowerLoraLoaderV2] Applied low variant LoRA '{low_variant_name}' (strength: {low_strength}) to model_low") except Exception as e: print(f"[PowerLoraLoaderV2] Error applying low variant LoRA '{low_variant_name}' to model_low: {e}") diff --git a/utility/rgthree_api/routes_model_info.py b/utility/rgthree_api/routes_model_info.py index 511a520..b539faf 100644 --- a/utility/rgthree_api/routes_model_info.py +++ b/utility/rgthree_api/routes_model_info.py @@ -434,12 +434,10 @@ async def api_save_lora_preview_image(request): base_dir = folder_paths.get_folder_paths('checkpoints')[0] # Use _power_preview subdirectory for model previews power_preview_dir = os.path.join(base_dir, '_power_preview') - print(f"[Preview Image Debug] MODEL: Using directory {power_preview_dir}") else: # For LoRAs, use loras directory base_dir = folder_paths.get_folder_paths('loras')[0] power_preview_dir = os.path.join(base_dir, '_power_preview') - print(f"[Preview Image Debug] LORA: Using directory {power_preview_dir}") if lora_path: save_dir = os.path.join(power_preview_dir, lora_path) @@ -448,13 +446,10 @@ async def api_save_lora_preview_image(request): # Ensure directory exists os.makedirs(save_dir, exist_ok=True) - print(f"[Preview Image Debug] Directory ensured: {save_dir}") - print(f"[Preview Image Debug] Directory exists check: {os.path.exists(save_dir)}") # Construct save path with optional suffix filename = f"{lora_name}{suffix}.jpg" save_path = os.path.join(save_dir, filename) - print(f"[Preview Image Debug] Saving image to: {save_path}") # Read image data image_data = image_file.file.read() diff --git a/web/power_lora_loader_js/power_lora_loader_v2.js b/web/power_lora_loader_js/power_lora_loader_v2.js index af83469..8ac16bf 100644 --- a/web/power_lora_loader_js/power_lora_loader_v2.js +++ b/web/power_lora_loader_js/power_lora_loader_v2.js @@ -146,6 +146,7 @@ app.registerExtension({ this.hasClip = false; // Initialize clip connection state this.properties = this.properties || {}; this.properties['Show Strengths'] = "Single Strength"; + this.properties['output_both'] = false; this.addNonLoraWidgets(); @@ -221,6 +222,12 @@ app.registerExtension({ } } } + + // Sync output_both property to widget + const outputBothWidget = this.widgets.find(w => w.name === "output_both"); + if (outputBothWidget && this.properties.output_both !== undefined) { + outputBothWidget.value = this.properties.output_both; + } }; nodeType.prototype.addNewLoraWidget = function () { @@ -253,6 +260,12 @@ app.registerExtension({ { serialize: true }); uiStateWidget.type = "hidden"; // Keep serialize: true to ensure UI state is saved in workflow JSON + + // Add hidden widget for output_both toggle state + const outputBothValue = this.properties.output_both || false; + const outputBothWidget = this.addWidget("boolean", "output_both", outputBothValue, () => {}, + { serialize: true }); + outputBothWidget.type = "hidden"; }; nodeType.prototype.allLorasState = function() { diff --git a/web/power_lora_loader_js/widgets.js b/web/power_lora_loader_js/widgets.js index 10b5b4b..267cbd0 100644 --- a/web/power_lora_loader_js/widgets.js +++ b/web/power_lora_loader_js/widgets.js @@ -252,6 +252,7 @@ export class StrengthCopyWidget extends RgthreeBaseWidget { super("StrengthCopyWidget"); this.value = {}; this.hitAreas = { + output_both_toggle: { bounds: [0, 0] }, high_to_low_button: { bounds: [0, 0] }, low_to_high_button: { bounds: [0, 0] }, }; @@ -260,19 +261,39 @@ export class StrengthCopyWidget extends RgthreeBaseWidget { } draw(ctx, node, w, posY, height) { + // Ensure property exists (off by default) + if (!node.properties) node.properties = {}; + if (node.properties['output_both'] === undefined) node.properties['output_both'] = false; + // Constants const margin = 20; const buttonWidth = 25; const buttonHeight = height; const buttonSpacing = 36; const rightOffset = 25; + const innerMargin = 8; // Calculated values const midY = posY + height * 0.5; const totalButtonsWidth = (buttonWidth * 2) + buttonSpacing; - const rightX = w - margin - totalButtonsWidth - rightOffset; ctx.save(); + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; + + // Draw "Output both" toggle + const toggleSpacing = 15; // Extra spacing between toggle and buttons + let posX = w - margin - totalButtonsWidth - rightOffset - toggleSpacing; + const outputBothValue = node.properties['output_both'] || false; + // Draw label and toggle from right to left + const toggleBounds = drawTogglePart(ctx, [posX - height * 1.5, posY], height, outputBothValue); + this.hitAreas['output_both_toggle'].bounds = toggleBounds; + const labelText = "Use loras on both"; + ctx.fillText(labelText, posX - height * 1.5 - ctx.measureText(labelText).width - innerMargin, midY); + + // Adjust rightX for buttons (they stay in same position) + const rightX = w - margin - totalButtonsWidth - rightOffset; // Draw ">" button (high to low) const highToLowX = rightX; @@ -288,6 +309,21 @@ export class StrengthCopyWidget extends RgthreeBaseWidget { } onMouseDown(event, pos, node) { + // Ensure property exists + if (!node.properties) node.properties = {}; + if (node.properties['output_both'] === undefined) node.properties['output_both'] = false; + + if (this.clickWasWithinBounds(pos, this.hitAreas.output_both_toggle.bounds)) { + node.properties['output_both'] = !node.properties['output_both']; + // Also sync to hidden widget for Python to read + const outputBothWidget = node.widgets.find(w => w.name === "output_both"); + if (outputBothWidget) { + outputBothWidget.value = node.properties['output_both']; + } + this.cancelMouseDown(); + node.setDirtyCanvas(true, true); + return true; + } if (this.clickWasWithinBounds(pos, this.hitAreas.high_to_low_button.bounds)) { this.highToLowPressed = true; node.setDirtyCanvas(true, true); @@ -339,6 +375,15 @@ export class StrengthCopyWidget extends RgthreeBaseWidget { } } + serializeValue(node, index) { + // Ensure output_both property exists and is synchronized + if (!node.properties) node.properties = {}; + if (node.properties['output_both'] === undefined) node.properties['output_both'] = false; + // Return the value to ensure it's serialized + node.properties['output_both'] = node.properties['output_both'] || false; + return {}; + } + computeSize(width) { return [width, LiteGraph.NODE_WIDGET_HEIGHT]; }