ai_service/scripts/generate_text.py

281 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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}")