Skip to content

Commit

Permalink
Merge pull request AUTOMATIC1111#3722 from evshiron/feat/progress-api
Browse files Browse the repository at this point in the history
prototype progress api
  • Loading branch information
AUTOMATIC1111 authored Oct 30, 2022
2 parents 61836bd + 9f4f894 commit 060ee5d
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 27 deletions.
91 changes: 75 additions & 16 deletions modules/api/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import time
import uvicorn
from gradio.processing_utils import encode_pil_to_base64, decode_base64_to_file, decode_base64_to_image
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
import modules.shared as shared
from modules import devices
from modules.api.models import *
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
from modules.sd_samplers import all_samplers
from modules.extras import run_extras, run_pnginfo

# copy from wrap_gradio_gpu_call of webui.py
# because queue lock will be acquired in api handlers
# and time start needs to be set
# the function has been modified into two parts

def before_gpu_call():
devices.torch_gc()

shared.state.sampling_step = 0
shared.state.job_count = -1
shared.state.job_no = 0
shared.state.job_timestamp = shared.state.get_job_timestamp()
shared.state.current_latent = None
shared.state.current_image = None
shared.state.current_image_sampling_step = 0
shared.state.skipped = False
shared.state.interrupted = False
shared.state.textinfo = None
shared.state.time_start = time.time()

def after_gpu_call():
shared.state.job = ""
shared.state.job_count = 0

devices.torch_gc()

def upscaler_to_index(name: str):
try:
return [x.name.lower() for x in shared.sd_upscalers].index(name.lower())
Expand All @@ -33,50 +61,53 @@ def __init__(self, app, queue_lock):
self.app.add_api_route("/sdapi/v1/extra-single-image", self.extras_single_image_api, methods=["POST"], response_model=ExtrasSingleImageResponse)
self.app.add_api_route("/sdapi/v1/extra-batch-images", self.extras_batch_images_api, methods=["POST"], response_model=ExtrasBatchImagesResponse)
self.app.add_api_route("/sdapi/v1/png-info", self.pnginfoapi, methods=["POST"], response_model=PNGInfoResponse)
self.app.add_api_route("/sdapi/v1/progress", self.progressapi, methods=["GET"], response_model=ProgressResponse)

def text2imgapi(self, txt2imgreq: StableDiffusionTxt2ImgProcessingAPI):
sampler_index = sampler_to_index(txt2imgreq.sampler_index)

if sampler_index is None:
raise HTTPException(status_code=404, detail="Sampler not found")
raise HTTPException(status_code=404, detail="Sampler not found")

populate = txt2imgreq.copy(update={ # Override __init__ params
"sd_model": shared.sd_model,
"sd_model": shared.sd_model,
"sampler_index": sampler_index[0],
"do_not_save_samples": True,
"do_not_save_grid": True
}
)
p = StableDiffusionProcessingTxt2Img(**vars(populate))
# Override object param
before_gpu_call()
with self.queue_lock:
processed = process_images(p)

after_gpu_call()

b64images = list(map(encode_pil_to_base64, processed.images))

return TextToImageResponse(images=b64images, parameters=vars(txt2imgreq), info=processed.js())

def img2imgapi(self, img2imgreq: StableDiffusionImg2ImgProcessingAPI):
sampler_index = sampler_to_index(img2imgreq.sampler_index)

if sampler_index is None:
raise HTTPException(status_code=404, detail="Sampler not found")
raise HTTPException(status_code=404, detail="Sampler not found")


init_images = img2imgreq.init_images
if init_images is None:
raise HTTPException(status_code=404, detail="Init image not found")
raise HTTPException(status_code=404, detail="Init image not found")

mask = img2imgreq.mask
if mask:
mask = decode_base64_to_image(mask)


populate = img2imgreq.copy(update={ # Override __init__ params
"sd_model": shared.sd_model,
"sd_model": shared.sd_model,
"sampler_index": sampler_index[0],
"do_not_save_samples": True,
"do_not_save_grid": True,
"do_not_save_grid": True,
"mask": mask
}
)
Expand All @@ -89,15 +120,17 @@ def img2imgapi(self, img2imgreq: StableDiffusionImg2ImgProcessingAPI):

p.init_images = imgs
# Override object param
before_gpu_call()
with self.queue_lock:
processed = process_images(p)

after_gpu_call()

b64images = list(map(encode_pil_to_base64, processed.images))

if (not img2imgreq.include_init_images):
img2imgreq.init_images = None
img2imgreq.mask = None

return ImageToImageResponse(images=b64images, parameters=vars(img2imgreq), info=processed.js())

def extras_single_image_api(self, req: ExtrasSingleImageRequest):
Expand Down Expand Up @@ -125,7 +158,7 @@ def prepareFiles(file):
result = run_extras(extras_mode=1, image="", input_dir="", output_dir="", **reqDict)

return ExtrasBatchImagesResponse(images=list(map(encode_pil_to_base64, result[0])), html_info=result[1])

def pnginfoapi(self, req: PNGInfoRequest):
if(not req.image.strip()):
return PNGInfoResponse(info="")
Expand All @@ -134,6 +167,32 @@ def pnginfoapi(self, req: PNGInfoRequest):

return PNGInfoResponse(info=result[1])

def progressapi(self, req: ProgressRequest = Depends()):
# copy from check_progress_call of ui.py

if shared.state.job_count == 0:
return ProgressResponse(progress=0, eta_relative=0, state=shared.state.dict())

# avoid dividing zero
progress = 0.01

if shared.state.job_count > 0:
progress += shared.state.job_no / shared.state.job_count
if shared.state.sampling_steps > 0:
progress += 1 / shared.state.job_count * shared.state.sampling_step / shared.state.sampling_steps

time_since_start = time.time() - shared.state.time_start
eta = (time_since_start/progress)
eta_relative = eta-time_since_start

progress = min(progress, 1)

current_image = None
if shared.state.current_image and not req.skip_current_image:
current_image = encode_pil_to_base64(shared.state.current_image)

return ProgressResponse(progress=progress, eta_relative=eta_relative, state=shared.state.dict(), current_image=current_image)

def launch(self, server_name, port):
self.app.include_router(self.router)
uvicorn.run(self.app, host=server_name, port=port)
31 changes: 20 additions & 11 deletions modules/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ def field_type_generator(k, v):
# field_type = str if not overrides.get(k) else overrides[k]["type"]
# print(k, v.annotation, v.default)
field_type = v.annotation

return Optional[field_type]

def merge_class_params(class_):
all_classes = list(filter(lambda x: x is not object, inspect.getmro(class_)))
parameters = {}
for classes in all_classes:
parameters = {**parameters, **inspect.signature(classes.__init__).parameters}
return parameters


self._model_name = model_name
self._class_data = merge_class_params(class_instance)
self._model_def = [
Expand All @@ -74,11 +74,11 @@ def merge_class_params(class_):
)
for (k,v) in self._class_data.items() if k not in API_NOT_ALLOWED
]

for fields in additional_fields:
self._model_def.append(ModelDef(
field=underscore(fields["key"]),
field_alias=fields["key"],
field=underscore(fields["key"]),
field_alias=fields["key"],
field_type=fields["type"],
field_value=fields["default"],
field_exclude=fields["exclude"] if "exclude" in fields else False))
Expand All @@ -95,15 +95,15 @@ def generate_model(self):
DynamicModel.__config__.allow_population_by_field_name = True
DynamicModel.__config__.allow_mutation = True
return DynamicModel

StableDiffusionTxt2ImgProcessingAPI = PydanticModelGenerator(
"StableDiffusionProcessingTxt2Img",
"StableDiffusionProcessingTxt2Img",
StableDiffusionProcessingTxt2Img,
[{"key": "sampler_index", "type": str, "default": "Euler"}]
).generate_model()

StableDiffusionImg2ImgProcessingAPI = PydanticModelGenerator(
"StableDiffusionProcessingImg2Img",
"StableDiffusionProcessingImg2Img",
StableDiffusionProcessingImg2Img,
[{"key": "sampler_index", "type": str, "default": "Euler"}, {"key": "init_images", "type": list, "default": None}, {"key": "denoising_strength", "type": float, "default": 0.75}, {"key": "mask", "type": str, "default": None}, {"key": "include_init_images", "type": bool, "default": False, "exclude" : True}]
).generate_model()
Expand Down Expand Up @@ -155,4 +155,13 @@ class PNGInfoRequest(BaseModel):
image: str = Field(title="Image", description="The base64 encoded PNG image")

class PNGInfoResponse(BaseModel):
info: str = Field(title="Image info", description="A string with all the info the image had")
info: str = Field(title="Image info", description="A string with all the info the image had")

class ProgressRequest(BaseModel):
skip_current_image: bool = Field(default=False, title="Skip current image", description="Skip current image serialization")

class ProgressResponse(BaseModel):
progress: float = Field(title="Progress", description="The progress with a range of 0 to 1")
eta_relative: float = Field(title="ETA in secs")
state: dict = Field(title="State", description="The current state snapshot")
current_image: str = Field(default=None, title="Current image", description="The current image in base64 format. opts.show_progress_every_n_steps is required for this to work.")
13 changes: 13 additions & 0 deletions modules/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ def nextjob(self):
def get_job_timestamp(self):
return datetime.datetime.now().strftime("%Y%m%d%H%M%S") # shouldn't this return job_timestamp?

def dict(self):
obj = {
"skipped": self.skipped,
"interrupted": self.skipped,
"job": self.job,
"job_count": self.job_count,
"job_no": self.job_no,
"sampling_step": self.sampling_step,
"sampling_steps": self.sampling_steps,
}

return obj


state = State()

Expand Down

0 comments on commit 060ee5d

Please sign in to comment.