gheinrich commited on
Commit
904bf44
·
1 Parent(s): 245d8da

Add Gradio T2I demo with Nemotron 3.5 guard, model write-up, and figures

Browse files

- Gradio Space app for nvidia/NL-Diffusion-Image (text-to-image)
- NSFW guard via nvidia/Nemotron-3.5-Content-Safety (EU-accessible)
- About-the-model section with sanitized announcement content + assets
- NVIDIA-green theme; PNG/GIF assets via Git LFS; ignore internal/local artifacts

.gitattributes CHANGED
@@ -4,6 +4,11 @@
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
 
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
+ *.gif filter=lfs diff=lfs merge=lfs -text
8
+ *.png filter=lfs diff=lfs merge=lfs -text
9
+ *.jpg filter=lfs diff=lfs merge=lfs -text
10
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
11
+ *.webp filter=lfs diff=lfs merge=lfs -text
12
  *.gz filter=lfs diff=lfs merge=lfs -text
13
  *.h5 filter=lfs diff=lfs merge=lfs -text
14
  *.joblib filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Internal announcement source (do not publish)
2
+ Nemotron Labs Diffusion Image - IPR.zip
3
+ announcement_extract/
4
+
5
+ # Local artifacts
6
+ __pycache__/
7
+ *.pyc
8
+ log.txt
9
+ test_hub_output.webp
app.py ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NL-Diffusion-Image Gradio demo for Hugging Face Spaces (and local testing).
2
+
3
+ Local test with private Hub model:
4
+
5
+ conda activate lavida
6
+ export HF_TOKEN=hf_...
7
+ export MODEL_ID=nvidia/NL-Diffusion-Image
8
+ python app.py
9
+
10
+ Post-generation NSFW guard (on by default — opt out with ENABLE_IMAGE_GUARD=0):
11
+
12
+ export ENABLE_IMAGE_GUARD=0
13
+ python app.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import gc
19
+ import os
20
+ import tempfile
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ ASSETS_DIR = Path(__file__).resolve().parent / "assets"
26
+
27
+
28
+ def _asset(name: str) -> str:
29
+ return str(ASSETS_DIR / name)
30
+
31
+ import gradio as gr
32
+ import imageio.v3 as iio
33
+ import torch
34
+ from PIL import ImageDraw
35
+ from transformers import AutoModelForCausalLM, PreTrainedTokenizerFast
36
+
37
+ from image_guard import (
38
+ DEFAULT_IMAGE_GUARD_MODEL_ID,
39
+ DEFAULT_IMAGE_GUARD_THRESHOLD,
40
+ ImageGuard,
41
+ )
42
+
43
+ try:
44
+ import spaces
45
+ except ImportError:
46
+
47
+ class _SpacesStub:
48
+ @staticmethod
49
+ def GPU(*args, **kwargs):
50
+ def decorator(fn):
51
+ return fn
52
+
53
+ if args and callable(args[0]):
54
+ return args[0]
55
+ return decorator
56
+
57
+ spaces = _SpacesStub()
58
+
59
+ os.environ.setdefault("DEBUG_FIX_PADDING", "1")
60
+ os.environ.setdefault("NOT_ALWASY_DO_2DPOOL", "1")
61
+
62
+ if "CUDA_HOME" not in os.environ:
63
+ _local_cuda = "/lustre/fsw/portfolios/llmservice/users/gheinrich/cuda/cuda_12.4"
64
+ if os.path.isdir(_local_cuda):
65
+ os.environ["CUDA_HOME"] = _local_cuda
66
+
67
+ MODEL_ID = os.getenv("MODEL_ID", "nvidia/NL-Diffusion-Image")
68
+ DEVICE = os.getenv("DEVICE", "cuda")
69
+ IMAGE_GUARD_MODEL_ID = os.getenv("IMAGE_GUARD_MODEL_ID", DEFAULT_IMAGE_GUARD_MODEL_ID)
70
+ IMAGE_GUARD_THRESHOLD = float(
71
+ os.getenv("IMAGE_GUARD_THRESHOLD", str(DEFAULT_IMAGE_GUARD_THRESHOLD))
72
+ )
73
+ IMAGE_GUARD_OFFLOAD_T2I = os.getenv("IMAGE_GUARD_OFFLOAD_T2I", "0") == "1"
74
+ # Opt-out: guard runs by default; set ENABLE_IMAGE_GUARD=0 or uncheck the UI box to disable.
75
+ DEFAULT_ENABLE_IMAGE_GUARD = os.getenv("ENABLE_IMAGE_GUARD", "1") == "1"
76
+
77
+ GUARD_ACCESS_HELP = (
78
+ "https://huggingface.co/nvidia/Nemotron-3.5-Content-Safety"
79
+ )
80
+
81
+
82
+ def _guard_unavailable_message(exc: Exception) -> str:
83
+ text = str(exc).lower()
84
+ if "gated repo" in text or "403" in text or "authorized list" in text:
85
+ return (
86
+ "NSFW filter is enabled but Nemotron 3.5 Content Safety is not accessible. "
87
+ f"See {GUARD_ACCESS_HELP}, ensure HF_TOKEN has read access, "
88
+ "or uncheck 'NSFW output filter' to opt out."
89
+ )
90
+ return f"NSFW filter is enabled but Nemotron 3.5 Content Safety failed: {exc}"
91
+
92
+
93
+ def _report_guard_failure(message: str) -> None:
94
+ """Surface guard failures in the Gradio UI without breaking output components."""
95
+ gr.Warning(message)
96
+ print(f"GUARD ERROR: {message}", flush=True)
97
+
98
+ # Defaults aligned with nemotron-diffusion-omni/gradio_t2i_demo.py
99
+ DEFAULT_PROMPT = (
100
+ "A full-body shot of hyper-realistic female cyborg, human facial skin seamlessly integrated "
101
+ "with a glossy white mechanical head shell. Features a realistic human ear, blue eyes. bright, "
102
+ "outdoor, background with blue sky, illuminated by striking bright white studio lighting, "
103
+ "casting soft shadows. Cyberpunk aesthetic, high-tech minimalism, shot on 85mm lens, "
104
+ "photorealistic, Unreal Engine 5 render, intricately detailed, 8k resolution, high dynamic "
105
+ "range, chest with whit armor plate, cute, beautiful, sexy, glossy surface, reflective, "
106
+ "Artstation, pixiv, no hair, 3D render, stylized eyesz"
107
+ )
108
+ DEFAULT_MICRO_COND = (
109
+ "ORIGINAL WIDTH : 1024; ORIGINAL HEIGHT : 1024; TOP : 0; LEFT : 0; "
110
+ "SCORE : 6.520; HPS: 3.220"
111
+ )
112
+
113
+ DEFAULT_GENERATION_CONFIG: dict[str, Any] = {
114
+ "guidance_scale": 5.0,
115
+ "n_steps": 64,
116
+ "shift": 5,
117
+ "schedule": "shift",
118
+ "alg_temp": 1.0,
119
+ "dynamic_temperature": False,
120
+ "min_temperature": 0.01,
121
+ "schedule_temp": "linear",
122
+ "temperature": 0.86,
123
+ "confidence_policy": "mmada",
124
+ "micro_cond": DEFAULT_MICRO_COND,
125
+ "edit_threshold": 0.6,
126
+ "block_policy": 2,
127
+ "is_legacy": False,
128
+ "use_cache": False,
129
+ }
130
+
131
+
132
+ def n_tokens_from_resolution(image_resolution: int) -> int:
133
+ return (image_resolution // 16) * (image_resolution // 16)
134
+
135
+
136
+ def process_gif(image_list):
137
+ if not image_list:
138
+ return None
139
+
140
+ with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp_file:
141
+ gif_path = tmp_file.name
142
+
143
+ frames = []
144
+ total_frames = len(image_list)
145
+ for i, img in enumerate(image_list):
146
+ frame = img.resize((400, 400))
147
+ draw = ImageDraw.Draw(frame)
148
+ text = f"Frame: {i + 1} / {total_frames}"
149
+ x, y = 15, 15
150
+ for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
151
+ draw.text((x + dx, y + dy), text, fill="black")
152
+ draw.text((x, y), text, fill="white")
153
+ frames.append(frame)
154
+
155
+ duration = [1000 / 20] * len(frames)
156
+ duration[-1] = 2000
157
+ iio.imwrite(gif_path, frames, extension=".gif", duration=duration, loop=0)
158
+ return gif_path
159
+
160
+
161
+ def process_webp(pil_image):
162
+ with tempfile.NamedTemporaryFile(suffix=".webp", delete=False) as tmp_file:
163
+ webp_path = tmp_file.name
164
+ pil_image.save(webp_path, "webp", quality=95)
165
+ return webp_path
166
+
167
+
168
+ def load_release_model_and_tokenizer(model_id: str, device: str):
169
+ hf_token = os.getenv("HF_TOKEN")
170
+ tokenizer = PreTrainedTokenizerFast.from_pretrained(model_id, token=hf_token)
171
+ if tokenizer.pad_token_id is None:
172
+ tokenizer.pad_token_id = tokenizer.eos_token_id
173
+ tokenizer.pad_token = tokenizer.eos_token
174
+
175
+ model = AutoModelForCausalLM.from_pretrained(
176
+ model_id,
177
+ trust_remote_code=True,
178
+ torch_dtype=torch.bfloat16,
179
+ low_cpu_mem_usage=False,
180
+ token=hf_token,
181
+ )
182
+ model.to(device)
183
+ model.eval()
184
+ model.requires_grad_(False)
185
+ model.config.dlm_paradigm = "bidirectional"
186
+ return tokenizer, model
187
+
188
+
189
+ def _format_guard_meta(result) -> str:
190
+ return (
191
+ f"guard={result.model_id} | label={result.label} | "
192
+ f"unsafe_score={result.score:.3f} | guard_time={result.inference_seconds:.2f}s"
193
+ )
194
+
195
+
196
+ class T2IEngine:
197
+ def __init__(self, model_id: str, device: str = "cuda") -> None:
198
+ self.model_id = model_id
199
+ self.device = device
200
+ self._tokenizer = None
201
+ self._model = None
202
+ self._image_guard: ImageGuard | None = None
203
+
204
+ def _lazy_load(self) -> None:
205
+ if self._model is not None and self._tokenizer is not None:
206
+ return
207
+ print(f"Loading model from {self.model_id} ...", flush=True)
208
+ self._tokenizer, self._model = load_release_model_and_tokenizer(
209
+ self.model_id, self.device
210
+ )
211
+ print("Model loaded.", flush=True)
212
+
213
+ def _get_image_guard(self) -> ImageGuard:
214
+ if self._image_guard is None:
215
+ print(f"Loading image guard from {IMAGE_GUARD_MODEL_ID} ...", flush=True)
216
+ self._image_guard = ImageGuard(
217
+ model_id=IMAGE_GUARD_MODEL_ID,
218
+ threshold=IMAGE_GUARD_THRESHOLD,
219
+ device=self.device,
220
+ )
221
+ return self._image_guard
222
+
223
+ def _offload_t2i_to_cpu(self) -> None:
224
+ if self._model is not None:
225
+ self._model.to("cpu")
226
+ gc.collect()
227
+ if torch.cuda.is_available():
228
+ torch.cuda.empty_cache()
229
+
230
+ def _reload_t2i_to_device(self) -> None:
231
+ if self._model is not None:
232
+ self._model.to(self.device)
233
+
234
+ def _moderate_output(
235
+ self,
236
+ result,
237
+ return_animation: bool,
238
+ enable_image_guard: bool,
239
+ ) -> tuple[bool, str]:
240
+ """Run post-generation moderation. Returns (ok, meta_suffix_or_error_message)."""
241
+ if not enable_image_guard:
242
+ return True, ""
243
+
244
+ if IMAGE_GUARD_OFFLOAD_T2I:
245
+ self._offload_t2i_to_cpu()
246
+
247
+ try:
248
+ guard = self._get_image_guard()
249
+ frames = result if return_animation else [result]
250
+ guard_parts = []
251
+ for frame_idx, frame in enumerate(frames):
252
+ check = guard.check_image(frame)
253
+ guard_parts.append(_format_guard_meta(check))
254
+ if not check.passed:
255
+ message = (
256
+ "Generated image blocked by NSFW filter "
257
+ f"(frame {frame_idx + 1}/{len(frames)}, "
258
+ f"unsafe_score={check.score:.3f}, threshold={IMAGE_GUARD_THRESHOLD})."
259
+ )
260
+ _report_guard_failure(message)
261
+ return False, message
262
+ return True, " | " + guard_parts[0] if guard_parts else ""
263
+ except Exception as exc:
264
+ message = _guard_unavailable_message(exc)
265
+ _report_guard_failure(message)
266
+ return False, message
267
+ finally:
268
+ if IMAGE_GUARD_OFFLOAD_T2I:
269
+ self._reload_t2i_to_device()
270
+
271
+ def generate(
272
+ self,
273
+ prompt: str,
274
+ image_resolution: int,
275
+ guidance_scale: float,
276
+ temperature: float,
277
+ n_steps: int,
278
+ schedule: str,
279
+ shift: int,
280
+ confidence_policy: str,
281
+ schedule_temp: str,
282
+ alg_temp: float,
283
+ dynamic_temperature: bool,
284
+ min_temperature: float,
285
+ edit_threshold: float,
286
+ seed: int,
287
+ micro_cond: str,
288
+ use_cache: bool,
289
+ is_legacy: bool,
290
+ return_animation: bool,
291
+ enable_image_guard: bool,
292
+ ):
293
+ self._lazy_load()
294
+
295
+ gen_cfg = dict(DEFAULT_GENERATION_CONFIG)
296
+ gen_cfg.update(
297
+ micro_cond=micro_cond,
298
+ guidance_scale=guidance_scale,
299
+ temperature=temperature,
300
+ edit_threshold=edit_threshold,
301
+ n_steps=int(n_steps),
302
+ schedule=schedule,
303
+ shift=int(shift),
304
+ confidence_policy=confidence_policy,
305
+ schedule_temp=schedule_temp,
306
+ alg_temp=alg_temp,
307
+ dynamic_temperature=dynamic_temperature,
308
+ min_temperature=min_temperature,
309
+ block_policy=2,
310
+ is_legacy=is_legacy,
311
+ use_cache=use_cache,
312
+ )
313
+
314
+ if seed < 0:
315
+ seed = int(torch.seed() % (2**31 - 1))
316
+ torch.manual_seed(int(seed))
317
+
318
+ n_tokens = n_tokens_from_resolution(int(image_resolution))
319
+
320
+ t0 = time.time()
321
+ with torch.no_grad():
322
+ with torch.inference_mode():
323
+ result = self._model.text_to_image(
324
+ prompt,
325
+ tokenizer=self._tokenizer,
326
+ **gen_cfg,
327
+ image_resolution=int(image_resolution),
328
+ n_tokens=n_tokens,
329
+ disable_tqdm=True,
330
+ return_intermediate_steps=return_animation,
331
+ )
332
+ latency = time.time() - t0
333
+
334
+ meta = (
335
+ f"model={self.model_id} | seed={seed} | res={image_resolution} | "
336
+ f"n_tokens={n_tokens} | steps={n_steps} | "
337
+ f"cfg={guidance_scale:.2f} | temp={temperature:.3f} | "
338
+ f"schedule={schedule} | shift={shift} | alg_temp={alg_temp:.2f} | "
339
+ f"dyn_temp={dynamic_temperature} | min_temp={min_temperature:.3f} | "
340
+ f"sch_temp={schedule_temp} | conf={confidence_policy} | "
341
+ f"edit_threshold={edit_threshold:.3f} | gen_time={latency:.2f}s"
342
+ )
343
+
344
+ guard_ok, guard_meta = self._moderate_output(
345
+ result, return_animation, enable_image_guard
346
+ )
347
+ if not guard_ok:
348
+ return None, f"ERROR: {guard_meta}\n\n{meta}"
349
+
350
+ meta += guard_meta
351
+
352
+ if return_animation:
353
+ return process_gif(result), meta
354
+ return process_webp(result), meta
355
+
356
+
357
+ engine = T2IEngine(model_id=MODEL_ID, device=DEVICE)
358
+
359
+
360
+ @spaces.GPU(duration=240)
361
+ def generate(
362
+ prompt: str,
363
+ image_resolution: int,
364
+ guidance_scale: float,
365
+ temperature: float,
366
+ n_steps: int,
367
+ schedule: str,
368
+ shift: int,
369
+ confidence_policy: str,
370
+ schedule_temp: str,
371
+ alg_temp: float,
372
+ dynamic_temperature: bool,
373
+ min_temperature: float,
374
+ edit_threshold: float,
375
+ seed: int,
376
+ micro_cond: str,
377
+ use_cache: bool,
378
+ is_legacy: bool,
379
+ return_animation: bool,
380
+ enable_image_guard: bool,
381
+ ):
382
+ return engine.generate(
383
+ prompt,
384
+ image_resolution,
385
+ guidance_scale,
386
+ temperature,
387
+ n_steps,
388
+ schedule,
389
+ shift,
390
+ confidence_policy,
391
+ schedule_temp,
392
+ alg_temp,
393
+ dynamic_temperature,
394
+ min_temperature,
395
+ edit_threshold,
396
+ seed,
397
+ micro_cond,
398
+ use_cache,
399
+ is_legacy,
400
+ return_animation,
401
+ enable_image_guard,
402
+ )
403
+
404
+
405
+ def build_demo() -> gr.Blocks:
406
+ nvidia_green = gr.themes.Color(
407
+ c50="#f3f9e6",
408
+ c100="#e3f1c2",
409
+ c200="#cfe88f",
410
+ c300="#b6dc56",
411
+ c400="#97c61f",
412
+ c500="#76b900",
413
+ c600="#69a600",
414
+ c700="#548400",
415
+ c800="#3f6300",
416
+ c900="#2a4200",
417
+ c950="#1a2900",
418
+ )
419
+ theme = gr.themes.Soft(primary_hue=nvidia_green, secondary_hue=nvidia_green)
420
+ with gr.Blocks(title="Nemotron Labs Diffusion Image", theme=theme) as demo:
421
+ gr.Markdown(
422
+ "# Nemotron Labs Diffusion Image\n\n"
423
+ "NL-Diffusion-Image generates high-resolution images via **masked discrete diffusion** "
424
+ "over tokenized image patches. Each image is encoded into discrete tokens "
425
+ "(131K codebook), and generation proceeds through iterative parallel unmasking—similar "
426
+ "to diffusion LLMs. The model is fine-tuned from "
427
+ "[Nemotron-Labs-Diffusion](https://huggingface.co/nvidia/Nemotron-Labs-Diffusion-8B) "
428
+ "with two key additions:\n\n"
429
+ "- **Token editing** — revise already-unmasked tokens during inference.\n"
430
+ "- **Grouped Cross-Entropy (GCE)** — efficient training with large vocabularies.\n\n"
431
+ "This aligns image generation with LLM training and inference infrastructure, "
432
+ "making it highly scalable.\n\n"
433
+ f"**Model:** [`{MODEL_ID}`](https://huggingface.co/nvidia/NL-Diffusion-Image)"
434
+ )
435
+
436
+ gr.Markdown(
437
+ "| GenEval | DPG | HPSv3 | Speed vs EMU3.5 |\n"
438
+ "|:---:|:---:|:---:|:---:|\n"
439
+ "| **0.90** | **86.9** | **10.76** | **42.4× faster** |"
440
+ )
441
+
442
+ gr.Markdown("## Generate an image")
443
+
444
+ with gr.Row():
445
+ with gr.Column(scale=2):
446
+ prompt = gr.Textbox(label="Prompt", lines=4, value=DEFAULT_PROMPT)
447
+ micro_cond = gr.Textbox(label="Micro Cond", lines=2, value=DEFAULT_MICRO_COND)
448
+ with gr.Row():
449
+ image_resolution = gr.Dropdown(
450
+ choices=[256, 512, 1024],
451
+ value=1024,
452
+ label="Image Resolution",
453
+ )
454
+ n_steps = gr.Slider(
455
+ minimum=1, maximum=128, value=64, step=1, label="Diffusion Steps"
456
+ )
457
+ with gr.Row():
458
+ guidance_scale = gr.Slider(
459
+ minimum=1.0, maximum=10.0, value=5.0, step=0.1, label="Guidance Scale"
460
+ )
461
+ temperature = gr.Slider(
462
+ minimum=0.05, maximum=1.5, value=0.86, step=0.01, label="Temperature"
463
+ )
464
+ with gr.Row():
465
+ schedule = gr.Dropdown(choices=["shift"], value="shift", label="Schedule")
466
+ shift = gr.Slider(minimum=0, maximum=16, value=5, step=1, label="Shift")
467
+ confidence_policy = gr.Dropdown(
468
+ choices=["mask_git", "mmada", "stratified"],
469
+ value="mmada",
470
+ label="Confidence Policy",
471
+ )
472
+ with gr.Row():
473
+ schedule_temp = gr.Dropdown(
474
+ choices=["linear", "cosine2", "shift", "exp"],
475
+ value="linear",
476
+ label="Schedule Temp",
477
+ )
478
+ alg_temp = gr.Slider(
479
+ minimum=0.1, maximum=3.0, value=1.0, step=0.1, label="Alg Temp"
480
+ )
481
+ dynamic_temperature = gr.Checkbox(label="Dynamic Temp", value=False)
482
+ with gr.Row():
483
+ min_temperature = gr.Slider(
484
+ minimum=0.0, maximum=1.0, value=0.01, step=0.01, label="Min Temp"
485
+ )
486
+ edit_threshold = gr.Slider(
487
+ minimum=-1.0, maximum=1.0, value=0.6, step=0.01, label="Edit Threshold"
488
+ )
489
+ seed = gr.Number(label="Seed (-1 for random)", value=42, precision=0)
490
+ with gr.Row():
491
+ use_cache = gr.Checkbox(label="Use Cache", value=False)
492
+ is_legacy = gr.Checkbox(label="Legacy Mode", value=False)
493
+ return_animation = gr.Checkbox(
494
+ label="Return Animation (resized to 400x400 for preview)",
495
+ value=False,
496
+ )
497
+ enable_image_guard = gr.Checkbox(
498
+ label="NSFW output filter (Nemotron 3.5 Content Safety) — uncheck to opt out",
499
+ value=DEFAULT_ENABLE_IMAGE_GUARD,
500
+ )
501
+
502
+ generate_btn = gr.Button("Generate", variant="primary")
503
+
504
+ with gr.Column(scale=3):
505
+ output_image = gr.Image(label="Generated Image", type="filepath")
506
+ output_meta = gr.Textbox(label="Generation Info", lines=6)
507
+
508
+ generate_btn.click(
509
+ fn=generate,
510
+ inputs=[
511
+ prompt,
512
+ image_resolution,
513
+ guidance_scale,
514
+ temperature,
515
+ n_steps,
516
+ schedule,
517
+ shift,
518
+ confidence_policy,
519
+ schedule_temp,
520
+ alg_temp,
521
+ dynamic_temperature,
522
+ min_temperature,
523
+ edit_threshold,
524
+ seed,
525
+ micro_cond,
526
+ use_cache,
527
+ is_legacy,
528
+ return_animation,
529
+ enable_image_guard,
530
+ ],
531
+ outputs=[output_image, output_meta],
532
+ )
533
+
534
+ with gr.Accordion("About the model", open=True):
535
+ gr.Markdown(
536
+ "_Masked Discrete Diffusion · Text-to-Image Synthesis · Token Editing · "
537
+ "Grouped Cross-Entropy (GCE) · High-Resolution Image Generation_"
538
+ )
539
+
540
+ gr.Markdown("### Sample outputs")
541
+ gr.Gallery(
542
+ value=[
543
+ _asset("demo_1.gif"),
544
+ _asset("demo_2.gif"),
545
+ _asset("demo_3.gif"),
546
+ ],
547
+ columns=3,
548
+ height="auto",
549
+ object_fit="contain",
550
+ show_label=False,
551
+ show_download_button=True,
552
+ )
553
+
554
+ gr.Markdown(
555
+ "### Generation speed\n\n"
556
+ "Side-by-side at 1024×1024. **Left:** NL-Diffusion-Image. "
557
+ "**Right:** EMU3.5 (autoregressive). NL-Diffusion-Image is **42.4× faster** "
558
+ "while scoring higher on GenEval."
559
+ )
560
+ gr.Image(
561
+ value=_asset("speed_comparison.gif"),
562
+ show_label=False,
563
+ interactive=False,
564
+ show_download_button=True,
565
+ )
566
+
567
+ with gr.Row(equal_height=True):
568
+ with gr.Column(scale=1):
569
+ gr.Markdown(
570
+ "### Architecture\n\n"
571
+ "16×16 image patches are encoded with a pretrained discrete tokenizer "
572
+ "from EMU3.5 (128K codebook). The Nemotron-Labs-Diffusion vocabulary is "
573
+ "expanded with randomly initialized embeddings and fine-tuned on "
574
+ "image/caption pairs."
575
+ )
576
+ with gr.Column(scale=1):
577
+ gr.Image(
578
+ value=_asset("architecture.png"),
579
+ show_label=False,
580
+ interactive=False,
581
+ show_download_button=True,
582
+ )
583
+
584
+ with gr.Row(equal_height=True):
585
+ with gr.Column(scale=1):
586
+ gr.Markdown(
587
+ "### Benchmarks\n\n"
588
+ "State-of-the-art among discrete image generators at 1024px text-to-image, "
589
+ "surpassing prior masked image generators on quality while remaining "
590
+ "dramatically faster than autoregressive baselines."
591
+ )
592
+ with gr.Column(scale=1):
593
+ gr.Image(
594
+ value=_asset("benchmarks.png"),
595
+ show_label=False,
596
+ interactive=False,
597
+ show_download_button=True,
598
+ )
599
+
600
+ gr.Markdown("### Key findings")
601
+
602
+ with gr.Row(equal_height=True):
603
+ with gr.Column(scale=1):
604
+ gr.Markdown(
605
+ "**Token editing for self-correction**\n\n"
606
+ "Token editing lets the model iteratively refine outputs during inference, "
607
+ "correcting artifacts and improving texture detail."
608
+ )
609
+ with gr.Column(scale=1):
610
+ gr.Image(
611
+ value=_asset("token_editing.png"),
612
+ show_label=False,
613
+ interactive=False,
614
+ show_download_button=True,
615
+ )
616
+
617
+ with gr.Row(equal_height=True):
618
+ with gr.Column(scale=1):
619
+ gr.Markdown(
620
+ "**Grouped Cross-Entropy (GCE)**\n\n"
621
+ "GCE alleviates codebook sparsity by supervising semantically close "
622
+ "non-top-1 tokens in embedding space.\n\n"
623
+ "A fused GCE operator cuts peak VRAM from 25.2 GB to 16.1 GB and latency "
624
+ "from 44.14 ms to 20.04 ms versus an eager implementation."
625
+ )
626
+ with gr.Column(scale=1):
627
+ gr.Image(
628
+ value=_asset("gce_objective.png"),
629
+ show_label=False,
630
+ interactive=False,
631
+ show_download_button=True,
632
+ )
633
+
634
+ with gr.Row(equal_height=True):
635
+ with gr.Column(scale=1):
636
+ gr.Markdown(
637
+ "**Few-step generation**\n\n"
638
+ "Unlike continuous flow-matching models that predict blurry mean fields at "
639
+ "low step counts, NL-Diffusion-Image produces reasonable quality in as few "
640
+ "as 4 steps without distillation."
641
+ )
642
+ with gr.Column(scale=1):
643
+ gr.Image(
644
+ value=_asset("few_step_generation.png"),
645
+ show_label=False,
646
+ interactive=False,
647
+ show_download_button=True,
648
+ )
649
+
650
+ gr.Markdown(
651
+ "**Future work:** extend the model to unified vision generation and understanding."
652
+ )
653
+
654
+ return demo
655
+
656
+
657
+ demo = build_demo()
658
+
659
+ if __name__ == "__main__":
660
+ demo.queue(default_concurrency_limit=1).launch(
661
+ server_name=os.getenv("HOST", "0.0.0.0"),
662
+ server_port=int(os.getenv("PORT", "7860")),
663
+ )
assets/architecture.png ADDED

Git LFS Details

  • SHA256: b01bd622def56b0fff996f5cfdf88d4a6bdadd49c71c2e0301b441f416df58aa
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB
assets/benchmarks.png ADDED

Git LFS Details

  • SHA256: 4ba303dd1e92ccb2653e49e9c0b3dcbbcde3dd86d7ff174b395d9e4e5efb4381
  • Pointer size: 132 Bytes
  • Size of remote file: 1.08 MB
assets/demo_1.gif ADDED

Git LFS Details

  • SHA256: 4e9ce93d1608863cc209c575a49235854d52abadda4ee8a3dc992694b1a5ac91
  • Pointer size: 133 Bytes
  • Size of remote file: 24.5 MB
assets/demo_2.gif ADDED

Git LFS Details

  • SHA256: 949a0d20dd043ef88b0dbe9cbb31a1c865d4076d26e891770174acad8af45098
  • Pointer size: 133 Bytes
  • Size of remote file: 23.7 MB
assets/demo_3.gif ADDED

Git LFS Details

  • SHA256: 4e0733da54e6b4d95084d9400b04c9ff6b5f40769edca2c1b3cc9b43a895d924
  • Pointer size: 133 Bytes
  • Size of remote file: 21.9 MB
assets/few_step_generation.png ADDED

Git LFS Details

  • SHA256: 6d43e9ea9f84e2a5c797056a7633e212c3e53f3991449c537f74b6cb9ac68ad8
  • Pointer size: 131 Bytes
  • Size of remote file: 877 kB
assets/gce_objective.png ADDED

Git LFS Details

  • SHA256: a9ed9a553c19f5b57dc6ba5fd454ea5da6d9951eaa3fa62eb1c956d75f9840d4
  • Pointer size: 131 Bytes
  • Size of remote file: 373 kB
assets/speed_comparison.gif ADDED

Git LFS Details

  • SHA256: 1693514926657993fe43521490d4d62515a094a8b6d3a53233e6f1d3a9b8fa54
  • Pointer size: 133 Bytes
  • Size of remote file: 15.9 MB
assets/token_editing.png ADDED

Git LFS Details

  • SHA256: 34c7afd46564cc0d9fa2ad23e1d3fee1152d6fb4400ea9c0ab86e6ea23904981
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
image_guard.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Adapted from NVIDIA asset-harvester (asset_harvester/utils/image_guard.py).
5
+
6
+ """Image moderation utility backed by Nemotron 3.5 Content Safety."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import gc
12
+ import io
13
+ import os
14
+ import re
15
+ import time
16
+ from dataclasses import asdict, dataclass
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ import torch
21
+ from PIL import Image
22
+ from transformers import AutoProcessor, Gemma3ForConditionalGeneration
23
+
24
+ DEFAULT_IMAGE_GUARD_MODEL_ID = "nvidia/Nemotron-3.5-Content-Safety"
25
+ DEFAULT_IMAGE_GUARD_THRESHOLD = 0.5
26
+ _MAX_NEW_TOKENS = 100
27
+ _MODERATION_PROMPT = (
28
+ "Evaluate whether this image is safe to display in a public text-to-image demo."
29
+ )
30
+ _USER_SAFETY_RE = re.compile(r"^\s*user safety:\s*(safe|unsafe)\s*$", re.IGNORECASE)
31
+
32
+
33
+ class ImageGuardError(RuntimeError):
34
+ """Raised when the content-safety model returns an unexpected moderation result."""
35
+
36
+
37
+ @dataclass
38
+ class ImageGuardResult:
39
+ passed: bool
40
+ score: float
41
+ label: str
42
+ raw_response: str
43
+ model_id: str
44
+ inference_seconds: float
45
+
46
+ def to_dict(self) -> dict:
47
+ return asdict(self)
48
+
49
+
50
+ class ImageGuard:
51
+ """Lazy-loading wrapper around Nemotron 3.5 Content Safety."""
52
+
53
+ def __init__(
54
+ self,
55
+ model_id: str = DEFAULT_IMAGE_GUARD_MODEL_ID,
56
+ threshold: float = DEFAULT_IMAGE_GUARD_THRESHOLD,
57
+ device: str | torch.device | None = None,
58
+ dtype: torch.dtype | None = None,
59
+ hf_token: str | None = None,
60
+ ) -> None:
61
+ if device is None:
62
+ device = "cuda" if torch.cuda.is_available() else "cpu"
63
+ self.device = torch.device(device)
64
+ if dtype is None:
65
+ if self.device.type == "cuda" and torch.cuda.is_bf16_supported():
66
+ dtype = torch.bfloat16
67
+ elif self.device.type == "cuda":
68
+ dtype = torch.float16
69
+ else:
70
+ dtype = torch.float32
71
+ self.dtype = dtype
72
+ self.model_id = model_id
73
+ self.threshold = threshold
74
+ self.hf_token = hf_token or os.getenv("HF_TOKEN")
75
+ self._processor = None
76
+ self._model = None
77
+
78
+ def _load(self) -> None:
79
+ if self._processor is not None and self._model is not None:
80
+ return
81
+
82
+ self._processor = AutoProcessor.from_pretrained(
83
+ self.model_id, token=self.hf_token
84
+ )
85
+ self._model = Gemma3ForConditionalGeneration.from_pretrained(
86
+ self.model_id,
87
+ torch_dtype=self.dtype,
88
+ token=self.hf_token,
89
+ ).to(self.device)
90
+ self._model.eval()
91
+
92
+ def load(self) -> None:
93
+ self._load()
94
+
95
+ def unload(self) -> None:
96
+ if self._model is not None:
97
+ self._model.to("cpu")
98
+ self._processor = None
99
+ self._model = None
100
+ gc.collect()
101
+ if torch.cuda.is_available():
102
+ torch.cuda.empty_cache()
103
+
104
+ def check_image(self, image: str | Path | Image.Image | np.ndarray) -> ImageGuardResult:
105
+ self._load()
106
+ image_pil = self._coerce_image(image)
107
+ start_time = time.perf_counter()
108
+ text = self._generate_response(image_pil)
109
+ inference_seconds = time.perf_counter() - start_time
110
+ label, score = self._parse_response(text)
111
+ return ImageGuardResult(
112
+ passed=label == "safe" and score < self.threshold,
113
+ score=score,
114
+ label=label,
115
+ raw_response=text,
116
+ model_id=self.model_id,
117
+ inference_seconds=inference_seconds,
118
+ )
119
+
120
+ def _generate_response(self, image: Image.Image) -> str:
121
+ messages = [
122
+ {
123
+ "role": "user",
124
+ "content": [
125
+ self._image_to_message_content(image),
126
+ {"type": "text", "text": _MODERATION_PROMPT},
127
+ ],
128
+ }
129
+ ]
130
+ inputs = self._processor.apply_chat_template(
131
+ messages,
132
+ add_generation_prompt=True,
133
+ tokenize=True,
134
+ return_dict=True,
135
+ return_tensors="pt",
136
+ request_categories="/categories",
137
+ enable_thinking=False,
138
+ ).to(self.device)
139
+
140
+ input_len = inputs["input_ids"].shape[-1]
141
+ with torch.inference_mode():
142
+ generation = self._model.generate(
143
+ **inputs,
144
+ max_new_tokens=_MAX_NEW_TOKENS,
145
+ do_sample=False,
146
+ )
147
+ generation = generation[0][input_len:]
148
+
149
+ return self._processor.decode(generation, skip_special_tokens=True).strip()
150
+
151
+ @staticmethod
152
+ def _image_to_message_content(image: Image.Image) -> dict[str, str]:
153
+ img_bytes = io.BytesIO()
154
+ image.save(img_bytes, format="JPEG")
155
+ return {
156
+ "type": "image",
157
+ "image": base64.b64encode(img_bytes.getvalue()).decode("utf-8"),
158
+ }
159
+
160
+ @staticmethod
161
+ def _parse_response(text: str) -> tuple[str, float]:
162
+ for line in text.splitlines():
163
+ match = _USER_SAFETY_RE.match(line)
164
+ if match:
165
+ label = match.group(1).lower()
166
+ score = 1.0 if label == "unsafe" else 0.0
167
+ return label, score
168
+
169
+ normalized = text.strip().lower()
170
+ if "user safety: unsafe" in normalized or normalized.startswith("unsafe"):
171
+ return "unsafe", 1.0
172
+ if "user safety: safe" in normalized or normalized.startswith("safe"):
173
+ return "safe", 0.0
174
+ raise ImageGuardError(f"Unexpected image guard response: {text!r}")
175
+
176
+ @staticmethod
177
+ def _coerce_image(image: str | Path | Image.Image | np.ndarray) -> Image.Image:
178
+ if isinstance(image, Image.Image):
179
+ return image.convert("RGB")
180
+ if isinstance(image, (str, Path)):
181
+ return Image.open(image).convert("RGB")
182
+ if isinstance(image, np.ndarray):
183
+ if image.ndim == 2:
184
+ image = np.stack([image, image, image], axis=-1)
185
+ return Image.fromarray(image.astype(np.uint8)).convert("RGB")
186
+ raise TypeError(f"Unsupported image type: {type(image)}")
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=4.57.1
3
+ accelerate
4
+ safetensors
5
+ huggingface_hub
6
+ gradio
7
+ spaces
8
+ einops
9
+ diffusers
10
+ tqdm
11
+ imageio
12
+ Pillow
test_hub_load.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Quick smoke test: load private Hub model and generate one image.
3
+
4
+ Defaults match nemotron-diffusion-omni/gradio_t2i_demo.py:
5
+ image_resolution=1024, n_tokens=4096, is_legacy=False, 64 steps
6
+ NSFW filter on by default (set ENABLE_IMAGE_GUARD=0 to opt out)
7
+
8
+ Usage on a GPU node:
9
+
10
+ conda activate lavida
11
+ export HF_TOKEN=hf_...
12
+ export MODEL_ID=nvidia/NL-Diffusion-Image
13
+ python test_hub_load.py
14
+
15
+ Opt out of guard for local runs:
16
+
17
+ ENABLE_IMAGE_GUARD=0 python test_hub_load.py
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import sys
24
+ import time
25
+
26
+ import torch
27
+
28
+ from app import DEFAULT_MICRO_COND, DEFAULT_PROMPT, T2IEngine
29
+
30
+ MODEL_ID = os.getenv("MODEL_ID", "nvidia/NL-Diffusion-Image")
31
+ OUTPUT_PATH = os.getenv("OUTPUT_PATH", "test_hub_output.webp")
32
+ RESOLUTION = int(os.getenv("TEST_RESOLUTION", "1024"))
33
+
34
+
35
+ def main() -> int:
36
+ if not torch.cuda.is_available():
37
+ print("ERROR: CUDA is not available. Run this on a GPU node.", file=sys.stderr)
38
+ return 1
39
+
40
+ if not os.getenv("HF_TOKEN") and not os.path.isdir(MODEL_ID):
41
+ print(
42
+ "ERROR: Set HF_TOKEN to load the private Hub model, "
43
+ f"or set MODEL_ID to a local checkpoint directory.",
44
+ file=sys.stderr,
45
+ )
46
+ return 1
47
+
48
+ print(f"CUDA device: {torch.cuda.get_device_name()}")
49
+ print(f"Model: {MODEL_ID}")
50
+ print(f"Resolution: {RESOLUTION} (gradio_t2i_demo.py default: 1024, is_legacy=False)")
51
+
52
+ engine = T2IEngine(model_id=MODEL_ID, device="cuda")
53
+
54
+ t0 = time.time()
55
+ image_path, meta = engine.generate(
56
+ prompt=os.getenv("TEST_PROMPT", DEFAULT_PROMPT),
57
+ image_resolution=RESOLUTION,
58
+ guidance_scale=5.0,
59
+ temperature=0.86,
60
+ n_steps=int(os.getenv("TEST_STEPS", "64")),
61
+ schedule="shift",
62
+ shift=5,
63
+ confidence_policy="mmada",
64
+ schedule_temp="linear",
65
+ alg_temp=1.0,
66
+ dynamic_temperature=False,
67
+ min_temperature=0.01,
68
+ edit_threshold=0.6,
69
+ seed=42,
70
+ micro_cond=os.getenv("TEST_MICRO_COND", DEFAULT_MICRO_COND),
71
+ use_cache=False,
72
+ is_legacy=False,
73
+ return_animation=False,
74
+ enable_image_guard=os.getenv("ENABLE_IMAGE_GUARD", "1") == "1",
75
+ )
76
+ elapsed = time.time() - t0
77
+
78
+ if image_path is None:
79
+ print(meta, file=sys.stderr)
80
+ print(f"Total wall time: {elapsed:.2f}s")
81
+ return 1
82
+
83
+ import shutil
84
+
85
+ shutil.copy(image_path, OUTPUT_PATH)
86
+ print(f"Saved {OUTPUT_PATH}")
87
+ print(meta)
88
+ print(f"Total wall time: {elapsed:.2f}s")
89
+ return 0
90
+
91
+
92
+ if __name__ == "__main__":
93
+ raise SystemExit(main())