|
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 |
|
from pptx import Presentation |
|
from pptx.util import Inches, Pt |
|
from pptx.dml.color import RGBColor |
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR |
|
import PyPDF2 |
|
import pandas as pd |
|
import chardet |
|
|
|
|
|
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN") |
|
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN") |
|
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN") |
|
|
|
|
|
STYLE_TEMPLATES = { |
|
"Title Slide (Hero)": { |
|
"name": "Title Slide", |
|
"description": "Impactful hero image for title slide", |
|
"use_case": "νμ§, νμ΄ν", |
|
"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" |
|
}, |
|
"Thank You Slide": { |
|
"name": "Thank You", |
|
"description": "Elegant closing slide design", |
|
"use_case": "λ§μ§λ§ μΈμ¬", |
|
"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" |
|
}, |
|
"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_TEMPLATES = { |
|
"λΉμ¦λμ€ μ μμ": { |
|
"description": "ν¬μ μ μΉ, μ¬μ
μ μμ©", |
|
"core_slides": [ |
|
{"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": "μ£Όμ λ§μΌμ€ν€"} |
|
], |
|
"optional_slides": [ |
|
{"title": "μμ₯ κ·λͺ¨", "style": "3D Bubble Chart", "prompt_hint": "μμ₯ κΈ°νμ μ±μ₯μ±"}, |
|
{"title": "κ²½μ λΆμ", "style": "Risk Heat Map", "prompt_hint": "κ²½μμ¬ ν¬μ§μ
λ"}, |
|
{"title": "λΉμ¦λμ€ λͺ¨λΈ", "style": "Value Chain", "prompt_hint": "μμ΅ κ΅¬μ‘°"}, |
|
{"title": "ν μκ°", "style": "Colorful Mind Map", "prompt_hint": "ν΅μ¬ νμκ³Ό μλ"}, |
|
{"title": "μ¬λ¬΄ κ³ν", "style": "KPI Dashboard", "prompt_hint": "μμ λ§€μΆκ³Ό μμ΅"}, |
|
{"title": "μν κ΄λ¦¬", "style": "Risk Heat Map", "prompt_hint": "μ£Όμ 리μ€ν¬μ λμ"}, |
|
{"title": "ννΈλμ", "style": "Business Workflow", "prompt_hint": "μ λ΅μ μ ν΄"}, |
|
{"title": "κΈ°μ μ€ν", "style": "Flowchart", "prompt_hint": "ν΅μ¬ κΈ°μ ꡬ쑰"}, |
|
{"title": "κ³ κ° μ¬λ‘", "style": "Industrial Design", "prompt_hint": "μ±κ³΅ μ¬λ‘"}, |
|
{"title": "μ±μ₯ μ λ΅", "style": "Timeline Ribbon", "prompt_hint": "νμ₯ κ³ν"}, |
|
{"title": "ν¬μ νμ©", "style": "Pyramid/Funnel", "prompt_hint": "μκΈ μ¬μ© κ³ν"}, |
|
{"title": "Exit μ λ΅", "style": "Timeline Ribbon", "prompt_hint": "μΆκ΅¬ μ λ΅"} |
|
] |
|
}, |
|
"μ ν μκ°": { |
|
"description": "μ μ ν λ°μΉ, μλΉμ€ μκ°μ©", |
|
"core_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": "λ°μΉ λ‘λλ§΅"} |
|
], |
|
"optional_slides": [ |
|
{"title": "νκ² κ³ κ°", "style": "Colorful Mind Map", "prompt_hint": "μ£Όμ κ³ κ°μΈ΅"}, |
|
{"title": "κ°κ²© μ μ±
", "style": "Pyramid/Funnel", "prompt_hint": "κ°κ²© μ λ΅"}, |
|
{"title": "κΈ°μ μ°μ", "style": "Industrial Design", "prompt_hint": "ν΅μ¬ κΈ°μ "}, |
|
{"title": "μ¬μ© μλ리μ€", "style": "Business Workflow", "prompt_hint": "νμ© μ¬λ‘"}, |
|
{"title": "κ³ κ° νκΈ°", "style": "KPI Dashboard", "prompt_hint": "μ¬μ©μ νκ°"}, |
|
{"title": "νλ§€ μ±λ", "style": "Value Chain", "prompt_hint": "μ ν΅ μ λ΅"}, |
|
{"title": "λ§μΌν
μ λ΅", "style": "Timeline Ribbon", "prompt_hint": "ν보 κ³ν"}, |
|
{"title": "μ±λ₯ λΉκ΅", "style": "3D Bubble Chart", "prompt_hint": "λ²€μΉλ§ν¬"} |
|
] |
|
}, |
|
"νλ‘μ νΈ λ³΄κ³ ": { |
|
"description": "μ§ν μν©, μ±κ³Ό λ³΄κ³ μ©", |
|
"core_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": "λ€μ λ¨κ³"} |
|
], |
|
"optional_slides": [ |
|
{"title": "μμ° νν©", "style": "Pyramid/Funnel", "prompt_hint": "μμ° μ§ν"}, |
|
{"title": "ν μ±κ³Ό", "style": "3D Bubble Chart", "prompt_hint": "νλ³ κΈ°μ¬λ"}, |
|
{"title": "μ΄μ κ΄λ¦¬", "style": "Risk Heat Map", "prompt_hint": "μ£Όμ μ΄μ"}, |
|
{"title": "κ°μ μ¬ν", "style": "Colorful Mind Map", "prompt_hint": "νλ‘μΈμ€ κ°μ "}, |
|
{"title": "κ΅ν", "style": "Business Workflow", "prompt_hint": "λ°°μ΄ μ "} |
|
] |
|
}, |
|
"μ λ΅ κΈ°ν": { |
|
"description": "μ€μ₯κΈ° μ λ΅, λΉμ μ립μ©", |
|
"core_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 λͺ©ν"} |
|
], |
|
"optional_slides": [ |
|
{"title": "μμ₯ μ λ§", "style": "3D Bubble Chart", "prompt_hint": "λ―Έλ μμ₯"}, |
|
{"title": "νμ λ°©ν₯", "style": "Industrial Design", "prompt_hint": "νμ μ λ΅"}, |
|
{"title": "μ‘°μ§ λ³ν", "style": "Business Workflow", "prompt_hint": "μ‘°μ§ κ°νΈ"}, |
|
{"title": "λμ§νΈ μ ν", "style": "Flowchart", "prompt_hint": "DX μ λ΅"}, |
|
{"title": "μ§μκ°λ₯μ±", "style": "Timeline Ribbon", "prompt_hint": "ESG μ λ΅"} |
|
] |
|
}, |
|
"μ¬μ©μ μ μ": { |
|
"description": "μ§μ ꡬμ±νκΈ°", |
|
"core_slides": [], |
|
"optional_slides": [] |
|
} |
|
} |
|
|
|
def brave_search(query: str) -> List[Dict]: |
|
"""Brave Search APIλ₯Ό μ¬μ©ν μΉ κ²μ""" |
|
if not BRAVE_API_TOKEN: |
|
print("[Brave Search] API ν ν°μ΄ μμ΄ κ²μμ 건λλλλ€.") |
|
return [] |
|
|
|
print(f"[Brave Search] κ²μμ΄: {query}") |
|
|
|
headers = { |
|
"Accept": "application/json", |
|
"X-Subscription-Token": BRAVE_API_TOKEN |
|
} |
|
|
|
params = { |
|
"q": query, |
|
"count": 5 |
|
} |
|
|
|
try: |
|
response = requests.get( |
|
"https://api.search.brave.com/res/v1/web/search", |
|
headers=headers, |
|
params=params, |
|
timeout=10 |
|
) |
|
|
|
if response.status_code == 200: |
|
data = response.json() |
|
results = [] |
|
for item in data.get("web", {}).get("results", [])[:3]: |
|
results.append({ |
|
"title": item.get("title", ""), |
|
"description": item.get("description", ""), |
|
"url": item.get("url", "") |
|
}) |
|
print(f"[Brave Search] {len(results)}κ° κ²°κ³Ό νλ") |
|
return results |
|
else: |
|
print(f"[Brave Search] μ€λ₯: {response.status_code}") |
|
return [] |
|
except Exception as e: |
|
print(f"[Brave Search] μμΈ: {str(e)}") |
|
return [] |
|
|
|
def read_uploaded_file(file_path: str) -> str: |
|
"""μ
λ‘λλ νμΌ μ½κΈ° (PDF, CSV, TXT)""" |
|
print(f"[νμΌ μ½κΈ°] {file_path}") |
|
|
|
try: |
|
|
|
ext = os.path.splitext(file_path)[1].lower() |
|
|
|
if ext == '.pdf': |
|
|
|
with open(file_path, 'rb') as file: |
|
pdf_reader = PyPDF2.PdfReader(file) |
|
text = "" |
|
for page in pdf_reader.pages: |
|
text += page.extract_text() + "\n" |
|
return text[:5000] |
|
|
|
elif ext == '.csv': |
|
|
|
|
|
with open(file_path, 'rb') as file: |
|
raw_data = file.read() |
|
result = chardet.detect(raw_data) |
|
encoding = result['encoding'] or 'utf-8' |
|
|
|
df = pd.read_csv(file_path, encoding=encoding) |
|
return f"CSV λ°μ΄ν°:\n{df.head(20).to_string()}\n\nμμ½: {len(df)} ν, {len(df.columns)} μ΄" |
|
|
|
elif ext in ['.txt', '.text']: |
|
|
|
with open(file_path, 'rb') as file: |
|
raw_data = file.read() |
|
result = chardet.detect(raw_data) |
|
encoding = result['encoding'] or 'utf-8' |
|
|
|
with open(file_path, 'r', encoding=encoding) as file: |
|
return file.read()[:5000] |
|
else: |
|
return "μ§μνμ§ μλ νμΌ νμμ
λλ€." |
|
|
|
except Exception as e: |
|
return f"νμΌ μ½κΈ° μ€λ₯: {str(e)}" |
|
|
|
def generate_presentation_notes(topic: str, slide_title: str, content: Dict) -> 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 coach who creates natural, conversational speaker notes. |
|
|
|
Create speaker notes that: |
|
1. Sound natural and conversational, as if speaking to an audience |
|
2. Include transitions and engagement phrases |
|
3. Reference the slide content but expand with additional context |
|
4. Use a warm, professional tone |
|
5. Be 100-150 words long |
|
6. Include pauses and emphasis markers where appropriate |
|
|
|
Format: |
|
- Use conversational language |
|
- Include transition phrases |
|
- Add engagement questions or comments |
|
- Keep it professional but friendly""" |
|
|
|
bullet_text = "\n".join(content.get("bullet_points", [])) |
|
user_message = f"""Topic: {topic} |
|
Slide Title: {slide_title} |
|
Subtitle: {content.get('subtitle', '')} |
|
Key Points: |
|
{bullet_text} |
|
|
|
Create natural speaker notes for presenting this slide.""" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"temperature": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
notes = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
if any(ord('κ°') <= ord(char) <= ord('ν£') for char in topic): |
|
notes = translate_content_to_korean(notes) |
|
|
|
return notes |
|
else: |
|
return "μ΄ μ¬λΌμ΄λμμλ ν΅μ¬ λ΄μ©μ μ€λͺ
ν΄ μ£ΌμΈμ." |
|
except Exception as e: |
|
print(f"[λ°νμ λ
ΈνΈ] μ€λ₯: {str(e)}") |
|
return "μ΄ μ¬λΌμ΄λμμλ ν΅μ¬ λ΄μ©μ μ€λͺ
ν΄ μ£ΌμΈμ." |
|
|
|
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]: |
|
"""κ° μ¬λΌμ΄λμ ν
μ€νΈ λ΄μ© μμ±""" |
|
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}""" |
|
|
|
|
|
if uploaded_content: |
|
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}" |
|
|
|
|
|
if web_search_results: |
|
search_context = "\n\nWeb Search Results:\n" |
|
for result in web_search_results[:3]: |
|
search_context += f"- {result['title']}: {result['description']}\n" |
|
user_message += search_context |
|
|
|
user_message += "\n\nCreate 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() |
|
|
|
|
|
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] |
|
} |
|
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, uploaded_content: 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}" |
|
if uploaded_content: |
|
user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}" |
|
|
|
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 μμ±""" |
|
|
|
|
|
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 = f""" |
|
<div class="slide-container" style=" |
|
width: 100%; |
|
max-width: 1200px; |
|
margin: 20px auto; |
|
background: white; |
|
border-radius: 8px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
overflow: hidden; |
|
"> |
|
<div class="slide-header" style=" |
|
background: #2c3e50; |
|
color: white; |
|
padding: 15px 30px; |
|
font-size: 18px; |
|
font-weight: bold; |
|
"> |
|
μ¬λΌμ΄λ {slide_data.get('slide_number', '')}: {slide_data.get('title', '')} |
|
</div> |
|
|
|
<div class="slide-content" style=" |
|
display: flex; |
|
height: 0; |
|
padding-bottom: 56.25%; /* 16:9 λΉμ¨ */ |
|
position: relative; |
|
"> |
|
<div class="slide-inner" style=" |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
display: flex; |
|
"> |
|
""" |
|
|
|
|
|
if slide_data.get('title') in ['νμ§', 'Thank You']: |
|
html += f""" |
|
<!-- μ 체 νλ©΄ μ΄λ―Έμ§ --> |
|
<div style=" |
|
width: 100%; |
|
height: 100%; |
|
position: relative; |
|
background: #e9ecef; |
|
"> |
|
""" |
|
|
|
if img_base64: |
|
html += f""" |
|
<img src="data:image/png;base64,{img_base64}" style=" |
|
width: 100%; |
|
height: 100%; |
|
object-fit: cover; |
|
" alt="Slide Image"> |
|
<div style=" |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
text-align: center; |
|
color: white; |
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); |
|
"> |
|
<h1 style="font-size: 48px; margin-bottom: 20px;">{slide_data.get('topic', '')}</h1> |
|
<h2 style="font-size: 24px;">{subtitle}</h2> |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
""" |
|
else: |
|
|
|
html += f""" |
|
<!-- ν
μ€νΈ μμ (μ’μΈ‘) --> |
|
<div class="text-area" style=" |
|
flex: 1; |
|
padding: 40px; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
background: #f8f9fa; |
|
"> |
|
<h2 style=" |
|
color: #2c3e50; |
|
font-size: 28px; |
|
margin-bottom: 30px; |
|
font-weight: 600; |
|
">{subtitle}</h2> |
|
|
|
<ul style=" |
|
list-style: none; |
|
padding: 0; |
|
margin: 0; |
|
"> |
|
""" |
|
|
|
for point in bullet_points: |
|
html += f""" |
|
<li style=" |
|
margin-bottom: 15px; |
|
padding-left: 25px; |
|
position: relative; |
|
color: #34495e; |
|
font-size: 16px; |
|
line-height: 1.6; |
|
"> |
|
<span style=" |
|
position: absolute; |
|
left: 0; |
|
color: #3498db; |
|
">βΆ</span> |
|
{point.replace('β’', '').strip()} |
|
</li> |
|
""" |
|
|
|
html += f""" |
|
</ul> |
|
</div> |
|
|
|
<!-- μ΄λ―Έμ§ μμ (μ°μΈ‘) --> |
|
<div class="image-area" style=" |
|
flex: 1; |
|
background: #e9ecef; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 20px; |
|
"> |
|
""" |
|
|
|
if img_base64: |
|
html += f""" |
|
<img src="data:image/png;base64,{img_base64}" style=" |
|
max-width: 100%; |
|
max-height: 100%; |
|
object-fit: contain; |
|
border-radius: 4px; |
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|
" alt="Slide Image"> |
|
""" |
|
else: |
|
html += """ |
|
<div style=" |
|
color: #6c757d; |
|
text-align: center; |
|
"> |
|
<div style="font-size: 48px;">πΌοΈ</div> |
|
<p>μ΄λ―Έμ§ μμ± μ€...</p> |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
</div> |
|
</div> |
|
""" |
|
|
|
return html |
|
|
|
def create_pptx_file(results: List[Dict], topic: str, template_name: str) -> str: |
|
"""μμ±λ κ²°κ³Όλ₯Ό PPTX νμΌλ‘ λ³ν (λ°νμ λ
ΈνΈ ν¬ν¨)""" |
|
print("[PPTX] νμΌ μμ± μμ...") |
|
|
|
|
|
prs = Presentation() |
|
prs.slide_width = Inches(16) |
|
prs.slide_height = Inches(9) |
|
|
|
|
|
for i, result in enumerate(results): |
|
if not result.get("success", False): |
|
continue |
|
|
|
slide_data = result.get("slide_data", {}) |
|
|
|
|
|
blank_layout = prs.slide_layouts[6] |
|
slide = prs.slides.add_slide(blank_layout) |
|
|
|
|
|
if slide_data.get('title') == 'νμ§': |
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
0, 0, |
|
width=prs.slide_width, |
|
height=prs.slide_height |
|
) |
|
|
|
slide.shapes._spTree.remove(pic._element) |
|
slide.shapes._spTree.insert(2, pic._element) |
|
except Exception as e: |
|
print(f"[PPTX] νμ§ μ΄λ―Έμ§ μΆκ° μ€ν¨: {str(e)}") |
|
|
|
|
|
title_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(3), |
|
Inches(12), Inches(3) |
|
) |
|
title_frame = title_box.text_frame |
|
title_frame.text = topic |
|
title_para = title_frame.paragraphs[0] |
|
title_para.font.size = Pt(48) |
|
title_para.font.bold = True |
|
title_para.font.color.rgb = RGBColor(255, 255, 255) |
|
title_para.alignment = PP_ALIGN.CENTER |
|
|
|
|
|
subtitle_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(6), |
|
Inches(12), Inches(2) |
|
) |
|
subtitle_frame = subtitle_box.text_frame |
|
subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI νλ μ ν
μ΄μ
') |
|
subtitle_para = subtitle_frame.paragraphs[0] |
|
subtitle_para.font.size = Pt(24) |
|
subtitle_para.font.color.rgb = RGBColor(255, 255, 255) |
|
subtitle_para.alignment = PP_ALIGN.CENTER |
|
|
|
|
|
elif slide_data.get('title') == 'Thank You': |
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
0, 0, |
|
width=prs.slide_width, |
|
height=prs.slide_height |
|
) |
|
|
|
slide.shapes._spTree.remove(pic._element) |
|
slide.shapes._spTree.insert(2, pic._element) |
|
except Exception as e: |
|
print(f"[PPTX] Thank You μ΄λ―Έμ§ μΆκ° μ€ν¨: {str(e)}") |
|
|
|
|
|
thanks_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(3.5), |
|
Inches(12), Inches(2) |
|
) |
|
thanks_frame = thanks_box.text_frame |
|
thanks_frame.text = "Thank You" |
|
thanks_para = thanks_frame.paragraphs[0] |
|
thanks_para.font.size = Pt(60) |
|
thanks_para.font.bold = True |
|
thanks_para.font.color.rgb = RGBColor(255, 255, 255) |
|
thanks_para.alignment = PP_ALIGN.CENTER |
|
|
|
|
|
else: |
|
|
|
title_box = slide.shapes.add_textbox( |
|
Inches(0.5), Inches(0.3), |
|
Inches(15), Inches(0.8) |
|
) |
|
title_frame = title_box.text_frame |
|
title_frame.text = f"{slide_data.get('title', '')}" |
|
title_para = title_frame.paragraphs[0] |
|
title_para.font.size = Pt(28) |
|
title_para.font.bold = True |
|
title_para.font.color.rgb = RGBColor(44, 62, 80) |
|
|
|
|
|
text_box = slide.shapes.add_textbox( |
|
Inches(0.5), Inches(1.5), |
|
Inches(7.5), Inches(6.5) |
|
) |
|
text_frame = text_box.text_frame |
|
text_frame.word_wrap = True |
|
|
|
|
|
subtitle_para = text_frame.paragraphs[0] |
|
subtitle_para.text = slide_data.get('subtitle', '') |
|
subtitle_para.font.size = Pt(20) |
|
subtitle_para.font.bold = True |
|
subtitle_para.font.color.rgb = RGBColor(52, 73, 94) |
|
subtitle_para.space_after = Pt(20) |
|
|
|
|
|
bullet_points = slide_data.get('bullet_points', []) |
|
for point in bullet_points: |
|
p = text_frame.add_paragraph() |
|
p.text = point.replace('β’', '').strip() |
|
p.font.size = Pt(16) |
|
p.font.color.rgb = RGBColor(52, 73, 94) |
|
p.level = 0 |
|
p.space_after = Pt(12) |
|
p.line_spacing = 1.5 |
|
|
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
Inches(8.5), Inches(1.5), |
|
width=Inches(7), height=Inches(6) |
|
) |
|
|
|
pic.line.color.rgb = RGBColor(189, 195, 199) |
|
pic.line.width = Pt(1) |
|
|
|
except Exception as e: |
|
print(f"[PPTX] μ΄λ―Έμ§ μΆκ° μ€ν¨: {str(e)}") |
|
|
|
|
|
page_num = slide.shapes.add_textbox( |
|
Inches(15), Inches(8.5), |
|
Inches(1), Inches(0.5) |
|
) |
|
page_frame = page_num.text_frame |
|
page_frame.text = str(i + 1) |
|
page_para = page_frame.paragraphs[0] |
|
page_para.font.size = Pt(12) |
|
page_para.font.color.rgb = RGBColor(127, 140, 141) |
|
page_para.alignment = PP_ALIGN.RIGHT |
|
|
|
|
|
notes_slide = slide.notes_slide |
|
notes_slide.notes_text_frame.text = slide_data.get('speaker_notes', '') |
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
filename = f"presentation_{timestamp}.pptx" |
|
filepath = os.path.join("/tmp", filename) |
|
prs.save(filepath) |
|
|
|
print(f"[PPTX] νμΌ μμ± μλ£: {filename}") |
|
return filepath |
|
|
|
def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> List[Dict]: |
|
"""μ νλ μ¬λΌμ΄λ μμ λ°λΌ λμ μΌλ‘ μ¬λΌμ΄λ ꡬμ±""" |
|
core_slides = template.get("core_slides", []) |
|
optional_slides = template.get("optional_slides", []) |
|
|
|
|
|
content_slide_count = slide_count |
|
|
|
|
|
if len(core_slides) > content_slide_count: |
|
selected_slides = core_slides[:content_slide_count] |
|
else: |
|
|
|
selected_slides = core_slides.copy() |
|
remaining = content_slide_count - len(core_slides) |
|
|
|
if remaining > 0 and optional_slides: |
|
|
|
additional = optional_slides[:remaining] |
|
selected_slides.extend(additional) |
|
|
|
|
|
slides = [{"title": "νμ§", "style": "Title Slide (Hero)", "prompt_hint": "νλ μ ν
μ΄μ
νμ§"}] |
|
|
|
|
|
slides.extend(selected_slides) |
|
|
|
|
|
slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "κ°μ¬ μΈμ¬"}) |
|
|
|
return slides |
|
|
|
def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict], |
|
slide_count: int, seed: int, uploaded_file: str, |
|
use_web_search: bool, progress=gr.Progress()): |
|
"""PPT μ΄λ―Έμ§μ ν
μ€νΈ λ΄μ©μ ν¨κ» μμ± (λ°νμ λ
ΈνΈ ν¬ν¨)""" |
|
results = [] |
|
preview_html = "" |
|
|
|
|
|
uploaded_content = "" |
|
if uploaded_file: |
|
uploaded_content = read_uploaded_file(uploaded_file.name) |
|
print(f"[νμΌ μ
λ‘λ] λ΄μ© κΈΈμ΄: {len(uploaded_content)}μ") |
|
|
|
|
|
if template_name == "μ¬μ©μ μ μ" and custom_slides: |
|
slides = [{"title": "νμ§", "style": "Title Slide (Hero)", "prompt_hint": "νλ μ ν
μ΄μ
νμ§"}] |
|
slides.extend(custom_slides) |
|
slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "κ°μ¬ μΈμ¬"}) |
|
else: |
|
template = PPT_TEMPLATES[template_name] |
|
slides = generate_dynamic_slides(topic, template, slide_count) |
|
|
|
if not slides: |
|
yield "", "μ¬λΌμ΄λκ° μ μλμ§ μμμ΅λλ€.", None |
|
return |
|
|
|
total_slides = len(slides) |
|
print(f"\n[PPT μμ±] μμ - μ΄ {total_slides}κ° μ¬λΌμ΄λ (νμ§ + λ³Έλ¬Έ {slide_count} + Thank You)") |
|
print(f"[PPT μμ±] μ£Όμ : {topic}") |
|
print(f"[PPT μμ±] ν
νλ¦Ώ: {template_name}") |
|
print(f"[PPT μμ±] μΉ κ²μ: {'μ¬μ©' if use_web_search else 'λ―Έμ¬μ©'}") |
|
|
|
|
|
web_search_results = [] |
|
if use_web_search and BRAVE_API_TOKEN: |
|
progress(0.05, "μΉ κ²μ μ€...") |
|
web_search_results = brave_search(topic) |
|
|
|
|
|
preview_html = """ |
|
<style> |
|
.slides-container { |
|
width: 100%; |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
} |
|
</style> |
|
<div class="slides-container"> |
|
""" |
|
|
|
|
|
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, |
|
uploaded_content, web_search_results |
|
) |
|
|
|
|
|
speaker_notes = generate_presentation_notes(topic, slide['title'], content) |
|
|
|
|
|
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, uploaded_content |
|
) |
|
|
|
|
|
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"], |
|
"speaker_notes": speaker_notes, |
|
"topic": topic |
|
} |
|
|
|
|
|
preview_html += create_slide_preview_html(slide_data) |
|
|
|
|
|
yield preview_html + "</div>", f"### π {slide_info} μμ± μ€...", None |
|
|
|
results.append({ |
|
"slide_data": slide_data, |
|
"success": img is not None |
|
}) |
|
|
|
|
|
progress(0.95, "PPTX νμΌ μμ± μ€...") |
|
pptx_path = None |
|
try: |
|
pptx_path = create_pptx_file(results, topic, template_name) |
|
except Exception as e: |
|
print(f"[PPTX] νμΌ μμ± μ€λ₯: {str(e)}") |
|
|
|
|
|
preview_html += "</div>" |
|
progress(1.0, "μλ£!") |
|
successful = sum(1 for r in results if r["success"]) |
|
final_status = f"### π μμ± μλ£! μ΄ {total_slides}κ° μ¬λΌμ΄λ μ€ {successful}κ° μ±κ³΅" |
|
|
|
if pptx_path: |
|
final_status += f"\n\n### π₯ PPTX νμΌμ΄ μ€λΉλμμ΅λλ€! μλμμ λ€μ΄λ‘λνμΈμ." |
|
final_status += f"\n\nπ‘ **λ°νμ λ
ΈνΈκ° κ° μ¬λΌμ΄λμ ν¬ν¨λμ΄ μμ΅λλ€!**" |
|
|
|
yield preview_html, final_status, pptx_path |
|
|
|
def create_custom_slides_ui(): |
|
"""μ¬μ©μ μ μ μ¬λΌμ΄λ κ΅¬μ± UI (3-20μ₯)""" |
|
slides = [] |
|
for i in range(20): |
|
with gr.Row(visible=(i < 3)): |
|
with gr.Column(scale=2): |
|
title = gr.Textbox( |
|
label=f"μ¬λΌμ΄λ {i+1} μ λͺ©", |
|
placeholder="μ: νν© λΆμ, μ루μ
, λ‘λλ§΅...", |
|
) |
|
with gr.Column(scale=3): |
|
style = gr.Dropdown( |
|
choices=list(STYLE_TEMPLATES.keys()), |
|
label=f"μ€νμΌ μ ν", |
|
value="Colorful Mind Map" |
|
) |
|
with gr.Column(scale=3): |
|
hint = gr.Textbox( |
|
label=f"ν둬ννΈ ννΈ", |
|
placeholder="μ΄ μ¬λΌμ΄λμμ νννκ³ μΆμ λ΄μ©" |
|
) |
|
slides.append({"title": title, "style": style, "hint": hint, "row": gr.Row}) |
|
return slides |
|
|
|
|
|
with gr.Blocks(title="PPT μ΄λ―Έμ§ μμ±κΈ°", theme=gr.themes.Soft(), css=""" |
|
.preview-container { max-width: 1400px; margin: 0 auto; } |
|
""") as demo: |
|
gr.Markdown(""" |
|
# π― AI κΈ°λ° PPT ν΅ν© μμ±κΈ° (μ
κ·Έλ μ΄λ λ²μ ) |
|
|
|
### ν
μ€νΈμ μ΄λ―Έμ§κ° μλ²½νκ² μ‘°νλ νλ μ ν
μ΄μ
μ μλμΌλ‘ μμ±νκ³ λ€μ΄λ‘λνμΈμ! |
|
|
|
#### π μλ‘μ΄ κΈ°λ₯: |
|
- π **νμ§μ Thank You μ¬λΌμ΄λ** μλ μΆκ° |
|
- π **λ°νμ λ
ΈνΈ** μλ μμ± (ꡬμ΄μ²΄) |
|
- π **νμΌ μ
λ‘λ** μ§μ (PDF/CSV/TXT) |
|
- π **μΉ κ²μ** κΈ°λ₯ (Brave Search) |
|
- ποΈ **μ¬λΌμ΄λ μ μ‘°μ ** (6-20μ₯) |
|
""") |
|
|
|
|
|
token_status = [] |
|
if not REPLICATE_API_TOKEN: |
|
token_status.append("β οΈ RAPI_TOKEN νκ²½ λ³μκ° μ€μ λμ§ μμμ΅λλ€.") |
|
if not FRIENDLI_TOKEN: |
|
token_status.append("β οΈ FRIENDLI_TOKEN νκ²½ λ³μκ° μ€μ λμ§ μμμ΅λλ€.") |
|
if not BRAVE_API_TOKEN: |
|
token_status.append("βΉοΈ BAPI_TOKENμ΄ μμ΄ μΉ κ²μ κΈ°λ₯μ΄ λΉνμ±νλ©λλ€.") |
|
|
|
if token_status: |
|
gr.Markdown("\n".join(token_status)) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
topic_input = gr.Textbox( |
|
label="νλ μ ν
μ΄μ
μ£Όμ ", |
|
placeholder="μ: AI μ€ννΈμ
ν¬μ μ μΉ, μ μ ν λ°μΉ, λμ§νΈ μ ν μ λ΅", |
|
lines=2 |
|
) |
|
|
|
|
|
template_select = gr.Dropdown( |
|
choices=list(PPT_TEMPLATES.keys()), |
|
label="PPT ν
νλ¦Ώ μ ν", |
|
value="λΉμ¦λμ€ μ μμ", |
|
info="λͺ©μ μ λ§λ ν
νλ¦Ώμ μ ννμΈμ" |
|
) |
|
|
|
|
|
slide_count = gr.Slider( |
|
minimum=6, |
|
maximum=20, |
|
value=8, |
|
step=1, |
|
label="λ³Έλ¬Έ μ¬λΌμ΄λ μ (νμ§μ Thank You μ μΈ)", |
|
info="μμ±ν λ³Έλ¬Έ μ¬λΌμ΄λ μλ₯Ό μ ννμΈμ" |
|
) |
|
|
|
|
|
file_upload = gr.File( |
|
label="μ°Έκ³ μλ£ μ
λ‘λ (μ ν)", |
|
file_types=[".pdf", ".csv", ".txt"], |
|
type="filepath" |
|
) |
|
|
|
|
|
use_web_search = gr.Checkbox( |
|
label="μΉ κ²μ μ¬μ©", |
|
value=False, |
|
info="Brave Searchλ₯Ό μ¬μ©νμ¬ μ΅μ μ 보 λ°μ" |
|
) |
|
|
|
|
|
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.Row(): |
|
download_file = gr.File( |
|
label="π₯ μμ±λ PPTX νμΌ λ€μ΄λ‘λ", |
|
visible=True, |
|
elem_id="download-file" |
|
) |
|
|
|
|
|
with gr.Accordion("π μ¬μ©μ μ μ μ¬λΌμ΄λ ꡬμ±", open=False) as custom_accordion: |
|
gr.Markdown("ν
νλ¦Ώμ μ¬μ©νμ§ μκ³ μ§μ μ¬λΌμ΄λλ₯Ό ꡬμ±νμΈμ. (3-20μ₯)") |
|
|
|
|
|
custom_slide_count = gr.Slider( |
|
minimum=3, |
|
maximum=20, |
|
value=3, |
|
step=1, |
|
label="μ¬μ©μ μ μ μ¬λΌμ΄λ μ" |
|
) |
|
|
|
custom_slides_components = create_custom_slides_ui() |
|
|
|
|
|
status_output = gr.Markdown( |
|
value="### π ν
νλ¦Ώμ μ ννκ³ μμ± λ²νΌμ ν΄λ¦νμΈμ!" |
|
) |
|
|
|
|
|
preview_output = gr.HTML( |
|
label="PPT ν리뷰 (16:9)", |
|
elem_classes="preview-container" |
|
) |
|
|
|
|
|
def update_template_info(template_name, slide_count): |
|
if template_name in PPT_TEMPLATES: |
|
template = PPT_TEMPLATES[template_name] |
|
info = f"**{template['description']}**\n\n" |
|
|
|
|
|
slides = generate_dynamic_slides("", template, slide_count) |
|
info += f"μμ±λ μ¬λΌμ΄λ ({len(slides)}μ₯):\n" |
|
for i, slide in enumerate(slides): |
|
style_info = STYLE_TEMPLATES.get(slide['style'], {}) |
|
info += f"{i+1}. {slide['title']} - {style_info.get('use_case', '')}\n" |
|
|
|
return info |
|
return "" |
|
|
|
def update_custom_slides_visibility(count): |
|
"""μ¬μ©μ μ μ μ¬λΌμ΄λ μμ λ°λΌ UI νμ/μ¨κΉ""" |
|
updates = [] |
|
for i in range(20): |
|
updates.extend([ |
|
gr.update(visible=(i < count)), |
|
gr.update(visible=(i < count)), |
|
gr.update(visible=(i < count)) |
|
]) |
|
return updates |
|
|
|
def generate_ppt_handler(topic, template_name, slide_count, seed, file_upload, |
|
use_web_search, custom_slide_count, progress=gr.Progress(), |
|
*custom_inputs): |
|
if not topic.strip(): |
|
yield "", "β μ£Όμ λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.", None |
|
return |
|
|
|
|
|
custom_slides = [] |
|
if template_name == "μ¬μ©μ μ μ": |
|
for i in range(0, custom_slide_count * 3, 3): |
|
if i < len(custom_inputs): |
|
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 preview, status, pptx_file in generate_ppt_with_content( |
|
topic, template_name, custom_slides, slide_count, seed, |
|
file_upload, use_web_search, progress |
|
): |
|
yield preview, status, pptx_file |
|
|
|
|
|
template_select.change( |
|
fn=update_template_info, |
|
inputs=[template_select, slide_count], |
|
outputs=[template_info] |
|
) |
|
|
|
slide_count.change( |
|
fn=update_template_info, |
|
inputs=[template_select, slide_count], |
|
outputs=[template_info] |
|
) |
|
|
|
custom_slide_count.change( |
|
fn=update_custom_slides_visibility, |
|
inputs=[custom_slide_count], |
|
outputs=[comp for slide in custom_slides_components for comp in [slide["title"], slide["style"], slide["hint"]]] |
|
) |
|
|
|
|
|
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, slide_count, seed_input, |
|
file_upload, use_web_search, custom_slide_count] + all_custom_inputs, |
|
outputs=[preview_output, status_output, download_file] |
|
) |
|
|
|
|
|
demo.load( |
|
fn=update_template_info, |
|
inputs=[template_select, slide_count], |
|
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 νμΈλ¨") |
|
|
|
if not BRAVE_API_TOKEN: |
|
print("βΉοΈ BAPI_TOKENμ΄ μμ΄ μΉ κ²μ κΈ°λ₯μ΄ λΉνμ±νλ©λλ€.") |
|
else: |
|
print("β
BAPI_TOKEN νμΈλ¨") |
|
|
|
print("="*50 + "\n") |
|
|
|
demo.launch() |