From 3597eb264cc9cc3b19d861e4b65b7817ec89af39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Wed, 2 Sep 2026 17:00:47 +0800 Subject: [PATCH] 1 --- server/README.md | 2 +- server/biz/consts/consts.go | 15 ++++++++------- server/biz/service/dataset.go | 28 +++++++++------------------- server/data/observer.db | Bin 655360 -> 655360 bytes server/技术设计.md | 6 +++--- 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/server/README.md b/server/README.md index fbf6cce..afaa7b3 100644 --- a/server/README.md +++ b/server/README.md @@ -185,7 +185,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow | POST | `/admin/datasets/gen-pools` | 重新生成数据集生成参数池 `{"datasetId":1}`:按数据集名(物种)调 VLM(qwen3.6-35b-a3b) 生成轮廓色/站高/场景/动作/遮挡并写表(覆盖旧值);失败报错保留旧值 | | POST | `/admin/datasets/images/vlm-review` | VLM 藏匿位补检 `{"datasetId":1,"imageId":5}`(两阶段标注第二阶段,**须与图像生成显存互斥**):qwen3.6-35b-a3b(+mmproj) 排除已确认框,按环境/光线/习性推理藏匿位,追加 ≤3 个疑似框(class 1)进同一 labels_json | | POST | `/admin/datasets/cover` | 上传数据集封面(multipart:`datasetId`+`file`,jpg/jpeg/png ≤10MB):**自动缩放 1248x704 + 转 jpg + UUID 命名**落盘并覆盖旧封面 | -| POST | `/admin/datasets/cover/generate` | 生成数据集封面 `{"datasetId":1}`(z-image 文生图:16:9(1248x704)、1 雄 1 雌,物种取 gen_species 空回退数据集名):覆盖旧封面,返回 `{cover}` 新文件名;`datasetId=0` 时传 `{"name":"家鸽"}` 新建预生成(数据集未创建,封面仅落盘不写库,创建请求带 cover 回传写库);与生成任务显存互斥,失败不覆盖旧封面 | +| POST | `/admin/datasets/cover/generate` | 生成数据集封面 `{"datasetId":1}`(z-image 文生图:16:9(1248x704)、1 雄 1 雌,物种取 gen_species 空回退数据集名):覆盖旧封面,返回 `{cover}` 新文件名;`datasetId=0` 时传 `{"name":"家鸽"}` 新建预生成(数据集未创建,封面仅落盘不写库,创建请求带 cover 回传写库);生成任务进行中亦可直接调用(与任务图片同走 z-image、由 local-ai 服务端排队串行,2026-09-02 放开整任务拒绝),失败不覆盖旧封面 | | GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位;`datasetId=0` 时按 `name`+`filename` 直读——新建对话框预生成封面回显) | | POST | `/admin/datasets/cover/delete` | 删除数据集封面 `{"datasetId":1}`:删文件 + 清 cover 字段 | | POST | `/admin/datasets/upload` | 上传图片(multipart/form-data:`datasetId` + `files` 多张,仅接受 `.jpg/.jpeg/.png`),存 `app.datasetDir`/`datasets//`,逐张入库 | diff --git a/server/biz/consts/consts.go b/server/biz/consts/consts.go index f702e1f..e27e49f 100644 --- a/server/biz/consts/consts.go +++ b/server/biz/consts/consts.go @@ -72,6 +72,7 @@ const ( // 按最大 class 0 框的归一化框高 h 划分,边界为占图高百分比,下含上不含。 // <2% 极远档豁免——recall 瓶颈档只缺不多,不进候选清单(配额 0 固定不适用); // 其余档 Quota 为「目标保留张数」缺省值,preview 请求可覆盖(管理端配额表默认展示即此表)。 +// 配额随目标尺寸单调递减(2026-09-02 用户定案):尺寸越大越易检出、冗余切得越狠,小档护稀缺硬样本。 type CleanBucket struct { Label string MinPct float64 @@ -81,11 +82,11 @@ type CleanBucket struct { var CleanBuckets = []CleanBucket{ {"<2%", 0, 2, 0}, - {"2-3%", 2, 3, 30}, - {"3-4%", 3, 4, 40}, - {"4-6%", 4, 6, 50}, - {"6-8%", 6, 8, 40}, - {"8-12%", 8, 12, 40}, - {"12-20%", 12, 20, 30}, - {">20%", 20, 100, 20}, + {"2-3%", 2, 3, 60}, + {"3-4%", 3, 4, 50}, + {"4-6%", 4, 6, 40}, + {"6-8%", 6, 8, 30}, + {"8-12%", 8, 12, 20}, + {"12-20%", 12, 20, 15}, + {">20%", 20, 100, 10}, } diff --git a/server/biz/service/dataset.go b/server/biz/service/dataset.go index aa2618f..bd9d965 100644 --- a/server/biz/service/dataset.go +++ b/server/biz/service/dataset.go @@ -631,17 +631,12 @@ func (s *datasetService) genCoverWithImageGen(ctx context.Context, datasetId int if species == "" { species = datasetName } - // 显存互斥:z-image 生成任务占显存时跳过(同步等待不现实) - running, err := dao.GenTask.ListRunning(ctx) - if err != nil { - return "", err - } - if len(running) > 0 { - return "", gerror.New("生成任务进行中(z-image 占用显存),未生成封面") - } + // 生成任务进行中直接调用:封面与任务图片同为 z-image 单张请求,由 local-ai 服务端排队串行, + // 无需整任务互斥(曾按 ListRunning 整任务拒绝,2026-09-02 放开) prompt := fmt.Sprintf("野外实拍照片:一只雄性%s和一只雌性%s并排站立在开阔的自然栖息地中,"+ "雄雌各一只清晰可见,16:9 横幅构图,真实照片质感,光线自然,画面清晰美观", species, species) - genCtx, cancel := context.WithTimeout(ctx, 120*time.Second) + // 180s:放开互斥后须覆盖 local-ai 排队等待(排队 ≤ 当前一张 ~40s + 自身生成),曾 120s 仅直发余量 + genCtx, cancel := context.WithTimeout(ctx, 180*time.Second) defer cancel() // 直接指定封面统一尺寸 1248x704(z-image 接受 16 整除尺寸);resizeCover 兜底统一规格 data, err := provider.Generate(genCtx, prompt, "1248x704") @@ -703,21 +698,15 @@ func (s *datasetService) AdminGenPools(ctx context.Context, req *dto.AdminGenPoo // genPoolsWithVLM 调 qwen3.6-35b-a3b 生成数据集生成参数池(单物种规则:物种=数据集名, // VLM 只生成轮廓色/站高/场景/动作/遮挡池)。 -// 无 localAi 配置/显存被生成任务占用/JSON 非法/校验不过 → 返回 error(调用方决定是否阻断); +// 无 localAi 配置/JSON 非法/校验不过 → 返回 error(调用方决定是否阻断); +// 生成任务进行中直接调用——VLM 与 z-image 同机共存,请求由 local-ai 服务端排队 +// (曾按 ListRunning 整任务拒绝,2026-09-02 放开); // 校验通过返回 entity.Dataset(仅 7 个池字段,其余空)。 // 纯文本场景下 llama.cpp mmproj 需图片输入,传 64x64 纯灰占位图(提示词声明忽略图片)。 func (s *datasetService) genPoolsWithVLM(ctx context.Context, datasetName string) (*entity.Dataset, error) { if common.LocalAiClient(ctx) == nil { return nil, nil // 未配置标注服务:不生成也不报错(池为空,生成图片时提示补参数) } - // 显存互斥:z-image 生成任务占显存时跳过(同步等待不现实——生成任务可达小时级) - running, err := dao.GenTask.ListRunning(ctx) - if err != nil { - return nil, err - } - if len(running) > 0 { - return nil, gerror.New("生成任务进行中(z-image 占用显存),暂无法生成参数,稍后重试") - } target := datasetName prompt := fmt.Sprintf( "你是野外野生动物监测数据集专家。请为数据集「%s」设计 AI 生成训练图的生成参数池。目标动物:%s。\n"+ @@ -738,7 +727,8 @@ func (s *datasetService) genPoolsWithVLM(ctx context.Context, datasetName string if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 60}); err != nil { return nil, gerror.Wrap(err, "构造 VLM 输入失败") } - genCtx, cancel := context.WithTimeout(ctx, 90*time.Second) + // 120s:放开互斥后排队等待(生成任务图片 ~40s/张)+ 自身推理余量(曾 90s 仅直发余量) + genCtx, cancel := context.WithTimeout(ctx, 120*time.Second) defer cancel() content, err := common.QwenVL(genCtx, buf.Bytes(), "image/jpeg", prompt) if err != nil { diff --git a/server/data/observer.db b/server/data/observer.db index e9b923b1f2ba2d99da299e8136698dd5b4c89be5..10a2f6281d493d33e3f7b701184f55ea1811a257 100644 GIT binary patch delta 25067 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 delta 15380 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 a=rvAzPTj2hMHx%4A>+?c>)NlXr2H3t?%Uu1 diff --git a/server/技术设计.md b/server/技术设计.md index 320b78e..d2e471d 100644 --- a/server/技术设计.md +++ b/server/技术设计.md @@ -340,8 +340,8 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 token;iOS 不 - **决策**:per-dataset 生成参数(物种/轮廓色/站高/场景/动作/遮挡/标注类别名)从 `config.yml` 迁入 `dataset` 表——配置随数据集走,新建数据集用 VLM(qwen3.6-35b-a3b)自动生成,**config.yml 不再兜底**(表为空即缺失,不静默回退) - **单物种规则(2026-08-28 定案)**:每数据集只对应**一个物种**(每数据集训练一个模型,类别固定 `[物种, suspect]`);**生成表单无任何物种/动物名称输入,物种直接取数据集名**(生成提示词按数据集名组装);不同物种须拆到不同数据集(近缘种不共数据集) - **表结构(4 单值 + 3 JSON 数组,2026-08-28 简化)**:`gen_species`(单值 = 数据集物种,即标注类别 0 名)、`gen_tone`(单值 轮廓色词,白化个体配浅色)、`gen_heights`(REAL 数值 站高 cm)、`gen_scenes`/`gen_actions`/`gen_occlusions`(JSON 数组,各 ≥3 条,保持多样性)、`gen_classes`(单值 第二标注类别名 = "suspect",第一类别 = gen_species,写入训练 data.yaml 的 names);**简化动机**:单物种规则下原 JSON 对象/数组恒为 1 key 1 value(`[物种]`/`{物种:值}`),两性差异由提示词性别随机词表达,站高单值对距离感公式(像素高 = 站高 × focalPx ÷ 距离)影响 <1 像素量级,无需分键 -- **创建数据集同步生成**:`POST /datasets` 不接收任何物种/动物名称参数,**物种 = 数据集名**(单物种规则,新建对话框无动物名称输入框);调 `QwenVL`(64x64 占位图 + 文本,llama.cpp mmproj 需图片输入)输出严格 JSON `{tone, height_cm, scenes[], actions[], occlusions[]}`,校验(scenes/actions/occlusions ≥3 条、height_cm 数值 10~200、tone 为颜色词)后组装写表:`gen_species=物种`、`gen_tone=tone`、`gen_heights=height_cm`、`gen_classes="suspect"`;**站高属数据库数据(gen_heights),代码不硬编码物种→高度**——VLM 输出即最终值,界面不维护(2026-08-28 用户定案:gen_* 7 列全部仅由模型生成,编辑界面只读展示,值不准走 `gen-pools` 重新生成);**失败不阻断创建**——返回 `poolsGenerated:false` + `poolError`,前端提示(可事后 `POST /datasets/gen-pools` 补生成);显存互斥:存在任意 running 生成任务(z-image 占显存)时跳过生成并提示;前端创建请求超时放宽 180s -- **封面自动生成(2026-08-28)**:参数池生成成功后取数据集物种调 z-image 文生图,**16:9 横幅(1248x704,2026-08-31 由 1024x576 调整)**,提示词要求**「一只雄性 X 和一只雌性 X 并排站立」**(1 雄 1 雌),失败不阻断创建(返回 `coverGenerated:false` + `coverError`,可编辑模式重新生成);结果 `resizeCover` 转 jpg(统一 1248x704 兜底)后按封面规范落盘(UUIDv4.jpg,删除旧封面,写 `cover` 列);与生成任务显存互斥(有 running 生成任务时跳过) +- **创建数据集同步生成**:`POST /datasets` 不接收任何物种/动物名称参数,**物种 = 数据集名**(单物种规则,新建对话框无动物名称输入框);调 `QwenVL`(64x64 占位图 + 文本,llama.cpp mmproj 需图片输入)输出严格 JSON `{tone, height_cm, scenes[], actions[], occlusions[]}`,校验(scenes/actions/occlusions ≥3 条、height_cm 数值 10~200、tone 为颜色词)后组装写表:`gen_species=物种`、`gen_tone=tone`、`gen_heights=height_cm`、`gen_classes="suspect"`;**站高属数据库数据(gen_heights),代码不硬编码物种→高度**——VLM 输出即最终值,界面不维护(2026-08-28 用户定案:gen_* 7 列全部仅由模型生成,编辑界面只读展示,值不准走 `gen-pools` 重新生成);**失败不阻断创建**——返回 `poolsGenerated:false` + `poolError`,前端提示(可事后 `POST /datasets/gen-pools` 补生成);**2026-09-02 放开整任务拒绝**:生成任务进行中不再跳过(VLM 请求与任务图片均由 local-ai 服务端排队串行,此前按整任务显存互斥拒绝);VLM 调用超时 90s→120s 留排队余量;前端创建请求超时放宽 180s +- **封面自动生成(2026-08-28)**:参数池生成成功后取数据集物种调 z-image 文生图,**16:9 横幅(1248x704,2026-08-31 由 1024x576 调整)**,提示词要求**「一只雄性 X 和一只雌性 X 并排站立」**(1 雄 1 雌),失败不阻断创建(返回 `coverGenerated:false` + `coverError`,可编辑模式重新生成);结果 `resizeCover` 转 jpg(统一 1248x704 兜底)后按封面规范落盘(UUIDv4.jpg,删除旧封面,写 `cover` 列);**2026-09-02 放开整任务拒绝**:封面与任务图片同为 z-image 单张请求,由 local-ai 服务端排队串行,生成任务进行中直接调用(单张调用超时 120s→180s 覆盖排队等待) - **封面手动生成(2026-08-28)**:新增 `POST /datasets/cover/generate`(对话框「AI 生成封面」按钮)——**新建模式** `datasetId=0` + `name` 预生成:数据集未创建,封面仅落盘于数据集目录(物种=表单数据集名),创建请求带 `cover` 回传写库(创建接口 cover 非空且文件存在则跳过自动生成);**编辑模式** `datasetId` 完整链路(物种取 gen_species 空回退数据集名,写库+删旧封面);返回 `{cover}` 供前端回显;封面访问接口支持 `datasetId=0` 按 `name`+`filename` 直读(filename 须 UUID jpg 规范防路径穿越) - **生成图统一转 jpg(2026-08-28)**:凡模型产出的图片(z-image 训练图、封面)入库前经 `ensureJpeg` 解码校验,非 jpeg 一律重编码 jpg(Quality 92)——RF-DETR 按扩展名推断 mime,扩展名与编码不一致会报「Not a JPEG file」类错误;命名统一 `.jpg` - **封面统一尺寸(2026-08-31,用户定案)**:封面一律 **1248x704**(16:9 横幅,App 列表卡片展示规格)——AI 生成直接向 provider 指定该尺寸(z-image 接受 16 整除尺寸);手动上传任意尺寸经 `resizeCover` 中心裁剪缩放统一(上传限制放宽 2MB→10MB);存量封面启动时 `CompressExistingCovers` 幂等压缩覆盖写(非 1248x704 才重压,目标尺寸跳过) @@ -407,7 +407,7 @@ training: - **语义(区别于视觉判重)**:不是删「长得像」的图,而是按标注算目标尺寸、按**尺寸档配额**去重——易档(中近距离)堆量、难档(<2% 极远)稀缺是当前数据集冗余的真实形态(2026-09-02 实测:dHash 整图/目标裁剪双重判定,生成图逐张随机 scene/action 几乎无近重复,严格阈值可去近 0 张;而 环颈雉 280 张实拍中 4-8% 档 109 张占 39%——同场地连拍帧为主,删冗余几乎无损) - **目标尺寸口径**:`labels_json` 归一化框高 h 即目标占图高比例(**不需解码图片**);多框取最大 class 0 框定档(候选行展示的主尺寸);**档内保留取舍参考图内最小确认目标**(2026-09-02:多目标图里的小目标样本比主目标稀缺,同档冗余取舍先保它们);像素高 ≈ h×1280(训练尺度,等比近似);**仅 class 1 疑似图与空检出 `[]` 图不入清单**(无确认目标,无从定尺寸,等人工确认后自然进入统计) -- **档位**:按图高占比细档 <2% / 2-3 / 3-4 / 4-6 / 6-8 / 8-12 / 12-20 / >20%——宽档(如 4-8%)跨 2 倍尺度,直接按宽档配额会把半档删光,须细档分别设配额;**<2% 极远档固定豁免**(recall 瓶颈档,只缺不多);各档配额请求可传,缺省用代码默认(consts.go) +- **档位**:按图高占比细档 <2% / 2-3 / 3-4 / 4-6 / 6-8 / 8-12 / 12-20 / >20%——宽档(如 4-8%)跨 2 倍尺度,直接按宽档配额会把半档删光,须细档分别设配额;**<2% 极远档固定豁免**(recall 瓶颈档,只缺不多);各档配额请求可传,缺省用代码默认(consts.go);**配额随目标尺寸单调递减(2026-09-02 用户定案)**:尺寸越大越易检出、单张边际训练价值越低,冗余切得越狠——小档护稀缺硬样本,默认 2-3%:60 / 3-4%:50 / 4-6%:40 / 6-8%:30 / 8-12%:20 / 12-20%:15 / >20%:10(曾中档 4-6% 峰值为 50、小档仅 30,违反该定则) - **桶内多样性保留**:超配桶内保留谁不随机——**先按图内最小确认目标(class 0)尺寸升序扫描**(小目标样本稀缺,同档图里带更小目标的先占保留名额;同最小尺寸按 id 保持稳定),整图 dHash(common 层,9x8 灰度 64 位)与已保留图最小汉明距离大于阈值才留,**留够配额即停,其余全进候选清单**(连拍帧天然相邻近哈希,只留首帧;不同场景同尺寸的目标尽量都保);候选行展示主尺寸 + 最小目标占比 + 目标数,供人工判断取舍 - **执行语义(排除而非删除)**:候选清单人工过目后打 `dataset_image.clean_excluded=1`(EnsureColumn 迁移)——**只影响训练集打包**(prepare_yolo 跳过),图片/标注不动、列表/标注/预标全流程照常可见;**可恢复**(清除标记即回训练集,付费资产原则:清洗默认不删文件,物理删除仍走人工逐删) - 接口:`POST /admin/datasets/clean/preview`(datasetId + 可选 quotas → 各档分布/配额/超配候选清单 + 已排除列表);`POST /admin/datasets/clean/apply`(imageIds + exclude bool 批量置/清标记);**管理端详情页承载:「数据清洗」按钮在已标注 tab 工具条**(配额表可编辑 + 候选清单默认全选 → 执行排除),**已排除的图集中到「已清洗」tab 展示,逐张/批量恢复(apply exclude=false)**,图片列表接口返回 cleanExcluded 供前端归类