File size: 31,023 Bytes
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c05efe
904bf44
 
522c6a9
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991484b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522c6a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
978f0a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
bfecd26
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c05efe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978f0a8
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978f0a8
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522c6a9
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfecd26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfecd26
 
cd66dcf
904bf44
bfecd26
 
 
 
 
 
 
 
cd66dcf
 
522c6a9
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd66dcf
 
 
991484b
 
904bf44
991484b
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
abad219
904bf44
 
 
 
bfecd26
 
904bf44
cd66dcf
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991484b
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfecd26
 
cd66dcf
904bf44
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
a794ce5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
082667a
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c05efe
 
904bf44
8c05efe
bfecd26
 
 
8c05efe
 
 
 
 
 
904bf44
8c05efe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
8c05efe
 
 
 
 
 
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abad219
 
904bf44
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
 
 
 
 
 
 
 
 
c9c896d
904bf44
 
 
 
 
8c05efe
 
 
904bf44
 
 
 
 
 
c9c896d
 
 
 
 
 
 
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
"""NL-Diffusion-Image Gradio demo for Hugging Face Spaces (and local testing).

Local test with private Hub model:

    conda activate lavida
    export HF_TOKEN=hf_...
    export MODEL_ID=nvidia/NL-Diffusion-Image
    python app.py

Post-generation NSFW guard (on by default — opt out with ENABLE_IMAGE_GUARD=0):

    export ENABLE_IMAGE_GUARD=0
    python app.py
"""

from __future__ import annotations

import gc
import os
import random
import tempfile
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Any

ASSETS_DIR = Path(__file__).resolve().parent / "assets"


def _asset(name: str) -> str:
    return str(ASSETS_DIR / name)

import gradio as gr
import imageio.v3 as iio
import torch
from PIL import ImageDraw
from transformers import AutoModelForCausalLM, PreTrainedTokenizerFast

from image_guard import (
    DEFAULT_IMAGE_GUARD_MODEL_ID,
    DEFAULT_IMAGE_GUARD_THRESHOLD,
    ImageGuard,
)

try:
    import spaces
except ImportError:

    class _SpacesStub:
        @staticmethod
        def GPU(*args, **kwargs):
            def decorator(fn):
                return fn

            if args and callable(args[0]):
                return args[0]
            return decorator

    spaces = _SpacesStub()


# The released model drives its denoising loop with a tqdm bar over a
# length-less iterable (`enumerate(...)` plus a separate `total=`). Gradio's
# track_tqdm ignores that explicit total and the bar has no description, so the
# UI shows a bogus "Downloading (incomplete total...)" indicator. This shim
# backfills the missing length/description onto the tracked bar, but only while
# a generation is in flight (so download bars during model load are untouched).
_DENOISE_PROGRESS = {"active": False, "total": None, "desc": "Generating image"}


def _install_progress_shim() -> None:
    try:
        from gradio import helpers as _gr_helpers
    except Exception:
        return
    if getattr(_gr_helpers.Progress.tqdm, "_denoise_shim", False):
        return
    _orig_tqdm = _gr_helpers.Progress.tqdm

    def _patched_tqdm(self, *args, **kwargs):
        out = _orig_tqdm(self, *args, **kwargs)
        try:
            if _DENOISE_PROGRESS["active"] and getattr(self, "iterables", None):
                ti = self.iterables[-1]
                if getattr(ti, "length", None) in (None, 0) and _DENOISE_PROGRESS["total"]:
                    ti.length = _DENOISE_PROGRESS["total"]
                if not getattr(ti, "desc", None):
                    ti.desc = _DENOISE_PROGRESS["desc"]
        except Exception:
            pass
        return out

    _patched_tqdm._denoise_shim = True
    _gr_helpers.Progress.tqdm = _patched_tqdm


_install_progress_shim()


@contextmanager
def _suppress_tqdm_tracking():
    """Stop track_tqdm from capturing tqdm bars (e.g. Hugging Face download
    bars during model loading), so only our explicit status message shows."""
    try:
        from gradio.context import LocalContext
    except Exception:
        yield
        return
    token = LocalContext.progress.set(None)
    try:
        yield
    finally:
        try:
            LocalContext.progress.reset(token)
        except Exception:
            pass

os.environ.setdefault("DEBUG_FIX_PADDING", "1")
os.environ.setdefault("NOT_ALWASY_DO_2DPOOL", "1")

if "CUDA_HOME" not in os.environ:
    _local_cuda = "/lustre/fsw/portfolios/llmservice/users/gheinrich/cuda/cuda_12.4"
    if os.path.isdir(_local_cuda):
        os.environ["CUDA_HOME"] = _local_cuda

def resolve_hf_token() -> str | None:
    """Return an HF token from any of the common env var names (or None)."""
    for var in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACEHUB_API_TOKEN"):
        value = os.getenv(var)
        if value:
            return value.strip()
    return None


HF_TOKEN = resolve_hf_token()


def _log_token_status() -> None:
    if HF_TOKEN:
        masked = f"{HF_TOKEN[:4]}{HF_TOKEN[-2:]}" if len(HF_TOKEN) > 6 else "set"
        print(f"HF token detected ({masked}).", flush=True)
    else:
        print(
            "WARNING: no HF token found (HF_TOKEN / HUGGING_FACE_HUB_TOKEN). "
            "Private models will fail to load with a 404. "
            "Add an HF_TOKEN secret in the Space Settings with read access.",
            flush=True,
        )


_log_token_status()

MODEL_ID = os.getenv("MODEL_ID", "nvidia/NL-Diffusion-Image")
DEVICE = os.getenv("DEVICE", "cuda")
IMAGE_GUARD_MODEL_ID = os.getenv("IMAGE_GUARD_MODEL_ID", DEFAULT_IMAGE_GUARD_MODEL_ID)
IMAGE_GUARD_THRESHOLD = float(
    os.getenv("IMAGE_GUARD_THRESHOLD", str(DEFAULT_IMAGE_GUARD_THRESHOLD))
)
IMAGE_GUARD_OFFLOAD_T2I = os.getenv("IMAGE_GUARD_OFFLOAD_T2I", "0") == "1"
# Opt-out: guard runs by default; set ENABLE_IMAGE_GUARD=0 or uncheck the UI box to disable.
DEFAULT_ENABLE_IMAGE_GUARD = os.getenv("ENABLE_IMAGE_GUARD", "1") == "1"
# Pre-generation prompt safety check (input filter), same content-safety model.
DEFAULT_ENABLE_PROMPT_GUARD = os.getenv("ENABLE_PROMPT_GUARD", "1") == "1"

GUARD_ACCESS_HELP = (
    "https://huggingface.co/nvidia/Nemotron-3.5-Content-Safety"
)


def _guard_unavailable_message(exc: Exception) -> str:
    text = str(exc).lower()
    if "gated repo" in text or "403" in text or "authorized list" in text:
        return (
            "NSFW filter is enabled but Nemotron 3.5 Content Safety is not accessible. "
            f"See {GUARD_ACCESS_HELP}, ensure HF_TOKEN has read access, "
            "or uncheck 'NSFW output filter' to opt out."
        )
    return f"NSFW filter is enabled but Nemotron 3.5 Content Safety failed: {exc}"


def _report_guard_failure(message: str) -> None:
    """Surface guard failures in the Gradio UI without breaking output components."""
    gr.Warning(message)
    print(f"GUARD ERROR: {message}", flush=True)

# Defaults aligned with nemotron-diffusion-omni/gradio_t2i_demo.py
DEFAULT_PROMPT = (
    "A full-body shot of hyper-realistic female cyborg, human facial skin seamlessly integrated "
    "with a glossy white mechanical head shell. Features a realistic human ear, blue eyes. bright, "
    "outdoor, background with blue sky, illuminated by striking bright white studio lighting, "
    "casting soft shadows. Cyberpunk aesthetic, high-tech minimalism, shot on 85mm lens, "
    "photorealistic, Unreal Engine 5 render, intricately detailed, 8k resolution, high dynamic "
    "range, chest with whit armor plate, cute, beautiful, sexy, glossy surface, reflective, "
    "Artstation, pixiv, no hair, 3D render, stylized eyesz"
)
DEFAULT_MICRO_COND = (
    "ORIGINAL WIDTH : 1024; ORIGINAL HEIGHT : 1024; TOP : 0; LEFT : 0; "
    "SCORE : 6.520; HPS: 3.220"
)

EXAMPLE_PROMPTS = [
    "A photorealistic portrait of an astronaut riding a horse on the moon, "
    "golden hour lighting, 85mm lens, ultra detailed, sharp focus",
    "A cozy bookstore cafe interior, warm lighting, hanging plants, wooden shelves, "
    "cinematic, highly detailed",
    "A majestic snow leopard standing on a rocky cliff, national geographic "
    "photography, crisp fur detail, soft bokeh background",
    "A futuristic city skyline at dusk, neon signs, wet streets with reflections, "
    "cyberpunk aesthetic, 8k, cinematic lighting",
    "A steaming bowl of ramen with a soft-boiled egg, studio food photography, "
    "shallow depth of field, rich colors",
    "An oil painting of a lighthouse on a stormy coast, dramatic clouds, crashing "
    "waves, impressionist style",
]

CITATION_BIBTEX = """@article{li2026nemotron,
  title  = {Nemotron-Labs-Diffusion-Image: Advancing Masked Discrete Diffusion
            for High-Resolution Image Synthesis},
  author = {Li, Shufan and Heinrich, Greg and Ye, Hanrong and Fu, Yonggan and
            Grover, Aditya and Kautz, Jan and Molchanov, Pavlo},
  journal = {arXiv preprint arXiv:2606.29814},
  year   = {2026}
}"""

DEFAULT_GENERATION_CONFIG: dict[str, Any] = {
    "guidance_scale": 5.0,
    "n_steps": 64,
    "shift": 5,
    "alg_temp": 1.0,
    "dynamic_temperature": False,
    "min_temperature": 0.01,
    "schedule_temp": "linear",
    "temperature": 0.86,
    "confidence_policy": "mmada",
    "micro_cond": DEFAULT_MICRO_COND,
    "edit_threshold": 0.6,
    "is_legacy": False,
}


def n_tokens_from_resolution(image_resolution: int) -> int:
    return (image_resolution // 16) * (image_resolution // 16)


def process_gif(image_list):
    if not image_list:
        return None

    with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp_file:
        gif_path = tmp_file.name

    frames = []
    total_frames = len(image_list)
    for i, img in enumerate(image_list):
        frame = img.resize((400, 400))
        draw = ImageDraw.Draw(frame)
        text = f"Frame: {i + 1} / {total_frames}"
        x, y = 15, 15
        for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
            draw.text((x + dx, y + dy), text, fill="black")
        draw.text((x, y), text, fill="white")
        frames.append(frame)

    duration = [1000 / 20] * len(frames)
    duration[-1] = 2000
    iio.imwrite(gif_path, frames, extension=".gif", duration=duration, loop=0)
    return gif_path


def process_webp(pil_image):
    with tempfile.NamedTemporaryFile(suffix=".webp", delete=False) as tmp_file:
        webp_path = tmp_file.name
    pil_image.save(webp_path, "webp", quality=95)
    return webp_path


def load_release_model_and_tokenizer(model_id: str, device: str):
    hf_token = HF_TOKEN
    if hf_token is None and not os.path.isdir(model_id):
        raise RuntimeError(
            f"Cannot load '{model_id}': no HF token found. "
            "This is a private repo — add an HF_TOKEN secret in the Space Settings "
            "(Settings → Variables and secrets) using a token with read access to "
            f"{model_id}."
        )
    tokenizer = PreTrainedTokenizerFast.from_pretrained(model_id, token=hf_token)
    if tokenizer.pad_token_id is None:
        tokenizer.pad_token_id = tokenizer.eos_token_id
        tokenizer.pad_token = tokenizer.eos_token

    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        trust_remote_code=True,
        torch_dtype=torch.bfloat16,
        low_cpu_mem_usage=False,
        token=hf_token,
    )
    model.to(device)
    model.eval()
    model.requires_grad_(False)
    model.config.dlm_paradigm = "bidirectional"
    return tokenizer, model


def _format_guard_meta(result) -> str:
    return (
        f"guard={result.model_id} | label={result.label} | "
        f"unsafe_score={result.score:.3f} | guard_time={result.inference_seconds:.2f}s"
    )


class T2IEngine:
    def __init__(self, model_id: str, device: str = "cuda") -> None:
        self.model_id = model_id
        self.device = device
        self._tokenizer = None
        self._model = None
        self._image_guard: ImageGuard | None = None

    def _lazy_load(self) -> None:
        if self._model is not None and self._tokenizer is not None:
            return
        print(f"Loading model from {self.model_id} ...", flush=True)
        self._tokenizer, self._model = load_release_model_and_tokenizer(
            self.model_id, self.device
        )
        print("Model loaded.", flush=True)

    def _get_image_guard(self) -> ImageGuard:
        if self._image_guard is None:
            print(f"Loading image guard from {IMAGE_GUARD_MODEL_ID} ...", flush=True)
            self._image_guard = ImageGuard(
                model_id=IMAGE_GUARD_MODEL_ID,
                threshold=IMAGE_GUARD_THRESHOLD,
                device=self.device,
                hf_token=HF_TOKEN,
            )
        return self._image_guard

    def _offload_t2i_to_cpu(self) -> None:
        if self._model is not None:
            self._model.to("cpu")
            gc.collect()
            if torch.cuda.is_available():
                torch.cuda.empty_cache()

    def _reload_t2i_to_device(self) -> None:
        if self._model is not None:
            self._model.to(self.device)

    def _moderate_output(
        self,
        result,
        return_animation: bool,
        enable_image_guard: bool,
    ) -> tuple[bool, str]:
        """Run post-generation moderation. Returns (ok, meta_suffix_or_error_message)."""
        if not enable_image_guard:
            return True, ""

        if IMAGE_GUARD_OFFLOAD_T2I:
            self._offload_t2i_to_cpu()

        try:
            with _suppress_tqdm_tracking():
                guard = self._get_image_guard()
            frames = result if return_animation else [result]
            guard_parts = []
            for frame_idx, frame in enumerate(frames):
                check = guard.check_image(frame)
                guard_parts.append(_format_guard_meta(check))
                if not check.passed:
                    message = (
                        "Generated image blocked by NSFW filter "
                        f"(frame {frame_idx + 1}/{len(frames)}, "
                        f"unsafe_score={check.score:.3f}, threshold={IMAGE_GUARD_THRESHOLD})."
                    )
                    _report_guard_failure(message)
                    return False, message
            return True, " | " + guard_parts[0] if guard_parts else ""
        except Exception as exc:
            message = _guard_unavailable_message(exc)
            _report_guard_failure(message)
            return False, message
        finally:
            if IMAGE_GUARD_OFFLOAD_T2I:
                self._reload_t2i_to_device()

    def _moderate_prompt(self, prompt: str) -> tuple[bool, str]:
        """Run pre-generation prompt moderation. Returns (ok, meta_or_error)."""
        try:
            with _suppress_tqdm_tracking():
                guard = self._get_image_guard()
            check = guard.check_text(prompt)
            if not check.passed:
                message = (
                    "Prompt blocked by content-safety filter "
                    f"(label={check.label})."
                )
                _report_guard_failure(message)
                return False, message
            return True, "prompt_" + _format_guard_meta(check)
        except Exception as exc:
            message = _guard_unavailable_message(exc)
            _report_guard_failure(message)
            return False, message

    def generate(
        self,
        prompt: str,
        image_resolution: int,
        guidance_scale: float,
        temperature: float,
        n_steps: int,
        shift: int,
        confidence_policy: str,
        schedule_temp: str,
        alg_temp: float,
        dynamic_temperature: bool,
        min_temperature: float,
        edit_threshold: float,
        seed: int,
        micro_cond: str,
        return_animation: bool,
        enable_image_guard: bool = DEFAULT_ENABLE_IMAGE_GUARD,
        enable_prompt_guard: bool = DEFAULT_ENABLE_PROMPT_GUARD,
        progress: gr.Progress | None = None,
    ):
        prompt_guard_meta = ""
        if enable_prompt_guard:
            if progress is not None:
                progress(0.0, desc="Checking prompt…")
            prompt_ok, prompt_guard_meta = self._moderate_prompt(prompt)
            if not prompt_ok:
                return None, f"ERROR: {prompt_guard_meta}"

        if progress is not None and self._model is None:
            progress(0.0, desc="Loading model (first run, this can take 1-2 min)…")
        with _suppress_tqdm_tracking():
            self._lazy_load()

        gen_cfg = dict(DEFAULT_GENERATION_CONFIG)
        gen_cfg.update(
            micro_cond=micro_cond,
            guidance_scale=guidance_scale,
            temperature=temperature,
            edit_threshold=edit_threshold,
            n_steps=int(n_steps),
            shift=int(shift),
            confidence_policy=confidence_policy,
            schedule_temp=schedule_temp,
            alg_temp=alg_temp,
            dynamic_temperature=dynamic_temperature,
            min_temperature=min_temperature,
        )

        if seed < 0:
            seed = int(torch.seed() % (2**31 - 1))
        torch.manual_seed(int(seed))

        n_tokens = n_tokens_from_resolution(int(image_resolution))

        if progress is not None:
            progress(0.0, desc="Generating…")

        _DENOISE_PROGRESS["total"] = int(n_steps)
        _DENOISE_PROGRESS["active"] = progress is not None
        t0 = time.time()
        try:
            with torch.no_grad():
                with torch.inference_mode():
                    result = self._model.text_to_image(
                        prompt,
                        tokenizer=self._tokenizer,
                        **gen_cfg,
                        image_resolution=int(image_resolution),
                        n_tokens=n_tokens,
                        disable_tqdm=progress is None,
                        return_intermediate_steps=return_animation,
                    )
        finally:
            _DENOISE_PROGRESS["active"] = False
        latency = time.time() - t0

        meta = (
            f"model={self.model_id} | seed={seed} | res={image_resolution} | "
            f"n_tokens={n_tokens} | steps={n_steps} | "
            f"cfg={guidance_scale:.2f} | temp={temperature:.3f} | "
            f"shift={shift} | alg_temp={alg_temp:.2f} | "
            f"dyn_temp={dynamic_temperature} | min_temp={min_temperature:.3f} | "
            f"sch_temp={schedule_temp} | conf={confidence_policy} | "
            f"edit_threshold={edit_threshold:.3f} | gen_time={latency:.2f}s"
        )
        if prompt_guard_meta:
            meta += " | " + prompt_guard_meta

        if progress is not None and enable_image_guard:
            progress(1.0, desc="Running safety filter…")

        guard_ok, guard_meta = self._moderate_output(
            result, return_animation, enable_image_guard
        )
        if not guard_ok:
            return None, f"ERROR: {guard_meta}\n\n{meta}"

        meta += guard_meta

        if return_animation:
            return process_gif(result), meta
        return process_webp(result), meta


engine = T2IEngine(model_id=MODEL_ID, device=DEVICE)


@spaces.GPU(duration=240)
def generate(
    prompt: str,
    image_resolution: int,
    guidance_scale: float,
    temperature: float,
    n_steps: int,
    shift: int,
    confidence_policy: str,
    schedule_temp: str,
    alg_temp: float,
    dynamic_temperature: bool,
    min_temperature: float,
    edit_threshold: float,
    seed: int,
    micro_cond: str,
    return_animation: bool,
    progress: gr.Progress = gr.Progress(track_tqdm=True),
):
    return engine.generate(
        prompt,
        image_resolution,
        guidance_scale,
        temperature,
        n_steps,
        shift,
        confidence_policy,
        schedule_temp,
        alg_temp,
        dynamic_temperature,
        min_temperature,
        edit_threshold,
        seed,
        micro_cond,
        return_animation,
        enable_image_guard=DEFAULT_ENABLE_IMAGE_GUARD,
        enable_prompt_guard=DEFAULT_ENABLE_PROMPT_GUARD,
        progress=progress,
    )


def make_theme() -> gr.themes.Base:
    nvidia_green = gr.themes.Color(
        c50="#f3f9e6",
        c100="#e3f1c2",
        c200="#cfe88f",
        c300="#b6dc56",
        c400="#97c61f",
        c500="#76b900",
        c600="#69a600",
        c700="#548400",
        c800="#3f6300",
        c900="#2a4200",
        c950="#1a2900",
    )
    return gr.themes.Soft(
        primary_hue=nvidia_green,
        secondary_hue=nvidia_green,
        font=[
            gr.themes.GoogleFont("Inter"),
            "ui-sans-serif",
            "system-ui",
            "sans-serif",
        ],
        font_mono=[
            gr.themes.GoogleFont("JetBrains Mono"),
            "ui-monospace",
            "monospace",
        ],
    )


# Gradio 6.0 moved `theme` from Blocks(...) to launch(...).
_GRADIO_MAJOR = int(gr.__version__.split(".")[0])


def build_demo() -> gr.Blocks:
    theme = make_theme()
    blocks_kwargs: dict[str, Any] = {"title": "Nemotron Labs Diffusion Image"}
    if _GRADIO_MAJOR < 6:
        blocks_kwargs["theme"] = theme
    with gr.Blocks(**blocks_kwargs) as demo:
        gr.Markdown(
            "# Nemotron Labs Diffusion Image\n\n"
            "NL-Diffusion-Image generates high-resolution images via **masked discrete diffusion** "
            "over tokenized image patches. Each image is encoded into discrete tokens "
            "(131K codebook), and generation proceeds through iterative parallel unmasking—similar "
            "to diffusion LLMs. The model is fine-tuned from "
            "[Nemotron-Labs-Diffusion](https://huggingface.co/nvidia/Nemotron-Labs-Diffusion-8B) "
            "with two key additions:\n\n"
            "- **Token editing** — revise already-unmasked tokens during inference.\n"
            "- **Grouped Cross-Entropy (GCE)** — efficient training with large vocabularies.\n\n"
            "This aligns image generation with LLM training and inference infrastructure, "
            "making it highly scalable.\n\n"
            "📄 [Paper (arXiv:2606.29814)](https://arxiv.org/abs/2606.29814) · "
            "🤗 [Model](https://huggingface.co/nvidia/NL-Diffusion-Image)"
        )

        gr.Markdown(
            "| GenEval | DPG | HPSv3 | Speed vs EMU3.5 |\n"
            "|:---:|:---:|:---:|:---:|\n"
            "| **0.90** | **86.9** | **10.76** | **42.4× faster** |"
        )

        gr.Markdown("## Generate an image")

        with gr.Row():
            with gr.Column(scale=2):
                prompt = gr.Textbox(label="Prompt", lines=4, value=DEFAULT_PROMPT)
                with gr.Row():
                    image_resolution = gr.Dropdown(
                        choices=[256, 512, 1024],
                        value=1024,
                        label="Image Resolution",
                    )
                    n_steps = gr.Slider(
                        minimum=1, maximum=128, value=64, step=1, label="Diffusion Steps"
                    )
                with gr.Row():
                    guidance_scale = gr.Slider(
                        minimum=1.0, maximum=10.0, value=5.0, step=0.1, label="Guidance Scale"
                    )
                    temperature = gr.Slider(
                        minimum=0.05, maximum=1.5, value=0.86, step=0.01, label="Temperature"
                    )
                with gr.Row():
                    seed = gr.Number(
                        label="Seed (-1 for random)", value=42, precision=0, scale=3
                    )
                    randomize_seed_btn = gr.Button("🎲 Randomize", scale=1)
                gr.Markdown(
                    "Safety filters (Nemotron 3.5 Content Safety) run on both the "
                    "prompt and the generated image and cannot be disabled."
                )
                generate_btn = gr.Button("Generate", variant="primary")

                with gr.Accordion("Advanced settings", open=False):
                    micro_cond = gr.Textbox(
                        label="Micro Cond", lines=2, value=DEFAULT_MICRO_COND
                    )
                    with gr.Row():
                        shift = gr.Slider(
                            minimum=0, maximum=16, value=5, step=1, label="Shift"
                        )
                        confidence_policy = gr.Dropdown(
                            choices=["mask_git", "mmada", "stratified"],
                            value="mmada",
                            label="Confidence Policy",
                        )
                    with gr.Row():
                        schedule_temp = gr.Dropdown(
                            choices=["linear", "cosine2", "shift", "exp"],
                            value="linear",
                            label="Schedule Temp",
                        )
                        alg_temp = gr.Slider(
                            minimum=0.1, maximum=3.0, value=1.0, step=0.1, label="Alg Temp"
                        )
                        dynamic_temperature = gr.Checkbox(label="Dynamic Temp", value=False)
                    with gr.Row():
                        min_temperature = gr.Slider(
                            minimum=0.0, maximum=1.0, value=0.01, step=0.01, label="Min Temp"
                        )
                        edit_threshold = gr.Slider(
                            minimum=-1.0, maximum=1.0, value=0.6, step=0.01,
                            label="Edit Threshold",
                        )
                    return_animation = gr.Checkbox(
                        label="Return Animation (resized to 400x400 for preview)",
                        value=False,
                    )

            with gr.Column(scale=3):
                output_image = gr.Image(label="Generated Image", type="filepath")
                output_meta = gr.Textbox(label="Generation Info", lines=6)

        gr.Examples(examples=EXAMPLE_PROMPTS, inputs=[prompt], label="Example prompts")

        randomize_seed_btn.click(
            fn=lambda: random.randint(0, 2**31 - 1), inputs=None, outputs=seed
        )

        generate_btn.click(
            fn=generate,
            inputs=[
                prompt,
                image_resolution,
                guidance_scale,
                temperature,
                n_steps,
                shift,
                confidence_policy,
                schedule_temp,
                alg_temp,
                dynamic_temperature,
                min_temperature,
                edit_threshold,
                seed,
                micro_cond,
                return_animation,
            ],
            outputs=[output_image, output_meta],
        )

        with gr.Accordion("About the model", open=True):
            gr.Markdown(
                "_Masked Discrete Diffusion · Text-to-Image Synthesis · Token Editing · "
                "Grouped Cross-Entropy (GCE) · High-Resolution Image Generation_"
            )

            gr.Markdown("### Sample outputs")
            gr.Gallery(
                value=[
                    _asset("demo_1.gif"),
                    _asset("demo_2.gif"),
                    _asset("demo_3.gif"),
                ],
                columns=3,
                height="auto",
                object_fit="contain",
                show_label=False,
            )

            gr.Markdown(
                "### Generation speed\n\n"
                "Side-by-side at 1024×1024. **Left:** NL-Diffusion-Image. "
                "**Right:** EMU3.5 (autoregressive). NL-Diffusion-Image is **42.4× faster** "
                "while scoring higher on GenEval."
            )
            gr.Image(
                value=_asset("speed_comparison.gif"),
                show_label=False,
                interactive=False,            )

            with gr.Row(equal_height=True):
                with gr.Column(scale=1):
                    gr.Markdown(
                        "### Architecture\n\n"
                        "16×16 image patches are encoded with a pretrained discrete tokenizer "
                        "from EMU3.5 (128K codebook). The Nemotron-Labs-Diffusion vocabulary is "
                        "expanded with randomly initialized embeddings and fine-tuned on "
                        "image/caption pairs."
                    )
                with gr.Column(scale=1):
                    gr.Image(
                        value=_asset("architecture.png"),
                        show_label=False,
                        interactive=False,                    )

            with gr.Row(equal_height=True):
                with gr.Column(scale=1):
                    gr.Markdown(
                        "### Benchmarks\n\n"
                        "State-of-the-art among discrete image generators at 1024px text-to-image, "
                        "surpassing prior masked image generators on quality while remaining "
                        "dramatically faster than autoregressive baselines."
                    )
                with gr.Column(scale=1):
                    gr.Image(
                        value=_asset("benchmarks.png"),
                        show_label=False,
                        interactive=False,                    )

            gr.Markdown("### Key findings")

            with gr.Row(equal_height=True):
                with gr.Column(scale=1):
                    gr.Markdown(
                        "**Token editing for self-correction**\n\n"
                        "Token editing lets the model iteratively refine outputs during inference, "
                        "correcting artifacts and improving texture detail."
                    )
                with gr.Column(scale=1):
                    gr.Image(
                        value=_asset("token_editing.png"),
                        show_label=False,
                        interactive=False,                    )

            with gr.Row(equal_height=True):
                with gr.Column(scale=1):
                    gr.Markdown(
                        "**Grouped Cross-Entropy (GCE)**\n\n"
                        "GCE alleviates codebook sparsity by supervising semantically close "
                        "non-top-1 tokens in embedding space.\n\n"
                        "A fused GCE operator cuts peak VRAM from 25.2 GB to 16.1 GB and latency "
                        "from 44.14 ms to 20.04 ms versus an eager implementation."
                    )
                with gr.Column(scale=1):
                    gr.Image(
                        value=_asset("gce_objective.png"),
                        show_label=False,
                        interactive=False,                    )

            with gr.Row(equal_height=True):
                with gr.Column(scale=1):
                    gr.Markdown(
                        "**Few-step generation**\n\n"
                        "Unlike continuous flow-matching models that predict blurry mean fields at "
                        "low step counts, NL-Diffusion-Image produces reasonable quality in as few "
                        "as 4 steps without distillation."
                    )
                with gr.Column(scale=1):
                    gr.Image(
                        value=_asset("few_step_generation.png"),
                        show_label=False,
                        interactive=False,                    )

            gr.Markdown(
                "**Future work:** extend the model to unified vision generation and understanding."
            )

            gr.Markdown("### Citation")
            gr.Code(value=CITATION_BIBTEX, language=None, label="BibTeX")

    return demo


demo = build_demo()

if __name__ == "__main__":
    launch_kwargs: dict[str, Any] = {
        "server_name": os.getenv("HOST", "0.0.0.0"),
        "server_port": int(os.getenv("PORT", "7860")),
    }
    if _GRADIO_MAJOR >= 6:
        launch_kwargs["theme"] = make_theme()
    demo.queue(default_concurrency_limit=1).launch(**launch_kwargs)