GINI-Deck / app.py
ginipick's picture
Update app.py
7a610d2 verified
raw
history blame
70.3 kB
import gradio as gr
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
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
# ๋””์ž์ธ ํ…Œ๋งˆ ์ •์˜
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": "Thank You",
"description": "Elegant closing slide design",
"use_case": "๋งˆ์ง€๋ง‰ ์ธ์‚ฌ",
"example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for 'Thank You' text. Minimalist, professional, warm feeling"
},
"3D Style (Pixar-like)": {
"name": "3D Style",
"description": "Pixar-esque 3D render with volumetric lighting",
"use_case": "ํ‘œ์ง€, ๋น„์ „, ๋ฏธ๋ž˜ ์ปจ์…‰",
"example": "A fluffy ginger cat wearing a tiny spacesuit, floating amidst a vibrant nebula in a 3D render. The cat is gazing curiously at a swirling planet with rings made of candy. Background is filled with sparkling stars and colorful gas clouds, lit with soft, volumetric lighting. Style: Pixar-esque, highly detailed, playful. Colors: Deep blues, purples, oranges, and pinks. Rendered in Octane, 8k resolution."
},
"Elegant SWOT Quadrant": {
"name": "SWOT Analysis",
"description": "Flat-design 4-grid layout with minimal shadows",
"use_case": "ํ˜„ํ™ฉ ๋ถ„์„, ์ „๋žต ํ‰๊ฐ€",
"example": "Elegant SWOT quadrant: flat-design 4-grid on matte-white backdrop, thin pastel separators, top-left 'Strengths' panel shows glowing shield icon and subtle motif, top-right 'Weaknesses' panel with cracked chain icon in soft crimson, bottom-left 'Opportunities' panel with sunrise-over-horizon icon in optimistic teal, bottom-right 'Threats' panel with storm-cloud & lightning icon in deep indigo, minimal shadows, no text, no watermark, 16:9, 4K"
},
"Colorful Mind Map": {
"name": "Mind Map",
"description": "Hand-drawn educational style with vibrant colors",
"use_case": "๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ, ์•„์ด๋””์–ด ์ •๋ฆฌ",
"example": "A handrawn colorful mind map diagram: educational style, vibrant colors, clear hierarchy, golden ratio layout. Central concept with branching sub-topics, each branch with unique color coding, organic flowing connections, doodle-style icons for each node"
},
"Business Workflow": {
"name": "Business Process",
"description": "End-to-end business workflow with clear phases",
"use_case": "ํ”„๋กœ์„ธ์Šค ์„ค๋ช…, ๋‹จ๊ณ„๋ณ„ ์ง„ํ–‰",
"example": "A detailed hand-drawn diagram illustrating an end-to-end business workflow with Market Analysis, Strategy Development, Product Design, Implementation, and Post-Launch Review phases. Clear directional arrows, iconography for each component, vibrant educational yet professional style"
},
"Industrial Design": {
"name": "Product Design",
"description": "Sleek industrial design concept sketch",
"use_case": "์ œํ’ˆ ์†Œ๊ฐœ, ์ปจ์…‰ ๋””์ž์ธ",
"example": "A sleek industrial design concept: Curved metallic body with minimal bezel, Touchscreen panel for settings, Modern matte black finish, Hand-drawn concept sketch style with annotations and dimension lines"
},
"3D Bubble Chart": {
"name": "Bubble Chart",
"description": "Clean 3D bubble visualization",
"use_case": "๋น„๊ต ๋ถ„์„, ํฌ์ง€์…”๋‹",
"example": "3-D bubble chart on clean white 2ร—2 grid, quadrant titles hidden, four translucent spheres in lime, azure, amber, magenta, gentle depth-of-field, modern consulting aesthetic, no text, 4K"
},
"Timeline Ribbon": {
"name": "Timeline",
"description": "Horizontal ribbon timeline with cyber-futuristic vibe",
"use_case": "์ผ์ •, ๋กœ๋“œ๋งต, ๋งˆ์ผ์Šคํ†ค",
"example": "Horizontal ribbon timeline, milestone pins glowing hot pink on charcoal, year markers as circles, faint motion streaks, cyber-futuristic vibe, no text, 1920ร—1080"
},
"Risk Heat Map": {
"name": "Heat Map",
"description": "Risk assessment heat map with gradient colors",
"use_case": "๋ฆฌ์Šคํฌ ๋ถ„์„, ์šฐ์„ ์ˆœ์œ„",
"example": "Risk Heat Map: square grid, smooth gradient from mint to fire-red, cells beveled, simple legend strip hidden, long subtle shadow, sterile white frame, no text"
},
"Pyramid/Funnel": {
"name": "Funnel Chart",
"description": "Multi-layer gradient funnel visualization",
"use_case": "๋‹จ๊ณ„๋ณ„ ์ถ•์†Œ, ํ•ต์‹ฌ ๋„์ถœ",
"example": "Pyramid / Funnel: 5-layer gradient funnel narrowing downwards, top vivid sky-blue, mid mint-green, bottom sunset-orange, glass reflection, minimal background, no text"
},
"KPI Dashboard": {
"name": "Dashboard",
"description": "Dark-mode analytics dashboard with sci-fi interface",
"use_case": "์„ฑ๊ณผ ์ง€ํ‘œ, ์‹ค์  ๋Œ€์‹œ๋ณด๋“œ",
"example": "KPI Dashboard: Dark-mode analytic dashboard, three glass speedometers glowing neon lime, two sparkline charts under, black glass background, sci-fi interface, no text, 4K"
},
"Value Chain": {
"name": "Value Chain",
"description": "Horizontal value chain with industrial look",
"use_case": "๊ฐ€์น˜ ์‚ฌ์Šฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ",
"example": "Value Chain Diagram: Horizontal value chain blocks, steel-blue gradient bars with subtle bevel, small gear icons above each segment, sleek industrial look, shadow cast, no text"
},
"Gantt Chart": {
"name": "Gantt Chart",
"description": "Hand-drawn style Gantt chart with playful colors",
"use_case": "ํ”„๋กœ์ ํŠธ ์ผ์ •, ์ž‘์—… ๊ด€๋ฆฌ",
"example": "Gantt Chart: Hand-drawn style Gantt bars sketched with vibrant markers on dotted grid notebook page, sticky-note color palette, playful yet organized, perspective tilt, no text"
},
"Mobile App Mockup": {
"name": "App Mockup",
"description": "Clean wireframe for mobile app design",
"use_case": "์•ฑ/์›น UI, ํ™”๋ฉด ์„ค๊ณ„",
"example": "MOCKUP DESIGN: A clean hand-drawn style wireframe for a mobile app with Title screen, Login screen, Dashboard with sections, Bottom navigation bar, minimalist design with annotations"
},
"Flowchart": {
"name": "Flowchart",
"description": "Vibrant flowchart with minimalistic icons",
"use_case": "์˜์‚ฌ๊ฒฐ์ •, ํ”„๋กœ์„ธ์Šค ํ๋ฆ„",
"example": "FLOWCHART DESIGN: A hand-drawn style flowchart, vibrant colors, minimalistic icons showing process flow from START to END with decision points, branches, and clear directional arrows"
}
}
# PPT ํ…œํ”Œ๋ฆฟ ์ •์˜ (๋™์ ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ ์ˆ˜ ์กฐ์ • ๊ฐ€๋Šฅ)
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.
Create speaker notes that:
1. Sound natural and conversational, as if speaking to an audience
2. Include transitions and engagement phrases
3. Reference the slide content but expand with additional context
4. Use a warm, professional tone
5. Be 100-150 words long
6. Include pauses and emphasis markers where appropriate
Format:
- Use conversational language
- Include transition phrases
- Add engagement questions or comments
- Keep it professional but friendly"""
bullet_text = "\n".join(content.get("bullet_points", []))
user_message = f"""Topic: {topic}
Slide Title: {slide_title}
Subtitle: {content.get('subtitle', '')}
Key Points:
{bullet_text}
Create natural speaker notes for presenting this slide."""
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_message
}
],
"max_tokens": 300,
"temperature": 0.8,
"stream": False
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()
notes = result['choices'][0]['message']['content'].strip()
# ํ•œ๊ธ€ ์ฃผ์ œ์ธ ๊ฒฝ์šฐ ๋ฒˆ์—ญ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
notes = translate_content_to_korean(notes)
return notes
else:
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
except Exception as e:
print(f"[๋ฐœํ‘œ์ž ๋…ธํŠธ] ์˜ค๋ฅ˜: {str(e)}")
return "์ด ์Šฌ๋ผ์ด๋“œ์—์„œ๋Š” ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”."
def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
"""๊ฐ ์Šฌ๋ผ์ด๋“œ์˜ ํ…์ŠคํŠธ ๋‚ด์šฉ ์ƒ์„ฑ"""
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] {slide_title} ํ…์ŠคํŠธ ์ƒ์„ฑ ์ค‘...")
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content.
Your task is to create:
1. A compelling subtitle (max 10 words)
2. Exactly 5 bullet points, each being a complete, concise sentence
3. Each bullet point should be 10-15 words
Guidelines:
- Be specific and actionable
- Use professional business language
- Include relevant data points or metrics when appropriate
- Ensure content aligns with the slide's purpose
- Make each point distinct and valuable
- Use active voice and strong verbs
Output format:
Subtitle: [subtitle here]
โ€ข [Point 1]
โ€ข [Point 2]
โ€ข [Point 3]
โ€ข [Point 4]
โ€ข [Point 5]"""
user_message = f"""Topic: {topic}
Slide Title: {slide_title}
Context: {slide_context}"""
# ์—…๋กœ๋“œ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
if uploaded_content:
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
# ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
if web_search_results:
search_context = "\n\nWeb Search Results:\n"
for result in web_search_results[:3]:
search_context += f"- {result['title']}: {result['description']}\n"
user_message += search_context
user_message += "\n\nCreate compelling content for this presentation slide."
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_message
}
],
"max_tokens": 300,
"top_p": 0.8,
"temperature": 0.7,
"stream": False
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()
content = result['choices'][0]['message']['content'].strip()
# Parse content
lines = content.split('\n')
subtitle = ""
bullet_points = []
for line in lines:
if line.startswith("Subtitle:"):
subtitle = line.replace("Subtitle:", "").strip()
elif line.strip().startswith("โ€ข"):
bullet_points.append(line.strip())
# ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
if any(ord('๊ฐ€') <= ord(char) <= ord('ํžฃ') for char in topic):
subtitle = translate_content_to_korean(subtitle)
bullet_points = [translate_content_to_korean(point) for point in bullet_points]
return {
"subtitle": subtitle,
"bullet_points": bullet_points[:5] # ์ตœ๋Œ€ 5๊ฐœ
}
else:
return {
"subtitle": slide_title,
"bullet_points": ["๋‚ด์šฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."] * 5
}
except Exception as e:
print(f"[์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ] ์˜ค๋ฅ˜: {str(e)}")
return {
"subtitle": slide_title,
"bullet_points": ["๋‚ด์šฉ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."] * 5
}
def translate_content_to_korean(text: str) -> str:
"""์˜์–ด ํ…์ŠคํŠธ๋ฅผ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญ"""
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
headers = {
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"model": "dep89a2fld32mcm",
"messages": [
{
"role": "system",
"content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation."
},
{
"role": "user",
"content": text
}
],
"max_tokens": 200,
"top_p": 0.8,
"stream": False
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content'].strip()
else:
return text
except Exception as e:
return text
def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, 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;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
">
<h1 style="font-size: 48px; margin-bottom: 20px;">{slide_data.get('topic', '')}</h1>
<h2 style="font-size: 24px;">{subtitle}</h2>
</div>
"""
html += """
</div>
"""
else:
# ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ๋ ˆ์ด์•„์›ƒ
html += f"""
<!-- ํ…์ŠคํŠธ ์˜์—ญ (์ขŒ์ธก) -->
<div class="text-area" style="
flex: 1;
padding: 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:
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>
{point.replace('โ€ข', '').strip()}
</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_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.5),
Inches(12), Inches(4)
)
title_bg.fill.solid()
title_bg.fill.fore_color.rgb = RGBColor(0, 0, 0)
title_bg.fill.transparency = 0.5
title_bg.line.fill.background()
# ์ œ๋ชฉ ํ…์ŠคํŠธ ์ถ”๊ฐ€
title_box = slide.shapes.add_textbox(
Inches(2), Inches(3),
Inches(12), Inches(3)
)
title_frame = title_box.text_frame
title_frame.text = topic
title_para = title_frame.paragraphs[0]
title_para.font.size = Pt(48)
title_para.font.bold = True
title_para.font.color.rgb = RGBColor(255, 255, 255)
title_para.alignment = PP_ALIGN.CENTER
# ๋ถ€์ œ๋ชฉ ์ถ”๊ฐ€
subtitle_box = slide.shapes.add_textbox(
Inches(2), Inches(6),
Inches(12), Inches(2)
)
subtitle_frame = subtitle_box.text_frame
subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI ํ”„๋ ˆ์  ํ…Œ์ด์…˜')
subtitle_para = subtitle_frame.paragraphs[0]
subtitle_para.font.size = Pt(24)
subtitle_para.font.color.rgb = RGBColor(255, 255, 255)
subtitle_para.alignment = PP_ALIGN.CENTER
# 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(3), Inches(3),
Inches(10), Inches(3)
)
thanks_bg.fill.solid()
thanks_bg.fill.fore_color.rgb = RGBColor(0, 0, 0)
thanks_bg.fill.transparency = 0.5
thanks_bg.line.fill.background()
# Thank You ํ…์ŠคํŠธ
thanks_box = slide.shapes.add_textbox(
Inches(2), Inches(3.5),
Inches(12), Inches(2)
)
thanks_frame = thanks_box.text_frame
thanks_frame.text = "Thank You"
thanks_para = thanks_frame.paragraphs[0]
thanks_para.font.size = Pt(60)
thanks_para.font.bold = True
thanks_para.font.color.rgb = RGBColor(255, 255, 255)
thanks_para.alignment = PP_ALIGN.CENTER
# ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ
else:
# ๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ •
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()
p.text = point.replace('โ€ข', '').strip()
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.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 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', '')}"
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 ์ƒ์„ฑ
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}๊ฐœ ์„ฑ๊ณต"
if pptx_path:
final_status += f"\n\n### ๐Ÿ“ฅ PPTX ํŒŒ์ผ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์•„๋ž˜์—์„œ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”."
final_status += f"\n\n๐Ÿ’ก **๋ฐœํ‘œ์ž ๋…ธํŠธ๊ฐ€ ๊ฐ ์Šฌ๋ผ์ด๋“œ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!**"
yield preview_html, final_status, pptx_path
def create_custom_slides_ui():
"""์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ผ์ด๋“œ ๊ตฌ์„ฑ UI (3-20์žฅ)"""
slides = []
for i in range(20): # ์ตœ๋Œ€ 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; }
""") as demo:
gr.Markdown("""
# ๐ŸŽฏ AI ๊ธฐ๋ฐ˜ PPT ํ†ตํ•ฉ ์ƒ์„ฑ๊ธฐ (๋””์ž์ธ ํ…Œ๋งˆ ํฌํ•จ)
### ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์กฐํ™”๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”!
#### ๐Ÿ†• ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ:
- ๐ŸŽจ **๋””์ž์ธ ํ…Œ๋งˆ**: 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):
# ๊ธฐ๋ณธ ์ž…๋ ฅ
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 ์ „์ฒด ์ƒ์„ฑ (ํ…์ŠคํŠธ + ์ด๋ฏธ์ง€ + ๋ฐœํ‘œ๋…ธํŠธ)", variant="primary", size="lg")
# 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 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
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
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_components in custom_slides_components:
all_custom_inputs.extend([
slide_components["title"],
slide_components["style"],
slide_components["hint"]
])
generate_btn.click(
fn=generate_ppt_handler,
inputs=[topic_input, template_select, 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("๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ˆ์„œ", 8),
update_theme_preview("๋ฏธ๋‹ˆ๋ฉ€ ๋ผ์ดํŠธ")
),
inputs=[],
outputs=[template_info, theme_preview]
)
# ์•ฑ ์‹คํ–‰
if __name__ == "__main__":
print("\n" + "="*50)
print("๐Ÿš€ PPT ํ†ตํ•ฉ ์ƒ์„ฑ๊ธฐ (๋””์ž์ธ ํ…Œ๋งˆ ๋ฒ„์ „) ์‹œ์ž‘!")
print("="*50)
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ
if not REPLICATE_API_TOKEN:
print("โš ๏ธ ๊ฒฝ๊ณ : RAPI_TOKEN ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
else:
print("โœ… RAPI_TOKEN ํ™•์ธ๋จ")
if not FRIENDLI_TOKEN:
print("โš ๏ธ ๊ฒฝ๊ณ : FRIENDLI_TOKEN ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
else:
print("โœ… FRIENDLI_TOKEN ํ™•์ธ๋จ")
if not BRAVE_API_TOKEN:
print("โ„น๏ธ BAPI_TOKEN์ด ์—†์–ด ์›น ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
else:
print("โœ… BAPI_TOKEN ํ™•์ธ๋จ")
print("="*50 + "\n")
# SSR ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ์‹คํ–‰
demo.launch(ssr_mode=False)