Spaces:
Running on Zero
Running on Zero
multimodalart HF Staff
Add quality model option and streamline blocked-content handling (#4)
6824d71 | """MageFlow text-to-image + image-edit inference pipeline. | |
| Self-contained MageFlow t2i / edit inference: load a HuggingFace diffusers-style | |
| repo (model_index.json + transformer/ vae/ scheduler/), then generate or edit | |
| images. No training/eval deps. | |
| Both ``generate_images`` and ``generate_edits`` support PACKED multi-resolution | |
| inference: several samples (each at its own resolution) are concatenated into a | |
| single varlen sequence and processed in one transformer forward per denoise | |
| step. Per-sample ``cu_seqlens`` (inside the flash-attn varlen kernel) isolate | |
| samples, exactly mirroring training-time packing. These packed functions are the | |
| sole implementation — the single-image case is just a pack of size 1, exposed | |
| via the ``MageFlowPipeline.generate`` / ``.edit`` convenience methods. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import random | |
| import torch | |
| from einops import rearrange | |
| from PIL import Image | |
| from diffusers import FlowMatchEulerDiscreteScheduler | |
| from .models.mage_flow import MageFlowModel, ModelConfig | |
| from .models.utils import PROMPT_TEMPLATE, get_noise, unpack | |
| from .models.modules.mage_text import make_refusal_image | |
| from .models.modules.mage_latent import encode_noise, resolve_gs_key | |
| # --------------------------------------------------------------------------- | |
| # Scheduler — diffusers FlowMatchEulerDiscreteScheduler | |
| # --------------------------------------------------------------------------- | |
| def build_scheduler(num_steps: int, device=None, shift: float = 6.0): | |
| """Construct a diffusers ``FlowMatchEulerDiscreteScheduler`` whose sigma | |
| schedule reproduces our default preset exactly. | |
| The base sigmas ``linspace(1, 1/num_steps, num_steps)`` fed to | |
| ``set_timesteps`` are run through the scheduler's built-in static shift | |
| ``shift·s/(1+(shift-1)·s)`` and a terminal 0 is appended — the static-shift | |
| schedule (the only supported schedule). | |
| """ | |
| scheduler = FlowMatchEulerDiscreteScheduler( | |
| num_train_timesteps=1000, shift=shift, use_dynamic_shifting=False) | |
| base_sigmas = torch.linspace(1.0, 1.0 / num_steps, num_steps).tolist() | |
| scheduler.set_timesteps(sigmas=base_sigmas, device=device) | |
| return scheduler | |
| def _get_scheduler(model, steps, device, static_shift): | |
| scheduler = getattr(model, "scheduler", None) | |
| if scheduler is None: | |
| return build_scheduler(steps, device=device, | |
| shift=(static_shift if static_shift is not None else 6.0)) | |
| if static_shift is not None: | |
| scheduler.set_shift(static_shift) | |
| scheduler.set_timesteps(sigmas=torch.linspace(1.0, 1.0 / steps, steps).tolist(), device=device) | |
| return scheduler | |
| # --------------------------------------------------------------------------- | |
| # Small helpers | |
| # --------------------------------------------------------------------------- | |
| def _template_info(name: str | None) -> dict: | |
| name = name or "mage-flow" | |
| if name not in PROMPT_TEMPLATE: | |
| raise ValueError(f"Unknown prompt template: {name}") | |
| return PROMPT_TEMPLATE[name] | |
| def _as_list(val, default, n): | |
| """Broadcast a scalar/None to a length-n list, or validate a given list.""" | |
| if val is None: | |
| return [default] * n | |
| if isinstance(val, (list, tuple)): | |
| if len(val) != n: | |
| raise ValueError(f"expected {n} values, got {len(val)}") | |
| return list(val) | |
| return [val] * n | |
| def _lens_to_cu(lens, device): | |
| """Sequence lengths -> cumulative cu_seqlens [0, l0, l0+l1, ...] (int32).""" | |
| t = torch.tensor(lens, device=device, dtype=torch.int32) | |
| return torch.cat([torch.zeros(1, dtype=torch.int32, device=device), | |
| torch.cumsum(t, dim=0, dtype=torch.int32)]) | |
| def _make_divisible_by_16(size: int) -> int: | |
| return max(16, 16 * (size // 16)) | |
| def _compute_aspect_ratio_size(pil_img: Image.Image, max_size: int): | |
| """Longest side = ``max_size``, short side from aspect ratio, both /16.""" | |
| w, h = pil_img.size | |
| if h >= w: | |
| new_h, new_w = max_size, int(round(w * max_size / h)) | |
| else: | |
| new_w, new_h = max_size, int(round(h * max_size / w)) | |
| return _make_divisible_by_16(new_h), _make_divisible_by_16(new_w) | |
| def _edit_target_size(pil_img: Image.Image, max_size, height, width): | |
| """Output (H, W) for an edit sample, derived from its PRIMARY reference. | |
| Precedence: explicit ``height`` AND ``width`` (custom size) > ``max_size`` | |
| (longest side, short side by aspect ratio) > the source image's own size. | |
| All rounded down to a multiple of 16. | |
| """ | |
| if height and width: | |
| return _make_divisible_by_16(height), _make_divisible_by_16(width) | |
| if max_size: | |
| return _compute_aspect_ratio_size(pil_img, max_size) | |
| # Nothing specified: keep the source resolution (its own longest side). | |
| return _compute_aspect_ratio_size(pil_img, max(pil_img.size)) | |
| def _decode_one(model, tokens, height, width, dev): | |
| """Unpack one sample's image tokens [1, H*W, C] and VAE-decode to a PIL image.""" | |
| with torch.autocast(device_type=dev.type, dtype=torch.bfloat16): | |
| out = model.vae.decode(unpack(tokens.float(), height, width)) | |
| out = rearrange(out.clamp(-1, 1), "b c h w -> b h w c") | |
| out = (127.5 * (out + 1.0)).cpu().byte().numpy() | |
| return Image.fromarray(out[0]) | |
| def _build_pack_ctx(img_ids, img_cu, img_shapes, img_lens, txt, txt_cu, txt_mask, vec, | |
| neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, device): | |
| """Precompute the static per-step transformer inputs for a packed batch. | |
| When a negative branch is present and ``batch_cfg`` is True, the conditional | |
| and unconditional passes are fused into ONE varlen forward: the image tokens | |
| are duplicated (cond copy + uncond copy) and the positive/negative texts are | |
| concatenated, so cond sample i and uncond sample i become two independent | |
| varlen segments processed in a single kernel launch. flash_attn_varlen_func | |
| keeps every segment isolated via cu_seqlens, so this is numerically identical | |
| to two separate forwards — just one launch instead of two. | |
| """ | |
| na = len(img_lens) | |
| ctx = { | |
| "na": na, "cfg": cfg, "renorm": renormalization, "batch_cfg": batch_cfg, | |
| "has_neg": neg_txt is not None, | |
| "img_ids": img_ids, "img_cu": img_cu, "img_shapes": img_shapes, | |
| "img_max": int(max(img_lens)), | |
| "txt": txt, "txt_ids": torch.zeros(1, txt.shape[1], 3, device=device), | |
| "txt_cu": txt_cu, "txt_mask": txt_mask, "vec": vec, | |
| "txt_max": int((txt_cu[1:] - txt_cu[:-1]).max().item()), | |
| } | |
| if neg_txt is None: | |
| return ctx | |
| ctx.update({ | |
| "neg_txt": neg_txt, "neg_ids": torch.zeros(1, neg_txt.shape[1], 3, device=device), | |
| "neg_cu": neg_cu, "neg_mask": neg_mask, "neg_vec": neg_vec, | |
| "neg_max": int((neg_cu[1:] - neg_cu[:-1]).max().item()), | |
| }) | |
| if batch_cfg: | |
| # Duplicate image segments (cond then uncond) and concat pos+neg text. | |
| d_txt = torch.cat([txt, neg_txt], dim=1) | |
| pos_lens = (txt_cu[1:] - txt_cu[:-1]).tolist() | |
| neg_lens = (neg_cu[1:] - neg_cu[:-1]).tolist() | |
| ctx.update({ | |
| "d_img_ids": torch.cat([img_ids, img_ids], dim=1), | |
| "d_img_cu": _lens_to_cu(list(img_lens) + list(img_lens), device), | |
| "d_img_shapes": [img_shapes[0] + img_shapes[0]], | |
| "d_txt": d_txt, | |
| "d_txt_ids": torch.zeros(1, d_txt.shape[1], 3, device=device), | |
| "d_txt_cu": _lens_to_cu(pos_lens + neg_lens, device), | |
| "d_txt_mask": torch.ones(1, d_txt.shape[1], device=device), | |
| "d_vec": torch.cat([vec, neg_vec], dim=0), | |
| "d_txt_max": int(max(pos_lens + neg_lens)), | |
| }) | |
| return ctx | |
| def _velocity(transformer, img, ctx, sigma): | |
| """CFG-combined image-token velocity for a packed batch at noise level ``sigma``. | |
| Returns [1, sum_img_len, C] in the conditional sample order. When | |
| ``batch_cfg`` is set the cond+uncond passes share a single fused varlen | |
| forward; otherwise they are two forwards. | |
| """ | |
| dev = img.device | |
| na = ctx["na"] | |
| def _fwd(x, n, img_ids, img_cu, img_max, img_shapes, txt, txt_ids, txt_cu, txt_mask, txt_max, vec): | |
| t_vec = torch.full((n,), sigma, dtype=x.dtype, device=dev) | |
| return transformer(img=x, txt=txt, timesteps=t_vec, img_shapes=img_shapes, | |
| img_cu_seqlens=img_cu, txt_cu_seqlens=txt_cu) | |
| if not ctx["has_neg"]: | |
| return _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"], | |
| ctx["txt"], ctx["txt_ids"], ctx["txt_cu"], ctx["txt_mask"], ctx["txt_max"], ctx["vec"]) | |
| if ctx["batch_cfg"]: | |
| n_img = img.shape[1] | |
| out = _fwd(torch.cat([img, img], dim=1), 2 * na, | |
| ctx["d_img_ids"], ctx["d_img_cu"], ctx["img_max"], ctx["d_img_shapes"], | |
| ctx["d_txt"], ctx["d_txt_ids"], ctx["d_txt_cu"], ctx["d_txt_mask"], ctx["d_txt_max"], ctx["d_vec"]) | |
| cond, unc = out[:, :n_img, :], out[:, n_img:, :] | |
| else: | |
| cond = _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"], | |
| ctx["txt"], ctx["txt_ids"], ctx["txt_cu"], ctx["txt_mask"], ctx["txt_max"], ctx["vec"]) | |
| unc = _fwd(img, na, ctx["img_ids"], ctx["img_cu"], ctx["img_max"], ctx["img_shapes"], | |
| ctx["neg_txt"], ctx["neg_ids"], ctx["neg_cu"], ctx["neg_mask"], ctx["neg_max"], ctx["neg_vec"]) | |
| cfg = ctx["cfg"] | |
| if ctx["renorm"]: | |
| # CFG renormalization: rescale the guided velocity per token back to the | |
| # conditional velocity's norm (reduces oversaturation at high cfg). | |
| comb = unc + cfg * (cond - unc) | |
| return comb * (torch.norm(cond, dim=-1, keepdim=True) / | |
| (torch.norm(comb, dim=-1, keepdim=True) + 1e-6)) | |
| return unc + cfg * (cond - unc) | |
| def _encode_texts_packed(model, prompts, template, drop_idx, device): | |
| """Encode a LIST of templated text-only prompts in ONE packed varlen forward | |
| (``TextEncoder.forward`` — varlen cu_seqlens isolates each prompt, | |
| verified zero cross-contamination). Returns (txt_flat [ΣLi, D], vec [N, D], | |
| per-prompt token lengths list).""" | |
| tokenizer = model.txt_enc.tokenizer | |
| max_len = model.txt_enc.tokenizer_max_length + drop_idx | |
| ids_list = [ | |
| tokenizer(template.format(p), max_length=max_len, truncation=True, | |
| return_tensors="pt").input_ids.squeeze(0) | |
| for p in prompts | |
| ] | |
| input_ids = torch.cat(ids_list).to(device) | |
| cu_seqlens = _lens_to_cu([int(t.numel()) for t in ids_list], device) | |
| res = model.txt_enc( | |
| input_ids, cu_seqlens, drop_idx_override=drop_idx) | |
| return res["txt"], res["vec"], res["txt_seq_lens"].tolist() | |
| def _slice_packed(txt_flat, vec, lens, start, count, device): | |
| """Format a contiguous ``count``-prompt slice (starting at prompt ``start``) of a | |
| packed text encode into the (txt [1, ΣL, D], cu_seqlens, ones-mask, vec [count, D]) | |
| tuple that ``_build_pack_ctx`` consumes.""" | |
| seg_lens = lens[start:start + count] | |
| tok_start = sum(lens[:start]) | |
| tok_end = tok_start + sum(seg_lens) | |
| txt = txt_flat[tok_start:tok_end].reshape(1, -1, txt_flat.shape[-1]).to(device) | |
| return (txt, _lens_to_cu(seg_lens, device), | |
| torch.ones(1, txt.shape[1], device=device), vec[start:start + count].to(device)) | |
| # --------------------------------------------------------------------------- | |
| # Text-to-image (packed, multi-resolution) | |
| # --------------------------------------------------------------------------- | |
| def generate_images(model, prompts, neg_prompts=None, seeds=None, steps=30, cfg=5.0, | |
| heights=None, widths=None, device="cuda", | |
| prompt_template="mage-flow", static_shift=None, | |
| gs_key=None, | |
| renormalization=False, batch_cfg=True): | |
| """Generate one image per prompt. Prompts may request DIFFERENT resolutions; | |
| all are packed into a single varlen forward per denoise step — samples are | |
| kept isolated by ``flash_attn_varlen_func`` via per-sample ``cu_seqlens`` (no | |
| cross-sample attention), mirroring training-time packing. When ``cfg > 1`` and | |
| ``batch_cfg`` is set, the positive and negative passes are fused into that | |
| same varlen forward. Returns a list of PIL images aligned with ``prompts``. | |
| """ | |
| if isinstance(prompts, str): | |
| prompts = [prompts] | |
| n = len(prompts) | |
| neg_prompts = _as_list(neg_prompts, " ", n) | |
| seeds = _as_list(seeds, 42, n) | |
| heights = _as_list(heights, 1024, n) | |
| widths = _as_list(widths, 1024, n) | |
| info = _template_info(prompt_template) | |
| template = info.get("template", "{}") | |
| drop_idx = int(info.get("start_idx", 0)) | |
| dev = torch.device(device) | |
| # Content-policy gate per sample (MANDATORY — runs on the same text-encoder | |
| # weights as conditioning, no opt-out). Violating prompts get a refusal | |
| # placeholder and are dropped from the pack. | |
| results = [None] * n | |
| active = [] | |
| for i in range(n): | |
| if seeds[i] == -1: | |
| seeds[i] = random.randint(0, 2**32 - 1) | |
| verdict = model.txt_enc.screen_text(prompts[i]) | |
| if verdict.violates: | |
| h_, w_ = _make_divisible_by_16(heights[i]), _make_divisible_by_16(widths[i]) | |
| results[i] = make_refusal_image(verdict, height=h_, width=w_) | |
| continue | |
| active.append(i) | |
| if not active: | |
| return results | |
| gs_key_int = resolve_gs_key(gs_key) | |
| # Per-sample noise tokens + position ids + shapes (MageVAE: flatten, no packing). | |
| ch = model.vae.latent_channels | |
| img_list, ids_list, lens, shapes, hw = [], [], [], [], [] | |
| for i in active: | |
| h_, w_ = _make_divisible_by_16(heights[i]), _make_divisible_by_16(widths[i]) | |
| torch.manual_seed(seeds[i]) | |
| x = get_noise(num_samples=1, channel=ch, height=h_, width=w_, | |
| device=dev, dtype=torch.bfloat16, seed=seeds[i]) | |
| # Distribution-preserving watermark in the initial noise (same shape, | |
| # still ~N(0,1)); detect by inverting the flow ODE back to noise. | |
| x = encode_noise(tuple(x.shape[1:]), key=gs_key_int, | |
| seed=seeds[i], device=dev, dtype=torch.bfloat16) | |
| _, _, gh, gw = x.shape | |
| img_list.append(rearrange(x, "b c h w -> b (h w) c")[0]) | |
| ids = torch.zeros(gh, gw, 3, device=dev) | |
| ids[..., 1] = ids[..., 1] + torch.arange(gh, device=dev)[:, None] | |
| ids[..., 2] = ids[..., 2] + torch.arange(gw, device=dev)[None, :] | |
| ids_list.append(rearrange(ids, "h w c -> (h w) c")) | |
| lens.append(gh * gw); shapes.append((1, gh, gw)); hw.append((h_, w_)) | |
| img = torch.cat(img_list, 0).unsqueeze(0) | |
| img_ids = torch.cat(ids_list, 0).unsqueeze(0) | |
| img_cu = _lens_to_cu(lens, dev) | |
| img_shapes = [shapes] | |
| # Packed text: positive prompts AND (for CFG) negative prompts are encoded | |
| # TOGETHER in ONE varlen forward, then split back — cu_seqlens keeps every | |
| # prompt isolated (verified zero cross-contamination). | |
| pos_prompts = [prompts[i] for i in active] | |
| na = len(active) | |
| use_neg = cfg > 1.0 and any(neg_prompts[i] for i in active) | |
| if use_neg: | |
| neg_list = [neg_prompts[i] or " " for i in active] | |
| txt_flat, vec_all, lens_t = _encode_texts_packed( | |
| model, pos_prompts + neg_list, template, drop_idx, dev) | |
| txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev) | |
| neg_txt, neg_cu, neg_mask, neg_vec = _slice_packed(txt_flat, vec_all, lens_t, na, na, dev) | |
| else: | |
| txt_flat, vec_all, lens_t = _encode_texts_packed(model, pos_prompts, template, drop_idx, dev) | |
| txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev) | |
| neg_txt = neg_cu = neg_mask = neg_vec = None | |
| ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, lens, txt, txt_cu, txt_mask, vec, | |
| neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, dev) | |
| scheduler = _get_scheduler(model, steps, device, static_shift) | |
| for si, t in enumerate(scheduler.timesteps): | |
| pred = _velocity(model.transformer, img, ctx, scheduler.sigmas[si].item()) | |
| img = scheduler.step(pred, t, img, return_dict=False)[0] | |
| off = 0 | |
| for k, i in enumerate(active): | |
| L = lens[k] | |
| h_, w_ = hw[k] | |
| results[i] = _decode_one(model, img[:, off:off + L, :], h_, w_, dev) | |
| off += L | |
| return results | |
| # --------------------------------------------------------------------------- | |
| # Image edit (packed, multi-resolution) | |
| # --------------------------------------------------------------------------- | |
| def _preprocess_ref_image(pil_img: Image.Image, height: int, width: int, device) -> torch.Tensor: | |
| """Resize an RGB reference image to (height, width) and normalize to [-1, 1].""" | |
| from torchvision.transforms import functional as TF | |
| img = pil_img.convert("RGB") | |
| img = TF.resize(img, [height, width], interpolation=TF.InterpolationMode.BICUBIC) | |
| t = TF.to_tensor(img) # [3, H, W] in [0, 1] | |
| t = TF.normalize(t, [0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # -> [-1, 1] | |
| return t.to(device) | |
| def _resize_long_edge(image: Image.Image, max_long_edge: int | None) -> Image.Image: | |
| """Cap the VL conditioning image's long edge, preserving aspect ratio. | |
| Matches training's data.processor._resize_long_edge (BICUBIC). Without this, | |
| inference feeds a full-resolution image to the Qwen-VL processor whose | |
| default max_pixels is far larger than 384**2 — a train/test mismatch. | |
| """ | |
| if max_long_edge is None or max_long_edge <= 0: | |
| return image | |
| w, h = image.size | |
| long_edge = max(w, h) | |
| if long_edge <= max_long_edge: | |
| return image | |
| scale = max_long_edge / long_edge | |
| new_w = max(1, int(round(w * scale))) | |
| new_h = max(1, int(round(h * scale))) | |
| return image.resize((new_w, new_h), Image.BICUBIC) | |
| # Fixed image placeholder used at edit training time (one per reference image). | |
| _EDIT_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>" | |
| def _edit_prompt_body(instruction, num_refs): | |
| """Training-time multi-reference prompt body: ``Image 1: <ph>Image 2: <ph>…{instruction}``.""" | |
| prefix = "".join(f"Image {j}: {_EDIT_IMAGE_PLACEHOLDER}" for j in range(1, num_refs + 1)) | |
| return prefix + instruction | |
| def _encode_edits_packed(model, ref_pils_per_sample, instructions, template, drop_idx, device): | |
| """Encode ALL image-conditioned edit instructions in ONE packed multimodal | |
| varlen forward (pixel_values/image_grid_thw concatenated across samples, | |
| cu_seqlens isolates each). Returns (txt_flat [ΣLi, D], vec [N, D], per-sample lens).""" | |
| processor = model.txt_enc.processor | |
| ids_list, pv_list, thw_list = [], [], [] | |
| for ref_pils, instr in zip(ref_pils_per_sample, instructions, strict=False): | |
| formatted = template.format(_edit_prompt_body(instr, len(ref_pils))) | |
| vl = processor(text=[formatted], images=list(ref_pils), padding=True, return_tensors="pt") | |
| vl = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in vl.items()} | |
| ids_list.append(vl["input_ids"].squeeze(0)) | |
| if vl.get("pixel_values") is not None: | |
| pv_list.append(vl["pixel_values"]); thw_list.append(vl["image_grid_thw"]) | |
| input_ids = torch.cat(ids_list).to(device) | |
| cu = _lens_to_cu([int(t.numel()) for t in ids_list], device) | |
| inputs = {"input_ids": input_ids, "cu_seqlens": cu} | |
| if pv_list: | |
| inputs["pixel_values"] = torch.cat(pv_list, dim=0) | |
| inputs["image_grid_thw"] = torch.cat(thw_list, dim=0) | |
| res = model.txt_enc( | |
| input_ids, cu, inputs=inputs, drop_idx_override=drop_idx) | |
| return res["txt"], res["vec"], res["txt_seq_lens"].tolist() | |
| def generate_edits(model, prompts, ref_images, neg_prompts=None, seeds=None, steps=30, cfg=5.0, | |
| max_size=None, heights=None, widths=None, device="cuda", | |
| prompt_template="mage-flow-edit", static_shift=None, | |
| gs_key=None, | |
| vl_cond_long_edge=384, | |
| renormalization=False, batch_cfg=True): | |
| """Edit reference image(s) per prompt. Each ``ref_images[i]`` may be a single | |
| image/path OR a list of source images (multi-image edit, like training — | |
| trained with up to 3, but more are accepted) — all produce ONE edited output. Each sample's | |
| ``[target, ref_1, …, ref_N]`` latent tokens are sequence-concatenated, and | |
| all samples are packed into one varlen forward per denoise step. | |
| Output resolution (derived from the first/primary reference of each sample): | |
| if both ``heights[i]`` and ``widths[i]`` are given, use them; else if | |
| ``max_size`` is given, the longest side is ``max_size`` and the short side | |
| follows the reference's aspect ratio; otherwise the output keeps the source | |
| image's own resolution. All references are VAE-encoded at that target size. | |
| Returns a list of PIL images. | |
| """ | |
| if isinstance(prompts, str): | |
| prompts = [prompts] | |
| ref_images = [ref_images] | |
| n = len(prompts) | |
| neg_prompts = _as_list(neg_prompts, " ", n) | |
| seeds = _as_list(seeds, 42, n) | |
| heights = _as_list(heights, None, n) | |
| widths = _as_list(widths, None, n) | |
| info = _template_info(prompt_template) | |
| template = info.get("template", "{}") | |
| drop_idx = int(info.get("start_idx", 0)) | |
| dev = torch.device(device) | |
| # Normalize each sample's references to a list of 1..3 PIL images. | |
| def _load_pil(r): | |
| if isinstance(r, str): | |
| r = Image.open(r) | |
| return r.convert("RGB") | |
| pils_per_sample = [] | |
| for r in ref_images: | |
| refs = list(r) if isinstance(r, (list, tuple)) else [r] | |
| if not refs: | |
| raise ValueError("each edit sample needs at least one reference image") | |
| pils_per_sample.append([_load_pil(x) for x in refs]) | |
| # Per-sample output resolution (from the first/primary reference) + content gate. | |
| results = [None] * n | |
| res_hw = [None] * n | |
| active = [] | |
| for i in range(n): | |
| res_hw[i] = _edit_target_size(pils_per_sample[i][0], max_size, heights[i], widths[i]) | |
| if seeds[i] == -1: | |
| seeds[i] = random.randint(0, 2**32 - 1) | |
| # Multimodal gate (MANDATORY): inspect the source image(s) AND the | |
| # instruction, so NSFW / copyrighted-character / real-public-figure | |
| # source photos are blocked even under an innocuous instruction. | |
| verdict = model.txt_enc.screen_edit(prompts[i], pils_per_sample[i]) | |
| if verdict.violates: | |
| h_, w_ = res_hw[i] | |
| results[i] = make_refusal_image(verdict, height=h_, width=w_) | |
| continue | |
| active.append(i) | |
| if not active: | |
| return results | |
| gs_key_int = resolve_gs_key(gs_key) | |
| # Per sample: reference latent tokens (clean) + target noise tokens, plus the | |
| # combined [target, ref_1, …, ref_N] position ids and shapes. ``target_idx`` | |
| # records where each sample's target tokens land in the packed sequence so we | |
| # can slice the velocity and step only the target portion. | |
| ch = model.vae.latent_channels | |
| targets, refs, ids_list, shape_seq, samp_lens, tgt_lens, hw = [], [], [], [], [], [], [] | |
| target_idx_parts = [] | |
| off = 0 | |
| for i in active: | |
| h_, w_ = res_hw[i] | |
| torch.manual_seed(seeds[i]) # MageVAE.encode samples the posterior (global RNG) | |
| # All references resized to the target resolution and VAE-encoded together. | |
| ref_tensors = [_preprocess_ref_image(p, h_, w_, dev) for p in pils_per_sample[i]] | |
| ref_tok, ref_shapes, ref_ids = model.compute_vae_encodings(ref_tensors, with_ids=True) | |
| ref_tok = ref_tok.to(torch.bfloat16) # [1, N*Lr, C] | |
| x = get_noise(num_samples=1, channel=ch, height=h_, width=w_, | |
| device=dev, dtype=torch.bfloat16, seed=seeds[i]) | |
| x = encode_noise(tuple(x.shape[1:]), key=gs_key_int, | |
| seed=seeds[i], device=dev, dtype=torch.bfloat16) | |
| _, _, gh, gw = x.shape | |
| tgt = rearrange(x, "b c h w -> b (h w) c") # [1, Lt, C] | |
| tgt_ids = torch.zeros(gh, gw, 3, device=dev) | |
| tgt_ids[..., 1] = tgt_ids[..., 1] + torch.arange(gh, device=dev)[:, None] | |
| tgt_ids[..., 2] = tgt_ids[..., 2] + torch.arange(gw, device=dev)[None, :] | |
| tgt_ids = rearrange(tgt_ids, "h w c -> (h w) c").unsqueeze(0) | |
| lt, lr = tgt.shape[1], ref_tok.shape[1] | |
| targets.append(tgt); refs.append(ref_tok) | |
| ids_list.append(torch.cat([tgt_ids, ref_ids.to(dev)], dim=1)[0]) # [Lt + N*Lr, 3] | |
| shape_seq.append((1, gh, gw)) # target frame idx 0 | |
| shape_seq.extend(s[0] for s in ref_shapes) # ref_j frame idx j | |
| samp_lens.append(lt + lr); tgt_lens.append(lt); hw.append((h_, w_)) | |
| target_idx_parts.append(torch.arange(off, off + lt, device=dev)) | |
| off += lt + lr | |
| img_ids = torch.cat(ids_list, 0).unsqueeze(0) | |
| img_cu = _lens_to_cu(samp_lens, dev) | |
| img_shapes = [shape_seq] | |
| target_idx = torch.cat(target_idx_parts) | |
| # Packed edit text — positive AND (for CFG) negative are encoded TOGETHER in | |
| # ONE packed multimodal forward, then split. Both branches share the same | |
| # reference images; cu_seqlens isolates every sequence (zero cross-contamination). | |
| # The VL conditioning image's long edge is capped (default 384) to match | |
| # training preprocessing — the VAE path above keeps the full target resolution. | |
| na = len(active) | |
| edit_refs = [[_resize_long_edge(p, vl_cond_long_edge) for p in pils_per_sample[i]] | |
| for i in active] | |
| if cfg > 1.0: | |
| pos_instr = [prompts[i] for i in active] | |
| neg_instr = [neg_prompts[i] or " " for i in active] | |
| txt_flat, vec_all, lens_t = _encode_edits_packed( | |
| model, edit_refs + edit_refs, pos_instr + neg_instr, template, drop_idx, dev) | |
| txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev) | |
| neg_txt, neg_cu, neg_mask, neg_vec = _slice_packed(txt_flat, vec_all, lens_t, na, na, dev) | |
| else: | |
| txt_flat, vec_all, lens_t = _encode_edits_packed( | |
| model, edit_refs, [prompts[i] for i in active], template, drop_idx, dev) | |
| txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, na, dev) | |
| neg_txt = neg_cu = neg_mask = neg_vec = None | |
| ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, samp_lens, txt, txt_cu, txt_mask, vec, | |
| neg_txt, neg_cu, neg_mask, neg_vec, cfg, renormalization, batch_cfg, dev) | |
| scheduler = _get_scheduler(model, steps, device, static_shift) | |
| for si, t in enumerate(scheduler.timesteps): | |
| parts = [] | |
| for k in range(na): | |
| parts.append(targets[k]); parts.append(refs[k]) | |
| img = torch.cat(parts, dim=1) # [1, sum(Lt+Lr), C], ref clean | |
| vel = _velocity(model.transformer, img, ctx, scheduler.sigmas[si].item()) | |
| pred_t = vel[:, target_idx, :] # [1, sum Lt, C] — target tokens only | |
| tgt_packed = torch.cat(targets, dim=1) # [1, sum Lt, C] | |
| stepped = scheduler.step(pred_t, t, tgt_packed, return_dict=False)[0] | |
| o = 0 | |
| new_targets = [] | |
| for k in range(na): | |
| lt = tgt_lens[k] | |
| new_targets.append(stepped[:, o:o + lt, :]); o += lt | |
| targets = new_targets | |
| for k, i in enumerate(active): | |
| h_, w_ = hw[k] | |
| results[i] = _decode_one(model, targets[k], h_, w_, dev) | |
| return results | |
| # --------------------------------------------------------------------------- | |
| # Flow-ODE inversion (Gaussian-Shading watermark detection) | |
| # --------------------------------------------------------------------------- | |
| def invert_to_noise(model, z0, height, width, steps=30, device="cuda", | |
| prompt_template="mage-flow", static_shift=None, prompt=""): | |
| """Reverse the flow ODE from a clean latent ``z0`` back to the initial noise. | |
| This is the detection primitive for the Gaussian-Shading watermark: VAE-encode | |
| the image to ``z0`` (posterior MEAN — deterministic), run this to recover the | |
| initial noise, then read the signs via ``mage_latent.decode_bits``. | |
| Inversion uses an empty prompt at cfg=1 (the standard Tree-Ring / | |
| Gaussian-Shading setup). Reverse Euler recovers ``x_i`` from ``x_{i+1}`` with | |
| the velocity evaluated at the point in hand; the sign-only watermark tolerates | |
| the resulting approximation error (see the module's redundancy). | |
| Args: | |
| z0: clean latent ``[1, C, gh, gw]`` (e.g. the mean of ``model.vae.encode``). | |
| Returns: | |
| recovered initial-noise latent ``[1, C, gh, gw]`` (float32). | |
| """ | |
| dev = torch.device(device) | |
| info = _template_info(prompt_template) | |
| template = info.get("template", "{}") | |
| drop_idx = int(info.get("start_idx", 0)) | |
| z0 = z0.to(dev) | |
| _, ch, gh, gw = z0.shape | |
| img = rearrange(z0, "b c h w -> b (h w) c").to(torch.bfloat16) # [1, gh*gw, C] | |
| ids = torch.zeros(gh, gw, 3, device=dev) | |
| ids[..., 1] = ids[..., 1] + torch.arange(gh, device=dev)[:, None] | |
| ids[..., 2] = ids[..., 2] + torch.arange(gw, device=dev)[None, :] | |
| img_ids = rearrange(ids, "h w c -> (h w) c").unsqueeze(0) | |
| lens = [gh * gw] | |
| img_cu = _lens_to_cu(lens, dev) | |
| img_shapes = [[(1, gh, gw)]] | |
| # Empty-prompt conditioning, no negative branch, cfg=1 (single forward). | |
| txt_flat, vec_all, lens_t = _encode_texts_packed(model, [prompt], template, drop_idx, dev) | |
| txt, txt_cu, txt_mask, vec = _slice_packed(txt_flat, vec_all, lens_t, 0, 1, dev) | |
| ctx = _build_pack_ctx(img_ids, img_cu, img_shapes, lens, txt, txt_cu, txt_mask, vec, | |
| None, None, None, None, 1.0, False, False, dev) | |
| scheduler = _get_scheduler(model, steps, device, static_shift) | |
| sigmas = scheduler.sigmas | |
| n = len(scheduler.timesteps) | |
| # Forward step si: x_{si+1} = x_si + (s_{si+1}-s_si)·v(x_si, s_si). | |
| # Reverse it from clean (x_n, sigma 0) up to noise (x_0), using x_{si+1} as the | |
| # proxy for x_si at the forward eval sigma s_si. | |
| for si in range(n - 1, -1, -1): | |
| s_cur = sigmas[si].item() | |
| s_next = sigmas[si + 1].item() | |
| vel = _velocity(model.transformer, img, ctx, s_cur) | |
| img = img - (s_next - s_cur) * vel | |
| return unpack(img.float(), height, width) # [1, C, gh, gw] | |
| # --------------------------------------------------------------------------- | |
| # High-level pipeline wrapper | |
| # --------------------------------------------------------------------------- | |
| class MageFlowPipeline: | |
| """``MageFlowPipeline.from_pretrained(repo).generate(...) / .edit(...)``. | |
| ``generate`` / ``edit`` are packed multi-resolution calls: they take a list | |
| of prompts (a single string is accepted and treated as a pack of size 1) and | |
| return a list of PIL images. Per-sample ``heights``/``widths``/``seeds`` are | |
| lists. Every prompt is screened by the text encoder's mandatory content | |
| gate (no opt-out); banned prompts come back as refusal placeholders | |
| interleaved with the real images. Real outputs always carry a Gaussian-Shading | |
| watermark in the initial noise (no toggle), using the configured secret key. | |
| """ | |
| def __init__(self, model, device="cuda"): | |
| self.model = model | |
| self.device = device | |
| def from_pretrained(cls, repo_dir: str, device: str = "cuda"): | |
| """Load a Mage-Flow diffusers-style repo (``model_index.json`` + | |
| ``transformer/`` ``vae/`` ``scheduler/`` ``text_encoder/``). | |
| ``repo_dir`` may be a local directory OR a Hugging Face Hub repo id | |
| (e.g. ``"microsoft/Mage-Flow-4B"``), which is downloaded and cached | |
| automatically on first use. | |
| """ | |
| return cls(load_from_repo(repo_dir, device), device) | |
| def generate(self, prompts, **kw) -> list[Image.Image]: | |
| """Packed multi-resolution t2i. ``prompts`` is a list (or a single | |
| string); pass per-sample ``heights``/``widths``/``seeds`` as lists.""" | |
| kw.setdefault("device", self.device) | |
| return generate_images(self.model, prompts, **kw) | |
| def edit(self, prompts, ref_images, **kw) -> list[Image.Image]: | |
| """Packed multi-resolution edit. ``prompts`` is a list (or a single | |
| string); each ``ref_images[i]`` is one reference or a list of references.""" | |
| kw.setdefault("device", self.device) | |
| return generate_edits(self.model, prompts, ref_images, **kw) | |
| def invert_to_noise(self, z0, height, width, **kw): | |
| """Recover the initial noise from a clean latent (Gaussian-Shading detect).""" | |
| kw.setdefault("device", self.device) | |
| return invert_to_noise(self.model, z0, height, width, **kw) | |
| def _safe_subpath(root: str, *parts: str) -> str: | |
| """Join ``parts`` under ``root`` and confirm the result stays inside ``root``. | |
| ``root`` is normalized up front; the joined path is normalized **lexically** | |
| (``os.path.normpath`` — symlinks are *not* followed, so a Hugging Face cache | |
| whose weight files are symlinks into the shared blob store still loads) and | |
| rejected if it escapes ``root``. This guards the user-supplied model path | |
| against path traversal (CWE-22 / CodeQL ``py/path-injection``). | |
| """ | |
| root = os.path.realpath(root) | |
| full = os.path.normpath(os.path.join(root, *parts)) | |
| if full != root and not full.startswith(root + os.sep): | |
| raise ValueError( | |
| f"Resolved path {os.path.join(*parts)!r} escapes repo directory {root!r}" | |
| ) | |
| return full | |
| def _resolve_repo_dir(repo_dir: str) -> str: | |
| """Return a local directory for ``repo_dir``. | |
| If ``repo_dir`` is an existing local path it is returned as a normalized | |
| absolute path; otherwise it is treated as a Hugging Face Hub repo id (e.g. | |
| ``microsoft/Mage-Flow``) and downloaded/cached via | |
| ``huggingface_hub.snapshot_download``. | |
| """ | |
| candidate = os.path.realpath(repo_dir) | |
| if os.path.isdir(candidate): | |
| return candidate | |
| from huggingface_hub import snapshot_download | |
| return snapshot_download(repo_id=repo_dir) | |
| def load_from_repo(repo_dir: str, device: str = "cuda") -> MageFlowModel: | |
| """Load a Mage-Flow diffusers-style repo (model_index.json + transformer/ | |
| vae/ scheduler/). Transformer weights come from the bf16 safetensors; | |
| VAE + text encoder are built from the sources recorded in model_index.json. | |
| ``repo_dir`` may be a local directory OR a Hugging Face Hub repo id (e.g. | |
| ``microsoft/Mage-Flow-4B``), which is downloaded/cached automatically. | |
| """ | |
| from safetensors.torch import load_file | |
| repo_dir = _resolve_repo_dir(repo_dir) | |
| mi = json.load(open(_safe_subpath(repo_dir, "model_index.json"))) | |
| tcfg = json.load(open(_safe_subpath(repo_dir, "transformer", "config.json"))) | |
| # Keys stripped from the checkpoint config before it becomes model_structure. | |
| # ``schedule_mode`` is a legacy field still present in some config.json files; | |
| # Keys of the checkpoint config that are NOT MageFlowParams constructor args | |
| # (legacy/unused fields). Everything else becomes model_structure. The DiT only | |
| # reads: in_channels, out_channels, context_in_dim, hidden_size, num_heads, | |
| # depth, axes_dim, checkpoint, patch_size. | |
| _meta = {"_class_name", "txt_max_length", "max_sequence_length", "param_dtype", | |
| "packing", "schedule_mode", "static_shift", "use_time_shift", | |
| "rope_type", "apply_text_rotary_emb", | |
| "mlp_ratio", "depth_single_blocks", "theta", "qkv_bias", "guidance_embed", | |
| "vec_in_dim", "vec_type", "time_type", "double_block_type"} | |
| structure = {k: v for k, v in tcfg.items() if k not in _meta} | |
| def _resolve(p): | |
| return p if os.path.isabs(p) else _safe_subpath(repo_dir, p) | |
| cfg = ModelConfig( | |
| vae_path=_resolve(mi.get("_vae_source")), | |
| txt_enc_path=_resolve(mi.get("_text_encoder_path")), | |
| model_structure=structure, | |
| txt_max_length=tcfg.get("txt_max_length", 2048), | |
| packing=tcfg.get("packing", True), | |
| static_shift=tcfg.get("static_shift", 6.0), | |
| ) | |
| model = MageFlowModel(cfg) | |
| sd = load_file(_safe_subpath(repo_dir, "transformer", "diffusion_pytorch_model.safetensors"), | |
| device="cpu") | |
| model.transformer.load_state_dict(sd, strict=False, assign=True) | |
| model.to(device) | |
| model.transformer.to(torch.bfloat16) | |
| model.txt_enc.to(torch.bfloat16) | |
| if model.vae is not None: | |
| model.vae.to(torch.bfloat16) | |
| model.eval() | |
| # Diffusers FlowMatchEulerDiscreteScheduler (scheduler/scheduler_config.json). | |
| model.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( | |
| _safe_subpath(repo_dir, "scheduler")) | |
| return model | |