SuperRealCo commited on
Commit
1096f5d
·
verified ·
1 Parent(s): 9859f82

Delete server.py

Browse files
Files changed (1) hide show
  1. server.py +0 -975
server.py DELETED
@@ -1,975 +0,0 @@
1
- import os
2
- import sys
3
- import asyncio
4
- import traceback
5
-
6
- import nodes
7
- import folder_paths
8
- import execution
9
- import uuid
10
- import urllib
11
- import json
12
- import glob
13
- import struct
14
- import ssl
15
- import socket
16
- import ipaddress
17
- from PIL import Image, ImageOps
18
- from PIL.PngImagePlugin import PngInfo
19
- from io import BytesIO
20
-
21
- import aiohttp
22
- from aiohttp import web
23
- import logging
24
-
25
- import mimetypes
26
- from comfy.cli_args import args
27
- import comfy.utils
28
- import comfy.model_management
29
- from comfy_api import feature_flags
30
- import node_helpers
31
- from comfyui_version import __version__
32
- from app.frontend_management import FrontendManager
33
-
34
- from app.user_manager import UserManager
35
- from app.model_manager import ModelFileManager
36
- from app.custom_node_manager import CustomNodeManager
37
- from typing import Optional, Union
38
- from api_server.routes.internal.internal_routes import InternalRoutes
39
- from protocol import BinaryEventTypes
40
-
41
- async def send_socket_catch_exception(function, message):
42
- try:
43
- await function(message)
44
- except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError, BrokenPipeError, ConnectionError) as err:
45
- logging.warning("send error: {}".format(err))
46
-
47
- @web.middleware
48
- async def cache_control(request: web.Request, handler):
49
- response: web.Response = await handler(request)
50
- if request.path.endswith('.js') or request.path.endswith('.css') or request.path.endswith('index.json'):
51
- response.headers.setdefault('Cache-Control', 'no-cache')
52
- return response
53
-
54
-
55
- @web.middleware
56
- async def compress_body(request: web.Request, handler):
57
- accept_encoding = request.headers.get("Accept-Encoding", "")
58
- response: web.Response = await handler(request)
59
- if not isinstance(response, web.Response):
60
- return response
61
- if response.content_type not in ["application/json", "text/plain"]:
62
- return response
63
- if response.body and "gzip" in accept_encoding:
64
- response.enable_compression()
65
- return response
66
-
67
-
68
- def create_cors_middleware(allowed_origin: str):
69
- @web.middleware
70
- async def cors_middleware(request: web.Request, handler):
71
- if request.method == "OPTIONS":
72
- # Pre-flight request. Reply successfully:
73
- response = web.Response()
74
- else:
75
- response = await handler(request)
76
-
77
- response.headers['Access-Control-Allow-Origin'] = allowed_origin
78
- response.headers['Access-Control-Allow-Methods'] = 'POST, GET, DELETE, PUT, OPTIONS'
79
- response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
80
- response.headers['Access-Control-Allow-Credentials'] = 'true'
81
- return response
82
-
83
- return cors_middleware
84
-
85
- def is_loopback(host):
86
- if host is None:
87
- return False
88
- try:
89
- if ipaddress.ip_address(host).is_loopback:
90
- return True
91
- else:
92
- return False
93
- except:
94
- pass
95
-
96
- loopback = False
97
- for family in (socket.AF_INET, socket.AF_INET6):
98
- try:
99
- r = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
100
- for family, _, _, _, sockaddr in r:
101
- if not ipaddress.ip_address(sockaddr[0]).is_loopback:
102
- return loopback
103
- else:
104
- loopback = True
105
- except socket.gaierror:
106
- pass
107
-
108
- return loopback
109
-
110
-
111
- def create_origin_only_middleware():
112
- @web.middleware
113
- async def origin_only_middleware(request: web.Request, handler):
114
- #this code is used to prevent the case where a random website can queue comfy workflows by making a POST to 127.0.0.1 which browsers don't prevent for some dumb reason.
115
- #in that case the Host and Origin hostnames won't match
116
- #I know the proper fix would be to add a cookie but this should take care of the problem in the meantime
117
- if 'Host' in request.headers and 'Origin' in request.headers:
118
- host = request.headers['Host']
119
- origin = request.headers['Origin']
120
- host_domain = host.lower()
121
- parsed = urllib.parse.urlparse(origin)
122
- origin_domain = parsed.netloc.lower()
123
- host_domain_parsed = urllib.parse.urlsplit('//' + host_domain)
124
-
125
- #limit the check to when the host domain is localhost, this makes it slightly less safe but should still prevent the exploit
126
- loopback = is_loopback(host_domain_parsed.hostname)
127
-
128
- if parsed.port is None: #if origin doesn't have a port strip it from the host to handle weird browsers, same for host
129
- host_domain = host_domain_parsed.hostname
130
- if host_domain_parsed.port is None:
131
- origin_domain = parsed.hostname
132
-
133
- if loopback and host_domain is not None and origin_domain is not None and len(host_domain) > 0 and len(origin_domain) > 0:
134
- if host_domain != origin_domain:
135
- logging.warning("WARNING: request with non matching host and origin {} != {}, returning 403".format(host_domain, origin_domain))
136
- return web.Response(status=403)
137
-
138
- if request.method == "OPTIONS":
139
- response = web.Response()
140
- else:
141
- response = await handler(request)
142
-
143
- return response
144
-
145
- return origin_only_middleware
146
-
147
- class PromptServer():
148
- def __init__(self, loop):
149
- PromptServer.instance = self
150
-
151
- mimetypes.init()
152
- mimetypes.add_type('application/javascript; charset=utf-8', '.js')
153
- mimetypes.add_type('image/webp', '.webp')
154
-
155
- self.user_manager = UserManager()
156
- self.model_file_manager = ModelFileManager()
157
- self.custom_node_manager = CustomNodeManager()
158
- self.internal_routes = InternalRoutes(self)
159
- self.supports = ["custom_nodes_from_web"]
160
- self.prompt_queue = execution.PromptQueue(self)
161
- self.loop = loop
162
- self.messages = asyncio.Queue()
163
- self.client_session:Optional[aiohttp.ClientSession] = None
164
- self.number = 0
165
-
166
- middlewares = [cache_control]
167
- if args.enable_compress_response_body:
168
- middlewares.append(compress_body)
169
-
170
- if args.enable_cors_header:
171
- middlewares.append(create_cors_middleware(args.enable_cors_header))
172
- else:
173
- middlewares.append(create_origin_only_middleware())
174
-
175
- max_upload_size = round(args.max_upload_size * 1024 * 1024)
176
- self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares)
177
- self.sockets = dict()
178
- self.sockets_metadata = dict()
179
- self.web_root = (
180
- FrontendManager.init_frontend(args.front_end_version)
181
- if args.front_end_root is None
182
- else args.front_end_root
183
- )
184
- logging.info(f"[Prompt Server] web root: {self.web_root}")
185
- routes = web.RouteTableDef()
186
- self.routes = routes
187
- self.last_node_id = None
188
- self.client_id = None
189
-
190
- self.on_prompt_handlers = []
191
-
192
- @routes.get('/ws')
193
- async def websocket_handler(request):
194
- ws = web.WebSocketResponse()
195
- await ws.prepare(request)
196
- sid = request.rel_url.query.get('clientId', '')
197
- if sid:
198
- # Reusing existing session, remove old
199
- self.sockets.pop(sid, None)
200
- else:
201
- sid = uuid.uuid4().hex
202
-
203
- # Store WebSocket for backward compatibility
204
- self.sockets[sid] = ws
205
- # Store metadata separately
206
- self.sockets_metadata[sid] = {"feature_flags": {}}
207
-
208
- try:
209
- # Send initial state to the new client
210
- await self.send("status", {"status": self.get_queue_info(), "sid": sid}, sid)
211
- # On reconnect if we are the currently executing client send the current node
212
- if self.client_id == sid and self.last_node_id is not None:
213
- await self.send("executing", { "node": self.last_node_id }, sid)
214
-
215
- # Flag to track if we've received the first message
216
- first_message = True
217
-
218
- async for msg in ws:
219
- if msg.type == aiohttp.WSMsgType.ERROR:
220
- logging.warning('ws connection closed with exception %s' % ws.exception())
221
- elif msg.type == aiohttp.WSMsgType.TEXT:
222
- try:
223
- data = json.loads(msg.data)
224
- # Check if first message is feature flags
225
- if first_message and data.get("type") == "feature_flags":
226
- # Store client feature flags
227
- client_flags = data.get("data", {})
228
- self.sockets_metadata[sid]["feature_flags"] = client_flags
229
-
230
- # Send server feature flags in response
231
- await self.send(
232
- "feature_flags",
233
- feature_flags.get_server_features(),
234
- sid,
235
- )
236
-
237
- logging.info(
238
- f"Feature flags negotiated for client {sid}: {client_flags}"
239
- )
240
- first_message = False
241
- except json.JSONDecodeError:
242
- logging.warning(
243
- f"Invalid JSON received from client {sid}: {msg.data}"
244
- )
245
- except Exception as e:
246
- logging.error(f"Error processing WebSocket message: {e}")
247
- finally:
248
- self.sockets.pop(sid, None)
249
- self.sockets_metadata.pop(sid, None)
250
- return ws
251
-
252
- @routes.get("/")
253
- async def get_root(request):
254
- response = web.FileResponse(os.path.join(self.web_root, "index.html"))
255
- response.headers['Cache-Control'] = 'no-cache'
256
- response.headers["Pragma"] = "no-cache"
257
- response.headers["Expires"] = "0"
258
- return response
259
-
260
- @routes.get("/embeddings")
261
- def get_embeddings(request):
262
- embeddings = folder_paths.get_filename_list("embeddings")
263
- return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings)))
264
-
265
- @routes.get("/models")
266
- def list_model_types(request):
267
- model_types = list(folder_paths.folder_names_and_paths.keys())
268
-
269
- return web.json_response(model_types)
270
-
271
- @routes.get("/models/{folder}")
272
- async def get_models(request):
273
- folder = request.match_info.get("folder", None)
274
- if not folder in folder_paths.folder_names_and_paths:
275
- return web.Response(status=404)
276
- files = folder_paths.get_filename_list(folder)
277
- return web.json_response(files)
278
-
279
- @routes.get("/extensions")
280
- async def get_extensions(request):
281
- files = glob.glob(os.path.join(
282
- glob.escape(self.web_root), 'extensions/**/*.js'), recursive=True)
283
-
284
- extensions = list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files))
285
-
286
- for name, dir in nodes.EXTENSION_WEB_DIRS.items():
287
- files = glob.glob(os.path.join(glob.escape(dir), '**/*.js'), recursive=True)
288
- extensions.extend(list(map(lambda f: "/extensions/" + urllib.parse.quote(
289
- name) + "/" + os.path.relpath(f, dir).replace("\\", "/"), files)))
290
-
291
- return web.json_response(extensions)
292
-
293
- def get_dir_by_type(dir_type):
294
- if dir_type is None:
295
- dir_type = "input"
296
-
297
- if dir_type == "input":
298
- type_dir = folder_paths.get_input_directory()
299
- elif dir_type == "temp":
300
- type_dir = folder_paths.get_temp_directory()
301
- elif dir_type == "output":
302
- type_dir = folder_paths.get_output_directory()
303
-
304
- return type_dir, dir_type
305
-
306
- def compare_image_hash(filepath, image):
307
- hasher = node_helpers.hasher()
308
-
309
- # function to compare hashes of two images to see if it already exists, fix to #3465
310
- if os.path.exists(filepath):
311
- a = hasher()
312
- b = hasher()
313
- with open(filepath, "rb") as f:
314
- a.update(f.read())
315
- b.update(image.file.read())
316
- image.file.seek(0)
317
- return a.hexdigest() == b.hexdigest()
318
- return False
319
-
320
- def image_upload(post, image_save_function=None):
321
- image = post.get("image")
322
- overwrite = post.get("overwrite")
323
- image_is_duplicate = False
324
-
325
- image_upload_type = post.get("type")
326
- upload_dir, image_upload_type = get_dir_by_type(image_upload_type)
327
-
328
- if image and image.file:
329
- filename = image.filename
330
- if not filename:
331
- return web.Response(status=400)
332
-
333
- subfolder = post.get("subfolder", "")
334
- full_output_folder = os.path.join(upload_dir, os.path.normpath(subfolder))
335
- filepath = os.path.abspath(os.path.join(full_output_folder, filename))
336
-
337
- if os.path.commonpath((upload_dir, filepath)) != upload_dir:
338
- return web.Response(status=400)
339
-
340
- if not os.path.exists(full_output_folder):
341
- os.makedirs(full_output_folder)
342
-
343
- split = os.path.splitext(filename)
344
-
345
- if overwrite is not None and (overwrite == "true" or overwrite == "1"):
346
- pass
347
- else:
348
- i = 1
349
- while os.path.exists(filepath):
350
- if compare_image_hash(filepath, image): #compare hash to prevent saving of duplicates with same name, fix for #3465
351
- image_is_duplicate = True
352
- break
353
- filename = f"{split[0]} ({i}){split[1]}"
354
- filepath = os.path.join(full_output_folder, filename)
355
- i += 1
356
-
357
- if not image_is_duplicate:
358
- if image_save_function is not None:
359
- image_save_function(image, post, filepath)
360
- else:
361
- with open(filepath, "wb") as f:
362
- f.write(image.file.read())
363
-
364
- return web.json_response({"name" : filename, "subfolder": subfolder, "type": image_upload_type})
365
- else:
366
- return web.Response(status=400)
367
-
368
- @routes.post("/upload/image")
369
- async def upload_image(request):
370
- post = await request.post()
371
- return image_upload(post)
372
-
373
-
374
- @routes.post("/upload/mask")
375
- async def upload_mask(request):
376
- post = await request.post()
377
-
378
- def image_save_function(image, post, filepath):
379
- original_ref = json.loads(post.get("original_ref"))
380
- filename, output_dir = folder_paths.annotated_filepath(original_ref['filename'])
381
-
382
- if not filename:
383
- return web.Response(status=400)
384
-
385
- # validation for security: prevent accessing arbitrary path
386
- if filename[0] == '/' or '..' in filename:
387
- return web.Response(status=400)
388
-
389
- if output_dir is None:
390
- type = original_ref.get("type", "output")
391
- output_dir = folder_paths.get_directory_by_type(type)
392
-
393
- if output_dir is None:
394
- return web.Response(status=400)
395
-
396
- if original_ref.get("subfolder", "") != "":
397
- full_output_dir = os.path.join(output_dir, original_ref["subfolder"])
398
- if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
399
- return web.Response(status=403)
400
- output_dir = full_output_dir
401
-
402
- file = os.path.join(output_dir, filename)
403
-
404
- if os.path.isfile(file):
405
- with Image.open(file) as original_pil:
406
- metadata = PngInfo()
407
- if hasattr(original_pil,'text'):
408
- for key in original_pil.text:
409
- metadata.add_text(key, original_pil.text[key])
410
- original_pil = original_pil.convert('RGBA')
411
- mask_pil = Image.open(image.file).convert('RGBA')
412
-
413
- # alpha copy
414
- new_alpha = mask_pil.getchannel('A')
415
- original_pil.putalpha(new_alpha)
416
- original_pil.save(filepath, compress_level=4, pnginfo=metadata)
417
-
418
- return image_upload(post, image_save_function)
419
-
420
- @routes.get("/view")
421
- async def view_image(request):
422
- if "filename" in request.rel_url.query:
423
- filename = request.rel_url.query["filename"]
424
- filename, output_dir = folder_paths.annotated_filepath(filename)
425
-
426
- if not filename:
427
- return web.Response(status=400)
428
-
429
- # validation for security: prevent accessing arbitrary path
430
- if filename[0] == '/' or '..' in filename:
431
- return web.Response(status=400)
432
-
433
- if output_dir is None:
434
- type = request.rel_url.query.get("type", "output")
435
- output_dir = folder_paths.get_directory_by_type(type)
436
-
437
- if output_dir is None:
438
- return web.Response(status=400)
439
-
440
- if "subfolder" in request.rel_url.query:
441
- full_output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"])
442
- if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
443
- return web.Response(status=403)
444
- output_dir = full_output_dir
445
-
446
- filename = os.path.basename(filename)
447
- file = os.path.join(output_dir, filename)
448
-
449
- if os.path.isfile(file):
450
- if 'preview' in request.rel_url.query:
451
- with Image.open(file) as img:
452
- preview_info = request.rel_url.query['preview'].split(';')
453
- image_format = preview_info[0]
454
- if image_format not in ['webp', 'jpeg'] or 'a' in request.rel_url.query.get('channel', ''):
455
- image_format = 'webp'
456
-
457
- quality = 90
458
- if preview_info[-1].isdigit():
459
- quality = int(preview_info[-1])
460
-
461
- buffer = BytesIO()
462
- if image_format in ['jpeg'] or request.rel_url.query.get('channel', '') == 'rgb':
463
- img = img.convert("RGB")
464
- img.save(buffer, format=image_format, quality=quality)
465
- buffer.seek(0)
466
-
467
- return web.Response(body=buffer.read(), content_type=f'image/{image_format}',
468
- headers={"Content-Disposition": f"filename=\"{filename}\""})
469
-
470
- if 'channel' not in request.rel_url.query:
471
- channel = 'rgba'
472
- else:
473
- channel = request.rel_url.query["channel"]
474
-
475
- if channel == 'rgb':
476
- with Image.open(file) as img:
477
- if img.mode == "RGBA":
478
- r, g, b, a = img.split()
479
- new_img = Image.merge('RGB', (r, g, b))
480
- else:
481
- new_img = img.convert("RGB")
482
-
483
- buffer = BytesIO()
484
- new_img.save(buffer, format='PNG')
485
- buffer.seek(0)
486
-
487
- return web.Response(body=buffer.read(), content_type='image/png',
488
- headers={"Content-Disposition": f"filename=\"{filename}\""})
489
-
490
- elif channel == 'a':
491
- with Image.open(file) as img:
492
- if img.mode == "RGBA":
493
- _, _, _, a = img.split()
494
- else:
495
- a = Image.new('L', img.size, 255)
496
-
497
- # alpha img
498
- alpha_img = Image.new('RGBA', img.size)
499
- alpha_img.putalpha(a)
500
- alpha_buffer = BytesIO()
501
- alpha_img.save(alpha_buffer, format='PNG')
502
- alpha_buffer.seek(0)
503
-
504
- return web.Response(body=alpha_buffer.read(), content_type='image/png',
505
- headers={"Content-Disposition": f"filename=\"{filename}\""})
506
- else:
507
- # Get content type from mimetype, defaulting to 'application/octet-stream'
508
- content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
509
-
510
- # For security, force certain mimetypes to download instead of display
511
- if content_type in {'text/html', 'text/html-sandboxed', 'application/xhtml+xml', 'text/javascript', 'text/css'}:
512
- content_type = 'application/octet-stream' # Forces download
513
-
514
- return web.FileResponse(
515
- file,
516
- headers={
517
- "Content-Disposition": f"filename=\"{filename}\"",
518
- "Content-Type": content_type
519
- }
520
- )
521
-
522
- return web.Response(status=404)
523
-
524
- @routes.get("/view_metadata/{folder_name}")
525
- async def view_metadata(request):
526
- folder_name = request.match_info.get("folder_name", None)
527
- if folder_name is None:
528
- return web.Response(status=404)
529
- if not "filename" in request.rel_url.query:
530
- return web.Response(status=404)
531
-
532
- filename = request.rel_url.query["filename"]
533
- if not filename.endswith(".safetensors"):
534
- return web.Response(status=404)
535
-
536
- safetensors_path = folder_paths.get_full_path(folder_name, filename)
537
- if safetensors_path is None:
538
- return web.Response(status=404)
539
- out = comfy.utils.safetensors_header(safetensors_path, max_size=1024*1024)
540
- if out is None:
541
- return web.Response(status=404)
542
- dt = json.loads(out)
543
- if not "__metadata__" in dt:
544
- return web.Response(status=404)
545
- return web.json_response(dt["__metadata__"])
546
-
547
- @routes.get("/system_stats")
548
- async def system_stats(request):
549
- device = comfy.model_management.get_torch_device()
550
- device_name = comfy.model_management.get_torch_device_name(device)
551
- cpu_device = comfy.model_management.torch.device("cpu")
552
- ram_total = comfy.model_management.get_total_memory(cpu_device)
553
- ram_free = comfy.model_management.get_free_memory(cpu_device)
554
- vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
555
- vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
556
-
557
- system_stats = {
558
- "system": {
559
- "os": os.name,
560
- "ram_total": ram_total,
561
- "ram_free": ram_free,
562
- "comfyui_version": __version__,
563
- "python_version": sys.version,
564
- "pytorch_version": comfy.model_management.torch_version,
565
- "embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded",
566
- "argv": sys.argv
567
- },
568
- "devices": [
569
- {
570
- "name": device_name,
571
- "type": device.type,
572
- "index": device.index,
573
- "vram_total": vram_total,
574
- "vram_free": vram_free,
575
- "torch_vram_total": torch_vram_total,
576
- "torch_vram_free": torch_vram_free,
577
- }
578
- ]
579
- }
580
- return web.json_response(system_stats)
581
-
582
- @routes.get("/features")
583
- async def get_features(request):
584
- return web.json_response(feature_flags.get_server_features())
585
-
586
- @routes.get("/prompt")
587
- async def get_prompt(request):
588
- return web.json_response(self.get_queue_info())
589
-
590
- def node_info(node_class):
591
- obj_class = nodes.NODE_CLASS_MAPPINGS[node_class]
592
- info = {}
593
- info['input'] = obj_class.INPUT_TYPES()
594
- info['input_order'] = {key: list(value.keys()) for (key, value) in obj_class.INPUT_TYPES().items()}
595
- info['output'] = obj_class.RETURN_TYPES
596
- info['output_is_list'] = obj_class.OUTPUT_IS_LIST if hasattr(obj_class, 'OUTPUT_IS_LIST') else [False] * len(obj_class.RETURN_TYPES)
597
- info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output']
598
- info['name'] = node_class
599
- info['display_name'] = nodes.NODE_DISPLAY_NAME_MAPPINGS[node_class] if node_class in nodes.NODE_DISPLAY_NAME_MAPPINGS.keys() else node_class
600
- info['description'] = obj_class.DESCRIPTION if hasattr(obj_class,'DESCRIPTION') else ''
601
- info['python_module'] = getattr(obj_class, "RELATIVE_PYTHON_MODULE", "nodes")
602
- info['category'] = 'sd'
603
- if hasattr(obj_class, 'OUTPUT_NODE') and obj_class.OUTPUT_NODE == True:
604
- info['output_node'] = True
605
- else:
606
- info['output_node'] = False
607
-
608
- if hasattr(obj_class, 'CATEGORY'):
609
- info['category'] = obj_class.CATEGORY
610
-
611
- if hasattr(obj_class, 'OUTPUT_TOOLTIPS'):
612
- info['output_tooltips'] = obj_class.OUTPUT_TOOLTIPS
613
-
614
- if getattr(obj_class, "DEPRECATED", False):
615
- info['deprecated'] = True
616
- if getattr(obj_class, "EXPERIMENTAL", False):
617
- info['experimental'] = True
618
-
619
- if hasattr(obj_class, 'API_NODE'):
620
- info['api_node'] = obj_class.API_NODE
621
- return info
622
-
623
- @routes.get("/object_info")
624
- async def get_object_info(request):
625
- with folder_paths.cache_helper:
626
- out = {}
627
- for x in nodes.NODE_CLASS_MAPPINGS:
628
- try:
629
- out[x] = node_info(x)
630
- except Exception:
631
- logging.error(f"[ERROR] An error occurred while retrieving information for the '{x}' node.")
632
- logging.error(traceback.format_exc())
633
- return web.json_response(out)
634
-
635
- @routes.get("/object_info/{node_class}")
636
- async def get_object_info_node(request):
637
- node_class = request.match_info.get("node_class", None)
638
- out = {}
639
- if (node_class is not None) and (node_class in nodes.NODE_CLASS_MAPPINGS):
640
- out[node_class] = node_info(node_class)
641
- return web.json_response(out)
642
-
643
- @routes.get("/history")
644
- async def get_history(request):
645
- max_items = request.rel_url.query.get("max_items", None)
646
- if max_items is not None:
647
- max_items = int(max_items)
648
- return web.json_response(self.prompt_queue.get_history(max_items=max_items))
649
-
650
- @routes.get("/history/{prompt_id}")
651
- async def get_history_prompt_id(request):
652
- prompt_id = request.match_info.get("prompt_id", None)
653
- return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id))
654
-
655
- @routes.get("/queue")
656
- async def get_queue(request):
657
- queue_info = {}
658
- current_queue = self.prompt_queue.get_current_queue_volatile()
659
- queue_info['queue_running'] = current_queue[0]
660
- queue_info['queue_pending'] = current_queue[1]
661
- return web.json_response(queue_info)
662
-
663
- @routes.post("/prompt")
664
- async def post_prompt(request):
665
- logging.info("got prompt")
666
- json_data = await request.json()
667
- json_data = self.trigger_on_prompt(json_data)
668
-
669
- if "number" in json_data:
670
- number = float(json_data['number'])
671
- else:
672
- number = self.number
673
- if "front" in json_data:
674
- if json_data['front']:
675
- number = -number
676
-
677
- self.number += 1
678
-
679
- if "prompt" in json_data:
680
- prompt = json_data["prompt"]
681
- prompt_id = str(json_data.get("prompt_id", uuid.uuid4()))
682
- valid = await execution.validate_prompt(prompt_id, prompt)
683
- extra_data = {}
684
- if "extra_data" in json_data:
685
- extra_data = json_data["extra_data"]
686
-
687
- if "client_id" in json_data:
688
- extra_data["client_id"] = json_data["client_id"]
689
- if valid[0]:
690
- outputs_to_execute = valid[2]
691
- self.prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute))
692
- response = {"prompt_id": prompt_id, "number": number, "node_errors": valid[3]}
693
- return web.json_response(response)
694
- else:
695
- logging.warning("invalid prompt: {}".format(valid[1]))
696
- return web.json_response({"error": valid[1], "node_errors": valid[3]}, status=400)
697
- else:
698
- error = {
699
- "type": "no_prompt",
700
- "message": "No prompt provided",
701
- "details": "No prompt provided",
702
- "extra_info": {}
703
- }
704
- return web.json_response({"error": error, "node_errors": {}}, status=400)
705
-
706
- @routes.post("/queue")
707
- async def post_queue(request):
708
- json_data = await request.json()
709
- if "clear" in json_data:
710
- if json_data["clear"]:
711
- self.prompt_queue.wipe_queue()
712
- if "delete" in json_data:
713
- to_delete = json_data['delete']
714
- for id_to_delete in to_delete:
715
- delete_func = lambda a: a[1] == id_to_delete
716
- self.prompt_queue.delete_queue_item(delete_func)
717
-
718
- return web.Response(status=200)
719
-
720
- @routes.post("/interrupt")
721
- async def post_interrupt(request):
722
- nodes.interrupt_processing()
723
- return web.Response(status=200)
724
-
725
- @routes.post("/free")
726
- async def post_free(request):
727
- json_data = await request.json()
728
- unload_models = json_data.get("unload_models", False)
729
- free_memory = json_data.get("free_memory", False)
730
- if unload_models:
731
- self.prompt_queue.set_flag("unload_models", unload_models)
732
- if free_memory:
733
- self.prompt_queue.set_flag("free_memory", free_memory)
734
- return web.Response(status=200)
735
-
736
- @routes.post("/history")
737
- async def post_history(request):
738
- json_data = await request.json()
739
- if "clear" in json_data:
740
- if json_data["clear"]:
741
- self.prompt_queue.wipe_history()
742
- if "delete" in json_data:
743
- to_delete = json_data['delete']
744
- for id_to_delete in to_delete:
745
- self.prompt_queue.delete_history_item(id_to_delete)
746
-
747
- return web.Response(status=200)
748
-
749
- async def setup(self):
750
- timeout = aiohttp.ClientTimeout(total=None) # no timeout
751
- self.client_session = aiohttp.ClientSession(timeout=timeout)
752
-
753
- def add_routes(self):
754
- self.user_manager.add_routes(self.routes)
755
- self.model_file_manager.add_routes(self.routes)
756
- self.custom_node_manager.add_routes(self.routes, self.app, nodes.LOADED_MODULE_DIRS.items())
757
- self.app.add_subapp('/internal', self.internal_routes.get_app())
758
-
759
- # Prefix every route with /api for easier matching for delegation.
760
- # This is very useful for frontend dev server, which need to forward
761
- # everything except serving of static files.
762
- # Currently both the old endpoints without prefix and new endpoints with
763
- # prefix are supported.
764
- api_routes = web.RouteTableDef()
765
- for route in self.routes:
766
- # Custom nodes might add extra static routes. Only process non-static
767
- # routes to add /api prefix.
768
- if isinstance(route, web.RouteDef):
769
- api_routes.route(route.method, "/api" + route.path)(route.handler, **route.kwargs)
770
- self.app.add_routes(api_routes)
771
- self.app.add_routes(self.routes)
772
-
773
- # Add routes from web extensions.
774
- for name, dir in nodes.EXTENSION_WEB_DIRS.items():
775
- self.app.add_routes([web.static('/extensions/' + name, dir)])
776
-
777
- workflow_templates_path = FrontendManager.templates_path()
778
- if workflow_templates_path:
779
- self.app.add_routes([
780
- web.static('/templates', workflow_templates_path)
781
- ])
782
-
783
- # Serve embedded documentation from the package
784
- embedded_docs_path = FrontendManager.embedded_docs_path()
785
- if embedded_docs_path:
786
- self.app.add_routes([
787
- web.static('/docs', embedded_docs_path)
788
- ])
789
-
790
- self.app.add_routes([
791
- web.static('/', self.web_root),
792
- ])
793
-
794
- def get_queue_info(self):
795
- prompt_info = {}
796
- exec_info = {}
797
- exec_info['queue_remaining'] = self.prompt_queue.get_tasks_remaining()
798
- prompt_info['exec_info'] = exec_info
799
- return prompt_info
800
-
801
- async def send(self, event, data, sid=None):
802
- if event == BinaryEventTypes.UNENCODED_PREVIEW_IMAGE:
803
- await self.send_image(data, sid=sid)
804
- elif event == BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA:
805
- # data is (preview_image, metadata)
806
- preview_image, metadata = data
807
- await self.send_image_with_metadata(preview_image, metadata, sid=sid)
808
- elif isinstance(data, (bytes, bytearray)):
809
- await self.send_bytes(event, data, sid)
810
- else:
811
- await self.send_json(event, data, sid)
812
-
813
- def encode_bytes(self, event, data):
814
- if not isinstance(event, int):
815
- raise RuntimeError(f"Binary event types must be integers, got {event}")
816
-
817
- packed = struct.pack(">I", event)
818
- message = bytearray(packed)
819
- message.extend(data)
820
- return message
821
-
822
- async def send_image(self, image_data, sid=None):
823
- image_type = image_data[0]
824
- image = image_data[1]
825
- max_size = image_data[2]
826
- if max_size is not None:
827
- if hasattr(Image, 'Resampling'):
828
- resampling = Image.Resampling.BILINEAR
829
- else:
830
- resampling = Image.Resampling.LANCZOS
831
-
832
- image = ImageOps.contain(image, (max_size, max_size), resampling)
833
- type_num = 1
834
- if image_type == "JPEG":
835
- type_num = 1
836
- elif image_type == "PNG":
837
- type_num = 2
838
-
839
- bytesIO = BytesIO()
840
- header = struct.pack(">I", type_num)
841
- bytesIO.write(header)
842
- image.save(bytesIO, format=image_type, quality=95, compress_level=1)
843
- preview_bytes = bytesIO.getvalue()
844
- await self.send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid)
845
-
846
- async def send_image_with_metadata(self, image_data, metadata=None, sid=None):
847
- image_type = image_data[0]
848
- image = image_data[1]
849
- max_size = image_data[2]
850
- if max_size is not None:
851
- if hasattr(Image, 'Resampling'):
852
- resampling = Image.Resampling.BILINEAR
853
- else:
854
- resampling = Image.Resampling.LANCZOS
855
-
856
- image = ImageOps.contain(image, (max_size, max_size), resampling)
857
-
858
- mimetype = "image/png" if image_type == "PNG" else "image/jpeg"
859
-
860
- # Prepare metadata
861
- if metadata is None:
862
- metadata = {}
863
- metadata["image_type"] = mimetype
864
-
865
- # Serialize metadata as JSON
866
- import json
867
- metadata_json = json.dumps(metadata).encode('utf-8')
868
- metadata_length = len(metadata_json)
869
-
870
- # Prepare image data
871
- bytesIO = BytesIO()
872
- image.save(bytesIO, format=image_type, quality=95, compress_level=1)
873
- image_bytes = bytesIO.getvalue()
874
-
875
- # Combine metadata and image
876
- combined_data = bytearray()
877
- combined_data.extend(struct.pack(">I", metadata_length))
878
- combined_data.extend(metadata_json)
879
- combined_data.extend(image_bytes)
880
-
881
- await self.send_bytes(BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA, combined_data, sid=sid)
882
-
883
- async def send_bytes(self, event, data, sid=None):
884
- message = self.encode_bytes(event, data)
885
-
886
- if sid is None:
887
- sockets = list(self.sockets.values())
888
- for ws in sockets:
889
- await send_socket_catch_exception(ws.send_bytes, message)
890
- elif sid in self.sockets:
891
- await send_socket_catch_exception(self.sockets[sid].send_bytes, message)
892
-
893
- async def send_json(self, event, data, sid=None):
894
- message = {"type": event, "data": data}
895
-
896
- if sid is None:
897
- sockets = list(self.sockets.values())
898
- for ws in sockets:
899
- await send_socket_catch_exception(ws.send_json, message)
900
- elif sid in self.sockets:
901
- await send_socket_catch_exception(self.sockets[sid].send_json, message)
902
-
903
- def send_sync(self, event, data, sid=None):
904
- self.loop.call_soon_threadsafe(
905
- self.messages.put_nowait, (event, data, sid))
906
-
907
- def queue_updated(self):
908
- self.send_sync("status", { "status": self.get_queue_info() })
909
-
910
- async def publish_loop(self):
911
- while True:
912
- msg = await self.messages.get()
913
- await self.send(*msg)
914
-
915
- async def start(self, address, port, verbose=True, call_on_start=None):
916
- await self.start_multi_address([(address, port)], call_on_start=call_on_start)
917
-
918
- async def start_multi_address(self, addresses, call_on_start=None, verbose=True):
919
- runner = web.AppRunner(self.app, access_log=None)
920
- await runner.setup()
921
- ssl_ctx = None
922
- scheme = "http"
923
- if args.tls_keyfile and args.tls_certfile:
924
- ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER, verify_mode=ssl.CERT_NONE)
925
- ssl_ctx.load_cert_chain(certfile=args.tls_certfile,
926
- keyfile=args.tls_keyfile)
927
- scheme = "https"
928
-
929
- if verbose:
930
- logging.info("Starting server\n")
931
- for addr in addresses:
932
- address = addr[0]
933
- port = addr[1]
934
- site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
935
- await site.start()
936
-
937
- if not hasattr(self, 'address'):
938
- self.address = address #TODO: remove this
939
- self.port = port
940
-
941
- if ':' in address:
942
- address_print = "[{}]".format(address)
943
- else:
944
- address_print = address
945
-
946
- if verbose:
947
- logging.info("To see the GUI go to: {}://{}:{}".format(scheme, address_print, port))
948
-
949
- if call_on_start is not None:
950
- call_on_start(scheme, self.address, self.port)
951
-
952
- def add_on_prompt_handler(self, handler):
953
- self.on_prompt_handlers.append(handler)
954
-
955
- def trigger_on_prompt(self, json_data):
956
- for handler in self.on_prompt_handlers:
957
- try:
958
- json_data = handler(json_data)
959
- except Exception:
960
- logging.warning("[ERROR] An error occurred during the on_prompt_handler processing")
961
- logging.warning(traceback.format_exc())
962
-
963
- return json_data
964
-
965
- def send_progress_text(
966
- self, text: Union[bytes, bytearray, str], node_id: str, sid=None
967
- ):
968
- if isinstance(text, str):
969
- text = text.encode("utf-8")
970
- node_id_bytes = str(node_id).encode("utf-8")
971
-
972
- # Pack the node_id length as a 4-byte unsigned integer, followed by the node_id bytes
973
- message = struct.pack(">I", len(node_id_bytes)) + node_id_bytes + text
974
-
975
- self.send_sync(BinaryEventTypes.TEXT, message, sid)