feat: 添加动漫色情检测(WD Tagger v3),媒体库展示优化

- 新增 anime_nsfw 模块:WD Tagger v3 模型检测动漫/二次元图片

- 修复误判:当 general 评分最高时判定为安全,避免非动漫图误判

- 数据库存储四个 rating 原始分数(JSON),无需重扫即可查看

- 媒体库新增动漫色情检测列、文件路径列(支持一键复制)

- 修复预览图加载占位符显示两个点的问题

- 移动文件按钮不再限制扫描完成后才显示
This commit is contained in:
2025-08-24 21:14:04 +08:00
parent bd84bc98e8
commit d6798ef1ee
12 changed files with 934 additions and 31 deletions
+36
View File
@@ -49,6 +49,9 @@ type MediaInfo struct {
TailMd5 string `xorm:"char(32) index('idx_fp') 'tail_md5'" json:"tailMd5"` // 文件尾 256KB 的 MD5(指纹用)
NsfwScore float64 `xorm:"real 'nsfw_score'" json:"nsfwScore"` // NSFW 检测分 0~1(0 表示未检测或非图片)
NsfwThreshold float64 `xorm:"real 'nsfw_threshold'" json:"nsfwThreshold"` // NSFW 判定阈值
AnimeNsfwScore float64 `xorm:"real 'anime_nsfw_score'" json:"animeNsfwScore"` // 动漫 NSFW 检测分 0~1(0 表示未检测或非动漫)
AnimeNsfwThreshold float64 `xorm:"real 'anime_nsfw_threshold'" json:"animeNsfwThreshold"` // 动漫 NSFW 判定阈值
AnimeNsfwRatings string `xorm:"text 'anime_nsfw_ratings'" json:"animeNsfwRatings"` // WD Tagger 四个 rating 的 JSON(G/S/Q/E)
ManualNsfw string `xorm:"varchar(1) 'manual_nsfw'" json:"manualNsfw"` // 手动标记是否色情:Y/N,默认 N
CreatedAt time.Time `xorm:"created 'created_at'" json:"createdAt"` // 首次入库时间
UpdatedAt time.Time `xorm:"updated 'updated_at'" json:"updatedAt"` // 最近更新时间
@@ -190,6 +193,18 @@ func (s *Store) SaveScanLogs(entries []ScanLog) error {
return sess.Commit()
}
// HasScanLog 检查是否有扫描记录(用于判断是否可以直接移动文件)。
func (s *Store) HasScanLog() (bool, error) {
if s == nil || s.engine == nil {
return false, nil
}
total, err := s.engine.Count(new(ScanLog))
if err != nil {
return false, err
}
return total > 0, nil
}
// ClearScanLog 清空全部续传记录。移动完成后调用。
func (s *Store) ClearScanLog() error {
if s == nil || s.engine == nil {
@@ -220,6 +235,27 @@ func (s *Store) UpdateMediaFilePaths(moved map[string]string) error {
return sess.Commit()
}
// AvgManualNsfwScore 计算 manual_nsfw='Y' 的记录的 nsfw_score 均值。
func (s *Store) AvgManualNsfwScore() (float64, error) {
if s == nil || s.engine == nil {
return 0, nil
}
var result struct {
Avg float64 `xorm:"avg"`
}
has, err := s.engine.Table("media_info").
Select("AVG(nsfw_score) as avg").
Where("manual_nsfw = 'Y'").
Get(&result)
if err != nil {
return 0, err
}
if !has {
return 0, nil
}
return result.Avg, nil
}
// UpdateManualNsfw 更新单条记录的手动色情标记。
func (s *Store) UpdateManualNsfw(id int64, value string) error {
if s == nil || s.engine == nil {