Spaces:
Running
Running
#!/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 |