feat(poster_v2): 实现智能海报引擎 V2

- 新增 poster_smart_v2.py 引擎
- 双输出: preview_base64 (无底图预览) + fabric_json (前端编辑)
- 5 种布局的 Fabric.js JSON 生成器
- 复用 AI 文案生成和布局渲染
- 测试脚本和输出样例
This commit is contained in:
jinye_huang 2025-12-10 15:43:27 +08:00
parent fa6b5967f5
commit 5cc31fc733
5 changed files with 971 additions and 0 deletions

View File

@ -22,6 +22,7 @@ from .poster_generate_v3 import PosterGenerateEngineV3
# 智能海报引擎 (AI生成文案 + poster_v2) # 智能海报引擎 (AI生成文案 + poster_v2)
from .poster_smart_v1 import PosterSmartEngine from .poster_smart_v1 import PosterSmartEngine
from .poster_smart_v2 import PosterSmartEngineV2
__all__ = [ __all__ = [
'BaseAIGCEngine', 'BaseAIGCEngine',
@ -31,4 +32,5 @@ __all__ = [
'PosterGenerateEngine', 'PosterGenerateEngine',
'PosterGenerateEngineV3', 'PosterGenerateEngineV3',
'PosterSmartEngine', 'PosterSmartEngine',
'PosterSmartEngineV2',
] ]

View File

@ -0,0 +1,811 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
智能海报生成引擎 V2
输出:
1. preview_base64 - 无底图预览 PNG
2. fabric_json - Fabric.js 编辑用 JSON
"""
import json
import os
import base64
import io
from typing import Optional, Dict, Any, List
from .base import BaseAIGCEngine as BaseEngine, EngineResult
from poster_v2 import PosterFactory, PosterContent
from poster_v2.schemas.theme import THEMES, Theme
class PosterSmartEngineV2(BaseEngine):
"""智能海报生成引擎 V2 - 双输出 (预览PNG + Fabric JSON)"""
name = "poster_smart_v2"
description = "智能海报生成 (AI文案 + 预览PNG + Fabric JSON)"
version = "2.0.0"
# 画布尺寸
CANVAS_WIDTH = 1080
CANVAS_HEIGHT = 1440
# 类型到布局的映射
CATEGORY_LAYOUT_MAP = {
"景点": "hero_bottom",
"美食": "overlay_bottom",
"酒店": "card_float",
"民宿": "split_vertical",
"活动": "overlay_center",
"攻略": "hero_bottom",
}
# 类型到主题的映射
CATEGORY_THEME_MAP = {
"景点": "ocean",
"美食": "peach",
"酒店": "ocean",
"民宿": "latte",
"活动": "sunset",
"攻略": "mint",
}
def __init__(self):
super().__init__()
self._ai_agent = None
self._poster_factory = None
def get_param_schema(self) -> dict:
"""返回参数 schema"""
return {
"type": "object",
"properties": {
"category": {"type": "string", "description": "类型 (景点/美食/酒店/民宿/活动/攻略)"},
"name": {"type": "string", "description": "名称"},
"description": {"type": "string", "description": "描述"},
"price": {"type": "string", "description": "价格"},
"location": {"type": "string", "description": "地点"},
"features": {"type": "string", "description": "特色/卖点,逗号分隔"},
"image_url": {"type": "string", "description": "图片 URL"},
"override_layout": {"type": "string", "description": "强制布局"},
"override_theme": {"type": "string", "description": "强制主题"},
"skip_ai": {"type": "boolean", "description": "跳过 AI 生成"},
},
"required": ["category", "name"]
}
async def execute(self, params: dict) -> EngineResult:
"""执行引擎"""
try:
# 1. 提取参数
category = params.get("category", "景点")
name = params.get("name", "")
description = params.get("description", "")
price = params.get("price", "")
location = params.get("location", "")
features = params.get("features", "")
image_url = params.get("image_url", "")
# 覆盖布局/主题
override_layout = params.get("override_layout")
override_theme = params.get("override_theme")
skip_ai = params.get("skip_ai", False)
# 2. 生成文案 (AI 或 备用)
if skip_ai:
content = self._fallback_content(params)
else:
content = await self._generate_ai_content(params)
# 3. 确定布局和主题
layout = override_layout or content.get("suggested_layout") or self.CATEGORY_LAYOUT_MAP.get(category, "hero_bottom")
theme_name = override_theme or content.get("suggested_theme") or self.CATEGORY_THEME_MAP.get(category, "sunset")
# 验证布局和主题
valid_layouts = ["hero_bottom", "overlay_center", "overlay_bottom", "split_vertical", "card_float"]
if layout not in valid_layouts:
layout = "hero_bottom"
if theme_name not in THEMES:
theme_name = "sunset"
theme = THEMES[theme_name]
# 4. 生成预览 PNG (无底图)
preview_base64 = self._generate_preview(content, layout, theme)
# 5. 生成 Fabric JSON
fabric_json = self._generate_fabric_json(content, layout, theme, image_url)
return EngineResult(
success=True,
data={
"preview_base64": preview_base64,
"fabric_json": fabric_json,
"layout": layout,
"theme": theme_name,
"content": content
}
)
except Exception as e:
self.log(f"执行失败: {e}", level='error')
import traceback
traceback.print_exc()
return EngineResult(success=False, error=str(e))
async def _generate_ai_content(self, params: dict) -> dict:
"""AI 生成文案"""
try:
ai_agent = self._get_ai_agent()
if not ai_agent:
return self._fallback_content(params)
# 加载 prompt
from domain.prompt import PromptRegistry
registry = PromptRegistry("prompts")
prompt_config = registry.get("poster_copywriting")
if not prompt_config:
return self._fallback_content(params)
# 构建用户提示
category = params.get("category", "景点")
name = params.get("name", "")
description = params.get("description", "")
price = params.get("price", "")
location = params.get("location", "")
features = params.get("features", "")
target_audience = params.get("target_audience", "")
style_hint = params.get("style_hint", "")
user_prompt = prompt_config.user
user_prompt = user_prompt.replace("{category}", category)
user_prompt = user_prompt.replace("{name}", name)
user_prompt = user_prompt.replace("{description}", description or "无详细描述")
user_prompt = user_prompt.replace("{price}", price or "")
user_prompt = user_prompt.replace("{location}", location or "")
user_prompt = user_prompt.replace("{features}", features or "")
user_prompt = user_prompt.replace("{target_audience}", target_audience or "")
user_prompt = user_prompt.replace("{style_hint}", style_hint or "")
# 调用 AI
content_text, _, _, _ = await ai_agent.generate_text(
system_prompt=prompt_config.system,
user_prompt=user_prompt,
temperature=0.7,
use_stream=False,
)
# 提取 JSON
json_content = self._extract_json(content_text)
if json_content:
return json_content
else:
return self._fallback_content(params)
except Exception as e:
self.log(f"AI 生成失败: {e}", level='warning')
return self._fallback_content(params)
def _fallback_content(self, params: dict) -> dict:
"""备用文案生成"""
category = params.get("category", "景点")
name = params.get("name", "精选推荐")
description = params.get("description", "")
price = params.get("price", "")
features = params.get("features", "")
# 提取价格数字和后缀
price_display = ""
price_suffix = ""
if price:
import re
match = re.match(r'([¥¥]?\d+(?:\.\d+)?)(.*)', price.replace("", ""))
if match:
price_num = match.group(1)
if not price_num.startswith("¥"):
price_num = "¥" + price_num
price_display = price_num
suffix_part = match.group(2).strip()
if suffix_part:
price_suffix = "/" + suffix_part.lstrip("/")
else:
price_display = price
# 处理特色
features_list = []
if features:
features_list = [f.strip() for f in features.replace("", ",").split(",") if f.strip()]
return {
"title": name,
"subtitle": description[:30] if description else f"探索{category}的精彩",
"highlights": features_list[:4] if features_list else [],
"details": [],
"price": price_display,
"price_suffix": price_suffix,
"tags": [],
"suggested_layout": self.CATEGORY_LAYOUT_MAP.get(category, "hero_bottom"),
"suggested_theme": self.CATEGORY_THEME_MAP.get(category, "sunset"),
}
def _generate_preview(self, content: dict, layout: str, theme: Theme) -> str:
"""生成预览 PNG (无底图)"""
factory = self._get_poster_factory()
# 构建 PosterContent
poster_content = PosterContent(
title=content.get("title", ""),
subtitle=content.get("subtitle", ""),
price=content.get("price", ""),
price_suffix=content.get("price_suffix", ""),
highlights=content.get("highlights", []),
features=content.get("highlights", []), # 复用 highlights
details=content.get("details", []),
tags=content.get("tags", []),
label=content.get("label", ""),
image=None, # 无底图
)
# 生成海报
poster_image = factory.generate_from_content(poster_content, layout=layout, theme=theme.name)
# 转 Base64
buffer = io.BytesIO()
poster_image.convert("RGB").save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")
def _generate_fabric_json(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> dict:
"""生成 Fabric.js JSON"""
objects = []
# 通用配置
margin = 48
content_width = self.CANVAS_WIDTH - margin * 2
# 1. 背景图片占位
objects.append({
"id": "background_image",
"type": "image",
"src": image_url or "",
"left": 0,
"top": 0,
"width": self.CANVAS_WIDTH,
"height": self.CANVAS_HEIGHT,
"scaleX": 1,
"scaleY": 1,
"selectable": True,
"evented": True,
})
# 2. 根据布局生成不同的结构
if layout == "hero_bottom":
objects.extend(self._fabric_hero_bottom(content, theme, margin, content_width))
elif layout == "overlay_center":
objects.extend(self._fabric_overlay_center(content, theme, margin, content_width))
elif layout == "overlay_bottom":
objects.extend(self._fabric_overlay_bottom(content, theme, margin, content_width))
elif layout == "split_vertical":
objects.extend(self._fabric_split_vertical(content, theme, margin, content_width))
elif layout == "card_float":
objects.extend(self._fabric_card_float(content, theme, margin, content_width))
return {
"version": "5.3.0",
"canvas": {
"width": self.CANVAS_WIDTH,
"height": self.CANVAS_HEIGHT,
"backgroundColor": theme.secondary,
},
"layout": layout,
"theme": theme.name,
"objects": objects,
}
def _fabric_hero_bottom(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
"""hero_bottom 布局的 Fabric 对象"""
objects = []
# 渐变遮罩
objects.append({
"id": "gradient_overlay",
"type": "rect",
"left": 0,
"top": 700,
"width": self.CANVAS_WIDTH,
"height": 740,
"fill": {
"type": "linear",
"coords": {"x1": 0, "y1": 0, "x2": 0, "y2": 740},
"colorStops": [
{"offset": 0, "color": f"rgba({theme.primary_rgb[0]},{theme.primary_rgb[1]},{theme.primary_rgb[2]},0)"},
{"offset": 0.4, "color": f"rgba({theme.primary_rgb[0]},{theme.primary_rgb[1]},{theme.primary_rgb[2]},0.7)"},
{"offset": 1, "color": f"rgba({theme.primary_rgb[0]},{theme.primary_rgb[1]},{theme.primary_rgb[2]},0.95)"},
]
},
"selectable": False,
})
cur_y = 900
# 标题
objects.append({
"id": "title",
"type": "textbox",
"text": content.get("title", ""),
"left": margin,
"top": cur_y,
"width": content_width,
"fontSize": 80,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text,
"shadow": "rgba(0,0,0,0.3) 2px 2px 4px",
"selectable": True,
})
cur_y += 100
# 副标题
if content.get("subtitle"):
objects.append({
"id": "subtitle",
"type": "textbox",
"text": content.get("subtitle", ""),
"left": margin,
"top": cur_y,
"width": content_width,
"fontSize": 36,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba(255,255,255,0.85)",
"selectable": True,
})
cur_y += 60
# 价格
if content.get("price"):
objects.append({
"id": "price_bg",
"type": "rect",
"left": margin - 12,
"top": cur_y + 40,
"width": 200,
"height": 60,
"rx": 12,
"ry": 12,
"fill": f"rgba({theme.accent_rgb[0]},{theme.accent_rgb[1]},{theme.accent_rgb[2]},0.3)",
"selectable": False,
})
objects.append({
"id": "price",
"type": "text",
"text": content.get("price", ""),
"left": margin,
"top": cur_y + 48,
"fontSize": 48,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text,
"selectable": True,
})
if content.get("price_suffix"):
objects.append({
"id": "price_suffix",
"type": "text",
"text": content.get("price_suffix", ""),
"left": margin + 120,
"top": cur_y + 65,
"fontSize": 28,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba(255,255,255,0.8)",
"selectable": True,
})
return objects
def _fabric_overlay_center(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
"""overlay_center 布局的 Fabric 对象"""
objects = []
# 暗化遮罩
objects.append({
"id": "dark_overlay",
"type": "rect",
"left": 0,
"top": 0,
"width": self.CANVAS_WIDTH,
"height": self.CANVAS_HEIGHT,
"fill": "rgba(0,0,0,0.4)",
"selectable": False,
})
center_y = self.CANVAS_HEIGHT // 2 - 100
# 装饰线
objects.append({
"id": "deco_line_top",
"type": "rect",
"left": (self.CANVAS_WIDTH - 80) // 2,
"top": center_y - 20,
"width": 80,
"height": 4,
"fill": theme.accent,
"selectable": False,
})
# 标题
objects.append({
"id": "title",
"type": "textbox",
"text": content.get("title", ""),
"left": margin,
"top": center_y + 20,
"width": content_width,
"fontSize": 96,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text,
"textAlign": "center",
"shadow": "rgba(0,0,0,0.5) 3px 3px 6px",
"selectable": True,
})
# 副标题
if content.get("subtitle"):
objects.append({
"id": "subtitle",
"type": "textbox",
"text": content.get("subtitle", ""),
"left": margin,
"top": center_y + 140,
"width": content_width,
"fontSize": 36,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": "rgba(255,255,255,0.85)",
"textAlign": "center",
"selectable": True,
})
return objects
def _fabric_overlay_bottom(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
"""overlay_bottom 布局的 Fabric 对象"""
objects = []
# 底部毛玻璃区域
glass_y = 750
objects.append({
"id": "glass_bg",
"type": "rect",
"left": 0,
"top": glass_y,
"width": self.CANVAS_WIDTH,
"height": self.CANVAS_HEIGHT - glass_y,
"fill": "rgba(255,255,255,0.92)",
"selectable": False,
})
cur_y = glass_y + 40
# 标题
objects.append({
"id": "title",
"type": "textbox",
"text": content.get("title", ""),
"left": margin,
"top": cur_y,
"width": content_width,
"fontSize": 72,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text_dark,
"selectable": True,
})
cur_y += 90
# 副标题
if content.get("subtitle"):
objects.append({
"id": "subtitle",
"type": "textbox",
"text": content.get("subtitle", ""),
"left": margin,
"top": cur_y,
"width": content_width,
"fontSize": 32,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba({theme.text_dark_rgb[0]},{theme.text_dark_rgb[1]},{theme.text_dark_rgb[2]},0.7)",
"selectable": True,
})
cur_y += 60
# 亮点标签
if content.get("highlights"):
hl_x = margin
for i, hl in enumerate(content.get("highlights", [])[:4]):
objects.append({
"id": f"highlight_{i}",
"type": "textbox",
"text": hl,
"left": hl_x,
"top": cur_y,
"fontSize": 26,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": theme.accent,
"backgroundColor": f"rgba({theme.accent_rgb[0]},{theme.accent_rgb[1]},{theme.accent_rgb[2]},0.15)",
"padding": 12,
"selectable": True,
})
hl_x += 140
return objects
def _fabric_split_vertical(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
"""split_vertical 布局的 Fabric 对象"""
objects = []
split = self.CANVAS_WIDTH // 2
# 左侧渐变背景
objects.append({
"id": "left_gradient",
"type": "rect",
"left": 0,
"top": 0,
"width": split,
"height": self.CANVAS_HEIGHT,
"fill": {
"type": "linear",
"coords": {"x1": 0, "y1": 0, "x2": 0, "y2": self.CANVAS_HEIGHT},
"colorStops": [
{"offset": 0, "color": theme.gradient[0]},
{"offset": 1, "color": theme.gradient[1]},
]
},
"selectable": False,
})
# 右侧背景
objects.append({
"id": "right_bg",
"type": "rect",
"left": split,
"top": 0,
"width": split,
"height": self.CANVAS_HEIGHT,
"fill": theme.secondary,
"selectable": False,
})
content_x = split + margin
right_width = split - margin * 2
cur_y = 80
# 标题
objects.append({
"id": "title",
"type": "textbox",
"text": content.get("title", ""),
"left": content_x,
"top": cur_y,
"width": right_width,
"fontSize": 72,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text_dark,
"selectable": True,
})
cur_y += 120
# 装饰线
objects.append({
"id": "deco_line",
"type": "rect",
"left": content_x,
"top": cur_y,
"width": 50,
"height": 4,
"fill": theme.accent,
"selectable": False,
})
cur_y += 30
# 副标题
if content.get("subtitle"):
objects.append({
"id": "subtitle",
"type": "textbox",
"text": content.get("subtitle", ""),
"left": content_x,
"top": cur_y,
"width": right_width,
"fontSize": 32,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba({theme.text_dark_rgb[0]},{theme.text_dark_rgb[1]},{theme.text_dark_rgb[2]},0.7)",
"selectable": True,
})
cur_y += 80
# 特色列表
if content.get("highlights"):
for i, feature in enumerate(content.get("highlights", [])[:5]):
objects.append({
"id": f"feature_{i}",
"type": "text",
"text": f"· {feature}",
"left": content_x,
"top": cur_y,
"fontSize": 28,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba({theme.text_dark_rgb[0]},{theme.text_dark_rgb[1]},{theme.text_dark_rgb[2]},0.8)",
"selectable": True,
})
cur_y += 44
# 价格
if content.get("price"):
price_y = self.CANVAS_HEIGHT - 180
objects.append({
"id": "price",
"type": "text",
"text": content.get("price", ""),
"left": content_x,
"top": price_y,
"fontSize": 72,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.primary,
"selectable": True,
})
return objects
def _fabric_card_float(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
"""card_float 布局的 Fabric 对象"""
objects = []
card_margin = 40
card_y = 500
card_height = 800
# 悬浮卡片
objects.append({
"id": "card_bg",
"type": "rect",
"left": card_margin,
"top": card_y,
"width": self.CANVAS_WIDTH - card_margin * 2,
"height": card_height,
"rx": 28,
"ry": 28,
"fill": "rgba(255,255,255,0.97)",
"shadow": "rgba(0,0,0,0.15) 0px 8px 32px",
"selectable": False,
})
card_content_x = card_margin + 32
card_content_width = self.CANVAS_WIDTH - card_margin * 2 - 64
cur_y = card_y + 40
# 标签
if content.get("label"):
objects.append({
"id": "label",
"type": "textbox",
"text": content.get("label", ""),
"left": card_content_x,
"top": cur_y,
"fontSize": 24,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": theme.accent,
"backgroundColor": f"rgba({theme.accent_rgb[0]},{theme.accent_rgb[1]},{theme.accent_rgb[2]},0.15)",
"padding": 10,
"selectable": True,
})
cur_y += 50
# 标题
objects.append({
"id": "title",
"type": "textbox",
"text": content.get("title", ""),
"left": card_content_x,
"top": cur_y,
"width": card_content_width,
"fontSize": 72,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text_dark,
"selectable": True,
})
cur_y += 100
# 副标题
if content.get("subtitle"):
objects.append({
"id": "subtitle",
"type": "textbox",
"text": content.get("subtitle", ""),
"left": card_content_x,
"top": cur_y,
"width": card_content_width,
"fontSize": 30,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": f"rgba({theme.text_dark_rgb[0]},{theme.text_dark_rgb[1]},{theme.text_dark_rgb[2]},0.7)",
"selectable": True,
})
cur_y += 70
# 价格
if content.get("price"):
objects.append({
"id": "price",
"type": "text",
"text": content.get("price", ""),
"left": card_content_x,
"top": card_y + card_height - 100,
"fontSize": 64,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.primary,
"selectable": True,
})
return objects
def _extract_json(self, text: str) -> Optional[dict]:
"""从文本中提取 JSON"""
import re
# 尝试直接解析
try:
return json.loads(text)
except:
pass
# 尝试提取 ```json ... ``` 块
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
if json_match:
try:
return json.loads(json_match.group(1))
except:
pass
# 尝试提取 {...} 块
brace_match = re.search(r'\{[\s\S]*\}', text)
if brace_match:
try:
return json.loads(brace_match.group())
except:
pass
return None
def _get_ai_agent(self):
"""获取 AI Agent"""
if self._ai_agent is not None:
return self._ai_agent
try:
from core.ai.ai_agent import AIAgent
from core.config_loader import get_config
from core.config import AIModelConfig
config = get_config()
ai_config = AIModelConfig(
model="qwen-plus",
api_url=config.get("ai_model.api_url"),
api_key=config.get("ai_model.api_key") or os.environ.get("AI_API_KEY", ""),
temperature=0.7,
timeout=30000,
)
self._ai_agent = AIAgent(ai_config)
except Exception as e:
self.log(f"获取 AI Agent 失败: {e}", level='warning')
self._ai_agent = None
return self._ai_agent
def _get_poster_factory(self):
"""获取海报工厂"""
if self._poster_factory is None:
self._poster_factory = PosterFactory()
return self._poster_factory

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试智能海报生成引擎 V2
输出:
1. preview PNG - 无底图预览
2. fabric JSON - 前端编辑用
"""
import asyncio
import base64
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from domain.aigc.engines.poster_smart_v2 import PosterSmartEngineV2
OUTPUT_DIR = Path(__file__).parent.parent / "result" / "poster_smart_v2_test"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 所有布局
ALL_LAYOUTS = ["hero_bottom", "overlay_center", "overlay_bottom", "split_vertical", "card_float"]
ALL_THEMES = ["ocean", "sunset", "peach", "mint", "latte"]
async def test_all_layouts():
"""测试所有布局"""
print("=" * 60)
print("测试智能海报引擎 V2 - 双输出")
print("=" * 60)
engine = PosterSmartEngineV2()
# 每个布局使用不同的测试内容
layout_test_data = {
"hero_bottom": {
"category": "景点", "name": "西湖十景",
"description": "杭州最美的风景线,四季皆宜",
"price": "免费", "location": "杭州西湖",
"features": "湖光山色, 历史古迹, 文化底蕴, 四季美景",
"image_url": "https://example.com/xihu.jpg",
},
"overlay_center": {
"category": "活动", "name": "周末露营派对",
"description": "逃离城市,拥抱自然",
"price": "299元", "location": "从化流溪河",
"features": "篝火晚会, 星空观测",
"image_url": "https://example.com/camping.jpg",
},
"overlay_bottom": {
"category": "美食", "name": "探店网红甜品",
"description": "ins风下午茶打卡地",
"price": "人均68元", "location": "深圳万象城",
"features": "颜值超高, 味道在线, 出片率满分, 闺蜜必去",
"image_url": "https://example.com/dessert.jpg",
},
"split_vertical": {
"category": "民宿", "name": "山舍云端民宿",
"description": "藏在莫干山的治愈系民宿,推开窗便是云海与竹林",
"price": "458元/晚", "location": "莫干山",
"features": "独立庭院, 手冲咖啡, 山景露台, 有机早餐, 管家服务",
"image_url": "https://example.com/minsu.jpg",
},
"card_float": {
"category": "酒店", "name": "三亚亚特兰蒂斯",
"description": "住进海底世界的浪漫体验",
"price": "2888元/晚", "location": "三亚海棠湾",
"features": "水族馆景观, 无边泳池, 私人沙滩, 水上乐园",
"image_url": "https://example.com/hotel.jpg",
},
}
for i, layout in enumerate(ALL_LAYOUTS, 1):
theme = ALL_THEMES[i % len(ALL_THEMES)]
print(f"\n[{i}] 布局: {layout}, 主题: {theme}")
test_data = layout_test_data.get(layout, layout_test_data["hero_bottom"])
params = {**test_data, "override_layout": layout, "override_theme": theme, "skip_ai": True}
result = await engine.execute(params)
if result.success:
print(f" ✓ 成功!")
# 保存预览 PNG
preview_path = OUTPUT_DIR / f"{i:02d}_{layout}_preview.png"
with open(preview_path, 'wb') as f:
f.write(base64.b64decode(result.data.get('preview_base64')))
print(f" 预览 PNG: {preview_path.name}")
# 保存 Fabric JSON
json_path = OUTPUT_DIR / f"{i:02d}_{layout}_fabric.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(result.data.get('fabric_json'), f, ensure_ascii=False, indent=2)
print(f" Fabric JSON: {json_path.name}")
# 打印内容摘要
content = result.data.get('content', {})
print(f" 标题: {content.get('title', 'N/A')}")
print(f" 对象数: {len(result.data.get('fabric_json', {}).get('objects', []))}")
else:
print(f" ✗ 失败: {result.error}")
print("\n" + "=" * 60)
print(f"✓ 完成! 输出目录: {OUTPUT_DIR}")
print("=" * 60)
async def test_with_ai():
"""测试 AI 生成"""
print("\n" + "=" * 60)
print("测试 AI 文案生成")
print("=" * 60)
engine = PosterSmartEngineV2()
params = {
"category": "景点",
"name": "正佳极地海洋世界",
"description": "位于广州正佳广场的大型海洋馆,有企鹅、海豚表演",
"price": "199元/人",
"location": "广州天河",
"features": "企鹅馆, 海豚表演, 儿童乐园, 室内恒温",
"target_audience": "亲子家庭",
"image_url": "https://example.com/ocean.jpg",
}
result = await engine.execute(params)
if result.success:
print("✓ AI 生成成功!")
content = result.data.get('content', {})
print(f" 布局: {result.data.get('layout')}")
print(f" 主题: {result.data.get('theme')}")
print(f" 标题: {content.get('title')}")
print(f" 副标题: {content.get('subtitle')}")
print(f" 亮点: {content.get('highlights')}")
# 保存
preview_path = OUTPUT_DIR / "ai_preview.png"
with open(preview_path, 'wb') as f:
f.write(base64.b64decode(result.data.get('preview_base64')))
print(f" 预览: {preview_path.name}")
json_path = OUTPUT_DIR / "ai_fabric.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(result.data.get('fabric_json'), f, ensure_ascii=False, indent=2)
print(f" JSON: {json_path.name}")
else:
print(f"✗ 失败: {result.error}")
if __name__ == "__main__":
asyncio.run(test_all_layouts())
asyncio.run(test_with_ai())