feat: 二次元识别功能 + 模型目录整理(nwfs/wd-tagger 子目录),修复 wd-tagger 输入维度(NHWC)
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div class="toolbar">
|
||||
<span class="title">二次元识别</span>
|
||||
<div class="toolbar-right">
|
||||
<span class="hint" v-if="!hentaiEnabled">识别功能未开启:请在 application.properties 设 hentai.enabled=true 并重新扫描</span>
|
||||
<span class="hint" v-else-if="!hentaiDetector">识别已配置开启,但最近一次扫描时检测服务没起来:请确认本机已安装 python 和 onnxruntime/Pillow/numpy,再重新扫描</span>
|
||||
<el-button type="primary" size="small" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip-bar">
|
||||
<i class="el-icon-view"></i>
|
||||
这是最近一次开启二次元识别的扫描判出的图片,按检测分从高到低排列。点缩略图可放大预览,用来核对识别结果准不准。
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column label="预览" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image
|
||||
class="thumb"
|
||||
:src="previewUrl(scope.row.path)"
|
||||
:preview-src-list="[previewUrl(scope.row.path)]"
|
||||
fit="cover"
|
||||
lazy
|
||||
style="width: 84px; height: 84px; border-radius: 6px; border: 1px solid #e6efff">
|
||||
<div slot="error" class="thumb-error">
|
||||
<i class="el-icon-picture-outline"></i>
|
||||
</div>
|
||||
</el-image>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileName" label="文件名" min-width="140" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="file-name" :title="scope.row.path">{{ scope.row.fileName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分级" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="rating" :class="'rating-' + scope.row.rating">{{ ratingLabel(scope.row.rating) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检测分" width="160" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="score" :class="scoreTag(scope.row)">{{ (scope.row.score * 100).toFixed(1) }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="阈值" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="muted">{{ (scope.row.threshold * 100).toFixed(1) }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="tags-text">{{ formatTags(scope.row.topTags) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完整路径" min-width="240" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="path-text">{{ scope.row.path }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="检测时间" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="muted">{{ scope.row.createdAt }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty
|
||||
v-if="!loading && list.length === 0"
|
||||
:description="emptyDesc"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 二次元识别页面。
|
||||
* 展示最近一次开启二次元识别的扫描判出的图片:分级 + 检测分 + 标签 + 完整路径 + 图片预览。
|
||||
* 数据来自 /api/hentai/list,预览走 /api/hentai/preview(后端按路径读本地文件)。
|
||||
*/
|
||||
export default {
|
||||
name: 'HentaiList',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
hentaiEnabled: true, // 配置里是否开了 hentai.enabled
|
||||
hentaiDetector: true, // 最近一次扫描时检测服务是否真的起来了
|
||||
hentaiConfigured: true // 上两者都满足才算"识别可用"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 空列表时的提示文案
|
||||
emptyDesc() {
|
||||
if (this.hentaiConfigured) {
|
||||
return '识别已开启,最近一次扫描没有判出二次元敏感图片。'
|
||||
}
|
||||
return '暂无记录。开启 hentai.enabled 并确保检测服务可用后重新扫描,判为二次元敏感的图片会出现在这里。'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
async loadList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await request.get('/api/hentai/list')
|
||||
this.list = res.data.list || []
|
||||
// 后端真实上报:配置是否开启、检测服务上次是否真的起来了
|
||||
this.hentaiEnabled = res.data.hentaiEnabled !== false
|
||||
this.hentaiDetector = res.data.hentaiDetector === true
|
||||
this.hentaiConfigured = this.hentaiEnabled && this.hentaiDetector
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
previewUrl(path) {
|
||||
if (!path) return ''
|
||||
return '/api/hentai/preview?path=' + encodeURIComponent(path)
|
||||
},
|
||||
// 分级标签翻译
|
||||
ratingLabel(rating) {
|
||||
const map = {
|
||||
'safe': '安全',
|
||||
'questionable': '可疑',
|
||||
'explicit': '明确'
|
||||
}
|
||||
return map[rating] || rating || '未知'
|
||||
},
|
||||
// 检测分越接近 1 越"实锤":>=90% 红色,>=75% 橙色,否则灰色
|
||||
scoreTag(row) {
|
||||
if (row.score >= 0.9) return 'score-high'
|
||||
if (row.score >= 0.75) return 'score-mid'
|
||||
return ''
|
||||
},
|
||||
// 格式化标签:把 JSON 数组转成可读字符串
|
||||
formatTags(topTags) {
|
||||
if (!topTags) return ''
|
||||
try {
|
||||
const tags = JSON.parse(topTags)
|
||||
if (!Array.isArray(tags)) return ''
|
||||
return tags.map(t => `${t.tag}(${(t.score * 100).toFixed(0)}%)`).join(', ')
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #e6a23c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid #e6efff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
color: #4a5a78;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tip-bar .el-icon-view {
|
||||
color: #1677ff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.thumb-error {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
font-size: 26px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #1f3a6b;
|
||||
}
|
||||
|
||||
.path-text {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tags-text {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rating {
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rating-safe {
|
||||
color: #67c23a;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.rating-questionable {
|
||||
color: #e6a23c;
|
||||
background: #fdf6ec;
|
||||
}
|
||||
|
||||
.rating-explicit {
|
||||
color: #f56c6c;
|
||||
background: #fef0f0;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-weight: 700;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.score-high {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.score-mid {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user