Text-to-Image
Diffusers
English
structured-diffusion-guidance
image-generation
Eval Results (legacy)
Instructions to use creative-graphic-design/structure-diffusion with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use creative-graphic-design/structure-diffusion with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("creative-graphic-design/structure-diffusion", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
Publish Structure Diffusion pipeline at 647bfc6e796a
Browse files- README.md +318 -0
- modeling_structure_diffusion.py +369 -0
- pipeline.py +499 -0
- processing_structure_diffusion.py +204 -0
README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- en
|
| 4 |
+
license: apache-2.0
|
| 5 |
+
library_name: diffusers
|
| 6 |
+
pipeline_tag: text-to-image
|
| 7 |
+
tags:
|
| 8 |
+
- structured-diffusion-guidance
|
| 9 |
+
- text-to-image
|
| 10 |
+
- image-generation
|
| 11 |
+
datasets: []
|
| 12 |
+
base_model: CompVis/stable-diffusion-v1-4
|
| 13 |
+
model-index:
|
| 14 |
+
- name: "StructureDiffusion"
|
| 15 |
+
results:
|
| 16 |
+
- task:
|
| 17 |
+
type: "text-to-image"
|
| 18 |
+
name: "Text-to-image generation"
|
| 19 |
+
dataset:
|
| 20 |
+
type: "synthetic"
|
| 21 |
+
name: "CPU parity fixture"
|
| 22 |
+
split: "one prompt at 64x64 and 512x512"
|
| 23 |
+
metrics:
|
| 24 |
+
- type: "vendor-parity"
|
| 25 |
+
value: 0
|
| 26 |
+
name: "Bit-exact CPU image comparison against reference images"
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
<!-- --8<-- [start:card] -->
|
| 30 |
+
|
| 31 |
+
# Model Card for StructureDiffusion
|
| 32 |
+
|
| 33 |
+
[](https://arxiv.org/abs/2212.05032)
|
| 34 |
+

|
| 35 |
+

|
| 36 |
+

|
| 37 |
+

|
| 38 |
+

|
| 39 |
+

|
| 40 |
+
|
| 41 |
+
This package implements [StructureDiffusion](https://arxiv.org/abs/2212.05032), a training-free text-to-image method from ICLR 2023. It parses a prompt into noun phrases, encodes the full prompt and each phrase, then combines cross-attention keys and values to improve object and attribute binding. The [`🧨diffusers`](https://huggingface.co/docs/diffusers/index) pipeline uses the [Stable Diffusion 1.4 checkpoint](https://huggingface.co/CompVis/stable-diffusion-v1-4).
|
| 42 |
+
|
| 43 |
+
## Model Details
|
| 44 |
+
|
| 45 |
+
### Model Description
|
| 46 |
+
|
| 47 |
+
`StructureDiffusionPipeline` subclasses [`diffusers.StableDiffusionPipeline`](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/text2img), so it uses the framework's standard loading, device placement, callbacks, scheduler, and image output behavior. `parser_type` selects constituency parsing or optional scene-graph parsing. `conjunction=True` uses phrase-specific keys with one combined value; the default uses the full prompt as the key and the full prompt plus phrase encodings as values.
|
| 48 |
+
|
| 49 |
+
- **Developed by:** Weixi Feng et al.
|
| 50 |
+
- **Shared by:** creative-graphic-design.
|
| 51 |
+
- **Model type:** image generation; task: text-to-image; conditioning: text.
|
| 52 |
+
- **Language(s):** English prompts.
|
| 53 |
+
- **Code license:** Apache-2.0.
|
| 54 |
+
- **Reference implementation license:** MIT.
|
| 55 |
+
- **Base checkpoint:** CompVis Stable Diffusion v1.4; its weights have separate CreativeML OpenRAIL-M terms.
|
| 56 |
+
|
| 57 |
+
### Model Sources
|
| 58 |
+
|
| 59 |
+
- **Repository:** [official Structured Diffusion Guidance implementation](https://github.com/weixi-feng/Structured-Diffusion-Guidance/tree/773b2ba068c6cc60443d5d747a725413c8c9d46f)
|
| 60 |
+
- **Paper:** [arXiv:2212.05032](https://arxiv.org/abs/2212.05032)
|
| 61 |
+
- **Released base weights:** [CompVis/stable-diffusion-v1-4](https://huggingface.co/CompVis/stable-diffusion-v1-4)
|
| 62 |
+
|
| 63 |
+
## Supported Checkpoints
|
| 64 |
+
|
| 65 |
+
| Checkpoint | Hub ID | Status |
|
| 66 |
+
| ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------ |
|
| 67 |
+
| Stable Diffusion v1.4 | `CompVis/stable-diffusion-v1-4` | Gated base checkpoint; load with `from_pretrained` after access is approved. |
|
| 68 |
+
| Structure Diffusion package artifact | `creative-graphic-design/structure-diffusion` | Published code only; this package does not upload Structure Diffusion weights. |
|
| 69 |
+
|
| 70 |
+
## Uses
|
| 71 |
+
|
| 72 |
+
### Direct Use
|
| 73 |
+
|
| 74 |
+
Use the pipeline for research inference on English prompts that describe multiple objects, attributes, or relations. The method changes cross-attention conditioning and does not train or fine-tune Stable Diffusion weights.
|
| 75 |
+
|
| 76 |
+
### Downstream Use
|
| 77 |
+
|
| 78 |
+
Use generated images as draft visual material for creative-design workflows after inspecting them for prompt adherence and content suitability.
|
| 79 |
+
|
| 80 |
+
### Out-of-Scope Use
|
| 81 |
+
|
| 82 |
+
Do not treat generated images as verified depictions of real people or events, or use them as evidence that prompt attributes were rendered correctly. The pipeline does not provide an image safety guarantee.
|
| 83 |
+
|
| 84 |
+
## Bias, Risks, and Limitations
|
| 85 |
+
|
| 86 |
+
Generation behavior inherits the training data, biases, and restrictions of the selected Stable Diffusion checkpoint. The default constituency parser is English-only and requires the [Stanza](https://stanfordnlp.github.io/stanza/) English tokenizer, POS, and constituency models to be installed separately. Scene-graph parsing is an optional extra with its own [spaCy](https://spacy.io/) English model prerequisite. Outputs vary with the checkpoint, parser, scheduler, device, precision, and library versions.
|
| 87 |
+
|
| 88 |
+
### Recommendations
|
| 89 |
+
|
| 90 |
+
Inspect generated images before downstream use, and record the checkpoint revision, parser, scheduler, dimensions, seed, and inference steps when comparing results.
|
| 91 |
+
|
| 92 |
+
## How to Get Started with the Model
|
| 93 |
+
|
| 94 |
+
Install the package from this repository archive and download the English parser models before the first inference:
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
pip install "structure-diffusion @ https://github.com/creative-graphic-design/design-generators/archive/647bfc6e796a36794bb7141047dfc6c1d930679a.zip#subdirectory=models/structure-diffusion"
|
| 98 |
+
python -c "import stanza; stanza.download('en', processors='tokenize,pos,constituency')"
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
[`CompVis/stable-diffusion-v1-4`](https://huggingface.co/CompVis/stable-diffusion-v1-4) is a gated model. Request access and accept its CreativeML OpenRAIL-M terms on the model page before downloading its weights. Set `HF_TOKEN_FILE` to a private file containing a token that can access the checkpoint.
|
| 102 |
+
|
| 103 |
+
```python
|
| 104 |
+
import os
|
| 105 |
+
from pathlib import Path
|
| 106 |
+
|
| 107 |
+
import torch
|
| 108 |
+
from diffusers import DiffusionPipeline
|
| 109 |
+
|
| 110 |
+
token = Path(os.environ["HF_TOKEN_FILE"]).read_text().strip()
|
| 111 |
+
pipe = DiffusionPipeline.from_pretrained(
|
| 112 |
+
"CompVis/stable-diffusion-v1-4",
|
| 113 |
+
revision="133a221b8aa7292a167afc5127cb63fb5005638b",
|
| 114 |
+
custom_pipeline="creative-graphic-design/structure-diffusion",
|
| 115 |
+
torch_dtype=torch.float32,
|
| 116 |
+
token=token,
|
| 117 |
+
trust_remote_code=True,
|
| 118 |
+
)
|
| 119 |
+
pipe.to("cpu")
|
| 120 |
+
result = pipe(
|
| 121 |
+
"A red car and a white sheep",
|
| 122 |
+
parser_type="constituency",
|
| 123 |
+
height=512,
|
| 124 |
+
width=512,
|
| 125 |
+
num_inference_steps=10,
|
| 126 |
+
generator=torch.Generator(device="cpu").manual_seed(42),
|
| 127 |
+
)
|
| 128 |
+
image = result.images[0]
|
| 129 |
+
print("image size: {}x{}".format(image.size[0], image.size[1]))
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
```text
|
| 133 |
+
image size: 512x512
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
The default constituency parser needs the English Stanza model assets downloaded separately with the command above. The optional scene-graph parser is available through the scene-graph extra and requires its spaCy English model. See [REPRODUCING.md](https://github.com/creative-graphic-design/design-generators/blob/main/models/structure-diffusion/REPRODUCING.md) for local testing and CPU comparison instructions.
|
| 137 |
+
|
| 138 |
+
## Examples
|
| 139 |
+
|
| 140 |
+
The gallery compares `diffusers.StableDiffusionPipeline` with `StructureDiffusionPipeline` using the same prompt and seed. It includes every output for seeds `42`, `0`, `1`, and `2` for each prompt. The results vary by seed and do not show a consistent compositional improvement from structured guidance. For the car and sheep prompt, the sheep is absent from both seed-42 images; at seed 0 the structured result shows only part of a sheep, at seed 1 the baseline shows a sheep while the structured result does not, and both show a sheep at seed 2. For the backpack and elephant prompt, the baseline shows both objects while the structured result omits the elephant at seed 42; both omit the backpack at seed 0 and the elephant at seed 1; at seed 2 both show the objects, with the elephant more fully visible in the structured result. For the fruit prompt, neither pipeline clearly shows a banana at seeds 42, 0, or 1; at seed 2 both show several apples and banana-like shapes. The prompts are taken from Figure 1 (car and sheep; backpack and elephant) and Figure 13 (fruit examples) of the [paper](https://openreview.net/pdf?id=PUIqjT4rzq7). All outputs are shown without selecting among seeds.
|
| 141 |
+
|
| 142 |
+
### A red car and a white sheep
|
| 143 |
+
|
| 144 |
+
Source: [Figure 1, attribute leakage](https://openreview.net/pdf?id=PUIqjT4rzq7).
|
| 145 |
+
|
| 146 |
+
| Seed | Stable Diffusion v1.4 | StructureDiffusion |
|
| 147 |
+
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 148 |
+
| 42 |  |  |
|
| 149 |
+
| 0 |  |  |
|
| 150 |
+
| 1 |  |  |
|
| 151 |
+
| 2 |  |  |
|
| 152 |
+
|
| 153 |
+
### A blue backpack and a brown elephant
|
| 154 |
+
|
| 155 |
+
Source: [Figure 1, missing objects](https://openreview.net/pdf?id=PUIqjT4rzq7).
|
| 156 |
+
|
| 157 |
+
| Seed | Stable Diffusion v1.4 | StructureDiffusion |
|
| 158 |
+
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 159 |
+
| 42 |  |  |
|
| 160 |
+
| 0 |  |  |
|
| 161 |
+
| 1 |  |  |
|
| 162 |
+
| 2 |  |  |
|
| 163 |
+
|
| 164 |
+
### A green apple and a red banana
|
| 165 |
+
|
| 166 |
+
Source: [Figure 13, CC-500 qualitative results](https://openreview.net/pdf?id=PUIqjT4rzq7).
|
| 167 |
+
|
| 168 |
+
| Seed | Stable Diffusion v1.4 | StructureDiffusion |
|
| 169 |
+
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 170 |
+
| 42 |  |  |
|
| 171 |
+
| 0 |  |  |
|
| 172 |
+
| 1 |  |  |
|
| 173 |
+
| 2 |  |  |
|
| 174 |
+
|
| 175 |
+
Every image uses `CompVis/stable-diffusion-v1-4` at revision `133a221b8aa7292a167afc5127cb63fb5005638b`, the checkpoint's PNDM scheduler configuration (`skip_prk_steps=True`), 30 inference steps, 512×512 resolution, guidance scale `7.5`, and CPU float32. The structured pipeline uses the constituency parser with `conjunction=True`; the baseline uses `diffusers.StableDiffusionPipeline`. The base checkpoint weights are licensed under CreativeML OpenRAIL-M. The image files are hosted in the public documentation-images dataset and are not committed to this repository.
|
| 176 |
+
|
| 177 |
+
This code recreates all displayed pairs with the same prompts and seeds. Set `HF_TOKEN_FILE` to a private file containing a Hugging Face token after accepting the checkpoint's access terms. Install the package and download the English Stanza parser models as shown above before running it.
|
| 178 |
+
|
| 179 |
+
```python
|
| 180 |
+
import gc
|
| 181 |
+
import os
|
| 182 |
+
from pathlib import Path
|
| 183 |
+
|
| 184 |
+
import torch
|
| 185 |
+
from diffusers import StableDiffusionPipeline
|
| 186 |
+
from structure_diffusion import StructureDiffusionPipeline
|
| 187 |
+
|
| 188 |
+
MODEL_ID = "CompVis/stable-diffusion-v1-4"
|
| 189 |
+
MODEL_REVISION = "133a221b8aa7292a167afc5127cb63fb5005638b"
|
| 190 |
+
OUTPUT_DIR = Path("structure-diffusion-examples")
|
| 191 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 192 |
+
EXAMPLES = (
|
| 193 |
+
("red-car-white-sheep", "A red car and a white sheep"),
|
| 194 |
+
("blue-backpack-brown-elephant", "A blue backpack and a brown elephant"),
|
| 195 |
+
("green-apple-red-banana", "A green apple and a red banana"),
|
| 196 |
+
)
|
| 197 |
+
SEEDS = (42, 0, 1, 2)
|
| 198 |
+
|
| 199 |
+
torch.set_num_threads(2)
|
| 200 |
+
token = Path(os.environ["HF_TOKEN_FILE"]).read_text().strip()
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def make_generator(seed):
|
| 204 |
+
return torch.Generator(device="cpu").manual_seed(seed)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
baseline = StableDiffusionPipeline.from_pretrained(
|
| 208 |
+
MODEL_ID,
|
| 209 |
+
revision=MODEL_REVISION,
|
| 210 |
+
torch_dtype=torch.float32,
|
| 211 |
+
token=token,
|
| 212 |
+
)
|
| 213 |
+
baseline.to("cpu")
|
| 214 |
+
for slug, prompt in EXAMPLES:
|
| 215 |
+
for seed in SEEDS:
|
| 216 |
+
image = baseline(
|
| 217 |
+
prompt,
|
| 218 |
+
height=512,
|
| 219 |
+
width=512,
|
| 220 |
+
num_inference_steps=30,
|
| 221 |
+
guidance_scale=7.5,
|
| 222 |
+
generator=make_generator(seed),
|
| 223 |
+
).images[0]
|
| 224 |
+
image.save(OUTPUT_DIR / f"stable-diffusion-v1-4-{slug}-seed{seed}.png")
|
| 225 |
+
del baseline
|
| 226 |
+
|
| 227 |
+
gc.collect()
|
| 228 |
+
structured = StructureDiffusionPipeline.from_pretrained(
|
| 229 |
+
MODEL_ID,
|
| 230 |
+
revision=MODEL_REVISION,
|
| 231 |
+
torch_dtype=torch.float32,
|
| 232 |
+
token=token,
|
| 233 |
+
)
|
| 234 |
+
structured.to("cpu")
|
| 235 |
+
for slug, prompt in EXAMPLES:
|
| 236 |
+
for seed in SEEDS:
|
| 237 |
+
image = structured(
|
| 238 |
+
prompt,
|
| 239 |
+
parser_type="constituency",
|
| 240 |
+
conjunction=True,
|
| 241 |
+
height=512,
|
| 242 |
+
width=512,
|
| 243 |
+
num_inference_steps=30,
|
| 244 |
+
guidance_scale=7.5,
|
| 245 |
+
generator=make_generator(seed),
|
| 246 |
+
).images[0]
|
| 247 |
+
image.save(OUTPUT_DIR / f"structure-diffusion-{slug}-seed{seed}.png")
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
## Training Details
|
| 251 |
+
|
| 252 |
+
### Training Data
|
| 253 |
+
|
| 254 |
+
This method adds no training data or learned weights. The selected Stable Diffusion checkpoint retains its original data provenance and terms.
|
| 255 |
+
|
| 256 |
+
### Training Procedure
|
| 257 |
+
|
| 258 |
+
No training or checkpoint conversion is performed by this package. It loads the released pipeline-format base checkpoint and changes the denoising pipeline's text-conditioning path.
|
| 259 |
+
|
| 260 |
+
## Evaluation
|
| 261 |
+
|
| 262 |
+
### Testing Data, Factors & Metrics
|
| 263 |
+
|
| 264 |
+
The independent CPU comparison covers 64×64 and 512×512 outputs for `A red car and a white sheep`, seed 42, 10 PNDM steps through the PLMS path in `diffusers`, guidance scale 7.5, and CPU float32 execution. Each reference/package pair receives the same precomputed starting latent. The vendor demo calls `preprocess_prompts` before parsing and token alignment, which lowercases the prompt; its `get_token_alignment_map` helper is case-sensitive only when called directly with mismatched prompt and tokenizer casing. The package also accepts capitalized input.
|
| 265 |
+
|
| 266 |
+
The test directly compares vendor-derived noun phrases and token spans, and compares independently assembled key/value conditioning with the package conditioning. For the image comparison, a separate base `diffusers` pipeline uses the pinned vendor attention processors to generate the reference image; the test compares that final image with the `StructureDiffusionPipeline` output. It does not capture or compare per-layer attention outputs. Both model loading and sampling use `diffusers`, so this checks structured conditioning and its end-to-end image result without reproducing the vendor checkpoint loader or sampler implementation.
|
| 267 |
+
|
| 268 |
+
### Parity Results
|
| 269 |
+
|
| 270 |
+
| Check | Cases | Criterion | Result |
|
| 271 |
+
| ---------------------------------------------------- | ----: | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 272 |
+
| 64×64 CPU image output against the reference image | 1 | Exact phrase and token-span match; K/V max difference ≤ `1e-6`; image MAE < `1e-3` | `a red car` / `a white sheep`; spans `[1, 4)` / `[5, 8)`; 1 key / 3 values; K/V max differences `0.0` / `0.0`; pixel max/mean/RMS differences `0.0` / `0.0` / `0.0`; bit-exact, SHA-256 match. |
|
| 273 |
+
| 512×512 CPU image output against the reference image | 1 | Exact phrase and token-span match; K/V max difference ≤ `1e-6`; image MAE < `1e-3` | `a red car` / `a white sheep`; spans `[1, 4)` / `[5, 8)`; 1 key / 3 values; K/V max differences `0.0` / `0.0`; pixel max/mean/RMS differences `0.0` / `0.0` / `0.0`; bit-exact, SHA-256 match. |
|
| 274 |
+
|
| 275 |
+
The independent comparison ran on commit `786fcf6cfbcfb68b9c9565d8ee275c32cd5a28ac`; the reviewer recorded the full inputs, initial latents, measurements, and runtimes in the [formal parity result](https://github.com/creative-graphic-design/design-generators/issues/485#issuecomment-6608854871). Both resolutions were bit-exact: the package and reference image SHA-256 values matched.
|
| 276 |
+
|
| 277 |
+
## Reproducibility
|
| 278 |
+
|
| 279 |
+
Run the CPU comparison test in [REPRODUCING.md](https://github.com/creative-graphic-design/design-generators/blob/main/models/structure-diffusion/REPRODUCING.md) to compare noun phrases, token spans, and K/V conditioning directly, then compare final generated images using the same scheduler, seed, and initial latent. A separate base `diffusers` pipeline runs the pinned reference attention processors to generate the reference image; the test does not compare per-layer attention outputs. `diffusers` supplies model loading and sampling in both paths, so the result does not claim parity with the reference checkpoint loader or sampler. The independently reviewed comparison on commit `786fcf6` matched phrase and span outputs exactly, had zero measured K/V differences, and produced bit-exact 64×64 and 512×512 images with identical SHA-256 values.
|
| 280 |
+
|
| 281 |
+
## Environmental Impact
|
| 282 |
+
|
| 283 |
+
No model training is performed. The formal standard-CPU comparison took 126.777 seconds at 64×64 and 948.902 seconds at 512×512; no GPU comparison is included.
|
| 284 |
+
|
| 285 |
+
## Technical Specifications
|
| 286 |
+
|
| 287 |
+
### Model Architecture and Objective
|
| 288 |
+
|
| 289 |
+
The pipeline retains Stable Diffusion's VAE, CLIP text encoder, U-Net, and scheduler. It extracts phrase spans, encodes each phrase independently, aligns phrase tokens to the full prompt, and applies the official method's attention-map and value-composition rules in U-Net cross-attention layers.
|
| 290 |
+
|
| 291 |
+
### Compute Infrastructure
|
| 292 |
+
|
| 293 |
+
The package supports CPU and accelerator execution with `diffusers`. The recorded comparison timings are test-process runtimes on a standard Colab CPU; setup time and GPU performance are not included.
|
| 294 |
+
|
| 295 |
+
#### Software
|
| 296 |
+
|
| 297 |
+
Runtime dependencies are declared in `models/structure-diffusion/pyproject.toml`. The default parser requires Stanza models; scene-graph parsing also requires the optional `scene-graph` extra and its spaCy model.
|
| 298 |
+
|
| 299 |
+
## Citation
|
| 300 |
+
|
| 301 |
+
```bibtex
|
| 302 |
+
@inproceedings{feng2023trainingfree,
|
| 303 |
+
title={Training-Free Structured Diffusion Guidance for Compositional Text-to-Image Synthesis},
|
| 304 |
+
author={Feng, Weixi and He, Xuehai and Fu, Tsu-Jui and Jampani, Varun and Akula, Arjun and Narayana, Pradyumna and Basu, Sugato and Wang, Xin Eric and Wang, William Yang},
|
| 305 |
+
booktitle={International Conference on Learning Representations},
|
| 306 |
+
year={2023},
|
| 307 |
+
eprint={2212.05032},
|
| 308 |
+
archivePrefix={arXiv},
|
| 309 |
+
primaryClass={cs.CV},
|
| 310 |
+
url = "https://arxiv.org/abs/2212.05032"
|
| 311 |
+
}
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
## Model Card Contact
|
| 315 |
+
|
| 316 |
+
Open an issue or pull request in the [design-generators repository](https://github.com/creative-graphic-design/design-generators).
|
| 317 |
+
|
| 318 |
+
<!-- --8<-- [end:card] -->
|
modeling_structure_diffusion.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cross-attention processor for structured diffusion guidance."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from diffusers.models.attention_processor import Attention
|
| 9 |
+
from jaxtyping import Bool, Float
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class StructuredConditioning:
|
| 14 |
+
"""Full-prompt keys, phrase-composed values, and per-example validity."""
|
| 15 |
+
|
| 16 |
+
keys: tuple[Float[torch.Tensor, "batch tokens dim"], ...]
|
| 17 |
+
values: tuple[Float[torch.Tensor, "batch tokens dim"], ...]
|
| 18 |
+
key_validity: tuple[Bool[torch.Tensor, "batch"], ...]
|
| 19 |
+
value_validity: tuple[Bool[torch.Tensor, "batch"], ...]
|
| 20 |
+
do_classifier_free_guidance: bool
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class StructuredAttentionProcessor:
|
| 24 |
+
"""Apply noun-phrase key/value attention in a Diffusers cross-attention layer."""
|
| 25 |
+
|
| 26 |
+
def __init__(self, layer_name: str, save_attention_maps: bool) -> None:
|
| 27 |
+
"""Create per-layer state for the requested attention-map behavior.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
layer_name: U-Net attention processor name used to label saved maps.
|
| 31 |
+
save_attention_maps: Whether to retain conditional attention maps.
|
| 32 |
+
"""
|
| 33 |
+
self.layer_name = layer_name
|
| 34 |
+
self.save_attention_maps = save_attention_maps
|
| 35 |
+
self.conditioning: StructuredConditioning | None = None
|
| 36 |
+
self.attention_maps: list[Float[torch.Tensor, "..."]] = []
|
| 37 |
+
self._last_probabilities: list[
|
| 38 |
+
Float[torch.Tensor, "batch_heads query tokens"]
|
| 39 |
+
] = []
|
| 40 |
+
|
| 41 |
+
def __call__(
|
| 42 |
+
self,
|
| 43 |
+
attn: Attention,
|
| 44 |
+
hidden_states: Float[torch.Tensor, "..."],
|
| 45 |
+
encoder_hidden_states: Float[torch.Tensor, "batch tokens dim"] | None = None,
|
| 46 |
+
attention_mask: Bool[torch.Tensor, "..."]
|
| 47 |
+
| Float[torch.Tensor, "..."]
|
| 48 |
+
| None = None,
|
| 49 |
+
temb: Float[torch.Tensor, "batch channels"] | None = None,
|
| 50 |
+
scale: float | None = None,
|
| 51 |
+
) -> Float[torch.Tensor, "..."]:
|
| 52 |
+
"""Run ordinary attention or noun-phrase-aware key/value attention."""
|
| 53 |
+
del scale
|
| 54 |
+
if (
|
| 55 |
+
attn.to_q is None
|
| 56 |
+
or attn.to_k is None
|
| 57 |
+
or attn.to_v is None
|
| 58 |
+
or attn.to_out is None
|
| 59 |
+
):
|
| 60 |
+
raise ValueError(
|
| 61 |
+
"Attention layers must define query, key, value, and output projections."
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
residual = hidden_states
|
| 65 |
+
if attn.spatial_norm is not None:
|
| 66 |
+
hidden_states = attn.spatial_norm(hidden_states, temb)
|
| 67 |
+
|
| 68 |
+
input_ndim = hidden_states.ndim
|
| 69 |
+
spatial_shape: tuple[int, int] | None = None
|
| 70 |
+
spatial_dimensions: tuple[int, int, int] | None = None
|
| 71 |
+
if input_ndim == 4:
|
| 72 |
+
batch_size, channel, height, width = hidden_states.shape
|
| 73 |
+
spatial_shape = (height, width)
|
| 74 |
+
spatial_dimensions = (channel, height, width)
|
| 75 |
+
spatial_tokens: Float[torch.Tensor, "batch channels tokens"] = (
|
| 76 |
+
hidden_states.view(batch_size, channel, height * width)
|
| 77 |
+
)
|
| 78 |
+
hidden_states = spatial_tokens.transpose(1, 2)
|
| 79 |
+
|
| 80 |
+
batch_size = hidden_states.shape[0]
|
| 81 |
+
if attn.group_norm is not None:
|
| 82 |
+
channels_first: Float[torch.Tensor, "batch channels tokens"] = (
|
| 83 |
+
hidden_states.transpose(1, 2)
|
| 84 |
+
)
|
| 85 |
+
normalized_channels = attn.group_norm(channels_first)
|
| 86 |
+
hidden_states = normalized_channels.transpose(1, 2)
|
| 87 |
+
|
| 88 |
+
query = attn.to_q(hidden_states)
|
| 89 |
+
if encoder_hidden_states is None:
|
| 90 |
+
encoder_hidden_states = hidden_states
|
| 91 |
+
elif attn.norm_cross:
|
| 92 |
+
encoder_hidden_states = attn.norm_encoder_hidden_states(
|
| 93 |
+
encoder_hidden_states
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
conditioning = self.conditioning
|
| 97 |
+
if conditioning is None:
|
| 98 |
+
output = self._normal_attention(
|
| 99 |
+
attn,
|
| 100 |
+
query,
|
| 101 |
+
encoder_hidden_states,
|
| 102 |
+
attention_mask,
|
| 103 |
+
)
|
| 104 |
+
# CFG concatenates unconditional and conditional embeddings before this processor.
|
| 105 |
+
# Keep the unconditional half on ordinary attention and structure the conditional half.
|
| 106 |
+
elif conditioning.do_classifier_free_guidance:
|
| 107 |
+
base_batch = conditioning.keys[0].shape[0]
|
| 108 |
+
if batch_size != 2 * base_batch:
|
| 109 |
+
raise ValueError(
|
| 110 |
+
"Classifier-free guidance queries must contain matching unconditional and conditional batches."
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
unconditional_query, conditional_query = query.split(base_batch, dim=0)
|
| 114 |
+
unconditional_context = encoder_hidden_states[:base_batch]
|
| 115 |
+
unconditional_mask, conditional_mask = self._split_mask(
|
| 116 |
+
attention_mask,
|
| 117 |
+
base_batch,
|
| 118 |
+
batch_size,
|
| 119 |
+
)
|
| 120 |
+
unconditional_output = self._normal_attention(
|
| 121 |
+
attn,
|
| 122 |
+
unconditional_query,
|
| 123 |
+
unconditional_context,
|
| 124 |
+
unconditional_mask,
|
| 125 |
+
)
|
| 126 |
+
conditional_output = self._structured_attention(
|
| 127 |
+
attn,
|
| 128 |
+
conditional_query,
|
| 129 |
+
conditional_mask,
|
| 130 |
+
)
|
| 131 |
+
output = torch.cat((unconditional_output, conditional_output), dim=0)
|
| 132 |
+
else:
|
| 133 |
+
if batch_size != conditioning.keys[0].shape[0]:
|
| 134 |
+
raise ValueError(
|
| 135 |
+
"Structured conditioning and query batches must match."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
output = self._structured_attention(attn, query, attention_mask)
|
| 139 |
+
|
| 140 |
+
if self.save_attention_maps:
|
| 141 |
+
if spatial_shape is None:
|
| 142 |
+
query_length = self._last_probabilities[0].shape[-2]
|
| 143 |
+
side_length = int(query_length**0.5)
|
| 144 |
+
if side_length * side_length == query_length:
|
| 145 |
+
spatial_shape = (side_length, side_length)
|
| 146 |
+
|
| 147 |
+
if self.save_attention_maps and spatial_shape is not None:
|
| 148 |
+
height, width = spatial_shape
|
| 149 |
+
for probabilities in self._last_probabilities:
|
| 150 |
+
map_batch = probabilities.shape[0] // attn.heads
|
| 151 |
+
spatial_attention: Float[
|
| 152 |
+
torch.Tensor, "batch heads height width tokens"
|
| 153 |
+
] = probabilities.reshape(
|
| 154 |
+
map_batch,
|
| 155 |
+
attn.heads,
|
| 156 |
+
height,
|
| 157 |
+
width,
|
| 158 |
+
probabilities.shape[-1],
|
| 159 |
+
)
|
| 160 |
+
saved_tokens = spatial_attention[..., :20]
|
| 161 |
+
saved_attention = saved_tokens.detach().cpu()
|
| 162 |
+
self.attention_maps.append(saved_attention)
|
| 163 |
+
|
| 164 |
+
output = attn.to_out[0](output)
|
| 165 |
+
output = attn.to_out[1](output)
|
| 166 |
+
if spatial_dimensions is not None:
|
| 167 |
+
channel, height, width = spatial_dimensions
|
| 168 |
+
spatial_tokens = output.transpose(-1, -2)
|
| 169 |
+
output = spatial_tokens.reshape(batch_size, channel, height, width)
|
| 170 |
+
|
| 171 |
+
if attn.residual_connection:
|
| 172 |
+
output = output + residual
|
| 173 |
+
|
| 174 |
+
return output / attn.rescale_output_factor
|
| 175 |
+
|
| 176 |
+
def _normal_attention(
|
| 177 |
+
self,
|
| 178 |
+
attn: Attention,
|
| 179 |
+
query: Float[torch.Tensor, "batch sequence channels"],
|
| 180 |
+
context: Float[torch.Tensor, "batch tokens dim"],
|
| 181 |
+
attention_mask: Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 182 |
+
) -> Float[torch.Tensor, "batch sequence channels"]:
|
| 183 |
+
if attn.to_k is None or attn.to_v is None:
|
| 184 |
+
raise ValueError("Attention layers must define key and value projections.")
|
| 185 |
+
|
| 186 |
+
query_heads = self._to_heads(query, attn.heads)
|
| 187 |
+
key_projection = attn.to_k(context)
|
| 188 |
+
key_heads = self._to_heads(key_projection, attn.heads)
|
| 189 |
+
value_projection = attn.to_v(context)
|
| 190 |
+
value_heads = self._to_heads(value_projection, attn.heads)
|
| 191 |
+
if attn.norm_q is not None:
|
| 192 |
+
query_heads = attn.norm_q(query_heads)
|
| 193 |
+
|
| 194 |
+
if attn.norm_k is not None:
|
| 195 |
+
key_heads = attn.norm_k(key_heads)
|
| 196 |
+
|
| 197 |
+
probabilities = self._attention_probabilities(
|
| 198 |
+
attn,
|
| 199 |
+
query_heads,
|
| 200 |
+
key_heads,
|
| 201 |
+
attention_mask,
|
| 202 |
+
)
|
| 203 |
+
self._last_probabilities = [probabilities]
|
| 204 |
+
return self._from_heads(torch.bmm(probabilities, value_heads), attn.heads)
|
| 205 |
+
|
| 206 |
+
def _structured_attention(
|
| 207 |
+
self,
|
| 208 |
+
attn: Attention,
|
| 209 |
+
query: Float[torch.Tensor, "batch sequence channels"],
|
| 210 |
+
attention_mask: Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 211 |
+
) -> Float[torch.Tensor, "batch sequence channels"]:
|
| 212 |
+
conditioning = self.conditioning
|
| 213 |
+
if conditioning is None:
|
| 214 |
+
raise RuntimeError("Structured conditioning has not been installed.")
|
| 215 |
+
|
| 216 |
+
if attn.to_k is None or attn.to_v is None:
|
| 217 |
+
raise ValueError("Attention layers must define key and value projections.")
|
| 218 |
+
|
| 219 |
+
query_heads = self._to_heads(query, attn.heads)
|
| 220 |
+
if attn.norm_q is not None:
|
| 221 |
+
query_heads = attn.norm_q(query_heads)
|
| 222 |
+
|
| 223 |
+
# Project keys and values from their separate contexts before pairing them below.
|
| 224 |
+
key_heads: list[Float[torch.Tensor, "batch_heads tokens head_dim"]] = []
|
| 225 |
+
for context in conditioning.keys:
|
| 226 |
+
normalized = (
|
| 227 |
+
attn.norm_encoder_hidden_states(context) if attn.norm_cross else context
|
| 228 |
+
)
|
| 229 |
+
key_projection = attn.to_k(normalized)
|
| 230 |
+
projected = self._to_heads(key_projection, attn.heads)
|
| 231 |
+
if attn.norm_k is not None:
|
| 232 |
+
projected = attn.norm_k(projected)
|
| 233 |
+
|
| 234 |
+
key_heads.append(projected)
|
| 235 |
+
|
| 236 |
+
value_heads: list[Float[torch.Tensor, "batch_heads tokens head_dim"]] = []
|
| 237 |
+
for context in conditioning.values:
|
| 238 |
+
value_projection = attn.to_v(context)
|
| 239 |
+
value_heads.append(self._to_heads(value_projection, attn.heads))
|
| 240 |
+
|
| 241 |
+
probabilities = [
|
| 242 |
+
self._attention_probabilities(attn, query_heads, key, attention_mask)
|
| 243 |
+
for key in key_heads
|
| 244 |
+
]
|
| 245 |
+
|
| 246 |
+
# Pair each key's attention scores with the corresponding value contexts.
|
| 247 |
+
# A singleton side is reused when only one key or value context is present.
|
| 248 |
+
if len(probabilities) == len(value_heads):
|
| 249 |
+
pairs = list(
|
| 250 |
+
zip(range(len(probabilities)), range(len(value_heads)), strict=True)
|
| 251 |
+
)
|
| 252 |
+
elif len(probabilities) == 1:
|
| 253 |
+
pairs = [(0, value_index) for value_index in range(len(value_heads))]
|
| 254 |
+
elif len(value_heads) == 1:
|
| 255 |
+
pairs = [(key_index, 0) for key_index in range(len(probabilities))]
|
| 256 |
+
else:
|
| 257 |
+
raise ValueError(
|
| 258 |
+
"Structured attention requires matching lists or a singleton side."
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
outputs = torch.stack(
|
| 262 |
+
[
|
| 263 |
+
torch.bmm(probabilities[key_index], value_heads[value_index])
|
| 264 |
+
for key_index, value_index in pairs
|
| 265 |
+
]
|
| 266 |
+
)
|
| 267 |
+
validity = torch.stack(
|
| 268 |
+
[
|
| 269 |
+
conditioning.key_validity[key_index]
|
| 270 |
+
& conditioning.value_validity[value_index]
|
| 271 |
+
for key_index, value_index in pairs
|
| 272 |
+
]
|
| 273 |
+
).repeat_interleave(attn.heads, dim=1)
|
| 274 |
+
# Placeholder phrase contexts are excluded when averaging the valid pairs.
|
| 275 |
+
output_weights: Float[torch.Tensor, "pairs batch_heads 1 1"] = (
|
| 276 |
+
validity.to(outputs.dtype).unsqueeze(-1).unsqueeze(-1)
|
| 277 |
+
)
|
| 278 |
+
weighted_outputs = outputs * output_weights
|
| 279 |
+
summed_weights = output_weights.sum(dim=0).clamp_min(1)
|
| 280 |
+
outputs = weighted_outputs.sum(dim=0) / summed_weights
|
| 281 |
+
|
| 282 |
+
saved_probabilities: list[Float[torch.Tensor, "batch_heads query tokens"]] = []
|
| 283 |
+
for key_index, attention in enumerate(probabilities):
|
| 284 |
+
key_validity = conditioning.key_validity[key_index]
|
| 285 |
+
head_validity = key_validity.repeat_interleave(attn.heads)
|
| 286 |
+
attention_weights: Float[torch.Tensor, "batch_heads 1 1"] = (
|
| 287 |
+
head_validity.to(attention.dtype).unsqueeze(-1).unsqueeze(-1)
|
| 288 |
+
)
|
| 289 |
+
saved_probabilities.append(attention * attention_weights)
|
| 290 |
+
|
| 291 |
+
self._last_probabilities = saved_probabilities
|
| 292 |
+
return self._from_heads(outputs, attn.heads)
|
| 293 |
+
|
| 294 |
+
@staticmethod
|
| 295 |
+
def _to_heads(
|
| 296 |
+
projected: Float[torch.Tensor, "batch sequence inner_dim"], heads: int
|
| 297 |
+
) -> Float[torch.Tensor, "batch_heads sequence head_dim"]:
|
| 298 |
+
batch_size, sequence_length, inner_dim = projected.shape
|
| 299 |
+
head_dim = inner_dim // heads
|
| 300 |
+
head_layout: Float[torch.Tensor, "batch sequence heads head_dim"] = (
|
| 301 |
+
projected.view(batch_size, sequence_length, heads, head_dim)
|
| 302 |
+
)
|
| 303 |
+
heads_first: Float[torch.Tensor, "batch heads sequence head_dim"] = (
|
| 304 |
+
head_layout.transpose(1, 2)
|
| 305 |
+
)
|
| 306 |
+
merged_batch = heads_first.reshape(
|
| 307 |
+
batch_size * heads, sequence_length, head_dim
|
| 308 |
+
)
|
| 309 |
+
return merged_batch
|
| 310 |
+
|
| 311 |
+
@staticmethod
|
| 312 |
+
def _from_heads(
|
| 313 |
+
values: Float[torch.Tensor, "batch_heads sequence head_dim"], heads: int
|
| 314 |
+
) -> Float[torch.Tensor, "batch sequence inner_dim"]:
|
| 315 |
+
batch_heads, sequence_length, head_dim = values.shape
|
| 316 |
+
batch_size = batch_heads // heads
|
| 317 |
+
head_layout: Float[torch.Tensor, "batch heads sequence head_dim"] = values.view(
|
| 318 |
+
batch_size, heads, sequence_length, head_dim
|
| 319 |
+
)
|
| 320 |
+
batch_first: Float[torch.Tensor, "batch sequence heads head_dim"] = (
|
| 321 |
+
head_layout.transpose(1, 2)
|
| 322 |
+
)
|
| 323 |
+
merged_heads = batch_first.reshape(
|
| 324 |
+
batch_size, sequence_length, heads * head_dim
|
| 325 |
+
)
|
| 326 |
+
return merged_heads
|
| 327 |
+
|
| 328 |
+
@staticmethod
|
| 329 |
+
def _attention_probabilities(
|
| 330 |
+
attn: Attention,
|
| 331 |
+
query: Float[torch.Tensor, "batch_heads query head_dim"],
|
| 332 |
+
key: Float[torch.Tensor, "batch_heads tokens head_dim"],
|
| 333 |
+
attention_mask: Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 334 |
+
) -> Float[torch.Tensor, "batch_heads query tokens"]:
|
| 335 |
+
scores = torch.bmm(query, key.transpose(1, 2)) * attn.scale
|
| 336 |
+
if attention_mask is not None:
|
| 337 |
+
if attention_mask.ndim == 2:
|
| 338 |
+
mask_per_head = attention_mask.repeat_interleave(attn.heads, dim=0)
|
| 339 |
+
mask = mask_per_head.unsqueeze(1)
|
| 340 |
+
elif attention_mask.ndim == 4:
|
| 341 |
+
mask = attention_mask.reshape(
|
| 342 |
+
-1, attention_mask.shape[-2], attention_mask.shape[-1]
|
| 343 |
+
)
|
| 344 |
+
elif attention_mask.shape[0] * attn.heads == scores.shape[0]:
|
| 345 |
+
mask = attention_mask.repeat_interleave(attn.heads, dim=0)
|
| 346 |
+
else:
|
| 347 |
+
mask = attention_mask
|
| 348 |
+
|
| 349 |
+
if mask.dtype == torch.bool:
|
| 350 |
+
scores = scores.masked_fill(~mask, torch.finfo(scores.dtype).min)
|
| 351 |
+
else:
|
| 352 |
+
scores = scores + mask
|
| 353 |
+
|
| 354 |
+
return scores.softmax(dim=-1)
|
| 355 |
+
|
| 356 |
+
@staticmethod
|
| 357 |
+
def _split_mask(
|
| 358 |
+
attention_mask: Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 359 |
+
base_batch: int,
|
| 360 |
+
total_batch: int,
|
| 361 |
+
) -> tuple[
|
| 362 |
+
Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 363 |
+
Bool[torch.Tensor, "..."] | Float[torch.Tensor, "..."] | None,
|
| 364 |
+
]:
|
| 365 |
+
if attention_mask is None or attention_mask.shape[0] != total_batch:
|
| 366 |
+
return attention_mask, attention_mask
|
| 367 |
+
|
| 368 |
+
unconditional_mask, conditional_mask = attention_mask.split(base_batch, dim=0)
|
| 369 |
+
return unconditional_mask, conditional_mask
|
pipeline.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Diffusers pipeline for training-free structured diffusion guidance."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Callable
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from enum import StrEnum, auto
|
| 8 |
+
from typing import Final, Literal, cast
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from diffusers import StableDiffusionPipeline
|
| 13 |
+
from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
|
| 14 |
+
from diffusers.image_processor import PipelineImageInput
|
| 15 |
+
from diffusers.pipelines.stable_diffusion.pipeline_output import (
|
| 16 |
+
StableDiffusionPipelineOutput,
|
| 17 |
+
)
|
| 18 |
+
from jaxtyping import Float
|
| 19 |
+
from PIL import Image
|
| 20 |
+
|
| 21 |
+
from .modeling_structure_diffusion import (
|
| 22 |
+
StructuredAttentionProcessor,
|
| 23 |
+
StructuredConditioning,
|
| 24 |
+
)
|
| 25 |
+
from .processing_structure_diffusion import (
|
| 26 |
+
NounPhrase,
|
| 27 |
+
align_phrase_context,
|
| 28 |
+
constituency_noun_phrases,
|
| 29 |
+
normalize_prompt,
|
| 30 |
+
scene_graph_noun_phrases,
|
| 31 |
+
token_span_from_char_span,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ParserType(StrEnum):
|
| 36 |
+
"""Supported linguistic parsers for extracting prompt noun phrases."""
|
| 37 |
+
|
| 38 |
+
constituency = auto()
|
| 39 |
+
scene_graph = auto()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
_PARSER_TYPES: Final[frozenset[ParserType]] = frozenset(ParserType)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def normalize_parser_type(parser_type: ParserType | str) -> ParserType:
|
| 46 |
+
"""Normalize a public parser name and reject unsupported parser choices."""
|
| 47 |
+
if isinstance(parser_type, ParserType):
|
| 48 |
+
normalized = parser_type
|
| 49 |
+
else:
|
| 50 |
+
try:
|
| 51 |
+
normalized = ParserType(parser_type.lower().replace("-", "_"))
|
| 52 |
+
except ValueError as exc:
|
| 53 |
+
choices = ", ".join(parser.value for parser in ParserType)
|
| 54 |
+
raise ValueError(
|
| 55 |
+
f"Unsupported parser_type {parser_type!r}; choose one of: {choices}."
|
| 56 |
+
) from exc
|
| 57 |
+
|
| 58 |
+
if normalized not in _PARSER_TYPES:
|
| 59 |
+
raise ValueError(f"Unsupported parser_type: {parser_type}")
|
| 60 |
+
|
| 61 |
+
return normalized
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass(frozen=True)
|
| 65 |
+
class _StructuredCall:
|
| 66 |
+
prompts: tuple[str, ...]
|
| 67 |
+
parser_type: ParserType
|
| 68 |
+
conjunction: bool
|
| 69 |
+
save_attention_maps: bool
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
OutputType = Literal["pil", "np", "latent"]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class StructureDiffusionPipeline(StableDiffusionPipeline):
|
| 76 |
+
"""Generate images with noun-phrase-aware cross-attention.
|
| 77 |
+
|
| 78 |
+
The pipeline retains Stable Diffusion's component loading and sampling API.
|
| 79 |
+
Its three method options control noun-phrase extraction, the paper's
|
| 80 |
+
conjunction key/value arrangement, and optional attention-map collection.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
vae: Latent image decoder.
|
| 84 |
+
text_encoder: CLIP text encoder.
|
| 85 |
+
tokenizer: CLIP tokenizer.
|
| 86 |
+
unet: Stable Diffusion denoising network.
|
| 87 |
+
scheduler: Diffusion scheduler.
|
| 88 |
+
safety_checker: Optional output safety checker.
|
| 89 |
+
feature_extractor: Optional safety-checker image processor.
|
| 90 |
+
image_encoder: Optional image encoder.
|
| 91 |
+
requires_safety_checker: Whether the safety checker is required.
|
| 92 |
+
|
| 93 |
+
Examples:
|
| 94 |
+
>>> from structure_diffusion import StructureDiffusionPipeline
|
| 95 |
+
>>> pipe = StructureDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4")
|
| 96 |
+
>>> pipe("A red car and a white sheep", num_inference_steps=10).images[0]
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
attention_maps: tuple[tuple[str, Float[torch.Tensor, "..."]], ...] = ()
|
| 100 |
+
|
| 101 |
+
def encode_prompt(
|
| 102 |
+
self,
|
| 103 |
+
prompt: str | list[str] | None,
|
| 104 |
+
device: torch.device,
|
| 105 |
+
num_images_per_prompt: int,
|
| 106 |
+
do_classifier_free_guidance: bool,
|
| 107 |
+
negative_prompt: str | list[str] | None = None,
|
| 108 |
+
prompt_embeds: Float[torch.Tensor, "batch tokens dim"] | None = None,
|
| 109 |
+
negative_prompt_embeds: Float[torch.Tensor, "batch tokens dim"] | None = None,
|
| 110 |
+
lora_scale: float | None = None,
|
| 111 |
+
clip_skip: int | None = None,
|
| 112 |
+
) -> tuple[
|
| 113 |
+
Float[torch.Tensor, "batch tokens dim"],
|
| 114 |
+
Float[torch.Tensor, "batch tokens dim"] | None,
|
| 115 |
+
]:
|
| 116 |
+
"""Encode standard prompts and attach parsed key/value contexts per call."""
|
| 117 |
+
prompt_embeds, negative_prompt_embeds = super().encode_prompt(
|
| 118 |
+
prompt=prompt,
|
| 119 |
+
device=device,
|
| 120 |
+
num_images_per_prompt=num_images_per_prompt,
|
| 121 |
+
do_classifier_free_guidance=do_classifier_free_guidance,
|
| 122 |
+
negative_prompt=negative_prompt,
|
| 123 |
+
prompt_embeds=prompt_embeds,
|
| 124 |
+
negative_prompt_embeds=negative_prompt_embeds,
|
| 125 |
+
lora_scale=lora_scale,
|
| 126 |
+
clip_skip=clip_skip,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
request = getattr(self, "_call_request", None)
|
| 130 |
+
if request is not None:
|
| 131 |
+
self._install_conditioning(
|
| 132 |
+
request,
|
| 133 |
+
prompt_embeds,
|
| 134 |
+
device,
|
| 135 |
+
num_images_per_prompt,
|
| 136 |
+
do_classifier_free_guidance,
|
| 137 |
+
lora_scale,
|
| 138 |
+
clip_skip,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
return prompt_embeds, negative_prompt_embeds
|
| 142 |
+
|
| 143 |
+
@torch.no_grad()
|
| 144 |
+
def __call__(
|
| 145 |
+
self,
|
| 146 |
+
prompt: str | list[str],
|
| 147 |
+
parser_type: ParserType | str = ParserType.constituency,
|
| 148 |
+
conjunction: bool = False,
|
| 149 |
+
save_attention_maps: bool = False,
|
| 150 |
+
height: int | None = None,
|
| 151 |
+
width: int | None = None,
|
| 152 |
+
num_inference_steps: int = 50,
|
| 153 |
+
timesteps: list[int] | None = None,
|
| 154 |
+
sigmas: list[float] | None = None,
|
| 155 |
+
guidance_scale: float = 7.5,
|
| 156 |
+
negative_prompt: str | list[str] | None = None,
|
| 157 |
+
num_images_per_prompt: int = 1,
|
| 158 |
+
eta: float = 0.0,
|
| 159 |
+
generator: torch.Generator | list[torch.Generator] | None = None,
|
| 160 |
+
latents: Float[torch.Tensor, "batch channels latent_height latent_width"]
|
| 161 |
+
| None = None,
|
| 162 |
+
prompt_embeds: Float[torch.Tensor, "batch tokens dim"] | None = None,
|
| 163 |
+
negative_prompt_embeds: Float[torch.Tensor, "batch tokens dim"] | None = None,
|
| 164 |
+
ip_adapter_image: PipelineImageInput | None = None,
|
| 165 |
+
ip_adapter_image_embeds: list[Float[torch.Tensor, "..."]] | None = None,
|
| 166 |
+
output_type: OutputType = "pil",
|
| 167 |
+
return_dict: bool = True,
|
| 168 |
+
cross_attention_kwargs: dict[str, float | bool] | None = None,
|
| 169 |
+
guidance_rescale: float = 0.0,
|
| 170 |
+
clip_skip: int | None = None,
|
| 171 |
+
callback_on_step_end: (
|
| 172 |
+
PipelineCallback
|
| 173 |
+
| MultiPipelineCallbacks
|
| 174 |
+
| Callable[
|
| 175 |
+
[
|
| 176 |
+
StableDiffusionPipeline,
|
| 177 |
+
int,
|
| 178 |
+
int,
|
| 179 |
+
dict[str, Float[torch.Tensor, "..."]],
|
| 180 |
+
],
|
| 181 |
+
dict[str, Float[torch.Tensor, "..."]],
|
| 182 |
+
]
|
| 183 |
+
| None
|
| 184 |
+
) = None,
|
| 185 |
+
callback_on_step_end_tensor_inputs: list[str] = ["latents"],
|
| 186 |
+
) -> (
|
| 187 |
+
StableDiffusionPipelineOutput
|
| 188 |
+
| tuple[
|
| 189 |
+
Float[torch.Tensor, "batch channels height width"]
|
| 190 |
+
| Float[np.ndarray, "batch height width channels"]
|
| 191 |
+
| list[Image.Image],
|
| 192 |
+
list[bool] | None,
|
| 193 |
+
]
|
| 194 |
+
):
|
| 195 |
+
"""Generate one or more images using structured cross-attention.
|
| 196 |
+
|
| 197 |
+
Args:
|
| 198 |
+
prompt: Prompt text or prompt batch to parse and render.
|
| 199 |
+
parser_type: Constituency or scene-graph noun-phrase parser.
|
| 200 |
+
conjunction: Use separate aligned noun phrases for both keys and values.
|
| 201 |
+
save_attention_maps: Keep conditional attention maps on ``attention_maps``.
|
| 202 |
+
height: Requested output image height.
|
| 203 |
+
width: Requested output image width.
|
| 204 |
+
num_inference_steps: Number of scheduler denoising steps.
|
| 205 |
+
timesteps: Optional explicit scheduler timesteps.
|
| 206 |
+
sigmas: Optional explicit scheduler noise levels.
|
| 207 |
+
guidance_scale: Classifier-free guidance scale.
|
| 208 |
+
negative_prompt: Optional negative prompt or prompt batch.
|
| 209 |
+
num_images_per_prompt: Number of outputs to generate for each prompt.
|
| 210 |
+
eta: Scheduler stochasticity parameter when supported.
|
| 211 |
+
generator: Optional random generator or per-output generator list.
|
| 212 |
+
latents: Optional initial latent tensor.
|
| 213 |
+
prompt_embeds: Optional full-prompt CLIP embeddings.
|
| 214 |
+
negative_prompt_embeds: Optional negative-prompt embeddings.
|
| 215 |
+
ip_adapter_image: Optional IP-Adapter image input.
|
| 216 |
+
ip_adapter_image_embeds: Optional precomputed IP-Adapter embeddings.
|
| 217 |
+
output_type: Diffusers output format.
|
| 218 |
+
return_dict: Whether to return ``StableDiffusionPipelineOutput``.
|
| 219 |
+
cross_attention_kwargs: Optional Diffusers cross-attention settings.
|
| 220 |
+
guidance_rescale: Optional classifier-free guidance rescaling.
|
| 221 |
+
clip_skip: Number of CLIP layers to skip.
|
| 222 |
+
callback_on_step_end: Optional Diffusers step callback.
|
| 223 |
+
callback_on_step_end_tensor_inputs: Tensors exposed to the callback.
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
The standard Stable Diffusion output container or tuple.
|
| 227 |
+
|
| 228 |
+
Raises:
|
| 229 |
+
ValueError: If the parser or prompt batch is invalid.
|
| 230 |
+
"""
|
| 231 |
+
# 1. Normalize and validate the structured prompt inputs.
|
| 232 |
+
normalized_parser = normalize_parser_type(parser_type)
|
| 233 |
+
prompt_items = [prompt] if isinstance(prompt, str) else prompt
|
| 234 |
+
normalized_prompts = tuple(normalize_prompt(item) for item in prompt_items)
|
| 235 |
+
if not normalized_prompts or any(not item for item in normalized_prompts):
|
| 236 |
+
raise ValueError("prompt must contain at least one non-empty string.")
|
| 237 |
+
|
| 238 |
+
if num_images_per_prompt < 1:
|
| 239 |
+
raise ValueError("num_images_per_prompt must be positive.")
|
| 240 |
+
|
| 241 |
+
# 2. Prepare the per-call parser and attention-map state.
|
| 242 |
+
request = _StructuredCall(
|
| 243 |
+
prompts=normalized_prompts,
|
| 244 |
+
parser_type=normalized_parser,
|
| 245 |
+
conjunction=conjunction,
|
| 246 |
+
save_attention_maps=save_attention_maps,
|
| 247 |
+
)
|
| 248 |
+
self.__dict__["attention_maps"] = ()
|
| 249 |
+
previous_processors = cast(
|
| 250 |
+
dict[str, Callable[..., Float[torch.Tensor, "..."]]],
|
| 251 |
+
dict(self.unet.attn_processors),
|
| 252 |
+
)
|
| 253 |
+
active_processors: list[StructuredAttentionProcessor] = []
|
| 254 |
+
replacement_processors: dict[
|
| 255 |
+
str, Callable[..., Float[torch.Tensor, "..."]]
|
| 256 |
+
] = {}
|
| 257 |
+
for name, processor in previous_processors.items():
|
| 258 |
+
if name.endswith("attn2.processor"):
|
| 259 |
+
structured_processor = StructuredAttentionProcessor(
|
| 260 |
+
name,
|
| 261 |
+
save_attention_maps,
|
| 262 |
+
)
|
| 263 |
+
active_processors.append(structured_processor)
|
| 264 |
+
replacement_processors[name] = structured_processor
|
| 265 |
+
else:
|
| 266 |
+
replacement_processors[name] = processor
|
| 267 |
+
|
| 268 |
+
if not active_processors:
|
| 269 |
+
raise RuntimeError(
|
| 270 |
+
"The U-Net has no cross-attention processors to replace."
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# 3. Replace only cross-attention processors; self-attention stays unchanged.
|
| 274 |
+
self.unet.set_attn_processor(replacement_processors)
|
| 275 |
+
self._call_request = request
|
| 276 |
+
self._processors_for_call = active_processors
|
| 277 |
+
try:
|
| 278 |
+
# 4. Run the inherited text-to-image denoising and decoding stages.
|
| 279 |
+
result = super().__call__(
|
| 280 |
+
prompt=(
|
| 281 |
+
normalized_prompts[0]
|
| 282 |
+
if isinstance(prompt, str)
|
| 283 |
+
else list(normalized_prompts)
|
| 284 |
+
),
|
| 285 |
+
height=height,
|
| 286 |
+
width=width,
|
| 287 |
+
num_inference_steps=num_inference_steps,
|
| 288 |
+
timesteps=timesteps,
|
| 289 |
+
sigmas=sigmas,
|
| 290 |
+
guidance_scale=guidance_scale,
|
| 291 |
+
negative_prompt=negative_prompt,
|
| 292 |
+
num_images_per_prompt=num_images_per_prompt,
|
| 293 |
+
eta=eta,
|
| 294 |
+
generator=generator,
|
| 295 |
+
latents=latents,
|
| 296 |
+
prompt_embeds=prompt_embeds,
|
| 297 |
+
negative_prompt_embeds=negative_prompt_embeds,
|
| 298 |
+
ip_adapter_image=ip_adapter_image,
|
| 299 |
+
ip_adapter_image_embeds=ip_adapter_image_embeds,
|
| 300 |
+
output_type=output_type,
|
| 301 |
+
return_dict=return_dict,
|
| 302 |
+
cross_attention_kwargs=cross_attention_kwargs,
|
| 303 |
+
guidance_rescale=guidance_rescale,
|
| 304 |
+
clip_skip=clip_skip,
|
| 305 |
+
callback_on_step_end=callback_on_step_end,
|
| 306 |
+
callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
|
| 307 |
+
)
|
| 308 |
+
self.__dict__["attention_maps"] = tuple(
|
| 309 |
+
(processor.layer_name, attention_map)
|
| 310 |
+
for processor in active_processors
|
| 311 |
+
for attention_map in processor.attention_maps
|
| 312 |
+
)
|
| 313 |
+
return result
|
| 314 |
+
finally:
|
| 315 |
+
# 5. Restore the U-Net processors even if generation raises an error.
|
| 316 |
+
del self._call_request
|
| 317 |
+
del self._processors_for_call
|
| 318 |
+
self.unet.set_attn_processor(dict(previous_processors))
|
| 319 |
+
|
| 320 |
+
def _install_conditioning(
|
| 321 |
+
self,
|
| 322 |
+
request: _StructuredCall,
|
| 323 |
+
prompt_embeds: Float[torch.Tensor, "batch tokens dim"],
|
| 324 |
+
device: torch.device,
|
| 325 |
+
num_images_per_prompt: int,
|
| 326 |
+
do_classifier_free_guidance: bool,
|
| 327 |
+
lora_scale: float | None,
|
| 328 |
+
clip_skip: int | None,
|
| 329 |
+
) -> None:
|
| 330 |
+
batch_size = len(request.prompts)
|
| 331 |
+
expected_batch = batch_size * num_images_per_prompt
|
| 332 |
+
if prompt_embeds.shape[0] != expected_batch:
|
| 333 |
+
raise ValueError(
|
| 334 |
+
"The encoded prompt batch does not match prompt and num_images_per_prompt."
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
prompt_contexts: Float[torch.Tensor, "prompts images tokens dim"] = (
|
| 338 |
+
prompt_embeds.reshape(
|
| 339 |
+
batch_size,
|
| 340 |
+
num_images_per_prompt,
|
| 341 |
+
prompt_embeds.shape[1],
|
| 342 |
+
prompt_embeds.shape[2],
|
| 343 |
+
)
|
| 344 |
+
)
|
| 345 |
+
base_contexts: Float[torch.Tensor, "batch tokens dim"] = prompt_contexts[:, 0]
|
| 346 |
+
phrase_groups = [
|
| 347 |
+
self._parse_prompt(prompt_text, request.parser_type)
|
| 348 |
+
for prompt_text in request.prompts
|
| 349 |
+
]
|
| 350 |
+
# Encode each parsed phrase separately so its CLIP tokens can be aligned below.
|
| 351 |
+
phrase_texts = [phrase.text for group in phrase_groups for phrase in group]
|
| 352 |
+
phrase_contexts: Float[torch.Tensor, "phrases tokens dim"] | None = None
|
| 353 |
+
if phrase_texts:
|
| 354 |
+
phrase_contexts, _ = StableDiffusionPipeline.encode_prompt(
|
| 355 |
+
self,
|
| 356 |
+
prompt=phrase_texts,
|
| 357 |
+
device=device,
|
| 358 |
+
num_images_per_prompt=1,
|
| 359 |
+
do_classifier_free_guidance=False,
|
| 360 |
+
lora_scale=lora_scale,
|
| 361 |
+
clip_skip=clip_skip,
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
aligned_groups: list[
|
| 365 |
+
list[
|
| 366 |
+
tuple[
|
| 367 |
+
Float[torch.Tensor, "batch tokens dim"],
|
| 368 |
+
tuple[int, int],
|
| 369 |
+
]
|
| 370 |
+
]
|
| 371 |
+
] = []
|
| 372 |
+
phrase_cursor = 0
|
| 373 |
+
for prompt_index, phrase_group in enumerate(phrase_groups):
|
| 374 |
+
aligned: list[
|
| 375 |
+
tuple[
|
| 376 |
+
Float[torch.Tensor, "batch tokens dim"],
|
| 377 |
+
tuple[int, int],
|
| 378 |
+
]
|
| 379 |
+
] = []
|
| 380 |
+
for phrase in phrase_group:
|
| 381 |
+
phrase_embedding = (
|
| 382 |
+
phrase_contexts[phrase_cursor : phrase_cursor + 1]
|
| 383 |
+
if phrase_contexts is not None
|
| 384 |
+
else base_contexts[prompt_index : prompt_index + 1]
|
| 385 |
+
)
|
| 386 |
+
phrase_cursor += 1
|
| 387 |
+
token_span = token_span_from_char_span(
|
| 388 |
+
self.tokenizer,
|
| 389 |
+
request.prompts[prompt_index],
|
| 390 |
+
phrase.start,
|
| 391 |
+
phrase.end,
|
| 392 |
+
prompt_embeds.shape[1],
|
| 393 |
+
)
|
| 394 |
+
if token_span is None:
|
| 395 |
+
continue
|
| 396 |
+
|
| 397 |
+
phrase_token_count = len(self.tokenizer.tokenize(phrase.text))
|
| 398 |
+
if phrase_token_count < token_span[1] - token_span[0]:
|
| 399 |
+
continue
|
| 400 |
+
|
| 401 |
+
aligned.append(
|
| 402 |
+
(
|
| 403 |
+
align_phrase_context(
|
| 404 |
+
base_contexts[prompt_index : prompt_index + 1],
|
| 405 |
+
phrase_embedding,
|
| 406 |
+
token_span,
|
| 407 |
+
phrase_token_count,
|
| 408 |
+
),
|
| 409 |
+
token_span,
|
| 410 |
+
)
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
aligned_groups.append(aligned)
|
| 414 |
+
|
| 415 |
+
max_phrase_count = max((len(group) for group in aligned_groups), default=0)
|
| 416 |
+
aligned_context_batches: list[Float[torch.Tensor, "batch tokens dim"]] = []
|
| 417 |
+
for phrase_index in range(max_phrase_count):
|
| 418 |
+
phrase_contexts_for_batch: list[
|
| 419 |
+
Float[torch.Tensor, "batch tokens dim"]
|
| 420 |
+
] = []
|
| 421 |
+
for prompt_index, group in enumerate(aligned_groups):
|
| 422 |
+
if phrase_index < len(group):
|
| 423 |
+
aligned_context, _token_span = group[phrase_index]
|
| 424 |
+
prompt_phrase_context = aligned_context
|
| 425 |
+
else:
|
| 426 |
+
prompt_phrase_context = base_contexts[
|
| 427 |
+
prompt_index : prompt_index + 1
|
| 428 |
+
]
|
| 429 |
+
|
| 430 |
+
phrase_contexts_for_batch.append(prompt_phrase_context)
|
| 431 |
+
|
| 432 |
+
phrase_context_batch = torch.cat(phrase_contexts_for_batch, dim=0)
|
| 433 |
+
aligned_context_batches.append(phrase_context_batch)
|
| 434 |
+
|
| 435 |
+
aligned_contexts = tuple(aligned_context_batches)
|
| 436 |
+
aligned_validity = tuple(
|
| 437 |
+
torch.tensor(
|
| 438 |
+
[index < len(group) for group in aligned_groups],
|
| 439 |
+
dtype=torch.bool,
|
| 440 |
+
device=device,
|
| 441 |
+
)
|
| 442 |
+
for index in range(max_phrase_count)
|
| 443 |
+
)
|
| 444 |
+
base_validity = torch.ones(batch_size, dtype=torch.bool, device=device)
|
| 445 |
+
# Conjunction mode uses phrase keys with one composed value; the default uses
|
| 446 |
+
# one full-prompt key with separate phrase values.
|
| 447 |
+
if request.conjunction:
|
| 448 |
+
combined_contexts: list[Float[torch.Tensor, "batch tokens dim"]] = []
|
| 449 |
+
for prompt_index, base_context in enumerate(base_contexts):
|
| 450 |
+
combined = base_context.clone()
|
| 451 |
+
for aligned_context, (start, end) in aligned_groups[prompt_index]:
|
| 452 |
+
combined[start:end] = aligned_context[0, start:end]
|
| 453 |
+
|
| 454 |
+
combined_contexts.append(combined)
|
| 455 |
+
|
| 456 |
+
values = (torch.stack(combined_contexts),)
|
| 457 |
+
keys = (base_contexts, *aligned_contexts)
|
| 458 |
+
key_validity = (base_validity, *aligned_validity)
|
| 459 |
+
value_validity = (base_validity,)
|
| 460 |
+
else:
|
| 461 |
+
keys = (base_contexts,)
|
| 462 |
+
values = (base_contexts, *aligned_contexts)
|
| 463 |
+
key_validity = (base_validity,)
|
| 464 |
+
value_validity = (base_validity, *aligned_validity)
|
| 465 |
+
|
| 466 |
+
repeated_keys = tuple(
|
| 467 |
+
context.repeat_interleave(num_images_per_prompt, dim=0) for context in keys
|
| 468 |
+
)
|
| 469 |
+
repeated_values = tuple(
|
| 470 |
+
context.repeat_interleave(num_images_per_prompt, dim=0)
|
| 471 |
+
for context in values
|
| 472 |
+
)
|
| 473 |
+
repeated_key_validity = tuple(
|
| 474 |
+
validity.repeat_interleave(num_images_per_prompt, dim=0)
|
| 475 |
+
for validity in key_validity
|
| 476 |
+
)
|
| 477 |
+
repeated_value_validity = tuple(
|
| 478 |
+
validity.repeat_interleave(num_images_per_prompt, dim=0)
|
| 479 |
+
for validity in value_validity
|
| 480 |
+
)
|
| 481 |
+
conditioning = StructuredConditioning(
|
| 482 |
+
keys=repeated_keys,
|
| 483 |
+
values=repeated_values,
|
| 484 |
+
key_validity=repeated_key_validity,
|
| 485 |
+
value_validity=repeated_value_validity,
|
| 486 |
+
do_classifier_free_guidance=do_classifier_free_guidance,
|
| 487 |
+
)
|
| 488 |
+
for processor in getattr(self, "_processors_for_call", ()):
|
| 489 |
+
processor.conditioning = conditioning
|
| 490 |
+
|
| 491 |
+
@staticmethod
|
| 492 |
+
def _parse_prompt(prompt: str, parser_type: ParserType) -> list[NounPhrase]:
|
| 493 |
+
if parser_type is ParserType.constituency:
|
| 494 |
+
return constituency_noun_phrases(prompt)
|
| 495 |
+
|
| 496 |
+
if parser_type is ParserType.scene_graph:
|
| 497 |
+
return scene_graph_noun_phrases(prompt)
|
| 498 |
+
|
| 499 |
+
raise ValueError(f"Unsupported parser_type: {parser_type}")
|
processing_structure_diffusion.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prompt parsing and CLIP token alignment for structured diffusion guidance."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from typing import Protocol, cast
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from jaxtyping import Float
|
| 12 |
+
from nltk.tree import Tree
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _StanzaWord(Protocol):
|
| 16 |
+
start_char: int | None
|
| 17 |
+
end_char: int | None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class _StanzaSentence(Protocol):
|
| 21 |
+
constituency: str
|
| 22 |
+
words: list[_StanzaWord]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _StanzaDocument(Protocol):
|
| 26 |
+
sentences: list[_StanzaSentence]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class _StanzaPipeline(Protocol):
|
| 30 |
+
def __call__(self, text: str) -> _StanzaDocument: ...
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class _Tokenizer(Protocol):
|
| 34 |
+
def tokenize(self, text: str) -> list[str]: ...
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass(frozen=True)
|
| 38 |
+
class NounPhrase:
|
| 39 |
+
"""A parsed noun phrase with character offsets into normalized prompt text."""
|
| 40 |
+
|
| 41 |
+
text: str
|
| 42 |
+
start: int
|
| 43 |
+
end: int
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def normalize_prompt(prompt: str) -> str:
|
| 47 |
+
"""Normalize casing and trailing punctuation before parsing and token alignment."""
|
| 48 |
+
return prompt.lower().strip().strip(".").strip()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def constituency_noun_phrases(prompt: str) -> list[NounPhrase]:
|
| 52 |
+
"""Parse a prompt and return its multi-token noun phrases.
|
| 53 |
+
|
| 54 |
+
The full prompt is represented separately by the pipeline. Returned offsets
|
| 55 |
+
refer to normalized prompt text so they can be aligned to CLIP BPE tokens.
|
| 56 |
+
Stanza models must be installed before this function runs.
|
| 57 |
+
"""
|
| 58 |
+
document = _constituency_pipeline()(prompt)
|
| 59 |
+
if not document.sentences:
|
| 60 |
+
return []
|
| 61 |
+
|
| 62 |
+
sentence = document.sentences[0]
|
| 63 |
+
tree = Tree.fromstring(str(sentence.constituency))
|
| 64 |
+
word_offsets: list[tuple[int, int]] = []
|
| 65 |
+
for word in sentence.words:
|
| 66 |
+
if word.start_char is None or word.end_char is None:
|
| 67 |
+
raise ValueError("The constituency parser did not return word offsets.")
|
| 68 |
+
|
| 69 |
+
word_offsets.append((word.start_char, word.end_char))
|
| 70 |
+
|
| 71 |
+
if len(tree.leaves()) != len(word_offsets):
|
| 72 |
+
raise ValueError(
|
| 73 |
+
"The constituency tree and parser word offsets have different lengths."
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
phrases: list[NounPhrase] = []
|
| 77 |
+
|
| 78 |
+
def visit(node: Tree | str, word_start: int) -> int:
|
| 79 |
+
if isinstance(node, str):
|
| 80 |
+
return 1
|
| 81 |
+
|
| 82 |
+
cursor = word_start
|
| 83 |
+
for child in node:
|
| 84 |
+
cursor += visit(child, cursor)
|
| 85 |
+
|
| 86 |
+
if node.label() == "NP" and cursor - word_start > 1:
|
| 87 |
+
char_start = word_offsets[word_start][0]
|
| 88 |
+
char_end = word_offsets[cursor - 1][1]
|
| 89 |
+
text = prompt[char_start:char_end].strip()
|
| 90 |
+
if text and text != prompt:
|
| 91 |
+
phrases.append(NounPhrase(text=text, start=char_start, end=char_end))
|
| 92 |
+
|
| 93 |
+
return cursor - word_start
|
| 94 |
+
|
| 95 |
+
visit(tree, 0)
|
| 96 |
+
return phrases
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def scene_graph_noun_phrases(prompt: str) -> list[NounPhrase]:
|
| 100 |
+
"""Return entity and relation spans from the optional scene-graph parser."""
|
| 101 |
+
try:
|
| 102 |
+
import sng_parser
|
| 103 |
+
except ImportError as exc:
|
| 104 |
+
raise ImportError(
|
| 105 |
+
"Install structure-diffusion[scene-graph] to use parser_type='scene_graph'."
|
| 106 |
+
) from exc
|
| 107 |
+
|
| 108 |
+
word_offsets = [
|
| 109 |
+
(match.start(), match.end()) for match in re.finditer(r"\S+", prompt)
|
| 110 |
+
]
|
| 111 |
+
graph = sng_parser.parse(prompt)
|
| 112 |
+
spans: list[tuple[int, int]] = []
|
| 113 |
+
|
| 114 |
+
for entity in graph["entities"]:
|
| 115 |
+
start, end = entity["span_bounds"]
|
| 116 |
+
if end - start > 1 and 0 <= start < end <= len(word_offsets):
|
| 117 |
+
spans.append((start, end))
|
| 118 |
+
|
| 119 |
+
for relation in graph["relations"]:
|
| 120 |
+
subject_start, subject_end = graph["entities"][relation["subject"]][
|
| 121 |
+
"span_bounds"
|
| 122 |
+
]
|
| 123 |
+
object_start, object_end = graph["entities"][relation["object"]]["span_bounds"]
|
| 124 |
+
start = min(subject_start, object_start)
|
| 125 |
+
end = max(subject_end, object_end)
|
| 126 |
+
if end - start > 1 and 0 <= start < end <= len(word_offsets):
|
| 127 |
+
spans.append((start, end))
|
| 128 |
+
|
| 129 |
+
phrases: list[NounPhrase] = []
|
| 130 |
+
seen: set[tuple[int, int]] = set()
|
| 131 |
+
for start, end in spans:
|
| 132 |
+
if (start, end) in seen:
|
| 133 |
+
continue
|
| 134 |
+
|
| 135 |
+
seen.add((start, end))
|
| 136 |
+
char_start = word_offsets[start][0]
|
| 137 |
+
char_end = word_offsets[end - 1][1]
|
| 138 |
+
text = prompt[char_start:char_end].strip()
|
| 139 |
+
if text and text != prompt:
|
| 140 |
+
phrases.append(NounPhrase(text=text, start=char_start, end=char_end))
|
| 141 |
+
|
| 142 |
+
return phrases
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def token_span_from_char_span(
|
| 146 |
+
tokenizer: _Tokenizer,
|
| 147 |
+
prompt: str,
|
| 148 |
+
start: int,
|
| 149 |
+
end: int,
|
| 150 |
+
sequence_length: int,
|
| 151 |
+
) -> tuple[int, int] | None:
|
| 152 |
+
"""Map prompt character offsets to a BOS-shifted, truncated CLIP token span."""
|
| 153 |
+
if start < 0 or end <= start or end > len(prompt):
|
| 154 |
+
raise ValueError("Phrase character offsets must be a non-empty prompt span.")
|
| 155 |
+
|
| 156 |
+
# CLIP reserves token index 0 for BOS, so shift text-token offsets by one.
|
| 157 |
+
start_token = len(tokenizer.tokenize(prompt[:start])) + 1
|
| 158 |
+
end_token = len(tokenizer.tokenize(prompt[:end])) + 1
|
| 159 |
+
end_token = min(end_token, sequence_length - 1)
|
| 160 |
+
if start_token >= end_token:
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
return start_token, end_token
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def align_phrase_context(
|
| 167 |
+
prompt_context: Float[torch.Tensor, "batch tokens dim"],
|
| 168 |
+
phrase_context: Float[torch.Tensor, "batch tokens dim"],
|
| 169 |
+
token_span: tuple[int, int],
|
| 170 |
+
phrase_token_count: int,
|
| 171 |
+
) -> Float[torch.Tensor, "batch tokens dim"]:
|
| 172 |
+
"""Copy only actual phrase-token embeddings into the matching prompt span."""
|
| 173 |
+
start, end = token_span
|
| 174 |
+
span_length = end - start
|
| 175 |
+
if start < 1 or end > prompt_context.shape[1] or span_length < 1:
|
| 176 |
+
raise ValueError("The aligned token span is outside the prompt context.")
|
| 177 |
+
|
| 178 |
+
if phrase_token_count < span_length:
|
| 179 |
+
raise ValueError("The phrase encoding has fewer real tokens than its span.")
|
| 180 |
+
|
| 181 |
+
if phrase_context.shape[0] != prompt_context.shape[0]:
|
| 182 |
+
raise ValueError("Prompt and phrase context batches must match.")
|
| 183 |
+
|
| 184 |
+
aligned = prompt_context.clone()
|
| 185 |
+
# Skip the phrase encoding's BOS token and copy only the aligned phrase tokens.
|
| 186 |
+
aligned[:, start:end] = phrase_context[:, 1 : 1 + span_length]
|
| 187 |
+
return aligned
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@lru_cache(maxsize=1)
|
| 191 |
+
def _constituency_pipeline() -> _StanzaPipeline:
|
| 192 |
+
"""Build a reusable English constituency parser without downloading models."""
|
| 193 |
+
import stanza
|
| 194 |
+
|
| 195 |
+
return cast(
|
| 196 |
+
_StanzaPipeline,
|
| 197 |
+
stanza.Pipeline(
|
| 198 |
+
lang="en",
|
| 199 |
+
processors="tokenize,pos,constituency",
|
| 200 |
+
download_method=None,
|
| 201 |
+
use_gpu=False,
|
| 202 |
+
verbose=False,
|
| 203 |
+
),
|
| 204 |
+
)
|