!8 !7 feat: 新增精灵表清洗拆分组件

Merge pull request !8 from 何朝晖/develop
This commit is contained in:
GYhde
2026-05-23 09:01:22 +00:00
committed by Gitee
24 changed files with 3004 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules
frontend/node_modules
frontend/dist
backend/bin
.git
+3
View File
@@ -13,3 +13,6 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# Frontend
**/node_modules/
+162
View File
@@ -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 使用中文或英文均可,但需要清晰描述变更内容。
+18
View File
@@ -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"]
+30
View File
@@ -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)
}
}
+37
View File
@@ -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
)
+89
View File
@@ -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=
+35
View File
@@ -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
}
+12
View File
@@ -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"}))
}
+19
View File
@@ -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}
}
+299
View File
@@ -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
}
+21
View File
@@ -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
View File
@@ -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"
}
}
```
+234
View File
@@ -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)
+17
View File
@@ -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
+12
View File
@@ -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>
+21
View File
@@ -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;
}
}
+1849
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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"
}
}
+10
View File
@@ -0,0 +1,10 @@
function App() {
return (
<div>
<h1>gen2d</h1>
<p>AI 驱动的 2D 游戏素材生成工具</p>
</div>
)
}
export default App
+9
View File
@@ -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>,
)
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+21
View File
@@ -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"]
}
+15
View File
@@ -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,
},
},
},
})