Merge remote-tracking branch 'origin/main' into docs/arxiv-paper-review

# Conflicts:
#	docs/installation.md
#	docs/supported-signals.md
#	docs/verification-plan.md
This commit is contained in:
Victor Kuznetsov
2026-08-28 11:18:08 -07:00
122 changed files with 2634 additions and 456 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
__version__ = "0.31.1"
__version__ = "0.34.1"
__all__ = [
"BatchSummary",
+22 -5
View File
@@ -426,6 +426,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
source_types: list[str] = []
soft_binding_algorithms: list[str] = []
soft_binding_values: list[str] = []
# Raw signer/generator identity strings, used to scope SynthID evidence to
# the vendor that actually asserted the manifest. A vendor token appearing
# anywhere else in the chain (e.g. Microsoft Designer's "Azure OpenAI
# ImageGen" softwareAgent) is a service name, not that vendor's provenance.
identity_strings: list[str] = []
claim_generator_asserts_ai = False
def add_tool_matches(value: str, *, asserts_ai: bool = False) -> None:
@@ -443,9 +448,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
value = signature.get(key)
if isinstance(value, str):
issuers.extend(_ordered_matches(value.encode(), C2PA_ISSUERS))
identity_strings.append(value)
direct_generator = manifest.get("claim_generator")
if isinstance(direct_generator, str):
identity_strings.append(direct_generator)
add_tool_matches(direct_generator, asserts_ai=True)
candidates = manifest.get("claim_generator_info")
@@ -456,6 +463,7 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
name = cast("dict[object, object]", candidate_value).get("name")
if isinstance(name, str):
add_tool_matches(name, asserts_ai=True)
identity_strings.append(name)
assertions = manifest.get("assertions")
if not isinstance(assertions, list):
@@ -528,9 +536,14 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
has_watermark_action = any(action.startswith("watermarked") for action in actions)
if has_watermark_action:
info["watermarked"] = True
if info.get("ai_source_kind"):
selected_bytes = json.dumps(chain, ensure_ascii=False).encode()
synthid = synthid_evidence_vendors_in(selected_bytes, has_watermark_action=has_watermark_action)
if info.get("ai_source_kind") and not soft_binding_algorithms:
# Evidence scope: only the signer/generator identity strings above, never
# the whole chain - a vendor named inside another vendor's manifest (the
# Designer case) must not turn into that vendor's SynthID provenance. A
# manifest that names its own forensic soft-binding algorithm carries
# that vendor's mark and is excluded from the generic inference entirely.
identity_bytes = json.dumps(identity_strings, ensure_ascii=False).encode()
synthid = synthid_evidence_vendors_in(identity_bytes, has_watermark_action=has_watermark_action)
if synthid:
info["synthid_vendors"] = synthid
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
@@ -611,12 +624,16 @@ def _populate_registry_fields(buffer: bytes, info: dict[str, Any]) -> bool:
if b"c2pa.watermarked" in buffer:
info["watermarked"] = True
synthid = synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
soft_bindings = soft_binding_vendors_in(buffer)
synthid = (
[]
if soft_bindings
else synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
)
if ai_source and synthid:
info["synthid_vendors"] = synthid
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
soft_bindings = soft_binding_vendors_in(buffer)
if soft_bindings:
info["soft_binding_vendors"] = soft_bindings
info["soft_binding"] = ", ".join(soft_bindings)
+23 -18
View File
@@ -70,7 +70,7 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = (
_vendor(
b"OpenAI",
"OpenAI",
"OpenAI (ChatGPT / gpt-image / DALL-E / Sora)",
"OpenAI (ChatGPT / GPT Image / DALL·E / Sora)",
"OpenAI",
synthid=True,
synthid_requires_watermark_action=True,
@@ -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"),
@@ -128,8 +131,8 @@ C2PA_AI_TOOLS = {
("GPT-4o", "GPT-4o"),
("ChatGPT", "ChatGPT"),
("Sora", "Sora"),
("DALL-E", "DALL-E"),
("DALL", "DALL-E"),
("DALL-E", "DALL·E"),
("DALL", "DALL·E"),
("Imagen", "Imagen"),
("Firefly", "Firefly"),
("Dreamina", "Dreamina"),
@@ -138,16 +141,18 @@ C2PA_AI_TOOLS = {
C2PA_SOFT_BINDINGS = {
b"com.adobe.trustmark": "Adobe TrustMark",
b"com.adobe.icn": "Adobe (content fingerprint)",
b"com.digimarc": "Digimarc",
b"com.adobe.icn": "Adobe Image Comparator Network",
b"com.digimarc": "Digimarc Validate",
b"com.imatag.lamark": "Imatag (Lamark)",
b"ai.steg": "Steg.AI",
b"com.microsoft.invismark": "Microsoft InvisMark",
b"com.microsoft.wavmark": "Microsoft WavMark",
b"com.verimatrix": "Verimatrix",
b"com.nagra.nexguard": "NAGRA NexGuard",
b"com.aiwatermark": "AIWatermark (Meta PixelSeal)",
b"ai.trufo": "Trufo",
b"com.aiwatermark.pixelseal": "AIWatermark PixelSeal",
b"com.aiwatermark.videoseal": "AIWatermark VideoSeal",
b"com.aiwatermark.audioseal": "AIWatermark AudioSeal",
b"ai.trufo": "Trufo PawPrint",
b"app.overlai": "Overlai",
b"com.markany": "MarkAny",
b"com.mentaport": "Mentaport",
@@ -199,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"})
@@ -1074,29 +1074,14 @@ class QwenZImagePipeline:
fidelity_anchor: bool = False,
) -> Image.Image:
"""Execute global regeneration and masked face repair."""
if text_manifest is not None and tile:
raise ValueError("Verified text restoration is not calibrated with tiled diffusion")
self._require_cuda()
seed = resolve_seed(seed)
donor = None
if text_manifest is not None:
self._progress("Reconstructing the verified text donor with the Qwen VAE...")
if tile and max(image.size) > tile_size:
from remove_ai_watermarks._internal.tiling import run_tiled
donor = run_tiled(
self._qwen_vae_roundtrip,
image,
tile_size,
tile_overlap,
lambda message: self._progress(
message.replace(
"Tiled diffusion",
"Reconstructing the verified text donor",
1,
)
),
)
else:
donor = self._qwen_vae_roundtrip(image)
donor = self._qwen_vae_roundtrip(image)
global_strength = (
resolution_adaptive_denoise(image.width, image.height) if strength is None else float(strength)
)
@@ -79,17 +79,34 @@ QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.27
# cross-source spread (0.00725) to the worst clean boundary: 0.0695 + 0.00725.
QWEN_ZIMAGE_OPENAI_STRENGTH = 0.07675
# Microsoft's public detector returned Inconclusive rather than an API-level
# watermark-negative verdict. Three valid Paint sources first cleared at 0.04125,
# Microsoft's public detector (https://ai.azure.com/nextgen/validate) returned
# Inconclusive rather than an API-level watermark-negative verdict. Three valid
# Paint sources first cleared at 0.04125,
# 0.055, and 0.095. Add one full observed cross-source spread to the worst clean
# boundary: 0.095 + (0.095 - 0.04125) = 0.14875, rounded up to 0.15. This is a
# measured corpus margin, not a universal InvisMark threshold.
QWEN_ZIMAGE_MICROSOFT_STRENGTH = 0.15
# Meta Muse Image stamps every output with Content Seal, but no provenance signal
# survives to route it: the outputs carry no C2PA, and their IPTC
# trainedAlgorithmicMedia companion tag is a standard code many platforms use, so
# it cannot key this cohort the way an issuer keys the others. The floor is
# therefore selected by an explicit --vendor meta override, never by detection.
# Derivation (oracle meta.ai/identification, 2026-08-26/27, corpus in
# data/contentseal/): five independent 2.56 MP generations bracketed at
# lighthouse (0.0525, 0.06], fox (0.03, 0.0375], night_city (0.03, 0.0375],
# mug <= 0.03, text <= 0.015. Worst clean boundary plus one full observed
# cross-source spread: 0.06 + (0.0525 - 0.015) = 0.0975, rounded up to 0.1.
# sdxl-zimage has no measured Meta floor; its vendor map stays without a meta
# entry so an explicit --vendor meta there falls to the unknown 0.25, which is
# above this floor and therefore conservative.
QWEN_ZIMAGE_META_STRENGTH = 0.1
_QWEN_ZIMAGE_FLAT_STRENGTH_BY_VENDOR: dict[str, float] = {
"google": QWEN_ZIMAGE_GOOGLE_STRENGTH,
"openai": QWEN_ZIMAGE_OPENAI_STRENGTH,
"microsoft": QWEN_ZIMAGE_MICROSOFT_STRENGTH,
"meta": QWEN_ZIMAGE_META_STRENGTH,
}
@@ -129,7 +146,8 @@ def strength_default_help() -> str:
return (
"profile-adaptive (qwen-zimage uses resolution-adaptive denoise, with a "
f"flat OpenAI {QWEN_ZIMAGE_OPENAI_STRENGTH} / Google {QWEN_ZIMAGE_GOOGLE_STRENGTH} / "
f"Microsoft InvisMark {QWEN_ZIMAGE_MICROSOFT_STRENGTH} floors; sdxl-zimage "
f"Microsoft InvisMark {QWEN_ZIMAGE_MICROSOFT_STRENGTH} / Meta Content Seal "
f"{QWEN_ZIMAGE_META_STRENGTH} floors; sdxl-zimage "
f"uses OpenAI {SDXL_ZIMAGE_OPENAI_STRENGTH} / Google {SDXL_ZIMAGE_GEMINI_STRENGTH} / "
f"unknown {SDXL_ZIMAGE_UNKNOWN_STRENGTH}, from the C2PA issuer)"
)
@@ -167,8 +185,18 @@ def resolve_strength(
return resolution_adaptive_denoise(*size)
def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "microsoft"] | None:
"""Select the strength cohort from non-invalid pixel-watermark provenance."""
def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "microsoft", "meta"] | None:
"""Select the strength cohort from non-invalid pixel-watermark provenance.
OpenAI / Google / Microsoft come from their C2PA issuers. Meta is the
fallback cohort: Muse Image carries no C2PA at all, and its only readable
companion is the IPTC ``trainedAlgorithmicMedia`` XMP tag -- a standard code
other platforms also use. Attributing that tag to Meta is a measured bet,
not an identification: the other tag users in this project's model (ByteDance
products, X) ship no invisible pixel watermark this profile targets, so the
worst misroute spends the Meta floor (0.1) where the resolution curve would
have spent a similar amount, and Google/OpenAI files never reach this arm
because their C2PA matched first."""
try:
from remove_ai_watermarks._internal.c2pa import (
c2pa_info_has_invalid_credential,
@@ -187,4 +215,25 @@ def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "micros
return "openai"
if not c2pa_info_has_invalid_credential(info) and c2pa_info_has_invismark(info):
return "microsoft"
if _standalone_iptc_ai_tag(image_path):
return "meta"
return None
def _standalone_iptc_ai_tag(image_path: Path) -> bool:
"""True when the file carries an AI IPTC marker with no C2PA around it.
Mirrors identify's ``standalone_iptc`` condition (the tag is only
trustworthy as platform evidence when no manifest supersedes it) without
importing the heavy identify module: the shared chunk-aware
:func:`metadata.scan_head` window -- Muse WebP outputs place their XMP
packet in a tail chunk up to hundreds of KB past a plain head read, which
is exactly what scan_head's extensions exist to catch.
"""
try:
from remove_ai_watermarks.metadata import IPTC_AI_MARKERS, c2pa_marker_in, scan_head
scan = scan_head(image_path)
except Exception:
return False
return any(marker in scan for marker in IPTC_AI_MARKERS) and not c2pa_marker_in(scan)
+22 -13
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)
@@ -695,7 +704,7 @@ class TextMarkEngine:
OVERRIDABLE, and the override contract is specifically the DETECTOR'S MATCH BOX:
a mark whose removable footprint reaches beyond what the NCC localizes -- Baidu's
flat white tag right of the text run, LibLibAI's triangle logo left of the
flat white tag right of the text run, LiblibAI's triangle logo left of the
wordmark -- supplies its own extension here and inherits the rest of the
footprint path. The blob-bbox branch never routes through an override.
"""
@@ -771,7 +780,7 @@ class TextMarkEngine:
"""Footprint policy for a mark whose fill must be bounded by the DETECTOR's match
box and never by the binary glyph blob.
Baidu's white tag has a flat interior a top-hat cannot answer, and LibLibAI's
Baidu's white tag has a flat interior a top-hat cannot answer, and LiblibAI's
blob bleeds up into background structure; in both cases the blob bbox is
measurably wrong and the NCC match box is right. ``force`` takes priority here,
unlike the default policy: a ``--no-detect`` caller named the mark, so the whole
+13 -7
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(
@@ -232,6 +233,7 @@ class InvisibleOptions:
strength: float | None = None
pipeline: str = "qwen-zimage"
vendor: str | None = None
seed: int | None = None
hf_token: str | None = None
humanize: float = 0.0
@@ -487,13 +489,17 @@ def _run_invisible(
if not is_available():
say("invisible", "unavailable")
return "unavailable"
if not (force or evidence.has_invisible_target()):
if not (force or opts.vendor is not None or evidence.has_invisible_target()):
say("invisible", "no-signal")
return "no-signal"
from remove_ai_watermarks._internal.watermark_profiles import resolve_strength, vendor_for_strength
vendor = vendor_for_strength(vendor_source)
# An explicit vendor override wins over detection and implies the scrub runs:
# naming the cohort (e.g. "meta" for Muse Image Content Seal, which carries no
# provenance to detect) asserts the pixel watermark is present, so the no-signal
# gate must not skip it.
vendor = opts.vendor or vendor_for_strength(vendor_source)
# Report the strength the engine will actually execute, resolved the same way it
# resolves it, so the reported value cannot drift from the executed one.
with suppress(Exception):
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+58 -11
View File
@@ -274,6 +274,32 @@ _strength_option = click.option(
default=None,
help=f"Denoising strength (0.0-1.0). Default: {strength_default_help()}.",
)
# Explicit strength-cohort override. Auto-detection reads the C2PA issuer, so it
# covers OpenAI / Google / Microsoft; Meta Content Seal has no provenance signal
# (no C2PA; the IPTC tag is a standard code), and an unknown or stripped manifest
# also leaves the resolution-adaptive curve in charge -- this flag is the way to
# name the cohort when the user knows what the file does not say.
_vendor_option = click.option(
"--vendor",
type=click.Choice(["auto", "openai", "google", "microsoft", "meta"]),
default="auto",
help=(
"Strength cohort for the invisible-removal default, and it implies the scrub "
"runs even without a local signal: naming the cohort asserts the pixel "
"watermark is present. auto: derive from C2PA provenance, else "
"resolution-adaptive. Set explicitly when the source is known but unreadable "
"(e.g. meta for Muse Image Content Seal, which never carries C2PA)."
),
)
def _explicit_vendor(vendor: str | None) -> str | None:
"""Normalize --vendor's ``auto`` default to None for the engine/API seam.
One helper so the three diffusion commands cannot drift on the spelling."""
return None if vendor in (None, "auto") else vendor
_seed_option = click.option(
"--seed",
type=int,
@@ -281,7 +307,7 @@ _seed_option = click.option(
help="Random seed for reproducibility. Default 0: both profiles are certified "
"at a fixed seed, because SynthID removal near the strength floor is seed-dependent.",
)
_hf_token_option = click.option("--hf-token", type=str, default=None, help="HuggingFace API token.")
_hf_token_option = click.option("--hf-token", type=str, default=None, help="Hugging Face API token.")
_humanize_option = click.option(
"--humanize", type=float, default=0.0, help="Analog Humanizer film grain intensity (0 = off, typical: 2.0-6.0)."
)
@@ -515,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)
@@ -796,6 +822,7 @@ def cmd_erase(
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@_output_option
@_strength_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -815,6 +842,7 @@ def cmd_invisible(
source: Path,
output: Path | None,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -831,7 +859,7 @@ def cmd_invisible(
text_manifest: Path | None,
fidelity_anchor: bool,
) -> None:
"""Remove invisible AI watermarks (SynthID, StableSignature, TreeRing).
"""Attempt to disrupt invisible AI watermarks through pixel regeneration.
Regenerates the pixels with the two-stage diffusion profile. CUDA-only:
pip install 'remove-ai-watermarks[qwen-zimage]'
@@ -851,11 +879,17 @@ def cmd_invisible(
if output is None:
output = source.with_stem(source.stem + "_clean")
# An explicit --vendor wins over detection (see the option help) and implies the
# scrub runs: naming the cohort asserts the pixel watermark is present, so the
# no-signal gate must not skip it. Resolved BEFORE the gate for the same reason.
resolved_vendor = _explicit_vendor(vendor)
# Gate BEFORE building the engine: skip the destructive regeneration when no
# invisible AI watermark is locally detectable (it would only degrade a clean
# image -- dominant paid score-0 cause), so the common skip path pays nothing for
# engine construction. A skip never claims the image is clean; --force overrides.
if _should_skip_invisible_scrub(force, source):
# engine construction. A skip never claims the image is clean; --force and an
# explicit --vendor override.
if _should_skip_invisible_scrub(force or resolved_vendor is not None, source):
_no_invisible_signal_exit(source)
def progress_cb(msg: str) -> None:
@@ -870,11 +904,18 @@ def cmd_invisible(
)
# Detect the SynthID vendor from the ORIGINAL (before processing strips C2PA) so the
# displayed and executed strength agree on the vendor-adaptive default.
vendor = vendor_for_strength(source)
# displayed and executed strength agree on the vendor-adaptive default. An explicit
# --vendor override wins over detection: it names a cohort the file cannot prove
# (Meta Content Seal never carries C2PA; a stripped manifest proves nothing).
detected_vendor = vendor_for_strength(source) if resolved_vendor is None else None
vendor_label = resolved_vendor or detected_vendor
vendor_note = " (override)" if resolved_vendor else ""
console.print(f" Input: {source.name}")
console.print(f" Pipeline: {pipeline}")
console.print(f" Strength: {_resolved_strength_for_display(source, strength, vendor, pipeline)}")
console.print(
f" Strength: {_resolved_strength_for_display(source, strength, vendor_label, pipeline)}"
+ (f" [vendor: {vendor_label}{vendor_note}]" if vendor_label else "")
)
t0 = time.monotonic()
try:
@@ -887,7 +928,7 @@ def cmd_invisible(
unsharp=unsharp,
adaptive_polish=adaptive_polish,
max_resolution=max_resolution,
vendor=vendor,
vendor=vendor_label,
tile=tile,
tile_size=tile_size,
tile_overlap=tile_overlap,
@@ -960,8 +1001,8 @@ def cmd_metadata(
Strips EXIF AI tags, PNG text chunks, C2PA provenance manifests, and the
China TC260 AIGC label. Beyond images (PNG/JPEG/WebP/AVIF/HEIF/JXL) it also
strips provenance metadata from MP4/MOV/M4V/M4A containers and, via ffmpeg,
from WebM/MKV/AVI/FLV/MP3/WAV/FLAC/OGG. The coded image, audio, and video
data are left untouched.
from WebM/MKV/MKA/AVI/FLV/MP3/WAV/FLAC/OGG/OGA/Opus/AAC. The coded image,
audio, and video data are left untouched.
"""
from remove_ai_watermarks.metadata import get_ai_metadata, has_ai_metadata, strip_and_verify
@@ -1493,6 +1534,7 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo
@_visible_backend_option
@_visible_sensitivity_option
@_strength_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -1514,6 +1556,7 @@ def cmd_all(
backend: str,
sensitivity: str,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -1594,6 +1637,7 @@ def cmd_all(
sensitivity=_parse_sensitivity(sensitivity),
invisible=InvisibleOptions(
strength=strength,
vendor=_explicit_vendor(vendor),
pipeline=pipeline,
seed=seed,
hf_token=hf_token,
@@ -1688,6 +1732,7 @@ def _batch_engine(mode: str, options: InvisibleOptions) -> object | None:
@_visible_backend_option
@_visible_sensitivity_option
@_humanize_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -1705,6 +1750,7 @@ def cmd_batch(
mode: str,
output_dir: Path | None,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -1742,6 +1788,7 @@ def cmd_batch(
invisible_options = InvisibleOptions(
strength=strength,
vendor=_explicit_vendor(vendor),
pipeline=pipeline,
seed=seed,
hf_token=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.
+53 -15
View File
@@ -135,12 +135,17 @@ _C2PA_INVALID_CAVEAT = (
"are retained only as removal hints, not as verified provenance."
)
_IPTC_ONLY_CAVEAT = "The IPTC 'Made with AI' tag flags AI provenance but does not identify the specific platform."
_CONTENT_SEAL_CAVEAT = (
"Meta Muse Image outputs carry the invisible Content Seal pixel watermark, which has no "
"local decoder; `invisible` removes it (auto when this tag is present, or `--vendor meta` "
"on stripped files) and meta.ai/identification verifies it."
)
_INVISIBLE_WM_CAVEAT = (
"The open invisible watermark is fragile: it does not survive JPEG re-encoding "
"or resizing, so it confirms origin only on a pristine (un-re-encoded) file."
)
_HF_JOB_CAVEAT = (
"The hf-job-id tag marks a HuggingFace-hosted job (commonly diffusion "
"The hf-job-id tag marks a Hugging Face-hosted job (commonly diffusion "
"generation) but names neither the model nor the content type, so it is a "
"medium-confidence signal, not proof the pixels are AI-generated."
)
@@ -440,7 +445,7 @@ def evidence_from_metadata_record(
if iptc_system:
ai_metadata.setdefault("ai_system", f"IPTC 2025.1 AI disclosure ({iptc_system})")
if hf_job:
ai_metadata.setdefault("huggingface_job", f"HuggingFace-hosted job ({hf_job})")
ai_metadata.setdefault("huggingface_job", f"Hugging Face-hosted job ({hf_job})")
if samsung is not None:
ai_metadata.setdefault("samsung_genai", f"Samsung Galaxy AI editing marker (genAIType={samsung})")
@@ -948,7 +953,7 @@ def _visible_sparkle(image_path: Path, *, image: NDArray[Any] | None = None) ->
# metadata label); the per-engine detection thresholds live in the registry.
# Text mark -> the platform sentence this report prints when that mark is the strongest
# evidence, DERIVED from the registry rows so registering a mark is one edit. It was a
# hand-maintained copy, and that class of copy is how LibLibAI ended up registered but
# hand-maintained copy, and that class of copy is how LiblibAI ended up registered but
# missing from the pill veto. Insertion order is the registry's, which is what fixes the
# scan order below. The Gemini sparkle and the capture-less pill carry no platform of
# their own (`KnownMark.platform is None`) and are excluded here: the sparkle has its
@@ -1100,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)"
@@ -1268,7 +1273,19 @@ def _identify_from_evidence(
# reusing the derived `has_c2pa` / `source_kind` above, which are broader:
# the file path's answer must not move.
trained_source = b"trainedAlgorithmicMedia" in head or b"TrainedAlgorithmicMedia" in head
if not synthid and trained_source and c2pa_marker_in(head) and (vendors := synthid_evidence_vendors_in(region)):
# Same suppression as every other inference site: bytes that name their own
# forensic soft-binding algorithm carry that vendor's mark, and the generic
# vendor-token inference must not add a second, differently-attributed
# invisible watermark (Microsoft Designer: "Azure OpenAI ImageGen" agent +
# the InvisMark watermarked action read as "SynthID per OpenAI").
soft_binding_vendors = soft_binding_vendors_in(region)
if (
not synthid
and trained_source
and c2pa_marker_in(head)
and not soft_binding_vendors
and (vendors := synthid_evidence_vendors_in(region))
):
synthid = synthid_verdict(", ".join(vendors))
if synthid:
watermarks.append(
@@ -1283,7 +1300,7 @@ def _identify_from_evidence(
# ── C2PA soft-binding: a named forensic/third-party watermark vendor ─
# (Adobe TrustMark, Digimarc, Imatag, ...). Present in the manifest even when
# the watermark itself can't be decoded; names whose watermark stamped the pixels.
soft_binding = meta.get("soft_binding") or (", ".join(v) if (v := soft_binding_vendors_in(region)) else None)
soft_binding = meta.get("soft_binding") or (", ".join(soft_binding_vendors) if soft_binding_vendors else None)
if soft_binding:
soft_binding_algorithm = meta.get("soft_binding_algorithm") or info.get("soft_binding_algorithm")
soft_binding_value = meta.get("soft_binding_value") or info.get("soft_binding_value")
@@ -1317,17 +1334,38 @@ def _identify_from_evidence(
if standalone_iptc:
signals.append(Signal("iptc", "digitalSourceType (Made with AI)", "high"))
watermarks.append("IPTC digitalSourceType (Made with AI)")
# Muse Image stamps every output with the invisible Content Seal, and this
# tag is the only provenance such a file carries - the same measured bet
# the strength router makes (vendor_for_strength -> "meta"). Emit the seal
# as its own stable signal, the way InvisMark is additive over
# soft_binding, so clients select pixel removal from the signal list
# instead of parsing caveats. It is an attribution, not a decode: no
# public Content Seal decoder exists, hence "medium".
signals.append(
Signal(
"content_seal",
"Meta Muse Content Seal pixel watermark (attributed by the standalone AI digital-source tag)",
"medium",
)
)
watermarks.append("Invisible Content Seal watermark (Meta Muse attribution)")
caveats.append(_IPTC_ONLY_CAVEAT)
caveats.append(_CONTENT_SEAL_CAVEAT)
if platform is None:
# Apple Photos Clean Up (Apple Intelligence object removal) marks
# the edit with photoshop:Credit / IPTC "Apple Photos Clean Up"
# next to compositeWithTrainedAlgorithmicMedia. It was detected but
# previously never attributed.
platform = (
"Apple Photos (Clean Up AI edit)"
if b"Apple Photos Clean Up" in head
else "Made-with-AI tag (e.g. Meta AI); platform not specified"
)
if b"Apple Photos Clean Up" in head:
platform = "Apple Photos (Clean Up AI edit)"
else:
# The platform line follows the same measured bet the seal signal
# and the strength router make: Muse Image is the tag writer whose
# outputs this profile targets, so a hedged Muse attribution is
# more useful than "platform not specified" while the panel below
# already prices the Content Seal removal. The hedge stays in the
# wording - it names the attribution basis, not a detection.
platform = "Meta Muse Image (attributed by the standalone AI digital-source tag)"
# ── IPTC 2025.1 AI-disclosure fields (Iptc4xmpExt:AISystemUsed etc.) ─
iptc_ai = any(m in head for m in IPTC_AI_FIELD_MARKERS)
@@ -1388,17 +1426,17 @@ def _identify_from_evidence(
platform = "xAI (Grok / Aurora)"
ai_vendor_claims["xai"] = "xAI"
# ── HuggingFace-hosted job marker (hf-job-id PNG text chunk) ─────
# ── Hugging Face-hosted job marker (hf-job-id PNG text chunk) ─────
# Marks the hosting job, not a model -- medium confidence (commonly diffusion
# output). Like the visible sparkle, it lifts an otherwise-Unknown verdict to
# a tentative AI, but never overrides a high-confidence metadata signal.
hf_job = evidence.huggingface_job
if hf_job:
signals.append(Signal("hf_job", f"HuggingFace job {hf_job}", "medium"))
watermarks.append("HuggingFace-hosted job (hf-job-id)")
signals.append(Signal("hf_job", f"Hugging Face job {hf_job}", "medium"))
watermarks.append("Hugging Face-hosted job (hf-job-id)")
caveats.append(_HF_JOB_CAVEAT)
if platform is None:
platform = "HuggingFace-hosted job (model not identified)"
platform = "Hugging Face-hosted job (model not identified)"
# ── Samsung Galaxy AI editing marker (genAIType) ─────────────────
# Galaxy AI tools stamp a proprietary genAIType in PhotoEditor_Re_Edit_Data.
+6 -5
View File
@@ -33,7 +33,7 @@ warnings.filterwarnings("ignore", category=UserWarning, module="huggingface_hub"
warnings.filterwarnings("ignore", category=UserWarning, module="diffusers")
warnings.filterwarnings("ignore", module="transformers")
# Suppress HuggingFace internal logging
# Suppress Hugging Face internal logging
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
os.environ["DIFFUSERS_VERBOSITY"] = "error"
@@ -104,7 +104,7 @@ class InvisibleEngine:
global pass, vendor-adaptive strength because an SDXL global stage
needs more of it). BOTH ARE CUDA-ONLY -- there is no CPU or MPS path
for invisible-watermark removal.
hf_token: HuggingFace API token.
hf_token: Hugging Face API token.
progress_callback: Optional callback for progress messages.
controlnet_conditioning_scale: Canny ControlNet structure-preservation
strength on the global stage of both profiles.
@@ -187,9 +187,8 @@ class InvisibleEngine:
pixels. Enables the experimental Qwen-VAE ``vae-glyphs`` post-pass.
Requires the ``text-restoration`` extra and the ``qwen-zimage``
profile. Incompatible with downscaling, humanize, unsharp, and
adaptive polish. Tiling is supported: the VAE donor uses the same
overlapping tiles as the global pass, then glyph restore runs on
the blended full frame.
adaptive polish. Tiling is rejected because that combination has
no provider-oracle calibration.
fidelity_anchor: Blend 15% of the Qwen-VAE donor across the whole frame
before glyph restoration. OFF by default since 0.27.1: that global
blend was measured to return detector-visible OpenAI SynthID on
@@ -211,6 +210,8 @@ class InvisibleEngine:
if text_manifest is not None:
if self._remover.model_profile != QWEN_ZIMAGE_PROFILE:
raise ValueError("--text-manifest is supported only by the qwen-zimage profile")
if tile:
raise ValueError("--text-manifest is not calibrated with --tile")
if max_resolution != 0:
raise ValueError("--text-manifest requires --max-resolution 0")
if humanize > 0.0 or unsharp > 0.0 or adaptive_polish:
@@ -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)
+8 -8
View File
@@ -1,13 +1,13 @@
"""LibLibAI visible watermark detector/localizer.
"""LiblibAI visible watermark detector/localizer.
LibLibAI (哩布哩布AI, USCC 91110105MACJ6K1C8A) stamps its generations with a
white triangle logo + "LibLibAI" latin wordmark at **bottom-center** (not a
LiblibAI (哩布哩布AI, USCC 91110105MACJ6K1C8A) stamps its generations with a
white triangle logo + "LiblibAI" Latin wordmark at **bottom-center** (not a
corner -- the locate box is horizontally centered). Detection matches the
bundled font-rendered "LibLibAI" silhouette (the triangle logo is NOT rendered
bundled font-rendered "LiblibAI" silhouette (the triangle logo is NOT rendered
-- logos vary, the wordmark discriminates); removal is the shared **localize ->
fill** (the glyph blob covers logo + wordmark, both bright).
This module supplies only LibLibAI's tuned :class:`TextMarkConfig`
This module supplies only LiblibAI's tuned :class:`TextMarkConfig`
(``assets/liblib_alpha.png`` from ``scripts/render_vendor_silhouettes.py``,
never cut from an upload).
@@ -64,7 +64,7 @@ _ALPHA_HEIGHT_FRAC = 0.026
_LADDER = (0.9, 1.0, 1.1)
_CONFIG = TextMarkConfig(
name="LibLibAI",
name="LiblibAI",
asset_name="liblib_alpha.png",
corner="bc",
margin_floor=4,
@@ -90,12 +90,12 @@ _CONFIG = TextMarkConfig(
def _alpha_template() -> NDArray[Any] | None:
"""The bundled LibLibAI alpha template (float [0,1]), or None."""
"""The bundled LiblibAI alpha template (float [0,1]), or None."""
return _text_mark_engine.load_alpha_template(_CONFIG.asset_name)
class LibLibEngine(TextMarkEngine):
"""Detect/localize the visible LibLibAI wordmark (bottom-center; localize -> fill)."""
"""Detect/localize the visible LiblibAI wordmark (bottom-center; localize -> fill)."""
# Per-mark size floor prevents small generic icons from matching the wordmark.
_MIN_SHORT_SIDE = 480
+14 -7
View File
@@ -199,7 +199,7 @@ def parse_tc260_aigc_json(value: bytes) -> dict[str, str] | None:
return fields if TC260_AIGC_FIELDS & fields.keys() else None
# HuggingFace-hosted GPU jobs (Jobs / Spaces) stamp generated PNGs with this
# Hugging Face-hosted GPU jobs (Jobs / Spaces) stamp generated PNGs with this
# ``tEXt`` chunk key holding the job UUID. It marks the hosting job, not a
# specific model -- a medium-confidence AI signal (commonly diffusion output).
_HF_JOB_KEY: str = "hf-job-id"
@@ -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"})
@@ -510,7 +510,7 @@ def has_ai_metadata(image_path: Path) -> bool:
# only the XMP form; the raw-JSON tEXt chunk needs the PIL-based parse).
if aigc_label(image_path) is not None:
return True
# HuggingFace-hosted job marker (hf-job-id PNG text chunk).
# Hugging Face-hosted job marker (hf-job-id PNG text chunk).
if huggingface_job(image_path):
return True
# xAI / Grok: no C2PA/IPTC/XMP -- only the EXIF Signature + UUID-Artist pair.
@@ -682,10 +682,10 @@ def c2pa_cloud_manifest(image_path: Path) -> str | None:
def _huggingface_job_impl(image_path: Path) -> str | None:
"""Return the HuggingFace job id if the image carries an ``hf-job-id`` PNG
"""Return the Hugging Face job id if the image carries an ``hf-job-id`` PNG
text chunk, else None.
HuggingFace-hosted GPU jobs (Jobs / Spaces) stamp generated PNGs with an
Hugging Face-hosted GPU jobs (Jobs / Spaces) stamp generated PNGs with an
``hf-job-id`` ``tEXt`` chunk holding the job's UUID. It identifies the
*hosting job*, not a specific model, and is most commonly seen on diffusion-
generation output -- a medium-confidence AI signal, not proof of AI pixels
@@ -840,6 +840,13 @@ def synthid_source(image_path: Path, *, c2pa_info: dict[str, Any] | None = None)
ai_source = b"trainedAlgorithmicMedia" in data or b"TrainedAlgorithmicMedia" in data
if not (has_c2pa and ai_source):
return None
from remove_ai_watermarks._internal.c2pa import soft_binding_vendors_in
# A scan that names its own forensic soft-binding algorithm carries that
# vendor's mark; the generic vendor-token inference must not add a second,
# differently-attributed invisible watermark from the same bytes.
if soft_binding_vendors_in(data):
return None
matched = synthid_evidence_vendors_in(data)
return ", ".join(matched) if matched else None
@@ -1210,9 +1217,9 @@ def get_ai_metadata(image_path: Path) -> dict[str, str]:
if system := iptc_ai_system(image_path):
result.setdefault("ai_system", f"IPTC 2025.1 AI disclosure ({system})")
# HuggingFace-hosted job marker (hf-job-id PNG text chunk).
# Hugging Face-hosted job marker (hf-job-id PNG text chunk).
if job := huggingface_job(image_path):
result.setdefault("huggingface_job", f"HuggingFace-hosted job ({job})")
result.setdefault("huggingface_job", f"Hugging Face-hosted job ({job})")
# Samsung Galaxy AI editing marker (genAIType in PhotoEditor_Re_Edit_Data).
if (genai := samsung_genai(image_path)) is not None:
result.setdefault("samsung_genai", f"Samsung Galaxy AI editing marker (genAIType={genai})")
@@ -0,0 +1,108 @@
"""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.
* Provenance relaxation 0.7 (relaxed gate 0.266), enabled 2026-08-28 when the
cohort the strict-only note was waiting for became available: an OCR badge
census split the 343 Microsoft-C2PA uploads into 86 badge carriers and 257
true badge-less files (the watermark is a per-user opt-in, so 75% of MS
uploads carry none). Badge-less max 0.251 / p99 0.213, so the relaxed band
[0.251, 0.38) holds three genuine faint badges and zero false fills
(measured 3/3; doubao ships 0.7 on a 58%-precision band). Strict controls
max 0.293 / p99 0.200 vs the 0.38 gate.
* 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=0.7,
)
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
+4 -4
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.
"""
@@ -179,8 +179,8 @@ _VISIBLE_PLATFORM = {
"veo": "Google Veo",
"seedance": "ByteDance Seedance",
"dola": "ByteDance Dola",
"hailuo": "MiniMax Hailuo",
"kling": "Kuaishou Kling",
"hailuo": "MiniMax Hailuo AI",
"kling": "Kuaishou Kling AI",
}
+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.
+43 -19
View File
@@ -17,17 +17,18 @@ localizer stays cheap (cv2/numpy, CPU) so a memory-tight caller can run it on a
small worker; the heavy fill (MI-GAN / LaMa) is opt-in and chosen by the caller.
Entries:
- ``gemini`` -- Google Gemini / Nano Banana sparkle, bottom-right.
- ``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.
- ``kling`` -- Kuaishou Kling "可灵AI 3.0" 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.
- ``jimeng_pill`` -- Jimeng-basic "AI生成" pill, top-left (capture-less).
- ``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`` -- 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,11 +628,11 @@ 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",
"Google Gemini sparkle",
"Google Gemini visible watermark (sparkle)",
"bottom-right",
True,
"gemini",
@@ -651,14 +663,14 @@ _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(
"kling",
"Kling 可灵AI 3.0 text",
"Kling AI 可灵AI 3.0 text",
"bottom-right",
platform="Kuaishou Kling (visible 可灵AI 3.0 mark detected)",
platform="Kuaishou Kling AI (visible 可灵AI 3.0 mark detected)",
tc260_producer_codes=("91110108335469089C",),
),
_text_mark(
@@ -693,11 +705,23 @@ _REGISTRY: tuple[KnownMark, ...] = (
),
_text_mark(
"liblib",
"LibLibAI wordmark",
"LiblibAI wordmark",
"bottom-center",
platform="LibLibAI (visible LibLibAI mark detected)",
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",
@@ -794,7 +818,7 @@ def tc260_producer_vendors() -> dict[str, str]:
def _pill_suppressors() -> set[str]:
"""Marks whose detection vetoes the capture-less pill: same label regime as the
pill, different product. Derived so a newly registered TC260 mark cannot be
forgotten here -- which is exactly how LibLibAI ended up missing."""
forgotten here -- which is exactly how LiblibAI ended up missing."""
pill = get_mark("jimeng_pill")
return {
m.key
@@ -822,9 +846,9 @@ def _keep_pill(keys: set[str], *, provenance: frozenset[str], footprint_flat: bo
No confirmation at all -> never remove (blocks false fires on non-Jimeng content).
The suppressor set is DERIVED from the registry (same label regime, different
product), not hand-listed. The hand-written list had drifted: LibLibAI was
product), not hand-listed. The hand-written list had drifted: LiblibAI was
registered alongside RunningHub and Baidu but never added to it, so a confident
LibLibAI detection did not veto the pill the way its two siblings did. Marks
LiblibAI detection did not veto the pill the way its two siblings did. Marks
outside the TC260 regime (Gemini, Samsung) are deliberately NOT suppressors --
neither can put ``"jimeng"`` into ``provenance``, so neither can enable the arm
they would be vetoing."""