Update app.py
Browse files
app.py
CHANGED
@@ -10,6 +10,8 @@ from PIL import Image
|
|
10 |
from typing import List, Tuple, Dict
|
11 |
import zipfile
|
12 |
from datetime import datetime
|
|
|
|
|
13 |
|
14 |
# 환경 변수에서 토큰 가져오기
|
15 |
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
|
@@ -159,13 +161,14 @@ PPT_TEMPLATES = {
|
|
159 |
|
160 |
def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None) -> str:
|
161 |
"""주제와 스타일 예제를 받아서 LLM을 사용해 이미지 프롬프트를 생성"""
|
|
|
|
|
162 |
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
163 |
headers = {
|
164 |
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
|
165 |
"Content-Type": "application/json"
|
166 |
}
|
167 |
|
168 |
-
# PPT 제작을 위한 강화된 시스템 프롬프트
|
169 |
system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides.
|
170 |
|
171 |
Your task is to create prompts that:
|
@@ -208,20 +211,28 @@ Important guidelines:
|
|
208 |
}
|
209 |
|
210 |
try:
|
211 |
-
response = requests.post(url, json=payload, headers=headers)
|
212 |
if response.status_code == 200:
|
213 |
result = response.json()
|
214 |
-
|
|
|
|
|
215 |
else:
|
216 |
-
|
|
|
|
|
217 |
except Exception as e:
|
218 |
-
|
|
|
|
|
219 |
|
220 |
def translate_to_english(text: str) -> str:
|
221 |
"""한글 텍스트를 영어로 번역 (LLM 사용)"""
|
222 |
if not any(ord('가') <= ord(char) <= ord('힣') for char in text):
|
223 |
return text
|
224 |
|
|
|
|
|
225 |
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
226 |
headers = {
|
227 |
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
|
@@ -246,23 +257,33 @@ def translate_to_english(text: str) -> str:
|
|
246 |
}
|
247 |
|
248 |
try:
|
249 |
-
response = requests.post(url, json=payload, headers=headers)
|
250 |
if response.status_code == 200:
|
251 |
result = response.json()
|
252 |
-
|
|
|
|
|
253 |
else:
|
|
|
254 |
return text
|
255 |
except Exception as e:
|
|
|
256 |
return text
|
257 |
|
258 |
-
def generate_image(prompt: str, seed: int = 10) -> Tuple[Image.Image, str]:
|
259 |
"""Replicate API를 사용해 이미지 생성"""
|
|
|
|
|
|
|
260 |
try:
|
261 |
english_prompt = translate_to_english(prompt)
|
262 |
|
263 |
if not REPLICATE_API_TOKEN:
|
264 |
-
|
|
|
|
|
265 |
|
|
|
266 |
client = replicate.Client(api_token=REPLICATE_API_TOKEN)
|
267 |
|
268 |
input_params = {
|
@@ -272,28 +293,42 @@ def generate_image(prompt: str, seed: int = 10) -> Tuple[Image.Image, str]:
|
|
272 |
"output_quality": 80
|
273 |
}
|
274 |
|
|
|
275 |
output = client.run(
|
276 |
"prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645",
|
277 |
input=input_params
|
278 |
)
|
279 |
|
|
|
|
|
|
|
280 |
if output:
|
281 |
if isinstance(output, str) and output.startswith('http'):
|
282 |
-
|
|
|
283 |
img = Image.open(BytesIO(response.content))
|
|
|
284 |
return img, english_prompt
|
285 |
else:
|
|
|
286 |
img = Image.open(BytesIO(output.read()))
|
|
|
287 |
return img, english_prompt
|
288 |
else:
|
289 |
-
|
|
|
|
|
290 |
|
291 |
except Exception as e:
|
292 |
-
|
|
|
|
|
|
|
293 |
|
294 |
-
def
|
295 |
-
"""PPT 템플릿에 따라
|
296 |
results = []
|
|
|
297 |
|
298 |
# 템플릿 선택
|
299 |
if template_name == "사용자 정의" and custom_slides:
|
@@ -302,69 +337,94 @@ def generate_ppt_images(topic: str, template_name: str, custom_slides: List[Dict
|
|
302 |
slides = PPT_TEMPLATES[template_name]["slides"]
|
303 |
|
304 |
if not slides:
|
305 |
-
|
|
|
306 |
|
307 |
-
|
308 |
-
|
309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
style_key = slide["style"]
|
311 |
if style_key in STYLE_TEMPLATES:
|
312 |
style_info = STYLE_TEMPLATES[style_key]
|
313 |
slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
|
|
|
|
|
314 |
prompt = generate_prompt_with_llm(topic, style_info["example"], slide_context)
|
315 |
-
|
316 |
-
|
317 |
-
"prompt": prompt,
|
318 |
-
"style_info": style_info
|
319 |
-
})
|
320 |
-
|
321 |
-
# 병렬로 이미지 생성
|
322 |
-
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
323 |
-
future_to_slide = {}
|
324 |
-
for i, prompt_data in enumerate(prompts):
|
325 |
-
# 각 슬라이드마다 다른 시드 사용하여 다양성 확보
|
326 |
slide_seed = seed + i
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
img,
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
# 결과
|
|
|
353 |
successful = sum(1 for r in results if r["success"])
|
354 |
-
|
|
|
355 |
|
356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
|
358 |
def create_custom_slides_ui():
|
359 |
"""사용자 정의 슬라이드 구성 UI"""
|
360 |
slides = []
|
361 |
-
for i in range(10):
|
362 |
with gr.Row():
|
363 |
with gr.Column(scale=2):
|
364 |
title = gr.Textbox(
|
365 |
label=f"슬라이드 {i+1} 제목",
|
366 |
placeholder="예: 표지, 목차, 현황 분석...",
|
367 |
-
visible=(i < 3)
|
368 |
)
|
369 |
with gr.Column(scale=3):
|
370 |
style = gr.Dropdown(
|
@@ -381,24 +441,6 @@ def create_custom_slides_ui():
|
|
381 |
slides.append({"title": title, "style": style, "hint": hint})
|
382 |
return slides
|
383 |
|
384 |
-
def format_results_for_display(results: List[Dict]) -> Tuple[List[Tuple[Image.Image, str]], str]:
|
385 |
-
"""결과를 갤러리 표시용으로 포맷"""
|
386 |
-
images = []
|
387 |
-
details = []
|
388 |
-
|
389 |
-
for i, result in enumerate(results):
|
390 |
-
if result["success"] and result["image"]:
|
391 |
-
caption = f"슬라이드 {i+1}: {result['slide_title']} ({result['style'].split('(')[0].strip()})"
|
392 |
-
images.append((result["image"], caption))
|
393 |
-
details.append(f"**슬라이드 {i+1}: {result['slide_title']}**\n"
|
394 |
-
f"스타일: {result['style']}\n"
|
395 |
-
f"프롬프트: {result['prompt']}\n")
|
396 |
-
else:
|
397 |
-
details.append(f"**슬라이드 {i+1}: {result['slide_title']}** - 생성 실패\n")
|
398 |
-
|
399 |
-
details_text = "\n---\n".join(details)
|
400 |
-
return images, details_text
|
401 |
-
|
402 |
# Gradio 인터페이스 생성
|
403 |
with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
404 |
gr.Markdown("""
|
@@ -407,6 +449,12 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
407 |
### 전문적인 프레젠테이션을 위한 맞춤형 이미지를 한 번에 생성하세요!
|
408 |
""")
|
409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
with gr.Row():
|
411 |
with gr.Column(scale=1):
|
412 |
# 기본 입력
|
@@ -442,10 +490,14 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
442 |
with gr.Accordion("📝 사용자 정의 슬라이드 구성", open=False) as custom_accordion:
|
443 |
gr.Markdown("템플릿을 사용하지 않고 직접 슬라이드를 구성하세요.")
|
444 |
custom_slides_components = create_custom_slides_ui()
|
445 |
-
add_slide_btn = gr.Button("슬라이드 추가", size="sm")
|
446 |
|
447 |
with gr.Column(scale=2):
|
448 |
-
#
|
|
|
|
|
|
|
|
|
|
|
449 |
output_gallery = gr.Gallery(
|
450 |
label="생성된 PPT 이미지",
|
451 |
show_label=True,
|
@@ -453,11 +505,7 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
453 |
columns=3,
|
454 |
rows=3,
|
455 |
object_fit="contain",
|
456 |
-
height="
|
457 |
-
)
|
458 |
-
|
459 |
-
details_output = gr.Markdown(
|
460 |
-
value="생성된 이미지의 상세 정보가 여기에 표시됩니다."
|
461 |
)
|
462 |
|
463 |
# 스타일 가이드
|
@@ -476,10 +524,8 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
476 |
|
477 |
1. **템플릿 활용**: 목적에 맞는 템플릿을 선택하면 최적화된 슬라이드 구성을 제공합니다
|
478 |
2. **일관성**: 하나의 주제로 전체 슬라이드를 생성하여 시각적 일관성을 유지합니다
|
479 |
-
3.
|
480 |
4. **고화질**: 모든 이미지는 프레젠테이션에 적합한 고해상도로 생성됩니다
|
481 |
-
|
482 |
-
**추천 워크플로우**: 템플릿 선택 → 주제 입력 → 이미지 생성 → PPT에 삽입 → 텍스트 추가
|
483 |
""")
|
484 |
|
485 |
# 이벤트 핸들러
|
@@ -492,9 +538,10 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
492 |
return info
|
493 |
return ""
|
494 |
|
495 |
-
def generate_ppt_images_handler(topic, template_name, seed, *custom_inputs):
|
496 |
if not topic.strip():
|
497 |
-
|
|
|
498 |
|
499 |
# 사용자 정의 슬라이드 처리
|
500 |
custom_slides = []
|
@@ -511,13 +558,9 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
511 |
"prompt_hint": hint
|
512 |
})
|
513 |
|
514 |
-
# 이미지
|
515 |
-
|
516 |
-
|
517 |
-
# 결과 포맷팅
|
518 |
-
images, details = format_results_for_display(results)
|
519 |
-
|
520 |
-
return images, f"{status}\n\n{details}"
|
521 |
|
522 |
# 이벤트 연결
|
523 |
template_select.change(
|
@@ -538,23 +581,9 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
538 |
generate_btn.click(
|
539 |
fn=generate_ppt_images_handler,
|
540 |
inputs=[topic_input, template_select, seed_input] + all_custom_inputs,
|
541 |
-
outputs=[output_gallery,
|
542 |
)
|
543 |
|
544 |
-
# 슬라이드 추가 버튼 (간단한 가시성 토글)
|
545 |
-
def show_more_slides(*current_values):
|
546 |
-
# 더 많은 슬라이드 입력 필드 표시 로직
|
547 |
-
updates = []
|
548 |
-
for i, val in enumerate(current_values):
|
549 |
-
if i % 3 == 0: # title fields
|
550 |
-
if not val and i > 6: # 처음 3개 이후
|
551 |
-
updates.append(gr.update(visible=False))
|
552 |
-
else:
|
553 |
-
updates.append(gr.update(visible=True))
|
554 |
-
else:
|
555 |
-
updates.append(gr.update(visible=updates[-3].visible if i % 3 == 1 else updates[-2].visible))
|
556 |
-
return updates
|
557 |
-
|
558 |
# 초기 템플릿 정보 표시
|
559 |
demo.load(
|
560 |
fn=update_template_info,
|
@@ -564,10 +593,21 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo:
|
|
564 |
|
565 |
# 앱 실행
|
566 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
567 |
# 환경 변수 확인
|
568 |
if not REPLICATE_API_TOKEN:
|
569 |
-
print("경고: RAPI_TOKEN 환경 변수가 설정되지 않았습니다.")
|
|
|
|
|
|
|
570 |
if not FRIENDLI_TOKEN:
|
571 |
-
print("경고: FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.")
|
|
|
|
|
|
|
|
|
572 |
|
573 |
demo.launch()
|
|
|
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")
|
|
|
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:
|
|
|
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}",
|
|
|
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 = {
|
|
|
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:
|
|
|
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(
|
|
|
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("""
|
|
|
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 |
# 기본 입력
|
|
|
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,
|
|
|
505 |
columns=3,
|
506 |
rows=3,
|
507 |
object_fit="contain",
|
508 |
+
height="auto"
|
|
|
|
|
|
|
|
|
509 |
)
|
510 |
|
511 |
# 스타일 가이드
|
|
|
524 |
|
525 |
1. **템플릿 활용**: 목적에 맞는 템플릿을 선택하면 최적화된 슬라이드 구성을 제공합니다
|
526 |
2. **일관성**: 하나의 주제로 전체 슬라이드를 생성하여 시각적 일관성을 유지합니다
|
527 |
+
3. **실시간 확인**: 각 슬라이드가 생성될 때마다 진행 상황을 확인할 수 있습니다
|
528 |
4. **고화질**: 모든 이미지는 프레젠테이션에 적합한 고해상도로 생성됩니다
|
|
|
|
|
529 |
""")
|
530 |
|
531 |
# 이벤트 핸들러
|
|
|
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 = []
|
|
|
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(
|
|
|
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,
|
|
|
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()
|