[hf staff] UI suggestion - stream the denoising process

#1
by linoyts HF Staff - opened
Files changed (1) hide show
  1. app.py +80 -7
app.py CHANGED
@@ -36,6 +36,72 @@ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
36
  MODEL_ID = os.environ.get("MODEL_ID", "Photoroom/prxpixel-t2i")
37
  HF_TOKEN = os.environ.get("HF_TOKEN")
38
  MAX_SEED = 2**31 - 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  # --- Safety guard ---------------------------------------------------------
41
  # Small open-source NSFW prompt classifier (safe/nsfw) + a keyword blocklist for violence/gore
@@ -49,7 +115,7 @@ VIOLENCE_WORDS = [
49
  ]
50
 
51
  print(f"Loading {MODEL_ID} on CPU ...")
52
- pipe = PRXPixelPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, token=HF_TOKEN)
53
  print(f"Loading safety classifier {SAFETY_MODEL} on CPU ...")
54
  safety_tokenizer = AutoTokenizer.from_pretrained(SAFETY_MODEL)
55
  safety_model = AutoModelForSequenceClassification.from_pretrained(SAFETY_MODEL)
@@ -84,9 +150,8 @@ def generate(prompt, negative_prompt, steps, guidance_scale, shift, resolution,
84
  pipe.scheduler = FlowMatchEulerDiscreteScheduler(shift=float(shift))
85
 
86
  res = int(resolution)
87
- # The pipeline already denormalizes to a proper PIL image. (Do NOT post-process the latents
88
- # with (x+1)/2 yourself — output_type="pt" is already in [0,1], so re-normalizing washes it out.)
89
- image = pipe(
90
  prompt,
91
  negative_prompt=negative_prompt or "",
92
  height=res,
@@ -94,8 +159,13 @@ def generate(prompt, negative_prompt, steps, guidance_scale, shift, resolution,
94
  num_inference_steps=int(steps),
95
  guidance_scale=float(guidance_scale),
96
  generator=torch.Generator("cuda").manual_seed(seed),
97
- ).images[0]
98
- return image, seed
 
 
 
 
 
99
 
100
 
101
  # (caption, prompt) for each pre-rendered example image examples/ex{i}.png
@@ -146,6 +216,9 @@ with gr.Blocks(title="PRX Pixel") as demo:
146
  randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
147
  run = gr.Button("Generate", variant="primary")
148
  with gr.Column():
 
 
 
149
  out_image = gr.Image(label="Output", type="pil")
150
  out_seed = gr.Number(label="Seed used", interactive=False)
151
 
@@ -168,7 +241,7 @@ with gr.Blocks(title="PRX Pixel") as demo:
168
  run.click(
169
  generate,
170
  inputs=[prompt, negative_prompt, steps, guidance_scale, shift, resolution, seed, randomize_seed],
171
- outputs=[out_image, out_seed],
172
  )
173
 
174
 
 
36
  MODEL_ID = os.environ.get("MODEL_ID", "Photoroom/prxpixel-t2i")
37
  HF_TOKEN = os.environ.get("HF_TOKEN")
38
  MAX_SEED = 2**31 - 1
39
+ PREVIEW_SIZE = 512 # downscaled size for the per-step preview stream
40
+
41
+
42
+ class StreamingPRXPixelPipeline(PRXPixelPipeline):
43
+ """PRXPixel pipeline whose denoising loop is a generator that yields the per-step x0 estimate.
44
+
45
+ Identical math to the parent `__call__` (same prompt encoding, noise_scale init, flow-matching
46
+ velocity conversion and scheduler step) — it just `yield`s the model's current clean-image
47
+ prediction x0 each step. Because PRXPixel is pixel-space, x0 is already an RGB image in [-1, 1],
48
+ so we can show it directly with no VAE decode. The final image is the running sample, matching
49
+ what the standard `__call__` returns.
50
+ """
51
+
52
+ @torch.inference_mode()
53
+ def stream(self, prompt, negative_prompt="", height=1024, width=1024,
54
+ num_inference_steps=28, guidance_scale=1.0, generator=None):
55
+ device = self._execution_device
56
+ self._guidance_scale = guidance_scale
57
+
58
+ text_embeddings, cross_attn_mask, uncond_text_embeddings, uncond_cross_attn_mask = self.encode_prompt(
59
+ prompt, device,
60
+ do_classifier_free_guidance=self.do_classifier_free_guidance,
61
+ negative_prompt=negative_prompt or "",
62
+ num_images_per_prompt=1,
63
+ )
64
+ self.scheduler.set_timesteps(num_inference_steps, device=device)
65
+ timesteps = self.scheduler.timesteps
66
+ latents = self.prepare_latents(
67
+ 1, self.transformer.config.in_channels, height, width,
68
+ text_embeddings.dtype, device, generator, None,
69
+ )
70
+ if self.do_classifier_free_guidance:
71
+ ca_embed = torch.cat([uncond_text_embeddings, text_embeddings], dim=0)
72
+ ca_mask = (
73
+ torch.cat([uncond_cross_attn_mask, cross_attn_mask], dim=0)
74
+ if cross_attn_mask is not None and uncond_cross_attn_mask is not None else None
75
+ )
76
+ else:
77
+ ca_embed, ca_mask = text_embeddings, cross_attn_mask
78
+
79
+ total = len(timesteps)
80
+ for i, t in enumerate(timesteps):
81
+ if self.do_classifier_free_guidance:
82
+ latents_in = torch.cat([latents, latents], dim=0)
83
+ t_cont = (t.float() / self.scheduler.config.num_train_timesteps).view(1).repeat(2).to(device)
84
+ else:
85
+ latents_in = latents
86
+ t_cont = (t.float() / self.scheduler.config.num_train_timesteps).view(1).to(device)
87
+
88
+ pred = self.transformer(
89
+ hidden_states=latents_in, timestep=t_cont,
90
+ encoder_hidden_states=ca_embed, attention_mask=ca_mask, return_dict=False,
91
+ )[0]
92
+ if self.do_classifier_free_guidance:
93
+ pred_uncond, pred_text = pred.chunk(2, dim=0)
94
+ pred = pred_uncond + guidance_scale * (pred_text - pred_uncond)
95
+
96
+ # `pred` is the predicted clean image x0 in [-1, 1] — the model's current guess.
97
+ yield i + 1, total, self.image_processor.postprocess(pred.float(), output_type="pil")[0]
98
+
99
+ t_x = torch.clamp(t.float() / self.scheduler.config.num_train_timesteps, min=0.05)
100
+ latents = self.scheduler.step((latents - pred) / t_x, t, latents).prev_sample
101
+
102
+ # Final image is the running sample, matching the standard pipeline output.
103
+ yield total, total, self.image_processor.postprocess(latents.float(), output_type="pil")[0]
104
+
105
 
106
  # --- Safety guard ---------------------------------------------------------
107
  # Small open-source NSFW prompt classifier (safe/nsfw) + a keyword blocklist for violence/gore
 
115
  ]
116
 
117
  print(f"Loading {MODEL_ID} on CPU ...")
118
+ pipe = StreamingPRXPixelPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, token=HF_TOKEN)
119
  print(f"Loading safety classifier {SAFETY_MODEL} on CPU ...")
120
  safety_tokenizer = AutoTokenizer.from_pretrained(SAFETY_MODEL)
121
  safety_model = AutoModelForSequenceClassification.from_pretrained(SAFETY_MODEL)
 
150
  pipe.scheduler = FlowMatchEulerDiscreteScheduler(shift=float(shift))
151
 
152
  res = int(resolution)
153
+ # Stream the per-step x0 (clean-image) estimate so you can watch the image form, then the final.
154
+ for i, total, x0 in pipe.stream(
 
155
  prompt,
156
  negative_prompt=negative_prompt or "",
157
  height=res,
 
159
  num_inference_steps=int(steps),
160
  guidance_scale=float(guidance_scale),
161
  generator=torch.Generator("cuda").manual_seed(seed),
162
+ ):
163
+ if i < total:
164
+ preview = x0.copy()
165
+ preview.thumbnail((PREVIEW_SIZE, PREVIEW_SIZE))
166
+ yield preview, seed, f"Step {i} / {total}"
167
+ else:
168
+ yield x0, seed, f"Done — {total} steps" # final, full resolution
169
 
170
 
171
  # (caption, prompt) for each pre-rendered example image examples/ex{i}.png
 
216
  randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
217
  run = gr.Button("Generate", variant="primary")
218
  with gr.Column():
219
+ # The output now updates live with the model's per-step x0 estimate (pixel space → no decode),
220
+ # so you can watch the image form, blurry → sharp. Final frame is full resolution.
221
+ step_status = gr.Markdown("")
222
  out_image = gr.Image(label="Output", type="pil")
223
  out_seed = gr.Number(label="Seed used", interactive=False)
224
 
 
241
  run.click(
242
  generate,
243
  inputs=[prompt, negative_prompt, steps, guidance_scale, shift, resolution, seed, randomize_seed],
244
+ outputs=[out_image, out_seed, step_status],
245
  )
246
 
247