import gradio as gr from shared.utils.plugins import WAN2GPPlugin import json import traceback class LoraMultipliersUIPlugin(WAN2GPPlugin): def __init__(self): super().__init__() self.name = "Lora Multipliers UI" self.version = "1.0.9" self.description = "Dynamically set lora multipliers with a fast, JavaScript-powered UI." self.previous_loras_state = {} self.request_component("loras_multipliers") self.request_component("loras_choices") self.request_component("guidance_phases") self.request_component("num_inference_steps") self.request_component("main") def post_ui_setup(self, components: dict) -> dict: try: loras_multipliers = components["loras_multipliers"] loras_choices = components["loras_choices"] guidance_phases = components["guidance_phases"] num_inference_steps = components["num_inference_steps"] main_ui_block = components["main"] instance_id = loras_multipliers._id if instance_id not in self.previous_loras_state: self.previous_loras_state[instance_id] = {'loras': [], 'accelerators': [], 'multipliers': {}} def create_and_wire_ui(): container_id = f"lora_multiplier_ui_container_{instance_id}" update_btn_id = f"lora_mults_update_btn_{instance_id}" hidden_input_id = f"lora_mults_hidden_input_{instance_id}" js_renderer_func = f"wgpLoraUIRenderer_{instance_id}" css = f""" """ main_js_script = f""" () => {{ const debounce = (func, delay) => {{ let timeout; return (...args) => {{ clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }}; }}; const updatePythonTextbox_{instance_id} = debounce(() => {{ const container = document.getElementById('{container_id}'); if (!container) return; const loras = Array.from(container.querySelectorAll('.lora-main-container')); const textboxStrings = []; for (const loraEl of loras) {{ const splits = Array.from(loraEl.querySelectorAll('.lora-step-split-container')); const loraStepStrings = []; for (const splitEl of splits) {{ const sliders = Array.from(splitEl.querySelectorAll('input[type=range]')); const phaseValues = sliders .filter(s => !s.closest('.lora-slider-group').classList.contains('hidden')) .map(s => {{ const val = parseFloat(s.value); return val % 1 === 0 ? String(val) : val.toFixed(2).replace(/\\.?0+$/, ''); }}); if (phaseValues.length > 0) {{ loraStepStrings.push(phaseValues.join(';')); }} }} if (loraStepStrings.length > 0) {{ textboxStrings.push(loraStepStrings.join(',')); }} }} const separatorIndex = parseInt(container.dataset.separatorIndex || '-1'); let finalString = ""; if (separatorIndex > 0 && separatorIndex <= textboxStrings.length) {{ const part1 = textboxStrings.slice(0, separatorIndex).join(' '); const part2 = textboxStrings.slice(separatorIndex).join(' '); finalString = part1 + '|' + part2; }} else {{ finalString = textboxStrings.join(' '); }} const hiddenInput = document.querySelector('#{hidden_input_id} textarea'); const updateButton = document.getElementById('{update_btn_id}'); if (hiddenInput && updateButton) {{ hiddenInput.value = finalString; hiddenInput.dispatchEvent(new Event('input', {{ bubbles: true }})); updateButton.click(); }} }}, 200); function createSlider_{instance_id}(phase, value, isVisible) {{ const container = document.createElement('div'); container.className = 'lora-slider-group'; if (!isVisible) container.classList.add('hidden'); const initialValue = parseFloat(value); container.innerHTML = `
`; const rangeInput = container.querySelector('input[type="range"]'); const numberInput = container.querySelector('input[type="number"]'); const syncAndUpdate = (source) => {{ let val = parseFloat(source.value); if (isNaN(val)) val = 0; val = Math.max(0, Math.min(1, val)); if (source === rangeInput) {{ numberInput.value = val.toFixed(2); }} else {{ rangeInput.value = val; if (source.value !== val.toFixed(2)) {{ numberInput.value = val.toFixed(2); }} }} updatePythonTextbox_{instance_id}(); }}; rangeInput.addEventListener('input', () => syncAndUpdate(rangeInput)); numberInput.addEventListener('input', () => syncAndUpdate(numberInput)); return container; }} function createStepSplit_{instance_id}(loraIndex, splitIndex, values, guidancePhases, stepText) {{ const splitContainer = document.createElement('div'); splitContainer.className = 'lora-step-split-container'; const sliderRow = document.createElement('div'); sliderRow.className = 'lora-slider-row'; const title = document.createElement('div'); title.className = 'lora-split-title'; title.innerHTML = `${{stepText}}`; splitContainer.appendChild(title); for (let i = 0; i < 3; i++) {{ const isVisible = (i + 1) <= guidancePhases; const sliderValue = values[i] !== undefined ? values[i] : 1.0; sliderRow.appendChild(createSlider_{instance_id}(i + 1, sliderValue, isVisible)); }} splitContainer.appendChild(sliderRow); return splitContainer; }} function updateRejoinVisibility_{instance_id}(loraContainer) {{ if (!loraContainer) return; const splits = loraContainer.querySelectorAll('.lora-step-split-container'); const rejoinBtn = loraContainer.querySelector('.rejoin-btn'); if (rejoinBtn) {{ rejoinBtn.style.display = splits.length > 1 ? 'inline-block' : 'none'; }} }} function recalculateStepRanges_{instance_id}(loraContainer) {{ const totalSteps = window.wgp_total_steps_{instance_id} || 1; const splits = loraContainer.querySelectorAll('.lora-step-split-container'); const numSplits = splits.length; if (numSplits === 0) return; const stepsPerSplit = Math.floor(totalSteps / numSplits); const remainder = totalSteps % numSplits; let startStep = 0; splits.forEach((split, i) => {{ const stepsInThisSplit = stepsPerSplit + (i < remainder ? 1 : 0); const endStep = startStep + stepsInThisSplit; const titleStrong = split.querySelector('.lora-split-title strong'); if(titleStrong) {{ const displayEnd = Math.max(startStep + 1, endStep); titleStrong.textContent = `Steps ${{startStep + 1}} to ${{displayEnd}}`; }} startStep = endStep; }}); }} function handleSplit_{instance_id}(e) {{ const loraIndex = parseInt(e.target.dataset.loraIndex); const loraContainer = document.getElementById(`lora-container-{instance_id}-${{loraIndex}}`); const newSplit = createStepSplit_{instance_id}(loraIndex, -1, [1.0, 1.0, 1.0], window.wgp_guidance_phases_{instance_id}, ""); loraContainer.appendChild(newSplit); recalculateStepRanges_{instance_id}(loraContainer); updateRejoinVisibility_{instance_id}(loraContainer); updatePythonTextbox_{instance_id}(); }} function handleRejoin_{instance_id}(e) {{ const loraIndex = parseInt(e.target.dataset.loraIndex); const loraContainer = document.getElementById(`lora-container-{instance_id}-${{loraIndex}}`); const splits = loraContainer.querySelectorAll('.lora-step-split-container'); if (splits.length > 1) {{ splits[splits.length - 1].remove(); recalculateStepRanges_{instance_id}(loraContainer); updateRejoinVisibility_{instance_id}(loraContainer); updatePythonTextbox_{instance_id}(); }} }} window.{js_renderer_func} = (jsonData) => {{ let data; try {{ data = JSON.parse(jsonData); }} catch (e) {{ console.error('Error parsing Lora UI JSON:', e); return; }} if (!data) return; const container = document.getElementById('{container_id}'); if (!container) return; container.innerHTML = ''; window.wgp_guidance_phases_{instance_id} = data.guidance_phases; window.wgp_total_steps_{instance_id} = data.total_steps; container.dataset.separatorIndex = data.separator_index; const createHeader = (text) => {{ const headerDiv = document.createElement('div'); headerDiv.className = 'lora-section-header'; headerDiv.innerHTML = `