artificialguybr commited on
Commit
9c9eecf
·
1 Parent(s): abf8eed

Delete modules

Browse files
Files changed (3) hide show
  1. modules/images.py +0 -778
  2. modules/scripts.py +0 -758
  3. modules/shared.py +0 -87
modules/images.py DELETED
@@ -1,778 +0,0 @@
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 DELETED
@@ -1,758 +0,0 @@
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 DELETED
@@ -1,87 +0,0 @@
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