Skip to content

Commit 3d9b0fd

Browse files
authored
Merge branch 'invoke-ai:main' into main
2 parents 3cd31f4 + 80eb0bb commit 3d9b0fd

17 files changed

Lines changed: 1712 additions & 877 deletions

File tree

USER_ISOLATION_IMPLEMENTATION.md

Lines changed: 0 additions & 169 deletions
This file was deleted.

invokeai/app/api/routers/utilities.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from invokeai.app.api.routers._access import assert_image_read_access
1818
from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
1919
from invokeai.app.services.model_records.model_records_base import UnknownModelException
20+
from invokeai.app.util.dynamicprompts import find_missing_wildcards
2021
from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
2122
from invokeai.backend.model_manager.taxonomy import ModelType
2223
from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, TextLLMPipeline
@@ -52,8 +53,19 @@ async def parse_dynamicprompts(
5253
"""Creates a batch process"""
5354
max_prompts = min(max_prompts, 10000)
5455
generator: Union[RandomPromptGenerator, CombinatorialPromptGenerator]
56+
error: Optional[str] = None
57+
58+
# An unknown wildcard used as a variant value sends the combinatorial generator into an infinite
59+
# loop, so bail out early with a clear message instead of hanging the request (and with it the UI
60+
# preview). The random generator handles unknown wildcards gracefully, so only the combinatorial
61+
# path is guarded.
62+
if combinatorial:
63+
missing_wildcards = find_missing_wildcards(prompt)
64+
if missing_wildcards:
65+
wildcards = ", ".join(missing_wildcards)
66+
return DynamicPromptsResponse(prompts=[prompt], error=f"No values found for wildcard(s): {wildcards}")
67+
5568
try:
56-
error: Optional[str] = None
5769
if combinatorial:
5870
generator = CombinatorialPromptGenerator()
5971
prompts = generator.generate(prompt, max_prompts=max_prompts)

invokeai/app/invocations/flux_text_encoder.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from contextlib import ExitStack
2-
from typing import Iterator, Literal, Optional, Tuple, Union
2+
from typing import Iterator, Literal, Optional, Tuple
33

44
import torch
5-
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer, T5TokenizerFast
5+
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
66

77
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
88
from invokeai.app.invocations.fields import (
@@ -86,7 +86,7 @@ def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
8686
ExitStack() as exit_stack,
8787
):
8888
assert isinstance(t5_text_encoder, T5EncoderModel)
89-
assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
89+
assert isinstance(t5_tokenizer, T5Tokenizer)
9090

9191
# Determine if the model is quantized.
9292
# If the model is quantized, then we need to apply the LoRA weights as sidecar layers. This results in
@@ -186,7 +186,7 @@ def _t5_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelP
186186
def _log_t5_tokenization(
187187
self,
188188
context: InvocationContext,
189-
tokenizer: Union[T5Tokenizer, T5TokenizerFast],
189+
tokenizer: T5Tokenizer,
190190
) -> None:
191191
"""Logs the tokenization of a prompt for a T5-based model like FLUX."""
192192

invokeai/app/invocations/prompt.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from invokeai.app.invocations.fields import InputField, UIComponent
1010
from invokeai.app.invocations.primitives import StringCollectionOutput
1111
from invokeai.app.services.shared.invocation_context import InvocationContext
12+
from invokeai.app.util.dynamicprompts import find_missing_wildcards
1213

1314

1415
@invocation(
@@ -31,6 +32,13 @@ class DynamicPromptInvocation(BaseInvocation):
3132

3233
def invoke(self, context: InvocationContext) -> StringCollectionOutput:
3334
if self.combinatorial:
35+
# An unknown wildcard used as a variant value sends the combinatorial generator into an
36+
# infinite loop, so fail fast with a clear message instead of hanging the invocation. The
37+
# random generator handles unknown wildcards gracefully and needs no guard.
38+
missing_wildcards = find_missing_wildcards(self.prompt)
39+
if missing_wildcards:
40+
raise ValueError(f"No values found for wildcard(s): {', '.join(missing_wildcards)}")
41+
3442
generator = CombinatorialPromptGenerator()
3543
prompts = generator.generate(self.prompt, max_prompts=self.max_prompts)
3644
else:

invokeai/app/invocations/sd3_text_encoder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
CLIPTokenizer,
99
T5EncoderModel,
1010
T5Tokenizer,
11-
T5TokenizerFast,
1211
)
1312

1413
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
@@ -103,7 +102,7 @@ def _t5_encode(self, context: InvocationContext, max_seq_len: int) -> torch.Tens
103102
):
104103
context.util.signal_progress("Running T5 encoder")
105104
assert isinstance(t5_text_encoder, T5EncoderModel)
106-
assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
105+
assert isinstance(t5_tokenizer, T5Tokenizer)
107106
t5_device = get_effective_device(t5_text_encoder)
108107

109108
text_inputs = t5_tokenizer(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterator
4+
5+
from dynamicprompts.commands import (
6+
Command,
7+
SequenceCommand,
8+
VariantCommand,
9+
WildcardCommand,
10+
WrapCommand,
11+
)
12+
from dynamicprompts.parser.parse import parse
13+
from dynamicprompts.wildcards import WildcardManager
14+
from pyparsing import ParseException
15+
16+
17+
def _iter_wildcard_names(command: Command, in_variant: bool = False) -> Iterator[str]:
18+
"""Recursively yield the statically-known wildcard names that appear as (part of) a variant value.
19+
20+
Only wildcards reachable from a `VariantCommand` value are yielded: those are the references that
21+
hang the combinatorial generator (see `find_missing_wildcards`). The same wildcard used as plain
22+
literal text (e.g. `a __nope__ b`) generates fine, so it is intentionally not reported.
23+
"""
24+
if isinstance(command, WildcardCommand):
25+
# The wildcard name may itself be a dynamic Command (e.g. `__${var}__`). Only plain string
26+
# names can be validated ahead of time, so the dynamic case is intentionally skipped.
27+
if in_variant and isinstance(command.wildcard, str):
28+
yield command.wildcard
29+
elif isinstance(command, SequenceCommand):
30+
for token in command.tokens:
31+
yield from _iter_wildcard_names(token, in_variant)
32+
elif isinstance(command, VariantCommand):
33+
# Everything below a variant value is a variant-nested reference, even across sequences.
34+
for value in command.values:
35+
yield from _iter_wildcard_names(value, in_variant=True)
36+
elif isinstance(command, WrapCommand):
37+
yield from _iter_wildcard_names(command.wrapper, in_variant)
38+
yield from _iter_wildcard_names(command.inner, in_variant)
39+
# LiteralCommand and variable commands reference no wildcards we can resolve statically.
40+
41+
42+
def find_missing_wildcards(prompt: str, wildcard_manager: WildcardManager | None = None) -> list[str]:
43+
"""Return the unique unknown wildcard names in `prompt` that hang the combinatorial generator.
44+
45+
Referencing an unknown wildcard *as a variant value* (e.g. `{__nope__|x}`) makes dynamicprompts'
46+
combinatorial generator loop forever: its not-found fallback (`get_wildcard_not_found_fallback`)
47+
yields the wrapped wildcard infinitely, and the combinatorial variant logic dedupes those
48+
duplicates away without ever advancing. Detecting these names up front lets callers report a clear
49+
error instead of hanging. Only the combinatorial generator is affected, and only for wildcards
50+
nested in a variant — a bare `a __nope__ b` generates fine and is not reported.
51+
52+
Without a configured `wildcard_manager`, an empty one is used so that every referenced wildcard is
53+
treated as missing (wildcards are not resolved against any files here).
54+
"""
55+
if wildcard_manager is None:
56+
wildcard_manager = WildcardManager()
57+
58+
try:
59+
tree = parse(prompt)
60+
except ParseException:
61+
# Malformed prompts are surfaced separately by the generators; nothing to validate here.
62+
return []
63+
64+
missing: list[str] = []
65+
for name in _iter_wildcard_names(tree):
66+
if name not in missing and not wildcard_manager.get_values(name):
67+
missing.append(name)
68+
return missing

invokeai/backend/image_util/safety_checker.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
1111
from PIL import Image, ImageFilter
12-
from transformers import AutoFeatureExtractor
12+
from transformers import AutoImageProcessor
1313

1414
import invokeai.backend.util.logging as logger
1515
from invokeai.app.services.config.config_default import get_config
@@ -36,14 +36,14 @@ def _load_safety_checker(cls):
3636
try:
3737
model_path = get_config().models_path / CHECKER_PATH
3838
if model_path.exists():
39-
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
39+
cls.feature_extractor = AutoImageProcessor.from_pretrained(model_path)
4040
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path)
4141
else:
4242
model_path.mkdir(parents=True, exist_ok=True)
43-
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id)
44-
cls.feature_extractor.save_pretrained(model_path, safe_serialization=True)
43+
cls.feature_extractor = AutoImageProcessor.from_pretrained(repo_id)
44+
cls.feature_extractor.save_pretrained(model_path)
4545
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id)
46-
cls.safety_checker.save_pretrained(model_path, safe_serialization=True)
46+
cls.safety_checker.save_pretrained(model_path)
4747
except Exception as e:
4848
logger.warning(f"Could not load NSFW checker: {str(e)}")
4949

0 commit comments

Comments
 (0)