├── 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 | [](https://github.com/ArtBIT/stable-gimpfusion) [](https://github.com/ArtBIT/stable-gimpfusion) [](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 |
--------------------------------------------------------------------------------