File size: 8,537 Bytes
8a42224 c020606 df7fff2 c020606 8a42224 df7fff2 8a42224 206dd37 8d06786 8a42224 c020606 8a42224 df7fff2 8a42224 8d06786 8a42224 8d06786 c020606 8d06786 c020606 df7fff2 c020606 df7fff2 8a42224 c020606 8a42224 206dd37 8a42224 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | """Accuracy audit for taxonomy routing + classify gates (score fixtures)."""
from __future__ import annotations
from pathlib import Path
import pytest
from PIL import Image
from app.api import _classify_from_scores
from app.taxonomy import choose_best_destination, reload_taxonomy
SELECTED_ALL = {
"fertilization",
"NTR",
"incest",
"nakadashi",
"fellatio",
"paizuri",
"footjob",
"tentacles",
"sex",
"loli",
"shota",
"monster_girl",
"android",
"furry",
"Pokemon",
"bestiality",
"Voyeur",
}
# Fallback homes are opt-in; they yield to every folder in SELECTED_ALL.
SELECTED_FALLBACKS = {"SFW", "comic", "scenery"}
@pytest.fixture(autouse=True)
def _reload_default_taxonomy() -> None:
reload_taxonomy()
@pytest.mark.parametrize(
"scores,expected",
[
({"loli": 0.92, "flat_chest": 0.99, "lolita_fashion": 0.95}, "loli"),
({"shota": 0.88, "1boy": 0.99, "child": 0.9}, "shota"),
({"netorare": 0.81, "caught": 0.99}, "NTR"),
({"incest": 0.77, "siblings": 0.99}, "incest"),
({"internal_cumshot": 0.9, "cum_in_pussy": 0.95}, "nakadashi"),
({"fertilization": 0.8, "cum_in_pussy": 0.99}, "fertilization"),
({"impregnation": 0.85, "pregnant": 0.99}, "fertilization"),
({"irrumatio": 0.9}, "fellatio"),
({"monster_girl": 0.9, "horns": 0.99, "wings": 0.98}, "monster_girl"),
({"slime_girl": 0.86}, "monster_girl"),
({"lamia": 0.9}, "monster_girl"),
({"scylla": 0.88}, "monster_girl"),
({"shark_girl": 0.85}, "monster_girl"),
({"moth_girl": 0.84}, "monster_girl"),
({"traditional_youkai": 0.9}, "monster_girl"),
({"monsterification": 0.8}, "monster_girl"),
({"cow_girl": 0.99}, None),
({"extra_eyes": 0.95}, None),
({"android": 0.9}, "android"),
({"robot_girl": 0.88}, "android"),
({"cyborg": 0.85}, "android"),
({"mecha_musume": 0.84}, "android"),
({"mecha": 0.99}, None),
({"mechanical_arms": 0.95}, None),
({"furry": 0.9, "animal_ears": 0.99}, "furry"),
({"pokemon_(creature)": 0.91, "pokemon_ears": 0.99}, "Pokemon"),
# False-positive clusters must not route
({"lolita_fashion": 0.99, "gothic_lolita": 0.98, "flat_chest": 0.97}, None),
({"1boy": 0.99, "male_focus": 0.98, "otoko_no_ko": 0.9}, None),
({"animal_ears": 0.99, "fake_animal_ears": 0.9}, None),
({"siblings": 0.99}, None),
({"pregnant": 0.99}, None),
# voyeurism is Voyeur/caught evidence (not NTR); caught/watching alone must not route
({"voyeurism": 0.99, "caught": 0.9}, "Voyeur/caught"),
({"watching": 0.99, "caught": 0.9}, None),
({"horns": 0.99, "wings": 0.98, "tail": 0.97}, None),
({"oral": 0.99}, None),
({"pantyshot": 0.9}, "Voyeur/panties"),
({"spread_pussy": 0.9}, "Voyeur/pussy"),
# Surface cum is tease, not a hard act, so the pussy tease folder keeps it.
({"pussy": 0.88, "cum": 0.61}, "Voyeur/pussy"),
({"soles": 0.9, "feet": 0.85}, "Voyeur/feet"),
({"upskirt": 0.9, "loli": 0.6}, "loli"),
# Voyeur sub-folder funnel
({"highleg_leotard": 0.9, "covered_nipples": 0.7}, "Voyeur/leotard"),
({"bikini": 0.95}, "Voyeur/swimsuit"),
({"nude": 0.9, "nipples": 0.88}, "Voyeur/nude"),
({"lingerie": 0.9}, "Voyeur/lingerie"),
({"pantyhose": 0.95, "zettai_ryouiki": 0.8}, "Voyeur/legwear"),
({"undressing": 0.9, "open_clothes": 0.7}, "Voyeur/undressing"),
({"see-through": 0.9, "nipples": 0.6}, "Voyeur/see_through"),
({"exhibitionism": 0.9}, "Voyeur/public"),
({"cameltoe": 0.9}, "Voyeur"),
# see-through needs body exposure; crystalline characters must not match
({"see-through": 0.85, "androgynous": 0.9, "other_focus": 0.93}, None),
# New act folders
({"sex": 0.95, "vaginal": 0.9, "hetero": 0.99}, "sex"),
({"paizuri": 0.9, "breasts": 0.99}, "paizuri"),
({"footjob": 0.9, "feet": 0.99}, "footjob"),
({"tentacle_sex": 0.9, "tentacles": 0.85}, "tentacles"),
({"consensual_tentacles": 0.88}, "tentacles"),
({"tentacles": 0.95}, None),
({"pussy": 0.9, "fingering": 0.9}, "sex"),
({"sex": 0.95, "fellatio": 0.7}, "fellatio"),
({"sex": 0.95, "cum_in_pussy": 0.6}, "nakadashi"),
({"sex": 0.95, "tentacle_sex": 0.6}, "tentacles"),
],
)
def test_taxonomy_routing_matrix(scores: dict[str, float], expected: str | None) -> None:
folder, _score, _secondary = choose_best_destination(scores, SELECTED_ALL)
assert folder == expected
@pytest.mark.parametrize(
"scores,expected",
[
({"1girl": 0.99, "solo": 0.98, "smile": 0.9}, "SFW"),
({"comic": 0.9, "speech_bubble": 0.8, "1girl": 0.85}, "comic"),
({"scenery": 0.8, "no_humans": 0.95}, "scenery"),
# Fallbacks yield to any real destination.
({"1girl": 0.99, "pantyshot": 0.4}, "Voyeur/panties"),
({"1girl": 0.99, "loli": 0.3}, "loli"),
({"1girl": 0.99, "nipples": 0.9}, "Voyeur/nude"),
# Explicit content with no destination goes to review, never to SFW.
({"1girl": 0.99, "censored": 0.6, "penis": 0.2}, None),
],
)
def test_fallback_routing_matrix(scores: dict[str, float], expected: str | None) -> None:
folder, _score, _secondary = choose_best_destination(
scores, SELECTED_ALL | SELECTED_FALLBACKS
)
assert folder == expected
def test_classify_gate_clears_weak_taxonomy_winner(tmp_path: Path) -> None:
result = _classify_from_scores(
tmp_path / "x.jpg",
{"loli": 0.2, "1girl": 0.99},
{"loli", "shota"},
confidence_threshold=0.6,
)
assert result.primary_tag is None
assert result.needs_review is True
assert result.secondary[0]["tag"] == "loli"
def test_classify_keeps_mid_character_primary_for_review(tmp_path: Path) -> None:
"""Character hits below confidence stay as primary (needs_review), not cleared."""
result = _classify_from_scores(
tmp_path / "x.jpg",
{"loli": 0.42, "1girl": 0.99},
{"loli", "shota", "NTR", "fellatio"},
confidence_threshold=0.6,
)
assert result.primary_tag == "loli"
assert result.primary_score == 0.42
assert result.needs_review is True
assert "threshold" in (result.reason or "").lower()
def test_classify_still_clears_mid_act_primary(tmp_path: Path) -> None:
result = _classify_from_scores(
tmp_path / "x.jpg",
{"fellatio": 0.42, "1girl": 0.99},
{"loli", "fellatio"},
confidence_threshold=0.6,
)
assert result.primary_tag is None
assert result.needs_review is True
assert result.secondary[0]["tag"] == "fellatio"
def test_classify_assigns_strong_taxonomy_winner(tmp_path: Path) -> None:
result = _classify_from_scores(
tmp_path / "x.jpg",
{"loli": 0.91, "flat_chest": 0.95, "shota": 0.4},
{"loli", "shota", "furry"},
confidence_threshold=0.6,
)
assert result.primary_tag == "loli"
assert result.primary_score == 0.91
assert result.needs_review is False
def test_priority_fertilization_over_nakadashi_on_tie(tmp_path: Path) -> None:
result = _classify_from_scores(
tmp_path / "x.jpg",
{"fertilization": 0.8, "internal_cumshot": 0.8},
{"fertilization", "nakadashi"},
confidence_threshold=0.6,
)
assert result.primary_tag == "fertilization"
def test_model_smoke_nonempty_scores_all_taggers(tmp_path: Path) -> None:
"""Smoke: each tagger returns a non-empty score dict on a tiny RGB image.
This is not semantic accuracy (no labels); it catches broken sessions /
empty-output regressions across models.
"""
pytest.importorskip("imgutils")
from app.services import extract_scores
img = tmp_path / "probe.png"
Image.new("RGB", (448, 448), color=(180, 120, 160)).save(img)
models = ["ml_danbooru", "wd_swinv2_v3", "wd_eva02_large"]
for model in models:
try:
scores = extract_scores(img, tagger_model=model, wd_general_threshold=0.35)
except Exception as err: # pragma: no cover - environment-specific
pytest.skip(f"tagger {model} unavailable: {err}")
assert isinstance(scores, dict)
assert len(scores) > 0, f"{model} returned empty scores"
assert all(0.0 <= float(v) <= 1.0 for v in scores.values())
|