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() | |
| 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 |