This commit is contained in:
2026-08-27 14:11:50 +08:00
parent ed535ff739
commit f89e1e4de8
5 changed files with 112 additions and 16 deletions
+3 -3
View File
@@ -14,7 +14,7 @@
| 套餐 | `config.yml` `plans` 节点配置三档套餐(改价 = 改配置重启),价格**整数分**1000 / 5600 / 18000 |
| 后台管理端 | `server_admin/`Vue3 + Element Plus)管理页面:订单查询、账号/授权管理(手动授权/撤销)、App 版本管理;构建产物由后端 `/admin/` 托管,登录页输入 token 后以 `X-Admin-Token` 头鉴权(`config.yml admin.token` |
| 版本管理 | 后台管理端上传 Android APK + 更新说明,APK 存服务器 `app.apkDir`(默认 `./workspace/`,与 `./data` 平级、挂载持久化)**固定文件名 `observer-latest.apk`,上传即覆盖,目录永远只保留最新一个文件**;**版本号从文件名识别**:文件须命名为 `observer-x.y.z.apk`(Flutter 打包产物即此命名,版本号取自 pubspec);客户端启动时 `GET /api/v1/app/update` 检查更新:服务器版本高于本地版本即弹更新提示(不可跳过)。**仅 Android 检查,iOS 不做版本下发**iOS 走 App Store 自行更新)。版本记录可删除:删最新版本联动删除 APK 文件,删历史版本仅删记录 |
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程:**数据集卡片列表**(封面图/描述/图片数/已标注数/**训练状态徽标**),**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片入库(手动上传/AI 生成)自动触发 RF-DETR 全图扫描标注**,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗内 canvas 直接画框/点框删除/清空并保存——AI 自动标注结果直接作为标注,人工可修改/清理全部框;封面(上传自动转 jpg + UUID 命名)/**描述**/AI 生成图片(provider 抽象,默认 qwen-image/DashScope 付费 API);AI 标注端点与训练机 SSH 为**全局配置,直接读 `config.yml`**`localAi` / `training.ssh` 节点,改配置需重启服务);图片落服务器 `app.datasetDir`/`datasets/<数据集名>/`DB 存元数据 + 标注 JSON |
| 数据训练(唯一入口) | 后台管理端「数据训练」一个菜单承载数据集全流程:**数据集卡片列表**(封面图/描述/图片数/已标注数/**训练状态徽标**),**卡片下方直接展示训练任务进度条与状态**(无独立训练页);详情页为**图片与标注一体视图**:分页(每页 20 条)逐行「原图 ‖ 标注图」对照展示;**图片入库(手动上传/AI 生成)自动触发 RF-DETR 全图扫描标注**,进度条展示在页顶;页顶另有「全量标注」按钮可手动重标全部图片(覆盖各图已有标注);点击原图/标注图弹窗放大,弹窗内 canvas 直接画框/点框删除/清空并保存——AI 自动标注结果直接作为标注,人工可修改/清理全部框;封面(上传自动转 jpg + UUID 命名)/**描述**/AI 生成图片(provider 抽象dashscope 通义万相付费 API / localai 训练机 local-ai qwen-image`config.yml imageGen` 节点切换,见配置说明);AI 标注端点与训练机 SSH 为**全局配置,直接读 `config.yml`**`localAi` / `training.ssh` 节点,改配置需重启服务);图片落服务器 `app.datasetDir`/`datasets/<数据集名>/`DB 存元数据 + 标注 JSON |
| 模型训练 | 从数据集卡片「开始训练」一键触发(参数 imgsz/epochs/batch/device 默认走 `config.yml` `training` 节点,部署级配置):进度/日志/指标监控(每 epoch 粒度)、取消;训练通道 `training` 节点可配置 subprocess(与 Go 服务同机直接起 python)/ ssh(异机执行,SSH 凭据取 `config.yml` `training.ssh` 节点),并发度 1(GPU 独占);训练脚本 `server/training/train_server.py`(随项目迁移,2026-08-26)参数化,产物(best.tflite/best.pt/曲线)拉回服务器;训练收尾自动做 **tflite 产物自检**(输入/输出 shape 校验,原 `inspect_tflite.py` 逻辑内嵌脚本),自检失败任务置失败并带出原因;`dump_graph.py` 留作训练机人工深度调试 |
| 模型版本与热更新 | **每数据集一个模型**:训练完成后一键「发布」(训练任务操作列)——tflite 落 `workspace/trainings/<数据集名>.tflite` + sha256/指标/类别名入 `model_version`(按数据集独立版本序列 m1.0.0 递增)。管理端**无模型管理界面**(版本记录仅支撑客户端下发)。**App 模型热更新**:`GET /api/v1/app/update` 扩展返回 `models` 目录数组,客户端独立检查,新模型下载校验替换,失败回退旧模型——模型迭代不再重打包 APK |
| 模型目录与多模型推理 | `GET /api/v1/models`(登录态)返回全部数据集当前生效模型(数据集/版本/类别/大小/sha256/下载地址),下载 URL `/download/trainings/<数据集名>.tflite`;**App 模型管理页**用户自由下载/删除/启用模型,识别时**加载全部已启用模型并行推理 + 跨模型 NMS 合并**(按类别名),内置 assets 模型兜底 |
@@ -185,7 +185,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow
| GET | `/admin/datasets/cover` | 封面文件(静态字节流,`datasetId` 定位) |
| 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,"prompt":"...","count":1}`(count 1..8,同步执行):调 `imageGen` provider默认 qwen-image/DashScope)逐张生成落盘 + 入库(记录 prompt),任意失败返回错误 |
| POST | `/admin/datasets/generate` | AI 生成图片 `{"datasetId":1,"prompt":"...","count":1}`count 1..8,同步执行,每张超时 `imageGen.timeoutSeconds`):调 `imageGen` providerdashscope / localaiconfig 切换)逐张生成落盘 + 入库(记录 prompt),任意失败返回错误 |
| GET | `/admin/datasets/images` | 数据集图片列表 `{"datasetId":1}`,返回图片元数据(文件名/来源/prompt/创建时间) |
| GET | `/admin/datasets/image` | 图片文件(静态字节流,`datasetId` + `filename` 定位,供缩略图/查看) |
| POST | `/admin/datasets/images/delete` | 删除图片 `{"datasetId":1,"ids":[1,2]}`:删文件 + 删记录(AI 生成图是付费资产,前端确认文案提示) |
@@ -223,7 +223,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`(二期标注用 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 + timeoutSeconds,训练机约 5 分钟/张建议 600))`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 不内嵌构建产物)
+4 -2
View File
@@ -447,7 +447,7 @@ func saveImageFile(f *ghttp.UploadFile, dir string) error {
return nil
}
// AdminGenerateImages AI 生成图片:同步逐张生成(每张 2min 超时),逐张落盘 + 入库;
// AdminGenerateImages AI 生成图片:同步逐张生成(每张超时 imageGen.timeoutSeconds),逐张落盘 + 入库;
// 中途失败返回错误并附已成功张数(已生成的图为付费资产,保留不删除)。
func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.AdminDatasetGenerateReq) (*dto.AdminDatasetGenerateRes, error) {
provider := common.ImageGen(ctx)
@@ -472,8 +472,10 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin
now := gtime.Now()
generated := 0
addedIds := make([]int64, 0, req.Count)
// 每张生成超时走配置(dashscope 默认 120slocalai 训练机约 5min/张,建议 600)
genTimeout := time.Duration(g.Cfg().MustGet(ctx, "imageGen.timeoutSeconds", 120).Int()) * time.Second
for i := 0; i < req.Count; i++ {
genCtx, cancel := context.WithTimeout(ctx, 120*time.Second)
genCtx, cancel := context.WithTimeout(ctx, genTimeout)
data, genErr := provider.Generate(genCtx, req.Prompt, req.Size)
cancel()
if genErr != nil {
+93 -4
View File
@@ -6,9 +6,11 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
@@ -18,13 +20,21 @@ type ImageGenProvider interface {
Generate(ctx context.Context, prompt, size string) ([]byte, error)
}
// ImageGen 当前配置的图像生成 provider 单例(未配置返回 nil,调用方判 CodeImageGenNotConfigured)。
// ImageGen 当前配置的图像生成 provider 单例(provider 必填项缺失返回 nil,调用方判 CodeImageGenNotConfigured)。
func ImageGen(ctx context.Context) ImageGenProvider {
if g.Cfg().MustGet(ctx, "imageGen.apiKey").String() == "" {
return nil
}
switch g.Cfg().MustGet(ctx, "imageGen.provider", "dashscope").String() {
case "localai":
if g.Cfg().MustGet(ctx, "imageGen.baseUrl").String() == "" {
return nil
}
return &localAiImageGen{
baseUrl: g.Cfg().MustGet(ctx, "imageGen.baseUrl").String(),
model: g.Cfg().MustGet(ctx, "imageGen.model", "qwen-image").String(),
}
case "dashscope":
if g.Cfg().MustGet(ctx, "imageGen.apiKey").String() == "" {
return nil
}
return &dashScopeImageGen{apiKey: g.Cfg().MustGet(ctx, "imageGen.apiKey").String()}
default:
return nil
@@ -158,3 +168,82 @@ func truncateStr(s string, n int) string {
}
return s[:n] + "..."
}
// localAiImageGen local-aiOpenAI 兼容 /v1/images/generations)实现:同步生成 → 下载产物图片。
// 训练机 local-ai 已加载 qwen-image 文生图;返回 url 为容器内 localhost 地址,需替换为配置 baseUrl 下载。
type localAiImageGen struct {
baseUrl string
model string
}
func (p *localAiImageGen) Generate(ctx context.Context, prompt, size string) ([]byte, error) {
body, err := json.Marshal(map[string]any{
"model": p.model,
"prompt": prompt,
"size": size,
"n": 1,
})
if err != nil {
return nil, err
}
url := strings.TrimSuffix(p.baseUrl, "/") + "/v1/images/generations"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(body)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, gerror.Wrap(err, "local-ai 生成请求失败")
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
msg := truncateStr(string(raw), 200)
if msg == "" {
msg = resp.Status
}
return nil, fmt.Errorf("local-ai 生成失败: HTTP %d: %s", resp.StatusCode, msg)
}
var m struct {
Data []struct {
Url string `json:"url"`
} `json:"data"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("local-ai 响应解析失败: %v", err)
}
if len(m.Data) == 0 || m.Data[0].Url == "" {
return nil, fmt.Errorf("local-ai 成功但无图片 URL")
}
return p.download(ctx, m.Data[0].Url)
}
// download 下载生成产物图片:url 为容器内地址(localhost),替换 scheme/host 为配置 baseUrl 后下载
func (p *localAiImageGen) download(ctx context.Context, imageUrl string) ([]byte, error) {
base, err := url.Parse(p.baseUrl)
if err != nil {
return nil, err
}
u, err := url.Parse(imageUrl)
if err != nil {
return nil, err
}
u.Scheme, u.Host = base.Scheme, base.Host
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, gerror.Wrap(err, "下载生成图片失败")
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("下载生成图片失败: HTTP %d", resp.StatusCode)
}
return io.ReadAll(io.LimitReader(resp.Body, 16<<20))
}
+6 -4
View File
@@ -31,11 +31,13 @@ app:
# 模型训练体系运行时数据根目录(图片/标注/模型/训练产物;与 apkDir 同目录时共用 workspace
datasetDir: "./workspace"
# 图像生成(管理端「AI 生成图片」):未配置 apiKey 时生成接口返回「图像生成服务未配置」
# 图像生成(管理端「AI 生成图片」):provider 必填项缺失时生成接口返回「图像生成服务未配置」
imageGen:
provider: dashscope # 当前唯一实现
apiKey: "" # DashScope API Key
model: qwen-image-3.0
provider: localai # dashscope | localai
baseUrl: "http://192.168.3.210:18080" # localai 必填:训练机 local-ai 地址(/v1/images/generations
model: qwen-image # localai 上加载的模型名;dashscope 用 qwen-image-3.0
apiKey: "" # dashscope 必填:DashScope API Key
timeoutSeconds: 600 # 每张生成超时(默认 120localai 训练机约 5min/张,建议 600)
# 训练通道(并发度 1:GPU 独占,同时仅一个 running 任务):
# mode=subprocess 训练机与服务器同机;mode=ssh 异机(训练脚本/数据集经 ssh 通道同步)
+6 -3
View File
@@ -324,9 +324,12 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 tokeniOS 不
### AI 生成图片(provider 抽象)
- `imageGen` 配置节点:`provider: dashscope`(当前唯一实现)+ `apiKey` + `model: qwen-image-3.0`接口在 `common` 层抽象 `ImageGenProvider``Generate(ctx, prompt, size) ([]byte, error)`),后续可加本地 SD 等实现,config 切换
- `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 模型配置默认值,不随接口传
- `baseUrl`/`apiKey` 缺失时对应 provider 视为未配置,生成接口返回「图像生成服务未配置」
- 尺寸参数由前端传(下拉可选,默认 1152x2048 竖图,与既有训练图一致;另提供 704x704 档)
- 生成同步执行(count 1..8,超时 2min/张),逐张落盘 + 入库(记录 prompt 便于追溯);中途失败返回错误(已成功的图保留,不删除——付费资产原则)
- 生成同步执行(count 1..8每张超时 `imageGen.timeoutSeconds`,默认 120slocalai 建议 600),逐张落盘 + 入库(记录 prompt 便于追溯);中途失败返回错误(已成功的图保留,不删除——付费资产原则)
- prompt 由前端预设模板 + 用户微调;**遵守项目提示词规范:不得包含目标位置描述**(位置由模型自行推理,前端模板与校验文案落实此约束)
### 训练通道(runner 抽象,决策:同机/异机不确定 → 可配置)
@@ -393,6 +396,6 @@ training:
- iOS App Store 对数字内容强制 IAP,微信/支付宝直连有被拒风险(已决策,记录在案)
- 管理端静态 token 由登录页输入(浏览器 localStorage),属内网管理凭据:`config.yml admin.token` 失配/未配置时管理接口全部拒绝;token 不进入前端构建产物与 git
- 手动授权等同免费发卡,仅限运营/客诉排查场景,前端页面带确认弹窗,后端不限制频率(按需再加审计)
- 模型训练体系依赖外部环境:训练机 GPU + ultralytics venv`training.venvPython`)、RF-DETR 服务从服务器可达(`localAi.baseUrl`)、DashScope API key`imageGen.apiKey`)——未配置对应节点时相关管理接口返回配置缺失错误,不影响支付/授权主链路
- 模型训练体系依赖外部环境:训练机 GPU + ultralytics venv`training.venvPython`)、RF-DETR 服务从服务器可达(`localAi.baseUrl`)、图像生成服务可达(`imageGen`dashscope 需外网 + apiKeylocalai 需训练机 local-ai 可达 + `imageGen.baseUrl`)——未配置对应节点时相关管理接口返回配置缺失错误,不影响支付/授权主链路
- subprocess 模式下训练与在线服务同机,训练吃满 GPU 可能影响识别类在线任务(当前在线服务无 GPU 推理,风险低);异机部署切换 `training.mode: ssh`
- 训练产物与数据集为磁盘占用大户(图片百 MB~GB、每轮产物几十 MB),`workspace/` 已挂载持久化;清理入口:删除数据集(付费资产需确认);模型无存档,仅 latest.tflite 被新发布覆盖,无需单独清理