fix: prefer checkout venv for SynthID scorer subprocess (#115) (#123)

This commit is contained in:
Guillaume Meyer (The Opinionated Man)
2026-08-17 16:34:51 -07:00
committed by GitHub
parent 00596a7270
commit a430827827
2 changed files with 63 additions and 6 deletions
+19 -4
View File
@@ -1240,6 +1240,17 @@ def _synthid_score_http(
return payload
def _synthid_python(upstream: Path) -> str:
"""Prefer the checkout venv so the scorer deps (cv2, sklearn) are importable."""
if os.name == "nt":
venv = upstream / ".venv" / "Scripts" / "python.exe"
else:
venv = upstream / ".venv" / "bin" / "python"
if venv.is_file():
return str(venv)
return sys.executable
def run_synthid_score(
path: Path,
upstream_dir: str | None = None,
@@ -1248,8 +1259,9 @@ def run_synthid_score(
Uses the HTTP sidecar when WATERMARKS_SYNTHID_SCORER_URL is set,
otherwise a subprocess against a local checkout. Returns None when the
scorer is not configured or unavailable (exit 3), so callers can keep
the default "no SynthID score" behavior.
scorer is not configured; a dict with "available": False and an "error"
when it is configured but unavailable (e.g. exit 3), so callers can
distinguish "not scored" from "scored and clean".
"""
scorer_url = os.environ.get("WATERMARKS_SYNTHID_SCORER_URL", "").strip()
if scorer_url:
@@ -1266,7 +1278,7 @@ def run_synthid_score(
script = SCRIPTS_DIR / "score_synthid.py"
cmd = [
sys.executable,
_synthid_python(Path(upstream_dir)),
str(script),
str(path),
"--upstream-dir",
@@ -1286,7 +1298,10 @@ def run_synthid_score(
return {"available": False, "error": str(e)}
if r.returncode == 3:
return None
return {
"available": False,
"error": (r.stderr or "SynthID scorer unavailable (exit 3)").strip()[:2000],
}
if r.returncode != 0:
return {"available": False, "error": (r.stderr or "").strip()[:2000]}
try:
+44 -2
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
@@ -45,7 +46,7 @@ def test_run_synthid_score_unconfigured_returns_none(
assert run_synthid_score(Path("x.png")) is None
def test_run_synthid_score_unavailable_returns_none(
def test_run_synthid_score_unavailable_returns_error(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
@@ -53,7 +54,48 @@ def test_run_synthid_score_unavailable_returns_none(
return SimpleNamespace(returncode=3, stdout="", stderr="unavailable")
monkeypatch.setattr(image_meta.subprocess, "run", fake_run)
assert run_synthid_score(Path("x.png"), upstream_dir=str(tmp_path / "upstream")) is None
result = run_synthid_score(Path("x.png"), upstream_dir=str(tmp_path / "upstream"))
assert result is not None
assert result.get("available") is False
assert "unavailable" in result.get("error", "")
def test_run_synthid_score_prefers_checkout_venv_python(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
upstream = tmp_path / "upstream"
venv_python = upstream / ".venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
venv_python.parent.mkdir(parents=True)
venv_python.write_text("#!/bin/sh\n")
captured: dict = {}
def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
return SimpleNamespace(returncode=0, stdout="{}", stderr="")
monkeypatch.setattr(image_meta.subprocess, "run", fake_run)
run_synthid_score(Path("img.png"), upstream_dir=str(upstream))
assert captured["cmd"][0] == str(venv_python)
def test_run_synthid_score_falls_back_to_sys_executable(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
upstream = tmp_path / "upstream" # no .venv present
captured: dict = {}
def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
return SimpleNamespace(returncode=0, stdout="{}", stderr="")
monkeypatch.setattr(image_meta.subprocess, "run", fake_run)
run_synthid_score(Path("img.png"), upstream_dir=str(upstream))
assert captured["cmd"][0] == sys.executable
def test_run_synthid_score_parses_json(