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
+16 -46
View File
@@ -83,9 +83,7 @@ type Organizer struct {
resumeSkip int32 // 本次续传跳过的文件数(并行累计)
pendingScan []store.ScanLog // 待写入续传表的记录(攒一批再批量写)
nsfwChecked int32 // 色情识别已检查的图片数(并行累计,只涨不跌)
nsfwHits int32 // 色情识别命中的图片数(并行累计)
animeNsfwChecked int32 // 动漫色情识别已检查的图片数
animeNsfwHits int32 // 动漫色情识别命中的图片数
// 阶段耗时统计(并行 worker 累计,单位纳秒,只用于日志诊断瓶颈)
tImageProbe int64 // 图片尺寸探测总耗时
tImgMd5 int64 // 图片 MD5/指纹计算总耗时
@@ -405,10 +403,8 @@ type imageProbeResult struct {
darkScore float64 // 暗色像素占比(用于亮度分类)
lightScore float64 // 亮色像素占比
nsfwScore float64 // 色情检测最高分(未检测为 0)
nsfw bool // 是否判定为色情(检测可用时才可能为 true)
nsfwDetections []nsfw.NsfwDetection // NudeNet 检测详情
animeNsfwScore float64 // 动漫色情检测最高分(未检测为 0)
animeNsfw bool // 是否判定为动漫色情
animeNsfwRatings anime_nsfw.AnimeRatings // WD Tagger 四个 rating 的原始分数
}
@@ -465,48 +461,36 @@ func (o *Organizer) probeImage(f scanner.File) imageProbeResult {
// 色情识别(GIF 不做;没启用检测器就直接跳过)
if o.nsfw != nil && !strings.EqualFold(f.Ext, ".gif") {
nsfwStart := time.Now()
score, hit, detections, derr := o.nsfw.Detect(f.Path)
score, detections, derr := o.nsfw.Detect(f.Path)
atomic.AddInt64(&o.tNsfw, int64(time.Since(nsfwStart)))
if derr != nil {
// 检测失败按"未命中"处理,不影响整理流程
fmt.Printf("=== 色情检测失败(按未命中归类): %s, 错误: %v \n", f.Path, derr)
// 检测失败不影响整理流程
fmt.Printf("=== 色情检测失败: %s, 错误: %v \n", f.Path, derr)
} else {
r.nsfwScore = score
r.nsfw = hit
r.nsfwDetections = detections
checked := atomic.AddInt32(&o.nsfwChecked, 1)
if hit {
atomic.AddInt32(&o.nsfwHits, 1)
fmt.Printf("=== 色情命中: %s, 检测分=%.4f (阈值=%.4f)\n", f.Path, score, o.nsfw.Threshold())
} else {
fmt.Printf("=== 色情未命中: %s, 检测分=%.4f (阈值=%.4f)\n", f.Path, score, o.nsfw.Threshold())
}
fmt.Printf("=== 色情检测: %s, 评分=%.4f\n", f.Path, score)
// 每固定张数打一行进度,让前端能看到识别在推进(不逐张刷屏)
if checked%nsfwProgressInterval == 0 {
fmt.Printf("=== 色情识别进度: 已检查 %d 张, 命中 %d 张\n", checked, atomic.LoadInt32(&o.nsfwHits))
fmt.Printf("=== 色情识别进度: 已检查 %d 张\n", checked)
}
}
}
// 动漫色情识别(GIF 不做;没启用检测器就直接跳过)
if o.animeNsfw != nil && !strings.EqualFold(f.Ext, ".gif") {
animeStart := time.Now()
score, hit, ratings, derr := o.animeNsfw.Detect(f.Path)
score, ratings, derr := o.animeNsfw.Detect(f.Path)
atomic.AddInt64(&o.tAnimeNsfw, int64(time.Since(animeStart)))
if derr != nil {
fmt.Printf("=== 动漫色情检测失败(按未命中归类): %s, 错误: %v \n", f.Path, derr)
fmt.Printf("=== 动漫色情检测失败: %s, 错误: %v \n", f.Path, derr)
} else {
r.animeNsfwScore = score
r.animeNsfw = hit
r.animeNsfwRatings = ratings
checked := atomic.AddInt32(&o.animeNsfwChecked, 1)
if hit {
atomic.AddInt32(&o.animeNsfwHits, 1)
fmt.Printf("=== 动漫色情命中: %s, 检测分=%.4f (阈值=%.4f)\n", f.Path, score, o.animeNsfw.Threshold())
} else {
fmt.Printf("=== 动漫色情未命中: %s, 检测分=%.4f (阈值=%.4f)\n", f.Path, score, o.animeNsfw.Threshold())
}
fmt.Printf("=== 动漫色情检测: %s, 评分=%.4f\n", f.Path, score)
if checked%nsfwProgressInterval == 0 {
fmt.Printf("=== 动漫色情识别进度: 已检查 %d 张, 命中 %d 张\n", checked, atomic.LoadInt32(&o.animeNsfwHits))
fmt.Printf("=== 动漫色情识别进度: 已检查 %d 张\n", checked)
}
}
}
@@ -554,7 +538,6 @@ func (o *Organizer) correctImageRotation(f scanner.File, res *probe.ProbeResult)
func (o *Organizer) ProcessImages(images []scanner.File) {
// 统计结果,最后打一行总结给用户看
success, fail, ignore, dup := 0, 0, 0, 0
nsfwCount := 0 // 命中的色情图片数
total := len(images)
phaseStart := time.Now()
if o.nsfw != nil {
@@ -596,13 +579,11 @@ func (o *Organizer) ProcessImages(images []scanner.File) {
dup++
fmt.Printf("=== 重复图片(同路径), 忽略: %s \n", f.Path)
default:
// 正常图片:按分辨率/方向/明暗归入对应的分类目录;
// 被判为色情的改去「图片_敏感_*」目录,明暗子目录照常保留
// 正常图片:按分辨率/方向/明暗/色情评分归入对应的分类目录
info := o.record(f, "image", r.res, r.md5Str, r.headMd5, r.tailMd5, r.size)
// 检测过的图片都记录分数(不管是否命中阈值),方便后续调整阈值重新判定
// 检测过的图片都记录分数
if info != nil && r.nsfwScore > 0 {
info.NsfwScore = r.nsfwScore
info.NsfwThreshold = o.nsfw.Threshold()
if len(r.nsfwDetections) > 0 {
if detJSON, err := json.Marshal(r.nsfwDetections); err == nil {
info.NsfwDetections = string(detJSON)
@@ -611,20 +592,16 @@ func (o *Organizer) ProcessImages(images []scanner.File) {
}
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 {
info.AnimeNsfwRatings = string(ratingsJSON)
}
}
// 真人色情或动漫色情任一命中,都归入敏感目录
if r.nsfw || r.animeNsfw {
o.addClassified(f, classify.ImageDirSensitive(f.Ext, r.res.Width, r.res.Height, r.darkScore, r.lightScore), r.size, r.headMd5, r.tailMd5)
if r.nsfw {
nsfwCount++
}
} else {
o.addClassified(f, classify.ImageDir(f.Ext, r.res.Width, r.res.Height, r.darkScore, r.lightScore), r.size, r.headMd5, r.tailMd5)
nsfwEnabled := o.nsfw != nil || o.animeNsfw != nil
nsfwScore := r.nsfwScore
if r.animeNsfwScore > nsfwScore {
nsfwScore = r.animeNsfwScore
}
o.addClassified(f, classify.ImageDir(f.Ext, r.res.Width, r.res.Height, r.darkScore, r.lightScore, nsfwScore, nsfwEnabled), r.size, r.headMd5, r.tailMd5)
success++
}
}
@@ -641,13 +618,6 @@ func (o *Organizer) ProcessImages(images []scanner.File) {
time.Duration(atomic.LoadInt64(&o.tBright)).Seconds(),
time.Duration(atomic.LoadInt64(&o.tNsfw)).Seconds(),
time.Duration(atomic.LoadInt64(&o.tAnimeNsfw)).Seconds())
animeNsfwHits := atomic.LoadInt32(&o.animeNsfwHits)
if o.nsfw != nil && nsfwCount > 0 {
fmt.Printf("=== 色情识别: 共命中 %d 张,已归入「图片_敏感_*」目录,可在【媒体库】页面查看并通过<色情标记>人工复核\n", nsfwCount)
}
if o.animeNsfw != nil && animeNsfwHits > 0 {
fmt.Printf("=== 动漫色情识别: 共命中 %d 张,已归入「图片_敏感_*」目录,可在【媒体库】页面查看并通过<色情标记>人工复核\n", animeNsfwHits)
}
}
// ============================ 视频/音频整理 ============================