Skip to content

Add UniPic3Pipeline for multi-image editing and composition#14201

Open
chenyangzhu1 wants to merge 1 commit into
huggingface:mainfrom
chenyangzhu1:add-unipic3-pipeline-final
Open

Add UniPic3Pipeline for multi-image editing and composition#14201
chenyangzhu1 wants to merge 1 commit into
huggingface:mainfrom
chenyangzhu1:add-unipic3-pipeline-final

Conversation

@chenyangzhu1

@chenyangzhu1 chenyangzhu1 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds UniPic3Pipeline to src/diffusers/pipelines/unipic3/ for multi-image editing and composition
  • Reuses existing model components (QwenImageTransformer2DModel, AutoencoderKLQwenImage, FlowMatchEulerDiscreteScheduler)
  • Adds unit tests, API documentation, and toctree entry
  • Registered in all three __init__.py levels and dummy objects

What is UniPic-3?

UniPic-3 is a unified framework by Skywork for single-image editing and multi-image composition, supporting 1-6 input images with arbitrary output resolutions. It achieves SOTA multi-image composition quality.

Model HuggingFace Inference Steps
Base Model Skywork/Unipic3 50 steps
Consistency Model Skywork/Unipic3-Consistency-Model 8 steps
DMD Model Skywork/Unipic3-DMD 8 steps

Key Design Decisions

  1. Pipeline-only change: UniPic-3 uses the exact same model components as the existing QwenImageEdit pipeline. No new model classes, schedulers, or weight conversion scripts are needed.

  2. Base pipeline only (50-step): The CM/DMD fast variant (8-step) uses a custom sampling loop that bypasses scheduler.step(), violating diffusers conventions (.ai/pipelines.md gotcha make from hub import work #3). This can be added later with a proper scheduler implementation.

  3. Separate pipeline vs extending QwenImageEditPipeline: Per diffusers conventions, each pipeline gets its own file and class. The multi-image input, chat-template prompt encoding, and per-image latent handling are sufficiently different from QwenImageEditPipeline to warrant a new class.

Key Differences from QwenImageEditPipeline

Aspect QwenImageEditPipeline UniPic3Pipeline
Input image: PipelineImageInput (single) images: list[PipelineImageInput] (1-6)
Prompt encoding Hardcoded template string processor.apply_chat_template() with system message + multi-image
Image preprocessing Single image → calculate_dimensions Total pixel budget normalization across all images
Latent preparation Single image latent tensor List of per-image latent tensors
img_shapes 2 entries [output, input] N+1 entries [output, input1, ..., inputN]

Files Changed

New files

  • src/diffusers/pipelines/unipic3/__init__.py — Lazy import module
  • src/diffusers/pipelines/unipic3/pipeline_unipic3.py — Main pipeline implementation
  • tests/pipelines/unipic3/__init__.py — Test package init
  • tests/pipelines/unipic3/test_unipic3.py — Pipeline unit tests
  • docs/source/en/api/pipelines/unipic3.md — API documentation

Modified files

  • src/diffusers/__init__.py — Register UniPic3Pipeline in exports
  • src/diffusers/pipelines/__init__.py — Register unipic3 module
  • src/diffusers/utils/dummy_torch_and_transformers_objects.py — Add dummy class
  • docs/source/en/_toctree.yml — Add documentation entry

Test plan

  • from diffusers import UniPic3Pipeline works
  • test_inference — Multi-image generation produces correct output shape
  • test_inference_single_image — Single image input works
  • make style passes
  • make fix-copies passes

Usage Examples

Single image editing

import torch
from PIL import Image
from diffusers import UniPic3Pipeline

pipe = UniPic3Pipeline.from_pretrained("Skywork/Unipic3", torch_dtype=torch.bfloat16)
pipe.to("cuda")

image = Image.open("cat.png").convert("RGB")

result = pipe(
    images=[image],
    prompt="Make the cat wear a small red party hat on its head.",
    negative_prompt=" ",
    num_inference_steps=50,
    true_cfg_scale=4.0,
    generator=torch.manual_seed(42),
).images[0]

result.save("cat_with_hat.png")

Two-image composition

import torch
from PIL import Image
from diffusers import UniPic3Pipeline

pipe = UniPic3Pipeline.from_pretrained("Skywork/Unipic3", torch_dtype=torch.bfloat16)
pipe.to("cuda")

cat = Image.open("cat.png").convert("RGB")
landscape = Image.open("landscape.png").convert("RGB")

result = pipe(
    images=[cat, landscape],
    prompt="The cat is sitting on a cliff in the fantasy landscape, looking at the view.",
    negative_prompt=" ",
    num_inference_steps=50,
    true_cfg_scale=4.0,
    generator=torch.manual_seed(42),
).images[0]

result.save("cat_in_landscape.png")

Three-image composition

import torch
from PIL import Image
from diffusers import UniPic3Pipeline

pipe = UniPic3Pipeline.from_pretrained("Skywork/Unipic3", torch_dtype=torch.bfloat16)
pipe.to("cuda")

cat = Image.open("cat.png").convert("RGB")
pikachu = Image.open("pikachu.png").convert("RGB")
landscape = Image.open("landscape.png").convert("RGB")

result = pipe(
    images=[cat, pikachu, landscape],
    prompt="The cat and the pikachu are exploring the fantasy landscape together, standing on a cliff.",
    negative_prompt=" ",
    num_inference_steps=50,
    true_cfg_scale=4.0,
    generator=torch.manual_seed(42),
).images[0]

result.save("cat_pikachu_landscape.png")

Loading components separately

import torch
from diffusers import (
    UniPic3Pipeline,
    FlowMatchEulerDiscreteScheduler,
    QwenImageTransformer2DModel,
    AutoencoderKLQwenImage,
)
from transformers import AutoModel, AutoTokenizer, Qwen2VLProcessor

model_path = "Skywork/Unipic3"

scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(model_path, subfolder="scheduler")
text_encoder = AutoModel.from_pretrained(model_path, subfolder="text_encoder", device_map="auto", torch_dtype=torch.bfloat16)
tokenizer = AutoTokenizer.from_pretrained(model_path, subfolder="tokenizer")
processor = Qwen2VLProcessor.from_pretrained(model_path, subfolder="processor")
transformer = QwenImageTransformer2DModel.from_pretrained(model_path, subfolder="transformer", device_map="auto", torch_dtype=torch.bfloat16)
vae = AutoencoderKLQwenImage.from_pretrained(model_path, subfolder="vae", torch_dtype=torch.bfloat16).to(transformer.device)

pipe = UniPic3Pipeline(
    scheduler=scheduler, vae=vae, text_encoder=text_encoder,
    tokenizer=tokenizer, processor=processor, transformer=transformer,
)

Real-Weight Validation (Skywork/Unipic3, 50-step)

5 additional tests using real photographs and artwork, all passed:

A: Single Image Edit — Cat wearing a hat (1 image)

  • Input: Real cat photo (1024×704)
    • cat
  • Prompt: "Make the cat wear a small red party hat on its head."
  • Result: ✓ Cat preserved with a red party hat added naturally on its head (1248×864)
    • A_cat_hat

B: Single Image Edit — Style transfer (1 image)

  • Input: Yarn art Pikachu (1024×1024)
    • pikachu
  • Prompt: "Transform this yarn art pikachu into a watercolor painting style."
  • Result: ✓ Pikachu converted to watercolor style while preserving pose and features (1024×1024)
    • B_pikachu_watercolor

C: Two-Image Composition — Cat in landscape (2 images)

  • Input: Real cat photo + Fantasy landscape painting
    • cat
    • landscape
  • Prompt: "The cat is sitting on a cliff in the fantasy landscape, looking at the view."
  • Result: ✓ Cat seamlessly composited into the fantasy landscape, sitting on a cliff (1056×736)
    • C_cat_landscape

D: Two-Image Composition — Character mashup (2 images)

  • Input: Yarn art Pikachu + 3D cute monster
    • pikachu
    • cute_monster
  • Prompt: "The pikachu and the cute furry monster are sitting together as friends."
  • Result: ✓ Both characters composed together naturally, maintaining their original styles (736×736)
    • D_pikachu_monster

E: Three-Image Composition — Multi-character scene (3 images)

  • Input: Real cat + Yarn art Pikachu + Fantasy landscape
    • cat
    • pikachu
    • landscape
  • Prompt: "The cat and the pikachu are exploring the fantasy landscape together, standing on a cliff."
  • Result: ✓ All three elements composed into a coherent scene (736×512)
    • E_cat_pikachu_landscape

Self-Review Report

Blocking Issues — All Fixed

  1. B1: VersatileDiffusionDualGuidedPipeline accidentally removed from TYPE_CHECKING importsFixed: Restored
  2. B2: VersatileDiffusionDualGuidedPipeline dummy object replacedFixed: Restored via check_dummies.py
  3. B3: max_sequence_length accepted but unusedConsistent with QwenImageEditPipeline (parameter is validated in check_inputs but not truncated — same pattern)

Non-Blocking Issues

  1. NB1: _import_structure["unipic3"] placement in pipelines/__init__.py not perfectly alphabetical — consistent with how chronoedit and glm_image are appended in the same block
  2. NB2: images=None not validatedFixed: Added validation in __call__
  3. NB3: sigmas inlined — consistent with QwenImageEditPipeline pattern

Dead Code (Advisory — Kept for Consistency)

  • self.tokenizer_max_length = 1024 — same as QwenImageEditPipeline, may be used by LoRA mixins
  • self.default_sample_size = 128 — same as QwenImageEditPipeline
  • retrieve_latents "sample" branch — # Copied from function, kept for sync

Verdict: READY

Fixes #13053

Before submitting

  • Did you use an AI agent (Claude Code, Codex, Cursor, etc.) to help with this PR? If so:
    • Did you read the Coding with AI agents guide?
    • Did you run the self-review skill on the diff?
    • Did you share the final self-review notes in the PR description or a comment?
  • Did you read the contributor guideline?
  • Did you read our philosophy doc? (important for complex PRs)
  • Was this discussed/approved via a GitHub issue or the forum? Please add a link to it if that's the case.
  • Did you make sure to update the documentation with your changes? Here are the
    documentation guidelines, and
    here are tips on formatting docstrings.
  • Did you write any new necessary tests?
  • Are you the author (or part of the team) of the model/pipeline (only applicable for model/pipeline related PRs)?

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@yiyixuxu @dg845 @asomoza @sayakpaul @DN6

UniPic-3 is a unified framework by Skywork for single-image editing and
multi-image composition (1-6 input images). It reuses the existing
QwenImageTransformer2DModel, AutoencoderKLQwenImage, and
FlowMatchEulerDiscreteScheduler. The key differences from QwenImageEditPipeline
are chat-template-based prompt encoding with system messages, per-image VAE
encoding with sequence-dimension concatenation, and total pixel budget
normalization across multiple input images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added fixes-issue size/L PR with diff > 200 LOC documentation Improvements or additions to documentation tests utils pipelines and removed size/L PR with diff > 200 LOC labels Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation fixes-issue pipelines tests utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support Unipic 3.0 Pipeline

1 participant