From b85d98b95aecacabc867fccadc9fda8748f686a1 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Sat, 26 Apr 2025 14:53:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E9=85=8D=E5=9B=BE?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/test_additional_images.py | 129 ++++++ examples/test_poster_notes.py | 226 +++++++++++ main.py | 50 +++ poster_gen_config.json | 5 +- poster_gen_config.json.bak | 80 ++++ test_additional_images.py | 88 +++++ .../output_handler.cpython-312.pyc | Bin 11678 -> 12559 bytes .../poster_notes_creator.cpython-312.pyc | Bin 0 -> 13852 bytes utils/output_handler.py | 51 ++- utils/poster_notes_creator.py | 372 ++++++++++++++++++ 10 files changed, 982 insertions(+), 19 deletions(-) create mode 100644 examples/test_additional_images.py create mode 100644 examples/test_poster_notes.py create mode 100644 poster_gen_config.json.bak create mode 100644 test_additional_images.py create mode 100644 utils/__pycache__/poster_notes_creator.cpython-312.pyc create mode 100644 utils/poster_notes_creator.py diff --git a/examples/test_additional_images.py b/examples/test_additional_images.py new file mode 100644 index 0000000..1cb5e5e --- /dev/null +++ b/examples/test_additional_images.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import logging +import argparse +import json +import random +from pathlib import Path +from datetime import datetime +from PIL import Image + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.output_handler import FileSystemOutputHandler +from utils.poster_notes_creator import select_additional_images + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def create_sample_metadata(image_dir, num_images=2): + """创建示例海报元数据 + + Args: + image_dir: 图像目录 + num_images: 使用的图像数量 + + Returns: + dict: 海报元数据 + """ + # 获取目录中的图像 + image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') + available_images = [ + f for f in os.listdir(image_dir) + if os.path.isfile(os.path.join(image_dir, f)) and + f.lower().endswith(image_extensions) + ] + + if len(available_images) < num_images: + logger.warning(f"可用图像数量({len(available_images)})少于请求数量({num_images}),将使用所有可用图像") + selected_images = available_images + else: + selected_images = random.sample(available_images, num_images) + + # 创建元数据 + metadata = { + "title": "示例海报", + "description": "这是一个用于测试额外配图功能的示例海报", + "collage_images": selected_images, + "generation_time": datetime.now().isoformat(), + "style": "modern" + } + + return metadata + +def main(): + # 解析命令行参数 + parser = argparse.ArgumentParser(description='测试额外配图功能') + parser.add_argument('--image_dir', type=str, required=True, + help='包含图像的目录路径') + parser.add_argument('--output_dir', type=str, default='./output', + help='输出目录路径') + parser.add_argument('--num_images', type=int, default=3, + help='要选择的额外配图数量') + + args = parser.parse_args() + + # 验证图像目录 + if not os.path.exists(args.image_dir) or not os.path.isdir(args.image_dir): + logger.error(f"图像目录不存在: {args.image_dir}") + return 1 + + # 创建输出目录 + os.makedirs(args.output_dir, exist_ok=True) + + # 初始化输出处理器 + output_handler = FileSystemOutputHandler(args.output_dir) + + # 创建测试运行ID和主题/变体索引 + run_id = datetime.now().strftime("%Y%m%d_%H%M%S") + topic_index = 1 + variant_index = 1 + + # 创建一个模拟的海报元数据文件 + metadata = create_sample_metadata(args.image_dir, 2) + metadata_path = os.path.join(args.output_dir, f"{run_id}_{topic_index}_{variant_index}_metadata.json") + + # 保存元数据到文件 + with open(metadata_path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + logger.info(f"模拟海报元数据已保存: {metadata_path}") + + # 打印将要使用的"已用"图像 + logger.info(f"模拟海报使用的图像: {', '.join(metadata['collage_images'])}") + + # 调用额外配图选择函数 + logger.info(f"开始选择额外配图...") + image_paths = select_additional_images( + run_id=run_id, + topic_index=topic_index, + variant_index=variant_index, + poster_metadata_path=metadata_path, + source_image_dir=args.image_dir, + num_additional_images=args.num_images, + output_handler=output_handler + ) + + # 打印结果 + if image_paths: + logger.info(f"已选择并保存 {len(image_paths)} 张额外配图:") + for i, path in enumerate(image_paths): + logger.info(f" {i+1}. {path}") + + logger.info(f"额外配图已保存到: {os.path.join(args.output_dir, run_id, f'{topic_index}_{variant_index}', 'image')}") + logger.info(f"测试成功完成!") + else: + logger.error("未能选择任何额外配图.") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/examples/test_poster_notes.py b/examples/test_poster_notes.py new file mode 100644 index 0000000..e9ccee2 --- /dev/null +++ b/examples/test_poster_notes.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import logging +import argparse +import json +import random +from pathlib import Path +from datetime import datetime +from PIL import Image + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.output_handler import OutputHandler +from utils.poster_notes_creator import process_poster_for_notes + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class SimpleOutputHandler(OutputHandler): + """简单的输出处理器,用于测试目的""" + + def __init__(self, output_dir): + """初始化输出处理器 + + Args: + output_dir: 输出目录路径 + """ + self.output_dir = output_dir + os.makedirs(output_dir, exist_ok=True) + logger.info(f"已初始化输出处理器,输出目录: {output_dir}") + + def handle_generated_image(self, run_id, topic_index, variant_index, + image_type, image_data, output_filename, metadata=None): + """处理生成的图像 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + image_type: 图像类型 + image_data: 图像数据 + output_filename: 输出文件名 + metadata: 图像元数据 + + Returns: + str: 保存的图像路径 + """ + # 创建目录结构 + output_dir = "" + if image_type == 'note': + # 笔记图像保存在image目录 + output_dir = os.path.join(self.output_dir, f"{run_id}/{topic_index}_{variant_index}/image") + else: + # 其他类型图像保存在各自的类型目录 + output_dir = os.path.join(self.output_dir, image_type) + + os.makedirs(output_dir, exist_ok=True) + + # 保存图像 + image_path = os.path.join(output_dir, output_filename) + image_data.save(image_path) + + # 如果有元数据,保存它 + if metadata: + metadata_path = os.path.splitext(image_path)[0] + '.json' + with open(metadata_path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + + logger.info(f"已保存图像: {image_path}") + return image_path + + def handle_poster_configs(self, run_id, topic_index, variant_index, + poster_configs, output_filename): + """处理海报配置 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + poster_configs: 海报配置 + output_filename: 输出文件名 + + Returns: + str: 保存的配置路径 + """ + # 创建目录 + config_dir = os.path.join(self.output_dir, 'configs') + os.makedirs(config_dir, exist_ok=True) + + # 保存配置 + config_path = os.path.join(config_dir, output_filename) + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(poster_configs, f, ensure_ascii=False, indent=2) + + logger.info(f"已保存配置: {config_path}") + return config_path + +def create_sample_metadata(image_dir, num_images=3): + """创建示例海报元数据 + + Args: + image_dir: 图像目录 + num_images: 使用的图像数量 + + Returns: + dict: 海报元数据 + """ + # 获取目录中的图像 + image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') + available_images = [ + f for f in os.listdir(image_dir) + if os.path.isfile(os.path.join(image_dir, f)) and + f.lower().endswith(image_extensions) + ] + + if len(available_images) < num_images: + logger.warning(f"可用图像数量({len(available_images)})少于请求数量({num_images}),将使用所有可用图像") + selected_images = available_images + else: + selected_images = random.sample(available_images, num_images) + + # 创建元数据 + metadata = { + "title": "示例海报", + "description": "这是一个用于测试的示例海报", + "collage_images": selected_images, + "generation_time": datetime.now().isoformat(), + "style": "modern" + } + + return metadata + +def main(): + # 解析命令行参数 + parser = argparse.ArgumentParser(description='测试海报笔记创建器') + parser.add_argument('--image_dir', type=str, required=True, + help='包含图像的目录路径') + parser.add_argument('--output_dir', type=str, default='./output', + help='输出目录路径') + parser.add_argument('--num_notes', type=int, default=3, + help='要创建的笔记数量') + + args = parser.parse_args() + + # 验证图像目录 + if not os.path.exists(args.image_dir) or not os.path.isdir(args.image_dir): + logger.error(f"图像目录不存在: {args.image_dir}") + return 1 + + # 创建输出目录 + os.makedirs(args.output_dir, exist_ok=True) + + # 初始化输出处理器 + output_handler = SimpleOutputHandler(args.output_dir) + + # 创建一个示例海报和元数据 + run_id = datetime.now().strftime("%Y%m%d_%H%M%S") + topic_index = 0 + variant_index = 0 + + # 从图像目录中选择一张图像作为"海报" + image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') + available_images = [ + f for f in os.listdir(args.image_dir) + if os.path.isfile(os.path.join(args.image_dir, f)) and + f.lower().endswith(image_extensions) + ] + + if not available_images: + logger.error(f"图像目录中没有找到图像: {args.image_dir}") + return 1 + + # 选择第一张图像作为海报 + poster_image_name = available_images[0] + poster_image_path = os.path.join(args.image_dir, poster_image_name) + + # 创建示例元数据 + poster_metadata = create_sample_metadata(args.image_dir, 2) + + # 保存海报图像 + poster_image = Image.open(poster_image_path) + saved_poster_path = output_handler.handle_generated_image( + run_id, + topic_index, + variant_index, + 'poster', + poster_image, + 'sample_poster.jpg', + poster_metadata + ) + + # 保存海报元数据 + poster_metadata_path = os.path.splitext(saved_poster_path)[0] + '.json' + + logger.info(f"已创建示例海报: {saved_poster_path}") + logger.info(f"元数据路径: {poster_metadata_path}") + + # 处理海报笔记 + logger.info("开始创建海报笔记...") + note_paths = process_poster_for_notes( + run_id, + topic_index, + variant_index, + saved_poster_path, + poster_metadata_path, + args.image_dir, + args.num_notes, + output_handler + ) + + logger.info(f"创建了 {len(note_paths)} 个笔记图像:") + for i, path in enumerate(note_paths): + logger.info(f" {i+1}. {path}") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/main.py b/main.py index 9a5590e..2f5e9e3 100644 --- a/main.py +++ b/main.py @@ -24,6 +24,8 @@ import random from utils.output_handler import FileSystemOutputHandler, OutputHandler from core.topic_parser import TopicParser from utils.tweet_generator import tweetTopicRecord # Needed only if loading old topics files? +# 导入额外配图选择函数 +from utils.poster_notes_creator import select_additional_images def load_config(config_path="poster_gen_config.json"): """Loads configuration from a JSON file.""" @@ -200,6 +202,54 @@ def generate_content_and_posters_step(config, run_id, topics_list, output_handle ) if posters_attempted: logging.info(f"Poster generation process completed for Topic {topic_index}.") + + # --- 为每个变体添加额外配图 --- + logging.info(f"开始为主题 {topic_index} 添加额外配图...") + additional_images_count = config.get("additional_images_count", 3) + + # 循环处理每个变体 + for j in range(poster_variants): + variant_index = j + 1 + variant_dir = os.path.join(config["output_dir"], run_id, f"{topic_index}_{variant_index}") + + # 获取海报元数据路径 + poster_dir = os.path.join(variant_dir, poster_subdir) + metadata_files = [f for f in os.listdir(poster_dir) + if f.endswith("_metadata.json") and os.path.isfile(os.path.join(poster_dir, f))] + + if metadata_files: + poster_metadata_path = os.path.join(poster_dir, metadata_files[0]) + logging.info(f"为变体 {variant_index} 选择额外配图,使用元数据: {poster_metadata_path}") + + # 调用额外配图选择函数 + try: + object_name = topic_item.get("object", "").split(".")[0].replace("景点信息-", "").strip() + source_image_dir = os.path.join(img_base_dir, object_name) + + if os.path.exists(source_image_dir) and os.path.isdir(source_image_dir): + additional_paths = select_additional_images( + run_id=run_id, + topic_index=topic_index, + variant_index=variant_index, + poster_metadata_path=poster_metadata_path, + source_image_dir=source_image_dir, + num_additional_images=additional_images_count, + output_handler=output_handler + ) + + if additional_paths: + logging.info(f"已为变体 {variant_index} 选择 {len(additional_paths)} 张额外配图") + else: + logging.warning(f"未能为变体 {variant_index} 选择任何额外配图") + else: + logging.warning(f"源图像目录不存在: {source_image_dir}") + except Exception as e: + logging.exception(f"选择额外配图时发生错误: {e}") + else: + logging.warning(f"未找到变体 {variant_index} 的海报元数据文件,跳过额外配图选择") + + # --- 结束添加额外配图 --- + success_flag = True # Mark overall success if content AND poster attempts were made else: logging.warning(f"Poster generation skipped or failed early for Topic {topic_index}.") diff --git a/poster_gen_config.json b/poster_gen_config.json index 28f70c4..c0a5ed7 100644 --- a/poster_gen_config.json +++ b/poster_gen_config.json @@ -76,5 +76,8 @@ "text_possibility": 0.3, "img_frame_possibility": 0, "text_bg_possibility": 0, - "collage_style": ["grid_2x2", "overlap", "mosaic", "fullscreen", "vertical_stack"] + "collage_style": ["grid_2x2", "overlap", "mosaic", "fullscreen", "vertical_stack"], + + "additional_images_count": 3, + "additional_images_enabled": true } \ No newline at end of file diff --git a/poster_gen_config.json.bak b/poster_gen_config.json.bak new file mode 100644 index 0000000..28f70c4 --- /dev/null +++ b/poster_gen_config.json.bak @@ -0,0 +1,80 @@ +{ + "date": "4月30日, 4月28日, 5月1日", + "num": 1, + "variants": 3, + "topic_temperature": 0.2, + "topic_top_p": 0.3, + "topic_presence_penalty": 1.5, + "content_temperature": 0.3, + "content_top_p": 0.4, + "content_presence_penalty": 1.5, + "model": "qwenQWQ", + "api_url": "http://localhost:8000/v1/", + "api_key": "EMPTY", + "topic_system_prompt": "./SelectPrompt/systemPrompt.txt", + "topic_user_prompt": "./SelectPrompt/userPrompt.txt", + "content_system_prompt": "./genPrompts/systemPrompt.txt", + "poster_content_system_prompt": "./genPrompts/poster_content_systemPrompt.txt", + "prompts_config": [ + { + "type": "Style", + "file_path": [ + "./genPrompts/Style/攻略风文案提示词.txt" + ] + }, + { + "type": "Demand", + "file_path": [ + "./genPrompts/Demand/亲子向文旅需求.txt", + "./genPrompts/Demand/周边游文旅需求.txt", + "./genPrompts/Demand/情侣向文旅需求.txt" + ] + }, + { + "type": "Refer", + "file_path": [ + "./genPrompts/Refer/标题参考格式.txt", + "./genPrompts/Refer/正文开头引入段落参考.txt" + ] + } + ], + "resource_dir": [ + { + "type": "Object", + "file_path": [ + "./resource/Object/笔架山居森林度假酒店.txt", + "./resource/Object/乌镇民宿.txt" + ] + }, + { + "type": "Description", + "file_path": [ + "./resource/Object/笔架山居森林度假酒店.txt", + "./resource/Object/乌镇民宿.txt" + ] + }, + { + "type": "Product", + "file_path": [ + ] + } + ], + "output_dir": "./result", + "image_base_dir": "/root/autodl-tmp/TravelContentCreator/hotel_img", + "poster_assets_base_dir": "/root/autodl-tmp/poster_baseboard_0403", + "request_timeout": 210, + "max_retries": 3, + "description_filename": "description.txt", + "output_collage_subdir": "collage_img", + "output_poster_subdir": "poster", + "output_poster_filename": "poster.jpg", + "poster_target_size": [ + 900, + 1200 + ], + "title_possibility": 0.3, + "text_possibility": 0.3, + "img_frame_possibility": 0, + "text_bg_possibility": 0, + "collage_style": ["grid_2x2", "overlap", "mosaic", "fullscreen", "vertical_stack"] +} \ No newline at end of file diff --git a/test_additional_images.py b/test_additional_images.py new file mode 100644 index 0000000..8d0be57 --- /dev/null +++ b/test_additional_images.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import json +import logging +import argparse +from datetime import datetime + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from utils.output_handler import FileSystemOutputHandler +from utils.poster_notes_creator import select_additional_images + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + parser = argparse.ArgumentParser(description='测试额外配图选择功能') + parser.add_argument('--run_id', type=str, required=True, help='指定运行ID') + parser.add_argument('--topic_index', type=int, required=True, help='指定主题索引') + parser.add_argument('--variant_index', type=int, required=True, help='指定变体索引') + parser.add_argument('--source_dir', type=str, required=True, help='源图像目录路径') + parser.add_argument('--output_dir', type=str, default='./result', help='输出目录路径') + parser.add_argument('--num_images', type=int, default=3, help='要选择的配图数量') + args = parser.parse_args() + + # 检查源目录是否存在 + if not os.path.exists(args.source_dir) or not os.path.isdir(args.source_dir): + logger.error(f"源图像目录不存在: {args.source_dir}") + return 1 + + # 检查输出目录结构 + run_dir = os.path.join(args.output_dir, args.run_id) + variant_dir = os.path.join(run_dir, f"{args.topic_index}_{args.variant_index}") + poster_dir = os.path.join(variant_dir, "poster") + + if not os.path.exists(poster_dir): + logger.error(f"海报目录不存在: {poster_dir}") + return 1 + + # 查找海报元数据文件 + metadata_files = [f for f in os.listdir(poster_dir) + if f.endswith("_metadata.json") and os.path.isfile(os.path.join(poster_dir, f))] + + if not metadata_files: + logger.error(f"未找到海报元数据文件") + return 1 + + metadata_path = os.path.join(poster_dir, metadata_files[0]) + logger.info(f"使用元数据文件: {metadata_path}") + + # 初始化输出处理器 + output_handler = FileSystemOutputHandler(args.output_dir) + + # 调用额外配图选择函数 + logger.info(f"开始为运行ID {args.run_id} 主题 {args.topic_index} 变体 {args.variant_index} 选择额外配图...") + image_paths = select_additional_images( + run_id=args.run_id, + topic_index=args.topic_index, + variant_index=args.variant_index, + poster_metadata_path=metadata_path, + source_image_dir=args.source_dir, + num_additional_images=args.num_images, + output_handler=output_handler + ) + + # 输出结果 + if image_paths: + logger.info(f"已选择并保存 {len(image_paths)} 张额外配图:") + for i, path in enumerate(image_paths): + logger.info(f" {i+1}. {path}") + logger.info(f"额外配图已保存到: {os.path.join(args.output_dir, args.run_id, f'{args.topic_index}_{args.variant_index}', 'image')}") + logger.info("测试成功完成!") + else: + logger.error("未能选择任何额外配图") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/utils/__pycache__/output_handler.cpython-312.pyc b/utils/__pycache__/output_handler.cpython-312.pyc index 71038d0edb95af409a3fdb7d654aeb28ee339074..a8708abed41b09064924a0aea51ea5d3691f93f0 100644 GIT binary patch delta 2056 zcmZ`(drVVj6#s6YwA{XH?Sn#vj@k(-PRxdnDCn>#ivqJL>acJT%dqNgD@1R_OoC|>9QrrvR688oNu>jx~p?4Ay!JCeV>1Wacm6+fU3bHW_P=fYc`j%Fm zcLQS!Jwl3?p%WlFH%1BkxrnMUxL5j zfgRpZ84c1*2N{J!&AbX6#;KywyPPCR=?6K$@=YP zx>v8V>oR-NBW}V#DPadeb_a+J-iq>o67^uMPOKCB54UOtcQNAbX1Vjw9YAi?e{ib~ zz^^;Q4Xqd4R-?}gbtbMJPMv%&b-F(pIGOt3aN@%K#Gw<VlXuUiZrx2>ejnx} zd)`SL?@wMl`sDUd;{M^(T{?OB@EFg%%iB68b`pfk>)PdZc91KfmN9;Ys)1Yr9lE2q zV5?6Jt>ndzQvDyYB3NFN#fNU%0^6}Npo+wazR7!ZGI)no^7FxWtr)oiX88mV*fd`* zG^eHsgz0#3S+`G$LPriI&xDi5ZbF_4lMjcXyXRyIQO=ai;$V)+1=st zwsv{Pa+;lzI&mk;ou7!VIzwK|O*71Wp8RLa?goD;DdrWys$OVN8@+W)mzP zQ|u;Nx4XJpiKgwY4wh7edN%ELLP=;zWF-XpCrcTorWJElv}tZt_kQ@Nb`Ycm0N3+HuU7skl`a8S~cBsJ|SlreUwY4yW`>}0T zup(H}9dd?c%z~9s)2cK8d$Bbf1~<-Rb1(~6#k7@2ZID2t54=7q)AczX$%;pH#l3=2 zg`uzOk-{>no6{?pl+QjV>yu%d` z+2iGFqUCEwT(R;EG4n>Iu0CpRVC-#Ca~mU{Gj1xpCL9o6!w2xex*_{@`%u&MreQ~{ zw3@Ls#!Q<}O41y@VjI1}TGjnn-H?_7xsJ)vrj0tH>c zX&hI@abpxW2Juj1XnpsvXv8{Vh*U=QL|T~qx;S3X;Pn$SE!6WoEkqT6dC-UVm8P=P zHVZu9N!Oad1Eqegn){%n2-*=ff2~m%(Fu`l64{j8h&A6P;XV}dVE#i1A4VQ3xyYU+ zsu6M@=I7UNxL4Y_!cqsepLUj(C{}YYN#D*t06Iwpl>ZMj>xJV0 delta 1500 zcmZ{j%TF6e9LHz8etXxC*lTRSurY>Ui#Rb&tKhbgnt*8th=f;ogwlnzz$Dn#dJ|)7 z9eM<*QcCs!5+Z^VNN{j_Lh6MoP(|ux4XrQ{hg9kx;J&5i(ix0_R_(6zd(8ZPpZU&j zwa-Rh3>m+pC_O;W;S@RHtp3uNcl#O%02TlTGg?u}DIV4Xz!Myi(MD@ft~|+=D6f$I zxLoZ}iOMCwsh5cjh(Q3rVz2aqw9`;jh=i!!)NCk=n;D&;mVP!?DkcrmDrvRqMD>Q^ zWJ|9DqH2=D>t*^gnsCAeVA3efms?#(7dlZV>X*Q>@~lqO@Xt$d3x+t=rA2ZnKvz^ z3*y`3x5R|t-}>(r^k}S{EFplRbd1<~9 z%kfltt;bWbVJb(9M#Mh7KBl4XX?$u-A!XPnVG{fg*>sLviODlXvK`MGkxlowPQ4;V~h`r~J}#eRX@<)*F2ojU}TeBq0?KGndlkm;4MmED{~Zyiz|O ziwS-&PmW-2VO94oM>HOQn*AvW>|6(UXobuek_d;!Bh zz%czOgz=jA09tyjNwRzt3E*#`jK7Xe6E;D-sl+J05r^7D{82;*PYe7#lveOvxTqVO z9&A`_dXe#(C7aJ{!`k_q$h_8J#z>qMCZv=vpfsa3=__C7%qsdpJ_Bzo^sG8>pckEc z26vuz&xP_-+2Zv*swPjn=0ZP_)@Pk_opM!UHkfUYZ}iKR1K*Q_dD=M}dP6%8XkU)@ zt-qIjf18r~9>}zBpB|poAKB~+t@Eu5?epy`xhkjC)P9s#zmf$@M|GscoOR^95B< zs4Sec)NpI&2U|~p0%pgbT1a=m0>Jj|Z))JU_zQp*4j$Pm7CPoTRywn7*$=iduZCZB z%0t8QC!fm0tn3~=u#M$xV{Z!tuEl+3C+b5gk4WnQ>B^C=6;oEfE^Y<4Bilpr-7dMi zNABvCtNISe`#JJHi(66oUkN<;Z#*Bp(^3mwI*iQ$@UpJFxt7=gH7MSxRW}`V@nTUypkV&C>MM0ho~Xt~OX6Lr5)7 M{SL6_E$9k=1BNS&&j0`b diff --git a/utils/__pycache__/poster_notes_creator.cpython-312.pyc b/utils/__pycache__/poster_notes_creator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67ef9c1650d862d86f741ec45367fca44e04913d GIT binary patch literal 13852 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 List[str]: + """ + 创建笔记图像 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + poster_image_path: 海报图像路径 + poster_metadata_path: 海报元数据路径 + source_image_dir: 源图像目录 + num_additional_images: 要使用的额外图像数量 + output_filename_template: 输出文件名模板 + + Returns: + List[str]: 保存的笔记图像路径列表 + """ + # 检查输入路径是否存在 + if not os.path.exists(poster_image_path): + logger.error(f"海报图像不存在: {poster_image_path}") + return [] + + if not os.path.exists(poster_metadata_path): + logger.error(f"海报元数据不存在: {poster_metadata_path}") + return [] + + if not os.path.exists(source_image_dir) or not os.path.isdir(source_image_dir): + logger.error(f"源图像目录不存在: {source_image_dir}") + return [] + + # 从元数据文件中读取已使用的图像信息 + try: + with open(poster_metadata_path, 'r', encoding='utf-8') as f: + poster_metadata = json.load(f) + except Exception as e: + logger.error(f"无法读取海报元数据: {e}") + return [] + + # 获取已经在海报中使用的图像 + used_images = [] + if 'collage_images' in poster_metadata: + used_images = poster_metadata['collage_images'] + logger.info(f"海报中已使用 {len(used_images)} 张图像: {', '.join(used_images)}") + + # 列出源目录中的所有图像文件 + image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') + available_images = [ + f for f in os.listdir(source_image_dir) + if os.path.isfile(os.path.join(source_image_dir, f)) and + f.lower().endswith(image_extensions) + ] + + if not available_images: + logger.error(f"源目录中没有找到图像: {source_image_dir}") + return [] + + logger.info(f"源目录中找到 {len(available_images)} 张图像") + + # 过滤掉已经在海报中使用的图像 + available_images = [img for img in available_images if img not in used_images] + + if not available_images: + logger.warning("所有图像都已在海报中使用,无法创建额外笔记") + return [] + + logger.info(f"过滤后可用图像数量: {len(available_images)}") + + # 如果可用图像少于请求数量,进行警告但继续处理 + if len(available_images) < num_additional_images: + logger.warning( + f"可用图像数量 ({len(available_images)}) 少于请求的笔记数量 ({num_additional_images})," + f"将使用所有可用图像" + ) + selected_images = available_images + else: + # 随机选择额外图像 + selected_images = random.sample(available_images, num_additional_images) + + logger.info(f"已选择 {len(selected_images)} 张图像作为笔记") + + # 保存选择的笔记图像 + saved_paths = [] + for i, image_filename in enumerate(selected_images): + try: + # 加载图像 + image_path = os.path.join(source_image_dir, image_filename) + image = Image.open(image_path) + + # 生成输出文件名 + output_filename = output_filename_template.format(index=i+1) + + # 创建元数据 + note_metadata = { + "original_image": image_filename, + "note_index": i + 1, + "source_dir": source_image_dir, + "associated_poster": os.path.basename(poster_image_path) + } + + # 使用输出处理器保存图像 + saved_path = self.output_handler.handle_generated_image( + run_id, + topic_index, + variant_index, + 'note', # 图像类型为note + image, + output_filename, + note_metadata + ) + + saved_paths.append(saved_path) + logger.info(f"已保存笔记图像 {i+1}/{len(selected_images)}: {saved_path}") + + except Exception as e: + logger.error(f"处理图像时出错 '{image_filename}': {e}") + + return saved_paths + + def create_additional_images( + self, + run_id: str, + topic_index: int, + variant_index: int, + poster_metadata_path: str, + source_image_dir: str, + num_additional_images: int = 3, + output_filename_template: str = "additional_{index}.jpg" + ) -> List[str]: + """ + 选择未被海报使用的图像作为额外配图 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + poster_metadata_path: 海报元数据路径 + source_image_dir: 源图像目录 + num_additional_images: 要选择的额外图像数量 + output_filename_template: 输出文件名模板 + + Returns: + List[str]: 保存的额外配图路径列表 + """ + logger.info(f"开始为主题 {topic_index} 变体 {variant_index} 选择额外配图") + + # 检查输入路径是否存在 + if not os.path.exists(poster_metadata_path): + logger.error(f"海报元数据不存在: {poster_metadata_path}") + return [] + + if not os.path.exists(source_image_dir) or not os.path.isdir(source_image_dir): + logger.error(f"源图像目录不存在: {source_image_dir}") + return [] + + # 从元数据文件中读取已使用的图像信息 + try: + with open(poster_metadata_path, 'r', encoding='utf-8') as f: + poster_metadata = json.load(f) + except Exception as e: + logger.error(f"无法读取海报元数据: {e}") + return [] + + # 获取已经在海报中使用的图像 + used_images = [] + if 'collage_images' in poster_metadata: + used_images = poster_metadata['collage_images'] + logger.info(f"海报中已使用 {len(used_images)} 张图像: {', '.join(used_images)}") + + # 列出源目录中的所有图像文件 + image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') + available_images = [ + f for f in os.listdir(source_image_dir) + if os.path.isfile(os.path.join(source_image_dir, f)) and + f.lower().endswith(image_extensions) + ] + + if not available_images: + logger.error(f"源目录中没有找到图像: {source_image_dir}") + return [] + + logger.info(f"源目录中找到 {len(available_images)} 张图像") + + # 过滤掉已经在海报中使用的图像 + available_images = [img for img in available_images if img not in used_images] + + if not available_images: + logger.warning("所有图像都已在海报中使用,无法创建额外配图") + return [] + + logger.info(f"过滤后可用图像数量: {len(available_images)}") + + # 如果可用图像少于请求数量,进行警告但继续处理 + if len(available_images) < num_additional_images: + logger.warning( + f"可用图像数量 ({len(available_images)}) 少于请求的配图数量 ({num_additional_images})," + f"将使用所有可用图像" + ) + selected_images = available_images + else: + # 随机选择额外图像 + selected_images = random.sample(available_images, num_additional_images) + + logger.info(f"已选择 {len(selected_images)} 张图像作为额外配图") + + # 保存选择的额外配图 + saved_paths = [] + for i, image_filename in enumerate(selected_images): + try: + # 加载图像 + image_path = os.path.join(source_image_dir, image_filename) + image = Image.open(image_path) + + # 生成输出文件名 + output_filename = output_filename_template.format(index=i+1) + + # 创建元数据 + additional_metadata = { + "original_image": image_filename, + "additional_index": i + 1, + "source_dir": source_image_dir, + "is_additional_image": True + } + + # 使用输出处理器保存图像 + saved_path = self.output_handler.handle_generated_image( + run_id, + topic_index, + variant_index, + 'additional', # 图像类型为additional + image, + output_filename, + additional_metadata + ) + + saved_paths.append(saved_path) + logger.info(f"已保存额外配图 {i+1}/{len(selected_images)}: {saved_path}") + + except Exception as e: + logger.error(f"处理图像时出错 '{image_filename}': {e}") + + return saved_paths + +def process_poster_for_notes( + run_id: str, + topic_index: int, + variant_index: int, + poster_image_path: str, + poster_metadata_path: str, + source_image_dir: str, + num_additional_images: int, + output_handler: OutputHandler, + output_filename_template: str = "note_{index}.jpg" +) -> List[str]: + """ + 处理海报并创建笔记图像 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + poster_image_path: 海报图像路径 + poster_metadata_path: 海报元数据路径 + source_image_dir: 源图像目录 + num_additional_images: 要使用的额外图像数量 + output_handler: 输出处理器 + output_filename_template: 输出文件名模板 + + Returns: + List[str]: 保存的笔记图像路径列表 + """ + logger.info(f"开始为海报创建笔记图像: {poster_image_path}") + + # 验证输入 + if not os.path.exists(poster_image_path): + logger.error(f"海报图像不存在: {poster_image_path}") + return [] + + # 创建处理器实例并处理 + creator = PosterNotesCreator(output_handler) + return creator.create_notes_images( + run_id, + topic_index, + variant_index, + poster_image_path, + poster_metadata_path, + source_image_dir, + num_additional_images, + output_filename_template + ) + +def select_additional_images( + run_id: str, + topic_index: int, + variant_index: int, + poster_metadata_path: str, + source_image_dir: str, + num_additional_images: int, + output_handler: OutputHandler, + output_filename_template: str = "additional_{index}.jpg" +) -> List[str]: + """ + 选择未被海报使用的图像作为额外配图 + + Args: + run_id: 运行ID + topic_index: 主题索引 + variant_index: 变体索引 + poster_metadata_path: 海报元数据路径 + source_image_dir: 源图像目录 + num_additional_images: 要选择的额外图像数量 + output_handler: 输出处理器 + output_filename_template: 输出文件名模板 + + Returns: + List[str]: 保存的额外配图路径列表 + """ + logger.info(f"开始为主题 {topic_index} 变体 {variant_index} 选择额外配图") + + # 验证输入 + if not os.path.exists(poster_metadata_path): + logger.error(f"海报元数据不存在: {poster_metadata_path}") + return [] + + # 创建处理器实例并处理 + creator = PosterNotesCreator(output_handler) + return creator.create_additional_images( + run_id, + topic_index, + variant_index, + poster_metadata_path, + source_image_dir, + num_additional_images, + output_filename_template + ) \ No newline at end of file