├── LICENSE ├── README.md ├── assets ├── gimpfusion-posterframe.png ├── icon.png ├── ladygaga-inpainting-result.png ├── ladygaga-inpainting.png ├── monalisa-controlnet-to-ladygaga.png ├── monalisa.png └── youtube-icon.jpg ├── stable_gimpfusion.py └── version.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ArtBIT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/github/license/ArtBIT/stable-gimpfusion.svg)](https://github.com/ArtBIT/stable-gimpfusion) [![GitHub stars](https://img.shields.io/github/stars/ArtBIT/stable-gimpfusion.svg)](https://github.com/ArtBIT/stable-gimpfusion) [![awesomeness](https://img.shields.io/badge/awesomeness-maximum-red.svg)](https://github.com/ArtBIT/stable-gimpfusion) 2 | 3 | # Stable-Gimpfusion 4 | 5 | 6 | 7 | This is a simple Gimp plugin that allows you to augment your painting using [Automatic1111's StableDiffusion Web-UI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API) from your local StableDiffusion server. 8 | 9 | # Example usage 10 | 11 | Using Mona Lisa as the ControlNet 12 | 13 | 14 | 15 | Converting it to Lady Gaga 16 | 17 | 18 | 19 | And inpainting some nerdy glasses 20 | 21 | 22 | 23 | 24 | See the demo video on YouTube 25 | 26 | 27 | 28 | # Requirements 29 | 30 | - [Automatic1111's StableDiffusion Web-UI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui#installation-and-running) 31 | - [Mikubill's StableDiffusion Web-UI ControlNet](https://github.com/Mikubill/sd-webui-controlnet) 32 | 33 | # Installation 34 | 35 | - Download the script file [stable-gimpfusion.py](https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/main/stable_gimpfusion.py), 36 | save into your gimp plug-ins directory, ie: 37 | - Linux: `$HOME/.gimp-2.10/plug-ins/` or `$XDG_CONFIG_HOME/GIMP/2.10/plug-ins/` 38 | - Windows: `%APPDATA%\GIMP\2.10\plug-ins\` or `C:\Users\{your_id}\AppData\Roaming\GIMP\2.10\plug-ins\` 39 | - OSX: `$HOME/Library/GIMP/2.10/plug-ins/` or `$HOME/Library/Application Support/GIMP/2.10/plug-ins` 40 | - Ensure the execute bit is set on MacOS and Linux by running `chmod +x /stable_gimpfusion.py` 41 | - Restart Gimp, and you will see a new AI menu item 42 | - Run script via `AI -> Stable Gimpfusion -> Config` and set the backend API URL base (should be `http://127.0.0.1:7860/` by default) 43 | 44 | # Troubleshooting 45 | 46 | - Make sure you're running the Automatic1111's Web UI in API mode (`--api`) [Automatic1111's StableDiffusion Web-UI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API) try accessing [http://127.0.0.1:7860/docs](http://127.0.0.1:7860/docs) and verify the `/sdapi/` routes are present to make sure it's running 47 | - Ensure your Gimp installation has python support (You should see `Filters>Python-fu>Console` in the menu) 48 | - Verify the plugin folder that you are using (~/.config/GIMP/2.20/plug-ins) listed in the GIMP's plug-ins folders. (`Edit>Preferences>Folders>Plug-Ins`) 49 | 50 | # License 51 | 52 | [MIT](LICENSE.md) 53 | -------------------------------------------------------------------------------- /assets/gimpfusion-posterframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/gimpfusion-posterframe.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/icon.png -------------------------------------------------------------------------------- /assets/ladygaga-inpainting-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/ladygaga-inpainting-result.png -------------------------------------------------------------------------------- /assets/ladygaga-inpainting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/ladygaga-inpainting.png -------------------------------------------------------------------------------- /assets/monalisa-controlnet-to-ladygaga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/monalisa-controlnet-to-ladygaga.png -------------------------------------------------------------------------------- /assets/monalisa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/monalisa.png -------------------------------------------------------------------------------- /assets/youtube-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/3a92fcf71ae8675d0ab9aaa261bee868cd4ac973/assets/youtube-icon.jpg -------------------------------------------------------------------------------- /stable_gimpfusion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set noai ts=4 sw=4 expandtab 3 | 4 | # Stable Gimpfusion 5 | # v1.0.14 6 | # Thin API client for Automatic1111's StableDiffusion API 7 | # https://github.com/AUTOMATIC1111/stable-diffusion-webui 8 | 9 | import base64 10 | import json 11 | import os 12 | import random 13 | import tempfile 14 | import logging 15 | import urllib 16 | import urllib2 17 | 18 | import gimp 19 | import gimpenums 20 | import gimpfu 21 | 22 | VERSION = 15 23 | PLUGIN_NAME = "StableGimpfusion" 24 | PLUGIN_VERSION_URL = "https://raw.githubusercontent.com/ArtBIT/stable-gimpfusion/main/version.json" 25 | MAX_BATCH_SIZE = 20 26 | 27 | # Initialize debugging 28 | if os.environ.get('DEBUG'): 29 | DEBUG = True 30 | logging.basicConfig(level=logging.DEBUG) 31 | else: 32 | DEBUG = False 33 | logging.basicConfig(level=logging.INFO) 34 | 35 | logging.info("StableGimfusion version %d" % VERSION) 36 | 37 | 38 | # GLOBALS 39 | layer_counter = 1 40 | settings = None 41 | api = None 42 | models = None 43 | sd_model_checkpoint = None 44 | is_server_running = False 45 | 46 | 47 | STABLE_GIMPFUSION_DEFAULT_SETTINGS = { 48 | "sampler_name": "Euler a", 49 | "denoising_strength": 0.8, 50 | "cfg_scale": 7.5, 51 | "steps": 50, 52 | "width": 512, 53 | "height": 512, 54 | "prompt": "", 55 | "negative_prompt": "", 56 | "batch_size": 1, 57 | "mask_blur": 4, 58 | "seed": -1, 59 | "api_base": "http://127.0.0.1:7860", 60 | "model": "", 61 | "models": [], 62 | "cn_models": [], 63 | "sd_model_checkpoint": None, 64 | "is_server_running": False 65 | } 66 | 67 | RESIZE_MODES = { 68 | "Just Resize": 0, 69 | "Crop And Resize": 1, 70 | "Resize And Fill": 2, 71 | "Just Resize (Latent Upscale)": 3 72 | } 73 | 74 | CONTROL_MODES = { 75 | "Balanced": 0, 76 | "My prompt is more important": 1, 77 | "ControlNet is more important": 2, 78 | } 79 | 80 | SAMPLERS = [ 81 | "Euler a", 82 | "Euler", 83 | "LMS", 84 | "Heun", 85 | "DPM2", 86 | "DPM2 a", 87 | "DPM++ 2S a", 88 | "DPM++ 2M", 89 | "DPM++ SDE", 90 | "DPM fast", 91 | "DPM adaptive", 92 | "LMS Karras", 93 | "DPM2 Karras", 94 | "DPM2 a Karras", 95 | "DPM++ 2S a Karras", 96 | "DPM++ 2M Karras", 97 | "DPM++ SDE Karras", 98 | "DDIM" 99 | ] 100 | 101 | CONTROLNET_RESIZE_MODES = [ 102 | "Just Resize", 103 | "Scale to Fit (Inner Fit)", 104 | "Envelope (Outer Fit)", 105 | ] 106 | 107 | CONTROLNET_MODULES = [ 108 | "none", 109 | "canny", 110 | "depth", 111 | "depth_leres", 112 | "hed", 113 | "mlsd", 114 | "normal_map", 115 | "openpose", 116 | "openpose_hand", 117 | "clip_vision", 118 | "color", 119 | "pidinet", 120 | "scribble", 121 | "fake_scribble", 122 | "segmentation", 123 | "binary" 124 | ] 125 | 126 | CONTROLNET_DEFAULT_SETTINGS = { 127 | "input_image": "", 128 | "mask": "", 129 | "module": "none", 130 | "model": "none", 131 | "weight": 1.0, 132 | "resize_mode": "Scale to Fit (Inner Fit)", 133 | "lowvram": False, 134 | "processor_res": 64, 135 | "threshold_a": 64, 136 | "threshold_b": 64, 137 | "guidance": 1.0, 138 | "guidance_start": 0.0, 139 | "guidance_end": 1.0, 140 | "control_mode": 0, 141 | } 142 | 143 | GENERATION_MESSAGES = [ 144 | "Making happy little pixels...", 145 | "Fetching pixels from a digital art museum...", 146 | "Waiting for bot-painters to finish...", 147 | "Waiting for the prompt to bake...", 148 | "Fetching random pixels from the internet", 149 | "Taking a random screenshot from an AI dream", 150 | "Throwing pixels at screen and seeing what sticks", 151 | "Converting random internet comment to RGB values", 152 | "Computer make pretty picture, you happy.", 153 | "Computer is hand-painting pixels...", 154 | "Turning the Gimp knob up to 11...", 155 | "Pixelated dreams come true, thanks to AI.", 156 | "AI is doing its magic...", 157 | "Pocket Picasso is speed-painting...", 158 | "Instant Rembrandt! Well, relatively instant...", 159 | "Doodle buddy is doing its thing...", 160 | "Waiting for the digital paint to dry..." 161 | ] 162 | 163 | 164 | def roundToMultiple(value, multiple): 165 | return multiple * round(float(value)/multiple) 166 | 167 | def deunicodeDict(data): 168 | """Recursively converts dictionary keys to strings.""" 169 | if isinstance(data, unicode): 170 | return str(data) 171 | if not isinstance(data, dict): 172 | return data 173 | return dict((str(k), deunicodeDict(v)) 174 | for k, v in data.items()) 175 | 176 | class ApiClient(): 177 | """ Simple API client used to interface with StableDiffusion JSON endpoints """ 178 | def __init__(self, base_url): 179 | self.setBaseUrl(base_url) 180 | 181 | def setBaseUrl(self, base_url): 182 | self.base_url = base_url 183 | 184 | def post(self, endpoint, data={}, params={}, headers=None): 185 | try: 186 | url = self.base_url + endpoint + "?" + urllib.urlencode(params) 187 | logging.debug("POST %s" % url) 188 | data = json.dumps(data) 189 | 190 | logging.debug('post data %s', data) 191 | 192 | headers = headers or {"Content-Type": "application/json", "Accept": "application/json"} 193 | request = urllib2.Request(url=url, data=data, headers=headers) 194 | response = urllib2.urlopen(request) 195 | data = response.read() 196 | data = json.loads(data) 197 | 198 | logging.debug('response: %s', data) 199 | return data 200 | except Exception as ex: 201 | logging.exception("ERROR: ApiClient.post") 202 | 203 | def get(self, endpoint, params={}, headers=None): 204 | try: 205 | url = self.base_url + endpoint + "?" + urllib.urlencode(params) 206 | logging.debug("POST %s" % url) 207 | headers = headers or {"Content-Type": "application/json", "Accept": "application/json"} 208 | request = urllib2.Request(url=url, headers=headers) 209 | response = urllib2.urlopen(request) 210 | data = response.read() 211 | data = json.loads(data) 212 | return data 213 | except Exception as ex: 214 | logging.exception("ERROR: ApiClient.get") 215 | 216 | 217 | """ Get the StableDiffusion data needed for dynamic gimpfu.PF_OPTION lists """ 218 | def fetch_stablediffusion_options(): 219 | global api, settings 220 | try: 221 | options = deunicodeDict(api.get("/sdapi/v1/options") or {}) 222 | sd_model_checkpoint = options.get("sd_model_checkpoint", None) 223 | models = map(lambda data: data["title"], api.get("/sdapi/v1/sd-models") or []) 224 | cn_models = (api.get("/controlnet/model_list") or {}).get("model_list", []) 225 | cn_models = ["None"] + cn_models 226 | 227 | settings.save({"models": models, 228 | "cn_models": cn_models, 229 | "sd_model_checkpoint": sd_model_checkpoint, 230 | "is_server_running": True}) 231 | except Exception as ex: 232 | logging.exception("ERROR: DynamicDropdownData.fetch") 233 | settings.save({"is_server_running", False}) 234 | 235 | # We need persistent data before the gimp system has initialized so we cannot use parasites nor gimpshelf 236 | class MyShelf(): 237 | """ GimpShelf is not available at init time, so we keep our persistent data in a json file """ 238 | def __init__(self, default_shelf = {}): 239 | self.file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'stable_gimpfusion.json') 240 | self.load(default_shelf) 241 | 242 | def load(self, default_shelf = {}): 243 | self.data = default_shelf 244 | try: 245 | if os.path.isfile(self.file_path): 246 | logging.info("Loading shelf from %s" % self.file_path) 247 | with open(self.file_path, "r") as f: 248 | self.data = json.load(f) 249 | logging.info("Successfully loaded shelf") 250 | except Exception as e: 251 | logging.debug(e) 252 | 253 | def save(self, data = {}): 254 | try: 255 | self.data.update(data) 256 | logging.info("Saving shelf to %s" % self.file_path) 257 | with open(self.file_path, "w") as f: 258 | json.dump(self.data, f) 259 | logging.info("Successfully saved shelf") 260 | 261 | except Exception as e: 262 | logging.debug(e) 263 | 264 | 265 | def get(self, name, default_value=None): 266 | if name in self.data: 267 | return self.data[name] 268 | return default_value 269 | 270 | def set(self, name, default_value=None): 271 | self.data[name] = default_value 272 | self.save() 273 | 274 | class StableGimpfusionPlugin(): 275 | def __init__(self, image): 276 | global settings,api 277 | self.name = "stable_gimpfusion" 278 | self.image = image 279 | 280 | global is_server_running 281 | if not is_server_running: 282 | gimp.pdb.gimp_message("It seems that StableDiffusion is not runing on "+settings.get("api_base")) 283 | 284 | try: 285 | self.api = api 286 | self.files = TempFiles() 287 | except Exception as e: 288 | logging.exception("ERROR: StableGimpfusionPlugin.__init__") 289 | 290 | def showMessage(self, text): 291 | gimp.pdb.gimp_message(text) 292 | 293 | def checkUpdate(self): 294 | try: 295 | gimp.get_data("update_checked") 296 | updateChecked = True 297 | except Exception as ex: 298 | updateChecked = False 299 | 300 | if updateChecked is False: 301 | try: 302 | response = urllib2.urlopen(PLUGIN_VERSION_URL) 303 | data = response.read() 304 | data = json.loads(data) 305 | gimp.set_data("update_checked", "1") 306 | 307 | if VERSION < int(data["version"]): 308 | gimp.pdb.gimp_message(data["message"]) 309 | except Exception as ex: 310 | ex = ex 311 | 312 | def getLayerAsBase64(self, layer): 313 | # store active_layer 314 | active_layer = layer.image.active_layer 315 | copy = Layer(layer).copy().insert() 316 | result = copy.toBase64() 317 | copy.remove() 318 | # restore active_layer 319 | gimp.pdb.gimp_image_set_active_layer(active_layer.image, active_layer) 320 | return result 321 | 322 | def getActiveLayerAsBase64(self): 323 | return self.getLayerAsBase64(self.image.active_layer) 324 | 325 | def getLayerMaskAsBase64(self, layer): 326 | non_empty, x1, y1, x2, y2 = gimp.pdb.gimp_selection_bounds(layer.image) 327 | if non_empty: 328 | # selection to base64 329 | 330 | # store active_layer 331 | active_layer = layer.image.active_layer 332 | 333 | # selection to file 334 | #disable=pdb.gimp_image_undo_disable(layer.image) 335 | tmp_layer = Layer.create(layer.image, "mask", layer.image.width, layer.image.height, gimpenums.RGBA_IMAGE, 100, gimpenums.NORMAL_MODE) 336 | tmp_layer.addSelectionAsMask().insert() 337 | 338 | result = tmp_layer.maskToBase64() 339 | tmp_layer.remove() 340 | #enable = pdb.gimp_image_undo_enable(layer.image) 341 | 342 | # restore active_layer 343 | gimp.pdb.gimp_image_set_active_layer(active_layer.image, active_layer) 344 | 345 | return result 346 | elif layer.mask: 347 | # mask to file 348 | tmp_layer = Layer(layer) 349 | return tmp_layer.maskToBase64() 350 | else: 351 | return "" 352 | 353 | def getActiveMaskAsBase64(self): 354 | return self.getLayerMaskAsBase64(self.image.active_layer) 355 | 356 | def getSelectionBounds(self): 357 | non_empty, x1, y1, x2, y2 = gimp.pdb.gimp_selection_bounds(self.image) 358 | if non_empty: 359 | return x1, y1, x2-x1, y2-y1 360 | return 0, 0, self.image.width, self.image.height 361 | 362 | def cleanup(self): 363 | self.files.removeAll() 364 | self.checkUpdate() 365 | 366 | def getControlNetParams(self, cn_layer): 367 | if cn_layer: 368 | layer = Layer(cn_layer) 369 | data = layer.loadData(CONTROLNET_DEFAULT_SETTINGS) 370 | # ControlNet image size need to be in multiples of 64 371 | layer64 = layer.copy().insert().resizeToMultipleOf(64) 372 | data.update({"input_image": layer64.toBase64()}) 373 | if cn_layer.mask: 374 | data.update({"mask": layer64.maskToBase64()}) 375 | layer64.remove() 376 | return data 377 | return None 378 | 379 | def imageToImage(self, *args): 380 | global settings 381 | resize_mode, prompt, negative_prompt, seed, batch_size, steps, mask_blur, width, height, cfg_scale, denoising_strength, sampler_index, cn1_enabled, cn1_layer, cn2_enabled, cn2_layer, cn_skip_annotator_layers = args 382 | image = self.image 383 | 384 | x, y, origWidth, origHeight = self.getSelectionBounds() 385 | 386 | data = { 387 | "resize_mode": resize_mode, 388 | "init_images": [self.getActiveLayerAsBase64()], 389 | 390 | "prompt": (prompt + " " + settings.get("prompt")).strip(), 391 | "negative_prompt": (negative_prompt + " " + settings.get("negative_prompt")).strip(), 392 | "denoising_strength": float(denoising_strength), 393 | "steps": int(steps), 394 | "cfg_scale": float(cfg_scale), 395 | "width": roundToMultiple(width, 8), 396 | "height": roundToMultiple(height, 8), 397 | "sampler_index": SAMPLERS[sampler_index], 398 | "batch_size": min(MAX_BATCH_SIZE, max(1, batch_size)), 399 | "seed": seed or -1 400 | } 401 | 402 | try: 403 | gimp.pdb.gimp_progress_init("", None) 404 | gimp.pdb.gimp_progress_set_text(random.choice(GENERATION_MESSAGES)) 405 | 406 | controlnet_units = [] 407 | if cn1_enabled: 408 | controlnet_units.append(self.getControlNetParams(cn1_layer)) 409 | if cn2_enabled: 410 | controlnet_units.append(self.getControlNetParams(cn2_layer)) 411 | if len(controlnet_units) > 0: 412 | alwayson_scripts = { 413 | "controlnet": { 414 | "args": controlnet_units 415 | } 416 | } 417 | data.update({"alwayson_scripts": alwayson_scripts}) 418 | 419 | response = self.api.post("/sdapi/v1/img2img", data) 420 | 421 | ResponseLayers(image, response, {"skip_annotator_layers": cn_skip_annotator_layers}).resize(origWidth, origHeight) 422 | 423 | except Exception as ex: 424 | logging.exception("ERROR: StableGimpfusionPlugin.imageToImage") 425 | self.showMessage(repr(ex)) 426 | finally: 427 | gimp.pdb.gimp_progress_end() 428 | self.cleanup() 429 | 430 | def inpainting(self, *args): 431 | global settings 432 | resize_mode, prompt, negative_prompt, seed, batch_size, steps, mask_blur, width, height, cfg_scale, denoising_strength, sampler_index, cn1_enabled, cn1_layer, cn2_enabled, cn2_layer, cn_skip_annotator_layers, invert_mask, inpaint_full_res = args 433 | image = self.image 434 | 435 | x, y, origWidth, origHeight = self.getSelectionBounds() 436 | 437 | init_images = [self.getActiveLayerAsBase64()] 438 | mask = self.getActiveMaskAsBase64() 439 | if mask == "": 440 | logging.exception("ERROR: StableGimpfusionPlugin.inpainting") 441 | raise Exception("Inpainting must use either a selection or layer mask") 442 | 443 | data = { 444 | "mask": mask, 445 | "inpaint_full_res": inpaint_full_res, 446 | "inpaint_full_res_padding": 10, 447 | "inpainting_mask_invert": 1 if invert_mask else 0, 448 | 449 | "resize_mode": resize_mode, 450 | "init_images": init_images, 451 | 452 | "prompt": (prompt + " " + settings.get("prompt")).strip(), 453 | "negative_prompt": (negative_prompt + " " + settings.get("negative_prompt")).strip(), 454 | "denoising_strength": float(denoising_strength), 455 | "steps": int(steps), 456 | "cfg_scale": float(cfg_scale), 457 | "width": roundToMultiple(width, 8), 458 | "height": roundToMultiple(height, 8), 459 | "sampler_index": SAMPLERS[sampler_index], 460 | "batch_size": min(MAX_BATCH_SIZE, max(1, batch_size)), 461 | "seed": seed or -1 462 | } 463 | 464 | try: 465 | gimp.pdb.gimp_progress_init("", None) 466 | gimp.pdb.gimp_progress_set_text(random.choice(GENERATION_MESSAGES)) 467 | 468 | controlnet_units = [] 469 | if cn1_enabled: 470 | controlnet_units.append(self.getControlNetParams(cn1_layer)) 471 | if cn2_enabled: 472 | controlnet_units.append(self.getControlNetParams(cn2_layer)) 473 | 474 | if len(controlnet_units) > 0: 475 | alwayson_scripts = { 476 | "controlnet": { 477 | "args": controlnet_units 478 | } 479 | } 480 | data.update({"alwayson_scripts": alwayson_scripts}) 481 | 482 | response = self.api.post("/sdapi/v1/img2img", data) 483 | 484 | ResponseLayers(image, response, {"skip_annotator_layers": cn_skip_annotator_layers}).resize(self.image.width, self.image.height) 485 | 486 | except Exception as ex: 487 | logging.exception("ERROR: StableGimpfusionPlugin.inpainting") 488 | self.showMessage(repr(ex)) 489 | finally: 490 | gimp.pdb.gimp_progress_end() 491 | self.cleanup() 492 | 493 | def textToImage(self, *args): 494 | global settings 495 | prompt, negative_prompt, seed, batch_size, steps, mask_blur, width, height, cfg_scale, denoising_strength, sampler_index, cn1_enabled, cn1_layer, cn2_enabled, cn2_layer, cn_skip_annotator_layers = args 496 | image = self.image 497 | 498 | x, y, origWidth, origHeight = self.getSelectionBounds() 499 | 500 | data = { 501 | "prompt": (prompt + " " + settings.get("prompt")).strip(), 502 | "negative_prompt": (negative_prompt + " " + settings.get("negative_prompt")).strip(), 503 | "cfg_scale": float(cfg_scale), 504 | "denoising_strength": float(denoising_strength), 505 | "steps": int(steps), 506 | "width": roundToMultiple(width, 8), 507 | "height": roundToMultiple(height, 8), 508 | "sampler_index": SAMPLERS[sampler_index], 509 | "batch_size": min(MAX_BATCH_SIZE, max(1, batch_size)), 510 | "seed": seed or -1 511 | } 512 | 513 | try: 514 | gimp.pdb.gimp_progress_init("", None) 515 | gimp.pdb.gimp_progress_set_text(random.choice(GENERATION_MESSAGES)) 516 | 517 | controlnet_units = [] 518 | if cn1_enabled: 519 | controlnet_units.append(self.getControlNetParams(cn1_layer)) 520 | if cn2_enabled: 521 | controlnet_units.append(self.getControlNetParams(cn2_layer)) 522 | 523 | if len(controlnet_units) > 0: 524 | alwayson_scripts = { 525 | "controlnet": { 526 | "args": controlnet_units 527 | } 528 | } 529 | data.update({"alwayson_scripts": alwayson_scripts}) 530 | 531 | response = self.api.post("/sdapi/v1/txt2img", data) 532 | 533 | ResponseLayers(image, response, {"skip_annotator_layers": cn_skip_annotator_layers}).resize(origWidth, origHeight).translate((x, y)).addSelectionAsMask() 534 | 535 | except Exception as ex: 536 | logging.exception("ERROR: StableGimpfusionPlugin.textToImage") 537 | self.showMessage(repr(ex)) 538 | finally: 539 | gimp.pdb.gimp_progress_end() 540 | self.cleanup() 541 | 542 | def showLayerInfo(self, *args): 543 | """ Show any layer info associated with the active layer """ 544 | 545 | data = LayerData(self.image.active_layer).data 546 | gimp.pdb.gimp_message("This layer has the following data associated with it\n" + json.dumps(data, sort_keys=True, indent=4)) 547 | 548 | 549 | def saveControlLayer(self, module, model, weight, resize_mode, lowvram, control_mode, guidance_start, guidance_end, guidance, processor_res, threshold_a, threshold_b): 550 | """ Take the form params and save them to the layer as gimp.Parasite """ 551 | global settings 552 | cn_models = settings.get("cn_models", []) 553 | cn_settings = { 554 | "module": CONTROLNET_MODULES[module], 555 | "model": cn_models[model], 556 | "weight": weight, 557 | "resize_mode": CONTROLNET_RESIZE_MODES[resize_mode], 558 | "lowvram": lowvram, 559 | "control_mode": control_mode, 560 | "guidance_start": guidance_start, 561 | "guidance_end": guidance_end, 562 | "guidance": guidance, 563 | "processor_res": processor_res, 564 | "threshold_a": threshold_a, 565 | "threshold_b": threshold_b, 566 | } 567 | active_layer = self.image.active_layer 568 | cnlayer = Layer(active_layer) 569 | cnlayer.saveData(cn_settings) 570 | cnlayer.rename("ControlNet"+str(cnlayer.id)) 571 | 572 | def config(self, prompt, negative_prompt, url): 573 | global settings 574 | settings.save({ 575 | "prompt": prompt, 576 | "negative_prompt": negative_prompt, 577 | "api_base": url, 578 | }) 579 | 580 | def changeModel(self, model): 581 | global settings 582 | if settings.get("model") != model: 583 | gimp.pdb.gimp_progress_init("", None) 584 | gimp.pdb.gimp_progress_set_text("Changing model...") 585 | try: 586 | self.api.post("/sdapi/v1/options", { "sd_model_checkpoint": models[model] } ) 587 | settings.set("sd_model_checkpoint", model) 588 | except Exception as e: 589 | logging.error(e) 590 | gimp.pdb.gimp_progress_end() 591 | 592 | 593 | class TempFiles(object): 594 | def __new__(cls): 595 | if not hasattr(cls, 'instance'): 596 | cls.instance = super(TempFiles, cls).__new__(cls) 597 | return cls.instance 598 | 599 | def __init__(self): 600 | self.files = [] 601 | 602 | def get(self, filename): 603 | self.files.append(filename) 604 | return r"{}".format(os.path.join(tempfile.gettempdir(), filename)) 605 | 606 | def removeAll(self): 607 | try: 608 | unique_list = (list(set(self.files))) 609 | for tmpfile in unique_list: 610 | if os.path.exists(tmpfile): 611 | os.remove(tmpfile) 612 | except Exception as ex: 613 | ex = ex 614 | 615 | 616 | class LayerData(): 617 | def __init__(self, layer, defaults = {}): 618 | self.name = 'gimpfusion' 619 | self.layer = layer 620 | self.image = layer.image 621 | self.defaults = defaults 622 | self.had_parasite = False 623 | self.load() 624 | 625 | def load(self): 626 | parasite = self.layer.parasite_find(self.name) 627 | if not parasite: 628 | self.data = self.defaults.copy() 629 | else: 630 | self.had_parasite = True 631 | self.data = json.loads(parasite.data) 632 | self.data = deunicodeDict(self.data) 633 | return self.data 634 | 635 | def save(self, data): 636 | parasite = gimp.Parasite(self.name, gimpenums.PARASITE_PERSISTENT, deunicodeDict(json.dumps(data))) 637 | self.layer.parasite_attach(parasite) 638 | 639 | class Layer(): 640 | def __init__(self, layer = None): 641 | global layer_counter 642 | self.id = layer_counter 643 | layer_counter = layer_counter + 1 644 | if layer is not None: 645 | self.layer = layer 646 | self.image = layer.image 647 | 648 | @staticmethod 649 | def create(image, name, width, height, image_type, opacity, mode): 650 | layer = gimp.Layer(image, name, width, height, image_type, opacity, mode) 651 | return Layer(layer) 652 | 653 | @staticmethod 654 | def fromBase64(img, base64Data): 655 | filepath = TempFiles().get("generated.png") 656 | imageFile = open(filepath, "wb+") 657 | imageFile.write(base64.b64decode(base64Data)) 658 | imageFile.close() 659 | layer = gimp.pdb.gimp_file_load_layer(img, filepath) 660 | return Layer(layer) 661 | 662 | 663 | def rename(self, name): 664 | gimp.pdb.gimp_layer_set_name(self.layer, name) 665 | return self 666 | 667 | def saveData(self, data): 668 | LayerData(self.layer).save(data) 669 | return self 670 | 671 | def loadData(self, default_data): 672 | return LayerData(self.layer, default_data).data.copy() 673 | 674 | def copy(self): 675 | copy = gimp.pdb.gimp_layer_copy(self.layer, True) 676 | return Layer(copy) 677 | 678 | def scale(self, new_scale=1.0): 679 | if new_scale != 1.0: 680 | gimp.pdb.gimp_layer_scale(self.layer, int(new_scale * self.layer.width), int(new_scale * self.layer.height), False) 681 | return self 682 | 683 | def resize(self, width, height): 684 | logging.info("Resizing to %dx%d", width, height) 685 | gimp.pdb.gimp_layer_scale(self.layer, width, height, False) 686 | 687 | def resizeToMultipleOf(self, multiple): 688 | gimp.pdb.gimp_layer_scale(self.layer, roundToMultiple(self.layer.width, multiple), roundToMultiple(self.layer.height, multiple), False) 689 | return self 690 | 691 | def translate(self, offset=None): 692 | if offset is not None: 693 | gimp.pdb.gimp_layer_set_offsets(self.layer, offset[0], offset[1]) 694 | return self 695 | 696 | def insert(self): 697 | gimp.pdb.gimp_image_insert_layer(self.image, self.layer, None, -1) 698 | return self 699 | 700 | def insertTo(self, image=None): 701 | image = image or self.image 702 | gimp.pdb.gimp_image_insert_layer(image, self.layer, None, -1) 703 | return self 704 | 705 | def addSelectionAsMask(self): 706 | mask = self.layer.create_mask(gimpenums.ADD_SELECTION_MASK) 707 | self.layer.add_mask(mask) 708 | return self 709 | 710 | def saveMaskAs(self, filepath): 711 | gimp.pdb.file_png_save(self.image, self.layer.mask, filepath, filepath, False, 9, True, True, True, True, True) 712 | return self 713 | 714 | def saveAs(self, filepath): 715 | gimp.pdb.file_png_save(self.image, self.layer, filepath, filepath, False, 9, True, True, True, True, True) 716 | return self 717 | 718 | def maskToBase64(self): 719 | filepath = TempFiles().get("mask"+str(self.id)+".png") 720 | self.saveMaskAs(filepath) 721 | file = open(filepath, "rb") 722 | return base64.b64encode(file.read()) 723 | 724 | def toBase64(self): 725 | filepath = TempFiles().get("layer"+str(self.id)+".png") 726 | self.saveAs(filepath) 727 | file = open(filepath, "rb") 728 | return base64.b64encode(file.read()) 729 | 730 | def remove(self): 731 | gimp.pdb.gimp_image_remove_layer(self.layer.image, self.layer) 732 | return self 733 | 734 | 735 | class ResponseLayers(): 736 | def __init__(self, img, response, options = {}): 737 | self.image = img 738 | color = gimp.pdb.gimp_context_get_foreground() 739 | gimp.pdb.gimp_context_set_foreground((0, 0, 0)) 740 | 741 | layers = [] 742 | try: 743 | info = json.loads(response["info"]) 744 | infotexts = info["infotexts"] 745 | seeds = info["all_seeds"] 746 | index = 0 747 | logging.debug(infotexts) 748 | logging.debug(seeds) 749 | total_images = len(seeds) 750 | for image in response["images"]: 751 | if index < total_images: 752 | layer_data = {"info": infotexts[index], "seed": seeds[index]} 753 | layer = Layer.fromBase64(img, image).rename("Generated Layer "+str(seeds[index])).saveData(layer_data).insertTo(img) 754 | else: 755 | # annotator layers 756 | if "skip_annotator_layers" in options and not options["skip_annotator_layers"]: 757 | layer = Layer.fromBase64(img, image).rename("Annotator Layer").insertTo(img) 758 | layers.append(layer.layer) 759 | index += 1 760 | except Exception as e: 761 | logging.exception("ResponseLayers") 762 | 763 | gimp.pdb.gimp_context_set_foreground(color) 764 | self.layers = layers 765 | 766 | def scale(self, new_scale=1.0): 767 | if new_scale != 1.0: 768 | for layer in self.layers: 769 | Layer(layer).scale(new_scale) 770 | return self 771 | 772 | def resize(self, width, height): 773 | for layer in self.layers: 774 | Layer(layer).resize(width, height) 775 | return self 776 | 777 | def translate(self, offset=None): 778 | if offset is not None: 779 | for layer in self.layers: 780 | Layer(layer).translate(offset) 781 | return self 782 | 783 | def insertTo(self, image=None): 784 | image = image or self.image 785 | for layer in self.layers: 786 | Layer(layer).insertTo(image) 787 | return self 788 | 789 | def addSelectionAsMask(self): 790 | non_empty, x1, y1, x2, y2 = gimp.pdb.gimp_selection_bounds(self.image) 791 | if not non_empty: 792 | return 793 | if (x1 == 0) and (y1 == 0) and (x2 - x1 == self.image.width) and (y2 - y1 == self.image.height): 794 | return 795 | for layer in self.layers: 796 | Layer(layer).addSelectionAsMask() 797 | return self 798 | 799 | def handleConfig(image, drawable, *args): 800 | print((image, drawable, args)) 801 | StableGimpfusionPlugin(image).config(*args) 802 | 803 | def handleChangeModel(image, drawable, *args): 804 | logging.info(image, drawable, *args) 805 | StableGimpfusionPlugin(image).changeModel(*args) 806 | 807 | def handleImageToImage(image, drawable, *args): 808 | StableGimpfusionPlugin(image).imageToImage(*args) 809 | 810 | def handleInpainting(image, drawable, *args): 811 | StableGimpfusionPlugin(image).inpainting(*args) 812 | 813 | def handleTextToImage(image, drawable, *args): 814 | StableGimpfusionPlugin(image).textToImage(*args) 815 | 816 | def handleControlNetLayerConfig(image, drawable, *args): 817 | StableGimpfusionPlugin(image).saveControlLayer(*args) 818 | 819 | def handleShowLayerInfo(image, drawable, *args): 820 | StableGimpfusionPlugin(image).showLayerInfo(*args) 821 | 822 | def handleImageToImageFromLayersContext(image, drawable, *args): 823 | StableGimpfusionPlugin(image).imageToImage(*args) 824 | 825 | def handleInpaintingFromLayersContext(image, drawable, *args): 826 | StableGimpfusionPlugin(image).inpainting(*args) 827 | 828 | def handleTextToImageFromLayersContext(image, drawable, *args): 829 | StableGimpfusionPlugin(image).textToImage(*args) 830 | 831 | def handleControlNetLayerConfigFromLayersContext(image, drawable, *args): 832 | StableGimpfusionPlugin(image).saveControlLayer(*args) 833 | 834 | def handleShowLayerInfoContext(image, drawable, *args): 835 | StableGimpfusionPlugin(image).showLayerInfo(*args) 836 | 837 | def init_plugin(): 838 | global settings, api, sd_model, models, is_server_running 839 | 840 | settings = MyShelf(STABLE_GIMPFUSION_DEFAULT_SETTINGS) 841 | api = ApiClient(settings.get("api_base")) 842 | fetch_stablediffusion_options() 843 | models = settings.get("models", []) 844 | sd_model_checkpoint = settings.get("sd_model_checkpoint") 845 | is_server_running = settings.get("is_server_running") 846 | logging.info(settings) 847 | 848 | PLUGIN_FIELDS_IMAGE = [ 849 | (gimpfu.PF_IMAGE, "image", "Image", None), 850 | (gimpfu.PF_DRAWABLE, "drawable", "Drawable", None), 851 | ] 852 | 853 | PLUGIN_FIELDS_LAYERS = [ 854 | (gimpfu.PF_IMAGE, "image", "Image", None), 855 | (gimpfu.PF_LAYER, "layer", "Layer", None), 856 | ] 857 | 858 | PLUGIN_FIELDS_COMMON = [ 859 | (gimpfu.PF_TEXT, "prompt", "Prompt", settings.get("prompt")), 860 | (gimpfu.PF_TEXT, "negative_prompt", "Negative Prompt", settings.get("negative_prompt")), 861 | (gimpfu.PF_INT32, "seed", "Seed", settings.get("seed")), 862 | (gimpfu.PF_SLIDER, "batch_size", "Batch count", settings.get("batch_size"), (1, 20, 1.0)), 863 | (gimpfu.PF_SLIDER, "steps", "Steps", settings.get("steps"), (10, 150, 1.0)), 864 | (gimpfu.PF_SLIDER, "mask_blur", "Mask Blur", settings.get("mask_blur"), (1, 10, 1.0)), 865 | (gimpfu.PF_SLIDER, "width", "Width", settings.get("width"), (64, 2048, 8)), 866 | (gimpfu.PF_SLIDER, "height", "Height", settings.get("height"), (64, 2048, 8)), 867 | (gimpfu.PF_SLIDER, "cfg_scale", "CFG Scale", settings.get("cfg_scale"), (0, 20, 0.5)), 868 | (gimpfu.PF_SLIDER, "denoising_strength", "Denoising Strength", settings.get("denoising_strength"), (0.0, 1.0, 0.01)), 869 | (gimpfu.PF_OPTION, "sampler_index", "Sampler", SAMPLERS.index(settings.get("sampler_name")), SAMPLERS), 870 | ] 871 | 872 | PLUGIN_FIELDS_CONTROLNET_OPTIONS = [ 873 | (gimpfu.PF_TOGGLE, "cn1_enabled", "Enable ControlNet 1", False), 874 | (gimpfu.PF_LAYER, "cn1_layer", "ControlNet 1 Layer", None), 875 | (gimpfu.PF_TOGGLE, "cn2_enabled", "Enable ControlNet 2", False), 876 | (gimpfu.PF_LAYER, "cn2_layer", "ControlNet 2 Layer", None), 877 | (gimpfu.PF_TOGGLE, "cn_skip_annotator_layers", "Skip annotator layers", True), 878 | ] 879 | 880 | PLUGIN_FIELDS_CONFIG = [ 881 | (gimpfu.PF_STRING, "prompt", "Prompt Suffix", settings.get("prompt")), 882 | (gimpfu.PF_STRING, "negative_prompt", "Negative Prompt Suffix", settings.get("negative_prompt")), 883 | (gimpfu.PF_STRING, "api_base", "Backend API URL base", settings.get("api_base")), 884 | ] 885 | 886 | logging.info(models) 887 | if sd_model_checkpoint is not None: 888 | PLUGIN_FIELDS_CHECKPOINT = [ 889 | (gimpfu.PF_OPTION, "model", "Model", models.index(sd_model_checkpoint), models) 890 | ] 891 | else: 892 | PLUGIN_FIELDS_CHECKPOINT = [] 893 | 894 | 895 | PLUGIN_FIELDS_RESIZE_MODE = [(gimpfu.PF_OPTION, "resize_mode", "Resize Mode", 0, tuple(RESIZE_MODES.keys()))] 896 | PLUGIN_FIELDS_TXT2IMG = [] + PLUGIN_FIELDS_COMMON + PLUGIN_FIELDS_CONTROLNET_OPTIONS 897 | PLUGIN_FIELDS_IMG2IMG = [] + PLUGIN_FIELDS_RESIZE_MODE + PLUGIN_FIELDS_TXT2IMG 898 | PLUGIN_FIELDS_INPAINTING = [ 899 | (gimpfu.PF_TOGGLE, "invert_mask", "Invert Mask", False), 900 | (gimpfu.PF_TOGGLE, "inpaint_full_res", "Inpaint Whole Picture", True), 901 | ] 902 | 903 | 904 | PLUGIN_FIELDS_CONTROLNET = [] + [ 905 | (gimpfu.PF_OPTION, "module", "Module", 0, CONTROLNET_MODULES), 906 | (gimpfu.PF_OPTION, "model", "Model", 0, settings.get("cn_models", ["none"])), 907 | (gimpfu.PF_SLIDER, "weight", "Weight", 1, (0, 2, 0.05)), 908 | (gimpfu.PF_OPTION, "resize_mode", "Resize Mode", 1, CONTROLNET_RESIZE_MODES), 909 | (gimpfu.PF_BOOL, "lowvram", "Low VRAM", False), 910 | (gimpfu.PF_OPTION, "control_mode", "Control Mode", 0, tuple(CONTROL_MODES.keys())), 911 | (gimpfu.PF_SLIDER, "guidance_start", "Guidance Start (T)", 0, (0, 1, 0.01)), 912 | (gimpfu.PF_SLIDER, "guidance_end", "Guidance End (T)", 1, (0, 1, 0.01)), 913 | (gimpfu.PF_SLIDER, "guidance", "Guidance", 1, (0, 1, 0.01)), 914 | (gimpfu.PF_SLIDER, "processor_res", "Processor Resolution", 512, (64, 2048, 1)), 915 | (gimpfu.PF_SLIDER, "threshold_a", "Threshold A", 64, (100, 2048, 1)), 916 | (gimpfu.PF_SLIDER, "threshold_b", "Threshold B", 64, (200, 2048, 1)), 917 | ] 918 | 919 | gimpfu.register( 920 | "stable-gimpfusion-config", 921 | "This is where you configure params that are shared between all API requests", 922 | "Gimp Client for the StableDiffusion Automatic1111 API", 923 | "ArtBIT", 924 | "ArtBIT", 925 | "2023", 926 | "Global", 927 | "*", # Alternately use RGB, RGB*, GRAY*, INDEXED etc. 928 | [] + PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_CONFIG, 929 | [], 930 | handleConfig, menu="/GimpFusion/Config", 931 | ) 932 | 933 | gimpfu.register( 934 | "stable-gimpfusion-config-model", 935 | "Change the Checkpoint Model", 936 | "Change the Checkpoint Model", 937 | "ArtBIT", 938 | "ArtBIT", 939 | "2023", 940 | "Change Model", 941 | "*", 942 | [] + PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_CHECKPOINT, 943 | [], 944 | handleChangeModel, menu="/GimpFusion/Config" 945 | ) 946 | 947 | gimpfu.register( 948 | "stable-gimpfusion-txt2img", 949 | "Text to image", 950 | "Text to image", 951 | "ArtBIT", 952 | "ArtBIT", 953 | "2023", 954 | "Text to image", 955 | "*", 956 | []+ PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_TXT2IMG, 957 | [], 958 | handleTextToImage, menu="/GimpFusion" 959 | ) 960 | 961 | 962 | gimpfu.register( 963 | "stable-gimpfusion-txt2img-context", 964 | "Text to image", 965 | "Text to image", 966 | "ArtBIT", 967 | "ArtBIT", 968 | "2023", 969 | "Text to image", 970 | "*", 971 | [] + PLUGIN_FIELDS_LAYERS + PLUGIN_FIELDS_TXT2IMG, 972 | [], 973 | handleTextToImageFromLayersContext, menu="/GimpFusion" 974 | ) 975 | 976 | 977 | gimpfu.register( 978 | "stable-gimpfusion-img2img", 979 | "Image to image", 980 | "Image to image", 981 | "ArtBIT", 982 | "ArtBIT", 983 | "2023", 984 | "Image to image", 985 | "*", 986 | []+ PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_IMG2IMG, 987 | [], 988 | handleImageToImage, menu="/GimpFusion" 989 | ) 990 | 991 | gimpfu.register( 992 | "stable-gimpfusion-img2img-context", 993 | "Image to image", 994 | "Image to image", 995 | "ArtBIT", 996 | "ArtBIT", 997 | "2023", 998 | "Image to image", 999 | "*", 1000 | [] + PLUGIN_FIELDS_LAYERS + PLUGIN_FIELDS_IMG2IMG, 1001 | [], 1002 | handleImageToImageFromLayersContext, menu="/GimpFusion" 1003 | ) 1004 | 1005 | gimpfu.register( 1006 | "stable-gimpfusion-inpainting", 1007 | "Inpainting", 1008 | "Inpainting", 1009 | "ArtBIT", 1010 | "ArtBIT", 1011 | "2023", 1012 | "Inpainting", 1013 | "*", 1014 | []+ PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_IMG2IMG + PLUGIN_FIELDS_INPAINTING, 1015 | [], 1016 | handleInpainting, menu="/GimpFusion" 1017 | ) 1018 | 1019 | gimpfu.register( 1020 | "stable-gimpfusion-inpainting-context", 1021 | "Inpainting", 1022 | "Inpainting", 1023 | "ArtBIT", 1024 | "ArtBIT", 1025 | "2023", 1026 | "Inpainting", 1027 | "*", 1028 | [] + PLUGIN_FIELDS_LAYERS + PLUGIN_FIELDS_IMG2IMG, 1029 | [], 1030 | handleInpaintingFromLayersContext, menu="/GimpFusion" 1031 | ) 1032 | 1033 | gimpfu.register( 1034 | "stable-gimpfusion-config-controlnet-layer", 1035 | "Convert current layer to ControlNet layer or edit ControlNet Layer's options", 1036 | "ControlNet Layer", 1037 | "ArtBIT", 1038 | "ArtBIT", 1039 | "2023", 1040 | "Active layer as ControlNet", 1041 | "*", 1042 | []+ PLUGIN_FIELDS_IMAGE + PLUGIN_FIELDS_CONTROLNET, 1043 | [], 1044 | handleControlNetLayerConfig, menu="/GimpFusion" 1045 | ) 1046 | 1047 | gimpfu.register( 1048 | "stable-gimpfusion-config-controlnet-layer-context", 1049 | "Convert current layer to ControlNet layer or edit ControlNet Layer's options", 1050 | "ControlNet Layer", 1051 | "ArtBIT", 1052 | "ArtBIT", 1053 | "2023", 1054 | "Use as ControlNet", 1055 | "*", 1056 | [] + PLUGIN_FIELDS_LAYERS + PLUGIN_FIELDS_CONTROLNET, 1057 | [], 1058 | handleControlNetLayerConfigFromLayersContext, menu="/GimpFusion" 1059 | ) 1060 | 1061 | gimpfu.register( 1062 | "stable-gimpfusion-layer-info", 1063 | "Show stable gimpfusion info associated with this layer", 1064 | "Layer Info", 1065 | "ArtBIT", 1066 | "ArtBIT", 1067 | "2023", 1068 | "Layer Info", 1069 | "*", 1070 | [] + PLUGIN_FIELDS_IMAGE, 1071 | [], 1072 | handleShowLayerInfo, menu="/GimpFusion/Config" 1073 | ) 1074 | 1075 | gimpfu.register( 1076 | "stable-gimpfusion-layer-info-context", 1077 | "Show stable gimpfusion info associated with this layer", 1078 | "Layer Info", 1079 | "ArtBIT", 1080 | "ArtBIT", 1081 | "2023", 1082 | "Layer Info", 1083 | "*", 1084 | [] + PLUGIN_FIELDS_LAYERS, 1085 | [], 1086 | handleShowLayerInfoContext, menu="/GimpFusion" 1087 | ) 1088 | 1089 | init_plugin() 1090 | gimpfu.main() 1091 | 1092 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 15, 3 | "message": "Fix an issue with changing models", 4 | 5 | "history": [ 6 | { 7 | "version": 14, 8 | "message": "Update controlnet endpoints." 9 | }, 10 | { 11 | "version": 13, 12 | "message": "Update controlnet endpoints." 13 | }, 14 | { 15 | "version": 12, 16 | "message": "Added inpainting options." 17 | }, 18 | { 19 | "version": 11, 20 | "message": "Added support for width/height." 21 | }, 22 | { 23 | "version": 10, 24 | "message": "Added support for the second ControlNet." 25 | }, 26 | { 27 | "version": 9, 28 | "message": "Fixes an issue with the Mac OSX version of GIMP. Added persistent json storage." 29 | }, 30 | { 31 | "version": 8, 32 | "message": "There is newer version of the StableGimpfusion plugin available! It introduces basic ControlNet support." 33 | }, 34 | { 35 | "version": 7, 36 | "message": "There is newer version of the StableGimpfusion plugin available! It introduces basic ControlNet support." 37 | }, 38 | { 39 | "version": 6, 40 | "message": "There is newer version of the StableGimpfusion plugin available! It introduces basic ControlNet support." 41 | }, 42 | { 43 | "version": 5, 44 | "message": "There is newer version of the StableGimpfusion plugin available! It introduces basic ControlNet support." 45 | }, 46 | { "version": 2, "message": "Basic ControlNet support" }, 47 | { 48 | "version": 1, 49 | "message": "New version of stable-diffusion local plugin is available. Please update! Please also rebuild your server." 50 | } 51 | ] 52 | } 53 | --------------------------------------------------------------------------------