Skip to main content

pagedMark

test platform python license backend

AI watermark removal built for Apple Silicon. Strip provenance from images you generated yourself — invisible pixel watermarks, visible vendor labels, and the metadata that carries the rest — on the GPU already in your Mac.

Most tooling in this space assumes an NVIDIA card and treats anything else as a fallback. pagedMark is the opposite: Metal is the primary target, and every decision in the pipeline — which sampler runs, when the VAE tiles, how faces are repaired, where the numerical floors sit — was measured on Apple hardware rather than inherited from CUDA.

uv tool install --force "pagedmark[diffusion]"
pagedmark invisible photo.png -o clean.png
  Input:    photo.png
  Pipeline: sdxl-zimage on mps
  Strength: 0.15
  Running SDXL Canny pass: strength=0.1500, steps=16 of 107...
  Repairing face 1/4 with SDXL: strength=0.0500, steps=8...
  Saved: clean.png  (2253 KB, 152.1s)

Pipeline

flowchart LR
    IN([input]) --> ID[identify<br/><i>what does it carry?</i>]

    ID --> V[visible mark<br/>locate → mask → fill]
    ID --> P[pixel watermark]
    ID --> M[metadata<br/>C2PA · EXIF · XMP · IPTC]

    P --> G[global regeneration<br/>SDXL + Canny ControlNet<br/><b>Metal</b> or CUDA]
    G --> F[face repair<br/>YuNet → SAM → crop regen]

    V --> OUT([clean output])
    F --> OUT
    M --> OUT

Each branch runs only where its signal was found, and each reports what it did. The two GPU stages are the only ones that need Metal; everything else runs on any machine.

Capabilities

Signal Approach GPU
Invisible pixel watermarks — SynthID-class marks from gpt-image, Gemini, Nano Banana Structure-guided diffusion regeneration disrupts the embedded pattern Metal or CUDA
Visible AI labels — Gemini sparkle, Doubao, Jimeng, Qwen, Kling, Yuanbao, Baidu, LibLibAI, Samsung Galaxy AI Located in its expected region, masked, and filled None
Provenance metadata — C2PA Content Credentials, EXIF, XMP, IPTC, generator parameters, TC260 AIGC tags Format-aware stripping that never recompresses the image None
Video — Sora, Veo, Seedance, Dola, Hailuo, Kling marks, and video SynthID Per-frame removal with audio copied untouched and timestamps preserved None for marks and metadata; Metal or CUDA for SynthID
Region erase — anything you point at User-directed inpainting with OpenCV, MI-GAN, or LaMa None

Always start by asking what a file actually carries:

pagedmark identify photo.png

identify reports unknown rather than clean when it finds nothing. Metadata is erased by re-encoding, screenshots and uploads, and no public local decoder exists for SynthID-class watermarks — so absence of evidence is reported as exactly that.

Why Metal-first matters

Apple Silicon is not a smaller CUDA card. Four differences change the output, and each number below was measured on an M5 whose unified memory budget (torch.mps.recommended_max_memory()) is 11.84 GiB.

The distilled sampler had to go

Four-step distillation LoRAs are trained for four timesteps spanning the entire noise range. A low-strength edit runs the tail of a long schedule instead, which is off that distribution — and wherever nothing conditions the model (flat dark fabric offers the ControlNet no edges) it fills the gap by inventing texture.

Global stage · 1448×1080 · strength 0.15 Invented texture PSNR Wall
Distilled, 4 steps 1.73× source 28.54 dB 41 s
Distilled, 8 steps 1.80× 28.19 dB 29 s
Distilled, 16 steps 1.84× 27.85 dB 62 s
Undistilled base, 16 steps 1.19× 29.25 dB 71 s
Undistilled base, 24 steps 1.20× 29.17 dB 132 s

More steps of the distilled model make it worse, which is what identifies the distillation rather than the step count as the cause. 24 undistilled steps buy nothing over 16, so 16 is a measured knee.

Faces are repaired by the stack already in memory

The high-fidelity face model in this lineage streams float8 weights, and Metal implements no float8 tensor type at all. So the stage keeps its shape — face detection, SAM masks, each crop regenerated from the original at a lower strength, feathered back in — and substitutes the SDXL stack already resident for the global pass. Nothing extra to load.

Four faces · measured against the source Whole frame Faces (mean)
Global stage only 29.00 dB 27.39 dB
With the face stage 29.14 dB 30.50 dB

Memory is measured, not hoped for

Metal answers an oversized allocation by paging rather than failing, so a run that "works" can silently take an hour. pagedMark reads the budget and decides:

  • VAE tiling only where it pays. Untiled, a 1.57 MP frame needs 18.74 GiB against an 11.84 GiB budget and survives on swap alone, at 2.6× the wall time. On a 64 GiB Mac the same frame decodes whole, because tile boundaries leave a faint texture worth avoiding when there is room for none.
  • Oversized frames tile at native geometry, so nothing is quietly downscaled to fit.
  • Attention slicing always on, the device cache released between passes, and noise drawn on a CPU generator so one seed means one result across machines.

Video runs here too

Video is not a CUDA-only corner of this project. Visible mark removal and metadata stripping need no GPU at all, and SynthID regeneration — a VAE round-trip with one latent-noise field shared across time — resolves to Metal through the same probed device ladder as the image path, releasing the device cache per frame batch so a clip's peak stays at one batch instead of growing with its length.

pagedmark video identify clip.mp4      # what the container and pixels carry
pagedmark video all clip.mp4 -o clean.mp4   # visible marks + metadata, CPU
pagedmark video invisible clip.mp4 -o clean.mp4   # SynthID regeneration, Metal

Audio is copied without re-encoding, variable frame intervals survive through a timestamped bridge rather than being flattened, and the completed encode is published atomically. The regeneration profile's own numbers were measured on CUDA; the Metal path runs the same graph, and the wall time is the difference to expect.

8 GB Macs are a supported target, not a rounding error

The stack is 7.7 GiB of weights once the text encoders are gone, and an 8 GB Mac reports a working set of roughly 5.3 GiB — so it does not fit, with or without that saving. Rather than refuse, pagedMark reads the budget and streams the weights from the CPU one module at a time. Same output, and a peak that fits anything:

Same frame, same seed Peak device memory Wall
Weights resident (16 GB Mac) 7.70 GiB 7.1 s
Weights streamed (8 GB Mac) 0.28 GiB 24.1 s

The plan is chosen from the measured budget and then stated: a run three times slower than the fast path says so before you wait through it, instead of looking broken. Below about 2 GiB nothing is left to trade, and the refusal names the commands that still work there rather than failing inside a model loader.

One saving was measured, implemented, and then withdrawn. Encoding the two fixed prompts once and releasing the text encoders frees 1.52 GiB of a loaded 8.79 GiB — and with them released, two of the four face crops in a test photo came back as all-zero black rectangles, deterministically, at the same seed that produced correct faces with them resident. The embeddings are not at fault (CPU and Metal encodings of that prompt agree to 0.0009 on tensors with a standard deviation of 3.06, and the same crop generated in isolation is correct either way); the allocation pattern the crops meet after the global pass is. 1.5 GiB is not worth a black rectangle over someone's face, and the offloaded path does not need it. A guard now drops any empty crop and keeps the global stage's face instead, whatever produced it.

A preview, sized to be worth running

--preview answers "what will this do to my picture" before the real run. It caps the frame at 512 px, shrinks the face crops with it, and repairs the largest face only — because capping the frame alone changes nothing: the face stage scales every crop toward a 768 px guide regardless of the frame, so a naive preview cost 66 s against the real run's 113 s. With all three caps it is 46.6 s against 112.6 s on the measured photo.

It writes <source>_preview.png, never the _clean name, and prints what it is. Its fidelity numbers are not the run's numbers: a preview shows character, not final quality, because the downscale and upscale are part of what you are looking at.

Metal has numerical floors

Below a face strength of 0.05 the fp16 path stops producing an image: three of four faces returned as garbage at the 0.025 a naive policy computes. That floor lives in code and is pinned by a test, not carried as folklore.

Requirements

  • Apple Silicon (M-series) running macOS, or an NVIDIA GPU
  • Python 3.11 – 3.14
  • ~10 GB disk for the diffusion model stack on first run
  • 16 GB unified memory is enough for the Metal path; more removes the tiling compromises

There is no CPU path for invisible removal, and pagedMark refuses a device that cannot run it instead of falling back to something that will not finish. Visible-mark removal, metadata stripping and identify run anywhere.

Install

You want Install
Metadata inspection and stripping pagedmark
Visible marks and region erasing pagedmark[visible]
Invisible removal on Apple Silicon pagedmark[diffusion]
Invisible removal on NVIDIA pagedmark[qwen-zimage]
Video pagedmark[video]
Everything available on your Python pagedmark[all]

On macOS the default PyTorch wheel already ships Metal support, so pagedmark[diffusion] is the complete Apple Silicon install — it deliberately omits the float8 model stack that cannot load there.

Usage

# What is in this file?
pagedmark identify photo.png

# Visible mark and metadata — no GPU required
pagedmark visible photo.png -o clean.png
pagedmark metadata photo.png --remove -o clean.png

# Erase a region you choose
pagedmark erase photo.png --region 1640,1930,400,100 -o clean.png

# A faster, lower-resolution look before committing to the real run
pagedmark invisible photo.png --preview

# The full pipeline: visible → invisible → metadata
pagedmark all photo.png -o clean.png

# A whole directory
pagedmark batch ./photos --mode all

# Video
pagedmark video identify clip.mp4
pagedmark video all clip.mp4 -o clean.mp4
pagedmark video invisible clip.mp4 -o clean.mp4
pagedmark video batch ./clips --mode all

The device is detected and the pipeline follows from it. Both are printed before the run starts, so a substituted stage is something you read rather than something you discover in the output.

Python

import pagedmark as pm

report = pm.identify("photo.png")
result, removed = pm.remove_visible("watermarked.png", "clean.png")

What this does not claim

  • Regeneration is not payload deletion. The image changes; faces, text and fine detail can move. Every figure above is the measured size of that change, so the tradeoff is yours to accept rather than a promise to trust.
  • Metal is not bit-identical to CUDA. The same seed and strength produce a different image on the two backends. Operating points transfer between them; verdicts recorded against a provider's verifier do not.
  • The Metal face stage is a different model from the CUDA one. It measured better than no face stage, and face crops carry less disruption than the frame does — the same bargain the CUDA stage strikes.
  • Verify what matters. Where a provider publishes a verifier, run the output through it before relying on the result.

Scope

For content you generated or own. pagedMark does not target stock-agency previews, marketplace or classifieds watermarks, or any mark protecting someone else's paid asset: the visible-mark registry accepts AI-generation labels only, and the region eraser is user-directed by design. See scope, safety and legal notes.

Documentation

License

Apache 2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pagedmark-0.1.0.tar.gz (355.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pagedmark-0.1.0-py3-none-any.whl (389.4 kB view details)

Uploaded Python 3

File details

Details for the file pagedmark-0.1.0.tar.gz.

File metadata

  • Download URL: pagedmark-0.1.0.tar.gz
  • Upload date:
  • Size: 355.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pagedmark-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e4d6b435e45c34176af4e89e734dc7bc191daae193280e71de12c0f9139f7570
MD5 18b12592b1ad9a3198eecca7e1e6363e
BLAKE2b-256 6ab1ef50994613e807fd2f9a47fd5b4d5db2e165cdcd8866ef398baee712abbc

See more details on using hashes here.

File details

Details for the file pagedmark-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pagedmark-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 389.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pagedmark-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c492115eace0a075a18b7c7942777ea395648e2c4f5e846e3466f934d80d3a1
MD5 fd00575de848729f6973e34387f3ccc6
BLAKE2b-256 177f7af47a3c1b4934a28685d8d85546400e85d4f3da05de682fe82b16eaf58e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page