This commit is contained in:
2026-08-27 17:54:48 +08:00
parent f89e1e4de8
commit b30ca673cc
12 changed files with 168 additions and 92 deletions
+1 -1
View File
@@ -180,7 +180,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow
| POST | `/admin/app-versions/delete` | 删除版本记录 `{"id":1}`:删**最新版本**时联动删除 APK 文件(客户端不再提示更新、下载 404);删历史版本只删记录不动文件 |
| POST | `/admin/datasets` | 创建数据集 `{"name":"pheasant_v2","source":"manual"\|"ai"}`(name ≤50 字唯一,目录自动建) |
| GET | `/admin/datasets` | 数据集列表:`page/size` 分页,返回 `{total, list}`(含 imageCount/labeledCount/status/cover/description/**training 聚合状态**:最新训练记录的 status/currentEpoch/totalEpochs |
| POST | `/admin/datasets/update` | 更新数据集配置 `{"id":1,"description":"...","cover":"a.jpg"}`:描述/封面,空值字段不覆盖原值 |
| POST | `/admin/datasets/update` | 更新数据集配置 `{"id":1,"name":"新名","description":"...","cover":"a.jpg"}`名称(改名)/描述/封面,空值字段不覆盖原值;改名同步迁移图片目录与模型文件,标注/训练进行中拒绝 |
| POST | `/admin/datasets/cover` | 上传数据集封面(multipart`datasetId`+`file`jpg/jpeg/png ≤2MB):**自动转 jpg + UUID 命名**落盘并覆盖旧封面 |
| GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位) |
| POST | `/admin/datasets/cover/delete` | 删除数据集封面 `{"datasetId":1}`:删文件 + 清 cover 字段 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>视野管理端</title>
<script type="module" crossorigin src="/admin/assets/index-Dtws4Ia3.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-C62TRiv2.css">
<script type="module" crossorigin src="/admin/assets/index-C_zeeHvw.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-f1eLEuzq.css">
</head>
<body>
<div id="app"></div>
+6 -2
View File
@@ -97,10 +97,14 @@ func (d *datasetDao) UpdateCounters(ctx context.Context, id int64, imgDelta, lab
return err
}
// UpdateConfigs 更新数据集展示配置(封面/描述;仅更新非零字段,留空字段保留原值——
// 前端提交整包配置,未填项不覆盖。AI 端点/训练机 SSH 为全局训练配置,不在此表维护)
// UpdateConfigs 更新数据集展示配置(名称唯一性与目录迁移由 service 校验后传入;
// 仅更新非零字段,留空字段保留原值——前端提交整包配置,未填项不覆盖。
// AI 端点/训练机 SSH 为全局训练配置,不在此表维护)
func (d *datasetDao) UpdateConfigs(ctx context.Context, id int64, m *entity.Dataset) error {
data := g.Map{"updated_at": gtime.Now()}
if m.Name != "" {
data["name"] = m.Name
}
if m.Cover != "" {
data["cover"] = m.Cover
}
+4 -2
View File
@@ -45,11 +45,13 @@ type AdminDatasetListRes struct {
List []*AdminDatasetItem `json:"list"`
}
// AdminDatasetUpdateReq 更新数据集展示配置(空值字段不覆盖原值,保留现有配置;
// AdminDatasetUpdateReq 更新数据集配置(空值字段不覆盖原值,保留现有配置;改名时同步迁移
// 图片目录与模型文件,名称唯一且同是磁盘目录名;
// AI 端点/训练机 SSH 走 config.yml 全局配置(localAi / training.ssh))
type AdminDatasetUpdateReq struct {
g.Meta `path:"/datasets/update" method:"post" summary:"更新数据集配置" tags:"管理端"`
Id int64 `json:"id" v:"required|min:1" dc:"数据集 id"`
Name string `json:"name" v:"length:0,50" dc:"数据集名称(非空且变化才更新;同是目录名)"`
Description string `json:"description" v:"length:0,500" dc:"描述(非空才更新)"`
Cover string `json:"cover" v:"length:0,200" dc:"封面文件名(非空才更新)"`
}
@@ -95,7 +97,7 @@ type AdminDatasetGenerateReq struct {
DatasetId int64 `json:"datasetId" v:"required|min:1" dc:"数据集 id"`
Prompt string `json:"prompt" v:"required|length:5,500" dc:"生成提示词(禁止含位置描述)"`
Count int `json:"count" v:"required|integer|min:1|max:8" dc:"生成张数 1-8"`
Size string `json:"size" v:"required|in:1152x2048,704x704" dc:"图片尺寸"`
Size string `json:"size" v:"required|in:704x1248" dc:"图片尺寸"`
}
type AdminDatasetGenerateRes struct {
+81
View File
@@ -35,6 +35,10 @@ var Dataset = &datasetService{}
// imgExts 允许上传/保存的图片扩展名
var imgExts = map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
// datasetNameRe 数据集名称规则(与新建接口 DTO 校验一致:中文/字母/数字/下划线/短横线,1-50)
// 更新接口的名称为可选字段,空值合法,正则不能进 DTO 校验链,放 service 侧判定。
var datasetNameRe = regexp.MustCompile(`^[a-zA-Z0-9_一-龥-]{1,50}$`)
// AdminListDatasets 数据集分页列表(卡片展示:封面/描述/训练配置标记 + 最新训练状态聚合)
func (s *datasetService) AdminListDatasets(ctx context.Context, req *dto.AdminDatasetListReq) (*dto.AdminDatasetListRes, error) {
page, size := common.NormalizePage(req.Page, req.Size)
@@ -93,6 +97,13 @@ func (s *datasetService) AdminUpdateDataset(ctx context.Context, req *dto.AdminD
if existing == nil {
return nil, gerror.NewCode(common.CodeDatasetNotFound)
}
newName := strings.TrimSpace(req.Name)
if newName != "" && newName != existing.Name {
if err := s.renameDataset(ctx, existing.Id, existing.Name, newName); err != nil {
return nil, err
}
existing.Name = newName
}
// 封面须属于该数据集图片(防伪造文件名指向任意文件)
if req.Cover != "" {
img, err := dao.DatasetImage.GetByFilename(ctx, req.Id, req.Cover)
@@ -104,6 +115,7 @@ func (s *datasetService) AdminUpdateDataset(ctx context.Context, req *dto.AdminD
}
}
if err := dao.Dataset.UpdateConfigs(ctx, req.Id, &entity.Dataset{
Name: newName,
Cover: req.Cover,
Description: req.Description,
}); err != nil {
@@ -112,6 +124,75 @@ func (s *datasetService) AdminUpdateDataset(ctx context.Context, req *dto.AdminD
return &dto.AdminDatasetUpdateRes{Id: req.Id}, nil
}
// renameDataset 数据集改名:名称同是磁盘目录名,需同步迁移图片目录与当前生效模型文件。
// 进行中的标注/训练任务持有旧路径,须先等任务结束;改名在 Serial 串行执行,与其他写操作互斥。
func (s *datasetService) renameDataset(ctx context.Context, id int64, oldName, newName string) error {
err := common.Serial().Submit(ctx, func() error {
if !datasetNameRe.MatchString(newName) {
return gerror.New("数据集名称仅支持中文/字母/数字/下划线/短横线,长度 1-50")
}
// 预标注任务进行中(RF-DETR 正在按旧目录读图)
if t, err := dao.LabelTask.GetRunningByDataset(ctx, id); err != nil {
return err
} else if t != nil {
return gerror.NewCode(common.CodeLabelTaskRunning)
}
// 训练进行中(同步目录与产物路径以旧名构建)
if t, err := dao.Training.RunningByDataset(ctx, id); err != nil {
return err
} else if t != nil {
return gerror.New("该数据集有训练任务进行中,无法重命名")
}
// 同名占用检查(UNIQUE 约束兜底)
exists, err := dao.Dataset.GetByName(ctx, newName)
if err != nil {
return err
}
if exists != nil {
return gerror.NewCode(common.CodeDatasetNameDuplicate)
}
// 目录迁移:任一步失败回滚已迁移项(图片目录 / 当前生效模型文件可能不存在,缺省跳过)
oldImgDir := common.DatasetImagesDir(ctx, oldName)
newImgDir := common.DatasetImagesDir(ctx, newName)
oldModel := common.TrainingModelPath(ctx, oldName)
newModel := common.TrainingModelPath(ctx, newName)
movedImgDir, movedModel := false, false
if _, err := os.Stat(oldImgDir); err == nil {
if err := os.Rename(oldImgDir, newImgDir); err != nil {
return gerror.Wrap(err, "迁移图片目录失败")
}
movedImgDir = true
}
if _, err := os.Stat(oldModel); err == nil {
if err := os.Rename(oldModel, newModel); err != nil {
if movedImgDir {
if rbErr := os.Rename(newImgDir, oldImgDir); rbErr != nil {
g.Log().Errorf(ctx, "回滚图片目录失败: %+v", rbErr)
}
}
return gerror.Wrap(err, "迁移模型文件失败")
}
movedModel = true
}
if err := dao.Dataset.UpdateConfigs(ctx, id, &entity.Dataset{Name: newName}); err != nil {
if movedModel {
if rbErr := os.Rename(newModel, oldModel); rbErr != nil {
g.Log().Errorf(ctx, "回滚模型文件失败: %+v", rbErr)
}
}
if movedImgDir {
if rbErr := os.Rename(newImgDir, oldImgDir); rbErr != nil {
g.Log().Errorf(ctx, "回滚图片目录失败: %+v", rbErr)
}
}
return err
}
g.Log().Infof(ctx, "数据集 %d 改名完成: %s -> %s", id, oldName, newName)
return nil
})
return err
}
// AdminUploadCover 上传数据集封面:解码校验图片 → 转 jpg(Quality 92)→ UUID 命名落盘,
// 删除旧封面文件(UUID 互不覆盖,但 DB 指向切换)→ 更新 cover 字段。
func (s *datasetService) AdminUploadCover(ctx context.Context, req *dto.AdminDatasetCoverUploadReq) (*dto.AdminDatasetCoverUploadRes, error) {
+3 -3
View File
@@ -35,9 +35,9 @@ app:
imageGen:
provider: localai # dashscope | localai
baseUrl: "http://192.168.3.210:18080" # localai 必填:训练机 local-ai 地址(/v1/images/generations
model: qwen-image # localai 上加载的模型名;dashscope 用 qwen-image-3.0
model: z-image-turbo # localai 上加载的模型名;dashscope 用 qwen-image-3.0
apiKey: "" # dashscope 必填:DashScope API Key
timeoutSeconds: 600 # 每张生成超时(默认 120;localai 训练机约 5min/张,建议 600
timeoutSeconds: 1800 # 每张生成超时(默认 120;localai 训练机 704x1248 竖版实测约 26min/张
# 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务):
# mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步)
@@ -52,7 +52,7 @@ training:
workdir: /opt/pheasant_data # 训练机工作目录(train_server.py / yolov8n.pt 所在)
venvPython: /opt/pheasant_data/venv/bin/python
datasetDir: datasets # 训练机数据集根目录(相对 workdiryolo/<name> 为子目录)
timeoutMinutes: 600 # 训练超时判死
timeoutMinutes: 240 # 训练超时判死started_at 起算;实测 100 张约 3 分钟,千图级约 1 小时)
imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐)
epochs: 150 # 训练轮数(patience 30 早停,设大可自动停)
batch: 16 # 批大小(按训练机显存调整)
Binary file not shown.
+2 -2
View File
@@ -328,7 +328,7 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 tokeniOS 不
- `dashscope`(通义万相付费 API):`apiKey` + `model: qwen-image-3.0`;内部自管 120s 轮询上限
- `localai`(本机/训练机 local-ai`POST {baseUrl}/v1/images/generations`OpenAI 兼容):`baseUrl` + `model`(local-ai 上加载的模型名,如 qwen-image+ `timeoutSeconds`(训练机 qwen-image 1024x1024/30 步约 5 分钟,默认 600);返回 url 为容器内 localhost 地址,下载时替换为 `baseUrl` 的 hoststep/cfg 等采样参数用 local-ai 模型配置默认值,不随接口传
- `baseUrl`/`apiKey` 缺失时对应 provider 视为未配置,生成接口返回「图像生成服务未配置」
- 尺寸参数由前端传(下拉可选,默认 1152x2048 竖图,与既有训练图一致;另提供 704x704 档)
- 尺寸参数由前端传(固定竖版 704x1248,比例与既有训练图 1152x2048 一致、32 倍数对齐);**实测竖版图在 local-ai 上极慢(约 26 分钟/张,Qwen-Image 行列注意力对非方形输入效率差,与分辨率关系不大;方形 1024x1024 仅 5 分钟)**,超时与前端等待按此配置,一次建议 1 张
- 生成同步执行(count 1..8,每张超时 `imageGen.timeoutSeconds`,默认 120slocalai 建议 600),逐张落盘 + 入库(记录 prompt 便于追溯);中途失败返回错误(已成功的图保留,不删除——付费资产原则)
- prompt 由前端预设模板 + 用户微调;**遵守项目提示词规范:不得包含目标位置描述**(位置由模型自行推理,前端模板与校验文案落实此约束)
@@ -342,7 +342,7 @@ training:
venvPython: /opt/pheasant_data/venv/bin/python
datasetDir: datasets # 训练机数据集根目录(相对 workdir,数据集为子目录)
concurrency: 1 # GPU 独占:同时仅一个 running,新任务排队
timeoutMinutes: 600 # 超时判死
timeoutMinutes: 240 # 超时判死started_at 起算;100 张实测约 3 分钟)
imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐)
epochs: 150 # 训练轮数(patience 30 早停,设大可自动停)
batch: 16 # 批大小(按训练机显存调整)