feat: 引入 Eino 框架,实现四阶段生成管线
基于 compose.Graph 编排 PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter, 含质检重试分支(最多 3 次)和降级输出。推理层提供 mock 模式,可替换为真实 API。
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// GenerateImages 调用 AI 推理 API 生成图片。
|
||||
// MVP 阶段返回 mock 占位图。
|
||||
func GenerateImages(ctx context.Context, prompt string, params AssetParams) ([]GeneratedImage, error) {
|
||||
size := params.Resolution
|
||||
if size <= 0 {
|
||||
size = 64
|
||||
}
|
||||
|
||||
count := 1
|
||||
if params.Frames.Directions > 0 && params.Frames.FramesPerDirection > 0 {
|
||||
count = params.Frames.Directions * params.Frames.FramesPerDirection
|
||||
}
|
||||
|
||||
images := make([]GeneratedImage, count)
|
||||
for i := 0; i < count; i++ {
|
||||
data, err := generateMockImage(size, i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate mock image %d: %w", i, err)
|
||||
}
|
||||
images[i] = GeneratedImage{
|
||||
Data: data,
|
||||
Width: size,
|
||||
Height: size,
|
||||
Format: "png",
|
||||
}
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// QualityChecker 质检函数,可替换用于测试。
|
||||
// 签名:(ctx, images, style) → (pass, reason, error)
|
||||
var QualityChecker = defaultCheckQuality
|
||||
|
||||
// CheckQuality 调用当前 QualityChecker。
|
||||
func CheckQuality(ctx context.Context, images []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
return QualityChecker(ctx, images, style)
|
||||
}
|
||||
|
||||
// defaultCheckQuality 默认 mock 质检,始终返回 pass。
|
||||
func defaultCheckQuality(ctx context.Context, images []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// NewCountedQualityChecker 创建一个在第 passOnRetry 次调用时返回 pass 的质检函数。
|
||||
func NewCountedQualityChecker(passOnRetry int) func(context.Context, []GeneratedImage, map[string]string) (bool, string, error) {
|
||||
var callCount int
|
||||
return func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
callCount++
|
||||
if callCount >= passOnRetry {
|
||||
return true, "", nil
|
||||
}
|
||||
return false, fmt.Sprintf("风格不一致(第 %d 次质检)", callCount), nil
|
||||
}
|
||||
}
|
||||
|
||||
// AlwaysFailQualityChecker 始终返回 fail 的质检函数。
|
||||
func AlwaysFailQualityChecker() func(context.Context, []GeneratedImage, map[string]string) (bool, string, error) {
|
||||
return func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
return false, "风格不一致", nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateMockImage 生成一张带随机色块的 PNG 占位图
|
||||
func generateMockImage(size int, seed int) ([]byte, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
|
||||
// 用 seed 生成不同颜色
|
||||
r := uint8((seed*47 + 13) % 256)
|
||||
g := uint8((seed*83 + 37) % 256)
|
||||
b := uint8((seed*61 + 71) % 256)
|
||||
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// generateRandomBytes 用于生成随机数据(备用)
|
||||
func generateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
return b, err
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
)
|
||||
|
||||
// PromptBuilder 节点:接收输入,输出三段式提示词
|
||||
var promptBuilderNode = compose.InvokableLambda(func(ctx context.Context, in PipelineInput) (string, error) {
|
||||
return buildPrompt(in), nil
|
||||
})
|
||||
|
||||
// promptBuilderPreHandler 首次运行时保存输入到 state;重试时注入 RejectReason
|
||||
func promptBuilderPreHandler(ctx context.Context, in PipelineInput, state *PipelineState) (PipelineInput, error) {
|
||||
if state.RetryCount == 0 {
|
||||
state.Input = in
|
||||
} else if state.RejectReason != "" {
|
||||
in.RejectReason = state.RejectReason
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// promptBuilderPostHandler 将提示词写入全局状态
|
||||
func promptBuilderPostHandler(ctx context.Context, out string, state *PipelineState) (string, error) {
|
||||
state.FinalPrompt = out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AssetGenerator 节点:调用 AI 推理 API 出图
|
||||
var assetGeneratorNode = compose.InvokableLambda(func(ctx context.Context, prompt string) ([]GeneratedImage, error) {
|
||||
var params AssetParams
|
||||
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
params = state.Input.Params
|
||||
return nil
|
||||
})
|
||||
return GenerateImages(ctx, prompt, params)
|
||||
})
|
||||
|
||||
// assetGeneratorPostHandler 将原始图片写入全局状态
|
||||
func assetGeneratorPostHandler(ctx context.Context, out []GeneratedImage, state *PipelineState) ([]GeneratedImage, error) {
|
||||
state.RawImages = out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// QualitySupervisor 节点:质检,输出 PipelineInput 供下游节点消费。
|
||||
// 将图片存入 state,设置路由目标 NextNode。
|
||||
var qualitySupervisorNode = compose.InvokableLambda(func(ctx context.Context, images []GeneratedImage) (PipelineInput, error) {
|
||||
var input PipelineInput
|
||||
err := compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
// 保存图片到状态
|
||||
state.RawImages = images
|
||||
|
||||
// 质检
|
||||
style := mergeStyle(state.Input.ProjectStyle, state.Input.TaskStyle)
|
||||
pass, reason, checkErr := CheckQuality(ctx, images, style)
|
||||
if checkErr != nil {
|
||||
return fmt.Errorf("quality check: %w", checkErr)
|
||||
}
|
||||
|
||||
state.PassQuality = pass
|
||||
if !pass {
|
||||
state.RejectReason = reason
|
||||
}
|
||||
|
||||
// 决定路由
|
||||
if pass {
|
||||
state.NextNode = nodeFormatAdapter
|
||||
} else if state.RetryCount >= 3 {
|
||||
state.NextNode = nodeFormatAdapter // 超过重试次数,降级输出
|
||||
} else {
|
||||
state.RetryCount++
|
||||
state.NextNode = nodePromptBuilder // 重生成
|
||||
}
|
||||
|
||||
input = state.Input
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return PipelineInput{}, err
|
||||
}
|
||||
return input, nil
|
||||
})
|
||||
|
||||
// formatAdapterNode 节点:从 state 读取图片,格式转换,组装输出
|
||||
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 {
|
||||
images = state.RawImages
|
||||
return nil
|
||||
})
|
||||
|
||||
assets := make([]Asset, len(images))
|
||||
for i, img := range images {
|
||||
assets[i] = Asset{
|
||||
Data: img.Data,
|
||||
Format: img.Format,
|
||||
URL: fmt.Sprintf("output/%d.%s", i, img.Format),
|
||||
}
|
||||
}
|
||||
|
||||
resolution := input.Params.Resolution
|
||||
if resolution <= 0 {
|
||||
resolution = 64
|
||||
}
|
||||
|
||||
metadata := AssetMetadata{
|
||||
FrameWidth: resolution,
|
||||
FrameHeight: resolution,
|
||||
FrameCount: len(images),
|
||||
Directions: input.Params.Frames.Directions,
|
||||
}
|
||||
|
||||
return PipelineOutput{
|
||||
Assets: assets,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
})
|
||||
|
||||
// buildPrompt 构建三段式提示词
|
||||
func buildPrompt(in PipelineInput) string {
|
||||
var parts []string
|
||||
|
||||
// 【主题】
|
||||
parts = append(parts, fmt.Sprintf("【主题】%s", in.Prompt))
|
||||
|
||||
// 【约束】
|
||||
constraints := buildConstraints(in)
|
||||
parts = append(parts, fmt.Sprintf("【约束】%s", constraints))
|
||||
|
||||
// 【内容】
|
||||
content := buildContent(in)
|
||||
parts = append(parts, fmt.Sprintf("【内容】%s", content))
|
||||
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// buildConstraints 合并风格 + 负面提示词 + 重试原因
|
||||
func buildConstraints(in PipelineInput) string {
|
||||
style := mergeStyle(in.ProjectStyle, in.TaskStyle)
|
||||
|
||||
var parts []string
|
||||
for k, v := range style {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", k, v))
|
||||
}
|
||||
|
||||
if in.RejectReason != "" {
|
||||
parts = append(parts, fmt.Sprintf("上次质检问题:%s", in.RejectReason))
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "无特殊约束"
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// buildContent 构建技术参数段
|
||||
func buildContent(in PipelineInput) string {
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("素材类型: %s", in.AssetType))
|
||||
if in.Params.Resolution > 0 {
|
||||
parts = append(parts, fmt.Sprintf("分辨率: %d", in.Params.Resolution))
|
||||
}
|
||||
if in.Params.Frames.Directions > 0 {
|
||||
parts = append(parts, fmt.Sprintf("方向数: %d", in.Params.Frames.Directions))
|
||||
}
|
||||
if in.Params.Frames.FramesPerDirection > 0 {
|
||||
parts = append(parts, fmt.Sprintf("每方向帧数: %d", in.Params.Frames.FramesPerDirection))
|
||||
}
|
||||
if in.Params.Format != "" {
|
||||
parts = append(parts, fmt.Sprintf("输出格式: %s", in.Params.Format))
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// mergeStyle 合并工程风格与任务风格覆盖,任务同名键覆盖工程
|
||||
func mergeStyle(projectStyle, taskStyle map[string]string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range projectStyle {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range taskStyle {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
)
|
||||
|
||||
const (
|
||||
nodePromptBuilder = "prompt_builder"
|
||||
nodeAssetGenerator = "asset_generator"
|
||||
nodeQualitySupervisor = "quality_supervisor"
|
||||
nodeFormatAdapter = "format_adapter"
|
||||
)
|
||||
|
||||
// NewGenerateGraph 创建四阶段生成管线 Graph。
|
||||
//
|
||||
// START → PromptBuilder → AssetGenerator → QualitySupervisor
|
||||
// ├── pass → FormatAdapter → END
|
||||
// └── fail, retry<3 → PromptBuilder
|
||||
// └── fail, retry>=3 → FormatAdapter (降级)
|
||||
func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
|
||||
g := compose.NewGraph[PipelineInput, PipelineOutput](
|
||||
compose.WithGenLocalState(func(ctx context.Context) *PipelineState {
|
||||
return &PipelineState{}
|
||||
}),
|
||||
)
|
||||
|
||||
// 添加节点
|
||||
if err := g.AddLambdaNode(nodePromptBuilder, promptBuilderNode,
|
||||
compose.WithStatePreHandler(promptBuilderPreHandler),
|
||||
compose.WithStatePostHandler(promptBuilderPostHandler),
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodePromptBuilder, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeAssetGenerator, assetGeneratorNode,
|
||||
compose.WithStatePostHandler(assetGeneratorPostHandler),
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeAssetGenerator, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeQualitySupervisor, qualitySupervisorNode); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeQualitySupervisor, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeFormatAdapter, formatAdapterNode); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeFormatAdapter, err)
|
||||
}
|
||||
|
||||
// 连线:正常路径
|
||||
if err := g.AddEdge(compose.START, nodePromptBuilder); err != nil {
|
||||
return nil, fmt.Errorf("add edge START->%s: %w", nodePromptBuilder, err)
|
||||
}
|
||||
if err := g.AddEdge(nodePromptBuilder, nodeAssetGenerator); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->%s: %w", nodePromptBuilder, nodeAssetGenerator, err)
|
||||
}
|
||||
if err := g.AddEdge(nodeAssetGenerator, nodeQualitySupervisor); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->%s: %w", nodeAssetGenerator, nodeQualitySupervisor, err)
|
||||
}
|
||||
if err := g.AddEdge(nodeFormatAdapter, compose.END); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->END: %w", nodeFormatAdapter, err)
|
||||
}
|
||||
|
||||
// 连线:质检分支(从 state.NextNode 读取路由目标)
|
||||
if err := g.AddBranch(nodeQualitySupervisor, compose.NewGraphBranch(
|
||||
func(ctx context.Context, _ PipelineInput) (string, error) {
|
||||
var next string
|
||||
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
next = state.NextNode
|
||||
return nil
|
||||
})
|
||||
return next, nil
|
||||
},
|
||||
map[string]bool{nodePromptBuilder: true, nodeFormatAdapter: true},
|
||||
)); err != nil {
|
||||
return nil, fmt.Errorf("add branch at %s: %w", nodeQualitySupervisor, err)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// RunPipeline 编译并执行生成管线
|
||||
func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) {
|
||||
g, err := NewGenerateGraph()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create graph: %w", err)
|
||||
}
|
||||
|
||||
r, err := g.Compile(ctx, compose.WithMaxRunSteps(20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compile graph: %w", err)
|
||||
}
|
||||
|
||||
output, err := r.Invoke(ctx, in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invoke pipeline: %w", err)
|
||||
}
|
||||
|
||||
return &output, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPipeline_HappyPath(t *testing.T) {
|
||||
// 质检一次通过
|
||||
QualityChecker = func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
return true, "", nil
|
||||
}
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一个拿剑的小人",
|
||||
AssetType: "sprite",
|
||||
ProjectStyle: map[string]string{
|
||||
"artStyle": "pixel",
|
||||
"palette": "warm",
|
||||
},
|
||||
Params: AssetParams{
|
||||
Resolution: 64,
|
||||
Format: "spritesheet",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output.Assets) == 0 {
|
||||
t.Fatal("expected non-empty assets")
|
||||
}
|
||||
if output.Metadata.FrameWidth != 64 {
|
||||
t.Errorf("expected FrameWidth=64, got %d", output.Metadata.FrameWidth)
|
||||
}
|
||||
if output.Metadata.FrameHeight != 64 {
|
||||
t.Errorf("expected FrameHeight=64, got %d", output.Metadata.FrameHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_RetryThenPass(t *testing.T) {
|
||||
// 质检前 2 次 fail,第 3 次 pass
|
||||
QualityChecker = NewCountedQualityChecker(3)
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一把火焰剑",
|
||||
AssetType: "sprite",
|
||||
Params: AssetParams{
|
||||
Resolution: 32,
|
||||
Frames: FrameParams{
|
||||
Directions: 4,
|
||||
FramesPerDirection: 2,
|
||||
},
|
||||
Format: "spritesheet",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
// 4 directions × 2 frames = 8 张图
|
||||
if len(output.Assets) != 8 {
|
||||
t.Errorf("expected 8 assets, got %d", len(output.Assets))
|
||||
}
|
||||
if output.Metadata.FrameCount != 8 {
|
||||
t.Errorf("expected FrameCount=8, got %d", output.Metadata.FrameCount)
|
||||
}
|
||||
if output.Metadata.Directions != 4 {
|
||||
t.Errorf("expected Directions=4, got %d", output.Metadata.Directions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_MaxRetryDegrade(t *testing.T) {
|
||||
// 质检始终 fail,超过 3 次后降级输出
|
||||
QualityChecker = AlwaysFailQualityChecker()
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一只飞龙",
|
||||
AssetType: "sprite",
|
||||
Params: AssetParams{
|
||||
Resolution: 48,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
// 降级也应该有输出
|
||||
if len(output.Assets) == 0 {
|
||||
t.Fatal("expected non-empty assets even on degrade")
|
||||
}
|
||||
if output.Metadata.FrameWidth != 48 {
|
||||
t.Errorf("expected FrameWidth=48, got %d", output.Metadata.FrameWidth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_StyleMerge(t *testing.T) {
|
||||
// 验证风格合并:task 覆盖 project
|
||||
QualityChecker = func(_ context.Context, _ []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
// 验证合并结果
|
||||
if style["artStyle"] != "realistic" {
|
||||
t.Errorf("expected artStyle=realistic (task override), got %s", style["artStyle"])
|
||||
}
|
||||
if style["palette"] != "warm" {
|
||||
t.Errorf("expected palette=warm (from project), got %s", style["palette"])
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
_, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "测试风格合并",
|
||||
AssetType: "sprite",
|
||||
ProjectStyle: map[string]string{
|
||||
"artStyle": "pixel",
|
||||
"palette": "warm",
|
||||
},
|
||||
TaskStyle: map[string]string{
|
||||
"artStyle": "realistic", // 覆盖 project
|
||||
},
|
||||
Params: AssetParams{Resolution: 64},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package service
|
||||
|
||||
// PipelineInput 管线入口输入
|
||||
type PipelineInput struct {
|
||||
Prompt string // 用户原始文本
|
||||
AssetType string // 素材类型:sprite / background / ui / animation
|
||||
ProjectStyle map[string]string // 工程风格键值对
|
||||
TaskStyle map[string]string // 任务风格覆盖
|
||||
Params AssetParams // 技术参数
|
||||
RejectReason string // 重试时由 state 注入
|
||||
}
|
||||
|
||||
// PipelineState Graph 全局状态,通过 WithGenLocalState 注入
|
||||
type PipelineState struct {
|
||||
Input PipelineInput
|
||||
FinalPrompt string // PromptBuilder 输出的三段式提示词
|
||||
RawImages []GeneratedImage // AssetGenerator 输出的原始图片
|
||||
PassQuality bool // QualitySupervisor 质检结果
|
||||
RejectReason string // 质检不通过原因
|
||||
RetryCount int // 重试次数
|
||||
NextNode string // QualitySupervisor 设置的路由目标
|
||||
}
|
||||
|
||||
// PipelineOutput 管线最终输出
|
||||
type PipelineOutput struct {
|
||||
Assets []Asset // 生成结果素材列表
|
||||
Metadata AssetMetadata // 元数据
|
||||
}
|
||||
|
||||
// AssetParams 技术参数
|
||||
type AssetParams struct {
|
||||
Resolution int
|
||||
Frames FrameParams
|
||||
Format string // "spritesheet" / "individual"
|
||||
}
|
||||
|
||||
// FrameParams 帧参数
|
||||
type FrameParams struct {
|
||||
Directions int
|
||||
FramesPerDirection int
|
||||
}
|
||||
|
||||
// GeneratedImage AI 生成的原始图片
|
||||
type GeneratedImage struct {
|
||||
Data []byte
|
||||
Width int
|
||||
Height int
|
||||
Format string
|
||||
}
|
||||
|
||||
// Asset 最终输出素材
|
||||
type Asset struct {
|
||||
Data []byte
|
||||
Format string
|
||||
URL string
|
||||
}
|
||||
|
||||
// AssetMetadata 素材元数据
|
||||
type AssetMetadata struct {
|
||||
FrameWidth int
|
||||
FrameHeight int
|
||||
FrameCount int
|
||||
Directions int
|
||||
}
|
||||
Reference in New Issue
Block a user