openfree commited on
Commit
ffe737f
ยท
verified ยท
1 Parent(s): daecf3e

Delete ppt_generator.py

Browse files
Files changed (1) hide show
  1. ppt_generator.py +0 -994
ppt_generator.py DELETED
@@ -1,994 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- import os
4
- import re
5
- import json
6
- import tempfile
7
- import random
8
- from typing import Dict, List, Optional, Tuple
9
- from loguru import logger
10
-
11
- # PPT ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
12
- try:
13
- from pptx import Presentation
14
- from pptx.util import Inches, Pt
15
- from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
16
- from pptx.dml.color import RGBColor
17
- from pptx.enum.shapes import MSO_SHAPE
18
- from pptx.chart.data import CategoryChartData
19
- from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
20
- PPTX_AVAILABLE = True
21
- except ImportError:
22
- PPTX_AVAILABLE = False
23
- logger.warning("python-pptx ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. pip install python-pptx")
24
-
25
- from PIL import Image
26
-
27
-
28
- ##############################################################################
29
- # Slide Layout Helper Functions
30
- ##############################################################################
31
- def clean_slide_placeholders(slide):
32
- """์Šฌ๋ผ์ด๋“œ์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ชจ๋“  placeholder ์ œ๊ฑฐ"""
33
- shapes_to_remove = []
34
-
35
- for shape in slide.shapes:
36
- # Placeholder์ธ์ง€ ํ™•์ธ
37
- if hasattr(shape, 'placeholder_format') and shape.placeholder_format:
38
- # ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
39
- if shape.has_text_frame:
40
- text = shape.text_frame.text.strip()
41
- # ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๊ธฐ๋ณธ placeholder ํ…์ŠคํŠธ์ธ ๊ฒฝ์šฐ
42
- if (not text or
43
- 'ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
44
- 'ํ…์ŠคํŠธ ์ž…๋ ฅ' in text or
45
- 'Click to add' in text or
46
- 'Content Placeholder' in text or
47
- '์ œ๋ชฉ ์ถ”๊ฐ€' in text or
48
- '๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€' in text or
49
- '์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
50
- '๋ถ€์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค' in text or
51
- '๋งˆ์Šคํ„ฐ ์ œ๋ชฉ ์Šคํƒ€์ผ ํŽธ์ง‘' in text or
52
- '๋งˆ์Šคํ„ฐ ํ…์ŠคํŠธ ์Šคํƒ€์ผ' in text or
53
- '์ œ๋ชฉ ์—†๋Š” ์Šฌ๋ผ์ด๋“œ' in text):
54
- shapes_to_remove.append(shape)
55
- else:
56
- # ํ…์ŠคํŠธ ํ”„๋ ˆ์ž„์ด ์—†๋Š” placeholder๋„ ์ œ๊ฑฐ
57
- shapes_to_remove.append(shape)
58
-
59
- # ์ œ๊ฑฐ ์‹คํ–‰
60
- for shape in shapes_to_remove:
61
- try:
62
- sp = shape._element
63
- sp.getparent().remove(sp)
64
- except Exception as e:
65
- logger.warning(f"Failed to remove placeholder: {e}")
66
- pass # ์ด๋ฏธ ์ œ๊ฑฐ๋œ ๊ฒฝ์šฐ ๋ฌด์‹œ
67
-
68
-
69
- def force_font_size(text_frame, font_size_pt: int, theme: Dict):
70
- """Force font size for all paragraphs and runs in a text frame"""
71
- if not text_frame:
72
- return
73
-
74
- try:
75
- # Ensure paragraphs exist
76
- if not hasattr(text_frame, 'paragraphs'):
77
- return
78
-
79
- for paragraph in text_frame.paragraphs:
80
- try:
81
- # Set paragraph level font
82
- if hasattr(paragraph, 'font'):
83
- paragraph.font.size = Pt(font_size_pt)
84
- paragraph.font.name = theme['fonts']['body']
85
- paragraph.font.color.rgb = theme['colors']['text']
86
-
87
- # Set run level font (most important for actual rendering)
88
- if hasattr(paragraph, 'runs'):
89
- for run in paragraph.runs:
90
- run.font.size = Pt(font_size_pt)
91
- run.font.name = theme['fonts']['body']
92
- run.font.color.rgb = theme['colors']['text']
93
-
94
- # If paragraph has no runs but has text, create a run
95
- if paragraph.text and (not hasattr(paragraph, 'runs') or len(paragraph.runs) == 0):
96
- # Force creation of runs by modifying text
97
- temp_text = paragraph.text
98
- paragraph.text = temp_text # This creates runs
99
- if hasattr(paragraph, 'runs'):
100
- for run in paragraph.runs:
101
- run.font.size = Pt(font_size_pt)
102
- run.font.name = theme['fonts']['body']
103
- run.font.color.rgb = theme['colors']['text']
104
- except Exception as e:
105
- logger.warning(f"Error setting font for paragraph: {e}")
106
- continue
107
- except Exception as e:
108
- logger.warning(f"Error in force_font_size: {e}")
109
-
110
-
111
- def apply_theme_to_slide(slide, theme: Dict, layout_type: str = 'title_content'):
112
- """Apply design theme to a slide with consistent styling"""
113
- # ๋จผ์ € ๋ชจ๋“  placeholder ์ •๋ฆฌ
114
- clean_slide_placeholders(slide)
115
-
116
- # Add colored background shape for all slides
117
- bg_shape = slide.shapes.add_shape(
118
- MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(5.625)
119
- )
120
- bg_shape.fill.solid()
121
-
122
- # Use lighter background for content slides
123
- if layout_type in ['title_content', 'two_content', 'comparison']:
124
- # Light background with subtle gradient effect
125
- bg_shape.fill.fore_color.rgb = theme['colors']['background']
126
-
127
- # Add accent strip at top
128
- accent_strip = slide.shapes.add_shape(
129
- MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(0.5)
130
- )
131
- accent_strip.fill.solid()
132
- accent_strip.fill.fore_color.rgb = theme['colors']['primary']
133
- accent_strip.line.fill.background()
134
-
135
- # Add bottom accent
136
- bottom_strip = slide.shapes.add_shape(
137
- MSO_SHAPE.RECTANGLE, 0, Inches(5.125), Inches(10), Inches(0.5)
138
- )
139
- bottom_strip.fill.solid()
140
- bottom_strip.fill.fore_color.rgb = theme['colors']['secondary']
141
- bottom_strip.fill.transparency = 0.7
142
- bottom_strip.line.fill.background()
143
-
144
- else:
145
- # Section headers get primary color background
146
- bg_shape.fill.fore_color.rgb = theme['colors']['primary']
147
-
148
- bg_shape.line.fill.background()
149
-
150
- # Move background shapes to back
151
- slide.shapes._spTree.remove(bg_shape._element)
152
- slide.shapes._spTree.insert(2, bg_shape._element)
153
-
154
-
155
- def add_gradient_background(slide, color1: RGBColor, color2: RGBColor):
156
- """Add gradient-like background to slide using shapes"""
157
- # Note: python-pptx doesn't directly support gradients in backgrounds,
158
- # so we'll create a gradient effect using overlapping shapes
159
- left = top = 0
160
- width = Inches(10)
161
- height = Inches(5.625)
162
-
163
- # Add base color rectangle
164
- shape1 = slide.shapes.add_shape(
165
- MSO_SHAPE.RECTANGLE, left, top, width, height
166
- )
167
- shape1.fill.solid()
168
- shape1.fill.fore_color.rgb = color1
169
- shape1.line.fill.background()
170
-
171
- # Add semi-transparent overlay for gradient effect
172
- shape2 = slide.shapes.add_shape(
173
- MSO_SHAPE.RECTANGLE, left, top, width, Inches(2.8)
174
- )
175
- shape2.fill.solid()
176
- shape2.fill.fore_color.rgb = color2
177
- shape2.fill.transparency = 0.5
178
- shape2.line.fill.background()
179
-
180
- # Move shapes to back
181
- slide.shapes._spTree.remove(shape1._element)
182
- slide.shapes._spTree.remove(shape2._element)
183
- slide.shapes._spTree.insert(2, shape1._element)
184
- slide.shapes._spTree.insert(3, shape2._element)
185
-
186
-
187
- def add_decorative_shapes(slide, theme: Dict):
188
- """Add decorative shapes to enhance visual appeal"""
189
- try:
190
- # Placeholder ์ •๋ฆฌ ๋จผ์ € ์‹คํ–‰
191
- clean_slide_placeholders(slide)
192
-
193
- # Add corner accent circle
194
- shape1 = slide.shapes.add_shape(
195
- MSO_SHAPE.OVAL,
196
- Inches(9.3), Inches(4.8),
197
- Inches(0.7), Inches(0.7)
198
- )
199
- shape1.fill.solid()
200
- shape1.fill.fore_color.rgb = theme['colors']['accent']
201
- shape1.fill.transparency = 0.3
202
- shape1.line.fill.background()
203
-
204
- # Add smaller accent
205
- shape2 = slide.shapes.add_shape(
206
- MSO_SHAPE.OVAL,
207
- Inches(0.1), Inches(0.1),
208
- Inches(0.4), Inches(0.4)
209
- )
210
- shape2.fill.solid()
211
- shape2.fill.fore_color.rgb = theme['colors']['secondary']
212
- shape2.fill.transparency = 0.5
213
- shape2.line.fill.background()
214
-
215
- except Exception as e:
216
- logger.warning(f"Failed to add decorative shapes: {e}")
217
-
218
-
219
- def create_chart_slide(slide, chart_data: Dict, theme: Dict):
220
- """Create a chart on the slide based on data"""
221
- try:
222
- # Add chart
223
- x, y, cx, cy = Inches(1), Inches(2), Inches(8), Inches(4.5)
224
-
225
- # Prepare chart data
226
- chart_data_obj = CategoryChartData()
227
-
228
- # Simple bar chart example
229
- if 'columns' in chart_data and 'sample_data' in chart_data:
230
- # Use first numeric column for chart
231
- numeric_cols = []
232
- for col in chart_data['columns']:
233
- try:
234
- # Check if column has numeric data
235
- float(chart_data['sample_data'][0].get(col, 0))
236
- numeric_cols.append(col)
237
- except:
238
- pass
239
-
240
- if numeric_cols:
241
- categories = [str(row.get(chart_data['columns'][0], ''))
242
- for row in chart_data['sample_data'][:5]]
243
- chart_data_obj.categories = categories
244
-
245
- for col in numeric_cols[:3]: # Max 3 series
246
- values = [float(row.get(col, 0))
247
- for row in chart_data['sample_data'][:5]]
248
- chart_data_obj.add_series(col, values)
249
-
250
- chart = slide.shapes.add_chart(
251
- XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data_obj
252
- ).chart
253
-
254
- # Style the chart
255
- chart.has_legend = True
256
- chart.legend.position = XL_LEGEND_POSITION.BOTTOM
257
- except Exception as e:
258
- logger.warning(f"Chart creation failed: {e}")
259
- # If chart fails, add a text placeholder instead
260
- textbox = slide.shapes.add_textbox(x, y, cx, cy)
261
- text_frame = textbox.text_frame
262
- text_frame.text = "Data Chart (Chart generation failed)"
263
- text_frame.paragraphs[0].font.size = Pt(16)
264
- text_frame.paragraphs[0].font.color.rgb = theme['colors']['secondary']
265
-
266
-
267
- ##############################################################################
268
- # Main PPT Generation Function
269
- ##############################################################################
270
- def create_advanced_ppt_from_content(
271
- slides_data: list,
272
- topic: str,
273
- theme_name: str,
274
- include_charts: bool = False,
275
- include_ai_image: bool = False,
276
- include_diagrams: bool = False,
277
- include_flux_images: bool = False,
278
- # ํ•„์š”ํ•œ ์™ธ๋ถ€ ํ•จ์ˆ˜์™€ ๋ฐ์ดํ„ฐ๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์Œ
279
- DESIGN_THEMES: Dict = None,
280
- detect_diagram_type_with_score = None,
281
- generate_diagram_json = None,
282
- generate_diagram_locally = None,
283
- DIAGRAM_GENERATORS_AVAILABLE: bool = False,
284
- generate_cover_image_prompt = None,
285
- generate_conclusion_image_prompt = None,
286
- generate_diverse_prompt = None,
287
- generate_flux_prompt = None,
288
- pick_flux_style = None,
289
- generate_ai_image_via_3d_api = None,
290
- AI_IMAGE_ENABLED: bool = False,
291
- has_emoji = None,
292
- get_emoji_for_content = None
293
- ) -> str:
294
- """Create advanced PPT file with enhanced visual content
295
- ํ‘œ์ง€ 3D 1์žฅ + ์ผ๋ฐ˜ ๋‹ค์ด์–ด๊ทธ๋žจ 2์žฅ + FLUX ์Šคํƒ€์ผ 4์žฅ ์ด์ƒ + 3D ์ด๋ฏธ์ง€ 2์žฅ ์ด์ƒ"""
296
- if not PPTX_AVAILABLE:
297
- raise ImportError("python-pptx library is required")
298
-
299
- prs = Presentation()
300
- theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES['professional'])
301
-
302
- # Set slide size (16:9)
303
- prs.slide_width = Inches(10)
304
- prs.slide_height = Inches(5.625)
305
-
306
- # ์ด๋ฏธ์ง€ ์นด์šดํ„ฐ ๋ฐ ์ถ”์ 
307
- image_count_3d = 0
308
- flux_style_count = 0
309
- diagram_count = 0
310
- max_flux_style = 4 # FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ ์ตœ์†Œ 4๊ฐœ๋กœ ์ฆ๊ฐ€
311
- max_diagrams = 2 # ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ 2๊ฐœ
312
- max_3d_content = 2 # ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ์šฉ 3D ์ด๋ฏธ์ง€ ์ตœ์†Œ 2๊ฐœ
313
- content_3d_count = 0 # ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ 3D ์ด๋ฏธ์ง€ ์นด์šดํ„ฐ
314
-
315
- # ๋‹ค์ด์–ด๊ทธ๋žจ์ด ํ•„์š”ํ•œ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋ฏธ๋ฆฌ ๋ถ„์„
316
- diagram_candidates = []
317
- if include_diagrams:
318
- for i, slide_data in enumerate(slides_data):
319
- title = slide_data.get('title', '')
320
- content = slide_data.get('content', '')
321
- diagram_type, score = detect_diagram_type_with_score(title, content)
322
- if diagram_type and score > 0:
323
- diagram_candidates.append((i, diagram_type, score))
324
-
325
- # ํ•„์š”๋„ ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜๊ณ  ์ƒ์œ„ 2๊ฐœ๋งŒ ์„ ํƒ
326
- diagram_candidates.sort(key=lambda x: x[2], reverse=True)
327
- diagram_candidates = diagram_candidates[:max_diagrams]
328
- diagram_slide_indices = [x[0] for x in diagram_candidates]
329
- else:
330
- diagram_slide_indices = []
331
-
332
- # FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์ƒ์„ฑํ•  ์Šฌ๋ผ์ด๋“œ ์„ ํƒ (๋น„์ค‘ ์ฆ๊ฐ€)
333
- flux_style_indices = []
334
- if include_flux_images:
335
- # ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋Š” ์Šฌ๋ผ์ด๋“œ ์ค‘์—์„œ ์„ ํƒ
336
- available_slides = [i for i in range(len(slides_data)) if i not in diagram_slide_indices]
337
-
338
- # ์ „์ฒด ์Šฌ๋ผ์ด๋“œ์˜ 40% ์ด์ƒ์„ FLUX ์Šคํƒ€์ผ๋กœ (์ตœ์†Œ 4๊ฐœ)
339
- target_flux_count = max(max_flux_style, int(len(available_slides) * 0.4))
340
-
341
- # 2์žฅ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ์„ ํƒํ•˜๋˜, ์ตœ์†Œ 4๊ฐœ๋Š” ๋ณด์žฅ
342
- for i in available_slides:
343
- if len(flux_style_indices) < target_flux_count:
344
- if i % 2 == 0 or len(flux_style_indices) < max_flux_style:
345
- flux_style_indices.append(i)
346
-
347
- # ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•  ์Šฌ๋ผ์ด๋“œ ์„ ํƒ (FLUX์™€ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์—†๋Š” ์Šฌ๋ผ์ด๋“œ)
348
- additional_3d_indices = []
349
- if include_ai_image:
350
- used_indices = set(diagram_slide_indices + flux_style_indices)
351
- available_for_3d = [i for i in range(len(slides_data)) if i not in used_indices]
352
-
353
- # ์ „๋žต์ ์œผ๋กœ 3D ์ด๋ฏธ์ง€ ๋ฐฐ์น˜ (์ค‘๊ฐ„๊ณผ ํ›„๋ฐ˜๋ถ€์—)
354
- if len(available_for_3d) >= max_3d_content:
355
- # ์ค‘๊ฐ„ ์ง€์ 
356
- mid_point = len(slides_data) // 2
357
- # 3/4 ์ง€์ 
358
- three_quarter_point = (3 * len(slides_data)) // 4
359
-
360
- # ์ค‘๊ฐ„ ์ง€์  ๊ทผ์ฒ˜์—์„œ ํ•˜๋‚˜
361
- for i in range(max(0, mid_point - 2), min(len(slides_data), mid_point + 3)):
362
- if i in available_for_3d and len(additional_3d_indices) < 1:
363
- additional_3d_indices.append(i)
364
- break
365
-
366
- # 3/4 ์ง€์  ๊ทผ์ฒ˜์—์„œ ํ•˜๋‚˜
367
- for i in range(max(0, three_quarter_point - 2), min(len(slides_data), three_quarter_point + 3)):
368
- if i in available_for_3d and i not in additional_3d_indices and len(additional_3d_indices) < 2:
369
- additional_3d_indices.append(i)
370
- break
371
-
372
- # ๋ถ€์กฑํ•˜๋ฉด ๋žœ๋คํ•˜๊ฒŒ ์ถ”๊ฐ€
373
- remaining = [i for i in available_for_3d if i not in additional_3d_indices]
374
- while len(additional_3d_indices) < max_3d_content and remaining:
375
- idx = remaining.pop(0)
376
- additional_3d_indices.append(idx)
377
-
378
- logger.info(f"Visual distribution planning:")
379
- logger.info(f"- Diagram slides (max {max_diagrams}): {diagram_slide_indices}")
380
- logger.info(f"- FLUX style slides (min {max_flux_style}): {flux_style_indices[:max_flux_style]} (total: {len(flux_style_indices)})")
381
- logger.info(f"- Additional 3D slides (min {max_3d_content}): {additional_3d_indices}")
382
-
383
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
384
- # 1) ์ œ๋ชฉ ์Šฌ๋ผ์ด๋“œ(ํ‘œ์ง€) ์ƒ์„ฑ
385
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
386
- title_slide_layout = prs.slide_layouts[6] # Blank layout ์‚ฌ์šฉ
387
- slide = prs.slides.add_slide(title_slide_layout)
388
-
389
- # Placeholder ์ •๋ฆฌ
390
- clean_slide_placeholders(slide)
391
-
392
- # ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋””์–ธํŠธ
393
- add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary'])
394
-
395
- # ์ œ๋ชฉ ์ถ”๊ฐ€ (์ง์ ‘ ํ…์ŠคํŠธ๋ฐ•์Šค๋กœ)
396
- title_box = slide.shapes.add_textbox(
397
- Inches(0.5), Inches(1.0), Inches(9), Inches(1.2)
398
- )
399
- title_frame = title_box.text_frame
400
- title_frame.text = topic
401
- title_frame.word_wrap = True
402
-
403
- p = title_frame.paragraphs[0]
404
- p.font.name = theme['fonts']['title']
405
- p.font.size = Pt(36)
406
- p.font.bold = True
407
- p.font.color.rgb = RGBColor(255, 255, 255)
408
- p.alignment = PP_ALIGN.CENTER
409
-
410
- # ๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€
411
- subtitle_box = slide.shapes.add_textbox(
412
- Inches(0.5), Inches(2.2), Inches(9), Inches(0.9)
413
- )
414
- subtitle_frame = subtitle_box.text_frame
415
- subtitle_frame.word_wrap = True
416
-
417
- p2 = subtitle_frame.paragraphs[0]
418
- p2.font.name = theme['fonts']['subtitle']
419
- p2.font.size = Pt(20)
420
- p2.font.color.rgb = RGBColor(255, 255, 255)
421
- p2.alignment = PP_ALIGN.CENTER
422
-
423
- # ํ‘œ์ง€ ์ด๋ฏธ์ง€ (3D API)
424
- if include_ai_image and AI_IMAGE_ENABLED:
425
- logger.info("Generating AI cover image via 3D API...")
426
-
427
- prompt_3d = generate_cover_image_prompt(topic)
428
- ai_image_path = generate_ai_image_via_3d_api(prompt_3d)
429
-
430
- if ai_image_path and os.path.exists(ai_image_path):
431
- try:
432
- img = Image.open(ai_image_path)
433
- img_width, img_height = img.size
434
-
435
- max_width = Inches(3.5)
436
- max_height = Inches(2.5)
437
-
438
- ratio = img_height / img_width
439
- img_w = max_width
440
- img_h = max_width * ratio
441
-
442
- if img_h > max_height:
443
- img_h = max_height
444
- img_w = max_height / ratio
445
-
446
- left = prs.slide_width - img_w - Inches(0.5)
447
- top = prs.slide_height - img_h - Inches(0.8)
448
-
449
- pic = slide.shapes.add_picture(ai_image_path, left, top, width=img_w, height=img_h)
450
- pic.shadow.inherit = False
451
- pic.shadow.visible = True
452
- pic.shadow.blur_radius = Pt(15)
453
- pic.shadow.distance = Pt(8)
454
- pic.shadow.angle = 45
455
-
456
- caption_box = slide.shapes.add_textbox(
457
- left, top - Inches(0.3),
458
- img_w, Inches(0.3)
459
- )
460
- caption_tf = caption_box.text_frame
461
- caption_tf.text = "AI Generated - 3D Style"
462
- caption_p = caption_tf.paragraphs[0]
463
- caption_p.font.size = Pt(10)
464
- caption_p.font.color.rgb = RGBColor(255, 255, 255)
465
- caption_p.alignment = PP_ALIGN.CENTER
466
-
467
- image_count_3d += 1
468
-
469
- # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
470
- try:
471
- os.unlink(ai_image_path)
472
- except:
473
- pass
474
-
475
- except Exception as e:
476
- logger.error(f"Failed to add cover image: {e}")
477
-
478
- add_decorative_shapes(slide, theme)
479
-
480
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
481
- # 2) ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ ์ƒ์„ฑ
482
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
483
- for i, slide_data in enumerate(slides_data):
484
- layout_type = slide_data.get('layout', 'title_content')
485
-
486
- logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}")
487
- logger.debug(f"Content length: {len(slide_data.get('content', ''))}")
488
-
489
- # ํ•ญ์ƒ ๋นˆ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ
490
- slide_layout = prs.slide_layouts[6] # Blank layout
491
- slide = prs.slides.add_slide(slide_layout)
492
-
493
- # Placeholder ์ •๋ฆฌ
494
- clean_slide_placeholders(slide)
495
-
496
- # Apply theme
497
- apply_theme_to_slide(slide, theme, layout_type)
498
-
499
- # Add bridge phrase if available (previous slide transition)
500
- if i > 0 and slide_data.get('bridge_phrase'):
501
- bridge_box = slide.shapes.add_textbox(
502
- Inches(0.5), Inches(0.1), Inches(9), Inches(0.3)
503
- )
504
- bridge_tf = bridge_box.text_frame
505
- bridge_tf.text = slide_data['bridge_phrase']
506
- bridge_tf.word_wrap = True
507
-
508
- for paragraph in bridge_tf.paragraphs:
509
- paragraph.font.size = Pt(12)
510
- paragraph.font.italic = True
511
- paragraph.font.color.rgb = theme['colors']['secondary']
512
- paragraph.alignment = PP_ALIGN.LEFT
513
-
514
- # ์ œ๋ชฉ ์ถ”๊ฐ€ (์ง์ ‘ ํ…์ŠคํŠธ๋ฐ•์Šค๋กœ)
515
- title_box = slide.shapes.add_textbox(
516
- Inches(0.5), Inches(0.5), Inches(9), Inches(1)
517
- )
518
- title_frame = title_box.text_frame
519
- title_frame.text = slide_data.get('title', 'No Title')
520
- title_frame.word_wrap = True
521
-
522
- # ์ œ๋ชฉ ์Šคํƒ€์ผ ์ ์šฉ
523
- for paragraph in title_frame.paragraphs:
524
- if layout_type == 'section_header':
525
- paragraph.font.size = Pt(28)
526
- paragraph.font.color.rgb = RGBColor(255, 255, 255)
527
- paragraph.alignment = PP_ALIGN.CENTER
528
- else:
529
- paragraph.font.size = Pt(24)
530
- paragraph.font.color.rgb = theme['colors']['primary']
531
- paragraph.font.bold = True
532
- paragraph.font.name = theme['fonts']['title']
533
-
534
- # ์Šฌ๋ผ์ด๋“œ ์ •๋ณด
535
- slide_title = slide_data.get('title', '')
536
- slide_content = slide_data.get('content', '')
537
-
538
- # ๊ฒฐ๋ก /ํ•˜์ด๋ผ์ดํŠธ ์Šฌ๋ผ์ด๋“œ ๊ฐ์ง€
539
- is_conclusion_slide = any(word in slide_title.lower() for word in
540
- ['๊ฒฐ๋ก ', 'conclusion', '์š”์•ฝ', 'summary', 'ํ•ต์‹ฌ', 'key',
541
- '๋งˆ๋ฌด๋ฆฌ', 'closing', '์ •๋ฆฌ', 'takeaway', '์‹œ์‚ฌ์ ', 'implication'])
542
-
543
- # ์‹œ๊ฐ์  ์š”์†Œ ์ถ”๊ฐ€ ์—ฌ๋ถ€ ๊ฒฐ์ •
544
- should_add_visual = False
545
- visual_type = None
546
-
547
- # 1. ๋‹ค์ด์–ด๊ทธ๋žจ ๋ชจ๋“ˆ ์‚ฌ์šฉ
548
- if i in diagram_slide_indices and diagram_count < max_diagrams:
549
- should_add_visual = True
550
- diagram_info = next(x for x in diagram_candidates if x[0] == i)
551
- visual_type = ('diagram', diagram_info[1])
552
- diagram_count += 1
553
- # 2. ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ (3D API)
554
- elif is_conclusion_slide and include_ai_image:
555
- should_add_visual = True
556
- visual_type = ('conclusion_image', None)
557
- # 3. FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ ์ด๋ฏธ์ง€ (์ฆ๊ฐ€๋œ ๋น„์ค‘)
558
- elif i in flux_style_indices and flux_style_count < len(flux_style_indices):
559
- should_add_visual = True
560
- visual_type = ('flux_style_diagram', None)
561
- flux_style_count += 1
562
- # 4. ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€ (์ƒˆ๋กœ ์ถ”๊ฐ€)
563
- elif i in additional_3d_indices and content_3d_count < max_3d_content:
564
- should_add_visual = True
565
- visual_type = ('content_3d_image', None)
566
- content_3d_count += 1
567
-
568
-
569
- # ์‹œ๊ฐ์  ์š”์†Œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ขŒ-์šฐ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
570
- if should_add_visual and layout_type not in ['section_header']:
571
- # ์ขŒ์ธก์— ํ…์ŠคํŠธ ๋ฐฐ์น˜
572
- left_box = slide.shapes.add_textbox(
573
- Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5)
574
- )
575
- left_tf = left_box.text_frame
576
- left_tf.clear()
577
-
578
- # ๋‚ด์šฉ์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
579
- if not slide_content or not slide_content.strip():
580
- slide_content = slide_data.get('content', '')
581
- logger.warning(f"Slide {i+1} content was empty, retrieved: {slide_content[:50]}...")
582
-
583
- left_tf.text = slide_content
584
- left_tf.word_wrap = True
585
- force_font_size(left_tf, 14, theme)
586
-
587
-
588
- # Apply emoji bullets
589
- for paragraph in left_tf.paragraphs:
590
- text = paragraph.text.strip()
591
- if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
592
- clean_text = text.lstrip('-โ€ขโ— ')
593
- emoji = get_emoji_for_content(clean_text)
594
- paragraph.text = f"{emoji} {clean_text}"
595
- force_font_size(left_tf, 14, theme)
596
-
597
- # ์šฐ์ธก์— ์‹œ๊ฐ์  ์š”์†Œ ์ถ”๊ฐ€
598
- visual_added = False
599
-
600
- if visual_type[0] == 'diagram' and DIAGRAM_GENERATORS_AVAILABLE:
601
- # ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
602
- logger.info(f"Generating {visual_type[1]} for slide {i+1} (Diagram {diagram_count}/{max_diagrams})")
603
- diagram_json = generate_diagram_json(slide_title, slide_content, visual_type[1])
604
-
605
- if diagram_json:
606
- diagram_path = generate_diagram_locally(diagram_json, visual_type[1], "png")
607
- if diagram_path and os.path.exists(diagram_path):
608
- try:
609
- pic = slide.shapes.add_picture(
610
- diagram_path,
611
- Inches(5.2), Inches(1.5),
612
- width=Inches(4.3), height=Inches(3.0)
613
- )
614
- visual_added = True
615
-
616
- caption_box = slide.shapes.add_textbox(
617
- Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
618
- )
619
- caption_tf = caption_box.text_frame
620
- caption_tf.text = f"{visual_type[1]} Diagram"
621
- caption_p = caption_tf.paragraphs[0]
622
- caption_p.font.size = Pt(10)
623
- caption_p.font.color.rgb = theme['colors']['secondary']
624
- caption_p.alignment = PP_ALIGN.CENTER
625
-
626
- try:
627
- os.unlink(diagram_path)
628
- except:
629
- pass
630
- except Exception as e:
631
- logger.error(f"Failed to add diagram: {e}")
632
-
633
- elif visual_type[0] == 'conclusion_image':
634
- # ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ์šฉ 3D ์ด๋ฏธ์ง€ ์ƒ์„ฑ
635
- logger.info(f"Generating conclusion image for slide {i+1}")
636
- prompt_3d = generate_conclusion_image_prompt(slide_title, slide_content)
637
-
638
- selected_image = generate_ai_image_via_3d_api(prompt_3d)
639
-
640
- if selected_image and os.path.exists(selected_image):
641
- try:
642
- pic = slide.shapes.add_picture(
643
- selected_image,
644
- Inches(5.2), Inches(1.5),
645
- width=Inches(4.3), height=Inches(3.0)
646
- )
647
- visual_added = True
648
- image_count_3d += 1
649
-
650
- caption_box = slide.shapes.add_textbox(
651
- Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
652
- )
653
- caption_tf = caption_box.text_frame
654
- caption_tf.text = "Key Takeaway Visualization - 3D Style"
655
- caption_p = caption_tf.paragraphs[0]
656
- caption_p.font.size = Pt(10)
657
- caption_p.font.color.rgb = theme['colors']['secondary']
658
- caption_p.alignment = PP_ALIGN.CENTER
659
-
660
- try:
661
- os.unlink(selected_image)
662
- except:
663
- pass
664
- except Exception as e:
665
- logger.error(f"Failed to add conclusion image: {e}")
666
-
667
- elif visual_type[0] == 'flux_style_diagram':
668
- # FLUX ์Šคํƒ€์ผ ๋‹ค์ด์–ด๊ทธ๋žจ์„ 3D API๋กœ ์ƒ์„ฑ
669
- logger.info(
670
- f"Generating FLUX-style diagram image for slide {i+1} "
671
- f"(FLUX style {flux_style_count}/{len(flux_style_indices)})"
672
- )
673
-
674
- # FLUX ์Šคํƒ€์ผ ์„ ํƒ ๋ฐ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
675
- style_key = pick_flux_style(i)
676
- logger.info(f"[FLUX Style] Selected: {style_key}")
677
-
678
- # FLUX ์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ๋ฅผ 3D API์šฉ์œผ๋กœ ๋ณ€ํ™˜
679
- flux_prompt = generate_flux_prompt(slide_title, slide_content, style_key)
680
-
681
- # 3D API์šฉ ํ”„๋กฌํ”„ํŠธ๋กœ ๋ณ€ํ™˜ (wbgmsst ์ถ”๊ฐ€ ๋ฐ 3D ์š”์†Œ ๊ฐ•์กฐ)
682
- prompt_3d = f"wbgmsst, 3D {style_key.lower()} style, {flux_prompt}, isometric 3D perspective"
683
-
684
- selected_image = generate_ai_image_via_3d_api(prompt_3d)
685
-
686
- if selected_image and os.path.exists(selected_image):
687
- try:
688
- pic = slide.shapes.add_picture(
689
- selected_image,
690
- Inches(5.2), Inches(1.5),
691
- width=Inches(4.3), height=Inches(3.0)
692
- )
693
- visual_added = True
694
- image_count_3d += 1
695
-
696
- # ์บก์…˜์— FLUX ์Šคํƒ€์ผ ํ‘œ์‹œ
697
- caption_box = slide.shapes.add_textbox(
698
- Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
699
- )
700
- caption_tf = caption_box.text_frame
701
- caption_tf.text = f"AI Generated - {style_key} Style (3D)"
702
- caption_p = caption_tf.paragraphs[0]
703
- caption_p.font.size = Pt(10)
704
- caption_p.font.color.rgb = theme['colors']['secondary']
705
- caption_p.alignment = PP_ALIGN.CENTER
706
-
707
- logger.info(f"[3D API] Successfully generated {style_key} style image")
708
-
709
- except Exception as e:
710
- logger.error(f"Failed to add FLUX-style image: {e}")
711
- finally:
712
- # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
713
- if selected_image and os.path.exists(selected_image):
714
- try:
715
- os.unlink(selected_image)
716
- except:
717
- pass
718
-
719
- elif visual_type[0] == 'content_3d_image':
720
- # ์ถ”๊ฐ€ 3D ์ด๋ฏธ์ง€ ์ƒ์„ฑ
721
- logger.info(f"Generating additional 3D image for slide {i+1} ({content_3d_count}/{max_3d_content})")
722
- prompt_3d = generate_diverse_prompt(slide_title, slide_content, i)
723
-
724
- selected_image = generate_ai_image_via_3d_api(prompt_3d)
725
-
726
- if selected_image and os.path.exists(selected_image):
727
- try:
728
- pic = slide.shapes.add_picture(
729
- selected_image,
730
- Inches(5.2), Inches(1.5),
731
- width=Inches(4.3), height=Inches(3.0)
732
- )
733
- visual_added = True
734
- image_count_3d += 1
735
-
736
- caption_box = slide.shapes.add_textbox(
737
- Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
738
- )
739
- caption_tf = caption_box.text_frame
740
- caption_tf.text = "Content Visualization - 3D Style"
741
- caption_p = caption_tf.paragraphs[0]
742
- caption_p.font.size = Pt(10)
743
- caption_p.font.color.rgb = theme['colors']['secondary']
744
- caption_p.alignment = PP_ALIGN.CENTER
745
-
746
- try:
747
- os.unlink(selected_image)
748
- except:
749
- pass
750
- except Exception as e:
751
- logger.error(f"Failed to add content 3D image: {e}")
752
-
753
- # ์‹œ๊ฐ์  ์š”์†Œ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ถ”๊ฐ€
754
- if not visual_added:
755
- placeholder_box = slide.shapes.add_textbox(
756
- Inches(5.2), Inches(2.5), Inches(4.3), Inches(1.0)
757
- )
758
- placeholder_tf = placeholder_box.text_frame
759
- placeholder_tf.text = f"{visual_type[1] if visual_type[0] == 'diagram' else 'Visual'} Placeholder"
760
- placeholder_tf.paragraphs[0].font.size = Pt(14)
761
- placeholder_tf.paragraphs[0].font.color.rgb = theme['colors']['secondary']
762
- placeholder_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
763
-
764
- else:
765
- # ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ (์‹œ๊ฐ์  ์š”์†Œ ์—†์Œ)
766
- if layout_type == 'section_header':
767
- content = slide_data.get('content', '')
768
- if content:
769
- logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...")
770
- textbox = slide.shapes.add_textbox(
771
- Inches(1), Inches(3.5), Inches(8), Inches(1.5)
772
- )
773
- tf = textbox.text_frame
774
- tf.clear()
775
- tf.text = content
776
- tf.word_wrap = True
777
-
778
- for paragraph in tf.paragraphs:
779
- paragraph.font.name = theme['fonts']['body']
780
- paragraph.font.size = Pt(16)
781
- paragraph.font.color.rgb = RGBColor(255, 255, 255)
782
- paragraph.alignment = PP_ALIGN.CENTER
783
-
784
- line = slide.shapes.add_shape(
785
- MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4)
786
- )
787
- line.fill.solid()
788
- line.fill.fore_color.rgb = RGBColor(255, 255, 255)
789
- line.line.fill.background()
790
-
791
- elif layout_type == 'two_content':
792
- content = slide_data.get('content', '')
793
- if content:
794
- logger.info(f"Creating two-column layout for slide {i+1}")
795
- content_lines = content.split('\n')
796
- mid_point = len(content_lines) // 2
797
-
798
- # Left column
799
- left_box = slide.shapes.add_textbox(
800
- Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5)
801
- )
802
- left_tf = left_box.text_frame
803
- left_tf.clear()
804
- left_content = '\n'.join(content_lines[:mid_point])
805
- if left_content:
806
- left_tf.text = left_content
807
- left_tf.word_wrap = True
808
- force_font_size(left_tf, 14, theme)
809
-
810
- for paragraph in left_tf.paragraphs:
811
- text = paragraph.text.strip()
812
- if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
813
- clean_text = text.lstrip('-โ€ขโ— ')
814
- emoji = get_emoji_for_content(clean_text)
815
- paragraph.text = f"{emoji} {clean_text}"
816
- force_font_size(left_tf, 14, theme)
817
-
818
- # Right column
819
- right_box = slide.shapes.add_textbox(
820
- Inches(5), Inches(1.5), Inches(4.5), Inches(3.5)
821
- )
822
- right_tf = right_box.text_frame
823
- right_tf.clear()
824
- right_content = '\n'.join(content_lines[mid_point:])
825
- if right_content:
826
- right_tf.text = right_content
827
- right_tf.word_wrap = True
828
- force_font_size(right_tf, 14, theme)
829
-
830
- for paragraph in right_tf.paragraphs:
831
- text = paragraph.text.strip()
832
- if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
833
- clean_text = text.lstrip('-โ€ขโ— ')
834
- emoji = get_emoji_for_content(clean_text)
835
- paragraph.text = f"{emoji} {clean_text}"
836
- force_font_size(right_tf, 14, theme)
837
-
838
- else:
839
- # Regular content
840
- content = slide_data.get('content', '')
841
-
842
- logger.info(f"Slide {i+1} - Content to add: '{content[:100]}...' (length: {len(content)})")
843
-
844
- if include_charts and slide_data.get('chart_data'):
845
- create_chart_slide(slide, slide_data['chart_data'], theme)
846
-
847
- if content and content.strip():
848
- textbox = slide.shapes.add_textbox(
849
- Inches(0.5), Inches(1.5), Inches(9), Inches(3.5)
850
- )
851
-
852
- tf = textbox.text_frame
853
- tf.clear()
854
-
855
- tf.text = content.strip()
856
- tf.word_wrap = True
857
-
858
- tf.margin_left = Inches(0.1)
859
- tf.margin_right = Inches(0.1)
860
- tf.margin_top = Inches(0.05)
861
- tf.margin_bottom = Inches(0.05)
862
-
863
- force_font_size(tf, 16, theme)
864
-
865
- for p_idx, paragraph in enumerate(tf.paragraphs):
866
- if paragraph.text.strip():
867
- text = paragraph.text.strip()
868
- if text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
869
- clean_text = text.lstrip('-โ€ขโ— ')
870
- emoji = get_emoji_for_content(clean_text)
871
- paragraph.text = f"{emoji} {clean_text}"
872
-
873
- if paragraph.runs:
874
- for run in paragraph.runs:
875
- run.font.size = Pt(16)
876
- run.font.name = theme['fonts']['body']
877
- run.font.color.rgb = theme['colors']['text']
878
- else:
879
- paragraph.font.size = Pt(16)
880
- paragraph.font.name = theme['fonts']['body']
881
- paragraph.font.color.rgb = theme['colors']['text']
882
-
883
- paragraph.space_before = Pt(6)
884
- paragraph.space_after = Pt(6)
885
- paragraph.line_spacing = 1.3
886
-
887
- logger.info(f"Successfully added content to slide {i+1}")
888
- else:
889
- logger.warning(f"Slide {i+1} has no content or empty content")
890
-
891
- # Add slide notes if available
892
-
893
-
894
- # Add slide notes if available
895
- if slide_data.get('notes'):
896
- try:
897
- notes_slide = slide.notes_slide
898
- notes_text_frame = notes_slide.notes_text_frame
899
-
900
- # ๋…ธํŠธ ๋‚ด์šฉ ์ •๋ฆฌ (๊ฐ€์ด๋“œ ์ œ๊ฑฐ)
901
- notes_content = slide_data.get('notes', '')
902
-
903
- # ๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๊ฐ€์ด๋“œ ํ…์ŠคํŠธ ์ œ๊ฑฐ
904
- import re
905
- notes_content = re.sub(r'\([^)]*\)', '', notes_content).strip()
906
-
907
- # ๋…ธํŠธ๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ๊ธฐ๋ณธ ๋…ธํŠธ ์ถ”๊ฐ€
908
- if not notes_content:
909
- notes_content = f"์Šฌ๋ผ์ด๋“œ {i+1}: {slide_data.get('title', '')}์— ๋Œ€ํ•œ ์„ค๋ช…"
910
-
911
- notes_text_frame.text = notes_content
912
- logger.info(f"Added notes to slide {i+1}: {notes_content[:50]}...")
913
-
914
- except Exception as e:
915
- logger.warning(f"Failed to add slide notes: {e}")
916
-
917
-
918
-
919
- # Add slide number
920
- slide_number_bg = slide.shapes.add_shape(
921
- MSO_SHAPE.ROUNDED_RECTANGLE,
922
- Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5)
923
- )
924
- slide_number_bg.fill.solid()
925
- slide_number_bg.fill.fore_color.rgb = theme['colors']['primary']
926
- slide_number_bg.fill.transparency = 0.8
927
- slide_number_bg.line.fill.background()
928
-
929
- slide_number_box = slide.shapes.add_textbox(
930
- Inches(8.3), Inches(5.05), Inches(1.5), Inches(0.4)
931
- )
932
- slide_number_frame = slide_number_box.text_frame
933
- slide_number_frame.text = f"{i + 1} / {len(slides_data)}"
934
- slide_number_frame.paragraphs[0].font.size = Pt(10)
935
- slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
936
- slide_number_frame.paragraphs[0].font.bold = False
937
- slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
938
-
939
- if i % 2 == 0:
940
- accent_shape = slide.shapes.add_shape(
941
- MSO_SHAPE.OVAL,
942
- Inches(9.6), Inches(0.1),
943
- Inches(0.2), Inches(0.2)
944
- )
945
- accent_shape.fill.solid()
946
- accent_shape.fill.fore_color.rgb = theme['colors']['accent']
947
- accent_shape.line.fill.background()
948
-
949
- # ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋กœ๊ทธ
950
- logger.info(f"Total visual elements generated:")
951
- logger.info(f"- 3D API images: {image_count_3d} total")
952
- logger.info(f" - Cover: 1")
953
- logger.info(f" - Content slides: {content_3d_count}")
954
- logger.info(f" - Conclusion: {1 if any('conclusion' in str(v) for v in locals().values()) else 0}")
955
- logger.info(f" - FLUX-style: {flux_style_count}")
956
- logger.info(f"- Traditional diagrams: {diagram_count}")
957
-
958
- # Add thank you slide
959
- thank_you_layout = prs.slide_layouts[6] # Blank layout
960
- thank_you_slide = prs.slides.add_slide(thank_you_layout)
961
-
962
- # Placeholder ์ •๋ฆฌ
963
- clean_slide_placeholders(thank_you_slide)
964
-
965
- add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary'])
966
-
967
- # Thank you ์ œ๋ชฉ ์ถ”๊ฐ€
968
- thank_you_title_box = thank_you_slide.shapes.add_textbox(
969
- Inches(0.5), Inches(2.0), Inches(9), Inches(1.5)
970
- )
971
- thank_you_title_frame = thank_you_title_box.text_frame
972
- thank_you_title_frame.text = "Thank You"
973
- thank_you_title_frame.word_wrap = True
974
-
975
- for paragraph in thank_you_title_frame.paragraphs:
976
- paragraph.font.size = Pt(36)
977
- paragraph.font.bold = True
978
- paragraph.font.color.rgb = RGBColor(255, 255, 255)
979
- paragraph.alignment = PP_ALIGN.CENTER
980
- paragraph.font.name = theme['fonts']['title']
981
-
982
- info_box = thank_you_slide.shapes.add_textbox(
983
- Inches(2), Inches(3.5), Inches(6), Inches(1)
984
- )
985
- info_tf = info_box.text_frame
986
- info_tf.text = "AI-Generated Presentation"
987
- info_tf.paragraphs[0].font.size = Pt(18)
988
- info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
989
- info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
990
-
991
- # Save to temporary file
992
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp_file:
993
- prs.save(tmp_file.name)
994
- return tmp_file.name