""" * @file run_pipeline.py * @brief AI海报生成系统主服务入口和API服务器 * 集成多个AI模型提供统一的海报生成接口 * * @author 王秀强 (2310460@mail.nankai.edu.cn) * @date 2025.6.9 * @version v1.0.0 * * @details * 本文件主要实现: * - FastAPI服务器和RESTful API接口 * - 用户输入分析和海报生成流程编排 * - Vue组件代码生成和PSD文件合成 * - 会话管理和文件下载服务 * - 集成DeepSeek、Kimi、ComfyUI等AI服务 * * @note * - 依赖外部ComfyUI服务(101.201.50.90:8188)进行图片生成 * - 需要配置DEEPSEEK_API_KEY和MOONSHOT_API_KEY环境变量 * - PSD生成优先使用手动创建的模板文件 * - 支持CORS跨域访问,生产环境需调整安全配置 * * @usage * # API服务器模式 * python run_pipeline.py * # 选择: 2 (API服务器模式) * * # 本地测试模式 * python run_pipeline.py * # 选择: 1 (本地测试模式) * * @copyright * (c) 2025 砚生项目组 */ """ import os from dotenv import load_dotenv import yaml from prompt_analysis import llm_user_analysis from generate_layout import call_deepseek, generate_vue_code, save_code from generate_text import load_config_from_file, get_poster_content_suggestions from fastapi import FastAPI from fastapi.responses import FileResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from flux_con import comfyui_img_info from export_psd_from_json import create_psd_from_images as create_psd_impl from colorama import init, Fore, Style import json import shutil import uuid from datetime import datetime # 初始化colorama init(autoreset=True) # 配置路径 config_paths = { "font": "../configs/font.yaml", "output_folder": "../outputs/", } # 加载环境变量和配置 load_dotenv() app = FastAPI(title="AI海报生成系统API", version="1.0.0") # 添加CORS中间件,允许前端跨域访问 app.add_middleware( CORSMiddleware, allow_origins=["*"], # 在生产环境中应该设置具体的域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 请求模型 class PosterRequest(BaseModel): user_input: str session_id: str = None # 可选的会话ID,用于跟踪同一个用户的请求 class GenerateVueRequest(BaseModel): user_input: str session_id: str = None class GeneratePSDRequest(BaseModel): user_input: str session_id: str = None use_manual_psd: bool = False # 是否使用手动创建的PSD文件 # 响应模型 class ApiResponse(BaseModel): status: str message: str data: dict = None session_id: str = None # 全局变量存储会话数据 sessions = {} # 加载字体配置 try: with open(config_paths["font"], "r", encoding="utf-8") as f: fonts_config = yaml.safe_load(f) print(f"{Fore.GREEN}✅ 字体配置加载成功{Style.RESET_ALL}") except Exception as e: print(f"{Fore.YELLOW}⚠️ 字体配置加载失败: {e},使用默认配置{Style.RESET_ALL}") fonts_config = {} # 辅助函数 def print_step(step_num, description, status="进行中"): """打印带颜色的步骤信息""" if status == "进行中": print(f"{Fore.BLUE}📋 步骤{step_num}: {description}...{Style.RESET_ALL}") elif status == "完成": print(f"{Fore.GREEN}✅ 步骤{step_num}: {description} - 完成{Style.RESET_ALL}") elif status == "错误": print(f"{Fore.RED}❌ 步骤{step_num}: {description} - 出错{Style.RESET_ALL}") def print_result(key, value): """打印结果信息""" print(f"{Fore.CYAN}📊 {key}: {value}{Style.RESET_ALL}") def get_session_folder(session_id): """获取会话专用的输出文件夹""" if not session_id: session_id = str(uuid.uuid4()) session_folder = os.path.join(config_paths["output_folder"], f"session_{session_id}") os.makedirs(session_folder, exist_ok=True) return session_folder, session_id # 生成prompts.yaml的函数 def generate_prompts_yaml(user_input=None): """ 动态生成prompts.yaml配置文件 """ if not user_input: user_input = "端午节海报,包含背景、活动亮点和图标" prompts_config = { "default_logo_text": "", "available_fonts": [ { "name": "Microsoft YaHei", "displayName": "微软雅黑", "tags": ["现代", "清晰"], "roles": ["title", "subtitle", "content"] }, { "name": "SimHei", "displayName": "黑体", "tags": ["通用", "标准"], "roles": ["title", "subtitle", "content"] } ], "NAMING_COLORS": { "primary": "#1976D2", "secondary": "#424242", "accent": "#FF5722" }, "STYLE_RULES": { "modern": { "primary_font": "Microsoft YaHei", "secondary_font": "SimHei" } }, "LOGO_RULES": { "default_position": "bottom", "fallback_text": "活动主办方" } } # 保存到临时文件 temp_prompts_path = os.path.join(config_paths["output_folder"], "temp_prompts.yaml") os.makedirs(os.path.dirname(temp_prompts_path), exist_ok=True) with open(temp_prompts_path, 'w', encoding='utf-8') as f: yaml.dump(prompts_config, f, allow_unicode=True, default_flow_style=False) print(f"{Fore.GREEN}✅ 临时prompts.yaml已生成: {temp_prompts_path}{Style.RESET_ALL}") return temp_prompts_path # 修复PSD合成接口 - 使用临时固定图片列表 def create_psd_from_images_wrapper(img_list, vue_layout_path, output_path): """ 临时接口:使用固定的图片文件创建PSD文件 使用outputs目录下的: lotus.jpg, nankai.png, stamp.jpg, background.png """ print(f"{Fore.CYAN}🎨 开始合成PSD文件(临时接口)...{Style.RESET_ALL}") try: # 使用固定的图片文件列表 fixed_image_files = ["background.png", "lotus.jpg", "nankai.png", "stamp.jpg"] image_paths = [] for img_file in fixed_image_files: img_path = os.path.join(config_paths["output_folder"], img_file) if os.path.exists(img_path): image_paths.append(img_path) print(f"{Fore.GREEN}✓ 找到图片: {img_file}{Style.RESET_ALL}") else: print(f"{Fore.YELLOW}⚠️ 图片不存在: {img_file} (路径: {img_path}){Style.RESET_ALL}") if not image_paths: print(f"{Fore.RED}❌ 没有找到任何指定的图片文件{Style.RESET_ALL}") return None print(f"{Fore.CYAN}📋 将合并以下图片 (共{len(image_paths)}张):") for i, path in enumerate(image_paths): print(f" {i + 1}. {os.path.basename(path)}") # 确保输出目录存在 os.makedirs(os.path.dirname(output_path), exist_ok=True) # 调用export_psd中的PSD创建函数 create_psd_impl( image_paths=image_paths, output_path=output_path, canvas_size=(1080, 1920), # 使用标准海报尺寸 mode='RGB' ) print(f"{Fore.GREEN}✅ PSD文件创建成功: {output_path}{Style.RESET_ALL}") # 验证PSD文件是否成功创建 if os.path.exists(output_path): file_size = os.path.getsize(output_path) / (1024 * 1024) # 转换为MB print(f"{Fore.CYAN}📁 PSD文件大小: {file_size:.2f} MB{Style.RESET_ALL}") return output_path except Exception as e: print(f"{Fore.RED}❌ PSD文件创建失败: {str(e)}{Style.RESET_ALL}") import traceback traceback.print_exc() return None # 增强Vue代码生成,确保包含文案内容 def generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions=None): """ 生成更完整的Vue布局提示,包含文案内容 """ width = user_input_analysis_result["width"] height = user_input_analysis_result["height"] 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 generate_fallback_vue_code(theme, width=1080, height=1920): """ 生成备用的Vue代码 """ return f""" """ # 一键执行流程 def run_pipeline(user_input=None): """ 自动执行海报生成流程 """ try: print(f"{Fore.MAGENTA}{'=' * 50}") print(f"{Fore.MAGENTA}🎨 海报生成流程启动 🎨") print(f"{'=' * 50}{Style.RESET_ALL}") print_step(1, "加载配置文件") prompts_yaml_path = generate_prompts_yaml(user_input) load_config_from_file(prompts_yaml_path) print_step(1, "加载配置文件", "完成") print_step(2, "分析用户输入") user_input_analysis_result = llm_user_analysis(user_input) print_result("分析结果", user_input_analysis_result.get('main_theme', '未知')) print_step(2, "分析用户输入", "完成") print_step(3, "生成图片信息") system_prompt = user_input_analysis_result["analyzed_prompt"] parse_imglist = comfyui_img_info(user_input_analysis_result, system_prompt) print_result("生成图片数量", len(parse_imglist)) print_step(3, "生成图片信息", "完成") print_step(4, "生成文案建议") suggestions = get_poster_content_suggestions(user_input_analysis_result["analyzed_prompt"]) print(f"{Fore.CYAN}文案生成结果:") print(json.dumps(suggestions, indent=2, ensure_ascii=False)) # 保存文案到文件 suggestions_path = os.path.join(config_paths["output_folder"], "poster_content.json") with open(suggestions_path, "w", encoding="utf-8") as f: json.dump(suggestions, f, indent=2, ensure_ascii=False) print_step(4, "生成文案建议", "完成") print_step(5, "生成Vue排版") dynamic_prompt = generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions) vue_code = generate_vue_code(dynamic_prompt) vue_path = os.path.join(config_paths["output_folder"], "generated_code.vue") save_code(vue_code, file_path=vue_path) # 验证Vue文件是否成功生成 if os.path.exists(vue_path): print(f"{Fore.GREEN}✅ Vue文件已生成: {vue_path}{Style.RESET_ALL}") # 显示Vue代码的前几行用于验证 with open(vue_path, 'r', encoding='utf-8') as f: preview = f.read()[:500] print(f"{Fore.CYAN}Vue代码预览:\n{preview}...{Style.RESET_ALL}") else: print(f"{Fore.RED}❌ Vue文件生成失败{Style.RESET_ALL}") print_step(5, "生成Vue排版", "完成") print_step(6, "合成PSD文件") img_list = [(pic["picture_name"], pic["picture_type"]) for pic in parse_imglist] psd_path = os.path.join(config_paths["output_folder"], "final_poster.psd") result_path = create_psd_from_images_wrapper( img_list=img_list, vue_layout_path=vue_path, output_path=psd_path ) if result_path: print_step(6, "合成PSD文件", "完成") else: print_step(6, "合成PSD文件", "错误") print(f"\n{Fore.GREEN}{'=' * 50}") print(f"✅ 流程执行完成!") print(f"{'=' * 50}{Style.RESET_ALL}") return os.path.join(config_paths["output_folder"], "final_poster.png") except Exception as e: print_step("X", f"Pipeline执行", "错误") print(f"{Fore.RED}错误详情: {str(e)}{Style.RESET_ALL}") import traceback traceback.print_exc() return None # 本地运行函数 def run_local_pipeline(user_input=None): """ 本地运行整个管道流程,输出结果到控制台和文件系统。 """ print(f"{Fore.CYAN}🎬 Starting local pipeline with input: {Style.BRIGHT}{user_input}{Style.RESET_ALL}") output_path = run_pipeline(user_input) if output_path: print(f"{Fore.GREEN}🎊 Pipeline completed successfully!") print(f"{Fore.YELLOW}📁 Results saved to:") print(f" - Vue layout: {os.path.join(config_paths['output_folder'], 'generated_code.vue')}") print(f" - PSD file: {os.path.join(config_paths['output_folder'], 'final_poster.psd')}") print(f" - Content JSON: {os.path.join(config_paths['output_folder'], 'poster_content.json')}") print(f"{Fore.CYAN}💡 Check the outputs/ directory for generated files.{Style.RESET_ALL}") else: print(f"{Fore.RED}❌ Pipeline执行失败{Style.RESET_ALL}") # API路由 @app.get("/") def read_root(): return { "message": "AI海报生成系统API", "version": "1.0.0", "endpoints": { "generate_poster": "/api/generate-poster", # 主要接口 "download": "/api/download/{file_type}", "health": "/health", "status": "/api/status/{session_id}" } } @app.get("/health") def health_check(): """健康检查接口""" return {"status": "healthy", "timestamp": datetime.now().isoformat()} @app.post("/api/generate-poster", response_model=ApiResponse) async def generate_poster_api(request: PosterRequest): """ 一键生成完整海报(包含Vue代码和PSD文件)的主要API接口 """ try: session_folder, session_id = get_session_folder(request.session_id) print(f"{Fore.BLUE}🎨 开始生成海报...{Style.RESET_ALL}") print(f"{Fore.CYAN}用户输入: {request.user_input}{Style.RESET_ALL}") # === 步骤1: 生成配置文件 === print(f"{Fore.BLUE}📋 步骤1: 生成配置文件{Style.RESET_ALL}") temp_prompts_path = os.path.join(session_folder, "temp_prompts.yaml") prompts_config = { "default_logo_text": "", "available_fonts": [ { "name": "Microsoft YaHei", "displayName": "微软雅黑", "tags": ["现代", "清晰"], "roles": ["title", "subtitle", "content"] }, { "name": "SimHei", "displayName": "黑体", "tags": ["通用", "标准"], "roles": ["title", "subtitle", "content"] } ] } with open(temp_prompts_path, 'w', encoding='utf-8') as f: yaml.dump(prompts_config, f, allow_unicode=True, default_flow_style=False) load_config_from_file(temp_prompts_path) # === 步骤2: 分析用户输入 === print(f"{Fore.BLUE}📋 步骤2: 分析用户输入{Style.RESET_ALL}") user_input_analysis_result = llm_user_analysis(request.user_input) # === 步骤3: 生成图片信息 === print(f"{Fore.BLUE}📋 步骤3: 生成图片信息{Style.RESET_ALL}") system_prompt = user_input_analysis_result["analyzed_prompt"] parse_imglist = comfyui_img_info(user_input_analysis_result, system_prompt) # === 步骤4: 生成文案建议 === print(f"{Fore.BLUE}📋 步骤4: 生成文案建议{Style.RESET_ALL}") suggestions = get_poster_content_suggestions(user_input_analysis_result["analyzed_prompt"]) # 保存文案到会话文件夹 suggestions_path = os.path.join(session_folder, "poster_content.json") with open(suggestions_path, "w", encoding="utf-8") as f: json.dump(suggestions, f, indent=2, ensure_ascii=False) # === 步骤5: 生成Vue排版 === print(f"{Fore.BLUE}📋 步骤5: 生成Vue排版{Style.RESET_ALL}") dynamic_prompt = generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions) vue_code = generate_vue_code(dynamic_prompt) vue_path = os.path.join(session_folder, "generated_code.vue") save_code(vue_code, file_path=vue_path) # === 步骤6: 合成PSD文件 === print(f"{Fore.BLUE}📋 步骤6: 合成PSD文件{Style.RESET_ALL}") img_list = [(pic["picture_name"], pic["picture_type"]) for pic in parse_imglist] psd_path = os.path.join(session_folder, "final_poster.psd") # 修复PSD创建调用 try: # 使用固定的图片文件列表 fixed_image_files = ["background.png", "lotus.jpg", "nankai.png", "stamp.jpg"] image_paths = [] for img_file in fixed_image_files: img_path = os.path.join(config_paths["output_folder"], img_file) if os.path.exists(img_path): image_paths.append(img_path) if image_paths: # 确保输出目录存在 os.makedirs(os.path.dirname(psd_path), exist_ok=True) # 调用PSD创建函数 create_psd_impl( image_paths=image_paths, output_path=psd_path, canvas_size=(1080, 1920), mode='RGB' ) print(f"{Fore.GREEN}✅ PSD文件创建成功: {psd_path}{Style.RESET_ALL}") psd_created = True else: print(f"{Fore.YELLOW}⚠️ 没有找到图片文件,跳过PSD创建{Style.RESET_ALL}") psd_created = False except Exception as psd_error: print(f"{Fore.RED}❌ PSD文件创建失败: {str(psd_error)}{Style.RESET_ALL}") psd_created = False # 返回API响应 return ApiResponse( status="success", message="海报生成完成", data={ "vue_file": vue_path if os.path.exists(vue_path) else None, "psd_file": psd_path if psd_created else None, "content_file": suggestions_path, "analysis_result": user_input_analysis_result, "images_info": parse_imglist, "suggestions": suggestions }, session_id=session_id ) except Exception as e: print(f"{Fore.RED}❌ API错误: {str(e)}{Style.RESET_ALL}") import traceback traceback.print_exc() return ApiResponse( status="error", message=f"生成失败: {str(e)}", data=None, session_id=session_id if 'session_id' in locals() else None ) @app.get("/api/download/{file_type}") async def download_file(file_type: str, session_id: str = None): """ 下载生成的文件 """ try: if session_id: session_folder = os.path.join(config_paths["output_folder"], f"session_{session_id}") else: session_folder = config_paths["output_folder"] if file_type == "vue": file_path = os.path.join(session_folder, "generated_code.vue") media_type = "text/plain" elif file_type == "psd": file_path = os.path.join(session_folder, "final_poster.psd") media_type = "application/octet-stream" elif file_type == "json": file_path = os.path.join(session_folder, "poster_content.json") media_type = "application/json" else: return JSONResponse( status_code=400, content={"error": "不支持的文件类型"} ) if os.path.exists(file_path): return FileResponse( path=file_path, media_type=media_type, filename=os.path.basename(file_path) ) else: return JSONResponse( status_code=404, content={"error": "文件不存在"} ) except Exception as e: return JSONResponse( status_code=500, content={"error": f"下载失败: {str(e)}"} ) @app.get("/api/status/{session_id}") async def get_session_status(session_id: str): """ 获取会话状态 """ try: session_folder = os.path.join(config_paths["output_folder"], f"session_{session_id}") if not os.path.exists(session_folder): return JSONResponse( status_code=404, content={"error": "会话不存在"} ) files_status = { "vue_file": os.path.exists(os.path.join(session_folder, "generated_code.vue")), "psd_file": os.path.exists(os.path.join(session_folder, "final_poster.psd")), "content_file": os.path.exists(os.path.join(session_folder, "poster_content.json")) } return { "session_id": session_id, "files": files_status, "folder": session_folder } except Exception as e: return JSONResponse( status_code=500, content={"error": f"状态查询失败: {str(e)}"} ) if __name__ == "__main__": import uvicorn # 启动本地运行(可选) print(f"{Fore.BLUE}🔧 运行模式选择:{Style.RESET_ALL}") print(f"{Fore.YELLOW}1. 本地测试模式") print(f"2. API服务器模式{Style.RESET_ALL}") choice = input("请选择运行模式 (1/2): ").strip() if choice == "1": # 本地测试 test_input = input("请输入海报需求 (留空使用默认): ").strip() if not test_input: test_input = "端午节安康," run_local_pipeline(test_input) else: # 启动API服务器 print(f"{Fore.GREEN}🚀 启动API服务器...{Style.RESET_ALL}") uvicorn.run(app, host="0.0.0.0", port=8000)