diff --git a/.gitignore b/.gitignore index 491809c..58c5fb9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ __pycache__/ *.egg-info/ dist/ build/ +tmp/ +.sc/ # Environment secrets .env diff --git a/CLAUDE.md b/CLAUDE.md index 6633339..ab0e939 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Per-command exit-code semantics (the no-signal / GPU-missing skip branches), tes - **CI** (`.github/workflows/test.yml`): runs on push to `main` + every PR. A `lint` job (ubuntu: `ruff check` + `ruff format --check`) plus a `test` matrix (ubuntu/macos/windows x py3.10/3.12) that does `uv sync --frozen --extra dev` then `pytest`. The matrix installs only core + dev (no `gpu` extra), so the GPU/model-running tests skip there and it exercises the metadata/identify/visible/cv2-eraser surface on all three OSes. Keep `uv.lock` valid (don't break `--frozen`) when editing `pyproject.toml`. - Dependency PR checks run the GitHub merge ref against current `main`, not the contributor branch in isolation. If `main` moves after the dependency branch was opened, merge current `main` locally and rerun the full gate; a new linter can expose stale directives in code that landed later. -- **Release flow + distribution channels** (PyPI publish via `publish.yml`/`uv publish`, the automated Homebrew-tap + HF-Space bumps in `distribute.yml`, conda-forge, ComfyUI Registry, the sdist `data/` exclusion, hatchling pin history): see `docs/release-and-distribution.md` before cutting a release. +- **Release flow + distribution channels** (PyPI publish via `publish.yml`/`uv publish`, the automated Homebrew-tap + HF-Space bumps in `distribute.yml`, conda-forge, ComfyUI Registry, the sdist public allowlist, hatchling pin history): see `docs/release-and-distribution.md` before cutting a release. - `bash maintain.sh` — uv-outdated, uv-secure, ruff check/fix, ruff format, pyright (scoped `src/`, see the OOM note below), pytest -n auto. The helper tools live in the `dev` extra (`pytest-xdist`, plus `uv-outdated`/`uv-secure` marker-gated to py3.12+ so the py3.10 resolution stays solvable) — a bare env without `--extra dev` does not have them. - **Strict pyright is clean across `src/` (0 errors).** The cv2/torch/diffusers boundary files (`gemini_engine`, `region_eraser`, `doubao_engine`, `humanizer`, `invisible_engine`, `noai/watermark_remover`) carry a documented per-file `# pyright:` relax pragma that turns off only the unknown-type / untyped-third-party rules — those libs ship no usable types, so strict typing there fights the ecosystem. Pure-logic files stay fully strict; `typings/piexif/__init__.pyi` is a local stub so `metadata.py`/`extractor.py` resolve piexif. Public ndarray-returning signatures on the relaxed engines are still annotated `NDArray[Any]` so strict consumers (`cli.py`) stay clean. When touching a relaxed file, prefer fixing real issues over widening the pragma; keep the pragma scoped to genuinely-untyped boundaries. The `uv-secure` CVE-resolution history (idna/aiohttp bumps, retired basicsr, the dismissed torch `GHSA-rrmf-rvhw-rf47`) lives in `docs/release-and-distribution.md` — read it before re-triaging a dependency alert. - **Full-project `uv run pyright` (no path) OOMs/crashes node on this ML-heavy repo** (emits a `libnode` stack frame, no summary) — a known environment limit, not a code error. Gate with `uv run --extra dev --extra gpu pyright src/` (completes, authoritative) or scope to changed files; also run `uv run ruff check` and `uv run pytest` directly. diff --git a/docs/known-limitations.md b/docs/known-limitations.md index 0a09587..fc5a1fc 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -212,10 +212,10 @@ 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. -### Video pixel removal is provider-specific and still experimental +### Video pixel removal is provider-specific -The experimental `video metadata` command and high level video API inspect and -strip supported AI provenance metadata without transcoding streams. +The `video metadata` command and high level video API inspect and strip +supported AI provenance metadata without transcoding streams. `video visible` and `remove_video_visible` additionally support the moving Sora 2 mascot and wordmark, the current Veo four-point diamond, the legacy diff --git a/docs/release-and-distribution.md b/docs/release-and-distribution.md index 7757451..4f0006d 100644 --- a/docs/release-and-distribution.md +++ b/docs/release-and-distribution.md @@ -65,10 +65,14 @@ current line according to the environment. The wheel includes the package under `src/`. -The source distribution explicitly excludes `/data` through -`[tool.hatch.build.targets.sdist]` in `pyproject.toml`. Keep that exclusion: -calibration captures and test corpora do not belong in the published package -archive. +The source distribution uses an explicit allowlist for `/src`, `/LICENSE`, +`/README.md`, and `/pyproject.toml` through +`[tool.hatch.build.targets.sdist]` in `pyproject.toml`. It also defensively +excludes `/data`, `/tmp`, and `/.sc`. Keep both controls: calibration captures, +test corpora, generated research outputs, and local session state do not belong +in the published package archive. The matching local-root entries in +`.gitignore` prevent accidental commits, but are not a substitute for the build +boundary because hatchling may include untracked files. ## Build backend diff --git a/docs/verification-plan.md b/docs/verification-plan.md index ed6dc44..a850587 100644 --- a/docs/verification-plan.md +++ b/docs/verification-plan.md @@ -242,9 +242,9 @@ was negative on both carriers, and a weaker candidate was negative on one. On ordinary Gemini to ignore and reinterpret the detector result. Those follow-ups were mistakenly treated as a stricter oracle; they were not detector reruns. The implementation remains exposed as `video invisible` and -`remove_video_invisible`. Its default is a calibrated operating point, while -each important result still requires a fresh control-positive, -candidate-negative pair. +`remove_video_invisible`. Its oracle-certified default is the product operating +point. A fresh control-positive, candidate-negative pair remains an optional +per-file audit after provider changes or for unusually important files. The 2026-07-31 full-clip check added a public eight-second Veo carrier. The source and the complete `0.10` product output were both detected, proving the diff --git a/packaging/conda/recipe.yaml b/packaging/conda/recipe.yaml index ff2cccd..9e592c1 100644 --- a/packaging/conda/recipe.yaml +++ b/packaging/conda/recipe.yaml @@ -59,7 +59,7 @@ about: Detect and remove registered visible AI-provenance marks and strip AI-provenance metadata (C2PA, EXIF, IPTC, and PNG text chunks) from images and supported video containers. The core package covers the identify, - metadata, visible, erase, and experimental video command surface. Optional + metadata, visible, erase, and product-oriented video command surface. Optional pip extras add SynthID diffusion removal and additional invisible-watermark detectors. license: Apache-2.0 diff --git a/pyproject.toml b/pyproject.toml index 4e73042..a82d5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,9 +191,20 @@ build-backend = "hatchling.build" packages = ["src/remove_ai_watermarks"] [tool.hatch.build.targets.sdist] -# Keep the source distribution small: ship the package and metadata, not -# calibration or evaluation data under data/. The wheel ships only src/. -exclude = ["/data"] +# Keep the source distribution small and public-safe: ship tracked source and +# metadata, not corpora or local research/session artifacts. The wheel ships +# only src/. +include = [ + "/src", + "/LICENSE", + "/README.md", + "/pyproject.toml", +] +exclude = [ + "/data", + "/tmp", + "/.sc", +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/remove_ai_watermarks/video_synthid.py b/src/remove_ai_watermarks/video_synthid.py index 612ec01..e262a2c 100644 --- a/src/remove_ai_watermarks/video_synthid.py +++ b/src/remove_ai_watermarks/video_synthid.py @@ -1,4 +1,4 @@ -"""Shared configuration for experimental video SynthID regeneration.""" +"""Shared configuration for oracle-certified video SynthID removal.""" DEFAULT_VIDEO_SYNTHID_VAE = "stabilityai/sd-vae-ft-mse" DEFAULT_VIDEO_SYNTHID_NOISE_STD = 0.15 diff --git a/tests/test_distribution_boundary.py b/tests/test_distribution_boundary.py new file mode 100644 index 0000000..ce6993b --- /dev/null +++ b/tests/test_distribution_boundary.py @@ -0,0 +1,22 @@ +"""Distribution-boundary regression tests.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +def _array_values(section: str, key: str) -> set[str]: + match = re.search(rf"(?ms)^{key}\s*=\s*\[(.*?)^\]", section) + assert match is not None + return set(re.findall(r'"([^"]+)"', match.group(1))) + + +def test_sdist_has_explicit_public_boundary() -> None: + """Hatchling must publish only package source and required metadata.""" + root = Path(__file__).resolve().parents[1] + config = (root / "pyproject.toml").read_text(encoding="utf-8") + sdist_config = config.split("[tool.hatch.build.targets.sdist]", maxsplit=1)[1].split("\n[", maxsplit=1)[0] + + assert _array_values(sdist_config, "include") == {"/src", "/LICENSE", "/README.md", "/pyproject.toml"} + assert {"/data", "/tmp", "/.sc"} <= _array_values(sdist_config, "exclude")