# Prompt compiler — strip comments, split pos/neg, deduplicate, normalize. # Also handles wildcard resolution ({a|b|c}, __name__) and ", re.DOTALL | re.IGNORECASE) def _process_script_template(text: str): """Returns (template, remaining_text) or (None, text).""" if not text: return None, text match = SCRIPT_PATTERN.search(text) if not match: return None, text template = match.group(1) before = text[:match.start()].strip() after = text[match.end():].strip() prompt_parts = [p for p in [before, after] if p] return template, "\n".join(prompt_parts) if prompt_parts else "" # ============================================================================= # Full Compilation Pipeline # ============================================================================= def compile_prompt(text: str, mode: str = "combine", switch_index: int = 1, load_image_prompts: dict | None = None, wildcard_modes: dict | None = None): """Returns (positive, negative, metadata). load_image_prompts: optional dict mapping keywords like "__LoadImagePositive__" / "__LoadImageNegative__" to replacement text. wildcard_modes: optional dict mapping wildcard names to mode overrides, e.g. {"creatures": {"mode": "switch", "index": 2}}. metadata includes wildcard_results: dict mapping wildcard names to {"index": N, "label": "..."} for randomly picked options. """ if not text: return "", "", {} # Process the full text — label selection and wildcard resolution happen # BEFORE splitting on "Negative Prompt:" so per-option negatives stay # paired with their positive text. wc_results = {} result = _process_part(text, mode=mode, switch_index=switch_index, load_image_prompts=load_image_prompts, wildcard_modes=wildcard_modes, wildcard_results=wc_results) metadata = {} if wc_results: metadata["wildcard_results"] = wc_results if isinstance(result, tuple): resolved, roll_selected = result metadata["roll_selected"] = roll_selected else: resolved = result # Labels still present (combine mode preserves them for parent nodes # to switch on) — don't split; negatives are per-label. if re.search(r"^::([^:]+)::", resolved, re.MULTILINE): return resolved, "", metadata parts = re.split(r"negative\s+prompt\s*:", resolved, maxsplit=1, flags=re.IGNORECASE) positive = deduplicate(parts[0].strip()) negative = deduplicate(parts[1].strip()) if len(parts) > 1 else "" return positive, negative, metadata def _process_part(text: str, mode: str = "combine", switch_index: int = 1, load_image_prompts: dict | None = None, wildcard_modes: dict | None = None, wildcard_results: dict | None = None): if not text: return "" has_script = SCRIPT_PATTERN.search(text) is not None text = strip_comments(text) # Flat output keeps region bodies inline (braces removed) so non-regional # use is unchanged; the regional path calls compile_regions instead. text = strip_region_markers(text) # resolve __LoadImagePositive__ / __LoadImageNegative__ before wildcard # expansion so they don't get misidentified as wildcard file references if load_image_prompts and "__LoadImage" in text: for keyword, replacement in load_image_prompts.items(): if keyword in text: text = text.replace(keyword, replacement) text = expand_wildcard_files(text, wildcard_modes=wildcard_modes, wildcard_results=wildcard_results) result = resolve_wildcards(text, mode=mode, switch_index=switch_index) roll_idx = None if isinstance(result, tuple): text, roll_idx = result else: text = result has_labels = bool(re.search(r"^::([^:]+)::", text, re.MULTILINE)) if not has_script and not has_labels: text = re.sub(r"\s+", " ", text).strip() text = deduplicate(text) if roll_idx is not None: return (text, roll_idx) return text