Open-GAMMA / ppt_generator.py
openfree's picture
Update ppt_generator.py
096f71d verified
raw
history blame
44.5 kB
#!/usr/bin/env python
import os
import re
import json
import tempfile
import random
from typing import Dict, List, Optional, Tuple
from loguru import logger
# PPT ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
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")
from PIL import Image
##############################################################################
# Slide Layout Helper Functions
##############################################################################
def clean_slide_placeholders(slide):
"""์Šฌ๋ผ์ด๋“œ์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ชจ๋“  placeholder ์ œ๊ฑฐ"""
shapes_to_remove = []
for shape in slide.shapes:
# Placeholder์ธ์ง€ ํ™•์ธ
if hasattr(shape, 'placeholder_format') and shape.placeholder_format:
# ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
if shape.has_text_frame:
text = shape.text_frame.text.strip()
# ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๊ธฐ๋ณธ placeholder ํ…์ŠคํŠธ์ธ ๊ฒฝ์šฐ
if (not text or
'ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
'ํ…์ŠคํŠธ ์ž…๋ ฅ' in text or
'Click to add' in text or
'Content Placeholder' in text or
'์ œ๋ชฉ ์ถ”๊ฐ€' in text or
'๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€' in text or
'์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
'๋ถ€์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
'๋งˆ์Šคํ„ฐ ์ œ๋ชฉ ์Šคํƒ€์ผ ํŽธ์ง‘' in text or
'๋งˆ์Šคํ„ฐ ํ…์ŠคํŠธ ์Šคํƒ€์ผ' in text or
'์ œ๋ชฉ ์—†๋Š” ์Šฌ๋ผ์ด๋“œ' in text):
shapes_to_remove.append(shape)
else:
# ํ…์ŠคํŠธ ํ”„๋ ˆ์ž„์ด ์—†๋Š” placeholder๋„ ์ œ๊ฑฐ
shapes_to_remove.append(shape)
# ์ œ๊ฑฐ ์‹คํ–‰
for shape in shapes_to_remove:
try:
sp = shape._element
sp.getparent().remove(sp)
except Exception as e:
logger.warning(f"Failed to remove placeholder: {e}")
pass # ์ด๋ฏธ ์ œ๊ฑฐ๋œ ๊ฒฝ์šฐ ๋ฌด์‹œ
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"""
# ๋จผ์ € ๋ชจ๋“  placeholder ์ •๋ฆฌ
clean_slide_placeholders(slide)
# 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)
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:
# Placeholder ์ •๋ฆฌ ๋จผ์ € ์‹คํ–‰
clean_slide_placeholders(slide)
# 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 = "Data Chart (Chart generation failed)"
text_frame.paragraphs[0].font.size = Pt(16)
text_frame.paragraphs[0].font.color.rgb = theme['colors']['secondary']
##############################################################################
# Main PPT Generation Function
##############################################################################
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,
# ํ•„์š”ํ•œ ์™ธ๋ถ€ ํ•จ์ˆ˜์™€ ๋ฐ์ดํ„ฐ๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์Œ
DESIGN_THEMES: Dict = None,
detect_diagram_type_with_score = None,
generate_diagram_json = None,
generate_diagram_locally = None,
DIAGRAM_GENERATORS_AVAILABLE: bool = False,
generate_cover_image_prompt = None,
generate_conclusion_image_prompt = None,
generate_diverse_prompt = None,
generate_flux_prompt = None,
pick_flux_style = None,
generate_ai_image_via_3d_api = None,
AI_IMAGE_ENABLED: bool = False,
has_emoji = None,
get_emoji_for_content = None
) -> str:
"""Create advanced PPT file with enhanced visual content
ํ‘œ์ง€ 3D 1์žฅ + ์ผ๋ฐ˜ ๋‹ค์ด์–ด๊ทธ๋žจ 2์žฅ + FLUX ์Šคํƒ€์ผ 4์žฅ ์ด์ƒ + 3D ์ด๋ฏธ์ง€ 2์žฅ ์ด์ƒ"""
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
flux_style_count = 0
diagram_count = 0
max_flux_style = 4 # FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 4๊ฐœ๋กœ ์ฆ๊ฐ€
max_diagrams = 2 # ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ 2๊ฐœ
max_3d_content = 2 # ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ์šฉ 3D ์ด๋ฏธ์ง€ ์ตœ์†Œ 2๊ฐœ
content_3d_count = 0 # ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ 3D ์ด๋ฏธ์ง€ ์นด์šดํ„ฐ
# ๋‹ค์ด์–ด๊ทธ๋žจ์ด ํ•„์š”ํ•œ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋ฏธ๋ฆฌ ๋ถ„์„
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 = []
# FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์ƒ์„ฑํ•  ์Šฌ๋ผ์ด๋“œ ์„ ํƒ (๋น„์ค‘ ์ฆ๊ฐ€)
flux_style_indices = []
if include_flux_images:
# ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋Š” ์Šฌ๋ผ์ด๋“œ ์ค‘์—์„œ ์„ ํƒ
available_slides = [i for i in range(len(slides_data)) if i not in diagram_slide_indices]
# ์ „์ฒด ์Šฌ๋ผ์ด๋“œ์˜ 40% ์ด์ƒ์„ FLUX ์Šคํƒ€์ผ๋กœ (์ตœ์†Œ 4๊ฐœ)
target_flux_count = max(max_flux_style, int(len(available_slides) * 0.4))
# 2์žฅ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ์„ ํƒํ•˜๋˜, ์ตœ์†Œ 4๊ฐœ๋Š” ๋ณด์žฅ
for i in available_slides:
if len(flux_style_indices) < target_flux_count:
if i % 2 == 0 or len(flux_style_indices) < max_flux_style:
flux_style_indices.append(i)
# ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•  ์Šฌ๋ผ์ด๋“œ ์„ ํƒ (FLUX์™€ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์—†๋Š” ์Šฌ๋ผ์ด๋“œ)
additional_3d_indices = []
if include_ai_image:
used_indices = set(diagram_slide_indices + flux_style_indices)
available_for_3d = [i for i in range(len(slides_data)) if i not in used_indices]
# ์ „๋žต์ ์œผ๋กœ 3D ์ด๋ฏธ์ง€ ๋ฐฐ์น˜ (์ค‘๊ฐ„๊ณผ ํ›„๋ฐ˜๋ถ€์—)
if len(available_for_3d) >= max_3d_content:
# ์ค‘๊ฐ„ ์ง€์ 
mid_point = len(slides_data) // 2
# 3/4 ์ง€์ 
three_quarter_point = (3 * len(slides_data)) // 4
# ์ค‘๊ฐ„ ์ง€์  ๊ทผ์ฒ˜์—์„œ ํ•˜๋‚˜
for i in range(max(0, mid_point - 2), min(len(slides_data), mid_point + 3)):
if i in available_for_3d and len(additional_3d_indices) < 1:
additional_3d_indices.append(i)
break
# 3/4 ์ง€์  ๊ทผ์ฒ˜์—์„œ ํ•˜๋‚˜
for i in range(max(0, three_quarter_point - 2), min(len(slides_data), three_quarter_point + 3)):
if i in available_for_3d and i not in additional_3d_indices and len(additional_3d_indices) < 2:
additional_3d_indices.append(i)
break
# ๋ถ€์กฑํ•˜๋ฉด ๋žœ๋คํ•˜๊ฒŒ ์ถ”๊ฐ€
remaining = [i for i in available_for_3d if i not in additional_3d_indices]
while len(additional_3d_indices) < max_3d_content and remaining:
idx = remaining.pop(0)
additional_3d_indices.append(idx)
logger.info(f"Visual distribution planning:")
logger.info(f"- Diagram slides (max {max_diagrams}): {diagram_slide_indices}")
logger.info(f"- FLUX style slides (min {max_flux_style}): {flux_style_indices[:max_flux_style]} (total: {len(flux_style_indices)})")
logger.info(f"- Additional 3D slides (min {max_3d_content}): {additional_3d_indices}")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1) ์ œ๋ชฉ ์Šฌ๋ผ์ด๋“œ(ํ‘œ์ง€) ์ƒ์„ฑ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
title_slide_layout = prs.slide_layouts[6] # Blank layout ์‚ฌ์šฉ
slide = prs.slides.add_slide(title_slide_layout)
# Placeholder ์ •๋ฆฌ
clean_slide_placeholders(slide)
# ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋””์–ธํŠธ
add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary'])
# ์ œ๋ชฉ ์ถ”๊ฐ€ (์ง์ ‘ ํ…์ŠคํŠธ๋ฐ•์Šค๋กœ)
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(1.0), Inches(9), Inches(1.2)
)
title_frame = title_box.text_frame
title_frame.text = topic
title_frame.word_wrap = True
p = title_frame.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
# ๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€
subtitle_box = slide.shapes.add_textbox(
Inches(0.5), Inches(2.2), Inches(9), Inches(0.9)
)
subtitle_frame = subtitle_box.text_frame
subtitle_frame.word_wrap = True
p2 = subtitle_frame.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
# ํ‘œ์ง€ ์ด๋ฏธ์ง€ (3D API)
if include_ai_image and AI_IMAGE_ENABLED:
logger.info("Generating AI cover image via 3D API...")
prompt_3d = generate_cover_image_prompt(topic)
ai_image_path = generate_ai_image_via_3d_api(prompt_3d)
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 - 3D Style"
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
image_count_3d += 1
# ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
try:
os.unlink(ai_image_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', ''))}")
# ํ•ญ์ƒ ๋นˆ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ
slide_layout = prs.slide_layouts[6] # Blank layout
slide = prs.slides.add_slide(slide_layout)
# Placeholder ์ •๋ฆฌ
clean_slide_placeholders(slide)
# Apply theme
apply_theme_to_slide(slide, theme, layout_type)
# Add bridge phrase if available (previous slide transition)
if i > 0 and slide_data.get('bridge_phrase'):
bridge_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.1), Inches(9), Inches(0.3)
)
bridge_tf = bridge_box.text_frame
bridge_tf.text = slide_data['bridge_phrase']
bridge_tf.word_wrap = True
for paragraph in bridge_tf.paragraphs:
paragraph.font.size = Pt(12)
paragraph.font.italic = True
paragraph.font.color.rgb = theme['colors']['secondary']
paragraph.alignment = PP_ALIGN.LEFT
# ์ œ๋ชฉ ์ถ”๊ฐ€ (์ง์ ‘ ํ…์ŠคํŠธ๋ฐ•์Šค๋กœ)
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.5), Inches(9), Inches(1)
)
title_frame = title_box.text_frame
title_frame.text = slide_data.get('title', 'No Title')
title_frame.word_wrap = True
# ์ œ๋ชฉ ์Šคํƒ€์ผ ์ ์šฉ
for paragraph in title_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']
# ์Šฌ๋ผ์ด๋“œ ์ •๋ณด
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. ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ (3D API)
elif is_conclusion_slide and include_ai_image:
should_add_visual = True
visual_type = ('conclusion_image', None)
# 3. FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ ์ด๋ฏธ์ง€ (์ฆ๊ฐ€๋œ ๋น„์ค‘)
elif i in flux_style_indices and flux_style_count < len(flux_style_indices):
should_add_visual = True
visual_type = ('flux_style_diagram', None)
flux_style_count += 1
# 4. ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€ (์ƒˆ๋กœ ์ถ”๊ฐ€)
elif i in additional_3d_indices and content_3d_count < max_3d_content:
should_add_visual = True
visual_type = ('content_3d_image', None)
content_3d_count += 1
# ์‹œ๊ฐ์  ์š”์†Œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ขŒ-์šฐ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
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()
# ๋‚ด์šฉ์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
if not slide_content or not slide_content.strip():
slide_content = slide_data.get('content', '')
logger.warning(f"Slide {i+1} content was empty, retrieved: {slide_content[:50]}...")
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_image':
# ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ์šฉ 3D ์ด๋ฏธ์ง€ ์ƒ์„ฑ
logger.info(f"Generating conclusion image for slide {i+1}")
prompt_3d = generate_conclusion_image_prompt(slide_title, slide_content)
selected_image = generate_ai_image_via_3d_api(prompt_3d)
if selected_image and os.path.exists(selected_image):
try:
pic = slide.shapes.add_picture(
selected_image,
Inches(5.2), Inches(1.5),
width=Inches(4.3), height=Inches(3.0)
)
visual_added = True
image_count_3d += 1
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 - 3D Style"
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(selected_image)
except:
pass
except Exception as e:
logger.error(f"Failed to add conclusion image: {e}")
elif visual_type[0] == 'flux_style_diagram':
# FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ์„ 3D API๋กœ ์ƒ์„ฑ
logger.info(
f"Generating FLUX-style diagram image for slide {i+1} "
f"(FLUX style {flux_style_count}/{len(flux_style_indices)})"
)
# FLUX ์Šคํƒ€์ผ ์„ ํƒ ๋ฐ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
style_key = pick_flux_style(i)
logger.info(f"[FLUX Style] Selected: {style_key}")
# FLUX ์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ๋ฅผ 3D API์šฉ์œผ๋กœ ๋ณ€ํ™˜
flux_prompt = generate_flux_prompt(slide_title, slide_content, style_key)
# 3D API์šฉ ํ”„๋กฌํ”„ํŠธ๋กœ ๋ณ€ํ™˜ (wbgmsst ์ถ”๊ฐ€ ๋ฐ 3D ์š”์†Œ ๊ฐ•์กฐ)
prompt_3d = f"wbgmsst, 3D {style_key.lower()} style, {flux_prompt}, isometric 3D perspective"
selected_image = generate_ai_image_via_3d_api(prompt_3d)
if selected_image and os.path.exists(selected_image):
try:
pic = slide.shapes.add_picture(
selected_image,
Inches(5.2), Inches(1.5),
width=Inches(4.3), height=Inches(3.0)
)
visual_added = True
image_count_3d += 1
# ์บก์…˜์— FLUX ์Šคํƒ€์ผ ํ‘œ์‹œ
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"AI Generated - {style_key} Style (3D)"
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
logger.info(f"[3D API] Successfully generated {style_key} style image")
except Exception as e:
logger.error(f"Failed to add FLUX-style image: {e}")
finally:
# ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
if selected_image and os.path.exists(selected_image):
try:
os.unlink(selected_image)
except:
pass
elif visual_type[0] == 'content_3d_image':
# ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€ ์ƒ์„ฑ
logger.info(f"Generating additional 3D image for slide {i+1} ({content_3d_count}/{max_3d_content})")
prompt_3d = generate_diverse_prompt(slide_title, slide_content, i)
selected_image = generate_ai_image_via_3d_api(prompt_3d)
if selected_image and os.path.exists(selected_image):
try:
pic = slide.shapes.add_picture(
selected_image,
Inches(5.2), Inches(1.5),
width=Inches(4.3), height=Inches(3.0)
)
visual_added = True
image_count_3d += 1
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 = "Content Visualization - 3D Style"
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(selected_image)
except:
pass
except Exception as e:
logger.error(f"Failed to add content 3D image: {e}")
# ์‹œ๊ฐ์  ์š”์†Œ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ถ”๊ฐ€
if not visual_added:
placeholder_box = slide.shapes.add_textbox(
Inches(5.2), Inches(2.5), Inches(4.3), Inches(1.0)
)
placeholder_tf = placeholder_box.text_frame
placeholder_tf.text = f"{visual_type[1] if visual_type[0] == 'diagram' else 'Visual'} Placeholder"
placeholder_tf.paragraphs[0].font.size = Pt(14)
placeholder_tf.paragraphs[0].font.color.rgb = theme['colors']['secondary']
placeholder_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
else:
# ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ (์‹œ๊ฐ์  ์š”์†Œ ์—†์Œ)
if layout_type == 'section_header':
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
# 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_content = slide_data.get('notes', '')
# ๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๊ฐ€์ด๋“œ ํ…์ŠคํŠธ ์ œ๊ฑฐ
import re
notes_content = re.sub(r'\([^)]*\)', '', notes_content).strip()
# ๋…ธํŠธ๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ๊ธฐ๋ณธ ๋…ธํŠธ ์ถ”๊ฐ€
if not notes_content:
notes_content = f"์Šฌ๋ผ์ด๋“œ {i+1}: {slide_data.get('title', '')}์— ๋Œ€ํ•œ ์„ค๋ช…"
notes_text_frame.text = notes_content
logger.info(f"Added notes to slide {i+1}: {notes_content[:50]}...")
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 visual elements generated:")
logger.info(f"- 3D API images: {image_count_3d} total")
logger.info(f" - Cover: 1")
logger.info(f" - Content slides: {content_3d_count}")
logger.info(f" - Conclusion: {1 if any('conclusion' in str(v) for v in locals().values()) else 0}")
logger.info(f" - FLUX-style: {flux_style_count}")
logger.info(f"- Traditional diagrams: {diagram_count}")
# Add thank you slide
thank_you_layout = prs.slide_layouts[6] # Blank layout
thank_you_slide = prs.slides.add_slide(thank_you_layout)
# Placeholder ์ •๋ฆฌ
clean_slide_placeholders(thank_you_slide)
add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary'])
# Thank you ์ œ๋ชฉ ์ถ”๊ฐ€
thank_you_title_box = thank_you_slide.shapes.add_textbox(
Inches(0.5), Inches(2.0), Inches(9), Inches(1.5)
)
thank_you_title_frame = thank_you_title_box.text_frame
thank_you_title_frame.text = "Thank You"
thank_you_title_frame.word_wrap = True
for paragraph in thank_you_title_frame.paragraphs:
paragraph.font.size = Pt(36)
paragraph.font.bold = True
paragraph.font.color.rgb = RGBColor(255, 255, 255)
paragraph.alignment = PP_ALIGN.CENTER
paragraph.font.name = theme['fonts']['title']
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-Generated Presentation"
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