ginipick commited on
Commit
7d1c228
Β·
verified Β·
1 Parent(s): bbf803e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1004 -0
app.py ADDED
@@ -0,0 +1,1004 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import replicate
3
+ import requests
4
+ import os
5
+ import json
6
+ import asyncio
7
+ import concurrent.futures
8
+ from io import BytesIO
9
+ from PIL import Image
10
+ from typing import List, Tuple, Dict
11
+ import zipfile
12
+ from datetime import datetime
13
+ import time
14
+ import traceback
15
+ import base64
16
+ from pptx import Presentation
17
+ from pptx.util import Inches, Pt
18
+ from pptx.dml.color import RGBColor
19
+ from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
20
+
21
+ # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°
22
+ REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
23
+ FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
24
+
25
+ # μŠ€νƒ€μΌ μ •μ˜
26
+ STYLE_TEMPLATES = {
27
+ "3D Style (Pixar-like)": {
28
+ "name": "3D Style",
29
+ "description": "Pixar-esque 3D render with volumetric lighting",
30
+ "use_case": "ν‘œμ§€, λΉ„μ „, 미래 컨셉",
31
+ "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."
32
+ },
33
+ "Elegant SWOT Quadrant": {
34
+ "name": "SWOT Analysis",
35
+ "description": "Flat-design 4-grid layout with minimal shadows",
36
+ "use_case": "ν˜„ν™© 뢄석, μ „λž΅ 평가",
37
+ "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"
38
+ },
39
+ "Colorful Mind Map": {
40
+ "name": "Mind Map",
41
+ "description": "Hand-drawn educational style with vibrant colors",
42
+ "use_case": "λΈŒλ ˆμΈμŠ€ν† λ°, 아이디어 정리",
43
+ "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"
44
+ },
45
+ "Business Workflow": {
46
+ "name": "Business Process",
47
+ "description": "End-to-end business workflow with clear phases",
48
+ "use_case": "ν”„λ‘œμ„ΈμŠ€ μ„€λͺ…, 단계별 μ§„ν–‰",
49
+ "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"
50
+ },
51
+ "Industrial Design": {
52
+ "name": "Product Design",
53
+ "description": "Sleek industrial design concept sketch",
54
+ "use_case": "μ œν’ˆ μ†Œκ°œ, 컨셉 λ””μžμΈ",
55
+ "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"
56
+ },
57
+ "3D Bubble Chart": {
58
+ "name": "Bubble Chart",
59
+ "description": "Clean 3D bubble visualization",
60
+ "use_case": "비ꡐ 뢄석, 포지셔닝",
61
+ "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"
62
+ },
63
+ "Timeline Ribbon": {
64
+ "name": "Timeline",
65
+ "description": "Horizontal ribbon timeline with cyber-futuristic vibe",
66
+ "use_case": "일정, λ‘œλ“œλ§΅, λ§ˆμΌμŠ€ν†€",
67
+ "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"
68
+ },
69
+ "Risk Heat Map": {
70
+ "name": "Heat Map",
71
+ "description": "Risk assessment heat map with gradient colors",
72
+ "use_case": "리슀크 뢄석, μš°μ„ μˆœμœ„",
73
+ "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"
74
+ },
75
+ "Pyramid/Funnel": {
76
+ "name": "Funnel Chart",
77
+ "description": "Multi-layer gradient funnel visualization",
78
+ "use_case": "단계별 μΆ•μ†Œ, 핡심 λ„μΆœ",
79
+ "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"
80
+ },
81
+ "KPI Dashboard": {
82
+ "name": "Dashboard",
83
+ "description": "Dark-mode analytics dashboard with sci-fi interface",
84
+ "use_case": "μ„±κ³Ό μ§€ν‘œ, 싀적 λŒ€μ‹œλ³΄λ“œ",
85
+ "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"
86
+ },
87
+ "Value Chain": {
88
+ "name": "Value Chain",
89
+ "description": "Horizontal value chain with industrial look",
90
+ "use_case": "κ°€μΉ˜ μ‚¬μŠ¬, λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λΈ",
91
+ "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"
92
+ },
93
+ "Gantt Chart": {
94
+ "name": "Gantt Chart",
95
+ "description": "Hand-drawn style Gantt chart with playful colors",
96
+ "use_case": "ν”„λ‘œμ νŠΈ 일정, μž‘μ—… 관리",
97
+ "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"
98
+ },
99
+ "Mobile App Mockup": {
100
+ "name": "App Mockup",
101
+ "description": "Clean wireframe for mobile app design",
102
+ "use_case": "μ•±/μ›Ή UI, ν™”λ©΄ 섀계",
103
+ "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"
104
+ },
105
+ "Flowchart": {
106
+ "name": "Flowchart",
107
+ "description": "Vibrant flowchart with minimalistic icons",
108
+ "use_case": "μ˜μ‚¬κ²°μ •, ν”„λ‘œμ„ΈμŠ€ 흐름",
109
+ "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"
110
+ }
111
+ }
112
+
113
+ # PPT ν…œν”Œλ¦Ώ μ •μ˜
114
+ PPT_TEMPLATES = {
115
+ "λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ": {
116
+ "description": "투자 유치, 사업 μ œμ•ˆμš©",
117
+ "slides": [
118
+ {"title": "ν‘œμ§€", "style": "3D Style (Pixar-like)", "prompt_hint": "νšŒμ‚¬ λΉ„μ „κ³Ό 미래"},
119
+ {"title": "λͺ©μ°¨", "style": "Flowchart", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ ꡬ쑰"},
120
+ {"title": "문제 μ •μ˜", "style": "Colorful Mind Map", "prompt_hint": "ν˜„μž¬ μ‹œμž₯의 문제점"},
121
+ {"title": "ν˜„ν™© 뢄석", "style": "Elegant SWOT Quadrant", "prompt_hint": "강점, 약점, 기회, μœ„ν˜‘"},
122
+ {"title": "μ†”λ£¨μ…˜", "style": "Industrial Design", "prompt_hint": "μ œν’ˆ/μ„œλΉ„μŠ€ 컨셉"},
123
+ {"title": "ν”„λ‘œμ„ΈμŠ€", "style": "Business Workflow", "prompt_hint": "μ‹€ν–‰ 단계"},
124
+ {"title": "일정", "style": "Timeline Ribbon", "prompt_hint": "μ£Όμš” λ§ˆμΌμŠ€ν†€"},
125
+ {"title": "μ„±κ³Ό 예츑", "style": "KPI Dashboard", "prompt_hint": "μ˜ˆμƒ μ„±κ³Ό μ§€ν‘œ"},
126
+ {"title": "투자 μš”μ²­", "style": "Pyramid/Funnel", "prompt_hint": "투자 규λͺ¨μ™€ ν™œμš©"}
127
+ ]
128
+ },
129
+ "μ œν’ˆ μ†Œκ°œ": {
130
+ "description": "μ‹ μ œν’ˆ 런칭, μ„œλΉ„μŠ€ μ†Œκ°œμš©",
131
+ "slides": [
132
+ {"title": "μ œν’ˆ 컨셉", "style": "Industrial Design", "prompt_hint": "μ œν’ˆ λ””μžμΈ"},
133
+ {"title": "μ‚¬μš©μž λ‹ˆμ¦ˆ", "style": "Colorful Mind Map", "prompt_hint": "고객 페인포인트"},
134
+ {"title": "κΈ°λŠ₯ μ†Œκ°œ", "style": "Mobile App Mockup", "prompt_hint": "UI/UX ν™”λ©΄"},
135
+ {"title": "μž‘λ™ 원리", "style": "Flowchart", "prompt_hint": "κΈ°λŠ₯ ν”Œλ‘œμš°"},
136
+ {"title": "μ‹œμž₯ ν¬μ§€μ…˜", "style": "3D Bubble Chart", "prompt_hint": "κ²½μŸμ‚¬ 비ꡐ"},
137
+ {"title": "μΆœμ‹œ 일정", "style": "Timeline Ribbon", "prompt_hint": "런칭 λ‘œλ“œλ§΅"}
138
+ ]
139
+ },
140
+ "ν”„λ‘œμ νŠΈ 보고": {
141
+ "description": "μ§„ν–‰ 상황, μ„±κ³Ό 보고용",
142
+ "slides": [
143
+ {"title": "ν”„λ‘œμ νŠΈ κ°œμš”", "style": "Business Workflow", "prompt_hint": "전체 ν”„λ‘œμ„ΈμŠ€"},
144
+ {"title": "μ§„ν–‰ ν˜„ν™©", "style": "Gantt Chart", "prompt_hint": "μž‘μ—… 일정"},
145
+ {"title": "리슀크 관리", "style": "Risk Heat Map", "prompt_hint": "μœ„ν—˜ μš”μ†Œ"},
146
+ {"title": "μ„±κ³Ό μ§€ν‘œ", "style": "KPI Dashboard", "prompt_hint": "달성 싀적"},
147
+ {"title": "ν–₯ν›„ κ³„νš", "style": "Timeline Ribbon", "prompt_hint": "λ‹€μŒ 단계"}
148
+ ]
149
+ },
150
+ "μ „λž΅ 기획": {
151
+ "description": "쀑μž₯κΈ° μ „λž΅, λΉ„μ „ 수립용",
152
+ "slides": [
153
+ {"title": "λΉ„μ „", "style": "3D Style (Pixar-like)", "prompt_hint": "미래 λΉ„μ „"},
154
+ {"title": "ν™˜κ²½ 뢄석", "style": "Elegant SWOT Quadrant", "prompt_hint": "λ‚΄μ™ΈλΆ€ ν™˜κ²½"},
155
+ {"title": "μ „λž΅ 체계", "style": "Colorful Mind Map", "prompt_hint": "μ „λž΅ ꡬ쑰"},
156
+ {"title": "κ°€μΉ˜ μ‚¬μŠ¬", "style": "Value Chain", "prompt_hint": "λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λΈ"},
157
+ {"title": "μ‹€ν–‰ λ‘œλ“œλ§΅", "style": "Timeline Ribbon", "prompt_hint": "단계별 κ³„νš"},
158
+ {"title": "λͺ©ν‘œ μ§€ν‘œ", "style": "KPI Dashboard", "prompt_hint": "KPI λͺ©ν‘œ"}
159
+ ]
160
+ },
161
+ "μ‚¬μš©μž μ •μ˜": {
162
+ "description": "직접 κ΅¬μ„±ν•˜κΈ°",
163
+ "slides": []
164
+ }
165
+ }
166
+
167
+ def generate_slide_content(topic: str, slide_title: str, slide_context: str) -> Dict[str, str]:
168
+ """각 μŠ¬λΌμ΄λ“œμ˜ ν…μŠ€νŠΈ λ‚΄μš© 생성"""
169
+ print(f"[μŠ¬λΌμ΄λ“œ λ‚΄μš©] {slide_title} ν…μŠ€νŠΈ 생성 쀑...")
170
+
171
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
172
+ headers = {
173
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
174
+ "Content-Type": "application/json"
175
+ }
176
+
177
+ system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content.
178
+
179
+ Your task is to create:
180
+ 1. A compelling subtitle (max 10 words)
181
+ 2. Exactly 5 bullet points, each being a complete, concise sentence
182
+ 3. Each bullet point should be 10-15 words
183
+
184
+ Guidelines:
185
+ - Be specific and actionable
186
+ - Use professional business language
187
+ - Include relevant data points or metrics when appropriate
188
+ - Ensure content aligns with the slide's purpose
189
+ - Make each point distinct and valuable
190
+ - Use active voice and strong verbs
191
+
192
+ Output format:
193
+ Subtitle: [subtitle here]
194
+ β€’ [Point 1]
195
+ β€’ [Point 2]
196
+ β€’ [Point 3]
197
+ β€’ [Point 4]
198
+ β€’ [Point 5]"""
199
+
200
+ user_message = f"""Topic: {topic}
201
+ Slide Title: {slide_title}
202
+ Context: {slide_context}
203
+
204
+ Create compelling content for this presentation slide."""
205
+
206
+ payload = {
207
+ "model": "dep89a2fld32mcm",
208
+ "messages": [
209
+ {
210
+ "role": "system",
211
+ "content": system_prompt
212
+ },
213
+ {
214
+ "role": "user",
215
+ "content": user_message
216
+ }
217
+ ],
218
+ "max_tokens": 300,
219
+ "top_p": 0.8,
220
+ "temperature": 0.7,
221
+ "stream": False
222
+ }
223
+
224
+ try:
225
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
226
+ if response.status_code == 200:
227
+ result = response.json()
228
+ content = result['choices'][0]['message']['content'].strip()
229
+
230
+ # Parse content
231
+ lines = content.split('\n')
232
+ subtitle = ""
233
+ bullet_points = []
234
+
235
+ for line in lines:
236
+ if line.startswith("Subtitle:"):
237
+ subtitle = line.replace("Subtitle:", "").strip()
238
+ elif line.strip().startswith("β€’"):
239
+ bullet_points.append(line.strip())
240
+
241
+ # ν•œκΈ€λ‘œ λ²ˆμ—­μ΄ ν•„μš”ν•œ 경우
242
+ if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
243
+ subtitle = translate_content_to_korean(subtitle)
244
+ bullet_points = [translate_content_to_korean(point) for point in bullet_points]
245
+
246
+ return {
247
+ "subtitle": subtitle,
248
+ "bullet_points": bullet_points[:5] # μ΅œλŒ€ 5개
249
+ }
250
+ else:
251
+ return {
252
+ "subtitle": slide_title,
253
+ "bullet_points": ["λ‚΄μš©μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."] * 5
254
+ }
255
+ except Exception as e:
256
+ print(f"[μŠ¬λΌμ΄λ“œ λ‚΄μš©] 였λ₯˜: {str(e)}")
257
+ return {
258
+ "subtitle": slide_title,
259
+ "bullet_points": ["λ‚΄μš©μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."] * 5
260
+ }
261
+
262
+ def translate_content_to_korean(text: str) -> str:
263
+ """μ˜μ–΄ ν…μŠ€νŠΈλ₯Ό ν•œκΈ€λ‘œ λ²ˆμ—­"""
264
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
265
+ headers = {
266
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
267
+ "Content-Type": "application/json"
268
+ }
269
+
270
+ payload = {
271
+ "model": "dep89a2fld32mcm",
272
+ "messages": [
273
+ {
274
+ "role": "system",
275
+ "content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation."
276
+ },
277
+ {
278
+ "role": "user",
279
+ "content": text
280
+ }
281
+ ],
282
+ "max_tokens": 200,
283
+ "top_p": 0.8,
284
+ "stream": False
285
+ }
286
+
287
+ try:
288
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
289
+ if response.status_code == 200:
290
+ result = response.json()
291
+ return result['choices'][0]['message']['content'].strip()
292
+ else:
293
+ return text
294
+ except Exception as e:
295
+ return text
296
+
297
+ def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None) -> str:
298
+ """μ£Όμ œμ™€ μŠ€νƒ€μΌ 예제λ₯Ό λ°›μ•„μ„œ LLM을 μ‚¬μš©ν•΄ 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성"""
299
+ print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ‹œμž‘: {slide_context}")
300
+
301
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
302
+ headers = {
303
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
304
+ "Content-Type": "application/json"
305
+ }
306
+
307
+ system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides.
308
+
309
+ Your task is to create prompts that:
310
+ 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals
311
+ 2. Consider the slide's purpose and maintain consistency across a presentation
312
+ 3. Include style references matching the given example
313
+ 4. Focus on clean, professional visuals that won't distract from text overlays
314
+ 5. Ensure high contrast areas for text readability when needed
315
+ 6. Maintain brand consistency and professional aesthetics
316
+
317
+ Important guidelines:
318
+ - If given a style example, adapt the topic to match that specific visual style
319
+ - Consider the slide context (e.g., "ν‘œμ§€", "ν˜„ν™© 뢄석") to create appropriate visuals
320
+ - Always output ONLY the prompt without any explanation
321
+ - Keep prompts between 50-150 words for optimal results
322
+ - Ensure the visual supports rather than overwhelms the slide content"""
323
+
324
+ user_message = f"Topic: {topic}"
325
+ if style_example:
326
+ user_message += f"\n\nStyle reference to follow:\n{style_example}"
327
+ if slide_context:
328
+ user_message += f"\n\nSlide context: {slide_context}"
329
+
330
+ payload = {
331
+ "model": "dep89a2fld32mcm",
332
+ "messages": [
333
+ {
334
+ "role": "system",
335
+ "content": system_prompt
336
+ },
337
+ {
338
+ "role": "user",
339
+ "content": user_message
340
+ }
341
+ ],
342
+ "max_tokens": 300,
343
+ "top_p": 0.8,
344
+ "temperature": 0.7,
345
+ "stream": False
346
+ }
347
+
348
+ try:
349
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
350
+ if response.status_code == 200:
351
+ result = response.json()
352
+ prompt = result['choices'][0]['message']['content'].strip()
353
+ print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ™„λ£Œ: {prompt[:50]}...")
354
+ return prompt
355
+ else:
356
+ error_msg = f"ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: {response.status_code}"
357
+ print(f"[LLM] {error_msg}")
358
+ return error_msg
359
+ except Exception as e:
360
+ error_msg = f"ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
361
+ print(f"[LLM] {error_msg}")
362
+ return error_msg
363
+
364
+ def translate_to_english(text: str) -> str:
365
+ """ν•œκΈ€ ν…μŠ€νŠΈλ₯Ό μ˜μ–΄λ‘œ λ²ˆμ—­ (LLM μ‚¬μš©)"""
366
+ if not any(ord('κ°€') <= ord(char) <= ord('힣') for char in text):
367
+ return text
368
+
369
+ print(f"[λ²ˆμ—­] ν•œκΈ€ 감지, μ˜μ–΄λ‘œ λ²ˆμ—­ μ‹œμž‘")
370
+
371
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
372
+ headers = {
373
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
374
+ "Content-Type": "application/json"
375
+ }
376
+
377
+ payload = {
378
+ "model": "dep89a2fld32mcm",
379
+ "messages": [
380
+ {
381
+ "role": "system",
382
+ "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation."
383
+ },
384
+ {
385
+ "role": "user",
386
+ "content": text
387
+ }
388
+ ],
389
+ "max_tokens": 500,
390
+ "top_p": 0.8,
391
+ "stream": False
392
+ }
393
+
394
+ try:
395
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
396
+ if response.status_code == 200:
397
+ result = response.json()
398
+ translated = result['choices'][0]['message']['content'].strip()
399
+ print(f"[λ²ˆμ—­] μ™„λ£Œ")
400
+ return translated
401
+ else:
402
+ print(f"[λ²ˆμ—­] μ‹€νŒ¨, 원본 μ‚¬μš©")
403
+ return text
404
+ except Exception as e:
405
+ print(f"[λ²ˆμ—­] 였λ₯˜: {str(e)}, 원본 μ‚¬μš©")
406
+ return text
407
+
408
+ def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]:
409
+ """Replicate APIλ₯Ό μ‚¬μš©ν•΄ 이미지 생성"""
410
+ print(f"\n[이미지 생성] {slide_info}")
411
+ print(f"[이미지 생성] ν”„λ‘¬ν”„νŠΈ: {prompt[:50]}...")
412
+
413
+ try:
414
+ english_prompt = translate_to_english(prompt)
415
+
416
+ if not REPLICATE_API_TOKEN:
417
+ error_msg = "RAPI_TOKEN ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
418
+ print(f"[이미지 생성] 였λ₯˜: {error_msg}")
419
+ return None, error_msg
420
+
421
+ print(f"[이미지 생성] Replicate API 호좜 쀑...")
422
+ client = replicate.Client(api_token=REPLICATE_API_TOKEN)
423
+
424
+ input_params = {
425
+ "seed": seed,
426
+ "prompt": english_prompt,
427
+ "speed_mode": "Extra Juiced πŸš€ (even more speed)",
428
+ "output_quality": 100
429
+ }
430
+
431
+ start_time = time.time()
432
+ output = client.run(
433
+ "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645",
434
+ input=input_params
435
+ )
436
+
437
+ elapsed = time.time() - start_time
438
+ print(f"[이미지 생성] API 응닡 λ°›μŒ ({elapsed:.1f}초)")
439
+
440
+ if output:
441
+ if isinstance(output, str) and output.startswith('http'):
442
+ print(f"[이미지 생성] URLμ—μ„œ 이미지 λ‹€μš΄λ‘œλ“œ 쀑...")
443
+ response = requests.get(output, timeout=30)
444
+ img = Image.open(BytesIO(response.content))
445
+ print(f"[이미지 생성] μ™„λ£Œ!")
446
+ return img, english_prompt
447
+ else:
448
+ print(f"[이미지 생성] λ°”μ΄λ„ˆλ¦¬ 데이터 처리 쀑...")
449
+ img = Image.open(BytesIO(output.read()))
450
+ print(f"[이미지 생성] μ™„λ£Œ!")
451
+ return img, english_prompt
452
+ else:
453
+ error_msg = "이미지 생성 μ‹€νŒ¨ - 빈 응닡"
454
+ print(f"[이미지 생성] {error_msg}")
455
+ return None, error_msg
456
+
457
+ except Exception as e:
458
+ error_msg = f"였λ₯˜: {str(e)}"
459
+ print(f"[이미지 생성] {error_msg}")
460
+ print(f"[이미지 생성] 상세 였λ₯˜:\n{traceback.format_exc()}")
461
+ return None, error_msg
462
+
463
+ def create_slide_preview_html(slide_data: Dict) -> str:
464
+ """16:9 λΉ„μœ¨μ˜ μŠ¬λΌμ΄λ“œ 프리뷰 HTML 생성"""
465
+
466
+ # 이미지λ₯Ό base64둜 인코딩
467
+ img_base64 = ""
468
+ if slide_data.get("image"):
469
+ buffered = BytesIO()
470
+ slide_data["image"].save(buffered, format="PNG")
471
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
472
+
473
+ # ν…μŠ€νŠΈ λ‚΄μš© κ°€μ Έμ˜€κΈ°
474
+ subtitle = slide_data.get("subtitle", "")
475
+ bullet_points = slide_data.get("bullet_points", [])
476
+
477
+ # HTML 생성
478
+ html = f"""
479
+ <div class="slide-container" style="
480
+ width: 100%;
481
+ max-width: 1200px;
482
+ margin: 20px auto;
483
+ background: white;
484
+ border-radius: 8px;
485
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
486
+ overflow: hidden;
487
+ ">
488
+ <div class="slide-header" style="
489
+ background: #2c3e50;
490
+ color: white;
491
+ padding: 15px 30px;
492
+ font-size: 18px;
493
+ font-weight: bold;
494
+ ">
495
+ μŠ¬λΌμ΄λ“œ {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}
496
+ </div>
497
+
498
+ <div class="slide-content" style="
499
+ display: flex;
500
+ height: 0;
501
+ padding-bottom: 56.25%; /* 16:9 λΉ„μœ¨ */
502
+ position: relative;
503
+ ">
504
+ <div class="slide-inner" style="
505
+ position: absolute;
506
+ top: 0;
507
+ left: 0;
508
+ width: 100%;
509
+ height: 100%;
510
+ display: flex;
511
+ ">
512
+ <!-- ν…μŠ€νŠΈ μ˜μ—­ (쒌츑) -->
513
+ <div class="text-area" style="
514
+ flex: 1;
515
+ padding: 40px;
516
+ display: flex;
517
+ flex-direction: column;
518
+ justify-content: center;
519
+ background: #f8f9fa;
520
+ ">
521
+ <h2 style="
522
+ color: #2c3e50;
523
+ font-size: 28px;
524
+ margin-bottom: 30px;
525
+ font-weight: 600;
526
+ ">{subtitle}</h2>
527
+
528
+ <ul style="
529
+ list-style: none;
530
+ padding: 0;
531
+ margin: 0;
532
+ ">
533
+ """
534
+
535
+ for point in bullet_points:
536
+ html += f"""
537
+ <li style="
538
+ margin-bottom: 15px;
539
+ padding-left: 25px;
540
+ position: relative;
541
+ color: #34495e;
542
+ font-size: 16px;
543
+ line-height: 1.6;
544
+ ">
545
+ <span style="
546
+ position: absolute;
547
+ left: 0;
548
+ color: #3498db;
549
+ ">β–Ά</span>
550
+ {point.replace('β€’', '').strip()}
551
+ </li>
552
+ """
553
+
554
+ html += f"""
555
+ </ul>
556
+ </div>
557
+
558
+ <!-- 이미지 μ˜μ—­ (우츑) -->
559
+ <div class="image-area" style="
560
+ flex: 1;
561
+ background: #e9ecef;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ padding: 20px;
566
+ ">
567
+ """
568
+
569
+ if img_base64:
570
+ html += f"""
571
+ <img src="data:image/png;base64,{img_base64}" style="
572
+ max-width: 100%;
573
+ max-height: 100%;
574
+ object-fit: contain;
575
+ border-radius: 4px;
576
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
577
+ " alt="Slide Image">
578
+ """
579
+ else:
580
+ html += """
581
+ <div style="
582
+ color: #6c757d;
583
+ text-align: center;
584
+ ">
585
+ <div style="font-size: 48px;">πŸ–ΌοΈ</div>
586
+ <p>이미지 생성 쀑...</p>
587
+ </div>
588
+ """
589
+
590
+ html += """
591
+ </div>
592
+ </div>
593
+ </div>
594
+ </div>
595
+ """
596
+
597
+ return html
598
+
599
+ def create_pptx_file(results: List[Dict], topic: str, template_name: str) -> str:
600
+ """μƒμ„±λœ κ²°κ³Όλ₯Ό PPTX 파일둜 λ³€ν™˜"""
601
+ print("[PPTX] 파일 생성 μ‹œμž‘...")
602
+
603
+ # ν”„λ ˆμ  ν…Œμ΄μ…˜ 생성 (16:9 λΉ„μœ¨)
604
+ prs = Presentation()
605
+ prs.slide_width = Inches(16)
606
+ prs.slide_height = Inches(9)
607
+
608
+ # 타이틀 μŠ¬λΌμ΄λ“œ λ ˆμ΄μ•„μ›ƒ
609
+ title_layout = prs.slide_layouts[0]
610
+
611
+ # ν‘œμ§€ μŠ¬λΌμ΄λ“œ μΆ”κ°€
612
+ slide = prs.slides.add_slide(title_layout)
613
+ title = slide.shapes.title
614
+ subtitle = slide.placeholders[1]
615
+
616
+ title.text = topic
617
+ subtitle.text = f"{template_name} - AI둜 μƒμ„±λœ ν”„λ ˆμ  ν…Œμ΄μ…˜"
618
+
619
+ # 각 κ²°κ³Ό μŠ¬λΌμ΄λ“œ μΆ”κ°€
620
+ for i, result in enumerate(results):
621
+ if not result.get("success", False):
622
+ continue
623
+
624
+ slide_data = result.get("slide_data", {})
625
+
626
+ # 빈 λ ˆμ΄μ•„μ›ƒ μ‚¬μš©
627
+ blank_layout = prs.slide_layouts[5]
628
+ slide = prs.slides.add_slide(blank_layout)
629
+
630
+ # μŠ¬λΌμ΄λ“œ 제λͺ© μΆ”κ°€ (상단)
631
+ title_box = slide.shapes.add_textbox(
632
+ Inches(0.5), Inches(0.3),
633
+ Inches(15), Inches(0.8)
634
+ )
635
+ title_frame = title_box.text_frame
636
+ title_frame.text = f"{slide_data.get('title', '')}"
637
+ title_para = title_frame.paragraphs[0]
638
+ title_para.font.size = Pt(28)
639
+ title_para.font.bold = True
640
+ title_para.font.color.rgb = RGBColor(44, 62, 80) # μ§„ν•œ νŒŒλž€μƒ‰
641
+
642
+ # 쒌츑 ν…μŠ€νŠΈ μ˜μ—­
643
+ text_box = slide.shapes.add_textbox(
644
+ Inches(0.5), Inches(1.5),
645
+ Inches(7.5), Inches(6.5)
646
+ )
647
+ text_frame = text_box.text_frame
648
+ text_frame.word_wrap = True
649
+
650
+ # μ†Œμ œλͺ© μΆ”κ°€
651
+ subtitle_para = text_frame.paragraphs[0]
652
+ subtitle_para.text = slide_data.get('subtitle', '')
653
+ subtitle_para.font.size = Pt(20)
654
+ subtitle_para.font.bold = True
655
+ subtitle_para.font.color.rgb = RGBColor(52, 73, 94)
656
+ subtitle_para.space_after = Pt(20)
657
+
658
+ # 뢈릿 포인트 μΆ”κ°€
659
+ bullet_points = slide_data.get('bullet_points', [])
660
+ for point in bullet_points:
661
+ p = text_frame.add_paragraph()
662
+ p.text = point.replace('β€’', '').strip()
663
+ p.font.size = Pt(16)
664
+ p.font.color.rgb = RGBColor(52, 73, 94)
665
+ p.level = 0
666
+ p.space_after = Pt(12)
667
+ p.line_spacing = 1.5
668
+
669
+ # 우츑 이미지 μΆ”κ°€
670
+ if slide_data.get('image'):
671
+ try:
672
+ # PIL 이미지λ₯Ό BytesIO둜 λ³€ν™˜
673
+ img_buffer = BytesIO()
674
+ slide_data['image'].save(img_buffer, format='PNG')
675
+ img_buffer.seek(0)
676
+
677
+ # 이미지 μΆ”κ°€ (우츑)
678
+ pic = slide.shapes.add_picture(
679
+ img_buffer,
680
+ Inches(8.5), Inches(1.5),
681
+ width=Inches(7), height=Inches(6)
682
+ )
683
+
684
+ # 이미지 ν…Œλ‘λ¦¬ μŠ€νƒ€μΌ
685
+ pic.line.color.rgb = RGBColor(189, 195, 199)
686
+ pic.line.width = Pt(1)
687
+
688
+ except Exception as e:
689
+ print(f"[PPTX] 이미지 μΆ”κ°€ μ‹€νŒ¨: {str(e)}")
690
+
691
+ # νŽ˜μ΄μ§€ 번호 μΆ”κ°€
692
+ page_num = slide.shapes.add_textbox(
693
+ Inches(15), Inches(8.5),
694
+ Inches(1), Inches(0.5)
695
+ )
696
+ page_frame = page_num.text_frame
697
+ page_frame.text = str(i + 2) # ν‘œμ§€κ°€ 1νŽ˜μ΄μ§€
698
+ page_para = page_frame.paragraphs[0]
699
+ page_para.font.size = Pt(12)
700
+ page_para.font.color.rgb = RGBColor(127, 140, 141)
701
+ page_para.alignment = PP_ALIGN.RIGHT
702
+
703
+ # 파일 μ €μž₯
704
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
705
+ filename = f"presentation_{timestamp}.pptx"
706
+ filepath = os.path.join("/tmp", filename)
707
+ prs.save(filepath)
708
+
709
+ print(f"[PPTX] 파일 생성 μ™„λ£Œ: {filename}")
710
+ return filepath
711
+
712
+ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict], seed: int, progress=gr.Progress()):
713
+ """PPT 이미지와 ν…μŠ€νŠΈ λ‚΄μš©μ„ ν•¨κ»˜ 생성"""
714
+ results = []
715
+ preview_html = ""
716
+
717
+ # ν…œν”Œλ¦Ώ 선택
718
+ if template_name == "μ‚¬μš©μž μ •μ˜" and custom_slides:
719
+ slides = custom_slides
720
+ else:
721
+ slides = PPT_TEMPLATES[template_name]["slides"]
722
+
723
+ if not slides:
724
+ yield "", "μŠ¬λΌμ΄λ“œκ°€ μ •μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.", None
725
+ return
726
+
727
+ total_slides = len(slides)
728
+ print(f"\n[PPT 생성] μ‹œμž‘ - 총 {total_slides}개 μŠ¬λΌμ΄λ“œ")
729
+ print(f"[PPT 생성] 주제: {topic}")
730
+ print(f"[PPT 생성] ν…œν”Œλ¦Ώ: {template_name}")
731
+
732
+ # CSS μŠ€νƒ€μΌ οΏ½οΏ½οΏ½κ°€
733
+ preview_html = """
734
+ <style>
735
+ .slides-container {
736
+ width: 100%;
737
+ max-width: 1400px;
738
+ margin: 0 auto;
739
+ }
740
+ </style>
741
+ <div class="slides-container">
742
+ """
743
+
744
+ # 각 μŠ¬λΌμ΄λ“œ 순차 처리
745
+ for i, slide in enumerate(slides):
746
+ progress((i + 1) / (total_slides + 1), f"μŠ¬λΌμ΄λ“œ {i+1}/{total_slides} 처리 쀑...")
747
+
748
+ slide_info = f"μŠ¬λΌμ΄λ“œ {i+1}: {slide['title']}"
749
+
750
+ # ν…μŠ€νŠΈ λ‚΄μš© 생성
751
+ slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
752
+ content = generate_slide_content(topic, slide['title'], slide_context)
753
+
754
+ # ν”„λ‘¬ν”„νŠΈ 생성 및 이미지 생성
755
+ style_key = slide["style"]
756
+ if style_key in STYLE_TEMPLATES:
757
+ style_info = STYLE_TEMPLATES[style_key]
758
+ prompt = generate_prompt_with_llm(topic, style_info["example"], slide_context)
759
+
760
+ # 이미지 생성
761
+ slide_seed = seed + i
762
+ img, used_prompt = generate_image(prompt, slide_seed, slide_info)
763
+
764
+ # μŠ¬λΌμ΄λ“œ 데이터 ꡬ성
765
+ slide_data = {
766
+ "slide_number": i + 1,
767
+ "title": slide["title"],
768
+ "subtitle": content["subtitle"],
769
+ "bullet_points": content["bullet_points"],
770
+ "image": img,
771
+ "style": style_info["name"]
772
+ }
773
+
774
+ # 프리뷰 HTML 생성
775
+ preview_html += create_slide_preview_html(slide_data)
776
+
777
+ # ν˜„μž¬κΉŒμ§€μ˜ μƒνƒœ μ—…λ°μ΄νŠΈ
778
+ yield preview_html + "</div>", f"### πŸ”„ {slide_info} 생성 쀑...", None
779
+
780
+ results.append({
781
+ "slide_data": slide_data,
782
+ "success": img is not None
783
+ })
784
+
785
+ # PPTX 파일 생성
786
+ progress(0.95, "PPTX 파일 생성 쀑...")
787
+ pptx_path = None
788
+ try:
789
+ pptx_path = create_pptx_file(results, topic, template_name)
790
+ except Exception as e:
791
+ print(f"[PPTX] 파일 생성 였λ₯˜: {str(e)}")
792
+
793
+ # μ΅œμ’… κ²°κ³Ό
794
+ preview_html += "</div>"
795
+ progress(1.0, "μ™„λ£Œ!")
796
+ successful = sum(1 for r in results if r["success"])
797
+ final_status = f"### πŸŽ‰ 생성 μ™„λ£Œ! 총 {total_slides}개 μŠ¬λΌμ΄λ“œ 쀑 {successful}개 성곡"
798
+
799
+ if pptx_path:
800
+ final_status += f"\n\n### πŸ“₯ PPTX 파일이 μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€! μ•„λž˜μ—μ„œ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”."
801
+
802
+ yield preview_html, final_status, pptx_path
803
+
804
+ def create_custom_slides_ui():
805
+ """μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ ꡬ성 UI"""
806
+ slides = []
807
+ for i in range(10):
808
+ with gr.Row():
809
+ with gr.Column(scale=2):
810
+ title = gr.Textbox(
811
+ label=f"μŠ¬λΌμ΄λ“œ {i+1} 제λͺ©",
812
+ placeholder="예: ν‘œμ§€, λͺ©μ°¨, ν˜„ν™© 뢄석...",
813
+ visible=(i < 3)
814
+ )
815
+ with gr.Column(scale=3):
816
+ style = gr.Dropdown(
817
+ choices=list(STYLE_TEMPLATES.keys()),
818
+ label=f"μŠ€νƒ€μΌ 선택",
819
+ visible=(i < 3)
820
+ )
821
+ with gr.Column(scale=3):
822
+ hint = gr.Textbox(
823
+ label=f"ν”„λ‘¬ν”„νŠΈ 힌트",
824
+ placeholder="이 μŠ¬λΌμ΄λ“œμ—μ„œ ν‘œν˜„ν•˜κ³  싢은 λ‚΄μš©",
825
+ visible=(i < 3)
826
+ )
827
+ slides.append({"title": title, "style": style, "hint": hint})
828
+ return slides
829
+
830
+ # Gradio μΈν„°νŽ˜μ΄μŠ€ 생성
831
+ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
832
+ .preview-container { max-width: 1400px; margin: 0 auto; }
833
+ """) as demo:
834
+ gr.Markdown("""
835
+ # 🎯 AI 기반 PPT 톡합 생성기
836
+
837
+ ### ν…μŠ€νŠΈμ™€ 이미지가 μ™„λ²½ν•˜κ²Œ μ‘°ν™”λœ ν”„λ ˆμ  ν…Œμ΄μ…˜μ„ μžλ™μœΌλ‘œ μƒμ„±ν•˜κ³  λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”!
838
+ """)
839
+
840
+ # API 토큰 μƒνƒœ 확인
841
+ if not REPLICATE_API_TOKEN:
842
+ gr.Markdown("⚠️ **κ²½κ³ **: RAPI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
843
+ if not FRIENDLI_TOKEN:
844
+ gr.Markdown("⚠️ **κ²½κ³ **: FRIENDLI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
845
+
846
+ with gr.Row():
847
+ with gr.Column(scale=1):
848
+ # κΈ°λ³Έ μž…λ ₯
849
+ topic_input = gr.Textbox(
850
+ label="ν”„λ ˆμ  ν…Œμ΄μ…˜ 주제",
851
+ placeholder="예: AI μŠ€νƒ€νŠΈμ—… 투자 유치, μ‹ μ œν’ˆ 런칭, λ””μ§€ν„Έ μ „ν™˜ μ „λž΅",
852
+ lines=2
853
+ )
854
+
855
+ # PPT ν…œν”Œλ¦Ώ 선택
856
+ template_select = gr.Dropdown(
857
+ choices=list(PPT_TEMPLATES.keys()),
858
+ label="PPT ν…œν”Œλ¦Ώ 선택",
859
+ value="λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ",
860
+ info="λͺ©μ μ— λ§žλŠ” ν…œν”Œλ¦Ώμ„ μ„ νƒν•˜μ„Έμš”"
861
+ )
862
+
863
+ # ν…œν”Œλ¦Ώ μ„€λͺ…
864
+ template_info = gr.Markdown()
865
+
866
+ # μ‹œλ“œ κ°’
867
+ seed_input = gr.Slider(
868
+ minimum=1,
869
+ maximum=100,
870
+ value=10,
871
+ step=1,
872
+ label="μ‹œλ“œ κ°’"
873
+ )
874
+
875
+ generate_btn = gr.Button("πŸš€ PPT 전체 생성 (ν…μŠ€νŠΈ + 이미지)", variant="primary", size="lg")
876
+
877
+ # PPTX λ‹€μš΄λ‘œλ“œ μ˜μ—­
878
+ with gr.Row():
879
+ download_file = gr.File(
880
+ label="πŸ“₯ μƒμ„±λœ PPTX 파일 λ‹€μš΄λ‘œλ“œ",
881
+ visible=True,
882
+ elem_id="download-file"
883
+ )
884
+
885
+ # μ‚¬μš©μž μ •μ˜ μ„Ήμ…˜
886
+ with gr.Accordion("πŸ“ μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ ꡬ성", open=False) as custom_accordion:
887
+ gr.Markdown("ν…œν”Œλ¦Ώμ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³  직접 μŠ¬λΌμ΄λ“œλ₯Ό κ΅¬μ„±ν•˜μ„Έμš”.")
888
+ custom_slides_components = create_custom_slides_ui()
889
+
890
+ # μƒνƒœ ν‘œμ‹œ
891
+ status_output = gr.Markdown(
892
+ value="### πŸ‘† ν…œν”Œλ¦Ώμ„ μ„ νƒν•˜κ³  생성 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”!"
893
+ )
894
+
895
+ # 프리뷰 μ˜μ—­
896
+ preview_output = gr.HTML(
897
+ label="PPT 프리뷰 (16:9)",
898
+ elem_classes="preview-container"
899
+ )
900
+
901
+ # ν™œμš© 팁
902
+ gr.Markdown("""
903
+ ---
904
+ ### πŸ’‘ μƒˆλ‘œμš΄ κΈ°λŠ₯:
905
+
906
+ 1. **μžλ™ ν…μŠ€νŠΈ 생성**: 각 μŠ¬λΌμ΄λ“œλ§ˆλ‹€ μ μ ˆν•œ 제λͺ©κ³Ό 5개의 핡심 포인트 μžλ™ 생성
907
+ 2. **16:9 프리뷰**: μ‹€μ œ PPT와 λ™μΌν•œ λΉ„μœ¨λ‘œ 미리보기
908
+ 3. **PPTX λ‹€μš΄λ‘œλ“œ**: μƒμ„±λœ λ‚΄μš©μ„ μ‹€μ œ PowerPoint 파일둜 λ‹€μš΄λ‘œλ“œ
909
+ 4. **쒌우 λ ˆμ΄μ•„μ›ƒ**: 쒌츑 ν…μŠ€νŠΈ, 우츑 μ΄λ―Έμ§€λ‘œ κΉ”λ”ν•œ ꡬ성
910
+
911
+ ### πŸ“Œ ν•„μš”ν•œ μΆ”κ°€ νŒ¨ν‚€μ§€:
912
+ ```bash
913
+ pip install python-pptx
914
+ ```
915
+
916
+ ### πŸ“Š ν™œμš© 팁:
917
+ - λ‹€μš΄λ‘œλ“œν•œ PPTX νŒŒμΌμ€ PowerPointμ—μ„œ λ°”λ‘œ νŽΈμ§‘ κ°€λŠ₯
918
+ - 폰트, 색상, λ ˆμ΄μ•„μ›ƒμ„ μΆ”κ°€λ‘œ μ»€μŠ€ν„°λ§ˆμ΄μ§• κ°€λŠ₯
919
+ - μƒμ„±λœ μ΄λ―Έμ§€λŠ” κ³ ν•΄μƒλ„λ‘œ PPT에 포함됨
920
+ """)
921
+
922
+ # 이벀트 ν•Έλ“€λŸ¬
923
+ def update_template_info(template_name):
924
+ if template_name in PPT_TEMPLATES:
925
+ template = PPT_TEMPLATES[template_name]
926
+ info = f"**{template['description']}**\n\nν¬ν•¨λœ μŠ¬λΌμ΄λ“œ:\n"
927
+ for i, slide in enumerate(template['slides']):
928
+ info += f"{i+1}. {slide['title']} - {STYLE_TEMPLATES[slide['style']]['use_case']}\n"
929
+ return info
930
+ return ""
931
+
932
+ def generate_ppt_handler(topic, template_name, seed, progress=gr.Progress(), *custom_inputs):
933
+ if not topic.strip():
934
+ yield "", "❌ 주제λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.", None
935
+ return
936
+
937
+ # μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ 처리
938
+ custom_slides = []
939
+ if template_name == "μ‚¬μš©μž μ •μ˜":
940
+ for i in range(0, len(custom_inputs), 3):
941
+ title = custom_inputs[i]
942
+ style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None
943
+ hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else ""
944
+
945
+ if title and style:
946
+ custom_slides.append({
947
+ "title": title,
948
+ "style": style,
949
+ "prompt_hint": hint
950
+ })
951
+
952
+ # PPT 생성
953
+ for preview, status, pptx_file in generate_ppt_with_content(topic, template_name, custom_slides, seed, progress):
954
+ yield preview, status, pptx_file
955
+
956
+ # 이벀트 μ—°κ²°
957
+ template_select.change(
958
+ fn=update_template_info,
959
+ inputs=[template_select],
960
+ outputs=[template_info]
961
+ )
962
+
963
+ # μ‚¬μš©μž μ •μ˜ μž…λ ₯ μˆ˜μ§‘
964
+ all_custom_inputs = []
965
+ for slide_components in custom_slides_components:
966
+ all_custom_inputs.extend([
967
+ slide_components["title"],
968
+ slide_components["style"],
969
+ slide_components["hint"]
970
+ ])
971
+
972
+ generate_btn.click(
973
+ fn=generate_ppt_handler,
974
+ inputs=[topic_input, template_select, seed_input] + all_custom_inputs,
975
+ outputs=[preview_output, status_output, download_file]
976
+ )
977
+
978
+ # 초기 ν…œν”Œλ¦Ώ 정보 ν‘œμ‹œ
979
+ demo.load(
980
+ fn=update_template_info,
981
+ inputs=[template_select],
982
+ outputs=[template_info]
983
+ )
984
+
985
+ # μ•± μ‹€ν–‰
986
+ if __name__ == "__main__":
987
+ print("\n" + "="*50)
988
+ print("πŸš€ PPT 톡합 생성기 μ‹œμž‘!")
989
+ print("="*50)
990
+
991
+ # ν™˜κ²½ λ³€μˆ˜ 확인
992
+ if not REPLICATE_API_TOKEN:
993
+ print("⚠️ κ²½κ³ : RAPI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
994
+ else:
995
+ print("βœ… RAPI_TOKEN 확인됨")
996
+
997
+ if not FRIENDLI_TOKEN:
998
+ print("⚠️ κ²½κ³ : FRIENDLI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
999
+ else:
1000
+ print("βœ… FRIENDLI_TOKEN 확인됨")
1001
+
1002
+ print("="*50 + "\n")
1003
+
1004
+ demo.launch()