* Configured seed size per host. This allows the extension no use ComfyUIs 64bit seeds without cropping them.

* Allowed use of echoed-only variables (no value, only the last echo default) in comparisons.
This commit is contained in:
Antonio Cordero Balcazar
2026-05-22 16:02:31 +02:00
parent a35dfc2514
commit 996a0b7c39
6 changed files with 59 additions and 29 deletions
+1 -4
View File
@@ -6,7 +6,7 @@ The extension supports a configuration file `ppp_config.yaml` with some settings
By default this configuration is read from the file `ppp_config.yaml.defaults` in the extension folder. That file must not be modified. If you want to personalize any settings you should first copy it as `ppp_config.yaml` in the same folder, or in the *ComfyUI* user folder (preferred, but only possible in *ComfyUI*). The options in this new file will take precedence over those in the defaults file.
This file contains some options for how the host applications (WebUIs) should act in certain operations, and also define the supported models, including how to detect them and the model variants definitions. Host names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options.
This file contains some options for how the host applications (WebUIs) should act in certain operations, and also defines the supported models, including how to detect them and the model variants definitions. Host names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options.
The model variants now support regular expressions instead of a list of strings to detect the variant. If you used a non default value in previous versions you should create a configuration file and add them with the new format. As before, the default file defines variants for *Pony* and *Illustrious* models.
@@ -43,9 +43,6 @@ Inputs:
* **cup_options**: Connection to a Cleanup options node.
* **en_options**: Connection to a ExtraNetworkMapping options node.
> [!NOTE]
> The node uses 32-bit seeds, to be compatible with all the UIs. This means that 64-bit input values will be cut at 32-bits. If you connect an external seed that is also connected directly to the ksampler, the values may differ. This doesn't really matter in practice, but if you want the same value use the output `_input_seed` variable to send to the ksampler.
The options nodes are optional. If you don't need to change any of the default values then you don't need to use them.
The model and modelname are also optional, but if you don't set them you will not be capable of choosing content based on the model type or variant. Native model loader nodes do not output the filename, but there are custom nodes that do (like those from [ComfyUI Image Saver](https://github.com/alexopus/ComfyUI-Image-Saver)).
+21 -6
View File
@@ -382,7 +382,11 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
logging.WARNING,
f"Variant name '{escape_single_quotes(v)}' in model '{escape_single_quotes(m)}' conflicts with a known model name. Discarding variant.",
)
self.log(logging.DEBUG, f"Host configuration ({escape_single_quotes(app)}): {host_config}", min_level=DEBUG_LEVEL.minimal)
self.log(
logging.DEBUG,
f"Host configuration ({escape_single_quotes(app)}): {host_config}",
min_level=DEBUG_LEVEL.minimal,
)
return host_config
@@ -684,7 +688,6 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
vs.update_system(input_vars)
def init_wildcards_options(self):
"""Initializes the wildcard options."""
_tree = TreeProcessor(self.state, np.random.default_rng())
@@ -874,6 +877,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
result: tuple[str, list[tuple[str, bool]], dict[str, VariableEntry]],
) -> tuple[str, str, dict[str, str | None]]:
all_variables = self.state.variables.all_system
unechoed_variables: list[str] = []
unified_prompt, rem_wildcards, variables_snapshot = result
# Split the unified prompt back into prompt and negative prompt
@@ -892,13 +896,21 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
var_keys = sorted(variables_snapshot.keys())
for k in var_keys:
entry = variables_snapshot[k]
ev = entry.last_echoed_value if entry.last_echoed_value is not None else entry.value
if entry.last_echoed_evaluated_value is None and entry.value is not None:
unechoed_variables.append(k)
ev = entry.last_echoed_evaluated_value if entry.last_echoed_evaluated_value is not None else entry.value
if ev is not None:
if isinstance(ev, str) and self.state.options.cup_cleanup_variables:
ev = self.__cleanup(ev, 0)
all_variables[k] = ev
self.log(logging.INFO, f"Result variables: {all_variables}")
if unechoed_variables:
unechoed_values = {k: v for k, v in all_variables.items() if k in unechoed_variables}
self.log(
logging.INFO,
f"Variables that were never echoed: {unechoed_values}",
)
# Result checks
warnings = []
@@ -995,7 +1007,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
self.state.variables.clear_user()
# We update the input state
self.state.inputs.seed = int(seed & 0xFFFFFFFF)
self.state.inputs.seed = int(seed & ((1 << self.state.host_config.seed_bits) - 1))
self.state.inputs.pos_prompt = prompt
self.state.inputs.neg_prompt = negative_prompt
@@ -1008,7 +1020,10 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
elif isinstance(input_value, Enum):
self.state.variables.set_system(var_name, str(input_value).split(".", 1)[-1])
else:
self.log(logging.WARNING, f"Input '{input_name}' has unsupported type {type(input_value).__name__} for system variable and will be skipped.")
self.log(
logging.WARNING,
f"Input '{input_name}' has an unsupported type {type(input_value).__name__} for a system variable and will be skipped.",
)
filtered_sysvars_inputs = {k: v for k, v in self.state.variables.all_system.items() if k.startswith("_input_")}
self.log(logging.INFO, f"Inputs: {filtered_sysvars_inputs}")
@@ -1082,7 +1097,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
results: list[tuple[str, str, dict[str, Any]]]
try:
if seed == -1:
seed = np.random.randint(0, 2**32, dtype=np.int64)
seed = np.random.randint(0, 2**self.state.host_config.seed_bits, dtype=np.int64)
prompt = original_prompt
negative_prompt = original_negative_prompt
t1 = time.monotonic_ns()
+3
View File
@@ -66,6 +66,9 @@ class HostConfig(BaseModel):
alternation: AlternationOption = "ok"
and_: AndOption = Field("ok", alias="and")
break_: BreakOption = Field("ok", alias="break")
seed_bits: int = Field(
64, validator=lambda v: v if v in (8, 16, 32, 64) else ValueError("seed_bits must be one of 8, 16, 32, or 64")
)
# ------------------- Model detection -------------------
+7
View File
@@ -37,6 +37,7 @@ hosts:
# - comma: replace with comma
# - remove: remove the operator
# - error: raise an error
# seed_bits: number of bits to use for the seed; if not specified, it defaults to 64 bits
# Options in the user file override all these options by host (individual options are not inherited and if missing will use defaults in code).
a1111:
attention: ok
@@ -44,30 +45,35 @@ hosts:
alternation: ok
and: ok
break: ok
seed_bits: 32
forge:
attention: ok
scheduling: ok
alternation: ok
and: ok
break: ok
seed_bits: 32
forgeneo:
attention: ok
scheduling: ok
alternation: ok
and: ok
break: ok
seed_bits: 32
reforge:
attention: ok
scheduling: ok
alternation: ok
and: ok
break: ok
seed_bits: 32
sdnext:
attention: ok
scheduling: ok
alternation: ok
and: ok
break: ok
seed_bits: 32
comfyui:
# ComfyUI only supports parentheses-based attention
attention: parentheses
@@ -76,6 +82,7 @@ hosts:
alternation: error
and: comma
break: comma
seed_bits: 64
# Supported base models, variants, and options
# Check supported models for each host in:
+17 -13
View File
@@ -284,6 +284,7 @@ class TreeProcessor(lark.visitors.Interpreter):
evaluate=True,
add_to_content=False,
restore_state=True,
include_echoed=False,
) -> str | int | float | bool | list | None:
"""
Get the value of a variable.
@@ -294,7 +295,7 @@ class TreeProcessor(lark.visitors.Interpreter):
evaluate (bool): Whether to evaluate the variable.
add_to_content (bool): Whether to add the content after visiting.
restore_state (bool): Whether to restore the state after visiting.
include_echoed (bool): Whether to include echoed values.
Returns:
str|int|float|bool|list|None: The value of the variable, with strings coerced to their most specific type.
"""
@@ -317,6 +318,8 @@ class TreeProcessor(lark.visitors.Interpreter):
v = self.state.variables.get(name)
if v is None:
if include_echoed:
return self.state.variables.get_echoed_value(name)
return None
is_array = name[-2:] == "[]"
if is_array:
@@ -483,9 +486,9 @@ class TreeProcessor(lark.visitors.Interpreter):
return False
if c.lower() == "true":
return True
# Bare identifier - resolve as variable reference
# Bare identifier - resolve as variable reference (we consider echoed values if no value exists)
varname, varspecifier = self.__separate_arrayref(c)
val = self.__get_variable_value(varname, varspecifier, True, False)
val = self.__get_variable_value(varname, varspecifier, True, False, True, True)
if val is None:
val = ""
vartype = "system" if self.state.variables.name_is_system(c) else "user"
@@ -1457,22 +1460,23 @@ class TreeProcessor(lark.visitors.Interpreter):
default_value = None
is_array = variable_name[-2:] == "[]"
vname = f"{variable_name[0:-2]}[{variable_specifier}]" if variable_specifier is not None else variable_name
# value = self.__get_variable_value(variable_name, variable_specifier, True, True)
value = self.get_final_scalar_variable(variable_name, variable_specifier)
if value is None:
value = self.__get_variable_value(variable_name, variable_specifier, False, False, False)
evaluated_value = self.get_final_scalar_variable(variable_name, variable_specifier)
if evaluated_value is None:
if default is not None:
self.log(logging.DEBUG, f"Variable '{escape_single_quotes(vname)}' not found, using default value")
value = self.__visit(default, False, True)
self.__result += value
default_value = value
value = default
evaluated_value = self.__visit(default, False, True)
self.__result += evaluated_value
default_value = evaluated_value
else:
self.warn_or_stop(f"Unknown variable {escape_single_quotes(vname)}")
value = None
evaluated_value = ""
default_value = ""
value = ""
else:
self.__result += self.__value_to_str(value)
if not self.state.variables.name_is_system(variable_name):
self.state.variables.set_echoed_value(vname, value)
self.__result += self.__value_to_str(evaluated_value)
self.state.variables.set_echoed_value(vname, value, evaluated_value)
t2 = time.monotonic_ns()
info = variable_name
if is_array and variable_specifier is not None:
+10 -6
View File
@@ -10,7 +10,8 @@ class VariableEntry:
"""Holds all state for a single user variable."""
value: Any = field(default=None) # raw unevaluated value or evaluated on set
last_echoed_value: ScalarValue | None = field(default=None)
last_echoed_value: Any = field(default=None) # raw unevaluated value at last echo
last_echoed_evaluated_value: ScalarValue | None = field(default=None) # evaluated value at last echo
class VariableRepository:
@@ -106,21 +107,24 @@ class VariableRepository:
"""Return the set of all user-variable keys (those with any non-None field)."""
return set(self._vars)
def set_echoed_value(self, name: str, value: ScalarValue) -> None:
def set_echoed_value(self, name: str, value: Any, evaluated_value: ScalarValue) -> None:
"""Record that *name* was echoed into the prompt with *value*."""
self._entry(name).last_echoed_value = value
if not self.name_is_system(name):
entry = self._entry(name)
entry.last_echoed_value = value
entry.last_echoed_evaluated_value = evaluated_value
def get_echoed_value(self, name: str, default: ScalarValue | None = None) -> ScalarValue | None:
"""Return the last echoed value for *name*, or *default* if it has not been echoed."""
entry = self._vars.get(name)
if entry is None:
return default
return entry.last_echoed_value if entry.last_echoed_value is not None else default
return entry.last_echoed_evaluated_value if entry.last_echoed_evaluated_value is not None else default
def backup_user(self) -> dict[str, VariableEntry]:
"""Return a per-entry shallow-copy snapshot of all user variables for rollback."""
return {
name: VariableEntry(entry.value, entry.last_echoed_value)
name: VariableEntry(entry.value, entry.last_echoed_value, entry.last_echoed_evaluated_value)
for name, entry in self._vars.items()
}
@@ -129,7 +133,7 @@ class VariableRepository:
self._vars.clear()
self._vars.update(
{
name: VariableEntry(entry.value, entry.last_echoed_value)
name: VariableEntry(entry.value, entry.last_echoed_value, entry.last_echoed_evaluated_value)
for name, entry in backup.items()
}
)