1
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
| 后台管理端 | `server_admin/`(Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销)、App 版本管理;构建产物由后端 `/admin/` 托管,登录页输入 token 后以 `X-Admin-Token` 头鉴权(`config.yml admin.token`) |
|
| 后台管理端 | `server_admin/`(Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销)、App 版本管理;构建产物由后端 `/admin/` 托管,登录页输入 token 后以 `X-Admin-Token` 头鉴权(`config.yml admin.token`) |
|
||||||
| 版本管理 | 后台管理端上传 Android APK + 更新说明,APK 存服务器 `app.apkDir`(默认 `./workspace/`,与 `./data` 平级、挂载持久化)**固定文件名 `observer-latest.apk`,上传即覆盖,目录永远只保留最新一个文件**;**版本号从文件名识别**:文件须命名为 `observer-x.y.z.apk`(Flutter 打包产物即此命名,版本号取自 pubspec);客户端启动时 `GET /api/v1/app/update` 检查更新:服务器版本高于本地版本即弹更新提示(不可跳过)。**仅 Android 检查,iOS 不做版本下发**(iOS 走 App Store 自行更新)。版本记录可删除:删最新版本联动删除 APK 文件,删历史版本仅删记录 |
|
| 版本管理 | 后台管理端上传 Android APK + 更新说明,APK 存服务器 `app.apkDir`(默认 `./workspace/`,与 `./data` 平级、挂载持久化)**固定文件名 `observer-latest.apk`,上传即覆盖,目录永远只保留最新一个文件**;**版本号从文件名识别**:文件须命名为 `observer-x.y.z.apk`(Flutter 打包产物即此命名,版本号取自 pubspec);客户端启动时 `GET /api/v1/app/update` 检查更新:服务器版本高于本地版本即弹更新提示(不可跳过)。**仅 Android 检查,iOS 不做版本下发**(iOS 走 App Store 自行更新)。版本记录可删除:删最新版本联动删除 APK 文件,删历史版本仅删记录 |
|
||||||
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程:**数据集卡片列表**(封面图/描述/图片数/已标注数/**训练状态徽标**),**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片入库(手动上传/AI 生成)自动触发 RF-DETR 全图扫描标注**,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗内 canvas 直接画框/点框删除/清空并保存——AI 自动标注结果直接作为标注,人工可修改/清理全部框;封面(上传自动转 jpg + UUID 命名)/**描述**/AI 生成图片(provider 抽象,默认 qwen-image/DashScope 付费 API);AI 标注端点与训练机 SSH 为**全局配置,直接读 `config.yml`**(`localAi` / `training.ssh` 节点,改配置需重启服务);图片落服务器 `app.datasetDir`/`datasets/<数据集名>/`,DB 存元数据 + 标注 JSON |
|
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程:**数据集卡片列表**(封面图/描述/图片数/已标注数/**训练状态徽标**),**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片入库(手动上传/AI 生成)自动触发 RF-DETR 全图扫描标注**,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗内 canvas 直接画框/点框删除/清空并保存——AI 自动标注结果直接作为标注,人工可修改/清理全部框;封面(上传自动转 jpg + UUID 命名)/**描述**/AI 生成图片(provider 抽象,默认 qwen-image/DashScope 付费 API);AI 标注端点与训练机 SSH 为**全局配置,直接读 `config.yml`**(`localAi` / `training.ssh` 节点,改配置需重启服务);图片落服务器 `app.datasetDir`/`datasets/<数据集名>/`,DB 存元数据 + 标注 JSON |
|
||||||
| 模型训练 | 从数据集卡片「开始训练」触发:参数(数据集/imgsz/epochs/batch)、进度/日志/指标监控(每 epoch 粒度)、取消;训练通道 `training` 节点可配置 subprocess(与 Go 服务同机直接起 python)/ ssh(异机执行,SSH 凭据取 `config.yml` `training.ssh` 节点),并发度 1(GPU 独占);训练脚本 `server/training/train_server.py`(随项目迁移,2026-08-26)参数化,产物(best.tflite/best.pt/曲线)拉回服务器;训练收尾自动做 **tflite 产物自检**(输入/输出 shape 校验,原 `inspect_tflite.py` 逻辑内嵌脚本),自检失败任务置失败并带出原因;`dump_graph.py` 留作训练机人工深度调试 |
|
| 模型训练 | 从数据集卡片「开始训练」一键触发(参数 imgsz/epochs/batch/device 默认走 `config.yml` `training` 节点,部署级配置):进度/日志/指标监控(每 epoch 粒度)、取消;训练通道 `training` 节点可配置 subprocess(与 Go 服务同机直接起 python)/ ssh(异机执行,SSH 凭据取 `config.yml` `training.ssh` 节点),并发度 1(GPU 独占);训练脚本 `server/training/train_server.py`(随项目迁移,2026-08-26)参数化,产物(best.tflite/best.pt/曲线)拉回服务器;训练收尾自动做 **tflite 产物自检**(输入/输出 shape 校验,原 `inspect_tflite.py` 逻辑内嵌脚本),自检失败任务置失败并带出原因;`dump_graph.py` 留作训练机人工深度调试 |
|
||||||
| 模型版本与热更新 | **每数据集一个模型**:训练完成后一键「发布」(训练任务操作列)——tflite 落 `workspace/models/<数据集>/latest.tflite` + sha256/指标/类别名入 `model_version`(按数据集独立版本序列 m1.0.0 递增)。管理端**无模型管理界面**(版本记录仅支撑客户端下发)。**App 模型热更新**:`GET /api/v1/app/update` 扩展返回 `models` 目录数组,客户端独立检查,新模型下载校验替换,失败回退旧模型——模型迭代不再重打包 APK |
|
| 模型版本与热更新 | **每数据集一个模型**:训练完成后一键「发布」(训练任务操作列)——tflite 落 `workspace/models/<数据集>/latest.tflite` + sha256/指标/类别名入 `model_version`(按数据集独立版本序列 m1.0.0 递增)。管理端**无模型管理界面**(版本记录仅支撑客户端下发)。**App 模型热更新**:`GET /api/v1/app/update` 扩展返回 `models` 目录数组,客户端独立检查,新模型下载校验替换,失败回退旧模型——模型迭代不再重打包 APK |
|
||||||
| 模型目录与多模型推理 | `GET /api/v1/models`(登录态)返回全部数据集当前生效模型(数据集/版本/类别/大小/sha256/下载地址),下载 URL `/download/models/<数据集>/latest.tflite`;**App 模型管理页**用户自由下载/删除/启用模型,识别时**加载全部已启用模型并行推理 + 跨模型 NMS 合并**(按类别名),内置 assets 模型兜底 |
|
| 模型目录与多模型推理 | `GET /api/v1/models`(登录态)返回全部数据集当前生效模型(数据集/版本/类别/大小/sha256/下载地址),下载 URL `/download/models/<数据集>/latest.tflite`;**App 模型管理页**用户自由下载/删除/启用模型,识别时**加载全部已启用模型并行推理 + 跨模型 NMS 合并**(按类别名),内置 assets 模型兜底 |
|
||||||
| 标注 | **图片入库自动触发**:手动上传/AI 生成成功后,新增图自动调 `config.yml` `localAi` 节点配置的 AI 端点做 RF-DETR 全图扫描(`label_task` 记录进度,页顶进度条展示;**localAi 未配置 → 上传/生成接口直接报错;已有标注任务在跑(忙)→ 不报错**,当前任务成功完成后自动补标未标注图)→ 扫描结果(**重叠去重**:NMS 风格按置信度降序保留,重叠比 > `localAi.overlapThreshold` 默认 0.3 的框剔除——重叠比 = 交叠面积/两框较小面积,RF-DETR 同目标常输出一大一小两框,此判据能命中,同目标只留置信度最高者)**直接写 `dataset_image.labels_json`**(覆盖该图已有标注,即重标语义);点击弹窗放大后在 canvas 上画框/点框删除/清空/改类别 → 保存即整体覆写 `dataset_image.labels_json`(YOLO 归一化 JSON 数组,AI 与人工框同存,人工可修改/清理);`POST /admin/label-tasks` 详情页「全量标注」按钮入口(另有自动触发),可发起全量/指定图重标;自动/手动/混合并存,训练前自动整理(prepare_yolo 逻辑在服务端) |
|
| 标注 | **图片入库自动触发**:手动上传/AI 生成成功后,新增图自动调 `config.yml` `localAi` 节点配置的 AI 端点做 RF-DETR 全图扫描(`label_task` 记录进度,页顶进度条展示;**localAi 未配置 → 上传/生成接口直接报错;已有标注任务在跑(忙)→ 不报错**,当前任务成功完成后自动补标未标注图)→ 扫描结果(**重叠去重**:NMS 风格按置信度降序保留,重叠比 > `localAi.overlapThreshold` 默认 0.3 的框剔除——重叠比 = 交叠面积/两框较小面积,RF-DETR 同目标常输出一大一小两框,此判据能命中,同目标只留置信度最高者)**直接写 `dataset_image.labels_json`**(覆盖该图已有标注,即重标语义);点击弹窗放大后在 canvas 上画框/点框删除/清空/改类别 → 保存即整体覆写 `dataset_image.labels_json`(YOLO 归一化 JSON 数组,AI 与人工框同存,人工可修改/清理);`POST /admin/label-tasks` 详情页「全量标注」按钮入口(另有自动触发),可发起全量/指定图重标;自动/手动/混合并存,训练前自动整理(prepare_yolo 逻辑在服务端) |
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>视野管理端</title>
|
<title>视野管理端</title>
|
||||||
<script type="module" crossorigin src="/admin/assets/index-jKHdJYxb.js"></script>
|
<script type="module" crossorigin src="/admin/assets/index-CIe9te1H.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-B0ehGsu8.css">
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-BaD1qigE.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -207,15 +207,11 @@ type AdminTrainingListRes struct {
|
|||||||
List []*AdminTrainingItem `json:"list"`
|
List []*AdminTrainingItem `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminTrainingStartReq 发起训练(并发度 1:已有 running 任务时拒绝)
|
// AdminTrainingStartReq 发起训练(并发度 1:已有 running 任务时拒绝;训练参数走 config.yml training 节点,name 空则自动生成)
|
||||||
type AdminTrainingStartReq struct {
|
type AdminTrainingStartReq struct {
|
||||||
g.Meta `path:"/trainings" method:"post" summary:"发起训练" tags:"管理端"`
|
g.Meta `path:"/trainings" method:"post" summary:"发起训练" tags:"管理端"`
|
||||||
DatasetId int64 `json:"datasetId" v:"required|min:1" dc:"数据集 id"`
|
DatasetId int64 `json:"datasetId" v:"required|min:1" dc:"数据集 id"`
|
||||||
Name string `json:"name" v:"required|length:1,50" dc:"任务名称"`
|
Name string `json:"name" v:"length:0,50" dc:"任务名称,空自动生成"`
|
||||||
Imgsz int `json:"imgsz" v:"integer|min:64|max:2048" dc:"输入尺寸,默认 704"`
|
|
||||||
Epochs int `json:"epochs" v:"integer|min:1|max:1000" dc:"训练轮数,默认 150"`
|
|
||||||
Batch int `json:"batch" v:"integer|min:1|max:128" dc:"批大小,默认 16"`
|
|
||||||
Device string `json:"device" v:"length:0,16" dc:"设备,默认 0"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminTrainingStartRes struct {
|
type AdminTrainingStartRes struct {
|
||||||
|
|||||||
@@ -317,21 +317,12 @@ func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.Admin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
imgsz := req.Imgsz
|
// 训练参数为部署级配置(config.yml training 节点,界面不传):device 随训练机硬件、
|
||||||
if imgsz <= 0 {
|
// imgsz 须与 App 端推理对齐、epochs 随算力预期
|
||||||
imgsz = 704
|
imgsz, epochs, batch, device := cfg.Imgsz, cfg.Epochs, cfg.Batch, cfg.Device
|
||||||
}
|
name := req.Name
|
||||||
epochs := req.Epochs
|
if name == "" {
|
||||||
if epochs <= 0 {
|
name = fmt.Sprintf("%s 训练 %s", dataset.Name, gtime.Now().Format("01-02 15:04"))
|
||||||
epochs = 150
|
|
||||||
}
|
|
||||||
batch := req.Batch
|
|
||||||
if batch <= 0 {
|
|
||||||
batch = 16
|
|
||||||
}
|
|
||||||
device := req.Device
|
|
||||||
if device == "" {
|
|
||||||
device = "0"
|
|
||||||
}
|
}
|
||||||
now := gtime.Now()
|
now := gtime.Now()
|
||||||
var taskId int64
|
var taskId int64
|
||||||
@@ -344,7 +335,7 @@ func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.Admin
|
|||||||
return gerror.NewCode(common.CodeTrainingRunning)
|
return gerror.NewCode(common.CodeTrainingRunning)
|
||||||
}
|
}
|
||||||
taskId, err = dao.Training.Insert(ctx, &entity.ModelTraining{
|
taskId, err = dao.Training.Insert(ctx, &entity.ModelTraining{
|
||||||
Name: req.Name,
|
Name: name,
|
||||||
Status: consts.TrainingStatusRunning,
|
Status: consts.TrainingStatusRunning,
|
||||||
DatasetId: dataset.Id,
|
DatasetId: dataset.Id,
|
||||||
Imgsz: imgsz,
|
Imgsz: imgsz,
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ type TrainingConfig struct {
|
|||||||
DatasetDir string
|
DatasetDir string
|
||||||
Python string
|
Python string
|
||||||
TimeoutMins int
|
TimeoutMins int
|
||||||
|
Imgsz int
|
||||||
|
Epochs int
|
||||||
|
Batch int
|
||||||
|
Device string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrainingConfigOf 读取训练通道配置(未配置返回 ok=false)
|
// TrainingConfigOf 读取训练通道配置(未配置返回 ok=false)
|
||||||
@@ -97,6 +101,10 @@ func TrainingConfigOf(ctx context.Context) (TrainingConfig, bool) {
|
|||||||
DatasetDir: g.Cfg().MustGet(ctx, "training.datasetDir", "datasets").String(),
|
DatasetDir: g.Cfg().MustGet(ctx, "training.datasetDir", "datasets").String(),
|
||||||
Python: g.Cfg().MustGet(ctx, "training.venvPython").String(),
|
Python: g.Cfg().MustGet(ctx, "training.venvPython").String(),
|
||||||
TimeoutMins: g.Cfg().MustGet(ctx, "training.timeoutMinutes", 600).Int(),
|
TimeoutMins: g.Cfg().MustGet(ctx, "training.timeoutMinutes", 600).Int(),
|
||||||
|
Imgsz: g.Cfg().MustGet(ctx, "training.imgsz", 704).Int(),
|
||||||
|
Epochs: g.Cfg().MustGet(ctx, "training.epochs", 150).Int(),
|
||||||
|
Batch: g.Cfg().MustGet(ctx, "training.batch", 16).Int(),
|
||||||
|
Device: g.Cfg().MustGet(ctx, "training.device", "0").String(),
|
||||||
}
|
}
|
||||||
return cfg, cfg.Workdir != "" && cfg.Python != ""
|
return cfg, cfg.Workdir != "" && cfg.Python != ""
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -40,7 +40,7 @@ imageGen:
|
|||||||
# 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务):
|
# 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务):
|
||||||
# mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步)
|
# mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步)
|
||||||
training:
|
training:
|
||||||
mode: subprocess # subprocess | ssh
|
mode: ssh # subprocess | ssh
|
||||||
ssh:
|
ssh:
|
||||||
host: "192.168.3.210" # 训练机地址(mode=ssh 必填)
|
host: "192.168.3.210" # 训练机地址(mode=ssh 必填)
|
||||||
user: "root"
|
user: "root"
|
||||||
@@ -51,6 +51,10 @@ training:
|
|||||||
venvPython: /opt/pheasant_data/venv/bin/python
|
venvPython: /opt/pheasant_data/venv/bin/python
|
||||||
datasetDir: datasets # 训练机数据集根目录(相对 workdir,yolo/<name> 为子目录)
|
datasetDir: datasets # 训练机数据集根目录(相对 workdir,yolo/<name> 为子目录)
|
||||||
timeoutMinutes: 600 # 训练超时判死
|
timeoutMinutes: 600 # 训练超时判死
|
||||||
|
imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐)
|
||||||
|
epochs: 150 # 训练轮数(patience 30 早停,设大可自动停)
|
||||||
|
batch: 16 # 批大小(按训练机显存调整)
|
||||||
|
device: "0" # GPU 编号(cpu 用 cpu)
|
||||||
|
|
||||||
# RF-DETR 预标注服务(管理端标注工作台):需从服务器可达,未配置时预标注接口返回「标注服务未配置」
|
# RF-DETR 预标注服务(管理端标注工作台):需从服务器可达,未配置时预标注接口返回「标注服务未配置」
|
||||||
localAi:
|
localAi:
|
||||||
|
|||||||
@@ -350,8 +350,14 @@ training:
|
|||||||
datasetDir: datasets # 训练机数据集根目录(相对 workdir,数据集为子目录)
|
datasetDir: datasets # 训练机数据集根目录(相对 workdir,数据集为子目录)
|
||||||
concurrency: 1 # GPU 独占:同时仅一个 running,新任务排队
|
concurrency: 1 # GPU 独占:同时仅一个 running,新任务排队
|
||||||
timeoutMinutes: 600 # 超时判死
|
timeoutMinutes: 600 # 超时判死
|
||||||
|
imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐)
|
||||||
|
epochs: 150 # 训练轮数(patience 30 早停,设大可自动停)
|
||||||
|
batch: 16 # 批大小(按训练机显存调整)
|
||||||
|
device: "0" # GPU 编号(cpu 用 cpu)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **训练参数默认走配置(2026-08-26)**:imgsz/epochs/batch/device 不随管理端请求传(界面一键开始),由 `training` 节点统一配置——device 取决于训练机硬件、imgsz 必须与端侧推理对齐、epochs 取决于算力预期,均为部署级参数;任务记录仍存各值(`model_training.imgsz/epochs/batch/device`)供列表展示
|
||||||
|
|
||||||
- service 内 `Runner` 接口:`Start(ctx, *TrainingJob) (pid, error)` / `FetchLogTail(ctx, job)` / `IsAlive(ctx, job) bool` / `Cancel(ctx, job)` / `FetchArtifacts(ctx, job, destDir)`;`subprocess` 与 `ssh` 两个实现,按 config `mode` 选择;**ssh 凭据直接读本节点 `training.ssh` 配置**(见「全局训练配置」节)
|
- service 内 `Runner` 接口:`Start(ctx, *TrainingJob) (pid, error)` / `FetchLogTail(ctx, job)` / `IsAlive(ctx, job) bool` / `Cancel(ctx, job)` / `FetchArtifacts(ctx, job, destDir)`;`subprocess` 与 `ssh` 两个实现,按 config `mode` 选择;**ssh 凭据直接读本节点 `training.ssh` 配置**(见「全局训练配置」节)
|
||||||
- **训练脚本**(`server/training/train_server.py`,随项目迁移):支持 `--task-json <file>`(含 dataset/imgsz/epochs/batch/device/project 名),每 epoch 输出一行机器可读 JSON 到 `--log-file`(`{"epoch":1,"total":150,"metrics":{...}}`),结束写 `result.json`(最终指标)+ 自动打包 `artifact.zip`(best.pt + results.csv + 曲线);Go 侧解析日志行更新进度、轮询日志尾部截断 N KB 存 `model_training.log_tail`
|
- **训练脚本**(`server/training/train_server.py`,随项目迁移):支持 `--task-json <file>`(含 dataset/imgsz/epochs/batch/device/project 名),每 epoch 输出一行机器可读 JSON 到 `--log-file`(`{"epoch":1,"total":150,"metrics":{...}}`),结束写 `result.json`(最终指标)+ 自动打包 `artifact.zip`(best.pt + results.csv + 曲线);Go 侧解析日志行更新进度、轮询日志尾部截断 N KB 存 `model_training.log_tail`
|
||||||
- **tflite 产物自检**(2026-08-26):`inspect_tflite.py` 的 flatbuffer 解析逻辑内嵌进 `train_server.py`(`check_tflite`),训练收尾定位 `best.tflite` 后自动校验并写 `result.json` 的 `tflite_check` 字段:`{"ok":bool,"reason":string,"inputs":[{"name","shape","type"}],"outputs":[...]}`;校验规则 = 输入恰 1 张且 4 维、元素总数 == imgsz²×3(兼容 NCHW/NHWC)、输出 ≥ 1 张且 batch 维 = 1;`ok=false`(如 shape 漂移、导出异常)时 Go 侧在拉产物前直接置训练失败并带出 reason,杜绝坏产物进入发布链路;`dump_graph.py` 保留作训练机人工深度调试
|
- **tflite 产物自检**(2026-08-26):`inspect_tflite.py` 的 flatbuffer 解析逻辑内嵌进 `train_server.py`(`check_tflite`),训练收尾定位 `best.tflite` 后自动校验并写 `result.json` 的 `tflite_check` 字段:`{"ok":bool,"reason":string,"inputs":[{"name","shape","type"}],"outputs":[...]}`;校验规则 = 输入恰 1 张且 4 维、元素总数 == imgsz²×3(兼容 NCHW/NHWC)、输出 ≥ 1 张且 batch 维 = 1;`ok=false`(如 shape 漂移、导出异常)时 Go 侧在拉产物前直接置训练失败并带出 reason,杜绝坏产物进入发布链路;`dump_graph.py` 保留作训练机人工深度调试
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ const coverFile = ref(null) // 新选择的封面文件(el-upload 单文件)
|
|||||||
const coverFileList = ref([]) // 封面回显:已有封面(服务端 url)或新选文件(本地预览)
|
const coverFileList = ref([]) // 封面回显:已有封面(服务端 url)或新选文件(本地预览)
|
||||||
const coverDeleted = ref(false) // 用户删除了已有封面(保存时调删除接口)
|
const coverDeleted = ref(false) // 用户删除了已有封面(保存时调删除接口)
|
||||||
|
|
||||||
const trainVisible = ref(false)
|
|
||||||
const training = ref(false)
|
const training = ref(false)
|
||||||
const trainForm = reactive({ datasetId: 0, name: '', imgsz: 704, epochs: 150, batch: 16, device: '0' })
|
|
||||||
|
|
||||||
const statusMap = { building: '建设中', labeled: '已标注', synced: '已同步' }
|
const statusMap = { building: '建设中', labeled: '已标注', synced: '已同步' }
|
||||||
const statusTag = { building: 'info', labeled: 'success', synced: 'primary' }
|
const statusTag = { building: 'info', labeled: 'success', synced: 'primary' }
|
||||||
@@ -133,29 +131,14 @@ async function submitConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 开始训练 ----------
|
// ---------- 开始训练(一键发起:任务名自动生成,参数走 config.yml training 节点) ----------
|
||||||
|
|
||||||
function openTrain(row) {
|
function startTrain(row) {
|
||||||
trainForm.datasetId = row.id
|
|
||||||
trainForm.name = `${row.name} 训练`
|
|
||||||
trainForm.imgsz = 704
|
|
||||||
trainForm.epochs = 150
|
|
||||||
trainForm.batch = 16
|
|
||||||
trainForm.device = '0'
|
|
||||||
trainVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitTrain() {
|
|
||||||
if (!trainForm.name.trim()) {
|
|
||||||
ElMessage.warning('请输入任务名称')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
training.value = true
|
training.value = true
|
||||||
request
|
request
|
||||||
.post('/trainings', { ...trainForm })
|
.post('/trainings', { datasetId: row.id })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success('训练任务已发起,完成后可发布为模型版本')
|
ElMessage.success('训练任务已发起,完成后可发布为模型版本')
|
||||||
trainVisible.value = false
|
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -268,7 +251,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ds-actions" @click.stop>
|
<div class="ds-actions" @click.stop>
|
||||||
<el-button type="primary" size="small" :icon="VideoPlay" :disabled="row.trainingStatus === 'running'" @click="openTrain(row)">
|
<el-button type="primary" size="small" :icon="VideoPlay" :disabled="row.trainingStatus === 'running'" @click="startTrain(row)">
|
||||||
{{ row.trainingStatus === 'running' ? '训练中' : '开始训练' }}
|
{{ row.trainingStatus === 'running' ? '训练中' : '开始训练' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" :icon="Setting" @click="openConfig(row)">配置</el-button>
|
<el-button size="small" :icon="Setting" @click="openConfig(row)">配置</el-button>
|
||||||
@@ -361,32 +344,6 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 开始训练 -->
|
|
||||||
<el-dialog v-model="trainVisible" title="开始训练" width="min(480px, 92vw)">
|
|
||||||
<el-form label-width="90px">
|
|
||||||
<el-form-item label="任务名称">
|
|
||||||
<el-input v-model="trainForm.name" maxlength="50" show-word-limit placeholder="如:雉鸡数据集 v2 训练" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分辨率">
|
|
||||||
<el-input-number v-model="trainForm.imgsz" :min="64" :max="2048" :step="32" />
|
|
||||||
<span class="field-tip">与端侧推理对齐,默认 704</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="轮数">
|
|
||||||
<el-input-number v-model="trainForm.epochs" :min="1" :max="1000" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Batch">
|
|
||||||
<el-input-number v-model="trainForm.batch" :min="1" :max="128" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="设备">
|
|
||||||
<el-input v-model="trainForm.device" maxlength="16" placeholder="GPU 编号 0,CPU 填 cpu" />
|
|
||||||
</el-form-item>
|
|
||||||
<div class="start-tip">训练前自动整理 80/20 训练/验证集,须先完成图片标注;训练机并发度 1,已有运行中任务会被拒绝。</div>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="trainVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="training" @click="submitTrain">发起训练</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -512,12 +469,6 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
.start-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-left: 90px;
|
|
||||||
}
|
|
||||||
.cover-row {
|
.cover-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user