This commit is contained in:
2026-09-02 11:18:04 +08:00
parent f55ec114cc
commit 1eb97faaa7
11 changed files with 142 additions and 89 deletions
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-BWn_FP8l.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-nixix_KO.css">
<script type="module" crossorigin src="/admin/assets/index-S93JCAwW.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-66aoa8zi.css">
</head>
<body>
<div id="app"></div>
+15 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"observer-server/biz/consts"
"observer-server/biz/model/entity"
@@ -22,7 +23,6 @@ func init() {
dataset_id INTEGER NOT NULL,
filename TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'manual',
prompt TEXT,
animal_count INTEGER NOT NULL DEFAULT 0,
labels_json TEXT,
created_at TEXT NOT NULL,
@@ -35,6 +35,20 @@ func init() {
// 注意 EnsureColumn 的 ddl 参数须自带列名(拼接为 ALTER TABLE ADD COLUMN <ddl>
common.EnsureColumn(ctx, consts.TableDatasetImage, "labels_json", "labels_json TEXT")
common.EnsureColumn(ctx, consts.TableDatasetImage, "animal_count", "animal_count INTEGER NOT NULL DEFAULT 0")
// 存量库迁移:删除 prompt 列(生成提示词不再存储,2026-09-02 决策)
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(dataset_image)")
if err != nil {
panic(err)
}
for _, col := range cols {
if gconv.String(col["name"]) == "prompt" {
if _, err := g.DB().Exec(ctx, "ALTER TABLE dataset_image DROP COLUMN prompt"); err != nil {
panic(err)
}
g.Log().Warningf(ctx, "存量表 dataset_image 已迁移:删除 prompt 列")
break
}
}
}
// Insert 插入图片记录
@@ -43,7 +57,6 @@ func (d *datasetImageDao) Insert(ctx context.Context, m *entity.DatasetImage) (i
"dataset_id": m.DatasetId,
"filename": m.Filename,
"source": m.Source,
"prompt": m.Prompt,
"animal_count": m.AnimalCount,
"created_at": m.CreatedAt,
}).Insert()
-1
View File
@@ -178,7 +178,6 @@ type AdminImageItem struct {
Id int64 `json:"id"`
Filename string `json:"filename"`
Source string `json:"source"` // manual | ai
Prompt string `json:"prompt"`
Url string `json:"url"`
CreatedAt *gtime.Time `json:"createdAt"`
}
+2 -4
View File
@@ -2,15 +2,13 @@ package entity
import "github.com/gogf/gf/v2/os/gtime"
// DatasetImage 数据集图片:文件在 datasets/<数据集名>/<filename>
// AI 生成图记录 prompt(付费资产,追溯用);标注为 JSON 数组存本表
// (元素同 dto.AdminLabelBoxclass/cx/cy/w/h/confidenceYOLO 归一化)。
// DatasetImage 数据集图片:文件在 datasets/<数据集名>/<filename>
// 标注为 JSON 数组存本表(元素同 dto.AdminLabelBoxclass/cx/cy/w/h/confidenceYOLO 归一化)。
type DatasetImage struct {
Id int64 `json:"id" orm:"id" description:"自增主键"`
DatasetId int64 `json:"datasetId" orm:"dataset_id" description:"所属数据集"`
Filename string `json:"filename" orm:"filename" description:"唯一文件名(防重名加时间戳后缀)"`
Source string `json:"source" orm:"source" description:"manual|ai"`
Prompt string `json:"prompt" orm:"prompt" description:"AI 生成图提示词"`
AnimalCount int `json:"animalCount" orm:"animal_count" description:"AI 生成时的目标数量上限(0=未限定),自动标注按置信度裁剪到此数"`
LabelsJson string `json:"labelsJson" orm:"labels_json" description:"标注 JSON 数组(AI 自动标注与人工标注同存,null/''/'[]'=未标注)"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
-2
View File
@@ -1274,7 +1274,6 @@ func (s *datasetService) runGenTask(taskId int64, dataset *entity.Dataset, req *
DatasetId: dataset.Id,
Filename: filename,
Source: "ai",
Prompt: prompt,
AnimalCount: 1,
CreatedAt: now,
})
@@ -1375,7 +1374,6 @@ func (s *datasetService) AdminListImages(ctx context.Context, req *dto.AdminData
Id: v.Id,
Filename: v.Filename,
Source: v.Source,
Prompt: v.Prompt,
Url: datasetImageUrl(ctx, dataset.Id, v.Filename),
CreatedAt: v.CreatedAt,
})
+7 -9
View File
@@ -335,20 +335,18 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
}
exclude = strings.Join(parts, " ")
}
// 物种:从生成提示词与数据集物种(gen_species 单值)匹配(匹配不到交给 VLM 从已确认框自行判断)
species := ""
if dataset.GenSpecies != "" && strings.Contains(img.Prompt, dataset.GenSpecies) {
species = dataset.GenSpecies
}
// 物种:数据集物种(gen_species)直接注入;提示词不再存储(2026-09-02 清理),
// 无法从提示词匹配,交给 VLM 从已确认框自行判断
speciesLine := "从已确认目标框自行判断物种"
if species != "" {
speciesLine = species
if dataset.GenSpecies != "" {
speciesLine = dataset.GenSpecies
}
prompt := fmt.Sprintf(
"你是野生动物监测照片分析助手。\n物种:%s\n拍摄场景与时间光线线索:%s\n已确认目标位置(归一化 cx,cy,w,h,已排除,不要再输出这些位置):%s\n"+
"你是野生动物监测照片分析助手。\n物种:%s\n已确认目标位置(归一化 cx,cy,w,h):%s\n"+
"不要出现与已有位置绘制矩形重叠的框。\n"+
"请基于环境、天气、光线与该物种习性(觅食路线、隐蔽处、阴影边缘、植被深处),分析除已确认位置外还可能藏匿个体的位置。\n"+
"输出归一化 bbox 的 JSON 数组:[{\"cx\":0.5,\"cy\":0.5,\"w\":0.2,\"h\":0.15}],最多 3 个,按可能性从高到低;确无可能则输出 []。只输出 JSON 数组本身,不要其他文字。",
speciesLine, img.Prompt, exclude)
speciesLine, exclude)
content, err := common.QwenVL(ctx, data, imageMime(img.Filename), prompt)
if err != nil {
return nil, gerror.Wrap(err, "VLM 推理失败")
Binary file not shown.
+7 -6
View File
@@ -83,13 +83,13 @@ CREATE TABLE IF NOT EXISTS dataset_image (
dataset_id INTEGER NOT NULL, -- → dataset.id
filename TEXT NOT NULL, -- 唯一文件名(防重名加时间戳后缀)
source TEXT NOT NULL DEFAULT 'manual', -- manual | ai
prompt TEXT, -- AI 生成图记录提示词(追溯用)
labels_json TEXT, -- 标注 JSON 数组(YOLO 归一化 xywh+类别+置信度),AI 自动标注直写、人工仅审核确认(2026-08-28:疑似确认/误检删除,不手动画框),null/''/'[]'=未标注
created_at TEXT NOT NULL,
UNIQUE (dataset_id, filename)
);
-- 存量库迁移:EnsureColumn 逐列检测补列(v9);candidates_json 列已随 v10 删除(DROP COLUMN);
-- model_version.model_file 列已随 v11 删除(模型无存档回退机制,只留 latest.tflite
-- model_version.model_file 列已随 v11 删除(模型无存档回退机制,只留 latest.tflite
-- dataset_image.prompt 列已随 2026-09-02 删除(生成提示词不再存储/展示,生成仍临时使用但不落库)
CREATE TABLE IF NOT EXISTS model_training (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -319,7 +319,7 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 tokeniOS 不
- 图片目录:`app.datasetDir`(默认 `./workspace/`)下 `datasets/<数据集名>/`(图片平铺,文件名唯一防重名);标注存 `dataset_image.labels_json`JSON 数组,每元素 `{class,cx,cy,w,h,confidence}` 归一化,AI 自动标注直写、人工仅审核确认——2026-08-28:疑似确认/误检删除,不手动画框);**历史 labels/ txt 目录与 yolo 暂存目录已删除(2026-08-26,标注全在 DB,训练集流式化不落本地盘)**
- **封面命名规范(2026-08-26)**:封面文件不得固定命名 `cover`,上传时解码为 **jpg 格式 + UUIDv4 命名**`common.UuidV4()` 生成,crypto/rand 无第三方依赖,落盘 `datasets/<name>/<uuid>.jpg`,旧封面删除);DB `cover` 列只存文件名,前端用 DB 值拼 URL 零改动;删除/校验按 `isCoverName` 正则(UUID v4 + `.jpg` 后缀)识别,不认固定名——历史 `cover.jpg` 等存量封面启动时 `MigrateLegacyCovers` 幂等重命名迁移
- DB 只存文件名/来源/prompt 等元数据(`dataset` / `dataset_image`),**禁止图片进库**
- DB 只存文件名/来源等元数据(`dataset` / `dataset_image`;生成提示词不落库,2026-09-02 起),**禁止图片进库**
- 生成/上传图片是**付费资产**:删除接口必须带前端确认文案(提示 AI 生成图有成本);删除 = 删文件 + 删记录,目录清理
- 训练机与 Go 服务器可能异机:训练前按 `training` 通道同步(subprocess 同机 cp、ssh 异机 scp);**数据集在训练机上的权威路径** `training.workdir`/`training.datasetDir`/`<name>/`DB `model_training.dataset` 只存数据集名
@@ -330,10 +330,10 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 tokeniOS 不
- `localai`(本机/训练机 local-ai`POST {baseUrl}/v1/images/generations`OpenAI 兼容):`baseUrl` + `model`(local-ai 上加载的模型名,如 qwen-image);返回 url 为容器内 localhost 地址,下载时替换为 `baseUrl` 的 hoststep/cfg 等采样参数用 local-ai 模型配置默认值,不随接口传;生成耗时取决于模型与硬件,不设调用超时(见下)
- `baseUrl`/`apiKey` 缺失时对应 provider 视为未配置,生成接口返回「图像生成服务未配置」
- 尺寸参数由前端传(固定竖版 704x1248,比例与既有训练图 1152x2048 一致、32 倍数对齐);**实测竖版图在 local-ai 上极慢(约 26 分钟/张,Qwen-Image 行列注意力对非方形输入效率差,与分辨率关系不大;方形 1024x1024 仅 5 分钟)**,前端等待按此预估,一次建议 1 张
- **生成异步化(2026-08-28count 1..1000**`/datasets/generate` 仅做校验(provider/数据集/标注服务/Serial 内并发检查已有 running 生成任务)+ 插 `gen_task` 落 running 记录即返回 `{taskId, total}`(毫秒级,前端 10s 超时无忧);后台协程逐张生成(`context.Background()` 生命周期模式,与预标注 `runDetection` 同构;`imageGen.poolSize` 并发池,z-image 显存独占默认 1),**每张不设调用超时(2026-08-29)**——异步任务超时只会造成假失败:超时仅放弃等待、取消不了 local-ai backend,任务置 failed 而模型仍被占用、poolSize=1 的池被占死、后续请求连环超时;失败判定完全由 provider 真实返回决定(dashscope 内部自限 120s 轮询不受影响);极端情况 local-ai 挂死时任务保持 running(状态诚实),由启动恢复兜底;逐张落盘 + 入库(记录 prompt 便于追溯)+ Serial 更新进度,完成(或部分失败)后对本次新增图触发自动标注,置 done/failed 由前端轮询 `/datasets/gen-task` 呈现;中途失败保留已生成图(不删除——付费资产原则);启动恢复 `recoverGenTasks` 孤儿 running 置 failed;VLM 补检与生成显存互斥(该数据集有 running 生成任务时拒绝)
- **生成异步化(2026-08-28count 1..1000**`/datasets/generate` 仅做校验(provider/数据集/标注服务/Serial 内并发检查已有 running 生成任务)+ 插 `gen_task` 落 running 记录即返回 `{taskId, total}`(毫秒级,前端 10s 超时无忧);后台协程逐张生成(`context.Background()` 生命周期模式,与预标注 `runDetection` 同构;`imageGen.poolSize` 并发池,z-image 显存独占默认 1),**每张不设调用超时(2026-08-29)**——异步任务超时只会造成假失败:超时仅放弃等待、取消不了 local-ai backend,任务置 failed 而模型仍被占用、poolSize=1 的池被占死、后续请求连环超时;失败判定完全由 provider 真实返回决定(dashscope 内部自限 120s 轮询不受影响);极端情况 local-ai 挂死时任务保持 running(状态诚实),由启动恢复兜底;逐张落盘 + 入库(2026-09-02 起不记录 prompt——生成提示词临时使用不落库)+ Serial 更新进度,完成(或部分失败)后对本次新增图触发自动标注,置 done/failed 由前端轮询 `/datasets/gen-task` 呈现;中途失败保留已生成图(不删除——付费资产原则);启动恢复 `recoverGenTasks` 孤儿 running 置 failed;VLM 补检与生成显存互斥(该数据集有 running 生成任务时拒绝)
- prompt 手填覆盖;留空走 `imageGen.promptTemplate` 通用模板逐张组装(**物种/场景/动作/遮挡/轮廓色/站高池均从数据集表读取(见下节),每张独立组装保持多样性**;**性别每张随机雄/雌(2026-08-28)**——两性体型大小与外观差异大(雉鸡雄艳雌褐、野鸭雄艳雌素等),随机让训练数据覆盖两性形态,`{species}` 替换为「雄/雌性+物种」;物种只写名字不写羽毛细节,细节会拉近镜头);**位置/镜头描述(2026-08-31 v4 最终定案:手机原相机主摄不变焦)**:2026-08-28 曾定「不得包含目标位置描述」(上部1/3 弱约束遵循力差、角度与位置耦合沉底);2026-08-31 用户确认恢复距离感锚定,镜头词四版演进:广角(构图先验=前景大主体,用户实测野鸡在画面下半部)→ 长焦(**长焦物理放大与 13px 像素数学矛盾**,用户实测野鸡占半图宽)→ 全删 → **「手机原相机主摄不变焦」**(用户提议;与 13px 像素数学完全自洽——公式即按主摄 vfov 52° 计算;手机拍远处目标天然显小,先验同向;顺带对齐推理端域——App 就是手机拍摄);保留「**在{distanceWord}的地平线附近**」位置锚定(地平线是远景场景锚点,不同于上部1/3 弱约束);**远景小目标模板 `promptTemplateTiny`2026-08-31**:尺寸占比 <2% 时切换——目标小到动作/遮挡/性别细节无法呈现,保留细节描写(低头啄食/只露出头背)会迫使模型把目标画大(语义矛盾:1% 目标不可见细节),只留「远到看不清任何细节,只是隐约可辨的小点」
- **生成图距离/数量校验(2026-08-28)**:表单填 `distance`(米,固定值) 与 `animalCount` 后逐张启用;**距离校验已取消(2026-08-28)**:生成即入库,无逐张拒检;参数与公式见 git 历史(物理公式 d=体高×focalPx÷像素高)
- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.6-35b-a3b(+mmproj) 以「图片+物种(生成 prompt 物种池匹配)+场景光线线索+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_jsonVL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.6-35b-a3b yaml 自带 mmproj 实为多模态模型(生成校验);自动标注框数按置信度降序裁剪 ≤ animal_count(存 dataset_image.animal_count 列),估算距离 = 物种站高 × focalPx ÷ 最大框高像素(focalPx=`图高/2/tan(vfov/2)`,站高按物种查 `imageGen.speciesHeights`,单位 cm 换算为米,未配置物种代码兜底 35cm;**2026-08-28 语义修正**speciesHeights 为站高(垂直尺度,鸟=脚到头顶、兽=蹲坐高),勿用体长——与提示词「高度」描述及检测框高自洽;避免跨物种共用一个标定系数)(K=1.8:6%≈30m、4.5%≈40m,K 隐含镜头视场角假设);**镜头固定主摄(2026-08-28 定案)**vfov 固定 `imageGen.assumedVfovDeg=52°`(手机默认主摄;曾随机 [15,90] 覆盖面广但目标像素波动 7~57px 不可控,曾试超广角 90 亦否决);**场景/动作按数据集(2026-08-28**`imageGen.sceneByDataset`/`actionByDataset` 按数据集名配池(鸭→水面、雉鸡→灌丛草丛、兔→草坡荒地、鹌鹑→草丛),未配置回退通用 scenes/actions**遮挡描述(2026-08-28**:模板加 `{occlusion}` 占位(插在「在{action}」之后)——身体完全暴露的个体对识别/标注训练无意义,野外目标必然存在植被等遮挡;`imageGen.occlusionByDataset` 按数据集配遮挡池(雉鸡→草丛灌丛半掩、兔→荒草只露头耳、鸭→芦苇水面半没、鹌鹑→密草藏身),未配置回退通用 `imageGen.occlusions`;池内条目覆盖轻(身体半掩)~重(只露头背)遮挡,每张随机取一条,杜绝完全暴露;遮挡物多样(杂草/树叶/枝条,2026-08-28 补树叶)且须与 sceneByDataset 场景自洽;遮挡后可见部分小于站高,检测框高与距离估算按可见部分仍自洽(真实场景同此);**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的{tone}轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种;`{tone}` 轮廓色词按 `imageGen.speciesTone.<物种>` 取,默认深色剪影,**白雉鸡配浅色**——白羽个体用「深色轮廓」描述会与模型渲染矛盾,2026-08-28)维持远景感,手填提示词仅追加数量约束;**2026-08-31 改版(最终定案)**:镜头词全删(广角/长焦实测均失败);「{distanceWord}有且仅有」→「在{distanceWord}的地平线附近有且仅有」;{action} 从句末主体降级至遮挡之后(动作=叙事焦点=视觉焦点,弱化动作降低主体视觉权重);{sizeHint} 由「像素数+占比」改「画面占比分级尺寸词(微小几乎难辨/很小/较小/中等/较大)+占比」(量化像素数与固定物体类比词针尖/米粒/拳头均不跨物种/距离通用,用户否决);**远景小目标模板 `promptTemplateTiny`**:占比 <2% 时切换,动作/遮挡/性别细节全删(保留细节描写迫使模型画大);不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留)
- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.6-35b-a3b(+mmproj) 以「图片+物种(数据集单物种 gen_species 注入;2026-09-02 prompt 不再存储,原「prompt 物种名才注入」匹配逻辑删除)+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_jsonVL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.6-35b-a3b yaml 自带 mmproj 实为多模态模型(生成校验);自动标注框数按置信度降序裁剪 ≤ animal_count(存 dataset_image.animal_count 列),估算距离 = 物种站高 × focalPx ÷ 最大框高像素(focalPx=`图高/2/tan(vfov/2)`,站高按物种查 `imageGen.speciesHeights`,单位 cm 换算为米,未配置物种代码兜底 35cm;**2026-08-28 语义修正**speciesHeights 为站高(垂直尺度,鸟=脚到头顶、兽=蹲坐高),勿用体长——与提示词「高度」描述及检测框高自洽;避免跨物种共用一个标定系数)(K=1.8:6%≈30m、4.5%≈40m,K 隐含镜头视场角假设);**镜头固定主摄(2026-08-28 定案)**vfov 固定 `imageGen.assumedVfovDeg=52°`(手机默认主摄;曾随机 [15,90] 覆盖面广但目标像素波动 7~57px 不可控,曾试超广角 90 亦否决);**场景/动作按数据集(2026-08-28**`imageGen.sceneByDataset`/`actionByDataset` 按数据集名配池(鸭→水面、雉鸡→灌丛草丛、兔→草坡荒地、鹌鹑→草丛),未配置回退通用 scenes/actions**遮挡描述(2026-08-28**:模板加 `{occlusion}` 占位(插在「在{action}」之后)——身体完全暴露的个体对识别/标注训练无意义,野外目标必然存在植被等遮挡;`imageGen.occlusionByDataset` 按数据集配遮挡池(雉鸡→草丛灌丛半掩、兔→荒草只露头耳、鸭→芦苇水面半没、鹌鹑→密草藏身),未配置回退通用 `imageGen.occlusions`;池内条目覆盖轻(身体半掩)~重(只露头背)遮挡,每张随机取一条,杜绝完全暴露;遮挡物多样(杂草/树叶/枝条,2026-08-28 补树叶)且须与 sceneByDataset 场景自洽;遮挡后可见部分小于站高,检测框高与距离估算按可见部分仍自洽(真实场景同此);**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的{tone}轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种;`{tone}` 轮廓色词按 `imageGen.speciesTone.<物种>` 取,默认深色剪影,**白雉鸡配浅色**——白羽个体用「深色轮廓」描述会与模型渲染矛盾,2026-08-28)维持远景感,手填提示词仅追加数量约束;**2026-08-31 改版(最终定案)**:镜头词全删(广角/长焦实测均失败);「{distanceWord}有且仅有」→「在{distanceWord}的地平线附近有且仅有」;{action} 从句末主体降级至遮挡之后(动作=叙事焦点=视觉焦点,弱化动作降低主体视觉权重);{sizeHint} 由「像素数+占比」改「画面占比分级尺寸词(微小几乎难辨/很小/较小/中等/较大)+占比」(量化像素数与固定物体类比词针尖/米粒/拳头均不跨物种/距离通用,用户否决);**远景小目标模板 `promptTemplateTiny`**:占比 <2% 时切换,动作/遮挡/性别细节全删(保留细节描写迫使模型画大);不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留)
### 生成参数存储于数据集表(2026-08-28)
@@ -395,7 +395,8 @@ training:
### 标注工作台(依赖 local-ai 可达;2026-08-26 布局重构)
- `localAi` 配置节点:`{ baseUrl: "http://127.0.0.1:18080", model: "rfdetr-xlarge", threshold: 0.08, confConfirmed: 0.2, overlapThreshold: 0.3 }`——**部署前提:RF-DETR 服务须从 Go 服务器可达**(现跑在 Mac 上,部署时搬服务器/训练机;**未配置时图片上传/生成接口直接报错**——标注是强语义,不允许产生无标注图片;配置调不通则任务 failed,页顶错误条展示,不自动重试);**端点为配置唯一来源,无运行时覆盖**
- **预标注自动触发(2026-08-26 决策,无手动按钮)**:手动上传/AI 生成图片入库成功后,自动对**本次新增图**发起标注;**`localAi` 未配置 → 上传/生成接口直接报错**(同步检查,生成场景在第一张生成前检查避免付费资产生成后标不了);**已有 running 标注任务(忙)→ 不报错**:上传/生成照常成功,新图由「任务成功完成后自动补标未标注图」机制兜底(每轮任务成功结束时检查该数据集未标注图,有则自动续一轮只标未标注图——失败任务不续,防配置坏时无限重试);service 逐张调 local-ai 推理(common 池并行,`label_task` 记录 total/done 进度)→ 扫描结果(YOLO 归一化 xywh + 置信度 + 建议类别,conf≥confConfirmed 为 class 0**直写 `dataset_image.labels_json`(重跑覆盖该图标注)**;**重叠去重(2026-08-26 修订)**:NMS 风格、按置信度降序依次保留,与已保留框**重叠比(minIoU = 交叠面积/两框较小面积)> `localAi.overlapThreshold`(默认 0.3)**的框剔除(同目标被重复检出只留置信度最高者,跨 class 去重;人工画框不参与去重)——**用 minIoU 而非 IoU**RF-DETR 对同一目标常输出一大一小两个框(标准 IoU 仅 0.3~0.5 会漏杀,实测红黄重叠即此形态),大框套小框时小框被覆盖比例高,minIoU 命中;相邻目标两框互有外露,minIoU 通常 < 0.3;**全图扫描**(项目既定规则:不套用生成规格的位置裁剪,候选宁多勿漏);不设候选确认两阶段(2026-08-26 撤销 candidates_json);`POST /admin/label-tasks` 接口保留(详情页「全量标注」按钮,2026-08-26 恢复),可手动全量/指定图重标
- **预标注自动触发(2026-08-26 决策,无手动按钮)**:手动上传/AI 生成图片入库成功后,自动对**本次新增图**发起标注;**`localAi` 未配置 → 上传/生成接口直接报错**(同步检查,生成场景在第一张生成前检查避免付费资产生成后标不了);**已有 running 标注任务(忙)→ 不报错**:上传/生成照常成功,新图由「任务成功完成后自动补标未标注图」机制兜底(每轮任务成功结束时检查该数据集未标注图,有则自动续一轮只标未标注图——失败任务不续,防配置坏时无限重试);service 逐张调 local-ai 推理(common 池并行,`label_task` 记录 total/done 进度)→ 扫描结果(YOLO 归一化 xywh + 置信度 + 建议类别,conf≥confConfirmed 为 class 0**直写 `dataset_image.labels_json`(重跑覆盖该图标注)**;**重叠去重(2026-08-26 修订)**:NMS 风格、按置信度降序依次保留,与已保留框**重叠比(minIoU = 交叠面积/两框较小面积)> `localAi.overlapThreshold`(默认 0.3)**的框剔除(同目标被重复检出只留置信度最高者,跨 class 去重;人工画框不参与去重)——**用 minIoU 而非 IoU**RF-DETR 对同一目标常输出一大一小两个框(标准 IoU 仅 0.3~0.5 会漏杀,实测红黄重叠即此形态),大框套小框时小框被覆盖比例高,minIoU 命中;相邻目标两框互有外露,minIoU 通常 < 0.3;**全图扫描**(项目既定规则:不套用生成规格的位置裁剪,候选宁多勿漏);不设候选确认两阶段(2026-08-26 撤销 candidates_json);`POST /admin/label-tasks` 接口保留(服务端 `filenames` 参数支持指定图批量重标,2026-09-02 前端入口收敛后仍可全量——不传 filenames 即全量扫描)
- **手动标注入口收敛(2026-09-02 决策)**:详情页删除「全量标注」按钮与图片卡片「审核」按钮,**标注入口仅两处(勾选图片驱动,顶栏)**:①「预标」= POST `/admin/label-tasks` 传选中图 `filenames`(只扫选中图,覆盖各图已有标注);②「补标」= 对选中图逐张 POST `/datasets/images/vlm-review`(VLM 藏匿位补检,追加疑似框,批量=前端循环单图接口,依赖已确认框作为排除基准)——VLM 补检按钮从标注编辑器迁出(2026-09-02);点卡片缩略图仍进人工编辑器(确认/清理/画框,编辑器内不再有 VLM 按钮),编辑器文案「图片审核」→「图片标注」;服务端全量重标能力保留但不再有 UI 入口
- **详情页布局(无选项卡)**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照;页顶标注任务进度条(自动触发任务进度,含错误展示);标注图 = 原图 + 标注框叠加(只读 canvas,与工作台同一绘制逻辑)
- **弹窗标注**:点击原图/标注图打开放大弹窗(大 canvas,1152x2048 原尺寸),支持两点画框/点框删除/清空/类别切换(确认 0/疑似 1)/上下一张/保存——**AI 自动标注与人工框同层可编辑**(含清理 AI 框);保存即整体覆写 `dataset_image.labels_json`JSON 数组,YOLO 归一化 xywh+类别+置信度,空=清空标注);未保存修改切换图片有确认
- 无历史标注任务时详情页工作台数据源为 `GET /admin/label-workbench?dataset_id=`(图片 + 全量标注框),避免依赖人工先跑一次任务