diff --git a/backend/internal/service/nodes.go b/backend/internal/service/nodes.go index c64156d..56c35b3 100755 --- a/backend/internal/service/nodes.go +++ b/backend/internal/service/nodes.go @@ -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 { diff --git a/backend/internal/service/types.go b/backend/internal/service/types.go index 9db9f27..ee97c3c 100755 --- a/backend/internal/service/types.go +++ b/backend/internal/service/types.go @@ -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) }