Spaces:
Running
Running
#!/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 gradio as gr | |
from loguru import logger | |
import pandas as pd | |
import PyPDF2 | |
from PIL import Image | |
from gradio_client import Client | |
import time | |
# python-pptx ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ธ | |
try: | |
from pptx import Presentation | |
from pptx.util import Inches, Pt | |
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR | |
from pptx.dml.color import RGBColor | |
from pptx.enum.shapes import MSO_SHAPE | |
from pptx.chart.data import CategoryChartData | |
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION | |
PPTX_AVAILABLE = True | |
except ImportError: | |
PPTX_AVAILABLE = False | |
logger.warning("python-pptx ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์น๋์ง ์์์ต๋๋ค. pip install python-pptx") | |
############################################################################## | |
# API Configuration | |
############################################################################## | |
FRIENDLI_TOKEN = os.environ.get("FRIENDLI_TOKEN") | |
if not FRIENDLI_TOKEN: | |
raise ValueError("Please set FRIENDLI_TOKEN environment variable") | |
FRIENDLI_MODEL_ID = "dep89a2fld32mcm" | |
FRIENDLI_API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" | |
# SERPHouse API key | |
SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "") | |
if not SERPHOUSE_API_KEY: | |
logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.") | |
############################################################################## | |
# AI Image Generation API Configuration | |
############################################################################## | |
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 ์ด๊ธฐํ""" | |
global AI_IMAGE_ENABLED, ai_image_client | |
try: | |
logger.info("Connecting to AI image generation API...") | |
ai_image_client = Client(AI_IMAGE_API_URL) | |
AI_IMAGE_ENABLED = True | |
logger.info("AI image generation API 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 | |
############################################################################## | |
# 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 'โถ๏ธ' | |
############################################################################## | |
# 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 API | |
############################################################################## | |
def generate_cover_image_prompt(topic: str, slides_data: list) -> str: | |
"""PPT ์ฃผ์ ์ ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ง ์ด๋ฏธ์ง ํ๋กฌํํธ ์์ฑ - ํ๊ธ ์ง์""" | |
# ์ฃผ์ ํค์๋ ์ถ์ถ | |
keywords = [] | |
topic_keywords = extract_keywords(topic, top_k=3) | |
keywords.extend(topic_keywords.split()) | |
# ๊ฐ ์ฌ๋ผ์ด๋ ์ ๋ชฉ์์ ํค์๋ ์ถ์ถ | |
for slide in slides_data[:5]: | |
title = slide.get('title', '') | |
if title: | |
slide_keywords = extract_keywords(title, top_k=2) | |
keywords.extend(slide_keywords.split()) | |
unique_keywords = list(dict.fromkeys(keywords))[:5] | |
# ์ฃผ์ ๋ถ์์ ํตํ ์คํ์ผ ๊ฒฐ์ | |
style = "ํ๋์ ์ด๊ณ ์ ๋ฌธ์ ์ธ" | |
topic_lower = topic.lower() | |
if any(word in topic_lower for word in ['๊ธฐ์ ', 'tech', 'ai', '์ธ๊ณต์ง๋ฅ', 'digital', '๋์งํธ']): | |
style = "๋ฏธ๋์งํฅ์ ์ด๊ณ ํ์ดํ ํฌํ ๋์งํธ ์ํธ" | |
elif any(word in topic_lower for word in ['๋น์ฆ๋์ค', 'business', '๊ฒฝ์', 'management']): | |
style = "์ ๋ฌธ์ ์ด๊ณ ๊ธฐ์ ์ ์ธ ํ๋์ " | |
elif any(word in topic_lower for word in ['๊ต์ก', 'education', 'ํ์ต', 'learning']): | |
style = "๊ต์ก์ ์ด๊ณ ์๊ฐ์ ์ฃผ๋ ๋ฐ์" | |
elif any(word in topic_lower for word in ['ํ๊ฒฝ', 'environment', '์์ฐ', 'nature']): | |
style = "์์ฐ์ ์ด๊ณ ์นํ๊ฒฝ์ ์ธ ๋ น์" | |
elif any(word in topic_lower for word in ['์๋ฃ', 'medical', '๊ฑด๊ฐ', 'health']): | |
style = "์๋ฃ์ ์ด๊ณ ๊ฑด๊ฐํ ๊นจ๋ํ" | |
elif any(word in topic_lower for word in ['๊ธ์ต', 'finance', 'ํฌ์', 'investment']): | |
style = "๊ธ์ต์ ์ด๊ณ ์ ๋ฌธ์ ์ธ ์ ๋ขฐ๊ฐ ์๋" | |
# wbgmsst ์คํ์ผ์ ์ํ ํ๊ธ ํ๋กฌํํธ ๊ตฌ์ฑ | |
prompt = f"wbgmsst, 3D, {' '.join(unique_keywords)}๋ฅผ ์์งํ๋ ์์ด์๋ฉํธ๋ฆญ ์คํ์ผ์ {style} ์ผ๋ฌ์คํธ๋ ์ด์ , ํฐ์ ๋ฐฐ๊ฒฝ, ๊น๋ํ ๋ฏธ๋๋ฉ๋ฆฌ์คํฑ ๊ตฌ์ฑ, ์ ๋ฌธ์ ์ธ ํ๋ ์ ํ ์ด์ ํ์ง, ๊ณ ํ์ง, ์คํ๋์ค ์กฐ๋ช " | |
return prompt | |
def generate_ai_cover_image_via_api(topic: str, slides_data: list) -> Optional[str]: | |
"""API๋ฅผ ํตํด AI ํ์ง ์ด๋ฏธ์ง ์์ฑ(PNG ๋ณํ ํฌํจ)""" | |
if not AI_IMAGE_ENABLED or not ai_image_client: | |
logger.warning("AI image generation API is not available") | |
return None | |
try: | |
# โโ 1. ํ๋กฌํํธ ๋ฐ ํ๋ผ๋ฏธํฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
prompt = generate_cover_image_prompt(topic, slides_data) | |
logger.info(f"Generated image prompt: {prompt}") | |
height, width = 1024.0, 1024.0 | |
steps, scales = 8.0, 3.5 | |
seed = float(random.randint(0, 1_000_000)) | |
logger.info(f"Calling AI image APIโฆ (h={height}, w={width}, steps={steps}, scales={scales}, seed={seed})") | |
result = ai_image_client.predict( | |
height=height, | |
width=width, | |
steps=steps, | |
scales=scales, | |
prompt=prompt, | |
seed=seed, | |
api_name="/process_and_save_image" | |
) | |
logger.info(f"API call successful. Result type: {type(result)}") | |
# โโ 2. ๋ด๋ถ ํฌํผ: WEBP โ PNG ๋ณํ โโโโโโโโโโโโโโโโโโโโโโโโ | |
def _to_png(src_path: str) -> Optional[str]: | |
try: | |
with Image.open(src_path) as im: | |
png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") | |
im.save(png_tmp.name, format="PNG") | |
logger.info(f"Converted {src_path} โ {png_tmp.name}") | |
return png_tmp.name | |
except Exception as e: | |
logger.error(f"PNG ๋ณํ ์คํจ: {e}") | |
return None | |
# โโ 3. ๊ฒฐ๊ณผ ์ฒ๋ฆฌ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
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): | |
ext = os.path.splitext(image_path)[1].lower() | |
if ext == ".png": | |
# ์ด๋ฏธ PNG๋ผ๋ฉด ๊ทธ๋๋ก ๋ณต์ฌ | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: | |
shutil.copy2(image_path, tmp.name) | |
logger.info(f"PNG copied from {image_path} โ {tmp.name}") | |
return tmp.name | |
else: | |
# WEBPยทJPEG ๋ฑ์ PNG๋ก ๋ณํ | |
return _to_png(image_path) | |
logger.error(f"Image path not found or invalid: {result}") | |
return None | |
except Exception as e: | |
logger.error(f"Failed to generate AI image via API: {e}", exc_info=True) | |
return None | |
############################################################################## | |
# PPT Generation Functions - FIXED VERSION | |
############################################################################## | |
def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list: | |
"""Parse LLM response to extract slide content - COMPLETELY FIXED VERSION""" | |
slides = [] | |
# Debug: ์ ์ฒด ์๋ต ํ์ธ | |
logger.info(f"Parsing LLM response, total length: {len(response)}") | |
logger.debug(f"First 500 chars: {response[:500]}") | |
# Try JSON parsing first | |
try: | |
json_match = re.search(r'\[[\s\S]*\]', response) | |
if json_match: | |
slides_data = json.loads(json_match.group()) | |
return slides_data | |
except: | |
pass | |
# Split by slide markers and process each section | |
# ์ฌ๋ผ์ด๋๋ฅผ ๊ตฌ๋ถํ๋ ๋ ๊ฐ๋ ฅํ ์ ๊ท์ | |
slide_pattern = r'(?:^|\n)(?:์ฌ๋ผ์ด๋|Slide)\s*\d+|(?:^|\n)\d+[\.)](?:\s|$)' | |
# ์ฌ๋ผ์ด๋ ์น์ ์ผ๋ก ๋ถํ | |
sections = re.split(slide_pattern, response, flags=re.MULTILINE | re.IGNORECASE) | |
# ์ฒซ ๋ฒ์งธ ๋น ์น์ ์ ๊ฑฐ | |
if sections and not sections[0].strip(): | |
sections = sections[1:] | |
logger.info(f"Found {len(sections)} potential slide sections") | |
for idx, section in enumerate(sections): | |
if not section.strip(): | |
continue | |
logger.debug(f"Processing section {idx}: {section[:100]}...") | |
slide = { | |
'title': '', | |
'content': '', | |
'notes': '', | |
'layout': 'title_content', | |
'chart_data': None | |
} | |
# ์น์ ๋ด์์ ์ ๋ชฉ, ๋ด์ฉ, ๋ ธํธ ์ถ์ถ | |
lines = section.strip().split('\n') | |
current_part = None | |
title_lines = [] | |
content_lines = [] | |
notes_lines = [] | |
for line in lines: | |
line = line.strip() | |
if not line: | |
continue | |
# ์ ๋ชฉ ์น์ ๊ฐ์ง | |
if line.startswith('์ ๋ชฉ:') or line.startswith('Title:'): | |
current_part = 'title' | |
title_text = line.split(':', 1)[1].strip() if ':' in line else '' | |
if title_text: | |
title_lines.append(title_text) | |
# ๋ด์ฉ ์น์ ๊ฐ์ง | |
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 title_lines: | |
title_lines.append(line) | |
elif current_part == 'content': | |
content_lines.append(line) | |
elif current_part == 'notes': | |
notes_lines.append(line) | |
elif not current_part and not title_lines: | |
# ์ฒซ ๋ฒ์งธ ์ค์ ์ ๋ชฉ์ผ๋ก | |
title_lines.append(line) | |
current_part = 'content' # ์ดํ ์ค๋ค์ content๋ก | |
elif not current_part: | |
content_lines.append(line) | |
# ์ฌ๋ผ์ด๋ ๋ฐ์ดํฐ ์ค์ | |
slide['title'] = ' '.join(title_lines).strip() | |
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) | |
slide['title'] = re.sub(r'^(์ ๋ชฉ|Title)\s*[:๏ผ]\s*', '', slide['title'], flags=re.IGNORECASE) | |
# ๋ด์ฉ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ถ๊ฐ | |
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: | |
logger.warning("Primary parsing failed, trying fallback method...") | |
# ๋๋ธ ๋ด๋ผ์ธ์ผ๋ก ๊ตฌ๋ถ | |
sections = response.split('\n\n') | |
for section in sections: | |
lines = section.strip().split('\n') | |
if len(lines) >= 2: # ์ต์ ์ ๋ชฉ๊ณผ ๋ด์ฉ์ด ์์ด์ผ ํจ | |
slide = { | |
'title': lines[0].strip(), | |
'content': '\n'.join(lines[1:]).strip(), | |
'notes': '', | |
'layout': 'title_content', | |
'chart_data': None | |
} | |
# ์ ๋ชฉ ์ ๋ฆฌ | |
slide['title'] = re.sub(r'^(์ฌ๋ผ์ด๋|Slide)\s*\d+\s*[:๏ผ\-]?\s*', '', slide['title'], flags=re.IGNORECASE) | |
if slide['title'] and slide['content']: | |
slides.append(slide) | |
logger.info(f"Total slides parsed: {len(slides)}") | |
return slides | |
def force_font_size(text_frame, font_size_pt: int, theme: Dict): | |
"""Force font size for all paragraphs and runs in a text frame""" | |
if not text_frame: | |
return | |
try: | |
# Ensure paragraphs exist | |
if not hasattr(text_frame, 'paragraphs'): | |
return | |
for paragraph in text_frame.paragraphs: | |
try: | |
# Set paragraph level font | |
if hasattr(paragraph, 'font'): | |
paragraph.font.size = Pt(font_size_pt) | |
paragraph.font.name = theme['fonts']['body'] | |
paragraph.font.color.rgb = theme['colors']['text'] | |
# Set run level font (most important for actual rendering) | |
if hasattr(paragraph, 'runs'): | |
for run in paragraph.runs: | |
run.font.size = Pt(font_size_pt) | |
run.font.name = theme['fonts']['body'] | |
run.font.color.rgb = theme['colors']['text'] | |
# If paragraph has no runs but has text, create a run | |
if paragraph.text and (not hasattr(paragraph, 'runs') or len(paragraph.runs) == 0): | |
# Force creation of runs by modifying text | |
temp_text = paragraph.text | |
paragraph.text = temp_text # This creates runs | |
if hasattr(paragraph, 'runs'): | |
for run in paragraph.runs: | |
run.font.size = Pt(font_size_pt) | |
run.font.name = theme['fonts']['body'] | |
run.font.color.rgb = theme['colors']['text'] | |
except Exception as e: | |
logger.warning(f"Error setting font for paragraph: {e}") | |
continue | |
except Exception as e: | |
logger.warning(f"Error in force_font_size: {e}") | |
def apply_theme_to_slide(slide, theme: Dict, layout_type: str = 'title_content'): | |
"""Apply design theme to a slide with consistent styling""" | |
# Add colored background shape for all slides | |
bg_shape = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(5.625) | |
) | |
bg_shape.fill.solid() | |
# Use lighter background for content slides | |
if layout_type in ['title_content', 'two_content', 'comparison']: | |
# Light background with subtle gradient effect | |
bg_shape.fill.fore_color.rgb = theme['colors']['background'] | |
# Add accent strip at top | |
accent_strip = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(0.5) | |
) | |
accent_strip.fill.solid() | |
accent_strip.fill.fore_color.rgb = theme['colors']['primary'] | |
accent_strip.line.fill.background() | |
# Add bottom accent | |
bottom_strip = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, 0, Inches(5.125), Inches(10), Inches(0.5) | |
) | |
bottom_strip.fill.solid() | |
bottom_strip.fill.fore_color.rgb = theme['colors']['secondary'] | |
bottom_strip.fill.transparency = 0.7 | |
bottom_strip.line.fill.background() | |
else: | |
# Section headers get primary color background | |
bg_shape.fill.fore_color.rgb = theme['colors']['primary'] | |
bg_shape.line.fill.background() | |
# Move background shapes to back | |
slide.shapes._spTree.remove(bg_shape._element) | |
slide.shapes._spTree.insert(2, bg_shape._element) | |
# Apply title formatting if exists | |
if slide.shapes.title: | |
try: | |
title = slide.shapes.title | |
if title.text_frame and title.text_frame.paragraphs: | |
for paragraph in title.text_frame.paragraphs: | |
paragraph.font.name = theme['fonts']['title'] | |
paragraph.font.bold = True | |
# UPDATED: Increased font sizes for better readability | |
if layout_type == 'section_header': | |
paragraph.font.size = Pt(28) # Increased from 20 | |
paragraph.font.color.rgb = RGBColor(255, 255, 255) | |
paragraph.alignment = PP_ALIGN.CENTER | |
else: | |
paragraph.font.size = Pt(24) # Increased from 18 | |
paragraph.font.color.rgb = theme['colors']['primary'] | |
paragraph.alignment = PP_ALIGN.LEFT | |
except Exception as e: | |
logger.warning(f"Title formatting failed: {e}") | |
# Apply content formatting with improved readability | |
# NOTE: Do NOT add emojis here - they will be added in create_advanced_ppt_from_content | |
for shape in slide.shapes: | |
if shape.has_text_frame and shape != slide.shapes.title: | |
try: | |
text_frame = shape.text_frame | |
# Set text frame margins for better spacing | |
text_frame.margin_left = Inches(0.25) | |
text_frame.margin_right = Inches(0.25) | |
text_frame.margin_top = Inches(0.1) | |
text_frame.margin_bottom = Inches(0.1) | |
# Only apply font formatting, no content modification | |
if text_frame.text.strip(): | |
# Use force_font_size helper to ensure font is applied | |
force_font_size(text_frame, 16, theme) # Increased from 12 | |
for paragraph in text_frame.paragraphs: | |
# Add line spacing for better readability | |
paragraph.space_after = Pt(4) # Increased from 3 | |
paragraph.line_spacing = 1.2 # Increased from 1.1 | |
except Exception as e: | |
logger.warning(f"Content formatting failed: {e}") | |
def add_gradient_background(slide, color1: RGBColor, color2: RGBColor): | |
"""Add gradient-like background to slide using shapes""" | |
# Note: python-pptx doesn't directly support gradients in backgrounds, | |
# so we'll create a gradient effect using overlapping shapes | |
left = top = 0 | |
width = Inches(10) | |
height = Inches(5.625) | |
# Add base color rectangle | |
shape1 = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, left, top, width, height | |
) | |
shape1.fill.solid() | |
shape1.fill.fore_color.rgb = color1 | |
shape1.line.fill.background() | |
# Add semi-transparent overlay for gradient effect | |
shape2 = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, left, top, width, Inches(2.8) | |
) | |
shape2.fill.solid() | |
shape2.fill.fore_color.rgb = color2 | |
shape2.fill.transparency = 0.5 | |
shape2.line.fill.background() | |
# Move shapes to back | |
slide.shapes._spTree.remove(shape1._element) | |
slide.shapes._spTree.remove(shape2._element) | |
slide.shapes._spTree.insert(2, shape1._element) | |
slide.shapes._spTree.insert(3, shape2._element) | |
def add_decorative_shapes(slide, theme: Dict): | |
"""Add decorative shapes to enhance visual appeal""" | |
try: | |
# Add corner accent circle | |
shape1 = slide.shapes.add_shape( | |
MSO_SHAPE.OVAL, | |
Inches(9.3), Inches(4.8), | |
Inches(0.7), Inches(0.7) | |
) | |
shape1.fill.solid() | |
shape1.fill.fore_color.rgb = theme['colors']['accent'] | |
shape1.fill.transparency = 0.3 | |
shape1.line.fill.background() | |
# Add smaller accent | |
shape2 = slide.shapes.add_shape( | |
MSO_SHAPE.OVAL, | |
Inches(0.1), Inches(0.1), | |
Inches(0.4), Inches(0.4) | |
) | |
shape2.fill.solid() | |
shape2.fill.fore_color.rgb = theme['colors']['secondary'] | |
shape2.fill.transparency = 0.5 | |
shape2.line.fill.background() | |
except Exception as e: | |
logger.warning(f"Failed to add decorative shapes: {e}") | |
def create_chart_slide(slide, chart_data: Dict, theme: Dict): | |
"""Create a chart on the slide based on data""" | |
try: | |
# Add chart | |
x, y, cx, cy = Inches(1), Inches(2), Inches(8), Inches(4.5) | |
# Prepare chart data | |
chart_data_obj = CategoryChartData() | |
# Simple bar chart example | |
if 'columns' in chart_data and 'sample_data' in chart_data: | |
# Use first numeric column for chart | |
numeric_cols = [] | |
for col in chart_data['columns']: | |
try: | |
# Check if column has numeric data | |
float(chart_data['sample_data'][0].get(col, 0)) | |
numeric_cols.append(col) | |
except: | |
pass | |
if numeric_cols: | |
categories = [str(row.get(chart_data['columns'][0], '')) | |
for row in chart_data['sample_data'][:5]] | |
chart_data_obj.categories = categories | |
for col in numeric_cols[:3]: # Max 3 series | |
values = [float(row.get(col, 0)) | |
for row in chart_data['sample_data'][:5]] | |
chart_data_obj.add_series(col, values) | |
chart = slide.shapes.add_chart( | |
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data_obj | |
).chart | |
# Style the chart | |
chart.has_legend = True | |
chart.legend.position = XL_LEGEND_POSITION.BOTTOM | |
except Exception as e: | |
logger.warning(f"Chart creation failed: {e}") | |
# If chart fails, add a text placeholder instead | |
textbox = slide.shapes.add_textbox(x, y, cx, cy) | |
text_frame = textbox.text_frame | |
text_frame.text = "๋ฐ์ดํฐ ์ฐจํธ (์ฐจํธ ์์ฑ ์คํจ)" | |
text_frame.paragraphs[0].font.size = Pt(16) # Increased font size | |
text_frame.paragraphs[0].font.color.rgb = theme['colors']['secondary'] | |
def create_advanced_ppt_from_content( | |
slides_data: list, | |
topic: str, | |
theme_name: str, | |
include_charts: bool = False, | |
include_ai_image: bool = False | |
) -> str: | |
"""Create advanced PPT file with consistent visual design and AI image option""" | |
if not PPTX_AVAILABLE: | |
raise ImportError("python-pptx library is required") | |
prs = Presentation() | |
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES['professional']) | |
# Set slide size (16:9) | |
prs.slide_width = Inches(10) | |
prs.slide_height = Inches(5.625) | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 1) ์ ๋ชฉ ์ฌ๋ผ์ด๋(ํ์ง) ์์ฑ | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
title_slide_layout = prs.slide_layouts[0] | |
slide = prs.slides.add_slide(title_slide_layout) | |
# ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋์ธํธ | |
add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary']) | |
# AI ์ด๋ฏธ์ง(ํ์ง ๊ทธ๋ฆผ) | |
title_top_position = Inches(2.0) | |
if include_ai_image and AI_IMAGE_ENABLED: | |
logger.info("Generating AI cover image via API...") | |
ai_image_path = generate_ai_cover_image_via_api(topic, slides_data) | |
if ai_image_path and os.path.exists(ai_image_path): | |
img = Image.open(ai_image_path) | |
img_width, img_height = img.size | |
# ์ด๋ฏธ์ง ๋ ํฌ๊ฒ (60% ํญ, ์ต๋ ๋์ด 4") | |
max_width = Inches(6) | |
ratio = img_height / img_width | |
img_w = max_width | |
img_h = max_width * ratio | |
max_height = Inches(4) | |
if img_h > max_height: | |
img_h = max_height | |
img_w = max_height / ratio | |
left = (prs.slide_width - img_w) / 2 | |
top = Inches(0.6) | |
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 | |
title_top_position = top + img_h + Inches(0.35) | |
try: | |
os.unlink(ai_image_path) | |
except Exception as e: | |
logger.warning(f"Temp image delete failed: {e}") | |
# ์ ๋ชฉ / ๋ถ์ ๋ชฉ ํ ์คํธ | |
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 = int(title_top_position) | |
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(32) | |
p.font.bold = True | |
p.font.color.rgb = RGBColor(255, 255, 255) | |
p.alignment = PP_ALIGN.CENTER | |
# ๊ฐ๋ก ๋ฐฉํฅ ๊ณ ์ | |
bodyPr = tf._txBody.bodyPr | |
bodyPr.set('vert', 'horz') | |
# ์ต์์ ๋ ์ด์ด๋ก ์ด๋ | |
slide.shapes._spTree.remove(title_shape._element) | |
slide.shapes._spTree.append(title_shape._element) | |
if subtitle_shape: | |
subtitle_shape.left = Inches(0.5) | |
subtitle_shape.width = prs.slide_width - Inches(1) | |
subtitle_shape.top = int(title_top_position + Inches(1.0)) | |
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(18) | |
p2.font.color.rgb = RGBColor(255, 255, 255) | |
p2.alignment = PP_ALIGN.CENTER | |
bodyPr2 = tf2._txBody.bodyPr | |
bodyPr2.set('vert', 'horz') | |
slide.shapes._spTree.remove(subtitle_shape._element) | |
slide.shapes._spTree.append(subtitle_shape._element) | |
# ์ฅ์ ์์ | |
add_decorative_shapes(slide, theme) | |
# Add content slides with consistent design | |
for i, slide_data in enumerate(slides_data): | |
layout_type = slide_data.get('layout', 'title_content') | |
# Log slide creation | |
logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}") | |
logger.debug(f"Content length: {len(slide_data.get('content', ''))}") | |
# Choose appropriate layout | |
if layout_type == 'section_header' and len(prs.slide_layouts) > 2: | |
slide_layout = prs.slide_layouts[2] | |
elif layout_type == 'two_content' and len(prs.slide_layouts) > 3: | |
slide_layout = prs.slide_layouts[3] | |
elif layout_type == 'comparison' and len(prs.slide_layouts) > 4: | |
slide_layout = prs.slide_layouts[4] | |
else: | |
slide_layout = prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0] | |
slide = prs.slides.add_slide(slide_layout) | |
# Apply theme to EVERY slide for consistency | |
apply_theme_to_slide(slide, theme, layout_type) | |
# Set title | |
if slide.shapes.title: | |
slide.shapes.title.text = slide_data.get('title', '์ ๋ชฉ ์์') | |
# IMMEDIATELY set title font size after setting text | |
try: | |
title_text_frame = slide.shapes.title.text_frame | |
if title_text_frame and title_text_frame.paragraphs: | |
for paragraph in title_text_frame.paragraphs: | |
if layout_type == 'section_header': | |
paragraph.font.size = Pt(28) # Increased from 20 | |
paragraph.font.color.rgb = RGBColor(255, 255, 255) | |
paragraph.alignment = PP_ALIGN.CENTER | |
else: | |
paragraph.font.size = Pt(24) # Increased from 18 | |
paragraph.font.color.rgb = theme['colors']['primary'] | |
paragraph.font.bold = True | |
paragraph.font.name = theme['fonts']['title'] | |
except Exception as e: | |
logger.warning(f"Title font sizing failed: {e}") | |
# Add content based on layout - COMPLETELY FIXED VERSION | |
if layout_type == 'section_header': | |
# Section header content handling | |
content = slide_data.get('content', '') | |
if content: | |
logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...") | |
textbox = slide.shapes.add_textbox( | |
Inches(1), Inches(3.5), Inches(8), Inches(1.5) | |
) | |
tf = textbox.text_frame | |
tf.clear() | |
tf.text = content | |
tf.word_wrap = True | |
for paragraph in tf.paragraphs: | |
paragraph.font.name = theme['fonts']['body'] | |
paragraph.font.size = Pt(16) | |
paragraph.font.color.rgb = RGBColor(255, 255, 255) | |
paragraph.alignment = PP_ALIGN.CENTER | |
# Add decorative line | |
line = slide.shapes.add_shape( | |
MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4) | |
) | |
line.fill.solid() | |
line.fill.fore_color.rgb = RGBColor(255, 255, 255) | |
line.line.fill.background() | |
elif layout_type == 'two_content': | |
content = slide_data.get('content', '') | |
if content: | |
logger.info(f"Creating two-column layout for slide {i+1}") | |
content_lines = content.split('\n') | |
mid_point = len(content_lines) // 2 | |
# Left column | |
left_box = slide.shapes.add_textbox( | |
Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5) | |
) | |
left_tf = left_box.text_frame | |
left_tf.clear() | |
left_content = '\n'.join(content_lines[:mid_point]) | |
if left_content: | |
left_tf.text = left_content | |
left_tf.word_wrap = True | |
force_font_size(left_tf, 14, theme) | |
# Apply emoji bullets | |
for paragraph in left_tf.paragraphs: | |
text = paragraph.text.strip() | |
if text and text.startswith(('-', 'โข', 'โ')) and not has_emoji(text): | |
clean_text = text.lstrip('-โขโ ') | |
emoji = get_emoji_for_content(clean_text) | |
paragraph.text = f"{emoji} {clean_text}" | |
force_font_size(left_tf, 14, theme) | |
# Right column | |
right_box = slide.shapes.add_textbox( | |
Inches(5), Inches(1.5), Inches(4.5), Inches(3.5) | |
) | |
right_tf = right_box.text_frame | |
right_tf.clear() | |
right_content = '\n'.join(content_lines[mid_point:]) | |
if right_content: | |
right_tf.text = right_content | |
right_tf.word_wrap = True | |
force_font_size(right_tf, 14, theme) | |
# Apply emoji bullets | |
for paragraph in right_tf.paragraphs: | |
text = paragraph.text.strip() | |
if text and text.startswith(('-', 'โข', 'โ')) and not has_emoji(text): | |
clean_text = text.lstrip('-โขโ ') | |
emoji = get_emoji_for_content(clean_text) | |
paragraph.text = f"{emoji} {clean_text}" | |
force_font_size(right_tf, 14, theme) | |
else: | |
# Regular content - MOST IMPORTANT PART - COMPLETELY FIXED | |
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) | |
# content๊ฐ ์์ผ๋ฉด ๋ฌด์กฐ๊ฑด ํ ์คํธ๋ฐ์ค ์ถ๊ฐ | |
if content and content.strip(): | |
# ๋ช ํํ ์์น์ ํฌ๊ธฐ๋ก ํ ์คํธ๋ฐ์ค ์์ฑ | |
textbox = slide.shapes.add_textbox( | |
Inches(0.5), # left | |
Inches(1.5), # top | |
Inches(9), # width | |
Inches(3.5) # height | |
) | |
tf = textbox.text_frame | |
tf.clear() | |
# ํ ์คํธ ์ค์ | |
tf.text = content.strip() | |
tf.word_wrap = True | |
# ํ ์คํธ ํ๋ ์ ์ฌ๋ฐฑ ์ค์ | |
tf.margin_left = Inches(0.1) | |
tf.margin_right = Inches(0.1) | |
tf.margin_top = Inches(0.05) | |
tf.margin_bottom = Inches(0.05) | |
# ํฐํธ ๊ฐ์ ์ ์ฉ | |
force_font_size(tf, 16, theme) | |
# ๊ฐ ๋จ๋ฝ์ ๋ํด ์ฒ๋ฆฌ | |
for p_idx, paragraph in enumerate(tf.paragraphs): | |
if paragraph.text.strip(): | |
# ์ด๋ชจ์ง ์ถ๊ฐ | |
text = paragraph.text.strip() | |
if text.startswith(('-', 'โข', 'โ')) and not has_emoji(text): | |
clean_text = text.lstrip('-โขโ ') | |
emoji = get_emoji_for_content(clean_text) | |
paragraph.text = f"{emoji} {clean_text}" | |
# ํฐํธ ์ฌ์ ์ฉ - ๊ฐ run์ ๋ํด ๋ช ์์ ์ผ๋ก ์ค์ | |
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: | |
# runs๊ฐ ์์ผ๋ฉด ์์ฑ | |
paragraph.font.size = Pt(16) | |
paragraph.font.name = theme['fonts']['body'] | |
paragraph.font.color.rgb = theme['colors']['text'] | |
# ๋จ๋ฝ ๊ฐ๊ฒฉ | |
paragraph.space_before = Pt(6) | |
paragraph.space_after = Pt(6) | |
paragraph.line_spacing = 1.3 | |
logger.info(f"Successfully added content to slide {i+1}") | |
else: | |
logger.warning(f"Slide {i+1} has no content or empty content") | |
# Add slide notes if available | |
if slide_data.get('notes'): | |
try: | |
notes_slide = slide.notes_slide | |
notes_text_frame = notes_slide.notes_text_frame | |
notes_text_frame.text = slide_data.get('notes', '') | |
except Exception as e: | |
logger.warning(f"Failed to add slide notes: {e}") | |
# Add slide number with better design | |
slide_number_bg = slide.shapes.add_shape( | |
MSO_SHAPE.ROUNDED_RECTANGLE, | |
Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5) | |
) | |
slide_number_bg.fill.solid() | |
slide_number_bg.fill.fore_color.rgb = theme['colors']['primary'] | |
slide_number_bg.fill.transparency = 0.8 | |
slide_number_bg.line.fill.background() | |
slide_number_box = slide.shapes.add_textbox( | |
Inches(8.3), Inches(5.05), Inches(1.5), Inches(0.4) | |
) | |
slide_number_frame = slide_number_box.text_frame | |
slide_number_frame.text = f"{i + 1} / {len(slides_data)}" | |
slide_number_frame.paragraphs[0].font.size = Pt(10) # Increased from 8 | |
slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) | |
slide_number_frame.paragraphs[0].font.bold = False | |
slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER | |
# Add subtle design element on alternating slides | |
if i % 2 == 0: | |
accent_shape = slide.shapes.add_shape( | |
MSO_SHAPE.OVAL, | |
Inches(9.6), Inches(0.1), | |
Inches(0.2), Inches(0.2) | |
) | |
accent_shape.fill.solid() | |
accent_shape.fill.fore_color.rgb = theme['colors']['accent'] | |
accent_shape.line.fill.background() | |
# Add thank you slide with consistent design | |
thank_you_layout = prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[0] | |
thank_you_slide = prs.slides.add_slide(thank_you_layout) | |
# Apply gradient background | |
add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary']) | |
if thank_you_slide.shapes.title: | |
thank_you_slide.shapes.title.text = "๊ฐ์ฌํฉ๋๋ค" | |
try: | |
if thank_you_slide.shapes.title.text_frame and thank_you_slide.shapes.title.text_frame.paragraphs: | |
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36) # Increased from 28 | |
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.bold = True | |
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) | |
thank_you_slide.shapes.title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER | |
except Exception as e: | |
logger.warning(f"Thank you slide styling failed: {e}") | |
# Add contact or additional info placeholder | |
info_box = thank_you_slide.shapes.add_textbox( | |
Inches(2), Inches(3.5), Inches(6), Inches(1) | |
) | |
info_tf = info_box.text_frame | |
info_tf.text = "AI๋ก ์์ฑ๋ ํ๋ ์ ํ ์ด์ " | |
info_tf.paragraphs[0].font.size = Pt(18) # Increased from 14 | |
info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) | |
info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER | |
# Save to temporary file | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp_file: | |
prs.save(tmp_file.name) | |
return tmp_file.name | |
############################################################################## | |
# Streaming Response Handler for PPT Generation - IMPROVED VERSION | |
############################################################################## | |
def generate_ppt_content(topic: str, num_slides: int, additional_context: str, use_korean: bool = False, layout_style: str = "consistent") -> Iterator[str]: | |
"""Generate PPT content using LLM with clearer format""" | |
# Layout instructions based on style | |
layout_instructions = "" | |
if layout_style == "varied": | |
layout_instructions = """ | |
์ฌ๋ผ์ด๋ ๋ ์ด์์์ ๋ค์ํ๊ฒ ๊ตฌ์ฑํด์ฃผ์ธ์: | |
- ๋งค 5๋ฒ์งธ ์ฌ๋ผ์ด๋๋ '์น์ ๊ตฌ๋ถ' ์ฌ๋ผ์ด๋๋ก ๋ง๋ค์ด์ฃผ์ธ์ | |
- ๋น๊ต๋ ๋์กฐ ๋ด์ฉ์ด ์์ผ๋ฉด '๋น๊ต' ๋ ์ด์์์ ์ฌ์ฉํ์ธ์ | |
- ๋ด์ฉ์ด ๋ง์ผ๋ฉด 2๋จ ๊ตฌ์ฑ์ ๊ณ ๋ คํ์ธ์ | |
""" | |
elif layout_style == "consistent": | |
layout_instructions = """ | |
์ผ๊ด๋ ๋ ์ด์์์ ์ ์งํด์ฃผ์ธ์: | |
- ๋ชจ๋ ์ฌ๋ผ์ด๋๋ ๋์ผํ ๊ตฌ์กฐ๋ก ์์ฑ | |
- ์ ๋ชฉ๊ณผ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ํ์ ํต์ผ | |
- ๊ฐ๊ฒฐํ๊ณ ๋ช ํํ ๊ตฌ์ฑ | |
""" | |
# ๋ ๋ช ํํ ์์คํ ํ๋กฌํํธ | |
if use_korean: | |
system_prompt = f"""๋น์ ์ ์ ๋ฌธ์ ์ธ PPT ํ๋ ์ ํ ์ด์ ์์ฑ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
์ฃผ์ด์ง ์ฃผ์ ์ ๋ํด ์ ํํ {num_slides}์ฅ์ ์ฌ๋ผ์ด๋ ๋ด์ฉ์ ์์ฑํด์ฃผ์ธ์. | |
**๋ฐ๋์ ์๋ ํ์์ ์ ํํ ๋ฐ๋ผ์ฃผ์ธ์:** | |
์ฌ๋ผ์ด๋ 1 | |
์ ๋ชฉ: [์ฌ๋ผ์ด๋ ์ ๋ชฉ - "์ฌ๋ผ์ด๋ 1" ๊ฐ์ ๋ฒํธ๋ ํฌํจํ์ง ๋ง์ธ์] | |
๋ด์ฉ: | |
- ์ฒซ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ์ธ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ค ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ค์ฏ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
๋ ธํธ: [๋ฐํ์๊ฐ ์ด ์ฌ๋ผ์ด๋๋ฅผ ์ค๋ช ํ ๋ ์ฌ์ฉํ ๊ตฌ์ด์ฒด ์คํฌ๋ฆฝํธ] | |
์ฌ๋ผ์ด๋ 2 | |
์ ๋ชฉ: [์ฌ๋ผ์ด๋ ์ ๋ชฉ] | |
๋ด์ฉ: | |
- ์ฒซ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ์ธ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ค ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
- ๋ค์ฏ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ | |
๋ ธํธ: [๋ฐํ ์คํฌ๋ฆฝํธ] | |
(์ด๋ฐ ์์ผ๋ก ์ฌ๋ผ์ด๋ {num_slides}๊น์ง ๊ณ์) | |
{layout_instructions} | |
**์ค์ ์ง์นจ:** | |
1. ๊ฐ ์ฌ๋ผ์ด๋๋ "์ฌ๋ผ์ด๋ ์ซ์"๋ก ์์ | |
2. ์ ๋ชฉ: ๋ค์ ์ค์ ์ ๋ชฉ ์์ฑ (๋ฒํธ ์ ์ธ) | |
3. ๋ด์ฉ: ๋ค์ ์ ํํ 5๊ฐ์ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ํฌ์ธํธ | |
4. ๋ ธํธ: ๋ค์ ๋ฐํ ์คํฌ๋ฆฝํธ | |
5. ๊ฐ ์น์ ์ฌ์ด์ ๋น ์ค ์์ | |
6. ์ด {num_slides}์ฅ ์์ฑ | |
7. ๊ฐ ํฌ์ธํธ๋ '-' ๊ธฐํธ๋ก ์์ํ์ธ์ (์ด๋ชจ์ง๋ ์๋์ผ๋ก ์ถ๊ฐ๋ฉ๋๋ค) | |
8. ๋ ธํธ๋ ํด๋น ์ฌ๋ผ์ด๋์ ๋ด์ฉ์ ๋ฐํ์๊ฐ ์ฒญ์ค์๊ฒ ์ค๋ช ํ๋ ๊ตฌ์ด์ฒด ๋๋ณธ์ผ๋ก ์์ฑํ์ธ์""" | |
else: | |
system_prompt = f"""You are a professional PPT presentation expert. | |
Create content for exactly {num_slides} slides on the given topic. | |
**You MUST follow this exact format:** | |
Slide 1 | |
Title: [Slide title - do NOT include "Slide 1" in the title] | |
Content: | |
- First key point | |
- Second key point | |
- Third key point | |
- Fourth key point | |
- Fifth key point | |
Notes: [Speaker script in conversational style for explaining this slide] | |
Slide 2 | |
Title: [Slide title] | |
Content: | |
- First key point | |
- Second key point | |
- Third key point | |
- Fourth key point | |
- Fifth key point | |
Notes: [Speaker script] | |
(Continue this way until Slide {num_slides}) | |
**Important instructions:** | |
1. Each slide starts with "Slide number" | |
2. Title: followed by the actual title (no numbers) | |
3. Content: followed by exactly 5 bullet points | |
4. Notes: followed by speaker script | |
5. No empty lines between sections | |
6. Create exactly {num_slides} slides | |
7. Start each point with '-' (emojis will be added automatically) | |
8. Notes should be a speaker script explaining the slide content in conversational style""" | |
# Add search results if web search is performed | |
if additional_context: | |
system_prompt += f"\n\n์ฐธ๊ณ ์ ๋ณด:\n{additional_context}" | |
# Prepare messages | |
user_prompt = f"์ฃผ์ : {topic}\n\n์์์ ์ค๋ช ํ ํ์์ ๋ง์ถฐ ์ ํํ {num_slides}์ฅ์ PPT ์ฌ๋ผ์ด๋ ๋ด์ฉ์ ์์ฑํด์ฃผ์ธ์. ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ํฌ์ธํธ์ ํจ๊ป, ๋ฐํ์๊ฐ ์ฒญ์ค์๊ฒ ํด๋น ๋ด์ฉ์ ์ค๋ช ํ๋ ๊ตฌ์ด์ฒด ๋ฐํ ๋๋ณธ์ ๋ ธํธ๋ก ์์ฑํด์ฃผ์ธ์." | |
if not use_korean: | |
user_prompt = f"Topic: {topic}\n\nPlease create exactly {num_slides} PPT slides following the format described above. Include exactly 5 key points per slide, and write speaker notes as a conversational script explaining the content to the audience." | |
messages = [ | |
{"role": "system", "content": system_prompt}, | |
{"role": "user", "content": user_prompt} | |
] | |
# Call LLM API | |
headers = { | |
"Authorization": f"Bearer {FRIENDLI_TOKEN}", | |
"Content-Type": "application/json" | |
} | |
payload = { | |
"model": FRIENDLI_MODEL_ID, | |
"messages": messages, | |
"max_tokens": min(4000, num_slides * 300), # More tokens for 5 points + notes | |
"top_p": 0.9, | |
"temperature": 0.8, | |
"stream": True, | |
"stream_options": { | |
"include_usage": True | |
} | |
} | |
try: | |
response = requests.post( | |
FRIENDLI_API_URL, | |
headers=headers, | |
json=payload, | |
stream=True, | |
timeout=60 | |
) | |
response.raise_for_status() | |
full_response = "" | |
for line in response.iter_lines(): | |
if line: | |
line_text = line.decode('utf-8') | |
if line_text.startswith("data: "): | |
data_str = line_text[6:] | |
if data_str == "[DONE]": | |
break | |
try: | |
data = json.loads(data_str) | |
if "choices" in data and len(data["choices"]) > 0: | |
delta = data["choices"][0].get("delta", {}) | |
content = delta.get("content", "") | |
if content: | |
full_response += content | |
yield full_response | |
except json.JSONDecodeError: | |
logger.warning(f"JSON parsing failed: {data_str}") | |
continue | |
except Exception as e: | |
logger.error(f"LLM API error: {str(e)}") | |
yield f"โ ๏ธ Error generating content: {str(e)}" | |
############################################################################## | |
# Main PPT Generation Function - IMPROVED VERSION with API Image | |
############################################################################## | |
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 | |
) -> tuple: | |
"""Main function to generate PPT with advanced design and API-based AI image""" | |
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: | |
# AI ์ด๋ฏธ์ง ์์ฑ์ด ์์ฒญ๋์์ง๋ง ์ด๊ธฐํ๋์ง ์์ ๊ฒฝ์ฐ | |
if include_ai_image and not AI_IMAGE_ENABLED: | |
yield None, "๐ AI ์ด๋ฏธ์ง ์์ฑ API์ ์ฐ๊ฒฐํ๋ ์ค...", "" | |
if initialize_ai_image_api(): | |
yield None, "โ AI ์ด๋ฏธ์ง API ์ฐ๊ฒฐ ์ฑ๊ณต!", "" | |
else: | |
include_ai_image = False | |
yield None, "โ ๏ธ AI ์ด๋ฏธ์ง API ์ฐ๊ฒฐ ์คํจ. AI ์ด๋ฏธ์ง ์์ด ์งํํฉ๋๋ค.", "" | |
# Process reference files if provided | |
additional_context = "" | |
chart_data = None | |
if reference_files: | |
file_contents = [] | |
for file_path in reference_files: | |
if file_path.lower().endswith(".csv"): | |
csv_content = analyze_csv_file(file_path) | |
file_contents.append(csv_content) | |
# Extract chart data if available | |
if "CHART_DATA:" in csv_content: | |
chart_json = csv_content.split("CHART_DATA:")[1] | |
try: | |
chart_data = json.loads(chart_json) | |
except: | |
pass | |
elif file_path.lower().endswith(".txt"): | |
file_contents.append(analyze_txt_file(file_path)) | |
elif file_path.lower().endswith(".pdf"): | |
file_contents.append(pdf_to_markdown(file_path)) | |
if file_contents: | |
additional_context = "\n\n".join(file_contents) | |
# Web search if enabled | |
if use_web_search: | |
search_query = extract_keywords(topic, top_k=5) | |
search_results = do_web_search(search_query, use_korean=use_korean) | |
if not search_results.startswith("Web search"): | |
additional_context += f"\n\n{search_results}" | |
# Generate PPT content | |
llm_response = "" | |
for response in generate_ppt_content(topic, num_slides, additional_context, use_korean, layout_style): | |
llm_response = response | |
yield None, f"๐ ์์ฑ ์ค...\n\n{response}", response | |
# Parse LLM response | |
slides_data = parse_llm_ppt_response(llm_response, layout_style) | |
# Debug: ํ์ฑ๋ ๊ฐ ์ฌ๋ผ์ด๋ ๋ด์ฉ ์ถ๋ ฅ | |
logger.info(f"=== Parsed Slides Debug Info ===") | |
for i, slide in enumerate(slides_data): | |
logger.info(f"Slide {i+1}:") | |
logger.info(f" Title: {slide.get('title', 'NO TITLE')}") | |
logger.info(f" Content: {slide.get('content', 'NO CONTENT')[:100]}...") | |
logger.info(f" Content Length: {len(slide.get('content', ''))}") | |
logger.info("---") | |
# Add chart data to relevant slides if available | |
if chart_data and include_charts: | |
for slide in slides_data: | |
if '๋ฐ์ดํฐ' in slide.get('title', '') or 'data' in slide.get('title', '').lower(): | |
slide['chart_data'] = chart_data | |
break | |
# Debug logging | |
logger.info(f"Parsed {len(slides_data)} slides from LLM response") | |
logger.info(f"Design theme: {design_theme}, Layout style: {layout_style}") | |
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 ์ด๋ฏธ์ง ์์ฑ ์๋ฆผ | |
if include_ai_image and AI_IMAGE_ENABLED: | |
yield None, f"๐ ์ฌ๋ผ์ด๋ ์์ฑ ์๋ฃ!\n\n๐จ AI 3D ํ์ง ์ด๋ฏธ์ง ์์ฑ ์ค... (30์ด ์ ๋ ์์)", 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) | |
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: | |
success_msg += f"๐ผ๏ธ AI ์์ฑ 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง ํฌํจ\n" | |
success_msg += f"๐ ์์ฑ๋ ์ฌ๋ผ์ด๋:\n" | |
for i, slide in enumerate(slides_data[:5]): # Show first 5 slides | |
success_msg += f" {i+1}. {slide.get('title', '์ ๋ชฉ ์์')} [{slide.get('layout', 'standard')}]\n" | |
if slide.get('notes'): | |
success_msg += f" ๐ก ๋ ธํธ: {slide.get('notes', '')[:50]}...\n" | |
if len(slides_data) > 5: | |
success_msg += f" ... ์ธ {len(slides_data) - 5}์ฅ" | |
yield ppt_path, success_msg, llm_response | |
except Exception as e: | |
logger.error(f"PPT generation error: {str(e)}") | |
import traceback | |
error_details = traceback.format_exc() | |
logger.error(f"Error details: {error_details}") | |
yield None, f"โ PPT ์์ฑ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}\n\n์์ธ ์ค๋ฅ:\n{error_details}", "" | |
############################################################################## | |
# Gradio UI | |
############################################################################## | |
css = """ | |
/* Full width UI */ | |
.gradio-container { | |
background: rgba(255, 255, 255, 0.98); | |
padding: 40px 50px; | |
margin: 30px auto; | |
width: 100% !important; | |
max-width: 1400px !important; | |
border-radius: 20px; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
} | |
/* Background */ | |
body { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; | |
} | |
/* Title styling */ | |
h1 { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
background-clip: text; | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
font-weight: 700; | |
margin-bottom: 10px; | |
} | |
/* Button styles */ | |
button.primary { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
border: none; | |
color: white !important; | |
font-weight: 600; | |
padding: 15px 30px !important; | |
font-size: 18px !important; | |
transition: all 0.3s ease; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
} | |
button.primary:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); | |
} | |
/* Input styles */ | |
.textbox, textarea, input[type="text"], input[type="number"] { | |
border: 2px solid #e5e7eb; | |
border-radius: 12px; | |
padding: 15px; | |
font-size: 16px; | |
transition: all 0.3s ease; | |
background: white; | |
} | |
.textbox:focus, textarea:focus, input[type="text"]:focus { | |
border-color: #667eea; | |
outline: none; | |
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
} | |
/* Card style */ | |
.card { | |
background: white; | |
border-radius: 16px; | |
padding: 25px; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); | |
margin-bottom: 25px; | |
border: 1px solid rgba(102, 126, 234, 0.1); | |
} | |
/* Dropdown styles */ | |
.dropdown { | |
border: 2px solid #e5e7eb; | |
border-radius: 12px; | |
padding: 12px; | |
background: white; | |
transition: all 0.3s ease; | |
} | |
.dropdown:hover { | |
border-color: #667eea; | |
} | |
/* Slider styles */ | |
.gr-slider input[type="range"] { | |
background: linear-gradient(to right, #667eea 0%, #764ba2 100%); | |
height: 8px; | |
border-radius: 4px; | |
} | |
/* File upload area */ | |
.file-upload { | |
border: 3px dashed #667eea; | |
border-radius: 16px; | |
padding: 40px; | |
text-align: center; | |
transition: all 0.3s ease; | |
background: rgba(102, 126, 234, 0.02); | |
} | |
.file-upload:hover { | |
border-color: #764ba2; | |
background: rgba(102, 126, 234, 0.05); | |
transform: scale(1.01); | |
} | |
/* Checkbox styles */ | |
input[type="checkbox"] { | |
width: 20px; | |
height: 20px; | |
margin-right: 10px; | |
cursor: pointer; | |
} | |
/* Tab styles */ | |
.tabs { | |
border-radius: 12px; | |
overflow: hidden; | |
margin-bottom: 20px; | |
} | |
.tab-nav { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
padding: 5px; | |
} | |
.tab-nav button { | |
background: transparent; | |
color: white; | |
border: none; | |
padding: 10px 20px; | |
margin: 0 5px; | |
border-radius: 8px; | |
transition: all 0.3s ease; | |
} | |
.tab-nav button.selected { | |
background: white; | |
color: #667eea; | |
} | |
/* Section headers */ | |
.section-header { | |
font-size: 20px; | |
font-weight: 600; | |
color: #667eea; | |
margin: 20px 0 15px 0; | |
padding-bottom: 10px; | |
border-bottom: 2px solid rgba(102, 126, 234, 0.2); | |
} | |
/* Status box styling */ | |
.status-box { | |
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); | |
border-radius: 12px; | |
padding: 20px; | |
} | |
/* Preview box styling */ | |
.preview-box { | |
background: #f8f9fa; | |
border-radius: 12px; | |
padding: 20px; | |
font-family: 'Courier New', monospace; | |
font-size: 13px; | |
line-height: 1.5; | |
max-height: 500px; | |
overflow-y: auto; | |
} | |
""" | |
with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo: | |
gr.Markdown( | |
""" | |
# ๐ฏ AI ๊ธฐ๋ฐ PPT ์๋ ์์ฑ ์์คํ Pro | |
๊ณ ๊ธ ๋์์ธ ํ ๋ง์ ๋ ์ด์์์ ํ์ฉํ ์ ๋ฌธ์ ์ธ ํ๋ ์ ํ ์ด์ ์ ์๋์ผ๋ก ์์ฑํฉ๋๋ค. | |
๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ํฌ์ธํธ์ ๋ฐํ์ ๋ ธํธ๋ฅผ ํฌํจํฉ๋๋ค. | |
""" | |
) | |
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("<div class='section-header'>๐จ ๋์์ธ ์ต์ </div>") | |
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 ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ ์ฐจํธ ์์ฑ" | |
) | |
include_ai_image = gr.Checkbox( | |
label="๐ผ๏ธ AI 3D ํ์ง ์ด๋ฏธ์ง", | |
value=False, | |
info="AI๋ก ์์ฑํ 3D ์คํ์ผ ์ด๋ฏธ์ง๋ฅผ ํ์ง์ ์ถ๊ฐ (์์ฑ ์๊ฐ 30์ด ์ถ๊ฐ)" | |
) | |
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. **์ถ๊ฐ ์ต์ ์ค์ **: ์น ๊ฒ์, ์ฐจํธ ํฌํจ ๋ฑ | |
5. **AI 3D ์ด๋ฏธ์ง**: ์ฃผ์ ๋ฅผ ์์งํ๋ 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง ์์ฑ | |
6. **์ฐธ๊ณ ์๋ฃ ์ ๋ก๋**: PDF, CSV, TXT ํ์ผ ์ง์ | |
7. **์์ฑ ๋ฒํผ ํด๋ฆญ**: AI๊ฐ ์๋์ผ๋ก PPT ์์ฑ | |
### ๐จ ๋์์ธ ํ ๋ง ํน์ง | |
- **ํ๋กํ์ ๋**: ๋น์ฆ๋์ค ํ๋ ์ ํ ์ด์ ์ ์ ํฉํ ๊น๋ํ ๋์์ธ | |
- **๋ชจ๋**: ํธ๋ ๋ํ๊ณ ์ธ๋ จ๋ ์์ ์กฐํฉ | |
- **์์ฐ**: ํธ์ํ๊ณ ์น๊ทผํ ๋๋์ ์์ฐ ์์ | |
- **ํฌ๋ฆฌ์์ดํฐ๋ธ**: ๋๋ดํ๊ณ ํ๊ธฐ์ฐฌ ์์์ผ๋ก ์ฃผ๋ชฉ๋ ๋์ | |
- **๋ฏธ๋๋ฉ**: ๊น๋ํ๊ณ ๋จ์ํ ํ๋ฐฑ ๋์์ธ | |
### โจ ์๋ก์ด ๊ธฐ๋ฅ | |
- **5๊ฐ ํต์ฌ ํฌ์ธํธ**: ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ๋ด์ฉ | |
- **๋ฐํ์ ๋ ธํธ**: ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค ๋ฐํ์๋ฅผ ์ํ ๋ ธํธ ์๋ ์์ฑ | |
- **ํฅ์๋ ํฐํธ ํฌ๊ธฐ**: ๋ ํฐ ํฐํธ๋ก ๊ฐ๋ ์ฑ ํฅ์ | |
- **AI 3D ์ด๋ฏธ์ง**: API๋ฅผ ํตํ ๊ณ ํ์ง 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง (ํ๊ธ ํ๋กฌํํธ ์ง์) | |
### ๐ก ๊ณ ๊ธ ํ | |
- CSV ํ์ผ ์ ๋ก๋ ์ '์ฐจํธ ํฌํจ' ์ต์ ์ผ๋ก ๋ฐ์ดํฐ ์๊ฐํ ๊ฐ๋ฅ | |
- ์น ๊ฒ์ ํ์ฑํ๋ก ์ต์ ์ ๋ณด ๋ฐ์ | |
- AI ์ด๋ฏธ์ง๋ ์ฃผ์ ์ ํต์ฌ ํค์๋๋ฅผ ๋ถ์ํ์ฌ ์๋ ์์ฑ | |
- ๋ค์ํ ๋ ์ด์์์ผ๋ก ์๊ฐ์ ํฅ๋ฏธ ์ ๋ฐ | |
""" | |
) | |
# Examples | |
gr.Examples( | |
examples=[ | |
["์ธ๊ณต์ง๋ฅ์ ๋ฏธ๋์ ์ฐ์ ์ ์ฉ ์ฌ๋ก", 10, False, True, [], "professional", "modern", "consistent", False, False], | |
["2024๋ ๋์งํธ ๋ง์ผํ ํธ๋ ๋", 12, True, True, [], "modern", "modern", "consistent", False, True], | |
["๊ธฐํ๋ณํ์ ์ง์๊ฐ๋ฅํ ๋ฐ์ ", 15, True, True, [], "nature", "classic", "consistent", False, True], | |
["์คํํธ์ ์ฌ์ ๊ณํ์", 8, False, True, [], "creative", "modern", "varied", False, True], | |
], | |
inputs=[topic_input, num_slides, use_web_search, use_korean, reference_files, | |
design_theme, font_style, layout_style, include_charts, include_ai_image], | |
) | |
# 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 | |
], | |
outputs=[download_file, status_text, content_preview] | |
) | |
if __name__ == "__main__": | |
demo.launch() |