From bdd885eab4b6391d6a981c6704613ba7736963e4 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Nov 2024 12:37:38 -0500 Subject: [PATCH] multiple param validation fixes Signed-off-by: Vladimir Mandic --- cli/{api-faces.py => api-detect.py} | 11 +++++------ cli/api-faceid.py | 21 ++------------------- cli/api-json.py | 2 +- cli/api-progress.py | 6 +++++- cli/api-txt2img.py | 2 +- cli/api-upscale.py | 5 +++-- modules/api/api.py | 2 +- modules/api/generate.py | 12 +++++++++--- modules/api/process.py | 24 +++++++++++++++++------- modules/api/script.py | 20 ++++++++++++++------ modules/postprocess/yolo.py | 14 +++++++++++--- modules/processing_args.py | 5 +++-- modules/vqa.py | 4 ++-- 13 files changed, 74 insertions(+), 54 deletions(-) rename cli/{api-faces.py => api-detect.py} (80%) diff --git a/cli/api-faces.py b/cli/api-detect.py similarity index 80% rename from cli/api-faces.py rename to cli/api-detect.py index 0a98843c0..ca121b220 100755 --- a/cli/api-faces.py +++ b/cli/api-detect.py @@ -44,16 +44,15 @@ def encode(f): def detect(args): # pylint: disable=redefined-outer-name - data = post('/sdapi/v1/faces', { 'image': encode(args.image) }) - for face in zip(data['images'], data['scores']): - log.info(f'Face: score={face[1]}') - image = Image.open(io.BytesIO(base64.b64decode(face[0]))) - image.save(f'/tmp/face_{face[1]}.jpg') + data = post('/sdapi/v1/detect', { 'image': encode(args.image), 'model': args.model }) + for i in range(len(data['images'])): + log.info(f"Item {i}: score={data['scores'][i]} cls={data['classes'][i]} box={data['boxes'][i]} label={data['labels'][i]}") if __name__ == "__main__": parser = argparse.ArgumentParser(description = 'api-faces') parser.add_argument('--image', required=True, help='input image') + parser.add_argument('--model', required=False, default='', help='model') args = parser.parse_args() - log.info(f'api-faces: {args}') + log.info(f'api-detect: {args}') detect(args) diff --git a/cli/api-faceid.py b/cli/api-faceid.py index e656a4a47..18f1d3503 100755 --- a/cli/api-faceid.py +++ b/cli/api-faceid.py @@ -63,7 +63,7 @@ def generate(args): # pylint: disable=redefined-outer-name options['height'] = args.height options['face'] = { 'mode': 'FaceID', - 'ip_model': 'FaceID Base', + 'ip_model': 'FaceID XL', 'source_images': [encode(args.face)], } data = post('/sdapi/v1/txt2img', options) @@ -86,7 +86,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description = 'api-faceid') parser.add_argument('--width', required=False, default=512, help='image width') parser.add_argument('--height', required=False, default=512, help='image height') - parser.add_argument('--face', required=False, help='face image') + parser.add_argument('--face', required=True, help='face image') parser.add_argument('--prompt', required=False, default='', help='prompt text') parser.add_argument('--negative', required=False, default='', help='negative prompt text') parser.add_argument('--steps', required=False, default=20, help='number of steps') @@ -97,20 +97,3 @@ if __name__ == "__main__": args = parser.parse_args() log.info(f'api-faceid: {args}') generate(args) - -""" -request.face.mode, -request.face.source_images, -request.face.ip_model, -request.face.ip_override_sampler, -request.face.ip_cache_model, -request.face.ip_strength, -request.face.ip_structure, -request.face.id_strength, -request.face.id_conditioning, -request.face.id_cache, -request.face.pm_trigger, -request.face.pm_strength, -request.face.pm_start, -request.face.fs_cache -""" diff --git a/cli/api-json.py b/cli/api-json.py index 79c0ebc3b..61e5ec3ce 100755 --- a/cli/api-json.py +++ b/cli/api-json.py @@ -45,7 +45,7 @@ if __name__ == "__main__": log.info(f'api-json: {args}') if os.path.isfile(args.json[0]): with open(args.json[0], 'r', encoding='ascii') as f: - dct = json.load(f) # TODO fails with b64 encoded images inside json due to string encoding + dct = json.load(f) else: dct = json.loads(args.json[0]) res = post(endpoint=args.endpoint[0], payload=dct) diff --git a/cli/api-progress.py b/cli/api-progress.py index 2c90fe95f..00ed618d2 100755 --- a/cli/api-progress.py +++ b/cli/api-progress.py @@ -1,5 +1,9 @@ #!/usr/bin/env python +""" +check progress of last job and shutdown system if timeout reached +""" + import os import time import datetime @@ -16,7 +20,7 @@ opts = Dot({ "timeout": 3600, "frequency": 60, "action": "sudo shutdown now", - "url": "https://127.0.0.1:7860", + "url": "http://127.0.0.1:7860", "user": "", "password": "", }) diff --git a/cli/api-txt2img.py b/cli/api-txt2img.py index 89d84be80..868b13eee 100755 --- a/cli/api-txt2img.py +++ b/cli/api-txt2img.py @@ -48,7 +48,7 @@ def generate(args): # pylint: disable=redefined-outer-name options['sampler_name'] = args.sampler options['width'] = int(args.width) options['height'] = int(args.height) - if args.faces: + if args.detailer: options['detailer'] = args.detailer options['denoising_strength'] = 0.5 options['hr_sampler_name'] = args.sampler diff --git a/cli/api-upscale.py b/cli/api-upscale.py index 7f188650f..488f2db45 100755 --- a/cli/api-upscale.py +++ b/cli/api-upscale.py @@ -73,7 +73,8 @@ def upscale(args): # pylint: disable=redefined-outer-name if 'image' in data: b64 = data['image'].split(',',1)[0] image = Image.open(io.BytesIO(base64.b64decode(b64))) - image.save(args.output) + if args.output: + image.save(args.output) log.info(f'received: image={image} file={args.output} time={t1-t0:.2f}') else: log.warning(f'no images received: {data}') @@ -82,7 +83,7 @@ def upscale(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": parser = argparse.ArgumentParser(description = 'api-upscale') parser.add_argument('--input', required=True, help='input image') - parser.add_argument('--output', required=True, help='output image') + parser.add_argument('--output', required=False, help='output image') parser.add_argument('--upscaler', required=False, default='Nearest', help='upscaler name') parser.add_argument('--scale', required=False, default=2, help='upscaler scale') args = parser.parse_args() diff --git a/modules/api/api.py b/modules/api/api.py index 47612559b..0ba388855 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -57,7 +57,7 @@ class Api: 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", self.process.post_preprocess, methods=["POST"]) self.add_api_route("/sdapi/v1/mask", self.process.post_mask, methods=["POST"]) - self.add_api_route("/sdapi/v1/faces", self.process.post_face, methods=["POST"]) + self.add_api_route("/sdapi/v1/detect", self.process.post_detect, methods=["POST"]) # 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/generate.py b/modules/api/generate.py index e22102057..d940036fc 100644 --- a/modules/api/generate.py +++ b/modules/api/generate.py @@ -40,7 +40,7 @@ class APIGenerate(): sanitize_str(request.script_args) def prepare_face_module(self, request): - if hasattr(request, "face") and request.face and not request.script_name and (not request.alwayson_scripts or "face" not in request.alwayson_scripts.keys()): + if getattr(request, "face", None) is not None and (not request.alwayson_scripts or "face" not in request.alwayson_scripts.keys()): request.script_name = "face" request.script_args = [ request.face.mode, @@ -116,7 +116,10 @@ class APIGenerate(): p.script_args = tuple(script_args) # Need to pass args as tuple here processed = process_images(p) shared.state.end(api=False) - b64images = list(map(helpers.encode_pil_to_base64, processed.images)) if send_images else [] + if processed.images is None or len(processed.images) == 0: + b64images = [] + else: + b64images = list(map(helpers.encode_pil_to_base64, processed.images)) if send_images else [] self.sanitize_b64(txt2imgreq) return models.ResTxt2Img(images=b64images, parameters=vars(txt2imgreq), info=processed.js()) @@ -162,7 +165,10 @@ class APIGenerate(): p.script_args = tuple(script_args) # Need to pass args as tuple here processed = process_images(p) shared.state.end(api=False) - b64images = list(map(helpers.encode_pil_to_base64, processed.images)) if send_images else [] + if processed.images is None or len(processed.images) == 0: + b64images = [] + else: + b64images = list(map(helpers.encode_pil_to_base64, processed.images)) if send_images else [] if not img2imgreq.include_init_images: img2imgreq.init_images = None img2imgreq.mask = None diff --git a/modules/api/process.py b/modules/api/process.py index f50d58381..80b19c52e 100644 --- a/modules/api/process.py +++ b/modules/api/process.py @@ -28,8 +28,12 @@ class ReqMask(BaseModel): class ReqFace(BaseModel): image: str = Field(title="Image", description="The base64 encoded image") + model: Optional[str] = Field(title="Model", description="The model to use for detection") class ResFace(BaseModel): + classes: List[int] = Field(title="Class", description="The class of detected item") + labels: List[str] = Field(title="Label", description="The label of detected item") + boxes: List[List[int]] = Field(title="Box", description="The bounding box of detected item") images: List[str] = Field(title="Image", description="The base64 encoded images of detected faces") scores: List[float] = Field(title="Scores", description="The scores of the detected faces") @@ -106,16 +110,22 @@ class APIProcess(): image = encode_pil_to_base64(processed) return ResMask(mask=image) - def post_face(self, req: ReqFace): - from shared import yolo # pylint: disable=no-name-in-module + def post_detect(self, req: ReqFace): + from modules.shared import yolo # pylint: disable=no-name-in-module image = decode_base64_to_image(req.image) shared.state.begin('API-FACE', api=True) images = [] scores = [] + classes = [] + boxes = [] + labels = [] with self.queue_lock: - faces = yolo.predict('face-yolo8n', image) - for face in faces: - images.append(encode_pil_to_base64(face.item)) - scores.append(face.score) + items = yolo.predict(req.model, image) + for item in items: + images.append(encode_pil_to_base64(item.item)) + scores.append(item.score) + classes.append(item.cls) + labels.append(item.label) + boxes.append(item.box) shared.state.end(api=False) - return ResFace(images=images, scores=scores) + return ResFace(classes=classes, labels=labels, scores=scores, boxes=boxes, images=images) diff --git a/modules/api/script.py b/modules/api/script.py index e7d6b1f1a..2c0814ef0 100644 --- a/modules/api/script.py +++ b/modules/api/script.py @@ -9,12 +9,15 @@ from modules.errors import log def script_name_to_index(name, scripts_list): if name is None or len(name) == 0 or name == 'none': return None - try: - return [script.title().lower() for script in scripts_list].index(name.lower()) - except Exception: - log.error(f'API: script={name} not found') - return None - # raise HTTPException(status_code=422, detail=f"Script '{name}' not found") from e + available = [script.title().lower() for script in scripts_list] + if name.lower() in available: + return available.index(name.lower()) + short = [available.split(':')[0] for available in available] + if name.lower() in short: + return short.index(name.lower()) + log.error(f'API: script={name} available={available} not found') + return None + def get_selectable_script(script_name, script_runner): if script_name is None or script_name == "" or script_name == 'none': @@ -25,12 +28,14 @@ def get_selectable_script(script_name, script_runner): script = script_runner.selectable_scripts[script_idx] return script, script_idx + def get_scripts_list(): t2ilist = [script.name for script in scripts.scripts_txt2img.scripts if script.name is not None] i2ilist = [script.name for script in scripts.scripts_img2img.scripts if script.name is not None] control = [script.name for script in scripts.scripts_control.scripts if script.name is not None] return models.ResScripts(txt2img = t2ilist, img2img = i2ilist, control = control) + def get_script_info(script_name: Optional[str] = None): res = [] for script_list in [scripts.scripts_txt2img.scripts, scripts.scripts_img2img.scripts, scripts.scripts_control.scripts]: @@ -39,6 +44,7 @@ def get_script_info(script_name: Optional[str] = None): res.append(script.api_info) return res + def get_script(script_name, script_runner): if script_name is None or script_name == "" or script_name == 'none': return None, None @@ -47,6 +53,7 @@ def get_script(script_name, script_runner): return None return script_runner.scripts[script_idx] + def init_default_script_args(script_runner): # find max idx from the scripts in runner and generate a none array to init script_args last_arg_index = 1 @@ -69,6 +76,7 @@ def init_default_script_args(script_runner): script_args[script.args_from:script.args_to] = ui_default_values return script_args + def init_script_args(p, request, default_script_args, selectable_scripts, selectable_script_idx, script_runner): script_args = default_script_args.copy() # position 0 in script_arg is the idx+1 of the selectable script that is going to be run when using scripts.scripts_*2img.run() diff --git a/modules/postprocess/yolo.py b/modules/postprocess/yolo.py index 1a52e1334..920e126e2 100644 --- a/modules/postprocess/yolo.py +++ b/modules/postprocess/yolo.py @@ -29,6 +29,9 @@ class YoloResult: self.height = height self.args = args + def __str__(self): + return f'cls={self.cls} label={self.label} score={self.score} box={self.box} mask={self.mask} item={self.item} size={self.width}x{self.height} args={self.args}' + class YoloRestorer(Detailer): def __init__(self): @@ -76,11 +79,15 @@ class YoloRestorer(Detailer): offload: bool = shared.opts.detailer_unload, ) -> list[YoloResult]: + if model is None or (isinstance(model, str) and len(model) == 0): + model = 'yolo11m' result = [] if isinstance(model, str): - model = self.models.get(model, None) - if model is None: + cached = self.models.get(model, None) + if cached is None: _, model = self.load(model) + else: + model = cached if model is None: return result args = { @@ -136,7 +143,8 @@ class YoloRestorer(Detailer): draw = ImageDraw.Draw(mask_image) draw.rectangle(box, fill="white", outline=None, width=0) cropped = image.crop(box) - result.append(YoloResult(cls=cls, label=label, score=round(score, 2), box=box, mask=mask_image, item=cropped, width=w, height=h, args=args)) + res = YoloResult(cls=cls, label=label, score=round(score, 2), box=box, mask=mask_image, item=cropped, width=w, height=h, args=args) + result.append(res) if len(result) >= shared.opts.detailer_max: break return result diff --git a/modules/processing_args.py b/modules/processing_args.py index 0a066ec6e..6c9a98369 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -20,8 +20,9 @@ def task_specific_kwargs(p, model): task_args = {} is_img2img_model = bool('Zero123' in shared.sd_model.__class__.__name__) if len(getattr(p, 'init_images', [])) > 0: - p.init_images = [helpers.decode_base64_to_image(i, quiet=True) for i in p.init_images if isinstance(i, str)] - p.init_images = [i.convert('RGB') for i in p.init_images if isinstance(i, Image.Image)] + if isinstance(p.init_images[0], str): + p.init_images = [helpers.decode_base64_to_image(i, quiet=True) for i in p.init_images] + p.init_images = [i.convert('RGB') if i.mode != 'RGB' else i for i in p.init_images] if sd_models.get_diffusers_task(model) == sd_models.DiffusersTaskType.TEXT_2_IMAGE or len(getattr(p, 'init_images', [])) == 0 and not is_img2img_model: p.ops.append('txt2img') if hasattr(p, 'width') and hasattr(p, 'height'): diff --git a/modules/vqa.py b/modules/vqa.py index a0a5d147c..ee4197a5e 100644 --- a/modules/vqa.py +++ b/modules/vqa.py @@ -156,8 +156,8 @@ def florence(question: str, image: Image.Image, repo: str = None, revision: str task = question.split('>', 1)[0] + '>' else: task = '' - question = task + question - inputs = processor(text=question, images=image, return_tensors="pt") + # question = task + question + inputs = processor(text=task, images=image, return_tensors="pt") input_ids = inputs['input_ids'].to(devices.device) pixel_values = inputs['pixel_values'].to(devices.device, devices.dtype) with devices.inference_context():