feat: FormatAdapter 接入 splitsprite+gifmaker 管线
- FormatAdapter: 精灵表模式(单图)自动调用 splitsprite.Process 拆分帧 - 拆分后每帧编码为独立 PNG Asset,Metadata 包含 GIFPreview 字节 - 多图/非精灵表模式保持原样透传 - AssetParams 新增 GridRows/GridCols 覆盖投影检测 - AssetMetadata 新增 GIFPreview 字段
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user