|
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_TEMPLATES = { |
|
"๋น์ฆ๋์ค ์ ์์": { |
|
"description": "ํฌ์ ์ ์น, ์ฌ์
์ ์์ฉ", |
|
"core_slides": [ |
|
{"title": "๋ชฉ์ฐจ", "style": "Flowchart", "prompt_hint": "ํ๋ ์ ํ
์ด์
๊ตฌ์กฐ"}, |
|
{"title": "๋ฌธ์ ์ ์", "style": "Colorful Mind Map", "prompt_hint": "ํ์ฌ ์์ฅ์ ๋ฌธ์ ์ "}, |
|
{"title": "ํํฉ ๋ถ์", "style": "Elegant SWOT Quadrant", "prompt_hint": "๊ฐ์ , ์ฝ์ , ๊ธฐํ, ์ํ"}, |
|
{"title": "์๋ฃจ์
", "style": "Industrial Design", "prompt_hint": "์ ํ/์๋น์ค ์ปจ์
"}, |
|
{"title": "ํ๋ก์ธ์ค", "style": "Business Workflow", "prompt_hint": "์คํ ๋จ๊ณ"}, |
|
{"title": "์ผ์ ", "style": "Timeline Ribbon", "prompt_hint": "์ฃผ์ ๋ง์ผ์คํค"} |
|
], |
|
"optional_slides": [ |
|
{"title": "์์ฅ ๊ท๋ชจ", "style": "3D Bubble Chart", "prompt_hint": "์์ฅ ๊ธฐํ์ ์ฑ์ฅ์ฑ"}, |
|
{"title": "๊ฒฝ์ ๋ถ์", "style": "Risk Heat Map", "prompt_hint": "๊ฒฝ์์ฌ ํฌ์ง์
๋"}, |
|
{"title": "๋น์ฆ๋์ค ๋ชจ๋ธ", "style": "Value Chain", "prompt_hint": "์์ต ๊ตฌ์กฐ"}, |
|
{"title": "ํ ์๊ฐ", "style": "Colorful Mind Map", "prompt_hint": "ํต์ฌ ํ์๊ณผ ์ญ๋"}, |
|
{"title": "์ฌ๋ฌด ๊ณํ", "style": "KPI Dashboard", "prompt_hint": "์์ ๋งค์ถ๊ณผ ์์ต"}, |
|
{"title": "์ํ ๊ด๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ฃผ์ ๋ฆฌ์คํฌ์ ๋์"}, |
|
{"title": "ํํธ๋์ญ", "style": "Business Workflow", "prompt_hint": "์ ๋ต์ ์ ํด"}, |
|
{"title": "๊ธฐ์ ์คํ", "style": "Flowchart", "prompt_hint": "ํต์ฌ ๊ธฐ์ ๊ตฌ์กฐ"}, |
|
{"title": "๊ณ ๊ฐ ์ฌ๋ก", "style": "Industrial Design", "prompt_hint": "์ฑ๊ณต ์ฌ๋ก"}, |
|
{"title": "์ฑ์ฅ ์ ๋ต", "style": "Timeline Ribbon", "prompt_hint": "ํ์ฅ ๊ณํ"}, |
|
{"title": "ํฌ์ ํ์ฉ", "style": "Pyramid/Funnel", "prompt_hint": "์๊ธ ์ฌ์ฉ ๊ณํ"}, |
|
{"title": "Exit ์ ๋ต", "style": "Timeline Ribbon", "prompt_hint": "์ถ๊ตฌ ์ ๋ต"} |
|
] |
|
}, |
|
"์ ํ ์๊ฐ": { |
|
"description": "์ ์ ํ ๋ฐ์นญ, ์๋น์ค ์๊ฐ์ฉ", |
|
"core_slides": [ |
|
{"title": "์ ํ ์ปจ์
", "style": "Industrial Design", "prompt_hint": "์ ํ ๋์์ธ"}, |
|
{"title": "์ฌ์ฉ์ ๋์ฆ", "style": "Colorful Mind Map", "prompt_hint": "๊ณ ๊ฐ ํ์ธํฌ์ธํธ"}, |
|
{"title": "๊ธฐ๋ฅ ์๊ฐ", "style": "Mobile App Mockup", "prompt_hint": "UI/UX ํ๋ฉด"}, |
|
{"title": "์๋ ์๋ฆฌ", "style": "Flowchart", "prompt_hint": "๊ธฐ๋ฅ ํ๋ก์ฐ"}, |
|
{"title": "์์ฅ ํฌ์ง์
", "style": "3D Bubble Chart", "prompt_hint": "๊ฒฝ์์ฌ ๋น๊ต"}, |
|
{"title": "์ถ์ ์ผ์ ", "style": "Timeline Ribbon", "prompt_hint": "๋ฐ์นญ ๋ก๋๋งต"} |
|
], |
|
"optional_slides": [ |
|
{"title": "ํ๊ฒ ๊ณ ๊ฐ", "style": "Colorful Mind Map", "prompt_hint": "์ฃผ์ ๊ณ ๊ฐ์ธต"}, |
|
{"title": "๊ฐ๊ฒฉ ์ ์ฑ
", "style": "Pyramid/Funnel", "prompt_hint": "๊ฐ๊ฒฉ ์ ๋ต"}, |
|
{"title": "๊ธฐ์ ์ฐ์", "style": "Industrial Design", "prompt_hint": "ํต์ฌ ๊ธฐ์ "}, |
|
{"title": "์ฌ์ฉ ์๋๋ฆฌ์ค", "style": "Business Workflow", "prompt_hint": "ํ์ฉ ์ฌ๋ก"}, |
|
{"title": "๊ณ ๊ฐ ํ๊ธฐ", "style": "KPI Dashboard", "prompt_hint": "์ฌ์ฉ์ ํ๊ฐ"}, |
|
{"title": "ํ๋งค ์ฑ๋", "style": "Value Chain", "prompt_hint": "์ ํต ์ ๋ต"}, |
|
{"title": "๋ง์ผํ
์ ๋ต", "style": "Timeline Ribbon", "prompt_hint": "ํ๋ณด ๊ณํ"}, |
|
{"title": "์ฑ๋ฅ ๋น๊ต", "style": "3D Bubble Chart", "prompt_hint": "๋ฒค์น๋งํฌ"} |
|
] |
|
}, |
|
"ํ๋ก์ ํธ ๋ณด๊ณ ": { |
|
"description": "์งํ ์ํฉ, ์ฑ๊ณผ ๋ณด๊ณ ์ฉ", |
|
"core_slides": [ |
|
{"title": "ํ๋ก์ ํธ ๊ฐ์", "style": "Business Workflow", "prompt_hint": "์ ์ฒด ํ๋ก์ธ์ค"}, |
|
{"title": "์งํ ํํฉ", "style": "Gantt Chart", "prompt_hint": "์์
์ผ์ "}, |
|
{"title": "๋ฆฌ์คํฌ ๊ด๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ํ ์์"}, |
|
{"title": "์ฑ๊ณผ ์งํ", "style": "KPI Dashboard", "prompt_hint": "๋ฌ์ฑ ์ค์ "}, |
|
{"title": "ํฅํ ๊ณํ", "style": "Timeline Ribbon", "prompt_hint": "๋ค์ ๋จ๊ณ"} |
|
], |
|
"optional_slides": [ |
|
{"title": "์์ฐ ํํฉ", "style": "Pyramid/Funnel", "prompt_hint": "์์ฐ ์งํ"}, |
|
{"title": "ํ ์ฑ๊ณผ", "style": "3D Bubble Chart", "prompt_hint": "ํ๋ณ ๊ธฐ์ฌ๋"}, |
|
{"title": "์ด์ ๊ด๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ฃผ์ ์ด์"}, |
|
{"title": "๊ฐ์ ์ฌํญ", "style": "Colorful Mind Map", "prompt_hint": "ํ๋ก์ธ์ค ๊ฐ์ "}, |
|
{"title": "๊ตํ", "style": "Business Workflow", "prompt_hint": "๋ฐฐ์ด ์ "} |
|
] |
|
}, |
|
"์ ๋ต ๊ธฐํ": { |
|
"description": "์ค์ฅ๊ธฐ ์ ๋ต, ๋น์ ์๋ฆฝ์ฉ", |
|
"core_slides": [ |
|
{"title": "๋น์ ", "style": "3D Style (Pixar-like)", "prompt_hint": "๋ฏธ๋ ๋น์ "}, |
|
{"title": "ํ๊ฒฝ ๋ถ์", "style": "Elegant SWOT Quadrant", "prompt_hint": "๋ด์ธ๋ถ ํ๊ฒฝ"}, |
|
{"title": "์ ๋ต ์ฒด๊ณ", "style": "Colorful Mind Map", "prompt_hint": "์ ๋ต ๊ตฌ์กฐ"}, |
|
{"title": "๊ฐ์น ์ฌ์ฌ", "style": "Value Chain", "prompt_hint": "๋น์ฆ๋์ค ๋ชจ๋ธ"}, |
|
{"title": "์คํ ๋ก๋๋งต", "style": "Timeline Ribbon", "prompt_hint": "๋จ๊ณ๋ณ ๊ณํ"}, |
|
{"title": "๋ชฉํ ์งํ", "style": "KPI Dashboard", "prompt_hint": "KPI ๋ชฉํ"} |
|
], |
|
"optional_slides": [ |
|
{"title": "์์ฅ ์ ๋ง", "style": "3D Bubble Chart", "prompt_hint": "๋ฏธ๋ ์์ฅ"}, |
|
{"title": "ํ์ ๋ฐฉํฅ", "style": "Industrial Design", "prompt_hint": "ํ์ ์ ๋ต"}, |
|
{"title": "์กฐ์ง ๋ณํ", "style": "Business Workflow", "prompt_hint": "์กฐ์ง ๊ฐํธ"}, |
|
{"title": "๋์งํธ ์ ํ", "style": "Flowchart", "prompt_hint": "DX ์ ๋ต"}, |
|
{"title": "์ง์๊ฐ๋ฅ์ฑ", "style": "Timeline Ribbon", "prompt_hint": "ESG ์ ๋ต"} |
|
] |
|
}, |
|
"์ฌ์ฉ์ ์ ์": { |
|
"description": "์ง์ ๊ตฌ์ฑํ๊ธฐ", |
|
"core_slides": [], |
|
"optional_slides": [] |
|
} |
|
} |
|
|
|
def brave_search(query: str) -> List[Dict]: |
|
"""Brave Search API๋ฅผ ์ฌ์ฉํ ์น ๊ฒ์""" |
|
if not BRAVE_API_TOKEN: |
|
print("[Brave Search] API ํ ํฐ์ด ์์ด ๊ฒ์์ ๊ฑด๋๋๋๋ค.") |
|
return [] |
|
|
|
print(f"[Brave Search] ๊ฒ์์ด: {query}") |
|
|
|
headers = { |
|
"Accept": "application/json", |
|
"X-Subscription-Token": BRAVE_API_TOKEN |
|
} |
|
|
|
params = { |
|
"q": query, |
|
"count": 5 |
|
} |
|
|
|
try: |
|
response = requests.get( |
|
"https://api.search.brave.com/res/v1/web/search", |
|
headers=headers, |
|
params=params, |
|
timeout=10 |
|
) |
|
|
|
if response.status_code == 200: |
|
data = response.json() |
|
results = [] |
|
for item in data.get("web", {}).get("results", [])[:3]: |
|
results.append({ |
|
"title": item.get("title", ""), |
|
"description": item.get("description", ""), |
|
"url": item.get("url", "") |
|
}) |
|
print(f"[Brave Search] {len(results)}๊ฐ ๊ฒฐ๊ณผ ํ๋") |
|
return results |
|
else: |
|
print(f"[Brave Search] ์ค๋ฅ: {response.status_code}") |
|
return [] |
|
except Exception as e: |
|
print(f"[Brave Search] ์์ธ: {str(e)}") |
|
return [] |
|
|
|
def read_uploaded_file(file_path: str) -> str: |
|
"""์
๋ก๋๋ ํ์ผ ์ฝ๊ธฐ (PDF, CSV, TXT)""" |
|
print(f"[ํ์ผ ์ฝ๊ธฐ] {file_path}") |
|
|
|
try: |
|
|
|
ext = os.path.splitext(file_path)[1].lower() |
|
|
|
if ext == '.pdf': |
|
|
|
with open(file_path, 'rb') as file: |
|
pdf_reader = PyPDF2.PdfReader(file) |
|
text = "" |
|
for page in pdf_reader.pages: |
|
text += page.extract_text() + "\n" |
|
return text[:5000] |
|
|
|
elif ext == '.csv': |
|
|
|
|
|
with open(file_path, 'rb') as file: |
|
raw_data = file.read() |
|
result = chardet.detect(raw_data) |
|
encoding = result['encoding'] or 'utf-8' |
|
|
|
df = pd.read_csv(file_path, encoding=encoding) |
|
return f"CSV ๋ฐ์ดํฐ:\n{df.head(20).to_string()}\n\n์์ฝ: {len(df)} ํ, {len(df.columns)} ์ด" |
|
|
|
elif ext in ['.txt', '.text']: |
|
|
|
with open(file_path, 'rb') as file: |
|
raw_data = file.read() |
|
result = chardet.detect(raw_data) |
|
encoding = result['encoding'] or 'utf-8' |
|
|
|
with open(file_path, 'r', encoding=encoding) as file: |
|
return file.read()[:5000] |
|
else: |
|
return "์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค." |
|
|
|
except Exception as e: |
|
return f"ํ์ผ ์ฝ๊ธฐ ์ค๋ฅ: {str(e)}" |
|
|
|
def generate_presentation_notes(topic: str, slide_title: str, content: Dict) -> str: |
|
"""๊ฐ ์ฌ๋ผ์ด๋์ ๋ฐํ์ ๋
ธํธ ์์ฑ (๊ตฌ์ด์ฒด)""" |
|
print(f"[๋ฐํ์ ๋
ธํธ] {slide_title} ์์ฑ ์ค...") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = """You are a professional presentation coach who creates natural, conversational speaker notes. |
|
|
|
Create speaker notes that: |
|
1. Sound natural and conversational, as if speaking to an audience |
|
2. Include transitions and engagement phrases |
|
3. Reference the slide content but expand with additional context |
|
4. Use a warm, professional tone |
|
5. Be 100-150 words long |
|
6. Include pauses and emphasis markers where appropriate |
|
|
|
Format: |
|
- Use conversational language |
|
- Include transition phrases |
|
- Add engagement questions or comments |
|
- Keep it professional but friendly""" |
|
|
|
bullet_text = "\n".join(content.get("bullet_points", [])) |
|
user_message = f"""Topic: {topic} |
|
Slide Title: {slide_title} |
|
Subtitle: {content.get('subtitle', '')} |
|
Key Points: |
|
{bullet_text} |
|
|
|
Create natural speaker notes for presenting this slide.""" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"temperature": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
notes = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
notes = translate_content_to_korean(notes) |
|
|
|
return notes |
|
else: |
|
return "์ด ์ฌ๋ผ์ด๋์์๋ ํต์ฌ ๋ด์ฉ์ ์ค๋ช
ํด ์ฃผ์ธ์." |
|
except Exception as e: |
|
print(f"[๋ฐํ์ ๋
ธํธ] ์ค๋ฅ: {str(e)}") |
|
return "์ด ์ฌ๋ผ์ด๋์์๋ ํต์ฌ ๋ด์ฉ์ ์ค๋ช
ํด ์ฃผ์ธ์." |
|
|
|
def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]: |
|
"""๊ฐ ์ฌ๋ผ์ด๋์ ํ
์คํธ ๋ด์ฉ ์์ฑ""" |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] {slide_title} ํ
์คํธ ์์ฑ ์ค...") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = """You are a professional presentation content writer specializing in creating concise, impactful slide content. |
|
|
|
Your task is to create: |
|
1. A compelling subtitle (max 10 words) |
|
2. Exactly 5 bullet points, each being a complete, concise sentence |
|
3. Each bullet point should be 10-15 words |
|
|
|
Guidelines: |
|
- Be specific and actionable |
|
- Use professional business language |
|
- Include relevant data points or metrics when appropriate |
|
- Ensure content aligns with the slide's purpose |
|
- Make each point distinct and valuable |
|
- Use active voice and strong verbs |
|
|
|
Output format: |
|
Subtitle: [subtitle here] |
|
โข [Point 1] |
|
โข [Point 2] |
|
โข [Point 3] |
|
โข [Point 4] |
|
โข [Point 5]""" |
|
|
|
user_message = f"""Topic: {topic} |
|
Slide Title: {slide_title} |
|
Context: {slide_context}""" |
|
|
|
|
|
if uploaded_content: |
|
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}" |
|
|
|
|
|
if web_search_results: |
|
search_context = "\n\nWeb Search Results:\n" |
|
for result in web_search_results[:3]: |
|
search_context += f"- {result['title']}: {result['description']}\n" |
|
user_message += search_context |
|
|
|
user_message += "\n\nCreate compelling content for this presentation slide." |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"top_p": 0.8, |
|
"temperature": 0.7, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
content = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
lines = content.split('\n') |
|
subtitle = "" |
|
bullet_points = [] |
|
|
|
for line in lines: |
|
if line.startswith("Subtitle:"): |
|
subtitle = line.replace("Subtitle:", "").strip() |
|
elif line.strip().startswith("โข"): |
|
bullet_points.append(line.strip()) |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
subtitle = translate_content_to_korean(subtitle) |
|
bullet_points = [translate_content_to_korean(point) for point in bullet_points] |
|
|
|
return { |
|
"subtitle": subtitle, |
|
"bullet_points": bullet_points[:5] |
|
} |
|
else: |
|
return { |
|
"subtitle": slide_title, |
|
"bullet_points": ["๋ด์ฉ์ ์์ฑํ ์ ์์ต๋๋ค."] * 5 |
|
} |
|
except Exception as e: |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] ์ค๋ฅ: {str(e)}") |
|
return { |
|
"subtitle": slide_title, |
|
"bullet_points": ["๋ด์ฉ์ ์์ฑํ ์ ์์ต๋๋ค."] * 5 |
|
} |
|
|
|
def translate_content_to_korean(text: str) -> str: |
|
"""์์ด ํ
์คํธ๋ฅผ ํ๊ธ๋ก ๋ฒ์ญ""" |
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation." |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 200, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
return result['choices'][0]['message']['content'].strip() |
|
else: |
|
return text |
|
except Exception as e: |
|
return text |
|
|
|
def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str: |
|
"""์ฃผ์ ์ ์คํ์ผ ์์ ๋ฅผ ๋ฐ์์ LLM์ ์ฌ์ฉํด ์ด๋ฏธ์ง ํ๋กฌํํธ๋ฅผ ์์ฑ""" |
|
print(f"[LLM] ํ๋กฌํํธ ์์ฑ ์์: {slide_context}") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides. |
|
|
|
Your task is to create prompts that: |
|
1. Are highly specific and visual, perfect for PPT backgrounds or main visuals |
|
2. Consider the slide's purpose and maintain consistency across a presentation |
|
3. Include style references matching the given example |
|
4. Focus on clean, professional visuals that won't distract from text overlays |
|
5. Ensure high contrast areas for text readability when needed |
|
6. Maintain brand consistency and professional aesthetics |
|
|
|
Important guidelines: |
|
- If given a style example, adapt the topic to match that specific visual style |
|
- Consider the slide context (e.g., "ํ์ง", "ํํฉ ๋ถ์") to create appropriate visuals |
|
- Always output ONLY the prompt without any explanation |
|
- Keep prompts between 50-150 words for optimal results |
|
- Ensure the visual supports rather than overwhelms the slide content""" |
|
|
|
user_message = f"Topic: {topic}" |
|
if style_example: |
|
user_message += f"\n\nStyle reference to follow:\n{style_example}" |
|
if slide_context: |
|
user_message += f"\n\nSlide context: {slide_context}" |
|
if uploaded_content: |
|
user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"top_p": 0.8, |
|
"temperature": 0.7, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
prompt = result['choices'][0]['message']['content'].strip() |
|
print(f"[LLM] ํ๋กฌํํธ ์์ฑ ์๋ฃ: {prompt[:50]}...") |
|
return prompt |
|
else: |
|
error_msg = f"ํ๋กฌํํธ ์์ฑ ์คํจ: {response.status_code}" |
|
print(f"[LLM] {error_msg}") |
|
return error_msg |
|
except Exception as e: |
|
error_msg = f"ํ๋กฌํํธ ์์ฑ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}" |
|
print(f"[LLM] {error_msg}") |
|
return error_msg |
|
|
|
def translate_to_english(text: str) -> str: |
|
"""ํ๊ธ ํ
์คํธ๋ฅผ ์์ด๋ก ๋ฒ์ญ (LLM ์ฌ์ฉ)""" |
|
if not any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in text): |
|
return text |
|
|
|
print(f"[๋ฒ์ญ] ํ๊ธ ๊ฐ์ง, ์์ด๋ก ๋ฒ์ญ ์์") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation." |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 500, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
translated = result['choices'][0]['message']['content'].strip() |
|
print(f"[๋ฒ์ญ] ์๋ฃ") |
|
return translated |
|
else: |
|
print(f"[๋ฒ์ญ] ์คํจ, ์๋ณธ ์ฌ์ฉ") |
|
return text |
|
except Exception as e: |
|
print(f"[๋ฒ์ญ] ์ค๋ฅ: {str(e)}, ์๋ณธ ์ฌ์ฉ") |
|
return text |
|
|
|
def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]: |
|
"""Replicate API๋ฅผ ์ฌ์ฉํด ์ด๋ฏธ์ง ์์ฑ""" |
|
print(f"\n[์ด๋ฏธ์ง ์์ฑ] {slide_info}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ํ๋กฌํํธ: {prompt[:50]}...") |
|
|
|
try: |
|
english_prompt = translate_to_english(prompt) |
|
|
|
if not REPLICATE_API_TOKEN: |
|
error_msg = "RAPI_TOKEN ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค." |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์ค๋ฅ: {error_msg}") |
|
return None, error_msg |
|
|
|
print(f"[์ด๋ฏธ์ง ์์ฑ] Replicate API ํธ์ถ ์ค...") |
|
client = replicate.Client(api_token=REPLICATE_API_TOKEN) |
|
|
|
input_params = { |
|
"seed": seed, |
|
"prompt": english_prompt, |
|
"speed_mode": "Extra Juiced ๐ (even more speed)", |
|
"output_quality": 100 |
|
} |
|
|
|
start_time = time.time() |
|
output = client.run( |
|
"prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645", |
|
input=input_params |
|
) |
|
|
|
elapsed = time.time() - start_time |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] API ์๋ต ๋ฐ์ ({elapsed:.1f}์ด)") |
|
|
|
if output: |
|
if isinstance(output, str) and output.startswith('http'): |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] URL์์ ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ์ค...") |
|
response = requests.get(output, timeout=30) |
|
img = Image.open(BytesIO(response.content)) |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์๋ฃ!") |
|
return img, english_prompt |
|
else: |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ค...") |
|
img = Image.open(BytesIO(output.read())) |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์๋ฃ!") |
|
return img, english_prompt |
|
else: |
|
error_msg = "์ด๋ฏธ์ง ์์ฑ ์คํจ - ๋น ์๋ต" |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] {error_msg}") |
|
return None, error_msg |
|
|
|
except Exception as e: |
|
error_msg = f"์ค๋ฅ: {str(e)}" |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] {error_msg}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์์ธ ์ค๋ฅ:\n{traceback.format_exc()}") |
|
return None, error_msg |
|
|
|
def create_slide_preview_html(slide_data: Dict) -> str: |
|
"""16:9 ๋น์จ์ ์ฌ๋ผ์ด๋ ํ๋ฆฌ๋ทฐ HTML ์์ฑ (๋ฅ๊ทผ ๋ชจ์๋ฆฌ์ ์
์ฒด๊ฐ)""" |
|
|
|
|
|
img_base64 = "" |
|
if slide_data.get("image"): |
|
buffered = BytesIO() |
|
slide_data["image"].save(buffered, format="PNG") |
|
img_base64 = base64.b64encode(buffered.getvalue()).decode() |
|
|
|
|
|
subtitle = slide_data.get("subtitle", "") |
|
bullet_points = slide_data.get("bullet_points", []) |
|
|
|
|
|
html = f""" |
|
<div class="slide-container" style=" |
|
width: 100%; |
|
max-width: 1200px; |
|
margin: 20px auto; |
|
background: white; |
|
border-radius: 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}") |
|
|
|
|
|
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 |
|
|
|
|
|
elif slide_data.get('title') == 'Thank You': |
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
0, 0, |
|
width=prs.slide_width, |
|
height=prs.slide_height |
|
) |
|
|
|
slide.shapes._spTree.remove(pic._element) |
|
slide.shapes._spTree.insert(2, pic._element) |
|
except Exception as e: |
|
print(f"[PPTX] Thank You ์ด๋ฏธ์ง ์ถ๊ฐ ์คํจ: {str(e)}") |
|
|
|
|
|
thanks_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() |
|
|
|
|
|
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", []) |
|
|
|
|
|
content_slide_count = slide_count |
|
|
|
|
|
if len(core_slides) > content_slide_count: |
|
selected_slides = core_slides[:content_slide_count] |
|
else: |
|
|
|
selected_slides = core_slides.copy() |
|
remaining = content_slide_count - len(core_slides) |
|
|
|
if remaining > 0 and optional_slides: |
|
|
|
additional = optional_slides[:remaining] |
|
selected_slides.extend(additional) |
|
|
|
|
|
slides = [{"title": "ํ์ง", "style": "Title Slide (Hero)", "prompt_hint": "ํ๋ ์ ํ
์ด์
ํ์ง"}] |
|
|
|
|
|
slides.extend(selected_slides) |
|
|
|
|
|
slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "๊ฐ์ฌ ์ธ์ฌ"}) |
|
|
|
return slides |
|
|
|
def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict], |
|
slide_count: int, seed: int, uploaded_file, |
|
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) |
|
|
|
|
|
preview_html = """ |
|
<style> |
|
.slides-container { |
|
width: 100%; |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
} |
|
</style> |
|
<div class="slides-container"> |
|
""" |
|
|
|
|
|
for i, slide in enumerate(slides): |
|
progress((i + 1) / (total_slides + 1), f"์ฌ๋ผ์ด๋ {i+1}/{total_slides} ์ฒ๋ฆฌ ์ค...") |
|
|
|
slide_info = f"์ฌ๋ผ์ด๋ {i+1}: {slide['title']}" |
|
|
|
|
|
slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}" |
|
content = generate_slide_content( |
|
topic, slide['title'], slide_context, |
|
uploaded_content, web_search_results |
|
) |
|
|
|
|
|
speaker_notes = generate_presentation_notes(topic, slide['title'], content) |
|
|
|
|
|
style_key = slide["style"] |
|
if style_key in STYLE_TEMPLATES: |
|
style_info = STYLE_TEMPLATES[style_key] |
|
prompt = generate_prompt_with_llm( |
|
topic, style_info["example"], |
|
slide_context, uploaded_content |
|
) |
|
|
|
|
|
slide_seed = seed + i |
|
img, used_prompt = generate_image(prompt, slide_seed, slide_info) |
|
|
|
|
|
slide_data = { |
|
"slide_number": i + 1, |
|
"title": slide["title"], |
|
"subtitle": content["subtitle"], |
|
"bullet_points": content["bullet_points"], |
|
"image": img, |
|
"style": style_info["name"], |
|
"speaker_notes": speaker_notes, |
|
"topic": topic |
|
} |
|
|
|
|
|
preview_html += create_slide_preview_html(slide_data) |
|
|
|
|
|
yield preview_html + "</div>", f"### ๐ {slide_info} ์์ฑ ์ค...", None |
|
|
|
results.append({ |
|
"slide_data": slide_data, |
|
"success": img is not None |
|
}) |
|
|
|
|
|
progress(0.95, "PPTX ํ์ผ ์์ฑ ์ค...") |
|
pptx_path = None |
|
try: |
|
pptx_path = create_pptx_file(results, topic, template_name, 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): |
|
with gr.Row(visible=(i < 3)): |
|
with gr.Column(scale=2): |
|
title = gr.Textbox( |
|
label=f"์ฌ๋ผ์ด๋ {i+1} ์ ๋ชฉ", |
|
placeholder="์: ํํฉ ๋ถ์, ์๋ฃจ์
, ๋ก๋๋งต...", |
|
) |
|
with gr.Column(scale=3): |
|
style = gr.Dropdown( |
|
choices=list(STYLE_TEMPLATES.keys()), |
|
label=f"์คํ์ผ ์ ํ", |
|
value="Colorful Mind Map" |
|
) |
|
with gr.Column(scale=3): |
|
hint = gr.Textbox( |
|
label=f"ํ๋กฌํํธ ํํธ", |
|
placeholder="์ด ์ฌ๋ผ์ด๋์์ ํํํ๊ณ ์ถ์ ๋ด์ฉ" |
|
) |
|
slides.append({"title": title, "style": style, "hint": hint, "row": gr.Row}) |
|
return slides |
|
|
|
|
|
with gr.Blocks(title="PPT ์ด๋ฏธ์ง ์์ฑ๊ธฐ", theme=gr.themes.Soft(), css=""" |
|
.preview-container { max-width: 1400px; margin: 0 auto; } |
|
""") as demo: |
|
gr.Markdown(""" |
|
# ๐ฏ AI ๊ธฐ๋ฐ PPT ํตํฉ ์์ฑ๊ธฐ (๋์์ธ ํ
๋ง ํฌํจ) |
|
|
|
### ํ
์คํธ์ ์ด๋ฏธ์ง๊ฐ ์๋ฒฝํ๊ฒ ์กฐํ๋ ํ๋ ์ ํ
์ด์
์ ์๋์ผ๋ก ์์ฑํ๊ณ ๋ค์ด๋ก๋ํ์ธ์! |
|
|
|
#### ๐ ์๋ก์ด ๊ธฐ๋ฅ: |
|
- ๐จ **๋์์ธ ํ
๋ง**: 5๊ฐ์ง ์ ๋ฌธ์ ์ธ ๋์์ธ ํ
๋ง ์ ํ ๊ฐ๋ฅ |
|
- ๐ **ํ์ง์ Thank You ์ฌ๋ผ์ด๋** ์๋ ์ถ๊ฐ |
|
- ๐ **๋ฐํ์ ๋
ธํธ** ์๋ ์์ฑ (๊ตฌ์ด์ฒด) |
|
- ๐ **ํ์ผ ์
๋ก๋** ์ง์ (PDF/CSV/TXT) |
|
- ๐ **์น ๊ฒ์** ๊ธฐ๋ฅ (Brave Search) |
|
- ๐๏ธ **์ฌ๋ผ์ด๋ ์ ์กฐ์ ** (6-20์ฅ) |
|
- ๐ **์
์ฒด๊ฐ ์๋ ๋์์ธ**: ๋ฅ๊ทผ ๋ชจ์๋ฆฌ์ ๊ทธ๋ฆผ์ ํจ๊ณผ |
|
""") |
|
|
|
|
|
token_status = [] |
|
if not REPLICATE_API_TOKEN: |
|
token_status.append("โ ๏ธ RAPI_TOKEN ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
|
if not FRIENDLI_TOKEN: |
|
token_status.append("โ ๏ธ FRIENDLI_TOKEN ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
|
if not BRAVE_API_TOKEN: |
|
token_status.append("โน๏ธ BAPI_TOKEN์ด ์์ด ์น ๊ฒ์ ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ฉ๋๋ค.") |
|
|
|
if token_status: |
|
gr.Markdown("\n".join(token_status)) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
topic_input = gr.Textbox( |
|
label="ํ๋ ์ ํ
์ด์
์ฃผ์ ", |
|
placeholder="์: AI ์คํํธ์
ํฌ์ ์ ์น, ์ ์ ํ ๋ฐ์นญ, ๋์งํธ ์ ํ ์ ๋ต", |
|
lines=2 |
|
) |
|
|
|
|
|
template_select = gr.Dropdown( |
|
choices=list(PPT_TEMPLATES.keys()), |
|
label="PPT ํ
ํ๋ฆฟ ์ ํ", |
|
value="๋น์ฆ๋์ค ์ ์์", |
|
info="๋ชฉ์ ์ ๋ง๋ ํ
ํ๋ฆฟ์ ์ ํํ์ธ์" |
|
) |
|
|
|
|
|
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") |
|
|
|
|
|
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["๋ฏธ๋๋ฉ ๋ผ์ดํธ"]) |
|
|
|
|
|
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)), |
|
gr.update(visible=(i < count)), |
|
gr.update(visible=(i < count)) |
|
]) |
|
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 |
|
}) |
|
|
|
|
|
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") |
|
|
|
|
|
demo.launch(ssr_mode=False) |