From 67722a5c721c7c32c372b0dbfaadfc87e3da50b9 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Tue, 22 Apr 2025 18:14:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E4=BC=A0=E9=80=92=E6=96=B9=E5=BC=8F,=20?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E6=9B=B4=E9=80=82=E5=90=88API=E5=B0=81?= =?UTF-8?q?=E8=A3=85=E8=B0=83=E7=94=A8=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +- core/__pycache__/posterGen.cpython-312.pyc | Bin 31625 -> 30042 bytes .../simple_collage.cpython-312.pyc | Bin 29780 -> 30203 bytes core/posterGen.py | 166 ++--- core/simple_collage.py | 93 ++- main.py | 155 +++- .../output_handler.cpython-312.pyc | Bin 0 -> 10649 bytes .../prompt_manager.cpython-312.pyc | Bin 14257 -> 14384 bytes .../tweet_generator.cpython-312.pyc | Bin 33373 -> 28029 bytes utils/output_handler.py | 145 ++++ utils/prompt_manager.py | 78 +- utils/tweet_generator.py | 700 ++++++++---------- 12 files changed, 769 insertions(+), 598 deletions(-) create mode 100644 utils/__pycache__/output_handler.cpython-312.pyc create mode 100644 utils/output_handler.py diff --git a/README.md b/README.md index b414ef0..a040faf 100644 --- a/README.md +++ b/README.md @@ -237,9 +237,9 @@ finally: **注意:** 如果你只需要最终的完整结果而不需要流式处理,可以直接调用 `ai_agent.work(...)` 方法,它会内部处理好拼接并直接返回结果字符串。 -### Planned Refactoring: Decoupling Generation and Output Handling +### Refactoring Complete: Decoupling Generation and Output Handling -To enhance the flexibility and extensibility of this tool, we are planning a refactoring effort to separate the core content/image generation logic from the output handling (currently, saving results to the local filesystem). +To enhance the flexibility and extensibility of this tool, a refactoring effort has been completed to separate the core content/image generation logic from the output handling (previously, saving results directly to the local filesystem). **Motivation:** @@ -247,27 +247,29 @@ To enhance the flexibility and extensibility of this tool, we are planning a ref * **Alternative Storage:** Enable saving results to different backends like databases, cloud storage (e.g., S3, OSS), etc. * **Modularity:** Improve code structure by separating concerns. -**Approach:** +**Approach Taken:** -1. **Modify Core Functions:** Functions responsible for generating topics, content, and posters (primarily in `utils/tweet_generator.py` and potentially `core` modules) will be updated to **return** the generated data (e.g., Python dictionaries, lists, PIL Image objects, or image bytes) rather than directly writing files to the `./result` directory. -2. **Introduce Output Handlers:** An "Output Handler" pattern will be implemented. - * An abstract base class or interface (`OutputHandler`) will define methods for processing different types of results (topics, content, configurations, images). - * An initial concrete implementation (`FileSystemOutputHandler`) will replicate the current behavior of saving all results to the `./result/{run_id}/...` directory structure. -3. **Update Main Workflow:** The main script (`main.py`) will be modified to: - * Instantiate a specific `OutputHandler` (initially the `FileSystemOutputHandler`). - * Call the generation functions to get the data. - * Pass the returned data to the `OutputHandler` instance for processing (e.g., saving). +1. **Modified Core Functions:** Functions responsible for generating topics, content, and posters (primarily in `utils/tweet_generator.py`, `core/simple_collage.py`, `core/posterGen.py`) have been updated: + * Topic generation now returns the generated data (`run_id`, `topics_list`, prompts). + * Image generation functions (`simple_collage.process_directory`, `posterGen.create_poster`) now return PIL Image objects instead of saving files. + * Content and poster generation workflows accept an `OutputHandler` instance to process results (content JSON, prompts, configurations, image data) immediately after generation. +2. **Introduced Output Handlers:** An "Output Handler" pattern has been implemented (`utils/output_handler.py`). + * An abstract base class (`OutputHandler`) defines methods for processing different types of results. + * A concrete implementation (`FileSystemOutputHandler`) replicates the original behavior of saving all results to the `./result/{run_id}/...` directory structure. +3. **Updated Main Workflow:** The main script (`main.py`) now: + * Instantiates a specific `OutputHandler` (currently `FileSystemOutputHandler`). + * Calls the generation functions, passing the `OutputHandler` where needed. + * Uses the `OutputHandler` to process data returned by the topic generation step. +4. **Reduced Config Dependency:** Core logic functions (`PromptManager`, `generate_content_for_topic`, `generate_posters_for_topic`, etc.) now receive necessary configuration values as specific parameters rather than relying on the entire `config` dictionary, making them more independent and testable. **Future Possibilities:** -This refactoring will make it straightforward to add new output handlers in the future, such as: +This refactoring makes it straightforward to add new output handlers in the future, such as: * `ApiOutputHandler`: Formats results for API responses. * `DatabaseOutputHandler`: Stores results in a database. * `CloudStorageOutputHandler`: Uploads results (especially images) to cloud storage and potentially stores metadata elsewhere. -**(Note:** This refactoring is in progress. The current documentation reflects the existing file-saving behavior, but expect changes in how results are handled internally.) - ### 配置文件说明 (Configuration) 主配置文件为 `poster_gen_config.json` (可以复制 `example_config.json` 并修改)。主要包含以下部分: \ No newline at end of file diff --git a/core/__pycache__/posterGen.cpython-312.pyc b/core/__pycache__/posterGen.cpython-312.pyc index 8ae523a6d7be31742de5e079a56872870af5e1c8..910007a82d05fc1dc1b2326a059718f2340181c3 100644 GIT binary patch delta 4054 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 delta 2790 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/prompt_manager.cpython-312.pyc b/utils/__pycache__/prompt_manager.cpython-312.pyc index 898dd978d1fb57d498033812841448f81f087de7..1039bfb65bb77f0229102cf115ade2d58a3ecbab 100644 GIT binary patch delta 5878 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 delta 5708 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 diff --git a/utils/__pycache__/tweet_generator.cpython-312.pyc b/utils/__pycache__/tweet_generator.cpython-312.pyc index b6849f75e3cd082a169d128f29131af812e67ed8..ff707a982129c8e29a20de48b14e348cf78bc11a 100644 GIT binary patch literal 28029 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 Rw~j>X0oaoUjFR)J{vRy%1t$Oi literal 33373 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 p-QOaIpWb%eXH=~+nQpODPALuJp str: + """Gets the specific directory for a run, creating it if necessary.""" + run_dir = os.path.join(self.base_output_dir, run_id) + os.makedirs(run_dir, exist_ok=True) + return run_dir + + def _get_variant_dir(self, run_id: str, topic_index: int, variant_index: int, subdir: str | None = None) -> str: + """Gets the specific directory for a variant, optionally within a subdirectory (e.g., 'poster'), creating it if necessary.""" + run_dir = self._get_run_dir(run_id) + variant_base_dir = os.path.join(run_dir, f"{topic_index}_{variant_index}") + target_dir = variant_base_dir + if subdir: + target_dir = os.path.join(variant_base_dir, subdir) + os.makedirs(target_dir, exist_ok=True) + return target_dir + + def handle_topic_results(self, run_id: str, topics_list: list, system_prompt: str, user_prompt: str): + run_dir = self._get_run_dir(run_id) + + # Save topics list + topics_path = os.path.join(run_dir, f"tweet_topic_{run_id}.json") + try: + with open(topics_path, "w", encoding="utf-8") as f: + json.dump(topics_list, f, ensure_ascii=False, indent=4) + logging.info(f"Topics list saved successfully to: {topics_path}") + except Exception as e: + logging.exception(f"Error saving topic JSON file to {topics_path}:") + + # Save prompts + prompt_path = os.path.join(run_dir, f"topic_prompt_{run_id}.txt") + try: + with open(prompt_path, "w", encoding="utf-8") as f: + f.write("--- SYSTEM PROMPT ---\n") + f.write(system_prompt + "\n\n") + f.write("--- USER PROMPT ---\n") + f.write(user_prompt + "\n") + logging.info(f"Topic prompts saved successfully to: {prompt_path}") + except Exception as e: + logging.exception(f"Error saving topic prompts file to {prompt_path}:") + + def handle_content_variant(self, run_id: str, topic_index: int, variant_index: int, content_data: dict, prompt_data: str): + """Saves content JSON and prompt for a specific variant.""" + variant_dir = self._get_variant_dir(run_id, topic_index, variant_index) + + # Save content JSON + content_path = os.path.join(variant_dir, "article.json") + try: + with open(content_path, "w", encoding="utf-8") as f: + json.dump(content_data, f, ensure_ascii=False, indent=4) + logging.info(f"Content JSON saved to: {content_path}") + except Exception as e: + logging.exception(f"Failed to save content JSON to {content_path}: {e}") + + # Save content prompt + prompt_path = os.path.join(variant_dir, "tweet_prompt.txt") + try: + with open(prompt_path, "w", encoding="utf-8") as f: + # Assuming prompt_data is the user prompt used for this variant + f.write(prompt_data + "\n") + logging.info(f"Content prompt saved to: {prompt_path}") + except Exception as e: + logging.exception(f"Failed to save content prompt to {prompt_path}: {e}") + + def handle_poster_configs(self, run_id: str, topic_index: int, config_data: list | dict): + """Saves the complete poster configuration list/dict for a topic.""" + run_dir = self._get_run_dir(run_id) + config_path = os.path.join(run_dir, f"topic_{topic_index}_poster_configs.json") + try: + with open(config_path, 'w', encoding='utf-8') as f_cfg_topic: + json.dump(config_data, f_cfg_topic, ensure_ascii=False, indent=4) + logging.info(f"Saved complete poster configurations for topic {topic_index} to: {config_path}") + except Exception as save_err: + logging.error(f"Failed to save complete poster configurations for topic {topic_index} to {config_path}: {save_err}") + + def handle_generated_image(self, run_id: str, topic_index: int, variant_index: int, image_type: str, image_data, output_filename: str): + """Saves a generated image (PIL Image) to the appropriate variant subdirectory.""" + subdir = None + if image_type == 'collage': + subdir = 'collage_img' # TODO: Make these subdir names configurable? + elif image_type == 'poster': + subdir = 'poster' + else: + logging.warning(f"Unknown image type '{image_type}'. Saving to variant root.") + subdir = None # Save directly in variant dir if type is unknown + + target_dir = self._get_variant_dir(run_id, topic_index, variant_index, subdir=subdir) + save_path = os.path.join(target_dir, output_filename) + + try: + # Assuming image_data is a PIL Image object based on posterGen/simple_collage + image_data.save(save_path) + logging.info(f"Saved {image_type} image to: {save_path}") + except Exception as e: + logging.exception(f"Failed to save {image_type} image to {save_path}: {e}") + + def finalize(self, run_id: str): + logging.info(f"FileSystemOutputHandler finalizing run: {run_id}. No specific actions needed.") + pass # Nothing specific to do for file system finalize \ No newline at end of file diff --git a/utils/prompt_manager.py b/utils/prompt_manager.py index 2a9fd2e..383fb69 100644 --- a/utils/prompt_manager.py +++ b/utils/prompt_manager.py @@ -12,49 +12,54 @@ from .resource_loader import ResourceLoader # Use relative import within the sam class PromptManager: """Handles the loading and construction of prompts.""" - def __init__(self, config): - """Initializes the PromptManager with the global configuration.""" - self.config = config - # Instantiate ResourceLoader once, assuming it's mostly static methods or stateless - self.resource_loader = ResourceLoader() + def __init__(self, + topic_system_prompt_path: str, + topic_user_prompt_path: str, + content_system_prompt_path: str, + prompts_dir: str, + resource_dir_config: list, + topic_gen_num: int = 1, # Default values if needed + topic_gen_date: str = "" + ): + self.topic_system_prompt_path = topic_system_prompt_path + self.topic_user_prompt_path = topic_user_prompt_path + self.content_system_prompt_path = content_system_prompt_path + self.prompts_dir = prompts_dir + self.resource_dir_config = resource_dir_config + self.topic_gen_num = topic_gen_num + self.topic_gen_date = topic_gen_date def get_topic_prompts(self): """Constructs the system and user prompts for topic generation.""" logging.info("Constructing prompts for topic generation...") try: # --- System Prompt --- - system_prompt_path = self.config.get("topic_system_prompt") - if not system_prompt_path: - logging.error("topic_system_prompt path not specified in config.") + if not self.topic_system_prompt_path: + logging.error("Topic system prompt path not provided during PromptManager initialization.") return None, None - - # Use ResourceLoader's static method directly - system_prompt = ResourceLoader.load_file_content(system_prompt_path) + system_prompt = ResourceLoader.load_file_content(self.topic_system_prompt_path) if not system_prompt: - logging.error(f"Failed to load topic system prompt from '{system_prompt_path}'.") + logging.error(f"Failed to load topic system prompt from '{self.topic_system_prompt_path}'.") return None, None # --- User Prompt --- - user_prompt_path = self.config.get("topic_user_prompt") - if not user_prompt_path: - logging.error("topic_user_prompt path not specified in config.") + if not self.topic_user_prompt_path: + logging.error("Topic user prompt path not provided during PromptManager initialization.") return None, None - - base_user_prompt = ResourceLoader.load_file_content(user_prompt_path) - if base_user_prompt is None: # Check for None explicitly - logging.error(f"Failed to load base topic user prompt from '{user_prompt_path}'.") + base_user_prompt = ResourceLoader.load_file_content(self.topic_user_prompt_path) + if base_user_prompt is None: + logging.error(f"Failed to load base topic user prompt from '{self.topic_user_prompt_path}'.") return None, None # --- Build the dynamic part of the user prompt (Logic moved from prepare_topic_generation) --- user_prompt_dynamic = "你拥有的创作资料如下:\n" # Add genPrompts directory structure - gen_prompts_path = self.config.get("prompts_dir") - if gen_prompts_path and os.path.isdir(gen_prompts_path): + if self.prompts_dir and os.path.isdir(self.prompts_dir): try: - gen_prompts_list = os.listdir(gen_prompts_path) + gen_prompts_list = os.listdir(self.prompts_dir) for gen_prompt_folder in gen_prompts_list: - folder_path = os.path.join(gen_prompts_path, gen_prompt_folder) + folder_path = os.path.join(self.prompts_dir, gen_prompt_folder) if os.path.isdir(folder_path): try: # List files, filter out subdirs if needed @@ -63,13 +68,12 @@ class PromptManager: except OSError as e: logging.warning(f"Could not list directory {folder_path}: {e}") except OSError as e: - logging.warning(f"Could not list base prompts directory {gen_prompts_path}: {e}") + logging.warning(f"Could not list base prompts directory {self.prompts_dir}: {e}") else: - logging.warning(f"Prompts directory '{gen_prompts_path}' not found or invalid.") + logging.warning(f"Prompts directory '{self.prompts_dir}' not found or invalid.") # Add resource directory contents - resource_dir_config = self.config.get("resource_dir", []) - for dir_info in resource_dir_config: + for dir_info in self.resource_dir_config: source_type = dir_info.get("type", "UnknownType") source_file_paths = dir_info.get("file_path", []) for file_path in source_file_paths: @@ -81,7 +85,7 @@ class PromptManager: logging.warning(f"Could not load resource file {file_path}") # Add dateline information (optional) - user_prompt_dir = os.path.dirname(user_prompt_path) + user_prompt_dir = os.path.dirname(self.topic_user_prompt_path) dateline_path = os.path.join(user_prompt_dir, "2025各月节日宣传节点时间表.md") # Consider making this configurable if os.path.exists(dateline_path): dateline_content = ResourceLoader.load_file_content(dateline_path) @@ -91,9 +95,7 @@ class PromptManager: # Combine dynamic part, base template, and final parameters user_prompt = user_prompt_dynamic + base_user_prompt - select_num = self.config.get("num", 1) # Default to 1 if not specified - select_date = self.config.get("date", "") - user_prompt += f"\n选题数量:{select_num}\n选题日期:{select_date}\n" + user_prompt += f"\n选题数量:{self.topic_gen_num}\n选题日期:{self.topic_gen_date}\n" # --- End of moved logic --- logging.info(f"Topic prompts constructed. System: {len(system_prompt)} chars, User: {len(user_prompt)} chars.") @@ -108,20 +110,18 @@ class PromptManager: logging.info(f"Constructing content prompts for topic: {topic_item.get('object', 'N/A')}...") try: # --- System Prompt --- - system_prompt_path = self.config.get("content_system_prompt") - if not system_prompt_path: - logging.error("content_system_prompt path not specified in config.") + if not self.content_system_prompt_path: + logging.error("Content system prompt path not provided during PromptManager initialization.") return None, None - # Use ResourceLoader's static method. load_system_prompt was just load_file_content. - system_prompt = ResourceLoader.load_file_content(system_prompt_path) + system_prompt = ResourceLoader.load_file_content(self.content_system_prompt_path) if not system_prompt: - logging.error(f"Failed to load content system prompt from '{system_prompt_path}'.") + logging.error(f"Failed to load content system prompt from '{self.content_system_prompt_path}'.") return None, None # --- User Prompt (Logic moved from ResourceLoader.build_user_prompt) --- user_prompt = "" - prompts_dir = self.config.get("prompts_dir") - resource_dir_config = self.config.get("resource_dir", []) + prompts_dir = self.prompts_dir + resource_dir_config = self.resource_dir_config if not prompts_dir or not os.path.isdir(prompts_dir): logging.warning(f"Prompts directory '{prompts_dir}' not found or invalid. Content user prompt might be incomplete.") diff --git a/utils/tweet_generator.py b/utils/tweet_generator.py index 7ca1ee6..a5f26aa 100644 --- a/utils/tweet_generator.py +++ b/utils/tweet_generator.py @@ -26,6 +26,7 @@ from utils.prompt_manager import PromptManager # Keep this as it's importing fro from core import contentGen as core_contentGen from core import posterGen as core_posterGen from core import simple_collage as core_simple_collage +from .output_handler import OutputHandler # <-- 添加导入 class tweetTopic: def __init__(self, index, date, logic, object, product, product_logic, style, style_logic, target_audience, target_audience_logic): @@ -41,60 +42,29 @@ class tweetTopic: self.target_audience_logic = target_audience_logic class tweetTopicRecord: - def __init__(self, topics_list, system_prompt, user_prompt, output_dir, run_id): + def __init__(self, topics_list, system_prompt, user_prompt, run_id): self.topics_list = topics_list self.system_prompt = system_prompt self.user_prompt = user_prompt - self.output_dir = output_dir self.run_id = run_id - def save_topics(self, path): - try: - with open(path, "w", encoding="utf-8") as f: - json.dump(self.topics_list, f, ensure_ascii=False, indent=4) - logging.info(f"Topics list successfully saved to {path}") # Change to logging - except Exception as e: - # Keep print for traceback, but add logging - logging.exception(f"保存选题失败到 {path}: {e}") # Log exception - # print(f"保存选题失败到 {path}: {e}") - # print("--- Traceback for save_topics error ---") - # traceback.print_exc() - # print("--- End Traceback ---") - return False - return True - - def save_prompt(self, path): - try: - with open(path, "w", encoding="utf-8") as f: - f.write(self.system_prompt + "\n") - f.write(self.user_prompt + "\n") - # f.write(self.output_dir + "\n") # Output dir not needed in prompt file? - # f.write(self.run_id + "\n") # run_id not needed in prompt file? - logging.info(f"Prompts saved to {path}") - except Exception as e: - logging.exception(f"保存提示词失败: {e}") - # print(f"保存提示词失败: {e}") - return False - return True - class tweetContent: - def __init__(self, result, prompt, output_dir, run_id, article_index, variant_index): + def __init__(self, result, prompt, run_id, article_index, variant_index): self.result = result self.prompt = prompt - self.output_dir = output_dir self.run_id = run_id self.article_index = article_index self.variant_index = variant_index try: self.title, self.content = self.split_content(result) - self.json_file = self.gen_result_json() + self.json_data = self.gen_result_json() except Exception as e: logging.error(f"Failed to parse AI result for {article_index}_{variant_index}: {e}") logging.debug(f"Raw result: {result[:500]}...") # Log partial raw result self.title = "[Parsing Error]" self.content = "[Failed to parse AI content]" - self.json_file = {"title": self.title, "content": self.content, "error": True, "raw_result": result} + self.json_data = {"title": self.title, "content": self.content, "error": True, "raw_result": result} def split_content(self, result): # Assuming split logic might still fail, keep it simple or improve with regex/json @@ -122,28 +92,19 @@ class tweetContent: "title": self.title, "content": self.content } + # Add error flag if it exists + if hasattr(self, 'json_data') and self.json_data.get('error'): + json_file['error'] = True + json_file['raw_result'] = self.json_data.get('raw_result') return json_file - def save_content(self, json_path): - try: - with open(json_path, "w", encoding="utf-8") as f: - # If parsing failed, save the error structure - json.dump(self.json_file, f, ensure_ascii=False, indent=4) - logging.info(f"Content JSON saved to: {json_path}") - except Exception as e: - logging.exception(f"Failed to save content JSON to {json_path}: {e}") - return None # Indicate failure - return json_path + def get_json_data(self): + """Returns the generated JSON data dictionary.""" + return self.json_data - def save_prompt(self, path): - try: - with open(path, "w", encoding="utf-8") as f: - f.write(self.prompt + "\n") - logging.info(f"Content prompt saved to: {path}") - except Exception as e: - logging.exception(f"Failed to save content prompt to {path}: {e}") - return None # Indicate failure - return path + def get_prompt(self): + """Returns the user prompt used to generate this content.""" + return self.prompt def get_content(self): return self.content @@ -151,12 +112,9 @@ class tweetContent: def get_title(self): return self.title - def get_json_file(self): - return self.json_file - -def generate_topics(ai_agent, system_prompt, user_prompt, output_dir, run_id, temperature=0.2, top_p=0.5, presence_penalty=1.5): - """生成选题列表 (run_id is now passed in)""" +def generate_topics(ai_agent, system_prompt, user_prompt, run_id, temperature=0.2, top_p=0.5, presence_penalty=1.5): + """生成选题列表 (run_id is now passed in, output_dir removed as argument)""" logging.info("Starting topic generation...") time_start = time.time() @@ -171,26 +129,20 @@ def generate_topics(ai_agent, system_prompt, user_prompt, output_dir, run_id, te result_list = TopicParser.parse_topics(result) if not result_list: logging.warning("Topic parsing resulted in an empty list.") - # Optionally save raw result here if parsing fails? - error_log_path = os.path.join(output_dir, run_id, f"topic_parsing_error_{run_id}.txt") - try: - os.makedirs(os.path.dirname(error_log_path), exist_ok=True) - with open(error_log_path, "w", encoding="utf-8") as f_err: - f_err.write("--- Topic Parsing Failed ---\n") - f_err.write(result) - logging.info(f"Saved raw AI output due to parsing failure to: {error_log_path}") - except Exception as log_err: - logging.error(f"Failed to save raw AI output on parsing failure: {log_err}") + # Optionally handle raw result logging here if needed, but saving is responsibility of OutputHandler + # error_log_path = os.path.join(output_dir, run_id, f"topic_parsing_error_{run_id}.txt") # output_dir is not available here + # try: + # # ... (save raw output logic) ... + # except Exception as log_err: + # logging.error(f"Failed to save raw AI output on parsing failure: {log_err}") - # Create record object (even if list is empty) - tweet_topic_record = tweetTopicRecord(result_list, system_prompt, user_prompt, output_dir, run_id) - - return tweet_topic_record # Return only the record + # 直接返回解析后的列表 + return result_list -def generate_single_content(ai_agent, system_prompt, user_prompt, item, output_dir, run_id, +def generate_single_content(ai_agent, system_prompt, user_prompt, item, run_id, article_index, variant_index, temperature=0.3, top_p=0.4, presence_penalty=1.5): - """生成单篇文章内容. Requires prompts to be passed in.""" + """Generates single content variant data. Returns (content_json, user_prompt) or (None, None).""" logging.info(f"Generating content for topic {article_index}, variant {variant_index}") try: if not system_prompt or not user_prompt: @@ -201,29 +153,37 @@ def generate_single_content(ai_agent, system_prompt, user_prompt, item, output_d time.sleep(random.random() * 0.5) - # Generate content (updated return values) + # Generate content (non-streaming work returns result, tokens, time_cost) result, tokens, time_cost = ai_agent.work( system_prompt, user_prompt, "", temperature, top_p, presence_penalty ) + if result is None: # Check if AI call failed + logging.error(f"AI agent work failed for {article_index}_{variant_index}. No result returned.") + return None, None + logging.info(f"Content generation for {article_index}_{variant_index} completed in {time_cost:.2f}s. Estimated tokens: {tokens}") - # --- Correct directory structure --- - run_specific_output_dir = os.path.join(output_dir, run_id) - variant_result_dir = os.path.join(run_specific_output_dir, f"{article_index}_{variant_index}") - os.makedirs(variant_result_dir, exist_ok=True) + # --- Create tweetContent object (handles parsing) --- + # Pass user_prompt instead of full prompt? Yes, user_prompt is what we need later. + tweet_content = tweetContent(result, user_prompt, run_id, article_index, variant_index) - # Create tweetContent object (handles potential parsing errors inside its __init__) - tweet_content = tweetContent(result, user_prompt, output_dir, run_id, article_index, variant_index) - - # Save content and prompt - content_save_path = os.path.join(variant_result_dir, "article.json") - prompt_save_path = os.path.join(variant_result_dir, "tweet_prompt.txt") - tweet_content.save_content(content_save_path) - tweet_content.save_prompt(prompt_save_path) - # logging.info(f" Saved article content to: {content_save_path}") # Already logged in save_content + # --- Remove Saving Logic --- + # run_specific_output_dir = os.path.join(output_dir, run_id) # output_dir no longer available + # variant_result_dir = os.path.join(run_specific_output_dir, f"{article_index}_{variant_index}") + # os.makedirs(variant_result_dir, exist_ok=True) + # content_save_path = os.path.join(variant_result_dir, "article.json") + # prompt_save_path = os.path.join(variant_result_dir, "tweet_prompt.txt") + # tweet_content.save_content(content_save_path) # Method removed + # tweet_content.save_prompt(prompt_save_path) # Method removed + # --- End Remove Saving Logic --- + + # Return the data needed by the output handler + content_json = tweet_content.get_json_data() + prompt_data = tweet_content.get_prompt() # Get the stored user prompt + + return content_json, prompt_data # Return data pair - return tweet_content, result # Return object and raw result except Exception as e: logging.exception(f"Error generating single content for {article_index}_{variant_index}:") return None, None @@ -256,7 +216,7 @@ def generate_content(ai_agent, system_prompt, topics, output_dir, run_id, prompt # 调用单篇文章生成函数 tweet_content, result = generate_single_content( - ai_agent, system_prompt, item, output_dir, run_id, i+1, j+1, temperature + ai_agent, system_prompt, item, run_id, i+1, j+1, temperature ) if tweet_content: @@ -270,207 +230,248 @@ def generate_content(ai_agent, system_prompt, topics, output_dir, run_id, prompt return processed_results -def prepare_topic_generation( - config # Pass the whole config dictionary now - # select_date, select_num, - # system_prompt_path, user_prompt_path, - # base_url="vllm", model_name="qwenQWQ", api_key="EMPTY", - # gen_prompts_path="/root/autodl-tmp/TravelContentCreator/genPrompts", - # resource_dir="/root/autodl-tmp/TravelContentCreator/resource", - # output_dir="/root/autodl-tmp/TravelContentCreator/result" -): - """准备选题生成的环境和参数. Returns agent and prompts.""" +def prepare_topic_generation(prompt_manager: PromptManager, + api_url: str, + model_name: str, + api_key: str, + timeout: int, + max_retries: int): + """准备选题生成的环境和参数. Returns agent, system_prompt, user_prompt. - # Initialize PromptManager - prompt_manager = PromptManager(config) - - # Get prompts using PromptManager + Args: + prompt_manager: An initialized PromptManager instance. + api_url, model_name, api_key, timeout, max_retries: Parameters for AI_Agent. + """ + logging.info("Preparing for topic generation (using provided PromptManager)...") + # 从传入的 PromptManager 获取 prompts system_prompt, user_prompt = prompt_manager.get_topic_prompts() if not system_prompt or not user_prompt: - print("Error: Failed to get topic generation prompts.") - return None, None, None, None + logging.error("Failed to get topic generation prompts from PromptManager.") + return None, None, None # Return three Nones - # 创建AI Agent (still create agent here for the topic generation phase) + # 使用传入的参数初始化 AI Agent try: logging.info("Initializing AI Agent for topic generation...") - # --- Read timeout/retry from config --- - request_timeout = config.get("request_timeout", 30) # Default 30 seconds - max_retries = config.get("max_retries", 3) # Default 3 retries - # --- Pass values to AI_Agent --- ai_agent = AI_Agent( - config["api_url"], - config["model"], - config["api_key"], - timeout=request_timeout, - max_retries=max_retries + api_url, # Use passed arg + model_name, # Use passed arg + api_key, # Use passed arg + timeout=timeout, # Use passed arg + max_retries=max_retries # Use passed arg ) except Exception as e: logging.exception("Error initializing AI Agent for topic generation:") - traceback.print_exc() - return None, None, None, None + return None, None, None # Return three Nones - # Removed prompt loading/building logic, now handled by PromptManager - - # Return agent and the generated prompts - return ai_agent, system_prompt, user_prompt, config["output_dir"] + # 返回 agent 和 prompts + return ai_agent, system_prompt, user_prompt def run_topic_generation_pipeline(config, run_id=None): - """Runs the complete topic generation pipeline based on the configuration.""" + """ + Runs the complete topic generation pipeline based on the configuration. + Returns: (run_id, topics_list, system_prompt, user_prompt) or (None, None, None, None) on failure. + """ logging.info("Starting Step 1: Topic Generation Pipeline...") - - # --- Handle run_id --- + if run_id is None: logging.info("No run_id provided, generating one based on timestamp.") run_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") else: logging.info(f"Using provided run_id: {run_id}") - # --- End run_id handling --- - # Prepare necessary inputs and the AI agent for topic generation - ai_agent, system_prompt, user_prompt, base_output_dir = None, None, None, None + ai_agent, system_prompt, user_prompt = None, None, None # Initialize + topics_list = None + prompt_manager = None # Initialize prompt_manager try: - # Pass the config directly to prepare_topic_generation - ai_agent, system_prompt, user_prompt, base_output_dir = prepare_topic_generation(config) + # --- 读取 PromptManager 所需参数 --- + topic_sys_prompt_path = config.get("topic_system_prompt") + topic_user_prompt_path = config.get("topic_user_prompt") + content_sys_prompt_path = config.get("content_system_prompt") # 虽然这里不用,但 PromptManager 可能需要 + prompts_dir_path = config.get("prompts_dir") + resource_config = config.get("resource_dir", []) + topic_num = config.get("num", 1) + topic_date = config.get("date", "") + # --- 创建 PromptManager 实例 --- + prompt_manager = PromptManager( + topic_system_prompt_path=topic_sys_prompt_path, + topic_user_prompt_path=topic_user_prompt_path, + content_system_prompt_path=content_sys_prompt_path, + prompts_dir=prompts_dir_path, + resource_dir_config=resource_config, + topic_gen_num=topic_num, + topic_gen_date=topic_date + ) + logging.info("PromptManager instance created.") + + # --- 读取 AI Agent 所需参数 --- + ai_api_url = config.get("api_url") + ai_model = config.get("model") + ai_api_key = config.get("api_key") + ai_timeout = config.get("request_timeout", 30) + ai_max_retries = config.get("max_retries", 3) + + # 检查必需的 AI 参数是否存在 + if not all([ai_api_url, ai_model, ai_api_key]): + raise ValueError("Missing required AI configuration (api_url, model, api_key) in config.") + + # --- 调用修改后的 prepare_topic_generation --- + ai_agent, system_prompt, user_prompt = prepare_topic_generation( + prompt_manager, # Pass instance + ai_api_url, + ai_model, + ai_api_key, + ai_timeout, + ai_max_retries + ) if not ai_agent or not system_prompt or not user_prompt: raise ValueError("Failed to prepare topic generation (agent or prompts missing).") - except Exception as e: - logging.exception("Error during topic generation preparation:") - traceback.print_exc() - return None, None - - # Generate topics using the prepared agent and prompts - try: - # Pass the determined run_id to generate_topics - tweet_topic_record = generate_topics( - ai_agent, system_prompt, user_prompt, config["output_dir"], - run_id, # Pass the run_id + # --- Generate topics (保持不变) --- + topics_list = generate_topics( + ai_agent, system_prompt, user_prompt, + run_id, # Pass run_id config.get("topic_temperature", 0.2), config.get("topic_top_p", 0.5), config.get("topic_presence_penalty", 1.5) ) except Exception as e: - logging.exception("Error during topic generation API call:") - traceback.print_exc() - if ai_agent: ai_agent.close() # Ensure agent is closed on error - return None, None + logging.exception("Error during topic generation pipeline execution:") + # Ensure agent is closed even if generation fails mid-way + if ai_agent: ai_agent.close() + return None, None, None, None # Signal failure + finally: + # Ensure the AI agent is closed after generation attempt (if initialized) + if ai_agent: + logging.info("Closing topic generation AI Agent...") + ai_agent.close() - # Ensure the AI agent is closed after generation - if ai_agent: - logging.info("Closing topic generation AI Agent...") - ai_agent.close() + if topics_list is None: # Check if generate_topics returned None (though it currently returns list) + logging.error("Topic generation failed (generate_topics returned None or an error occurred).") + return None, None, None, None + elif not topics_list: # Check if list is empty + logging.warning(f"Topic generation completed for run {run_id}, but the resulting topic list is empty.") + # Return empty list and prompts anyway, let caller decide - # Process results - if not tweet_topic_record: - logging.error("Topic generation failed (generate_topics returned None).") - return None, None # Return None for run_id as well if record is None + # --- Saving logic removed previously --- - # Use the determined run_id for output directory - output_dir = os.path.join(config["output_dir"], run_id) - try: - os.makedirs(output_dir, exist_ok=True) - # --- Debug: Print the data before attempting to save --- - logging.info("--- Debug: Data to be saved in tweet_topic.json ---") - logging.info(tweet_topic_record.topics_list) - logging.info("--- End Debug ---") - # --- End Debug --- - - # Save topics and prompt details - save_topics_success = tweet_topic_record.save_topics(os.path.join(output_dir, "tweet_topic.json")) - save_prompt_success = tweet_topic_record.save_prompt(os.path.join(output_dir, "tweet_prompt.txt")) - if not save_topics_success or not save_prompt_success: - logging.warning("Warning: Failed to save topic generation results or prompts.") - # Continue but warn user - - except Exception as e: - logging.exception("Error saving topic generation results:") - traceback.print_exc() - # Return the generated data even if saving fails, but maybe warn more strongly? - # return run_id, tweet_topic_record # Decide if partial success is okay - return None, None # Or consider failure if saving is critical - - logging.info(f"Topics generated successfully. Run ID: {run_id}") - # Return the determined run_id and the record - return run_id, tweet_topic_record + logging.info(f"Topic generation pipeline completed successfully. Run ID: {run_id}") + # Return the raw data needed by the OutputHandler + return run_id, topics_list, system_prompt, user_prompt # --- Decoupled Functional Units (Moved from main.py) --- -def generate_content_for_topic(ai_agent, prompt_manager, config, topic_item, output_dir, run_id, topic_index): - """Generates all content variants for a single topic item. - +def generate_content_for_topic(ai_agent: AI_Agent, + prompt_manager: PromptManager, + topic_item: dict, + run_id: str, + topic_index: int, + output_handler: OutputHandler, # Changed name to match convention + # 添加具体参数,移除 config 和 output_dir + variants: int, + temperature: float, + top_p: float, + presence_penalty: float): + """Generates all content variants for a single topic item and uses OutputHandler. + Args: - ai_agent: An initialized AI_Agent instance. - prompt_manager: An initialized PromptManager instance. - config: The global configuration dictionary. - topic_item: The dictionary representing a single topic. - output_dir: The base output directory for the entire run (e.g., ./result). - run_id: The ID for the current run. - topic_index: The 1-based index of the current topic. - + ai_agent: Initialized AI_Agent instance. + prompt_manager: Initialized PromptManager instance. + topic_item: Dictionary representing the topic. + run_id: Current run ID. + topic_index: 1-based index of the topic. + output_handler: An instance of OutputHandler to process results. + variants: Number of variants to generate. + temperature, top_p, presence_penalty: AI generation parameters. Returns: - A list of tweet content data (dictionaries) generated for the topic, - or None if generation failed. + bool: True if at least one variant was successfully generated and handled, False otherwise. """ logging.info(f"Generating content for Topic {topic_index} (Object: {topic_item.get('object', 'N/A')})...") - tweet_content_list = [] - variants = config.get("variants", 1) + success_flag = False # Track if any variant succeeded + # 使用传入的 variants 参数 + # variants = config.get("variants", 1) for j in range(variants): variant_index = j + 1 - logging.info(f"Generating Variant {variant_index}/{variants}...") + logging.info(f" Generating Variant {variant_index}/{variants}...") - # Get prompts for this specific topic item - # Assuming prompt_manager is correctly initialized and passed + # PromptManager 实例已传入,直接调用 content_system_prompt, content_user_prompt = prompt_manager.get_content_prompts(topic_item) if not content_system_prompt or not content_user_prompt: - logging.warning(f"Skipping Variant {variant_index} due to missing content prompts.") - continue # Skip this variant + logging.warning(f" Skipping Variant {variant_index} due to missing content prompts.") + continue time.sleep(random.random() * 0.5) try: - # Call the core generation function (generate_single_content is in this file) - tweet_content, gen_result = generate_single_content( + # Call generate_single_content with passed-in parameters + content_json, prompt_data = generate_single_content( ai_agent, content_system_prompt, content_user_prompt, topic_item, - output_dir, run_id, topic_index, variant_index, - config.get("content_temperature", 0.3), - config.get("content_top_p", 0.4), - config.get("content_presence_penalty", 1.5) + run_id, topic_index, variant_index, + temperature, # 使用传入的参数 + top_p, # 使用传入的参数 + presence_penalty # 使用传入的参数 ) - if tweet_content: - try: - tweet_content_data = tweet_content.get_json_file() - if tweet_content_data: - tweet_content_list.append(tweet_content_data) - else: - logging.warning(f"Warning: tweet_content.get_json_file() for Topic {topic_index}, Variant {variant_index} returned empty data.") - except Exception as parse_err: - logging.error(f"Error processing tweet content after generation for Topic {topic_index}, Variant {variant_index}: {parse_err}") + + # Check if generation succeeded and parsing was okay (or error handled within json) + if content_json is not None and prompt_data is not None: + # Use the output handler to process/save the result + output_handler.handle_content_variant( + run_id, topic_index, variant_index, content_json, prompt_data + ) + success_flag = True # Mark success for this topic + + # Check specifically if the AI result itself indicated a parsing error internally + if content_json.get("error"): + logging.error(f" Content generation for Topic {topic_index}, Variant {variant_index} succeeded but response parsing failed (error flag set in content). Raw data logged by handler.") + else: + logging.info(f" Successfully generated and handled content for Topic {topic_index}, Variant {variant_index}.") else: - logging.warning(f"Failed to generate content for Topic {topic_index}, Variant {variant_index}. Skipping.") + logging.error(f" Content generation failed for Topic {topic_index}, Variant {variant_index}. Skipping handling.") + except Exception as e: - logging.exception(f"Error during content generation for Topic {topic_index}, Variant {variant_index}:") - # traceback.print_exc() + logging.exception(f" Error during content generation call or handling for Topic {topic_index}, Variant {variant_index}:") - if not tweet_content_list: - logging.warning(f"No valid content generated for Topic {topic_index}.") - return None - else: - logging.info(f"Successfully generated {len(tweet_content_list)} content variants for Topic {topic_index}.") - return tweet_content_list + # Return the success flag for this topic + return success_flag -def generate_posters_for_topic(config, topic_item, tweet_content_list, output_dir, run_id, topic_index): - """Generates all posters for a single topic item based on its generated content. +def generate_posters_for_topic(topic_item: dict, + output_dir: str, + run_id: str, + topic_index: int, + output_handler: OutputHandler, # 添加 handler + variants: int, + poster_assets_base_dir: str, + image_base_dir: str, + modify_image_subdir: str, + resource_dir_config: list, + poster_target_size: tuple, + text_possibility: float, + output_collage_subdir: str, + output_poster_subdir: str, + output_poster_filename: str, + camera_image_subdir: str + ): + """Generates all posters for a single topic item, handling image data via OutputHandler. Args: - config: The global configuration dictionary. topic_item: The dictionary representing a single topic. - tweet_content_list: List of content data generated by generate_content_for_topic. output_dir: The base output directory for the entire run (e.g., ./result). run_id: The ID for the current run. topic_index: The 1-based index of the current topic. + variants: Number of variants. + poster_assets_base_dir: Path to poster assets (fonts, frames etc.). + image_base_dir: Base path for source images. + modify_image_subdir: Subdirectory for modified images. + resource_dir_config: Configuration for resource directories (used for Description). + poster_target_size: Target size tuple (width, height) for the poster. + text_possibility: Probability of adding secondary text. + output_collage_subdir: Subdirectory name for saving collages. + output_poster_subdir: Subdirectory name for saving posters. + output_poster_filename: Filename for the final poster. + camera_image_subdir: Subdirectory for camera images (currently unused in logic?). + output_handler: An instance of OutputHandler to process results. Returns: True if poster generation was attempted (regardless of individual variant success), @@ -478,30 +479,47 @@ def generate_posters_for_topic(config, topic_item, tweet_content_list, output_di """ logging.info(f"Generating posters for Topic {topic_index} (Object: {topic_item.get('object', 'N/A')})...") - # Initialize necessary generators here, assuming they are stateless or cheap to create - # Alternatively, pass initialized instances if they hold state or are expensive + # --- Load content data from files --- + loaded_content_list = [] + logging.info(f"Attempting to load content data for {variants} variants for topic {topic_index}...") + for j in range(variants): + variant_index = j + 1 + variant_dir = os.path.join(output_dir, run_id, f"{topic_index}_{variant_index}") + content_path = os.path.join(variant_dir, "article.json") + try: + if os.path.exists(content_path): + with open(content_path, 'r', encoding='utf-8') as f_content: + content_data = json.load(f_content) + if isinstance(content_data, dict) and 'title' in content_data and 'content' in content_data: + loaded_content_list.append(content_data) + logging.debug(f" Successfully loaded content from: {content_path}") + else: + logging.warning(f" Content file {content_path} has invalid format. Skipping.") + else: + logging.warning(f" Content file not found for variant {variant_index}: {content_path}. Skipping.") + except json.JSONDecodeError: + logging.error(f" Error decoding JSON from content file: {content_path}. Skipping.") + except Exception as e: + logging.exception(f" Error loading content file {content_path}: {e}") + + if not loaded_content_list: + logging.error(f"No valid content data loaded for topic {topic_index}. Cannot generate posters.") + return False + logging.info(f"Successfully loaded content data for {len(loaded_content_list)} variants.") + # --- End Load content data --- + + # Initialize generators using parameters try: content_gen_instance = core_contentGen.ContentGenerator() - # poster_gen_instance = core_posterGen.PosterGenerator() - # --- Read poster assets base dir from config --- - poster_assets_base_dir = config.get("poster_assets_base_dir") if not poster_assets_base_dir: - logging.error("Error: 'poster_assets_base_dir' not found in configuration. Cannot generate posters.") - return False # Cannot proceed without assets base dir - # --- Initialize PosterGenerator with the base dir --- + logging.error("Error: 'poster_assets_base_dir' not provided. Cannot generate posters.") + return False poster_gen_instance = core_posterGen.PosterGenerator(base_dir=poster_assets_base_dir) except Exception as e: logging.exception("Error initializing generators for poster creation:") return False - # --- Setup: Paths and Object Name --- - image_base_dir = config.get("image_base_dir") - if not image_base_dir: - logging.error("Error: image_base_dir missing in config for poster generation.") - return False - modify_image_subdir = config.get("modify_image_subdir", "modify") - camera_image_subdir = config.get("camera_image_subdir", "相机") - + # --- Setup: Paths and Object Name --- object_name = topic_item.get("object", "") if not object_name: logging.warning("Warning: Topic object name is missing. Cannot generate posters.") @@ -513,110 +531,95 @@ def generate_posters_for_topic(config, topic_item, tweet_content_list, output_di if not object_name_cleaned: logging.warning(f"Warning: Object name '{object_name}' resulted in empty string after cleaning. Skipping posters.") return False - object_name = object_name_cleaned # Use the cleaned name for searching + object_name = object_name_cleaned except Exception as e: logging.warning(f"Warning: Could not fully clean object name '{object_name}': {e}. Skipping posters.") return False - # Construct and check INPUT image paths (still needed for collage) - input_img_dir_path = os.path.join(image_base_dir, modify_image_subdir, object_name) + # Construct and check INPUT image paths + input_img_dir_path = os.path.join(image_base_dir, modify_image_subdir, object_name) if not os.path.exists(input_img_dir_path) or not os.path.isdir(input_img_dir_path): logging.warning(f"Warning: Modify Image directory not found or not a directory: '{input_img_dir_path}'. Skipping posters for this topic.") return False - # --- NEW: Locate Description File using resource_dir type "Description" --- + # Locate Description File using resource_dir_config parameter info_directory = [] description_file_path = None - resource_dir_config = config.get("resource_dir", []) found_description = False - for dir_info in resource_dir_config: if dir_info.get("type") == "Description": for file_path in dir_info.get("file_path", []): - # Match description file based on object name containment if object_name in os.path.basename(file_path): description_file_path = file_path if os.path.exists(description_file_path): - info_directory = [description_file_path] # Pass the found path + info_directory = [description_file_path] logging.info(f"Found and using description file from config: {description_file_path}") found_description = True else: logging.warning(f"Warning: Description file specified in config not found: {description_file_path}") - break # Found the matching entry in this list - if found_description: # Stop searching resource_dir if found + break + if found_description: break - if not found_description: logging.info(f"Warning: No matching description file found for object '{object_name}' in config resource_dir (type='Description').") - # --- End NEW Description File Logic --- - - # --- Generate Text Configurations for All Variants --- + + # Generate Text Configurations for All Variants try: - poster_text_configs_raw = content_gen_instance.run(info_directory, config["variants"], tweet_content_list) + poster_text_configs_raw = content_gen_instance.run(info_directory, variants, loaded_content_list) if not poster_text_configs_raw: logging.warning("Warning: ContentGenerator returned empty configuration data. Skipping posters.") return False - # --- Save the COMPLETE poster configs list for this topic --- - run_output_dir_base = os.path.join(output_dir, run_id) - topic_config_save_path = os.path.join(run_output_dir_base, f"topic_{topic_index}_poster_configs.json") - try: - # Assuming poster_text_configs_raw is JSON-serializable (likely a list/dict) - with open(topic_config_save_path, 'w', encoding='utf-8') as f_cfg_topic: - json.dump(poster_text_configs_raw, f_cfg_topic, ensure_ascii=False, indent=4) - logging.info(f"Saved complete poster configurations for topic {topic_index} to: {topic_config_save_path}") - except Exception as save_err: - logging.error(f"Failed to save complete poster configurations for topic {topic_index} to {topic_config_save_path}: {save_err}") - # --- End Save Complete Config --- + # --- 使用 OutputHandler 保存 Poster Config --- + output_handler.handle_poster_configs(run_id, topic_index, poster_text_configs_raw) + # --- 结束使用 Handler 保存 --- poster_config_summary = core_posterGen.PosterConfig(poster_text_configs_raw) except Exception as e: logging.exception("Error running ContentGenerator or parsing poster configs:") traceback.print_exc() - return False # Cannot proceed if text config fails + return False - # --- Poster Generation Loop for each variant --- - poster_num = config.get("variants", 1) - target_size = tuple(config.get("poster_target_size", [900, 1200])) + # Poster Generation Loop for each variant + poster_num = variants any_poster_attempted = False - text_possibility = config.get("text_possibility", 0.3) # Get from config for j_index in range(poster_num): variant_index = j_index + 1 logging.info(f"Generating Poster {variant_index}/{poster_num}...") any_poster_attempted = True + 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 not poster_config: logging.warning(f"Warning: Could not get poster config for index {j_index}. Skipping.") continue - # Define output directories for this specific variant - run_output_dir = os.path.join(output_dir, run_id) - variant_output_dir = os.path.join(run_output_dir, f"{topic_index}_{variant_index}") - output_collage_subdir = config.get("output_collage_subdir", "collage_img") - output_poster_subdir = config.get("output_poster_subdir", "poster") - collage_output_dir = os.path.join(variant_output_dir, output_collage_subdir) - poster_output_dir = os.path.join(variant_output_dir, output_poster_subdir) - os.makedirs(collage_output_dir, exist_ok=True) - os.makedirs(poster_output_dir, exist_ok=True) - - # --- Image Collage --- + # --- Image Collage --- logging.info(f"Generating collage from: {input_img_dir_path}") - img_list = core_simple_collage.process_directory( + collage_images = core_simple_collage.process_directory( input_img_dir_path, - target_size=target_size, - output_count=1, - output_dir=collage_output_dir + target_size=poster_target_size, + output_count=1 ) - if not img_list or len(img_list) == 0 or not img_list[0].get('path'): + if not collage_images: # 检查列表是否为空 logging.warning(f"Warning: Failed to generate collage image for Variant {variant_index}. Skipping poster.") continue - collage_img_path = img_list[0]['path'] - logging.info(f"Using collage image: {collage_img_path}") + collage_img = collage_images[0] # 获取第一个 PIL Image + logging.info(f"Collage image generated successfully (in memory).") + + # --- 使用 Handler 保存 Collage 图片 --- + output_handler.handle_generated_image( + run_id, topic_index, variant_index, + image_type='collage', + image_data=collage_img, + output_filename='collage.png' # 或者其他期望的文件名 + ) + # --- 结束保存 Collage --- - # --- Create Poster --- + # --- Create Poster --- text_data = { "title": poster_config.get('main_title', 'Default Title'), "subtitle": "", @@ -624,98 +627,37 @@ def generate_posters_for_topic(config, topic_item, tweet_content_list, output_di } texts = poster_config.get('texts', []) if texts: - # Ensure TEXT_POSBILITY is accessible, maybe pass via config? - # text_possibility = config.get("text_possibility", 0.3) text_data["additional_texts"].append({"text": texts[0], "position": "bottom", "size_factor": 0.5}) - if len(texts) > 1 and random.random() < text_possibility: # Use variable from config + if len(texts) > 1 and random.random() < text_possibility: text_data["additional_texts"].append({"text": texts[1], "position": "bottom", "size_factor": 0.5}) - # final_poster_path = os.path.join(poster_output_dir, "poster.jpg") # Filename "poster.jpg" is hardcoded - output_poster_filename = config.get("output_poster_filename", "poster.jpg") - final_poster_path = os.path.join(poster_output_dir, output_poster_filename) - result_path = poster_gen_instance.create_poster(collage_img_path, text_data, final_poster_path) # Uses hardcoded output filename - if result_path: - logging.info(f"Successfully generated poster: {result_path}") + # 调用修改后的 create_poster, 接收 PIL Image + poster_img = poster_gen_instance.create_poster(collage_img, text_data) + + if poster_img: + logging.info(f"Poster image generated successfully (in memory).") + # --- 使用 Handler 保存 Poster 图片 --- + output_handler.handle_generated_image( + run_id, topic_index, variant_index, + image_type='poster', + image_data=poster_img, + output_filename=output_poster_filename # 使用参数中的文件名 + ) + # --- 结束保存 Poster --- else: - logging.warning(f"Warning: Poster generation function did not return a valid path for {final_poster_path}.") + logging.warning(f"Warning: Poster generation function returned None for variant {variant_index}.") except Exception as e: logging.exception(f"Error during poster generation for Variant {variant_index}:") traceback.print_exc() - continue # Continue to next variant + continue return any_poster_attempted -def main(): - """主函数入口""" - config_file = { - "date": "4月17日", - "num": 5, - "model": "qwenQWQ", - "api_url": "vllm", - "api_key": "EMPTY", - "topic_system_prompt": "/root/autodl-tmp/TravelContentCreator/SelectPrompt/systemPrompt.txt", - "topic_user_prompt": "/root/autodl-tmp/TravelContentCreator/SelectPrompt/userPrompt.txt", - "content_system_prompt": "/root/autodl-tmp/TravelContentCreator/genPrompts/systemPrompt.txt", - "resource_dir": [{ - "type": "Object", - "num": 4, - "file_path": ["/root/autodl-tmp/TravelContentCreator/resource/Object/景点信息-尚书第.txt", - "/root/autodl-tmp/TravelContentCreator/resource/Object/景点信息-明清园.txt", - "/root/autodl-tmp/TravelContentCreator/resource/Object/景点信息-泰宁古城.txt", - "/root/autodl-tmp/TravelContentCreator/resource/Object/景点信息-甘露寺.txt" - ]}, - { - "type": "Product", - "num": 0, - "file_path": [] - } - ], - "prompts_dir": "/root/autodl-tmp/TravelContentCreator/genPrompts", - "output_dir": "/root/autodl-tmp/TravelContentCreator/result", - "variants": 2, - "topic_temperature": 0.2, - "content_temperature": 0.3 - } - - if True: - # 1. 首先生成选题 - ai_agent, system_prompt, user_prompt, output_dir = prepare_topic_generation( - config_file - ) - - run_id, tweet_topic_record = generate_topics( - ai_agent, system_prompt, user_prompt, config_file["output_dir"], - config_file["topic_temperature"], 0.5, 1.5 - ) - - output_dir = os.path.join(config_file["output_dir"], run_id) - os.makedirs(output_dir, exist_ok=True) - tweet_topic_record.save_topics(os.path.join(output_dir, "tweet_topic.json")) - tweet_topic_record.save_prompt(os.path.join(output_dir, "tweet_prompt.txt")) - # raise Exception("选题生成失败,退出程序") - if not run_id or not tweet_topic_record: - print("选题生成失败,退出程序") - return - - # 2. 然后生成内容 - print("\n开始根据选题生成内容...") - - # 加载内容生成的系统提示词 - content_system_prompt = ResourceLoader.load_system_prompt(config_file["content_system_prompt"]) - - - if not content_system_prompt: - print("内容生成系统提示词为空,使用选题生成的系统提示词") - content_system_prompt = system_prompt - - # 直接使用同一个AI Agent实例 - result = generate_content( - ai_agent, content_system_prompt, tweet_topic_record.topics_list, output_dir, run_id, config_file["prompts_dir"], config_file["resource_dir"], - config_file["variants"], config_file["content_temperature"] - ) - - - -if __name__ == "__main__": - main() \ No newline at end of file +# main 函数不再使用,注释掉或移除 +# def main(): +# """主函数入口""" +# # ... (旧的 main 函数逻辑) +# +# if __name__ == "__main__": +# main() \ No newline at end of file