Forge Neo moved parse_prompt_attention from modules.prompt_parser to backend.text_processing.parsing, causing dtg.py and tipo.py to fail during script loading. Keep the existing import as the primary path and fall back only when it raises ImportError. This preserves the current path for Original Forge and reForge while restoring script loading on Forge Neo 2.29.1. Verified GGUF prompt generation and image generation on Forge Neo after the fallback was applied.
942 lines
36 KiB
Python
942 lines
36 KiB
Python
import ast
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import random
|
|
import re
|
|
import sys
|
|
|
|
import gradio as gr
|
|
import torch
|
|
from modules import devices, infotext_utils, options, scripts, shared, ui
|
|
from modules.extra_networks import parse_prompt
|
|
from modules.processing import (
|
|
StableDiffusionProcessingImg2Img,
|
|
StableDiffusionProcessingTxt2Img,
|
|
fix_seed,
|
|
)
|
|
try:
|
|
from modules.prompt_parser import parse_prompt_attention
|
|
except ImportError:
|
|
from backend.text_processing.parsing import parse_prompt_attention
|
|
from modules.scripts import OnComponent, basedir
|
|
from modules.shared import opts
|
|
from modules.ui_components import ToolButton
|
|
|
|
# Try to import InputAccordion (WebUI >= 1.7)
|
|
try:
|
|
from modules.ui_components import InputAccordion
|
|
except ImportError:
|
|
InputAccordion = None
|
|
|
|
from kgen import models
|
|
from kgen.executor import tipo
|
|
from kgen.executor.tipo import (
|
|
parse_tipo_request,
|
|
tipo_runner,
|
|
)
|
|
from kgen.formatter import apply_format, seperate_tags
|
|
from kgen.logging import logger
|
|
from kgen.metainfo import TIPO_DEFAULT_FORMAT
|
|
|
|
ext_dir = basedir()
|
|
models.model_dir = pathlib.Path(ext_dir) / "models"
|
|
|
|
if ext_dir not in sys.path:
|
|
sys.path.insert(0, ext_dir)
|
|
|
|
from tipo_installer import ensure_runtime
|
|
|
|
SEED_MAX = 2**31 - 1
|
|
QUOTESWAP = str.maketrans("'\"", "\"'")
|
|
TOTAL_TAG_LENGTH = {
|
|
"VERY_SHORT": "very short",
|
|
"SHORT": "short",
|
|
"LONG": "long",
|
|
"VERY_LONG": "very long",
|
|
}
|
|
PROCESSING_TIMING = {
|
|
"BEFORE": "Before applying other prompt processings",
|
|
"AFTER": "After applying other prompt processings",
|
|
}
|
|
EXTRA_INPUT_LAYOUT = ["Side by side", "Stacked"]
|
|
DEFAULT_FORMAT = """<|special|>,
|
|
<|characters|>, <|copyrights|>,
|
|
<|artist|>,
|
|
|
|
<|general|>,
|
|
|
|
<|quality|>, <|meta|>, <|rating|>"""
|
|
TIMING_INFO_TEMPLATE = (
|
|
"_Prompt upsampling will be applied to {} "
|
|
"sd-dynamic-promps and the webui's styles feature are applied_"
|
|
)
|
|
INFOTEXT_KEY = "TIPO Parameters"
|
|
INFOTEXT_KEY_PROMPT = "TIPO prompt"
|
|
INFOTEXT_NL_PROMPT = "TIPO nl prompt"
|
|
INFOTEXT_KEY_FORMAT = "TIPO format"
|
|
|
|
PROMPT_INDICATE_HTML = """
|
|
<div style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center">
|
|
<span>
|
|
Original Prompt Loaded.<br>
|
|
Click "Apply" to apply the original prompt.
|
|
</span>
|
|
</div>
|
|
"""
|
|
RECOMMEND_MARKDOWN = """
|
|
### Recommended Model and Settings:
|
|
|
|
"""
|
|
MODEL_NAME_LIST = [
|
|
f"{model_name} | {file}"
|
|
for model_name, ggufs in models.tipo_model_list
|
|
for file in ggufs
|
|
] + [i[0] for i in models.tipo_model_list]
|
|
|
|
|
|
def on_process_timing_dropdown_changed(timing: str):
|
|
info = ""
|
|
if timing == PROCESSING_TIMING["BEFORE"]:
|
|
info = "**only the first image in batch**, **before**"
|
|
elif timing == PROCESSING_TIMING["AFTER"]:
|
|
info = "**all images in batch**, **after**"
|
|
else:
|
|
raise ValueError(f"Unknown timing: {timing}")
|
|
return TIMING_INFO_TEMPLATE.format(info)
|
|
|
|
|
|
def apply_strength(tag_map, strength_map, strength_map_nl, break_map):
|
|
for cate in tag_map:
|
|
new_list = []
|
|
# Skip natural language output at first
|
|
if isinstance(tag_map[cate], str):
|
|
# Ensure all the parts in the strength_map are in the prompt
|
|
if all(part in tag_map[cate] for part, strength in strength_map_nl):
|
|
org_prompt = tag_map[cate]
|
|
new_prompt = ""
|
|
for part, strength in strength_map_nl:
|
|
before, org_prompt = org_prompt.split(part, 1)
|
|
new_prompt += before.replace("(", "\\(").replace(")", "\\)")
|
|
part = part.replace("(", "\\(").replace(")", "\\)")
|
|
new_prompt += f"({part}:{strength})"
|
|
new_prompt += org_prompt
|
|
tag_map[cate] = new_prompt
|
|
continue
|
|
for org_tag in tag_map[cate]:
|
|
tag = org_tag.replace("(", "\\(").replace(")", "\\)")
|
|
if org_tag in strength_map:
|
|
new_list.append(f"({tag}:{strength_map[org_tag]})")
|
|
else:
|
|
new_list.append(tag)
|
|
if tag in break_map or org_tag in break_map:
|
|
new_list.append("BREAK")
|
|
tag_map[cate] = new_list
|
|
|
|
return tag_map
|
|
|
|
|
|
COMMENT_LINE = re.compile(r"^[^\S\n]*#[^\n]*(?:\n|$)", re.MULTILINE)
|
|
DANGLING = re.compile(r"(?:^[\s,]+|[\s,]+$)")
|
|
REPEATED_COMMA = re.compile(r",\s*(?=,)")
|
|
|
|
|
|
def strip_comments(text: str) -> str:
|
|
"""Drop lines whose first non-space character is '#' (issue #22)."""
|
|
if not text or "#" not in text:
|
|
return text
|
|
cleaned = REPEATED_COMMA.sub("", COMMENT_LINE.sub("", text))
|
|
return DANGLING.sub("", cleaned)
|
|
|
|
|
|
def embedding_names() -> set:
|
|
try:
|
|
from modules.sd_hijack import model_hijack
|
|
|
|
return {str(name).lower() for name in model_hijack.embedding_db.word_embeddings}
|
|
except Exception:
|
|
return set()
|
|
|
|
|
|
def embedding_spellings(tags) -> dict:
|
|
"""Map kgen's spaced form back to the original, for embeddings only.
|
|
|
|
seperate_tags spaces out underscores for any token no tag list claims. That
|
|
is correct for general tags, but an embedding name looks exactly like a tag,
|
|
so ask the webui which names are loaded and protect only those (issue #119).
|
|
"""
|
|
known = embedding_names()
|
|
if not known:
|
|
return {}
|
|
return {
|
|
tag.replace("_", " "): tag
|
|
for tag in tags
|
|
if len(tag) >= 4 and "_" in tag and tag.lower() in known
|
|
}
|
|
|
|
|
|
def restore_embeddings(tag_map, mapping):
|
|
if not mapping:
|
|
return tag_map
|
|
for cate, value in tag_map.items():
|
|
if isinstance(value, list):
|
|
tag_map[cate] = [mapping.get(tag, tag) for tag in value]
|
|
elif isinstance(value, str):
|
|
for spaced, original in mapping.items():
|
|
value = value.replace(spaced, original)
|
|
tag_map[cate] = value
|
|
return tag_map
|
|
|
|
|
|
class TIPOScript(scripts.Script):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prompt_area = [None, None, None, None]
|
|
self.tag_prompt_area = [None, None]
|
|
self.prompt_area_row = [None, None]
|
|
self.tipo_accordion_row = [None, None]
|
|
self.current_model = None
|
|
self.generation_info = [None, None] # For txt2img and img2img
|
|
self.seed_reuse_btn = [None, None] # Store reuse buttons
|
|
self.seed_num_input = [None, None] # Store seed inputs
|
|
self.on_after_component_elem_id = [
|
|
("txt2img_prompt_row", lambda x: self.create_new_prompt_area(0, x)),
|
|
("txt2img_prompt", lambda x: self.set_prompt_area(0, x)),
|
|
("img2img_prompt_row", lambda x: self.create_new_prompt_area(1, x)),
|
|
("img2img_prompt", lambda x: self.set_prompt_area(1, x)),
|
|
("generation_info_txt2img", lambda x: self.set_generation_info(0, x)),
|
|
("generation_info_img2img", lambda x: self.set_generation_info(1, x)),
|
|
]
|
|
|
|
def create_new_prompt_area(self, i2i: int, prompt_row: OnComponent):
|
|
stacked = getattr(opts, "tipo_extra_input_layout", EXTRA_INPUT_LAYOUT[0])
|
|
lines = 2 if stacked == EXTRA_INPUT_LAYOUT[1] else 3
|
|
tag_kwargs = {
|
|
"label": "Tag Prompt",
|
|
"lines": lines,
|
|
"placeholder": "Tag Prompt for TIPO (Put Tags to Prompt region)",
|
|
}
|
|
nl_kwargs = {
|
|
"label": "Natural Language Prompt",
|
|
"lines": lines,
|
|
"placeholder": "Natural Language Prompt for TIPO (Put Tags to Prompt region)",
|
|
}
|
|
|
|
with gr.Row(visible=not opts.tipo_no_extra_input):
|
|
if stacked == EXTRA_INPUT_LAYOUT[1]:
|
|
# One full-width column keeps the main prompt at full width
|
|
# instead of splitting the row in half (issue #47).
|
|
with gr.Column(scale=1):
|
|
new_tag_prompt_area = gr.Textbox(**tag_kwargs)
|
|
new_prompt_area = gr.Textbox(**nl_kwargs)
|
|
else:
|
|
with gr.Column(scale=1):
|
|
new_tag_prompt_area = gr.Textbox(**tag_kwargs)
|
|
with gr.Column(scale=1):
|
|
new_prompt_area = gr.Textbox(**nl_kwargs)
|
|
|
|
# Create second row: Generate Prompt button (below the two input areas)
|
|
self.prompt_area_row[i2i] = gr.Row()
|
|
# Create third row: TIPO accordion
|
|
self.tipo_accordion_row[i2i] = gr.Row()
|
|
|
|
self.tag_prompt_area[i2i] = new_tag_prompt_area
|
|
self.prompt_area[i2i * 2 + 1] = new_prompt_area
|
|
|
|
def set_prompt_area(self, i2i: int, component: OnComponent):
|
|
self.prompt_area[i2i * 2] = component.component
|
|
|
|
def set_generation_info(self, i2i: int, component: OnComponent):
|
|
"""Store generation_info component reference and connect reuse seed button"""
|
|
self.generation_info[i2i] = component.component
|
|
|
|
# Connect reuse seed button if both components are available
|
|
if (
|
|
self.seed_reuse_btn[i2i] is not None
|
|
and self.seed_num_input[i2i] is not None
|
|
):
|
|
self.connect_reuse_seed_button(i2i)
|
|
|
|
def connect_reuse_seed_button(self, i2i: int):
|
|
"""Connect the reuse seed button to retrieve TIPO seed from generation info"""
|
|
if self.generation_info[i2i] is None:
|
|
return
|
|
|
|
self.seed_reuse_btn[i2i].click(
|
|
fn=self.get_tipo_seed_from_generation_info,
|
|
_js="(x, y) => [x, selected_gallery_index()]",
|
|
show_progress=False,
|
|
inputs=[self.generation_info[i2i]],
|
|
outputs=[self.seed_num_input[i2i]],
|
|
)
|
|
|
|
def title(self):
|
|
return "TIPO"
|
|
|
|
def show(self, _):
|
|
return scripts.AlwaysVisible
|
|
|
|
def ui(self, is_img2img):
|
|
with self.prompt_area_row[is_img2img]:
|
|
with gr.Column(
|
|
scale=1, min_width=180, visible=not opts.tipo_no_extra_input
|
|
):
|
|
prompt_gen = gr.Button(value="Generate Prompt")
|
|
with gr.Column(scale=6):
|
|
# Create accordion with enable checkbox on the label
|
|
tab_name = "img2img" if is_img2img else "txt2img"
|
|
|
|
if InputAccordion is not None:
|
|
# WebUI >= 1.7: Use InputAccordion (checkbox on accordion label)
|
|
tipo_acc = InputAccordion(
|
|
value=False,
|
|
label=self.title(),
|
|
elem_id=f"{tab_name}_tipo_accordion",
|
|
visible=True,
|
|
)
|
|
enabled_check = tipo_acc
|
|
else:
|
|
# WebUI < 1.7: Use regular Accordion
|
|
tipo_acc = gr.Accordion(
|
|
open=False,
|
|
label=self.title(),
|
|
elem_id=f"{tab_name}_tipo_accordion",
|
|
)
|
|
enabled_check = None # Will be created inside
|
|
|
|
with tipo_acc:
|
|
# For older WebUI versions, add enable checkbox inside
|
|
if InputAccordion is None:
|
|
enabled_check = gr.Checkbox(
|
|
label="Enabled",
|
|
value=False,
|
|
elem_id=f"{tab_name}_tipo_enabled",
|
|
)
|
|
|
|
with gr.Row():
|
|
with gr.Column():
|
|
with gr.Row():
|
|
with gr.Column(scale=1):
|
|
read_orig_prompt_btn = gr.Button(
|
|
size="sm",
|
|
value="Apply original prompt",
|
|
visible=False,
|
|
min_width=20,
|
|
)
|
|
with gr.Column(scale=3):
|
|
orig_prompt_area = gr.TextArea(visible=False)
|
|
orig_prompt_light = gr.HTML("")
|
|
orig_prompt_area.change(
|
|
lambda x: PROMPT_INDICATE_HTML * bool(x),
|
|
inputs=orig_prompt_area,
|
|
outputs=orig_prompt_light,
|
|
)
|
|
orig_prompt_area.change(
|
|
lambda x: gr.update(visible=bool(x)),
|
|
inputs=orig_prompt_area,
|
|
outputs=read_orig_prompt_btn,
|
|
)
|
|
read_orig_prompt_btn.click(
|
|
fn=lambda x: x,
|
|
inputs=[orig_prompt_area],
|
|
outputs=self.prompt_area[is_img2img],
|
|
)
|
|
|
|
tag_length_radio = gr.Radio(
|
|
label="Tags Length target",
|
|
choices=list(TOTAL_TAG_LENGTH.values()),
|
|
value=TOTAL_TAG_LENGTH["LONG"],
|
|
)
|
|
nl_length_radio = gr.Radio(
|
|
label="NL Length target",
|
|
choices=list(TOTAL_TAG_LENGTH.values()),
|
|
value=TOTAL_TAG_LENGTH["LONG"],
|
|
)
|
|
ban_tags_textbox = gr.Textbox(
|
|
label="Ban tags",
|
|
info="Separate with comma. Regex supported.",
|
|
value="",
|
|
placeholder="umbrella, official.*, .*text, ...",
|
|
)
|
|
format_dropdown = gr.Dropdown(
|
|
label="Prompt Format",
|
|
info="The format you want to apply to final prompt",
|
|
choices=list(TIPO_DEFAULT_FORMAT.keys()) + ["custom"],
|
|
value="Both, tag first (recommend)",
|
|
)
|
|
format_textarea = gr.TextArea(
|
|
value=TIPO_DEFAULT_FORMAT[
|
|
"Both, tag first (recommend)"
|
|
],
|
|
label="Custom Prompt Format",
|
|
visible=False,
|
|
placeholder="<|extended|>. <|general|>",
|
|
)
|
|
format_dropdown.change(
|
|
lambda x: gr.update(
|
|
visible=x == "custom",
|
|
value=TIPO_DEFAULT_FORMAT.get(
|
|
x, next(iter(TIPO_DEFAULT_FORMAT.values()))
|
|
),
|
|
),
|
|
inputs=format_dropdown,
|
|
outputs=format_textarea,
|
|
)
|
|
|
|
with gr.Group():
|
|
with gr.Row(elem_classes=["form"]):
|
|
seed_num_input = gr.Number(
|
|
label="Seed for upsampling tags",
|
|
minimum=-1,
|
|
step=1,
|
|
scale=4,
|
|
value=-1,
|
|
)
|
|
seed_random_btn = ToolButton(value=ui.random_symbol)
|
|
seed_reuse_btn = ToolButton(value=ui.reuse_symbol)
|
|
seed_shuffle_btn = gr.Button(
|
|
value="Shuffle", size="sm", scale=0
|
|
)
|
|
|
|
# Store references for later connection
|
|
self.seed_num_input[is_img2img] = seed_num_input
|
|
self.seed_reuse_btn[is_img2img] = seed_reuse_btn
|
|
|
|
seed_random_btn.click(
|
|
lambda: -1, outputs=[seed_num_input]
|
|
)
|
|
# Reuse button will be connected in set_generation_info
|
|
# when generation_info component becomes available
|
|
seed_shuffle_btn.click(
|
|
lambda: random.randint(0, 2**31 - 1),
|
|
outputs=[seed_num_input],
|
|
)
|
|
|
|
# Try to connect now if generation_info is already available
|
|
if self.generation_info[is_img2img] is not None:
|
|
self.connect_reuse_seed_button(is_img2img)
|
|
|
|
use_generation_seed = gr.Checkbox(
|
|
label="Follow the image generation seed",
|
|
info=(
|
|
"Upsample with the seed of the image being "
|
|
"generated, so the prompt tracks the image "
|
|
"instead of needing its own seed."
|
|
),
|
|
value=False,
|
|
)
|
|
|
|
with gr.Group():
|
|
process_timing_dropdown = gr.Dropdown(
|
|
label="Upsampling timing",
|
|
choices=list(PROCESSING_TIMING.values()),
|
|
value=PROCESSING_TIMING["AFTER"],
|
|
)
|
|
|
|
process_timing_md = gr.Markdown(
|
|
on_process_timing_dropdown_changed(
|
|
process_timing_dropdown.value
|
|
)
|
|
)
|
|
|
|
process_timing_dropdown.change(
|
|
on_process_timing_dropdown_changed,
|
|
inputs=[process_timing_dropdown],
|
|
outputs=[process_timing_md],
|
|
)
|
|
|
|
with gr.Column():
|
|
gr.Markdown(RECOMMEND_MARKDOWN)
|
|
model_dropdown = gr.Dropdown(
|
|
label="Model",
|
|
choices=MODEL_NAME_LIST,
|
|
value=MODEL_NAME_LIST[0],
|
|
)
|
|
gguf_use_cpu = gr.Checkbox(label="Use CPU (GGUF)")
|
|
no_formatting = gr.Checkbox(
|
|
label="No formatting", value=False
|
|
)
|
|
temperature_slider = gr.Slider(
|
|
label="Temperature",
|
|
info="← less random | more random →",
|
|
maximum=1.5,
|
|
minimum=0.1,
|
|
step=0.05,
|
|
value=0.5,
|
|
)
|
|
top_p_slider = gr.Slider(
|
|
label="Top-p",
|
|
info="← less unconfident tokens | more unconfident tokens →",
|
|
maximum=1,
|
|
minimum=0,
|
|
step=0.05,
|
|
value=0.95,
|
|
)
|
|
top_k_slider = gr.Slider(
|
|
label="Top-k",
|
|
info="← less unconfident tokens | more unconfident tokens →",
|
|
maximum=150,
|
|
minimum=0,
|
|
step=1,
|
|
value=80,
|
|
)
|
|
|
|
aspect_ratio_place_holder = gr.Number(value=1.0, visible=False)
|
|
|
|
prompt_gen.click(
|
|
self.prompt_gen_only,
|
|
inputs=[
|
|
self.tag_prompt_area[is_img2img],
|
|
self.prompt_area[is_img2img * 2 + 1],
|
|
aspect_ratio_place_holder,
|
|
seed_num_input,
|
|
tag_length_radio,
|
|
nl_length_radio,
|
|
ban_tags_textbox,
|
|
format_dropdown,
|
|
format_textarea,
|
|
temperature_slider,
|
|
top_p_slider,
|
|
top_k_slider,
|
|
model_dropdown,
|
|
gguf_use_cpu,
|
|
no_formatting,
|
|
self.tag_prompt_area[is_img2img],
|
|
],
|
|
outputs=[
|
|
self.prompt_area[is_img2img * 2],
|
|
],
|
|
)
|
|
|
|
self.infotext_fields = [
|
|
(
|
|
(tipo_acc, lambda d: gr.update(value=INFOTEXT_KEY in d))
|
|
if InputAccordion
|
|
else (tipo_acc, lambda d: gr.update(open=INFOTEXT_KEY in d))
|
|
),
|
|
(
|
|
self.prompt_area[is_img2img * 2],
|
|
lambda d: d.get(INFOTEXT_KEY_PROMPT, d["Prompt"]),
|
|
),
|
|
(
|
|
self.prompt_area[is_img2img * 2 + 1],
|
|
lambda d: d.get(INFOTEXT_NL_PROMPT, ""),
|
|
),
|
|
(orig_prompt_area, lambda d: d["Prompt"]),
|
|
(enabled_check, lambda d: INFOTEXT_KEY in d),
|
|
(seed_num_input, lambda d: self.get_infotext(d, "seed", None)),
|
|
(tag_length_radio, lambda d: self.get_infotext(d, "tag_length", None)),
|
|
(nl_length_radio, lambda d: self.get_infotext(d, "nl_length", None)),
|
|
(ban_tags_textbox, lambda d: self.get_infotext(d, "ban_tags", None)),
|
|
(format_dropdown, lambda d: self.get_infotext(d, "format", None)),
|
|
(format_textarea, lambda d: d.get(INFOTEXT_KEY_FORMAT, None)),
|
|
(
|
|
process_timing_dropdown,
|
|
lambda d: PROCESSING_TIMING.get(
|
|
self.get_infotext(d, "timing", None), None
|
|
),
|
|
),
|
|
(temperature_slider, lambda d: self.get_infotext(d, "temperature", None)),
|
|
(top_p_slider, lambda d: self.get_infotext(d, "top_p", None)),
|
|
(top_k_slider, lambda d: self.get_infotext(d, "top_k", None)),
|
|
(
|
|
model_dropdown,
|
|
lambda d: self.get_infotext(d, "model", None),
|
|
),
|
|
(gguf_use_cpu, lambda d: self.get_infotext(d, "gguf_cpu", None)),
|
|
(no_formatting, lambda d: self.get_infotext(d, "no_formatting", None)),
|
|
(
|
|
use_generation_seed,
|
|
lambda d: self.get_infotext(d, "follow_generation_seed", None),
|
|
),
|
|
]
|
|
|
|
# use_generation_seed sits right after the seed it overrides, so every
|
|
# later position keeps the index write_infotext and _process expect.
|
|
return [
|
|
enabled_check,
|
|
process_timing_dropdown,
|
|
seed_num_input,
|
|
use_generation_seed,
|
|
tag_length_radio,
|
|
nl_length_radio,
|
|
ban_tags_textbox,
|
|
format_dropdown,
|
|
format_textarea,
|
|
temperature_slider,
|
|
top_p_slider,
|
|
top_k_slider,
|
|
model_dropdown,
|
|
gguf_use_cpu,
|
|
no_formatting,
|
|
self.tag_prompt_area[is_img2img],
|
|
self.prompt_area[is_img2img * 2 + 1],
|
|
]
|
|
|
|
def get_tipo_seed_from_generation_info(self, gen_info_string: str, index: int = 0):
|
|
"""
|
|
Retrieves TIPO seed from generation_info JSON data.
|
|
For batch generation, retrieves the seed for the selected image using the index.
|
|
|
|
Args:
|
|
gen_info_string: JSON string of generation_info
|
|
index: Gallery index (default: 0) - which image in batch to get seed from
|
|
|
|
Returns:
|
|
int: TIPO seed value, or -1 if not found or on error
|
|
"""
|
|
res = -1
|
|
try:
|
|
gen_info = json.loads(gen_info_string)
|
|
|
|
# Get infotext for the specific image index
|
|
infotexts = gen_info.get("infotexts", [])
|
|
if index < len(infotexts):
|
|
infotext = infotexts[index]
|
|
|
|
# Parse generation parameters from infotext
|
|
gen_parameters = infotext_utils.parse_generation_parameters(
|
|
infotext, []
|
|
)
|
|
|
|
# Get TIPO Parameters string from parsed parameters
|
|
tipo_params_str = gen_parameters.get("TIPO Parameters", "")
|
|
|
|
if tipo_params_str:
|
|
# Convert JavaScript boolean literals to Python
|
|
dict_string = tipo_params_str.replace("false", "False").replace(
|
|
"true", "True"
|
|
)
|
|
|
|
# Parse as dictionary
|
|
tipo_params = ast.literal_eval(dict_string)
|
|
|
|
# Get seed value
|
|
res = int(tipo_params.get("seed", -1))
|
|
else:
|
|
# Fallback to extra_generation_params if index is out of range
|
|
extra_params = gen_info.get("extra_generation_params", {})
|
|
tipo_params_str = extra_params.get("TIPO Parameters", "")
|
|
|
|
if tipo_params_str:
|
|
dict_string = tipo_params_str.replace("false", "False").replace(
|
|
"true", "True"
|
|
)
|
|
tipo_params = ast.literal_eval(dict_string)
|
|
res = int(tipo_params.get("seed", -1))
|
|
|
|
except Exception as e:
|
|
if gen_info_string:
|
|
logger.warning(f"Error retrieving TIPO seed from generation info: {e}")
|
|
|
|
return res
|
|
|
|
def get_infotext(self, d, target, default):
|
|
return d.get(INFOTEXT_KEY, {}).get(target, default)
|
|
|
|
def write_infotext(
|
|
self,
|
|
p: StableDiffusionProcessingTxt2Img | StableDiffusionProcessingImg2Img,
|
|
prompt: str,
|
|
process_timing: str,
|
|
seed: int,
|
|
follow_generation_seed: bool,
|
|
*args,
|
|
):
|
|
p.extra_generation_params[INFOTEXT_KEY] = json.dumps(
|
|
{
|
|
"seed": seed,
|
|
"follow_generation_seed": follow_generation_seed,
|
|
"timing": process_timing,
|
|
"tag_length": args[0],
|
|
"nl_length": args[1],
|
|
"ban_tags": args[2],
|
|
"format_selected": args[3],
|
|
"format": args[4],
|
|
"temperature": args[5],
|
|
"top_p": args[6],
|
|
"top_k": args[7],
|
|
"model": args[8],
|
|
"gguf_cpu": args[9],
|
|
"no_formatting": args[10],
|
|
},
|
|
ensure_ascii=False,
|
|
).translate(QUOTESWAP)
|
|
p.extra_generation_params[INFOTEXT_KEY_PROMPT] = prompt.strip() or args[-1]
|
|
p.extra_generation_params[INFOTEXT_NL_PROMPT] = args[-2]
|
|
if args[3] != DEFAULT_FORMAT:
|
|
p.extra_generation_params[INFOTEXT_KEY_FORMAT] = args[3]
|
|
|
|
def process(
|
|
self,
|
|
p: StableDiffusionProcessingTxt2Img | StableDiffusionProcessingImg2Img,
|
|
is_enabled: bool,
|
|
process_timing: str,
|
|
seed: int,
|
|
follow_generation_seed: bool,
|
|
*args,
|
|
):
|
|
"""This method will be called after sd-dynamic-prompts and the styles are applied."""
|
|
|
|
if not is_enabled:
|
|
return
|
|
|
|
if process_timing != PROCESSING_TIMING["AFTER"]:
|
|
return
|
|
|
|
self.original_prompt = p.all_prompts
|
|
self.original_hr_prompt = getattr(p, "all_hr_prompts", None)
|
|
aspect_ratio = p.width / p.height
|
|
if seed == -1:
|
|
seed = random.randrange(2**31 - 1)
|
|
seed = int(seed)
|
|
|
|
args = list(args)
|
|
if args[3] != "custom":
|
|
args[4] = TIPO_DEFAULT_FORMAT.get(args[3], args[4])
|
|
|
|
self.write_infotext(p, p.prompt, "AFTER", seed, follow_generation_seed, *args)
|
|
|
|
args = list(args)
|
|
nl_prompt = args.pop()
|
|
new_all_prompts = []
|
|
for prompt, sub_seed in zip(p.all_prompts, p.all_seeds):
|
|
# Following the image seed keeps each batch item's prompt tied to the
|
|
# image it produces (issue #14).
|
|
tipo_seed = int(sub_seed) if follow_generation_seed else seed + sub_seed
|
|
new_all_prompts.append(
|
|
self._process(prompt, nl_prompt, aspect_ratio, tipo_seed, *args)
|
|
)
|
|
|
|
hr_fix_enabled = getattr(p, "enable_hr", False)
|
|
|
|
if hr_fix_enabled:
|
|
if p.hr_prompt != p.prompt:
|
|
new_hr_prompts = []
|
|
for prompt, hr_prompt in zip(p.all_prompts, p.all_hr_prompts):
|
|
if prompt == hr_prompt:
|
|
new_hr_prompts.append(prompt)
|
|
else:
|
|
new_hr_prompts.append(hr_prompt)
|
|
p.all_hr_prompts = new_hr_prompts
|
|
else:
|
|
p.all_hr_prompts = new_all_prompts
|
|
p.all_prompts = new_all_prompts
|
|
|
|
def before_process(
|
|
self,
|
|
p: StableDiffusionProcessingTxt2Img | StableDiffusionProcessingImg2Img,
|
|
is_enabled: bool,
|
|
process_timing: str,
|
|
seed: int,
|
|
follow_generation_seed: bool,
|
|
*args,
|
|
):
|
|
"""This method will be called before sd-dynamic-prompts and the styles are applied."""
|
|
|
|
if not is_enabled:
|
|
return
|
|
|
|
if process_timing != PROCESSING_TIMING["BEFORE"]:
|
|
return
|
|
|
|
self.original_prompt = p.prompt
|
|
self.original_hr_prompt = p.hr_prompt
|
|
aspect_ratio = p.width / p.height
|
|
if seed == -1:
|
|
seed = random.randrange(4294967294)
|
|
self.write_infotext(p, p.prompt, "BEFORE", seed, follow_generation_seed, *args)
|
|
fix_seed(p)
|
|
seed = int(p.seed) if follow_generation_seed else int(seed + p.seed)
|
|
|
|
args = list(args)
|
|
p.prompt = self._process(p.prompt, args.pop(), aspect_ratio, seed, *args)
|
|
|
|
def prompt_gen_only(self, *args):
|
|
args = list(args)
|
|
seed = args[3]
|
|
if seed == -1:
|
|
seed = random.randrange(2**31 - 1)
|
|
args[3] = seed
|
|
return self._process(*args)
|
|
|
|
def _process(
|
|
self,
|
|
prompt: str,
|
|
nl_prompt: str,
|
|
aspect_ratio: float,
|
|
seed: int,
|
|
tag_length: str,
|
|
nl_length: str,
|
|
ban_tags: str,
|
|
format_select: str,
|
|
format: str,
|
|
temperature: float,
|
|
top_p: float,
|
|
top_k: int,
|
|
model: str,
|
|
gguf_use_cpu: bool,
|
|
no_formatting: bool,
|
|
tag_prompt: str,
|
|
):
|
|
prompt = prompt.strip() or tag_prompt
|
|
seed = int(seed) % SEED_MAX
|
|
if model != self.current_model:
|
|
ensure_runtime()
|
|
if " | " in model:
|
|
model_name, gguf_name = model.split(" | ")
|
|
target_file = f"{model_name.split('/')[-1]}_{gguf_name}"
|
|
if str(models.model_dir / target_file) not in models.list_gguf():
|
|
models.download_gguf(model_name, gguf_name)
|
|
target = os.path.join(str(models.model_dir), target_file)
|
|
gguf = True
|
|
else:
|
|
target = model
|
|
gguf = False
|
|
target_device = "cpu" if gguf_use_cpu else str(devices.device)
|
|
models.load_model(target, gguf, device=target_device)
|
|
self.current_model = model
|
|
prompt = strip_comments(prompt)
|
|
nl_prompt = strip_comments(nl_prompt)
|
|
prompt_preview = prompt.replace("\n", " ")[:40]
|
|
logger.info(f"Processing prompt: {prompt_preview}...")
|
|
logger.info(f"Processing with seed: {seed}")
|
|
prompt_without_extranet, res = parse_prompt(prompt)
|
|
prompt_parse_strength = parse_prompt_attention(prompt_without_extranet)
|
|
|
|
nl_prompt_parse_strength = parse_prompt_attention(nl_prompt)
|
|
nl_prompt = ""
|
|
strength_map_nl = []
|
|
for part, strength in nl_prompt_parse_strength:
|
|
nl_prompt += part
|
|
if strength == 1:
|
|
continue
|
|
strength_map_nl.append((part, strength))
|
|
|
|
rebuild_extranet = ""
|
|
for name, params in res.items():
|
|
for param in params:
|
|
items = ":".join(param.items)
|
|
rebuild_extranet += f" <{name}:{items}>"
|
|
|
|
black_list = [tag.strip() for tag in ban_tags.split(",") if tag.strip()]
|
|
tipo.BAN_TAGS = black_list
|
|
all_tags = []
|
|
strength_map = {}
|
|
break_map = set()
|
|
for part, strength in prompt_parse_strength:
|
|
part_tags = [tag.strip() for tag in part.strip().split(",") if tag.strip()]
|
|
if part == "BREAK" and strength == -1:
|
|
break_map.add(all_tags[-1])
|
|
continue
|
|
all_tags.extend(part_tags)
|
|
if strength == 1:
|
|
continue
|
|
for tag in part_tags:
|
|
strength_map[tag] = strength
|
|
|
|
tag_length = tag_length.replace(" ", "_")
|
|
nl_length = nl_length.replace(" ", "_")
|
|
embeddings = embedding_spellings(all_tags)
|
|
org_tag_map = restore_embeddings(seperate_tags(all_tags), embeddings)
|
|
|
|
meta, operations, general, nl_prompt = parse_tipo_request(
|
|
org_tag_map,
|
|
nl_prompt,
|
|
tag_length_target=tag_length,
|
|
nl_length_target=nl_length,
|
|
generate_extra_nl_prompt=(not nl_prompt and "<|extended|>" in format)
|
|
or "<|generated|>" in format,
|
|
)
|
|
meta["aspect_ratio"] = f"{aspect_ratio:.1f}"
|
|
|
|
if isinstance(models.text_model, torch.nn.Module):
|
|
models.text_model.to(devices.device)
|
|
tag_map, _ = tipo_runner(
|
|
meta,
|
|
operations,
|
|
general,
|
|
nl_prompt,
|
|
temperature=temperature,
|
|
top_p=top_p,
|
|
top_k=top_k,
|
|
seed=seed,
|
|
)
|
|
if isinstance(models.text_model, torch.nn.Module):
|
|
models.text_model.cpu()
|
|
devices.torch_gc()
|
|
|
|
tag_map = restore_embeddings(tag_map, embeddings)
|
|
addon = {
|
|
"tags": [],
|
|
"nl": "",
|
|
}
|
|
for cate in tag_map:
|
|
if cate == "generated" and addon["nl"] == "":
|
|
addon["nl"] = tag_map[cate]
|
|
continue
|
|
if cate == "extended":
|
|
extended = tag_map[cate]
|
|
addon["nl"] = extended
|
|
continue
|
|
if cate not in org_tag_map:
|
|
continue
|
|
for tag in tag_map[cate]:
|
|
if tag in org_tag_map[cate]:
|
|
continue
|
|
addon["tags"].append(tag)
|
|
addon = apply_strength(addon, strength_map, strength_map_nl, break_map)
|
|
unformatted_prompt_by_tipo = (
|
|
prompt_without_extranet
|
|
+ ", "
|
|
+ ", ".join(addon["tags"])
|
|
+ "\n"
|
|
+ addon["nl"]
|
|
)
|
|
tag_map = apply_strength(tag_map, strength_map, strength_map_nl, break_map)
|
|
formatted_prompt_by_tipo = apply_format(tag_map, format).replace(
|
|
"BREAK,", "BREAK"
|
|
)
|
|
|
|
if no_formatting:
|
|
final_prompt = unformatted_prompt_by_tipo
|
|
else:
|
|
final_prompt = formatted_prompt_by_tipo
|
|
|
|
result = final_prompt + "\n" + rebuild_extranet
|
|
logger.info("Prompt processing done.")
|
|
return result
|
|
|
|
|
|
def parse_infotext(_, params):
|
|
try:
|
|
params[INFOTEXT_KEY] = json.loads(params[INFOTEXT_KEY].translate(QUOTESWAP))
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
scripts.script_callbacks.on_infotext_pasted(parse_infotext)
|
|
|
|
options.categories.register_category("prompt_gen", "Prompt Gen")
|
|
shared.options_templates.update(
|
|
shared.options_section(
|
|
("TIPO", "TIPO", "prompt_gen"),
|
|
{
|
|
"tipo_no_extra_input": shared.OptionInfo(
|
|
False,
|
|
(
|
|
"Disable extra input for TIPO"
|
|
", Natural Language Prompt and Tag Prompt will be hidden."
|
|
" (UI Reload Needed)"
|
|
),
|
|
),
|
|
"tipo_extra_input_layout": shared.OptionInfo(
|
|
EXTRA_INPUT_LAYOUT[0],
|
|
"Layout for the TIPO prompt inputs (UI Reload Needed)",
|
|
gr.Radio,
|
|
{"choices": EXTRA_INPUT_LAYOUT},
|
|
),
|
|
},
|
|
)
|
|
)
|