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.
- sd_backend_client/__init__.py +1 -0
- sd_backend_client/api/__init__.py +1 -0
- sd_backend_client/api/a1111_webservice.py +646 -0
- sd_backend_client/api/comfyui/__init__.py +1 -0
- sd_backend_client/api/comfyui/basic_upscale_workflow_builder.py +40 -0
- sd_backend_client/api/comfyui/comfyui_diffusion_params.py +20 -0
- sd_backend_client/api/comfyui/comfyui_generation_handle.py +138 -0
- sd_backend_client/api/comfyui/comfyui_types.py +265 -0
- sd_backend_client/api/comfyui/controlnet_comfyui_utils.py +179 -0
- sd_backend_client/api/comfyui/diffusion_workflow_builder.py +620 -0
- sd_backend_client/api/comfyui/latent_upscale_workflow_builder.py +300 -0
- sd_backend_client/api/comfyui/nodes/__init__.py +1 -0
- sd_backend_client/api/comfyui/nodes/apply_upscaler_node.py +28 -0
- sd_backend_client/api/comfyui/nodes/basic_scaling_node.py +35 -0
- sd_backend_client/api/comfyui/nodes/clip_skip_node.py +32 -0
- sd_backend_client/api/comfyui/nodes/comfy_node.py +68 -0
- sd_backend_client/api/comfyui/nodes/comfy_node_graph.py +51 -0
- sd_backend_client/api/comfyui/nodes/controlnet/__init__.py +1 -0
- sd_backend_client/api/comfyui/nodes/controlnet/apply_controlnet_node.py +53 -0
- sd_backend_client/api/comfyui/nodes/controlnet/dynamic_preprocessor_node.py +40 -0
- sd_backend_client/api/comfyui/nodes/controlnet/load_controlnet_node.py +31 -0
- sd_backend_client/api/comfyui/nodes/image_scale_node.py +39 -0
- sd_backend_client/api/comfyui/nodes/inpaint_model_conditioning_node.py +45 -0
- sd_backend_client/api/comfyui/nodes/input/__init__.py +1 -0
- sd_backend_client/api/comfyui/nodes/input/checkpoint_loader_node.py +32 -0
- sd_backend_client/api/comfyui/nodes/input/clip_text_encode_node.py +29 -0
- sd_backend_client/api/comfyui/nodes/input/empty_latent_image_node.py +31 -0
- sd_backend_client/api/comfyui/nodes/input/load_image_mask_node.py +29 -0
- sd_backend_client/api/comfyui/nodes/input/load_image_node.py +27 -0
- sd_backend_client/api/comfyui/nodes/input/load_upscaler_node.py +23 -0
- sd_backend_client/api/comfyui/nodes/input/simple_checkpoint_loader_node.py +25 -0
- sd_backend_client/api/comfyui/nodes/ksampler_node.py +52 -0
- sd_backend_client/api/comfyui/nodes/latent_mask_node.py +28 -0
- sd_backend_client/api/comfyui/nodes/model_extensions/__init__.py +1 -0
- sd_backend_client/api/comfyui/nodes/model_extensions/hypernet_loader_node.py +34 -0
- sd_backend_client/api/comfyui/nodes/model_extensions/lora_loader_node.py +41 -0
- sd_backend_client/api/comfyui/nodes/repeat_latent_node.py +30 -0
- sd_backend_client/api/comfyui/nodes/save_image_node.py +26 -0
- sd_backend_client/api/comfyui/nodes/ultimate_upscale_node.py +107 -0
- sd_backend_client/api/comfyui/nodes/upscale_latent_node.py +39 -0
- sd_backend_client/api/comfyui/nodes/vae/__init__.py +1 -0
- sd_backend_client/api/comfyui/nodes/vae/vae_decode_node.py +31 -0
- sd_backend_client/api/comfyui/nodes/vae/vae_decode_tiled_node.py +44 -0
- sd_backend_client/api/comfyui/nodes/vae/vae_encode_node.py +31 -0
- sd_backend_client/api/comfyui/nodes/vae/vae_encode_tiled_node.py +42 -0
- sd_backend_client/api/comfyui/preprocessor_preview_workflow_builder.py +46 -0
- sd_backend_client/api/comfyui/workflow_builder_utils.py +21 -0
- sd_backend_client/api/comfyui_webservice.py +686 -0
- sd_backend_client/api/shared_data/__init__.py +1 -0
- sd_backend_client/api/shared_data/api_datatypes.py +104 -0
- sd_backend_client/api/shared_data/controlnet/__init__.py +5 -0
- sd_backend_client/api/shared_data/controlnet/controlnet_category_builder.py +86 -0
- sd_backend_client/api/shared_data/controlnet/controlnet_constants.py +198 -0
- sd_backend_client/api/shared_data/controlnet/controlnet_model.py +74 -0
- sd_backend_client/api/shared_data/controlnet/controlnet_preprocessor.py +120 -0
- sd_backend_client/api/shared_data/controlnet/controlnet_unit.py +37 -0
- sd_backend_client/api/shared_data/diffusion_params.py +82 -0
- sd_backend_client/api/shared_data/generation_handle.py +176 -0
- sd_backend_client/api/webservice.py +215 -0
- sd_backend_client/api/webui/__init__.py +1 -0
- sd_backend_client/api/webui/controlnet_webui_constants.py +248 -0
- sd_backend_client/api/webui/controlnet_webui_utils.py +113 -0
- sd_backend_client/api/webui/diffusion_request_body.py +254 -0
- sd_backend_client/api/webui/request_formats.py +161 -0
- sd_backend_client/api/webui/response_formats.py +151 -0
- sd_backend_client/api/webui/script_info_types.py +35 -0
- sd_backend_client/api/webui/webui_generation_handle.py +339 -0
- sd_backend_client/py.typed +0 -0
- sd_backend_client/util/__init__.py +1 -0
- sd_backend_client/util/api_defaults.py +6 -0
- sd_backend_client/util/geometry.py +60 -0
- sd_backend_client/util/visual/__init__.py +1 -0
- sd_backend_client/util/visual/image_utils.py +83 -0
- sd_backend_client-0.2.1.dist-info/METADATA +186 -0
- sd_backend_client-0.2.1.dist-info/RECORD +78 -0
- sd_backend_client-0.2.1.dist-info/WHEEL +5 -0
- sd_backend_client-0.2.1.dist-info/licenses/LICENSE +24 -0
- 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."""
|