#!/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() 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 if slide_data.get('notes'): try: notes_slide = slide.notes_slide notes_text_frame = notes_slide.notes_text_frame notes_text_frame.text = slide_data.get('notes', '') except Exception as e: logger.warning(f"Failed to add slide notes: {e}") # Add slide number slide_number_bg = slide.shapes.add_shape( MSO_SHAPE.ROUNDED_RECTANGLE, Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5) ) slide_number_bg.fill.solid() slide_number_bg.fill.fore_color.rgb = theme['colors']['primary'] slide_number_bg.fill.transparency = 0.8 slide_number_bg.line.fill.background() slide_number_box = slide.shapes.add_textbox( Inches(8.3), Inches(5.05), Inches(1.5), Inches(0.4) ) slide_number_frame = slide_number_box.text_frame slide_number_frame.text = f"{i + 1} / {len(slides_data)}" slide_number_frame.paragraphs[0].font.size = Pt(10) slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) slide_number_frame.paragraphs[0].font.bold = False slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER if i % 2 == 0: accent_shape = slide.shapes.add_shape( MSO_SHAPE.OVAL, Inches(9.6), Inches(0.1), Inches(0.2), Inches(0.2) ) accent_shape.fill.solid() accent_shape.fill.fore_color.rgb = theme['colors']['accent'] accent_shape.line.fill.background() # 이미지 생성 로그 logger.info(f"Total 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