| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os |
| import re |
| import traceback |
| 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 |
|
|
| |
| 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: |
| """Превращаем Lark-исключение в понятный отчёт с подсветкой и советами.""" |
| 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" |
|
|
| |
| |
| |
|
|
| 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: |
| """Вернёт True, если переменные изменились и модуль нужно перезагрузить.""" |
| 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): |
| """Если флаги изменились, перезагружаем modules.prompt_parser, чтобы обновить грамматику.""" |
| 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): |
| """Пробует распарсить и вернуть (tree_str, error_md).""" |
| 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]: |
| """Возвращает (schedule, error_md). schedule: [[end_step, text], ...]""" |
| 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: |
| """Markdown-таблица из расписания.""" |
| 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: |
| """Показывает активный текст на каждом шаге 1..steps.""" |
| 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: |
| """Сравнение use_visitor=True/False.""" |
| 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) |
|
|
| 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]: |
| """ |
| Главная точка: возвращаем 5 выходов: |
| 1) status_md — статус/ошибки/подсказки |
| 2) schedule_md — таблица расписания |
| 3) timeline_md — поминутка по шагам |
| 4) text_at_step_md — текст на указанном шаге |
| 5) tree_md — дерево/сравнение, в зависимости от режима |
| """ |
| |
| 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 |
|
|
| |
| |
| |
|
|
| def build_group(items_csv: str) -> str: |
| """'a, b, c' -> '{ a, b, c }'""" |
| 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: |
| """'a|b|c' -> '[ a|b|c ]' или '[ a|b|c ]!'""" |
| 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 и 'a|b|c' или '{...}' -> 'N! {...}' / 'N {...}'""" |
| 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='character' |
| pairs_text='outfit: leather jacket; mood: brooding' |
| terminator = '!' или ';' |
| """ |
| 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: |
| """top-level: owner ::: ... !! , trailing""" |
| 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 helpers_demo_md() -> str: |
| return """ |
| ### Быстрые шпаргалки (конструкторы) |
| |
| - **Group:** `{ a, b, c }` |
| Смысл: держим фразы как один блок (можно затем оборачивать в `1! { ... }`, `N! {...}`). |
| |
| - **Alternates:** `[ a | b | c ]` / `[ a | b | c ]!` |
| Карусель по шагам (если EXPAND_ALT_PER_STEP=1) или один выбор на прогон (`]!`). |
| |
| - **Numbered:** `3! { a | b | c | d }` |
| Возьми 3 уникальных варианта без повторов (или `3 { ... }` с повторами). |
| |
| - **Sequence:** |
| `character :: outfit :: leather jacket, mood :: brooding !` |
| |
| - **Top-level sequence + хвост:** |
| `portrait ::: hair :: auburn, eyes :: blue !!, ultra-detailed skin texture` |
| |
| - **Scheduled (особый случай с одним элементом):** |
| `[ "add rim light" ] : 25` — до 25-го шага пусто, после — фраза включается. |
| """ |
|
|
| |
| |
| |
|
|
| 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(): |
| 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() |
|
|
| with gr.Tab("Builders"): |
| 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") |
| gr.Markdown("—") |
| 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") |
|
|
| 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], |
| ) |
|
|
| grp_btn.click(fn=build_group, inputs=[grp_items], outputs=[grp_out]) |
| alt_btn.click(fn=build_alternate, inputs=[alt_items, alt_distinct], outputs=[alt_out]) |
| num_btn.click(fn=build_numbered, inputs=[num_n, num_body, num_distinct], outputs=[num_out]) |
| seq_btn.click(fn=build_sequence, inputs=[seq_owner, seq_pairs, seq_term], outputs=[seq_out]) |
| top_btn.click(fn=build_top_level_sequence, inputs=[top_owner, top_pairs, top_tail], outputs=[top_out]) |
|
|
| return [prompt_in, steps, seed, at_step_n, use_visitor] |
|
|
| def on_ui_settings(): |
| section = ('prompt-parser-analyzer', "Prompt Parser Analyzer") |
| |
| |
|
|
| script_callbacks.on_ui_settings(on_ui_settings) |
|
|