Add streaming video SynthID regeneration

This commit is contained in:
Victor Kuznetsov
2026-07-29 22:03:23 -07:00
parent c9b8585105
commit 63cde7f32a
20 changed files with 1278 additions and 387 deletions
+32
View File
@@ -153,6 +153,38 @@ different container extension.
Visible video labels and invisible video watermarks are not handled by this
command.
## Generate a video SynthID candidate
```bash
uv tool install --force "remove-ai-watermarks[gpu]"
remove-ai-watermarks video invisible input.mp4 -o candidate.mp4
```
The experimental command supports MP4, MOV, and M4V. It samples the complete
sequence at the configured frame rate, resizes frames to the configured long
side, regenerates them through a VAE, and applies one deterministic latent-noise
field to every frame. Reusing one spatial field avoids the unnecessary flicker
caused by independent per-frame noise. Frames are regenerated in bounded
batches and streamed directly to ffmpeg, which encodes the result, copies
audio, and drops source metadata.
The output is always an unverified candidate. The project has no local video
SynthID decoder, and PSNR or temporal-residual metrics cannot prove watermark
absence. After generation, upload the candidate to Gemini Flash and ask:
> Was this uploaded video created or edited by Google AI? Use the built-in
> content verification result.
Only an explicit built-in verification result is an oracle verdict. A response
based on the visible logo, content appearance, or metadata is not. The command
prints `UNVERIFIED` even when generation succeeds.
The default output is `<source>_synthid_candidate` in the same container. The
source is never overwritten. Use `--noise-std`, `--long-side`, `--fps`,
`--batch-size`, `--seed`, and `--device` to control the regeneration. The
defaults are calibrated operating points, not a guarantee for every carrier or
future verifier version.
## Remove a supported visible video mark
```bash
+26 -11
View File
@@ -79,17 +79,31 @@ For important outputs:
Provider systems can change, so a result verified on one file, seed, or version
is not a permanent certification.
### Video regeneration is research only
### Video regeneration produces an unverified candidate
The package does not expose a `video invisible` command. The separate
`scripts/video_synthid_sweep.py` harness generates a matched transcode control
and VAE-regenerated candidates for external verification. It deliberately
leaves verdicts empty because neither the metadata proxy nor a visual quality
metric can prove that the pixel watermark is gone.
The `video invisible` command and `remove_video_invisible` API regenerate video
pixels through a VAE, but cannot verify the proprietary SynthID payload locally.
Every successful result remains explicitly unverified and requires Google's
matching content-verification flow. A quiet metadata scan, paired PSNR, and the
temporal-residual metric are not substitutes for that oracle.
The control must use the same clip, frame rate, dimensions, and final codec as
the candidates. If the control is not detected by the matching provider oracle,
the experiment cannot attribute a quiet candidate to regeneration.
the candidates. The separate `scripts/video_synthid_sweep.py` harness produces
that matched control. If the control is not detected by the matching provider
oracle, the experiment cannot attribute a quiet candidate to regeneration.
The 2026-07-29 calibration used two public Veo clips. Both matched controls were
SynthID-positive. The default stronger VAE candidate was negative on both, while
a weaker candidate remained positive on one. This small calibration validates
the mechanism and exposes its content dependence; it does not certify all
videos, full-length sequences, alternate settings, or future verifier versions.
The shipped engine streams sampled frames in bounded batches, computes its
fidelity metrics incrementally, and pipes regenerated pixels directly to
ffmpeg. Its frame and latent memory is therefore bounded by `--batch-size`
rather than clip duration. Runtime still grows linearly with duration, and the
separate multi-candidate research sweep deliberately retains its short sampled
prefix so it can reuse identical latents across candidate strengths.
### Strength is content and seed dependent
@@ -186,7 +200,7 @@ WebM, Matroska, MP3, WAV, FLAC, OGG, Opus, and AAC container metadata is strippe
through ffmpeg with stream copying. The operation fails if ffmpeg is absent or
cannot parse the input.
### Visible video removal is provider-specific and still experimental
### Video pixel removal is provider-specific and still experimental
The experimental `video metadata` command and high level video API inspect and
strip supported AI provenance metadata without transcoding streams.
@@ -201,8 +215,9 @@ after visual evidence exists, so metadata alone does not erase a clean API
export.
Historical Sora Turbo exports use a small OpenAI swirl in the corner rather
than the moving mascot-and-wordmark design; that earlier variant is not
detected by the `sora` video mark. Other provider video labels and proprietary
invisible video watermarks are not supported yet.
detected by the `sora` video mark. Other provider video labels are not
supported yet. Google video SynthID has a candidate-producing VAE path, while
other proprietary invisible video watermarks have no registered attack.
Visible removal transcodes the video stream and copies audio. Its frame-local
fill is not a motion-aware video inpainting model. OpenCV can leave a visible
+26 -1
View File
@@ -90,6 +90,7 @@ Regression coverage:
video entry point:
- `inspect_video_metadata`
- `remove_video_invisible`
- `remove_video_metadata`
- `remove_video_visible`
@@ -97,7 +98,7 @@ The video API validates both the supported extension and container signature,
then delegates all metadata detection and stripping to `metadata.py`. It
requires a separate same-container output, defaulting to `<source>_clean`, so
the experimental path does not overwrite an original. The package root exposes
both functions lazily.
all four functions lazily.
Native MP4/MOV TC260 labels follow TC260-PG-20257A:
`moov.udta.meta.keys` maps an `AIGC` key to a raw JSON value in `ilst`.
@@ -114,6 +115,30 @@ only a `Segment.Tags.Tag.SimpleTag` pairing `TagName=AIGC` with a JSON
`TagString` carrying a TC260 field. The existing ffmpeg stream-copy path removes
those container tags without transcoding the encoded streams.
[`video_encoding.py`](../src/remove_ai_watermarks/video_encoding.py) owns the
raw-BGR ffmpeg command and pipe lifecycle shared by visible removal and
invisible regeneration. It centralizes container codecs, optional audio stream
copying, metadata/chapter policy, and encode-failure reporting.
[`video_invisible.py`](../src/remove_ai_watermarks/video_invisible.py)
implements the oracle-gated video SynthID candidate engine. It samples frames
uniformly, resizes to a VAE-aligned geometry, encodes each frame to latent
space, applies one seeded spatial-noise field across the entire sequence, and
decodes fresh pixels. Reusing a single noise field avoids independent
frame-to-frame noise. The shipped path retains only one configured frame batch,
updates PSNR and temporal residuals incrementally, and streams BGR frames
directly to ffmpeg. ffmpeg encodes H.264 video, maps optional source audio, and
drops all source metadata. The result is written through a same-directory
temporary file and atomically replaced only after a successful encode.
The engine returns PSNR and a motion-compensated temporal-residual ratio as
quality measurements. Neither is a watermark detector. The high-level
`VideoInvisibleResult.requires_external_verification` flag is always true, and
the CLI prints an `UNVERIFIED` warning plus the Gemini Flash verification
prompt. The companion `scripts/video_synthid_sweep.py` imports the same engine
helpers to build a matched control and candidate grid, preventing research and
shipped regeneration paths from drifting.
[`video_visible.py`](../src/remove_ai_watermarks/video_visible.py) implements
the first pixel stages for Sora, Veo, Seedance, and Dola. The Sora detector
searches a normalized frame with a fully synthetic mascot-and-text silhouette
+28
View File
@@ -154,6 +154,34 @@ stream bytes. MKV/WebM inspection recognizes the corresponding
`Segment.Tags.Tag.SimpleTag` representation; its removal requires ffmpeg for a
stream-copy remux.
## Generate a video SynthID candidate
```python
import remove_ai_watermarks as raiw
result = raiw.remove_video_invisible(
"input.mp4",
"candidate.mp4",
device="auto",
)
assert result.requires_external_verification
if result.remaining_metadata:
raise RuntimeError(f"AI metadata remains: {result.remaining_metadata}")
```
`remove_video_invisible` supports MP4, MOV, and M4V. It regenerates the complete
video through a VAE in bounded batches, shares one seeded latent-noise field
across all frames, streams pixels to ffmpeg, copies audio, and strips source
metadata. The default output is
`input_synthid_candidate.mp4`; a distinct same-container output is required.
The returned `VideoInvisibleResult` includes output geometry, frame rate, frame
count, paired PSNR, and the motion-compensated temporal-residual ratio. Those
fields measure fidelity and flicker only. They are not a SynthID detector.
`requires_external_verification` is always true because Google does not publish
a local decoder for this video payload. Verify the candidate with Gemini
Flash's built-in content verification before treating it as watermark-negative.
## Remove a supported visible video mark
```python
+5
View File
@@ -113,6 +113,10 @@ SynthID does not have a public local pixel decoder in this project. The tool can
infer likely presence from supported provenance metadata, but after that
metadata is removed a local negative result is inconclusive.
For MP4, MOV, and M4V, `video invisible` can regenerate the video through a VAE
and strip source metadata. This is a candidate-producing attack, not a local
decoder. Every result still requires Gemini Flash's built-in verification.
The optional `detect` extra is different: it provides a local decoder for the
open DWT-DCT watermark used by some Stable Diffusion, SDXL, and FLUX workflows.
That signal is carrier and transformation sensitive, so a negative is still
@@ -123,6 +127,7 @@ not a universal clean verdict.
| Provider or family | Visible | Invisible path | Metadata or provenance |
| --- | --- | --- | --- |
| Google Gemini | Sparkle | Diffusion regeneration for SynthID | C2PA and related source signals |
| Google Veo video | Veo diamond and legacy text | VAE regeneration candidate for SynthID | C2PA and related source signals |
| OpenAI image generators | None registered | Diffusion regeneration for supported invisible signals | C2PA and generator provenance |
| Stable Diffusion and SDXL | None registered | Diffusion regeneration; optional open decoder | Embedded parameters and text metadata |
| FLUX | None registered | Diffusion regeneration; optional open decoder | C2PA for supported sources |
+13 -2
View File
@@ -331,8 +331,11 @@ framework.
Gemini's verification flow can report the portions of a video where it detects
Google SynthID. This is still a proprietary oracle: a normal Gemini answer that
describes visual clues, metadata, or an unavailable decoder is not a pixel
verdict. Use the dedicated verification flow offered to an eligible signed-in
account; some versions expose an explicit `@synthid` trigger.
verdict. Google's current support flow is to upload the file to an eligible
signed-in Gemini account and ask whether it was created or edited by Google AI.
In the 2026-07-29 calibration, Flash invoked the built-in verifier while Pro
first answered from the visible Veo logo; the model mode is therefore part of
the recorded procedure, not an interchangeable chat preference.
The research harness `scripts/video_synthid_sweep.py` tests a VAE regeneration
attack without pretending to detect success locally. It emits:
@@ -350,6 +353,14 @@ control-positive, candidate-negative pair is evidence about the regeneration
attack. PSNR and temporal residual measure fidelity and flicker, never watermark
presence.
The shipped `video invisible` command and `remove_video_invisible` API reuse the
same VAE regeneration mechanism for a complete input sequence. They always
label the output as requiring external verification. Calibration on 2026-07-29
used two public Veo clips: both matched controls were positive, the stronger
default candidate was negative on both, and a weaker candidate was negative on
only one. This establishes a content-dependent operating point, not a universal
clean verdict.
The VAE perturbation follows the general regeneration-attack construction from
Zhao et al. The video-specific control and temporal metric are local additions.
VideoMarkBench motivates testing frame aggregation and matched perturbations,
+8
View File
@@ -232,6 +232,14 @@ The harness shares one latent-noise field across the sequence to avoid adding
independent frame noise. Its temporal-residual metric is a fidelity check, not a
watermark detector.
The 2026-07-29 run completed the first two-carrier calibration through Gemini
Flash's built-in content verification. Both matched Veo controls were positive.
The stronger default VAE candidate was negative on both carriers; a weaker
candidate was negative on only one. The implementation is exposed as
`video invisible` and `remove_video_invisible`, but every result remains
explicitly oracle-required because the small calibration does not establish
full-corpus or future-verifier coverage.
## Tier E -- robustness and adversarial inputs
Malformed and hostile inputs, including truncated files: