feat: 目录层级变更
This commit is contained in:
@@ -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) != "" {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user