Standardize configurable Comfy pipeline
Browse files- API.md +23 -2
- app.py +196 -251
- requirements.txt +0 -1
- workflow_api.py +82 -28
API.md
CHANGED
|
@@ -16,7 +16,28 @@ Returns `{"status":true}` when the application is ready.
|
|
| 16 |
POST /api/generate?p=PASSWORD
|
| 17 |
Content-Type: application/json
|
| 18 |
|
| 19 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
```
|
| 21 |
|
| 22 |
-
Returns the generated PNG at 50% of its full dimensions with content type `image/png`. Add `&res=full` to return the full resolution. Only `prompt` is required
|
|
|
|
| 16 |
POST /api/generate?p=PASSWORD
|
| 17 |
Content-Type: application/json
|
| 18 |
|
| 19 |
+
{
|
| 20 |
+
"prompt": "an orange cat",
|
| 21 |
+
"negative": "text, watermark",
|
| 22 |
+
"width": 1152,
|
| 23 |
+
"height": 896,
|
| 24 |
+
"model": "52_novaAnimeXL_ilV190.safetensors",
|
| 25 |
+
"loras": [{"name": "8_bikabaka.safetensors", "strength": 1, "clip": 0}],
|
| 26 |
+
"sampler": "euler_ancestral",
|
| 27 |
+
"scheduler": "karras",
|
| 28 |
+
"steps": 16,
|
| 29 |
+
"cfg": 4,
|
| 30 |
+
"upscale": true,
|
| 31 |
+
"upscale_method": "bislerp",
|
| 32 |
+
"upscale_scale": 1.1,
|
| 33 |
+
"second_model": "",
|
| 34 |
+
"second_loras": [],
|
| 35 |
+
"second_sampler": "euler",
|
| 36 |
+
"second_scheduler": "karras",
|
| 37 |
+
"second_steps": 18,
|
| 38 |
+
"second_cfg": 5,
|
| 39 |
+
"denoise": 0.5
|
| 40 |
+
}
|
| 41 |
```
|
| 42 |
|
| 43 |
+
Returns the generated PNG at 50% of its full dimensions with content type `image/png`. Add `&res=full` to return the full resolution. Only `prompt` is required for the built-in site defaults. LoRAs run in array order. Each request creates a random first-pass seed and, when `upscale` is true, performs latent upscaling followed by a second sampler with another random seed. Leave `second_model` empty to reuse the first model and LoRA chain; otherwise `second_loras` applies to the selected second model. Width and height must be multiples of 8 from 64 through 2048; steps must be from 1 through 100.
|
app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import math
|
| 2 |
import os
|
| 3 |
import random
|
| 4 |
import re
|
|
@@ -12,7 +11,6 @@ import warnings
|
|
| 12 |
from asyncio.base_events import BaseEventLoop
|
| 13 |
from concurrent.futures import ThreadPoolExecutor
|
| 14 |
from datetime import datetime
|
| 15 |
-
from io import BytesIO
|
| 16 |
from pathlib import Path
|
| 17 |
from zoneinfo import ZoneInfo
|
| 18 |
|
|
@@ -34,8 +32,6 @@ import gradio as gr
|
|
| 34 |
import numpy as np
|
| 35 |
import spaces
|
| 36 |
import torch
|
| 37 |
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
| 38 |
-
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
| 39 |
from fastapi import Depends, HTTPException, Query
|
| 40 |
from fastapi.middleware.cors import CORSMiddleware
|
| 41 |
from fastapi.responses import FileResponse
|
|
@@ -56,10 +52,6 @@ OUTPUT_DIR = DATA_DIR / "output"
|
|
| 56 |
TIMEZONE = ZoneInfo("Asia/Singapore")
|
| 57 |
MIB = 1024 * 1024
|
| 58 |
COPY_SIZE = 16 * MIB
|
| 59 |
-
SALT_SIZE = 16
|
| 60 |
-
NONCE_SIZE = 12
|
| 61 |
-
SCRYPT_N = 2**14
|
| 62 |
-
FILE_MAGIC = b"EPNG1"
|
| 63 |
RETURN_SCALE = .5
|
| 64 |
PORT = int(os.environ.get("PORT", "7860"))
|
| 65 |
LOCAL_URL = os.environ.get("LOCAL_URL", f"http://127.0.0.1:{PORT}")
|
|
@@ -94,11 +86,6 @@ ASSETS = {
|
|
| 94 |
"vae",
|
| 95 |
"vae/3_sdxlVAE_sdxlVAE.safetensors",
|
| 96 |
),
|
| 97 |
-
"upscale": (
|
| 98 |
-
"1x-Archivist_Soft.pth",
|
| 99 |
-
"upscale_models",
|
| 100 |
-
"upscale_models/1x-Archivist_Soft.pth",
|
| 101 |
-
),
|
| 102 |
}
|
| 103 |
|
| 104 |
DEFAULT_NEGATIVE = (
|
|
@@ -110,6 +97,19 @@ DEFAULT_NEGATIVE = (
|
|
| 110 |
DEFAULT_SAMPLER = "euler_ancestral"
|
| 111 |
DEFAULT_SCHEDULER = "karras"
|
| 112 |
DEFAULT_CFG = 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
GPU_ABORT_ERROR = "GPU task aborted"
|
| 114 |
|
| 115 |
paths = {}
|
|
@@ -207,6 +207,42 @@ def stage_assets():
|
|
| 207 |
log(f"Copied {target.name}: {size // MIB} MiB")
|
| 208 |
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
def ensure_comfy():
|
| 211 |
if COMFYUI_PATH.is_dir():
|
| 212 |
log(f"Found ComfyUI at {COMFYUI_PATH}")
|
|
@@ -242,90 +278,44 @@ def init_comfy():
|
|
| 242 |
|
| 243 |
args.cpu_vae = False
|
| 244 |
|
| 245 |
-
from comfy_extras.nodes_upscale_model import (
|
| 246 |
-
ImageUpscaleWithModel,
|
| 247 |
-
UpscaleModelLoader,
|
| 248 |
-
)
|
| 249 |
from nodes import (
|
|
|
|
| 250 |
CheckpointLoaderSimple,
|
| 251 |
EmptyLatentImage,
|
| 252 |
KSampler,
|
|
|
|
| 253 |
LoraLoader,
|
| 254 |
VAEDecode,
|
| 255 |
VAELoader,
|
| 256 |
)
|
| 257 |
|
|
|
|
|
|
|
|
|
|
| 258 |
for key, path in paths.items():
|
| 259 |
kind = ASSETS[key][1]
|
| 260 |
folder_paths.add_model_folder_path(kind, str(path.parent))
|
| 261 |
|
| 262 |
-
log(f"Loading checkpoint {paths['checkpoint'].name}")
|
| 263 |
-
checkpoint = CheckpointLoaderSimple().load_checkpoint(
|
| 264 |
-
ckpt_name=paths["checkpoint"].name
|
| 265 |
-
)
|
| 266 |
-
|
| 267 |
-
loader = LoraLoader()
|
| 268 |
-
|
| 269 |
-
log(f"Loading LoRA {paths['bikabaka'].name}")
|
| 270 |
-
first = loader.load_lora(
|
| 271 |
-
model=checkpoint[0],
|
| 272 |
-
clip=checkpoint[1],
|
| 273 |
-
lora_name=paths["bikabaka"].name,
|
| 274 |
-
strength_model=.5,
|
| 275 |
-
strength_clip=0,
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
log(f"Loading LoRA {paths['5cm'].name}")
|
| 279 |
-
second = loader.load_lora(
|
| 280 |
-
model=first[0],
|
| 281 |
-
clip=first[1],
|
| 282 |
-
lora_name=paths["5cm"].name,
|
| 283 |
-
strength_model=1.2,
|
| 284 |
-
strength_clip=0,
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
log(f"Loading LoRA {paths['saturation'].name}")
|
| 288 |
-
final = loader.load_lora(
|
| 289 |
-
model=second[0],
|
| 290 |
-
clip=second[1],
|
| 291 |
-
lora_name=paths["saturation"].name,
|
| 292 |
-
strength_model=-1.4,
|
| 293 |
-
strength_clip=0,
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
log(f"Loading VAE {paths['vae'].name}")
|
| 297 |
-
log(f"Loading upscale model {paths['upscale'].name}")
|
| 298 |
|
| 299 |
state.update(
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
vae=VAELoader().load_vae(
|
| 304 |
vae_name=paths["vae"].name
|
| 305 |
)[0],
|
| 306 |
latent=EmptyLatentImage(),
|
| 307 |
sample=KSampler(),
|
| 308 |
decode=VAEDecode(),
|
| 309 |
-
|
| 310 |
-
paths["upscale"].name
|
| 311 |
-
)[0],
|
| 312 |
-
upscale=ImageUpscaleWithModel(),
|
| 313 |
)
|
| 314 |
|
| 315 |
log("ComfyUI initialization complete")
|
| 316 |
|
| 317 |
|
| 318 |
-
@spaces.GPU(duration=
|
| 319 |
-
def generate_core(
|
| 320 |
-
steps,
|
| 321 |
-
positive,
|
| 322 |
-
negative,
|
| 323 |
-
latent,
|
| 324 |
-
seed,
|
| 325 |
-
sampler,
|
| 326 |
-
scheduler,
|
| 327 |
-
cfg,
|
| 328 |
-
):
|
| 329 |
with torch.inference_mode(), warnings.catch_warnings():
|
| 330 |
warnings.filterwarnings(
|
| 331 |
"ignore",
|
|
@@ -334,90 +324,105 @@ def generate_core(
|
|
| 334 |
module=r"torchsde\._brownian\.brownian_interval",
|
| 335 |
)
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
samples = state["sample"].sample(
|
| 338 |
-
model=
|
| 339 |
-
seed=
|
| 340 |
-
steps=steps,
|
| 341 |
-
cfg=cfg,
|
| 342 |
-
sampler_name=sampler,
|
| 343 |
-
scheduler=scheduler,
|
| 344 |
-
positive=
|
| 345 |
-
negative=
|
| 346 |
latent_image=latent,
|
| 347 |
denoise=1,
|
| 348 |
)[0]
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
image = state["decode"].decode(
|
| 351 |
vae=state["vae"],
|
| 352 |
samples=samples,
|
| 353 |
)[0]
|
|
|
|
| 354 |
|
| 355 |
-
return state["upscale"].upscale(
|
| 356 |
-
state["upscale_model"],
|
| 357 |
-
image,
|
| 358 |
-
)[0][0]
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
scheduler=DEFAULT_SCHEDULER,
|
| 369 |
-
cfg=DEFAULT_CFG,
|
| 370 |
-
tag="ST",
|
| 371 |
-
):
|
| 372 |
-
width = int(width)
|
| 373 |
-
height = int(height)
|
| 374 |
-
steps = int(steps)
|
| 375 |
-
cfg = float(cfg)
|
| 376 |
-
|
| 377 |
-
if (
|
| 378 |
-
not 64 <= width <= 2048
|
| 379 |
-
or width % 8
|
| 380 |
-
or not 64 <= height <= 2048
|
| 381 |
-
or height % 8
|
| 382 |
-
):
|
| 383 |
-
raise ValueError(
|
| 384 |
-
"Width and height must be multiples of 8 from 64 to 2048"
|
| 385 |
)
|
| 386 |
|
| 387 |
-
|
| 388 |
-
raise ValueError("Steps must be from 1 to 100")
|
| 389 |
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
with lock:
|
| 394 |
init_comfy()
|
| 395 |
|
| 396 |
options = state["sample"].INPUT_TYPES()["required"]
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
raise ValueError("Unsupported sampler or scheduler")
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
latent = state["latent"].generate(
|
| 406 |
-
width=width,
|
| 407 |
-
height=height,
|
| 408 |
-
batch_size=1,
|
| 409 |
-
)[0]
|
| 410 |
-
seed = random.randrange(2**64)
|
| 411 |
-
|
| 412 |
-
image = generate_core(
|
| 413 |
-
steps,
|
| 414 |
-
positive,
|
| 415 |
-
negative_tokens,
|
| 416 |
-
latent,
|
| 417 |
-
seed,
|
| 418 |
-
sampler,
|
| 419 |
-
scheduler,
|
| 420 |
-
cfg,
|
| 421 |
)
|
| 422 |
|
| 423 |
result = Image.fromarray(
|
|
@@ -430,15 +435,9 @@ def generate_image(
|
|
| 430 |
|
| 431 |
result.info.update(
|
| 432 |
image_metadata(
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
steps,
|
| 437 |
-
seed,
|
| 438 |
-
negative,
|
| 439 |
-
sampler,
|
| 440 |
-
scheduler,
|
| 441 |
-
cfg,
|
| 442 |
)
|
| 443 |
)
|
| 444 |
|
|
@@ -450,38 +449,22 @@ def archive_image(image, tag):
|
|
| 450 |
try:
|
| 451 |
path = DATA_DIR / "img" / datetime.now(TIMEZONE).date().isoformat()
|
| 452 |
path.mkdir(parents=True, exist_ok=True)
|
| 453 |
-
data = BytesIO()
|
| 454 |
pnginfo = PngInfo()
|
| 455 |
|
| 456 |
-
for key in (
|
| 457 |
-
pnginfo.add_text(key,
|
| 458 |
|
| 459 |
-
image.save(
|
| 460 |
-
data,
|
| 461 |
-
format="PNG",
|
| 462 |
-
pnginfo=pnginfo,
|
| 463 |
-
)
|
| 464 |
-
salt = os.urandom(SALT_SIZE)
|
| 465 |
-
nonce = os.urandom(NONCE_SIZE)
|
| 466 |
-
key = Scrypt(
|
| 467 |
-
salt=salt,
|
| 468 |
-
length=32,
|
| 469 |
-
n=SCRYPT_N,
|
| 470 |
-
r=8,
|
| 471 |
-
p=1,
|
| 472 |
-
).derive(PASSWORD.encode())
|
| 473 |
-
encrypted = AESGCM(key).encrypt(nonce, data.getvalue(), FILE_MAGIC)
|
| 474 |
number = (
|
| 475 |
sum(
|
| 476 |
-
file.suffix.casefold() == ".
|
| 477 |
for file in path.iterdir()
|
| 478 |
)
|
| 479 |
+ 1
|
| 480 |
)
|
| 481 |
-
name = f"{number}{tag}.
|
| 482 |
output = path / name
|
| 483 |
-
temp = output.with_suffix(".
|
| 484 |
-
|
| 485 |
temp.replace(output)
|
| 486 |
log(f"Saved img/{path.name}/{name}")
|
| 487 |
except Exception as error:
|
|
@@ -506,68 +489,33 @@ def generate(
|
|
| 506 |
if not valid_pass(p):
|
| 507 |
raise gr.Error("Invalid password")
|
| 508 |
|
| 509 |
-
|
| 510 |
-
prompt,
|
| 511 |
-
width,
|
| 512 |
-
height,
|
| 513 |
-
steps,
|
| 514 |
-
negative,
|
| 515 |
-
sampler,
|
| 516 |
-
scheduler,
|
| 517 |
-
cfg,
|
| 518 |
-
"GR",
|
| 519 |
)
|
|
|
|
| 520 |
|
| 521 |
|
| 522 |
-
def api_generate(
|
| 523 |
-
prompt,
|
| 524 |
-
width=1152,
|
| 525 |
-
height=896,
|
| 526 |
-
steps=16,
|
| 527 |
-
negative=DEFAULT_NEGATIVE,
|
| 528 |
-
sampler=DEFAULT_SAMPLER,
|
| 529 |
-
scheduler=DEFAULT_SCHEDULER,
|
| 530 |
-
cfg=DEFAULT_CFG,
|
| 531 |
-
p="",
|
| 532 |
-
):
|
| 533 |
if not valid_pass(p):
|
| 534 |
raise gr.Error("Invalid password")
|
| 535 |
|
| 536 |
-
return generate_image(
|
| 537 |
-
prompt,
|
| 538 |
-
width,
|
| 539 |
-
height,
|
| 540 |
-
steps,
|
| 541 |
-
negative,
|
| 542 |
-
sampler,
|
| 543 |
-
scheduler,
|
| 544 |
-
cfg,
|
| 545 |
-
)
|
| 546 |
|
| 547 |
|
| 548 |
-
def queued_generate(
|
| 549 |
-
prompt,
|
| 550 |
-
width,
|
| 551 |
-
height,
|
| 552 |
-
steps,
|
| 553 |
-
negative=DEFAULT_NEGATIVE,
|
| 554 |
-
sampler=DEFAULT_SAMPLER,
|
| 555 |
-
scheduler=DEFAULT_SCHEDULER,
|
| 556 |
-
cfg=DEFAULT_CFG,
|
| 557 |
-
):
|
| 558 |
client = Client(LOCAL_URL, verbose=False)
|
| 559 |
|
| 560 |
for attempt in range(2):
|
| 561 |
try:
|
| 562 |
result = client.predict(
|
| 563 |
-
|
| 564 |
-
width,
|
| 565 |
-
height,
|
| 566 |
-
steps,
|
| 567 |
-
negative,
|
| 568 |
-
sampler,
|
| 569 |
-
scheduler,
|
| 570 |
-
cfg,
|
| 571 |
PASSWORD,
|
| 572 |
api_name="/generate",
|
| 573 |
)
|
|
@@ -654,14 +602,16 @@ def run_job(job_id, workflow):
|
|
| 654 |
) = workflow_values(workflow)
|
| 655 |
|
| 656 |
image = queued_generate(
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
| 665 |
)
|
| 666 |
|
| 667 |
filename = f"{job_id}.png"
|
|
@@ -706,17 +656,6 @@ def run_job(job_id, workflow):
|
|
| 706 |
}
|
| 707 |
|
| 708 |
|
| 709 |
-
class DirectRequest(BaseModel):
|
| 710 |
-
prompt: str
|
| 711 |
-
width: int = Field(1152, ge=64, le=2048)
|
| 712 |
-
height: int = Field(896, ge=64, le=2048)
|
| 713 |
-
steps: int = Field(16, ge=1, le=100)
|
| 714 |
-
negative: str = DEFAULT_NEGATIVE
|
| 715 |
-
sampler: str = DEFAULT_SAMPLER
|
| 716 |
-
scheduler: str = DEFAULT_SCHEDULER
|
| 717 |
-
cfg: float = Field(DEFAULT_CFG, ge=0, le=100)
|
| 718 |
-
|
| 719 |
-
|
| 720 |
api = App()
|
| 721 |
api.add_middleware(
|
| 722 |
CORSMiddleware,
|
|
@@ -755,8 +694,11 @@ def system_stats():
|
|
| 755 |
dependencies=[Depends(require_pass)],
|
| 756 |
)
|
| 757 |
def object_info():
|
| 758 |
-
|
| 759 |
options = state["sample"].INPUT_TYPES()["required"]
|
|
|
|
|
|
|
|
|
|
| 760 |
|
| 761 |
return {
|
| 762 |
"KSampler": {
|
|
@@ -775,7 +717,25 @@ def object_info():
|
|
| 775 |
"input": {
|
| 776 |
"required": {
|
| 777 |
"ckpt_name": [
|
| 778 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
]
|
| 780 |
}
|
| 781 |
}
|
|
@@ -871,16 +831,7 @@ def direct(
|
|
| 871 |
res: str = Query(""),
|
| 872 |
):
|
| 873 |
try:
|
| 874 |
-
image = queued_generate(
|
| 875 |
-
body.prompt,
|
| 876 |
-
body.width,
|
| 877 |
-
body.height,
|
| 878 |
-
body.steps,
|
| 879 |
-
body.negative,
|
| 880 |
-
body.sampler,
|
| 881 |
-
body.scheduler,
|
| 882 |
-
body.cfg,
|
| 883 |
-
)
|
| 884 |
except ValueError as error:
|
| 885 |
raise HTTPException(422, str(error)) from error
|
| 886 |
|
|
@@ -967,6 +918,7 @@ with gr.Blocks(title="Image API") as demo:
|
|
| 967 |
|
| 968 |
api_button = gr.Button(visible=False)
|
| 969 |
api_pass_input = gr.Textbox(visible=False)
|
|
|
|
| 970 |
negative_input = gr.Textbox(
|
| 971 |
DEFAULT_NEGATIVE,
|
| 972 |
visible=False,
|
|
@@ -1013,14 +965,7 @@ with gr.Blocks(title="Image API") as demo:
|
|
| 1013 |
api_button.click(
|
| 1014 |
api_generate,
|
| 1015 |
[
|
| 1016 |
-
|
| 1017 |
-
width_input,
|
| 1018 |
-
height_input,
|
| 1019 |
-
steps_input,
|
| 1020 |
-
negative_input,
|
| 1021 |
-
sampler_input,
|
| 1022 |
-
scheduler_input,
|
| 1023 |
-
cfg_input,
|
| 1024 |
api_pass_input,
|
| 1025 |
],
|
| 1026 |
output,
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import random
|
| 3 |
import re
|
|
|
|
| 11 |
from asyncio.base_events import BaseEventLoop
|
| 12 |
from concurrent.futures import ThreadPoolExecutor
|
| 13 |
from datetime import datetime
|
|
|
|
| 14 |
from pathlib import Path
|
| 15 |
from zoneinfo import ZoneInfo
|
| 16 |
|
|
|
|
| 32 |
import numpy as np
|
| 33 |
import spaces
|
| 34 |
import torch
|
|
|
|
|
|
|
| 35 |
from fastapi import Depends, HTTPException, Query
|
| 36 |
from fastapi.middleware.cors import CORSMiddleware
|
| 37 |
from fastapi.responses import FileResponse
|
|
|
|
| 52 |
TIMEZONE = ZoneInfo("Asia/Singapore")
|
| 53 |
MIB = 1024 * 1024
|
| 54 |
COPY_SIZE = 16 * MIB
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
RETURN_SCALE = .5
|
| 56 |
PORT = int(os.environ.get("PORT", "7860"))
|
| 57 |
LOCAL_URL = os.environ.get("LOCAL_URL", f"http://127.0.0.1:{PORT}")
|
|
|
|
| 86 |
"vae",
|
| 87 |
"vae/3_sdxlVAE_sdxlVAE.safetensors",
|
| 88 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
DEFAULT_NEGATIVE = (
|
|
|
|
| 97 |
DEFAULT_SAMPLER = "euler_ancestral"
|
| 98 |
DEFAULT_SCHEDULER = "karras"
|
| 99 |
DEFAULT_CFG = 4
|
| 100 |
+
DEFAULT_MODEL = ASSETS["checkpoint"][0]
|
| 101 |
+
DEFAULT_LORAS = (
|
| 102 |
+
(ASSETS["bikabaka"][0], .5, 0),
|
| 103 |
+
(ASSETS["5cm"][0], 1.2, 0),
|
| 104 |
+
(ASSETS["saturation"][0], -1.4, 0),
|
| 105 |
+
)
|
| 106 |
+
DEFAULT_UPSCALE_METHOD = "bislerp"
|
| 107 |
+
DEFAULT_UPSCALE_SCALE = 1.1
|
| 108 |
+
DEFAULT_SECOND_SAMPLER = "euler"
|
| 109 |
+
DEFAULT_SECOND_SCHEDULER = "karras"
|
| 110 |
+
DEFAULT_SECOND_STEPS = 18
|
| 111 |
+
DEFAULT_SECOND_CFG = 5
|
| 112 |
+
DEFAULT_DENOISE = .5
|
| 113 |
GPU_ABORT_ERROR = "GPU task aborted"
|
| 114 |
|
| 115 |
paths = {}
|
|
|
|
| 207 |
log(f"Copied {target.name}: {size // MIB} MiB")
|
| 208 |
|
| 209 |
|
| 210 |
+
class LoraRequest(BaseModel):
|
| 211 |
+
name: str
|
| 212 |
+
strength: float = 1
|
| 213 |
+
clip: float = 0
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def default_loras():
|
| 217 |
+
return [
|
| 218 |
+
LoraRequest(name=name, strength=strength, clip=clip)
|
| 219 |
+
for name, strength, clip in DEFAULT_LORAS
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class DirectRequest(BaseModel):
|
| 224 |
+
prompt: str
|
| 225 |
+
negative: str = DEFAULT_NEGATIVE
|
| 226 |
+
width: int = Field(1152, ge=64, le=2048)
|
| 227 |
+
height: int = Field(896, ge=64, le=2048)
|
| 228 |
+
model: str = DEFAULT_MODEL
|
| 229 |
+
loras: list[LoraRequest] = Field(default_factory=default_loras)
|
| 230 |
+
sampler: str = DEFAULT_SAMPLER
|
| 231 |
+
scheduler: str = DEFAULT_SCHEDULER
|
| 232 |
+
steps: int = Field(16, ge=1, le=100)
|
| 233 |
+
cfg: float = Field(DEFAULT_CFG, ge=0, le=100)
|
| 234 |
+
upscale: bool = False
|
| 235 |
+
upscale_method: str = DEFAULT_UPSCALE_METHOD
|
| 236 |
+
upscale_scale: float = Field(DEFAULT_UPSCALE_SCALE, gt=0)
|
| 237 |
+
second_model: str = ""
|
| 238 |
+
second_loras: list[LoraRequest] = Field(default_factory=list)
|
| 239 |
+
second_sampler: str = DEFAULT_SECOND_SAMPLER
|
| 240 |
+
second_scheduler: str = DEFAULT_SECOND_SCHEDULER
|
| 241 |
+
second_steps: int = Field(DEFAULT_SECOND_STEPS, ge=1, le=100)
|
| 242 |
+
second_cfg: float = Field(DEFAULT_SECOND_CFG, ge=0, le=100)
|
| 243 |
+
denoise: float = Field(DEFAULT_DENOISE, ge=0, le=1)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
def ensure_comfy():
|
| 247 |
if COMFYUI_PATH.is_dir():
|
| 248 |
log(f"Found ComfyUI at {COMFYUI_PATH}")
|
|
|
|
| 278 |
|
| 279 |
args.cpu_vae = False
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
from nodes import (
|
| 282 |
+
CLIPTextEncode,
|
| 283 |
CheckpointLoaderSimple,
|
| 284 |
EmptyLatentImage,
|
| 285 |
KSampler,
|
| 286 |
+
LatentUpscaleBy,
|
| 287 |
LoraLoader,
|
| 288 |
VAEDecode,
|
| 289 |
VAELoader,
|
| 290 |
)
|
| 291 |
|
| 292 |
+
for kind in {asset[1] for asset in ASSETS.values()}:
|
| 293 |
+
folder_paths.add_model_folder_path(kind, str(DATA_DIR / kind))
|
| 294 |
+
|
| 295 |
for key, path in paths.items():
|
| 296 |
kind = ASSETS[key][1]
|
| 297 |
folder_paths.add_model_folder_path(kind, str(path.parent))
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
log(f"Loading VAE {paths['vae'].name}")
|
|
|
|
| 300 |
|
| 301 |
state.update(
|
| 302 |
+
checkpoint=CheckpointLoaderSimple(),
|
| 303 |
+
lora=LoraLoader(),
|
| 304 |
+
encode=CLIPTextEncode(),
|
| 305 |
vae=VAELoader().load_vae(
|
| 306 |
vae_name=paths["vae"].name
|
| 307 |
)[0],
|
| 308 |
latent=EmptyLatentImage(),
|
| 309 |
sample=KSampler(),
|
| 310 |
decode=VAEDecode(),
|
| 311 |
+
upscale=LatentUpscaleBy(),
|
|
|
|
|
|
|
|
|
|
| 312 |
)
|
| 313 |
|
| 314 |
log("ComfyUI initialization complete")
|
| 315 |
|
| 316 |
|
| 317 |
+
@spaces.GPU(duration=30)
|
| 318 |
+
def generate_core(body):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
with torch.inference_mode(), warnings.catch_warnings():
|
| 320 |
warnings.filterwarnings(
|
| 321 |
"ignore",
|
|
|
|
| 324 |
module=r"torchsde\._brownian\.brownian_interval",
|
| 325 |
)
|
| 326 |
|
| 327 |
+
request = DirectRequest.model_validate_json(body)
|
| 328 |
+
model, clip = load_chain(request.model, request.loras)
|
| 329 |
+
positive = state["encode"].encode(request.prompt, clip)[0]
|
| 330 |
+
negative = state["encode"].encode(request.negative, clip)[0]
|
| 331 |
+
latent = state["latent"].generate(
|
| 332 |
+
width=request.width,
|
| 333 |
+
height=request.height,
|
| 334 |
+
batch_size=1,
|
| 335 |
+
)[0]
|
| 336 |
+
first_seed = random.randrange(2**64)
|
| 337 |
samples = state["sample"].sample(
|
| 338 |
+
model=model,
|
| 339 |
+
seed=first_seed,
|
| 340 |
+
steps=request.steps,
|
| 341 |
+
cfg=request.cfg,
|
| 342 |
+
sampler_name=request.sampler,
|
| 343 |
+
scheduler=request.scheduler,
|
| 344 |
+
positive=positive,
|
| 345 |
+
negative=negative,
|
| 346 |
latent_image=latent,
|
| 347 |
denoise=1,
|
| 348 |
)[0]
|
| 349 |
|
| 350 |
+
second_seed = None
|
| 351 |
+
if request.upscale:
|
| 352 |
+
samples = state["upscale"].upscale(
|
| 353 |
+
samples=samples,
|
| 354 |
+
upscale_method=request.upscale_method,
|
| 355 |
+
scale_by=request.upscale_scale,
|
| 356 |
+
)[0]
|
| 357 |
+
if request.second_model:
|
| 358 |
+
model, clip = load_chain(
|
| 359 |
+
request.second_model,
|
| 360 |
+
request.second_loras,
|
| 361 |
+
)
|
| 362 |
+
positive = state["encode"].encode(request.prompt, clip)[0]
|
| 363 |
+
negative = state["encode"].encode(request.negative, clip)[0]
|
| 364 |
+
|
| 365 |
+
second_seed = random.randrange(2**64)
|
| 366 |
+
samples = state["sample"].sample(
|
| 367 |
+
model=model,
|
| 368 |
+
seed=second_seed,
|
| 369 |
+
steps=request.second_steps,
|
| 370 |
+
cfg=request.second_cfg,
|
| 371 |
+
sampler_name=request.second_sampler,
|
| 372 |
+
scheduler=request.second_scheduler,
|
| 373 |
+
positive=positive,
|
| 374 |
+
negative=negative,
|
| 375 |
+
latent_image=samples,
|
| 376 |
+
denoise=request.denoise,
|
| 377 |
+
)[0]
|
| 378 |
+
|
| 379 |
image = state["decode"].decode(
|
| 380 |
vae=state["vae"],
|
| 381 |
samples=samples,
|
| 382 |
)[0]
|
| 383 |
+
return image[0], first_seed, second_seed
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
+
def load_chain(model_name, loras):
|
| 387 |
+
model, clip = state["checkpoint"].load_checkpoint(
|
| 388 |
+
ckpt_name=model_name
|
| 389 |
+
)[:2]
|
| 390 |
|
| 391 |
+
for lora in loras:
|
| 392 |
+
model, clip = state["lora"].load_lora(
|
| 393 |
+
model=model,
|
| 394 |
+
clip=clip,
|
| 395 |
+
lora_name=lora.name,
|
| 396 |
+
strength_model=lora.strength,
|
| 397 |
+
strength_clip=lora.clip,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
)
|
| 399 |
|
| 400 |
+
return model, clip
|
|
|
|
| 401 |
|
| 402 |
+
|
| 403 |
+
def generate_image(request, tag="ST"):
|
| 404 |
+
if (
|
| 405 |
+
request.width % 8
|
| 406 |
+
or request.height % 8
|
| 407 |
+
):
|
| 408 |
+
raise ValueError("Width and height must be multiples of 8")
|
| 409 |
|
| 410 |
with lock:
|
| 411 |
init_comfy()
|
| 412 |
|
| 413 |
options = state["sample"].INPUT_TYPES()["required"]
|
| 414 |
+
samplers = [request.sampler]
|
| 415 |
+
schedulers = [request.scheduler]
|
| 416 |
+
if request.upscale:
|
| 417 |
+
samplers.append(request.second_sampler)
|
| 418 |
+
schedulers.append(request.second_scheduler)
|
| 419 |
+
if any(value not in options["sampler_name"][0] for value in samplers):
|
| 420 |
+
raise ValueError("Unsupported sampler or scheduler")
|
| 421 |
+
if any(value not in options["scheduler"][0] for value in schedulers):
|
| 422 |
raise ValueError("Unsupported sampler or scheduler")
|
| 423 |
|
| 424 |
+
image, first_seed, second_seed = generate_core(
|
| 425 |
+
request.model_dump_json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
)
|
| 427 |
|
| 428 |
result = Image.fromarray(
|
|
|
|
| 435 |
|
| 436 |
result.info.update(
|
| 437 |
image_metadata(
|
| 438 |
+
request.model_dump(),
|
| 439 |
+
[first_seed, second_seed],
|
| 440 |
+
paths["vae"].name,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
)
|
| 442 |
)
|
| 443 |
|
|
|
|
| 449 |
try:
|
| 450 |
path = DATA_DIR / "img" / datetime.now(TIMEZONE).date().isoformat()
|
| 451 |
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 452 |
pnginfo = PngInfo()
|
| 453 |
|
| 454 |
+
for key, value in image.info.items():
|
| 455 |
+
pnginfo.add_text(key, value)
|
| 456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
number = (
|
| 458 |
sum(
|
| 459 |
+
file.suffix.casefold() == ".png"
|
| 460 |
for file in path.iterdir()
|
| 461 |
)
|
| 462 |
+ 1
|
| 463 |
)
|
| 464 |
+
name = f"{number}_{tag}.png"
|
| 465 |
output = path / name
|
| 466 |
+
temp = output.with_suffix(".png.part")
|
| 467 |
+
image.save(temp, format="PNG", pnginfo=pnginfo)
|
| 468 |
temp.replace(output)
|
| 469 |
log(f"Saved img/{path.name}/{name}")
|
| 470 |
except Exception as error:
|
|
|
|
| 489 |
if not valid_pass(p):
|
| 490 |
raise gr.Error("Invalid password")
|
| 491 |
|
| 492 |
+
request = DirectRequest(
|
| 493 |
+
prompt=prompt,
|
| 494 |
+
width=width,
|
| 495 |
+
height=height,
|
| 496 |
+
steps=steps,
|
| 497 |
+
negative=negative,
|
| 498 |
+
sampler=sampler,
|
| 499 |
+
scheduler=scheduler,
|
| 500 |
+
cfg=cfg,
|
|
|
|
| 501 |
)
|
| 502 |
+
return generate_image(request, "GR")
|
| 503 |
|
| 504 |
|
| 505 |
+
def api_generate(body, p=""):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
if not valid_pass(p):
|
| 507 |
raise gr.Error("Invalid password")
|
| 508 |
|
| 509 |
+
return generate_image(DirectRequest.model_validate_json(body))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
|
| 512 |
+
def queued_generate(request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
client = Client(LOCAL_URL, verbose=False)
|
| 514 |
|
| 515 |
for attempt in range(2):
|
| 516 |
try:
|
| 517 |
result = client.predict(
|
| 518 |
+
request.model_dump_json(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
PASSWORD,
|
| 520 |
api_name="/generate",
|
| 521 |
)
|
|
|
|
| 602 |
) = workflow_values(workflow)
|
| 603 |
|
| 604 |
image = queued_generate(
|
| 605 |
+
DirectRequest(
|
| 606 |
+
prompt=prompt,
|
| 607 |
+
width=width,
|
| 608 |
+
height=height,
|
| 609 |
+
steps=steps,
|
| 610 |
+
negative=negative,
|
| 611 |
+
sampler=sampler,
|
| 612 |
+
scheduler=scheduler,
|
| 613 |
+
cfg=cfg,
|
| 614 |
+
)
|
| 615 |
)
|
| 616 |
|
| 617 |
filename = f"{job_id}.png"
|
|
|
|
| 656 |
}
|
| 657 |
|
| 658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
api = App()
|
| 660 |
api.add_middleware(
|
| 661 |
CORSMiddleware,
|
|
|
|
| 694 |
dependencies=[Depends(require_pass)],
|
| 695 |
)
|
| 696 |
def object_info():
|
| 697 |
+
init_comfy()
|
| 698 |
options = state["sample"].INPUT_TYPES()["required"]
|
| 699 |
+
checkpoints = state["checkpoint"].INPUT_TYPES()["required"]
|
| 700 |
+
loras = state["lora"].INPUT_TYPES()["required"]
|
| 701 |
+
upscalers = state["upscale"].INPUT_TYPES()["required"]
|
| 702 |
|
| 703 |
return {
|
| 704 |
"KSampler": {
|
|
|
|
| 717 |
"input": {
|
| 718 |
"required": {
|
| 719 |
"ckpt_name": [
|
| 720 |
+
checkpoints["ckpt_name"][0]
|
| 721 |
+
]
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
},
|
| 725 |
+
"LoraLoader": {
|
| 726 |
+
"input": {
|
| 727 |
+
"required": {
|
| 728 |
+
"lora_name": [
|
| 729 |
+
loras["lora_name"][0]
|
| 730 |
+
]
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
},
|
| 734 |
+
"LatentUpscaleBy": {
|
| 735 |
+
"input": {
|
| 736 |
+
"required": {
|
| 737 |
+
"upscale_method": [
|
| 738 |
+
upscalers["upscale_method"][0]
|
| 739 |
]
|
| 740 |
}
|
| 741 |
}
|
|
|
|
| 831 |
res: str = Query(""),
|
| 832 |
):
|
| 833 |
try:
|
| 834 |
+
image = queued_generate(body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
except ValueError as error:
|
| 836 |
raise HTTPException(422, str(error)) from error
|
| 837 |
|
|
|
|
| 918 |
|
| 919 |
api_button = gr.Button(visible=False)
|
| 920 |
api_pass_input = gr.Textbox(visible=False)
|
| 921 |
+
request_input = gr.Textbox(visible=False)
|
| 922 |
negative_input = gr.Textbox(
|
| 923 |
DEFAULT_NEGATIVE,
|
| 924 |
visible=False,
|
|
|
|
| 965 |
api_button.click(
|
| 966 |
api_generate,
|
| 967 |
[
|
| 968 |
+
request_input,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
api_pass_input,
|
| 970 |
],
|
| 971 |
output,
|
requirements.txt
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
fastapi
|
| 2 |
-
cryptography
|
| 3 |
gradio==6.20.0
|
| 4 |
huggingface_hub>=1.6
|
| 5 |
numpy
|
|
|
|
| 1 |
fastapi
|
|
|
|
| 2 |
gradio==6.20.0
|
| 3 |
huggingface_hub>=1.6
|
| 4 |
numpy
|
workflow_api.py
CHANGED
|
@@ -582,36 +582,90 @@ def build_extra_pnginfo() -> dict[str, Any] | None:
|
|
| 582 |
}
|
| 583 |
|
| 584 |
|
| 585 |
-
def image_metadata(
|
| 586 |
-
api =
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
return {
|
| 613 |
"prompt": json.dumps(api, separators=(",", ":")),
|
| 614 |
-
"
|
| 615 |
}
|
| 616 |
|
| 617 |
|
|
|
|
| 582 |
}
|
| 583 |
|
| 584 |
|
| 585 |
+
def image_metadata(config, seeds, vae):
|
| 586 |
+
api = {}
|
| 587 |
+
|
| 588 |
+
def add(class_type, inputs):
|
| 589 |
+
node_id = str(len(api) + 1)
|
| 590 |
+
api[node_id] = {"inputs": inputs, "class_type": class_type}
|
| 591 |
+
return node_id
|
| 592 |
+
|
| 593 |
+
def load_chain(model_name, loras):
|
| 594 |
+
node_id = add("CheckpointLoaderSimple", {"ckpt_name": model_name})
|
| 595 |
+
model = [node_id, 0]
|
| 596 |
+
clip = [node_id, 1]
|
| 597 |
+
for lora in loras:
|
| 598 |
+
node_id = add("LoraLoader", {
|
| 599 |
+
"lora_name": lora["name"],
|
| 600 |
+
"strength_model": lora["strength"],
|
| 601 |
+
"strength_clip": lora["clip"],
|
| 602 |
+
"model": model,
|
| 603 |
+
"clip": clip,
|
| 604 |
+
})
|
| 605 |
+
model = [node_id, 0]
|
| 606 |
+
clip = [node_id, 1]
|
| 607 |
+
return model, clip
|
| 608 |
+
|
| 609 |
+
model, clip = load_chain(config["model"], config["loras"])
|
| 610 |
+
positive = [add("CLIPTextEncode", {
|
| 611 |
+
"text": config["prompt"], "clip": clip,
|
| 612 |
+
}), 0]
|
| 613 |
+
negative = [add("CLIPTextEncode", {
|
| 614 |
+
"text": config["negative"], "clip": clip,
|
| 615 |
+
}), 0]
|
| 616 |
+
latent = [add("EmptyLatentImage", {
|
| 617 |
+
"width": config["width"],
|
| 618 |
+
"height": config["height"],
|
| 619 |
+
"batch_size": 1,
|
| 620 |
+
}), 0]
|
| 621 |
+
samples = [add("KSampler", {
|
| 622 |
+
"seed": seeds[0],
|
| 623 |
+
"steps": config["steps"],
|
| 624 |
+
"cfg": config["cfg"],
|
| 625 |
+
"sampler_name": config["sampler"],
|
| 626 |
+
"scheduler": config["scheduler"],
|
| 627 |
+
"denoise": 1,
|
| 628 |
+
"model": model,
|
| 629 |
+
"positive": positive,
|
| 630 |
+
"negative": negative,
|
| 631 |
+
"latent_image": latent,
|
| 632 |
+
}), 0]
|
| 633 |
+
|
| 634 |
+
if config["upscale"]:
|
| 635 |
+
samples = [add("LatentUpscaleBy", {
|
| 636 |
+
"upscale_method": config["upscale_method"],
|
| 637 |
+
"scale_by": config["upscale_scale"],
|
| 638 |
+
"samples": samples,
|
| 639 |
+
}), 0]
|
| 640 |
+
if config["second_model"]:
|
| 641 |
+
model, clip = load_chain(
|
| 642 |
+
config["second_model"], config["second_loras"],
|
| 643 |
+
)
|
| 644 |
+
positive = [add("CLIPTextEncode", {
|
| 645 |
+
"text": config["prompt"], "clip": clip,
|
| 646 |
+
}), 0]
|
| 647 |
+
negative = [add("CLIPTextEncode", {
|
| 648 |
+
"text": config["negative"], "clip": clip,
|
| 649 |
+
}), 0]
|
| 650 |
+
samples = [add("KSampler", {
|
| 651 |
+
"seed": seeds[1],
|
| 652 |
+
"steps": config["second_steps"],
|
| 653 |
+
"cfg": config["second_cfg"],
|
| 654 |
+
"sampler_name": config["second_sampler"],
|
| 655 |
+
"scheduler": config["second_scheduler"],
|
| 656 |
+
"denoise": config["denoise"],
|
| 657 |
+
"model": model,
|
| 658 |
+
"positive": positive,
|
| 659 |
+
"negative": negative,
|
| 660 |
+
"latent_image": samples,
|
| 661 |
+
}), 0]
|
| 662 |
+
|
| 663 |
+
vae_node = add("VAELoader", {"vae_name": vae})
|
| 664 |
+
image = add("VAEDecode", {"samples": samples, "vae": [vae_node, 0]})
|
| 665 |
+
add("PreviewImage", {"images": [image, 0]})
|
| 666 |
return {
|
| 667 |
"prompt": json.dumps(api, separators=(",", ":")),
|
| 668 |
+
"parameters": json.dumps(config, separators=(",", ":")),
|
| 669 |
}
|
| 670 |
|
| 671 |
|