Text-to-Image
Diffusers
Safetensors
File size: 12,811 Bytes
aa1e121
 
 
 
 
 
 
 
 
845c08d
aa1e121
 
 
 
845c08d
aa1e121
 
845c08d
aa1e121
 
 
7b12282
 
aa1e121
 
 
efa99e4
 
 
 
 
 
ae9c446
445fd7f
ae9c446
63008cd
38721cd
 
ae9c446
445fd7f
ae9c446
 
445fd7f
 
 
 
 
 
 
 
 
ae9c446
445fd7f
63008cd
445fd7f
63008cd
445fd7f
33e4060
aa1e121
a4e4f02
 
 
 
 
 
 
 
 
 
 
 
845c08d
a4e4f02
 
 
 
 
 
 
845c08d
a4e4f02
 
845c08d
a4e4f02
 
 
 
 
845c08d
a4e4f02
 
 
 
 
 
 
 
 
 
 
aa1e121
a4e4f02
 
aa1e121
 
 
 
 
7b12282
 
 
 
aa1e121
 
 
7b12282
aa1e121
7b12282
aa1e121
 
 
 
 
741b3a0
ed66552
aa1e121
 
 
445fd7f
 
aa1e121
0bec123
aa1e121
 
845c08d
aa1e121
 
 
9dbef7d
da395bc
0bec123
da395bc
0381357
 
 
2ba6a00
63008cd
 
2ba6a00
63008cd
0381357
 
 
 
 
2ba6a00
0381357
63008cd
a29e660
0381357
 
 
 
 
 
 
a29e660
 
0381357
 
 
2ba6a00
 
 
a29e660
0381357
2ba6a00
0381357
 
 
 
 
7b12282
741b3a0
 
aa1e121
da395bc
 
e189156
da395bc
5b80164
 
 
ed66552
5b80164
 
 
 
da395bc
63008cd
445fd7f
63008cd
741b3a0
da395bc
ae9c446
da395bc
 
63008cd
9dbef7d
efa99e4
da395bc
 
63008cd
453a03d
63008cd
 
 
 
da395bc
 
 
 
63008cd
da395bc
 
 
 
445fd7f
efa99e4
da395bc
 
 
445fd7f
da395bc
 
445fd7f
da395bc
63008cd
741b3a0
da395bc
 
aa1e121
 
 
 
 
5b80164
aa1e121
 
da395bc
 
aa1e121
 
da395bc
63008cd
aa1e121
da395bc
 
aa1e121
0381357
 
da395bc
efa99e4
f9cc797
da395bc
ae9c446
 
da395bc
aa1e121
da395bc
ae9c446
 
da395bc
aa1e121
0381357
0313699
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import torch
import numpy as np
from PIL import Image
from typing import List, Union, Optional, Tuple
from dataclasses import dataclass

from diffusers import DiffusionPipeline
from diffusers.utils import BaseOutput
from tqdm import tqdm
from transformers import Qwen3_5ForConditionalGeneration, Qwen3_5Tokenizer

@dataclass
class SdxsPipelineOutput(BaseOutput):
    images: Union[List[Image.Image], np.ndarray]
    prompt: Optional[Union[str, List[str]]] = None

class SdxsPipeline(DiffusionPipeline):
    def __init__(self, vae, text_encoder, tokenizer, unet, scheduler):
        super().__init__()
        self.register_modules(
            vae=vae, 
            text_encoder=text_encoder,
            tokenizer=tokenizer,
            unet=unet, 
            scheduler=scheduler
        )
        self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)            
        mean = getattr(self.vae.config, "latents_mean", None)
        std = getattr(self.vae.config, "latents_std", None)
        if mean is not None and std is not None:
            self.vae_latents_std = torch.tensor(std, device=self.unet.device, dtype=self.unet.dtype).view(1, len(std), 1, 1)
            self.vae_latents_mean = torch.tensor(mean, device=self.unet.device, dtype=self.unet.dtype).view(1, len(mean), 1, 1)
        
    def preprocess_image(self, image: Image.Image, width: int, height: int):
        """Ресайз и центрированный кроп изображения для асимметричного VAE."""
        # Для энкодера с масштабом 8
        target_height = ((height // self.vae_scale_factor) * self.vae_scale_factor)
        target_width = ((width // self.vae_scale_factor) * self.vae_scale_factor)
        
        w, h = image.size
        aspect_ratio = target_width / target_height
        
        if w / h > aspect_ratio:
            new_w = int(h * aspect_ratio)
            left = (w - new_w) // 2
            image = image.crop((left, 0, left + new_w, h))
        else:
            new_h = int(w / aspect_ratio)
            top = (h - new_h) // 2
            image = image.crop((0, top, w, top + new_h))
        
        image = image.resize((target_width, target_height), resample=Image.LANCZOS)
        image = np.array(image).astype(np.float32) / 255.0
        image = image[None].transpose(0, 3, 1, 2)  # [1, C, H, W]
        image = torch.from_numpy(image)
        return 2.0 * image - 1.0  # [-1, 1]


    def encode_prompt(self, prompt, negative_prompt, device, dtype):
        def get_encode(texts):
            if texts is None:
                texts = ""

            if isinstance(texts, str):
                texts = [texts]

            with torch.no_grad():
                # 1. Собираем текстовые промпты оборачивая их в Chat Template
                formatted_prompts = []
                for t in texts:
                    messages = [{"role": "user", "content": [{"type": "text", "text": t}]}]
                    res_text = self.tokenizer.apply_chat_template(
                        messages, 
                        add_generation_prompt=True, 
                        tokenize=False 
                    )
                    formatted_prompts.append(res_text)

                # 2. Токенизируем, режем и добавляем паддинг за один раз
                toks = self.tokenizer(
                    formatted_prompts,
                    padding="max_length",
                    max_length=255,
                    truncation=True, # Не забываем обрезать, если вдруг длиннее
                    return_tensors="pt"
                ).to(device)

                # 3. Прогоняем через модель
                outputs = self.text_encoder(
                    input_ids=toks.input_ids, 
                    attention_mask=toks.attention_mask,
                    output_hidden_states=True
                )

                layer_index = -2
                last_hidden = outputs.hidden_states[layer_index]
                seq_len = toks.attention_mask.sum(dim=1) - 1
                pooled = last_hidden[torch.arange(len(last_hidden)), seq_len.clamp(min=0)]

                return last_hidden, toks.attention_mask, pooled

        pos_embeds, pos_mask, pos_pooled = get_encode(prompt)
        neg_embeds, neg_mask, neg_pooled = get_encode(negative_prompt)

        batch_size = pos_embeds.shape[0]
        if neg_embeds.shape[0] != batch_size:
            neg_embeds = neg_embeds.repeat(batch_size, 1, 1)
            neg_mask = neg_mask.repeat(batch_size, 1)
            neg_pooled = neg_pooled.repeat(batch_size, 1)

        if pos_pooled.shape[0] != batch_size:
            pos_pooled = pos_pooled.repeat(batch_size, 1)

        text_embeddings = torch.cat([neg_embeds, pos_embeds], dim=0)
        final_mask = torch.cat([neg_mask, pos_mask], dim=0)
        pooled_embeds = torch.cat([neg_pooled, pos_pooled], dim=0)

        return text_embeddings.to(dtype=dtype), final_mask.to(dtype=torch.int64), pooled_embeds.to(dtype=dtype)

    @torch.no_grad()
    def __call__(
        self,
        prompt: Union[str, List[str]],
        image: Optional[Union[Image.Image, List[Image.Image]]] = None,
        coef: float = 0.97, # ← strength (0.0 = оригинал, 1.0 = полный шум)
        negative_prompt: Optional[Union[str, List[str]]] = None,
        height: int = 1024,
        width: int = 1024,
        num_inference_steps: int = 40,
        guidance_scale: float = 4.0,
        generator: Optional[torch.Generator] = None,
        seed: Optional[int] = None,
        output_type: str = "pil",
        return_dict: bool = True,
        refine_prompt: bool = False,
        **kwargs,
    ):
        device = self.device
        dtype = self.unet.dtype
    
        if generator is None and seed is not None:
            generator = torch.Generator(device=device).manual_seed(seed)
            
        # ==================== REFINE PROMPT (INLINE) ====================
        if refine_prompt and prompt:
            sys_msg = (
                "You are a skilled text-to-image prompt engineer whose sole function is to transform the user's input into an aesthetically optimized, detailed, and visually descriptive three-sentence output. "
                "**The primary subject (e.g., 'girl', 'dog', 'house') MUST be the main focus of the revised prompt and MUST be described in rich detail within the first sentence or two.** "
                "Output **only** the final revised prompt in **English**, with absolutely no commentary.\n Don't use cliches like warm,soft,vibrant, wildflowers. Be creative "
                "User input prompt: "
            )
            prompts_list = [prompt] if isinstance(prompt, str) else prompt
            refined_list = []
            
            for p in prompts_list:
                messages = [{"role": "user", "content": [{"type": "text", "text": sys_msg + p}]}]
                
                # Используем Qwen-Instruct формат (apply_chat_template сам подставит system/user/assistant токены)
                inputs = self.tokenizer.apply_chat_template(
                    messages,
                    tokenize=True,
                    add_generation_prompt=True,
                    return_dict=True,
                    return_tensors="pt"
                ).to(device)
                
                generated_ids = self.text_encoder.generate(
                    **inputs, max_new_tokens=255, do_sample=True,temperature = 0.7
                )
                
                # Обрезаем входные токены из ответа
                generated_ids_trimmed = [
                    out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
                ]
                output_text = self.tokenizer.batch_decode(
                    generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
                )
                refined_list.append(output_text)
                
            prompt = refined_list[0] if isinstance(prompt, str) else refined_list

        # ==================== ENCODE PROMPTS ====================
        text_embeddings, attention_mask, pooled_embeds = self.encode_prompt(
            prompt, negative_prompt, device, dtype
        )
        batch_size = 1 if isinstance(prompt, str) else len(prompt)
    
        # 2. Scheduler timesteps
        self.scheduler.set_timesteps(num_inference_steps, device=device)
        timesteps = self.scheduler.timesteps

        # ==================== TIME IDS =======================================
        time_ids = torch.zeros(
            pooled_embeds.shape[0],
            6, 
            device=device, 
            dtype=torch.long
        )
    
        # ==================== IMG2IMG БЛОК (НОВАЯ ВЕРСИЯ) ====================
        if image is not None:
            # --- Подготовка изображения ---
            if isinstance(image, Image.Image):
                image_tensor = self.preprocess_image(image, width, height).to(device, self.vae.dtype)
            else:
                image_tensor = self.preprocess_image(image[0], width, height).to(device, self.vae.dtype)
    
            # --- Кодируем в latent ---
            latents_clean = self.vae.encode(image_tensor).latent_dist.sample(generator=generator)
            latents_clean = (latents_clean - self.vae_latents_mean.to(device, self.vae.dtype)) / self.vae_latents_std.to(device, self.vae.dtype)
            latents_clean = latents_clean.to(dtype)
    
            # --- Добавляем шум по Rectified Flow формуле ---
            noise = torch.randn_like(latents_clean)
    
            # coef = strength (0.0 → оригинал, 1.0 → чистый шум)
            sigma = coef                                      # в Flow Matching sigma = t
            if hasattr(self.scheduler, "sigma_shift"):        # если есть shift (Flux-style)
                sigma = self.scheduler.sigma_shift(sigma)
    
            latents = (1.0 - sigma) * latents_clean + sigma * noise
    
            # Обрезаем timesteps начиная с текущего sigma
            init_timestep = int(num_inference_steps * coef)
            t_start = max(num_inference_steps - init_timestep, 0)
            timesteps = timesteps[t_start:]
    
        else:
            # txt2img
            latent_h = height // self.vae_scale_factor
            latent_w = width // self.vae_scale_factor
    
            latents = torch.randn(
                (batch_size, self.unet.config.in_channels, latent_h, latent_w),
                generator=generator, device=device, dtype=dtype
            )
    
        # ==================== DENOISING LOOP (одинаковый для txt2img и img2img) ====================
        for i, t in enumerate(tqdm(timesteps, desc="Sampling")):
            latent_model_input = torch.cat([latents] * 2) if guidance_scale > 1.0 else latents
    
            model_out = self.unet(
                latent_model_input,
                t,
                encoder_hidden_states=text_embeddings,
                encoder_attention_mask=attention_mask,
                added_cond_kwargs={"text_embeds": pooled_embeds,"time_ids": time_ids},
                return_dict=False,
            )[0]
    
            if guidance_scale > 1.0:
                flow_uncond, flow_cond = model_out.chunk(2)
                model_out = flow_uncond + guidance_scale * (flow_cond - flow_uncond)
    
            # Важно: используем scheduler.step — он сам знает, что делать с velocity
            latents = self.scheduler.step(model_out, t, latents, return_dict=False)[0]
    
        # ==================== DECODE ====================
        if output_type == "latent":
            if not return_dict: return (latents, prompt)
            return SdxsPipelineOutput(images=latents, prompt=prompt)
    
        latents = latents * self.vae_latents_std.to(device, self.vae.dtype) + self.vae_latents_mean.to(device, self.vae.dtype)
        image_output = self.vae.decode(latents.to(self.vae.dtype), return_dict=False)[0]
    
        image_output = (image_output.clamp(-1, 1) + 1) / 2
        image_np = image_output.cpu().permute(0, 2, 3, 1).float().numpy()
    
        if output_type == "pil":
            images = [(Image.fromarray((img * 255).round().astype("uint8"))) for img in image_np]
        else:
            images = image_np
    
        if not return_dict:
            return (images, prompt)
        return SdxsPipelineOutput(images=images, prompt=prompt)