mirror of
https://github.com/guillaumemeyer/watermarks-remover.git
synced 2026-08-22 13:11:57 +02:00
fix: --json no longer suppresses the residual-signal exit code (#30)
clean_file.py and clean_image.py computed the failure exit code inside the human-output branch, so `--json` always exited 0 even when the clean left C2PA/AI signals behind. A script gating on `clean_file --json` would treat a still-marked file as clean. Move the residual (and degraded-PDF) decision out of the output branch in both entry points so the exit code is the same regardless of --json. Human output is unchanged; degraded best-effort PDF copies stay non-failures. Adds tests asserting json and human modes return the same exit code for residual, clean, and degraded cases. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
256d90d1b1
commit
ba42162b66
@@ -148,16 +148,16 @@ def main() -> int:
|
||||
eprint(f"error: {e}")
|
||||
return 1
|
||||
result = {"kind": "image", **result}
|
||||
residual = result["still_has_c2pa"] or result["still_has_ai_metadata"]
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
eprint(f"wrote {result['output']} ({result['bytes_in']} -> {result['bytes_out']})")
|
||||
for a in result["actions"]:
|
||||
eprint(f" - {a}")
|
||||
if result["still_has_c2pa"] or result["still_has_ai_metadata"]:
|
||||
if residual:
|
||||
eprint("warning: residual C2PA/AI signals may remain")
|
||||
return 1
|
||||
return 0
|
||||
return 1 if residual else 0
|
||||
|
||||
try:
|
||||
result = clean_container(src, dest)
|
||||
@@ -165,21 +165,20 @@ def main() -> int:
|
||||
eprint(f"error: {e}")
|
||||
return 1
|
||||
result = {"kind": "container", **result}
|
||||
residual = result["still_has_c2pa"] or result["still_has_ai_metadata"]
|
||||
degraded = bool(result.get("meta", {}).get("degraded"))
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
eprint(f"wrote {result['output']} format={result['format']}")
|
||||
for a in result["actions"]:
|
||||
eprint(f" - {a}")
|
||||
if result["still_has_c2pa"] or result["still_has_ai_metadata"]:
|
||||
if residual:
|
||||
eprint("warning: residual C2PA/AI signals may remain")
|
||||
for f in result.get("post_findings") or []:
|
||||
eprint(f" ! {f}")
|
||||
# degraded PDF copy is not a hard failure if we only warn
|
||||
if result.get("meta", {}).get("degraded"):
|
||||
return 0
|
||||
return 1
|
||||
return 0
|
||||
# A degraded (best-effort) PDF copy warns but is not a hard failure.
|
||||
return 1 if (residual and not degraded) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -109,6 +109,10 @@ def main() -> int:
|
||||
eprint(f"error: {e}")
|
||||
return 1
|
||||
|
||||
pr = result.get("pixel_removal")
|
||||
residual = result["still_has_c2pa"] or result["still_has_ai_metadata"]
|
||||
failed = residual or (pr is not None and not pr.get("available"))
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
@@ -129,24 +133,16 @@ def main() -> int:
|
||||
f"confidence {result['synthid_after'].get('confidence', 0.0):.3f} "
|
||||
f"(watermarked: {label})"
|
||||
)
|
||||
pr = result.get("pixel_removal")
|
||||
if pr is not None:
|
||||
if pr.get("available"):
|
||||
eprint(f"CtrlRegen: removed on {pr.get('device', 'unknown device')}")
|
||||
else:
|
||||
eprint(f"CtrlRegen: unavailable: {pr.get('error', 'unknown error')}")
|
||||
|
||||
failed = False
|
||||
if result["still_has_c2pa"] or result["still_has_ai_metadata"]:
|
||||
if residual:
|
||||
eprint("warning: residual C2PA/AI signals may remain")
|
||||
for f in result.get("post_findings") or []:
|
||||
eprint(f" ! {f}")
|
||||
failed = True
|
||||
if pr is not None and not pr.get("available"):
|
||||
failed = True
|
||||
if failed:
|
||||
return 1
|
||||
return 0
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""--json must not suppress the residual-signal exit code (was: always 0)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS = ROOT / "skills" / "remove-ai-marks" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import clean_file # noqa: E402
|
||||
import clean_image # noqa: E402
|
||||
|
||||
|
||||
def _container_result(dest: Path, residual: bool, degraded: bool = False) -> dict:
|
||||
return {
|
||||
"input": "x.md",
|
||||
"output": str(dest),
|
||||
"format": "markdown",
|
||||
"actions": ["clean"],
|
||||
"bytes_in": 1,
|
||||
"bytes_out": 1,
|
||||
"still_has_c2pa": residual,
|
||||
"still_has_ai_metadata": residual,
|
||||
"post_findings": ["marker:c2pa"] if residual else [],
|
||||
"meta": {"degraded": degraded},
|
||||
}
|
||||
|
||||
|
||||
def _run_clean_file(monkeypatch, tmp_path, *, json_flag, residual, degraded=False):
|
||||
src = tmp_path / "x.md"
|
||||
src.write_text("---\ngenerator: Claude\n---\nhi\n", encoding="utf-8")
|
||||
dest = tmp_path / "x.cleaned.md"
|
||||
monkeypatch.setattr(
|
||||
clean_file, "clean_container", lambda *a, **k: _container_result(dest, residual, degraded)
|
||||
)
|
||||
argv = ["clean_file.py", str(src), "-o", str(dest)]
|
||||
if json_flag:
|
||||
argv.append("--json")
|
||||
monkeypatch.setattr(sys, "argv", argv)
|
||||
return clean_file.main()
|
||||
|
||||
|
||||
def test_clean_file_json_and_human_agree_on_residual(monkeypatch, tmp_path):
|
||||
# The bug: --json returned 0 while human mode returned 1.
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=False, residual=True) == 1
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=True, residual=True) == 1
|
||||
|
||||
|
||||
def test_clean_file_json_and_human_agree_on_clean(monkeypatch, tmp_path):
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=False, residual=False) == 0
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=True, residual=False) == 0
|
||||
|
||||
|
||||
def test_clean_file_degraded_pdf_is_not_a_failure_in_either_mode(monkeypatch, tmp_path):
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=False, residual=True, degraded=True) == 0
|
||||
assert _run_clean_file(monkeypatch, tmp_path, json_flag=True, residual=True, degraded=True) == 0
|
||||
|
||||
|
||||
def _image_result(dest: Path, residual: bool) -> dict:
|
||||
return {
|
||||
"input": "x.png",
|
||||
"output": str(dest),
|
||||
"actions": ["strip"],
|
||||
"bytes_in": 1,
|
||||
"bytes_out": 1,
|
||||
"still_has_c2pa": residual,
|
||||
"still_has_ai_metadata": residual,
|
||||
"post_findings": ["byte-scan c2pa"] if residual else [],
|
||||
"synthid_before": None,
|
||||
"synthid_after": None,
|
||||
"pixel_removal": None,
|
||||
}
|
||||
|
||||
|
||||
def _run_clean_image(monkeypatch, tmp_path, *, json_flag, residual):
|
||||
src = tmp_path / "x.png"
|
||||
src.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
dest = tmp_path / "x.cleaned.png"
|
||||
monkeypatch.setattr(clean_image, "clean_image", lambda *a, **k: _image_result(dest, residual))
|
||||
argv = ["clean_image.py", str(src), "-o", str(dest)]
|
||||
if json_flag:
|
||||
argv.append("--json")
|
||||
monkeypatch.setattr(sys, "argv", argv)
|
||||
return clean_image.main()
|
||||
|
||||
|
||||
def test_clean_image_json_and_human_agree_on_residual(monkeypatch, tmp_path):
|
||||
assert _run_clean_image(monkeypatch, tmp_path, json_flag=False, residual=True) == 1
|
||||
assert _run_clean_image(monkeypatch, tmp_path, json_flag=True, residual=True) == 1
|
||||
assert _run_clean_image(monkeypatch, tmp_path, json_flag=True, residual=False) == 0
|
||||
Reference in New Issue
Block a user