diff --git a/domain/aigc/engines/__init__.py b/domain/aigc/engines/__init__.py index 8852d73..e794f31 100644 --- a/domain/aigc/engines/__init__.py +++ b/domain/aigc/engines/__init__.py @@ -22,6 +22,7 @@ from .poster_generate_v3 import PosterGenerateEngineV3 # 智能海报引擎 (AI生成文案 + poster_v2) from .poster_smart_v1 import PosterSmartEngine +from .poster_smart_v2 import PosterSmartEngineV2 __all__ = [ 'BaseAIGCEngine', @@ -31,4 +32,5 @@ __all__ = [ 'PosterGenerateEngine', 'PosterGenerateEngineV3', 'PosterSmartEngine', + 'PosterSmartEngineV2', ] diff --git a/domain/aigc/engines/__pycache__/__init__.cpython-312.pyc b/domain/aigc/engines/__pycache__/__init__.cpython-312.pyc index 9bb576e..f61af4b 100644 Binary files a/domain/aigc/engines/__pycache__/__init__.cpython-312.pyc and b/domain/aigc/engines/__pycache__/__init__.cpython-312.pyc differ diff --git a/domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc b/domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc new file mode 100644 index 0000000..389c79f Binary files /dev/null and b/domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc differ diff --git a/domain/aigc/engines/poster_smart_v2.py b/domain/aigc/engines/poster_smart_v2.py new file mode 100644 index 0000000..35ba517 --- /dev/null +++ b/domain/aigc/engines/poster_smart_v2.py @@ -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 diff --git a/scripts/test_poster_smart_v2.py b/scripts/test_poster_smart_v2.py new file mode 100644 index 0000000..926a790 --- /dev/null +++ b/scripts/test_poster_smart_v2.py @@ -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())