This commit is contained in:
2026-08-29 21:03:03 +08:00
parent 5c43370fc9
commit 6bc2ade628
10 changed files with 43 additions and 29 deletions
+2 -2
View File
@@ -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` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 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` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 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-aibaseUrl + 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-aibaseUrl + 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/` 并提交产物**(后端托管的是构建产物,不重新构建则线上/部署版本不生效;禁止只改源码不构建)
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-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>
+4 -6
View File
@@ -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 默认 120slocalai 训练机约 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
View File
@@ -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 # 每张生成超时(默认 120z-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
View File
@@ -326,10 +326,10 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 tokeniOS 不
- `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` 的 hoststep/cfg 等采样参数用 local-ai 模型配置默认值,不随接口传
- `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),每张超时 `imageGen.timeoutSeconds`(默认 120slocalai 建议 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-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 生成任务时拒绝)
- 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_jsonVL 与 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`),连续不合格整体报错(已合格张数保留)
+28 -11
View File
@@ -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;