@@ -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)
|
||||
# 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