Fix agent skill dependency checks and metadata guidance

This commit is contained in:
Victor Kuznetsov committed 2026-09-05 11:32:22 -07:00
1 parent ecbe296b31
commit 0fd139aac4
11 files changed
+180 -42

No files matched your search

+2 -2
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Agent skill and Claude Code plugin for the remove-ai-watermarks CLI.",
"version": "1.0.0"
"version": "1.0.1"
},
"plugins": [
{
@@ -16,4 +16,4 @@
"strict": false
}
]
}
}
+7 -4
View File
@@ -63,10 +63,13 @@ Do these in order after the skill is on the default branch.
7. **OpenAI ChatGPT / Codex plugin directory.** Skills-only plugins are a
supported submission type at
[platform.openai.com/plugins](https://platform.openai.com/plugins). This is
the largest consumer surface that actually reviews listings. It needs a
verified Platform identity, public website / support / privacy / terms URLs,
starter prompts, and five positive plus three negative test cases. Claude
marketplace approval does not transfer. Convert with
the largest consumer surface that actually reviews listings. It needs Apps
Management write access, a verified Platform identity, public website /
support / privacy / terms URLs, starter prompts, and five positive plus three
negative test cases. Claude marketplace approval does not transfer. Contact
your OpenAI partner before submitting: this skill's core value requires local
execution, arbitrary file access, and optional hardware access, which the
official guide routes through that product-specific review. Convert with
[Submit your Claude Code plugin to OpenAI](https://developers.openai.com/plugins/guides/submit-claude-plugin).
Do not treat Smithery or other MCP directories as skill catalogs. Do not open
+8 -5
View File
@@ -32,12 +32,15 @@ brew install wiltodelta/tap/remove-ai-watermarks
```
A formula cannot carry extras, so this installs the default package: metadata
commands work, and `visible`, `erase`, `all`, `batch` and every `video` command
stop with an install hint naming the extra that fixes them. `identify` still
commands work, including `video metadata`, and `video identify --no-visible`
provides metadata-only provenance inspection. `batch --mode metadata` and
`video batch --mode metadata` also work. `visible`, `erase`, `all`, pixel-processing
image batch modes, full `video identify`, and the video pixel-processing commands
stop with an install hint naming the extra that fixes them. Image `identify` also
runs, but reports metadata only and says so in its caveats. `metadata --remove`
also works on a file whose markers survive the first strip: the raster
normalization that recovery needs is skipped rather than crashing, and the
survivors are reported. Reinstall with uv or pipx to add the extra.
works on a file whose markers survive the first strip: the raster normalization
that recovery needs is skipped rather than crashing, and the survivors are
reported. Reinstall with uv or pipx to add the extra.
## Visible watermark removal
+6 -5
View File
@@ -57,11 +57,12 @@ Important contracts:
default package ships without those dependencies and the Homebrew formula
installs exactly that build, so a first run following the project's own
instructions used to die on `ModuleNotFoundError: No module named 'cv2'`.
`_pixels_required` wraps the four image commands (the video ones reach the same
build through `video._require_video_runtime`, which names the `video` extra --
the one that actually makes them work); `remove_batch` re-raises the same
ImportError instead of counting it once per file, because one absent package
is not N broken images. All three sites ask `optional_deps.pixels_available()`
`_pixels_required` wraps the four image commands. Video pixel-processing paths
reach the same build through `video._require_video_runtime`, which names the
`video` extra, while `video metadata` and `video identify --no-visible` bypass
the pixel runtime. `remove_batch` re-raises the same ImportError instead of
counting it once per file, because one absent package is not N broken images.
All three image sites ask `optional_deps.pixels_available()`
rather than matching the exception's module name: an ImportError raised inside
`cv2/__init__.py` carries `cv2.cv2` and a numpy ABI mismatch carries `None`, so
a name match let exactly the tracebacks this guard exists for through.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "remove-ai-watermarks",
"displayName": "Remove AI Watermarks",
"version": "1.0.0",
"version": "1.0.1",
"description": "Identify and remove AI provenance marks from images and video the user generated or edited, using the remove-ai-watermarks CLI.",
"author": {
"name": "wiltodelta",
+12 -9
View File
@@ -5,7 +5,7 @@ license: Apache-2.0
compatibility: Python 3.11-3.14. Invisible image removal needs NVIDIA CUDA. Some video writes need ffmpeg. Installer may be uv, pipx, or pip.
metadata:
author: wiltodelta
version: "1.0.0"
version: "1.0.1"
homepage: https://raiw.cc
repository: https://github.com/wiltodelta/remove-ai-watermarks
---
@@ -69,9 +69,11 @@ writes itself, because a CLI that exists is not a CLI that works.
Read the JSON. Do not infer CUDA, ffmpeg, or an installer from the OS name.
- `pixel_stack` = `missing`: the installed build has no pixel dependencies.
Only `identify` and `metadata` answer. `identify` skips the visible detectors
and says so in its caveats, so read that caveat rather than its quiet verdict.
Go to step 3.
Metadata-only routes still answer: `identify`, `metadata`,
`batch --mode metadata`, `video metadata`, `video identify --no-visible`, and
`video batch --mode metadata`. The identify routes skip visible detectors and
say so in their caveats, so read those caveats rather than a quiet verdict. Go
to step 3 before running a pixel command.
- `pixel_stack` = `unknown`: the check gave an unrecognized result. Do not
claim a command will work; run it and read the real error.
- `advice.upgrade_cli` present: the installed CLI is older than this skill's
@@ -95,9 +97,10 @@ removal, it is paid and runs on a GPU. Do not quote a price: check the site.
Install when `cli.found` is false OR `pixel_stack` is `missing`. A present
binary is not enough: the default package, which is what Homebrew installs,
carries no pixel dependencies, so `visible`, `erase`, `all`, `batch` and every
`video` command stop with an install hint, and `identify` sees metadata only.
Reinstalling with the extra fixes it.
carries no pixel dependencies. `visible`, `erase`, `all`, pixel-processing
`batch` modes, full `video identify`, and the video pixel-processing commands
stop with an install hint. The metadata-only routes listed in step 2 still work.
Reinstalling with the appropriate extra fixes the pixel commands.
Read [references/install.md](references/install.md) for the extra list and the
uv / pipx / pip commands. Default extra is `visible`. Always quote extras:
@@ -109,13 +112,13 @@ Default: inspect first.
| User goal | Command | Stop if probe says |
| --- | --- | --- |
| What is on this file? | `identify` or `video identify` | |
| What is on this file? | `identify` or `video identify` | use `video identify --no-visible` when `pixel_stack=missing` |
| Known visible AI label on an image | `visible` | |
| User-supplied box on their own image | `erase` | |
| AI metadata only | `metadata` or `video metadata` | |
| Known visible AI label on video | `video visible` or `video all` | `video_visible=needs_ffmpeg` |
| Invisible image / image SynthID | `invisible` | `invisible_images=unavailable_no_cuda` |
| Video SynthID | `video invisible` | `video_invisible=needs_ffmpeg` |
| Video SynthID | `video invisible` | `video_invisible=needs_video_and_diffusion_extras` or `needs_ffmpeg` |
| Everything on an image | `all` | `image_all=do_not_run_writes_nothing` |
| A directory | `batch` or `video batch` | same as the mode |
| No signal found, user still wants a guess | `classify` | it is a guess, not provenance |
@@ -31,10 +31,12 @@ python3 -m pip install --upgrade "remove-ai-watermarks[visible]"
On Windows, `python` may exist when `python3` does not.
Homebrew is macOS/Linux only and CANNOT carry extras: the formula installs the
default package, which has no pixel dependencies, so `visible`, `erase`, `all`,
`batch` and the `video` commands stop with an install hint, and `identify`
reports metadata only. That is exactly what `pixel_stack: missing` means in the
probe. Use it for metadata-only work,
default package, which has no pixel dependencies. `visible`, `erase`, `all`,
pixel-processing `batch` modes, full `video identify`, and video pixel-processing
commands stop with an install hint. `identify`, `metadata`,
`batch --mode metadata`, `video metadata`, `video identify --no-visible`, and
`video batch --mode metadata` still provide metadata-only results. That is exactly
what `pixel_stack: missing` means in the probe. Use Homebrew for metadata-only work,
or when the user asks for brew by name and accepts that limit:
```bash
+11 -8
View File
@@ -224,26 +224,29 @@ def _advice(
"detail": f"Install with {installer}. Default extra is visible.",
}
# A found CLI without the pixel stack can still read metadata, and NOTHING else.
# Reporting these as "ok" is what sent an agent into a missing-cv2 crash on a
# machine the probe had just called ready.
# A found CLI without the pixel stack can still use the metadata-only image and
# video routes. Reporting pixel commands as "ok" is what sent an agent into a
# missing-cv2 crash on a machine the probe had just called ready.
no_pixels = pixels == "missing"
advice = {
"identify": "metadata_only_no_pixels" if no_pixels else "ok",
"visible": "needs_visible_extra" if no_pixels else "ok",
"metadata": "ok",
"video_visible": "needs_visible_extra" if no_pixels else ("ok" if ffmpeg else "needs_ffmpeg"),
"video_identify": "metadata_only_no_pixels" if no_pixels else "ok",
"video_metadata": "ok",
"video_visible": "needs_video_extra" if no_pixels else ("ok" if ffmpeg else "needs_ffmpeg"),
"invisible_images": "unavailable_no_cuda" if not cuda else ("needs_visible_extra" if no_pixels else "ok"),
"image_all": _image_all(no_pixels=no_pixels, cuda=cuda, invisible=invisible),
"video_invisible": "ok_cpu_or_gpu" if ffmpeg else "needs_ffmpeg",
"video_invisible": (
"needs_video_and_diffusion_extras" if no_pixels else ("ok_cpu_or_gpu" if ffmpeg else "needs_ffmpeg")
),
"no_cuda_fallback": host,
"pixel_stack": pixels,
"invisible_stack": invisible,
}
if no_pixels:
advice["next"] = "install_visible"
advice["detail"] = (
"The installed CLI has no pixel dependencies, so only metadata commands work. "
"The installed CLI has no pixel dependencies. Metadata-only image and video "
"commands still work, including both metadata batch modes. "
f"Reinstall with the visible extra using {installer or 'uv, pipx or pip'}. "
"A Homebrew install cannot carry extras."
)
+14 -4
View File
@@ -11,11 +11,12 @@ while the actual import fails. Every ``is_available()`` guard routes through
from __future__ import annotations
import importlib
import importlib.util
def module_available(*names: str) -> bool:
"""True when every named module resolves to a real, importable package.
"""True when every named module resolves to a concrete package.
A spec with ``loader is None`` is a pure namespace package -- for our
optional deps that means a stale directory remnant, not an installed
@@ -29,7 +30,7 @@ def module_available(*names: str) -> bool:
def pixels_available() -> bool:
"""True when this build can decode and inpaint pixels.
"""True when this build's shared pixel dependencies resolve and import.
The default package ships without the ``visible`` extra -- the Homebrew formula
installs exactly that build -- so every visible-mark command and the visible arm
@@ -38,8 +39,17 @@ def pixels_available() -> bool:
and all three then agreed only by coincidence: an ``ImportError`` raised INSIDE
``cv2/__init__.py`` carries ``name == "cv2.cv2"`` and a numpy ABI mismatch
carries ``name is None``, so a name match answered "that is some other import
error" and let the traceback through.
error" and let the traceback through. A concrete spec alone is also not enough:
importing cv2 can fail when its NumPy ABI is broken, so this predicate performs
the real imports on the error path that asks it.
"""
from remove_ai_watermarks._internal.watermark_profiles import PIXELS_MODULES
return module_available(*PIXELS_MODULES)
if not module_available(*PIXELS_MODULES):
return False
try:
for name in PIXELS_MODULES:
importlib.import_module(name)
except ImportError:
return False
return True
+71
View File
@@ -14,6 +14,9 @@ ROOT = Path(__file__).resolve().parents[1]
SKILL = ROOT / "skills" / "remove-ai-watermarks" / "SKILL.md"
PLUGIN = ROOT / "skills" / ".claude-plugin" / "plugin.json"
MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json"
AGENT_DOC = ROOT / "docs" / "agent-skill.md"
INSTALL_DOC = ROOT / "docs" / "installation.md"
INSTALL_REFERENCE = ROOT / "skills" / "remove-ai-watermarks" / "references" / "install.md"
_NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
@@ -349,6 +352,74 @@ def test_probe_reads_capability_not_presence() -> None:
assert any(call[1:2] == ["visible"] for call in calls), "the probe never exercised the pixel path"
def test_probe_keeps_metadata_only_video_routes_without_pixels() -> None:
probe = _probe_module()
report = _probe_with_fake_cli(
probe,
{"--version": _current_version_reply(probe), "visible": _NO_PIXELS_REPLY, "invisible": _NO_PIXELS_REPLY},
)
assert report["advice"]["video_identify"] == "metadata_only_no_pixels"
assert report["advice"]["video_metadata"] == "ok"
assert report["advice"]["video_visible"] == "needs_video_extra"
assert report["advice"]["video_invisible"] == "needs_video_and_diffusion_extras"
assert "pixel_stack" not in report["advice"]
assert "invisible_stack" not in report["advice"]
def test_skill_and_install_guide_name_pixel_free_video_routes() -> None:
texts = [
SKILL.read_text(encoding="utf-8"),
INSTALL_REFERENCE.read_text(encoding="utf-8"),
INSTALL_DOC.read_text(encoding="utf-8"),
]
for command in (
"`batch --mode metadata`",
"`video metadata`",
"`video identify --no-visible`",
"`video batch --mode metadata`",
):
assert all(command in text for text in texts)
assert all("every `video` command" not in text for text in texts)
assert all("the `video` commands stop" not in text for text in texts)
def test_metadata_only_routes_run_without_the_pixel_stack(tmp_path: Path, monkeypatch) -> None:
from click.testing import CliRunner
from remove_ai_watermarks.cli import main
image_dir = tmp_path / "images"
image_dir.mkdir()
(image_dir / "input.png").write_bytes(
(ROOT / "data" / "fixtures" / "provenance" / "ideogram-c2pa.png").read_bytes()
)
video = ROOT / "data" / "fixtures" / "visible" / "sora" / "example.mp4"
video_dir = tmp_path / "videos"
video_dir.mkdir()
(video_dir / "input.mp4").write_bytes(video.read_bytes())
monkeypatch.setitem(sys.modules, "cv2", None)
for args in (
["batch", str(image_dir), "--mode", "metadata", "-o", str(tmp_path / "clean-images")],
["video", "metadata", str(video), "--check"],
["video", "identify", str(video), "--no-visible"],
["video", "batch", str(video_dir), "--mode", "metadata", "-o", str(tmp_path / "clean-videos")],
):
result = CliRunner().invoke(main, args, catch_exceptions=False)
assert result.exit_code == 0, result.output
def test_openai_submission_names_the_partner_gate() -> None:
text = " ".join(AGENT_DOC.read_text(encoding="utf-8").split())
assert "Contact your OpenAI partner before submitting" in text
assert "local execution" in text
assert "arbitrary file access" in text
assert "hardware access" in text
def test_probe_calls_a_working_install_ok() -> None:
probe = _probe_module()
report = _probe_with_fake_cli(
+42
View File
@@ -2,10 +2,14 @@
from __future__ import annotations
import importlib
import importlib.machinery
import importlib.util
import pytest
from remove_ai_watermarks import optional_deps
from remove_ai_watermarks._internal.watermark_profiles import PIXELS_MODULES, VISIBLE_EXTRA
def _fake_find_spec(specs: dict[str, importlib.machinery.ModuleSpec | None]):
@@ -49,6 +53,44 @@ class TestModuleAvailable:
assert optional_deps.module_available("json", "logging") is True
class TestPixelsAvailable:
@pytest.mark.parametrize("broken_name", PIXELS_MODULES)
def test_import_failure_inside_present_package_is_unavailable(self, broken_name, monkeypatch):
monkeypatch.setattr(optional_deps, "module_available", lambda *_names: True)
imported: list[str] = []
def import_module(name: str):
imported.append(name)
if name == broken_name:
raise ImportError("numpy.core.multiarray failed to import")
return object()
monkeypatch.setattr(importlib, "import_module", import_module)
assert optional_deps.pixels_available() is False
assert broken_name in imported
def test_cli_guard_converts_an_internal_import_failure_to_the_install_hint(self, capsys, monkeypatch):
from remove_ai_watermarks.cli import _pixels_required
monkeypatch.setattr(optional_deps, "module_available", lambda *_names: True)
def import_module(_name: str):
raise ImportError("numpy.core.multiarray failed to import")
monkeypatch.setattr(importlib, "import_module", import_module)
@_pixels_required
def broken_command() -> None:
raise ImportError("numpy.core.multiarray failed to import")
with pytest.raises(SystemExit) as caught:
broken_command()
assert caught.value.code == 1
assert VISIBLE_EXTRA in capsys.readouterr().out
class TestGuardsUseSharedHelper:
def test_trustmark_is_available_rejects_namespace_remnant(self, monkeypatch):
from remove_ai_watermarks import trustmark_detector