# -*- coding: utf-8 -*- import os from openai import OpenAI from dotenv import load_dotenv import json import yaml from colorama import init, Fore, Back, Style # 初始化colorama init(autoreset=True) # 加载 .env 文件 load_dotenv() # 从环境变量中获取 API Key MOONSHOT_API_KEY = os.getenv("MOONSHOT_API_KEY") # --- 可配置的常量 --- DEFAULT_LOGO_TEXT = "" AVAILABLE_FONTS = [] NAMING_COLORS = {} STYLE_RULES = {} LOGO_RULES = {} def load_config_from_file(file_path): """从 YAML 文件加载配置""" global DEFAULT_LOGO_TEXT, AVAILABLE_FONTS, NAMING_COLORS, STYLE_RULES, LOGO_RULES print(f"{Fore.BLUE}📁 正在加载配置文件: {file_path}{Style.RESET_ALL}") try: with open(file_path, 'r', encoding='utf-8') as file: config_data = yaml.safe_load(file) DEFAULT_LOGO_TEXT = config_data.get("default_logo_text", DEFAULT_LOGO_TEXT) AVAILABLE_FONTS = config_data.get("available_fonts", []) NAMING_COLORS = config_data.get("NAMING_COLORS", {}) STYLE_RULES = config_data.get("STYLE_RULES", {}) LOGO_RULES = config_data.get("LOGO_RULES", {}) if not AVAILABLE_FONTS: print(f"{Fore.YELLOW}⚠️ 警告:未能从 YAML 配置中加载可用字体列表,或列表为空。{Style.RESET_ALL}") AVAILABLE_FONTS = [{"name": "SimHei", "displayName": "黑体 (简体)", "tags": ["通用"], "roles": ["title", "subtitle", "content"]}] print(f"{Fore.CYAN}📝 已使用默认字体配置{Style.RESET_ALL}") else: print(f"{Fore.GREEN}✅ 成功加载 {Style.BRIGHT}{len(AVAILABLE_FONTS)}{Style.RESET_ALL} 个字体配置") print(f"{Fore.GREEN}✅ YAML 配置文件加载成功。{Style.RESET_ALL}") print(f"{Fore.CYAN}📝 默认 Logo 文字: {Style.BRIGHT}{DEFAULT_LOGO_TEXT if DEFAULT_LOGO_TEXT else '未设置'}{Style.RESET_ALL}") except yaml.YAMLError as e: print(f"{Fore.RED}❌ 解析 YAML 文件时发生错误: {e}{Style.RESET_ALL}") AVAILABLE_FONTS = [{"name": "SimHei", "displayName": "黑体 (简体)", "tags": ["通用"], "roles": ["title", "subtitle", "content"]}] print(f"{Fore.YELLOW}📝 已使用内部备用字体列表。{Style.RESET_ALL}") except FileNotFoundError: print(f"{Fore.RED}❌ 错误:未找到文件 {file_path}{Style.RESET_ALL}") AVAILABLE_FONTS = [{"name": "SimHei", "displayName": "黑体 (简体)", "tags": ["通用"], "roles": ["title", "subtitle", "content"]}] print(f"{Fore.YELLOW}📝 已使用内部备用字体列表。{Style.RESET_ALL}") if not MOONSHOT_API_KEY: print(f"{Fore.RED}❌ 错误:未能从环境变量中获取 MOONSHOT_API_KEY,请检查 .env 文件。{Style.RESET_ALL}") exit() # 初始化 OpenAI 客户端 try: client = OpenAI(api_key=MOONSHOT_API_KEY, base_url="https://api.moonshot.cn/v1") print(f"{Fore.GREEN}✅ Kimi客户端初始化成功。{Style.RESET_ALL}") except Exception as e: print(f"{Fore.RED}❌ 初始化OpenAI客户端失败: {e}{Style.RESET_ALL}") exit() def extract_parameters_from_input(user_input): """ 使用模型从用户输入中提取主题、风格、元素、背景颜色和自定义Logo文字等信息。 """ print(f"{Fore.CYAN}🔍 正在提取参数: {Style.BRIGHT}{user_input[:50]}...{Style.RESET_ALL}") extraction_prompt = f""" 你是一个专业的自然语言理解助手,请从以下用户输入中提取海报设计的关键信息,并严格按照JSON格式返回: 用户输入:{user_input} 输出格式: {{ "poster_theme": "这里是海报的主题,例如:世界读书日、南开大学校庆", "style_desc": "这里是海报的风格描述,例如:现代、简约、有文化气息", "elements_to_include": ["这里是需要包含的元素,例如:南开大学教学楼", "书本", "阅读的人"], "background_is_light": true, "custom_logo_text": "这里是用户在输入中明确指定的、用作Logo的文字内容。例如,如果用户说'Logo文字用'技术创新研讨会'',则提取'技术创新研讨会'。如果用户没有明确指定Logo文字,则返回null或空字符串。" }} 注意:对于 "background_is_light",如果用户输入明确提及背景色深浅,则据此判断;若未提及或无法判断,默认为true (浅色)。 对于 "custom_logo_text",仅当用户明确指定logo文字时才提取,否则应为null或空字符串。 """ try: completion = client.chat.completions.create( model="moonshot-v1-8k", messages=[ {"role": "system", "content": "你是一个专业的自然语言理解助手,专门负责从用户输入中提取海报设计的关键参数,并严格以JSON格式输出。"}, {"role": "user", "content": extraction_prompt} ], temperature=0.2 ) response_content = completion.choices[0].message.content print(f"{Fore.GREEN}✅ 参数提取完成{Style.RESET_ALL}") json_str = response_content.strip() if "```json" in json_str: json_str = json_str.split("```json")[1].split("```")[0].strip() elif json_str.startswith("```") and json_str.endswith("```"): json_str = json_str[3:-3].strip() extracted_params = json.loads(json_str) # 提供默认值 if "background_is_light" not in extracted_params: extracted_params["background_is_light"] = True if "custom_logo_text" not in extracted_params: extracted_params["custom_logo_text"] = None return extracted_params except Exception as e: print(f"{Fore.RED}❌ 调用模型提取参数或解析JSON时发生错误: {e}{Style.RESET_ALL}") return { "poster_theme": "默认主题", "style_desc": "通用风格", "elements_to_include": [], "background_is_light": True, "custom_logo_text": None, "error": f"参数提取失败: {e}" } def generate_text_content_for_layers(poster_theme, style_desc, background_is_light, logo_text_to_display, elements_to_include=None): """ 调用 Kimi API 生成文案内容和设计建议(具体字体名称、颜色),包括指定的Logo文字。 AI 将从 AVAILABLE_FONTS (来自YAML) 中选择字体。 """ global AVAILABLE_FONTS # 确保能访问到从YAML加载的字体列表 if elements_to_include is None: elements_to_include = [] if not client: return {"error": "Kimi客户端未初始化."} background_color_desc = '浅色' if background_is_light else '深色' # 构建字体列表供AI参考 font_list_for_prompt = "当前可用的字体列表如下 (请从中选择 'name' 作为 font_name 的值):\n" if AVAILABLE_FONTS: for font in AVAILABLE_FONTS: font_list_for_prompt += ( f"- 名称(name): '{font['name']}' (显示名: {font.get('displayName', font['name'])}) - " f"风格标签: [{', '.join(font.get('tags', []))}] - " f"建议角色: [{', '.join(font.get('roles', []))}]\n" ) else: font_list_for_prompt += "- 名称(name): 'SimHei' (显示名: 黑体) - 风格标签: [通用] - 建议角色: [标题, 内容]\n" font_list_for_prompt += "注意:可用字体列表似乎未正确加载,请基于通用字体(如SimHei)给出建议。\n" # 提取背景颜色描述到变量中 background_color_desc = '浅色' if background_is_light else '深色' prompt_parts = [ f"你是一个专业的海报文案及设计元素建议助手,请为关于“{poster_theme}”的主题海报创作宣传文案和设计建议。", f"海报整体风格要求:{style_desc}。", f"设计背景是{background_color_desc}。", f"Logo文字已确定为:“{logo_text_to_display}”。", "--------------------------------------------------", "可用字体参考(请为下面的 'font_name' 字段从以下列表的 '名称(name)' 中选择一个最合适的):", font_list_for_prompt, # 注入格式化后的字体列表 "--------------------------------------------------", "请严格按照以下JSON格式返回结果,仅包含针对指定图层的文本内容、从上述列表中选择的具体字体名称和颜色。所有建议需与海报主题、风格及背景色协调:", "{", f' "layer5_logo_content": {{ "text": "{logo_text_to_display}", "color": "这里是为Logo文字“{logo_text_to_display}”在背景色 ({background_color_desc}) 上推荐的颜色。例如:浅色背景可使用主题色如南开紫(#7E0C6E)或黑色(#000000),深色背景可使用白色(#FFFFFF)。请提供符合要求的具体十六进制颜色值。" }},', f' "layer6_title_content": {{ "content": "这里是为“{poster_theme}”生成的主标题。如果用户输入中明确指定了主题(例如:世界读书日、端午、秋分),则标题必须直接使用该主题,且长度严格控制在2到8个汉字之间。", "font_name": "从上面提供的可用字体列表中为标题选择的具体字体\'名称(name)\' (例如: SimHei, FZLanTingHei-ExtraBold-GB)。", "color": "这里是建议的标题文字颜色,需确保在背景上高对比度且与整体风格和谐,可以根据背景选择白色(#FFFFFF)、米色(#f4efd9)、黑色(#000000)等。请提供具体十六进制颜色值。" }},', f''' "layer7_subtitle_content": {{ "content": "这里是为“{poster_theme}”生成的副标题或说明文案。请根据主题选择以下一种形式并创作,确保内容富有文化内涵或情感共鸣,避免空洞口号:\n 1. 点题短语或对偶句(总计10-25字)。\n 2. 精美描述性或介绍性文字(一段话,20-35字)。\n 3. 若主题适合诗歌,可为原创短诗(例如绝句的前两句或类似形式,总计10-20字)。", "font_name": "从上面提供的可用字体列表中为副标题选择的具体字体\'名称(name)\',应与主标题协调。", "color": "这里是建议的副标题文字颜色,确保清晰可读,并与整体色调和谐,避免过于鲜艳的颜色。请提供具体十六进制颜色值。" }}''', "}", "重要提示:", "1. 为 'content' 字段生成的文本必须是直接的文字内容,不应包含诸如“这里是...”之类的描述性前缀。", "2. 'font_name' 字段的值必须是从上面提供的可用字体列表中的一个确切的 '名称(name)'。", "3. 'color' 字段应提供明确的十六进制颜色代码(例如“#FFFFFF”)。", "4. 确保最终输出是完整且严格符合上述格式的JSON对象,不要在JSON内容之外添加任何解释、注释、Markdown标记(如 ```json ```)或其他任何非JSON字符。" ] user_prompt = "\n".join(prompt_parts) system_prompt_content = "你是一个专业的海报文案及设计元素建议助手。请严格按照用户要求的JSON格式输出,确保内容符合设计原则、主题文化内涵,并直接输出JSON内容,不要包含任何修饰性或解释性的文字,例如 '好的,这是您要求的JSON:' 或 '```json' 等标记。" print(f"{Fore.BLUE}✍️ 正在生成文案内容...{Style.RESET_ALL}") try: completion = client.chat.completions.create( model="moonshot-v1-8k", messages=[ {"role": "system", "content": system_prompt_content}, {"role": "user", "content": user_prompt} ], temperature=0.4 ) response_content = completion.choices[0].message.content json_str = response_content.strip() if "```json" in json_str: json_str = json_str.split("```json")[1].split("```")[0].strip() elif json_str.startswith("```") and json_str.endswith("```"): json_str = json_str[3:-3].strip() parsed_json = json.loads(json_str) print(f"{Fore.GREEN}✅ 文案生成成功{Style.RESET_ALL}") return parsed_json except Exception as e: print(f"{Fore.RED}❌ 调用Kimi或解析JSON时发生错误: {e}{Style.RESET_ALL}") print(f"{Fore.YELLOW}原始回复内容: {response_content if 'response_content' in locals() else 'N/A'}{Style.RESET_ALL}") return {"error": f"LLM调用或解析失败: {e}"} def get_poster_content_suggestions(user_input_string: str): """ 主接口函数:接收用户输入,提取参数,确定Logo文字,并生成海报图层内容建议。 返回包含建议内容的字典,或包含错误信息的字典。 """ print(f"{Fore.MAGENTA}--- 开始处理用户输入 ---{Style.RESET_ALL}") # 1. 提取参数 extracted_params = extract_parameters_from_input(user_input_string) if "error" in extracted_params: print(f"{Fore.RED}参数提取过程中发生错误: {extracted_params['error']}{Style.RESET_ALL}") return extracted_params # 返回包含错误信息的字典 poster_theme = extracted_params.get("poster_theme", "未知主题") style_desc = extracted_params.get("style_desc", "默认风格") elements_to_include = extracted_params.get("elements_to_include", []) background_is_light = extracted_params.get("background_is_light", True) custom_logo_text = extracted_params.get("custom_logo_text") print(f"{Fore.YELLOW}📊 提取到的参数:") print(f" 主题: {Style.BRIGHT}{poster_theme}{Style.RESET_ALL}") print(f" 风格: {style_desc}") print(f" 背景: {'浅色' if background_is_light else '深色'}") print(f" Logo: {custom_logo_text if custom_logo_text else '未指定'}{Style.RESET_ALL}") # 2. Logo文字选择逻辑 final_logo_text = DEFAULT_LOGO_TEXT # 默认为 "" if custom_logo_text and custom_logo_text.strip(): # 如果用户指定了且不为空 final_logo_text = custom_logo_text.strip() print(f"{Fore.CYAN}📝 使用用户自定义Logo文字: “{Style.BRIGHT}{final_logo_text}{Style.RESET_ALL}”") else: print(f"{Fore.CYAN}📝 使用默认Logo文字: “{final_logo_text}”{Style.RESET_ALL}") # 3. 调用生成函数 generated_content = generate_text_content_for_layers( poster_theme, style_desc, background_is_light, final_logo_text, # 传递最终确定的Logo文字 elements_to_include ) if "error" in generated_content: print(f"{Fore.RED}❌ 内容生成过程中发生错误: {generated_content['error']}{Style.RESET_ALL}") else: print(f"{Fore.GREEN}✅ 成功生成文案及设计建议{Style.RESET_ALL}") return generated_content if __name__ == "__main__": load_config_from_file("../configs/font.yaml") user_input = input("请输入您的海报需求:") # 调用主接口函数 suggestions = get_poster_content_suggestions(user_input) # 打印结果 if suggestions: output_folder = "../outputs" output_file = os.path.join(output_folder, "poster_content.json") try: with open(output_file, "w", encoding="utf-8") as file: json.dump(suggestions, file, indent=2, ensure_ascii=False) print(f"{Fore.GREEN}✅ 内容已成功保存到文件: {output_file}{Style.RESET_ALL}") except Exception as e: print(f"{Fore.RED}❌ 保存到文件时发生错误: {e}{Style.RESET_ALL}") else: print(f"{Fore.RED}❌ 未生成任何内容,无法保存到文件。{Style.RESET_ALL}")