dikdimon's picture
Upload stable-diffusion-webui-prompt-parser3 using SD-Hub
7168315 verified
raw
history blame
33.7 kB
# -*- coding: utf-8 -*-
# Prompt Parser Analyzer — расширенная версия
# Доработки: Линтер+Автофиксы, Конструктор расписаний, Экспорт/Дифф, Конвертеры/Стратегии.
#
# Требования: 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.shared import opts
# Импортируем модуль парсера (второй файл), чтобы при смене ENV можно было reload(...)
from modules import prompt_parser as pp
# ---------------------------------------------------------------------
# Утилиты: подсветка ошибок, советы
# ---------------------------------------------------------------------
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 _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, "{", "}")))
# Sequence без терминатора
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))
# 'a | b' вне квадратных скобок
if re.search(r"(?:^|[^[])\b[^]\n]*\|[^[]", src) and "[" not in src:
issues.append(Issue("Hint", "Используется '|' вне '[]' — это alternate1/2 (рандом), а не карусель по шагам.",
"Заменить на [ a | b ] для перебора по шагам.",
bars_to_square_alternate))
# '&' в plain
if "&" in src:
issues.append(Issue("Hint", "Символ '&' активирует and_rule (логическая сцепка).",
"Если это текст, замените на 'and' или запятую.",
replace_amp_with_and))
# Dead group: {a, b, c} без |
if "{" in src and "|" not in src:
issues.append(Issue("Hint", "Группа без '|' (dead group) — просто склейка, фигурные скобки не нужны.",
"Уберите {} — оставьте плоский список.",
remove_dead_groups))
# alternate_distinct ']]!!'
if re.search(r"\[[^]]+\]!\s*!", src):
issues.append(Issue("Warning", "Лишний '!' после alternate_distinct.",
"Должно быть '[a|b]!' (одно '!').",
lambda s: re.sub(r"\]!\s*!", "]!", s)))
# numbered: N! {a|b} с недостатком опций
for m in re.finditer(r"(\d+)\s*!?\s*{([^}]+)}", src):
n = int(m.group(1))
body = m.group(2)
opts = [o.strip() for o in body.split("|") if o.strip()]
if "!" in m.group(0) and len(opts) < n:
issues.append(Issue("Warning", f"В numbered distinct запрошено {n}, но вариантов всего {len(opts)}.",
"Уменьшите N или уберите '!'.",
lambda s, _n=n: re.sub(rf"\b{_n}\s*!", str(_n), s, count=1)))
# alternate2 под use_visitor=True
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:
# Превращаем одинарный 'a | b' снаружи в '[ a | b ]'
# Осторожно: если уже есть [..], не трогаем.
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:
# {a, b, c} -> a, b, c (только если внутри нет '|')
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_multiline: каждая строка — один вариант (p1, p2, p3)
ranges_text: '30, 70' или '10%-30%, 60-80'
reverse: True/False
"""
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:
"""
Детеминированный переключатель «каждые K шагов».
Для n опций строим [o1 : o2 : ...] : K, 2K, 3K ...
"""
opts = [o.strip() for o in options_bar.split("|") if o.strip()]
if not opts:
return ""
boundaries = []
# границы по кратным k, но не превышая steps
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```"
# ---------------------------------------------------------------------
# Основной анализ
# ---------------------------------------------------------------------
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) -> Tuple[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, "", "", "", ""
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, "", "", "", 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, tree_md
# ---------------------------------------------------------------------
# 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")
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()
tree_md = gr.Markdown()
# ---------------- Lint/Fix ----------------
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 Builder ----------------
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)
# ---------------- Converters ----------------
with gr.Tab("Converters"):
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 ----------------
with gr.Tab("Export & Diff"):
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()
# ------- Bindings -------
analyze_btn.click(
fn=analyze,
inputs=[prompt_in, steps, seed, use_visitor, allow_empty_alt, expand_alt, group_limit, view_mode, at_step_n],
outputs=[status_md, schedule_md, timeline_md, text_at_step_md, tree_md],
)
# 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])
# 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])
# Для совместимости вернём ряд ключевых инпутов
return [ ]
def on_ui_settings():
section = ('prompt-parser-analyzer', "Prompt Parser Analyzer")
# Можно добавить глобальные Settings при желании.
script_callbacks.on_ui_settings(on_ui_settings)