dikdimon's picture
Update stable-diffusion-webui-prompt-parser7/scripts/prompt-refactor.py
fea61c5 verified
raw
history blame
94.7 kB
# -*- 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
esc = re.sub(r"(\[\]|\[|\])", r'<span style="color:#4aa3ff">\1</span>', esc)
esc = re.sub(r"(\{\}|\{|\})", r'<span style="color:#ffa640">\1</span>', esc)
esc = re.sub(r"(\(|\))", r'<span style="color:#92d050">\1</span>', esc)
esc = re.sub(r"(::|:::|!!|!|;|\|)", r'<span style="color:#ff5b8a">\1</span>', esc)
esc = re.sub(r"\bAND\b(?!_)", r'<span style="color:#ffd000">AND</span>', esc)
return f'<pre style="white-space:pre-wrap; font-family:monospace; line-height:1.3">{esc}</pre>'
# ---------------------------------------------------------------------
# Генерация (интеграция с 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": <int>, "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)