"""Build oracle-gated video regeneration candidates for SynthID research. This is a research harness, not a shipped removal command. Google does not publish a local video SynthID decoder, so the script cannot label a candidate as clean. It produces: * a re-encode control with the same duration, frame rate, dimensions, and codec; * one VAE-regenerated video per requested latent-noise level; * paired fidelity and temporal-residual measurements; * a CSV column for the external Gemini SynthID verdict. The control is load-bearing. If it reads clean, the experiment is invalid: resize, frame-rate conversion, or H.264 compression already silenced the oracle, so a VAE candidate cannot be credited with removal. The regeneration attack follows the general encode, perturb, reconstruct family from WatermarkAttacker (NeurIPS 2024). A single spatial latent-noise sample is shared by every frame. Independent per-frame noise creates avoidable flicker and does not test the video-specific question. Run with the project's GPU extra: uv run --extra gpu python scripts/video_synthid_sweep.py input.mp4 -o out/ Then upload ``control.mp4`` and each candidate in separate Gemini chats, invoke the built-in SynthID verifier (``@synthid``), and use the question printed by the script. Do not follow the verdict with an adversarial prompt asking the chat model to reinterpret the detector: that switches back to ordinary reasoning. Only a control-positive, candidate-negative pair from the built-in verifier is removal evidence. """ from __future__ import annotations import csv import hashlib import logging from pathlib import Path from typing import TYPE_CHECKING import click import numpy as np from remove_ai_watermarks.video_invisible import ( _decode_frame_latents, _encode_frame_latents, _fit_size, _probe_video, _shared_latent_noise, build_temporal_reference, encode_video_frames, load_video_vae_runtime, paired_psnr, read_sampled_frames, temporal_residual_ratio, ) from remove_ai_watermarks.video_synthid import ( DEFAULT_VIDEO_SYNTHID_FPS, DEFAULT_VIDEO_SYNTHID_LONG_SIDE, DEFAULT_VIDEO_SYNTHID_VAE, VIDEO_SYNTHID_LATENT_MULTIPLE, VIDEO_SYNTHID_VERIFICATION_PROMPT, ) if TYPE_CHECKING: from collections.abc import Sequence log = logging.getLogger(__name__) def _parse_noise_levels(values: str) -> tuple[float, ...]: levels = tuple(float(value.strip()) for value in values.split(",") if value.strip()) if not levels: raise click.BadParameter("At least one noise level is required") if any(not 0.0 <= value <= 1.0 for value in levels): raise click.BadParameter("Noise levels must be between 0 and 1") return levels def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _write_manifest(output_dir: Path, rows: Sequence[dict[str, str]]) -> Path: path = output_dir / "sweep.csv" # Mirrors the tracked manifest's run-configuration fields (data/README.md) so a # row carries into data/evaluations/video-synthid-oracle.csv without hand # reconstruction. The two 2026-07-31 rows show the cost of not doing this: their # geometry was never recorded and cannot be recovered from the row. fieldnames = [ "variant", "source_sha256", "source_width", "source_height", "source_fps", "duration_seconds", "vae", "noise_std", "long_side", "fps", "seed", "psnr_db", "temporal_residual_ratio", "file", "sha256", "synthid_oracle", ] with path.open("w", newline="", encoding="utf-8") as stream: writer = csv.DictWriter(stream, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) return path @click.command() @click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option("-o", "--output-dir", required=True, type=click.Path(file_okay=False, path_type=Path)) @click.option("--noise-levels", default="0,0.05,0.1,0.15", show_default=True) @click.option("--duration", type=click.FloatRange(min=0.1), default=2.0, show_default=True) @click.option("--fps", type=click.FloatRange(min=1.0), default=DEFAULT_VIDEO_SYNTHID_FPS, show_default=True) @click.option( "--long-side", type=click.IntRange(min=VIDEO_SYNTHID_LATENT_MULTIPLE), default=DEFAULT_VIDEO_SYNTHID_LONG_SIDE, show_default=True, ) @click.option("--batch-size", type=click.IntRange(min=1), default=4, show_default=True) @click.option("--seed", type=int, default=0, show_default=True) @click.option("--model", default=DEFAULT_VIDEO_SYNTHID_VAE, show_default=True) @click.option("--device", type=click.Choice(["auto", "cuda", "mps", "cpu"]), default="auto", show_default=True) def main( source: Path, output_dir: Path, noise_levels: str, duration: float, fps: float, long_side: int, batch_size: int, seed: int, model: str, device: str, ) -> None: """Generate VAE video candidates from the prefix of SOURCE.""" logging.basicConfig(level=logging.INFO, format="%(message)s") levels = _parse_noise_levels(noise_levels) width, height, source_fps = _probe_video(source) size = _fit_size(width, height, long_side) frames, effective_fps = read_sampled_frames(source, duration=duration, output_fps=fps, size=size) run = { "source_sha256": _sha256(source), "source_width": str(width), "source_height": str(height), "source_fps": f"{source_fps:.4f}", "duration_seconds": f"{duration:.4f}", "vae": model, "long_side": str(long_side), "fps": f"{effective_fps:.4f}", "seed": str(seed), } output_dir.mkdir(parents=True, exist_ok=True) control_path = output_dir / "control.mp4" encode_video_frames(frames, source, control_path, fps=effective_fps) rows: list[dict[str, str]] = [ { **run, "variant": "control", "noise_std": "", "psnr_db": "inf", "temporal_residual_ratio": "1", "file": control_path.name, "sha256": _sha256(control_path), "synthid_oracle": "", } ] # Load through the engine's own loader rather than repeating it here: the # scaling-factor gate lives there, and the harness that produces the certified # rows is the last place that should be exempt from it. runtime = load_video_vae_runtime(model=model, device=device) vae, resolved_device = runtime.vae, runtime.resolved_device log.info("Encoding source frames") latent_batches = _encode_frame_latents( frames, vae=vae, device=resolved_device, batch_size=batch_size, scaling_factor=runtime.scaling_factor, ) first_latents = latent_batches[0] shared_noise = _shared_latent_noise( first_latents.shape[1:], seed=seed, device=resolved_device, dtype=first_latents.dtype, ) reference_stack = np.stack(frames) temporal_maps, temporal_baseline = build_temporal_reference(frames) for level in levels: log.info("Decoding latent noise %.4f", level) regenerated = _decode_frame_latents( latent_batches, vae=vae, noise_std=level, shared_noise=shared_noise, scaling_factor=runtime.scaling_factor, ) output_path = output_dir / f"vae-noise-{level:.4f}.mp4" encode_video_frames( regenerated, source, output_path, fps=effective_fps, ) psnr = paired_psnr(reference_stack, np.stack(regenerated)) temporal_ratio = temporal_residual_ratio(regenerated, temporal_maps, temporal_baseline) rows.append( { **run, "variant": "vae", "noise_std": f"{level:.4f}", "psnr_db": f"{psnr:.4f}", "temporal_residual_ratio": f"{temporal_ratio:.4f}", "file": output_path.name, "sha256": _sha256(output_path), "synthid_oracle": "", } ) manifest = _write_manifest(output_dir, rows) log.info("Wrote %s", manifest) log.info( "Verify control.mp4 first with Gemini's built-in SynthID verifier and this question: %s", VIDEO_SYNTHID_VERIFICATION_PROMPT, ) if __name__ == "__main__": main()