mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-21 00:11:14 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df94f2c908 | ||
|
|
b2e46ee8db |
No files matched your search
@@ -114,10 +114,100 @@ jobs:
|
||||
needs: resolve
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Factory-rebuild the HuggingFace Space
|
||||
- name: Bump the Space pin, then factory-rebuild
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RAIW_HF_SPACE_TOKEN }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
VERSION: ${{ needs.resolve.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo=wiltodelta/raiw-hf-space
|
||||
workflow=pin-library.yml
|
||||
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "RAIW_HF_SPACE_TOKEN secret is not set; skipping the pin bump."
|
||||
else
|
||||
dispatched_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
gh workflow run "$workflow" \
|
||||
--repo "$repo" \
|
||||
--ref main \
|
||||
--raw-field library_version="$VERSION"
|
||||
|
||||
run_id=""
|
||||
for attempt in $(seq 1 30); do
|
||||
run_id=$(
|
||||
gh run list \
|
||||
--repo "$repo" \
|
||||
--workflow "$workflow" \
|
||||
--event workflow_dispatch \
|
||||
--limit 10 \
|
||||
--json databaseId,createdAt \
|
||||
| python3 -c '
|
||||
import json
|
||||
import sys
|
||||
|
||||
dispatched_at = sys.argv[1]
|
||||
runs = json.load(sys.stdin)
|
||||
print(next(
|
||||
(run["databaseId"] for run in runs if run["createdAt"] >= dispatched_at),
|
||||
"",
|
||||
))
|
||||
' "$dispatched_at"
|
||||
)
|
||||
if [ -n "$run_id" ]; then
|
||||
break
|
||||
fi
|
||||
echo "Space pin run not visible yet (attempt $attempt), waiting..."
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::The dispatched Space pin run did not appear"
|
||||
exit 1
|
||||
fi
|
||||
for attempt in $(seq 1 180); do
|
||||
read -r status conclusion <<< "$(
|
||||
gh run view "$run_id" \
|
||||
--repo "$repo" \
|
||||
--json status,conclusion \
|
||||
--jq '[.status, (.conclusion // "")] | @tsv'
|
||||
)"
|
||||
if [ "$status" = "completed" ]; then
|
||||
if [ "$conclusion" = "success" ]; then
|
||||
echo "Space pin run $run_id succeeded"
|
||||
break
|
||||
fi
|
||||
echo "::error::Space pin run $run_id completed with $conclusion"
|
||||
exit 1
|
||||
fi
|
||||
echo "Space pin run $run_id is $status (attempt $attempt), waiting..."
|
||||
sleep 5
|
||||
done
|
||||
if [ "${status:-}" != "completed" ]; then
|
||||
echo "::error::Space pin run $run_id did not finish in time"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for the Space requirements pin to show $VERSION..."
|
||||
pinned=""
|
||||
for attempt in $(seq 1 40); do
|
||||
reqs=$(curl -fsSL \
|
||||
"https://huggingface.co/spaces/wiltodelta/remove-ai-watermarks/raw/main/requirements.txt" \
|
||||
|| true)
|
||||
pinned=$(printf '%s\n' "$reqs" \
|
||||
| sed -n 's/^remove-ai-watermarks==\([0-9][0-9a-zA-Z.]*\).*/\1/p' \
|
||||
| head -1)
|
||||
echo " attempt $attempt: Space pin=$pinned"
|
||||
if [ "$pinned" = "$VERSION" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
if [ "$pinned" != "$VERSION" ]; then
|
||||
echo "::error::Space requirements.txt still pinned to ${pinned:-<none>}, not $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
python3 -m pip install --quiet huggingface_hub
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
@@ -217,7 +307,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 180); do
|
||||
# 0.40.0's node sync finished after this waiter: 180 * 5s is 15 min,
|
||||
# and the node suite plus registry upload ran longer. Keep the
|
||||
# workflow wait, then accept a registry row for this library version
|
||||
# so a late-but-published node is not reported as a missing release.
|
||||
version="${{ needs.resolve.outputs.version }}"
|
||||
for attempt in $(seq 1 360); do
|
||||
read -r status conclusion <<< "$(
|
||||
gh run view "$run_id" \
|
||||
--repo "$repo" \
|
||||
@@ -235,5 +330,39 @@ jobs:
|
||||
echo "ComfyUI sync run $run_id is $status (attempt $attempt), waiting..."
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::ComfyUI sync run $run_id did not finish in time"
|
||||
echo "ComfyUI sync run $run_id did not finish in time; checking the registry."
|
||||
python3 -m pip install --quiet packaging
|
||||
if curl -fsSL "https://api.comfy.org/nodes/remove-ai-watermarks/versions" \
|
||||
| VERSION="$version" python3 -c '
|
||||
import json, os, sys
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
from packaging.utils import canonicalize_name
|
||||
from packaging.version import Version
|
||||
version = os.environ["VERSION"]
|
||||
target = Version(version)
|
||||
def requires_release(value):
|
||||
try:
|
||||
requirement = Requirement(value)
|
||||
except InvalidRequirement:
|
||||
return False
|
||||
return (
|
||||
canonicalize_name(requirement.name) == "remove-ai-watermarks"
|
||||
and requirement.url is None
|
||||
and requirement.marker is None
|
||||
and target in requirement.specifier
|
||||
and any(s.operator == ">=" and Version(s.version) == target for s in requirement.specifier)
|
||||
)
|
||||
matches = [
|
||||
v for v in json.load(sys.stdin)
|
||||
if any(requires_release(d) for d in (v.get("dependencies") or []))
|
||||
]
|
||||
if not matches:
|
||||
sys.exit(1)
|
||||
newest = max(matches, key=lambda v: v.get("createdAt") or "")
|
||||
print("%s %s" % (newest.get("version"), newest.get("status")))
|
||||
'; then
|
||||
echo "Registry already lists a node for $version; the wait timed out after publish."
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::ComfyUI sync run $run_id did not finish in time and the registry has no node for $version"
|
||||
exit 1
|
||||
+36
-23
@@ -49,10 +49,14 @@ Do these in order after the skill is on the default branch.
|
||||
4. **Awesome-list PRs.** Hand-curated GitHub lists are how people browse skills
|
||||
today. One accurate PR per list, with the scope sentence intact.
|
||||
- `ComposioHQ/awesome-claude-skills`: submitted 2026-09-05 as PR 1841
|
||||
(Creative & Media); no maturity bar in its contribution rules.
|
||||
(Creative & Media); no maturity bar in its contribution rules. Rechecked
|
||||
2026-09-11: still open, CI green, no review comments, waiting on the
|
||||
list maintainers.
|
||||
- `heilcheng/awesome-agent-skills`: submitted 2026-09-09 as PR 483 under
|
||||
Community Skills > Media. The skill's three concrete input/output examples
|
||||
satisfy the list's working-example requirement.
|
||||
satisfy the list's working-example requirement. Rechecked 2026-09-11:
|
||||
still open; the only check failure is their Vercel deploy authorization,
|
||||
which a contributor cannot grant.
|
||||
- `VoltAgent/awesome-agent-skills`: DEFERRED. Its CONTRIBUTING rejects
|
||||
brand-new skills ("give your skill time to mature and gain users before
|
||||
submitting"). The skill now has real installs and shipped releases, but
|
||||
@@ -67,29 +71,31 @@ Do these in order after the skill is on the default branch.
|
||||
with `claude plugin validate` first. SUBMITTED 2026-09-07 for Claude Code
|
||||
only (strict validation passed; Apache 2.0; description carries the
|
||||
scope boundary; platform selection limited to Claude Code because Cowork
|
||||
was not tested). Pending review; the team may reach out by email. The
|
||||
official Anthropic marketplace is
|
||||
curated and has no application.
|
||||
was not tested). Pending review; the team may reach out by email.
|
||||
Rechecked 2026-09-11: `platform.claude.com/plugins` is 404, and
|
||||
`claude.ai/directory` is the MCP connector catalog, not plugins; a search
|
||||
there does not list this skill. Do not open a second submission. The
|
||||
official Anthropic marketplace is curated and has no application.
|
||||
6. **ClawHub.** `clawhub login` then `clawhub skill publish` from
|
||||
`skills/remove-ai-watermarks/`. LISTED 2026-09-09: versions 1.0.5 and 1.0.6,
|
||||
submitted by the `clawhub` job in `distribute.yml` during the 0.39.0 and
|
||||
0.39.1 releases, are public. A green job proves only a submission, so read
|
||||
the catalog itself:
|
||||
`skills/remove-ai-watermarks/`. LISTED 2026-09-09: versions 1.0.5, 1.0.6,
|
||||
and 1.0.8, submitted by the `clawhub` job in `distribute.yml` during the
|
||||
0.39.0, 0.39.1, and 0.40.0 releases, are public. A green job proves only a
|
||||
submission, so read the catalog itself:
|
||||
`clawhub search remove-ai-watermarks` returns the row and
|
||||
`https://clawhub.ai/api/v1/skills/remove-ai-watermarks` reports
|
||||
`latestVersion.version` 1.0.6, while its `/versions` endpoint returns those
|
||||
two published versions. The earlier 1.0.3 and 1.0.4 submissions never became
|
||||
`latestVersion.version` 1.0.8, while its `/versions` endpoint returns
|
||||
1.0.5, 1.0.6, and 1.0.8. The earlier 1.0.3 and 1.0.4 submissions never became
|
||||
public and do not appear in that version list, so they were bypassed rather
|
||||
than recovered. The issue that tracked
|
||||
them, openclaw/clawhub#3643, was closed by the reporter on 2026-09-09 once
|
||||
than recovered. 1.0.7 was a post-0.39.1 bump that never reached ClawHub.
|
||||
The issue that tracked 1.0.3 and 1.0.4, openclaw/clawhub#3643, was closed
|
||||
by the reporter on 2026-09-09 once
|
||||
1.0.5 went through; nothing upstream was confirmed fixed, and it is the
|
||||
fifth report of this shape (#3624, #3466, #3351, #3284), so treat a stalled
|
||||
submission as recurring and file a fresh one rather than assuming the
|
||||
pipeline is repaired. Release-time publishing is automated (see
|
||||
[release-and-distribution.md](release-and-distribution.md)), and the job
|
||||
fires only on a published GitHub Release: a skill version bumped after a
|
||||
release reaches ClawHub with the next one. 0.40.0 publishes 1.0.8; 1.0.7 was
|
||||
the post-0.39.1 bump and is not a ClawHub version. Do not publish by
|
||||
release reaches ClawHub with the next one. Do not publish by
|
||||
hand to close that gap. The listing records the license as MIT-0 while
|
||||
`SKILL.md` declares Apache-2.0, and that is not a listing error to repair:
|
||||
MIT-0 is a platform-wide constant in ClawHub's own client
|
||||
@@ -114,16 +120,23 @@ Do these in order after the skill is on the default branch.
|
||||
[Submit your Claude Code plugin to OpenAI](https://developers.openai.com/plugins/guides/submit-claude-plugin).
|
||||
8. **AgenticSkills.** SUBMITTED 2026-09-09 as review issue 174 under AI/ML
|
||||
Development. The submission carries the user-owned-content boundary and
|
||||
links to the repository rather than hosting a separate copy.
|
||||
links to the repository rather than hosting a separate copy. Rechecked
|
||||
2026-09-11: the public index still does not list it (`/skills/remove-ai-watermarks`
|
||||
is 404; search for `wiltodelta` returns 0 of 192). Their form promises a
|
||||
48-hour review; that window has passed. Issue 174 is not a public GitHub
|
||||
issue. Do not open a second submission. Followed up 2026-09-11 through
|
||||
their contact form as a listing correction.
|
||||
9. **AI Skills Catalog.** SUBMITTED 2026-09-09 as reference 2 under Video Media,
|
||||
with Codex as the primary agent and every supported harness marked compatible.
|
||||
The catalog reviews source quality, security, license, and compatibility
|
||||
before publication. Once accepted, treat material skill-version updates as
|
||||
manual because the catalog stores compatible packages in addition to source
|
||||
metadata and does not promise that a GitHub change refreshes those packages.
|
||||
Use the update route supplied with acceptance; if none is supplied, contact
|
||||
the catalog or resubmit, then verify the live result. Do not open a duplicate
|
||||
while the initial review is pending.
|
||||
Rechecked 2026-09-11: the public catalog at skillscatalog.ai shows 0 skills
|
||||
from 0 publishers, `/api/featured` returns 401, and `/api/auth/session`
|
||||
returns 500, so a stranger cannot confirm the listing. Do not open a
|
||||
duplicate while that initial review is pending. Once accepted, treat
|
||||
material skill-version updates as manual because the catalog stores
|
||||
compatible packages in addition to source metadata and does not promise
|
||||
that a GitHub change refreshes those packages. Use the update route
|
||||
supplied with acceptance; if none is supplied, contact the catalog or
|
||||
resubmit, then verify the live result.
|
||||
|
||||
The conditional update actions for every catalog are part of the canonical
|
||||
release checklist in
|
||||
|
||||
@@ -1391,6 +1391,13 @@ The shared text engine handles `千问AI生成`; `QwenEngine` also scans a squar
|
||||
bottom-right region for Qwen Create's three-lobe symbol. Its 0.65 NCC gate was
|
||||
measured against the cleared provider fixture (0.848) and 132 decoded tracked
|
||||
controls other than that positive (maximum 0.516 on 2026-09-10, zero fires).
|
||||
NCC is not portable across OpenCV builds: issue 108 reported 0.81 on
|
||||
`data/fixtures/provenance/chatgpt-1.png` (sand and denim, no glyph) while this
|
||||
tree's OpenCV 4.11 scores 0.437 on the same file. A fire also requires
|
||||
binarized IoU of the bright residual with the three-lobe silhouette at or
|
||||
above 0.70; the true fixture stays at or above 0.75 down to 512 px, and
|
||||
chatgpt-1 sits at 0.55. `provenance_ncc_factor` remains 1.0, so OpenAI C2PA
|
||||
cannot relax this mark.
|
||||
The removal mask reuses the aligned three-lobe alpha and expands it by 6% of
|
||||
the detected side; a solid square was rejected because classical inpainting
|
||||
left a visible rectangular blur.
|
||||
|
||||
@@ -11,7 +11,7 @@ A release is complete only after all four published surfaces are verified:
|
||||
|---|---|---|
|
||||
| PyPI | The `remove-ai-watermarks` wheel and source distribution | `publish.yml` |
|
||||
| Homebrew | The `remove-ai-watermarks` formula in `wiltodelta/homebrew-tap` | `distribute.yml` |
|
||||
| Hugging Face Space | The Space demo deployed on the new release through a requirements-pin bump in `wiltodelta/raiw-hf-space` | `raiw-hf-space` sync workflow; the pin bump is manual, `distribute.yml` only re-installs the pinned version |
|
||||
| Hugging Face Space | The Space demo deployed on the new release through a requirements-pin bump in `wiltodelta/raiw-hf-space` | `distribute.yml` dispatches `pin-library.yml` in that repo, waits for the Hub `requirements.txt` pin, then factory-rebuilds; `sync-to-hf.yml` mirrors the pin onto the Space |
|
||||
| ComfyUI Registry | A compatible release of `wiltodelta/ComfyUI-remove-ai-watermarks` with its own node version | `distribute.yml` and the node repository's workflows |
|
||||
|
||||
The GitHub Release is the trigger and release record for this flow. Conda is
|
||||
@@ -62,16 +62,20 @@ PyPI API token from the repository.
|
||||
waits for the matching source distribution to appear on PyPI, then:
|
||||
|
||||
- updates the Homebrew tap formula URL and SHA-256;
|
||||
- triggers a factory rebuild of the Hugging Face Space: this re-installs the
|
||||
version pinned in the Space repo, it does NOT upgrade the demo;
|
||||
- bumps the Hugging Face Space pin and factory-rebuilds the demo on that pin;
|
||||
- synchronizes, tests, versions, and publishes the ComfyUI nodes.
|
||||
|
||||
Upgrading the Hugging Face Space is a separate, manual step: bump
|
||||
`remove-ai-watermarks[visible,heif]` in `wiltodelta/raiw-hf-space` (`pyproject.toml`,
|
||||
`uv lock`, and the re-exported `requirements.txt`), then push. That repository's
|
||||
`sync-to-hf.yml` mirrors the files onto the Space, which rebuilds on the new
|
||||
pin. Its callback smoke tests run in that repository's CI on every push; they
|
||||
are the scenario check for the demo surface.
|
||||
Upgrading the Hugging Face Space is dispatched from `distribute.yml`: it runs
|
||||
`pin-library.yml` in `wiltodelta/raiw-hf-space`, which bumps
|
||||
`remove-ai-watermarks[visible,heif]` in `pyproject.toml`, refreshes `uv.lock`,
|
||||
and pushes. That repository's `sync-to-hf.yml` then mirrors the files onto the
|
||||
Space. `distribute.yml` waits for the Hub `requirements.txt` pin to match,
|
||||
then factory-rebuilds. The `RAIW_HF_SPACE_TOKEN` repository secret is a
|
||||
fine-grained token limited to the Space repository, with Contents write and
|
||||
Actions write. Without the secret the pin bump is skipped and only the
|
||||
factory rebuild runs, which reinstalls whatever version is already pinned.
|
||||
The Space repo's callback smoke tests still run on every push; they are the
|
||||
scenario check for the demo surface.
|
||||
|
||||
The Hugging Face **model** `wiltodelta/raiw-photo-classify` is not part of that
|
||||
release fan-out. It holds the photo-classify freeze weights. The library extra
|
||||
@@ -253,9 +257,18 @@ failure.
|
||||
|
||||
The ComfyUI sync run can fail the same way on its own: it resolves the version
|
||||
from the PyPI JSON API (which updates first) but installs the test dependency
|
||||
with pip against the simple index, whose CDN edges lag by minutes. Rerun the
|
||||
failed `distribute.yml` comfyui job once the simple index lists the release;
|
||||
nothing about the release itself is wrong.
|
||||
with pip against the simple index, whose CDN edges lag by minutes. The
|
||||
`distribute.yml` waiter also has a clock: 0.40.0's node suite finished after
|
||||
the 15-minute poll, so the job now waits 30 minutes and then accepts a
|
||||
registry row that already requires this library version. Rerun the failed
|
||||
`comfyui` job only when the registry still has no such row.
|
||||
|
||||
The automatic `verify-release.yml` run after `distribute.yml` checks the
|
||||
Hugging Face Space pin. When `RAIW_HF_SPACE_TOKEN` is set, that pin lands
|
||||
during distribution. Without the secret, bump the pin by dispatching
|
||||
`pin-library.yml` in `wiltodelta/raiw-hf-space` and then dispatch
|
||||
`verify-release.yml` with the release version. Do not read a missing Space
|
||||
pin as a broken PyPI or Homebrew publish.
|
||||
|
||||
If the release changed CLI routing, extras, exit codes, mark keys, or the
|
||||
intended-use boundary, confirm `skills/remove-ai-watermarks/` still matches.
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# on it, including the ComfyUI node package. The console script below carries
|
||||
# the same weight, since users have it on PATH.
|
||||
name = "remove-ai-watermarks"
|
||||
version = "0.40.0"
|
||||
version = "0.40.1"
|
||||
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11,<3.15"
|
||||
|
||||
@@ -33,7 +33,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.40.0"
|
||||
__version__ = "0.40.1"
|
||||
|
||||
__all__ = [
|
||||
"BatchItemResult",
|
||||
|
||||
@@ -109,6 +109,14 @@ _SYMBOL_MARGIN_FRAC = 0.012
|
||||
_SYMBOL_SIZE_FRAC = 0.03125
|
||||
_SYMBOL_LADDER = (0.75, 0.875, 1.0, 1.125, 1.25)
|
||||
_SYMBOL_NCC_THRESHOLD = 0.65
|
||||
# NCC alone is not portable: issue 108 reported 0.81 on
|
||||
# data/fixtures/provenance/chatgpt-1.png (sand/denim, no glyph) while this
|
||||
# OpenCV build scores 0.437 on the same file. True Qwen Create matches keep
|
||||
# binarized IoU >= 0.75 down to 512 px; that fixture sits at 0.55. Require
|
||||
# both the NCC gate and this shape overlap so a high-NCC blob cannot fire.
|
||||
_SYMBOL_SHAPE_IOU_THRESHOLD = 0.70
|
||||
_SYMBOL_TEMPLATE_MASK_FRAC = 0.25
|
||||
_SYMBOL_RESPONSE_MASK_FRAC = 0.35
|
||||
_SYMBOL_MIN_SHORT_SIDE = 256
|
||||
_SYMBOL_MASK_ALPHA = 4 / 255
|
||||
_SYMBOL_MASK_DILATE_FRAC = 0.06
|
||||
@@ -165,6 +173,23 @@ class QwenEngine(TextMarkEngine):
|
||||
self._symbol_template = template
|
||||
self._symbol_template_scales: dict[int, NDArray[Any]] = {}
|
||||
|
||||
@staticmethod
|
||||
def _symbol_shape_iou(response_crop: NDArray[Any], template: NDArray[Any]) -> float:
|
||||
"""Overlap of the bright residual with the three-lobe silhouette."""
|
||||
if response_crop.size == 0 or template.size == 0:
|
||||
return 0.0
|
||||
template_peak = float(template.max())
|
||||
response_peak = float(response_crop.max())
|
||||
if template_peak <= 0.0 or response_peak <= 0.0:
|
||||
return 0.0
|
||||
template_mask = template > (template_peak * _SYMBOL_TEMPLATE_MASK_FRAC)
|
||||
response_mask = response_crop > (response_peak * _SYMBOL_RESPONSE_MASK_FRAC)
|
||||
union = int(np.logical_or(template_mask, response_mask).sum())
|
||||
if union == 0:
|
||||
return 0.0
|
||||
intersection = int(np.logical_and(template_mask, response_mask).sum())
|
||||
return intersection / union
|
||||
|
||||
def _scaled_symbol_template(self, side: int) -> NDArray[Any]:
|
||||
"""Return a cached square symbol template for a measured ladder size."""
|
||||
template = self._symbol_template_scales.get(side)
|
||||
@@ -195,6 +220,7 @@ class QwenEngine(TextMarkEngine):
|
||||
|
||||
best_score = 0.0
|
||||
best_region = (0, 0, 0, 0)
|
||||
best_iou = 0.0
|
||||
for rung in _SYMBOL_LADDER:
|
||||
side = max(12, int(base * _SYMBOL_SIZE_FRAC * rung))
|
||||
if side >= search_side:
|
||||
@@ -202,11 +228,15 @@ class QwenEngine(TextMarkEngine):
|
||||
template = self._scaled_symbol_template(side)
|
||||
scores = cv2.matchTemplate(response, template, cv2.TM_CCOEFF_NORMED)
|
||||
_minimum, score, _min_location, location = cv2.minMaxLoc(scores)
|
||||
if score > best_score:
|
||||
best_score = float(score)
|
||||
best_region = (origin_x + location[0], origin_y + location[1], side, side)
|
||||
ncc = float(score)
|
||||
if ncc <= best_score:
|
||||
continue
|
||||
crop = response[location[1] : location[1] + side, location[0] : location[0] + side]
|
||||
best_score = ncc
|
||||
best_region = (origin_x + location[0], origin_y + location[1], side, side)
|
||||
best_iou = self._symbol_shape_iou(crop, template)
|
||||
det.confidence = best_score
|
||||
det.detected = best_score >= _SYMBOL_NCC_THRESHOLD
|
||||
det.detected = best_score >= _SYMBOL_NCC_THRESHOLD and best_iou >= _SYMBOL_SHAPE_IOU_THRESHOLD
|
||||
det.region = best_region
|
||||
return det
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from remove_ai_watermarks.qwen_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
_LADDER,
|
||||
_SYMBOL_SHAPE_IOU_THRESHOLD,
|
||||
QwenEngine,
|
||||
_alpha_template,
|
||||
_glyph_silhouette,
|
||||
@@ -32,6 +33,7 @@ _BIG_MODE, _SMALL_MODE = 0.203, 0.124
|
||||
_MARGIN = 0.025 # measured right/bottom margin of the real mark
|
||||
_ROOT = Path(__file__).resolve().parents[1]
|
||||
_QWEN_CREATE = _ROOT / "data" / "fixtures" / "provenance" / "qwen-create-qwen-image-2.png"
|
||||
_CHATGPT_1 = _ROOT / "data" / "fixtures" / "provenance" / "chatgpt-1.png"
|
||||
|
||||
|
||||
def _compose(w: int, h: int, mode: float = _BIG_MODE, bg: float = 100.0):
|
||||
@@ -146,6 +148,26 @@ class TestDetect:
|
||||
assert y > image.shape[0] * 0.90
|
||||
assert width == height
|
||||
|
||||
def test_chatgpt_1_fixture_does_not_fire_qwen(self):
|
||||
# Issue 108: 0.40.0 reported visible_qwen 0.81 on this OpenAI C2PA
|
||||
# beach photo and auto-inpainted sand. NCC on the three-lobe template
|
||||
# is OpenCV-sensitive; the shape overlap must still refuse it.
|
||||
from remove_ai_watermarks.identify import identify
|
||||
from remove_ai_watermarks.image_io import imread
|
||||
|
||||
image = imread(_CHATGPT_1)
|
||||
assert image is not None
|
||||
det = QwenEngine().detect(image)
|
||||
assert not det.detected
|
||||
record = identify(_CHATGPT_1, check_visible=True, check_invisible=False)
|
||||
assert all(signal.name != "visible_qwen" for signal in record.signals)
|
||||
|
||||
def test_symbol_shape_iou_rejects_a_solid_blob(self):
|
||||
template = QwenEngine()._scaled_symbol_template(32)
|
||||
blob = np.full_like(template, float(template.max()))
|
||||
assert QwenEngine._symbol_shape_iou(blob, template) < _SYMBOL_SHAPE_IOU_THRESHOLD
|
||||
assert QwenEngine._symbol_shape_iou(template, template) >= _SYMBOL_SHAPE_IOU_THRESHOLD
|
||||
|
||||
def test_unaccepted_symbol_candidate_cannot_hide_detected_text(self):
|
||||
text_detection = TextMarkDetection(detected=True, confidence=0.45)
|
||||
symbol_detection = TextMarkDetection(detected=False, confidence=0.46)
|
||||
@@ -176,6 +198,16 @@ class TestFootprintMaskAndRemoval:
|
||||
clean = cv2.GaussianBlur(np.full((640, 853, 3), 120, np.uint8), (5, 5), 0)
|
||||
assert QwenEngine().footprint_mask(clean, force=False) is None
|
||||
|
||||
def test_chatgpt_1_auto_remove_does_not_touch_pixels(self):
|
||||
from remove_ai_watermarks.api import remove_visible_detailed
|
||||
from remove_ai_watermarks.image_io import imread
|
||||
|
||||
image = imread(_CHATGPT_1)
|
||||
assert image is not None
|
||||
report = remove_visible_detailed(image, sensitivity="auto")
|
||||
assert report.status == "no_watermark"
|
||||
assert np.array_equal(report.image, image)
|
||||
|
||||
def test_removes_qwen_create_symbol(self):
|
||||
image = cv2.imread(str(_QWEN_CREATE), cv2.IMREAD_COLOR)
|
||||
assert image is not None
|
||||
|
||||
@@ -164,6 +164,19 @@ def test_distributed_version_reads_the_flat_single_artifact_layout(tmp_path: Pat
|
||||
assert "version=0.37.0" in (tmp_path / "output").read_text()
|
||||
|
||||
|
||||
def test_comfyui_waiter_outlasts_the_node_suite_then_reads_the_registry() -> None:
|
||||
script = _step("distribute.yml", "comfyui", "Sync and publish the ComfyUI nodes")["run"]
|
||||
assert "seq 1 360" in script
|
||||
assert "api.comfy.org/nodes/remove-ai-watermarks/versions" in script
|
||||
|
||||
|
||||
def test_space_pin_is_dispatched_before_the_factory_rebuild() -> None:
|
||||
script = _step("distribute.yml", "hf-space", "Bump the Space pin, then factory-rebuild")["run"]
|
||||
assert "pin-library.yml" in script
|
||||
assert "RAIW_HF_SPACE_TOKEN secret is not set" in script
|
||||
assert "restart_space" in script
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "posix" or shutil.which("bash") is None, reason="Executes an Ubuntu Bash workflow")
|
||||
def test_clawhub_uses_local_install(tmp_path: Path) -> None:
|
||||
script = _step("distribute.yml", "clawhub", "Publish the agent skill when its version moved")["run"]
|
||||
|
||||
Reference in new issue
Block a user