Diffusers

Read this if you run a diffusers pipeline in your own Python process and want every image it returns marked. You will attach Laidline to the pipeline with one call; each image then carries the watermark, Content Credentials and an evidence entry. Skip if your images come from an OpenAI-shaped server (OpenAI proxy) or from ComfyUI (ComfyUI). Time: 10 minutes plus the checkpoint download. Needs: Python 3.12, pip install laidline-diffusers "laidline[image]", a laidline.yaml from laidline init, and for SDXL a GPU with 12 GB of VRAM.

In short

  • attach(pipe, marker) wraps the pipeline's __call__; every image it returns is marked exactly once.
  • out.assets[i] is the signed file (watermark and Content Credentials); out.images carry the watermark only.
  • SDXL's invisible-watermark step is replaced by default, and detach(pipe) restores it.
  • A marking failure raises MarkError: no unmarked image is returned.
  • Everything runs in your process; the prompt never reaches the marker.

laidline-diffusers marks every image a diffusers pipeline returns (PixelSeal watermark by default, C2PA manifest, evidence entry), exactly once per image, inside your own process.

# brings laidline[image]; `laidline-diffusers[diffusers]` adds diffusers 0.40, transformers and torch
# (CUDA torch by default, about a 5.7 GB environment; see the install page for the CPU index)
pip install laidline-diffusers "laidline[image]"
from diffusers import StableDiffusionXLPipeline
from laidline import Marker
from laidline_diffusers import attach

# load once per process
marker = Marker.from_config("laidline.yaml")
pipe = attach(StableDiffusionXLPipeline.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0"), marker)

out = pipe(prompt="a lighthouse at dusk")
# the signed file: watermark and C2PA manifest
out.assets[0].save("lighthouse.png")
# PIL image with the watermark, no manifest
image = out.images[0]

Images under 128 px on the short side are signed but not watermarked (watermark_skipped:too_small in out.assets[i] warnings).

What you need

On a GPU install torch from your CUDA index first, then the rest: one install that resolved a local-version torch pin (torch==2.14.0+cu130) together with Laidline and diffusers failed on an empty environment, and installing torch first worked:

pip install torch==2.14.0+cu130 torchvision==0.29.0+cu130 --index-url https://download.pytorch.org/whl/cu130
pip install laidline-diffusers "laidline[image]" "diffusers>=0.40,<0.41" transformers accelerate invisible-watermark
  • invisible-watermark for SDXL. Without it diffusers builds SDXL pipelines with no watermarker (pipe.watermark is None), so attach has nothing to replace.
  • 12 GB of VRAM or more for SDXL base at 1024 px in fp16 (the run peaked at 11.28 GB allocated, before the CUDA context and the allocator's reserve).
  • For offline runs, HF_HUB_OFFLINE=1 once the checkpoint is in the Hugging Face cache, and a pinned checkpoint revision for repeatable runs (the run used 462165984030d82259a11f4367a4eed129e94a7b).

What attach does

  • It wraps __call__. The pipeline's class is swapped for a cached subclass with the same __name__, __qualname__ and __module__, so type(pipe).__name__ is unchanged and save_pretrained still writes the original _class_name. image_processor.postprocess is not wrapped because a pipeline may call it more than once per run (Stable Diffusion's safety checker does), which would mark every image twice.
  • out.images carry the watermark only. Each image goes through Marker.mark_image, and out.images holds the signed file's decoded pixels in the original type, shape and dtype (PIL list, np array or pt tensor). A manifest cannot live in pixels.
  • out.assets[i] is the signed file (MarkedAsset: data, asset_id, manifest_id, b64(), save(path)). Store or serve asset.data; re-encoding the pixels from out.images drops the manifest.
  • With return_dict=False the tuple's first element is replaced; the assets are then reachable only through the evidence log. output_type="latent" is not marked (out.assets == []).
  • SDXL's invisible watermarker is replaced. SDXL pipelines apply StableDiffusionXLWatermarker (the invisible-watermark DWT-DCT mark, known to leave visible artifacts) before postprocess; attach sets pipe.watermark = None (pass replace_watermarker=False to keep it). detach(pipe) restores the class and the watermarker.
  • Fail closed. A MarkError propagates from the pipeline call: no unmarked image is returned.

Options: attach(pipe, marker=None, *, fmt="png", ctx=None, replace_watermarker=True). Without marker the process-wide marker is used (LAIDLINE_URL for a remote sidecar, or LAIDLINE_CONFIG), resolved at attach so a missing environment fails before any generation. fmt is png, jpeg or webp. Evidence entries carry route: diffusers:<PipelineClass> (or ctx.route) and extra: {index, model}, where model is the pipeline's name_or_path. The prompt never reaches the marker.

What was proven

Checked on diffusers 0.40.0 tiny random pipelines on CPU (a Stable Diffusion and an SDXL one, downloaded from the Hugging Face hub), run through the real pipeline code with attach. Each run produced one evidence entry per image, a C2PA manifest that validates as trusted, an unchanged class name (and _class_name after save_pretrained), no prompt in the file, and for SDXL a stand-in watermarker replaced by attach and restored by detach. Random weights at 128 px produce noise that may fall below the PSNR floor, so those runs accept a skipped watermark. That the watermark survives into both out.images and out.assets was checked on a fake pipeline, which also covers the double-postprocess case (the Stable Diffusion run used safety_checker=None, so no real safety checker was exercised).

On 21 Oct 2026 a production checkpoint was run too: stabilityai/stable-diffusion-xl-base-1.0 (fp16, diffusers 0.40.0, torch 2.14.0+cu130) on an RTX 5080. Four 1024 px images (20 steps) were marked with attach. Each was detected as marked (grade B, the PixelSeal payload matched the evidence log, the C2PA manifest trusted under the development chain the run generated), the same pixels without the manifest were still detected, and an image from the detached pipeline was not. Peak VRAM was 11.3 GB, and marking and signing added about 7.5 to 10 s per image. This used a development signing chain and unkeyed payloads, so it shows the wiring and the watermark on a real checkpoint, not a registered issuer. invisible-watermark has to be installed for SDXL to carry its own watermarker at all (diffusers skips it when the package is absent), so attach's replacement only matters then.

Reference

  • Integrations: what each integration was proven against, and how.
  • Evidence: the route and extra fields an entry carries for a diffusers image.
  • Install: extras, the CPU and CUDA torch indexes, model downloads.
  • Config: the laidline.yaml that Marker.from_config reads.
  • Grading: how a detected image is graded.