ginipick commited on
Commit
37e200d
ยท
verified ยท
1 Parent(s): 2de4d6d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +23 -2254
app.py CHANGED
@@ -1,2266 +1,35 @@
1
- import replicate
2
- import requests
3
  import os
4
- import json
5
- from io import BytesIO
6
- from PIL import Image
7
- from typing import List, Tuple, Dict
8
- from datetime import datetime
9
- import time
10
- import traceback
11
- import base64
12
- from pptx import Presentation
13
- from pptx.util import Inches, Pt
14
- from pptx.dml.color import RGBColor
15
- from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
16
- from pptx.enum.shapes import MSO_SHAPE
17
- import PyPDF2
18
- import pandas as pd
19
- import chardet
20
- import gradio as gr
21
 
22
- # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ๊ธฐ ์ž„ํฌํŠธ ์ถ”๊ฐ€
23
- try:
24
- from process_flow_generator import generate_process_flow_for_ppt
25
- PROCESS_FLOW_AVAILABLE = True
26
- except ImportError:
27
- PROCESS_FLOW_AVAILABLE = False
28
- print("[๊ฒฝ๊ณ ] process_flow_generator๋ฅผ ์ž„ํฌํŠธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
29
-
30
- # ์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ ์ •์˜
31
- EXAMPLE_TOPICS = {
32
- "๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ": "AI ๊ธฐ๋ฐ˜ ๊ณ ๊ฐ ์„œ๋น„์Šค ์ž๋™ํ™” ํ”Œ๋žซํผ ํˆฌ์ž ์ œ์•ˆ",
33
- "์ œํ’ˆ ์†Œ๊ฐœ": "์Šค๋งˆํŠธ ํ™ˆ IoT ๋ณด์•ˆ ์‹œ์Šคํ…œ ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ",
34
- "ํ”„๋กœ์ ํŠธ ๋ณด๊ณ ": "๋””์ง€ํ„ธ ์ „ํ™˜ ํ”„๋กœ์ ํŠธ 3๋ถ„๊ธฐ ์„ฑ๊ณผ ๋ณด๊ณ ",
35
- "์ „๋žต ๊ธฐํš": "2025๋…„ ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ง„์ถœ ์ „๋žต ์ˆ˜๋ฆฝ"
36
- }
37
-
38
- # ์˜ค๋””์–ธ์Šค ํƒ€์ž… ์ •์˜
39
- AUDIENCE_TYPES = {
40
- "๊ฒฝ์˜์ง„/์ž„์›": {
41
- "description": "C-level, ์˜์‚ฌ๊ฒฐ์ •๊ถŒ์ž",
42
- "tone": "์ „๋žต์ , ๊ฒฐ๊ณผ ์ค‘์‹ฌ, ROI ๊ฐ•์กฐ",
43
- "focus": "๋น„์ฆˆ๋‹ˆ์Šค ๊ฐ€์น˜, ํˆฌ์ž ์ˆ˜์ต๋ฅ , ์ „๋žต์  ์˜ํ–ฅ"
44
- },
45
- "ํˆฌ์ž์ž": {
46
- "description": "VC, ์—”์ คํˆฌ์ž์ž, ๊ธฐ๊ด€ํˆฌ์ž์ž",
47
- "tone": "์ˆ˜์น˜ ๊ธฐ๋ฐ˜, ์„ฑ์žฅ ๊ฐ€๋Šฅ์„ฑ, ์‹œ์žฅ ๊ธฐํšŒ",
48
- "focus": "์‹œ์žฅ ๊ทœ๋ชจ, ์„ฑ์žฅ๋ฅ , ๊ฒฝ์Ÿ์šฐ์œ„, Exit ์ „๋žต"
49
- },
50
- "๊ธฐ์ˆ ํŒ€": {
51
- "description": "๊ฐœ๋ฐœ์ž, ์—”์ง€๋‹ˆ์–ด, IT ์ „๋ฌธ๊ฐ€",
52
- "tone": "๊ธฐ์ˆ ์ , ๊ตฌ์ฒด์ , ์‹ค์šฉ์ ",
53
- "focus": "๊ธฐ์ˆ  ์Šคํƒ, ์•„ํ‚คํ…์ฒ˜, ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ์„ฑ๋Šฅ"
54
- },
55
- "์ผ๋ฐ˜ ์ง์›": {
56
- "description": "์‹ค๋ฌด์ž, ํŒ€์›",
57
- "tone": "์นœ๊ทผํ•œ, ์‹ค๋ฌด์ , ํ˜‘์—… ์ค‘์‹ฌ",
58
- "focus": "์‹คํ–‰ ๊ณ„ํš, ์—ญํ• , ํ”„๋กœ์„ธ์Šค, ํ˜‘์—… ๋ฐฉ์•ˆ"
59
- },
60
- "๊ณ ๊ฐ/ํŒŒํŠธ๋„ˆ": {
61
- "description": "B2B ๊ณ ๊ฐ, ๋น„์ฆˆ๋‹ˆ์Šค ํŒŒํŠธ๋„ˆ",
62
- "tone": "์‹ ๋ขฐ๊ฐ, ์ „๋ฌธ์ , ํ˜œํƒ ์ค‘์‹ฌ",
63
- "focus": "๊ณ ๊ฐ ๊ฐ€์น˜, ํ˜œํƒ, ์‚ฌ๋ก€, ์ง€์› ์ฒด๊ณ„"
64
- },
65
- "์ผ๋ฐ˜ ๋Œ€์ค‘": {
66
- "description": "B2C ๊ณ ๊ฐ, ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž",
67
- "tone": "์‰ฝ๊ณ  ์นœ๊ทผํ•œ, ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด",
68
- "focus": "์‚ฌ์šฉ ํŽธ์˜์„ฑ, ํ˜œํƒ, ๊ฐ€๊ฒฉ, ์ฐจ๋ณ„์ "
69
- }
70
- }
71
-
72
- # ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
73
- REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
74
- FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
75
- BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
76
-
77
- # ์ „์—ญ ๋ณ€์ˆ˜๋กœ ํ˜„์žฌ ์Šฌ๋ผ์ด๋“œ ๋ฐ์ดํ„ฐ ์ €์žฅ
78
- current_slides_data = []
79
- current_topic = ""
80
- current_template = ""
81
- current_theme = ""
82
- uploaded_content = "" # ์ „์—ญ ๋ณ€์ˆ˜๋กœ ์ถ”๊ฐ€
83
-
84
- # ๋””์ž์ธ ํ…Œ๋งˆ ์ •์˜
85
- DESIGN_THEMES = {
86
- "๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ": {
87
- "name": "Minimal Light",
88
- "description": "๋ฐ๊ณ  ๊นจ๋—ํ•œ ๋ฏธ๋‹ˆ๋ฉ€ ๋””์ž์ธ",
89
- "background": RGBColor(250, 250, 252),
90
- "title_color": RGBColor(33, 37, 41),
91
- "subtitle_color": RGBColor(52, 58, 64),
92
- "text_color": RGBColor(73, 80, 87),
93
- "accent_color": RGBColor(0, 123, 255),
94
- "box_fill": RGBColor(255, 255, 255),
95
- "box_opacity": 0.95,
96
- "shadow": True,
97
- "gradient": False
98
- },
99
- "๋ชจ๋˜ ๊ทธ๋ผ๋””์–ธํŠธ": {
100
- "name": "Modern Gradient",
101
- "description": "๋ถ€๋“œ๋Ÿฌ์šด ๊ทธ๋ผ๋””์–ธํŠธ์™€ ํ˜„๋Œ€์  ๋А๋‚Œ",
102
- "background": RGBColor(245, 247, 250),
103
- "title_color": RGBColor(25, 42, 86),
104
- "subtitle_color": RGBColor(68, 85, 102),
105
- "text_color": RGBColor(85, 102, 119),
106
- "accent_color": RGBColor(103, 58, 183),
107
- "box_fill": RGBColor(249, 250, 251),
108
- "box_opacity": 0.9,
109
- "shadow": True,
110
- "gradient": True
111
- },
112
- "๋‹คํฌ ์—˜๋ ˆ๊ฐ•์Šค": {
113
- "name": "Dark Elegance",
114
- "description": "์„ธ๋ จ๋œ ๋‹คํฌ ๋ชจ๋“œ ๋””์ž์ธ",
115
- "background": RGBColor(25, 25, 35),
116
- "title_color": RGBColor(240, 240, 245),
117
- "subtitle_color": RGBColor(200, 200, 210),
118
- "text_color": RGBColor(170, 170, 180),
119
- "accent_color": RGBColor(0, 188, 212),
120
- "box_fill": RGBColor(35, 35, 45),
121
- "box_opacity": 0.85,
122
- "shadow": False,
123
- "gradient": False
124
- },
125
- "๋„ค์ด์ฒ˜ ๊ทธ๋ฆฐ": {
126
- "name": "Nature Green",
127
- "description": "์ž์—ฐ ์นœํ™”์ ์ธ ๊ทธ๋ฆฐ ํ…Œ๋งˆ",
128
- "background": RGBColor(242, 248, 244),
129
- "title_color": RGBColor(27, 67, 50),
130
- "subtitle_color": RGBColor(45, 106, 79),
131
- "text_color": RGBColor(64, 125, 98),
132
- "accent_color": RGBColor(76, 175, 80),
133
- "box_fill": RGBColor(255, 255, 255),
134
- "box_opacity": 0.92,
135
- "shadow": True,
136
- "gradient": False
137
- },
138
- "์ฝ”ํผ๋ ˆ์ดํŠธ ๋ธ”๋ฃจ": {
139
- "name": "Corporate Blue",
140
- "description": "์ „๋ฌธ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ์Šคํƒ€์ผ",
141
- "background": RGBColor(244, 247, 252),
142
- "title_color": RGBColor(13, 71, 161),
143
- "subtitle_color": RGBColor(25, 118, 210),
144
- "text_color": RGBColor(42, 63, 84),
145
- "accent_color": RGBColor(33, 150, 243),
146
- "box_fill": RGBColor(255, 255, 255),
147
- "box_opacity": 0.95,
148
- "shadow": True,
149
- "gradient": False
150
- }
151
- }
152
-
153
- # ์Šคํƒ€์ผ ์ •์˜ (ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ถ”๊ฐ€)
154
- STYLE_TEMPLATES = {
155
- "Title Slide (Hero)": {
156
- "name": "Title Slide",
157
- "description": "Impactful hero image for title slide",
158
- "use_case": "ํ‘œ์ง€, ํƒ€์ดํ‹€",
159
- "example": "A dramatic wide-angle view of a modern glass skyscraper reaching into clouds with golden sunset lighting, symbolizing growth and ambition. Ultra-realistic photography style, cinematic composition, lens flare, professional corporate aesthetic"
160
- },
161
- "Thank You Slide": {
162
- "name": "Closing Slide",
163
- "description": "Elegant closing slide design with conclusion",
164
- "use_case": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋งˆ๋ฌด๋ฆฌ",
165
- "example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for conclusion text. Minimalist, professional, warm feeling"
166
- },
167
- "3D Style (Pixar-like)": {
168
- "name": "3D Style",
169
- "description": "Pixar-esque 3D render with volumetric lighting",
170
- "use_case": "ํ‘œ์ง€, ๋น„์ „, ๋ฏธ๋ž˜ ์ปจ์…‰",
171
- "example": "A fluffy ginger cat wearing a tiny spacesuit, floating amidst a vibrant nebula in a 3D render. The cat is gazing curiously at a swirling planet with rings made of candy. Background is filled with sparkling stars and colorful gas clouds, lit with soft, volumetric lighting. Style: Pixar-esque, highly detailed, playful. Colors: Deep blues, purples, oranges, and pinks. Rendered in Octane, 8k resolution."
172
- },
173
- "Elegant SWOT Quadrant": {
174
- "name": "SWOT Analysis",
175
- "description": "Flat-design 4-grid layout with minimal shadows",
176
- "use_case": "ํ˜„ํ™ฉ ๋ถ„์„, ์ „๋žต ํ‰๊ฐ€",
177
- "example": "Elegant SWOT quadrant: flat-design 4-grid on matte-white backdrop, thin pastel separators, top-left 'Strengths' panel shows glowing shield icon and subtle motif, top-right 'Weaknesses' panel with cracked chain icon in soft crimson, bottom-left 'Opportunities' panel with sunrise-over-horizon icon in optimistic teal, bottom-right 'Threats' panel with storm-cloud & lightning icon in deep indigo, minimal shadows, no text, no watermark, 16:9, 4K"
178
- },
179
- "Colorful Mind Map": {
180
- "name": "Mind Map",
181
- "description": "Hand-drawn educational style with vibrant colors",
182
- "use_case": "๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ, ์•„์ด๋””์–ด ์ •๋ฆฌ",
183
- "example": "A handrawn colorful mind map diagram: educational style, vibrant colors, clear hierarchy, golden ratio layout. Central concept with branching sub-topics, each branch with unique color coding, organic flowing connections, doodle-style icons for each node"
184
- },
185
- "Business Workflow": {
186
- "name": "Business Process",
187
- "description": "End-to-end business workflow with clear phases",
188
- "use_case": "ํ”„๋กœ์„ธ์Šค ์„ค๋ช…, ๋‹จ๊ณ„๋ณ„ ์ง„ํ–‰",
189
- "example": "A detailed hand-drawn diagram illustrating an end-to-end business workflow with Market Analysis, Strategy Development, Product Design, Implementation, and Post-Launch Review phases. Clear directional arrows, iconography for each component, vibrant educational yet professional style",
190
- "is_process_flow": True # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์‚ฌ์šฉ ํ‘œ์‹œ
191
- },
192
- "Industrial Design": {
193
- "name": "Product Design",
194
- "description": "Sleek industrial design concept sketch",
195
- "use_case": "์ œํ’ˆ ์†Œ๊ฐœ, ์ปจ์…‰ ๋””์ž์ธ",
196
- "example": "A sleek industrial design concept: Curved metallic body with minimal bezel, Touchscreen panel for settings, Modern matte black finish, Hand-drawn concept sketch style with annotations and dimension lines"
197
- },
198
- "3D Bubble Chart": {
199
- "name": "Bubble Chart",
200
- "description": "Clean 3D bubble visualization",
201
- "use_case": "๋น„๊ต ๋ถ„์„, ํฌ์ง€์…”๋‹",
202
- "example": "3-D bubble chart on clean white 2ร—2 grid, quadrant titles hidden, four translucent spheres in lime, azure, amber, magenta, gentle depth-of-field, modern consulting aesthetic, no text, 4K"
203
- },
204
- "Timeline Ribbon": {
205
- "name": "Timeline",
206
- "description": "Horizontal ribbon timeline with cyber-futuristic vibe",
207
- "use_case": "์ผ์ •, ๋กœ๋“œ๋งต, ๋งˆ์ผ์Šคํ†ค",
208
- "example": "Horizontal ribbon timeline, milestone pins glowing hot pink on charcoal, year markers as circles, faint motion streaks, cyber-futuristic vibe, no text, 1920ร—1080"
209
- },
210
- "Risk Heat Map": {
211
- "name": "Heat Map",
212
- "description": "Risk assessment heat map with gradient colors",
213
- "use_case": "๋ฆฌ์Šคํฌ ๋ถ„์„, ์šฐ์„ ์ˆœ์œ„",
214
- "example": "Risk Heat Map: square grid, smooth gradient from mint to fire-red, cells beveled, simple legend strip hidden, long subtle shadow, sterile white frame, no text"
215
- },
216
- "Pyramid/Funnel": {
217
- "name": "Funnel Chart",
218
- "description": "Multi-layer gradient funnel visualization",
219
- "use_case": "๋‹จ๊ณ„๋ณ„ ์ถ•์†Œ, ํ•ต์‹ฌ ๋„์ถœ",
220
- "example": "Pyramid / Funnel: 5-layer gradient funnel narrowing downwards, top vivid sky-blue, mid mint-green, bottom sunset-orange, glass reflection, minimal background, no text"
221
- },
222
- "KPI Dashboard": {
223
- "name": "Dashboard",
224
- "description": "Dark-mode analytics dashboard with sci-fi interface",
225
- "use_case": "์„ฑ๊ณผ ์ง€ํ‘œ, ์‹ค์  ๋Œ€์‹œ๋ณด๋“œ",
226
- "example": "KPI Dashboard: Dark-mode analytic dashboard, three glass speedometers glowing neon lime, two sparkline charts under, black glass background, sci-fi interface, no text, 4K"
227
- },
228
- "Value Chain": {
229
- "name": "Value Chain",
230
- "description": "Horizontal value chain with industrial look",
231
- "use_case": "๊ฐ€์น˜ ์‚ฌ์Šฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ",
232
- "example": "Value Chain Diagram: Horizontal value chain blocks, steel-blue gradient bars with subtle bevel, small gear icons above each segment, sleek industrial look, shadow cast, no text"
233
- },
234
- "Gantt Chart": {
235
- "name": "Gantt Chart",
236
- "description": "Hand-drawn style Gantt chart with playful colors",
237
- "use_case": "ํ”„๋กœ์ ํŠธ ์ผ์ •, ์ž‘์—… ๊ด€๋ฆฌ",
238
- "example": "Gantt Chart: Hand-drawn style Gantt bars sketched with vibrant markers on dotted grid notebook page, sticky-note color palette, playful yet organized, perspective tilt, no text"
239
- },
240
- "Mobile App Mockup": {
241
- "name": "App Mockup",
242
- "description": "Clean wireframe for mobile app design",
243
- "use_case": "์•ฑ/์›น UI, ํ™”๋ฉด ์„ค๊ณ„",
244
- "example": "MOCKUP DESIGN: A clean hand-drawn style wireframe for a mobile app with Title screen, Login screen, Dashboard with sections, Bottom navigation bar, minimalist design with annotations"
245
- },
246
- "Flowchart": {
247
- "name": "Flowchart",
248
- "description": "Vibrant flowchart with minimalistic icons",
249
- "use_case": "์˜์‚ฌ๊ฒฐ์ •, ํ”„๋กœ์„ธ์Šค ํ๋ฆ„",
250
- "example": "FLOWCHART DESIGN: A hand-drawn style flowchart, vibrant colors, minimalistic icons showing process flow from START to END with decision points, branches, and clear directional arrows",
251
- "is_process_flow": True # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์‚ฌ์šฉ ํ‘œ์‹œ
252
- }
253
- }
254
-
255
- # PPT ํ…œํ”Œ๋ฆฟ ์ •์˜ (๋™์ ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ • ๊ฐ€๋Šฅ)
256
- # PPT_TEMPLATES ์ˆ˜์ • - ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ๋Š” ํŠน์ • ์Šฌ๋ผ์ด๋“œ์—๋งŒ ์ ์šฉ
257
- PPT_TEMPLATES = {
258
- "๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ": {
259
- "description": "ํˆฌ์ž ์œ ์น˜, ์‚ฌ์—… ์ œ์•ˆ์šฉ",
260
- "core_slides": [
261
- {"title": "๋ชฉ์ฐจ", "style": "Flowchart", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์กฐ"}, # ์ด๊ฑด ๋ชฉ์ฐจ์šฉ ํ”Œ๋กœ์šฐ์ฐจํŠธ
262
- {"title": "๋ฌธ์ œ ์ •์˜", "style": "Colorful Mind Map", "prompt_hint": "ํ˜„์žฌ ์‹œ์žฅ์˜ ๋ฌธ์ œ์ "},
263
- {"title": "ํ˜„ํ™ฉ ๋ถ„์„", "style": "Elegant SWOT Quadrant", "prompt_hint": "๊ฐ•์ , ์•ฝ์ , ๊ธฐํšŒ, ์œ„ํ˜‘"},
264
- {"title": "์†”๋ฃจ์…˜", "style": "Industrial Design", "prompt_hint": "์ œํ’ˆ/์„œ๋น„์Šค ์ปจ์…‰"},
265
- {"title": "ํ”„๋กœ์„ธ์Šค", "style": "Business Workflow", "prompt_hint": "์‹คํ–‰ ๋‹จ๊ณ„"}, # ์—ฌ๊ธฐ์„œ๋งŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ
266
- {"title": "์ผ์ •", "style": "Timeline Ribbon", "prompt_hint": "์ฃผ์š” ๋งˆ์ผ์Šคํ†ค"}
267
- ],
268
- "optional_slides": [
269
- {"title": "์‹œ์žฅ ๊ทœ๋ชจ", "style": "3D Bubble Chart", "prompt_hint": "์‹œ์žฅ ๊ธฐํšŒ์™€ ์„ฑ์žฅ์„ฑ"},
270
- {"title": "๊ฒฝ์Ÿ ๋ถ„์„", "style": "Risk Heat Map", "prompt_hint": "๊ฒฝ์Ÿ์‚ฌ ํฌ์ง€์…”๋‹"},
271
- {"title": "๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ", "style": "Value Chain", "prompt_hint": "์ˆ˜์ต ๊ตฌ์กฐ"},
272
- {"title": "ํŒ€ ์†Œ๊ฐœ", "style": "Colorful Mind Map", "prompt_hint": "ํ•ต์‹ฌ ํŒ€์›๊ณผ ์—ญ๋Ÿ‰"},
273
- {"title": "์žฌ๋ฌด ๊ณ„ํš", "style": "KPI Dashboard", "prompt_hint": "์˜ˆ์ƒ ๋งค์ถœ๊ณผ ์†์ต"},
274
- {"title": "์œ„ํ—˜ ๊ด€๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ฃผ์š” ๋ฆฌ์Šคํฌ์™€ ๋Œ€์‘"},
275
- {"title": "ํŒŒํŠธ๋„ˆ์‹ญ", "style": "Value Chain", "prompt_hint": "์ „๋žต์  ์ œํœด"}, # Business Workflow ๋Œ€์‹  Value Chain
276
- {"title": "๊ธฐ์ˆ  ์Šคํƒ", "style": "Industrial Design", "prompt_hint": "ํ•ต์‹ฌ ๊ธฐ์ˆ  ๊ตฌ์กฐ"}, # Flowchart ๋Œ€์‹ 
277
- {"title": "๊ณ ๊ฐ ์‚ฌ๋ก€", "style": "Industrial Design", "prompt_hint": "์„ฑ๊ณต ์‚ฌ๋ก€"},
278
- {"title": "์„ฑ์žฅ ์ „๋žต", "style": "Timeline Ribbon", "prompt_hint": "ํ™•์žฅ ๊ณ„ํš"},
279
- {"title": "ํˆฌ์ž ํ™œ์šฉ", "style": "Pyramid/Funnel", "prompt_hint": "์ž๊ธˆ ์‚ฌ์šฉ ๊ณ„ํš"},
280
- {"title": "Exit ์ „๋žต", "style": "Timeline Ribbon", "prompt_hint": "์ถœ๊ตฌ ์ „๋žต"}
281
- ]
282
- },
283
- "์ œํ’ˆ ์†Œ๊ฐœ": {
284
- "description": "์‹ ์ œํ’ˆ ๋Ÿฐ์นญ, ์„œ๋น„์Šค ์†Œ๊ฐœ์šฉ",
285
- "core_slides": [
286
- {"title": "์ œํ’ˆ ์ปจ์…‰", "style": "Industrial Design", "prompt_hint": "์ œํ’ˆ ๋””์ž์ธ"},
287
- {"title": "์‚ฌ์šฉ์ž ๋‹ˆ์ฆˆ", "style": "Colorful Mind Map", "prompt_hint": "๊ณ ๊ฐ ํŽ˜์ธํฌ์ธํŠธ"},
288
- {"title": "๊ธฐ๋Šฅ ์†Œ๊ฐœ", "style": "Mobile App Mockup", "prompt_hint": "UI/UX ํ™”๋ฉด"},
289
- {"title": "์ž‘๋™ ์›๋ฆฌ", "style": "Flowchart", "prompt_hint": "๊ธฐ๋Šฅ ํ”Œ๋กœ์šฐ"}, # ์—ฌ๊ธฐ์„œ๋งŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ
290
- {"title": "์‹œ์žฅ ํฌ์ง€์…˜", "style": "3D Bubble Chart", "prompt_hint": "๊ฒฝ์Ÿ์‚ฌ ๋น„๊ต"},
291
- {"title": "์ถœ์‹œ ์ผ์ •", "style": "Timeline Ribbon", "prompt_hint": "๋Ÿฐ์นญ ๋กœ๋“œ๋งต"}
292
- ],
293
- "optional_slides": [
294
- {"title": "ํƒ€๊ฒŸ ๊ณ ๊ฐ", "style": "Colorful Mind Map", "prompt_hint": "์ฃผ์š” ๊ณ ๊ฐ์ธต"},
295
- {"title": "๊ฐ€๊ฒฉ ์ •์ฑ…", "style": "Pyramid/Funnel", "prompt_hint": "๊ฐ€๊ฒฉ ์ „๋žต"},
296
- {"title": "๊ธฐ์ˆ  ์šฐ์œ„", "style": "Industrial Design", "prompt_hint": "ํ•ต์‹ฌ ๊ธฐ์ˆ "},
297
- {"title": "์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค", "style": "Mobile App Mockup", "prompt_hint": "ํ™œ์šฉ ์‚ฌ๋ก€"}, # Business Workflow ๋Œ€์‹ 
298
- {"title": "๊ณ ๊ฐ ํ›„๊ธฐ", "style": "KPI Dashboard", "prompt_hint": "์‚ฌ์šฉ์ž ํ‰๊ฐ€"},
299
- {"title": "ํŒ๋งค ์ฑ„๋„", "style": "Value Chain", "prompt_hint": "์œ ํ†ต ์ „๋žต"},
300
- {"title": "๋งˆ์ผ€ํŒ… ์ „๋žต", "style": "Timeline Ribbon", "prompt_hint": "ํ™๋ณด ๊ณ„ํš"},
301
- {"title": "์„ฑ๋Šฅ ๋น„๊ต", "style": "3D Bubble Chart", "prompt_hint": "๋ฒค์น˜๋งˆํฌ"}
302
- ]
303
- },
304
- "ํ”„๋กœ์ ํŠธ ๋ณด๊ณ ": {
305
- "description": "์ง„ํ–‰ ์ƒํ™ฉ, ์„ฑ๊ณผ ๋ณด๊ณ ์šฉ",
306
- "core_slides": [
307
- {"title": "ํ”„๋กœ์ ํŠธ ๊ฐœ์š”", "style": "Business Workflow", "prompt_hint": "์ „์ฒด ํ”„๋กœ์„ธ์Šค"}, # ์—ฌ๊ธฐ์„œ๋งŒ
308
- {"title": "์ง„ํ–‰ ํ˜„ํ™ฉ", "style": "Gantt Chart", "prompt_hint": "์ž‘์—… ์ผ์ •"},
309
- {"title": "๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์œ„ํ—˜ ์š”์†Œ"},
310
- {"title": "์„ฑ๊ณผ ์ง€ํ‘œ", "style": "KPI Dashboard", "prompt_hint": "๋‹ฌ์„ฑ ์‹ค์ "},
311
- {"title": "ํ–ฅํ›„ ๊ณ„ํš", "style": "Timeline Ribbon", "prompt_hint": "๋‹ค์Œ ๋‹จ๊ณ„"}
312
- ],
313
- "optional_slides": [
314
- {"title": "์˜ˆ์‚ฐ ํ˜„ํ™ฉ", "style": "Pyramid/Funnel", "prompt_hint": "์˜ˆ์‚ฐ ์ง‘ํ–‰"},
315
- {"title": "ํŒ€ ์„ฑ๊ณผ", "style": "3D Bubble Chart", "prompt_hint": "ํŒ€๋ณ„ ๊ธฐ์—ฌ๋„"},
316
- {"title": "์ด์Šˆ ๊ด€๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ฃผ์š” ์ด์Šˆ"},
317
- {"title": "๊ฐœ์„  ์‚ฌํ•ญ", "style": "Colorful Mind Map", "prompt_hint": "ํ”„๋กœ์„ธ์Šค ๊ฐœ์„ "},
318
- {"title": "๊ตํ›ˆ", "style": "Colorful Mind Map", "prompt_hint": "๋ฐฐ์šด ์ "} # Business Workflow ๋Œ€์‹ 
319
- ]
320
- },
321
- "์ „๋žต ๊ธฐํš": {
322
- "description": "์ค‘์žฅ๊ธฐ ์ „๋žต, ๋น„์ „ ์ˆ˜๋ฆฝ์šฉ",
323
- "core_slides": [
324
- {"title": "๋น„์ „", "style": "3D Style (Pixar-like)", "prompt_hint": "๋ฏธ๋ž˜ ๋น„์ „"},
325
- {"title": "ํ™˜๊ฒฝ ๋ถ„์„", "style": "Elegant SWOT Quadrant", "prompt_hint": "๋‚ด์™ธ๋ถ€ ํ™˜๊ฒฝ"},
326
- {"title": "์ „๋žต ์ฒด๊ณ„", "style": "Colorful Mind Map", "prompt_hint": "์ „๋žต ๊ตฌ์กฐ"},
327
- {"title": "๊ฐ€์น˜ ์‚ฌ์Šฌ", "style": "Value Chain", "prompt_hint": "๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ"},
328
- {"title": "์‹คํ–‰ ๋กœ๋“œ๋งต", "style": "Timeline Ribbon", "prompt_hint": "๋‹จ๊ณ„๋ณ„ ๊ณ„ํš"},
329
- {"title": "๋ชฉํ‘œ ์ง€ํ‘œ", "style": "KPI Dashboard", "prompt_hint": "KPI ๋ชฉํ‘œ"}
330
- ],
331
- "optional_slides": [
332
- {"title": "์‹œ์žฅ ์ „๋ง", "style": "3D Bubble Chart", "prompt_hint": "๋ฏธ๋ž˜ ์‹œ์žฅ"},
333
- {"title": "ํ˜์‹  ๋ฐฉํ–ฅ", "style": "Industrial Design", "prompt_hint": "ํ˜์‹  ์ „๋žต"},
334
- {"title": "์กฐ์ง ๋ณ€ํ™”", "style": "Value Chain", "prompt_hint": "์กฐ์ง ๊ฐœํŽธ"}, # Business Workflow ๋Œ€์‹ 
335
- {"title": "๋””์ง€ํ„ธ ์ „ํ™˜", "style": "Industrial Design", "prompt_hint": "DX ์ „๋žต"}, # Flowchart ๋Œ€์‹ 
336
- {"title": "์ง€์†๊ฐ€๋Šฅ์„ฑ", "style": "Timeline Ribbon", "prompt_hint": "ESG ์ „๋žต"}
337
- ]
338
- },
339
- "์‚ฌ์šฉ์ž ์ •์˜": {
340
- "description": "์ง์ ‘ ๊ตฌ์„ฑํ•˜๊ธฐ",
341
- "core_slides": [],
342
- "optional_slides": []
343
- }
344
- }
345
-
346
- def brave_search(query: str) -> List[Dict]:
347
- """Brave Search API๋ฅผ ์‚ฌ์šฉํ•œ ์›น ๊ฒ€์ƒ‰"""
348
- if not BRAVE_API_TOKEN:
349
- print("[Brave Search] API ํ† ํฐ์ด ์—†์–ด ๊ฒ€์ƒ‰์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
350
- return []
351
-
352
- print(f"[Brave Search] ๊ฒ€์ƒ‰์–ด: {query}")
353
-
354
- headers = {
355
- "Accept": "application/json",
356
- "X-Subscription-Token": BRAVE_API_TOKEN
357
- }
358
-
359
- params = {
360
- "q": query,
361
- "count": 5
362
- }
363
-
364
- try:
365
- response = requests.get(
366
- "https://api.search.brave.com/res/v1/web/search",
367
- headers=headers,
368
- params=params,
369
- timeout=10
370
- )
371
-
372
- if response.status_code == 200:
373
- data = response.json()
374
- results = []
375
- for item in data.get("web", {}).get("results", [])[:3]:
376
- results.append({
377
- "title": item.get("title", ""),
378
- "description": item.get("description", ""),
379
- "url": item.get("url", "")
380
- })
381
- print(f"[Brave Search] {len(results)}๊ฐœ ๊ฒฐ๊ณผ ํš๋“")
382
- return results
383
- else:
384
- print(f"[Brave Search] ์˜ค๋ฅ˜: {response.status_code}")
385
- return []
386
- except Exception as e:
387
- print(f"[Brave Search] ์˜ˆ์™ธ: {str(e)}")
388
- return []
389
-
390
- def read_uploaded_file(file_path: str) -> str:
391
- """์—…๋กœ๋“œ๋œ ํŒŒ์ผ ์ฝ๊ธฐ (PDF, CSV, TXT)"""
392
- print(f"[ํŒŒ์ผ ์ฝ๊ธฐ] {file_path}")
393
-
394
- try:
395
- # ํŒŒ์ผ ํ™•์žฅ์ž ํ™•์ธ
396
- ext = os.path.splitext(file_path)[1].lower()
397
-
398
- if ext == '.pdf':
399
- # PDF ์ฝ๊ธฐ
400
- with open(file_path, 'rb') as file:
401
- pdf_reader = PyPDF2.PdfReader(file)
402
- text = ""
403
- for page in pdf_reader.pages:
404
- text += page.extract_text() + "\n"
405
- return text[:5000] # ์ตœ๋Œ€ 5000์ž
406
-
407
- elif ext == '.csv':
408
- # CSV ์ฝ๊ธฐ
409
- # ์ธ์ฝ”๋”ฉ ๊ฐ์ง€
410
- with open(file_path, 'rb') as file:
411
- raw_data = file.read()
412
- result = chardet.detect(raw_data)
413
- encoding = result['encoding'] or 'utf-8'
414
-
415
- df = pd.read_csv(file_path, encoding=encoding)
416
- return f"CSV ๋ฐ์ดํ„ฐ:\n{df.head(20).to_string()}\n\n์š”์•ฝ: {len(df)} ํ–‰, {len(df.columns)} ์—ด"
417
-
418
- elif ext in ['.txt', '.text']:
419
- # ํ…์ŠคํŠธ ํŒŒ์ผ ์ฝ๊ธฐ
420
- with open(file_path, 'rb') as file:
421
- raw_data = file.read()
422
- result = chardet.detect(raw_data)
423
- encoding = result['encoding'] or 'utf-8'
424
-
425
- with open(file_path, 'r', encoding=encoding) as file:
426
- return file.read()[:5000] # ์ตœ๋Œ€ 5000์ž
427
- else:
428
- return "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค."
429
-
430
- except Exception as e:
431
- return f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜: {str(e)}"
432
-
433
- def generate_presentation_notes(topic: str, slide_title: str, content: Dict, audience_type: str) -> str:
434
- """๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ƒ์„ฑ (๊ตฌ์–ด์ฒด)"""
435
- print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] {slide_title} ์ƒ์„ฑ ์ค‘...")
436
-
437
- # ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
438
- audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
439
-
440
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
441
- headers = {
442
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
443
- "Content-Type": "application/json"
444
- }
445
-
446
- system_prompt = f"""You are a professional presentation coach who creates natural, conversational speaker notes.
447
-
448
- The audience for this presentation is: {audience_type} - {audience_info['description']}
449
- Tone: {audience_info['tone']}
450
- Focus areas: {audience_info['focus']}
451
-
452
- The slide content uses concise noun-ending style, but your speaker notes should be natural and conversational.
453
-
454
- Create speaker notes that:
455
- 1. Sound natural and conversational, as if speaking to {audience_type}
456
- 2. Expand on the concise bullet points with additional context relevant to this audience
457
- 3. Include transitions and engagement phrases appropriate for {audience_type}
458
- 4. Use a warm, professional tone suitable for {audience_type}
459
- 5. Be 100-150 words long
460
- 6. Include pauses and emphasis markers where appropriate
461
-
462
- Note: The bullet points may include emojis - incorporate their meaning into your speech.
463
-
464
- Format:
465
- - Use conversational language appropriate for {audience_type}
466
- - Include transition phrases
467
- - Add engagement questions or comments that resonate with this audience
468
- - Keep it professional but friendly"""
469
-
470
- bullet_text = "\n".join(content.get("bullet_points", []))
471
- user_message = f"""Topic: {topic}
472
- Slide Title: {slide_title}
473
- Subtitle: {content.get('subtitle', '')}
474
- Key Points (concise style with emojis):
475
- {bullet_text}
476
-
477
- Create natural speaker notes that expand on these concise points for presenting this slide to {audience_type}."""
478
-
479
- payload = {
480
- "model": "dep89a2fld32mcm",
481
- "messages": [
482
- {
483
- "role": "system",
484
- "content": system_prompt
485
- },
486
- {
487
- "role": "user",
488
- "content": user_message
489
- }
490
- ],
491
- "max_tokens": 300,
492
- "temperature": 0.8,
493
- "stream": False
494
- }
495
-
496
- try:
497
- response = requests.post(url, json=payload, headers=headers, timeout=30)
498
- if response.status_code == 200:
499
- result = response.json()
500
- notes = result['choices'][0]['message']['content'].strip()
501
-
502
- # ํ•œ๊ธ€ ์ฃผ์ œ์ธ ๊ฒฝ์šฐ ๋ฒˆ๏ฟฝ๏ฟฝ
503
- if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
504
- notes = translate_content_to_korean_natural(notes)
505
-
506
- return notes
507
- else:
508
- return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
509
- except Exception as e:
510
- print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
511
- return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
512
-
513
- def generate_closing_notes(topic: str, conclusion_phrase: str, audience_type: str) -> str:
514
- """๋งˆ์ง€๋ง‰ ์Šฌ๋ผ์ด๋“œ์˜ ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ƒ์„ฑ"""
515
- print(f"[๋งˆ๋ฌด๋ฆฌ ๋…ธํŠธ] ์ƒ์„ฑ ์ค‘...")
516
-
517
- # ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
518
- audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
519
-
520
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
521
- headers = {
522
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
523
- "Content-Type": "application/json"
524
- }
525
-
526
- system_prompt = f"""You are a professional presentation coach creating closing speaker notes.
527
-
528
- The audience for this presentation is: {audience_type} - {audience_info['description']}
529
- Tone: {audience_info['tone']}
530
-
531
- Create natural closing remarks that:
532
- 1. Thank the {audience_type} audience warmly and appropriately
533
- 2. Briefly summarize the key message relevant to {audience_type}
534
- 3. Reference the conclusion phrase naturally
535
- 4. End with an invitation for questions or next steps appropriate for {audience_type}
536
- 5. Be 80-100 words long
537
- 6. Sound conversational and warm
538
- 7. NO stage directions or parentheses - only spoken words
539
-
540
- Write only what the speaker would say out loud."""
541
-
542
- user_message = f"""Presentation topic: {topic}
543
- Audience: {audience_type}
544
- Conclusion phrase on screen: {conclusion_phrase}
545
-
546
- Create natural closing speaker notes that wrap up the presentation effectively for {audience_type}."""
547
-
548
- payload = {
549
- "model": "dep89a2fld32mcm",
550
- "messages": [
551
- {
552
- "role": "system",
553
- "content": system_prompt
554
- },
555
- {
556
- "role": "user",
557
- "content": user_message
558
- }
559
- ],
560
- "max_tokens": 200,
561
- "temperature": 0.8,
562
- "stream": False
563
- }
564
-
565
- try:
566
- response = requests.post(url, json=payload, headers=headers, timeout=30)
567
- if response.status_code == 200:
568
- result = response.json()
569
- notes = result['choices'][0]['message']['content'].strip()
570
-
571
- # ํ•œ๊ธ€ ์ฃผ์ œ์ธ ๊ฒฝ์šฐ ๋ฒˆ์—ญ
572
- if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
573
- notes = translate_content_to_korean_natural(notes)
574
-
575
- return notes
576
- else:
577
- return "์˜ค๋Š˜ ๋ฐœํ‘œ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ๊ฐ์‚ฌ์˜ ๋ง์”€์„ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ํŽธํ•˜๊ฒŒ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”."
578
- except Exception as e:
579
- print(f"[๋งˆ๋ฌด๋ฆฌ ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
580
- return "์˜ค๋Š˜ ๋ฐœํ‘œ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ๊ฐ์‚ฌ์˜ ๋ง์”€์„ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ํŽธํ•˜๊ฒŒ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”."
581
-
582
- def generate_conclusion_phrase(topic: str, audience_type: str) -> str:
583
- """ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ํ•ต์‹ฌ์„ ๋‹ด์€ ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์ƒ์„ฑ"""
584
- print(f"[๊ฒฐ๋ก  ๋ฌธ๊ตฌ] ์ƒ์„ฑ ์ค‘...")
585
-
586
- # ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
587
- audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
588
-
589
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
590
- headers = {
591
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
592
- "Content-Type": "application/json"
593
- }
594
-
595
- system_prompt = f"""You are a professional copywriter creating powerful closing statements for presentations.
596
-
597
- The audience is: {audience_type} - {audience_info['description']}
598
- Focus on: {audience_info['focus']}
599
-
600
- Create a concise, impactful closing phrase that:
601
- 1. Captures the essence of the presentation topic
602
- 2. Resonates with {audience_type}
603
- 3. Is memorable and inspirational
604
- 4. Maximum 5-7 words
605
- 5. Uses powerful, action-oriented language appropriate for {audience_type}
606
- 6. Leaves a lasting impression
607
-
608
- Examples for different audiences:
609
- - For executives: "Excellence Through Strategic Innovation"
610
- - For investors: "Maximum Returns, Minimal Risk"
611
- - For technical teams: "Code Today, Transform Tomorrow"
612
- - For customers: "Your Success, Our Mission"
613
-
614
- Output only the phrase, no explanation."""
615
-
616
- user_message = f"""Presentation topic: {topic}
617
- Target audience: {audience_type}
618
-
619
- Create a powerful closing phrase that encapsulates the main message for {audience_type}."""
620
-
621
- payload = {
622
- "model": "dep89a2fld32mcm",
623
- "messages": [
624
- {
625
- "role": "system",
626
- "content": system_prompt
627
- },
628
- {
629
- "role": "user",
630
- "content": user_message
631
- }
632
- ],
633
- "max_tokens": 50,
634
- "temperature": 0.9,
635
- "stream": False
636
- }
637
-
638
- try:
639
- response = requests.post(url, json=payload, headers=headers, timeout=30)
640
- if response.status_code == 200:
641
- result = response.json()
642
- phrase = result['choices'][0]['message']['content'].strip()
643
-
644
- # ํ•œ๊ธ€ ์ฃผ์ œ์ธ ๊ฒฝ์šฐ ๋ฒˆ์—ญ
645
- if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
646
- phrase = translate_content_to_korean_concise(phrase)
647
-
648
- return phrase
649
- else:
650
- return "ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ๋ฏธ๋ž˜"
651
- except Exception as e:
652
- print(f"[๊ฒฐ๋ก  ๋ฌธ๊ตฌ] ์˜ค๋ฅ˜: {str(e)}")
653
- return "ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ๋ฏธ๋ž˜"
654
-
655
- def generate_slide_content(topic: str, slide_title: str, slide_context: str, audience_type: str,
656
- uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
657
- """๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ํ…์ŠคํŠธ ๋‚ด์šฉ ์ƒ์„ฑ"""
658
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] {slide_title} ํ…์ŠคํŠธ ์ƒ์„ฑ ์ค‘... (๋Œ€์ƒ: {audience_type})")
659
-
660
- # ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
661
- audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
662
-
663
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
664
- headers = {
665
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
666
- "Content-Type": "application/json"
667
- }
668
-
669
- system_prompt = f"""You are a professional presentation content writer specializing in creating concise, impactful slide content.
670
-
671
- Target Audience: {audience_type} - {audience_info['description']}
672
- Tone: {audience_info['tone']}
673
- Focus: {audience_info['focus']}
674
-
675
- Your task is to create content specifically tailored for {audience_type}:
676
- 1. A compelling subtitle (max 10 words) that resonates with {audience_type}
677
- 2. Exactly 5 bullet points with emojis relevant to {audience_type}'s interests
678
- 3. Each bullet point should be 8-12 words
679
- 4. Use noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ) for Korean or concise fragments for English
680
- 5. Content should address {audience_type}'s specific concerns and interests
681
-
682
- IMPORTANT Style Guidelines:
683
- - End sentences with nouns or concise phrases
684
- - Avoid long verb endings like "์ž…๋‹ˆ๋‹ค", "์Šต๋‹ˆ๋‹ค"
685
- - Use short endings like "์ž„", "ํ•จ" or noun forms
686
- - Start each bullet with a relevant emoji that {audience_type} would appreciate
687
- - Be extremely concise and impactful
688
-
689
- Audience-specific emoji guidelines:
690
- - For executives: ๐Ÿ“Š ๐ŸŽฏ ๐Ÿ’ฐ ๐Ÿ† ๐Ÿš€ ๐Ÿ“ˆ ๐Ÿ” ๐Ÿ’ก
691
- - For investors: ๐Ÿ’ฐ ๐Ÿ“ˆ ๐Ÿ’Ž ๐Ÿฆ ๐Ÿ’ธ ๐Ÿ“Š ๐Ÿš€ ๐Ÿ”’
692
- - For technical teams: ๐Ÿ”ง ๐Ÿ’ป ๐Ÿ› ๏ธ โš™๏ธ ๐Ÿ” ๐ŸŒ ๐Ÿ“ฑ ๐Ÿค–
693
- - For general staff: ๐Ÿค ๐Ÿ’ก ๐Ÿ“‹ โœ… ๐ŸŽฏ ๐ŸŒŸ ๐Ÿ“… ๐Ÿ’ช
694
- - For customers: โญ ๐ŸŽ ๐Ÿ’ ๐Ÿ›ก๏ธ ๐ŸŒŸ โœจ ๐Ÿ… ๐Ÿ‘
695
- - For general public: ๐Ÿ˜Š ๐Ÿ  ๐ŸŒ โค๏ธ ๐ŸŽ‰ ๐ŸŒˆ โœจ ๐ŸŽฏ
696
-
697
- Output format (EXACTLY FOLLOW THIS FORMAT):
698
- Subtitle: [subtitle here]
699
- - ๐ŸŽฏ [Point 1 - tailored for {audience_type}]
700
- - ๐Ÿ“Š [Point 2 - tailored for {audience_type}]
701
- - ๐Ÿ’ก [Point 3 - tailored for {audience_type}]
702
- - ๐Ÿš€ [Point 4 - tailored for {audience_type}]
703
- - โšก [Point 5 - tailored for {audience_type}]"""
704
-
705
- user_message = f"""Topic: {topic}
706
- Slide Title: {slide_title}
707
- Context: {slide_context}
708
- Target Audience: {audience_type}"""
709
-
710
- # ์—…๋กœ๋“œ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
711
- if uploaded_content:
712
- user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
713
-
714
- # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
715
- if web_search_results:
716
- search_context = "\n\nWeb Search Results:\n"
717
- for result in web_search_results[:3]:
718
- search_context += f"- {result['title']}: {result['description']}\n"
719
- user_message += search_context
720
-
721
- user_message += f"\n\nCreate compelling content for this presentation slide specifically tailored for {audience_type}. Remember to use emojis and concise noun-ending style."
722
-
723
- payload = {
724
- "model": "dep89a2fld32mcm",
725
- "messages": [
726
- {
727
- "role": "system",
728
- "content": system_prompt
729
- },
730
- {
731
- "role": "user",
732
- "content": user_message
733
- }
734
- ],
735
- "max_tokens": 300,
736
- "top_p": 0.8,
737
- "temperature": 0.7,
738
- "stream": False
739
- }
740
-
741
- try:
742
- response = requests.post(url, json=payload, headers=headers, timeout=30)
743
- if response.status_code == 200:
744
- result = response.json()
745
- content = result['choices'][0]['message']['content'].strip()
746
-
747
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] LLM ์‘๋‹ต:\n{content}")
748
-
749
- # Parse content - ๋” ๊ฐ•๊ฑดํ•œ ํŒŒ์‹ฑ
750
- lines = content.split('\n')
751
- subtitle = ""
752
- bullet_points = []
753
-
754
- for line in lines:
755
- line = line.strip()
756
- if not line:
757
- continue
758
-
759
- # Subtitle ํŒŒ์‹ฑ
760
- if line.lower().startswith("subtitle:") or line.startswith("Subtitle:"):
761
- subtitle = line.split(':', 1)[1].strip()
762
- # Bullet point ํŒŒ์‹ฑ - โ€ข ๋˜๋Š” - ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ ๋ชจ๋‘ ์ฒ˜๋ฆฌ
763
- elif line.startswith("โ€ข") or line.startswith("-") or (len(line) > 2 and line[1] == ' ' and ord(line[0]) >= 128):
764
- # ์ด๋ฏธ โ€ข ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ, ์—†์œผ๋ฉด ์ถ”๊ฐ€
765
- if not line.startswith("โ€ข"):
766
- line = "โ€ข " + line.lstrip("- ")
767
- bullet_points.append(line)
768
-
769
- # ๋งŒ์•ฝ subtitle์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
770
- if not subtitle:
771
- subtitle = f"{slide_title} ๊ฐœ์š”"
772
-
773
- # ๋งŒ์•ฝ bullet_points๊ฐ€ 5๊ฐœ ๋ฏธ๋งŒ์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€
774
- while len(bullet_points) < 5:
775
- bullet_points.append(f"โ€ข ๐Ÿ“Œ ํฌ์ธํŠธ {len(bullet_points) + 1}")
776
-
777
- # 5๊ฐœ๋งŒ ์„ ํƒ
778
- bullet_points = bullet_points[:5]
779
-
780
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ํŒŒ์‹ฑ๋œ subtitle: {subtitle}")
781
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ํŒŒ์‹ฑ๋œ bullets: {bullet_points}")
782
-
783
- # ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
784
- if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
785
- subtitle = translate_content_to_korean(subtitle)
786
- # ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ์ด๋ชจ์ง€๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฒˆ์—ญ
787
- translated_bullets = []
788
- for point in bullet_points:
789
- # โ€ข ์ œ๊ฑฐํ•˜๊ณ  ๋‚ด์šฉ๋งŒ ์ถ”์ถœ
790
- clean_point = point.replace('โ€ข', '').strip()
791
-
792
- # ์ด๋ชจ์ง€์™€ ํ…์ŠคํŠธ ๋ถ„๋ฆฌ (์ฒซ ๋ฒˆ์งธ ๊ณต๋ฐฑ์„ ๊ธฐ์ค€์œผ๋กœ)
793
- if len(clean_point) > 0 and clean_point[0] in '๐ŸŽฏ๐Ÿ“Š๐Ÿ’ก๐Ÿš€โšก๐Ÿ”๐Ÿ“ˆ๐Ÿ’ฐ๐Ÿ†๐Ÿ”ง๐ŸŒ๐Ÿ”โญ๐ŸŽจ๐Ÿ“ฑ๐Ÿค๐Ÿ“๐ŸŽ–๏ธ๐Ÿ—๏ธ๐ŸŒฑ๐Ÿ’ป๐Ÿ› ๏ธโš™๏ธ๐Ÿค–๐Ÿ“‹โœ…๐ŸŒŸ๐Ÿ“…๐Ÿ’ช๐ŸŽ๐Ÿ’๐Ÿ›ก๏ธโœจ๐Ÿ…๐Ÿ‘๐Ÿ˜Š๐Ÿ ๐ŸŒโค๏ธ๐ŸŽ‰๐ŸŒˆ':
794
- # ์ด๋ชจ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
795
- emoji = clean_point[0]
796
- text = clean_point[1:].strip()
797
- translated_text = translate_content_to_korean_concise(text)
798
- translated_bullets.append(f"โ€ข {emoji} {translated_text}")
799
- else:
800
- # ์ด๋ชจ์ง€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
801
- translated_bullets.append(f"โ€ข {translate_content_to_korean_concise(clean_point)}")
802
-
803
- bullet_points = translated_bullets
804
-
805
- return {
806
- "subtitle": subtitle,
807
- "bullet_points": bullet_points
808
- }
809
- else:
810
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] API ์˜ค๋ฅ˜: {response.status_code}")
811
- return {
812
- "subtitle": slide_title,
813
- "bullet_points": ["โ€ข ๐Ÿ“Œ ๋‚ด์šฉ ์ƒ์„ฑ ๋ถˆ๊ฐ€"] * 5
814
- }
815
- except Exception as e:
816
- print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ์˜ค๋ฅ˜: {str(e)}")
817
- return {
818
- "subtitle": slide_title,
819
- "bullet_points": ["โ€ข โŒ ๋‚ด์šฉ ์ƒ์„ฑ ์˜ค๋ฅ˜"] * 5
820
- }
821
-
822
- def translate_content_to_korean(text: str) -> str:
823
- """์˜์–ด ํ…์ŠคํŠธ๋ฅผ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ (์ผ๋ฐ˜์šฉ)"""
824
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
825
- headers = {
826
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
827
- "Content-Type": "application/json"
828
- }
829
-
830
- payload = {
831
- "model": "dep89a2fld32mcm",
832
- "messages": [
833
- {
834
- "role": "system",
835
- "content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation."
836
- },
837
- {
838
- "role": "user",
839
- "content": text
840
- }
841
- ],
842
- "max_tokens": 200,
843
- "top_p": 0.8,
844
- "stream": False
845
- }
846
-
847
- try:
848
- response = requests.post(url, json=payload, headers=headers, timeout=30)
849
- if response.status_code == 200:
850
- result = response.json()
851
- return result['choices'][0]['message']['content'].strip()
852
- else:
853
- return text
854
- except Exception as e:
855
- return text
856
-
857
- def translate_content_to_korean_concise(text: str) -> str:
858
- """์˜์–ด ํ…์ŠคํŠธ๋ฅผ ๊ฐ„๊ฒฐํ•œ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ)"""
859
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
860
- headers = {
861
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
862
- "Content-Type": "application/json"
863
- }
864
-
865
- payload = {
866
- "model": "dep89a2fld32mcm",
867
- "messages": [
868
- {
869
- "role": "system",
870
- "content": """You are a translator specializing in concise Korean business presentations.
871
- Translate to Korean using noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ์–ด๋ฏธ).
872
- End with "์ž„", "ํ•จ", or noun forms instead of "์ž…๋‹ˆ๋‹ค", "์Šต๋‹ˆ๋‹ค".
873
- Keep it extremely concise and professional.
874
- Examples: "์ „๋žต์  ํ™•๋Œ€", "ํ•ต์‹ฌ ๊ณผ์ œ ๋„์ถœ", "์‹œ์žฅ ์„ ๋„ ์ „๋žต ์ˆ˜๋ฆฝ"
875
- Only return the translation without any explanation."""
876
- },
877
- {
878
- "role": "user",
879
- "content": text
880
- }
881
- ],
882
- "max_tokens": 200,
883
- "top_p": 0.8,
884
- "stream": False
885
- }
886
-
887
- try:
888
- response = requests.post(url, json=payload, headers=headers, timeout=30)
889
- if response.status_code == 200:
890
- result = response.json()
891
- return result['choices'][0]['message']['content'].strip()
892
- else:
893
- return text
894
- except Exception as e:
895
- return text
896
-
897
- def translate_content_to_korean_natural(text: str) -> str:
898
- """์˜์–ด ํ…์ŠคํŠธ๋ฅผ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ (๋ฐœํ‘œ ๋…ธํŠธ์šฉ)"""
899
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
900
- headers = {
901
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
902
- "Content-Type": "application/json"
903
- }
904
-
905
- payload = {
906
- "model": "dep89a2fld32mcm",
907
- "messages": [
908
- {
909
- "role": "system",
910
- "content": """You are a translator. Translate the given English text to natural, conversational Korean.
911
- Use polite spoken language suitable for presentations.
912
- Only return the translation without any explanation."""
913
- },
914
- {
915
- "role": "user",
916
- "content": text
917
- }
918
- ],
919
- "max_tokens": 300,
920
- "top_p": 0.8,
921
- "stream": False
922
- }
923
-
924
- try:
925
- response = requests.post(url, json=payload, headers=headers, timeout=30)
926
- if response.status_code == 200:
927
- result = response.json()
928
- return result['choices'][0]['message']['content'].strip()
929
- else:
930
- return text
931
- except Exception as e:
932
- return text
933
-
934
- def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str:
935
- """์ฃผ์ œ์™€ ์Šคํƒ€์ผ ์˜ˆ์ œ๋ฅผ ๋ฐ›์•„์„œ LLM์„ ์‚ฌ์šฉํ•ด ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑ"""
936
- print(f"[LLM] ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹œ์ž‘: {slide_context}")
937
-
938
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
939
- headers = {
940
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
941
- "Content-Type": "application/json"
942
- }
943
-
944
- system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides.
945
-
946
- Your task is to create prompts that:
947
- 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals
948
- 2. Consider the slide's purpose and maintain consistency across a presentation
949
- 3. Include style references matching the given example
950
- 4. Focus on clean, professional visuals that won't distract from text overlays
951
- 5. Ensure high contrast areas for text readability when needed
952
- 6. Maintain brand consistency and professional aesthetics
953
-
954
- Important guidelines:
955
- - If given a style example, adapt the topic to match that specific visual style
956
- - Consider the slide context (e.g., "ํ‘œ์ง€", "ํ˜„ํ™ฉ ๋ถ„์„") to create appropriate visuals
957
- - Always output ONLY the prompt without any explanation
958
- - Keep prompts between 50-150 words for optimal results
959
- - Ensure the visual supports rather than overwhelms the slide content"""
960
-
961
- user_message = f"Topic: {topic}"
962
- if style_example:
963
- user_message += f"\n\nStyle reference to follow:\n{style_example}"
964
- if slide_context:
965
- user_message += f"\n\nSlide context: {slide_context}"
966
- if uploaded_content:
967
- user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}"
968
-
969
- payload = {
970
- "model": "dep89a2fld32mcm",
971
- "messages": [
972
- {
973
- "role": "system",
974
- "content": system_prompt
975
- },
976
- {
977
- "role": "user",
978
- "content": user_message
979
- }
980
- ],
981
- "max_tokens": 300,
982
- "top_p": 0.8,
983
- "temperature": 0.7,
984
- "stream": False
985
- }
986
-
987
- try:
988
- response = requests.post(url, json=payload, headers=headers, timeout=30)
989
- if response.status_code == 200:
990
- result = response.json()
991
- prompt = result['choices'][0]['message']['content'].strip()
992
- print(f"[LLM] ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์™„๋ฃŒ: {prompt[:50]}...")
993
- return prompt
994
- else:
995
- error_msg = f"ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ: {response.status_code}"
996
- print(f"[LLM] {error_msg}")
997
- return error_msg
998
- except Exception as e:
999
- error_msg = f"ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
1000
- print(f"[LLM] {error_msg}")
1001
- return error_msg
1002
-
1003
- def translate_to_english(text: str) -> str:
1004
- """ํ•œ๊ธ€ ํ…์ŠคํŠธ๋ฅผ ์˜์–ด๋กœ ๋ฒˆ์—ญ (LLM ์‚ฌ์šฉ)"""
1005
- if not any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in text):
1006
- return text
1007
-
1008
- print(f"[๋ฒˆ์—ญ] ํ•œ๊ธ€ ๊ฐ์ง€, ์˜์–ด๋กœ ๋ฒˆ์—ญ ์‹œ์ž‘")
1009
-
1010
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
1011
- headers = {
1012
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
1013
- "Content-Type": "application/json"
1014
- }
1015
-
1016
- payload = {
1017
- "model": "dep89a2fld32mcm",
1018
- "messages": [
1019
- {
1020
- "role": "system",
1021
- "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation."
1022
- },
1023
- {
1024
- "role": "user",
1025
- "content": text
1026
- }
1027
- ],
1028
- "max_tokens": 500,
1029
- "top_p": 0.8,
1030
- "stream": False
1031
- }
1032
-
1033
- try:
1034
- response = requests.post(url, json=payload, headers=headers, timeout=30)
1035
- if response.status_code == 200:
1036
- result = response.json()
1037
- translated = result['choices'][0]['message']['content'].strip()
1038
- print(f"[๋ฒˆ์—ญ] ์™„๋ฃŒ")
1039
- return translated
1040
- else:
1041
- print(f"[๋ฒˆ์—ญ] ์‹คํŒจ, ์›๋ณธ ์‚ฌ์šฉ")
1042
- return text
1043
- except Exception as e:
1044
- print(f"[๋ฒˆ์—ญ] ์˜ค๋ฅ˜: {str(e)}, ์›๋ณธ ์‚ฌ์šฉ")
1045
- return text
1046
-
1047
- def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]:
1048
- """Replicate API๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋˜๋Š” ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ"""
1049
- print(f"\n[์ด๋ฏธ์ง€ ์ƒ์„ฑ] {slide_info}")
1050
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ํ”„๋กฌํ”„ํŠธ: {prompt[:50]}...")
1051
-
1052
- global current_topic, uploaded_content
1053
-
1054
- # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์กฐ๊ฑด ํ™•์ธ
1055
- should_generate_process_flow = False
1056
-
1057
- # ์Šฌ๋ผ์ด๋“œ ์ •๋ณด์—์„œ ์ œ๋ชฉ ์ถ”์ถœ
1058
- slide_title = ""
1059
- if ":" in slide_info:
1060
- # "์Šฌ๋ผ์ด๋“œ 5: ํ”„๋กœ์„ธ์Šค" ํ˜•ํƒœ์—์„œ "ํ”„๋กœ์„ธ์Šค" ์ถ”์ถœ
1061
- parts = slide_info.split(":")
1062
- if len(parts) >= 2:
1063
- slide_title = parts[1].strip()
1064
-
1065
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์ถ”์ถœ๋œ ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ: '{slide_title}'")
1066
-
1067
- # 1. ํ”„๋กœ์„ธ์Šค ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
1068
- process_keywords = [
1069
- "ํ”„๋กœ์„ธ์Šค", "์ž‘๋™", "ํ”Œ๋กœ์šฐ", "ํ๋ฆ„", "์›Œํฌํ”Œ๋กœ์šฐ",
1070
- "์ ˆ์ฐจ", "๋‹จ๊ณ„", "์ฒ˜๋ฆฌ", "์ง„ํ–‰", "๊ฐœ์š”"
1071
- ]
1072
-
1073
- # 2. ์Šคํƒ€์ผ ํ™•์ธ
1074
- is_workflow_style = False
1075
- if any(style in prompt for style in ["Business Workflow", "Flowchart", "Business Process"]):
1076
- is_workflow_style = True
1077
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] Business Workflow ๋˜๋Š” Flowchart ์Šคํƒ€์ผ ๊ฐ์ง€")
1078
-
1079
- # 3. ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๊ฑฐ๋‚˜, ์Šคํƒ€์ผ์ด ์›Œํฌํ”Œ๋กœ์šฐ์ธ ๊ฒฝ์šฐ
1080
- title_has_process = any(keyword in slide_title for keyword in process_keywords)
1081
- prompt_has_process = any(keyword in prompt.lower() for keyword in ["process", "flow", "workflow"])
1082
-
1083
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ: {title_has_process}")
1084
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์›Œํฌํ”Œ๋กœ์šฐ ์Šคํƒ€์ผ: {is_workflow_style}")
1085
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ํ”„๋กฌํ”„ํŠธ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ: {prompt_has_process}")
1086
-
1087
- # ์กฐ๊ฑด ์™„ํ™”: ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๊ณ , ์Šคํƒ€์ผ์ด ๋งž๊ฑฐ๋‚˜ ํ”„๋กฌํ”„ํŠธ์— ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ƒ์„ฑ
1088
- if title_has_process and (is_workflow_style or prompt_has_process):
1089
- should_generate_process_flow = True
1090
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โœ… ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์กฐ๊ฑด ์ถฉ์กฑ!")
1091
-
1092
- # ํŠน๋ณ„ ์ผ€์ด์Šค: "๋ชฉ์ฐจ"๋Š” ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ์ด๋ฏธ์ง€๋กœ
1093
- if "๋ชฉ์ฐจ" in slide_title:
1094
- should_generate_process_flow = False
1095
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ๋ชฉ์ฐจ๋Š” ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ œ์™ธ")
1096
-
1097
- # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
1098
- if PROCESS_FLOW_AVAILABLE and should_generate_process_flow:
1099
- try:
1100
- print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ๐Ÿ”ง ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์‹œ์ž‘...")
1101
-
1102
- # ์Šฌ๋ผ์ด๋“œ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์ ˆํ•œ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ
1103
- img = generate_process_flow_for_ppt(
1104
- topic=current_topic,
1105
- context=slide_info,
1106
- style="Business Workflow"
1107
- )
1108
-
1109
- if isinstance(img, Image.Image):
1110
- print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โœ… ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์„ฑ๊ณต!")
1111
- return img, "Process flow diagram generated with Korean support"
1112
- else:
1113
- print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์‹คํŒจ, ์ผ๋ฐ˜ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด")
1114
- except Exception as e:
1115
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
1116
- import traceback
1117
- traceback.print_exc()
1118
- # ์‹คํŒจ์‹œ ์ผ๋ฐ˜ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์œผ๋กœ ํด๋ฐฑ
1119
- else:
1120
- if not PROCESS_FLOW_AVAILABLE:
1121
- print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โš ๏ธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ")
1122
- else:
1123
- print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โ„น๏ธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ, ์ผ๋ฐ˜ ์ด๋ฏธ์ง€ ์ƒ์„ฑ")
1124
-
1125
- # ๊ธฐ์กด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋กœ์ง
1126
  try:
1127
- english_prompt = translate_to_english(prompt)
1128
-
1129
- if not REPLICATE_API_TOKEN:
1130
- error_msg = "RAPI_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
1131
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์˜ค๋ฅ˜: {error_msg}")
1132
- return None, error_msg
1133
-
1134
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] Replicate API ํ˜ธ์ถœ ์ค‘...")
1135
- client = replicate.Client(api_token=REPLICATE_API_TOKEN)
1136
 
1137
- input_params = {
1138
- "seed": seed,
1139
- "prompt": english_prompt,
1140
- "speed_mode": "Extra Juiced ๐Ÿš€ (even more speed)",
1141
- "output_quality": 100
1142
- }
1143
-
1144
- start_time = time.time()
1145
- output = client.run(
1146
- "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645",
1147
- input=input_params
1148
- )
1149
-
1150
- elapsed = time.time() - start_time
1151
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] API ์‘๋‹ต ๋ฐ›์Œ ({elapsed:.1f}์ดˆ)")
1152
-
1153
- if output:
1154
- if isinstance(output, str) and output.startswith('http'):
1155
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] URL์—์„œ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์ค‘...")
1156
- response = requests.get(output, timeout=30)
1157
- img = Image.open(BytesIO(response.content))
1158
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์™„๋ฃŒ!")
1159
- return img, english_prompt
1160
- else:
1161
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ค‘...")
1162
- img = Image.open(BytesIO(output.read()))
1163
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์™„๋ฃŒ!")
1164
- return img, english_prompt
1165
- else:
1166
- error_msg = "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ - ๋นˆ ์‘๋‹ต"
1167
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] {error_msg}")
1168
- return None, error_msg
1169
-
1170
- except Exception as e:
1171
- error_msg = f"์˜ค๋ฅ˜: {str(e)}"
1172
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] {error_msg}")
1173
- print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์ƒ์„ธ ์˜ค๋ฅ˜:\n{traceback.format_exc()}")
1174
- return None, error_msg
1175
-
1176
- def create_slide_preview_html(slide_data: Dict) -> str:
1177
- """16:9 ๋น„์œจ์˜ ์Šฌ๋ผ์ด๋“œ ํ”„๋ฆฌ๋ทฐ HTML ์ƒ์„ฑ (ํŽธ์ง‘ ๊ธฐ๋Šฅ ์ œ๊ฑฐ)"""
1178
-
1179
- # ์ด๋ฏธ์ง€๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉ
1180
- img_base64 = ""
1181
- if slide_data.get("image"):
1182
- buffered = BytesIO()
1183
- slide_data["image"].save(buffered, format="PNG")
1184
- img_base64 = base64.b64encode(buffered.getvalue()).decode()
1185
-
1186
- # ํ…์ŠคํŠธ ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
1187
- subtitle = slide_data.get("subtitle", "")
1188
- bullet_points = slide_data.get("bullet_points", [])
1189
-
1190
- # HTML ์ƒ์„ฑ
1191
- html = f"""
1192
- <div class="slide-container" style="
1193
- width: 100%;
1194
- max-width: 1200px;
1195
- margin: 20px auto;
1196
- background: white;
1197
- border-radius: 12px;
1198
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
1199
- overflow: hidden;
1200
- ">
1201
- <div class="slide-header" style="
1202
- background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
1203
- color: white;
1204
- padding: 15px 30px;
1205
- font-size: 18px;
1206
- font-weight: bold;
1207
- ">
1208
- ์Šฌ๋ผ์ด๋“œ {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}
1209
- </div>
1210
-
1211
- <div class="slide-content" style="
1212
- display: flex;
1213
- height: 0;
1214
- padding-bottom: 56.25%; /* 16:9 ๋น„์œจ */
1215
- position: relative;
1216
- background: #fafbfc;
1217
- ">
1218
- <div class="slide-inner" style="
1219
- position: absolute;
1220
- top: 0;
1221
- left: 0;
1222
- width: 100%;
1223
- height: 100%;
1224
- display: flex;
1225
- padding: 20px;
1226
- gap: 20px;
1227
- ">
1228
- """
1229
-
1230
- # ํ‘œ์ง€์™€ ๋งˆ์ง€๋ง‰ ์Šฌ๋ผ์ด๋“œ๋Š” ์ „์ฒด ํ™”๋ฉด ์ด๋ฏธ์ง€
1231
- if slide_data.get('title') in ['ํ‘œ์ง€', 'Thank You']:
1232
- html += f"""
1233
- <!-- ์ „์ฒด ํ™”๋ฉด ์ด๋ฏธ์ง€ -->
1234
- <div style="
1235
- width: 100%;
1236
- height: 100%;
1237
- position: relative;
1238
- background: #e9ecef;
1239
- border-radius: 12px;
1240
- overflow: hidden;
1241
- ">
1242
- """
1243
-
1244
- if img_base64:
1245
- html += f"""
1246
- <img src="data:image/png;base64,{img_base64}" style="
1247
- width: 100%;
1248
- height: 100%;
1249
- object-fit: cover;
1250
- " alt="Slide Image">
1251
- <div style="
1252
- position: absolute;
1253
- top: 50%;
1254
- left: 50%;
1255
- transform: translate(-50%, -50%);
1256
- text-align: center;
1257
- background: rgba(255, 255, 255, 0.9);
1258
- padding: 30px 60px;
1259
- border-radius: 20px;
1260
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
1261
- backdrop-filter: blur(10px);
1262
- ">
1263
- """
1264
-
1265
- if slide_data.get('title') == 'ํ‘œ์ง€':
1266
- html += f"""
1267
- <h1 style="font-size: 48px; margin-bottom: 10px; color: #000000; font-weight: 700;">{slide_data.get('topic', '')}</h1>
1268
- <h2 style="font-size: 24px; margin-top: 0; color: #212529; font-weight: 400;">{subtitle}</h2>
1269
- """
1270
- else: # Thank You
1271
- html += f"""
1272
- <h1 style="font-size: 42px; color: #000000; font-weight: 700; line-height: 1.2;">{subtitle}</h1>
1273
- """
1274
-
1275
- html += """
1276
- </div>
1277
- """
1278
-
1279
- html += """
1280
- </div>
1281
- """
1282
- else:
1283
- # ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ๋ ˆ์ด์•„์›ƒ
1284
- html += f"""
1285
- <!-- ํ…์ŠคํŠธ ์˜์—ญ (์ขŒ์ธก) -->
1286
- <div class="text-area" style="
1287
- flex: 1;
1288
- padding: 30px;
1289
- display: flex;
1290
- flex-direction: column;
1291
- justify-content: center;
1292
- background: rgba(255, 255, 255, 0.95);
1293
- border-radius: 12px;
1294
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
1295
- ">
1296
- <h2 style="
1297
- color: #212529;
1298
- font-size: 28px;
1299
- margin-bottom: 30px;
1300
- font-weight: 600;
1301
- ">{subtitle}</h2>
1302
-
1303
- <ul style="
1304
- list-style: none;
1305
- padding: 0;
1306
- margin: 0;
1307
- ">
1308
- """
1309
-
1310
- for point in bullet_points:
1311
- clean_point = point.replace('โ€ข', '').strip()
1312
- html += f"""
1313
- <li style="
1314
- margin-bottom: 16px;
1315
- padding-left: 28px;
1316
- position: relative;
1317
- color: #495057;
1318
- font-size: 16px;
1319
- line-height: 1.6;
1320
- ">
1321
- <span style="
1322
- position: absolute;
1323
- left: 0;
1324
- color: #007bff;
1325
- font-size: 18px;
1326
- ">โ€ข</span>
1327
- {clean_point}
1328
- </li>
1329
- """
1330
-
1331
- html += f"""
1332
- </ul>
1333
- </div>
1334
-
1335
- <!-- ์ด๋ฏธ์ง€ ์˜์—ญ (์šฐ์ธก) -->
1336
- <div class="image-area" style="
1337
- flex: 1;
1338
- display: flex;
1339
- align-items: center;
1340
- justify-content: center;
1341
- padding: 20px;
1342
- ">
1343
- """
1344
-
1345
- if img_base64:
1346
- html += f"""
1347
- <img src="data:image/png;base64,{img_base64}" style="
1348
- max-width: 100%;
1349
- max-height: 100%;
1350
- object-fit: contain;
1351
- border-radius: 8px;
1352
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1353
- " alt="Slide Image">
1354
- """
1355
- else:
1356
- html += """
1357
- <div style="
1358
- color: #6c757d;
1359
- text-align: center;
1360
- ">
1361
- <div style="font-size: 48px;">๐Ÿ–ผ๏ธ</div>
1362
- <p>์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...</p>
1363
- </div>
1364
- """
1365
 
1366
- html += """
1367
- </div>
1368
- """
1369
-
1370
- html += """
1371
- </div>
1372
- </div>
1373
- </div>
1374
- """
1375
-
1376
- return html
1377
-
1378
- def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_name: str = "๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ") -> str:
1379
- """์ƒ์„ฑ๋œ ๊ฒฐ๊ณผ๋ฅผ PPTX ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ (๋ฐœํ‘œ์ž ๋…ธํŠธ ํฌํ•จ)"""
1380
- print(f"[PPTX] ํŒŒ์ผ ์ƒ์„ฑ ์‹œ์ž‘... ํ…Œ๋งˆ: {theme_name}")
1381
-
1382
- # ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ƒ์„ฑ (16:9 ๋น„์œจ)
1383
- prs = Presentation()
1384
- prs.slide_width = Inches(16)
1385
- prs.slide_height = Inches(9)
1386
-
1387
- # ์„ ํƒ๋œ ํ…Œ๋งˆ ๊ฐ€์ ธ์˜ค๊ธฐ
1388
- theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ"])
1389
-
1390
- # ๊ฐ ๊ฒฐ๊ณผ ์Šฌ๋ผ์ด๋“œ ์ถ”๊ฐ€
1391
- for i, result in enumerate(results):
1392
- if not result.get("success", False):
1393
- continue
1394
-
1395
- slide_data = result.get("slide_data", {})
1396
 
1397
- # ๋นˆ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ (์ œ๋ชฉ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์—†๋Š” ์™„์ „ํ•œ ๋นˆ ์Šฌ๋ผ์ด๋“œ)
1398
- blank_layout = prs.slide_layouts[6] # ์™„์ „ํžˆ ๋นˆ ๋ ˆ์ด์•„์›ƒ
1399
- slide = prs.slides.add_slide(blank_layout)
1400
 
1401
- # ํ‘œ์ง€ ์Šฌ๋ผ์ด๋“œ
1402
- if slide_data.get('title') == 'ํ‘œ์ง€':
1403
- # ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
1404
- if slide_data.get('image'):
1405
- try:
1406
- img_buffer = BytesIO()
1407
- slide_data['image'].save(img_buffer, format='PNG')
1408
- img_buffer.seek(0)
1409
-
1410
- # ์ „์ฒด ํ™”๋ฉด ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€
1411
- pic = slide.shapes.add_picture(
1412
- img_buffer,
1413
- 0, 0,
1414
- width=prs.slide_width,
1415
- height=prs.slide_height
1416
- )
1417
- # ๋งจ ๋’ค๋กœ ๋ณด๋‚ด๊ธฐ
1418
- slide.shapes._spTree.remove(pic._element)
1419
- slide.shapes._spTree.insert(2, pic._element)
1420
- except Exception as e:
1421
- print(f"[PPTX] ํ‘œ์ง€ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ์‹คํŒจ: {str(e)}")
1422
-
1423
- # ์ œ๋ชฉ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค (๋ฐ˜ํˆฌ๋ช… - ๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
1424
- title_bg = slide.shapes.add_shape(
1425
- MSO_SHAPE.ROUNDED_RECTANGLE,
1426
- Inches(2), Inches(2.8),
1427
- Inches(12), Inches(3.2)
1428
- )
1429
- title_bg.fill.solid()
1430
- title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
1431
- title_bg.fill.transparency = 0.8 # 50% ํˆฌ๋ช…๋„ (๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
1432
- title_bg.line.fill.background()
1433
-
1434
- # ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ์•ฝํ•˜๊ฒŒ)
1435
- shadow = title_bg.shadow
1436
- shadow.visible = True
1437
- shadow.distance = Pt(6)
1438
- shadow.size = 100
1439
- shadow.blur_radius = Pt(12)
1440
- shadow.transparency = 0.8
1441
- shadow.angle = 45
1442
-
1443
- # ์ œ๋ชฉ ํ…์ŠคํŠธ ์ถ”๊ฐ€
1444
- title_box = slide.shapes.add_textbox(
1445
- Inches(2), Inches(3.2),
1446
- Inches(12), Inches(1.5)
1447
- )
1448
- title_frame = title_box.text_frame
1449
- title_frame.text = topic
1450
- title_para = title_frame.paragraphs[0]
1451
- title_para.font.size = Pt(48)
1452
- title_para.font.bold = True
1453
- title_para.font.color.rgb = RGBColor(0, 0, 0) # ์™„์ „ํ•œ ๊ฒ€์ •์ƒ‰
1454
- title_para.alignment = PP_ALIGN.CENTER
1455
-
1456
- # ๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€ (์ œ๋ชฉ๊ณผ ๊ฐ€๊นŒ์ด)
1457
- subtitle_box = slide.shapes.add_textbox(
1458
- Inches(2), Inches(4.3),
1459
- Inches(12), Inches(1.0)
1460
- )
1461
- subtitle_frame = subtitle_box.text_frame
1462
- subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI ํ”„๋ ˆ์  ํ…Œ์ด์…˜')
1463
- subtitle_para = subtitle_frame.paragraphs[0]
1464
- subtitle_para.font.size = Pt(28) # 24์—์„œ 28๋กœ ์ฆ๊ฐ€
1465
- subtitle_para.font.color.rgb = RGBColor(33, 37, 41) # ์ง„ํ•œ ํšŒ์ƒ‰
1466
- subtitle_para.alignment = PP_ALIGN.CENTER
1467
-
1468
- # Thank You ์Šฌ๋ผ์ด๋“œ
1469
- elif slide_data.get('title') == 'Thank You':
1470
- # ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
1471
- if slide_data.get('image'):
1472
- try:
1473
- img_buffer = BytesIO()
1474
- slide_data['image'].save(img_buffer, format='PNG')
1475
- img_buffer.seek(0)
1476
-
1477
- # ์ „์ฒด ํ™”๋ฉด ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€
1478
- pic = slide.shapes.add_picture(
1479
- img_buffer,
1480
- 0, 0,
1481
- width=prs.slide_width,
1482
- height=prs.slide_height
1483
- )
1484
- # ๋งจ ๋’ค๋กœ ๋ณด๋‚ด๊ธฐ
1485
- slide.shapes._spTree.remove(pic._element)
1486
- slide.shapes._spTree.insert(2, pic._element)
1487
- except Exception as e:
1488
- print(f"[PPTX] Thank You ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ์‹คํŒจ: {str(e)}")
1489
-
1490
- # Thank You ๋ฐฐ๊ฒฝ ๋ฐ•์Šค (๋ฐ˜ํˆฌ๋ช… - ๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
1491
- thanks_bg = slide.shapes.add_shape(
1492
- MSO_SHAPE.ROUNDED_RECTANGLE,
1493
- Inches(2), Inches(3.5),
1494
- Inches(12), Inches(2.5)
1495
- )
1496
- thanks_bg.fill.solid()
1497
- thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
1498
- thanks_bg.fill.transparency = 0.8 # 50% ํˆฌ๋ช…๋„ (๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
1499
- thanks_bg.line.fill.background()
1500
-
1501
- # ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ์•ฝํ•˜๊ฒŒ)
1502
- shadow = thanks_bg.shadow
1503
- shadow.visible = True
1504
- shadow.distance = Pt(6)
1505
- shadow.size = 100
1506
- shadow.blur_radius = Pt(12)
1507
- shadow.transparency = 0.8
1508
- shadow.angle = 45
1509
-
1510
- # Thank You ํ…์ŠคํŠธ (๊ฒฐ๋ก  ๋ฌธ๊ตฌ)
1511
- thanks_box = slide.shapes.add_textbox(
1512
- Inches(2), Inches(4),
1513
- Inches(12), Inches(1.5)
1514
- )
1515
- thanks_frame = thanks_box.text_frame
1516
- thanks_frame.text = slide_data.get('subtitle', 'Thank You') # ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์‚ฌ์šฉ
1517
- thanks_para = thanks_frame.paragraphs[0]
1518
- thanks_para.font.size = Pt(42) # ํฌ๊ธฐ๋ฅผ ์กฐ๊ธˆ ์ค„์ž„
1519
- thanks_para.font.bold = True
1520
- thanks_para.font.color.rgb = RGBColor(0, 0, 0) # ์™„์ „ํ•œ ๊ฒ€์ •์ƒ‰
1521
- thanks_para.alignment = PP_ALIGN.CENTER
1522
-
1523
- # ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ
1524
- else:
1525
- # ๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ •
1526
- background = slide.background
1527
- fill = background.fill
1528
- fill.solid()
1529
- fill.fore_color.rgb = theme["background"]
1530
-
1531
- # ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค (๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ ํšจ๊ณผ)
1532
- title_box_bg = slide.shapes.add_shape(
1533
- MSO_SHAPE.ROUNDED_RECTANGLE,
1534
- Inches(0.3), Inches(0.2),
1535
- Inches(15.4), Inches(1.0)
1536
- )
1537
- title_box_bg.fill.solid()
1538
- title_box_bg.fill.fore_color.rgb = theme["box_fill"]
1539
- title_box_bg.fill.transparency = 1 - theme["box_opacity"]
1540
-
1541
- # ๊ทธ๋ฆผ์ž ํšจ๊ณผ
1542
- if theme["shadow"]:
1543
- shadow = title_box_bg.shadow
1544
- shadow.visible = True
1545
- shadow.distance = Pt(4)
1546
- shadow.size = 100
1547
- shadow.blur_radius = Pt(8)
1548
- shadow.transparency = 0.75
1549
- shadow.angle = 45
1550
-
1551
- title_box_bg.line.fill.background() # ํ…Œ๋‘๋ฆฌ ์—†์Œ
1552
-
1553
- # ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ ์ถ”๊ฐ€
1554
- title_box = slide.shapes.add_textbox(
1555
- Inches(0.5), Inches(0.3),
1556
- Inches(15), Inches(0.8)
1557
- )
1558
- title_frame = title_box.text_frame
1559
- title_frame.text = f"{slide_data.get('title', '')}"
1560
- title_para = title_frame.paragraphs[0]
1561
- title_para.font.size = Pt(28)
1562
- title_para.font.bold = True
1563
- title_para.font.color.rgb = theme["title_color"]
1564
-
1565
- # ์ขŒ์ธก ํ…์ŠคํŠธ ์˜์—ญ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค
1566
- text_box_bg = slide.shapes.add_shape(
1567
- MSO_SHAPE.ROUNDED_RECTANGLE,
1568
- Inches(0.3), Inches(1.4),
1569
- Inches(7.8), Inches(6.8)
1570
- )
1571
- text_box_bg.fill.solid()
1572
- text_box_bg.fill.fore_color.rgb = theme["box_fill"]
1573
- text_box_bg.fill.transparency = 1 - theme["box_opacity"]
1574
-
1575
- if theme["shadow"]:
1576
- shadow = text_box_bg.shadow
1577
- shadow.visible = True
1578
- shadow.distance = Pt(5)
1579
- shadow.size = 100
1580
- shadow.blur_radius = Pt(10)
1581
- shadow.transparency = 0.7
1582
- shadow.angle = 45
1583
-
1584
- text_box_bg.line.fill.background()
1585
-
1586
- # ์ขŒ์ธก ํ…์ŠคํŠธ ์˜์—ญ
1587
- text_box = slide.shapes.add_textbox(
1588
- Inches(0.8), Inches(1.8),
1589
- Inches(7.0), Inches(6.0)
1590
- )
1591
- text_frame = text_box.text_frame
1592
- text_frame.word_wrap = True
1593
-
1594
- # ์†Œ์ œ๋ชฉ ์ถ”๊ฐ€
1595
- subtitle_para = text_frame.paragraphs[0]
1596
- subtitle_para.text = slide_data.get('subtitle', '')
1597
- subtitle_para.font.size = Pt(20)
1598
- subtitle_para.font.bold = True
1599
- subtitle_para.font.color.rgb = theme["subtitle_color"]
1600
- subtitle_para.space_after = Pt(20)
1601
-
1602
- # ๋ถˆ๋ฆฟ ํฌ์ธํŠธ ์ถ”๊ฐ€
1603
- bullet_points = slide_data.get('bullet_points', [])
1604
- for point in bullet_points:
1605
- p = text_frame.add_paragraph()
1606
- # โ€ข ์ œ๊ฑฐํ•˜๊ณ  ํ…์ŠคํŠธ๋งŒ ์ถ”๊ฐ€ (์ด๋ชจ์ง€๋Š” ์œ ์ง€)
1607
- clean_text = point.replace('โ€ข', '').strip()
1608
- p.text = clean_text
1609
- p.font.size = Pt(16)
1610
- p.font.color.rgb = theme["text_color"]
1611
- # ๋ถˆ๋ฆฟ ์—†์ด ๋“ค์—ฌ์“ฐ๊ธฐ๋งŒ
1612
- p.level = 0
1613
- p.space_after = Pt(12)
1614
- p.line_spacing = 1.5
1615
- # ์™ผ์ชฝ ์—ฌ๋ฐฑ ์„ค์ •
1616
- p.left_indent = Pt(0)
1617
-
1618
- # ๋ถˆ๋ฆฟ ์ƒ‰์ƒ
1619
- p.font.color.rgb = theme["text_color"]
1620
-
1621
- # ์šฐ์ธก ๏ฟฝ๏ฟฝ๋ฏธ์ง€ ์ถ”๊ฐ€ (๋ฐฐ๊ฒฝ ๋ฐ•์Šค ์ œ๊ฑฐ)
1622
- if slide_data.get('image'):
1623
- try:
1624
- img_buffer = BytesIO()
1625
- slide_data['image'].save(img_buffer, format='PNG')
1626
- img_buffer.seek(0)
1627
-
1628
- pic = slide.shapes.add_picture(
1629
- img_buffer,
1630
- Inches(8.5), Inches(1.6),
1631
- width=Inches(6.8), height=Inches(6.4)
1632
- )
1633
-
1634
- # ์ด๋ฏธ์ง€ ํ…Œ๋‘๋ฆฌ ์ œ๊ฑฐ
1635
- pic.line.fill.background()
1636
-
1637
- except Exception as e:
1638
- print(f"[PPTX] ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ์‹คํŒจ: {str(e)}")
1639
-
1640
- # ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ์ถ”๊ฐ€
1641
- page_num = slide.shapes.add_textbox(
1642
- Inches(15), Inches(8.5),
1643
- Inches(1), Inches(0.5)
1644
- )
1645
- page_frame = page_num.text_frame
1646
- page_frame.text = str(i + 1)
1647
- page_para = page_frame.paragraphs[0]
1648
- page_para.font.size = Pt(12)
1649
- page_para.font.color.rgb = theme["text_color"]
1650
- page_para.alignment = PP_ALIGN.RIGHT
1651
-
1652
- # ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ถ”๊ฐ€
1653
- notes_slide = slide.notes_slide
1654
- notes_slide.notes_text_frame.text = slide_data.get('speaker_notes', '')
1655
-
1656
- # ํŒŒ์ผ ์ €์žฅ
1657
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1658
- filename = f"presentation_{timestamp}.pptx"
1659
- filepath = os.path.join("/tmp", filename)
1660
- prs.save(filepath)
1661
-
1662
- print(f"[PPTX] ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ: {filename}")
1663
- return filepath
1664
-
1665
- def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> List[Dict]:
1666
- """์„ ํƒ๋œ ์Šฌ๋ผ์ด๋“œ ์ˆ˜์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ"""
1667
- core_slides = template.get("core_slides", [])
1668
- optional_slides = template.get("optional_slides", [])
1669
-
1670
- # ํ‘œ์ง€์™€ Thank You๋ฅผ ์ œ์™ธํ•œ ๋ณธ๋ฌธ ์Šฌ๋ผ์ด๋“œ ์ˆ˜
1671
- content_slide_count = slide_count
1672
-
1673
- # ์ฝ”์–ด ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์š”์ฒญ๋œ ์ˆ˜๋ณด๋‹ค ๋งŽ์œผ๋ฉด ์กฐ์ •
1674
- if len(core_slides) > content_slide_count:
1675
- selected_slides = core_slides[:content_slide_count]
1676
- else:
1677
- # ์ฝ”์–ด ์Šฌ๋ผ์ด๋“œ + ์˜ต์…”๋„ ์Šฌ๋ผ์ด๋“œ์—์„œ ์„ ํƒ
1678
- selected_slides = core_slides.copy()
1679
- remaining = content_slide_count - len(core_slides)
1680
-
1681
- if remaining > 0 and optional_slides:
1682
- # ์˜ต์…”๋„ ์Šฌ๋ผ์ด๋“œ์—์„œ ์ถ”๊ฐ€ ์„ ํƒ
1683
- additional = optional_slides[:remaining]
1684
- selected_slides.extend(additional)
1685
-
1686
- # ํ‘œ์ง€ ์ถ”๊ฐ€ (๋งจ ์•ž)
1687
- slides = [{"title": "ํ‘œ์ง€", "style": "Title Slide (Hero)", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ํ‘œ์ง€"}]
1688
-
1689
- # ๋ณธ๋ฌธ ์Šฌ๋ผ์ด๋“œ ์ถ”๊ฐ€
1690
- slides.extend(selected_slides)
1691
-
1692
- # Thank You ์Šฌ๋ผ์ด๋“œ ์ถ”๊ฐ€ (๋งจ ๋’ค)
1693
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋งˆ๋ฌด๋ฆฌ์™€ ํ•ต์‹ฌ ๋ฉ”์‹œ์ง€"})
1694
-
1695
- return slides
1696
-
1697
- def create_custom_slides_ui(initial_count=5):
1698
- """์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ UI (3-20์žฅ)"""
1699
- slides = []
1700
- for i in range(20): # ์ตœ๋Œ€ 20์žฅ
1701
- row = gr.Row(visible=(i < initial_count)) # Row๋ฅผ ๋ณ€์ˆ˜์— ํ• ๋‹น
1702
- with row:
1703
- with gr.Column(scale=2):
1704
- title = gr.Textbox(
1705
- label=f"์Šฌ๋ผ์ด๋“œ {i+1} ์ œ๋ชฉ",
1706
- placeholder="์˜ˆ: ํ˜„ํ™ฉ ๋ถ„์„, ์†”๋ฃจ์…˜, ๋กœ๋“œ๋งต...",
1707
- )
1708
- with gr.Column(scale=3):
1709
- style = gr.Dropdown(
1710
- choices=list(STYLE_TEMPLATES.keys()),
1711
- label=f"์Šคํƒ€์ผ ์„ ํƒ",
1712
- value="Colorful Mind Map"
1713
- )
1714
- with gr.Column(scale=3):
1715
- hint = gr.Textbox(
1716
- label=f"ํ”„๋กฌํ”„ํŠธ ํžŒํŠธ",
1717
- placeholder="์ด ์Šฌ๋ผ์ด๋“œ์—์„œ ํ‘œํ˜„ํ•˜๊ณ  ์‹ถ์€ ๋‚ด์šฉ"
1718
- )
1719
- slides.append({"title": title, "style": style, "hint": hint, "row": row})
1720
- return slides
1721
-
1722
- def generate_ppt_with_content(topic: str, template_name: str, audience_type: str, custom_slides: List[Dict],
1723
- slide_count: int, seed: int, uploaded_file,
1724
- use_web_search: bool, theme_name: str = "๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ",
1725
- progress=gr.Progress()):
1726
- """PPT ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ ๋‚ด์šฉ์„ ํ•จ๊ป˜ ์ƒ์„ฑ (๋ฐœํ‘œ์ž ๋…ธํŠธ ํฌํ•จ)"""
1727
- results = []
1728
- preview_html = ""
1729
-
1730
- # ์ „์—ญ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ
1731
- global current_topic, uploaded_content
1732
- current_topic = topic
1733
-
1734
- # ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐ
1735
- uploaded_content = ""
1736
- if uploaded_file is not None:
1737
  try:
1738
- uploaded_content = read_uploaded_file(uploaded_file.name)
1739
- print(f"[ํŒŒ์ผ ์—…๋กœ๋“œ] ๋‚ด์šฉ ๊ธธ์ด: {len(uploaded_content)}์ž")
1740
- except Exception as e:
1741
- print(f"[ํŒŒ์ผ ์—…๋กœ๋“œ] ์˜ค๋ฅ˜: {str(e)}")
1742
- uploaded_content = ""
1743
-
1744
- # ํ…œํ”Œ๋ฆฟ ์„ ํƒ ๋ฐ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ
1745
- if template_name == "์‚ฌ์šฉ์ž ์ •์˜" and custom_slides:
1746
- slides = [{"title": "ํ‘œ์ง€", "style": "Title Slide (Hero)", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ํ‘œ์ง€"}]
1747
- slides.extend(custom_slides)
1748
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋งˆ๋ฌด๋ฆฌ์™€ ํ•ต์‹ฌ ๋ฉ”์‹œ์ง€"})
1749
- else:
1750
- template = PPT_TEMPLATES[template_name]
1751
- slides = generate_dynamic_slides(topic, template, slide_count)
1752
-
1753
- if not slides:
1754
- yield "", "์Šฌ๋ผ์ด๋“œ๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", None
1755
- return
1756
-
1757
- total_slides = len(slides)
1758
- print(f"\n[PPT ์ƒ์„ฑ] ์‹œ์ž‘ - ์ด {total_slides}๊ฐœ ์Šฌ๋ผ์ด๋“œ (ํ‘œ์ง€ + ๋ณธ๋ฌธ {slide_count} + Thank You)")
1759
- print(f"[PPT ์ƒ์„ฑ] ์ฃผ์ œ: {topic}")
1760
- print(f"[PPT ์ƒ์„ฑ] ํ…œํ”Œ๋ฆฟ: {template_name}")
1761
- print(f"[PPT ์ƒ์„ฑ] ์˜ค๋””์–ธ์Šค: {audience_type}")
1762
- print(f"[PPT ์ƒ์„ฑ] ๋””์ž์ธ ํ…Œ๋งˆ: {theme_name}")
1763
- print(f"[PPT ์ƒ์„ฑ] ์›น ๊ฒ€์ƒ‰: {'์‚ฌ์šฉ' if use_web_search else '๋ฏธ์‚ฌ์šฉ'}")
1764
-
1765
- # ์›น ๊ฒ€์ƒ‰ ์‹คํ–‰ (์„ ํƒ๋œ ๊ฒฝ์šฐ)
1766
- web_search_results = []
1767
- if use_web_search and BRAVE_API_TOKEN:
1768
- progress(0.05, "์›น ๊ฒ€์ƒ‰ ์ค‘...")
1769
- web_search_results = brave_search(topic)
1770
-
1771
- # CSS ์Šคํƒ€์ผ ์ถ”๊ฐ€
1772
- preview_html = """
1773
- <style>
1774
- .slides-container {
1775
- width: 100%;
1776
- max-width: 1400px;
1777
- margin: 0 auto;
1778
- }
1779
- </style>
1780
- <div class="slides-container">
1781
- """
1782
-
1783
- # ๊ฐ ์Šฌ๋ผ์ด๋“œ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ
1784
- for i, slide in enumerate(slides):
1785
- progress((i + 1) / (total_slides + 1), f"์Šฌ๋ผ์ด๋“œ {i+1}/{total_slides} ์ฒ˜๋ฆฌ ์ค‘...")
1786
-
1787
- slide_info = f"์Šฌ๋ผ์ด๋“œ {i+1}: {slide['title']}"
1788
-
1789
- # ํ…์ŠคํŠธ ๋‚ด์šฉ ์ƒ์„ฑ
1790
- slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
1791
-
1792
- # Thank You ์Šฌ๋ผ์ด๋“œ๋Š” ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์ƒ์„ฑ
1793
- if slide['title'] == 'Thank You':
1794
- conclusion_phrase = generate_conclusion_phrase(topic, audience_type)
1795
- content = {
1796
- "subtitle": conclusion_phrase,
1797
- "bullet_points": []
1798
- }
1799
- # Thank You ์Šฌ๋ผ์ด๋“œ์šฉ ํŠน๋ณ„ ๋…ธํŠธ
1800
- speaker_notes = generate_closing_notes(topic, conclusion_phrase, audience_type)
1801
- else:
1802
- content = generate_slide_content(
1803
- topic, slide['title'], slide_context, audience_type,
1804
- uploaded_content, web_search_results
1805
- )
1806
- # ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ๋ฐœํ‘œ์ž ๋…ธํŠธ
1807
- speaker_notes = generate_presentation_notes(topic, slide['title'], content, audience_type)
1808
-
1809
- # ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ๋ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
1810
- style_key = slide["style"]
1811
- if style_key in STYLE_TEMPLATES:
1812
- style_info = STYLE_TEMPLATES[style_key]
1813
- prompt = generate_prompt_with_llm(
1814
- topic, style_info["example"],
1815
- slide_context, uploaded_content
1816
- )
1817
 
1818
- # ์ด๋ฏธ์ง€ ์ƒ์„ฑ (ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ง€์› ํฌํ•จ)
1819
- slide_seed = seed + i
1820
- img, used_prompt = generate_image(prompt, slide_seed, slide_info)
1821
-
1822
- # ์Šฌ๋ผ์ด๋“œ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
1823
- slide_data = {
1824
- "slide_number": i + 1,
1825
- "title": slide["title"],
1826
- "subtitle": content["subtitle"],
1827
- "bullet_points": content["bullet_points"],
1828
- "image": img,
1829
- "style": style_info["name"],
1830
- "speaker_notes": speaker_notes,
1831
- "topic": topic # ํ‘œ์ง€์šฉ
1832
- }
1833
-
1834
- # ํ”„๋ฆฌ๋ทฐ HTML ์ƒ์„ฑ (ํŽธ์ง‘ ๊ธฐ๋Šฅ ์ œ๊ฑฐ)
1835
- preview_html += create_slide_preview_html(slide_data)
1836
-
1837
- # ํ˜„์žฌ๊นŒ์ง€์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1838
- yield preview_html + "</div>", f"### ๐Ÿ”„ {slide_info} ์ƒ์„ฑ ์ค‘...", None
1839
-
1840
- results.append({
1841
- "slide_data": slide_data,
1842
- "success": img is not None
1843
- })
1844
-
1845
- # PPTX ํŒŒ์ผ ์ƒ์„ฑ
1846
- progress(0.95, "PPTX ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...")
1847
- pptx_path = None
1848
- try:
1849
- pptx_path = create_pptx_file(results, topic, template_name, theme_name)
1850
  except Exception as e:
1851
- print(f"[PPTX] ํŒŒ์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
1852
-
1853
- preview_html += "</div>"
1854
- progress(1.0, "์™„๋ฃŒ!")
1855
- successful = sum(1 for r in results if r["success"])
1856
- final_status = f"### ๐ŸŽ‰ ์ƒ์„ฑ ์™„๋ฃŒ! ์ด {total_slides}๊ฐœ ์Šฌ๋ผ์ด๋“œ ์ค‘ {successful}๊ฐœ ์„ฑ๊ณต"
1857
-
1858
- # ์ „์—ญ ๋ณ€์ˆ˜ ์ €์žฅ
1859
- global current_slides_data, current_template, current_theme
1860
- current_slides_data = results
1861
- current_template = template_name
1862
- current_theme = theme_name
1863
-
1864
- if pptx_path:
1865
- final_status += f"\n\n### ๐Ÿ“ฅ PPTX ํŒŒ์ผ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค!"
1866
- final_status += f"\n\n๐ŸŽค **{audience_type}๋ฅผ ์œ„ํ•œ ๋ฐœํ‘œ์ž ๋…ธํŠธ๊ฐ€ ๊ฐ ์Šฌ๋ผ์ด๋“œ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!**"
1867
- if PROCESS_FLOW_AVAILABLE:
1868
- final_status += f"\n\n๐Ÿ”ง **ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค (ํ•œ๊ธ€ ์ง€์›)**"
1869
-
1870
- yield preview_html, final_status, pptx_path
1871
-
1872
- # Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
1873
- with gr.Blocks(title="PPT ์ด๋ฏธ์ง€ ์ƒ์„ฑ๊ธฐ", theme=gr.themes.Soft(), css="""
1874
- .preview-container { max-width: 1400px; margin: 0 auto; }
1875
- .slide-container { transition: all 0.3s ease; }
1876
- .slide-container:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important; }
1877
- #custom_accordion .form {
1878
- max-height: 600px;
1879
- overflow-y: auto;
1880
- padding-right: 10px;
1881
- }
1882
- #custom_accordion .form::-webkit-scrollbar {
1883
- width: 6px;
1884
- }
1885
- #custom_accordion .form::-webkit-scrollbar-track {
1886
- background: #f1f1f1;
1887
- border-radius: 3px;
1888
- }
1889
- #custom_accordion .form::-webkit-scrollbar-thumb {
1890
- background: #888;
1891
- border-radius: 3px;
1892
- }
1893
- #custom_accordion .form::-webkit-scrollbar-thumb:hover {
1894
- background: #555;
1895
- }
1896
- """) as demo:
1897
- gr.Markdown("""
1898
- # ๐ŸŽฏ AI ๊ธฐ๋ฐ˜ PPT ํ†ตํ•ฉ ์ƒ์„ฑ๊ธฐ (๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ ๋ฒ„์ „)
1899
-
1900
- ### ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์กฐํ™”๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜์„ธ์š”!
1901
-
1902
- #### ๐Ÿ†• ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ:
1903
- - ๐ŸŽญ **์˜ค๋””์–ธ์Šค๋ณ„ ์ตœ์ ํ™”**: ๋ฐœํ‘œ ๋Œ€์ƒ์— ๋งž์ถ˜ ๋‚ด์šฉ๊ณผ ํ†ค ์ž๋™ ์กฐ์ •
1904
- - ๐Ÿ“ **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ํ…์ŠคํŠธ**: "~์ž„", "~ํ•จ" ์Šคํƒ€์ผ์˜ ํ”„๋กœํŽ˜์…”๋„ํ•œ ๋ฌธ์ฒด
1905
- - ๐ŸŽจ **์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€**: ๊ฐ ํฌ์ธํŠธ๋ณ„ ์ ์ ˆํ•œ ์ด๋ชจ์ง€๋กœ ์‹œ๊ฐ์  ๊ตฌ๋ถ„
1906
- - ๐Ÿ“‹ **์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ**: ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์˜ˆ์ œ ์ฃผ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
1907
- - ๐ŸŽจ **๋””์ž์ธ ํ…Œ๋งˆ**: 5๊ฐ€์ง€ ์ „๋ฌธ์ ์ธ ๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ ๊ฐ€๋Šฅ
1908
- - ๐Ÿ’Ž **๊ฐœ์„ ๋œ ์Šฌ๋ผ์ด๋“œ ๋””์ž์ธ**: ๋” ํˆฌ๋ช…ํ•œ ๋ฐฐ๊ฒฝ๊ณผ ๊น”๋”ํ•œ ๋ ˆ์ด์•„์›ƒ
1909
- - ๐Ÿ“Š **ํ‘œ์ง€์™€ Thank You ์Šฌ๋ผ์ด๋“œ** ์ž๋™ ์ถ”๊ฐ€
1910
- - ๐ŸŽค **์˜ค๋””์–ธ์Šค๋ณ„ ๋ฐœํ‘œ์ž ๋…ธํŠธ** ์ž๋™ ์ƒ์„ฑ (๊ตฌ์–ด์ฒด)
1911
- - ๐Ÿ“ **ํŒŒ์ผ ์—…๋กœ๋“œ** ์ง€์› (PDF/CSV/TXT)
1912
- - ๐Ÿ” **์›น ๊ฒ€์ƒ‰** ๊ธฐ๋Šฅ (Brave Search)
1913
- - ๐ŸŽš๏ธ **์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ ˆ** (6-20์žฅ)
1914
- - ๐Ÿ”ง **ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ** ์ž๋™ ์ƒ์„ฑ (ํ•œ๊ธ€ ์ง€์›)
1915
- - ๐ŸŽฏ **์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ**: 3-20์žฅ์„ ์ž์œ ๋กญ๊ฒŒ ๊ตฌ์„ฑํ•˜์—ฌ ๋‚˜๋งŒ์˜ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ œ์ž‘
1916
- """)
1917
-
1918
- # API ํ† ํฐ ์ƒํƒœ ํ™•์ธ
1919
- token_status = []
1920
- if not REPLICATE_API_TOKEN:
1921
- token_status.append("โš ๏ธ RAPI_TOKEN ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
1922
- if not FRIENDLI_TOKEN:
1923
- token_status.append("โš ๏ธ FRIENDLI_TOKEN ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
1924
- if not BRAVE_API_TOKEN:
1925
- token_status.append("โ„น๏ธ BAPI_TOKEN์ด ์—†์–ด ์›น ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
1926
- if not PROCESS_FLOW_AVAILABLE:
1927
- token_status.append("โ„น๏ธ process_flow_generator๊ฐ€ ์—†์–ด ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
1928
-
1929
- if token_status:
1930
- gr.Markdown("\n".join(token_status))
1931
-
1932
- with gr.Row():
1933
- with gr.Column(scale=1):
1934
- # ์˜ˆ์ œ ์„ ํƒ
1935
- with gr.Row():
1936
- example_btn = gr.Button("๐Ÿ“‹ ์˜ˆ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", size="sm", variant="secondary")
1937
-
1938
- # ๊ธฐ๋ณธ ์ž…๋ ฅ
1939
- topic_input = gr.Textbox(
1940
- label="ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ฃผ์ œ",
1941
- placeholder="์˜ˆ: AI ์Šคํƒ€ํŠธ์—… ํˆฌ์ž ์œ ์น˜, ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ, ๋””์ง€ํ„ธ ์ „ํ™˜ ์ „๋žต",
1942
- lines=2
1943
- )
1944
-
1945
- # ์˜ค๋””์–ธ์Šค ์„ ํƒ (์ƒˆ๋กœ ์ถ”๊ฐ€)
1946
- audience_select = gr.Dropdown(
1947
- choices=list(AUDIENCE_TYPES.keys()),
1948
- label="๐ŸŽญ ๋ฐœํ‘œ ๋Œ€์ƒ ์˜ค๋””์–ธ์Šค",
1949
- value="์ผ๋ฐ˜ ์ง์›",
1950
- info="๋ฐœํ‘œ ๋Œ€์ƒ์— ๋”ฐ๋ผ ๋‚ด์šฉ๊ณผ ํ†ค์ด ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค"
1951
- )
1952
-
1953
- # ์˜ค๋””์–ธ์Šค ์„ค๋ช… ํ‘œ์‹œ
1954
- audience_info = gr.Markdown()
1955
-
1956
- # PPT ํ…œํ”Œ๋ฆฟ ์„ ํƒ
1957
- template_select = gr.Dropdown(
1958
- choices=list(PPT_TEMPLATES.keys()),
1959
- label="PPT ํ…œํ”Œ๋ฆฟ ์„ ํƒ",
1960
- value="๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ",
1961
- info="๋ชฉ์ ์— ๋งž๋Š” ํ…œํ”Œ๋ฆฟ์„ ์„ ํƒํ•˜์„ธ์š”"
1962
- )
1963
-
1964
- # ๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ
1965
- theme_select = gr.Dropdown(
1966
- choices=list(DESIGN_THEMES.keys()),
1967
- label="๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ",
1968
- value="๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ",
1969
- info="ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ์ „์ฒด์ ์ธ ๋””์ž์ธ ์Šคํƒ€์ผ"
1970
- )
1971
-
1972
- # ํ…Œ๋งˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
1973
- theme_preview = gr.HTML()
1974
-
1975
- # ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
1976
- slide_count = gr.Slider(
1977
- minimum=6,
1978
- maximum=20,
1979
- value=8,
1980
- step=1,
1981
- label="๋ณธ๋ฌธ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ (ํ‘œ์ง€์™€ Thank You ์ œ์™ธ)",
1982
- info="์ƒ์„ฑํ•  ๋ณธ๋ฌธ ์Šฌ๋ผ์ด๋“œ ์ˆ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š” (์‚ฌ์šฉ์ž ์ •์˜ ํ…œํ”Œ๋ฆฟ์—์„œ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ)"
1983
- )
1984
-
1985
- # ํŒŒ์ผ ์—…๋กœ๋“œ
1986
- file_upload = gr.File(
1987
- label="์ฐธ๊ณ  ์ž๋ฃŒ ์—…๋กœ๋“œ (์„ ํƒ)",
1988
- file_types=[".pdf", ".csv", ".txt"],
1989
- type="filepath",
1990
- value=None
1991
- )
1992
-
1993
- # ์›น ๊ฒ€์ƒ‰ ์˜ต์…˜
1994
- use_web_search = gr.Checkbox(
1995
- label="์›น ๊ฒ€์ƒ‰ ์‚ฌ์šฉ",
1996
- value=False,
1997
- info="Brave Search๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์‹  ์ •๋ณด ๋ฐ˜์˜"
1998
- )
1999
-
2000
- # ํ…œํ”Œ๋ฆฟ ์„ค๋ช…
2001
- template_info = gr.Markdown()
2002
-
2003
- # ์‹œ๋“œ ๊ฐ’
2004
- seed_input = gr.Slider(
2005
- minimum=1,
2006
- maximum=100,
2007
- value=10,
2008
- step=1,
2009
- label="์‹œ๋“œ ๊ฐ’"
2010
- )
2011
-
2012
- generate_btn = gr.Button("๐Ÿš€ PPT ์ƒ์„ฑ ์‹œ์ž‘ (AI๊ฐ€ ๋ชจ๋“  ๋‚ด์šฉ์„ ์ž๋™ ์ƒ์„ฑ)", variant="primary", size="lg")
2013
-
2014
- # ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ์•ˆ๋‚ด
2015
- with gr.Accordion("๐Ÿ“– ์‚ฌ์šฉ ๋ฐฉ๋ฒ•", open=False):
2016
- gr.Markdown("""
2017
- ### ๐Ÿ”„ ์ž‘์—… ์ˆœ์„œ:
2018
- 1. **์˜ˆ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ** ๋ฒ„ํŠผ์œผ๋กœ ์ƒ˜ํ”Œ ์ฃผ์ œ๋ฅผ ๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ์ž…๋ ฅ
2019
- 2. **์˜ค๋””์–ธ์Šค ์„ ํƒ**: ๋ฐœํ‘œ ๋Œ€์ƒ์— ๋”ฐ๋ผ ๋‚ด์šฉ์ด ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค
2020
- 3. **ํ…œํ”Œ๋ฆฟ๊ณผ ํ…Œ๋งˆ ์„ ํƒ** ํ›„ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ
2021
- 4. **๋‹ค์šด๋กœ๋“œ**: ์ƒ์„ฑ ์™„๋ฃŒ ํ›„ PPTX ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
2022
-
2023
- ### ๐ŸŽญ ์˜ค๋””์–ธ์Šค๋ณ„ ํŠน์ง•:
2024
- - **๊ฒฝ์˜์ง„/์ž„์›**: ์ „๋žต์  ๊ฐ€์น˜, ROI, ๋น„์ฆˆ๋‹ˆ์Šค ์ž„ํŒฉํŠธ ์ค‘์‹ฌ
2025
- - **ํˆฌ์ž์ž**: ์‹œ์žฅ ๊ธฐํšŒ, ์„ฑ์žฅ ๊ฐ€๋Šฅ์„ฑ, ์ˆ˜์ต์„ฑ ๊ฐ•์กฐ
2026
- - **๊ธฐ์ˆ ํŒ€**: ๊ธฐ์ˆ ์  ์„ธ๋ถ€์‚ฌํ•ญ, ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ์„ฑ๋Šฅ ์ง€ํ‘œ
2027
- - **์ผ๋ฐ˜ ์ง์›**: ์‹ค๋ฌด์  ๋‚ด์šฉ, ํ˜‘์—… ๋ฐฉ์•ˆ, ์‹คํ–‰ ๊ณ„ํš
2028
- - **๊ณ ๊ฐ/ํŒŒํŠธ๋„ˆ**: ๊ณ ๊ฐ ๊ฐ€์น˜, ํ˜œํƒ, ์„ฑ๊ณต ์‚ฌ๋ก€
2029
- - **์ผ๋ฐ˜ ๋Œ€์ค‘**: ์‰ฌ์šด ์„ค๋ช…, ์‚ฌ์šฉ ํŽธ์˜์„ฑ, ์‹ค์งˆ์  ์ด์ 
2030
-
2031
- ### ๐Ÿ’ก ํ…์ŠคํŠธ ์Šคํƒ€์ผ ํŠน์ง•:
2032
- - **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ์ข…๊ฒฐ**: "~์ž„", "~ํ•จ" ๋˜๋Š” ๋ช…์‚ฌ๋กœ ๋๋‚จ
2033
- - **์ด๋ชจ์ง€ ํ™œ์šฉ**: ๊ฐ ํฌ์ธํŠธ ์•ž์— ๋‚ด์šฉ ๊ด€๋ จ ์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€
2034
- - **8-12๋‹จ์–ด**: ๊ฐ ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ๋งค์šฐ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌ์„ฑ
2035
-
2036
- ### ๐Ÿ“ ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ:
2037
- - **ํ…œํ”Œ๋ฆฟ ๋Œ€์‹  ์ง์ ‘ ๊ตฌ์„ฑ**: "์‚ฌ์šฉ์ž ์ •์˜" ํ…œํ”Œ๋ฆฟ ์„ ํƒ ์‹œ ํ™œ์„ฑํ™”
2038
- - **3-20์žฅ ์ž์œ ๋กญ๊ฒŒ ๊ตฌ์„ฑ**: ์Šฌ๋ผ์ด๋”๋กœ ์›ํ•˜๋Š” ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
2039
- - **๊ฐ ์Šฌ๋ผ์ด๋“œ๋ณ„ ์Šคํƒ€์ผ ์ง€์ •**: 16๊ฐ€์ง€ ์Šคํƒ€์ผ ์ค‘ ์„ ํƒ
2040
- - **ํžŒํŠธ ์ œ๊ณต**: ๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ๋‚ด์šฉ ๋ฐฉํ–ฅ์„ ํ”„๋กฌํ”„ํŠธ ํžŒํŠธ๋กœ ์ง€์ •
2041
-
2042
- ### ๐Ÿ”ง ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ:
2043
- - **Business Workflow** ๋˜๋Š” **Flowchart** ์Šคํƒ€์ผ ์„ ํƒ ์‹œ ์ž๋™์œผ๋กœ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
2044
- - ํ•œ๊ธ€ ํ…์ŠคํŠธ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•˜๋Š” ๋‹ค์ด์–ด๊ทธ๋žจ
2045
- - ํ”„๋กœ์„ธ์Šค, ์˜์‚ฌ๊ฒฐ์ •, ์ž…์ถœ๋ ฅ ๋“ฑ ๋‹ค์–‘ํ•œ ๋…ธ๋“œ ํƒ€์ž… ์ง€์›
2046
- """)
2047
-
2048
- # PPTX ๋‹ค์šด๋กœ๋“œ ์˜์—ญ
2049
- with gr.Row():
2050
- download_file = gr.File(
2051
- label="๐Ÿ“ฅ ์ƒ์„ฑ๋œ PPTX ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ",
2052
- visible=True,
2053
- elem_id="download-file"
2054
- )
2055
-
2056
- # ์‚ฌ์šฉ์ž ์ •์˜ ์„น์…˜
2057
- with gr.Accordion("๐Ÿ“ ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ", open=False, elem_id="custom_accordion") as custom_accordion:
2058
- gr.Markdown("ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š”. (3-20์žฅ)")
2059
-
2060
- # ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
2061
- custom_slide_count = gr.Slider(
2062
- minimum=3,
2063
- maximum=20,
2064
- value=5,
2065
- step=1,
2066
- label="์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜",
2067
- info="์ƒ์„ฑํ•  ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š”"
2068
- )
2069
-
2070
- custom_slides_components = create_custom_slides_ui(initial_count=5)
2071
-
2072
- # ์ƒํƒœ ํ‘œ์‹œ
2073
- status_output = gr.Markdown(
2074
- value="### ๐Ÿ‘† ํ…œํ”Œ๋ฆฟ์„ ์„ ํƒํ•˜๊ณ  ์ƒ์„ฑ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”!"
2075
- )
2076
-
2077
- # ํ”„๋ฆฌ๋ทฐ ์˜์—ญ
2078
- preview_output = gr.HTML(
2079
- label="PPT ํ”„๋ฆฌ๋ทฐ (16:9)",
2080
- elem_classes="preview-container"
2081
- )
2082
-
2083
- # ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
2084
- def load_example(template_name):
2085
- """ํ…œํ”Œ๋ฆฟ์— ๋งž๋Š” ์˜ˆ์ œ ์ฃผ์ œ ๋กœ๋“œ"""
2086
- example_topic = EXAMPLE_TOPICS.get(template_name, "")
2087
- return example_topic
2088
-
2089
- def update_audience_info(audience_type):
2090
- """์˜ค๋””์–ธ์Šค ์ •๋ณด ํ‘œ์‹œ"""
2091
- info = AUDIENCE_TYPES.get(audience_type, {})
2092
- return f"""**{info.get('description', '')}**
2093
- - ํ†ค: {info.get('tone', '')}
2094
- - ํฌ์ปค์Šค: {info.get('focus', '')}"""
2095
-
2096
- def update_theme_preview(theme_name):
2097
- """ํ…Œ๋งˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ HTML ์ƒ์„ฑ"""
2098
- theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ"])
2099
-
2100
- # RGB ๊ฐ’์„ hex๋กœ ๋ณ€ํ™˜
2101
- def rgb_to_hex(rgb_color):
2102
- return f"#{rgb_color[0]:02x}{rgb_color[1]:02x}{rgb_color[2]:02x}"
2103
-
2104
- bg_hex = rgb_to_hex(theme["background"])
2105
- box_hex = rgb_to_hex(theme["box_fill"])
2106
- title_hex = rgb_to_hex(theme["title_color"])
2107
- text_hex = rgb_to_hex(theme["text_color"])
2108
- accent_hex = rgb_to_hex(theme["accent_color"])
2109
-
2110
- preview_html = f"""
2111
- <div style="
2112
- background: {bg_hex};
2113
- padding: 20px;
2114
- border-radius: 8px;
2115
- margin: 10px 0;
2116
- ">
2117
- <div style="
2118
- background: {box_hex};
2119
- opacity: {theme['box_opacity']};
2120
- padding: 15px;
2121
- border-radius: 12px;
2122
- margin-bottom: 10px;
2123
- {'box-shadow: 0 4px 8px rgba(0,0,0,0.1);' if theme['shadow'] else ''}
2124
- ">
2125
- <h4 style="color: {title_hex}; margin: 0 0 10px 0;">{theme['name']}</h4>
2126
- <p style="color: {text_hex}; margin: 0; font-size: 14px;">
2127
- {theme['description']}
2128
- </p>
2129
- <div style="
2130
- width: 40px;
2131
- height: 4px;
2132
- background: {accent_hex};
2133
- margin-top: 10px;
2134
- border-radius: 2px;
2135
- "></div>
2136
- </div>
2137
- </div>
2138
- """
2139
- return preview_html
2140
-
2141
- def update_template_info(template_name, slide_count):
2142
- is_custom = template_name == "์‚ฌ์šฉ์ž ์ •์˜"
2143
- info = ""
2144
-
2145
- if not is_custom and template_name in PPT_TEMPLATES:
2146
- template = PPT_TEMPLATES[template_name]
2147
- info = f"**{template['description']}**\n\n"
2148
-
2149
- # ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋  ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ ํ‘œ์‹œ
2150
- slides = generate_dynamic_slides("", template, slide_count)
2151
- info += f"์ƒ์„ฑ๋  ์Šฌ๋ผ์ด๋“œ ({len(slides)}์žฅ):\n"
2152
- for i, slide in enumerate(slides):
2153
- style_info = STYLE_TEMPLATES.get(slide['style'], {})
2154
- info += f"{i+1}. {slide['title']} - {style_info.get('use_case', '')}"
2155
- if style_info.get('is_process_flow'):
2156
- info += " ๐Ÿ”ง"
2157
- info += "\n"
2158
- elif is_custom:
2159
- info = "**์‚ฌ์šฉ์ž ์ •์˜ ํ…œํ”Œ๋ฆฟ**\n\n์•„๋ž˜ '์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ' ์„น์…˜์—์„œ ์ง์ ‘ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š”."
2160
-
2161
- # ์‚ฌ์šฉ์ž ์ •์˜ ํ…œํ”Œ๋ฆฟ์ผ ๋•Œ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ ์ˆจ๊ธฐ๊ธฐ
2162
- slide_count_visible = not is_custom
2163
- custom_accordion_open = is_custom
2164
-
2165
- return info, gr.update(visible=slide_count_visible), gr.update(open=custom_accordion_open)
2166
-
2167
- def update_custom_slides_visibility(count):
2168
- """์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜์— ๋”ฐ๋ผ UI ํ‘œ์‹œ/์ˆจ๊น€"""
2169
- updates = []
2170
- for i in range(20):
2171
- updates.append(gr.update(visible=(i < count))) # row
2172
- return updates
2173
-
2174
- def generate_ppt_handler(topic, template_name, audience_type, theme_name, slide_count, seed, file_upload,
2175
- use_web_search, custom_slide_count, progress=gr.Progress(),
2176
- *custom_inputs):
2177
- if not topic.strip():
2178
- yield "", "โŒ ์ฃผ์ œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", None
2179
- return
2180
-
2181
- # ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ฒ˜๋ฆฌ
2182
- custom_slides = []
2183
- if template_name == "์‚ฌ์šฉ์ž ์ •์˜":
2184
- for i in range(0, custom_slide_count * 3, 3):
2185
- if i < len(custom_inputs):
2186
- title = custom_inputs[i]
2187
- style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None
2188
- hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else ""
2189
-
2190
- if title and style:
2191
- custom_slides.append({
2192
- "title": title,
2193
- "style": style,
2194
- "prompt_hint": hint
2195
- })
2196
-
2197
- # PPT ์ƒ์„ฑ
2198
- for preview, status, pptx_file in generate_ppt_with_content(
2199
- topic, template_name, audience_type, custom_slides, slide_count, seed,
2200
- file_upload, use_web_search, theme_name, progress
2201
- ):
2202
- yield preview, status, pptx_file
2203
-
2204
- # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
2205
- example_btn.click(
2206
- fn=load_example,
2207
- inputs=[template_select],
2208
- outputs=[topic_input]
2209
- )
2210
-
2211
- audience_select.change(
2212
- fn=update_audience_info,
2213
- inputs=[audience_select],
2214
- outputs=[audience_info]
2215
- )
2216
-
2217
- theme_select.change(
2218
- fn=update_theme_preview,
2219
- inputs=[theme_select],
2220
- outputs=[theme_preview]
2221
- )
2222
-
2223
- template_select.change(
2224
- fn=update_template_info,
2225
- inputs=[template_select, slide_count],
2226
- outputs=[template_info, slide_count, custom_accordion]
2227
- )
2228
-
2229
- slide_count.change(
2230
- fn=lambda t, s: update_template_info(t, s)[0],
2231
- inputs=[template_select, slide_count],
2232
- outputs=[template_info]
2233
- )
2234
-
2235
- custom_slide_count.change(
2236
- fn=update_custom_slides_visibility,
2237
- inputs=[custom_slide_count],
2238
- outputs=[slide["row"] for slide in custom_slides_components]
2239
- )
2240
-
2241
- # ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ ํ‰ํƒ„ํ™”
2242
- all_custom_inputs = []
2243
- for slide in custom_slides_components:
2244
- all_custom_inputs.extend([slide["title"], slide["style"], slide["hint"]])
2245
-
2246
- generate_btn.click(
2247
- fn=generate_ppt_handler,
2248
- inputs=[
2249
- topic_input, template_select, audience_select, theme_select, slide_count,
2250
- seed_input, file_upload, use_web_search, custom_slide_count
2251
- ] + all_custom_inputs,
2252
- outputs=[preview_output, status_output, download_file]
2253
- )
2254
-
2255
- # ์ดˆ๊ธฐ ๋กœ๋“œ์‹œ ํ…œํ”Œ๋ฆฟ/ํ…Œ๋งˆ ์ •๋ณด ํ‘œ์‹œ
2256
- demo.load(
2257
- fn=lambda: (update_template_info(template_select.value, slide_count.value)[0],
2258
- update_theme_preview(theme_select.value),
2259
- update_audience_info(audience_select.value)),
2260
- inputs=[],
2261
- outputs=[template_info, theme_preview, audience_info]
2262
- )
2263
 
2264
- # ์•ฑ ์‹คํ–‰
2265
  if __name__ == "__main__":
2266
- demo.launch(ssr_mode=False)
 
 
 
1
  import os
2
+ import sys
3
+ import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("โš ๏ธ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
 
22
 
23
+ # Clean up the temporary file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
+ os.unlink(tmp_path)
26
+ except:
27
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ st.error(f"โš ๏ธ Error loading or executing the application: {str(e)}")
31
+ import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
34
  if __name__ == "__main__":
35
+ main()