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 {