GINI-Deck / app.py
ginipick's picture
Update app.py
8d9cd4b verified
raw
history blame
96.6 kB
import replicate
import requests
import os
import json
from io import BytesIO
from PIL import Image
from typing import List, Tuple, Dict
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
from pptx.enum.shapes import MSO_SHAPE
import PyPDF2
import pandas as pd
import chardet
import gradio as gr
# ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ๊ธฐ ์ž„ํฌํŠธ ์ถ”๊ฐ€
try:
from process_flow_generator import generate_process_flow_for_ppt
PROCESS_FLOW_AVAILABLE = True
except ImportError:
PROCESS_FLOW_AVAILABLE = False
print("[๊ฒฝ๊ณ ] process_flow_generator๋ฅผ ์ž„ํฌํŠธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
# ์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ ์ •์˜
EXAMPLE_TOPICS = {
"๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ": "AI ๊ธฐ๋ฐ˜ ๊ณ ๊ฐ ์„œ๋น„์Šค ์ž๋™ํ™” ํ”Œ๋žซํผ ํˆฌ์ž ์ œ์•ˆ",
"์ œํ’ˆ ์†Œ๊ฐœ": "์Šค๋งˆํŠธ ํ™ˆ IoT ๋ณด์•ˆ ์‹œ์Šคํ…œ ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ",
"ํ”„๋กœ์ ํŠธ ๋ณด๊ณ ": "๋””์ง€ํ„ธ ์ „ํ™˜ ํ”„๋กœ์ ํŠธ 3๋ถ„๊ธฐ ์„ฑ๊ณผ ๋ณด๊ณ ",
"์ „๋žต ๊ธฐํš": "2025๋…„ ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ง„์ถœ ์ „๋žต ์ˆ˜๋ฆฝ"
}
# ์˜ค๋””์–ธ์Šค ํƒ€์ž… ์ •์˜
AUDIENCE_TYPES = {
"๊ฒฝ์˜์ง„/์ž„์›": {
"description": "C-level, ์˜์‚ฌ๊ฒฐ์ •๊ถŒ์ž",
"tone": "์ „๋žต์ , ๊ฒฐ๊ณผ ์ค‘์‹ฌ, ROI ๊ฐ•์กฐ",
"focus": "๋น„์ฆˆ๋‹ˆ์Šค ๊ฐ€์น˜, ํˆฌ์ž ์ˆ˜์ต๋ฅ , ์ „๋žต์  ์˜ํ–ฅ"
},
"ํˆฌ์ž์ž": {
"description": "VC, ์—”์ คํˆฌ์ž์ž, ๊ธฐ๊ด€ํˆฌ์ž์ž",
"tone": "์ˆ˜์น˜ ๊ธฐ๋ฐ˜, ์„ฑ์žฅ ๊ฐ€๋Šฅ์„ฑ, ์‹œ์žฅ ๊ธฐํšŒ",
"focus": "์‹œ์žฅ ๊ทœ๋ชจ, ์„ฑ์žฅ๋ฅ , ๊ฒฝ์Ÿ์šฐ์œ„, Exit ์ „๋žต"
},
"๊ธฐ์ˆ ํŒ€": {
"description": "๊ฐœ๋ฐœ์ž, ์—”์ง€๋‹ˆ์–ด, IT ์ „๋ฌธ๊ฐ€",
"tone": "๊ธฐ์ˆ ์ , ๊ตฌ์ฒด์ , ์‹ค์šฉ์ ",
"focus": "๊ธฐ์ˆ  ์Šคํƒ, ์•„ํ‚คํ…์ฒ˜, ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ์„ฑ๋Šฅ"
},
"์ผ๋ฐ˜ ์ง์›": {
"description": "์‹ค๋ฌด์ž, ํŒ€์›",
"tone": "์นœ๊ทผํ•œ, ์‹ค๋ฌด์ , ํ˜‘์—… ์ค‘์‹ฌ",
"focus": "์‹คํ–‰ ๊ณ„ํš, ์—ญํ• , ํ”„๋กœ์„ธ์Šค, ํ˜‘์—… ๋ฐฉ์•ˆ"
},
"๊ณ ๊ฐ/ํŒŒํŠธ๋„ˆ": {
"description": "B2B ๊ณ ๊ฐ, ๋น„์ฆˆ๋‹ˆ์Šค ํŒŒํŠธ๋„ˆ",
"tone": "์‹ ๋ขฐ๊ฐ, ์ „๋ฌธ์ , ํ˜œํƒ ์ค‘์‹ฌ",
"focus": "๊ณ ๊ฐ ๊ฐ€์น˜, ํ˜œํƒ, ์‚ฌ๋ก€, ์ง€์› ์ฒด๊ณ„"
},
"์ผ๋ฐ˜ ๋Œ€์ค‘": {
"description": "B2C ๊ณ ๊ฐ, ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž",
"tone": "์‰ฝ๊ณ  ์นœ๊ทผํ•œ, ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด",
"focus": "์‚ฌ์šฉ ํŽธ์˜์„ฑ, ํ˜œํƒ, ๊ฐ€๊ฒฉ, ์ฐจ๋ณ„์ "
}
}
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
# ์ „์—ญ ๋ณ€์ˆ˜๋กœ ํ˜„์žฌ ์Šฌ๋ผ์ด๋“œ ๋ฐ์ดํ„ฐ ์ €์žฅ
current_slides_data = []
current_topic = ""
current_template = ""
current_theme = ""
uploaded_content = "" # ์ „์—ญ ๋ณ€์ˆ˜๋กœ ์ถ”๊ฐ€
# ๋””์ž์ธ ํ…Œ๋งˆ ์ •์˜
DESIGN_THEMES = {
"๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ": {
"name": "Minimal Light",
"description": "๋ฐ๊ณ  ๊นจ๋—ํ•œ ๋ฏธ๋‹ˆ๋ฉ€ ๋””์ž์ธ",
"background": RGBColor(250, 250, 252),
"title_color": RGBColor(33, 37, 41),
"subtitle_color": RGBColor(52, 58, 64),
"text_color": RGBColor(73, 80, 87),
"accent_color": RGBColor(0, 123, 255),
"box_fill": RGBColor(255, 255, 255),
"box_opacity": 0.95,
"shadow": True,
"gradient": False
},
"๋ชจ๋˜ ๊ทธ๋ผ๋””์–ธํŠธ": {
"name": "Modern Gradient",
"description": "๋ถ€๋“œ๋Ÿฌ์šด ๊ทธ๋ผ๋””์–ธํŠธ์™€ ํ˜„๋Œ€์  ๋А๋‚Œ",
"background": RGBColor(245, 247, 250),
"title_color": RGBColor(25, 42, 86),
"subtitle_color": RGBColor(68, 85, 102),
"text_color": RGBColor(85, 102, 119),
"accent_color": RGBColor(103, 58, 183),
"box_fill": RGBColor(249, 250, 251),
"box_opacity": 0.9,
"shadow": True,
"gradient": True
},
"๋‹คํฌ ์—˜๋ ˆ๊ฐ•์Šค": {
"name": "Dark Elegance",
"description": "์„ธ๋ จ๋œ ๋‹คํฌ ๋ชจ๋“œ ๋””์ž์ธ",
"background": RGBColor(25, 25, 35),
"title_color": RGBColor(240, 240, 245),
"subtitle_color": RGBColor(200, 200, 210),
"text_color": RGBColor(170, 170, 180),
"accent_color": RGBColor(0, 188, 212),
"box_fill": RGBColor(35, 35, 45),
"box_opacity": 0.85,
"shadow": False,
"gradient": False
},
"๋„ค์ด์ฒ˜ ๊ทธ๋ฆฐ": {
"name": "Nature Green",
"description": "์ž์—ฐ ์นœํ™”์ ์ธ ๊ทธ๋ฆฐ ํ…Œ๋งˆ",
"background": RGBColor(242, 248, 244),
"title_color": RGBColor(27, 67, 50),
"subtitle_color": RGBColor(45, 106, 79),
"text_color": RGBColor(64, 125, 98),
"accent_color": RGBColor(76, 175, 80),
"box_fill": RGBColor(255, 255, 255),
"box_opacity": 0.92,
"shadow": True,
"gradient": False
},
"์ฝ”ํผ๋ ˆ์ดํŠธ ๋ธ”๋ฃจ": {
"name": "Corporate Blue",
"description": "์ „๋ฌธ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ์Šคํƒ€์ผ",
"background": RGBColor(244, 247, 252),
"title_color": RGBColor(13, 71, 161),
"subtitle_color": RGBColor(25, 118, 210),
"text_color": RGBColor(42, 63, 84),
"accent_color": RGBColor(33, 150, 243),
"box_fill": RGBColor(255, 255, 255),
"box_opacity": 0.95,
"shadow": True,
"gradient": False
}
}
# ์Šคํƒ€์ผ ์ •์˜ (ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ถ”๊ฐ€)
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": "Closing Slide",
"description": "Elegant closing slide design with conclusion",
"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 conclusion 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",
"is_process_flow": True # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์‚ฌ์šฉ ํ‘œ์‹œ
},
"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",
"is_process_flow": True # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์‚ฌ์šฉ ํ‘œ์‹œ
}
}
# PPT ํ…œํ”Œ๋ฆฟ ์ •์˜ (๋™์ ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ • ๊ฐ€๋Šฅ)
# PPT_TEMPLATES ์ˆ˜์ • - ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ๋Š” ํŠน์ • ์Šฌ๋ผ์ด๋“œ์—๋งŒ ์ ์šฉ
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": "Value Chain", "prompt_hint": "์ „๋žต์  ์ œํœด"}, # Business Workflow ๋Œ€์‹  Value Chain
{"title": "๊ธฐ์ˆ  ์Šคํƒ", "style": "Industrial Design", "prompt_hint": "ํ•ต์‹ฌ ๊ธฐ์ˆ  ๊ตฌ์กฐ"}, # Flowchart ๋Œ€์‹ 
{"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": "Mobile App Mockup", "prompt_hint": "ํ™œ์šฉ ์‚ฌ๋ก€"}, # Business Workflow ๋Œ€์‹ 
{"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": "Colorful Mind Map", "prompt_hint": "๋ฐฐ์šด ์ "} # Business Workflow ๋Œ€์‹ 
]
},
"์ „๋žต ๊ธฐํš": {
"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": "Value Chain", "prompt_hint": "์กฐ์ง ๊ฐœํŽธ"}, # Business Workflow ๋Œ€์‹ 
{"title": "๋””์ง€ํ„ธ ์ „ํ™˜", "style": "Industrial Design", "prompt_hint": "DX ์ „๋žต"}, # Flowchart ๋Œ€์‹ 
{"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':
# 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] # ์ตœ๋Œ€ 5000์ž
elif ext == '.csv':
# 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] # ์ตœ๋Œ€ 5000์ž
else:
return "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค."
except Exception as e:
return f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜: {str(e)}"
def generate_presentation_notes(topic: str, slide_title: str, content: Dict, audience_type: str) -> str:
"""๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ƒ์„ฑ (๊ตฌ์–ด์ฒด)"""
print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] {slide_title} ์ƒ์„ฑ ์ค‘...")
# ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
system_prompt = f"""You are a professional presentation coach who creates natural, conversational speaker notes.
The audience for this presentation is: {audience_type} - {audience_info['description']}
Tone: {audience_info['tone']}
Focus areas: {audience_info['focus']}
The slide content uses concise noun-ending style, but your speaker notes should be natural and conversational.
Create speaker notes that:
1. Sound natural and conversational, as if speaking to {audience_type}
2. Expand on the concise bullet points with additional context relevant to this audience
3. Include transitions and engagement phrases appropriate for {audience_type}
4. Use a warm, professional tone suitable for {audience_type}
5. Be 100-150 words long
6. Include pauses and emphasis markers where appropriate
Note: The bullet points may include emojis - incorporate their meaning into your speech.
Format:
- Use conversational language appropriate for {audience_type}
- Include transition phrases
- Add engagement questions or comments that resonate with this audience
- 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 (concise style with emojis):
{bullet_text}
Create natural speaker notes that expand on these concise points for presenting this slide to {audience_type}."""
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_natural(notes)
return notes
else:
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
except Exception as e:
print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
def generate_closing_notes(topic: str, conclusion_phrase: str, audience_type: str) -> str:
"""๋งˆ์ง€๋ง‰ ์Šฌ๋ผ์ด๋“œ์˜ ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ƒ์„ฑ"""
print(f"[๋งˆ๋ฌด๋ฆฌ ๋…ธํŠธ] ์ƒ์„ฑ ์ค‘...")
# ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
system_prompt = f"""You are a professional presentation coach creating closing speaker notes.
The audience for this presentation is: {audience_type} - {audience_info['description']}
Tone: {audience_info['tone']}
Create natural closing remarks that:
1. Thank the {audience_type} audience warmly and appropriately
2. Briefly summarize the key message relevant to {audience_type}
3. Reference the conclusion phrase naturally
4. End with an invitation for questions or next steps appropriate for {audience_type}
5. Be 80-100 words long
6. Sound conversational and warm
7. NO stage directions or parentheses - only spoken words
Write only what the speaker would say out loud."""
user_message = f"""Presentation topic: {topic}
Audience: {audience_type}
Conclusion phrase on screen: {conclusion_phrase}
Create natural closing speaker notes that wrap up the presentation effectively for {audience_type}."""
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_message
}
],
"max_tokens": 200,
"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_natural(notes)
return notes
else:
return "์˜ค๋Š˜ ๋ฐœํ‘œ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ๊ฐ์‚ฌ์˜ ๋ง์”€์„ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ํŽธํ•˜๊ฒŒ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”."
except Exception as e:
print(f"[๋งˆ๋ฌด๋ฆฌ ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
return "์˜ค๋Š˜ ๋ฐœํ‘œ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ๊ฐ์‚ฌ์˜ ๋ง์”€์„ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ํŽธํ•˜๊ฒŒ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”."
def generate_conclusion_phrase(topic: str, audience_type: str) -> str:
"""ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ํ•ต์‹ฌ์„ ๋‹ด์€ ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์ƒ์„ฑ"""
print(f"[๊ฒฐ๋ก  ๋ฌธ๊ตฌ] ์ƒ์„ฑ ์ค‘...")
# ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
system_prompt = f"""You are a professional copywriter creating powerful closing statements for presentations.
The audience is: {audience_type} - {audience_info['description']}
Focus on: {audience_info['focus']}
Create a concise, impactful closing phrase that:
1. Captures the essence of the presentation topic
2. Resonates with {audience_type}
3. Is memorable and inspirational
4. Maximum 5-7 words
5. Uses powerful, action-oriented language appropriate for {audience_type}
6. Leaves a lasting impression
Examples for different audiences:
- For executives: "Excellence Through Strategic Innovation"
- For investors: "Maximum Returns, Minimal Risk"
- For technical teams: "Code Today, Transform Tomorrow"
- For customers: "Your Success, Our Mission"
Output only the phrase, no explanation."""
user_message = f"""Presentation topic: {topic}
Target audience: {audience_type}
Create a powerful closing phrase that encapsulates the main message for {audience_type}."""
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_message
}
],
"max_tokens": 50,
"temperature": 0.9,
"stream": False
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()
phrase = result['choices'][0]['message']['content'].strip()
# ํ•œ๊ธ€ ์ฃผ์ œ์ธ ๊ฒฝ์šฐ ๋ฒˆ์—ญ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
phrase = translate_content_to_korean_concise(phrase)
return phrase
else:
return "ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ๋ฏธ๋ž˜"
except Exception as e:
print(f"[๊ฒฐ๋ก  ๋ฌธ๊ตฌ] ์˜ค๋ฅ˜: {str(e)}")
return "ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ๋ฏธ๋ž˜"
def generate_slide_content(topic: str, slide_title: str, slide_context: str, audience_type: str,
uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
"""๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ํ…์ŠคํŠธ ๋‚ด์šฉ ์ƒ์„ฑ"""
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] {slide_title} ํ…์ŠคํŠธ ์ƒ์„ฑ ์ค‘... (๋Œ€์ƒ: {audience_type})")
# ์˜ค๋””์–ธ์Šค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ˜ ์ง์›"])
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
system_prompt = f"""You are a professional presentation content writer specializing in creating concise, impactful slide content.
Target Audience: {audience_type} - {audience_info['description']}
Tone: {audience_info['tone']}
Focus: {audience_info['focus']}
Your task is to create content specifically tailored for {audience_type}:
1. A compelling subtitle (max 10 words) that resonates with {audience_type}
2. Exactly 5 bullet points with emojis relevant to {audience_type}'s interests
3. Each bullet point should be 8-12 words
4. Use noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ) for Korean or concise fragments for English
5. Content should address {audience_type}'s specific concerns and interests
IMPORTANT Style Guidelines:
- End sentences with nouns or concise phrases
- Avoid long verb endings like "์ž…๋‹ˆ๋‹ค", "์Šต๋‹ˆ๋‹ค"
- Use short endings like "์ž„", "ํ•จ" or noun forms
- Start each bullet with a relevant emoji that {audience_type} would appreciate
- Be extremely concise and impactful
Audience-specific emoji guidelines:
- For executives: ๐Ÿ“Š ๐ŸŽฏ ๐Ÿ’ฐ ๐Ÿ† ๐Ÿš€ ๐Ÿ“ˆ ๐Ÿ” ๐Ÿ’ก
- For investors: ๐Ÿ’ฐ ๐Ÿ“ˆ ๐Ÿ’Ž ๐Ÿฆ ๐Ÿ’ธ ๐Ÿ“Š ๐Ÿš€ ๐Ÿ”’
- For technical teams: ๐Ÿ”ง ๐Ÿ’ป ๐Ÿ› ๏ธ โš™๏ธ ๐Ÿ” ๐ŸŒ ๐Ÿ“ฑ ๐Ÿค–
- For general staff: ๐Ÿค ๐Ÿ’ก ๐Ÿ“‹ โœ… ๐ŸŽฏ ๐ŸŒŸ ๐Ÿ“… ๐Ÿ’ช
- For customers: โญ ๐ŸŽ ๐Ÿ’ ๐Ÿ›ก๏ธ ๐ŸŒŸ โœจ ๐Ÿ… ๐Ÿ‘
- For general public: ๐Ÿ˜Š ๐Ÿ  ๐ŸŒ โค๏ธ ๐ŸŽ‰ ๐ŸŒˆ โœจ ๐ŸŽฏ
Output format (EXACTLY FOLLOW THIS FORMAT):
Subtitle: [subtitle here]
- ๐ŸŽฏ [Point 1 - tailored for {audience_type}]
- ๐Ÿ“Š [Point 2 - tailored for {audience_type}]
- ๐Ÿ’ก [Point 3 - tailored for {audience_type}]
- ๐Ÿš€ [Point 4 - tailored for {audience_type}]
- โšก [Point 5 - tailored for {audience_type}]"""
user_message = f"""Topic: {topic}
Slide Title: {slide_title}
Context: {slide_context}
Target Audience: {audience_type}"""
# ์—…๋กœ๋“œ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
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 += f"\n\nCreate compelling content for this presentation slide specifically tailored for {audience_type}. Remember to use emojis and concise noun-ending style."
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()
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] LLM ์‘๋‹ต:\n{content}")
# Parse content - ๋” ๊ฐ•๊ฑดํ•œ ํŒŒ์‹ฑ
lines = content.split('\n')
subtitle = ""
bullet_points = []
for line in lines:
line = line.strip()
if not line:
continue
# Subtitle ํŒŒ์‹ฑ
if line.lower().startswith("subtitle:") or line.startswith("Subtitle:"):
subtitle = line.split(':', 1)[1].strip()
# Bullet point ํŒŒ์‹ฑ - โ€ข ๋˜๋Š” - ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ ๋ชจ๋‘ ์ฒ˜๋ฆฌ
elif line.startswith("โ€ข") or line.startswith("-") or (len(line) > 2 and line[1] == ' ' and ord(line[0]) >= 128):
# ์ด๋ฏธ โ€ข ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ, ์—†์œผ๋ฉด ์ถ”๊ฐ€
if not line.startswith("โ€ข"):
line = "โ€ข " + line.lstrip("- ")
bullet_points.append(line)
# ๋งŒ์•ฝ subtitle์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
if not subtitle:
subtitle = f"{slide_title} ๊ฐœ์š”"
# ๋งŒ์•ฝ bullet_points๊ฐ€ 5๊ฐœ ๋ฏธ๋งŒ์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€
while len(bullet_points) < 5:
bullet_points.append(f"โ€ข ๐Ÿ“Œ ํฌ์ธํŠธ {len(bullet_points) + 1}")
# 5๊ฐœ๋งŒ ์„ ํƒ
bullet_points = bullet_points[:5]
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ํŒŒ์‹ฑ๋œ subtitle: {subtitle}")
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ํŒŒ์‹ฑ๋œ bullets: {bullet_points}")
# ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
subtitle = translate_content_to_korean(subtitle)
# ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ์ด๋ชจ์ง€๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฒˆ์—ญ
translated_bullets = []
for point in bullet_points:
# โ€ข ์ œ๊ฑฐํ•˜๊ณ  ๋‚ด์šฉ๋งŒ ์ถ”์ถœ
clean_point = point.replace('โ€ข', '').strip()
# ์ด๋ชจ์ง€์™€ ํ…์ŠคํŠธ ๋ถ„๋ฆฌ (์ฒซ ๋ฒˆ์งธ ๊ณต๋ฐฑ์„ ๊ธฐ์ค€์œผ๋กœ)
if len(clean_point) > 0 and clean_point[0] in '๐ŸŽฏ๐Ÿ“Š๐Ÿ’ก๐Ÿš€โšก๐Ÿ”๐Ÿ“ˆ๐Ÿ’ฐ๐Ÿ†๐Ÿ”ง๐ŸŒ๐Ÿ”โญ๐ŸŽจ๐Ÿ“ฑ๐Ÿค๐Ÿ“๐ŸŽ–๏ธ๐Ÿ—๏ธ๐ŸŒฑ๐Ÿ’ป๐Ÿ› ๏ธโš™๏ธ๐Ÿค–๐Ÿ“‹โœ…๐ŸŒŸ๐Ÿ“…๐Ÿ’ช๐ŸŽ๐Ÿ’๐Ÿ›ก๏ธโœจ๐Ÿ…๐Ÿ‘๐Ÿ˜Š๐Ÿ ๐ŸŒโค๏ธ๐ŸŽ‰๐ŸŒˆ':
# ์ด๋ชจ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
emoji = clean_point[0]
text = clean_point[1:].strip()
translated_text = translate_content_to_korean_concise(text)
translated_bullets.append(f"โ€ข {emoji} {translated_text}")
else:
# ์ด๋ชจ์ง€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
translated_bullets.append(f"โ€ข {translate_content_to_korean_concise(clean_point)}")
bullet_points = translated_bullets
return {
"subtitle": subtitle,
"bullet_points": bullet_points
}
else:
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] API ์˜ค๋ฅ˜: {response.status_code}")
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 translate_content_to_korean_concise(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 specializing in concise Korean business presentations.
Translate to Korean using noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ์–ด๋ฏธ).
End with "์ž„", "ํ•จ", or noun forms instead of "์ž…๋‹ˆ๋‹ค", "์Šต๋‹ˆ๋‹ค".
Keep it extremely concise and professional.
Examples: "์ „๋žต์  ํ™•๋Œ€", "ํ•ต์‹ฌ ๊ณผ์ œ ๋„์ถœ", "์‹œ์žฅ ์„ ๋„ ์ „๋žต ์ˆ˜๋ฆฝ"
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 translate_content_to_korean_natural(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 natural, conversational Korean.
Use polite spoken language suitable for presentations.
Only return the translation without any explanation."""
},
{
"role": "user",
"content": text
}
],
"max_tokens": 300,
"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]}...")
global current_topic, uploaded_content
# ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์กฐ๊ฑด ํ™•์ธ
should_generate_process_flow = False
# ์Šฌ๋ผ์ด๋“œ ์ •๋ณด์—์„œ ์ œ๋ชฉ ์ถ”์ถœ
slide_title = ""
if ":" in slide_info:
# "์Šฌ๋ผ์ด๋“œ 5: ํ”„๋กœ์„ธ์Šค" ํ˜•ํƒœ์—์„œ "ํ”„๋กœ์„ธ์Šค" ์ถ”์ถœ
parts = slide_info.split(":")
if len(parts) >= 2:
slide_title = parts[1].strip()
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์ถ”์ถœ๋œ ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ: '{slide_title}'")
# 1. ํ”„๋กœ์„ธ์Šค ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
process_keywords = [
"ํ”„๋กœ์„ธ์Šค", "์ž‘๋™", "ํ”Œ๋กœ์šฐ", "ํ๋ฆ„", "์›Œํฌํ”Œ๋กœ์šฐ",
"์ ˆ์ฐจ", "๋‹จ๊ณ„", "์ฒ˜๋ฆฌ", "์ง„ํ–‰", "๊ฐœ์š”"
]
# 2. ์Šคํƒ€์ผ ํ™•์ธ
is_workflow_style = False
if any(style in prompt for style in ["Business Workflow", "Flowchart", "Business Process"]):
is_workflow_style = True
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] Business Workflow ๋˜๋Š” Flowchart ์Šคํƒ€์ผ ๊ฐ์ง€")
# 3. ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๊ฑฐ๋‚˜, ์Šคํƒ€์ผ์ด ์›Œํฌํ”Œ๋กœ์šฐ์ธ ๊ฒฝ์šฐ
title_has_process = any(keyword in slide_title for keyword in process_keywords)
prompt_has_process = any(keyword in prompt.lower() for keyword in ["process", "flow", "workflow"])
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ: {title_has_process}")
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ์›Œํฌํ”Œ๋กœ์šฐ ์Šคํƒ€์ผ: {is_workflow_style}")
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ํ”„๋กฌํ”„ํŠธ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ: {prompt_has_process}")
# ์กฐ๊ฑด ์™„ํ™”: ์ œ๋ชฉ์— ํ”„๋กœ์„ธ์Šค ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๊ณ , ์Šคํƒ€์ผ์ด ๋งž๊ฑฐ๋‚˜ ํ”„๋กฌํ”„ํŠธ์— ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ƒ์„ฑ
if title_has_process and (is_workflow_style or prompt_has_process):
should_generate_process_flow = True
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โœ… ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์กฐ๊ฑด ์ถฉ์กฑ!")
# ํŠน๋ณ„ ์ผ€์ด์Šค: "๋ชฉ์ฐจ"๋Š” ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ์ด๋ฏธ์ง€๋กœ
if "๋ชฉ์ฐจ" in slide_title:
should_generate_process_flow = False
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ๋ชฉ์ฐจ๋Š” ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ œ์™ธ")
# ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
if PROCESS_FLOW_AVAILABLE and should_generate_process_flow:
try:
print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] ๐Ÿ”ง ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์‹œ์ž‘...")
# ์Šฌ๋ผ์ด๋“œ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์ ˆํ•œ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ
img = generate_process_flow_for_ppt(
topic=current_topic,
context=slide_info,
style="Business Workflow"
)
if isinstance(img, Image.Image):
print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โœ… ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์„ฑ๊ณต!")
return img, "Process flow diagram generated with Korean support"
else:
print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์‹คํŒจ, ์ผ๋ฐ˜ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด")
except Exception as e:
print(f"[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โŒ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
import traceback
traceback.print_exc()
# ์‹คํŒจ์‹œ ์ผ๋ฐ˜ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์œผ๋กœ ํด๋ฐฑ
else:
if not PROCESS_FLOW_AVAILABLE:
print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โš ๏ธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ")
else:
print("[์ด๋ฏธ์ง€ ์ƒ์„ฑ] โ„น๏ธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ, ์ผ๋ฐ˜ ์ด๋ฏธ์ง€ ์ƒ์„ฑ")
# ๊ธฐ์กด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋กœ์ง
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"""
<div class="slide-container" style="
width: 100%;
max-width: 1200px;
margin: 20px auto;
background: white;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: hidden;
">
<div class="slide-header" style="
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
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;
background: #fafbfc;
">
<div class="slide-inner" style="
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
padding: 20px;
gap: 20px;
">
"""
# ํ‘œ์ง€์™€ ๋งˆ์ง€๋ง‰ ์Šฌ๋ผ์ด๋“œ๋Š” ์ „์ฒด ํ™”๋ฉด ์ด๋ฏธ์ง€
if slide_data.get('title') in ['ํ‘œ์ง€', 'Thank You']:
html += f"""
<!-- ์ „์ฒด ํ™”๋ฉด ์ด๋ฏธ์ง€ -->
<div style="
width: 100%;
height: 100%;
position: relative;
background: #e9ecef;
border-radius: 12px;
overflow: hidden;
">
"""
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;
background: rgba(255, 255, 255, 0.9);
padding: 30px 60px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
">
"""
if slide_data.get('title') == 'ํ‘œ์ง€':
html += f"""
<h1 style="font-size: 48px; margin-bottom: 10px; color: #000000; font-weight: 700;">{slide_data.get('topic', '')}</h1>
<h2 style="font-size: 24px; margin-top: 0; color: #212529; font-weight: 400;">{subtitle}</h2>
"""
else: # Thank You
html += f"""
<h1 style="font-size: 42px; color: #000000; font-weight: 700; line-height: 1.2;">{subtitle}</h1>
"""
html += """
</div>
"""
html += """
</div>
"""
else:
# ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ๋ ˆ์ด์•„์›ƒ
html += f"""
<!-- ํ…์ŠคํŠธ ์˜์—ญ (์ขŒ์ธก) -->
<div class="text-area" style="
flex: 1;
padding: 30px;
display: flex;
flex-direction: column;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
">
<h2 style="
color: #212529;
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:
clean_point = point.replace('โ€ข', '').strip()
html += f"""
<li style="
margin-bottom: 16px;
padding-left: 28px;
position: relative;
color: #495057;
font-size: 16px;
line-height: 1.6;
">
<span style="
position: absolute;
left: 0;
color: #007bff;
font-size: 18px;
">โ€ข</span>
{clean_point}
</li>
"""
html += f"""
</ul>
</div>
<!-- ์ด๋ฏธ์ง€ ์˜์—ญ (์šฐ์ธก) -->
<div class="image-area" style="
flex: 1;
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: 8px;
box-shadow: 0 2px 8px 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, theme_name: str = "๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ") -> str:
"""์ƒ์„ฑ๋œ ๊ฒฐ๊ณผ๋ฅผ PPTX ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ (๋ฐœํ‘œ์ž ๋…ธํŠธ ํฌํ•จ)"""
print(f"[PPTX] ํŒŒ์ผ ์ƒ์„ฑ ์‹œ์ž‘... ํ…Œ๋งˆ: {theme_name}")
# ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ƒ์„ฑ (16:9 ๋น„์œจ)
prs = Presentation()
prs.slide_width = Inches(16)
prs.slide_height = Inches(9)
# ์„ ํƒ๋œ ํ…Œ๋งˆ ๊ฐ€์ ธ์˜ค๊ธฐ
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ"])
# ๊ฐ ๊ฒฐ๊ณผ ์Šฌ๋ผ์ด๋“œ ์ถ”๊ฐ€
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_bg = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(2), Inches(2.8),
Inches(12), Inches(3.2)
)
title_bg.fill.solid()
title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
title_bg.fill.transparency = 0.8 # 50% ํˆฌ๋ช…๋„ (๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
title_bg.line.fill.background()
# ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ์•ฝํ•˜๊ฒŒ)
shadow = title_bg.shadow
shadow.visible = True
shadow.distance = Pt(6)
shadow.size = 100
shadow.blur_radius = Pt(12)
shadow.transparency = 0.8
shadow.angle = 45
# ์ œ๋ชฉ ํ…์ŠคํŠธ ์ถ”๊ฐ€
title_box = slide.shapes.add_textbox(
Inches(2), Inches(3.2),
Inches(12), Inches(1.5)
)
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(0, 0, 0) # ์™„์ „ํ•œ ๊ฒ€์ •์ƒ‰
title_para.alignment = PP_ALIGN.CENTER
# ๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€ (์ œ๋ชฉ๊ณผ ๊ฐ€๊นŒ์ด)
subtitle_box = slide.shapes.add_textbox(
Inches(2), Inches(4.3),
Inches(12), Inches(1.0)
)
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(28) # 24์—์„œ 28๋กœ ์ฆ๊ฐ€
subtitle_para.font.color.rgb = RGBColor(33, 37, 41) # ์ง„ํ•œ ํšŒ์ƒ‰
subtitle_para.alignment = PP_ALIGN.CENTER
# Thank You ์Šฌ๋ผ์ด๋“œ
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)}")
# Thank You ๋ฐฐ๊ฒฝ ๋ฐ•์Šค (๋ฐ˜ํˆฌ๋ช… - ๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
thanks_bg = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(2), Inches(3.5),
Inches(12), Inches(2.5)
)
thanks_bg.fill.solid()
thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
thanks_bg.fill.transparency = 0.8 # 50% ํˆฌ๋ช…๋„ (๋” ํˆฌ๋ช…ํ•˜๊ฒŒ)
thanks_bg.line.fill.background()
# ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ์•ฝํ•˜๊ฒŒ)
shadow = thanks_bg.shadow
shadow.visible = True
shadow.distance = Pt(6)
shadow.size = 100
shadow.blur_radius = Pt(12)
shadow.transparency = 0.8
shadow.angle = 45
# Thank You ํ…์ŠคํŠธ (๊ฒฐ๋ก  ๋ฌธ๊ตฌ)
thanks_box = slide.shapes.add_textbox(
Inches(2), Inches(4),
Inches(12), Inches(1.5)
)
thanks_frame = thanks_box.text_frame
thanks_frame.text = slide_data.get('subtitle', 'Thank You') # ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์‚ฌ์šฉ
thanks_para = thanks_frame.paragraphs[0]
thanks_para.font.size = Pt(42) # ํฌ๊ธฐ๋ฅผ ์กฐ๊ธˆ ์ค„์ž„
thanks_para.font.bold = True
thanks_para.font.color.rgb = RGBColor(0, 0, 0) # ์™„์ „ํ•œ ๊ฒ€์ •์ƒ‰
thanks_para.alignment = PP_ALIGN.CENTER
# ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ
else:
# ๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ •
background = slide.background
fill = background.fill
fill.solid()
fill.fore_color.rgb = theme["background"]
# ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค (๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ ํšจ๊ณผ)
title_box_bg = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(0.3), Inches(0.2),
Inches(15.4), Inches(1.0)
)
title_box_bg.fill.solid()
title_box_bg.fill.fore_color.rgb = theme["box_fill"]
title_box_bg.fill.transparency = 1 - theme["box_opacity"]
# ๊ทธ๋ฆผ์ž ํšจ๊ณผ
if theme["shadow"]:
shadow = title_box_bg.shadow
shadow.visible = True
shadow.distance = Pt(4)
shadow.size = 100
shadow.blur_radius = Pt(8)
shadow.transparency = 0.75
shadow.angle = 45
title_box_bg.line.fill.background() # ํ…Œ๋‘๋ฆฌ ์—†์Œ
# ์Šฌ๋ผ์ด๋“œ ์ œ๋ชฉ ์ถ”๊ฐ€
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 = theme["title_color"]
# ์ขŒ์ธก ํ…์ŠคํŠธ ์˜์—ญ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค
text_box_bg = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(0.3), Inches(1.4),
Inches(7.8), Inches(6.8)
)
text_box_bg.fill.solid()
text_box_bg.fill.fore_color.rgb = theme["box_fill"]
text_box_bg.fill.transparency = 1 - theme["box_opacity"]
if theme["shadow"]:
shadow = text_box_bg.shadow
shadow.visible = True
shadow.distance = Pt(5)
shadow.size = 100
shadow.blur_radius = Pt(10)
shadow.transparency = 0.7
shadow.angle = 45
text_box_bg.line.fill.background()
# ์ขŒ์ธก ํ…์ŠคํŠธ ์˜์—ญ
text_box = slide.shapes.add_textbox(
Inches(0.8), Inches(1.8),
Inches(7.0), Inches(6.0)
)
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 = theme["subtitle_color"]
subtitle_para.space_after = Pt(20)
# ๋ถˆ๋ฆฟ ํฌ์ธํŠธ ์ถ”๊ฐ€
bullet_points = slide_data.get('bullet_points', [])
for point in bullet_points:
p = text_frame.add_paragraph()
# โ€ข ์ œ๊ฑฐํ•˜๊ณ  ํ…์ŠคํŠธ๋งŒ ์ถ”๊ฐ€ (์ด๋ชจ์ง€๋Š” ์œ ์ง€)
clean_text = point.replace('โ€ข', '').strip()
p.text = clean_text
p.font.size = Pt(16)
p.font.color.rgb = theme["text_color"]
# ๋ถˆ๋ฆฟ ์—†์ด ๋“ค์—ฌ์“ฐ๊ธฐ๋งŒ
p.level = 0
p.space_after = Pt(12)
p.line_spacing = 1.5
# ์™ผ์ชฝ ์—ฌ๋ฐฑ ์„ค์ •
p.left_indent = Pt(0)
# ๋ถˆ๋ฆฟ ์ƒ‰์ƒ
p.font.color.rgb = theme["text_color"]
# ์šฐ์ธก ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ (๋ฐฐ๊ฒฝ ๋ฐ•์Šค ์ œ๊ฑฐ)
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.6),
width=Inches(6.8), height=Inches(6.4)
)
# ์ด๋ฏธ์ง€ ํ…Œ๋‘๋ฆฌ ์ œ๊ฑฐ
pic.line.fill.background()
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 = theme["text_color"]
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", [])
# ํ‘œ์ง€์™€ Thank You๋ฅผ ์ œ์™ธํ•œ ๋ณธ๋ฌธ ์Šฌ๋ผ์ด๋“œ ์ˆ˜
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)
# Thank You ์Šฌ๋ผ์ด๋“œ ์ถ”๊ฐ€ (๋งจ ๋’ค)
slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋งˆ๋ฌด๋ฆฌ์™€ ํ•ต์‹ฌ ๋ฉ”์‹œ์ง€"})
return slides
def create_custom_slides_ui(initial_count=5):
"""์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ UI (3-20์žฅ)"""
slides = []
for i in range(20): # ์ตœ๋Œ€ 20์žฅ
row = gr.Row(visible=(i < initial_count)) # Row๋ฅผ ๋ณ€์ˆ˜์— ํ• ๋‹น
with row:
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": row})
return slides
def generate_ppt_with_content(topic: str, template_name: str, audience_type: str, custom_slides: List[Dict],
slide_count: int, seed: int, uploaded_file,
use_web_search: bool, theme_name: str = "๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ",
progress=gr.Progress()):
"""PPT ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ ๋‚ด์šฉ์„ ํ•จ๊ป˜ ์ƒ์„ฑ (๋ฐœํ‘œ์ž ๋…ธํŠธ ํฌํ•จ)"""
results = []
preview_html = ""
# ์ „์—ญ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ
global current_topic, uploaded_content
current_topic = topic
# ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐ
uploaded_content = ""
if uploaded_file is not None:
try:
uploaded_content = read_uploaded_file(uploaded_file.name)
print(f"[ํŒŒ์ผ ์—…๋กœ๋“œ] ๋‚ด์šฉ ๊ธธ์ด: {len(uploaded_content)}์ž")
except Exception as e:
print(f"[ํŒŒ์ผ ์—…๋กœ๋“œ] ์˜ค๋ฅ˜: {str(e)}")
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 ์ƒ์„ฑ] ์˜ค๋””์–ธ์Šค: {audience_type}")
print(f"[PPT ์ƒ์„ฑ] ๋””์ž์ธ ํ…Œ๋งˆ: {theme_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)
# CSS ์Šคํƒ€์ผ ์ถ”๊ฐ€
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', '')}"
# Thank You ์Šฌ๋ผ์ด๋“œ๋Š” ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์ƒ์„ฑ
if slide['title'] == 'Thank You':
conclusion_phrase = generate_conclusion_phrase(topic, audience_type)
content = {
"subtitle": conclusion_phrase,
"bullet_points": []
}
# Thank You ์Šฌ๋ผ์ด๋“œ์šฉ ํŠน๋ณ„ ๋…ธํŠธ
speaker_notes = generate_closing_notes(topic, conclusion_phrase, audience_type)
else:
content = generate_slide_content(
topic, slide['title'], slide_context, audience_type,
uploaded_content, web_search_results
)
# ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ๋ฐœํ‘œ์ž ๋…ธํŠธ
speaker_notes = generate_presentation_notes(topic, slide['title'], content, audience_type)
# ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ๋ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
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 # ํ‘œ์ง€์šฉ
}
# ํ”„๋ฆฌ๋ทฐ HTML ์ƒ์„ฑ (ํŽธ์ง‘ ๊ธฐ๋Šฅ ์ œ๊ฑฐ)
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
})
# PPTX ํŒŒ์ผ ์ƒ์„ฑ
progress(0.95, "PPTX ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...")
pptx_path = None
try:
pptx_path = create_pptx_file(results, topic, template_name, theme_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}๊ฐœ ์„ฑ๊ณต"
# ์ „์—ญ ๋ณ€์ˆ˜ ์ €์žฅ
global current_slides_data, current_template, current_theme
current_slides_data = results
current_template = template_name
current_theme = theme_name
if pptx_path:
final_status += f"\n\n### ๐Ÿ“ฅ PPTX ํŒŒ์ผ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค!"
final_status += f"\n\n๐ŸŽค **{audience_type}๋ฅผ ์œ„ํ•œ ๋ฐœํ‘œ์ž ๋…ธํŠธ๊ฐ€ ๊ฐ ์Šฌ๋ผ์ด๋“œ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!**"
if PROCESS_FLOW_AVAILABLE:
final_status += f"\n\n๐Ÿ”ง **ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค (ํ•œ๊ธ€ ์ง€์›)**"
yield preview_html, final_status, pptx_path
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
with gr.Blocks(title="PPT ์ด๋ฏธ์ง€ ์ƒ์„ฑ๊ธฐ", theme=gr.themes.Soft(), css="""
.preview-container { max-width: 1400px; margin: 0 auto; }
.slide-container { transition: all 0.3s ease; }
.slide-container:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important; }
#custom_accordion .form {
max-height: 600px;
overflow-y: auto;
padding-right: 10px;
}
#custom_accordion .form::-webkit-scrollbar {
width: 6px;
}
#custom_accordion .form::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
#custom_accordion .form::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
#custom_accordion .form::-webkit-scrollbar-thumb:hover {
background: #555;
}
""") as demo:
gr.Markdown("""
# ๐ŸŽฏ AI ๊ธฐ๋ฐ˜ PPT ํ†ตํ•ฉ ์ƒ์„ฑ๊ธฐ (๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ ๋ฒ„์ „)
### ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์กฐํ™”๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜์„ธ์š”!
#### ๐Ÿ†• ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ:
- ๐ŸŽญ **์˜ค๋””์–ธ์Šค๋ณ„ ์ตœ์ ํ™”**: ๋ฐœํ‘œ ๋Œ€์ƒ์— ๋งž์ถ˜ ๋‚ด์šฉ๊ณผ ํ†ค ์ž๋™ ์กฐ์ •
- ๐Ÿ“ **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ํ…์ŠคํŠธ**: "~์ž„", "~ํ•จ" ์Šคํƒ€์ผ์˜ ํ”„๋กœํŽ˜์…”๋„ํ•œ ๋ฌธ์ฒด
- ๐ŸŽจ **์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€**: ๊ฐ ํฌ์ธํŠธ๋ณ„ ์ ์ ˆํ•œ ์ด๋ชจ์ง€๋กœ ์‹œ๊ฐ์  ๊ตฌ๋ถ„
- ๐Ÿ“‹ **์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ**: ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์˜ˆ์ œ ์ฃผ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
- ๐ŸŽจ **๋””์ž์ธ ํ…Œ๋งˆ**: 5๊ฐ€์ง€ ์ „๋ฌธ์ ์ธ ๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ ๊ฐ€๋Šฅ
- ๐Ÿ’Ž **๊ฐœ์„ ๋œ ์Šฌ๋ผ์ด๋“œ ๋””์ž์ธ**: ๋” ํˆฌ๋ช…ํ•œ ๋ฐฐ๊ฒฝ๊ณผ ๊น”๋”ํ•œ ๋ ˆ์ด์•„์›ƒ
- ๐Ÿ“Š **ํ‘œ์ง€์™€ Thank You ์Šฌ๋ผ์ด๋“œ** ์ž๋™ ์ถ”๊ฐ€
- ๐ŸŽค **์˜ค๋””์–ธ์Šค๋ณ„ ๋ฐœํ‘œ์ž ๋…ธํŠธ** ์ž๋™ ์ƒ์„ฑ (๊ตฌ์–ด์ฒด)
- ๐Ÿ“ **ํŒŒ์ผ ์—…๋กœ๋“œ** ์ง€์› (PDF/CSV/TXT)
- ๐Ÿ” **์›น ๊ฒ€์ƒ‰** ๊ธฐ๋Šฅ (Brave Search)
- ๐ŸŽš๏ธ **์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ ˆ** (6-20์žฅ)
- ๐Ÿ”ง **ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ** ์ž๋™ ์ƒ์„ฑ (ํ•œ๊ธ€ ์ง€์›)
- ๐ŸŽฏ **์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ**: 3-20์žฅ์„ ์ž์œ ๋กญ๊ฒŒ ๊ตฌ์„ฑํ•˜์—ฌ ๋‚˜๋งŒ์˜ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ œ์ž‘
""")
# API ํ† ํฐ ์ƒํƒœ ํ™•์ธ
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 not PROCESS_FLOW_AVAILABLE:
token_status.append("โ„น๏ธ process_flow_generator๊ฐ€ ์—†์–ด ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
if token_status:
gr.Markdown("\n".join(token_status))
with gr.Row():
with gr.Column(scale=1):
# ์˜ˆ์ œ ์„ ํƒ
with gr.Row():
example_btn = gr.Button("๐Ÿ“‹ ์˜ˆ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", size="sm", variant="secondary")
# ๊ธฐ๋ณธ ์ž…๋ ฅ
topic_input = gr.Textbox(
label="ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ฃผ์ œ",
placeholder="์˜ˆ: AI ์Šคํƒ€ํŠธ์—… ํˆฌ์ž ์œ ์น˜, ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ, ๋””์ง€ํ„ธ ์ „ํ™˜ ์ „๋žต",
lines=2
)
# ์˜ค๋””์–ธ์Šค ์„ ํƒ (์ƒˆ๋กœ ์ถ”๊ฐ€)
audience_select = gr.Dropdown(
choices=list(AUDIENCE_TYPES.keys()),
label="๐ŸŽญ ๋ฐœํ‘œ ๋Œ€์ƒ ์˜ค๋””์–ธ์Šค",
value="์ผ๋ฐ˜ ์ง์›",
info="๋ฐœํ‘œ ๋Œ€์ƒ์— ๋”ฐ๋ผ ๋‚ด์šฉ๊ณผ ํ†ค์ด ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค"
)
# ์˜ค๋””์–ธ์Šค ์„ค๋ช… ํ‘œ์‹œ
audience_info = gr.Markdown()
# PPT ํ…œํ”Œ๋ฆฟ ์„ ํƒ
template_select = gr.Dropdown(
choices=list(PPT_TEMPLATES.keys()),
label="PPT ํ…œํ”Œ๋ฆฟ ์„ ํƒ",
value="๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ",
info="๋ชฉ์ ์— ๋งž๋Š” ํ…œํ”Œ๋ฆฟ์„ ์„ ํƒํ•˜์„ธ์š”"
)
# ๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ
theme_select = gr.Dropdown(
choices=list(DESIGN_THEMES.keys()),
label="๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ",
value="๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ",
info="ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ์ „์ฒด์ ์ธ ๋””์ž์ธ ์Šคํƒ€์ผ"
)
# ํ…Œ๋งˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
theme_preview = gr.HTML()
# ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
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",
value=None
)
# ์›น ๊ฒ€์ƒ‰ ์˜ต์…˜
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 ์ƒ์„ฑ ์‹œ์ž‘ (AI๊ฐ€ ๋ชจ๋“  ๋‚ด์šฉ์„ ์ž๋™ ์ƒ์„ฑ)", variant="primary", size="lg")
# ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ์•ˆ๋‚ด
with gr.Accordion("๐Ÿ“– ์‚ฌ์šฉ ๋ฐฉ๋ฒ•", open=False):
gr.Markdown("""
### ๐Ÿ”„ ์ž‘์—… ์ˆœ์„œ:
1. **์˜ˆ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ** ๋ฒ„ํŠผ์œผ๋กœ ์ƒ˜ํ”Œ ์ฃผ์ œ๋ฅผ ๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ์ž…๋ ฅ
2. **์˜ค๋””์–ธ์Šค ์„ ํƒ**: ๋ฐœํ‘œ ๋Œ€์ƒ์— ๋”ฐ๋ผ ๋‚ด์šฉ์ด ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค
3. **ํ…œํ”Œ๋ฆฟ๊ณผ ํ…Œ๋งˆ ์„ ํƒ** ํ›„ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ
4. **๋‹ค์šด๋กœ๋“œ**: ์ƒ์„ฑ ์™„๋ฃŒ ํ›„ PPTX ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
### ๐ŸŽญ ์˜ค๋””์–ธ์Šค๋ณ„ ํŠน์ง•:
- **๊ฒฝ์˜์ง„/์ž„์›**: ์ „๋žต์  ๊ฐ€์น˜, ROI, ๋น„์ฆˆ๋‹ˆ์Šค ์ž„ํŒฉํŠธ ์ค‘์‹ฌ
- **ํˆฌ์ž์ž**: ์‹œ์žฅ ๊ธฐํšŒ, ์„ฑ์žฅ ๊ฐ€๋Šฅ์„ฑ, ์ˆ˜์ต์„ฑ ๊ฐ•์กฐ
- **๊ธฐ์ˆ ํŒ€**: ๊ธฐ์ˆ ์  ์„ธ๋ถ€์‚ฌํ•ญ, ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ์„ฑ๋Šฅ ์ง€ํ‘œ
- **์ผ๋ฐ˜ ์ง์›**: ์‹ค๋ฌด์  ๋‚ด์šฉ, ํ˜‘์—… ๋ฐฉ์•ˆ, ์‹คํ–‰ ๊ณ„ํš
- **๊ณ ๊ฐ/ํŒŒํŠธ๋„ˆ**: ๊ณ ๊ฐ ๊ฐ€์น˜, ํ˜œํƒ, ์„ฑ๊ณต ์‚ฌ๋ก€
- **์ผ๋ฐ˜ ๋Œ€์ค‘**: ์‰ฌ์šด ์„ค๋ช…, ์‚ฌ์šฉ ํŽธ์˜์„ฑ, ์‹ค์งˆ์  ์ด์ 
### ๐Ÿ’ก ํ…์ŠคํŠธ ์Šคํƒ€์ผ ํŠน์ง•:
- **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ์ข…๊ฒฐ**: "~์ž„", "~ํ•จ" ๋˜๋Š” ๋ช…์‚ฌ๋กœ ๋๋‚จ
- **์ด๋ชจ์ง€ ํ™œ์šฉ**: ๊ฐ ํฌ์ธํŠธ ์•ž์— ๋‚ด์šฉ ๊ด€๋ จ ์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€
- **8-12๋‹จ์–ด**: ๊ฐ ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ๋งค์šฐ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌ์„ฑ
### ๐Ÿ“ ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ:
- **ํ…œํ”Œ๋ฆฟ ๋Œ€์‹  ์ง์ ‘ ๊ตฌ์„ฑ**: "์‚ฌ์šฉ์ž ์ •์˜" ํ…œํ”Œ๋ฆฟ ์„ ํƒ ์‹œ ํ™œ์„ฑํ™”
- **3-20์žฅ ์ž์œ ๋กญ๊ฒŒ ๊ตฌ์„ฑ**: ์Šฌ๋ผ์ด๋”๋กœ ์›ํ•˜๋Š” ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
- **๊ฐ ์Šฌ๋ผ์ด๋“œ๋ณ„ ์Šคํƒ€์ผ ์ง€์ •**: 16๊ฐ€์ง€ ์Šคํƒ€์ผ ์ค‘ ์„ ํƒ
- **ํžŒํŠธ ์ œ๊ณต**: ๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ๋‚ด์šฉ ๋ฐฉํ–ฅ์„ ํ”„๋กฌํ”„ํŠธ ํžŒํŠธ๋กœ ์ง€์ •
### ๐Ÿ”ง ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ:
- **Business Workflow** ๋˜๋Š” **Flowchart** ์Šคํƒ€์ผ ์„ ํƒ ์‹œ ์ž๋™์œผ๋กœ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
- ํ•œ๊ธ€ ํ…์ŠคํŠธ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•˜๋Š” ๋‹ค์ด์–ด๊ทธ๋žจ
- ํ”„๋กœ์„ธ์Šค, ์˜์‚ฌ๊ฒฐ์ •, ์ž…์ถœ๋ ฅ ๋“ฑ ๋‹ค์–‘ํ•œ ๋…ธ๋“œ ํƒ€์ž… ์ง€์›
""")
# PPTX ๋‹ค์šด๋กœ๋“œ ์˜์—ญ
with gr.Row():
download_file = gr.File(
label="๐Ÿ“ฅ ์ƒ์„ฑ๋œ PPTX ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ",
visible=True,
elem_id="download-file"
)
# ์‚ฌ์šฉ์ž ์ •์˜ ์„น์…˜
with gr.Accordion("๐Ÿ“ ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ", open=False, elem_id="custom_accordion") as custom_accordion:
gr.Markdown("ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š”. (3-20์žฅ)")
# ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ
custom_slide_count = gr.Slider(
minimum=3,
maximum=20,
value=5,
step=1,
label="์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜",
info="์ƒ์„ฑํ•  ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š”"
)
custom_slides_components = create_custom_slides_ui(initial_count=5)
# ์ƒํƒœ ํ‘œ์‹œ
status_output = gr.Markdown(
value="### ๐Ÿ‘† ํ…œํ”Œ๋ฆฟ์„ ์„ ํƒํ•˜๊ณ  ์ƒ์„ฑ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”!"
)
# ํ”„๋ฆฌ๋ทฐ ์˜์—ญ
preview_output = gr.HTML(
label="PPT ํ”„๋ฆฌ๋ทฐ (16:9)",
elem_classes="preview-container"
)
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
def load_example(template_name):
"""ํ…œํ”Œ๋ฆฟ์— ๋งž๋Š” ์˜ˆ์ œ ์ฃผ์ œ ๋กœ๋“œ"""
example_topic = EXAMPLE_TOPICS.get(template_name, "")
return example_topic
def update_audience_info(audience_type):
"""์˜ค๋””์–ธ์Šค ์ •๋ณด ํ‘œ์‹œ"""
info = AUDIENCE_TYPES.get(audience_type, {})
return f"""**{info.get('description', '')}**
- ํ†ค: {info.get('tone', '')}
- ํฌ์ปค์Šค: {info.get('focus', '')}"""
def update_theme_preview(theme_name):
"""ํ…Œ๋งˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ HTML ์ƒ์„ฑ"""
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ"])
# RGB ๊ฐ’์„ hex๋กœ ๋ณ€ํ™˜
def rgb_to_hex(rgb_color):
return f"#{rgb_color[0]:02x}{rgb_color[1]:02x}{rgb_color[2]:02x}"
bg_hex = rgb_to_hex(theme["background"])
box_hex = rgb_to_hex(theme["box_fill"])
title_hex = rgb_to_hex(theme["title_color"])
text_hex = rgb_to_hex(theme["text_color"])
accent_hex = rgb_to_hex(theme["accent_color"])
preview_html = f"""
<div style="
background: {bg_hex};
padding: 20px;
border-radius: 8px;
margin: 10px 0;
">
<div style="
background: {box_hex};
opacity: {theme['box_opacity']};
padding: 15px;
border-radius: 12px;
margin-bottom: 10px;
{'box-shadow: 0 4px 8px rgba(0,0,0,0.1);' if theme['shadow'] else ''}
">
<h4 style="color: {title_hex}; margin: 0 0 10px 0;">{theme['name']}</h4>
<p style="color: {text_hex}; margin: 0; font-size: 14px;">
{theme['description']}
</p>
<div style="
width: 40px;
height: 4px;
background: {accent_hex};
margin-top: 10px;
border-radius: 2px;
"></div>
</div>
</div>
"""
return preview_html
def update_template_info(template_name, slide_count):
is_custom = template_name == "์‚ฌ์šฉ์ž ์ •์˜"
info = ""
if not is_custom and 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', '')}"
if style_info.get('is_process_flow'):
info += " ๐Ÿ”ง"
info += "\n"
elif is_custom:
info = "**์‚ฌ์šฉ์ž ์ •์˜ ํ…œํ”Œ๋ฆฟ**\n\n์•„๋ž˜ '์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ' ์„น์…˜์—์„œ ์ง์ ‘ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š”."
# ์‚ฌ์šฉ์ž ์ •์˜ ํ…œํ”Œ๋ฆฟ์ผ ๋•Œ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์„ ํƒ ์ˆจ๊ธฐ๊ธฐ
slide_count_visible = not is_custom
custom_accordion_open = is_custom
return info, gr.update(visible=slide_count_visible), gr.update(open=custom_accordion_open)
def update_custom_slides_visibility(count):
"""์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ˆ˜์— ๋”ฐ๋ผ UI ํ‘œ์‹œ/์ˆจ๊น€"""
updates = []
for i in range(20):
updates.append(gr.update(visible=(i < count))) # row
return updates
def generate_ppt_handler(topic, template_name, audience_type, theme_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
})
# PPT ์ƒ์„ฑ
for preview, status, pptx_file in generate_ppt_with_content(
topic, template_name, audience_type, custom_slides, slide_count, seed,
file_upload, use_web_search, theme_name, progress
):
yield preview, status, pptx_file
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
example_btn.click(
fn=load_example,
inputs=[template_select],
outputs=[topic_input]
)
audience_select.change(
fn=update_audience_info,
inputs=[audience_select],
outputs=[audience_info]
)
theme_select.change(
fn=update_theme_preview,
inputs=[theme_select],
outputs=[theme_preview]
)
template_select.change(
fn=update_template_info,
inputs=[template_select, slide_count],
outputs=[template_info, slide_count, custom_accordion]
)
slide_count.change(
fn=lambda t, s: update_template_info(t, s)[0],
inputs=[template_select, slide_count],
outputs=[template_info]
)
custom_slide_count.change(
fn=update_custom_slides_visibility,
inputs=[custom_slide_count],
outputs=[slide["row"] for slide in custom_slides_components]
)
# ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ ํ‰ํƒ„ํ™”
all_custom_inputs = []
for slide in custom_slides_components:
all_custom_inputs.extend([slide["title"], slide["style"], slide["hint"]])
generate_btn.click(
fn=generate_ppt_handler,
inputs=[
topic_input, template_select, audience_select, theme_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=lambda: (update_template_info(template_select.value, slide_count.value)[0],
update_theme_preview(theme_select.value),
update_audience_info(audience_select.value)),
inputs=[],
outputs=[template_info, theme_preview, audience_info]
)
# ์•ฑ ์‹คํ–‰
if __name__ == "__main__":
demo.launch(ssr_mode=False)