Add AI disclosure layouts, tighten detection and provenance warnings

This commit is contained in:
Victor Kuznetsov committed 2026-10-04 11:03:37 -07:00
1 parent 72752feb33
commit 898f8be789
37 files changed
+1011 -112

No files matched your search

+5 -5
View File
@@ -370,11 +370,11 @@ remove-ai-watermarks batch ./images --mode all
Visible mark support includes:
- Google Gemini and Nano Banana visible sparkle watermark;
- Doubao, the Jimeng wordmark and top-left `AI生成` pill, Qwen, Kling AI,
Yuanbao, Baidu, LiblibAI's bottom-center wordmark and compact top-left pill,
and RunningHub labels;
- one calibrated Microsoft top-right white AI-badge variant;
- one calibrated Samsung Galaxy AI label variant;
- Doubao, the Jimeng wordmark and top-left `AI生成` layouts, Qwen, Kling AI,
Yuanbao, Dola, WorkBuddy, Baidu, LiblibAI's bottom-center wordmark and compact
top-left pill, and RunningHub labels;
- calibrated Microsoft top-right white and beige AI-badge layouts;
- calibrated Italian and Korean Samsung Galaxy AI disclosures;
- a generic brand-less bottom-right `AI生成` fallback for OS/gallery-level
AI-edit stamps that carry no vendor wordmark of their own (vivo and Xiaomi
Gallery confirmed); synthetic-only calibration, weaker evidence than the
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+36 -16
View File
@@ -1596,8 +1596,8 @@ their own: the Gemini sparkle has its own higher-confidence path, and the pill
alone is too weak to attribute.
The set of marks that veto the Jimeng pill is derived from the registry rows:
every other product from the same manufacturer. Doubao therefore vetoes the pill
because both products are ByteDance, while Qwen, Kling, Yuanbao, RunningHub,
every other product from the same manufacturer. Doubao and Dola therefore veto the pill
because all three products are ByteDance, while Qwen, Kling, Yuanbao, RunningHub,
Baidu, and LiblibAI do not become siblings merely because they use the same TC260
standard. Marks from other manufacturers cannot enable or veto the
ByteDance-specific arm.
@@ -1656,8 +1656,7 @@ assets and template ladder are immutable.
A decorative sparkle/glint/star glyph in stylized typography -- a bright,
near-white four-point accent, the same silhouette family the engine's own
template captures -- is a known, unresolved false-positive class. Incident
2026-08-27 (raiw-app, Spaces uid 7e01ef8cb99949bba14ab4607a66ae06): a book-cover
template captures -- is a known, unresolved false-positive class. A book-cover
title's decorative diamond accent inside a neon-glow letterform scored 0.64, just
under `_SPARKLE_FP_CONF`, and the removal step painted a permanent smudge over
the artwork. Investigation (margin, gradient, saturation, and a prototyped local
@@ -1697,6 +1696,11 @@ Regression coverage:
[`_text_mark_engine.py`](../src/remove_ai_watermarks/_text_mark_engine.py)
provides common localization, detection front ends, template caching, rival
comparison, and footprint construction.
[`_text_mark_variants.py`](../src/remove_ai_watermarks/_text_mark_variants.py)
combines independently gated layouts under one registry key while retaining the
winning layout's template and mask geometry. Its bottom-right variant searches
only anchor-valid placements, so an unrelated stronger match elsewhere in the
corner ROI cannot hide the real edge-hugging mark.
Each vendor module supplies a `TextMarkConfig` and only the behavior that cannot
be represented by the shared base:
@@ -1706,6 +1710,8 @@ be represented by the shared base:
- [`qwen_engine.py`](../src/remove_ai_watermarks/qwen_engine.py)
- [`kling_engine.py`](../src/remove_ai_watermarks/kling_engine.py)
- [`yuanbao_engine.py`](../src/remove_ai_watermarks/yuanbao_engine.py)
- [`dola_engine.py`](../src/remove_ai_watermarks/dola_engine.py)
- [`workbuddy_engine.py`](../src/remove_ai_watermarks/workbuddy_engine.py)
- [`samsung_engine.py`](../src/remove_ai_watermarks/samsung_engine.py)
- [`runninghub_engine.py`](../src/remove_ai_watermarks/runninghub_engine.py)
- [`baidu_engine.py`](../src/remove_ai_watermarks/baidu_engine.py)
@@ -1750,7 +1756,10 @@ on 2026-09-04 uses `KlingAI 3.0`. The Latin template scored 0.429 on that 1024 x
across the 94 available neighboring real and synthetic image controls, while an
adversarial solid corner blob reached 0.379, so its strict gate is 0.40. This is
one-positive provisional calibration, intentionally separate from the older
CJK detector rather than presented as broad recall evidence.
CJK detector rather than presented as broad recall evidence. Both layouts now
project row-wide and column-wide structure out of the winning patch before a
second correlation. This preserves glyph detail while rejecting ordinary panel
and image boundaries that previously matched the Latin template.
The measured Microsoft badge variant (2026-08-27 registration) is the first
`tr`-corner mark and the first `long`-side scale basis: the pill tracks the
@@ -1761,7 +1770,8 @@ undersized the template by the aspect ratio (portrait carriers fell to
its synthetic internal shapes knocked out - the holes are what separate it from
any other bright rounded corner element (a plain white pill scores below the gate
in the tests). It does not claim coverage of Microsoft's other documented icon,
wording, or position variants.
wording, or position variants beyond the separately gated beige `Made with AI`
text pill.
The 2026-08-27 rerun used the registered engine through
`scripts/registered_mark_calibrate.py`, rather than a copied detector
@@ -1798,8 +1808,9 @@ box grows), not the whole `footprint_mask`. Baidu extends right to the corner ta
and LiblibAI extends left to the triangle logo; both inherit every guard around
that arithmetic.
Doubao, Kling, and Samsung are deliberate sparse-mask exceptions because their alpha assets
supply more information than a rectangle. Doubao's continuous top-hat response
Doubao, Kling, and Samsung retain deliberate sparse masks for their original
layouts because those alpha assets supply more information than a rectangle.
Doubao's continuous top-hat response
locates the mark, then `DoubaoEngine.footprint_mask` resizes the alpha to that same winning
`match_box` and masks only the glyphs. The canonical 2048-pixel fixture has bright
branch texture behind the bottom-edge wordmark; bounding the thresholded response,
@@ -1809,6 +1820,13 @@ with large triangular wedges. The aligned sparse mask keeps both frame edges
untouched and still clears the detector. `force` cannot align a missing detection,
so it retains the shared geometry-box fallback.
The added outlined Doubao, stacked Yuanbao, Dola, WorkBuddy, Korean Samsung, and
beige Microsoft layouts use solid detector-aligned footprints where a sparse
synthetic glyph mask left visible outlines, sparkles, capsules, or shadows in
rendered removal checks. Each layout keeps its own detection threshold and mask
geometry; selecting one does not replace the calibrated behavior of the older
layout under the same registry key.
Doubao keeps the original low-saturation scan for pale corners. When enough of
its corner is strongly colored and that scan falls below the strict gate, a
wider color filter searches for white glyphs tinted by the background. The
@@ -1936,14 +1954,12 @@ exists.
#### OpenArt wordmark
Registered 2026-09-21 for a confirmed production miss reported through raiw-app:
a free `visible`-mode run (2026-09-11, 1528x2712 portrait, Spaces uid
`aefb1e07306f4a7280615a8daea01a39`) left a large, prominent OpenArt wordmark
Registered 2026-09-21 after a `visible`-mode run left a large, prominent OpenArt wordmark
(a bowtie/infinity icon followed by "OpenArt" in bold sans-serif, semi-
transparent white) completely unremoved -- pixel-identical before and after.
`identify` found nothing at all for that case: no C2PA, EXIF AI tag, or China
AIGC/TC260 label, so the visible mark is the only known attribution route for
a bare OpenArt export. The user rated the result 0 ("Didn't work").
a bare OpenArt export.
**This is the only registered mark placed at the FRAME CENTER rather than a
corner.** Every other engine in this family anchors to one of the four corners
@@ -2007,10 +2023,14 @@ geometry, and until one exists the bare-arm question is settled by this
measurement, not by gate tuning. The local calibration harness is not tracked.
(cohorts A/B/C over the no-signal pool).
The weak Jimeng pill detector lives in
[`pill_engine.py`](../src/remove_ai_watermarks/pill_engine.py). It uses a
synthetic silhouette for detection and a fixed top-left footprint. Its public,
published real-world regression carrier is
The Jimeng pill detector lives in
[`pill_engine.py`](../src/remove_ai_watermarks/pill_engine.py). It combines the
existing weak synthetic-outline path with a high-confidence contrast template
for the measured solid `AI生成` label. A Jimeng provenance match can corroborate
that verified label without the flat-background guard; the weak outline keeps
both the provenance and background gates described above. Removal follows the
matched label when verified and otherwise uses the fixed top-left footprint. Its
public, published real-world regression carrier is
`data/fixtures/visible/jimeng_pill/provider-published-example.jpg`; the direct
detector scores the pill at 0.28 and the same image's Jimeng wordmark at 0.61.
+8 -6
View File
@@ -11,19 +11,21 @@ The `visible` command registers these mark keys:
| Key | Mark | Expected area | Important limit |
| --- | --- | --- | --- |
| `gemini` | Google Gemini visible watermark (sparkle) | Usually bottom right | Detection includes a false positive gate. |
| `doubao` | `豆包AI生成` | Bottom right | Vendor specific text detector. |
| `doubao` | `豆包AI生成` | Bottom right | Covers filled and outlined text layouts; the weak outlined layout requires matching Doubao provenance. |
| `jimeng` | `★ 即梦AI` | Bottom right | Vendor specific text detector. |
| `qwen` | `千问AI生成` or the three-lobe Qwen symbol | Bottom right | Separate strict templates cover the text and Qwen Create symbol variants. |
| `wan` | Wan logo and `Wan` wordmark | Bottom right, hugging the corner | Provisional: one real Wan 2.7 Pro export. Requires NCC >= 0.55 and a match that ends within 0.015 of the short side from both edges. |
| `kling` | `可灵AI 3.0` or `KlingAI 3.0` | Bottom right | Separate calibrated silhouettes cover the older CJK and current IMAGE 3.0 Latin variants. |
| `yuanbao` | `元宝` over `AI生成` | Bottom right | Standard two-line variant only. |
| `samsung` | `✦ Contenuti generati dall'AI` | Bottom left | Calibrated for the Italian text variant. |
| `kling` | `可灵AI 3.0` or `KlingAI 3.0` | Bottom right | Separate calibrated silhouettes cover the older CJK and current IMAGE 3.0 Latin variants; a glyph-detail gate rejects plain UI boundaries. |
| `yuanbao` | `元宝` over `AI生成` | Bottom right | Separate strict and provenance-corroborated templates cover the measured two-line rasterizations. |
| `dola` | `Dola AI` | Bottom right | Text detector and detector-aligned wordmark footprint. |
| `workbuddy` | `AI生成` over `WORKBUDDY>_` | Bottom right | Text detector and detector-aligned stacked-label footprint. |
| `samsung` | Italian or Korean Galaxy AI disclosure | Bottom left | Independently gated text layouts cover `Contenuti generati dall'AI` and `AI로 생성한 콘텐츠`. |
| `runninghub` | `RunningHub AI生成` | Top left | Strict visual and position gates. |
| `baidu` | `百度 AI生成` | Bottom right | Detector and extended removal footprint. |
| `liblib` | `LiblibAI` | Bottom center | Historical wordmark variant; includes a minimum image size gate. |
| `liblib_pill` | Compact `AI生成` pill | Top left | Auto removal requires LiblibAI metadata or the bottom-center wordmark, plus a flat background under the pill. |
| `microsoft` | One Microsoft white AI-badge variant | Top right | Strict uses the visual gate; auto can use Microsoft provenance for the measured [relaxed gate](module-internals.md#visible-mark-removal). Other documented icon, text, and position variants are not covered. |
| `jimeng_pill` | `AI生成` pill | Top left | Weak detector with additional product and background gates. |
| `microsoft` | Microsoft white AI badge or beige `Made with AI` pill | Top right | Each layout has its own strict visual gate; auto can use Microsoft provenance for the original badge's measured [relaxed gate](module-internals.md#visible-mark-removal). |
| `jimeng_pill` | `AI生成` pill | Top left | A verified label layout can bypass the flat-background guard only with Jimeng provenance; the weak outline detector retains the product and background gates. |
| `generic_ai_label` | Bare `AI生成` text, no vendor wordmark | Bottom right | Brand-less TC260 compliance-stamp fallback (vivo and Xiaomi Gallery AI-edit output confirmed; see [module-internals.md](module-internals.md#generic-bare-ai生成-label)). Font-rendered synthetic asset; **calibrated on a synthetic corpus only**, not a real captured generator screenshot -- treat as weaker evidence than the vendor-tuned detectors above. |
| `openart` | `OpenArt` wordmark (bowtie/infinity icon + brand name) | Frame center, not a corner | Confirmed from one customer-reported production case; no known metadata signal, so the visible mark is the only attribution route. **UNCALIBRATED**: procedurally reconstructed asset, no captured corpus -- see [module-internals.md](module-internals.md#openart-wordmark). |
@@ -23,7 +23,6 @@ from the provider's canonical name, the provider is named separately.
| `samsung_en_alpha.png` | Samsung Galaxy AI label, English locale ("AI-generated content", bottom-left) | 5 corpus files | POS 0.11-0.30 vs NEG max 0.40 (binary); tophat/gray tried, no separation. The registered Italian engine scores 0.18-0.31 on the same files -- same layout class, wrong glyph template. |
| `gemini_text_alpha.png` | "Generated with Gemini" text label (bottom-right; the registered gemini mark is the sparkle icon) | 3 corpus files | POS 0.07-0.22 vs NEG max 0.32; coverage gate finds the blob (0.29-0.35) but the Arial silhouette misses Google's letterforms. |
| `notebooklm_alpha.png` | NotebookLM wordmark (bottom-right) | 12 corpus files | locate geometry not yet fitted; POS max 0.12. |
| `dola_alpha.png` | DolaAI on images (the video mark is registered) | 12 corpus files | POS 0.11-0.21 vs NEG max 0.30. |
| `mindvideo_alpha.png` | MindVideo.AI (top-right) | 11 corpus files | POS 0.29-0.32 vs NEG max 0.30 -- borderline overlap, not shippable. |
| `higgsfield_alpha.png` | `HIGGSFIELD AI` wordmark (Higgsfield AI, bottom-right; the boxed `AI` variant shares the cohort) | 5 wordmark files (16 in the boxed-AI OCR cluster) | POS max 0.26 vs NEG max 0.22 -- no separation; the mark may be two-part (wordmark + boxed AI) and needs a composed template. |
| `jianying_alpha.png` | `剪映AI` wordmark (Jianying / 剪映, the China product in the CapCut family; bottom-right) | 2 corpus files | POS 0.29 vs NEG max 0.35. |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

+68
View File
@@ -0,0 +1,68 @@
"""Rebuild synthetic silhouettes for additional AI disclosure layouts."""
from __future__ import annotations
import logging
import sys
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.render_vendor_silhouettes import render # noqa: E402
log = logging.getLogger(__name__)
ASSETS = ROOT / "src" / "remove_ai_watermarks" / "assets"
CJK = {"font": "/System/Library/Fonts/Hiragino Sans GB.ttc", "font_index": 2}
def centered_lines(
top: str, bottom: str, *, top_scale: float, bottom_height: float = 1.0, shear: float = 0.0
) -> np.ndarray:
upper, lower = render(top, opts=CJK), render(bottom, opts=CJK)
if bottom_height != 1.0:
lower = cv2.resize(lower, (upper.shape[1], round(upper.shape[0] * bottom_height)))
else:
upper = cv2.resize(upper, (round(lower.shape[1] * top_scale), round(lower.shape[0] * 0.95)))
lines = [upper, lower]
result = np.zeros((sum(line.shape[0] for line in lines) + 2, max(line.shape[1] for line in lines)), np.uint8)
y = 0
for line in lines:
x = (result.shape[1] - line.shape[1]) // 2
result[y : y + line.shape[0], x : x + line.shape[1]] = line
y += line.shape[0] + 2
if shear:
h, w = result.shape
extra = round(abs(shear) * h)
result = cv2.warpAffine(result, np.float32([[1, shear, extra], [0, 1, 0]]), (w + extra, h))
return result
def silhouettes() -> dict[str, np.ndarray]:
return {
"workbuddy_alpha.png": centered_lines("AI生成", "WORKBUDDY>_", top_scale=1.0, bottom_height=0.45),
"yuanbao_compact_alpha.png": centered_lines("元宝", "AI生成", top_scale=0.65, shear=-0.15),
"yuanbao_compact_gray_alpha.png": centered_lines("元宝", "AI生成", top_scale=0.65, shear=-0.15),
"doubao_outline_alpha.png": render("豆包AI生成", opts=CJK),
"samsung_ko_alpha.png": render(
"AI로 생성한 콘텐츠", opts={"font": "/System/Library/Fonts/AppleSDGothicNeo.ttc"}
),
"microsoft_text_alpha.png": render(
"Made with AI", opts={"font": "/System/Library/Fonts/Supplemental/Arial.ttf"}
),
"jimeng_label_alpha.png": render("AI生成", opts={"font": "/System/Library/Fonts/STHeiti Medium.ttc"}),
}
def main() -> None:
logging.basicConfig(level=logging.INFO)
for name, alpha in silhouettes().items():
Image.fromarray(alpha).save(ASSETS / name)
log.info("Rendered %s, %s x %s", name, alpha.shape[1], alpha.shape[0])
if __name__ == "__main__":
main()
+2 -1
View File
@@ -73,7 +73,7 @@ MARKS = {
"gemini_text_alpha.png": "Generated with Gemini",
# Candidate wordmarks measured on a local evaluation corpus (unregistered).
"notebooklm_alpha.png": "NotebookLM",
"dola_alpha.png": "DolaAI",
"dola_alpha.png": "Dola AI",
"mindvideo_alpha.png": "MindVideo.AI",
"higgsfield_alpha.png": "HIGGSFIELD AI",
"capcut_alpha.png": "CapCut AI",
@@ -92,6 +92,7 @@ _REGISTERED = {f"{key}_alpha.png" for key in mark_keys()} & MARKS.keys()
# Per-mark post-processing for the multi-line / slanted stamps (see render()).
MARK_OPTS: dict[str, dict[str, Any]] = {
"dola_alpha.png": {"font": "/System/Library/Fonts/Supplemental/Arial Bold.ttf"},
# Hiragino Sans GB W6, tight leading, dilation, and negative shear match the
# standard Yuanbao stamp without clipping the lower line.
"yuanbao_alpha.png": {
@@ -21,7 +21,7 @@ remove-ai-watermarks visible image.png --keep-metadata -o clean.png
```
Image mark keys: `gemini`, `doubao`, `jimeng`, `qwen`, `wan`, `kling`, `yuanbao`,
`samsung`, `runninghub`, `baidu`, `liblib`, `liblib_pill`, `microsoft`,
`dola`, `workbuddy`, `samsung`, `runninghub`, `baidu`, `liblib`, `liblib_pill`, `microsoft`,
`generic_ai_label`, `openart`, `jimeng_pill`. `wan` is new (since CLI 0.43.0). Default
`--mark auto` removes every selected match. `microsoft` is the top-right AI
badge, separate from the invisible InvisMark watermark on the same vendor's
@@ -181,6 +181,7 @@ class TextMarkDetection:
# ``footprint_mask`` reuses a threaded detection only when it matches the STRICT
# level its own re-detect would have used -- see TextMarkEngine._strict_detection.
provenance: bool = False
template_asset: str | None = None
def best_detection(*detections: TextMarkDetection) -> TextMarkDetection:
@@ -665,7 +666,7 @@ class TextMarkEngine:
def _verdict(self, scan: TextMarkScan, *, provenance: bool) -> TextMarkDetection:
"""Apply the trust-level-dependent tail to a scan, as a fresh result object."""
c = self.config
det = TextMarkDetection(provenance=provenance)
det = TextMarkDetection(provenance=provenance, template_asset=c.asset_name)
if scan.loc is None or scan.box is None:
return det
det.region = scan.loc.bbox
@@ -0,0 +1,219 @@
"""Combine independently gated text layouts and retain their mask geometry."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import cv2
from remove_ai_watermarks._text_mark_engine import (
BottomRightAnchoredEngine,
TextMarkConfig,
TextMarkDetection,
TextMarkEngine,
TextMarkLocation,
best_detection,
)
if TYPE_CHECKING:
from numpy.typing import NDArray
class TextMarkVariantsMixin(TextMarkEngine):
"""Add independently configured layouts to an existing text-mark engine."""
_variants: tuple[TextMarkEngine, ...] = ()
config: TextMarkConfig
def detect_both(self, image: NDArray[Any] | None) -> tuple[TextMarkDetection, TextMarkDetection]:
pairs = [super().detect_both(image), *(engine.detect_both(image) for engine in self._variants)]
return best_detection(*(pair[0] for pair in pairs)), best_detection(*(pair[1] for pair in pairs))
def detect(self, image: NDArray[Any], *, provenance: bool = False) -> TextMarkDetection:
return self.detect_both(image)[int(provenance)]
def variant_engine(self, detection: TextMarkDetection) -> TextMarkEngine:
for engine in self._variants:
if engine.config.asset_name == detection.template_asset:
return engine
return self
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
if image is None or image.size == 0 or force:
return super().footprint_mask(image, force=force, dilate=dilate, detection=detection)
det = detection if detection is not None else self.detect(image)
engine = self.variant_engine(det)
if engine is self:
return super().footprint_mask(image, force=False, dilate=dilate, detection=det)
return engine.footprint_mask(image, force=False, dilate=dilate, detection=det)
class GlyphTextMarkEngine(TextMarkEngine):
"""Use the accepted template's aligned strokes instead of a textured ROI."""
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
from remove_ai_watermarks._text_mark_engine import load_alpha_template
if force:
return super().footprint_mask(image, force=True, dilate=dilate, detection=detection)
if image is None or image.size == 0:
return None
det = detection if detection is not None else self.detect(image)
alpha = load_alpha_template(self.config.asset_name)
if alpha is None or not det.detected:
return None
return self._aligned_alpha_mask(
image,
det,
alpha,
alpha_floor=0.05,
dilate=2 if dilate is None else max(0, dilate),
)
class BottomRightGlyphTextMarkEngine(GlyphTextMarkEngine, BottomRightAnchoredEngine):
"""Glyph-mask variant whose accepted match must hug the bottom-right corner."""
_ANCHOR_MAX_RIGHT = 0.04
_ANCHOR_MAX_BOTTOM = 0.04
def _ladder_best(
self, image: NDArray[Any], loc: TextMarkLocation
) -> tuple[float, tuple[int, int, int, int] | None]:
"""Find the strongest match among placements that satisfy the anchor."""
response = self._detect_response(image, loc)
silhouette = self._glyph_silhouette()
if response is None or silhouette is None:
return 0.0, None
frame_h, frame_w = image.shape[:2]
anchor_base = min(frame_h, frame_w)
base = self.scale_base(image)
best_score = 0.0
best_box: tuple[int, int, int, int] | None = None
for scale in self.config.ladder:
glyph_w = max(self.config.min_gw, int(self.config.alpha_width_frac * base * scale))
glyph_h = max(4, int(self.config.alpha_height_frac * base * scale))
if glyph_w >= response.shape[1] or glyph_h >= response.shape[0]:
continue
template = cv2.resize(silhouette, (glyph_w, glyph_h), interpolation=cv2.INTER_AREA)
scores = cv2.matchTemplate(response, template, cv2.TM_CCOEFF_NORMED)
min_x = max(0, round(frame_w - loc.x - glyph_w - self._ANCHOR_MAX_RIGHT * anchor_base))
max_x = min(scores.shape[1] - 1, frame_w - loc.x - glyph_w)
min_y = max(0, round(frame_h - loc.y - glyph_h - self._ANCHOR_MAX_BOTTOM * anchor_base))
max_y = min(scores.shape[0] - 1, frame_h - loc.y - glyph_h)
if min_x > max_x or min_y > max_y:
continue
anchored = scores[min_y : max_y + 1, min_x : max_x + 1]
_, score, _, point = cv2.minMaxLoc(anchored)
if score > best_score:
x, y = min_x + int(point[0]), min_y + int(point[1])
best_score = float(score)
best_box = (x, y, x + glyph_w - 1, y + glyph_h - 1)
return best_score, best_box
def _box_footprint(
engine: GlyphTextMarkEngine,
image: NDArray[Any] | None,
*,
force: bool,
dilate: int | None,
detection: TextMarkDetection | None,
pad_left_height: float,
pad_right_height: float,
) -> NDArray[Any] | None:
"""Mask the full winning text rectangle for outlined or stacked layouts."""
if image is None or image.size == 0 or force:
return GlyphTextMarkEngine.footprint_mask(
engine,
image,
force=force,
dilate=dilate,
detection=detection,
)
det = detection if detection is not None else engine.detect(image)
if not det.detected or det.match_box is None:
return None
from remove_ai_watermarks import region_eraser
loc = engine.locate(image)
x0, y0, x1, y1 = det.match_box
extra_left = round((y1 - y0 + 1) * pad_left_height)
extra_right = round((y1 - y0 + 1) * pad_right_height)
pad = 2 if dilate is None else max(0, dilate)
return region_eraser.boxes_to_mask(
image.shape[:2],
[
(
loc.x + x0 - extra_left,
loc.y + y0,
x1 - x0 + 1 + extra_left + extra_right,
y1 - y0 + 1,
)
],
dilate=pad,
)
class BoxTextMarkEngine(GlyphTextMarkEngine):
"""Text detector with a solid detector-aligned removal footprint."""
_BOX_PAD_LEFT_HEIGHT = 0.0
_BOX_PAD_RIGHT_HEIGHT = 0.0
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
return _box_footprint(
self,
image,
force=force,
dilate=dilate,
detection=detection,
pad_left_height=self._BOX_PAD_LEFT_HEIGHT,
pad_right_height=self._BOX_PAD_RIGHT_HEIGHT,
)
class BottomRightBoxTextMarkEngine(BottomRightGlyphTextMarkEngine):
"""Anchored text detector with a solid detector-aligned footprint."""
_BOX_PAD_LEFT_HEIGHT = 0.0
_BOX_PAD_RIGHT_HEIGHT = 0.0
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
return _box_footprint(
self,
image,
force=force,
dilate=dilate,
detection=detection,
pad_left_height=self._BOX_PAD_LEFT_HEIGHT,
pad_right_height=self._BOX_PAD_RIGHT_HEIGHT,
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+35
View File
@@ -0,0 +1,35 @@
"""Detect the bottom-right Dola AI image disclosure, independently of video marks."""
from remove_ai_watermarks._text_mark_engine import TextMarkConfig
from remove_ai_watermarks._text_mark_variants import BoxTextMarkEngine
_CONFIG = TextMarkConfig(
name="Dola AI image disclosure",
asset_name="dola_alpha.png",
corner="br",
margin_floor=4,
width_frac=0.28,
height_frac=0.09,
margin_x_frac=0.002,
margin_bottom_frac=0.002,
max_saturation=255,
logo_min_luma=110,
tophat_delta=8,
morph_open_size=3,
detect_min_coverage=0.01,
detect_ncc_threshold=0.50,
alpha_width_frac=0.10,
alpha_height_frac=0.10 * 86 / 347,
detect_frontend="tophat",
scale_basis="width",
ladder=(0.95, 1.0, 1.05, 1.20, 1.26),
provenance_ncc_factor=1.0,
min_gw=24,
)
class DolaEngine(BoxTextMarkEngine):
"""Detect and localize the Dola AI image wordmark."""
def __init__(self) -> None:
super().__init__(_CONFIG)
+61 -3
View File
@@ -25,10 +25,12 @@ from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING, Any
import cv2
import numpy as np
from remove_ai_watermarks import _text_mark_engine, image_io
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine, TextMarkScan
from remove_ai_watermarks._text_mark_variants import BottomRightBoxTextMarkEngine, TextMarkVariantsMixin
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -117,6 +119,41 @@ _CONFIG = TextMarkConfig(
_TINTED_ENGINE = TextMarkEngine(dataclasses.replace(_CONFIG, max_saturation=_TINTED_MAX_SATURATION))
class _OutlinedDoubaoEngine(BottomRightBoxTextMarkEngine):
# The outlined layout's provenance-relaxed NCC floor overlaps ordinary corner
# texture. Require bright neutral strokes and edge detail in the winning box
# to distinguish the label from unmarked corners. Metadata confirms
# Doubao as the producer, not that the export retained a visible label.
_MIN_BRIGHT_NEUTRAL_FRACTION = 0.29
_MIN_MEAN_LAPLACIAN = 10.0
_BOX_PAD_LEFT_HEIGHT = 1.2
_BOX_PAD_RIGHT_HEIGHT = 0.7
@classmethod
def _outline_pixels_ok(cls, patch: NDArray[Any]) -> bool:
if patch.size == 0:
return False
patch = image_io.to_bgr(patch)
gray = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
saturation = cv2.cvtColor(patch, cv2.COLOR_BGR2HSV)[:, :, 1]
bright_neutral = float(np.mean((gray > 160) & (saturation < 60)))
edge_detail = float(np.mean(np.abs(cv2.Laplacian(gray, cv2.CV_32F))))
return bright_neutral >= cls._MIN_BRIGHT_NEUTRAL_FRACTION and edge_detail >= cls._MIN_MEAN_LAPLACIAN
def _scan(self, image: NDArray[Any] | None) -> TextMarkScan:
scan = super()._scan(image)
if image is None or scan.loc is None or scan.match_box is None or scan.score is None:
return scan
x0, y0, x1, y1 = scan.match_box
patch = image[
scan.loc.y + y0 : scan.loc.y + y1 + 1,
scan.loc.x + x0 : scan.loc.x + x1 + 1,
]
if not self._outline_pixels_ok(patch):
return dataclasses.replace(scan, score=0.0)
return scan
def _alpha_template() -> NDArray[Any] | None:
"""The bundled Doubao alpha template (float [0,1]), or None."""
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
@@ -132,11 +169,29 @@ def _template_match_score(box_mask: NDArray[Any], scale_base: int) -> float:
return _text_mark_engine.template_match_score(box_mask, scale_base, _CONFIG)
class DoubaoEngine(TextMarkEngine):
class DoubaoEngine(TextMarkVariantsMixin):
"""Detect/localize the visible Doubao "豆包AI生成" watermark (locate -> mask; mask feeds the fill)."""
def __init__(self) -> None:
super().__init__(_CONFIG)
self._variants = (
_OutlinedDoubaoEngine(
dataclasses.replace(
_CONFIG,
asset_name="doubao_outline_alpha.png",
width_frac=0.28,
height_frac=0.13,
scale_basis="width",
alpha_width_frac=0.14,
alpha_height_frac=0.14 * 0.75 * 77 / 346,
max_saturation=255,
detect_frontend="contrast",
ladder=(0.95, 1.0, 1.05),
detect_ncc_threshold=0.50,
provenance_ncc_factor=0.25,
)
),
)
def _scan(self, image: NDArray[Any] | None) -> TextMarkScan:
base = super()._scan(image)
@@ -179,12 +234,15 @@ class DoubaoEngine(TextMarkEngine):
det = detection if detection is not None else self.detect(image)
if not det.detected or det.match_box is None:
return super().footprint_mask(image, force=False, dilate=dilate, detection=det)
alpha = _alpha_template()
engine = self.variant_engine(det)
if engine is not self:
return engine.footprint_mask(image, force=False, dilate=dilate, detection=det)
alpha = _text_mark_engine.load_alpha_template(engine.config.asset_name)
if alpha is None:
return super().footprint_mask(image, force=False, dilate=dilate, detection=det)
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
return self._aligned_alpha_mask(
return engine._aligned_alpha_mask(
image,
det,
alpha,
+6 -3
View File
@@ -957,7 +957,8 @@ def _integrity_clashes(
clashes.append(
"Conflicting AI-origin attributions from independent signals: "
+ " vs ".join(parts)
+ " -- one provenance set was likely spoofed, transplanted, or laundered."
+ " -- these fields may describe different stages of an edit chain; "
"the available evidence does not establish which attribution describes the final pixels."
)
# Rule 2: a camera-capture C2PA device next to an AI-generation marker. Only
@@ -967,13 +968,15 @@ def _integrity_clashes(
# AI edit in ONE manifest, so the AI vendor is named only from that same
# manifest (C2PA issuer + SynthID provenance) -- a legitimate edit chain, not a
# spoof. An EXIF/XMP generator, IPTC field, TC260 AIGC label, or second
# manifest naming AI on a camera capture is the real laundering tell.
# manifest naming AI on a camera capture records a second stage whose relation
# to the capture cannot be resolved from these fields alone.
independent_ai_marker = any(grp != _C2PA_MANIFEST_SOURCE for grp in source.values())
if camera_label and camera_has_ai_marker and independent_ai_marker:
vendors = ", ".join(sorted(set(ai_vendors.values()))) or "present"
clashes.append(
f"Camera-capture C2PA credentials ({camera_label}) coexist with AI-generation markers "
f"({vendors}) -- a genuine camera capture is not AI-generated, so the provenance is inconsistent."
f"({vendors}) -- these fields may describe a captured image followed by an AI edit; "
"the available evidence does not establish which stage describes the final pixels."
)
return clashes
+41 -10
View File
@@ -56,6 +56,7 @@ from dataclasses import replace
from typing import TYPE_CHECKING, Any
import cv2
import numpy as np
from remove_ai_watermarks import _text_mark_engine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine
@@ -136,8 +137,12 @@ _CONFIG = TextMarkConfig(
_LATIN_CONFIG = replace(
_CONFIG,
asset_name="kling_latin_alpha.png",
alpha_width_frac=0.095,
alpha_height_frac=0.028,
width_frac=0.22,
height_frac=0.07,
margin_x_frac=0.002,
margin_bottom_frac=0.002,
alpha_width_frac=0.14,
alpha_height_frac=0.032,
ladder=(0.9, 1.0, 1.1),
detect_ncc_threshold=0.40,
)
@@ -153,12 +158,37 @@ def _glyph_silhouette() -> NDArray[Any] | None:
return _text_mark_engine.glyph_silhouette(_CONFIG.asset_name)
class KlingEngine(TextMarkEngine):
class _KlingTextEngine(TextMarkEngine):
"""Require glyph structure beyond a horizontal or vertical background edge."""
def _ladder_best(
self, image: NDArray[Any], loc: _text_mark_engine.TextMarkLocation
) -> tuple[float, tuple[int, int, int, int] | None]:
score, box = super()._ladder_best(image, loc)
response = self._detect_response(image, loc)
silhouette = self._glyph_silhouette()
if box is None or response is None or silhouette is None:
return score, box
x, y, x1, y1 = box
patch = response[y : y1 + 1, x : x1 + 1].astype(np.float32)
template = cv2.resize(silhouette, (patch.shape[1], patch.shape[0]), interpolation=cv2.INTER_AREA)
def glyph_detail(array: NDArray[Any]) -> NDArray[Any]:
array = array.astype(np.float32)
return array - array.mean(axis=0, keepdims=True) - array.mean(axis=1, keepdims=True) + array.mean()
patch, template = glyph_detail(patch), glyph_detail(template)
denominator = float(np.linalg.norm(patch) * np.linalg.norm(template))
detail_score = float((patch * template).sum()) / denominator if denominator > 1e-6 else 0.0
return max(0.0, min(score, detail_score)), box
class KlingEngine(_KlingTextEngine):
"""Detect/localize Kling AI's CJK and Latin 3.0 marks (locate -> mask -> fill)."""
def __init__(self) -> None:
super().__init__(_CONFIG)
self._latin = TextMarkEngine(_LATIN_CONFIG)
self._latin = _KlingTextEngine(_LATIN_CONFIG)
def detect(self, image: NDArray[Any], *, provenance: bool = False) -> TextMarkDetection:
"""Return the strongest CJK or Latin Kling wordmark verdict."""
@@ -195,14 +225,15 @@ class KlingEngine(TextMarkEngine):
return super().footprint_mask(image, force=force, dilate=dilate, detection=detection)
det = detection if detection is not None else self.detect(image)
legacy = super().footprint_mask(image, force=False, dilate=dilate, detection=det)
if not det.detected:
return None
engine = self._latin if det.template_asset == _LATIN_CONFIG.asset_name else self
legacy = TextMarkEngine.footprint_mask(engine, image, force=False, dilate=dilate, detection=det)
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
result = legacy
for config in (_CONFIG, _LATIN_CONFIG):
alpha = _text_mark_engine.load_alpha_template(config.asset_name)
if alpha is None:
continue
core = self._aligned_alpha_mask(
alpha = _text_mark_engine.load_alpha_template(engine.config.asset_name)
if alpha is not None:
core = engine._aligned_alpha_mask(
image,
det,
alpha,
+1 -1
View File
@@ -170,7 +170,7 @@ class LibLibPillEngine:
"""Detect the compact top-left pill only when LiblibAI is corroborated."""
def __init__(self) -> None:
self._pill = PillEngine()
self._pill = PillEngine(include_label=False)
def _raw(self, image: NDArray[Any]) -> PillDetection:
return self._pill.detect(image)
+58 -2
View File
@@ -38,10 +38,12 @@ status was not adjudicated, and 1200 non-overlapping no-signal controls:
from __future__ import annotations
from dataclasses import replace
from typing import TYPE_CHECKING, Any
from remove_ai_watermarks import _text_mark_engine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkEngine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection
from remove_ai_watermarks._text_mark_variants import GlyphTextMarkEngine, TextMarkVariantsMixin
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -101,8 +103,62 @@ def _alpha_template() -> NDArray[Any] | None:
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
class MicrosoftEngine(TextMarkEngine):
class MicrosoftEngine(TextMarkVariantsMixin):
"""Detect/localize the measured Microsoft top-right AI badge."""
def __init__(self) -> None:
super().__init__(_CONFIG)
self._variants = (
GlyphTextMarkEngine(
replace(
_CONFIG,
asset_name="microsoft_text_alpha.png",
width_frac=0.20,
height_frac=0.065,
margin_x_frac=0.002,
margin_bottom_frac=0.002,
scale_basis="width",
alpha_width_frac=0.14,
alpha_height_frac=0.14 * 49 / 339,
detect_frontend="contrast",
ladder=(0.95, 1.0, 1.05),
detect_ncc_threshold=0.60,
provenance_ncc_factor=1.0,
max_saturation=255,
)
),
)
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
"""Cover the enclosing beige pill for the text-only layout."""
if force or image is None or image.size == 0:
return super().footprint_mask(image, force=force, dilate=dilate, detection=detection)
det = detection if detection is not None else self.detect(image)
engine = self.variant_engine(det)
if engine is self or not det.detected or det.match_box is None:
return super().footprint_mask(image, force=False, dilate=dilate, detection=det)
from remove_ai_watermarks import region_eraser
loc = engine.locate(image)
x0, y0, x1, y1 = det.match_box
pad_x = int(image.shape[1] * 0.012)
pad_y = int(image.shape[1] * 0.014)
box = (
max(0, loc.x + x0 - pad_x),
max(0, loc.y + y0 - pad_y),
x1 - x0 + 1 + 2 * pad_x,
y1 - y0 + 1 + 2 * pad_y,
)
return region_eraser.boxes_to_mask(
image.shape[:2],
[box],
dilate=0 if dilate is None else max(0, dilate),
)
+1 -2
View File
@@ -3,8 +3,7 @@
OpenArt (openart.ai) stamps free-plan generations with a semi-transparent white
"OpenArt" wordmark (a bowtie/infinity icon followed by the brand name) placed
**near the center of the frame**, not in a corner like every other text mark
this repository detects. Confirmed from one customer-reported production case
(raiw-app, 2026-09-11, 1528x2712 portrait output): the mark sat roughly centered
this repository detects. In the observed portrait layout, the mark sat roughly centered
over the subject's torso, and neither C2PA, EXIF AI tags, nor the China AIGC/TC260
label were present -- OpenArt apparently ships no machine-readable provenance
signal alongside this visible mark, so a bare export was previously reported as
+101 -25
View File
@@ -1,18 +1,20 @@
"""Jimeng-basic 'AI生成' pill: a CAPTURE-LESS visible mark (issue #54).
"""Jimeng-basic ``AI生成`` pill visible mark (issue #54).
The Jimeng free-tier TC260 label is a rounded pill with 'AI生成' in the TOP-LEFT
corner -- distinct from the ``jimeng`` "★ 即梦AI" wordmark (bottom-right). It has no
captured alpha map, so unlike the other marks it is detected purely by a synthetic
silhouette; like every mark it is then removed by the shared localize -> fill:
captured alpha map for its outline. Detection therefore has two paths before the
shared localize -> fill step:
* Detect: edge-NCC of a font-rendered SILHOUETTE (``assets/jimeng_pill.png``,
* A high-confidence contrast template for the measured solid ``AI生成`` label
(``assets/jimeng_label_alpha.png``). With Jimeng provenance this verified
label can bypass the flat-background safety gate.
* Edge-NCC of a font-rendered SILHOUETTE (``assets/jimeng_pill.png``,
synthetic, data-safe -- see ``scripts/render_pill_silhouette.py``) against the
top-left ROI, at the pill's known width fraction. The calibrated
``_DETECT_THRESHOLD`` is 0.22.
* Remove: place the pill footprint at the matched location and inpaint it
(MI-GAN / cv2 via the registry). Quality comes from the inpaint backend, so the
silhouette need not be pixel-accurate -- which is why a synthetic render is
sufficient and no source-derived asset is committed.
top-left ROI, at the pill's known width fraction. Its calibrated
``_DETECT_THRESHOLD`` is 0.22 and it retains the provenance and flat-background
product gates.
* Remove: mask the verified label match or the stable outline footprint and
inpaint it (MI-GAN / cv2 via the registry).
Geometry uses width ~0.161*W,
height ~0.091*W, top-left, margins ~0.02-0.05.
@@ -27,6 +29,7 @@ import cv2
import numpy as np
from remove_ai_watermarks import image_io
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -74,6 +77,32 @@ class PillDetection(NamedTuple):
detected: bool
confidence: float
region: tuple[int, int, int, int] # x, y, w, h of the matched pill
verified_label: bool = False
_LABEL_CONFIG = TextMarkConfig(
name="Jimeng AI生成 pill label",
asset_name="jimeng_label_alpha.png",
corner="tl",
margin_floor=2,
width_frac=0.34,
height_frac=0.14,
margin_x_frac=0.0,
margin_bottom_frac=0.0,
max_saturation=255,
logo_min_luma=100,
tophat_delta=8,
morph_open_size=3,
detect_min_coverage=0.01,
detect_ncc_threshold=0.55,
detect_frontend="contrast",
scale_basis="width",
ladder=(0.95, 1.0, 1.05),
alpha_width_frac=0.14,
alpha_height_frac=0.14 * 128 / 353,
min_gw=24,
provenance_ncc_factor=1.0,
)
def _load_silhouette() -> NDArray[Any] | None:
@@ -92,7 +121,18 @@ def _grad(gray: NDArray[Any]) -> NDArray[Any]:
class PillEngine:
"""Detect + build the removal mask for the top-left 'AI生成' pill (edge-NCC of a synthetic silhouette)."""
"""Detect the top-left ``AI生成`` label or its synthetic pill outline."""
def __init__(self, *, include_label: bool = True) -> None:
self._label = TextMarkEngine(_LABEL_CONFIG) if include_label else None
@staticmethod
def _label_box(detection: TextMarkDetection) -> tuple[int, int, int, int] | None:
if not detection.detected or detection.match_box is None:
return None
x, y, _width, _height = detection.region
x0, y0, x1, y1 = detection.match_box
return x + x0, y + y0, x1 - x0 + 1, y1 - y0 + 1
def _match(self, image: NDArray[Any]) -> tuple[float, tuple[int, int, int, int]] | None:
sil = _load_silhouette()
@@ -113,12 +153,33 @@ class PillEngine:
_, score, _, loc = cv2.minMaxLoc(res)
return float(score), (int(loc[0]), int(loc[1]), tw, th)
def detect(self, image: NDArray[Any]) -> PillDetection:
m = self._match(image)
if m is None:
return PillDetection(False, 0.0, (0, 0, 0, 0))
score, box = m
return PillDetection(score >= _DETECT_THRESHOLD, score, box)
def _detection(
self,
match: tuple[float, tuple[int, int, int, int]] | None,
label: TextMarkDetection,
) -> PillDetection:
label_box = self._label_box(label)
if match is None:
return PillDetection(label.detected, label.confidence, label_box or label.region, label.detected)
score, box = match
return PillDetection(
score >= _DETECT_THRESHOLD or label.detected,
max(score, label.confidence),
label_box if label.detected and label_box is not None else box,
label.detected,
)
def detect(self, image: NDArray[Any], *, provenance: bool = False) -> PillDetection:
"""Detect the outline, plus the solid label when Jimeng is corroborated."""
label_scan = self._label.detect(image) if self._label is not None else TextMarkDetection()
label = label_scan if provenance else TextMarkDetection()
return self._detection(self._match(image), label)
def detect_both(self, image: NDArray[Any]) -> tuple[PillDetection, PillDetection]:
"""Return strict outline and Jimeng-corroborated label verdicts from one scan."""
match = self._match(image)
label = self._label.detect(image) if self._label is not None else TextMarkDetection()
return self._detection(match, TextMarkDetection()), self._detection(match, label)
def _footprint_box(self, image: NDArray[Any]) -> tuple[int, int, int, int] | None:
h, w = image.shape[:2]
@@ -152,19 +213,34 @@ class PillEngine:
"""True when the top-left footprint is flat enough for an invisible inpaint."""
return self.footprint_texture(image) <= thresh
def footprint_mask(self, image: NDArray[Any], *, force: bool = False) -> NDArray[Any] | None:
def footprint_mask(
self,
image: NDArray[Any],
*,
force: bool = False,
detection: PillDetection | None = None,
) -> NDArray[Any] | None:
"""Full-frame uint8 mask (255 = pill) over the pill's known top-left region.
Uses stable GEOMETRY (a generous fixed box), not the NCC match position: the
synthetic silhouette localizes only approximately, so a match-positioned mask
leaves outline residue, while the top-left corner is negative space, so a
generous geometric box removes the pill cleanly and harmlessly. The caller
A verified label uses its detector-aligned box with measured padding. The
weaker synthetic outline uses stable geometry because its NCC position is
only approximate and a match-positioned mask leaves rim residue. The caller
gates on :meth:`detect`, so a clean corner is never masked. ``force`` is
accepted for a uniform engine signature but ignored (the geometry box is
fixed regardless)."""
accepted for a uniform engine signature but ignored."""
if image is None or image.size == 0:
return None
box = self._footprint_box(image)
if detection is not None and detection.verified_label:
x, y, width, height = detection.region
pad_x = int(image.shape[1] * 0.025)
pad_y = int(image.shape[1] * 0.014)
box = (
max(0, x - pad_x),
max(0, y - pad_y),
min(image.shape[1], x + width + pad_x),
min(image.shape[0], y + height + pad_y),
)
else:
box = self._footprint_box(image)
if box is None:
return None
# Same primitive the shared fill uses, rather than a private zeros/fill copy.
+42 -5
View File
@@ -25,10 +25,12 @@ signal via the registry.
from __future__ import annotations
from dataclasses import replace
from typing import TYPE_CHECKING, Any
from remove_ai_watermarks import _text_mark_engine, image_io
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection
from remove_ai_watermarks._text_mark_variants import GlyphTextMarkEngine, TextMarkVariantsMixin
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -107,11 +109,27 @@ def _template_match_score(box_mask: NDArray[Any], scale_base: int) -> float:
return _text_mark_engine.template_match_score(box_mask, scale_base, _CONFIG)
class SamsungEngine(TextMarkEngine):
class SamsungEngine(TextMarkVariantsMixin):
"""Detect/localize the visible Samsung Galaxy AI text mark (locate -> mask; mask feeds the fill)."""
def __init__(self) -> None:
super().__init__(_CONFIG)
self._variants = (
GlyphTextMarkEngine(
replace(
_CONFIG,
asset_name="samsung_ko_alpha.png",
width_frac=0.28,
height_frac=0.065,
alpha_width_frac=0.20,
alpha_height_frac=0.20 * 49 / 344,
ladder=(0.95, 1.0, 1.05),
max_saturation=255,
detect_frontend="contrast",
detect_ncc_threshold=0.46,
)
),
)
def footprint_mask(
self,
@@ -134,10 +152,29 @@ class SamsungEngine(TextMarkEngine):
image = image_io.to_bgr(image)
det = detection if detection is not None else self.detect(image)
alpha = _alpha_template()
engine = self.variant_engine(det)
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
if engine is not self and det.detected and det.match_box is not None:
from remove_ai_watermarks import region_eraser
x0, y0, x1, y1 = det.match_box
loc = engine.locate(image)
glyph_height = y1 - y0 + 1
return region_eraser.boxes_to_mask(
image.shape[:2],
[
(
max(0, loc.x + x0 - round(1.3 * glyph_height)),
loc.y + y0,
x1 - x0 + 1 + round(1.3 * glyph_height),
glyph_height,
)
],
dilate=radius,
)
alpha = _text_mark_engine.load_alpha_template(engine.config.asset_name)
if alpha is not None:
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
aligned = self._aligned_alpha_mask(
aligned = engine._aligned_alpha_mask(
image,
det,
alpha,
+80 -28
View File
@@ -24,13 +24,15 @@ Entries:
- ``qwen`` -- Alibaba Cloud Qwen "千问AI生成" text or three-lobe symbol, bottom-right.
- ``kling`` -- Kuaishou Kling AI "可灵AI 3.0" / "KlingAI 3.0" strip, bottom-right.
- ``yuanbao`` -- Tencent Yuanbao "元宝 / AI生成" two-line mark, bottom-right.
- ``samsung`` -- Samsung Galaxy AI "Contenuti generati dall'AI" strip, bottom-left.
- ``jimeng_pill`` -- Jimeng-basic "AI生成" pill, top-left (capture-less).
- ``dola`` -- ByteDance Dola "Dola AI" image disclosure, bottom-right.
- ``workbuddy`` -- Tencent WorkBuddy "AI生成 / WORKBUDDY" disclosure, bottom-right.
- ``samsung`` -- Samsung Galaxy AI Italian or Korean disclosure, bottom-left.
- ``jimeng_pill`` -- Jimeng-basic "AI生成" pill or solid label, top-left.
- ``runninghub`` -- RunningHub "RunningHub AI生成" text, top-left (gray front-end).
- ``baidu`` -- Baidu "百度 AI生成" text + white tag, bottom-right.
- ``liblib`` -- LiblibAI "LiblibAI" wordmark, bottom-center.
- ``liblib_pill`` -- LiblibAI compact "AI生成" pill, top-left.
- ``microsoft`` -- one measured Microsoft white AI-badge variant, top-right.
- ``microsoft`` -- measured Microsoft white AI badge or beige text pill, top-right.
- ``generic_ai_label`` -- brand-less bare "AI生成" TC260 label (no vendor wordmark),
bottom-right; the fallback for a compliance stamp not tied to any tuned mark above.
- ``openart`` -- OpenArt "OpenArt" wordmark (bowtie/infinity icon + brand name),
@@ -448,6 +450,8 @@ _ENGINE_CLASS: dict[str, tuple[str, str]] = {
"jimeng": ("jimeng_engine", "JimengEngine"),
"qwen": ("qwen_engine", "QwenEngine"),
"kling": ("kling_engine", "KlingEngine"),
"dola": ("dola_engine", "DolaEngine"),
"workbuddy": ("workbuddy_engine", "WorkBuddyEngine"),
"yuanbao": ("yuanbao_engine", "YuanbaoEngine"),
"samsung": ("samsung_engine", "SamsungEngine"),
"jimeng_pill": ("pill_engine", "PillEngine"),
@@ -641,29 +645,44 @@ def _text_mark(
)
# ── Capture-less mark: the Jimeng-basic "AI生成" pill (top-left) ──
# Detection is edge-NCC of a synthetic silhouette; the mask is a fixed top-left
# geometry box (see pill_engine). Removal is the same localize -> fill as the rest.
# ── Jimeng-basic "AI生成" pill or solid label (top-left) ──
# The weak outline uses synthetic edge-NCC and fixed geometry. The verified solid
# label uses its own contrast template and detector-aligned mask (see pill_engine).
def _pill_detect(image: NDArray[Any], *, provenance: bool = False) -> MarkDetection:
del provenance # the pill detector is provenance-independent; its relaxation lives entirely in _keep_pill
d = _engine("jimeng_pill").detect(image)
return MarkDetection("jimeng_pill", "Jimeng AI生成 pill", "top-left", d.detected, d.confidence, d.region)
d = _engine("jimeng_pill").detect(image, provenance=provenance)
return MarkDetection(
"jimeng_pill",
"Jimeng AI生成 pill",
"top-left",
d.detected,
d.confidence,
d.region,
engine_detection=d,
)
def _pill_detect_both(image: NDArray[Any]) -> tuple[MarkDetection, MarkDetection]:
# The pill detector is provenance-independent (`_pill_detect` discards the flag), so
# one call answers both levels. MarkDetection is frozen, so sharing it is safe.
d = _pill_detect(image)
return d, d
strict, relaxed = _engine("jimeng_pill").detect_both(image)
def wrap(detection: Any) -> MarkDetection:
return MarkDetection(
"jimeng_pill",
"Jimeng AI生成 pill",
"top-left",
detection.detected,
detection.confidence,
detection.region,
engine_detection=detection,
)
return wrap(strict), wrap(relaxed)
def _pill_mask(
image: NDArray[Any], *, force: bool = False, detection: MarkDetection | None = None
) -> NDArray[Any] | None:
# The pill mask is a fixed top-left geometry box, independent of the detection;
# accepted for the uniform _mask signature.
del detection
return _engine("jimeng_pill").footprint_mask(image, force=force)
engine_detection = detection.engine_detection if detection is not None else None
return _engine("jimeng_pill").footprint_mask(image, force=force, detection=engine_detection)
def _pill_features(image: NDArray[Any]) -> dict[str, float]:
@@ -732,6 +751,24 @@ _REGISTRY: tuple[KnownMark, ...] = (
platform="Tencent Yuanbao (visible 元宝 / AI生成 mark detected)",
manufacturer="tencent",
),
_text_mark(
"dola",
"Dola AI image wordmark",
"bottom-right",
platform="Dola AI (visible Dola AI disclosure)",
manufacturer="bytedance",
label_regime=None,
provenance_signals=(),
),
_text_mark(
"workbuddy",
"AI生成 / WORKBUDDY disclosure",
"bottom-right",
platform="WorkBuddy (visible AI content disclosure)",
manufacturer="tencent",
label_regime=None,
provenance_signals=(),
),
# Samsung Galaxy AI is a device editing marker (samsung_genai), not a TC260 label.
_text_mark(
"samsung",
@@ -940,8 +977,14 @@ def _pill_suppressors() -> set[str]:
return {m.key for m in _REGISTRY if m.manufacturer == pill.manufacturer and m.product != pill.product}
def _keep_pill(keys: set[str], *, provenance: frozenset[str], footprint_flat: bool) -> bool:
"""Whether to auto-remove the capture-less 'AI生成' pill given the fired marks.
def _keep_pill(
keys: set[str],
*,
provenance: frozenset[str],
footprint_flat: bool,
verified_label: bool = False,
) -> bool:
"""Whether to auto-remove the ``AI生成`` pill or verified label.
Pure decision (the flatness feature is precomputed at perception time and passed
in). The pill detector is weak and metadata/intent confirms the platform, not pill
@@ -950,23 +993,24 @@ def _keep_pill(keys: set[str], *, provenance: frozenset[str], footprint_flat: bo
* bottom-right "★ 即梦AI" wordmark fired -> ~94% precise, and it survives
metadata-STRIPPED images: remove the pill unrestricted;
* TC260 metadata confirms Jimeng (``"jimeng" in provenance``, no wordmark) -> remove ONLY when the
top-left footprint is flat enough for an invisible fill (``footprint_flat``),
so real flat-scene pills (and harmless flat false fires) are cleaned while the
damaging textured false fires are left untouched.
solid label template verifies the text, or when the weaker outline's top-left
footprint is flat enough for an invisible fill (``footprint_flat``). Real
flat-scene pills are cleaned while damaging textured false fires are left.
A Doubao image is TC260 too but is not Jimeng-basic, so its mark suppresses the
pill. Marks from other TC260 manufacturers are unrelated and do not participate in
this ByteDance-family decision.
pill. The independently attributed LiblibAI pill also wins over the generic
``AI生成`` label shape. Marks from other TC260 manufacturers are unrelated and do
not participate in this ByteDance-family decision.
No confirmation at all -> never remove (blocks false fires on non-Jimeng content).
The suppressor set is derived from the manufacturer and product fields rather than
from the shared TC260 standard. Marks from Google, Samsung, or another TC260
manufacturer cannot enable or veto this ByteDance-specific arm."""
if _pill_suppressors() & keys:
if "liblib_pill" in keys or _pill_suppressors() & keys:
return False
if "jimeng" in keys:
return True
if "jimeng" in provenance:
return footprint_flat
return footprint_flat or verified_label
return False
@@ -990,6 +1034,8 @@ def _build_candidates(image: NDArray[Any]) -> list[Candidate]:
continue
strict, relaxed = m.detect_both(image)
feats = m.features(image) if (strict.detected or relaxed.detected) else {}
if relaxed.engine_detection is not None and getattr(relaxed.engine_detection, "verified_label", False):
feats["verified_label"] = 1.0
cands.append(
Candidate(
m.key,
@@ -1010,7 +1056,7 @@ def decide(candidates: list[Candidate], context: Context) -> list[Decision]:
All policy lives here, in one place: per-mark trust resolution (:func:`resolve_trust`,
which needs the strict-detected siblings for ``auto`` cross-mark corroboration) and
the capture-less pill gate (:func:`_keep_pill`). No image, no I/O -- so it is unit-testable in isolation and
the Jimeng pill/label gate (:func:`_keep_pill`). No image, no I/O -- so it is unit-testable in isolation and
the same decision drives every caller."""
strict_keys = {c.key for c in candidates if c.detected_strict}
fired: list[Decision] = []
@@ -1026,7 +1072,13 @@ def decide(candidates: list[Candidate], context: Context) -> list[Decision]:
if "jimeng_pill" in keys:
pill = next(d for d in fired if d.candidate.key == "jimeng_pill")
flat = bool(pill.candidate.features.get("footprint_flat", 0.0))
if not _keep_pill(keys, provenance=context.provenance, footprint_flat=flat):
verified_label = bool(pill.candidate.features.get("verified_label", 0.0))
if not _keep_pill(
keys,
provenance=context.provenance,
footprint_flat=flat,
verified_label=verified_label,
):
fired = [d for d in fired if d.candidate.key != "jimeng_pill"]
return fired
@@ -0,0 +1,35 @@
"""Detect the corner AI生成 / WORKBUDDY disclosure without inferring a base model."""
from remove_ai_watermarks._text_mark_engine import TextMarkConfig
from remove_ai_watermarks._text_mark_variants import BottomRightBoxTextMarkEngine
_CONFIG = TextMarkConfig(
name="WorkBuddy AI disclosure",
asset_name="workbuddy_alpha.png",
corner="br",
margin_floor=4,
width_frac=0.22,
height_frac=0.13,
margin_x_frac=0.002,
margin_bottom_frac=0.002,
max_saturation=255,
logo_min_luma=110,
tophat_delta=8,
morph_open_size=3,
detect_min_coverage=0.01,
detect_ncc_threshold=0.60,
alpha_width_frac=0.12,
alpha_height_frac=0.12 * 177 / 351,
detect_frontend="tophat",
scale_basis="width",
ladder=(0.95, 1.0, 1.05),
provenance_ncc_factor=1.0,
min_gw=24,
)
class WorkBuddyEngine(BottomRightBoxTextMarkEngine):
"""Detect and localize the two-line WorkBuddy disclosure."""
def __init__(self) -> None:
super().__init__(_CONFIG)
+36 -1
View File
@@ -22,6 +22,7 @@ covered by this silhouette.
from __future__ import annotations
from dataclasses import replace
from typing import TYPE_CHECKING, Any
from remove_ai_watermarks import _text_mark_engine
@@ -29,6 +30,7 @@ from remove_ai_watermarks._text_mark_engine import (
BottomRightAnchoredEngine,
TextMarkConfig,
)
from remove_ai_watermarks._text_mark_variants import BottomRightBoxTextMarkEngine, TextMarkVariantsMixin
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -79,7 +81,7 @@ def _alpha_template() -> NDArray[Any] | None:
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
class YuanbaoEngine(BottomRightAnchoredEngine):
class YuanbaoEngine(TextMarkVariantsMixin, BottomRightAnchoredEngine):
"""Detect and localize the bottom-right Yuanbao mark."""
_ANCHOR_MAX_RIGHT = 0.04
@@ -87,3 +89,36 @@ class YuanbaoEngine(BottomRightAnchoredEngine):
def __init__(self) -> None:
super().__init__(_CONFIG)
self._variants = (
BottomRightBoxTextMarkEngine(
replace(
_CONFIG,
asset_name="yuanbao_compact_alpha.png",
width_frac=0.28,
height_frac=0.13,
scale_basis="width",
alpha_width_frac=0.09,
alpha_height_frac=0.09 * 238 / 387 * 0.75,
ladder=(0.95, 1.0, 1.05),
detect_ncc_threshold=0.50,
provenance_ncc_factor=0.62,
max_saturation=255,
)
),
BottomRightBoxTextMarkEngine(
replace(
_CONFIG,
asset_name="yuanbao_compact_gray_alpha.png",
width_frac=0.28,
height_frac=0.13,
scale_basis="width",
alpha_width_frac=0.06,
alpha_height_frac=0.06 * 238 / 387 * 0.90,
ladder=(0.95, 1.0, 1.05),
detect_frontend="gray",
detect_ncc_threshold=0.50,
provenance_ncc_factor=0.50,
max_saturation=255,
)
),
)
+26
View File
@@ -0,0 +1,26 @@
"""Clean UI boundaries must not become automatic Kling removal targets."""
import numpy as np
import pytest
from remove_ai_watermarks._text_mark_engine import TextMarkEngine
from remove_ai_watermarks.kling_engine import KlingEngine, _KlingTextEngine
from remove_ai_watermarks.watermark_registry import Context, _build_candidates, decide
def test_horizontal_boundary_is_not_a_kling_wordmark():
image = np.full((2244, 1080, 3), (175, 183, 190), dtype=np.uint8)
image[-40:] = 0
engine = KlingEngine()
strict, relaxed = engine.detect_both(image)
assert not strict.detected
assert not relaxed.detected
assert engine.footprint_mask(image) is None
assert "kling" not in {d.candidate.key for d in decide(_build_candidates(image), Context())}
def test_boundary_fixture_fires_when_glyph_detail_gate_is_removed(monkeypatch: pytest.MonkeyPatch):
image = np.full((2244, 1080, 3), (175, 183, 190), dtype=np.uint8)
image[-40:] = 0
monkeypatch.setattr(_KlingTextEngine, "_ladder_best", TextMarkEngine._ladder_best)
assert KlingEngine().detect(image).detected
+20
View File
@@ -0,0 +1,20 @@
"""Independent vendor fields cannot prove provenance fraud."""
from remove_ai_watermarks.identify import _integrity_clashes
def test_mixed_edit_provenance_preserves_sources_without_alleging_fraud():
warnings = _integrity_clashes({"c2pa": "Google", "xai": "xAI"}, None, camera_has_ai_marker=True)
assert len(warnings) == 1
assert "Google" in warnings[0]
assert "xAI" in warnings[0]
assert "edit" in warnings[0].lower()
assert "spoofed" not in warnings[0]
assert "laundered" not in warnings[0]
def test_camera_and_ai_fields_are_described_as_a_possible_edit_chain():
warnings = _integrity_clashes({"iptc": "Adobe Firefly"}, "Leica", camera_has_ai_marker=True)
assert len(warnings) == 1
assert "captured image followed by an AI edit" in warnings[0]
assert "inconsistent" not in warnings[0]
+106
View File
@@ -0,0 +1,106 @@
"""Synthetic regressions for independently gated text-mark layouts."""
from __future__ import annotations
import cv2
import numpy as np
import pytest
from remove_ai_watermarks import _text_mark_engine
from remove_ai_watermarks.doubao_engine import DoubaoEngine, _OutlinedDoubaoEngine
from remove_ai_watermarks.microsoft_engine import MicrosoftEngine
from remove_ai_watermarks.samsung_engine import SamsungEngine
from remove_ai_watermarks.yuanbao_engine import YuanbaoEngine
def _stamp_variant(engine, variant_index: int, *, width: int = 1280, height: int = 960):
variant = engine._variants[variant_index]
config = variant.config
background = 90 if config.asset_name != "microsoft_text_alpha.png" else 220
foreground = 235 if background < 128 else 35
clean = np.full((height, width, 3), background, np.uint8)
alpha = _text_mark_engine.load_alpha_template(config.asset_name)
assert alpha is not None
base = variant.scale_base(clean)
glyph_width = int(config.alpha_width_frac * base)
glyph_height = int(config.alpha_height_frac * base)
alpha = cv2.resize(alpha, (glyph_width, glyph_height), interpolation=cv2.INTER_LINEAR)
loc = variant.locate(clean)
x = loc.x if config.corner in {"bl", "tl"} else loc.x + loc.w - glyph_width
y = loc.y if config.corner in {"tl", "tr"} else loc.y + loc.h - glyph_height
if config.asset_name == "samsung_ko_alpha.png":
x += round(1.3 * glyph_height)
marked = clean.copy()
if config.asset_name == "microsoft_text_alpha.png":
pad_x, pad_y = int(width * 0.012), int(width * 0.014)
cv2.rectangle(
marked,
(x - pad_x, y - pad_y),
(x + glyph_width + pad_x, y + glyph_height + pad_y),
(242, 238, 232),
-1,
)
region = marked[y : y + glyph_height, x : x + glyph_width].astype(np.float32)
opacity = alpha[:, :, None]
marked[y : y + glyph_height, x : x + glyph_width] = np.clip(
region * (1.0 - opacity) + foreground * opacity, 0, 255
).astype(np.uint8)
return clean, marked, (x, y, glyph_width, glyph_height)
@pytest.mark.parametrize(
("engine", "asset"),
[
(DoubaoEngine(), "doubao_outline_alpha.png"),
(YuanbaoEngine(), "yuanbao_compact_alpha.png"),
(SamsungEngine(), "samsung_ko_alpha.png"),
(MicrosoftEngine(), "microsoft_text_alpha.png"),
],
)
def test_variant_detection_retains_winning_asset(engine, asset: str) -> None:
_clean, marked, _box = _stamp_variant(engine, 0)
detection = engine.detect(marked)
assert detection.detected
assert detection.template_asset == asset
def test_microsoft_text_variant_masks_the_enclosing_pill() -> None:
clean, marked, _box = _stamp_variant(MicrosoftEngine(), 0)
detection = MicrosoftEngine().detect(marked)
mask = MicrosoftEngine().footprint_mask(marked, detection=detection)
assert mask is not None
changed = np.any(clean != marked, axis=2)
assert np.count_nonzero(mask[changed]) / np.count_nonzero(changed) >= 0.98
def test_samsung_korean_variant_masks_the_leading_sparkle_area() -> None:
_clean, marked, (x, y, _width, height) = _stamp_variant(SamsungEngine(), 0)
detection = SamsungEngine().detect(marked)
mask = SamsungEngine().footprint_mask(marked, detection=detection)
assert mask is not None
sparkle_x = x - height // 2
sparkle_y = y + height // 2
assert mask[sparkle_y, sparkle_x] == 255
def test_low_confidence_cjk_layouts_require_matching_provenance() -> None:
doubao = DoubaoEngine()._variants[0].config
yuanbao_contrast, yuanbao_gray = (variant.config for variant in YuanbaoEngine()._variants)
assert (doubao.detect_ncc_threshold, doubao.provenance_ncc_factor) == (0.50, 0.25)
assert (yuanbao_contrast.detect_ncc_threshold, yuanbao_contrast.provenance_ncc_factor) == (0.50, 0.62)
assert (yuanbao_gray.detect_ncc_threshold, yuanbao_gray.provenance_ncc_factor) == (0.50, 0.50)
def test_doubao_provenance_does_not_confirm_a_colored_template_shape(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_clean, marked, _box = _stamp_variant(DoubaoEngine(), 0)
gray = cv2.cvtColor(marked, cv2.COLOR_BGR2GRAY).astype(np.float32)
blend = np.clip((gray - 90) / 145, 0, 1)[:, :, None]
background = np.array([30, 60, 120], np.float32)
foreground = np.array([60, 180, 250], np.float32)
colored_shape = (background * (1 - blend) + foreground * blend).astype(np.uint8)
assert not DoubaoEngine().detect(colored_shape, provenance=True).detected
monkeypatch.setattr(_OutlinedDoubaoEngine, "_outline_pixels_ok", lambda _cls, _patch: True)
assert DoubaoEngine().detect(colored_shape, provenance=True).detected
+2
View File
@@ -24,6 +24,8 @@ _ALTERNATE_SIZE = {
"wan": (1280, 1280),
"kling": (1280, 960),
"yuanbao": (1280, 960),
"dola": (1280, 960),
"workbuddy": (1280, 960),
"samsung": (2304, 1728),
"runninghub": (1280, 960),
"baidu": (1280, 960),
+19 -1
View File
@@ -22,6 +22,8 @@ class TestCatalog:
"wan",
"kling",
"yuanbao",
"dola",
"workbuddy",
"samsung",
"runninghub",
"baidu",
@@ -117,6 +119,8 @@ class TestScan:
"wan",
"kling",
"yuanbao",
"dola",
"workbuddy",
"samsung",
"runninghub",
"baidu",
@@ -722,12 +726,26 @@ class TestPillSuppressors:
m.key for m in reg.known_marks() if m.manufacturer == pill.manufacturer and m.product != pill.product
}
assert reg._pill_suppressors() == expected
assert expected == {"doubao"}
assert expected == {"dola", "doubao"}
def test_other_manufacturers_do_not_suppress_the_pill(self):
assert reg._keep_pill({"qwen"}, provenance=frozenset({"jimeng"}), footprint_flat=1.0)
assert reg._keep_pill({"liblib", "jimeng"}, provenance=frozenset(), footprint_flat=1.0)
def test_verified_label_bypasses_texture_only_with_jimeng_provenance(self):
assert reg._keep_pill(
{"jimeng_pill"},
provenance=frozenset({"jimeng"}),
footprint_flat=False,
verified_label=True,
)
assert not reg._keep_pill(
{"jimeng_pill"},
provenance=frozenset(),
footprint_flat=False,
verified_label=True,
)
def test_pill_survives_gemini_and_samsung(self):
"""Neither is a TC260 labeler, and neither can put "jimeng" into provenance,
so neither may veto the arm it could not have enabled."""