diff --git a/frontend/src/views/MediaList.vue b/frontend/src/views/MediaList.vue index b02913f..4f9b3ba 100644 --- a/frontend/src/views/MediaList.vue +++ b/frontend/src/views/MediaList.vue @@ -118,6 +118,9 @@ 安全
{{ formatAnimeRatings(scope.row.animeNsfwRatings) }}
+
+ 安全 +
- diff --git a/internal/anime_nsfw/server.py b/internal/anime_nsfw/server.py index a0609d6..38116be 100644 --- a/internal/anime_nsfw/server.py +++ b/internal/anime_nsfw/server.py @@ -272,16 +272,64 @@ class Handler(BaseHTTPRequestHandler): except Exception as exc: return self._send({"ok": False, "error": str(exc)}) - # NSFW 判定:explicit 或 questionable 分数 >= 阈值,且必须高于 general。 - # WD Tagger 的 rating 是独立 sigmoid(不归一化),非动漫图四个分值往往 - # 都很接近;若 general 最高说明模型认为是安全图,只看绝对值会大量误判。 + # NSFW 判定逻辑: + # WD Tagger v3 的 rating 是独立 sigmoid(不归一化),四个值可以同时很高。 + # 对于某些动漫图,rating 没有区分度(都在 0.5-0.6 之间)。 + # + # 判定策略: + # 1. 检查 top_tags 中是否有明确的色情标签(nude、genitals 等) + # 2. 如果有,判定为 NSFW + # 3. 否则,用 rating 辅助判断(explicit - general >= 0.1) + ratings = result["ratings"] - nsfw_raw = max(ratings["explicit"], ratings["questionable"]) - if nsfw_raw > ratings["general"]: - nsfw_score = nsfw_raw - nsfw = nsfw_score >= threshold + top_tags = result["top_tags"] + explicit = ratings["explicit"] + questionable = ratings["questionable"] + general = ratings["general"] + + # 打印 top_tags 到日志(调试用) + if top_tags: + tag_str = ", ".join([f"{t['tag']}:{t['score']:.2f}" for t in top_tags[:5]]) + print(f"=== AnimeNSFW 标签: {tag_str}", flush=True) + + # 定义明确的色情标签 + NSFW_TAGS = { + "nude", "naked", "genitals", "penis", "vagina", "breasts", + "nipples", "areola", "pubic_hair", "anus", "testicles", + "erection", "ejaculation", "orgasm", "masturbation", + "fellatio", "cunnilingus", "intercourse", "sex", + "pussy", "cock", "dick", "boobs", "tits", "ass", + "butt", "thighs", "legs", "feet", "toes", + } + + # 检查 top_tags 中是否有明确的色情标签 + nsfw_by_tags = False + nsfw_tag_score = 0.0 + for tag_info in top_tags: + tag_name = tag_info["tag"].lower() + tag_score = tag_info["score"] + # 检查标签名是否包含色情关键词 + for nsfw_keyword in NSFW_TAGS: + if nsfw_keyword in tag_name and tag_score > 0.5: + nsfw_by_tags = True + nsfw_tag_score = max(nsfw_tag_score, tag_score) + break + if nsfw_by_tags: + break + + # 判定逻辑 + # nsfw_score 始终返回 max(explicit, questionable) 的最高分, + # 这样 Go 侧可以通过 score > 0 判断检测器是否运行过。 + # nsfw 标记才表示是否真正判定为色情。 + nsfw_score = max(explicit, questionable) + + if nsfw_by_tags: + # 标签检测到明确的色情内容 + nsfw = True + elif nsfw_score >= threshold and (nsfw_score - general) >= 0.1: + # rating 辅助判断:最高分 >= 阈值 且 明显高于 general + nsfw = True else: - nsfw_score = 0.0 nsfw = False self._send({ diff --git a/internal/organize/organize.go b/internal/organize/organize.go index 274cfaa..7370621 100644 --- a/internal/organize/organize.go +++ b/internal/organize/organize.go @@ -609,7 +609,7 @@ func (o *Organizer) ProcessImages(images []scanner.File) { } } } - if info != nil && r.animeNsfwScore > 0 { + if info != nil && (r.animeNsfwScore > 0 || r.animeNsfwRatings.General > 0) { info.AnimeNsfwScore = r.animeNsfwScore info.AnimeNsfwThreshold = o.animeNsfw.Threshold() if ratingsJSON, err := json.Marshal(r.animeNsfwRatings); err == nil {