Merge branch 'AUTOMATIC1111:master' into master
This commit is contained in:
commit
47b298d58a
@ -66,8 +66,8 @@ titles = {
|
|||||||
|
|
||||||
"Interrogate": "Reconstruct prompt from existing image and put it into the prompt field.",
|
"Interrogate": "Reconstruct prompt from existing image and put it into the prompt field.",
|
||||||
|
|
||||||
"Images filename pattern": "Use following tags to define how filenames for images are chosen: [steps], [cfg], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
|
"Images filename pattern": "Use following tags to define how filenames for images are chosen: [steps], [cfg], [prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
|
||||||
"Directory name pattern": "Use following tags to define how subdirectories for images and grids are chosen: [steps], [cfg], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
|
"Directory name pattern": "Use following tags to define how subdirectories for images and grids are chosen: [steps], [cfg],[prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
|
||||||
"Max prompt words": "Set the maximum number of words to be used in the [prompt_words] option; ATTENTION: If the words are too long, they may exceed the maximum length of the file path that the system can handle",
|
"Max prompt words": "Set the maximum number of words to be used in the [prompt_words] option; ATTENTION: If the words are too long, they may exceed the maximum length of the file path that the system can handle",
|
||||||
|
|
||||||
"Loopback": "Process an image, use it as an input, repeat.",
|
"Loopback": "Process an image, use it as an input, repeat.",
|
||||||
|
@ -16,6 +16,7 @@ from PIL import Image, ImageFont, ImageDraw, PngImagePlugin
|
|||||||
from fonts.ttf import Roboto
|
from fonts.ttf import Roboto
|
||||||
import string
|
import string
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from modules import sd_samplers, shared, script_callbacks
|
from modules import sd_samplers, shared, script_callbacks
|
||||||
from modules.shared import opts, cmd_opts
|
from modules.shared import opts, cmd_opts
|
||||||
@ -198,7 +199,7 @@ def draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin=0):
|
|||||||
|
|
||||||
pad_top = 0 if sum(hor_text_heights) == 0 else max(hor_text_heights) + line_spacing * 2
|
pad_top = 0 if sum(hor_text_heights) == 0 else max(hor_text_heights) + line_spacing * 2
|
||||||
|
|
||||||
result = Image.new("RGB", (im.width + pad_left + margin * (rows-1), im.height + pad_top + margin * (cols-1)), "white")
|
result = Image.new("RGB", (im.width + pad_left + margin * (cols-1), im.height + pad_top + margin * (rows-1)), "white")
|
||||||
|
|
||||||
for row in range(rows):
|
for row in range(rows):
|
||||||
for col in range(cols):
|
for col in range(cols):
|
||||||
@ -222,7 +223,7 @@ def draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin=0):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def draw_prompt_matrix(im, width, height, all_prompts):
|
def draw_prompt_matrix(im, width, height, all_prompts, margin=0):
|
||||||
prompts = all_prompts[1:]
|
prompts = all_prompts[1:]
|
||||||
boundary = math.ceil(len(prompts) / 2)
|
boundary = math.ceil(len(prompts) / 2)
|
||||||
|
|
||||||
@ -232,7 +233,7 @@ def draw_prompt_matrix(im, width, height, all_prompts):
|
|||||||
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))]
|
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))]
|
||||||
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))]
|
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))]
|
||||||
|
|
||||||
return draw_grid_annotations(im, width, height, hor_texts, ver_texts)
|
return draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin)
|
||||||
|
|
||||||
|
|
||||||
def resize_image(resize_mode, im, width, height, upscaler_name=None):
|
def resize_image(resize_mode, im, width, height, upscaler_name=None):
|
||||||
@ -343,6 +344,7 @@ class FilenameGenerator:
|
|||||||
'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'),
|
'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'),
|
||||||
'datetime': lambda self, *args: self.datetime(*args), # accepts formats: [datetime], [datetime<Format>], [datetime<Format><Time Zone>]
|
'datetime': lambda self, *args: self.datetime(*args), # accepts formats: [datetime], [datetime<Format>], [datetime<Format><Time Zone>]
|
||||||
'job_timestamp': lambda self: getattr(self.p, "job_timestamp", shared.state.job_timestamp),
|
'job_timestamp': lambda self: getattr(self.p, "job_timestamp", shared.state.job_timestamp),
|
||||||
|
'prompt_hash': lambda self: hashlib.sha256(self.prompt.encode()).hexdigest()[0:8],
|
||||||
'prompt': lambda self: sanitize_filename_part(self.prompt),
|
'prompt': lambda self: sanitize_filename_part(self.prompt),
|
||||||
'prompt_no_styles': lambda self: self.prompt_no_style(),
|
'prompt_no_styles': lambda self: self.prompt_no_style(),
|
||||||
'prompt_spaces': lambda self: sanitize_filename_part(self.prompt, replace_spaces=False),
|
'prompt_spaces': lambda self: sanitize_filename_part(self.prompt, replace_spaces=False),
|
||||||
|
@ -45,6 +45,9 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None
|
|||||||
full_path = file
|
full_path = file
|
||||||
if os.path.isdir(full_path):
|
if os.path.isdir(full_path):
|
||||||
continue
|
continue
|
||||||
|
if os.path.islink(full_path) and not os.path.exists(full_path):
|
||||||
|
print(f"Skipping broken symlink: {full_path}")
|
||||||
|
continue
|
||||||
if ext_blacklist is not None and any([full_path.endswith(x) for x in ext_blacklist]):
|
if ext_blacklist is not None and any([full_path.endswith(x) for x in ext_blacklist]):
|
||||||
continue
|
continue
|
||||||
if len(ext_filter) != 0:
|
if len(ext_filter) != 0:
|
||||||
|
@ -20,8 +20,9 @@ class DisableInitialization:
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, disable_clip=True):
|
||||||
self.replaced = []
|
self.replaced = []
|
||||||
|
self.disable_clip = disable_clip
|
||||||
|
|
||||||
def replace(self, obj, field, func):
|
def replace(self, obj, field, func):
|
||||||
original = getattr(obj, field, None)
|
original = getattr(obj, field, None)
|
||||||
@ -75,12 +76,14 @@ class DisableInitialization:
|
|||||||
self.replace(torch.nn.init, 'kaiming_uniform_', do_nothing)
|
self.replace(torch.nn.init, 'kaiming_uniform_', do_nothing)
|
||||||
self.replace(torch.nn.init, '_no_grad_normal_', do_nothing)
|
self.replace(torch.nn.init, '_no_grad_normal_', do_nothing)
|
||||||
self.replace(torch.nn.init, '_no_grad_uniform_', do_nothing)
|
self.replace(torch.nn.init, '_no_grad_uniform_', do_nothing)
|
||||||
self.create_model_and_transforms = self.replace(open_clip, 'create_model_and_transforms', create_model_and_transforms_without_pretrained)
|
|
||||||
self.CLIPTextModel_from_pretrained = self.replace(ldm.modules.encoders.modules.CLIPTextModel, 'from_pretrained', CLIPTextModel_from_pretrained)
|
if self.disable_clip:
|
||||||
self.transformers_modeling_utils_load_pretrained_model = self.replace(transformers.modeling_utils.PreTrainedModel, '_load_pretrained_model', transformers_modeling_utils_load_pretrained_model)
|
self.create_model_and_transforms = self.replace(open_clip, 'create_model_and_transforms', create_model_and_transforms_without_pretrained)
|
||||||
self.transformers_tokenization_utils_base_cached_file = self.replace(transformers.tokenization_utils_base, 'cached_file', transformers_tokenization_utils_base_cached_file)
|
self.CLIPTextModel_from_pretrained = self.replace(ldm.modules.encoders.modules.CLIPTextModel, 'from_pretrained', CLIPTextModel_from_pretrained)
|
||||||
self.transformers_configuration_utils_cached_file = self.replace(transformers.configuration_utils, 'cached_file', transformers_configuration_utils_cached_file)
|
self.transformers_modeling_utils_load_pretrained_model = self.replace(transformers.modeling_utils.PreTrainedModel, '_load_pretrained_model', transformers_modeling_utils_load_pretrained_model)
|
||||||
self.transformers_utils_hub_get_from_cache = self.replace(transformers.utils.hub, 'get_from_cache', transformers_utils_hub_get_from_cache)
|
self.transformers_tokenization_utils_base_cached_file = self.replace(transformers.tokenization_utils_base, 'cached_file', transformers_tokenization_utils_base_cached_file)
|
||||||
|
self.transformers_configuration_utils_cached_file = self.replace(transformers.configuration_utils, 'cached_file', transformers_configuration_utils_cached_file)
|
||||||
|
self.transformers_utils_hub_get_from_cache = self.replace(transformers.utils.hub, 'get_from_cache', transformers_utils_hub_get_from_cache)
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
for obj, field, original in self.replaced:
|
for obj, field, original in self.replaced:
|
||||||
|
@ -354,6 +354,9 @@ def repair_config(sd_config):
|
|||||||
sd_config.model.params.unet_config.params.use_fp16 = True
|
sd_config.model.params.unet_config.params.use_fp16 = True
|
||||||
|
|
||||||
|
|
||||||
|
sd1_clip_weight = 'cond_stage_model.transformer.text_model.embeddings.token_embedding.weight'
|
||||||
|
sd2_clip_weight = 'cond_stage_model.model.transformer.resblocks.0.attn.in_proj_weight'
|
||||||
|
|
||||||
def load_model(checkpoint_info=None, already_loaded_state_dict=None, time_taken_to_load_state_dict=None):
|
def load_model(checkpoint_info=None, already_loaded_state_dict=None, time_taken_to_load_state_dict=None):
|
||||||
from modules import lowvram, sd_hijack
|
from modules import lowvram, sd_hijack
|
||||||
checkpoint_info = checkpoint_info or select_checkpoint()
|
checkpoint_info = checkpoint_info or select_checkpoint()
|
||||||
@ -374,6 +377,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None, time_taken_
|
|||||||
state_dict = get_checkpoint_state_dict(checkpoint_info, timer)
|
state_dict = get_checkpoint_state_dict(checkpoint_info, timer)
|
||||||
|
|
||||||
checkpoint_config = sd_models_config.find_checkpoint_config(state_dict, checkpoint_info)
|
checkpoint_config = sd_models_config.find_checkpoint_config(state_dict, checkpoint_info)
|
||||||
|
clip_is_included_into_sd = sd1_clip_weight in state_dict or sd2_clip_weight in state_dict
|
||||||
|
|
||||||
timer.record("find config")
|
timer.record("find config")
|
||||||
|
|
||||||
@ -386,7 +390,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None, time_taken_
|
|||||||
|
|
||||||
sd_model = None
|
sd_model = None
|
||||||
try:
|
try:
|
||||||
with sd_disable_initialization.DisableInitialization():
|
with sd_disable_initialization.DisableInitialization(disable_clip=clip_is_included_into_sd):
|
||||||
sd_model = instantiate_from_config(sd_config.model)
|
sd_model = instantiate_from_config(sd_config.model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
@ -26,7 +26,7 @@ def add_pages_to_demo(app):
|
|||||||
def fetch_file(filename: str = ""):
|
def fetch_file(filename: str = ""):
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
if not any([Path(x).resolve() in Path(filename).resolve().parents for x in allowed_dirs]):
|
if not any([Path(x).absolute() in Path(filename).absolute().parents for x in allowed_dirs]):
|
||||||
raise ValueError(f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages.")
|
raise ValueError(f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages.")
|
||||||
|
|
||||||
ext = os.path.splitext(filename)[1].lower()
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
@ -48,23 +48,17 @@ class Script(scripts.Script):
|
|||||||
gr.HTML('<br />')
|
gr.HTML('<br />')
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
put_at_start = gr.Checkbox(label='Put variable parts at start of prompt',
|
put_at_start = gr.Checkbox(label='Put variable parts at start of prompt', value=False, elem_id=self.elem_id("put_at_start"))
|
||||||
value=False, elem_id=self.elem_id("put_at_start"))
|
different_seeds = gr.Checkbox(label='Use different seed for each picture', value=False, elem_id=self.elem_id("different_seeds"))
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
# Radio buttons for selecting the prompt between positive and negative
|
prompt_type = gr.Radio(["positive", "negative"], label="Select prompt", elem_id=self.elem_id("prompt_type"), value="positive")
|
||||||
prompt_type = gr.Radio(["positive", "negative"], label="Select prompt",
|
variations_delimiter = gr.Radio(["comma", "space"], label="Select joining char", elem_id=self.elem_id("variations_delimiter"), value="comma")
|
||||||
elem_id=self.elem_id("prompt_type"), value="positive")
|
|
||||||
with gr.Row():
|
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
different_seeds = gr.Checkbox(
|
margin_size = gr.Slider(label="Grid margins (px)", min=0, max=500, value=0, step=2, elem_id=self.elem_id("margin_size"))
|
||||||
label='Use different seed for each picture', value=False, elem_id=self.elem_id("different_seeds"))
|
|
||||||
with gr.Column():
|
|
||||||
# Radio buttons for selecting the delimiter to use in the resulting prompt
|
|
||||||
variations_delimiter = gr.Radio(["comma", "space"], label="Select delimiter", elem_id=self.elem_id(
|
|
||||||
"variations_delimiter"), value="comma")
|
|
||||||
return [put_at_start, different_seeds, prompt_type, variations_delimiter]
|
|
||||||
|
|
||||||
def run(self, p, put_at_start, different_seeds, prompt_type, variations_delimiter):
|
return [put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size]
|
||||||
|
|
||||||
|
def run(self, p, put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size):
|
||||||
modules.processing.fix_seed(p)
|
modules.processing.fix_seed(p)
|
||||||
# Raise error if promp type is not positive or negative
|
# Raise error if promp type is not positive or negative
|
||||||
if prompt_type not in ["positive", "negative"]:
|
if prompt_type not in ["positive", "negative"]:
|
||||||
@ -106,7 +100,7 @@ class Script(scripts.Script):
|
|||||||
processed = process_images(p)
|
processed = process_images(p)
|
||||||
|
|
||||||
grid = images.image_grid(processed.images, p.batch_size, rows=1 << ((len(prompt_matrix_parts) - 1) // 2))
|
grid = images.image_grid(processed.images, p.batch_size, rows=1 << ((len(prompt_matrix_parts) - 1) // 2))
|
||||||
grid = images.draw_prompt_matrix(grid, p.width, p.height, prompt_matrix_parts)
|
grid = images.draw_prompt_matrix(grid, p.width, p.height, prompt_matrix_parts, margin_size)
|
||||||
processed.images.insert(0, grid)
|
processed.images.insert(0, grid)
|
||||||
processed.index_of_first_image = 1
|
processed.index_of_first_image = 1
|
||||||
processed.infotexts.insert(0, processed.infotexts[0])
|
processed.infotexts.insert(0, processed.infotexts[0])
|
||||||
|
@ -205,7 +205,7 @@ axis_options = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed):
|
def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size):
|
||||||
hor_texts = [[images.GridAnnotation(x)] for x in x_labels]
|
hor_texts = [[images.GridAnnotation(x)] for x in x_labels]
|
||||||
ver_texts = [[images.GridAnnotation(y)] for y in y_labels]
|
ver_texts = [[images.GridAnnotation(y)] for y in y_labels]
|
||||||
title_texts = [[images.GridAnnotation(z)] for z in z_labels]
|
title_texts = [[images.GridAnnotation(z)] for z in z_labels]
|
||||||
@ -292,7 +292,7 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend
|
|||||||
end_index = start_index + len(xs) * len(ys)
|
end_index = start_index + len(xs) * len(ys)
|
||||||
grid = images.image_grid(image_cache[start_index:end_index], rows=len(ys))
|
grid = images.image_grid(image_cache[start_index:end_index], rows=len(ys))
|
||||||
if draw_legend:
|
if draw_legend:
|
||||||
grid = images.draw_grid_annotations(grid, cell_size[0], cell_size[1], hor_texts, ver_texts)
|
grid = images.draw_grid_annotations(grid, cell_size[0], cell_size[1], hor_texts, ver_texts, margin_size)
|
||||||
sub_grids[i] = grid
|
sub_grids[i] = grid
|
||||||
if include_sub_grids and len(zs) > 1:
|
if include_sub_grids and len(zs) > 1:
|
||||||
processed_result.images.insert(i+1, grid)
|
processed_result.images.insert(i+1, grid)
|
||||||
@ -351,10 +351,16 @@ class Script(scripts.Script):
|
|||||||
fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False)
|
fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False)
|
||||||
|
|
||||||
with gr.Row(variant="compact", elem_id="axis_options"):
|
with gr.Row(variant="compact", elem_id="axis_options"):
|
||||||
draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend"))
|
with gr.Column():
|
||||||
include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images"))
|
draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend"))
|
||||||
include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids"))
|
no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds"))
|
||||||
no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds"))
|
with gr.Column():
|
||||||
|
include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images"))
|
||||||
|
include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids"))
|
||||||
|
with gr.Column():
|
||||||
|
margin_size = gr.Slider(label="Grid margins (px)", min=0, max=500, value=0, step=2, elem_id=self.elem_id("margin_size"))
|
||||||
|
|
||||||
|
with gr.Row(variant="compact", elem_id="swap_axes"):
|
||||||
swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button")
|
swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button")
|
||||||
swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button")
|
swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button")
|
||||||
swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button")
|
swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button")
|
||||||
@ -393,9 +399,9 @@ class Script(scripts.Script):
|
|||||||
(z_values, "Z Values"),
|
(z_values, "Z Values"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds]
|
return [x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size]
|
||||||
|
|
||||||
def run(self, p, x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds):
|
def run(self, p, x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size):
|
||||||
if not no_fixed_seeds:
|
if not no_fixed_seeds:
|
||||||
modules.processing.fix_seed(p)
|
modules.processing.fix_seed(p)
|
||||||
|
|
||||||
@ -590,7 +596,8 @@ class Script(scripts.Script):
|
|||||||
include_lone_images=include_lone_images,
|
include_lone_images=include_lone_images,
|
||||||
include_sub_grids=include_sub_grids,
|
include_sub_grids=include_sub_grids,
|
||||||
first_axes_processed=first_axes_processed,
|
first_axes_processed=first_axes_processed,
|
||||||
second_axes_processed=second_axes_processed
|
second_axes_processed=second_axes_processed,
|
||||||
|
margin_size=margin_size
|
||||||
)
|
)
|
||||||
|
|
||||||
if opts.grid_save and len(sub_grids) > 1:
|
if opts.grid_save and len(sub_grids) > 1:
|
||||||
|
Loading…
Reference in New Issue
Block a user