multimodalart HF Staff commited on
Commit
7296856
·
verified ·
1 Parent(s): 2266a70

Upload /tmp/hugging-demos-build-model_sensenova_SenseNova-U1.5-8B-MoT-anc7wu33/build/app.py with huggingface_hub

Browse files
tmp/hugging-demos-build-model_sensenova_SenseNova-U1.5-8B-MoT-anc7wu33/build/app.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import spaces # MUST be imported before torch / transformers / sensenova_u1
4
+
5
+ import os
6
+ import random
7
+
8
+ import gradio as gr
9
+ import numpy as np
10
+ import torch
11
+ from PIL import Image
12
+ from transformers import AutoConfig, AutoModel, AutoTokenizer
13
+
14
+ import sensenova_u1
15
+ from sensenova_u1.models.neo_unify.utils import smart_resize
16
+
17
+ MODEL_ID = "sensenova/SenseNova-U1.5-8B-MoT"
18
+
19
+ NORM_MEAN = (0.5, 0.5, 0.5)
20
+ NORM_STD = (0.5, 0.5, 0.5)
21
+
22
+ # U1.5 trained T2I aspect-ratio buckets (from the upstream examples/t2i/inference.py
23
+ # SUPPORTED_RESOLUTIONS table).
24
+ T2I_RESOLUTIONS: dict[str, tuple[int, int]] = {
25
+ "1:1": (2048, 2048),
26
+ "16:9": (2720, 1536),
27
+ "9:16": (1536, 2720),
28
+ "3:2": (2496, 1664),
29
+ "2:3": (1664, 2496),
30
+ "4:3": (2368, 1760),
31
+ "3:4": (1760, 2368),
32
+ "1:2": (1440, 2880),
33
+ "2:1": (2880, 1440),
34
+ "1:3": (1152, 3456),
35
+ "3:1": (3456, 1152),
36
+ }
37
+
38
+ # Reference config for SenseNova-U1.5 (from the model card Quick Start):
39
+ # cfg_scale=4.0, timestep_shift=3.0, num_steps=50
40
+ DEFAULT_CFG_SCALE = 4.0
41
+ DEFAULT_TIMESTEP_SHIFT = 3.0
42
+ DEFAULT_NUM_STEPS = 50
43
+
44
+ # Editing output grid factor (= patch_size * merge_size = 32).
45
+ EDIT_GRID_FACTOR = 32
46
+ EDIT_TARGET_PIXELS = 2048 * 2048
47
+ EDIT_INPUT_MAX_PIXELS = 2048 * 2048
48
+
49
+ MAX_SEED = 2**31 - 1
50
+
51
+
52
+ def _denorm(x: torch.Tensor) -> torch.Tensor:
53
+ mean = torch.tensor(NORM_MEAN, device=x.device, dtype=x.dtype).view(1, 3, 1, 1)
54
+ std = torch.tensor(NORM_STD, device=x.device, dtype=x.dtype).view(1, 3, 1, 1)
55
+ return (x * std + mean).clamp(0, 1)
56
+
57
+
58
+ def _to_pil(batch: torch.Tensor) -> list[Image.Image]:
59
+ arr = _denorm(batch.float()).permute(0, 2, 3, 1).cpu().numpy()
60
+ arr = (arr * 255.0).round().astype(np.uint8)
61
+ return [Image.fromarray(a) for a in arr]
62
+
63
+
64
+ def _coerce_pil(img) -> Image.Image:
65
+ if isinstance(img, Image.Image):
66
+ return img
67
+ if isinstance(img, tuple):
68
+ img = img[0]
69
+ if isinstance(img, str):
70
+ return Image.open(img)
71
+ return img
72
+
73
+
74
+ def _prep_input_image(img: Image.Image, max_pixels: int) -> Image.Image:
75
+ if img.mode == "RGBA":
76
+ bg = Image.new("RGB", img.size, (255, 255, 255))
77
+ bg.paste(img, mask=img.split()[3])
78
+ img = bg
79
+ img = img.convert("RGB")
80
+ h, w = smart_resize(
81
+ height=img.height,
82
+ width=img.width,
83
+ factor=EDIT_GRID_FACTOR,
84
+ min_pixels=max_pixels,
85
+ max_pixels=max_pixels,
86
+ )
87
+ if (w, h) != img.size:
88
+ img = img.resize((w, h), Image.LANCZOS)
89
+ return img
90
+
91
+
92
+ def _editing_output_size(input_img: Image.Image, target_pixels: int) -> tuple[int, int]:
93
+ h, w = smart_resize(
94
+ height=input_img.height,
95
+ width=input_img.width,
96
+ factor=EDIT_GRID_FACTOR,
97
+ min_pixels=target_pixels,
98
+ max_pixels=target_pixels,
99
+ )
100
+ return w, h
101
+
102
+
103
+ print("[startup] loading SenseNova-U1.5-8B-MoT (this may take a few minutes)...")
104
+ sensenova_u1.set_attn_backend("auto")
105
+ print(f"[startup] attn backend: {sensenova_u1.effective_attn_backend()!r}")
106
+
107
+ config = AutoConfig.from_pretrained(MODEL_ID)
108
+ sensenova_u1.check_checkpoint_compatibility(config)
109
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
110
+ model = AutoModel.from_pretrained(MODEL_ID, config=config, dtype=torch.bfloat16).to("cuda").eval()
111
+ print("[startup] model ready.")
112
+
113
+
114
+ def _estimate_duration(images, prompt, aspect_ratio, seed, randomize_seed, *args, **kwargs):
115
+ # Editing is heavier (image conditioning); give it more headroom.
116
+ has_input = images is not None and len(images) > 0
117
+ if has_input:
118
+ return 180
119
+ # T2I at 2048x2048 with 50 steps is the heaviest t2i case
120
+ w, h = T2I_RESOLUTIONS.get(aspect_ratio, (2048, 2048))
121
+ pixels = w * h
122
+ if pixels > 2048 * 2048:
123
+ return 180
124
+ return 120
125
+
126
+
127
+ @spaces.GPU(duration=_estimate_duration)
128
+ def generate(
129
+ images: list | None,
130
+ prompt: str,
131
+ aspect_ratio: str = "1:1",
132
+ seed: int = 42,
133
+ randomize_seed: bool = True,
134
+ progress=gr.Progress(track_tqdm=True),
135
+ ):
136
+ """Generate an image from a text prompt, or edit an uploaded image.
137
+
138
+ Args:
139
+ images: optional uploaded image(s) to edit; leave empty for text-to-image.
140
+ prompt: what to generate, or the edit instruction to apply to the input image.
141
+ aspect_ratio: output aspect ratio for text-to-image (ignored when editing).
142
+ seed: RNG seed for reproducible sampling.
143
+ randomize_seed: if True, pick a fresh random seed each run.
144
+ """
145
+ if not prompt or not prompt.strip():
146
+ raise gr.Error("Please enter a prompt.")
147
+ if randomize_seed:
148
+ seed = random.randint(0, MAX_SEED)
149
+
150
+ has_input = images is not None and len(images) > 0
151
+
152
+ with torch.inference_mode():
153
+ if not has_input:
154
+ width, height = T2I_RESOLUTIONS[aspect_ratio]
155
+ tensor = model.t2i_generate(
156
+ tokenizer,
157
+ prompt,
158
+ image_size=(width, height),
159
+ cfg_scale=DEFAULT_CFG_SCALE,
160
+ cfg_norm="none",
161
+ timestep_shift=DEFAULT_TIMESTEP_SHIFT,
162
+ cfg_interval=(0.0, 1.0),
163
+ num_steps=DEFAULT_NUM_STEPS,
164
+ batch_size=1,
165
+ seed=int(seed),
166
+ think_mode=False,
167
+ )
168
+ else:
169
+ pil_inputs = [_prep_input_image(_coerce_pil(item), EDIT_INPUT_MAX_PIXELS) for item in images]
170
+ out_w, out_h = _editing_output_size(pil_inputs[0], EDIT_TARGET_PIXELS)
171
+ tensor = model.it2i_generate(
172
+ tokenizer,
173
+ prompt,
174
+ pil_inputs,
175
+ image_size=(out_w, out_h),
176
+ cfg_scale=DEFAULT_CFG_SCALE,
177
+ img_cfg_scale=1.0,
178
+ cfg_norm="none",
179
+ timestep_shift=DEFAULT_TIMESTEP_SHIFT,
180
+ cfg_interval=(0.0, 1.0),
181
+ num_steps=DEFAULT_NUM_STEPS,
182
+ batch_size=1,
183
+ think_mode=False,
184
+ seed=int(seed),
185
+ )
186
+
187
+ images_out = _to_pil(tensor)
188
+ return images_out[0], seed
189
+
190
+
191
+ # T2I examples: prompt + aspect ratio
192
+ T2I_EXAMPLES = [
193
+ [
194
+ "A cinematic mountain lake at sunrise, realistic photography, golden mist over still water, snow-capped peaks reflected in the lake, ultra-detailed.",
195
+ "1:1",
196
+ ],
197
+ [
198
+ 'A neon bar sign that clearly reads "OPEN LATE", dark interior, moody reflections, easy text rendering.',
199
+ "16:9",
200
+ ],
201
+ [
202
+ "Close portrait of an elderly woman by a farmhouse window, textured skin, gentle smile, warm natural light, emotional documentary look.",
203
+ "2:3",
204
+ ],
205
+ [
206
+ "A cute fluffy corgi puppy wearing a tiny chef's hat, sitting at a wooden table with fresh-baked cookies, warm kitchen lighting, photorealistic.",
207
+ "1:1",
208
+ ],
209
+ [
210
+ "Lavender fields stretching to the horizon under a pastel sunset, a small stone farmhouse, highly detailed flowers, romantic countryside scene.",
211
+ "4:3",
212
+ ],
213
+ ]
214
+
215
+ # Editing examples: gallery input (list of paths) + prompt
216
+ EDIT_EXAMPLES = [
217
+ [["examples/edit_1.webp"], "Change the jacket of the person on the left to bright yellow."],
218
+ [["examples/edit_2.webp"], "Make the person in the image smile."],
219
+ [["examples/edit_3.webp"], "Add a bouquet of flowers."],
220
+ [["examples/edit_4.webp"], "Turn the image into an American comic style."],
221
+ ]
222
+
223
+
224
+ CSS = """
225
+ #col-container { max-width: 1100px; margin: 0 auto; }
226
+ .dark .gradio-container { color: var(--body-text-color); }
227
+ """
228
+
229
+ with gr.Blocks(title="SenseNova-U1.5-8B-MoT") as demo:
230
+ gr.Markdown(
231
+ """
232
+ # SenseNova-U1.5-8B-MoT
233
+
234
+ Unified text-to-image **and** image editing with
235
+ [**SenseNova-U1.5-8B-MoT**](https://huggingface.co/sensenova/SenseNova-U1.5-8B-MoT),
236
+ a natively unified multimodal model built on the
237
+ [NEO-unify](https://huggingface.co/blog/sensenova/neo-unify) architecture.
238
+ Leave the image upload empty for text-to-image, or upload an image and
239
+ write an edit instruction.
240
+ """
241
+ )
242
+
243
+ with gr.Row():
244
+ with gr.Column(scale=1):
245
+ image_input_gallery = gr.Gallery(
246
+ label="Upload image(s) to edit (leave empty for text-to-image)",
247
+ file_types=["image"],
248
+ height=200,
249
+ columns=4,
250
+ )
251
+ prompt_input = gr.Textbox(
252
+ label="Prompt",
253
+ placeholder="Describe the image to generate, or how to edit your input.",
254
+ lines=3,
255
+ )
256
+ aspect_ratio = gr.Dropdown(
257
+ label="Aspect ratio (text-to-image only — editing keeps input ratio)",
258
+ choices=list(T2I_RESOLUTIONS.keys()),
259
+ value="1:1",
260
+ )
261
+ with gr.Row():
262
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42)
263
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
264
+ generate_button = gr.Button("Generate", variant="primary")
265
+ with gr.Column(scale=1):
266
+ output_image = gr.Image(label="Output", type="pil", format="png", interactive=False)
267
+ used_seed = gr.Number(label="Seed used", interactive=False)
268
+
269
+ with gr.Accordion("Text-to-Image Examples", open=True):
270
+ gr.Examples(
271
+ examples=T2I_EXAMPLES,
272
+ inputs=[prompt_input, aspect_ratio],
273
+ outputs=[output_image, used_seed],
274
+ fn=generate,
275
+ cache_examples=False,
276
+ run_on_click=True,
277
+ )
278
+
279
+ with gr.Accordion("Image Editing Examples", open=True):
280
+ gr.Examples(
281
+ examples=EDIT_EXAMPLES,
282
+ inputs=[image_input_gallery, prompt_input],
283
+ outputs=[output_image, used_seed],
284
+ fn=generate,
285
+ cache_examples=False,
286
+ run_on_click=True,
287
+ )
288
+
289
+ generate_button.click(
290
+ fn=generate,
291
+ inputs=[image_input_gallery, prompt_input, aspect_ratio, seed, randomize_seed],
292
+ outputs=[output_image, used_seed],
293
+ api_name="generate",
294
+ )
295
+ prompt_input.submit(
296
+ fn=generate,
297
+ inputs=[image_input_gallery, prompt_input, aspect_ratio, seed, randomize_seed],
298
+ outputs=[output_image, used_seed],
299
+ api_name="generate_submit",
300
+ )
301
+
302
+
303
+ if __name__ == "__main__":
304
+ demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)