Skip to content
This repository was archived by the owner on Aug 2, 2025. It is now read-only.

Commit eff18cb

Browse files
committed
1 parent 1e9a550 commit eff18cb

2 files changed

Lines changed: 92 additions & 47 deletions

File tree

README.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
# RunPod-Fooocus-API
1+
![Static Badge](https://img.shields.io/badge/API_version-0.3.29-blue) ![Static Badge](https://img.shields.io/badge/API_coverage-100%25-vividgreen) ![Static Badge](https://img.shields.io/badge/API_tests-passed-vividgreen) ![Static Badge](https://img.shields.io/badge/Known_bugs-1-red) ![Static Badge](https://img.shields.io/badge/Fooocus_version-2.0.78-lightgrey)
22

3-
This is a RunPod Fooocus-API worker that expects a Fooocus-API v0.3.26 instance installed on a RunPod Network Volume.
3+
# RunPod-Fooocus-API
44

5-
Ready-to-use Docker Image with this repo's code: https://hub.docker.com/r/3wad/runpod-fooocus-api (use `3wad/runpod-fooocus-api:0.2.41`)
5+
This is a RunPod Fooocus-API worker that expects a **Fooocus-API `v0.3.29`** instance installed on a RunPod Network Volume.
6+
For ready-to-use serverless endpoint image with this repo's code use: [`3wad/runpod-fooocus-api:0.3.29`](https://hub.docker.com/r/3wad/runpod-fooocus-api/tags)
67

78
## How to prepare Network Volume
8-
- Create RunPod network volume. 15GB is just enough for the generic Foocus with Juggernaut model. You can increase its size any time if you need additional models, loras etc. But unfortunately, it cannot be reduced back.
9-
- Create a custom Pod Template and use the `konieshadow/fooocus-api:v0.3.26` image. I went with 30GB disk sizes, mount path /workspace, and expose http 8888 and tcp 22.
10-
- Run the network volume with the custom fooocus-api image you've just created. You don't need a strong GPU pod, the installation is CPU and download-intensive, but be aware that some older-gen pods might not support the required CUDA versions. Let it download and install everything. After the Juggernaut model is downloaded, use the connect button to load into the Fooocus-API docs running on the pod's 8888 port. Here you should try all the API methods you plan to use. Not only to verify they work, but also additional models are downloaded once you run inpaint, outpaint, upscale, vary and image inputs (canny, face swap etc.) endpoints for the first time.
11-
- After that you are ready to connect to the pod's console and use cp -r /app/* /workspace/ to copy everything into the persistent network volume
9+
- [**Create RunPod network volume:**](https://www.runpod.io/console/user/storage)
10+
17GB is just enough for the generic Foocus with Juggernaut and all controlnet models. You can increase its size any time if you need additional models, loras etc. But unfortunately, it cannot be reduced back without creating new one.
11+
- [**Create a custom Pod Template:**](https://www.runpod.io/console/user/templates) and use the `konieshadow/fooocus-api:v0.3.29` image. I went with 30GB disk sizes, mount path `/workspace`, and expose `http 8888` and `tcp 22`.
12+
- [**Run a GPU pod:**](https://www.runpod.io/console/gpu-secure-cloud) with network volume and custom fooocus-api template you've just created. You don't need a strong GPU pod, the installation is CPU and download-intensive, but be aware that some older-gen pods might not support the required CUDA versions. Let it download and install everything. After the Juggernaut model is downloaded, use the connect button to load into the Fooocus-API docs running on the pod's 8888 port. Here you should try all the API methods you plan to use. Not only to verify they work, but also because additional up-to-date models are downloaded once you run inpaint, outpaint, upscale, vary and img2img (canny, face swap etc.) endpoints for the first time.
13+
- After that you are ready to connect to the pod's console and use `cp -r /app/* /workspace/` to copy everything into the persistent network volume
1214
- Once everything is copied successfully, you can terminate the pod. You have the network volume ready.
13-
- ---
14-
- Now you can use our premade image: `3wad/runpod-fooocus-api:0.2.41` and skip the next step OR create your custom docker image from this repo that will run on the actual serverless API. Feel free to adjust handler.py based on how you want to make your requests and it's parameters, or add additional features.
15-
- Once you build it, upload it to the Docker Hub.
16-
- Now you create a custom Serverless Pod Template using the Docker Hub image you've just uploaded (or our premade one). Active container disk should be slightly bigger than the size of the worker docker image.
17-
- Create a new Serverless API Endpoint. Make sure to choose your (or ours) Docker Hub image and not the `konieshadow/fooocus-api` from step 2. In Advanced settings choose your created network volume.
18-
- Other settings are your choice, but I personally found that using 4090/L4 GPUs + Flashboot is the most cost-effective one. In frequent use, the 4090 is able to return an image in ~8s including cold start, making it ~4x cheaper to run this on RunPod than for example using DALLE-3 API. This fact can of course vary based on datacenter locations and GPU availability.
15+
---
16+
- Now you can use our premade image: `3wad/runpod-fooocus-api:0.3.29` and skip the next step OR create your custom docker image from this repo that will run on the actual serverless API. Feel free to adjust the code to your needs.
17+
- *If you built your own image, upload it to the Docker Hub.*
18+
- [**Create a custom Serverless Pod Template:**](https://www.runpod.io/console/serverless/user/templates) using the Docker Hub image you've just uploaded (or our premade one). Active container disk should be slightly bigger than the size of that docker image. In the case of our prebuild one, it's currently about 13.7GB
19+
- [**Create a new Serverless API Endpoint:**](https://www.runpod.io/console/serverless) Make sure to choose your (or our) Docker Hub image and not the `konieshadow/fooocus-api` from the step 2. In Advanced settings choose your created network volume.
20+
- Other settings are your choice, but I personally found that using 4090/L4 GPUs + Flashboot is the most cost-effective one. In frequent use, the 4090 is able to return a txt2img in ~8s including cold start, making it **~25x** cheaper to run Fooocus on RunPod than for example using DALLE-3 API. **(01/24 prices: 0,0016usd/img vs 0,04usd/img), This fact can of course vary based on datacenter locations and GPU availability.*
21+
22+
## How to send requests
23+
[request_examples.js](https://github.com/davefojtik/RunPod-Fooocus-API/blob/main/request_examples.js) contain example payloads for all endpoints on your serverless worker. But don't hesitate to ask in the [Discussions](https://github.com/davefojtik/RunPod-Fooocus-API/discussions) if you need more help.
1924

20-
## Contributors welcomed
21-
Feel free to do pull requests, fixes, improvements and suggestions to the code. I can spend limited time on this as it's only a side project for our community discord bot. So more people would definitely help me maintain this.
25+
## Contributors Welcomed
26+
Feel free to do pull requests, fixes, improvements and suggestions to the code. I can spend only limited time on this as it's a side project for our community discord bot. So any cooperation will help manage this repo better.

src/handler.py

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Native
2+
import shutil
3+
import os
24
import time
35
import requests
46
import re
@@ -29,15 +31,16 @@ def wait_for_service(url):
2931
print("Service not ready yet. Retrying...")
3032
except Exception as err:
3133
print("Error: ", err)
32-
33-
time.sleep(1)
34-
34+
time.sleep(0.5)
3535

3636
def run_inference(params):
3737
config = {
3838
"baseurl": "http://127.0.0.1:8888",
3939
"api": {
40-
"txt2img": ("POST", "/v1/generation/text-to-image"),
40+
"home": ("GET", "/"),
41+
"ping": ("GET", "/ping"),
42+
"txt2img": ("POST", "/v1/generation/text-to-image"),
43+
"txt2img-ip": ("POST", "/v2/generation/text-to-image-with-ip"),
4144
"upscale-vary": ("POST", "/v1/generation/image-upscale-vary"), #multipart/form-data
4245
"upscale-vary2": ("POST", "/v2/generation/image-upscale-vary"),
4346
"inpaint-outpaint": ("POST", "/v1/generation/image-inpait-outpaint"), #multipart/form-data
@@ -46,10 +49,12 @@ def run_inference(params):
4649
"img2img2": ("POST", "/v2/generation/image-prompt"),
4750
"queryjob": ("GET", "/v1/generation/query-job"),
4851
"jobqueue": ("GET", "/v1/generation/job-queue"),
49-
"stop": ("POST", "/v1/generation/stop"),
50-
"models": ("GET", "/v1/engines/all-models"),
51-
"models-refresh": ("POST", "/v1/engines/refresh-models"),
52-
"styles": ("GET", "/v1/engines/styles")
52+
"jobhistory": ("GET", "/v1/generation/job-history"),
53+
"stop": ("POST", "/v1/generation/stop"),
54+
"describe": ("POST", "/v1/tools/describe-image"), #multipart/form-data
55+
"models": ("GET", "/v1/engines/all-models"),
56+
"models-refresh": ("POST", "/v1/engines/refresh-models"),
57+
"styles": ("GET", "/v1/engines/styles")
5358
},
5459
"timeout": 300
5560
}
@@ -60,38 +65,56 @@ def run_inference(params):
6065
api_config = config["api"][api_name]
6166
else:
6267
raise Exception("Method '%s' not yet implemented" % api_name)
63-
#
68+
6469
api_verb = api_config[0]
6570
api_path = api_config[1]
6671
response = {}
6772

68-
if api_verb == "GET":
69-
response = sd_session.get(
70-
url='%s%s' % (config["baseurl"], api_path),
71-
timeout=config["timeout"])
73+
# You can send the input_image, input_mask, and cn_images as PNG: binary encoded into base64 string OR as url string
74+
input_imgs = {'input_image':None, 'input_mask':None, 'cn_img1':None, 'cn_img2':None, 'cn_img3':None, 'cn_img4':None, 'image_prompts':[None,None,None,None], 'image':None}
75+
76+
def process_img(value):
77+
if re.search(r'https?:\/\/\S+', value) is not None:
78+
return requests.get(value).content
79+
elif re.search(r'^[A-Za-z0-9+/]+[=]{0,2}$', value) is not None and value != "None":
80+
return base64.b64decode(value)
81+
else:
82+
return value
7283

73-
# You can send the input_image, input_mask, and cn_images as binary encoded into url-safe base64 string or as publicly accessible url link string
74-
input_imgs = {'input_image':None, 'input_mask':None, 'cn_img1':None, 'cn_img2':None, 'cn_img3':None, 'cn_img4':None,}
75-
for key in input_imgs.items():
84+
for key, value in input_imgs.items():
7685
if key in params:
7786
try:
78-
if not re.search(r'https?://\S+', params[key]) is None:
79-
img_url = params.get(key)
80-
input_imgs[key] = requests.get(img_url).content
87+
if key == "image_prompts":
88+
for index, prompt in enumerate(params.get("image_prompts", [])):
89+
input_imgs["image_prompts"][index] = process_img(prompt["cn_img"])
8190
else:
82-
input_imgs[key] = base64.urlsafe_b64decode(params[key])
91+
input_imgs[key] = process_img(params[key])
8392
except Exception as e:
8493
print("Image conversion task failed: ", e)
85-
return None
94+
return e
95+
96+
# --- Send requests to the Fooocus-API ---
97+
if api_verb == "GET":
98+
response = sd_session.get(url='%s%s' % (config["baseurl"], api_path), timeout=config["timeout"])
8699

87100
if api_verb == "POST":
88101
# If the request should be multipart/form-data, convert the application/json data into it.
89-
if api_name in ["upscale-vary", "inpaint-outpaint", "img2img"]:
102+
if api_name in ["upscale-vary", "inpaint-outpaint", "img2img", "describe"]:
90103
try:
91-
# Remove the input_image key/value from original request data (it gets confused otherwise)
92-
del params['input_image']
104+
# Replace the original image params with the processed ones
105+
for key, value in input_imgs.items():
106+
if value is not None:
107+
if type(value) == list:
108+
for i, value in enumerate(input_imgs['image_prompts']):
109+
if value is not None:
110+
params['image_prompts'][i]['cn_img'] = (key+'.png', value, 'image/png')
111+
else:
112+
if isinstance(value, bytes):
113+
params[key] = (key+'.png', value, 'image/png')
114+
else:
115+
params[key] = value
93116
# Convert
94-
multipart_data = MultipartEncoder(fields={key: (f'{key}.png', value, 'image/png') for key, value in input_imgs.items()}, **params)
117+
multipart_data = MultipartEncoder(fields={**params})
95118
headers = {'Content-Type': multipart_data.content_type}
96119
response = sd_session.post(
97120
url='%s%s' % (config["baseurl"], api_path),
@@ -100,17 +123,29 @@ def run_inference(params):
100123
timeout=config["timeout"])
101124
except Exception as e:
102125
print("multipart/form-data task failed: ", e)
103-
return None
126+
return e
104127
# If the final request should be application/json. Send the original request data
105128
else:
129+
# Convert the processed binary image back to url-safe-base64
106130
for key, value in input_imgs.items():
107-
if not value is None:
108-
params[key] = base64.urlsafe_b64encode(value).decode('utf-8')
131+
if value is not None:
132+
if type(value) == list:
133+
for i, value in enumerate(input_imgs['image_prompts']):
134+
if isinstance(value, bytes):
135+
params['image_prompts'][i]["cn_img"] = base64.b64encode(value).decode('utf-8')
136+
elif isinstance(value, bytes):
137+
params[key] = base64.b64encode(value).decode('utf-8')
109138
response = sd_session.post(
110139
url='%s%s' % (config["baseurl"], api_path),
111140
json=params,
112141
timeout=config["timeout"])
113-
return response.json()
142+
143+
# --- Return the API response to the RunPod ---
144+
content_type = response.headers.get('Content-Type', '')
145+
if 'application/json' in content_type:
146+
return response.json()
147+
else:
148+
return response.text
114149

115150
# ---------------------------------------------------------------------------- #
116151
# RunPod Handler #
@@ -119,16 +154,21 @@ def handler(event):
119154
'''
120155
This is the handler function that will be called by the serverless.
121156
'''
157+
# Clear outputs (Comment out or delete this part to keep images on the volume. Keep in mind you'll always need enough space to generate more)
158+
print("Clearing outputs...")
159+
shutil.rmtree('/workspace/outputs/files')
160+
os.makedirs('/workspace/outputs/files')
161+
shutil.rmtree('/workspace/repositories/Fooocus/outputs')
162+
os.makedirs('/workspace/repositories/Fooocus/outputs')
122163

123164
json = run_inference(event["input"])
124-
165+
125166
# Return the output that you want to be returned like pre-signed URLs to output artifacts
126167
return json
127168

128-
129169
if __name__ == "__main__":
130170
wait_for_service(url='http://127.0.0.1:8888/v1/generation/text-to-image')
131171

132172
print("Fooocus API Service is ready. Starting RunPod...")
133173

134-
runpod.serverless.start({"handler": handler})
174+
runpod.serverless.start({"handler": handler})

0 commit comments

Comments
 (0)