artificialguybr commited on
Commit
6a295ff
1 Parent(s): 1f2b323

Upload 3 files

Browse files
Files changed (3) hide show
  1. modules/images.py +778 -0
  2. modules/scripts.py +758 -0
  3. modules/shared.py +87 -0
modules/images.py ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+
5
+ import pytz
6
+ import io
7
+ import math
8
+ import os
9
+ from collections import namedtuple
10
+ import re
11
+
12
+ import numpy as np
13
+ import piexif
14
+ import piexif.helper
15
+ from PIL import Image, ImageFont, ImageDraw, ImageColor, PngImagePlugin
16
+ import string
17
+ import json
18
+ import hashlib
19
+
20
+ from modules import sd_samplers, shared, script_callbacks, errors
21
+ from modules.paths_internal import roboto_ttf_file
22
+ from modules.shared import opts
23
+
24
+ LANCZOS = (Image.Resampling.LANCZOS if hasattr(Image, 'Resampling') else Image.LANCZOS)
25
+
26
+
27
+ def get_font(fontsize: int):
28
+ try:
29
+ return ImageFont.truetype(opts.font or roboto_ttf_file, fontsize)
30
+ except Exception:
31
+ return ImageFont.truetype(roboto_ttf_file, fontsize)
32
+
33
+
34
+ def image_grid(imgs, batch_size=1, rows=None):
35
+ if rows is None:
36
+ if opts.n_rows > 0:
37
+ rows = opts.n_rows
38
+ elif opts.n_rows == 0:
39
+ rows = batch_size
40
+ elif opts.grid_prevent_empty_spots:
41
+ rows = math.floor(math.sqrt(len(imgs)))
42
+ while len(imgs) % rows != 0:
43
+ rows -= 1
44
+ else:
45
+ rows = math.sqrt(len(imgs))
46
+ rows = round(rows)
47
+ if rows > len(imgs):
48
+ rows = len(imgs)
49
+
50
+ cols = math.ceil(len(imgs) / rows)
51
+
52
+ params = script_callbacks.ImageGridLoopParams(imgs, cols, rows)
53
+ script_callbacks.image_grid_callback(params)
54
+
55
+ w, h = imgs[0].size
56
+ grid = Image.new('RGB', size=(params.cols * w, params.rows * h), color='black')
57
+
58
+ for i, img in enumerate(params.imgs):
59
+ grid.paste(img, box=(i % params.cols * w, i // params.cols * h))
60
+
61
+ return grid
62
+
63
+
64
+ Grid = namedtuple("Grid", ["tiles", "tile_w", "tile_h", "image_w", "image_h", "overlap"])
65
+
66
+
67
+ def split_grid(image, tile_w=512, tile_h=512, overlap=64):
68
+ w = image.width
69
+ h = image.height
70
+
71
+ non_overlap_width = tile_w - overlap
72
+ non_overlap_height = tile_h - overlap
73
+
74
+ cols = math.ceil((w - overlap) / non_overlap_width)
75
+ rows = math.ceil((h - overlap) / non_overlap_height)
76
+
77
+ dx = (w - tile_w) / (cols - 1) if cols > 1 else 0
78
+ dy = (h - tile_h) / (rows - 1) if rows > 1 else 0
79
+
80
+ grid = Grid([], tile_w, tile_h, w, h, overlap)
81
+ for row in range(rows):
82
+ row_images = []
83
+
84
+ y = int(row * dy)
85
+
86
+ if y + tile_h >= h:
87
+ y = h - tile_h
88
+
89
+ for col in range(cols):
90
+ x = int(col * dx)
91
+
92
+ if x + tile_w >= w:
93
+ x = w - tile_w
94
+
95
+ tile = image.crop((x, y, x + tile_w, y + tile_h))
96
+
97
+ row_images.append([x, tile_w, tile])
98
+
99
+ grid.tiles.append([y, tile_h, row_images])
100
+
101
+ return grid
102
+
103
+
104
+ def combine_grid(grid):
105
+ def make_mask_image(r):
106
+ r = r * 255 / grid.overlap
107
+ r = r.astype(np.uint8)
108
+ return Image.fromarray(r, 'L')
109
+
110
+ mask_w = make_mask_image(np.arange(grid.overlap, dtype=np.float32).reshape((1, grid.overlap)).repeat(grid.tile_h, axis=0))
111
+ mask_h = make_mask_image(np.arange(grid.overlap, dtype=np.float32).reshape((grid.overlap, 1)).repeat(grid.image_w, axis=1))
112
+
113
+ combined_image = Image.new("RGB", (grid.image_w, grid.image_h))
114
+ for y, h, row in grid.tiles:
115
+ combined_row = Image.new("RGB", (grid.image_w, h))
116
+ for x, w, tile in row:
117
+ if x == 0:
118
+ combined_row.paste(tile, (0, 0))
119
+ continue
120
+
121
+ combined_row.paste(tile.crop((0, 0, grid.overlap, h)), (x, 0), mask=mask_w)
122
+ combined_row.paste(tile.crop((grid.overlap, 0, w, h)), (x + grid.overlap, 0))
123
+
124
+ if y == 0:
125
+ combined_image.paste(combined_row, (0, 0))
126
+ continue
127
+
128
+ combined_image.paste(combined_row.crop((0, 0, combined_row.width, grid.overlap)), (0, y), mask=mask_h)
129
+ combined_image.paste(combined_row.crop((0, grid.overlap, combined_row.width, h)), (0, y + grid.overlap))
130
+
131
+ return combined_image
132
+
133
+
134
+ class GridAnnotation:
135
+ def __init__(self, text='', is_active=True):
136
+ self.text = text
137
+ self.is_active = is_active
138
+ self.size = None
139
+
140
+
141
+ def draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin=0):
142
+
143
+ color_active = ImageColor.getcolor(opts.grid_text_active_color, 'RGB')
144
+ color_inactive = ImageColor.getcolor(opts.grid_text_inactive_color, 'RGB')
145
+ color_background = ImageColor.getcolor(opts.grid_background_color, 'RGB')
146
+
147
+ def wrap(drawing, text, font, line_length):
148
+ lines = ['']
149
+ for word in text.split():
150
+ line = f'{lines[-1]} {word}'.strip()
151
+ if drawing.textlength(line, font=font) <= line_length:
152
+ lines[-1] = line
153
+ else:
154
+ lines.append(word)
155
+ return lines
156
+
157
+ def draw_texts(drawing, draw_x, draw_y, lines, initial_fnt, initial_fontsize):
158
+ for line in lines:
159
+ fnt = initial_fnt
160
+ fontsize = initial_fontsize
161
+ while drawing.multiline_textsize(line.text, font=fnt)[0] > line.allowed_width and fontsize > 0:
162
+ fontsize -= 1
163
+ fnt = get_font(fontsize)
164
+ drawing.multiline_text((draw_x, draw_y + line.size[1] / 2), line.text, font=fnt, fill=color_active if line.is_active else color_inactive, anchor="mm", align="center")
165
+
166
+ if not line.is_active:
167
+ drawing.line((draw_x - line.size[0] // 2, draw_y + line.size[1] // 2, draw_x + line.size[0] // 2, draw_y + line.size[1] // 2), fill=color_inactive, width=4)
168
+
169
+ draw_y += line.size[1] + line_spacing
170
+
171
+ fontsize = (width + height) // 25
172
+ line_spacing = fontsize // 2
173
+
174
+ fnt = get_font(fontsize)
175
+
176
+ pad_left = 0 if sum([sum([len(line.text) for line in lines]) for lines in ver_texts]) == 0 else width * 3 // 4
177
+
178
+ cols = im.width // width
179
+ rows = im.height // height
180
+
181
+ assert cols == len(hor_texts), f'bad number of horizontal texts: {len(hor_texts)}; must be {cols}'
182
+ assert rows == len(ver_texts), f'bad number of vertical texts: {len(ver_texts)}; must be {rows}'
183
+
184
+ calc_img = Image.new("RGB", (1, 1), color_background)
185
+ calc_d = ImageDraw.Draw(calc_img)
186
+
187
+ for texts, allowed_width in zip(hor_texts + ver_texts, [width] * len(hor_texts) + [pad_left] * len(ver_texts)):
188
+ items = [] + texts
189
+ texts.clear()
190
+
191
+ for line in items:
192
+ wrapped = wrap(calc_d, line.text, fnt, allowed_width)
193
+ texts += [GridAnnotation(x, line.is_active) for x in wrapped]
194
+
195
+ for line in texts:
196
+ bbox = calc_d.multiline_textbbox((0, 0), line.text, font=fnt)
197
+ line.size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
198
+ line.allowed_width = allowed_width
199
+
200
+ hor_text_heights = [sum([line.size[1] + line_spacing for line in lines]) - line_spacing for lines in hor_texts]
201
+ ver_text_heights = [sum([line.size[1] + line_spacing for line in lines]) - line_spacing * len(lines) for lines in ver_texts]
202
+
203
+ pad_top = 0 if sum(hor_text_heights) == 0 else max(hor_text_heights) + line_spacing * 2
204
+
205
+ result = Image.new("RGB", (im.width + pad_left + margin * (cols-1), im.height + pad_top + margin * (rows-1)), color_background)
206
+
207
+ for row in range(rows):
208
+ for col in range(cols):
209
+ cell = im.crop((width * col, height * row, width * (col+1), height * (row+1)))
210
+ result.paste(cell, (pad_left + (width + margin) * col, pad_top + (height + margin) * row))
211
+
212
+ d = ImageDraw.Draw(result)
213
+
214
+ for col in range(cols):
215
+ x = pad_left + (width + margin) * col + width / 2
216
+ y = pad_top / 2 - hor_text_heights[col] / 2
217
+
218
+ draw_texts(d, x, y, hor_texts[col], fnt, fontsize)
219
+
220
+ for row in range(rows):
221
+ x = pad_left / 2
222
+ y = pad_top + (height + margin) * row + height / 2 - ver_text_heights[row] / 2
223
+
224
+ draw_texts(d, x, y, ver_texts[row], fnt, fontsize)
225
+
226
+ return result
227
+
228
+
229
+ def draw_prompt_matrix(im, width, height, all_prompts, margin=0):
230
+ prompts = all_prompts[1:]
231
+ boundary = math.ceil(len(prompts) / 2)
232
+
233
+ prompts_horiz = prompts[:boundary]
234
+ prompts_vert = prompts[boundary:]
235
+
236
+ hor_texts = [[GridAnnotation(x, is_active=pos & (1 << i) != 0) for i, x in enumerate(prompts_horiz)] for pos in range(1 << len(prompts_horiz))]
237
+ ver_texts = [[GridAnnotation(x, is_active=pos & (1 << i) != 0) for i, x in enumerate(prompts_vert)] for pos in range(1 << len(prompts_vert))]
238
+
239
+ return draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin)
240
+
241
+
242
+ def resize_image(resize_mode, im, width, height, upscaler_name=None):
243
+ """
244
+ Resizes an image with the specified resize_mode, width, and height.
245
+
246
+ Args:
247
+ resize_mode: The mode to use when resizing the image.
248
+ 0: Resize the image to the specified width and height.
249
+ 1: Resize the image to fill the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, cropping the excess.
250
+ 2: Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, filling empty with data from image.
251
+ im: The image to resize.
252
+ width: The width to resize the image to.
253
+ height: The height to resize the image to.
254
+ upscaler_name: The name of the upscaler to use. If not provided, defaults to opts.upscaler_for_img2img.
255
+ """
256
+
257
+ upscaler_name = upscaler_name or opts.upscaler_for_img2img
258
+
259
+ def resize(im, w, h):
260
+ if upscaler_name is None or upscaler_name == "None" or im.mode == 'L':
261
+ return im.resize((w, h), resample=LANCZOS)
262
+
263
+ scale = max(w / im.width, h / im.height)
264
+
265
+ if scale > 1.0:
266
+ upscalers = [x for x in shared.sd_upscalers if x.name == upscaler_name]
267
+ if len(upscalers) == 0:
268
+ upscaler = shared.sd_upscalers[0]
269
+ print(f"could not find upscaler named {upscaler_name or '<empty string>'}, using {upscaler.name} as a fallback")
270
+ else:
271
+ upscaler = upscalers[0]
272
+
273
+ im = upscaler.scaler.upscale(im, scale, upscaler.data_path)
274
+
275
+ if im.width != w or im.height != h:
276
+ im = im.resize((w, h), resample=LANCZOS)
277
+
278
+ return im
279
+
280
+ if resize_mode == 0:
281
+ res = resize(im, width, height)
282
+
283
+ elif resize_mode == 1:
284
+ ratio = width / height
285
+ src_ratio = im.width / im.height
286
+
287
+ src_w = width if ratio > src_ratio else im.width * height // im.height
288
+ src_h = height if ratio <= src_ratio else im.height * width // im.width
289
+
290
+ resized = resize(im, src_w, src_h)
291
+ res = Image.new("RGB", (width, height))
292
+ res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
293
+
294
+ else:
295
+ ratio = width / height
296
+ src_ratio = im.width / im.height
297
+
298
+ src_w = width if ratio < src_ratio else im.width * height // im.height
299
+ src_h = height if ratio >= src_ratio else im.height * width // im.width
300
+
301
+ resized = resize(im, src_w, src_h)
302
+ res = Image.new("RGB", (width, height))
303
+ res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
304
+
305
+ if ratio < src_ratio:
306
+ fill_height = height // 2 - src_h // 2
307
+ if fill_height > 0:
308
+ res.paste(resized.resize((width, fill_height), box=(0, 0, width, 0)), box=(0, 0))
309
+ res.paste(resized.resize((width, fill_height), box=(0, resized.height, width, resized.height)), box=(0, fill_height + src_h))
310
+ elif ratio > src_ratio:
311
+ fill_width = width // 2 - src_w // 2
312
+ if fill_width > 0:
313
+ res.paste(resized.resize((fill_width, height), box=(0, 0, 0, height)), box=(0, 0))
314
+ res.paste(resized.resize((fill_width, height), box=(resized.width, 0, resized.width, height)), box=(fill_width + src_w, 0))
315
+
316
+ return res
317
+
318
+
319
+ invalid_filename_chars = '<>:"/\\|?*\n\r\t'
320
+ invalid_filename_prefix = ' '
321
+ invalid_filename_postfix = ' .'
322
+ re_nonletters = re.compile(r'[\s' + string.punctuation + ']+')
323
+ re_pattern = re.compile(r"(.*?)(?:\[([^\[\]]+)\]|$)")
324
+ re_pattern_arg = re.compile(r"(.*)<([^>]*)>$")
325
+ max_filename_part_length = 128
326
+ NOTHING_AND_SKIP_PREVIOUS_TEXT = object()
327
+
328
+
329
+ def sanitize_filename_part(text, replace_spaces=True):
330
+ if text is None:
331
+ return None
332
+
333
+ if replace_spaces:
334
+ text = text.replace(' ', '_')
335
+
336
+ text = text.translate({ord(x): '_' for x in invalid_filename_chars})
337
+ text = text.lstrip(invalid_filename_prefix)[:max_filename_part_length]
338
+ text = text.rstrip(invalid_filename_postfix)
339
+ return text
340
+
341
+
342
+ class FilenameGenerator:
343
+ replacements = {
344
+ 'seed': lambda self: self.seed if self.seed is not None else '',
345
+ 'seed_first': lambda self: self.seed if self.p.batch_size == 1 else self.p.all_seeds[0],
346
+ 'seed_last': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.batch_size == 1 else self.p.all_seeds[-1],
347
+ 'steps': lambda self: self.p and self.p.steps,
348
+ 'cfg': lambda self: self.p and self.p.cfg_scale,
349
+ 'width': lambda self: self.image.width,
350
+ 'height': lambda self: self.image.height,
351
+ 'styles': lambda self: self.p and sanitize_filename_part(", ".join([style for style in self.p.styles if not style == "None"]) or "None", replace_spaces=False),
352
+ 'sampler': lambda self: self.p and sanitize_filename_part(self.p.sampler_name, replace_spaces=False),
353
+ 'model_hash': lambda self: getattr(self.p, "sd_model_hash", shared.sd_model.sd_model_hash),
354
+ 'model_name': lambda self: sanitize_filename_part(shared.sd_model.sd_checkpoint_info.name_for_extra, replace_spaces=False),
355
+ 'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'),
356
+ 'datetime': lambda self, *args: self.datetime(*args), # accepts formats: [datetime], [datetime<Format>], [datetime<Format><Time Zone>]
357
+ 'job_timestamp': lambda self: getattr(self.p, "job_timestamp", shared.state.job_timestamp),
358
+ 'prompt_hash': lambda self, *args: self.string_hash(self.prompt, *args),
359
+ 'negative_prompt_hash': lambda self, *args: self.string_hash(self.p.negative_prompt, *args),
360
+ 'full_prompt_hash': lambda self, *args: self.string_hash(f"{self.p.prompt} {self.p.negative_prompt}", *args), # a space in between to create a unique string
361
+ 'prompt': lambda self: sanitize_filename_part(self.prompt),
362
+ 'prompt_no_styles': lambda self: self.prompt_no_style(),
363
+ 'prompt_spaces': lambda self: sanitize_filename_part(self.prompt, replace_spaces=False),
364
+ 'prompt_words': lambda self: self.prompt_words(),
365
+ 'batch_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.batch_size == 1 or self.zip else self.p.batch_index + 1,
366
+ 'batch_size': lambda self: self.p.batch_size,
367
+ 'generation_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if (self.p.n_iter == 1 and self.p.batch_size == 1) or self.zip else self.p.iteration * self.p.batch_size + self.p.batch_index + 1,
368
+ 'hasprompt': lambda self, *args: self.hasprompt(*args), # accepts formats:[hasprompt<prompt1|default><prompt2>..]
369
+ 'clip_skip': lambda self: opts.data["CLIP_stop_at_last_layers"],
370
+ 'denoising': lambda self: self.p.denoising_strength if self.p and self.p.denoising_strength else NOTHING_AND_SKIP_PREVIOUS_TEXT,
371
+ 'user': lambda self: self.p.user,
372
+ 'vae_filename': lambda self: self.get_vae_filename(),
373
+ 'none': lambda self: '', # Overrides the default, so you can get just the sequence number
374
+ 'image_hash': lambda self, *args: self.image_hash(*args) # accepts formats: [image_hash<length>] default full hash
375
+ }
376
+ default_time_format = '%Y%m%d%H%M%S'
377
+
378
+ def __init__(self, p, seed, prompt, image, zip=False):
379
+ self.p = p
380
+ self.seed = seed
381
+ self.prompt = prompt
382
+ self.image = image
383
+ self.zip = zip
384
+
385
+ def get_vae_filename(self):
386
+ """Get the name of the VAE file."""
387
+
388
+ import modules.sd_vae as sd_vae
389
+
390
+ if sd_vae.loaded_vae_file is None:
391
+ return "NoneType"
392
+
393
+ file_name = os.path.basename(sd_vae.loaded_vae_file)
394
+ split_file_name = file_name.split('.')
395
+ if len(split_file_name) > 1 and split_file_name[0] == '':
396
+ return split_file_name[1] # if the first character of the filename is "." then [1] is obtained.
397
+ else:
398
+ return split_file_name[0]
399
+
400
+
401
+ def hasprompt(self, *args):
402
+ lower = self.prompt.lower()
403
+ if self.p is None or self.prompt is None:
404
+ return None
405
+ outres = ""
406
+ for arg in args:
407
+ if arg != "":
408
+ division = arg.split("|")
409
+ expected = division[0].lower()
410
+ default = division[1] if len(division) > 1 else ""
411
+ if lower.find(expected) >= 0:
412
+ outres = f'{outres}{expected}'
413
+ else:
414
+ outres = outres if default == "" else f'{outres}{default}'
415
+ return sanitize_filename_part(outres)
416
+
417
+ def prompt_no_style(self):
418
+ if self.p is None or self.prompt is None:
419
+ return None
420
+
421
+ prompt_no_style = self.prompt
422
+ for style in shared.prompt_styles.get_style_prompts(self.p.styles):
423
+ if style:
424
+ for part in style.split("{prompt}"):
425
+ prompt_no_style = prompt_no_style.replace(part, "").replace(", ,", ",").strip().strip(',')
426
+
427
+ prompt_no_style = prompt_no_style.replace(style, "").strip().strip(',').strip()
428
+
429
+ return sanitize_filename_part(prompt_no_style, replace_spaces=False)
430
+
431
+ def prompt_words(self):
432
+ words = [x for x in re_nonletters.split(self.prompt or "") if x]
433
+ if len(words) == 0:
434
+ words = ["empty"]
435
+ return sanitize_filename_part(" ".join(words[0:opts.directories_max_prompt_words]), replace_spaces=False)
436
+
437
+ def datetime(self, *args):
438
+ time_datetime = datetime.datetime.now()
439
+
440
+ time_format = args[0] if (args and args[0] != "") else self.default_time_format
441
+ try:
442
+ time_zone = pytz.timezone(args[1]) if len(args) > 1 else None
443
+ except pytz.exceptions.UnknownTimeZoneError:
444
+ time_zone = None
445
+
446
+ time_zone_time = time_datetime.astimezone(time_zone)
447
+ try:
448
+ formatted_time = time_zone_time.strftime(time_format)
449
+ except (ValueError, TypeError):
450
+ formatted_time = time_zone_time.strftime(self.default_time_format)
451
+
452
+ return sanitize_filename_part(formatted_time, replace_spaces=False)
453
+
454
+ def image_hash(self, *args):
455
+ length = int(args[0]) if (args and args[0] != "") else None
456
+ return hashlib.sha256(self.image.tobytes()).hexdigest()[0:length]
457
+
458
+ def string_hash(self, text, *args):
459
+ length = int(args[0]) if (args and args[0] != "") else 8
460
+ return hashlib.sha256(text.encode()).hexdigest()[0:length]
461
+
462
+ def apply(self, x):
463
+ res = ''
464
+
465
+ for m in re_pattern.finditer(x):
466
+ text, pattern = m.groups()
467
+
468
+ if pattern is None:
469
+ res += text
470
+ continue
471
+
472
+ pattern_args = []
473
+ while True:
474
+ m = re_pattern_arg.match(pattern)
475
+ if m is None:
476
+ break
477
+
478
+ pattern, arg = m.groups()
479
+ pattern_args.insert(0, arg)
480
+
481
+ fun = self.replacements.get(pattern.lower())
482
+ if fun is not None:
483
+ try:
484
+ replacement = fun(self, *pattern_args)
485
+ except Exception:
486
+ replacement = None
487
+ errors.report(f"Error adding [{pattern}] to filename", exc_info=True)
488
+
489
+ if replacement == NOTHING_AND_SKIP_PREVIOUS_TEXT:
490
+ continue
491
+ elif replacement is not None:
492
+ res += text + str(replacement)
493
+ continue
494
+
495
+ res += f'{text}[{pattern}]'
496
+
497
+ return res
498
+
499
+
500
+ def get_next_sequence_number(path, basename):
501
+ """
502
+ Determines and returns the next sequence number to use when saving an image in the specified directory.
503
+
504
+ The sequence starts at 0.
505
+ """
506
+ result = -1
507
+ if basename != '':
508
+ basename = f"{basename}-"
509
+
510
+ prefix_length = len(basename)
511
+ for p in os.listdir(path):
512
+ if p.startswith(basename):
513
+ parts = os.path.splitext(p[prefix_length:])[0].split('-') # splits the filename (removing the basename first if one is defined, so the sequence number is always the first element)
514
+ try:
515
+ result = max(int(parts[0]), result)
516
+ except ValueError:
517
+ pass
518
+
519
+ return result + 1
520
+
521
+
522
+ def save_image_with_geninfo(image, geninfo, filename, extension=None, existing_pnginfo=None, pnginfo_section_name='parameters'):
523
+ """
524
+ Saves image to filename, including geninfo as text information for generation info.
525
+ For PNG images, geninfo is added to existing pnginfo dictionary using the pnginfo_section_name argument as key.
526
+ For JPG images, there's no dictionary and geninfo just replaces the EXIF description.
527
+ """
528
+
529
+ if extension is None:
530
+ extension = os.path.splitext(filename)[1]
531
+
532
+ image_format = Image.registered_extensions()[extension]
533
+
534
+ if extension.lower() == '.png':
535
+ existing_pnginfo = existing_pnginfo or {}
536
+ if opts.enable_pnginfo:
537
+ existing_pnginfo[pnginfo_section_name] = geninfo
538
+
539
+ if opts.enable_pnginfo:
540
+ pnginfo_data = PngImagePlugin.PngInfo()
541
+ for k, v in (existing_pnginfo or {}).items():
542
+ pnginfo_data.add_text(k, str(v))
543
+ else:
544
+ pnginfo_data = None
545
+
546
+ image.save(filename, format=image_format, quality=opts.jpeg_quality, pnginfo=pnginfo_data)
547
+
548
+ elif extension.lower() in (".jpg", ".jpeg", ".webp"):
549
+ if image.mode == 'RGBA':
550
+ image = image.convert("RGB")
551
+ elif image.mode == 'I;16':
552
+ image = image.point(lambda p: p * 0.0038910505836576).convert("RGB" if extension.lower() == ".webp" else "L")
553
+
554
+ image.save(filename, format=image_format, quality=opts.jpeg_quality, lossless=opts.webp_lossless)
555
+
556
+ if opts.enable_pnginfo and geninfo is not None:
557
+ exif_bytes = piexif.dump({
558
+ "Exif": {
559
+ piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(geninfo or "", encoding="unicode")
560
+ },
561
+ })
562
+
563
+ piexif.insert(exif_bytes, filename)
564
+ else:
565
+ image.save(filename, format=image_format, quality=opts.jpeg_quality)
566
+
567
+
568
+ def save_image(image, path, basename, seed=None, prompt=None, extension='png', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None):
569
+ """Save an image.
570
+
571
+ Args:
572
+ image (`PIL.Image`):
573
+ The image to be saved.
574
+ path (`str`):
575
+ The directory to save the image. Note, the option `save_to_dirs` will make the image to be saved into a sub directory.
576
+ basename (`str`):
577
+ The base filename which will be applied to `filename pattern`.
578
+ seed, prompt, short_filename,
579
+ extension (`str`):
580
+ Image file extension, default is `png`.
581
+ pngsectionname (`str`):
582
+ Specify the name of the section which `info` will be saved in.
583
+ info (`str` or `PngImagePlugin.iTXt`):
584
+ PNG info chunks.
585
+ existing_info (`dict`):
586
+ Additional PNG info. `existing_info == {pngsectionname: info, ...}`
587
+ no_prompt:
588
+ TODO I don't know its meaning.
589
+ p (`StableDiffusionProcessing`)
590
+ forced_filename (`str`):
591
+ If specified, `basename` and filename pattern will be ignored.
592
+ save_to_dirs (bool):
593
+ If true, the image will be saved into a subdirectory of `path`.
594
+
595
+ Returns: (fullfn, txt_fullfn)
596
+ fullfn (`str`):
597
+ The full path of the saved imaged.
598
+ txt_fullfn (`str` or None):
599
+ If a text file is saved for this image, this will be its full path. Otherwise None.
600
+ """
601
+ namegen = FilenameGenerator(p, seed, prompt, image)
602
+
603
+ # WebP and JPG formats have maximum dimension limits of 16383 and 65535 respectively. switch to PNG which has a much higher limit
604
+ if (image.height > 65535 or image.width > 65535) and extension.lower() in ("jpg", "jpeg") or (image.height > 16383 or image.width > 16383) and extension.lower() == "webp":
605
+ print('Image dimensions too large; saving as PNG')
606
+ extension = ".png"
607
+
608
+ if save_to_dirs is None:
609
+ save_to_dirs = (grid and opts.grid_save_to_dirs) or (not grid and opts.save_to_dirs and not no_prompt)
610
+
611
+ if save_to_dirs:
612
+ dirname = namegen.apply(opts.directories_filename_pattern or "[prompt_words]").lstrip(' ').rstrip('\\ /')
613
+ path = os.path.join(path, dirname)
614
+
615
+ os.makedirs(path, exist_ok=True)
616
+
617
+ if forced_filename is None:
618
+ if short_filename or seed is None:
619
+ file_decoration = ""
620
+ elif opts.save_to_dirs:
621
+ file_decoration = opts.samples_filename_pattern or "[seed]"
622
+ else:
623
+ file_decoration = opts.samples_filename_pattern or "[seed]-[prompt_spaces]"
624
+
625
+ file_decoration = namegen.apply(file_decoration) + suffix
626
+
627
+ add_number = opts.save_images_add_number or file_decoration == ''
628
+
629
+ if file_decoration != "" and add_number:
630
+ file_decoration = f"-{file_decoration}"
631
+
632
+ if add_number:
633
+ basecount = get_next_sequence_number(path, basename)
634
+ fullfn = None
635
+ for i in range(500):
636
+ fn = f"{basecount + i:05}" if basename == '' else f"{basename}-{basecount + i:04}"
637
+ fullfn = os.path.join(path, f"{fn}{file_decoration}.{extension}")
638
+ if not os.path.exists(fullfn):
639
+ break
640
+ else:
641
+ fullfn = os.path.join(path, f"{file_decoration}.{extension}")
642
+ else:
643
+ fullfn = os.path.join(path, f"{forced_filename}.{extension}")
644
+
645
+ pnginfo = existing_info or {}
646
+ if info is not None:
647
+ pnginfo[pnginfo_section_name] = info
648
+
649
+ params = script_callbacks.ImageSaveParams(image, p, fullfn, pnginfo)
650
+ script_callbacks.before_image_saved_callback(params)
651
+
652
+ image = params.image
653
+ fullfn = params.filename
654
+ info = params.pnginfo.get(pnginfo_section_name, None)
655
+
656
+ def _atomically_save_image(image_to_save, filename_without_extension, extension):
657
+ """
658
+ save image with .tmp extension to avoid race condition when another process detects new image in the directory
659
+ """
660
+ temp_file_path = f"{filename_without_extension}.tmp"
661
+
662
+ save_image_with_geninfo(image_to_save, info, temp_file_path, extension, existing_pnginfo=params.pnginfo, pnginfo_section_name=pnginfo_section_name)
663
+
664
+ os.replace(temp_file_path, filename_without_extension + extension)
665
+
666
+ fullfn_without_extension, extension = os.path.splitext(params.filename)
667
+ if hasattr(os, 'statvfs'):
668
+ max_name_len = os.statvfs(path).f_namemax
669
+ fullfn_without_extension = fullfn_without_extension[:max_name_len - max(4, len(extension))]
670
+ params.filename = fullfn_without_extension + extension
671
+ fullfn = params.filename
672
+ _atomically_save_image(image, fullfn_without_extension, extension)
673
+
674
+ image.already_saved_as = fullfn
675
+
676
+ oversize = image.width > opts.target_side_length or image.height > opts.target_side_length
677
+ if opts.export_for_4chan and (oversize or os.stat(fullfn).st_size > opts.img_downscale_threshold * 1024 * 1024):
678
+ ratio = image.width / image.height
679
+ resize_to = None
680
+ if oversize and ratio > 1:
681
+ resize_to = round(opts.target_side_length), round(image.height * opts.target_side_length / image.width)
682
+ elif oversize:
683
+ resize_to = round(image.width * opts.target_side_length / image.height), round(opts.target_side_length)
684
+
685
+ if resize_to is not None:
686
+ try:
687
+ # Resizing image with LANCZOS could throw an exception if e.g. image mode is I;16
688
+ image = image.resize(resize_to, LANCZOS)
689
+ except Exception:
690
+ image = image.resize(resize_to)
691
+ try:
692
+ _atomically_save_image(image, fullfn_without_extension, ".jpg")
693
+ except Exception as e:
694
+ errors.display(e, "saving image as downscaled JPG")
695
+
696
+ if opts.save_txt and info is not None:
697
+ txt_fullfn = f"{fullfn_without_extension}.txt"
698
+ with open(txt_fullfn, "w", encoding="utf8") as file:
699
+ file.write(f"{info}\n")
700
+ else:
701
+ txt_fullfn = None
702
+
703
+ script_callbacks.image_saved_callback(params)
704
+
705
+ return fullfn, txt_fullfn
706
+
707
+
708
+ IGNORED_INFO_KEYS = {
709
+ 'jfif', 'jfif_version', 'jfif_unit', 'jfif_density', 'dpi', 'exif',
710
+ 'loop', 'background', 'timestamp', 'duration', 'progressive', 'progression',
711
+ 'icc_profile', 'chromaticity', 'photoshop',
712
+ }
713
+
714
+
715
+ def read_info_from_image(image: Image.Image) -> tuple[str | None, dict]:
716
+ items = (image.info or {}).copy()
717
+
718
+ geninfo = items.pop('parameters', None)
719
+
720
+ if "exif" in items:
721
+ exif = piexif.load(items["exif"])
722
+ exif_comment = (exif or {}).get("Exif", {}).get(piexif.ExifIFD.UserComment, b'')
723
+ try:
724
+ exif_comment = piexif.helper.UserComment.load(exif_comment)
725
+ except ValueError:
726
+ exif_comment = exif_comment.decode('utf8', errors="ignore")
727
+
728
+ if exif_comment:
729
+ items['exif comment'] = exif_comment
730
+ geninfo = exif_comment
731
+
732
+ for field in IGNORED_INFO_KEYS:
733
+ items.pop(field, None)
734
+
735
+ if items.get("Software", None) == "NovelAI":
736
+ try:
737
+ json_info = json.loads(items["Comment"])
738
+ sampler = sd_samplers.samplers_map.get(json_info["sampler"], "Euler a")
739
+
740
+ geninfo = f"""{items["Description"]}
741
+ Negative prompt: {json_info["uc"]}
742
+ Steps: {json_info["steps"]}, Sampler: {sampler}, CFG scale: {json_info["scale"]}, Seed: {json_info["seed"]}, Size: {image.width}x{image.height}, Clip skip: 2, ENSD: 31337"""
743
+ except Exception:
744
+ errors.report("Error parsing NovelAI image generation parameters", exc_info=True)
745
+
746
+ return geninfo, items
747
+
748
+
749
+ def image_data(data):
750
+ import gradio as gr
751
+
752
+ try:
753
+ image = Image.open(io.BytesIO(data))
754
+ textinfo, _ = read_info_from_image(image)
755
+ return textinfo, None
756
+ except Exception:
757
+ pass
758
+
759
+ try:
760
+ text = data.decode('utf8')
761
+ assert len(text) < 10000
762
+ return text, None
763
+
764
+ except Exception:
765
+ pass
766
+
767
+ return gr.update(), None
768
+
769
+
770
+ def flatten(img, bgcolor):
771
+ """replaces transparency with bgcolor (example: "#ffffff"), returning an RGB mode image with no transparency"""
772
+
773
+ if img.mode == "RGBA":
774
+ background = Image.new('RGBA', img.size, bgcolor)
775
+ background.paste(img, mask=img)
776
+ img = background
777
+
778
+ return img.convert('RGB')
modules/scripts.py ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import sys
4
+ import inspect
5
+ from collections import namedtuple
6
+ from dataclasses import dataclass
7
+
8
+ import gradio as gr
9
+
10
+ from modules import shared, paths, script_callbacks, extensions, script_loading, scripts_postprocessing, errors, timer
11
+
12
+ AlwaysVisible = object()
13
+
14
+
15
+ class PostprocessImageArgs:
16
+ def __init__(self, image):
17
+ self.image = image
18
+
19
+
20
+ class PostprocessBatchListArgs:
21
+ def __init__(self, images):
22
+ self.images = images
23
+
24
+
25
+ @dataclass
26
+ class OnComponent:
27
+ component: gr.blocks.Block
28
+
29
+
30
+ class Script:
31
+ name = None
32
+ """script's internal name derived from title"""
33
+
34
+ section = None
35
+ """name of UI section that the script's controls will be placed into"""
36
+
37
+ filename = None
38
+ args_from = None
39
+ args_to = None
40
+ alwayson = False
41
+
42
+ is_txt2img = False
43
+ is_img2img = False
44
+ tabname = None
45
+
46
+ group = None
47
+ """A gr.Group component that has all script's UI inside it."""
48
+
49
+ create_group = True
50
+ """If False, for alwayson scripts, a group component will not be created."""
51
+
52
+ infotext_fields = None
53
+ """if set in ui(), this is a list of pairs of gradio component + text; the text will be used when
54
+ parsing infotext to set the value for the component; see ui.py's txt2img_paste_fields for an example
55
+ """
56
+
57
+ paste_field_names = None
58
+ """if set in ui(), this is a list of names of infotext fields; the fields will be sent through the
59
+ various "Send to <X>" buttons when clicked
60
+ """
61
+
62
+ api_info = None
63
+ """Generated value of type modules.api.models.ScriptInfo with information about the script for API"""
64
+
65
+ on_before_component_elem_id = None
66
+ """list of callbacks to be called before a component with an elem_id is created"""
67
+
68
+ on_after_component_elem_id = None
69
+ """list of callbacks to be called after a component with an elem_id is created"""
70
+
71
+ setup_for_ui_only = False
72
+ """If true, the script setup will only be run in Gradio UI, not in API"""
73
+
74
+ def title(self):
75
+ """this function should return the title of the script. This is what will be displayed in the dropdown menu."""
76
+
77
+ raise NotImplementedError()
78
+
79
+ def ui(self, is_img2img):
80
+ """this function should create gradio UI elements. See https://gradio.app/docs/#components
81
+ The return value should be an array of all components that are used in processing.
82
+ Values of those returned components will be passed to run() and process() functions.
83
+ """
84
+
85
+ pass
86
+
87
+ def show(self, is_img2img):
88
+ """
89
+ is_img2img is True if this function is called for the img2img interface, and Fasle otherwise
90
+
91
+ This function should return:
92
+ - False if the script should not be shown in UI at all
93
+ - True if the script should be shown in UI if it's selected in the scripts dropdown
94
+ - script.AlwaysVisible if the script should be shown in UI at all times
95
+ """
96
+
97
+ return True
98
+
99
+ def run(self, p, *args):
100
+ """
101
+ This function is called if the script has been selected in the script dropdown.
102
+ It must do all processing and return the Processed object with results, same as
103
+ one returned by processing.process_images.
104
+
105
+ Usually the processing is done by calling the processing.process_images function.
106
+
107
+ args contains all values returned by components from ui()
108
+ """
109
+
110
+ pass
111
+
112
+ def setup(self, p, *args):
113
+ """For AlwaysVisible scripts, this function is called when the processing object is set up, before any processing starts.
114
+ args contains all values returned by components from ui().
115
+ """
116
+ pass
117
+
118
+
119
+ def before_process(self, p, *args):
120
+ """
121
+ This function is called very early during processing begins for AlwaysVisible scripts.
122
+ You can modify the processing object (p) here, inject hooks, etc.
123
+ args contains all values returned by components from ui()
124
+ """
125
+
126
+ pass
127
+
128
+ def process(self, p, *args):
129
+ """
130
+ This function is called before processing begins for AlwaysVisible scripts.
131
+ You can modify the processing object (p) here, inject hooks, etc.
132
+ args contains all values returned by components from ui()
133
+ """
134
+
135
+ pass
136
+
137
+ def before_process_batch(self, p, *args, **kwargs):
138
+ """
139
+ Called before extra networks are parsed from the prompt, so you can add
140
+ new extra network keywords to the prompt with this callback.
141
+
142
+ **kwargs will have those items:
143
+ - batch_number - index of current batch, from 0 to number of batches-1
144
+ - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things
145
+ - seeds - list of seeds for current batch
146
+ - subseeds - list of subseeds for current batch
147
+ """
148
+
149
+ pass
150
+
151
+ def after_extra_networks_activate(self, p, *args, **kwargs):
152
+ """
153
+ Called after extra networks activation, before conds calculation
154
+ allow modification of the network after extra networks activation been applied
155
+ won't be call if p.disable_extra_networks
156
+
157
+ **kwargs will have those items:
158
+ - batch_number - index of current batch, from 0 to number of batches-1
159
+ - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things
160
+ - seeds - list of seeds for current batch
161
+ - subseeds - list of subseeds for current batch
162
+ - extra_network_data - list of ExtraNetworkParams for current stage
163
+ """
164
+ pass
165
+
166
+ def process_batch(self, p, *args, **kwargs):
167
+ """
168
+ Same as process(), but called for every batch.
169
+
170
+ **kwargs will have those items:
171
+ - batch_number - index of current batch, from 0 to number of batches-1
172
+ - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things
173
+ - seeds - list of seeds for current batch
174
+ - subseeds - list of subseeds for current batch
175
+ """
176
+
177
+ pass
178
+
179
+ def postprocess_batch(self, p, *args, **kwargs):
180
+ """
181
+ Same as process_batch(), but called for every batch after it has been generated.
182
+
183
+ **kwargs will have same items as process_batch, and also:
184
+ - batch_number - index of current batch, from 0 to number of batches-1
185
+ - images - torch tensor with all generated images, with values ranging from 0 to 1;
186
+ """
187
+
188
+ pass
189
+
190
+ def postprocess_batch_list(self, p, pp: PostprocessBatchListArgs, *args, **kwargs):
191
+ """
192
+ Same as postprocess_batch(), but receives batch images as a list of 3D tensors instead of a 4D tensor.
193
+ This is useful when you want to update the entire batch instead of individual images.
194
+
195
+ You can modify the postprocessing object (pp) to update the images in the batch, remove images, add images, etc.
196
+ If the number of images is different from the batch size when returning,
197
+ then the script has the responsibility to also update the following attributes in the processing object (p):
198
+ - p.prompts
199
+ - p.negative_prompts
200
+ - p.seeds
201
+ - p.subseeds
202
+
203
+ **kwargs will have same items as process_batch, and also:
204
+ - batch_number - index of current batch, from 0 to number of batches-1
205
+ """
206
+
207
+ pass
208
+
209
+ def postprocess_image(self, p, pp: PostprocessImageArgs, *args):
210
+ """
211
+ Called for every image after it has been generated.
212
+ """
213
+
214
+ pass
215
+
216
+ def postprocess(self, p, processed, *args):
217
+ """
218
+ This function is called after processing ends for AlwaysVisible scripts.
219
+ args contains all values returned by components from ui()
220
+ """
221
+
222
+ pass
223
+
224
+ def before_component(self, component, **kwargs):
225
+ """
226
+ Called before a component is created.
227
+ Use elem_id/label fields of kwargs to figure out which component it is.
228
+ This can be useful to inject your own components somewhere in the middle of vanilla UI.
229
+ You can return created components in the ui() function to add them to the list of arguments for your processing functions
230
+ """
231
+
232
+ pass
233
+
234
+ def after_component(self, component, **kwargs):
235
+ """
236
+ Called after a component is created. Same as above.
237
+ """
238
+
239
+ pass
240
+
241
+ def on_before_component(self, callback, *, elem_id):
242
+ """
243
+ Calls callback before a component is created. The callback function is called with a single argument of type OnComponent.
244
+
245
+ May be called in show() or ui() - but it may be too late in latter as some components may already be created.
246
+
247
+ This function is an alternative to before_component in that it also cllows to run before a component is created, but
248
+ it doesn't require to be called for every created component - just for the one you need.
249
+ """
250
+ if self.on_before_component_elem_id is None:
251
+ self.on_before_component_elem_id = []
252
+
253
+ self.on_before_component_elem_id.append((elem_id, callback))
254
+
255
+ def on_after_component(self, callback, *, elem_id):
256
+ """
257
+ Calls callback after a component is created. The callback function is called with a single argument of type OnComponent.
258
+ """
259
+ if self.on_after_component_elem_id is None:
260
+ self.on_after_component_elem_id = []
261
+
262
+ self.on_after_component_elem_id.append((elem_id, callback))
263
+
264
+ def describe(self):
265
+ """unused"""
266
+ return ""
267
+
268
+ def elem_id(self, item_id):
269
+ """helper function to generate id for a HTML element, constructs final id out of script name, tab and user-supplied item_id"""
270
+
271
+ need_tabname = self.show(True) == self.show(False)
272
+ tabkind = 'img2img' if self.is_img2img else 'txt2img'
273
+ tabname = f"{tabkind}_" if need_tabname else ""
274
+ title = re.sub(r'[^a-z_0-9]', '', re.sub(r'\s', '_', self.title().lower()))
275
+
276
+ return f'script_{tabname}{title}_{item_id}'
277
+
278
+ def before_hr(self, p, *args):
279
+ """
280
+ This function is called before hires fix start.
281
+ """
282
+ pass
283
+
284
+
285
+ class ScriptBuiltinUI(Script):
286
+ setup_for_ui_only = True
287
+
288
+ def elem_id(self, item_id):
289
+ """helper function to generate id for a HTML element, constructs final id out of tab and user-supplied item_id"""
290
+
291
+ need_tabname = self.show(True) == self.show(False)
292
+ tabname = ('img2img' if self.is_img2img else 'txt2img') + "_" if need_tabname else ""
293
+
294
+ return f'{tabname}{item_id}'
295
+
296
+
297
+ current_basedir = paths.script_path
298
+
299
+
300
+ def basedir():
301
+ """returns the base directory for the current script. For scripts in the main scripts directory,
302
+ this is the main directory (where webui.py resides), and for scripts in extensions directory
303
+ (ie extensions/aesthetic/script/aesthetic.py), this is extension's directory (extensions/aesthetic)
304
+ """
305
+ return current_basedir
306
+
307
+
308
+ ScriptFile = namedtuple("ScriptFile", ["basedir", "filename", "path"])
309
+
310
+ scripts_data = []
311
+ postprocessing_scripts_data = []
312
+ ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedir", "module"])
313
+
314
+
315
+ def list_scripts(scriptdirname, extension, *, include_extensions=True):
316
+ scripts_list = []
317
+
318
+ basedir = os.path.join(paths.script_path, scriptdirname)
319
+ if os.path.exists(basedir):
320
+ for filename in sorted(os.listdir(basedir)):
321
+ scripts_list.append(ScriptFile(paths.script_path, filename, os.path.join(basedir, filename)))
322
+
323
+ if include_extensions:
324
+ for ext in extensions.active():
325
+ scripts_list += ext.list_files(scriptdirname, extension)
326
+
327
+ scripts_list = [x for x in scripts_list if os.path.splitext(x.path)[1].lower() == extension and os.path.isfile(x.path)]
328
+
329
+ return scripts_list
330
+
331
+
332
+ def list_files_with_name(filename):
333
+ res = []
334
+
335
+ dirs = [paths.script_path] + [ext.path for ext in extensions.active()]
336
+
337
+ for dirpath in dirs:
338
+ if not os.path.isdir(dirpath):
339
+ continue
340
+
341
+ path = os.path.join(dirpath, filename)
342
+ if os.path.isfile(path):
343
+ res.append(path)
344
+
345
+ return res
346
+
347
+
348
+ def load_scripts():
349
+ global current_basedir
350
+ scripts_data.clear()
351
+ postprocessing_scripts_data.clear()
352
+ script_callbacks.clear_callbacks()
353
+
354
+ scripts_list = list_scripts("scripts", ".py") + list_scripts("modules/processing_scripts", ".py", include_extensions=False)
355
+
356
+ syspath = sys.path
357
+
358
+ def register_scripts_from_module(module):
359
+ for script_class in module.__dict__.values():
360
+ if not inspect.isclass(script_class):
361
+ continue
362
+
363
+ if issubclass(script_class, Script):
364
+ scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir, module))
365
+ elif issubclass(script_class, scripts_postprocessing.ScriptPostprocessing):
366
+ postprocessing_scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir, module))
367
+
368
+ def orderby(basedir):
369
+ # 1st webui, 2nd extensions-builtin, 3rd extensions
370
+ priority = {os.path.join(paths.script_path, "extensions-builtin"):1, paths.script_path:0}
371
+ for key in priority:
372
+ if basedir.startswith(key):
373
+ return priority[key]
374
+ return 9999
375
+
376
+ for scriptfile in sorted(scripts_list, key=lambda x: [orderby(x.basedir), x]):
377
+ try:
378
+ if scriptfile.basedir != paths.script_path:
379
+ sys.path = [scriptfile.basedir] + sys.path
380
+ current_basedir = scriptfile.basedir
381
+
382
+ script_module = script_loading.load_module(scriptfile.path)
383
+ register_scripts_from_module(script_module)
384
+
385
+ except Exception:
386
+ errors.report(f"Error loading script: {scriptfile.filename}", exc_info=True)
387
+
388
+ finally:
389
+ sys.path = syspath
390
+ current_basedir = paths.script_path
391
+ timer.startup_timer.record(scriptfile.filename)
392
+
393
+ global scripts_txt2img, scripts_img2img, scripts_postproc
394
+
395
+ scripts_txt2img = ScriptRunner()
396
+ scripts_img2img = ScriptRunner()
397
+ scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner()
398
+
399
+
400
+ def wrap_call(func, filename, funcname, *args, default=None, **kwargs):
401
+ try:
402
+ return func(*args, **kwargs)
403
+ except Exception:
404
+ errors.report(f"Error calling: {filename}/{funcname}", exc_info=True)
405
+
406
+ return default
407
+
408
+
409
+ class ScriptRunner:
410
+ def __init__(self):
411
+ self.scripts = []
412
+ self.selectable_scripts = []
413
+ self.alwayson_scripts = []
414
+ self.titles = []
415
+ self.title_map = {}
416
+ self.infotext_fields = []
417
+ self.paste_field_names = []
418
+ self.inputs = [None]
419
+
420
+ self.on_before_component_elem_id = {}
421
+ """dict of callbacks to be called before an element is created; key=elem_id, value=list of callbacks"""
422
+
423
+ self.on_after_component_elem_id = {}
424
+ """dict of callbacks to be called after an element is created; key=elem_id, value=list of callbacks"""
425
+
426
+ def initialize_scripts(self, is_img2img):
427
+ from modules import scripts_auto_postprocessing
428
+
429
+ self.scripts.clear()
430
+ self.alwayson_scripts.clear()
431
+ self.selectable_scripts.clear()
432
+
433
+ auto_processing_scripts = scripts_auto_postprocessing.create_auto_preprocessing_script_data()
434
+
435
+ for script_data in auto_processing_scripts + scripts_data:
436
+ script = script_data.script_class()
437
+ script.filename = script_data.path
438
+ script.is_txt2img = not is_img2img
439
+ script.is_img2img = is_img2img
440
+ script.tabname = "img2img" if is_img2img else "txt2img"
441
+
442
+ visibility = script.show(script.is_img2img)
443
+
444
+ if visibility == AlwaysVisible:
445
+ self.scripts.append(script)
446
+ self.alwayson_scripts.append(script)
447
+ script.alwayson = True
448
+
449
+ elif visibility:
450
+ self.scripts.append(script)
451
+ self.selectable_scripts.append(script)
452
+
453
+ self.apply_on_before_component_callbacks()
454
+
455
+ def apply_on_before_component_callbacks(self):
456
+ for script in self.scripts:
457
+ on_before = script.on_before_component_elem_id or []
458
+ on_after = script.on_after_component_elem_id or []
459
+
460
+ for elem_id, callback in on_before:
461
+ if elem_id not in self.on_before_component_elem_id:
462
+ self.on_before_component_elem_id[elem_id] = []
463
+
464
+ self.on_before_component_elem_id[elem_id].append((callback, script))
465
+
466
+ for elem_id, callback in on_after:
467
+ if elem_id not in self.on_after_component_elem_id:
468
+ self.on_after_component_elem_id[elem_id] = []
469
+
470
+ self.on_after_component_elem_id[elem_id].append((callback, script))
471
+
472
+ on_before.clear()
473
+ on_after.clear()
474
+
475
+ def create_script_ui(self, script):
476
+ import modules.api.models as api_models
477
+
478
+ script.args_from = len(self.inputs)
479
+ script.args_to = len(self.inputs)
480
+
481
+ controls = wrap_call(script.ui, script.filename, "ui", script.is_img2img)
482
+
483
+ if controls is None:
484
+ return
485
+
486
+ script.name = wrap_call(script.title, script.filename, "title", default=script.filename).lower()
487
+ api_args = []
488
+
489
+ for control in controls:
490
+ control.custom_script_source = os.path.basename(script.filename)
491
+
492
+ arg_info = api_models.ScriptArg(label=control.label or "")
493
+
494
+ for field in ("value", "minimum", "maximum", "step", "choices"):
495
+ v = getattr(control, field, None)
496
+ if v is not None:
497
+ setattr(arg_info, field, v)
498
+
499
+ api_args.append(arg_info)
500
+
501
+ script.api_info = api_models.ScriptInfo(
502
+ name=script.name,
503
+ is_img2img=script.is_img2img,
504
+ is_alwayson=script.alwayson,
505
+ args=api_args,
506
+ )
507
+
508
+ if script.infotext_fields is not None:
509
+ self.infotext_fields += script.infotext_fields
510
+
511
+ if script.paste_field_names is not None:
512
+ self.paste_field_names += script.paste_field_names
513
+
514
+ self.inputs += controls
515
+ script.args_to = len(self.inputs)
516
+
517
+ def setup_ui_for_section(self, section, scriptlist=None):
518
+ if scriptlist is None:
519
+ scriptlist = self.alwayson_scripts
520
+
521
+ for script in scriptlist:
522
+ if script.alwayson and script.section != section:
523
+ continue
524
+
525
+ if script.create_group:
526
+ with gr.Group(visible=script.alwayson) as group:
527
+ self.create_script_ui(script)
528
+
529
+ script.group = group
530
+ else:
531
+ self.create_script_ui(script)
532
+
533
+ def prepare_ui(self):
534
+ self.inputs = [None]
535
+
536
+ def setup_ui(self):
537
+ all_titles = [wrap_call(script.title, script.filename, "title") or script.filename for script in self.scripts]
538
+ self.title_map = {title.lower(): script for title, script in zip(all_titles, self.scripts)}
539
+ self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts]
540
+
541
+ self.setup_ui_for_section(None)
542
+
543
+ dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index")
544
+ self.inputs[0] = dropdown
545
+
546
+ self.setup_ui_for_section(None, self.selectable_scripts)
547
+
548
+ def select_script(script_index):
549
+ selected_script = self.selectable_scripts[script_index - 1] if script_index>0 else None
550
+
551
+ return [gr.update(visible=selected_script == s) for s in self.selectable_scripts]
552
+
553
+ def init_field(title):
554
+ """called when an initial value is set from ui-config.json to show script's UI components"""
555
+
556
+ if title == 'None':
557
+ return
558
+
559
+ script_index = self.titles.index(title)
560
+ self.selectable_scripts[script_index].group.visible = True
561
+
562
+ dropdown.init_field = init_field
563
+
564
+ dropdown.change(
565
+ fn=select_script,
566
+ inputs=[dropdown],
567
+ outputs=[script.group for script in self.selectable_scripts]
568
+ )
569
+
570
+ self.script_load_ctr = 0
571
+
572
+ def onload_script_visibility(params):
573
+ title = params.get('Script', None)
574
+ if title:
575
+ title_index = self.titles.index(title)
576
+ visibility = title_index == self.script_load_ctr
577
+ self.script_load_ctr = (self.script_load_ctr + 1) % len(self.titles)
578
+ return gr.update(visible=visibility)
579
+ else:
580
+ return gr.update(visible=False)
581
+
582
+ self.infotext_fields.append((dropdown, lambda x: gr.update(value=x.get('Script', 'None'))))
583
+ self.infotext_fields.extend([(script.group, onload_script_visibility) for script in self.selectable_scripts])
584
+
585
+ self.apply_on_before_component_callbacks()
586
+
587
+ return self.inputs
588
+
589
+ def run(self, p, *args):
590
+ script_index = args[0]
591
+
592
+ if script_index == 0:
593
+ return None
594
+
595
+ script = self.selectable_scripts[script_index-1]
596
+
597
+ if script is None:
598
+ return None
599
+
600
+ script_args = args[script.args_from:script.args_to]
601
+ processed = script.run(p, *script_args)
602
+
603
+ shared.total_tqdm.clear()
604
+
605
+ return processed
606
+
607
+ def before_process(self, p):
608
+ for script in self.alwayson_scripts:
609
+ try:
610
+ script_args = p.script_args[script.args_from:script.args_to]
611
+ script.before_process(p, *script_args)
612
+ except Exception:
613
+ errors.report(f"Error running before_process: {script.filename}", exc_info=True)
614
+
615
+ def process(self, p):
616
+ for script in self.alwayson_scripts:
617
+ try:
618
+ script_args = p.script_args[script.args_from:script.args_to]
619
+ script.process(p, *script_args)
620
+ except Exception:
621
+ errors.report(f"Error running process: {script.filename}", exc_info=True)
622
+
623
+ def before_process_batch(self, p, **kwargs):
624
+ for script in self.alwayson_scripts:
625
+ try:
626
+ script_args = p.script_args[script.args_from:script.args_to]
627
+ script.before_process_batch(p, *script_args, **kwargs)
628
+ except Exception:
629
+ errors.report(f"Error running before_process_batch: {script.filename}", exc_info=True)
630
+
631
+ def after_extra_networks_activate(self, p, **kwargs):
632
+ for script in self.alwayson_scripts:
633
+ try:
634
+ script_args = p.script_args[script.args_from:script.args_to]
635
+ script.after_extra_networks_activate(p, *script_args, **kwargs)
636
+ except Exception:
637
+ errors.report(f"Error running after_extra_networks_activate: {script.filename}", exc_info=True)
638
+
639
+ def process_batch(self, p, **kwargs):
640
+ for script in self.alwayson_scripts:
641
+ try:
642
+ script_args = p.script_args[script.args_from:script.args_to]
643
+ script.process_batch(p, *script_args, **kwargs)
644
+ except Exception:
645
+ errors.report(f"Error running process_batch: {script.filename}", exc_info=True)
646
+
647
+ def postprocess(self, p, processed):
648
+ for script in self.alwayson_scripts:
649
+ try:
650
+ script_args = p.script_args[script.args_from:script.args_to]
651
+ script.postprocess(p, processed, *script_args)
652
+ except Exception:
653
+ errors.report(f"Error running postprocess: {script.filename}", exc_info=True)
654
+
655
+ def postprocess_batch(self, p, images, **kwargs):
656
+ for script in self.alwayson_scripts:
657
+ try:
658
+ script_args = p.script_args[script.args_from:script.args_to]
659
+ script.postprocess_batch(p, *script_args, images=images, **kwargs)
660
+ except Exception:
661
+ errors.report(f"Error running postprocess_batch: {script.filename}", exc_info=True)
662
+
663
+ def postprocess_batch_list(self, p, pp: PostprocessBatchListArgs, **kwargs):
664
+ for script in self.alwayson_scripts:
665
+ try:
666
+ script_args = p.script_args[script.args_from:script.args_to]
667
+ script.postprocess_batch_list(p, pp, *script_args, **kwargs)
668
+ except Exception:
669
+ errors.report(f"Error running postprocess_batch_list: {script.filename}", exc_info=True)
670
+
671
+ def postprocess_image(self, p, pp: PostprocessImageArgs):
672
+ for script in self.alwayson_scripts:
673
+ try:
674
+ script_args = p.script_args[script.args_from:script.args_to]
675
+ script.postprocess_image(p, pp, *script_args)
676
+ except Exception:
677
+ errors.report(f"Error running postprocess_image: {script.filename}", exc_info=True)
678
+
679
+ def before_component(self, component, **kwargs):
680
+ for callback, script in self.on_before_component_elem_id.get(kwargs.get("elem_id"), []):
681
+ try:
682
+ callback(OnComponent(component=component))
683
+ except Exception:
684
+ errors.report(f"Error running on_before_component: {script.filename}", exc_info=True)
685
+
686
+ for script in self.scripts:
687
+ try:
688
+ script.before_component(component, **kwargs)
689
+ except Exception:
690
+ errors.report(f"Error running before_component: {script.filename}", exc_info=True)
691
+
692
+ def after_component(self, component, **kwargs):
693
+ for callback, script in self.on_after_component_elem_id.get(component.elem_id, []):
694
+ try:
695
+ callback(OnComponent(component=component))
696
+ except Exception:
697
+ errors.report(f"Error running on_after_component: {script.filename}", exc_info=True)
698
+
699
+ for script in self.scripts:
700
+ try:
701
+ script.after_component(component, **kwargs)
702
+ except Exception:
703
+ errors.report(f"Error running after_component: {script.filename}", exc_info=True)
704
+
705
+ def script(self, title):
706
+ return self.title_map.get(title.lower())
707
+
708
+ def reload_sources(self, cache):
709
+ for si, script in list(enumerate(self.scripts)):
710
+ args_from = script.args_from
711
+ args_to = script.args_to
712
+ filename = script.filename
713
+
714
+ module = cache.get(filename, None)
715
+ if module is None:
716
+ module = script_loading.load_module(script.filename)
717
+ cache[filename] = module
718
+
719
+ for script_class in module.__dict__.values():
720
+ if type(script_class) == type and issubclass(script_class, Script):
721
+ self.scripts[si] = script_class()
722
+ self.scripts[si].filename = filename
723
+ self.scripts[si].args_from = args_from
724
+ self.scripts[si].args_to = args_to
725
+
726
+ def before_hr(self, p):
727
+ for script in self.alwayson_scripts:
728
+ try:
729
+ script_args = p.script_args[script.args_from:script.args_to]
730
+ script.before_hr(p, *script_args)
731
+ except Exception:
732
+ errors.report(f"Error running before_hr: {script.filename}", exc_info=True)
733
+
734
+ def setup_scrips(self, p, *, is_ui=True):
735
+ for script in self.alwayson_scripts:
736
+ if not is_ui and script.setup_for_ui_only:
737
+ continue
738
+
739
+ try:
740
+ script_args = p.script_args[script.args_from:script.args_to]
741
+ script.setup(p, *script_args)
742
+ except Exception:
743
+ errors.report(f"Error running setup: {script.filename}", exc_info=True)
744
+
745
+
746
+ scripts_txt2img: ScriptRunner = None
747
+ scripts_img2img: ScriptRunner = None
748
+ scripts_postproc: scripts_postprocessing.ScriptPostprocessingRunner = None
749
+ scripts_current: ScriptRunner = None
750
+
751
+
752
+ def reload_script_body_only():
753
+ cache = {}
754
+ scripts_txt2img.reload_sources(cache)
755
+ scripts_img2img.reload_sources(cache)
756
+
757
+
758
+ reload_scripts = load_scripts # compatibility alias
modules/shared.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ import gradio as gr
4
+
5
+ from modules import shared_cmd_options, shared_gradio_themes, options, shared_items, sd_models_types
6
+ from modules.paths_internal import models_path, script_path, data_path, sd_configs_path, sd_default_config, sd_model_file, default_sd_model_file, extensions_dir, extensions_builtin_dir # noqa: F401
7
+ from modules import util
8
+
9
+ cmd_opts = shared_cmd_options.cmd_opts
10
+ parser = shared_cmd_options.parser
11
+
12
+ batch_cond_uncond = True # old field, unused now in favor of shared.opts.batch_cond_uncond
13
+ parallel_processing_allowed = True
14
+ styles_filename = cmd_opts.styles_file
15
+ config_filename = cmd_opts.ui_settings_file
16
+ hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config}
17
+
18
+ demo = None
19
+
20
+ device = None
21
+
22
+ weight_load_location = None
23
+
24
+ xformers_available = False
25
+
26
+ hypernetworks = {}
27
+
28
+ loaded_hypernetworks = []
29
+
30
+ state = None
31
+
32
+ prompt_styles = None
33
+
34
+ interrogator = None
35
+
36
+ face_restorers = []
37
+
38
+ options_templates = None
39
+ opts = None
40
+ restricted_opts = None
41
+
42
+ sd_model: sd_models_types.WebuiSdModel = None
43
+
44
+ settings_components = None
45
+ """assinged from ui.py, a mapping on setting names to gradio components repsponsible for those settings"""
46
+
47
+ tab_names = []
48
+
49
+ latent_upscale_default_mode = "Latent"
50
+ latent_upscale_modes = {
51
+ "Latent": {"mode": "bilinear", "antialias": False},
52
+ "Latent (antialiased)": {"mode": "bilinear", "antialias": True},
53
+ "Latent (bicubic)": {"mode": "bicubic", "antialias": False},
54
+ "Latent (bicubic antialiased)": {"mode": "bicubic", "antialias": True},
55
+ "Latent (nearest)": {"mode": "nearest", "antialias": False},
56
+ "Latent (nearest-exact)": {"mode": "nearest-exact", "antialias": False},
57
+ }
58
+
59
+ sd_upscalers = []
60
+
61
+ clip_model = None
62
+
63
+ progress_print_out = sys.stdout
64
+
65
+ gradio_theme = gr.themes.Base()
66
+
67
+ total_tqdm = None
68
+
69
+ mem_mon = None
70
+
71
+ options_section = options.options_section
72
+ OptionInfo = options.OptionInfo
73
+ OptionHTML = options.OptionHTML
74
+
75
+ natural_sort_key = util.natural_sort_key
76
+ listfiles = util.listfiles
77
+ html_path = util.html_path
78
+ html = util.html
79
+ walk_files = util.walk_files
80
+ ldm_print = util.ldm_print
81
+
82
+ reload_gradio_theme = shared_gradio_themes.reload_gradio_theme
83
+
84
+ list_checkpoint_tiles = shared_items.list_checkpoint_tiles
85
+ refresh_checkpoints = shared_items.refresh_checkpoints
86
+ list_samplers = shared_items.list_samplers
87
+ reload_hypernetworks = shared_items.reload_hypernetworks