1
0
Fork 0
onnx-web/api/onnx_web/serve.py

521 lines
16 KiB
Python
Raw Normal View History

from diffusers import (
2023-01-07 21:05:29 +00:00
# schedulers
DDIMScheduler,
2023-01-05 23:23:37 +00:00
DDPMScheduler,
DPMSolverMultistepScheduler,
DPMSolverSinglestepScheduler,
EulerDiscreteScheduler,
EulerAncestralDiscreteScheduler,
HeunDiscreteScheduler,
KDPM2AncestralDiscreteScheduler,
KDPM2DiscreteScheduler,
KarrasVeScheduler,
2023-01-05 23:23:37 +00:00
LMSDiscreteScheduler,
PNDMScheduler,
2023-01-07 21:05:29 +00:00
# onnx
OnnxStableDiffusionPipeline,
OnnxStableDiffusionImg2ImgPipeline,
2023-01-09 00:11:34 +00:00
OnnxStableDiffusionInpaintPipeline,
# types
DiffusionPipeline,
)
from flask import Flask, jsonify, request, send_from_directory, url_for
2023-01-14 16:18:53 +00:00
from flask_cors import CORS
from flask_executor import Executor
from hashlib import sha256
2023-01-07 21:05:29 +00:00
from io import BytesIO
from PIL import Image, ImageDraw
2023-01-08 16:24:21 +00:00
from struct import pack
from os import environ, makedirs, path, scandir
from typing import Any, Dict, Tuple, Union
import json
2023-01-05 06:44:28 +00:00
import numpy as np
2023-01-05 00:25:00 +00:00
2023-01-05 01:42:37 +00:00
# paths
bundle_path = environ.get('ONNX_WEB_BUNDLE_PATH',
path.join('..', 'gui', 'out'))
2023-01-12 03:59:58 +00:00
model_path = environ.get('ONNX_WEB_MODEL_PATH', path.join('..', 'models'))
output_path = environ.get('ONNX_WEB_OUTPUT_PATH', path.join('..', 'outputs'))
2023-01-12 03:59:58 +00:00
params_path = environ.get('ONNX_WEB_PARAMS_PATH', 'params.json')
2023-01-05 01:42:37 +00:00
# options
2023-01-14 16:18:53 +00:00
cors_origin = environ.get('ONNX_WEB_CORS_ORIGIN', '*').split(',')
num_workers = int(environ.get('ONNX_WEB_NUM_WORKERS', 1))
# pipeline caching
available_models = []
config_params = {}
last_pipeline_instance = None
2023-01-07 21:05:29 +00:00
last_pipeline_options = (None, None, None)
last_pipeline_scheduler = None
# pipeline params
platform_providers = {
'amd': 'DmlExecutionProvider',
'cpu': 'CPUExecutionProvider',
'nvidia': 'CUDAExecutionProvider',
}
2023-01-05 23:23:37 +00:00
pipeline_schedulers = {
'ddim': DDIMScheduler,
'ddpm': DDPMScheduler,
'dpm-multi': DPMSolverMultistepScheduler,
'dpm-single': DPMSolverSinglestepScheduler,
'euler': EulerDiscreteScheduler,
'euler-a': EulerAncestralDiscreteScheduler,
'heun': HeunDiscreteScheduler,
'k-dpm-2-a': KDPM2AncestralDiscreteScheduler,
'k-dpm-2': KDPM2DiscreteScheduler,
'karras-ve': KarrasVeScheduler,
'lms-discrete': LMSDiscreteScheduler,
'pndm': PNDMScheduler,
}
def get_and_clamp_float(args, key: str, default_value: float, max_value: float, min_value=0.0) -> float:
2023-01-08 15:17:23 +00:00
return min(max(float(args.get(key, default_value)), min_value), max_value)
def get_and_clamp_int(args, key: str, default_value: int, max_value: int, min_value=1) -> int:
return min(max(int(args.get(key, default_value)), min_value), max_value)
def get_from_map(args, key: str, values: Dict[str, Any], default: Any):
selected = args.get(key, default)
if selected in values:
return values[selected]
else:
return values[default]
2023-01-05 00:25:00 +00:00
def get_model_path(model: str):
2023-01-07 21:05:29 +00:00
return safer_join(model_path, model)
# from https://www.travelneil.com/stable-diffusion-updates.html
2023-01-05 06:44:28 +00:00
def get_latents_from_seed(seed: int, width: int, height: int) -> np.ndarray:
# 1 is batch size
latents_shape = (1, 4, height // 8, width // 8)
2023-01-05 06:44:28 +00:00
# Gotta use numpy instead of torch, because torch's randn() doesn't support DML
rng = np.random.default_rng(seed)
image_latents = rng.standard_normal(latents_shape).astype(np.float32)
return image_latents
2023-01-05 06:44:28 +00:00
2023-01-14 20:20:42 +00:00
def blend_pixel(source: Tuple[int, int, int], mask: Tuple[int, int, int], noise: int) -> Tuple[int, int, int]:
2023-01-14 20:34:35 +00:00
m = float(noise) / 256
n = 1.0 - m
2023-01-14 20:20:42 +00:00
return (
2023-01-14 20:34:35 +00:00
int((source[0] * n) + (mask[0] * m)),
int((source[1] * n) + (mask[1] * m)),
int((source[2] * n) + (mask[2] * m)),
2023-01-14 20:20:42 +00:00
)
# based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/scripts/outpainting_mk_2.py#L175-L232
def expand_image(source_image: Image, mask_image: Image, dims: Tuple[int, int, int, int]):
(left, right, top, bottom) = dims
full_width = left + source_image.width + right
full_height = top + source_image.height + bottom
full_source = Image.new('RGB', (full_width, full_height), 'white')
full_source.paste(source_image, (left, top))
full_mask = Image.new('RGB', (full_width, full_height), 'white')
full_mask.paste(mask_image, (left, top))
2023-01-14 20:34:35 +00:00
full_noise = Image.effect_noise((full_width, full_height), 200)
for x in range(full_source.width):
for y in range(full_source.height):
mask_color = full_mask.getpixel((x, y))
noise_color = full_noise.getpixel((x, y))
source_color = full_source.getpixel((x, y))
if mask_color[0] > 0:
2023-01-14 20:34:35 +00:00
full_source.putpixel((x, y), blend_pixel(source_color, mask_color, noise_color))
return (full_source, full_mask, (full_width, full_height))
def load_pipeline(pipeline: DiffusionPipeline, model: str, provider: str, scheduler):
global last_pipeline_instance
global last_pipeline_scheduler
global last_pipeline_options
2023-01-07 21:05:29 +00:00
options = (pipeline, model, provider)
if last_pipeline_instance != None and last_pipeline_options == options:
print('reusing existing pipeline')
pipe = last_pipeline_instance
else:
print('loading different pipeline')
2023-01-07 21:05:29 +00:00
pipe = pipeline.from_pretrained(
model,
provider=provider,
safety_checker=None,
2023-01-07 21:05:29 +00:00
scheduler=scheduler.from_pretrained(model, subfolder='scheduler')
)
last_pipeline_instance = pipe
last_pipeline_options = options
last_pipeline_scheduler = scheduler
if last_pipeline_scheduler != scheduler:
print('changing pipeline scheduler')
pipe.scheduler = scheduler.from_pretrained(
2023-01-07 21:05:29 +00:00
model, subfolder='scheduler')
last_pipeline_scheduler = scheduler
return pipe
def serve_bundle_file(filename='index.html'):
return send_from_directory(path.join('..', bundle_path), filename)
def make_output_path(mode: str, seed: int, params: Tuple[Union[str, int, float]]):
sha = sha256()
sha.update(mode.encode('utf-8'))
for param in params:
if param is None:
continue
2023-01-08 16:24:21 +00:00
elif isinstance(param, float):
sha.update(bytearray(pack('!f', param)))
elif isinstance(param, int):
sha.update(bytearray(pack('!I', param)))
elif isinstance(param, str):
sha.update(param.encode('utf-8'))
2023-01-08 16:24:21 +00:00
else:
print('cannot hash param: %s, %s' % (param, type(param)))
output_file = '%s_%s_%s.png' % (mode, seed, sha.hexdigest())
output_full = safer_join(output_path, output_file)
return (output_file, output_full)
def safer_join(base, tail):
safer_path = path.relpath(path.normpath(path.join('/', tail)), '/')
return path.join(base, safer_path)
def url_from_rule(rule):
options = {}
for arg in rule.arguments:
options[arg] = ":%s" % (arg)
return url_for(rule.endpoint, **options)
2023-01-11 05:00:18 +00:00
def pipeline_from_request():
2023-01-11 05:00:18 +00:00
user = request.remote_addr
# pipeline stuff
model = get_model_path(request.args.get(
'model', config_params.get('model').get('default')))
2023-01-11 05:00:18 +00:00
provider = get_from_map(request.args, 'platform',
platform_providers, config_params.get('platform').get('default'))
2023-01-11 05:00:18 +00:00
scheduler = get_from_map(request.args, 'scheduler',
pipeline_schedulers, config_params.get('scheduler').get('default'))
2023-01-11 05:00:18 +00:00
# image params
prompt = request.args.get(
'prompt', config_params.get('prompt').get('default'))
negative_prompt = request.args.get('negativePrompt', None)
2023-01-11 05:00:18 +00:00
if negative_prompt is not None and negative_prompt.strip() == '':
2023-01-11 05:00:18 +00:00
negative_prompt = None
cfg = get_and_clamp_float(
request.args, 'cfg',
config_params.get('cfg').get('default'),
config_params.get('cfg').get('max'),
config_params.get('cfg').get('min'))
steps = get_and_clamp_int(
request.args, 'steps',
config_params.get('steps').get('default'),
config_params.get('steps').get('max'),
config_params.get('steps').get('min'))
2023-01-11 05:00:18 +00:00
height = get_and_clamp_int(
request.args, 'height',
config_params.get('height').get('default'),
config_params.get('height').get('max'),
config_params.get('height').get('min'))
width = get_and_clamp_int(
request.args, 'width',
config_params.get('width').get('default'),
config_params.get('width').get('max'),
config_params.get('width').get('min'))
2023-01-11 05:00:18 +00:00
seed = int(request.args.get('seed', -1))
if seed == -1:
seed = np.random.randint(np.iinfo(np.int32).max)
print("request from %s: %s rounds of %s using %s on %s, %sx%s, %s, %s - %s" %
(user, steps, scheduler.__name__, model, provider, width, height, cfg, seed, prompt))
return (model, provider, scheduler, prompt, negative_prompt, cfg, steps, height, width, seed)
def run_txt2img_pipeline(model, provider, scheduler, prompt, negative_prompt, cfg, steps, seed, output, height, width):
pipe = load_pipeline(OnnxStableDiffusionPipeline,
model, provider, scheduler)
latents = get_latents_from_seed(seed, width, height)
rng = np.random.RandomState(seed)
image = pipe(
prompt,
height,
width,
generator=rng,
guidance_scale=cfg,
latents=latents,
negative_prompt=negative_prompt,
num_inference_steps=steps,
).images[0]
image.save(output)
print('saved txt2img output: %s' % (output))
2023-01-11 05:00:18 +00:00
def run_img2img_pipeline(model, provider, scheduler, prompt, negative_prompt, cfg, steps, seed, output, strength, input_image):
pipe = load_pipeline(OnnxStableDiffusionImg2ImgPipeline,
model, provider, scheduler)
rng = np.random.RandomState(seed)
image = pipe(
prompt,
generator=rng,
guidance_scale=cfg,
image=input_image,
negative_prompt=negative_prompt,
num_inference_steps=steps,
strength=strength,
).images[0]
image.save(output)
print('saved img2img output: %s' % (output))
def run_inpaint_pipeline(model, provider, scheduler, prompt, negative_prompt, cfg, steps, seed, output, height, width, source_image, mask_image):
pipe = load_pipeline(OnnxStableDiffusionInpaintPipeline,
model, provider, scheduler)
latents = get_latents_from_seed(seed, width, height)
rng = np.random.RandomState(seed)
extra = 256
(full_source, full_mask, full_dims) = expand_image(source_image, mask_image, (extra, extra, extra, extra))
2023-01-14 19:24:25 +00:00
image = pipe(
prompt,
generator=rng,
guidance_scale=cfg,
height=height,
2023-01-14 19:24:25 +00:00
image=full_source,
latents=latents,
2023-01-14 19:24:25 +00:00
mask_image=full_mask,
negative_prompt=negative_prompt,
num_inference_steps=steps,
width=width,
).images[0]
image.save(output)
print('saved inpaint output: %s' % (output))
2023-01-05 01:42:37 +00:00
# setup
def check_paths():
if not path.exists(model_path):
raise RuntimeError('model path must exist')
if not path.exists(output_path):
makedirs(output_path)
def load_models():
global available_models
available_models = [f.name for f in scandir(model_path) if f.is_dir()]
def load_params():
global config_params
with open(params_path) as f:
config_params = json.load(f)
check_paths()
load_models()
load_params()
2023-01-05 01:42:37 +00:00
app = Flask(__name__)
app.config['EXECUTOR_MAX_WORKERS'] = num_workers
2023-01-14 20:20:42 +00:00
app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True
2023-01-14 16:18:53 +00:00
CORS(app, origins=cors_origin)
executor = Executor(app)
2023-01-05 00:25:00 +00:00
2023-01-05 01:42:37 +00:00
# routes
2023-01-05 00:25:00 +00:00
@app.route('/')
def index():
return serve_bundle_file()
2023-01-13 04:10:46 +00:00
@app.route('/<path:filename>')
def index_path(filename):
return serve_bundle_file(filename)
2023-01-13 04:10:46 +00:00
@app.route('/api')
def introspect():
return {
'name': 'onnx-web',
'routes': [{
'path': url_from_rule(rule),
'methods': list(rule.methods).sort()
} for rule in app.url_map.iter_rules()]
}
2023-01-05 00:25:00 +00:00
2023-01-13 04:10:46 +00:00
@app.route('/api/settings/models')
2023-01-06 04:01:58 +00:00
def list_models():
2023-01-14 16:18:53 +00:00
return jsonify(available_models)
2023-01-06 04:01:58 +00:00
2023-01-13 04:10:46 +00:00
@app.route('/api/settings/params')
def list_params():
2023-01-14 16:18:53 +00:00
return jsonify(config_params)
2023-01-13 04:10:46 +00:00
@app.route('/api/settings/platforms')
def list_platforms():
2023-01-14 16:18:53 +00:00
return jsonify(list(platform_providers.keys()))
2023-01-13 04:10:46 +00:00
@app.route('/api/settings/schedulers')
def list_schedulers():
2023-01-14 16:18:53 +00:00
return jsonify(list(pipeline_schedulers.keys()))
2023-01-13 04:10:46 +00:00
@app.route('/api/img2img', methods=['POST'])
2023-01-07 21:05:29 +00:00
def img2img():
input_file = request.files.get('source')
input_image = Image.open(BytesIO(input_file.read())).convert('RGB')
2023-01-08 15:17:23 +00:00
strength = get_and_clamp_float(request.args, 'strength', 0.5, 1.0)
2023-01-07 21:19:24 +00:00
2023-01-08 19:05:02 +00:00
(model, provider, scheduler, prompt, negative_prompt, cfg, steps, height,
width, seed) = pipeline_from_request()
2023-01-07 21:05:29 +00:00
(output_file, output_full) = make_output_path('img2img', seed,
(prompt, cfg, negative_prompt, steps, strength, height, width))
print("img2img output: %s" % (output_full))
input_image.thumbnail((width, height))
executor.submit_stored(output_file, run_img2img_pipeline, model, provider,
scheduler, prompt, negative_prompt, cfg, steps, seed, output_full, strength, input_image)
2023-01-07 21:05:29 +00:00
2023-01-14 16:18:53 +00:00
return jsonify({
2023-01-07 21:05:29 +00:00
'output': output_file,
'params': {
'model': model,
'provider': provider,
'scheduler': scheduler.__name__,
'seed': seed,
'prompt': prompt,
2023-01-07 21:05:29 +00:00
'cfg': cfg,
'negativePrompt': negative_prompt,
2023-01-07 21:05:29 +00:00
'steps': steps,
'height': height,
'width': width,
2023-01-07 21:05:29 +00:00
}
})
2023-01-13 04:10:46 +00:00
@app.route('/api/txt2img', methods=['POST'])
2023-01-07 21:05:29 +00:00
def txt2img():
2023-01-08 19:05:02 +00:00
(model, provider, scheduler, prompt, negative_prompt, cfg, steps, height,
width, seed) = pipeline_from_request()
(output_file, output_full) = make_output_path('txt2img',
seed, (prompt, cfg, negative_prompt, steps, height, width))
print("txt2img output: %s" % (output_full))
executor.submit_stored(output_file, run_txt2img_pipeline, model,
provider, scheduler, prompt, negative_prompt, cfg, steps, seed, output_full, height, width)
2023-01-14 16:18:53 +00:00
return jsonify({
'output': output_file,
'params': {
'model': model,
'provider': provider,
'scheduler': scheduler.__name__,
'seed': seed,
'prompt': prompt,
'cfg': cfg,
'negativePrompt': negative_prompt,
'steps': steps,
'height': height,
'width': width,
}
})
2023-01-13 04:10:46 +00:00
@app.route('/api/inpaint', methods=['POST'])
2023-01-09 00:11:34 +00:00
def inpaint():
source_file = request.files.get('source')
source_image = Image.open(BytesIO(source_file.read())).convert('RGB')
mask_file = request.files.get('mask')
mask_image = Image.open(BytesIO(mask_file.read())).convert('RGB')
(model, provider, scheduler, prompt, negative_prompt, cfg, steps, height,
width, seed) = pipeline_from_request()
2023-01-09 00:11:34 +00:00
(output_file, output_full) = make_output_path(
'inpaint', seed, (prompt, cfg, steps, height, width, seed))
2023-01-09 00:11:34 +00:00
print("inpaint output: %s" % output_full)
source_image.thumbnail((width, height))
mask_image.thumbnail((width, height))
executor.submit_stored(output_file, run_inpaint_pipeline, model, provider, scheduler, prompt, negative_prompt,
cfg, steps, seed, output_full, height, width, source_image, mask_image)
2023-01-09 00:11:34 +00:00
2023-01-14 16:18:53 +00:00
return jsonify({
2023-01-09 00:11:34 +00:00
'output': output_file,
'params': {
'model': model,
'provider': provider,
'scheduler': scheduler.__name__,
'seed': seed,
'prompt': prompt,
2023-01-09 00:11:34 +00:00
'cfg': cfg,
'negativePrompt': negative_prompt,
2023-01-09 00:11:34 +00:00
'steps': steps,
'height': height,
'width': width,
2023-01-09 00:11:34 +00:00
}
})
2023-01-13 04:10:46 +00:00
@app.route('/api/ready')
def ready():
output_file = request.args.get('output', None)
2023-01-14 16:18:53 +00:00
return jsonify({
'ready': executor.futures.done(output_file),
})
2023-01-13 04:10:46 +00:00
@app.route('/api/output/<path:filename>')
def output(filename: str):
return send_from_directory(path.join('..', output_path), filename, as_attachment=False)