GINI-Deck / app.py
ginipick's picture
Update app.py
008e30d verified
raw
history blame
106 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
# ์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ ์ •์˜
EXAMPLE_TOPICS = {
"๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ": "AI ๊ธฐ๋ฐ˜ ๊ณ ๊ฐ ์„œ๋น„์Šค ์ž๋™ํ™” ํ”Œ๋žซํผ ํˆฌ์ž ์ œ์•ˆ",
"์ œํ’ˆ ์†Œ๊ฐœ": "์Šค๋งˆํŠธ ํ™ˆ IoT ๋ณด์•ˆ ์‹œ์Šคํ…œ ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ",
"ํ”„๋กœ์ ํŠธ ๋ณด๊ณ ": "๋””์ง€ํ„ธ ์ „ํ™˜ ํ”„๋กœ์ ํŠธ 3๋ถ„๊ธฐ ์„ฑ๊ณผ ๋ณด๊ณ ",
"์ „๋žต ๊ธฐํš": "2025๋…„ ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ง„์ถœ ์ „๋žต ์ˆ˜๋ฆฝ"
}
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
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 = ""
# ๋””์ž์ธ ํ…Œ๋งˆ ์ •์˜
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"
},
"Industrial Design": {
"name": "Product Design",
"description": "Sleek industrial design concept sketch",
"use_case": "์ œํ’ˆ ์†Œ๊ฐœ, ์ปจ์…‰ ๋””์ž์ธ",
"example": "A sleek industrial design concept: Curved metallic body with minimal bezel, Touchscreen panel for settings, Modern matte black finish, Hand-drawn concept sketch style with annotations and dimension lines"
},
"3D Bubble Chart": {
"name": "Bubble Chart",
"description": "Clean 3D bubble visualization",
"use_case": "๋น„๊ต ๋ถ„์„, ํฌ์ง€์…”๋‹",
"example": "3-D bubble chart on clean white 2ร—2 grid, quadrant titles hidden, four translucent spheres in lime, azure, amber, magenta, gentle depth-of-field, modern consulting aesthetic, no text, 4K"
},
"Timeline Ribbon": {
"name": "Timeline",
"description": "Horizontal ribbon timeline with cyber-futuristic vibe",
"use_case": "์ผ์ •, ๋กœ๋“œ๋งต, ๋งˆ์ผ์Šคํ†ค",
"example": "Horizontal ribbon timeline, milestone pins glowing hot pink on charcoal, year markers as circles, faint motion streaks, cyber-futuristic vibe, no text, 1920ร—1080"
},
"Risk Heat Map": {
"name": "Heat Map",
"description": "Risk assessment heat map with gradient colors",
"use_case": "๋ฆฌ์Šคํฌ ๋ถ„์„, ์šฐ์„ ์ˆœ์œ„",
"example": "Risk Heat Map: square grid, smooth gradient from mint to fire-red, cells beveled, simple legend strip hidden, long subtle shadow, sterile white frame, no text"
},
"Pyramid/Funnel": {
"name": "Funnel Chart",
"description": "Multi-layer gradient funnel visualization",
"use_case": "๋‹จ๊ณ„๋ณ„ ์ถ•์†Œ, ํ•ต์‹ฌ ๋„์ถœ",
"example": "Pyramid / Funnel: 5-layer gradient funnel narrowing downwards, top vivid sky-blue, mid mint-green, bottom sunset-orange, glass reflection, minimal background, no text"
},
"KPI Dashboard": {
"name": "Dashboard",
"description": "Dark-mode analytics dashboard with sci-fi interface",
"use_case": "์„ฑ๊ณผ ์ง€ํ‘œ, ์‹ค์  ๋Œ€์‹œ๋ณด๋“œ",
"example": "KPI Dashboard: Dark-mode analytic dashboard, three glass speedometers glowing neon lime, two sparkline charts under, black glass background, sci-fi interface, no text, 4K"
},
"Value Chain": {
"name": "Value Chain",
"description": "Horizontal value chain with industrial look",
"use_case": "๊ฐ€์น˜ ์‚ฌ์Šฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ",
"example": "Value Chain Diagram: Horizontal value chain blocks, steel-blue gradient bars with subtle bevel, small gear icons above each segment, sleek industrial look, shadow cast, no text"
},
"Gantt Chart": {
"name": "Gantt Chart",
"description": "Hand-drawn style Gantt chart with playful colors",
"use_case": "ํ”„๋กœ์ ํŠธ ์ผ์ •, ์ž‘์—… ๊ด€๋ฆฌ",
"example": "Gantt Chart: Hand-drawn style Gantt bars sketched with vibrant markers on dotted grid notebook page, sticky-note color palette, playful yet organized, perspective tilt, no text"
},
"Mobile App Mockup": {
"name": "App Mockup",
"description": "Clean wireframe for mobile app design",
"use_case": "์•ฑ/์›น UI, ํ™”๋ฉด ์„ค๊ณ„",
"example": "MOCKUP DESIGN: A clean hand-drawn style wireframe for a mobile app with Title screen, Login screen, Dashboard with sections, Bottom navigation bar, minimalist design with annotations"
},
"Flowchart": {
"name": "Flowchart",
"description": "Vibrant flowchart with minimalistic icons",
"use_case": "์˜์‚ฌ๊ฒฐ์ •, ํ”„๋กœ์„ธ์Šค ํ๋ฆ„",
"example": "FLOWCHART DESIGN: A hand-drawn style flowchart, vibrant colors, minimalistic icons showing process flow from START to END with decision points, branches, and clear directional arrows"
}
}
# PPT ํ…œํ”Œ๋ฆฟ ์ •์˜ (๋™์ ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ • ๊ฐ€๋Šฅ)
PPT_TEMPLATES = {
"๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ": {
"description": "ํˆฌ์ž ์œ ์น˜, ์‚ฌ์—… ์ œ์•ˆ์šฉ",
"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':
# 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) -> 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.
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 an audience
2. Expand on the concise bullet points with additional context
3. Include transitions and engagement phrases
4. Use a warm, professional tone
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
- 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 (concise style with emojis):
{bullet_text}
Create natural speaker notes that expand on these concise points 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_natural(notes)
return notes
else:
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
except Exception as e:
print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
def regenerate_slide_text(topic: str, slide_title: str, slide_context: str,
user_instruction: str, uploaded_content: str = None) -> Dict[str, str]:
"""์‚ฌ์šฉ์ž ์ง€์‹œ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์Šฌ๋ผ์ด๋“œ ํ…์ŠคํŠธ ์žฌ์ƒ์„ฑ"""
print(f"[AI ์žฌ์ž‘์„ฑ] {slide_title} - ์ง€์‹œ์‚ฌํ•ญ: {user_instruction}")
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.
Follow the user's specific instructions to rewrite slide content.
Your task is to create:
1. A compelling subtitle (max 10 words)
2. Exactly 5 bullet points with emojis
3. Each bullet point should be 8-12 words
4. Use noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ) for Korean or concise fragments for English
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
- Follow the user's instructions while maintaining this style
Emoji Guidelines:
- Professional emojis: ๐Ÿ“Š ๐ŸŽฏ ๐Ÿ’ก ๐Ÿš€ โšก ๐Ÿ” ๐Ÿ“ˆ ๐Ÿ’ฐ ๐Ÿ† ๐Ÿ”ง ๐ŸŒ ๐Ÿ” โญ ๐ŸŽจ ๐Ÿ“ฑ ๐Ÿค ๐Ÿ“ ๐ŸŽช ๐Ÿ—๏ธ ๐ŸŒฑ
- Match emoji to content meaning
- Use different emojis for variety"""
user_message = f"""Topic: {topic}
Slide Title: {slide_title}
Context: {slide_context}
User's specific instruction: {user_instruction}
Rewrite the content following the instruction above while maintaining concise, noun-ending style with emojis."""
if uploaded_content:
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
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()
content = result['choices'][0]['message']['content'].strip()
# Parse content
lines = content.split('\n')
subtitle = ""
bullet_points = []
for line in lines:
if line.startswith("Subtitle:"):
subtitle = line.replace("Subtitle:", "").strip()
elif line.strip().startswith("โ€ข"):
bullet_points.append(line.strip())
# ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
subtitle = translate_content_to_korean(subtitle)
# ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ์ด๋ชจ์ง€๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฒˆ์—ญ
translated_bullets = []
for point in bullet_points:
# ์ด๋ชจ์ง€์™€ ํ…์ŠคํŠธ ๋ถ„๋ฆฌ
parts = point.split(' ', 2)
if len(parts) >= 3 and len(parts[1]) <= 2: # ์ด๋ชจ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
emoji = parts[1]
text = ' '.join(parts[2:])
translated_text = translate_content_to_korean_concise(text)
translated_bullets.append(f"โ€ข {emoji} {translated_text}")
else:
translated_bullets.append(translate_content_to_korean_concise(point))
bullet_points = translated_bullets
return {
"subtitle": subtitle,
"bullet_points": bullet_points[:5]
}
else:
return {
"subtitle": slide_title,
"bullet_points": ["โ€ข โŒ ์žฌ์ƒ์„ฑ ์‹คํŒจ"] * 5
}
except Exception as e:
print(f"[AI ์žฌ์ž‘์„ฑ] ์˜ค๋ฅ˜: {str(e)}")
return {
"subtitle": slide_title,
"bullet_points": ["โ€ข โŒ ์žฌ์ƒ์„ฑ ์˜ค๋ฅ˜"] * 5
}
def generate_closing_notes(topic: str, conclusion_phrase: str) -> str:
"""๋งˆ์ง€๋ง‰ ์Šฌ๋ผ์ด๋“œ์˜ ๋ฐœํ‘œ์ž ๋…ธํŠธ ์ƒ์„ฑ"""
print(f"[๋งˆ๋ฌด๋ฆฌ ๋…ธํŠธ] ์ƒ์„ฑ ์ค‘...")
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 creating closing speaker notes.
Create natural closing remarks that:
1. Thank the audience warmly
2. Briefly summarize the key message of the presentation
3. Reference the conclusion phrase naturally
4. End with an invitation for questions or next steps
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}
Conclusion phrase on screen: {conclusion_phrase}
Create natural closing speaker notes that wrap up the presentation effectively."""
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) -> str:
"""ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ํ•ต์‹ฌ์„ ๋‹ด์€ ๊ฒฐ๋ก  ๋ฌธ๊ตฌ ์ƒ์„ฑ"""
print(f"[๊ฒฐ๋ก  ๋ฌธ๊ตฌ] ์ƒ์„ฑ ์ค‘...")
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 copywriter creating powerful closing statements for presentations.
Create a concise, impactful closing phrase that:
1. Captures the essence of the presentation topic
2. Is memorable and inspirational
3. Maximum 5-7 words
4. Uses powerful, action-oriented language
5. Leaves a lasting impression
Examples:
- "Innovation Starts Today"
- "Together We Transform"
- "Future Begins Now"
- "Excellence Through Innovation"
Output only the phrase, no explanation."""
user_message = f"""Presentation topic: {topic}
Create a powerful closing phrase that encapsulates the main message."""
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, 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 with emojis
3. Each bullet point should be 8-12 words
4. Use noun-ending style (๋ช…์‚ฌํ˜• ์ข…๊ฒฐ) for Korean or concise fragments for English
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 represents the content
- Be extremely concise and impactful
Emoji Guidelines:
- Use professional emojis that match the content
- Common emojis:
๐Ÿ“Š (data/analysis), ๐ŸŽฏ (goals/targets), ๐Ÿ’ก (innovation/ideas),
๐Ÿš€ (growth/launch), โšก (speed/efficiency), ๐Ÿ” (research/analysis),
๐Ÿ“ˆ (increase/growth), ๐Ÿ’ฐ (finance/revenue), ๐Ÿ† (achievement/success),
๐Ÿ”ง (tools/solutions), ๐ŸŒ (global/network), ๐Ÿ” (security/safety),
โญ (excellence/quality), ๐ŸŽจ (creative/design), ๐Ÿ“ฑ (mobile/tech),
๐Ÿค (partnership/collaboration), ๐Ÿ“ (location/focus), ๐ŸŽ–๏ธ (strategy),
๐Ÿ—๏ธ (building/development), ๐ŸŒฑ (sustainability/growth)
- Each bullet should have a different, relevant emoji
Output format:
Subtitle: [subtitle here]
โ€ข ๐ŸŽฏ [Point 1 - noun ending or fragment]
โ€ข ๐Ÿ“Š [Point 2 - noun ending or fragment]
โ€ข ๐Ÿ’ก [Point 3 - noun ending or fragment]
โ€ข ๐Ÿš€ [Point 4 - noun ending or fragment]
โ€ข โšก [Point 5 - noun ending or fragment]"""
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. 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()
# Parse content
lines = content.split('\n')
subtitle = ""
bullet_points = []
for line in lines:
if line.startswith("Subtitle:"):
subtitle = line.replace("Subtitle:", "").strip()
elif line.strip().startswith("โ€ข"):
bullet_points.append(line.strip())
# ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
subtitle = translate_content_to_korean(subtitle)
# ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ์ด๋ชจ์ง€๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฒˆ์—ญ
translated_bullets = []
for point in bullet_points:
# ์ด๋ชจ์ง€์™€ ํ…์ŠคํŠธ ๋ถ„๋ฆฌ
parts = point.split(' ', 2)
if len(parts) >= 3 and len(parts[1]) <= 2: # ์ด๋ชจ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
emoji = parts[1]
text = ' '.join(parts[2:])
translated_text = translate_content_to_korean_concise(text)
translated_bullets.append(f"โ€ข {emoji} {translated_text}")
else:
translated_bullets.append(translate_content_to_korean_concise(point))
bullet_points = translated_bullets
return {
"subtitle": subtitle,
"bullet_points": bullet_points[:5] # ์ตœ๋Œ€ 5๊ฐœ
}
else:
return {
"subtitle": slide_title,
"bullet_points": ["โ€ข ๐Ÿ“Œ ๋‚ด์šฉ ์ƒ์„ฑ ๋ถˆ๊ฐ€"] * 5
}
except Exception as e:
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ์˜ค๋ฅ˜: {str(e)}")
return {
"subtitle": slide_title,
"bullet_points": ["โ€ข โŒ ๋‚ด์šฉ ์ƒ์„ฑ ์˜ค๋ฅ˜"] * 5
}
def translate_content_to_korean(text: str) -> str:
"""์˜์–ด ํ…์ŠคํŠธ๋ฅผ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ (์ผ๋ฐ˜์šฉ)"""
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation."
},
{
"role": "user",
"content": text
}
],
"max_tokens": 200,
"top_p": 0.8,
"stream": False
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content'].strip()
else:
return text
except Exception as e:
return text
def 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]}...")
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;
background: rgba(248, 249, 250, 0.9);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
">
"""
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_final_pptx_with_edits(results: List[Dict], topic: str, template_name: str,
theme_name: str, edited_data: Dict = None) -> str:
"""ํŽธ์ง‘๋œ ๋‚ด์šฉ์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… PPTX ํŒŒ์ผ ์ƒ์„ฑ"""
print("[PPTX] ํŽธ์ง‘๋œ ๋‚ด์šฉ์œผ๋กœ ์ตœ์ข… ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...")
# ํŽธ์ง‘๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฒฐ๊ณผ์— ๋ฐ˜์˜
if edited_data:
for slide_index, edits in edited_data.items():
if slide_index < len(results):
results[slide_index]["slide_data"].update(edits)
# ๊ธฐ์กด create_pptx_file ํ•จ์ˆ˜ ํ˜ธ์ถœ
return create_pptx_file(results, topic, template_name, theme_name)
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.2 # 20% ํˆฌ๋ช…๋„ (80% ๋ถˆํˆฌ๋ช…)
title_bg.line.fill.background()
# ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ๊ฐ•ํ•˜๊ฒŒ)
shadow = title_bg.shadow
shadow.visible = True
shadow.distance = Pt(8)
shadow.size = 120
shadow.blur_radius = Pt(15)
shadow.transparency = 0.3
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.2 # 20% ํˆฌ๋ช…๋„
thanks_bg.line.fill.background()
# ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ (๋” ๊ฐ•ํ•˜๊ฒŒ)
shadow = thanks_bg.shadow
shadow.visible = True
shadow.distance = Pt(8)
shadow.size = 120
shadow.blur_radius = Pt(15)
shadow.transparency = 0.3
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"]
# ์šฐ์ธก ์ด๋ฏธ์ง€ ์˜์—ญ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค
img_box_bg = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(8.3), Inches(1.4),
Inches(7.4), Inches(6.8)
)
img_box_bg.fill.solid()
img_box_bg.fill.fore_color.rgb = RGBColor(248, 249, 250)
img_box_bg.fill.transparency = 0.1
if theme["shadow"]:
shadow = img_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
img_box_bg.line.fill.background()
# ์šฐ์ธก ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
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_editable_slide_interface(slide_data: Dict, slide_index: int, topic: str) -> str:
"""ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์Šฌ๋ผ์ด๋“œ ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ"""
# ์ด๋ฏธ์ง€๋ฅผ 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", [])
# ํ‘œ์ง€์™€ Thank You๋Š” ํŽธ์ง‘ ๋ถˆ๊ฐ€
if slide_data.get('title') in ['ํ‘œ์ง€', 'Thank You']:
return create_slide_preview_html(slide_data)
# HTML ์ƒ์„ฑ
html = f"""
<div class="slide-container" id="slide_container_{slide_index}" 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;
display: flex;
justify-content: space-between;
align-items: center;
">
<span>์Šฌ๋ผ์ด๋“œ {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}</span>
<button id="edit_btn_{slide_index}" style="
background: #3498db;
color: white;
border: none;
padding: 5px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">โœ๏ธ ํŽธ์ง‘</button>
</div>
<div class="slide-content" style="
display: flex;
min-height: 400px;
background: #fafbfc;
position: relative;
">
<div class="slide-inner" style="
width: 100%;
display: flex;
padding: 20px;
gap: 20px;
">
<!-- ํ…์ŠคํŠธ ์˜์—ญ (์ขŒ์ธก) -->
<div class="text-area" style="
flex: 1;
padding: 30px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
">
<div id="view_mode_{slide_index}" style="display: block;">
<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 id="edit_mode_{slide_index}" style="display: none;">
<input type="text" id="subtitle_{slide_index}" value="{subtitle}" style="
width: 100%;
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 5px;
" placeholder="์†Œ์ œ๋ชฉ ์ž…๋ ฅ">
<div style="margin-bottom: 20px;">
"""
for i, point in enumerate(bullet_points):
# ์ด๋ชจ์ง€๋ฅผ ํฌํ•จํ•œ ์ „์ฒด ํ…์ŠคํŠธ (โ€ข ์ œ๊ฑฐ)
clean_point = point.replace('โ€ข', '').strip()
html += f"""
<input type="text" id="bullet_{slide_index}_{i}" value="{clean_point}" style="
width: 100%;
margin-bottom: 10px;
padding: 8px 8px 8px 25px;
border: 1px solid #e9ecef;
border-radius: 5px;
font-size: 16px;
" placeholder="์˜ˆ: ๐ŸŽฏ ํฌ์ธํŠธ {i+1}">
"""
html += f"""
</div>
<div style="
background: #f3e5f5;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
">
<h4 style="margin-bottom: 10px; color: #7b1fa2;">๐Ÿค– AI๋กœ ๋‹ค์‹œ ์ž‘์„ฑํ•˜๊ธฐ</h4>
<textarea id="instruction_{slide_index}" style="
width: 100%;
height: 60px;
padding: 10px;
border: 2px solid #e1bee7;
border-radius: 5px;
font-size: 14px;
resize: vertical;
background: white;
" placeholder="์˜ˆ: ๋” ์ „๋ฌธ์ ์œผ๋กœ, ์ˆซ์ž์™€ ํ†ต๊ณ„ ํฌํ•จ, ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ, ์ž„ํŒฉํŠธ ์žˆ๊ฒŒ"></textarea>
<button id="ai_btn_{slide_index}" style="
background: #9b59b6;
color: white;
border: none;
padding: 8px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
font-size: 14px;
">๐Ÿ”„ AI ์ƒˆ๋กœ ์ž‘์„ฑ</button>
</div>
<div style="margin-top: 20px;">
<button id="save_btn_{slide_index}" style="
background: #27ae60;
color: white;
border: none;
padding: 8px 20px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
">์ €์žฅ</button>
<button id="cancel_btn_{slide_index}" style="
background: #95a5a6;
color: white;
border: none;
padding: 8px 20px;
border-radius: 5px;
cursor: pointer;
">์ทจ์†Œ</button>
</div>
</div>
</div>
<!-- ์ด๋ฏธ์ง€ ์˜์—ญ (์šฐ์ธก) -->
<div class="image-area" style="
flex: 1;
background: rgba(248, 249, 250, 0.9);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
">
"""
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">
"""
html += f"""
</div>
</div>
</div>
</div>
<script>
// ์ฆ‰์‹œ ์‹คํ–‰ ํ•จ์ˆ˜๋กœ ์Šค์ฝ”ํ”„ ๋ถ„๋ฆฌ
(function() {{
const slideIndex = {slide_index};
// DOM์ด ์ค€๋น„๋˜๋ฉด ์‹คํ–‰
setTimeout(function() {{
const viewMode = document.getElementById('view_mode_' + slideIndex);
const editMode = document.getElementById('edit_mode_' + slideIndex);
const editBtn = document.getElementById('edit_btn_' + slideIndex);
const saveBtn = document.getElementById('save_btn_' + slideIndex);
const cancelBtn = document.getElementById('cancel_btn_' + slideIndex);
const aiBtn = document.getElementById('ai_btn_' + slideIndex);
if (editBtn) {{
editBtn.onclick = function(e) {{
e.preventDefault();
e.stopPropagation();
viewMode.style.display = 'none';
editMode.style.display = 'block';
return false;
}};
}}
if (saveBtn) {{
saveBtn.onclick = function(e) {{
e.preventDefault();
e.stopPropagation();
const subtitle = document.getElementById('subtitle_' + slideIndex).value;
const bullets = [];
for (let i = 0; i < 5; i++) {{
const bullet = document.getElementById('bullet_' + slideIndex + '_' + i);
if (bullet && bullet.value) {{
let bulletText = bullet.value.trim();
if (!bulletText.startsWith('โ€ข')) {{
bulletText = 'โ€ข ' + bulletText;
}}
bullets.push(bulletText);
}}
}}
window.savedSlideData = window.savedSlideData || {{}};
window.savedSlideData[slideIndex] = {{
subtitle: subtitle,
bullet_points: bullets
}};
// ๋ทฐ ๋ชจ๋“œ ์—…๋ฐ์ดํŠธ
const h2 = viewMode.querySelector('h2');
if (h2) h2.textContent = subtitle;
const ul = viewMode.querySelector('ul');
if (ul) {{
ul.innerHTML = bullets.map(b => `
<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>
${{b.replace('โ€ข', '').trim()}}
</li>
`).join('');
}}
viewMode.style.display = 'block';
editMode.style.display = 'none';
return false;
}};
}}
if (cancelBtn) {{
cancelBtn.onclick = function(e) {{
e.preventDefault();
e.stopPropagation();
viewMode.style.display = 'block';
editMode.style.display = 'none';
return false;
}};
}}
if (aiBtn) {{
aiBtn.onclick = function(e) {{
e.preventDefault();
e.stopPropagation();
const instruction = document.getElementById('instruction_' + slideIndex).value;
if (instruction) {{
window.aiRegenerateRequest = {{
slideIndex: slideIndex,
instruction: instruction,
topic: "{topic}"
}};
alert('AI ์žฌ์ƒ์„ฑ์ด ์š”์ฒญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค.');
}}
return false;
}};
}}
}}, 100);
}})();
</script>
"""
return html
def generate_ppt_with_content(topic: str, template_name: 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 = ""
# ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐ
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 ์ƒ์„ฑ] ๋””์ž์ธ ํ…Œ๋งˆ: {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)
content = {
"subtitle": conclusion_phrase,
"bullet_points": []
}
# Thank You ์Šฌ๋ผ์ด๋“œ์šฉ ํŠน๋ณ„ ๋…ธํŠธ
speaker_notes = generate_closing_notes(topic, conclusion_phrase)
else:
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 # ํ‘œ์ง€์šฉ
}
# ํ”„๋ฆฌ๋ทฐ HTML ์ƒ์„ฑ (ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค)
if slide_data.get('title') not in ['ํ‘œ์ง€', 'Thank You']:
preview_html += create_editable_slide_interface(slide_data, i, topic)
else:
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 style="margin: 40px auto; max-width: 1200px; text-align: center; padding: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3);">
<h3 style="color: white; margin-bottom: 20px;">โœจ ๋ชจ๋“  ํŽธ์ง‘์ด ์™„๋ฃŒ๋˜์—ˆ๋‚˜์š”?</h3>
<button id="finalDownloadBtn" onclick="prepareFinalDownload()" style="
background: white;
color: #667eea;
border: none;
padding: 18px 50px;
border-radius: 50px;
cursor: pointer;
font-size: 20px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
transition: all 0.3s ease;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
๐Ÿ’พ ํŽธ์ง‘ ๋‚ด์šฉ ๋ฐ˜์˜ํ•˜์—ฌ ์ตœ์ข… ๋‹ค์šด๋กœ๋“œ
</button>
<p style="color: white; margin-top: 15px; font-size: 14px;">
๐Ÿ’ก ํŽธ์ง‘ํ•œ ๋‚ด์šฉ์ด ์ตœ์ข… PPT์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค
</p>
</div>
<script>
function prepareFinalDownload() {
// ํŽธ์ง‘๋œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
alert('ํŽธ์ง‘๋œ ๋‚ด์šฉ์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… PPT๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.');
// ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” Gradio ์ด๋ฒคํŠธ๋กœ ์ฒ˜๋ฆฌ
}
</script>
"""
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_topic, current_template, current_theme
current_slides_data = results
current_topic = topic
current_template = template_name
current_theme = theme_name
if pptx_path:
final_status += f"\n\n### ๐Ÿ“ฅ PPTX ํŒŒ์ผ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค!"
final_status += f"\n\n๐Ÿ’ก **๊ฐ ์Šฌ๋ผ์ด๋“œ๋ฅผ ํŽธ์ง‘ํ•œ ํ›„ 'ํŽธ์ง‘ ์™„๋ฃŒ ํ›„ ์ตœ์ข… ๋‹ค์šด๋กœ๋“œ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”**"
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): # ์ตœ๋Œ€ 20์žฅ
with gr.Row(visible=(i < 3)): # ๊ธฐ๋ณธ 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
# 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; }
input[type="text"], textarea { transition: all 0.3s ease; }
input[type="text"]:focus, textarea:focus { border-color: #3498db !important; outline: none; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); }
button {
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
cursor: pointer !important;
user-select: none;
-webkit-user-select: none;
}
button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); opacity: 0.9; }
button:active { transform: translateY(0); }
.edit-mode { background: #f8f9fa; padding: 20px; border-radius: 8px; }
.ai-instruction { background: #f3e5f5; padding: 15px; border-radius: 8px; margin-top: 15px; }
""") as demo:
gr.Markdown("""
# ๐ŸŽฏ AI ๊ธฐ๋ฐ˜ PPT ํ†ตํ•ฉ ์ƒ์„ฑ๊ธฐ (๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ ๋ฒ„์ „)
### ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์กฐํ™”๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ํŽธ์ง‘ ํ›„ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”!
#### ๐Ÿ†• ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ:
- ๐Ÿ“ **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ํ…์ŠคํŠธ**: "~์ž„", "~ํ•จ" ์Šคํƒ€์ผ์˜ ํ”„๋กœํŽ˜์…”๋„ํ•œ ๋ฌธ์ฒด
- ๐ŸŽจ **์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€**: ๊ฐ ํฌ์ธํŠธ๋ณ„ ์ ์ ˆํ•œ ์ด๋ชจ์ง€๋กœ ์‹œ๊ฐ์  ๊ตฌ๋ถ„
- โœ๏ธ **์Šฌ๋ผ์ด๋“œ ํ…์ŠคํŠธ ํŽธ์ง‘**: ์ƒ์„ฑ๋œ ๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ํ…์ŠคํŠธ๋ฅผ ์ง์ ‘ ์ˆ˜์ •
- ๐Ÿค– **AI ์žฌ์ž‘์„ฑ**: ์ง€์‹œ์‚ฌํ•ญ์„ ์ž…๋ ฅํ•˜๋ฉด AI๊ฐ€ ํ…์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑ
- ๐Ÿ“‹ **์˜ˆ์ œ ํ…œํ”Œ๋ฆฟ**: ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์˜ˆ์ œ ์ฃผ์ œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
- ๐ŸŽจ **๋””์ž์ธ ํ…Œ๋งˆ**: 5๊ฐ€์ง€ ์ „๋ฌธ์ ์ธ ๋””์ž์ธ ํ…Œ๋งˆ ์„ ํƒ ๊ฐ€๋Šฅ
- ๐Ÿ’Ž **๊ฐœ์„ ๋œ ํ‘œ์ง€ ๋””์ž์ธ**: ๋ฐ˜ํˆฌ๋ช… ๋ฐฐ๊ฒฝ๊ณผ ์„ ๋ช…ํ•œ ํ…์ŠคํŠธ
- ๐Ÿ“Š **ํ‘œ์ง€์™€ Thank You ์Šฌ๋ผ์ด๋“œ** ์ž๋™ ์ถ”๊ฐ€
- ๐Ÿ“ **๋ฐœํ‘œ์ž ๋…ธํŠธ** ์ž๋™ ์ƒ์„ฑ (๊ตฌ์–ด์ฒด)
- ๐Ÿ“ **ํŒŒ์ผ ์—…๋กœ๋“œ** ์ง€์› (PDF/CSV/TXT)
- ๐Ÿ” **์›น ๊ฒ€์ƒ‰** ๊ธฐ๋Šฅ (Brave Search)
- ๐ŸŽš๏ธ **์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ ˆ** (6-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 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
)
# 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. **AI ์žฌ์ž‘์„ฑ**: ์ง€์‹œ์‚ฌํ•ญ ์ž…๋ ฅ ํ›„ AI๋กœ ๋‹ค์‹œ ์ž‘์„ฑ
5. **์ตœ์ข… ๋‹ค์šด๋กœ๋“œ**: ํŽธ์ง‘ ์™„๋ฃŒ ํ›„ ๋‹ค์šด๋กœ๋“œ
### ๐Ÿ’ก ํ…์ŠคํŠธ ์Šคํƒ€์ผ ํŠน์ง•:
- **๊ฐ„๊ฒฐํ•œ ๋ช…์‚ฌํ˜• ์ข…๊ฒฐ**: "~์ž„", "~ํ•จ" ๋˜๋Š” ๋ช…์‚ฌ๋กœ ๋๋‚จ
- **์ด๋ชจ์ง€ ํ™œ์šฉ**: ๊ฐ ํฌ์ธํŠธ ์•ž์— ๋‚ด์šฉ ๊ด€๋ จ ์ด๋ชจ์ง€ ์ž๋™ ์ถ”๊ฐ€
- **8-12๋‹จ์–ด**: ๊ฐ ๋ถˆ๋ฆฟ ํฌ์ธํŠธ๋Š” ๋งค์šฐ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌ์„ฑ
**์Šคํƒ€์ผ ์˜ˆ์‹œ:**
- โŒ "์‹œ์žฅ ์ ์œ ์œจ์„ ํ™•๋Œ€ํ•˜๊ธฐ ์œ„ํ•œ ์ „๋žต์„ ์ˆ˜๋ฆฝํ•ฉ๋‹ˆ๋‹ค"
- โœ… "๐ŸŽฏ ์‹œ์žฅ ์ ์œ ์œจ ํ™•๋Œ€ ์ „๋žต ์ˆ˜๋ฆฝ"
- โŒ "AI ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•˜์—ฌ ํšจ์œจ์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค"
- โœ… "๐Ÿš€ AI ๊ธฐ์ˆ  ํ™œ์šฉํ•œ ํšจ์œจ์„ฑ ๊ทน๋Œ€ํ™”"
### ๐Ÿค– AI ์žฌ์ž‘์„ฑ ํŒ:
- "๋” ๊ฐ„๋‹จํ•˜๊ฒŒ": ํ•ต์‹ฌ๋งŒ ๋‚จ๊ธฐ๊ณ  ์ถ•์•ฝ
- "์ˆซ์ž ํฌํ•จ": ๊ตฌ์ฒด์  ์ˆ˜์น˜๋‚˜ ํ†ต๊ณ„ ์ถ”๊ฐ€
- "์ „๋ฌธ์ ์œผ๋กœ": ์—…๊ณ„ ์ „๋ฌธ ์šฉ์–ด ์‚ฌ์šฉ
- "์ž„ํŒฉํŠธ ์žˆ๊ฒŒ": ๊ฐ•์กฐ์ ๊ณผ ํ•ต์‹ฌ ์„ฑ๊ณผ ๋ถ€๊ฐ
""")
# PPTX ๋‹ค์šด๋กœ๋“œ ์˜์—ญ
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 load_example(template_name):
"""ํ…œํ”Œ๋ฆฟ์— ๋งž๋Š” ์˜ˆ์ œ ์ฃผ์ œ ๋กœ๋“œ"""
example_topic = EXAMPLE_TOPICS.get(template_name, "")
return example_topic
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):
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)), # title
gr.update(visible=(i < count)), # style
gr.update(visible=(i < count)) # hint
])
return updates
def generate_ppt_handler(topic, template_name, 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, 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]
)
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.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 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,
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),
update_theme_preview(theme_select.value),
),
inputs=[],
outputs=[template_info, theme_preview],
)
# --- ์•ฑ ์‹คํ–‰ --------------------------------------------------------
if __name__ == "__main__":
demo.launch(ssr_mode=False)