q6 commited on
Commit
7a107e5
·
1 Parent(s): d248e83

Support averaged style references

Browse files
Files changed (3) hide show
  1. API.md +5 -2
  2. app.py +55 -38
  3. workflow_api.py +18 -4
API.md CHANGED
@@ -26,7 +26,10 @@ Content-Type: application/json
26
 
27
  {
28
  "prompt": "an orange cat",
29
- "style_image": "BASE64_ENCODED_PNG_OR_JPEG",
 
 
 
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. `style_image` enables InstantStyle for SDXL and Illustrious checkpoints and accepts raw base64 or an image data URL up to 20 MiB. `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 reference `style-reference.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.
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
- style_image: str = ""
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, "style_image", ""))
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 decode_style_image(value):
965
- encoded = value.partition(",")[2] if value.startswith("data:") else value
966
- if len(encoded) > (MAX_STYLE_IMAGE_SIZE + 2) // 3 * 4:
967
- raise ValueError("InstantStyle image exceeds 20 MiB")
968
- try:
969
- data = base64.b64decode(encoded, validate=True)
970
- except (ValueError, binascii.Error) as error:
971
- raise ValueError("Invalid InstantStyle image") from error
972
- if len(data) > MAX_STYLE_IMAGE_SIZE:
973
- raise ValueError("InstantStyle image exceeds 20 MiB")
974
- try:
975
- with Image.open(BytesIO(data)) as image:
976
- pixels = np.asarray(image.convert("RGB"), dtype=np.float32) / 255
977
- except Exception as error:
978
- raise ValueError("Invalid InstantStyle image") from error
979
- return torch.from_numpy(pixels).unsqueeze(0)
 
 
 
 
 
 
 
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
- decode_style_image(request.style_image)
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["style_image"] = "style-reference.png"
 
 
 
1704
  metadata = image_metadata(
1705
  metadata_config,
1706
  [first_seed, second_seed],
@@ -2176,14 +2188,17 @@ def add_detailer(
2176
  ]
2177
 
2178
 
2179
- def encode_style_image(image):
2180
- if image is None:
2181
- return ""
2182
- data = BytesIO()
2183
- image.convert("RGB").save(data, format="PNG")
2184
- if data.tell() > MAX_STYLE_IMAGE_SIZE:
2185
- raise gr.Error("InstantStyle image exceeds 20 MiB")
2186
- return base64.b64encode(data.getvalue()).decode()
 
 
 
2187
 
2188
 
2189
  def generate(
@@ -2192,7 +2207,7 @@ def generate(
2192
  regions,
2193
  regional_mode,
2194
  model,
2195
- style_image,
2196
  style_scope,
2197
  style_weight,
2198
  style_end,
@@ -2227,7 +2242,7 @@ def generate(
2227
  negative=negative,
2228
  model=model,
2229
  loras=[],
2230
- style_image=encode_style_image(style_image),
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. The reference is center-cropped. "
3194
- "The default styles both generation passes but leaves "
 
3195
  "ADetailer focused on anatomy."
3196
  "</model-note>"
3197
  )
3198
- style_image_input = gr.Image(
3199
- label="Style reference",
3200
- type="pil",
3201
- image_mode="RGB",
 
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
- style_image_input,
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("style_image"):
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
- style_image = [add("LoadImage", {
686
- "image": config["style_image"],
687
- }), 0]
 
 
 
 
 
 
 
 
 
 
 
 
 
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,