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 import base64 # 환경 변수에서 토큰 가져오기 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_slide_content(topic: str, slide_title: str, slide_context: str) -> Dict[str, str]: """각 슬라이드의 텍스트 내용 생성""" print(f"[슬라이드 내용] {slide_title} 텍스트 생성 중...") url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": f"Bearer {FRIENDLI_TOKEN}", "Content-Type": "application/json" } system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content. Your task is to create: 1. A compelling subtitle (max 10 words) 2. Exactly 5 bullet points, each being a complete, concise sentence 3. Each bullet point should be 10-15 words Guidelines: - Be specific and actionable - Use professional business language - Include relevant data points or metrics when appropriate - Ensure content aligns with the slide's purpose - Make each point distinct and valuable - Use active voice and strong verbs Output format: Subtitle: [subtitle here] • [Point 1] • [Point 2] • [Point 3] • [Point 4] • [Point 5]""" user_message = f"""Topic: {topic} Slide Title: {slide_title} Context: {slide_context} Create compelling content for this presentation slide.""" 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() content = result['choices'][0]['message']['content'].strip() # Parse content lines = content.split('\n') subtitle = "" bullet_points = [] for line in lines: if line.startswith("Subtitle:"): subtitle = line.replace("Subtitle:", "").strip() elif line.strip().startswith("•"): bullet_points.append(line.strip()) # 한글로 번역이 필요한 경우 if any(ord('가') <= ord(char) <= ord('힣') for char in topic): subtitle = translate_content_to_korean(subtitle) bullet_points = [translate_content_to_korean(point) for point in bullet_points] return { "subtitle": subtitle, "bullet_points": bullet_points[:5] # 최대 5개 } else: return { "subtitle": slide_title, "bullet_points": ["내용을 생성할 수 없습니다."] * 5 } except Exception as e: print(f"[슬라이드 내용] 오류: {str(e)}") return { "subtitle": slide_title, "bullet_points": ["내용을 생성할 수 없습니다."] * 5 } def translate_content_to_korean(text: str) -> str: """영어 텍스트를 한글로 번역""" 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 English text to Korean. Maintain professional business tone. Only return the translation without any explanation." }, { "role": "user", "content": text } ], "max_tokens": 200, "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() return result['choices'][0]['message']['content'].strip() else: return text except Exception as e: return text 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 create_slide_preview_html(slide_data: Dict) -> str: """16:9 비율의 슬라이드 프리뷰 HTML 생성""" # 이미지를 base64로 인코딩 img_base64 = "" if slide_data.get("image"): buffered = BytesIO() slide_data["image"].save(buffered, format="PNG") img_base64 = base64.b64encode(buffered.getvalue()).decode() # 텍스트 내용 가져오기 subtitle = slide_data.get("subtitle", "") bullet_points = slide_data.get("bullet_points", []) # HTML 생성 html = f"""
슬라이드 {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}

{subtitle}

    """ for point in bullet_points: html += f"""
  • {point.replace('•', '').strip()}
  • """ html += f"""
""" if img_base64: html += f""" Slide Image """ else: html += """
🖼️

이미지 생성 중...

""" html += """
""" return html def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict], seed: int, progress=gr.Progress()): """PPT 이미지와 텍스트 내용을 함께 생성""" results = [] preview_html = "" # 템플릿 선택 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}") # CSS 스타일 추가 preview_html = """
""" # 각 슬라이드 순차 처리 for i, slide in enumerate(slides): progress((i + 1) / (total_slides + 1), f"슬라이드 {i+1}/{total_slides} 처리 중...") slide_info = f"슬라이드 {i+1}: {slide['title']}" # 텍스트 내용 생성 slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}" content = generate_slide_content(topic, slide['title'], slide_context) # 프롬프트 생성 및 이미지 생성 style_key = slide["style"] if style_key in STYLE_TEMPLATES: style_info = STYLE_TEMPLATES[style_key] 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) # 슬라이드 데이터 구성 slide_data = { "slide_number": i + 1, "title": slide["title"], "subtitle": content["subtitle"], "bullet_points": content["bullet_points"], "image": img, "style": style_info["name"] } # 프리뷰 HTML 생성 preview_html += create_slide_preview_html(slide_data) # 현재까지의 상태 업데이트 yield preview_html + "
", f"### 🔄 {slide_info} 생성 중..." results.append({ "slide_data": slide_data, "success": img is not None }) # 최종 결과 preview_html += "" progress(1.0, "완료!") successful = sum(1 for r in results if r["success"]) final_status = f"### 🎉 생성 완료! 총 {total_slides}개 슬라이드 중 {successful}개 성공" yield preview_html, final_status 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(), css=""" .preview-container { max-width: 1400px; margin: 0 auto; } """) 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() # 상태 표시 status_output = gr.Markdown( value="### 👆 템플릿을 선택하고 생성 버튼을 클릭하세요!" ) # 프리뷰 영역 preview_output = gr.HTML( label="PPT 프리뷰 (16:9)", elem_classes="preview-container" ) # 활용 팁 gr.Markdown(""" --- ### 💡 새로운 기능: 1. **자동 텍스트 생성**: 각 슬라이드마다 적절한 제목과 5개의 핵심 포인트 자동 생성 2. **16:9 프리뷰**: 실제 PPT와 동일한 비율로 미리보기 3. **좌우 레이아웃**: 좌측 텍스트, 우측 이미지로 깔끔한 구성 4. **실시간 업데이트**: 각 슬라이드가 생성될 때마다 즉시 확인 ### 📌 활용 팁: - 생성된 내용을 복사하여 실제 PPT에 바로 사용 가능 - 각 슬라이드의 텍스트와 이미지가 주제에 맞게 자동 조율 - 한글 주제 입력 시 한글로 텍스트 생성 """) # 이벤트 핸들러 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_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 }) # PPT 생성 for preview, status in generate_ppt_with_content(topic, template_name, custom_slides, seed, progress): yield preview, 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_handler, inputs=[topic_input, template_select, seed_input] + all_custom_inputs, outputs=[preview_output, 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()