ginipick commited on
Commit
4957e25
Β·
verified Β·
1 Parent(s): 3989466

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +613 -0
app.py ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
16
+ # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°
17
+ REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
18
+ FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
19
+
20
+ # μŠ€νƒ€μΌ μ •μ˜
21
+ STYLE_TEMPLATES = {
22
+ "3D Style (Pixar-like)": {
23
+ "name": "3D Style",
24
+ "description": "Pixar-esque 3D render with volumetric lighting",
25
+ "use_case": "ν‘œμ§€, λΉ„μ „, 미래 컨셉",
26
+ "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."
27
+ },
28
+ "Elegant SWOT Quadrant": {
29
+ "name": "SWOT Analysis",
30
+ "description": "Flat-design 4-grid layout with minimal shadows",
31
+ "use_case": "ν˜„ν™© 뢄석, μ „λž΅ 평가",
32
+ "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"
33
+ },
34
+ "Colorful Mind Map": {
35
+ "name": "Mind Map",
36
+ "description": "Hand-drawn educational style with vibrant colors",
37
+ "use_case": "λΈŒλ ˆμΈμŠ€ν† λ°, 아이디어 정리",
38
+ "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"
39
+ },
40
+ "Business Workflow": {
41
+ "name": "Business Process",
42
+ "description": "End-to-end business workflow with clear phases",
43
+ "use_case": "ν”„λ‘œμ„ΈμŠ€ μ„€λͺ…, 단계별 μ§„ν–‰",
44
+ "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"
45
+ },
46
+ "Industrial Design": {
47
+ "name": "Product Design",
48
+ "description": "Sleek industrial design concept sketch",
49
+ "use_case": "μ œν’ˆ μ†Œκ°œ, 컨셉 λ””μžμΈ",
50
+ "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"
51
+ },
52
+ "3D Bubble Chart": {
53
+ "name": "Bubble Chart",
54
+ "description": "Clean 3D bubble visualization",
55
+ "use_case": "비ꡐ 뢄석, 포지셔닝",
56
+ "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"
57
+ },
58
+ "Timeline Ribbon": {
59
+ "name": "Timeline",
60
+ "description": "Horizontal ribbon timeline with cyber-futuristic vibe",
61
+ "use_case": "일정, λ‘œλ“œλ§΅, λ§ˆμΌμŠ€ν†€",
62
+ "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"
63
+ },
64
+ "Risk Heat Map": {
65
+ "name": "Heat Map",
66
+ "description": "Risk assessment heat map with gradient colors",
67
+ "use_case": "리슀크 뢄석, μš°μ„ μˆœμœ„",
68
+ "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"
69
+ },
70
+ "Pyramid/Funnel": {
71
+ "name": "Funnel Chart",
72
+ "description": "Multi-layer gradient funnel visualization",
73
+ "use_case": "단계별 μΆ•μ†Œ, 핡심 λ„μΆœ",
74
+ "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"
75
+ },
76
+ "KPI Dashboard": {
77
+ "name": "Dashboard",
78
+ "description": "Dark-mode analytics dashboard with sci-fi interface",
79
+ "use_case": "μ„±κ³Ό μ§€ν‘œ, 싀적 λŒ€μ‹œλ³΄λ“œ",
80
+ "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"
81
+ },
82
+ "Value Chain": {
83
+ "name": "Value Chain",
84
+ "description": "Horizontal value chain with industrial look",
85
+ "use_case": "κ°€μΉ˜ μ‚¬μŠ¬, λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λΈ",
86
+ "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"
87
+ },
88
+ "Gantt Chart": {
89
+ "name": "Gantt Chart",
90
+ "description": "Hand-drawn style Gantt chart with playful colors",
91
+ "use_case": "ν”„λ‘œμ νŠΈ 일정, μž‘μ—… 관리",
92
+ "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"
93
+ },
94
+ "Mobile App Mockup": {
95
+ "name": "App Mockup",
96
+ "description": "Clean wireframe for mobile app design",
97
+ "use_case": "μ•±/μ›Ή UI, ν™”λ©΄ 섀계",
98
+ "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"
99
+ },
100
+ "Flowchart": {
101
+ "name": "Flowchart",
102
+ "description": "Vibrant flowchart with minimalistic icons",
103
+ "use_case": "μ˜μ‚¬κ²°μ •, ν”„λ‘œμ„ΈμŠ€ 흐름",
104
+ "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"
105
+ }
106
+ }
107
+
108
+ # PPT ν…œν”Œλ¦Ώ μ •μ˜
109
+ PPT_TEMPLATES = {
110
+ "λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ": {
111
+ "description": "투자 유치, 사업 μ œμ•ˆμš©",
112
+ "slides": [
113
+ {"title": "ν‘œμ§€", "style": "3D Style (Pixar-like)", "prompt_hint": "νšŒμ‚¬ λΉ„μ „κ³Ό 미래"},
114
+ {"title": "λͺ©μ°¨", "style": "Flowchart", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ ꡬ쑰"},
115
+ {"title": "문제 μ •μ˜", "style": "Colorful Mind Map", "prompt_hint": "ν˜„μž¬ μ‹œμž₯의 문제점"},
116
+ {"title": "ν˜„ν™© 뢄석", "style": "Elegant SWOT Quadrant", "prompt_hint": "강점, 약점, 기회, μœ„ν˜‘"},
117
+ {"title": "μ†”λ£¨μ…˜", "style": "Industrial Design", "prompt_hint": "μ œν’ˆ/μ„œλΉ„μŠ€ 컨셉"},
118
+ {"title": "ν”„λ‘œμ„ΈμŠ€", "style": "Business Workflow", "prompt_hint": "μ‹€ν–‰ 단계"},
119
+ {"title": "일정", "style": "Timeline Ribbon", "prompt_hint": "μ£Όμš” λ§ˆμΌμŠ€ν†€"},
120
+ {"title": "μ„±κ³Ό 예츑", "style": "KPI Dashboard", "prompt_hint": "μ˜ˆμƒ μ„±κ³Ό μ§€ν‘œ"},
121
+ {"title": "투자 μš”μ²­", "style": "Pyramid/Funnel", "prompt_hint": "투자 규λͺ¨μ™€ ν™œμš©"}
122
+ ]
123
+ },
124
+ "μ œν’ˆ μ†Œκ°œ": {
125
+ "description": "μ‹ μ œν’ˆ 런칭, μ„œλΉ„μŠ€ μ†Œκ°œμš©",
126
+ "slides": [
127
+ {"title": "μ œν’ˆ 컨셉", "style": "Industrial Design", "prompt_hint": "μ œν’ˆ λ””μžμΈ"},
128
+ {"title": "μ‚¬μš©μž λ‹ˆμ¦ˆ", "style": "Colorful Mind Map", "prompt_hint": "고객 페인포인트"},
129
+ {"title": "κΈ°λŠ₯ μ†Œκ°œ", "style": "Mobile App Mockup", "prompt_hint": "UI/UX ν™”λ©΄"},
130
+ {"title": "μž‘λ™ 원리", "style": "Flowchart", "prompt_hint": "κΈ°λŠ₯ ν”Œλ‘œμš°"},
131
+ {"title": "μ‹œμž₯ ν¬μ§€μ…˜", "style": "3D Bubble Chart", "prompt_hint": "κ²½μŸμ‚¬ 비ꡐ"},
132
+ {"title": "μΆœμ‹œ 일정", "style": "Timeline Ribbon", "prompt_hint": "런칭 λ‘œλ“œλ§΅"}
133
+ ]
134
+ },
135
+ "ν”„λ‘œμ νŠΈ 보고": {
136
+ "description": "μ§„ν–‰ 상황, μ„±κ³Ό 보고용",
137
+ "slides": [
138
+ {"title": "ν”„λ‘œμ νŠΈ κ°œμš”", "style": "Business Workflow", "prompt_hint": "전체 ν”„λ‘œμ„ΈμŠ€"},
139
+ {"title": "μ§„ν–‰ ν˜„ν™©", "style": "Gantt Chart", "prompt_hint": "μž‘μ—… 일정"},
140
+ {"title": "리슀크 관리", "style": "Risk Heat Map", "prompt_hint": "μœ„ν—˜ μš”μ†Œ"},
141
+ {"title": "μ„±κ³Ό μ§€ν‘œ", "style": "KPI Dashboard", "prompt_hint": "달성 싀적"},
142
+ {"title": "ν–₯ν›„ κ³„νš", "style": "Timeline Ribbon", "prompt_hint": "λ‹€μŒ 단계"}
143
+ ]
144
+ },
145
+ "μ „λž΅ 기획": {
146
+ "description": "쀑μž₯κΈ° μ „λž΅, λΉ„μ „ 수립용",
147
+ "slides": [
148
+ {"title": "λΉ„μ „", "style": "3D Style (Pixar-like)", "prompt_hint": "미래 λΉ„μ „"},
149
+ {"title": "ν™˜κ²½ 뢄석", "style": "Elegant SWOT Quadrant", "prompt_hint": "λ‚΄μ™ΈλΆ€ ν™˜κ²½"},
150
+ {"title": "μ „λž΅ 체계", "style": "Colorful Mind Map", "prompt_hint": "μ „λž΅ ꡬ쑰"},
151
+ {"title": "κ°€μΉ˜ μ‚¬μŠ¬", "style": "Value Chain", "prompt_hint": "λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λΈ"},
152
+ {"title": "μ‹€ν–‰ λ‘œλ“œλ§΅", "style": "Timeline Ribbon", "prompt_hint": "단계별 κ³„νš"},
153
+ {"title": "λͺ©ν‘œ μ§€ν‘œ", "style": "KPI Dashboard", "prompt_hint": "KPI λͺ©ν‘œ"}
154
+ ]
155
+ },
156
+ "μ‚¬μš©μž μ •μ˜": {
157
+ "description": "직접 κ΅¬μ„±ν•˜κΈ°",
158
+ "slides": []
159
+ }
160
+ }
161
+
162
+ def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None) -> str:
163
+ """μ£Όμ œμ™€ μŠ€νƒ€μΌ 예제λ₯Ό λ°›μ•„μ„œ LLM을 사��해 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성"""
164
+ print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ‹œμž‘: {slide_context}")
165
+
166
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
167
+ headers = {
168
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
169
+ "Content-Type": "application/json"
170
+ }
171
+
172
+ system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides.
173
+
174
+ Your task is to create prompts that:
175
+ 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals
176
+ 2. Consider the slide's purpose and maintain consistency across a presentation
177
+ 3. Include style references matching the given example
178
+ 4. Focus on clean, professional visuals that won't distract from text overlays
179
+ 5. Ensure high contrast areas for text readability when needed
180
+ 6. Maintain brand consistency and professional aesthetics
181
+
182
+ Important guidelines:
183
+ - If given a style example, adapt the topic to match that specific visual style
184
+ - Consider the slide context (e.g., "ν‘œμ§€", "ν˜„ν™© 뢄석") to create appropriate visuals
185
+ - Always output ONLY the prompt without any explanation
186
+ - Keep prompts between 50-150 words for optimal results
187
+ - Ensure the visual supports rather than overwhelms the slide content"""
188
+
189
+ user_message = f"Topic: {topic}"
190
+ if style_example:
191
+ user_message += f"\n\nStyle reference to follow:\n{style_example}"
192
+ if slide_context:
193
+ user_message += f"\n\nSlide context: {slide_context}"
194
+
195
+ payload = {
196
+ "model": "dep89a2fld32mcm",
197
+ "messages": [
198
+ {
199
+ "role": "system",
200
+ "content": system_prompt
201
+ },
202
+ {
203
+ "role": "user",
204
+ "content": user_message
205
+ }
206
+ ],
207
+ "max_tokens": 300,
208
+ "top_p": 0.8,
209
+ "temperature": 0.7,
210
+ "stream": False
211
+ }
212
+
213
+ try:
214
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
215
+ if response.status_code == 200:
216
+ result = response.json()
217
+ prompt = result['choices'][0]['message']['content'].strip()
218
+ print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ™„λ£Œ: {prompt[:50]}...")
219
+ return prompt
220
+ else:
221
+ error_msg = f"ν”„λ‘¬ν”„νŠΈ 생성 μ‹€νŒ¨: {response.status_code}"
222
+ print(f"[LLM] {error_msg}")
223
+ return error_msg
224
+ except Exception as e:
225
+ error_msg = f"ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
226
+ print(f"[LLM] {error_msg}")
227
+ return error_msg
228
+
229
+ def translate_to_english(text: str) -> str:
230
+ """ν•œκΈ€ ν…μŠ€νŠΈλ₯Ό μ˜μ–΄λ‘œ λ²ˆμ—­ (LLM μ‚¬μš©)"""
231
+ if not any(ord('κ°€') <= ord(char) <= ord('힣') for char in text):
232
+ return text
233
+
234
+ print(f"[λ²ˆμ—­] ν•œκΈ€ 감지, μ˜μ–΄λ‘œ λ²ˆμ—­ μ‹œμž‘")
235
+
236
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
237
+ headers = {
238
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
239
+ "Content-Type": "application/json"
240
+ }
241
+
242
+ payload = {
243
+ "model": "dep89a2fld32mcm",
244
+ "messages": [
245
+ {
246
+ "role": "system",
247
+ "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation."
248
+ },
249
+ {
250
+ "role": "user",
251
+ "content": text
252
+ }
253
+ ],
254
+ "max_tokens": 500,
255
+ "top_p": 0.8,
256
+ "stream": False
257
+ }
258
+
259
+ try:
260
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
261
+ if response.status_code == 200:
262
+ result = response.json()
263
+ translated = result['choices'][0]['message']['content'].strip()
264
+ print(f"[λ²ˆμ—­] μ™„λ£Œ")
265
+ return translated
266
+ else:
267
+ print(f"[λ²ˆμ—­] μ‹€νŒ¨, 원본 μ‚¬μš©")
268
+ return text
269
+ except Exception as e:
270
+ print(f"[λ²ˆμ—­] 였λ₯˜: {str(e)}, 원본 μ‚¬μš©")
271
+ return text
272
+
273
+ def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]:
274
+ """Replicate APIλ₯Ό μ‚¬μš©ν•΄ 이미지 생성"""
275
+ print(f"\n[이미지 생성] {slide_info}")
276
+ print(f"[이미지 생성] ν”„λ‘¬ν”„νŠΈ: {prompt[:50]}...")
277
+
278
+ try:
279
+ english_prompt = translate_to_english(prompt)
280
+
281
+ if not REPLICATE_API_TOKEN:
282
+ error_msg = "RAPI_TOKEN ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
283
+ print(f"[이미지 생성] 였λ₯˜: {error_msg}")
284
+ return None, error_msg
285
+
286
+ print(f"[이미지 생성] Replicate API 호좜 쀑...")
287
+ client = replicate.Client(api_token=REPLICATE_API_TOKEN)
288
+
289
+ input_params = {
290
+ "seed": seed,
291
+ "prompt": english_prompt,
292
+ "speed_mode": "Extra Juiced πŸš€ (even more speed)",
293
+ "output_quality": 80
294
+ }
295
+
296
+ start_time = time.time()
297
+ output = client.run(
298
+ "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645",
299
+ input=input_params
300
+ )
301
+
302
+ elapsed = time.time() - start_time
303
+ print(f"[이미지 생성] API 응닡 λ°›μŒ ({elapsed:.1f}초)")
304
+
305
+ if output:
306
+ if isinstance(output, str) and output.startswith('http'):
307
+ print(f"[이미지 생성] URLμ—μ„œ 이미지 λ‹€μš΄λ‘œλ“œ 쀑...")
308
+ response = requests.get(output, timeout=30)
309
+ img = Image.open(BytesIO(response.content))
310
+ print(f"[이미지 생성] μ™„λ£Œ!")
311
+ return img, english_prompt
312
+ else:
313
+ print(f"[이미지 생성] λ°”μ΄λ„ˆλ¦¬ 데이터 처리 쀑...")
314
+ img = Image.open(BytesIO(output.read()))
315
+ print(f"[이미지 생성] μ™„λ£Œ!")
316
+ return img, english_prompt
317
+ else:
318
+ error_msg = "이미지 생성 μ‹€νŒ¨ - 빈 응닡"
319
+ print(f"[이미지 생성] {error_msg}")
320
+ return None, error_msg
321
+
322
+ except Exception as e:
323
+ error_msg = f"였λ₯˜: {str(e)}"
324
+ print(f"[이미지 생성] {error_msg}")
325
+ print(f"[이미지 생성] 상세 였λ₯˜:\n{traceback.format_exc()}")
326
+ return None, error_msg
327
+
328
+ def generate_ppt_images_sequential(topic: str, template_name: str, custom_slides: List[Dict], seed: int, progress=gr.Progress()):
329
+ """PPT ν…œν”Œλ¦Ώμ— 따라 이미지λ₯Ό 순차적으둜 μƒμ„±ν•˜λ©° μ§„ν–‰ 상황 ν‘œμ‹œ"""
330
+ results = []
331
+ images_for_gallery = []
332
+
333
+ # ν…œν”Œλ¦Ώ 선택
334
+ if template_name == "μ‚¬μš©μž μ •μ˜" and custom_slides:
335
+ slides = custom_slides
336
+ else:
337
+ slides = PPT_TEMPLATES[template_name]["slides"]
338
+
339
+ if not slides:
340
+ yield [], "μŠ¬λΌμ΄λ“œκ°€ μ •μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
341
+ return
342
+
343
+ total_slides = len(slides)
344
+ print(f"\n[PPT 생성] μ‹œμž‘ - 총 {total_slides}개 μŠ¬λΌμ΄λ“œ")
345
+ print(f"[PPT 생성] 주제: {topic}")
346
+ print(f"[PPT 생성] ν…œν”Œλ¦Ώ: {template_name}")
347
+
348
+ # 각 μŠ¬λΌμ΄λ“œ 순차 처리
349
+ for i, slide in enumerate(slides):
350
+ progress((i + 1) / (total_slides + 1), f"μŠ¬λΌμ΄λ“œ {i+1}/{total_slides} 처리 쀑...")
351
+
352
+ slide_info = f"μŠ¬λΌμ΄λ“œ {i+1}: {slide['title']}"
353
+ status_msg = f"\n### πŸ”„ {slide_info} 생성 쀑...\n"
354
+
355
+ # ν˜„μž¬κΉŒμ§€μ˜ μƒνƒœ μ—…λ°μ΄νŠΈ
356
+ yield images_for_gallery, status_msg + format_results_status(results)
357
+
358
+ style_key = slide["style"]
359
+ if style_key in STYLE_TEMPLATES:
360
+ style_info = STYLE_TEMPLATES[style_key]
361
+ slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
362
+
363
+ # ν”„λ‘¬ν”„νŠΈ 생성
364
+ prompt = generate_prompt_with_llm(topic, style_info["example"], slide_context)
365
+
366
+ # 이미지 생성
367
+ slide_seed = seed + i
368
+ img, used_prompt = generate_image(prompt, slide_seed, slide_info)
369
+
370
+ # κ²°κ³Ό μ €μž₯
371
+ result = {
372
+ "slide_title": slide["title"],
373
+ "style": slide["style"],
374
+ "image": img,
375
+ "prompt": prompt,
376
+ "used_prompt": used_prompt,
377
+ "success": img is not None
378
+ }
379
+ results.append(result)
380
+
381
+ # μ„±κ³΅ν•œ μ΄λ―Έμ§€λŠ” κ°€λŸ¬λ¦¬μ— μΆ”κ°€
382
+ if img is not None:
383
+ caption = f"{i+1}. {slide['title']} ({style_info['name']})"
384
+ images_for_gallery.append((img, caption))
385
+
386
+ status_msg = f"\n### βœ… {slide_info} μ™„λ£Œ!\n"
387
+ else:
388
+ status_msg = f"\n### ❌ {slide_info} μ‹€νŒ¨: {used_prompt}\n"
389
+
390
+ # μ¦‰μ‹œ μ—…λ°μ΄νŠΈ
391
+ yield images_for_gallery, status_msg + format_results_status(results)
392
+
393
+ # μ΅œμ’… κ²°κ³Ό
394
+ progress(1.0, "μ™„λ£Œ!")
395
+ successful = sum(1 for r in results if r["success"])
396
+ final_status = f"\n## πŸŽ‰ 생성 μ™„λ£Œ!\n총 {total_slides}개 μŠ¬λΌμ΄λ“œ 쀑 {successful}개 성곡\n\n"
397
+ final_status += format_results_status(results)
398
+
399
+ yield images_for_gallery, final_status
400
+
401
+ def format_results_status(results: List[Dict]) -> str:
402
+ """κ²°κ³Όλ₯Ό μƒνƒœ λ©”μ‹œμ§€λ‘œ 포맷"""
403
+ if not results:
404
+ return ""
405
+
406
+ status_lines = ["### πŸ“Š 생성 κ²°κ³Ό:\n"]
407
+ for i, result in enumerate(results):
408
+ if result["success"]:
409
+ status_lines.append(f"βœ… **μŠ¬λΌμ΄λ“œ {i+1}: {result['slide_title']}**")
410
+ status_lines.append(f" - μŠ€νƒ€μΌ: {result['style'].split('(')[0].strip()}")
411
+ status_lines.append(f" - ν”„λ‘¬ν”„νŠΈ: {result['prompt'][:60]}...\n")
412
+ else:
413
+ status_lines.append(f"❌ **μŠ¬λΌμ΄λ“œ {i+1}: {result['slide_title']}** - μ‹€νŒ¨")
414
+ status_lines.append(f" - 였λ₯˜: {result['used_prompt']}\n")
415
+
416
+ return "\n".join(status_lines)
417
+
418
+ def create_custom_slides_ui():
419
+ """μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ ꡬ성 UI"""
420
+ slides = []
421
+ for i in range(10):
422
+ with gr.Row():
423
+ with gr.Column(scale=2):
424
+ title = gr.Textbox(
425
+ label=f"μŠ¬λΌμ΄λ“œ {i+1} 제λͺ©",
426
+ placeholder="예: ν‘œμ§€, λͺ©μ°¨, ν˜„ν™© 뢄석...",
427
+ visible=(i < 3)
428
+ )
429
+ with gr.Column(scale=3):
430
+ style = gr.Dropdown(
431
+ choices=list(STYLE_TEMPLATES.keys()),
432
+ label=f"μŠ€νƒ€μΌ 선택",
433
+ visible=(i < 3)
434
+ )
435
+ with gr.Column(scale=3):
436
+ hint = gr.Textbox(
437
+ label=f"ν”„λ‘¬ν”„νŠΈ 힌트",
438
+ placeholder="이 μŠ¬λΌμ΄λ“œμ—μ„œ ν‘œν˜„ν•˜κ³  싢은 λ‚΄μš©",
439
+ visible=(i < 3)
440
+ )
441
+ slides.append({"title": title, "style": style, "hint": hint})
442
+ return slides
443
+
444
+ # Gradio μΈν„°νŽ˜μ΄μŠ€ 생성
445
+ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
446
+ gr.Markdown("""
447
+ # 🎯 AI 기반 PPT 이미지 생성기
448
+
449
+ ### 전문적인 ν”„λ ˆμ  ν…Œμ΄μ…˜μ„ μœ„ν•œ λ§žμΆ€ν˜• 이미지λ₯Ό ν•œ λ²ˆμ— μƒμ„±ν•˜μ„Έμš”!
450
+ """)
451
+
452
+ # API 토큰 μƒνƒœ 확인
453
+ if not REPLICATE_API_TOKEN:
454
+ gr.Markdown("⚠️ **κ²½κ³ **: RAPI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
455
+ if not FRIENDLI_TOKEN:
456
+ gr.Markdown("⚠️ **κ²½κ³ **: FRIENDLI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
457
+
458
+ with gr.Row():
459
+ with gr.Column(scale=1):
460
+ # κΈ°λ³Έ μž…λ ₯
461
+ topic_input = gr.Textbox(
462
+ label="ν”„λ ˆμ  ν…Œμ΄μ…˜ 주제",
463
+ placeholder="예: AI μŠ€νƒ€νŠΈμ—… 투자 유치, μ‹ μ œν’ˆ 런칭, λ””μ§€ν„Έ μ „ν™˜ μ „λž΅",
464
+ lines=2
465
+ )
466
+
467
+ # PPT ν…œν”Œλ¦Ώ 선택
468
+ template_select = gr.Dropdown(
469
+ choices=list(PPT_TEMPLATES.keys()),
470
+ label="PPT ν…œν”Œλ¦Ώ 선택",
471
+ value="λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ",
472
+ info="λͺ©μ μ— λ§žλŠ” ν…œν”Œλ¦Ώμ„ μ„ νƒν•˜μ„Έμš”"
473
+ )
474
+
475
+ # ν…œν”Œλ¦Ώ μ„€λͺ…
476
+ template_info = gr.Markdown()
477
+
478
+ # μ‹œλ“œ κ°’
479
+ seed_input = gr.Slider(
480
+ minimum=1,
481
+ maximum=100,
482
+ value=10,
483
+ step=1,
484
+ label="μ‹œλ“œ κ°’"
485
+ )
486
+
487
+ generate_btn = gr.Button("πŸš€ PPT 이미지 μ„ΈνŠΈ 생성", variant="primary", size="lg")
488
+
489
+ # μ‚¬μš©μž μ •μ˜ μ„Ήμ…˜
490
+ with gr.Accordion("πŸ“ μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ ꡬ성", open=False) as custom_accordion:
491
+ gr.Markdown("ν…œν”Œλ¦Ώμ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³  직접 μŠ¬λΌμ΄λ“œλ₯Ό κ΅¬μ„±ν•˜μ„Έμš”.")
492
+ custom_slides_components = create_custom_slides_ui()
493
+
494
+ with gr.Column(scale=2):
495
+ # μ§„ν–‰ 상황 ν‘œμ‹œ
496
+ status_output = gr.Markdown(
497
+ value="### πŸ‘† ν…œν”Œλ¦Ώμ„ μ„ νƒν•˜κ³  생성 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”!\n\n생성 μ§„ν–‰ 상황이 μ‹€μ‹œκ°„μœΌλ‘œ ν‘œμ‹œλ©λ‹ˆλ‹€."
498
+ )
499
+
500
+ # 결과 가러리
501
+ output_gallery = gr.Gallery(
502
+ label="μƒμ„±λœ PPT 이미지",
503
+ show_label=True,
504
+ elem_id="gallery",
505
+ columns=3,
506
+ rows=3,
507
+ object_fit="contain",
508
+ height="auto"
509
+ )
510
+
511
+ # μŠ€νƒ€μΌ κ°€μ΄λ“œ
512
+ with gr.Accordion("πŸ“š μŠ€νƒ€μΌλ³„ ν™œμš© κ°€μ΄λ“œ", open=False):
513
+ style_guide = "### PPT μ œμž‘μ„ μœ„ν•œ μŠ€νƒ€μΌ κ°€μ΄λ“œ\n\n"
514
+ for style_name, style_info in STYLE_TEMPLATES.items():
515
+ style_guide += f"**{style_name}**\n"
516
+ style_guide += f"- μš©λ„: {style_info['use_case']}\n"
517
+ style_guide += f"- νŠΉμ§•: {style_info['description']}\n\n"
518
+ gr.Markdown(style_guide)
519
+
520
+ # ν™œμš© 팁
521
+ gr.Markdown("""
522
+ ---
523
+ ### πŸ’‘ PPT μ œμž‘ 팁:
524
+
525
+ 1. **ν…œν”Œλ¦Ώ ν™œμš©**: λͺ©μ μ— λ§žλŠ” ν…œν”Œλ¦Ώμ„ μ„ νƒν•˜λ©΄ μ΅œμ ν™”λœ μŠ¬λΌμ΄λ“œ ꡬ성을 μ œκ³΅ν•©λ‹ˆλ‹€
526
+ 2. **일관성**: ν•˜λ‚˜μ˜ 주제둜 전체 μŠ¬λΌμ΄λ“œλ₯Ό μƒμ„±ν•˜μ—¬ μ‹œκ°μ  일관성을 μœ μ§€ν•©λ‹ˆλ‹€
527
+ 3. **μ‹€μ‹œκ°„ 확인**: 각 μŠ¬λΌμ΄λ“œκ°€ 생성될 λ•Œλ§ˆλ‹€ μ§„ν–‰ 상황을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€
528
+ 4. **κ³ ν™”μ§ˆ**: λͺ¨λ“  μ΄λ―Έμ§€λŠ” ν”„λ ˆμ  ν…Œμ΄μ…˜μ— μ ν•©ν•œ κ³ ν•΄μƒλ„λ‘œ μƒμ„±λ©λ‹ˆλ‹€
529
+ """)
530
+
531
+ # 이벀트 ν•Έλ“€λŸ¬
532
+ def update_template_info(template_name):
533
+ if template_name in PPT_TEMPLATES:
534
+ template = PPT_TEMPLATES[template_name]
535
+ info = f"**{template['description']}**\n\nν¬ν•¨λœ μŠ¬λΌμ΄λ“œ:\n"
536
+ for i, slide in enumerate(template['slides']):
537
+ info += f"{i+1}. {slide['title']} - {STYLE_TEMPLATES[slide['style']]['use_case']}\n"
538
+ return info
539
+ return ""
540
+
541
+ def generate_ppt_images_handler(topic, template_name, seed, progress=gr.Progress(), *custom_inputs):
542
+ if not topic.strip():
543
+ yield [], "❌ 주제λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."
544
+ return
545
+
546
+ # μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ 처리
547
+ custom_slides = []
548
+ if template_name == "μ‚¬μš©μž μ •μ˜":
549
+ for i in range(0, len(custom_inputs), 3):
550
+ title = custom_inputs[i]
551
+ style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None
552
+ hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else ""
553
+
554
+ if title and style:
555
+ custom_slides.append({
556
+ "title": title,
557
+ "style": style,
558
+ "prompt_hint": hint
559
+ })
560
+
561
+ # 순차적으둜 이미지 μƒμ„±ν•˜λ©° μ§„ν–‰ 상황 ν‘œμ‹œ
562
+ for gallery, status in generate_ppt_images_sequential(topic, template_name, custom_slides, seed, progress):
563
+ yield gallery, status
564
+
565
+ # 이벀트 μ—°κ²°
566
+ template_select.change(
567
+ fn=update_template_info,
568
+ inputs=[template_select],
569
+ outputs=[template_info]
570
+ )
571
+
572
+ # μ‚¬μš©μž μ •μ˜ μž…λ ₯ μˆ˜μ§‘
573
+ all_custom_inputs = []
574
+ for slide_components in custom_slides_components:
575
+ all_custom_inputs.extend([
576
+ slide_components["title"],
577
+ slide_components["style"],
578
+ slide_components["hint"]
579
+ ])
580
+
581
+ generate_btn.click(
582
+ fn=generate_ppt_images_handler,
583
+ inputs=[topic_input, template_select, seed_input] + all_custom_inputs,
584
+ outputs=[output_gallery, status_output]
585
+ )
586
+
587
+ # 초기 ν…œν”Œλ¦Ώ 정보 ν‘œμ‹œ
588
+ demo.load(
589
+ fn=update_template_info,
590
+ inputs=[template_select],
591
+ outputs=[template_info]
592
+ )
593
+
594
+ # μ•± μ‹€ν–‰
595
+ if __name__ == "__main__":
596
+ print("\n" + "="*50)
597
+ print("πŸš€ PPT 이미지 생성기 μ‹œμž‘!")
598
+ print("="*50)
599
+
600
+ # ν™˜κ²½ λ³€μˆ˜ 확인
601
+ if not REPLICATE_API_TOKEN:
602
+ print("⚠️ κ²½κ³ : RAPI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
603
+ else:
604
+ print("βœ… RAPI_TOKEN 확인됨")
605
+
606
+ if not FRIENDLI_TOKEN:
607
+ print("⚠️ κ²½κ³ : FRIENDLI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
608
+ else:
609
+ print("βœ… FRIENDLI_TOKEN 확인됨")
610
+
611
+ print("="*50 + "\n")
612
+
613
+ demo.launch()