From adb2aa534066af796059d844d44a318304cad98d Mon Sep 17 00:00:00 2001 From: Ben Rugg Date: Sun, 27 Oct 2024 16:59:50 -0500 Subject: [PATCH] Fix upscaler for Stability API --- __init__.py | 2 +- config.py | 55 +++-- properties.py | 12 +- sd_backends/automatic1111_api.py | 218 ++++++++++++------- sd_backends/shark_api.py | 148 +++++++++---- sd_backends/stability_api.py | 36 +++- sd_backends/stablehorde_api.py | 125 ++++++++--- ui/ui_panels.py | 345 +++++++++++++++++++++++-------- 8 files changed, 686 insertions(+), 255 deletions(-) diff --git a/__init__.py b/__init__.py index fa12992..d9d99e8 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ "name": "AI Render - Stable Diffusion in Blender", "description": "Create amazing images using Stable Diffusion AI", "author": "Ben Rugg", - "version": (1, 0, 1), + "version": (1, 1, 0), "blender": (3, 0, 0), "location": "Render Properties > AI Render", "warning": "", diff --git a/config.py b/config.py index 921d204..3cb19f3 100644 --- a/config.py +++ b/config.py @@ -1,24 +1,51 @@ -package_name = 'AI-Render' -default_prompt_text = 'Describe anything you can imagine' +package_name = "AI-Render" +default_prompt_text = "Describe anything you can imagine" default_image_filename_template = "ai-render-{timestamp}-{prompt}" -filename_template_allowed_vars = ['timestamp', 'prompt', 'width', 'height', 'seed', 'steps', 'image_similarity', 'sampler', 'negative_prompt'] -tmp_path_subfolder = 'ai-render-temp' -animated_prompts_text_name = 'AI Render Animated Prompts' +filename_template_allowed_vars = [ + "timestamp", + "prompt", + "width", + "height", + "seed", + "steps", + "image_similarity", + "sampler", + "negative_prompt", +] +tmp_path_subfolder = "ai-render-temp" +animated_prompts_text_name = "AI Render Animated Prompts" ADDON_DOWNLOAD_URL = "https://blendermarket.com/products/ai-render" -STABILITY_API_URL = "https://api.stability.ai/v1/generation/" +STABILITY_API_V1_URL = "https://api.stability.ai/v1/generation/" +STABILITY_API_V2_URL = "https://api.stability.ai/v2beta/stable-image/" DREAM_STUDIO_URL = "https://dreamstudio.ai/account" STABLE_HORDE_API_URL_BASE = "https://stablehorde.net/api/v2" STABLE_HORDE_URL = "https://stablehorde.net/" VIDEO_TUTORIAL_URL = "https://www.youtube.com/watch?v=tmyln5bwnO8" -HELP_WITH_TIMEOUTS_URL = "https://github.com/benrugg/AI-Render/wiki/FAQ#%EF%B8%8F-ai-render-keeps-timing-out" -HELP_WITH_LOCAL_INSTALLATION_URL = "https://github.com/benrugg/AI-Render/wiki/Local-Installation" -HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL = "https://github.com/benrugg/AI-Render/wiki/Local-Installation#troubleshooting" -HELP_WITH_AUTOMATIC1111_UPSCALING_URL = "https://github.com/benrugg/AI-Render/wiki/Upscaling#upscaling-with-automatic1111" -HELP_WITH_ANIMATED_PROMPTS_URL = "https://github.com/benrugg/AI-Render/wiki/Animation#animated-prompts" -HELP_WITH_NEGATIVE_PROMPTS_URL = "https://github.com/benrugg/AI-Render/wiki/Animation#negative-prompts" +HELP_WITH_TIMEOUTS_URL = ( + "https://github.com/benrugg/AI-Render/wiki/FAQ#%EF%B8%8F-ai-render-keeps-timing-out" +) +HELP_WITH_LOCAL_INSTALLATION_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Local-Installation" +) +HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Local-Installation#troubleshooting" +) +HELP_WITH_AUTOMATIC1111_UPSCALING_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Upscaling#upscaling-with-automatic1111" +) +HELP_WITH_ANIMATED_PROMPTS_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Animation#animated-prompts" +) +HELP_WITH_NEGATIVE_PROMPTS_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Animation#negative-prompts" +) HELP_WITH_CONTROLNET_URL = "https://github.com/benrugg/AI-Render/wiki/ControlNet" -ANIMATION_TIPS_URL = "https://github.com/benrugg/AI-Render/wiki/Animation#animation-tips" +ANIMATION_TIPS_URL = ( + "https://github.com/benrugg/AI-Render/wiki/Animation#animation-tips" +) CONTRIBUTING_URL = "https://github.com/benrugg/AI-Render/blob/main/CONTRIBUTING.md" HELP_WITH_SHARK_INSTALLATION_URL = "https://github.com/AyaanShah2204/AI-Render/wiki/SHARK-by-nod.ai#running-shark-locally" -HELP_WITH_SHARK_TROUBLESHOOTING_URL = "https://github.com/AyaanShah2204/AI-Render/wiki/SHARK-by-nod.ai#troubleshooting" +HELP_WITH_SHARK_TROUBLESHOOTING_URL = ( + "https://github.com/AyaanShah2204/AI-Render/wiki/SHARK-by-nod.ai#troubleshooting" +) diff --git a/properties.py b/properties.py index 7cfc323..c7ad7d5 100644 --- a/properties.py +++ b/properties.py @@ -65,10 +65,20 @@ def ensure_upscaler_model(context): scene.air_props.upscaler_model = get_default_upscaler_model() +def ensure_upscaler_factor(context): + # """Ensure that the upscale factor is set to a valid value""" + scene = context.scene + if not utils.get_active_backend().supports_choosing_upscale_factor(): + scene.air_props.upscale_factor = ( + utils.get_active_backend().fixed_upscale_factor() + ) + + def ensure_properties(self, context): # """Ensure that any properties which could change with a change in preferences are set to valid values""" ensure_sampler(context) ensure_upscaler_model(context) + ensure_upscaler_factor(context) class AIRProperties(bpy.types.PropertyGroup): @@ -203,7 +213,7 @@ class AIRProperties(bpy.types.PropertyGroup): ) do_upscale_automatically: bpy.props.BoolProperty( name="Upscale Automatically", - default=True, + default=False, description="When true, will automatically upscale the image after each render", ) upscaler_model: bpy.props.EnumProperty( diff --git a/sd_backends/automatic1111_api.py b/sd_backends/automatic1111_api.py index 5f75cbb..a3047db 100644 --- a/sd_backends/automatic1111_api.py +++ b/sd_backends/automatic1111_api.py @@ -10,13 +10,16 @@ # CORE FUNCTIONS: + def generate(params, img_file, filename_prefix, props): # map the generic params to the specific ones for the Automatic1111 API map_params(params) # add a base 64 encoded image to the params - params["init_images"] = ["data:image/png;base64," + base64.b64encode(img_file.read()).decode()] + params["init_images"] = [ + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ] img_file.close() # add args for ControlNet if it's enabled @@ -26,15 +29,18 @@ def generate(params, img_file, filename_prefix, props): controlnet_weight = props.controlnet_weight if not controlnet_model: - return operators.handle_error(f"No ContolNet model selected. Either choose a new model or disable ControlNet. [Get help]({config.HELP_WITH_CONTROLNET_URL})", "controlnet_model_missing") + return operators.handle_error( + f"No ContolNet model selected. Either choose a new model or disable ControlNet. [Get help]({config.HELP_WITH_CONTROLNET_URL})", + "controlnet_model_missing", + ) params["alwayson_scripts"] = { "controlnet": { "args": [ { - "weight": controlnet_weight, - "module": controlnet_module, - "model": controlnet_model + "weight": controlnet_weight, + "module": controlnet_module, + "model": controlnet_model, } ] } @@ -44,7 +50,10 @@ def generate(params, img_file, filename_prefix, props): try: server_url = get_server_url("/sdapi/v1/img2img") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", + "local_server_url_missing", + ) # send the API request response = do_post(server_url, params) @@ -70,7 +79,9 @@ def upscale(img_file, filename_prefix, props): "codeformer_weight": 0, "upscaling_resize": props.upscale_factor, "upscaling_resize_w": utils.sanitized_upscaled_width(max_upscaled_image_size()), - "upscaling_resize_h": utils.sanitized_upscaled_height(max_upscaled_image_size()), + "upscaling_resize_h": utils.sanitized_upscaled_height( + max_upscaled_image_size() + ), "upscaling_crop": True, "upscaler_1": props.upscaler_model, "upscaler_2": "None", @@ -79,14 +90,19 @@ def upscale(img_file, filename_prefix, props): } # add a base 64 encoded image to the params - data["image"] = "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + data["image"] = ( + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ) img_file.close() # prepare the server url try: server_url = get_server_url("/sdapi/v1/extra-single-image") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", + "local_server_url_missing", + ) # send the API request response = do_post(server_url, data) @@ -113,28 +129,35 @@ def handle_success(response, filename_prefix): except: print("Automatic1111 response content: ") print(response.content) - return operators.handle_error("Received an unexpected response from the Automatic1111 Stable Diffusion server.", "unexpected_response") + return operators.handle_error( + "Received an unexpected response from the Automatic1111 Stable Diffusion server.", + "unexpected_response", + ) # create a temp file try: output_file = utils.create_temp_file(filename_prefix + "-") except: - return operators.handle_error("Couldn't create a temp file to save image.", "temp_file") + return operators.handle_error( + "Couldn't create a temp file to save image.", "temp_file" + ) # decode base64 image try: img_binary = base64.b64decode(base64_img.replace("data:image/png;base64,", "")) except: - return operators.handle_error("Couldn't decode base64 image from the Automatic1111 Stable Diffusion server.", "base64_decode") + return operators.handle_error( + "Couldn't decode base64 image from the Automatic1111 Stable Diffusion server.", + "base64_decode", + ) # save the image to the temp file try: - with open(output_file, 'wb') as file: + with open(output_file, "wb") as file: file.write(img_binary) except: return operators.handle_error("Couldn't write to temp file.", "temp_file_write") - # return the temp file return output_file @@ -145,21 +168,40 @@ def handle_error(response): try: response_obj = response.json() - if response_obj.get('detail') and response_obj['detail'] == "Not Found": - return operators.handle_error(f"It looks like the Automatic1111 server is running, but it's not in API mode. [Get help]({config.HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL})", "automatic1111_not_in_api_mode") - elif response_obj.get('detail') and response_obj['detail'] == "Sampler not found": - return operators.handle_error("The sampler you selected is not available on the Automatic1111 Stable Diffusion server. Please select a different sampler.", "invalid_sampler") + if response_obj.get("detail") and response_obj["detail"] == "Not Found": + return operators.handle_error( + f"It looks like the Automatic1111 server is running, but it's not in API mode. [Get help]({config.HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL})", + "automatic1111_not_in_api_mode", + ) + elif ( + response_obj.get("detail") + and response_obj["detail"] == "Sampler not found" + ): + return operators.handle_error( + "The sampler you selected is not available on the Automatic1111 Stable Diffusion server. Please select a different sampler.", + "invalid_sampler", + ) else: - return operators.handle_error(f"An error occurred in the Automatic1111 Stable Diffusion server. Full server response: {json.dumps(response_obj)}", "unknown_error") + return operators.handle_error( + f"An error occurred in the Automatic1111 Stable Diffusion server. Full server response: {json.dumps(response_obj)}", + "unknown_error", + ) except: - return operators.handle_error(f"It looks like the Automatic1111 server is running, but it's not in API mode. [Get help]({config.HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL})", "automatic1111_not_in_api_mode") + return operators.handle_error( + f"It looks like the Automatic1111 server is running, but it's not in API mode. [Get help]({config.HELP_WITH_AUTOMATIC1111_NOT_IN_API_MODE_URL})", + "automatic1111_not_in_api_mode", + ) else: - return operators.handle_error("An error occurred in the Automatic1111 Stable Diffusion server. Check the server logs for more info.", "unknown_error_response") + return operators.handle_error( + "An error occurred in the Automatic1111 Stable Diffusion server. Check the server logs for more info.", + "unknown_error_response", + ) # PRIVATE SUPPORT FUNCTIONS: + def create_headers(): return { "User-Agent": f"Blender/{bpy.app.version_string}", @@ -184,13 +226,24 @@ def map_params(params): def do_post(url, data): # send the API request try: - return requests.post(url, json=data, headers=create_headers(), timeout=utils.local_sd_timeout()) + return requests.post( + url, json=data, headers=create_headers(), timeout=utils.local_sd_timeout() + ) except requests.exceptions.ConnectionError: - return operators.handle_error(f"The local Stable Diffusion server couldn't be found. It's either not running, or it's running at a different location than what you specified in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", "local_server_not_found") + return operators.handle_error( + f"The local Stable Diffusion server couldn't be found. It's either not running, or it's running at a different location than what you specified in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", + "local_server_not_found", + ) except requests.exceptions.MissingSchema: - return operators.handle_error(f"The url for your local Stable Diffusion server is invalid. Please set it correctly in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", "local_server_url_invalid") + return operators.handle_error( + f"The url for your local Stable Diffusion server is invalid. Please set it correctly in the add-on preferences. [Get help]({config.HELP_WITH_LOCAL_INSTALLATION_URL})", + "local_server_url_invalid", + ) except requests.exceptions.ReadTimeout: - return operators.handle_error("The local Stable Diffusion server timed out. Set a longer timeout in AI Render preferences, or use a smaller image size.", "timeout") + return operators.handle_error( + "The local Stable Diffusion server timed out. Set a longer timeout in AI Render preferences, or use a smaller image size.", + "timeout", + ) def debug_log(response): @@ -209,52 +262,53 @@ def debug_log(response): # PUBLIC SUPPORT FUNCTIONS: + def get_samplers(): # NOTE: Keep the number values (fourth item in the tuples) in sync with DreamStudio's # values (in stability_api.py). These act like an internal unique ID for Blender # to use when switching between the lists. return [ - ('Euler', 'Euler', '', 10), - ('Euler a', 'Euler a', '', 20), - ('Heun', 'Heun', '', 30), - ('DPM2', 'DPM2', '', 40), - ('DPM2 a', 'DPM2 a', '', 50), - ('DPM2 Karras', 'DPM2 Karras', '', 52), - ('DPM2 a Karras', 'DPM2 a Karras', '', 55), - ('LMS', 'LMS', '', 60), - ('LMS Karras', 'LMS Karras', '', 65), - ('DPM fast', 'DPM fast', '', 70), - ('DPM adaptive', 'DPM adaptive', '', 80), - ('DPM++ 2S a Karras', 'DPM++ 2S a Karras', '', 90), - ('DPM++ 2M Karras', 'DPM++ 2M Karras', '', 100), - ('DPM++ SDE Karras', 'DPM++ SDE Karras', '', 105), - ('DPM++ 2S a', 'DPM++ 2S a', '', 110), - ('DPM++ 2M', 'DPM++ 2M', '', 120), - ('DPM++ SDE', 'DPM++ SDE', '', 125), - ('DPM++ 2M SDE', 'DPM++ 2M SDE', '', 130), - ('DPM++ 2M SDE Karras', 'DPM++ 2M SDE Karras', '', 135), - ('DPM++ 2M SDE Exponential', 'DPM++ 2M SDE Exponential', '', 140), - ('DPM++ 2M SDE Heun', 'DPM++ 2M SDE Heun', '', 145), - ('DPM++ 2M SDE Heun Karras', 'DPM++ 2M SDE Heun Karras', '', 150), - ('DPM++ 2M SDE Heun Exponential', 'DPM++ 2M SDE Heun Exponential', '', 155), - ('DPM++ 3M SDE', 'DPM++ 3M SDE', '', 160), - ('DPM++ 3M SDE Karras', 'DPM++ 3M SDE Karras', '', 165), - ('DPM++ 3M SDE Exponential', 'DPM++ 3M SDE Exponential', '', 170), - ('Restart', 'Restart', '', 190), - ('PLMS', 'PLMS', '', 200), - ('DDIM', 'DDIM', '', 210), - ('UniPC', 'UniPC', '', 250), + ("Euler", "Euler", "", 10), + ("Euler a", "Euler a", "", 20), + ("Heun", "Heun", "", 30), + ("DPM2", "DPM2", "", 40), + ("DPM2 a", "DPM2 a", "", 50), + ("DPM2 Karras", "DPM2 Karras", "", 52), + ("DPM2 a Karras", "DPM2 a Karras", "", 55), + ("LMS", "LMS", "", 60), + ("LMS Karras", "LMS Karras", "", 65), + ("DPM fast", "DPM fast", "", 70), + ("DPM adaptive", "DPM adaptive", "", 80), + ("DPM++ 2S a Karras", "DPM++ 2S a Karras", "", 90), + ("DPM++ 2M Karras", "DPM++ 2M Karras", "", 100), + ("DPM++ SDE Karras", "DPM++ SDE Karras", "", 105), + ("DPM++ 2S a", "DPM++ 2S a", "", 110), + ("DPM++ 2M", "DPM++ 2M", "", 120), + ("DPM++ SDE", "DPM++ SDE", "", 125), + ("DPM++ 2M SDE", "DPM++ 2M SDE", "", 130), + ("DPM++ 2M SDE Karras", "DPM++ 2M SDE Karras", "", 135), + ("DPM++ 2M SDE Exponential", "DPM++ 2M SDE Exponential", "", 140), + ("DPM++ 2M SDE Heun", "DPM++ 2M SDE Heun", "", 145), + ("DPM++ 2M SDE Heun Karras", "DPM++ 2M SDE Heun Karras", "", 150), + ("DPM++ 2M SDE Heun Exponential", "DPM++ 2M SDE Heun Exponential", "", 155), + ("DPM++ 3M SDE", "DPM++ 3M SDE", "", 160), + ("DPM++ 3M SDE Karras", "DPM++ 3M SDE Karras", "", 165), + ("DPM++ 3M SDE Exponential", "DPM++ 3M SDE Exponential", "", 170), + ("Restart", "Restart", "", 190), + ("PLMS", "PLMS", "", 200), + ("DDIM", "DDIM", "", 210), + ("UniPC", "UniPC", "", 250), ] def default_sampler(): - return 'LMS' + return "LMS" def get_upscaler_models(context): models = context.scene.air_props.automatic1111_available_upscaler_models - if (not models): + if not models: return [] else: enum_list = [] @@ -270,11 +324,11 @@ def is_upscaler_model_list_loaded(context=None): def default_upscaler_model(): - return 'ESRGAN_4x' + return "ESRGAN_4x" def get_image_format(): - return 'PNG' + return "PNG" def supports_negative_prompts(): @@ -289,10 +343,18 @@ def supports_upscaling(): return True +def supports_choosing_upscaler_model(): + return True + + def supports_reloading_upscaler_models(): return True +def supports_choosing_upscale_factor(): + return True + + def supports_inpainting(): return False @@ -323,7 +385,7 @@ def is_using_sdxl_1024_model(props): def get_available_controlnet_models(context): models = context.scene.air_props.controlnet_available_models - if (not models): + if not models: return [] else: enum_list = [] @@ -335,7 +397,7 @@ def get_available_controlnet_models(context): def get_available_controlnet_modules(context): modules = context.scene.air_props.controlnet_available_modules - if (not modules): + if not modules: return [] else: enum_list = [] @@ -352,7 +414,7 @@ def choose_controlnet_defaults(context): return # priority order for models and modules - priority_order = ['depth', 'openpose', 'normal', 'canny', 'scribble'] + priority_order = ["depth", "openpose", "normal", "canny", "scribble"] # choose a matching model and module in the priority order: for item in priority_order: @@ -382,7 +444,7 @@ def load_upscaler_models(context): # get the list of available upscaler models from the Automatic1111 api server_url = get_server_url("/sdapi/v1/upscalers") - headers = { "Accept": "application/json" } + headers = {"Accept": "application/json"} response = requests.get(server_url, headers=headers, timeout=5) response_obj = response.json() print("Upscaler models returned from Automatic1111 API:") @@ -390,14 +452,18 @@ def load_upscaler_models(context): # store the list of models in the scene properties if not response_obj: - return operators.handle_error(f"No upscaler models are installed in Automatic1111. [Get help]({config.HELP_WITH_AUTOMATIC1111_UPSCALING_URL})") + return operators.handle_error( + f"No upscaler models are installed in Automatic1111. [Get help]({config.HELP_WITH_AUTOMATIC1111_UPSCALING_URL})" + ) else: # map the response object to a list of model names upscaler_models = [] for model in response_obj: - if (model["name"] != "None"): + if model["name"] != "None": upscaler_models.append(model["name"]) - context.scene.air_props.automatic1111_available_upscaler_models = "||||".join(upscaler_models) + context.scene.air_props.automatic1111_available_upscaler_models = ( + "||||".join(upscaler_models) + ) # if the list of models was not already loaded, set the default model if not was_already_loaded: @@ -406,14 +472,16 @@ def load_upscaler_models(context): # return success return True except: - return operators.handle_error(f"Couldn't get the list of available upscaler models from the Automatic1111 server. [Get help]({config.HELP_WITH_AUTOMATIC1111_UPSCALING_URL})") + return operators.handle_error( + f"Couldn't get the list of available upscaler models from the Automatic1111 server. [Get help]({config.HELP_WITH_AUTOMATIC1111_UPSCALING_URL})" + ) def load_controlnet_models(context): try: # get the list of available controlnet models from the Automatic1111 api server_url = get_server_url("/controlnet/model_list") - headers = { "Accept": "application/json" } + headers = {"Accept": "application/json"} response = requests.get(server_url, headers=headers, timeout=5) response_obj = response.json() print("ControlNet models returned from Automatic1111 API:") @@ -422,19 +490,23 @@ def load_controlnet_models(context): # store the list of models in the scene properties models = response_obj["model_list"] if not models: - return operators.handle_error(f"You don't have any ControlNet models installed. You will need to download them from Hugging Face. [Get help]({config.HELP_WITH_CONTROLNET_URL})") + return operators.handle_error( + f"You don't have any ControlNet models installed. You will need to download them from Hugging Face. [Get help]({config.HELP_WITH_CONTROLNET_URL})" + ) else: context.scene.air_props.controlnet_available_models = "||||".join(models) return True except: - return operators.handle_error(f"Couldn't get the list of available ControlNet models from the Automatic1111 server. Make sure ControlNet is installed and activated. [Get help]({config.HELP_WITH_CONTROLNET_URL})") + return operators.handle_error( + f"Couldn't get the list of available ControlNet models from the Automatic1111 server. Make sure ControlNet is installed and activated. [Get help]({config.HELP_WITH_CONTROLNET_URL})" + ) def load_controlnet_modules(context): try: # get the list of available controlnet modules from the Automatic1111 api server_url = get_server_url("/controlnet/module_list") - headers = { "Accept": "application/json" } + headers = {"Accept": "application/json"} response = requests.get(server_url, headers=headers, timeout=5) response_obj = response.json() print("ControlNet modules returned from Automatic1111 API:") @@ -447,4 +519,6 @@ def load_controlnet_modules(context): context.scene.air_props.controlnet_available_modules = "||||".join(modules) return True except: - return operators.handle_error(f"Couldn't get the list of available ControlNet modules from the Automatic1111 server. Make sure ControlNet is installed and activated. [Get help]({config.HELP_WITH_CONTROLNET_URL})") + return operators.handle_error( + f"Couldn't get the list of available ControlNet modules from the Automatic1111 server. Make sure ControlNet is installed and activated. [Get help]({config.HELP_WITH_CONTROLNET_URL})" + ) diff --git a/sd_backends/shark_api.py b/sd_backends/shark_api.py index f1924b8..44c1aa1 100644 --- a/sd_backends/shark_api.py +++ b/sd_backends/shark_api.py @@ -11,20 +11,25 @@ # TODO: Controlnet Supprt # TODO: Support Model Choice + def generate(params, img_file, filename_prefix, props): # Configuring custom params for shark params["denoising_strength"] = round(1 - params["image_similarity"], 2) # add a base 64 encoded image to the params - params["init_images"] = ["data:image/png;base64," + - base64.b64encode(img_file.read()).decode()] + params["init_images"] = [ + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ] img_file.close() # get server url try: server_url = get_server_url("/sdapi/v1/img2img") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_url_missing", + ) # send the API request response = do_post(server_url, params) @@ -49,17 +54,21 @@ def upscale(img_file, filename_prefix, props): "width": utils.sanitized_upscaled_width(max_upscaled_image_size()), "steps": 50, "noise_level": 20, - "cfg_scale": 7 + "cfg_scale": 7, } - data["init_images"] = ["data:image/png;base64," + - base64.b64encode(img_file.read()).decode()] + data["init_images"] = [ + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ] img_file.close() try: server_url = get_server_url("/sdapi/v1/upscaler") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_url_missing", + ) response = do_post(server_url, data) @@ -74,16 +83,23 @@ def upscale(img_file, filename_prefix, props): def inpaint(params, img_file, mask_file, filename_prefix, props): - params["image"] = "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + params["image"] = ( + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ) img_file.close() - params["mask"] = "data:image/png;base64," + base64.b64encode(mask_file.read()).decode() + params["mask"] = ( + "data:image/png;base64," + base64.b64encode(mask_file.read()).decode() + ) mask_file.close() try: server_url = get_server_url("/sdapi/v1/inpaint") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_url_missing", + ) response = do_post(server_url, params) @@ -98,13 +114,18 @@ def inpaint(params, img_file, mask_file, filename_prefix, props): def outpaint(params, img_file, filename_prefix, props): - params["init_images"] = ["data:image/png;base64," + base64.b64encode(img_file.read()).decode()] + params["init_images"] = [ + "data:image/png;base64," + base64.b64encode(img_file.read()).decode() + ] img_file.close() try: server_url = get_server_url("/sdapi/v1/outpaint") except: - return operators.handle_error(f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_url_missing") + return operators.handle_error( + f"You need to specify a location for the local Stable Diffusion server in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_url_missing", + ) response = do_post(server_url, params) @@ -122,29 +143,35 @@ def handle_success(response, filename_prefix): # ensure we have the type of response we are expecting try: response_obj = response.json() - base64_img = response_obj.get("images", [False])[ - 0] or response_obj.get("image") + base64_img = response_obj.get("images", [False])[0] or response_obj.get("image") except: print("SHARK response content: ") print(response.content) - return operators.handle_error("Received an unexpected response from the Shark Stable Diffusion server.", "unexpected_response") + return operators.handle_error( + "Received an unexpected response from the Shark Stable Diffusion server.", + "unexpected_response", + ) # create a temp file try: output_file = utils.create_temp_file(filename_prefix + "-") except: - return operators.handle_error("Couldn't create a temp file to save image.", "temp_file") + return operators.handle_error( + "Couldn't create a temp file to save image.", "temp_file" + ) # decode base64 image try: - img_binary = base64.b64decode( - base64_img.replace("data:image/png;base64,", "")) + img_binary = base64.b64decode(base64_img.replace("data:image/png;base64,", "")) except: - return operators.handle_error("Couldn't decode base64 image from the Shark Stable Diffusion server.", "base64_decode") + return operators.handle_error( + "Couldn't decode base64 image from the Shark Stable Diffusion server.", + "base64_decode", + ) # save the image to the temp file try: - with open(output_file, 'wb') as file: + with open(output_file, "wb") as file: file.write(img_binary) except: return operators.handle_error("Couldn't write to temp file.", "temp_file_write") @@ -159,17 +186,35 @@ def handle_error(response): try: response_obj = response.json() - if response_obj.get('detail') and response_obj['detail'] == "Not Found": - return operators.handle_error(f"It looks like the SHARK server is running, but it's not in API mode. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", "automatic1111_not_in_api_mode") - elif response_obj.get('detail') and response_obj['detail'] == "Sampler not found": - return operators.handle_error("The sampler you selected is not available on the SHARK Stable Diffusion server. Please select a different sampler.", "invalid_sampler") + if response_obj.get("detail") and response_obj["detail"] == "Not Found": + return operators.handle_error( + f"It looks like the SHARK server is running, but it's not in API mode. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", + "automatic1111_not_in_api_mode", + ) + elif ( + response_obj.get("detail") + and response_obj["detail"] == "Sampler not found" + ): + return operators.handle_error( + "The sampler you selected is not available on the SHARK Stable Diffusion server. Please select a different sampler.", + "invalid_sampler", + ) else: - return operators.handle_error(f"An error occurred in the SHARK Stable Diffusion server. Full server response: {json.dumps(response_obj)}", "unknown_error") + return operators.handle_error( + f"An error occurred in the SHARK Stable Diffusion server. Full server response: {json.dumps(response_obj)}", + "unknown_error", + ) except: - return operators.handle_error(f"It looks like the SHARK server is running, but it's not in API mode. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", "automatic1111_not_in_api_mode") + return operators.handle_error( + f"It looks like the SHARK server is running, but it's not in API mode. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", + "automatic1111_not_in_api_mode", + ) else: - return operators.handle_error(f"An error occurred in the SHARK Stable Diffusion server. Check the server logs for more info, or check out the SHARK Troubleshooting guide. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", "unknown_error_response") + return operators.handle_error( + f"An error occurred in the SHARK Stable Diffusion server. Check the server logs for more info, or check out the SHARK Troubleshooting guide. [Get help]({config.HELP_WITH_SHARK_TROUBLESHOOTING_URL})", + "unknown_error_response", + ) def create_headers(): @@ -183,13 +228,24 @@ def create_headers(): def do_post(url, data): # send the API request try: - return requests.post(url, json=data, headers=create_headers(), timeout=utils.local_sd_timeout()) + return requests.post( + url, json=data, headers=create_headers(), timeout=utils.local_sd_timeout() + ) except requests.exceptions.ConnectionError: - return operators.handle_error(f"The local Stable Diffusion server couldn't be found. It's either not running, or it's running at a different location than what you specified in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_not_found") + return operators.handle_error( + f"The local Stable Diffusion server couldn't be found. It's either not running, or it's running at a different location than what you specified in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_not_found", + ) except requests.exceptions.MissingSchema: - return operators.handle_error(f"The url for your local Stable Diffusion server is invalid. Please set it correctly in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", "local_server_url_invalid") + return operators.handle_error( + f"The url for your local Stable Diffusion server is invalid. Please set it correctly in the add-on preferences. [Get help]({config.HELP_WITH_SHARK_INSTALLATION_URL})", + "local_server_url_invalid", + ) except requests.exceptions.ReadTimeout: - return operators.handle_error("The local Stable Diffusion server timed out. Set a longer timeout in AI Render preferences, or use a smaller image size.", "timeout") + return operators.handle_error( + "The local Stable Diffusion server timed out. Set a longer timeout in AI Render preferences, or use a smaller image size.", + "timeout", + ) def get_server_url(path): @@ -220,8 +276,24 @@ def supports_upscaling(): return True +def supports_choosing_upscaler_model(): + return False + + +def supports_reloading_upscaler_models(): + return False + + +def supports_choosing_upscale_factor(): + return False + + +def fixed_upscale_factor(): + return 2.0 + + def get_image_format(): - return 'PNG' + return "PNG" def supports_negative_prompts(): @@ -236,10 +308,6 @@ def is_upscaler_model_list_loaded(context=None): return True -def supports_reloading_upscaler_models(): - return False - - def supports_inpainting(): return True @@ -251,12 +319,12 @@ def supports_outpainting(): def get_upscaler_models(context): # NOTE: Shark does not look at model in API Req and defaults to stabilityai return [ - ('stabilityai/stable-diffusion-2-1-base', 'stabilityai', ''), + ("stabilityai/stable-diffusion-2-1-base", "stabilityai", ""), ] def default_upscaler_model(): - return 'stabilityai/stable-diffusion-2-1-base' + return "stabilityai/stable-diffusion-2-1-base" def get_samplers(): @@ -265,9 +333,9 @@ def get_samplers(): # to use when switching between the lists. # NOTE: Shark does not look at sampler in API Req and defaults to EulerDiscrete return [ - ('k_euler', 'Euler', '', 10), + ("k_euler", "Euler", "", 10), ] def default_sampler(): - return 'k_euler' + return "k_euler" diff --git a/sd_backends/stability_api.py b/sd_backends/stability_api.py index 897f645..b0d5517 100644 --- a/sd_backends/stability_api.py +++ b/sd_backends/stability_api.py @@ -23,7 +23,7 @@ def generate(params, img_file, filename_prefix, props): headers = create_headers() # prepare the URL (specifically setting the engine id) - api_url = f"{config.STABILITY_API_URL}{props.sd_model}/image-to-image" + api_url = f"{config.STABILITY_API_V1_URL}{props.sd_model}/image-to-image" # prepare the file input files = { @@ -62,7 +62,7 @@ def upscale(img_file, filename_prefix, props): headers = create_headers() # prepare the URL - api_url = f"{config.STABILITY_API_URL}{props.upscaler_model}/image-to-image/upscale" + api_url = f"{config.STABILITY_API_V2_URL}upscale/fast" # prepare the file input files = { @@ -70,7 +70,7 @@ def upscale(img_file, filename_prefix, props): } # prepare the params - data = {"width": utils.sanitized_upscaled_width(max_upscaled_image_size())} + data = {"output_format": get_image_format().lower()} # send the API request try: @@ -105,9 +105,17 @@ def handle_success(response, filename_prefix): ) try: - for i, image in enumerate(data["artifacts"]): + if "image" in data: with open(output_file, "wb") as file: - file.write(base64.b64decode(image["base64"])) + file.write(base64.b64decode(data["image"])) + elif "artifacts" in data: + for i, image in enumerate(data["artifacts"]): + with open(output_file, "wb") as file: + file.write(base64.b64decode(image["base64"])) + else: + return operators.handle_error( + f"DreamStudio returned an unexpected response", "unexpected_response" + ) return output_file except: @@ -284,7 +292,7 @@ def default_sampler(): def get_upscaler_models(context): return [ - ("esrgan-v1-x2plus", "ESRGAN X2+", ""), + ("fast", "fast", ""), ] @@ -293,7 +301,7 @@ def is_upscaler_model_list_loaded(context=None): def default_upscaler_model(): - return "esrgan-v1-x2plus" + return "fast" def request_timeout(): @@ -316,10 +324,22 @@ def supports_upscaling(): return True +def supports_choosing_upscaler_model(): + return False + + def supports_reloading_upscaler_models(): return False +def supports_choosing_upscale_factor(): + return False + + +def fixed_upscale_factor(): + return 4.0 + + def supports_inpainting(): return False @@ -337,7 +357,7 @@ def max_image_size(): def max_upscaled_image_size(): - return 2048 * 2048 + return 4096 * 4096 def is_using_sdxl_1024_model(props): diff --git a/sd_backends/stablehorde_api.py b/sd_backends/stablehorde_api.py index ca348c2..f56dd53 100644 --- a/sd_backends/stablehorde_api.py +++ b/sd_backends/stablehorde_api.py @@ -15,6 +15,7 @@ # CORE FUNCTIONS: + def generate(params, img_file, filename_prefix, props): # map the generic params to the specific ones for the Stable Horde API @@ -33,38 +34,56 @@ def generate(params, img_file, filename_prefix, props): start_time = time.monotonic() try: print(f"Sending request to Stable Horde API: {API_REQUEST_URL}") - response = requests.post(API_REQUEST_URL, json=stablehorde_params, headers=headers, timeout=20) + response = requests.post( + API_REQUEST_URL, json=stablehorde_params, headers=headers, timeout=20 + ) id = response.json()["id"] img_file.close() except requests.exceptions.ReadTimeout: img_file.close() - return operators.handle_error(f"There was an error sending this request to Stable Horde. Please try again in a moment.", "timeout") + return operators.handle_error( + f"There was an error sending this request to Stable Horde. Please try again in a moment.", + "timeout", + ) except Exception as e: img_file.close() - return operators.handle_error(f"Error with Stable Horde. Full error message: {e}", "unknown_error") + return operators.handle_error( + f"Error with Stable Horde. Full error message: {e}", "unknown_error" + ) # Check the status of the request (For at most request_timeout seconds) for i in range(request_timeout()): try: time.sleep(1) - URL=API_CHECK_URL + "/" + id + URL = API_CHECK_URL + "/" + id print(f"Checking status of request at Stable Horde API: {URL}") response = requests.get(URL, headers=headers, timeout=20) - print(f"Waiting for {str(time.monotonic() - start_time)}s. Response: {response.json()}") + print( + f"Waiting for {str(time.monotonic() - start_time)}s. Response: {response.json()}" + ) if response.json()["done"] == True: - print("The horde took " + str(time.monotonic() - start_time) + "s to imagine this frame.") + print( + "The horde took " + + str(time.monotonic() - start_time) + + "s to imagine this frame." + ) break except requests.exceptions.ReadTimeout: # Ignore timeouts print("WARN: Timeout while checking status") - except Exception as e: # Catch all other errors - return operators.handle_error(f"Error while checking status: {e}", "unknown_error") - if (i == request_timeout() - 1): - return operators.handle_error(f"Timeout generating image. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", "timeout") + except Exception as e: # Catch all other errors + return operators.handle_error( + f"Error while checking status: {e}", "unknown_error" + ) + if i == request_timeout() - 1: + return operators.handle_error( + f"Timeout generating image. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", + "timeout", + ) # Get the image try: - URL=API_GET_URL + "/" + id + URL = API_GET_URL + "/" + id print(f"Retrieving image from Stable Horde API: {URL}") response = requests.get(URL, headers=headers, timeout=20) # handle the response @@ -74,9 +93,14 @@ def generate(params, img_file, filename_prefix, props): return handle_error(response) except requests.exceptions.ReadTimeout: - return operators.handle_error(f"Timeout getting image from Stable Horde. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", "timeout") + return operators.handle_error( + f"Timeout getting image from Stable Horde. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", + "timeout", + ) except Exception as e: - return operators.handle_error(f"Error with Stable Horde. Full error message: {e}", "unknown_error") + return operators.handle_error( + f"Error with Stable Horde. Full error message: {e}", "unknown_error" + ) def handle_success(response, filename_prefix): @@ -85,18 +109,27 @@ def handle_success(response, filename_prefix): try: response_obj = response.json() img_url = response_obj["generations"][0]["img"] - print(f"Worker: {response_obj['generations'][0]['worker_name']}, " + - f"kudos: {response_obj['kudos']}") + print( + f"Worker: {response_obj['generations'][0]['worker_name']}, " + + f"kudos: {response_obj['kudos']}" + ) except: print("Stable Horde response content: ") print(response.content) - return operators.handle_error("Received an unexpected response from the Stable Horde server.", "unexpected_response") + return operators.handle_error( + "Received an unexpected response from the Stable Horde server.", + "unexpected_response", + ) # create a temp file try: - output_file = utils.create_temp_file(filename_prefix + "-", suffix=f".{get_image_format().lower()}") + output_file = utils.create_temp_file( + filename_prefix + "-", suffix=f".{get_image_format().lower()}" + ) except: - return operators.handle_error("Couldn't create a temp file to save image.", "temp_file") + return operators.handle_error( + "Couldn't create a temp file to save image.", "temp_file" + ) # Retrieve img from img_url and write it to the temp file img_binary = None @@ -105,11 +138,14 @@ def handle_success(response, filename_prefix): response = requests.get(img_url, timeout=20) img_binary = response.content except requests.exceptions.ReadTimeout: - return operators.handle_error(f"Timeout retrieving file. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", "timeout") + return operators.handle_error( + f"Timeout retrieving file. Try again in a moment, or get help. [Get help with timeouts]({config.HELP_WITH_TIMEOUTS_URL})", + "timeout", + ) # save the image to the temp file try: - with open(output_file, 'wb') as file: + with open(output_file, "wb") as file: file.write(img_binary) except: return operators.handle_error("Couldn't write to temp file.", "temp_file_write") @@ -119,21 +155,29 @@ def handle_success(response, filename_prefix): def handle_error(response): - return operators.handle_error("The Stable Horde server returned an error: " + str(response.content), "unknown_error") + return operators.handle_error( + "The Stable Horde server returned an error: " + str(response.content), + "unknown_error", + ) # PRIVATE SUPPORT FUNCTIONS: + def create_headers(): # if no api-key specified, use the default non-authenticated api-key - apikey = utils.get_stable_horde_api_key() if not utils.get_stable_horde_api_key().strip() == "" else "0000000000" + apikey = ( + utils.get_stable_horde_api_key() + if not utils.get_stable_horde_api_key().strip() == "" + else "0000000000" + ) # create the headers return { "User-Agent": f"Blender/{bpy.app.version_string}", "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", - "apikey": apikey + "apikey": apikey, } @@ -149,36 +193,37 @@ def map_params(params): "seed": str(params["seed"]), "steps": params["steps"], "sampler_name": params["sampler"], - } + }, } # PUBLIC SUPPORT FUNCTIONS: + def get_samplers(): # NOTE: Keep the number values (fourth item in the tuples) in sync with DreamStudio's # values (in stability_api.py). These act like an internal unique ID for Blender # to use when switching between the lists. return [ - ('k_euler', 'Euler', '', 10), - ('k_euler_a', 'Euler a', '', 20), - ('k_heun', 'Heun', '', 30), - ('k_dpm_2', 'DPM2', '', 40), - ('k_dpm_2_a', 'DPM2 a', '', 50), - ('k_lms', 'LMS', '', 60), + ("k_euler", "Euler", "", 10), + ("k_euler_a", "Euler a", "", 20), + ("k_heun", "Heun", "", 30), + ("k_dpm_2", "DPM2", "", 40), + ("k_dpm_2_a", "DPM2 a", "", 50), + ("k_lms", "LMS", "", 60), # TODO: Stable Horde does have karras support, but it's a separate boolean ] def default_sampler(): - return 'k_euler_a' + return "k_euler_a" def get_upscaler_models(context): # NOTE: Stable Horde does not support upscaling (at least as of the time of this writing), # but adding this here to keep the API consistent with other backends. return [ - ('esrgan-v1-x2plus', 'ESRGAN X2+', ''), + ("none", "none", ""), ] @@ -189,7 +234,7 @@ def is_upscaler_model_list_loaded(context=None): def default_upscaler_model(): - return 'esrgan-v1-x2plus' + return "none" def request_timeout(): @@ -197,7 +242,7 @@ def request_timeout(): def get_image_format(): - return 'WEBP' + return "WEBP" def supports_negative_prompts(): @@ -212,10 +257,22 @@ def supports_upscaling(): return False +def supports_choosing_upscaler_model(): + return False + + def supports_reloading_upscaler_models(): return False +def supports_choosing_upscale_factor(): + return False + + +def fixed_upscale_factor(): + return 2.0 + + def supports_inpainting(): return False diff --git a/ui/ui_panels.py b/ui/ui_panels.py index 18d446e..3da43de 100644 --- a/ui/ui_panels.py +++ b/ui/ui_panels.py @@ -10,7 +10,7 @@ def show_error_if_it_exists(layout, context, width_guess): props = context.scene.air_props - if (props.error_message): + if props.error_message: box = layout.box() row = box.row() @@ -49,7 +49,11 @@ def draw(self, context): row = layout.row() row.operator(operators.AIR_OT_enable.bl_idname) - utils.label_multiline(layout, text="Enable AI Render in this scene to start using Stable Diffusion", alignment="CENTER") + utils.label_multiline( + layout, + text="Enable AI Render in this scene to start using Stable Diffusion", + alignment="CENTER", + ) # Show updater if update is available addon_updater_ops.update_notice_box_ui(self, context) @@ -65,26 +69,39 @@ class AIR_PT_setup(bpy.types.Panel): @classmethod def is_api_key_valid(cls, context): - return utils.get_dream_studio_api_key(context) != '' and context.scene.air_props.error_key != 'api_key' + return ( + utils.get_dream_studio_api_key(context) != "" + and context.scene.air_props.error_key != "api_key" + ) @classmethod def has_dimensions_issue(cls, context): - return \ - not AIR_PT_setup.are_dimensions_valid(context) or \ - not AIR_PT_setup.are_dimensions_small_enough(context) or \ - not AIR_PT_setup.are_dimensions_large_enough(context) + return ( + not AIR_PT_setup.are_dimensions_valid(context) + or not AIR_PT_setup.are_dimensions_small_enough(context) + or not AIR_PT_setup.are_dimensions_large_enough(context) + ) @classmethod def are_dimensions_valid(cls, context): - return utils.are_dimensions_valid(context.scene) and context.scene.air_props.error_key != 'invalid_dimensions' + return ( + utils.are_dimensions_valid(context.scene) + and context.scene.air_props.error_key != "invalid_dimensions" + ) @classmethod def are_dimensions_small_enough(cls, context): - return not utils.are_dimensions_too_large(context.scene) and context.scene.air_props.error_key != 'dimensions_too_large' + return ( + not utils.are_dimensions_too_large(context.scene) + and context.scene.air_props.error_key != "dimensions_too_large" + ) @classmethod def are_dimensions_large_enough(cls, context): - return not utils.are_dimensions_too_small(context.scene) and context.scene.air_props.error_key != 'dimensions_too_small' + return ( + not utils.are_dimensions_too_small(context.scene) + and context.scene.air_props.error_key != "dimensions_too_small" + ) @classmethod def poll(cls, context): @@ -98,19 +115,35 @@ def draw(self, context): width_guess = 220 # if the api key is invalid, show the initial setup instructions - if not AIR_PT_setup.is_api_key_valid(context) and utils.sd_backend(context) == "dreamstudio": - - utils.label_multiline(layout, text="Setup is quick and easy. No downloads or installation. Just register for a DreamStudio API Key.", icon="INFO", width=width_guess) + if ( + not AIR_PT_setup.is_api_key_valid(context) + and utils.sd_backend(context) == "dreamstudio" + ): + + utils.label_multiline( + layout, + text="Setup is quick and easy. No downloads or installation. Just register for a DreamStudio API Key.", + icon="INFO", + width=width_guess, + ) row = layout.row() col = row.column() - col.operator(operators.AIR_OT_setup_instructions_popup.bl_idname, text="Instructions", icon="HELP") + col.operator( + operators.AIR_OT_setup_instructions_popup.bl_idname, + text="Instructions", + icon="HELP", + ) row = layout.row() - row.operator("wm.url_open", text="Watch Full Tutorial", icon="URL").url = config.VIDEO_TUTORIAL_URL + row.operator("wm.url_open", text="Watch Full Tutorial", icon="URL").url = ( + config.VIDEO_TUTORIAL_URL + ) row = layout.row() - row.operator("wm.url_open", text="Sign Up For DreamStudio (free)", icon="URL").url = config.DREAM_STUDIO_URL + row.operator( + "wm.url_open", text="Sign Up For DreamStudio (free)", icon="URL" + ).url = config.DREAM_STUDIO_URL row = layout.row() row.prop(utils.get_addon_preferences(context), "dream_studio_api_key") @@ -118,11 +151,26 @@ def draw(self, context): # show the image dimension help if the dimensions are invalid or too large elif AIR_PT_setup.has_dimensions_issue(context): if not AIR_PT_setup.are_dimensions_valid(context): - utils.label_multiline(layout, text="Adjust Image Size: \nStable Diffusion only works on certain image dimensions.", icon="INFO", width=width_guess) + utils.label_multiline( + layout, + text="Adjust Image Size: \nStable Diffusion only works on certain image dimensions.", + icon="INFO", + width=width_guess, + ) elif not AIR_PT_setup.are_dimensions_small_enough(context): - utils.label_multiline(layout, text=f"Adjust Image Size: \nImage dimensions are too large. Please decrease width and/or height. Total pixel area must be at most {round(utils.get_active_backend().max_image_size() / (1024*1024), 1)} megapixels.", icon="INFO", width=width_guess) + utils.label_multiline( + layout, + text=f"Adjust Image Size: \nImage dimensions are too large. Please decrease width and/or height. Total pixel area must be at most {round(utils.get_active_backend().max_image_size() / (1024*1024), 1)} megapixels.", + icon="INFO", + width=width_guess, + ) else: - utils.label_multiline(layout, text=f"Adjust Image Size: \nImage dimensions are too small. Please increase width and/or height. Total pixel area must be at least {round(utils.get_active_backend().min_image_size() / (1024*1024), 1)} megapixels.", icon="INFO", width=width_guess) + utils.label_multiline( + layout, + text=f"Adjust Image Size: \nImage dimensions are too small. Please increase width and/or height. Total pixel area must be at least {round(utils.get_active_backend().min_image_size() / (1024*1024), 1)} megapixels.", + icon="INFO", + width=width_guess, + ) layout.separator() @@ -134,26 +182,52 @@ def draw(self, context): col.operator(operators.AIR_OT_set_image_size_to_1024x1024.bl_idname) col = row.column() if utils.is_using_sdxl_1024_model(scene): - col.operator(operators.AIR_OT_show_dimension_options_for_sdxl_1024.bl_idname, text="Other") + col.operator( + operators.AIR_OT_show_dimension_options_for_sdxl_1024.bl_idname, + text="Other", + ) else: - col.operator(operators.AIR_OT_show_other_dimension_options.bl_idname, text="Other") - - if utils.get_active_backend().supports_upscaling() and props.do_upscale_automatically: + col.operator( + operators.AIR_OT_show_other_dimension_options.bl_idname, + text="Other", + ) + + if ( + utils.get_active_backend().supports_upscaling() + and props.do_upscale_automatically + ): layout.separator() box = layout.box() - utils.label_multiline(box, text=f"Final image will be upscaled {round(props.upscale_factor)}x larger than these initial dimensions.", width=width_guess-20) + utils.label_multiline( + box, + text=f"Final image will be upscaled {round(props.upscale_factor)}x larger than these initial dimensions.", + width=width_guess - 20, + ) # else, show the ready / getting started message and disable and change image size buttons else: - utils.label_multiline(layout, text="You're ready to start rendering!", width=width_guess, alignment="CENTER") + utils.label_multiline( + layout, + text="You're ready to start rendering!", + width=width_guess, + alignment="CENTER", + ) row = layout.row() - row.operator("wm.url_open", text="Help Getting Started", icon="URL").url = config.VIDEO_TUTORIAL_URL + row.operator("wm.url_open", text="Help Getting Started", icon="URL").url = ( + config.VIDEO_TUTORIAL_URL + ) row = layout.row(align=True) if utils.is_using_sdxl_1024_model(scene): - row.operator(operators.AIR_OT_show_dimension_options_for_sdxl_1024.bl_idname, text="Change Image Size") + row.operator( + operators.AIR_OT_show_dimension_options_for_sdxl_1024.bl_idname, + text="Change Image Size", + ) else: - row.operator(operators.AIR_OT_show_other_dimension_options.bl_idname, text="Change Image Size") + row.operator( + operators.AIR_OT_show_other_dimension_options.bl_idname, + text="Change Image Size", + ) row.separator() row.operator(operators.AIR_OT_disable.bl_idname, text="Disable AI Render") @@ -212,19 +286,27 @@ def draw(self, context): # Preset Styles box = layout.box() row = box.row() - label = "Apply a Preset Style (to All Prompts)" if props.use_animated_prompts else "Apply a Preset Style" + label = ( + "Apply a Preset Style (to All Prompts)" + if props.use_animated_prompts + else "Apply a Preset Style" + ) row.prop(props, "use_preset", text=label) if props.use_preset: row = box.row() - row.template_icon_view(props, "preset_style", show_labels=True, scale_popup=7.7) + row.template_icon_view( + props, "preset_style", show_labels=True, scale_popup=7.7 + ) row = box.row() col = row.column() - col.label(text=f"\"{props.preset_style}\"") + col.label(text=f'"{props.preset_style}"') col = row.column() - col.operator(operators.AIR_OT_copy_preset_text.bl_idname, text="", icon="COPYDOWN") + col.operator( + operators.AIR_OT_copy_preset_text.bl_idname, text="", icon="COPYDOWN" + ) class AIR_PT_advanced_options(bpy.types.Panel): @@ -234,7 +316,7 @@ class AIR_PT_advanced_options(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -248,10 +330,10 @@ def draw(self, context): # Seed row = layout.row() sub = row.column() - sub.prop(props, 'use_random_seed') + sub.prop(props, "use_random_seed") sub = row.column() - sub.prop(props, 'seed', text="", slider=False) + sub.prop(props, "seed", text="", slider=False) sub.enabled = not props.use_random_seed # Image Similarity @@ -259,21 +341,21 @@ def draw(self, context): sub = row.column() sub.label(text="Image Similarity") sub = row.column() - sub.prop(props, 'image_similarity', text="", slider=False) + sub.prop(props, "image_similarity", text="", slider=False) # Steps row = layout.row() sub = row.column() sub.label(text="Steps") sub = row.column() - sub.prop(props, 'steps', text="", slider=False) + sub.prop(props, "steps", text="", slider=False) # Prompt Strength row = layout.row() sub = row.column() sub.label(text="Prompt Strength") sub = row.column() - sub.prop(props, 'cfg_scale', text="", slider=False) + sub.prop(props, "cfg_scale", text="", slider=False) # SD Model if utils.get_active_backend().supports_choosing_model(): @@ -281,14 +363,14 @@ def draw(self, context): sub = row.column() sub.label(text="Model") sub = row.column() - sub.prop(props, 'sd_model', text="") + sub.prop(props, "sd_model", text="") # Sampler row = layout.row() sub = row.column() sub.label(text="Sampler") sub = row.column() - sub.prop(props, 'sampler', text="") + sub.prop(props, "sampler", text="") class AIR_PT_controlnet(bpy.types.Panel): @@ -298,11 +380,15 @@ class AIR_PT_controlnet(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return utils.is_installation_valid() and context.scene.air_props.is_enabled and utils.sd_backend(context) == "automatic1111" + return ( + utils.is_installation_valid() + and context.scene.air_props.is_enabled + and utils.sd_backend(context) == "automatic1111" + ) def draw(self, context): layout = self.layout @@ -319,21 +405,31 @@ def draw(self, context): split = row.split(align=True) split.prop(props, "controlnet_close_help", text="", icon="X", emboss=False) - utils.label_multiline(box, text="ControlNet is an extension for Automatic1111 that provides a spectacular ability to match scene details - layout, objects, poses - while recreating the scene in Stable Diffusion. It can also create much more stable animations than standard Stable Diffusion.", width=width_guess) + utils.label_multiline( + box, + text="ControlNet is an extension for Automatic1111 that provides a spectacular ability to match scene details - layout, objects, poses - while recreating the scene in Stable Diffusion. It can also create much more stable animations than standard Stable Diffusion.", + width=width_guess, + ) row = box.row() - row.operator("wm.url_open", text="Learn More", icon="URL").url = config.HELP_WITH_CONTROLNET_URL + row.operator("wm.url_open", text="Learn More", icon="URL").url = ( + config.HELP_WITH_CONTROLNET_URL + ) layout.separator() # Enable row = layout.row() - row.prop(props, 'controlnet_is_enabled', text="Enable") + row.prop(props, "controlnet_is_enabled", text="Enable") # ControlNet Load Models and Modules if not props.controlnet_available_models: row = layout.row() - row.operator(operators.AIR_OT_automatic1111_load_controlnet_models_and_modules.bl_idname, text="Load Models from Automatic1111", icon="FILE_REFRESH") + row.operator( + operators.AIR_OT_automatic1111_load_controlnet_models_and_modules.bl_idname, + text="Load Models from Automatic1111", + icon="FILE_REFRESH", + ) else: # Heads up box if props.controlnet_is_enabled: @@ -343,24 +439,32 @@ def draw(self, context): # ControlNet Module (Preprocessor) row = layout.row() - row.prop(props, 'controlnet_module', text="Preprocessor") + row.prop(props, "controlnet_module", text="Preprocessor") split = row.split(align=True) - split.operator(operators.AIR_OT_automatic1111_load_controlnet_modules.bl_idname, text="", icon="FILE_REFRESH") + split.operator( + operators.AIR_OT_automatic1111_load_controlnet_modules.bl_idname, + text="", + icon="FILE_REFRESH", + ) # ControlNet Model row = layout.row() - row.prop(props, 'controlnet_model', text="Model") + row.prop(props, "controlnet_model", text="Model") split = row.split(align=True) - split.operator(operators.AIR_OT_automatic1111_load_controlnet_models.bl_idname, text="", icon="FILE_REFRESH") + split.operator( + operators.AIR_OT_automatic1111_load_controlnet_models.bl_idname, + text="", + icon="FILE_REFRESH", + ) # ControlNet Weight row = layout.row() sub = row.column() sub.label(text="Weight") sub = row.column() - sub.prop(props, 'controlnet_weight', text="", slider=False) + sub.prop(props, "controlnet_weight", text="", slider=False) class AIR_PT_operation(bpy.types.Panel): @@ -370,7 +474,7 @@ class AIR_PT_operation(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -386,7 +490,7 @@ def draw(self, context): # Auto Run row = layout.row() col = row.column() - col.prop(props, 'auto_run') + col.prop(props, "auto_run") # Generate Image layout.separator() @@ -395,7 +499,10 @@ def draw(self, context): row.label(text="Run Manually:") row = layout.row() - row.enabled = 'Render Result' in bpy.data.images and bpy.data.images['Render Result'].has_data + row.enabled = ( + "Render Result" in bpy.data.images + and bpy.data.images["Render Result"].has_data + ) row.operator(operators.AIR_OT_generate_new_image_from_render.bl_idname) row = layout.row() @@ -416,8 +523,12 @@ def draw(self, context): row = layout.row() row.prop(props, "autosave_image_path", text="Path") - if (props.do_autosave_before_images or props.do_autosave_after_images) and not props.autosave_image_path: - utils.label_multiline(layout, text="Please specify a path", icon="ERROR", width=width_guess) + if ( + props.do_autosave_before_images or props.do_autosave_after_images + ) and not props.autosave_image_path: + utils.label_multiline( + layout, text="Please specify a path", icon="ERROR", width=width_guess + ) layout.separator() @@ -438,7 +549,7 @@ class AIR_PT_upscale(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -448,6 +559,14 @@ def poll(cls, context): def does_backend_support_upscaling(cls, context): return utils.get_active_backend().supports_upscaling() + @classmethod + def does_backend_support_choosing_upscaler_model(cls, context): + return utils.get_active_backend().supports_choosing_upscaler_model() + + @classmethod + def does_backend_support_choosing_upscale_factor(cls, context): + return utils.get_active_backend().supports_choosing_upscale_factor() + @classmethod def is_upscaler_model_list_loaded(cls, context): return utils.get_active_backend().is_upscaler_model_list_loaded(context) @@ -470,48 +589,82 @@ def draw(self, context): # if backend does not support upscaling, show message if not AIR_PT_upscale.does_backend_support_upscaling(context): box = layout.box() - utils.label_multiline(box, text=f"Upscaling is not supported by {utils.sd_backend_formatted_name()}. If you'd like to upscale your image, switch to DreamStudio or Automatic1111 in AI Render's preferences.", icon="ERROR", width=width_guess) + utils.label_multiline( + box, + text=f"Upscaling is not supported by {utils.sd_backend_formatted_name()}. If you'd like to upscale your image, switch to DreamStudio or Automatic1111 in AI Render's preferences.", + icon="ERROR", + width=width_guess, + ) return # if the upscaler model list hasn't been loaded, show message and button if not AIR_PT_upscale.is_upscaler_model_list_loaded(context): - utils.label_multiline(layout, text="To get started upscaling, load the available upscaler models", icon="ERROR", width=width_guess) - layout.operator(operators.AIR_OT_automatic1111_load_upscaler_models.bl_idname, text="Load Upscaler Models", icon="FILE_REFRESH") + utils.label_multiline( + layout, + text="To get started upscaling, load the available upscaler models", + icon="ERROR", + width=width_guess, + ) + layout.operator( + operators.AIR_OT_automatic1111_load_upscaler_models.bl_idname, + text="Load Upscaler Models", + icon="FILE_REFRESH", + ) return # upscale settings row = layout.row() row.prop(props, "do_upscale_automatically") - row = layout.row() - sub = row.column() - sub.label(text="Upscale Factor") - sub = row.column() - sub.prop(props, "upscale_factor", text="", slider=False) + if AIR_PT_upscale.does_backend_support_choosing_upscale_factor(context): + row = layout.row() + sub = row.column() + sub.label(text="Upscale Factor") + sub = row.column() + sub.prop(props, "upscale_factor", text="", slider=False) - row = layout.row() - sub = row.column() - sub.label(text="Upscaler Model") - sub = row.column() - sub.prop(props, "upscaler_model", text="") + if AIR_PT_upscale.does_backend_support_choosing_upscaler_model(context): + row = layout.row() + sub = row.column() + sub.label(text="Upscaler Model") + sub = row.column() + sub.prop(props, "upscaler_model", text="") box = layout.box() row = box.row() - row.label(text=f"Resulting image size: {utils.get_upscaled_width(scene)} x {utils.get_upscaled_height(scene)}") + row.label( + text=f"Resulting image size: {utils.get_upscaled_width(scene)} x {utils.get_upscaled_height(scene)}" + ) # if the dimensions are too large, show message if not AIR_PT_upscale.are_upscaled_dimensions_small_enough(context): - utils.label_multiline(layout, text="Upscaled dimensions are too large. Please decrease the scale factor.", icon="ERROR", width=width_guess) + error_message = ( + "Upscaled dimensions are too large. Please decrease the scale factor." + if AIR_PT_upscale.does_backend_support_choosing_upscale_factor(context) + else "Upscaled dimensions are too large. Please decrease your scene's image size." + ) + utils.label_multiline( + layout, + text=error_message, + icon="ERROR", + width=width_guess, + ) # if the backend supports reloading the upscaler model list, show button if AIR_PT_upscale.does_backend_support_reloading_upscaler_model_list(context): row = layout.row() - row.operator(operators.AIR_OT_automatic1111_load_upscaler_models.bl_idname, text="Reload Upscaler Models", icon="FILE_REFRESH") + row.operator( + operators.AIR_OT_automatic1111_load_upscaler_models.bl_idname, + text="Reload Upscaler Models", + icon="FILE_REFRESH", + ) # show button to manually upscale row = layout.row() row.enabled = props.last_generated_image_filename != "" - row.operator(operators.AIR_OT_upscale_last_sd_image.bl_idname, icon="FULLSCREEN_ENTER") + row.operator( + operators.AIR_OT_upscale_last_sd_image.bl_idname, icon="FULLSCREEN_ENTER" + ) # Inpainting @@ -522,11 +675,15 @@ class AIR_PT_inpaint(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return utils.is_installation_valid() and context.scene.air_props.is_enabled and utils.get_active_backend().supports_inpainting() + return ( + utils.is_installation_valid() + and context.scene.air_props.is_enabled + and utils.get_active_backend().supports_inpainting() + ) def draw(self, context): layout = self.layout @@ -542,13 +699,15 @@ def draw(self, context): sub = row.column() sub.label(text="Padding:") sub = row.column() - sub.prop(props, 'inpaint_padding', text="", slider=False) + sub.prop(props, "inpaint_padding", text="", slider=False) row = layout.row() row.prop(props, "inpaint_mask_path", text="Mask") row = layout.row() - row.enabled = props.last_generated_image_filename != "" and props.inpaint_mask_path != "" + row.enabled = ( + props.last_generated_image_filename != "" and props.inpaint_mask_path != "" + ) row.operator(operators.AIR_OT_inpaint_from_last_sd_image.bl_idname) @@ -559,11 +718,15 @@ class AIR_PT_outpaint(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return utils.is_installation_valid() and context.scene.air_props.is_enabled and utils.get_active_backend().supports_outpainting() + return ( + utils.is_installation_valid() + and context.scene.air_props.is_enabled + and utils.get_active_backend().supports_outpainting() + ) def draw(self, context): layout = self.layout @@ -614,7 +777,7 @@ class AIR_PT_animation(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -631,13 +794,19 @@ def draw(self, context): row = layout.row() is_animation_enabled_button_enabled = props.animation_output_path != "" if is_animation_enabled_button_enabled: - num_frames = math.floor(((scene.frame_end - scene.frame_start) / scene.frame_step) + 1) + num_frames = math.floor( + ((scene.frame_end - scene.frame_start) / scene.frame_step) + 1 + ) frame_or_frames = "Frame" if num_frames == 1 else "Frames" render_animation_text = f"Render Animation ({num_frames} {frame_or_frames})" else: render_animation_text = "Render Animation" - row.operator(operators.AIR_OT_render_animation.bl_idname, icon="RENDER_ANIMATION", text=render_animation_text) + row.operator( + operators.AIR_OT_render_animation.bl_idname, + icon="RENDER_ANIMATION", + text=render_animation_text, + ) row.enabled = is_animation_enabled_button_enabled # Path @@ -664,10 +833,16 @@ def draw(self, context): split = row.split(align=True) split.prop(props, "close_animation_tips", text="", icon="X", emboss=False) - utils.label_multiline(box, text="For more stable animations, consider using ControlNet (locally) or increasing \"Image Similarity\" to at least 0.7", width=width_guess) + utils.label_multiline( + box, + text='For more stable animations, consider using ControlNet (locally) or increasing "Image Similarity" to at least 0.7', + width=width_guess, + ) row = box.row() - row.operator("wm.url_open", text="Get Animation Tips", icon="URL").url = config.ANIMATION_TIPS_URL + row.operator("wm.url_open", text="Get Animation Tips", icon="URL").url = ( + config.ANIMATION_TIPS_URL + ) classes = [