diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2ab9896..4b97e8504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ -\# Change Log for SD.Next +# Change Log for SD.Next ## Update for 2026-08-05 +### Highlights for 2026-08-05 + +This release brings **Sefi-Image** and **Mage-Flow** models, plus a new **Nunchaku-Lite** inference engine +On the server side, there are quite a few *under-the-hood* improvements, including optimized startup, optimized webserver, end-to-end profiling, storage analyzer, etc. +There are also several new auxiliary models, such as **Lucida** for background removal +And video processing now supports scripts such as prompt enhance, nudenet, etc. +Plus several quality-of-life improvements and bug-fixes across the board + +### Details for 2026-08-05 + - **Models** - [SeFi-Image](https://huggingface.co/SeFi-Image/SeFi-Image-5B-RL) in *Base*, *Turbo* (distilled) and *RL* (finetuned) variants SeFi is an interesting model that separates generation into semantic and texture latent streams @@ -31,6 +41,7 @@ - **API** - add `/sdapi/v1/storage` endpoint to return storage usage info - **Internal** + - switch internal server to explicit `uvicorn` - update core requirements - **Fixes** - seedvr quality diff --git a/modules/server.py b/modules/server.py index ae17d541c..e00f40e56 100644 --- a/modules/server.py +++ b/modules/server.py @@ -1,36 +1,59 @@ import threading import logging +import time +import asyncio import uvicorn import fastapi +from modules.logger import log class UvicornServer(uvicorn.Server): - def __init__(self, app: fastapi.FastAPI, listen = None, port = None, keyfile = None, certfile = None, loop = "auto", http = "auto"): + def __init__(self, app: fastapi.FastAPI, host = None, listen = None, port = None, keyfile = None, certfile = None, loop = "auto", http = "auto"): self.app: fastapi.FastAPI = app self.thread: threading.Thread = None + self.loop = None self.wants_restart = False self.should_exit = False + kwargs = { + 'loop': loop, # auto, asyncio, uvloop + 'http': http, # auto, h11, httptools + 'interface': "auto", # auto, asgi3, asgi2, wsgi + 'ws': "auto", # auto, websockets, wsproto, websockets-sansio + 'timeout_keep_alive': 60, # default=5 + 'ws_max_size': 1024 * 1024 * 1024, # default 16MB + 'ws_max_queue': 64, # default=32 + 'ws_ping_interval': 30, # default=20 + 'ws_ping_timeout': 60, # default=20 + 'timeout_graceful_shutdown': 5, # default=None + 'access_log': False, # default=True + 'server_header': False, # default=True + 'date_header': False, # default=True + 'backlog': 4096, # default=2048 + 'reload': False, # default=False + } self.config = uvicorn.Config( app=self.app, - host = "0.0.0.0" if listen else "127.0.0.1", - port = port or 7861, - loop = loop, # auto, asyncio, uvloop - http = http, # auto, h11, httptools - interface = "auto", # auto, asgi3, asgi2, wsgi - ws = "auto", # auto, websockets, wsproto + host = host or ("0.0.0.0" if listen else "127.0.0.1"), + port = port or 7860, log_level = logging.WARNING, - backlog = 4096, # default=2048 - timeout_keep_alive = 60, # default=5 ssl_keyfile = keyfile, ssl_certfile = certfile, - ws_max_size = 1024 * 1024 * 1024, # default 16MB + **kwargs ) super().__init__(config=self.config) + log.info(f'Server: uvicorn={kwargs}') def start(self): self.thread = threading.Thread(target=self.run, daemon=True) self.wants_restart = False self.thread.start() + start = time.time() + while not self.started: + time.sleep(1e-3) + if time.time() - start > 5: + raise RuntimeError("Server failed to start. Please check that the port is available.") + policy = asyncio.get_event_loop_policy() + self.loop = f"{type(policy).__module__}.{type(policy).__name__}" def stop(self): self.should_exit = True @@ -71,7 +94,6 @@ class HypercornServer: self.thread = threading.Thread(target=self.run, daemon=True) self.thread.start() elif self.loop == 'asyncio': # does not run in thread - import asyncio from hypercorn.asyncio import serve self.server = serve(self.app, self.config) asyncio.run(self.server) diff --git a/requirements.txt b/requirements.txt index 9e72d22cd..2e9189da3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,11 +17,13 @@ voluptuous fasteners limits orjson -websockets ftfy +websockets +httptools # versioned fastapi==0.124.4 +uvicorn==0.52.1 rich==15.0.0 safetensors==0.8.0 peft==0.20.0 diff --git a/webui.py b/webui.py index d8d3cd606..2b39c7720 100644 --- a/webui.py +++ b/webui.py @@ -317,6 +317,41 @@ def mount_subpath(app): log.info(f'Mounted: subpath="{shared.opts.subpath}"') +def start_server( + blocks, + server_name: str | None = None, # pylint: disable=redefined-outer-name + server_port: int | None = None, + ssl_keyfile: str | None = None, + ssl_certfile: str | None = None, + ssl_keyfile_password: str | None = None, # pylint: disable=unused-argument + app_kwargs: dict | None = None, +): + from gradio.routes import App + from modules.server import UvicornServer + if ssl_keyfile is not None and ssl_certfile is None: + raise ValueError("ssl_certfile must be provided if ssl_keyfile is provided.") + server_name = server_name or "127.0.0.1" + server_port = server_port or 7860 + url_host_name = "localhost" if server_name == "0.0.0.0" else server_name + if server_name.startswith("[") and server_name.endswith("]"): + server_name = server_name[1:-1] + app = App.create_app(blocks, app_kwargs=app_kwargs) + server = UvicornServer( + app=app, + listen=server_name == '0.0.0.0', + host=server_name, + port=server_port, + keyfile=ssl_keyfile, + certfile=ssl_certfile, + ) + server.start() + if ssl_keyfile is not None: + path_to_local_server = f"https://{url_host_name}:{server_port}/" + else: + path_to_local_server = f"http://{url_host_name}:{server_port}/" + return server_name, server_port, path_to_local_server, app, server + + def start_ui(): log.debug('UI start sequence') log.debug(f'UI image support: kanvas={version.get("kanvas", "unknown")}') @@ -362,8 +397,10 @@ def start_ui(): allowed_paths.append(shared.cmd_opts.models_dir) if shared.cmd_opts.allowed_paths is not None: allowed_paths += [p for p in shared.cmd_opts.allowed_paths if os.path.isdir(p)] - log.debug(f'Root paths: {allowed_paths}') + log.info(f'Server: name={server_name} port={shared.cmd_opts.port} paths={allowed_paths} ssl={shared.cmd_opts.tls_keyfile}:{shared.cmd_opts.tls_certfile} auth={len(auth_pairs)}') with contextlib.redirect_stdout(stdout): + import gradio.networking + gradio.networking.start_server = start_server app, local_url, share_url = shared.demo.launch( # app is FastAPI(Starlette) instance share=shared.cmd_opts.share, server_name=server_name, @@ -382,6 +419,11 @@ def start_ui(): app_kwargs=fastapi_args, _frontend=True and shared.cmd_opts.share, ) + + uc = shared.demo.server.config + get_name = lambda c: getattr(c, '__name__', c) # pylint: disable=unnecessary-lambda-assignment + log.debug(f'Server config: loop={shared.demo.server.loop} http={get_name(uc.http_protocol_class)} ws={get_name(uc.ws_protocol_class)} interface={uc.interface} workers={uc.workers} backlog={uc.backlog} timeout_keep_alive={uc.timeout_keep_alive} timeout_notify={uc.timeout_notify} ws_max_size={uc.ws_max_size} ws_max_queue={uc.ws_max_queue} ws_ping_interval={uc.ws_ping_interval} ws_ping_timeout={uc.ws_ping_timeout}') + if shared.cmd_opts.data_dir is not None: modules.gr_tempdir.register_tmp_file(shared.demo, os.path.join(shared.cmd_opts.data_dir, 'x')) log.info(f'Local URL: {local_url}')