mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-27 16:02:28 +02:00
feat(identify): detect China TC260 AIGC label (Doubao et al.)
China-served generators embed an XMP <TC260:AIGC>{"Label":"1",...} block
(China's mandatory AI-content labeling, TC260 standard). Doubao (ByteDance)
uses it -- verified on the real #13 sample. It's none of C2PA / SynthID /
imwatermark / IPTC, so identify() previously returned unknown.
- metadata: AIGC_MARKERS + aigc_label() (json-decodes the HTML-entity-encoded
block); has_ai_metadata + get_ai_metadata now surface it.
- identify: new 'aigc' signal -> is_ai True, platform 'China AIGC-labeled
generator (TC260; e.g. Doubao)', carries the ContentProducer code.
- Container-agnostic raw-byte scan, so it covers the whole China-AIGC ecosystem
(Jimeng/Kling/Qwen/Ernie share the standard).
- Tests: synthetic TC260 block (metadata + identify). Docs updated.
Addresses #13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
768d997ef0
commit
c7f0d71f90
@@ -317,3 +317,35 @@ class TestIdentifyInvisibleWatermark:
|
||||
def test_check_invisible_false_skips(self, tmp_path: Path):
|
||||
r = identify(self._sdxl_watermarked(tmp_path), check_visible=False, check_invisible=False)
|
||||
assert not any(s.name == "invisible_watermark" for s in r.signals)
|
||||
|
||||
|
||||
class TestIdentifyAIGC:
|
||||
"""China TC260 AIGC label is detected and attributed (e.g. Doubao)."""
|
||||
|
||||
def _aigc_png(self, tmp_path: Path) -> Path:
|
||||
from PIL import Image
|
||||
|
||||
p = tmp_path / "doubao.png"
|
||||
Image.new("RGB", (32, 32)).save(p)
|
||||
xmp = (
|
||||
'<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF '
|
||||
'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
|
||||
'<rdf:Description xmlns:TC260="http://www.tc260.org.cn/ns/AIGC/1.0/">'
|
||||
"<TC260:AIGC>{"Label":"1","ContentProducer":"BYTEDANCE001"}"
|
||||
"</TC260:AIGC></rdf:Description></rdf:RDF></x:xmpmeta>"
|
||||
)
|
||||
with open(p, "ab") as f:
|
||||
f.write(xmp.encode())
|
||||
return p
|
||||
|
||||
def test_aigc_detected(self, tmp_path: Path):
|
||||
r = identify(self._aigc_png(tmp_path), check_visible=False)
|
||||
assert r.is_ai_generated is True
|
||||
assert r.platform is not None
|
||||
assert "AIGC" in r.platform or "TC260" in r.platform
|
||||
assert any("AIGC" in w for w in r.watermarks)
|
||||
|
||||
def test_aigc_signal_carries_producer(self, tmp_path: Path):
|
||||
r = identify(self._aigc_png(tmp_path), check_visible=False)
|
||||
sig = next(s for s in r.signals if s.name == "aigc")
|
||||
assert "BYTEDANCE001" in sig.detail
|
||||
|
||||
@@ -390,3 +390,46 @@ class TestExifGenerator:
|
||||
|
||||
def test_clean_image_is_none(self, tmp_clean_png: Path):
|
||||
assert exif_generator(tmp_clean_png) is None
|
||||
|
||||
|
||||
class TestAIGCLabel:
|
||||
"""China TC260 AIGC labeling (Doubao and other China-served generators)."""
|
||||
|
||||
def _aigc_png(self, tmp_path: Path, label: str = "1", producer: str = "TESTPRODUCER001") -> Path:
|
||||
from remove_ai_watermarks.metadata import aigc_label # noqa: F401 (import-time guard)
|
||||
|
||||
p = tmp_path / "doubao.png"
|
||||
Image.new("RGB", (32, 32)).save(p)
|
||||
# XMP is HTML-entity encoded in real files; aigc_label must unescape it.
|
||||
xmp = (
|
||||
'<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF '
|
||||
'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
|
||||
'<rdf:Description rdf:about="" '
|
||||
'xmlns:TC260="http://www.tc260.org.cn/ns/AIGC/1.0/"><TC260:AIGC>'
|
||||
f"{{"Label":"{label}","ContentProducer":"{producer}"}}"
|
||||
"</TC260:AIGC></rdf:Description></rdf:RDF></x:xmpmeta>"
|
||||
)
|
||||
with open(p, "ab") as f:
|
||||
f.write(xmp.encode())
|
||||
return p
|
||||
|
||||
def test_parses_label_and_producer(self, tmp_path: Path):
|
||||
from remove_ai_watermarks.metadata import aigc_label
|
||||
|
||||
info = aigc_label(self._aigc_png(tmp_path))
|
||||
assert info is not None
|
||||
assert info["Label"] == "1"
|
||||
assert info["ContentProducer"] == "TESTPRODUCER001"
|
||||
|
||||
def test_none_when_absent(self, tmp_clean_png):
|
||||
from remove_ai_watermarks.metadata import aigc_label
|
||||
|
||||
assert aigc_label(tmp_clean_png) is None
|
||||
|
||||
def test_has_ai_metadata_detects_aigc(self, tmp_path: Path):
|
||||
assert has_ai_metadata(self._aigc_png(tmp_path))
|
||||
|
||||
def test_get_ai_metadata_surfaces_aigc(self, tmp_path: Path):
|
||||
meta = get_ai_metadata(self._aigc_png(tmp_path))
|
||||
assert "aigc_label" in meta
|
||||
assert "TC260" in meta["aigc_label"]
|
||||
|
||||
Reference in New Issue
Block a user