"""Policy-level tests for the shared text-mark engine config. These assert TUNING that was set by corpus measurement, not algorithm behaviour -- they exist so a future edit cannot silently revert a calibrated constant back to a value that was measured to be wrong. The measurements themselves live in `docs/module-internals.md` and in the comment at `_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`. """ class TestRivalMargin: """Detection among same-corner marks is COMPETITIVE, not just absolute. Doubao "豆包AI生成" and Jimeng "★ 即梦AI" both sit bottom-right in near-white CJK and survive binarization as similar blobs, so an absolute NCC gate cannot tell them apart -- 33 of jimeng's 68 false additions were Doubao marks. Measured separability scoring both templates on the SAME blob (n=40 jimeng / 75 doubao): absolute ncc_jimeng 0.96, ncc_jimeng MINUS ncc_doubao 0.99. Corpus effect of the margin gate: jimeng precision 38% -> 63% with genuine detections unchanged at 40 (false fires 65 -> 23). """ def test_jimeng_competes_against_doubao(self): from remove_ai_watermarks import jimeng_engine assert "doubao_alpha.png" in jimeng_engine._CONFIG.rivals def test_doubao_has_no_rival_margin(self): """Asymmetric by measurement, not oversight: the symmetric gate cost Doubao 7 genuine detections to prevent 5 false ones (1.4:1 against), while Jimeng gained 25pp for free. Doubao's absolute detector is already 86% precise.""" from remove_ai_watermarks import doubao_engine assert doubao_engine._CONFIG.rivals == () def test_a_doubao_shaped_blob_loses_the_jimeng_margin(self): """The decisive case: a blob matching Doubao BETTER than Jimeng must not be booked as a Jimeng wordmark, however high its absolute Jimeng score.""" import numpy as np from remove_ai_watermarks import jimeng_engine from remove_ai_watermarks._text_mark_engine import glyph_silhouette eng = jimeng_engine.JimengEngine() doubao_blob = glyph_silhouette("doubao_alpha.png") assert doubao_blob is not None canvas = np.zeros((doubao_blob.shape[0] + 20, doubao_blob.shape[1] + 20), np.uint8) canvas[10 : 10 + doubao_blob.shape[0], 10 : 10 + doubao_blob.shape[1]] = doubao_blob width = int(doubao_blob.shape[1] / jimeng_engine._CONFIG.alpha_width_frac) jimeng_score = eng._template_match_score(canvas, width) assert not eng._rival_margin_ok(jimeng_score, canvas, width) class TestPerMarkProvenanceRelaxation: """The provenance NCC relaxation is PER MARK, not one shared multiplier. Corpus-measured 2026-07-18 on the default `auto` path (4417 unique TC260 carriers, blind hand-label, two-sided control): the single shared 0.7 ran at 76% precision on doubao but 17% on jimeng, because jimeng's relaxed silhouette keys on "text in the bottom-right corner" rather than the wordmark -- 33 of its 68 false additions were DOUBAO marks. Full table at `_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`. """ class TestScaleBasis: """Mark geometry scales with a PER-MARK image dimension, measured not assumed. Every tuned fraction was calibrated on PORTRAIT captures, where width and short side coincide, so the basis was never exercised until landscape inputs were measured. Corpus-measured 2026-07-18 (2572 unique TC260 carriers): doubao detection was portrait 60% / square 41% / **landscape 0% of 435** -- a width-scaled box is inflated by the aspect ratio on a wide image and the glyph never lands in it. A short-side basis recovered 56% of the previously-undetected landscape set. The same switch broke JIMENG (labelled landscape positives 13/13 -> 0/13), whose wordmark tracks the width -- hence per-mark, not a house rule. """ def test_doubao_scales_with_the_short_side(self): from remove_ai_watermarks import doubao_engine assert doubao_engine._CONFIG.scale_basis == "short" def test_jimeng_scales_with_width(self): """Measured, not an oversight: the short-side basis took jimeng's labelled landscape positives from 13/13 to 0/13.""" from remove_ai_watermarks import jimeng_engine assert jimeng_engine._CONFIG.scale_basis == "width" def test_samsung_keeps_width_because_it_is_unmeasured(self): """1 addition corpus-wide, so there is no evidence either way; an unmeasured change is not an improvement.""" from remove_ai_watermarks import samsung_engine assert samsung_engine._CONFIG.scale_basis == "width" def test_basis_only_differs_on_non_square_images(self): """The basis is a no-op wherever width IS the short side, which is why the bug survived: every calibration capture was portrait.""" import numpy as np from remove_ai_watermarks import doubao_engine, jimeng_engine portrait = np.zeros((1600, 900, 3), np.uint8) landscape = np.zeros((900, 1600, 3), np.uint8) d, j = doubao_engine.DoubaoEngine(), jimeng_engine.JimengEngine() assert d.scale_base(portrait) == j.scale_base(portrait) == 900 assert d.scale_base(landscape) == 900 assert j.scale_base(landscape) == 1600 def test_landscape_box_stays_inside_the_frame(self): """The concrete failure: a width-scaled box on a wide image overshoots the mark's real footprint. The short-side box must be proportionally smaller.""" import numpy as np from remove_ai_watermarks import doubao_engine landscape = np.zeros((900, 2400, 3), np.uint8) loc = doubao_engine.DoubaoEngine().locate(landscape) assert loc.w < int(2400 * doubao_engine._CONFIG.width_frac) assert loc.x + loc.w <= 2400 assert loc.y + loc.h <= 900 class TestTophatFrontend: """Detection can correlate the CONTINUOUS top-hat instead of a binarized blob. `extract_mask` thresholds the top-hat into a 0/255 glyph blob, which is fine for a mark stamped bold and opaque and destructive for a faint one -- a thin translucent overlay shatters into specks and no template can match a blob that is not there (measured: 千问 scored 0.170 mean vs doubao's 0.723 through the binary path, 0% over the gate). The `tophat` front-end never binarizes: the saturation/luma gates become weights, and the response is max-normalized so the score is contrast-invariant. Corpus effect on the 240-image unbiased recall sample: doubao recall 89% -> 92% at an unchanged 99% precision. """ def test_doubao_uses_the_continuous_frontend(self): from remove_ai_watermarks import doubao_engine assert doubao_engine._CONFIG.detect_frontend == "tophat" def test_other_marks_stay_binary_until_measured(self): """A front-end switch must be measured per mark before it ships; jimeng and samsung have no such measurement yet.""" from remove_ai_watermarks import jimeng_engine, samsung_engine assert jimeng_engine._CONFIG.detect_frontend == "binary" assert samsung_engine._CONFIG.detect_frontend == "binary" def test_response_is_contrast_invariant(self): """The whole point: a faint mark and a bold one produce the same response, so a single threshold works for both. Binarizing is what loses the faint one.""" import numpy as np from remove_ai_watermarks import doubao_engine eng = doubao_engine.DoubaoEngine() h, w = 400, 900 out = [] for amplitude in (12, 90): # a barely-there overlay and a bold one img = np.full((h, w, 3), 100, np.uint8) loc = eng.locate(img) x, y, bw, bh = loc.bbox img[y + bh // 3 : y + 2 * bh // 3, x + bw // 4 : x + 3 * bw // 4] = 100 + amplitude resp = eng.tophat_response(img, loc) assert resp is not None out.append(resp) # max-normalized, so the two responses agree despite a 7.5x contrast difference assert abs(int(out[0].max()) - int(out[1].max())) <= 1 def test_flat_input_yields_no_response(self): """A blank corner has no top-hat at all; the engine must return None rather than divide by a zero peak.""" import numpy as np from remove_ai_watermarks import doubao_engine eng = doubao_engine.DoubaoEngine() img = np.full((400, 900, 3), 128, np.uint8) assert eng.tophat_response(img, eng.locate(img)) is None def test_threshold_is_frontend_specific(self): """The continuous front-end scores higher overall (0.809 vs 0.723 mean on the same positives), so it needs its own gate; the binary-era 0.40 left the provenance-relaxed gate low enough to admit 8 false fires where 0.50 admits 1.""" from remove_ai_watermarks import doubao_engine assert doubao_engine._CONFIG.detect_ncc_threshold == 0.50