mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-31 17:50:35 +02:00
Align watermark content with detector coverage
This commit is contained in:
@@ -81,29 +81,32 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = (
|
||||
_vendor(
|
||||
b"volcengine",
|
||||
"ByteDance (Volcano Engine)",
|
||||
"ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)",
|
||||
"ByteDance",
|
||||
"ByteDance Volcano Engine",
|
||||
"Volcano Engine",
|
||||
),
|
||||
_vendor(
|
||||
"北京火山引擎科技有限公司",
|
||||
"ByteDance (Volcano Engine)",
|
||||
"ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)",
|
||||
"ByteDance",
|
||||
),
|
||||
_vendor(
|
||||
b"Byteplus", "BytePlus (ByteDance)", "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)", "ByteDance"
|
||||
"ByteDance Volcano Engine",
|
||||
"Volcano Engine",
|
||||
),
|
||||
_vendor(b"Byteplus", "BytePlus (ByteDance)", "BytePlus (ByteDance)", "BytePlus"),
|
||||
_vendor(
|
||||
b"Dreamina",
|
||||
"ByteDance (Dreamina)",
|
||||
"ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)",
|
||||
"ByteDance",
|
||||
"ByteDance Dreamina",
|
||||
"Dreamina",
|
||||
asserts_ai=True,
|
||||
),
|
||||
_vendor(b"Canva", "Canva", "Canva (Magic Media)", "Canva"),
|
||||
_vendor(b"Eleven Labs", "ElevenLabs", "ElevenLabs", "ElevenLabs"),
|
||||
_vendor(b"fal-ai", "fal.ai", "fal.ai", "fal.ai", asserts_ai=True),
|
||||
_vendor(b"Bria", "Bria Artificial Intelligence", "Bria AI", "Bria", asserts_ai=True),
|
||||
# Ideogram signs its downloads' Content Credentials with "Ideogram, Inc"; the
|
||||
# issuer token is the org prefix (same substring-match class as "Bria" in
|
||||
# "Bria Artificial Intelligence"). Found as an unmapped signer on 4 corpus
|
||||
# uploads 2026-08-08 that identify reported as unknown-signer C2PA.
|
||||
_vendor(b"Ideogram", "Ideogram", "Ideogram", "Ideogram", asserts_ai=True),
|
||||
_vendor(b"Truepic", "Truepic", None, None),
|
||||
)
|
||||
|
||||
@@ -116,7 +119,7 @@ C2PA_IDENTITY_AI_ORGS = frozenset(vendor.org for vendor in C2PA_AI_VENDORS if ve
|
||||
C2PA_CLAIM_GENERATOR_PLATFORMS: tuple[tuple[str, str], ...] = (
|
||||
("adobe_firefly", "Adobe Firefly"),
|
||||
("firefly", "Adobe Firefly"),
|
||||
("dreamina", "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)"),
|
||||
("dreamina", "ByteDance Dreamina"),
|
||||
("higgsfield ai", "Higgsfield AI"),
|
||||
("topaz labs image api", "Topaz Labs"),
|
||||
("tiktok ad creative toolbox", "TikTok Ad Creative Toolbox"),
|
||||
@@ -201,7 +204,7 @@ C2PA_ACTIONS = {f"c2pa.{action}".encode(): action for action in _C2PA_ACTION_NAM
|
||||
# what stops a newly registered TC260 vendor from silently falling back to ByteDance.
|
||||
#
|
||||
# What a TC260 label confirms when its producer is absent or unmapped. Historical
|
||||
# behaviour, kept as the fallback so an unrecognized producer never regresses to no
|
||||
# behavior, kept as the fallback so an unrecognized producer never regresses to no
|
||||
# relaxation at all: ByteDance's two products are the ones the relaxed band was
|
||||
# calibrated on (see _text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR).
|
||||
TC260_FALLBACK_VENDORS: frozenset[str] = frozenset({"doubao", "jimeng"})
|
||||
|
||||
@@ -52,8 +52,8 @@ _MIN_DETECT_SHORT_SIDE = 200
|
||||
# This used to be ONE shared 0.7 for every text mark. Measured 2026-07-18 on the
|
||||
# `auto` path (the default -- no flag, driven by TC260 metadata), it turned out to
|
||||
# mean two completely different things per mark. Blind hand-label of the ADDITIONS
|
||||
# (accepted with provenance, rejected without) over a labelled TC260 evaluation set,
|
||||
# two-sided control (labeller sensitivity 100%/96%, specificity 100%/100%):
|
||||
# (accepted with provenance, rejected without) over a labeled TC260 evaluation set,
|
||||
# two-sided control (labeler sensitivity 100%/96%, specificity 100%/100%):
|
||||
#
|
||||
# mark band precision 95% CI n
|
||||
# doubao whole arm 76% 61-87% 42
|
||||
@@ -89,8 +89,8 @@ class TextMarkConfig:
|
||||
name: str # short label for log lines (e.g. "Doubao")
|
||||
asset_name: str # bundled alpha PNG under assets/ (e.g. "doubao_alpha.png")
|
||||
corner: Literal[
|
||||
"br", "bl", "tl", "bc"
|
||||
] # bottom-right (Doubao/Jimeng), bottom-left (Samsung), top-left (RunningHub), bottom-center (LiblibAI)
|
||||
"br", "bl", "tl", "tr", "bc"
|
||||
] # br (Doubao/Jimeng), bl (Samsung), tl (RunningHub), tr (Microsoft), bc (LiblibAI)
|
||||
margin_floor: int # min margin in px for locate (4 for br marks, 2 for Samsung)
|
||||
# locate geometry (fraction of scale_base -- see scale_base())
|
||||
width_frac: float
|
||||
@@ -131,7 +131,7 @@ class TextMarkConfig:
|
||||
template_blur: float = 0.0
|
||||
# Which image dimension the mark's size and margins scale with. VENDOR-SPECIFIC,
|
||||
# measured, not assumed -- see TextMarkEngine.scale_base. "short" = min(h, w), "width" = w.
|
||||
scale_basis: Literal["short", "width"] = "width"
|
||||
scale_basis: Literal["short", "width", "long"] = "width"
|
||||
# Scale rungs ``_ladder_best`` sweeps (the detection comb). PER-MARK: a vendor
|
||||
# whose stamp sizes do not land on the shared 3-rung comb carries its own ladder
|
||||
# (measured for 千问, whose marks sit in two size modes ~1.6x apart -- one fraction
|
||||
@@ -309,7 +309,7 @@ class TextMarkEngine:
|
||||
provenance relaxation it stopped trying because many Jimeng false additions
|
||||
were actually Doubao marks.
|
||||
|
||||
Measured separability on hand-labelled examples, scoring BOTH templates
|
||||
Measured separability on hand-labeled examples, scoring BOTH templates
|
||||
against the same glyph blob:
|
||||
|
||||
feature separability (0.5 = useless, 1.0 = perfect)
|
||||
@@ -480,10 +480,19 @@ class TextMarkEngine:
|
||||
|
||||
China's GB 45438-2025 clause 5.2(e) mandates glyph height >= 5% of "the
|
||||
shortest side" for CN marks, which is why a short-side basis is the natural
|
||||
prior -- but Jimeng's measured behaviour overrides the prior, and measurement
|
||||
prior -- but Jimeng's measured behavior overrides the prior, and measurement
|
||||
wins over the standard's wording.
|
||||
|
||||
"long" (max of the two sides) is the Microsoft badge's measured basis: the
|
||||
pill tracks the RENDER dimension, so on a 1024x1536 portrait it scales with
|
||||
the 1536 (a width basis undersized the template by the aspect ratio and the
|
||||
portrait carriers fell to 0.15-0.32 NCC; measured 2026-08-27).
|
||||
"""
|
||||
return min(image.shape[:2]) if self.config.scale_basis == "short" else image.shape[1]
|
||||
if self.config.scale_basis == "short":
|
||||
return min(image.shape[:2])
|
||||
if self.config.scale_basis == "long":
|
||||
return max(image.shape[:2])
|
||||
return image.shape[1]
|
||||
|
||||
def locate(self, image: NDArray[Any]) -> TextMarkLocation:
|
||||
"""Anchor the watermark box in the configured corner, scaled by ``scale_basis``.
|
||||
@@ -499,14 +508,14 @@ class TextMarkEngine:
|
||||
wm_h = max(16, int(base * c.height_frac))
|
||||
margin_x = max(c.margin_floor, int(base * c.margin_x_frac))
|
||||
margin_b = max(c.margin_floor, int(base * c.margin_bottom_frac))
|
||||
if c.corner == "br":
|
||||
if c.corner == "br" or c.corner == "tr":
|
||||
x = max(0, w - margin_x - wm_w)
|
||||
elif c.corner == "bc": # bottom-center: horizontally centered, margin_x unused
|
||||
x = max(0, (w - wm_w) // 2)
|
||||
else:
|
||||
x = min(margin_x, max(0, w - wm_w))
|
||||
# "tl" anchors at the top instead: margin_bottom_frac is then the TOP margin.
|
||||
y = min(margin_b, max(0, h - wm_h)) if c.corner == "tl" else max(0, h - margin_b - wm_h)
|
||||
# "tl"/"tr" anchor at the top instead: margin_bottom_frac is then the TOP margin.
|
||||
y = min(margin_b, max(0, h - wm_h)) if c.corner in ("tl", "tr") else max(0, h - margin_b - wm_h)
|
||||
wm_w = min(wm_w, w - x)
|
||||
wm_h = min(wm_h, h - y)
|
||||
return TextMarkLocation(x=x, y=y, w=wm_w, h=wm_h)
|
||||
|
||||
@@ -73,7 +73,7 @@ def _tc260_vendors(path: Path) -> frozenset[str]:
|
||||
|
||||
An absent, unreadable or unmapped producer falls back to the historical pair rather
|
||||
than to nothing: the caller has already established that the AIGC signal fired, so
|
||||
the image IS China-AIGC labelled, and dropping to no relaxation would lose the
|
||||
the image IS China-AIGC labeled, and dropping to no relaxation would lose the
|
||||
detections the fallback recovers today. The re-read is deliberately isolated -- a
|
||||
failure here must narrow the answer, never discard the rest of the provenance.
|
||||
"""
|
||||
@@ -156,9 +156,10 @@ def remove_visible(
|
||||
) -> tuple[NDArray[Any], list[str]]:
|
||||
"""Remove every detected known visible AI mark through localize then fill.
|
||||
|
||||
The registry currently covers the Gemini sparkle; Doubao, Jimeng, Qwen, Kling,
|
||||
Yuanbao, Samsung, RunningHub, Baidu, and LiblibAI text marks; and the Jimeng
|
||||
pill. Returns ``(result_bgr, [labels removed])``.
|
||||
The registry currently covers the Gemini visible watermark; Doubao, Jimeng,
|
||||
Qwen, Kling AI, Yuanbao, Samsung, RunningHub, Baidu, and LiblibAI text marks;
|
||||
one Microsoft top-right AI-badge variant; and the Jimeng pill. Returns
|
||||
``(result_bgr, [labels removed])``.
|
||||
|
||||
``source`` is a file path OR a BGR ndarray. For a PATH, metadata provenance is read
|
||||
automatically (so ``sensitivity="auto"`` recovers a moved/faint mark whenever the
|
||||
@@ -184,7 +185,7 @@ def remove_visible(
|
||||
from remove_ai_watermarks import watermark_registry
|
||||
|
||||
# Reject a removed sensitivity loudly; `Sensitivity` is a Literal and not enforced
|
||||
# at runtime, so a 0.15 caller would otherwise get `auto` behaviour in silence.
|
||||
# at runtime, so a 0.15 caller would otherwise get `auto` behavior in silence.
|
||||
watermark_registry.validate_sensitivity(sensitivity)
|
||||
loaded = _load_visible_input(source)
|
||||
result, removed = watermark_registry.remove_auto_marks(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -541,7 +541,7 @@ def _should_skip_invisible_scrub(force: bool, image_path: Path) -> bool:
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging.")
|
||||
@click.pass_context
|
||||
def main(ctx: click.Context, verbose: bool) -> None:
|
||||
"""Remove visible and invisible AI watermarks from images, plus provenance metadata from video."""
|
||||
"""Remove visible and invisible AI watermarks, plus metadata provenance marks, from images and video."""
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv() # Load .env (e.g. HF_TOKEN)
|
||||
|
||||
@@ -97,7 +97,7 @@ class _DecodeMaxDct:
|
||||
return decoded
|
||||
|
||||
def _plane_bits(self, trimmed: NDArray[Any], channel: int, scale: int) -> NDArray[Any]:
|
||||
"""Block bits for one colour plane, a strip of block-rows at a time.
|
||||
"""Block bits for one color plane, a strip of block-rows at a time.
|
||||
|
||||
``dwt2`` is ``dwtn``: it transforms along axis 0, then along axis 1 over
|
||||
both halves, and three of the four bands it returns are discarded here.
|
||||
|
||||
@@ -1105,7 +1105,7 @@ def _collect_visible_signals(
|
||||
sparkle_conf = _visible_sparkle(image_path, image=image)
|
||||
if sparkle_conf is not None and sparkle_conf >= _SPARKLE_THRESHOLD:
|
||||
signals.append(Signal("visible_sparkle", f"NCC confidence {sparkle_conf:.2f}", "medium"))
|
||||
watermarks.append(f"Visible Gemini sparkle (confidence {sparkle_conf:.2f})")
|
||||
watermarks.append(f"Google Gemini visible watermark (sparkle; confidence {sparkle_conf:.2f})")
|
||||
if platform is None:
|
||||
platform = "Google Gemini family (visible sparkle detected)"
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ def detect_invisible_watermark(image_path: Path, *, image: NDArray[Any] | None =
|
||||
|
||||
# ``image`` lets a caller that has already decoded these pixels hand them in
|
||||
# (mirrors gemini_engine.detect_sparkle_confidence). The decoder only reads the
|
||||
# array -- it converts colour spaces into fresh buffers -- so no copy is needed.
|
||||
# array -- it converts color spaces into fresh buffers -- so no copy is needed.
|
||||
img = image if image is not None else image_io.imread(image_path)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Kling (可灵, Kuaishou) visible watermark detector/localizer.
|
||||
"""Kling AI (可灵AI, Kuaishou) visible watermark detector/localizer.
|
||||
|
||||
Kling stamps its generations with a thin, light-gray "可灵AI 3.0" text strip in the
|
||||
Kling AI stamps its generations with a thin, light-gray "可灵AI 3.0" text strip in the
|
||||
bottom-right corner, preceded by the vendor's spiral logo (not part of the detection
|
||||
silhouette -- logos vary between releases, the text run is what discriminates).
|
||||
Known variants: an "Omni" suffix release, a latin "KlingAI 3.0" release, and a
|
||||
@@ -9,7 +9,7 @@ suffix variants are only caught when the core run is bold enough (measured below
|
||||
|
||||
Detection matches the bundled glyph silhouette against the corner; removal is the
|
||||
shared **localize -> fill** (the glyph-bbox :meth:`footprint_mask` feeds
|
||||
``region_eraser``), NOT reverse-alpha. This module supplies only Kling's tuned
|
||||
``region_eraser``), NOT reverse-alpha. This module supplies only Kling AI's tuned
|
||||
:class:`TextMarkConfig` (``assets/kling_alpha.png`` -- a font-rendered synthetic
|
||||
silhouette from ``scripts/render_vendor_silhouettes.py``, never cut from an
|
||||
upload). It also feeds ``identify`` as the medium-confidence ``visible_kling``
|
||||
@@ -26,7 +26,7 @@ producer USCC 91110108335469089C names the entity, 2026-07-21; harness
|
||||
* ``alpha_height_frac`` comes from the silhouette aspect (0.239) at the fitted
|
||||
width, matching the aspect the fit converged on (0.25).
|
||||
* Gate 0.35, one step above the clean arm's max: on the cohort-vs-clean run
|
||||
(cohort-contamination-guarded, 286 hand-labelled clean frames) the clean arm
|
||||
(cohort-contamination-guarded, 286 hand-labeled clean frames) the clean arm
|
||||
scored p99 0.304 / max 0.320, and every cohort frame >= 0.35 carries a visible
|
||||
可灵AI 3.0 mark (9 of ~19 eyeballed visible marks fire = ~47% recall of visible
|
||||
marks; the misses are the faint "Omni"-suffix release, the latin "KlingAI"
|
||||
@@ -38,7 +38,7 @@ producer USCC 91110108335469089C names the entity, 2026-07-21; harness
|
||||
provenance relaxation exists for this mark.
|
||||
* No rival margin: at the shipped gate the template fires on 1 of 400
|
||||
Doubao-marked frames (0.2%, a 豆包 frame sitting INSIDE the Kling cohort, still
|
||||
below the gate), 0 of 298 Jimeng-marked frames and 0 of 286 hand-labelled clean
|
||||
below the gate), 0 of 298 Jimeng-marked frames and 0 of 286 hand-labeled clean
|
||||
frames, and a 0.10 rival margin costs zero genuine Kling detections -- so it is
|
||||
simply unnecessary (same conclusion shape as Qwen).
|
||||
"""
|
||||
@@ -74,7 +74,7 @@ LOGO_MIN_LUMA = 150
|
||||
TOPHAT_DELTA = 12
|
||||
|
||||
DETECT_MIN_COVERAGE = 0.04 # unused by the tophat front-end (kept for config parity)
|
||||
# Calibrated 2026-07-21 on the vendor cohort vs 286 hand-labelled clean frames
|
||||
# Calibrated 2026-07-21 on the vendor cohort vs 286 hand-labeled clean frames
|
||||
# (cohort-contamination-guarded): clean p99 0.304 / max 0.320, and every cohort
|
||||
# frame scoring >= 0.35 carries a visible 可灵AI 3.0 mark. 0.35 was picked over
|
||||
# 0.33 (also zero clean fires) for margin against unseen clean content at a cost
|
||||
@@ -87,7 +87,7 @@ _ALPHA_WIDTH_FRAC = 0.12
|
||||
_ALPHA_HEIGHT_FRAC = 0.0287
|
||||
|
||||
_CONFIG = TextMarkConfig(
|
||||
name="Kling",
|
||||
name="Kling AI",
|
||||
asset_name="kling_alpha.png",
|
||||
corner="br",
|
||||
margin_floor=4,
|
||||
@@ -114,7 +114,7 @@ _CONFIG = TextMarkConfig(
|
||||
|
||||
|
||||
def _alpha_template() -> NDArray[Any] | None:
|
||||
"""The bundled Kling alpha template (float [0,1]), or None."""
|
||||
"""The bundled Kling AI alpha template (float [0,1]), or None."""
|
||||
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def _glyph_silhouette() -> NDArray[Any] | None:
|
||||
|
||||
|
||||
class KlingEngine(TextMarkEngine):
|
||||
"""Detect/localize the visible Kling "可灵AI 3.0" watermark (locate -> mask; mask feeds the fill)."""
|
||||
"""Detect/localize the visible Kling AI "可灵AI 3.0" watermark (locate -> mask; mask feeds the fill)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(_CONFIG)
|
||||
|
||||
@@ -413,7 +413,7 @@ def _scan_head_impl(image_path: Path, size: int) -> bytes:
|
||||
# packet larger than this is not a provenance label.
|
||||
_DECODED_TEXT_LIMIT = 512 * 1024
|
||||
# Decoder values that are binary payloads with their own readers, not metadata text.
|
||||
# An ICC profile is colour data and can run to hundreds of kilobytes; appending it
|
||||
# An ICC profile is color data and can run to hundreds of kilobytes; appending it
|
||||
# would bloat the buffer every later detector re-scans, for no signal.
|
||||
_DECODER_BINARY_KEYS = frozenset({"icc_profile"})
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Microsoft top-right AI-badge detector/localizer.
|
||||
|
||||
This engine covers one measured Microsoft output variant: a white pill with dark
|
||||
internal shapes in the top-right corner. The evaluated files used both "Made with
|
||||
AI" and "AI-Generated" wording. This is narrower than Microsoft's documented
|
||||
watermark feature, which can use a Copilot icon or text and can place the mark in
|
||||
other positions. A Microsoft provenance signal therefore does not establish that
|
||||
this exact visible variant is present.
|
||||
|
||||
Detection matches a synthetic pill silhouette (white pill with the sparkle and
|
||||
text KNOCKED OUT) against the top-hat blob of the located box: the holes are what
|
||||
discriminate this pill from any other bright rounded element in the corner.
|
||||
Removal is the shared **localize -> fill**; the glyph-bbox :meth:`footprint_mask`
|
||||
covers the whole pill including its text.
|
||||
|
||||
The tuned numbers below were remeasured on 2026-08-27 with the registered engine
|
||||
and ``scripts/registered_mark_calibrate.py``. The arms were kept distinct: 17
|
||||
visually confirmed carriers, 343 Microsoft-provenance files whose visible-mark
|
||||
status was not adjudicated, and 1200 non-overlapping no-signal controls:
|
||||
|
||||
* Geometry is single-mode and tight: pill 0.152 x 0.040 of the LONG side
|
||||
(aspect 3.73-3.89 over 720..1536 px), margins ~0.010/0.007 of the same basis. One size
|
||||
mode, so the shared 3-rung ladder is untouched and the locate box simply
|
||||
wraps the pill with NCC slack.
|
||||
* STRICT ONLY (``provenance_ncc_factor`` 1.0): controls max 0.293 / p99
|
||||
0.200; confirmed carriers p50 0.519 / p90 0.578 / max 0.579, with 15/17
|
||||
above the 0.38 gate. Provenance alone does not relax the gate: only 78/343
|
||||
files in that cohort crossed it, and the rest were not visually labeled.
|
||||
* Front-end "binary": the pill is a bold opaque overlay; the tophat blob is
|
||||
solid with dark-text holes, exactly the template's shape.
|
||||
"""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from remove_ai_watermarks import _text_mark_engine
|
||||
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkEngine
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy.typing import NDArray
|
||||
|
||||
# Locate geometry as a fraction of the image LONG side (measured; scale_basis="long":
|
||||
# on 1024x1536 portraits the pill tracks the 1536, and a width basis undersized
|
||||
# the template until the portrait carriers fell to 0.15-0.32 NCC).
|
||||
# The box wraps the measured pill rect (0.152W x 0.040W) with NCC slack; margins
|
||||
# sit inside the pill's own ~0.010W-right / ~0.007W-top insets.
|
||||
WM_WIDTH_FRAC = 0.170
|
||||
WM_HEIGHT_FRAC = 0.055
|
||||
MARGIN_RIGHT_FRAC = 0.004
|
||||
MARGIN_TOP_FRAC = 0.003
|
||||
|
||||
# Glyph appearance: a bright near-white pill (luma ~245), gray-scale (sat < 60).
|
||||
MAX_SATURATION = 60
|
||||
LOGO_MIN_LUMA = 170
|
||||
TOPHAT_DELTA = 10
|
||||
|
||||
# Calibrated 2026-08-27: non-overlapping no-signal controls (n=1200) max 0.293 /
|
||||
# p99 0.200; visually confirmed carriers (n=17) p50 0.519 / p90 0.578 / max
|
||||
# 0.579, with 15/17 above the 0.38 gate. The two misses score 0.249 and 0.315.
|
||||
DETECT_MIN_COVERAGE = 0.30 # the pill fills most of its box; content corners do not
|
||||
DETECT_NCC_THRESHOLD = 0.38
|
||||
|
||||
# Pill silhouette geometry (fraction of width): 0.152W x 0.040W, aspect ~3.78.
|
||||
_ALPHA_NATIVE_WIDTH = 335
|
||||
_ALPHA_WIDTH_FRAC = 0.152
|
||||
_ALPHA_HEIGHT_FRAC = 0.040
|
||||
|
||||
_CONFIG = TextMarkConfig(
|
||||
name="Microsoft top-right AI badge",
|
||||
asset_name="microsoft_alpha.png",
|
||||
corner="tr",
|
||||
margin_floor=2,
|
||||
width_frac=WM_WIDTH_FRAC,
|
||||
height_frac=WM_HEIGHT_FRAC,
|
||||
margin_x_frac=MARGIN_RIGHT_FRAC,
|
||||
margin_bottom_frac=MARGIN_TOP_FRAC,
|
||||
max_saturation=MAX_SATURATION,
|
||||
logo_min_luma=LOGO_MIN_LUMA,
|
||||
tophat_delta=TOPHAT_DELTA,
|
||||
morph_open_size=5,
|
||||
detect_min_coverage=DETECT_MIN_COVERAGE,
|
||||
detect_ncc_threshold=DETECT_NCC_THRESHOLD,
|
||||
alpha_width_frac=_ALPHA_WIDTH_FRAC,
|
||||
alpha_height_frac=_ALPHA_HEIGHT_FRAC,
|
||||
min_gw=24,
|
||||
detect_frontend="binary",
|
||||
scale_basis="long",
|
||||
provenance_ncc_factor=1.0,
|
||||
)
|
||||
|
||||
|
||||
def _alpha_template() -> NDArray[Any] | None:
|
||||
"""The bundled Microsoft pill template (float [0,1]), or None."""
|
||||
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
|
||||
|
||||
|
||||
class MicrosoftEngine(TextMarkEngine):
|
||||
"""Detect/localize the measured Microsoft top-right AI badge."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(_CONFIG)
|
||||
@@ -64,7 +64,7 @@ _MASK_W, _MASK_H = 0.205, 0.115 # width of W, height of W
|
||||
#
|
||||
# Measured through the PRODUCT path (the `_keep_pill` gate), not the raw detector, by
|
||||
# ``scripts/pill_gate_audit.py`` -- the raw path bypasses the gate and reads as a
|
||||
# disaster that the shipped behaviour does not have. Re-run it when the gate changes.
|
||||
# disaster that the shipped behavior does not have. Re-run it when the gate changes.
|
||||
_FLAT_TEXTURE_MAX = 6.0
|
||||
|
||||
_silhouette: NDArray[Any] | None = None
|
||||
|
||||
@@ -23,7 +23,7 @@ whose producer USCC 91440101MA9Y9T4H7A names the entity, 2026-07-21; harness
|
||||
ratio 1.64 -- wider than the shared 3-rung ladder's 1.5625 span), so a single
|
||||
fraction on the shared ladder covers ~75% of marks and the rest land in the
|
||||
comb's collapse zone. Qwen therefore carries its OWN 2-rung ladder
|
||||
(``TextMarkConfig.ladder``), one rung centred on each mode; the shared
|
||||
(``TextMarkConfig.ladder``), one rung centered on each mode; the shared
|
||||
default is untouched for every other mark.
|
||||
* The mark also sits FARTHER off the corner than Doubao's box assumes (right
|
||||
margin ~0.025 vs 0.004 of the short side), so Doubao's locate box clipped the
|
||||
@@ -38,7 +38,7 @@ whose producer USCC 91440101MA9Y9T4H7A names the entity, 2026-07-21; harness
|
||||
arm would be mostly false fills. No provenance relaxation exists for this
|
||||
mark.
|
||||
* No rival margin: at the shipped gate the template fires on 0 of 400
|
||||
Doubao-marked frames, 0 of 298 Jimeng-marked frames and 0 of 286 hand-labelled
|
||||
Doubao-marked frames, 0 of 298 Jimeng-marked frames and 0 of 286 hand-labeled
|
||||
clean frames (the shared tail correlates at ~0.22, far below the gate), while
|
||||
a 0.10 rival margin would have suppressed ~10% of genuine Qwen detections.
|
||||
"""
|
||||
@@ -74,7 +74,7 @@ LOGO_MIN_LUMA = 150
|
||||
TOPHAT_DELTA = 12
|
||||
|
||||
DETECT_MIN_COVERAGE = 0.04 # unused by the tophat front-end (kept for config parity)
|
||||
# Calibrated 2026-07-21 on the vendor cohort vs 286 hand-labelled clean frames
|
||||
# Calibrated 2026-07-21 on the vendor cohort vs 286 hand-labeled clean frames
|
||||
# (cohort-contamination-guarded): clean p99 0.301 / max 0.316, and every cohort
|
||||
# frame scoring >= 0.45 carries a visible 千问AI生成 mark (86% of the eyeballed
|
||||
# visible marks fire, the misses being white-on-near-white contrast losses).
|
||||
|
||||
@@ -32,7 +32,7 @@ whose producer USCC names the entity, harvested 2026-07-22 by
|
||||
* STRICT ONLY (``provenance_ncc_factor`` 1.0): raw gray NCC is
|
||||
contrast-DEPENDENT and the sub-gate band of a corner-anchored gray match is
|
||||
unmeasured beyond the clean arm, so no provenance relaxation exists.
|
||||
* Gate 0.34: on 283 hand-labelled clean frames (cohort-contamination-guarded)
|
||||
* Gate 0.34: on 283 hand-labeled clean frames (cohort-contamination-guarded)
|
||||
corner-anchored gray NCC p99 is 0.264 / max 0.304, while the 4 positives
|
||||
score 0.38-0.54. 0.34 sits above the clean max with a small margin; the
|
||||
positives are few, so the margin is deliberately thin on the recall side.
|
||||
@@ -75,14 +75,14 @@ LOGO_MIN_LUMA = 150
|
||||
TOPHAT_DELTA = 12
|
||||
|
||||
DETECT_MIN_COVERAGE = 0.04 # unused by the gray front-end (kept for config parity)
|
||||
# Calibrated 2026-07-22 on the vendor cohort vs 283 hand-labelled clean frames:
|
||||
# Calibrated 2026-07-22 on the vendor cohort vs 283 hand-labeled clean frames:
|
||||
# corner-anchored gray NCC, clean p99 0.264 / max 0.304; positives 0.38-0.54.
|
||||
DETECT_NCC_THRESHOLD = 0.34
|
||||
|
||||
# Detection-silhouette geometry (fraction of the image width), measured on the
|
||||
# positives: mark width is ~0.320 of width on all three frame sizes (266px at 832,
|
||||
# 345px at 1080, 491px at 1536), and the NCC is razor-sharp in size (0.537 on-size,
|
||||
# 0.223 at +5.6% -- the same comb behaviour Qwen measured), so the nominal sits
|
||||
# 0.223 at +5.6% -- the same comb behavior Qwen measured), so the nominal sits
|
||||
# exactly on the measured size with a TIGHT ladder around it, not the shared 3 rungs
|
||||
# (whose nearest rung landed 5.6% off and collapsed the match to 0.22).
|
||||
_ALPHA_WIDTH_FRAC = 0.32
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""High-level video processing API.
|
||||
|
||||
The product path covers provenance identification, container-level AI metadata
|
||||
removal, temporally stabilized visible Sora, Veo, Seedance, Dola, Hailuo, and
|
||||
Kling removal, and an oracle-certified opt-in VAE profile for video SynthID.
|
||||
removal, temporally stabilized visible Sora, Veo, Seedance, Dola, Hailuo AI, and
|
||||
Kling AI removal, and an oracle-certified opt-in VAE profile for video SynthID.
|
||||
The visible pixel path reuses the image package's shared fill backends.
|
||||
"""
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Supported marks use fully synthetic silhouettes made from geometric primitives,
|
||||
OpenCV's built-in font, and Pillow's bundled font. Sora detection searches the
|
||||
full frame because the wordmark moves. Veo detection covers both the current
|
||||
four-point diamond and legacy ``Veo`` text. Seedance detects the boxed ``AI``
|
||||
label, Dola detects its compact text label, Hailuo detects the composite
|
||||
MINIMAX/Hailuo label, and Kling detects its version-independent wordmark core.
|
||||
label, Dola detects its compact text label, Hailuo AI detects the composite
|
||||
MINIMAX/Hailuo AI label, and Kling AI detects its version-independent wordmark core.
|
||||
A single frame is never enough to authorize removal: the temporal arbiter
|
||||
requires the candidate to recur at the same location across adjacent frames.
|
||||
This keeps isolated lookalikes in clean videos from becoming removal masks.
|
||||
|
||||
@@ -20,7 +20,7 @@ Entries:
|
||||
- ``gemini`` -- Google Gemini / Nano Banana visible watermark (sparkle), bottom-right.
|
||||
- ``doubao`` -- ByteDance Doubao "豆包AI生成" text strip, bottom-right.
|
||||
- ``jimeng`` -- ByteDance Jimeng / Dreamina "★ 即梦AI" wordmark, bottom-right.
|
||||
- ``qwen`` -- Alibaba Qwen "千问AI生成" text strip, bottom-right.
|
||||
- ``qwen`` -- Alibaba Cloud Qwen "千问AI生成" text strip, bottom-right.
|
||||
- ``kling`` -- Kuaishou Kling AI "可灵AI 3.0" text strip, bottom-right.
|
||||
- ``yuanbao`` -- Tencent Yuanbao "元宝 / AI生成" two-line mark, bottom-right.
|
||||
- ``samsung`` -- Samsung Galaxy AI "Contenuti generati dall'AI" strip, bottom-left.
|
||||
@@ -28,6 +28,7 @@ Entries:
|
||||
- ``runninghub`` -- RunningHub "RunningHub AI生成" text, top-left (gray front-end).
|
||||
- ``baidu`` -- Baidu "百度 AI生成" text + white tag, bottom-right.
|
||||
- ``liblib`` -- LiblibAI "LiblibAI" wordmark, bottom-center.
|
||||
- ``microsoft`` -- one measured Microsoft white AI-badge variant, top-right.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -147,7 +148,7 @@ _REMOVED_SENSITIVITIES = {
|
||||
"vendor made it or where the mark is. If you can see a mark the detector missed, "
|
||||
"act on what you see: erase(image, region=(x, y, w, h)), or the CLI "
|
||||
"`--mark <name> --no-detect` for a known text mark. Use sensitivity='auto' for "
|
||||
"the default evidence-driven behaviour."
|
||||
"the default evidence-driven behavior."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +157,7 @@ def validate_sensitivity(value: str) -> Sensitivity:
|
||||
"""Reject a removed sensitivity LOUDLY instead of silently falling back to ``auto``.
|
||||
|
||||
``Sensitivity`` is a ``Literal``, which is not enforced at runtime, so a caller
|
||||
upgrading from 0.15 would pass ``"assume_ai"`` and quietly get ``auto`` behaviour --
|
||||
upgrading from 0.15 would pass ``"assume_ai"`` and quietly get ``auto`` behavior --
|
||||
a silent semantic change on the one release where they most need to be told.
|
||||
"""
|
||||
if value in _REMOVED_SENSITIVITIES:
|
||||
@@ -368,7 +369,7 @@ _GEMINI_AUTO_MIN_CONF = GEMINI_SPARKLE_TRUST_CONF
|
||||
# that never had a mark rather than on moved ones.
|
||||
#
|
||||
# Measured blind on 954 unique Google-metadata uploads (detector never saw the
|
||||
# metadata), hand-labelled against a two-sided control (labeller sensitivity ~88%,
|
||||
# metadata), hand-labeled against a two-sided control (labeler sensitivity ~88%,
|
||||
# specificity 100%). "Additions" = accepted with provenance but not without:
|
||||
#
|
||||
# band precision 95% CI population
|
||||
@@ -409,6 +410,7 @@ _ENGINE_CLASS: dict[str, tuple[str, str]] = {
|
||||
"runninghub": ("runninghub_engine", "RunningHubEngine"),
|
||||
"baidu": ("baidu_engine", "BaiduEngine"),
|
||||
"liblib": ("liblib_engine", "LibLibEngine"),
|
||||
"microsoft": ("microsoft_engine", "MicrosoftEngine"),
|
||||
}
|
||||
|
||||
|
||||
@@ -485,7 +487,14 @@ def fill(image: NDArray[Any], mask: NDArray[Any], *, backend: Backend = "auto")
|
||||
def _gemini_wrap(d: Any, *, provenance: bool) -> MarkDetection:
|
||||
gate = _GEMINI_PROVENANCE_MIN_CONF if provenance else _GEMINI_AUTO_MIN_CONF
|
||||
detected = bool(d.detected) and d.confidence >= gate
|
||||
return MarkDetection("gemini", "Google Gemini sparkle", "bottom-right", detected, d.confidence, d.region)
|
||||
return MarkDetection(
|
||||
"gemini",
|
||||
"Google Gemini visible watermark (sparkle)",
|
||||
"bottom-right",
|
||||
detected,
|
||||
d.confidence,
|
||||
d.region,
|
||||
)
|
||||
|
||||
|
||||
def _gemini_detect(image: NDArray[Any], *, provenance: bool = False) -> MarkDetection:
|
||||
@@ -561,12 +570,14 @@ def _text_mark(
|
||||
label_regime: str | None = "tc260",
|
||||
provenance_signals: tuple[str, ...] = ("aigc",),
|
||||
tc260_producer_codes: tuple[str, ...] = (),
|
||||
provenance_platform_tokens: tuple[str, ...] = (),
|
||||
) -> KnownMark:
|
||||
"""Build a text-mark registry row from its shared detector and mask adapters.
|
||||
|
||||
``product`` defaults to the key (one mark, one product); pass it only when two
|
||||
marks share a product. ``label_regime`` and ``provenance_signals`` default to the
|
||||
China-AIGC label because every text mark registered so far except Samsung uses it.
|
||||
China-AIGC label because every text mark registered so far except Samsung and
|
||||
Microsoft uses it.
|
||||
"""
|
||||
return KnownMark(
|
||||
key,
|
||||
@@ -580,6 +591,7 @@ def _text_mark(
|
||||
_text_mark_mask(key),
|
||||
provenance_signals=provenance_signals,
|
||||
tc260_producer_codes=tc260_producer_codes,
|
||||
provenance_platform_tokens=provenance_platform_tokens,
|
||||
_detect_both=_text_mark_detect_both(key, label, location),
|
||||
)
|
||||
|
||||
@@ -616,7 +628,7 @@ def _pill_features(image: NDArray[Any]) -> dict[str, float]:
|
||||
|
||||
|
||||
_REGISTRY: tuple[KnownMark, ...] = (
|
||||
# Gemini is a Google C2PA/SynthID product, not a China-AIGC labeller: label_regime
|
||||
# Gemini is a Google C2PA/SynthID product, not a China-AIGC labeler: label_regime
|
||||
# is None so it can never act as a TC260 sibling in _keep_pill.
|
||||
KnownMark(
|
||||
"gemini",
|
||||
@@ -651,7 +663,7 @@ _REGISTRY: tuple[KnownMark, ...] = (
|
||||
"qwen",
|
||||
"Qwen 千问AI生成 text",
|
||||
"bottom-right",
|
||||
platform="Alibaba Qwen (visible 千问AI生成 mark detected)",
|
||||
platform="Alibaba Cloud Qwen (visible 千问AI生成 mark detected)",
|
||||
tc260_producer_codes=("91440101MA9Y9T4H7A",),
|
||||
),
|
||||
_text_mark(
|
||||
@@ -698,6 +710,18 @@ _REGISTRY: tuple[KnownMark, ...] = (
|
||||
platform="LiblibAI (visible LiblibAI mark detected)",
|
||||
tc260_producer_codes=("91110105MACJ6K1C8A",),
|
||||
),
|
||||
# One measured Microsoft visible-mark variant: a white top-right pill with
|
||||
# dark internal shapes. Microsoft's documented feature also permits other
|
||||
# icon, text, and placement variants, which this detector does not cover.
|
||||
_text_mark(
|
||||
"microsoft",
|
||||
"Microsoft top-right AI badge",
|
||||
"top-right",
|
||||
label_regime=None,
|
||||
provenance_signals=(),
|
||||
platform="Microsoft (visible top-right AI badge detected)",
|
||||
provenance_platform_tokens=("microsoft",),
|
||||
),
|
||||
# Same product as the Jimeng wordmark -- the one pair that cross-relaxes.
|
||||
KnownMark(
|
||||
"jimeng_pill",
|
||||
|
||||
Reference in New Issue
Block a user