From a058a173de52647b8645f371bc00a2eb5b928a53 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Tue, 22 Apr 2025 21:26:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E5=96=84=E4=BA=86=E6=B5=B7=E6=8A=A5?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 +++ SelectPrompt/systemPrompt.txt | 4 +- core/__pycache__/ai_agent.cpython-312.pyc | Bin 18376 -> 18051 bytes core/__pycache__/contentGen.cpython-312.pyc | Bin 14200 -> 18887 bytes core/__pycache__/posterGen.cpython-312.pyc | Bin 30042 -> 37587 bytes .../simple_collage.cpython-312.pyc | Bin 30203 -> 33898 bytes core/ai_agent.py | 87 +++--- core/contentGen.py | 127 +++++++- core/posterGen.py | 198 +++++++++++-- core/simple_collage.py | 276 +++++++++++------- poster_gen_config.json | 2 +- .../tweet_generator.cpython-312.pyc | Bin 28029 -> 29250 bytes utils/tweet_generator.py | 41 ++- 13 files changed, 585 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 15cafb3..cde0086 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ pip install numpy pandas opencv-python pillow openai - `contentGen.py`: **内容处理器**: 对 AI 生成的原始推文内容进行结构化处理,提取适用于海报的元素。 - `posterGen.py`: **海报生成器**: 负责将图片和文字元素组合生成最终的海报图片,处理字体、布局等。 - `simple_collage.py`: **图片拼贴工具**: 提供图片预处理和拼贴功能。 + - `ImageCollageCreator` 类:核心拼贴图实现类,提供多种拼贴样式(如标准网格、非对称布局、胶片条、重叠风格等)。 + - `process_directory` 函数:对外接口,接收图片目录路径和参数,使用 `ImageCollageCreator` 创建一组拼贴图。 + - 支持多种拼贴样式:标准 2x2 网格、非对称布局、胶片条、重叠效果、马赛克风格等。 + - 提供图像增强:自动调整对比度、亮度和饱和度,使拼贴图更加美观。 + - 智能处理不同大小、格式的图片,自动尝试加载所有支持的图片格式(.jpg, .jpeg, .png, .bmp)。 + - 错误处理与恢复:当部分图片加载失败时,会自动尝试使用其他可用图片代替。 - `utils/`: 工具与辅助模块 - `resource_loader.py`: **资源加载器**: 负责加载项目所需的各种**原始**资源文件。 - `prompt_manager.py`: **提示词管理器**: **集中管理**不同阶段提示词的构建逻辑(**已修正内容生成提示词构建逻辑,正确区分选题JSON中的文件名和描述性文本**)。 @@ -341,3 +347,34 @@ This refactoring makes it straightforward to add new output handlers in the futu * `stream_chunk_timeout` (可选, 默认 60): 处理流式响应时,允许的两个数据块之间的最大等待时间(秒),用于防止流长时间挂起。 项目提供了一个示例配置文件 `example_config.json`,请务必复制并修改: + +## 图片目录要求 + +为确保海报生成功能正常工作,请按照以下结构组织图片目录: + +1. **基础图片目录**:在 `poster_gen_config.json` 中设置 `image_base_dir` 参数指向基础图片目录。 +2. **原始照片目录**:在基础目录下创建 `相机` 子目录(或通过 `camera_image_subdir` 配置),包含原始照片。 +3. **修改图片目录**:在基础目录下创建 `modify` 子目录(或通过 `modify_image_subdir` 配置),并其中为每个景点创建子目录: + ``` + image_base_dir/ + ├── 相机/ + │ ├── 景点1/ + │ │ ├── 照片1.jpg + │ │ ├── 照片2.jpg + │ │ └── ... + │ └── 景点2/ + │ └── ... + └── modify/ + ├── 景点1/ + │ ├── 图片1.jpg + │ ├── 图片2.jpg + │ └── ... (至少4-9张图片,建议多于9张) + └── 景点2/ + └── ... + ``` + +**注意事项**: +- 每个景点的 `modify` 子目录中至少需要 4 张图片,建议包含 9 张或更多图片以保证拼贴效果多样性。 +- 图片应当质量良好,清晰、色彩丰富,尺寸适中(过大或过小的图片都可能导致处理问题)。 +- 支持的图片格式为 JPG (.jpg, .jpeg)、PNG (.png) 和 BMP (.bmp)。 +- 确保图片文件没有损坏且可以被 PIL 库正常打开。 diff --git a/SelectPrompt/systemPrompt.txt b/SelectPrompt/systemPrompt.txt index 9c2669d..cc486a7 100644 --- a/SelectPrompt/systemPrompt.txt +++ b/SelectPrompt/systemPrompt.txt @@ -47,9 +47,9 @@ "object": "...", "product": "...", "product_logic": "...", - "style": "历史文化风文案提示词.txt", + "style": "...", "style_logic": "...", - "target_audience": "文化爱好者文旅需求.txt", + "target_audience": "...", "target_audience_logic": "..." } ] diff --git a/core/__pycache__/ai_agent.cpython-312.pyc b/core/__pycache__/ai_agent.cpython-312.pyc index 6900778c0828a52ce7ebfed2845e1304af20ada9..e1356a8bcfebddffe982ce4adfe4fcf14ede58d0 100644 GIT binary patch delta 4390 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 delta 820 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 aHA#r%`+DyRwZ9ihUZ{E~Bt$xHfBruxQi5s# delta 4407 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 W%l`h~YTTb`4NulhYKS&fo%X-F?Sgp# diff --git a/core/__pycache__/simple_collage.cpython-312.pyc b/core/__pycache__/simple_collage.cpython-312.pyc index 505b93bf51f304f1d88420a71bede74e5ab3bb1d..5d870e5c3f2c2ed4e1d4299d87845f047572e5da 100644 GIT binary patch delta 9217 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 I%@9rhKj|6gbN~PV diff --git a/core/ai_agent.py b/core/ai_agent.py index e3dd8c3..63b5e33 100644 --- a/core/ai_agent.py +++ b/core/ai_agent.py @@ -55,33 +55,40 @@ class AI_Agent(): timeout=self.timeout ) - try: - self.encoding = tiktoken.encoding_for_model(self.model_name) - except KeyError: - logging.warning(f"Encoding for model '{self.model_name}' not found. Using 'cl100k_base' encoding.") - self.encoding = tiktoken.get_encoding("cl100k_base") + # try: + # self.encoding = tiktoken.encoding_for_model(self.model_name) + # except KeyError: + # logging.warning(f"Encoding for model '{self.model_name}' not found. Using 'cl100k_base' encoding.") + # self.encoding = tiktoken.get_encoding("cl100k_base") def generate_text(self, system_prompt, user_prompt, temperature, top_p, presence_penalty): """生成文本内容,并返回完整响应和token估计值""" - logging.info(f"Generating text with model: {self.model_name}, temp={temperature}, top_p={top_p}, presence_penalty={presence_penalty}") - logging.debug(f"System Prompt (first 100 chars): {system_prompt[:100]}...") - logging.debug(f"User Prompt (first 100 chars): {user_prompt[:100]}...") + logging.info("Starting text generation process...") + # logging.debug(f"System Prompt (first 100): {system_prompt[:100]}...") + # logging.debug(f"User Prompt (first 100): {user_prompt[:100]}...") # Avoid logging potentially huge prompts + logging.info(f"Generation Params: temp={temperature}, top_p={top_p}, presence_penalty={presence_penalty}") - time.sleep(random.random()) retry_count = 0 - max_retry_wait = 10 + max_retry_wait = 10 # Max wait time between retries + full_response = "" while retry_count <= self.max_retries: + call_start_time = None # Initialize start time try: - logging.info(f"Attempting API call (try {retry_count + 1}/{self.max_retries + 1})") + # --- Added Logging --- + user_prompt_size = len(user_prompt) + logging.info(f"Attempt {retry_count + 1}/{self.max_retries + 1}: Preparing API request. User prompt size: {user_prompt_size} chars.") + call_start_time = time.time() + # --- End Added Logging --- + response = self.client.chat.completions.create( model=self.model_name, - messages=[{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}], - temperature=temperature, + messages=[{"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}], + temperature=temperature, top_p=top_p, presence_penalty=presence_penalty, - stream=True, + stream=False, # Ensure this is False for non-streaming method max_tokens=8192, timeout=self.timeout, extra_body={ @@ -89,35 +96,35 @@ class AI_Agent(): }, ) - full_response = "" - stream_timed_out = False - try: - for chunk in response: - if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None: - content = chunk.choices[0].delta.content - full_response += content - if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].finish_reason == "stop": - break - # Successfully finished stream - break - - except Exception as stream_err: - logging.warning(f"Exception during stream processing: {stream_err}") - stream_timed_out = True + # --- Added Logging --- + call_end_time = time.time() + logging.info(f"Attempt {retry_count + 1}/{self.max_retries + 1}: API request function returned successfully after {call_end_time - call_start_time:.2f} seconds.") + # --- End Added Logging --- - if stream_timed_out: - if len(full_response) > 100: - logging.warning(f"Stream interrupted, but received {len(full_response)} characters. Using partial content.") - break - - retry_count += 1 + if response.choices and response.choices[0].message: + full_response = response.choices[0].message.content + logging.info(f"Received successful response. Content length: {len(full_response)} chars.") + break # Success, exit retry loop + else: + logging.warning("API response structure unexpected or empty content.") + full_response = "[Error: Empty or invalid response structure]" + # Decide if this specific case should retry or fail immediately + retry_count += 1 # Example: Treat as retryable if retry_count <= self.max_retries: - wait_time = min(2 ** retry_count + random.random(), max_retry_wait) - logging.warning(f"Stream error/timeout. Waiting {wait_time:.2f}s before retry ({retry_count}/{self.max_retries})...") - time.sleep(wait_time) + wait_time = min(2 ** retry_count + random.random(), max_retry_wait) + logging.warning(f"Retrying due to unexpected response structure ({retry_count}/{self.max_retries}), waiting {wait_time:.2f}s...") + time.sleep(wait_time) continue - + except (APITimeoutError, APIConnectionError, RateLimitError, APIStatusError) as e: + # --- Added Logging --- + if call_start_time: + call_fail_time = time.time() + logging.warning(f"Attempt {retry_count + 1}/{self.max_retries + 1}: API call failed/timed out after {call_fail_time - call_start_time:.2f} seconds.") + else: + logging.warning(f"Attempt {retry_count + 1}/{self.max_retries + 1}: API call failed before or during initiation.") + # --- End Added Logging --- + logging.warning(f"API Error occurred: {e}") should_retry = False if isinstance(e, (APITimeoutError, APIConnectionError, RateLimitError)): diff --git a/core/contentGen.py b/core/contentGen.py index 88d4ede..dbd2f75 100644 --- a/core/contentGen.py +++ b/core/contentGen.py @@ -6,6 +6,7 @@ import cv2 import time import random import json +import logging class ContentGenerator: def __init__(self, @@ -114,7 +115,70 @@ class ContentGenerator: 返回: 分割后的json内容 """ - return json.loads(content.split("```json")[1].split("```")[0]) + try: + # 首先尝试直接解析整个内容,以防已经是干净的 JSON + try: + return json.loads(content) + except json.JSONDecodeError: + pass # 不是干净的 JSON,继续处理 + + # 常规模式:查找 ```json 和 ``` 之间的内容 + if "```json" in content: + json_str = content.split("```json")[1].split("```")[0].strip() + try: + return json.loads(json_str) + except json.JSONDecodeError as e: + print(f"常规格式解析失败: {e}, 尝试其他方法") + + # 备用模式1:查找连续的 { 开头和 } 结尾的部分 + import re + json_pattern = r'(\[.*?\])' + json_matches = re.findall(json_pattern, content, re.DOTALL) + if json_matches: + for match in json_matches: + try: + result = json.loads(match) + if isinstance(result, list) and len(result) > 0: + return result + except: + continue + + # 备用模式2:查找 { 开头 和 } 结尾,并尝试解析 + content = content.strip() + square_bracket_start = content.find('[') + square_bracket_end = content.rfind(']') + + if square_bracket_start != -1 and square_bracket_end != -1: + potential_json = content[square_bracket_start:square_bracket_end + 1] + try: + return json.loads(potential_json) + except: + print("尝试提取方括号内容失败") + + # 最后一种尝试:查找所有可能的 JSON 结构并尝试解析 + json_structures = re.findall(r'({.*?})', content, re.DOTALL) + if json_structures: + items = [] + for i, struct in enumerate(json_structures): + try: + item = json.loads(struct) + # 验证结构包含预期字段 + if 'main_title' in item and ('texts' in item or 'index' in item): + items.append(item) + except: + continue + + if items: + return items + + # 都失败了,打印错误并引发异常 + print(f"无法解析内容,返回原始文本: {content[:200]}...") + raise ValueError("无法从响应中提取有效的 JSON 格式") + + except Exception as e: + print(f"解析内容时出错: {e}") + print(f"原始内容: {content[:200]}...") # 仅显示前200个字符 + raise e def generate_posters(self, poster_num, tweet_content, system_prompt=None, max_retries=3): """ @@ -292,27 +356,64 @@ class ContentGenerator: def run(self, info_directory, poster_num, tweet_content): """ - 运行完整的海报生成流程 + 运行海报内容生成流程,并返回生成的配置数据。 参数: - info_directory: 信息目录 - poster_num: 海报数量 - tweet_content: 推文内容 + info_directory: 信息目录路径列表 (e.g., ['/path/to/description.txt']) + poster_num: 需要生成的海报配置数量 + tweet_content: 用于生成内容的推文/文章内容 返回: - 结果保存路径 + list | dict | None: 生成的海报配置数据 (通常是列表),如果生成或解析失败则返回 None。 """ - ## 读取资料文件 self.load_infomation(info_directory) - ## 生成海报内容 + # Generate the raw string response from AI full_response = self.generate_posters(poster_num, tweet_content) - if self.output_dir: - ## 保存结果 - return self.save_result(full_response) - else: - return full_response + # Check if generation failed (indicated by return code 404 or other markers) + if full_response == 404 or not isinstance(full_response, str) or not full_response.strip(): + logging.error("Poster content generation failed or returned empty response.") + return None + + # Extract the JSON data from the raw response string + try: + result_data = self.split_content(full_response) # This should return the list/dict + + # 验证结果数据格式 + if isinstance(result_data, list): + for i, item in enumerate(result_data): + if not isinstance(item, dict): + logging.warning(f"配置项 {i+1} 不是字典格式: {item}") + continue + + # 检查并确保必需字段存在 + if 'main_title' not in item: + item['main_title'] = f"景点标题 {i+1}" + logging.warning(f"配置项 {i+1} 缺少 main_title 字段,已添加默认值") + + if 'texts' not in item: + item['texts'] = ["景点特色", "游玩体验"] + logging.warning(f"配置项 {i+1} 缺少 texts 字段,已添加默认值") + + logging.info(f"成功生成并解析海报配置数据,包含 {len(result_data)} 个项目") + else: + logging.warning(f"生成的配置数据不是列表格式: {type(result_data)}") + + return result_data # Return the actual data + except Exception as e: + logging.exception(f"Failed to parse JSON from AI response in ContentGenerator: {e}\nRaw Response:\n{full_response[:500]}...") # Log error and partial response + + # 失败后尝试创建一个默认配置 + logging.info("创建默认海报配置数据") + default_configs = [] + for i in range(poster_num): + default_configs.append({ + "index": i + 1, + "main_title": f"景点风光 {i+1}", + "texts": ["自然美景", "人文体验"] + }) + return default_configs def set_temperature(self, temperature): self.temperature = temperature diff --git a/core/posterGen.py b/core/posterGen.py index 29ba1ae..2f65e8c 100644 --- a/core/posterGen.py +++ b/core/posterGen.py @@ -164,13 +164,47 @@ class PosterGenerator: return os.path.join(font_dir, "华康海报体简.ttc") def create_base_layer(self, image_path, target_size): - """创建底层(图片层)""" + """创建底层(图片层) + + Args: + image_path: 可以是图片文件路径字符串,也可以是已加载的 PIL Image 对象 + target_size: 目标图片尺寸 (width, height) + + Returns: + 调整大小后的 PIL Image 对象 + """ try: - base_image = Image.open(image_path).convert('RGBA') + # 检查输入类型 + if isinstance(image_path, Image.Image): + # 如果已经是 PIL Image 对象 + print("输入已是 PIL Image 对象,无需加载") + base_image = image_path.convert('RGBA') + print(f"图像原始尺寸: {base_image.size}") + else: + # 否则,作为路径处理 + # 先验证图片路径 + if not image_path: + raise ValueError("图片路径为空") + + if not os.path.exists(image_path): + raise FileNotFoundError(f"图片文件不存在: {image_path}") + + print(f"尝试加载底图: {image_path}") + base_image = Image.open(image_path).convert('RGBA') + print(f"底图加载成功,原始尺寸: {base_image.size}") + + # 调整尺寸 base_image = base_image.resize(target_size, Image.Resampling.LANCZOS) + print(f"底图调整尺寸完成: {target_size}") + return base_image + except FileNotFoundError as e: + print(f"创建底层失败: {e}") + print(f"当前工作目录: {os.getcwd()}") + return Image.new('RGBA', target_size, (255, 255, 255, 255)) except Exception as e: print(f"创建底层失败: {e}") + traceback.print_exc() return Image.new('RGBA', target_size, (255, 255, 255, 255)) def add_frame(self, image, target_size): @@ -297,12 +331,15 @@ class PosterGenerator: self.selected_effect = "文字蓝色立体效果" print(f"使用文字效果: {self.selected_effect}") - # 如果没有文字数据,使用默认值 + # 检查文字数据 if text_data is None: - text_data = {'title': '泰宁县 甘露岩寺'} + print("警告: 未提供文本数据,使用默认文本") + text_data = {'title': '旅游景点', 'subtitle': '', 'additional_texts': []} + + print(f"处理文本数据: {text_data}") # 1. 处理主标题 - if hasattr(self, 'title_area') and 'title' in text_data: + if hasattr(self, 'title_area') and 'title' in text_data and text_data['title']: font_path = self._get_font_path() title = text_data['title'] @@ -321,6 +358,8 @@ class PosterGenerator: # 打印调试信息 self._print_text_debug_info("主标题", font, text_width, x, y, font_path) print(f"- 主标题颜色: 柠檬黄色 RGB(255, 250, 55)") + else: + print("警告: 无法处理主标题,可能缺少标题数据或title_area未定义") # 2. 处理副标题(如果有) if hasattr(self, 'title_area') and 'subtitle' in text_data and text_data['subtitle']: @@ -350,17 +389,28 @@ class PosterGenerator: # 3. 处理额外文本(如果有) if 'additional_texts' in text_data and text_data['additional_texts']: + # 打印接收到的额外文本 + print(f"接收到额外文本数据: {text_data['additional_texts']}") + # 过滤掉空文本项 - additional_texts = [item for item in text_data['additional_texts'] if item.get('text')] + valid_additional_texts = [] + for item in text_data['additional_texts']: + if isinstance(item, dict) and 'text' in item and item['text']: + valid_additional_texts.append(item) + elif isinstance(item, str) and item: + # 如果是字符串,转换为字典格式 + valid_additional_texts.append({"text": item, "position": "bottom", "size_factor": 0.5}) - if additional_texts and hasattr(self, 'title_area'): + print(f"有效额外文本项: {len(valid_additional_texts)}") + + if valid_additional_texts and hasattr(self, 'title_area'): # 获取主标题的字体大小 - main_title_font_size = font.size + main_title_font_size = font.size if 'font' in locals() else 48 # 默认字体大小 # 使用固定字体 specific_font_path = os.path.join("/root/autodl-tmp/poster_baseboard_0403/font", "华康海报体简.ttc") if not os.path.isfile(specific_font_path): - specific_font_path = font_path + specific_font_path = font_path if 'font_path' in locals() else self._get_font_path() # 计算额外文本在屏幕上的位置 height = target_size[1] @@ -376,7 +426,7 @@ class PosterGenerator: max_text_width = width - (safe_margin_x * 2) # 总文本行数 - total_lines = len(additional_texts) + total_lines = len(valid_additional_texts) line_height = extra_text_height // total_lines if total_lines > 0 else 0 print(f"额外文本区域: y={extra_text_y_start}, 高度={extra_text_height}, 每行高度={line_height}") @@ -384,11 +434,16 @@ class PosterGenerator: print(f"文本颜色: 统一白色") # 渲染每一行文本 - for i, text_item in enumerate(additional_texts): + for i, text_item in enumerate(valid_additional_texts): item_text = text_item['text'] - # 设置字体大小为主标题的0.8倍 - size_factor = 0.8 + # 检查文本内容 + if not item_text: + print(f"警告: 额外文本项 {i+1} 文本为空,跳过") + continue + + # 设置字体大小为主标题的0.8倍或使用指定的size_factor + size_factor = text_item.get('size_factor', 0.8) font_size = int(main_title_font_size * size_factor) text_font = ImageFont.truetype(specific_font_path, font_size) @@ -406,8 +461,20 @@ class PosterGenerator: text_bbox = draw.textbbox((0, 0), item_text, font=text_font) text_height = text_bbox[3] - text_bbox[1] - # 计算垂直位置 - 在分配的空间内居中 - line_y = extra_text_y_start + (i * line_height) + ((line_height - text_height) // 2) + # 获取位置参数 + position = text_item.get('position', 'bottom') + + # 根据位置设置垂直位置 + if position == 'top': + line_y = int(height * 0.05) + (i * line_height) + elif position == 'middle': + line_y = int(height * 0.45) + (i * line_height) + else: # position == 'bottom' 或其他 + # 在底部区域,使用更大的垂直间距,比如整个海报高度的65%到85% + bottom_start = int(height * 0.65) + bottom_height = int(height * 0.2) + bottom_line_height = bottom_height // total_lines if total_lines > 0 else 0 + line_y = bottom_start + (i * bottom_line_height) # 水平居中位置 line_x = (width - text_width) // 2 @@ -425,6 +492,8 @@ class PosterGenerator: print(f"- 文本颜色: 白色") print(f"- 字体大小: {font_size}px (主标题的{size_factor:.2f}倍)") print(f"- 位置: x={line_x}, y={line_y}") + else: + print("无法处理额外文本:没有有效的额外文本项或title_area未定义") return text_layer except Exception as e: @@ -590,16 +659,84 @@ class PosterGenerator: draw.text((x, y), text, font=font, fill=text_color) def _print_text_debug_info(self, text_type, font, text_width, x, y, font_path): - pass - # print(f" {text_type}: Font={os.path.basename(font_path)}, Size={font.size}, Width={text_width:.0f}, Pos=({x:.0f}, {y:.0f})") + """打印文本调试信息""" + print(f"- {text_type} 字体大小: {font.size}px") + print(f"- {text_type} 文本宽度: {text_width}px") + print(f"- {text_type} 位置: x={x}, y={y}") + print(f"- {text_type} 使用字体: {os.path.basename(font_path)}") - def create_poster(self, image_path, text_data): + def add_stickers(self, poster_image): + """ + 在海报上添加装饰性贴纸 + + Args: + poster_image: 要添加贴纸的海报图像对象 + + Returns: + 添加了贴纸的海报图像对象 + """ + if not hasattr(self, 'sticker_files') or not self.sticker_files: + print("没有可用的贴纸素材,跳过贴纸添加") + return poster_image + + try: + # 获取海报尺寸 + width, height = poster_image.size + + # 决定是否添加贴纸(50%概率) + if random.random() < 0.5: + print("随机决定不添加贴纸") + return poster_image + + # 随机决定添加1-3个贴纸 + sticker_count = random.randint(1, 3) + print(f"准备添加 {sticker_count} 个贴纸") + + # 创建一个新图层用于合成 + result_image = poster_image.copy() + + for i in range(sticker_count): + # 随机选择一个贴纸 + sticker_file = random.choice(self.sticker_files) + sticker_path = os.path.join(self.sticker_dir, sticker_file) + + # 加载贴纸图像 + sticker = Image.open(sticker_path).convert('RGBA') + + # 调整贴纸大小(原始尺寸的10%-30%) + sticker_size_factor = random.uniform(0.1, 0.3) + new_width = int(width * sticker_size_factor) + new_height = int(new_width * sticker.height / sticker.width) # 保持纵横比 + sticker = sticker.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 随机选择贴纸位置(避开中央区域) + margin = int(width * 0.1) # 边缘区域的10% + + # 生成随机位置(避开中间区域) + if random.random() < 0.5: # 左/右边缘 + x = random.randint(margin, int(width * 0.25)) if random.random() < 0.5 else random.randint(int(width * 0.75), width - new_width - margin) + y = random.randint(margin, height - new_height - margin) + else: # 上/下边缘 + x = random.randint(margin, width - new_width - margin) + y = random.randint(margin, int(height * 0.25)) if random.random() < 0.5 else random.randint(int(height * 0.75), height - new_height - margin) + + # 将贴纸粘贴到结果图像上 + result_image.paste(sticker, (x, y), sticker) + print(f"添加贴纸 {i+1}: {sticker_file}, 大小: {new_width}x{new_height}, 位置: ({x}, {y})") + + return result_image + except Exception as e: + print(f"添加贴纸失败: {e}") + traceback.print_exc() + return poster_image + + def create_poster(self, image_input, text_data): """ Creates a poster by combining the base image, frame (optional), stickers (optional), and text layers. Args: - image_path: Path to the base image (e.g., the generated collage). + image_input: 底图输入,可以是图片路径字符串或 PIL Image 对象 text_data: Dictionary containing text information ( { 'title': 'Main Title Text', @@ -614,13 +751,15 @@ class PosterGenerator: target_size = (900, 1200) # TODO: Make target_size a parameter? print(f"\n--- Creating Poster --- ") - print(f"Input Image: {image_path}") + if isinstance(image_input, Image.Image): + print(f"Input: PIL Image 对象,尺寸 {image_input.size}") + else: + print(f"Input Image: {image_input}") print(f"Text Data: {text_data}") - # print(f"Output Name: {output_name}") # output_name is removed try: # 1. 创建底层(图片) - base_layer = self.create_base_layer(image_path, target_size) + base_layer = self.create_base_layer(image_input, target_size) if not base_layer: raise ValueError("Failed to create base layer.") print("Base layer created.") @@ -700,7 +839,20 @@ def main(): } # 处理目录中的所有图片 img_path = "/root/autodl-tmp/poster_baseboard_0403/output_collage/random_collage_1_collage.png" - generator.create_poster(img_path, text_data) + + # 先加载图片,然后传递 PIL Image 对象 + try: + # 先加载图片 + collage_img = Image.open(img_path).convert('RGBA') + print(f"已加载拼贴图: {img_path}, 尺寸: {collage_img.size}") + + # 传递图片对象而不是路径 + generator.create_poster(collage_img, text_data) + except Exception as e: + print(f"加载或处理图片时出错: {e}") + traceback.print_exc() + # 失败时回退到使用路径 + generator.create_poster(img_path, text_data) if __name__ == "__main__": main() diff --git a/core/simple_collage.py b/core/simple_collage.py index 1da23c3..d1598b6 100644 --- a/core/simple_collage.py +++ b/core/simple_collage.py @@ -5,6 +5,7 @@ import traceback import math from pathlib import Path from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageOps +import logging # Import logging module class ImageCollageCreator: def __init__(self): @@ -137,6 +138,7 @@ class ImageCollageCreator: def create_collage_with_style(self, input_dir, style=None, target_size=None): """创建指定样式的拼接画布""" + logging.info(f"--- Starting Collage Creation for Directory: {input_dir} ---") # Start Log try: # 设置默认尺寸为3:4比例 if target_size is None: @@ -145,106 +147,123 @@ class ImageCollageCreator: # 如果没有指定样式,随机选择一种 if style is None or style not in self.collage_styles: style = random.choice(self.collage_styles) - print(f"使用拼接样式: {style}") + logging.info(f"Using collage style: {style} with target size: {target_size}") # 检查目录是否存在 if not os.path.exists(input_dir): - print(f"目录不存在: {input_dir}") + logging.error(f"Input directory does not exist: {input_dir}") return None # 支持的图片格式 image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') - # 获取目录中的所有图片文件 - all_images = [f for f in os.listdir(input_dir) - if f.lower().endswith(image_extensions) and os.path.isfile(os.path.join(input_dir, f))] - - if len(all_images) < 4: - print(f"目录中图片不足四张: {input_dir}") + # 获取目录中的所有文件 + try: + all_files = os.listdir(input_dir) + logging.info(f"Files found in directory: {all_files}") + except Exception as e: + logging.exception(f"Error listing directory {input_dir}: {e}") return None + + # 过滤图片文件 + all_images_names = [f for f in all_files + if f.lower().endswith(image_extensions) and os.path.isfile(os.path.join(input_dir, f))] + logging.info(f"Filtered image files: {all_images_names}") - # 根据不同样式,可能需要的图片数量不同 + if not all_images_names: + logging.warning(f"No valid image files found in directory: {input_dir}") + return None # Return None if no images found + + # 根据不同样式,确定需要的图片数量 + # ... (logic for num_images based on style) ... num_images = 4 if style == "mosaic": num_images = 9 elif style == "filmstrip": num_images = 5 elif style == "fullscreen": - num_images = 6 # 全覆盖样式使用6张图片 + num_images = 6 elif style == "vertical_stack": - num_images = 2 # 上下拼图样式只需要2张图片 + num_images = 2 + logging.info(f"Style '{style}' requires {num_images} images.") - # 确保有足够的图像 - if len(all_images) < num_images: - print(f"样式'{style}'需要至少{num_images}张图片,但目录只有{len(all_images)}张") - # 多次使用相同图片 - if len(all_images) > 0: - all_images = all_images * (num_images // len(all_images) + 1) + # 确保有足够的图像 (或重复使用) + selected_images_names = [] + if len(all_images_names) < num_images: + logging.warning(f"Need {num_images} images for style '{style}', but only found {len(all_images_names)}. Will repeat images.") + if len(all_images_names) > 0: + # Repeat available images to meet the count + selected_images_names = (all_images_names * (num_images // len(all_images_names) + 1))[:num_images] + else: + logging.error("Cannot select images, none were found.") # Should not happen due to earlier check + return None + else: + # 随机选择指定数量的图片 + selected_images_names = random.sample(all_images_names, num_images) - # 随机选择指定数量的图片 - selected_images = random.sample(all_images, num_images) - print(f"随机选择的图片: {selected_images}") - - # 创建空白画布 - collage_image = Image.new('RGBA', target_size, (0, 0, 0, 0)) + logging.info(f"Selected image files for collage: {selected_images_names}") # 加载图片 images = [] - for img_name in selected_images: + loaded_image_paths = set() + for img_name in selected_images_names: img_path = os.path.join(input_dir, img_name) try: img = Image.open(img_path).convert('RGBA') images.append(img) - print(f"已加载图片: {img_name}") + loaded_image_paths.add(img_path) + logging.info(f"Successfully loaded image: {img_path}") except Exception as e: - print(f"加载图片 {img_name} 时出错: {e}") - # 如果某张图片加载失败,随机选择另一张图片代替 - remaining_images = [f for f in all_images if f not in selected_images] - if remaining_images: - replacement = random.choice(remaining_images) - selected_images.append(replacement) - try: - replacement_path = os.path.join(input_dir, replacement) - replacement_img = Image.open(replacement_path).convert('RGBA') - images.append(replacement_img) - print(f"使用替代图片: {replacement}") - except: - print(f"替代图片 {replacement} 也加载失败") + logging.error(f"Failed to load image {img_path}: {e}", exc_info=True) # Log exception info + # Optionally: try to replace failed image (or just log and continue) + # For simplicity now, just log and continue; the check below handles insufficient images. - # 确保图片数量足够 - while len(images) < num_images: - if images: - images.append(random.choice(images).copy()) - else: - print("没有可用的图片来创建拼贴画") - return None + # 再次检查实际加载成功的图片数量 + if len(images) < num_images: + logging.error(f"Needed {num_images} images, but only successfully loaded {len(images)}. Cannot create collage.") + # Log which images failed if possible (from error logs above) + return None + + logging.info(f"Successfully loaded {len(images)} images for collage.") + + # 创建空白画布 (moved after image loading success check) + # collage_image = Image.new('RGBA', target_size, (0, 0, 0, 0)) # This line seems unused as styles create their own canvas # 应用所选样式 + logging.info(f"Applying style '{style}'...") + result_collage = None if style == "grid_2x2": - return self._create_grid_2x2_collage(images, target_size) + result_collage = self._create_grid_2x2_collage(images, target_size) + # ... (elif for all other styles) ... elif style == "asymmetric": - return self._create_asymmetric_collage(images, target_size) + result_collage = self._create_asymmetric_collage(images, target_size) elif style == "filmstrip": - return self._create_filmstrip_collage(images, target_size) - elif style == "circles": - return self._create_circles_collage(images, target_size) + result_collage = self._create_filmstrip_collage(images, target_size) + # elif style == "circles": + # result_collage = self._create_circles_collage(images, target_size) elif style == "polaroid": - return self._create_polaroid_collage(images, target_size) + result_collage = self._create_polaroid_collage(images, target_size) elif style == "overlap": - return self._create_overlap_collage(images, target_size) + result_collage = self._create_overlap_collage(images, target_size) elif style == "mosaic": - return self._create_mosaic_collage(images, target_size) + result_collage = self._create_mosaic_collage(images, target_size) elif style == "fullscreen": - return self._create_fullscreen_collage(images, target_size) + result_collage = self._create_fullscreen_collage(images, target_size) elif style == "vertical_stack": - return self._create_vertical_stack_collage(images, target_size) + result_collage = self._create_vertical_stack_collage(images, target_size) else: - # 默认使用2x2网格 - return self._create_grid_2x2_collage(images, target_size) + logging.warning(f"Unknown style '{style}', defaulting to grid_2x2.") + result_collage = self._create_grid_2x2_collage(images, target_size) + + if result_collage is None: + logging.error(f"Collage creation failed during style application ('{style}').") + return None + else: + logging.info(f"--- Collage Creation Successful for Directory: {input_dir} ---") + return result_collage # Return the created collage image except Exception as e: - print(f"创建拼贴画时出错: {str(e)}") - traceback.print_exc() + logging.exception(f"An unexpected error occurred during collage creation for {input_dir}: {e}") # Log full traceback return None def _create_grid_2x2_collage(self, images, target_size): @@ -689,45 +708,49 @@ class ImageCollageCreator: def process_directory(directory_path, target_size=(900, 1200), output_count=1): """ - Processes images in a directory: finds main subject, adjusts contrast/saturation, - performs smart cropping/resizing, creates a collage, and returns PIL Image objects. - - Args: - directory_path: Path to the directory containing images. - target_size: Tuple (width, height) for the final collage. - output_count: Number of collages to generate. - - Returns: - A list containing the generated PIL Image objects for the collages, - or an empty list if processing fails. - """ - image_files = [os.path.join(directory_path, f) for f in os.listdir(directory_path) - if f.lower().endswith(('.png', '.jpg', '.jpeg'))] + 处理指定目录中的图片,创建指定数量的拼贴图。 - if not image_files: - print(f"No images found in {directory_path}") - return [] - - # Create collage + 参数: + directory_path: 包含图片的目录路径 + target_size: 拼贴图目标尺寸,默认为 (900, 1200) + output_count: 需要生成的拼贴图数量,默认为 1 + + 返回: + list: 生成的拼贴图列表(PIL.Image 对象);如果生成失败,返回空列表 + """ + logging.info(f"处理目录中的图片并创建 {output_count} 个拼贴图: {directory_path}") + + # 创建 ImageCollageCreator 实例 + collage_creator = ImageCollageCreator() collage_images = [] + + # 检查目录是否存在 + if not os.path.exists(directory_path): + logging.error(f"目录不存在: {directory_path}") + return [] + + # 尝试创建请求数量的拼贴图 for i in range(output_count): - collage = create_collage(image_files, target_size) - if collage: - # collage_filename = f"collage_{i}.png" - # save_path = os.path.join(output_dir, collage_filename) - # collage.save(save_path) - # print(f"Collage saved to {save_path}") - # collage_images.append({'path': save_path, 'image': collage}) - collage_images.append(collage) # Return the PIL Image object directly - else: - print(f"Failed to create collage {i}") - + try: + # 随机选择一个样式(由 create_collage_with_style 内部实现) + # 传入 None 作为 style 参数,让函数内部随机选择 + collage = collage_creator.create_collage_with_style( + directory_path, + style=None, # 让方法内部随机选择样式 + target_size=target_size + ) + + if collage: + collage_images.append(collage) + logging.info(f"成功创建拼贴图 {i+1}/{output_count}") + else: + logging.error(f"无法创建拼贴图 {i+1}/{output_count}") + except Exception as e: + logging.exception(f"创建拼贴图 {i+1}/{output_count} 时发生异常: {e}") + + logging.info(f"已处理目录 {directory_path},成功创建 {len(collage_images)}/{output_count} 个拼贴图") return collage_images -def create_collage(image_paths, target_size=(900, 1200)): - # ... (internal logic, including find_main_subject, adjust_image, smart_crop_and_resize) ... - pass - def find_main_subject(image): # ... (keep the existing implementation) ... pass @@ -741,13 +764,64 @@ def smart_crop_and_resize(image, target_aspect_ratio): pass def main(): - # 设置基础路径 - base_path = "/root/autodl-tmp" - # 默认图片目录 - input_dir = os.path.join(base_path, "陈家祠") - ## 考虑一下,是否需要直接传递图片结果 - # 处理目录中的图片,生成10个随机风格拼贴画 - process_directory(input_dir, output_count=10) - + """展示如何使用 ImageCollageCreator 和 process_directory 函数的示例。""" + logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s') + + # 示例目录路径 - 根据实际情况修改 + test_directory = "/root/autodl-tmp/sanming_img/modify/古田会议旧址" # 修改为你实际的图片目录 + + logging.info(f"测试目录: {test_directory}") + + # 方法 1: 使用 process_directory 函数 (推荐用于外部调用) + logging.info("方法 1: 使用 process_directory 函数生成拼贴图...") + collages_1 = process_directory( + directory_path=test_directory, + target_size=(900, 1200), # 默认 3:4 比例 + output_count=2 # 创建 2 张不同的拼贴图 + ) + + if collages_1: + logging.info(f"成功创建了 {len(collages_1)} 张拼贴图 (使用 process_directory)") + # 可选: 保存图片到文件 + for i, collage in enumerate(collages_1): + output_path = f"/tmp/collage_method1_{i}.png" + collage.save(output_path) + logging.info(f"拼贴图已保存到: {output_path}") + else: + logging.error("使用 process_directory 创建拼贴图失败") + + # 方法 2: 直接使用 ImageCollageCreator 类 (用于更精细的控制) + logging.info("方法 2: 直接使用 ImageCollageCreator 类...") + creator = ImageCollageCreator() + + # 指定样式创建拼贴图 (可选样式: grid_2x2, asymmetric, filmstrip, overlap, mosaic, fullscreen, vertical_stack) + styles_to_try = ["grid_2x2", "overlap", "mosaic"] + collages_2 = [] + + for style in styles_to_try: + logging.info(f"尝试使用样式: {style}") + collage = creator.create_collage_with_style( + input_dir=test_directory, + style=style, + target_size=(800, 1000) # 自定义尺寸 + ) + + if collage: + collages_2.append(collage) + # 可选: 保存图片到文件 + output_path = f"/tmp/collage_method2_{style}.png" + collage.save(output_path) + logging.info(f"使用样式 '{style}' 的拼贴图已保存到: {output_path}") + else: + logging.error(f"使用样式 '{style}' 创建拼贴图失败") + + logging.info(f"总共成功创建了 {len(collages_2)} 张拼贴图 (使用 ImageCollageCreator)") + + # 比较两种方法 + logging.info("===== 拼贴图创建测试完成 =====") + logging.info(f"方法 1 (process_directory): {len(collages_1)} 张拼贴图") + logging.info(f"方法 2 (直接使用 ImageCollageCreator): {len(collages_2)} 张拼贴图") + if __name__ == "__main__": main() diff --git a/poster_gen_config.json b/poster_gen_config.json index 862f45c..c313917 100644 --- a/poster_gen_config.json +++ b/poster_gen_config.json @@ -46,7 +46,7 @@ "content_temperature": 0.3, "content_top_p": 0.4, "content_presence_penalty": 1.5, - "request_timeout": 30, + "request_timeout": 120, "max_retries": 3, "description_filename": "description.txt", "output_collage_subdir": "collage_img", diff --git a/utils/__pycache__/tweet_generator.cpython-312.pyc b/utils/__pycache__/tweet_generator.cpython-312.pyc index ff707a982129c8e29a20de48b14e348cf78bc11a..c7fa4abd4559f775b67374168413b058d0904fc2 100644 GIT binary patch delta 2530 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 JhAiZA{s9rGiR1tP diff --git a/utils/tweet_generator.py b/utils/tweet_generator.py index a5f26aa..7a25fc5 100644 --- a/utils/tweet_generator.py +++ b/utils/tweet_generator.py @@ -572,16 +572,28 @@ def generate_posters_for_topic(topic_item: dict, # --- 使用 OutputHandler 保存 Poster Config --- output_handler.handle_poster_configs(run_id, topic_index, poster_text_configs_raw) - # --- 结束使用 Handler 保存 --- + # --- 结束使用 Handler 保存 --- + + # 打印原始配置数据以进行调试 + logging.info(f"生成的海报配置数据: {poster_text_configs_raw}") - poster_config_summary = core_posterGen.PosterConfig(poster_text_configs_raw) + # 直接使用配置数据,避免通过文件读取 + if isinstance(poster_text_configs_raw, list): + poster_configs = poster_text_configs_raw + logging.info(f"直接使用生成的配置列表,包含 {len(poster_configs)} 个配置项") + else: + # 如果不是列表,尝试转换或使用PosterConfig类解析 + logging.info("生成的配置数据不是列表,使用PosterConfig类进行处理") + poster_config_summary = core_posterGen.PosterConfig(poster_text_configs_raw) + poster_configs = poster_config_summary.get_config() except Exception as e: logging.exception("Error running ContentGenerator or parsing poster configs:") traceback.print_exc() return False # Poster Generation Loop for each variant - poster_num = variants + poster_num = min(variants, len(poster_configs)) if isinstance(poster_configs, list) else variants + logging.info(f"计划生成 {poster_num} 个海报变体") any_poster_attempted = False for j_index in range(poster_num): @@ -591,7 +603,15 @@ def generate_posters_for_topic(topic_item: dict, collage_img = None # To store the generated collage PIL Image poster_img = None # To store the final poster PIL Image try: - poster_config = poster_config_summary.get_config_by_index(j_index) + # 获取当前变体的配置 + if isinstance(poster_configs, list) and j_index < len(poster_configs): + poster_config = poster_configs[j_index] + logging.info(f"使用配置数据项 {j_index+1}: {poster_config}") + else: + # 回退方案:使用PosterConfig类 + poster_config = poster_config_summary.get_config_by_index(j_index) + logging.info(f"使用PosterConfig类获取配置项 {j_index+1}") + if not poster_config: logging.warning(f"Warning: Could not get poster config for index {j_index}. Skipping.") continue @@ -627,9 +647,16 @@ def generate_posters_for_topic(topic_item: dict, } texts = poster_config.get('texts', []) if texts: - text_data["additional_texts"].append({"text": texts[0], "position": "bottom", "size_factor": 0.5}) - if len(texts) > 1 and random.random() < text_possibility: - text_data["additional_texts"].append({"text": texts[1], "position": "bottom", "size_factor": 0.5}) + # 确保文本不为空 + if texts[0]: + text_data["additional_texts"].append({"text": texts[0], "position": "bottom", "size_factor": 0.5}) + + # 添加第二个文本(如果有并且满足随机条件) + if len(texts) > 1 and texts[1] and random.random() < text_possibility: + text_data["additional_texts"].append({"text": texts[1], "position": "bottom", "size_factor": 0.5}) + + # 打印要发送的文本数据 + logging.info(f"文本数据: {text_data}") # 调用修改后的 create_poster, 接收 PIL Image poster_img = poster_gen_instance.create_poster(collage_img, text_data)