diff --git a/api/models/__pycache__/poster.cpython-312.pyc b/api/models/__pycache__/poster.cpython-312.pyc index a1f538f..baa0d02 100644 Binary files a/api/models/__pycache__/poster.cpython-312.pyc and b/api/models/__pycache__/poster.cpython-312.pyc differ diff --git a/api/models/__pycache__/tweet.cpython-312.pyc b/api/models/__pycache__/tweet.cpython-312.pyc index c556382..c84d72c 100644 Binary files a/api/models/__pycache__/tweet.cpython-312.pyc and b/api/models/__pycache__/tweet.cpython-312.pyc differ diff --git a/api/models/poster.py b/api/models/poster.py index c955e02..187643c 100644 --- a/api/models/poster.py +++ b/api/models/poster.py @@ -14,7 +14,7 @@ from pydantic import BaseModel, Field class PosterGenerateRequest(BaseModel): """海报生成请求模型""" templateId: str = Field("vibrant", description="模板ID") - imagesBase64: Optional[str] = Field(None, description="图像base64编码") + imagesBase64: Optional[List[str]] = Field(None, description="图像base64编码列表") posterContent: Optional[Dict[str, Any]] = Field(None, description="海报内容,如果提供则直接使用此内容") contentId: Optional[str] = Field(None, description="内容ID,用于AI生成内容") productId: Optional[str] = Field(None, description="产品ID,用于AI生成内容") @@ -28,7 +28,7 @@ class PosterGenerateRequest(BaseModel): json_schema_extra = { "example": { "templateId": "vibrant", - "imagesBase64": "", + "imagesBase64": ["base64_encoded_image_1", "base64_encoded_image_2"], "numVariations": 1, "forceLlmGeneration":False, "generatePsd": True, diff --git a/api/models/tweet.py b/api/models/tweet.py index 2e8d6a9..2772b2d 100644 --- a/api/models/tweet.py +++ b/api/models/tweet.py @@ -69,7 +69,7 @@ class ContentRequest(BaseModel): audienceIds: Optional[List[Union[int, str]]] = Field(None, description="受众ID列表") scenicSpotIds: Optional[List[Union[int, str]]] = Field(None, description="景区ID列表") productIds: Optional[List[Union[int, str]]] = Field(None, description="产品ID列表") - autoJudge: bool = Field(False, description="是否自动进行内容审核") + autoJudge: Optional[bool] = Field(True, description="是否自动进行内容审核") class Config: schema_extra = { @@ -185,7 +185,7 @@ class PipelineRequest(BaseModel): scenicSpotIds: Optional[List[Union[int, str]]] = Field(None, description="景区ID列表") productIds: Optional[List[Union[int, str]]] = Field(None, description="产品ID列表") skipJudge: bool = Field(False, description="是否跳过内容审核步骤") - autoJudge: bool = Field(False, description="是否在内容生成时进行内嵌审核") + autoJudge: Optional[bool] = Field(None, description="是否在内容生成时进行内嵌审核") class Config: schema_extra = { diff --git a/api/routers/__pycache__/tweet.cpython-312.pyc b/api/routers/__pycache__/tweet.cpython-312.pyc index 7b00885..774d297 100644 Binary files a/api/routers/__pycache__/tweet.cpython-312.pyc and b/api/routers/__pycache__/tweet.cpython-312.pyc differ diff --git a/api/services/__pycache__/database_service.cpython-312.pyc b/api/services/__pycache__/database_service.cpython-312.pyc index 34aac4f..f930bdf 100644 Binary files a/api/services/__pycache__/database_service.cpython-312.pyc and b/api/services/__pycache__/database_service.cpython-312.pyc differ diff --git a/api/services/__pycache__/poster.cpython-312.pyc b/api/services/__pycache__/poster.cpython-312.pyc index d966d66..24893a7 100644 Binary files a/api/services/__pycache__/poster.cpython-312.pyc and b/api/services/__pycache__/poster.cpython-312.pyc differ diff --git a/api/services/__pycache__/tweet.cpython-312.pyc b/api/services/__pycache__/tweet.cpython-312.pyc index 9cdbc52..4eb6165 100644 Binary files a/api/services/__pycache__/tweet.cpython-312.pyc and b/api/services/__pycache__/tweet.cpython-312.pyc differ diff --git a/api/services/poster.py b/api/services/poster.py index e77cce2..ce34516 100644 --- a/api/services/poster.py +++ b/api/services/poster.py @@ -234,26 +234,37 @@ class PosterService: # 获取模板的默认尺寸,如果获取不到则使用标准尺寸 template_size = getattr(template_handler, 'size', (900, 1200)) - if images_base64 and images_base64.strip(): + if images_base64 and len(images_base64) > 0: try: + logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据") + + # 处理第一张图片(目前模板只支持单张图片) + # 未来可以扩展为处理多张图片 + first_image_base64 = images_base64[0] if len(images_base64) > 0 else "" + + if not first_image_base64 or not first_image_base64.strip(): + raise ValueError("第一张图片的base64数据为空") + + logger.info(f"📊 处理第一张图片,base64长度: {len(first_image_base64)}") + # 移除可能存在的MIME类型前缀 - if images_base64.startswith("data:"): - images_base64 = images_base64.split(",", 1)[1] + if first_image_base64.startswith("data:"): + first_image_base64 = first_image_base64.split(",", 1)[1] # 彻底清理base64字符串 - 移除所有空白字符 - images_base64 = ''.join(images_base64.split()) + first_image_base64 = ''.join(first_image_base64.split()) # 验证base64字符串长度(应该是4的倍数) - if len(images_base64) % 4 != 0: + if len(first_image_base64) % 4 != 0: # 添加必要的填充 - images_base64 += '=' * (4 - len(images_base64) % 4) - logger.info(f"为base64字符串添加了填充,最终长度: {len(images_base64)}") + first_image_base64 += '=' * (4 - len(first_image_base64) % 4) + logger.info(f"为base64字符串添加了填充,最终长度: {len(first_image_base64)}") - logger.info(f"准备解码base64数据,长度: {len(images_base64)}, 前20字符: {images_base64[:20]}...") + logger.info(f"准备解码base64数据,长度: {len(first_image_base64)}, 前20字符: {first_image_base64[:20]}...") # 解码base64 - image_bytes = base64.b64decode(images_base64) - logger.info(f"base64解码成功,图片数据大小: {len(image_bytes)} bytes") + image_bytes = base64.b64decode(first_image_base64) + logger.info(f"✅ base64解码成功,图片数据大小: {len(image_bytes)} bytes") # 验证解码后的数据不为空 if len(image_bytes) == 0: @@ -262,11 +273,11 @@ class PosterService: # 检查文件头判断图片格式 file_header = image_bytes[:10] if file_header.startswith(b'\xff\xd8\xff'): - logger.info("检测到JPEG格式图片") + logger.info("✅ 检测到JPEG格式图片") elif file_header.startswith(b'\x89PNG'): - logger.info("检测到PNG格式图片") + logger.info("✅ 检测到PNG格式图片") else: - logger.warning(f"未识别的图片格式,文件头: {file_header.hex()}") + logger.warning(f"⚠️ 未识别的图片格式,文件头: {file_header.hex()}") # 创建PIL Image对象 image_io = BytesIO(image_bytes) @@ -279,16 +290,16 @@ class PosterService: image_io.seek(0) images = Image.open(image_io) - logger.info(f"图片解码成功,格式: {images.format}, 尺寸: {images.size}, 模式: {images.mode}") + logger.info(f"✅ 图片解码成功,格式: {images.format}, 尺寸: {images.size}, 模式: {images.mode}") except binascii.Error as e: - logger.error(f"Base64解码失败: {e}") - logger.error(f"问题数据长度: {len(images_base64) if 'images_base64' in locals() else 'unknown'}") + logger.error(f"❌ Base64解码失败: {e}") + logger.error(f"问题数据长度: {len(first_image_base64) if 'first_image_base64' in locals() else 'unknown'}") # 创建一个与目标大小一致的透明底图 images = Image.new('RGBA', template_size, color=(0, 0, 0, 0)) logger.info(f"创建默认透明背景图,尺寸: {template_size}") except Exception as e: - logger.error(f"图片处理失败: {e}") + logger.error(f"❌ 图片处理失败: {e}") logger.error(f"错误类型: {type(e).__name__}") if 'image_bytes' in locals(): logger.error(f"图片数据大小: {len(image_bytes)} bytes, 前20字节: {image_bytes[:20].hex()}") @@ -296,7 +307,7 @@ class PosterService: images = Image.new('RGBA', template_size, color=(0, 0, 0, 0)) logger.info(f"创建默认透明背景图,尺寸: {template_size}") else: - logger.warning("未提供图片数据,使用默认透明背景图") + logger.warning("⚠️ 未提供图片数据,使用默认透明背景图") # 创建一个与目标大小一致的透明底图 images = Image.new('RGBA', template_size, color=(0, 0, 0, 0)) logger.info(f"创建默认透明背景图,尺寸: {template_size}") diff --git a/api/services/tweet.py b/api/services/tweet.py index 028afbe..e07d3c4 100644 --- a/api/services/tweet.py +++ b/api/services/tweet.py @@ -10,6 +10,8 @@ import logging import uuid from typing import List, Dict, Any, Optional, Tuple from datetime import datetime +import re +from difflib import SequenceMatcher from core.config import ConfigManager, GenerateTopicConfig, GenerateContentConfig from core.ai import AIAgent @@ -24,6 +26,249 @@ from api.services.database_service import DatabaseService logger = logging.getLogger(__name__) +class TopicIDMappingManager: + """选题ID映射管理器 - 专门处理选题中的对象ID映射""" + + def __init__(self): + """初始化映射管理器""" + self.name_to_id = {} # 名称 -> ID映射 + self.mapping_data = { + 'styles': {}, + 'audiences': {}, + 'scenic_spots': {}, + 'products': {} + } + + def add_objects_mapping(self, + style_objects: Optional[List[Dict[str, Any]]] = None, + audience_objects: Optional[List[Dict[str, Any]]] = None, + scenic_spot_objects: Optional[List[Dict[str, Any]]] = None, + product_objects: Optional[List[Dict[str, Any]]] = None): + """ + 批量添加对象映射关系 + + Args: + style_objects: 风格对象列表 + audience_objects: 受众对象列表 + scenic_spot_objects: 景区对象列表 + product_objects: 产品对象列表 + """ + logger.info("开始建立选题ID映射关系") + mapping_count = 0 + + if style_objects: + logger.info(f"处理 {len(style_objects)} 个风格对象") + for obj in style_objects: + name = obj.get('styleName', '') + obj_id = obj.get('id') + if name and obj_id: + self.mapping_data['styles'][name] = str(obj_id) + self._add_name_variants('styles', name, str(obj_id)) + mapping_count += 1 + logger.info(f"添加风格映射: {name} -> ID {obj_id}") + + if audience_objects: + logger.info(f"处理 {len(audience_objects)} 个受众对象") + for obj in audience_objects: + name = obj.get('audienceName', '') + obj_id = obj.get('id') + if name and obj_id: + self.mapping_data['audiences'][name] = str(obj_id) + self._add_name_variants('audiences', name, str(obj_id)) + mapping_count += 1 + logger.info(f"添加受众映射: {name} -> ID {obj_id}") + + if scenic_spot_objects: + logger.info(f"处理 {len(scenic_spot_objects)} 个景区对象") + for obj in scenic_spot_objects: + name = obj.get('name', '') + obj_id = obj.get('id') + if name and obj_id: + self.mapping_data['scenic_spots'][name] = str(obj_id) + self._add_name_variants('scenic_spots', name, str(obj_id)) + mapping_count += 1 + logger.info(f"添加景区映射: {name} -> ID {obj_id}") + + if product_objects: + logger.info(f"处理 {len(product_objects)} 个产品对象") + for obj in product_objects: + name = obj.get('productName', '') + obj_id = obj.get('id') + if name and obj_id: + self.mapping_data['products'][name] = str(obj_id) + self._add_name_variants('products', name, str(obj_id)) + mapping_count += 1 + logger.info(f"添加产品映射: {name} -> ID {obj_id}") + + total_variants = len(self.name_to_id) + logger.info(f"ID映射关系建立完成: {mapping_count} 个对象,生成 {total_variants} 个名称变体") + + def _add_name_variants(self, category: str, name: str, obj_id: str): + """为名称添加各种变体以支持模糊匹配""" + variants = [name] + + # 去除标点符号版本 + clean_name = re.sub(r'[^\w\s]', '', name) + if clean_name != name: + variants.append(clean_name) + + # 去除空格版本 + no_space_name = name.replace(' ', '') + if no_space_name != name: + variants.append(no_space_name) + + # 添加简化版本(去除常见后缀) + simplified = re.sub(r'(风格|类型|系列|产品|景区|公园)$', '', name) + if simplified != name and simplified: + variants.append(simplified) + + # 为所有变体添加映射 + for variant in variants: + self.name_to_id[variant.lower()] = { + 'category': category, + 'id': obj_id, + 'original_name': name + } + + # 记录生成的变体(仅在有多个变体时记录) + if len(variants) > 1: + variants_str = ', '.join(f"'{v}'" for v in variants) + logger.info(f" 为 '{name}' 生成 {len(variants)} 个变体: {variants_str}") + + def find_ids_in_topic(self, topic: Dict[str, Any]) -> Dict[str, List[str]]: + """ + 在选题中查找相关的对象ID + + Args: + topic: 选题字典 + + Returns: + 按类型分组的ID列表 + """ + topic_index = topic.get('index', 'N/A') + + found_ids = { + 'style_ids': [], + 'audience_ids': [], + 'scenic_spot_ids': [], + 'product_ids': [] + } + + # 提取选题中的所有文本 + topic_text = self._extract_topic_text(topic) + topic_text_lower = topic_text.lower() + + logger.info(f"开始为选题 {topic_index} 进行ID匹配") + logger.info(f"选题 {topic_index} 提取的文本内容: '{topic_text}'") + logger.info(f"当前映射库包含 {len(self.name_to_id)} 个名称变体") + + # 显示映射库内容 + logger.info("映射库内容:") + for variant, info in self.name_to_id.items(): + logger.info(f" '{variant}' -> {info['original_name']} ({info['category']}, ID: {info['id']})") + + # 记录匹配过程 + match_details = [] + + # 在文本中查找匹配的名称 + for name_variant, mapping_info in self.name_to_id.items(): + if name_variant in topic_text_lower: + category = mapping_info['category'] + obj_id = mapping_info['id'] + original_name = mapping_info['original_name'] + + # 记录匹配详情 + match_details.append({ + 'variant': name_variant, + 'original_name': original_name, + 'category': category, + 'id': obj_id + }) + + # 映射到返回格式 + if category == 'styles' and obj_id not in found_ids['style_ids']: + found_ids['style_ids'].append(obj_id) + logger.info(f"选题 {topic_index} 匹配到风格: '{name_variant}' -> {original_name} (ID: {obj_id})") + elif category == 'audiences' and obj_id not in found_ids['audience_ids']: + found_ids['audience_ids'].append(obj_id) + logger.info(f"选题 {topic_index} 匹配到受众: '{name_variant}' -> {original_name} (ID: {obj_id})") + elif category == 'scenic_spots' and obj_id not in found_ids['scenic_spot_ids']: + found_ids['scenic_spot_ids'].append(obj_id) + logger.info(f"选题 {topic_index} 匹配到景区: '{name_variant}' -> {original_name} (ID: {obj_id})") + elif category == 'products' and obj_id not in found_ids['product_ids']: + found_ids['product_ids'].append(obj_id) + logger.info(f"选题 {topic_index} 匹配到产品: '{name_variant}' -> {original_name} (ID: {obj_id})") + + # 统计匹配结果 + total_found = sum(len(ids) for ids in found_ids.values()) + total_available = len(set(info['category'] + '_' + info['id'] for info in self.name_to_id.values())) + match_rate = (total_found / total_available * 100) if total_available > 0 else 0 + + # 输出详细的匹配结果 + if total_found > 0: + logger.info(f"选题 {topic_index} ID匹配完成: 找到 {total_found} 个相关对象,匹配率: {match_rate:.1f}%") + logger.info(f"选题 {topic_index} 匹配详情: {match_details}") + + # 按类别显示匹配结果 + for category, ids in found_ids.items(): + if ids: + category_name = { + 'style_ids': '风格', + 'audience_ids': '受众', + 'scenic_spot_ids': '景区', + 'product_ids': '产品' + }.get(category, category) + + # 获取匹配的原始名称 + matched_names = [] + # 建立正确的分类映射关系 + category_mapping = { + 'style_ids': 'styles', + 'audience_ids': 'audiences', + 'scenic_spot_ids': 'scenic_spots', + 'product_ids': 'products' + } + target_category = category_mapping.get(category) + + logger.info(f"选题 {topic_index} 处理分类 {category} -> {target_category}, IDs: {ids}") + + for detail in match_details: + if detail['id'] in ids and detail['category'] == target_category: + matched_names.append(f"{detail['original_name']}({detail['id']})") + + # 显示分类结果 + names_str = ', '.join(matched_names) if matched_names else '(无匹配名称)' + logger.info(f" {category_name}: {names_str}") + + # 如果没有匹配名称,输出调试信息 + if not matched_names: + logger.info(f"选题 {topic_index} {category_name}无匹配名称,详情检查:") + logger.info(f" IDs: {ids}") + logger.info(f" 目标分类: {target_category}") + for detail in match_details: + if detail['id'] in ids: + logger.info(f" 详情: {detail}") + else: + logger.info(f"选题 {topic_index} 未匹配到任何相关对象ID") + + return found_ids + + def _extract_topic_text(self, topic: Dict[str, Any]) -> str: + """提取选题中的所有文本内容""" + text_parts = [] + + # 提取主要字段的文本 + text_fields = ['object', 'style', 'targetAudience', 'product', 'logic', + 'productLogic', 'styleLogic', 'targetAudienceLogic'] + + for field in text_fields: + if field in topic and topic[field]: + text_parts.append(str(topic[field])) + + return ' '.join(text_parts) + + + class TweetService: """文字内容服务类""" @@ -49,6 +294,9 @@ class TweetService: self.prompt_service = PromptService(config_manager) self.prompt_builder = PromptBuilderService(config_manager, self.prompt_service) + # 初始化选题ID映射管理器 + self.topic_id_mapping_manager = TopicIDMappingManager() + async def generate_topics(self, dates: Optional[str] = None, numTopics: int = 5, styles: Optional[List[str]] = None, audiences: Optional[List[str]] = None, @@ -84,6 +332,14 @@ class TweetService: topic_config.topic.date = dates topic_config.topic.num = numTopics + # 建立ID映射关系(用于后续的ID反向映射) + self.topic_id_mapping_manager.add_objects_mapping( + style_objects=style_objects, + audience_objects=audience_objects, + scenic_spot_objects=scenic_spot_objects, + product_objects=product_objects + ) + # 使用PromptBuilderService构建提示词 system_prompt, user_prompt = self.prompt_builder.build_topic_prompt( products=products, @@ -103,12 +359,35 @@ class TweetService: if not topics: logger.error("未能生成任何选题") return str(uuid.uuid4()), [] + + # 为每个选题添加相关的对象ID + logger.info(f"开始为 {len(topics)} 个选题进行ID映射分析") + enhanced_topics = [] + total_mapped_topics = 0 + + for topic in topics: + enhanced_topic = topic.copy() + + # 查找选题中涉及的对象ID + related_object_ids = self.topic_id_mapping_manager.find_ids_in_topic(topic) + + # 只有当找到相关ID时才添加字段 + if any(related_object_ids.values()): + enhanced_topic['related_object_ids'] = related_object_ids + total_mapped_topics += 1 + logger.info(f"选题 {topic.get('index', 'N/A')} 找到相关ID: {related_object_ids}") + + enhanced_topics.append(enhanced_topic) + + # 统计总体映射情况 + mapping_coverage = (total_mapped_topics / len(topics) * 100) if topics else 0 + logger.info(f"选题ID映射完成: {total_mapped_topics}/{len(topics)} 个选题包含相关对象ID,覆盖率: {mapping_coverage:.1f}%") # 生成请求ID requestId = f"topic-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - logger.info(f"选题生成完成,请求ID: {requestId}, 数量: {len(topics)}") - return requestId, topics + logger.info(f"选题生成完成,请求ID: {requestId}, 数量: {len(enhanced_topics)}") + return requestId, enhanced_topics async def generate_content(self, topic: Optional[Dict[str, Any]] = None, autoJudge: bool = False, style_objects: Optional[List[Dict[str, Any]]] = None, @@ -198,10 +477,10 @@ class TweetService: async def _enhance_topic_with_database_data(self, topic: Dict[str, Any]) -> Dict[str, Any]: """ - 使用数据库数据增强选题信息 + 使用数据库数据增强选题信息,优先使用related_object_ids中的反射ID Args: - topic: 原始选题数据 + topic: 原始选题数据(可能包含related_object_ids字段) Returns: 增强后的选题数据 @@ -216,46 +495,119 @@ class TweetService: logger.warning("数据库服务不可用,无法增强选题数据") return enhanced_topic + # 优先使用related_object_ids中的反射ID(更精确) + related_ids = topic.get('related_object_ids', {}) + if related_ids: + logger.info(f"选题包含反射ID信息,优先使用: {related_ids}") + enhanced_topic = await self._enhance_with_related_ids(enhanced_topic, db_service, related_ids) + return enhanced_topic + # 处理风格ID if 'styleIds' in topic and topic['styleIds']: style_id = topic['styleIds'][0] if isinstance(topic['styleIds'], list) else topic['styleIds'] style_data = db_service.get_style_by_id(style_id) if style_data: + style_name = style_data.get('styleName') enhanced_topic['style_object'] = style_data - enhanced_topic['style'] = style_data.get('styleName') - logger.info(f"从数据库加载风格数据: {style_data.get('styleName')} (ID: {style_id})") + enhanced_topic['style'] = style_name + logger.info(f"从数据库加载风格数据: {style_name} (ID: {style_id})") # 处理受众ID if 'audienceIds' in topic and topic['audienceIds']: audience_id = topic['audienceIds'][0] if isinstance(topic['audienceIds'], list) else topic['audienceIds'] audience_data = db_service.get_audience_by_id(audience_id) if audience_data: + audience_name = audience_data.get('audienceName') enhanced_topic['audience_object'] = audience_data - enhanced_topic['targetAudience'] = audience_data.get('audienceName') - logger.info(f"从数据库加载受众数据: {audience_data.get('audienceName')} (ID: {audience_id})") + enhanced_topic['targetAudience'] = audience_name + logger.info(f"从数据库加载受众数据: {audience_name} (ID: {audience_id})") # 处理景区ID if 'scenicSpotIds' in topic and topic['scenicSpotIds']: spot_id = topic['scenicSpotIds'][0] if isinstance(topic['scenicSpotIds'], list) else topic['scenicSpotIds'] spot_data = db_service.get_scenic_spot_by_id(spot_id) if spot_data: + spot_name = spot_data.get('name') enhanced_topic['scenic_spot_object'] = spot_data - enhanced_topic['object'] = spot_data.get('name') - logger.info(f"从数据库加载景区数据: {spot_data.get('name')} (ID: {spot_id})") + enhanced_topic['object'] = spot_name + logger.info(f"从数据库加载景区数据: {spot_name} (ID: {spot_id})") # 处理产品ID if 'productIds' in topic and topic['productIds']: product_id = topic['productIds'][0] if isinstance(topic['productIds'], list) else topic['productIds'] product_data = db_service.get_product_by_id(product_id) if product_data: + product_name = product_data.get('productName') enhanced_topic['product_object'] = product_data - enhanced_topic['product'] = product_data.get('productName') - logger.info(f"从数据库加载产品数据: {product_data.get('productName')} (ID: {product_id})") + enhanced_topic['product'] = product_name + logger.info(f"从数据库加载产品数据: {product_name} (ID: {product_id})") except Exception as e: logger.error(f"增强选题数据时发生错误: {e}", exc_info=True) return enhanced_topic + + async def _enhance_with_related_ids(self, enhanced_topic: Dict[str, Any], db_service, related_ids: Dict[str, List[str]]) -> Dict[str, Any]: + """ + 使用related_object_ids中的反射ID进行数据库查询和增强 + + Args: + enhanced_topic: 待增强的选题数据 + db_service: 数据库服务实例 + related_ids: 反射的ID字典 + + Returns: + 增强后的选题数据 + """ + logger.info("开始使用反射ID进行选题数据增强") + + try: + # 处理风格ID + style_ids = related_ids.get('style_ids', []) + if style_ids: + style_id = int(style_ids[0]) # 取第一个ID + style_data = db_service.get_style_by_id(style_id) + if style_data: + enhanced_topic['style_object'] = style_data + enhanced_topic['style'] = style_data.get('styleName') + logger.info(f"通过反射ID加载风格数据: {style_data.get('styleName')} (ID: {style_id})") + + # 处理受众ID + audience_ids = related_ids.get('audience_ids', []) + if audience_ids: + audience_id = int(audience_ids[0]) # 取第一个ID + audience_data = db_service.get_audience_by_id(audience_id) + if audience_data: + enhanced_topic['audience_object'] = audience_data + enhanced_topic['targetAudience'] = audience_data.get('audienceName') + logger.info(f"通过反射ID加载受众数据: {audience_data.get('audienceName')} (ID: {audience_id})") + + # 处理景区ID + scenic_spot_ids = related_ids.get('scenic_spot_ids', []) + if scenic_spot_ids: + spot_id = int(scenic_spot_ids[0]) # 取第一个ID + spot_data = db_service.get_scenic_spot_by_id(spot_id) + if spot_data: + enhanced_topic['scenic_spot_object'] = spot_data + enhanced_topic['object'] = spot_data.get('name') + logger.info(f"通过反射ID加载景区数据: {spot_data.get('name')} (ID: {spot_id})") + + # 处理产品ID + product_ids = related_ids.get('product_ids', []) + if product_ids: + product_id = int(product_ids[0]) # 取第一个ID + product_data = db_service.get_product_by_id(product_id) + if product_data: + enhanced_topic['product_object'] = product_data + enhanced_topic['product'] = product_data.get('productName') + logger.info(f"通过反射ID加载产品数据: {product_data.get('productName')} (ID: {product_id})") + + logger.info("反射ID数据增强完成") + + except Exception as e: + logger.error(f"使用反射ID增强选题数据时发生错误: {e}", exc_info=True) + + return enhanced_topic async def generate_content_with_prompt(self, topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Tuple[str, str, Dict[str, Any]]: """ diff --git a/config/database.json b/config/database.json index fc9ad20..4ec8470 100644 --- a/config/database.json +++ b/config/database.json @@ -1,7 +1,7 @@ { "host": "localhost", "user": "root", - "password": "civmek-rezTed-0hovre", + "password": "Kj#9mP2$", "database": "travel_content", "port": 3306, "charset": "utf8mb4", diff --git a/run_api.py b/run_api.py index 2d0a05d..6886aae 100644 --- a/run_api.py +++ b/run_api.py @@ -32,7 +32,7 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--port", type=int, - default=int(os.getenv("PORT", 8000)), + default=int(os.getenv("PORT", 2714)), help="监听端口", ) parser.add_argument( diff --git a/海报Fabric.js前端对接文档.md b/海报Fabric.js前端对接文档.md new file mode 100644 index 0000000..797bc94 --- /dev/null +++ b/海报Fabric.js前端对接文档.md @@ -0,0 +1,711 @@ +# 海报Fabric.js前端对接文档 + +## 📋 概述 + +本文档详细说明如何在前端使用新的海报生成API返回的Fabric.js JSON数据,包括图层加载、图片替换、图层管理等功能。 + +## 🔄 核心变化 + +### **从PSD到Fabric.js JSON** +- **原先**: 返回PSD文件的base64编码 +- **现在**: 返回Fabric.js JSON文件的base64编码 + 原始JSON数据 +- **优势**: 可以直接在前端使用,支持实时编辑和图片替换 + +## 🎯 API接口说明 + +### 1. **生成海报接口** + +#### **请求示例** +```javascript +const generatePosterRequest = { + posterNumber: 1, + posters: [{ + templateId: "vibrant", + imagesBase64: "图片ID列表", + contentId: "内容ID", + productId: "产品ID", + generatePsd: true, // 现在实际生成JSON + numVariations: 1 + }] +}; + +const response = await fetch('/poster/generate', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(generatePosterRequest) +}); + +const result = await response.json(); +``` + +#### **响应结构** +```javascript +{ + "code": 0, + "message": "操作成功", + "data": [{ + "requestId": "poster-20250104-123456", + "templateId": "vibrant", + "resultImagesBase64": [{ + "id": "vibrant_v1", + "image": "base64编码的PNG图片", + "format": "PNG", + "size": [1350, 1800] + }], + "psdFiles": [{ // 现在是JSON文件 + "id": "vibrant_v1_json", + "filename": "template_fabric_v1_20250104.json", + "data": "base64编码的JSON文件", + "size": 15672, + "format": "JSON", + "jsonData": { // 原始JSON数据,可直接使用 + "version": "5.3.0", + "objects": [...], + "background": "white", + "width": 1350, + "height": 1800 + } + }] + }] +} +``` + +### 2. **获取Fabric.js JSON数据** +```javascript +// 获取指定海报的JSON数据 +const fabricJson = await fetch(`/poster/fabric-json/${posterId}`) + .then(res => res.json()) + .then(data => data.data); +``` + +### 3. **替换底层图片** +```javascript +// 单个图片替换 +const replaceImage = async (posterId, newImageBase64) => { + const formData = new FormData(); + formData.append('posterId', posterId); + formData.append('newImageBase64', newImageBase64); + + const response = await fetch('/poster/replace-image', { + method: 'POST', + body: formData + }); + + return response.json(); +}; +``` + +### 4. **获取图层信息** +```javascript +// 获取图层管理信息 +const layers = await fetch(`/poster/layers/${posterId}`) + .then(res => res.json()) + .then(data => data.data); + +// 图层信息结构 +// [{ +// "name": "图片层", +// "type": "image", +// "level": 0, +// "visible": true, +// "selectable": true, +// "replaceable": true +// }] +``` + +## 🛠️ 前端集成实现 + +### 1. **基础环境准备** + +#### **HTML结构** +```html + + +
+ +拖拽图片到这里或点击选择
+ +