1
This commit is contained in:
@@ -62,15 +62,27 @@ const addVisible = ref(false)
|
||||
const addMode = ref('upload') // upload | ai
|
||||
const uploadList = ref([])
|
||||
const uploading = ref(false)
|
||||
const genForm = reactive({ prompt: '', count: 1, size: '704x1248' })
|
||||
const genForm = reactive({ species: '', count: 1, size: '704x1248', animalCount: 3, distanceMin: 20, distanceMax: 30 })
|
||||
const speciesOptions = ref([])
|
||||
const generating = ref(false)
|
||||
|
||||
// 生成表单选项(物种池/场景池来自 config,随配置热更新)
|
||||
function loadGenOptions() {
|
||||
request.get('/datasets/gen-options', { params: { datasetId: datasetId.value } }).then((d) => {
|
||||
speciesOptions.value = d.species || []
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
addMode.value = 'upload'
|
||||
uploadList.value = []
|
||||
genForm.prompt = ''
|
||||
genForm.species = ''
|
||||
loadGenOptions()
|
||||
genForm.count = 1
|
||||
genForm.size = '704x1248'
|
||||
genForm.animalCount = 3
|
||||
genForm.distanceMin = 20
|
||||
genForm.distanceMax = 30
|
||||
addVisible.value = true
|
||||
}
|
||||
|
||||
@@ -98,17 +110,20 @@ function submitUpload() {
|
||||
}
|
||||
|
||||
function submitGen() {
|
||||
if (!genForm.prompt.trim() || genForm.prompt.trim().length < 5) {
|
||||
ElMessage.warning('请输入提示词(至少 5 个字符)')
|
||||
if (genForm.distanceMax < genForm.distanceMin) {
|
||||
ElMessage.warning('距离范围上限不能小于下限')
|
||||
return
|
||||
}
|
||||
generating.value = true
|
||||
request
|
||||
.post('/datasets/generate', {
|
||||
datasetId: datasetId.value,
|
||||
prompt: genForm.prompt.trim(),
|
||||
species: genForm.species.trim(),
|
||||
count: genForm.count,
|
||||
size: genForm.size,
|
||||
animalCount: genForm.animalCount,
|
||||
distanceMin: genForm.distanceMin,
|
||||
distanceMax: genForm.distanceMax,
|
||||
}, { timeout: 2400000 })
|
||||
.then((data) => {
|
||||
ElMessage.success(`已生成 ${data.generated || 0} 张(生成图片为付费资产,删除前请确认),自动标注处理中…`)
|
||||
@@ -170,6 +185,28 @@ function startFullLabelTask() {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// VLM 藏匿位补检(两阶段标注第二阶段):排除已确认框,按环境/光线/习性找可能藏匿处,追加疑似框
|
||||
const vlmReviewing = ref(false)
|
||||
async function vlmReview() {
|
||||
if (!currentImage.value) return
|
||||
const m = meta(currentImage.value)
|
||||
if (!m.id) return
|
||||
vlmReviewing.value = true
|
||||
try {
|
||||
const d = await request.post('/datasets/images/vlm-review', { datasetId: datasetId.value, imageId: m.id }, { timeout: 300000 })
|
||||
if (d.added > 0) {
|
||||
ElMessage.success(`VLM 补检新增 ${d.added} 个疑似框(黄框,请人工确认)`)
|
||||
} else {
|
||||
ElMessage.info(d.note || 'VLM 未发现可能的藏匿位')
|
||||
}
|
||||
dirty.value = false
|
||||
await loadAll()
|
||||
nextTick(resetEditor)
|
||||
} finally {
|
||||
vlmReviewing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单张图片(标注随图片删除,后端级联)
|
||||
function removeCardImage(item) {
|
||||
const m = meta(item)
|
||||
@@ -619,21 +656,38 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<el-form v-else label-width="90px">
|
||||
<el-form-item label="提示词">
|
||||
<el-input
|
||||
v-model="genForm.prompt"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="描述拍摄场景与目标外观,例如:户外田野背景,一只色彩鲜艳的野生雉鸡站立在草丛中,全身清晰可见。注意:不要描述目标在画面中的位置"
|
||||
/>
|
||||
<el-form-item label="动物名称">
|
||||
<el-select
|
||||
v-model="genForm.species"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="留空按数据集物种池随机"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="s in speciesOptions" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生成张数">
|
||||
<el-input-number v-model="genForm.count" :min="1" :max="8" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标数量">
|
||||
<div class="gen-inline">
|
||||
<el-input-number v-model="genForm.animalCount" :min="1" :max="20" />
|
||||
<span class="gen-note">画面中目标必须正好是该数量(提示词中的数量写法应与此一致),不符自动重新生成;标注框数不超过该数量</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="距离范围">
|
||||
<div class="gen-inline">
|
||||
<el-input-number v-model="genForm.distanceMin" :min="1" :max="500" />
|
||||
<span class="gen-note">至</span>
|
||||
<el-input-number v-model="genForm.distanceMax" :min="1" :max="500" />
|
||||
<span class="gen-note">米,逐张校验距离,不合格自动重新生成</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片尺寸">
|
||||
<span class="gen-size">竖版 704x1248(很慢,约 26 分钟/张,建议一次只生成 1 张)</span>
|
||||
<span class="gen-size">竖版 704x1248</span>
|
||||
</el-form-item>
|
||||
<div class="gen-tip">生成图片为付费资产,请勿随意删除;提示词中不要描述目标在画面中的位置(由模型自行构图)。</div>
|
||||
</el-form>
|
||||
@@ -672,6 +726,7 @@ onBeforeUnmount(() => {
|
||||
<el-radio-button :value="1"><i class="dot yellow" />疑似</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button size="small" :disabled="!confirmed.length" @click="clearAllBoxes">清空本张</el-button>
|
||||
<el-button size="small" type="warning" :loading="vlmReviewing" @click="vlmReview">VLM 补检</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="zoom-ctrl">
|
||||
<el-button size="small" circle :icon="Plus" @click="zoomIn" />
|
||||
@@ -825,6 +880,18 @@ onBeforeUnmount(() => {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* 生成表单行内组合(数量/距离范围 + 说明) */
|
||||
.gen-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gen-note {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* 上传方块:虚线边框 + 居中加号(el-upload picture-card 触发块) */
|
||||
.upload-tile {
|
||||
width: 100%;
|
||||
|
||||
@@ -15,11 +15,12 @@ const keyword = ref('')
|
||||
|
||||
const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
// 新建/编辑共用弹窗:edit 模式下名称只读(服务端 update 接口不支持改名)
|
||||
// 新建/编辑共用弹窗(改名/前缀/描述/封面;改名同步迁移磁盘目录)
|
||||
const dlgForm = reactive({
|
||||
mode: 'create', // create | edit
|
||||
id: 0,
|
||||
name: '',
|
||||
namePrefix: '',
|
||||
description: '',
|
||||
cover: '',
|
||||
})
|
||||
@@ -60,7 +61,7 @@ function resetCoverState() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(dlgForm, { mode: 'create', id: 0, name: '', description: '', cover: '' })
|
||||
Object.assign(dlgForm, { mode: 'create', id: 0, name: '', namePrefix: '', description: '', cover: '' })
|
||||
resetCoverState()
|
||||
coverFileList.value = []
|
||||
formVisible.value = true
|
||||
@@ -71,6 +72,7 @@ function openEdit(row) {
|
||||
mode: 'edit',
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
namePrefix: row.namePrefix || '',
|
||||
description: row.description || '',
|
||||
cover: row.cover || '',
|
||||
})
|
||||
@@ -111,7 +113,7 @@ async function submitDlg() {
|
||||
try {
|
||||
let id = dlgForm.id
|
||||
if (dlgForm.mode === 'create') {
|
||||
id = (await request.post('/datasets', { name, source: 'manual' })).id
|
||||
id = (await request.post('/datasets', { name, namePrefix: dlgForm.namePrefix.trim(), source: 'manual' })).id
|
||||
}
|
||||
if (coverFile.value) {
|
||||
const fd = new FormData()
|
||||
@@ -123,7 +125,7 @@ async function submitDlg() {
|
||||
}
|
||||
// create 且未填描述时跳过 update(服务端空值不覆盖,避免空跑一次)
|
||||
if (dlgForm.mode === 'edit' || dlgForm.description.trim()) {
|
||||
await request.post('/datasets/update', { id, name, description: dlgForm.description.trim(), cover: '' })
|
||||
await request.post('/datasets/update', { id, name, namePrefix: dlgForm.namePrefix.trim(), description: dlgForm.description.trim(), cover: '' })
|
||||
}
|
||||
ElMessage.success(dlgForm.mode === 'create' ? '数据集已创建,请上传或生成图片' : '已保存')
|
||||
formVisible.value = false
|
||||
@@ -266,10 +268,19 @@ onBeforeUnmount(() => {
|
||||
</el-button>
|
||||
<el-button size="small" :icon="EditPen" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="removeDataset(row)">删除</el-button>
|
||||
<!-- 训练成功且未发布过:发布按钮与其他操作同行(失败标签在下方状态行展示) -->
|
||||
<el-button
|
||||
v-if="row.trainingStatus === 'success' && row.trainingId && !row.trainingPublished"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="publishModel(row)"
|
||||
>
|
||||
发布模型
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 训练任务进度与状态(最新一条训练记录) -->
|
||||
<div v-if="row.trainingStatus" class="ds-train" @click.stop>
|
||||
<!-- 训练任务进度与状态(最新一条训练记录;成功态只留操作行按钮,不占独立行) -->
|
||||
<div v-if="row.trainingStatus && row.trainingStatus !== 'success'" class="ds-train" @click.stop>
|
||||
<template v-if="row.trainingStatus === 'running'">
|
||||
<el-progress :percentage="trainPercent(row)" :stroke-width="6" :show-text="false" class="ds-train-bar" />
|
||||
<div class="ds-train-text">
|
||||
@@ -282,13 +293,9 @@ onBeforeUnmount(() => {
|
||||
{{ trainStatusMap[row.trainingStatus] || row.trainingStatus }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<!-- 训练成功不显示「已训练」标签,只保留发布按钮 -->
|
||||
<el-tag v-else-if="row.trainingStatus !== 'success'" :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
|
||||
<el-tag v-else :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
|
||||
{{ trainStatusMap[row.trainingStatus] || row.trainingStatus }}
|
||||
</el-tag>
|
||||
<el-button v-if="row.trainingStatus === 'success' && row.trainingId" size="small" type="success" @click="publishModel(row)">
|
||||
发布模型
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,6 +323,9 @@ onBeforeUnmount(() => {
|
||||
placeholder="中文/字母/数字/下划线/短横线,唯一且作磁盘目录名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件名前缀">
|
||||
<el-input v-model="dlgForm.namePrefix" maxlength="50" placeholder="可选,如 pigeon:AI 生成图按 pigeon_01.jpg 顺序命名;留空用时间戳命名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="dlgForm.description" type="textarea" :rows="6" maxlength="500" show-word-limit placeholder="数据集说明,展示在卡片上" />
|
||||
</el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user