ginipick commited on
Commit
e393b2f
·
verified ·
1 Parent(s): f7d50e3

Delete app-backup-last2.py

Browse files
Files changed (1) hide show
  1. app-backup-last2.py +0 -1470
app-backup-last2.py DELETED
@@ -1,1470 +0,0 @@
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
- import PyPDF2
21
- import pandas as pd
22
- import chardet
23
-
24
- # 환경 변수에서 토큰 가져오기
25
- REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
26
- FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
27
- BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
28
-
29
- # 스타일 정의 (표지와 종료 슬라이드 스타일 추가)
30
- STYLE_TEMPLATES = {
31
- "Title Slide (Hero)": {
32
- "name": "Title Slide",
33
- "description": "Impactful hero image for title slide",
34
- "use_case": "표지, 타이틀",
35
- "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"
36
- },
37
- "Thank You Slide": {
38
- "name": "Thank You",
39
- "description": "Elegant closing slide design",
40
- "use_case": "마지막 인사",
41
- "example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for 'Thank You' text. Minimalist, professional, warm feeling"
42
- },
43
- "3D Style (Pixar-like)": {
44
- "name": "3D Style",
45
- "description": "Pixar-esque 3D render with volumetric lighting",
46
- "use_case": "표지, 비전, 미래 컨셉",
47
- "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."
48
- },
49
- "Elegant SWOT Quadrant": {
50
- "name": "SWOT Analysis",
51
- "description": "Flat-design 4-grid layout with minimal shadows",
52
- "use_case": "현황 분석, 전략 평가",
53
- "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"
54
- },
55
- "Colorful Mind Map": {
56
- "name": "Mind Map",
57
- "description": "Hand-drawn educational style with vibrant colors",
58
- "use_case": "브레인스토밍, 아이디어 정리",
59
- "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"
60
- },
61
- "Business Workflow": {
62
- "name": "Business Process",
63
- "description": "End-to-end business workflow with clear phases",
64
- "use_case": "프로세스 설명, 단계별 진행",
65
- "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"
66
- },
67
- "Industrial Design": {
68
- "name": "Product Design",
69
- "description": "Sleek industrial design concept sketch",
70
- "use_case": "제품 소개, 컨셉 디자인",
71
- "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"
72
- },
73
- "3D Bubble Chart": {
74
- "name": "Bubble Chart",
75
- "description": "Clean 3D bubble visualization",
76
- "use_case": "비교 분석, 포지셔닝",
77
- "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"
78
- },
79
- "Timeline Ribbon": {
80
- "name": "Timeline",
81
- "description": "Horizontal ribbon timeline with cyber-futuristic vibe",
82
- "use_case": "일정, 로드맵, 마일스톤",
83
- "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"
84
- },
85
- "Risk Heat Map": {
86
- "name": "Heat Map",
87
- "description": "Risk assessment heat map with gradient colors",
88
- "use_case": "리스크 분석, 우선순위",
89
- "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"
90
- },
91
- "Pyramid/Funnel": {
92
- "name": "Funnel Chart",
93
- "description": "Multi-layer gradient funnel visualization",
94
- "use_case": "단계별 축소, 핵심 도출",
95
- "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"
96
- },
97
- "KPI Dashboard": {
98
- "name": "Dashboard",
99
- "description": "Dark-mode analytics dashboard with sci-fi interface",
100
- "use_case": "성과 지표, 실적 대시보드",
101
- "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"
102
- },
103
- "Value Chain": {
104
- "name": "Value Chain",
105
- "description": "Horizontal value chain with industrial look",
106
- "use_case": "가치 사슬, 비즈니스 모델",
107
- "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"
108
- },
109
- "Gantt Chart": {
110
- "name": "Gantt Chart",
111
- "description": "Hand-drawn style Gantt chart with playful colors",
112
- "use_case": "프로젝트 일정, 작업 관리",
113
- "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"
114
- },
115
- "Mobile App Mockup": {
116
- "name": "App Mockup",
117
- "description": "Clean wireframe for mobile app design",
118
- "use_case": "앱/웹 UI, 화면 설계",
119
- "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"
120
- },
121
- "Flowchart": {
122
- "name": "Flowchart",
123
- "description": "Vibrant flowchart with minimalistic icons",
124
- "use_case": "의사결정, 프로세스 흐름",
125
- "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"
126
- }
127
- }
128
-
129
- # PPT 템플릿 정의 (동적으로 슬라이드 수 조정 가능)
130
- PPT_TEMPLATES = {
131
- "비즈니스 제안서": {
132
- "description": "투자 유치, 사업 제안용",
133
- "core_slides": [
134
- {"title": "목차", "style": "Flowchart", "prompt_hint": "프레젠테이션 구조"},
135
- {"title": "문제 정의", "style": "Colorful Mind Map", "prompt_hint": "현재 시장의 문제점"},
136
- {"title": "현황 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "강점, 약점, 기회, 위협"},
137
- {"title": "솔루션", "style": "Industrial Design", "prompt_hint": "제품/서비스 컨셉"},
138
- {"title": "프로세스", "style": "Business Workflow", "prompt_hint": "실행 단계"},
139
- {"title": "일정", "style": "Timeline Ribbon", "prompt_hint": "주요 마일스톤"}
140
- ],
141
- "optional_slides": [
142
- {"title": "시장 규모", "style": "3D Bubble Chart", "prompt_hint": "시장 기회와 성장성"},
143
- {"title": "경쟁 분석", "style": "Risk Heat Map", "prompt_hint": "경쟁사 포지셔닝"},
144
- {"title": "비즈니스 모델", "style": "Value Chain", "prompt_hint": "수익 구조"},
145
- {"title": "팀 소개", "style": "Colorful Mind Map", "prompt_hint": "핵심 팀원과 역량"},
146
- {"title": "재무 계획", "style": "KPI Dashboard", "prompt_hint": "예상 매출과 손익"},
147
- {"title": "위험 관리", "style": "Risk Heat Map", "prompt_hint": "주요 리스크와 대응"},
148
- {"title": "파트너십", "style": "Business Workflow", "prompt_hint": "전략적 제휴"},
149
- {"title": "기술 스택", "style": "Flowchart", "prompt_hint": "핵심 기술 구조"},
150
- {"title": "고객 사례", "style": "Industrial Design", "prompt_hint": "성공 사례"},
151
- {"title": "성장 전략", "style": "Timeline Ribbon", "prompt_hint": "확장 계획"},
152
- {"title": "투자 활용", "style": "Pyramid/Funnel", "prompt_hint": "자금 사용 계획"},
153
- {"title": "Exit 전략", "style": "Timeline Ribbon", "prompt_hint": "출구 전략"}
154
- ]
155
- },
156
- "제품 소개": {
157
- "description": "신제품 런칭, 서비스 소개용",
158
- "core_slides": [
159
- {"title": "제품 컨셉", "style": "Industrial Design", "prompt_hint": "제품 디자인"},
160
- {"title": "사용자 니즈", "style": "Colorful Mind Map", "prompt_hint": "고객 페인포인트"},
161
- {"title": "기능 소개", "style": "Mobile App Mockup", "prompt_hint": "UI/UX 화면"},
162
- {"title": "작동 원리", "style": "Flowchart", "prompt_hint": "기능 플로우"},
163
- {"title": "시장 포지션", "style": "3D Bubble Chart", "prompt_hint": "경쟁사 비교"},
164
- {"title": "출시 일정", "style": "Timeline Ribbon", "prompt_hint": "런칭 로드맵"}
165
- ],
166
- "optional_slides": [
167
- {"title": "타겟 고객", "style": "Colorful Mind Map", "prompt_hint": "주요 고객층"},
168
- {"title": "가격 정책", "style": "Pyramid/Funnel", "prompt_hint": "가격 전략"},
169
- {"title": "기술 우위", "style": "Industrial Design", "prompt_hint": "핵심 기술"},
170
- {"title": "사용 시나리오", "style": "Business Workflow", "prompt_hint": "활용 사례"},
171
- {"title": "고객 후기", "style": "KPI Dashboard", "prompt_hint": "사용자 평가"},
172
- {"title": "판매 채널", "style": "Value Chain", "prompt_hint": "유통 전략"},
173
- {"title": "마케팅 전략", "style": "Timeline Ribbon", "prompt_hint": "홍보 계획"},
174
- {"title": "성능 비교", "style": "3D Bubble Chart", "prompt_hint": "벤치마크"}
175
- ]
176
- },
177
- "프로젝트 보고": {
178
- "description": "진행 상황, 성과 보고용",
179
- "core_slides": [
180
- {"title": "프로젝트 개요", "style": "Business Workflow", "prompt_hint": "전체 프로세스"},
181
- {"title": "진행 현황", "style": "Gantt Chart", "prompt_hint": "작업 일정"},
182
- {"title": "리스크 관리", "style": "Risk Heat Map", "prompt_hint": "위험 요소"},
183
- {"title": "성과 지표", "style": "KPI Dashboard", "prompt_hint": "달성 실적"},
184
- {"title": "향후 계획", "style": "Timeline Ribbon", "prompt_hint": "다음 단계"}
185
- ],
186
- "optional_slides": [
187
- {"title": "예산 현황", "style": "Pyramid/Funnel", "prompt_hint": "예산 집행"},
188
- {"title": "팀 성과", "style": "3D Bubble Chart", "prompt_hint": "팀별 기여도"},
189
- {"title": "이슈 관리", "style": "Risk Heat Map", "prompt_hint": "주요 이슈"},
190
- {"title": "개선 사항", "style": "Colorful Mind Map", "prompt_hint": "프로세스 개선"},
191
- {"title": "교훈", "style": "Business Workflow", "prompt_hint": "배운 점"}
192
- ]
193
- },
194
- "전략 기획": {
195
- "description": "중장기 전략, 비전 수립용",
196
- "core_slides": [
197
- {"title": "비전", "style": "3D Style (Pixar-like)", "prompt_hint": "미래 비전"},
198
- {"title": "환경 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "내외부 환경"},
199
- {"title": "전략 체계", "style": "Colorful Mind Map", "prompt_hint": "전략 구조"},
200
- {"title": "가치 사슬", "style": "Value Chain", "prompt_hint": "비즈니스 모델"},
201
- {"title": "실행 로드맵", "style": "Timeline Ribbon", "prompt_hint": "단계별 계획"},
202
- {"title": "목표 지표", "style": "KPI Dashboard", "prompt_hint": "KPI 목표"}
203
- ],
204
- "optional_slides": [
205
- {"title": "시장 전망", "style": "3D Bubble Chart", "prompt_hint": "미래 시장"},
206
- {"title": "혁신 방향", "style": "Industrial Design", "prompt_hint": "혁신 전략"},
207
- {"title": "조직 변화", "style": "Business Workflow", "prompt_hint": "조직 개편"},
208
- {"title": "디지털 전환", "style": "Flowchart", "prompt_hint": "DX 전략"},
209
- {"title": "지속가능성", "style": "Timeline Ribbon", "prompt_hint": "ESG 전략"}
210
- ]
211
- },
212
- "사용자 정의": {
213
- "description": "직접 구성하기",
214
- "core_slides": [],
215
- "optional_slides": []
216
- }
217
- }
218
-
219
- def brave_search(query: str) -> List[Dict]:
220
- """Brave Search API를 사용한 웹 검색"""
221
- if not BRAVE_API_TOKEN:
222
- print("[Brave Search] API 토큰이 없어 검색을 건너뜁니다.")
223
- return []
224
-
225
- print(f"[Brave Search] 검색어: {query}")
226
-
227
- headers = {
228
- "Accept": "application/json",
229
- "X-Subscription-Token": BRAVE_API_TOKEN
230
- }
231
-
232
- params = {
233
- "q": query,
234
- "count": 5
235
- }
236
-
237
- try:
238
- response = requests.get(
239
- "https://api.search.brave.com/res/v1/web/search",
240
- headers=headers,
241
- params=params,
242
- timeout=10
243
- )
244
-
245
- if response.status_code == 200:
246
- data = response.json()
247
- results = []
248
- for item in data.get("web", {}).get("results", [])[:3]:
249
- results.append({
250
- "title": item.get("title", ""),
251
- "description": item.get("description", ""),
252
- "url": item.get("url", "")
253
- })
254
- print(f"[Brave Search] {len(results)}개 결과 획득")
255
- return results
256
- else:
257
- print(f"[Brave Search] 오류: {response.status_code}")
258
- return []
259
- except Exception as e:
260
- print(f"[Brave Search] 예외: {str(e)}")
261
- return []
262
-
263
- def read_uploaded_file(file_path: str) -> str:
264
- """업로드된 파일 읽기 (PDF, CSV, TXT)"""
265
- print(f"[파일 읽기] {file_path}")
266
-
267
- try:
268
- # 파일 확장자 확인
269
- ext = os.path.splitext(file_path)[1].lower()
270
-
271
- if ext == '.pdf':
272
- # PDF 읽기
273
- with open(file_path, 'rb') as file:
274
- pdf_reader = PyPDF2.PdfReader(file)
275
- text = ""
276
- for page in pdf_reader.pages:
277
- text += page.extract_text() + "\n"
278
- return text[:5000] # 최대 5000자
279
-
280
- elif ext == '.csv':
281
- # CSV 읽기
282
- # 인코딩 감지
283
- with open(file_path, 'rb') as file:
284
- raw_data = file.read()
285
- result = chardet.detect(raw_data)
286
- encoding = result['encoding'] or 'utf-8'
287
-
288
- df = pd.read_csv(file_path, encoding=encoding)
289
- return f"CSV 데이터:\n{df.head(20).to_string()}\n\n요약: {len(df)} 행, {len(df.columns)} 열"
290
-
291
- elif ext in ['.txt', '.text']:
292
- # 텍스트 파일 읽기
293
- with open(file_path, 'rb') as file:
294
- raw_data = file.read()
295
- result = chardet.detect(raw_data)
296
- encoding = result['encoding'] or 'utf-8'
297
-
298
- with open(file_path, 'r', encoding=encoding) as file:
299
- return file.read()[:5000] # 최대 5000자
300
- else:
301
- return "지원하지 않는 파일 형식입니다."
302
-
303
- except Exception as e:
304
- return f"파일 읽기 오류: {str(e)}"
305
-
306
- def generate_presentation_notes(topic: str, slide_title: str, content: Dict) -> str:
307
- """각 슬라이드의 발표자 노트 생성 (구어체)"""
308
- print(f"[발표자 노트] {slide_title} 생성 중...")
309
-
310
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
311
- headers = {
312
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
313
- "Content-Type": "application/json"
314
- }
315
-
316
- system_prompt = """You are a professional presentation coach who creates natural, conversational speaker notes.
317
-
318
- Create speaker notes that:
319
- 1. Sound natural and conversational, as if speaking to an audience
320
- 2. Include transitions and engagement phrases
321
- 3. Reference the slide content but expand with additional context
322
- 4. Use a warm, professional tone
323
- 5. Be 100-150 words long
324
- 6. Include pauses and emphasis markers where appropriate
325
-
326
- Format:
327
- - Use conversational language
328
- - Include transition phrases
329
- - Add engagement questions or comments
330
- - Keep it professional but friendly"""
331
-
332
- bullet_text = "\n".join(content.get("bullet_points", []))
333
- user_message = f"""Topic: {topic}
334
- Slide Title: {slide_title}
335
- Subtitle: {content.get('subtitle', '')}
336
- Key Points:
337
- {bullet_text}
338
-
339
- Create natural speaker notes for presenting this slide."""
340
-
341
- payload = {
342
- "model": "dep89a2fld32mcm",
343
- "messages": [
344
- {
345
- "role": "system",
346
- "content": system_prompt
347
- },
348
- {
349
- "role": "user",
350
- "content": user_message
351
- }
352
- ],
353
- "max_tokens": 300,
354
- "temperature": 0.8,
355
- "stream": False
356
- }
357
-
358
- try:
359
- response = requests.post(url, json=payload, headers=headers, timeout=30)
360
- if response.status_code == 200:
361
- result = response.json()
362
- notes = result['choices'][0]['message']['content'].strip()
363
-
364
- # 한글 주제인 경우 번역
365
- if any(ord('가') <= ord(char) <= ord('힣') for char in topic):
366
- notes = translate_content_to_korean(notes)
367
-
368
- return notes
369
- else:
370
- return "이 슬라이드에서는 핵심 내용을 설명해 주세요."
371
- except Exception as e:
372
- print(f"[발표자 노트] 오류: {str(e)}")
373
- return "이 슬라이드에서는 핵심 내용을 설명해 주세요."
374
-
375
- def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
376
- """각 슬라이드의 텍스트 내용 생성"""
377
- print(f"[슬라이드 내용] {slide_title} 텍스트 생성 중...")
378
-
379
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
380
- headers = {
381
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
382
- "Content-Type": "application/json"
383
- }
384
-
385
- system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content.
386
-
387
- Your task is to create:
388
- 1. A compelling subtitle (max 10 words)
389
- 2. Exactly 5 bullet points, each being a complete, concise sentence
390
- 3. Each bullet point should be 10-15 words
391
-
392
- Guidelines:
393
- - Be specific and actionable
394
- - Use professional business language
395
- - Include relevant data points or metrics when appropriate
396
- - Ensure content aligns with the slide's purpose
397
- - Make each point distinct and valuable
398
- - Use active voice and strong verbs
399
-
400
- Output format:
401
- Subtitle: [subtitle here]
402
- • [Point 1]
403
- • [Point 2]
404
- • [Point 3]
405
- • [Point 4]
406
- • [Point 5]"""
407
-
408
- user_message = f"""Topic: {topic}
409
- Slide Title: {slide_title}
410
- Context: {slide_context}"""
411
-
412
- # 업로드된 콘텐츠가 있으면 추가
413
- if uploaded_content:
414
- user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
415
-
416
- # 웹 검색 결과가 있으면 추가
417
- if web_search_results:
418
- search_context = "\n\nWeb Search Results:\n"
419
- for result in web_search_results[:3]:
420
- search_context += f"- {result['title']}: {result['description']}\n"
421
- user_message += search_context
422
-
423
- user_message += "\n\nCreate compelling content for this presentation slide."
424
-
425
- payload = {
426
- "model": "dep89a2fld32mcm",
427
- "messages": [
428
- {
429
- "role": "system",
430
- "content": system_prompt
431
- },
432
- {
433
- "role": "user",
434
- "content": user_message
435
- }
436
- ],
437
- "max_tokens": 300,
438
- "top_p": 0.8,
439
- "temperature": 0.7,
440
- "stream": False
441
- }
442
-
443
- try:
444
- response = requests.post(url, json=payload, headers=headers, timeout=30)
445
- if response.status_code == 200:
446
- result = response.json()
447
- content = result['choices'][0]['message']['content'].strip()
448
-
449
- # Parse content
450
- lines = content.split('\n')
451
- subtitle = ""
452
- bullet_points = []
453
-
454
- for line in lines:
455
- if line.startswith("Subtitle:"):
456
- subtitle = line.replace("Subtitle:", "").strip()
457
- elif line.strip().startswith("•"):
458
- bullet_points.append(line.strip())
459
-
460
- # 한글로 번역이 필요한 경우
461
- if any(ord('가') <= ord(char) <= ord('힣') for char in topic):
462
- subtitle = translate_content_to_korean(subtitle)
463
- bullet_points = [translate_content_to_korean(point) for point in bullet_points]
464
-
465
- return {
466
- "subtitle": subtitle,
467
- "bullet_points": bullet_points[:5] # 최대 5개
468
- }
469
- else:
470
- return {
471
- "subtitle": slide_title,
472
- "bullet_points": ["내용을 생성할 수 없습니다."] * 5
473
- }
474
- except Exception as e:
475
- print(f"[슬라이드 내용] 오류: {str(e)}")
476
- return {
477
- "subtitle": slide_title,
478
- "bullet_points": ["내용을 생성할 수 없습니다."] * 5
479
- }
480
-
481
- def translate_content_to_korean(text: str) -> str:
482
- """영어 텍스트를 한글로 번역"""
483
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
484
- headers = {
485
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
486
- "Content-Type": "application/json"
487
- }
488
-
489
- payload = {
490
- "model": "dep89a2fld32mcm",
491
- "messages": [
492
- {
493
- "role": "system",
494
- "content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation."
495
- },
496
- {
497
- "role": "user",
498
- "content": text
499
- }
500
- ],
501
- "max_tokens": 200,
502
- "top_p": 0.8,
503
- "stream": False
504
- }
505
-
506
- try:
507
- response = requests.post(url, json=payload, headers=headers, timeout=30)
508
- if response.status_code == 200:
509
- result = response.json()
510
- return result['choices'][0]['message']['content'].strip()
511
- else:
512
- return text
513
- except Exception as e:
514
- return text
515
-
516
- def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str:
517
- """주제와 스타일 예제를 받아서 LLM을 사용해 이미지 프롬프트를 생성"""
518
- print(f"[LLM] 프롬프트 생성 시작: {slide_context}")
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 = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides.
527
-
528
- Your task is to create prompts that:
529
- 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals
530
- 2. Consider the slide's purpose and maintain consistency across a presentation
531
- 3. Include style references matching the given example
532
- 4. Focus on clean, professional visuals that won't distract from text overlays
533
- 5. Ensure high contrast areas for text readability when needed
534
- 6. Maintain brand consistency and professional aesthetics
535
-
536
- Important guidelines:
537
- - If given a style example, adapt the topic to match that specific visual style
538
- - Consider the slide context (e.g., "표지", "현황 분석") to create appropriate visuals
539
- - Always output ONLY the prompt without any explanation
540
- - Keep prompts between 50-150 words for optimal results
541
- - Ensure the visual supports rather than overwhelms the slide content"""
542
-
543
- user_message = f"Topic: {topic}"
544
- if style_example:
545
- user_message += f"\n\nStyle reference to follow:\n{style_example}"
546
- if slide_context:
547
- user_message += f"\n\nSlide context: {slide_context}"
548
- if uploaded_content:
549
- user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}"
550
-
551
- payload = {
552
- "model": "dep89a2fld32mcm",
553
- "messages": [
554
- {
555
- "role": "system",
556
- "content": system_prompt
557
- },
558
- {
559
- "role": "user",
560
- "content": user_message
561
- }
562
- ],
563
- "max_tokens": 300,
564
- "top_p": 0.8,
565
- "temperature": 0.7,
566
- "stream": False
567
- }
568
-
569
- try:
570
- response = requests.post(url, json=payload, headers=headers, timeout=30)
571
- if response.status_code == 200:
572
- result = response.json()
573
- prompt = result['choices'][0]['message']['content'].strip()
574
- print(f"[LLM] 프롬프트 생성 완료: {prompt[:50]}...")
575
- return prompt
576
- else:
577
- error_msg = f"프롬프트 생성 실패: {response.status_code}"
578
- print(f"[LLM] {error_msg}")
579
- return error_msg
580
- except Exception as e:
581
- error_msg = f"프롬프트 생성 중 오류 발생: {str(e)}"
582
- print(f"[LLM] {error_msg}")
583
- return error_msg
584
-
585
- def translate_to_english(text: str) -> str:
586
- """한글 텍스트를 영어로 번역 (LLM 사용)"""
587
- if not any(ord('가') <= ord(char) <= ord('힣') for char in text):
588
- return text
589
-
590
- print(f"[번역] 한글 감지, 영어로 번역 시작")
591
-
592
- url = "https://api.friendli.ai/dedicated/v1/chat/completions"
593
- headers = {
594
- "Authorization": f"Bearer {FRIENDLI_TOKEN}",
595
- "Content-Type": "application/json"
596
- }
597
-
598
- payload = {
599
- "model": "dep89a2fld32mcm",
600
- "messages": [
601
- {
602
- "role": "system",
603
- "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation."
604
- },
605
- {
606
- "role": "user",
607
- "content": text
608
- }
609
- ],
610
- "max_tokens": 500,
611
- "top_p": 0.8,
612
- "stream": False
613
- }
614
-
615
- try:
616
- response = requests.post(url, json=payload, headers=headers, timeout=30)
617
- if response.status_code == 200:
618
- result = response.json()
619
- translated = result['choices'][0]['message']['content'].strip()
620
- print(f"[번역] 완료")
621
- return translated
622
- else:
623
- print(f"[번역] 실패, 원본 사용")
624
- return text
625
- except Exception as e:
626
- print(f"[번역] 오류: {str(e)}, 원본 사용")
627
- return text
628
-
629
- def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]:
630
- """Replicate API를 사용해 이미지 생성"""
631
- print(f"\n[이미지 생성] {slide_info}")
632
- print(f"[이미지 생성] 프롬프트: {prompt[:50]}...")
633
-
634
- try:
635
- english_prompt = translate_to_english(prompt)
636
-
637
- if not REPLICATE_API_TOKEN:
638
- error_msg = "RAPI_TOKEN 환경변수가 설정되지 않았습니다."
639
- print(f"[이미지 생성] 오류: {error_msg}")
640
- return None, error_msg
641
-
642
- print(f"[이미지 생성] Replicate API 호출 중...")
643
- client = replicate.Client(api_token=REPLICATE_API_TOKEN)
644
-
645
- input_params = {
646
- "seed": seed,
647
- "prompt": english_prompt,
648
- "speed_mode": "Extra Juiced 🚀 (even more speed)",
649
- "output_quality": 100
650
- }
651
-
652
- start_time = time.time()
653
- output = client.run(
654
- "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645",
655
- input=input_params
656
- )
657
-
658
- elapsed = time.time() - start_time
659
- print(f"[이미지 생성] API 응답 받음 ({elapsed:.1f}초)")
660
-
661
- if output:
662
- if isinstance(output, str) and output.startswith('http'):
663
- print(f"[이미지 생성] URL에서 이미지 다운로드 중...")
664
- response = requests.get(output, timeout=30)
665
- img = Image.open(BytesIO(response.content))
666
- print(f"[이미지 생성] 완료!")
667
- return img, english_prompt
668
- else:
669
- print(f"[이미지 생성] 바이너리 데이터 처리 중...")
670
- img = Image.open(BytesIO(output.read()))
671
- print(f"[이미지 생성] 완료!")
672
- return img, english_prompt
673
- else:
674
- error_msg = "이미지 생성 실패 - 빈 응답"
675
- print(f"[이미지 생성] {error_msg}")
676
- return None, error_msg
677
-
678
- except Exception as e:
679
- error_msg = f"오류: {str(e)}"
680
- print(f"[이미지 생성] {error_msg}")
681
- print(f"[이미지 생성] 상세 오류:\n{traceback.format_exc()}")
682
- return None, error_msg
683
-
684
- def create_slide_preview_html(slide_data: Dict) -> str:
685
- """16:9 비율의 슬라이드 프리뷰 HTML 생성"""
686
-
687
- # 이미지를 base64로 인코딩
688
- img_base64 = ""
689
- if slide_data.get("image"):
690
- buffered = BytesIO()
691
- slide_data["image"].save(buffered, format="PNG")
692
- img_base64 = base64.b64encode(buffered.getvalue()).decode()
693
-
694
- # 텍스트 내용 가져오기
695
- subtitle = slide_data.get("subtitle", "")
696
- bullet_points = slide_data.get("bullet_points", [])
697
-
698
- # HTML 생성
699
- html = f"""
700
- <div class="slide-container" style="
701
- width: 100%;
702
- max-width: 1200px;
703
- margin: 20px auto;
704
- background: white;
705
- border-radius: 8px;
706
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
707
- overflow: hidden;
708
- ">
709
- <div class="slide-header" style="
710
- background: #2c3e50;
711
- color: white;
712
- padding: 15px 30px;
713
- font-size: 18px;
714
- font-weight: bold;
715
- ">
716
- 슬라이드 {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}
717
- </div>
718
-
719
- <div class="slide-content" style="
720
- display: flex;
721
- height: 0;
722
- padding-bottom: 56.25%; /* 16:9 비율 */
723
- position: relative;
724
- ">
725
- <div class="slide-inner" style="
726
- position: absolute;
727
- top: 0;
728
- left: 0;
729
- width: 100%;
730
- height: 100%;
731
- display: flex;
732
- ">
733
- """
734
-
735
- # 표지와 마지막 슬라이드는 전체 화면 이미지
736
- if slide_data.get('title') in ['표지', 'Thank You']:
737
- html += f"""
738
- <!-- 전체 화면 이미지 -->
739
- <div style="
740
- width: 100%;
741
- height: 100%;
742
- position: relative;
743
- background: #e9ecef;
744
- ">
745
- """
746
-
747
- if img_base64:
748
- html += f"""
749
- <img src="data:image/png;base64,{img_base64}" style="
750
- width: 100%;
751
- height: 100%;
752
- object-fit: cover;
753
- " alt="Slide Image">
754
- <div style="
755
- position: absolute;
756
- top: 50%;
757
- left: 50%;
758
- transform: translate(-50%, -50%);
759
- text-align: center;
760
- color: white;
761
- text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
762
- ">
763
- <h1 style="font-size: 48px; margin-bottom: 20px;">{slide_data.get('topic', '')}</h1>
764
- <h2 style="font-size: 24px;">{subtitle}</h2>
765
- </div>
766
- """
767
-
768
- html += """
769
- </div>
770
- """
771
- else:
772
- # 일반 슬라이드 레이아웃
773
- html += f"""
774
- <!-- 텍스트 영역 (좌측) -->
775
- <div class="text-area" style="
776
- flex: 1;
777
- padding: 40px;
778
- display: flex;
779
- flex-direction: column;
780
- justify-content: center;
781
- background: #f8f9fa;
782
- ">
783
- <h2 style="
784
- color: #2c3e50;
785
- font-size: 28px;
786
- margin-bottom: 30px;
787
- font-weight: 600;
788
- ">{subtitle}</h2>
789
-
790
- <ul style="
791
- list-style: none;
792
- padding: 0;
793
- margin: 0;
794
- ">
795
- """
796
-
797
- for point in bullet_points:
798
- html += f"""
799
- <li style="
800
- margin-bottom: 15px;
801
- padding-left: 25px;
802
- position: relative;
803
- color: #34495e;
804
- font-size: 16px;
805
- line-height: 1.6;
806
- ">
807
- <span style="
808
- position: absolute;
809
- left: 0;
810
- color: #3498db;
811
- ">▶</span>
812
- {point.replace('•', '').strip()}
813
- </li>
814
- """
815
-
816
- html += f"""
817
- </ul>
818
- </div>
819
-
820
- <!-- 이미지 영역 (우측) -->
821
- <div class="image-area" style="
822
- flex: 1;
823
- background: #e9ecef;
824
- display: flex;
825
- align-items: center;
826
- justify-content: center;
827
- padding: 20px;
828
- ">
829
- """
830
-
831
- if img_base64:
832
- html += f"""
833
- <img src="data:image/png;base64,{img_base64}" style="
834
- max-width: 100%;
835
- max-height: 100%;
836
- object-fit: contain;
837
- border-radius: 4px;
838
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
839
- " alt="Slide Image">
840
- """
841
- else:
842
- html += """
843
- <div style="
844
- color: #6c757d;
845
- text-align: center;
846
- ">
847
- <div style="font-size: 48px;">🖼️</div>
848
- <p>이미지 생성 중...</p>
849
- </div>
850
- """
851
-
852
- html += """
853
- </div>
854
- """
855
-
856
- html += """
857
- </div>
858
- </div>
859
- </div>
860
- """
861
-
862
- return html
863
-
864
- def create_pptx_file(results: List[Dict], topic: str, template_name: str) -> str:
865
- """생성된 결과를 PPTX 파일로 변환 (발표자 노트 포함)"""
866
- print("[PPTX] 파일 생성 시작...")
867
-
868
- # 프레젠테이션 생성 (16:9 비율)
869
- prs = Presentation()
870
- prs.slide_width = Inches(16)
871
- prs.slide_height = Inches(9)
872
-
873
- # 각 결과 슬라이드 추가
874
- for i, result in enumerate(results):
875
- if not result.get("success", False):
876
- continue
877
-
878
- slide_data = result.get("slide_data", {})
879
-
880
- # 빈 레이아웃 사용 (제목 플레이스홀더 없는 완전한 빈 슬라이드)
881
- blank_layout = prs.slide_layouts[6] # 완전히 빈 레이아웃
882
- slide = prs.slides.add_slide(blank_layout)
883
-
884
- # 표지 슬라이드
885
- if slide_data.get('title') == '표지':
886
- # 배경 이미지 추가
887
- if slide_data.get('image'):
888
- try:
889
- img_buffer = BytesIO()
890
- slide_data['image'].save(img_buffer, format='PNG')
891
- img_buffer.seek(0)
892
-
893
- # 전체 화면 배경 이미지
894
- pic = slide.shapes.add_picture(
895
- img_buffer,
896
- 0, 0,
897
- width=prs.slide_width,
898
- height=prs.slide_height
899
- )
900
- # 맨 뒤로 보내기
901
- slide.shapes._spTree.remove(pic._element)
902
- slide.shapes._spTree.insert(2, pic._element)
903
- except Exception as e:
904
- print(f"[PPTX] 표지 이미지 추가 실패: {str(e)}")
905
-
906
- # 제목 텍스트 추가
907
- title_box = slide.shapes.add_textbox(
908
- Inches(2), Inches(3),
909
- Inches(12), Inches(3)
910
- )
911
- title_frame = title_box.text_frame
912
- title_frame.text = topic
913
- title_para = title_frame.paragraphs[0]
914
- title_para.font.size = Pt(48)
915
- title_para.font.bold = True
916
- title_para.font.color.rgb = RGBColor(255, 255, 255)
917
- title_para.alignment = PP_ALIGN.CENTER
918
-
919
- # 부제목 추가
920
- subtitle_box = slide.shapes.add_textbox(
921
- Inches(2), Inches(6),
922
- Inches(12), Inches(2)
923
- )
924
- subtitle_frame = subtitle_box.text_frame
925
- subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI 프레젠테이션')
926
- subtitle_para = subtitle_frame.paragraphs[0]
927
- subtitle_para.font.size = Pt(24)
928
- subtitle_para.font.color.rgb = RGBColor(255, 255, 255)
929
- subtitle_para.alignment = PP_ALIGN.CENTER
930
-
931
- # Thank You 슬라이드
932
- elif slide_data.get('title') == 'Thank You':
933
- # 배경 이미지 추가
934
- if slide_data.get('image'):
935
- try:
936
- img_buffer = BytesIO()
937
- slide_data['image'].save(img_buffer, format='PNG')
938
- img_buffer.seek(0)
939
-
940
- # 전체 화면 배경 이미지
941
- pic = slide.shapes.add_picture(
942
- img_buffer,
943
- 0, 0,
944
- width=prs.slide_width,
945
- height=prs.slide_height
946
- )
947
- # 맨 뒤로 보내기
948
- slide.shapes._spTree.remove(pic._element)
949
- slide.shapes._spTree.insert(2, pic._element)
950
- except Exception as e:
951
- print(f"[PPTX] Thank You 이미지 추가 실패: {str(e)}")
952
-
953
- # Thank You 텍스트
954
- thanks_box = slide.shapes.add_textbox(
955
- Inches(2), Inches(3.5),
956
- Inches(12), Inches(2)
957
- )
958
- thanks_frame = thanks_box.text_frame
959
- thanks_frame.text = "Thank You"
960
- thanks_para = thanks_frame.paragraphs[0]
961
- thanks_para.font.size = Pt(60)
962
- thanks_para.font.bold = True
963
- thanks_para.font.color.rgb = RGBColor(255, 255, 255)
964
- thanks_para.alignment = PP_ALIGN.CENTER
965
-
966
- # 일반 슬라이드
967
- else:
968
- # 슬라이드 제목 추가 (상단)
969
- title_box = slide.shapes.add_textbox(
970
- Inches(0.5), Inches(0.3),
971
- Inches(15), Inches(0.8)
972
- )
973
- title_frame = title_box.text_frame
974
- title_frame.text = f"{slide_data.get('title', '')}"
975
- title_para = title_frame.paragraphs[0]
976
- title_para.font.size = Pt(28)
977
- title_para.font.bold = True
978
- title_para.font.color.rgb = RGBColor(44, 62, 80)
979
-
980
- # 좌측 텍스트 영역
981
- text_box = slide.shapes.add_textbox(
982
- Inches(0.5), Inches(1.5),
983
- Inches(7.5), Inches(6.5)
984
- )
985
- text_frame = text_box.text_frame
986
- text_frame.word_wrap = True
987
-
988
- # 소제목 추가
989
- subtitle_para = text_frame.paragraphs[0]
990
- subtitle_para.text = slide_data.get('subtitle', '')
991
- subtitle_para.font.size = Pt(20)
992
- subtitle_para.font.bold = True
993
- subtitle_para.font.color.rgb = RGBColor(52, 73, 94)
994
- subtitle_para.space_after = Pt(20)
995
-
996
- # 불릿 포인트 추가
997
- bullet_points = slide_data.get('bullet_points', [])
998
- for point in bullet_points:
999
- p = text_frame.add_paragraph()
1000
- p.text = point.replace('•', '').strip()
1001
- p.font.size = Pt(16)
1002
- p.font.color.rgb = RGBColor(52, 73, 94)
1003
- p.level = 0
1004
- p.space_after = Pt(12)
1005
- p.line_spacing = 1.5
1006
-
1007
- # 우측 이미지 추가
1008
- if slide_data.get('image'):
1009
- try:
1010
- img_buffer = BytesIO()
1011
- slide_data['image'].save(img_buffer, format='PNG')
1012
- img_buffer.seek(0)
1013
-
1014
- pic = slide.shapes.add_picture(
1015
- img_buffer,
1016
- Inches(8.5), Inches(1.5),
1017
- width=Inches(7), height=Inches(6)
1018
- )
1019
-
1020
- pic.line.color.rgb = RGBColor(189, 195, 199)
1021
- pic.line.width = Pt(1)
1022
-
1023
- except Exception as e:
1024
- print(f"[PPTX] 이미지 추가 실패: {str(e)}")
1025
-
1026
- # 페이지 번호 추가
1027
- page_num = slide.shapes.add_textbox(
1028
- Inches(15), Inches(8.5),
1029
- Inches(1), Inches(0.5)
1030
- )
1031
- page_frame = page_num.text_frame
1032
- page_frame.text = str(i + 1)
1033
- page_para = page_frame.paragraphs[0]
1034
- page_para.font.size = Pt(12)
1035
- page_para.font.color.rgb = RGBColor(127, 140, 141)
1036
- page_para.alignment = PP_ALIGN.RIGHT
1037
-
1038
- # 발표자 노트 추가
1039
- notes_slide = slide.notes_slide
1040
- notes_slide.notes_text_frame.text = slide_data.get('speaker_notes', '')
1041
-
1042
- # 파일 저장
1043
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1044
- filename = f"presentation_{timestamp}.pptx"
1045
- filepath = os.path.join("/tmp", filename)
1046
- prs.save(filepath)
1047
-
1048
- print(f"[PPTX] 파일 생성 완료: {filename}")
1049
- return filepath
1050
-
1051
- def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> List[Dict]:
1052
- """선택된 슬라이드 수에 따라 동적으로 슬라이드 구성"""
1053
- core_slides = template.get("core_slides", [])
1054
- optional_slides = template.get("optional_slides", [])
1055
-
1056
- # 표지와 Thank You를 제외한 본문 슬라이드 수
1057
- content_slide_count = slide_count
1058
-
1059
- # 코어 슬라이드가 요청된 수보다 많으면 조정
1060
- if len(core_slides) > content_slide_count:
1061
- selected_slides = core_slides[:content_slide_count]
1062
- else:
1063
- # 코어 슬라이드 + 옵셔널 슬라이드에�� 선택
1064
- selected_slides = core_slides.copy()
1065
- remaining = content_slide_count - len(core_slides)
1066
-
1067
- if remaining > 0 and optional_slides:
1068
- # 옵셔널 슬라이드에서 추가 선택
1069
- additional = optional_slides[:remaining]
1070
- selected_slides.extend(additional)
1071
-
1072
- # 표지 추가 (맨 앞)
1073
- slides = [{"title": "표지", "style": "Title Slide (Hero)", "prompt_hint": "프레젠테이션 표지"}]
1074
-
1075
- # 본문 슬라이드 추가
1076
- slides.extend(selected_slides)
1077
-
1078
- # Thank You 슬라이드 추가 (맨 뒤)
1079
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"})
1080
-
1081
- return slides
1082
-
1083
- def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict],
1084
- slide_count: int, seed: int, uploaded_file: str,
1085
- use_web_search: bool, progress=gr.Progress()):
1086
- """PPT 이미지와 텍스트 내용을 함께 생성 (발표자 노트 포함)"""
1087
- results = []
1088
- preview_html = ""
1089
-
1090
- # 업로드된 파일 내용 읽기
1091
- uploaded_content = ""
1092
- if uploaded_file:
1093
- uploaded_content = read_uploaded_file(uploaded_file.name)
1094
- print(f"[파일 업로드] 내용 길이: {len(uploaded_content)}자")
1095
-
1096
- # 템플릿 선택 및 슬라이드 구성
1097
- if template_name == "사용자 정의" and custom_slides:
1098
- slides = [{"title": "표지", "style": "Title Slide (Hero)", "prompt_hint": "프레젠테이션 표지"}]
1099
- slides.extend(custom_slides)
1100
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"})
1101
- else:
1102
- template = PPT_TEMPLATES[template_name]
1103
- slides = generate_dynamic_slides(topic, template, slide_count)
1104
-
1105
- if not slides:
1106
- yield "", "슬라이드가 정의되지 않았습니다.", None
1107
- return
1108
-
1109
- total_slides = len(slides)
1110
- print(f"\n[PPT 생성] 시작 - 총 {total_slides}개 슬라이드 (표지 + 본문 {slide_count} + Thank You)")
1111
- print(f"[PPT 생성] 주제: {topic}")
1112
- print(f"[PPT 생성] 템플릿: {template_name}")
1113
- print(f"[PPT 생성] 웹 검색: {'사용' if use_web_search else '미사용'}")
1114
-
1115
- # 웹 검색 실행 (선택된 경우)
1116
- web_search_results = []
1117
- if use_web_search and BRAVE_API_TOKEN:
1118
- progress(0.05, "웹 검색 중...")
1119
- web_search_results = brave_search(topic)
1120
-
1121
- # CSS 스타일 추가
1122
- preview_html = """
1123
- <style>
1124
- .slides-container {
1125
- width: 100%;
1126
- max-width: 1400px;
1127
- margin: 0 auto;
1128
- }
1129
- </style>
1130
- <div class="slides-container">
1131
- """
1132
-
1133
- # 각 슬라이드 순차 처리
1134
- for i, slide in enumerate(slides):
1135
- progress((i + 1) / (total_slides + 1), f"슬라이드 {i+1}/{total_slides} 처리 중...")
1136
-
1137
- slide_info = f"슬라이드 {i+1}: {slide['title']}"
1138
-
1139
- # 텍스트 내용 생성
1140
- slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
1141
- content = generate_slide_content(
1142
- topic, slide['title'], slide_context,
1143
- uploaded_content, web_search_results
1144
- )
1145
-
1146
- # 발표자 노트 생성
1147
- speaker_notes = generate_presentation_notes(topic, slide['title'], content)
1148
-
1149
- # 프롬프트 생성 및 이미지 생성
1150
- style_key = slide["style"]
1151
- if style_key in STYLE_TEMPLATES:
1152
- style_info = STYLE_TEMPLATES[style_key]
1153
- prompt = generate_prompt_with_llm(
1154
- topic, style_info["example"],
1155
- slide_context, uploaded_content
1156
- )
1157
-
1158
- # 이미지 생성
1159
- slide_seed = seed + i
1160
- img, used_prompt = generate_image(prompt, slide_seed, slide_info)
1161
-
1162
- # 슬라이드 데이터 구성
1163
- slide_data = {
1164
- "slide_number": i + 1,
1165
- "title": slide["title"],
1166
- "subtitle": content["subtitle"],
1167
- "bullet_points": content["bullet_points"],
1168
- "image": img,
1169
- "style": style_info["name"],
1170
- "speaker_notes": speaker_notes,
1171
- "topic": topic # 표지용
1172
- }
1173
-
1174
- # 프리뷰 HTML 생성
1175
- preview_html += create_slide_preview_html(slide_data)
1176
-
1177
- # 현재까지의 상태 업데이트
1178
- yield preview_html + "</div>", f"### 🔄 {slide_info} 생성 중...", None
1179
-
1180
- results.append({
1181
- "slide_data": slide_data,
1182
- "success": img is not None
1183
- })
1184
-
1185
- # PPTX 파일 생성
1186
- progress(0.95, "PPTX 파일 생성 중...")
1187
- pptx_path = None
1188
- try:
1189
- pptx_path = create_pptx_file(results, topic, template_name)
1190
- except Exception as e:
1191
- print(f"[PPTX] 파일 생성 오류: {str(e)}")
1192
-
1193
- # 최종 결과
1194
- preview_html += "</div>"
1195
- progress(1.0, "완료!")
1196
- successful = sum(1 for r in results if r["success"])
1197
- final_status = f"### 🎉 생성 완료! 총 {total_slides}개 슬라이드 중 {successful}개 성공"
1198
-
1199
- if pptx_path:
1200
- final_status += f"\n\n### 📥 PPTX 파일이 준비되었습니다! 아래에서 다운로드하세요."
1201
- final_status += f"\n\n💡 **발표자 노트가 각 슬라이드에 포함되어 있습니다!**"
1202
-
1203
- yield preview_html, final_status, pptx_path
1204
-
1205
- def create_custom_slides_ui():
1206
- """사용자 정의 슬라이드 구성 UI (3-20장)"""
1207
- slides = []
1208
- for i in range(20): # 최대 20장
1209
- with gr.Row(visible=(i < 3)): # 기본 3장만 표시
1210
- with gr.Column(scale=2):
1211
- title = gr.Textbox(
1212
- label=f"슬라이드 {i+1} 제목",
1213
- placeholder="예: 현황 분석, 솔루션, 로드맵...",
1214
- )
1215
- with gr.Column(scale=3):
1216
- style = gr.Dropdown(
1217
- choices=list(STYLE_TEMPLATES.keys()),
1218
- label=f"스타일 선택",
1219
- value="Colorful Mind Map"
1220
- )
1221
- with gr.Column(scale=3):
1222
- hint = gr.Textbox(
1223
- label=f"프롬프트 힌트",
1224
- placeholder="이 슬라이드에서 표현하고 싶은 내용"
1225
- )
1226
- slides.append({"title": title, "style": style, "hint": hint, "row": gr.Row})
1227
- return slides
1228
-
1229
- # Gradio 인터페이스 생성
1230
- with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
1231
- .preview-container { max-width: 1400px; margin: 0 auto; }
1232
- """) as demo:
1233
- gr.Markdown("""
1234
- # 🎯 AI 기반 PPT 통합 생성기 (업그레이드 버전)
1235
-
1236
- ### 텍스트와 이미지가 완벽하게 조화된 프레젠테이션을 자동으로 생성하고 다운로드하세요!
1237
-
1238
- #### 🆕 새로운 기능:
1239
- - 📊 **표지와 Thank You 슬라이드** 자동 추가
1240
- - 📝 **발표자 노트** 자동 생성 (구어체)
1241
- - 📁 **파일 업로드** 지원 (PDF/CSV/TXT)
1242
- - 🔍 **웹 검색** 기능 (Brave Search)
1243
- - 🎚️ **슬라이드 수 조절** (6-20장)
1244
- """)
1245
-
1246
- # API 토큰 상태 확인
1247
- token_status = []
1248
- if not REPLICATE_API_TOKEN:
1249
- token_status.append("⚠️ RAPI_TOKEN 환경 변수가 설정되지 않았습니다.")
1250
- if not FRIENDLI_TOKEN:
1251
- token_status.append("⚠️ FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.")
1252
- if not BRAVE_API_TOKEN:
1253
- token_status.append("ℹ️ BAPI_TOKEN이 없어 웹 검색 기능이 비활성화됩니다.")
1254
-
1255
- if token_status:
1256
- gr.Markdown("\n".join(token_status))
1257
-
1258
- with gr.Row():
1259
- with gr.Column(scale=1):
1260
- # 기본 입력
1261
- topic_input = gr.Textbox(
1262
- label="프레젠테이션 주제",
1263
- placeholder="예: AI 스타트업 투자 유치, 신제품 런칭, 디지털 전환 전략",
1264
- lines=2
1265
- )
1266
-
1267
- # PPT 템플릿 선택
1268
- template_select = gr.Dropdown(
1269
- choices=list(PPT_TEMPLATES.keys()),
1270
- label="PPT 템플릿 선택",
1271
- value="비즈니스 제안서",
1272
- info="목적에 맞는 템플릿을 선택하세요"
1273
- )
1274
-
1275
- # 슬라이드 수 선택
1276
- slide_count = gr.Slider(
1277
- minimum=6,
1278
- maximum=20,
1279
- value=8,
1280
- step=1,
1281
- label="본문 슬라이드 수 (표지와 Thank You 제외)",
1282
- info="생성할 본문 슬라이드 수를 선택하세요"
1283
- )
1284
-
1285
- # 파일 업로드
1286
- file_upload = gr.File(
1287
- label="참고 자료 업로드 (선택)",
1288
- file_types=[".pdf", ".csv", ".txt"],
1289
- type="filepath"
1290
- )
1291
-
1292
- # 웹 검색 옵션
1293
- use_web_search = gr.Checkbox(
1294
- label="웹 검색 사용",
1295
- value=False,
1296
- info="Brave Search를 사용하여 최신 정보 반영"
1297
- )
1298
-
1299
- # 템플릿 설명
1300
- template_info = gr.Markdown()
1301
-
1302
- # 시드 값
1303
- seed_input = gr.Slider(
1304
- minimum=1,
1305
- maximum=100,
1306
- value=10,
1307
- step=1,
1308
- label="시드 값"
1309
- )
1310
-
1311
- generate_btn = gr.Button("🚀 PPT 전체 생성 (텍스트 + 이미지 + 발표노트)", variant="primary", size="lg")
1312
-
1313
- # PPTX 다운로드 영역
1314
- with gr.Row():
1315
- download_file = gr.File(
1316
- label="📥 생성된 PPTX 파일 다운로드",
1317
- visible=True,
1318
- elem_id="download-file"
1319
- )
1320
-
1321
- # 사용자 정의 섹션
1322
- with gr.Accordion("📝 사용자 정의 슬라이드 구성", open=False) as custom_accordion:
1323
- gr.Markdown("템플릿을 사용하지 않고 직접 슬라이드를 구성하세요. (3-20장)")
1324
-
1325
- # 사용자 정의 슬라이드 수 선택
1326
- custom_slide_count = gr.Slider(
1327
- minimum=3,
1328
- maximum=20,
1329
- value=3,
1330
- step=1,
1331
- label="사용자 정의 슬라이드 수"
1332
- )
1333
-
1334
- custom_slides_components = create_custom_slides_ui()
1335
-
1336
- # 상태 표시
1337
- status_output = gr.Markdown(
1338
- value="### 👆 템플릿을 선택하고 생성 버튼을 클릭하세요!"
1339
- )
1340
-
1341
- # 프리뷰 영역
1342
- preview_output = gr.HTML(
1343
- label="PPT 프리뷰 (16:9)",
1344
- elem_classes="preview-container"
1345
- )
1346
-
1347
- # 이벤트 핸들러
1348
- def update_template_info(template_name, slide_count):
1349
- if template_name in PPT_TEMPLATES:
1350
- template = PPT_TEMPLATES[template_name]
1351
- info = f"**{template['description']}**\n\n"
1352
-
1353
- # 동적으로 생성될 슬라이드 구성 표시
1354
- slides = generate_dynamic_slides("", template, slide_count)
1355
- info += f"생성될 슬라이드 ({len(slides)}장):\n"
1356
- for i, slide in enumerate(slides):
1357
- style_info = STYLE_TEMPLATES.get(slide['style'], {})
1358
- info += f"{i+1}. {slide['title']} - {style_info.get('use_case', '')}\n"
1359
-
1360
- return info
1361
- return ""
1362
-
1363
- def update_custom_slides_visibility(count):
1364
- """사용자 정의 슬라이드 수에 따라 UI 표시/숨김"""
1365
- updates = []
1366
- for i in range(20):
1367
- updates.extend([
1368
- gr.update(visible=(i < count)), # title
1369
- gr.update(visible=(i < count)), # style
1370
- gr.update(visible=(i < count)) # hint
1371
- ])
1372
- return updates
1373
-
1374
- def generate_ppt_handler(topic, template_name, slide_count, seed, file_upload,
1375
- use_web_search, custom_slide_count, progress=gr.Progress(),
1376
- *custom_inputs):
1377
- if not topic.strip():
1378
- yield "", "❌ 주제를 입력해주세요.", None
1379
- return
1380
-
1381
- # 사용자 정의 슬라이드 처리
1382
- custom_slides = []
1383
- if template_name == "사용자 정의":
1384
- for i in range(0, custom_slide_count * 3, 3):
1385
- if i < len(custom_inputs):
1386
- title = custom_inputs[i]
1387
- style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None
1388
- hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else ""
1389
-
1390
- if title and style:
1391
- custom_slides.append({
1392
- "title": title,
1393
- "style": style,
1394
- "prompt_hint": hint
1395
- })
1396
-
1397
- # PPT 생성
1398
- for preview, status, pptx_file in generate_ppt_with_content(
1399
- topic, template_name, custom_slides, slide_count, seed,
1400
- file_upload, use_web_search, progress
1401
- ):
1402
- yield preview, status, pptx_file
1403
-
1404
- # 이벤트 연결
1405
- template_select.change(
1406
- fn=update_template_info,
1407
- inputs=[template_select, slide_count],
1408
- outputs=[template_info]
1409
- )
1410
-
1411
- slide_count.change(
1412
- fn=update_template_info,
1413
- inputs=[template_select, slide_count],
1414
- outputs=[template_info]
1415
- )
1416
-
1417
- custom_slide_count.change(
1418
- fn=update_custom_slides_visibility,
1419
- inputs=[custom_slide_count],
1420
- outputs=[comp for slide in custom_slides_components for comp in [slide["title"], slide["style"], slide["hint"]]]
1421
- )
1422
-
1423
- # 사용자 정의 입력 수집
1424
- all_custom_inputs = []
1425
- for slide_components in custom_slides_components:
1426
- all_custom_inputs.extend([
1427
- slide_components["title"],
1428
- slide_components["style"],
1429
- slide_components["hint"]
1430
- ])
1431
-
1432
- generate_btn.click(
1433
- fn=generate_ppt_handler,
1434
- inputs=[topic_input, template_select, slide_count, seed_input,
1435
- file_upload, use_web_search, custom_slide_count] + all_custom_inputs,
1436
- outputs=[preview_output, status_output, download_file]
1437
- )
1438
-
1439
- # 초기 템플릿 정보 표시
1440
- demo.load(
1441
- fn=update_template_info,
1442
- inputs=[template_select, slide_count],
1443
- outputs=[template_info]
1444
- )
1445
-
1446
- # 앱 실행
1447
- if __name__ == "__main__":
1448
- print("\n" + "="*50)
1449
- print("🚀 PPT 통합 생성기 (업그레이드 버전) 시작!")
1450
- print("="*50)
1451
-
1452
- # 환경 변수 확인
1453
- if not REPLICATE_API_TOKEN:
1454
- print("⚠️ 경고: RAPI_TOKEN 환경 변수가 설정되지 않았습니다.")
1455
- else:
1456
- print("✅ RAPI_TOKEN 확인됨")
1457
-
1458
- if not FRIENDLI_TOKEN:
1459
- print("⚠️ 경고: FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.")
1460
- else:
1461
- print("✅ FRIENDLI_TOKEN 확인됨")
1462
-
1463
- if not BRAVE_API_TOKEN:
1464
- print("ℹ️ BAPI_TOKEN이 없어 웹 검색 기능이 비활성화됩니다.")
1465
- else:
1466
- print("✅ BAPI_TOKEN 확인됨")
1467
-
1468
- print("="*50 + "\n")
1469
-
1470
- demo.launch()