Every node id, input name and existing widget position is unchanged, so saved
workflows load; new inputs are appended only.
Wildcards
- core/wildcards: Dynamic Prompts / Impact Pack grammar, tag queries, variables,
conditionals, concept / verbatim / loose bindings, reproducible seeds.
Wired into every enhancer node; reads ComfyUI/models/wildcards.
- New nodes: Wildcard Processor, Semantic Variation (direction axes),
Concept Survival Check. Starter lists in wildcards/.
- Random choices use a private generator; global random state is untouched.
Platform profiles
- core/profiles + profiles/*.yaml: one file per model (format, length,
negative prompt or none, quality tokens, text and dialogue syntax, vendor
rewriter prompt fetched once and cached). profiles_user/ overrides by id.
- New node: Platform Prompt Rewriter (prose, tags, edit instructions, JSON,
MiniMax H3 fields, Hailuo brackets), with validators for each format.
- Text-to-Image and Image-to-Image read profiles through platforms.py;
corrected facts for FLUX, Kontext, Chroma, Wan, AuraFlow, Kolors, NoobAI,
Qwen-Image, HunyuanImage. Negative prompt is empty for models without one.
- Hosted / API models: Seedream 5, Nano Banana, GPT Image, Midjourney V8,
FLUX.2 pro, Ideogram 3, Recraft V4, Seedance 2.0 / 2.5, Kling 3.0, Veo 3.1,
Gemini Omni Flash, Runway, Wan 3.0, Luma, hosted H3, Grok, Vidu, PixVerse.
Reports say how to switch the vendor's own prompt rewriting off.
- Illustrious: spaced tags, quality tags last, 25-80 words (OnomaAI paper).
Video nodes
- target_model and duration_seconds appended to the three video expanders.
Default target is MiniMax H3; wan22_video is the original path, unchanged.
A workflow saved earlier loads with the default until the input is set.
Also included: the Alice, Ideogram Magic Prompt and prompt-database nodes that
were already registered in __init__.py but had not been committed.
Fixes: v1 {animal} / {label:a|b} wildcards were inert in the video nodes;
Image-to-Video save_to_file raised NameError; T2I padded short-prompt models.
.gitattributes added (text=auto) so CRLF working files stop showing as
whole-file changes. prompt_database.db and the cached Ideogram prompt are
git-ignored.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VpCi6nw5UECMdZ7ayYJvN
150 lines
7.8 KiB
Python
150 lines
7.8 KiB
Python
"""
|
|
Semantic Variation Node
|
|
|
|
Makes controlled variations of a wildcard prompt. A plain re-roll replaces every random pick
|
|
with an unrelated one. This node starts from the picks `seed` would make and moves each one to
|
|
a SEMANTIC NEIGHBOUR inside its own list: a little for most slots, a lot for a few.
|
|
|
|
variation 0 the plain seeded prompt, nothing moved
|
|
variation 1, 2... different variations of that same prompt, each reproducible
|
|
strength how far every slot drifts: 0.1 = nearest 1% of its list, 0.5 = a quarter of the way out,
|
|
0.7 = halfway, 1.0 = the far end of the list
|
|
far_jumps how many slots jump (far_strength) instead of drifting
|
|
avoid_recent build several candidates and keep the one least like what you generated lately
|
|
|
|
Slots are the wildcard constructs in the text: __name__, __objects[tag AND tag]__, {a|b|c}.
|
|
Fixed words in the prompt are not touched. Feed `text` and `llm_instructions` to an enhancer
|
|
node or straight to a text encoder.
|
|
|
|
Author: Eric Hiss (GitHub: EricRollei)
|
|
License: Dual License (Non-Commercial and Commercial Use). See LICENSE.
|
|
"""
|
|
|
|
import traceback
|
|
from typing import Tuple
|
|
|
|
from .core.variability import VariationSettings
|
|
from .core.wildcards import split_paths
|
|
from .variability_support import (
|
|
EMBEDDER_CHOICES, available_embedders, direction_choices, get_embedder, get_variator,
|
|
)
|
|
from .wildcard_support import database_provider
|
|
|
|
|
|
class SemanticVariation:
|
|
"""Vary a wildcard prompt by moving each random pick to a semantic neighbour."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {
|
|
"multiline": True,
|
|
"default": "__person__, __gestures__, beside __objects/**__ in __spaces/**__. __light/setups__.",
|
|
"tooltip": "Prompt with wildcard syntax. Every __name__, tag query and {a|b|c} is a slot that can be varied.",
|
|
}),
|
|
"seed": ("INT", {
|
|
"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF,
|
|
"tooltip": "Seed for the BASE picks. Keep it fixed and change `variation` to explore around one prompt.",
|
|
}),
|
|
"variation": ("INT", {
|
|
"default": 1, "min": 0, "max": 0xFFFFFFFF,
|
|
"tooltip": "0 = the base prompt untouched. 1, 2, 3... = different variations of it, each reproducible.",
|
|
}),
|
|
"strength": ("FLOAT", {
|
|
"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05,
|
|
"tooltip": "How far every slot drifts within its list. 0.1 = nearest few, 0.5 = a quarter of the way out, 0.7 = halfway, 1.0 = far end.",
|
|
}),
|
|
"far_jumps": ("INT", {
|
|
"default": 0, "min": 0, "max": 16,
|
|
"tooltip": "How many slots jump with far_strength instead of drifting. Which ones is decided by `variation`.",
|
|
}),
|
|
"far_strength": ("FLOAT", {
|
|
"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05,
|
|
"tooltip": "Strength used for the slots that jump.",
|
|
}),
|
|
"embedder": (EMBEDDER_CHOICES, {
|
|
"default": "auto",
|
|
"tooltip": (
|
|
"What 'similar' means. auto = connected clip, else Qwen3-VL-Embedding, else open_clip.\n"
|
|
"qwen3vl: similar in meaning and look. openclip / graph_clip: similar in look.\n"
|
|
"sentence: similar in meaning. lexical: spelling only (no model)."
|
|
),
|
|
}),
|
|
"avoid_recent": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Make several candidate variations and keep the one least similar to recently generated prompts.",
|
|
}),
|
|
},
|
|
"optional": {
|
|
"clip": ("CLIP", {
|
|
"tooltip": "Optional. A CLIP with a pooled output (CLIP-L/G). Used as the embedder when `embedder` is auto or graph_clip.",
|
|
}),
|
|
"legacy_syntax": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Also accept the v1 forms {animal} and {label:a|b|c}.",
|
|
}),
|
|
"extra_paths": ("STRING", {
|
|
"multiline": True,
|
|
"default": "",
|
|
"tooltip": "Extra wildcard folders, one per line, highest priority. ComfyUI/models/wildcards is always searched.",
|
|
}),
|
|
# New inputs are only ever APPENDED below this line, so saved workflows keep their widget order.
|
|
"direction": (direction_choices(), {
|
|
"default": "none",
|
|
"tooltip": (
|
|
"Lean every move along a named axis, e.g. ordinary_to_uncanny or new_to_decayed.\n"
|
|
"Axes are example phrases in variability/directions.yaml; add your own in directions_user.yaml.\n"
|
|
"strength still decides how far a slot may go; this decides which way."
|
|
),
|
|
}),
|
|
"direction_amount": ("FLOAT", {
|
|
"default": 0.0, "min": -3.0, "max": 3.0, "step": 0.1,
|
|
"tooltip": (
|
|
"0 = no lean. Above 0 leans toward the axis's `to` side (uncanny, decayed, ...),\n"
|
|
"below 0 toward its `from` side. 1.0 is a clear lean, 2-3 lets the axis dominate.\n"
|
|
"The report shows each slot's lean so you can see whether an axis bites on a given list."
|
|
),
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("text", "llm_instructions", "base_text", "report")
|
|
FUNCTION = "vary"
|
|
CATEGORY = "Eric Prompt Enhancers"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, avoid_recent=False, **kwargs):
|
|
"""With avoid_recent on, the result depends on the memory, so the node must not be cached."""
|
|
if avoid_recent:
|
|
import time
|
|
return float(time.time())
|
|
return ""
|
|
|
|
def vary(self, text, seed, variation, strength, far_jumps, far_strength, embedder, avoid_recent,
|
|
clip=None, legacy_syntax=True, extra_paths="", direction="none", direction_amount=0.0) -> Tuple[str, str, str, str]:
|
|
try:
|
|
chosen_embedder, notes = get_embedder(embedder, clip)
|
|
variator = get_variator(chosen_embedder, split_paths(extra_paths))
|
|
settings = VariationSettings(
|
|
variation=int(variation), strength=float(strength), far_jumps=int(far_jumps),
|
|
far_strength=float(far_strength), avoid_recent=bool(avoid_recent),
|
|
direction=str(direction or "none"), direction_amount=float(direction_amount or 0.0),
|
|
)
|
|
result = variator.vary(
|
|
text, int(seed), settings,
|
|
legacy=bool(legacy_syntax), providers={"db": database_provider},
|
|
)
|
|
result.notes = notes + result.notes
|
|
report = result.report()
|
|
if result.resolution.warnings:
|
|
report += "\n" + "\n".join("warning: " + warning for warning in result.resolution.warnings)
|
|
if chosen_embedder.name.startswith("lexical"):
|
|
found = available_embedders(clip)
|
|
report += "\nembedders available here: " + ", ".join(f"{name} ({where})" for name, where in found.items())
|
|
return (result.resolution.text, result.resolution.format_for_llm(), result.base_text, report)
|
|
except Exception as error: # never break the graph over a prompt
|
|
traceback.print_exc()
|
|
return (text, "", text, f"Semantic Variation error, text passed through unchanged: {error}")
|