feat: FormatAdapter 接入 splitsprite+gifmaker 管线

- FormatAdapter: 精灵表模式(单图)自动调用 splitsprite.Process 拆分帧
- 拆分后每帧编码为独立 PNG Asset,Metadata 包含 GIFPreview 字节
- 多图/非精灵表模式保持原样透传
- AssetParams 新增 GridRows/GridCols 覆盖投影检测
- AssetMetadata 新增 GIFPreview 字段
This commit is contained in:
2026-05-25 18:01:51 +08:00
parent 8309d32d4b
commit d5e2d7fa53
2 changed files with 91 additions and 13 deletions
+86 -12
View File
@@ -1,10 +1,16 @@
package service
import (
"bytes"
"context"
"fmt"
"image/png"
"strings"
"gen2d/internal/logger"
"gen2d/pkg/gifmaker"
"gen2d/pkg/splitsprite"
"github.com/cloudwego/eino/compose"
)
@@ -127,7 +133,7 @@ var qualitySupervisorNode = compose.InvokableLambda(func(ctx context.Context, im
return input, nil
})
// formatAdapterNode 节点:从 state 读取图片,格式转换,组装输出。
// formatAdapterNode 节点:精灵表格式时调用 splitsprite 拆分 + gifmaker 生成 GIF 预览。
var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input PipelineInput) (PipelineOutput, error) {
var images []GeneratedImage
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
@@ -135,6 +141,18 @@ var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input
return nil
})
params := input.Params
resolution := params.Resolution
if resolution <= 0 {
resolution = 64
}
// 精灵表模式:单张图时拆分 + GIF 预览;多图时已是独立帧,透传
if params.Format == "spritesheet" && len(images) == 1 {
return processSpriteSheet(ctx, images[0], params, resolution)
}
// 普通模式:原样透传
assets := make([]Asset, len(images))
for i, img := range images {
assets[i] = Asset{
@@ -144,23 +162,79 @@ var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input
}
}
resolution := input.Params.Resolution
if resolution <= 0 {
resolution = 64
return PipelineOutput{
Assets: assets,
Metadata: AssetMetadata{
FrameWidth: resolution,
FrameHeight: resolution,
FrameCount: len(images),
Directions: params.Frames.Directions,
},
}, nil
})
// 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))
if err != nil {
l.Error("format_adapter decode sprite sheet failed", "error", err)
return PipelineOutput{}, fmt.Errorf("decode sprite sheet: %w", err)
}
metadata := AssetMetadata{
FrameWidth: resolution,
FrameHeight: resolution,
FrameCount: len(images),
Directions: input.Params.Frames.Directions,
opts := splitsprite.DefaultOptions()
if params.GridRows > 0 && params.GridCols > 0 {
opts.GridRows = params.GridRows
opts.GridCols = params.GridCols
}
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.Info("format_adapter split sprite sheet", "frame_count", len(frames))
// 帧 → Asset
assets := make([]Asset, 0, len(frames))
for i, f := range frames {
var buf bytes.Buffer
if err := png.Encode(&buf, f); err != nil {
l.Error("format_adapter encode frame failed", "error", err)
return PipelineOutput{}, fmt.Errorf("encode frame %d: %w", i, err)
}
assets = append(assets, Asset{
Data: buf.Bytes(),
Format: "png",
URL: fmt.Sprintf("output/frame_%03d.png", i),
})
}
// GIF 预览
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())
}
fw, fh := 0, 0
if len(frames) > 0 {
b := frames[0].Bounds()
fw, fh = b.Dx(), b.Dy()
}
return PipelineOutput{
Assets: assets,
Metadata: metadata,
Assets: assets,
Metadata: AssetMetadata{
FrameWidth: fw,
FrameHeight: fh,
FrameCount: len(frames),
Directions: params.Frames.Directions,
GIFPreview: gifBuf.Bytes(),
},
}, nil
})
}
// buildStyleDescription 将风格键值对转为自然语言描述,供 PromptAgent 注入。
func buildStyleDescription(projectStyle, taskStyle map[string]string) string {
+5 -1
View File
@@ -36,11 +36,14 @@ type AssetParams struct {
Resolution int
Frames FrameParams
Format string // "spritesheet" / "individual"
// GridRows / GridCols override projection-based split for sprite sheets.
GridRows int
GridCols int
}
// FrameParams 帧参数
type FrameParams struct {
Directions int
Directions int
FramesPerDirection int
}
@@ -65,4 +68,5 @@ type AssetMetadata struct {
FrameHeight int
FrameCount int
Directions int
GIFPreview []byte `json:"-"` // animated GIF preview (not serialized)
}