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 e9b923b..10a2f62 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ 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 供前端归类