Files
remove-ai-watermarks/scripts/synthid_runtime_expert_scores.py
T
Victor Kuznetsov 5c6588b3ef Fix output preservation, research validation, and documentation drift
Repair image and metadata publication contracts, detection routing, research
artifact validation, release verification, and the published agent skill.
Refresh the dependency lock and reconcile documentation with verified behavior.

Add regression coverage and record the completed audit and corrected VideoSeal
measurements. Preserve the existing Accelerate and Lightning versions.

Validation: Ruff and Pyright pass; 2358 tests pass with 3 skipped. The maintenance
gate exits 2 on the documented Accelerate and Lightning security findings.
2026-09-08 19:07:10 -07:00

124 lines
4.1 KiB
Python

"""Export correctly named fixed, large, and routed registered observations.
The output is an input manifest for ``synthid_conformal_cascade.py``. All
experts consume decoded RGB pixels only. Unsupported geometry is recorded
explicitly and never represented by a synthetic score.
"""
from __future__ import annotations
import json
import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING, TypedDict
import click
import numpy as np
if TYPE_CHECKING:
from numpy.typing import NDArray
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from synthid_pixel_attack import load_rgb # noqa: E402
from synthid_research_manifest import artifact_sha256 # noqa: E402
from synthid_runtime import synthid_detector # noqa: E402
log = logging.getLogger(__name__)
FIXED_EXPERT_NAME = synthid_detector.DETECTOR_ID
REGISTERED_EXPERT_NAME = synthid_detector.REGISTERED_DETECTOR_ID
LARGE_EXPERT_NAME = synthid_detector.LARGE_DETECTOR_ID
OPPONENT_EXPERT_NAME = synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID
FINE_OPPONENT_EXPERT_NAME = synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID
EXPERT_NAMES = (
FIXED_EXPERT_NAME,
REGISTERED_EXPERT_NAME,
LARGE_EXPERT_NAME,
OPPONENT_EXPERT_NAME,
FINE_OPPONENT_EXPERT_NAME,
)
class ExpertScore(TypedDict):
"""One JSON-safe runtime expert observation."""
name: str
supported: bool
score: float | None
class ScoredImage(TypedDict):
"""One hash-pinned image with every runtime expert observation."""
id: str
path: str
width: int
height: int
observations: list[ExpertScore]
def _observation(name: str, supported: bool, score: float | None) -> ExpertScore:
return {"name": name, "supported": supported, "score": score}
def score_pixels(pixels: NDArray[np.uint8]) -> list[ExpertScore]:
"""Return five named slots; inactive registered fallbacks remain unsupported."""
if pixels.ndim != 3 or pixels.shape[2] != 3 or pixels.dtype != np.uint8:
raise ValueError("pixels must be an RGB uint8 array")
bgr_pixels = np.ascontiguousarray(pixels[:, :, ::-1])
native = synthid_detector.detect_synthid("decoded-image", image=bgr_pixels, register_scale=False)
registered = synthid_detector.detect_synthid("decoded-image", image=bgr_pixels, register_scale=True)
observations = {name: _observation(name, False, None) for name in EXPERT_NAMES}
for result, expected in (
(native, {FIXED_EXPERT_NAME, LARGE_EXPERT_NAME}),
(registered, {REGISTERED_EXPERT_NAME, OPPONENT_EXPERT_NAME, FINE_OPPONENT_EXPERT_NAME}),
):
if result.detector not in expected:
raise RuntimeError(f"unexpected SynthID expert: {result.detector}")
observations[result.detector] = _observation(result.detector, result.status != "unsupported", result.score)
return list(observations.values())
def score_path(path: Path) -> ScoredImage:
"""Decode PATH once and return one hash-pinned observation record."""
pixels = load_rgb(path)
height, width = pixels.shape[:2]
return {
"id": artifact_sha256(path),
"path": str(path),
"width": width,
"height": height,
"observations": score_pixels(pixels),
}
@click.command()
@click.argument("images", nargs=-1, required=True, type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option("--report-out", type=click.Path(dir_okay=False, path_type=Path), required=True)
def main(images: tuple[Path, ...], report_out: Path) -> None:
"""Score IMAGES with every shipped pixel expert."""
logging.basicConfig(level=logging.INFO, format="%(message)s")
records = [score_path(path) for path in images]
report_out.parent.mkdir(parents=True, exist_ok=True)
report_out.write_text(
json.dumps(
{
"schema_version": 2,
"experts": list(EXPERT_NAMES),
"records": records,
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
log.info("Wrote %d runtime expert score records: %s", len(records), report_out)
if __name__ == "__main__":
main()