clopeux commited on
Commit
aade3e0
·
verified ·
1 Parent(s): ff87ef3

Enhanced: Real-ESRGAN upscaler, GFPGAN face restoration, multi-stage detailer

Browse files
.gitattributes CHANGED
@@ -1,38 +1,38 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- disaster_girl.jpg filter=lfs diff=lfs merge=lfs -text
37
- grumpy.png filter=lfs diff=lfs merge=lfs -text
38
- wednesday.png filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ disaster_girl.jpg filter=lfs diff=lfs merge=lfs -text
37
+ grumpy.png filter=lfs diff=lfs merge=lfs -text
38
+ wednesday.png filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,58 +1,58 @@
1
- # IDE configuration files
2
- pyrightconfig.json
3
- .vscode/
4
-
5
- # Generated outputs
6
- outputs/
7
-
8
- # Custom folder (user-specific files)
9
- custom/
10
-
11
- # Python
12
- __pycache__/
13
- *.py[cod]
14
- *$py.class
15
- *.so
16
- .Python
17
- env/
18
- venv/
19
- ENV/
20
- build/
21
- develop-eggs/
22
- dist/
23
- downloads/
24
- eggs/
25
- .eggs/
26
- lib/
27
- lib64/
28
- parts/
29
- sdist/
30
- var/
31
- wheels/
32
- *.egg-info/
33
- .installed.cfg
34
- *.egg
35
-
36
- # Model files (if you don't want to track large files)
37
- *.pt
38
- *.pth
39
- *.bin
40
- *.safetensors
41
-
42
- # OS files
43
- .DS_Store
44
- Thumbs.db
45
-
46
- # Testing
47
- .pytest_cache/
48
- .coverage
49
- htmlcov/
50
-
51
- # Logs
52
- *.log
53
-
54
- # Backup files
55
- *.from_server
56
- *.backup
57
- *.bak
58
- .claude/
 
1
+ # IDE configuration files
2
+ pyrightconfig.json
3
+ .vscode/
4
+
5
+ # Generated outputs
6
+ outputs/
7
+
8
+ # Custom folder (user-specific files)
9
+ custom/
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+ *.so
16
+ .Python
17
+ env/
18
+ venv/
19
+ ENV/
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+
36
+ # Model files (if you don't want to track large files)
37
+ *.pt
38
+ *.pth
39
+ *.bin
40
+ *.safetensors
41
+
42
+ # OS files
43
+ .DS_Store
44
+ Thumbs.db
45
+
46
+ # Testing
47
+ .pytest_cache/
48
+ .coverage
49
+ htmlcov/
50
+
51
+ # Logs
52
+ *.log
53
+
54
+ # Backup files
55
+ *.from_server
56
+ *.backup
57
+ *.bak
58
+ .claude/
AGENT.md CHANGED
@@ -1,169 +1,169 @@
1
- # AGENT.md
2
-
3
- This file provides guidance to AI coding agents when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- This is a Gradio Space that implements "Next Scene" cinematic image generation using Qwen-Image-Edit-2509 with LoRA fine-tuning. The application generates visually progressive image sequences with natural cinematic transitions from frame to frame, optimized for fast 4-step inference.
8
-
9
- **Key Model Components:**
10
- - Base model: `Qwen/Qwen-Image-Edit-2509` (image editing diffusion model)
11
- - Accelerated transformer: `Phr00t/Qwen-Image-Edit-Rapid-AIO` v23 single-file checkpoint (4-step optimized variant)
12
- - LoRA adapter: `lovis93/next-scene-qwen-image-lora-2509` (cinematic progression fine-tune)
13
- - Text encoder: `Qwen2.5-VL-72B-Instruct` (via Hugging Face InferenceClient for prompt enhancement)
14
-
15
- ## Running the Application
16
-
17
- **Start the Gradio interface:**
18
- ```bash
19
- python app.py
20
- ```
21
-
22
- **Install dependencies:**
23
- ```bash
24
- pip install -r requirements.txt
25
- ```
26
-
27
- The app requires GPU access. It uses the `@spaces.GPU` decorator for Hugging Face Spaces zero-GPU allocation.
28
-
29
- ## Architecture
30
-
31
- ### Pipeline Flow
32
-
33
- 1. **Input Processing** (`app.py:infer`):
34
- - Accepts input images via Gradio Gallery (filepath-based)
35
- - Optional prompt rewriting using `Qwen2.5-VL-72B-Instruct` API
36
- - Automatic "Next Scene" prompt generation from images
37
-
38
- 2. **Image Generation** (`qwenimage/pipeline_qwenimage_edit_plus.py`):
39
- - Custom pipeline extending `DiffusionPipeline`
40
- - Encodes images using VAE at 1024x1024 for latents
41
- - Encodes conditioning images at 384x384 for text encoder
42
- - Packs latents into 2x2 patches (latent dims must be divisible by 2)
43
- - Uses `FlowMatchEulerDiscreteScheduler` for denoising
44
-
45
- 3. **Optimization** (`optimization.py`):
46
- - Ahead-of-time (AOT) compilation using `torch.export` and `spaces.aoti_compile`
47
- - Dynamic shapes for variable sequence lengths
48
- - Custom inductor configs for performance (max_autotune, cudagraphs)
49
- - FlashAttention 3 integration via `QwenDoubleStreamAttnProcessorFA3`
50
-
51
- 4. **Output Handling**:
52
- - Saves outputs to `outputs/` directory with unique timestamps
53
- - Maintains 20-image history gallery
54
- - Optional video generation through an owned Gradio Space configured by `VIDEO_SPACE_ID`
55
-
56
- ### Custom QwenImage Components
57
-
58
- **Location:** `qwenimage/` package
59
-
60
- - `pipeline_qwenimage_edit_plus.py` - Main diffusion pipeline with LoRA support
61
- - `transformer_qwenimage.py` - Custom transformer model with cache management
62
- - `qwen_fa3_processor.py` - FlashAttention 3 attention processor
63
-
64
- **Key architectural features:**
65
- - Latent packing/unpacking for 2x2 patch processing
66
- - Multi-image conditioning support
67
- - True CFG (classifier-free guidance) with separate pos/neg paths
68
- - Dual-stream attention with rotary embeddings
69
- - Cache contexts for conditional/unconditional forward passes
70
-
71
- ### Prompt Engineering
72
-
73
- **Two-stage prompt system:**
74
-
75
- 1. **Edit Instruction Rewriter** (`SYSTEM_PROMPT`):
76
- - Normalizes user prompts into professional editing instructions
77
- - Handles text replacement (requires quotes), object manipulation, style transfer
78
- - Used when `rewrite_prompt=True` checkbox is enabled
79
-
80
- 2. **Next Scene Generator** (`NEXT_SCENE_SYSTEM_PROMPT`):
81
- - Automatically suggests cinematic camera movements
82
- - Focus on visual progression (dolly, pan, zoom, framing changes)
83
- - Auto-triggers when input images change
84
-
85
- Both use `Qwen2.5-VL-72B-Instruct` via Hugging Face InferenceClient with Nebius provider. Requires `HF_TOKEN` environment variable.
86
-
87
- ## Important Implementation Details
88
-
89
- ### Image Dimension Handling
90
-
91
- Images are automatically resized based on `calculate_dimensions()` function:
92
- - VAE images: resized to maintain 1024×1024 area (1,048,576 pixels)
93
- - Condition images: resized to maintain 384×384 area (147,456 pixels)
94
- - Output dimensions must be divisible by 16 (vae_scale_factor × 2)
95
- - Height/width default to `None` which auto-calculates from input aspect ratio
96
-
97
- ### LoRA Integration
98
-
99
- The pipeline fuses the "next-scene" LoRA adapter at initialization:
100
- ```python
101
- pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", ...)
102
- pipe.set_adapters(["next-scene"], adapter_weights=[1.])
103
- pipe.fuse_lora(adapter_names=["next-scene"], lora_scale=1.)
104
- pipe.unload_lora_weights()
105
- ```
106
-
107
- After fusion, the adapter weights are merged into the base model and cannot be unfused.
108
-
109
- ### Video Generation Integration
110
-
111
- The `turn_into_video()` function:
112
- - Connects only when `VIDEO_SPACE_ID` is configured
113
- - Requires first input image and last output image
114
- - Uses the original prompt (or "smooth cinematic transition" fallback)
115
- - Returns video path for display
116
-
117
- ### Gradio Gallery Format
118
-
119
- Input/output galleries use `type="filepath"` (string paths) rather than PIL Image tuples. Helper functions handle format compatibility for legacy tuple support.
120
-
121
- ## Environment Variables
122
-
123
- - `HF_TOKEN` - Required for Qwen2.5-VL API access (prompt rewriting/generation)
124
-
125
- ## File Outputs
126
-
127
- Generated images are saved to `outputs/` directory with format:
128
- ```
129
- output_{seed}_{index}_{timestamp_ms}.png
130
- ```
131
-
132
- ## Local Development and API Testing
133
-
134
- The `custom/` directory is fully gitignored and used for local development files. Specifically, it contains:
135
-
136
- - **API client scripts** - For testing the Gradio Space remotely via API after deployment to Hugging Face
137
- - **`API_GUIDE.txt`** - Auto-generated Gradio API documentation showing endpoint signatures and example usage
138
- - **Local testing environments** - Virtual environments or test data that shouldn't be committed
139
-
140
- **API Integration Pattern:**
141
- Once the Space is deployed to Hugging Face, you can interact with it programmatically using `gradio_client`:
142
-
143
- ```python
144
- from gradio_client import Client, handle_file
145
-
146
- client = Client("clopeux/Pro-Realism-Edit-Studio")
147
- result = client.predict(
148
- images=[],
149
- prompt="Next Scene: Camera dollies forward...",
150
- seed=42,
151
- randomize_seed=False,
152
- true_guidance_scale=1.0,
153
- num_inference_steps=4,
154
- height=1024,
155
- width=1024,
156
- rewrite_prompt=False,
157
- api_name="/infer"
158
- )
159
- ```
160
-
161
- The `custom/API_GUIDE.txt` contains full documentation of all available endpoints including `/infer`, `/turn_into_video`, `/suggest_next_scene_prompt`, and utility functions.
162
-
163
- ## Development Notes
164
-
165
- - The model loads on startup and applies AOT compilation during first inference
166
- - Compilation uses dynamic shapes to support variable text/image sequence lengths
167
- - The transformer uses custom cache contexts ("cond"/"uncond") to optimize CFG passes
168
- - True CFG applies norm-based rescaling: `comb_pred * (cond_norm / noise_norm)`
169
- - FlashAttention 3 processor must be set before compilation
 
1
+ # AGENT.md
2
+
3
+ This file provides guidance to AI coding agents when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Gradio Space that implements "Next Scene" cinematic image generation using Qwen-Image-Edit-2509 with LoRA fine-tuning. The application generates visually progressive image sequences with natural cinematic transitions from frame to frame, optimized for fast 4-step inference.
8
+
9
+ **Key Model Components:**
10
+ - Base model: `Qwen/Qwen-Image-Edit-2509` (image editing diffusion model)
11
+ - Accelerated transformer: `Phr00t/Qwen-Image-Edit-Rapid-AIO` v23 single-file checkpoint (4-step optimized variant)
12
+ - LoRA adapter: `lovis93/next-scene-qwen-image-lora-2509` (cinematic progression fine-tune)
13
+ - Text encoder: `Qwen2.5-VL-72B-Instruct` (via Hugging Face InferenceClient for prompt enhancement)
14
+
15
+ ## Running the Application
16
+
17
+ **Start the Gradio interface:**
18
+ ```bash
19
+ python app.py
20
+ ```
21
+
22
+ **Install dependencies:**
23
+ ```bash
24
+ pip install -r requirements.txt
25
+ ```
26
+
27
+ The app requires GPU access. It uses the `@spaces.GPU` decorator for Hugging Face Spaces zero-GPU allocation.
28
+
29
+ ## Architecture
30
+
31
+ ### Pipeline Flow
32
+
33
+ 1. **Input Processing** (`app.py:infer`):
34
+ - Accepts input images via Gradio Gallery (filepath-based)
35
+ - Optional prompt rewriting using `Qwen2.5-VL-72B-Instruct` API
36
+ - Automatic "Next Scene" prompt generation from images
37
+
38
+ 2. **Image Generation** (`qwenimage/pipeline_qwenimage_edit_plus.py`):
39
+ - Custom pipeline extending `DiffusionPipeline`
40
+ - Encodes images using VAE at 1024x1024 for latents
41
+ - Encodes conditioning images at 384x384 for text encoder
42
+ - Packs latents into 2x2 patches (latent dims must be divisible by 2)
43
+ - Uses `FlowMatchEulerDiscreteScheduler` for denoising
44
+
45
+ 3. **Optimization** (`optimization.py`):
46
+ - Ahead-of-time (AOT) compilation using `torch.export` and `spaces.aoti_compile`
47
+ - Dynamic shapes for variable sequence lengths
48
+ - Custom inductor configs for performance (max_autotune, cudagraphs)
49
+ - FlashAttention 3 integration via `QwenDoubleStreamAttnProcessorFA3`
50
+
51
+ 4. **Output Handling**:
52
+ - Saves outputs to `outputs/` directory with unique timestamps
53
+ - Maintains 20-image history gallery
54
+ - Optional video generation through an owned Gradio Space configured by `VIDEO_SPACE_ID`
55
+
56
+ ### Custom QwenImage Components
57
+
58
+ **Location:** `qwenimage/` package
59
+
60
+ - `pipeline_qwenimage_edit_plus.py` - Main diffusion pipeline with LoRA support
61
+ - `transformer_qwenimage.py` - Custom transformer model with cache management
62
+ - `qwen_fa3_processor.py` - FlashAttention 3 attention processor
63
+
64
+ **Key architectural features:**
65
+ - Latent packing/unpacking for 2x2 patch processing
66
+ - Multi-image conditioning support
67
+ - True CFG (classifier-free guidance) with separate pos/neg paths
68
+ - Dual-stream attention with rotary embeddings
69
+ - Cache contexts for conditional/unconditional forward passes
70
+
71
+ ### Prompt Engineering
72
+
73
+ **Two-stage prompt system:**
74
+
75
+ 1. **Edit Instruction Rewriter** (`SYSTEM_PROMPT`):
76
+ - Normalizes user prompts into professional editing instructions
77
+ - Handles text replacement (requires quotes), object manipulation, style transfer
78
+ - Used when `rewrite_prompt=True` checkbox is enabled
79
+
80
+ 2. **Next Scene Generator** (`NEXT_SCENE_SYSTEM_PROMPT`):
81
+ - Automatically suggests cinematic camera movements
82
+ - Focus on visual progression (dolly, pan, zoom, framing changes)
83
+ - Auto-triggers when input images change
84
+
85
+ Both use `Qwen2.5-VL-72B-Instruct` via Hugging Face InferenceClient with Nebius provider. Requires `HF_TOKEN` environment variable.
86
+
87
+ ## Important Implementation Details
88
+
89
+ ### Image Dimension Handling
90
+
91
+ Images are automatically resized based on `calculate_dimensions()` function:
92
+ - VAE images: resized to maintain 1024×1024 area (1,048,576 pixels)
93
+ - Condition images: resized to maintain 384×384 area (147,456 pixels)
94
+ - Output dimensions must be divisible by 16 (vae_scale_factor × 2)
95
+ - Height/width default to `None` which auto-calculates from input aspect ratio
96
+
97
+ ### LoRA Integration
98
+
99
+ The pipeline fuses the "next-scene" LoRA adapter at initialization:
100
+ ```python
101
+ pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", ...)
102
+ pipe.set_adapters(["next-scene"], adapter_weights=[1.])
103
+ pipe.fuse_lora(adapter_names=["next-scene"], lora_scale=1.)
104
+ pipe.unload_lora_weights()
105
+ ```
106
+
107
+ After fusion, the adapter weights are merged into the base model and cannot be unfused.
108
+
109
+ ### Video Generation Integration
110
+
111
+ The `turn_into_video()` function:
112
+ - Connects only when `VIDEO_SPACE_ID` is configured
113
+ - Requires first input image and last output image
114
+ - Uses the original prompt (or "smooth cinematic transition" fallback)
115
+ - Returns video path for display
116
+
117
+ ### Gradio Gallery Format
118
+
119
+ Input/output galleries use `type="filepath"` (string paths) rather than PIL Image tuples. Helper functions handle format compatibility for legacy tuple support.
120
+
121
+ ## Environment Variables
122
+
123
+ - `HF_TOKEN` - Required for Qwen2.5-VL API access (prompt rewriting/generation)
124
+
125
+ ## File Outputs
126
+
127
+ Generated images are saved to `outputs/` directory with format:
128
+ ```
129
+ output_{seed}_{index}_{timestamp_ms}.png
130
+ ```
131
+
132
+ ## Local Development and API Testing
133
+
134
+ The `custom/` directory is fully gitignored and used for local development files. Specifically, it contains:
135
+
136
+ - **API client scripts** - For testing the Gradio Space remotely via API after deployment to Hugging Face
137
+ - **`API_GUIDE.txt`** - Auto-generated Gradio API documentation showing endpoint signatures and example usage
138
+ - **Local testing environments** - Virtual environments or test data that shouldn't be committed
139
+
140
+ **API Integration Pattern:**
141
+ Once the Space is deployed to Hugging Face, you can interact with it programmatically using `gradio_client`:
142
+
143
+ ```python
144
+ from gradio_client import Client, handle_file
145
+
146
+ client = Client("clopeux/Pro-Realism-Edit-Studio")
147
+ result = client.predict(
148
+ images=[],
149
+ prompt="Next Scene: Camera dollies forward...",
150
+ seed=42,
151
+ randomize_seed=False,
152
+ true_guidance_scale=1.0,
153
+ num_inference_steps=4,
154
+ height=1024,
155
+ width=1024,
156
+ rewrite_prompt=False,
157
+ api_name="/infer"
158
+ )
159
+ ```
160
+
161
+ The `custom/API_GUIDE.txt` contains full documentation of all available endpoints including `/infer`, `/turn_into_video`, `/suggest_next_scene_prompt`, and utility functions.
162
+
163
+ ## Development Notes
164
+
165
+ - The model loads on startup and applies AOT compilation during first inference
166
+ - Compilation uses dynamic shapes to support variable text/image sequence lengths
167
+ - The transformer uses custom cache contexts ("cond"/"uncond") to optimize CFG passes
168
+ - True CFG applies norm-based rescaling: `comb_pred * (cond_norm / noise_norm)`
169
+ - FlashAttention 3 processor must be set before compilation
CLAUDE.md CHANGED
@@ -1,154 +1,154 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- This is a Gradio Space that implements "Next Scene" cinematic image generation using Qwen-Image-Edit-2511 with LoRA fine-tuning. The application generates visually progressive image sequences with natural cinematic transitions from frame to frame, optimized for fast 4-step inference.
8
-
9
- **Key Model Components:**
10
- - Base model: `Qwen/Qwen-Image-Edit-2511` (image editing diffusion model)
11
- - Accelerated transformer: `Phr00t/Qwen-Image-Edit-Rapid-AIO` v23 single-file checkpoint (4-step optimized)
12
- - LoRA adapter: `lovis93/next-scene-qwen-image-lora-2509` (cinematic progression fine-tune, trained on 2509)
13
-
14
- ## Running the Application
15
-
16
- **Start the Gradio interface:**
17
- ```bash
18
- python app.py
19
- ```
20
-
21
- **Install dependencies:**
22
- ```bash
23
- pip install -r requirements.txt
24
- ```
25
-
26
- The app requires GPU access. It uses the `@spaces.GPU` decorator for Hugging Face Spaces zero-GPU allocation.
27
-
28
- ## Architecture
29
-
30
- ### Pipeline Flow
31
-
32
- 1. **Input Processing** (`app.py:infer`):
33
- - Accepts input images via Gradio Gallery (filepath-based)
34
- - Uses user-provided prompts directly without modification
35
-
36
- 2. **Image Generation** (`qwenimage/pipeline_qwenimage_edit_plus.py`):
37
- - Custom pipeline extending `DiffusionPipeline`
38
- - Encodes images using VAE at 1024x1024 for latents
39
- - Encodes conditioning images at 384x384 for text encoder
40
- - Packs latents into 2x2 patches (latent dims must be divisible by 2)
41
- - Uses `FlowMatchEulerDiscreteScheduler` for denoising
42
-
43
- 3. **Optimization** (`optimization.py`):
44
- - Ahead-of-time (AOT) compilation using `torch.export` and `spaces.aoti_compile`
45
- - Dynamic shapes for variable sequence lengths
46
- - Custom inductor configs for performance (max_autotune, cudagraphs)
47
- - FlashAttention 3 integration via `QwenDoubleStreamAttnProcessorFA3`
48
-
49
- 4. **Output Handling**:
50
- - Saves outputs to `outputs/` directory with unique timestamps
51
- - Maintains 20-image history gallery
52
- - Optional video generation through an owned Gradio Space configured by `VIDEO_SPACE_ID`
53
-
54
- ### Custom QwenImage Components
55
-
56
- **Location:** `qwenimage/` package
57
-
58
- - `pipeline_qwenimage_edit_plus.py` - Main diffusion pipeline with LoRA support
59
- - `transformer_qwenimage.py` - Custom transformer model with cache management
60
- - `qwen_fa3_processor.py` - FlashAttention 3 attention processor
61
-
62
- **Key architectural features:**
63
- - Latent packing/unpacking for 2x2 patch processing
64
- - Multi-image conditioning support
65
- - True CFG (classifier-free guidance) with separate pos/neg paths
66
- - Dual-stream attention with rotary embeddings
67
- - Cache contexts for conditional/unconditional forward passes
68
-
69
- ### Prompt Handling
70
-
71
- The application uses user-provided prompts directly without any preprocessing, rewriting, or AI-based enhancement. Users have full control over the exact prompt text that gets passed to the diffusion model.
72
-
73
- ## Important Implementation Details
74
-
75
- ### Image Dimension Handling
76
-
77
- Images are automatically resized based on `calculate_dimensions()` function:
78
- - VAE images: resized to maintain 1024×1024 area (1,048,576 pixels)
79
- - Condition images: resized to maintain 384×384 area (147,456 pixels)
80
- - Output dimensions must be divisible by 16 (vae_scale_factor × 2)
81
- - Height/width default to `None` which auto-calculates from input aspect ratio
82
-
83
- ### LoRA Integration
84
-
85
- The pipeline fuses the "next-scene" LoRA adapter at initialization:
86
- ```python
87
- pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", ...)
88
- pipe.set_adapters(["next-scene"], adapter_weights=[1.])
89
- pipe.fuse_lora(adapter_names=["next-scene"], lora_scale=1.)
90
- pipe.unload_lora_weights()
91
- ```
92
-
93
- After fusion, the adapter weights are merged into the base model and cannot be unfused.
94
-
95
- ### Video Generation Integration
96
-
97
- The `turn_into_video()` function:
98
- - Connects only when `VIDEO_SPACE_ID` is configured
99
- - Requires first input image and last output image
100
- - Uses the original prompt (or "smooth cinematic transition" fallback)
101
- - Returns video path for display
102
-
103
- ### Gradio Gallery Format
104
-
105
- Input/output galleries use `type="filepath"` (string paths) rather than PIL Image tuples. Helper functions handle format compatibility for legacy tuple support.
106
-
107
- ## Environment Variables
108
-
109
- No environment variables are required for basic operation. The application runs entirely with local models.
110
-
111
- ## File Outputs
112
-
113
- Generated images are saved to `outputs/` directory with format:
114
- ```
115
- output_{seed}_{index}_{timestamp_ms}.png
116
- ```
117
-
118
- ## Local Development and API Testing
119
-
120
- The `custom/` directory is fully gitignored and used for local development files. Specifically, it contains:
121
-
122
- - **API client scripts** - For testing the Gradio Space remotely via API after deployment to Hugging Face
123
- - **`API_GUIDE.txt`** - Auto-generated Gradio API documentation showing endpoint signatures and example usage
124
- - **Local testing environments** - Virtual environments or test data that shouldn't be committed
125
-
126
- **API Integration Pattern:**
127
- Once the Space is deployed to Hugging Face, you can interact with it programmatically using `gradio_client`:
128
-
129
- ```python
130
- from gradio_client import Client, handle_file
131
-
132
- client = Client("clopeux/Pro-Realism-Edit-Studio")
133
- result = client.predict(
134
- images=[],
135
- prompt="Camera dollies forward, revealing more of the scene",
136
- seed=42,
137
- randomize_seed=False,
138
- true_guidance_scale=1.0,
139
- num_inference_steps=4,
140
- height=1024,
141
- width=1024,
142
- api_name="/infer"
143
- )
144
- ```
145
-
146
- The `custom/API_GUIDE.txt` contains full documentation of all available endpoints including `/infer`, `/turn_into_video`, and utility functions.
147
-
148
- ## Development Notes
149
-
150
- - The model loads on startup and applies AOT compilation during first inference
151
- - Compilation uses dynamic shapes to support variable text/image sequence lengths
152
- - The transformer uses custom cache contexts ("cond"/"uncond") to optimize CFG passes
153
- - True CFG applies norm-based rescaling: `comb_pred * (cond_norm / noise_norm)`
154
- - FlashAttention 3 processor must be set before compilation
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Gradio Space that implements "Next Scene" cinematic image generation using Qwen-Image-Edit-2511 with LoRA fine-tuning. The application generates visually progressive image sequences with natural cinematic transitions from frame to frame, optimized for fast 4-step inference.
8
+
9
+ **Key Model Components:**
10
+ - Base model: `Qwen/Qwen-Image-Edit-2511` (image editing diffusion model)
11
+ - Accelerated transformer: `Phr00t/Qwen-Image-Edit-Rapid-AIO` v23 single-file checkpoint (4-step optimized)
12
+ - LoRA adapter: `lovis93/next-scene-qwen-image-lora-2509` (cinematic progression fine-tune, trained on 2509)
13
+
14
+ ## Running the Application
15
+
16
+ **Start the Gradio interface:**
17
+ ```bash
18
+ python app.py
19
+ ```
20
+
21
+ **Install dependencies:**
22
+ ```bash
23
+ pip install -r requirements.txt
24
+ ```
25
+
26
+ The app requires GPU access. It uses the `@spaces.GPU` decorator for Hugging Face Spaces zero-GPU allocation.
27
+
28
+ ## Architecture
29
+
30
+ ### Pipeline Flow
31
+
32
+ 1. **Input Processing** (`app.py:infer`):
33
+ - Accepts input images via Gradio Gallery (filepath-based)
34
+ - Uses user-provided prompts directly without modification
35
+
36
+ 2. **Image Generation** (`qwenimage/pipeline_qwenimage_edit_plus.py`):
37
+ - Custom pipeline extending `DiffusionPipeline`
38
+ - Encodes images using VAE at 1024x1024 for latents
39
+ - Encodes conditioning images at 384x384 for text encoder
40
+ - Packs latents into 2x2 patches (latent dims must be divisible by 2)
41
+ - Uses `FlowMatchEulerDiscreteScheduler` for denoising
42
+
43
+ 3. **Optimization** (`optimization.py`):
44
+ - Ahead-of-time (AOT) compilation using `torch.export` and `spaces.aoti_compile`
45
+ - Dynamic shapes for variable sequence lengths
46
+ - Custom inductor configs for performance (max_autotune, cudagraphs)
47
+ - FlashAttention 3 integration via `QwenDoubleStreamAttnProcessorFA3`
48
+
49
+ 4. **Output Handling**:
50
+ - Saves outputs to `outputs/` directory with unique timestamps
51
+ - Maintains 20-image history gallery
52
+ - Optional video generation through an owned Gradio Space configured by `VIDEO_SPACE_ID`
53
+
54
+ ### Custom QwenImage Components
55
+
56
+ **Location:** `qwenimage/` package
57
+
58
+ - `pipeline_qwenimage_edit_plus.py` - Main diffusion pipeline with LoRA support
59
+ - `transformer_qwenimage.py` - Custom transformer model with cache management
60
+ - `qwen_fa3_processor.py` - FlashAttention 3 attention processor
61
+
62
+ **Key architectural features:**
63
+ - Latent packing/unpacking for 2x2 patch processing
64
+ - Multi-image conditioning support
65
+ - True CFG (classifier-free guidance) with separate pos/neg paths
66
+ - Dual-stream attention with rotary embeddings
67
+ - Cache contexts for conditional/unconditional forward passes
68
+
69
+ ### Prompt Handling
70
+
71
+ The application uses user-provided prompts directly without any preprocessing, rewriting, or AI-based enhancement. Users have full control over the exact prompt text that gets passed to the diffusion model.
72
+
73
+ ## Important Implementation Details
74
+
75
+ ### Image Dimension Handling
76
+
77
+ Images are automatically resized based on `calculate_dimensions()` function:
78
+ - VAE images: resized to maintain 1024×1024 area (1,048,576 pixels)
79
+ - Condition images: resized to maintain 384×384 area (147,456 pixels)
80
+ - Output dimensions must be divisible by 16 (vae_scale_factor × 2)
81
+ - Height/width default to `None` which auto-calculates from input aspect ratio
82
+
83
+ ### LoRA Integration
84
+
85
+ The pipeline fuses the "next-scene" LoRA adapter at initialization:
86
+ ```python
87
+ pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", ...)
88
+ pipe.set_adapters(["next-scene"], adapter_weights=[1.])
89
+ pipe.fuse_lora(adapter_names=["next-scene"], lora_scale=1.)
90
+ pipe.unload_lora_weights()
91
+ ```
92
+
93
+ After fusion, the adapter weights are merged into the base model and cannot be unfused.
94
+
95
+ ### Video Generation Integration
96
+
97
+ The `turn_into_video()` function:
98
+ - Connects only when `VIDEO_SPACE_ID` is configured
99
+ - Requires first input image and last output image
100
+ - Uses the original prompt (or "smooth cinematic transition" fallback)
101
+ - Returns video path for display
102
+
103
+ ### Gradio Gallery Format
104
+
105
+ Input/output galleries use `type="filepath"` (string paths) rather than PIL Image tuples. Helper functions handle format compatibility for legacy tuple support.
106
+
107
+ ## Environment Variables
108
+
109
+ No environment variables are required for basic operation. The application runs entirely with local models.
110
+
111
+ ## File Outputs
112
+
113
+ Generated images are saved to `outputs/` directory with format:
114
+ ```
115
+ output_{seed}_{index}_{timestamp_ms}.png
116
+ ```
117
+
118
+ ## Local Development and API Testing
119
+
120
+ The `custom/` directory is fully gitignored and used for local development files. Specifically, it contains:
121
+
122
+ - **API client scripts** - For testing the Gradio Space remotely via API after deployment to Hugging Face
123
+ - **`API_GUIDE.txt`** - Auto-generated Gradio API documentation showing endpoint signatures and example usage
124
+ - **Local testing environments** - Virtual environments or test data that shouldn't be committed
125
+
126
+ **API Integration Pattern:**
127
+ Once the Space is deployed to Hugging Face, you can interact with it programmatically using `gradio_client`:
128
+
129
+ ```python
130
+ from gradio_client import Client, handle_file
131
+
132
+ client = Client("clopeux/Pro-Realism-Edit-Studio")
133
+ result = client.predict(
134
+ images=[],
135
+ prompt="Camera dollies forward, revealing more of the scene",
136
+ seed=42,
137
+ randomize_seed=False,
138
+ true_guidance_scale=1.0,
139
+ num_inference_steps=4,
140
+ height=1024,
141
+ width=1024,
142
+ api_name="/infer"
143
+ )
144
+ ```
145
+
146
+ The `custom/API_GUIDE.txt` contains full documentation of all available endpoints including `/infer`, `/turn_into_video`, and utility functions.
147
+
148
+ ## Development Notes
149
+
150
+ - The model loads on startup and applies AOT compilation during first inference
151
+ - Compilation uses dynamic shapes to support variable text/image sequence lengths
152
+ - The transformer uses custom cache contexts ("cond"/"uncond") to optimize CFG passes
153
+ - True CFG applies norm-based rescaling: `comb_pred * (cond_norm / noise_norm)`
154
+ - FlashAttention 3 processor must be set before compilation
README.md CHANGED
@@ -1,17 +1,17 @@
1
- ---
2
- title: Pro Realism Edit Studio
3
- emoji: 🎨
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- version: 0.1.3
12
- short_description: Powerful image editing - supports one or two input images.
13
- ---
14
-
15
- Pro Realism Edit Studio is a powerful image editor powered by [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer for 4-step inference. Upload one or two input images, write a prompt, get high-quality results. Optional Enhance modes add lazy Nomos ATD upscaling, masked skin cleanup, and final detail grain.
16
-
17
- Video generation is disabled unless an owned Gradio video Space is configured with `VIDEO_SPACE_ID`.
 
1
+ ---
2
+ title: Pro Realism Edit Studio
3
+ emoji: 🎨
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ version: 0.1.3
12
+ short_description: Powerful image editing - supports one or two input images.
13
+ ---
14
+
15
+ Pro Realism Edit Studio is a powerful image editor powered by [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer for 4-step inference. Upload one or two input images, write a prompt, get high-quality results. Optional Enhance modes add lazy Nomos ATD upscaling, masked skin cleanup, and final detail grain.
16
+
17
+ Video generation is disabled unless an owned Gradio video Space is configured with `VIDEO_SPACE_ID`.
app.py CHANGED
@@ -1,602 +1,986 @@
1
- import gradio as gr
2
- import numpy as np
3
- import random
4
- import torch
5
- import spaces
6
-
7
- from accelerate import init_empty_weights
8
- from collections import OrderedDict
9
- from PIL import Image, ImageEnhance, ImageFilter
10
- from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel
11
- from diffusers.models.model_loading_utils import load_model_dict_into_meta
12
- from huggingface_hub import hf_hub_download
13
- from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
14
- from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
15
- from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
16
- from safetensors import safe_open
17
-
18
- import os
19
- import time # Added for history update delay
20
-
21
- from gradio_client import Client, handle_file
22
- import tempfile
23
-
24
- BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511"
25
- APP_VERSION = "0.1.3"
26
- PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip()
27
- RAPID_TRANSFORMER_FILENAME = os.environ.get(
28
- "RAPID_TRANSFORMER_FILENAME",
29
- "v23/Qwen-Rapid-AIO-NSFW-v23.safetensors",
30
- ).strip()
31
- PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model."
32
- VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip()
33
- UPSCALER_MODEL_ID = os.environ.get("UPSCALER_MODEL_ID", "Phips/4xNomos8k_atd_jpg").strip()
34
- UPSCALER_MODEL_FILENAME = os.environ.get("UPSCALER_MODEL_FILENAME", "4xNomos8k_atd_jpg.safetensors").strip()
35
- UPSCALER_TILE_SIZE = int(os.environ.get("UPSCALER_TILE_SIZE", "512"))
36
- UPSCALER_TILE_OVERLAP = int(os.environ.get("UPSCALER_TILE_OVERLAP", "32"))
37
- ENHANCE_MAX_INPUT_EDGE = int(os.environ.get("ENHANCE_MAX_INPUT_EDGE", "1280"))
38
- ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.018"))
39
-
40
- ENHANCE_MODE_OFF = "Off"
41
- ENHANCE_MODE_UPSCALE = "Upscale"
42
- ENHANCE_MODE_CLEAN = "Clean"
43
- ENHANCE_MODE_MAX_DETAIL = "Max Detail"
44
- ENHANCE_MODE_CHOICES = [ENHANCE_MODE_OFF, ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL]
45
-
46
- _upscaler_model = None
47
-
48
- def turn_into_video(input_image, output_images, prompt, progress=gr.Progress(track_tqdm=True)):
49
- if not VIDEO_SPACE_ID:
50
- raise gr.Error("Video generation is not configured for this Space.")
51
- if not input_image or not output_images:
52
- raise gr.Error("Please generate an output image first.")
53
-
54
- progress(0.02, desc="Preparing images...")
55
-
56
- def extract_pil(img_entry):
57
- if isinstance(img_entry, tuple) and isinstance(img_entry[0], Image.Image):
58
- return img_entry[0]
59
- elif isinstance(img_entry, Image.Image):
60
- return img_entry
61
- elif isinstance(img_entry, str):
62
- return Image.open(img_entry)
63
- else:
64
- raise gr.Error(f"Unsupported image format: {type(img_entry)}")
65
-
66
- start_img = extract_pil(input_image)
67
- end_img = extract_pil(output_images[0])
68
-
69
- progress(0.10, desc="Saving temp files...")
70
-
71
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_start, \
72
- tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_end:
73
- start_img.save(tmp_start.name)
74
- end_img.save(tmp_end.name)
75
-
76
- progress(0.20, desc="Connecting to video Space...")
77
-
78
- client = Client(VIDEO_SPACE_ID)
79
-
80
- progress(0.35, desc="Generating video...")
81
-
82
- video_path, seed = client.predict(
83
- start_image_pil=handle_file(tmp_start.name),
84
- end_image_pil=handle_file(tmp_end.name),
85
- prompt=prompt or "smooth cinematic transition",
86
- api_name="/generate_video"
87
- )
88
-
89
- progress(0.95, desc="Finalizing...")
90
- print(video_path)
91
- return video_path['video']
92
-
93
-
94
- def update_history(new_images, history):
95
- """Updates the history gallery with the new images."""
96
- time.sleep(0.5) # Small delay to ensure images are ready
97
- if history is None:
98
- history = []
99
- if new_images is not None and len(new_images) > 0:
100
- if not isinstance(history, list):
101
- history = list(history) if history else []
102
- for img in new_images:
103
- history.insert(0, img)
104
- history = history[:20] # Keep only last 20 images
105
- return history
106
-
107
- def use_history_as_input(evt: gr.SelectData):
108
- """Sets the selected history image into the Image 1 slot."""
109
- if evt.value is not None:
110
- # gr.Image with type='filepath' accepts a path directly.
111
- return gr.update(value=evt.value)
112
- return gr.update()
113
-
114
- # --- Model Loading ---
115
- dtype = torch.bfloat16
116
- device = "cuda" if torch.cuda.is_available() else "cpu"
117
-
118
-
119
- def load_phr00t_rapid_transformer(torch_dtype):
120
- checkpoint_path = hf_hub_download(
121
- repo_id=PHR00T_REPO_ID,
122
- filename=RAPID_TRANSFORMER_FILENAME,
123
- )
124
- config = DiffusersQwenImageTransformer2DModel.load_config(
125
- BASE_MODEL_ID,
126
- subfolder="transformer",
127
- )
128
- with init_empty_weights():
129
- transformer = DiffusersQwenImageTransformer2DModel.from_config(config)
130
-
131
- expected_keys = set(transformer.state_dict().keys())
132
- state_dict = OrderedDict()
133
- with safe_open(checkpoint_path, framework="pt", device="cpu") as checkpoint:
134
- for key in checkpoint.keys():
135
- if not key.startswith(PHR00T_TRANSFORMER_PREFIX):
136
- continue
137
- mapped_key = key.removeprefix(PHR00T_TRANSFORMER_PREFIX)
138
- if mapped_key in expected_keys:
139
- state_dict[mapped_key] = checkpoint.get_tensor(key)
140
-
141
- missing_keys = sorted(expected_keys.difference(state_dict.keys()))
142
- if missing_keys:
143
- sample = ", ".join(missing_keys[:20])
144
- raise RuntimeError(
145
- f"Phr00t Rapid-AIO transformer checkpoint is missing {len(missing_keys)} "
146
- f"required diffusers keys after prefix conversion. First missing keys: {sample}"
147
- )
148
-
149
- load_model_dict_into_meta(transformer, state_dict, dtype=torch_dtype)
150
- meta_parameters = [name for name, parameter in transformer.named_parameters() if parameter.is_meta]
151
- if meta_parameters:
152
- sample = ", ".join(meta_parameters[:20])
153
- raise RuntimeError(
154
- f"Phr00t Rapid-AIO transformer still has {len(meta_parameters)} meta parameters "
155
- f"after loading. First meta parameters: {sample}"
156
- )
157
-
158
- transformer.eval()
159
- return transformer
160
-
161
-
162
- # Load Qwen-Image-Edit-2511 with Phr00t's v23 accelerated transformer (4-step inference)
163
- pipe = QwenImageEditPlusPipeline.from_pretrained(
164
- BASE_MODEL_ID,
165
- transformer=load_phr00t_rapid_transformer(dtype),
166
- torch_dtype=dtype
167
- ).to(device)
168
-
169
- # Load next-scene LoRA for cinematic progression
170
- # Note: This LoRA was trained on 2509, may need testing with 2511/v23
171
- # TODO: Re-enable after testing base 2511/v23 works correctly
172
- # pipe.load_lora_weights(
173
- # "lovis93/next-scene-qwen-image-lora-2509",
174
- # weight_name="next-scene_lora-v2-3000.safetensors",
175
- # adapter_name="next-scene"
176
- # )
177
- # pipe.set_adapters(["next-scene"], adapter_weights=[1.])
178
- # pipe.fuse_lora(adapter_names=["next-scene"], lora_scale=1.)
179
- # pipe.unload_lora_weights()
180
-
181
-
182
- # Apply the same optimizations from the first version
183
- pipe.transformer.__class__ = QwenImageTransformer2DModel
184
- pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
185
-
186
- # --- Ahead-of-time compilation ---
187
- # Note: optimize_pipeline_ handles text encoder offloading internally to save memory during torch.export
188
- # DISABLED 2026-05-12: HF build pipeline force-pins spaces==0.49.3 which has a regression in
189
- # zero.torch.patching._move() — NVML assert during worker_init kills AOTI compile at startup.
190
- # Restore once HF bumps the pipeline to spaces==0.50.0+.
191
- # optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
192
-
193
- # --- UI Constants and Helpers ---
194
- MAX_SEED = np.iinfo(np.int32).max
195
-
196
-
197
- def load_upscaler_model():
198
- global _upscaler_model
199
- if _upscaler_model is not None:
200
- return _upscaler_model
201
-
202
- try:
203
- import spandrel
204
- import spandrel_extra_arches
205
- except ImportError as exc:
206
- raise gr.Error("Enhance mode requires spandrel and spandrel_extra_arches to be installed.") from exc
207
-
208
- spandrel_extra_arches.install()
209
- model_path = hf_hub_download(repo_id=UPSCALER_MODEL_ID, filename=UPSCALER_MODEL_FILENAME)
210
- model = spandrel.ModelLoader().load_from_file(model_path)
211
- model.eval().to(device)
212
- _upscaler_model = model
213
- return _upscaler_model
214
-
215
-
216
- def image_to_tensor(image):
217
- array = np.asarray(image.convert("RGB")).astype(np.float32) / 255.0
218
- tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0)
219
- return tensor.to(device)
220
-
221
-
222
- def tensor_to_image(tensor):
223
- array = tensor.squeeze(0).detach().float().cpu().clamp(0, 1).permute(1, 2, 0).numpy()
224
- return Image.fromarray((array * 255.0).round().astype(np.uint8), mode="RGB")
225
-
226
-
227
- def validate_enhance_input_size(image):
228
- max_edge = max(image.size)
229
- if max_edge > ENHANCE_MAX_INPUT_EDGE:
230
- raise gr.Error(
231
- f"Enhance mode accepts images up to {ENHANCE_MAX_INPUT_EDGE}px on the longest edge. "
232
- f"Current image is {image.width}x{image.height}."
233
- )
234
-
235
-
236
- def tile_upscale(image):
237
- validate_enhance_input_size(image)
238
- model = load_upscaler_model()
239
- tensor = image_to_tensor(image)
240
- _, _, height, width = tensor.shape
241
- tile_size = max(64, min(UPSCALER_TILE_SIZE, height, width))
242
- overlap = max(0, min(UPSCALER_TILE_OVERLAP, tile_size // 2))
243
- step = max(1, tile_size - overlap)
244
- y_positions = sorted(set(list(range(0, height, step)) + [max(0, height - tile_size)]))
245
- x_positions = sorted(set(list(range(0, width, step)) + [max(0, width - tile_size)]))
246
- output = None
247
- weights = None
248
-
249
- with torch.inference_mode():
250
- for y in y_positions:
251
- for x in x_positions:
252
- y1 = min(y + tile_size, height)
253
- x1 = min(x + tile_size, width)
254
- tile = tensor[:, :, y:y1, x:x1]
255
- upscaled_tile = model(tile).clamp(0, 1)
256
- scale_y = upscaled_tile.shape[-2] // tile.shape[-2]
257
- scale_x = upscaled_tile.shape[-1] // tile.shape[-1]
258
- if output is None:
259
- output = torch.zeros(
260
- (1, 3, height * scale_y, width * scale_x),
261
- dtype=upscaled_tile.dtype,
262
- device=upscaled_tile.device,
263
- )
264
- weights = torch.zeros_like(output)
265
- oy0, oy1 = y * scale_y, y1 * scale_y
266
- ox0, ox1 = x * scale_x, x1 * scale_x
267
- output[:, :, oy0:oy1, ox0:ox1] += upscaled_tile
268
- weights[:, :, oy0:oy1, ox0:ox1] += 1
269
-
270
- output = output / weights.clamp_min(1)
271
- return tensor_to_image(output)
272
-
273
-
274
- def skin_repair_mask(image):
275
- ycbcr = np.asarray(image.convert("YCbCr"))
276
- cb = ycbcr[:, :, 1]
277
- cr = ycbcr[:, :, 2]
278
- mask = (
279
- (cr >= 135)
280
- & (cr <= 180)
281
- & (cb >= 75)
282
- & (cb <= 135)
283
- ).astype(np.uint8) * 255
284
- mask_image = Image.fromarray(mask, mode="L")
285
- return mask_image.filter(ImageFilter.GaussianBlur(radius=1.2))
286
-
287
-
288
- def repair_skin_texture(image):
289
- base = image.convert("RGB")
290
- mask = skin_repair_mask(base)
291
- repaired = base.filter(ImageFilter.MedianFilter(size=3)).filter(ImageFilter.GaussianBlur(radius=0.35))
292
- blended = Image.composite(repaired, base, mask)
293
- return ImageEnhance.Sharpness(blended).enhance(1.08)
294
-
295
-
296
- def add_film_grain(image, seed):
297
- base = image.convert("RGB")
298
- array = np.asarray(base).astype(np.float32)
299
- rng = np.random.default_rng(seed)
300
- grain = rng.normal(0.0, 255.0 * ENHANCE_GRAIN_STRENGTH, size=(array.shape[0], array.shape[1], 1))
301
- array = np.clip(array + grain, 0, 255)
302
- return Image.fromarray(array.astype(np.uint8), mode="RGB")
303
-
304
-
305
- def apply_enhancement(image, enhance_mode, seed=0, progress=None):
306
- mode = enhance_mode or ENHANCE_MODE_OFF
307
- if mode not in ENHANCE_MODE_CHOICES:
308
- raise gr.Error(f"Unknown enhance mode: {mode}")
309
- if mode == ENHANCE_MODE_OFF:
310
- return image
311
-
312
- enhanced = image.convert("RGB")
313
- if mode in (ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL):
314
- if progress:
315
- progress(0.76, desc="Repairing skin texture...")
316
- enhanced = repair_skin_texture(enhanced)
317
-
318
- if mode in (ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL):
319
- if progress:
320
- progress(0.82, desc="Upscaling image...")
321
- enhanced = tile_upscale(enhanced)
322
-
323
- if mode == ENHANCE_MODE_MAX_DETAIL:
324
- if progress:
325
- progress(0.92, desc="Adding final grain and sharpness...")
326
- enhanced = ImageEnhance.Sharpness(enhanced).enhance(1.12)
327
- enhanced = add_film_grain(enhanced, seed)
328
-
329
- return enhanced
330
-
331
- def use_output_as_input(output_images):
332
- """Move the first output image into the Image 1 slot."""
333
- if not output_images:
334
- return gr.update()
335
- first = output_images[0]
336
- # Gallery items can be filepath strings or (filepath, label) tuples.
337
- path = first[0] if isinstance(first, (list, tuple)) else first
338
- return gr.update(value=path)
339
-
340
- # --- Main Inference Function (with hardcoded negative prompt) ---
341
- @spaces.GPU(duration=60)
342
- def infer(
343
- image_1,
344
- image_2,
345
- prompt,
346
- seed=42,
347
- randomize_seed=False,
348
- true_guidance_scale=1.0,
349
- num_inference_steps=4,
350
- height=None,
351
- width=None,
352
- enhance_mode=ENHANCE_MODE_OFF,
353
- num_images_per_prompt=1,
354
- progress=gr.Progress(track_tqdm=True),
355
- ):
356
- """
357
- Generates an image using the local Qwen-Image diffusers pipeline.
358
- """
359
- # Hardcode the negative prompt as requested
360
- negative_prompt = " "
361
-
362
- if randomize_seed:
363
- seed = random.randint(0, MAX_SEED)
364
-
365
- # Set up the generator for reproducibility
366
- generator = torch.Generator(device=device).manual_seed(seed)
367
-
368
- # Load input images into PIL Images — two optional slots.
369
- pil_images = []
370
- for img in (image_1, image_2):
371
- if img is None:
372
- continue
373
- try:
374
- if isinstance(img, str):
375
- pil_images.append(Image.open(img).convert("RGB"))
376
- elif isinstance(img, Image.Image):
377
- pil_images.append(img.convert("RGB"))
378
- elif hasattr(img, "name"):
379
- pil_images.append(Image.open(img.name).convert("RGB"))
380
- except Exception:
381
- continue
382
-
383
- if height==256 and width==256:
384
- height, width = None, None
385
- print(f"Calling pipeline with prompt: '{prompt}'")
386
- print(f"Negative Prompt: '{negative_prompt}'")
387
- print(f"Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}, Size: {width}x{height}")
388
-
389
- # Generate the image
390
- images_pil = pipe(
391
- image=pil_images if len(pil_images) > 0 else None,
392
- prompt=prompt,
393
- height=height,
394
- width=width,
395
- negative_prompt=negative_prompt,
396
- num_inference_steps=num_inference_steps,
397
- generator=generator,
398
- true_cfg_scale=true_guidance_scale,
399
- num_images_per_prompt=num_images_per_prompt,
400
- ).images
401
-
402
- if enhance_mode != ENHANCE_MODE_OFF:
403
- images_pil = [
404
- apply_enhancement(img, enhance_mode, seed=seed + idx, progress=progress)
405
- for idx, img in enumerate(images_pil)
406
- ]
407
-
408
- # Save images to temporary files for proper serving
409
- output_paths = []
410
- os.makedirs("outputs", exist_ok=True)
411
- for idx, img in enumerate(images_pil):
412
- output_path = f"outputs/output_{seed}_{idx}_{int(time.time()*1000)}.png"
413
- img.save(output_path)
414
- output_paths.append(output_path)
415
-
416
- # Return image paths, seed, and make buttons visible when their feature is configured.
417
- return output_paths, seed, gr.update(visible=True), gr.update(visible=bool(VIDEO_SPACE_ID))
418
-
419
-
420
- # --- UI Layout ---
421
- css = """
422
- #col-container {
423
- margin: 0 auto;
424
- max-width: 1024px;
425
- }
426
- #logo-title {
427
- text-align: center;
428
- }
429
- #logo-title h1 {
430
- margin-bottom: 0;
431
- }
432
- #logo-title h2 {
433
- color: #5b47d1;
434
- font-style: italic;
435
- margin-top: 0;
436
- }
437
- #edit_text{margin-top: -62px !important}
438
- """
439
-
440
- with gr.Blocks(css=css) as demo:
441
- with gr.Column(elem_id="col-container"):
442
- gr.HTML(f"""
443
- <!-- v{APP_VERSION} -->
444
- <div id="logo-title">
445
- <h1>Pro Realism Edit Studio</h1>
446
- <h2>Rapid Edit ⚡</h2>
447
- </div>
448
- """)
449
- gr.Markdown("""
450
- This demo uses [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer + [AoT compilation & FA3](https://huggingface.co/blog/zerogpu-aoti) for fast 4-step inference.
451
-
452
- Upload an image and enter your prompt to edit it. The model will use your prompt exactly as provided.
453
- """)
454
- with gr.Row():
455
- with gr.Column():
456
- with gr.Row():
457
- image_1 = gr.Image(label="Image 1", type="filepath", interactive=True)
458
- image_2 = gr.Image(label="Image 2 (optional)", type="filepath", interactive=True)
459
-
460
- prompt = gr.Text(
461
- label="Prompt 🪄",
462
- show_label=True,
463
- placeholder="Enter your prompt here...",
464
- )
465
- enhance_mode = gr.Radio(
466
- label="Enhance / Amelioration",
467
- choices=ENHANCE_MODE_CHOICES,
468
- value=ENHANCE_MODE_OFF,
469
- interactive=True,
470
- )
471
- run_button = gr.Button("Edit!", variant="primary")
472
-
473
- with gr.Accordion("Advanced Settings", open=False):
474
-
475
-
476
- seed = gr.Slider(
477
- label="Seed",
478
- minimum=0,
479
- maximum=MAX_SEED,
480
- step=1,
481
- value=0,
482
- )
483
-
484
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
485
-
486
- with gr.Row():
487
-
488
- true_guidance_scale = gr.Slider(
489
- label="True guidance scale",
490
- minimum=1.0,
491
- maximum=10.0,
492
- step=0.1,
493
- value=1.0
494
- )
495
-
496
- num_inference_steps = gr.Slider(
497
- label="Number of inference steps",
498
- minimum=1,
499
- maximum=40,
500
- step=1,
501
- value=4,
502
- )
503
-
504
- height = gr.Slider(
505
- label="Height",
506
- minimum=256,
507
- maximum=2048,
508
- step=8,
509
- value=None,
510
- )
511
-
512
- width = gr.Slider(
513
- label="Width",
514
- minimum=256,
515
- maximum=2048,
516
- step=8,
517
- value=None,
518
- )
519
-
520
-
521
-
522
- with gr.Column():
523
- result = gr.Gallery(label="Result", show_label=False, type="filepath")
524
- with gr.Row():
525
- use_output_btn = gr.Button("↗️ Use as input", variant="secondary", size="sm", visible=False)
526
- turn_video_btn = gr.Button("🎬 Turn into Video", variant="secondary", size="sm", visible=False)
527
- output_video = gr.Video(label="Generated Video", autoplay=True, visible=False)
528
-
529
- with gr.Row(visible=False):
530
- gr.Markdown("### 📜 History")
531
- clear_history_button = gr.Button("🗑️ Clear History", size="sm", variant="stop")
532
-
533
- history_gallery = gr.Gallery(
534
- label="Click any image to use as input",
535
- interactive=False,
536
- show_label=True,
537
- visible=False
538
- )
539
-
540
-
541
-
542
-
543
-
544
- gr.on(
545
- triggers=[run_button.click, prompt.submit],
546
- fn=infer,
547
- inputs=[
548
- image_1,
549
- image_2,
550
- prompt,
551
- seed,
552
- randomize_seed,
553
- true_guidance_scale,
554
- num_inference_steps,
555
- height,
556
- width,
557
- enhance_mode,
558
- ],
559
- outputs=[result, seed, use_output_btn, turn_video_btn],
560
-
561
- ).then(
562
- fn=update_history,
563
- inputs=[result, history_gallery],
564
- outputs=history_gallery,
565
-
566
- )
567
-
568
- # Add the new event handler for the "Use Output as Input" button
569
- use_output_btn.click(
570
- fn=use_output_as_input,
571
- inputs=[result],
572
- outputs=[image_1]
573
- )
574
-
575
- # History gallery event handlers
576
- history_gallery.select(
577
- fn=use_history_as_input,
578
- inputs=None,
579
- outputs=[image_1],
580
-
581
- )
582
-
583
- clear_history_button.click(
584
- fn=lambda: [],
585
- inputs=None,
586
- outputs=history_gallery,
587
-
588
- )
589
-
590
- turn_video_btn.click(
591
- fn=lambda: gr.update(visible=True),
592
- inputs=None,
593
- outputs=[output_video],
594
- ).then(
595
- fn=turn_into_video,
596
- inputs=[image_1, result, prompt],
597
- outputs=[output_video],
598
- )
599
-
600
-
601
- if __name__ == "__main__":
602
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pro Realism Edit Studio - Enhanced Edition
4
+ =========================================
5
+
6
+ Advanced image editing and enhancement studio powered by:
7
+ - Qwen-Image-Edit-2511 with Phr00t's Rapid-AIO v23 accelerated transformer
8
+ - Real-ESRGAN for high-quality upscaling
9
+ - GFPGAN/CodeFormer for face restoration
10
+ - Multi-stage detail enhancement pipeline
11
+
12
+ Author: Enhanced with Hugging Face CLI and image generation expertise
13
+ Version: 1.0.0
14
+ """
15
+
16
+ import gradio as gr
17
+ import numpy as np
18
+ import random
19
+ import torch
20
+ import spaces
21
+ import os
22
+ import time
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+ # Advanced imports
27
+ from accelerate import init_empty_weights
28
+ from collections import OrderedDict
29
+ from PIL import Image, ImageEnhance, ImageFilter, ImageOps
30
+ from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel
31
+ from diffusers.models.model_loading_utils import load_model_dict_into_meta
32
+ from huggingface_hub import hf_hub_download, HfApi, login, whoami
33
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
34
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
35
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
36
+ from safetensors import safe_open
37
+
38
+ from gradio_client import Client, handle_file
39
+
40
+ # ============================================================================
41
+ # CONFIGURATION - Model IDs and Parameters
42
+ # ============================================================================
43
+
44
+ # Base model configuration
45
+ BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511"
46
+ APP_VERSION = "1.0.0"
47
+ PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip()
48
+ RAPID_TRANSFORMER_FILENAME = os.environ.get(
49
+ "RAPID_TRANSFORMER_FILENAME",
50
+ "v23/Qwen-Rapid-AIO-NSFW-v23.safetensors",
51
+ ).strip()
52
+ PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model."
53
+ VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip()
54
+
55
+ # Enhanced Upscaler Configuration
56
+ UPSCALER_MODEL_ID = os.environ.get("UPSCALER_MODEL_ID", "ai-forever/Real-ESRGAN").strip()
57
+ UPSCALER_MODEL_FILENAME = os.environ.get("UPSCALER_MODEL_FILENAME", "RealESRGAN_x4plus.pth").strip()
58
+ UPSCALER_TILE_SIZE = int(os.environ.get("UPSCALER_TILE_SIZE", "512"))
59
+ UPSCALER_TILE_OVERLAP = int(os.environ.get("UPSCALER_TILE_OVERLAP", "64"))
60
+ ENHANCE_MAX_INPUT_EDGE = int(os.environ.get("ENHANCE_MAX_INPUT_EDGE", "2048"))
61
+ ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.015"))
62
+
63
+ # Face Restoration Configuration
64
+ FACE_RESTORATION_MODEL = os.environ.get("FACE_RESTORATION_MODEL", "Xintao/GFPGAN").strip()
65
+ FACE_RESTORATION_WEIGHTS = os.environ.get("FACE_RESTORATION_WEIGHTS", "GFPGANv1.3.pth").strip()
66
+
67
+ # Advanced Detail Enhancement Configuration
68
+ DETAIL_ENHANCEMENT_ENABLED = os.environ.get("DETAIL_ENHANCEMENT_ENABLED", "true").lower() == "true"
69
+ SMART_SHARPENING_STRENGTH = float(os.environ.get("SMART_SHARPENING_STRENGTH", "1.15"))
70
+
71
+ # ============================================================================
72
+ # ENHANCEMENT MODES
73
+ # ============================================================================
74
+
75
+ ENHANCE_MODE_OFF = "Off"
76
+ ENHANCE_MODE_UPSCALE = "Upscale Only"
77
+ ENHANCE_MODE_CLEAN = "Clean & Restore"
78
+ ENHANCE_MODE_MAX_DETAIL = "Max Detail"
79
+ ENHANCE_MODE_FACE_ENHANCE = "Face Enhance"
80
+ ENHANCE_MODE_FULL_ENHANCE = "Full Enhance"
81
+ ENHANCE_MODE_CHOICES = [
82
+ ENHANCE_MODE_OFF,
83
+ ENHANCE_MODE_UPSCALE,
84
+ ENHANCE_MODE_CLEAN,
85
+ ENHANCE_MODE_MAX_DETAIL,
86
+ ENHANCE_MODE_FACE_ENHANCE,
87
+ ENHANCE_MODE_FULL_ENHANCE
88
+ ]
89
+
90
+ # ============================================================================
91
+ # GLOBAL MODEL CACHE
92
+ # ============================================================================
93
+
94
+ _upscaler_model = None
95
+ _face_restoration_model = None
96
+ _detail_enhancement_model = None
97
+
98
+ # ============================================================================
99
+ # HUGGING FACE CLI EXPERT FUNCTIONS
100
+ # ============================================================================
101
+
102
+ def download_model_with_retry(repo_id, filename, max_retries=3):
103
+ """Download model with retry logic and error handling"""
104
+ for attempt in range(max_retries):
105
+ try:
106
+ return hf_hub_download(repo_id=repo_id, filename=filename)
107
+ except Exception as e:
108
+ if attempt == max_retries - 1:
109
+ raise RuntimeError(f"Failed to download {filename} from {repo_id} after {max_retries} attempts: {e}")
110
+ time.sleep(2 ** attempt)
111
+ return None
112
+
113
+ # ============================================================================
114
+ # VIDEO GENERATION (Preserved from original)
115
+ # ============================================================================
116
+
117
+ def turn_into_video(input_image, output_images, prompt, progress=gr.Progress(track_tqdm=True)):
118
+ """Convert image edit into video transition"""
119
+ if not VIDEO_SPACE_ID:
120
+ raise gr.Error("Video generation is not configured for this Space.")
121
+ if not input_image or not output_images:
122
+ raise gr.Error("Please generate an output image first.")
123
+
124
+ progress(0.02, desc="Preparing images...")
125
+
126
+ def extract_pil(img_entry):
127
+ if isinstance(img_entry, tuple) and isinstance(img_entry[0], Image.Image):
128
+ return img_entry[0]
129
+ elif isinstance(img_entry, Image.Image):
130
+ return img_entry
131
+ elif isinstance(img_entry, str):
132
+ return Image.open(img_entry)
133
+ else:
134
+ raise gr.Error(f"Unsupported image format: {type(img_entry)}")
135
+
136
+ start_img = extract_pil(input_image)
137
+ end_img = extract_pil(output_images[0])
138
+
139
+ progress(0.10, desc="Saving temp files...")
140
+
141
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_start, \
142
+ tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_end:
143
+ start_img.save(tmp_start.name)
144
+ end_img.save(tmp_end.name)
145
+
146
+ progress(0.20, desc="Connecting to video Space...")
147
+
148
+ client = Client(VIDEO_SPACE_ID)
149
+
150
+ progress(0.35, desc="Generating video...")
151
+
152
+ video_path, seed = client.predict(
153
+ start_image_pil=handle_file(tmp_start.name),
154
+ end_image_pil=handle_file(tmp_end.name),
155
+ prompt=prompt or "smooth cinematic transition",
156
+ api_name="/generate_video"
157
+ )
158
+
159
+ progress(0.95, desc="Finalizing...")
160
+ return video_path['video']
161
+
162
+
163
+ # ============================================================================
164
+ # HISTORY MANAGEMENT (Enhanced)
165
+ # ============================================================================
166
+
167
+ def update_history(new_images, history):
168
+ """Updates the history gallery with the new images."""
169
+ time.sleep(0.3)
170
+ if history is None:
171
+ history = []
172
+ if new_images is not None and len(new_images) > 0:
173
+ if not isinstance(history, list):
174
+ history = list(history) if history else []
175
+ for img in new_images:
176
+ history.insert(0, img)
177
+ history = history[:50]
178
+ return history
179
+
180
+ def use_history_as_input(evt: gr.SelectData):
181
+ """Sets the selected history image into the Image 1 slot."""
182
+ if evt.value is not None:
183
+ return gr.update(value=evt.value)
184
+ return gr.update()
185
+
186
+ # ============================================================================
187
+ # MODEL LOADING (Enhanced with better error handling)
188
+ # ============================================================================
189
+
190
+ dtype = torch.bfloat16
191
+ device = "cuda" if torch.cuda.is_available() else "cpu"
192
+
193
+ def load_phr00t_rapid_transformer(torch_dtype):
194
+ """Load Phr00t's Rapid-AIO v23 transformer with enhanced error handling"""
195
+ checkpoint_path = download_model_with_retry(PHR00T_REPO_ID, RAPID_TRANSFORMER_FILENAME)
196
+
197
+ try:
198
+ config = DiffusersQwenImageTransformer2DModel.load_config(
199
+ BASE_MODEL_ID,
200
+ subfolder="transformer",
201
+ )
202
+ except Exception as e:
203
+ raise RuntimeError(f"Failed to load config for {BASE_MODEL_ID}: {e}")
204
+
205
+ with init_empty_weights():
206
+ transformer = DiffusersQwenImageTransformer2DModel.from_config(config)
207
+
208
+ expected_keys = set(transformer.state_dict().keys())
209
+ state_dict = OrderedDict()
210
+
211
+ try:
212
+ with safe_open(checkpoint_path, framework="pt", device="cpu") as checkpoint:
213
+ for key in checkpoint.keys():
214
+ if not key.startswith(PHR00T_TRANSFORMER_PREFIX):
215
+ continue
216
+ mapped_key = key.removeprefix(PHR00T_TRANSFORMER_PREFIX)
217
+ if mapped_key in expected_keys:
218
+ state_dict[mapped_key] = checkpoint.get_tensor(key)
219
+ except Exception as e:
220
+ raise RuntimeError(f"Failed to load checkpoint from {checkpoint_path}: {e}")
221
+
222
+ missing_keys = sorted(expected_keys.difference(state_dict.keys()))
223
+ if missing_keys:
224
+ sample = ", ".join(missing_keys[:20])
225
+ raise RuntimeError(
226
+ f"Phr00t Rapid-AIO transformer checkpoint is missing {len(missing_keys)} "
227
+ f"required diffusers keys after prefix conversion. First missing keys: {sample}"
228
+ )
229
+
230
+ try:
231
+ load_model_dict_into_meta(transformer, state_dict, dtype=torch_dtype)
232
+ except Exception as e:
233
+ raise RuntimeError(f"Failed to load state dict into meta: {e}")
234
+
235
+ meta_parameters = [name for name, parameter in transformer.named_parameters() if parameter.is_meta]
236
+ if meta_parameters:
237
+ sample = ", ".join(meta_parameters[:20])
238
+ raise RuntimeError(
239
+ f"Phr00t Rapid-AIO transformer still has {len(meta_parameters)} meta parameters "
240
+ f"after loading. First meta parameters: {sample}"
241
+ )
242
+
243
+ transformer.eval()
244
+ return transformer
245
+
246
+ # Load main pipeline
247
+ try:
248
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
249
+ BASE_MODEL_ID,
250
+ transformer=load_phr00t_rapid_transformer(dtype),
251
+ torch_dtype=dtype
252
+ ).to(device)
253
+ print("Successfully loaded Qwen-Image-Edit-2511 with Rapid-AIO v23 transformer")
254
+ except Exception as e:
255
+ print(f"Failed to load main pipeline: {e}")
256
+ raise
257
+
258
+ # Apply optimizations
259
+ pipe.transformer.__class__ = QwenImageTransformer2DModel
260
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
261
+ print("Applied FA3 attention processor optimization")
262
+
263
+ # ============================================================================
264
+ # ENHANCED UPSCALER (Real-ESRGAN based)
265
+ # ============================================================================
266
+
267
+ def load_upscaler_model():
268
+ """Load Real-ESRGAN model for high-quality upscaling"""
269
+ global _upscaler_model
270
+ if _upscaler_model is not None:
271
+ return _upscaler_model
272
+
273
+ try:
274
+ import spandrel
275
+ import spandrel_extra_arches
276
+ spandrel_extra_arches.install()
277
+ except ImportError as exc:
278
+ raise gr.Error("Enhance mode requires spandrel and spandrel_extra_arches to be installed. "
279
+ "Install with: pip install spandrel spandrel_extra_arches") from exc
280
+
281
+ try:
282
+ model_path = download_model_with_retry(UPSCALER_MODEL_ID, UPSCALER_MODEL_FILENAME)
283
+ model = spandrel.ModelLoader().load_from_file(model_path)
284
+ model.eval().to(device)
285
+ _upscaler_model = model
286
+ print(f"Successfully loaded upscaler: {UPSCALER_MODEL_ID}/{UPSCALER_MODEL_FILENAME}")
287
+ return _upscaler_model
288
+ except Exception as e:
289
+ print(f"Failed to load upscaler model: {e}")
290
+ print("Falling back to Nomos upscaler...")
291
+ try:
292
+ model_path = download_model_with_retry("Phips/4xNomos8k_atd_jpg", "4xNomos8k_atd_jpg.safetensors")
293
+ model = spandrel.ModelLoader().load_from_file(model_path)
294
+ model.eval().to(device)
295
+ _upscaler_model = model
296
+ return _upscaler_model
297
+ except Exception as fallback_error:
298
+ raise gr.Error(f"Failed to load all upscaler models: {e} | {fallback_error}")
299
+
300
+ def image_to_tensor(image):
301
+ """Convert PIL Image to tensor"""
302
+ array = np.asarray(image.convert("RGB")).astype(np.float32) / 255.0
303
+ tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0)
304
+ return tensor.to(device)
305
+
306
+ def tensor_to_image(tensor):
307
+ """Convert tensor to PIL Image"""
308
+ array = tensor.squeeze(0).detach().float().cpu().clamp(0, 1).permute(1, 2, 0).numpy()
309
+ return Image.fromarray((array * 255.0).round().astype(np.uint8), mode="RGB")
310
+
311
+ def validate_enhance_input_size(image):
312
+ """Validate image size for enhancement"""
313
+ max_edge = max(image.size)
314
+ if max_edge > ENHANCE_MAX_INPUT_EDGE:
315
+ raise gr.Error(
316
+ f"Enhance mode accepts images up to {ENHANCE_MAX_INPUT_EDGE}px on the longest edge. "
317
+ f"Current image is {image.width}x{image.height}. "
318
+ f"Consider resizing your image first."
319
+ )
320
+
321
+ def advanced_tile_upscale(image, scale=4):
322
+ """
323
+ Advanced tiling upscaler with improved blending and edge handling
324
+ Uses Real-ESRGAN for superior quality compared to Nomos
325
+ """
326
+ validate_enhance_input_size(image)
327
+ model = load_upscaler_model()
328
+ tensor = image_to_tensor(image)
329
+ _, _, height, width = tensor.shape
330
+
331
+ base_tile_size = UPSCALER_TILE_SIZE
332
+ optimal_tile_size = min(base_tile_size, max(height, width) // 2)
333
+ tile_size = max(64, optimal_tile_size)
334
+ overlap = max(0, min(UPSCALER_TILE_OVERLAP, tile_size // 2))
335
+ step = max(1, tile_size - overlap)
336
+
337
+ y_positions = list(range(0, height, step))
338
+ if y_positions[-1] + tile_size < height:
339
+ y_positions.append(max(0, height - tile_size))
340
+
341
+ x_positions = list(range(0, width, step))
342
+ if x_positions[-1] + tile_size < width:
343
+ x_positions.append(max(0, width - tile_size))
344
+
345
+ y_positions = sorted(set(y_positions))
346
+ x_positions = sorted(set(x_positions))
347
+
348
+ output = None
349
+ weights = None
350
+
351
+ with torch.inference_mode():
352
+ for y in y_positions:
353
+ for x in x_positions:
354
+ y1 = min(y + tile_size, height)
355
+ x1 = min(x + tile_size, width)
356
+ tile = tensor[:, :, y:y1, x:x1]
357
+
358
+ upscaled_tile = model(tile).clamp(0, 1)
359
+
360
+ scale_y = upscaled_tile.shape[-2] // tile.shape[-2]
361
+ scale_x = upscaled_tile.shape[-1] // tile.shape[-1]
362
+
363
+ if output is None:
364
+ output = torch.zeros(
365
+ (1, 3, height * scale_y, width * scale_x),
366
+ dtype=upscaled_tile.dtype,
367
+ device=upscaled_tile.device,
368
+ )
369
+ weights = torch.zeros_like(output)
370
+
371
+ oy0, oy1 = y * scale_y, y1 * scale_y
372
+ ox0, ox1 = x * scale_x, x1 * scale_x
373
+ output[:, :, oy0:oy1, ox0:ox1] += upscaled_tile
374
+ weights[:, :, oy0:oy1, ox0:ox1] += 1
375
+
376
+ output = output / weights.clamp_min(1)
377
+ return tensor_to_image(output)
378
+
379
+ # ============================================================================
380
+ # ENHANCED DETAILER (Multi-stage processing)
381
+ # ============================================================================
382
+
383
+ def smart_sharpen(image, strength=1.15):
384
+ """
385
+ Smart sharpening with edge detection to avoid oversharpening smooth areas
386
+ """
387
+ if strength <= 0:
388
+ return image
389
+
390
+ img_array = np.array(image.convert("RGB"))
391
+
392
+ if strength > 1.0:
393
+ enhanced = ImageEnhance.Sharpness(image).enhance(strength)
394
+
395
+ gray = image.convert("L")
396
+ edges = gray.filter(ImageFilter.FIND_EDGES)
397
+ edge_mask = edges.filter(ImageFilter.GaussianBlur(radius=1))
398
+ edge_mask = edge_mask.point(lambda x: min(x * 0.3, 255))
399
+
400
+ sharpened_array = np.array(enhanced)
401
+ original_array = img_array
402
+ edge_array = np.array(edge_mask).astype(float) / 255.0
403
+
404
+ for c in range(3):
405
+ sharpened_array[:, :, c] = (
406
+ edge_array * sharpened_array[:, :, c] +
407
+ (1 - edge_array) * original_array[:, :, c]
408
+ )
409
+
410
+ image = Image.fromarray(np.clip(sharpened_array, 0, 255).astype(np.uint8))
411
+
412
+ return image
413
+
414
+ def add_ultra_detail(image, strength=0.8):
415
+ """
416
+ Add ultra-fine details using high-frequency enhancement
417
+ """
418
+ if strength <= 0:
419
+ return image
420
+
421
+ original = image.convert("RGB")
422
+ blurred = original.filter(ImageFilter.GaussianBlur(radius=2))
423
+
424
+ high_freq = ImageChops.subtract(original, blurred)
425
+ high_freq_enhanced = ImageEnhance.Contrast(high_freq).enhance(1.0 + strength)
426
+ result = ImageChops.add(original, high_freq_enhanced)
427
+
428
+ return result
429
+
430
+ def apply_high_frequency_details(image, amount=0.6):
431
+ """
432
+ Apply high-frequency detail enhancement for crisp textures
433
+ """
434
+ if amount <= 0:
435
+ return image
436
+
437
+ scales = [1, 2, 4]
438
+ result = image.convert("RGB")
439
+
440
+ for scale in scales:
441
+ blurred = result.filter(ImageFilter.GaussianBlur(radius=scale))
442
+ high_freq = ImageChops.subtract(result, blurred)
443
+ enhanced_hf = ImageEnhance.Contrast(high_freq).enhance(1.0 + amount * 0.3)
444
+ result = ImageChops.add(result, enhanced_hf)
445
+
446
+ return result
447
+
448
+ # ============================================================================
449
+ # ENHANCED CLEANER (Face Restoration + Artifact Removal)
450
+ # ============================================================================
451
+
452
+ def load_face_restoration_model():
453
+ """Load GFPGAN model for face restoration"""
454
+ global _face_restoration_model
455
+ if _face_restoration_model is not None:
456
+ return _face_restoration_model
457
+
458
+ try:
459
+ from gfpgan import GFPGANer
460
+ model_path = download_model_with_retry(FACE_RESTORATION_MODEL, FACE_RESTORATION_WEIGHTS)
461
+
462
+ restorer = GFPGANer(
463
+ model_path=model_path,
464
+ upscale=1,
465
+ arch='clean',
466
+ channel_multiplier=2,
467
+ bg_upsampler=None
468
+ )
469
+
470
+ _face_restoration_model = restorer
471
+ print("Successfully loaded GFPGAN face restoration model")
472
+ return _face_restoration_model
473
+
474
+ except ImportError:
475
+ print("GFPGAN not available, face restoration will use fallback methods")
476
+ return None
477
+ except Exception as e:
478
+ print(f"Failed to load face restoration model: {e}")
479
+ return None
480
+
481
+ def restore_faces(image):
482
+ """Restore faces in an image using GFPGAN"""
483
+ restorer = load_face_restoration_model()
484
+ if restorer is None:
485
+ print("Face restoration model not available, using skin repair fallback")
486
+ return repair_skin_texture(image)
487
+
488
+ try:
489
+ img_array = np.array(image.convert("RGB"))
490
+ restored_array, _ = restorer.enhance(img_array, has_aligned=False, only_center_face=False, paste_back=True)
491
+ restored_image = Image.fromarray(restored_array.astype(np.uint8))
492
+ return restored_image
493
+ except Exception as e:
494
+ print(f"Face restoration failed: {e}, using skin repair fallback")
495
+ return repair_skin_texture(image)
496
+
497
+ def remove_artifacts(image):
498
+ """Remove compression artifacts and noise"""
499
+ denoised = image.filter(ImageFilter.MedianFilter(size=3))
500
+ smoothed = denoised.filter(ImageFilter.GaussianBlur(radius=0.5))
501
+ result = Image.blend(image, smoothed, alpha=0.3)
502
+ return result
503
+
504
+ def enhanced_skin_repair(image):
505
+ """Enhanced skin repair with better color detection and blending"""
506
+ base = image.convert("RGB")
507
+
508
+ ycbcr = np.asarray(base.convert("YCbCr"))
509
+ y, cb, cr = ycbcr[:, :, 0], ycbcr[:, :, 1], ycbcr[:, :, 2]
510
+
511
+ skin_mask = (
512
+ (cr > 130) & (cr < 170) &
513
+ (cb > 70) & (cb < 140) &
514
+ (y > 80)
515
+ ).astype(np.uint8) * 255
516
+
517
+ try:
518
+ import cv2
519
+ kernel = np.ones((5, 5), np.uint8)
520
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
521
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
522
+ skin_mask = cv2.GaussianBlur(skin_mask, (7, 7), 0)
523
+ except ImportError:
524
+ from scipy import ndimage
525
+ skin_mask = ndimage.binary_opening(skin_mask > 128, structure=np.ones((3, 3))).astype(np.uint8) * 255
526
+ skin_mask = ndimage.gaussian_filter(skin_mask, sigma=3)
527
+
528
+ mask_image = Image.fromarray(skin_mask, mode="L")
529
+ repaired = base.filter(ImageFilter.MedianFilter(size=3))
530
+ repaired = repaired.filter(ImageFilter.GaussianBlur(radius=0.4))
531
+ non_skin = ImageOps.invert(mask_image)
532
+ sharpened = ImageEnhance.Sharpness(base).enhance(1.15)
533
+ blended = Image.composite(repaired, sharpened, mask_image)
534
+ result = ImageEnhance.Sharpness(blended).enhance(1.05)
535
+ return result
536
+
537
+ # Original skin repair functions (preserved for compatibility)
538
+ def skin_repair_mask(image):
539
+ ycbcr = np.asarray(image.convert("YCbCr"))
540
+ cb = ycbcr[:, :, 1]
541
+ cr = ycbcr[:, :, 2]
542
+ mask = (
543
+ (cr >= 135)
544
+ & (cr <= 180)
545
+ & (cb >= 75)
546
+ & (cb <= 135)
547
+ ).astype(np.uint8) * 255
548
+ mask_image = Image.fromarray(mask, mode="L")
549
+ return mask_image.filter(ImageFilter.GaussianBlur(radius=1.2))
550
+
551
+ def repair_skin_texture(image):
552
+ base = image.convert("RGB")
553
+ mask = skin_repair_mask(base)
554
+ repaired = base.filter(ImageFilter.MedianFilter(size=3)).filter(ImageFilter.GaussianBlur(radius=0.35))
555
+ blended = Image.composite(repaired, base, mask)
556
+ return ImageEnhance.Sharpness(blended).enhance(1.08)
557
+
558
+ def add_film_grain(image, seed):
559
+ base = image.convert("RGB")
560
+ array = np.asarray(base).astype(np.float32)
561
+ rng = np.random.default_rng(seed)
562
+ grain = rng.normal(0.0, 255.0 * ENHANCE_GRAIN_STRENGTH, size=(array.shape[0], array.shape[1], 1))
563
+ array = np.clip(array + grain, 0, 255)
564
+ return Image.fromarray(array.astype(np.uint8), mode="RGB")
565
+
566
+ # ============================================================================
567
+ # ENHANCED APPLY ENHANCEMENT (Main enhancement pipeline)
568
+ # ============================================================================
569
+
570
+ def apply_enhancement(image, enhance_mode, seed=0, progress=None):
571
+ """
572
+ Apply various enhancement modes to the image
573
+ """
574
+ mode = enhance_mode or ENHANCE_MODE_OFF
575
+ if mode not in ENHANCE_MODE_CHOICES:
576
+ raise gr.Error(f"Unknown enhance mode: {mode}")
577
+ if mode == ENHANCE_MODE_OFF:
578
+ return image
579
+
580
+ enhanced = image.convert("RGB")
581
+
582
+ total_steps = 0
583
+ if mode == ENHANCE_MODE_UPSCALE:
584
+ total_steps = 1
585
+ elif mode == ENHANCE_MODE_CLEAN:
586
+ total_steps = 3
587
+ elif mode == ENHANCE_MODE_MAX_DETAIL:
588
+ total_steps = 4
589
+ elif mode == ENHANCE_MODE_FACE_ENHANCE:
590
+ total_steps = 2
591
+ elif mode == ENHANCE_MODE_FULL_ENHANCE:
592
+ total_steps = 5
593
+
594
+ step = 0
595
+
596
+ if mode == ENHANCE_MODE_FACE_ENHANCE:
597
+ if progress:
598
+ step += 1
599
+ progress(0.5 * step / total_steps, desc="Restoring faces...")
600
+ enhanced = restore_faces(enhanced)
601
+ if progress:
602
+ step += 1
603
+ progress(0.5 * step / total_steps, desc="Upscaling...")
604
+ enhanced = advanced_tile_upscale(enhanced)
605
+ return enhanced
606
+
607
+ if mode in (ENHANCE_MODE_CLEAN, ENHANCE_MODE_FULL_ENHANCE):
608
+ if progress:
609
+ step += 1
610
+ progress(0.7 * step / total_steps, desc="Removing artifacts...")
611
+ enhanced = remove_artifacts(enhanced)
612
+ if progress:
613
+ step += 1
614
+ progress(0.7 * step / total_steps, desc="Repairing skin and faces...")
615
+ enhanced = enhanced_skin_repair(enhanced)
616
+ enhanced = restore_faces(enhanced)
617
+
618
+ if mode in (ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE):
619
+ if progress:
620
+ step += 1
621
+ progress(0.8 * step / total_steps, desc="Upscaling image...")
622
+ enhanced = advanced_tile_upscale(enhanced)
623
+
624
+ if mode in (ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE):
625
+ if progress:
626
+ step += 1
627
+ progress(0.9 * step / total_steps, desc="Enhancing details...")
628
+ enhanced = add_ultra_detail(enhanced, strength=0.7)
629
+ enhanced = apply_high_frequency_details(enhanced, amount=0.5)
630
+ enhanced = smart_sharpen(enhanced, strength=SMART_SHARPENING_STRENGTH)
631
+ if progress:
632
+ step += 1
633
+ progress(0.95 * step / total_steps, desc="Adding final grain...")
634
+ enhanced = add_film_grain(enhanced, seed)
635
+
636
+ return enhanced
637
+
638
+ # ============================================================================
639
+ # UTILITY FUNCTIONS
640
+ # ============================================================================
641
+
642
+ def use_output_as_input(output_images):
643
+ """Move the first output image into the Image 1 slot."""
644
+ if not output_images:
645
+ return gr.update()
646
+ first = output_images[0]
647
+ path = first[0] if isinstance(first, (list, tuple)) else first
648
+ return gr.update(value=path)
649
+
650
+ def check_gpu_memory():
651
+ """Check available GPU memory"""
652
+ if device == "cuda":
653
+ try:
654
+ total = torch.cuda.get_device_properties(0).total_memory
655
+ reserved = torch.cuda.memory_reserved(0)
656
+ allocated = torch.cuda.memory_allocated(0)
657
+ free = total - reserved
658
+ print(f"GPU Memory: Total={total/1024**3:.2f}GB, Reserved={reserved/1024**3:.2f}GB, Allocated={allocated/1024**3:.2f}GB, Free={free/1024**3:.2f}GB")
659
+ return free > 1024**3
660
+ except Exception as e:
661
+ print(f"Failed to check GPU memory: {e}")
662
+ return True
663
+ return True
664
+
665
+ def clear_gpu_cache():
666
+ """Clear GPU cache to free up memory"""
667
+ if device == "cuda":
668
+ try:
669
+ torch.cuda.empty_cache()
670
+ import gc
671
+ gc.collect()
672
+ print("GPU cache cleared")
673
+ except Exception as e:
674
+ print(f"Failed to clear GPU cache: {e}")
675
+
676
+ # ============================================================================
677
+ # MAIN INFERENCE FUNCTION (Enhanced)
678
+ # ============================================================================
679
+
680
+ MAX_SEED = np.iinfo(np.int32).max
681
+
682
+ @spaces.GPU(duration=60)
683
+ def infer(
684
+ image_1,
685
+ image_2,
686
+ prompt,
687
+ seed=42,
688
+ randomize_seed=False,
689
+ true_guidance_scale=1.0,
690
+ num_inference_steps=4,
691
+ height=None,
692
+ width=None,
693
+ enhance_mode=ENHANCE_MODE_OFF,
694
+ num_images_per_prompt=1,
695
+ progress=gr.Progress(track_tqdm=True),
696
+ ):
697
+ """
698
+ Enhanced image generation with advanced editing and enhancement options
699
+ """
700
+ negative_prompt = " "
701
+
702
+ if randomize_seed:
703
+ seed = random.randint(0, MAX_SEED)
704
+
705
+ generator = torch.Generator(device=device).manual_seed(seed)
706
+
707
+ pil_images = []
708
+ for img in (image_1, image_2):
709
+ if img is None:
710
+ continue
711
+ try:
712
+ if isinstance(img, str):
713
+ pil_images.append(Image.open(img).convert("RGB"))
714
+ elif isinstance(img, Image.Image):
715
+ pil_images.append(img.convert("RGB"))
716
+ elif hasattr(img, "name"):
717
+ pil_images.append(Image.open(img.name).convert("RGB"))
718
+ except Exception:
719
+ continue
720
+
721
+ if height==256 and width==256:
722
+ height, width = None, None
723
+
724
+ print(f"Generation Parameters:")
725
+ print(f" Prompt: '{prompt}'")
726
+ print(f" Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}")
727
+ print(f" Size: {width}x{height}, Enhance Mode: {enhance_mode}")
728
+
729
+ if not check_gpu_memory():
730
+ clear_gpu_cache()
731
+ if not check_gpu_memory():
732
+ raise gr.Error("Insufficient GPU memory. Please reduce image size or close other applications.")
733
+
734
+ try:
735
+ images_pil = pipe(
736
+ image=pil_images if len(pil_images) > 0 else None,
737
+ prompt=prompt,
738
+ height=height,
739
+ width=width,
740
+ negative_prompt=negative_prompt,
741
+ num_inference_steps=num_inference_steps,
742
+ generator=generator,
743
+ true_cfg_scale=true_guidance_scale,
744
+ num_images_per_prompt=num_images_per_prompt,
745
+ ).images
746
+ except Exception as e:
747
+ clear_gpu_cache()
748
+ raise gr.Error(f"Image generation failed: {e}")
749
+
750
+ if enhance_mode != ENHANCE_MODE_OFF:
751
+ images_pil = [
752
+ apply_enhancement(img, enhance_mode, seed=seed + idx, progress=progress)
753
+ for idx, img in enumerate(images_pil)
754
+ ]
755
+
756
+ output_paths = []
757
+ os.makedirs("outputs", exist_ok=True)
758
+ for idx, img in enumerate(images_pil):
759
+ output_path = f"outputs/output_{seed}_{idx}_{int(time.time()*1000)}.png"
760
+ img.save(output_path)
761
+ output_paths.append(output_path)
762
+
763
+ clear_gpu_cache()
764
+
765
+ return output_paths, seed, gr.update(visible=True), gr.update(visible=bool(VIDEO_SPACE_ID))
766
+
767
+
768
+ # ============================================================================
769
+ # UI LAYOUT (Enhanced)
770
+ # ============================================================================
771
+
772
+ css = """
773
+ #col-container {
774
+ margin: 0 auto;
775
+ max-width: 1024px;
776
+ }
777
+ #logo-title {
778
+ text-align: center;
779
+ }
780
+ #logo-title h1 {
781
+ margin-bottom: 0;
782
+ }
783
+ #logo-title h2 {
784
+ color: #5b47d1;
785
+ font-style: italic;
786
+ margin-top: 0;
787
+ }
788
+ #edit_text{margin-top: -62px !important}
789
+ .enhance-info {
790
+ font-size: 0.9em;
791
+ color: #666;
792
+ margin-top: 5px;
793
+ }
794
+ """
795
+
796
+ with gr.Blocks(css=css) as demo:
797
+ with gr.Column(elem_id="col-container"):
798
+ gr.HTML(f"""
799
+ <div id="logo-title">
800
+ <h1>Pro Realism Edit Studio - Enhanced</h1>
801
+ <h2>Rapid Edit with Real-ESRGAN & Face Restoration</h2>
802
+ </div>
803
+ """)
804
+
805
+ gr.Markdown("""
806
+ Powered by:
807
+ - [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511)
808
+ - [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer
809
+ - [Real-ESRGAN](https://huggingface.co/ai-forever/Real-ESRGAN) for high-quality upscaling
810
+ - [GFPGAN](https://github.com/TencentARC/GFPGAN) for face restoration
811
+
812
+ Upload an image and enter your prompt to edit it.
813
+
814
+ Pro Tips:
815
+ - Use Face Enhance mode for portrait photography
816
+ - Use Max Detail for product shots and textures
817
+ - Use Full Enhance for comprehensive improvement
818
+ """)
819
+
820
+ with gr.Row():
821
+ with gr.Column():
822
+ with gr.Row():
823
+ image_1 = gr.Image(label="Image 1", type="filepath", interactive=True)
824
+ image_2 = gr.Image(label="Image 2 (optional)", type="filepath", interactive=True)
825
+
826
+ prompt = gr.Text(
827
+ label="Prompt",
828
+ show_label=True,
829
+ placeholder="Enter your prompt here...",
830
+ )
831
+
832
+ enhance_mode = gr.Radio(
833
+ label="Enhance Mode",
834
+ choices=ENHANCE_MODE_CHOICES,
835
+ value=ENHANCE_MODE_OFF,
836
+ interactive=True,
837
+ info="Choose enhancement level for your output"
838
+ )
839
+
840
+ enhance_info = gr.Markdown("""
841
+ Enhancement Options:
842
+ - Off: No post-processing
843
+ - Upscale Only: 4x upscaling with Real-ESRGAN
844
+ - Clean & Restore: Artifact removal + skin/face restoration
845
+ - Max Detail: Full detail enhancement with sharpening
846
+ - Face Enhance: Specialized face restoration + upscaling
847
+ - Full Enhance: Complete pipeline (clean + detail + face + upscale)
848
+ """, visible=False)
849
+
850
+ run_button = gr.Button("Generate!", variant="primary")
851
+
852
+ with gr.Accordion("Advanced Settings", open=False):
853
+ seed = gr.Slider(
854
+ label="Seed",
855
+ minimum=0,
856
+ maximum=MAX_SEED,
857
+ step=1,
858
+ value=0,
859
+ )
860
+
861
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
862
+
863
+ with gr.Row():
864
+ true_guidance_scale = gr.Slider(
865
+ label="True guidance scale",
866
+ minimum=1.0,
867
+ maximum=10.0,
868
+ step=0.1,
869
+ value=1.0
870
+ )
871
+
872
+ num_inference_steps = gr.Slider(
873
+ label="Number of inference steps",
874
+ minimum=1,
875
+ maximum=40,
876
+ step=1,
877
+ value=4,
878
+ )
879
+
880
+ with gr.Row():
881
+ height = gr.Slider(
882
+ label="Height",
883
+ minimum=256,
884
+ maximum=2048,
885
+ step=8,
886
+ value=None,
887
+ )
888
+
889
+ width = gr.Slider(
890
+ label="Width",
891
+ minimum=256,
892
+ maximum=2048,
893
+ step=8,
894
+ value=None,
895
+ )
896
+
897
+ gr.Markdown("""
898
+ Performance Tips:
899
+ - Use 4 steps for fastest results
900
+ - Increase steps (8-20) for better quality
901
+ - Lower guidance scale for more creative freedom
902
+ """)
903
+
904
+ with gr.Column():
905
+ result = gr.Gallery(label="Result", show_label=False, type="filepath")
906
+ with gr.Row():
907
+ use_output_btn = gr.Button("Use as input", variant="secondary", size="sm", visible=False)
908
+ turn_video_btn = gr.Button("Turn into Video", variant="secondary", size="sm", visible=False)
909
+ output_video = gr.Video(label="Generated Video", autoplay=True, visible=False)
910
+
911
+ with gr.Row():
912
+ gr.Markdown("### History")
913
+ clear_history_button = gr.Button("Clear History", size="sm", variant="stop")
914
+
915
+ history_gallery = gr.Gallery(
916
+ label="Click any image to use as input",
917
+ interactive=False,
918
+ show_label=True,
919
+ visible=True
920
+ )
921
+
922
+ gr.on(
923
+ triggers=[run_button.click, prompt.submit],
924
+ fn=infer,
925
+ inputs=[
926
+ image_1,
927
+ image_2,
928
+ prompt,
929
+ seed,
930
+ randomize_seed,
931
+ true_guidance_scale,
932
+ num_inference_steps,
933
+ height,
934
+ width,
935
+ enhance_mode,
936
+ ],
937
+ outputs=[result, seed, use_output_btn, turn_video_btn],
938
+
939
+ ).then(
940
+ fn=update_history,
941
+ inputs=[result, history_gallery],
942
+ outputs=history_gallery,
943
+ )
944
+
945
+ enhance_mode.change(
946
+ fn=lambda mode: gr.update(visible=mode != ENHANCE_MODE_OFF),
947
+ inputs=[enhance_mode],
948
+ outputs=[enhance_info]
949
+ )
950
+
951
+ use_output_btn.click(
952
+ fn=use_output_as_input,
953
+ inputs=[result],
954
+ outputs=[image_1]
955
+ )
956
+
957
+ history_gallery.select(
958
+ fn=use_history_as_input,
959
+ inputs=None,
960
+ outputs=[image_1],
961
+ )
962
+
963
+ clear_history_button.click(
964
+ fn=lambda: [],
965
+ inputs=None,
966
+ outputs=history_gallery,
967
+ )
968
+
969
+ turn_video_btn.click(
970
+ fn=lambda: gr.update(visible=True),
971
+ inputs=None,
972
+ outputs=[output_video],
973
+ ).then(
974
+ fn=turn_into_video,
975
+ inputs=[image_1, result, prompt],
976
+ outputs=[output_video],
977
+ )
978
+
979
+
980
+ if __name__ == "__main__":
981
+ print(f"Device: {device}")
982
+ if device == "cuda":
983
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
984
+ check_gpu_memory()
985
+ print(f"Starting Pro Realism Edit Studio v{APP_VERSION}")
986
+ demo.launch()
app_clean.py ADDED
@@ -0,0 +1,986 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pro Realism Edit Studio - Enhanced Edition
4
+ =========================================
5
+
6
+ Advanced image editing and enhancement studio powered by:
7
+ - Qwen-Image-Edit-2511 with Phr00t's Rapid-AIO v23 accelerated transformer
8
+ - Real-ESRGAN for high-quality upscaling
9
+ - GFPGAN/CodeFormer for face restoration
10
+ - Multi-stage detail enhancement pipeline
11
+
12
+ Author: Enhanced with Hugging Face CLI and image generation expertise
13
+ Version: 1.0.0
14
+ """
15
+
16
+ import gradio as gr
17
+ import numpy as np
18
+ import random
19
+ import torch
20
+ import spaces
21
+ import os
22
+ import time
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+ # Advanced imports
27
+ from accelerate import init_empty_weights
28
+ from collections import OrderedDict
29
+ from PIL import Image, ImageEnhance, ImageFilter, ImageOps
30
+ from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel
31
+ from diffusers.models.model_loading_utils import load_model_dict_into_meta
32
+ from huggingface_hub import hf_hub_download, HfApi, login, whoami
33
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
34
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
35
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
36
+ from safetensors import safe_open
37
+
38
+ from gradio_client import Client, handle_file
39
+
40
+ # ============================================================================
41
+ # CONFIGURATION - Model IDs and Parameters
42
+ # ============================================================================
43
+
44
+ # Base model configuration
45
+ BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511"
46
+ APP_VERSION = "1.0.0"
47
+ PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip()
48
+ RAPID_TRANSFORMER_FILENAME = os.environ.get(
49
+ "RAPID_TRANSFORMER_FILENAME",
50
+ "v23/Qwen-Rapid-AIO-NSFW-v23.safetensors",
51
+ ).strip()
52
+ PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model."
53
+ VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip()
54
+
55
+ # Enhanced Upscaler Configuration
56
+ UPSCALER_MODEL_ID = os.environ.get("UPSCALER_MODEL_ID", "ai-forever/Real-ESRGAN").strip()
57
+ UPSCALER_MODEL_FILENAME = os.environ.get("UPSCALER_MODEL_FILENAME", "RealESRGAN_x4plus.pth").strip()
58
+ UPSCALER_TILE_SIZE = int(os.environ.get("UPSCALER_TILE_SIZE", "512"))
59
+ UPSCALER_TILE_OVERLAP = int(os.environ.get("UPSCALER_TILE_OVERLAP", "64"))
60
+ ENHANCE_MAX_INPUT_EDGE = int(os.environ.get("ENHANCE_MAX_INPUT_EDGE", "2048"))
61
+ ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.015"))
62
+
63
+ # Face Restoration Configuration
64
+ FACE_RESTORATION_MODEL = os.environ.get("FACE_RESTORATION_MODEL", "Xintao/GFPGAN").strip()
65
+ FACE_RESTORATION_WEIGHTS = os.environ.get("FACE_RESTORATION_WEIGHTS", "GFPGANv1.3.pth").strip()
66
+
67
+ # Advanced Detail Enhancement Configuration
68
+ DETAIL_ENHANCEMENT_ENABLED = os.environ.get("DETAIL_ENHANCEMENT_ENABLED", "true").lower() == "true"
69
+ SMART_SHARPENING_STRENGTH = float(os.environ.get("SMART_SHARPENING_STRENGTH", "1.15"))
70
+
71
+ # ============================================================================
72
+ # ENHANCEMENT MODES
73
+ # ============================================================================
74
+
75
+ ENHANCE_MODE_OFF = "Off"
76
+ ENHANCE_MODE_UPSCALE = "Upscale Only"
77
+ ENHANCE_MODE_CLEAN = "Clean & Restore"
78
+ ENHANCE_MODE_MAX_DETAIL = "Max Detail"
79
+ ENHANCE_MODE_FACE_ENHANCE = "Face Enhance"
80
+ ENHANCE_MODE_FULL_ENHANCE = "Full Enhance"
81
+ ENHANCE_MODE_CHOICES = [
82
+ ENHANCE_MODE_OFF,
83
+ ENHANCE_MODE_UPSCALE,
84
+ ENHANCE_MODE_CLEAN,
85
+ ENHANCE_MODE_MAX_DETAIL,
86
+ ENHANCE_MODE_FACE_ENHANCE,
87
+ ENHANCE_MODE_FULL_ENHANCE
88
+ ]
89
+
90
+ # ============================================================================
91
+ # GLOBAL MODEL CACHE
92
+ # ============================================================================
93
+
94
+ _upscaler_model = None
95
+ _face_restoration_model = None
96
+ _detail_enhancement_model = None
97
+
98
+ # ============================================================================
99
+ # HUGGING FACE CLI EXPERT FUNCTIONS
100
+ # ============================================================================
101
+
102
+ def download_model_with_retry(repo_id, filename, max_retries=3):
103
+ """Download model with retry logic and error handling"""
104
+ for attempt in range(max_retries):
105
+ try:
106
+ return hf_hub_download(repo_id=repo_id, filename=filename)
107
+ except Exception as e:
108
+ if attempt == max_retries - 1:
109
+ raise RuntimeError(f"Failed to download {filename} from {repo_id} after {max_retries} attempts: {e}")
110
+ time.sleep(2 ** attempt)
111
+ return None
112
+
113
+ # ============================================================================
114
+ # VIDEO GENERATION (Preserved from original)
115
+ # ============================================================================
116
+
117
+ def turn_into_video(input_image, output_images, prompt, progress=gr.Progress(track_tqdm=True)):
118
+ """Convert image edit into video transition"""
119
+ if not VIDEO_SPACE_ID:
120
+ raise gr.Error("Video generation is not configured for this Space.")
121
+ if not input_image or not output_images:
122
+ raise gr.Error("Please generate an output image first.")
123
+
124
+ progress(0.02, desc="Preparing images...")
125
+
126
+ def extract_pil(img_entry):
127
+ if isinstance(img_entry, tuple) and isinstance(img_entry[0], Image.Image):
128
+ return img_entry[0]
129
+ elif isinstance(img_entry, Image.Image):
130
+ return img_entry
131
+ elif isinstance(img_entry, str):
132
+ return Image.open(img_entry)
133
+ else:
134
+ raise gr.Error(f"Unsupported image format: {type(img_entry)}")
135
+
136
+ start_img = extract_pil(input_image)
137
+ end_img = extract_pil(output_images[0])
138
+
139
+ progress(0.10, desc="Saving temp files...")
140
+
141
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_start, \
142
+ tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_end:
143
+ start_img.save(tmp_start.name)
144
+ end_img.save(tmp_end.name)
145
+
146
+ progress(0.20, desc="Connecting to video Space...")
147
+
148
+ client = Client(VIDEO_SPACE_ID)
149
+
150
+ progress(0.35, desc="Generating video...")
151
+
152
+ video_path, seed = client.predict(
153
+ start_image_pil=handle_file(tmp_start.name),
154
+ end_image_pil=handle_file(tmp_end.name),
155
+ prompt=prompt or "smooth cinematic transition",
156
+ api_name="/generate_video"
157
+ )
158
+
159
+ progress(0.95, desc="Finalizing...")
160
+ return video_path['video']
161
+
162
+
163
+ # ============================================================================
164
+ # HISTORY MANAGEMENT (Enhanced)
165
+ # ============================================================================
166
+
167
+ def update_history(new_images, history):
168
+ """Updates the history gallery with the new images."""
169
+ time.sleep(0.3)
170
+ if history is None:
171
+ history = []
172
+ if new_images is not None and len(new_images) > 0:
173
+ if not isinstance(history, list):
174
+ history = list(history) if history else []
175
+ for img in new_images:
176
+ history.insert(0, img)
177
+ history = history[:50]
178
+ return history
179
+
180
+ def use_history_as_input(evt: gr.SelectData):
181
+ """Sets the selected history image into the Image 1 slot."""
182
+ if evt.value is not None:
183
+ return gr.update(value=evt.value)
184
+ return gr.update()
185
+
186
+ # ============================================================================
187
+ # MODEL LOADING (Enhanced with better error handling)
188
+ # ============================================================================
189
+
190
+ dtype = torch.bfloat16
191
+ device = "cuda" if torch.cuda.is_available() else "cpu"
192
+
193
+ def load_phr00t_rapid_transformer(torch_dtype):
194
+ """Load Phr00t's Rapid-AIO v23 transformer with enhanced error handling"""
195
+ checkpoint_path = download_model_with_retry(PHR00T_REPO_ID, RAPID_TRANSFORMER_FILENAME)
196
+
197
+ try:
198
+ config = DiffusersQwenImageTransformer2DModel.load_config(
199
+ BASE_MODEL_ID,
200
+ subfolder="transformer",
201
+ )
202
+ except Exception as e:
203
+ raise RuntimeError(f"Failed to load config for {BASE_MODEL_ID}: {e}")
204
+
205
+ with init_empty_weights():
206
+ transformer = DiffusersQwenImageTransformer2DModel.from_config(config)
207
+
208
+ expected_keys = set(transformer.state_dict().keys())
209
+ state_dict = OrderedDict()
210
+
211
+ try:
212
+ with safe_open(checkpoint_path, framework="pt", device="cpu") as checkpoint:
213
+ for key in checkpoint.keys():
214
+ if not key.startswith(PHR00T_TRANSFORMER_PREFIX):
215
+ continue
216
+ mapped_key = key.removeprefix(PHR00T_TRANSFORMER_PREFIX)
217
+ if mapped_key in expected_keys:
218
+ state_dict[mapped_key] = checkpoint.get_tensor(key)
219
+ except Exception as e:
220
+ raise RuntimeError(f"Failed to load checkpoint from {checkpoint_path}: {e}")
221
+
222
+ missing_keys = sorted(expected_keys.difference(state_dict.keys()))
223
+ if missing_keys:
224
+ sample = ", ".join(missing_keys[:20])
225
+ raise RuntimeError(
226
+ f"Phr00t Rapid-AIO transformer checkpoint is missing {len(missing_keys)} "
227
+ f"required diffusers keys after prefix conversion. First missing keys: {sample}"
228
+ )
229
+
230
+ try:
231
+ load_model_dict_into_meta(transformer, state_dict, dtype=torch_dtype)
232
+ except Exception as e:
233
+ raise RuntimeError(f"Failed to load state dict into meta: {e}")
234
+
235
+ meta_parameters = [name for name, parameter in transformer.named_parameters() if parameter.is_meta]
236
+ if meta_parameters:
237
+ sample = ", ".join(meta_parameters[:20])
238
+ raise RuntimeError(
239
+ f"Phr00t Rapid-AIO transformer still has {len(meta_parameters)} meta parameters "
240
+ f"after loading. First meta parameters: {sample}"
241
+ )
242
+
243
+ transformer.eval()
244
+ return transformer
245
+
246
+ # Load main pipeline
247
+ try:
248
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
249
+ BASE_MODEL_ID,
250
+ transformer=load_phr00t_rapid_transformer(dtype),
251
+ torch_dtype=dtype
252
+ ).to(device)
253
+ print("Successfully loaded Qwen-Image-Edit-2511 with Rapid-AIO v23 transformer")
254
+ except Exception as e:
255
+ print(f"Failed to load main pipeline: {e}")
256
+ raise
257
+
258
+ # Apply optimizations
259
+ pipe.transformer.__class__ = QwenImageTransformer2DModel
260
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
261
+ print("Applied FA3 attention processor optimization")
262
+
263
+ # ============================================================================
264
+ # ENHANCED UPSCALER (Real-ESRGAN based)
265
+ # ============================================================================
266
+
267
+ def load_upscaler_model():
268
+ """Load Real-ESRGAN model for high-quality upscaling"""
269
+ global _upscaler_model
270
+ if _upscaler_model is not None:
271
+ return _upscaler_model
272
+
273
+ try:
274
+ import spandrel
275
+ import spandrel_extra_arches
276
+ spandrel_extra_arches.install()
277
+ except ImportError as exc:
278
+ raise gr.Error("Enhance mode requires spandrel and spandrel_extra_arches to be installed. "
279
+ "Install with: pip install spandrel spandrel_extra_arches") from exc
280
+
281
+ try:
282
+ model_path = download_model_with_retry(UPSCALER_MODEL_ID, UPSCALER_MODEL_FILENAME)
283
+ model = spandrel.ModelLoader().load_from_file(model_path)
284
+ model.eval().to(device)
285
+ _upscaler_model = model
286
+ print(f"Successfully loaded upscaler: {UPSCALER_MODEL_ID}/{UPSCALER_MODEL_FILENAME}")
287
+ return _upscaler_model
288
+ except Exception as e:
289
+ print(f"Failed to load upscaler model: {e}")
290
+ print("Falling back to Nomos upscaler...")
291
+ try:
292
+ model_path = download_model_with_retry("Phips/4xNomos8k_atd_jpg", "4xNomos8k_atd_jpg.safetensors")
293
+ model = spandrel.ModelLoader().load_from_file(model_path)
294
+ model.eval().to(device)
295
+ _upscaler_model = model
296
+ return _upscaler_model
297
+ except Exception as fallback_error:
298
+ raise gr.Error(f"Failed to load all upscaler models: {e} | {fallback_error}")
299
+
300
+ def image_to_tensor(image):
301
+ """Convert PIL Image to tensor"""
302
+ array = np.asarray(image.convert("RGB")).astype(np.float32) / 255.0
303
+ tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0)
304
+ return tensor.to(device)
305
+
306
+ def tensor_to_image(tensor):
307
+ """Convert tensor to PIL Image"""
308
+ array = tensor.squeeze(0).detach().float().cpu().clamp(0, 1).permute(1, 2, 0).numpy()
309
+ return Image.fromarray((array * 255.0).round().astype(np.uint8), mode="RGB")
310
+
311
+ def validate_enhance_input_size(image):
312
+ """Validate image size for enhancement"""
313
+ max_edge = max(image.size)
314
+ if max_edge > ENHANCE_MAX_INPUT_EDGE:
315
+ raise gr.Error(
316
+ f"Enhance mode accepts images up to {ENHANCE_MAX_INPUT_EDGE}px on the longest edge. "
317
+ f"Current image is {image.width}x{image.height}. "
318
+ f"Consider resizing your image first."
319
+ )
320
+
321
+ def advanced_tile_upscale(image, scale=4):
322
+ """
323
+ Advanced tiling upscaler with improved blending and edge handling
324
+ Uses Real-ESRGAN for superior quality compared to Nomos
325
+ """
326
+ validate_enhance_input_size(image)
327
+ model = load_upscaler_model()
328
+ tensor = image_to_tensor(image)
329
+ _, _, height, width = tensor.shape
330
+
331
+ base_tile_size = UPSCALER_TILE_SIZE
332
+ optimal_tile_size = min(base_tile_size, max(height, width) // 2)
333
+ tile_size = max(64, optimal_tile_size)
334
+ overlap = max(0, min(UPSCALER_TILE_OVERLAP, tile_size // 2))
335
+ step = max(1, tile_size - overlap)
336
+
337
+ y_positions = list(range(0, height, step))
338
+ if y_positions[-1] + tile_size < height:
339
+ y_positions.append(max(0, height - tile_size))
340
+
341
+ x_positions = list(range(0, width, step))
342
+ if x_positions[-1] + tile_size < width:
343
+ x_positions.append(max(0, width - tile_size))
344
+
345
+ y_positions = sorted(set(y_positions))
346
+ x_positions = sorted(set(x_positions))
347
+
348
+ output = None
349
+ weights = None
350
+
351
+ with torch.inference_mode():
352
+ for y in y_positions:
353
+ for x in x_positions:
354
+ y1 = min(y + tile_size, height)
355
+ x1 = min(x + tile_size, width)
356
+ tile = tensor[:, :, y:y1, x:x1]
357
+
358
+ upscaled_tile = model(tile).clamp(0, 1)
359
+
360
+ scale_y = upscaled_tile.shape[-2] // tile.shape[-2]
361
+ scale_x = upscaled_tile.shape[-1] // tile.shape[-1]
362
+
363
+ if output is None:
364
+ output = torch.zeros(
365
+ (1, 3, height * scale_y, width * scale_x),
366
+ dtype=upscaled_tile.dtype,
367
+ device=upscaled_tile.device,
368
+ )
369
+ weights = torch.zeros_like(output)
370
+
371
+ oy0, oy1 = y * scale_y, y1 * scale_y
372
+ ox0, ox1 = x * scale_x, x1 * scale_x
373
+ output[:, :, oy0:oy1, ox0:ox1] += upscaled_tile
374
+ weights[:, :, oy0:oy1, ox0:ox1] += 1
375
+
376
+ output = output / weights.clamp_min(1)
377
+ return tensor_to_image(output)
378
+
379
+ # ============================================================================
380
+ # ENHANCED DETAILER (Multi-stage processing)
381
+ # ============================================================================
382
+
383
+ def smart_sharpen(image, strength=1.15):
384
+ """
385
+ Smart sharpening with edge detection to avoid oversharpening smooth areas
386
+ """
387
+ if strength <= 0:
388
+ return image
389
+
390
+ img_array = np.array(image.convert("RGB"))
391
+
392
+ if strength > 1.0:
393
+ enhanced = ImageEnhance.Sharpness(image).enhance(strength)
394
+
395
+ gray = image.convert("L")
396
+ edges = gray.filter(ImageFilter.FIND_EDGES)
397
+ edge_mask = edges.filter(ImageFilter.GaussianBlur(radius=1))
398
+ edge_mask = edge_mask.point(lambda x: min(x * 0.3, 255))
399
+
400
+ sharpened_array = np.array(enhanced)
401
+ original_array = img_array
402
+ edge_array = np.array(edge_mask).astype(float) / 255.0
403
+
404
+ for c in range(3):
405
+ sharpened_array[:, :, c] = (
406
+ edge_array * sharpened_array[:, :, c] +
407
+ (1 - edge_array) * original_array[:, :, c]
408
+ )
409
+
410
+ image = Image.fromarray(np.clip(sharpened_array, 0, 255).astype(np.uint8))
411
+
412
+ return image
413
+
414
+ def add_ultra_detail(image, strength=0.8):
415
+ """
416
+ Add ultra-fine details using high-frequency enhancement
417
+ """
418
+ if strength <= 0:
419
+ return image
420
+
421
+ original = image.convert("RGB")
422
+ blurred = original.filter(ImageFilter.GaussianBlur(radius=2))
423
+
424
+ high_freq = ImageChops.subtract(original, blurred)
425
+ high_freq_enhanced = ImageEnhance.Contrast(high_freq).enhance(1.0 + strength)
426
+ result = ImageChops.add(original, high_freq_enhanced)
427
+
428
+ return result
429
+
430
+ def apply_high_frequency_details(image, amount=0.6):
431
+ """
432
+ Apply high-frequency detail enhancement for crisp textures
433
+ """
434
+ if amount <= 0:
435
+ return image
436
+
437
+ scales = [1, 2, 4]
438
+ result = image.convert("RGB")
439
+
440
+ for scale in scales:
441
+ blurred = result.filter(ImageFilter.GaussianBlur(radius=scale))
442
+ high_freq = ImageChops.subtract(result, blurred)
443
+ enhanced_hf = ImageEnhance.Contrast(high_freq).enhance(1.0 + amount * 0.3)
444
+ result = ImageChops.add(result, enhanced_hf)
445
+
446
+ return result
447
+
448
+ # ============================================================================
449
+ # ENHANCED CLEANER (Face Restoration + Artifact Removal)
450
+ # ============================================================================
451
+
452
+ def load_face_restoration_model():
453
+ """Load GFPGAN model for face restoration"""
454
+ global _face_restoration_model
455
+ if _face_restoration_model is not None:
456
+ return _face_restoration_model
457
+
458
+ try:
459
+ from gfpgan import GFPGANer
460
+ model_path = download_model_with_retry(FACE_RESTORATION_MODEL, FACE_RESTORATION_WEIGHTS)
461
+
462
+ restorer = GFPGANer(
463
+ model_path=model_path,
464
+ upscale=1,
465
+ arch='clean',
466
+ channel_multiplier=2,
467
+ bg_upsampler=None
468
+ )
469
+
470
+ _face_restoration_model = restorer
471
+ print("Successfully loaded GFPGAN face restoration model")
472
+ return _face_restoration_model
473
+
474
+ except ImportError:
475
+ print("GFPGAN not available, face restoration will use fallback methods")
476
+ return None
477
+ except Exception as e:
478
+ print(f"Failed to load face restoration model: {e}")
479
+ return None
480
+
481
+ def restore_faces(image):
482
+ """Restore faces in an image using GFPGAN"""
483
+ restorer = load_face_restoration_model()
484
+ if restorer is None:
485
+ print("Face restoration model not available, using skin repair fallback")
486
+ return repair_skin_texture(image)
487
+
488
+ try:
489
+ img_array = np.array(image.convert("RGB"))
490
+ restored_array, _ = restorer.enhance(img_array, has_aligned=False, only_center_face=False, paste_back=True)
491
+ restored_image = Image.fromarray(restored_array.astype(np.uint8))
492
+ return restored_image
493
+ except Exception as e:
494
+ print(f"Face restoration failed: {e}, using skin repair fallback")
495
+ return repair_skin_texture(image)
496
+
497
+ def remove_artifacts(image):
498
+ """Remove compression artifacts and noise"""
499
+ denoised = image.filter(ImageFilter.MedianFilter(size=3))
500
+ smoothed = denoised.filter(ImageFilter.GaussianBlur(radius=0.5))
501
+ result = Image.blend(image, smoothed, alpha=0.3)
502
+ return result
503
+
504
+ def enhanced_skin_repair(image):
505
+ """Enhanced skin repair with better color detection and blending"""
506
+ base = image.convert("RGB")
507
+
508
+ ycbcr = np.asarray(base.convert("YCbCr"))
509
+ y, cb, cr = ycbcr[:, :, 0], ycbcr[:, :, 1], ycbcr[:, :, 2]
510
+
511
+ skin_mask = (
512
+ (cr > 130) & (cr < 170) &
513
+ (cb > 70) & (cb < 140) &
514
+ (y > 80)
515
+ ).astype(np.uint8) * 255
516
+
517
+ try:
518
+ import cv2
519
+ kernel = np.ones((5, 5), np.uint8)
520
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
521
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
522
+ skin_mask = cv2.GaussianBlur(skin_mask, (7, 7), 0)
523
+ except ImportError:
524
+ from scipy import ndimage
525
+ skin_mask = ndimage.binary_opening(skin_mask > 128, structure=np.ones((3, 3))).astype(np.uint8) * 255
526
+ skin_mask = ndimage.gaussian_filter(skin_mask, sigma=3)
527
+
528
+ mask_image = Image.fromarray(skin_mask, mode="L")
529
+ repaired = base.filter(ImageFilter.MedianFilter(size=3))
530
+ repaired = repaired.filter(ImageFilter.GaussianBlur(radius=0.4))
531
+ non_skin = ImageOps.invert(mask_image)
532
+ sharpened = ImageEnhance.Sharpness(base).enhance(1.15)
533
+ blended = Image.composite(repaired, sharpened, mask_image)
534
+ result = ImageEnhance.Sharpness(blended).enhance(1.05)
535
+ return result
536
+
537
+ # Original skin repair functions (preserved for compatibility)
538
+ def skin_repair_mask(image):
539
+ ycbcr = np.asarray(image.convert("YCbCr"))
540
+ cb = ycbcr[:, :, 1]
541
+ cr = ycbcr[:, :, 2]
542
+ mask = (
543
+ (cr >= 135)
544
+ & (cr <= 180)
545
+ & (cb >= 75)
546
+ & (cb <= 135)
547
+ ).astype(np.uint8) * 255
548
+ mask_image = Image.fromarray(mask, mode="L")
549
+ return mask_image.filter(ImageFilter.GaussianBlur(radius=1.2))
550
+
551
+ def repair_skin_texture(image):
552
+ base = image.convert("RGB")
553
+ mask = skin_repair_mask(base)
554
+ repaired = base.filter(ImageFilter.MedianFilter(size=3)).filter(ImageFilter.GaussianBlur(radius=0.35))
555
+ blended = Image.composite(repaired, base, mask)
556
+ return ImageEnhance.Sharpness(blended).enhance(1.08)
557
+
558
+ def add_film_grain(image, seed):
559
+ base = image.convert("RGB")
560
+ array = np.asarray(base).astype(np.float32)
561
+ rng = np.random.default_rng(seed)
562
+ grain = rng.normal(0.0, 255.0 * ENHANCE_GRAIN_STRENGTH, size=(array.shape[0], array.shape[1], 1))
563
+ array = np.clip(array + grain, 0, 255)
564
+ return Image.fromarray(array.astype(np.uint8), mode="RGB")
565
+
566
+ # ============================================================================
567
+ # ENHANCED APPLY ENHANCEMENT (Main enhancement pipeline)
568
+ # ============================================================================
569
+
570
+ def apply_enhancement(image, enhance_mode, seed=0, progress=None):
571
+ """
572
+ Apply various enhancement modes to the image
573
+ """
574
+ mode = enhance_mode or ENHANCE_MODE_OFF
575
+ if mode not in ENHANCE_MODE_CHOICES:
576
+ raise gr.Error(f"Unknown enhance mode: {mode}")
577
+ if mode == ENHANCE_MODE_OFF:
578
+ return image
579
+
580
+ enhanced = image.convert("RGB")
581
+
582
+ total_steps = 0
583
+ if mode == ENHANCE_MODE_UPSCALE:
584
+ total_steps = 1
585
+ elif mode == ENHANCE_MODE_CLEAN:
586
+ total_steps = 3
587
+ elif mode == ENHANCE_MODE_MAX_DETAIL:
588
+ total_steps = 4
589
+ elif mode == ENHANCE_MODE_FACE_ENHANCE:
590
+ total_steps = 2
591
+ elif mode == ENHANCE_MODE_FULL_ENHANCE:
592
+ total_steps = 5
593
+
594
+ step = 0
595
+
596
+ if mode == ENHANCE_MODE_FACE_ENHANCE:
597
+ if progress:
598
+ step += 1
599
+ progress(0.5 * step / total_steps, desc="Restoring faces...")
600
+ enhanced = restore_faces(enhanced)
601
+ if progress:
602
+ step += 1
603
+ progress(0.5 * step / total_steps, desc="Upscaling...")
604
+ enhanced = advanced_tile_upscale(enhanced)
605
+ return enhanced
606
+
607
+ if mode in (ENHANCE_MODE_CLEAN, ENHANCE_MODE_FULL_ENHANCE):
608
+ if progress:
609
+ step += 1
610
+ progress(0.7 * step / total_steps, desc="Removing artifacts...")
611
+ enhanced = remove_artifacts(enhanced)
612
+ if progress:
613
+ step += 1
614
+ progress(0.7 * step / total_steps, desc="Repairing skin and faces...")
615
+ enhanced = enhanced_skin_repair(enhanced)
616
+ enhanced = restore_faces(enhanced)
617
+
618
+ if mode in (ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE):
619
+ if progress:
620
+ step += 1
621
+ progress(0.8 * step / total_steps, desc="Upscaling image...")
622
+ enhanced = advanced_tile_upscale(enhanced)
623
+
624
+ if mode in (ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE):
625
+ if progress:
626
+ step += 1
627
+ progress(0.9 * step / total_steps, desc="Enhancing details...")
628
+ enhanced = add_ultra_detail(enhanced, strength=0.7)
629
+ enhanced = apply_high_frequency_details(enhanced, amount=0.5)
630
+ enhanced = smart_sharpen(enhanced, strength=SMART_SHARPENING_STRENGTH)
631
+ if progress:
632
+ step += 1
633
+ progress(0.95 * step / total_steps, desc="Adding final grain...")
634
+ enhanced = add_film_grain(enhanced, seed)
635
+
636
+ return enhanced
637
+
638
+ # ============================================================================
639
+ # UTILITY FUNCTIONS
640
+ # ============================================================================
641
+
642
+ def use_output_as_input(output_images):
643
+ """Move the first output image into the Image 1 slot."""
644
+ if not output_images:
645
+ return gr.update()
646
+ first = output_images[0]
647
+ path = first[0] if isinstance(first, (list, tuple)) else first
648
+ return gr.update(value=path)
649
+
650
+ def check_gpu_memory():
651
+ """Check available GPU memory"""
652
+ if device == "cuda":
653
+ try:
654
+ total = torch.cuda.get_device_properties(0).total_memory
655
+ reserved = torch.cuda.memory_reserved(0)
656
+ allocated = torch.cuda.memory_allocated(0)
657
+ free = total - reserved
658
+ print(f"GPU Memory: Total={total/1024**3:.2f}GB, Reserved={reserved/1024**3:.2f}GB, Allocated={allocated/1024**3:.2f}GB, Free={free/1024**3:.2f}GB")
659
+ return free > 1024**3
660
+ except Exception as e:
661
+ print(f"Failed to check GPU memory: {e}")
662
+ return True
663
+ return True
664
+
665
+ def clear_gpu_cache():
666
+ """Clear GPU cache to free up memory"""
667
+ if device == "cuda":
668
+ try:
669
+ torch.cuda.empty_cache()
670
+ import gc
671
+ gc.collect()
672
+ print("GPU cache cleared")
673
+ except Exception as e:
674
+ print(f"Failed to clear GPU cache: {e}")
675
+
676
+ # ============================================================================
677
+ # MAIN INFERENCE FUNCTION (Enhanced)
678
+ # ============================================================================
679
+
680
+ MAX_SEED = np.iinfo(np.int32).max
681
+
682
+ @spaces.GPU(duration=60)
683
+ def infer(
684
+ image_1,
685
+ image_2,
686
+ prompt,
687
+ seed=42,
688
+ randomize_seed=False,
689
+ true_guidance_scale=1.0,
690
+ num_inference_steps=4,
691
+ height=None,
692
+ width=None,
693
+ enhance_mode=ENHANCE_MODE_OFF,
694
+ num_images_per_prompt=1,
695
+ progress=gr.Progress(track_tqdm=True),
696
+ ):
697
+ """
698
+ Enhanced image generation with advanced editing and enhancement options
699
+ """
700
+ negative_prompt = " "
701
+
702
+ if randomize_seed:
703
+ seed = random.randint(0, MAX_SEED)
704
+
705
+ generator = torch.Generator(device=device).manual_seed(seed)
706
+
707
+ pil_images = []
708
+ for img in (image_1, image_2):
709
+ if img is None:
710
+ continue
711
+ try:
712
+ if isinstance(img, str):
713
+ pil_images.append(Image.open(img).convert("RGB"))
714
+ elif isinstance(img, Image.Image):
715
+ pil_images.append(img.convert("RGB"))
716
+ elif hasattr(img, "name"):
717
+ pil_images.append(Image.open(img.name).convert("RGB"))
718
+ except Exception:
719
+ continue
720
+
721
+ if height==256 and width==256:
722
+ height, width = None, None
723
+
724
+ print(f"Generation Parameters:")
725
+ print(f" Prompt: '{prompt}'")
726
+ print(f" Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}")
727
+ print(f" Size: {width}x{height}, Enhance Mode: {enhance_mode}")
728
+
729
+ if not check_gpu_memory():
730
+ clear_gpu_cache()
731
+ if not check_gpu_memory():
732
+ raise gr.Error("Insufficient GPU memory. Please reduce image size or close other applications.")
733
+
734
+ try:
735
+ images_pil = pipe(
736
+ image=pil_images if len(pil_images) > 0 else None,
737
+ prompt=prompt,
738
+ height=height,
739
+ width=width,
740
+ negative_prompt=negative_prompt,
741
+ num_inference_steps=num_inference_steps,
742
+ generator=generator,
743
+ true_cfg_scale=true_guidance_scale,
744
+ num_images_per_prompt=num_images_per_prompt,
745
+ ).images
746
+ except Exception as e:
747
+ clear_gpu_cache()
748
+ raise gr.Error(f"Image generation failed: {e}")
749
+
750
+ if enhance_mode != ENHANCE_MODE_OFF:
751
+ images_pil = [
752
+ apply_enhancement(img, enhance_mode, seed=seed + idx, progress=progress)
753
+ for idx, img in enumerate(images_pil)
754
+ ]
755
+
756
+ output_paths = []
757
+ os.makedirs("outputs", exist_ok=True)
758
+ for idx, img in enumerate(images_pil):
759
+ output_path = f"outputs/output_{seed}_{idx}_{int(time.time()*1000)}.png"
760
+ img.save(output_path)
761
+ output_paths.append(output_path)
762
+
763
+ clear_gpu_cache()
764
+
765
+ return output_paths, seed, gr.update(visible=True), gr.update(visible=bool(VIDEO_SPACE_ID))
766
+
767
+
768
+ # ============================================================================
769
+ # UI LAYOUT (Enhanced)
770
+ # ============================================================================
771
+
772
+ css = """
773
+ #col-container {
774
+ margin: 0 auto;
775
+ max-width: 1024px;
776
+ }
777
+ #logo-title {
778
+ text-align: center;
779
+ }
780
+ #logo-title h1 {
781
+ margin-bottom: 0;
782
+ }
783
+ #logo-title h2 {
784
+ color: #5b47d1;
785
+ font-style: italic;
786
+ margin-top: 0;
787
+ }
788
+ #edit_text{margin-top: -62px !important}
789
+ .enhance-info {
790
+ font-size: 0.9em;
791
+ color: #666;
792
+ margin-top: 5px;
793
+ }
794
+ """
795
+
796
+ with gr.Blocks(css=css) as demo:
797
+ with gr.Column(elem_id="col-container"):
798
+ gr.HTML(f"""
799
+ <div id="logo-title">
800
+ <h1>Pro Realism Edit Studio - Enhanced</h1>
801
+ <h2>Rapid Edit with Real-ESRGAN & Face Restoration</h2>
802
+ </div>
803
+ """)
804
+
805
+ gr.Markdown("""
806
+ Powered by:
807
+ - [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511)
808
+ - [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer
809
+ - [Real-ESRGAN](https://huggingface.co/ai-forever/Real-ESRGAN) for high-quality upscaling
810
+ - [GFPGAN](https://github.com/TencentARC/GFPGAN) for face restoration
811
+
812
+ Upload an image and enter your prompt to edit it.
813
+
814
+ Pro Tips:
815
+ - Use Face Enhance mode for portrait photography
816
+ - Use Max Detail for product shots and textures
817
+ - Use Full Enhance for comprehensive improvement
818
+ """)
819
+
820
+ with gr.Row():
821
+ with gr.Column():
822
+ with gr.Row():
823
+ image_1 = gr.Image(label="Image 1", type="filepath", interactive=True)
824
+ image_2 = gr.Image(label="Image 2 (optional)", type="filepath", interactive=True)
825
+
826
+ prompt = gr.Text(
827
+ label="Prompt",
828
+ show_label=True,
829
+ placeholder="Enter your prompt here...",
830
+ )
831
+
832
+ enhance_mode = gr.Radio(
833
+ label="Enhance Mode",
834
+ choices=ENHANCE_MODE_CHOICES,
835
+ value=ENHANCE_MODE_OFF,
836
+ interactive=True,
837
+ info="Choose enhancement level for your output"
838
+ )
839
+
840
+ enhance_info = gr.Markdown("""
841
+ Enhancement Options:
842
+ - Off: No post-processing
843
+ - Upscale Only: 4x upscaling with Real-ESRGAN
844
+ - Clean & Restore: Artifact removal + skin/face restoration
845
+ - Max Detail: Full detail enhancement with sharpening
846
+ - Face Enhance: Specialized face restoration + upscaling
847
+ - Full Enhance: Complete pipeline (clean + detail + face + upscale)
848
+ """, visible=False)
849
+
850
+ run_button = gr.Button("Generate!", variant="primary")
851
+
852
+ with gr.Accordion("Advanced Settings", open=False):
853
+ seed = gr.Slider(
854
+ label="Seed",
855
+ minimum=0,
856
+ maximum=MAX_SEED,
857
+ step=1,
858
+ value=0,
859
+ )
860
+
861
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
862
+
863
+ with gr.Row():
864
+ true_guidance_scale = gr.Slider(
865
+ label="True guidance scale",
866
+ minimum=1.0,
867
+ maximum=10.0,
868
+ step=0.1,
869
+ value=1.0
870
+ )
871
+
872
+ num_inference_steps = gr.Slider(
873
+ label="Number of inference steps",
874
+ minimum=1,
875
+ maximum=40,
876
+ step=1,
877
+ value=4,
878
+ )
879
+
880
+ with gr.Row():
881
+ height = gr.Slider(
882
+ label="Height",
883
+ minimum=256,
884
+ maximum=2048,
885
+ step=8,
886
+ value=None,
887
+ )
888
+
889
+ width = gr.Slider(
890
+ label="Width",
891
+ minimum=256,
892
+ maximum=2048,
893
+ step=8,
894
+ value=None,
895
+ )
896
+
897
+ gr.Markdown("""
898
+ Performance Tips:
899
+ - Use 4 steps for fastest results
900
+ - Increase steps (8-20) for better quality
901
+ - Lower guidance scale for more creative freedom
902
+ """)
903
+
904
+ with gr.Column():
905
+ result = gr.Gallery(label="Result", show_label=False, type="filepath")
906
+ with gr.Row():
907
+ use_output_btn = gr.Button("Use as input", variant="secondary", size="sm", visible=False)
908
+ turn_video_btn = gr.Button("Turn into Video", variant="secondary", size="sm", visible=False)
909
+ output_video = gr.Video(label="Generated Video", autoplay=True, visible=False)
910
+
911
+ with gr.Row():
912
+ gr.Markdown("### History")
913
+ clear_history_button = gr.Button("Clear History", size="sm", variant="stop")
914
+
915
+ history_gallery = gr.Gallery(
916
+ label="Click any image to use as input",
917
+ interactive=False,
918
+ show_label=True,
919
+ visible=True
920
+ )
921
+
922
+ gr.on(
923
+ triggers=[run_button.click, prompt.submit],
924
+ fn=infer,
925
+ inputs=[
926
+ image_1,
927
+ image_2,
928
+ prompt,
929
+ seed,
930
+ randomize_seed,
931
+ true_guidance_scale,
932
+ num_inference_steps,
933
+ height,
934
+ width,
935
+ enhance_mode,
936
+ ],
937
+ outputs=[result, seed, use_output_btn, turn_video_btn],
938
+
939
+ ).then(
940
+ fn=update_history,
941
+ inputs=[result, history_gallery],
942
+ outputs=history_gallery,
943
+ )
944
+
945
+ enhance_mode.change(
946
+ fn=lambda mode: gr.update(visible=mode != ENHANCE_MODE_OFF),
947
+ inputs=[enhance_mode],
948
+ outputs=[enhance_info]
949
+ )
950
+
951
+ use_output_btn.click(
952
+ fn=use_output_as_input,
953
+ inputs=[result],
954
+ outputs=[image_1]
955
+ )
956
+
957
+ history_gallery.select(
958
+ fn=use_history_as_input,
959
+ inputs=None,
960
+ outputs=[image_1],
961
+ )
962
+
963
+ clear_history_button.click(
964
+ fn=lambda: [],
965
+ inputs=None,
966
+ outputs=history_gallery,
967
+ )
968
+
969
+ turn_video_btn.click(
970
+ fn=lambda: gr.update(visible=True),
971
+ inputs=None,
972
+ outputs=[output_video],
973
+ ).then(
974
+ fn=turn_into_video,
975
+ inputs=[image_1, result, prompt],
976
+ outputs=[output_video],
977
+ )
978
+
979
+
980
+ if __name__ == "__main__":
981
+ print(f"Device: {device}")
982
+ if device == "cuda":
983
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
984
+ check_gpu_memory()
985
+ print(f"Starting Pro Realism Edit Studio v{APP_VERSION}")
986
+ demo.launch()
optimization.py CHANGED
@@ -1,85 +1,85 @@
1
- """
2
- AOT compilation optimization for Qwen-Image-Edit pipeline.
3
- """
4
-
5
- import gc
6
- from typing import Any
7
- from typing import Callable
8
- from typing import ParamSpec
9
- from torchao.quantization import quantize_
10
- from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
11
- import spaces
12
- import torch
13
- from torch.utils._pytree import tree_map
14
-
15
-
16
- P = ParamSpec('P')
17
-
18
-
19
- TRANSFORMER_IMAGE_SEQ_LENGTH_DIM = torch.export.Dim('image_seq_length')
20
- TRANSFORMER_TEXT_SEQ_LENGTH_DIM = torch.export.Dim('text_seq_length')
21
-
22
- TRANSFORMER_DYNAMIC_SHAPES = {
23
- 'hidden_states': {
24
- 1: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM,
25
- },
26
- 'encoder_hidden_states': {
27
- 1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
28
- },
29
- 'encoder_hidden_states_mask': {
30
- 1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
31
- },
32
- 'image_rotary_emb': ({
33
- 0: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM,
34
- }, {
35
- 0: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
36
- }),
37
- }
38
-
39
-
40
- INDUCTOR_CONFIGS = {
41
- 'conv_1x1_as_mm': True,
42
- 'epilogue_fusion': False,
43
- 'coordinate_descent_tuning': True,
44
- 'coordinate_descent_check_all_directions': True,
45
- 'max_autotune': True,
46
- 'triton.cudagraphs': True,
47
- }
48
-
49
-
50
- def optimize_pipeline_(pipeline: Callable[P, Any], *args: P.args, **kwargs: P.kwargs):
51
-
52
- @spaces.GPU(duration=1000)
53
- def compile_transformer():
54
-
55
- # Run warmup pass to capture transformer inputs (needs all components on GPU)
56
- with spaces.aoti_capture(pipeline.transformer) as call:
57
- pipeline(*args, **kwargs)
58
-
59
- # Offload text encoder to CPU to free ~16GB during memory-intensive torch.export
60
- # This happens AFTER warmup but BEFORE export to avoid device mismatch
61
- text_encoder_device = next(pipeline.text_encoder.parameters()).device
62
- pipeline.text_encoder.to('cpu')
63
- gc.collect()
64
- torch.cuda.empty_cache()
65
-
66
- dynamic_shapes = tree_map(lambda t: None, call.kwargs)
67
- dynamic_shapes |= TRANSFORMER_DYNAMIC_SHAPES
68
-
69
- # quantize_(pipeline.transformer, Float8DynamicActivationFloat8WeightConfig())
70
-
71
- exported = torch.export.export(
72
- mod=pipeline.transformer,
73
- args=call.args,
74
- kwargs=call.kwargs,
75
- dynamic_shapes=dynamic_shapes,
76
- )
77
-
78
- compiled = spaces.aoti_compile(exported, INDUCTOR_CONFIGS)
79
-
80
- # Move text encoder back to original device
81
- pipeline.text_encoder.to(text_encoder_device)
82
-
83
- return compiled
84
-
85
  spaces.aoti_apply(compile_transformer(), pipeline.transformer)
 
1
+ """
2
+ AOT compilation optimization for Qwen-Image-Edit pipeline.
3
+ """
4
+
5
+ import gc
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import ParamSpec
9
+ from torchao.quantization import quantize_
10
+ from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
11
+ import spaces
12
+ import torch
13
+ from torch.utils._pytree import tree_map
14
+
15
+
16
+ P = ParamSpec('P')
17
+
18
+
19
+ TRANSFORMER_IMAGE_SEQ_LENGTH_DIM = torch.export.Dim('image_seq_length')
20
+ TRANSFORMER_TEXT_SEQ_LENGTH_DIM = torch.export.Dim('text_seq_length')
21
+
22
+ TRANSFORMER_DYNAMIC_SHAPES = {
23
+ 'hidden_states': {
24
+ 1: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM,
25
+ },
26
+ 'encoder_hidden_states': {
27
+ 1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
28
+ },
29
+ 'encoder_hidden_states_mask': {
30
+ 1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
31
+ },
32
+ 'image_rotary_emb': ({
33
+ 0: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM,
34
+ }, {
35
+ 0: TRANSFORMER_TEXT_SEQ_LENGTH_DIM,
36
+ }),
37
+ }
38
+
39
+
40
+ INDUCTOR_CONFIGS = {
41
+ 'conv_1x1_as_mm': True,
42
+ 'epilogue_fusion': False,
43
+ 'coordinate_descent_tuning': True,
44
+ 'coordinate_descent_check_all_directions': True,
45
+ 'max_autotune': True,
46
+ 'triton.cudagraphs': True,
47
+ }
48
+
49
+
50
+ def optimize_pipeline_(pipeline: Callable[P, Any], *args: P.args, **kwargs: P.kwargs):
51
+
52
+ @spaces.GPU(duration=1000)
53
+ def compile_transformer():
54
+
55
+ # Run warmup pass to capture transformer inputs (needs all components on GPU)
56
+ with spaces.aoti_capture(pipeline.transformer) as call:
57
+ pipeline(*args, **kwargs)
58
+
59
+ # Offload text encoder to CPU to free ~16GB during memory-intensive torch.export
60
+ # This happens AFTER warmup but BEFORE export to avoid device mismatch
61
+ text_encoder_device = next(pipeline.text_encoder.parameters()).device
62
+ pipeline.text_encoder.to('cpu')
63
+ gc.collect()
64
+ torch.cuda.empty_cache()
65
+
66
+ dynamic_shapes = tree_map(lambda t: None, call.kwargs)
67
+ dynamic_shapes |= TRANSFORMER_DYNAMIC_SHAPES
68
+
69
+ # quantize_(pipeline.transformer, Float8DynamicActivationFloat8WeightConfig())
70
+
71
+ exported = torch.export.export(
72
+ mod=pipeline.transformer,
73
+ args=call.args,
74
+ kwargs=call.kwargs,
75
+ dynamic_shapes=dynamic_shapes,
76
+ )
77
+
78
+ compiled = spaces.aoti_compile(exported, INDUCTOR_CONFIGS)
79
+
80
+ # Move text encoder back to original device
81
+ pipeline.text_encoder.to(text_encoder_device)
82
+
83
+ return compiled
84
+
85
  spaces.aoti_apply(compile_transformer(), pipeline.transformer)
qwenimage/pipeline_qwenimage_edit_plus.py CHANGED
@@ -1,889 +1,889 @@
1
- # Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- import inspect
16
- import math
17
- from typing import Any, Callable, Dict, List, Optional, Union
18
-
19
- import numpy as np
20
- import torch
21
- from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
22
-
23
- from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
24
- from diffusers.loaders import QwenImageLoraLoaderMixin
25
- from diffusers.models import AutoencoderKLQwenImage, QwenImageTransformer2DModel
26
- from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
27
- from diffusers.utils import is_torch_xla_available, logging, replace_example_docstring
28
- from diffusers.utils.torch_utils import randn_tensor
29
- from diffusers.pipelines.pipeline_utils import DiffusionPipeline
30
- from diffusers.pipelines.qwenimage.pipeline_output import QwenImagePipelineOutput
31
-
32
-
33
- if is_torch_xla_available():
34
- import torch_xla.core.xla_model as xm
35
-
36
- XLA_AVAILABLE = True
37
- else:
38
- XLA_AVAILABLE = False
39
-
40
-
41
- logger = logging.get_logger(__name__) # pylint: disable=invalid-name
42
-
43
- EXAMPLE_DOC_STRING = """
44
- Examples:
45
- ```py
46
- >>> import torch
47
- >>> from PIL import Image
48
- >>> from diffusers import QwenImageEditPlusPipeline
49
- >>> from diffusers.utils import load_image
50
-
51
- >>> pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2511", torch_dtype=torch.bfloat16)
52
- >>> pipe.to("cuda")
53
- >>> image = load_image("input.png").convert("RGB")
54
- >>> prompt = (
55
- ... "Make Pikachu hold a sign that says 'Qwen Edit is awesome', yarn art style, detailed, vibrant colors"
56
- ... )
57
- >>> # Depending on the variant being used, the pipeline call will slightly vary.
58
- >>> # Refer to the pipeline documentation for more details.
59
- >>> image = pipe(image, prompt, num_inference_steps=50).images[0]
60
- >>> image.save("qwenimage_edit_plus.png")
61
- ```
62
- """
63
-
64
- CONDITION_IMAGE_SIZE = 384 * 384
65
- VAE_IMAGE_SIZE = 1024 * 1024
66
-
67
-
68
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.calculate_shift
69
- def calculate_shift(
70
- image_seq_len,
71
- base_seq_len: int = 256,
72
- max_seq_len: int = 4096,
73
- base_shift: float = 0.5,
74
- max_shift: float = 1.15,
75
- ):
76
- m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
77
- b = base_shift - m * base_seq_len
78
- mu = image_seq_len * m + b
79
- return mu
80
-
81
-
82
- # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
83
- def retrieve_timesteps(
84
- scheduler,
85
- num_inference_steps: Optional[int] = None,
86
- device: Optional[Union[str, torch.device]] = None,
87
- timesteps: Optional[List[int]] = None,
88
- sigmas: Optional[List[float]] = None,
89
- **kwargs,
90
- ):
91
- r"""
92
- Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
93
- custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
94
-
95
- Args:
96
- scheduler (`SchedulerMixin`):
97
- The scheduler to get timesteps from.
98
- num_inference_steps (`int`):
99
- The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
100
- must be `None`.
101
- device (`str` or `torch.device`, *optional*):
102
- The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
103
- timesteps (`List[int]`, *optional*):
104
- Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
105
- `num_inference_steps` and `sigmas` must be `None`.
106
- sigmas (`List[float]`, *optional*):
107
- Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
108
- `num_inference_steps` and `timesteps` must be `None`.
109
-
110
- Returns:
111
- `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
112
- second element is the number of inference steps.
113
- """
114
- if timesteps is not None and sigmas is not None:
115
- raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
116
- if timesteps is not None:
117
- accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
118
- if not accepts_timesteps:
119
- raise ValueError(
120
- f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
121
- f" timestep schedules. Please check whether you are using the correct scheduler."
122
- )
123
- scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
124
- timesteps = scheduler.timesteps
125
- num_inference_steps = len(timesteps)
126
- elif sigmas is not None:
127
- accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
128
- if not accept_sigmas:
129
- raise ValueError(
130
- f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
131
- f" sigmas schedules. Please check whether you are using the correct scheduler."
132
- )
133
- scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
134
- timesteps = scheduler.timesteps
135
- num_inference_steps = len(timesteps)
136
- else:
137
- scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
138
- timesteps = scheduler.timesteps
139
- return timesteps, num_inference_steps
140
-
141
-
142
- # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
143
- def retrieve_latents(
144
- encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample"
145
- ):
146
- if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
147
- return encoder_output.latent_dist.sample(generator)
148
- elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
149
- return encoder_output.latent_dist.mode()
150
- elif hasattr(encoder_output, "latents"):
151
- return encoder_output.latents
152
- else:
153
- raise AttributeError("Could not access latents of provided encoder_output")
154
-
155
-
156
- def calculate_dimensions(target_area, ratio):
157
- width = math.sqrt(target_area * ratio)
158
- height = width / ratio
159
-
160
- width = round(width / 32) * 32
161
- height = round(height / 32) * 32
162
-
163
- return width, height
164
-
165
-
166
- class QwenImageEditPlusPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
167
- r"""
168
- The Qwen-Image-Edit pipeline for image editing.
169
-
170
- Args:
171
- transformer ([`QwenImageTransformer2DModel`]):
172
- Conditional Transformer (MMDiT) architecture to denoise the encoded image latents.
173
- scheduler ([`FlowMatchEulerDiscreteScheduler`]):
174
- A scheduler to be used in combination with `transformer` to denoise the encoded image latents.
175
- vae ([`AutoencoderKL`]):
176
- Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations.
177
- text_encoder ([`Qwen2.5-VL-7B-Instruct`]):
178
- [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), specifically the
179
- [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) variant.
180
- tokenizer (`QwenTokenizer`):
181
- Tokenizer of class
182
- [CLIPTokenizer](https://huggingface.co/docs/transformers/en/model_doc/clip#transformers.CLIPTokenizer).
183
- """
184
-
185
- model_cpu_offload_seq = "text_encoder->transformer->vae"
186
- _callback_tensor_inputs = ["latents", "prompt_embeds"]
187
-
188
- def __init__(
189
- self,
190
- scheduler: FlowMatchEulerDiscreteScheduler,
191
- vae: AutoencoderKLQwenImage,
192
- text_encoder: Qwen2_5_VLForConditionalGeneration,
193
- tokenizer: Qwen2Tokenizer,
194
- processor: Qwen2VLProcessor,
195
- transformer: QwenImageTransformer2DModel,
196
- ):
197
- super().__init__()
198
-
199
- self.register_modules(
200
- vae=vae,
201
- text_encoder=text_encoder,
202
- tokenizer=tokenizer,
203
- processor=processor,
204
- transformer=transformer,
205
- scheduler=scheduler,
206
- )
207
- self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
208
- self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16
209
- # QwenImage latents are turned into 2x2 patches and packed. This means the latent width and height has to be divisible
210
- # by the patch size. So the vae scale factor is multiplied by the patch size to account for this
211
- self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2)
212
- self.tokenizer_max_length = 1024
213
-
214
- self.prompt_template_encode = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
215
- self.prompt_template_encode_start_idx = 64
216
- self.default_sample_size = 128
217
-
218
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._extract_masked_hidden
219
- def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
220
- bool_mask = mask.bool()
221
- valid_lengths = bool_mask.sum(dim=1)
222
- selected = hidden_states[bool_mask]
223
- split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
224
-
225
- return split_result
226
-
227
- def _get_qwen_prompt_embeds(
228
- self,
229
- prompt: Union[str, List[str]] = None,
230
- image: Optional[torch.Tensor] = None,
231
- device: Optional[torch.device] = None,
232
- dtype: Optional[torch.dtype] = None,
233
- ):
234
- device = device or self._execution_device
235
- dtype = dtype or self.text_encoder.dtype
236
-
237
- prompt = [prompt] if isinstance(prompt, str) else prompt
238
- img_prompt_template = "Picture {}: <|vision_start|><|image_pad|><|vision_end|>"
239
- if isinstance(image, list):
240
- base_img_prompt = ""
241
- for i, img in enumerate(image):
242
- base_img_prompt += img_prompt_template.format(i + 1)
243
- elif image is not None:
244
- base_img_prompt = img_prompt_template.format(1)
245
- else:
246
- base_img_prompt = ""
247
-
248
- template = self.prompt_template_encode
249
-
250
- drop_idx = self.prompt_template_encode_start_idx
251
- txt = [template.format(base_img_prompt + e) for e in prompt]
252
-
253
- model_inputs = self.processor(
254
- text=txt,
255
- images=image,
256
- padding=True,
257
- return_tensors="pt",
258
- ).to(device)
259
-
260
- outputs = self.text_encoder(
261
- input_ids=model_inputs.input_ids,
262
- attention_mask=model_inputs.attention_mask,
263
- pixel_values=model_inputs.pixel_values,
264
- image_grid_thw=model_inputs.image_grid_thw,
265
- output_hidden_states=True,
266
- )
267
-
268
- hidden_states = outputs.hidden_states[-1]
269
- split_hidden_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask)
270
- split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
271
- attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
272
- max_seq_len = max([e.size(0) for e in split_hidden_states])
273
- prompt_embeds = torch.stack(
274
- [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
275
- )
276
- encoder_attention_mask = torch.stack(
277
- [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
278
- )
279
-
280
- prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
281
-
282
- return prompt_embeds, encoder_attention_mask
283
-
284
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.encode_prompt
285
- def encode_prompt(
286
- self,
287
- prompt: Union[str, List[str]],
288
- image: Optional[torch.Tensor] = None,
289
- device: Optional[torch.device] = None,
290
- num_images_per_prompt: int = 1,
291
- prompt_embeds: Optional[torch.Tensor] = None,
292
- prompt_embeds_mask: Optional[torch.Tensor] = None,
293
- max_sequence_length: int = 1024,
294
- ):
295
- r"""
296
-
297
- Args:
298
- prompt (`str` or `List[str]`, *optional*):
299
- prompt to be encoded
300
- image (`torch.Tensor`, *optional*):
301
- image to be encoded
302
- device: (`torch.device`):
303
- torch device
304
- num_images_per_prompt (`int`):
305
- number of images that should be generated per prompt
306
- prompt_embeds (`torch.Tensor`, *optional*):
307
- Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
308
- provided, text embeddings will be generated from `prompt` input argument.
309
- """
310
- device = device or self._execution_device
311
-
312
- prompt = [prompt] if isinstance(prompt, str) else prompt
313
- batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0]
314
-
315
- if prompt_embeds is None:
316
- prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, image, device)
317
-
318
- _, seq_len, _ = prompt_embeds.shape
319
- prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
320
- prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
321
- prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
322
- prompt_embeds_mask = prompt_embeds_mask.view(batch_size * num_images_per_prompt, seq_len)
323
-
324
- return prompt_embeds, prompt_embeds_mask
325
-
326
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.check_inputs
327
- def check_inputs(
328
- self,
329
- prompt,
330
- height,
331
- width,
332
- negative_prompt=None,
333
- prompt_embeds=None,
334
- negative_prompt_embeds=None,
335
- prompt_embeds_mask=None,
336
- negative_prompt_embeds_mask=None,
337
- callback_on_step_end_tensor_inputs=None,
338
- max_sequence_length=None,
339
- ):
340
- if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
341
- logger.warning(
342
- f"`height` and `width` have to be divisible by {self.vae_scale_factor * 2} but are {height} and {width}. Dimensions will be resized accordingly"
343
- )
344
-
345
- if callback_on_step_end_tensor_inputs is not None and not all(
346
- k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs
347
- ):
348
- raise ValueError(
349
- f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
350
- )
351
-
352
- if prompt is not None and prompt_embeds is not None:
353
- raise ValueError(
354
- f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
355
- " only forward one of the two."
356
- )
357
- elif prompt is None and prompt_embeds is None:
358
- raise ValueError(
359
- "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
360
- )
361
- elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
362
- raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
363
-
364
- if negative_prompt is not None and negative_prompt_embeds is not None:
365
- raise ValueError(
366
- f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
367
- f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
368
- )
369
-
370
- if prompt_embeds is not None and prompt_embeds_mask is None:
371
- raise ValueError(
372
- "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`."
373
- )
374
- if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
375
- raise ValueError(
376
- "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`."
377
- )
378
-
379
- if max_sequence_length is not None and max_sequence_length > 1024:
380
- raise ValueError(f"`max_sequence_length` cannot be greater than 1024 but is {max_sequence_length}")
381
-
382
- @staticmethod
383
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._pack_latents
384
- def _pack_latents(latents, batch_size, num_channels_latents, height, width):
385
- latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
386
- latents = latents.permute(0, 2, 4, 1, 3, 5)
387
- latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
388
-
389
- return latents
390
-
391
- @staticmethod
392
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._unpack_latents
393
- def _unpack_latents(latents, height, width, vae_scale_factor):
394
- batch_size, num_patches, channels = latents.shape
395
-
396
- # VAE applies 8x compression on images but we must also account for packing which requires
397
- # latent height and width to be divisible by 2.
398
- height = 2 * (int(height) // (vae_scale_factor * 2))
399
- width = 2 * (int(width) // (vae_scale_factor * 2))
400
-
401
- latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
402
- latents = latents.permute(0, 3, 1, 4, 2, 5)
403
-
404
- latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width)
405
-
406
- return latents
407
-
408
- # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline._encode_vae_image
409
- def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
410
- if isinstance(generator, list):
411
- image_latents = [
412
- retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i], sample_mode="argmax")
413
- for i in range(image.shape[0])
414
- ]
415
- image_latents = torch.cat(image_latents, dim=0)
416
- else:
417
- image_latents = retrieve_latents(self.vae.encode(image), generator=generator, sample_mode="argmax")
418
- latents_mean = (
419
- torch.tensor(self.vae.config.latents_mean)
420
- .view(1, self.latent_channels, 1, 1, 1)
421
- .to(image_latents.device, image_latents.dtype)
422
- )
423
- latents_std = (
424
- torch.tensor(self.vae.config.latents_std)
425
- .view(1, self.latent_channels, 1, 1, 1)
426
- .to(image_latents.device, image_latents.dtype)
427
- )
428
- image_latents = (image_latents - latents_mean) / latents_std
429
-
430
- return image_latents
431
-
432
- def prepare_latents(
433
- self,
434
- images,
435
- batch_size,
436
- num_channels_latents,
437
- height,
438
- width,
439
- dtype,
440
- device,
441
- generator,
442
- latents=None,
443
- ):
444
- # VAE applies 8x compression on images but we must also account for packing which requires
445
- # latent height and width to be divisible by 2.
446
- height = 2 * (int(height) // (self.vae_scale_factor * 2))
447
- width = 2 * (int(width) // (self.vae_scale_factor * 2))
448
-
449
- shape = (batch_size, 1, num_channels_latents, height, width)
450
-
451
- image_latents = None
452
- if images is not None:
453
- if not isinstance(images, list):
454
- images = [images]
455
- all_image_latents = []
456
- for image in images:
457
- image = image.to(device=device, dtype=dtype)
458
- if image.shape[1] != self.latent_channels:
459
- image_latents = self._encode_vae_image(image=image, generator=generator)
460
- else:
461
- image_latents = image
462
- if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0:
463
- # expand init_latents for batch_size
464
- additional_image_per_prompt = batch_size // image_latents.shape[0]
465
- image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0)
466
- elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0:
467
- raise ValueError(
468
- f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
469
- )
470
- else:
471
- image_latents = torch.cat([image_latents], dim=0)
472
-
473
- image_latent_height, image_latent_width = image_latents.shape[3:]
474
- image_latents = self._pack_latents(
475
- image_latents, batch_size, num_channels_latents, image_latent_height, image_latent_width
476
- )
477
- all_image_latents.append(image_latents)
478
- image_latents = torch.cat(all_image_latents, dim=1)
479
-
480
- if isinstance(generator, list) and len(generator) != batch_size:
481
- raise ValueError(
482
- f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
483
- f" size of {batch_size}. Make sure the batch size matches the length of the generators."
484
- )
485
- if latents is None:
486
- latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
487
- latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
488
- else:
489
- latents = latents.to(device=device, dtype=dtype)
490
-
491
- return latents, image_latents
492
-
493
- @property
494
- def guidance_scale(self):
495
- return self._guidance_scale
496
-
497
- @property
498
- def attention_kwargs(self):
499
- return self._attention_kwargs
500
-
501
- @property
502
- def num_timesteps(self):
503
- return self._num_timesteps
504
-
505
- @property
506
- def current_timestep(self):
507
- return self._current_timestep
508
-
509
- @property
510
- def interrupt(self):
511
- return self._interrupt
512
-
513
- @torch.no_grad()
514
- @replace_example_docstring(EXAMPLE_DOC_STRING)
515
- def __call__(
516
- self,
517
- image: Optional[PipelineImageInput] = None,
518
- prompt: Union[str, List[str]] = None,
519
- negative_prompt: Union[str, List[str]] = None,
520
- true_cfg_scale: float = 4.0,
521
- height: Optional[int] = None,
522
- width: Optional[int] = None,
523
- num_inference_steps: int = 50,
524
- sigmas: Optional[List[float]] = None,
525
- guidance_scale: Optional[float] = None,
526
- num_images_per_prompt: int = 1,
527
- generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
528
- latents: Optional[torch.Tensor] = None,
529
- prompt_embeds: Optional[torch.Tensor] = None,
530
- prompt_embeds_mask: Optional[torch.Tensor] = None,
531
- negative_prompt_embeds: Optional[torch.Tensor] = None,
532
- negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
533
- output_type: Optional[str] = "pil",
534
- return_dict: bool = True,
535
- attention_kwargs: Optional[Dict[str, Any]] = None,
536
- callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
537
- callback_on_step_end_tensor_inputs: List[str] = ["latents"],
538
- max_sequence_length: int = 512,
539
- ):
540
- r"""
541
- Function invoked when calling the pipeline for generation.
542
-
543
- Args:
544
- image (`torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, `List[torch.Tensor]`, `List[PIL.Image.Image]`, or `List[np.ndarray]`):
545
- `Image`, numpy array or tensor representing an image batch to be used as the starting point. For both
546
- numpy array and pytorch tensor, the expected value range is between `[0, 1]` If it's a tensor or a list
547
- or tensors, the expected shape should be `(B, C, H, W)` or `(C, H, W)`. If it is a numpy array or a
548
- list of arrays, the expected shape should be `(B, H, W, C)` or `(H, W, C)` It can also accept image
549
- latents as `image`, but if passing latents directly it is not encoded again.
550
- prompt (`str` or `List[str]`, *optional*):
551
- The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
552
- instead.
553
- negative_prompt (`str` or `List[str]`, *optional*):
554
- The prompt or prompts not to guide the image generation. If not defined, one has to pass
555
- `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `true_cfg_scale` is
556
- not greater than `1`).
557
- true_cfg_scale (`float`, *optional*, defaults to 1.0):
558
- true_cfg_scale (`float`, *optional*, defaults to 1.0): Guidance scale as defined in [Classifier-Free
559
- Diffusion Guidance](https://huggingface.co/papers/2207.12598). `true_cfg_scale` is defined as `w` of
560
- equation 2. of [Imagen Paper](https://huggingface.co/papers/2205.11487). Classifier-free guidance is
561
- enabled by setting `true_cfg_scale > 1` and a provided `negative_prompt`. Higher guidance scale
562
- encourages to generate images that are closely linked to the text `prompt`, usually at the expense of
563
- lower image quality.
564
- height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
565
- The height in pixels of the generated image. This is set to 1024 by default for the best results.
566
- width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
567
- The width in pixels of the generated image. This is set to 1024 by default for the best results.
568
- num_inference_steps (`int`, *optional*, defaults to 50):
569
- The number of denoising steps. More denoising steps usually lead to a higher quality image at the
570
- expense of slower inference.
571
- sigmas (`List[float]`, *optional*):
572
- Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
573
- their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
574
- will be used.
575
- guidance_scale (`float`, *optional*, defaults to None):
576
- A guidance scale value for guidance distilled models. Unlike the traditional classifier-free guidance
577
- where the guidance scale is applied during inference through noise prediction rescaling, guidance
578
- distilled models take the guidance scale directly as an input parameter during forward pass. Guidance
579
- scale is enabled by setting `guidance_scale > 1`. Higher guidance scale encourages to generate images
580
- that are closely linked to the text `prompt`, usually at the expense of lower image quality. This
581
- parameter in the pipeline is there to support future guidance-distilled models when they come up. It is
582
- ignored when not using guidance distilled models. To enable traditional classifier-free guidance,
583
- please pass `true_cfg_scale > 1.0` and `negative_prompt` (even an empty negative prompt like " " should
584
- enable classifier-free guidance computations).
585
- num_images_per_prompt (`int`, *optional*, defaults to 1):
586
- The number of images to generate per prompt.
587
- generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
588
- One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
589
- to make generation deterministic.
590
- latents (`torch.Tensor`, *optional*):
591
- Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image
592
- generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
593
- tensor will be generated by sampling using the supplied random `generator`.
594
- prompt_embeds (`torch.Tensor`, *optional*):
595
- Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
596
- provided, text embeddings will be generated from `prompt` input argument.
597
- negative_prompt_embeds (`torch.Tensor`, *optional*):
598
- Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
599
- weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
600
- argument.
601
- output_type (`str`, *optional*, defaults to `"pil"`):
602
- The output format of the generate image. Choose between
603
- [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
604
- return_dict (`bool`, *optional*, defaults to `True`):
605
- Whether or not to return a [`~pipelines.qwenimage.QwenImagePipelineOutput`] instead of a plain tuple.
606
- attention_kwargs (`dict`, *optional*):
607
- A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
608
- `self.processor` in
609
- [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
610
- callback_on_step_end (`Callable`, *optional*):
611
- A function that calls at the end of each denoising steps during the inference. The function is called
612
- with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int,
613
- callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by
614
- `callback_on_step_end_tensor_inputs`.
615
- callback_on_step_end_tensor_inputs (`List`, *optional*):
616
- The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
617
- will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
618
- `._callback_tensor_inputs` attribute of your pipeline class.
619
- max_sequence_length (`int` defaults to 512): Maximum sequence length to use with the `prompt`.
620
-
621
- Examples:
622
-
623
- Returns:
624
- [`~pipelines.qwenimage.QwenImagePipelineOutput`] or `tuple`:
625
- [`~pipelines.qwenimage.QwenImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
626
- returning a tuple, the first element is a list with the generated images.
627
- """
628
- image_size = image[0].size if isinstance(image, list) else image.size
629
- calculated_width, calculated_height = calculate_dimensions(1024 * 1024, image_size[0] / image_size[1])
630
- height = height or calculated_height
631
- width = width or calculated_width
632
-
633
- multiple_of = self.vae_scale_factor * 2
634
- width = width // multiple_of * multiple_of
635
- height = height // multiple_of * multiple_of
636
-
637
- # 1. Check inputs. Raise error if not correct
638
- self.check_inputs(
639
- prompt,
640
- height,
641
- width,
642
- negative_prompt=negative_prompt,
643
- prompt_embeds=prompt_embeds,
644
- negative_prompt_embeds=negative_prompt_embeds,
645
- prompt_embeds_mask=prompt_embeds_mask,
646
- negative_prompt_embeds_mask=negative_prompt_embeds_mask,
647
- callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
648
- max_sequence_length=max_sequence_length,
649
- )
650
-
651
- self._guidance_scale = guidance_scale
652
- self._attention_kwargs = attention_kwargs
653
- self._current_timestep = None
654
- self._interrupt = False
655
-
656
- # 2. Define call parameters
657
- if prompt is not None and isinstance(prompt, str):
658
- batch_size = 1
659
- elif prompt is not None and isinstance(prompt, list):
660
- batch_size = len(prompt)
661
- else:
662
- batch_size = prompt_embeds.shape[0]
663
-
664
- device = self._execution_device
665
- # 3. Preprocess image
666
- if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels):
667
- if not isinstance(image, list):
668
- image = [image]
669
- condition_image_sizes = []
670
- condition_images = []
671
- vae_image_sizes = []
672
- vae_images = []
673
- for img in image:
674
- image_width, image_height = img.size
675
- condition_width, condition_height = calculate_dimensions(
676
- CONDITION_IMAGE_SIZE, image_width / image_height
677
- )
678
- vae_width, vae_height = calculate_dimensions(VAE_IMAGE_SIZE, image_width / image_height)
679
- condition_image_sizes.append((condition_width, condition_height))
680
- vae_image_sizes.append((vae_width, vae_height))
681
- condition_images.append(self.image_processor.resize(img, condition_height, condition_width))
682
- vae_images.append(self.image_processor.preprocess(img, vae_height, vae_width).unsqueeze(2))
683
-
684
- has_neg_prompt = negative_prompt is not None or (
685
- negative_prompt_embeds is not None and negative_prompt_embeds_mask is not None
686
- )
687
-
688
- if true_cfg_scale > 1 and not has_neg_prompt:
689
- logger.warning(
690
- f"true_cfg_scale is passed as {true_cfg_scale}, but classifier-free guidance is not enabled since no negative_prompt is provided."
691
- )
692
- elif true_cfg_scale <= 1 and has_neg_prompt:
693
- logger.warning(
694
- " negative_prompt is passed but classifier-free guidance is not enabled since true_cfg_scale <= 1"
695
- )
696
-
697
- do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
698
- prompt_embeds, prompt_embeds_mask = self.encode_prompt(
699
- image=condition_images,
700
- prompt=prompt,
701
- prompt_embeds=prompt_embeds,
702
- prompt_embeds_mask=prompt_embeds_mask,
703
- device=device,
704
- num_images_per_prompt=num_images_per_prompt,
705
- max_sequence_length=max_sequence_length,
706
- )
707
- if do_true_cfg:
708
- negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
709
- image=condition_images,
710
- prompt=negative_prompt,
711
- prompt_embeds=negative_prompt_embeds,
712
- prompt_embeds_mask=negative_prompt_embeds_mask,
713
- device=device,
714
- num_images_per_prompt=num_images_per_prompt,
715
- max_sequence_length=max_sequence_length,
716
- )
717
-
718
- # 4. Prepare latent variables
719
- num_channels_latents = self.transformer.config.in_channels // 4
720
- latents, image_latents = self.prepare_latents(
721
- vae_images,
722
- batch_size * num_images_per_prompt,
723
- num_channels_latents,
724
- height,
725
- width,
726
- prompt_embeds.dtype,
727
- device,
728
- generator,
729
- latents,
730
- )
731
- img_shapes = [
732
- [
733
- (1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2),
734
- *[
735
- (1, vae_height // self.vae_scale_factor // 2, vae_width // self.vae_scale_factor // 2)
736
- for vae_width, vae_height in vae_image_sizes
737
- ],
738
- ]
739
- ] * batch_size
740
-
741
- # 5. Prepare timesteps
742
- sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
743
- image_seq_len = latents.shape[1]
744
- mu = calculate_shift(
745
- image_seq_len,
746
- self.scheduler.config.get("base_image_seq_len", 256),
747
- self.scheduler.config.get("max_image_seq_len", 4096),
748
- self.scheduler.config.get("base_shift", 0.5),
749
- self.scheduler.config.get("max_shift", 1.15),
750
- )
751
- timesteps, num_inference_steps = retrieve_timesteps(
752
- self.scheduler,
753
- num_inference_steps,
754
- device,
755
- sigmas=sigmas,
756
- mu=mu,
757
- )
758
- num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
759
- self._num_timesteps = len(timesteps)
760
-
761
- # handle guidance
762
- if self.transformer.config.guidance_embeds and guidance_scale is None:
763
- raise ValueError("guidance_scale is required for guidance-distilled model.")
764
- elif self.transformer.config.guidance_embeds:
765
- guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32)
766
- guidance = guidance.expand(latents.shape[0])
767
- elif not self.transformer.config.guidance_embeds and guidance_scale is not None:
768
- logger.warning(
769
- f"guidance_scale is passed as {guidance_scale}, but ignored since the model is not guidance-distilled."
770
- )
771
- guidance = None
772
- elif not self.transformer.config.guidance_embeds and guidance_scale is None:
773
- guidance = None
774
-
775
- if self.attention_kwargs is None:
776
- self._attention_kwargs = {}
777
-
778
- txt_seq_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None
779
-
780
- image_rotary_emb = self.transformer.pos_embed(img_shapes, txt_seq_lens, device=latents.device)
781
- if do_true_cfg:
782
- negative_txt_seq_lens = (
783
- negative_prompt_embeds_mask.sum(dim=1).tolist()
784
- if negative_prompt_embeds_mask is not None
785
- else None
786
- )
787
- uncond_image_rotary_emb = self.transformer.pos_embed(
788
- img_shapes, negative_txt_seq_lens, device=latents.device
789
- )
790
- else:
791
- uncond_image_rotary_emb = None
792
-
793
- # 6. Denoising loop
794
- self.scheduler.set_begin_index(0)
795
- with self.progress_bar(total=num_inference_steps) as progress_bar:
796
- for i, t in enumerate(timesteps):
797
- if self.interrupt:
798
- continue
799
-
800
- self._current_timestep = t
801
-
802
- latent_model_input = latents
803
- if image_latents is not None:
804
- latent_model_input = torch.cat([latents, image_latents], dim=1)
805
-
806
- # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
807
- timestep = t.expand(latents.shape[0]).to(latents.dtype)
808
- with self.transformer.cache_context("cond"):
809
- noise_pred = self.transformer(
810
- hidden_states=latent_model_input,
811
- timestep=timestep / 1000,
812
- guidance=guidance,
813
- encoder_hidden_states_mask=prompt_embeds_mask,
814
- encoder_hidden_states=prompt_embeds,
815
- image_rotary_emb=image_rotary_emb,
816
- attention_kwargs=self.attention_kwargs,
817
- return_dict=False,
818
- )[0]
819
- noise_pred = noise_pred[:, : latents.size(1)]
820
-
821
- if do_true_cfg:
822
- with self.transformer.cache_context("uncond"):
823
- neg_noise_pred = self.transformer(
824
- hidden_states=latent_model_input,
825
- timestep=timestep / 1000,
826
- guidance=guidance,
827
- encoder_hidden_states_mask=negative_prompt_embeds_mask,
828
- encoder_hidden_states=negative_prompt_embeds,
829
- image_rotary_emb=uncond_image_rotary_emb,
830
- attention_kwargs=self.attention_kwargs,
831
- return_dict=False,
832
- )[0]
833
- neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
834
- comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
835
-
836
- cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
837
- noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
838
- noise_pred = comb_pred * (cond_norm / noise_norm)
839
-
840
- # compute the previous noisy sample x_t -> x_t-1
841
- latents_dtype = latents.dtype
842
- latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
843
-
844
- if latents.dtype != latents_dtype:
845
- if torch.backends.mps.is_available():
846
- # some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
847
- latents = latents.to(latents_dtype)
848
-
849
- if callback_on_step_end is not None:
850
- callback_kwargs = {}
851
- for k in callback_on_step_end_tensor_inputs:
852
- callback_kwargs[k] = locals()[k]
853
- callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
854
-
855
- latents = callback_outputs.pop("latents", latents)
856
- prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
857
-
858
- # call the callback, if provided
859
- if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
860
- progress_bar.update()
861
-
862
- if XLA_AVAILABLE:
863
- xm.mark_step()
864
-
865
- self._current_timestep = None
866
- if output_type == "latent":
867
- image = latents
868
- else:
869
- latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
870
- latents = latents.to(self.vae.dtype)
871
- latents_mean = (
872
- torch.tensor(self.vae.config.latents_mean)
873
- .view(1, self.vae.config.z_dim, 1, 1, 1)
874
- .to(latents.device, latents.dtype)
875
- )
876
- latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
877
- latents.device, latents.dtype
878
- )
879
- latents = latents / latents_std + latents_mean
880
- image = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
881
- image = self.image_processor.postprocess(image, output_type=output_type)
882
-
883
- # Offload all models
884
- self.maybe_free_model_hooks()
885
-
886
- if not return_dict:
887
- return (image,)
888
-
889
- return QwenImagePipelineOutput(images=image)
 
1
+ # Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import math
17
+ from typing import Any, Callable, Dict, List, Optional, Union
18
+
19
+ import numpy as np
20
+ import torch
21
+ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
22
+
23
+ from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
24
+ from diffusers.loaders import QwenImageLoraLoaderMixin
25
+ from diffusers.models import AutoencoderKLQwenImage, QwenImageTransformer2DModel
26
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
27
+ from diffusers.utils import is_torch_xla_available, logging, replace_example_docstring
28
+ from diffusers.utils.torch_utils import randn_tensor
29
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
30
+ from diffusers.pipelines.qwenimage.pipeline_output import QwenImagePipelineOutput
31
+
32
+
33
+ if is_torch_xla_available():
34
+ import torch_xla.core.xla_model as xm
35
+
36
+ XLA_AVAILABLE = True
37
+ else:
38
+ XLA_AVAILABLE = False
39
+
40
+
41
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
42
+
43
+ EXAMPLE_DOC_STRING = """
44
+ Examples:
45
+ ```py
46
+ >>> import torch
47
+ >>> from PIL import Image
48
+ >>> from diffusers import QwenImageEditPlusPipeline
49
+ >>> from diffusers.utils import load_image
50
+
51
+ >>> pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2511", torch_dtype=torch.bfloat16)
52
+ >>> pipe.to("cuda")
53
+ >>> image = load_image("input.png").convert("RGB")
54
+ >>> prompt = (
55
+ ... "Make Pikachu hold a sign that says 'Qwen Edit is awesome', yarn art style, detailed, vibrant colors"
56
+ ... )
57
+ >>> # Depending on the variant being used, the pipeline call will slightly vary.
58
+ >>> # Refer to the pipeline documentation for more details.
59
+ >>> image = pipe(image, prompt, num_inference_steps=50).images[0]
60
+ >>> image.save("qwenimage_edit_plus.png")
61
+ ```
62
+ """
63
+
64
+ CONDITION_IMAGE_SIZE = 384 * 384
65
+ VAE_IMAGE_SIZE = 1024 * 1024
66
+
67
+
68
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.calculate_shift
69
+ def calculate_shift(
70
+ image_seq_len,
71
+ base_seq_len: int = 256,
72
+ max_seq_len: int = 4096,
73
+ base_shift: float = 0.5,
74
+ max_shift: float = 1.15,
75
+ ):
76
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
77
+ b = base_shift - m * base_seq_len
78
+ mu = image_seq_len * m + b
79
+ return mu
80
+
81
+
82
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
83
+ def retrieve_timesteps(
84
+ scheduler,
85
+ num_inference_steps: Optional[int] = None,
86
+ device: Optional[Union[str, torch.device]] = None,
87
+ timesteps: Optional[List[int]] = None,
88
+ sigmas: Optional[List[float]] = None,
89
+ **kwargs,
90
+ ):
91
+ r"""
92
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
93
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
94
+
95
+ Args:
96
+ scheduler (`SchedulerMixin`):
97
+ The scheduler to get timesteps from.
98
+ num_inference_steps (`int`):
99
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
100
+ must be `None`.
101
+ device (`str` or `torch.device`, *optional*):
102
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
103
+ timesteps (`List[int]`, *optional*):
104
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
105
+ `num_inference_steps` and `sigmas` must be `None`.
106
+ sigmas (`List[float]`, *optional*):
107
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
108
+ `num_inference_steps` and `timesteps` must be `None`.
109
+
110
+ Returns:
111
+ `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
112
+ second element is the number of inference steps.
113
+ """
114
+ if timesteps is not None and sigmas is not None:
115
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
116
+ if timesteps is not None:
117
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
118
+ if not accepts_timesteps:
119
+ raise ValueError(
120
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
121
+ f" timestep schedules. Please check whether you are using the correct scheduler."
122
+ )
123
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
124
+ timesteps = scheduler.timesteps
125
+ num_inference_steps = len(timesteps)
126
+ elif sigmas is not None:
127
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
128
+ if not accept_sigmas:
129
+ raise ValueError(
130
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
131
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
132
+ )
133
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
134
+ timesteps = scheduler.timesteps
135
+ num_inference_steps = len(timesteps)
136
+ else:
137
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
138
+ timesteps = scheduler.timesteps
139
+ return timesteps, num_inference_steps
140
+
141
+
142
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
143
+ def retrieve_latents(
144
+ encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample"
145
+ ):
146
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
147
+ return encoder_output.latent_dist.sample(generator)
148
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
149
+ return encoder_output.latent_dist.mode()
150
+ elif hasattr(encoder_output, "latents"):
151
+ return encoder_output.latents
152
+ else:
153
+ raise AttributeError("Could not access latents of provided encoder_output")
154
+
155
+
156
+ def calculate_dimensions(target_area, ratio):
157
+ width = math.sqrt(target_area * ratio)
158
+ height = width / ratio
159
+
160
+ width = round(width / 32) * 32
161
+ height = round(height / 32) * 32
162
+
163
+ return width, height
164
+
165
+
166
+ class QwenImageEditPlusPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
167
+ r"""
168
+ The Qwen-Image-Edit pipeline for image editing.
169
+
170
+ Args:
171
+ transformer ([`QwenImageTransformer2DModel`]):
172
+ Conditional Transformer (MMDiT) architecture to denoise the encoded image latents.
173
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
174
+ A scheduler to be used in combination with `transformer` to denoise the encoded image latents.
175
+ vae ([`AutoencoderKL`]):
176
+ Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations.
177
+ text_encoder ([`Qwen2.5-VL-7B-Instruct`]):
178
+ [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), specifically the
179
+ [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) variant.
180
+ tokenizer (`QwenTokenizer`):
181
+ Tokenizer of class
182
+ [CLIPTokenizer](https://huggingface.co/docs/transformers/en/model_doc/clip#transformers.CLIPTokenizer).
183
+ """
184
+
185
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
186
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
187
+
188
+ def __init__(
189
+ self,
190
+ scheduler: FlowMatchEulerDiscreteScheduler,
191
+ vae: AutoencoderKLQwenImage,
192
+ text_encoder: Qwen2_5_VLForConditionalGeneration,
193
+ tokenizer: Qwen2Tokenizer,
194
+ processor: Qwen2VLProcessor,
195
+ transformer: QwenImageTransformer2DModel,
196
+ ):
197
+ super().__init__()
198
+
199
+ self.register_modules(
200
+ vae=vae,
201
+ text_encoder=text_encoder,
202
+ tokenizer=tokenizer,
203
+ processor=processor,
204
+ transformer=transformer,
205
+ scheduler=scheduler,
206
+ )
207
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
208
+ self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16
209
+ # QwenImage latents are turned into 2x2 patches and packed. This means the latent width and height has to be divisible
210
+ # by the patch size. So the vae scale factor is multiplied by the patch size to account for this
211
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2)
212
+ self.tokenizer_max_length = 1024
213
+
214
+ self.prompt_template_encode = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
215
+ self.prompt_template_encode_start_idx = 64
216
+ self.default_sample_size = 128
217
+
218
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._extract_masked_hidden
219
+ def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
220
+ bool_mask = mask.bool()
221
+ valid_lengths = bool_mask.sum(dim=1)
222
+ selected = hidden_states[bool_mask]
223
+ split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
224
+
225
+ return split_result
226
+
227
+ def _get_qwen_prompt_embeds(
228
+ self,
229
+ prompt: Union[str, List[str]] = None,
230
+ image: Optional[torch.Tensor] = None,
231
+ device: Optional[torch.device] = None,
232
+ dtype: Optional[torch.dtype] = None,
233
+ ):
234
+ device = device or self._execution_device
235
+ dtype = dtype or self.text_encoder.dtype
236
+
237
+ prompt = [prompt] if isinstance(prompt, str) else prompt
238
+ img_prompt_template = "Picture {}: <|vision_start|><|image_pad|><|vision_end|>"
239
+ if isinstance(image, list):
240
+ base_img_prompt = ""
241
+ for i, img in enumerate(image):
242
+ base_img_prompt += img_prompt_template.format(i + 1)
243
+ elif image is not None:
244
+ base_img_prompt = img_prompt_template.format(1)
245
+ else:
246
+ base_img_prompt = ""
247
+
248
+ template = self.prompt_template_encode
249
+
250
+ drop_idx = self.prompt_template_encode_start_idx
251
+ txt = [template.format(base_img_prompt + e) for e in prompt]
252
+
253
+ model_inputs = self.processor(
254
+ text=txt,
255
+ images=image,
256
+ padding=True,
257
+ return_tensors="pt",
258
+ ).to(device)
259
+
260
+ outputs = self.text_encoder(
261
+ input_ids=model_inputs.input_ids,
262
+ attention_mask=model_inputs.attention_mask,
263
+ pixel_values=model_inputs.pixel_values,
264
+ image_grid_thw=model_inputs.image_grid_thw,
265
+ output_hidden_states=True,
266
+ )
267
+
268
+ hidden_states = outputs.hidden_states[-1]
269
+ split_hidden_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask)
270
+ split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
271
+ attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
272
+ max_seq_len = max([e.size(0) for e in split_hidden_states])
273
+ prompt_embeds = torch.stack(
274
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
275
+ )
276
+ encoder_attention_mask = torch.stack(
277
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
278
+ )
279
+
280
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
281
+
282
+ return prompt_embeds, encoder_attention_mask
283
+
284
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.encode_prompt
285
+ def encode_prompt(
286
+ self,
287
+ prompt: Union[str, List[str]],
288
+ image: Optional[torch.Tensor] = None,
289
+ device: Optional[torch.device] = None,
290
+ num_images_per_prompt: int = 1,
291
+ prompt_embeds: Optional[torch.Tensor] = None,
292
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
293
+ max_sequence_length: int = 1024,
294
+ ):
295
+ r"""
296
+
297
+ Args:
298
+ prompt (`str` or `List[str]`, *optional*):
299
+ prompt to be encoded
300
+ image (`torch.Tensor`, *optional*):
301
+ image to be encoded
302
+ device: (`torch.device`):
303
+ torch device
304
+ num_images_per_prompt (`int`):
305
+ number of images that should be generated per prompt
306
+ prompt_embeds (`torch.Tensor`, *optional*):
307
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
308
+ provided, text embeddings will be generated from `prompt` input argument.
309
+ """
310
+ device = device or self._execution_device
311
+
312
+ prompt = [prompt] if isinstance(prompt, str) else prompt
313
+ batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0]
314
+
315
+ if prompt_embeds is None:
316
+ prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, image, device)
317
+
318
+ _, seq_len, _ = prompt_embeds.shape
319
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
320
+ prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
321
+ prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
322
+ prompt_embeds_mask = prompt_embeds_mask.view(batch_size * num_images_per_prompt, seq_len)
323
+
324
+ return prompt_embeds, prompt_embeds_mask
325
+
326
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.check_inputs
327
+ def check_inputs(
328
+ self,
329
+ prompt,
330
+ height,
331
+ width,
332
+ negative_prompt=None,
333
+ prompt_embeds=None,
334
+ negative_prompt_embeds=None,
335
+ prompt_embeds_mask=None,
336
+ negative_prompt_embeds_mask=None,
337
+ callback_on_step_end_tensor_inputs=None,
338
+ max_sequence_length=None,
339
+ ):
340
+ if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
341
+ logger.warning(
342
+ f"`height` and `width` have to be divisible by {self.vae_scale_factor * 2} but are {height} and {width}. Dimensions will be resized accordingly"
343
+ )
344
+
345
+ if callback_on_step_end_tensor_inputs is not None and not all(
346
+ k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs
347
+ ):
348
+ raise ValueError(
349
+ f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
350
+ )
351
+
352
+ if prompt is not None and prompt_embeds is not None:
353
+ raise ValueError(
354
+ f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
355
+ " only forward one of the two."
356
+ )
357
+ elif prompt is None and prompt_embeds is None:
358
+ raise ValueError(
359
+ "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
360
+ )
361
+ elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
362
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
363
+
364
+ if negative_prompt is not None and negative_prompt_embeds is not None:
365
+ raise ValueError(
366
+ f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
367
+ f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
368
+ )
369
+
370
+ if prompt_embeds is not None and prompt_embeds_mask is None:
371
+ raise ValueError(
372
+ "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`."
373
+ )
374
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
375
+ raise ValueError(
376
+ "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`."
377
+ )
378
+
379
+ if max_sequence_length is not None and max_sequence_length > 1024:
380
+ raise ValueError(f"`max_sequence_length` cannot be greater than 1024 but is {max_sequence_length}")
381
+
382
+ @staticmethod
383
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._pack_latents
384
+ def _pack_latents(latents, batch_size, num_channels_latents, height, width):
385
+ latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
386
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
387
+ latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
388
+
389
+ return latents
390
+
391
+ @staticmethod
392
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._unpack_latents
393
+ def _unpack_latents(latents, height, width, vae_scale_factor):
394
+ batch_size, num_patches, channels = latents.shape
395
+
396
+ # VAE applies 8x compression on images but we must also account for packing which requires
397
+ # latent height and width to be divisible by 2.
398
+ height = 2 * (int(height) // (vae_scale_factor * 2))
399
+ width = 2 * (int(width) // (vae_scale_factor * 2))
400
+
401
+ latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
402
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
403
+
404
+ latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width)
405
+
406
+ return latents
407
+
408
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline._encode_vae_image
409
+ def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
410
+ if isinstance(generator, list):
411
+ image_latents = [
412
+ retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i], sample_mode="argmax")
413
+ for i in range(image.shape[0])
414
+ ]
415
+ image_latents = torch.cat(image_latents, dim=0)
416
+ else:
417
+ image_latents = retrieve_latents(self.vae.encode(image), generator=generator, sample_mode="argmax")
418
+ latents_mean = (
419
+ torch.tensor(self.vae.config.latents_mean)
420
+ .view(1, self.latent_channels, 1, 1, 1)
421
+ .to(image_latents.device, image_latents.dtype)
422
+ )
423
+ latents_std = (
424
+ torch.tensor(self.vae.config.latents_std)
425
+ .view(1, self.latent_channels, 1, 1, 1)
426
+ .to(image_latents.device, image_latents.dtype)
427
+ )
428
+ image_latents = (image_latents - latents_mean) / latents_std
429
+
430
+ return image_latents
431
+
432
+ def prepare_latents(
433
+ self,
434
+ images,
435
+ batch_size,
436
+ num_channels_latents,
437
+ height,
438
+ width,
439
+ dtype,
440
+ device,
441
+ generator,
442
+ latents=None,
443
+ ):
444
+ # VAE applies 8x compression on images but we must also account for packing which requires
445
+ # latent height and width to be divisible by 2.
446
+ height = 2 * (int(height) // (self.vae_scale_factor * 2))
447
+ width = 2 * (int(width) // (self.vae_scale_factor * 2))
448
+
449
+ shape = (batch_size, 1, num_channels_latents, height, width)
450
+
451
+ image_latents = None
452
+ if images is not None:
453
+ if not isinstance(images, list):
454
+ images = [images]
455
+ all_image_latents = []
456
+ for image in images:
457
+ image = image.to(device=device, dtype=dtype)
458
+ if image.shape[1] != self.latent_channels:
459
+ image_latents = self._encode_vae_image(image=image, generator=generator)
460
+ else:
461
+ image_latents = image
462
+ if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0:
463
+ # expand init_latents for batch_size
464
+ additional_image_per_prompt = batch_size // image_latents.shape[0]
465
+ image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0)
466
+ elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0:
467
+ raise ValueError(
468
+ f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
469
+ )
470
+ else:
471
+ image_latents = torch.cat([image_latents], dim=0)
472
+
473
+ image_latent_height, image_latent_width = image_latents.shape[3:]
474
+ image_latents = self._pack_latents(
475
+ image_latents, batch_size, num_channels_latents, image_latent_height, image_latent_width
476
+ )
477
+ all_image_latents.append(image_latents)
478
+ image_latents = torch.cat(all_image_latents, dim=1)
479
+
480
+ if isinstance(generator, list) and len(generator) != batch_size:
481
+ raise ValueError(
482
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
483
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
484
+ )
485
+ if latents is None:
486
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
487
+ latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
488
+ else:
489
+ latents = latents.to(device=device, dtype=dtype)
490
+
491
+ return latents, image_latents
492
+
493
+ @property
494
+ def guidance_scale(self):
495
+ return self._guidance_scale
496
+
497
+ @property
498
+ def attention_kwargs(self):
499
+ return self._attention_kwargs
500
+
501
+ @property
502
+ def num_timesteps(self):
503
+ return self._num_timesteps
504
+
505
+ @property
506
+ def current_timestep(self):
507
+ return self._current_timestep
508
+
509
+ @property
510
+ def interrupt(self):
511
+ return self._interrupt
512
+
513
+ @torch.no_grad()
514
+ @replace_example_docstring(EXAMPLE_DOC_STRING)
515
+ def __call__(
516
+ self,
517
+ image: Optional[PipelineImageInput] = None,
518
+ prompt: Union[str, List[str]] = None,
519
+ negative_prompt: Union[str, List[str]] = None,
520
+ true_cfg_scale: float = 4.0,
521
+ height: Optional[int] = None,
522
+ width: Optional[int] = None,
523
+ num_inference_steps: int = 50,
524
+ sigmas: Optional[List[float]] = None,
525
+ guidance_scale: Optional[float] = None,
526
+ num_images_per_prompt: int = 1,
527
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
528
+ latents: Optional[torch.Tensor] = None,
529
+ prompt_embeds: Optional[torch.Tensor] = None,
530
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
531
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
532
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
533
+ output_type: Optional[str] = "pil",
534
+ return_dict: bool = True,
535
+ attention_kwargs: Optional[Dict[str, Any]] = None,
536
+ callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
537
+ callback_on_step_end_tensor_inputs: List[str] = ["latents"],
538
+ max_sequence_length: int = 512,
539
+ ):
540
+ r"""
541
+ Function invoked when calling the pipeline for generation.
542
+
543
+ Args:
544
+ image (`torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, `List[torch.Tensor]`, `List[PIL.Image.Image]`, or `List[np.ndarray]`):
545
+ `Image`, numpy array or tensor representing an image batch to be used as the starting point. For both
546
+ numpy array and pytorch tensor, the expected value range is between `[0, 1]` If it's a tensor or a list
547
+ or tensors, the expected shape should be `(B, C, H, W)` or `(C, H, W)`. If it is a numpy array or a
548
+ list of arrays, the expected shape should be `(B, H, W, C)` or `(H, W, C)` It can also accept image
549
+ latents as `image`, but if passing latents directly it is not encoded again.
550
+ prompt (`str` or `List[str]`, *optional*):
551
+ The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
552
+ instead.
553
+ negative_prompt (`str` or `List[str]`, *optional*):
554
+ The prompt or prompts not to guide the image generation. If not defined, one has to pass
555
+ `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `true_cfg_scale` is
556
+ not greater than `1`).
557
+ true_cfg_scale (`float`, *optional*, defaults to 1.0):
558
+ true_cfg_scale (`float`, *optional*, defaults to 1.0): Guidance scale as defined in [Classifier-Free
559
+ Diffusion Guidance](https://huggingface.co/papers/2207.12598). `true_cfg_scale` is defined as `w` of
560
+ equation 2. of [Imagen Paper](https://huggingface.co/papers/2205.11487). Classifier-free guidance is
561
+ enabled by setting `true_cfg_scale > 1` and a provided `negative_prompt`. Higher guidance scale
562
+ encourages to generate images that are closely linked to the text `prompt`, usually at the expense of
563
+ lower image quality.
564
+ height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
565
+ The height in pixels of the generated image. This is set to 1024 by default for the best results.
566
+ width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
567
+ The width in pixels of the generated image. This is set to 1024 by default for the best results.
568
+ num_inference_steps (`int`, *optional*, defaults to 50):
569
+ The number of denoising steps. More denoising steps usually lead to a higher quality image at the
570
+ expense of slower inference.
571
+ sigmas (`List[float]`, *optional*):
572
+ Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
573
+ their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
574
+ will be used.
575
+ guidance_scale (`float`, *optional*, defaults to None):
576
+ A guidance scale value for guidance distilled models. Unlike the traditional classifier-free guidance
577
+ where the guidance scale is applied during inference through noise prediction rescaling, guidance
578
+ distilled models take the guidance scale directly as an input parameter during forward pass. Guidance
579
+ scale is enabled by setting `guidance_scale > 1`. Higher guidance scale encourages to generate images
580
+ that are closely linked to the text `prompt`, usually at the expense of lower image quality. This
581
+ parameter in the pipeline is there to support future guidance-distilled models when they come up. It is
582
+ ignored when not using guidance distilled models. To enable traditional classifier-free guidance,
583
+ please pass `true_cfg_scale > 1.0` and `negative_prompt` (even an empty negative prompt like " " should
584
+ enable classifier-free guidance computations).
585
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
586
+ The number of images to generate per prompt.
587
+ generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
588
+ One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
589
+ to make generation deterministic.
590
+ latents (`torch.Tensor`, *optional*):
591
+ Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image
592
+ generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
593
+ tensor will be generated by sampling using the supplied random `generator`.
594
+ prompt_embeds (`torch.Tensor`, *optional*):
595
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
596
+ provided, text embeddings will be generated from `prompt` input argument.
597
+ negative_prompt_embeds (`torch.Tensor`, *optional*):
598
+ Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
599
+ weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
600
+ argument.
601
+ output_type (`str`, *optional*, defaults to `"pil"`):
602
+ The output format of the generate image. Choose between
603
+ [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
604
+ return_dict (`bool`, *optional*, defaults to `True`):
605
+ Whether or not to return a [`~pipelines.qwenimage.QwenImagePipelineOutput`] instead of a plain tuple.
606
+ attention_kwargs (`dict`, *optional*):
607
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
608
+ `self.processor` in
609
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
610
+ callback_on_step_end (`Callable`, *optional*):
611
+ A function that calls at the end of each denoising steps during the inference. The function is called
612
+ with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int,
613
+ callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by
614
+ `callback_on_step_end_tensor_inputs`.
615
+ callback_on_step_end_tensor_inputs (`List`, *optional*):
616
+ The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
617
+ will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
618
+ `._callback_tensor_inputs` attribute of your pipeline class.
619
+ max_sequence_length (`int` defaults to 512): Maximum sequence length to use with the `prompt`.
620
+
621
+ Examples:
622
+
623
+ Returns:
624
+ [`~pipelines.qwenimage.QwenImagePipelineOutput`] or `tuple`:
625
+ [`~pipelines.qwenimage.QwenImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
626
+ returning a tuple, the first element is a list with the generated images.
627
+ """
628
+ image_size = image[0].size if isinstance(image, list) else image.size
629
+ calculated_width, calculated_height = calculate_dimensions(1024 * 1024, image_size[0] / image_size[1])
630
+ height = height or calculated_height
631
+ width = width or calculated_width
632
+
633
+ multiple_of = self.vae_scale_factor * 2
634
+ width = width // multiple_of * multiple_of
635
+ height = height // multiple_of * multiple_of
636
+
637
+ # 1. Check inputs. Raise error if not correct
638
+ self.check_inputs(
639
+ prompt,
640
+ height,
641
+ width,
642
+ negative_prompt=negative_prompt,
643
+ prompt_embeds=prompt_embeds,
644
+ negative_prompt_embeds=negative_prompt_embeds,
645
+ prompt_embeds_mask=prompt_embeds_mask,
646
+ negative_prompt_embeds_mask=negative_prompt_embeds_mask,
647
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
648
+ max_sequence_length=max_sequence_length,
649
+ )
650
+
651
+ self._guidance_scale = guidance_scale
652
+ self._attention_kwargs = attention_kwargs
653
+ self._current_timestep = None
654
+ self._interrupt = False
655
+
656
+ # 2. Define call parameters
657
+ if prompt is not None and isinstance(prompt, str):
658
+ batch_size = 1
659
+ elif prompt is not None and isinstance(prompt, list):
660
+ batch_size = len(prompt)
661
+ else:
662
+ batch_size = prompt_embeds.shape[0]
663
+
664
+ device = self._execution_device
665
+ # 3. Preprocess image
666
+ if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels):
667
+ if not isinstance(image, list):
668
+ image = [image]
669
+ condition_image_sizes = []
670
+ condition_images = []
671
+ vae_image_sizes = []
672
+ vae_images = []
673
+ for img in image:
674
+ image_width, image_height = img.size
675
+ condition_width, condition_height = calculate_dimensions(
676
+ CONDITION_IMAGE_SIZE, image_width / image_height
677
+ )
678
+ vae_width, vae_height = calculate_dimensions(VAE_IMAGE_SIZE, image_width / image_height)
679
+ condition_image_sizes.append((condition_width, condition_height))
680
+ vae_image_sizes.append((vae_width, vae_height))
681
+ condition_images.append(self.image_processor.resize(img, condition_height, condition_width))
682
+ vae_images.append(self.image_processor.preprocess(img, vae_height, vae_width).unsqueeze(2))
683
+
684
+ has_neg_prompt = negative_prompt is not None or (
685
+ negative_prompt_embeds is not None and negative_prompt_embeds_mask is not None
686
+ )
687
+
688
+ if true_cfg_scale > 1 and not has_neg_prompt:
689
+ logger.warning(
690
+ f"true_cfg_scale is passed as {true_cfg_scale}, but classifier-free guidance is not enabled since no negative_prompt is provided."
691
+ )
692
+ elif true_cfg_scale <= 1 and has_neg_prompt:
693
+ logger.warning(
694
+ " negative_prompt is passed but classifier-free guidance is not enabled since true_cfg_scale <= 1"
695
+ )
696
+
697
+ do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
698
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
699
+ image=condition_images,
700
+ prompt=prompt,
701
+ prompt_embeds=prompt_embeds,
702
+ prompt_embeds_mask=prompt_embeds_mask,
703
+ device=device,
704
+ num_images_per_prompt=num_images_per_prompt,
705
+ max_sequence_length=max_sequence_length,
706
+ )
707
+ if do_true_cfg:
708
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
709
+ image=condition_images,
710
+ prompt=negative_prompt,
711
+ prompt_embeds=negative_prompt_embeds,
712
+ prompt_embeds_mask=negative_prompt_embeds_mask,
713
+ device=device,
714
+ num_images_per_prompt=num_images_per_prompt,
715
+ max_sequence_length=max_sequence_length,
716
+ )
717
+
718
+ # 4. Prepare latent variables
719
+ num_channels_latents = self.transformer.config.in_channels // 4
720
+ latents, image_latents = self.prepare_latents(
721
+ vae_images,
722
+ batch_size * num_images_per_prompt,
723
+ num_channels_latents,
724
+ height,
725
+ width,
726
+ prompt_embeds.dtype,
727
+ device,
728
+ generator,
729
+ latents,
730
+ )
731
+ img_shapes = [
732
+ [
733
+ (1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2),
734
+ *[
735
+ (1, vae_height // self.vae_scale_factor // 2, vae_width // self.vae_scale_factor // 2)
736
+ for vae_width, vae_height in vae_image_sizes
737
+ ],
738
+ ]
739
+ ] * batch_size
740
+
741
+ # 5. Prepare timesteps
742
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
743
+ image_seq_len = latents.shape[1]
744
+ mu = calculate_shift(
745
+ image_seq_len,
746
+ self.scheduler.config.get("base_image_seq_len", 256),
747
+ self.scheduler.config.get("max_image_seq_len", 4096),
748
+ self.scheduler.config.get("base_shift", 0.5),
749
+ self.scheduler.config.get("max_shift", 1.15),
750
+ )
751
+ timesteps, num_inference_steps = retrieve_timesteps(
752
+ self.scheduler,
753
+ num_inference_steps,
754
+ device,
755
+ sigmas=sigmas,
756
+ mu=mu,
757
+ )
758
+ num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
759
+ self._num_timesteps = len(timesteps)
760
+
761
+ # handle guidance
762
+ if self.transformer.config.guidance_embeds and guidance_scale is None:
763
+ raise ValueError("guidance_scale is required for guidance-distilled model.")
764
+ elif self.transformer.config.guidance_embeds:
765
+ guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32)
766
+ guidance = guidance.expand(latents.shape[0])
767
+ elif not self.transformer.config.guidance_embeds and guidance_scale is not None:
768
+ logger.warning(
769
+ f"guidance_scale is passed as {guidance_scale}, but ignored since the model is not guidance-distilled."
770
+ )
771
+ guidance = None
772
+ elif not self.transformer.config.guidance_embeds and guidance_scale is None:
773
+ guidance = None
774
+
775
+ if self.attention_kwargs is None:
776
+ self._attention_kwargs = {}
777
+
778
+ txt_seq_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None
779
+
780
+ image_rotary_emb = self.transformer.pos_embed(img_shapes, txt_seq_lens, device=latents.device)
781
+ if do_true_cfg:
782
+ negative_txt_seq_lens = (
783
+ negative_prompt_embeds_mask.sum(dim=1).tolist()
784
+ if negative_prompt_embeds_mask is not None
785
+ else None
786
+ )
787
+ uncond_image_rotary_emb = self.transformer.pos_embed(
788
+ img_shapes, negative_txt_seq_lens, device=latents.device
789
+ )
790
+ else:
791
+ uncond_image_rotary_emb = None
792
+
793
+ # 6. Denoising loop
794
+ self.scheduler.set_begin_index(0)
795
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
796
+ for i, t in enumerate(timesteps):
797
+ if self.interrupt:
798
+ continue
799
+
800
+ self._current_timestep = t
801
+
802
+ latent_model_input = latents
803
+ if image_latents is not None:
804
+ latent_model_input = torch.cat([latents, image_latents], dim=1)
805
+
806
+ # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
807
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
808
+ with self.transformer.cache_context("cond"):
809
+ noise_pred = self.transformer(
810
+ hidden_states=latent_model_input,
811
+ timestep=timestep / 1000,
812
+ guidance=guidance,
813
+ encoder_hidden_states_mask=prompt_embeds_mask,
814
+ encoder_hidden_states=prompt_embeds,
815
+ image_rotary_emb=image_rotary_emb,
816
+ attention_kwargs=self.attention_kwargs,
817
+ return_dict=False,
818
+ )[0]
819
+ noise_pred = noise_pred[:, : latents.size(1)]
820
+
821
+ if do_true_cfg:
822
+ with self.transformer.cache_context("uncond"):
823
+ neg_noise_pred = self.transformer(
824
+ hidden_states=latent_model_input,
825
+ timestep=timestep / 1000,
826
+ guidance=guidance,
827
+ encoder_hidden_states_mask=negative_prompt_embeds_mask,
828
+ encoder_hidden_states=negative_prompt_embeds,
829
+ image_rotary_emb=uncond_image_rotary_emb,
830
+ attention_kwargs=self.attention_kwargs,
831
+ return_dict=False,
832
+ )[0]
833
+ neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
834
+ comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
835
+
836
+ cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
837
+ noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
838
+ noise_pred = comb_pred * (cond_norm / noise_norm)
839
+
840
+ # compute the previous noisy sample x_t -> x_t-1
841
+ latents_dtype = latents.dtype
842
+ latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
843
+
844
+ if latents.dtype != latents_dtype:
845
+ if torch.backends.mps.is_available():
846
+ # some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
847
+ latents = latents.to(latents_dtype)
848
+
849
+ if callback_on_step_end is not None:
850
+ callback_kwargs = {}
851
+ for k in callback_on_step_end_tensor_inputs:
852
+ callback_kwargs[k] = locals()[k]
853
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
854
+
855
+ latents = callback_outputs.pop("latents", latents)
856
+ prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
857
+
858
+ # call the callback, if provided
859
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
860
+ progress_bar.update()
861
+
862
+ if XLA_AVAILABLE:
863
+ xm.mark_step()
864
+
865
+ self._current_timestep = None
866
+ if output_type == "latent":
867
+ image = latents
868
+ else:
869
+ latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
870
+ latents = latents.to(self.vae.dtype)
871
+ latents_mean = (
872
+ torch.tensor(self.vae.config.latents_mean)
873
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
874
+ .to(latents.device, latents.dtype)
875
+ )
876
+ latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
877
+ latents.device, latents.dtype
878
+ )
879
+ latents = latents / latents_std + latents_mean
880
+ image = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
881
+ image = self.image_processor.postprocess(image, output_type=output_type)
882
+
883
+ # Offload all models
884
+ self.maybe_free_model_hooks()
885
+
886
+ if not return_dict:
887
+ return (image,)
888
+
889
+ return QwenImagePipelineOutput(images=image)
qwenimage/qwen_fa3_processor.py CHANGED
@@ -1,110 +1,110 @@
1
- """
2
- Paired with a good language model. Thanks!
3
- """
4
-
5
- import torch
6
- import torch.nn.functional as F
7
- from torch.nn.attention import sdpa_kernel, SDPBackend
8
- from typing import Optional, Tuple
9
- from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
10
-
11
-
12
- class QwenDoubleStreamAttnProcessorFA3:
13
- """
14
- FA3-grade attention processor for Qwen double-stream architecture.
15
- Routes through PyTorch's cuDNN attention backend, which on Hopper (SM 9.0+)
16
- dispatches to the same FlashAttention-3 family of kernels as vLLM's FA3 —
17
- bundled with the base image so there's no external-kernel ABI risk.
18
-
19
- Notes / limitations:
20
- - General attention masks are not supported here. `is_causal=False` and no arbitrary mask.
21
- - Expects an available `apply_rotary_emb_qwen` in scope (same as your non-FA3 processor).
22
- """
23
-
24
- _attention_backend = "fa3" # for parity with your other processors, not used internally
25
-
26
- @torch.no_grad()
27
- def __call__(
28
- self,
29
- attn, # Attention module with to_q/to_k/to_v/add_*_proj, norms, to_out, to_add_out, and .heads
30
- hidden_states: torch.FloatTensor, # (B, S_img, D_model) image stream
31
- encoder_hidden_states: torch.FloatTensor = None, # (B, S_txt, D_model) text stream
32
- encoder_hidden_states_mask: torch.FloatTensor = None, # unused in FA3 path
33
- attention_mask: Optional[torch.FloatTensor] = None, # unused in FA3 path
34
- image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # (img_freqs, txt_freqs)
35
- ) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
36
- if encoder_hidden_states is None:
37
- raise ValueError("QwenDoubleStreamAttnProcessorFA3 requires encoder_hidden_states (text stream).")
38
- if attention_mask is not None:
39
- raise NotImplementedError("attention_mask is not supported in this FA3 implementation.")
40
-
41
- B, S_img, _ = hidden_states.shape
42
- S_txt = encoder_hidden_states.shape[1]
43
-
44
- # ---- QKV projections (image/sample stream) ----
45
- img_q = attn.to_q(hidden_states) # (B, S_img, D)
46
- img_k = attn.to_k(hidden_states)
47
- img_v = attn.to_v(hidden_states)
48
-
49
- # ---- QKV projections (text/context stream) ----
50
- txt_q = attn.add_q_proj(encoder_hidden_states) # (B, S_txt, D)
51
- txt_k = attn.add_k_proj(encoder_hidden_states)
52
- txt_v = attn.add_v_proj(encoder_hidden_states)
53
-
54
- # ---- Reshape to (B, S, H, D_h) ----
55
- H = attn.heads
56
- img_q = img_q.unflatten(-1, (H, -1))
57
- img_k = img_k.unflatten(-1, (H, -1))
58
- img_v = img_v.unflatten(-1, (H, -1))
59
-
60
- txt_q = txt_q.unflatten(-1, (H, -1))
61
- txt_k = txt_k.unflatten(-1, (H, -1))
62
- txt_v = txt_v.unflatten(-1, (H, -1))
63
-
64
- # ---- Q/K normalization (per your module contract) ----
65
- if getattr(attn, "norm_q", None) is not None:
66
- img_q = attn.norm_q(img_q)
67
- if getattr(attn, "norm_k", None) is not None:
68
- img_k = attn.norm_k(img_k)
69
- if getattr(attn, "norm_added_q", None) is not None:
70
- txt_q = attn.norm_added_q(txt_q)
71
- if getattr(attn, "norm_added_k", None) is not None:
72
- txt_k = attn.norm_added_k(txt_k)
73
-
74
- # ---- RoPE (Qwen variant) ----
75
- if image_rotary_emb is not None:
76
- img_freqs, txt_freqs = image_rotary_emb
77
- img_q = apply_rotary_emb_qwen(img_q, img_freqs, use_real=False)
78
- img_k = apply_rotary_emb_qwen(img_k, img_freqs, use_real=False)
79
- txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs, use_real=False)
80
- txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs, use_real=False)
81
-
82
- # ---- Joint attention over [text, image] along sequence axis ----
83
- q = torch.cat([txt_q, img_q], dim=1) # (B, S_total, H, D_h)
84
- k = torch.cat([txt_k, img_k], dim=1)
85
- v = torch.cat([txt_v, img_v], dim=1)
86
-
87
- # SDPA wants (B, H, S, D_h); route through cuDNN's FA3 path on Hopper.
88
- with sdpa_kernel(SDPBackend.CUDNN_ATTENTION):
89
- out = F.scaled_dot_product_attention(
90
- q.transpose(1, 2),
91
- k.transpose(1, 2),
92
- v.transpose(1, 2),
93
- is_causal=False,
94
- ).transpose(1, 2) # back to (B, S_total, H, D_h)
95
-
96
- # ---- Back to (B, S, D_model) ----
97
- out = out.flatten(2, 3).to(q.dtype)
98
-
99
- # Split back to text / image segments
100
- txt_attn_out = out[:, :S_txt, :]
101
- img_attn_out = out[:, S_txt:, :]
102
-
103
- # ---- Output projections ----
104
- img_attn_out = attn.to_out[0](img_attn_out)
105
- if len(attn.to_out) > 1:
106
- img_attn_out = attn.to_out[1](img_attn_out) # dropout if present
107
-
108
- txt_attn_out = attn.to_add_out(txt_attn_out)
109
-
110
- return img_attn_out, txt_attn_out
 
1
+ """
2
+ Paired with a good language model. Thanks!
3
+ """
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch.nn.attention import sdpa_kernel, SDPBackend
8
+ from typing import Optional, Tuple
9
+ from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
10
+
11
+
12
+ class QwenDoubleStreamAttnProcessorFA3:
13
+ """
14
+ FA3-grade attention processor for Qwen double-stream architecture.
15
+ Routes through PyTorch's cuDNN attention backend, which on Hopper (SM 9.0+)
16
+ dispatches to the same FlashAttention-3 family of kernels as vLLM's FA3 —
17
+ bundled with the base image so there's no external-kernel ABI risk.
18
+
19
+ Notes / limitations:
20
+ - General attention masks are not supported here. `is_causal=False` and no arbitrary mask.
21
+ - Expects an available `apply_rotary_emb_qwen` in scope (same as your non-FA3 processor).
22
+ """
23
+
24
+ _attention_backend = "fa3" # for parity with your other processors, not used internally
25
+
26
+ @torch.no_grad()
27
+ def __call__(
28
+ self,
29
+ attn, # Attention module with to_q/to_k/to_v/add_*_proj, norms, to_out, to_add_out, and .heads
30
+ hidden_states: torch.FloatTensor, # (B, S_img, D_model) image stream
31
+ encoder_hidden_states: torch.FloatTensor = None, # (B, S_txt, D_model) text stream
32
+ encoder_hidden_states_mask: torch.FloatTensor = None, # unused in FA3 path
33
+ attention_mask: Optional[torch.FloatTensor] = None, # unused in FA3 path
34
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # (img_freqs, txt_freqs)
35
+ ) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
36
+ if encoder_hidden_states is None:
37
+ raise ValueError("QwenDoubleStreamAttnProcessorFA3 requires encoder_hidden_states (text stream).")
38
+ if attention_mask is not None:
39
+ raise NotImplementedError("attention_mask is not supported in this FA3 implementation.")
40
+
41
+ B, S_img, _ = hidden_states.shape
42
+ S_txt = encoder_hidden_states.shape[1]
43
+
44
+ # ---- QKV projections (image/sample stream) ----
45
+ img_q = attn.to_q(hidden_states) # (B, S_img, D)
46
+ img_k = attn.to_k(hidden_states)
47
+ img_v = attn.to_v(hidden_states)
48
+
49
+ # ---- QKV projections (text/context stream) ----
50
+ txt_q = attn.add_q_proj(encoder_hidden_states) # (B, S_txt, D)
51
+ txt_k = attn.add_k_proj(encoder_hidden_states)
52
+ txt_v = attn.add_v_proj(encoder_hidden_states)
53
+
54
+ # ---- Reshape to (B, S, H, D_h) ----
55
+ H = attn.heads
56
+ img_q = img_q.unflatten(-1, (H, -1))
57
+ img_k = img_k.unflatten(-1, (H, -1))
58
+ img_v = img_v.unflatten(-1, (H, -1))
59
+
60
+ txt_q = txt_q.unflatten(-1, (H, -1))
61
+ txt_k = txt_k.unflatten(-1, (H, -1))
62
+ txt_v = txt_v.unflatten(-1, (H, -1))
63
+
64
+ # ---- Q/K normalization (per your module contract) ----
65
+ if getattr(attn, "norm_q", None) is not None:
66
+ img_q = attn.norm_q(img_q)
67
+ if getattr(attn, "norm_k", None) is not None:
68
+ img_k = attn.norm_k(img_k)
69
+ if getattr(attn, "norm_added_q", None) is not None:
70
+ txt_q = attn.norm_added_q(txt_q)
71
+ if getattr(attn, "norm_added_k", None) is not None:
72
+ txt_k = attn.norm_added_k(txt_k)
73
+
74
+ # ---- RoPE (Qwen variant) ----
75
+ if image_rotary_emb is not None:
76
+ img_freqs, txt_freqs = image_rotary_emb
77
+ img_q = apply_rotary_emb_qwen(img_q, img_freqs, use_real=False)
78
+ img_k = apply_rotary_emb_qwen(img_k, img_freqs, use_real=False)
79
+ txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs, use_real=False)
80
+ txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs, use_real=False)
81
+
82
+ # ---- Joint attention over [text, image] along sequence axis ----
83
+ q = torch.cat([txt_q, img_q], dim=1) # (B, S_total, H, D_h)
84
+ k = torch.cat([txt_k, img_k], dim=1)
85
+ v = torch.cat([txt_v, img_v], dim=1)
86
+
87
+ # SDPA wants (B, H, S, D_h); route through cuDNN's FA3 path on Hopper.
88
+ with sdpa_kernel(SDPBackend.CUDNN_ATTENTION):
89
+ out = F.scaled_dot_product_attention(
90
+ q.transpose(1, 2),
91
+ k.transpose(1, 2),
92
+ v.transpose(1, 2),
93
+ is_causal=False,
94
+ ).transpose(1, 2) # back to (B, S_total, H, D_h)
95
+
96
+ # ---- Back to (B, S, D_model) ----
97
+ out = out.flatten(2, 3).to(q.dtype)
98
+
99
+ # Split back to text / image segments
100
+ txt_attn_out = out[:, :S_txt, :]
101
+ img_attn_out = out[:, S_txt:, :]
102
+
103
+ # ---- Output projections ----
104
+ img_attn_out = attn.to_out[0](img_attn_out)
105
+ if len(attn.to_out) > 1:
106
+ img_attn_out = attn.to_out[1](img_attn_out) # dropout if present
107
+
108
+ txt_attn_out = attn.to_add_out(txt_attn_out)
109
+
110
+ return img_attn_out, txt_attn_out
qwenimage/transformer_qwenimage.py CHANGED
@@ -1,642 +1,642 @@
1
- # Copyright 2025 Qwen-Image Team, The HuggingFace Team. All rights reserved.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- import functools
16
- import math
17
- from typing import Any, Dict, List, Optional, Tuple, Union
18
-
19
- import torch
20
- import torch.nn as nn
21
- import torch.nn.functional as F
22
-
23
- from diffusers.configuration_utils import ConfigMixin, register_to_config
24
- from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
25
- from diffusers.utils import USE_PEFT_BACKEND, logging, scale_lora_layers, unscale_lora_layers
26
- from diffusers.utils.torch_utils import maybe_allow_in_graph
27
- from diffusers.models.attention import FeedForward, AttentionMixin
28
- from diffusers.models.attention_dispatch import dispatch_attention_fn
29
- from diffusers.models.attention_processor import Attention
30
- from diffusers.models.cache_utils import CacheMixin
31
- from diffusers.models.embeddings import TimestepEmbedding, Timesteps
32
- from diffusers.models.modeling_outputs import Transformer2DModelOutput
33
- from diffusers.models.modeling_utils import ModelMixin
34
- from diffusers.models.normalization import AdaLayerNormContinuous, RMSNorm
35
-
36
-
37
- logger = logging.get_logger(__name__) # pylint: disable=invalid-name
38
-
39
-
40
- def get_timestep_embedding(
41
- timesteps: torch.Tensor,
42
- embedding_dim: int,
43
- flip_sin_to_cos: bool = False,
44
- downscale_freq_shift: float = 1,
45
- scale: float = 1,
46
- max_period: int = 10000,
47
- ) -> torch.Tensor:
48
- """
49
- This matches the implementation in Denoising Diffusion Probabilistic Models: Create sinusoidal timestep embeddings.
50
-
51
- Args
52
- timesteps (torch.Tensor):
53
- a 1-D Tensor of N indices, one per batch element. These may be fractional.
54
- embedding_dim (int):
55
- the dimension of the output.
56
- flip_sin_to_cos (bool):
57
- Whether the embedding order should be `cos, sin` (if True) or `sin, cos` (if False)
58
- downscale_freq_shift (float):
59
- Controls the delta between frequencies between dimensions
60
- scale (float):
61
- Scaling factor applied to the embeddings.
62
- max_period (int):
63
- Controls the maximum frequency of the embeddings
64
- Returns
65
- torch.Tensor: an [N x dim] Tensor of positional embeddings.
66
- """
67
- assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
68
-
69
- half_dim = embedding_dim // 2
70
- exponent = -math.log(max_period) * torch.arange(
71
- start=0, end=half_dim, dtype=torch.float32, device=timesteps.device
72
- )
73
- exponent = exponent / (half_dim - downscale_freq_shift)
74
-
75
- emb = torch.exp(exponent).to(timesteps.dtype)
76
- emb = timesteps[:, None].float() * emb[None, :]
77
-
78
- # scale embeddings
79
- emb = scale * emb
80
-
81
- # concat sine and cosine embeddings
82
- emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
83
-
84
- # flip sine and cosine embeddings
85
- if flip_sin_to_cos:
86
- emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
87
-
88
- # zero pad
89
- if embedding_dim % 2 == 1:
90
- emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
91
- return emb
92
-
93
-
94
- def apply_rotary_emb_qwen(
95
- x: torch.Tensor,
96
- freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]],
97
- use_real: bool = True,
98
- use_real_unbind_dim: int = -1,
99
- ) -> Tuple[torch.Tensor, torch.Tensor]:
100
- """
101
- Apply rotary embeddings to input tensors using the given frequency tensor. This function applies rotary embeddings
102
- to the given query or key 'x' tensors using the provided frequency tensor 'freqs_cis'. The input tensors are
103
- reshaped as complex numbers, and the frequency tensor is reshaped for broadcasting compatibility. The resulting
104
- tensors contain rotary embeddings and are returned as real tensors.
105
-
106
- Args:
107
- x (`torch.Tensor`):
108
- Query or key tensor to apply rotary embeddings. [B, S, H, D] xk (torch.Tensor): Key tensor to apply
109
- freqs_cis (`Tuple[torch.Tensor]`): Precomputed frequency tensor for complex exponentials. ([S, D], [S, D],)
110
-
111
- Returns:
112
- Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings.
113
- """
114
- if use_real:
115
- cos, sin = freqs_cis # [S, D]
116
- cos = cos[None, None]
117
- sin = sin[None, None]
118
- cos, sin = cos.to(x.device), sin.to(x.device)
119
-
120
- if use_real_unbind_dim == -1:
121
- # Used for flux, cogvideox, hunyuan-dit
122
- x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1) # [B, S, H, D//2]
123
- x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
124
- elif use_real_unbind_dim == -2:
125
- # Used for Stable Audio, OmniGen, CogView4 and Cosmos
126
- x_real, x_imag = x.reshape(*x.shape[:-1], 2, -1).unbind(-2) # [B, S, H, D//2]
127
- x_rotated = torch.cat([-x_imag, x_real], dim=-1)
128
- else:
129
- raise ValueError(f"`use_real_unbind_dim={use_real_unbind_dim}` but should be -1 or -2.")
130
-
131
- out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
132
-
133
- return out
134
- else:
135
- x_rotated = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
136
- freqs_cis = freqs_cis.unsqueeze(1)
137
- x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(3)
138
-
139
- return x_out.type_as(x)
140
-
141
-
142
- class QwenTimestepProjEmbeddings(nn.Module):
143
- def __init__(self, embedding_dim):
144
- super().__init__()
145
-
146
- self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000)
147
- self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)
148
-
149
- def forward(self, timestep, hidden_states):
150
- timesteps_proj = self.time_proj(timestep)
151
- timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_states.dtype)) # (N, D)
152
-
153
- conditioning = timesteps_emb
154
-
155
- return conditioning
156
-
157
-
158
- class QwenEmbedRope(nn.Module):
159
- def __init__(self, theta: int, axes_dim: List[int], scale_rope=False):
160
- super().__init__()
161
- self.theta = theta
162
- self.axes_dim = axes_dim
163
- pos_index = torch.arange(4096)
164
- neg_index = torch.arange(4096).flip(0) * -1 - 1
165
- self.pos_freqs = torch.cat(
166
- [
167
- self.rope_params(pos_index, self.axes_dim[0], self.theta),
168
- self.rope_params(pos_index, self.axes_dim[1], self.theta),
169
- self.rope_params(pos_index, self.axes_dim[2], self.theta),
170
- ],
171
- dim=1,
172
- )
173
- self.neg_freqs = torch.cat(
174
- [
175
- self.rope_params(neg_index, self.axes_dim[0], self.theta),
176
- self.rope_params(neg_index, self.axes_dim[1], self.theta),
177
- self.rope_params(neg_index, self.axes_dim[2], self.theta),
178
- ],
179
- dim=1,
180
- )
181
- self.rope_cache = {}
182
-
183
- # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
184
- self.scale_rope = scale_rope
185
-
186
- def rope_params(self, index, dim, theta=10000):
187
- """
188
- Args:
189
- index: [0, 1, 2, 3] 1D Tensor representing the position index of the token
190
- """
191
- assert dim % 2 == 0
192
- freqs = torch.outer(index, 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float32).div(dim)))
193
- freqs = torch.polar(torch.ones_like(freqs), freqs)
194
- return freqs
195
-
196
- def forward(self, video_fhw, txt_seq_lens, device):
197
- """
198
- Args: video_fhw: [frame, height, width] a list of 3 integers representing the shape of the video Args:
199
- txt_length: [bs] a list of 1 integers representing the length of the text
200
- """
201
- if self.pos_freqs.device != device:
202
- self.pos_freqs = self.pos_freqs.to(device)
203
- self.neg_freqs = self.neg_freqs.to(device)
204
-
205
- if isinstance(video_fhw, list):
206
- video_fhw = video_fhw[0]
207
- if not isinstance(video_fhw, list):
208
- video_fhw = [video_fhw]
209
-
210
- vid_freqs = []
211
- max_vid_index = 0
212
- for idx, fhw in enumerate(video_fhw):
213
- frame, height, width = fhw
214
- rope_key = f"{idx}_{height}_{width}"
215
-
216
- if not torch.compiler.is_compiling():
217
- if rope_key not in self.rope_cache:
218
- self.rope_cache[rope_key] = self._compute_video_freqs(frame, height, width, idx)
219
- video_freq = self.rope_cache[rope_key]
220
- else:
221
- video_freq = self._compute_video_freqs(frame, height, width, idx)
222
- video_freq = video_freq.to(device)
223
- vid_freqs.append(video_freq)
224
-
225
- if self.scale_rope:
226
- max_vid_index = max(height // 2, width // 2, max_vid_index)
227
- else:
228
- max_vid_index = max(height, width, max_vid_index)
229
-
230
- max_len = max(txt_seq_lens)
231
- txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...]
232
- vid_freqs = torch.cat(vid_freqs, dim=0)
233
-
234
- return vid_freqs, txt_freqs
235
-
236
- @functools.lru_cache(maxsize=None)
237
- def _compute_video_freqs(self, frame, height, width, idx=0):
238
- seq_lens = frame * height * width
239
- freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
240
- freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
241
-
242
- freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1)
243
- if self.scale_rope:
244
- freqs_height = torch.cat([freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0)
245
- freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1)
246
- freqs_width = torch.cat([freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0)
247
- freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1)
248
- else:
249
- freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1)
250
- freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1)
251
-
252
- freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1)
253
- return freqs.clone().contiguous()
254
-
255
-
256
- class QwenDoubleStreamAttnProcessor2_0:
257
- """
258
- Attention processor for Qwen double-stream architecture, matching DoubleStreamLayerMegatron logic. This processor
259
- implements joint attention computation where text and image streams are processed together.
260
- """
261
-
262
- _attention_backend = None
263
-
264
- def __init__(self):
265
- if not hasattr(F, "scaled_dot_product_attention"):
266
- raise ImportError(
267
- "QwenDoubleStreamAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
268
- )
269
-
270
- def __call__(
271
- self,
272
- attn: Attention,
273
- hidden_states: torch.FloatTensor, # Image stream
274
- encoder_hidden_states: torch.FloatTensor = None, # Text stream
275
- encoder_hidden_states_mask: torch.FloatTensor = None,
276
- attention_mask: Optional[torch.FloatTensor] = None,
277
- image_rotary_emb: Optional[torch.Tensor] = None,
278
- ) -> torch.FloatTensor:
279
- if encoder_hidden_states is None:
280
- raise ValueError("QwenDoubleStreamAttnProcessor2_0 requires encoder_hidden_states (text stream)")
281
-
282
- seq_txt = encoder_hidden_states.shape[1]
283
-
284
- # Compute QKV for image stream (sample projections)
285
- img_query = attn.to_q(hidden_states)
286
- img_key = attn.to_k(hidden_states)
287
- img_value = attn.to_v(hidden_states)
288
-
289
- # Compute QKV for text stream (context projections)
290
- txt_query = attn.add_q_proj(encoder_hidden_states)
291
- txt_key = attn.add_k_proj(encoder_hidden_states)
292
- txt_value = attn.add_v_proj(encoder_hidden_states)
293
-
294
- # Reshape for multi-head attention
295
- img_query = img_query.unflatten(-1, (attn.heads, -1))
296
- img_key = img_key.unflatten(-1, (attn.heads, -1))
297
- img_value = img_value.unflatten(-1, (attn.heads, -1))
298
-
299
- txt_query = txt_query.unflatten(-1, (attn.heads, -1))
300
- txt_key = txt_key.unflatten(-1, (attn.heads, -1))
301
- txt_value = txt_value.unflatten(-1, (attn.heads, -1))
302
-
303
- # Apply QK normalization
304
- if attn.norm_q is not None:
305
- img_query = attn.norm_q(img_query)
306
- if attn.norm_k is not None:
307
- img_key = attn.norm_k(img_key)
308
- if attn.norm_added_q is not None:
309
- txt_query = attn.norm_added_q(txt_query)
310
- if attn.norm_added_k is not None:
311
- txt_key = attn.norm_added_k(txt_key)
312
-
313
- # Apply RoPE
314
- if image_rotary_emb is not None:
315
- img_freqs, txt_freqs = image_rotary_emb
316
- img_query = apply_rotary_emb_qwen(img_query, img_freqs, use_real=False)
317
- img_key = apply_rotary_emb_qwen(img_key, img_freqs, use_real=False)
318
- txt_query = apply_rotary_emb_qwen(txt_query, txt_freqs, use_real=False)
319
- txt_key = apply_rotary_emb_qwen(txt_key, txt_freqs, use_real=False)
320
-
321
- # Concatenate for joint attention
322
- # Order: [text, image]
323
- joint_query = torch.cat([txt_query, img_query], dim=1)
324
- joint_key = torch.cat([txt_key, img_key], dim=1)
325
- joint_value = torch.cat([txt_value, img_value], dim=1)
326
-
327
- # Compute joint attention
328
- joint_hidden_states = dispatch_attention_fn(
329
- joint_query,
330
- joint_key,
331
- joint_value,
332
- attn_mask=attention_mask,
333
- dropout_p=0.0,
334
- is_causal=False,
335
- backend=self._attention_backend,
336
- )
337
-
338
- # Reshape back
339
- joint_hidden_states = joint_hidden_states.flatten(2, 3)
340
- joint_hidden_states = joint_hidden_states.to(joint_query.dtype)
341
-
342
- # Split attention outputs back
343
- txt_attn_output = joint_hidden_states[:, :seq_txt, :] # Text part
344
- img_attn_output = joint_hidden_states[:, seq_txt:, :] # Image part
345
-
346
- # Apply output projections
347
- img_attn_output = attn.to_out[0](img_attn_output)
348
- if len(attn.to_out) > 1:
349
- img_attn_output = attn.to_out[1](img_attn_output) # dropout
350
-
351
- txt_attn_output = attn.to_add_out(txt_attn_output)
352
-
353
- return img_attn_output, txt_attn_output
354
-
355
-
356
- @maybe_allow_in_graph
357
- class QwenImageTransformerBlock(nn.Module):
358
- def __init__(
359
- self, dim: int, num_attention_heads: int, attention_head_dim: int, qk_norm: str = "rms_norm", eps: float = 1e-6
360
- ):
361
- super().__init__()
362
-
363
- self.dim = dim
364
- self.num_attention_heads = num_attention_heads
365
- self.attention_head_dim = attention_head_dim
366
-
367
- # Image processing modules
368
- self.img_mod = nn.Sequential(
369
- nn.SiLU(),
370
- nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
371
- )
372
- self.img_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
373
- self.attn = Attention(
374
- query_dim=dim,
375
- cross_attention_dim=None, # Enable cross attention for joint computation
376
- added_kv_proj_dim=dim, # Enable added KV projections for text stream
377
- dim_head=attention_head_dim,
378
- heads=num_attention_heads,
379
- out_dim=dim,
380
- context_pre_only=False,
381
- bias=True,
382
- processor=QwenDoubleStreamAttnProcessor2_0(),
383
- qk_norm=qk_norm,
384
- eps=eps,
385
- )
386
- self.img_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
387
- self.img_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
388
-
389
- # Text processing modules
390
- self.txt_mod = nn.Sequential(
391
- nn.SiLU(),
392
- nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
393
- )
394
- self.txt_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
395
- # Text doesn't need separate attention - it's handled by img_attn joint computation
396
- self.txt_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
397
- self.txt_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
398
-
399
- def _modulate(self, x, mod_params):
400
- """Apply modulation to input tensor"""
401
- shift, scale, gate = mod_params.chunk(3, dim=-1)
402
- return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1)
403
-
404
- def forward(
405
- self,
406
- hidden_states: torch.Tensor,
407
- encoder_hidden_states: torch.Tensor,
408
- encoder_hidden_states_mask: torch.Tensor,
409
- temb: torch.Tensor,
410
- image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
411
- joint_attention_kwargs: Optional[Dict[str, Any]] = None,
412
- ) -> Tuple[torch.Tensor, torch.Tensor]:
413
- # Get modulation parameters for both streams
414
- img_mod_params = self.img_mod(temb) # [B, 6*dim]
415
- txt_mod_params = self.txt_mod(temb) # [B, 6*dim]
416
-
417
- # Split modulation parameters for norm1 and norm2
418
- img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
419
- txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
420
-
421
- # Process image stream - norm1 + modulation
422
- img_normed = self.img_norm1(hidden_states)
423
- img_modulated, img_gate1 = self._modulate(img_normed, img_mod1)
424
-
425
- # Process text stream - norm1 + modulation
426
- txt_normed = self.txt_norm1(encoder_hidden_states)
427
- txt_modulated, txt_gate1 = self._modulate(txt_normed, txt_mod1)
428
-
429
- # Use QwenAttnProcessor2_0 for joint attention computation
430
- # This directly implements the DoubleStreamLayerMegatron logic:
431
- # 1. Computes QKV for both streams
432
- # 2. Applies QK normalization and RoPE
433
- # 3. Concatenates and runs joint attention
434
- # 4. Splits results back to separate streams
435
- joint_attention_kwargs = joint_attention_kwargs or {}
436
- attn_output = self.attn(
437
- hidden_states=img_modulated, # Image stream (will be processed as "sample")
438
- encoder_hidden_states=txt_modulated, # Text stream (will be processed as "context")
439
- encoder_hidden_states_mask=encoder_hidden_states_mask,
440
- image_rotary_emb=image_rotary_emb,
441
- **joint_attention_kwargs,
442
- )
443
-
444
- # QwenAttnProcessor2_0 returns (img_output, txt_output) when encoder_hidden_states is provided
445
- img_attn_output, txt_attn_output = attn_output
446
-
447
- # Apply attention gates and add residual (like in Megatron)
448
- hidden_states = hidden_states + img_gate1 * img_attn_output
449
- encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn_output
450
-
451
- # Process image stream - norm2 + MLP
452
- img_normed2 = self.img_norm2(hidden_states)
453
- img_modulated2, img_gate2 = self._modulate(img_normed2, img_mod2)
454
- img_mlp_output = self.img_mlp(img_modulated2)
455
- hidden_states = hidden_states + img_gate2 * img_mlp_output
456
-
457
- # Process text stream - norm2 + MLP
458
- txt_normed2 = self.txt_norm2(encoder_hidden_states)
459
- txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2)
460
- txt_mlp_output = self.txt_mlp(txt_modulated2)
461
- encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output
462
-
463
- # Clip to prevent overflow for fp16
464
- if encoder_hidden_states.dtype == torch.float16:
465
- encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
466
- if hidden_states.dtype == torch.float16:
467
- hidden_states = hidden_states.clip(-65504, 65504)
468
-
469
- return encoder_hidden_states, hidden_states
470
-
471
-
472
- class QwenImageTransformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, CacheMixin, AttentionMixin):
473
- """
474
- The Transformer model introduced in Qwen.
475
-
476
- Args:
477
- patch_size (`int`, defaults to `2`):
478
- Patch size to turn the input data into small patches.
479
- in_channels (`int`, defaults to `64`):
480
- The number of channels in the input.
481
- out_channels (`int`, *optional*, defaults to `None`):
482
- The number of channels in the output. If not specified, it defaults to `in_channels`.
483
- num_layers (`int`, defaults to `60`):
484
- The number of layers of dual stream DiT blocks to use.
485
- attention_head_dim (`int`, defaults to `128`):
486
- The number of dimensions to use for each attention head.
487
- num_attention_heads (`int`, defaults to `24`):
488
- The number of attention heads to use.
489
- joint_attention_dim (`int`, defaults to `3584`):
490
- The number of dimensions to use for the joint attention (embedding/channel dimension of
491
- `encoder_hidden_states`).
492
- guidance_embeds (`bool`, defaults to `False`):
493
- Whether to use guidance embeddings for guidance-distilled variant of the model.
494
- axes_dims_rope (`Tuple[int]`, defaults to `(16, 56, 56)`):
495
- The dimensions to use for the rotary positional embeddings.
496
- """
497
-
498
- _supports_gradient_checkpointing = True
499
- _no_split_modules = ["QwenImageTransformerBlock"]
500
- _skip_layerwise_casting_patterns = ["pos_embed", "norm"]
501
- _repeated_blocks = ["QwenImageTransformerBlock"]
502
-
503
- @register_to_config
504
- def __init__(
505
- self,
506
- patch_size: int = 2,
507
- in_channels: int = 64,
508
- out_channels: Optional[int] = 16,
509
- num_layers: int = 60,
510
- attention_head_dim: int = 128,
511
- num_attention_heads: int = 24,
512
- joint_attention_dim: int = 3584,
513
- guidance_embeds: bool = False, # TODO: this should probably be removed
514
- axes_dims_rope: Tuple[int, int, int] = (16, 56, 56),
515
- ):
516
- super().__init__()
517
- self.out_channels = out_channels or in_channels
518
- self.inner_dim = num_attention_heads * attention_head_dim
519
-
520
- self.pos_embed = QwenEmbedRope(theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True)
521
-
522
- self.time_text_embed = QwenTimestepProjEmbeddings(embedding_dim=self.inner_dim)
523
-
524
- self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
525
-
526
- self.img_in = nn.Linear(in_channels, self.inner_dim)
527
- self.txt_in = nn.Linear(joint_attention_dim, self.inner_dim)
528
-
529
- self.transformer_blocks = nn.ModuleList(
530
- [
531
- QwenImageTransformerBlock(
532
- dim=self.inner_dim,
533
- num_attention_heads=num_attention_heads,
534
- attention_head_dim=attention_head_dim,
535
- )
536
- for _ in range(num_layers)
537
- ]
538
- )
539
-
540
- self.norm_out = AdaLayerNormContinuous(self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6)
541
- self.proj_out = nn.Linear(self.inner_dim, patch_size * patch_size * self.out_channels, bias=True)
542
-
543
- self.gradient_checkpointing = False
544
-
545
- def forward(
546
- self,
547
- hidden_states: torch.Tensor,
548
- encoder_hidden_states: torch.Tensor = None,
549
- encoder_hidden_states_mask: torch.Tensor = None,
550
- timestep: torch.LongTensor = None,
551
- image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
552
- guidance: torch.Tensor = None, # TODO: this should probably be removed
553
- attention_kwargs: Optional[Dict[str, Any]] = None,
554
- return_dict: bool = True,
555
- ) -> Union[torch.Tensor, Transformer2DModelOutput]:
556
- """
557
- The [`QwenTransformer2DModel`] forward method.
558
-
559
- Args:
560
- hidden_states (`torch.Tensor` of shape `(batch_size, image_sequence_length, in_channels)`):
561
- Input `hidden_states`.
562
- encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_sequence_length, joint_attention_dim)`):
563
- Conditional embeddings (embeddings computed from the input conditions such as prompts) to use.
564
- encoder_hidden_states_mask (`torch.Tensor` of shape `(batch_size, text_sequence_length)`):
565
- Mask of the input conditions.
566
- timestep ( `torch.LongTensor`):
567
- Used to indicate denoising step.
568
- attention_kwargs (`dict`, *optional*):
569
- A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
570
- `self.processor` in
571
- [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
572
- return_dict (`bool`, *optional*, defaults to `True`):
573
- Whether or not to return a [`~models.transformer_2d.Transformer2DModelOutput`] instead of a plain
574
- tuple.
575
-
576
- Returns:
577
- If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a
578
- `tuple` where the first element is the sample tensor.
579
- """
580
- if attention_kwargs is not None:
581
- attention_kwargs = attention_kwargs.copy()
582
- lora_scale = attention_kwargs.pop("scale", 1.0)
583
- else:
584
- lora_scale = 1.0
585
-
586
- if USE_PEFT_BACKEND:
587
- # weight the lora layers by setting `lora_scale` for each PEFT layer
588
- scale_lora_layers(self, lora_scale)
589
- else:
590
- if attention_kwargs is not None and attention_kwargs.get("scale", None) is not None:
591
- logger.warning(
592
- "Passing `scale` via `joint_attention_kwargs` when not using the PEFT backend is ineffective."
593
- )
594
-
595
- hidden_states = self.img_in(hidden_states)
596
-
597
- timestep = timestep.to(hidden_states.dtype)
598
- encoder_hidden_states = self.txt_norm(encoder_hidden_states)
599
- encoder_hidden_states = self.txt_in(encoder_hidden_states)
600
-
601
- if guidance is not None:
602
- guidance = guidance.to(hidden_states.dtype) * 1000
603
-
604
- temb = (
605
- self.time_text_embed(timestep, hidden_states)
606
- if guidance is None
607
- else self.time_text_embed(timestep, guidance, hidden_states)
608
- )
609
-
610
- for index_block, block in enumerate(self.transformer_blocks):
611
- if torch.is_grad_enabled() and self.gradient_checkpointing:
612
- encoder_hidden_states, hidden_states = self._gradient_checkpointing_func(
613
- block,
614
- hidden_states,
615
- encoder_hidden_states,
616
- encoder_hidden_states_mask,
617
- temb,
618
- image_rotary_emb,
619
- )
620
-
621
- else:
622
- encoder_hidden_states, hidden_states = block(
623
- hidden_states=hidden_states,
624
- encoder_hidden_states=encoder_hidden_states,
625
- encoder_hidden_states_mask=encoder_hidden_states_mask,
626
- temb=temb,
627
- image_rotary_emb=image_rotary_emb,
628
- joint_attention_kwargs=attention_kwargs,
629
- )
630
-
631
- # Use only the image part (hidden_states) from the dual-stream blocks
632
- hidden_states = self.norm_out(hidden_states, temb)
633
- output = self.proj_out(hidden_states)
634
-
635
- if USE_PEFT_BACKEND:
636
- # remove `lora_scale` from each PEFT layer
637
- unscale_lora_layers(self, lora_scale)
638
-
639
- if not return_dict:
640
- return (output,)
641
-
642
- return Transformer2DModelOutput(sample=output)
 
1
+ # Copyright 2025 Qwen-Image Team, The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import functools
16
+ import math
17
+ from typing import Any, Dict, List, Optional, Tuple, Union
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
24
+ from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
25
+ from diffusers.utils import USE_PEFT_BACKEND, logging, scale_lora_layers, unscale_lora_layers
26
+ from diffusers.utils.torch_utils import maybe_allow_in_graph
27
+ from diffusers.models.attention import FeedForward, AttentionMixin
28
+ from diffusers.models.attention_dispatch import dispatch_attention_fn
29
+ from diffusers.models.attention_processor import Attention
30
+ from diffusers.models.cache_utils import CacheMixin
31
+ from diffusers.models.embeddings import TimestepEmbedding, Timesteps
32
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
33
+ from diffusers.models.modeling_utils import ModelMixin
34
+ from diffusers.models.normalization import AdaLayerNormContinuous, RMSNorm
35
+
36
+
37
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
38
+
39
+
40
+ def get_timestep_embedding(
41
+ timesteps: torch.Tensor,
42
+ embedding_dim: int,
43
+ flip_sin_to_cos: bool = False,
44
+ downscale_freq_shift: float = 1,
45
+ scale: float = 1,
46
+ max_period: int = 10000,
47
+ ) -> torch.Tensor:
48
+ """
49
+ This matches the implementation in Denoising Diffusion Probabilistic Models: Create sinusoidal timestep embeddings.
50
+
51
+ Args
52
+ timesteps (torch.Tensor):
53
+ a 1-D Tensor of N indices, one per batch element. These may be fractional.
54
+ embedding_dim (int):
55
+ the dimension of the output.
56
+ flip_sin_to_cos (bool):
57
+ Whether the embedding order should be `cos, sin` (if True) or `sin, cos` (if False)
58
+ downscale_freq_shift (float):
59
+ Controls the delta between frequencies between dimensions
60
+ scale (float):
61
+ Scaling factor applied to the embeddings.
62
+ max_period (int):
63
+ Controls the maximum frequency of the embeddings
64
+ Returns
65
+ torch.Tensor: an [N x dim] Tensor of positional embeddings.
66
+ """
67
+ assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
68
+
69
+ half_dim = embedding_dim // 2
70
+ exponent = -math.log(max_period) * torch.arange(
71
+ start=0, end=half_dim, dtype=torch.float32, device=timesteps.device
72
+ )
73
+ exponent = exponent / (half_dim - downscale_freq_shift)
74
+
75
+ emb = torch.exp(exponent).to(timesteps.dtype)
76
+ emb = timesteps[:, None].float() * emb[None, :]
77
+
78
+ # scale embeddings
79
+ emb = scale * emb
80
+
81
+ # concat sine and cosine embeddings
82
+ emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
83
+
84
+ # flip sine and cosine embeddings
85
+ if flip_sin_to_cos:
86
+ emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
87
+
88
+ # zero pad
89
+ if embedding_dim % 2 == 1:
90
+ emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
91
+ return emb
92
+
93
+
94
+ def apply_rotary_emb_qwen(
95
+ x: torch.Tensor,
96
+ freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]],
97
+ use_real: bool = True,
98
+ use_real_unbind_dim: int = -1,
99
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
100
+ """
101
+ Apply rotary embeddings to input tensors using the given frequency tensor. This function applies rotary embeddings
102
+ to the given query or key 'x' tensors using the provided frequency tensor 'freqs_cis'. The input tensors are
103
+ reshaped as complex numbers, and the frequency tensor is reshaped for broadcasting compatibility. The resulting
104
+ tensors contain rotary embeddings and are returned as real tensors.
105
+
106
+ Args:
107
+ x (`torch.Tensor`):
108
+ Query or key tensor to apply rotary embeddings. [B, S, H, D] xk (torch.Tensor): Key tensor to apply
109
+ freqs_cis (`Tuple[torch.Tensor]`): Precomputed frequency tensor for complex exponentials. ([S, D], [S, D],)
110
+
111
+ Returns:
112
+ Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings.
113
+ """
114
+ if use_real:
115
+ cos, sin = freqs_cis # [S, D]
116
+ cos = cos[None, None]
117
+ sin = sin[None, None]
118
+ cos, sin = cos.to(x.device), sin.to(x.device)
119
+
120
+ if use_real_unbind_dim == -1:
121
+ # Used for flux, cogvideox, hunyuan-dit
122
+ x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1) # [B, S, H, D//2]
123
+ x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
124
+ elif use_real_unbind_dim == -2:
125
+ # Used for Stable Audio, OmniGen, CogView4 and Cosmos
126
+ x_real, x_imag = x.reshape(*x.shape[:-1], 2, -1).unbind(-2) # [B, S, H, D//2]
127
+ x_rotated = torch.cat([-x_imag, x_real], dim=-1)
128
+ else:
129
+ raise ValueError(f"`use_real_unbind_dim={use_real_unbind_dim}` but should be -1 or -2.")
130
+
131
+ out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
132
+
133
+ return out
134
+ else:
135
+ x_rotated = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
136
+ freqs_cis = freqs_cis.unsqueeze(1)
137
+ x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(3)
138
+
139
+ return x_out.type_as(x)
140
+
141
+
142
+ class QwenTimestepProjEmbeddings(nn.Module):
143
+ def __init__(self, embedding_dim):
144
+ super().__init__()
145
+
146
+ self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000)
147
+ self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)
148
+
149
+ def forward(self, timestep, hidden_states):
150
+ timesteps_proj = self.time_proj(timestep)
151
+ timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_states.dtype)) # (N, D)
152
+
153
+ conditioning = timesteps_emb
154
+
155
+ return conditioning
156
+
157
+
158
+ class QwenEmbedRope(nn.Module):
159
+ def __init__(self, theta: int, axes_dim: List[int], scale_rope=False):
160
+ super().__init__()
161
+ self.theta = theta
162
+ self.axes_dim = axes_dim
163
+ pos_index = torch.arange(4096)
164
+ neg_index = torch.arange(4096).flip(0) * -1 - 1
165
+ self.pos_freqs = torch.cat(
166
+ [
167
+ self.rope_params(pos_index, self.axes_dim[0], self.theta),
168
+ self.rope_params(pos_index, self.axes_dim[1], self.theta),
169
+ self.rope_params(pos_index, self.axes_dim[2], self.theta),
170
+ ],
171
+ dim=1,
172
+ )
173
+ self.neg_freqs = torch.cat(
174
+ [
175
+ self.rope_params(neg_index, self.axes_dim[0], self.theta),
176
+ self.rope_params(neg_index, self.axes_dim[1], self.theta),
177
+ self.rope_params(neg_index, self.axes_dim[2], self.theta),
178
+ ],
179
+ dim=1,
180
+ )
181
+ self.rope_cache = {}
182
+
183
+ # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
184
+ self.scale_rope = scale_rope
185
+
186
+ def rope_params(self, index, dim, theta=10000):
187
+ """
188
+ Args:
189
+ index: [0, 1, 2, 3] 1D Tensor representing the position index of the token
190
+ """
191
+ assert dim % 2 == 0
192
+ freqs = torch.outer(index, 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float32).div(dim)))
193
+ freqs = torch.polar(torch.ones_like(freqs), freqs)
194
+ return freqs
195
+
196
+ def forward(self, video_fhw, txt_seq_lens, device):
197
+ """
198
+ Args: video_fhw: [frame, height, width] a list of 3 integers representing the shape of the video Args:
199
+ txt_length: [bs] a list of 1 integers representing the length of the text
200
+ """
201
+ if self.pos_freqs.device != device:
202
+ self.pos_freqs = self.pos_freqs.to(device)
203
+ self.neg_freqs = self.neg_freqs.to(device)
204
+
205
+ if isinstance(video_fhw, list):
206
+ video_fhw = video_fhw[0]
207
+ if not isinstance(video_fhw, list):
208
+ video_fhw = [video_fhw]
209
+
210
+ vid_freqs = []
211
+ max_vid_index = 0
212
+ for idx, fhw in enumerate(video_fhw):
213
+ frame, height, width = fhw
214
+ rope_key = f"{idx}_{height}_{width}"
215
+
216
+ if not torch.compiler.is_compiling():
217
+ if rope_key not in self.rope_cache:
218
+ self.rope_cache[rope_key] = self._compute_video_freqs(frame, height, width, idx)
219
+ video_freq = self.rope_cache[rope_key]
220
+ else:
221
+ video_freq = self._compute_video_freqs(frame, height, width, idx)
222
+ video_freq = video_freq.to(device)
223
+ vid_freqs.append(video_freq)
224
+
225
+ if self.scale_rope:
226
+ max_vid_index = max(height // 2, width // 2, max_vid_index)
227
+ else:
228
+ max_vid_index = max(height, width, max_vid_index)
229
+
230
+ max_len = max(txt_seq_lens)
231
+ txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...]
232
+ vid_freqs = torch.cat(vid_freqs, dim=0)
233
+
234
+ return vid_freqs, txt_freqs
235
+
236
+ @functools.lru_cache(maxsize=None)
237
+ def _compute_video_freqs(self, frame, height, width, idx=0):
238
+ seq_lens = frame * height * width
239
+ freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
240
+ freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
241
+
242
+ freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1)
243
+ if self.scale_rope:
244
+ freqs_height = torch.cat([freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0)
245
+ freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1)
246
+ freqs_width = torch.cat([freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0)
247
+ freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1)
248
+ else:
249
+ freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1)
250
+ freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1)
251
+
252
+ freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1)
253
+ return freqs.clone().contiguous()
254
+
255
+
256
+ class QwenDoubleStreamAttnProcessor2_0:
257
+ """
258
+ Attention processor for Qwen double-stream architecture, matching DoubleStreamLayerMegatron logic. This processor
259
+ implements joint attention computation where text and image streams are processed together.
260
+ """
261
+
262
+ _attention_backend = None
263
+
264
+ def __init__(self):
265
+ if not hasattr(F, "scaled_dot_product_attention"):
266
+ raise ImportError(
267
+ "QwenDoubleStreamAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
268
+ )
269
+
270
+ def __call__(
271
+ self,
272
+ attn: Attention,
273
+ hidden_states: torch.FloatTensor, # Image stream
274
+ encoder_hidden_states: torch.FloatTensor = None, # Text stream
275
+ encoder_hidden_states_mask: torch.FloatTensor = None,
276
+ attention_mask: Optional[torch.FloatTensor] = None,
277
+ image_rotary_emb: Optional[torch.Tensor] = None,
278
+ ) -> torch.FloatTensor:
279
+ if encoder_hidden_states is None:
280
+ raise ValueError("QwenDoubleStreamAttnProcessor2_0 requires encoder_hidden_states (text stream)")
281
+
282
+ seq_txt = encoder_hidden_states.shape[1]
283
+
284
+ # Compute QKV for image stream (sample projections)
285
+ img_query = attn.to_q(hidden_states)
286
+ img_key = attn.to_k(hidden_states)
287
+ img_value = attn.to_v(hidden_states)
288
+
289
+ # Compute QKV for text stream (context projections)
290
+ txt_query = attn.add_q_proj(encoder_hidden_states)
291
+ txt_key = attn.add_k_proj(encoder_hidden_states)
292
+ txt_value = attn.add_v_proj(encoder_hidden_states)
293
+
294
+ # Reshape for multi-head attention
295
+ img_query = img_query.unflatten(-1, (attn.heads, -1))
296
+ img_key = img_key.unflatten(-1, (attn.heads, -1))
297
+ img_value = img_value.unflatten(-1, (attn.heads, -1))
298
+
299
+ txt_query = txt_query.unflatten(-1, (attn.heads, -1))
300
+ txt_key = txt_key.unflatten(-1, (attn.heads, -1))
301
+ txt_value = txt_value.unflatten(-1, (attn.heads, -1))
302
+
303
+ # Apply QK normalization
304
+ if attn.norm_q is not None:
305
+ img_query = attn.norm_q(img_query)
306
+ if attn.norm_k is not None:
307
+ img_key = attn.norm_k(img_key)
308
+ if attn.norm_added_q is not None:
309
+ txt_query = attn.norm_added_q(txt_query)
310
+ if attn.norm_added_k is not None:
311
+ txt_key = attn.norm_added_k(txt_key)
312
+
313
+ # Apply RoPE
314
+ if image_rotary_emb is not None:
315
+ img_freqs, txt_freqs = image_rotary_emb
316
+ img_query = apply_rotary_emb_qwen(img_query, img_freqs, use_real=False)
317
+ img_key = apply_rotary_emb_qwen(img_key, img_freqs, use_real=False)
318
+ txt_query = apply_rotary_emb_qwen(txt_query, txt_freqs, use_real=False)
319
+ txt_key = apply_rotary_emb_qwen(txt_key, txt_freqs, use_real=False)
320
+
321
+ # Concatenate for joint attention
322
+ # Order: [text, image]
323
+ joint_query = torch.cat([txt_query, img_query], dim=1)
324
+ joint_key = torch.cat([txt_key, img_key], dim=1)
325
+ joint_value = torch.cat([txt_value, img_value], dim=1)
326
+
327
+ # Compute joint attention
328
+ joint_hidden_states = dispatch_attention_fn(
329
+ joint_query,
330
+ joint_key,
331
+ joint_value,
332
+ attn_mask=attention_mask,
333
+ dropout_p=0.0,
334
+ is_causal=False,
335
+ backend=self._attention_backend,
336
+ )
337
+
338
+ # Reshape back
339
+ joint_hidden_states = joint_hidden_states.flatten(2, 3)
340
+ joint_hidden_states = joint_hidden_states.to(joint_query.dtype)
341
+
342
+ # Split attention outputs back
343
+ txt_attn_output = joint_hidden_states[:, :seq_txt, :] # Text part
344
+ img_attn_output = joint_hidden_states[:, seq_txt:, :] # Image part
345
+
346
+ # Apply output projections
347
+ img_attn_output = attn.to_out[0](img_attn_output)
348
+ if len(attn.to_out) > 1:
349
+ img_attn_output = attn.to_out[1](img_attn_output) # dropout
350
+
351
+ txt_attn_output = attn.to_add_out(txt_attn_output)
352
+
353
+ return img_attn_output, txt_attn_output
354
+
355
+
356
+ @maybe_allow_in_graph
357
+ class QwenImageTransformerBlock(nn.Module):
358
+ def __init__(
359
+ self, dim: int, num_attention_heads: int, attention_head_dim: int, qk_norm: str = "rms_norm", eps: float = 1e-6
360
+ ):
361
+ super().__init__()
362
+
363
+ self.dim = dim
364
+ self.num_attention_heads = num_attention_heads
365
+ self.attention_head_dim = attention_head_dim
366
+
367
+ # Image processing modules
368
+ self.img_mod = nn.Sequential(
369
+ nn.SiLU(),
370
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
371
+ )
372
+ self.img_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
373
+ self.attn = Attention(
374
+ query_dim=dim,
375
+ cross_attention_dim=None, # Enable cross attention for joint computation
376
+ added_kv_proj_dim=dim, # Enable added KV projections for text stream
377
+ dim_head=attention_head_dim,
378
+ heads=num_attention_heads,
379
+ out_dim=dim,
380
+ context_pre_only=False,
381
+ bias=True,
382
+ processor=QwenDoubleStreamAttnProcessor2_0(),
383
+ qk_norm=qk_norm,
384
+ eps=eps,
385
+ )
386
+ self.img_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
387
+ self.img_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
388
+
389
+ # Text processing modules
390
+ self.txt_mod = nn.Sequential(
391
+ nn.SiLU(),
392
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
393
+ )
394
+ self.txt_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
395
+ # Text doesn't need separate attention - it's handled by img_attn joint computation
396
+ self.txt_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
397
+ self.txt_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
398
+
399
+ def _modulate(self, x, mod_params):
400
+ """Apply modulation to input tensor"""
401
+ shift, scale, gate = mod_params.chunk(3, dim=-1)
402
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1)
403
+
404
+ def forward(
405
+ self,
406
+ hidden_states: torch.Tensor,
407
+ encoder_hidden_states: torch.Tensor,
408
+ encoder_hidden_states_mask: torch.Tensor,
409
+ temb: torch.Tensor,
410
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
411
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
412
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
413
+ # Get modulation parameters for both streams
414
+ img_mod_params = self.img_mod(temb) # [B, 6*dim]
415
+ txt_mod_params = self.txt_mod(temb) # [B, 6*dim]
416
+
417
+ # Split modulation parameters for norm1 and norm2
418
+ img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
419
+ txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
420
+
421
+ # Process image stream - norm1 + modulation
422
+ img_normed = self.img_norm1(hidden_states)
423
+ img_modulated, img_gate1 = self._modulate(img_normed, img_mod1)
424
+
425
+ # Process text stream - norm1 + modulation
426
+ txt_normed = self.txt_norm1(encoder_hidden_states)
427
+ txt_modulated, txt_gate1 = self._modulate(txt_normed, txt_mod1)
428
+
429
+ # Use QwenAttnProcessor2_0 for joint attention computation
430
+ # This directly implements the DoubleStreamLayerMegatron logic:
431
+ # 1. Computes QKV for both streams
432
+ # 2. Applies QK normalization and RoPE
433
+ # 3. Concatenates and runs joint attention
434
+ # 4. Splits results back to separate streams
435
+ joint_attention_kwargs = joint_attention_kwargs or {}
436
+ attn_output = self.attn(
437
+ hidden_states=img_modulated, # Image stream (will be processed as "sample")
438
+ encoder_hidden_states=txt_modulated, # Text stream (will be processed as "context")
439
+ encoder_hidden_states_mask=encoder_hidden_states_mask,
440
+ image_rotary_emb=image_rotary_emb,
441
+ **joint_attention_kwargs,
442
+ )
443
+
444
+ # QwenAttnProcessor2_0 returns (img_output, txt_output) when encoder_hidden_states is provided
445
+ img_attn_output, txt_attn_output = attn_output
446
+
447
+ # Apply attention gates and add residual (like in Megatron)
448
+ hidden_states = hidden_states + img_gate1 * img_attn_output
449
+ encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn_output
450
+
451
+ # Process image stream - norm2 + MLP
452
+ img_normed2 = self.img_norm2(hidden_states)
453
+ img_modulated2, img_gate2 = self._modulate(img_normed2, img_mod2)
454
+ img_mlp_output = self.img_mlp(img_modulated2)
455
+ hidden_states = hidden_states + img_gate2 * img_mlp_output
456
+
457
+ # Process text stream - norm2 + MLP
458
+ txt_normed2 = self.txt_norm2(encoder_hidden_states)
459
+ txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2)
460
+ txt_mlp_output = self.txt_mlp(txt_modulated2)
461
+ encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output
462
+
463
+ # Clip to prevent overflow for fp16
464
+ if encoder_hidden_states.dtype == torch.float16:
465
+ encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
466
+ if hidden_states.dtype == torch.float16:
467
+ hidden_states = hidden_states.clip(-65504, 65504)
468
+
469
+ return encoder_hidden_states, hidden_states
470
+
471
+
472
+ class QwenImageTransformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, CacheMixin, AttentionMixin):
473
+ """
474
+ The Transformer model introduced in Qwen.
475
+
476
+ Args:
477
+ patch_size (`int`, defaults to `2`):
478
+ Patch size to turn the input data into small patches.
479
+ in_channels (`int`, defaults to `64`):
480
+ The number of channels in the input.
481
+ out_channels (`int`, *optional*, defaults to `None`):
482
+ The number of channels in the output. If not specified, it defaults to `in_channels`.
483
+ num_layers (`int`, defaults to `60`):
484
+ The number of layers of dual stream DiT blocks to use.
485
+ attention_head_dim (`int`, defaults to `128`):
486
+ The number of dimensions to use for each attention head.
487
+ num_attention_heads (`int`, defaults to `24`):
488
+ The number of attention heads to use.
489
+ joint_attention_dim (`int`, defaults to `3584`):
490
+ The number of dimensions to use for the joint attention (embedding/channel dimension of
491
+ `encoder_hidden_states`).
492
+ guidance_embeds (`bool`, defaults to `False`):
493
+ Whether to use guidance embeddings for guidance-distilled variant of the model.
494
+ axes_dims_rope (`Tuple[int]`, defaults to `(16, 56, 56)`):
495
+ The dimensions to use for the rotary positional embeddings.
496
+ """
497
+
498
+ _supports_gradient_checkpointing = True
499
+ _no_split_modules = ["QwenImageTransformerBlock"]
500
+ _skip_layerwise_casting_patterns = ["pos_embed", "norm"]
501
+ _repeated_blocks = ["QwenImageTransformerBlock"]
502
+
503
+ @register_to_config
504
+ def __init__(
505
+ self,
506
+ patch_size: int = 2,
507
+ in_channels: int = 64,
508
+ out_channels: Optional[int] = 16,
509
+ num_layers: int = 60,
510
+ attention_head_dim: int = 128,
511
+ num_attention_heads: int = 24,
512
+ joint_attention_dim: int = 3584,
513
+ guidance_embeds: bool = False, # TODO: this should probably be removed
514
+ axes_dims_rope: Tuple[int, int, int] = (16, 56, 56),
515
+ ):
516
+ super().__init__()
517
+ self.out_channels = out_channels or in_channels
518
+ self.inner_dim = num_attention_heads * attention_head_dim
519
+
520
+ self.pos_embed = QwenEmbedRope(theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True)
521
+
522
+ self.time_text_embed = QwenTimestepProjEmbeddings(embedding_dim=self.inner_dim)
523
+
524
+ self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
525
+
526
+ self.img_in = nn.Linear(in_channels, self.inner_dim)
527
+ self.txt_in = nn.Linear(joint_attention_dim, self.inner_dim)
528
+
529
+ self.transformer_blocks = nn.ModuleList(
530
+ [
531
+ QwenImageTransformerBlock(
532
+ dim=self.inner_dim,
533
+ num_attention_heads=num_attention_heads,
534
+ attention_head_dim=attention_head_dim,
535
+ )
536
+ for _ in range(num_layers)
537
+ ]
538
+ )
539
+
540
+ self.norm_out = AdaLayerNormContinuous(self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6)
541
+ self.proj_out = nn.Linear(self.inner_dim, patch_size * patch_size * self.out_channels, bias=True)
542
+
543
+ self.gradient_checkpointing = False
544
+
545
+ def forward(
546
+ self,
547
+ hidden_states: torch.Tensor,
548
+ encoder_hidden_states: torch.Tensor = None,
549
+ encoder_hidden_states_mask: torch.Tensor = None,
550
+ timestep: torch.LongTensor = None,
551
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
552
+ guidance: torch.Tensor = None, # TODO: this should probably be removed
553
+ attention_kwargs: Optional[Dict[str, Any]] = None,
554
+ return_dict: bool = True,
555
+ ) -> Union[torch.Tensor, Transformer2DModelOutput]:
556
+ """
557
+ The [`QwenTransformer2DModel`] forward method.
558
+
559
+ Args:
560
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_sequence_length, in_channels)`):
561
+ Input `hidden_states`.
562
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_sequence_length, joint_attention_dim)`):
563
+ Conditional embeddings (embeddings computed from the input conditions such as prompts) to use.
564
+ encoder_hidden_states_mask (`torch.Tensor` of shape `(batch_size, text_sequence_length)`):
565
+ Mask of the input conditions.
566
+ timestep ( `torch.LongTensor`):
567
+ Used to indicate denoising step.
568
+ attention_kwargs (`dict`, *optional*):
569
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
570
+ `self.processor` in
571
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
572
+ return_dict (`bool`, *optional*, defaults to `True`):
573
+ Whether or not to return a [`~models.transformer_2d.Transformer2DModelOutput`] instead of a plain
574
+ tuple.
575
+
576
+ Returns:
577
+ If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a
578
+ `tuple` where the first element is the sample tensor.
579
+ """
580
+ if attention_kwargs is not None:
581
+ attention_kwargs = attention_kwargs.copy()
582
+ lora_scale = attention_kwargs.pop("scale", 1.0)
583
+ else:
584
+ lora_scale = 1.0
585
+
586
+ if USE_PEFT_BACKEND:
587
+ # weight the lora layers by setting `lora_scale` for each PEFT layer
588
+ scale_lora_layers(self, lora_scale)
589
+ else:
590
+ if attention_kwargs is not None and attention_kwargs.get("scale", None) is not None:
591
+ logger.warning(
592
+ "Passing `scale` via `joint_attention_kwargs` when not using the PEFT backend is ineffective."
593
+ )
594
+
595
+ hidden_states = self.img_in(hidden_states)
596
+
597
+ timestep = timestep.to(hidden_states.dtype)
598
+ encoder_hidden_states = self.txt_norm(encoder_hidden_states)
599
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
600
+
601
+ if guidance is not None:
602
+ guidance = guidance.to(hidden_states.dtype) * 1000
603
+
604
+ temb = (
605
+ self.time_text_embed(timestep, hidden_states)
606
+ if guidance is None
607
+ else self.time_text_embed(timestep, guidance, hidden_states)
608
+ )
609
+
610
+ for index_block, block in enumerate(self.transformer_blocks):
611
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
612
+ encoder_hidden_states, hidden_states = self._gradient_checkpointing_func(
613
+ block,
614
+ hidden_states,
615
+ encoder_hidden_states,
616
+ encoder_hidden_states_mask,
617
+ temb,
618
+ image_rotary_emb,
619
+ )
620
+
621
+ else:
622
+ encoder_hidden_states, hidden_states = block(
623
+ hidden_states=hidden_states,
624
+ encoder_hidden_states=encoder_hidden_states,
625
+ encoder_hidden_states_mask=encoder_hidden_states_mask,
626
+ temb=temb,
627
+ image_rotary_emb=image_rotary_emb,
628
+ joint_attention_kwargs=attention_kwargs,
629
+ )
630
+
631
+ # Use only the image part (hidden_states) from the dual-stream blocks
632
+ hidden_states = self.norm_out(hidden_states, temb)
633
+ output = self.proj_out(hidden_states)
634
+
635
+ if USE_PEFT_BACKEND:
636
+ # remove `lora_scale` from each PEFT layer
637
+ unscale_lora_layers(self, lora_scale)
638
+
639
+ if not return_dict:
640
+ return (output,)
641
+
642
+ return Transformer2DModelOutput(sample=output)
requirements.txt CHANGED
@@ -1,13 +1,34 @@
1
- diffusers==0.38.0
2
-
3
- transformers
4
- accelerate
5
- safetensors
6
- sentencepiece
7
- dashscope
8
- kernels==0.11.0
9
- torchvision
10
- peft
11
- torchao==0.11.0
12
- spandrel
13
- spandrel_extra_arches
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pro Realism Edit Studio - Enhanced Edition
2
+ # ====================================================
3
+ # Core dependencies
4
+ diffusers==0.38.0
5
+
6
+ transformers
7
+ accelerate
8
+ safetensors
9
+ sentencepiece
10
+ dashscope
11
+ kernels==0.11.0
12
+ torchvision
13
+ peft
14
+ torchao==0.11.0
15
+
16
+ # Image processing and upscaling
17
+ spandrel
18
+ spandrel_extra_arches
19
+ Pillow
20
+ numpy
21
+
22
+ # Face restoration (REQUIRED for enhanced version)
23
+ gfpgan
24
+
25
+ # OpenCV for face detection (REQUIRED for enhanced version)
26
+ opencv-python
27
+
28
+ # SciPy for advanced image processing (optional fallback)
29
+ scipy
30
+
31
+ # Gradio and utilities
32
+ gradio
33
+ spaces
34
+ huggingface_hub
tests/test_enhance_stage.py CHANGED
@@ -1,45 +1,45 @@
1
- from pathlib import Path
2
- import unittest
3
-
4
-
5
- ROOT = Path(__file__).resolve().parents[1]
6
-
7
-
8
- def read_text(relative_path: str) -> str:
9
- return (ROOT / relative_path).read_text(encoding="utf-8")
10
-
11
-
12
- class EnhanceStageContractTest(unittest.TestCase):
13
- def test_enhance_modes_match_product_contract(self):
14
- app = read_text("app.py")
15
-
16
- self.assertIn('ENHANCE_MODE_OFF = "Off"', app)
17
- self.assertIn('ENHANCE_MODE_UPSCALE = "Upscale"', app)
18
- self.assertIn('ENHANCE_MODE_CLEAN = "Clean"', app)
19
- self.assertIn('ENHANCE_MODE_MAX_DETAIL = "Max Detail"', app)
20
- self.assertIn(
21
- "ENHANCE_MODE_CHOICES = [ENHANCE_MODE_OFF, ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL]",
22
- app,
23
- )
24
- self.assertIn("enhance_mode = gr.Radio", app)
25
- self.assertIn("apply_enhancement", app)
26
-
27
- def test_upscaler_is_lazy_and_configurable(self):
28
- app = read_text("app.py")
29
-
30
- self.assertIn("load_upscaler_model", app)
31
- self.assertIn("UPSCALER_MODEL_ID", app)
32
- self.assertIn("UPSCALER_MODEL_FILENAME", app)
33
- self.assertIn("hf_hub_download(", app)
34
- self.assertIn("spandrel", app)
35
- self.assertIn("tile_upscale", app)
36
-
37
- def test_upscaler_dependencies_are_declared(self):
38
- requirements = read_text("requirements.txt")
39
-
40
- self.assertIn("spandrel", requirements)
41
- self.assertIn("spandrel_extra_arches", requirements)
42
-
43
-
44
- if __name__ == "__main__":
45
- unittest.main()
 
1
+ from pathlib import Path
2
+ import unittest
3
+
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+
7
+
8
+ def read_text(relative_path: str) -> str:
9
+ return (ROOT / relative_path).read_text(encoding="utf-8")
10
+
11
+
12
+ class EnhanceStageContractTest(unittest.TestCase):
13
+ def test_enhance_modes_match_product_contract(self):
14
+ app = read_text("app.py")
15
+
16
+ self.assertIn('ENHANCE_MODE_OFF = "Off"', app)
17
+ self.assertIn('ENHANCE_MODE_UPSCALE = "Upscale"', app)
18
+ self.assertIn('ENHANCE_MODE_CLEAN = "Clean"', app)
19
+ self.assertIn('ENHANCE_MODE_MAX_DETAIL = "Max Detail"', app)
20
+ self.assertIn(
21
+ "ENHANCE_MODE_CHOICES = [ENHANCE_MODE_OFF, ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL]",
22
+ app,
23
+ )
24
+ self.assertIn("enhance_mode = gr.Radio", app)
25
+ self.assertIn("apply_enhancement", app)
26
+
27
+ def test_upscaler_is_lazy_and_configurable(self):
28
+ app = read_text("app.py")
29
+
30
+ self.assertIn("load_upscaler_model", app)
31
+ self.assertIn("UPSCALER_MODEL_ID", app)
32
+ self.assertIn("UPSCALER_MODEL_FILENAME", app)
33
+ self.assertIn("hf_hub_download(", app)
34
+ self.assertIn("spandrel", app)
35
+ self.assertIn("tile_upscale", app)
36
+
37
+ def test_upscaler_dependencies_are_declared(self):
38
+ requirements = read_text("requirements.txt")
39
+
40
+ self.assertIn("spandrel", requirements)
41
+ self.assertIn("spandrel_extra_arches", requirements)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ unittest.main()
tests/test_external_sources.py CHANGED
@@ -1,51 +1,51 @@
1
- from pathlib import Path
2
- import unittest
3
-
4
-
5
- ROOT = Path(__file__).resolve().parents[1]
6
-
7
-
8
- def read_text(relative_path: str) -> str:
9
- return (ROOT / relative_path).read_text(encoding="utf-8")
10
-
11
-
12
- class ExternalSourcePolicyTest(unittest.TestCase):
13
- def test_app_uses_phr00t_v23_single_file_source(self):
14
- app = read_text("app.py")
15
-
16
- self.assertNotIn("Sneak-Moose", app)
17
- self.assertIn("Phr00t/Qwen-Image-Edit-Rapid-AIO", app)
18
- self.assertIn("v23/Qwen-Rapid-AIO-NSFW-v23.safetensors", app)
19
- self.assertIn("load_phr00t_rapid_transformer", app)
20
- self.assertIn('PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model."', app)
21
- self.assertIn("safe_open", app)
22
- self.assertNotIn("from_single_file", app)
23
-
24
- def test_private_diagnostics_callback_is_removed(self):
25
- app = read_text("app.py")
26
-
27
- self.assertNotIn("QUALITY_ENHANCEMENT_URL", app)
28
- self.assertNotIn("QUALITY_ENHANCEMENT_TOKEN", app)
29
- self.assertNotIn("requests.post", app)
30
- self.assertNotIn("_emit_diagnostics", app)
31
-
32
- def test_third_party_video_space_is_not_hardcoded(self):
33
- app = read_text("app.py")
34
-
35
- self.assertNotIn("multimodalart/wan-2-2-first-last-frame", app)
36
- self.assertIn('VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip()', app)
37
- self.assertIn("Client(VIDEO_SPACE_ID)", app)
38
-
39
- def test_no_external_logo_image_or_sneak_moose_docs(self):
40
- relevant_text = "\n".join(
41
- read_text(path)
42
- for path in ("app.py", "README.md", "AGENT.md", "CLAUDE.md")
43
- )
44
-
45
- self.assertNotIn("Sneak-Moose", relevant_text)
46
- self.assertNotIn("qianwen-res.oss-cn-beijing.aliyuncs.com", relevant_text)
47
- self.assertNotIn("Rapid-AIO v18", relevant_text)
48
-
49
-
50
- if __name__ == "__main__":
51
- unittest.main()
 
1
+ from pathlib import Path
2
+ import unittest
3
+
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+
7
+
8
+ def read_text(relative_path: str) -> str:
9
+ return (ROOT / relative_path).read_text(encoding="utf-8")
10
+
11
+
12
+ class ExternalSourcePolicyTest(unittest.TestCase):
13
+ def test_app_uses_phr00t_v23_single_file_source(self):
14
+ app = read_text("app.py")
15
+
16
+ self.assertNotIn("Sneak-Moose", app)
17
+ self.assertIn("Phr00t/Qwen-Image-Edit-Rapid-AIO", app)
18
+ self.assertIn("v23/Qwen-Rapid-AIO-NSFW-v23.safetensors", app)
19
+ self.assertIn("load_phr00t_rapid_transformer", app)
20
+ self.assertIn('PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model."', app)
21
+ self.assertIn("safe_open", app)
22
+ self.assertNotIn("from_single_file", app)
23
+
24
+ def test_private_diagnostics_callback_is_removed(self):
25
+ app = read_text("app.py")
26
+
27
+ self.assertNotIn("QUALITY_ENHANCEMENT_URL", app)
28
+ self.assertNotIn("QUALITY_ENHANCEMENT_TOKEN", app)
29
+ self.assertNotIn("requests.post", app)
30
+ self.assertNotIn("_emit_diagnostics", app)
31
+
32
+ def test_third_party_video_space_is_not_hardcoded(self):
33
+ app = read_text("app.py")
34
+
35
+ self.assertNotIn("multimodalart/wan-2-2-first-last-frame", app)
36
+ self.assertIn('VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip()', app)
37
+ self.assertIn("Client(VIDEO_SPACE_ID)", app)
38
+
39
+ def test_no_external_logo_image_or_sneak_moose_docs(self):
40
+ relevant_text = "\n".join(
41
+ read_text(path)
42
+ for path in ("app.py", "README.md", "AGENT.md", "CLAUDE.md")
43
+ )
44
+
45
+ self.assertNotIn("Sneak-Moose", relevant_text)
46
+ self.assertNotIn("qianwen-res.oss-cn-beijing.aliyuncs.com", relevant_text)
47
+ self.assertNotIn("Rapid-AIO v18", relevant_text)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ unittest.main()