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]", alaidline.yamlfromlaidline 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.imagescarry 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-watermarkfor SDXL. Without it diffusers builds SDXL pipelines with no watermarker (pipe.watermarkis None), soattachhas 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=1once the checkpoint is in the Hugging Face cache, and a pinned checkpoint revision for repeatable runs (the run used462165984030d82259a11f4367a4eed129e94a7b).
What attach does
- It wraps
__call__. The pipeline's class is swapped for a cached subclass with the same__name__,__qualname__and__module__, sotype(pipe).__name__is unchanged andsave_pretrainedstill writes the original_class_name.image_processor.postprocessis 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.imagescarry the watermark only. Each image goes throughMarker.mark_image, andout.imagesholds the signed file's decoded pixels in the original type, shape and dtype (PIL list,nparray orpttensor). A manifest cannot live in pixels.out.assets[i]is the signed file (MarkedAsset:data,asset_id,manifest_id,b64(),save(path)). Store or serveasset.data; re-encoding the pixels fromout.imagesdrops the manifest.- With
return_dict=Falsethe 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) beforepostprocess;attachsetspipe.watermark = None(passreplace_watermarker=Falseto keep it).detach(pipe)restores the class and the watermarker. - Fail closed. A
MarkErrorpropagates 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
routeandextrafields an entry carries for a diffusers image. - Install: extras, the CPU and CUDA torch indexes, model downloads.
- Config: the
laidline.yamlthatMarker.from_configreads. - Grading: how a detected image is graded.