Library / SDK
facebookresearch/stable_signature avatar
facebookresearch/stable_signature

stable_signature roots a watermark inside the latent diffusion decoder

Official implementation of the paper "The Stable Signature Rooting Watermarks in Latent Diffusion Models"

529 stars68 forksJupyter NotebookNOASSERTION

At a glance

What is it?
stable_signature is the reference implementation of the Stable Signature watermarking paper, a research codebase that fine-tunes a latent diffusion decoder so the watermark lives in the weights rather than in a post-hoc overlay. It suits researchers reproducing the paper, not teams looking for a drop-in watermarking service.
Who is it for?
stable_signature is worth adopting for research groups reproducing the Stable Signature results, for teams comparing rooted watermarks against post-hoc encoding, and for anyone who needs the message decoder and extractor checkpoints rather than a theory.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 52 days ago.
What is it written in?
Mainly Jupyter Notebook, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A watermark that lives in the decoder weights, not on the pixels

stable_signature is the official implementation of the paper "The Stable Signature: Rooting Watermarks in Latent Diffusion Models", presented at ICCV 2023. The name says what the method does: it roots a watermark in a latent diffusion model by fine-tuning the decoder, so that every image the model produces carries the signature without any extra encoding step at generation time.

The contrast with the common approach is what makes the method interesting, and the README states it directly. Standard Stable Diffusion generation applies a post-hoc watermark, and the documentation instructs you to comment out the line that does it, `img = put_watermark(img, wm_encoder)`. Removing that call is the whole point of the method. The signature is in the weights, so no extra encoding step is needed at generation time.

The repository is the paper's code, not a product. It is written mostly as Jupyter Notebook plus Python scripts, and the top-level entries include decoding.ipynb, finetune_ldm_decoder.py, run_evals.py, utils.py, utils_img.py, utils_model.py and a src directory.

Fine-tuning the decoder produces checkpoints, keys and sample images

The central training script is finetune_ldm_decoder.py, and its arguments make the shape of the method clear. You point it at an LDM config, an LDM checkpoint, a message decoder, and a train and validation directory of images, and you say how many keys you want:

bash
python finetune_ldm_decoder.py --num_keys 1 \
    --ldm_config path/to/ldm/config.yaml \
    --ldm_ckpt path/to/ldm/ckpt.pth \
    --msg_decoder_path path/to/msg/decoder/ckpt.torchscript.pt \
    --train_dir path/to/train/dir \
    --val_dir path/to/val/dir

The number of keys is the message capacity. Running with more than one key trains several independent signatures into the same decoder, and the checkpoint file name dec_48b.pth reflects the extractor's size.

Three kinds of output come out of a run. You get num_keys checkpoints of the watermark fine-tuned decoder, named checkpoint_000.pth and so on. You get a text file called keys.txt holding the keys used for fine-tuning, one per line, and that file is what lets you decode a generated image later. You also get an imgs folder of auto-encoded examples, which is a quick way to see what the decoder is producing after the fine-tuning. The paper's fine-tuning parameters and the training logs are linked from the README as external paste pages.

Installation is pinned to a 2022 era PyTorch stack

The setup instructions are explicit about versions, and the versions are old. The README says the codebase was developed with Python 3.8, PyTorch 1.12.0 and CUDA 11.3, and it recommends conda for the main dependencies:

bash
conda install -c pytorch torchvision pytorch==1.12.0 cudatoolkit==11.3

The rest of the dependencies come from a requirements.txt file installed with pip:

bash
pip install -r requirements.txt

That file is worth reading before you commit to the environment. It pins omegaconf 2.1.1, einops 0.3.0, open_clip_torch 2.0.2, torchmetrics 0.6.0, scipy 1.10.1, augly 1.0.0, scikit-image 0.20.0, pytorch-fid 0.3.0 and pandas 1.5.3, and it lists transformers at 5.0.0rc3, a release candidate. The mix of a release candidate with a fixed PyTorch 1.12.0 is a sign that this environment was assembled by hand rather than resolved cleanly, so expect to do some dependency work if your machine is not already close to it.

The repository is first cloned locally before any of this, and the README notes that the perceptual loss weights have to be fetched separately from the PerceptualSimilarity repository and copied into a losses folder, because src/loss/loss_provider.py expects them there.

Two extractor checkpoints, one trained against blur and rotation

The watermark extractor models are hosted as downloadable files, and the README draws a distinction that matters for anyone loading them. The .pth file has not been whitened, while the .torchscript.pt file has been and can be used without any further processing. A second extractor is also provided, one trained with blur and rotations, which the README says holds up better against blur and rotation, at the cost of slightly lower image quality, with a note that you might need to adjust the perceptual loss weight.

Two of these files are fetched directly:

bash
mkdir models
wget https://dl.fbaipublicfiles.com/ssl_watermarking/dec_48b_whit.torchscript.pt -P models/
wget https://dl.fbaipublicfiles.com/ssl_watermarking/other_dec_48b_whit.torchscript.pt -P models/

The table in the README lists four files in total, the .pth and .torchscript.pt variant of dec_48b and the same pair for other_dec_48b, all under the ssl_watermarking path on dl.fbaipublicfiles.com. Code to train the watermark encoder and extractor from scratch lives in a separate folder called hidden, and hidden/README.md covers that stage.

The perceptual loss is the other dependency you must not skip. The losses are based on the PerceptualSimilarity repository, and the weights have to be downloaded and placed in a folder named losses, which is where src/loss/loss_provider.py looks for them at line 22.

Swapping the fine-tuned decoder into a generation script

Using the method means loading your fine-tuned decoder in place of the stock one. With the Stability AI codebase, the README tells you to append four lines after the checkpoint is loaded, for example at line 220 of the SD repo's txt2img.py:

python
state_dict = torch.load(path/to/ldm/checkpoint_000.pth)['ldm_decoder']
msg = model.first_stage_model.load_state_dict(state_dict, strict=False)
print(f"loaded LDM decoder state_dict with message\n{msg}")
print("you should check that the decoder keys are correctly matched")

The strict=False flag is not optional in practice, because the checkpoint stores the decoder under an ldm_decoder key and the surrounding model expects the decoder's own parameters. The two print statements are there because a silent mismatch is the failure mode worth worrying about, and the README explicitly tells you to check that the decoder keys are correctly matched.

A ready-made set of watermarked weights is also published for the SD2 decoder, and because that file contains only the ldm_decoder key, the README notes that you only need `state_dict = torch.load(path/to/ckpt.pth)` in that case. The remaining manual step for either route is removing the post-hoc watermark call, since leaving it in would apply a second watermark on top of the rooted one.

The Diffusers path is marked work in progress

A second route is offered for people using the Diffusers library rather than the original Stable Diffusion scripts. The README gives a snippet that builds the LDM model from a config and checkpoint with OmegaConf and load_model_from_config, then reaches the first stage model through ldm_ae.first_stage_model and calls eval() on it. The config and checkpoint it shows are for the SD 2.1 base model.

The caveat is stated in the same paragraph: the snippet is still WIP and might be updated in the future, and it pins transformers to 4.25.1 and diffusers to 0.25.1, which conflicts with the transformers 5.0.0rc3 listed in requirements.txt. Two different transformers versions in the same repository is a sign that the two paths have not been reconciled.

The failure indicator is specific. The README warns to make sure that no "decoder.*" keys appear in the output of print(unexpected_keys), because that means the LDM decoder was not loaded at all. If the checkpoint came from finetune_ldm_decoder.py rather than the published weights, the assignment has to be made against the inner ldm_decoder key instead, and issue 29 in the repository is the place where this is discussed. A watermark that silently fails to embed is the worst kind of bug in this area, and that check is the only guard the README offers.

Training data, evaluation and the missing release history

The training data guidance is light but usable. The paper uses the COCO dataset to fine-tune the LDM decoder, with images containing people filtered out, and the README says all you need is around 500 images for training, preferably over 256x256. A few hundred images rather than a full dataset is a meaningful practical detail for anyone with limited time or disk.

Evaluation lives in run_evals.py, and the dependency list explains why: pytorch-fid and torchmetrics are there for image quality measurement, augly is there for the augmentation attacks used to test resistance, and open_clip_torch is there for perceptual similarity. The README also links to a project webpage, the arXiv entry, a Meta blog post and a Hugging Face demo space,

The maintenance picture needs stating directly. The repository is not archived, and the last push was on 2026-09-02, and it has no GitHub releases at all, so there is no version history to pin against. The code also predates the current Stable Diffusion line: the README says it should work for Stable Diffusion v1 without any change, and that for other models such as old LDMs or VQGANs you may need to adapt the code to load the checkpoints.

Editorial conclusion

stable_signature is worth adopting for research groups reproducing the Stable Signature results, for teams comparing rooted watermarks against post-hoc encoding, and for anyone who needs the message decoder and extractor checkpoints rather than a theory. It is the wrong choice for a production pipeline that needs a supported library, a stable API or current Stable Diffusion releases, because the codebase is pinned to Python 3.8, PyTorch 1.12.0 and CUDA 11.3, its Diffusers path is marked as work in progress, and the licence is a custom one that GitHub cannot classify. Before you start, read the LICENSE file at the repository root, and check that the pinned PyTorch and CUDA versions still exist for your accelerator.

Frequently asked questions

What does stable_signature do differently from post-hoc watermarking?

It fine-tunes the latent diffusion decoder so the watermark is rooted in the weights, and the README instructs you to comment out the post-hoc call `img = put_watermark(img, wm_encoder)` so the two are not combined.

How many images do I need to fine-tune the LDM decoder?

The README says around 500 images are enough for training, preferably over 256x256, using the COCO dataset with images containing people filtered out.

Which stable_signature extractor checkpoint should I download?

The .torchscript.pt files have been whitened and can be used without further processing, while the .pth files have not. A second extractor trained with blur and rotations is provided for greater resistance to those distortions at a slight cost in image quality.

What Python and PyTorch versions does stable_signature need?

The README states the codebase was developed with Python 3.8, PyTorch 1.12.0 and CUDA 11.3, with the PyTorch install given as `conda install -c pytorch torchvision pytorch==1.12.0 cudatoolkit==11.3` and the rest installed from requirements.txt.

How do I know the fine-tuned decoder actually loaded?

The README says to check that the decoder keys are correctly matched after load_state_dict, and in the Diffusers path to make sure no "decoder.*" keys appear in print(unexpected_keys), because that means the LDM decoder was not loaded.

Official sources

  1. facebookresearch/stable_signature on GitHub
  2. Issues
  3. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/facebookresearch-stable-signature.svg)](https://hysenlabs.com/projects/facebookresearch-stable-signature)

Was this useful?