Skip to content

Commit 060ee5d

Browse files
Merge pull request #3722 from evshiron/feat/progress-api
prototype progress api
2 parents 61836bd + 9f4f894 commit 060ee5d

File tree

3 files changed

+108
-27
lines changed

3 files changed

+108
-27
lines changed

modules/api/api.py

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1+
import time
12
import uvicorn
23
from gradio.processing_utils import encode_pil_to_base64, decode_base64_to_file, decode_base64_to_image
3-
from fastapi import APIRouter, HTTPException
4+
from fastapi import APIRouter, Depends, HTTPException
45
import modules.shared as shared
6+
from modules import devices
57
from modules.api.models import *
68
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
79
from modules.sd_samplers import all_samplers
810
from modules.extras import run_extras, run_pnginfo
911

12+
# copy from wrap_gradio_gpu_call of webui.py
13+
# because queue lock will be acquired in api handlers
14+
# and time start needs to be set
15+
# the function has been modified into two parts
16+
17+
def before_gpu_call():
18+
devices.torch_gc()
19+
20+
shared.state.sampling_step = 0
21+
shared.state.job_count = -1
22+
shared.state.job_no = 0
23+
shared.state.job_timestamp = shared.state.get_job_timestamp()
24+
shared.state.current_latent = None
25+
shared.state.current_image = None
26+
shared.state.current_image_sampling_step = 0
27+
shared.state.skipped = False
28+
shared.state.interrupted = False
29+
shared.state.textinfo = None
30+
shared.state.time_start = time.time()
31+
32+
def after_gpu_call():
33+
shared.state.job = ""
34+
shared.state.job_count = 0
35+
36+
devices.torch_gc()
37+
1038
def upscaler_to_index(name: str):
1139
try:
1240
return [x.name.lower() for x in shared.sd_upscalers].index(name.lower())
@@ -33,50 +61,53 @@ def __init__(self, app, queue_lock):
3361
self.app.add_api_route("/sdapi/v1/extra-single-image", self.extras_single_image_api, methods=["POST"], response_model=ExtrasSingleImageResponse)
3462
self.app.add_api_route("/sdapi/v1/extra-batch-images", self.extras_batch_images_api, methods=["POST"], response_model=ExtrasBatchImagesResponse)
3563
self.app.add_api_route("/sdapi/v1/png-info", self.pnginfoapi, methods=["POST"], response_model=PNGInfoResponse)
64+
self.app.add_api_route("/sdapi/v1/progress", self.progressapi, methods=["GET"], response_model=ProgressResponse)
3665

3766
def text2imgapi(self, txt2imgreq: StableDiffusionTxt2ImgProcessingAPI):
3867
sampler_index = sampler_to_index(txt2imgreq.sampler_index)
39-
68+
4069
if sampler_index is None:
41-
raise HTTPException(status_code=404, detail="Sampler not found")
42-
70+
raise HTTPException(status_code=404, detail="Sampler not found")
71+
4372
populate = txt2imgreq.copy(update={ # Override __init__ params
44-
"sd_model": shared.sd_model,
73+
"sd_model": shared.sd_model,
4574
"sampler_index": sampler_index[0],
4675
"do_not_save_samples": True,
4776
"do_not_save_grid": True
4877
}
4978
)
5079
p = StableDiffusionProcessingTxt2Img(**vars(populate))
5180
# Override object param
81+
before_gpu_call()
5282
with self.queue_lock:
5383
processed = process_images(p)
54-
84+
after_gpu_call()
85+
5586
b64images = list(map(encode_pil_to_base64, processed.images))
56-
87+
5788
return TextToImageResponse(images=b64images, parameters=vars(txt2imgreq), info=processed.js())
5889

5990
def img2imgapi(self, img2imgreq: StableDiffusionImg2ImgProcessingAPI):
6091
sampler_index = sampler_to_index(img2imgreq.sampler_index)
61-
92+
6293
if sampler_index is None:
63-
raise HTTPException(status_code=404, detail="Sampler not found")
94+
raise HTTPException(status_code=404, detail="Sampler not found")
6495

6596

6697
init_images = img2imgreq.init_images
6798
if init_images is None:
68-
raise HTTPException(status_code=404, detail="Init image not found")
99+
raise HTTPException(status_code=404, detail="Init image not found")
69100

70101
mask = img2imgreq.mask
71102
if mask:
72103
mask = decode_base64_to_image(mask)
73104

74-
105+
75106
populate = img2imgreq.copy(update={ # Override __init__ params
76-
"sd_model": shared.sd_model,
107+
"sd_model": shared.sd_model,
77108
"sampler_index": sampler_index[0],
78109
"do_not_save_samples": True,
79-
"do_not_save_grid": True,
110+
"do_not_save_grid": True,
80111
"mask": mask
81112
}
82113
)
@@ -89,15 +120,17 @@ def img2imgapi(self, img2imgreq: StableDiffusionImg2ImgProcessingAPI):
89120

90121
p.init_images = imgs
91122
# Override object param
123+
before_gpu_call()
92124
with self.queue_lock:
93125
processed = process_images(p)
94-
126+
after_gpu_call()
127+
95128
b64images = list(map(encode_pil_to_base64, processed.images))
96129

97130
if (not img2imgreq.include_init_images):
98131
img2imgreq.init_images = None
99132
img2imgreq.mask = None
100-
133+
101134
return ImageToImageResponse(images=b64images, parameters=vars(img2imgreq), info=processed.js())
102135

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

127160
return ExtrasBatchImagesResponse(images=list(map(encode_pil_to_base64, result[0])), html_info=result[1])
128-
161+
129162
def pnginfoapi(self, req: PNGInfoRequest):
130163
if(not req.image.strip()):
131164
return PNGInfoResponse(info="")
@@ -134,6 +167,32 @@ def pnginfoapi(self, req: PNGInfoRequest):
134167

135168
return PNGInfoResponse(info=result[1])
136169

170+
def progressapi(self, req: ProgressRequest = Depends()):
171+
# copy from check_progress_call of ui.py
172+
173+
if shared.state.job_count == 0:
174+
return ProgressResponse(progress=0, eta_relative=0, state=shared.state.dict())
175+
176+
# avoid dividing zero
177+
progress = 0.01
178+
179+
if shared.state.job_count > 0:
180+
progress += shared.state.job_no / shared.state.job_count
181+
if shared.state.sampling_steps > 0:
182+
progress += 1 / shared.state.job_count * shared.state.sampling_step / shared.state.sampling_steps
183+
184+
time_since_start = time.time() - shared.state.time_start
185+
eta = (time_since_start/progress)
186+
eta_relative = eta-time_since_start
187+
188+
progress = min(progress, 1)
189+
190+
current_image = None
191+
if shared.state.current_image and not req.skip_current_image:
192+
current_image = encode_pil_to_base64(shared.state.current_image)
193+
194+
return ProgressResponse(progress=progress, eta_relative=eta_relative, state=shared.state.dict(), current_image=current_image)
195+
137196
def launch(self, server_name, port):
138197
self.app.include_router(self.router)
139198
uvicorn.run(self.app, host=server_name, port=port)

modules/api/models.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ def field_type_generator(k, v):
5252
# field_type = str if not overrides.get(k) else overrides[k]["type"]
5353
# print(k, v.annotation, v.default)
5454
field_type = v.annotation
55-
55+
5656
return Optional[field_type]
57-
57+
5858
def merge_class_params(class_):
5959
all_classes = list(filter(lambda x: x is not object, inspect.getmro(class_)))
6060
parameters = {}
6161
for classes in all_classes:
6262
parameters = {**parameters, **inspect.signature(classes.__init__).parameters}
6363
return parameters
64-
65-
64+
65+
6666
self._model_name = model_name
6767
self._class_data = merge_class_params(class_instance)
6868
self._model_def = [
@@ -74,11 +74,11 @@ def merge_class_params(class_):
7474
)
7575
for (k,v) in self._class_data.items() if k not in API_NOT_ALLOWED
7676
]
77-
77+
7878
for fields in additional_fields:
7979
self._model_def.append(ModelDef(
80-
field=underscore(fields["key"]),
81-
field_alias=fields["key"],
80+
field=underscore(fields["key"]),
81+
field_alias=fields["key"],
8282
field_type=fields["type"],
8383
field_value=fields["default"],
8484
field_exclude=fields["exclude"] if "exclude" in fields else False))
@@ -95,15 +95,15 @@ def generate_model(self):
9595
DynamicModel.__config__.allow_population_by_field_name = True
9696
DynamicModel.__config__.allow_mutation = True
9797
return DynamicModel
98-
98+
9999
StableDiffusionTxt2ImgProcessingAPI = PydanticModelGenerator(
100-
"StableDiffusionProcessingTxt2Img",
100+
"StableDiffusionProcessingTxt2Img",
101101
StableDiffusionProcessingTxt2Img,
102102
[{"key": "sampler_index", "type": str, "default": "Euler"}]
103103
).generate_model()
104104

105105
StableDiffusionImg2ImgProcessingAPI = PydanticModelGenerator(
106-
"StableDiffusionProcessingImg2Img",
106+
"StableDiffusionProcessingImg2Img",
107107
StableDiffusionProcessingImg2Img,
108108
[{"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}]
109109
).generate_model()
@@ -155,4 +155,13 @@ class PNGInfoRequest(BaseModel):
155155
image: str = Field(title="Image", description="The base64 encoded PNG image")
156156

157157
class PNGInfoResponse(BaseModel):
158-
info: str = Field(title="Image info", description="A string with all the info the image had")
158+
info: str = Field(title="Image info", description="A string with all the info the image had")
159+
160+
class ProgressRequest(BaseModel):
161+
skip_current_image: bool = Field(default=False, title="Skip current image", description="Skip current image serialization")
162+
163+
class ProgressResponse(BaseModel):
164+
progress: float = Field(title="Progress", description="The progress with a range of 0 to 1")
165+
eta_relative: float = Field(title="ETA in secs")
166+
state: dict = Field(title="State", description="The current state snapshot")
167+
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.")

modules/shared.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ def nextjob(self):
147147
def get_job_timestamp(self):
148148
return datetime.datetime.now().strftime("%Y%m%d%H%M%S") # shouldn't this return job_timestamp?
149149

150+
def dict(self):
151+
obj = {
152+
"skipped": self.skipped,
153+
"interrupted": self.skipped,
154+
"job": self.job,
155+
"job_count": self.job_count,
156+
"job_no": self.job_no,
157+
"sampling_step": self.sampling_step,
158+
"sampling_steps": self.sampling_steps,
159+
}
160+
161+
return obj
162+
150163

151164
state = State()
152165

0 commit comments

Comments
 (0)