fix: 工程类型与素材风格标签分离,支持双栏自定义标签

- 工程层面:新增 PROJECT_TYPES(2D平台跳跃/角色扮演等8种),替代原先与素材生成重复的风格标签栏
- 素材生成层面:StyleSelector 作为素材风格主配置项,新增 CustomTagsEditor 支持任务级自定义标签
- extractTags 合并工程类型、风格分类、工程自定义标签、任务自定义标签
- StyleSelector 支持 categories 参数复用
- 后端:spritesheet 拆分失败回退为单帧、GIF 上传为独立素材、AssetResponse 增加 metadata 字段
This commit is contained in:
2026-05-25 19:31:36 +08:00
parent c48ba6e61d
commit ab2b72e746
14 changed files with 149 additions and 57 deletions
+25 -8
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"image"
"image/png"
"strings"
@@ -159,6 +160,7 @@ var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input
})
// processSpriteSheet 将单张精灵表拆分为独立帧并生成 GIF 预览。
// 拆分失败时回退为单帧,不阻塞管线。
func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetParams, resolution int) (PipelineOutput, error) {
l := logger.FromCtx(ctx)
src, err := png.Decode(bytes.NewReader(img.Data))
@@ -175,13 +177,19 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
frames, err := splitsprite.Process(src, opts)
if err != nil {
l.Error("format_adapter split sprite sheet failed", "error", err)
return PipelineOutput{}, fmt.Errorf("split sprite sheet: %w", err)
l.Warn("format_adapter split sprite sheet failed, falling back to single frame", "error", err)
// 回退:整张图作为单帧
frames = []image.Image{src}
}
l.Info("format_adapter split sprite sheet", "frame_count", len(frames))
// 帧 → Asset
assets := make([]Asset, 0, len(frames))
// 帧 → Asset(先保留原始精灵表,再追加拆分后的帧)
assets := make([]Asset, 0, len(frames)+2)
assets = append(assets, Asset{
Data: img.Data,
Format: img.Format,
URL: "output/spritesheet.png",
})
for i, f := range frames {
var buf bytes.Buffer
if err := png.Encode(&buf, f); err != nil {
@@ -195,12 +203,19 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
})
}
// GIF 预览
// GIF 预览 → Asset(排在帧之后)
var gifURL string
var gifBuf bytes.Buffer
if err := gifmaker.Encode(&gifBuf, frames, nil); err != nil {
l.Warn("format_adapter generate GIF preview failed", "error", err)
} else {
l.Info("format_adapter generated GIF preview", "size_bytes", gifBuf.Len())
gifURL = "output/preview.gif"
assets = append(assets, Asset{
Data: gifBuf.Bytes(),
Format: "gif",
URL: gifURL,
})
}
fw, fh := 0, 0
@@ -216,7 +231,7 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
FrameHeight: fh,
FrameCount: len(frames),
Directions: params.Frames.Directions,
GIFPreview: gifBuf.Bytes(),
GIFURL: gifURL,
},
}, nil
}
@@ -234,7 +249,7 @@ func buildStyleDescription(projectStyle, taskStyle map[string]string) string {
return "风格约束:" + strings.Join(parts, ";")
}
// appendTechNotes 在无标签(不走 PromptAgent)时补上技术参数段。
// appendTechNotes 在提示词末尾追加技术参数段。
func appendTechNotes(prompt, assetType string, params AssetParams) string {
var parts []string
if prompt != "" {
@@ -250,7 +265,9 @@ func appendTechNotes(prompt, assetType string, params AssetParams) string {
if params.Frames.FramesPerDirection > 0 {
parts = append(parts, fmt.Sprintf("每方向帧数: %d", params.Frames.FramesPerDirection))
}
if params.Format != "" {
if params.Format == "spritesheet" {
parts = append(parts, "输出格式: spritesheet(将所有帧排列在一张图上,帧之间用8-16像素纯白间隙分隔,等间距网格布局)")
} else if params.Format != "" {
parts = append(parts, fmt.Sprintf("输出格式: %s", params.Format))
}
return strings.Join(parts, ";")
+5 -5
View File
@@ -64,9 +64,9 @@ type Asset struct {
// AssetMetadata 素材元数据
type AssetMetadata struct {
FrameWidth int
FrameHeight int
FrameCount int
Directions int
GIFPreview []byte `json:"-"` // animated GIF preview (not serialized)
FrameWidth int `json:"frameWidth"`
FrameHeight int `json:"frameHeight"`
FrameCount int `json:"frameCount"`
Directions int `json:"directions"`
GIFURL string `json:"gifUrl,omitempty"` // animated GIF preview URL
}