# -*- coding: utf-8 -*- # Prompt Parser Analyzer — full edition (+ Grammar & Examples tab) # - Анализатор второго prompt_parser (из modules.prompt_parser) # - Препроцессор (синтаксический сахар): probabilistic alternates a{0.2}|b{0.8}, repeat N ( ... ), every K [ ... ] # - Линтер + автофиксы # - Конструкторы (group/alternate/numbered/sequence/top-level) # - Экспорт расписаний/таймлайна (CSV/JSON/Markdown), Diff # - Импорт/экспорт проекта (JSON) # - AND Debugger # - Интеграция генерации (txt2img + A/B placeholder) # - Подсветка синтаксиса в UI # - Визуальный редактор интервалов (Interval Editor) + магниты 25/50/75%, ±1 шаг, экспорт/импорт интервалов # - Grammar & Examples: полная справка по скобкам/операторам + интерактивные тесты # # Требования: modules.prompt_parser = «второй файл» (schedule_parser, get_schedule, at_step_from_schedule, ...) # Установка: сохраните этот файл в папку scripts/ или extensions/<...>/scripts/, перезапустите/Reload scripts. import os import re import json import difflib import importlib from typing import Any, List, Tuple, Optional import gradio as gr from modules import script_callbacks, scripts, shared # Для генерации from modules import processing, sd_samplers # Импортируем модуль парсера (второй файл), чтобы при смене ENV можно было reload(...) from modules import prompt_parser as pp import sys sys.path.append("/content/A1111/extensions/stable-diffusion-webui-prompt-parser7/scripts") import blueprints import linting import quickproof # --------------------------------------------------------------------- # Утилиты: подсветка ошибок, советы # --------------------------------------------------------------------- def _highlight_at(text: str, line: int, column: int, context: int = 2) -> str: lines = text.splitlines() i = max(0, line - 1) start = max(0, i - context) end = min(len(lines), i + context + 1) out = [] for idx in range(start, end): prefix = ">> " if idx == i else " " out.append(f"{prefix}{idx+1:>4}: {lines[idx]}") if idx == i: caret = " " * (column + 7 + len(str(idx+1))) + "^" out.append(caret) return "\n".join(out) def _guess_suggestion(src: str) -> List[str]: tips = [] if src.count("(") != src.count(")"): tips.append("Скобки () несбалансированы — проверьте пары.") if src.count("[") != src.count("]"): tips.append("Скобки [] несбалансированы — проверьте пары.") if src.count("{") != src.count("}"): tips.append("Скобки {} несбалансированы — проверьте пары.") if "::" in src and not any(term in src for term in ["!", ";", "!!"]): tips.append("Похоже, вы используете sequence (::), но не поставили терминатор (! или ; / !! для top-level).") if re.search(r":[A-Za-z]", src): tips.append("После ':' обычно ожидается число для веса (например, :1.2) — проверьте места с двоеточием.") if re.search(r"\[[^]]+\]!\s*!", src): tips.append("alternate_distinct пишется как [a|b]! — одно '!' сразу после ']'.") return tips def format_lark_error(prompt: str, err: Exception) -> str: title = f"**Ошибка парсинга:** `{type(err).__name__}`: {getattr(err, 'message', str(err))}" try: line = getattr(err, 'line', None) column = getattr(err, 'column', None) pos = getattr(err, 'pos_in_stream', None) except Exception: line = column = pos = None block = "" if line is not None and column is not None: block = "```\n" + _highlight_at(prompt, line, column) + "\n```" elif pos is not None: prefix = prompt[:pos] l = prefix.count("\n") + 1 c = len(prefix.split("\n")[-1]) + 1 block = "```\n" + _highlight_at(prompt, l, c) + "\n```" tips = _guess_suggestion(prompt) tips_md = "\n".join([f"- {t}" for t in tips]) if tips else "- Проверьте синтаксис во вкладке «Справка и примеры»." return f"{title}\n\n{block}\n\n**Как исправить:**\n{tips_md}\n" # --------------------------------------------------------------------- # ENV и перегрузка парсера # --------------------------------------------------------------------- class EnvState: def __init__(self): self.allow_empty_alt = os.environ.get("ALLOW_EMPTY_ALTERNATE", "0") self.expand_alt_per_step = os.environ.get("EXPAND_ALTERNATE_PER_STEP", "1") self.group_combo_limit = os.environ.get("GROUP_COMBO_LIMIT", "100") def apply(self, allow_empty_alt: bool, expand_alt: bool, combo_limit: int) -> bool: changed = False new_allow = "1" if allow_empty_alt else "0" new_expand = "1" if expand_alt else "0" new_limit = str(int(combo_limit)) if new_allow != self.allow_empty_alt: os.environ["ALLOW_EMPTY_ALTERNATE"] = new_allow self.allow_empty_alt = new_allow changed = True if new_expand != self.expand_alt_per_step: os.environ["EXPAND_ALTERNATE_PER_STEP"] = new_expand self.expand_alt_per_step = new_expand changed = True if new_limit != self.group_combo_limit: os.environ["GROUP_COMBO_LIMIT"] = new_limit self.group_combo_limit = new_limit changed = True return changed ENV_STATE = EnvState() def maybe_reload_prompt_parser(allow_empty_alt: bool, expand_alt: bool, combo_limit: int): changed = ENV_STATE.apply(allow_empty_alt, expand_alt, combo_limit) if changed: global pp from modules import prompt_parser as _pp pp = importlib.reload(_pp) # --------------------------------------------------------------------- # Препроцессор (синтаксический сахар) -> валидный синтаксис парсера # --------------------------------------------------------------------- def _pp_repeat_macro(src: str) -> Tuple[str, List[str]]: """ repeat N (text) -> text, text, ... (N раз) """ changes = [] pattern = re.compile(r"\brepeat\s+(\d+)\s*\(([^()]*?)\)", flags=re.IGNORECASE | re.DOTALL) while True: m = pattern.search(src) if not m: break n = max(1, int(m.group(1))) body = m.group(2).strip() repl = ", ".join([body] * n) src = src[:m.start()] + repl + src[m.end():] changes.append(f"repeat {n} (...) → {n} повторов") return src, changes def _pp_every_k_macro(src: str, total_steps: int) -> Tuple[str, List[str]]: """ every K [a|b|c] -> [ a : b : c ] : K, 2K, 3K ... (пока < steps) """ changes = [] rx = re.compile(r"\bevery\s+(\d+)\s*\[\s*([^\]]+?)\s*\]", flags=re.IGNORECASE) def repl(m): k = max(1, int(m.group(1))) opts_bar = m.group(2).strip() out = build_alternate_every_k(opts_bar, total_steps, k) changes.append(f"every {k} [...] → scheduled с границами кратными {k}") return out return rx.sub(repl, src), changes def _pp_prob_alt(src: str, total_steps: int) -> Tuple[str, List[str]]: """ probabilistic alternates: a{0.2}|b{0.8} → строим [ a : b ] : boundaries пропорционально весам """ changes = [] token_rx = re.compile(r"((?:[^|\n\[\]]+?\{\s*\d*\.?\d+\s*\}\s*\|\s*)+[^|\n\[\]]+?\{\s*\d*\.?\d+\s*\})") def convert_block(block: str) -> str: parts = [p.strip() for p in block.split("|")] items = [] total = 0.0 for p in parts: m = re.search(r"\{\s*(\d*\.?\d+)\s*\}\s*$", p) if not m: return block w = float(m.group(1)) text = re.sub(r"\{\s*\d*\.?\d+\s*\}\s*$", "", p).strip() items.append((text, w)) total += w if total <= 0: return block boundaries = [] accum = 0.0 for i in range(len(items) - 1): accum += items[i][1] / total b = int(round(accum * total_steps)) if 0 < b < total_steps: boundaries.append(str(b)) core = " : ".join([it[0] for it in items]) extra = f" : {', '.join(boundaries)}" if boundaries else "" return f"[ {core} ]{extra}" def repl(m): orig = m.group(1) out = convert_block(orig) if out != orig: changes.append("probabilistic a{p}|b{q} → scheduled с пропорциями весов") return out return token_rx.sub(repl, src), changes def preprocess_prompt(src: str, total_steps: int, enable: bool) -> Tuple[str, List[str]]: """ Применяем сахар по шагам. Возвращаем (новый_текст, список_изменений). """ if not enable: return src, [] changes_all: List[str] = [] src, ch = _pp_repeat_macro(src) changes_all += ch src, ch = _pp_every_k_macro(src, total_steps) changes_all += ch src, ch = _pp_prob_alt(src, total_steps) changes_all += ch return src, changes_all # --------------------------------------------------------------------- # Парс/расписания/превью # --------------------------------------------------------------------- def _safe_parse_tree(prompt: str): try: tree = pp.schedule_parser.parse(prompt) tree_str = tree.pretty() if hasattr(tree, "pretty") else str(tree) return tree_str, "" except Exception as e: return "", format_lark_error(prompt, e) def _make_schedule(prompt: str, steps: int, seed: Optional[int], use_visitor: bool) -> Tuple[List[List[Any]], str]: try: schedule = pp.get_schedule(prompt, steps, True, seed, use_visitor=use_visitor) if not schedule: return [], "**Пустое расписание.** Проверьте синтаксис." return schedule, "" except Exception as e: return [], f"**Ошибка при построении расписания:** {e}" def _schedule_table_md(schedule: List[List[Any]]) -> str: rows = ["| end_step | text |", "|---:|---|"] for end_step, text in schedule: safe_text = text.replace("|", "\\|") rows.append(f"| {end_step} | {safe_text} |") return "\n".join(rows) def _per_step_timeline_md(schedule: List[List[Any]], steps: int) -> str: rows = ["| step | active text |", "|---:|---|"] for s in range(1, steps + 1): txt = pp.at_step_from_schedule(s, schedule) safe_txt = txt.replace("|", "\\|") rows.append(f"| {s} | {safe_txt} |") return "\n".join(rows) def _compare_vis_vs_trans(prompt: str, steps: int, seed: Optional[int]) -> str: vis, e1 = _make_schedule(prompt, steps, seed, True) trn, e2 = _make_schedule(prompt, steps, seed, False) if e1 or e2: return (e1 or "") + (e2 or "") md = ["**Visitor (сбор в визиторе)**", _schedule_table_md(vis), "", "**Transformer (пересбор на end_step)**", _schedule_table_md(trn)] return "\n\n".join(md) # --------------------------------------------------------------------- # Линтер и автофиксы # --------------------------------------------------------------------- class Issue: def __init__(self, kind: str, msg: str, fix_hint: str = "", fixer=None): self.kind = kind # "Error"/"Warning"/"Hint" self.msg = msg self.fix_hint = fix_hint self.fixer = fixer # callable(src) -> src def lint_prompt(src: str, use_visitor: bool) -> List[Issue]: issues: List[Issue] = [] if src.count("(") != src.count(")"): issues.append(Issue("Error", "Несбалансированы круглые скобки ().", "Проверьте пары () или удалите лишние.", lambda s: auto_balance(s, "(", ")"))) if src.count("[") != src.count("]"): issues.append(Issue("Error", "Несбалансированы квадратные скобки [].", "Проверьте пары [] или удалите лишние.", lambda s: auto_balance(s, "[", "]"))) if src.count("{") != src.count("}"): issues.append(Issue("Warning", "Несбалансированы фигурные скобки {}.", "Если это 'dead group', лучше убрать фигурные скобки.", lambda s: auto_balance(s, "{", "}"))) if "::" in src and not any(t in src for t in ["!", ";", "!!"]): issues.append(Issue("Error", "Обнаружен sequence (::), но нет завершающего терминатора (! или ; / !!).", "Добавьте '!' или ';' в конец sequence.", ensure_sequence_terminator)) if re.search(r":[A-Za-z]", src): issues.append(Issue("Warning", "После ':' встречается буква — вероятно ожидалось число веса.", "Исправьте на ':1.2' или уберите ':'.", lambda s: s)) if re.search(r"(?:^|[^[])\b[^]\n]*\|[^[]", src) and "[" not in src: issues.append(Issue("Hint", "Используется '|' вне '[]' — это alternate1/2 (рандом), а не карусель по шагам.", "Заменить на [ a | b ] для перебора по шагам.", bars_to_square_alternate)) if "&" in src: issues.append(Issue("Hint", "Символ '&' активирует and_rule (логическая сцепка).", "Если это текст, замените на 'and' или запятую.", replace_amp_with_and)) if "{" in src and "|" not in src: issues.append(Issue("Hint", "Группа без '|' (dead group) — просто склейка, фигурные скобки не нужны.", "Уберите {} — оставьте плоский список.", remove_dead_groups)) if re.search(r"\[[^]]+\]!\s*!", src): issues.append(Issue("Warning", "Лишний '!' после alternate_distinct.", "Должно быть '[a|b]!' (одно '!').", lambda s: re.sub(r"\]!\s*!", "]!", s))) for m in re.finditer(r"(\d+)\s*!?\s*{([^}]+)}", src): n = int(m.group(1)) body = m.group(2) opts_list = [o.strip() for o in body.split("|") if o.strip()] if "!" in m.group(0) and len(opts_list) < n: issues.append(Issue("Warning", f"В numbered distinct запрошено {n}, но вариантов всего {len(opts_list)}.", "Уменьшите N или уберите '!'.", lambda s, _n=n: re.sub(rf"\b{_n}\s*!", str(_n), s, count=1))) if use_visitor and re.search(r"(^|[^[])\b[^]\n]+\|[^[]", src): issues.append(Issue("Hint", "Под use_visitor=True бар 'a|b' может остаться буквальным. " "Для случайного выбора используйте [a|b] или выключите use_visitor.", "Заменить на [a|b] или переключить режим.", bars_to_square_alternate)) return issues def auto_balance(src: str, l: str, r: str) -> str: c_l = src.count(l) c_r = src.count(r) if c_l > c_r: return src + (r * (c_l - c_r)) if c_r > c_l: return src return src def ensure_sequence_terminator(src: str) -> str: if "::" in src and not re.search(r"(!|;|!!)\s*$", src): return src.rstrip() + " !" return src def bars_to_square_alternate(src: str) -> str: if "[" in src and "]" in src: return src return "[ " + src.strip() + " ]" def replace_amp_with_and(src: str) -> str: return src.replace("&", "and") def remove_dead_groups(src: str) -> str: def repl(m): inner = m.group(1) if "|" in inner: return m.group(0) return inner return re.sub(r"\{\s*([^{}]+?)\s*\}", repl, src) def apply_safe_fixes(src: str, use_visitor: bool) -> Tuple[str, List[str]]: issues = lint_prompt(src, use_visitor) changes = [] out = src for issue in issues: if callable(issue.fixer): new_out = issue.fixer(out) if new_out != out: changes.append(f"{issue.kind}: {issue.msg} → {issue.fix_hint}") out = new_out return out, changes # --------------------------------------------------------------------- # Конструкторы/Конвертеры/Стратегии # --------------------------------------------------------------------- def build_group(items_csv: str) -> str: items = [s.strip() for s in items_csv.split(",") if s.strip()] return "{ " + ", ".join(items) + " }" if items else "{}" def build_alternate(items_bar: str, distinct: bool) -> str: core = items_bar.strip() if not core: return "[]" return f"[ {core} ]!" if distinct else f"[ {core} ]" def build_numbered(n: int, options_bar_or_group: str, distinct: bool) -> str: n = max(1, int(n)) body = options_bar_or_group.strip() mark = "!" if distinct else "" return f"{n}{mark} {body}" def build_sequence(owner: str, pairs_text: str, terminator: str) -> str: owner = owner.strip() pairs = [] for chunk in pairs_text.split(";"): chunk = chunk.strip() if not chunk: continue if ":" not in chunk: pairs.append(chunk) else: k, v = chunk.split(":", 1) pairs.append(f"{k.strip()} :: {v.strip()}") term = "!" if terminator == "!" else ";" return f"{owner} :: " + ", ".join(pairs) + f" {term}" def build_top_level_sequence(owner: str, pairs_text: str, trailing_plain: str) -> str: seq = build_sequence(owner, pairs_text, "!") inner = seq.replace(f"{owner} :: ", "").rstrip("!;").strip() tail = (", " + trailing_plain.strip()) if trailing_plain.strip() else "" return f"{owner.strip()} ::: {inner} !!{tail}" def build_scheduled_block(variants_multiline: str, ranges_text: str, reverse: bool) -> str: variants = [v.strip() for v in variants_multiline.split("\n") if v.strip()] if not variants: return "" core = " : ".join(variants) extra = f" : {ranges_text.strip()}" if ranges_text.strip() else "" suffix = " reverse" if reverse else "" return f"[ {core} ]{extra}{suffix}" def build_alternate_every_k(options_bar: str, steps: int, k: int) -> str: opts = [o.strip() for o in options_bar.split("|") if o.strip()] if not opts: return "" boundaries = [] for i in range(1, len(opts)): b = i * k if b < steps: boundaries.append(str(b)) extra = f" : {', '.join(boundaries)}" if boundaries else "" return f"[ {' : '.join(opts)} ]{extra}" def conv_dead_groups_to_plain(src: str) -> str: return remove_dead_groups(src) def conv_bars_to_square(src: str) -> str: return bars_to_square_alternate(src) def conv_add_seq_terminator(src: str) -> str: return ensure_sequence_terminator(src) # --------------------------------------------------------------------- # Экспорт и дифф # --------------------------------------------------------------------- def schedule_to_csv(schedule: List[List[Any]]) -> str: lines = ["end_step,text"] for end_step, text in schedule: safe = text.replace('"', '""') lines.append(f'{end_step},"{safe}"') return "\n".join(lines) def timeline_to_csv(schedule: List[List[Any]], steps: int) -> str: lines = ["step,text"] for s in range(1, steps + 1): t = pp.at_step_from_schedule(s, schedule) safe = t.replace('"', '""') lines.append(f'{s},"{safe}"') return "\n".join(lines) def schedule_to_json(schedule: List[List[Any]]) -> str: data = [{"end_step": end, "text": txt} for end, txt in schedule] return json.dumps(data, ensure_ascii=False, indent=2) def md_block(title: str, content: str) -> str: return f"### {title}\n\n```\n{content}\n```" def mk_diff(a: str, b: str, from_name="original", to_name="fixed") -> str: diff = difflib.unified_diff( a.splitlines(keepends=False), b.splitlines(keepends=False), fromfile=from_name, tofile=to_name, lineterm="" ) text = "\n".join(diff) return "```\n" + (text if text else "# (no changes)\n") + "\n```" # --------------------------------------------------------------------- # Примитивная “подсветка синтаксиса” (HTML) # --------------------------------------------------------------------- def highlight_prompt_html(src: str) -> str: esc = (src.replace("&", "&").replace("<", "<").replace(">", ">")) esc = re.sub(r"(\[\]|\[|\])", r'\1', esc) esc = re.sub(r"(\{\}|\{|\})", r'\1', esc) esc = re.sub(r"(\(|\))", r'\1', esc) esc = re.sub(r"(::|:::|!!|!|;|\|)", r'\1', esc) esc = re.sub(r"\bAND\b(?!_)", r'AND', esc) return f'
{esc}
' # --------------------------------------------------------------------- # Генерация (интеграция с A1111) # --------------------------------------------------------------------- def _list_samplers() -> List[str]: try: return [x.name for x in sd_samplers.all_samplers] except Exception: return ["Euler a", "Euler", "DPM++ 2M Karras"] def _generate_txt2img(prompt: str, negative: str, steps: int, width: int, height: int, cfg_scale: float, seed: int, sampler_name: str, batch_count: int, batch_size: int, restore_faces: bool, tiling: bool) -> Tuple[List[Any], str]: try: p = processing.StableDiffusionProcessingTxt2Img( sd_model=shared.sd_model, prompt=prompt, negative_prompt=negative, steps=int(steps), width=int(width), height=int(height), cfg_scale=float(cfg_scale), seed=int(seed), sampler_name=sampler_name, n_iter=int(batch_count), batch_size=int(batch_size), restore_faces=restore_faces, tiling=tiling, do_not_save_grid=True, do_not_save_samples=True, ) processed = processing.process_images(p) return processed.images, processed.info except Exception as e: return [], f"Ошибка генерации: {e}" def _generate_variants_with_placeholder(prompt: str, negative: str, placeholder: str, options_multiline: str, steps: int, width: int, height: int, cfg_scale: float, seed: int, sampler_name: str, restore_faces: bool, tiling: bool) -> Tuple[List[Any], str]: opts_list = [o.strip() for o in options_multiline.split("\n") if o.strip()] if not opts_list: return [], "Нет вариантов (пустой список)." images_all = [] infos = [] try: for i, opt_val in enumerate(opts_list, 1): pr = prompt.replace(placeholder, opt_val) p = processing.StableDiffusionProcessingTxt2Img( sd_model=shared.sd_model, prompt=pr, negative_prompt=negative, steps=int(steps), width=int(width), height=int(height), cfg_scale=float(cfg_scale), seed=int(seed + i - 1), sampler_name=sampler_name, n_iter=1, batch_size=1, restore_faces=restore_faces, tiling=tiling, do_not_save_grid=True, do_not_save_samples=True, ) processed = processing.process_images(p) images_all.extend(processed.images or []) infos.append(f"[{i}] {opt_val} — seed={int(seed + i - 1)}") return images_all, "\n".join(infos) except Exception as e: return [], f"Ошибка генерации вариантов: {e}" # --------------------------------------------------------------------- # AND-отладчик # --------------------------------------------------------------------- _AND_SPLIT_RX = re.compile(r"\bAND\b(?!_PERP|_SALT|_TOPK)") def and_debug_parse(src: str) -> List[Tuple[str, Optional[float]]]: parts = _AND_SPLIT_RX.split(src) out = [] for part in parts: part = part.strip() if not part: continue m = re.search(r"^(.*?)(?:\s*:\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?))?\s*$", part) if m: text = m.group(1).strip() w = float(m.group(2)) if m.group(2) is not None else None out.append((text, w)) else: out.append((part, None)) return out def and_debug_build(rows_text: str) -> str: lines = [ln.strip() for ln in rows_text.split("\n") if ln.strip()] parts = [] for ln in lines: m = re.match(r"^(.*?)(?:\s*:\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?))?$", ln) if m: t = m.group(1).strip() w = m.group(2) parts.append(f"{t} :{w}" if w else t) return " AND ".join(parts) # --------------------------------------------------------------------- # VISUAL INTERVAL EDITOR: utils (+ магниты, сдвиги, экспорт/импорт) # --------------------------------------------------------------------- def _ve_default_boundaries(n_variants: int, steps: int, as_percent: bool) -> List[float]: """Равномерные границы для n вариантов (n-1 границ). Возвращает шаги или проценты.""" if n_variants <= 1: return [] raw = [] for i in range(1, n_variants): b = round(i * steps / n_variants) if 0 < b < steps: raw.append(b) if as_percent: return [round(b * 100.0 / steps, 2) for b in raw] return raw def _ve_parse_variants(variants_multiline: str) -> List[str]: return [v.strip() for v in variants_multiline.split("\n") if v.strip()] def _ve_df_to_list(df_any: Any) -> List[List[Any]]: """Приводим вход от gr.Dataframe к списку списков [[val], ...] для унифицированной обработки.""" try: import pandas as _pd # локально, чтобы не требовать pandas жёстко if hasattr(df_any, "values") and isinstance(df_any, _pd.DataFrame): return df_any.values.tolist() except Exception: pass return df_any def _ve_normalize_boundaries(bound_list: Any, steps: int, as_percent: bool) -> Tuple[List[int], str]: """ Приводим user-ввод к отсортированным уникальным границам в шагах. Возвращаем (границы_в_шагах, warnings_text). """ warnings = [] bound_list = _ve_df_to_list(bound_list) vals: List[float] = [] for row in (bound_list or []): if isinstance(row, (list, tuple)) and row: v = row[0] else: v = row try: v = float(v) except Exception: continue if as_percent: if v <= 0: v = 0.01 if v >= 100: v = 99.99 v = v * steps / 100.0 v = int(round(v)) if v <= 0: v = 1 if v >= steps: v = steps - 1 vals.append(v) vals = sorted(set(vals)) vals = [v for v in vals if 0 < v < steps] if not vals and bound_list: warnings.append("Все границы вышли за допустимый диапазон и были удалены.") return vals, ("\n".join(f"- {w}" for w in warnings) if warnings else "") def _ve_build_scheduled_expr(variants: List[str], boundaries_steps: List[int], steps: int, as_percent: bool, reverse: bool) -> str: core = " : ".join(variants) if not boundaries_steps: expr = f"[ {core} ]" else: if as_percent: btxt = ", ".join(f"{int(round(b * 100.0 / steps))}%" for b in boundaries_steps) else: btxt = ", ".join(str(int(b)) for b in boundaries_steps) expr = f"[ {core} ] : {btxt}" if reverse: expr += " reverse" return expr def _ve_preview(variants_multiline: str, df_bounds: Any, steps: int, as_percent: bool, reverse: bool, seed: Optional[int], use_visitor: bool) -> Tuple[str, str, str, str]: """ Возвращает: (expr, schedule_md, timeline_md, status_md) """ variants = _ve_parse_variants(variants_multiline) if len(variants) == 0: return "", "", "", "**Введите хотя бы один вариант.**" # Один вариант — границы не нужны try: import pandas as _pd has_bounds = (df_bounds.shape[0] > 0) if isinstance(df_bounds, _pd.DataFrame) else bool(df_bounds) except Exception: has_bounds = bool(df_bounds) if len(variants) == 1 and has_bounds: return "", "", "", "Один вариант не требует границ (игнор)." bound_steps, warn = _ve_normalize_boundaries(df_bounds, steps, as_percent) needed = max(0, len(variants) - 1) status_extra = "" if len(bound_steps) != needed: bound_steps = [int(round(i * steps / len(variants))) for i in range(1, len(variants))] status_extra = f"Ожидалось {needed} границ, пересчитано равномерно." expr = _ve_build_scheduled_expr(variants, bound_steps, steps, as_percent, reverse) try: schedule = pp.get_schedule(expr, steps, True, seed, use_visitor=use_visitor) sched_md = _schedule_table_md(schedule) timeline_md = _per_step_timeline_md(schedule, steps) status = "**OK**" if warn: status += f"\n\n_Предупреждения:_\n{warn}" if status_extra: status += f"\n\n_Замечание:_ {status_extra}" return expr, sched_md, timeline_md, status except Exception as e: return expr, "", "", f"**Ошибка предпросмотра:** {e}" # --- Магниты, сдвиги, экспорт/импорт для редактора --- def _ve_snap_to_magnets(df_bounds: Any, steps: int, as_percent: bool) -> List[List[float]]: """ Притягиваем каждую границу к ближайшей из {25%, 50%, 75%}. Возвращаем список списков [[val], ...] в текущем режиме отображения (%, либо шаги). """ bounds_list = _ve_df_to_list(df_bounds) step_targets = [int(round(steps * 0.25)), int(round(steps * 0.50)), int(round(steps * 0.75))] snapped: List[int] = [] for row in (bounds_list or []): v = row[0] if isinstance(row, (list, tuple)) and row else row try: v = float(v) except Exception: continue if as_percent: v = int(round(max(1.0, min(99.0, v)) * steps / 100.0)) else: v = int(round(v)) # ближайший магнит best = min(step_targets, key=lambda t: abs(t - v)) best = max(1, min(steps - 1, best)) snapped.append(best) snapped = sorted(set(snapped)) if as_percent: return [[round(b * 100.0 / steps, 2)] for b in snapped] return [[b] for b in snapped] def _ve_shift_all(df_bounds: Any, steps: int, as_percent: bool, delta_steps: int) -> List[List[float]]: """ Сдвигаем все границы на delta_steps (в шагах), клэмпим в [1, steps-1]. Возвращаем в текущем режиме отображения. """ bounds_list = _ve_df_to_list(df_bounds) shifted: List[int] = [] for row in (bounds_list or []): v = row[0] if isinstance(row, (list, tuple)) and row else row try: v = float(v) except Exception: continue if as_percent: v = v * steps / 100.0 b = int(round(v)) + int(delta_steps) b = max(1, min(steps - 1, b)) shifted.append(b) shifted = sorted(set(shifted)) if as_percent: return [[round(b * 100.0 / steps, 2)] for b in shifted] return [[b] for b in shifted] def _ve_export_csv(df_bounds: Any, steps: int, as_percent: bool) -> str: """ CSV с одной колонкой 'boundary' — значения либо в шагах, либо в % (по текущему режиму). """ bounds_list = _ve_df_to_list(df_bounds) lines = ["boundary"] for row in (bounds_list or []): v = row[0] if isinstance(row, (list, tuple)) and row else row try: v = float(v) except Exception: continue lines.append(str(v if as_percent else int(round(v)))) return "\n".join(lines) def _ve_export_json(df_bounds: Any, steps: int, as_percent: bool) -> str: """ JSON: {"mode": "percent"|"steps", "steps": , "boundaries": [numbers]} """ bounds_list = _ve_df_to_list(df_bounds) vals: List[float] = [] for row in (bounds_list or []): v = row[0] if isinstance(row, (list, tuple)) and row else row try: v = float(v) except Exception: continue vals.append(v if as_percent else int(round(v))) data = {"mode": ("percent" if as_percent else "steps"), "steps": int(steps), "boundaries": vals} return json.dumps(data, ensure_ascii=False, indent=2) def _ve_import_intervals(text: str, steps: int, as_percent: bool, kind: str) -> List[List[float]]: """ Импортируем интервалы из CSV или JSON. Возвращаем [[val], ...] в текущем режиме отображения (as_percent). CSV: одна колонка boundary, без заголовка тоже допустимо (попробуем). JSON: {"mode": "...", "steps": S, "boundaries":[...]} """ vals_steps: List[int] = [] if kind == "CSV": rows = [ln.strip() for ln in (text or "").splitlines() if ln.strip()] if rows and rows[0].lower() == "boundary": rows = rows[1:] for r in rows: try: v = float(r) except Exception: continue if as_percent: vals_steps.append(int(round(max(0.01, min(99.99, v)) * steps / 100.0))) else: vals_steps.append(int(round(v))) else: # JSON try: obj = json.loads(text or "{}") except Exception: obj = {} mode = obj.get("mode", "steps") src_steps = int(obj.get("steps", steps) or steps) raw = obj.get("boundaries", []) or [] for v in raw: try: v = float(v) except Exception: continue if mode == "percent": vals_steps.append(int(round(max(0.01, min(99.99, v)) * steps / 100.0))) else: vals_steps.append(int(round(v))) vals_steps = sorted(set([max(1, min(steps - 1, int(v))) for v in vals_steps])) if as_percent: return [[round(v * 100.0 / steps, 2)] for v in vals_steps] return [[v] for v in vals_steps] # --------------------------------------------------------------------- # Основной анализ # --------------------------------------------------------------------- def analyze(prompt: str, steps: int, seed: Optional[int], use_visitor: bool, allow_empty_alt: bool, expand_alt_per_step: bool, group_combo_limit: int, view_mode: str, at_step_n: int, enable_pre: bool) -> Tuple[str, str, str, str, str, str]: try: maybe_reload_prompt_parser(allow_empty_alt, expand_alt_per_step, group_combo_limit) except Exception as e: err = f"**Не удалось применить ENV-флаги:** {e}" return err, "", "", "", "", "" pre_md = "" if enable_pre: pre_out, pre_changes = preprocess_prompt(prompt, steps, True) if pre_changes: pre_md = "**Препроцессор применил изменения:**\n" + "\n".join([f"- {c}" for c in pre_changes]) + \ "\n\n" + mk_diff(prompt, pre_out, "original", "preprocessed") prompt = pre_out status_md = "" tree_md = "" schedule_md = "" timeline_md = "" text_at_step_md = "" tree_str, err_md = _safe_parse_tree(prompt) if err_md: status_md = err_md else: status_md = "**OK:** Парсинг прошёл успешно." if view_mode == "Parse tree": tree_md = "```\n" + tree_str + "\n```" schedule, sched_err = _make_schedule(prompt, steps, seed, use_visitor) if sched_err: status_md += "\n\n" + sched_err return status_md, "", "", "", pre_md, tree_md if view_mode in ("Schedule", "All"): schedule_md = _schedule_table_md(schedule) if view_mode in ("Timeline (per step)", "All"): timeline_md = _per_step_timeline_md(schedule, steps) try: t = pp.at_step_from_schedule(at_step_n, schedule) text_at_step_md = f"**Шаг {at_step_n}:** `{t}`" except Exception as e: text_at_step_md = f"Ошибка получения текста на шаге {at_step_n}: {e}" if view_mode == "Visitor vs Transformer": tree_md = _compare_vis_vs_trans(prompt, steps, seed) return status_md, schedule_md, timeline_md, text_at_step_md, pre_md, tree_md # --------------------------------------------------------------------- # Проект: экспорт / импорт # --------------------------------------------------------------------- def export_project(an_src, steps, seed, use_visitor, allow_empty, expand_alt, group_limit, gen_cfg) -> str: data = { "analyzer": { "prompt": an_src, "steps": int(steps), "seed": int(seed) if seed is not None else None, "use_visitor": bool(use_visitor), "ALLOW_EMPTY_ALTERNATE": bool(allow_empty), "EXPAND_ALTERNATE_PER_STEP": bool(expand_alt), "GROUP_COMBO_LIMIT": int(group_limit), }, "generate": { "single": { "width": int(gen_cfg["w"]), "height": int(gen_cfg["h"]), "steps": int(gen_cfg["steps"]), "cfg": float(gen_cfg["cfg"]), "seed": int(gen_cfg["seed"]), "sampler": gen_cfg["sampler"], "batch_count": int(gen_cfg["bc"]), "batch_size": int(gen_cfg["bs"]), "restore_faces": bool(gen_cfg["rf"]), "tiling": bool(gen_cfg["tl"]), } } } return json.dumps(data, ensure_ascii=False, indent=2) def import_project(text: str): try: data = json.loads(text) a = data.get("analyzer", {}) g = data.get("generate", {}).get("single", {}) return ( a.get("prompt", ""), int(a.get("steps", 50)), int(a.get("seed", 42)) if a.get("seed") is not None else 42, bool(a.get("use_visitor", True)), bool(a.get("ALLOW_EMPTY_ALTERNATE", False)), bool(a.get("EXPAND_ALTERNATE_PER_STEP", True)), int(a.get("GROUP_COMBO_LIMIT", 100)), int(g.get("width", 512)), int(g.get("height", 768)), int(g.get("steps", 30)), float(g.get("cfg", 7.0)), int(g.get("seed", 42)), str(g.get("sampler", _list_samplers()[0])), int(g.get("batch_count", 1)), int(g.get("batch_size", 1)), bool(g.get("restore_faces", False)), bool(g.get("tiling", False)), ) except Exception: return ("", 50, 42, True, False, True, 100, 512, 768, 30, 7.0, 42, _list_samplers()[0], 1, 1, False, False) # --------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------- class PromptParserAnalyzerScript(scripts.Script): def title(self): return "Prompt Parser Analyzer" def show(self, is_txt2img): return scripts.AlwaysVisible def ui(self, is_txt2img): with gr.Group(): with gr.Accordion("Prompt Parser Analyzer", open=False): with gr.Tabs(): # ---------------- Analyzer ---------------- with gr.Tab("Analyzer"): prompt_in = gr.Textbox(label="Source Prompt", lines=6, placeholder="Вставьте промпт…") with gr.Row(): steps = gr.Slider(1, 300, value=50, step=1, label="Steps") seed = gr.Number(value=42, precision=0, label="Seed (для случайных конструкций)") at_step_n = gr.Slider(1, 300, value=25, step=1, label="Показать текст на шаге N") with gr.Row(): use_visitor = gr.Checkbox(value=True, label="use_visitor (быстрее; alternate2 может отличаться)") view_mode = gr.Dropdown( choices=["All", "Schedule", "Timeline (per step)", "Parse tree", "Visitor vs Transformer"], value="All", label="Режим вывода", ) with gr.Row(): allow_empty_alt = gr.Checkbox(value=False, label="ALLOW_EMPTY_ALTERNATE (разрешать пустые [a||b])") expand_alt = gr.Checkbox(value=True, label="EXPAND_ALTERNATE_PER_STEP (чередовать по шагам)") group_limit = gr.Slider(1, 5000, value=100, step=1, label="GROUP_COMBO_LIMIT") enable_pre = gr.Checkbox(value=True, label="Enable Preprocessor (синтаксический сахар)") analyze_btn = gr.Button("Analyze", variant="primary") status_md = gr.Markdown() schedule_md = gr.Markdown() timeline_md = gr.Markdown() text_at_step_md = gr.Markdown() pre_md = gr.Markdown() tree_md = gr.Markdown() gr.Markdown("#### Подсветка синтаксиса") highlight_html = gr.HTML() # ---------------- Grammar & Examples ---------------- with gr.Tab("Grammar & Examples"): gr.Markdown(""" ### Полная справка по синтаксису **() Круглые скобки — внимание/вес** - `(text)` — повышает вес по умолчанию (как в A1111). - `(text:1.2)` — явный вес. Разрешены и научные записи: `1e-2`. - Можно вкладывать: `((detail:1.3):1.1)`. - Ошибки: несбалансированные скобки; `:а` вместо числа. **{} Фигурные скобки — группы** - Без `|` внутри — просто «склеить вместе»: `{a, b, c}` → `a, b, c` (второй парсер не добавляет «магию»). - С `|` внутри — *комбинаторика*: `{a|b, c|d}` → `a, c`; `a, d`; `b, c`; `b, d` (до лимита `GROUP_COMBO_LIMIT`). - Можно вкладывать группы. - Недопустимо: пустая группа в критичных местах `{} ` (лучше избегать). **[] Квадратные скобки — альтернативы (alternate)** - `[a|b|c]` — «карусель» по шагам (если `EXPAND_ALTERNATE_PER_STEP=1`) или фиксированный выбор (если 0). - `[a|b]!` — *distinct*: выбрать **один** вариант на весь прогон. - `ALLOW_EMPTY_ALTERNATE=1` — позволяет пустые опции: `[a||b]`. - Ошибки: забыли `[]` и пишете просто `a|b` — это уже другой тип (alternate1/2). **Scheduled блоки — по шагам** - `[ a : b : c ] : 20%, 70%` — границы включаются снаружи через `:`. - Допустимы проценты (`30%`), шаги (`40`), диапазоны (`10-25`, `60%-80%`), `reverse`. - Пример: `[ sketch : lines : color ] : 5, 15 reverse`. - Ошибки: число границ должно быть **N-1** для **N** вариантов; мы подсказываем и правим равномерно. **Numbered — взять N вариантов** - `3 { a | b | c | d }` — выбрать 3 (с повторениями). - `3! { a | b | c | d }` — выбрать 3 **уникальных** (без повторений). - Тело может быть и в `[]` (если нужна пошаговая карусель). **Sequence (::) и Top-level (::: … !!)** - `owner :: key1 :: val1, key2 :: val2 !` — заканчивайте `!` или `;`. - Top-level: `owner ::: key :: val, key2 :: val2 !!, trailing plain`. - Ошибка: забыли `!`/`;`/`!!`. **AND-оператор** - `cat :0.7 AND dog :1.2 AND background :0.5` - Не режет `AND_PERP/AND_SALT/AND_TOPK`. - Ошибка: если это просто слово «and» в тексте — используйте запятую. Ниже — интерактивные тесты. Задайте свои примеры и жмите **Test**: увидите статус, Parse tree, Schedule, Timeline и подсветку. """) with gr.Row(): g_steps = gr.Slider(1, 300, value=50, step=1, label="Steps (для тестов)") g_seed = gr.Number(value=42, precision=0, label="Seed") g_at = gr.Slider(1, 300, value=25, step=1, label="Шаг N (preview)") with gr.Row(): g_use_visitor = gr.Checkbox(value=True, label="use_visitor") g_allow_empty = gr.Checkbox(value=False, label="ALLOW_EMPTY_ALTERNATE") g_expand = gr.Checkbox(value=True, label="EXPAND_ALTERNATE_PER_STEP") g_glimit = gr.Slider(1, 5000, value=100, step=1, label="GROUP_COMBO_LIMIT") g_pre = gr.Checkbox(value=False, label="Preprocessor (сахар)") def _grammar_test(prompt, steps, seed, use_vis, allow_empty, expand, glimit, atn, pre): st, sch, tl, at, premd, tree = analyze(prompt, int(steps), int(seed), bool(use_vis), bool(allow_empty), bool(expand), int(glimit), "All", int(atn), bool(pre)) return st, tree, sch, tl, at, highlight_prompt_html(prompt if not pre else preprocess_prompt(prompt, int(steps), True)[0]) # A) () with gr.Accordion("() Emphasis / Attention", open=False): a_paren = gr.Textbox(value="masterpiece, (detailed skin:1.2), (soft light)", lines=3, label="Prompt") a_btn = gr.Button("Test ()") a_stat = gr.Markdown() a_tree = gr.Markdown() a_sched = gr.Markdown() a_time = gr.Markdown() a_at = gr.Markdown() a_high = gr.HTML() a_btn.click(_grammar_test, [a_paren, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [a_stat, a_tree, a_sched, a_time, a_at, a_high]) # B) {} with gr.Accordion("{} Groups (dead / combos)", open=False): b_group = gr.Textbox(value="{red dress, windy street}, {blue jeans, rainy night}", lines=3, label="Prompt (dead groups)") b_btn = gr.Button("Test {} (dead)") b_stat = gr.Markdown(); b_tree = gr.Markdown(); b_sched = gr.Markdown(); b_time = gr.Markdown(); b_at = gr.Markdown(); b_high = gr.HTML() b_btn.click(_grammar_test, [b_group, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [b_stat, b_tree, b_sched, b_time, b_at, b_high]) b2_group = gr.Textbox(value="{red|blue, jeans|skirt}", lines=3, label="Prompt (with | → combos)") b2_btn = gr.Button("Test {} (combos)") b2_stat = gr.Markdown(); b2_tree = gr.Markdown(); b2_sched = gr.Markdown(); b2_time = gr.Markdown(); b2_at = gr.Markdown(); b2_high = gr.HTML() b2_btn.click(_grammar_test, [b2_group, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [b2_stat, b2_tree, b2_sched, b2_time, b2_at, b2_high]) # C) [] with gr.Accordion("[] Alternates / Distinct", open=False): c_alt = gr.Textbox(value="[red|blue|green] dress", lines=2, label="Prompt") c_btn = gr.Button("Test []") c_stat = gr.Markdown(); c_tree = gr.Markdown(); c_sched = gr.Markdown(); c_time = gr.Markdown(); c_at = gr.Markdown(); c_high = gr.HTML() c_btn.click(_grammar_test, [c_alt, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [c_stat, c_tree, c_sched, c_time, c_at, c_high]) c2_alt = gr.Textbox(value="[hair up|hair down]!", lines=2, label="Prompt (distinct)") c2_btn = gr.Button("Test []!") c2_stat = gr.Markdown(); c2_tree = gr.Markdown(); c2_sched = gr.Markdown(); c2_time = gr.Markdown(); c2_at = gr.Markdown(); c2_high = gr.HTML() c2_btn.click(_grammar_test, [c2_alt, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [c2_stat, c2_tree, c2_sched, c2_time, c2_at, c2_high]) # D) Scheduled with gr.Accordion("Scheduled [ ... ] : boundaries (steps/%, ranges, reverse)", open=False): d_sched = gr.Textbox(value="[ morning : afternoon : night ] : 20%, 70%", lines=2, label="Prompt") d_btn = gr.Button("Test scheduled") d_stat = gr.Markdown(); d_tree = gr.Markdown(); d_sched_md = gr.Markdown(); d_time = gr.Markdown(); d_at = gr.Markdown(); d_high = gr.HTML() d_btn.click(_grammar_test, [d_sched, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [d_stat, d_tree, d_sched_md, d_time, d_at, d_high]) d2_sched = gr.Textbox(value="[ sketch : lines : color ] : 5, 15 reverse", lines=2, label="Prompt (reverse)") d2_btn = gr.Button("Test scheduled (reverse)") d2_stat = gr.Markdown(); d2_tree = gr.Markdown(); d2_sched_md = gr.Markdown(); d2_time = gr.Markdown(); d2_at = gr.Markdown(); d2_high = gr.HTML() d2_btn.click(_grammar_test, [d2_sched, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [d2_stat, d2_tree, d2_sched_md, d2_time, d2_at, d2_high]) # E) Numbered with gr.Accordion("Numbered N / N! { ... }", open=False): e_num = gr.Textbox(value="3! { red | blue | green | yellow }", lines=2, label="Prompt") e_btn = gr.Button("Test numbered") e_stat = gr.Markdown(); e_tree = gr.Markdown(); e_sched = gr.Markdown(); e_time = gr.Markdown(); e_at = gr.Markdown(); e_high = gr.HTML() e_btn.click(_grammar_test, [e_num, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [e_stat, e_tree, e_sched, e_time, e_at, e_high]) # F) Sequence with gr.Accordion("Sequence (::) / Top-level (::: … !!)", open=False): f_seq = gr.Textbox(value="character :: outfit :: leather jacket, mood :: brooding !", lines=2, label="Prompt (:: ... !)") f_btn = gr.Button("Test sequence") f_stat = gr.Markdown(); f_tree = gr.Markdown(); f_sched = gr.Markdown(); f_time = gr.Markdown(); f_at = gr.Markdown(); f_high = gr.HTML() f_btn.click(_grammar_test, [f_seq, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [f_stat, f_tree, f_sched, f_time, f_at, f_high]) f2_seq = gr.Textbox(value="portrait ::: hair :: auburn, eyes :: blue !!, ultra-detailed", lines=2, label="Prompt (::: ... !!)") f2_btn = gr.Button("Test top-level sequence") f2_stat = gr.Markdown(); f2_tree = gr.Markdown(); f2_sched = gr.Markdown(); f2_time = gr.Markdown(); f2_at = gr.Markdown(); f2_high = gr.HTML() f2_btn.click(_grammar_test, [f2_seq, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [f2_stat, f2_tree, f2_sched, f2_time, f2_at, f2_high]) # G) AND with gr.Accordion("AND operator", open=False): g_and = gr.Textbox(value="cat :0.7 AND dog :1.3 AND background :0.5", lines=2, label="Prompt") g_btn = gr.Button("Test AND") g_stat = gr.Markdown(); g_tree = gr.Markdown(); g_sched_md = gr.Markdown(); g_time = gr.Markdown(); g_atxt = gr.Markdown(); g_highl = gr.HTML() g_btn.click(_grammar_test, [g_and, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [g_stat, g_tree, g_sched_md, g_time, g_atxt, g_highl]) # H) Plain with gr.Accordion("Plain text", open=False): h_plain = gr.Textbox(value="simple photo of a cat in a park, golden hour lighting", lines=2, label="Prompt") h_btn = gr.Button("Test plain") h_stat = gr.Markdown(); h_tree = gr.Markdown(); h_sched = gr.Markdown(); h_time = gr.Markdown(); h_at = gr.Markdown(); h_high = gr.HTML() h_btn.click(_grammar_test, [h_plain, g_steps, g_seed, g_use_visitor, g_allow_empty, g_expand, g_glimit, g_at, g_pre], [h_stat, h_tree, h_sched, h_time, h_at, h_high]) # ---------------- Lint / Fix ---------------- with gr.Tab("Blueprints / Composer"): import json, os bps = blueprints.get_blueprints() bp_ids = [bp.id for bp in bps] bp_select = gr.Dropdown(choices=bp_ids, value=bp_ids[0], label="Blueprint") bp_values = gr.Textbox(label="Values (JSON)", lines=8) bp_pairs = gr.Textbox(label="Pairs helper (key=val per line, optional)", lines=4) with gr.Row(): bp_make_btn = gr.Button("Assemble", variant="primary") bp_validate_btn = gr.Button("Validate only") bp_out = gr.Textbox(label="Output prompt", lines=6) bp_lints_md = gr.Markdown() def _bp_default_json(bp_id): bp = next(b for b in bps if b.id==bp_id) vals = {} for f in bp.fields: if f.default is not None: vals[f.name]=f.default else: vals[f.name]=([] if f.kind in ("list","choice") else "") if "pairs" in vals and not vals["pairs"]: vals["pairs"] = [{"key":"hair","val":"auburn"},{"key":"eyes","val":"blue"}] return json.dumps(vals, ensure_ascii=False, indent=2) bp_values.value = _bp_default_json(bp_ids[0]) bp_select.change(_bp_default_json, bp_select, bp_values, queue=False) def _bp_parse_pairs(s): res=[] for line in (s or "").splitlines(): if "=" in line: k,v = line.split("=",1) res.append({"key":k.strip(), "val":v.strip()}) return res def _bp_assemble(bp_id, values_json, pairs_helper): try: vals=json.loads(values_json or "{}") except Exception as e: return "", f"**JSON error:** {e}" bp = next(b for b in bps if b.id==bp_id) if "pairs" in [f.name for f in bp.fields] and (pairs_helper or "").strip(): vals["pairs"] = _bp_parse_pairs(pairs_helper) msgs=[] for c in bp.constraints: try: ok=c.check(vals) except Exception: ok=False if not ok: msgs.append(f"- {c.message}") if msgs: return "", "### Violations\n" + "\n".join(msgs) out = bp.assemble(vals) lint_msgs=[] for issue in linting.run_lints(out): lint_msgs.append(f"- **{issue.code}**: {issue.msg} @{issue.span}") return out, ("\n".join(lint_msgs) or "Нет предупреждений") bp_make_btn.click(_bp_assemble, [bp_select, bp_values, bp_pairs], [bp_out, bp_lints_md], queue=False) bp_validate_btn.click(lambda a,b,c: _bp_assemble(a,b,c)[1], [bp_select, bp_values, bp_pairs], bp_lints_md, queue=False) with gr.Tab("Checks / QuickProof"): import json, os from modules import prompt_parser as pp qp_in = gr.Textbox(label="Input prompt", lines=6) with gr.Row(): qp_steps = gr.Slider(1, 300, value=50, step=1, label="Steps") qp_seed = gr.Number(value=42, precision=0, label="Seed") qp_expected = gr.Textbox(label="Expected canonical (schedule JSON)", lines=8) with gr.Row(): qp_run = gr.Button("Run", variant="primary") qp_save = gr.Button("Save case (.json)") qp_result = gr.Markdown() qp_diff = gr.Markdown() qp_path = gr.Textbox(label="Save path", value="quickproof/case1.json") def _qp_canon(prompt, steps, seed): try: sched = pp.get_schedule(prompt, int(steps), True, int(seed), use_visitor=True) return quickproof.canonical_schedule_json(sched) except Exception as e: return f"**Error:** {e}" def _qp_run(inp, steps, seed, expected): canon = _qp_canon(inp, steps, seed) if expected.strip(): ok, diff = quickproof.compare_canonical(expected, canon) head = "✅ OK" if ok else "❌ Mismatch" body = ("```diff\n"+diff+"\n```") if diff else "" return head, body else: return "Canonical:", "```json\n"+canon+"\n```" def _qp_save(inp, steps, seed, expected, path): os.makedirs(os.path.dirname(path), exist_ok=True) case={"input":inp, "steps":int(steps), "seed":int(seed), "expected_canonical":expected} quickproof.save_case(path, case) return f"Saved to **{path}**" qp_run.click(_qp_run, [qp_in, qp_steps, qp_seed, qp_expected], [qp_result, qp_diff], queue=False) qp_save.click(_qp_save, [qp_in, qp_steps, qp_seed, qp_expected, qp_path], qp_result, queue=False) with gr.Tab("Lint Registry"): lint_src = gr.Textbox(label="Text to lint", lines=6) with gr.Row(): lint_run = gr.Button("Run lints") lint_fix_break = gr.Button("Autofix BREAK spacing") lint_out = gr.Markdown() lint_run.click(lambda s: "\n".join([f"- **{i.code}**: {i.msg} @{i.span}" for i in linting.run_lints(s)]) or "Нет предупреждений", lint_src, lint_out, queue=False) def _fix_break(s): out=s for i in linting.run_lints(s): if i.code=="BREAK_SPACE" and i.autofix: out=i.autofix(out) return out lint_fix_break.click(_fix_break, lint_src, lint_src, queue=False) with gr.Tab("Lint / Fix"): lint_src = gr.Textbox(label="Prompt для проверки", lines=6, placeholder="Вставьте промпт…") with gr.Row(): lint_use_visitor = gr.Checkbox(value=True, label="use_visitor (для эвристик)") lint_btn = gr.Button("Run Lint", variant="primary") fix_btn = gr.Button("Apply Safe Fixes") lint_report = gr.Markdown() fixed_out = gr.Textbox(label="Fixed Prompt (предпросмотр)", lines=6) diff_md = gr.Markdown() # ---------------- Scheduler ---------------- with gr.Tab("Scheduler"): gr.Markdown("Соберите `scheduled`: варианты по строкам, интервалы (%, шаги), опционально `reverse`.") sched_variants = gr.Textbox(label="Варианты (каждый с новой строки)", value="A\nB\nC", lines=6) sched_ranges = gr.Textbox(label="Интервалы", placeholder="30, 70 или 10%-30%, 60-80") sched_reverse = gr.Checkbox(value=False, label="reverse") build_sched_btn = gr.Button("Build [ ... ] : ranges", variant="primary") sched_expr = gr.Textbox(label="Результат", lines=2) gr.Markdown("---") gr.Markdown("Альтернативы: детерминированный переключатель каждые K шагов (строится через scheduled).") alt_bar = gr.Textbox(label="Опции (через |)", value="red|blue|green") k_steps = gr.Slider(1, 100, value=10, step=1, label="K (каждые K шагов)") total_steps_for_k = gr.Slider(1, 300, value=50, step=1, label="Steps (для расчёта границ)") build_every_k = gr.Button("Build alternate every K", variant="secondary") every_k_expr = gr.Textbox(label="Результат", lines=2) # --- VISUAL INTERVAL EDITOR --- gr.Markdown("## Interval Editor (visual)") with gr.Row(): ve_variants = gr.Textbox( label="Варианты (каждый с новой строки)", value="Look A\nLook B\nLook C", lines=6, show_label=True, ) with gr.Column(): ve_steps = gr.Slider(1, 300, value=50, step=1, label="Steps (для предпросмотра)") ve_as_percent = gr.Checkbox(value=False, label="Редактировать в % (иначе — в шагах)") ve_reverse = gr.Checkbox(value=False, label="reverse") ve_use_visitor = gr.Checkbox(value=True, label="use_visitor") ve_seed = gr.Number(value=42, precision=0, label="Seed") gr.Markdown("**Границы:** задайте N-1 границ для N вариантов. В процентах или шагах — по переключателю выше.") ve_df = gr.Dataframe( headers=["boundary"], datatype=["number"], row_count=(0, "dynamic"), col_count=1, label="Boundary list", interactive=True, type="pandas" ) with gr.Row(): ve_init = gr.Button("Init (по вариантам)") ve_even = gr.Button("Равномерно") ve_sort = gr.Button("Сортировать/Клэмп") with gr.Row(): ve_snap = gr.Button("Snap → 25/50/75%") ve_minus1 = gr.Button("−1 шаг") ve_plus1 = gr.Button("+1 шаг") with gr.Row(): ve_exp_csv = gr.Button("Export CSV") ve_exp_json = gr.Button("Export JSON") ve_imp_kind = gr.Dropdown(choices=["CSV", "JSON"], value="CSV", label="Import format") ve_imp_text = gr.Textbox(label="Paste CSV/JSON here и нажмите Import", lines=6) ve_imp_btn = gr.Button("Import", variant="secondary") ve_expr = gr.Textbox(label="Собранный scheduled-выражение", lines=2) ve_status = gr.Markdown() ve_sched_md = gr.Markdown() ve_timeline_md = gr.Markdown() ve_export_out = gr.Textbox(label="Exported Intervals (CSV/JSON)", lines=8) # ---------------- Converters / Presets ---------------- with gr.Tab("Converters / Presets"): conv_src = gr.Textbox(label="Prompt для преобразования", lines=6) with gr.Row(): conv_dead = gr.Button("Dead groups → plain") conv_bars = gr.Button("a|b → [a|b]") conv_seq_term = gr.Button("Sequence: добавить терминатор") conv_out = gr.Textbox(label="Результат", lines=6) gr.Markdown("---") gr.Markdown("Конструкторы Lark") with gr.Row(): grp_items = gr.Textbox(label="Group items (через запятую)", placeholder="a, b, c") grp_out = gr.Textbox(label="Результат", lines=2) grp_btn = gr.Button("Build {group}") with gr.Row(): alt_items = gr.Textbox(label="Alternates (через |)", placeholder="a|b|c") alt_distinct = gr.Checkbox(value=False, label="distinct (!)") alt_out = gr.Textbox(label="Результат", lines=2) alt_btn = gr.Button("Build [alternate]") with gr.Row(): num_n = gr.Number(value=3, precision=0, label="N (сколько взять)") num_body = gr.Textbox(label="Опции: '{...}' или 'a|b|c'", placeholder="{ a | b | c } или a|b|c") num_distinct = gr.Checkbox(value=True, label="distinct (!)") num_out = gr.Textbox(label="Результат", lines=2) num_btn = gr.Button("Build numbered") with gr.Row(): seq_owner = gr.Textbox(label="Sequence owner", value="character") seq_pairs = gr.Textbox(label="Sequence pairs (k:v; k:v)", value="outfit: leather jacket; mood: brooding") seq_term = gr.Dropdown(choices=["!", ";"], value="!", label="Terminator") seq_out = gr.Textbox(label="Результат", lines=2) seq_btn = gr.Button("Build sequence") with gr.Row(): top_owner = gr.Textbox(label="Top-level owner", value="portrait") top_pairs = gr.Textbox(label="Pairs (k:v; k:v)", value="hair: auburn; eyes: blue") top_tail = gr.Textbox(label="Trailing plain (опционально)", value="ultra-detailed skin texture") top_out = gr.Textbox(label="Результат", lines=2) top_btn = gr.Button("Build top-level sequence") # ---------------- Export / Diff / Project ---------------- with gr.Tab("Export / Diff / Project"): exp_src = gr.Textbox(label="Prompt (источник для экспорта)", lines=6) with gr.Row(): exp_steps = gr.Slider(1, 300, value=50, step=1, label="Steps") exp_seed = gr.Number(value=42, precision=0, label="Seed") exp_use_visitor = gr.Checkbox(value=True, label="use_visitor") exp_btn = gr.Button("Build schedule/timeline", variant="primary") exp_sched_csv = gr.Textbox(label="Schedule CSV", lines=6) exp_sched_json = gr.Textbox(label="Schedule JSON", lines=8) exp_timeline_csv = gr.Textbox(label="Timeline CSV", lines=8) copy_md_btn = gr.Button("Copy Markdown of schedule/timeline (ниже)") exp_sched_md = gr.Markdown() exp_timeline_md = gr.Markdown() gr.Markdown("---") gr.Markdown("### Project JSON") proj_export_btn = gr.Button("Export Project JSON") proj_json = gr.Textbox(label="Project JSON", lines=14) proj_import_btn = gr.Button("Import Project JSON") proj_status = gr.Markdown() # ---------------- AND Debugger ---------------- with gr.Tab("AND Debugger"): and_src = gr.Textbox(label="AND-chain prompt", lines=6) and_parse_btn = gr.Button("Parse AND chain") and_table = gr.Markdown() and_builder_txt = gr.Textbox(label="Builder: text[:weight] на каждой строке", lines=6) and_build_btn = gr.Button("Build AND chain") and_out = gr.Textbox(label="Результат", lines=2) # ---------------- Generate (txt2img) ---------------- with gr.Tab("Generate"): gr.Markdown("### Single") gen_prompt = gr.Textbox(label="Prompt", lines=6, placeholder="Возьмём из Analyzer → Source Prompt, можно отредактировать") gen_negative = gr.Textbox(label="Negative", lines=4, value="") with gr.Row(): gen_width = gr.Slider(64, 1536, value=512, step=64, label="Width") gen_height = gr.Slider(64, 1536, value=768, step=64, label="Height") gen_steps = gr.Slider(1, 300, value=30, step=1, label="Steps") with gr.Row(): gen_cfg = gr.Slider(1, 20, value=7.0, step=0.5, label="CFG") gen_seed = gr.Number(value=42, precision=0, label="Seed") gen_sampler = gr.Dropdown(choices=_list_samplers(), value=_list_samplers()[0], label="Sampler") with gr.Row(): gen_batch_count = gr.Slider(1, 16, value=1, step=1, label="Batch Count") gen_batch_size = gr.Slider(1, 8, value=1, step=1, label="Batch Size") with gr.Row(): gen_restore_faces = gr.Checkbox(value=False, label="Restore faces") gen_tiling = gr.Checkbox(value=False, label="Tiling") gen_btn = gr.Button("Generate", variant="primary") gen_gallery = gr.Gallery(label="Result").style(grid=4) gen_info = gr.Textbox(label="Info", lines=4) gr.Markdown("---") gr.Markdown("### A/B Variants (placeholder)") gr.Markdown("Задайте плейсхолдер в промпте, напр. `{{ALT}}`, и список вариантов построчно.") var_prompt = gr.Textbox(label="Prompt (с плейсхолдером)", lines=6) var_negative = gr.Textbox(label="Negative", lines=4, value="") var_placeholder = gr.Textbox(label="Placeholder", value="{{ALT}}") var_options = gr.Textbox(label="Options (каждая на новой строке)", value="red dress\nblue jeans\nblack coat", lines=6) with gr.Row(): var_width = gr.Slider(64, 1536, value=512, step=64, label="Width") var_height = gr.Slider(64, 1536, value=768, step=64, label="Height") var_steps = gr.Slider(1, 300, value=30, step=1, label="Steps") with gr.Row(): var_cfg = gr.Slider(1, 20, value=7.0, step=0.5, label="CFG") var_seed = gr.Number(value=1000, precision=0, label="Base Seed") var_sampler = gr.Dropdown(choices=_list_samplers(), value=_list_samplers()[0], label="Sampler") with gr.Row(): var_restore_faces = gr.Checkbox(value=False, label="Restore faces") var_tiling = gr.Checkbox(value=False, label="Tiling") var_btn = gr.Button("Generate A/B", variant="secondary") var_gallery = gr.Gallery(label="Variants").style(grid=6) var_info = gr.Textbox(label="Info", lines=6) # ------- Bindings ------- def _analyze_wrap(src, s, sd, uv, aea, ealt, gl, vm, stepn, en_pre): res = analyze(src, s, sd, uv, aea, ealt, gl, vm, stepn, en_pre) analyzed_prompt = src if en_pre: pre_prompt, _ = preprocess_prompt(src, s, True) analyzed_prompt = pre_prompt return (*res, highlight_prompt_html(analyzed_prompt)) analyze_btn.click( fn=_analyze_wrap, inputs=[prompt_in, steps, seed, use_visitor, allow_empty_alt, expand_alt, group_limit, view_mode, at_step_n, enable_pre], outputs=[status_md, schedule_md, timeline_md, text_at_step_md, pre_md, tree_md, highlight_html], ) # Grammar & Examples: (логика в _grammar_test) # Lint/Fix def _run_lint(src: str, use_vis: bool): issues = lint_prompt(src, use_vis) if not issues: return "**OK:** проблем не найдено.", "" lines = [] for it in issues: lines.append(f"- **{it.kind}**: {it.msg} — _{it.fix_hint or '…'}_") return "\n".join(lines), "" lint_btn.click(_run_lint, [lint_src, lint_use_visitor], [lint_report, fixed_out]) def _apply_fixes(src: str, use_vis: bool): fixed, changes = apply_safe_fixes(src, use_vis) report = "**Применены правки:**\n" + "\n".join([f"- {c}" for c in changes]) if changes else "_Нечего править (safe-fixes)._" return fixed, report, mk_diff(src, fixed) fix_btn.click(_apply_fixes, [lint_src, lint_use_visitor], [fixed_out, lint_report, diff_md]) # Scheduler (быстрые билдеры) build_sched_btn.click(lambda v, r, rev: build_scheduled_block(v, r, rev), [sched_variants, sched_ranges, sched_reverse], [sched_expr]) build_every_k.click(lambda bar, s, k: build_alternate_every_k(bar, int(s), int(k)), [alt_bar, total_steps_for_k, k_steps], [every_k_expr]) # --- Interval Editor: callbacks --- def _ve_init_cb(variants_text, steps_val, as_percent): variants = _ve_parse_variants(variants_text) bounds = _ve_default_boundaries(len(variants), int(steps_val), bool(as_percent)) df = [[b] for b in bounds] return df ve_init.click(_ve_init_cb, [ve_variants, ve_steps, ve_as_percent], [ve_df]) # предпросмотр ve_init.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) def _ve_even_cb(variants_text, steps_val, as_percent, df_current): return _ve_init_cb(variants_text, steps_val, as_percent) ve_even.click(_ve_even_cb, [ve_variants, ve_steps, ve_as_percent, ve_df], [ve_df]) ve_even.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) def _ve_sort_cb(df_current, steps_val, as_percent): vals, _ = _ve_normalize_boundaries(df_current, int(steps_val), bool(as_percent)) return [[v if not as_percent else round(v * 100.0 / int(steps_val), 2)] for v in vals] ve_sort.click(_ve_sort_cb, [ve_df, ve_steps, ve_as_percent], [ve_df]) ve_sort.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) def _ve_snap_cb(df_current, steps_val, as_percent): return _ve_snap_to_magnets(df_current, int(steps_val), bool(as_percent)) ve_snap.click(_ve_snap_cb, [ve_df, ve_steps, ve_as_percent], [ve_df]) ve_snap.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) def _ve_minus1_cb(df_current, steps_val, as_percent): return _ve_shift_all(df_current, int(steps_val), bool(as_percent), -1) ve_minus1.click(_ve_minus1_cb, [ve_df, ve_steps, ve_as_percent], [ve_df]) ve_minus1.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) def _ve_plus1_cb(df_current, steps_val, as_percent): return _ve_shift_all(df_current, int(steps_val), bool(as_percent), +1) ve_plus1.click(_ve_plus1_cb, [ve_df, ve_steps, ve_as_percent], [ve_df]) ve_plus1.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) # Живой предпросмотр при любом изменении def _ve_preview_cb(variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_visitor_val): expr, sched_md, timeline_md, status = _ve_preview( variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_visitor_val) ) return expr, sched_md, timeline_md, status for _comp in [ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor, ve_variants]: _comp.change( _ve_preview_cb, inputs=[ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], outputs=[ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) # Export/Import интервалов ve_exp_csv.click(lambda df_c, s, ap: _ve_export_csv(df_c, int(s), bool(ap)), [ve_df, ve_steps, ve_as_percent], [ve_export_out]) ve_exp_json.click(lambda df_c, s, ap: _ve_export_json(df_c, int(s), bool(ap)), [ve_df, ve_steps, ve_as_percent], [ve_export_out]) ve_imp_btn.click(lambda text, s, ap, kind: _ve_import_intervals(text, int(s), bool(ap), kind), [ve_imp_text, ve_steps, ve_as_percent, ve_imp_kind], [ve_df]) ve_imp_btn.click( lambda variants_text, df_current, steps_val, as_percent, reverse, seed_val, use_vis: _ve_preview(variants_text, df_current, int(steps_val), bool(as_percent), bool(reverse), int(seed_val) if seed_val is not None else None, bool(use_vis)), [ve_variants, ve_df, ve_steps, ve_as_percent, ve_reverse, ve_seed, ve_use_visitor], [ve_expr, ve_sched_md, ve_timeline_md, ve_status] ) # Converters conv_dead.click(lambda s: conv_dead_groups_to_plain(s), [conv_src], [conv_out]) conv_bars.click(lambda s: conv_bars_to_square(s), [conv_src], [conv_out]) conv_seq_term.click(lambda s: conv_add_seq_terminator(s), [conv_src], [conv_out]) # Builders grp_btn.click(build_group, [grp_items], [grp_out]) alt_btn.click(build_alternate, [alt_items, alt_distinct], [alt_out]) num_btn.click(build_numbered, [num_n, num_body, num_distinct], [num_out]) seq_btn.click(build_sequence, [seq_owner, seq_pairs, seq_term], [seq_out]) top_btn.click(build_top_level_sequence, [top_owner, top_pairs, top_tail], [top_out]) # Export расписаний def _export_all(src: str, s: int, sd: Optional[int], use_vis: bool): sched, err = _make_schedule(src, s, sd, use_vis) if err: return "", "", "", f"**Ошибка:** {err}", "" csv_sched = schedule_to_csv(sched) json_sched = schedule_to_json(sched) csv_timeline = timeline_to_csv(sched, s) md1 = md_block("Schedule", _schedule_table_md(sched)) md2 = md_block("Timeline", _per_step_timeline_md(sched, s)) return csv_sched, json_sched, csv_timeline, md1, md2 exp_btn.click(_export_all, [exp_src, exp_steps, exp_seed, exp_use_visitor], [exp_sched_csv, exp_sched_json, exp_timeline_csv, exp_sched_md, exp_timeline_md]) def _copy_md(md1: str, md2: str): return md1 + "\n\n" + md2 copy_md_btn.click(_copy_md, [exp_sched_md, exp_timeline_md], [exp_sched_md]) # Project export/import def _proj_export(an_src_v, steps_v, seed_v, uv_v, aea_v, ealt_v, gl_v, w, h, st, cfg, ssd, samp, bc, bs, rf, tl): cfg_map = {"w": w, "h": h, "steps": st, "cfg": cfg, "seed": ssd, "sampler": samp, "bc": bc, "bs": bs, "rf": rf, "tl": tl} return export_project(an_src_v, steps_v, seed_v, uv_v, aea_v, ealt_v, gl_v, cfg_map) proj_export_btn.click(_proj_export, [exp_src, exp_steps, exp_seed, exp_use_visitor, allow_empty_alt, expand_alt, group_limit, gen_width, gen_height, gen_steps, gen_cfg, gen_seed, gen_sampler, gen_batch_count, gen_batch_size, gen_restore_faces, gen_tiling], [proj_json]) def _proj_import(text): vals = import_project(text) return vals proj_import_btn.click(_proj_import, [proj_json], [exp_src, exp_steps, exp_seed, exp_use_visitor, allow_empty_alt, expand_alt, group_limit, gen_width, gen_height, gen_steps, gen_cfg, gen_seed, gen_sampler, gen_batch_count, gen_batch_size, gen_restore_faces, gen_tiling]) # AND Debugger def _and_parse(src): rows = and_debug_parse(src) if not rows: return "_Нет сегментов._", "" lines = ["| # | text | weight |", "|---:|---|---:|"] builder_lines = [] for i, (t, w) in enumerate(rows, 1): wt = "" if w is None else str(w) safe_t = t.replace("|", "\\|") lines.append(f"| {i} | {safe_t} | {wt} |") builder_lines.append(f"{t} : {wt}" if wt else t) return "\n".join(lines), "\n".join(builder_lines) and_parse_btn.click(_and_parse, [and_src], [and_table, and_builder_txt]) and_build_btn.click(lambda rows: and_debug_build(rows), [and_builder_txt], [and_out]) # Generate — Single gen_btn.click( fn=_generate_txt2img, inputs=[gen_prompt, gen_negative, gen_steps, gen_width, gen_height, gen_cfg, gen_seed, gen_sampler, gen_batch_count, gen_batch_size, gen_restore_faces, gen_tiling], outputs=[gen_gallery, gen_info] ) # Generate — A/B Variants var_btn.click( fn=_generate_variants_with_placeholder, inputs=[var_prompt, var_negative, var_placeholder, var_options, var_steps, var_width, var_height, var_cfg, var_seed, var_sampler, var_restore_faces, var_tiling], outputs=[var_gallery, var_info] ) return [] # A1111 не требует возвращать список компонентов def on_ui_settings(): section = ('prompt-parser-analyzer', "Prompt Parser Analyzer") # Можно добавить глобальные Settings при желании. script_callbacks.on_ui_settings(on_ui_settings)