1
This commit is contained in:
+2
-2
@@ -189,7 +189,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow
|
||||
| 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/<name>/`,逐张入库 |
|
||||
| POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"count":1,"distance":25}`(**物种固定取数据集名,表单无需填写**;scene/action/occlusion 后端从数据集表池随机,light 走 config 通用池;**性别每张随机雄/雌**(两性体型外观差异大,随机让训练数据覆盖两性形态);prompt 可手填覆盖模板;**每张固定 1 个目标**(数量词 "1只",animal_count=1 供标注裁剪);distance 必填注入提示词距离描述;距离校验已取消;文件名前缀取数据集属性)(prompt 留空时按 `imageGen.promptTemplate` 组装:**物种=数据集名,场景/动作/遮挡/站高从 dataset 表生成参数池读取(创建数据集时 VLM 生成,无 config 兜底——池为空报错提示补参数或手填提示词)**;手填覆盖)(**count 1..1000,异步任务**:校验通过即返回 `{taskId, total}`,后台协程逐张生成,每张超时 `imageGen.timeoutSeconds`,进度/结果轮询 GET `/admin/datasets/gen-task`):调 `imageGen` provider(dashscope / localai,config 切换)逐张生成落盘 + 入库(记录 prompt);完成或部分失败后自动触发标注;任意失败任务置 failed(已生成图保留,付费资产原则);`namePrefix` 可选,按 `<前缀>_<两位序号>.jpg` 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;`animalCount` 仅作为自动标注框数上限(按置信度裁剪 ≤N),不参与生成校验 |
|
||||
| POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"count":1,"distance":25}`(**物种固定取数据集名,表单无需填写**;scene/action/occlusion 后端从数据集表池随机,light 走 config 通用池;**性别每张随机雄/雌**(两性体型外观差异大,随机让训练数据覆盖两性形态);prompt 可手填覆盖模板;**每张固定 1 个目标**(数量词 "1只",animal_count=1 供标注裁剪);distance 必填注入提示词距离描述;距离校验已取消;文件名前缀取数据集属性)(prompt 留空时按 `imageGen.promptTemplate` 组装:**物种=数据集名,场景/动作/遮挡/站高从 dataset 表生成参数池读取(创建数据集时 VLM 生成,无 config 兜底——池为空报错提示补参数或手填提示词)**;手填覆盖)(**count 1..1000,异步任务**:校验通过即返回 `{taskId, total}`,后台协程逐张生成(不设调用超时,失败由 provider 真实返回判定),进度/结果轮询 GET `/admin/datasets/gen-task`):调 `imageGen` provider(dashscope / localai,config 切换)逐张生成落盘 + 入库(记录 prompt);完成或部分失败后自动触发标注;任意失败任务置 failed(已生成图保留,付费资产原则);`namePrefix` 可选,按 `<前缀>_<两位序号>.jpg` 顺序命名并续接已有最大序号(如 pigeon_01.jpg),留空用时间戳命名;`animalCount` 仅作为自动标注框数上限(按置信度裁剪 ≤N),不参与生成校验 |
|
||||
| GET | `/admin/datasets/gen-task` | 生成任务进度 `{"datasetId":1}`:返回最近一次任务 `{id, status(running/done/failed), total, done, error, createdAt, finishedAt}`;无任务返回 null |
|
||||
| GET | `/admin/datasets/images` | 数据集图片列表 `{"datasetId":1}`,返回图片元数据(文件名/来源/prompt/创建时间) |
|
||||
| GET | `/admin/datasets/image` | 图片文件(静态字节流,`datasetId` + `filename` 定位,供缩略图/查看) |
|
||||
@@ -228,7 +228,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 配置 `config.yml`:监听端口、数据库路径、登录 token 签名密钥 `auth.secret`(必填,换值即全员下线)、套餐 `plans` 节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端 `admin.token`;模型训练相关节点:`training`(训练通道 mode=subprocess/ssh、ssh 连接信息、训练机工作目录/venv/数据集目录、并发度 1、超时)、`imageGen`(AI 生成图片 provider:`dashscope` 通义万相(apiKey + model qwen-image-3.0)/ `localai` 本地 local-ai(baseUrl + model 如 qwen-image + timeoutSeconds,训练机约 5 分钟/张建议 600))、`localAi`(二期标注用 RF-DETR 服务地址);SQLite 库由服务启动时自动建表并迁移,无需手工初始化
|
||||
1. 配置 `config.yml`:监听端口、数据库路径、登录 token 签名密钥 `auth.secret`(必填,换值即全员下线)、套餐 `plans` 节点、微信支付(appid/mchid/商户私钥/证书序列号/APIv3 密钥)、支付宝(appid/应用私钥/支付宝公钥)、管理端 `admin.token`;模型训练相关节点:`training`(训练通道 mode=subprocess/ssh、ssh 连接信息、训练机工作目录/venv/数据集目录、并发度 1、超时)、`imageGen`(AI 生成图片 provider:`dashscope` 通义万相(apiKey + model qwen-image-3.0)/ `localai` 本地 local-ai(baseUrl + model 如 qwen-image;每张不设调用超时,失败由 provider 真实返回判定))、`localAi`(二期标注用 RF-DETR 服务地址);SQLite 库由服务启动时自动建表并迁移,无需手工初始化
|
||||
2. `go build ./...` 编译验证
|
||||
3. 本地运行 `go run main.go`;服务层白盒测试:`GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/`(独立测试库,见 `biz/service/testdata/`)
|
||||
4. 后台管理端:`cd server_admin && npm run build`(构建产物输出到 `server/admin_dist/`,由后端 `/admin/` 托管);开发联调 `npm run dev`(Vite 代理 `/api` → `:8080`)。首次访问 `/admin/` 进入登录页,输入 `config.yml admin.token` 对应的管理 token(存浏览器 localStorage,随请求携带;token 不内嵌构建产物)。**硬性要求:只要修改了 `server_admin/` 源码,必须同步重新构建 `admin_dist/` 并提交产物**(后端托管的是构建产物,不重新构建则线上/部署版本不生效;禁止只改源码不构建)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -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-B2nod8DU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-CgjCJs5M.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-C8_wFaUY.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-VLYrhJmc.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -987,7 +987,7 @@ func pickCfgList(pool []string) string {
|
||||
}
|
||||
|
||||
// AdminGenerateImages AI 生成图片(异步任务):校验通过后插 gen_task 立即返回 TaskId,
|
||||
// 后台协程逐张生成(每张超时 imageGen.timeoutSeconds),进度落库供前端轮询;
|
||||
// 后台协程逐张生成(不设调用超时,失败由 provider 返回错误决定),进度落库供前端轮询;
|
||||
// 完成(或部分失败)后对本次新增图触发自动标注(付费资产,失败保留已生成图不删除)。
|
||||
func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.AdminDatasetGenerateReq) (*dto.AdminDatasetGenerateRes, error) {
|
||||
provider := common.ImageGen(ctx)
|
||||
@@ -1071,8 +1071,8 @@ func (s *datasetService) runGenTask(taskId int64, dataset *entity.Dataset, req *
|
||||
bgCtx := context.Background()
|
||||
go func() {
|
||||
dir := common.DatasetImagesDir(bgCtx, dataset.Name)
|
||||
// 每张生成超时走配置(dashscope 默认 120s;localai 训练机约 5min/张,建议 600)
|
||||
genTimeout := time.Duration(g.Cfg().MustGet(bgCtx, "imageGen.timeoutSeconds", 120).Int()) * time.Second
|
||||
// 异步任务不设调用超时:超时只放弃等待、取消不了 local-ai backend,
|
||||
// 会造成「任务已失败、模型仍在跑」的假失败并占住 poolSize=1 的池;失败判定交给 provider 真实返回
|
||||
handWritten := strings.TrimSpace(req.Prompt) != ""
|
||||
generated := 0
|
||||
addedIds := make([]int64, 0, req.Count)
|
||||
@@ -1093,14 +1093,12 @@ func (s *datasetService) runGenTask(taskId int64, dataset *entity.Dataset, req *
|
||||
break
|
||||
}
|
||||
}
|
||||
genCtx, cancel := context.WithTimeout(bgCtx, genTimeout)
|
||||
var data []byte
|
||||
err := common.GenTaskPoolInstance().Submit(genCtx, func(ctx context.Context) error {
|
||||
err := common.GenTaskPoolInstance().Submit(bgCtx, func(ctx context.Context) error {
|
||||
d, e := provider.Generate(ctx, prompt, req.Size)
|
||||
data = d
|
||||
return e
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
// 付费资产保留原则:已生成的不删除
|
||||
failed = fmt.Sprintf("第 %d 张生成失败(已生成 %d 张): %v", i+1, generated, err)
|
||||
|
||||
+2
-3
@@ -34,10 +34,9 @@ app:
|
||||
# 图像生成(管理端「AI 生成图片」):provider 必填项缺失时生成接口返回「图像生成服务未配置」
|
||||
imageGen:
|
||||
provider: localai # dashscope | localai
|
||||
baseUrl: "http://localhost:8080" # localai 必填:训练机 local-ai 地址(/v1/images/generations)
|
||||
model: flux2-klein # localai 上加载的模型名;dashscope 用 qwen-image-3.0
|
||||
baseUrl: "http://localhost:8080" # localai 必填:本机 local-ai 地址(/v1/images/generations)
|
||||
model: z-image-turbo # localai 上加载的模型名;dashscope 用 qwen-image-3.0
|
||||
apiKey: "" # dashscope 必填:DashScope API Key
|
||||
timeoutSeconds: 600 # 每张生成超时(默认 120;z-image-turbo 首次加载含模型入显存,稳态秒级~分钟级)
|
||||
poolSize: 1 # 生成任务并发度(z-image 显存独占,默认 1)
|
||||
# 通用提示词模板:{species} 取数据集 gen_species(单值,空回退数据集名);{count} 为数量词
|
||||
# (固定 "1只",每张仅 1 个目标;原表单数量选择已取消);{scene}/{action}/{light} 池内随机
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 360 KiB |
+3
-3
@@ -326,10 +326,10 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 token;iOS 不
|
||||
|
||||
- `imageGen` 配置节点:`provider: dashscope | localai`,接口在 `common` 层抽象 `ImageGenProvider`(`Generate(ctx, prompt, size) ([]byte, error)`),config 切换
|
||||
- `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` 的 host;step/cfg 等采样参数用 local-ai 模型配置默认值,不随接口传
|
||||
- `localai`(本机/训练机 local-ai,`POST {baseUrl}/v1/images/generations`,OpenAI 兼容):`baseUrl` + `model`(local-ai 上加载的模型名,如 qwen-image);返回 url 为容器内 localhost 地址,下载时替换为 `baseUrl` 的 host;step/cfg 等采样参数用 local-ai 模型配置默认值,不随接口传;生成耗时取决于模型与硬件,不设调用超时(见下)
|
||||
- `baseUrl`/`apiKey` 缺失时对应 provider 视为未配置,生成接口返回「图像生成服务未配置」
|
||||
- 尺寸参数由前端传(固定竖版 704x1248,比例与既有训练图 1152x2048 一致、32 倍数对齐);**实测竖版图在 local-ai 上极慢(约 26 分钟/张,Qwen-Image 行列注意力对非方形输入效率差,与分辨率关系不大;方形 1024x1024 仅 5 分钟)**,超时与前端等待按此配置,一次建议 1 张
|
||||
- **生成异步化(2026-08-28,count 1..1000)**:`/datasets/generate` 仅做校验(provider/数据集/标注服务/Serial 内并发检查已有 running 生成任务)+ 插 `gen_task` 落 running 记录即返回 `{taskId, total}`(毫秒级,前端 10s 超时无忧);后台协程逐张生成(`context.Background()` 生命周期模式,与预标注 `runDetection` 同构;`imageGen.poolSize` 并发池,z-image 显存独占默认 1),每张超时 `imageGen.timeoutSeconds`(默认 120s;localai 建议 600);逐张落盘 + 入库(记录 prompt 便于追溯)+ Serial 更新进度,完成(或部分失败)后对本次新增图触发自动标注,置 done/failed 由前端轮询 `/datasets/gen-task` 呈现;中途失败保留已生成图(不删除——付费资产原则);启动恢复 `recoverGenTasks` 孤儿 running 置 failed;VLM 补检与生成显存互斥(该数据集有 running 生成任务时拒绝)
|
||||
- 尺寸参数由前端传(固定竖版 704x1248,比例与既有训练图 1152x2048 一致、32 倍数对齐);**实测竖版图在 local-ai 上极慢(约 26 分钟/张,Qwen-Image 行列注意力对非方形输入效率差,与分辨率关系不大;方形 1024x1024 仅 5 分钟)**,前端等待按此预估,一次建议 1 张
|
||||
- **生成异步化(2026-08-28,count 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 生成任务时拒绝)
|
||||
- prompt 手填覆盖;留空走 `imageGen.promptTemplate` 通用模板逐张组装(**物种/场景/动作/遮挡/轮廓色/站高池均从数据集表读取(见下节),每张独立组装保持多样性**;**性别每张随机雄/雌(2026-08-28)**——两性体型大小与外观差异大(雉鸡雄艳雌褐、野鸭雄艳雌素等),随机让训练数据覆盖两性形态,`{species}` 替换为「雄/雌性+物种」;物种只写名字不写羽毛细节,细节会拉近镜头);**遵守项目提示词规范:不得包含目标位置描述**(位置由模型自行推理,前端模板与校验文案落实此约束)
|
||||
- **生成图距离/数量校验(2026-08-28)**:表单填 `distance`(米,固定值) 与 `animalCount` 后逐张启用;**距离校验已取消(2026-08-28)**:生成即入库,无逐张拒检;参数与公式见 git 历史(物理公式 d=体高×focalPx÷像素高)
|
||||
- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.8-9b(+mmproj) 以「图片+物种(生成 prompt 与物种池匹配)+场景光线线索+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_json;VL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.8-9b 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)维持远景感,手填提示词仅追加数量约束;不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留)
|
||||
|
||||
@@ -10,7 +10,7 @@ const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(12)
|
||||
const size = ref(20)
|
||||
const keyword = ref('')
|
||||
|
||||
const formVisible = ref(false)
|
||||
@@ -64,6 +64,12 @@ function search() {
|
||||
load()
|
||||
}
|
||||
|
||||
// 每页条数输入框变更:回到第一页重新加载(避免当前页超出新总页数)
|
||||
function onSizeChange() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
// ---------- 新建 / 编辑数据集(名称 + 描述 + 封面;AI 端点/训练机 SSH 走 config.yml,与数据集无关) ----------
|
||||
|
||||
function resetCoverState() {
|
||||
@@ -425,15 +431,18 @@ onBeforeUnmount(() => {
|
||||
<el-empty v-if="!loading && !list.length" description="暂无数据集,点击右上角新建" />
|
||||
</div>
|
||||
|
||||
<el-pagination
|
||||
class="pager"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[12, 24, 48]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
<div class="pager-row">
|
||||
<span class="pager-label">每页</span>
|
||||
<el-input-number v-model="size" :min="1" :max="500" :step="10" size="small" @change="onSizeChange" />
|
||||
<span class="pager-label">条</span>
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新建 / 编辑数据集(同一弹窗,编辑模式回填原值且允许改名) -->
|
||||
<el-dialog v-model="formVisible" :title="dlgForm.mode === 'create' ? '新建数据集' : '编辑数据集'" width="min(720px, 94vw)">
|
||||
@@ -528,9 +537,17 @@ onBeforeUnmount(() => {
|
||||
.kw {
|
||||
width: 220px;
|
||||
}
|
||||
.pager {
|
||||
.pager-row {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.pager-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ds-grid {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user