├── .gitignore ├── LICENSE ├── README.md └── scripts └── prompthacker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 scruffynerf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prompt_hacker 2 | Automatic 1111 script to mess with your prompt in a variety of ways 3 | 4 | Inspired by Tokenizer and Test My Prompt [https://github.com/Extraltodeus/test_my_prompt] 5 | As if they had a baby, who grew up to run wild and graffiti all over your precious prompts 6 | 7 | Still under heavy construction - v0.9 - Major rewrite/refactor... 8 | 9 | Current features: 10 | * Remove - remove one or more tokens, front or back of a group, or entire group 11 | * Randomize - Similar to remove, but change the token to something random 12 | * Custom Word - Similar to remove, but change the token to your choice 13 | * Shuffle - Shuffle a group of tokens (must 'take' at least 2 tokens at a time, if only 1 picked, still does 2) 14 | * Strengthen/Weaken - adds a parenthesis and colon and a strength of 0.1 to 2.0 (or negative) 15 | * Before and After - adds [Before:After:When] (When is 0-1) 16 | * MORE? Ideas welcomed. 17 | 18 | * Can work on Prompt or Negative Prompt - dropdown 19 | * Now support TI embeds, LORAs, weighted prompts, and even lets you set "protected" words to not split - textfield 20 | * Option to avoid split words into multiple tokens and treat it as one entity 21 | * Choice of many different options of operation, you can pick more than one at a time... 22 | * Custom Word - text field 23 | * Can select where in the prompt to start and end by percentage - 2 sliders 24 | * Process/group 1+ token(s) at a time - slider 25 | * Options to ignore punctuation marks or common words - checkboxes 26 | * Power of Strength/Weakness (also Before/After duration)) - slider + negative value checkbox 27 | * Grid options - radio buttons 28 | -------------------------------------------------------------------------------- /scripts/prompthacker.py: -------------------------------------------------------------------------------- 1 | # Inspired by (and started out using concepts/code from) 'Test My Prompt' and 'Tokenizer' 2 | # Purpose: to rip your prompt into small bits and then staple/tape/pound it back to together in new and interesting ways... 3 | # 4 | # Authored by Scruffynerf - msg me @Scruffy in the Civitai Discord 5 | 6 | from modules.processing import Processed, StableDiffusionProcessingImg2Img, process_images, images 7 | import modules.scripts as scripts 8 | from modules import script_callbacks, shared, sd_hijack 9 | from PIL import Image, ImageFont, ImageDraw, ImageOps 10 | from fonts.ttf import Roboto 11 | import gradio as gr 12 | from collections import namedtuple 13 | from random import randint, sample 14 | from ldm.modules.encoders.modules import FrozenCLIPEmbedder, FrozenOpenCLIPEmbedder 15 | import open_clip.tokenizer 16 | import re 17 | import random 18 | 19 | class VanillaClip: 20 | def __init__(self, clip): 21 | self.clip = clip 22 | 23 | def vocab(self): 24 | return self.clip.tokenizer.get_vocab() 25 | 26 | def byte_decoder(self): 27 | return self.clip.tokenizer.byte_decoder 28 | 29 | class OpenClip: 30 | def __init__(self, clip): 31 | self.clip = clip 32 | self.tokenizer = open_clip.tokenizer._tokenizer 33 | 34 | def vocab(self): 35 | return self.tokenizer.encoder 36 | 37 | def byte_decoder(self): 38 | return self.tokenizer.byte_decoder 39 | 40 | class Script(scripts.Script): 41 | GridSaveFlags = namedtuple('GridSaveFlags', ['never_grid', 'always_grid', 'always_save_grid'], defaults=(False, False, False)) 42 | grid_options_mapping = { 43 | "Use user settings": GridSaveFlags(), 44 | "Don't generate": GridSaveFlags(never_grid=True), 45 | "Generate": GridSaveFlags(always_grid=True), 46 | "Generate and always save": GridSaveFlags(always_grid=True, always_save_grid=True), 47 | } 48 | default_grid_opt = list(grid_options_mapping.keys())[-1] 49 | 50 | def title(self): 51 | return "Prompt Hacker" 52 | 53 | def ui(self, is_img2img): 54 | with gr.Blocks(): 55 | with gr.Box(): 56 | neg_pos = gr.Dropdown(label="Hack apart the positive or negative prompt?", choices=["Positive","Negative"], value="Positive") 57 | with gr.Row(): 58 | startat = gr.Slider(minimum=0, maximum=100, step=1, label='Start processing at % of prompt - 0%=start', value=0) 59 | endat = gr.Slider(minimum=0, maximum=100, step=1, label='Finish processing at % of prompt - 100%=end', value=100) 60 | with gr.Box(): 61 | take_x_atonce = gr.Slider(minimum=1, maximum=100, step=1, label='Process X words/tokens at a time, as a group', value=1) 62 | nosplitwords = gr.Checkbox(label='Keep complex words whole (versus splitting into 2+ tokens)', value=True) 63 | ignorepunct = gr.Checkbox(label='Ignore+Remove punctuation like commas or periods', value=True) 64 | ignorecommon = gr.Checkbox(label="Ignore+Remove common words like 'a' 'an' 'the' etc", value=False) 65 | magicwords = gr.Textbox(label="Special Word handling (if not autorecognized)",lines=1,value="yourcustomword,yourcustomword2") 66 | customword = gr.Textbox(label="Custom word - when you want to force a word in",lines=1,value="") 67 | # handle token removal functions, then token adding, then token replacement, then token reorganizing, then wrapping/weights 68 | with gr.Box(): 69 | tokenremoval = gr.Dropdown(label="Word/Token removal",choices=["","Remove Word/Token/Group", "Remove First in Group", "Remove Last in Group"],value="") 70 | tokenadding = gr.Dropdown(label="Word adding", choices=["TBD"],value="TBD") 71 | tokenreplacement = gr.Dropdown(label="Word Replacement", choices=["","Change All to 1 Custom", "Change First to Custom", "Change Last to Custom", "Change All to 1 Random", "Change Each to Random", "Change First to Random", "Change Last to Random"], value="") 72 | tokenrearrange = gr.Dropdown(label="Rearranging Words", choices=["","Shuffle All"],value="") 73 | with gr.Box(): 74 | tokenreweight = gr.Dropdown(label="Weights and Transitions", choices=["","Before and After", "Change Weight"], value="") 75 | with gr.Row(): 76 | power = gr.Slider(minimum=0, maximum=2, step=0.1, label='Stronger/Weaker weight (0-2) or Before/After (0-1) value', value=1) 77 | powerneg = gr.Checkbox(label='make Negative value', value=False) 78 | with gr.Box(): 79 | font_size = gr.Slider(minimum=12, maximum=64, step=1, label='Font size', value=32) 80 | grid_option = gr.Radio(choices=list(self.grid_options_mapping.keys()), label='Grid generation', value=self.default_grid_opt) 81 | return [neg_pos, startat, endat, take_x_atonce, nosplitwords, ignorepunct, ignorecommon, magicwords, customword, tokenremoval, tokenadding, tokenreplacement, tokenrearrange, tokenreweight, power, powerneg, font_size, grid_option] 82 | 83 | def run(self, p, neg_pos, startat, endat, take_x_atonce, nosplitwords, ignorepunct, ignorecommon, magicwords, customword, tokenremoval, tokenadding, tokenreplacement, tokenrearrange, tokenreweight, power, powerneg, font_size, grid_option): 84 | 85 | def write_on_image(img, msg): 86 | ix,iy = img.size 87 | draw = ImageDraw.Draw(img) 88 | margin=2 89 | fontsize=font_size 90 | draw = ImageDraw.Draw(img) 91 | font = ImageFont.truetype(Roboto, fontsize) 92 | text_height=iy-60 93 | tx = draw.textbbox((0,0),msg,font) 94 | draw.text((int((ix-tx[2])/2),text_height+margin),msg,(0,0,0),font=font) 95 | draw.text((int((ix-tx[2])/2),text_height-margin),msg,(0,0,0),font=font) 96 | draw.text((int((ix-tx[2])/2+margin),text_height),msg,(0,0,0),font=font) 97 | draw.text((int((ix-tx[2])/2-margin),text_height),msg,(0,0,0),font=font) 98 | draw.text((int((ix-tx[2])/2),text_height), msg,(255,255,255),font=font) 99 | return img 100 | 101 | def vocabsize(): 102 | clip = shared.sd_model.cond_stage_model.wrapped 103 | if isinstance(clip, FrozenCLIPEmbedder): 104 | clip = VanillaClip(shared.sd_model.cond_stage_model.wrapped) 105 | elif isinstance(clip, FrozenOpenCLIPEmbedder): 106 | clip = OpenClip(shared.sd_model.cond_stage_model.wrapped) 107 | else: 108 | raise RuntimeError(f'Unknown CLIP model: {type(clip).__name__}') 109 | vocab = {v: k for k, v in clip.vocab().items()} 110 | return len(vocab) 111 | 112 | def addnotreallytoken(word): 113 | notreallytokens.append(word) 114 | return (len(notreallytokens)-1)*-1 # returns the negative location of the word stored 115 | 116 | def tokenize(promptinput, input_is_ids=False): 117 | tokens = [] 118 | 119 | if input_is_ids: 120 | # if we've already got token ids, use em 121 | tokens = promptinput 122 | else: 123 | # Just trusting the tokenizer code is no good, as it doesn't support embeddings, loras, parens/brackets/weights and some trigger words. 124 | splitintowords = promptinput.split(" ") 125 | for thisword in splitintowords: 126 | commaonend = False 127 | if thisword.rstrip(",") != thisword and thisword != ",": 128 | # comma on the end 129 | commaonend = True 130 | thisword = thisword.rstrip(",") 131 | if thisword in sd_hijack.model_hijack.embedding_db.word_embeddings.keys(): 132 | # this word is embedding 133 | tokens.append(addnotreallytoken(thisword)) 134 | elif re.match("<.*>", thisword): 135 | # this word is a lora 136 | tokens.append(addnotreallytoken(thisword)) 137 | elif re.match("\(.*\)", thisword): 138 | # this word is weighted 139 | tokens.append(addnotreallytoken(thisword)) 140 | elif re.match("\[.*\]", thisword): 141 | # this word is weighted 142 | tokens.append(addnotreallytoken(thisword)) 143 | elif thisword in magicwords.split(","): 144 | # this word is given to us to flag specifically 145 | tokens.append(addnotreallytoken(thisword)) 146 | else: 147 | # tokenize this 148 | thistoken = shared.sd_model.cond_stage_model.tokenize([thisword])[0] 149 | if nosplitwords and len(thistoken) > 1: 150 | tokens.append(addnotreallytoken(thisword)) 151 | else: 152 | tokens.extend(thistoken) 153 | if commaonend: 154 | tokens.append(267) 155 | 156 | #print(f"TOKENDEBUG {tokens}") 157 | 158 | clip = shared.sd_model.cond_stage_model.wrapped 159 | if isinstance(clip, FrozenCLIPEmbedder): 160 | clip = VanillaClip(shared.sd_model.cond_stage_model.wrapped) 161 | elif isinstance(clip, FrozenOpenCLIPEmbedder): 162 | clip = OpenClip(shared.sd_model.cond_stage_model.wrapped) 163 | else: 164 | raise RuntimeError(f'Unknown CLIP model: {type(clip).__name__}') 165 | vocab = {v: k for k, v in clip.vocab().items()} 166 | byte_decoder = clip.byte_decoder() 167 | 168 | prompttext = '' 169 | ids = [] 170 | current_ids = [] 171 | 172 | def dump(last=False): 173 | nonlocal prompttext, ids, current_ids 174 | if len(current_ids) == 1 and current_ids[0] < 0: 175 | #special case word, negative of location in notreallytokens array 176 | word = notreallytokens[-current_ids[0]] + " " 177 | else: 178 | words = [vocab.get(x, "") for x in current_ids] 179 | try: 180 | word = bytearray([byte_decoder[x] for x in ''.join(words)]).decode("utf-8") 181 | except UnicodeDecodeError: 182 | if last: 183 | word = "❌" * len(current_ids) 184 | elif len(current_ids) > 4: 185 | id = current_ids[0] 186 | ids += [id] 187 | local_ids = current_ids[1:] 188 | prompttext += "❌" 189 | 190 | current_ids = [] 191 | for id in local_ids: 192 | current_ids.append(id) 193 | dump() 194 | return 195 | else: 196 | return 197 | 198 | word = word.replace("", " ") 199 | prompttext += word 200 | ids += current_ids 201 | current_ids = [] 202 | 203 | for token in tokens: 204 | token = int(token) 205 | current_ids.append(token) 206 | dump() 207 | 208 | dump(last=True) 209 | return prompttext, ids 210 | 211 | # variable setup 212 | notreallytokens = ["0placeholder"] 213 | 214 | if p.seed == -1: 215 | p.seed = randint(1,4294967295) 216 | 217 | if neg_pos == "Positive": 218 | initial_prompt = p.prompt 219 | prompt = p.prompt 220 | else: 221 | initial_prompt = p.negative_prompt 222 | prompt = p.negative_prompt 223 | 224 | full_prompt, tokens = tokenize(prompt) 225 | 226 | # process the ignored items 227 | commonlist = [320, 539, 593, 518, 550] # a of with the an 228 | if ignorecommon: 229 | tokens = list(filter(lambda x: x not in commonlist, tokens)) 230 | 231 | punctlist = [267,269,281] # ,.: 232 | if ignorepunct: 233 | tokens = list(filter(lambda x: x not in punctlist, tokens)) 234 | 235 | # handle specific limitations or requirements of chosen functionality 236 | 237 | if customword =="" or customword is None: 238 | customword = "blank" 239 | customword_id = addnotreallytoken(customword) 240 | 241 | if tokenrearrange == "Shuffle All" and take_x_atonce == 1: 242 | take_x_atonce = 2 # no point in shuffling if only one token at a time, so force to 2 minimum 243 | 244 | if tokenreweight == "Before and After": 245 | take_x_atonce = 2 # before and after means 2 tokens are needed. 246 | # if we add functions that change a single token into 2+, this will need to be revised 247 | 248 | # checked the negative box, TODO, see if I can get negative slider values 249 | if powerneg: 250 | power = -power 251 | 252 | # what if you do nothing... 253 | if tokenrearrange == "" and tokenreweight == "" and tokenreplacement == "" and tokenremoval == "": 254 | # let's default to removal of the items 255 | tokenremoval = "Remove Word/Token/Group" 256 | 257 | tokenslength = len(tokens) 258 | vocab_size = vocabsize() 259 | 260 | if take_x_atonce > tokenslength: 261 | take_x_atonce = tokenslength 262 | 263 | #startat and endat are 0-100 %, so we divide by 100, and then round result 264 | if startat >= endat: # don't do this, if so, ignore it entirely 265 | startat = 0 266 | endat = 100 267 | starttoken = round(tokenslength * startat / 100) # so 0% = length * zero or token 0 268 | endtoken = round(tokenslength * endat / 100) # so 100% = length * 1 or full tokenlength (since range is always end-1, this is fine) 269 | 270 | p.do_not_save_samples = True 271 | 272 | # first generate the potentially 'cleaned' prompt (which may or may not be different from the original), and label, and setup things to add new images afterward 273 | clean_prompt,clean_token_ids = tokenize(tokens,True) 274 | print(f"\n{clean_prompt}") 275 | 276 | if neg_pos == "Positive": 277 | p.prompt = clean_prompt 278 | else: 279 | p.negative_prompt = clean_prompt 280 | 281 | proc = process_images(p) 282 | proc.images[0] = write_on_image(proc.images[0], "Cleaned Prompt") 283 | if shared.opts.samples_save: 284 | images.save_image(proc.images[0], p.outpath_samples, "", proc.seed, p.prompt, shared.opts.samples_format, info=proc.infotexts[0], p=p) 285 | 286 | # loop to process prompt changes, and generate an image for each prompt. 287 | 288 | for g in range(endtoken): # loops from zero to 1 less than end token, ie every token 0 thru last desired token 289 | 290 | #items to reset each loop 291 | new_tokens = tokens.copy() 292 | 293 | if g < starttoken: # if we are skipping from the start 294 | continue 295 | if g > (endtoken - take_x_atonce): # avoid grabbing last tokens if we're grouping, and not enough are left to grab a full set 296 | break 297 | 298 | #process the rest of the prompt, except for the piece under question: 299 | working_preprompt,returned_pretoken_ids = tokenize(new_tokens[:g],True) 300 | working_postprompt,returned_posttoken_ids = tokenize(new_tokens[g+take_x_atonce:],True) 301 | 302 | # get the tokens in question: 303 | working_tokens = new_tokens[g:g+take_x_atonce] 304 | original_prompt, working_tokens = tokenize(working_tokens,True) 305 | working_prompt = original_prompt 306 | 307 | # process all functions desired, this can now be more than one... 308 | 309 | # handle token removal functions, then token adding, then token replacement, then token reorganizing, then wrapping/weights 310 | # this order should make the most sense... 311 | 312 | # functions that reduce tokens to just one 313 | 314 | if tokenreplacement == "Change All to 1 Custom": # Replace group with just 1 custom word 315 | working_prompt,working_tokens = tokenize([customword_id],True) 316 | image_caption = f"{original_prompt}->\n{working_prompt}" 317 | 318 | if tokenreplacement == "Change All to 1 Random": # Replace group Randomly with just 1 319 | working_tokens = [randint(1,vocab_size)] 320 | working_prompt,working_tokens = tokenize(working_tokens,True) 321 | image_caption = f"{original_prompt}->\n{working_prompt}" 322 | 323 | # potentially changes token counts, either up or down... 324 | if tokenremoval == "Remove One or More of a Group": # Remove one or more of group 325 | #todo 326 | pass 327 | 328 | if tokenremoval == "Remove First in Group": # Remove first of group 329 | working_tokens.pop(0) 330 | working_prompt,working_tokens = tokenize(working_tokens,True) 331 | image_caption = f"{original_prompt}->\n{working_prompt}" 332 | 333 | if tokenremoval == "Remove Last in Group": # Remove last of group 334 | working_tokens.pop() 335 | working_prompt,working_tokens = tokenize(working_tokens,True) 336 | image_caption = f"{original_prompt}->\n{working_prompt}" 337 | 338 | # partial token replacement, leaves token count intact... 339 | if tokenreplacement == "Change First to Custom": # Replace first in group with custom word 340 | working_tokens[0] = customword_id 341 | working_prompt,working_tokens = tokenize(working_tokens,True) 342 | image_caption = f"{original_prompt}->\n{working_prompt}" 343 | 344 | if tokenreplacement == "Change Last to Custom": # Replace last in group with custom word 345 | working_tokens[-1] = customword_id 346 | working_prompt,working_tokens = tokenize(working_tokens,True) 347 | image_caption = f"{original_prompt}->\n{working_prompt}" 348 | 349 | if tokenreplacement == "Change First to Random": # Replace First Randomly in group 350 | random_token = randint(1,vocab_size) 351 | working_tokens[0] = random_token 352 | working_prompt,working_tokens = tokenize(working_tokens,True) 353 | image_caption = f"{original_prompt}->\n{working_prompt}" 354 | 355 | if tokenreplacement == "Change Last to Random": # Replace Last Randomly in group 356 | random_token = randint(1,vocab_size) 357 | working_tokens[-1] = random_token 358 | working_prompt,working_tokens = tokenize(working_tokens,True) 359 | image_caption = f"{original_prompt}->\n{working_prompt}" 360 | 361 | if tokenreplacement == "Change Each to Random": # Replace each Randomly in group 362 | for x in range(len(working_tokens)): 363 | random_token = randint(1,vocab_size) 364 | working_tokens[x] = random_token 365 | working_prompt,working_tokens = tokenize(working_tokens,True) 366 | image_caption = f"{original_prompt}->\n{working_prompt}" 367 | 368 | # rearrange token order functions 369 | 370 | if tokenrearrange == "Shuffle All": # Shuffle group 371 | shufcount = 0 372 | original_tokens_order = working_tokens.copy() 373 | while shufcount < 5: # if 5 mixes fail to find a different order than the input, move on... to handle poor shuffling of small amount of tokens 374 | random.shuffle(working_tokens) 375 | if working_tokens != original_tokens_order: 376 | shufcount = 5 377 | shufcount += 1 378 | working_prompt,working_tokens = tokenize(working_tokens,True) 379 | image_caption = f"{original_prompt}->\n{working_prompt}" 380 | 381 | # functions that add wrapping items like weights/etc 382 | 383 | if tokenreweight == "Before and After" and len(working_tokens) == 2: # Before and After 384 | token0,returned_token_id = tokenize([working_tokens[0]],True) 385 | token1,returned_token_id = tokenize([working_tokens[1]],True) 386 | working_prompt = "[" + token0.rstrip(" ") + ":" + token1.rstrip(" ") + ":" + str(power) + "] " 387 | image_caption = working_prompt 388 | # this really isn't conductive to further manipulation so it should be one of the last functions 389 | 390 | if tokenreweight == "Change Weight": # Strengthen or Weaken 391 | # do we already have a weight already we can change? 392 | if re.search(r":[0-9\.-]+", working_prompt): # TODO do not changing bracketed weights, only parens or carets 393 | # yes, let's change that one instead of wrapping it with a new one 394 | working_prompt = re.sub(r":[0-9\.-]+", ':'+str(power), working_prompt) 395 | # potentially this mean a group won't get a weight if one part is already weighted TODO fix 396 | else: 397 | # no weight in current piece so wrap it all with a weight 398 | working_prompt = f"({working_prompt.rstrip(' ')}:{power}) " 399 | # this really isn't very conductive to further manipulation so it should be one of the last functions 400 | image_caption = working_prompt 401 | 402 | # the nuclear option - remove entire section in question, no matter was done above, this will just wipe it out. 403 | if tokenremoval == "Remove Word/Token/Group": # Remove all of group 404 | image_caption = "No " + working_prompt 405 | working_prompt = "" 406 | # this really isn't conductive to any further manipulation so it should be the last function. Boom. 407 | 408 | # put things back together, new_preprompt + working_prompt + new_postprompt 409 | new_prompt = working_preprompt + working_prompt + working_postprompt 410 | 411 | # this puts the prompt up before the generation, useful for console watching 412 | print(f"\n{new_prompt}") 413 | 414 | if neg_pos == "Positive": 415 | p.prompt = new_prompt 416 | else: 417 | p.negative_prompt = new_prompt 418 | 419 | appendimages = process_images(p) 420 | proc.images.append(appendimages.images[0]) 421 | proc.infotexts.append(appendimages.infotexts[0]) 422 | proc.images[-1] = write_on_image(proc.images[-1], image_caption) 423 | if shared.opts.samples_save: 424 | images.save_image(proc.images[-1], p.outpath_samples, "", proc.seed, p.prompt, shared.opts.samples_format, info=proc.infotexts[-1], p=p) 425 | # loop end 426 | 427 | # generate the original prompt as given, and label, this way the final result is the prompt is recorded as identical to the original given, and restore works correctly. 428 | # otherwise, the last prompt, no matter how mangled would be the recorded one, undesired. 429 | print(f"\n{initial_prompt}") 430 | 431 | if neg_pos == "Positive": 432 | p.prompt = initial_prompt 433 | else: 434 | p.negative_prompt = initial_prompt 435 | 436 | appendimages = process_images(p) 437 | proc.images.append(appendimages.images[0]) 438 | proc.infotexts.append(appendimages.infotexts[0]) 439 | proc.images[-1] = write_on_image(proc.images[-1], "Original Prompt") 440 | if shared.opts.samples_save: 441 | images.save_image(proc.images[-1], p.outpath_samples, "", proc.seed, p.prompt, shared.opts.samples_format, info=proc.infotexts[-1], p=p) 442 | 443 | # make grid, if desired, since all images are generated 444 | grid_flags = self.grid_options_mapping[grid_option] 445 | unwanted_grid_because_of_img_count = len(proc.images) < 2 and shared.opts.grid_only_if_multiple 446 | if ((shared.opts.return_grid or shared.opts.grid_save) and not p.do_not_save_grid and not grid_flags.never_grid and not unwanted_grid_because_of_img_count) or grid_flags.always_grid: 447 | grid = images.image_grid(proc.images) 448 | proc.images.insert(0,grid) 449 | proc.infotexts.insert(0, proc.infotexts[0]) 450 | if shared.opts.grid_save or grid_flags.always_save_grid: 451 | images.save_image(grid, p.outpath_grids, "grid", p.seed, p.prompt, shared.opts.grid_format, info=proc.info, short_filename=not shared.opts.grid_extended_filename, p=p, grid=True) 452 | 453 | return proc 454 | --------------------------------------------------------------------------------