#!/usr/bin/env python import os import re import json import requests from collections.abc import Iterator from threading import Thread import tempfile import random from typing import Dict, List, Tuple, Optional import shutil import concurrent.futures import gradio as gr from loguru import logger import pandas as pd import PyPDF2 from PIL import Image from gradio_client import Client import time # python-pptx 라이브러리 확인 try: from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION PPTX_AVAILABLE = True except ImportError: PPTX_AVAILABLE = False logger.warning("python-pptx 라이브러리가 설치되지 않았습니다. pip install python-pptx") ############################################################################## # API Configuration ############################################################################## FRIENDLI_TOKEN = os.environ.get("FRIENDLI_TOKEN") if not FRIENDLI_TOKEN: raise ValueError("Please set FRIENDLI_TOKEN environment variable") FRIENDLI_MODEL_ID = "dep89a2fld32mcm" FRIENDLI_API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" # SERPHouse API key SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "") if not SERPHOUSE_API_KEY: logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.") ############################################################################## # AI Image Generation API Configuration - 3D Style ############################################################################## AI_IMAGE_API_URL = "http://211.233.58.201:7971/" AI_IMAGE_ENABLED = False ai_image_client = None def initialize_ai_image_api(): """AI 이미지 생성 API 초기화 (3D 스타일)""" global AI_IMAGE_ENABLED, ai_image_client try: logger.info("Connecting to AI image generation API (3D style)...") ai_image_client = Client(AI_IMAGE_API_URL) AI_IMAGE_ENABLED = True logger.info("AI image generation API (3D style) connected successfully") return True except Exception as e: logger.error(f"Failed to connect to AI image API: {e}") AI_IMAGE_ENABLED = False return False ############################################################################## # AI Image Generation API Configuration - FLUX API ############################################################################## FLUX_API_URL = "http://211.233.58.201:7896" FLUX_API_ENABLED = False flux_api_client = None def initialize_flux_api(): """FLUX API 초기화""" global FLUX_API_ENABLED, flux_api_client try: logger.info("Connecting to FLUX API...") flux_api_client = Client(FLUX_API_URL) FLUX_API_ENABLED = True logger.info("FLUX API connected successfully") return True except Exception as e: logger.error(f"Failed to connect to FLUX API: {e}") FLUX_API_ENABLED = False return False ############################################################################## # Diagram Generation API Configuration ############################################################################## DIAGRAM_API_URL = "http://211.233.58.201:7860" # ChartGPT API URL DIAGRAM_API_ENABLED = False diagram_api_client = None def initialize_diagram_api(): """다이어그램 생성 API 초기화""" global DIAGRAM_API_ENABLED, diagram_api_client try: logger.info("Connecting to Diagram Generation API...") diagram_api_client = Client(DIAGRAM_API_URL) DIAGRAM_API_ENABLED = True logger.info("Diagram API connected successfully") return True except Exception as e: logger.error(f"Failed to connect to Diagram API: {e}") DIAGRAM_API_ENABLED = False return False ############################################################################## # Design Themes and Color Schemes ############################################################################## DESIGN_THEMES = { "professional": { "name": "프로페셔널", "colors": { "primary": RGBColor(46, 134, 171), # #2E86AB "secondary": RGBColor(162, 59, 114), # #A23B72 "accent": RGBColor(241, 143, 1), # #F18F01 "background": RGBColor(250, 250, 250), # #FAFAFA - Lighter background "text": RGBColor(44, 44, 44), # #2C2C2C - Darker text for better contrast }, "fonts": { "title": "Arial", "subtitle": "Arial", "body": "Calibri" } }, "modern": { "name": "모던", "colors": { "primary": RGBColor(114, 9, 183), # #7209B7 "secondary": RGBColor(247, 37, 133), # #F72585 "accent": RGBColor(76, 201, 240), # #4CC9F0 "background": RGBColor(252, 252, 252), # #FCFCFC - Very light background "text": RGBColor(40, 40, 40), # #282828 - Dark text }, "fonts": { "title": "Arial", "subtitle": "Arial", "body": "Helvetica" } }, "nature": { "name": "자연", "colors": { "primary": RGBColor(45, 106, 79), # #2D6A4F "secondary": RGBColor(82, 183, 136), # #52B788 "accent": RGBColor(181, 233, 185), # #B5E9B9 - Softer accent "background": RGBColor(248, 252, 248), # #F8FCF8 - Light green tint "text": RGBColor(27, 38, 44), # #1B262C }, "fonts": { "title": "Georgia", "subtitle": "Verdana", "body": "Calibri" } }, "creative": { "name": "크리에이티브", "colors": { "primary": RGBColor(255, 0, 110), # #FF006E "secondary": RGBColor(251, 86, 7), # #FB5607 "accent": RGBColor(255, 190, 11), # #FFBE0B "background": RGBColor(255, 248, 240), # #FFF8F0 - Light warm background "text": RGBColor(33, 33, 33), # #212121 - Dark text on light bg }, "fonts": { "title": "Impact", "subtitle": "Arial", "body": "Segoe UI" } }, "minimal": { "name": "미니멀", "colors": { "primary": RGBColor(55, 55, 55), # #373737 - Softer than pure black "secondary": RGBColor(120, 120, 120), # #787878 "accent": RGBColor(0, 122, 255), # #007AFF - Blue accent "background": RGBColor(252, 252, 252), # #FCFCFC "text": RGBColor(33, 33, 33), # #212121 }, "fonts": { "title": "Helvetica", "subtitle": "Helvetica", "body": "Arial" } } } ############################################################################## # Slide Layout Types ############################################################################## SLIDE_LAYOUTS = { "title": 0, # 제목 슬라이드 "title_content": 1, # 제목과 내용 "section_header": 2, # 섹션 헤더 "two_content": 3, # 2단 레이아웃 "comparison": 4, # 비교 레이아웃 "title_only": 5, # 제목만 "blank": 6 # 빈 슬라이드 } ############################################################################## # Emoji Bullet Points Mapping ############################################################################## def has_emoji(text: str) -> bool: """Check if text already contains emoji""" # Check for common emoji unicode ranges for char in text[:3]: # Check first 3 characters code = ord(char) # Common emoji ranges if (0x1F300 <= code <= 0x1F9FF) or \ (0x2600 <= code <= 0x26FF) or \ (0x2700 <= code <= 0x27BF) or \ (0x1F000 <= code <= 0x1F02F) or \ (0x1F0A0 <= code <= 0x1F0FF) or \ (0x1F100 <= code <= 0x1F1FF): return True return False def get_emoji_for_content(text: str) -> str: """Get relevant emoji based on content""" text_lower = text.lower() # Technology if any(word in text_lower for word in ['ai', '인공지능', 'ml', '머신러닝', '딥러닝', 'deep learning']): return '🤖' elif any(word in text_lower for word in ['데이터', 'data', '분석', 'analysis', '통계']): return '📊' elif any(word in text_lower for word in ['코드', 'code', '프로그래밍', 'programming', '개발']): return '💻' elif any(word in text_lower for word in ['클라우드', 'cloud', '서버', 'server']): return '☁️' elif any(word in text_lower for word in ['보안', 'security', '안전', 'safety']): return '🔒' elif any(word in text_lower for word in ['네트워크', 'network', '연결', 'connection', '인터넷']): return '🌐' elif any(word in text_lower for word in ['모바일', 'mobile', '스마트폰', 'smartphone', '앱']): return '📱' # Business elif any(word in text_lower for word in ['성장', 'growth', '증가', 'increase', '상승']): return '📈' elif any(word in text_lower for word in ['목표', 'goal', 'target', '타겟', '목적']): return '🎯' elif any(word in text_lower for word in ['돈', 'money', '비용', 'cost', '예산', 'budget', '수익']): return '💰' elif any(word in text_lower for word in ['팀', 'team', '협업', 'collaboration', '협력']): return '👥' elif any(word in text_lower for word in ['시간', 'time', '일정', 'schedule', '기한']): return '⏰' elif any(word in text_lower for word in ['아이디어', 'idea', '창의', 'creative', '혁신']): return '💡' elif any(word in text_lower for word in ['전략', 'strategy', '계획', 'plan']): return '📋' elif any(word in text_lower for word in ['성공', 'success', '달성', 'achieve']): return '🏆' # Education elif any(word in text_lower for word in ['학습', 'learning', '교육', 'education', '공부']): return '📚' elif any(word in text_lower for word in ['연구', 'research', '조사', 'study', '실험']): return '🔬' elif any(word in text_lower for word in ['문서', 'document', '보고서', 'report']): return '📄' elif any(word in text_lower for word in ['정보', 'information', '지식', 'knowledge']): return '📖' # Communication elif any(word in text_lower for word in ['소통', 'communication', '대화', 'conversation']): return '💬' elif any(word in text_lower for word in ['이메일', 'email', '메일', 'mail']): return '📧' elif any(word in text_lower for word in ['전화', 'phone', 'call', '통화']): return '📞' elif any(word in text_lower for word in ['회의', 'meeting', '미팅', '컨퍼런스']): return '👔' # Nature/Environment elif any(word in text_lower for word in ['환경', 'environment', '자연', 'nature']): return '🌱' elif any(word in text_lower for word in ['지속가능', 'sustainable', '친환경', 'eco']): return '♻️' elif any(word in text_lower for word in ['에너지', 'energy', '전력', 'power']): return '⚡' elif any(word in text_lower for word in ['지구', 'earth', '세계', 'world']): return '🌍' # Process/Steps elif any(word in text_lower for word in ['프로세스', 'process', '절차', 'procedure', '단계']): return '🔄' elif any(word in text_lower for word in ['체크', 'check', '확인', 'verify', '검증']): return '✅' elif any(word in text_lower for word in ['주의', 'warning', '경고', 'caution']): return '⚠️' elif any(word in text_lower for word in ['중요', 'important', '핵심', 'key', '필수']): return '⭐' elif any(word in text_lower for word in ['질문', 'question', '문의', 'ask']): return '❓' elif any(word in text_lower for word in ['해결', 'solution', '답', 'answer']): return '💯' # Actions elif any(word in text_lower for word in ['시작', 'start', '출발', 'begin']): return '🚀' elif any(word in text_lower for word in ['완료', 'complete', '종료', 'finish']): return '🏁' elif any(word in text_lower for word in ['개선', 'improve', '향상', 'enhance']): return '🔧' elif any(word in text_lower for word in ['변화', 'change', '변경', 'modify']): return '🔄' # Industries elif any(word in text_lower for word in ['의료', 'medical', '병원', 'hospital', '건강']): return '🏥' elif any(word in text_lower for word in ['금융', 'finance', '은행', 'bank']): return '🏦' elif any(word in text_lower for word in ['제조', 'manufacturing', '공장', 'factory']): return '🏭' elif any(word in text_lower for word in ['농업', 'agriculture', '농장', 'farm']): return '🌾' # Emotion/Status elif any(word in text_lower for word in ['행복', 'happy', '기쁨', 'joy']): return '😊' elif any(word in text_lower for word in ['위험', 'danger', 'risk', '리스크']): return '⚡' elif any(word in text_lower for word in ['새로운', 'new', '신규', 'novel']): return '✨' # Numbers elif text_lower.startswith(('첫째', 'first', '1.', '첫번째', '첫 번째')): return '1️⃣' elif text_lower.startswith(('둘째', 'second', '2.', '두번째', '두 번째')): return '2️⃣' elif text_lower.startswith(('셋째', 'third', '3.', '세번째', '세 번째')): return '3️⃣' elif text_lower.startswith(('넷째', 'fourth', '4.', '네번째', '네 번째')): return '4️⃣' elif text_lower.startswith(('다섯째', 'fifth', '5.', '다섯번째', '다섯 번째')): return '5️⃣' # Default else: return '▶️' ############################################################################## # Diagram Type Detection ############################################################################## def detect_diagram_type(title: str, content: str) -> Optional[str]: """슬라이드 내용을 분석하여 적절한 다이어그램 타입 결정""" combined_text = f"{title} {content}".lower() # Process Flow keywords if any(word in combined_text for word in ['프로세스', 'process', '절차', 'procedure', '단계', 'step', 'flow', '흐름', '워크플로우', 'workflow']): return "Process Flow" # WBS keywords elif any(word in combined_text for word in ['wbs', '작업분해', '프로젝트', 'project', '업무분해', 'breakdown', '구조도']): return "WBS Diagram" # Concept Map keywords elif any(word in combined_text for word in ['개념', 'concept', '관계', 'relationship', '연관', 'connection', '마인드맵', 'mindmap']): return "Concept Map" # Radial Diagram keywords elif any(word in combined_text for word in ['중심', 'central', '방사형', 'radial', '핵심', 'core', '주요', 'main']): return "Radial Diagram" # Synoptic Chart keywords elif any(word in combined_text for word in ['개요', 'overview', '전체', 'overall', '요약', 'summary', '시놉틱', 'synoptic']): return "Synoptic Chart" return None ############################################################################## # Generate Diagram JSON using LLM ############################################################################## def generate_diagram_json(title: str, content: str, diagram_type: str) -> Optional[str]: """LLM을 사용하여 다이어그램용 JSON 생성""" if not FRIENDLI_TOKEN: return None # 다이어그램 타입별 JSON 구조 가이드 json_guides = { "Concept Map": """Generate a JSON for a concept map with the EXACT following structure: { "central_node": "Main Topic", "nodes": [ { "id": "node1", "label": "First Concept", "relationship": "is part of", "subnodes": [ { "id": "node1_1", "label": "Sub Concept 1", "relationship": "includes", "subnodes": [] } ] } ] }""", "Process Flow": """Generate a JSON for a process flow diagram with the EXACT following structure: { "start_node": "Start Process", "nodes": [ {"id": "step1", "label": "First Step", "type": "process"}, {"id": "step2", "label": "Decision Point", "type": "decision"}, {"id": "end", "label": "End Process", "type": "end"} ], "connections": [ {"from": "start_node", "to": "step1", "label": "Begin"}, {"from": "step1", "to": "step2", "label": "Next"}, {"from": "step2", "to": "end", "label": "Complete"} ] }""", "WBS Diagram": """Generate a JSON for a WBS diagram with the EXACT following structure: { "project_title": "Project Name", "phases": [ { "id": "phase1", "label": "Phase 1", "tasks": [ { "id": "task1_1", "label": "Task 1.1", "subtasks": [] } ] } ] }""", "Radial Diagram": """Generate a JSON for a radial diagram with the EXACT following structure: { "central_node": "Central Concept", "nodes": [ { "id": "branch1", "label": "Branch 1", "relationship": "connected to", "subnodes": [] } ] }""", "Synoptic Chart": """Generate a JSON for a synoptic chart with the EXACT following structure: { "central_node": "Chart Title", "nodes": [ { "id": "phase1", "label": "Phase 1 Name", "relationship": "starts with", "subnodes": [] } ] }""" } system_prompt = f"""You are a helpful assistant that generates JSON structures for diagrams. {json_guides.get(diagram_type, '')} Important rules: 1. Generate ONLY valid JSON without any explanation or markdown formatting 2. The JSON must follow the EXACT structure shown above 3. Create content based on the provided title and content 4. Use the user's language (Korean or English) for the content values 5. Keep it simple with 3-5 main nodes/steps""" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Create a {diagram_type} JSON for:\nTitle: {title}\nContent: {content}"} ] headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": FRIENDLI_MODEL_ID, "messages": messages, "max_tokens": 1000, "temperature": 0.7, "stream": False } try: response = requests.post(FRIENDLI_API_URL, headers=headers, json=payload, timeout=30) if response.status_code == 200: response_data = response.json() if 'choices' in response_data and len(response_data['choices']) > 0: content = response_data['choices'][0]['message']['content'] # Extract JSON from response content = content.strip() if content.startswith("```json"): content = content[7:] if content.startswith("```"): content = content[3:] if content.endswith("```"): content = content[:-3] # Validate JSON json.loads(content) # This will raise exception if invalid return content except Exception as e: logger.error(f"Error generating diagram JSON: {e}") return None ############################################################################## # Generate Diagram using API ############################################################################## def generate_diagram_via_api(json_data: str, diagram_type: str) -> Optional[str]: """다이어그램 API를 통해 다이어그램 생성""" if not DIAGRAM_API_ENABLED or not diagram_api_client: return None try: # API 호출 result = diagram_api_client.predict( prompt_input=f"Generate {diagram_type}", # 프롬프트 diagram_type_select=diagram_type, # 다이어그램 타입 design_type_select="None", # 디자인 타입은 None output_format_radio="png", # PNG 형식 use_search_checkbox=False, # 검색 사용 안함 api_name="/generate_with_llm" ) # 결과에서 이미지 경로 추출 if isinstance(result, tuple) and len(result) > 0: image_path = result[0] if image_path and os.path.exists(image_path): # 임시 파일로 복사 with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: shutil.copy2(image_path, tmp.name) return tmp.name return None except Exception as e: logger.error(f"Failed to generate diagram via API: {e}") return None ############################################################################## # FLUX Image Generation Functions ############################################################################## def generate_flux_prompt(title: str, content: str) -> str: """슬라이드 내용을 기반으로 FLUX 이미지 프롬프트 생성""" # LLM을 사용하여 프롬프트 생성 system_prompt = """You are an expert at creating visual prompts for AI image generation. Create a concise, visually descriptive prompt in English based on the slide content. The prompt should describe a professional, modern illustration that represents the key concepts. Keep it under 100 words and focus on visual elements, style, and mood. Output ONLY the prompt without any explanation.""" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Create an image prompt for:\nTitle: {title}\nContent: {content[:500]}"} ] headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": FRIENDLI_MODEL_ID, "messages": messages, "max_tokens": 200, "temperature": 0.8, "stream": False } try: response = requests.post(FRIENDLI_API_URL, headers=headers, json=payload, timeout=30) if response.status_code == 200: response_data = response.json() if 'choices' in response_data and len(response_data['choices']) > 0: prompt = response_data['choices'][0]['message']['content'].strip() return f"Professional business presentation slide illustration: {prompt}, modern clean style, corporate colors, white background" except Exception as e: logger.error(f"Error generating FLUX prompt: {e}") # Fallback prompt return f"Professional business presentation illustration about {title}, modern minimalist style, clean design, corporate colors" def generate_flux_image_via_api(prompt: str) -> Optional[str]: """FLUX API를 통해 이미지 생성""" if not FLUX_API_ENABLED or not flux_api_client: return None try: logger.info(f"Generating FLUX image with prompt: {prompt[:100]}...") result = flux_api_client.predict( prompt=prompt, width=768, height=768, guidance=3.5, inference_steps=8, seed=random.randint(1, 1000000), do_img2img=False, init_image=None, image2image_strength=0.8, resize_img=True, api_name="/generate_image" ) if isinstance(result, tuple) and len(result) > 0: image_path = result[0] if image_path and os.path.exists(image_path): # PNG로 변환 with Image.open(image_path) as img: png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") img.save(png_tmp.name, format="PNG") logger.info(f"FLUX image generated and saved to {png_tmp.name}") return png_tmp.name return None except Exception as e: logger.error(f"Failed to generate FLUX image: {e}") return None ############################################################################## # Icon and Shape Mappings ############################################################################## SHAPE_ICONS = { "목표": MSO_SHAPE.STAR_5_POINT, "프로세스": MSO_SHAPE.BLOCK_ARC, "성장": MSO_SHAPE.UP_ARROW, "아이디어": MSO_SHAPE.LIGHTNING_BOLT, "체크": MSO_SHAPE.RECTANGLE, "주의": MSO_SHAPE.DIAMOND, "질문": MSO_SHAPE.OVAL, "분석": MSO_SHAPE.PENTAGON, "시간": MSO_SHAPE.DONUT, "팀": MSO_SHAPE.HEXAGON, } ############################################################################## # File Processing Constants ############################################################################## MAX_FILE_SIZE = 30 * 1024 * 1024 # 30MB MAX_CONTENT_CHARS = 6000 ############################################################################## # Improved Keyword Extraction ############################################################################## def extract_keywords(text: str, top_k: int = 5) -> str: """ Extract keywords: supports English and Korean """ stop_words = {'은', '는', '이', '가', '을', '를', '의', '에', '에서', 'the', 'is', 'at', 'on', 'in', 'a', 'an', 'and', 'or', 'but'} text = re.sub(r"[^a-zA-Z0-9가-힣\s]", "", text) tokens = text.split() key_tokens = [ token for token in tokens if token.lower() not in stop_words and len(token) > 1 ][:top_k] return " ".join(key_tokens) ############################################################################## # File Size Validation ############################################################################## def validate_file_size(file_path: str) -> bool: """Check if file size is within limits""" try: file_size = os.path.getsize(file_path) return file_size <= MAX_FILE_SIZE except: return False ############################################################################## # Web Search Function ############################################################################## def do_web_search(query: str, use_korean: bool = False) -> str: """ Search web and return top 20 organic results """ if not SERPHOUSE_API_KEY: return "Web search unavailable. API key not configured." try: url = "https://api.serphouse.com/serp/live" params = { "q": query, "domain": "google.com", "serp_type": "web", "device": "desktop", "lang": "ko" if use_korean else "en", "num": "20" } headers = { "Authorization": f"Bearer {SERPHOUSE_API_KEY}" } logger.info(f"Calling SerpHouse API... Query: {query}") response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() data = response.json() # Parse results results = data.get("results", {}) organic = None if isinstance(results, dict) and "organic" in results: organic = results["organic"] elif isinstance(results, dict) and "results" in results: if isinstance(results["results"], dict) and "organic" in results["results"]: organic = results["results"]["organic"] elif "organic" in data: organic = data["organic"] if not organic: return "No search results found or unexpected API response structure." max_results = min(20, len(organic)) limited_organic = organic[:max_results] summary_lines = [] for idx, item in enumerate(limited_organic, start=1): title = item.get("title", "No title") link = item.get("link", "#") snippet = item.get("snippet", "No description") displayed_link = item.get("displayed_link", link) summary_lines.append( f"### Result {idx}: {title}\n\n" f"{snippet}\n\n" f"**Source**: [{displayed_link}]({link})\n\n" f"---\n" ) instructions = """ # Web Search Results Below are the search results. Use this information when answering questions: 1. Reference the title, content, and source links 2. Explicitly cite sources in your answer (e.g., "According to source X...") 3. Include actual source links in your response 4. Synthesize information from multiple sources """ search_results = instructions + "\n".join(summary_lines) return search_results except requests.exceptions.Timeout: logger.error("Web search timeout") return "Web search timed out. Please try again." except requests.exceptions.RequestException as e: logger.error(f"Web search network error: {e}") return "Network error during web search." except Exception as e: logger.error(f"Web search failed: {e}") return f"Web search failed: {str(e)}" ############################################################################## # File Analysis Functions ############################################################################## def analyze_csv_file(path: str) -> str: """Analyze CSV file with size validation and encoding handling""" if not validate_file_size(path): return f"⚠️ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit." try: encodings = ['utf-8', 'cp949', 'euc-kr', 'latin-1'] df = None for encoding in encodings: try: df = pd.read_csv(path, encoding=encoding, nrows=50) break except UnicodeDecodeError: continue if df is None: return f"Failed to read CSV: Unsupported encoding" total_rows = len(pd.read_csv(path, encoding=encoding, usecols=[0])) if df.shape[1] > 10: df = df.iloc[:, :10] summary = f"**Data size**: {total_rows} rows x {df.shape[1]} columns\n" summary += f"**Showing**: Top {min(50, total_rows)} rows\n" summary += f"**Columns**: {', '.join(df.columns)}\n\n" # Extract data for charts chart_data = { "columns": list(df.columns), "sample_data": df.head(10).to_dict('records') } df_str = df.to_string() if len(df_str) > MAX_CONTENT_CHARS: df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..." return f"**[CSV File: {os.path.basename(path)}]**\n\n{summary}{df_str}\n\nCHART_DATA:{json.dumps(chart_data)}" except Exception as e: logger.error(f"CSV read error: {e}") return f"Failed to read CSV file ({os.path.basename(path)}): {str(e)}" def analyze_txt_file(path: str) -> str: """Analyze text file with automatic encoding detection""" if not validate_file_size(path): return f"⚠️ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit." encodings = ['utf-8', 'cp949', 'euc-kr', 'latin-1', 'utf-16'] for encoding in encodings: try: with open(path, "r", encoding=encoding) as f: text = f.read() file_size = os.path.getsize(path) size_info = f"**File size**: {file_size/1024:.1f}KB\n\n" if len(text) > MAX_CONTENT_CHARS: text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..." return f"**[TXT File: {os.path.basename(path)}]**\n\n{size_info}{text}" except UnicodeDecodeError: continue return f"Failed to read text file ({os.path.basename(path)}): Unsupported encoding" def pdf_to_markdown(pdf_path: str) -> str: """Convert PDF to markdown with improved error handling""" if not validate_file_size(pdf_path): return f"⚠️ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit." text_chunks = [] try: with open(pdf_path, "rb") as f: reader = PyPDF2.PdfReader(f) total_pages = len(reader.pages) max_pages = min(5, total_pages) text_chunks.append(f"**Total pages**: {total_pages}") text_chunks.append(f"**Showing**: First {max_pages} pages\n") for page_num in range(max_pages): try: page = reader.pages[page_num] page_text = page.extract_text() or "" page_text = page_text.strip() if page_text: if len(page_text) > MAX_CONTENT_CHARS // max_pages: page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)" text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n") except Exception as e: text_chunks.append(f"## Page {page_num+1}\n\nFailed to read page: {str(e)}\n") if total_pages > max_pages: text_chunks.append(f"\n...({max_pages}/{total_pages} pages shown)...") except Exception as e: logger.error(f"PDF read error: {e}") return f"Failed to read PDF file ({os.path.basename(pdf_path)}): {str(e)}" full_text = "\n".join(text_chunks) if len(full_text) > MAX_CONTENT_CHARS: full_text = full_text[:MAX_CONTENT_CHARS] + "\n...(truncated)..." return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}" ############################################################################## # AI Image Generation Functions using Multiple APIs ############################################################################## def generate_diverse_prompt(title: str, content: str, slide_index: int) -> Tuple[str, str]: """슬라이드별로 다양한 프롬프트 생성 - 3D와 포토리얼리스틱 버전""" # 주요 키워드 추출 keywords = extract_keywords(f"{title} {content}", top_k=5).split() # 슬라이드 인덱스에 따라 다양한 스타일 적용 styles_3d = [ "isometric 3D illustration", "low poly 3D art", "3D cartoon style", "3D glass morphism", "3D neon glow effect", "3D paper cut art", "3D clay render", "3D geometric abstract" ] styles_photo = [ "professional photography", "cinematic shot", "minimalist photography", "aerial view photograph", "macro photography", "dramatic lighting photo", "architectural photography", "lifestyle photography" ] # 내용 기반 시각 메타포 선택 visual_metaphors = [] content_lower = (title + " " + content).lower() if any(word in content_lower for word in ['성장', 'growth', '증가', 'increase']): visual_metaphors = ["ascending stairs", "growing tree", "rocket launch", "mountain peak", "rising graph"] elif any(word in content_lower for word in ['혁신', 'innovation', '창의', 'creative']): visual_metaphors = ["lightbulb moment", "puzzle pieces connecting", "spark of genius", "breaking boundaries", "colorful explosion"] elif any(word in content_lower for word in ['협업', 'collaboration', '팀', 'team']): visual_metaphors = ["hands joining together", "connected network", "team huddle", "bridge building", "interlocking gears"] elif any(word in content_lower for word in ['데이터', 'data', '분석', 'analysis']): visual_metaphors = ["data visualization", "digital dashboard", "flowing data streams", "analytical charts", "information network"] elif any(word in content_lower for word in ['미래', 'future', '전망', 'vision']): visual_metaphors = ["horizon view", "crystal ball", "futuristic cityscape", "pathway to tomorrow", "digital transformation"] elif any(word in content_lower for word in ['프로세스', 'process', '단계', 'step']): visual_metaphors = ["flowing river", "assembly line", "domino effect", "clockwork mechanism", "journey path"] elif any(word in content_lower for word in ['목표', 'goal', '성공', 'success']): visual_metaphors = ["target with arrow", "trophy on pedestal", "finish line", "mountain summit", "golden key"] else: visual_metaphors = ["abstract shapes", "dynamic composition", "symbolic representation", "conceptual art", "modern design"] # 스타일과 메타포 선택 style_3d = styles_3d[slide_index % len(styles_3d)] style_photo = styles_photo[slide_index % len(styles_photo)] metaphor = random.choice(visual_metaphors) # 색상 팔레트 다양화 color_palettes = [ "vibrant blue and orange", "elegant purple and gold", "fresh green and white", "bold red and black", "soft pastel tones", "monochromatic blue", "warm sunset colors", "cool ocean palette" ] colors = color_palettes[slide_index % len(color_palettes)] # 3D 스타일 프롬프트 (한글) prompt_3d = f"wbgmsst, {style_3d}, {metaphor} representing {' '.join(keywords[:3])}, {colors}, professional presentation slide, high quality, white background" # 포토리얼리스틱 프롬프트 (영어) prompt_photo = f"{style_photo} of {metaphor} symbolizing {' '.join(keywords[:3])}, {colors} color scheme, professional business context, clean composition, high resolution" return prompt_3d, prompt_photo def generate_cover_image_prompts(topic: str, slides_data: list) -> Tuple[str, str]: """표지용 3D와 포토리얼리스틱 프롬프트 생성""" keywords = extract_keywords(topic, top_k=3).split() # 주제별 특화된 시각 요소 topic_lower = topic.lower() if any(word in topic_lower for word in ['기술', 'tech', 'ai', '인공지능']): visual_3d = "futuristic 3D holographic interface" visual_photo = "modern technology workspace with holographic displays" elif any(word in topic_lower for word in ['비즈니스', 'business', '경영']): visual_3d = "3D corporate building with glass architecture" visual_photo = "professional business meeting in modern office" elif any(word in topic_lower for word in ['교육', 'education', '학습']): visual_3d = "3D books transforming into knowledge symbols" visual_photo = "inspiring educational environment with digital elements" elif any(word in topic_lower for word in ['환경', 'environment', '자연']): visual_3d = "3D earth with renewable energy icons" visual_photo = "pristine nature landscape with sustainable elements" else: visual_3d = "abstract 3D geometric composition" visual_photo = "professional abstract photography" prompt_3d = f"wbgmsst, {visual_3d}, {' '.join(keywords)} theme, premium 3D render, elegant composition, gradient background" prompt_photo = f"{visual_photo} featuring {' '.join(keywords)}, cinematic lighting, professional presentation cover, high-end photography" return prompt_3d, prompt_photo def generate_conclusion_image_prompts(title: str, content: str) -> Tuple[str, str]: """결론 슬라이드용 특별한 프롬프트 생성""" keywords = extract_keywords(f"{title} {content}", top_k=4).split() # 결론 스타일 비주얼 prompt_3d = f"wbgmsst, 3D trophy or achievement symbol, {' '.join(keywords[:2])} success visualization, golden lighting, celebration mood, premium quality" prompt_photo = f"inspirational sunrise or horizon view symbolizing {' '.join(keywords[:2])}, bright future ahead, professional photography, uplifting atmosphere" return prompt_3d, prompt_photo def generate_ai_image_via_3d_api(prompt: str) -> Optional[str]: """3D 스타일 API를 통해 이미지 생성""" if not AI_IMAGE_ENABLED or not ai_image_client: return None try: logger.info(f"Generating 3D style image with prompt: {prompt[:100]}...") result = ai_image_client.predict( height=1024.0, width=1024.0, steps=8.0, scales=3.5, prompt=prompt, seed=float(random.randint(0, 1000000)), api_name="/process_and_save_image" ) # 결과 처리 image_path = None if isinstance(result, dict): image_path = result.get("path") elif isinstance(result, str): image_path = result if image_path and os.path.exists(image_path): # PNG로 변환 with Image.open(image_path) as img: png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") img.save(png_tmp.name, format="PNG") logger.info(f"3D image generated and saved to {png_tmp.name}") return png_tmp.name return None except Exception as e: logger.error(f"Failed to generate 3D image: {e}") return None def generate_flux_image_via_api(prompt: str) -> Optional[str]: """FLUX API를 통해 포토리얼리스틱 이미지 생성""" if not FLUX_API_ENABLED or not flux_api_client: return None try: logger.info(f"Generating FLUX photorealistic image with prompt: {prompt[:100]}...") result = flux_api_client.predict( prompt=prompt, width=768, height=768, guidance=3.5, inference_steps=8, seed=random.randint(1, 1000000), do_img2img=False, init_image=None, image2image_strength=0.8, resize_img=True, api_name="/generate_image" ) if isinstance(result, tuple) and len(result) > 0: image_path = result[0] if image_path and os.path.exists(image_path): with Image.open(image_path) as img: png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") img.save(png_tmp.name, format="PNG") logger.info(f"FLUX image generated and saved to {png_tmp.name}") return png_tmp.name return None except Exception as e: logger.error(f"Failed to generate FLUX image: {e}") return None def generate_images_parallel(prompt_3d: str, prompt_photo: str) -> Tuple[Optional[str], Optional[str]]: """두 API를 병렬로 호출하여 이미지 생성""" import concurrent.futures with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: # 두 API를 동시에 호출 future_3d = executor.submit(generate_ai_image_via_3d_api, prompt_3d) future_photo = executor.submit(generate_flux_image_via_api, prompt_photo) # 결과 대기 image_3d = future_3d.result() image_photo = future_photo.result() return image_3d, image_photo ############################################################################## # PPT Generation Functions - FIXED VERSION ############################################################################## def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list: """Parse LLM response to extract slide content - FIXED VERSION""" slides = [] logger.info(f"Parsing LLM response, total length: {len(response)}") logger.debug(f"First 500 chars: {response[:500]}") # Try JSON parsing first try: json_match = re.search(r'\[[\s\S]*\]', response) if json_match: slides_data = json.loads(json_match.group()) return slides_data except: pass # 더 정확한 슬라이드 구분 패턴 # "슬라이드 1", "슬라이드 2" 또는 "Slide 1", "Slide 2" 형식을 찾음 slide_markers = [] # 슬라이드 마커의 위치를 먼저 찾음 for match in re.finditer(r'^(?:슬라이드|Slide)\s*(\d+)\s*$', response, re.MULTILINE): slide_markers.append({ 'index': int(match.group(1)), 'start': match.start(), 'end': match.end() }) logger.info(f"Found {len(slide_markers)} slide markers") # 슬라이드 마커가 없으면 다른 패턴 시도 if not slide_markers: # 숫자만으로 시작하는 패턴도 찾기 (예: "1.", "2." 등) for match in re.finditer(r'^(\d+)[.)]\s*$', response, re.MULTILINE): slide_markers.append({ 'index': int(match.group(1)), 'start': match.start(), 'end': match.end() }) # 각 슬라이드 마커 사이의 내용을 추출 for i, marker in enumerate(slide_markers): # 현재 슬라이드의 시작과 끝 위치 start = marker['end'] if i < len(slide_markers) - 1: end = slide_markers[i + 1]['start'] else: end = len(response) section = response[start:end].strip() if not section: continue logger.debug(f"Processing slide {marker['index']}: {section[:100]}...") slide = { 'title': '', 'content': '', 'notes': '', 'layout': 'title_content', 'chart_data': None } # 섹션 내에서 제목, 내용, 노트 추출 lines = section.split('\n') current_part = None title_found = False content_lines = [] notes_lines = [] for line in lines: line = line.strip() if not line: continue # 제목 섹션 감지 if (line.startswith('제목:') or line.startswith('Title:')) and not title_found: current_part = 'title' title_text = line.split(':', 1)[1].strip() if ':' in line else '' slide['title'] = title_text title_found = True # 내용 섹션 감지 elif line.startswith('내용:') or line.startswith('Content:'): current_part = 'content' content_text = line.split(':', 1)[1].strip() if ':' in line else '' if content_text: content_lines.append(content_text) # 노트 섹션 감지 elif line.startswith('노트:') or line.startswith('Notes:') or line.startswith('발표자 노트:'): current_part = 'notes' notes_text = line.split(':', 1)[1].strip() if ':' in line else '' if notes_text: notes_lines.append(notes_text) # 현재 섹션에 따라 내용 추가 else: if current_part == 'title' and not slide['title']: slide['title'] = line elif current_part == 'content': content_lines.append(line) elif current_part == 'notes': notes_lines.append(line) elif not title_found and not slide['title']: # 첫 번째 줄을 제목으로 slide['title'] = line title_found = True current_part = 'content' elif current_part is None and title_found: current_part = 'content' content_lines.append(line) # 슬라이드 데이터 설정 slide['content'] = '\n'.join(content_lines).strip() slide['notes'] = ' '.join(notes_lines).strip() # 내용이 있는 경우에만 추가 if slide['title'] or slide['content']: logger.info(f"Slide {len(slides)+1}: Title='{slide['title'][:30]}...', Content length={len(slide['content'])}") slides.append(slide) # 만약 위 방법으로 파싱이 안 되었다면, 더 유연한 방법 시도 if not slides or len(slides) < 3: logger.warning(f"Primary parsing resulted in only {len(slides)} slides, trying alternative method...") slides = [] # "제목:" 패턴으로 슬라이드 구분 시도 sections = re.split(r'\n(?=제목:|Title:)', response) for section in sections: if not section.strip(): continue slide = { 'title': '', 'content': '', 'notes': '', 'layout': 'title_content', 'chart_data': None } lines = section.strip().split('\n') current_part = None content_lines = [] notes_lines = [] for line in lines: line = line.strip() if not line: continue if line.startswith('제목:') or line.startswith('Title:'): slide['title'] = line.split(':', 1)[1].strip() if ':' in line else '' current_part = 'content' elif line.startswith('내용:') or line.startswith('Content:'): current_part = 'content' elif line.startswith('노트:') or line.startswith('Notes:'): current_part = 'notes' notes_text = line.split(':', 1)[1].strip() if ':' in line else '' if notes_text: notes_lines.append(notes_text) elif current_part == 'content': content_lines.append(line) elif current_part == 'notes': notes_lines.append(line) slide['content'] = '\n'.join(content_lines).strip() slide['notes'] = ' '.join(notes_lines).strip() # 슬라이드 번호 제거 slide['title'] = re.sub(r'^(슬라이드|Slide)\s*\d+\s*[::\-]?\s*', '', slide['title'], flags=re.IGNORECASE) if slide['title'] or slide['content']: slides.append(slide) logger.info(f"Total slides parsed: {len(slides)}") # 파싱 결과 검증 if len(slides) < 3: logger.error("Parsing resulted in too few slides. Raw response preview:") logger.error(response[:1000]) return slides def force_font_size(text_frame, font_size_pt: int, theme: Dict): """Force font size for all paragraphs and runs in a text frame""" if not text_frame: return try: # Ensure paragraphs exist if not hasattr(text_frame, 'paragraphs'): return for paragraph in text_frame.paragraphs: try: # Set paragraph level font if hasattr(paragraph, 'font'): paragraph.font.size = Pt(font_size_pt) paragraph.font.name = theme['fonts']['body'] paragraph.font.color.rgb = theme['colors']['text'] # Set run level font (most important for actual rendering) if hasattr(paragraph, 'runs'): for run in paragraph.runs: run.font.size = Pt(font_size_pt) run.font.name = theme['fonts']['body'] run.font.color.rgb = theme['colors']['text'] # If paragraph has no runs but has text, create a run if paragraph.text and (not hasattr(paragraph, 'runs') or len(paragraph.runs) == 0): # Force creation of runs by modifying text temp_text = paragraph.text paragraph.text = temp_text # This creates runs if hasattr(paragraph, 'runs'): for run in paragraph.runs: run.font.size = Pt(font_size_pt) run.font.name = theme['fonts']['body'] run.font.color.rgb = theme['colors']['text'] except Exception as e: logger.warning(f"Error setting font for paragraph: {e}") continue except Exception as e: logger.warning(f"Error in force_font_size: {e}") def apply_theme_to_slide(slide, theme: Dict, layout_type: str = 'title_content'): """Apply design theme to a slide with consistent styling""" # Add colored background shape for all slides bg_shape = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(5.625) ) bg_shape.fill.solid() # Use lighter background for content slides if layout_type in ['title_content', 'two_content', 'comparison']: # Light background with subtle gradient effect bg_shape.fill.fore_color.rgb = theme['colors']['background'] # Add accent strip at top accent_strip = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(0.5) ) accent_strip.fill.solid() accent_strip.fill.fore_color.rgb = theme['colors']['primary'] accent_strip.line.fill.background() # Add bottom accent bottom_strip = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, 0, Inches(5.125), Inches(10), Inches(0.5) ) bottom_strip.fill.solid() bottom_strip.fill.fore_color.rgb = theme['colors']['secondary'] bottom_strip.fill.transparency = 0.7 bottom_strip.line.fill.background() else: # Section headers get primary color background bg_shape.fill.fore_color.rgb = theme['colors']['primary'] bg_shape.line.fill.background() # Move background shapes to back slide.shapes._spTree.remove(bg_shape._element) slide.shapes._spTree.insert(2, bg_shape._element) # Apply title formatting if exists if slide.shapes.title: try: title = slide.shapes.title if title.text_frame and title.text_frame.paragraphs: for paragraph in title.text_frame.paragraphs: paragraph.font.name = theme['fonts']['title'] paragraph.font.bold = True # UPDATED: Increased font sizes for better readability if layout_type == 'section_header': paragraph.font.size = Pt(28) # Increased from 20 paragraph.font.color.rgb = RGBColor(255, 255, 255) paragraph.alignment = PP_ALIGN.CENTER else: paragraph.font.size = Pt(24) # Increased from 18 paragraph.font.color.rgb = theme['colors']['primary'] paragraph.alignment = PP_ALIGN.LEFT except Exception as e: logger.warning(f"Title formatting failed: {e}") # Apply content formatting with improved readability # NOTE: Do NOT add emojis here - they will be added in create_advanced_ppt_from_content for shape in slide.shapes: if shape.has_text_frame and shape != slide.shapes.title: try: text_frame = shape.text_frame # Set text frame margins for better spacing text_frame.margin_left = Inches(0.25) text_frame.margin_right = Inches(0.25) text_frame.margin_top = Inches(0.1) text_frame.margin_bottom = Inches(0.1) # Only apply font formatting, no content modification if text_frame.text.strip(): # Use force_font_size helper to ensure font is applied force_font_size(text_frame, 16, theme) # Increased from 12 for paragraph in text_frame.paragraphs: # Add line spacing for better readability paragraph.space_after = Pt(4) # Increased from 3 paragraph.line_spacing = 1.2 # Increased from 1.1 except Exception as e: logger.warning(f"Content formatting failed: {e}") def add_gradient_background(slide, color1: RGBColor, color2: RGBColor): """Add gradient-like background to slide using shapes""" # Note: python-pptx doesn't directly support gradients in backgrounds, # so we'll create a gradient effect using overlapping shapes left = top = 0 width = Inches(10) height = Inches(5.625) # Add base color rectangle shape1 = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, left, top, width, height ) shape1.fill.solid() shape1.fill.fore_color.rgb = color1 shape1.line.fill.background() # Add semi-transparent overlay for gradient effect shape2 = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, left, top, width, Inches(2.8) ) shape2.fill.solid() shape2.fill.fore_color.rgb = color2 shape2.fill.transparency = 0.5 shape2.line.fill.background() # Move shapes to back slide.shapes._spTree.remove(shape1._element) slide.shapes._spTree.remove(shape2._element) slide.shapes._spTree.insert(2, shape1._element) slide.shapes._spTree.insert(3, shape2._element) def add_decorative_shapes(slide, theme: Dict): """Add decorative shapes to enhance visual appeal""" try: # Add corner accent circle shape1 = slide.shapes.add_shape( MSO_SHAPE.OVAL, Inches(9.3), Inches(4.8), Inches(0.7), Inches(0.7) ) shape1.fill.solid() shape1.fill.fore_color.rgb = theme['colors']['accent'] shape1.fill.transparency = 0.3 shape1.line.fill.background() # Add smaller accent shape2 = slide.shapes.add_shape( MSO_SHAPE.OVAL, Inches(0.1), Inches(0.1), Inches(0.4), Inches(0.4) ) shape2.fill.solid() shape2.fill.fore_color.rgb = theme['colors']['secondary'] shape2.fill.transparency = 0.5 shape2.line.fill.background() except Exception as e: logger.warning(f"Failed to add decorative shapes: {e}") def create_chart_slide(slide, chart_data: Dict, theme: Dict): """Create a chart on the slide based on data""" try: # Add chart x, y, cx, cy = Inches(1), Inches(2), Inches(8), Inches(4.5) # Prepare chart data chart_data_obj = CategoryChartData() # Simple bar chart example if 'columns' in chart_data and 'sample_data' in chart_data: # Use first numeric column for chart numeric_cols = [] for col in chart_data['columns']: try: # Check if column has numeric data float(chart_data['sample_data'][0].get(col, 0)) numeric_cols.append(col) except: pass if numeric_cols: categories = [str(row.get(chart_data['columns'][0], '')) for row in chart_data['sample_data'][:5]] chart_data_obj.categories = categories for col in numeric_cols[:3]: # Max 3 series values = [float(row.get(col, 0)) for row in chart_data['sample_data'][:5]] chart_data_obj.add_series(col, values) chart = slide.shapes.add_chart( XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data_obj ).chart # Style the chart chart.has_legend = True chart.legend.position = XL_LEGEND_POSITION.BOTTOM except Exception as e: logger.warning(f"Chart creation failed: {e}") # If chart fails, add a text placeholder instead textbox = slide.shapes.add_textbox(x, y, cx, cy) text_frame = textbox.text_frame text_frame.text = "데이터 차트 (차트 생성 실패)" text_frame.paragraphs[0].font.size = Pt(16) # Increased font size text_frame.paragraphs[0].font.color.rgb = theme['colors']['secondary'] def create_advanced_ppt_from_content( slides_data: list, topic: str, theme_name: str, include_charts: bool = False, include_ai_image: bool = False, include_diagrams: bool = False, include_flux_images: bool = False ) -> str: """Create advanced PPT file with consistent visual design and AI-generated visuals""" if not PPTX_AVAILABLE: raise ImportError("python-pptx library is required") prs = Presentation() theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES['professional']) # Set slide size (16:9) prs.slide_width = Inches(10) prs.slide_height = Inches(5.625) # ───────────────────────────────────────────────────────── # 1) 제목 슬라이드(표지) 생성 - 수정된 레이아웃 # ───────────────────────────────────────────────────────── title_slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(title_slide_layout) # 배경 그라디언트 add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary']) # 제목과 부제목을 중앙 상단에 배치 title_shape = slide.shapes.title subtitle_shape = slide.placeholders[1] if len(slide.placeholders) > 1 else None if title_shape: title_shape.left = Inches(0.5) title_shape.width = prs.slide_width - Inches(1) title_shape.top = Inches(1.0) # 상단에 배치 title_shape.height = Inches(1.2) tf = title_shape.text_frame tf.clear() tf.text = topic p = tf.paragraphs[0] p.font.name = theme['fonts']['title'] p.font.size = Pt(36) p.font.bold = True p.font.color.rgb = RGBColor(255, 255, 255) p.alignment = PP_ALIGN.CENTER if subtitle_shape: subtitle_shape.left = Inches(0.5) subtitle_shape.width = prs.slide_width - Inches(1) subtitle_shape.top = Inches(2.2) # 제목 아래에 배치 subtitle_shape.height = Inches(0.9) tf2 = subtitle_shape.text_frame tf2.clear() tf2.text = f"자동 생성된 프레젠테이션 • 총 {len(slides_data)}장" p2 = tf2.paragraphs[0] p2.font.name = theme['fonts']['subtitle'] p2.font.size = Pt(20) p2.font.color.rgb = RGBColor(255, 255, 255) p2.alignment = PP_ALIGN.CENTER # AI 이미지를 우측 하단에 배치 (두 API 병렬 사용) if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED): logger.info("Generating AI cover images via parallel APIs...") # 두 가지 프롬프트 생성 prompt_3d, prompt_photo = generate_cover_image_prompts(topic, slides_data) # 병렬로 이미지 생성 image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo) # 성공한 이미지 중 하나 선택 (3D 우선) ai_image_path = image_3d or image_photo if ai_image_path and os.path.exists(ai_image_path): try: img = Image.open(ai_image_path) img_width, img_height = img.size # 이미지를 우측 하단에 배치 max_width = Inches(3.5) max_height = Inches(2.5) ratio = img_height / img_width img_w = max_width img_h = max_width * ratio if img_h > max_height: img_h = max_height img_w = max_height / ratio # 우측 하단 배치 left = prs.slide_width - img_w - Inches(0.5) top = prs.slide_height - img_h - Inches(0.8) pic = slide.shapes.add_picture(ai_image_path, left, top, width=img_w, height=img_h) pic.shadow.inherit = False pic.shadow.visible = True pic.shadow.blur_radius = Pt(15) pic.shadow.distance = Pt(8) pic.shadow.angle = 45 # 이미지 위에 작은 캡션 추가 caption_box = slide.shapes.add_textbox( left, top - Inches(0.3), img_w, Inches(0.3) ) caption_tf = caption_box.text_frame caption_tf.text = "AI Generated" caption_p = caption_tf.paragraphs[0] caption_p.font.size = Pt(10) caption_p.font.color.rgb = RGBColor(255, 255, 255) caption_p.alignment = PP_ALIGN.CENTER # 임시 파일 정리 for temp_path in [image_3d, image_photo]: if temp_path and os.path.exists(temp_path): try: os.unlink(temp_path) except: pass except Exception as e: logger.error(f"Failed to add cover image: {e}") # 장식 요소 add_decorative_shapes(slide, theme) # Add content slides with consistent design for i, slide_data in enumerate(slides_data): layout_type = slide_data.get('layout', 'title_content') # Log slide creation logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}") logger.debug(f"Content length: {len(slide_data.get('content', ''))}") # Choose appropriate layout if layout_type == 'section_header' and len(prs.slide_layouts) > 2: slide_layout = prs.slide_layouts[2] elif layout_type == 'two_content' and len(prs.slide_layouts) > 3: slide_layout = prs.slide_layouts[3] elif layout_type == 'comparison' and len(prs.slide_layouts) > 4: slide_layout = prs.slide_layouts[4] else: slide_layout = prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0] slide = prs.slides.add_slide(slide_layout) # Apply theme to EVERY slide for consistency apply_theme_to_slide(slide, theme, layout_type) # Set title if slide.shapes.title: slide.shapes.title.text = slide_data.get('title', '제목 없음') # IMMEDIATELY set title font size after setting text try: title_text_frame = slide.shapes.title.text_frame if title_text_frame and title_text_frame.paragraphs: for paragraph in title_text_frame.paragraphs: if layout_type == 'section_header': paragraph.font.size = Pt(28) # Increased from 20 paragraph.font.color.rgb = RGBColor(255, 255, 255) paragraph.alignment = PP_ALIGN.CENTER else: paragraph.font.size = Pt(24) # Increased from 18 paragraph.font.color.rgb = theme['colors']['primary'] paragraph.font.bold = True paragraph.font.name = theme['fonts']['title'] except Exception as e: logger.warning(f"Title font sizing failed: {e}") # Detect if this slide should have a diagram or image slide_title = slide_data.get('title', '') slide_content = slide_data.get('content', '') # 결론/하이라이트 슬라이드 감지 is_conclusion_slide = any(word in slide_title.lower() for word in ['결론', 'conclusion', '요약', 'summary', '핵심', 'key', '마무리', 'closing', '정리', 'takeaway', '시사점', 'implication']) # 다이어그램 또는 이미지 생성 여부 결정 should_add_visual = False visual_type = None # 결론 슬라이드는 항상 이미지 추가 if is_conclusion_slide and include_flux_images: should_add_visual = True visual_type = ('conclusion_images', None) elif include_diagrams: diagram_type = detect_diagram_type(slide_title, slide_content) if diagram_type: should_add_visual = True visual_type = ('diagram', diagram_type) elif not should_add_visual and include_flux_images and i % 2 == 0: # 매 2번째 슬라이드에 이미지 should_add_visual = True visual_type = ('diverse_images', None) # 시각적 요소가 있는 경우 좌-우 레이아웃 적용 if should_add_visual and layout_type not in ['section_header']: # 좌측에 텍스트 배치 left_box = slide.shapes.add_textbox( Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5) ) left_tf = left_box.text_frame left_tf.clear() left_tf.text = slide_content left_tf.word_wrap = True force_font_size(left_tf, 14, theme) # Apply emoji bullets for paragraph in left_tf.paragraphs: text = paragraph.text.strip() if text and text.startswith(('-', '•', '●')) and not has_emoji(text): clean_text = text.lstrip('-•● ') emoji = get_emoji_for_content(clean_text) paragraph.text = f"{emoji} {clean_text}" force_font_size(left_tf, 14, theme) # 우측에 시각적 요소 추가 visual_added = False if visual_type[0] == 'diagram': # 다이어그램 생성 logger.info(f"Generating {visual_type[1]} for slide {i+1}") diagram_json = generate_diagram_json(slide_title, slide_content, visual_type[1]) if diagram_json: diagram_path = generate_diagram_via_api(diagram_json, visual_type[1]) if diagram_path and os.path.exists(diagram_path): try: # 다이어그램 이미지 추가 pic = slide.shapes.add_picture( diagram_path, Inches(5.2), Inches(1.5), width=Inches(4.3), height=Inches(3.0) ) visual_added = True # 임시 파일 삭제 os.unlink(diagram_path) except Exception as e: logger.error(f"Failed to add diagram: {e}") elif visual_type[0] == 'conclusion_images': # 결론 슬라이드용 이미지 생성 (두 API 병렬) logger.info(f"Generating conclusion images for slide {i+1}") prompt_3d, prompt_photo = generate_conclusion_image_prompts(slide_title, slide_content) image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo) # 성공한 이미지 중 하나 선택 selected_image = image_photo or image_3d # 결론은 포토리얼리스틱 우선 if selected_image and os.path.exists(selected_image): try: pic = slide.shapes.add_picture( selected_image, Inches(5.2), Inches(1.5), width=Inches(4.3), height=Inches(3.0) ) visual_added = True # 이미지 캡션 추가 caption_box = slide.shapes.add_textbox( Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3) ) caption_tf = caption_box.text_frame caption_tf.text = "Key Takeaway Visualization" caption_p = caption_tf.paragraphs[0] caption_p.font.size = Pt(10) caption_p.font.color.rgb = theme['colors']['secondary'] caption_p.alignment = PP_ALIGN.CENTER # 임시 파일 정리 for temp_path in [image_3d, image_photo]: if temp_path and os.path.exists(temp_path): try: os.unlink(temp_path) except: pass except Exception as e: logger.error(f"Failed to add conclusion image: {e}") elif visual_type[0] == 'diverse_images': # 다양한 슬라이드 이미지 생성 (두 API 병렬) logger.info(f"Generating diverse images for slide {i+1}") prompt_3d, prompt_photo = generate_diverse_prompt(slide_title, slide_content, i) image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo) # 슬라이드 인덱스에 따라 번갈아 선택 selected_image = image_3d if i % 2 == 0 else image_photo if not selected_image: # 실패시 다른 것 선택 selected_image = image_photo if i % 2 == 0 else image_3d if selected_image and os.path.exists(selected_image): try: pic = slide.shapes.add_picture( selected_image, Inches(5.2), Inches(1.5), width=Inches(4.3), height=Inches(3.0) ) visual_added = True # 임시 파일 정리 for temp_path in [image_3d, image_photo]: if temp_path and os.path.exists(temp_path): try: os.unlink(temp_path) except: pass except Exception as e: logger.error(f"Failed to add slide image: {e}") # 시각적 요소가 추가되지 않은 경우 플레이스홀더 추가 if not visual_added: placeholder_box = slide.shapes.add_textbox( Inches(5.2), Inches(2.5), Inches(4.3), Inches(1.0) ) placeholder_tf = placeholder_box.text_frame placeholder_tf.text = f"{visual_type[1] if visual_type[0] == 'diagram' else 'Visual'} Placeholder" placeholder_tf.paragraphs[0].font.size = Pt(14) placeholder_tf.paragraphs[0].font.color.rgb = theme['colors']['secondary'] placeholder_tf.paragraphs[0].alignment = PP_ALIGN.CENTER else: # 기본 레이아웃 (시각적 요소 없음) if layout_type == 'section_header': # Section header content handling content = slide_data.get('content', '') if content: logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...") textbox = slide.shapes.add_textbox( Inches(1), Inches(3.5), Inches(8), Inches(1.5) ) tf = textbox.text_frame tf.clear() tf.text = content tf.word_wrap = True for paragraph in tf.paragraphs: paragraph.font.name = theme['fonts']['body'] paragraph.font.size = Pt(16) paragraph.font.color.rgb = RGBColor(255, 255, 255) paragraph.alignment = PP_ALIGN.CENTER # Add decorative line line = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4) ) line.fill.solid() line.fill.fore_color.rgb = RGBColor(255, 255, 255) line.line.fill.background() elif layout_type == 'two_content': content = slide_data.get('content', '') if content: logger.info(f"Creating two-column layout for slide {i+1}") content_lines = content.split('\n') mid_point = len(content_lines) // 2 # Left column left_box = slide.shapes.add_textbox( Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5) ) left_tf = left_box.text_frame left_tf.clear() left_content = '\n'.join(content_lines[:mid_point]) if left_content: left_tf.text = left_content left_tf.word_wrap = True force_font_size(left_tf, 14, theme) # Apply emoji bullets for paragraph in left_tf.paragraphs: text = paragraph.text.strip() if text and text.startswith(('-', '•', '●')) and not has_emoji(text): clean_text = text.lstrip('-•● ') emoji = get_emoji_for_content(clean_text) paragraph.text = f"{emoji} {clean_text}" force_font_size(left_tf, 14, theme) # Right column right_box = slide.shapes.add_textbox( Inches(5), Inches(1.5), Inches(4.5), Inches(3.5) ) right_tf = right_box.text_frame right_tf.clear() right_content = '\n'.join(content_lines[mid_point:]) if right_content: right_tf.text = right_content right_tf.word_wrap = True force_font_size(right_tf, 14, theme) # Apply emoji bullets for paragraph in right_tf.paragraphs: text = paragraph.text.strip() if text and text.startswith(('-', '•', '●')) and not has_emoji(text): clean_text = text.lstrip('-•● ') emoji = get_emoji_for_content(clean_text) paragraph.text = f"{emoji} {clean_text}" force_font_size(right_tf, 14, theme) else: # Regular content content = slide_data.get('content', '') logger.info(f"Slide {i+1} - Content to add: '{content[:100]}...' (length: {len(content)})") if include_charts and slide_data.get('chart_data'): create_chart_slide(slide, slide_data['chart_data'], theme) if content and content.strip(): textbox = slide.shapes.add_textbox( Inches(0.5), # left Inches(1.5), # top Inches(9), # width Inches(3.5) # height ) tf = textbox.text_frame tf.clear() tf.text = content.strip() tf.word_wrap = True tf.margin_left = Inches(0.1) tf.margin_right = Inches(0.1) tf.margin_top = Inches(0.05) tf.margin_bottom = Inches(0.05) force_font_size(tf, 16, theme) for p_idx, paragraph in enumerate(tf.paragraphs): if paragraph.text.strip(): text = paragraph.text.strip() if text.startswith(('-', '•', '●')) and not has_emoji(text): clean_text = text.lstrip('-•● ') emoji = get_emoji_for_content(clean_text) paragraph.text = f"{emoji} {clean_text}" if paragraph.runs: for run in paragraph.runs: run.font.size = Pt(16) run.font.name = theme['fonts']['body'] run.font.color.rgb = theme['colors']['text'] else: paragraph.font.size = Pt(16) paragraph.font.name = theme['fonts']['body'] paragraph.font.color.rgb = theme['colors']['text'] paragraph.space_before = Pt(6) paragraph.space_after = Pt(6) paragraph.line_spacing = 1.3 logger.info(f"Successfully added content to slide {i+1}") else: logger.warning(f"Slide {i+1} has no content or empty content") # Add slide notes if available if slide_data.get('notes'): try: notes_slide = slide.notes_slide notes_text_frame = notes_slide.notes_text_frame notes_text_frame.text = slide_data.get('notes', '') except Exception as e: logger.warning(f"Failed to add slide notes: {e}") # Add slide number with better design slide_number_bg = slide.shapes.add_shape( MSO_SHAPE.ROUNDED_RECTANGLE, Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5) ) slide_number_bg.fill.solid() slide_number_bg.fill.fore_color.rgb = theme['colors']['primary'] slide_number_bg.fill.transparency = 0.8 slide_number_bg.line.fill.background() slide_number_box = slide.shapes.add_textbox( Inches(8.3), Inches(5.05), Inches(1.5), Inches(0.4) ) slide_number_frame = slide_number_box.text_frame slide_number_frame.text = f"{i + 1} / {len(slides_data)}" slide_number_frame.paragraphs[0].font.size = Pt(10) # Increased from 8 slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) slide_number_frame.paragraphs[0].font.bold = False slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER # Add subtle design element on alternating slides if i % 2 == 0: accent_shape = slide.shapes.add_shape( MSO_SHAPE.OVAL, Inches(9.6), Inches(0.1), Inches(0.2), Inches(0.2) ) accent_shape.fill.solid() accent_shape.fill.fore_color.rgb = theme['colors']['accent'] accent_shape.line.fill.background() # Add thank you slide with consistent design thank_you_layout = prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[0] thank_you_slide = prs.slides.add_slide(thank_you_layout) # Apply gradient background add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary']) if thank_you_slide.shapes.title: thank_you_slide.shapes.title.text = "감사합니다" try: if thank_you_slide.shapes.title.text_frame and thank_you_slide.shapes.title.text_frame.paragraphs: thank_you_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36) # Increased from 28 thank_you_slide.shapes.title.text_frame.paragraphs[0].font.bold = True thank_you_slide.shapes.title.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) thank_you_slide.shapes.title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER except Exception as e: logger.warning(f"Thank you slide styling failed: {e}") # Add contact or additional info placeholder info_box = thank_you_slide.shapes.add_textbox( Inches(2), Inches(3.5), Inches(6), Inches(1) ) info_tf = info_box.text_frame info_tf.text = "AI로 생성된 프레젠테이션" info_tf.paragraphs[0].font.size = Pt(18) # Increased from 14 info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER # Save to temporary file with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp_file: prs.save(tmp_file.name) return tmp_file.name ############################################################################## # Streaming Response Handler for PPT Generation - IMPROVED VERSION ############################################################################## def generate_ppt_content(topic: str, num_slides: int, additional_context: str, use_korean: bool = False, layout_style: str = "consistent") -> Iterator[str]: """Generate PPT content using LLM with clearer format""" # Layout instructions based on style layout_instructions = "" if layout_style == "varied": layout_instructions = """ 슬라이드 레이아웃을 다양하게 구성해주세요: - 매 5번째 슬라이드는 '섹션 구분' 슬라이드로 만들어주세요 - 비교나 대조 내용이 있으면 '비교' 레이아웃을 사용하세요 - 내용이 많으면 2단 구성을 고려하세요 """ elif layout_style == "consistent": layout_instructions = """ 일관된 레이아웃을 유지해주세요: - 모든 슬라이드는 동일한 구조로 작성 - 제목과 글머리 기호 형식 통일 - 간결하고 명확한 구성 """ # 더 명확한 시스템 프롬프트 if use_korean: system_prompt = f"""당신은 전문적인 PPT 프레젠테이션 작성 전문가입니다. 주어진 주제에 대해 정확히 {num_slides}장의 슬라이드 내용을 작성해주세요. **반드시 아래 형식을 정확히 따라주세요:** 슬라이드 1 제목: [슬라이드 제목 - "슬라이드 1" 같은 번호는 포함하지 마세요] 내용: - 첫 번째 핵심 포인트 - 두 번째 핵심 포인트 - 세 번째 핵심 포인트 - 네 번째 핵심 포인트 - 다섯 번째 핵심 포인트 노트: [발표자가 이 슬라이드를 설명할 때 사용할 구어체 스크립트] 슬라이드 2 제목: [슬라이드 제목] 내용: - 첫 번째 핵심 포인트 - 두 번째 핵심 포인트 - 세 번째 핵심 포인트 - 네 번째 핵심 포인트 - 다섯 번째 핵심 포인트 노트: [발표 스크립트] (이런 식으로 슬라이드 {num_slides}까지 계속) {layout_instructions} **중요 지침:** 1. 각 슬라이드는 "슬라이드 숫자"로 시작 2. 제목: 뒤에 실제 제목 작성 (번호 제외) 3. 내용: 뒤에 정확히 5개의 글머리 기호 포인트 4. 노트: 뒤에 발표 스크립트 5. 각 섹션 사이에 빈 줄 없음 6. 총 {num_slides}장 작성 7. 각 포인트는 '-' 기호로 시작하세요 (이모지는 자동으로 추가됩니다) 8. 노트는 해당 슬라이드의 내용을 발표자가 청중에게 설명하는 구어체 대본으로 작성하세요""" else: system_prompt = f"""You are a professional PPT presentation expert. Create content for exactly {num_slides} slides on the given topic. **You MUST follow this exact format:** Slide 1 Title: [Slide title - do NOT include "Slide 1" in the title] Content: - First key point - Second key point - Third key point - Fourth key point - Fifth key point Notes: [Speaker script in conversational style for explaining this slide] Slide 2 Title: [Slide title] Content: - First key point - Second key point - Third key point - Fourth key point - Fifth key point Notes: [Speaker script] (Continue this way until Slide {num_slides}) **Important instructions:** 1. Each slide starts with "Slide number" 2. Title: followed by the actual title (no numbers) 3. Content: followed by exactly 5 bullet points 4. Notes: followed by speaker script 5. No empty lines between sections 6. Create exactly {num_slides} slides 7. Start each point with '-' (emojis will be added automatically) 8. Notes should be a speaker script explaining the slide content in conversational style""" # Add search results if web search is performed if additional_context: system_prompt += f"\n\n참고 정보:\n{additional_context}" # Prepare messages user_prompt = f"주제: {topic}\n\n위에서 설명한 형식에 맞춰 정확히 {num_slides}장의 PPT 슬라이드 내용을 작성해주세요. 각 슬라이드마다 5개의 핵심 포인트와 함께, 발표자가 청중에게 해당 내용을 설명하는 구어체 발표 대본을 노트로 작성해주세요." if not use_korean: user_prompt = f"Topic: {topic}\n\nPlease create exactly {num_slides} PPT slides following the format described above. Include exactly 5 key points per slide, and write speaker notes as a conversational script explaining the content to the audience." messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ] # Call LLM API headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": FRIENDLI_MODEL_ID, "messages": messages, "max_tokens": min(4000, num_slides * 300), # More tokens for 5 points + notes "top_p": 0.9, "temperature": 0.8, "stream": True, "stream_options": { "include_usage": True } } try: response = requests.post( FRIENDLI_API_URL, headers=headers, json=payload, stream=True, timeout=60 ) response.raise_for_status() full_response = "" for line in response.iter_lines(): if line: line_text = line.decode('utf-8') if line_text.startswith("data: "): data_str = line_text[6:] if data_str == "[DONE]": break try: data = json.loads(data_str) if "choices" in data and len(data["choices"]) > 0: delta = data["choices"][0].get("delta", {}) content = delta.get("content", "") if content: full_response += content yield full_response except json.JSONDecodeError: logger.warning(f"JSON parsing failed: {data_str}") continue except Exception as e: logger.error(f"LLM API error: {str(e)}") yield f"⚠️ Error generating content: {str(e)}" ############################################################################## # Main PPT Generation Function - IMPROVED VERSION with Enhanced Features ############################################################################## def generate_ppt( topic: str, num_slides: int = 10, use_web_search: bool = False, use_korean: bool = True, reference_files: list = None, design_theme: str = "professional", font_style: str = "modern", layout_style: str = "consistent", include_charts: bool = False, include_ai_image: bool = False, include_diagrams: bool = False, include_flux_images: bool = False ) -> tuple: """Main function to generate PPT with advanced design and enhanced visuals""" if not PPTX_AVAILABLE: return None, "❌ python-pptx 라이브러리가 설치되지 않았습니다.\n\n설치 명령: pip install python-pptx", "" if not topic.strip(): return None, "❌ PPT 주제를 입력해주세요.", "" if num_slides < 3 or num_slides > 20: return None, "❌ 슬라이드 수는 3장 이상 20장 이하로 설정해주세요.", "" try: # 3D 스타일 API 초기화 (표지 이미지용) if include_ai_image and not AI_IMAGE_ENABLED: yield None, "🔄 3D 스타일 이미지 생성 API에 연결하는 중...", "" if initialize_ai_image_api(): yield None, "✅ 3D 스타일 API 연결 성공!", "" else: include_ai_image = False yield None, "⚠️ 3D 스타일 API 연결 실패. AI 이미지 없이 진행합니다.", "" # FLUX API 초기화 (포토리얼리스틱 이미지용) if (include_ai_image or include_flux_images) and not FLUX_API_ENABLED: yield None, "🔄 FLUX 포토리얼리스틱 API에 연결하는 중...", "" if initialize_flux_api(): yield None, "✅ FLUX API 연결 성공!", "" else: if include_ai_image and not AI_IMAGE_ENABLED: include_ai_image = False include_flux_images = False yield None, "⚠️ FLUX API 연결 실패. 포토리얼리스틱 이미지 없이 진행합니다.", "" # 다이어그램 API 초기화 if include_diagrams and not DIAGRAM_API_ENABLED: yield None, "🔄 다이어그램 생성 API에 연결하는 중...", "" if initialize_diagram_api(): yield None, "✅ 다이어그램 API 연결 성공!", "" else: include_diagrams = False yield None, "⚠️ 다이어그램 API 연결 실패. 다이어그램 없이 진행합니다.", "" # Process reference files if provided additional_context = "" chart_data = None if reference_files: file_contents = [] for file_path in reference_files: if file_path.lower().endswith(".csv"): csv_content = analyze_csv_file(file_path) file_contents.append(csv_content) # Extract chart data if available if "CHART_DATA:" in csv_content: chart_json = csv_content.split("CHART_DATA:")[1] try: chart_data = json.loads(chart_json) except: pass elif file_path.lower().endswith(".txt"): file_contents.append(analyze_txt_file(file_path)) elif file_path.lower().endswith(".pdf"): file_contents.append(pdf_to_markdown(file_path)) if file_contents: additional_context = "\n\n".join(file_contents) # Web search if enabled if use_web_search: search_query = extract_keywords(topic, top_k=5) search_results = do_web_search(search_query, use_korean=use_korean) if not search_results.startswith("Web search"): additional_context += f"\n\n{search_results}" # Generate PPT content llm_response = "" for response in generate_ppt_content(topic, num_slides, additional_context, use_korean, layout_style): llm_response = response yield None, f"📝 생성 중...\n\n{response}", response # Parse LLM response slides_data = parse_llm_ppt_response(llm_response, layout_style) # Debug: 파싱된 각 슬라이드 내용 출력 logger.info(f"=== Parsed Slides Debug Info ===") for i, slide in enumerate(slides_data): logger.info(f"Slide {i+1}:") logger.info(f" Title: {slide.get('title', 'NO TITLE')}") logger.info(f" Content: {slide.get('content', 'NO CONTENT')[:100]}...") logger.info(f" Content Length: {len(slide.get('content', ''))}") logger.info("---") # Add chart data to relevant slides if available if chart_data and include_charts: for slide in slides_data: if '데이터' in slide.get('title', '') or 'data' in slide.get('title', '').lower(): slide['chart_data'] = chart_data break # Debug logging logger.info(f"Parsed {len(slides_data)} slides from LLM response") logger.info(f"Design theme: {design_theme}, Layout style: {layout_style}") logger.info(f"Include diagrams: {include_diagrams}, Include FLUX images: {include_flux_images}") if not slides_data: # Show the raw response for debugging error_msg = "❌ PPT 내용 파싱에 실패했습니다.\n\n" error_msg += "LLM 응답을 확인해주세요:\n" error_msg += "=" * 50 + "\n" error_msg += llm_response[:500] + "..." if len(llm_response) > 500 else llm_response yield None, error_msg, llm_response return # AI 이미지 및 다이어그램 생성 알림 visual_features = [] if include_ai_image and FLUX_API_ENABLED: visual_features.append("AI 3D 표지 이미지") if include_diagrams and DIAGRAM_API_ENABLED: visual_features.append("다이어그램") if include_flux_images and FLUX_API_ENABLED: visual_features.append("AI 생성 이미지") if visual_features: yield None, f"📝 슬라이드 생성 완료!\n\n🎨 생성 중: {', '.join(visual_features)}... (시간이 소요될 수 있습니다)", llm_response # Create PPT file with advanced design ppt_path = create_advanced_ppt_from_content( slides_data, topic, design_theme, include_charts, include_ai_image, include_diagrams, include_flux_images ) success_msg = f"✅ PPT 파일이 성공적으로 생성되었습니다!\n\n" success_msg += f"📊 주제: {topic}\n" success_msg += f"📄 슬라이드 수: {len(slides_data)}장\n" success_msg += f"🎨 디자인 테마: {DESIGN_THEMES[design_theme]['name']}\n" success_msg += f"📐 레이아웃 스타일: {layout_style}\n" if include_ai_image and FLUX_API_ENABLED: success_msg += f"🖼️ AI 생성 표지 이미지 포함\n" if include_diagrams and DIAGRAM_API_ENABLED: success_msg += f"📊 AI 생성 다이어그램 포함\n" if include_flux_images and FLUX_API_ENABLED: success_msg += f"🎨 AI 생성 슬라이드 이미지 포함\n" success_msg += f"📝 생성된 슬라이드:\n" for i, slide in enumerate(slides_data[:5]): # Show first 5 slides success_msg += f" {i+1}. {slide.get('title', '제목 없음')} [{slide.get('layout', 'standard')}]\n" if slide.get('notes'): success_msg += f" 💡 노트: {slide.get('notes', '')[:50]}...\n" if len(slides_data) > 5: success_msg += f" ... 외 {len(slides_data) - 5}장" yield ppt_path, success_msg, llm_response except Exception as e: logger.error(f"PPT generation error: {str(e)}") import traceback error_details = traceback.format_exc() logger.error(f"Error details: {error_details}") yield None, f"❌ PPT 생성 중 오류 발생: {str(e)}\n\n상세 오류:\n{error_details}", "" ############################################################################## # Gradio UI ############################################################################## css = """ /* Full width UI */ .gradio-container { background: rgba(255, 255, 255, 0.98); padding: 40px 50px; margin: 30px auto; width: 100% !important; max-width: 1400px !important; border-radius: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } /* Background */ body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin: 0; padding: 0; font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; } /* Title styling */ h1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; margin-bottom: 10px; } /* Button styles */ button.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; border: none; color: white !important; font-weight: 600; padding: 15px 30px !important; font-size: 18px !important; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px; } button.primary:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); } /* Input styles */ .textbox, textarea, input[type="text"], input[type="number"] { border: 2px solid #e5e7eb; border-radius: 12px; padding: 15px; font-size: 16px; transition: all 0.3s ease; background: white; } .textbox:focus, textarea:focus, input[type="text"]:focus { border-color: #667eea; outline: none; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } /* Card style */ .card { background: white; border-radius: 16px; padding: 25px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); margin-bottom: 25px; border: 1px solid rgba(102, 126, 234, 0.1); } /* Dropdown styles */ .dropdown { border: 2px solid #e5e7eb; border-radius: 12px; padding: 12px; background: white; transition: all 0.3s ease; } .dropdown:hover { border-color: #667eea; } /* Slider styles */ .gr-slider input[type="range"] { background: linear-gradient(to right, #667eea 0%, #764ba2 100%); height: 8px; border-radius: 4px; } /* File upload area */ .file-upload { border: 3px dashed #667eea; border-radius: 16px; padding: 40px; text-align: center; transition: all 0.3s ease; background: rgba(102, 126, 234, 0.02); } .file-upload:hover { border-color: #764ba2; background: rgba(102, 126, 234, 0.05); transform: scale(1.01); } /* Checkbox styles */ input[type="checkbox"] { width: 20px; height: 20px; margin-right: 10px; cursor: pointer; } /* Tab styles */ .tabs { border-radius: 12px; overflow: hidden; margin-bottom: 20px; } .tab-nav { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 5px; } .tab-nav button { background: transparent; color: white; border: none; padding: 10px 20px; margin: 0 5px; border-radius: 8px; transition: all 0.3s ease; } .tab-nav button.selected { background: white; color: #667eea; } /* Section headers */ .section-header { font-size: 20px; font-weight: 600; color: #667eea; margin: 20px 0 15px 0; padding-bottom: 10px; border-bottom: 2px solid rgba(102, 126, 234, 0.2); } /* Status box styling */ .status-box { background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-radius: 12px; padding: 20px; } /* Preview box styling */ .preview-box { background: #f8f9fa; border-radius: 12px; padding: 20px; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.5; max-height: 500px; overflow-y: auto; } """ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo: gr.Markdown( """ # 🎯 AI 기반 PPT 자동 생성 시스템 Pro 고급 디자인 테마와 레이아웃을 활용한 전문적인 프레젠테이션을 자동으로 생성합니다. FLUX AI로 생성한 고품질 이미지와 다이어그램을 포함하여 시각적으로 풍부한 PPT를 만듭니다. """ ) with gr.Row(): with gr.Column(scale=2): topic_input = gr.Textbox( label="📌 PPT 주제", placeholder="예: 인공지능의 미래와 산업 적용 사례", lines=2, elem_classes="card" ) with gr.Row(): with gr.Column(): num_slides = gr.Slider( label="📄 슬라이드 수", minimum=3, maximum=20, step=1, value=10, info="생성할 슬라이드 개수 (3-20장)" ) with gr.Column(): use_korean = gr.Checkbox( label="🇰🇷 한국어", value=True, info="한국어로 생성" ) use_web_search = gr.Checkbox( label="🔍 웹 검색", value=False, info="최신 정보 검색" ) # Design Options Section gr.Markdown("
🎨 디자인 옵션
") with gr.Row(): design_theme = gr.Dropdown( label="디자인 테마", choices=[ ("프로페셔널 (파랑/회색)", "professional"), ("모던 (보라/핑크)", "modern"), ("자연 (초록/갈색)", "nature"), ("크리에이티브 (다채로운)", "creative"), ("미니멀 (흑백)", "minimal") ], value="professional", elem_classes="dropdown" ) layout_style = gr.Dropdown( label="레이아웃 스타일", choices=[ ("일관된 레이아웃", "consistent"), ("다양한 레이아웃", "varied"), ("미니멀 레이아웃", "minimal") ], value="consistent", elem_classes="dropdown" ) with gr.Row(): font_style = gr.Dropdown( label="폰트 스타일", choices=[ ("모던", "modern"), ("클래식", "classic"), ("캐주얼", "casual") ], value="modern", elem_classes="dropdown" ) include_charts = gr.Checkbox( label="📊 차트 포함", value=False, info="CSV 데이터가 있을 경우 차트 생성" ) # Visual Enhancement Options gr.Markdown("
🖼️ 시각적 향상 옵션
") with gr.Row(): include_ai_image = gr.Checkbox( label="🖼️ AI 표지 이미지", value=False, info="FLUX로 생성한 표지 이미지 추가" ) include_diagrams = gr.Checkbox( label="📊 AI 다이어그램", value=False, info="내용에 맞는 다이어그램 자동 생성" ) include_flux_images = gr.Checkbox( label="🎨 슬라이드 이미지", value=False, info="일부 슬라이드에 FLUX 이미지 추가" ) reference_files = gr.File( label="📎 참고 자료 (선택사항)", file_types=[".pdf", ".csv", ".txt"], file_count="multiple", elem_classes="file-upload" ) generate_btn = gr.Button( "🚀 PPT 생성하기", variant="primary", size="lg" ) with gr.Column(scale=1): download_file = gr.File( label="📥 생성된 PPT 다운로드", interactive=False, elem_classes="card" ) status_text = gr.Textbox( label="📊 생성 상태", lines=10, interactive=False, elem_classes="status-box" ) with gr.Row(): content_preview = gr.Textbox( label="📝 생성된 내용 미리보기", lines=20, interactive=False, visible=True, elem_classes="preview-box" ) gr.Markdown( """ ### 📋 사용 방법 1. **PPT 주제 입력**: 구체적인 주제일수록 더 좋은 결과 2. **슬라이드 수 선택**: 3-20장 범위에서 선택 3. **디자인 테마 선택**: 5가지 전문적인 테마 중 선택 4. **시각적 옵션 설정**: AI 이미지, 다이어그램, FLUX 이미지 추가 5. **참고 자료 업로드**: PDF, CSV, TXT 파일 지원 6. **생성 버튼 클릭**: AI가 자동으로 PPT 생성 ### 🎨 새로운 기능 - **병렬 AI 이미지 생성**: 3D 스타일과 포토리얼리스틱 API를 동시에 사용하여 생성 시간 단축 - **다양한 이미지 스타일**: 각 슬라이드마다 다른 스타일과 메타포로 이미지 생성 - **AI 표지 이미지**: 우측 하단 배치로 텍스트와 겹치지 않음 - **결론 슬라이드 강조**: 결론/요약 슬라이드에 특별한 이미지 추가 - **좌-우 레이아웃**: 텍스트는 좌측, 시각적 요소는 우측 배치 ### 💡 고급 팁 - 표지 이미지는 우측 하단에 배치되어 제목/부제와 겹치지 않습니다 - 3D 스타일과 포토리얼리스틱 이미지가 병렬로 생성되어 시간이 절약됩니다 - 각 슬라이드는 내용에 맞는 고유한 시각 메타포와 스타일로 이미지가 생성됩니다 - 결론/요약 슬라이드는 자동으로 감지되어 특별한 이미지가 추가됩니다 """ ) # Examples gr.Examples( examples=[ ["인공지능의 미래와 산업 적용 사례", 10, False, True, [], "professional", "modern", "consistent", False, True, True, False], ["2024년 디지털 마케팅 트렌드", 12, True, True, [], "modern", "modern", "consistent", False, True, True, True], ["기후변화와 지속가능한 발전", 15, True, True, [], "nature", "classic", "consistent", False, True, True, True], ["스타트업 사업계획서", 8, False, True, [], "creative", "modern", "varied", False, True, True, True], ], inputs=[topic_input, num_slides, use_web_search, use_korean, reference_files, design_theme, font_style, layout_style, include_charts, include_ai_image, include_diagrams, include_flux_images], ) # Event handler generate_btn.click( fn=generate_ppt, inputs=[ topic_input, num_slides, use_web_search, use_korean, reference_files, design_theme, font_style, layout_style, include_charts, include_ai_image, include_diagrams, include_flux_images ], outputs=[download_file, status_text, content_preview] ) # Initialize APIs on startup if __name__ == "__main__": # Try to initialize APIs in parallel with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: futures = [] if AI_IMAGE_API_URL: futures.append(executor.submit(initialize_ai_image_api)) if FLUX_API_URL: futures.append(executor.submit(initialize_flux_api)) if DIAGRAM_API_URL: futures.append(executor.submit(initialize_diagram_api)) # Wait for all to complete for future in concurrent.futures.as_completed(futures): try: future.result() except Exception as e: logger.error(f"API initialization failed: {e}") demo.launch()