- 后端:/admin/trainings/combined 发起(≥2 数据集、类别重映射、防重名、负样本单份); model_training/model_version 加 kind+dataset_ids(迁移 v14),综合任务 dataset_id=0、 文件基名 combined(_n)、版本序列独立;训练列表补 published 标记 - 管理端:数据训练页工具栏发起综合训练;横幅常驻进行中任务 + 每档最近一条已结束任务, 成功未发布给「发布模型」入口(可关闭收起) - App:目录解析 kind/datasetIds、激活覆盖互斥、自动更新退场改目标档待办横幅手动一键下载
88 lines
3.0 KiB
Go
88 lines
3.0 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/url"
|
||
|
||
"observer-server/biz/consts"
|
||
"observer-server/biz/dao"
|
||
"observer-server/biz/model/dto"
|
||
)
|
||
|
||
// modelVersionService 模型版本业务:管理端无模型管理界面(2026-08-26 决策),
|
||
// 版本记录仅支撑客户端模型目录(App 多模型下载热更新)。文件布局见 common/workspace.go。
|
||
type modelVersionService struct{}
|
||
|
||
var ModelVersion = &modelVersionService{}
|
||
|
||
// ClientCatalog 客户端模型目录:全部数据集当前生效模型 + 综合模型(App 多模型下载热更新)。
|
||
// 双档位(2026-09-03):每数据集 s/n 两档 is_latest=1 各一条;downloadUrl 复用 /download 静态托管
|
||
// (/download/trainings/<基名>.tflite,n 档带 _n 后缀,基名前缀空回退数据集名)。
|
||
// 综合模型(2026-09-09):kind=combined 行 dataset_id=0,条目 datasetName=综合、fileBase=combined(+_n)、
|
||
// 带 datasetIds 覆盖列表,无封面。
|
||
func (s *modelVersionService) ClientCatalog(ctx context.Context) (*dto.ModelCatalogRes, error) {
|
||
list, err := dao.ModelVersion.ListAllLatest(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
names := Training.datasetNameMap(ctx)
|
||
modelNames := Training.datasetModelNameMap(ctx)
|
||
items := make([]*dto.ModelCatalogItem, 0, len(list))
|
||
for _, v := range list {
|
||
kind := v.Kind
|
||
if kind == "" {
|
||
kind = consts.TrainingKindSpecies
|
||
}
|
||
var datasetName, fileBase, coverUrl string
|
||
var datasetIds []int64
|
||
if kind == consts.TrainingKindCombined {
|
||
datasetName = "综合"
|
||
fileBase = consts.TrainingCombinedBase
|
||
if v.Variant == consts.TrainingVariantN {
|
||
fileBase += consts.TrainingVariantNFileSuffix
|
||
}
|
||
if err := json.Unmarshal([]byte(v.DatasetIds), &datasetIds); err != nil || datasetIds == nil {
|
||
datasetIds = []int64{}
|
||
}
|
||
} else {
|
||
datasetName = names[v.DatasetId]
|
||
if datasetName == "" {
|
||
continue
|
||
}
|
||
// 双档位:每数据集最多 2 条生效记录(is_latest 按 (数据集,档位) 各一),n 档 downloadUrl 带 _n 后缀;
|
||
// cover 属数据集(两档同物种同图源),沿用基名前缀
|
||
fileBase = modelNames[v.DatasetId]
|
||
if v.Variant == consts.TrainingVariantN {
|
||
fileBase += consts.TrainingVariantNFileSuffix
|
||
}
|
||
coverUrl = "/api/v1/app/cover?namePrefix=" + url.PathEscape(modelNames[v.DatasetId])
|
||
}
|
||
items = append(items, &dto.ModelCatalogItem{
|
||
DatasetId: v.DatasetId,
|
||
DatasetName: datasetName,
|
||
Variant: v.Variant,
|
||
Kind: kind,
|
||
DatasetIds: datasetIds,
|
||
Version: v.Version,
|
||
Labels: parseLabels(v.Labels),
|
||
SizeBytes: v.SizeBytes,
|
||
Sha256: v.Sha256,
|
||
Notes: v.Notes,
|
||
PublishedAt: v.CreatedAt,
|
||
DownloadUrl: "/download/trainings/" + fileBase + ".tflite",
|
||
CoverUrl: coverUrl,
|
||
})
|
||
}
|
||
return &dto.ModelCatalogRes{List: items}, nil
|
||
}
|
||
|
||
// parseLabels 解析类别名 JSON 数组
|
||
func parseLabels(s string) []string {
|
||
var out []string
|
||
if err := json.Unmarshal([]byte(s), &out); err != nil || len(out) == 0 {
|
||
return []string{"class0", "class1"}
|
||
}
|
||
return out
|
||
}
|