import gradio as gr import replicate import requests import os import json import asyncio import concurrent.futures from io import BytesIO from PIL import Image from typing import List, Tuple, Dict import zipfile from datetime import datetime import time import traceback import base64 from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR import PyPDF2 import pandas as pd import chardet # 환경 변수에서 토큰 가져오기 REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN") FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN") BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN") # 스타일 정의 (표지와 종료 슬라이드 스타일 추가) STYLE_TEMPLATES = { "Title Slide (Hero)": { "name": "Title Slide", "description": "Impactful hero image for title slide", "use_case": "표지, 타이틀", "example": "A dramatic wide-angle view of a modern glass skyscraper reaching into clouds with golden sunset lighting, symbolizing growth and ambition. Ultra-realistic photography style, cinematic composition, lens flare, professional corporate aesthetic" }, "Thank You Slide": { "name": "Thank You", "description": "Elegant closing slide design", "use_case": "마지막 인사", "example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for 'Thank You' text. Minimalist, professional, warm feeling" }, "3D Style (Pixar-like)": { "name": "3D Style", "description": "Pixar-esque 3D render with volumetric lighting", "use_case": "표지, 비전, 미래 컨셉", "example": "A fluffy ginger cat wearing a tiny spacesuit, floating amidst a vibrant nebula in a 3D render. The cat is gazing curiously at a swirling planet with rings made of candy. Background is filled with sparkling stars and colorful gas clouds, lit with soft, volumetric lighting. Style: Pixar-esque, highly detailed, playful. Colors: Deep blues, purples, oranges, and pinks. Rendered in Octane, 8k resolution." }, "Elegant SWOT Quadrant": { "name": "SWOT Analysis", "description": "Flat-design 4-grid layout with minimal shadows", "use_case": "현황 분석, 전략 평가", "example": "Elegant SWOT quadrant: flat-design 4-grid on matte-white backdrop, thin pastel separators, top-left 'Strengths' panel shows glowing shield icon and subtle motif, top-right 'Weaknesses' panel with cracked chain icon in soft crimson, bottom-left 'Opportunities' panel with sunrise-over-horizon icon in optimistic teal, bottom-right 'Threats' panel with storm-cloud & lightning icon in deep indigo, minimal shadows, no text, no watermark, 16:9, 4K" }, "Colorful Mind Map": { "name": "Mind Map", "description": "Hand-drawn educational style with vibrant colors", "use_case": "브레인스토밍, 아이디어 정리", "example": "A handrawn colorful mind map diagram: educational style, vibrant colors, clear hierarchy, golden ratio layout. Central concept with branching sub-topics, each branch with unique color coding, organic flowing connections, doodle-style icons for each node" }, "Business Workflow": { "name": "Business Process", "description": "End-to-end business workflow with clear phases", "use_case": "프로세스 설명, 단계별 진행", "example": "A detailed hand-drawn diagram illustrating an end-to-end business workflow with Market Analysis, Strategy Development, Product Design, Implementation, and Post-Launch Review phases. Clear directional arrows, iconography for each component, vibrant educational yet professional style" }, "Industrial Design": { "name": "Product Design", "description": "Sleek industrial design concept sketch", "use_case": "제품 소개, 컨셉 디자인", "example": "A sleek industrial design concept: Curved metallic body with minimal bezel, Touchscreen panel for settings, Modern matte black finish, Hand-drawn concept sketch style with annotations and dimension lines" }, "3D Bubble Chart": { "name": "Bubble Chart", "description": "Clean 3D bubble visualization", "use_case": "비교 분석, 포지셔닝", "example": "3-D bubble chart on clean white 2×2 grid, quadrant titles hidden, four translucent spheres in lime, azure, amber, magenta, gentle depth-of-field, modern consulting aesthetic, no text, 4K" }, "Timeline Ribbon": { "name": "Timeline", "description": "Horizontal ribbon timeline with cyber-futuristic vibe", "use_case": "일정, 로드맵, 마일스톤", "example": "Horizontal ribbon timeline, milestone pins glowing hot pink on charcoal, year markers as circles, faint motion streaks, cyber-futuristic vibe, no text, 1920×1080" }, "Risk Heat Map": { "name": "Heat Map", "description": "Risk assessment heat map with gradient colors", "use_case": "리스크 분석, 우선순위", "example": "Risk Heat Map: square grid, smooth gradient from mint to fire-red, cells beveled, simple legend strip hidden, long subtle shadow, sterile white frame, no text" }, "Pyramid/Funnel": { "name": "Funnel Chart", "description": "Multi-layer gradient funnel visualization", "use_case": "단계별 축소, 핵심 도출", "example": "Pyramid / Funnel: 5-layer gradient funnel narrowing downwards, top vivid sky-blue, mid mint-green, bottom sunset-orange, glass reflection, minimal background, no text" }, "KPI Dashboard": { "name": "Dashboard", "description": "Dark-mode analytics dashboard with sci-fi interface", "use_case": "성과 지표, 실적 대시보드", "example": "KPI Dashboard: Dark-mode analytic dashboard, three glass speedometers glowing neon lime, two sparkline charts under, black glass background, sci-fi interface, no text, 4K" }, "Value Chain": { "name": "Value Chain", "description": "Horizontal value chain with industrial look", "use_case": "가치 사슬, 비즈니스 모델", "example": "Value Chain Diagram: Horizontal value chain blocks, steel-blue gradient bars with subtle bevel, small gear icons above each segment, sleek industrial look, shadow cast, no text" }, "Gantt Chart": { "name": "Gantt Chart", "description": "Hand-drawn style Gantt chart with playful colors", "use_case": "프로젝트 일정, 작업 관리", "example": "Gantt Chart: Hand-drawn style Gantt bars sketched with vibrant markers on dotted grid notebook page, sticky-note color palette, playful yet organized, perspective tilt, no text" }, "Mobile App Mockup": { "name": "App Mockup", "description": "Clean wireframe for mobile app design", "use_case": "앱/웹 UI, 화면 설계", "example": "MOCKUP DESIGN: A clean hand-drawn style wireframe for a mobile app with Title screen, Login screen, Dashboard with sections, Bottom navigation bar, minimalist design with annotations" }, "Flowchart": { "name": "Flowchart", "description": "Vibrant flowchart with minimalistic icons", "use_case": "의사결정, 프로세스 흐름", "example": "FLOWCHART DESIGN: A hand-drawn style flowchart, vibrant colors, minimalistic icons showing process flow from START to END with decision points, branches, and clear directional arrows" } } # PPT 템플릿 정의 (동적으로 슬라이드 수 조정 가능) PPT_TEMPLATES = { "비즈니스 제안서": { "description": "투자 유치, 사업 제안용", "core_slides": [ {"title": "목차", "style": "Flowchart", "prompt_hint": "프레젠테이션 구조"}, {"title": "문제 정의", "style": "Colorful Mind Map", "prompt_hint": "현재 시장의 문제점"}, {"title": "현황 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "강점, 약점, 기회, 위협"}, {"title": "솔루션", "style": "Industrial Design", "prompt_hint": "제품/서비스 컨셉"}, {"title": "프로세스", "style": "Business Workflow", "prompt_hint": "실행 단계"}, {"title": "일정", "style": "Timeline Ribbon", "prompt_hint": "주요 마일스톤"} ], "optional_slides": [ {"title": "시장 규모", "style": "3D Bubble Chart", "prompt_hint": "시장 기회와 성장성"}, {"title": "경쟁 분석", "style": "Risk Heat Map", "prompt_hint": "경쟁사 포지셔닝"}, {"title": "비즈니스 모델", "style": "Value Chain", "prompt_hint": "수익 구조"}, {"title": "팀 소개", "style": "Colorful Mind Map", "prompt_hint": "핵심 팀원과 역량"}, {"title": "재무 계획", "style": "KPI Dashboard", "prompt_hint": "예상 매출과 손익"}, {"title": "위험 관리", "style": "Risk Heat Map", "prompt_hint": "주요 리스크와 대응"}, {"title": "파트너십", "style": "Business Workflow", "prompt_hint": "전략적 제휴"}, {"title": "기술 스택", "style": "Flowchart", "prompt_hint": "핵심 기술 구조"}, {"title": "고객 사례", "style": "Industrial Design", "prompt_hint": "성공 사례"}, {"title": "성장 전략", "style": "Timeline Ribbon", "prompt_hint": "확장 계획"}, {"title": "투자 활용", "style": "Pyramid/Funnel", "prompt_hint": "자금 사용 계획"}, {"title": "Exit 전략", "style": "Timeline Ribbon", "prompt_hint": "출구 전략"} ] }, "제품 소개": { "description": "신제품 런칭, 서비스 소개용", "core_slides": [ {"title": "제품 컨셉", "style": "Industrial Design", "prompt_hint": "제품 디자인"}, {"title": "사용자 니즈", "style": "Colorful Mind Map", "prompt_hint": "고객 페인포인트"}, {"title": "기능 소개", "style": "Mobile App Mockup", "prompt_hint": "UI/UX 화면"}, {"title": "작동 원리", "style": "Flowchart", "prompt_hint": "기능 플로우"}, {"title": "시장 포지션", "style": "3D Bubble Chart", "prompt_hint": "경쟁사 비교"}, {"title": "출시 일정", "style": "Timeline Ribbon", "prompt_hint": "런칭 로드맵"} ], "optional_slides": [ {"title": "타겟 고객", "style": "Colorful Mind Map", "prompt_hint": "주요 고객층"}, {"title": "가격 정책", "style": "Pyramid/Funnel", "prompt_hint": "가격 전략"}, {"title": "기술 우위", "style": "Industrial Design", "prompt_hint": "핵심 기술"}, {"title": "사용 시나리오", "style": "Business Workflow", "prompt_hint": "활용 사례"}, {"title": "고객 후기", "style": "KPI Dashboard", "prompt_hint": "사용자 평가"}, {"title": "판매 채널", "style": "Value Chain", "prompt_hint": "유통 전략"}, {"title": "마케팅 전략", "style": "Timeline Ribbon", "prompt_hint": "홍보 계획"}, {"title": "성능 비교", "style": "3D Bubble Chart", "prompt_hint": "벤치마크"} ] }, "프로젝트 보고": { "description": "진행 상황, 성과 보고용", "core_slides": [ {"title": "프로젝트 개요", "style": "Business Workflow", "prompt_hint": "전체 프로세스"}, {"title": "진행 현황", "style": "Gantt Chart", "prompt_hint": "작업 일정"}, {"title": "리스크 관리", "style": "Risk Heat Map", "prompt_hint": "위험 요소"}, {"title": "성과 지표", "style": "KPI Dashboard", "prompt_hint": "달성 실적"}, {"title": "향후 계획", "style": "Timeline Ribbon", "prompt_hint": "다음 단계"} ], "optional_slides": [ {"title": "예산 현황", "style": "Pyramid/Funnel", "prompt_hint": "예산 집행"}, {"title": "팀 성과", "style": "3D Bubble Chart", "prompt_hint": "팀별 기여도"}, {"title": "이슈 관리", "style": "Risk Heat Map", "prompt_hint": "주요 이슈"}, {"title": "개선 사항", "style": "Colorful Mind Map", "prompt_hint": "프로세스 개선"}, {"title": "교훈", "style": "Business Workflow", "prompt_hint": "배운 점"} ] }, "전략 기획": { "description": "중장기 전략, 비전 수립용", "core_slides": [ {"title": "비전", "style": "3D Style (Pixar-like)", "prompt_hint": "미래 비전"}, {"title": "환경 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "내외부 환경"}, {"title": "전략 체계", "style": "Colorful Mind Map", "prompt_hint": "전략 구조"}, {"title": "가치 사슬", "style": "Value Chain", "prompt_hint": "비즈니스 모델"}, {"title": "실행 로드맵", "style": "Timeline Ribbon", "prompt_hint": "단계별 계획"}, {"title": "목표 지표", "style": "KPI Dashboard", "prompt_hint": "KPI 목표"} ], "optional_slides": [ {"title": "시장 전망", "style": "3D Bubble Chart", "prompt_hint": "미래 시장"}, {"title": "혁신 방향", "style": "Industrial Design", "prompt_hint": "혁신 전략"}, {"title": "조직 변화", "style": "Business Workflow", "prompt_hint": "조직 개편"}, {"title": "디지털 전환", "style": "Flowchart", "prompt_hint": "DX 전략"}, {"title": "지속가능성", "style": "Timeline Ribbon", "prompt_hint": "ESG 전략"} ] }, "사용자 정의": { "description": "직접 구성하기", "core_slides": [], "optional_slides": [] } } def brave_search(query: str) -> List[Dict]: """Brave Search API를 사용한 웹 검색""" if not BRAVE_API_TOKEN: print("[Brave Search] API 토큰이 없어 검색을 건너뜁니다.") return [] print(f"[Brave Search] 검색어: {query}") headers = { "Accept": "application/json", "X-Subscription-Token": BRAVE_API_TOKEN } params = { "q": query, "count": 5 } try: response = requests.get( "https://api.search.brave.com/res/v1/web/search", headers=headers, params=params, timeout=10 ) if response.status_code == 200: data = response.json() results = [] for item in data.get("web", {}).get("results", [])[:3]: results.append({ "title": item.get("title", ""), "description": item.get("description", ""), "url": item.get("url", "") }) print(f"[Brave Search] {len(results)}개 결과 획득") return results else: print(f"[Brave Search] 오류: {response.status_code}") return [] except Exception as e: print(f"[Brave Search] 예외: {str(e)}") return [] def read_uploaded_file(file_path: str) -> str: """업로드된 파일 읽기 (PDF, CSV, TXT)""" print(f"[파일 읽기] {file_path}") try: # 파일 확장자 확인 ext = os.path.splitext(file_path)[1].lower() if ext == '.pdf': # PDF 읽기 with open(file_path, 'rb') as file: pdf_reader = PyPDF2.PdfReader(file) text = "" for page in pdf_reader.pages: text += page.extract_text() + "\n" return text[:5000] # 최대 5000자 elif ext == '.csv': # CSV 읽기 # 인코딩 감지 with open(file_path, 'rb') as file: raw_data = file.read() result = chardet.detect(raw_data) encoding = result['encoding'] or 'utf-8' df = pd.read_csv(file_path, encoding=encoding) return f"CSV 데이터:\n{df.head(20).to_string()}\n\n요약: {len(df)} 행, {len(df.columns)} 열" elif ext in ['.txt', '.text']: # 텍스트 파일 읽기 with open(file_path, 'rb') as file: raw_data = file.read() result = chardet.detect(raw_data) encoding = result['encoding'] or 'utf-8' with open(file_path, 'r', encoding=encoding) as file: return file.read()[:5000] # 최대 5000자 else: return "지원하지 않는 파일 형식입니다." except Exception as e: return f"파일 읽기 오류: {str(e)}" def generate_presentation_notes(topic: str, slide_title: str, content: Dict) -> str: """각 슬라이드의 발표자 노트 생성 (구어체)""" print(f"[발표자 노트] {slide_title} 생성 중...") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } system_prompt = """You are a professional presentation coach who creates natural, conversational speaker notes. Create speaker notes that: 1. Sound natural and conversational, as if speaking to an audience 2. Include transitions and engagement phrases 3. Reference the slide content but expand with additional context 4. Use a warm, professional tone 5. Be 100-150 words long 6. Include pauses and emphasis markers where appropriate Format: - Use conversational language - Include transition phrases - Add engagement questions or comments - Keep it professional but friendly""" bullet_text = "\n".join(content.get("bullet_points", [])) user_message = f"""Topic: {topic} Slide Title: {slide_title} Subtitle: {content.get('subtitle', '')} Key Points: {bullet_text} Create natural speaker notes for presenting this slide.""" payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": system_prompt }, { "role": "user", "content": user_message } ], "max_tokens": 300, "temperature": 0.8, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() notes = result['choices'][0]['message']['content'].strip() # 한글 주제인 경우 번역 if any(ord('가') <= ord(char) <= ord('힣') for char in topic): notes = translate_content_to_korean(notes) return notes else: return "이 슬라이드에서는 핵심 내용을 설명해 주세요." except Exception as e: print(f"[발표자 노트] 오류: {str(e)}") return "이 슬라이드에서는 핵심 내용을 설명해 주세요." def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]: """각 슬라이드의 텍스트 내용 생성""" print(f"[슬라이드 내용] {slide_title} 텍스트 생성 중...") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content. Your task is to create: 1. A compelling subtitle (max 10 words) 2. Exactly 5 bullet points, each being a complete, concise sentence 3. Each bullet point should be 10-15 words Guidelines: - Be specific and actionable - Use professional business language - Include relevant data points or metrics when appropriate - Ensure content aligns with the slide's purpose - Make each point distinct and valuable - Use active voice and strong verbs Output format: Subtitle: [subtitle here] • [Point 1] • [Point 2] • [Point 3] • [Point 4] • [Point 5]""" user_message = f"""Topic: {topic} Slide Title: {slide_title} Context: {slide_context}""" # 업로드된 콘텐츠가 있으면 추가 if uploaded_content: user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}" # 웹 검색 결과가 있으면 추가 if web_search_results: search_context = "\n\nWeb Search Results:\n" for result in web_search_results[:3]: search_context += f"- {result['title']}: {result['description']}\n" user_message += search_context user_message += "\n\nCreate compelling content for this presentation slide." payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": system_prompt }, { "role": "user", "content": user_message } ], "max_tokens": 300, "top_p": 0.8, "temperature": 0.7, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() content = result['choices'][0]['message']['content'].strip() # Parse content lines = content.split('\n') subtitle = "" bullet_points = [] for line in lines: if line.startswith("Subtitle:"): subtitle = line.replace("Subtitle:", "").strip() elif line.strip().startswith("•"): bullet_points.append(line.strip()) # 한글로 번역이 필요한 경우 if any(ord('가') <= ord(char) <= ord('힣') for char in topic): subtitle = translate_content_to_korean(subtitle) bullet_points = [translate_content_to_korean(point) for point in bullet_points] return { "subtitle": subtitle, "bullet_points": bullet_points[:5] # 최대 5개 } else: return { "subtitle": slide_title, "bullet_points": ["내용을 생성할 수 없습니다."] * 5 } except Exception as e: print(f"[슬라이드 내용] 오류: {str(e)}") return { "subtitle": slide_title, "bullet_points": ["내용을 생성할 수 없습니다."] * 5 } def translate_content_to_korean(text: str) -> str: """영어 텍스트를 한글로 번역""" url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation." }, { "role": "user", "content": text } ], "max_tokens": 200, "top_p": 0.8, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() return result['choices'][0]['message']['content'].strip() else: return text except Exception as e: return text def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str: """주제와 스타일 예제를 받아서 LLM을 사용해 이미지 프롬프트를 생성""" print(f"[LLM] 프롬프트 생성 시작: {slide_context}") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides. Your task is to create prompts that: 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals 2. Consider the slide's purpose and maintain consistency across a presentation 3. Include style references matching the given example 4. Focus on clean, professional visuals that won't distract from text overlays 5. Ensure high contrast areas for text readability when needed 6. Maintain brand consistency and professional aesthetics Important guidelines: - If given a style example, adapt the topic to match that specific visual style - Consider the slide context (e.g., "표지", "현황 분석") to create appropriate visuals - Always output ONLY the prompt without any explanation - Keep prompts between 50-150 words for optimal results - Ensure the visual supports rather than overwhelms the slide content""" user_message = f"Topic: {topic}" if style_example: user_message += f"\n\nStyle reference to follow:\n{style_example}" if slide_context: user_message += f"\n\nSlide context: {slide_context}" if uploaded_content: user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}" payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": system_prompt }, { "role": "user", "content": user_message } ], "max_tokens": 300, "top_p": 0.8, "temperature": 0.7, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() prompt = result['choices'][0]['message']['content'].strip() print(f"[LLM] 프롬프트 생성 완료: {prompt[:50]}...") return prompt else: error_msg = f"프롬프트 생성 실패: {response.status_code}" print(f"[LLM] {error_msg}") return error_msg except Exception as e: error_msg = f"프롬프트 생성 중 오류 발생: {str(e)}" print(f"[LLM] {error_msg}") return error_msg def translate_to_english(text: str) -> str: """한글 텍스트를 영어로 번역 (LLM 사용)""" if not any(ord('가') <= ord(char) <= ord('힣') for char in text): return text print(f"[번역] 한글 감지, 영어로 번역 시작") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation." }, { "role": "user", "content": text } ], "max_tokens": 500, "top_p": 0.8, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() translated = result['choices'][0]['message']['content'].strip() print(f"[번역] 완료") return translated else: print(f"[번역] 실패, 원본 사용") return text except Exception as e: print(f"[번역] 오류: {str(e)}, 원본 사용") return text def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]: """Replicate API를 사용해 이미지 생성""" print(f"\n[이미지 생성] {slide_info}") print(f"[이미지 생성] 프롬프트: {prompt[:50]}...") try: english_prompt = translate_to_english(prompt) if not REPLICATE_API_TOKEN: error_msg = "RAPI_TOKEN 환경변수가 설정되지 않았습니다." print(f"[이미지 생성] 오류: {error_msg}") return None, error_msg print(f"[이미지 생성] Replicate API 호출 중...") client = replicate.Client(api_token=REPLICATE_API_TOKEN) input_params = { "seed": seed, "prompt": english_prompt, "speed_mode": "Extra Juiced 🚀 (even more speed)", "output_quality": 100 } start_time = time.time() output = client.run( "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645", input=input_params ) elapsed = time.time() - start_time print(f"[이미지 생성] API 응답 받음 ({elapsed:.1f}초)") if output: if isinstance(output, str) and output.startswith('http'): print(f"[이미지 생성] URL에서 이미지 다운로드 중...") response = requests.get(output, timeout=30) img = Image.open(BytesIO(response.content)) print(f"[이미지 생성] 완료!") return img, english_prompt else: print(f"[이미지 생성] 바이너리 데이터 처리 중...") img = Image.open(BytesIO(output.read())) print(f"[이미지 생성] 완료!") return img, english_prompt else: error_msg = "이미지 생성 실패 - 빈 응답" print(f"[이미지 생성] {error_msg}") return None, error_msg except Exception as e: error_msg = f"오류: {str(e)}" print(f"[이미지 생성] {error_msg}") print(f"[이미지 생성] 상세 오류:\n{traceback.format_exc()}") return None, error_msg def create_slide_preview_html(slide_data: Dict) -> str: """16:9 비율의 슬라이드 프리뷰 HTML 생성""" # 이미지를 base64로 인코딩 img_base64 = "" if slide_data.get("image"): buffered = BytesIO() slide_data["image"].save(buffered, format="PNG") img_base64 = base64.b64encode(buffered.getvalue()).decode() # 텍스트 내용 가져오기 subtitle = slide_data.get("subtitle", "") bullet_points = slide_data.get("bullet_points", []) # HTML 생성 html = f"""
슬라이드 {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}
""" # 표지와 마지막 슬라이드는 전체 화면 이미지 if slide_data.get('title') in ['표지', 'Thank You']: html += f"""
""" if img_base64: html += f""" Slide Image

{slide_data.get('topic', '')}

{subtitle}

""" html += """
""" else: # 일반 슬라이드 레이아웃 html += f"""

{subtitle}

    """ for point in bullet_points: html += f"""
  • {point.replace('•', '').strip()}
  • """ html += f"""
""" if img_base64: html += f""" Slide Image """ else: html += """
🖼️

이미지 생성 중...

""" html += """
""" html += """
""" return html def create_pptx_file(results: List[Dict], topic: str, template_name: str) -> str: """생성된 결과를 PPTX 파일로 변환 (발표자 노트 포함)""" print("[PPTX] 파일 생성 시작...") # 프레젠테이션 생성 (16:9 비율) prs = Presentation() prs.slide_width = Inches(16) prs.slide_height = Inches(9) # 각 결과 슬라이드 추가 for i, result in enumerate(results): if not result.get("success", False): continue slide_data = result.get("slide_data", {}) # 빈 레이아웃 사용 (제목 플레이스홀더 없는 완전한 빈 슬라이드) blank_layout = prs.slide_layouts[6] # 완전히 빈 레이아웃 slide = prs.slides.add_slide(blank_layout) # 표지 슬라이드 if slide_data.get('title') == '표지': # 배경 이미지 추가 if slide_data.get('image'): try: img_buffer = BytesIO() slide_data['image'].save(img_buffer, format='PNG') img_buffer.seek(0) # 전체 화면 배경 이미지 pic = slide.shapes.add_picture( img_buffer, 0, 0, width=prs.slide_width, height=prs.slide_height ) # 맨 뒤로 보내기 slide.shapes._spTree.remove(pic._element) slide.shapes._spTree.insert(2, pic._element) except Exception as e: print(f"[PPTX] 표지 이미지 추가 실패: {str(e)}") # 제목 텍스트 추가 title_box = slide.shapes.add_textbox( Inches(2), Inches(3), Inches(12), Inches(3) ) title_frame = title_box.text_frame title_frame.text = topic title_para = title_frame.paragraphs[0] title_para.font.size = Pt(48) title_para.font.bold = True title_para.font.color.rgb = RGBColor(255, 255, 255) title_para.alignment = PP_ALIGN.CENTER # 부제목 추가 subtitle_box = slide.shapes.add_textbox( Inches(2), Inches(6), Inches(12), Inches(2) ) subtitle_frame = subtitle_box.text_frame subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI 프레젠테이션') subtitle_para = subtitle_frame.paragraphs[0] subtitle_para.font.size = Pt(24) subtitle_para.font.color.rgb = RGBColor(255, 255, 255) subtitle_para.alignment = PP_ALIGN.CENTER # Thank You 슬라이드 elif slide_data.get('title') == 'Thank You': # 배경 이미지 추가 if slide_data.get('image'): try: img_buffer = BytesIO() slide_data['image'].save(img_buffer, format='PNG') img_buffer.seek(0) # 전체 화면 배경 이미지 pic = slide.shapes.add_picture( img_buffer, 0, 0, width=prs.slide_width, height=prs.slide_height ) # 맨 뒤로 보내기 slide.shapes._spTree.remove(pic._element) slide.shapes._spTree.insert(2, pic._element) except Exception as e: print(f"[PPTX] Thank You 이미지 추가 실패: {str(e)}") # Thank You 텍스트 thanks_box = slide.shapes.add_textbox( Inches(2), Inches(3.5), Inches(12), Inches(2) ) thanks_frame = thanks_box.text_frame thanks_frame.text = "Thank You" thanks_para = thanks_frame.paragraphs[0] thanks_para.font.size = Pt(60) thanks_para.font.bold = True thanks_para.font.color.rgb = RGBColor(255, 255, 255) thanks_para.alignment = PP_ALIGN.CENTER # 일반 슬라이드 else: # 슬라이드 제목 추가 (상단) title_box = slide.shapes.add_textbox( Inches(0.5), Inches(0.3), Inches(15), Inches(0.8) ) title_frame = title_box.text_frame title_frame.text = f"{slide_data.get('title', '')}" title_para = title_frame.paragraphs[0] title_para.font.size = Pt(28) title_para.font.bold = True title_para.font.color.rgb = RGBColor(44, 62, 80) # 좌측 텍스트 영역 text_box = slide.shapes.add_textbox( Inches(0.5), Inches(1.5), Inches(7.5), Inches(6.5) ) text_frame = text_box.text_frame text_frame.word_wrap = True # 소제목 추가 subtitle_para = text_frame.paragraphs[0] subtitle_para.text = slide_data.get('subtitle', '') subtitle_para.font.size = Pt(20) subtitle_para.font.bold = True subtitle_para.font.color.rgb = RGBColor(52, 73, 94) subtitle_para.space_after = Pt(20) # 불릿 포인트 추가 bullet_points = slide_data.get('bullet_points', []) for point in bullet_points: p = text_frame.add_paragraph() p.text = point.replace('•', '').strip() p.font.size = Pt(16) p.font.color.rgb = RGBColor(52, 73, 94) p.level = 0 p.space_after = Pt(12) p.line_spacing = 1.5 # 우측 이미지 추가 if slide_data.get('image'): try: img_buffer = BytesIO() slide_data['image'].save(img_buffer, format='PNG') img_buffer.seek(0) pic = slide.shapes.add_picture( img_buffer, Inches(8.5), Inches(1.5), width=Inches(7), height=Inches(6) ) pic.line.color.rgb = RGBColor(189, 195, 199) pic.line.width = Pt(1) except Exception as e: print(f"[PPTX] 이미지 추가 실패: {str(e)}") # 페이지 번호 추가 page_num = slide.shapes.add_textbox( Inches(15), Inches(8.5), Inches(1), Inches(0.5) ) page_frame = page_num.text_frame page_frame.text = str(i + 1) page_para = page_frame.paragraphs[0] page_para.font.size = Pt(12) page_para.font.color.rgb = RGBColor(127, 140, 141) page_para.alignment = PP_ALIGN.RIGHT # 발표자 노트 추가 notes_slide = slide.notes_slide notes_slide.notes_text_frame.text = slide_data.get('speaker_notes', '') # 파일 저장 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"presentation_{timestamp}.pptx" filepath = os.path.join("/tmp", filename) prs.save(filepath) print(f"[PPTX] 파일 생성 완료: {filename}") return filepath def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> List[Dict]: """선택된 슬라이드 수에 따라 동적으로 슬라이드 구성""" core_slides = template.get("core_slides", []) optional_slides = template.get("optional_slides", []) # 표지와 Thank You를 제외한 본문 슬라이드 수 content_slide_count = slide_count # 코어 슬라이드가 요청된 수보다 많으면 조정 if len(core_slides) > content_slide_count: selected_slides = core_slides[:content_slide_count] else: # 코어 슬라이드 + 옵셔널 슬라이드에서 선택 selected_slides = core_slides.copy() remaining = content_slide_count - len(core_slides) if remaining > 0 and optional_slides: # 옵셔널 슬라이드에서 추가 선택 additional = optional_slides[:remaining] selected_slides.extend(additional) # 표지 추가 (맨 앞) slides = [{"title": "표지", "style": "Title Slide (Hero)", "prompt_hint": "프레젠테이션 표지"}] # 본문 슬라이드 추가 slides.extend(selected_slides) # Thank You 슬라이드 추가 (맨 뒤) slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"}) return slides def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict], slide_count: int, seed: int, uploaded_file: str, use_web_search: bool, progress=gr.Progress()): """PPT 이미지와 텍스트 내용을 함께 생성 (발표자 노트 포함)""" results = [] preview_html = "" # 업로드된 파일 내용 읽기 uploaded_content = "" if uploaded_file: uploaded_content = read_uploaded_file(uploaded_file.name) print(f"[파일 업로드] 내용 길이: {len(uploaded_content)}자") # 템플릿 선택 및 슬라이드 구성 if template_name == "사용자 정의" and custom_slides: slides = [{"title": "표지", "style": "Title Slide (Hero)", "prompt_hint": "프레젠테이션 표지"}] slides.extend(custom_slides) slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"}) else: template = PPT_TEMPLATES[template_name] slides = generate_dynamic_slides(topic, template, slide_count) if not slides: yield "", "슬라이드가 정의되지 않았습니다.", None return total_slides = len(slides) print(f"\n[PPT 생성] 시작 - 총 {total_slides}개 슬라이드 (표지 + 본문 {slide_count} + Thank You)") print(f"[PPT 생성] 주제: {topic}") print(f"[PPT 생성] 템플릿: {template_name}") print(f"[PPT 생성] 웹 검색: {'사용' if use_web_search else '미사용'}") # 웹 검색 실행 (선택된 경우) web_search_results = [] if use_web_search and BRAVE_API_TOKEN: progress(0.05, "웹 검색 중...") web_search_results = brave_search(topic) # CSS 스타일 추가 preview_html = """
""" # 각 슬라이드 순차 처리 for i, slide in enumerate(slides): progress((i + 1) / (total_slides + 1), f"슬라이드 {i+1}/{total_slides} 처리 중...") slide_info = f"슬라이드 {i+1}: {slide['title']}" # 텍스트 내용 생성 slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}" content = generate_slide_content( topic, slide['title'], slide_context, uploaded_content, web_search_results ) # 발표자 노트 생성 speaker_notes = generate_presentation_notes(topic, slide['title'], content) # 프롬프트 생성 및 이미지 생성 style_key = slide["style"] if style_key in STYLE_TEMPLATES: style_info = STYLE_TEMPLATES[style_key] prompt = generate_prompt_with_llm( topic, style_info["example"], slide_context, uploaded_content ) # 이미지 생성 slide_seed = seed + i img, used_prompt = generate_image(prompt, slide_seed, slide_info) # 슬라이드 데이터 구성 slide_data = { "slide_number": i + 1, "title": slide["title"], "subtitle": content["subtitle"], "bullet_points": content["bullet_points"], "image": img, "style": style_info["name"], "speaker_notes": speaker_notes, "topic": topic # 표지용 } # 프리뷰 HTML 생성 preview_html += create_slide_preview_html(slide_data) # 현재까지의 상태 업데이트 yield preview_html + "
", f"### 🔄 {slide_info} 생성 중...", None results.append({ "slide_data": slide_data, "success": img is not None }) # PPTX 파일 생성 progress(0.95, "PPTX 파일 생성 중...") pptx_path = None try: pptx_path = create_pptx_file(results, topic, template_name) except Exception as e: print(f"[PPTX] 파일 생성 오류: {str(e)}") # 최종 결과 preview_html += "" progress(1.0, "완료!") successful = sum(1 for r in results if r["success"]) final_status = f"### 🎉 생성 완료! 총 {total_slides}개 슬라이드 중 {successful}개 성공" if pptx_path: final_status += f"\n\n### 📥 PPTX 파일이 준비되었습니다! 아래에서 다운로드하세요." final_status += f"\n\n💡 **발표자 노트가 각 슬라이드에 포함되어 있습니다!**" yield preview_html, final_status, pptx_path def create_custom_slides_ui(): """사용자 정의 슬라이드 구성 UI (3-20장)""" slides = [] for i in range(20): # 최대 20장 with gr.Row(visible=(i < 3)): # 기본 3장만 표시 with gr.Column(scale=2): title = gr.Textbox( label=f"슬라이드 {i+1} 제목", placeholder="예: 현황 분석, 솔루션, 로드맵...", ) with gr.Column(scale=3): style = gr.Dropdown( choices=list(STYLE_TEMPLATES.keys()), label=f"스타일 선택", value="Colorful Mind Map" ) with gr.Column(scale=3): hint = gr.Textbox( label=f"프롬프트 힌트", placeholder="이 슬라이드에서 표현하고 싶은 내용" ) slides.append({"title": title, "style": style, "hint": hint, "row": gr.Row}) return slides # Gradio 인터페이스 생성 with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css=""" .preview-container { max-width: 1400px; margin: 0 auto; } """) as demo: gr.Markdown(""" # 🎯 AI 기반 PPT 통합 생성기 (업그레이드 버전) ### 텍스트와 이미지가 완벽하게 조화된 프레젠테이션을 자동으로 생성하고 다운로드하세요! #### 🆕 새로운 기능: - 📊 **표지와 Thank You 슬라이드** 자동 추가 - 📝 **발표자 노트** 자동 생성 (구어체) - 📁 **파일 업로드** 지원 (PDF/CSV/TXT) - 🔍 **웹 검색** 기능 (Brave Search) - 🎚️ **슬라이드 수 조절** (6-20장) """) # API 토큰 상태 확인 token_status = [] if not REPLICATE_API_TOKEN: token_status.append("⚠️ RAPI_TOKEN 환경 변수가 설정되지 않았습니다.") if not FRIENDLI_TOKEN: token_status.append("⚠️ FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.") if not BRAVE_API_TOKEN: token_status.append("ℹ️ BAPI_TOKEN이 없어 웹 검색 기능이 비활성화됩니다.") if token_status: gr.Markdown("\n".join(token_status)) with gr.Row(): with gr.Column(scale=1): # 기본 입력 topic_input = gr.Textbox( label="프레젠테이션 주제", placeholder="예: AI 스타트업 투자 유치, 신제품 런칭, 디지털 전환 전략", lines=2 ) # PPT 템플릿 선택 template_select = gr.Dropdown( choices=list(PPT_TEMPLATES.keys()), label="PPT 템플릿 선택", value="비즈니스 제안서", info="목적에 맞는 템플릿을 선택하세요" ) # 슬라이드 수 선택 slide_count = gr.Slider( minimum=6, maximum=20, value=8, step=1, label="본문 슬라이드 수 (표지와 Thank You 제외)", info="생성할 본문 슬라이드 수를 선택하세요" ) # 파일 업로드 file_upload = gr.File( label="참고 자료 업로드 (선택)", file_types=[".pdf", ".csv", ".txt"], type="filepath" ) # 웹 검색 옵션 use_web_search = gr.Checkbox( label="웹 검색 사용", value=False, info="Brave Search를 사용하여 최신 정보 반영" ) # 템플릿 설명 template_info = gr.Markdown() # 시드 값 seed_input = gr.Slider( minimum=1, maximum=100, value=10, step=1, label="시드 값" ) generate_btn = gr.Button("🚀 PPT 전체 생성 (텍스트 + 이미지 + 발표노트)", variant="primary", size="lg") # PPTX 다운로드 영역 with gr.Row(): download_file = gr.File( label="📥 생성된 PPTX 파일 다운로드", visible=True, elem_id="download-file" ) # 사용자 정의 섹션 with gr.Accordion("📝 사용자 정의 슬라이드 구성", open=False) as custom_accordion: gr.Markdown("템플릿을 사용하지 않고 직접 슬라이드를 구성하세요. (3-20장)") # 사용자 정의 슬라이드 수 선택 custom_slide_count = gr.Slider( minimum=3, maximum=20, value=3, step=1, label="사용자 정의 슬라이드 수" ) custom_slides_components = create_custom_slides_ui() # 상태 표시 status_output = gr.Markdown( value="### 👆 템플릿을 선택하고 생성 버튼을 클릭하세요!" ) # 프리뷰 영역 preview_output = gr.HTML( label="PPT 프리뷰 (16:9)", elem_classes="preview-container" ) # 이벤트 핸들러 def update_template_info(template_name, slide_count): if template_name in PPT_TEMPLATES: template = PPT_TEMPLATES[template_name] info = f"**{template['description']}**\n\n" # 동적으로 생성될 슬라이드 구성 표시 slides = generate_dynamic_slides("", template, slide_count) info += f"생성될 슬라이드 ({len(slides)}장):\n" for i, slide in enumerate(slides): style_info = STYLE_TEMPLATES.get(slide['style'], {}) info += f"{i+1}. {slide['title']} - {style_info.get('use_case', '')}\n" return info return "" def update_custom_slides_visibility(count): """사용자 정의 슬라이드 수에 따라 UI 표시/숨김""" updates = [] for i in range(20): updates.extend([ gr.update(visible=(i < count)), # title gr.update(visible=(i < count)), # style gr.update(visible=(i < count)) # hint ]) return updates def generate_ppt_handler(topic, template_name, slide_count, seed, file_upload, use_web_search, custom_slide_count, progress=gr.Progress(), *custom_inputs): if not topic.strip(): yield "", "❌ 주제를 입력해주세요.", None return # 사용자 정의 슬라이드 처리 custom_slides = [] if template_name == "사용자 정의": for i in range(0, custom_slide_count * 3, 3): if i < len(custom_inputs): title = custom_inputs[i] style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else "" if title and style: custom_slides.append({ "title": title, "style": style, "prompt_hint": hint }) # PPT 생성 for preview, status, pptx_file in generate_ppt_with_content( topic, template_name, custom_slides, slide_count, seed, file_upload, use_web_search, progress ): yield preview, status, pptx_file # 이벤트 연결 template_select.change( fn=update_template_info, inputs=[template_select, slide_count], outputs=[template_info] ) slide_count.change( fn=update_template_info, inputs=[template_select, slide_count], outputs=[template_info] ) custom_slide_count.change( fn=update_custom_slides_visibility, inputs=[custom_slide_count], outputs=[comp for slide in custom_slides_components for comp in [slide["title"], slide["style"], slide["hint"]]] ) # 사용자 정의 입력 수집 all_custom_inputs = [] for slide_components in custom_slides_components: all_custom_inputs.extend([ slide_components["title"], slide_components["style"], slide_components["hint"] ]) generate_btn.click( fn=generate_ppt_handler, inputs=[topic_input, template_select, slide_count, seed_input, file_upload, use_web_search, custom_slide_count] + all_custom_inputs, outputs=[preview_output, status_output, download_file] ) # 초기 템플릿 정보 표시 demo.load( fn=update_template_info, inputs=[template_select, slide_count], outputs=[template_info] ) # 앱 실행 if __name__ == "__main__": print("\n" + "="*50) print("🚀 PPT 통합 생성기 (업그레이드 버전) 시작!") print("="*50) # 환경 변수 확인 if not REPLICATE_API_TOKEN: print("⚠️ 경고: RAPI_TOKEN 환경 변수가 설정되지 않았습니다.") else: print("✅ RAPI_TOKEN 확인됨") if not FRIENDLI_TOKEN: print("⚠️ 경고: FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.") else: print("✅ FRIENDLI_TOKEN 확인됨") if not BRAVE_API_TOKEN: print("ℹ️ BAPI_TOKEN이 없어 웹 검색 기능이 비활성화됩니다.") else: print("✅ BAPI_TOKEN 확인됨") print("="*50 + "\n") demo.launch()