@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
backend/bin
|
||||||
|
.git
|
||||||
@@ -13,3 +13,6 @@
|
|||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
**/node_modules/
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
gen2d — AI 驱动的 2D 游戏素材生成工具。用户通过文本提示词或可视化参数,生成风格一致、管线友好的 Sprite、背景、UI 元素与动画帧。目标是无缝融入 Unity / Godot 等主流 2D 游戏引擎的工作流。
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
gen2d/
|
||||||
|
├── backend/ # Go + Gin API 服务
|
||||||
|
│ ├── cmd/ # 入口 (main.go)
|
||||||
|
│ ├── internal/ # 业务逻辑(不对外暴露)
|
||||||
|
│ │ ├── handler/ # HTTP handlers
|
||||||
|
│ │ ├── service/ # 业务 service 层
|
||||||
|
│ │ ├── model/ # 数据模型 / DTO
|
||||||
|
│ │ ├── middleware/
|
||||||
|
│ │ └── config/
|
||||||
|
│ ├── pkg/ # 可复用的公共库
|
||||||
|
│ ├── go.mod
|
||||||
|
│ └── go.sum
|
||||||
|
├── frontend/ # Vite + React 前端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ ├── pages/
|
||||||
|
│ │ ├── hooks/
|
||||||
|
│ │ ├── services/ # API 调用层
|
||||||
|
│ │ ├── stores/ # 状态管理
|
||||||
|
│ │ ├── types/
|
||||||
|
│ │ └── utils/
|
||||||
|
│ ├── public/
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── vite.config.ts
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── package.json
|
||||||
|
└── CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Backend (Go + Gin)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# 运行开发服务器
|
||||||
|
go run cmd/main.go
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
go build -o bin/gen2d cmd/main.go
|
||||||
|
|
||||||
|
# 运行全部测试
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# 运行单个包的测试
|
||||||
|
go test ./internal/service/...
|
||||||
|
|
||||||
|
# 运行单个测试函数
|
||||||
|
go test -run TestFunctionName ./internal/service/
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
golangci-lint run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (Vite + React)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 预览生产构建
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run typecheck
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# 运行单个测试文件
|
||||||
|
npm run test -- src/components/AssetCard.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 后端分层
|
||||||
|
|
||||||
|
- **Handler** — 解析 HTTP 请求,调用 service,返回响应。不包含业务逻辑。
|
||||||
|
- **Service** — 核心业务逻辑:提示词处理、AI 模型调用、素材后处理。
|
||||||
|
- **Model** — 请求/响应 DTO、数据库实体(如有)。
|
||||||
|
- **Pkg** — 通用工具:图片处理、颜色分析、sprite 切片等。
|
||||||
|
|
||||||
|
### 前端架构
|
||||||
|
|
||||||
|
- 使用 React + TypeScript,状态管理使用 zustand 或 React Context。
|
||||||
|
- API 调用集中在 `services/` 目录,组件不直接发请求。
|
||||||
|
- 素材预览和参数调整是核心交互,需要关注 Canvas/WebGL 渲染性能。
|
||||||
|
|
||||||
|
### 前后端通信
|
||||||
|
|
||||||
|
- RESTful API,JSON 格式。
|
||||||
|
- 前端开发时通过 Vite proxy 转发到后端 `localhost:PORT`。
|
||||||
|
- 图片素材通过 base64 或对象存储 URL 传输,大文件走 OSS/COS。
|
||||||
|
|
||||||
|
## 2D 游戏素材生成约束
|
||||||
|
|
||||||
|
### 素材类型
|
||||||
|
|
||||||
|
项目需要覆盖以下常见 2D 游戏素材类型:
|
||||||
|
|
||||||
|
- **Sprite / 角色** — 像素风、卡通、手绘风等,需支持多帧动画序列
|
||||||
|
- **Tilemap / 地形瓦片** — 可无缝拼接的地形纹理(草地、水域、道路等)
|
||||||
|
- **UI 元素** — 按钮、血条、对话框、图标等
|
||||||
|
- **背景 / 场景** — 横版卷轴背景、俯视视角场景
|
||||||
|
- **特效** — 火焰、烟雾、魔法等粒子效果帧
|
||||||
|
|
||||||
|
### 风格一致性
|
||||||
|
|
||||||
|
- 提供**风格种子 (Style Seed)** 机制:用户选定一套风格后,后续生成继承相同的色板、线条风格、分辨率。
|
||||||
|
- 支持**参考图上传**:以已有素材作为风格参考。
|
||||||
|
- 生成时返回**风格向量 / 特征参数**,可用于后续生成的风格锁定。
|
||||||
|
|
||||||
|
### 管线兼容性
|
||||||
|
|
||||||
|
生成的素材需满足主流游戏引擎的导入要求:
|
||||||
|
|
||||||
|
- **Sprite Sheet** — 输出标准的 spritesheet 拆分格式,附带 JSON/CSV 元数据(frame 位置、锚点、碰撞框)。
|
||||||
|
- **透明通道** — PNG 格式,带 alpha 通道,背景透明。
|
||||||
|
- **尺寸规范** — 支持常见尺寸(16×16, 32×32, 48×48, 64×64, 128×128, 256×256),可自定义。
|
||||||
|
- **命名规范** — 输出文件遵循 `{category}_{name}_{index}.png` 格式,便于引擎批量导入。
|
||||||
|
- **Unity / Godot 导出** — 可选生成 `.aseprite` 元数据或 Unity Sprite Editor 兼容的 `.meta` 信息。
|
||||||
|
|
||||||
|
### 生成效率与成本
|
||||||
|
|
||||||
|
- 支持**批量生成**:一次请求生成多个变体(如角色的行走 8 方向 × 4 帧)。
|
||||||
|
- 提供**低分辨率预览 → 高分辨率出图**的两阶段流程,减少无效生成。
|
||||||
|
- 后端应做**请求去重与缓存**:相同提示词 + 参数的结果缓存复用。
|
||||||
|
- AI 模型调用支持**异步队列**,前端轮询或 WebSocket 获取结果。
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- 后端 Go 代码遵循 [Effective Go](https://go.dev/doc/effective_go) 和 Go 官方 style。
|
||||||
|
- 前端使用 ESLint + Prettier,提交前自动格式化。
|
||||||
|
- API 路径统一使用 `/api/v1/` 前缀。
|
||||||
|
- 错误响应统一格式:`{"code": int, "message": string, "data": any}`。
|
||||||
|
- Git commit message 使用中文或英文均可,但需要清晰描述变更内容。
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ---- Build Stage ----
|
||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/gen2d ./cmd/main.go
|
||||||
|
|
||||||
|
# ---- Runtime Stage ----
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
COPY --from=builder /bin/gen2d /usr/local/bin/gen2d
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["gen2d"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// gen2d backend 入口
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gen2d/internal/config"
|
||||||
|
"gen2d/internal/handler"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
gin.SetMode(cfg.Mode)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机
|
||||||
|
|
||||||
|
// API v1 路由组
|
||||||
|
r.GET("/api/v1/health", handler.Health) // 健康检查
|
||||||
|
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||||
|
log.Printf("gen2d backend starting on %s", addr)
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
log.Fatalf("server failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module gen2d
|
||||||
|
|
||||||
|
go 1.26.3
|
||||||
|
|
||||||
|
require github.com/gin-gonic/gin v1.12.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Package config 负责从环境变量加载应用配置。
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 应用全局配置,优先读取环境变量,未设置时使用默认值。
|
||||||
|
type Config struct {
|
||||||
|
Port int // HTTP 监听端口,默认 8080,环境变量 GEN2D_PORT
|
||||||
|
Mode string // Gin 运行模式 (debug/release),环境变量 GEN2D_MODE
|
||||||
|
MaxFileSize int64 // 上传文件大小上限(字节),默认 10MB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 从环境变量加载配置并返回。
|
||||||
|
func Load() *Config {
|
||||||
|
cfg := &Config{
|
||||||
|
Port: 8080,
|
||||||
|
Mode: "debug",
|
||||||
|
MaxFileSize: 10 << 20, // 10MB
|
||||||
|
}
|
||||||
|
|
||||||
|
if port := os.Getenv("GEN2D_PORT"); port != "" {
|
||||||
|
if p, err := strconv.Atoi(port); err == nil {
|
||||||
|
cfg.Port = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode := os.Getenv("GEN2D_MODE"); mode != "" {
|
||||||
|
cfg.Mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gen2d/internal/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Health 健康检查接口,返回服务运行状态。
|
||||||
|
func Health(c *gin.Context) {
|
||||||
|
c.JSON(200, model.OK(gin.H{"status": "healthy"}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package model 定义请求/响应的数据结构。
|
||||||
|
package model
|
||||||
|
|
||||||
|
// Response 统一 API 响应格式,所有接口均返回此结构。
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"` // 业务状态码,0 表示成功
|
||||||
|
Message string `json:"message"` // 状态描述
|
||||||
|
Data any `json:"data,omitempty"` // 响应数据,错误时省略
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK 构造成功响应。
|
||||||
|
func OK(data any) Response {
|
||||||
|
return Response{Code: 0, Message: "ok", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail 构造错误响应。
|
||||||
|
func Fail(code int, msg string) Response {
|
||||||
|
return Response{Code: code, Message: msg}
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
// Package splitsprite provides PNG sprite sheet splitting utilities.
|
||||||
|
//
|
||||||
|
// Pipeline: green screen removal → projection-based gap detection →
|
||||||
|
// split into tiles → filter out low-fill tiles → trim transparent edges.
|
||||||
|
package splitsprite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options configures the sprite sheet splitting pipeline.
|
||||||
|
type Options struct {
|
||||||
|
// GreenScreen enables green background removal.
|
||||||
|
GreenScreen bool
|
||||||
|
// GreenTolerance controls how aggressively green pixels are removed (0–1, default 0.2).
|
||||||
|
GreenTolerance float64
|
||||||
|
|
||||||
|
// GapThreshold is the max fraction of non-transparent pixels a row/column
|
||||||
|
// can have to be considered a gap (0–1, default 0.03).
|
||||||
|
GapThreshold float64
|
||||||
|
// MinGapWidth is the minimum width in pixels a gap must have.
|
||||||
|
MinGapWidth int
|
||||||
|
|
||||||
|
// MinFillRatio is the minimum fraction of non-transparent pixels a tile
|
||||||
|
// must have to be kept (0–1, default 0.3).
|
||||||
|
MinFillRatio float64
|
||||||
|
|
||||||
|
// Trim removes transparent borders from output tiles.
|
||||||
|
Trim bool
|
||||||
|
// OutW / OutH specify the output tile size (0 = keep original).
|
||||||
|
OutW, OutH int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns sensible default splitting options.
|
||||||
|
func DefaultOptions() *Options {
|
||||||
|
return &Options{
|
||||||
|
GreenScreen: true,
|
||||||
|
GreenTolerance: 0.2,
|
||||||
|
GapThreshold: 0.03,
|
||||||
|
MinGapWidth: 2,
|
||||||
|
MinFillRatio: 0.3,
|
||||||
|
Trim: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process splits a sprite sheet image into individual cleaned tile images.
|
||||||
|
// It runs the full pipeline: green screen removal → split → trim → resize.
|
||||||
|
func Process(img image.Image, opts *Options) ([]image.Image, error) {
|
||||||
|
if opts == nil {
|
||||||
|
opts = DefaultOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
src := toRGBA(img)
|
||||||
|
|
||||||
|
if opts.GreenScreen {
|
||||||
|
src = removeGreenScreen(src, opts.GreenTolerance)
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles := projectionSplit(src, opts.GapThreshold, opts.MinGapWidth, opts.MinFillRatio)
|
||||||
|
if len(tiles) == 0 {
|
||||||
|
return nil, fmt.Errorf("no tiles detected — try lowering GapThreshold or adjusting GreenTolerance")
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]image.Image, len(tiles))
|
||||||
|
for i, t := range tiles {
|
||||||
|
sub := image.NewRGBA(image.Rect(0, 0, t.w, t.h))
|
||||||
|
draw.Draw(sub, sub.Bounds(), src, image.Point{t.x, t.y}, draw.Src)
|
||||||
|
|
||||||
|
if opts.Trim {
|
||||||
|
sub = trimAlpha(sub)
|
||||||
|
}
|
||||||
|
if opts.OutW > 0 && opts.OutH > 0 {
|
||||||
|
sub = resize(sub, opts.OutW, opts.OutH)
|
||||||
|
}
|
||||||
|
results[i] = sub
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveGreenScreen removes green-dominant background pixels, making them transparent.
|
||||||
|
func RemoveGreenScreen(img image.Image, tol float64) image.Image {
|
||||||
|
return removeGreenScreen(toRGBA(img), tol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimAlpha removes fully transparent borders from an image.
|
||||||
|
func TrimAlpha(img image.Image) image.Image {
|
||||||
|
return trimAlpha(toRGBA(img))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize resizes an image using nearest-neighbor interpolation.
|
||||||
|
func Resize(img image.Image, w, h int) image.Image {
|
||||||
|
return resize(toRGBA(img), w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// internal
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
type tile struct {
|
||||||
|
x, y, w, h int
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeGreenScreen(rgba *image.RGBA, tol float64) *image.RGBA {
|
||||||
|
b := rgba.Bounds()
|
||||||
|
dst := image.NewRGBA(b)
|
||||||
|
absTol := tol * 255
|
||||||
|
|
||||||
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||||
|
for x := b.Min.X; x < b.Max.X; x++ {
|
||||||
|
r16, g16, bl16, a16 := rgba.At(x, y).RGBA()
|
||||||
|
if a16 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r, g, bl := float64(r16>>8), float64(g16>>8), float64(bl16>>8)
|
||||||
|
gDominance := g - (r+bl)/2
|
||||||
|
if gDominance > absTol {
|
||||||
|
alpha := 1.0 - math.Min(gDominance/(absTol*2), 1.0)
|
||||||
|
dst.SetRGBA(x, y, color.RGBA{
|
||||||
|
R: uint8(r), G: uint8(g), B: uint8(bl),
|
||||||
|
A: uint8(alpha * 255),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dst.Set(x, y, rgba.At(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectionSplit(rgba *image.RGBA, gapThreshold float64, minGap int, minFill float64) []tile {
|
||||||
|
bounds := rgba.Bounds()
|
||||||
|
W, H := bounds.Dx(), bounds.Dy()
|
||||||
|
|
||||||
|
rowRatio := make([]float64, H)
|
||||||
|
colRatio := make([]float64, W)
|
||||||
|
for y := 0; y < H; y++ {
|
||||||
|
n := 0
|
||||||
|
for x := 0; x < W; x++ {
|
||||||
|
if alphaAt(rgba, x, y) > 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowRatio[y] = float64(n) / float64(W)
|
||||||
|
}
|
||||||
|
for x := 0; x < W; x++ {
|
||||||
|
n := 0
|
||||||
|
for y := 0; y < H; y++ {
|
||||||
|
if alphaAt(rgba, x, y) > 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colRatio[x] = float64(n) / float64(H)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCuts := findCuts(rowRatio, gapThreshold, minGap)
|
||||||
|
colCuts := findCuts(colRatio, gapThreshold, minGap)
|
||||||
|
|
||||||
|
if len(rowCuts) < 2 || len(colCuts) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tiles []tile
|
||||||
|
for ri := 0; ri < len(rowCuts)-1; ri++ {
|
||||||
|
for ci := 0; ci < len(colCuts)-1; ci++ {
|
||||||
|
x := colCuts[ci]
|
||||||
|
y := rowCuts[ri]
|
||||||
|
w := colCuts[ci+1] - x
|
||||||
|
h := rowCuts[ri+1] - y
|
||||||
|
if tileFillRatio(rgba, x, y, w, h) >= minFill {
|
||||||
|
tiles = append(tiles, tile{x: x, y: y, w: w, h: h})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
func tileFillRatio(rgba *image.RGBA, x0, y0, w, h int) float64 {
|
||||||
|
total := w * h
|
||||||
|
if total == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for y := y0; y < y0+h; y++ {
|
||||||
|
for x := x0; x < x0+w; x++ {
|
||||||
|
if alphaAt(rgba, x, y) > 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return float64(n) / float64(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCuts(ratios []float64, threshold float64, minGap int) []int {
|
||||||
|
n := len(ratios)
|
||||||
|
isGap := make([]bool, n)
|
||||||
|
for i, r := range ratios {
|
||||||
|
isGap[i] = r < threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
type segment struct{ start, end int }
|
||||||
|
var gaps []segment
|
||||||
|
i := 0
|
||||||
|
for i < n {
|
||||||
|
if isGap[i] {
|
||||||
|
start := i
|
||||||
|
for i < n && isGap[i] {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i-start >= minGap {
|
||||||
|
gaps = append(gaps, segment{start, i})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gaps) == 0 {
|
||||||
|
return []int{0, n}
|
||||||
|
}
|
||||||
|
|
||||||
|
cuts := []int{0}
|
||||||
|
for _, seg := range gaps {
|
||||||
|
cuts = append(cuts, seg.start+(seg.end-seg.start)/2)
|
||||||
|
}
|
||||||
|
cuts = append(cuts, n)
|
||||||
|
sort.Ints(cuts)
|
||||||
|
|
||||||
|
dedup := cuts[:1]
|
||||||
|
for j := 1; j < len(cuts); j++ {
|
||||||
|
if cuts[j] != dedup[len(dedup)-1] {
|
||||||
|
dedup = append(dedup, cuts[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dedup
|
||||||
|
}
|
||||||
|
|
||||||
|
func alphaAt(rgba *image.RGBA, x, y int) uint8 {
|
||||||
|
return rgba.Pix[rgba.PixOffset(x, y)+3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimAlpha(rgba *image.RGBA) *image.RGBA {
|
||||||
|
b := rgba.Bounds()
|
||||||
|
minX, minY := b.Max.X, b.Max.Y
|
||||||
|
maxX, maxY := b.Min.X, b.Min.Y
|
||||||
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||||
|
for x := b.Min.X; x < b.Max.X; x++ {
|
||||||
|
if alphaAt(rgba, x-b.Min.X, y-b.Min.Y) > 0 {
|
||||||
|
if x < minX {
|
||||||
|
minX = x
|
||||||
|
}
|
||||||
|
if x > maxX {
|
||||||
|
maxX = x
|
||||||
|
}
|
||||||
|
if y < minY {
|
||||||
|
minY = y
|
||||||
|
}
|
||||||
|
if y > maxY {
|
||||||
|
maxY = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w := maxX - minX + 1
|
||||||
|
h := maxY - minY + 1
|
||||||
|
if w <= 0 || h <= 0 {
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
draw.Draw(dst, dst.Bounds(), rgba, image.Point{minX, minY}, draw.Src)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(rgba *image.RGBA, w, h int) *image.RGBA {
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
sw, sh := rgba.Bounds().Dx(), rgba.Bounds().Dy()
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
sx := x * sw / w
|
||||||
|
sy := y * sh / h
|
||||||
|
dst.Set(x, y, rgba.At(sx, sy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRGBA(src image.Image) *image.RGBA {
|
||||||
|
if rgba, ok := src.(*image.RGBA); ok {
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
b := src.Bounds()
|
||||||
|
rgba := image.NewRGBA(b)
|
||||||
|
draw.Draw(rgba, b, src, b.Min, draw.Src)
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
environment:
|
||||||
|
- GEN2D_MODE=release
|
||||||
|
- GEN2D_PORT=8080
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "10000:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
# gen2d API 文档
|
||||||
|
|
||||||
|
Base URL: `http://localhost:8080`
|
||||||
|
|
||||||
|
## 统一响应格式
|
||||||
|
|
||||||
|
所有接口均返回以下 JSON 结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --------- | ------ | -------------------------------------- |
|
||||||
|
| `code` | int | 业务状态码。`0` 表示成功,非零为错误码 |
|
||||||
|
| `message` | string | 状态描述 |
|
||||||
|
| `data` | any | 响应数据,错误时可能不返回此字段 |
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "error description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"status": "healthy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# gen2d 多智能体架构设计
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
gen2d 是一个 AI 驱动的 2D 游戏素材生成工具。核心思路是采用**多智能体协作流水线**,将素材生成拆分为 **处理前 → 处理中 → 处理后** 三个阶段,每个阶段由专门的 subagent 负责。
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ PromptBuilder │ ──▶ │ AssetGenerator │ ──▶ │ PostProcess │
|
||||||
|
│ (提示词工程) │ │ (AI 出图) │ │ (质检+格式适配) │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
三个 Agent 顺序执行,无需主控编排——上一步的输出即是下一步的输入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、处理前 — 提示词工程 & 风格注入
|
||||||
|
|
||||||
|
### 1.1 PromptBuilder Agent
|
||||||
|
|
||||||
|
将用户输入一次性加工为最终生成提示词,合并了原本分散在多个步骤中的提示词改写、风格注入、管线标签拼接。
|
||||||
|
|
||||||
|
- **输入**:用户原始文本 + 素材类型标签 + StyleSeed 对象 + 技术参数
|
||||||
|
- **输出**:直接可用于 `AssetGenerator` 的完整提示词
|
||||||
|
- **处理逻辑**:
|
||||||
|
- 补全缺失细节(根据素材类型推断默认视角、构图等)
|
||||||
|
- 注入游戏美术专用术语(pixel art / hand-drawn / vector flat 等)
|
||||||
|
- 注入 StyleSeed 风格参数(色板、线条粗细、参考图特征向量)
|
||||||
|
- 拼接管线兼容性标签(`--ar 1:1 --res 64x64 --format png --alpha` 等)
|
||||||
|
- 添加质量关键词和负向提示词
|
||||||
|
|
||||||
|
**StyleSeed 数据结构**(前端维护,透传至 PromptBuilder):
|
||||||
|
- `palette`: 主色调 + 辅助色 + 高光/阴影色
|
||||||
|
- `lineWeight`: 线条粗细等级
|
||||||
|
- `styleRef`: 参考图特征向量(可选,来自用户上传的参考图)
|
||||||
|
- `resolution`: 目标分辨率 (16/32/48/64/128/256)
|
||||||
|
- `artStyle`: 美术风格枚举 (pixel/cartoon/hand-drawn/vector/flat)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、处理时 — 生成 & 风格控制
|
||||||
|
|
||||||
|
### 2.1 AssetGenerator(统一生成 Agent)
|
||||||
|
|
||||||
|
所有素材类型共用同一个生成 Agent,调用底层 AI 推理 API 出图。不同素材类型的差异化需求已在预处理阶段由 `PromptBuilder` 按 `assetType` 注入到提示词中,不体现在生成阶段。
|
||||||
|
|
||||||
|
原本归属在"生成 Agent"上的特殊约束(帧间一致性、边缘拼接、UI 安全区、视差层、帧循环等),本质上是**后处理阶段**的验证和适配工作,应下沉到 `QualitySupervisor` 和 `FormatAdapter` 中按 `assetType` 做策略分发。
|
||||||
|
|
||||||
|
### 2.2 序列帧生成策略
|
||||||
|
|
||||||
|
对于多帧动画(角色行走、特效等),逐帧独立生成容易出现帧间外观不一致。采用 **版图生成法**:
|
||||||
|
|
||||||
|
- AI 生成一张包含完整动作序列的大图(action plate),帧按网格排列
|
||||||
|
- 后处理阶段由 `FormatAdapter` 按网格坐标切分为单帧
|
||||||
|
- 一张版图内的角色外观、光影、比例天然一致
|
||||||
|
|
||||||
|
### 2.3 两阶段生成机制
|
||||||
|
|
||||||
|
- **Phase 1 — 低分辨率预览**:以目标分辨率的 1/4 快速生成缩略图,用户确认方向
|
||||||
|
- **Phase 2 — 高分辨率出图**:确认后生成全分辨率素材
|
||||||
|
- 减少无效生成消耗,节省 API 调用成本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、处理后 — 质量监督 & 管线适配
|
||||||
|
|
||||||
|
### 3.1 QualitySupervisor Agent
|
||||||
|
|
||||||
|
- **职责**:评估生成素材质量,决定是否需要重新生成
|
||||||
|
- **检查维度**:
|
||||||
|
- 边缘清晰度(无模糊/锯齿)
|
||||||
|
- Alpha 通道正确性(背景是否完全透明)
|
||||||
|
- 风格一致性(与 StyleSeed 的偏差是否在阈值内)
|
||||||
|
- 分辨率匹配(输出尺寸是否与目标一致)
|
||||||
|
- 帧完整性(多帧素材是否缺帧)
|
||||||
|
- 无缝拼接(瓦片类素材的四边连接测试)
|
||||||
|
- **输出**:每张素材的质量评分 (0-100) + 问题描述
|
||||||
|
- **决策**:评分 < 阈值 → 自动触发重新生成(最多重试 3 次)
|
||||||
|
|
||||||
|
### 3.2 FormatAdapter Agent
|
||||||
|
|
||||||
|
- **职责**:将原始生成结果转换为游戏引擎可用的格式
|
||||||
|
- **功能**:
|
||||||
|
- **版图拆分**:将序列帧版图按网格切分为单帧(配合 2.2 版图生成法)
|
||||||
|
- **SpriteSheet 打包**:将单帧图片合并为 spritesheet,生成 JSON/CSV 元数据(帧位置、尺寸、锚点、碰撞框)
|
||||||
|
- **元数据生成**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"name": "walk_down_0",
|
||||||
|
"rect": [0, 0, 64, 64],
|
||||||
|
"anchor": [32, 56],
|
||||||
|
"duration": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "size": [512, 512], "format": "RGBA8888" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **引擎导出**:可选生成 `.aseprite` 元数据或 Unity `.meta` 文件
|
||||||
|
- **命名规范化**:确保输出遵循 `{category}_{name}_{index}.png`
|
||||||
|
|
||||||
|
### 3.3 Cache & Dedup
|
||||||
|
|
||||||
|
- **请求去重**:相同 `(提示词, 参数, StyleSeed)` 的生成请求直接返回缓存结果
|
||||||
|
- **缓存分层**:
|
||||||
|
- L1: 内存 LRU(热点素材快速响应)
|
||||||
|
- L2: 本地文件/对象存储(持久化,跨实例共享)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、API 设计
|
||||||
|
|
||||||
|
所有接口统一前缀 `/api/v1/`,统一响应格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": 0, "message": "ok", "data": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 素材生成
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/v1/generate` | 提交生成任务,返回 jobId |
|
||||||
|
| GET | `/api/v1/generate/:jobId` | 查询任务状态与进度 |
|
||||||
|
| GET | `/api/v1/generate/:jobId/result` | 获取生成结果(素材 URL + 元数据) |
|
||||||
|
| WS | `/api/v1/generate/:jobId/ws` | WebSocket 实时进度推送 |
|
||||||
|
|
||||||
|
请求体示例 (POST /api/v1/generate):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "a brave knight in shining armor",
|
||||||
|
"assetType": "sprite",
|
||||||
|
"styleSeedId": "seed_abc123",
|
||||||
|
"params": {
|
||||||
|
"resolution": 64,
|
||||||
|
"frames": { "directions": 8, "framesPerDirection": 4 },
|
||||||
|
"format": "spritesheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 风格管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/v1/style/seed` | 创建风格种子(可上传参考图) |
|
||||||
|
| GET | `/api/v1/style/seed/:id` | 获取风格种子详情 |
|
||||||
|
| GET | `/api/v1/style/seeds` | 列出所有风格种子 |
|
||||||
|
| POST | `/api/v1/style/extract` | 从参考图提取风格特征向量 |
|
||||||
|
|
||||||
|
### 4.3 缓存管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| DELETE | `/api/v1/cache/:key` | 清除特定缓存 |
|
||||||
|
| POST | `/api/v1/cache/clear` | 批量清除缓存 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、后端分层结构
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/internal/
|
||||||
|
├── handler/ # HTTP handlers(薄层,只做参数绑定 + 调用 service)
|
||||||
|
│ ├── generate.go # 生成任务提交 / 查询 / WebSocket
|
||||||
|
│ ├── style.go # 风格种子 CRUD + 特征提取
|
||||||
|
│ └── health.go # [已有] 健康检查
|
||||||
|
├── service/ # 业务逻辑层
|
||||||
|
│ ├── pipeline.go # 核心管线:接收请求 → PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter
|
||||||
|
│ ├── inference.go # AI 推理 API 调用封装(可替换模型提供商)
|
||||||
|
│ └── style.go # 风格种子管理 & 参考图特征提取
|
||||||
|
├── model/ # 数据模型 / DTO
|
||||||
|
│ ├── response.go # [已有] 统一响应
|
||||||
|
│ ├── task.go # 生成任务 & 素材
|
||||||
|
│ └── style.go # 风格种子
|
||||||
|
├── middleware/ # 中间件
|
||||||
|
│ ├── cors.go
|
||||||
|
│ ├── logger.go
|
||||||
|
│ └── ratelimit.go
|
||||||
|
├── config/ # [已有] 配置
|
||||||
|
│ └── config.go
|
||||||
|
├── queue/ # 异步任务队列
|
||||||
|
│ └── jobqueue.go
|
||||||
|
└── cache/ # 缓存层(请求去重 + 结果缓存)
|
||||||
|
└── cache.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分层原则
|
||||||
|
|
||||||
|
- **handler**:只做参数绑定、校验、调用 service、返回响应。一个 handler 对应一组 API 路由。
|
||||||
|
- **service**:承载所有业务逻辑。`pipeline.go` 是唯一编排入口,不再拆分成 orchestrator/preprocess/generator/postprocess 四个文件——这些是同一根管线的顺序步骤,拆开反而增加耦合面。
|
||||||
|
- **model**:纯数据结构,不含业务逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、前端集成
|
||||||
|
|
||||||
|
### 核心交互流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户输入提示词 → 选择风格种子 → 配置参数 → 提交生成
|
||||||
|
→ 轮询/WebSocket 进度 → 预览缩略图 → 确认/调整 → 下载素材包
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键组件树
|
||||||
|
|
||||||
|
```
|
||||||
|
App
|
||||||
|
├── AssetGenerator # 生成工作台(主页面)
|
||||||
|
│ ├── PromptInput # 提示词输入 + 素材类型选择
|
||||||
|
│ ├── StyleSelector # 风格种子选择器(含预览色板)
|
||||||
|
│ ├── ParamPanel # 参数面板(分辨率/帧数/格式/引擎)
|
||||||
|
│ └── ReferenceUpload # 参考图上传
|
||||||
|
├── GenerationProgress # 生成进度展示
|
||||||
|
│ ├── ProgressBar # 总进度
|
||||||
|
│ ├── ThumbnailGrid # 缩略图网格(低分辨率预览)
|
||||||
|
│ └── AssetCard # 单张素材预览 + 质量评分
|
||||||
|
├── StyleManager # 风格种子管理
|
||||||
|
│ └── StylePalette # 色板可视化
|
||||||
|
└── ExportPanel # 导出面板
|
||||||
|
├── FormatSelector # 格式选择(spritesheet/json/unity/godot)
|
||||||
|
└── DownloadButton # 打包下载
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态管理 (zustand)
|
||||||
|
|
||||||
|
- `useGenerationStore` — 生成任务队列、进度、结果
|
||||||
|
- `useStyleStore` — 风格种子列表、当前选中风格
|
||||||
|
- `useWebSocket` — WebSocket 连接管理 (自定义 hook)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# ---- Build Stage ----
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Runtime Stage ----
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>gen2d</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA fallback
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 反向代理 /api 到后端服务
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1849
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "gen2d-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>gen2d</h1>
|
||||||
|
<p>AI 驱动的 2D 游戏素材生成工具</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user