Skip to content

Commit fd3c71d

Browse files
committed
Add option to evaluate dynamic prompts
1 parent be31d15 commit fd3c71d

File tree

6 files changed

+2063
-6
lines changed

6 files changed

+2063
-6
lines changed

ai_diffusion/region.py

+50-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .jobs import JobRegion
1212
from .control import ControlLayerList
1313
from .settings import settings
14+
from .text import evaluate_dynamic_prompt
1415

1516

1617
class RegionLink(Enum):
@@ -26,6 +27,7 @@ class Region(QObject, ObservableProperties):
2627

2728
_parent: RootRegion
2829
_layers: list[QUuid]
30+
_positive_evaluated: str | None = None # None indicates not yet evaluated
2931

3032
layer_ids = Property("", persist=True, setter="_set_layer_ids")
3133
positive = Property("", persist=True)
@@ -142,6 +144,19 @@ async def translate_prompt(self, client: Client):
142144
if positive == self.positive:
143145
self.positive = translated
144146

147+
@property
148+
def positive_evaluated(self) -> str:
149+
"""Return evaluated version if it exists, otherwise return original"""
150+
return self._positive_evaluated if self._positive_evaluated is not None else self.positive
151+
152+
def evaluate_dynamic_prompt(self):
153+
"""Evaluate the dynamic prompt and store result. Should only be called once during processing."""
154+
self._positive_evaluated = evaluate_dynamic_prompt(self.positive)
155+
156+
def clear_dynamic_prompt_eval(self):
157+
"""Clear the evaluated prompt, must be called when processing of dynamic prompt syntax is complete"""
158+
self._positive_evaluated = None
159+
145160

146161
class RootRegion(QObject, ObservableProperties):
147162
"""Manages a collection of regions, each of which is linked to one or more layers in the document.
@@ -153,6 +168,7 @@ class RootRegion(QObject, ObservableProperties):
153168
_regions: list[Region]
154169
_active: Region | None = None
155170
_active_layer: QUuid | None = None
171+
_positive_evaluated: str | None = None # None indicates not yet evaluated
156172

157173
positive = Property("", persist=True)
158174
negative = Property("", persist=True)
@@ -344,6 +360,19 @@ async def translate_prompt(self, client: Client):
344360
if negative == self.negative:
345361
self.negative = translated
346362

363+
@property
364+
def positive_evaluated(self) -> str:
365+
"""Return evaluated version if it exists, otherwise return original"""
366+
return self._positive_evaluated if self._positive_evaluated is not None else self.positive
367+
368+
def evaluate_dynamic_prompt(self):
369+
"""Evaluate the dynamic prompt and store result. Should only be called once during processing."""
370+
self._positive_evaluated = evaluate_dynamic_prompt(self.positive)
371+
372+
def clear_dynamic_prompt_eval(self):
373+
"""Clear the evaluated prompt, must be called when processing of dynamic prompt syntax is complete"""
374+
self._positive_evaluated = None
375+
347376
def __len__(self):
348377
return len(self._regions)
349378

@@ -368,13 +397,24 @@ def get_region_inpaint_mask(region_layer: Layer, max_extent: Extent, min_size=0)
368397
return mask_image.to_mask(bounds)
369398

370399

400+
def clear_dynamic_prompt_evals(root: RootRegion):
401+
"""Clear the evaluated prompts for all regions."""
402+
root.clear_dynamic_prompt_eval()
403+
for region in root._regions:
404+
region.clear_dynamic_prompt_eval()
405+
406+
371407
def process_regions(
372408
root: RootRegion,
373409
bounds: Bounds,
374410
parent_layer: Layer | None = None,
375411
min_coverage=0.02,
376412
time: int | None = None,
377413
):
414+
if settings.dynamic_prompts:
415+
root.evaluate_dynamic_prompt()
416+
for region in root._regions:
417+
region.evaluate_dynamic_prompt()
378418
parent_region = None
379419
if parent_layer and not parent_layer.is_root:
380420
parent_region = root.find_linked(parent_layer)
@@ -383,11 +423,11 @@ def process_regions(
383423
job_info = []
384424
control = root.control.to_api(bounds, time)
385425
if parent_layer and parent_region:
386-
parent_prompt = parent_region.positive
426+
parent_prompt = parent_region.positive_evaluated
387427
control += parent_region.control.to_api(bounds, time)
388428
job_info = [JobRegion(parent_layer.id_string, parent_prompt, bounds)]
389429
result = ConditioningInput(
390-
positive=workflow.merge_prompt(parent_prompt, root.positive),
430+
positive=workflow.merge_prompt(parent_prompt, root.positive_evaluated),
391431
negative=root.negative,
392432
control=control,
393433
)
@@ -401,6 +441,7 @@ def process_regions(
401441
layer_regions = ((l, root.find_linked(l, RegionLink.direct)) for l in child_layers)
402442
layer_regions = [(l, r) for l, r in layer_regions if r is not None]
403443
if len(layer_regions) == 0:
444+
clear_dynamic_prompt_evals(root)
404445
return result, job_info
405446

406447
# Get region masks. Filter out regions with:
@@ -416,13 +457,14 @@ def process_regions(
416457
if coverage_rough < 2 * min_coverage:
417458
continue
418459

460+
region_positive = region.positive_evaluated
419461
region_result = RegionInput(
420462
layer.get_mask(bounds),
421463
layer_bounds,
422-
workflow.merge_prompt(region.positive, root.positive),
464+
workflow.merge_prompt(region_positive, root.positive_evaluated),
423465
control=region.control.to_api(bounds, time),
424466
)
425-
job_params = JobRegion(layer.id_string, region.positive, layer_bounds)
467+
job_params = JobRegion(layer.id_string, region_positive, layer_bounds)
426468
result_regions.append((region_result, job_params))
427469

428470
# Remove from each region mask any overlapping areas from regions above it.
@@ -437,8 +479,9 @@ def process_regions(
437479
coverage = mask.average()
438480
if coverage > 0.9 and min_coverage > 0:
439481
# Single region covers (almost) entire image, don't use regional conditioning.
440-
result.positive = region.positive
482+
result.positive = region.positive_evaluated
441483
result.control += region.control
484+
clear_dynamic_prompt_evals(root)
442485
return result, [job_region]
443486
elif coverage < min_coverage:
444487
# Region has less than minimum coverage, remove it.
@@ -452,6 +495,7 @@ def process_regions(
452495

453496
# If there are no regions left, don't use regional conditioning.
454497
if len(result_regions) == 0:
498+
clear_dynamic_prompt_evals(root)
455499
return result, job_info
456500

457501
# If the region(s) don't cover the entire image, add a final region for the remaining area.
@@ -463,5 +507,6 @@ def process_regions(
463507
job = JobRegion(parent_layer.id_string, "background", bounds, is_background=True)
464508
result_regions.insert(0, (input, job))
465509

510+
clear_dynamic_prompt_evals(root)
466511
result.regions = [r for r, _ in result_regions]
467512
return result, [j for _, j in result_regions]

ai_diffusion/settings.py

+7
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ class Settings(QObject):
208208
_("Show Steps"), False, _("Display the number of steps to be evaluated in the weights box.")
209209
)
210210

211+
dynamic_prompts: bool
212+
_dynamic_prompts = Setting(
213+
_("Dynamic Prompts"),
214+
False,
215+
_("Evaluate dynamic prompts (variables, variants and wildcards) at job queue time."),
216+
)
217+
211218
tag_files: list[str]
212219
_tag_files = Setting(
213220
_("Tag Auto-Completion"),

0 commit comments

Comments
 (0)