Align watermark content with detector coverage

This commit is contained in:
Victor Kuznetsov
2026-08-27 20:05:03 -07:00
parent 17408b958e
commit a8d4bce14a
61 changed files with 933 additions and 198 deletions
+14 -11
View File
@@ -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"})
+20 -11
View File
@@ -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)
+6 -5
View File
@@ -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

+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+9 -9
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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.
"""
+2 -2
View File
@@ -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.
+32 -8
View File
@@ -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",