feat: 目录层级变更

This commit is contained in:
2025-09-24 21:19:55 +08:00
parent 7792f9281c
commit 72c9d67d9b
12 changed files with 156 additions and 317 deletions
+15 -27
View File
@@ -45,7 +45,6 @@ const requestTimeout = 60 * time.Second
// Detector 封装一个本地 python 服务的客户端。
type Detector struct {
url string
threshold float64
workers int
client *http.Client
cmd *exec.Cmd
@@ -60,10 +59,7 @@ type Detector struct {
}
// Start 拉起本地 python 检测服务并等它就绪。
func Start(pythonPath string, threshold float64, workers int) (*Detector, error) {
if threshold <= 0 || threshold >= 1 {
threshold = 0.5
}
func Start(pythonPath string, workers int) (*Detector, error) {
if workers <= 0 {
workers = 2 // 默认 2 个 worker
}
@@ -88,7 +84,6 @@ func Start(pythonPath string, threshold float64, workers int) (*Detector, error)
}
d := &Detector{
threshold: threshold,
workers: workers,
client: &http.Client{Timeout: requestTimeout},
python: py,
@@ -189,33 +184,32 @@ type AnimeRatings struct {
Explicit float64 `json:"explicit"`
}
// Detect 检测一张动漫图片是否色情。返回:
// Detect 检测一张动漫图片的色情评分。返回:
// - score:max(explicit, questionable) 的 sigmoid 分数(若 G 最高则为 0)
// - nsfw:是否判定为色情(分数 >= 阈值)
// - ratings:四个 rating 的原始分数
// - err:检测失败
func (d *Detector) Detect(path string) (score float64, nsfw bool, ratings AnimeRatings, err error) {
score, nsfw, ratings, err = d.detectOnce(path)
func (d *Detector) Detect(path string) (score float64, ratings AnimeRatings, err error) {
score, ratings, err = d.detectOnce(path)
if err == nil {
return score, nsfw, ratings, nil
return score, ratings, nil
}
var ce *connError
if !errors.As(err, &ce) {
return score, nsfw, ratings, err
return score, ratings, err
}
d.restartMu.Lock()
defer d.restartMu.Unlock()
if d.isStopped() {
return 0, false, ratings, err
return 0, ratings, err
}
if d.healthy() {
return d.detectOnce(path)
}
fmt.Printf("=== 动漫检测服务失联(%v), 正在重启…\n", ce.err)
if rerr := d.launch(); rerr != nil {
return 0, false, ratings, fmt.Errorf("动漫检测服务重启失败: %v", rerr)
return 0, ratings, fmt.Errorf("动漫检测服务重启失败: %v", rerr)
}
fmt.Println("=== 动漫检测服务已重启,继续识别")
return d.detectOnce(path)
@@ -242,24 +236,22 @@ func (d *Detector) isStopped() bool {
return d.stopped
}
func (d *Detector) detectOnce(path string) (score float64, nsfw bool, ratings AnimeRatings, err error) {
func (d *Detector) detectOnce(path string) (score float64, ratings AnimeRatings, err error) {
body, _ := json.Marshal(map[string]interface{}{
"path": path,
"threshold": d.threshold,
"path": path,
})
resp, err := d.client.Post(d.url+"/detect", "application/json", bytes.NewReader(body))
if err != nil {
return 0, false, ratings, &connError{err}
return 0, ratings, &connError{err}
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return 0, false, ratings, err
return 0, ratings, err
}
var out struct {
Ok bool `json:"ok"`
Error string `json:"error"`
Nsfw bool `json:"nsfw"`
Score float64 `json:"score"`
Ratings struct {
General float64 `json:"general"`
@@ -270,10 +262,10 @@ func (d *Detector) detectOnce(path string) (score float64, nsfw bool, ratings An
TopTags []map[string]interface{} `json:"top_tags"`
}
if err := json.Unmarshal(data, &out); err != nil {
return 0, false, ratings, fmt.Errorf("解析检测结果失败: %v", err)
return 0, ratings, fmt.Errorf("解析检测结果失败: %v", err)
}
if !out.Ok {
return 0, false, ratings, fmt.Errorf("检测服务返回错误: %s", out.Error)
return 0, ratings, fmt.Errorf("检测服务返回错误: %s", out.Error)
}
ratings = AnimeRatings{
General: out.Ratings.General,
@@ -284,7 +276,7 @@ func (d *Detector) detectOnce(path string) (score float64, nsfw bool, ratings An
// 打印 rating 详情(调试用)
fmt.Printf("=== AnimeNSFW 检测详情 %s: [G=%.3f S=%.3f Q=%.3f E=%.3f]\n",
path, ratings.General, ratings.Sensitive, ratings.Questionable, ratings.Explicit)
return out.Score, out.Nsfw, ratings, nil
return out.Score, ratings, nil
}
type connError struct{ err error }
@@ -305,10 +297,6 @@ func (d *Detector) Stop() {
}
}
func (d *Detector) Threshold() float64 {
return d.threshold
}
func resolvePython(configured string) string {
candidates := []string{}
if strings.TrimSpace(configured) != "" {
+5 -51
View File
@@ -265,76 +265,30 @@ class Handler(BaseHTTPRequestHandler):
try:
body = json.loads(self.rfile.read(length) or b"{}")
path = body.get("path", "")
threshold = float(body.get("threshold", 0.5))
if not path:
return self._send({"ok": False, "error": "缺少图片路径"})
result = _executor.submit(_infer, path).result()
except Exception as exc:
return self._send({"ok": False, "error": str(exc)})
# 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"]
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 = False
# nsfw_score 返回 max(explicit, questionable) 的最高分
nsfw_score = max(explicit, questionable)
self._send({
"ok": True,
"nsfw": nsfw,
"score": nsfw_score,
"ratings": ratings,
"top_tags": result["top_tags"],