diff --git a/__init__.py b/__init__.py index 2bfe08b..bdbef6d 100644 --- a/__init__.py +++ b/__init__.py @@ -34,9 +34,12 @@ async def comfy_entrypoint() -> LCSExtension: # V2 backward compatibility from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +WEB_DIRECTORY = "./web" + __all__ = [ "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", + "WEB_DIRECTORY", "LCSExtension", "comfy_entrypoint", ] diff --git a/nodes/intervene.py b/nodes/intervene.py index fcb5fa4..0492183 100644 --- a/nodes/intervene.py +++ b/nodes/intervene.py @@ -440,18 +440,12 @@ def _build_tone_fn(lcs_data, contrast, brightness, saturation, color_temperature return post_cfg_fn -TONE_PRESETS = { - "Cinematic": {"contrast": 1.20, "brightness": -0.05, "saturation": 0.90, "color_temperature": 0.15}, - "HDR": {"contrast": 1.40, "brightness": 0.0, "saturation": 1.20, "color_temperature": 0.0}, - "Vivid": {"contrast": 1.10, "brightness": 0.0, "saturation": 1.50, "color_temperature": 0.0}, - "Dramatic": {"contrast": 1.50, "brightness": -0.10, "saturation": 0.85, "color_temperature": 0.0}, - "Low Key": {"contrast": 1.30, "brightness": -0.20, "saturation": 0.80, "color_temperature": 0.0}, - "High Key": {"contrast": 0.80, "brightness": 0.20, "saturation": 0.90, "color_temperature": 0.0}, - "Warm": {"contrast": 1.0, "brightness": 0.0, "saturation": 1.0, "color_temperature": 0.50}, - "Cool": {"contrast": 1.0, "brightness": 0.0, "saturation": 1.0, "color_temperature": -0.50}, - "Desaturated": {"contrast": 1.0, "brightness": 0.0, "saturation": 0.40, "color_temperature": 0.0}, - "Custom": None, -} +# Preset names for the combo widget. Actual values live in web/js/tone_preset.js +# which syncs them into the slider widgets on the frontend. +TONE_PRESET_NAMES = [ + "Cinematic", "HDR", "Vivid", "Dramatic", "Low Key", + "High Key", "Warm", "Cool", "Desaturated", "Custom", +] class LCSToneAdjust(io.ComfyNode): @@ -476,16 +470,16 @@ class LCSToneAdjust(io.ComfyNode): inputs=[ io.Model.Input("model"), LCS_DATA.Input("lcs_data", tooltip="Calibration data from LCSCalibrate or LCSLoadData"), - io.Combo.Input("preset", options=list(TONE_PRESETS.keys()), default="Custom", + io.Combo.Input("preset", options=TONE_PRESET_NAMES, default="Custom", tooltip="Select a tonal preset or Custom to use the sliders below"), - io.Float.Input("contrast", default=1.0, min=0.0, max=3.0, step=0.05, optional=True, - tooltip="[Custom only] Lightness contrast multiplier (>1 = more contrast, <1 = less, 1 = no change)"), - io.Float.Input("brightness", default=0.0, min=-1.0, max=1.0, step=0.05, optional=True, - tooltip="[Custom only] Lightness shift (>0 = brighter, <0 = darker)"), - io.Float.Input("saturation", default=1.0, min=0.0, max=3.0, step=0.05, optional=True, - tooltip="[Custom only] Saturation multiplier (>1 = more vivid, <1 = more muted, 0 = grayscale)"), - io.Float.Input("color_temperature", default=0.0, min=-2.0, max=2.0, step=0.05, optional=True, - tooltip="[Custom only] Color temperature shift (>0 = warmer/amber, <0 = cooler/blue)"), + io.Float.Input("contrast", default=1.0, min=0.0, max=3.0, step=0.05, + tooltip="Lightness contrast multiplier (>1 = more contrast, <1 = less, 1 = no change)"), + io.Float.Input("brightness", default=0.0, min=-1.0, max=1.0, step=0.05, + tooltip="Lightness shift (>0 = brighter, <0 = darker)"), + io.Float.Input("saturation", default=1.0, min=0.0, max=3.0, step=0.05, + tooltip="Saturation multiplier (>1 = more vivid, <1 = more muted, 0 = grayscale)"), + io.Float.Input("color_temperature", default=0.0, min=-2.0, max=2.0, step=0.05, + tooltip="Color temperature shift (>0 = warmer/amber, <0 = cooler/blue)"), io.Int.Input("start_step", default=5, min=0, max=50, tooltip="First step to apply adjustment"), io.Int.Input("end_step", default=15, min=0, max=50, @@ -499,22 +493,13 @@ class LCSToneAdjust(io.ComfyNode): ) @classmethod - def execute(cls, model, lcs_data, preset, start_step, end_step, - contrast=None, brightness=None, saturation=None, color_temperature=None, - mask=None) -> io.NodeOutput: - """Clone model, attach LCS tone adjustment hook. Returns patched MODEL.""" - if preset != "Custom": - p = TONE_PRESETS[preset] - contrast = p["contrast"] - brightness = p["brightness"] - saturation = p["saturation"] - color_temperature = p["color_temperature"] - else: - contrast = contrast if contrast is not None else 1.0 - brightness = brightness if brightness is not None else 0.0 - saturation = saturation if saturation is not None else 1.0 - color_temperature = color_temperature if color_temperature is not None else 0.0 + def execute(cls, model, lcs_data, preset, contrast, brightness, saturation, + color_temperature, start_step, end_step, mask=None) -> io.NodeOutput: + """Clone model, attach LCS tone adjustment hook. Returns patched MODEL. + The preset combo sets slider values via frontend JS; execute() always + uses the actual slider values so the user can tweak after selecting a preset. + """ m = model.clone() # Skip hook entirely when all parameters are at default (true no-op) if not _is_default_tone(contrast, brightness, saturation, color_temperature): diff --git a/web/js/tone_preset.js b/web/js/tone_preset.js new file mode 100644 index 0000000..b2e639e --- /dev/null +++ b/web/js/tone_preset.js @@ -0,0 +1,49 @@ +import { app } from "../../../scripts/app.js"; + +// Preset values — the single source of truth for preset parameters. +// TONE_PRESET_NAMES in nodes/intervene.py must list matching keys. +const TONE_PRESETS = { + "Cinematic": { contrast: 1.20, brightness: -0.05, saturation: 0.90, color_temperature: 0.15 }, + "HDR": { contrast: 1.40, brightness: 0.0, saturation: 1.20, color_temperature: 0.0 }, + "Vivid": { contrast: 1.10, brightness: 0.0, saturation: 1.50, color_temperature: 0.0 }, + "Dramatic": { contrast: 1.50, brightness: -0.10, saturation: 0.85, color_temperature: 0.0 }, + "Low Key": { contrast: 1.30, brightness: -0.20, saturation: 0.80, color_temperature: 0.0 }, + "High Key": { contrast: 0.80, brightness: 0.20, saturation: 0.90, color_temperature: 0.0 }, + "Warm": { contrast: 1.0, brightness: 0.0, saturation: 1.0, color_temperature: 0.50 }, + "Cool": { contrast: 1.0, brightness: 0.0, saturation: 1.0, color_temperature: -0.50 }, + "Desaturated": { contrast: 1.0, brightness: 0.0, saturation: 0.40, color_temperature: 0.0 }, +}; + +const SLIDER_NAMES = ["contrast", "brightness", "saturation", "color_temperature"]; + +function syncPreset(node, presetName) { + const preset = TONE_PRESETS[presetName]; + if (!preset) return; // "Custom" — leave sliders as-is + + for (const name of SLIDER_NAMES) { + const widget = node.widgets?.find(w => w.name === name); + if (widget && name in preset) { + widget.value = preset[name]; + } + } + node.graph?.setDirtyCanvas(true, true); +} + +app.registerExtension({ + name: "ComfyUI-LCS.TonePresetSync", + + nodeCreated(node) { + if (node.comfyClass !== "LCSToneAdjust") return; + + const presetWidget = node.widgets?.find(w => w.name === "preset"); + if (!presetWidget) return; + + // Only sync on explicit user interaction (dropdown change), not on + // workflow load or paste — those restore saved slider values directly. + const origCallback = presetWidget.callback; + presetWidget.callback = function (value, canvas, nodeRef, pos, event) { + if (origCallback) origCallback.call(this, value, canvas, nodeRef, pos, event); + syncPreset(node, value); + }; + }, +});