dikdimon's picture
Upload stable-diffusion-webui-prompt-parser using SD-Hub
4ea37a8 verified
raw
history blame
30.4 kB
# Created by Jake Carter @ https://github.com/JakeCarterDPM/stable-diffusion-webui-prompt-refactor
# Rewritten as Prompt Parser Analyzer based on advanced prompt parser
# Fixed emphasized handling to avoid 'Tree' object has no attribute 'type' error
import re
import gradio as gr
from modules import script_callbacks, scripts, shared
from modules.shared import opts
from collections import namedtuple
import lark
import random
from functools import lru_cache
import hashlib
from itertools import product
import os
import logging
import traceback
logger = logging.getLogger(__name__)
# Feature flags (can be overridden via env)
def _env_bool(name: str, default: str = "0") -> bool:
v = str(os.getenv(name, default)).strip().lower()
return v not in ("0", "", "false", "no", "off")
ALLOW_EMPTY_ALTERNATE = _env_bool("ALLOW_EMPTY_ALTERNATE", "0")
EXPAND_ALTERNATE_PER_STEP = _env_bool("EXPAND_ALTERNATE_PER_STEP", "1")
GROUP_COMBO_LIMIT = int(os.getenv("GROUP_COMBO_LIMIT", "100"))
CACHE_SIZE = int(os.getenv('PROMPT_PARSER_CACHE_SIZE', 4096))
# Lark Grammar
_alt_rule = r' "[" prompt ("|" prompt)* "]" ' if not ALLOW_EMPTY_ALTERNATE else r' "[" prompt ("|" [prompt])+ "]" '
_grammar = r"""
!start: (prompt | /[][():|]/+)*
prompt: (scheduled | emphasized | grouped
| alternate | alternate_distinct
| alternate1 | alternate2
| top_level_sequence | sequence
| compound | numbered | and_rule
| plain | WHITESPACE)*
!emphasized: "(" prompt ")"
| "(" prompt ":" prompt ")"
| "(" prompt ":" NUMBER ")"
| "[" prompt "]"
scheduled: "[" [prompt (":" prompt)+] "]" ":" NUMBER (step_range_list | reverse_flag | step_range_list reverse_flag)?
reverse_flag: "reverse" | "r"
step_range_list: step_range ("," step_range)*
step_range: NUMBER "-" NUMBER | NUMBER "%" "-" NUMBER "%"
alternate: """ + _alt_rule + r"""
!alternate_distinct: "[" prompt ("|" prompt)* "]!"
alternate1: (prompt) "|" (prompt)+
alternate2: (plain | compound) ("|" (plain | compound))+
grouped: "{" ((NUMBER_Q | prompt | sequence | grouped) ("," | "|")?)+ "}"
top_level_sequence: prompt ("::" sequence)+ "!!" ("," plain)?
sequence: prompt "::" prompt ("," | WHITESPACE)* nested_sequence* ("!" | ";")
nested_sequence: "::" prompt ("," | WHITESPACE)* ("!" | ";" | "~")
compound: /[a-zA-Z0-9]+(_[a-zA-Z0-9]+)+/
numbered: NUMBER_Q ("!" | "_")? (grouped | sequence | compound | and_rule | plain | alternate | alternate_distinct | alternate1 | alternate2)
and_rule: (plain | compound) ("&" (plain | compound))+
WHITESPACE: /\s+/
plain: /([^\\\[\]()&]|\\.)+/
%import common.SIGNED_NUMBER -> NUMBER
%import common.INT -> NUMBER_Q
"""
schedule_parser = lark.Lark(_grammar)
@lru_cache(maxsize=CACHE_SIZE)
def hash_tree(tree: lark.Tree | lark.Token) -> str:
if isinstance(tree, lark.Tree):
return hashlib.md5((tree.data + ''.join(hash_tree(c) for c in tree.children)).encode()).hexdigest()
return hashlib.md5(str(tree).encode()).hexdigest()
def resolve_tree(tree: lark.Tree | lark.Token, keep_spacing: bool = True) -> str:
if isinstance(tree, lark.Tree):
children = []
for child in tree.children:
if isinstance(child, lark.Token) and child.type == "WHITESPACE":
if keep_spacing:
children.append(" ")
continue
children.append(resolve_tree(child, keep_spacing))
result = "".join(str(c) for c in children if c)
return re.sub(r"[\s\u2028\u2029]+", " ", result).strip() if keep_spacing else result.strip()
return str(tree).strip()
class ScheduleTransformer(lark.Transformer):
def __init__(self, total_steps: int, current_step: int = 1, seed: int | None = 42):
super().__init__()
self.total_steps = total_steps
self.current_step = current_step
self.seed = seed
self.rng = random.Random(seed) if seed is not None else random
def start(self, args):
return "".join(str(arg) for arg in args if arg)
def prompt(self, args):
return "".join(str(arg) for arg in args if arg)
def plain(self, args):
return args[0].value
def compound(self, args):
return "_".join(str(arg) for arg in args)
def and_rule(self, args):
return " and ".join(resolve_tree(arg, keep_spacing=True) for arg in args if resolve_tree(arg))
def grouped(self, args):
return ", ".join(resolve_tree(arg, keep_spacing=True) for arg in args if resolve_tree(arg).strip(" ,|"))
def alternate(self, args):
vals = []
for arg in args:
s = resolve_tree(arg, keep_spacing=True)
if s or s == "":
vals.append(s)
return vals[(self.current_step - 1) % len(vals)] if vals else "empty_prompt"
def alternate_distinct(self, args):
options = [resolve_tree(arg, keep_spacing=True) for arg in args if resolve_tree(arg)]
return self.rng.choice(options) if options else "empty_prompt"
def alternate1(self, args):
options = [resolve_tree(arg, keep_spacing=True) for arg in args if resolve_tree(arg)]
return self.rng.choice(options) if options else "empty_prompt"
def alternate2(self, args):
options = [resolve_tree(arg, keep_spacing=True) for arg in args if resolve_tree(arg)]
return self.rng.choice(options) if options else "empty_prompt"
def numbered(self, args):
quantity = int(args[0])
distinct = False
if len(args) > 1:
mark = str(args[1])
distinct = mark in ("!", "_")
target = args[-1]
options = []
if isinstance(target, lark.Tree) and target.data in ("alternate", "alternate1", "alternate2"):
for child in target.children:
val = self.visit(child)
if val:
options.append(val)
elif isinstance(target, lark.Token):
options = [resolve_tree(target, keep_spacing=True)]
else:
for child in target.children:
val = self.visit(child)
if val:
options.append(val)
if not options:
return "empty_prompt"
if distinct:
if quantity >= len(options):
unique = self.rng.sample(options, len(options)) if options else []
pad = self.rng.choices(options, k=quantity - len(unique)) if quantity > len(unique) else []
selected = unique + pad
else:
selected = self.rng.sample(options, quantity)
else:
selected = self.rng.choices(options, k=quantity)
return ", ".join(selected)
def sequence(self, args, parent=None):
owner = resolve_tree(args[0], keep_spacing=True) if parent is None else parent
descriptors = [resolve_tree(arg, keep_spacing=True).strip(" ,~!;") for arg in args[1:] if resolve_tree(arg).strip(" ,~!;")]
return f"{owner}: {', '.join(descriptors)}"
def top_level_sequence(self, args):
owner = resolve_tree(args[0], keep_spacing=True).strip()
sequences = []
trailing_text = []
for child in args[1:]:
if isinstance(child, lark.Tree) and child.data == "sequence":
sequences.append(self.sequence(child.children, owner))
elif isinstance(child, str) and child.strip() == "!!":
continue
else:
t = resolve_tree(child, keep_spacing=True).strip(" ,")
if t:
trailing_text.append(t)
text = f"{owner} -> {', '.join(sequences)}"
if trailing_text:
text += f", {', '.join(trailing_text)}"
return text
def nested_sequence(self, args):
elements = [resolve_tree(arg, keep_spacing=True).strip(" ,~!;") for arg in args[:-1] if resolve_tree(arg).strip(" ,~!;")]
terminator = args[-1] if args and isinstance(args[-1], str) else None
if terminator == "~":
return self.rng.choice(elements) if elements else "empty_prompt"
return f"[{' | '.join(elements)}]"
def emphasized(self, args):
prompt = args[0]
if len(args) > 1:
if isinstance(args[1], lark.Token) and args[1].type == "NUMBER":
weight = float(args[1].value)
return f"({prompt}:{weight})"
else:
second = args[1]
return f"({prompt}:{second})"
else:
return f"({prompt}:1.1)"
def scheduled(self, args):
prompts = [arg for arg in args[:-1] if not isinstance(arg, lark.Token) or arg.type != "NUMBER"]
number_node = args[-1]
if isinstance(number_node, lark.Tree):
number_node = resolve_tree(number_node, keep_spacing=True)
try:
weight = float(number_node)
except ValueError:
weight = 1.0
boundary = int(weight * self.total_steps) if weight <= 1.0 else int(weight)
boundary = max(1, min(boundary, self.total_steps))
if not prompts:
return "empty_prompt"
if len(prompts) == 1:
return f"({resolve_tree(prompts[0], keep_spacing=True)}:{weight})" if self.current_step >= boundary else ""
step_increment = boundary / max(1, len(prompts))
for i, prompt in enumerate(prompts):
step = min(self.total_steps, int(i * step_increment)) if i < len(prompts) - 1 else self.total_steps
if self.current_step <= step:
return f"({resolve_tree(prompt, keep_spacing=True)}:{weight})"
return f"({resolve_tree(prompts[-1], keep_spacing=True)}:{weight})"
class CollectSteps(lark.Visitor):
def __init__(self, steps, prefix="", suffix="", depth=0, use_scheduling=True, seed=None):
super().__init__()
self.steps = steps
self.prefix = prefix
self.suffix = suffix
self.depth = depth
self.use_scheduling = use_scheduling
self.seed = seed
self.rng = random.Random(seed) if seed is not None else random
self.schedules = []
def visit(self, tree):
if isinstance(tree, lark.Tree):
method_name = f"visit_{tree.data}"
method = getattr(self, method_name, self._default_visit)
return method(tree)
elif isinstance(tree, lark.Token):
return self._visit_token(tree)
return []
def _default_visit(self, tree):
schedules = []
for i, child in enumerate(tree.children):
if isinstance(child, lark.Token) and child.type == "WHITESPACE":
continue
pre = "".join(resolve_tree(c, keep_spacing=True) for j, c in enumerate(tree.children) if j < i and not (isinstance(c, lark.Token) and c.type == "WHITESPACE"))
post = "".join(resolve_tree(c, keep_spacing=True) for j, c in enumerate(tree.children) if j > i and not (isinstance(c, lark.Token) and c.type == "WHITESPACE"))
collector = CollectSteps(self.steps, prefix=self.prefix + pre, suffix=post + self.suffix, depth=self.depth + 1, use_scheduling=self.use_scheduling, seed=self.seed)
child_schedules = collector.visit(child)
schedules.extend(child_schedules)
return schedules
def _visit_token(self, token):
if token.type == "WHITESPACE":
return []
return [[self.steps, self.prefix + str(token) + self.suffix]]
def visit_plain(self, tree):
text = resolve_tree(tree, keep_spacing=True)
return [[self.steps, self.prefix + text + self.suffix]]
def visit_top_level_sequence(self, tree):
transformer = ScheduleTransformer(self.steps, 1, self.seed)
text = transformer.transform(tree)
return [[self.steps, self.prefix + text + self.suffix]]
def visit_scheduled(self, tree):
if not tree.children:
return [[self.steps, self.prefix + "empty_prompt" + self.suffix]]
prompts = [
p for p in tree.children
if not (isinstance(p, lark.Token) and p.type == "NUMBER")
and not (isinstance(p, lark.Tree) and p.data in ("step_range_list", "reverse_flag"))
]
number_node = next((p for p in tree.children if isinstance(p, lark.Token) and p.type == "NUMBER"), None)
step_range_list = next((p for p in tree.children if isinstance(p, lark.Tree) and p.data == "step_range_list"), None)
is_reverse = any(isinstance(p, lark.Tree) and p.data == "reverse_flag" for p in tree.children)
weight = float(number_node.value) if number_node else 1.0
def _clamp_step(x: int) -> int:
return max(1, min(x, self.steps))
step_intervals = []
explicit_ranges = False
if step_range_list:
explicit_ranges = True
for sr in step_range_list.children:
if not (isinstance(sr, lark.Tree) and sr.data == "step_range"):
continue
if len(sr.children) != 2:
continue
start_txt = resolve_tree(sr.children[0], keep_spacing=False)
end_txt = resolve_tree(sr.children[1], keep_spacing=False)
def _to_steps(txt: str) -> int:
s = txt.strip()
if s.endswith("%"):
try:
return int(round(float(s[:-1]) / 100.0 * self.steps))
except ValueError:
return 1
try:
return int(round(float(s)))
except ValueError:
return 1
start_step = _clamp_step(_to_steps(start_txt))
end_step = _clamp_step(_to_steps(end_txt))
if start_step < end_step:
step_intervals.append((start_step, end_step))
else:
num_prompts = len(prompts)
boundary = _clamp_step(int(round(weight * self.steps)) if weight <= 1.0 else int(round(weight)))
if num_prompts == 1:
schedules = []
schedules.append([boundary - 1, self.prefix + self.suffix])
last_text = resolve_tree(prompts[0], keep_spacing=True)
schedules.append([self.steps, self.prefix + last_text + self.suffix])
return schedules
if boundary < num_prompts:
boundary = num_prompts
step_size = boundary / num_prompts
for i in range(num_prompts):
start = _clamp_step(int(round(i * step_size)) + 1)
end = _clamp_step(int(round((i + 1) * step_size)))
if start < end:
step_intervals.append((start, end))
if is_reverse:
prompts = prompts[::-1]
step_intervals = step_intervals[::-1]
schedules = []
if step_intervals and step_intervals[0][0] > 1:
schedules.append([step_intervals[0][0] - 1, self.prefix + self.suffix])
for i, (start, end) in enumerate(step_intervals):
end = min(end, self.steps)
if start < end:
p = prompts[i]
if isinstance(p, lark.Tree):
child_schedules = self.visit(p)
else:
text = resolve_tree(p, keep_spacing=True)
child_schedules = [[self.steps, text]]
for sched in child_schedules:
schedules.append([end, self.prefix + sched[1] + self.suffix])
if step_intervals and step_intervals[-1][1] < self.steps:
tail_text = resolve_tree(prompts[-1], keep_spacing=True)
schedules.append([self.steps, self.prefix + tail_text + self.suffix])
if not schedules:
return [[self.steps, self.prefix + resolve_tree(tree, keep_spacing=True) + self.suffix]]
return schedules
def visit_alternate(self, tree):
options = []
for child in tree.children:
if isinstance(child, lark.Token) and child.type == "WHITESPACE":
continue
child_schedules = self.visit(child)
child_options = [
sched[1].strip(" ,|")
for sched in child_schedules
if sched[1].strip(" ,|")
]
options.append(
child_options
or [resolve_tree(child, keep_spacing=True).strip(" ,|")]
)
if not options:
return [[self.steps, self.prefix + "empty_prompt" + self.suffix]]
if EXPAND_ALTERNATE_PER_STEP:
schedules = []
for step in range(1, self.steps + 1):
option = options[(step - 1) % len(options)]
for sched in option:
schedules.append([step, self.prefix + sched + self.suffix])
return schedules
else:
group = options[self.rng.randrange(len(options))]
choice = self.rng.choice(group) if group else "empty_prompt"
return [[self.steps, self.prefix + choice + self.suffix]]
def visit_alternate_distinct(self, tree):
options = []
for child in tree.children:
if isinstance(child, lark.Token) and child.type == "WHITESPACE":
continue
child_schedules = self.visit(child)
child_options = [
sched[1].strip(" ,|")
for sched in child_schedules
if sched[1].strip(" ,|")
]
options.append(
child_options
or [resolve_tree(child, keep_spacing=True).strip(" ,|")]
)
flat = [opt for group in options for opt in group]
if not flat:
return [[self.steps, self.prefix + "empty_prompt" + self.suffix]]
selected = self.rng.choice(flat)
return [[self.steps, self.prefix + selected + self.suffix]]
def visit_alternate1(self, tree):
return self.visit_alternate_distinct(tree)
def visit_alternate2(self, tree):
options = [resolve_tree(c).strip() for c in tree.children]
combined_options = []
for option in options:
if "_" in option:
combined_options.append(option)
else:
suffix = options[0].split("_")[-1] if "_" in options[0] else ""
combined_options.append(f"{option}_{suffix}" if suffix else option)
return [[self.steps, self.prefix + "|".join(combined_options) + self.suffix]]
def visit_grouped(self, tree):
all_options = []
for child in tree.children:
if isinstance(child, lark.Token) and child.type == "WHITESPACE":
continue
child_schedules = self.visit(child)
child_options = [sched[1].strip(" ,|") for sched in child_schedules if sched[1].strip(" ,|")]
all_options.append(child_options or [resolve_tree(child, keep_spacing=True).strip(" ,|")])
out = []
for i, combo in enumerate(product(*all_options)):
if i >= GROUP_COMBO_LIMIT:
break
text = ", ".join(combo).strip()
if text:
out.append([self.steps, self.prefix + text + self.suffix])
return out or [[self.steps, self.prefix + "empty_prompt" + self.suffix]]
def visit_sequence(self, tree):
transformer = ScheduleTransformer(self.steps, 1, self.seed)
text = transformer.transform(tree)
return [[self.steps, self.prefix + text + self.suffix]]
def visit_nested_sequence(self, tree):
elements = [resolve_tree(child, keep_spacing=True).strip(" ,~!;") for child in tree.children[:-1] if resolve_tree(child).strip(" ,~!;")]
terminator = tree.children[-1].value if tree.children and isinstance(tree.children[-1], lark.Token) else None
if terminator == "~":
text = self.rng.choice(elements) if elements else "empty_prompt"
else:
text = f"[{' | '.join(elements)}]"
return [[self.steps, self.prefix + text + self.suffix]]
def visit_numbered(self, tree):
quantity = int(tree.children[0])
distinct = False
if len(tree.children) > 1:
mark = str(tree.children[1])
distinct = mark in ("!", "_")
target = tree.children[-1]
child_schedules = self.visit(target)
options = [
sched[1].strip(" ,|")
for sched in child_schedules
if sched[1].strip(" ,|")
]
if not options:
options = [resolve_tree(target, keep_spacing=True).strip(" ,|")]
if not options:
return [[self.steps, self.prefix + "empty_prompt" + self.suffix]]
if distinct:
if quantity >= len(options):
unique = self.rng.sample(options, len(options)) if options else []
pad = self.rng.choices(options, k=quantity - len(unique)) if quantity > len(unique) else []
selected = unique + pad
else:
selected = self.rng.sample(options, quantity)
else:
selected = self.rng.choices(options, k=quantity)
return [[self.steps, self.prefix + ", ".join(selected) + self.suffix]]
def visit_and_rule(self, tree):
text = " and ".join(resolve_tree(c, keep_spacing=True) for c in tree.children if resolve_tree(c, keep_spacing=True))
return [[self.steps, self.prefix + text + self.suffix]]
def visit_emphasized(self, tree):
prompt = resolve_tree(tree.children[0], keep_spacing=True)
if len(tree.children) > 1:
if isinstance(tree.children[1], lark.Token) and tree.children[1].type == "NUMBER":
weight = float(tree.children[1].value)
text = f"({prompt}:{weight})"
else:
second = resolve_tree(tree.children[1], keep_spacing=True)
text = f"({prompt}:{second})"
else:
text = f"({prompt}:1.1)"
return [[self.steps, self.prefix + text + self.suffix]]
def __call__(self, tree):
self.schedules = self.visit(tree)
return self.schedules or [[self.steps, self.prefix + resolve_tree(tree, keep_spacing=True) + self.suffix]]
def at_step_from_schedule(step, schedule):
if not schedule:
return ""
for end_step, text in schedule:
if step <= int(end_step):
return text
return schedule[-1][1]
def at_step(step: int, prompt_or_schedule, *, steps: int | None = None,
seed: int | None = 42, use_visitor: bool = True) -> str:
if isinstance(prompt_or_schedule, list) and prompt_or_schedule and isinstance(prompt_or_schedule[0], list):
return at_step_from_schedule(step, prompt_or_schedule)
prompt = str(prompt_or_schedule)
if steps is None:
raise ValueError("steps is required when passing a prompt string to at_step(...)")
sched = get_schedule(prompt, steps, True, seed, use_visitor)
return at_step_from_schedule(step, sched)
@lru_cache(maxsize=CACHE_SIZE)
def get_schedule(prompt: str, steps: int, use_scheduling: bool, seed: int | None, use_visitor: bool = True):
try:
tree = schedule_parser.parse(prompt)
except lark.exceptions.LarkError as e:
logger.warning("Prompt parse failed: '%s' — %s", prompt, e)
return [[steps, prompt]]
collector = CollectSteps(steps, use_scheduling=use_scheduling, seed=seed)
schedules = collector(tree)
if not use_visitor:
rebuilt = []
for end, _ in schedules:
transformer = ScheduleTransformer(steps, end, seed)
text = transformer.transform(tree)
rebuilt.append([end, text])
return rebuilt
return schedules
def analyze_prompt(input_prompt, steps, seed, use_visitor, output_mode, specific_step):
tree_str = ""
schedule_str = ""
result = ""
error_msg = ""
try:
tree = schedule_parser.parse(input_prompt)
tree_str = tree.pretty()
except lark.exceptions.LarkError as e:
error_msg = f"Parse Error: {str(e)}\n\nPossible causes:\n- Unbalanced brackets or invalid syntax.\n- Missing colons, commas, or incorrect operators.\n- Suggestion: Validate grammar rules for scheduled, alternate, grouped, sequences, etc.\n- Ensure no invalid characters in plain text.\nTraceback: {traceback.format_exc()}"
return result, tree_str, schedule_str, error_msg
try:
schedule = get_schedule(input_prompt, steps, True, seed, use_visitor)
schedule_str = "\n".join([f"Up to step {end}: {text}" for end, text in schedule])
except Exception as e:
error_msg = f"Schedule Error: {str(e)}\nTraceback: {traceback.format_exc()}"
return result, tree_str, schedule_str, error_msg
if output_mode == "Full Schedule":
result = schedule_str
elif output_mode == "Parse Tree":
result = tree_str
elif output_mode == "At Specific Step":
result = at_step_from_schedule(specific_step, schedule)
return result, tree_str, schedule_str, error_msg
# Helper functions for converting prompt to specific formats
def convert_to_grouped(input_prompt):
return f"{{{input_prompt}}}"
def convert_to_sequence(input_prompt):
parts = [p.strip() for p in input_prompt.split(',')]
if len(parts) < 2:
return input_prompt + "::descriptor!"
owner = parts[0]
descriptors = ', '.join(parts[1:])
return f"{owner}::{descriptors}!"
def convert_to_numbered(input_prompt, num, distinct):
mark = "!" if distinct else ""
return f"{num}{mark} {{{input_prompt}}}"
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.Tab("Analyze Prompt"):
input_prompt = gr.Textbox(label="Input Prompt", lines=5)
steps = gr.Number(label="Steps", value=100, precision=0)
seed = gr.Number(label="Seed (for random elements)", value=42, precision=0)
use_visitor = gr.Checkbox(label="Use Visitor Mode (faster for simple prompts)", value=True)
output_mode = gr.Dropdown(choices=["Full Schedule", "Parse Tree", "At Specific Step"], label="Output Mode", value="Full Schedule")
specific_step = gr.Number(label="Specific Step (for At Specific Step mode)", value=50, precision=0)
analyze_btn = gr.Button("Analyze Prompt")
output_result = gr.Textbox(label="Analysis Result", lines=10)
output_tree = gr.Textbox(label="Parse Tree (Debug)", lines=5)
output_schedule = gr.Textbox(label="Full Schedule (Debug)", lines=5)
error_output = gr.Textbox(label="Errors & Suggestions", lines=5)
analyze_btn.click(
fn=analyze_prompt,
inputs=[input_prompt, steps, seed, use_visitor, output_mode, specific_step],
outputs=[output_result, output_tree, output_schedule, error_output]
)
with gr.Tab("Format Converters"):
gr.HTML(value="<p>These helpers convert plain prompts to advanced formats. Read the explanations for usage.</p>")
with gr.Accordion("Grouped {} - For combinations of attributes"):
gr.HTML(value="<p>Wraps items in {}, generates combinations if | (alternates) inside. <br>Example: {red|blue, car|bike} resolves to 'red, car', 'red, bike', 'blue, car', 'blue, bike'. <br>Use for multiple independent attributes like colors, objects. Limit combos with GROUP_COMBO_LIMIT env var.</p>")
conv_group_input = gr.Textbox(label="Input for Grouped (e.g., red|blue, car|bike)", lines=2)
conv_group_output = gr.Textbox(label="Converted Grouped Prompt", lines=2)
conv_group_btn = gr.Button("Convert to Grouped")
conv_group_btn.click(convert_to_grouped, inputs=conv_group_input, outputs=conv_group_output)
with gr.Accordion("Sequence :: - For structured descriptions"):
gr.HTML(value="<p>Structures as owner::descriptors!. Nested with :: and ~ for random or ! for close. <br>Example: character::hair:ponytail, eyes:blue! -> 'character: hair ponytail, eyes blue'. <br>Use for hierarchical entities like characters, outfits. Close with ! or ;. Top-level with !!! for groups.</p>")
conv_seq_input = gr.Textbox(label="Input for Sequence (e.g., character, hair:ponytail, eyes:blue)", lines=2)
conv_seq_output = gr.Textbox(label="Converted Sequence Prompt", lines=2)
conv_seq_btn = gr.Button("Convert to Sequence")
conv_seq_btn.click(convert_to_sequence, inputs=conv_seq_input, outputs=conv_seq_output)
with gr.Accordion("Numbered N! or N_ - For repeats"):
gr.HTML(value="<p>N{group} repeats N times, ! or _ for distinct (no repeats). <br>Example: 3! {a|b|c} -> 'a, b, c' (unique sample). <br>Use for multiples like '3 cats'. If N > options, pads with repeats unless distinct.</p>")
conv_num_input = gr.Textbox(label="Input for Numbered (e.g., a|b|c)", lines=2)
conv_num_num = gr.Number(label="Number (N)", value=3, precision=0)
conv_num_distinct = gr.Checkbox(label="Distinct (! or _)", value=True)
conv_num_output = gr.Textbox(label="Converted Numbered Prompt", lines=2)
conv_num_btn = gr.Button("Convert to Numbered")
conv_num_btn.click(convert_to_numbered, inputs=[conv_num_input, conv_num_num, conv_num_distinct], outputs=conv_num_output)
return [input_prompt, steps, seed, use_visitor, output_mode, specific_step, analyze_btn, output_result]
def on_ui_settings():
section = ('prompt-parser-analyzer', "Prompt Parser Analyzer")
script_callbacks.on_ui_settings(on_ui_settings)