feat: 提示词优化支持多布局模式识别与纯白背景统一

- buildMetaPrompt 按素材类型给出双模式指令(单素材/网格瓦片集),LLM 根据用户意图选择
- 新增 isSheetRequest 自动识别精灵表/瓦片集/tileset 关键字
- 模板回退链路全面支持 isSheet 双模式,默认单素材模式
- 所有素材类型统一纯白色背景(#FFFFFF),由后期 format 节点清洗去背
- 补充 sprite/background/ui/animation 四类素材的完整生成场景覆盖
This commit is contained in:
2026-05-25 16:23:00 +08:00
parent 57cbd11ff3
commit aff48b6780
2 changed files with 165 additions and 35 deletions
+77 -13
View File
@@ -89,7 +89,23 @@ func buildMetaPrompt(in PromptAgentInput) string {
sb.WriteString("输出要求:\n") sb.WriteString("输出要求:\n")
sb.WriteString("1. 三段式结构:【主题】描述画面主体与场景,【风格】描述艺术风格与视觉特征,【技术】描述分辨率、方向数等技术参数\n") sb.WriteString("1. 三段式结构:【主题】描述画面主体与场景,【风格】描述艺术风格与视觉特征,【技术】描述分辨率、方向数等技术参数\n")
sb.WriteString("2. 使用专业术语,描述具体、可执行\n") sb.WriteString("2. 使用专业术语,描述具体、可执行\n")
sb.WriteString("3. 风格一致,适合游戏资产管线\n\n") sb.WriteString("3. 风格一致,适合游戏资产管线\n")
sb.WriteString("4. 根据用户标签和描述,识别素材布局模式,在【技术】段明确标注格式:\n")
switch in.AssetType {
case "sprite":
sb.WriteString(" - 单个精灵(默认):独立PNG,纯白色背景(#FFFFFF),描述为单个角色立绘/道具图标\n")
sb.WriteString(" - 精灵表(spritesheet):角色/道具按行列等距网格排列,纯白色背景(#FFFFFF),帧间固定间距(2-4px),标注行列数,便于脚本一键拆分\n")
case "background":
sb.WriteString(" - 独立场景(默认):单张完整背景图,层次分明\n")
sb.WriteString(" - 场景瓦片集(tileset):地形/建筑元件按规则网格排列,纯白色背景(#FFFFFF),元件间固定间距,标注行列数与瓦片尺寸,确保无缝拼接\n")
case "ui":
sb.WriteString(" - 独立UI元素(默认):单个按钮/面板/图标,纯白色背景(#FFFFFF),独立PNG\n")
sb.WriteString(" - UI瓦片集(tileset):UI元件按规则网格排列,纯白色背景(#FFFFFF),元件间固定间距,标注行列数,支持九宫格缩放,便于脚本一键拆分\n")
case "animation":
sb.WriteString(" - 帧序列(默认):连续动画帧,独立帧文件或帧条带,纯白色背景(#FFFFFF)\n")
sb.WriteString(" - 动画精灵表(spritesheet):动画帧按行列等距网格排列,纯白色背景(#FFFFFF),帧间固定间距,标注行列数与方向数,便于脚本一键拆分\n")
}
sb.WriteString("\n\n")
tagStr := strings.Join(in.Tags, "、") tagStr := strings.Join(in.Tags, "、")
sb.WriteString(fmt.Sprintf("用户选择标签: %s\n", tagStr)) sb.WriteString(fmt.Sprintf("用户选择标签: %s\n", tagStr))
@@ -242,12 +258,13 @@ func parseStreamResponse(r io.Reader) (string, error) {
// ======================== 模板回退 ======================== // ======================== 模板回退 ========================
func fallbackRefine(metaPrompt string) PromptAgentOutput { func fallbackRefine(metaPrompt string) PromptAgentOutput {
tags, assetType := parseTagsFromMeta(metaPrompt) tags, assetType, userPrompt := parseTagsFromMeta(metaPrompt)
prompt := generateStructuredPrompt(tags, assetType) isSheet := isSheetRequest(tags, userPrompt)
prompt := generateStructuredPrompt(tags, assetType, isSheet)
return PromptAgentOutput{Prompt: prompt, RawText: prompt} return PromptAgentOutput{Prompt: prompt, RawText: prompt}
} }
func parseTagsFromMeta(meta string) (tags []string, assetType string) { func parseTagsFromMeta(meta string) (tags []string, assetType string, userPrompt string) {
lines := strings.Split(meta, "\n") lines := strings.Split(meta, "\n")
for _, line := range lines { for _, line := range lines {
if strings.HasPrefix(line, "用户选择标签:") { if strings.HasPrefix(line, "用户选择标签:") {
@@ -262,34 +279,69 @@ func parseTagsFromMeta(meta string) (tags []string, assetType string) {
if strings.HasPrefix(line, "素材类型:") { if strings.HasPrefix(line, "素材类型:") {
assetType = strings.TrimSpace(strings.TrimPrefix(line, "素材类型: ")) assetType = strings.TrimSpace(strings.TrimPrefix(line, "素材类型: "))
} }
if strings.HasPrefix(line, "用户原始描述:") {
userPrompt = strings.TrimSpace(strings.TrimPrefix(line, "用户原始描述: "))
}
} }
return return
} }
func generateStructuredPrompt(tags []string, assetType string) string { // isSheetRequest 从标签和提示词中识别是否为网格/瓦片集/精灵表模式。
func isSheetRequest(tags []string, prompt string) bool {
keywords := []string{"精灵表", "spritesheet", "瓦片集", "tileset", "tilemap"}
for _, t := range tags {
tLower := strings.ToLower(t)
for _, kw := range keywords {
if strings.Contains(tLower, kw) {
return true
}
}
}
promptLower := strings.ToLower(prompt)
for _, kw := range keywords {
if strings.Contains(promptLower, kw) {
return true
}
}
return false
}
func generateStructuredPrompt(tags []string, assetType string, isSheet bool) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("【主题】") sb.WriteString("【主题】")
sb.WriteString(buildSubject(tags, assetType)) sb.WriteString(buildSubject(tags, assetType, isSheet))
sb.WriteString("\n") sb.WriteString("\n")
sb.WriteString("【风格】") sb.WriteString("【风格】")
sb.WriteString(buildStyle(tags)) sb.WriteString(buildStyle(tags))
sb.WriteString("\n") sb.WriteString("\n")
sb.WriteString("【技术】") sb.WriteString("【技术】")
sb.WriteString(buildTechNotes(assetType)) sb.WriteString(buildTechNotes(assetType, isSheet))
sb.WriteString("\n") sb.WriteString("\n")
return sb.String() return sb.String()
} }
func buildSubject(tags []string, assetType string) string { func buildSubject(tags []string, assetType string, isSheet bool) string {
tagStr := strings.Join(tags, "、") tagStr := strings.Join(tags, "、")
switch assetType { switch assetType {
case "sprite": case "sprite":
if isSheet {
return fmt.Sprintf("一个融合%s元素的游戏角色精灵表,角色按行列等距网格排列,轮廓清晰,适合作为2D游戏角色", tagStr)
}
return fmt.Sprintf("一个融合%s元素的游戏角色精灵图,正面站立姿势,轮廓清晰,适合作为2D游戏角色", tagStr) return fmt.Sprintf("一个融合%s元素的游戏角色精灵图,正面站立姿势,轮廓清晰,适合作为2D游戏角色", tagStr)
case "background": case "background":
if isSheet {
return fmt.Sprintf("一套%s风格的游戏场景瓦片集,地形/建筑元件按规则网格排列,适合2D游戏地图拼接", tagStr)
}
return fmt.Sprintf("一个%s风格的游戏场景背景,层次分明,包含前景、中景和远景", tagStr) return fmt.Sprintf("一个%s风格的游戏场景背景,层次分明,包含前景、中景和远景", tagStr)
case "ui": case "ui":
if isSheet {
return fmt.Sprintf("一套%s风格的UI瓦片集,UI元件按规则网格排列,适合脚本一键拆分", tagStr)
}
return fmt.Sprintf("一套%s风格的游戏UI元素,包括按钮、面板和图标", tagStr) return fmt.Sprintf("一套%s风格的游戏UI元素,包括按钮、面板和图标", tagStr)
case "animation": case "animation":
if isSheet {
return fmt.Sprintf("一个%s风格的角色动画精灵表,动画帧按行列等距网格排列,动作流畅连贯", tagStr)
}
return fmt.Sprintf("一个%s风格的角色动画帧序列,动作流畅连贯", tagStr) return fmt.Sprintf("一个%s风格的角色动画帧序列,动作流畅连贯", tagStr)
default: default:
return fmt.Sprintf("一个%s风格的游戏素材,高质量,适合2D游戏使用", tagStr) return fmt.Sprintf("一个%s风格的游戏素材,高质量,适合2D游戏使用", tagStr)
@@ -304,17 +356,29 @@ func buildStyle(tags []string) string {
return strings.Join(parts, ";") return strings.Join(parts, ";")
} }
func buildTechNotes(assetType string) string { func buildTechNotes(assetType string, isSheet bool) string {
switch assetType { switch assetType {
case "sprite": case "sprite":
return "输出格式: spritesheet;分辨率: 64x64 或 128x128;透明背景" if isSheet {
return "输出格式: spritesheet;行列间隔完全相等、可脚本一键拆分对齐;纯白色背景(#FFFFFF,无渐变无噪点);帧间固定间距(2-4px);标注行列数"
}
return "输出格式: 独立PNG;纯白色背景(#FFFFFF);尺寸: 按角色比例适配"
case "background": case "background":
if isSheet {
return "输出格式: 场景瓦片集(tileset);规则网格排列;纯白色背景(#FFFFFF);瓦片间固定间距;标注行列数与瓦片尺寸;确保无缝拼接"
}
return "输出格式: 独立PNG;分辨率: 1920x1080;层次分明的前中后景" return "输出格式: 独立PNG;分辨率: 1920x1080;层次分明的前中后景"
case "ui": case "ui":
return "输出格式: 独立PNG素材;分辨率: 按元素适配;支持九宫格缩放" if isSheet {
return "输出格式: UI瓦片集(tileset);规则网格排列;纯白色背景(#FFFFFF);元件间固定间距;标注行列数;支持九宫格缩放;可脚本一键拆分"
}
return "输出格式: 独立PNG素材;纯白色背景(#FFFFFF);分辨率: 按元素适配;支持九宫格缩放"
case "animation": case "animation":
return "输出格式: spritesheet或帧序列;建议4方向x4帧;透明背景" if isSheet {
return "输出格式: 动画精灵表(spritesheet);行列间隔相等;纯白色背景(#FFFFFF,无渐变无噪点);帧间固定间距;标注行列数与方向数;可脚本一键拆分"
}
return "输出格式: 帧序列或帧条带;独立帧文件;纯白色背景(#FFFFFF);建议4方向x4帧"
default: default:
return "输出格式: PNG;分辨率: 标准2D游戏分辨率" return "输出格式: PNG;纯白色背景(#FFFFFF);分辨率: 标准2D游戏分辨率"
} }
} }
+87 -21
View File
@@ -31,14 +31,30 @@ func TestRunPromptAgent_Fallback(t *testing.T) {
func TestRunPromptAgent_Sprite(t *testing.T) { func TestRunPromptAgent_Sprite(t *testing.T) {
output, err := RunPromptAgent(context.Background(), PromptAgentInput{ output, err := RunPromptAgent(context.Background(), PromptAgentInput{
Tags: []string{"像素", "中世纪", "战士"}, Tags: []string{"像素", "中世纪", "战士", "精灵表"},
AssetType: "sprite", AssetType: "sprite",
}) })
if err != nil { if err != nil {
t.Fatalf("RunPromptAgent failed: %v", err) t.Fatalf("RunPromptAgent failed: %v", err)
} }
if !strings.Contains(output.Prompt, "spritesheet") { if !strings.Contains(output.Prompt, "spritesheet") {
t.Errorf("sprite output should mention spritesheet: %s", output.Prompt) t.Errorf("sprite sheet output should mention spritesheet: %s", output.Prompt)
}
}
func TestRunPromptAgent_SpriteSingle(t *testing.T) {
output, err := RunPromptAgent(context.Background(), PromptAgentInput{
Tags: []string{"像素", "中世纪", "战士"},
AssetType: "sprite",
})
if err != nil {
t.Fatalf("RunPromptAgent failed: %v", err)
}
if !strings.Contains(output.Prompt, "纯白色背景") {
t.Errorf("single sprite output should mention 纯白色背景: %s", output.Prompt)
}
if strings.Contains(output.Prompt, "spritesheet") {
t.Errorf("single sprite output should not mention spritesheet: %s", output.Prompt)
} }
} }
@@ -251,8 +267,9 @@ func TestBuildMetaPrompt_NoUserNote(t *testing.T) {
func TestParseTagsFromMeta(t *testing.T) { func TestParseTagsFromMeta(t *testing.T) {
meta := `用户选择标签: 像素、中世纪、战士 meta := `用户选择标签: 像素、中世纪、战士
素材类型: sprite` 素材类型: sprite
tags, assetType := parseTagsFromMeta(meta) 用户原始描述: 一个持剑角色`
tags, assetType, userPrompt := parseTagsFromMeta(meta)
if len(tags) != 3 { if len(tags) != 3 {
t.Fatalf("expected 3 tags, got %d: %v", len(tags), tags) t.Fatalf("expected 3 tags, got %d: %v", len(tags), tags)
} }
@@ -262,26 +279,29 @@ func TestParseTagsFromMeta(t *testing.T) {
if assetType != "sprite" { if assetType != "sprite" {
t.Errorf("expected assetType=sprite, got %s", assetType) t.Errorf("expected assetType=sprite, got %s", assetType)
} }
if userPrompt != "一个持剑角色" {
t.Errorf("expected userPrompt='一个持剑角色', got %s", userPrompt)
}
} }
func TestParseTagsFromMeta_SingleTag(t *testing.T) { func TestParseTagsFromMeta_SingleTag(t *testing.T) {
meta := `用户选择标签: 赛博朋克 meta := `用户选择标签: 赛博朋克
素材类型: background` 素材类型: background`
tags, _ := parseTagsFromMeta(meta) tags, _, _ := parseTagsFromMeta(meta)
if len(tags) != 1 || tags[0] != "赛博朋克" { if len(tags) != 1 || tags[0] != "赛博朋克" {
t.Errorf("expected [赛博朋克], got %v", tags) t.Errorf("expected [赛博朋克], got %v", tags)
} }
} }
func TestParseTagsFromMeta_Empty(t *testing.T) { func TestParseTagsFromMeta_Empty(t *testing.T) {
tags, assetType := parseTagsFromMeta("no tags here") tags, assetType, _ := parseTagsFromMeta("no tags here")
if len(tags) != 0 || assetType != "" { if len(tags) != 0 || assetType != "" {
t.Errorf("expected empty, got tags=%v assetType=%s", tags, assetType) t.Errorf("expected empty, got tags=%v assetType=%s", tags, assetType)
} }
} }
func TestGenerateStructuredPrompt(t *testing.T) { func TestGenerateStructuredPrompt(t *testing.T) {
prompt := generateStructuredPrompt([]string{"像素", "战士"}, "sprite") prompt := generateStructuredPrompt([]string{"像素", "战士", "精灵表"}, "sprite", true)
if !strings.HasPrefix(prompt, "【主题】") { if !strings.HasPrefix(prompt, "【主题】") {
t.Error("prompt should start with 【主题】") t.Error("prompt should start with 【主题】")
} }
@@ -293,21 +313,36 @@ func TestGenerateStructuredPrompt(t *testing.T) {
} }
} }
func TestGenerateStructuredPrompt_Single(t *testing.T) {
prompt := generateStructuredPrompt([]string{"像素", "战士"}, "sprite", false)
if !strings.Contains(prompt, "独立PNG") {
t.Errorf("single sprite should contain 独立PNG: %s", prompt)
}
if strings.Contains(prompt, "spritesheet") {
t.Errorf("single sprite should not contain spritesheet: %s", prompt)
}
}
func TestBuildSubject(t *testing.T) { func TestBuildSubject(t *testing.T) {
tags := []string{"像素", "战士"} tags := []string{"像素", "战士"}
tests := []struct { tests := []struct {
assetType, want string assetType, want string
isSheet bool
}{ }{
{"sprite", "精灵图"}, {"sprite", "精灵图", false},
{"background", "场景背景"}, {"sprite", "精灵表", true},
{"ui", "UI元素"}, {"background", "场景背景", false},
{"animation", "动画帧序列"}, {"background", "瓦片集", true},
{"unknown", "游戏素材"}, {"ui", "UI元素", false},
{"ui", "瓦片集", true},
{"animation", "动画帧序列", false},
{"animation", "精灵表", true},
{"unknown", "游戏素材", false},
} }
for _, tt := range tests { for _, tt := range tests {
result := buildSubject(tags, tt.assetType) result := buildSubject(tags, tt.assetType, tt.isSheet)
if !strings.Contains(result, tt.want) { if !strings.Contains(result, tt.want) {
t.Errorf("buildSubject(%q) = %s, want containing %q", tt.assetType, result, tt.want) t.Errorf("buildSubject(%q, isSheet=%v) = %s, want containing %q", tt.assetType, tt.isSheet, result, tt.want)
} }
} }
} }
@@ -327,17 +362,48 @@ func TestBuildStyle(t *testing.T) {
func TestBuildTechNotes(t *testing.T) { func TestBuildTechNotes(t *testing.T) {
tests := []struct { tests := []struct {
assetType, want string assetType, want string
isSheet bool
}{ }{
{"sprite", "spritesheet"}, // 默认单人模式
{"background", "1920x1080"}, {"sprite", "纯白色背景", false},
{"ui", "九宫格"}, {"background", "1920x1080", false},
{"animation", "4方向x4帧"}, {"ui", "九宫格", false},
{"unknown", "PNG"}, {"animation", "4方向x4帧", false},
{"unknown", "PNG", false},
// 瓦片集/精灵表模式
{"sprite", "spritesheet", true},
{"background", "tileset", true},
{"ui", "瓦片集", true},
{"animation", "spritesheet", true},
} }
for _, tt := range tests { for _, tt := range tests {
result := buildTechNotes(tt.assetType) result := buildTechNotes(tt.assetType, tt.isSheet)
if !strings.Contains(result, tt.want) { if !strings.Contains(result, tt.want) {
t.Errorf("buildTechNotes(%q) = %s, want containing %q", tt.assetType, result, tt.want) t.Errorf("buildTechNotes(%q, isSheet=%v) = %s, want containing %q", tt.assetType, tt.isSheet, result, tt.want)
}
}
}
func TestIsSheetRequest(t *testing.T) {
tests := []struct {
tags []string
prompt string
want bool
}{
{[]string{"像素", "精灵表"}, "", true},
{[]string{"像素", "spritesheet"}, "", true},
{[]string{"地形", "瓦片集"}, "", true},
{[]string{"UI", "tileset"}, "", true},
{[]string{"场景", "tilemap"}, "", true},
{[]string{"像素", "战士"}, "", false},
{[]string{"像素"}, "生成一个精灵表", true},
{[]string{"森林"}, "场景瓦片集", true},
{nil, "", false},
}
for _, tt := range tests {
got := isSheetRequest(tt.tags, tt.prompt)
if got != tt.want {
t.Errorf("isSheetRequest(tags=%v, prompt=%q) = %v, want %v", tt.tags, tt.prompt, got, tt.want)
} }
} }
} }