diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 1a0e19d..b5b9715 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -24,6 +24,10 @@
统计看板
+
+
+ 系统配置
+
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index bdff5c0..931450b 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -3,6 +3,7 @@ import Router from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import MediaList from '../views/MediaList.vue'
import Scan from '../views/Scan.vue'
+import Settings from '../views/Settings.vue'
Vue.use(Router)
@@ -11,6 +12,7 @@ export default new Router({
{path: '/', redirect: '/scan'},
{path: '/dashboard', component: Dashboard},
{path: '/media', component: MediaList},
- {path: '/scan', component: Scan}
+ {path: '/scan', component: Scan},
+ {path: '/settings', component: Settings}
]
})
diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue
new file mode 100644
index 0000000..a9f92b7
--- /dev/null
+++ b/frontend/src/views/Settings.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+ 关闭
+ 人类 (NudeNet)
+ 二次元 (WD Tagger)
+
+
+ 不启用色情检测
+ 使用 NudeNet 检测真人色情内容
+ 使用 WD Tagger v3 检测二次元色情内容
+
+
+
+
+
+ 分数高于此值判定为色情(推荐 0.2-0.4)
+
+
+
+
+ explicit/questionable 分数高于此值判定为色情(推荐 0.5-0.7)
+
+
+
+ 保存配置
+
+
+
+
+
+
+
+
+
diff --git a/internal/api/config_controller.go b/internal/api/config_controller.go
new file mode 100644
index 0000000..45ec747
--- /dev/null
+++ b/internal/api/config_controller.go
@@ -0,0 +1,106 @@
+// Package api 配置相关接口。
+package api
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+
+ "kenaito-media-picker/internal/store"
+)
+
+// 配置键名(存储在 AppConfig 表中)
+const (
+ ConfigKeyNsfwType = "nsfw.type" // 检测类型:"" / "human" / "anime"
+ ConfigKeyNsfwThreshold = "nsfw.threshold" // 人类检测阈值
+ ConfigKeyAnimeNsfwThreshold = "anime.threshold" // 二次元检测阈值
+)
+
+// ConfigController 配置相关接口。
+type ConfigController struct {
+ store *store.Store
+}
+
+// NewConfigController 创建配置控制器。
+func NewConfigController(st *store.Store) *ConfigController {
+ return &ConfigController{store: st}
+}
+
+// NsfwConfig NSFW 配置响应。
+type NsfwConfig struct {
+ Type string `json:"type"` // 检测类型:"" / "human" / "anime"
+ HumanThreshold float64 `json:"humanThreshold"` // 人类检测阈值
+ AnimeThreshold float64 `json:"animeThreshold"` // 二次元检测阈值
+}
+
+// GetNsfwConfig 处理 GET /api/config/nsfw:获取 NSFW 配置。
+func (c *ConfigController) GetNsfwConfig(ctx *gin.Context) {
+ nsfwType := c.store.GetConfig(ConfigKeyNsfwType)
+
+ humanThreshold := 0.5
+ if v := c.store.GetConfig(ConfigKeyNsfwThreshold); v != "" {
+ if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 {
+ humanThreshold = t
+ }
+ }
+
+ animeThreshold := 0.5
+ if v := c.store.GetConfig(ConfigKeyAnimeNsfwThreshold); v != "" {
+ if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 {
+ animeThreshold = t
+ }
+ }
+
+ Success(ctx, NsfwConfig{
+ Type: nsfwType,
+ HumanThreshold: humanThreshold,
+ AnimeThreshold: animeThreshold,
+ })
+}
+
+// UpdateNsfwConfigReq 更新 NSFW 配置请求。
+type UpdateNsfwConfigReq struct {
+ Type string `json:"type"` // 检测类型:"" / "human" / "anime"
+ HumanThreshold float64 `json:"humanThreshold"` // 人类检测阈值
+ AnimeThreshold float64 `json:"animeThreshold"` // 二次元检测阈值
+}
+
+// UpdateNsfwConfig 处理 PUT /api/config/nsfw:更新 NSFW 配置。
+func (c *ConfigController) UpdateNsfwConfig(ctx *gin.Context) {
+ var req UpdateNsfwConfigReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ Error(ctx, http.StatusBadRequest, "请求参数错误:"+err.Error())
+ return
+ }
+
+ // 校验检测类型
+ if req.Type != "" && req.Type != "human" && req.Type != "anime" {
+ Error(ctx, http.StatusBadRequest, "检测类型必须是空、human 或 anime")
+ return
+ }
+
+ // 校验阈值范围
+ if req.HumanThreshold <= 0 || req.HumanThreshold >= 1 {
+ req.HumanThreshold = 0.5
+ }
+ if req.AnimeThreshold <= 0 || req.AnimeThreshold >= 1 {
+ req.AnimeThreshold = 0.5
+ }
+
+ // 保存配置
+ if err := c.store.SaveConfig(ConfigKeyNsfwType, req.Type); err != nil {
+ Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error())
+ return
+ }
+ if err := c.store.SaveConfig(ConfigKeyNsfwThreshold, strconv.FormatFloat(req.HumanThreshold, 'f', 2, 64)); err != nil {
+ Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error())
+ return
+ }
+ if err := c.store.SaveConfig(ConfigKeyAnimeNsfwThreshold, strconv.FormatFloat(req.AnimeThreshold, 'f', 2, 64)); err != nil {
+ Error(ctx, http.StatusInternalServerError, "保存配置失败:"+err.Error())
+ return
+ }
+
+ Success(ctx, nil)
+}
diff --git a/internal/api/router.go b/internal/api/router.go
index 2d602fa..e417090 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -47,6 +47,11 @@ func NewRouter(st *store.Store) *gin.Engine {
// 文件目录浏览(页面上"选择目录")
fc := NewFsController()
apiGroup.GET("/fs/list", fc.List)
+
+ // 系统配置
+ cc := NewConfigController(st)
+ apiGroup.GET("/config/nsfw", cc.GetNsfwConfig)
+ apiGroup.PUT("/config/nsfw", cc.UpdateNsfwConfig)
}
return r
}
diff --git a/internal/service/scan.go b/internal/service/scan.go
index 36526c3..4e9136a 100644
--- a/internal/service/scan.go
+++ b/internal/service/scan.go
@@ -19,6 +19,7 @@ import (
"path/filepath"
"regexp"
"runtime/debug"
+ "strconv"
"strings"
"sync"
"time"
@@ -671,12 +672,20 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro
}
// ---------- 第五步(可选):色情识别 ----------
- // 用本机 python + NudeNet 起一个本地检测服务。没装 python/nudenet 时跳过,
- // 不影响图片整理——检测结果只决定走普通目录还是「图片_敏感」目录。
+ // 从数据库读取 NSFW 配置(支持运行时修改,立即生效)
+ nsfwType := s.store.GetConfig("nsfw.type") // "" / "human" / "anime"
+ pythonPath := config.GetAppConfig().NSFWPython
+
nsfwDet := (*nsfw.Detector)(nil)
s.setNsfwDetectorReady(false)
- if cfg := config.GetAppConfig(); cfg.NSFWEnabled {
- if d, err := nsfw.Start(cfg.NSFWPython, cfg.NSFWThreshold); err != nil {
+ if nsfwType == "human" {
+ threshold := 0.5
+ if v := s.store.GetConfig("nsfw.threshold"); v != "" {
+ if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 {
+ threshold = t
+ }
+ }
+ if d, err := nsfw.Start(pythonPath, threshold); err != nil {
fmt.Printf("=== 色情识别未启用(图片照常处理): %v\n", err)
} else {
nsfwDet = d
@@ -687,10 +696,15 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro
}
// ---------- 第六步(可选):动漫色情识别 ----------
- // 用本机 python + WD Tagger v3 起一个本地检测服务。没装 python/onnxruntime 时跳过。
animeNsfwDet := (*anime_nsfw.Detector)(nil)
- if cfg := config.GetAppConfig(); cfg.AnimeNSFWEnabled {
- if d, err := anime_nsfw.Start(cfg.NSFWPython, cfg.AnimeNSFWThreshold); err != nil {
+ if nsfwType == "anime" {
+ threshold := 0.5
+ if v := s.store.GetConfig("anime.threshold"); v != "" {
+ if t, err := strconv.ParseFloat(v, 64); err == nil && t > 0 && t < 1 {
+ threshold = t
+ }
+ }
+ if d, err := anime_nsfw.Start(pythonPath, threshold); err != nil {
fmt.Printf("=== 动漫色情识别未启用(图片照常处理): %v\n", err)
} else {
animeNsfwDet = d