From fa5de3dc2228ceea4754203c5d95a46f030dc4ab Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 21 Nov 2024 11:28:19 -0500 Subject: [PATCH] api resiliency improvements Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + cli/api-control.js | 70 ++++++++++++++++++++++++++++++----------- modules/api/generate.py | 4 +-- modules/api/helpers.py | 6 +++- package.json | 4 +-- scripts/pulid_ext.py | 29 ++++++++++------- wiki | 2 +- 7 files changed, 81 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e92293d92..b58d1f450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ And quite a few more improvements and fixes since the last update - for full det - Auto pipeline switching coveres wrapper classes and nested pipelines - Full settings validation on load of `config.json` - Refactor of all params in main processing classes + - Improve API scripts usage resiliency - Fixes: - custom watermark add alphablending diff --git a/cli/api-control.js b/cli/api-control.js index d7d36f83e..fde0ae43b 100755 --- a/cli/api-control.js +++ b/cli/api-control.js @@ -3,32 +3,65 @@ // simple nodejs script to test sdnext api const fs = require('fs'); +const path = require('path'); const process = require('process'); +const argparse = require('argparse'); const sd_url = process.env.SDAPI_URL || 'http://127.0.0.1:7860'; const sd_username = process.env.SDAPI_USR; const sd_password = process.env.SDAPI_PWD; -const sd_options = { - // first pass - prompt: 'beautiful lady, in the steampunk style', - negative_prompt: 'foggy, blurry', - seed: -1, - steps: 20, - batch_size: 1, - n_iter: 1, - cfg_scale: 6, - width: 1280, - height: 800, - // api return options - save_images: false, - send_images: true, - script_name: 'pulid', -}; + +function b64(file) { + const data = fs.readFileSync(file); + const b64 = Buffer.from(data).toString('base64'); + const ext = path.extname(file).replace('.', ''); + str = `data:image/${ext};base64,${b64}`; + // console.log('b64:', ext, b64.length); + return str; +} + +function options() { + const opt = { + // first pass + prompt: args.prompt || 'beautiful lady, in the steampunk style', + negative_prompt: args.negative || 'foggy, blurry', + seed: -1, + steps: 20, + batch_size: 1, + n_iter: 1, + cfg_scale: 6, + width: args.width || 1024, + height: args.height || 1024, + // api return options + save_images: false, + send_images: true, + }; + if (args.pulid) { + const b64image = b64(args.pulid); + opt.script_name = 'pulid'; + opt.script_args = [b64image, 0.9]; + } + // console.log('options:', opt); + return opt; +} + +function init() { + const parser = new argparse.ArgumentParser({ description: 'SD.Next API' }); + parser.add_argument('--prompt', { type: 'str', help: 'prompt' }); + parser.add_argument('--negative', { type: 'str', help: 'negative' }); + parser.add_argument('--width', { type: 'int', help: 'width' }); + parser.add_argument('--height', { type: 'int', help: 'height' }); + parser.add_argument('--pulid', { type: 'str', help: 'pulid init image' }); + parser.add_argument('--output', { type: 'str', help: 'output path' }); + const args = parser.parse_args(); + return args +} async function main() { const method = 'POST'; const headers = new Headers(); - const body = JSON.stringify(sd_options); + const opt = options(); + const body = JSON.stringify(opt); headers.set('Content-Type', 'application/json'); if (sd_username && sd_password) headers.set({ Authorization: `Basic ${btoa('sd_username:sd_password')}` }); const res = await fetch(`${sd_url}/sdapi/v1/txt2img`, { method, headers, body }); @@ -39,7 +72,7 @@ async function main() { const json = await res.json(); console.log('result:', json.info); for (const i in json.images) { // eslint-disable-line guard-for-in - const file = `/tmp/test-${i}.jpg`; + const file = args.output || `/tmp/test-${i}.jpg`; const data = atob(json.images[i]) fs.writeFileSync(file, data, 'binary'); console.log('image saved:', file); @@ -47,4 +80,5 @@ async function main() { } } +const args = init(); main(); diff --git a/modules/api/generate.py b/modules/api/generate.py index deee8db3d..201aeb785 100644 --- a/modules/api/generate.py +++ b/modules/api/generate.py @@ -110,10 +110,10 @@ class APIGenerate(): setattr(p, key, value) shared.state.begin('API TXT', api=True) script_args = script.init_script_args(p, txt2imgreq, self.default_script_arg_txt2img, selectable_scripts, selectable_script_idx, script_runner) + p.script_args = tuple(script_args) # Need to pass args as tuple here if selectable_scripts is not None: processed = scripts.scripts_txt2img.run(p, *script_args) # Need to pass args as list here else: - p.script_args = tuple(script_args) # Need to pass args as tuple here processed = process_images(p) shared.state.end(api=False) if processed is None or processed.images is None or len(processed.images) == 0: @@ -160,10 +160,10 @@ class APIGenerate(): setattr(p, key, value) shared.state.begin('API-IMG', api=True) script_args = script.init_script_args(p, img2imgreq, self.default_script_arg_img2img, selectable_scripts, selectable_script_idx, script_runner) + p.script_args = tuple(script_args) # Need to pass args as tuple here if selectable_scripts is not None: processed = scripts.scripts_img2img.run(p, *script_args) # Need to pass args as list here else: - p.script_args = tuple(script_args) # Need to pass args as tuple here processed = process_images(p) shared.state.end(api=False) if processed is None or processed.images is None or len(processed.images) == 0: diff --git a/modules/api/helpers.py b/modules/api/helpers.py index d9a87537e..2d6ae8110 100644 --- a/modules/api/helpers.py +++ b/modules/api/helpers.py @@ -18,10 +18,14 @@ def decode_base64_to_image(encoding, quiet=False): if encoding.startswith("data:image/"): encoding = encoding.split(";")[1].split(",")[1] try: - image = Image.open(io.BytesIO(base64.b64decode(encoding))) + decoded = base64.b64decode(encoding) + data = io.BytesIO(decoded) + image = Image.open(data) return image except Exception as e: shared.log.warning(f'API cannot decode image: {e}') + from modules import errors + errors.display(e, 'API cannot decode image') if not quiet: raise HTTPException(status_code=500, detail="Invalid encoded image") from e return None diff --git a/package.json b/package.json index c6657a1f3..776a81883 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,12 @@ "esbuild": "^0.18.15" }, "dependencies": { + "argparse": "^2.0.1", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-css": "^0.9.2", "eslint-plugin-html": "^8.1.1", "eslint-plugin-json": "^3.1.0", - "eslint-plugin-markdown": "^4.0.1", - "inkjet": "^3.0.0" + "eslint-plugin-markdown": "^4.0.1" } } diff --git a/scripts/pulid_ext.py b/scripts/pulid_ext.py index 800a4645b..676fa79f3 100644 --- a/scripts/pulid_ext.py +++ b/scripts/pulid_ext.py @@ -6,6 +6,7 @@ import gradio as gr import numpy as np from PIL import Image from modules import shared, devices, errors, scripts, processing, processing_helpers, sd_models +from modules.api.api import decode_base64_to_image debug = os.environ.get('SD_PULID_DEBUG', None) is not None @@ -63,7 +64,6 @@ class Script(scripts.Script): for file in files or []: try: if isinstance(file, str): - from modules.api.api import decode_base64_to_image image = decode_base64_to_image(file) elif isinstance(file, Image.Image): image = file @@ -98,28 +98,33 @@ class Script(scripts.Script): with gr.Row(): gallery = gr.Gallery(show_label=False, value=[], visible=False, container=False, rows=1) files.change(fn=self.load_images, inputs=[files], outputs=[gallery]) - return [strength, zero, sampler, ortho, gallery, restore, offload, version] + return [gallery, strength, zero, sampler, ortho, restore, offload, version] def run( self, p: processing.StableDiffusionProcessing, + gallery: list = [], strength: float = 0.8, zero: int = 20, sampler: str = 'dpmpp_sde', ortho: str = 'v2', - gallery: list = [], restore: bool = False, offload: bool = True, version: str = 'v1.1' ): # pylint: disable=arguments-differ, unused-argument images = [] try: - if gallery is None or isinstance(gallery, str) or len(gallery) == 0: - from modules.api.api import decode_base64_to_image + if gallery is None or (isinstance(gallery, list) and len(gallery) == 0): images = getattr(p, 'pulid_images', uploaded_images) images = [decode_base64_to_image(image) if isinstance(image, str) else image for image in images] + elif isinstance(gallery[0], dict): + images = [Image.open(f['name']) for f in gallery] + elif isinstance(gallery, str): + images = [decode_base64_to_image(gallery)] + elif isinstance(gallery[0], str): + images = [decode_base64_to_image(f) for f in gallery] else: - images = [Image.open(f['name']) if isinstance(f, dict) else f for f in gallery] + images = gallery images = [np.array(image) for image in images] except Exception as e: shared.log.error(f'PuLID: failed to load images: {e}') @@ -127,11 +132,6 @@ class Script(scripts.Script): if len(images) == 0: shared.log.error('PuLID: no images') return None - try: - images = [self.pulid.resize(image, 1024) for image in images] - except Exception as e: - shared.log.error(f'PuLID: failed to resize images: {e}') - return None supported_model_list = ['sdxl'] if shared.sd_model_type not in supported_model_list: @@ -152,6 +152,13 @@ class Script(scripts.Script): if self.pulid is None: shared.log.error('PuLID: failed to load PuLID library') return None + + try: + images = [self.pulid.resize(image, 1024) for image in images] + except Exception as e: + shared.log.error(f'PuLID: failed to resize images: {e}') + return None + if p.batch_size > 1: shared.log.warning('PuLID: batch size not supported') p.batch_size = 1 diff --git a/wiki b/wiki index 713906e92..30f3265bb 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 713906e920e02607ea04951858aabeff7ce641f2 +Subproject commit 30f3265bb06ac738e4467f58be4df3fc4b49c08b