526 lines
17 KiB
Python
526 lines
17 KiB
Python
"""
|
||
@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}")
|