prompt-enhance-v2 with steering

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2026-05-22 16:58:20 +02:00
parent e484850e9e
commit 082498e6bd
10 changed files with 472 additions and 139 deletions
+218 -127
View File
@@ -5,6 +5,7 @@ import re
import time
import random
import base64
import textwrap
import torch
import transformers
import gradio as gr
@@ -13,6 +14,7 @@ from modules import scripts_manager, shared, devices, errors, processing, sd_mod
from modules import ui_control_helpers
from modules.sd_offload_aux import register_aux, deregister_aux, move_aux_to_gpu, offload_aux
from modules.logger import log
from modules.caption.logits import LogitsParser
debug_enabled = os.environ.get('SD_LLM_DEBUG', None) is not None
@@ -111,24 +113,10 @@ class Options:
'google/gemma-3n-E4B-it',
'google/gemma-4-E2B-it',
'google/gemma-4-E4B-it',
# Gemma Finetunes
'nidum/Nidum-Gemma-3-4B-it-Uncensored',
'allura-org/Gemma-3-Glitter-4B',
'coder3101/gemma-3-27b-it-heretic-v2',
'p-e-w/gemma-3-12b-it-heretic',
'p-e-w/gemma-4-E2B-it-heretic-ara',
'DavidAU/gemma-3-4b-it-heretic-uncensored-abliterated-Extreme',
'DavidAU/Gemma-3-4B-VL-it-Gemini-Pro-Heretic-Uncensored-Thinking',
'DavidAU/Gemma3-27B-it-vl-GLM-4.7-Uncensored-Heretic-Deep-Reasoning',
'coder3101/Big-Tiger-Gemma-27B-v3-heretic-v2',
# Qwen3.5
'Qwen/Qwen3.5-0.8B',
'Qwen/Qwen3.5-2B',
'Qwen/Qwen3.5-4B',
'Qwen/Qwen3.5-9B',
'Qwen/Qwen3.5-27B',
'Qwen/Qwen3.5-35B-A3B',
'coder3101/Qwen3.5-27B-heretic',
# Qwen3-VL
'Qwen/Qwen3-VL-2B-Instruct',
'Qwen/Qwen3-VL-2B-Thinking',
@@ -136,39 +124,26 @@ class Options:
'Qwen/Qwen3-VL-4B-Thinking',
'Qwen/Qwen3-VL-8B-Instruct',
'Qwen/Qwen3-VL-8B-Thinking',
# Qwen3-VL Finetunes
'coder3101/Qwen3-VL-2B-Instruct-heretic',
'coder3101/Qwen3-VL-2B-Thinking-heretic',
'coder3101/Qwen3-VL-4B-Instruct-heretic',
'coder3101/Qwen3-VL-4B-Thinking-heretic',
'coder3101/Qwen3-VL-8B-Instruct-heretic',
'coder3101/Qwen3-VL-32B-Instruct-heretic-v2',
'coder3101/Qwen3-VL-32B-Thinking-heretic-v2',
'prithivMLmods/Qwen3-VL-8B-Abliterated-Caption-it',
# Qwen2.5-VL
'Qwen/Qwen2.5-VL-3B-Instruct',
# Qwen2.5-VL Finetunes
'coder3101/Qwen2.5-VL-3B-Instruct-heretic',
'coder3101/Qwen2.5-VL-7B-Instruct-heretic',
'coder3101/Qwen2.5-VL-32B-Instruct-heretic',
'coder3101/Qwen2.5-VL-72B-Instruct-heretic',
# Mistral
'mistralai/Ministral-3-3B-Instruct-2512-BF16',
'mistralai/Ministral-3-8B-Instruct-2512-BF16',
'mistralai/Ministral-3-14B-Instruct-2512-BF16',
'mistralai/Devstral-Small-2-24B-Instruct-2512',
'mistralai/Ministral-3-3B-Reasoning-2512',
'mistralai/Ministral-3-8B-Reasoning-2512',
'mistralai/Ministral-3-14B-Reasoning-2512',
# Mistral Finetunes
'coder3101/Mistral-Small-3.2-24B-Instruct-2506-heretic',
'coder3101/Ministral-3-3B-Reasoning-2512-heretic',
'coder3101/Ministral-3-8B-Reasoning-2512-heretic',
'coder3101/Ministral-3-14B-Reasoning-2512-heretic',
# Finetunes
'p-e-w/gemma-4-E2B-it-heretic-ara',
'trohrbaugh/gemma-4-E4B-it-heretic-ara',
'trohrbaugh/Qwen3.5-9B-heretic-v2',
]
cloud = [
'google/gemini-3.5-flash',
'google/gemini-3.1-pro-preview',
'google/gemini-3-flash-preview',
'google/gemini-3.1-flash-lite',
'google/gemini-3.1-flash-lite-preview',
'google/gemini-2.5-flash',
'google/gemini-2.5-flash-lite',
'google/gemini-2.5-pro',
]
models = {
# Gemma
@@ -178,23 +153,11 @@ class Options:
'google/gemma-3n-E4B-it': {},
'google/gemma-4-E2B-it': {},
'google/gemma-4-E4B-it': {},
# Gemma Finetunes
'nidum/Nidum-Gemma-3-4B-it-Uncensored': {},
'allura-org/Gemma-3-Glitter-4B': {},
'coder3101/gemma-3-27b-it-heretic-v2': {},
'p-e-w/gemma-3-12b-it-heretic': {},
'DavidAU/gemma-3-4b-it-heretic-uncensored-abliterated-Extreme': {},
'DavidAU/Gemma-3-4B-VL-it-Gemini-Pro-Heretic-Uncensored-Thinking': {},
'DavidAU/Gemma3-27B-it-vl-GLM-4.7-Uncensored-Heretic-Deep-Reasoning': {},
'coder3101/Big-Tiger-Gemma-27B-v3-heretic-v2': {},
# Qwen3.5
'Qwen/Qwen3.5-0.8B': {},
'Qwen/Qwen3.5-2B': {},
'Qwen/Qwen3.5-4B': {},
'Qwen/Qwen3.5-9B': {},
'Qwen/Qwen3.5-27B': {},
'Qwen/Qwen3.5-35B-A3B': {},
'coder3101/Qwen3.5-27B-heretic': {},
# Qwen3
'Qwen/Qwen3-0.6B': {},
'Qwen/Qwen3-1.7B': {},
@@ -207,42 +170,25 @@ class Options:
'Qwen/Qwen3-VL-4B-Thinking': {},
'Qwen/Qwen3-VL-8B-Instruct': {},
'Qwen/Qwen3-VL-8B-Thinking': {},
# Qwen3-VL Finetunes
'coder3101/Qwen3-VL-2B-Instruct-heretic': {},
'coder3101/Qwen3-VL-2B-Thinking-heretic': {},
'coder3101/Qwen3-VL-4B-Instruct-heretic': {},
'coder3101/Qwen3-VL-4B-Thinking-heretic': {},
'coder3101/Qwen3-VL-8B-Instruct-heretic': {},
'coder3101/Qwen3-VL-32B-Instruct-heretic-v2': {},
'coder3101/Qwen3-VL-32B-Thinking-heretic-v2': {},
'prithivMLmods/Qwen3-VL-8B-Abliterated-Caption-it': {},
# Qwen2.5
'Qwen/Qwen2.5-0.5B-Instruct': {},
'Qwen/Qwen2.5-1.5B-Instruct': {},
'Qwen/Qwen2.5-3B-Instruct': {},
# Qwen2.5-VL
'Qwen/Qwen2.5-VL-3B-Instruct': {},
# Qwen2.5-VL Finetunes
'coder3101/Qwen2.5-VL-3B-Instruct-heretic': {},
'coder3101/Qwen2.5-VL-7B-Instruct-heretic': {},
'coder3101/Qwen2.5-VL-32B-Instruct-heretic': {},
'coder3101/Qwen2.5-VL-72B-Instruct-heretic': {},
# Llama
'meta-llama/Llama-3.2-1B-Instruct': {},
'meta-llama/Llama-3.2-3B-Instruct': {},
'meta-llama/Llama-3.2-8B-Instruct': {},
'cognitivecomputations/Dolphin3.0-Llama3.2-1B': {},
'cognitivecomputations/Dolphin3.0-Llama3.2-3B': {},
'mradermacher/Llama-3.2-1B-Instruct-Uncensored-i1-GGUF': {
'repo': 'meta-llama/Llama-3.2-1B-Instruct', # original repo so we can load missing components
'type': 'llama', # required so gguf loader knows what to do
'gguf': 'mradermacher/Llama-3.2-1B-Instruct-Uncensored-i1-GGUF', # gguf repo
'file': 'Llama-3.2-1B-Instruct-Uncensored.i1-Q4_0.gguf', # gguf file inside repo
},
'google/gemini-3.5-flash': {},
'google/gemini-3.1-pro-preview': {},
'google/gemini-3.1-flash-lite': {},
'google/gemini-3.1-flash-lite-preview': {},
'google/gemini-3-flash-preview': {},
'google/gemini-2.5-pro': {},
'google/gemini-2.5-flash': {},
'google/gemini-2.5-flash-lite': {},
'google/gemini-2.5-pro': {},
# SmolLM
'HuggingFaceTB/SmolLM2-135M-Instruct': {},
'HuggingFaceTB/SmolLM2-360M-Instruct': {},
@@ -253,17 +199,19 @@ class Options:
# Mistral
'mistralai/Ministral-3-3B-Instruct-2512-BF16': {},
'mistralai/Ministral-3-8B-Instruct-2512-BF16': {},
'mistralai/Ministral-3-14B-Instruct-2512-BF16': {},
'mistralai/Devstral-Small-2-24B-Instruct-2512': {},
'mistralai/Ministral-3-3B-Reasoning-2512': {},
'mistralai/Ministral-3-8B-Reasoning-2512': {},
'mistralai/Ministral-3-14B-Reasoning-2512': {},
# Mistral Finetunes
'coder3101/Mistral-Small-3.2-24B-Instruct-2506-heretic': {},
'p-e-w/Mistral-Nemo-Instruct-2407-heretic-noslop': {},
'coder3101/Ministral-3-3B-Reasoning-2512-heretic': {},
'coder3101/Ministral-3-8B-Reasoning-2512-heretic': {},
'coder3101/Ministral-3-14B-Reasoning-2512-heretic': {},
# Finetunes
'p-e-w/gemma-4-E2B-it-heretic-ara': {},
'trohrbaugh/gemma-4-E4B-it-heretic-ara': {},
'trohrbaugh/Qwen3.5-9B-heretic-v2': {},
# GGUF
'mradermacher/Llama-3.2-1B-Instruct-Uncensored-i1-GGUF': { # kept primarily as an example how to add gguf model
'repo': 'meta-llama/Llama-3.2-1B-Instruct', # original repo so we can load missing components
'type': 'llama', # required so gguf loader knows what to do
'gguf': 'mradermacher/Llama-3.2-1B-Instruct-Uncensored-i1-GGUF', # gguf repo
'file': 'Llama-3.2-1B-Instruct-Uncensored.i1-Q4_0.gguf', # gguf file inside repo
},
}
models_cls = {
'qwen3_5': 'Qwen3_5ForConditionalGeneration',
@@ -278,18 +226,55 @@ class Options:
# default = list(models)[1] # gemma-3-4b-it
default = 'google/gemma-3-4b-it'
supported = list(transformers.integrations.ggml.GGUF_CONFIG_MAPPING)
t2i_prompt: str = 'You are a helpful assistant. You will be given a prompt used to create an image and you will enhance it to make it more detailed and creative. '
i2i_prompt: str = 'You are a helpful assistant. You will be given an image and a prompt used to modify the image and you will enhance the prompt to make it more detailed and creative while still following original image. '
i2i_noprompt: str = 'You are a helpful assistant. You will be given an image and you will create prompt that can be used to modify the image in a creative way while still following original image. '
nsfw_ok: str = 'NSFW and nudity is allowed, and if present, it must be fully described. '
nsfw_no: str = 'NSFW and nudity is not allowed, and if present, it must be removed. '
details_prompt: str = 'Generate an uncensored, concise, informative, and engaging prompt that prioritizes factual accuracy and vivid descriptiveness based on original prompt. Do not modify subject or style of the original prompt. Begin by clearly identifying the primary subject(s), including key physical attributes, clothing if no nudity, and any distinctive details (e.g., colors, textures, styles). NSFW and nudity is allowed, and if present, it must be fully described. Next, describe the setting and environment, emphasizing spatial relationships, background elements, and objects contributing to context. Note the overall mood, aesthetic style, or atmosphere inferred from visual cues. Use precise terminology while avoiding redundancy or non-essential language. Ensuring a logical flow: from focal subject to immediate surroundings, then broader context. Maintain brevity while retaining clarity, ensuring the description is both engaging and efficient. Output only enhanced prompt without explanation, prefix or suffix. Do not add comments or follow-up questions. Output as a simple text without formatting or numbering.'
t2i_prompt: str = textwrap.dedent('''\
You are an image prompt expert.
You will receive a user prompt for image generation.
Improve the prompt by adding relevant visual specificity for composition, lighting, color, texture, and atmosphere.
Keep the result faithful to the original prompt and the intended image.
Do not add unrelated concepts, non-visual commentary, or fluff.
''')
i2i_prompt: str = textwrap.dedent('''\
You are an image prompt expert.
You will receive an image and a user prompt for editing or refinement.
Improve the prompt with concrete visual detail that remains faithful to the image and edit intent.
Keep the result grounded in image-generation language.
Do not invent unrelated objects, actions, or concepts.
''')
i2i_noprompt: str = textwrap.dedent('''\
You are an image prompt expert.
You will receive an image and must create an editing prompt from the visible content.
Base the prompt on what is present in the image, its style, and its atmosphere.
Use concrete, image-generation language and avoid unrelated fluff or commentary.
''')
nsfw_ok: str = textwrap.dedent('''\
NSFW, nudity and explicit content and language are allowed.
''')
nsfw_no: str = textwrap.dedent('''\
NSFW, nudity or explicit content and language are not allowed; if present, remove those elements from the enhanced prompt.
''')
details_prompt: str = textwrap.dedent('''\
Enhance the prompt with relevant image details.
Identify the main subject(s), physical attributes, clothing if non-nude, and distinctive visual features.
Describe the scene, environment, key objects, and overall mood or atmosphere.
Be precise and efficient; avoid redundancy, abstract commentary, unrelated fluff, or instructions.
Do not invent any objects, settings, or themes not implied by the input.
Do not add era, background props, or atmosphere unless explicitly present in the prompt.
''')
details_format: str = textwrap.dedent('''\
Output exactly one enhanced prompt string.
Do not add greetings, comments, explanations, follow-up questions, labels, formatting, or numbering.
Do not include any extra prose or analysis.
Start immediately with the prompt content.
No stray tokens!
''')
censored = ["i cannot", "i can't", "i am sorry", "against my programming", "i am not able", "i am unable", 'i am not allowed']
max_delim_index: int = 60
max_tokens: int = 1024
min_tokens: int = 0
max_tokens: int = 256
do_sample: bool = True
temperature: float = 0.7
temperature: float = 0.6
repetition_penalty: float = 1.2
top_k: int = 0
top_p: float = 0.0
@@ -311,7 +296,8 @@ class PromptEnhanceScript(scripts_manager.Script):
image: gr.Image = None
model: str = None
llm: transformers.AutoModelForCausalLM = None
tokenizer: transformers.AutoProcessor = None
processor: transformers.AutoProcessor = None
tokenizer: transformers.AutoTokenizer = None
busy: bool = False
options = Options()
@@ -332,11 +318,13 @@ class PromptEnhanceScript(scripts_manager.Script):
name = get_model_repo_from_display(name) if name else self.options.default
if self.busy:
log.debug('Prompt enhance: busy')
return
return model_repo
if is_cloud_model(name):
return
return model_repo
if (self.model is not None) and (self.model == name):
return
return model_repo
model_repo = sd_models.path_to_repo(model_repo) if model_repo else None
self.busy = True
from modules import modelloader, model_quant, ggml
@@ -355,7 +343,7 @@ class PromptEnhanceScript(scripts_manager.Script):
log.error(f'Prompt enhance: name="{name}" repo="{model_repo}" fn="{model_file}" type={model_type} gguf not supported')
log.trace(f'Prompt enhance: gguf supported={self.options.supported}')
self.busy = False
return
return model_repo
ggml.install_gguf()
gguf_args['model_type'] = model_type
gguf_args['gguf_file'] = model_file
@@ -369,8 +357,9 @@ class PromptEnhanceScript(scripts_manager.Script):
sd_models.move_model(self.llm, devices.cpu, force=True)
self.llm = None
self.tokenizer = None
self.processor = None
devices.torch_gc(force=True, reason='prompt-enhance:load')
log.debug(f'Prompt enhance: name="{self.model}" unload')
log.debug(f'Prompt enhance: unload="{self.model}"')
self.model = None
load_args = { 'pretrained_model_name_or_path': model_repo if not gguf_args else model_gguf }
if model_subfolder:
@@ -401,15 +390,12 @@ class PromptEnhanceScript(scripts_manager.Script):
sd_models.set_huggingface_options(quiet=True)
self.llm.eval()
register_aux('prompt_enhance', self.llm)
if model_repo in self.options.img2img:
cls = transformers.AutoProcessor # required to encode image
else:
cls = transformers.AutoTokenizer
tokenizer_args = { 'pretrained_model_name_or_path': model_repo }
if model_tokenizer:
tokenizer_args['subfolder'] = model_tokenizer
self.tokenizer = cls.from_pretrained(**tokenizer_args, cache_dir=shared.opts.hfcache_dir)
self.tokenizer.is_processor = model_repo in self.options.img2img
self.tokenizer = transformers.AutoTokenizer.from_pretrained(**tokenizer_args, cache_dir=shared.opts.hfcache_dir)
if model_repo in self.options.img2img:
self.processor = transformers.AutoProcessor.from_pretrained(**tokenizer_args, cache_dir=shared.opts.hfcache_dir)
if debug_enabled:
modules = sd_modules.get_model_stats(self.llm) + sd_modules.get_model_stats(self.tokenizer)
@@ -417,7 +403,7 @@ class PromptEnhanceScript(scripts_manager.Script):
debug_log(f'Prompt enhance: {m}')
self.model = name
t1 = time.time()
log.info(f'Prompt enhance: cls={self.llm.__class__.__name__} name="{name}" repo="{model_repo}" fn="{model_file}" time={t1-t0:.2f} loaded')
log.info(f'Prompt enhance: cls={self.llm.__class__.__name__} name="{name}" repo="{model_repo}" fn="{model_file}" processor="{self.processor.__class__.__name__ if self.processor else None}" tokenizer="{self.tokenizer.__class__.__name__ if self.tokenizer else None}" time={t1-t0:.2f} loaded')
self.compile()
except Exception as e:
log.error(f'Prompt enhance: load {e}')
@@ -425,6 +411,7 @@ class PromptEnhanceScript(scripts_manager.Script):
errors.display(e, 'Prompt enhance')
devices.torch_gc()
self.busy = False
return model_repo
def censored(self, response):
text = response.lower().replace("i'm", "i am")
@@ -439,6 +426,7 @@ class PromptEnhanceScript(scripts_manager.Script):
self.model = None
self.llm = None
self.tokenizer = None
self.processor = None
devices.torch_gc(force=True, reason='prompt-enhance:unload')
log.debug(f'Prompt enhance: model="{model_name}" unloaded')
else:
@@ -460,8 +448,9 @@ class PromptEnhanceScript(scripts_manager.Script):
# remove special characters
response = response.replace('"', '').replace("'", "").replace('"', '').replace('"', '').replace('**', '')
# remove repeating characters
# remove repeating characters and short repeated tokens from model collapse
response = response.replace('\n\n', '\n').replace(' ', ' ').replace('...', '.')
response = re.sub(r'\b([A-Za-z]{1,3})(?:\s+\1){1,}\b', r'\1', response, flags=re.IGNORECASE)
# remove comments between brackets (but not Reasoning:/Answer: which we may have added)
response = re.sub(r'<.*?>', '', response)
@@ -489,6 +478,14 @@ class PromptEnhanceScript(scripts_manager.Script):
response = response.strip()
# Remove leading conversational filler that some LLMs prepend
response = re.sub(
r'^(?:\s*(?:wait|okay|ok|sure|alright|yes|yep|hello|hi|thanks|thank you|no problem|of course|got it|i love|i like|i appreciate|great|excellent|right)[^.!?]*[.!?]\s*)+',
'',
response,
flags=re.IGNORECASE,
)
# Handle prefill retention/removal
prefill_text = (prefill_text or '').strip()
if prefill_text:
@@ -541,6 +538,52 @@ class PromptEnhanceScript(scripts_manager.Script):
current_image = None
return current_image
def get_default_args(self):
to_remove = ['_from_model_config', 'transformers_version']
config = {}
for k, v in transformers.GenerationConfig._get_default_generation_params().items(): # pylint: disable=protected-access
if v is not None:
config[k] = v
for k, v in transformers.GenerationConfig.from_model_config(self.llm.config).to_dict().items():
if v is not None:
config[k] = v
for k, v in self.llm.generation_config.to_dict().items():
if v is not None:
config[k] = v
config = {k: v for k, v in config.items() if k not in to_remove}
return config
def get_custom_args(self, args_str):
args = {}
if args_str is not None and len(args_str) > 0:
default_args = self.get_default_args()
pairs = re.split(r'[;\n]+', args_str)
for pair in pairs:
if '=' in pair:
key, value = pair.split('=', maxsplit=1)
key = key.strip()
value = value.strip()
if key not in default_args:
log.warning(f'Prompt enhance: key="{key}" invalid')
continue
default_value = default_args[key]
try:
if isinstance(default_value, bool):
value = value.lower() in ['true', '1', 'yes']
elif isinstance(default_value, int):
value = int(value)
elif isinstance(default_value, float):
value = float(value)
elif isinstance(default_value, list):
value = [v.strip() for v in value.split(',')]
elif isinstance(default_value, str):
pass
except ValueError:
log.warning(f'Prompt enhance: key="{key}" value="{value}" typecast failed')
if key and value:
args[key] = value
return args
def enhance(self,
model: str | None=None,
prompt:str | None=None,
@@ -548,7 +591,8 @@ class PromptEnhanceScript(scripts_manager.Script):
prefix:str | None=None,
suffix:str | None=None,
sample:bool | None=None,
tokens:int | None=None,
min_tokens:int | None=None,
max_tokens:int | None=None,
temperature:float | None=None,
penalty:float | None=None,
top_k:int | None=None,
@@ -561,6 +605,10 @@ class PromptEnhanceScript(scripts_manager.Script):
prefill:str='',
keep_prefill:bool=False,
keep_thinking:bool=False,
custom_args:str | None=None,
process_words:str='',
semantic_threshold:float=0.0,
embedding_similarity:float=0.0,
):
# Strip symbols from model name if present
model = get_model_repo_from_display(model) if model else self.options.default
@@ -572,7 +620,8 @@ class PromptEnhanceScript(scripts_manager.Script):
use_vision = False
prefix = prefix or ''
suffix = suffix or ''
tokens = tokens or self.options.max_tokens
min_tokens = min_tokens or self.options.min_tokens
max_tokens = max_tokens or self.options.max_tokens
penalty = penalty or self.options.repetition_penalty
temperature = temperature or self.options.temperature
top_k = top_k if top_k is not None else self.options.top_k
@@ -633,7 +682,7 @@ class PromptEnhanceScript(scripts_manager.Script):
if current_image is not None and isinstance(current_image, Image.Image):
if is_cloud_model(model):
pass
elif (self.tokenizer is None) or (not self.tokenizer.is_processor):
elif self.processor is None:
log.error('Prompt enhance: image not supported by model')
return prompt_text # Return original text part if image cannot be processed
if prompt_text is not None and len(prompt_text) > 0:
@@ -641,6 +690,7 @@ class PromptEnhanceScript(scripts_manager.Script):
system = self.options.i2i_prompt
system += self.options.nsfw_ok if nsfw else self.options.nsfw_no
system += self.options.details_prompt
system += self.options.details_format
chat_template = [
{ "role": "system", "content": [
{"type": "text", "text": system }
@@ -655,6 +705,7 @@ class PromptEnhanceScript(scripts_manager.Script):
system = self.options.i2i_noprompt
system += self.options.nsfw_ok if nsfw else self.options.nsfw_no
system += self.options.details_prompt
system += self.options.details_format
chat_template = [
{ "role": "system", "content": [
{"type": "text", "text": system }
@@ -668,7 +719,8 @@ class PromptEnhanceScript(scripts_manager.Script):
system = self.options.t2i_prompt
system += self.options.nsfw_ok if nsfw else self.options.nsfw_no
system += self.options.details_prompt
if (self.tokenizer is None) or (not self.tokenizer.is_processor):
system += self.options.details_format
if self.processor is None:
chat_template = [
{ "role": "system", "content": system },
{ "role": "user", "content": prompt_text },
@@ -688,6 +740,8 @@ class PromptEnhanceScript(scripts_manager.Script):
use_prefill = len(prefill_text) > 0
is_thinking = is_thinking_model(model)
debug_log(f'Prompt enhance: system="{system}"')
debug_log(f'Prompt enhance: prompt="{prompt_text}"')
debug_log(f'Prompt template: roles={[msg["role"] for msg in chat_template]} thinking={is_thinking}:{thinking} prefill={use_prefill}')
t0 = time.time()
self.busy = True
@@ -697,12 +751,13 @@ class PromptEnhanceScript(scripts_manager.Script):
from modules.caption import gemini
kwargs = {
'temperature': temperature,
'max_output_tokens': tokens,
'min_output_tokens': min_tokens,
'max_output_tokens': max_tokens,
}
model_name = model.replace('google/', '')
response = gemini.predict(prompt_text, current_image, model_name, system, model, prefill_text, thinking, kwargs)
t1 = time.time()
log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} temperature={temperature} prefill="{prefill_text[:20] if prefill_text else None}" response={len(response)}')
log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} prefill="{prefill_text[:20] if prefill_text else None}" response={len(response)}')
debug_log(f'Prompt enhance: response="{response}"')
self.busy = False
return response
@@ -716,15 +771,16 @@ class PromptEnhanceScript(scripts_manager.Script):
template_kwargs = {'enable_thinking': thinking} if is_qwen35 else {}
# Generate text prompt using template
apply_fn = self.processor if self.processor is not None else self.tokenizer
try:
text_prompt = self.tokenizer.apply_chat_template(
text_prompt = apply_fn.apply_chat_template(
chat_template,
add_generation_prompt=True,
tokenize=False,
**template_kwargs,
)
except TypeError:
text_prompt = self.tokenizer.apply_chat_template(
text_prompt = apply_fn.apply_chat_template(
chat_template,
tokenize=False,
)
@@ -748,43 +804,63 @@ class PromptEnhanceScript(scripts_manager.Script):
if use_prefill:
text_prompt += prefill_text
debug_log(f'Prompt enhance: final text_prompt (last 200 chars)="{text_prompt[-200:]}"')
# debug_log(f'Prompt enhance: template="{text_prompt}"')
# Tokenize the final prompt
# For VL models with images, pass the image to the processor (like VQA does)
if self.tokenizer.is_processor and current_image is not None:
inputs = self.tokenizer(text=[text_prompt], images=[current_image], padding=True, return_tensors="pt")
elif self.tokenizer.is_processor:
if self.processor is not None and current_image is not None:
inputs = self.processor(text=[text_prompt], images=[current_image], padding=True, return_tensors="pt")
elif self.processor is not None:
# VL processor without image - must use explicit text= parameter
inputs = self.tokenizer(text=[text_prompt], images=None, padding=True, return_tensors="pt")
inputs = self.processor(text=[text_prompt], images=None, padding=True, return_tensors="pt")
else:
inputs = self.tokenizer(text_prompt, return_tensors="pt")
inputs = inputs.to(devices.device).to(devices.dtype)
input_len = inputs['input_ids'].shape[1]
debug_log(f'Prompt enhance: len={input_len} shape={inputs["input_ids"].shape} sample={sample} temp={temperature} penalty={penalty} max={tokens}')
except Exception as e:
log.error(f'Prompt enhance tokenize: {e}')
if debug_enabled:
errors.display(e, 'Prompt enhance')
self.busy = False
return prompt_text # Return original text part on error
try:
with devices.inference_context():
move_aux_to_gpu('prompt_enhance')
gen_kwargs = {
'do_sample': sample,
'temperature': float(temperature),
'max_new_tokens': int(tokens),
'max_new_tokens': int(max_tokens),
'repetition_penalty': float(penalty),
}
if min_tokens > 0:
gen_kwargs['min_new_tokens'] = int(min_tokens)
if top_k > 0:
gen_kwargs['top_k'] = int(top_k)
if top_p > 0:
gen_kwargs['top_p'] = float(top_p)
logits_processor = None
if process_words is not None and len(process_words.strip()) > 0 and self.tokenizer is not None:
logits_processor = LogitsParser(self.tokenizer, process_words, semantic_threshold=semantic_threshold, embedding_similarity=embedding_similarity)
gen_kwargs['logits_processor'] = [logits_processor]
custom = self.get_custom_args(custom_args)
for k, v in custom.items():
gen_kwargs[k] = v
log.debug(f'Prompt enhance: cls={self.llm.__class__.__name__} model="{model}" tokens={input_len} args={gen_kwargs} custom={custom}')
defaults = {k: v for k, v in self.get_default_args().items() if k not in gen_kwargs}
log.debug(f'Prompt enhance: defaults={defaults}')
outputs = self.llm.generate(**inputs, **gen_kwargs)
if logits_processor is not None:
log.debug(f'Prompt enhance: process={logits_processor.get_replacements()}')
outputs_cropped = outputs[:, input_len:]
response = self.tokenizer.batch_decode(
decode_fn = self.processor if self.processor is not None else self.tokenizer
response = decode_fn.batch_decode(
outputs_cropped,
skip_special_tokens=True,
clean_up_tokenization_spaces=True,
@@ -810,7 +886,7 @@ class PromptEnhanceScript(scripts_manager.Script):
if not is_censored:
response = self.clean(response, keep_thinking=keep_thinking, prefill_text=prefill_text, keep_prefill=keep_prefill)
response = self.post(response, prefix, suffix, networks)
log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} seed={seed} sample={sample} temperature={temperature} penalty={penalty} thinking={thinking} keep={keep_thinking}:{keep_prefill} prefill="{prefill_text[:20] if prefill_text else None}" tokens={tokens} inputs={input_len} outputs={outputs.shape[-1] if isinstance(outputs, torch.Tensor) else 0} prompt={len(prompt_text)} response={len(response)}')
log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} seed={seed} thinking={thinking} keep={keep_thinking}:{keep_prefill} prefill="{prefill_text[:20] if prefill_text else None}" inputs={input_len} outputs={outputs.shape[-1] if isinstance(outputs, torch.Tensor) else 0} prompt={len(prompt_text)} response={len(response)}')
debug_log(f'Prompt enhance: prompt="{prompt_text}"')
debug_log(f'Prompt enhance: response_after_clean="{response}"')
self.busy = False
@@ -819,7 +895,7 @@ class PromptEnhanceScript(scripts_manager.Script):
return prompt # Return original full prompt on censorship
return response
def apply(self, prompt, image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking):
def apply(self, prompt, image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, min_tokens, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking, custom_args, process_words, semantic_threshold, embedding_similarity):
response = self.enhance(
prompt=prompt,
image=image,
@@ -828,7 +904,8 @@ class PromptEnhanceScript(scripts_manager.Script):
model=llm_model,
system=prompt_system,
sample=do_sample,
tokens=max_tokens,
min_tokens=min_tokens,
max_tokens=max_tokens,
temperature=temperature,
penalty=repetition_penalty,
top_k=top_k,
@@ -839,6 +916,10 @@ class PromptEnhanceScript(scripts_manager.Script):
prefill=prefill_text,
keep_prefill=keep_prefill,
keep_thinking=keep_thinking,
custom_args=custom_args,
process_words=process_words,
semantic_threshold=semantic_threshold,
embedding_similarity=embedding_similarity,
)
if apply_prompt:
return [response, response]
@@ -893,11 +974,12 @@ class PromptEnhanceScript(scripts_manager.Script):
model_file = gr.Textbox(label='Model file', value=None, interactive=True, elem_id='prompt_enhance_model_file', placeholder='Optional GGUF model file inside GGUF model repo')
with gr.Row():
custom_btn = gr.Button(value='Load custom model', elem_id='prompt_enhance_custom_load', variant='secondary')
custom_btn.click(fn=self.load, inputs=[model_repo, model_repo, model_gguf, model_type, model_file], outputs=[])
custom_btn.click(fn=self.load, inputs=[model_repo, model_repo, model_gguf, model_type, model_file], outputs=[llm_model])
llm_model.change(fn=self.get_custom, inputs=[llm_model], outputs=[model_repo, model_gguf, model_type, model_file])
gr.HTML('<br>')
with gr.Accordion('Options', open=False, elem_id='prompt_enhance_options'):
with gr.Row():
min_tokens = gr.Slider(label='Min tokens', value=self.options.min_tokens, minimum=0, maximum=4096, step=1, interactive=True)
max_tokens = gr.Slider(label='Max tokens', value=self.options.max_tokens, minimum=10, maximum=4096, step=1, interactive=True)
do_sample = gr.Checkbox(label='Use samplers', value=self.options.do_sample, interactive=True)
with gr.Row():
@@ -912,6 +994,8 @@ class PromptEnhanceScript(scripts_manager.Script):
with gr.Row():
keep_thinking = gr.Checkbox(label='Keep Thinking Trace', value=False, interactive=True)
keep_prefill = gr.Checkbox(label='Keep Prefill', value=False, interactive=True)
with gr.Row():
custom_args = gr.Textbox(label='Custom arguments', value='', placeholder='Optional: custom arguments for the model as k=v, semicolon delimited', interactive=True, lines=1)
with gr.Row():
prefill_text = gr.Textbox(label='Prefill text', value='', placeholder='Optional: pre-fill start of model response', interactive=True, lines=1)
gr.HTML('<br>')
@@ -922,6 +1006,12 @@ class PromptEnhanceScript(scripts_manager.Script):
prompt_suffix = gr.Textbox(label='Prompt suffix', value='', placeholder='Text appended to the enhanced result', interactive=True, lines=2, elem_id='prompt_enhance_suffix')
with gr.Row():
prompt_system = gr.Textbox(label='System prompt', value='', placeholder='Leave empty to use built-in enhancement instructions', interactive=True, lines=4, elem_id='prompt_enhance_system')
with gr.Accordion('Process', open=False, elem_id='prompt_enhance_logits'): # Corrected elem_id reference
with gr.Row():
process_words = gr.Textbox(label='Words to process', value='', placeholder='list of words with optional substitutions', interactive=True, lines=3, elem_id='prompt_enhance_process_words')
with gr.Row():
semantic_threshold = gr.Slider(label='Semantic threshold', value=0.0, minimum=0.0, maximum=1.0, step=0.01, interactive=True, elem_id='prompt_enhance_semantic_threshold')
embedding_similarity = gr.Slider(label='Embedding similarity', value=0.0, minimum=0.0, maximum=1.0, step=0.01, interactive=True, elem_id='prompt_enhance_embedding_similarity')
with gr.Accordion('Output', open=True, elem_id='prompt_enhance_output'): # Corrected elem_id reference
with gr.Row():
prompt_output = gr.Textbox(label='Enhanced prompt', value='', placeholder='Enhanced prompt will appear here', interactive=True, lines=4, max_lines=12, elem_id='prompt_enhance_result')
@@ -936,8 +1026,8 @@ class PromptEnhanceScript(scripts_manager.Script):
# Update vision toggle interactivity when model changes
llm_model.change(fn=self.update_vision_toggle, inputs=[llm_model], outputs=[use_vision], show_progress=False)
if self.prompt:
apply_btn.click(fn=self.apply, inputs=[self.prompt, self.image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking], outputs=[prompt_output, self.prompt])
return [self.prompt, self.image, apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking]
apply_btn.click(fn=self.apply, inputs=[self.prompt, self.image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, min_tokens,max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking, custom_args, process_words, semantic_threshold, embedding_similarity], outputs=[prompt_output, self.prompt])
return [self.prompt, self.image, apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking, custom_args, process_words, semantic_threshold, embedding_similarity]
def after_component(self, component, **_kwargs): # searching for actual ui prompt components
if getattr(component, 'elem_id', '') in ['txt2img_prompt', 'img2img_prompt', 'control_prompt', 'video_prompt']:
@@ -948,7 +1038,7 @@ class PromptEnhanceScript(scripts_manager.Script):
self.image.use_original = True
def before_process(self, p: processing.StableDiffusionProcessing, *args, **kwargs): # pylint: disable=unused-argument
_self_prompt, self_image, apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking = args
_self_prompt, self_image, apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, min_tokens, max_tokens, do_sample, temperature, repetition_penalty, top_k, top_p, thinking_mode, nsfw_mode, use_vision, prefill_text, keep_prefill, keep_thinking = args
if not apply_auto and not p.enhance_prompt:
return
if shared.state.skipped or shared.state.interrupted:
@@ -969,7 +1059,8 @@ class PromptEnhanceScript(scripts_manager.Script):
model=llm_model,
system=prompt_system,
sample=do_sample,
tokens=max_tokens,
min_tokens=min_tokens,
max_tokens=max_tokens,
temperature=temperature,
penalty=repetition_penalty,
top_k=top_k,