sd-backend-client 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. sd_backend_client/__init__.py +1 -0
  2. sd_backend_client/api/__init__.py +1 -0
  3. sd_backend_client/api/a1111_webservice.py +646 -0
  4. sd_backend_client/api/comfyui/__init__.py +1 -0
  5. sd_backend_client/api/comfyui/basic_upscale_workflow_builder.py +40 -0
  6. sd_backend_client/api/comfyui/comfyui_diffusion_params.py +20 -0
  7. sd_backend_client/api/comfyui/comfyui_generation_handle.py +138 -0
  8. sd_backend_client/api/comfyui/comfyui_types.py +265 -0
  9. sd_backend_client/api/comfyui/controlnet_comfyui_utils.py +179 -0
  10. sd_backend_client/api/comfyui/diffusion_workflow_builder.py +620 -0
  11. sd_backend_client/api/comfyui/latent_upscale_workflow_builder.py +300 -0
  12. sd_backend_client/api/comfyui/nodes/__init__.py +1 -0
  13. sd_backend_client/api/comfyui/nodes/apply_upscaler_node.py +28 -0
  14. sd_backend_client/api/comfyui/nodes/basic_scaling_node.py +35 -0
  15. sd_backend_client/api/comfyui/nodes/clip_skip_node.py +32 -0
  16. sd_backend_client/api/comfyui/nodes/comfy_node.py +68 -0
  17. sd_backend_client/api/comfyui/nodes/comfy_node_graph.py +51 -0
  18. sd_backend_client/api/comfyui/nodes/controlnet/__init__.py +1 -0
  19. sd_backend_client/api/comfyui/nodes/controlnet/apply_controlnet_node.py +53 -0
  20. sd_backend_client/api/comfyui/nodes/controlnet/dynamic_preprocessor_node.py +40 -0
  21. sd_backend_client/api/comfyui/nodes/controlnet/load_controlnet_node.py +31 -0
  22. sd_backend_client/api/comfyui/nodes/image_scale_node.py +39 -0
  23. sd_backend_client/api/comfyui/nodes/inpaint_model_conditioning_node.py +45 -0
  24. sd_backend_client/api/comfyui/nodes/input/__init__.py +1 -0
  25. sd_backend_client/api/comfyui/nodes/input/checkpoint_loader_node.py +32 -0
  26. sd_backend_client/api/comfyui/nodes/input/clip_text_encode_node.py +29 -0
  27. sd_backend_client/api/comfyui/nodes/input/empty_latent_image_node.py +31 -0
  28. sd_backend_client/api/comfyui/nodes/input/load_image_mask_node.py +29 -0
  29. sd_backend_client/api/comfyui/nodes/input/load_image_node.py +27 -0
  30. sd_backend_client/api/comfyui/nodes/input/load_upscaler_node.py +23 -0
  31. sd_backend_client/api/comfyui/nodes/input/simple_checkpoint_loader_node.py +25 -0
  32. sd_backend_client/api/comfyui/nodes/ksampler_node.py +52 -0
  33. sd_backend_client/api/comfyui/nodes/latent_mask_node.py +28 -0
  34. sd_backend_client/api/comfyui/nodes/model_extensions/__init__.py +1 -0
  35. sd_backend_client/api/comfyui/nodes/model_extensions/hypernet_loader_node.py +34 -0
  36. sd_backend_client/api/comfyui/nodes/model_extensions/lora_loader_node.py +41 -0
  37. sd_backend_client/api/comfyui/nodes/repeat_latent_node.py +30 -0
  38. sd_backend_client/api/comfyui/nodes/save_image_node.py +26 -0
  39. sd_backend_client/api/comfyui/nodes/ultimate_upscale_node.py +107 -0
  40. sd_backend_client/api/comfyui/nodes/upscale_latent_node.py +39 -0
  41. sd_backend_client/api/comfyui/nodes/vae/__init__.py +1 -0
  42. sd_backend_client/api/comfyui/nodes/vae/vae_decode_node.py +31 -0
  43. sd_backend_client/api/comfyui/nodes/vae/vae_decode_tiled_node.py +44 -0
  44. sd_backend_client/api/comfyui/nodes/vae/vae_encode_node.py +31 -0
  45. sd_backend_client/api/comfyui/nodes/vae/vae_encode_tiled_node.py +42 -0
  46. sd_backend_client/api/comfyui/preprocessor_preview_workflow_builder.py +46 -0
  47. sd_backend_client/api/comfyui/workflow_builder_utils.py +21 -0
  48. sd_backend_client/api/comfyui_webservice.py +686 -0
  49. sd_backend_client/api/shared_data/__init__.py +1 -0
  50. sd_backend_client/api/shared_data/api_datatypes.py +104 -0
  51. sd_backend_client/api/shared_data/controlnet/__init__.py +5 -0
  52. sd_backend_client/api/shared_data/controlnet/controlnet_category_builder.py +86 -0
  53. sd_backend_client/api/shared_data/controlnet/controlnet_constants.py +198 -0
  54. sd_backend_client/api/shared_data/controlnet/controlnet_model.py +74 -0
  55. sd_backend_client/api/shared_data/controlnet/controlnet_preprocessor.py +120 -0
  56. sd_backend_client/api/shared_data/controlnet/controlnet_unit.py +37 -0
  57. sd_backend_client/api/shared_data/diffusion_params.py +82 -0
  58. sd_backend_client/api/shared_data/generation_handle.py +176 -0
  59. sd_backend_client/api/webservice.py +215 -0
  60. sd_backend_client/api/webui/__init__.py +1 -0
  61. sd_backend_client/api/webui/controlnet_webui_constants.py +248 -0
  62. sd_backend_client/api/webui/controlnet_webui_utils.py +113 -0
  63. sd_backend_client/api/webui/diffusion_request_body.py +254 -0
  64. sd_backend_client/api/webui/request_formats.py +161 -0
  65. sd_backend_client/api/webui/response_formats.py +151 -0
  66. sd_backend_client/api/webui/script_info_types.py +35 -0
  67. sd_backend_client/api/webui/webui_generation_handle.py +339 -0
  68. sd_backend_client/py.typed +0 -0
  69. sd_backend_client/util/__init__.py +1 -0
  70. sd_backend_client/util/api_defaults.py +6 -0
  71. sd_backend_client/util/geometry.py +60 -0
  72. sd_backend_client/util/visual/__init__.py +1 -0
  73. sd_backend_client/util/visual/image_utils.py +83 -0
  74. sd_backend_client-0.2.1.dist-info/METADATA +186 -0
  75. sd_backend_client-0.2.1.dist-info/RECORD +78 -0
  76. sd_backend_client-0.2.1.dist-info/WHEEL +5 -0
  77. sd_backend_client-0.2.1.dist-info/licenses/LICENSE +24 -0
  78. sd_backend_client-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """IntraPaint standalone API package."""
@@ -0,0 +1 @@
1
+ """api provides modules handling remote REST APIs."""
@@ -0,0 +1,646 @@
1
+ """
2
+ Accesses the A1111/stable-diffusion-webui through its REST API, providing access to image generation and editing
3
+ through Stable Diffusion.
4
+ """
5
+ import json
6
+ import logging
7
+ from copy import deepcopy
8
+ from typing import Optional, Any, Callable, cast, TYPE_CHECKING
9
+ from typing_extensions import TypedDict
10
+
11
+ import requests # type: ignore
12
+ from PIL import Image # type: ignore
13
+ from requests import Response
14
+
15
+ from sd_backend_client.api.shared_data.controlnet.controlnet_category_builder import ControlNetCategoryBuilder
16
+ from sd_backend_client.api.shared_data.controlnet.controlnet_preprocessor import ControlNetPreprocessor, \
17
+ PreprocessorParams
18
+ from sd_backend_client.api.shared_data.api_datatypes import DiffusionUpscalingParams, REDRAW_MODES, SEAM_FIX_MODES
19
+ from sd_backend_client.api.shared_data.controlnet.controlnet_unit import ControlNetUnit
20
+ from sd_backend_client.api.webservice import WebService, AuthError, DEFAULT_REQUEST_TIMEOUT
21
+ from sd_backend_client.api.webui.controlnet_webui_constants import (ControlNetModelResponse, ControlNetModuleResponse,
22
+ ControlTypeDef, ControlTypeResponse,
23
+ FIRST_GENERIC_PARAMETER_KEY, SECOND_GENERIC_PARAMETER_KEY,
24
+ PREPROCESSOR_RES_PARAM_KEY)
25
+ from sd_backend_client.api.webui.controlnet_webui_utils import get_all_preprocessors
26
+ from sd_backend_client.api.webui.diffusion_request_body import DiffusionRequestBody
27
+ from sd_backend_client.api.webui.request_formats import UpscalingRequestBody
28
+ from sd_backend_client.api.webui.response_formats import GenerationInfoData, ProgressResponseBody, \
29
+ InterrogateResponse, PromptStyleData, SamplerInfo, UpscalerInfo, ModelInfo, VaeInfo, LoraInfo
30
+ from sd_backend_client.api.webui.script_info_types import ScriptResponseData, ScriptInfo
31
+ from sd_backend_client.util.visual.image_utils import (image_to_base64, image_from_base64, image_from_bytes,
32
+ mask_to_grayscale)
33
+
34
+ if TYPE_CHECKING:
35
+ from sd_backend_client.api.webui.webui_generation_handle import WebUIDispatcher, WebUIGenerationHandle
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class ImageResponse(TypedDict):
41
+ """Defines image generation response format."""
42
+ images: list[Image.Image]
43
+ info: Optional[GenerationInfoData]
44
+
45
+
46
+ ULTIMATE_UPSCALE_SCRIPT = 'ultimate sd upscale'
47
+ INTERROGATE_DEFAULT_MODEL = 'clip'
48
+ DEFAULT_GENERATION_TIMEOUT = 600.0
49
+ MAX_LOGIN_ATTEMPTS = 3
50
+ SETTINGS_UPDATE_TIMEOUT = 90
51
+
52
+
53
+ class A1111Webservice(WebService):
54
+ """
55
+ A1111Webservice provides access to the a1111/stable-diffusion-webui through the REST API.
56
+ """
57
+
58
+ # noinspection SpellCheckingInspection
59
+ class Endpoints:
60
+ """REST API endpoint constants."""
61
+ OPTIONS = '/sdapi/v1/options'
62
+ REFRESH_CKPT = '/sdapi/v1/refresh-checkpoints'
63
+ REFRESH_VAE = '/sdapi/v1/refresh-vae'
64
+ REFRESH_LORA = '/sdapi/v1/refresh-loras'
65
+ PROGRESS = '/sdapi/v1/progress'
66
+ IMG2IMG = '/sdapi/v1/img2img'
67
+ TXT2IMG = '/sdapi/v1/txt2img'
68
+ UPSCALE = '/sdapi/v1/extra-single-image'
69
+ INTERROGATE = '/sdapi/v1/interrogate'
70
+ INTERRUPT = '/sdapi/v1/interrupt'
71
+ STYLES = '/sdapi/v1/prompt-styles'
72
+ SCRIPTS = '/sdapi/v1/scripts'
73
+ SCRIPT_INFO = '/sdapi/v1/script-info'
74
+ SAMPLERS = '/sdapi/v1/samplers'
75
+ UPSCALERS = '/sdapi/v1/upscalers'
76
+ LATENT_UPSCALE_MODES = '/sdapi/v1/latent-upscale-modes'
77
+ HYPERNETWORKS = '/sdapi/v1/hypernetworks'
78
+ SD_MODELS = '/sdapi/v1/sd-models'
79
+ VAE_MODELS = '/sdapi/v1/sd-vae'
80
+ LORA_MODELS = '/sdapi/v1/loras'
81
+ CONTROLNET_VERSION = '/controlnet/version'
82
+ CONTROLNET_MODELS = '/controlnet/model_list'
83
+ CONTROLNET_MODULES = '/controlnet/module_list'
84
+ CONTROLNET_CONTROL_TYPES = '/controlnet/control_types'
85
+ CONTROLNET_SETTINGS = '/controlnet/settings'
86
+ CONTROLNET_PREVIEW = '/controlnet/detect'
87
+ LOGIN = '/login'
88
+ EXTRA_NW_DATA = '/sd_extra_networks/metadata'
89
+ EXTRA_NW_THUMB = '/sd_extra_networks/thumb'
90
+ EXTRA_NW_CARD = '/sd_extra_networks/card'
91
+
92
+ class ForgeEndpoints:
93
+ """REST API endpoint constants (Forge WebUI alternates)"""
94
+ SD_MODULES = '/sdapi/v1/sd-modules'
95
+
96
+ def __init__(self, url: str,
97
+ credentials_provider: Optional[Callable[[], Optional[tuple[str, str]]]] = None,
98
+ request_timeout: Optional[float] = DEFAULT_REQUEST_TIMEOUT,
99
+ generation_timeout: Optional[float] = DEFAULT_GENERATION_TIMEOUT) -> None:
100
+ """Create the webservice client.
101
+
102
+ Parameters
103
+ ----------
104
+ url: str
105
+ Base URL of the A1111 / Forge WebUI server.
106
+ credentials_provider: optional callable
107
+ Invoked with no arguments when the server requires authentication. It should return a
108
+ (username, password) pair to attempt, or None to abort (raising AuthError). IntraPaint prompted for
109
+ these through a Qt login dialog; a standalone caller injects its own prompt or fixed credentials here.
110
+ request_timeout: float, optional, default=DEFAULT_REQUEST_TIMEOUT
111
+ Timeout in seconds for metadata, settings and other non-generation requests. None waits indefinitely.
112
+ generation_timeout: float, optional, default=DEFAULT_GENERATION_TIMEOUT
113
+ Timeout in seconds for requests that block until images are ready: txt2img, img2img, upscale and
114
+ ControlNet preprocessor previews. None waits indefinitely.
115
+ """
116
+ super().__init__(url, request_timeout)
117
+ self._generation_timeout = generation_timeout
118
+ self._preprocessor_cache: Optional[list[ControlNetPreprocessor]] = None
119
+ self._credentials_provider = credentials_provider
120
+ self._dispatcher: Optional['WebUIDispatcher'] = None
121
+
122
+ @property
123
+ def generation_timeout(self) -> Optional[float]:
124
+ """Timeout in seconds for requests that block until images are ready, or None to wait indefinitely."""
125
+ return self._generation_timeout
126
+
127
+ @property
128
+ def _generation_dispatcher(self) -> 'WebUIDispatcher':
129
+ """Lazily-created client-side dispatch queue backing the async ``submit_*`` methods."""
130
+ if self._dispatcher is None:
131
+ from sd_backend_client.api.webui.webui_generation_handle import WebUIDispatcher
132
+ self._dispatcher = WebUIDispatcher(self)
133
+ return self._dispatcher
134
+
135
+ def submit_txt2img(self, request_body: Optional[DiffusionRequestBody] = None
136
+ ) -> 'WebUIGenerationHandle':
137
+ """Async counterpart to :meth:`txt2img`: enqueue the job and return a handle immediately.
138
+
139
+ The blocking POST is deferred to the client-side dispatcher (see ``webui_generation_handle``), so
140
+ the returned handle starts ``PENDING`` and is cleanly cancellable until it is actually dispatched.
141
+ Call ``handle.wait()`` for the blocking result, or poll ``handle.poll()`` for progress.
142
+ The request body is copied at submit time, so later changes to the caller's body do not affect the job and
143
+ the body can be reused for further submissions.
144
+ """
145
+ from sd_backend_client.api.webui.webui_generation_handle import create_task_id
146
+ snapshot = DiffusionRequestBody() if request_body is None else request_body.model_copy(deep=True)
147
+ task_id = snapshot.force_task_id or create_task_id('txt2img')
148
+ snapshot.force_task_id = task_id
149
+ return self._generation_dispatcher.submit(lambda: self.txt2img(snapshot), task_id)
150
+
151
+ def submit_img2img(self, image: Image.Image, mask: Optional[Image.Image] = None,
152
+ request_body: Optional[DiffusionRequestBody] = None) -> 'WebUIGenerationHandle':
153
+ """Async counterpart to :meth:`img2img`: enqueue the job and return a handle immediately.
154
+
155
+ See :meth:`submit_txt2img` for the dispatch/cancel semantics. The image, mask and request body are copied at
156
+ submit time.
157
+ """
158
+ from sd_backend_client.api.webui.webui_generation_handle import create_task_id
159
+ snapshot = DiffusionRequestBody() if request_body is None else request_body.model_copy(deep=True)
160
+ image_copy = image.copy()
161
+ mask_copy = None if mask is None else mask.copy()
162
+ task_id = snapshot.force_task_id or create_task_id('img2img')
163
+ snapshot.force_task_id = task_id
164
+ return self._generation_dispatcher.submit(lambda: self.img2img(image_copy, mask_copy, snapshot), task_id)
165
+
166
+ # General utility:
167
+ def login_check(self):
168
+ """Calls the login check endpoint, returning a status 401 response if a login is required."""
169
+ return self.get('/login_check')
170
+
171
+ def set_config(self, config_updates: dict) -> None:
172
+ """
173
+ Updates the Stable Diffusion WebUI configuration.
174
+
175
+ Parameters
176
+ ----------
177
+ config_updates: dict
178
+ Maps settings that should change to their updated values. Use the get_settings method's response body
179
+ to check available options.
180
+ """
181
+ self.post(A1111Webservice.Endpoints.OPTIONS, config_updates, timeout=SETTINGS_UPDATE_TIMEOUT).json()
182
+
183
+ def refresh_checkpoints(self) -> requests.Response:
184
+ """Requests an updated list of available Stable Diffusion models.
185
+
186
+ Returns
187
+ -------
188
+ response
189
+ HTTP response with the list of updated Stable Diffusion models.
190
+ """
191
+ return self.post(A1111Webservice.Endpoints.REFRESH_CKPT, body={})
192
+
193
+ def refresh_vae(self) -> requests.Response:
194
+ """Requests an updated list of available Stable Diffusion VAE models.
195
+
196
+ VAE models handle the conversion between images and the latent image space. Different VAE models can be used
197
+ to adjust performance and final image quality.
198
+
199
+
200
+ Returns
201
+ -------
202
+ response
203
+ HTTP response with the list of updated Stable Diffusion VAE models.
204
+ """
205
+ return self.post(A1111Webservice.Endpoints.REFRESH_VAE, body={})
206
+
207
+ def refresh_loras(self) -> requests.Response:
208
+ """Requests an updated list of available Stable Diffusion LoRA models.
209
+
210
+ LoRA models augment existing Stable Diffusion models, usually to provide support for new concepts, characters,
211
+ or art styles.
212
+
213
+ Returns
214
+ -------
215
+ response
216
+ HTTP response with the list of updated Stable Diffusion LoRA models.
217
+ """
218
+ return self.post(A1111Webservice.Endpoints.REFRESH_LORA, body={})
219
+
220
+ def progress_check(self) -> ProgressResponseBody:
221
+ """Checks the progress of an ongoing image operation."""
222
+ return ProgressResponseBody.model_validate(
223
+ self.get(A1111Webservice.Endpoints.PROGRESS).json())
224
+
225
+ # Image manipulation:
226
+ def img2img(self, image: Image.Image, mask: Optional[Image.Image] = None,
227
+ request_body: Optional[DiffusionRequestBody] = None) -> ImageResponse:
228
+ """Sends a request to alter an image section using selected parameters.
229
+
230
+ Parameters
231
+ ----------
232
+ image: Image.Image
233
+ Source image to transform. If request_body is not None, and it already has an image, this parameter is
234
+ ignored.
235
+ mask: Optional[Image.Image] = None
236
+ Optional inpainting mask. This will also be ignored if request_body is not None, and it already has a mask.
237
+ request_body : Optional[DiffusionRequestBody] = None
238
+ Optional initial request body to use. If None, a default DiffusionRequestBody is used.
239
+ The caller's body is not modified.
240
+ Returns
241
+ -------
242
+ ImageResponse
243
+ All generated images, plus accompanying image generation data if available.
244
+ """
245
+ if request_body is None:
246
+ request_body = DiffusionRequestBody()
247
+ request_body.init_images = [image]
248
+ request_body.mask=mask
249
+ else:
250
+ request_body = request_body.model_copy()
251
+ if not request_body.init_images:
252
+ request_body.init_images = [image]
253
+ else:
254
+ request_body.init_images = list(request_body.init_images)
255
+ if request_body.mask is None and mask is not None:
256
+ request_body.mask = mask
257
+ res = self.post(A1111Webservice.Endpoints.IMG2IMG, request_body.to_dict(), timeout=self._generation_timeout)
258
+ return self._handle_image_response(res)
259
+
260
+ def txt2img(self, request_body: Optional[DiffusionRequestBody] = None) -> ImageResponse:
261
+ """Sends a request to generate new images using selected parameter.
262
+
263
+ Parameters
264
+ ----------
265
+ request_body : Optional[DiffusionRequestBody] = None
266
+ Optional initial request body to use. If None, a new one will be constructed from default parameters.
267
+ Returns
268
+ -------
269
+ ImageResponse
270
+ All generated images, plus accompanying image generation data if available.
271
+ """
272
+ if request_body is None:
273
+ request_body = DiffusionRequestBody()
274
+ res = self.post(A1111Webservice.Endpoints.TXT2IMG, request_body.to_dict(), timeout=self._generation_timeout)
275
+ return self._handle_image_response(res)
276
+
277
+ def controlnet_preprocessor_preview(self, image: Image.Image, mask: Optional[Image.Image],
278
+ preprocessor: ControlNetPreprocessor | PreprocessorParams) -> Image.Image:
279
+ """Gets a preview image for a ControlNet preprocessor.
280
+
281
+ `preprocessor` may be a bare `ControlNetPreprocessor`, in which case the server applies its own
282
+ parameter defaults, or a `PreprocessorParams` to override threshold_a/threshold_b/processor_res.
283
+ """
284
+ input_images: list[str] = [image_to_base64(image, True)]
285
+ if mask is not None:
286
+ input_images.append(image_to_base64(mask_to_grayscale(mask), True))
287
+ typedef = preprocessor.typedef if isinstance(preprocessor, PreprocessorParams) else preprocessor
288
+ body: dict[str, int | float | str | list[str]] = {
289
+ 'controlnet_module': typedef.name,
290
+ 'controlnet_input_images': input_images
291
+ }
292
+ if isinstance(preprocessor, PreprocessorParams):
293
+ detect_param_keys = {
294
+ FIRST_GENERIC_PARAMETER_KEY: 'controlnet_threshold_a',
295
+ SECOND_GENERIC_PARAMETER_KEY: 'controlnet_threshold_b',
296
+ PREPROCESSOR_RES_PARAM_KEY: 'controlnet_processor_res',
297
+ }
298
+ for param_key, detect_key in detect_param_keys.items():
299
+ if param_key in preprocessor.parameter_values:
300
+ body[detect_key] = preprocessor.parameter_values[param_key]
301
+ res = self.post(A1111Webservice.Endpoints.CONTROLNET_PREVIEW, body, timeout=self._generation_timeout)
302
+ return self._handle_image_response(res)['images'][0]
303
+
304
+ def _validate_tile_controlnet(self, tile_control_unit: Optional[ControlNetUnit]) -> Optional[ControlNetUnit]:
305
+ """Return the tile ControlNet unit only if it's fully specified and installed on the server, else None."""
306
+ if tile_control_unit is None:
307
+ return None
308
+ if (tile_control_unit.model is None
309
+ or tile_control_unit.preprocessor is None
310
+ or float(tile_control_unit.control_strength) == 0.0
311
+ or float(tile_control_unit.control_start) >= float(tile_control_unit.control_end)):
312
+ return None
313
+ models = self.get_controlnet_models().model_list
314
+ preprocessors = [preprocessor.name for preprocessor in self.get_controlnet_preprocessors()]
315
+ if (tile_control_unit.model.full_model_name not in models
316
+ or tile_control_unit.preprocessor.typedef.name not in preprocessors):
317
+ return None
318
+ return tile_control_unit
319
+
320
+ def upscale(self,
321
+ image: Image.Image,
322
+ width: int,
323
+ height: int,
324
+ sd_upscale_params: Optional[DiffusionUpscalingParams] = None) -> ImageResponse:
325
+ """Sends a request to upscale an image.
326
+
327
+ Parameters
328
+ ----------
329
+ image : Image.Image
330
+ Source image to upscale.
331
+ width : int
332
+ New image width in pixels requested.
333
+ height : int
334
+ New image height in pixels requested.
335
+ sd_upscale_params : Optional[DiffusionUpscalingParams]
336
+ Parameters for stable diffusion upscaling using the "Ultimate SD Upscale" workflow. If None, only basic
337
+ upscaling will be used.
338
+ Returns
339
+ -------
340
+ ImageResponse
341
+ The generated image, plus accompanying image generation data if available.
342
+ """
343
+ upscale_options = [upscaler.name for upscaler in self.get_upscalers()]
344
+ # Default to the first real upscaler rather than upscale_options[0], which is the no-op 'None' upscaler that
345
+ # would leave the image unchanged.
346
+ upscaler: str = next((name for name in upscale_options if name.lower() != 'none'), upscale_options[0])
347
+ if sd_upscale_params is not None and sd_upscale_params.upscaling_mode in upscale_options:
348
+ upscaler = sd_upscale_params.upscaling_mode
349
+
350
+ if sd_upscale_params is not None and sd_upscale_params.use_stable_diffusion_upscaling:
351
+ # Populate the core diffusion pass (prompt, seed, cfg, sampler, checkpoint, ...) from diffusion_params,
352
+ # then override the upscale-specific bits, which take precedence:
353
+ request_body = DiffusionRequestBody(**sd_upscale_params.diffusion_params.model_dump())
354
+ request_body.init_images = [image]
355
+ request_body.denoising_strength = sd_upscale_params.denoising_strength
356
+ request_body.steps = sd_upscale_params.step_count
357
+ request_body.width = width
358
+ request_body.height = height
359
+ request_body.batch_size = 1
360
+ request_body.n_iter = 1
361
+
362
+ # Attach the optional tile ControlNet via controlnet_units; DiffusionRequestBody.to_dict() serializes these
363
+ # into alwayson_scripts['controlnet']. (Setting alwayson_scripts directly here would be wiped by to_dict.)
364
+ tile_control_unit = self._validate_tile_controlnet(sd_upscale_params.tile_controlnet)
365
+ request_body.controlnet_units = [tile_control_unit] if tile_control_unit is not None else []
366
+
367
+ if sd_upscale_params.use_ultimate_upscale_script:
368
+ request_body.script_name = ULTIMATE_UPSCALE_SCRIPT
369
+ # Positional args for the "ultimate sd upscale" script (order confirmed via /sdapi/v1/script-info):
370
+ request_body.script_args = [
371
+ None, # [0] not used
372
+ sd_upscale_params.tile_width, # [1] tile width
373
+ sd_upscale_params.tile_height, # [2] tile height
374
+ sd_upscale_params.mask_blur, # [3] mask_blur
375
+ sd_upscale_params.tile_padding, # [4] tile padding
376
+ sd_upscale_params.seam_fix_width, # [5] seams_fix_width
377
+ sd_upscale_params.seam_fix_denoise, # [6] seams_fix_denoise
378
+ sd_upscale_params.seam_fix_padding, # [7] seams_fix_padding
379
+ upscale_options.index(upscaler), # [8] upscaler_index (into /sdapi/v1/upscalers)
380
+ sd_upscale_params.save_upscaled_image, # [9] save_upscaled_image
381
+ REDRAW_MODES.index(sd_upscale_params.redraw_mode), # [10] redraw mode (0=Linear,1=Chess,2=None)
382
+ sd_upscale_params.save_seams_fix_image, # [11] save_seams_fix_image
383
+ sd_upscale_params.seam_fix_mask_blur, # [12] seams_fix_mask_blur
384
+ SEAM_FIX_MODES.index(sd_upscale_params.seam_fix_mode), # [13] seams_fix_type
385
+ 1, # [14] target_size_type (1 = use custom width/height below)
386
+ width, # [15] custom_width
387
+ height, # [16] custom_height
388
+ None # [17] custom_scale (ignored when target_size_type=1)
389
+ ]
390
+ return self.img2img(image, None, request_body)
391
+ # otherwise, normal upscaling without controlNet:
392
+ body: UpscalingRequestBody = {
393
+ 'resize_mode': 1,
394
+ 'upscaling_resize_w': width,
395
+ 'upscaling_resize_h': height,
396
+ 'upscaler_1': upscaler,
397
+ 'image': image_to_base64(image, include_prefix=True)
398
+ }
399
+ res = self.post(A1111Webservice.Endpoints.UPSCALE, body, timeout=self._generation_timeout)
400
+ return self._handle_image_response(res)
401
+
402
+ def interrogate(self, image: Image.Image, interrogate_model: Optional[str] = None) -> str:
403
+ """Requests text describing an image.
404
+
405
+ Parameters
406
+ ----------
407
+ image : PIL Image
408
+ The image to describe.
409
+ interrogate_model : Optional[str]
410
+ Specific image interrogation model to use. Must be supported by the backend. Defaults to
411
+ INTERROGATE_DEFAULT_MODEL.
412
+ Returns
413
+ -------
414
+ str
415
+ A brief description of the image.
416
+ """
417
+ if interrogate_model is None:
418
+ interrogate_model = INTERROGATE_DEFAULT_MODEL
419
+ body = {
420
+ 'model': interrogate_model,
421
+ 'image': image_to_base64(image, include_prefix=True)
422
+ }
423
+ res = self.post(A1111Webservice.Endpoints.INTERROGATE, body, timeout=60).json()
424
+ if isinstance(res, dict):
425
+ res = InterrogateResponse.model_validate(res)
426
+ return res.caption
427
+ assert isinstance(res, str)
428
+ return res
429
+
430
+ def interrupt(self) -> dict:
431
+ """
432
+ Attempts to interrupt an ongoing image operation, returning a dict from the response body indicating the
433
+ result.
434
+ """
435
+ res = self.post(A1111Webservice.Endpoints.INTERRUPT, body={})
436
+ return res.json()
437
+
438
+ @staticmethod
439
+ def _handle_image_response(res: Response) -> ImageResponse:
440
+ if res.status_code != 200:
441
+ raise RuntimeError(res.json())
442
+ res_body = res.json()
443
+ images = []
444
+ info_data: Optional[GenerationInfoData] = None
445
+ if 'images' in res_body:
446
+ for image in res_body['images']:
447
+ images.append(image_from_base64(image))
448
+ info = res_body.get('info') # absent on some endpoints (e.g. /controlnet/detect)
449
+ if isinstance(info, str):
450
+ try:
451
+ info_data = GenerationInfoData.model_validate(json.loads(info))
452
+ except json.JSONDecodeError:
453
+ logger.error(f'Image response info not valid JSON, got {info}')
454
+ info_data = None
455
+ elif info is not None:
456
+ info_data = GenerationInfoData.model_validate(info)
457
+ elif 'image' in res_body: # basic upscaling result
458
+ images = [image_from_base64(res_body['image'])]
459
+ info_data = None
460
+ image_response: ImageResponse = {
461
+ 'images': images,
462
+ 'info': info_data
463
+ }
464
+ return image_response
465
+
466
+ # Load misc. service info:
467
+ def get_config(self) -> dict[str, Any]:
468
+ """Returns a dict containing the current Stable Diffusion WebUI configuration."""
469
+ return self.get('/sdapi/v1/options').json()
470
+
471
+ def get_styles(self) -> list[PromptStyleData]:
472
+ """Returns a list of image generation style objects saved by the Stable Diffusion WebUI."""
473
+ res_body = self.get(A1111Webservice.Endpoints.STYLES).json()
474
+ all_styles: list[PromptStyleData] = []
475
+ for serialized_style in res_body:
476
+ all_styles.append(PromptStyleData.model_validate(serialized_style))
477
+ return all_styles
478
+
479
+ def get_scripts(self) -> ScriptResponseData:
480
+ """Returns available scripts installed to the Stable Diffusion WebUI.
481
+ Returns
482
+ -------
483
+ dict
484
+ Response will have 'txt2img' and 'img2img' keys, each holding a list of scripts available for that mode.
485
+ """
486
+ return ScriptResponseData.model_validate(self.get(A1111Webservice.Endpoints.SCRIPTS).json())
487
+
488
+ def get_script_info(self) -> list[ScriptInfo]:
489
+ """Returns information on expected script parameters
490
+ Returns
491
+ -------
492
+ list of dict
493
+ Objects defining all parameters required by each script.
494
+ """
495
+ return [ScriptInfo.model_validate(item) for item in self.get(A1111Webservice.Endpoints.SCRIPT_INFO).json()]
496
+
497
+ def _get_name_list(self, endpoint: str) -> list[str]:
498
+ res_body = self.get(endpoint).json()
499
+ return [obj['name'] for obj in res_body]
500
+
501
+ def get_samplers(self) -> list[SamplerInfo]:
502
+ """Returns the list of image sampler algorithms available for image generation."""
503
+ return [SamplerInfo.model_validate(item)
504
+ for item in self.get(A1111Webservice.Endpoints.SAMPLERS).json()]
505
+
506
+ def get_upscalers(self) -> list[UpscalerInfo]:
507
+ """Returns the list of image upscalers available."""
508
+ return [UpscalerInfo.model_validate(item)
509
+ for item in self.get(A1111Webservice.Endpoints.UPSCALERS).json()]
510
+
511
+ def get_latent_upscale_modes(self) -> list[str]:
512
+ """Returns the list of Stable Diffusion enhanced upscaling modes."""
513
+ return self._get_name_list(A1111Webservice.Endpoints.LATENT_UPSCALE_MODES)
514
+
515
+ def get_hypernetworks(self) -> list[str]:
516
+ """Returns the list of hypernetworks available.
517
+
518
+ Hypernetworks are a simpler form of model for augmenting full Stable Diffusion models. Each hypernetwork
519
+ introduces a single style or concept.
520
+ """
521
+ return self._get_name_list(A1111Webservice.Endpoints.HYPERNETWORKS)
522
+
523
+ def get_models(self) -> list[ModelInfo]:
524
+ """Returns the list of available Stable Diffusion models cached by the webui.
525
+
526
+ If available models may have changed, instead consider using the slower refresh_checkpoints method.
527
+ """
528
+ return [ModelInfo.model_validate(item)
529
+ for item in self.get(A1111Webservice.Endpoints.SD_MODELS).json()]
530
+
531
+ def get_vae(self) -> list[VaeInfo]:
532
+ """Returns the list of available Stable Diffusion VAE models cached by the webui.
533
+
534
+ If available models may have changed, instead consider using the slower refresh_vae method.
535
+ """
536
+ try:
537
+ vae_models = self.get(A1111Webservice.Endpoints.VAE_MODELS).json()
538
+ except RuntimeError:
539
+ vae_models = self.get(A1111Webservice.ForgeEndpoints.SD_MODULES).json()
540
+ return [VaeInfo.model_validate(item) for item in vae_models]
541
+
542
+ def get_controlnet_version(self) -> int:
543
+ """
544
+ Returns the installed version of the Stable Diffusion ControlNet extension, or raises if the exception is not
545
+ installed.
546
+ """
547
+ return self.get(A1111Webservice.Endpoints.CONTROLNET_VERSION).json()['version']
548
+
549
+ def get_controlnet_models(self) -> ControlNetModelResponse:
550
+ """Returns a dict defining the models available to the Stable Diffusion ControlNet extension."""
551
+ return ControlNetModelResponse.model_validate(
552
+ self.get(A1111Webservice.Endpoints.CONTROLNET_MODELS).json())
553
+
554
+ def get_controlnet_modules(self) -> ControlNetModuleResponse:
555
+ """Returns a dict defining the modules available to the Stable Diffusion ControlNet extension."""
556
+ return ControlNetModuleResponse.model_validate(
557
+ self.get(A1111Webservice.Endpoints.CONTROLNET_MODULES).json())
558
+
559
+ def get_controlnet_control_types(self) -> ControlTypeResponse:
560
+ """Returns a dict defining the control types available to the Stable Diffusion ControlNet extension."""
561
+ return cast(ControlTypeResponse, self.get(A1111Webservice.Endpoints.CONTROLNET_CONTROL_TYPES).json())
562
+
563
+ def get_controlnet_settings(self) -> dict[str, Any]:
564
+ """Returns the current settings applied to the Stable Diffusion ControlNet extension."""
565
+ return self.get(A1111Webservice.Endpoints.CONTROLNET_SETTINGS).json()
566
+
567
+ def get_controlnet_preprocessors(self, update_cache=False) -> list[ControlNetPreprocessor]:
568
+ """Queries the API for ControlNet preprocessor modules, and parameterizes and returns all options."""
569
+ if update_cache or self._preprocessor_cache is None:
570
+ modules = self.get_controlnet_modules()
571
+ module_names = modules.module_list
572
+ module_details = modules.module_details
573
+ self._preprocessor_cache = get_all_preprocessors(module_names, module_details)
574
+ assert self._preprocessor_cache is not None
575
+ return deepcopy(self._preprocessor_cache)
576
+
577
+ def get_controlnet_type_categories(self) -> dict[str, ControlTypeDef]:
578
+ """Gets the set of valid ControlNet preprocessor/model categories, taking into account available options and
579
+ API category definitions if possible."""
580
+ modules = self.get_controlnet_modules()
581
+ models = self.get_controlnet_models()
582
+ try:
583
+ control_type_defs = self.get_controlnet_control_types()
584
+ except (KeyError, RuntimeError):
585
+ control_type_defs = None
586
+ preprocessor_names = modules.module_list
587
+ model_names = models.model_list
588
+ control_type_builder = ControlNetCategoryBuilder(preprocessor_names, model_names, None, control_type_defs)
589
+ return control_type_builder.get_control_types()
590
+
591
+ def get_loras(self) -> list[LoraInfo]:
592
+ """Returns the list of available Stable Diffusion LoRA models cached by the webui.
593
+
594
+ If available models may have changed, instead consider using the slower refresh_loras method.
595
+ """
596
+ return [LoraInfo.model_validate(item)
597
+ for item in self.get(A1111Webservice.Endpoints.LORA_MODELS).json()]
598
+
599
+ def get_thumbnail(self, file_path: str) -> Optional[Image.Image]:
600
+ """Attempts to load one of the extra model thumbnails given a path parameter."""
601
+ try:
602
+ res = self.get(A1111Webservice.Endpoints.EXTRA_NW_THUMB,
603
+ url_params={'filename': file_path})
604
+ if not res.ok:
605
+ return None
606
+ return image_from_bytes(res.content)
607
+ except (RuntimeError, IOError) as err:
608
+ logger.error(f'Failed to load thumbnail "{file_path}": {err}')
609
+ return None
610
+
611
+ def login(self, username: str, password: str) -> requests.Response:
612
+ """Attempt to log in with a username and password."""
613
+ body = {'username': username, 'password': password}
614
+ return self.post(A1111Webservice.Endpoints.LOGIN, body, 'x-www-form-urlencoded',
615
+ throw_on_failure=False)
616
+
617
+ def _handle_auth_error(self):
618
+ """Asks the credentials provider for credentials, up to MAX_LOGIN_ATTEMPTS times.
619
+
620
+ Credentials are checked with an authenticated GET to an /sdapi/v1/ endpoint, which is what --api-auth
621
+ protects. They are installed on the session only once accepted. Raises AuthError if no provider is
622
+ configured, the provider returns None, or every attempt is rejected."""
623
+ if self._credentials_provider is None:
624
+ raise AuthError('Authentication required, but no credentials_provider was configured.')
625
+ for attempt in range(1, MAX_LOGIN_ATTEMPTS + 1):
626
+ credentials = self._credentials_provider()
627
+ if credentials is None:
628
+ logger.info('Login aborted')
629
+ raise AuthError('Login aborted: the credentials provider returned no credentials.')
630
+ previous_auth = self._session.auth
631
+ self.set_auth(credentials)
632
+ try:
633
+ response = self.get(A1111Webservice.Endpoints.PROGRESS,
634
+ url_params={'skip_current_image': 'true'},
635
+ fail_on_auth_error=True, throw_on_failure=False)
636
+ except BaseException:
637
+ self.set_auth(previous_auth)
638
+ raise
639
+ if response.status_code != 401:
640
+ if not response.ok:
641
+ logger.warning(f'Credential check returned status {response.status_code}')
642
+ return
643
+ self.set_auth(previous_auth)
644
+ logger.warning(f'Login attempt {attempt} of {MAX_LOGIN_ATTEMPTS} was rejected (status 401).')
645
+ raise AuthError(f'Authentication failed after {MAX_LOGIN_ATTEMPTS} attempts: the server returned status 401.'
646
+ ' Check the credentials and the server\'s --api-auth setting.')
@@ -0,0 +1 @@
1
+ """Misc. data definitions and helper classes for ComfyUIWebservice."""