upd
This commit is contained in:
@@ -24,6 +24,10 @@
|
||||
<i class="el-icon-data-line"></i>
|
||||
<span slot="title">统计看板</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings">
|
||||
<i class="el-icon-setting"></i>
|
||||
<span slot="title">系统配置</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<div class="aside-footer">kenaito-media-picker</div>
|
||||
</el-aside>
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span>色情检测配置</span>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" label-width="120px" class="settings-form">
|
||||
<el-form-item label="检测类型">
|
||||
<el-radio-group v-model="form.type" @change="handleTypeChange">
|
||||
<el-radio label="">关闭</el-radio>
|
||||
<el-radio label="human">人类 (NudeNet)</el-radio>
|
||||
<el-radio label="anime">二次元 (WD Tagger)</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">
|
||||
<span v-if="form.type === ''">不启用色情检测</span>
|
||||
<span v-else-if="form.type === 'human'">使用 NudeNet 检测真人色情内容</span>
|
||||
<span v-else-if="form.type === 'anime'">使用 WD Tagger v3 检测二次元色情内容</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === 'human'" label="检测阈值">
|
||||
<el-slider v-model="form.humanThreshold" :min="0.1" :max="0.9" :step="0.05" show-input></el-slider>
|
||||
<div class="form-tip">分数高于此值判定为色情(推荐 0.2-0.4)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === 'anime'" label="检测阈值">
|
||||
<el-slider v-model="form.animeThreshold" :min="0.1" :max="0.9" :step="0.05" show-input></el-slider>
|
||||
<div class="form-tip">explicit/questionable 分数高于此值判定为色情(推荐 0.5-0.7)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveConfig" :loading="saving">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
type: '',
|
||||
humanThreshold: 0.5,
|
||||
animeThreshold: 0.5
|
||||
},
|
||||
saving: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadConfig()
|
||||
},
|
||||
methods: {
|
||||
async loadConfig() {
|
||||
try {
|
||||
const res = await request.get('/api/config/nsfw')
|
||||
if (res.data) {
|
||||
this.form.type = res.data.type || ''
|
||||
this.form.humanThreshold = res.data.humanThreshold || 0.5
|
||||
this.form.animeThreshold = res.data.animeThreshold || 0.5
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载配置失败')
|
||||
}
|
||||
},
|
||||
handleTypeChange() {
|
||||
// 切换类型时不需要特殊处理
|
||||
},
|
||||
async saveConfig() {
|
||||
this.saving = true
|
||||
try {
|
||||
await request.put('/api/config/nsfw', {
|
||||
type: this.form.type,
|
||||
humanThreshold: this.form.humanThreshold,
|
||||
animeThreshold: this.form.animeThreshold
|
||||
})
|
||||
this.$message.success('配置已保存,下次扫描生效')
|
||||
} catch (error) {
|
||||
this.$message.error('保存失败')
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user