From 5cc31fc7335ca80c2dbe791d2f02401638784466 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Wed, 10 Dec 2025 15:43:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(poster=5Fv2):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E6=B5=B7=E6=8A=A5=E5=BC=95=E6=93=8E=20V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 poster_smart_v2.py 引擎 - 双输出: preview_base64 (无底图预览) + fabric_json (前端编辑) - 5 种布局的 Fabric.js JSON 生成器 - 复用 AI 文案生成和布局渲染 - 测试脚本和输出样例 --- domain/aigc/engines/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 938 -> 1003 bytes .../poster_smart_v2.cpython-312.pyc | Bin 0 -> 25848 bytes domain/aigc/engines/poster_smart_v2.py | 811 ++++++++++++++++++ scripts/test_poster_smart_v2.py | 158 ++++ 5 files changed, 971 insertions(+) create mode 100644 domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc create mode 100644 domain/aigc/engines/poster_smart_v2.py create mode 100644 scripts/test_poster_smart_v2.py 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 9bb576e125747ac31fbf97cbaebf60739e9ab17d..f61af4ba019bbaaaf1c16b3519ce803e1ec62ba6 100644 GIT binary patch delta 175 zcmZ3*{+gZdG%qg~0}!aFSZ2DgOyrYb%$TSiDv`pN!jU7GD-@8ri!#zOoBVD0h6ApP-WMnzncg_xtbq<|_Ifw)+Iav8G*|6K-y PPb{3=OpWYC5O`gqU#K<-I9FrU)_vE)svWz^FxtWb8hcJh6NdV1Z1ma?y$y=B;cs{Xka5FWs I7l{Fd0G7fSKL7v# 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 0000000000000000000000000000000000000000..389c79f653a50047a76cb84b6906d1b625ca354c GIT binary patch literal 25848 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 G?EeQXJ{@rY literal 0 HcmV?d00001 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())