ai_service/scripts/generate_layout.py
Wang Xiuqiang 20802db28a 重构run_pipeline和generate_layout
将辅助函数移动整合到utils.py当中
2025-07-03 11:47:48 +08:00

526 lines
17 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.

"""
@file generate_layout.py
@brief Vue组件代码生成模块
基于DeepSeek API生成响应式Vue 3组件布局代码支持预定义模板
@author 王秀强 (2310460@mail.nankai.edu.cn)
@date 2025.5.19
@version v2.0.0
@details
本文件主要实现:
- DeepSeek API调用封装和错误处理
- Vue 3 Composition API组件代码生成
- 海报布局的动态排版和样式生成
- 预定义Vue模板系统
- 增强Vue代码生成逻辑
- 代码清理和格式化处理
@note
- 需要配置DEEPSEEK_API_KEY环境变量
- 支持流式和非流式响应模式
- 生成的Vue代码包含完整的template、script和style部分
- 具备指数退避重试机制处理API限流
- 支持基于图片类型的预定义模板
@usage
# 生成Vue组件代码
vue_code = generate_vue_code_enhanced("生成端午节海报Vue组件", parse_imglist, suggestions)
save_code(vue_code, "../outputs/poster.vue")
@copyright
(c) 2025 砚生项目组
"""
import os
import yaml
from openai import OpenAI
from dotenv import load_dotenv
import time
from colorama import init, Fore, Back, Style
from typing import List, Dict, Optional
# 初始化colorama
init(autoreset=True)
# === Config LLM call ===
load_dotenv()
deepseek_url = 'https://api.deepseek.com/v1' # set to be compatible with the OpenAI API
deepseek_api = os.getenv("DEEPSEEK_API_KEY")
if not deepseek_api:
raise ValueError("DEEPSEEK_API_KEY not set!")
def call_deepseek(
messages=None,
system_prompt="你是一个擅长前端开发的AI专注于生成Vue.js代码。",
prompt=None,
model='deepseek-chat',
temperature=0.6,
max_tokens=2000, # 增加token数量
stream=False,
max_retries=3,
):
"""
调用 DeepSeek API,支持多轮对话和流式/非流式响应
"""
# 初始化 OpenAI 客户端
client = OpenAI(
api_key=deepseek_api,
base_url=deepseek_url,
)
# 参数验证
if messages is None:
if not prompt:
raise ValueError("必须提供 message 或 prompt 参数")
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
elif not isinstance(messages, list) or not messages:
raise ValueError("message 参数必须是非空列表")
# 模型验证
models = ["deepseek-chat", "deepseek-reasoner"]
if model not in models:
raise ValueError(f"无效的模型名称: {model},可用模型: {models}")
# 调用 API
for attempt in range(max_retries):
try:
print(f"{Fore.BLUE}📡 正在调用DeepSeek API (尝试 {attempt + 1}/{max_retries})...{Style.RESET_ALL}")
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=stream
)
# 流式响应
if stream:
def stream_generator():
usage = None
for chunk in response:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
return usage
return stream_generator(), None
else:
# 非流式响应
content = response.choices[0].message.content
print(f"{Fore.GREEN}✅ API调用成功返回内容长度: {len(content)}{Style.RESET_ALL}")
return content, response.usage
except Exception as e:
print(f"{Fore.RED}❌ API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}{Style.RESET_ALL}")
if hasattr(e, 'status_code') and e.status_code == 429: # 限流
print(f"{Fore.YELLOW}⏳ 请求过于频繁,等待重试...{Style.RESET_ALL}")
time.sleep(2 ** attempt) # 指数退避
elif attempt == max_retries - 1:
raise
else:
time.sleep(1)
raise Exception("达到最大重试次数API 调用失败")
def load_vue_templates() -> Dict:
"""加载预定义的Vue模板配置"""
template_path = "../configs/vue_templates.yaml"
try:
with open(template_path, 'r', encoding='utf-8') as f:
templates = yaml.safe_load(f)
print(f"{Fore.GREEN}✅ Vue模板配置加载成功{Style.RESET_ALL}")
return templates.get('vue_templates', {})
except Exception as e:
print(f"{Fore.YELLOW}⚠️ Vue模板配置加载失败: {e},使用默认模板{Style.RESET_ALL}")
return {}
def get_template_by_images(parse_imglist: List[Dict]) -> Optional[str]:
"""
根据图片列表选择合适的预定义模板
参数:
parse_imglist: 图片信息列表
返回:
选中的模板代码如果没有匹配的模板则返回None
"""
templates = load_vue_templates()
if not templates or not parse_imglist:
return None
# 检查固定的图片文件
fixed_images = ["lotus.jpg", "nku.png", "stamp.jpg", "background.png"]
for img_name in fixed_images:
img_path = f"../outputs/{img_name}"
if os.path.exists(img_path) and img_name in templates:
print(f"{Fore.CYAN}📋 找到匹配的预定义模板: {img_name}{Style.RESET_ALL}")
return templates[img_name]['template']
print(f"{Fore.YELLOW}⚠️ 未找到匹配的预定义模板将使用AI生成{Style.RESET_ALL}")
return None
def generate_layout_prompt(user_input_analysis_result: Dict, parse_imglist: List[Dict], suggestions: Dict = None) -> str:
"""
生成增强的Vue布局提示包含文案内容
这个函数从run_pipeline.py移动到这里
"""
width = user_input_analysis_result.get("width", 1080)
height = user_input_analysis_result.get("height", 1920)
theme = user_input_analysis_result.get("main_theme", "活动海报")
# 构造图片信息字符串
images_info = "\n".join(
[f"- {img['picture_name']} ({img['picture_description']})" for img in parse_imglist]
)
# 构造文案信息
content_info = ""
if suggestions:
try:
if 'layer6_title_content' in suggestions:
title = suggestions['layer6_title_content'].get('content', theme)
content_info += f"- 主标题: {title}\n"
if 'layer7_subtitle_content' in suggestions:
subtitle = suggestions['layer7_subtitle_content'].get('content', '精彩活动,敬请参与')
content_info += f"- 副标题: {subtitle}\n"
if 'layer5_logo_content' in suggestions:
logo = suggestions['layer5_logo_content'].get('text', '主办方')
content_info += f"- Logo文字: {logo}\n"
except Exception as e:
print(f"{Fore.YELLOW}⚠️ 文案信息解析错误: {e}{Style.RESET_ALL}")
content_info = f"- 主标题: {theme}\n- 副标题: 精彩活动,敬请参与\n"
# 调用DeepSeek生成动态排版Prompt
system_prompt = "你是一个擅长前端开发的AI专注于生成Vue.js代码。请根据提供的信息生成完整的Vue组件包含所有必要的HTML结构和基础定位样式。"
prompt = f"""
请生成一个Vue.js组件代码用于{theme}海报,要求如下:
组件尺寸: {width}x{height}px
图片资源:
{images_info}
文案内容:
{content_info}
布局要求:
1. 背景图层: 使用第一张图片作为背景,占据整个组件区域
2. 主标题: 位于画布上方1/3处居中显示
3. 副标题: 位于主标题下方,居中显示
4. 内容区域: 使用剩余图片,合理布局在中间区域
5. Logo区域: 位于底部,居中显示
技术要求:
- 使用Vue 3 Composition API
- 使用absolute定位进行精确布局
- 包含完整的template、script和style部分
- 确保所有文本内容都正确显示
- 图片使用相对路径引用
请生成完整可用的Vue组件代码不要包含任何说明文字。
"""
try:
result, _ = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4)
return result
except Exception as e:
print(f"{Fore.RED}❌ 布局提示生成失败: {e}{Style.RESET_ALL}")
return generate_fallback_vue_code(theme, width, height)
def fill_template_content(template: str, suggestions: Dict) -> str:
"""
填充模板中的动态内容
参数:
template: Vue模板代码
suggestions: 文案建议
返回:
填充后的Vue代码
"""
filled_template = template
try:
# 提取文案内容
title_content = suggestions.get('layer6_title_content', {}).get('content', '默认标题')
subtitle_content = suggestions.get('layer7_subtitle_content', {}).get('content', '默认副标题')
logo_content = suggestions.get('layer5_logo_content', {}).get('text', '主办方')
# 替换模板占位符
filled_template = filled_template.replace('{{ title_content }}', title_content)
filled_template = filled_template.replace('{{ subtitle_content }}', subtitle_content)
filled_template = filled_template.replace('{{ logo_content }}', logo_content)
print(f"{Fore.GREEN}✅ 模板内容填充完成{Style.RESET_ALL}")
except Exception as e:
print(f"{Fore.YELLOW}⚠️ 模板填充出错: {e},使用默认内容{Style.RESET_ALL}")
return filled_template
def generate_vue_code_enhanced(
user_input_analysis_result: Dict,
parse_imglist: List[Dict],
suggestions: Dict = None,
prompt: str = None
) -> str:
"""
增强的Vue代码生成函数
优先使用预定义模板如果没有匹配的模板则使用AI生成
参数:
user_input_analysis_result: 用户输入分析结果
parse_imglist: 图片信息列表
suggestions: 文案建议
prompt: 自定义提示词(可选)
返回:
Vue组件代码
"""
print(f"{Fore.CYAN}🎨 开始增强Vue代码生成...{Style.RESET_ALL}")
# 1. 尝试使用预定义模板
template_code = get_template_by_images(parse_imglist)
if template_code and suggestions:
print(f"{Fore.GREEN}✅ 使用预定义模板生成Vue代码{Style.RESET_ALL}")
vue_code = fill_template_content(template_code, suggestions)
return vue_code
# 2. 如果没有合适的模板使用AI生成
print(f"{Fore.BLUE}🤖 使用AI生成Vue代码{Style.RESET_ALL}")
if not prompt:
prompt = generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions)
return generate_vue_code(prompt)
def generate_vue_code(prompt=None):
"""
原有的Vue代码生成函数保持兼容性
"""
if not prompt:
prompt = (
"生成一个Vue组件代码用于端午节活动海报包含以下部分并指定排版位置"
"1. 背景图层div占据整个组件区域。"
"2. 主体图层div位于顶部1/4处居中包含标题和副标题。"
"3. 活动亮点div位于底部1/4处居中使用网格布局展示三项活动每项包含图标、标题和描述"
"4. 页脚div位于底部居中包含主办单位信息和logo图片。"
"组件尺寸为1080x1920px布局使用absolute定位生成完整可用的Vue 3代码。"
)
system_prompt = (
"你是一个擅长前端开发的AI专注于生成Vue.js代码。"
"请生成完整的Vue 3组件包含template、script setup和style部分。"
"确保代码结构清晰,语法正确,可以直接使用。"
"不要包含任何解释文字,只返回纯代码。"
)
try:
print(f"{Fore.CYAN}🎨 正在生成Vue组件代码...{Style.RESET_ALL}")
result, usage = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4)
# 清理代码移除可能的markdown标记
if result:
# 移除markdown代码块标记
if "```vue" in result:
result = result.split("```vue")[1].split("```")[0].strip()
elif "```html" in result:
result = result.split("```html")[1].split("```")[0].strip()
elif result.startswith("```") and result.endswith("```"):
result = result[3:-3].strip()
print(f"{Fore.GREEN}✅ Vue代码生成成功长度: {len(result)} 字符{Style.RESET_ALL}")
return result
else:
print(f"{Fore.RED}❌ Vue代码生成失败返回空内容{Style.RESET_ALL}")
return generate_fallback_vue_code()
except Exception as e:
print(f"{Fore.RED}❌ Vue代码生成异常: {str(e)}{Style.RESET_ALL}")
return generate_fallback_vue_code()
def generate_fallback_vue_code(theme="默认主题", width=1080, height=1920):
"""
生成备用的Vue代码
"""
print(f"{Fore.YELLOW}⚠️ 使用备用Vue模板{Style.RESET_ALL}")
return f"""<template>
<div class="poster-container" :style="containerStyle">
<div class="background-layer">
<img src="../outputs/background.png" alt="背景" class="background-image" />
</div>
<div class="content-layer">
<div class="title-section">
<h1 class="main-title">{theme}</h1>
<h2 class="subtitle">精彩活动,敬请参与</h2>
</div>
<div class="main-content">
<div class="image-gallery">
<img src="../outputs/image1.png" alt="活动图片" class="content-image" />
</div>
</div>
<div class="footer-section">
<div class="logo-area">
<span class="logo-text">主办方</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {{ computed }} from 'vue'
const containerStyle = computed(() => ({{
width: '{width}px',
height: '{height}px',
position: 'relative',
overflow: 'hidden'
}}))
</script>
<style scoped>
.poster-container {{
position: relative;
background: #ffffff;
}}
.background-layer {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}}
.background-image {{
width: 100%;
height: 100%;
object-fit: cover;
}}
.content-layer {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}}
.title-section {{
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
text-align: center;
}}
.main-title {{
font-size: 48px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}}
.subtitle {{
font-size: 24px;
color: #666;
margin-bottom: 40px;
}}
.main-content {{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}}
.content-image {{
max-width: 400px;
max-height: 300px;
object-fit: cover;
}}
.footer-section {{
position: absolute;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
}}
.logo-text {{
font-size: 18px;
color: #666;
}}
</style>"""
def save_code(code, file_path="../outputs/generated_code.vue"):
"""
保存代码到文件
"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 写入文件
with open(file_path, "w", encoding="utf-8") as f:
f.write(code)
print(f"{Fore.GREEN}✅ Vue代码已保存到: {file_path}{Style.RESET_ALL}")
# 验证文件是否成功创建
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
print(f"{Fore.CYAN}📁 文件大小: {file_size} 字节{Style.RESET_ALL}")
else:
print(f"{Fore.RED}❌ 文件保存失败{Style.RESET_ALL}")
except Exception as e:
print(f"{Fore.RED}❌ 保存代码时出错: {str(e)}{Style.RESET_ALL}")
if __name__ == "__main__":
print(f"{Fore.MAGENTA}🚀 开始生成Vue组件...{Style.RESET_ALL}")
# 测试增强的Vue代码生成
test_analysis = {
"width": 1080,
"height": 1920,
"main_theme": "端午节海报"
}
test_imglist = [
{"picture_name": "background", "picture_description": "背景图片"},
{"picture_name": "lotus", "picture_description": "荷花装饰"}
]
test_suggestions = {
"layer6_title_content": {"content": "端午安康"},
"layer7_subtitle_content": {"content": "粽叶飘香,龙舟竞渡"},
"layer5_logo_content": {"text": "主办方"}
}
vue_code = generate_vue_code_enhanced(test_analysis, test_imglist, test_suggestions)
save_code(vue_code)
print(f"{Fore.GREEN}✅ Vue组件代码生成完成{Style.RESET_ALL}")