q6 commited on
Commit
f3b9916
·
1 Parent(s): bb1985e

Generate configurable image batches

Browse files
Files changed (3) hide show
  1. API.md +2 -1
  2. app.py +83 -48
  3. workflow_api.py +1 -1
API.md CHANGED
@@ -50,6 +50,7 @@ Content-Type: application/json
50
  "negative": "text, watermark",
51
  "width": 1152,
52
  "height": 896,
 
53
  "model": "52_novaAnimeXL_ilV190.safetensors",
54
  "loras": [{"name": "8_bikabaka.safetensors", "strength": 1, "clip": 0}],
55
  "sampler": "euler_ancestral",
@@ -70,7 +71,7 @@ Content-Type: application/json
70
  }
71
  ```
72
 
73
- 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`. 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. 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.
74
 
75
  ## Archive
76
 
 
50
  "negative": "text, watermark",
51
  "width": 1152,
52
  "height": 896,
53
+ "batch_size": 2,
54
  "model": "52_novaAnimeXL_ilV190.safetensors",
55
  "loras": [{"name": "8_bikabaka.safetensors", "strength": 1, "clip": 0}],
56
  "sampler": "euler_ancestral",
 
71
  }
72
  ```
73
 
74
+ 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 `2`. 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. 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.
75
 
76
  ## Archive
77
 
app.py CHANGED
@@ -101,6 +101,8 @@ IMAGE_SUFFIXES = (".epng",)
101
  PREVIEW_QUALITY = 75
102
  PREVIEW_SCALE = .25
103
  DEFAULT_RETURN_SCALE = 1
 
 
104
  ENVIRONMENT_START = .2
105
  REGIONAL_GLOBAL_STRENGTH = .6
106
  CUSTOM_NODE_REPOS = {
@@ -211,7 +213,7 @@ MATRIX_TYPES = (
211
  "sampler+scheduler",
212
  COMBINED_MATRIX_TYPE,
213
  )
214
- GPU_DURATION = 13
215
  GPU_ATTEMPTS = 3
216
  GPU_RETRY_ERRORS = (
217
  "gpu task aborted",
@@ -416,6 +418,7 @@ class DirectRequest(BaseModel):
416
  negative: str = DEFAULT_NEGATIVE
417
  width: int = Field(1152, ge=64, le=2048)
418
  height: int = Field(896, ge=64, le=2048)
 
419
  sampler: str = DEFAULT_SAMPLER
420
  scheduler: str = DEFAULT_SCHEDULER
421
  steps: int = Field(DEFAULT_STEPS, ge=1, le=100)
@@ -1031,14 +1034,14 @@ def infer_image(
1031
  image=image,
1032
  )[0]
1033
 
1034
- return image[0]
1035
 
1036
 
1037
  def generate_gpu(request, regions):
1038
  latent = state["latent"].generate(
1039
  width=request.width,
1040
  height=request.height,
1041
- batch_size=1,
1042
  )[0]
1043
  first_seed = secrets.randbits(64)
1044
  second_seed = secrets.randbits(64) if request.upscale else None
@@ -1492,7 +1495,7 @@ def model_options(kind, local):
1492
  return sorted(set(local) | remote_models[kind].keys(), key=str.casefold)
1493
 
1494
 
1495
- def generate_image(request, from_api=True):
1496
  if (
1497
  request.width % 8
1498
  or request.height % 8
@@ -1533,34 +1536,60 @@ def generate_image(request, from_api=True):
1533
  regions,
1534
  )
1535
 
1536
- result = Image.fromarray(
1537
- np.clip(
1538
- image.detach().numpy() * 255,
1539
- 0,
1540
- 255,
1541
- ).astype(np.uint8)
 
 
 
 
 
 
 
 
1542
  )
1543
-
1544
- result.info.update(
1545
- image_metadata(
1546
- request.model_dump(),
1547
- [first_seed, second_seed],
1548
- detailer_seeds,
1549
- [
1550
- vae_name(detailer.model)
1551
- if detailer.model
1552
- else second_vae if request.upscale else first_vae
1553
- for detailer in request.detailers
1554
- ],
1555
- [first_vae, second_vae],
1556
- regions,
1557
- ENVIRONMENT_START,
1558
- REGIONAL_GLOBAL_STRENGTH,
1559
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  )
 
 
1561
 
1562
- backup_pool.submit(archive_image, result, from_api)
1563
- return result
 
 
 
 
 
 
1564
 
1565
 
1566
  def archive_image(image, from_api):
@@ -1991,6 +2020,7 @@ def generate(
1991
  detailers,
1992
  width=1152,
1993
  height=896,
 
1994
  sampler=DEFAULT_SAMPLER,
1995
  scheduler=DEFAULT_SCHEDULER,
1996
  steps=DEFAULT_STEPS,
@@ -2021,6 +2051,7 @@ def generate(
2021
  detailers=clean_detailers(detailers),
2022
  width=width,
2023
  height=height,
 
2024
  sampler=sampler,
2025
  scheduler=scheduler,
2026
  steps=steps,
@@ -2038,23 +2069,22 @@ def generate(
2038
  denoise=denoise,
2039
  return_scale=return_scale,
2040
  )
2041
- image = generate_image(request, False)
2042
- if return_scale != 1:
2043
- image = image.resize(
2044
- (
2045
- round(image.width * return_scale),
2046
- round(image.height * return_scale),
2047
- ),
2048
- Image.Resampling.LANCZOS,
2049
- )
2050
- return image
2051
 
2052
 
2053
  def api_generate(body, p=""):
2054
  if not valid_pass(p):
2055
  raise gr.Error("Invalid password")
2056
 
2057
- image = generate_image(DirectRequest.model_validate_json(body))
 
 
 
 
 
2058
  temp = tempfile.NamedTemporaryFile(suffix=".epng", delete=False)
2059
  temp.write(encrypted_image_bytes(image))
2060
  temp.close()
@@ -2334,6 +2364,7 @@ def workflow_values(workflow):
2334
  inputs.get("sampler_name", DEFAULT_SAMPLER),
2335
  inputs.get("scheduler", DEFAULT_SCHEDULER),
2336
  inputs.get("cfg", DEFAULT_CFG),
 
2337
  )
2338
 
2339
 
@@ -2348,6 +2379,7 @@ def run_job(job_id, workflow):
2348
  sampler,
2349
  scheduler,
2350
  cfg,
 
2351
  ) = workflow_values(workflow)
2352
 
2353
  image = queued_generate(
@@ -2360,6 +2392,7 @@ def run_job(job_id, workflow):
2360
  sampler=sampler,
2361
  scheduler=scheduler,
2362
  cfg=cfg,
 
2363
  )
2364
  )
2365
 
@@ -2652,14 +2685,7 @@ def direct(
2652
  except ValueError as error:
2653
  raise HTTPException(422, str(error)) from error
2654
 
2655
- if body.return_scale != 1:
2656
- image = image.resize(
2657
- (
2658
- round(image.width * body.return_scale),
2659
- round(image.height * body.return_scale),
2660
- ),
2661
- Image.Resampling.LANCZOS,
2662
- )
2663
 
2664
  return Response(
2665
  content=encrypted_image_bytes(image),
@@ -2993,6 +3019,13 @@ with gr.Blocks(title="Image generation") as demo:
2993
  with gr.Row():
2994
  width_input = gr.Number(1152, label="Width", precision=0)
2995
  height_input = gr.Number(896, label="Height", precision=0)
 
 
 
 
 
 
 
2996
  with gr.Row():
2997
  sampler_input = gr.Dropdown(
2998
  SAMPLER_NAMES,
@@ -3090,10 +3123,11 @@ with gr.Blocks(title="Image generation") as demo:
3090
  )
3091
  button = gr.Button("Generate", variant="primary")
3092
  with gr.Column(scale=6, min_width=420):
3093
- output = gr.Image(
3094
  label="Preview",
3095
  format="png",
3096
  elem_id="output",
 
3097
  )
3098
 
3099
  api_button = gr.Button(visible=False)
@@ -3126,6 +3160,7 @@ with gr.Blocks(title="Image generation") as demo:
3126
  detailer_input,
3127
  width_input,
3128
  height_input,
 
3129
  sampler_input,
3130
  scheduler_input,
3131
  steps_input,
 
101
  PREVIEW_QUALITY = 75
102
  PREVIEW_SCALE = .25
103
  DEFAULT_RETURN_SCALE = 1
104
+ DEFAULT_BATCH_SIZE = 2
105
+ MAX_BATCH_SIZE = 8
106
  ENVIRONMENT_START = .2
107
  REGIONAL_GLOBAL_STRENGTH = .6
108
  CUSTOM_NODE_REPOS = {
 
213
  "sampler+scheduler",
214
  COMBINED_MATRIX_TYPE,
215
  )
216
+ GPU_DURATION = 18
217
  GPU_ATTEMPTS = 3
218
  GPU_RETRY_ERRORS = (
219
  "gpu task aborted",
 
418
  negative: str = DEFAULT_NEGATIVE
419
  width: int = Field(1152, ge=64, le=2048)
420
  height: int = Field(896, ge=64, le=2048)
421
+ batch_size: int = Field(DEFAULT_BATCH_SIZE, ge=1, le=MAX_BATCH_SIZE)
422
  sampler: str = DEFAULT_SAMPLER
423
  scheduler: str = DEFAULT_SCHEDULER
424
  steps: int = Field(DEFAULT_STEPS, ge=1, le=100)
 
1034
  image=image,
1035
  )[0]
1036
 
1037
+ return image
1038
 
1039
 
1040
  def generate_gpu(request, regions):
1041
  latent = state["latent"].generate(
1042
  width=request.width,
1043
  height=request.height,
1044
+ batch_size=request.batch_size,
1045
  )[0]
1046
  first_seed = secrets.randbits(64)
1047
  second_seed = secrets.randbits(64) if request.upscale else None
 
1495
  return sorted(set(local) | remote_models[kind].keys(), key=str.casefold)
1496
 
1497
 
1498
+ def generate_images(request, from_api=True):
1499
  if (
1500
  request.width % 8
1501
  or request.height % 8
 
1536
  regions,
1537
  )
1538
 
1539
+ metadata = image_metadata(
1540
+ request.model_dump(),
1541
+ [first_seed, second_seed],
1542
+ detailer_seeds,
1543
+ [
1544
+ vae_name(detailer.model)
1545
+ if detailer.model
1546
+ else second_vae if request.upscale else first_vae
1547
+ for detailer in request.detailers
1548
+ ],
1549
+ [first_vae, second_vae],
1550
+ regions,
1551
+ ENVIRONMENT_START,
1552
+ REGIONAL_GLOBAL_STRENGTH,
1553
  )
1554
+ results = [
1555
+ Image.fromarray(
1556
+ np.clip(item.detach().numpy() * 255, 0, 255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
1557
  )
1558
+ for item in image
1559
+ ]
1560
+ for result in results:
1561
+ result.info.update(metadata)
1562
+ backup_pool.submit(archive_image, result, from_api)
1563
+ return results
1564
+
1565
+
1566
+ def combine_images(images, width, height):
1567
+ if len(images) == 1:
1568
+ return images[0]
1569
+ vertical = width > height
1570
+ image_width, image_height = images[0].size
1571
+ size = (
1572
+ (image_width, image_height * len(images))
1573
+ if vertical
1574
+ else (image_width * len(images), image_height)
1575
+ )
1576
+ combined = Image.new(images[0].mode, size)
1577
+ for index, image in enumerate(images):
1578
+ combined.paste(
1579
+ image,
1580
+ (0, index * image_height) if vertical else (index * image_width, 0),
1581
  )
1582
+ combined.info.update(images[0].info)
1583
+ return combined
1584
 
1585
+
1586
+ def scale_image(image, scale):
1587
+ if scale == 1:
1588
+ return image
1589
+ return image.resize(
1590
+ (round(image.width * scale), round(image.height * scale)),
1591
+ Image.Resampling.LANCZOS,
1592
+ )
1593
 
1594
 
1595
  def archive_image(image, from_api):
 
2020
  detailers,
2021
  width=1152,
2022
  height=896,
2023
+ batch_size=DEFAULT_BATCH_SIZE,
2024
  sampler=DEFAULT_SAMPLER,
2025
  scheduler=DEFAULT_SCHEDULER,
2026
  steps=DEFAULT_STEPS,
 
2051
  detailers=clean_detailers(detailers),
2052
  width=width,
2053
  height=height,
2054
+ batch_size=batch_size,
2055
  sampler=sampler,
2056
  scheduler=scheduler,
2057
  steps=steps,
 
2069
  denoise=denoise,
2070
  return_scale=return_scale,
2071
  )
2072
+ return [
2073
+ scale_image(image, return_scale)
2074
+ for image in generate_images(request, False)
2075
+ ]
 
 
 
 
 
 
2076
 
2077
 
2078
  def api_generate(body, p=""):
2079
  if not valid_pass(p):
2080
  raise gr.Error("Invalid password")
2081
 
2082
+ request = DirectRequest.model_validate_json(body)
2083
+ image = combine_images(
2084
+ generate_images(request),
2085
+ request.width,
2086
+ request.height,
2087
+ )
2088
  temp = tempfile.NamedTemporaryFile(suffix=".epng", delete=False)
2089
  temp.write(encrypted_image_bytes(image))
2090
  temp.close()
 
2364
  inputs.get("sampler_name", DEFAULT_SAMPLER),
2365
  inputs.get("scheduler", DEFAULT_SCHEDULER),
2366
  inputs.get("cfg", DEFAULT_CFG),
2367
+ latent.get("batch_size", DEFAULT_BATCH_SIZE),
2368
  )
2369
 
2370
 
 
2379
  sampler,
2380
  scheduler,
2381
  cfg,
2382
+ batch_size,
2383
  ) = workflow_values(workflow)
2384
 
2385
  image = queued_generate(
 
2392
  sampler=sampler,
2393
  scheduler=scheduler,
2394
  cfg=cfg,
2395
+ batch_size=batch_size,
2396
  )
2397
  )
2398
 
 
2685
  except ValueError as error:
2686
  raise HTTPException(422, str(error)) from error
2687
 
2688
+ image = scale_image(image, body.return_scale)
 
 
 
 
 
 
 
2689
 
2690
  return Response(
2691
  content=encrypted_image_bytes(image),
 
3019
  with gr.Row():
3020
  width_input = gr.Number(1152, label="Width", precision=0)
3021
  height_input = gr.Number(896, label="Height", precision=0)
3022
+ batch_size_input = gr.Slider(
3023
+ 1,
3024
+ MAX_BATCH_SIZE,
3025
+ DEFAULT_BATCH_SIZE,
3026
+ step=1,
3027
+ label="Images",
3028
+ )
3029
  with gr.Row():
3030
  sampler_input = gr.Dropdown(
3031
  SAMPLER_NAMES,
 
3123
  )
3124
  button = gr.Button("Generate", variant="primary")
3125
  with gr.Column(scale=6, min_width=420):
3126
+ output = gr.Gallery(
3127
  label="Preview",
3128
  format="png",
3129
  elem_id="output",
3130
+ columns=2,
3131
  )
3132
 
3133
  api_button = gr.Button(visible=False)
 
3160
  detailer_input,
3161
  width_input,
3162
  height_input,
3163
+ batch_size_input,
3164
  sampler_input,
3165
  scheduler_input,
3166
  steps_input,
workflow_api.py CHANGED
@@ -844,7 +844,7 @@ def image_metadata(
844
  latent = [add("EmptyLatentImage", {
845
  "width": config["width"],
846
  "height": config["height"],
847
- "batch_size": 1,
848
  }), 0]
849
  samples = sample(
850
  model,
 
844
  latent = [add("EmptyLatentImage", {
845
  "width": config["width"],
846
  "height": config["height"],
847
+ "batch_size": config["batch_size"],
848
  }), 0]
849
  samples = sample(
850
  model,