综合训练(多物种合并模型)全链路:合并打包/独立版本序列/App 覆盖互斥 + 管理端发布入口

- 后端:/admin/trainings/combined 发起(≥2 数据集、类别重映射、防重名、负样本单份);
  model_training/model_version 加 kind+dataset_ids(迁移 v14),综合任务 dataset_id=0、
  文件基名 combined(_n)、版本序列独立;训练列表补 published 标记
- 管理端:数据训练页工具栏发起综合训练;横幅常驻进行中任务 + 每档最近一条已结束任务,
  成功未发布给「发布模型」入口(可关闭收起)
- App:目录解析 kind/datasetIds、激活覆盖互斥、自动更新退场改目标档待办横幅手动一键下载
This commit is contained in:
2026-09-09 15:45:51 +08:00
parent d8853ee2ab
commit 87ab06221c
17 changed files with 796 additions and 104 deletions
+183 -1
View File
@@ -1,7 +1,7 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { EditPen, Loading, Picture, Plus, VideoPlay } from '@element-plus/icons-vue'
import { EditPen, Loading, Picture, Plus, Share, VideoPlay } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import request from '../api/request'
import FalseTargets from './FalseTargets.vue'
@@ -267,6 +267,101 @@ function resetCoverState() {
coverDeleted.value = false
}
// ---------- 综合训练(2026-09-09):多数据集合并训练一个全类模型 ----------
const combinedDlg = ref(false)
const combinedIds = ref([]) // 勾选的数据集 id(≥2
const combinedVariants = ref(['s', 'n']) // 档位(s=高精度 n=高性能)
const combinedSubmitting = ref(false)
const combinedAll = ref([]) // 最近拉取的综合任务(/trainings 倒序,最多 50 条)
const combinedDismissed = ref([]) // 已结束任务本会话关闭的 id(发布/失败回看后收起)
// 横幅行 = 进行中任务(queued/running)+ 每档位最近一条已结束任务(成功未发布=发布入口,
// 发布过/失败仅状态回看);只留最近一条避免横幅被历史任务堆满
const combinedRows = computed(() => {
const all = combinedAll.value.filter((t) => !combinedDismissed.value.includes(t.id))
const active = all.filter((t) => t.status === 'running' || t.status === 'queued')
const latestDone = {}
for (const t of all) {
if (t.status !== 'success' && t.status !== 'failed') continue
if (!latestDone[t.variant] || t.id > latestDone[t.variant].id) latestDone[t.variant] = t
}
return [...active, ...Object.values(latestDone)]
})
const variantLabels = { s: '高精度', n: '高性能' }
function openCombined() {
if (list.value.length < 2) {
ElMessage.warning('综合训练至少需要 2 个数据集')
return
}
combinedIds.value = []
combinedVariants.value = ['s', 'n']
combinedDlg.value = true
}
function combinedSubmit() {
if (combinedIds.value.length < 2) {
ElMessage.warning('请至少勾选 2 个数据集')
return
}
if (!combinedVariants.value.length) {
ElMessage.warning('请选择训练档位')
return
}
combinedSubmitting.value = true
request
.post('/trainings/combined', {
datasetIds: combinedIds.value,
variants: combinedVariants.value,
})
.then(() => {
ElMessage.success('综合训练已发起,进入队列')
combinedDlg.value = false
loadCombined()
})
.finally(() => {
combinedSubmitting.value = false
})
}
function loadCombined() {
request
.get('/trainings', { params: { page: 1, size: 50 } })
.then((data) => {
combinedAll.value = (data.list || []).filter((t) => t.kind === 'combined')
})
.catch(() => {})
}
function cancelCombined(t) {
ElMessageBox.confirm(`终止${t.status === 'queued' ? '排队中的' : '运行中的'}综合训练(${variantLabels[t.variant]})?`, '取消综合训练', { type: 'warning' })
.then(() => request.post('/trainings/cancel', { id: t.id }).then(() => {
ElMessage.success('已取消')
loadCombined()
}))
.catch(() => {})
}
// 发布综合模型版本:任务成功即直写 trainings/combined(_n).tflite,发布只落版本记录
function publishCombined(t) {
ElMessageBox.confirm(
`确定将综合模型 ${variantLabels[t.variant]}档(训练 #${t.id}${t.name || ''}」)结果发布为模型版本?版本号综合序列独立自增,App 按识别档位热更新下载。`,
'发布模型版本',
{ confirmButtonText: '发布', cancelButtonText: '取消', type: 'warning' },
)
.then(() => request.post('/trainings/publish', { id: t.id }))
.then((data) => {
ElMessage.success(`已发布版本 ${data.version}`)
loadCombined()
})
.catch(() => {})
}
// 关闭横幅中已结束任务的回看行(本会话内不再展示;进行中任务不可关)
function dismissCombined(t) {
combinedDismissed.value.push(t.id)
}
function openCreate() {
Object.assign(dlgForm, {
mode: 'create',
@@ -583,8 +678,10 @@ let trainPoll = null
onMounted(() => {
load()
loadFtPendingCount()
loadCombined()
trainPoll = setInterval(() => {
if (list.value.some((r) => trList(r).some((t) => t.status === 'running'))) load()
if (combinedRows.value.some((t) => t.status === 'running' || t.status === 'queued')) loadCombined()
}, 10000)
})
@@ -620,10 +717,58 @@ onBeforeUnmount(() => {
<el-button @click="search">搜索</el-button>
</div>
<div class="toolbar-right">
<el-button :icon="Share" @click="openCombined">综合训练</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新建数据集</el-button>
</div>
</div>
<!-- 综合训练横幅进行中任务进度 + 每档位最近一条已结束任务综合任务 dataset_id=0
不归属任何数据集卡片成功未发布在此给发布入口失败/已发布可关闭收起 -->
<div v-if="combinedRows.length" class="combined-strip">
<div v-for="t in combinedRows" :key="t.id" class="combined-line">
<el-tag type="warning" size="small">综合训练</el-tag>
<span class="combined-name">{{ variantLabels[t.variant] }}</span>
<template v-if="t.status === 'running'">
<el-progress
class="combined-bar"
:percentage="trainPercent(t)"
:stroke-width="6"
:show-text="false"
/>
<span class="combined-text">{{ t.currentEpoch || 0 }}/{{ t.totalEpochs || '-' }} </span>
<span v-if="t.etaMinutes" class="combined-text">· 剩余约 {{ etaText(t.etaMinutes) }}</span>
</template>
<el-tag v-else-if="t.status === 'queued'" size="small" type="info">排队中</el-tag>
<template v-else-if="t.status === 'success'">
<el-tag type="success" size="small">已完成</el-tag>
<el-button
v-if="!t.published"
size="small"
type="success"
class="ds-pub"
@click="publishCombined(t)"
>
发布模型
</el-button>
<span v-else class="combined-text">已发布</span>
</template>
<el-tooltip v-else-if="t.status === 'failed' && t.error" :content="t.error" placement="top">
<el-tag type="danger" size="small">失败</el-tag>
</el-tooltip>
<el-tag v-else type="danger" size="small">失败</el-tag>
<el-button
v-if="t.status === 'running' || t.status === 'queued'"
text
type="danger"
size="small"
@click="cancelCombined(t)"
>
{{ t.status === 'queued' ? '取消排队' : '终止' }}
</el-button>
<el-button v-else text size="small" @click="dismissCombined(t)">关闭</el-button>
</div>
</div>
<div v-loading="loading" class="ds-grid">
<div v-for="row in list" :key="row.id" class="ds-card" @click="router.push(`/datasets/${row.id}`)">
<div class="ds-cover">
@@ -768,6 +913,43 @@ onBeforeUnmount(() => {
<FalseTargets v-if="pageTab === 'falseTargets'" ref="ftRef" />
<!-- 负样本批量生成内置场景池 + RF-DETR 空检自动剔除进度在负样本 tab 内展示 -->
<!-- 综合训练发起多选数据集2+ 档位合并训练一个全类模型 -->
<el-dialog v-model="combinedDlg" title="综合训练" width="min(480px, 94vw)">
<el-alert
type="info"
:closable="false"
show-icon
title="多个数据集合并训练出一个全类模型,App 下载一个模型即可识别多种动物;任一物种补数据后需重新发起综合训练"
style="margin-bottom: 14px"
/>
<el-form label-width="70px">
<el-form-item label="数据集">
<el-checkbox-group v-model="combinedIds">
<el-checkbox v-for="d in list" :key="d.id" :value="d.id">
{{ d.name }}{{ d.imageCount }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="档位">
<el-checkbox-group v-model="combinedVariants">
<el-checkbox value="s">高精度s@1280</el-checkbox>
<el-checkbox value="n">高性能n@704</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="combinedDlg = false">取消</el-button>
<el-button
type="primary"
:loading="combinedSubmitting"
:disabled="combinedIds.length < 2"
@click="combinedSubmit"
>
发起训练{{ combinedIds.length }} 个数据集
</el-button>
</template>
</el-dialog>
<el-dialog v-model="negGenVisible" title="AI 生成负样本" width="min(480px, 94vw)">
<el-form label-width="90px">
<el-form-item label="生成张数">