import gradio as gr import replicate import requests import os import json import asyncio import concurrent.futures from io import BytesIO from PIL import Image from typing import List, Tuple, Dict import zipfile from datetime import datetime import time import traceback # 환경 변수에서 토큰 가져오기 REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN") FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN") # 스타일 정의 STYLE_TEMPLATES = { "3D Style (Pixar-like)": { "name": "3D Style", "description": "Pixar-esque 3D render with volumetric lighting", "use_case": "표지, 비전, 미래 컨셉", "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." }, "Elegant SWOT Quadrant": { "name": "SWOT Analysis", "description": "Flat-design 4-grid layout with minimal shadows", "use_case": "현황 분석, 전략 평가", "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" }, "Colorful Mind Map": { "name": "Mind Map", "description": "Hand-drawn educational style with vibrant colors", "use_case": "브레인스토밍, 아이디어 정리", "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" }, "Business Workflow": { "name": "Business Process", "description": "End-to-end business workflow with clear phases", "use_case": "프로세스 설명, 단계별 진행", "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" }, "Industrial Design": { "name": "Product Design", "description": "Sleek industrial design concept sketch", "use_case": "제품 소개, 컨셉 디자인", "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" }, "3D Bubble Chart": { "name": "Bubble Chart", "description": "Clean 3D bubble visualization", "use_case": "비교 분석, 포지셔닝", "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" }, "Timeline Ribbon": { "name": "Timeline", "description": "Horizontal ribbon timeline with cyber-futuristic vibe", "use_case": "일정, 로드맵, 마일스톤", "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" }, "Risk Heat Map": { "name": "Heat Map", "description": "Risk assessment heat map with gradient colors", "use_case": "리스크 분석, 우선순위", "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" }, "Pyramid/Funnel": { "name": "Funnel Chart", "description": "Multi-layer gradient funnel visualization", "use_case": "단계별 축소, 핵심 도출", "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" }, "KPI Dashboard": { "name": "Dashboard", "description": "Dark-mode analytics dashboard with sci-fi interface", "use_case": "성과 지표, 실적 대시보드", "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" }, "Value Chain": { "name": "Value Chain", "description": "Horizontal value chain with industrial look", "use_case": "가치 사슬, 비즈니스 모델", "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" }, "Gantt Chart": { "name": "Gantt Chart", "description": "Hand-drawn style Gantt chart with playful colors", "use_case": "프로젝트 일정, 작업 관리", "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" }, "Mobile App Mockup": { "name": "App Mockup", "description": "Clean wireframe for mobile app design", "use_case": "앱/웹 UI, 화면 설계", "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" }, "Flowchart": { "name": "Flowchart", "description": "Vibrant flowchart with minimalistic icons", "use_case": "의사결정, 프로세스 흐름", "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" } } # PPT 템플릿 정의 PPT_TEMPLATES = { "비즈니스 제안서": { "description": "투자 유치, 사업 제안용", "slides": [ {"title": "표지", "style": "3D Style (Pixar-like)", "prompt_hint": "회사 비전과 미래"}, {"title": "목차", "style": "Flowchart", "prompt_hint": "프레젠테이션 구조"}, {"title": "문제 정의", "style": "Colorful Mind Map", "prompt_hint": "현재 시장의 문제점"}, {"title": "현황 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "강점, 약점, 기회, 위협"}, {"title": "솔루션", "style": "Industrial Design", "prompt_hint": "제품/서비스 컨셉"}, {"title": "프로세스", "style": "Business Workflow", "prompt_hint": "실행 단계"}, {"title": "일정", "style": "Timeline Ribbon", "prompt_hint": "주요 마일스톤"}, {"title": "성과 예측", "style": "KPI Dashboard", "prompt_hint": "예상 성과 지표"}, {"title": "투자 요청", "style": "Pyramid/Funnel", "prompt_hint": "투자 규모와 활용"} ] }, "제품 소개": { "description": "신제품 런칭, 서비스 소개용", "slides": [ {"title": "제품 컨셉", "style": "Industrial Design", "prompt_hint": "제품 디자인"}, {"title": "사용자 니즈", "style": "Colorful Mind Map", "prompt_hint": "고객 페인포인트"}, {"title": "기능 소개", "style": "Mobile App Mockup", "prompt_hint": "UI/UX 화면"}, {"title": "작동 원리", "style": "Flowchart", "prompt_hint": "기능 플로우"}, {"title": "시장 포지션", "style": "3D Bubble Chart", "prompt_hint": "경쟁사 비교"}, {"title": "출시 일정", "style": "Timeline Ribbon", "prompt_hint": "런칭 로드맵"} ] }, "프로젝트 보고": { "description": "진행 상황, 성과 보고용", "slides": [ {"title": "프로젝트 개요", "style": "Business Workflow", "prompt_hint": "전체 프로세스"}, {"title": "진행 현황", "style": "Gantt Chart", "prompt_hint": "작업 일정"}, {"title": "리스크 관리", "style": "Risk Heat Map", "prompt_hint": "위험 요소"}, {"title": "성과 지표", "style": "KPI Dashboard", "prompt_hint": "달성 실적"}, {"title": "향후 계획", "style": "Timeline Ribbon", "prompt_hint": "다음 단계"} ] }, "전략 기획": { "description": "중장기 전략, 비전 수립용", "slides": [ {"title": "비전", "style": "3D Style (Pixar-like)", "prompt_hint": "미래 비전"}, {"title": "환경 분석", "style": "Elegant SWOT Quadrant", "prompt_hint": "내외부 환경"}, {"title": "전략 체계", "style": "Colorful Mind Map", "prompt_hint": "전략 구조"}, {"title": "가치 사슬", "style": "Value Chain", "prompt_hint": "비즈니스 모델"}, {"title": "실행 로드맵", "style": "Timeline Ribbon", "prompt_hint": "단계별 계획"}, {"title": "목표 지표", "style": "KPI Dashboard", "prompt_hint": "KPI 목표"} ] }, "사용자 정의": { "description": "직접 구성하기", "slides": [] } } def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None) -> str: """주제와 스타일 예제를 받아서 LLM을 사용해 이미지 프롬프트를 생성""" print(f"[LLM] 프롬프트 생성 시작: {slide_context}") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides. Your task is to create prompts that: 1. Are highly specific and visual, perfect for PPT backgrounds or main visuals 2. Consider the slide's purpose and maintain consistency across a presentation 3. Include style references matching the given example 4. Focus on clean, professional visuals that won't distract from text overlays 5. Ensure high contrast areas for text readability when needed 6. Maintain brand consistency and professional aesthetics Important guidelines: - If given a style example, adapt the topic to match that specific visual style - Consider the slide context (e.g., "표지", "현황 분석") to create appropriate visuals - Always output ONLY the prompt without any explanation - Keep prompts between 50-150 words for optimal results - Ensure the visual supports rather than overwhelms the slide content""" user_message = f"Topic: {topic}" if style_example: user_message += f"\n\nStyle reference to follow:\n{style_example}" if slide_context: user_message += f"\n\nSlide context: {slide_context}" payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": system_prompt }, { "role": "user", "content": user_message } ], "max_tokens": 300, "top_p": 0.8, "temperature": 0.7, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() prompt = result['choices'][0]['message']['content'].strip() print(f"[LLM] 프롬프트 생성 완료: {prompt[:50]}...") return prompt else: error_msg = f"프롬프트 생성 실패: {response.status_code}" print(f"[LLM] {error_msg}") return error_msg except Exception as e: error_msg = f"프롬프트 생성 중 오류 발생: {str(e)}" print(f"[LLM] {error_msg}") return error_msg def translate_to_english(text: str) -> str: """한글 텍스트를 영어로 번역 (LLM 사용)""" if not any(ord('가') <= ord(char) <= ord('힣') for char in text): return text print(f"[번역] 한글 감지, 영어로 번역 시작") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } payload = { "model": "dep89a2fld32mcm", "messages": [ { "role": "system", "content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation." }, { "role": "user", "content": text } ], "max_tokens": 500, "top_p": 0.8, "stream": False } try: response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: result = response.json() translated = result['choices'][0]['message']['content'].strip() print(f"[번역] 완료") return translated else: print(f"[번역] 실패, 원본 사용") return text except Exception as e: print(f"[번역] 오류: {str(e)}, 원본 사용") return text def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]: """Replicate API를 사용해 이미지 생성""" print(f"\n[이미지 생성] {slide_info}") print(f"[이미지 생성] 프롬프트: {prompt[:50]}...") try: english_prompt = translate_to_english(prompt) if not REPLICATE_API_TOKEN: error_msg = "RAPI_TOKEN 환경변수가 설정되지 않았습니다." print(f"[이미지 생성] 오류: {error_msg}") return None, error_msg print(f"[이미지 생성] Replicate API 호출 중...") client = replicate.Client(api_token=REPLICATE_API_TOKEN) input_params = { "seed": seed, "prompt": english_prompt, "speed_mode": "Extra Juiced 🚀 (even more speed)", "output_quality": 100 } start_time = time.time() output = client.run( "prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645", input=input_params ) elapsed = time.time() - start_time print(f"[이미지 생성] API 응답 받음 ({elapsed:.1f}초)") if output: if isinstance(output, str) and output.startswith('http'): print(f"[이미지 생성] URL에서 이미지 다운로드 중...") response = requests.get(output, timeout=30) img = Image.open(BytesIO(response.content)) print(f"[이미지 생성] 완료!") return img, english_prompt else: print(f"[이미지 생성] 바이너리 데이터 처리 중...") img = Image.open(BytesIO(output.read())) print(f"[이미지 생성] 완료!") return img, english_prompt else: error_msg = "이미지 생성 실패 - 빈 응답" print(f"[이미지 생성] {error_msg}") return None, error_msg except Exception as e: error_msg = f"오류: {str(e)}" print(f"[이미지 생성] {error_msg}") print(f"[이미지 생성] 상세 오류:\n{traceback.format_exc()}") return None, error_msg def generate_ppt_images_sequential(topic: str, template_name: str, custom_slides: List[Dict], seed: int, progress=gr.Progress()): """PPT 템플릿에 따라 이미지를 순차적으로 생성하며 진행 상황 표시""" results = [] images_for_gallery = [] # 템플릿 선택 if template_name == "사용자 정의" and custom_slides: slides = custom_slides else: slides = PPT_TEMPLATES[template_name]["slides"] if not slides: yield [], "슬라이드가 정의되지 않았습니다." return total_slides = len(slides) print(f"\n[PPT 생성] 시작 - 총 {total_slides}개 슬라이드") print(f"[PPT 생성] 주제: {topic}") print(f"[PPT 생성] 템플릿: {template_name}") # 각 슬라이드 순차 처리 for i, slide in enumerate(slides): progress((i + 1) / (total_slides + 1), f"슬라이드 {i+1}/{total_slides} 처리 중...") slide_info = f"슬라이드 {i+1}: {slide['title']}" status_msg = f"\n### 🔄 {slide_info} 생성 중...\n" # 현재까지의 상태 업데이트 yield images_for_gallery, status_msg + format_results_status(results) style_key = slide["style"] if style_key in STYLE_TEMPLATES: style_info = STYLE_TEMPLATES[style_key] slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}" # 프롬프트 생성 prompt = generate_prompt_with_llm(topic, style_info["example"], slide_context) # 이미지 생성 slide_seed = seed + i img, used_prompt = generate_image(prompt, slide_seed, slide_info) # 결과 저장 result = { "slide_title": slide["title"], "style": slide["style"], "image": img, "prompt": prompt, "used_prompt": used_prompt, "success": img is not None } results.append(result) # 성공한 이미지는 갤러리에 추가 if img is not None: caption = f"{i+1}. {slide['title']} ({style_info['name']})" images_for_gallery.append((img, caption)) status_msg = f"\n### ✅ {slide_info} 완료!\n" else: status_msg = f"\n### ❌ {slide_info} 실패: {used_prompt}\n" # 즉시 업데이트 yield images_for_gallery, status_msg + format_results_status(results) # 최종 결과 progress(1.0, "완료!") successful = sum(1 for r in results if r["success"]) final_status = f"\n## 🎉 생성 완료!\n총 {total_slides}개 슬라이드 중 {successful}개 성공\n\n" final_status += format_results_status(results) yield images_for_gallery, final_status def format_results_status(results: List[Dict]) -> str: """결과를 상태 메시지로 포맷""" if not results: return "" status_lines = ["### 📊 생성 결과:\n"] for i, result in enumerate(results): if result["success"]: status_lines.append(f"✅ **슬라이드 {i+1}: {result['slide_title']}**") status_lines.append(f" - 스타일: {result['style'].split('(')[0].strip()}") status_lines.append(f" - 프롬프트: {result['prompt'][:60]}...\n") else: status_lines.append(f"❌ **슬라이드 {i+1}: {result['slide_title']}** - 실패") status_lines.append(f" - 오류: {result['used_prompt']}\n") return "\n".join(status_lines) def create_custom_slides_ui(): """사용자 정의 슬라이드 구성 UI""" slides = [] for i in range(10): with gr.Row(): with gr.Column(scale=2): title = gr.Textbox( label=f"슬라이드 {i+1} 제목", placeholder="예: 표지, 목차, 현황 분석...", visible=(i < 3) ) with gr.Column(scale=3): style = gr.Dropdown( choices=list(STYLE_TEMPLATES.keys()), label=f"스타일 선택", visible=(i < 3) ) with gr.Column(scale=3): hint = gr.Textbox( label=f"프롬프트 힌트", placeholder="이 슬라이드에서 표현하고 싶은 내용", visible=(i < 3) ) slides.append({"title": title, "style": style, "hint": hint}) return slides # Gradio 인터페이스 생성 with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🎯 AI 기반 PPT 이미지 생성기 ### 전문적인 프레젠테이션을 위한 맞춤형 이미지를 한 번에 생성하세요! """) # API 토큰 상태 확인 if not REPLICATE_API_TOKEN: gr.Markdown("⚠️ **경고**: RAPI_TOKEN 환경 변수가 설정되지 않았습니다.") if not FRIENDLI_TOKEN: gr.Markdown("⚠️ **경고**: FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.") with gr.Row(): with gr.Column(scale=1): # 기본 입력 topic_input = gr.Textbox( label="프레젠테이션 주제", placeholder="예: AI 스타트업 투자 유치, 신제품 런칭, 디지털 전환 전략", lines=2 ) # PPT 템플릿 선택 template_select = gr.Dropdown( choices=list(PPT_TEMPLATES.keys()), label="PPT 템플릿 선택", value="비즈니스 제안서", info="목적에 맞는 템플릿을 선택하세요" ) # 템플릿 설명 template_info = gr.Markdown() # 시드 값 seed_input = gr.Slider( minimum=1, maximum=100, value=10, step=1, label="시드 값" ) generate_btn = gr.Button("🚀 PPT 이미지 세트 생성", variant="primary", size="lg") # 사용자 정의 섹션 with gr.Accordion("📝 사용자 정의 슬라이드 구성", open=False) as custom_accordion: gr.Markdown("템플릿을 사용하지 않고 직접 슬라이드를 구성하세요.") custom_slides_components = create_custom_slides_ui() with gr.Column(scale=2): # 진행 상황 표시 status_output = gr.Markdown( value="### 👆 템플릿을 선택하고 생성 버튼을 클릭하세요!\n\n생성 진행 상황이 실시간으로 표시됩니다." ) # 결과 갤러리 output_gallery = gr.Gallery( label="생성된 PPT 이미지", show_label=True, elem_id="gallery", columns=3, rows=3, object_fit="contain", height="auto" ) # 스타일 가이드 with gr.Accordion("📚 스타일별 활용 가이드", open=False): style_guide = "### PPT 제작을 위한 스타일 가이드\n\n" for style_name, style_info in STYLE_TEMPLATES.items(): style_guide += f"**{style_name}**\n" style_guide += f"- 용도: {style_info['use_case']}\n" style_guide += f"- 특징: {style_info['description']}\n\n" gr.Markdown(style_guide) # 활용 팁 gr.Markdown(""" --- ### 💡 PPT 제작 팁: 1. **템플릿 활용**: 목적에 맞는 템플릿을 선택하면 최적화된 슬라이드 구성을 제공합니다 2. **일관성**: 하나의 주제로 전체 슬라이드를 생성하여 시각적 일관성을 유지합니다 3. **실시간 확인**: 각 슬라이드가 생성될 때마다 진행 상황을 확인할 수 있습니다 4. **고화질**: 모든 이미지는 프레젠테이션에 적합한 고해상도로 생성됩니다 """) # 이벤트 핸들러 def update_template_info(template_name): if template_name in PPT_TEMPLATES: template = PPT_TEMPLATES[template_name] info = f"**{template['description']}**\n\n포함된 슬라이드:\n" for i, slide in enumerate(template['slides']): info += f"{i+1}. {slide['title']} - {STYLE_TEMPLATES[slide['style']]['use_case']}\n" return info return "" def generate_ppt_images_handler(topic, template_name, seed, progress=gr.Progress(), *custom_inputs): if not topic.strip(): yield [], "❌ 주제를 입력해주세요." return # 사용자 정의 슬라이드 처리 custom_slides = [] if template_name == "사용자 정의": for i in range(0, len(custom_inputs), 3): title = custom_inputs[i] style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else "" if title and style: custom_slides.append({ "title": title, "style": style, "prompt_hint": hint }) # 순차적으로 이미지 생성하며 진행 상황 표시 for gallery, status in generate_ppt_images_sequential(topic, template_name, custom_slides, seed, progress): yield gallery, status # 이벤트 연결 template_select.change( fn=update_template_info, inputs=[template_select], outputs=[template_info] ) # 사용자 정의 입력 수집 all_custom_inputs = [] for slide_components in custom_slides_components: all_custom_inputs.extend([ slide_components["title"], slide_components["style"], slide_components["hint"] ]) generate_btn.click( fn=generate_ppt_images_handler, inputs=[topic_input, template_select, seed_input] + all_custom_inputs, outputs=[output_gallery, status_output] ) # 초기 템플릿 정보 표시 demo.load( fn=update_template_info, inputs=[template_select], outputs=[template_info] ) # 앱 실행 if __name__ == "__main__": print("\n" + "="*50) print("🚀 PPT 이미지 생성기 시작!") print("="*50) # 환경 변수 확인 if not REPLICATE_API_TOKEN: print("⚠️ 경고: RAPI_TOKEN 환경 변수가 설정되지 않았습니다.") else: print("✅ RAPI_TOKEN 확인됨") if not FRIENDLI_TOKEN: print("⚠️ 경고: FRIENDLI_TOKEN 환경 변수가 설정되지 않았습니다.") else: print("✅ FRIENDLI_TOKEN 확인됨") print("="*50 + "\n") demo.launch()