diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3045 +1,35 @@ -#!/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 - -# ────────────────── 한글 / 폰트 설정 ────────────────── -import matplotlib as mpl -import matplotlib.font_manager as fm -import subprocess - -import base64, io, requests - -NANUM_PATH = "/usr/share/fonts/truetype/nanum/NanumGothic.ttf" # 시스템 설치 경로 - -# matplotlib용 글꼴 등록 -fm.fontManager.addfont(NANUM_PATH) -mpl.rcParams["font.family"] = "NanumGothic" - -# Graphviz가 찾을 수 있도록 폰트 경로 지정 -os.environ["GDFONTPATH"] = "/usr/share/fonts/truetype/nanum" - -# 폰트 캐시 강제 갱신(한 번만 실행돼도 무방) -subprocess.run(["fc-cache", "-fv"]) - -# ────────────────── 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") - -# 다이어그램 생성기 모듈 import -try: - from concept_map_generator import generate_concept_map - from synoptic_chart_generator import generate_synoptic_chart - from radial_diagram_generator import generate_radial_diagram - from process_flow_generator import generate_process_flow_diagram - from wbs_diagram_generator import generate_wbs_diagram - DIAGRAM_GENERATORS_AVAILABLE = True - logger.info("다이어그램 생성기 모듈 로드 성공") -except ImportError as e: - DIAGRAM_GENERATORS_AVAILABLE = False - logger.warning(f"다이어그램 생성기 모듈을 찾을 수 없습니다: {e}") - -# ────────────────── 한글 폰트 경로 설정 (다이어그램용) ────────────────── -KOREAN_FONT_PATH = os.path.join(os.path.dirname(__file__), "NanumGothic-Regular.ttf") -if not os.path.exists(KOREAN_FONT_PATH): - # 로컬 폰트가 없으면 시스템 설치 경로 사용 - KOREAN_FONT_PATH = NANUM_PATH - -# 다른 모듈에서 사용할 수 있도록 환경변수 등록 -os.environ["KOREAN_FONT_PATH"] = KOREAN_FONT_PATH - - -############################################################################## -# 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(f"Connecting to FLUX API at {FLUX_API_URL}...") - flux_api_client = Client(FLUX_API_URL) - - # API 정보 확인 - if hasattr(flux_api_client, 'view_api'): - api_info = flux_api_client.view_api() - logger.info(f"FLUX API endpoints: {api_info}") - - 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 - -############################################################################## -# 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 with Priority Score -############################################################################## -def detect_diagram_type_with_score(title: str, content: str) -> Tuple[Optional[str], float]: - """슬라이드 내용을 분석하여 적절한 다이어그램 타입과 필요도 점수 결정""" - combined_text = f"{title} {content}".lower() - - # 각 다이어그램 타입별 키워드와 가중치 - diagram_scores = { - "Process Flow": 0, - "WBS Diagram": 0, - "Concept Map": 0, - "Radial Diagram": 0, - "Synoptic Chart": 0 - } - - # Process Flow keywords with weights - if any(word in combined_text for word in ['프로세스', 'process', '절차', 'procedure']): - diagram_scores["Process Flow"] += 3 - if any(word in combined_text for word in ['단계', 'step', 'flow', '흐름']): - diagram_scores["Process Flow"] += 2 - if any(word in combined_text for word in ['워크플로우', 'workflow', '순서', 'sequence']): - diagram_scores["Process Flow"] += 2 - - # WBS keywords with weights - if any(word in combined_text for word in ['wbs', '작업분해', '프로젝트', 'project']): - diagram_scores["WBS Diagram"] += 3 - if any(word in combined_text for word in ['업무분해', 'breakdown', '구조도', '작업구조']): - diagram_scores["WBS Diagram"] += 2 - - # Concept Map keywords with weights - if any(word in combined_text for word in ['개념', 'concept', '관계', 'relationship']): - diagram_scores["Concept Map"] += 3 - if any(word in combined_text for word in ['연관', 'connection', '마인드맵', 'mindmap']): - diagram_scores["Concept Map"] += 2 - if any(word in combined_text for word in ['구조', 'structure', '체계', 'system']): - diagram_scores["Concept Map"] += 1 - - # Radial Diagram keywords with weights - if any(word in combined_text for word in ['중심', 'central', '방사형', 'radial']): - diagram_scores["Radial Diagram"] += 3 - if any(word in combined_text for word in ['핵심', 'core', '주요', 'main']): - diagram_scores["Radial Diagram"] += 2 - - # Synoptic Chart keywords with weights - if any(word in combined_text for word in ['개요', 'overview', '전체', 'overall']): - diagram_scores["Synoptic Chart"] += 3 - if any(word in combined_text for word in ['요약', 'summary', '시놉틱', 'synoptic']): - diagram_scores["Synoptic Chart"] += 2 - - # 추가 점수 부여: 리스트나 구조화된 내용이 많은 경우 - if content.count('\n-') > 3 or content.count('\n•') > 3: - diagram_scores["Concept Map"] += 1 - if any(char in content for char in ['1.', '2.', '3.', '①', '②', '③']): - diagram_scores["Process Flow"] += 1 - - # 가장 높은 점수의 다이어그램 타입 선택 - max_score = max(diagram_scores.values()) - if max_score > 0: - best_type = max(diagram_scores.items(), key=lambda x: x[1])[0] - # 필요도 점수 계산 (0-1 범위) - necessity_score = min(max_score / 5.0, 1.0) # 최대 5점을 1.0으로 정규화 - return best_type, necessity_score - - return None, 0.0 - -############################################################################## -# 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 -6. For Process Flow: 'type' can be "process", "decision", "start", "end" -7. Ensure all connections reference existing node IDs""" - - 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 Local Generators with Korean Font -############################################################################## -def generate_diagram_locally(json_data: str, diagram_type: str, output_format: str = "png") -> Optional[str]: - """로컬 생성기를 사용하여 다이어그램 생성 (한글 폰트 적용)""" - if not DIAGRAM_GENERATORS_AVAILABLE: - logger.error("다이어그램 생성기 모듈을 사용할 수 없습니다") - return None - - try: - # 한글 폰트 경로를 환경변수로 설정 - os.environ['KOREAN_FONT_PATH'] = KOREAN_FONT_PATH - - # 적절한 생성기를 사용하여 다이어그램 생성 - if diagram_type == "Concept Map": - result = generate_concept_map(json_data, output_format) - elif diagram_type == "Synoptic Chart": - result = generate_synoptic_chart(json_data, output_format) - elif diagram_type == "Radial Diagram": - result = generate_radial_diagram(json_data, output_format) - elif diagram_type == "Process Flow": - result = generate_process_flow_diagram(json_data, output_format) - elif diagram_type == "WBS Diagram": - result = generate_wbs_diagram(json_data, output_format) - else: - logger.error(f"Unknown diagram type: {diagram_type}") - return None - - # 결과가 문자열이고 에러 메시지인 경우 - if isinstance(result, str) and result.startswith("Error:"): - logger.error(f"Diagram generation error: {result}") - return None - - # 성공적으로 생성된 경우 파일 경로 반환 - return result - - except Exception as e: - logger.error(f"Failed to generate diagram locally: {e}") - return None - - -############################################################################## -# FLUX Image Prompt Generator – supports 6 diagram styles -############################################################################## -# ────────────────── Prompt Templates (6 Styles) ────────────────── -EXAMPLE_PROMPTS: dict[str, str] = { - "Product Design": ( - "A sleek industrial product-design sketch showing the hierarchy below.\n" - "{tree}" - ), - "Mindmap": ( - "A hand-drawn, colorful MIND-MAP of the following tree (central node on the left, radial branches).\n" - "{tree}" - ), - "Mockup": ( - "A clean hand-drawn wire-frame mock-up – lay out the following screens/pages as boxes and arrows.\n" - "{tree}" - ), - "Infographic": ( - "A flat CORPORATE INFOGRAPHIC that arranges each item below as its own icon + label.\n" - "{tree}" - ), - "Diagram": ( - "A hand-drawn BUSINESS-PROCESS DIAGRAM. Use swim-lanes/boxes connected by arrows following the tree below.\n" - "{tree}" - ), - "Flowchart": ( - "A vibrant hand-drawn FLOW-CHART. Use decision diamonds / process boxes exactly in this order:\n" - "{tree}" - ), -} - -STYLE_KEYS = list(EXAMPLE_PROMPTS.keys()) - -def pick_flux_style(slide_idx: int) -> str: - """ - 슬라이드 번호(i)를 6가지 FLUX 스타일 키 - ('Product Design' … 'Flowchart') 중 하나로 매핑한다. - """ - return STYLE_KEYS[slide_idx % len(STYLE_KEYS)] - -# ------------------------------------------------------------------ -# 2) generate_flux_prompt 함수 전체 ★ 수정 완료 -# ------------------------------------------------------------------ -def generate_flux_prompt(title: str, content: str, style_key: str) -> str: - """ - Build a FLUX image-generation prompt for one slide, using six pre-defined - visual styles (Product Design, Mindmap, Mockup, Infographic, Diagram, - Flowchart). - """ - # 1) clean bullet points (max 8) - bullets = [ - re.sub(r'^[\+\-\•\●]\s*', '', line).strip() - for line in content.splitlines() - if line.lstrip().startswith(('+', '-', '•', '●')) - ][:8] - - if bullets: - tree_block = '\n'.join( - f"{'├──' if i < len(bullets)-1 else '└──'} {b}" - for i, b in enumerate(bullets) - ) - else: - # 불릿이 없으면 제목을 사용 - tree_block = f"└── {title}" - - # 2) choose template & inject tree only (not nodes) - template = EXAMPLE_PROMPTS.get(style_key, EXAMPLE_PROMPTS["Diagram"]) - prompt_body = template.format(tree=tree_block) # ← nodes 제거, tree만 사용 - - # 3) stylistic tail - tail = ( - "corporate colour palette, white background, hand-drawn line style; " - "boxes, arrows, connectors clearly visible; high-resolution vector." - ) - - final_prompt = f"{prompt_body}\n\n{tail}".strip() - - # 디버깅용 로그 추가 - logger.info(f"[FLUX Prompt] Style: {style_key}") - logger.info(f"[FLUX Prompt] Generated: {final_prompt[:200]}...") - - return final_prompt - - - -def _save_temp_image(img_bytes: bytes, suffix=".png") -> str: - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) - tmp.write(img_bytes) - tmp.close() - return tmp.name - -def generate_flux_image_via_api(prompt: str) -> Optional[str]: - if not FLUX_API_ENABLED or not flux_api_client: - logger.error("FLUX API is not enabled or client not initialized") - return None - - try: - logger.info(f"[FLUX] Calling API with prompt length: {len(prompt)}") - - # 빈 이미지 데이터 생성 (필수 파라미터를 위한 더미 데이터) - dummy_image = { - "path": None, - "url": None, - "size": None, - "orig_name": None, - "mime_type": None, - "is_stream": False, - "meta": {} - } - - result = flux_api_client.predict( - prompt=prompt, - width=768, - height=768, - guidance=3.5, - inference_steps=8, - seed=random.randint(1, 1_000_000), - do_img2img=False, - init_image=dummy_image, # ← 필수 파라미터 추가 - image2image_strength=0.8, # 기본값 - resize_img=True, # 기본값 - api_name="/generate_image", - ) - - logger.info(f"[FLUX] API response type: {type(result)}") - logger.info(f"[FLUX] API response (first 200 chars): {str(result)[:200]}") - - # 결과가 튜��인 경우 (generated_image, used_seed) - if isinstance(result, tuple) and len(result) >= 1: - generated_image = result[0] - - # generated_image가 dict 형태인 경우 - if isinstance(generated_image, dict) and 'path' in generated_image: - candidate = generated_image['path'] - logger.info(f"[FLUX] Extracted path from result dict: {candidate}") - else: - candidate = generated_image - logger.info(f"[FLUX] Using first element of tuple: {candidate}") - - # 결과가 dict인 경우 - elif isinstance(result, dict): - if 'path' in result: - candidate = result['path'] - elif 'url' in result: - candidate = _save_temp_image(requests.get(result['url']).content) - else: - candidate = None - - # 문자열인 경우 - elif isinstance(result, str): - if os.path.exists(result): - candidate = result - elif result.startswith(("http://", "https://")): - candidate = _save_temp_image(requests.get(result).content) - else: - candidate = None - - else: - candidate = None - logger.error(f"[FLUX] Unexpected result type: {type(result)}") - - # 최종 검증 - if candidate and os.path.exists(candidate): - logger.info(f"[FLUX] ✅ Success! Image saved at: {candidate}") - return candidate - else: - logger.error(f"[FLUX] ❌ Failed to get valid image path. Candidate: {candidate}") - return None - - except Exception as e: - logger.error(f"[FLUX] ❌ Image generation failed: {str(e)}") - import traceback - logger.error(f"[FLUX] Traceback: {traceback.format_exc()}") - 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_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 +import sys +import streamlit as st +from tempfile import NamedTemporaryFile -############################################################################## -# PPT Generation Functions - FIXED WITH LIMITED DIAGRAMS AND IMAGES -############################################################################## -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 +def main(): 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) + # Get the code from secrets + code = os.environ.get("MAIN_CODE") - 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'): + if not code: + st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.") 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 limited visuals (max 6 images + 2 diagrams)""" - 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) - - # 이미지와 다이어그램 카운터 및 추적 - image_count_3d = 0 - image_count_flux = 0 - diagram_count = 0 - max_images_per_api = 3 - max_diagrams = 2 - - # 다이어그램이 필요한 슬라이드를 미리 분석 - diagram_candidates = [] - if include_diagrams: - for i, slide_data in enumerate(slides_data): - title = slide_data.get('title', '') - content = slide_data.get('content', '') - diagram_type, score = detect_diagram_type_with_score(title, content) - if diagram_type and score > 0: - diagram_candidates.append((i, diagram_type, score)) - - # 필요도 점수가 높은 순으로 정렬하고 상위 2개만 선택 - diagram_candidates.sort(key=lambda x: x[2], reverse=True) - diagram_candidates = diagram_candidates[:max_diagrams] - diagram_slide_indices = [x[0] for x in diagram_candidates] - else: - diagram_slide_indices = [] - - # ───────────────────────────────────────────────────────── - # 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 - - # 표지 이미지 (카운트에 포함) - 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 = None - if image_3d and image_count_3d < max_images_per_api: - ai_image_path = image_3d - image_count_3d += 1 - elif image_photo and image_count_flux < max_images_per_api: - ai_image_path = image_photo - image_count_flux += 1 - - 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) - - # ───────────────────────────────────────────────────────── - # 2) 컨텐츠 슬라이드 생성 - # ───────────────────────────────────────────────────────── - for i, slide_data in enumerate(slides_data): - layout_type = slide_data.get('layout', 'title_content') - - 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) + # Create a temporary Python file + with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp: + tmp.write(code) + tmp_path = tmp.name - # Set title - if slide.shapes.title: - slide.shapes.title.text = slide_data.get('title', '제목 없음') - 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) - paragraph.font.color.rgb = RGBColor(255, 255, 255) - paragraph.alignment = PP_ALIGN.CENTER - else: - paragraph.font.size = Pt(24) - 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}") + # Execute the code + exec(compile(code, tmp_path, 'exec'), globals()) - # 슬라이드 정보 - 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 - - # 1. 다이어그램 우선 확인 - if i in diagram_slide_indices and diagram_count < max_diagrams: - should_add_visual = True - diagram_info = next(x for x in diagram_candidates if x[0] == i) - visual_type = ('diagram', diagram_info[1]) - diagram_count += 1 - # 2. 결론 슬라이드는 이미지 추가 (이미지 제한 내에서) - elif is_conclusion_slide and include_flux_images and (image_count_3d + image_count_flux) < (max_images_per_api * 2): - should_add_visual = True - visual_type = ('conclusion_images', None) - # 3. 일반 슬라이드 이미지 (이미지 제한 내에서, 3~4장마다 하나씩) - elif include_flux_images and (image_count_3d + image_count_flux) < (max_images_per_api * 2) and i % 3 == 0: - 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' and DIAGRAM_GENERATORS_AVAILABLE: - # 다이어그램 생성 - logger.info(f"Generating {visual_type[1]} for slide {i+1} (Diagram {diagram_count}/{max_diagrams})") - diagram_json = generate_diagram_json(slide_title, slide_content, visual_type[1]) - - if diagram_json: - diagram_path = generate_diagram_locally(diagram_json, visual_type[1], "png") - 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 - - 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 = f"{visual_type[1]} Diagram" - 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 - - try: - os.unlink(diagram_path) - except: - pass - except Exception as e: - logger.error(f"Failed to add diagram: {e}") - - elif visual_type[0] == 'conclusion_images': - # 결론 슬라이드용 이미지 생성 - logger.info(f"Generating conclusion images for slide {i+1}") - prompt_3d, prompt_photo = generate_conclusion_image_prompts(slide_title, slide_content) - - image_3d = None - image_photo = None - - if image_count_3d < max_images_per_api: - image_3d = generate_ai_image_via_3d_api(prompt_3d) - if image_count_flux < max_images_per_api: - image_photo = generate_flux_image_via_api(prompt_photo) - - # 선택 - selected_image = None - if image_photo and image_count_flux < max_images_per_api: - selected_image = image_photo - image_count_flux += 1 - elif image_3d and image_count_3d < max_images_per_api: - selected_image = image_3d - image_count_3d += 1 - - 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': - # --- FLUX 다이어그램(6-style) & 3D 이미지 생성 --- - logger.info( - f"Generating FLUX diagram-style image for slide {i+1} " - f"(3D {image_count_3d}/{max_images_per_api}, " - f"FLUX {image_count_flux}/{max_images_per_api})" - ) - - # 3D-API용(그림·일러스트) 프롬프트는 그대로 - prompt_3d, _ = generate_diverse_prompt(slide_title, slide_content, i) - - # ⚡ FLUX-API용 프롬프트: 6가지 다이어그램 구도 중 하나 선택 - style_key = pick_flux_style(i) # ← ① 새 함수 사용 - prompt_flux = generate_flux_prompt(slide_title, slide_content, style_key) - - selected_image = None - - # 3D-API 먼저 시도 (짝수 슬라이드 우선이라는 기존 정책 유지) - if (i % 2 == 0) and (image_count_3d < max_images_per_api): - img_3d = generate_ai_image_via_3d_api(prompt_3d) - if img_3d: - selected_image = img_3d - image_count_3d += 1 - - # FLUX-API (다이어그램 스타일) 시도 - if (selected_image is None) and (image_count_flux < max_images_per_api): - img_flux = generate_flux_image_via_api(prompt_flux) - if img_flux: - selected_image = img_flux - image_count_flux += 1 - - # 슬라이드에 삽입 - 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 - except Exception as e: - logger.error(f"Failed to add slide image: {e}") - -# 그림을 실제로 넣었을 때만 삭제 - if visual_added and selected_image and os.path.exists(selected_image): - try: - os.unlink(selected_image) - except Exception as e: - logger.warning(f"Temp image delete failed: {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': - 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 - - 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) - - 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) - - 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), Inches(1.5), Inches(9), Inches(3.5) - ) - - 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 - 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) - 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 - - 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() - - # 이미지 생성 로그 - logger.info(f"Total images generated - 3D: {image_count_3d}, FLUX: {image_count_flux}, Diagrams: {diagram_count}") - - # Add thank you slide - 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) - - add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary']) - - if thank_you_slide.shapes.title: - thank_you_slide.shapes.title.text = "감사합니다" + # Clean up the temporary file 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) - 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}") - - 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) - 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. 노트는 해당 슬라이드의 내용을 발표자가 청중에게 설명하는 구어체 대본으로 작성하세요 -9. 각 글머리 기호 포인트는 구체적이고 명확하게 작성하세요 -10. 다이어그램이나 플로우차트로 표현하기 좋은 내용은 구조화된 방식으로 작성하세요""" - 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}) - -{layout_instructions} - -**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 -9. Make each bullet point specific and clear -10. Structure content that would work well as diagrams or flowcharts""" - - # 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 - - # 최종 응답 로깅 (디버깅용) - logger.info(f"Generated {len(full_response)} characters of content") - logger.debug(f"First 500 chars of response: {full_response[:500]}") - - except requests.exceptions.Timeout: - logger.error("LLM API timeout") - yield "⚠️ Error: Request timed out. Please try again." - except requests.exceptions.RequestException as e: - logger.error(f"LLM API request error: {str(e)}") - yield f"⚠️ Error: Network error - {str(e)}" - except Exception as e: - logger.error(f"LLM API error: {str(e)}") - import traceback - logger.error(f"Traceback: {traceback.format_exc()}") - 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 연결 실패. 포토리얼리스틱 이미지 없이 진행합니다.", "" - - # 다이어그램 생성기 확인 - if include_diagrams and not DIAGRAM_GENERATORS_AVAILABLE: - yield None, "⚠️ 다이어그램 생성기 모듈을 찾을 수 없습니다. 다이어그램 없이 진행합니다.", "" - include_diagrams = False - - # 한글 폰트 확인 - if include_diagrams and not os.path.exists(KOREAN_FONT_PATH): - yield None, f"⚠️ 한글 폰트를 찾을 수 없습니다: {KOREAN_FONT_PATH}\n다이어그램에 한글이 깨질 수 있습니다.", "" - - # 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)) + os.unlink(tmp_path) + except: + pass - 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 (AI_IMAGE_ENABLED or FLUX_API_ENABLED): - visual_features.append("AI 3D 표지 이미지") - if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE: - visual_features.append("다이어그램 (최대 2개)") - if include_flux_images and FLUX_API_ENABLED: - visual_features.append("AI 생성 이미지 (각 API별 최대 3개)") - - 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 (AI_IMAGE_ENABLED or FLUX_API_ENABLED): - success_msg += f"🖼️ AI 생성 표지 이미지 포함\n" - if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE: - success_msg += f"📊 AI 생성 다이어그램 포함 (최대 2개)\n" - if include_flux_images and FLUX_API_ENABLED: - success_msg += f"🎨 AI 생성 슬라이드 이미지 포함 (API별 최대 3개)\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)}") + st.error(f"⚠️ Error loading or executing the application: {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}", "" + st.code(traceback.format_exc()) -############################################################################## -# 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; -} - -/* 반응형 디자인 */ -@media (max-width: 768px) { - .main-container { - padding: 15px; - margin: 10px; - } - - .header-section h1 { - font-size: 2em; - } - - .gr-tab-item { - padding: 10px 15px; - font-size: 1em; - } -} -""" - -with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo: - gr.Markdown( - """ - # 🎯 AI 기반 PPT 자동 생성 시스템 Pro - - 고급 디자인 테마와 레이아웃을 활용한 전문적인 프레젠테이션을 자동으로 생성합니다. - 6가지 다이어그램 타입과 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="필요도가 높은 2개 슬라이드에 다이어그램 자동 생성" - ) - - include_flux_images = gr.Checkbox( - label="🎨 슬라이드 이미지", - value=False, - info="주요 슬라이드에 FLUX 이미지 추가 (API별 최대 3개)" - ) - - 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 생성 - - ### 🎨 개선된 기능 - 최적화된 시각 요소 - - **다이어그램 개수 제한**: 전체 슬라이드 중 필요도가 가장 높은 2개 슬라이드에만 다이어그램 생성 - - **이미지 개수 제한**: 각 API별로 최대 3개씩, 총 6개의 AI 생성 이미지 - - **한글 폰트 지원**: NanumGothic-Regular.ttf를 사용하여 다이어그램의 한글 깨짐 문제 해결 - - **스마트 배치**: 다이어그램과 이미지는 텍스트와 겹치지 않도록 우측에 자동 배치 - - ### 💡 고급 팁 - - **다이어그램 우선순위**: 프로세스, WBS, 개념도 등의 키워드가 있는 슬라이드가 우선 선택됩니다 - - **이미지 분배**: 3D 스타일과 포토리얼리스틱 이미지가 균형있게 분배됩니다 - - **표지 이미지**: 주제에 맞는 프리미엄 표지 이미지가 자동 생성됩니다 - - **한글 지원**: app.py와 같은 경로에 NanumGothic-Regular.ttf 파일을 배치하세요 - """ - ) - - # 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], - ["프로젝트 관리 방법론", 10, False, True, [], "professional", "modern", "consistent", False, False, True, False], - ["머신러닝 프로세스 가이드", 12, False, True, [], "modern", "modern", "consistent", False, False, True, False], - ], - 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__": - # 한글 폰트 확인 - if os.path.exists(KOREAN_FONT_PATH): - logger.info(f"✅ 한글 폰트 파일을 찾았습니다: {KOREAN_FONT_PATH}") - else: - logger.warning(f"⚠️ 한글 폰트 파일을 찾을 수 없습니다: {KOREAN_FONT_PATH}") - logger.warning("다이어그램에서 한글이 깨질 수 있습니다. NanumGothic-Regular.ttf 파일을 app.py와 같은 경로에 배치하세요.") - - # Try to initialize APIs in parallel - with concurrent.futures.ThreadPoolExecutor(max_workers=2) 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)) - - # 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}") - - # 다이어그램 생성기 모듈 상태 확인 - if DIAGRAM_GENERATORS_AVAILABLE: - logger.info("✅ 다이어그램 생성기 모듈이 정상적으로 로드되었습니다") - logger.info("지원되는 다이어그램 타입: Process Flow, Concept Map, WBS, Radial, Synoptic Chart") - else: - logger.warning("⚠️ 다이어그램 생성기 모듈을 찾을 수 없습니다. 다이어그램 기능이 비활성화됩니다.") - - demo.launch() \ No newline at end of file + main() \ No newline at end of file