Skip to content

Commit 4018b30

Browse files
lsteinclaude
andcommitted
Modernize Compel for Transformers 5 and add diffusers smoke coverage
Apply upstream PR damian0815#129: - raise support floor to Python >=3.10 and align CI matrix - update transformers guidance to >=5,<6; pin pyparsing~=3.0 - migrate deprecated pyparsing API (delimited_list -> DelimitedList) - remove stray networkx import in convenience_wrappers - add public-API/CLIP/SD/SDXL/T5 smoke tests and diffusers prompt_embeds/pooled_prompt_embeds coverage (incl. opt-in local SDXL single-file checkpoint tests) - add provider tests for pooled output and BOS-less T5 behavior - README cleanup and example typo fixes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 951446d commit 4018b30

9 files changed

Lines changed: 425 additions & 17 deletions

File tree

.github/workflows/python-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2020

2121
steps:
2222
- uses: actions/checkout@v4

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Note that cross-attention control `.swap()` is currently ignored by Compel, but
1313

1414
`pip install compel`
1515

16+
Current main-branch support targets:
17+
18+
- Python `>=3.10`
19+
- `transformers >=5,<6`
20+
1621
## Documentation
1722

1823
Documentation is [here](doc/).
@@ -60,7 +65,7 @@ negative_prompt = "blurry, low quality, deformed"
6065
conditioning = compel(prompt, negative_prompt=negative_prompt)
6166

6267
# generate image
63-
images = pipeline(prompt_embeds=conditioning.embeds, negative_prompt_embed=conditioning.negative_embeds, num_inference_steps=20).images
68+
images = pipeline(prompt_embeds=conditioning.embeds, negative_prompt_embeds=conditioning.negative_embeds, num_inference_steps=20).images
6469
images[0].save("image.jpg")
6570
```
6671

@@ -79,7 +84,7 @@ negative_prompt = ["blurry, low quality, deformed", "painting"]
7984
conditioning = compel(prompt, negative_prompt=negative_prompt)
8085

8186
# generate image
82-
images = pipeline(prompt_embeds=conditioning.embeds, negative_prompt_embed=conditioning.negative_embeds, num_inference_steps=20).images
87+
images = pipeline(prompt_embeds=conditioning.embeds, negative_prompt_embeds=conditioning.negative_embeds, num_inference_steps=20).images
8388
images[0].save("image.jpg")
8489
```
8590

@@ -178,10 +183,26 @@ If this doesn't help, you could try this advice offered by @kshieh1:
178183
179184
See https://github.com/damian0815/compel/issues/24 for more details. Thanks @kshieh1 !
180185

186+
## Local Checkpoint Smoke Tests
187+
188+
The default test suite uses tiny local diffusers components so it stays fast and CI-friendly.
189+
190+
If you want to validate real local SDXL `.safetensors` checkpoints with `StableDiffusionXLPipeline.from_single_file(...)`,
191+
run:
192+
193+
```bash
194+
COMPEL_RUN_LOCAL_CHECKPOINT_TESTS=1 python -m unittest test.test_diffusers_smoke -v
195+
```
196+
197+
This opt-in path only uses local files and does not pull remote model weights.
198+
181199
## Changelog
182200

183201
#### 2.3.1 - Fix for 78 tokens / 77 tokens issue with SDXL; add `device` arg to `CompelFor*` constructors (thanks @dx2-66)
184202

203+
Current main-branch validation also includes opt-in local SDXL `.safetensors` single-file smoke tests for
204+
`StableDiffusionXLPipeline.from_single_file(...)`; see `COMPEL_RUN_LOCAL_CHECKPOINT_TESTS=1` above.
205+
185206
### 2.3.0 - Tokenization info, SplitLongTextMode CLS token handling, negative/style bugfixes
186207

187208
* `CompelFor*` objects now return tokenization info via `conditioning.tokenization_info` dict, which contains keys for `main_positive` and (where appropriate) `main_negative`, `style_positive` and `style_negative`.
@@ -352,4 +373,3 @@ negative_conditioning = compel.build_conditioning_tensor(negative_prompt)
352373
#### 0.1.8 - downgrade Python min version to 3.7
353374

354375
#### 0.1.7 - InvokeAI compatibility
355-

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ authors = [
66
]
77
description = "A prompting enhancement library for transformers-type text embedding systems."
88
readme = "README.md"
9-
requires-python = ">=3.7"
9+
requires-python = ">=3.10"
1010
classifiers = [
1111
"Programming Language :: Python :: 3",
12+
"Programming Language :: Python :: 3 :: Only",
13+
"Programming Language :: Python :: 3.10",
14+
"Programming Language :: Python :: 3.11",
15+
"Programming Language :: Python :: 3.12",
16+
"Programming Language :: Python :: 3.13",
1217
"License :: OSI Approved :: MIT License",
1318
"Operating System :: OS Independent",
1419
]
@@ -17,7 +22,7 @@ dependencies = [
1722
"notebook>=6.5.7",
1823
"pyparsing ~= 3.0",
1924
"torch",
20-
"transformers ~= 4.25",
25+
"transformers >= 5, < 6",
2126
]
2227

2328
[project.urls]
@@ -27,4 +32,3 @@ dependencies = [
2732
[build-system]
2833
requires = ["setuptools>=61.0"]
2934
build-backend = "setuptools.build_meta"
30-

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pyparsing
1+
pyparsing~=3.0
22
torch
3-
transformers
4-
diffusers
3+
transformers>=5,<6
4+
diffusers

src/compel/convenience_wrappers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import torch
55
from diffusers import FluxPipeline, StableDiffusionXLPipeline, StableDiffusionPipeline
6-
from networkx.algorithms.shortest_paths.weighted import negative_edge_cycle
76

87
import compel.embeddings_provider
98
from compel import Compel, ReturnedEmbeddingsType, BaseTextualInversionManager
@@ -213,4 +212,4 @@ def _duplicate_negative_conditioning_if_required(embeds: torch.Tensor, negative_
213212
else:
214213
embeds = combined_embeds[0:negative_start_index]
215214
negative_embeds = combined_embeds[negative_start_index:]
216-
return embeds, negative_embeds
215+
return embeds, negative_embeds

src/compel/prompt_parser.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ def parse_fragment_str(x, expression: pp.ParserElement, in_quotes: bool = False)
595595
keyword # flag
596596
]))
597597
# options for an operator, eg "s_start=0.1, 0.3, no_normalize"
598-
options = pp.Dict(pp.Optional(pp.delimited_list(option)))
598+
options = pp.Dict(pp.Optional(pp.DelimitedList(option)))
599599
options.set_name('options')
600600
options.set_debug(False)
601601

@@ -663,7 +663,7 @@ def parse_fragment_str(x, expression: pp.ParserElement, in_quotes: bool = False)
663663
# a blend/lerp between the feature vectors for two or more prompts
664664
blend = (
665665
lparen
666-
+ pp.Group(pp.delimited_list(pp.Group(potential_operator_target | quoted_prompt), min=1)).set_name('bl-target').set_debug(False)
666+
+ pp.Group(pp.DelimitedList(pp.Group(potential_operator_target | quoted_prompt), min=1)).set_name('bl-target').set_debug(False)
667667
+ rparen
668668
+ pp.Literal(".blend").set_name('bl-operator').set_debug(False)
669669
+ lparen
@@ -677,7 +677,7 @@ def parse_fragment_str(x, expression: pp.ParserElement, in_quotes: bool = False)
677677
# an operator to direct stable diffusion to step multiple times, once for each target, and then add the results together with different weights
678678
explicit_conjunction = (
679679
lparen
680-
+ pp.Group(pp.delimited_list(pp.Group(potential_operator_target | quoted_prompt), min=1)).set_name('cj-target').set_debug(False)
680+
+ pp.Group(pp.DelimitedList(pp.Group(potential_operator_target | quoted_prompt), min=1)).set_name('cj-target').set_debug(False)
681681
+ rparen
682682
+ pp.one_of([".and", ".add"]).set_name('cj-operator').set_debug(False)
683683
+ lparen
@@ -696,4 +696,3 @@ def parse_fragment_str(x, expression: pp.ParserElement, in_quotes: bool = False)
696696
conjunction = (explicit_conjunction | implicit_conjunction)
697697

698698
return conjunction, prompt
699-

test/test_diffusers_smoke.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import gc
2+
import os
3+
import unittest
4+
from pathlib import Path
5+
6+
import torch
7+
from diffusers import AutoencoderKL, EulerDiscreteScheduler, StableDiffusionPipeline, StableDiffusionXLPipeline, UNet2DConditionModel
8+
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTextModelWithProjection
9+
10+
from compel import CompelForSD, CompelForSDXL
11+
12+
try:
13+
from prompting_test_utils import DummyTokenizer
14+
except ModuleNotFoundError:
15+
from test.prompting_test_utils import DummyTokenizer
16+
17+
18+
LOCAL_SDXL_CHECKPOINTS = {
19+
"sd_xl_turbo": Path("/mnt/s/Code/Models/ImageGen/CUI-Archived/checkpoints/sd_xl_turbo_1.0_fp16.safetensors"),
20+
"cyberrealisticXL_v80": Path("/mnt/s/Code/Models/ImageGen/CUI-Archived/checkpoints/cyberrealisticXL_v80_fp16.safetensors"),
21+
}
22+
RUN_LOCAL_CHECKPOINT_TESTS = os.getenv("COMPEL_RUN_LOCAL_CHECKPOINT_TESTS") == "1"
23+
24+
25+
def make_clip_text_config(hidden_size: int = 32, projection_dim: int = 32, max_position_embeddings: int = 16) -> CLIPTextConfig:
26+
return CLIPTextConfig(
27+
vocab_size=32,
28+
hidden_size=hidden_size,
29+
intermediate_size=37,
30+
projection_dim=projection_dim,
31+
num_hidden_layers=2,
32+
num_attention_heads=4,
33+
max_position_embeddings=max_position_embeddings,
34+
bos_token_id=10,
35+
pad_token_id=11,
36+
eos_token_id=12,
37+
)
38+
39+
40+
def make_tiny_vae() -> AutoencoderKL:
41+
return AutoencoderKL(
42+
in_channels=3,
43+
out_channels=3,
44+
down_block_types=["DownEncoderBlock2D"],
45+
up_block_types=["UpDecoderBlock2D"],
46+
block_out_channels=[32],
47+
layers_per_block=1,
48+
latent_channels=4,
49+
norm_num_groups=8,
50+
sample_size=32,
51+
)
52+
53+
54+
def make_tiny_sd_unet(cross_attention_dim: int) -> UNet2DConditionModel:
55+
return UNet2DConditionModel(
56+
sample_size=32,
57+
in_channels=4,
58+
out_channels=4,
59+
layers_per_block=1,
60+
block_out_channels=(32, 64),
61+
down_block_types=("CrossAttnDownBlock2D", "DownBlock2D"),
62+
up_block_types=("UpBlock2D", "CrossAttnUpBlock2D"),
63+
cross_attention_dim=cross_attention_dim,
64+
attention_head_dim=(4, 8),
65+
norm_num_groups=8,
66+
)
67+
68+
69+
def make_tiny_sdxl_unet(cross_attention_dim: int, projection_dim: int) -> UNet2DConditionModel:
70+
return UNet2DConditionModel(
71+
sample_size=32,
72+
in_channels=4,
73+
out_channels=4,
74+
layers_per_block=1,
75+
block_out_channels=(32, 64),
76+
down_block_types=("CrossAttnDownBlock2D", "DownBlock2D"),
77+
up_block_types=("UpBlock2D", "CrossAttnUpBlock2D"),
78+
cross_attention_dim=cross_attention_dim,
79+
attention_head_dim=(4, 8),
80+
norm_num_groups=8,
81+
addition_embed_type="text_time",
82+
addition_time_embed_dim=8,
83+
projection_class_embeddings_input_dim=(8 * 6) + projection_dim,
84+
)
85+
86+
87+
class DiffusersSmokeTestCase(unittest.TestCase):
88+
def test_stable_diffusion_pipeline_accepts_compel_prompt_embeds(self):
89+
tokenizer = DummyTokenizer(model_max_length=16)
90+
text_encoder = CLIPTextModel(make_clip_text_config(hidden_size=32, projection_dim=32, max_position_embeddings=16))
91+
pipe = StableDiffusionPipeline(
92+
vae=make_tiny_vae(),
93+
text_encoder=text_encoder,
94+
tokenizer=tokenizer,
95+
unet=make_tiny_sd_unet(cross_attention_dim=32),
96+
scheduler=EulerDiscreteScheduler(num_train_timesteps=10, steps_offset=1),
97+
safety_checker=None,
98+
feature_extractor=None,
99+
requires_safety_checker=False,
100+
).to("cpu")
101+
pipe.set_progress_bar_config(disable=True)
102+
103+
conditioning = CompelForSD(pipe)("a b c", negative_prompt="a")
104+
result = pipe(
105+
prompt_embeds=conditioning.embeds,
106+
negative_prompt_embeds=conditioning.negative_embeds,
107+
num_inference_steps=1,
108+
guidance_scale=2.0,
109+
output_type="np",
110+
)
111+
112+
self.assertEqual(conditioning.embeds.shape, (1, 16, 32))
113+
self.assertEqual(conditioning.negative_embeds.shape, (1, 16, 32))
114+
self.assertEqual(len(result.images), 1)
115+
self.assertEqual(result.images[0].shape, (32, 32, 3))
116+
117+
def test_sdxl_pipeline_accepts_compel_prompt_and_pooled_embeds(self):
118+
tokenizer = DummyTokenizer(model_max_length=16)
119+
text_encoder = CLIPTextModel(make_clip_text_config(hidden_size=32, projection_dim=16, max_position_embeddings=16))
120+
text_encoder_2 = CLIPTextModelWithProjection(
121+
make_clip_text_config(hidden_size=32, projection_dim=16, max_position_embeddings=16)
122+
)
123+
pipe = StableDiffusionXLPipeline(
124+
vae=make_tiny_vae(),
125+
text_encoder=text_encoder,
126+
text_encoder_2=text_encoder_2,
127+
tokenizer=tokenizer,
128+
tokenizer_2=DummyTokenizer(model_max_length=16),
129+
unet=make_tiny_sdxl_unet(cross_attention_dim=64, projection_dim=16),
130+
scheduler=EulerDiscreteScheduler(num_train_timesteps=10, steps_offset=1),
131+
image_encoder=None,
132+
feature_extractor=None,
133+
).to("cpu")
134+
pipe.set_progress_bar_config(disable=True)
135+
136+
conditioning = CompelForSDXL(pipe)(
137+
"a b c",
138+
style_prompt="b c",
139+
negative_prompt="a",
140+
negative_style_prompt="a",
141+
)
142+
result = pipe(
143+
prompt_embeds=conditioning.embeds,
144+
pooled_prompt_embeds=conditioning.pooled_embeds,
145+
negative_prompt_embeds=conditioning.negative_embeds,
146+
negative_pooled_prompt_embeds=conditioning.negative_pooled_embeds,
147+
num_inference_steps=1,
148+
guidance_scale=2.0,
149+
output_type="np",
150+
)
151+
152+
self.assertEqual(conditioning.embeds.shape, (1, 16, 64))
153+
self.assertEqual(conditioning.pooled_embeds.shape, (1, 16))
154+
self.assertEqual(conditioning.negative_embeds.shape, (1, 16, 64))
155+
self.assertEqual(conditioning.negative_pooled_embeds.shape, (1, 16))
156+
self.assertEqual(len(result.images), 1)
157+
self.assertEqual(result.images[0].shape, (32, 32, 3))
158+
159+
160+
@unittest.skipUnless(RUN_LOCAL_CHECKPOINT_TESTS, "set COMPEL_RUN_LOCAL_CHECKPOINT_TESTS=1 to run local checkpoint smoke tests")
161+
class LocalCheckpointSmokeTestCase(unittest.TestCase):
162+
def test_sdxl_single_file_checkpoints_load_locally(self):
163+
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
164+
165+
for checkpoint_name, checkpoint_path in LOCAL_SDXL_CHECKPOINTS.items():
166+
with self.subTest(checkpoint=checkpoint_name):
167+
self.assertTrue(checkpoint_path.exists(), f"missing local checkpoint: {checkpoint_path}")
168+
169+
pipe = StableDiffusionXLPipeline.from_single_file(
170+
str(checkpoint_path),
171+
local_files_only=True,
172+
torch_dtype=dtype,
173+
)
174+
pipe.set_progress_bar_config(disable=True)
175+
176+
self.assertIsInstance(pipe, StableDiffusionXLPipeline)
177+
self.assertIsNotNone(pipe.text_encoder)
178+
self.assertIsNotNone(pipe.text_encoder_2)
179+
self.assertGreater(pipe.unet.config.cross_attention_dim, 0)
180+
181+
del pipe
182+
gc.collect()
183+
184+
185+
if __name__ == "__main__":
186+
unittest.main()

0 commit comments

Comments
 (0)