Files
AISeDam e3779864e7 v0.5.60: RPKSampler use_base=False fixes, LowVram patch leak fix, comment cleanup
Bug Fixes:
- Fix RPKSampler use_base=False: null BASE anchor to prevent COL bleeding
- Fix RPKSampler use_base=False: correct BASE skip, lora_offset, exclusive area
- Fix RPKSampler use_base=True: BASE text encoding unaffected by null BASE logic
- Fix all samplers/detailers: patches={} instead of patches.clear()
- Fix all samplers/detailers: unpatch_model() before sampling
- Fix RPRegionalDetailerZImage: pre-build LoRA models before inpaint loop
- Fix RPRegionalDetailerZImage: fresh model.clone() base per run

Changed:
- core/lora_manager.py: lora_offset parameter added
- core/prompt_parser.py: subprompts_raw col-only
- RPKSampler: common pre-merged into DIV texts once
2026-04-30 14:43:18 +09:00

203 lines
7.2 KiB
Python

"""
Regional Prompter - Prompt Parser (2D support)
1D (ADDCOL only):
[common] ADDCOMM [base] ADDBASE [col0] ADDCOL [col1] ADDCOL [col2]
→ subprompts = [base, col0, col1, col2]
2D (ADDROW + ADDCOL combined):
Columns mode:
ADDROW = row separator (top→bottom)
ADDCOL = col separator (left→right)
structure: row0_col0 ADDCOL row0_col1 ADDROW row1_col0 ADDCOL row1_col1
→ subprompts order: left→right, top→bottom (row0_col0, row0_col1, row1_col0, row1_col1)
→ is_2d=True, rows_structure: [[n_cols per row]] returned
common is not generated as standalone chunk (SD-WebUI original behavior).
col_lora_map key = 1:1 with subprompts array index.
"""
import re
from typing import List, Dict, Tuple, Optional
KEYCOMM = "ADDCOMM"
KEYBASE = "ADDBASE"
KEYCOL = "ADDCOL"
KEYROW = "ADDROW"
_RE_LORA = re.compile(r"<lora:[^>]+>", re.IGNORECASE)
def _strip_lora(text: str) -> str:
return _RE_LORA.sub("", text).strip()
def _parse_lora_tags(text: str) -> Dict[str, float]:
result = {}
for m in re.finditer(r"<lora:([^:> ]+)(?::([0-9.]+))?>", text, re.IGNORECASE):
name = m.group(1).strip()
weight = float(m.group(2)) if m.group(2) else 1.0
if name:
result[name] = weight
return result
def _clean(text: str) -> str:
text = re.sub(r",\s*,", ",", text)
text = re.sub(r"\s{2,}", " ", text)
return text.strip().strip(",").strip()
def _join(a: str, b: str) -> str:
a = a.strip(); b = b.strip()
if a and b:
return f"{a}, {b}"
return a or b
def is_2d_prompt(prompt: str) -> bool:
"""Returns True if both ADDCOL and ADDROW are present (2D mode)."""
return KEYCOL in prompt and KEYROW in prompt
def parse_prompt(
prompt: str,
usebase: bool = False,
usecom: bool = True,
) -> Tuple[List[str], List[str], Dict[int, Dict[str, float]], str, str, List[str]]:
"""
Parse ADDCOMM/ADDBASE/ADDCOL/ADDROW syntax.
Both ADDCOL + ADDROW present → 2D mode.
Returns
-------
subprompts_raw : raw chunks incl. LoRA (left→right, top→bottom)
subprompts_nolora : chunks with LoRA removed
col_lora_map : {chunk_index: {lora_name: weight}}
and_prompt : AND-joined string
common_text : common text (for Prompt-EX)
col_texts : col-only parts (for Prompt-EX)
"""
# ── 1. Split ADDCOMM ──────────────────────────────
if KEYCOMM in prompt:
before_comm, after_comm = prompt.split(KEYCOMM, 1)
_common = _clean(before_comm)
else:
_common = ""
after_comm = prompt
# ── 2. Split ADDBASE ──────────────────────────────
if KEYBASE in after_comm:
before_base, after_cols = after_comm.split(KEYBASE, 1)
_base_part = _clean(before_base)
elif usebase:
# No ADDBASE but use_base=True → first segment before ADDCOL/ADDROW as base
for kw in (KEYCOL, KEYROW):
if kw in after_comm:
before_base, after_cols = after_comm.split(kw, 1)
_base_part = _clean(before_base)
after_cols = kw + after_cols # restore separator
break
else:
_base_part = _clean(after_comm)
after_cols = ""
else:
_base_part = ""
after_cols = after_comm
# ── 3. Branch: 2D vs 1D ──────────────────────────
_two_d = is_2d_prompt(after_cols) or (KEYCOL in after_cols and KEYROW in after_cols)
if _two_d:
# ── 2D: split rows by ADDROW, split cols by ADDCOL per row ──
# Columns mode: ADDROW → row, ADDCOL → col
row_parts = after_cols.split(KEYROW)
_col_parts = []
_row_structure = [] # [[row0_col_count, ...], [row1_col_count, ...]]
for row_text in row_parts:
cols_in_row = [_clean(c) for c in row_text.split(KEYCOL)]
cols_in_row = [c for c in cols_in_row if c]
if cols_in_row:
_col_parts.extend(cols_in_row)
_row_structure.append(len(cols_in_row))
elif _row_structure:
_row_structure[-1] = _row_structure[-1] # ignore empty row
else:
# ── 1D: treat ADDCOL/ADDROW identically ────────
for kw in (KEYCOL, KEYROW):
after_cols = after_cols.replace(kw, "\x00")
_col_parts = [_clean(c) for c in after_cols.split("\x00") if _clean(c)]
_row_structure = [len(_col_parts)] if _col_parts else []
# ── 4. Build chunks ──────────────────────────────────
# common is merged in KSampler pre-merge; col-only text here
subprompts_raw: List[str] = []
if usebase and _base_part:
subprompts_raw.append(_base_part)
for col in _col_parts:
if col:
subprompts_raw.append(col)
if not subprompts_raw:
subprompts_raw = [_clean(prompt)]
# ── 5. LoRA-removed version ────────────────────────
subprompts_nolora = [_strip_lora(p) for p in subprompts_raw]
# ── 6. col_lora_map ──────────────────────────
col_lora_map: Dict[int, Dict[str, float]] = {}
for i, raw in enumerate(subprompts_raw):
loras = _parse_lora_tags(raw)
if loras:
col_lora_map[i] = loras
and_prompt = " AND ".join(subprompts_nolora)
# For Prompt-EX
# common_text: global/scene text before ADDCOMM (controlled by use_common)
common_text = _strip_lora(_clean(prompt.split(KEYCOMM)[0])) if KEYCOMM in prompt else ""
# col_texts:
# usebase=True → [0]=BASE col_only(_base_part), [1..]=DIV col_only(_col_parts)
# usebase=False → [0..]=DIV col_only(_col_parts)
# _base_part = between ADDCOMM and ADDBASE (controlled by use_base)
# _col_parts[0] = between ADDBASE and first ADDCOL = DIV[0,0]
# _col_parts[1] = after first ADDCOL = DIV[0,1]
col_texts_raw = []
if usebase and _base_part:
col_texts_raw.append(_base_part) # BASE prompt (quality tags etc.)
col_texts_raw.extend(_col_parts) # DIV[0,0], DIV[0,1], ...
col_texts = [_strip_lora(t) for t in col_texts_raw]
return subprompts_raw, subprompts_nolora, col_lora_map, and_prompt, common_text, col_texts
def get_2d_structure(prompt: str, usebase: bool = False) -> Optional[List[int]]:
"""
Return col count per row for 2D prompt.
Returns None for 1D.
Example: "A ADDCOL B ADDROW C ADDCOL D" → [2, 2]
"""
if KEYCOMM in prompt:
_, prompt = prompt.split(KEYCOMM, 1)
if KEYBASE in prompt:
_, prompt = prompt.split(KEYBASE, 1)
elif usebase:
for kw in (KEYCOL, KEYROW):
if kw in prompt:
_, rest = prompt.split(kw, 1)
prompt = kw + rest
break
if not (KEYCOL in prompt and KEYROW in prompt):
return None
structure = []
for row_text in prompt.split(KEYROW):
cols = [c for c in row_text.split(KEYCOL) if _clean(c)]
if cols:
structure.append(len(cols))
return structure if structure else None