* Better logging.

* Refactored tests.
This commit is contained in:
Antonio Cordero Balcazar
2026-05-03 17:06:57 +02:00
parent 73996b1609
commit f1a4e2fc62
7 changed files with 74 additions and 25 deletions
+4 -2
View File
@@ -10,9 +10,11 @@ This file contains some options for how the host applications (WebUIs) should ac
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.
## Notes
## Important
Beware of the combinatorial mode with no limits. Very few choice/wildcard constructs can cause a combinatorial explosion!
**Beware of the combinatorial mode with no limits**. Even very few choice/wildcard constructs can cause a *combinatorial explosion*!
The console log can help you determine the number of combinations that it is trying to generate. There will be an **"Estimated combinations"** message that shows an estimate. You can try first with a limit of 1, then check this message in the log. But note that it is a lower bound estimate, and there could be more combinations.
## ComfyUI
+11 -8
View File
@@ -869,16 +869,12 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
if rem_wildcards:
w_found_p = [wc for wc, n in rem_wildcards if not n]
w_found_n = [wc for wc, n in rem_wildcards if n]
if self.state.options.if_wildcards == IFWILDCARDS_CHOICES.stop:
self.log(logging.ERROR, "Found unprocessed wildcards!")
else:
self.log(logging.INFO, "Found unprocessed wildcards.")
ppwl = ", ".join(w_found_p)
npwl = ", ".join(w_found_n)
if ppwl:
self.log(logging.ERROR, f"In the prompt: {ppwl}")
self.log(logging.WARN, f"Unprocessed wildcards in the prompt: {ppwl}")
if npwl:
self.log(logging.ERROR, f"In the negative prompt: {npwl}")
self.log(logging.WARN, f"Unprocessed wildcards in the negative prompt: {npwl}")
if self.state.options.if_wildcards == IFWILDCARDS_CHOICES.warn:
prompt = self.WILDCARD_WARNING + prompt
elif self.state.options.if_wildcards == IFWILDCARDS_CHOICES.stop:
@@ -929,6 +925,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
# Parse both prompts
processor = TreeProcessor(self.state, rng)
# We use the ASCII Group Separator character between prompt and negative prompt since it's unlikely to appear in prompts
unified_prompt = prompt + "\x1d" + negative_prompt
prompt_parser, parser_description = self.__get_best_parser(unified_prompt)
self.log(logging.DEBUG, f"Using {parser_description} for prompt")
@@ -967,6 +964,11 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
self.log(logging.INFO, "Combinations shuffled")
return final_results
def process_prompts_group_start(self):
"""Start of a prompt processing group."""
self.log(logging.INFO, f"System variables: {self.state.variables.get_all_system()}")
self.log(logging.INFO, f"Combinatorial: {self.state.options.do_combinatorial}")
def process_prompt(
self,
original_prompt: str,
@@ -989,11 +991,9 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
seed = np.random.randint(0, 2**32, dtype=np.int64)
prompt = original_prompt
negative_prompt = original_negative_prompt
self.log(logging.INFO, f"System variables: {self.state.variables.get_all_system()}")
self.log(logging.INFO, f"Input seed: {seed}")
self.log(logging.INFO, f"Input prompt: {prompt}")
self.log(logging.INFO, f"Input negative_prompt: {negative_prompt}")
self.log(logging.INFO, f"Combinatorial: {self.state.options.do_combinatorial}")
t1 = time.monotonic_ns()
if self.state.cyclical_state.last_prompt_pair != (original_prompt, original_negative_prompt):
self.state.cyclical_state.reset()
@@ -1015,3 +1015,6 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
except Exception: # pylint: disable=broad-exception-caught
self.log(logging.ERROR, "Unexpected error", exc_info=True)
return [(original_prompt, original_negative_prompt, {})]
def process_prompts_group_end(self):
"""End of a prompt processing group."""
+2
View File
@@ -412,7 +412,9 @@ class PromptPostProcessorComfyUINode:
self.wildcards_obj,
self.extranetwork_mappings_obj,
)
self.ppp.process_prompts_group_start()
results = self.ppp.process_prompt(pos_prompt, neg_prompt, seed if seed is not None else 1)
self.ppp.process_prompts_group_end()
# with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "logs", "last_prompts_comfyui.txt"), "w", encoding="utf-8") as f:
# f.write(f"Seed: {seed if seed is not None else 1}\n")
+12 -7
View File
@@ -1,4 +1,5 @@
from collections import namedtuple
from functools import reduce
from itertools import combinations, combinations_with_replacement, permutations, product
import logging
import math
@@ -129,6 +130,10 @@ class TreeProcessor(lark.visitors.Interpreter):
results.append(
(self.__result, self.__detectedWildcards.copy(), self.state.variables.backup_user_and_echoed())
)
if len(results) == 1:
first_run_estimate = reduce(lambda x, y: x * y, self.__comb_trace, 1)
self.log(logging.INFO, f"Estimated combinations (lower bound): {first_run_estimate}")
self.log(logging.INFO, f"Added combination {len(results)}")
return tuple(self.__comb_trace)
limit_reached = False
@@ -1425,29 +1430,29 @@ class TreeProcessor(lark.visitors.Interpreter):
found_mappings.append(v)
else:
else_mapping = v
if found_mappings:
num_mappings = len(found_mappings)
if num_mappings > 0:
if self.state.options.do_combinatorial:
N = len(found_mappings)
decision_idx = len(self.__comb_trace)
self.__comb_trace.append(N)
self.__comb_trace.append(num_mappings)
chosen_idx = (
min(self.__comb_forced_path[decision_idx], N - 1)
min(self.__comb_forced_path[decision_idx], num_mappings - 1)
if decision_idx < len(self.__comb_forced_path)
else 0
)
found = found_mappings[chosen_idx]
elif len(found_mappings) == 1:
elif num_mappings == 1:
found = found_mappings[0]
else:
found = found_mappings[
self.__rng.choice(
len(found_mappings),
num_mappings,
p=[v.weight or 1 for v in found_mappings],
)
]
else:
found = else_mapping
if len(found_mappings) < 2:
if num_mappings < 2:
self.state.extranetwork_mappings_obj.cached_mappings[extnet_id] = found
if found:
if found.name:
+13 -3
View File
@@ -159,7 +159,15 @@ class PromptPostProcessorA1111Script(scripts.Script):
min_width=120,
elem_id="ppp_combinatorial_limit",
)
return [force_equal_seeds, unlink_seed, seed, incremental_seed, combinatorial, combinatorial_shuffle, combinatorial_limit]
return [
force_equal_seeds,
unlink_seed,
seed,
incremental_seed,
combinatorial,
combinatorial_shuffle,
combinatorial_limit,
]
def process(
self,
@@ -386,6 +394,7 @@ class PromptPostProcessorA1111Script(scripts.Script):
if hiresfix_exists:
prompts_list[(hiresfix_type, i)] = None
ppp.process_prompts_group_start()
if input_combinatorial:
seed_for_comb = calculated_seeds[0] if calculated_seeds else 0
regular_copy = (rpr.copy() if rpr else None, rnr.copy() if rnr else None)
@@ -399,7 +408,7 @@ class PromptPostProcessorA1111Script(scripts.Script):
for i in range(len(rpr)): # pylint: disable=consider-using-enumerate
posp, negp, _ = comb_results[i % num_comb]
prompts_list[(regular_type, i)] = (posp, negp)
extra_params["PPP combination"] = [1 + (i % num_comb) for i in range(len(rpr))]
extra_params["PPP combination"] = [str(1 + (i % num_comb)) for i in range(len(rpr))]
if hiresfix_exists:
hiresfix_equal = regular_exists and rph == rpr and rnh == rnr
if hiresfix_equal:
@@ -423,7 +432,7 @@ class PromptPostProcessorA1111Script(scripts.Script):
for i in range(len(rph)): # pylint: disable=consider-using-enumerate
posp, negp, _ = comb_results_hr[i % num_comb_hr]
prompts_list[(hiresfix_type, i)] = (posp, negp)
extra_params["PPP HR combination"] = [1 + (i % num_comb_hr) for i in range(len(rph))]
extra_params["PPP HR combination"] = [str(1 + (i % num_comb_hr)) for i in range(len(rph))]
else:
# processes prompts
for prompttype, typeindex in prompts_list.keys():
@@ -450,6 +459,7 @@ class PromptPostProcessorA1111Script(scripts.Script):
else:
log(self.ppp_logger, self.ppp_debug_level, logging.INFO, "result already in cache")
prompts_list[(prompttype, typeindex)] = cached
ppp.process_prompts_group_end()
# with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "logs", f"last_prompts_{app.value}.txt"), "w", encoding="utf-8") as f:
# for (prompttype, typeindex), (posp, negp) in prompts_list.items():
+31 -5
View File
@@ -48,13 +48,12 @@ class TestPromptPostProcessorBase(unittest.TestCase):
self.ppp_logger = self.lf.log
self.ppp_logger.setLevel(logging.DEBUG)
self.grammar_content = None
self.interrupted = False
self.defopts = PPPStateOptions(
debug_level=DEBUG_LEVEL.full,
on_warning=ONWARNING_CHOICES.stop,
strict_operators=True,
process_wildcards=True,
if_wildcards=IFWILDCARDS_CHOICES.ignore,
if_wildcards=IFWILDCARDS_CHOICES.stop,
choice_separator=", ",
keep_choices_order=False,
stn_separator=", ",
@@ -75,6 +74,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
cup_remove_extranetwork_tags=False,
do_combinatorial=False,
combinatorial_limit=0,
combinatorial_shuffle=False,
)
self.def_env_info = {
"app": "tests",
@@ -115,7 +115,10 @@ class TestPromptPostProcessorBase(unittest.TestCase):
self.interrupted = True
def init_obj(
self, ppp: Optional[str | PromptPostProcessor] = None, combinatorial: bool = False
self,
ppp: Optional[str | PromptPostProcessor] = None,
combinatorial: bool = False,
combinatorial_limit: int = 0,
) -> PromptPostProcessor:
if isinstance(ppp, str):
if ppp == "nocup":
@@ -138,6 +141,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
cup_extranetwork_tags=False,
cup_merge_attention=False,
do_combinatorial=combinatorial,
combinatorial_limit=combinatorial_limit,
),
self.grammar_content,
self.interrupt,
@@ -152,6 +156,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
self.defopts,
strict_operators=False,
do_combinatorial=combinatorial,
combinatorial_limit=combinatorial_limit,
),
self.grammar_content,
self.interrupt,
@@ -167,6 +172,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
replace(
self.defopts,
do_combinatorial=combinatorial,
combinatorial_limit=combinatorial_limit,
),
self.grammar_content,
self.interrupt,
@@ -183,6 +189,9 @@ class TestPromptPostProcessorBase(unittest.TestCase):
ppp: Optional[str | PromptPostProcessor] = None,
interrupted: bool = False,
combinatorial: bool = False,
combinatorial_limit: int = 0,
specific_wc_folders: Optional[list[str]] = None,
specific_em_folders: Optional[list[str]] = None,
):
"""
Process the prompt and compare the results with the expected prompts.
@@ -194,11 +203,24 @@ class TestPromptPostProcessorBase(unittest.TestCase):
ppp (Optional[str | PromptPostProcessor], optional): The PromptPostProcessor instance or type. Defaults to None.
interrupted (bool, optional): The interrupted flag. Defaults to False.
combinatorial (bool, optional): The combinatorial flag. Defaults to False.
combinatorial_limit (int, optional): The combinatorial limit. Defaults to 0.
specific_wc_folders (Optional[list[str]], optional): A list of specific wildcard folders to refresh. Defaults to None.
specific_em_folders (Optional[list[str]], optional): A list of specific extranetwork mapping folders to refresh. Defaults to None.
Returns:
None
"""
the_obj: PromptPostProcessor = self.init_obj(ppp, combinatorial)
if specific_wc_folders is not None:
self.wildcards_obj.refresh_wildcards(
DEBUG_LEVEL.full,
specific_wc_folders,
)
if specific_em_folders is not None:
self.extranetwork_maps_obj.refresh_extranetwork_mappings(
DEBUG_LEVEL.full,
specific_em_folders,
)
the_obj: PromptPostProcessor = self.init_obj(ppp, combinatorial, combinatorial_limit)
out = (
[OutputTuple("", "", None)]
if expected_output is None
@@ -207,11 +229,13 @@ class TestPromptPostProcessorBase(unittest.TestCase):
if the_obj.state.options.do_combinatorial:
# combinatorial
errors = []
the_obj.process_prompts_group_start()
result = the_obj.process_prompt(
input_prompts.prompt,
input_prompts.negative_prompt,
seed,
)
the_obj.process_prompts_group_end()
if self.interrupted != interrupted:
errors.append(f"Interrupted flag is incorrect: expected {interrupted}, got {self.interrupted}")
elif not self.interrupted and expected_output is not None:
@@ -245,6 +269,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
return
# non-combinatorial
errors = []
the_obj.process_prompts_group_start()
for eo in out:
result = the_obj.process_prompt(
input_prompts.prompt,
@@ -254,7 +279,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
if self.interrupted != interrupted:
errors.append(f"Interrupted flag is incorrect: expected {interrupted}, got {self.interrupted}")
elif not self.interrupted and expected_output is not None:
result_prompt, result_negative_prompt, output_variables = (result[0] if result else (None, None, None))
result_prompt, result_negative_prompt, output_variables = result[0] if result else (None, None, None)
if result_prompt != eo.prompt or result_negative_prompt != eo.negative_prompt:
errors.append(
f"Incorrect result '{eo.prompt}' / '{eo.negative_prompt}', got '{result_prompt}' / '{result_negative_prompt}'"
@@ -273,6 +298,7 @@ class TestPromptPostProcessorBase(unittest.TestCase):
f"Result '{eo.prompt}' / '{eo.negative_prompt}' found, but variables do not match: expected {expected_values}, got {unmatched_vars}"
)
seed += 1
the_obj.process_prompts_group_end()
self.assertFalse(
bool(errors),
"\n" + "\n".join(errors),
+1
View File
@@ -125,6 +125,7 @@ class TestWildcards(TestPromptPostProcessorBase):
InputTuple("the choices are: ___invalid__", ""),
OutputTuple("the choices are: ___invalid__", ""),
ppp="nocup",
interrupted=True,
)
def test_wc_wildcard1a_text(self): # simple text wildcard