refactor: rename Style Pick \u2192 Pick Style, Style Blend \u2192 Blend Style (verb-first node names)

This commit is contained in:
Immac
2026-03-04 23:52:50 -06:00
parent 1552484f65
commit f5f4e050c9
8 changed files with 110 additions and 43 deletions
+4 -4
View File
@@ -9,9 +9,9 @@ from comfy_api.latest import ComfyExtension, io
from .src.immac_tools.api import register_routes
from .src.immac_tools.style_mix_node import PickMixNode, StyleMixNode
from .src.immac_tools.style_pick_node import StylePickNode
from .src.immac_tools.style_pick_node import PickStyleNode
from .src.immac_tools.style_weight_node import StyleWeightNode
from .src.immac_tools.style_blend_node import StyleBlendNode
from .src.immac_tools.style_blend_node import BlendStyleNode
from .src.immac_tools.save_mix_node import SaveMixNode
from .src.immac_tools.save_style_node import SaveStyleNode
@@ -20,9 +20,9 @@ class ImmacStyleMixerExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
PickMixNode,
StylePickNode,
PickStyleNode,
StyleWeightNode,
StyleBlendNode,
BlendStyleNode,
SaveMixNode,
SaveStyleNode,
]
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+89 -22
View File
@@ -47,11 +47,19 @@ _MODE = [
]
def _update_mix(mix: dict, mix_styles: list, example_image, name: str) -> None:
"""Patch styles and optionally the image on an existing mix dict in place."""
def _update_mix(
mix: dict,
mix_styles: list,
example_image,
name: str,
favorite: bool | None = None,
) -> None:
"""Patch styles and optionally the image/name/favorite on an existing mix dict in place."""
mix["styles"] = mix_styles
if name:
mix["name"] = name
if favorite is not None:
mix["favorite"] = favorite
if example_image is not None:
mix["image_filename"] = _save_mix_image(
example_image,
@@ -61,27 +69,64 @@ def _update_mix(mix: dict, mix_styles: list, example_image, name: str) -> None:
mix["image_updated_at"] = int(time.time())
def _derive_name(mix_styles: list, data: dict) -> str:
"""Build a 'Quick Mix (A, B, C)' label from the style names in blend order.
Unresolved style_ids fall back to the first 8 hex chars of the id.
"""
style_index = {s["id"]: s.get("name", "") for s in data.get("styles", [])}
parts = []
for entry in mix_styles:
sid = entry.get("style_id", "")
parts.append(style_index.get(sid) or sid[:8])
joined = ", ".join(parts) if parts else "empty"
return f"Quick Mix ({joined})"
class SaveMixNode(io.ComfyNode):
"""Creates or updates a saved mix from a blend_json (output of Style Blend).
mode controls how the target record is resolved and what happens when it
does or does not exist:
Create — inserts a new mix. Requires a non-empty name. The id input
must not be connected (ids are assigned automatically).
Create — inserts a new mix. The id input must not be connected (ids are
assigned automatically). If name is empty an auto-name of the
form 'Quick Mix (A, B, C)' is derived from the blend contents.
Fails if a mix with the same name already exists.
Update — modifies an existing mix (by id or by name); fails if not found.
Connect either an id or enter a name — not both.
Save — updates if found (by id or by name), creates if not found by
name. Fails if id is given but no match exists. Requires either
an id or a name, but not both.
name. If name is empty on create an auto-name is derived.
Fails if id is given but no match exists.
The blend_json input must be wired from the blend_json output of a Style
Blend node. Each entry in that JSON array encodes a {style_id, weight}
pair that will be stored in the mix exactly as produced by Style Blend
(including any deduplication / normalisation already applied there).
Behaviour matrix (id = connected, name = non-empty, * = auto-derived):
mode id name found? result
Create – – exists ✅ replace existing Quick Mix (auto-derived name upsert)
Create – – – ✅ insert new record (name auto-derived)
Create – ✓ exists error: name already taken; use Update or Save
Create – ✓ – ✅ insert new record
Create ✓ – – error: id must not be connected in Create mode
Create ✓ ✓ – error: id must not be connected in Create mode
Update – – – error: id or name required
Update – ✓ exists ✅ update by name
Update – ✓ – error: no mix named '{name}'
Update ✓ – exists ✅ update by id
Update ✓ – – error: no mix with id '{id}'
Update ✓ ✓ – error: provide id OR name, not both
Save – – – ✅ insert new record (name auto-derived)
Save – ✓ exists ✅ update by name
Save – ✓ – ✅ insert new record
Save ✓ – exists ✅ update by id
Save ✓ – – error: no mix with id '{id}'; provide a name to create
Save ✓ ✓ – error: provide id OR name, not both
"""
@classmethod
@@ -105,10 +150,12 @@ class SaveMixNode(io.ComfyNode):
io.String.Input("name", default="", multiline=False, optional=True),
io.Combo.Input("mode", options=_MODE, default="Save"),
io.String.Input("id", optional=True, default="", multiline=False),
io.Boolean.Input("favorite", optional=True, default=False),
io.Image.Input("example_image", optional=True),
],
outputs=[
io.String.Output(display_name="mix_id"),
io.String.Output(display_name="mix_name"),
],
)
@@ -126,6 +173,7 @@ class SaveMixNode(io.ComfyNode):
name: str = "",
mode: str = "Save",
id: str = "",
favorite: bool = False,
example_image=None,
) -> io.NodeOutput:
name = (name or "").strip()
@@ -138,18 +186,14 @@ class SaveMixNode(io.ComfyNode):
"— ids are assigned automatically."
)
# ── Guard: Create mode requires a name ───────────────────────────────
if mode == "Create" and not name:
raise ValueError("[SaveMixNode] A name is required to create a new mix.")
# ── Guard: Update / Save must not have both id and name ──────────────
if mode in ("Update", "Save") and mix_id_input and name:
raise ValueError(
"[SaveMixNode] Connect either an id or enter a name — not both."
)
# ── Guard: Update / Save require at least one identifier ─────────────
if mode in ("Update", "Save") and not mix_id_input and not name:
# ── Guard: Update requires at least one identifier ───────────────────
if mode == "Update" and not mix_id_input and not name:
raise ValueError(
"[SaveMixNode] Connect an id or enter a name to identify the mix."
)
@@ -179,23 +223,39 @@ class SaveMixNode(io.ComfyNode):
if mix_id_input:
existing = next((m for m in mixes if m.get("id") == mix_id_input), None)
else:
existing = next((m for m in mixes if m.get("name") == name), None)
existing = next((m for m in mixes if m.get("name") == name), None) if name else None
# ── Mode: Create ─────────────────────────────────────────────────────
if mode == "Create":
# Auto-derive a name if none was given
name_was_derived = not name
if name_was_derived:
name = _derive_name(mix_styles, data)
if existing is None and name:
existing = next((m for m in mixes if m.get("name") == name), None)
if existing is not None:
if name_was_derived:
# Quick Mix names are temporary — replace the existing record
_update_mix(existing, mix_styles, example_image, name, favorite)
save_data(data)
return io.NodeOutput(existing["id"], existing["name"])
raise RuntimeError(
f"[SaveMixNode] A mix named '{name}' already exists "
f"(id={existing['id']}). Switch to Update or Save mode to modify it."
)
new_mix_id = str(uuid.uuid4())
new_mix: dict = {"id": new_mix_id, "name": name, "styles": mix_styles}
new_mix: dict = {
"id": new_mix_id,
"name": name,
"favorite": favorite if favorite is not None else False,
"styles": mix_styles,
}
if example_image is not None:
new_mix["image_filename"] = _save_mix_image(example_image, name)
new_mix["image_updated_at"] = int(time.time())
mixes.append(new_mix)
save_data(data)
return io.NodeOutput(new_mix_id)
return io.NodeOutput(new_mix_id, name)
# ── Mode: Update ─────────────────────────────────────────────────────
if mode == "Update":
@@ -208,9 +268,9 @@ class SaveMixNode(io.ComfyNode):
raise RuntimeError(
f"[SaveMixNode] No mix named '{name}' was found."
)
_update_mix(existing, mix_styles, example_image, name)
_update_mix(existing, mix_styles, example_image, name, favorite)
save_data(data)
return io.NodeOutput(existing["id"])
return io.NodeOutput(existing["id"], existing["name"])
# ── Mode: Save ───────────────────────────────────────────────────────
if mix_id_input and existing is None:
@@ -220,16 +280,23 @@ class SaveMixNode(io.ComfyNode):
)
if existing is not None:
_update_mix(existing, mix_styles, example_image, name)
_update_mix(existing, mix_styles, example_image, name, favorite)
save_data(data)
return io.NodeOutput(existing["id"])
return io.NodeOutput(existing["id"], existing["name"])
else:
# Save + name + not found → create
# Save + (name or auto-derive) + not found → create
if not name:
name = _derive_name(mix_styles, data)
new_mix_id = str(uuid.uuid4())
new_mix = {"id": new_mix_id, "name": name, "styles": mix_styles}
new_mix = {
"id": new_mix_id,
"name": name,
"favorite": favorite if favorite is not None else False,
"styles": mix_styles,
}
if example_image is not None:
new_mix["image_filename"] = _save_mix_image(example_image, name)
new_mix["image_updated_at"] = int(time.time())
mixes.append(new_mix)
save_data(data)
return io.NodeOutput(new_mix_id)
return io.NodeOutput(new_mix_id, name)
+4 -4
View File
@@ -1,4 +1,4 @@
"""StyleBlendNode — aggregates weighted style_entry slots into a blend JSON + prompt."""
"""BlendStyleNode — aggregates weighted style_entry slots into a blend JSON + prompt."""
import json
import os
@@ -22,7 +22,7 @@ _WEIGHT_MODE = [
]
class StyleBlendNode(io.ComfyNode):
class BlendStyleNode(io.ComfyNode):
"""Aggregates style_entry inputs (from Weight Style nodes) into a blend.
Each slot accepts a style_entry JSON string produced by the Weight Style node.
@@ -50,8 +50,8 @@ class StyleBlendNode(io.ComfyNode):
max=16,
)
return io.Schema(
node_id="StyleBlendImmacStyleMixer",
display_name="Style Blend",
node_id="BlendStyleImmacStyleMixer",
display_name="Blend Style",
category="Immac/Style Mixer",
description=(
"Aggregates weighted style_entry inputs (from Weight Style nodes) "
+4 -4
View File
@@ -1,4 +1,4 @@
"""StylePickNode — picks a saved style and outputs its name, id, and value."""
"""PickStyleNode — picks a saved style and outputs its name, id, and value."""
import os
@@ -13,14 +13,14 @@ def _style_names() -> list[str]:
return names if names else ["(no styles saved)"]
class StylePickNode(io.ComfyNode):
class PickStyleNode(io.ComfyNode):
"""Picks a saved style and outputs its name, id, and prompt value."""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="StylePickImmacStyleMixer",
display_name="Style Pick",
node_id="PickStyleImmacStyleMixer",
display_name="Pick Style",
category="Immac/Style Mixer",
description="Picks a saved style and outputs its name, id, and prompt value.",
inputs=[
+1 -1
View File
@@ -76,7 +76,7 @@ export default function StyleCard({ style, onUpdate, onDelete, inCurrentMix, onA
if (document.elementFromPoint(e.clientX, e.clientY) !== canvasEl) return
const pos: [number, number] = lgCanvas.convertEventToCanvasOffset(e)
const LG = (window as any).LiteGraph
const node = LG?.createNode('StylePickImmacStyleMixer')
const node = LG?.createNode('PickStyleImmacStyleMixer')
if (!node) return
node.pos = pos
;(window as any).app?.graph?.add(node)
+5 -5
View File
@@ -168,11 +168,11 @@ async function init(): Promise<void> {
nodeCreated(node: any) {
if (
node.comfyClass !== 'PickMixImmacStyleMixer' &&
node.comfyClass !== 'StylePickImmacStyleMixer'
node.comfyClass !== 'PickStyleImmacStyleMixer'
) return
// ── shared DOM preview widget ────────────────────────────────────────
const isPickNode = node.comfyClass === 'StylePickImmacStyleMixer'
// ── shared DOM preview widget ────────────────────────────────────────────────────────────────────
const isPickNode = node.comfyClass === 'PickStyleImmacStyleMixer'
const widgetName = isPickNode ? 'style' : 'mix'
const comboWidget = node.widgets?.find((w: any) => w.name === widgetName)
@@ -289,7 +289,7 @@ async function init(): Promise<void> {
node._immacPreviewScheduled = false
const isPickNode = node.comfyClass === 'StylePickImmacStyleMixer'
const isPickNode = node.comfyClass === 'PickStyleImmacStyleMixer'
const widgetName = isPickNode ? 'style' : 'mix'
const comboWidget = node.widgets?.find((w: any) => w.name === widgetName)
const currentVal = String(comboWidget?.value ?? '')
@@ -321,7 +321,7 @@ async function init(): Promise<void> {
// We listen on `execution_success` (fires once per completed queue item)
// via the directly-imported `api` singleton. Any run could have updated an
// image_filename via SaveMix / CreateStyle / ModifyStyle, so we refresh all
// PickMix and StylePick node previews. The image_updated_at cache-buster
// PickMix and Pick Style node previews. The image_updated_at cache-buster
// ensures the browser fetches the new file only when it actually changed.
function refreshAllPreviews() {
const nodes: any[] = (app as any).graph?._nodes ?? []