From f1a4e2fc627cdbfe72eb1dffa40a6360b14dc7c2 Mon Sep 17 00:00:00 2001 From: Antonio Cordero Balcazar Date: Sun, 3 May 2026 15:05:43 +0200 Subject: [PATCH] * Better logging. * Refactored tests. --- docs/CONFIG.md | 6 ++++-- ppp.py | 19 +++++++++++-------- ppp_comfyui.py | 2 ++ ppp_tree.py | 19 ++++++++++++------- scripts/ppp_script.py | 16 +++++++++++++--- tests/base_tests.py | 36 +++++++++++++++++++++++++++++++----- tests/tests_wildcards.py | 1 + 7 files changed, 74 insertions(+), 25 deletions(-) diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 3eafc95..f91354d 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -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 diff --git a/ppp.py b/ppp.py index 73a166d..656b92e 100644 --- a/ppp.py +++ b/ppp.py @@ -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.""" diff --git a/ppp_comfyui.py b/ppp_comfyui.py index 62d5493..e8fe831 100644 --- a/ppp_comfyui.py +++ b/ppp_comfyui.py @@ -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") diff --git a/ppp_tree.py b/ppp_tree.py index 1f17489..48a794d 100644 --- a/ppp_tree.py +++ b/ppp_tree.py @@ -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: diff --git a/scripts/ppp_script.py b/scripts/ppp_script.py index ae0dffe..7c15ee7 100644 --- a/scripts/ppp_script.py +++ b/scripts/ppp_script.py @@ -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(): diff --git a/tests/base_tests.py b/tests/base_tests.py index 2b40e6d..93cebb8 100644 --- a/tests/base_tests.py +++ b/tests/base_tests.py @@ -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), diff --git a/tests/tests_wildcards.py b/tests/tests_wildcards.py index 6d8f3a0..0048a41 100644 --- a/tests/tests_wildcards.py +++ b/tests/tests_wildcards.py @@ -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