Support averaged style references
Browse files- API.md +5 -2
- app.py +55 -38
- workflow_api.py +18 -4
API.md
CHANGED
|
@@ -26,7 +26,10 @@ Content-Type: application/json
|
|
| 26 |
|
| 27 |
{
|
| 28 |
"prompt": "an orange cat",
|
| 29 |
-
"
|
|
|
|
|
|
|
|
|
|
| 30 |
"style_scope": "generation",
|
| 31 |
"style_weight": 1,
|
| 32 |
"style_end": 1,
|
|
@@ -75,7 +78,7 @@ Content-Type: application/json
|
|
| 75 |
}
|
| 76 |
```
|
| 77 |
|
| 78 |
-
Returns an encrypted `.epng` with ComfyUI workflow and parameter metadata, scaled after generation by the decimal `return_scale` from `0.01` through `1`, with content type `application/octet-stream`. `batch_size` generates from 1 through 8 images at once and defaults to `1`. Multiple landscape images are stacked top to bottom; portrait or square images are joined left to right. Regional prompts and their mask nodes are included in the metadata. Use `1` for full resolution. `regions` accepts up to three spatial prompts on a 5 by 5 grid. Set `regional_mode` to `attention` for Attention Couple (PPM), or omit it for soft conditioning. Regional prompts establish subjects from the first sampling step. The reduced-strength global prompt starts after the first fifth to complete the environment while regions remain active. Each internal region edge feathers by half a grid cell, so overlap adjacent regions by one cell for a smooth crossfade. `area` accepts `auto`, `full`, `tl`, `tc`, `tr`, `ml`, `mc`, `mr`, `bl`, `bc`, `br`, `th`, `mh`, `bh`, `lh`, `ch`, or `rh`. It also accepts any single cell from `a1` through `e5`, or a rectangular inclusive range such as `a1:c5`. `auto` places one prompt in the center, two left and right, or three left, center, and right. Columns run left to right and rows run top to bottom. Region strength defaults to `1`. Use the global `prompt` for environment, lighting, shared style, and composition outside regions. Omitting `regions` keeps whole-image generation unchanged. Set either scheduler to `AlignYourSteps` to use `AlignYourStepsScheduler` with fixed model type `SDXL` and the corresponding steps and denoise values. Only `prompt` is required for the built-in site defaults. `
|
| 79 |
|
| 80 |
## Archive
|
| 81 |
|
|
|
|
| 26 |
|
| 27 |
{
|
| 28 |
"prompt": "an orange cat",
|
| 29 |
+
"style_images": [
|
| 30 |
+
"BASE64_ENCODED_IMAGE_1",
|
| 31 |
+
"BASE64_ENCODED_IMAGE_2"
|
| 32 |
+
],
|
| 33 |
"style_scope": "generation",
|
| 34 |
"style_weight": 1,
|
| 35 |
"style_end": 1,
|
|
|
|
| 78 |
}
|
| 79 |
```
|
| 80 |
|
| 81 |
+
Returns an encrypted `.epng` with ComfyUI workflow and parameter metadata, scaled after generation by the decimal `return_scale` from `0.01` through `1`, with content type `application/octet-stream`. `batch_size` generates from 1 through 8 images at once and defaults to `1`. Multiple landscape images are stacked top to bottom; portrait or square images are joined left to right. Regional prompts and their mask nodes are included in the metadata. Use `1` for full resolution. `regions` accepts up to three spatial prompts on a 5 by 5 grid. Set `regional_mode` to `attention` for Attention Couple (PPM), or omit it for soft conditioning. Regional prompts establish subjects from the first sampling step. The reduced-strength global prompt starts after the first fifth to complete the environment while regions remain active. Each internal region edge feathers by half a grid cell, so overlap adjacent regions by one cell for a smooth crossfade. `area` accepts `auto`, `full`, `tl`, `tc`, `tr`, `ml`, `mc`, `mr`, `bl`, `bc`, `br`, `th`, `mh`, `bh`, `lh`, `ch`, or `rh`. It also accepts any single cell from `a1` through `e5`, or a rectangular inclusive range such as `a1:c5`. `auto` places one prompt in the center, two left and right, or three left, center, and right. Columns run left to right and rows run top to bottom. Region strength defaults to `1`. Use the global `prompt` for environment, lighting, shared style, and composition outside regions. Omitting `regions` keeps whole-image generation unchanged. Set either scheduler to `AlignYourSteps` to use `AlignYourStepsScheduler` with fixed model type `SDXL` and the corresponding steps and denoise values. Only `prompt` is required for the built-in site defaults. `style_images` enables InstantStyle for SDXL and Illustrious checkpoints and accepts one through four raw base64 images or image data URLs up to 20 MiB each. References are center-cropped and their embeddings are averaged, allowing varied subjects and palettes to contribute their shared style. `style_weight` ranges from `0` through `5`, and `style_end` controls the fraction of sampling during which style guidance remains active. `style_scope` accepts `first`, `generation`, or `all`. `first` only styles the initial pass. `generation`, the default, styles the first pass and the upscale second pass while leaving ADetailer unconditioned to prioritize facial anatomy. `all` also styles every ADetailer pass and is useful when detailed regions visibly lose the reference style. A one-pass request treats `generation` as `first`. The exported ComfyUI workflow names the external references `style-reference-1.png` through `style-reference-4.png`. LoRAs and detailers run in array order. A blank detailer `prompt` or `negative` inherits the matching main prompt. Each request creates fresh sampler seeds. When `upscale` is true, the selected upscaler is normalized to the requested `upscale_scale` resolution before the second sampler. 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.
|
| 82 |
|
| 83 |
## Archive
|
| 84 |
|
app.py
CHANGED
|
@@ -51,7 +51,7 @@ from huggingface_hub import (
|
|
| 51 |
download_bucket_files,
|
| 52 |
list_bucket_tree,
|
| 53 |
)
|
| 54 |
-
from PIL import Image, ImageDraw, ImageFont
|
| 55 |
from PIL.PngImagePlugin import PngInfo
|
| 56 |
from pydantic import BaseModel, ConfigDict, Field
|
| 57 |
from starlette.background import BackgroundTask
|
|
@@ -208,6 +208,8 @@ STYLE_SCOPES = ("first", "generation", "all")
|
|
| 208 |
DEFAULT_STYLE_SCOPE = "generation"
|
| 209 |
DEFAULT_STYLE_WEIGHT = 1
|
| 210 |
DEFAULT_STYLE_END = 1
|
|
|
|
|
|
|
| 211 |
MAX_STYLE_IMAGE_SIZE = 20 * MIB
|
| 212 |
BUILTIN_ASSETS = {
|
| 213 |
("ipadapter", STYLE_IPADAPTER): (
|
|
@@ -469,7 +471,7 @@ class DirectRequest(BaseModel):
|
|
| 469 |
regions: list[RegionRequest] = Field(default_factory=list, max_length=3)
|
| 470 |
regional_mode: str = REGIONAL_MODES[0]
|
| 471 |
detailers: list[DetailerRequest] = Field(default_factory=list)
|
| 472 |
-
|
| 473 |
style_scope: str = DEFAULT_STYLE_SCOPE
|
| 474 |
style_weight: float = Field(DEFAULT_STYLE_WEIGHT, ge=0, le=5)
|
| 475 |
style_end: float = Field(DEFAULT_STYLE_END, gt=0, le=1)
|
|
@@ -944,7 +946,7 @@ def encode_positive(
|
|
| 944 |
|
| 945 |
|
| 946 |
def has_style(request):
|
| 947 |
-
return bool(getattr(request, "
|
| 948 |
|
| 949 |
|
| 950 |
def wants_style(request):
|
|
@@ -961,22 +963,29 @@ def style_stage_enabled(request, stage):
|
|
| 961 |
return request.style_scope == "all"
|
| 962 |
|
| 963 |
|
| 964 |
-
def
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
|
| 981 |
|
| 982 |
def load_style_pipeline(model):
|
|
@@ -1662,7 +1671,7 @@ def generate_images(request, from_api=True):
|
|
| 1662 |
|
| 1663 |
regions = prepare_regions(request.regions)
|
| 1664 |
style_image = (
|
| 1665 |
-
|
| 1666 |
if has_style(request)
|
| 1667 |
else None
|
| 1668 |
)
|
|
@@ -1700,7 +1709,10 @@ def generate_images(request, from_api=True):
|
|
| 1700 |
|
| 1701 |
metadata_config = request.model_dump()
|
| 1702 |
if has_style(request):
|
| 1703 |
-
metadata_config["
|
|
|
|
|
|
|
|
|
|
| 1704 |
metadata = image_metadata(
|
| 1705 |
metadata_config,
|
| 1706 |
[first_seed, second_seed],
|
|
@@ -2176,14 +2188,17 @@ def add_detailer(
|
|
| 2176 |
]
|
| 2177 |
|
| 2178 |
|
| 2179 |
-
def
|
| 2180 |
-
|
| 2181 |
-
|
| 2182 |
-
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
|
| 2186 |
-
|
|
|
|
|
|
|
|
|
|
| 2187 |
|
| 2188 |
|
| 2189 |
def generate(
|
|
@@ -2192,7 +2207,7 @@ def generate(
|
|
| 2192 |
regions,
|
| 2193 |
regional_mode,
|
| 2194 |
model,
|
| 2195 |
-
|
| 2196 |
style_scope,
|
| 2197 |
style_weight,
|
| 2198 |
style_end,
|
|
@@ -2227,7 +2242,7 @@ def generate(
|
|
| 2227 |
negative=negative,
|
| 2228 |
model=model,
|
| 2229 |
loras=[],
|
| 2230 |
-
|
| 2231 |
style_scope=style_scope,
|
| 2232 |
style_weight=style_weight,
|
| 2233 |
style_end=style_end,
|
|
@@ -3190,15 +3205,17 @@ with gr.Blocks(title="Image generation") as demo:
|
|
| 3190 |
with gr.Accordion("InstantStyle", open=False):
|
| 3191 |
gr.HTML(
|
| 3192 |
"<model-note>"
|
| 3193 |
-
"SDXL and Illustrious only.
|
| 3194 |
-
"
|
|
|
|
| 3195 |
"ADetailer focused on anatomy."
|
| 3196 |
"</model-note>"
|
| 3197 |
)
|
| 3198 |
-
|
| 3199 |
-
label="Style
|
| 3200 |
-
|
| 3201 |
-
|
|
|
|
| 3202 |
)
|
| 3203 |
style_scope_input = gr.Dropdown(
|
| 3204 |
[
|
|
@@ -3430,7 +3447,7 @@ with gr.Blocks(title="Image generation") as demo:
|
|
| 3430 |
regions_input,
|
| 3431 |
regional_mode_input,
|
| 3432 |
model_input,
|
| 3433 |
-
|
| 3434 |
style_scope_input,
|
| 3435 |
style_weight_input,
|
| 3436 |
style_end_input,
|
|
|
|
| 51 |
download_bucket_files,
|
| 52 |
list_bucket_tree,
|
| 53 |
)
|
| 54 |
+
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
| 55 |
from PIL.PngImagePlugin import PngInfo
|
| 56 |
from pydantic import BaseModel, ConfigDict, Field
|
| 57 |
from starlette.background import BackgroundTask
|
|
|
|
| 208 |
DEFAULT_STYLE_SCOPE = "generation"
|
| 209 |
DEFAULT_STYLE_WEIGHT = 1
|
| 210 |
DEFAULT_STYLE_END = 1
|
| 211 |
+
MAX_STYLE_IMAGES = 4
|
| 212 |
+
STYLE_IMAGE_SIZE = 1024
|
| 213 |
MAX_STYLE_IMAGE_SIZE = 20 * MIB
|
| 214 |
BUILTIN_ASSETS = {
|
| 215 |
("ipadapter", STYLE_IPADAPTER): (
|
|
|
|
| 471 |
regions: list[RegionRequest] = Field(default_factory=list, max_length=3)
|
| 472 |
regional_mode: str = REGIONAL_MODES[0]
|
| 473 |
detailers: list[DetailerRequest] = Field(default_factory=list)
|
| 474 |
+
style_images: list[str] = Field(default_factory=list, max_length=MAX_STYLE_IMAGES)
|
| 475 |
style_scope: str = DEFAULT_STYLE_SCOPE
|
| 476 |
style_weight: float = Field(DEFAULT_STYLE_WEIGHT, ge=0, le=5)
|
| 477 |
style_end: float = Field(DEFAULT_STYLE_END, gt=0, le=1)
|
|
|
|
| 946 |
|
| 947 |
|
| 948 |
def has_style(request):
|
| 949 |
+
return bool(getattr(request, "style_images", []))
|
| 950 |
|
| 951 |
|
| 952 |
def wants_style(request):
|
|
|
|
| 963 |
return request.style_scope == "all"
|
| 964 |
|
| 965 |
|
| 966 |
+
def decode_style_images(values):
|
| 967 |
+
images = []
|
| 968 |
+
for value in values:
|
| 969 |
+
encoded = value.partition(",")[2] if value.startswith("data:") else value
|
| 970 |
+
if len(encoded) > (MAX_STYLE_IMAGE_SIZE + 2) // 3 * 4:
|
| 971 |
+
raise ValueError("InstantStyle image exceeds 20 MiB")
|
| 972 |
+
try:
|
| 973 |
+
data = base64.b64decode(encoded, validate=True)
|
| 974 |
+
except (ValueError, binascii.Error) as error:
|
| 975 |
+
raise ValueError("Invalid InstantStyle image") from error
|
| 976 |
+
if len(data) > MAX_STYLE_IMAGE_SIZE:
|
| 977 |
+
raise ValueError("InstantStyle image exceeds 20 MiB")
|
| 978 |
+
try:
|
| 979 |
+
with Image.open(BytesIO(data)) as image:
|
| 980 |
+
image = ImageOps.fit(
|
| 981 |
+
image.convert("RGB"),
|
| 982 |
+
(STYLE_IMAGE_SIZE, STYLE_IMAGE_SIZE),
|
| 983 |
+
Image.Resampling.LANCZOS,
|
| 984 |
+
)
|
| 985 |
+
images.append(np.asarray(image, dtype=np.float32) / 255)
|
| 986 |
+
except Exception as error:
|
| 987 |
+
raise ValueError("Invalid InstantStyle image") from error
|
| 988 |
+
return torch.from_numpy(np.stack(images))
|
| 989 |
|
| 990 |
|
| 991 |
def load_style_pipeline(model):
|
|
|
|
| 1671 |
|
| 1672 |
regions = prepare_regions(request.regions)
|
| 1673 |
style_image = (
|
| 1674 |
+
decode_style_images(request.style_images)
|
| 1675 |
if has_style(request)
|
| 1676 |
else None
|
| 1677 |
)
|
|
|
|
| 1709 |
|
| 1710 |
metadata_config = request.model_dump()
|
| 1711 |
if has_style(request):
|
| 1712 |
+
metadata_config["style_images"] = [
|
| 1713 |
+
f"style-reference-{index}.png"
|
| 1714 |
+
for index in range(1, len(request.style_images) + 1)
|
| 1715 |
+
]
|
| 1716 |
metadata = image_metadata(
|
| 1717 |
metadata_config,
|
| 1718 |
[first_seed, second_seed],
|
|
|
|
| 2188 |
]
|
| 2189 |
|
| 2190 |
|
| 2191 |
+
def encode_style_images(files):
|
| 2192 |
+
files = files or []
|
| 2193 |
+
if len(files) > MAX_STYLE_IMAGES:
|
| 2194 |
+
raise gr.Error("InstantStyle accepts up to 4 images")
|
| 2195 |
+
images = []
|
| 2196 |
+
for file in files:
|
| 2197 |
+
data = Path(file).read_bytes()
|
| 2198 |
+
if len(data) > MAX_STYLE_IMAGE_SIZE:
|
| 2199 |
+
raise gr.Error("InstantStyle image exceeds 20 MiB")
|
| 2200 |
+
images.append(base64.b64encode(data).decode())
|
| 2201 |
+
return images
|
| 2202 |
|
| 2203 |
|
| 2204 |
def generate(
|
|
|
|
| 2207 |
regions,
|
| 2208 |
regional_mode,
|
| 2209 |
model,
|
| 2210 |
+
style_images,
|
| 2211 |
style_scope,
|
| 2212 |
style_weight,
|
| 2213 |
style_end,
|
|
|
|
| 2242 |
negative=negative,
|
| 2243 |
model=model,
|
| 2244 |
loras=[],
|
| 2245 |
+
style_images=encode_style_images(style_images),
|
| 2246 |
style_scope=style_scope,
|
| 2247 |
style_weight=style_weight,
|
| 2248 |
style_end=style_end,
|
|
|
|
| 3205 |
with gr.Accordion("InstantStyle", open=False):
|
| 3206 |
gr.HTML(
|
| 3207 |
"<model-note>"
|
| 3208 |
+
"SDXL and Illustrious only. Add up to four references; "
|
| 3209 |
+
"their center crops are averaged. Use varied subjects and "
|
| 3210 |
+
"palettes. The default styles both generation passes but leaves "
|
| 3211 |
"ADetailer focused on anatomy."
|
| 3212 |
"</model-note>"
|
| 3213 |
)
|
| 3214 |
+
style_images_input = gr.File(
|
| 3215 |
+
label="Style references",
|
| 3216 |
+
file_count="multiple",
|
| 3217 |
+
file_types=["image"],
|
| 3218 |
+
type="filepath",
|
| 3219 |
)
|
| 3220 |
style_scope_input = gr.Dropdown(
|
| 3221 |
[
|
|
|
|
| 3447 |
regions_input,
|
| 3448 |
regional_mode_input,
|
| 3449 |
model_input,
|
| 3450 |
+
style_images_input,
|
| 3451 |
style_scope_input,
|
| 3452 |
style_weight_input,
|
| 3453 |
style_end_input,
|
workflow_api.py
CHANGED
|
@@ -21,6 +21,7 @@ DETAILER_DROP_SIZE = 10
|
|
| 21 |
STYLE_PRESET = "PLUS (high strength)"
|
| 22 |
STYLE_WEIGHT_TYPE = "style transfer"
|
| 23 |
STYLE_EMBEDS_SCALING = "V only"
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
def upscale_size(width, height, scale):
|
|
@@ -668,7 +669,7 @@ def image_metadata(
|
|
| 668 |
style_pipeline = None
|
| 669 |
|
| 670 |
def style_enabled(stage):
|
| 671 |
-
if not config.get("
|
| 672 |
return False
|
| 673 |
scope = config.get("style_scope", "generation")
|
| 674 |
if stage == "first":
|
|
@@ -682,9 +683,22 @@ def image_metadata(
|
|
| 682 |
if not style_enabled(stage):
|
| 683 |
return model
|
| 684 |
if style_image is None:
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
if style_pipeline is None:
|
| 689 |
loader = add("IPAdapterUnifiedLoader", {
|
| 690 |
"model": model,
|
|
|
|
| 21 |
STYLE_PRESET = "PLUS (high strength)"
|
| 22 |
STYLE_WEIGHT_TYPE = "style transfer"
|
| 23 |
STYLE_EMBEDS_SCALING = "V only"
|
| 24 |
+
STYLE_IMAGE_SIZE = 1024
|
| 25 |
|
| 26 |
|
| 27 |
def upscale_size(width, height, scale):
|
|
|
|
| 669 |
style_pipeline = None
|
| 670 |
|
| 671 |
def style_enabled(stage):
|
| 672 |
+
if not config.get("style_images"):
|
| 673 |
return False
|
| 674 |
scope = config.get("style_scope", "generation")
|
| 675 |
if stage == "first":
|
|
|
|
| 683 |
if not style_enabled(stage):
|
| 684 |
return model
|
| 685 |
if style_image is None:
|
| 686 |
+
for name in config["style_images"]:
|
| 687 |
+
image = [add("LoadImage", {"image": name}), 0]
|
| 688 |
+
image = [add("ImageScale", {
|
| 689 |
+
"image": image,
|
| 690 |
+
"upscale_method": "lanczos",
|
| 691 |
+
"width": STYLE_IMAGE_SIZE,
|
| 692 |
+
"height": STYLE_IMAGE_SIZE,
|
| 693 |
+
"crop": "center",
|
| 694 |
+
}), 0]
|
| 695 |
+
if style_image is None:
|
| 696 |
+
style_image = image
|
| 697 |
+
else:
|
| 698 |
+
style_image = [add("ImageBatch", {
|
| 699 |
+
"image1": style_image,
|
| 700 |
+
"image2": image,
|
| 701 |
+
}), 0]
|
| 702 |
if style_pipeline is None:
|
| 703 |
loader = add("IPAdapterUnifiedLoader", {
|
| 704 |
"model": model,
|