From c7d5096f7e02369fc44ae4b4cf28d75dac1559d8 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 1 Mar 2024 12:52:07 -0500 Subject: [PATCH] add preprocess api --- CHANGELOG.md | 2 + TODO.md | 3 +- cli/simple-preprocess.py | 71 +++++++++++++++++++++++++++++++++++ modules/api/api.py | 6 ++- modules/api/models.py | 2 +- modules/control/api.py | 44 ++++++++++++++++++++++ modules/control/processors.py | 4 +- 7 files changed, 126 insertions(+), 6 deletions(-) create mode 100755 cli/simple-preprocess.py create mode 100644 modules/control/api.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 17efbc8f3..24bb509f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ - add **ROCm** 6.0 nightly option to installer, thanks @jicka - support models with their own YAML model config files - support models with their own JSON per-component config files, for example: `playground-v2.5_vae.config` +- **API** + - add preprocessor api endpoints: `/sdapi/v1/preprocessors`, `/sdapi/v1/preprocess` - **Internal** - remove obsolete textual inversion training code - remove obsolete hypernetworks training code diff --git a/TODO.md b/TODO.md index 79ab68161..242ec0a84 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - ipadapter masking: - x-adapter: - async lowvram: -- init latents: variations, tiling, img2img +- init latents: variations, img2img - diffusers public callbacks - remove builtin: controlnet - remove builtin: image-browser @@ -19,4 +19,3 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - second pass: - control api - masking api -- preprocess api diff --git a/cli/simple-preprocess.py b/cli/simple-preprocess.py new file mode 100755 index 000000000..81bfea77d --- /dev/null +++ b/cli/simple-preprocess.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +import io +import os +import time +import base64 +import logging +import argparse +import requests +import urllib3 +from PIL import Image + + +sd_url = os.environ.get('SDAPI_URL', "http://127.0.0.1:7860") +sd_username = os.environ.get('SDAPI_USR', None) +sd_password = os.environ.get('SDAPI_PWD', None) + + +logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def auth(): + if sd_username is not None and sd_password is not None: + return requests.auth.HTTPBasicAuth(sd_username, sd_password) + return None + + +def get(endpoint: str, dct: dict = None): + req = requests.get(f'{sd_url}{endpoint}', json=dct, timeout=300, verify=False, auth=auth()) + if req.status_code != 200: + return { 'error': req.status_code, 'reason': req.reason, 'url': req.url } + else: + return req.json() + + +def post(endpoint: str, dct: dict = None): + req = requests.post(f'{sd_url}{endpoint}', json = dct, timeout=300, verify=False, auth=auth()) + if req.status_code != 200: + return { 'error': req.status_code, 'reason': req.reason, 'url': req.url } + else: + return req.json() + + +def info(args): # pylint: disable=redefined-outer-name + t0 = time.time() + with open(args.input, 'rb') as f: + content = f.read() + models = get('/sdapi/v1/preprocessors') + log.info(f'models: {models}') + req = { + 'model': 'Canny', + 'image': base64.b64encode(content).decode(), + 'config': { 'low_threshold': 50 }, + } + data = post('/sdapi/v1/preprocess', req) + t1 = time.time() + if 'image' in data: + b64 = data['image'].split(',',1)[0] + image = Image.open(io.BytesIO(base64.b64decode(b64))) + log.info(f'received image: size={image.size} time={t1-t0:.2f}') + else: + log.info(f'received: {data} time={t1-t0:.2f}') + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description = 'simple-info') + parser.add_argument('--input', required=True, help='input image') + args = parser.parse_args() + log.info(f'info: {args}') + info(args) diff --git a/modules/api/api.py b/modules/api/api.py index 4224f78d3..cf2334b66 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -6,6 +6,7 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.exceptions import HTTPException from modules import errors, shared, postprocessing from modules.api import models, endpoints, script, helpers, server, nvml, generate +from modules.control import api as control_api errors.install() @@ -43,14 +44,15 @@ class Api: self.add_api_route("/sdapi/v1/options", server.get_config, methods=["GET"], response_model=models.OptionsModel) self.add_api_route("/sdapi/v1/options", server.set_config, methods=["POST"]) self.add_api_route("/sdapi/v1/cmd-flags", server.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel) - app.add_api_route("/sdapi/v1/nvml", nvml.get_nvml, methods=["GET"], response_model=List[models.ResNVML]) - + self.add_api_route("/sdapi/v1/nvml", nvml.get_nvml, methods=["GET"], response_model=List[models.ResNVML]) # core api using locking self.add_api_route("/sdapi/v1/txt2img", self.generate.post_text2img, methods=["POST"], response_model=models.ResTxt2Img) self.add_api_route("/sdapi/v1/img2img", self.generate.post_img2img, methods=["POST"], response_model=models.ResImg2Img) self.add_api_route("/sdapi/v1/extra-single-image", self.extras_single_image_api, methods=["POST"], response_model=models.ResProcessImage) self.add_api_route("/sdapi/v1/extra-batch-images", self.extras_batch_images_api, methods=["POST"], response_model=models.ResProcessBatch) + self.add_api_route("/sdapi/v1/preprocess", control_api.post_preprocess, methods=["POST"]) + self.add_api_route("/sdapi/v1/preprocessors", control_api.get_preprocess, methods=["GET"]) # api dealing with optional scripts self.add_api_route("/sdapi/v1/scripts", script.get_scripts_list, methods=["GET"], response_model=models.ResScripts) diff --git a/modules/api/models.py b/modules/api/models.py index f234b0d7e..f8195938a 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -272,7 +272,7 @@ class ResProcessBatch(ResProcess): images: List[str] = Field(title="Images", description="The generated images in base64 format.") class ReqImageInfo(BaseModel): - image: str = Field(title="Image", description="The base64 encoded PNG image") + image: str = Field(title="Image", description="The base64 encoded image") class ResImageInfo(BaseModel): info: str = Field(title="Image info", description="A string with the parameters used to generate the image") diff --git a/modules/control/api.py b/modules/control/api.py new file mode 100644 index 000000000..ad0923564 --- /dev/null +++ b/modules/control/api.py @@ -0,0 +1,44 @@ +from typing import Optional +from pydantic import BaseModel, Field # pylint: disable=no-name-in-module +from fastapi.exceptions import HTTPException +from fastapi.responses import JSONResponse +from modules.api.helpers import decode_base64_to_image, encode_pil_to_base64 + + +processor = None # cached instance of processor + + +class ReqPreprocess(BaseModel): + image: str = Field(default=None, title="Image", description="The base64 encoded image") + model: str = Field(default=None, title="Model", description="The model to use for preprocessing") + params: Optional[dict] = Field(default={}, title="Settings", description="Preprocessor settings") + + +class ResPreprocess(BaseModel): + model: str = Field(default=None, title="Model", description="The processor model used") + image: str = Field(default=None, title="Image", description="The processed image in base64 format") + + +def get_preprocess(): + from modules.control import processors + p = {} + for k, v in processors.config.items(): + p[k] = v.get('params') + return JSONResponse(p) + + +def post_preprocess(req: ReqPreprocess): + global processor # pylint: disable=global-statement + from modules.control import processors + models = list(processors.config) + if req.model not in models: + raise HTTPException(status_code=404, detail=f"Processor model not found: id={req.model}") + image = decode_base64_to_image(req.image) + if processor is None or processor.processor_id != req.model: + processor = processors.Processor(req.model) + for k, v in req.params.items(): + if k not in processors.config[processor.processor_id]['params']: + raise HTTPException(status_code=400, detail=f"Processor invalid parameter: id={req.model} {k}={v}") + processed = processor(image, local_config=req.params) + image = encode_pil_to_base64(processed) + return ResPreprocess(model=processor.processor_id, image=image) diff --git a/modules/control/processors.py b/modules/control/processors.py index c5ff7003a..d8eb60714 100644 --- a/modules/control/processors.py +++ b/modules/control/processors.py @@ -206,7 +206,7 @@ class Processor(): display(e, 'Control Processor load') return f'Processor load filed: {processor_id}' - def __call__(self, image_input: Image, mode: str = 'RGB', resize_mode: int = 0, resize_name: str = 'None', scale_tab: int = 1, scale_by: float = 1.0): + def __call__(self, image_input: Image, mode: str = 'RGB', resize_mode: int = 0, resize_name: str = 'None', scale_tab: int = 1, scale_by: float = 1.0, local_config: dict = {}): if self.processor_id is None or self.processor_id == 'None': return image_input if self.override is not None: @@ -232,6 +232,8 @@ class Processor(): try: t0 = time.time() kwargs = config.get(self.processor_id, {}).get('params', None) + if kwargs: + kwargs.update(local_config) if self.resize: image_resized = image_input.resize((512, 512), Image.Resampling.LANCZOS) else: