From 5637305a36214021437900f07bf7b4490bdcb4e4 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Wed, 16 Jul 2025 18:24:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=BB=BA=E4=BA=86poster=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/__pycache__/main.cpython-312.pyc | Bin 3233 -> 3384 bytes .../poster_config_manager.cpython-312.pyc | Bin 0 -> 10221 bytes api/config/poster_config_manager.py | 214 +++++++++ api/config/poster_prompts.yaml | 102 +++++ api/main.py | 3 +- api/models/__pycache__/tweet.cpython-312.pyc | Bin 9880 -> 9875 bytes .../vibrant_poster.cpython-312.pyc | Bin 0 -> 14646 bytes api/models/vibrant_poster.py | 331 ++++++++++++++ .../poster_unified.cpython-312.pyc | Bin 0 -> 12338 bytes api/routers/__pycache__/tweet.cpython-312.pyc | Bin 14645 -> 14657 bytes .../vibrant_poster.cpython-312.pyc | Bin 0 -> 8207 bytes api/routers/poster_unified.py | 314 ++++++++++++++ .../poster_service.cpython-312.pyc | Bin 0 -> 18539 bytes .../__pycache__/tweet.cpython-312.pyc | Bin 15676 -> 16419 bytes .../vibrant_poster.cpython-312.pyc | Bin 0 -> 23337 bytes api/services/poster_service.py | 409 ++++++++++++++++++ 16 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 api/config/__pycache__/poster_config_manager.cpython-312.pyc create mode 100644 api/config/poster_config_manager.py create mode 100644 api/config/poster_prompts.yaml create mode 100644 api/models/__pycache__/vibrant_poster.cpython-312.pyc create mode 100644 api/models/vibrant_poster.py create mode 100644 api/routers/__pycache__/poster_unified.cpython-312.pyc create mode 100644 api/routers/__pycache__/vibrant_poster.cpython-312.pyc create mode 100644 api/routers/poster_unified.py create mode 100644 api/services/__pycache__/poster_service.cpython-312.pyc create mode 100644 api/services/__pycache__/vibrant_poster.cpython-312.pyc create mode 100644 api/services/poster_service.py diff --git a/api/__pycache__/main.cpython-312.pyc b/api/__pycache__/main.cpython-312.pyc index 4a026e398c153e914092ed314c0f7580e987670b..a1541403d7cb68357927cbfc813e3e6e96bbe321 100644 GIT binary patch delta 772 zcmZ1|xkHNYG%qg~0}$|4lxL)HPUMqd?AWNT%BYYl6(z;UkSdU64w6L$slr(nlQS6A z8OwZZ6v+Uss1kq)rsOA==BDPA6v={w*`dNkazI{@{Nz@S%~EPW=4T*gDB=eaw^)ku z^Gk|=9%FEy9K@N#s5p5m=Y_~31&|zHW?phmX-aB*QGO}NoorB=IXxAk2!x4<_H_+Q3o3c%32tGDH64iQLX& ncGrb9E(&X0Fm$}&8F|4Y>H>q%6^7`^>$ue>f8bur0x}K&9MqVj delta 618 zcmdlXwNR4pG%qg~0}#mYm1X?kn8+u==(bT^l~IO~AypvD6eNQRQiZe3Cg(7!i(``$ z0m`kyAtwryd%>vAXgXPdNtDrSvKkY1GsJ=FQkc~1v6(NCWrpe+5HCeGMLwNzjo4~t zh#XFrOXAS2fT7!xfuSDVq7=o|j1aLDr51+OQ0XYyRNfTjYz+p6BIy*y6s8=xC^;sE zRJklWbp1ews-&o;iN41HeenC-wZb33zPqF z$(ez~Zn2k^WhUnr<=tW~D$Oeb=`B(N5n#O>Ho5sJr8%i~MYcdOP{0%)m>j^pfg^$O WIz#+LhWN=GJkFD|c}}u`^Z)>I8Epap diff --git a/api/config/__pycache__/poster_config_manager.cpython-312.pyc b/api/config/__pycache__/poster_config_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d5143581e6e9566503b482916f5a171ed7fcbac GIT binary patch literal 10221 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 F{{RXy+S~vD literal 0 HcmV?d00001 diff --git a/api/config/poster_config_manager.py b/api/config/poster_config_manager.py new file mode 100644 index 0000000..175221d --- /dev/null +++ b/api/config/poster_config_manager.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +海报配置管理器 +负责加载和管理海报相关的配置信息 +""" + +import os +import yaml +import json +import logging +from typing import Dict, Any, Optional, List +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class PosterConfigManager: + """海报配置管理器""" + + def __init__(self, config_file: Optional[str] = None): + """ + 初始化配置管理器 + + Args: + config_file: 配置文件路径,如果为空则使用默认路径 + """ + if config_file is None: + config_file = os.path.join(os.path.dirname(__file__), "poster_prompts.yaml") + + self.config_file = config_file + self.config = {} + self.load_config() + + def load_config(self): + """加载配置文件""" + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + self.config = yaml.safe_load(f) + logger.info(f"成功加载配置文件: {self.config_file}") + except FileNotFoundError: + logger.error(f"配置文件不存在: {self.config_file}") + self.config = self._get_default_config() + except yaml.YAMLError as e: + logger.error(f"解析配置文件失败: {e}") + self.config = self._get_default_config() + except Exception as e: + logger.error(f"加载配置文件时发生未知错误: {e}") + self.config = self._get_default_config() + + def _get_default_config(self) -> Dict[str, Any]: + """获取默认配置""" + return { + "poster_prompts": {}, + "templates": {}, + "defaults": { + "template": "vibrant", + "temperature": 0.7, + "output_dir": "result/posters", + "image_dir": "/root/TravelContentCreator/data/images", + "font_dir": "/root/TravelContentCreator/assets/font" + } + } + + def get_template_list(self) -> List[Dict[str, Any]]: + """获取所有可用的模板列表""" + templates = self.config.get("templates", {}) + return [ + { + "id": template_id, + "name": template_info.get("name", template_id), + "description": template_info.get("description", ""), + "size": template_info.get("size", [900, 1200]), + "required_fields": template_info.get("required_fields", []), + "optional_fields": template_info.get("optional_fields", []) + } + for template_id, template_info in templates.items() + ] + + def get_template_info(self, template_id: str) -> Optional[Dict[str, Any]]: + """获取指定模板的详细信息""" + return self.config.get("templates", {}).get(template_id) + + def get_prompt_config(self, template_id: str) -> Optional[Dict[str, Any]]: + """获取指定模板的提示词配置""" + template_info = self.get_template_info(template_id) + if not template_info: + return None + + prompt_key = template_info.get("prompt_key", template_id) + return self.config.get("poster_prompts", {}).get(prompt_key) + + def get_system_prompt(self, template_id: str) -> Optional[str]: + """获取系统提示词""" + prompt_config = self.get_prompt_config(template_id) + if prompt_config: + return prompt_config.get("system_prompt") + return None + + def get_user_prompt_template(self, template_id: str) -> Optional[str]: + """获取用户提示词模板""" + prompt_config = self.get_prompt_config(template_id) + if prompt_config: + return prompt_config.get("user_prompt_template") + return None + + def format_user_prompt(self, template_id: str, **kwargs) -> Optional[str]: + """ + 格式化用户提示词 + + Args: + template_id: 模板ID + **kwargs: 用于格式化的参数 + + Returns: + 格式化后的用户提示词 + """ + template = self.get_user_prompt_template(template_id) + if not template: + return None + + try: + # 确保参数中的字典类型转为JSON字符串 + formatted_kwargs = {} + for key, value in kwargs.items(): + if isinstance(value, (dict, list)): + formatted_kwargs[key] = json.dumps(value, ensure_ascii=False, indent=2) + else: + formatted_kwargs[key] = str(value) + + return template.format(**formatted_kwargs) + except KeyError as e: + logger.error(f"格式化提示词失败,缺少参数: {e}") + return None + except Exception as e: + logger.error(f"格式化提示词时发生错误: {e}") + return None + + def get_default_config(self, key: str) -> Any: + """获取默认配置值""" + return self.config.get("defaults", {}).get(key) + + def validate_template_content(self, template_id: str, content: Dict[str, Any]) -> tuple[bool, List[str]]: + """ + 验证模板内容是否符合要求 + + Args: + template_id: 模板ID + content: 要验证的内容 + + Returns: + (是否有效, 错误信息列表) + """ + template_info = self.get_template_info(template_id) + if not template_info: + return False, [f"未知的模板ID: {template_id}"] + + errors = [] + required_fields = template_info.get("required_fields", []) + + # 检查必填字段 + for field in required_fields: + if field not in content: + errors.append(f"缺少必填字段: {field}") + elif not content[field]: + errors.append(f"必填字段 {field} 不能为空") + + return len(errors) == 0, errors + + def get_template_class(self, template_id: str): + """ + 动态获取模板类 + + Args: + template_id: 模板ID + + Returns: + 模板类 + """ + template_mapping = { + "vibrant": "poster.templates.vibrant_template.VibrantTemplate", + "business": "poster.templates.business_template.BusinessTemplate" + } + + class_path = template_mapping.get(template_id) + if not class_path: + raise ValueError(f"未支持的模板类型: {template_id}") + + # 动态导入模板类 + module_path, class_name = class_path.rsplit(".", 1) + try: + module = __import__(module_path, fromlist=[class_name]) + return getattr(module, class_name) + except ImportError as e: + logger.error(f"导入模板类失败: {e}") + raise ValueError(f"无法加载模板类: {template_id}") + + def reload_config(self): + """重新加载配置""" + logger.info("正在重新加载配置...") + self.load_config() + + +# 全局配置管理器实例 +_config_manager = None + + +def get_poster_config_manager() -> PosterConfigManager: + """获取全局配置管理器实例""" + global _config_manager + if _config_manager is None: + _config_manager = PosterConfigManager() + return _config_manager \ No newline at end of file diff --git a/api/config/poster_prompts.yaml b/api/config/poster_prompts.yaml new file mode 100644 index 0000000..9f5bd54 --- /dev/null +++ b/api/config/poster_prompts.yaml @@ -0,0 +1,102 @@ +# 海报生成提示词配置文件 +poster_prompts: + vibrant: + system_prompt: | + 你是一名专业的海报设计师,专门设计宣传海报。你现在要根据用户提供的信息,生成适合Vibrant模板的海报内容。 + + ## Vibrant模板特点: + - 单图背景,毛玻璃渐变效果 + - 两栏布局(左栏内容,右栏价格) + - 适合展示套餐内容和价格信息 + + ## 你需要生成的数据结构包含以下字段: + + **必填字段:** + 1. `title`: 主标题(8-12字符,体现产品特色) + 2. `slogan`: 副标题/宣传语(10-20字符,吸引人的描述) + 3. `price`: 价格数字(纯数字,不含符号),如果材料中没有价格,则用"欢迎咨询"替代 + 4. `ticket_type`: 票种类型(如"成人票"、"套餐票"、"夜场票"等) + 5. `content_button`: 内容按钮文字(通常为"套餐内容"、"包含项目"等) + 6. `content_items`: 套餐内容列表(3-5个项目,每项5-15字符,不要只包含项目名称,要做合适的美化,可以适当省略) + + **可选字段:** + 7. `remarks`: 备注信息(1-3条,每条10-20字符) + 8. `tag`: 标签(1条, 如"#限时优惠"等) + 9. `pagination`: 分页信息(如"1/3",可为空) + + ## 内容创作要求: + 1. 套餐内容要具体实用:明确说明包含的服务、时间、数量 + 2. 价格要有吸引力:突出性价比和优惠信息 + + ## 输出格式: + 请严格按照JSON格式输出,不要有任何额外内容。 + + user_prompt_template: | + 请根据以下信息,生成适合在旅游海报上展示的文案: + + ## 景区信息 + {scenic_info} + + ## 产品信息 + {product_info} + + ## 推文信息 + {tweet_info} + + 请提取关键信息并整合成一个JSON对象,包含title、slogan、price、ticket_type、content_items、remarks和tag字段。 + + business: + system_prompt: | + 你是一名专业的商务海报设计师。你需要根据提供的信息生成适合Business模板的海报内容。 + + ## Business模板特点: + - 商务风格,简洁专业 + - 突出核心信息和价值主张 + - 适合企业服务推广 + + ## 生成字段结构: + 1. `title`: 服务标题(6-10字符) + 2. `subtitle`: 副标题(12-20字符) + 3. `features`: 核心特性列表(3-4个特性) + 4. `price`: 价格信息 + 5. `contact`: 联系方式信息 + + 请以JSON格式输出结果。 + + user_prompt_template: | + 请为以下商务信息生成海报内容: + + ## 服务信息 + {service_info} + + ## 目标客户 + {target_audience} + + ## 核心卖点 + {key_points} + +# 模板配置 +templates: + vibrant: + name: "Vibrant活力模板" + description: "适合旅游景点、娱乐活动的活力海报模板" + size: [900, 1200] + required_fields: ["title", "slogan", "price", "ticket_type", "content_items"] + optional_fields: ["remarks", "tag", "pagination"] + prompt_key: "vibrant" + + business: + name: "Business商务模板" + description: "适合企业服务、B2B推广的商务海报模板" + size: [1080, 1920] + required_fields: ["title", "subtitle", "features", "price"] + optional_fields: ["contact"] + prompt_key: "business" + +# 默认配置 +defaults: + template: "vibrant" + temperature: 0.7 + output_dir: "result/posters" + image_dir: "/root/TravelContentCreator/data/images" + font_dir: "/root/TravelContentCreator/assets/font" \ No newline at end of file diff --git a/api/main.py b/api/main.py index aba7b57..b000300 100644 --- a/api/main.py +++ b/api/main.py @@ -56,11 +56,12 @@ app.add_middleware( ) # 导入路由 -from api.routers import tweet, poster, prompt, document, data, integration, content_integration +from api.routers import tweet, poster, poster_unified, prompt, document, data, integration, content_integration # 包含路由 app.include_router(tweet.router, prefix="/api/v1/tweet", tags=["tweet"]) app.include_router(poster.router, prefix="/api/v1/poster", tags=["poster"]) +app.include_router(poster_unified.router, prefix="/api/v2/poster", tags=["poster-unified"]) app.include_router(prompt.router, prefix="/api/v1/prompt", tags=["prompt"]) app.include_router(document.router, prefix="/api/v1/document", tags=["document"]) app.include_router(data.router, prefix="/api/v1", tags=["data"]) diff --git a/api/models/__pycache__/tweet.cpython-312.pyc b/api/models/__pycache__/tweet.cpython-312.pyc index 45e950e372b9e52d4bcd36cb4a6ffc67717b854d..e19f132ae9217fb2c77ee7278747cdc5c38cd000 100644 GIT binary patch delta 271 zcmbQ?JK2}-G%qg~0}zOom1jIs-^iyeHTkWe)#OsC3XY;!pm<18!em`8Ij&3~dkGL1 zYfaXZ7N6`UtS~u4`Y1mSE7vCmAW@_alq*V~oWL!)IaEfFnJpK@_XKHW%?C2~P41VU z$Cx`=PeF+*2P6d|HcyUHxXPHb*+6j<6Qkzj_fn$5nk+?zAZcR|Q3N82!Df^KnO7#u zs%&S23Vz^{;3@>kgNU<}|ENr5ESx+^HA)3+I#@;xBn@_#29R}&!zMRBr8FniuBc+N XfLbDtFeBFl&o2x>>Wj|gS~Y0^^hrbF delta 335 zcmbR2JHwanG%qg~0}wPOmuGNmY~<6H;^F}Df#9>wWJMN<$^1g>Y$+UbSW?(0r%APn zhpc2NiUZ0Pg@cHA5D@_+CV!OD)YSztS{NSih~AL3y&PKNN3BC`^Jd+z$tR~k<&t&6e<@&_HyIEGon3*wea-h62YXQ)l{gW%@=K=W= z3QAnLAX`AhmdP#(S6M-<&>#T7%2N#h diff --git a/api/models/__pycache__/vibrant_poster.cpython-312.pyc b/api/models/__pycache__/vibrant_poster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a4d3ab210f3ff02690f48d5dfc7785b9b06d731 GIT binary patch literal 14646 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 bf!Mw*9dP2t8Tc7roC(+2j~VzO*7&~x7Be?D literal 0 HcmV?d00001 diff --git a/api/models/vibrant_poster.py b/api/models/vibrant_poster.py new file mode 100644 index 0000000..c1b6218 --- /dev/null +++ b/api/models/vibrant_poster.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +海报API通用模型定义 +支持多种模板类型的海报生成 +""" + +from typing import List, Dict, Any, Optional, Union +from pydantic import BaseModel, Field + + +# 基础模板信息 +class TemplateInfo(BaseModel): + """模板信息模型""" + id: str = Field(..., description="模板ID") + name: str = Field(..., description="模板名称") + description: str = Field(..., description="模板描述") + size: List[int] = Field(..., description="模板尺寸 [宽, 高]") + required_fields: List[str] = Field(..., description="必填字段列表") + optional_fields: List[str] = Field(default=[], description="可选字段列表") + + class Config: + schema_extra = { + "example": { + "id": "vibrant", + "name": "Vibrant活力模板", + "description": "适合旅游景点、娱乐活动的活力海报模板", + "size": [900, 1200], + "required_fields": ["title", "slogan", "price", "ticket_type", "content_items"], + "optional_fields": ["remarks", "tag", "pagination"] + } + } + + +# 通用内容模型(支持任意字段) +class PosterContent(BaseModel): + """通用海报内容模型,支持动态字段""" + + class Config: + extra = "allow" # 允许额外字段 + schema_extra = { + "example": { + "title": "海洋奇幻世界", + "slogan": "探索深海秘境,感受蓝色奇迹的无限魅力", + "price": "299", + "ticket_type": "成人票", + "content_items": [ + "海洋馆门票1张(含所有展区)", + "海豚表演VIP座位" + ] + } + } + + +# 保持向后兼容的Vibrant内容模型 +class VibrantPosterContent(PosterContent): + """Vibrant海报内容模型(向后兼容)""" + title: Optional[str] = Field(None, description="主标题(8-12字符)") + slogan: Optional[str] = Field(None, description="副标题/宣传语(10-20字符)") + price: Optional[str] = Field(None, description="价格,如果没有价格则用'欢迎咨询'") + ticket_type: Optional[str] = Field(None, description="票种类型(如成人票、套餐票等)") + content_button: Optional[str] = Field(None, description="内容按钮文字") + content_items: Optional[List[str]] = Field(None, description="套餐内容列表(3-5个项目)") + remarks: Optional[List[str]] = Field(None, description="备注信息(1-3条)") + tag: Optional[str] = Field(None, description="标签") + pagination: Optional[str] = Field(None, description="分页信息") + + +# 通用海报生成请求 +class PosterGenerationRequest(BaseModel): + """通用海报生成请求模型""" + # 模板相关 + template_id: str = Field(..., description="模板ID") + + # 内容相关(二选一) + content: Optional[Dict[str, Any]] = Field(None, description="直接提供的海报内容") + source_data: Optional[Dict[str, Any]] = Field(None, description="源数据,用于AI生成内容") + + # 生成参数 + topic_name: Optional[str] = Field(None, description="主题名称,用于文件命名") + image_path: Optional[str] = Field(None, description="指定图片路径,如果不提供则随机选择") + image_dir: Optional[str] = Field(None, description="图片目录,如果不提供使用默认目录") + output_dir: Optional[str] = Field(None, description="输出目录,如果不提供使用默认目录") + + # AI参数 + temperature: Optional[float] = Field(default=0.7, description="AI生成温度参数") + + class Config: + schema_extra = { + "example": { + "template_id": "vibrant", + "source_data": { + "scenic_info": { + "name": "天津冒险湾", + "location": "天津市", + "type": "水上乐园" + }, + "product_info": { + "name": "冒险湾门票", + "price": 299, + "type": "成人票" + }, + "tweet_info": { + "title": "夏日清凉首选", + "content": "天津冒险湾水上乐园,多种刺激项目等你来挑战..." + } + }, + "topic_name": "天津冒险湾", + "temperature": 0.7 + } + } + + +# 内容生成请求 +class ContentGenerationRequest(BaseModel): + """内容生成请求模型""" + template_id: str = Field(..., description="模板ID") + source_data: Dict[str, Any] = Field(..., description="源数据,用于AI生成内容") + temperature: Optional[float] = Field(default=0.7, description="AI生成温度参数") + + class Config: + schema_extra = { + "example": { + "template_id": "vibrant", + "source_data": { + "scenic_info": {"name": "天津冒险湾", "type": "水上乐园"}, + "product_info": {"name": "门票", "price": 299}, + "tweet_info": {"title": "夏日清凉", "content": "水上乐园体验"} + }, + "temperature": 0.7 + } + } + + +# 保持向后兼容 +class VibrantPosterRequest(PosterGenerationRequest): + """Vibrant海报生成请求模型(向后兼容)""" + template_id: str = Field(default="vibrant", description="模板ID,默认为vibrant") + + # 兼容旧的字段结构 + scenic_info: Optional[Dict[str, Any]] = Field(None, description="景区信息") + product_info: Optional[Dict[str, Any]] = Field(None, description="产品信息") + tweet_info: Optional[Dict[str, Any]] = Field(None, description="推文信息") + + def __init__(self, **data): + # 转换旧格式到新格式 + if 'scenic_info' in data or 'product_info' in data or 'tweet_info' in data: + source_data = {} + for key in ['scenic_info', 'product_info', 'tweet_info']: + if key in data and data[key] is not None: + source_data[key] = data.pop(key) + if source_data and 'source_data' not in data: + data['source_data'] = source_data + + super().__init__(**data) + + +# 标准API响应基础类 +class BaseAPIResponse(BaseModel): + """API响应基础模型""" + success: bool = Field(..., description="操作是否成功") + message: str = Field(default="", description="响应消息") + request_id: str = Field(..., description="请求ID") + timestamp: str = Field(..., description="响应时间戳") + + +# 通用海报生成响应 +class PosterGenerationResponse(BaseAPIResponse): + """通用海报生成响应模型""" + data: Optional[Dict[str, Any]] = Field(None, description="响应数据") + + class Config: + schema_extra = { + "example": { + "success": True, + "message": "海报生成成功", + "request_id": "poster-20240715-123456-a1b2c3d4", + "timestamp": "2024-07-15T12:34:56Z", + "data": { + "template_id": "vibrant", + "topic_name": "天津冒险湾", + "poster_path": "/result/posters/vibrant_海洋奇幻世界_20240715_123456.png", + "content": { + "title": "海洋奇幻世界", + "slogan": "探索深海秘境,感受蓝色奇迹的无限魅力", + "price": "299" + }, + "metadata": { + "image_used": "/data/images/ocean_park.jpg", + "generation_method": "ai_generated", + "template_size": [900, 1200], + "processing_time": 3.2 + } + } + } + } + + +# 内容生成响应 +class ContentGenerationResponse(BaseAPIResponse): + """内容生成响应模型""" + data: Optional[Dict[str, Any]] = Field(None, description="生成的内容") + + class Config: + schema_extra = { + "example": { + "success": True, + "message": "内容生成成功", + "request_id": "content-20240715-123456-a1b2c3d4", + "timestamp": "2024-07-15T12:34:56Z", + "data": { + "template_id": "vibrant", + "content": { + "title": "冒险湾水世界", + "slogan": "夏日激情体验,清凉无限乐趣", + "price": "299", + "ticket_type": "成人票" + }, + "metadata": { + "generation_method": "ai_generated", + "temperature": 0.7 + } + } + } + } + + +# 模板列表响应 +class TemplateListResponse(BaseAPIResponse): + """模板列表响应模型""" + data: Optional[List[TemplateInfo]] = Field(None, description="模板列表") + + class Config: + schema_extra = { + "example": { + "success": True, + "message": "获取模板列表成功", + "request_id": "templates-20240715-123456", + "timestamp": "2024-07-15T12:34:56Z", + "data": [ + { + "id": "vibrant", + "name": "Vibrant活力模板", + "description": "适合旅游景点、娱乐活动的活力海报模板", + "size": [900, 1200], + "required_fields": ["title", "slogan", "price"], + "optional_fields": ["remarks", "tag"] + } + ] + } + } + + +# 保持向后兼容的响应模型 +class VibrantPosterResponse(BaseModel): + """Vibrant海报生成响应模型(向后兼容)""" + request_id: str = Field(..., description="请求ID") + topic_name: str = Field(..., description="主题名称") + poster_path: str = Field(..., description="生成的海报文件路径") + generated_content: Dict[str, Any] = Field(..., description="生成或使用的海报内容") + image_used: str = Field(..., description="使用的图片路径") + generation_method: str = Field(..., description="生成方式:direct(直接使用提供内容)或ai_generated(AI生成)") + + class Config: + schema_extra = { + "example": { + "request_id": "vibrant-20240715-123456-a1b2c3d4", + "topic_name": "天津冒险湾", + "poster_path": "/result/posters/vibrant_海洋奇幻世界_20240715_123456.png", + "generated_content": { + "title": "海洋奇幻世界", + "slogan": "探索深海秘境,感受蓝色奇迹的无限魅力", + "price": "299", + "ticket_type": "成人票", + "content_items": ["海洋馆门票1张", "海豚表演VIP座位"] + }, + "image_used": "/data/images/ocean_park.jpg", + "generation_method": "ai_generated" + } + } + + +class BatchVibrantPosterRequest(BaseModel): + """批量Vibrant海报生成请求模型""" + base_path: str = Field(..., description="包含多个topic目录的基础路径") + image_dir: Optional[str] = Field(None, description="图片目录") + scenic_info_file: Optional[str] = Field(None, description="景区信息文件路径") + product_info_file: Optional[str] = Field(None, description="产品信息文件路径") + output_base: Optional[str] = Field(default="result/posters", description="输出基础目录") + parallel_count: Optional[int] = Field(default=3, description="并发处理数量") + temperature: Optional[float] = Field(default=0.7, description="AI生成温度参数") + + class Config: + schema_extra = { + "example": { + "base_path": "/root/TravelContentCreator/result/run_20250710_165327", + "image_dir": "/root/TravelContentCreator/data/images", + "scenic_info_file": "/root/TravelContentCreator/resource/data/Object/天津冒险湾.txt", + "product_info_file": "/root/TravelContentCreator/resource/data/Product/product.bak", + "output_base": "result/posters", + "parallel_count": 3, + "temperature": 0.7 + } + } + + +class BatchVibrantPosterResponse(BaseModel): + """批量Vibrant海报生成响应模型""" + request_id: str = Field(..., description="批量处理请求ID") + total_topics: int = Field(..., description="总共处理的topic数量") + successful_count: int = Field(..., description="成功生成的海报数量") + failed_count: int = Field(..., description="失败的海报数量") + output_base_dir: str = Field(..., description="输出基础目录") + successful_topics: List[str] = Field(..., description="成功处理的topic列表") + failed_topics: List[Dict[str, str]] = Field(..., description="失败的topic及错误信息") + + class Config: + schema_extra = { + "example": { + "request_id": "batch-vibrant-20240715-123456", + "total_topics": 5, + "successful_count": 4, + "failed_count": 1, + "output_base_dir": "/result/posters/run_20250710_165327", + "successful_topics": ["topic_1", "topic_2", "topic_3", "topic_4"], + "failed_topics": [ + {"topic": "topic_5", "error": "图片文件不存在"} + ] + } + } \ No newline at end of file diff --git a/api/routers/__pycache__/poster_unified.cpython-312.pyc b/api/routers/__pycache__/poster_unified.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3af06e76a40351838bbfccfa24ef944c4da9ee52 GIT binary patch literal 12338 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 diff --git a/api/routers/__pycache__/vibrant_poster.cpython-312.pyc b/api/routers/__pycache__/vibrant_poster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdab10c385b1740beb93dc7e21f02d535a427bc0 GIT binary patch literal 8207 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 XSQ#g6g-QDcpV;L22PVkzXe0j*lIwss literal 0 HcmV?d00001 diff --git a/api/routers/poster_unified.py b/api/routers/poster_unified.py new file mode 100644 index 0000000..86c87c4 --- /dev/null +++ b/api/routers/poster_unified.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +统一海报API路由 +支持多种模板类型的海报生成,配置化管理 +""" + +import logging +import uuid +from datetime import datetime, timezone +from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks +from typing import Dict, Any, List + +from core.ai import AIAgent +from api.services.poster_service import UnifiedPosterService +from api.models.vibrant_poster import ( + PosterGenerationRequest, PosterGenerationResponse, + ContentGenerationRequest, ContentGenerationResponse, + TemplateListResponse, TemplateInfo, + BaseAPIResponse +) + +# 从依赖注入模块导入依赖 +from api.dependencies import get_ai_agent + +logger = logging.getLogger(__name__) + +# 创建路由 +router = APIRouter() + +# 依赖注入函数 +def get_unified_poster_service( + ai_agent: AIAgent = Depends(get_ai_agent) +) -> UnifiedPosterService: + """获取统一海报服务""" + return UnifiedPosterService(ai_agent) + + +def create_response(success: bool, message: str, data: Any = None, request_id: str = None) -> Dict[str, Any]: + """创建标准响应""" + if request_id is None: + request_id = f"req-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" + + return { + "success": success, + "message": message, + "request_id": request_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "data": data + } + + +@router.get("/templates", response_model=TemplateListResponse, summary="获取所有可用模板") +async def get_templates( + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 获取所有可用的海报模板列表 + + 返回每个模板的详细信息,包括: + - 模板ID和名称 + - 模板描述 + - 模板尺寸 + - 必填字段和可选字段 + """ + try: + templates = service.get_available_templates() + response_data = create_response( + success=True, + message="获取模板列表成功", + data=templates + ) + return TemplateListResponse(**response_data) + + except Exception as e: + logger.error(f"获取模板列表失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取模板列表失败: {str(e)}") + + +@router.get("/templates/{template_id}", response_model=BaseAPIResponse, summary="获取指定模板信息") +async def get_template_info( + template_id: str, + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 获取指定模板的详细信息 + + 参数: + - **template_id**: 模板ID + """ + try: + template_info = service.get_template_info(template_id) + if not template_info: + raise HTTPException(status_code=404, detail=f"模板 {template_id} 不存在") + + response_data = create_response( + success=True, + message="获取模板信息成功", + data=template_info.dict() + ) + return BaseAPIResponse(**response_data) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取模板信息失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取模板信息失败: {str(e)}") + + +@router.post("/content/generate", response_model=ContentGenerationResponse, summary="生成海报内容") +async def generate_content( + request: ContentGenerationRequest, + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 根据源数据生成海报内容,不生成实际图片 + + 用于: + 1. 预览生成的内容 + 2. 调试和测试内容生成 + 3. 分步骤生成(先生成内容,再生成图片) + + 参数: + - **template_id**: 模板ID + - **source_data**: 源数据,用于AI生成内容 + - **temperature**: AI生成温度参数 + """ + try: + content = await service.generate_content( + template_id=request.template_id, + source_data=request.source_data, + temperature=request.temperature + ) + + response_data = create_response( + success=True, + message="内容生成成功", + data={ + "template_id": request.template_id, + "content": content, + "metadata": { + "generation_method": "ai_generated", + "temperature": request.temperature + } + } + ) + return ContentGenerationResponse(**response_data) + + except Exception as e: + logger.error(f"生成内容失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"生成内容失败: {str(e)}") + + +@router.post("/generate", response_model=PosterGenerationResponse, summary="生成海报") +async def generate_poster( + request: PosterGenerationRequest, + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 生成海报图片 + + 支持两种模式: + 1. 直接提供内容(content字段) + 2. 提供源数据让AI生成内容(source_data字段) + + 参数: + - **template_id**: 模板ID + - **content**: 直接提供的海报内容(可选) + - **source_data**: 源数据,用于AI生成内容(可选) + - **topic_name**: 主题名称,用于文件命名 + - **image_path**: 指定图片路径(可选) + - **image_dir**: 图片目录(可选) + - **output_dir**: 输出目录(可选) + - **temperature**: AI生成温度参数 + """ + try: + result = await service.generate_poster( + template_id=request.template_id, + content=request.content, + source_data=request.source_data, + topic_name=request.topic_name, + image_path=request.image_path, + image_dir=request.image_dir, + output_dir=request.output_dir, + temperature=request.temperature + ) + + response_data = create_response( + success=True, + message="海报生成成功", + data=result, + request_id=result["request_id"] + ) + return PosterGenerationResponse(**response_data) + + except Exception as e: + logger.error(f"生成海报失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"生成海报失败: {str(e)}") + + +@router.post("/batch", response_model=BaseAPIResponse, summary="批量生成海报") +async def batch_generate_posters( + template_id: str, + base_path: str, + image_dir: str = None, + source_files: Dict[str, str] = None, + output_base: str = "result/posters", + parallel_count: int = 3, + temperature: float = 0.7, + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 批量生成海报 + + 自动扫描指定目录下的topic文件夹,为每个topic生成海报。 + + 参数: + - **template_id**: 模板ID + - **base_path**: 包含多个topic目录的基础路径 + - **image_dir**: 图片目录(可选) + - **source_files**: 源文件配置字典(可选) + - **output_base**: 输出基础目录 + - **parallel_count**: 并发处理数量 + - **temperature**: AI生成温度参数 + """ + try: + result = await service.batch_generate_posters( + template_id=template_id, + base_path=base_path, + image_dir=image_dir, + source_files=source_files or {}, + output_base=output_base, + parallel_count=parallel_count, + temperature=temperature + ) + + response_data = create_response( + success=True, + message=f"批量生成完成,成功: {result['successful_count']}, 失败: {result['failed_count']}", + data=result, + request_id=result["request_id"] + ) + return BaseAPIResponse(**response_data) + + except Exception as e: + logger.error(f"批量生成海报失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"批量生成海报失败: {str(e)}") + + +@router.post("/config/reload", response_model=BaseAPIResponse, summary="重新加载配置") +async def reload_config( + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """ + 重新加载海报配置 + + 用于在不重启服务的情况下更新配置,包括: + - 提示词模板 + - 模板配置 + - 默认参数 + """ + try: + service.reload_config() + response_data = create_response( + success=True, + message="配置重新加载成功" + ) + return BaseAPIResponse(**response_data) + + except Exception as e: + logger.error(f"重新加载配置失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"重新加载配置失败: {str(e)}") + + +@router.get("/health", summary="健康检查") +async def health_check(): + """服务健康检查""" + return create_response( + success=True, + message="统一海报服务运行正常", + data={ + "service": "unified_poster", + "status": "healthy", + "version": "2.0.0" + } + ) + + +@router.get("/config", summary="获取服务配置") +async def get_service_config( + service: UnifiedPosterService = Depends(get_unified_poster_service) +): + """获取服务配置信息""" + try: + config_info = { + "default_image_dir": service.config_manager.get_default_config("image_dir"), + "default_output_dir": service.config_manager.get_default_config("output_dir"), + "default_font_dir": service.config_manager.get_default_config("font_dir"), + "default_template": service.config_manager.get_default_config("template"), + "supported_image_formats": ["png", "jpg", "jpeg", "webp"], + "available_templates": len(service.get_available_templates()) + } + + response_data = create_response( + success=True, + message="获取配置成功", + data=config_info + ) + return BaseAPIResponse(**response_data) + + except Exception as e: + logger.error(f"获取配置失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取配置失败: {str(e)}") \ No newline at end of file diff --git a/api/services/__pycache__/poster_service.cpython-312.pyc b/api/services/__pycache__/poster_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43ad6735a8c7f5f3e75872bba23aaa6f12a0f59c GIT binary patch literal 18539 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 diff --git a/api/services/__pycache__/vibrant_poster.cpython-312.pyc b/api/services/__pycache__/vibrant_poster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e191c0ef2212e546cea91b196afdf803c72afff GIT binary patch literal 23337 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 literal 0 HcmV?d00001 diff --git a/api/services/poster_service.py b/api/services/poster_service.py new file mode 100644 index 0000000..026c13c --- /dev/null +++ b/api/services/poster_service.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +通用海报服务层 +支持多种模板类型的海报生成,配置化管理 +""" + +import os +import sys +import json +import random +import asyncio +import logging +import uuid +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import List, Dict, Any, Optional, Tuple + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from core.ai.ai_agent import AIAgent +from api.config.poster_config_manager import get_poster_config_manager +from api.models.vibrant_poster import TemplateInfo + +logger = logging.getLogger(__name__) + + +class UnifiedPosterService: + """统一海报服务类""" + + def __init__(self, ai_agent: AIAgent): + """ + 初始化统一海报服务 + + Args: + ai_agent: AI代理 + """ + self.ai_agent = ai_agent + self.config_manager = get_poster_config_manager() + + def get_available_templates(self) -> List[TemplateInfo]: + """获取所有可用的模板列表""" + template_list = self.config_manager.get_template_list() + return [TemplateInfo(**template) for template in template_list] + + def get_template_info(self, template_id: str) -> Optional[TemplateInfo]: + """获取指定模板的信息""" + template_info = self.config_manager.get_template_info(template_id) + if template_info: + return TemplateInfo( + id=template_id, + name=template_info.get("name", template_id), + description=template_info.get("description", ""), + size=template_info.get("size", [900, 1200]), + required_fields=template_info.get("required_fields", []), + optional_fields=template_info.get("optional_fields", []) + ) + return None + + async def generate_content(self, template_id: str, source_data: Dict[str, Any], + temperature: float = 0.7) -> Dict[str, Any]: + """ + 生成海报内容 + + Args: + template_id: 模板ID + source_data: 源数据 + temperature: AI生成温度参数 + + Returns: + 生成的内容字典 + """ + logger.info(f"正在为模板 {template_id} 生成内容...") + + # 获取系统提示词 + system_prompt = self.config_manager.get_system_prompt(template_id) + if not system_prompt: + raise ValueError(f"模板 {template_id} 没有配置系统提示词") + + # 格式化用户提示词 + user_prompt = self.config_manager.format_user_prompt(template_id, **source_data) + if not user_prompt: + raise ValueError(f"模板 {template_id} 用户提示词格式化失败") + + try: + # 调用AI生成内容 + response, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + temperature=temperature, + stage=f"海报内容生成-{template_id}" + ) + + # 解析JSON响应 + json_start = response.find('{') + json_end = response.rfind('}') + 1 + + if json_start >= 0 and json_end > json_start: + json_str = response[json_start:json_end] + content_dict = json.loads(json_str) + logger.info(f"AI成功生成内容: {content_dict}") + + # 确保所有值都是字符串类型(除了列表) + for key, value in content_dict.items(): + if isinstance(value, (int, float)): + content_dict[key] = str(value) + elif isinstance(value, list): + content_dict[key] = [str(item) if isinstance(item, (int, float)) else item for item in value] + + return content_dict + else: + logger.error(f"无法在AI响应中找到JSON对象: {response}") + raise ValueError("AI响应格式不正确") + + except json.JSONDecodeError as e: + logger.error(f"无法解析AI响应为JSON: {e}") + raise ValueError("AI响应JSON解析失败") + except Exception as e: + logger.error(f"调用AI生成内容时发生错误: {e}") + raise + + def select_random_image(self, image_dir: Optional[str] = None) -> str: + """从指定目录随机选择一张图片""" + if image_dir is None: + image_dir = self.config_manager.get_default_config("image_dir") + + try: + image_files = [f for f in os.listdir(image_dir) + if f.lower().endswith(('png', 'jpg', 'jpeg', 'webp'))] + if not image_files: + raise ValueError(f"在目录 {image_dir} 中未找到任何图片文件") + + random_image_name = random.choice(image_files) + image_path = os.path.join(image_dir, random_image_name) + logger.info(f"随机选择图片: {image_path}") + return image_path + except FileNotFoundError: + raise ValueError(f"图片目录不存在: {image_dir}") + + def validate_content(self, template_id: str, content: Dict[str, Any]) -> None: + """验证内容是否符合模板要求""" + is_valid, errors = self.config_manager.validate_template_content(template_id, content) + if not is_valid: + raise ValueError(f"内容验证失败: {', '.join(errors)}") + + async def generate_poster(self, + template_id: str, + content: Optional[Dict[str, Any]] = None, + source_data: Optional[Dict[str, Any]] = None, + topic_name: Optional[str] = None, + image_path: Optional[str] = None, + image_dir: Optional[str] = None, + output_dir: Optional[str] = None, + temperature: float = 0.7) -> Dict[str, Any]: + """ + 生成海报 + + Args: + template_id: 模板ID + content: 直接提供的内容(可选) + source_data: 源数据,用于AI生成内容(可选) + topic_name: 主题名称 + image_path: 指定图片路径 + image_dir: 图片目录 + output_dir: 输出目录 + temperature: AI生成温度参数 + + Returns: + 生成结果字典 + """ + start_time = time.time() + + logger.info(f"开始生成海报,模板: {template_id}, 主题: {topic_name}") + + # 生成请求ID + request_id = f"poster-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" + + # 获取模板信息 + template_info = self.get_template_info(template_id) + if not template_info: + raise ValueError(f"未知的模板ID: {template_id}") + + # 确定内容 + if content is None: + if source_data is None: + raise ValueError("必须提供content或source_data中的一个") + + # 使用AI生成内容 + content = await self.generate_content(template_id, source_data, temperature) + generation_method = "ai_generated" + else: + generation_method = "direct" + + # 验证内容 + self.validate_content(template_id, content) + + # 选择图片 + if image_path is None: + image_path = self.select_random_image(image_dir) + + if not os.path.exists(image_path): + raise ValueError(f"指定的图片文件不存在: {image_path}") + + # 设置默认值 + if output_dir is None: + output_dir = self.config_manager.get_default_config("output_dir") + if topic_name is None: + topic_name = f"poster_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + # 获取模板类并生成海报 + try: + template_class = self.config_manager.get_template_class(template_id) + template_instance = template_class(template_info.size) + + # 设置字体(如果支持) + font_dir = self.config_manager.get_default_config("font_dir") + if hasattr(template_instance, 'set_font_dir') and font_dir: + template_instance.set_font_dir(font_dir) + + poster = template_instance.generate(image_path=image_path, content=content) + + if not poster: + raise ValueError("海报生成失败,模板返回了 None") + + except Exception as e: + logger.error(f"生成海报时发生错误: {e}", exc_info=True) + raise ValueError(f"海报生成失败: {str(e)}") + + # 保存海报 + try: + os.makedirs(output_dir, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # 生成文件名 + title = content.get('title', topic_name) + if isinstance(title, str): + title = title.replace('/', '_').replace('\\', '_') + output_filename = f"{template_id}_{title}_{timestamp}.png" + poster_path = os.path.join(output_dir, output_filename) + + poster.save(poster_path, 'PNG') + logger.info(f"海报已成功生成并保存至: {poster_path}") + + processing_time = round(time.time() - start_time, 2) + + return { + "request_id": request_id, + "template_id": template_id, + "topic_name": topic_name, + "poster_path": poster_path, + "content": content, + "metadata": { + "image_used": image_path, + "generation_method": generation_method, + "template_size": template_info.size, + "processing_time": processing_time, + "timestamp": datetime.now(timezone.utc).isoformat() + } + } + + except Exception as e: + logger.error(f"保存海报失败: {e}", exc_info=True) + raise ValueError(f"保存海报失败: {str(e)}") + + async def batch_generate_posters(self, + template_id: str, + base_path: str, + image_dir: Optional[str] = None, + source_files: Optional[Dict[str, str]] = None, + output_base: str = "result/posters", + parallel_count: int = 3, + temperature: float = 0.7) -> Dict[str, Any]: + """ + 批量生成海报 + + Args: + template_id: 模板ID + base_path: 包含多个topic目录的基础路径 + image_dir: 图片目录 + source_files: 源文件配置字典 + output_base: 输出基础目录 + parallel_count: 并发数量 + temperature: AI生成温度参数 + + Returns: + 批量处理结果 + """ + logger.info(f"开始批量生成海报,模板: {template_id}, 基础路径: {base_path}") + + # 生成批处理ID + batch_request_id = f"batch-{template_id}-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + + # 查找topic目录 + topic_dirs = self._find_topic_directories(base_path) + + if not topic_dirs: + raise ValueError("未找到任何包含article_judged.json的topic目录") + + logger.info(f"找到 {len(topic_dirs)} 个topic目录,准备批量生成海报") + + # 准备输出目录 + base_name = Path(base_path).name + output_base_dir = os.path.join(output_base, base_name) + + # 这里简化实现,实际项目中可以使用asyncio.gather进行真正的异步批处理 + results = [] + successful_count = 0 + failed_count = 0 + + for topic_path, topic_name in topic_dirs: + try: + article_path = os.path.join(topic_path, 'article_judged.json') + topic_output_dir = os.path.join(output_base_dir, topic_name) + + # 读取文章数据 + source_data = self._read_data_file(article_path) + if not source_data: + raise ValueError(f"无法读取文章文件: {article_path}") + + # 构建源数据 + final_source_data = {"tweet_info": source_data} + + # 如果提供了额外的源文件,读取并添加 + if source_files: + for key, file_path in source_files.items(): + if file_path and os.path.exists(file_path): + data = self._read_data_file(file_path) + if data: + final_source_data[key] = data + + # 生成海报 + result = await self.generate_poster( + template_id=template_id, + source_data=final_source_data, + topic_name=topic_name, + image_dir=image_dir, + output_dir=topic_output_dir, + temperature=temperature + ) + + results.append({ + "topic": topic_name, + "success": True, + "result": result + }) + successful_count += 1 + logger.info(f"成功生成海报: {topic_name}") + + except Exception as e: + error_msg = str(e) + results.append({ + "topic": topic_name, + "success": False, + "error": error_msg + }) + failed_count += 1 + logger.error(f"生成海报失败 {topic_name}: {error_msg}") + + successful_topics = [r["topic"] for r in results if r["success"]] + failed_topics = [{"topic": r["topic"], "error": r["error"]} for r in results if not r["success"]] + + return { + "request_id": batch_request_id, + "template_id": template_id, + "total_topics": len(topic_dirs), + "successful_count": successful_count, + "failed_count": failed_count, + "output_base_dir": output_base_dir, + "successful_topics": successful_topics, + "failed_topics": failed_topics, + "detailed_results": results + } + + def _find_topic_directories(self, base_path: str) -> List[Tuple[str, str]]: + """查找topic目录""" + topic_dirs = [] + base_path = Path(base_path) + + if not base_path.exists(): + return topic_dirs + + for item in base_path.iterdir(): + if item.is_dir() and item.name.startswith('topic_'): + article_path = item / 'article_judged.json' + if article_path.exists(): + topic_dirs.append((str(item), item.name)) + + return topic_dirs + + def _read_data_file(self, file_path: str) -> Optional[Dict[str, Any]]: + """读取数据文件(简化版)""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + try: + return json.loads(content) + except json.JSONDecodeError: + # 简单的文本内容处理 + return {"content": content} + except Exception as e: + logger.error(f"读取文件失败 {file_path}: {e}") + return None + + def reload_config(self): + """重新加载配置""" + self.config_manager.reload_config() \ No newline at end of file