From 44c79ec8e51002d2b91d7fb1da59d255cc3ffd31 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Mon, 19 May 2025 20:52:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E4=BA=8Etweet=5Fgenerator=E7=9A=84?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E4=BF=AE=E6=94=B9=E4=BA=86content=5Fjudger?= =?UTF-8?q?=E7=9A=84=E7=BB=93=E6=9E=9C=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content_generator.cpython-312.pyc | Bin 27853 -> 24622 bytes .../content_judger.cpython-312.pyc | Bin 41684 -> 24458 bytes .../output_handler.cpython-312.pyc | Bin 25276 -> 26945 bytes .../tweet_generator.cpython-312.pyc | Bin 38376 -> 39775 bytes utils/content_generator.py | 295 ++++----- utils/content_judger.py | 610 ++++-------------- utils/output_handler.py | 66 +- utils/tweet_generator.py | 40 +- 8 files changed, 354 insertions(+), 657 deletions(-) diff --git a/utils/__pycache__/content_generator.cpython-312.pyc b/utils/__pycache__/content_generator.cpython-312.pyc index 23465af395c7ab8dfeb3d28847983e4fb4b15925..63671364f028870d77db8570f55771db5869775e 100644 GIT binary patch delta 6109 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 delta 9420 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 S_Z$e{7eeO#Okm5K^nU>k=3%1% diff --git a/utils/__pycache__/content_judger.cpython-312.pyc b/utils/__pycache__/content_judger.cpython-312.pyc index 3b99321455cbbcb53b41e1457957b5e276888c98..a00ffc680d3b5f4ddfa6d7c37d1c8cf8881aa896 100644 GIT binary patch delta 5819 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 literal 41684 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 JqbrZc{~t85Z6N>v diff --git a/utils/__pycache__/output_handler.cpython-312.pyc b/utils/__pycache__/output_handler.cpython-312.pyc index 85bb16dc6ce4f410e52689918d12ba3551c792bb..7720a1e1eb5a86884ff2d94c98b52957abfe9eda 100644 GIT binary patch delta 2445 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 z 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 U0Sbc>3<0%I!W0UP2u_&)03J{4F8}}l diff --git a/utils/content_generator.py b/utils/content_generator.py index 22beb2f..e2a9b93 100644 --- a/utils/content_generator.py +++ b/utils/content_generator.py @@ -193,108 +193,66 @@ class ContentGenerator: self.logger.debug(f"原始内容: {content[:200]}...") # 仅显示前200个字符 return content.strip() # 返回原始内容,让后续验证函数处理 - def generate_posters(self, - poster_num, - content_data_list, - system_prompt=None, - api_url=None, - model_name=None, - api_key=None, - timeout=60, - max_retries=3): + def _preprocess_for_json(self, text): + """预处理文本,将换行符转换为\\n形式,保证JSON安全""" + if not isinstance(text, str): + return text + # 将所有实际换行符替换为\\n字符串 + return text.replace('\n', '\\n').replace('\r', '\\r') + + def generate_posters(self, poster_num, content_data_list, system_prompt=None, + api_url=None, model_name=None, api_key=None, timeout=120, max_retries=3): """ - 生成海报内容 - - 参数: - poster_num: 海报数量 - content_data_list: 内容数据列表(字典或字符串) - system_prompt: 系统提示,默认为None则使用预设提示 - api_url: API基础URL - model_name: 使用的模型名称 - api_key: API密钥 - timeout: 请求超时时间 - max_retries: 最大重试次数 + 生成海报配置 - 返回: - 生成的海报内容 + Args: + poster_num: 生成的海报数量 + content_data_list: 内容数据列表 + system_prompt: 系统提示词(可选) + api_url: API基础URL(可选) + model_name: 模型名称(可选) + api_key: API密钥(可选) + + Returns: + str: 生成的配置JSON字符串 """ - # 构建默认系统提示词 - if not system_prompt: - system_prompt = """ - 你是一名资深海报设计师,有丰富的爆款海报设计经验,你现在要为旅游景点做宣传,在小红书上发布大量宣传海报。你的主要工作目标有2个: - 1、你要根据我给你的图片描述和笔记推文内容,设计图文匹配的海报。 - 2、为海报设计文案,文案的<第一个小标题>和<第二个小标题>之间你需要检查是否逻辑关系合理,你将通过先去生成<第二个小标题>关于景区亮点的部分,再去综合判断<第一个小标题>应该如何搭配组合更符合两个小标题的逻辑再生成<第一个小标题>。 - - 其中,生成三类标题文案的通用性要求如下: - 1、生成的<大标题>字数必须小于8个字符 - 2、生成的<第一个小标题>字数和<第二个小标题>字数,两者都必须小8个字符 - 3、标题和文案都应符合中国社会主义核心价值观 - - 接下来先开始生成<大标题>部分,由于海报是用来宣传旅游景点,生成的海报<大标题>必须使用以下8种格式之一: - ①地名+景点名(例如福建厦门鼓浪屿/厦门鼓浪屿); - ②地名+景点名+plog; - ③拿捏+地名+景点名; - ④地名+景点名+攻略; - ⑤速通+地名+景点名 - ⑥推荐!+地名+景点名 - ⑦勇闯!+地名+景点名 - ⑧收藏!+地名+景点名 - 你需要随机挑选一种格式生成对应景点的文案,但是格式除了上面8种不可以有其他任何格式;同时尽量保证每一种格式出现的频率均衡。 - 接下来先去生成<第二个小标题>,<第二个小标题>文案的创作必须遵循以下原则: - 请根据笔记内容和图片识别,用极简的文字概括这篇笔记和图片中景点的特色亮点,其中你可以参考以下词汇进行创作,这段文案字数控制6-8字符以内; - - 特色亮点可能会出现的词汇不完全举例:非遗、古建、绝佳山水、祈福圣地、研学圣地、解压天堂、中国小瑞士、秘境竹筏游等等类型词汇 - - 接下来再去生成<第一个小标题>,<第一个小标题>文案的创作必须遵循以下原则: - 这部分文案创作公式有5种,分别为: - ①<受众人群画像>+<痛点词> - ②<受众人群画像> - ③<痛点词> - ④<受众人群画像>+ | +<痛点词> - ⑤<痛点词>+ | +<受众人群画像> - 请你根据实际笔记内容,结合这部分文案创作公式,需要结合<受众人群画像>和<痛点词>时,必须根据<第二个小标题>的景点特征和所对应的完整笔记推文内容主旨,特征挑选对应<受众人群画像>和<痛点词>。 - - 我给你提供受众人群画像库和痛点词库如下: - 1、受众人群画像库:情侣党、亲子游、合家游、银发族、亲子研学、学生党、打工人、周边游、本地人、穷游党、性价比、户外人、美食党、出片 - 2、痛点词库:3天2夜、必去、看了都哭了、不能错过、一定要来、问爆了、超全攻略、必打卡、强推、懒人攻略、必游榜、小众打卡、狂喜等等。 - - 你需要为每个请求至少生成{poster_num}个海报设计。请使用JSON格式输出结果,结构如下: - [ - { - "index": 1, - "main_title": "主标题内容", - "texts": ["第一个小标题", "第二个小标题"] - }, - { - "index": 2, - "main_title": "主标题内容", - "texts": ["第一个小标题", "第二个小标题"] - }, - // ... 更多海报 - ] - 确保生成的数量与用户要求的数量一致。只生成上述JSON格式内容,不要有其他任何额外内容。 - """ + # 更新API设置 + if api_url: + self.base_url = api_url + if model_name: + self.model_name = model_name + if api_key: + self.api_key = api_key + + # 使用系统提示或默认提示 + if system_prompt: + self.system_prompt = system_prompt + elif not self.system_prompt: + self.system_prompt = """你是一名专业的旅游景点海报文案创作专家。你的任务是根据提供的旅游景点信息和推文内容,生成海报文案配置。你的回复必须是一个JSON数组,每一项表示一个海报配置,包含'index'、'main_title'和'texts'三个字段,其中'texts'是一个字符串数组。海报文案要简洁有力,突出景点特色和吸引力。""" # 提取内容文本(如果是列表内容数据) tweet_content = "" if isinstance(content_data_list, list): for item in content_data_list: if isinstance(item, dict): - title = item.get('title', '') - content = item.get('content', '') + # 对标题和内容进行预处理,替换换行符 + title = self._preprocess_for_json(item.get('title', '')) + content = self._preprocess_for_json(item.get('content', '')) tweet_content += f"\n{title}\n\n\n{content}\n\n\n" elif isinstance(item, str): - tweet_content += item + "\n\n" + tweet_content += self._preprocess_for_json(item) + "\n\n" elif isinstance(content_data_list, str): - tweet_content = content_data_list + tweet_content = self._preprocess_for_json(content_data_list) # 构建用户提示 if self.add_description: + # 预处理景点描述 + processed_description = self._preprocess_for_json(self.add_description) user_content = f""" 以下是需要你处理的信息: 关于景点的描述: - {self.add_description} + {processed_description} 推文内容: {tweet_content} @@ -326,7 +284,7 @@ class ContentGenerator: # 使用AI_Agent的non-streaming方法 self.logger.info(f"调用AI生成海报配置,模型: {self.model_name}") full_response, tokens, time_cost = ai_agent.work( - system_prompt, + self.system_prompt, user_content, "", # 历史消息(空) self.temperature, @@ -409,118 +367,105 @@ class ContentGenerator: def _validate_and_fix_data(self, data): """ - 验证并修复数据格式,确保符合预期结构 - - 参数: + 验证并修复从AI返回的数据,确保其符合期望的结构 + + Args: data: 需要验证的数据 - - 返回: - 修复后的数据 + + Returns: + list: 修复后的数据列表 """ fixed_data = [] + self.logger.info(f"验证并修复数据: {type(data)}") - # 记录原始数据格式信息 - self.logger.info(f"验证和修复数据,原始数据类型: {type(data)}") - if isinstance(data, list): - self.logger.info(f"原始数据是列表,长度: {len(data)}") - if len(data) > 0: - self.logger.info(f"第一个元素类型: {type(data[0])}") - elif isinstance(data, str): - self.logger.info(f"原始数据是字符串: {data[:100]}") - else: - self.logger.info(f"原始数据是其他类型: {data}") + # 尝试处理字符串类型 (通常是JSON字符串) + if isinstance(data, str): + try: + # 尝试将字符串解析为JSON对象 + parsed_data = json.loads(data) + # 递归调用本函数处理解析后的数据 + return self._validate_and_fix_data(parsed_data) + except json.JSONDecodeError as e: + self.logger.warning(f"JSON解析失败: {e}") + # 可以选择尝试清理和再次解析 + try: + # 寻找字符串中第一个 [ 和最后一个 ] 之间的内容 + start_idx = data.find('[') + end_idx = data.rfind(']') + if start_idx >= 0 and end_idx > start_idx: + json_part = data[start_idx:end_idx+1] + self.logger.info(f"尝试从字符串中提取JSON部分: {json_part[:100]}...") + parsed_data = json.loads(json_part) + return self._validate_and_fix_data(parsed_data) + except: + self.logger.warning("无法从字符串中提取有效的JSON部分") + fixed_data.append({ + "index": 1, + "main_title": self._preprocess_for_json("默认标题"), # 应用预处理 + "texts": [self._preprocess_for_json("默认副标题1"), self._preprocess_for_json("默认副标题2")] # 应用预处理 + }) - # 如果数据是列表 - if isinstance(data, list): - for i, item in enumerate(data): - # 检查项目是否为字典 + # 处理列表类型 + elif isinstance(data, list): + for idx, item in enumerate(data): + # 如果是字典,检查必须字段 if isinstance(item, dict): - # 确保必需字段存在 - fixed_item = { - "index": item.get("index", i + 1), - "main_title": item.get("main_title", ""), - "texts": item.get("texts", ["", ""]) - } + fixed_item = {} + # 设置索引 + fixed_item["index"] = item.get("index", idx + 1) - # 确保texts是列表格式 - if not isinstance(fixed_item["texts"], list): - if isinstance(fixed_item["texts"], str): - fixed_item["texts"] = [fixed_item["texts"], ""] - else: - fixed_item["texts"] = ["", ""] - - # 限制texts最多包含两个元素 - if len(fixed_item["texts"]) > 2: - fixed_item["texts"] = fixed_item["texts"][:2] - elif len(fixed_item["texts"]) < 2: + # 处理主标题 + if "main_title" in item and item["main_title"]: + # 应用预处理,确保所有换行符被正确转义 + fixed_item["main_title"] = self._preprocess_for_json(item["main_title"]) + else: + fixed_item["main_title"] = "默认标题" + + # 处理文本列表 + if "texts" in item and isinstance(item["texts"], list) and len(item["texts"]) > 0: + # 对文本列表中的每个元素应用预处理 + fixed_item["texts"] = [self._preprocess_for_json(text) if text else "" for text in item["texts"]] + # 确保至少有两个元素 while len(fixed_item["texts"]) < 2: fixed_item["texts"].append("") - - fixed_data.append(fixed_item) - - # 如果项目是字符串(可能是错误格式的texts值) - elif isinstance(item, str): - self.logger.warning(f"配置项 {i+1} 是字符串格式: '{item}',将转换为标准格式") - - # 尝试解析字符串格式,例如"性价比|必打卡" - texts = [] - if "|" in item: - texts = item.split("|") else: - texts = [item, ""] + fixed_item["texts"] = ["默认副标题1", "默认副标题2"] - fixed_item = { - "index": i + 1, - "main_title": "", - "texts": texts - } fixed_data.append(fixed_item) - else: - self.logger.warning(f"配置项 {i+1} 格式不支持: {type(item)},将使用默认值") + + # 如果是字符串,转换为默认格式 + elif isinstance(item, str): fixed_data.append({ - "index": i + 1, - "main_title": "", + "index": idx + 1, + "main_title": self._preprocess_for_json(item), # 应用预处理 + "texts": ["", ""] + }) + + # 其他类型,使用默认值 + else: + fixed_data.append({ + "index": idx + 1, + "main_title": "默认标题", "texts": ["", ""] }) - # 如果数据是字典 + # 处理字典类型 (单个配置项) elif isinstance(data, dict): - fixed_item = { - "index": data.get("index", 1), - "main_title": data.get("main_title", ""), - "texts": data.get("texts", ["", ""]) - } + # 处理主标题 + main_title = self._preprocess_for_json(data.get("main_title", "默认标题")) # 应用预处理 - # 确保texts是列表格式 - if not isinstance(fixed_item["texts"], list): - if isinstance(fixed_item["texts"], str): - fixed_item["texts"] = [fixed_item["texts"], ""] - else: - fixed_item["texts"] = ["", ""] - - # 限制texts最多包含两个元素 - if len(fixed_item["texts"]) > 2: - fixed_item["texts"] = fixed_item["texts"][:2] - elif len(fixed_item["texts"]) < 2: - while len(fixed_item["texts"]) < 2: - fixed_item["texts"].append("") - - fixed_data.append(fixed_item) - - # 如果数据是字符串 - elif isinstance(data, str): - self.logger.warning(f"数据是字符串格式: '{data}',尝试转换为标准格式") - - # 尝试解析字符串格式,例如"性价比|必打卡" + # 处理文本列表 texts = [] - if "|" in data: - texts = data.split("|") - else: - texts = [data, ""] + if "texts" in data and isinstance(data["texts"], list): + texts = [self._preprocess_for_json(text) if text else "" for text in data["texts"]] # 应用预处理 + + # 确保文本列表至少有两个元素 + while len(texts) < 2: + texts.append("") fixed_data.append({ - "index": 1, - "main_title": "", + "index": data.get("index", 1), + "main_title": main_title, "texts": texts }) diff --git a/utils/content_judger.py b/utils/content_judger.py index d1e1e06..feab470 100644 --- a/utils/content_judger.py +++ b/utils/content_judger.py @@ -4,13 +4,14 @@ 内容审核模块:检查生成的内容是否符合产品资料要求并提供修改建议 """ -import simplejson as json +import json import logging -import re import os import time import traceback import sys +import base64 +import re sys.path.append('/root/autodl-tmp/TravelContentCreator') # 添加项目根目录 from core.ai_agent import AI_Agent @@ -107,18 +108,8 @@ class ContentJudger: } 输出结果: -{ "analysis" : " - 1、观察文案标题和内容,可以看出此文案主要面向亲子出游人群,因此修改后的文案也应该围绕亲子出游这一主题。 - 2、文章标题字数为28个字,超过19个字,因此属于不符内容。由于要求中提到尽量保留emoji,并且标题中数字后面的"元"字应删去,所以修改为:五一遛娃👶必囤!喜来登1088景观房 - 3、产品资料中未提及儿童乐园开放时间和儿童乐园配置,但文案中提到儿童乐园10:00-20:00全程开放,滑梯/积木/绘本一应俱全,因此属于不符内容。应修改为:儿童乐园:免费儿童乐园和丰富的游乐设施,让孩子们可以尽情玩耍。 - 4、产品材料中未提及户外泳池开放时间和消毒频次,但文案中提到户外泳池:9:00-18:00恒温开放(五一期间每日消毒3次),因此属于不符内容。应修改为:户外泳池:酒店配有户外无边泳池,供大人小孩一同享受清凉时光。 - 5、产品材料中未提及健身房开放时间与具体细节,但文案中提到健身房:8:00-22:00配备亲子瑜伽课程(需提前预约),因此属于不符内容。应修改为:健身房:酒店提供免费健身中心,方便您和家人一起强身健体。 - 6、产品材料中未提及餐厅硬件配置,但文案中提到自助晚餐隐藏彩蛋:儿童餐区设独立洗手台+热食保温柜,因此属于虚构内容。应修改为:自助餐厅:供应鲜美海鲜、精美甜品等任君选择,大人小孩都爱吃! - 7、产品材料中未提及酒店安保措施,但文案中提到安全保障:全区域监控+24小时安保巡逻,因此属于不符内容。应修改为:安全保障:酒店设有完善的监控系统和安保措施,无需担心您与家人的安全。 - 8、产品材料中未提及房内配有加厚床垫/卡通洗漱杯/尿布台(无需额外购买),因此属于不符内容。应回顾产品资料中关于房内配置的内容,修改为:房内配置:55英寸超大纯平电视+独立的浴缸+超大的落地玻璃窗,尽览蕉门河风景,尽享亲子度假时光。 - 9、产品材料中未提及五一专属加码,但文案中提到5月1-5日期间入住,凭房卡可免费领取儿童防晒冰袖+湿巾礼包,因此属于不符内容。应回顾产品资料,找到现有文案未提及的产品特色,修改为:套餐专属福利:1、豪华客房一间一晚(周一至四只开放双床房) 2、2大1小自助早晚餐 3、赠送2大1小水鸟世界门票(酒店前台领取),无需额外购买 - 10、产品资料中未提及水鸟世界门票领取有时间限制,但文案中提到水鸟世界门票需提前1小时至前台领取纸质票,因此属于不符内容。应修改为:酒店前台领取水鸟世界纸质门票 - 综合以上分析结果,将修改应用到原文案中,得到修改后的文案。", +{ + "analysis" : "1、观察文案标题和内容,可以看出此文案主要面向亲子出游人群,因此修改后的文案也应该围绕亲子出游这一主题。\n2、文章标题字数为28个字,超过19个字,因此属于不符内容。由于要求中提到尽量保留emoji,并且标题中数字后面的"元"字应删去,所以修改为:五一遛娃👶必囤!喜来登1088景观房\n3、产品资料中未提及儿童乐园开放时间和儿童乐园配置,但文案中提到儿童乐园10:00-20:00全程开放,滑梯/积木/绘本一应俱全,因此属于不符内容。应修改为:儿童乐园:免费儿童乐园和丰富的游乐设施,让孩子们可以尽情玩耍。\n4、产品材料中未提及户外泳池开放时间和消毒频次,但文案中提到户外泳池:9:00-18:00恒温开放(五一期间每日消毒3次),因此属于不符内容。应修改为:户外泳池:酒店配有户外无边泳池,供大人小孩一同享受清凉时光。 \n5、产品材料中未提及健身房开放时间与具体细节,但文案中提到健身房:8:00-22:00配备亲子瑜伽课程(需提前预约),因此属于不符内容。应修改为:健身房:酒店提供免费健身中心,方便您和家人一起强身健体。\n6、产品材料中未提及餐厅硬件配置,但文案中提到自助晚餐隐藏彩蛋:儿童餐区设独立洗手台+热食保温柜,因此属于虚构内容。应修改为:自助餐厅:供应鲜美海鲜、精美甜品等任君选择,大人小孩都爱吃!\n7、产品材料中未提及酒店安保措施,但文案中提到安全保障:全区域监控+24小时安保巡逻,因此属于不符内容。应修改为:安全保障:酒店设有完善的监控系统和安保措施,无需担心您与家人的安全。\n8、产品材料中未提及房内配有加厚床垫/卡通洗漱杯/尿布台(无需额外购买),因此属于不符内容。应回顾产品资料中关于房内配置的内容,修改为:房内配置:55英寸超大纯平电视+独立的浴缸+超大的落地玻璃窗,尽览蕉门河风景,尽享亲子度假时光。\n9、产品材料中未提及五一专属加码,但文案中提到5月1-5日期间入住,凭房卡可免费领取儿童防晒冰袖+湿巾礼包,因此属于不符内容。应回顾产品资料,找到现有文案未提及的产品特色,修改为:套餐专属福利:1、豪华客房一间一晚(周一至四只开放双床房) 2、2大1小自助早晚餐 3、赠送2大1小水鸟世界门票(酒店前台领取),无需额外购买。\n10、产品资料中未提及水鸟世界门票领取有时间限制,但文案中提到水鸟世界门票需提前1小时至前台领取纸质票,因此属于不符内容。应修改为:酒店前台领取水鸟世界纸质门票\n综合以上分析结果,将修改应用到原文案中,得到修改后的文案。", "title": "五一遛娃👶必囤!喜来登1088景观房", "content": "五一不想挤人潮?南沙这家酒店直接承包遛娃+度假双重快乐‼️\n地铁直达!2大1小1088r住景观房,含双早+自助晚餐+水鸟世界门票,儿童乐园/泳池/健身房全开放!\n🌟【遛娃刚需全配齐】\n✅ 儿童乐园:酒店设有免费儿童乐园,提供丰富的游乐设施,让孩子们尽情玩耍\n✅ 户外泳池:酒店配有户外无边泳池,供大人小孩一同享受清凉时光 \n✅ 健身房:酒店提供免费健身中心,适合家庭成员共同锻炼。\n\n📍【1小时玩转南沙】\n① 南沙天后宫(车程20分钟):穿汉服拍大片,听妈祖传说涨知识\n② 南沙湿地公园(40分钟):5月芦苇摇曳,带娃认鸟类+乘船探秘\n③ 十九涌海鲜街(45分钟):现捞现煮生猛海鲜,人均50r吃到撑 \n\n🍽️【家长友好细节】 \n• 自助餐厅:供应鲜美海鲜、精美甜品等任君选择,大人小孩都爱吃 \n• 房内配置:55英寸超大纯平电视+独立的浴缸+超大的落地玻璃窗,尽览蕉门河风景,尽享亲子度假时光 \n• 安全保障:酒店设有完善的监控系统和安保措施,全力保障您与家人的安全 \n\n🎁【套餐专属福利】\n1、豪华客房一间一晚(周一至四只开放双床房) \n2、2大1小自助早晚餐 \n3、赠送2大1小水鸟世界门票(酒店前台领取),无需额外购买 \n\n📌Tips: \n1. 周一至周四仅限双床房型,周五起可选大床房 \n2. 酒店前台领取水鸟世界纸质门票 \n3. 地铁四号线金洲站下车,打车15分钟直达酒店 \n\n这个五一,南沙喜来登让你躺着遛娃!不用长途跋涉,家门口就能玩出仪式感~\n" } @@ -163,125 +154,157 @@ class ContentJudger: logging.error(f"从PromptManager获取系统提示词失败: {e}") return False - def judge_content(self, product_info, content, temperature=0.2, top_p=0.5, presence_penalty=0.0): + def _split_content(self, result): """ - 审核内容是否符合产品资料并提供修改建议 + 参考tweet_generator的处理方式,解析AI返回的内容 Args: - product_info: 产品资料信息字符串 - content_json: 需要审核的内容JSON对象或JSON字符串 - temperature: 温度参数,控制随机性 - top_p: 核采样参数 - presence_penalty: 存在惩罚参数 + result: AI返回的原始结果 Returns: - dict: 审核后的结果JSON,包含修改后的title和content以及judge_success状态 + dict: 解析后的JSON数据 """ + try: + # 处理AI可能返回的思考部分 + processed_result = result + if "" in result: + processed_result = result.split("")[1] # 取标签后的内容 + + # 直接尝试解析JSON + json_data = json.loads(processed_result) + json_data["error"] = False + json_data["judge_success"] = True + return json_data + + except json.JSONDecodeError as json_err: + # JSON解析失败,记录错误并尝试更基本的处理方法 + logging.warning(f"解析内容时出错: {json_err}, 尝试提取JSON部分") + + try: + # 尝试找到JSON部分(从第一个{到最后一个}) + json_start = processed_result.find('{') + json_end = processed_result.rfind('}') + 1 + + if json_start >= 0 and json_end > json_start: + json_str = processed_result[json_start:json_end] + json_data = json.loads(json_str) + json_data["error"] = False + json_data["judge_success"] = True + return json_data + except Exception as e: + logging.error(f"尝试提取JSON部分失败: {e}") + + except Exception as e: + logging.error(f"解析内容时出错: {e}") + + # 所有解析方法都失败,返回一个默认结果 + return { + "title": "", + "content": "", + "error": True, + "judge_success": False, + "analysis": f"内容解析失败,错误信息: {str(e)}" + } + + def judge_content(self, product_info, content, temperature=0.2, top_p=0.5, presence_penalty=0.0): + """审核内容""" logging.info("开始内容审核流程") + # 构建用户提示词 user_prompt = self._build_user_prompt(product_info, content) + response_id = int(time.time()) try: - # 调用AI模型进行内容审核 - logging.info("调用AI模型进行内容审核") - start_time = time.time() - - # 使用AI_Agent的工作方法 + # 调用AI模型 result, _, _ = self.ai_agent.work( system_prompt=self._system_prompt, user_prompt=user_prompt, - file_folder=None, # 不使用文件夹 + file_folder=None, temperature=self._temperature, top_p=self._topp, presence_penalty=self._presence_penatly, ) - end_time = time.time() - logging.info(f"AI模型响应完成,耗时:{end_time - start_time:.2f}秒") + # 保存原始响应以便调试 + self._save_response(result, response_id) - # 保存原始响应用于调试 - response_log_dir = "/root/autodl-tmp/TravelContentCreator/log/judge_responses" - os.makedirs(response_log_dir, exist_ok=True) - response_log_file = f"{response_log_dir}/response_{int(time.time())}.txt" - with open(response_log_file, "w", encoding="utf-8") as f: - f.write(result) - logging.info(f"原始响应已保存到: {response_log_file}") + # 使用简化的解析方法处理响应 + content_json = self._split_content(result) + + # 检查解析结果是否有错误 + if content_json.get("error", False): + logging.warning(f"内容解析失败,使用原内容") + return self._create_fallback_result(content) + + # 检查必要字段是否存在 + if "title" not in content_json or "content" not in content_json: + logging.warning(f"解析结果缺少必要字段 'title' 或 'content'") + content_json["judge_success"] = False + return self._create_fallback_result(content) + + # 添加Base64编码内容 + result_dict = { + "judge_success": content_json.get("judge_success", True), + "judged": True, + "title": content_json["title"], + "content": content_json["content"], + "title_base64": base64.b64encode(content_json["title"].encode('utf-8')).decode('utf-8'), + "content_base64": base64.b64encode(content_json["content"].encode('utf-8')).decode('utf-8') + } + + # 如果有analysis字段,也包含 + if "analysis" in content_json: + result_dict["analysis"] = content_json["analysis"] + result_dict["analysis_base64"] = base64.b64encode(content_json["analysis"].encode('utf-8')).decode('utf-8') + + return result_dict - # 提取修改后的内容 - modified_content = self._extract_modified_content(result) - if modified_content: - logging.info("成功提取修改后的内容") - # 添加judge_success字段 - modified_content["judge_success"] = True - - # 对内容进行最终清理,确保可以安全序列化为JSON - modified_content = self._prepare_content_for_serialization(modified_content) - - # 记录处理后的内容用于调试 - debug_log_file = f"{response_log_dir}/processed_{int(time.time())}.json" - try: - serialized_content = json.dumps(modified_content, ensure_ascii=False, allow_nan=True, indent=2) - with open(debug_log_file, "w", encoding="utf-8") as f: - f.write(serialized_content) - logging.info(f"处理后的内容已保存到: {debug_log_file}") - except Exception as e: - logging.error(f"尝试记录处理后内容时序列化失败: {e}") - with open(debug_log_file, "w", encoding="utf-8") as f: - f.write(f"序列化失败: {str(e)}\n\n") - f.write(f"title: {modified_content.get('title', 'N/A')}\n") - f.write(f"content前100字符: {str(modified_content.get('content', 'N/A'))[:100]}") - - # 验证序列化是否成功 - try: - json.dumps(modified_content, ensure_ascii=False, allow_nan=True) - logging.info("内容可以安全序列化为JSON") - except Exception as e: - logging.error(f"验证序列化时出错: {e}") - # 找出导致错误的字段 - for key, value in modified_content.items(): - if isinstance(value, str): - try: - json.dumps(value, ensure_ascii=False) - except Exception as sub_e: - logging.error(f"字段 '{key}' 无法序列化: {sub_e}") - # 尝试定位问题字符 - for i, char in enumerate(value): - try: - json.dumps(char, ensure_ascii=False) - except: - logging.error(f"位置 {i}, 字符 '{char}' (Unicode: U+{ord(char):04X}) 导致错误") - - modified_content["raw_result"] = str(e) - modified_content["error"] = True - - return modified_content - else: - logging.error("无法从响应中提取有效内容") - # 尝试使用原始内容并标记审核失败 - if isinstance(content, dict) and "title" in content and "content" in content: - result_content = { - "title": content.get("title", "提取失败"), - "content": content.get("content", "无法从响应中提取有效内容"), - "judge_success": False - } - # 确保可以序列化 - return self._prepare_content_for_serialization(result_content) - result_content = { - "title": "提取失败", - "content": "无法从响应中提取有效内容", - "judge_success": False - } - return self._prepare_content_for_serialization(result_content) - except Exception as e: logging.exception(f"审核过程中出错: {e}") - result_content = { - "title": "审核失败", - "content": f"审核过程中出错: {str(e)}", - "judge_success": False - } - return self._prepare_content_for_serialization(result_content) - + return self._create_fallback_result(content, error_msg=str(e)) + + def _save_response(self, response, response_id): + """保存原始响应""" + try: + response_log_dir = "/root/autodl-tmp/TravelContentCreator/log/judge_responses" + os.makedirs(response_log_dir, exist_ok=True) + with open(f"{response_log_dir}/response_{response_id}.txt", "w", encoding="utf-8") as f: + f.write(response) + except Exception as e: + logging.error(f"保存原始响应失败: {e}") + + def _create_fallback_result(self, content, error_msg="解析失败"): + """创建回退结果""" + if isinstance(content, str): + # 尝试解析内容字符串看是否是JSON字符串 + try: + content_obj = json.loads(content) + title = content_obj.get("title", "") + content_text = content_obj.get("content", "") + except: + # 不是JSON字符串,视为纯文本内容 + title = "审核失败" + content_text = content + elif isinstance(content, dict): + # 已经是字典对象 + title = content.get("title", "") + content_text = content.get("content", "") + else: + # 其他类型,创建空内容 + title = "审核失败" + content_text = f"无法解析内容: {error_msg}" + + return { + "judge_success": False, + "judged": True, + "title": title, + "content": content_text, + "title_base64": base64.b64encode(title.encode('utf-8')).decode('utf-8'), + "content_base64": base64.b64encode(content_text.encode('utf-8')).decode('utf-8'), + "analysis": f"内容审核失败: {error_msg}", + "analysis_base64": base64.b64encode(f"内容审核失败: {error_msg}".encode('utf-8')).decode('utf-8') + } + def _build_user_prompt(self, product_info, content_gen): """ 构建用户提示词 @@ -293,367 +316,16 @@ class ContentJudger: Returns: str: 构建好的用户提示词 """ + # 确保content_gen为字符串格式 + if isinstance(content_gen, dict): + content_str = f"title: {content_gen.get('title', '')}\n\ncontent: {content_gen.get('content', '')}" + else: + content_str = str(content_gen) + return f""" ## 产品资料(真实信息,作为判断依据): {product_info} ## 运营生成的文案(需要审核的内容): -{content_gen} -""" - - def _extract_modified_content(self, result_text): - """从检测结果文本中提取修改后的文案内容""" - try: - processed_text = result_text # Work on a copy of the input text - # 记录原始文本前100个字符用于调试 - logging.debug(f"原始响应文本前100字符: {result_text[:100]}") - - # 尝试方法1: 使用标签分离内容 - if "" in processed_text: - processed_text = processed_text.split("", 1)[1].strip() - logging.debug("检测到标签并分离内容") - - # 尝试方法2: 预处理文本并尝试解析JSON - try: - # 彻底清理文本,去除所有可能影响JSON解析的控制字符 - cleaned_text = self._sanitize_json_text(processed_text) - logging.debug(f"清理后文本前100字符: {cleaned_text[:100]}") - - content_json = json.loads(cleaned_text) - if "title" in content_json and "content" in content_json: - logging.info("成功通过JSON解析提取内容") - title = content_json.get("title", "").strip() - content = content_json.get("content", "").strip() - analysis = content_json.get("analysis", "") - logging.debug(f"提取到标题: {title[:30]}...") - return { - "title": title, - "content": content, - "analysis": analysis - } - except json.JSONDecodeError as e: - logging.warning(f"JSON解析失败: {e},将尝试其他提取方法") - # 记录更多错误信息以便调试 - error_position = e.pos - error_context = cleaned_text[max(0, error_position-30):min(len(cleaned_text), error_position+30)] - logging.debug(f"错误位置附近的文本: {error_context}") - logging.debug(f"错误行列: 行 {e.lineno}, 列 {e.colno}") - - # 尝试方法3: 从文本中提取JSON格式部分 - json_start = processed_text.find('{') - json_end = processed_text.rfind('}') + 1 - if json_start >= 0 and json_end > json_start: - json_str = processed_text[json_start:json_end] - logging.debug(f"找到JSON字符串,长度: {len(json_str)},前100字符: {json_str[:100]}") - - # 清理可能破坏JSON解析的控制字符 - json_str_cleaned = self._sanitize_json_text(json_str) - try: - content_json = json.loads(json_str_cleaned) - if "title" in content_json and "content" in content_json: - logging.info("成功从文本中提取JSON部分并解析") - return { - "title": content_json.get("title", "").strip(), - "content": content_json.get("content", "").strip(), - "analysis": content_json.get("analysis", "") - } - except json.JSONDecodeError as e: - logging.warning(f"JSON子串解析失败: {e},将尝试正则表达式提取") - # 保存导致错误的JSON字符串到文件 - self._save_problematic_json(json_str_cleaned, e) - - # 尝试方法4: 手动解析JSON格式的关键字段 - try: - logging.debug("尝试手动解析JSON结构") - manual_result = self._manual_json_extract(processed_text) - if manual_result and "title" in manual_result and "content" in manual_result: - logging.info("成功通过手动解析JSON提取内容") - return manual_result - except Exception as e: - logging.warning(f"手动解析JSON失败: {e}") - - # 尝试方法5: 使用正则表达式提取 - logging.debug("尝试使用正则表达式提取") - # 更强大的正则表达式,处理多行内容 - title_match = re.search(r'"title"\s*:\s*"((?:[^"\\]|\\.|[\r\n])+)"', processed_text, re.DOTALL) - content_match = re.search(r'"content"\s*:\s*"((?:[^"\\]|\\.|[\r\n])+)"', processed_text, re.DOTALL) - analysis_match = re.search(r'"analysis"\s*:\s*"((?:[^"\\]|\\.|[\r\n])+)"', processed_text, re.DOTALL) - - if title_match and content_match: - logging.info("成功使用正则表达式提取标题和内容") - return { - "title": title_match.group(1).replace('\\"', '"').strip(), - "content": content_match.group(1).replace('\\"', '"').strip(), - "analysis": analysis_match.group(1).replace('\\"', '"').strip() if analysis_match else "" - } - - # 尝试方法6: 查找使用单引号的内容 - logging.debug("尝试查找使用单引号的内容") - title_match = re.search(r'"title"\s*:\s*\'((?:[^\'\\]|\\.|[\r\n])+)\'', processed_text, re.DOTALL) - content_match = re.search(r'"content"\s*:\s*\'((?:[^\'\\]|\\.|[\r\n])+)\'', processed_text, re.DOTALL) - analysis_match = re.search(r'"analysis"\s*:\s*\'((?:[^\'\\]|\\.|[\r\n])+)\'', processed_text, re.DOTALL) - - if title_match and content_match: - logging.info("成功使用单引号正则表达式提取内容") - return { - "title": title_match.group(1).strip(), - "content": content_match.group(1).strip(), - "analysis": analysis_match.group(1).strip() if analysis_match else "" - } - - # 尝试方法7: 使用非标准格式提取 - logging.debug("尝试非标准格式提取") - title_pattern = re.compile(r'["""]?title["""]?[::]\s*["""]([^"""]+)["""]', re.IGNORECASE | re.DOTALL) - content_pattern = re.compile(r'["""]?content["""]?[::]\s*["""]([^"""]+)["""]', re.IGNORECASE | re.DOTALL) - analysis_pattern = re.compile(r'["""]?analysis["""]?[::]\s*["""]([^"""]+)["""]', re.IGNORECASE | re.DOTALL) - - title_match = title_pattern.search(processed_text) - content_match = content_pattern.search(processed_text) - analysis_match = analysis_pattern.search(processed_text) - - if title_match and content_match: - logging.info("成功使用灵活模式匹配提取内容") - return { - "title": title_match.group(1).strip(), - "content": content_match.group(1).strip(), - "analysis": analysis_match.group(1).strip() if analysis_match else "" - } - - logging.warning(f"所有提取方法失败,响应前300字符: {processed_text[:300]}...") - return None # 所有方法失败时的回退选项 - - except Exception as e: - logging.error(f"内容提取过程中发生意外错误: {e}\n{traceback.format_exc()}") - return None - - def _sanitize_json_text(self, text): - """彻底清理文本,确保可以安全解析为JSON,同时保留换行符""" - # 步骤1: 处理控制字符,但保留换行符、回车和制表符 - cleaned = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', text) - - # 不再将实际换行符转换为\n字符串,保留原始换行符 - # cleaned = cleaned.replace('\n', '\\n').replace('\r', '\\r') - - # 步骤3: 处理内容字段中开始或结束可能存在的多余空格或引号 - cleaned = re.sub(r'"content"\s*:\s*"\s*', '"content":"', cleaned) - cleaned = re.sub(r'"\s*,', '",', cleaned) - - # 步骤4: 处理未转义的引号和反斜杠 - cleaned = re.sub(r'(?= 0: - colon_pos = text.find(':', title_start) - if colon_pos > 0: - quote_pos = text.find('"', colon_pos) - if quote_pos > 0: - end_quote_pos = text.find('"', quote_pos + 1) - while end_quote_pos > 0 and text[end_quote_pos-1] == '\\': - end_quote_pos = text.find('"', end_quote_pos + 1) - if end_quote_pos > 0: - result['title'] = text[quote_pos+1:end_quote_pos].replace('\\"', '"').strip() - - # 查找content字段 - content_start = text.find('"content"') - if content_start >= 0: - colon_pos = text.find(':', content_start) - if colon_pos > 0: - quote_pos = text.find('"', colon_pos) - if quote_pos > 0: - # 查找非转义双引号 - pos = quote_pos + 1 - content_end = -1 - while pos < len(text): - if text[pos] == '"' and (pos == 0 or text[pos-1] != '\\'): - content_end = pos - break - pos += 1 - - if content_end > 0: - content = text[quote_pos+1:content_end].replace('\\"', '"') - # 处理反斜杠转义的换行符,如果字符串中有'\n',将其转换为实际换行符 - # 但如果已经是实际的换行符,则保留 - if '\\n' in content: - content = content.replace('\\n', '\n') - if '\\r' in content: - content = content.replace('\\r', '\r') - result['content'] = content.strip() - - # 查找analysis字段 - analysis_start = text.find('"analysis"') - if analysis_start >= 0: - colon_pos = text.find(':', analysis_start) - if colon_pos > 0: - quote_pos = text.find('"', colon_pos) - if quote_pos > 0: - pos = quote_pos + 1 - analysis_end = -1 - while pos < len(text): - if text[pos] == '"' and (pos == 0 or text[pos-1] != '\\'): - analysis_end = pos - break - pos += 1 - - if analysis_end > 0: - analysis = text[quote_pos+1:analysis_end].replace('\\"', '"') - # 处理反斜杠转义的换行符 - if '\\n' in analysis: - analysis = analysis.replace('\\n', '\n') - if '\\r' in analysis: - analysis = analysis.replace('\\r', '\r') - result['analysis'] = analysis.strip() - - return result if 'title' in result and 'content' in result else None - except Exception as e: - logging.error(f"手动解析过程中出错: {e}") - return None - - def _save_problematic_json(self, json_text, error): - """保存导致解析错误的JSON字符串,用于调试""" - try: - error_log_dir = "/root/autodl-tmp/TravelContentCreator/log/json_errors" - os.makedirs(error_log_dir, exist_ok=True) - error_log_file = f"{error_log_dir}/error_{int(time.time())}.json" - - with open(error_log_file, "w", encoding="utf-8") as f: - f.write(f"# 错误信息: {str(error)}\n") - f.write(f"# 错误位置: 行 {error.lineno}, 列 {error.colno}\n") - f.write(json_text) - - logging.info(f"已保存问题JSON到: {error_log_file}") - except Exception as e: - logging.error(f"保存问题JSON时出错: {e}") - - def test_extraction_from_file(self, response_file_path): - """ - 从文件中读取响应并测试提取功能 - - Args: - response_file_path: 响应文件路径 - - Returns: - dict: 提取结果 - """ - try: - logging.info(f"从文件测试提取: {response_file_path}") - with open(response_file_path, 'r', encoding='utf-8') as f: - response_text = f.read() - - result = self._extract_modified_content(response_text) - if result: - logging.info(f"成功从文件提取内容: {result.get('title', '')[:30]}...") - return {"success": True, "result": result} - else: - logging.error(f"从文件中提取内容失败") - return {"success": False, "error": "提取失败"} - - except Exception as e: - logging.exception(f"测试提取时发生错误: {e}") - return {"success": False, "error": str(e)} - - def _prepare_content_for_serialization(self, content_dict): - """ - 对内容进行处理,确保可以安全序列化为JSON,同时保留emoji字符和换行符 - - Args: - content_dict: 内容字典 - - Returns: - dict: 处理后的内容字典 - """ - try: - # 创建一个新字典,避免修改原始内容 - safe_dict = {} - - for key, value in content_dict.items(): - # 处理字符串类型的值 - if isinstance(value, str): - # 第一步:清理控制字符,但保留换行符、回车和制表符 - safe_value = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', value) - - # 确保文本中的反斜杠换行符(如\\n)被转换为实际换行符 - if '\\n' in safe_value: - safe_value = safe_value.replace('\\n', '\n') - if '\\r' in safe_value: - safe_value = safe_value.replace('\\r', '\r') - - # 第二步:将emoji字符和其他非ASCII字符转换为相应的Unicode转义序列 - char_list = [] - for char in safe_value: - # 保留常见的控制字符(换行符、回车、制表符) - if char in '\n\r\t': - char_list.append(char) - elif ord(char) > 127: # 非ASCII字符 - # 尝试保留高位字符(包括emoji) - try: - # 验证这个字符是否可以安全序列化 - json.dumps(char, ensure_ascii=False) - char_list.append(char) - except: - # 如果这个字符无法序列化,使用其Unicode码点的字符串表示 - char_list.append(f"\\u{ord(char):04x}") - else: - char_list.append(char) - - processed_value = ''.join(char_list) - - # 最终验证这个值是否可以安全序列化 - try: - json.dumps(processed_value, ensure_ascii=False) - safe_dict[key] = processed_value - except Exception as e: - logging.warning(f"处理后的'{key}'值仍无法序列化: {e},将进行更严格处理") - # 更严格的处理:保留ASCII字符和基本控制字符 - safe_value = '' - for c in processed_value: - if c in '\n\r\t' or (32 <= ord(c) < 127): - safe_value += c - safe_dict[key] = safe_value - else: - safe_dict[key] = value - - # 最终验证整个字典是否可序列化 - try: - # 使用ensure_ascii=False允许非ASCII字符直接出现在JSON中 - # 使用allow_nan=True允许特殊浮点数值 - json_str = json.dumps(safe_dict, ensure_ascii=False, allow_nan=True) - # 验证生成的JSON是否有效 - json.loads(json_str) - except Exception as e: - logging.error(f"最终字典序列化验证失败: {e}") - # 如果依然失败,返回一个绝对安全的结果,但保留换行符 - safe_content = '' - original_content = content_dict.get("content", "内容包含无法安全序列化的字符") - for c in original_content: - if c in '\n\r\t' or (32 <= ord(c) < 127): - safe_content += c - - return { - "title": re.sub(r'[^\x20-\x7E]', '', content_dict.get("title", "序列化处理失败")), - "content": safe_content, - "judge_success": content_dict.get("judge_success", False), - "error": True, - "raw_result": str(e) - } - - return safe_dict - except Exception as e: - logging.error(f"序列化准备过程中发生意外错误: {e}") - return { - "title": "序列化处理失败", - "content": "处理内容时发生意外错误", - "judge_success": False, - "error": True, - "raw_result": str(e) - } \ No newline at end of file +{content_str} +""" \ No newline at end of file diff --git a/utils/output_handler.py b/utils/output_handler.py index 18353f6..b6d9b14 100644 --- a/utils/output_handler.py +++ b/utils/output_handler.py @@ -281,17 +281,65 @@ class FileSystemOutputHandler(OutputHandler): return "" return ''.join(c for c in text if 32 <= ord(c) <= 126) + def _preprocess_for_json(self, text): + """预处理文本,将换行符转换为\\n形式,保证JSON安全""" + if not isinstance(text, str): + return text + # 将所有实际换行符替换为\\n字符串 + return text.replace('\n', '\\n').replace('\r', '\\r') + 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 - json.dump(config_data, f_cfg_topic, ensure_ascii=False, indent=4, ignore_nan=True) - 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}") + # 创建目标目录 + variant_dir = os.path.join(self._get_run_dir(run_id), f"{topic_index}_1") + os.makedirs(variant_dir, exist_ok=True) + + # 确保配置数据是可序列化的 + processed_configs = [] + if isinstance(config_data, list): + for config in config_data: + processed_config = {} + # 处理索引字段 + processed_config["index"] = config.get("index", 0) + + # 处理标题字段,应用JSON预处理 + main_title = config.get("main_title", "") + processed_config["main_title"] = self._preprocess_for_json(main_title) + + # 处理文本字段列表,对每个文本应用JSON预处理 + texts = config.get("texts", []) + processed_texts = [] + for text in texts: + processed_texts.append(self._preprocess_for_json(text)) + processed_config["texts"] = processed_texts + + processed_configs.append(processed_config) + else: + # 如果不是列表,可能是字典或其他格式,尝试转换 + if isinstance(config_data, dict): + # 处理单个配置字典 + processed_config = {} + processed_config["index"] = config_data.get("index", 0) + processed_config["main_title"] = self._preprocess_for_json(config_data.get("main_title", "")) + + texts = config_data.get("texts", []) + processed_texts = [] + for text in texts: + processed_texts.append(self._preprocess_for_json(text)) + processed_config["texts"] = processed_texts + + processed_configs.append(processed_config) + + # 保存配置到JSON文件 + config_file_path = os.path.join(variant_dir, f"topic_{topic_index}_poster_configs.json") + with open(config_file_path, 'w', encoding='utf-8') as f: + json.dump(processed_configs, f, ensure_ascii=False, indent=4, cls=self.SafeJSONEncoder) + logging.info(f"Successfully saved poster configs to {config_file_path}") + except Exception as e: + logging.error(f"Error saving poster configs: {e}") + traceback.print_exc() def handle_generated_image(self, run_id: str, topic_index: int, variant_index: int, image_type: str, image_data, output_filename: str, metadata: dict = None): """处理生成的图像,对于笔记图像和额外配图保存到image目录,其他类型保持原有路径结构""" diff --git a/utils/tweet_generator.py b/utils/tweet_generator.py index 7bf8725..23b90c8 100644 --- a/utils/tweet_generator.py +++ b/utils/tweet_generator.py @@ -620,15 +620,47 @@ def generate_posters_for_topic(topic_item: dict, if os.path.exists(content_path): with open(content_path, 'r', encoding='utf-8') as f_content: content_data = json.load(f_content) + + # 支持Base64编码格式的文件 + if 'title_base64' in content_data and 'content_base64' in content_data: + import base64 + logging.info(f"检测到Base64编码的内容文件: {content_path}") + + # 解码Base64内容 + try: + title = base64.b64decode(content_data.get('title_base64', '')).decode('utf-8') + content = base64.b64decode(content_data.get('content_base64', '')).decode('utf-8') + + # 创建包含解码内容的新数据对象 + decoded_data = { + 'title': title, + 'content': content, + 'judge_success': content_data.get('judge_success', True), + 'judged': content_data.get('judged', True) + } + + # 如果有标签,也解码 + if 'tags_base64' in content_data: + tags = base64.b64decode(content_data.get('tags_base64', '')).decode('utf-8') + decoded_data['tags'] = tags + + loaded_content_list.append(decoded_data) + logging.debug(f" 已成功解码并加载Base64内容: {content_path}") + continue + except Exception as decode_error: + logging.error(f" 解码Base64内容时出错: {decode_error},跳过此文件") + continue + + # 常规JSON格式检查 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}") + 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.") + 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.") + 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}")