|
import replicate |
|
import requests |
|
import os |
|
import json |
|
from io import BytesIO |
|
from PIL import Image |
|
from typing import List, Tuple, Dict |
|
from datetime import datetime |
|
import time |
|
import traceback |
|
import base64 |
|
from pptx import Presentation |
|
from pptx.util import Inches, Pt |
|
from pptx.dml.color import RGBColor |
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR |
|
from pptx.enum.shapes import MSO_SHAPE |
|
import PyPDF2 |
|
import pandas as pd |
|
import chardet |
|
import gradio as gr |
|
|
|
|
|
try: |
|
from process_flow_generator import generate_process_flow_for_ppt |
|
PROCESS_FLOW_AVAILABLE = True |
|
except ImportError: |
|
PROCESS_FLOW_AVAILABLE = False |
|
print("[๊ฒฝ๊ณ ] process_flow_generator๋ฅผ ์ํฌํธํ ์ ์์ต๋๋ค. ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ฉ๋๋ค.") |
|
|
|
|
|
EXAMPLE_TOPICS = { |
|
"๋น์ฆ๋์ค ์ ์์": "AI ๊ธฐ๋ฐ ๊ณ ๊ฐ ์๋น์ค ์๋ํ ํ๋ซํผ ํฌ์ ์ ์", |
|
"์ ํ ์๊ฐ": "์ค๋งํธ ํ IoT ๋ณด์ ์์คํ
์ ์ ํ ๋ฐ์นญ", |
|
"ํ๋ก์ ํธ ๋ณด๊ณ ": "๋์งํธ ์ ํ ํ๋ก์ ํธ 3๋ถ๊ธฐ ์ฑ๊ณผ ๋ณด๊ณ ", |
|
"์ ๋ต ๊ธฐํ": "2025๋
๊ธ๋ก๋ฒ ์์ฅ ์ง์ถ ์ ๋ต ์๋ฆฝ" |
|
} |
|
|
|
|
|
AUDIENCE_TYPES = { |
|
"๊ฒฝ์์ง/์์": { |
|
"description": "C-level, ์์ฌ๊ฒฐ์ ๊ถ์", |
|
"tone": "์ ๋ต์ , ๊ฒฐ๊ณผ ์ค์ฌ, ROI ๊ฐ์กฐ", |
|
"focus": "๋น์ฆ๋์ค ๊ฐ์น, ํฌ์ ์์ต๋ฅ , ์ ๋ต์ ์ํฅ" |
|
}, |
|
"ํฌ์์": { |
|
"description": "VC, ์์ คํฌ์์, ๊ธฐ๊ดํฌ์์", |
|
"tone": "์์น ๊ธฐ๋ฐ, ์ฑ์ฅ ๊ฐ๋ฅ์ฑ, ์์ฅ ๊ธฐํ", |
|
"focus": "์์ฅ ๊ท๋ชจ, ์ฑ์ฅ๋ฅ , ๊ฒฝ์์ฐ์, Exit ์ ๋ต" |
|
}, |
|
"๊ธฐ์ ํ": { |
|
"description": "๊ฐ๋ฐ์, ์์ง๋์ด, IT ์ ๋ฌธ๊ฐ", |
|
"tone": "๊ธฐ์ ์ , ๊ตฌ์ฒด์ , ์ค์ฉ์ ", |
|
"focus": "๊ธฐ์ ์คํ, ์ํคํ
์ฒ, ๊ตฌํ ๋ฐฉ๋ฒ, ์ฑ๋ฅ" |
|
}, |
|
"์ผ๋ฐ ์ง์": { |
|
"description": "์ค๋ฌด์, ํ์", |
|
"tone": "์น๊ทผํ, ์ค๋ฌด์ , ํ์
์ค์ฌ", |
|
"focus": "์คํ ๊ณํ, ์ญํ , ํ๋ก์ธ์ค, ํ์
๋ฐฉ์" |
|
}, |
|
"๊ณ ๊ฐ/ํํธ๋": { |
|
"description": "B2B ๊ณ ๊ฐ, ๋น์ฆ๋์ค ํํธ๋", |
|
"tone": "์ ๋ขฐ๊ฐ, ์ ๋ฌธ์ , ํํ ์ค์ฌ", |
|
"focus": "๊ณ ๊ฐ ๊ฐ์น, ํํ, ์ฌ๋ก, ์ง์ ์ฒด๊ณ" |
|
}, |
|
"์ผ๋ฐ ๋์ค": { |
|
"description": "B2C ๊ณ ๊ฐ, ์ผ๋ฐ ์ฌ์ฉ์", |
|
"tone": "์ฝ๊ณ ์น๊ทผํ, ์ดํดํ๊ธฐ ์ฌ์ด", |
|
"focus": "์ฌ์ฉ ํธ์์ฑ, ํํ, ๊ฐ๊ฒฉ, ์ฐจ๋ณ์ " |
|
} |
|
} |
|
|
|
|
|
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN") |
|
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN") |
|
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN") |
|
|
|
|
|
current_slides_data = [] |
|
current_topic = "" |
|
current_template = "" |
|
current_theme = "" |
|
uploaded_content = "" |
|
|
|
|
|
DESIGN_THEMES = { |
|
"๋ฏธ๋๋ฉ ๋ผ์ดํธ": { |
|
"name": "Minimal Light", |
|
"description": "๋ฐ๊ณ ๊นจ๋ํ ๋ฏธ๋๋ฉ ๋์์ธ", |
|
"background": RGBColor(250, 250, 252), |
|
"title_color": RGBColor(33, 37, 41), |
|
"subtitle_color": RGBColor(52, 58, 64), |
|
"text_color": RGBColor(73, 80, 87), |
|
"accent_color": RGBColor(0, 123, 255), |
|
"box_fill": RGBColor(255, 255, 255), |
|
"box_opacity": 0.95, |
|
"shadow": True, |
|
"gradient": False |
|
}, |
|
"๋ชจ๋ ๊ทธ๋ผ๋์ธํธ": { |
|
"name": "Modern Gradient", |
|
"description": "๋ถ๋๋ฌ์ด ๊ทธ๋ผ๋์ธํธ์ ํ๋์ ๋๋", |
|
"background": RGBColor(245, 247, 250), |
|
"title_color": RGBColor(25, 42, 86), |
|
"subtitle_color": RGBColor(68, 85, 102), |
|
"text_color": RGBColor(85, 102, 119), |
|
"accent_color": RGBColor(103, 58, 183), |
|
"box_fill": RGBColor(249, 250, 251), |
|
"box_opacity": 0.9, |
|
"shadow": True, |
|
"gradient": True |
|
}, |
|
"๋คํฌ ์๋ ๊ฐ์ค": { |
|
"name": "Dark Elegance", |
|
"description": "์ธ๋ จ๋ ๋คํฌ ๋ชจ๋ ๋์์ธ", |
|
"background": RGBColor(25, 25, 35), |
|
"title_color": RGBColor(240, 240, 245), |
|
"subtitle_color": RGBColor(200, 200, 210), |
|
"text_color": RGBColor(170, 170, 180), |
|
"accent_color": RGBColor(0, 188, 212), |
|
"box_fill": RGBColor(35, 35, 45), |
|
"box_opacity": 0.85, |
|
"shadow": False, |
|
"gradient": False |
|
}, |
|
"๋ค์ด์ฒ ๊ทธ๋ฆฐ": { |
|
"name": "Nature Green", |
|
"description": "์์ฐ ์นํ์ ์ธ ๊ทธ๋ฆฐ ํ
๋ง", |
|
"background": RGBColor(242, 248, 244), |
|
"title_color": RGBColor(27, 67, 50), |
|
"subtitle_color": RGBColor(45, 106, 79), |
|
"text_color": RGBColor(64, 125, 98), |
|
"accent_color": RGBColor(76, 175, 80), |
|
"box_fill": RGBColor(255, 255, 255), |
|
"box_opacity": 0.92, |
|
"shadow": True, |
|
"gradient": False |
|
}, |
|
"์ฝํผ๋ ์ดํธ ๋ธ๋ฃจ": { |
|
"name": "Corporate Blue", |
|
"description": "์ ๋ฌธ์ ์ธ ๋น์ฆ๋์ค ์คํ์ผ", |
|
"background": RGBColor(244, 247, 252), |
|
"title_color": RGBColor(13, 71, 161), |
|
"subtitle_color": RGBColor(25, 118, 210), |
|
"text_color": RGBColor(42, 63, 84), |
|
"accent_color": RGBColor(33, 150, 243), |
|
"box_fill": RGBColor(255, 255, 255), |
|
"box_opacity": 0.95, |
|
"shadow": True, |
|
"gradient": False |
|
} |
|
} |
|
|
|
|
|
STYLE_TEMPLATES = { |
|
"Title Slide (Hero)": { |
|
"name": "Title Slide", |
|
"description": "Impactful hero image for title slide", |
|
"use_case": "ํ์ง, ํ์ดํ", |
|
"example": "A dramatic wide-angle view of a modern glass skyscraper reaching into clouds with golden sunset lighting, symbolizing growth and ambition. Ultra-realistic photography style, cinematic composition, lens flare, professional corporate aesthetic" |
|
}, |
|
"Thank You Slide": { |
|
"name": "Closing Slide", |
|
"description": "Elegant closing slide design with conclusion", |
|
"use_case": "ํ๋ ์ ํ
์ด์
๋ง๋ฌด๋ฆฌ", |
|
"example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for conclusion text. Minimalist, professional, warm feeling" |
|
}, |
|
"3D Style (Pixar-like)": { |
|
"name": "3D Style", |
|
"description": "Pixar-esque 3D render with volumetric lighting", |
|
"use_case": "ํ์ง, ๋น์ , ๋ฏธ๋ ์ปจ์
", |
|
"example": "A fluffy ginger cat wearing a tiny spacesuit, floating amidst a vibrant nebula in a 3D render. The cat is gazing curiously at a swirling planet with rings made of candy. Background is filled with sparkling stars and colorful gas clouds, lit with soft, volumetric lighting. Style: Pixar-esque, highly detailed, playful. Colors: Deep blues, purples, oranges, and pinks. Rendered in Octane, 8k resolution." |
|
}, |
|
"Elegant SWOT Quadrant": { |
|
"name": "SWOT Analysis", |
|
"description": "Flat-design 4-grid layout with minimal shadows", |
|
"use_case": "ํํฉ ๋ถ์, ์ ๋ต ํ๊ฐ", |
|
"example": "Elegant SWOT quadrant: flat-design 4-grid on matte-white backdrop, thin pastel separators, top-left 'Strengths' panel shows glowing shield icon and subtle motif, top-right 'Weaknesses' panel with cracked chain icon in soft crimson, bottom-left 'Opportunities' panel with sunrise-over-horizon icon in optimistic teal, bottom-right 'Threats' panel with storm-cloud & lightning icon in deep indigo, minimal shadows, no text, no watermark, 16:9, 4K" |
|
}, |
|
"Colorful Mind Map": { |
|
"name": "Mind Map", |
|
"description": "Hand-drawn educational style with vibrant colors", |
|
"use_case": "๋ธ๋ ์ธ์คํ ๋ฐ, ์์ด๋์ด ์ ๋ฆฌ", |
|
"example": "A handrawn colorful mind map diagram: educational style, vibrant colors, clear hierarchy, golden ratio layout. Central concept with branching sub-topics, each branch with unique color coding, organic flowing connections, doodle-style icons for each node" |
|
}, |
|
"Business Workflow": { |
|
"name": "Business Process", |
|
"description": "End-to-end business workflow with clear phases", |
|
"use_case": "ํ๋ก์ธ์ค ์ค๋ช
, ๋จ๊ณ๋ณ ์งํ", |
|
"example": "A detailed hand-drawn diagram illustrating an end-to-end business workflow with Market Analysis, Strategy Development, Product Design, Implementation, and Post-Launch Review phases. Clear directional arrows, iconography for each component, vibrant educational yet professional style", |
|
"is_process_flow": True |
|
}, |
|
"Industrial Design": { |
|
"name": "Product Design", |
|
"description": "Sleek industrial design concept sketch", |
|
"use_case": "์ ํ ์๊ฐ, ์ปจ์
๋์์ธ", |
|
"example": "A sleek industrial design concept: Curved metallic body with minimal bezel, Touchscreen panel for settings, Modern matte black finish, Hand-drawn concept sketch style with annotations and dimension lines" |
|
}, |
|
"3D Bubble Chart": { |
|
"name": "Bubble Chart", |
|
"description": "Clean 3D bubble visualization", |
|
"use_case": "๋น๊ต ๋ถ์, ํฌ์ง์
๋", |
|
"example": "3-D bubble chart on clean white 2ร2 grid, quadrant titles hidden, four translucent spheres in lime, azure, amber, magenta, gentle depth-of-field, modern consulting aesthetic, no text, 4K" |
|
}, |
|
"Timeline Ribbon": { |
|
"name": "Timeline", |
|
"description": "Horizontal ribbon timeline with cyber-futuristic vibe", |
|
"use_case": "์ผ์ , ๋ก๋๋งต, ๋ง์ผ์คํค", |
|
"example": "Horizontal ribbon timeline, milestone pins glowing hot pink on charcoal, year markers as circles, faint motion streaks, cyber-futuristic vibe, no text, 1920ร1080" |
|
}, |
|
"Risk Heat Map": { |
|
"name": "Heat Map", |
|
"description": "Risk assessment heat map with gradient colors", |
|
"use_case": "๋ฆฌ์คํฌ ๋ถ์, ์ฐ์ ์์", |
|
"example": "Risk Heat Map: square grid, smooth gradient from mint to fire-red, cells beveled, simple legend strip hidden, long subtle shadow, sterile white frame, no text" |
|
}, |
|
"Pyramid/Funnel": { |
|
"name": "Funnel Chart", |
|
"description": "Multi-layer gradient funnel visualization", |
|
"use_case": "๋จ๊ณ๋ณ ์ถ์, ํต์ฌ ๋์ถ", |
|
"example": "Pyramid / Funnel: 5-layer gradient funnel narrowing downwards, top vivid sky-blue, mid mint-green, bottom sunset-orange, glass reflection, minimal background, no text" |
|
}, |
|
"KPI Dashboard": { |
|
"name": "Dashboard", |
|
"description": "Dark-mode analytics dashboard with sci-fi interface", |
|
"use_case": "์ฑ๊ณผ ์งํ, ์ค์ ๋์๋ณด๋", |
|
"example": "KPI Dashboard: Dark-mode analytic dashboard, three glass speedometers glowing neon lime, two sparkline charts under, black glass background, sci-fi interface, no text, 4K" |
|
}, |
|
"Value Chain": { |
|
"name": "Value Chain", |
|
"description": "Horizontal value chain with industrial look", |
|
"use_case": "๊ฐ์น ์ฌ์ฌ, ๋น์ฆ๋์ค ๋ชจ๋ธ", |
|
"example": "Value Chain Diagram: Horizontal value chain blocks, steel-blue gradient bars with subtle bevel, small gear icons above each segment, sleek industrial look, shadow cast, no text" |
|
}, |
|
"Gantt Chart": { |
|
"name": "Gantt Chart", |
|
"description": "Hand-drawn style Gantt chart with playful colors", |
|
"use_case": "ํ๋ก์ ํธ ์ผ์ , ์์
๊ด๋ฆฌ", |
|
"example": "Gantt Chart: Hand-drawn style Gantt bars sketched with vibrant markers on dotted grid notebook page, sticky-note color palette, playful yet organized, perspective tilt, no text" |
|
}, |
|
"Mobile App Mockup": { |
|
"name": "App Mockup", |
|
"description": "Clean wireframe for mobile app design", |
|
"use_case": "์ฑ/์น UI, ํ๋ฉด ์ค๊ณ", |
|
"example": "MOCKUP DESIGN: A clean hand-drawn style wireframe for a mobile app with Title screen, Login screen, Dashboard with sections, Bottom navigation bar, minimalist design with annotations" |
|
}, |
|
"Flowchart": { |
|
"name": "Flowchart", |
|
"description": "Vibrant flowchart with minimalistic icons", |
|
"use_case": "์์ฌ๊ฒฐ์ , ํ๋ก์ธ์ค ํ๋ฆ", |
|
"example": "FLOWCHART DESIGN: A hand-drawn style flowchart, vibrant colors, minimalistic icons showing process flow from START to END with decision points, branches, and clear directional arrows", |
|
"is_process_flow": True |
|
} |
|
} |
|
|
|
|
|
|
|
PPT_TEMPLATES = { |
|
"๋น์ฆ๋์ค ์ ์์": { |
|
"description": "ํฌ์ ์ ์น, ์ฌ์
์ ์์ฉ", |
|
"core_slides": [ |
|
{"title": "๋ชฉ์ฐจ", "style": "Flowchart", "prompt_hint": "ํ๋ ์ ํ
์ด์
๊ตฌ์กฐ"}, |
|
{"title": "๋ฌธ์ ์ ์", "style": "Colorful Mind Map", "prompt_hint": "ํ์ฌ ์์ฅ์ ๋ฌธ์ ์ "}, |
|
{"title": "ํํฉ ๋ถ์", "style": "Elegant SWOT Quadrant", "prompt_hint": "๊ฐ์ , ์ฝ์ , ๊ธฐํ, ์ํ"}, |
|
{"title": "์๋ฃจ์
", "style": "Industrial Design", "prompt_hint": "์ ํ/์๋น์ค ์ปจ์
"}, |
|
{"title": "ํ๋ก์ธ์ค", "style": "Business Workflow", "prompt_hint": "์คํ ๋จ๊ณ"}, |
|
{"title": "์ผ์ ", "style": "Timeline Ribbon", "prompt_hint": "์ฃผ์ ๋ง์ผ์คํค"} |
|
], |
|
"optional_slides": [ |
|
{"title": "์์ฅ ๊ท๋ชจ", "style": "3D Bubble Chart", "prompt_hint": "์์ฅ ๊ธฐํ์ ์ฑ์ฅ์ฑ"}, |
|
{"title": "๊ฒฝ์ ๋ถ์", "style": "Risk Heat Map", "prompt_hint": "๊ฒฝ์์ฌ ํฌ์ง์
๋"}, |
|
{"title": "๋น์ฆ๋์ค ๋ชจ๋ธ", "style": "Value Chain", "prompt_hint": "์์ต ๊ตฌ์กฐ"}, |
|
{"title": "ํ ์๊ฐ", "style": "Colorful Mind Map", "prompt_hint": "ํต์ฌ ํ์๊ณผ ์ญ๋"}, |
|
{"title": "์ฌ๋ฌด ๊ณํ", "style": "KPI Dashboard", "prompt_hint": "์์ ๋งค์ถ๊ณผ ์์ต"}, |
|
{"title": "์ํ ๊ด๋ฆฌ", "style": "Risk Heat Map", "prompt_hint": "์ฃผ์ ๋ฆฌ์คํฌ์ ๋์"}, |
|
{"title": "ํํธ๋์ญ", "style": "Value Chain", "prompt_hint": "์ ๋ต์ ์ ํด"}, |
|
{"title": "๊ธฐ์ ์คํ", "style": "Industrial Design", "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": "Mobile App Mockup", "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": "Colorful Mind Map", "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": "Value Chain", "prompt_hint": "์กฐ์ง ๊ฐํธ"}, |
|
{"title": "๋์งํธ ์ ํ", "style": "Industrial Design", "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, audience_type: str) -> str: |
|
"""๊ฐ ์ฌ๋ผ์ด๋์ ๋ฐํ์ ๋
ธํธ ์์ฑ (๊ตฌ์ด์ฒด)""" |
|
print(f"[๋ฐํ์ ๋
ธํธ] {slide_title} ์์ฑ ์ค...") |
|
|
|
|
|
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ ์ง์"]) |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = f"""You are a professional presentation coach who creates natural, conversational speaker notes. |
|
|
|
The audience for this presentation is: {audience_type} - {audience_info['description']} |
|
Tone: {audience_info['tone']} |
|
Focus areas: {audience_info['focus']} |
|
|
|
The slide content uses concise noun-ending style, but your speaker notes should be natural and conversational. |
|
|
|
Create speaker notes that: |
|
1. Sound natural and conversational, as if speaking to {audience_type} |
|
2. Expand on the concise bullet points with additional context relevant to this audience |
|
3. Include transitions and engagement phrases appropriate for {audience_type} |
|
4. Use a warm, professional tone suitable for {audience_type} |
|
5. Be 100-150 words long |
|
6. Include pauses and emphasis markers where appropriate |
|
|
|
Note: The bullet points may include emojis - incorporate their meaning into your speech. |
|
|
|
Format: |
|
- Use conversational language appropriate for {audience_type} |
|
- Include transition phrases |
|
- Add engagement questions or comments that resonate with this audience |
|
- Keep it professional but friendly""" |
|
|
|
bullet_text = "\n".join(content.get("bullet_points", [])) |
|
user_message = f"""Topic: {topic} |
|
Slide Title: {slide_title} |
|
Subtitle: {content.get('subtitle', '')} |
|
Key Points (concise style with emojis): |
|
{bullet_text} |
|
|
|
Create natural speaker notes that expand on these concise points for presenting this slide to {audience_type}.""" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"temperature": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
notes = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
notes = translate_content_to_korean_natural(notes) |
|
|
|
return notes |
|
else: |
|
return "์ด ์ฌ๋ผ์ด๋์์๋ ํต์ฌ ๋ด์ฉ์ ์ค๋ช
ํด ์ฃผ์ธ์." |
|
except Exception as e: |
|
print(f"[๋ฐํ์ ๋
ธํธ] ์ค๋ฅ: {str(e)}") |
|
return "์ด ์ฌ๋ผ์ด๋์์๋ ํต์ฌ ๋ด์ฉ์ ์ค๋ช
ํด ์ฃผ์ธ์." |
|
|
|
def generate_closing_notes(topic: str, conclusion_phrase: str, audience_type: str) -> str: |
|
"""๋ง์ง๋ง ์ฌ๋ผ์ด๋์ ๋ฐํ์ ๋
ธํธ ์์ฑ""" |
|
print(f"[๋ง๋ฌด๋ฆฌ ๋
ธํธ] ์์ฑ ์ค...") |
|
|
|
|
|
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ ์ง์"]) |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = f"""You are a professional presentation coach creating closing speaker notes. |
|
|
|
The audience for this presentation is: {audience_type} - {audience_info['description']} |
|
Tone: {audience_info['tone']} |
|
|
|
Create natural closing remarks that: |
|
1. Thank the {audience_type} audience warmly and appropriately |
|
2. Briefly summarize the key message relevant to {audience_type} |
|
3. Reference the conclusion phrase naturally |
|
4. End with an invitation for questions or next steps appropriate for {audience_type} |
|
5. Be 80-100 words long |
|
6. Sound conversational and warm |
|
7. NO stage directions or parentheses - only spoken words |
|
|
|
Write only what the speaker would say out loud.""" |
|
|
|
user_message = f"""Presentation topic: {topic} |
|
Audience: {audience_type} |
|
Conclusion phrase on screen: {conclusion_phrase} |
|
|
|
Create natural closing speaker notes that wrap up the presentation effectively for {audience_type}.""" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 200, |
|
"temperature": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
notes = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
notes = translate_content_to_korean_natural(notes) |
|
|
|
return notes |
|
else: |
|
return "์ค๋ ๋ฐํ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ฉฐ ๊ฐ์ฌ์ ๋ง์์ ๋๋ฆฝ๋๋ค. ์ง๋ฌธ์ด ์์ผ์๋ฉด ํธํ๊ฒ ๋ง์ํด ์ฃผ์ธ์." |
|
except Exception as e: |
|
print(f"[๋ง๋ฌด๋ฆฌ ๋
ธํธ] ์ค๋ฅ: {str(e)}") |
|
return "์ค๋ ๋ฐํ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ฉฐ ๊ฐ์ฌ์ ๋ง์์ ๋๋ฆฝ๋๋ค. ์ง๋ฌธ์ด ์์ผ์๋ฉด ํธํ๊ฒ ๋ง์ํด ์ฃผ์ธ์." |
|
|
|
def generate_conclusion_phrase(topic: str, audience_type: str) -> str: |
|
"""ํ๋ ์ ํ
์ด์
์ ํต์ฌ์ ๋ด์ ๊ฒฐ๋ก ๋ฌธ๊ตฌ ์์ฑ""" |
|
print(f"[๊ฒฐ๋ก ๋ฌธ๊ตฌ] ์์ฑ ์ค...") |
|
|
|
|
|
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ ์ง์"]) |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = f"""You are a professional copywriter creating powerful closing statements for presentations. |
|
|
|
The audience is: {audience_type} - {audience_info['description']} |
|
Focus on: {audience_info['focus']} |
|
|
|
Create a concise, impactful closing phrase that: |
|
1. Captures the essence of the presentation topic |
|
2. Resonates with {audience_type} |
|
3. Is memorable and inspirational |
|
4. Maximum 5-7 words |
|
5. Uses powerful, action-oriented language appropriate for {audience_type} |
|
6. Leaves a lasting impression |
|
|
|
Examples for different audiences: |
|
- For executives: "Excellence Through Strategic Innovation" |
|
- For investors: "Maximum Returns, Minimal Risk" |
|
- For technical teams: "Code Today, Transform Tomorrow" |
|
- For customers: "Your Success, Our Mission" |
|
|
|
Output only the phrase, no explanation.""" |
|
|
|
user_message = f"""Presentation topic: {topic} |
|
Target audience: {audience_type} |
|
|
|
Create a powerful closing phrase that encapsulates the main message for {audience_type}.""" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 50, |
|
"temperature": 0.9, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
phrase = result['choices'][0]['message']['content'].strip() |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
phrase = translate_content_to_korean_concise(phrase) |
|
|
|
return phrase |
|
else: |
|
return "ํจ๊ป ๋ง๋๋ ๋ฏธ๋" |
|
except Exception as e: |
|
print(f"[๊ฒฐ๋ก ๋ฌธ๊ตฌ] ์ค๋ฅ: {str(e)}") |
|
return "ํจ๊ป ๋ง๋๋ ๋ฏธ๋" |
|
|
|
def generate_slide_content(topic: str, slide_title: str, slide_context: str, audience_type: str, |
|
uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]: |
|
"""๊ฐ ์ฌ๋ผ์ด๋์ ํ
์คํธ ๋ด์ฉ ์์ฑ""" |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] {slide_title} ํ
์คํธ ์์ฑ ์ค... (๋์: {audience_type})") |
|
|
|
|
|
audience_info = AUDIENCE_TYPES.get(audience_type, AUDIENCE_TYPES["์ผ๋ฐ ์ง์"]) |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = f"""You are a professional presentation content writer specializing in creating concise, impactful slide content. |
|
|
|
Target Audience: {audience_type} - {audience_info['description']} |
|
Tone: {audience_info['tone']} |
|
Focus: {audience_info['focus']} |
|
|
|
Your task is to create content specifically tailored for {audience_type}: |
|
1. A compelling subtitle (max 10 words) that resonates with {audience_type} |
|
2. Exactly 5 bullet points with emojis relevant to {audience_type}'s interests |
|
3. Each bullet point should be 8-12 words |
|
4. Use noun-ending style (๋ช
์ฌํ ์ข
๊ฒฐ) for Korean or concise fragments for English |
|
5. Content should address {audience_type}'s specific concerns and interests |
|
|
|
IMPORTANT Style Guidelines: |
|
- End sentences with nouns or concise phrases |
|
- Avoid long verb endings like "์
๋๋ค", "์ต๋๋ค" |
|
- Use short endings like "์", "ํจ" or noun forms |
|
- Start each bullet with a relevant emoji that {audience_type} would appreciate |
|
- Be extremely concise and impactful |
|
|
|
Audience-specific emoji guidelines: |
|
- For executives: ๐ ๐ฏ ๐ฐ ๐ ๐ ๐ ๐ ๐ก |
|
- For investors: ๐ฐ ๐ ๐ ๐ฆ ๐ธ ๐ ๐ ๐ |
|
- For technical teams: ๐ง ๐ป ๐ ๏ธ โ๏ธ ๐ ๐ ๐ฑ ๐ค |
|
- For general staff: ๐ค ๐ก ๐ โ
๐ฏ ๐ ๐
๐ช |
|
- For customers: โญ ๐ ๐ ๐ก๏ธ ๐ โจ ๐
๐ |
|
- For general public: ๐ ๐ ๐ โค๏ธ ๐ ๐ โจ ๐ฏ |
|
|
|
Output format (EXACTLY FOLLOW THIS FORMAT): |
|
Subtitle: [subtitle here] |
|
- ๐ฏ [Point 1 - tailored for {audience_type}] |
|
- ๐ [Point 2 - tailored for {audience_type}] |
|
- ๐ก [Point 3 - tailored for {audience_type}] |
|
- ๐ [Point 4 - tailored for {audience_type}] |
|
- โก [Point 5 - tailored for {audience_type}]""" |
|
|
|
user_message = f"""Topic: {topic} |
|
Slide Title: {slide_title} |
|
Context: {slide_context} |
|
Target Audience: {audience_type}""" |
|
|
|
|
|
if uploaded_content: |
|
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}" |
|
|
|
|
|
if web_search_results: |
|
search_context = "\n\nWeb Search Results:\n" |
|
for result in web_search_results[:3]: |
|
search_context += f"- {result['title']}: {result['description']}\n" |
|
user_message += search_context |
|
|
|
user_message += f"\n\nCreate compelling content for this presentation slide specifically tailored for {audience_type}. Remember to use emojis and concise noun-ending style." |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"top_p": 0.8, |
|
"temperature": 0.7, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
content = result['choices'][0]['message']['content'].strip() |
|
|
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] LLM ์๋ต:\n{content}") |
|
|
|
|
|
lines = content.split('\n') |
|
subtitle = "" |
|
bullet_points = [] |
|
|
|
for line in lines: |
|
line = line.strip() |
|
if not line: |
|
continue |
|
|
|
|
|
if line.lower().startswith("subtitle:") or line.startswith("Subtitle:"): |
|
subtitle = line.split(':', 1)[1].strip() |
|
|
|
elif line.startswith("โข") or line.startswith("-") or (len(line) > 2 and line[1] == ' ' and ord(line[0]) >= 128): |
|
|
|
if not line.startswith("โข"): |
|
line = "โข " + line.lstrip("- ") |
|
bullet_points.append(line) |
|
|
|
|
|
if not subtitle: |
|
subtitle = f"{slide_title} ๊ฐ์" |
|
|
|
|
|
while len(bullet_points) < 5: |
|
bullet_points.append(f"โข ๐ ํฌ์ธํธ {len(bullet_points) + 1}") |
|
|
|
|
|
bullet_points = bullet_points[:5] |
|
|
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] ํ์ฑ๋ subtitle: {subtitle}") |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] ํ์ฑ๋ bullets: {bullet_points}") |
|
|
|
|
|
if any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in topic): |
|
subtitle = translate_content_to_korean(subtitle) |
|
|
|
translated_bullets = [] |
|
for point in bullet_points: |
|
|
|
clean_point = point.replace('โข', '').strip() |
|
|
|
|
|
if len(clean_point) > 0 and clean_point[0] in '๐ฏ๐๐ก๐โก๐๐๐ฐ๐๐ง๐๐โญ๐จ๐ฑ๐ค๐๐๏ธ๐๏ธ๐ฑ๐ป๐ ๏ธโ๏ธ๐ค๐โ
๐๐
๐ช๐๐๐ก๏ธโจ๐
๐๐๐ ๐โค๏ธ๐๐': |
|
|
|
emoji = clean_point[0] |
|
text = clean_point[1:].strip() |
|
translated_text = translate_content_to_korean_concise(text) |
|
translated_bullets.append(f"โข {emoji} {translated_text}") |
|
else: |
|
|
|
translated_bullets.append(f"โข {translate_content_to_korean_concise(clean_point)}") |
|
|
|
bullet_points = translated_bullets |
|
|
|
return { |
|
"subtitle": subtitle, |
|
"bullet_points": bullet_points |
|
} |
|
else: |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] API ์ค๋ฅ: {response.status_code}") |
|
return { |
|
"subtitle": slide_title, |
|
"bullet_points": ["โข ๐ ๋ด์ฉ ์์ฑ ๋ถ๊ฐ"] * 5 |
|
} |
|
except Exception as e: |
|
print(f"[์ฌ๋ผ์ด๋ ๋ด์ฉ] ์ค๋ฅ: {str(e)}") |
|
return { |
|
"subtitle": slide_title, |
|
"bullet_points": ["โข โ ๋ด์ฉ ์์ฑ ์ค๋ฅ"] * 5 |
|
} |
|
|
|
def translate_content_to_korean(text: str) -> str: |
|
"""์์ด ํ
์คํธ๋ฅผ ํ๊ธ๋ก ๋ฒ์ญ (์ผ๋ฐ์ฉ)""" |
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "You are a translator. Translate the given English text to Korean. Maintain professional business tone. Only return the translation without any explanation." |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 200, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
return result['choices'][0]['message']['content'].strip() |
|
else: |
|
return text |
|
except Exception as e: |
|
return text |
|
|
|
def translate_content_to_korean_concise(text: str) -> str: |
|
"""์์ด ํ
์คํธ๋ฅผ ๊ฐ๊ฒฐํ ํ๊ธ๋ก ๋ฒ์ญ (๋ช
์ฌํ ์ข
๊ฒฐ)""" |
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": """You are a translator specializing in concise Korean business presentations. |
|
Translate to Korean using noun-ending style (๋ช
์ฌํ ์ข
๊ฒฐ์ด๋ฏธ). |
|
End with "์", "ํจ", or noun forms instead of "์
๋๋ค", "์ต๋๋ค". |
|
Keep it extremely concise and professional. |
|
Examples: "์ ๋ต์ ํ๋", "ํต์ฌ ๊ณผ์ ๋์ถ", "์์ฅ ์ ๋ ์ ๋ต ์๋ฆฝ" |
|
Only return the translation without any explanation.""" |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 200, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
return result['choices'][0]['message']['content'].strip() |
|
else: |
|
return text |
|
except Exception as e: |
|
return text |
|
|
|
def translate_content_to_korean_natural(text: str) -> str: |
|
"""์์ด ํ
์คํธ๋ฅผ ์์ฐ์ค๋ฌ์ด ํ๊ธ๋ก ๋ฒ์ญ (๋ฐํ ๋
ธํธ์ฉ)""" |
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": """You are a translator. Translate the given English text to natural, conversational Korean. |
|
Use polite spoken language suitable for presentations. |
|
Only return the translation without any explanation.""" |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 300, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
return result['choices'][0]['message']['content'].strip() |
|
else: |
|
return text |
|
except Exception as e: |
|
return text |
|
|
|
def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str: |
|
"""์ฃผ์ ์ ์คํ์ผ ์์ ๋ฅผ ๋ฐ์์ LLM์ ์ฌ์ฉํด ์ด๋ฏธ์ง ํ๋กฌํํธ๋ฅผ ์์ฑ""" |
|
print(f"[LLM] ํ๋กฌํํธ ์์ฑ ์์: {slide_context}") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
system_prompt = """You are an expert image prompt engineer specializing in creating prompts for professional presentation slides. |
|
|
|
Your task is to create prompts that: |
|
1. Are highly specific and visual, perfect for PPT backgrounds or main visuals |
|
2. Consider the slide's purpose and maintain consistency across a presentation |
|
3. Include style references matching the given example |
|
4. Focus on clean, professional visuals that won't distract from text overlays |
|
5. Ensure high contrast areas for text readability when needed |
|
6. Maintain brand consistency and professional aesthetics |
|
|
|
Important guidelines: |
|
- If given a style example, adapt the topic to match that specific visual style |
|
- Consider the slide context (e.g., "ํ์ง", "ํํฉ ๋ถ์") to create appropriate visuals |
|
- Always output ONLY the prompt without any explanation |
|
- Keep prompts between 50-150 words for optimal results |
|
- Ensure the visual supports rather than overwhelms the slide content""" |
|
|
|
user_message = f"Topic: {topic}" |
|
if style_example: |
|
user_message += f"\n\nStyle reference to follow:\n{style_example}" |
|
if slide_context: |
|
user_message += f"\n\nSlide context: {slide_context}" |
|
if uploaded_content: |
|
user_message += f"\n\nAdditional context from document:\n{uploaded_content[:500]}" |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_message |
|
} |
|
], |
|
"max_tokens": 300, |
|
"top_p": 0.8, |
|
"temperature": 0.7, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
prompt = result['choices'][0]['message']['content'].strip() |
|
print(f"[LLM] ํ๋กฌํํธ ์์ฑ ์๋ฃ: {prompt[:50]}...") |
|
return prompt |
|
else: |
|
error_msg = f"ํ๋กฌํํธ ์์ฑ ์คํจ: {response.status_code}" |
|
print(f"[LLM] {error_msg}") |
|
return error_msg |
|
except Exception as e: |
|
error_msg = f"ํ๋กฌํํธ ์์ฑ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}" |
|
print(f"[LLM] {error_msg}") |
|
return error_msg |
|
|
|
def translate_to_english(text: str) -> str: |
|
"""ํ๊ธ ํ
์คํธ๋ฅผ ์์ด๋ก ๋ฒ์ญ (LLM ์ฌ์ฉ)""" |
|
if not any(ord('๊ฐ') <= ord(char) <= ord('ํฃ') for char in text): |
|
return text |
|
|
|
print(f"[๋ฒ์ญ] ํ๊ธ ๊ฐ์ง, ์์ด๋ก ๋ฒ์ญ ์์") |
|
|
|
url = "https://api.friendli.ai/dedicated/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {FRIENDLI_TOKEN}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": "dep89a2fld32mcm", |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "You are a translator. Translate the given Korean text to English. Only return the translation without any explanation." |
|
}, |
|
{ |
|
"role": "user", |
|
"content": text |
|
} |
|
], |
|
"max_tokens": 500, |
|
"top_p": 0.8, |
|
"stream": False |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=30) |
|
if response.status_code == 200: |
|
result = response.json() |
|
translated = result['choices'][0]['message']['content'].strip() |
|
print(f"[๋ฒ์ญ] ์๋ฃ") |
|
return translated |
|
else: |
|
print(f"[๋ฒ์ญ] ์คํจ, ์๋ณธ ์ฌ์ฉ") |
|
return text |
|
except Exception as e: |
|
print(f"[๋ฒ์ญ] ์ค๋ฅ: {str(e)}, ์๋ณธ ์ฌ์ฉ") |
|
return text |
|
|
|
def generate_image(prompt: str, seed: int = 10, slide_info: str = "") -> Tuple[Image.Image, str]: |
|
"""Replicate API๋ฅผ ์ฌ์ฉํด ์ด๋ฏธ์ง ์์ฑ ๋๋ ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ""" |
|
print(f"\n[์ด๋ฏธ์ง ์์ฑ] {slide_info}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ํ๋กฌํํธ: {prompt[:50]}...") |
|
|
|
global current_topic, uploaded_content |
|
|
|
|
|
should_generate_process_flow = False |
|
|
|
|
|
slide_title = "" |
|
if ":" in slide_info: |
|
|
|
parts = slide_info.split(":") |
|
if len(parts) >= 2: |
|
slide_title = parts[1].strip() |
|
|
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์ถ์ถ๋ ์ฌ๋ผ์ด๋ ์ ๋ชฉ: '{slide_title}'") |
|
|
|
|
|
process_keywords = [ |
|
"ํ๋ก์ธ์ค", "์๋", "ํ๋ก์ฐ", "ํ๋ฆ", "์ํฌํ๋ก์ฐ", |
|
"์ ์ฐจ", "๋จ๊ณ", "์ฒ๋ฆฌ", "์งํ", "๊ฐ์" |
|
] |
|
|
|
|
|
is_workflow_style = False |
|
if any(style in prompt for style in ["Business Workflow", "Flowchart", "Business Process"]): |
|
is_workflow_style = True |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] Business Workflow ๋๋ Flowchart ์คํ์ผ ๊ฐ์ง") |
|
|
|
|
|
title_has_process = any(keyword in slide_title for keyword in process_keywords) |
|
prompt_has_process = any(keyword in prompt.lower() for keyword in ["process", "flow", "workflow"]) |
|
|
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์ ๋ชฉ์ ํ๋ก์ธ์ค ํค์๋: {title_has_process}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์ํฌํ๋ก์ฐ ์คํ์ผ: {is_workflow_style}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ํ๋กฌํํธ์ ํ๋ก์ธ์ค ํค์๋: {prompt_has_process}") |
|
|
|
|
|
if title_has_process and (is_workflow_style or prompt_has_process): |
|
should_generate_process_flow = True |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] โ
ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ ์กฐ๊ฑด ์ถฉ์กฑ!") |
|
|
|
|
|
if "๋ชฉ์ฐจ" in slide_title: |
|
should_generate_process_flow = False |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ๋ชฉ์ฐจ๋ ํ๋ก์ธ์ค ํ๋ก์ฐ ์ ์ธ") |
|
|
|
|
|
if PROCESS_FLOW_AVAILABLE and should_generate_process_flow: |
|
try: |
|
print("[์ด๋ฏธ์ง ์์ฑ] ๐ง ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ ์์...") |
|
|
|
|
|
img = generate_process_flow_for_ppt( |
|
topic=current_topic, |
|
context=slide_info, |
|
style="Business Workflow" |
|
) |
|
|
|
if isinstance(img, Image.Image): |
|
print("[์ด๋ฏธ์ง ์์ฑ] โ
ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ ์ฑ๊ณต!") |
|
return img, "Process flow diagram generated with Korean support" |
|
else: |
|
print("[์ด๋ฏธ์ง ์์ฑ] โ ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ ์คํจ, ์ผ๋ฐ ์ด๋ฏธ์ง๋ก ๋์ฒด") |
|
except Exception as e: |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] โ ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ ์ค๋ฅ: {str(e)}") |
|
import traceback |
|
traceback.print_exc() |
|
|
|
else: |
|
if not PROCESS_FLOW_AVAILABLE: |
|
print("[์ด๋ฏธ์ง ์์ฑ] โ ๏ธ ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ๊ธฐ๋ฅผ ์ฌ์ฉํ ์ ์์") |
|
else: |
|
print("[์ด๋ฏธ์ง ์์ฑ] โน๏ธ ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ, ์ผ๋ฐ ์ด๋ฏธ์ง ์์ฑ") |
|
|
|
|
|
try: |
|
english_prompt = translate_to_english(prompt) |
|
|
|
if not REPLICATE_API_TOKEN: |
|
error_msg = "RAPI_TOKEN ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค." |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์ค๋ฅ: {error_msg}") |
|
return None, error_msg |
|
|
|
print(f"[์ด๋ฏธ์ง ์์ฑ] Replicate API ํธ์ถ ์ค...") |
|
client = replicate.Client(api_token=REPLICATE_API_TOKEN) |
|
|
|
input_params = { |
|
"seed": seed, |
|
"prompt": english_prompt, |
|
"speed_mode": "Extra Juiced ๐ (even more speed)", |
|
"output_quality": 100 |
|
} |
|
|
|
start_time = time.time() |
|
output = client.run( |
|
"prunaai/hidream-l1-fast:17c237d753218fed0ed477cb553902b6b75735f48c128537ab829096ef3d3645", |
|
input=input_params |
|
) |
|
|
|
elapsed = time.time() - start_time |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] API ์๋ต ๋ฐ์ ({elapsed:.1f}์ด)") |
|
|
|
if output: |
|
if isinstance(output, str) and output.startswith('http'): |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] URL์์ ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ์ค...") |
|
response = requests.get(output, timeout=30) |
|
img = Image.open(BytesIO(response.content)) |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์๋ฃ!") |
|
return img, english_prompt |
|
else: |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ค...") |
|
img = Image.open(BytesIO(output.read())) |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์๋ฃ!") |
|
return img, english_prompt |
|
else: |
|
error_msg = "์ด๋ฏธ์ง ์์ฑ ์คํจ - ๋น ์๋ต" |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] {error_msg}") |
|
return None, error_msg |
|
|
|
except Exception as e: |
|
error_msg = f"์ค๋ฅ: {str(e)}" |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] {error_msg}") |
|
print(f"[์ด๋ฏธ์ง ์์ฑ] ์์ธ ์ค๋ฅ:\n{traceback.format_exc()}") |
|
return None, error_msg |
|
|
|
def create_slide_preview_html(slide_data: Dict) -> str: |
|
"""16:9 ๋น์จ์ ์ฌ๋ผ์ด๋ ํ๋ฆฌ๋ทฐ HTML ์์ฑ (ํธ์ง ๊ธฐ๋ฅ ์ ๊ฑฐ)""" |
|
|
|
|
|
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; |
|
background: rgba(255, 255, 255, 0.9); |
|
padding: 30px 60px; |
|
border-radius: 20px; |
|
box-shadow: 0 10px 40px rgba(0,0,0,0.3); |
|
backdrop-filter: blur(10px); |
|
"> |
|
""" |
|
|
|
if slide_data.get('title') == 'ํ์ง': |
|
html += f""" |
|
<h1 style="font-size: 48px; margin-bottom: 10px; color: #000000; font-weight: 700;">{slide_data.get('topic', '')}</h1> |
|
<h2 style="font-size: 24px; margin-top: 0; color: #212529; font-weight: 400;">{subtitle}</h2> |
|
""" |
|
else: |
|
html += f""" |
|
<h1 style="font-size: 42px; color: #000000; font-weight: 700; line-height: 1.2;">{subtitle}</h1> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
""" |
|
else: |
|
|
|
html += f""" |
|
<!-- ํ
์คํธ ์์ญ (์ข์ธก) --> |
|
<div class="text-area" style=" |
|
flex: 1; |
|
padding: 30px; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
background: rgba(255, 255, 255, 0.95); |
|
border-radius: 12px; |
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); |
|
"> |
|
<h2 style=" |
|
color: #212529; |
|
font-size: 28px; |
|
margin-bottom: 30px; |
|
font-weight: 600; |
|
">{subtitle}</h2> |
|
|
|
<ul style=" |
|
list-style: none; |
|
padding: 0; |
|
margin: 0; |
|
"> |
|
""" |
|
|
|
for point in bullet_points: |
|
clean_point = point.replace('โข', '').strip() |
|
html += f""" |
|
<li style=" |
|
margin-bottom: 16px; |
|
padding-left: 28px; |
|
position: relative; |
|
color: #495057; |
|
font-size: 16px; |
|
line-height: 1.6; |
|
"> |
|
<span style=" |
|
position: absolute; |
|
left: 0; |
|
color: #007bff; |
|
font-size: 18px; |
|
">โข</span> |
|
{clean_point} |
|
</li> |
|
""" |
|
|
|
html += f""" |
|
</ul> |
|
</div> |
|
|
|
<!-- ์ด๋ฏธ์ง ์์ญ (์ฐ์ธก) --> |
|
<div class="image-area" style=" |
|
flex: 1; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 20px; |
|
"> |
|
""" |
|
|
|
if img_base64: |
|
html += f""" |
|
<img src="data:image/png;base64,{img_base64}" style=" |
|
max-width: 100%; |
|
max-height: 100%; |
|
object-fit: contain; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
" alt="Slide Image"> |
|
""" |
|
else: |
|
html += """ |
|
<div style=" |
|
color: #6c757d; |
|
text-align: center; |
|
"> |
|
<div style="font-size: 48px;">๐ผ๏ธ</div> |
|
<p>์ด๋ฏธ์ง ์์ฑ ์ค...</p> |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
""" |
|
|
|
html += """ |
|
</div> |
|
</div> |
|
</div> |
|
""" |
|
|
|
return html |
|
|
|
def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_name: str = "๋ฏธ๋๋ฉ ๋ผ์ดํธ") -> str: |
|
"""์์ฑ๋ ๊ฒฐ๊ณผ๋ฅผ PPTX ํ์ผ๋ก ๋ณํ (๋ฐํ์ ๋
ธํธ ํฌํจ)""" |
|
print(f"[PPTX] ํ์ผ ์์ฑ ์์... ํ
๋ง: {theme_name}") |
|
|
|
|
|
prs = Presentation() |
|
prs.slide_width = Inches(16) |
|
prs.slide_height = Inches(9) |
|
|
|
|
|
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋๋ฉ ๋ผ์ดํธ"]) |
|
|
|
|
|
for i, result in enumerate(results): |
|
if not result.get("success", False): |
|
continue |
|
|
|
slide_data = result.get("slide_data", {}) |
|
|
|
|
|
blank_layout = prs.slide_layouts[6] |
|
slide = prs.slides.add_slide(blank_layout) |
|
|
|
|
|
if slide_data.get('title') == 'ํ์ง': |
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
0, 0, |
|
width=prs.slide_width, |
|
height=prs.slide_height |
|
) |
|
|
|
slide.shapes._spTree.remove(pic._element) |
|
slide.shapes._spTree.insert(2, pic._element) |
|
except Exception as e: |
|
print(f"[PPTX] ํ์ง ์ด๋ฏธ์ง ์ถ๊ฐ ์คํจ: {str(e)}") |
|
|
|
|
|
title_bg = slide.shapes.add_shape( |
|
MSO_SHAPE.ROUNDED_RECTANGLE, |
|
Inches(2), Inches(2.8), |
|
Inches(12), Inches(3.2) |
|
) |
|
title_bg.fill.solid() |
|
title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) |
|
title_bg.fill.transparency = 0.8 |
|
title_bg.line.fill.background() |
|
|
|
|
|
shadow = title_bg.shadow |
|
shadow.visible = True |
|
shadow.distance = Pt(6) |
|
shadow.size = 100 |
|
shadow.blur_radius = Pt(12) |
|
shadow.transparency = 0.8 |
|
shadow.angle = 45 |
|
|
|
|
|
title_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(3.2), |
|
Inches(12), Inches(1.5) |
|
) |
|
title_frame = title_box.text_frame |
|
title_frame.text = topic |
|
title_para = title_frame.paragraphs[0] |
|
title_para.font.size = Pt(48) |
|
title_para.font.bold = True |
|
title_para.font.color.rgb = RGBColor(0, 0, 0) |
|
title_para.alignment = PP_ALIGN.CENTER |
|
|
|
|
|
subtitle_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(4.3), |
|
Inches(12), Inches(1.0) |
|
) |
|
subtitle_frame = subtitle_box.text_frame |
|
subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI ํ๋ ์ ํ
์ด์
') |
|
subtitle_para = subtitle_frame.paragraphs[0] |
|
subtitle_para.font.size = Pt(28) |
|
subtitle_para.font.color.rgb = RGBColor(33, 37, 41) |
|
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(2), Inches(3.5), |
|
Inches(12), Inches(2.5) |
|
) |
|
thanks_bg.fill.solid() |
|
thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) |
|
thanks_bg.fill.transparency = 0.8 |
|
thanks_bg.line.fill.background() |
|
|
|
|
|
shadow = thanks_bg.shadow |
|
shadow.visible = True |
|
shadow.distance = Pt(6) |
|
shadow.size = 100 |
|
shadow.blur_radius = Pt(12) |
|
shadow.transparency = 0.8 |
|
shadow.angle = 45 |
|
|
|
|
|
thanks_box = slide.shapes.add_textbox( |
|
Inches(2), Inches(4), |
|
Inches(12), Inches(1.5) |
|
) |
|
thanks_frame = thanks_box.text_frame |
|
thanks_frame.text = slide_data.get('subtitle', 'Thank You') |
|
thanks_para = thanks_frame.paragraphs[0] |
|
thanks_para.font.size = Pt(42) |
|
thanks_para.font.bold = True |
|
thanks_para.font.color.rgb = RGBColor(0, 0, 0) |
|
thanks_para.alignment = PP_ALIGN.CENTER |
|
|
|
|
|
else: |
|
|
|
background = slide.background |
|
fill = background.fill |
|
fill.solid() |
|
fill.fore_color.rgb = theme["background"] |
|
|
|
|
|
title_box_bg = slide.shapes.add_shape( |
|
MSO_SHAPE.ROUNDED_RECTANGLE, |
|
Inches(0.3), Inches(0.2), |
|
Inches(15.4), Inches(1.0) |
|
) |
|
title_box_bg.fill.solid() |
|
title_box_bg.fill.fore_color.rgb = theme["box_fill"] |
|
title_box_bg.fill.transparency = 1 - theme["box_opacity"] |
|
|
|
|
|
if theme["shadow"]: |
|
shadow = title_box_bg.shadow |
|
shadow.visible = True |
|
shadow.distance = Pt(4) |
|
shadow.size = 100 |
|
shadow.blur_radius = Pt(8) |
|
shadow.transparency = 0.75 |
|
shadow.angle = 45 |
|
|
|
title_box_bg.line.fill.background() |
|
|
|
|
|
title_box = slide.shapes.add_textbox( |
|
Inches(0.5), Inches(0.3), |
|
Inches(15), Inches(0.8) |
|
) |
|
title_frame = title_box.text_frame |
|
title_frame.text = f"{slide_data.get('title', '')}" |
|
title_para = title_frame.paragraphs[0] |
|
title_para.font.size = Pt(28) |
|
title_para.font.bold = True |
|
title_para.font.color.rgb = theme["title_color"] |
|
|
|
|
|
text_box_bg = slide.shapes.add_shape( |
|
MSO_SHAPE.ROUNDED_RECTANGLE, |
|
Inches(0.3), Inches(1.4), |
|
Inches(7.8), Inches(6.8) |
|
) |
|
text_box_bg.fill.solid() |
|
text_box_bg.fill.fore_color.rgb = theme["box_fill"] |
|
text_box_bg.fill.transparency = 1 - theme["box_opacity"] |
|
|
|
if theme["shadow"]: |
|
shadow = text_box_bg.shadow |
|
shadow.visible = True |
|
shadow.distance = Pt(5) |
|
shadow.size = 100 |
|
shadow.blur_radius = Pt(10) |
|
shadow.transparency = 0.7 |
|
shadow.angle = 45 |
|
|
|
text_box_bg.line.fill.background() |
|
|
|
|
|
text_box = slide.shapes.add_textbox( |
|
Inches(0.8), Inches(1.8), |
|
Inches(7.0), Inches(6.0) |
|
) |
|
text_frame = text_box.text_frame |
|
text_frame.word_wrap = True |
|
|
|
|
|
subtitle_para = text_frame.paragraphs[0] |
|
subtitle_para.text = slide_data.get('subtitle', '') |
|
subtitle_para.font.size = Pt(20) |
|
subtitle_para.font.bold = True |
|
subtitle_para.font.color.rgb = theme["subtitle_color"] |
|
subtitle_para.space_after = Pt(20) |
|
|
|
|
|
bullet_points = slide_data.get('bullet_points', []) |
|
for point in bullet_points: |
|
p = text_frame.add_paragraph() |
|
|
|
clean_text = point.replace('โข', '').strip() |
|
p.text = clean_text |
|
p.font.size = Pt(16) |
|
p.font.color.rgb = theme["text_color"] |
|
|
|
p.level = 0 |
|
p.space_after = Pt(12) |
|
p.line_spacing = 1.5 |
|
|
|
p.left_indent = Pt(0) |
|
|
|
|
|
p.font.color.rgb = theme["text_color"] |
|
|
|
|
|
if slide_data.get('image'): |
|
try: |
|
img_buffer = BytesIO() |
|
slide_data['image'].save(img_buffer, format='PNG') |
|
img_buffer.seek(0) |
|
|
|
pic = slide.shapes.add_picture( |
|
img_buffer, |
|
Inches(8.5), Inches(1.6), |
|
width=Inches(6.8), height=Inches(6.4) |
|
) |
|
|
|
|
|
pic.line.fill.background() |
|
|
|
except Exception as e: |
|
print(f"[PPTX] ์ด๋ฏธ์ง ์ถ๊ฐ ์คํจ: {str(e)}") |
|
|
|
|
|
page_num = slide.shapes.add_textbox( |
|
Inches(15), Inches(8.5), |
|
Inches(1), Inches(0.5) |
|
) |
|
page_frame = page_num.text_frame |
|
page_frame.text = str(i + 1) |
|
page_para = page_frame.paragraphs[0] |
|
page_para.font.size = Pt(12) |
|
page_para.font.color.rgb = theme["text_color"] |
|
page_para.alignment = PP_ALIGN.RIGHT |
|
|
|
|
|
notes_slide = slide.notes_slide |
|
notes_slide.notes_text_frame.text = slide_data.get('speaker_notes', '') |
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
filename = f"presentation_{timestamp}.pptx" |
|
filepath = os.path.join("/tmp", filename) |
|
prs.save(filepath) |
|
|
|
print(f"[PPTX] ํ์ผ ์์ฑ ์๋ฃ: {filename}") |
|
return filepath |
|
|
|
def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> List[Dict]: |
|
"""์ ํ๋ ์ฌ๋ผ์ด๋ ์์ ๋ฐ๋ผ ๋์ ์ผ๋ก ์ฌ๋ผ์ด๋ ๊ตฌ์ฑ""" |
|
core_slides = template.get("core_slides", []) |
|
optional_slides = template.get("optional_slides", []) |
|
|
|
|
|
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 create_custom_slides_ui(initial_count=5): |
|
"""์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ๊ตฌ์ฑ UI (3-20์ฅ)""" |
|
slides = [] |
|
for i in range(20): |
|
row = gr.Row(visible=(i < initial_count)) |
|
with row: |
|
with gr.Column(scale=2): |
|
title = gr.Textbox( |
|
label=f"์ฌ๋ผ์ด๋ {i+1} ์ ๋ชฉ", |
|
placeholder="์: ํํฉ ๋ถ์, ์๋ฃจ์
, ๋ก๋๋งต...", |
|
) |
|
with gr.Column(scale=3): |
|
style = gr.Dropdown( |
|
choices=list(STYLE_TEMPLATES.keys()), |
|
label=f"์คํ์ผ ์ ํ", |
|
value="Colorful Mind Map" |
|
) |
|
with gr.Column(scale=3): |
|
hint = gr.Textbox( |
|
label=f"ํ๋กฌํํธ ํํธ", |
|
placeholder="์ด ์ฌ๋ผ์ด๋์์ ํํํ๊ณ ์ถ์ ๋ด์ฉ" |
|
) |
|
slides.append({"title": title, "style": style, "hint": hint, "row": row}) |
|
return slides |
|
|
|
def generate_ppt_with_content(topic: str, template_name: str, audience_type: str, custom_slides: List[Dict], |
|
slide_count: int, seed: int, uploaded_file, |
|
use_web_search: bool, theme_name: str = "๋ฏธ๋๋ฉ ๋ผ์ดํธ", |
|
progress=gr.Progress()): |
|
"""PPT ์ด๋ฏธ์ง์ ํ
์คํธ ๋ด์ฉ์ ํจ๊ป ์์ฑ (๋ฐํ์ ๋
ธํธ ํฌํจ)""" |
|
results = [] |
|
preview_html = "" |
|
|
|
|
|
global current_topic, uploaded_content |
|
current_topic = topic |
|
|
|
|
|
uploaded_content = "" |
|
if uploaded_file is not None: |
|
try: |
|
uploaded_content = read_uploaded_file(uploaded_file.name) |
|
print(f"[ํ์ผ ์
๋ก๋] ๋ด์ฉ ๊ธธ์ด: {len(uploaded_content)}์") |
|
except Exception as e: |
|
print(f"[ํ์ผ ์
๋ก๋] ์ค๋ฅ: {str(e)}") |
|
uploaded_content = "" |
|
|
|
|
|
if template_name == "์ฌ์ฉ์ ์ ์" and custom_slides: |
|
slides = [{"title": "ํ์ง", "style": "Title Slide (Hero)", "prompt_hint": "ํ๋ ์ ํ
์ด์
ํ์ง"}] |
|
slides.extend(custom_slides) |
|
slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ํ๋ ์ ํ
์ด์
๋ง๋ฌด๋ฆฌ์ ํต์ฌ ๋ฉ์์ง"}) |
|
else: |
|
template = PPT_TEMPLATES[template_name] |
|
slides = generate_dynamic_slides(topic, template, slide_count) |
|
|
|
if not slides: |
|
yield "", "์ฌ๋ผ์ด๋๊ฐ ์ ์๋์ง ์์์ต๋๋ค.", None |
|
return |
|
|
|
total_slides = len(slides) |
|
print(f"\n[PPT ์์ฑ] ์์ - ์ด {total_slides}๊ฐ ์ฌ๋ผ์ด๋ (ํ์ง + ๋ณธ๋ฌธ {slide_count} + Thank You)") |
|
print(f"[PPT ์์ฑ] ์ฃผ์ : {topic}") |
|
print(f"[PPT ์์ฑ] ํ
ํ๋ฆฟ: {template_name}") |
|
print(f"[PPT ์์ฑ] ์ค๋์ธ์ค: {audience_type}") |
|
print(f"[PPT ์์ฑ] ๋์์ธ ํ
๋ง: {theme_name}") |
|
print(f"[PPT ์์ฑ] ์น ๊ฒ์: {'์ฌ์ฉ' if use_web_search else '๋ฏธ์ฌ์ฉ'}") |
|
|
|
|
|
web_search_results = [] |
|
if use_web_search and BRAVE_API_TOKEN: |
|
progress(0.05, "์น ๊ฒ์ ์ค...") |
|
web_search_results = brave_search(topic) |
|
|
|
|
|
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', '')}" |
|
|
|
|
|
if slide['title'] == 'Thank You': |
|
conclusion_phrase = generate_conclusion_phrase(topic, audience_type) |
|
content = { |
|
"subtitle": conclusion_phrase, |
|
"bullet_points": [] |
|
} |
|
|
|
speaker_notes = generate_closing_notes(topic, conclusion_phrase, audience_type) |
|
else: |
|
content = generate_slide_content( |
|
topic, slide['title'], slide_context, audience_type, |
|
uploaded_content, web_search_results |
|
) |
|
|
|
speaker_notes = generate_presentation_notes(topic, slide['title'], content, audience_type) |
|
|
|
|
|
style_key = slide["style"] |
|
if style_key in STYLE_TEMPLATES: |
|
style_info = STYLE_TEMPLATES[style_key] |
|
prompt = generate_prompt_with_llm( |
|
topic, style_info["example"], |
|
slide_context, uploaded_content |
|
) |
|
|
|
|
|
slide_seed = seed + i |
|
img, used_prompt = generate_image(prompt, slide_seed, slide_info) |
|
|
|
|
|
slide_data = { |
|
"slide_number": i + 1, |
|
"title": slide["title"], |
|
"subtitle": content["subtitle"], |
|
"bullet_points": content["bullet_points"], |
|
"image": img, |
|
"style": style_info["name"], |
|
"speaker_notes": speaker_notes, |
|
"topic": topic |
|
} |
|
|
|
|
|
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}๊ฐ ์ฑ๊ณต" |
|
|
|
|
|
global current_slides_data, current_template, current_theme |
|
current_slides_data = results |
|
current_template = template_name |
|
current_theme = theme_name |
|
|
|
if pptx_path: |
|
final_status += f"\n\n### ๐ฅ PPTX ํ์ผ์ด ์ค๋น๋์์ต๋๋ค!" |
|
final_status += f"\n\n๐ค **{audience_type}๋ฅผ ์ํ ๋ฐํ์ ๋
ธํธ๊ฐ ๊ฐ ์ฌ๋ผ์ด๋์ ํฌํจ๋์ด ์์ต๋๋ค!**" |
|
if PROCESS_FLOW_AVAILABLE: |
|
final_status += f"\n\n๐ง **ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ์ด ์๋์ผ๋ก ์์ฑ๋ฉ๋๋ค (ํ๊ธ ์ง์)**" |
|
|
|
yield preview_html, final_status, pptx_path |
|
|
|
|
|
with gr.Blocks(title="PPT ์ด๋ฏธ์ง ์์ฑ๊ธฐ", theme=gr.themes.Soft(), css=""" |
|
.preview-container { max-width: 1400px; margin: 0 auto; } |
|
.slide-container { transition: all 0.3s ease; } |
|
.slide-container:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important; } |
|
#custom_accordion .form { |
|
max-height: 600px; |
|
overflow-y: auto; |
|
padding-right: 10px; |
|
} |
|
#custom_accordion .form::-webkit-scrollbar { |
|
width: 6px; |
|
} |
|
#custom_accordion .form::-webkit-scrollbar-track { |
|
background: #f1f1f1; |
|
border-radius: 3px; |
|
} |
|
#custom_accordion .form::-webkit-scrollbar-thumb { |
|
background: #888; |
|
border-radius: 3px; |
|
} |
|
#custom_accordion .form::-webkit-scrollbar-thumb:hover { |
|
background: #555; |
|
} |
|
""") as demo: |
|
gr.Markdown(""" |
|
# ๐ฏ AI ๊ธฐ๋ฐ PPT ํตํฉ ์์ฑ๊ธฐ (๊ฐ๊ฒฐํ ์คํ์ผ ๋ฒ์ ) |
|
|
|
### ํ
์คํธ์ ์ด๋ฏธ์ง๊ฐ ์๋ฒฝํ๊ฒ ์กฐํ๋ ํ๋ ์ ํ
์ด์
์ ์๋์ผ๋ก ์์ฑํ์ธ์! |
|
|
|
#### ๐ ์๋ก์ด ๊ธฐ๋ฅ: |
|
- ๐ญ **์ค๋์ธ์ค๋ณ ์ต์ ํ**: ๋ฐํ ๋์์ ๋ง์ถ ๋ด์ฉ๊ณผ ํค ์๋ ์กฐ์ |
|
- ๐ **๊ฐ๊ฒฐํ ๋ช
์ฌํ ํ
์คํธ**: "~์", "~ํจ" ์คํ์ผ์ ํ๋กํ์
๋ํ ๋ฌธ์ฒด |
|
- ๐จ **์ด๋ชจ์ง ์๋ ์ถ๊ฐ**: ๊ฐ ํฌ์ธํธ๋ณ ์ ์ ํ ์ด๋ชจ์ง๋ก ์๊ฐ์ ๊ตฌ๋ถ |
|
- ๐ **์์ ํ
ํ๋ฆฟ**: ๋ฒํผ ํด๋ฆญ์ผ๋ก ์์ ์ฃผ์ ๋ถ๋ฌ์ค๊ธฐ |
|
- ๐จ **๋์์ธ ํ
๋ง**: 5๊ฐ์ง ์ ๋ฌธ์ ์ธ ๋์์ธ ํ
๋ง ์ ํ ๊ฐ๋ฅ |
|
- ๐ **๊ฐ์ ๋ ์ฌ๋ผ์ด๋ ๋์์ธ**: ๋ ํฌ๋ช
ํ ๋ฐฐ๊ฒฝ๊ณผ ๊น๋ํ ๋ ์ด์์ |
|
- ๐ **ํ์ง์ Thank You ์ฌ๋ผ์ด๋** ์๋ ์ถ๊ฐ |
|
- ๐ค **์ค๋์ธ์ค๋ณ ๋ฐํ์ ๋
ธํธ** ์๋ ์์ฑ (๊ตฌ์ด์ฒด) |
|
- ๐ **ํ์ผ ์
๋ก๋** ์ง์ (PDF/CSV/TXT) |
|
- ๐ **์น ๊ฒ์** ๊ธฐ๋ฅ (Brave Search) |
|
- ๐๏ธ **์ฌ๋ผ์ด๋ ์ ์กฐ์ ** (6-20์ฅ) |
|
- ๐ง **ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ** ์๋ ์์ฑ (ํ๊ธ ์ง์) |
|
- ๐ฏ **์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋**: 3-20์ฅ์ ์์ ๋กญ๊ฒ ๊ตฌ์ฑํ์ฌ ๋๋ง์ ํ๋ ์ ํ
์ด์
์ ์ |
|
""") |
|
|
|
|
|
token_status = [] |
|
if not REPLICATE_API_TOKEN: |
|
token_status.append("โ ๏ธ RAPI_TOKEN ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
|
if not FRIENDLI_TOKEN: |
|
token_status.append("โ ๏ธ FRIENDLI_TOKEN ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
|
if not BRAVE_API_TOKEN: |
|
token_status.append("โน๏ธ BAPI_TOKEN์ด ์์ด ์น ๊ฒ์ ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ฉ๋๋ค.") |
|
if not PROCESS_FLOW_AVAILABLE: |
|
token_status.append("โน๏ธ process_flow_generator๊ฐ ์์ด ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ์ด ๋นํ์ฑํ๋ฉ๋๋ค.") |
|
|
|
if token_status: |
|
gr.Markdown("\n".join(token_status)) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
with gr.Row(): |
|
example_btn = gr.Button("๐ ์์ ๋ถ๋ฌ์ค๊ธฐ", size="sm", variant="secondary") |
|
|
|
|
|
topic_input = gr.Textbox( |
|
label="ํ๋ ์ ํ
์ด์
์ฃผ์ ", |
|
placeholder="์: AI ์คํํธ์
ํฌ์ ์ ์น, ์ ์ ํ ๋ฐ์นญ, ๋์งํธ ์ ํ ์ ๋ต", |
|
lines=2 |
|
) |
|
|
|
|
|
audience_select = gr.Dropdown( |
|
choices=list(AUDIENCE_TYPES.keys()), |
|
label="๐ญ ๋ฐํ ๋์ ์ค๋์ธ์ค", |
|
value="์ผ๋ฐ ์ง์", |
|
info="๋ฐํ ๋์์ ๋ฐ๋ผ ๋ด์ฉ๊ณผ ํค์ด ์๋์ผ๋ก ์ต์ ํ๋ฉ๋๋ค" |
|
) |
|
|
|
|
|
audience_info = gr.Markdown() |
|
|
|
|
|
template_select = gr.Dropdown( |
|
choices=list(PPT_TEMPLATES.keys()), |
|
label="PPT ํ
ํ๋ฆฟ ์ ํ", |
|
value="๋น์ฆ๋์ค ์ ์์", |
|
info="๋ชฉ์ ์ ๋ง๋ ํ
ํ๋ฆฟ์ ์ ํํ์ธ์" |
|
) |
|
|
|
|
|
theme_select = gr.Dropdown( |
|
choices=list(DESIGN_THEMES.keys()), |
|
label="๋์์ธ ํ
๋ง ์ ํ", |
|
value="๋ฏธ๋๋ฉ ๋ผ์ดํธ", |
|
info="ํ๋ ์ ํ
์ด์
์ ์ ์ฒด์ ์ธ ๋์์ธ ์คํ์ผ" |
|
) |
|
|
|
|
|
theme_preview = gr.HTML() |
|
|
|
|
|
slide_count = gr.Slider( |
|
minimum=6, |
|
maximum=20, |
|
value=8, |
|
step=1, |
|
label="๋ณธ๋ฌธ ์ฌ๋ผ์ด๋ ์ (ํ์ง์ Thank You ์ ์ธ)", |
|
info="์์ฑํ ๋ณธ๋ฌธ ์ฌ๋ผ์ด๋ ์๋ฅผ ์ ํํ์ธ์ (์ฌ์ฉ์ ์ ์ ํ
ํ๋ฆฟ์์๋ ์ฌ์ฉ๋์ง ์์)" |
|
) |
|
|
|
|
|
file_upload = gr.File( |
|
label="์ฐธ๊ณ ์๋ฃ ์
๋ก๋ (์ ํ)", |
|
file_types=[".pdf", ".csv", ".txt"], |
|
type="filepath", |
|
value=None |
|
) |
|
|
|
|
|
use_web_search = gr.Checkbox( |
|
label="์น ๊ฒ์ ์ฌ์ฉ", |
|
value=False, |
|
info="Brave Search๋ฅผ ์ฌ์ฉํ์ฌ ์ต์ ์ ๋ณด ๋ฐ์" |
|
) |
|
|
|
|
|
template_info = gr.Markdown() |
|
|
|
|
|
seed_input = gr.Slider( |
|
minimum=1, |
|
maximum=100, |
|
value=10, |
|
step=1, |
|
label="์๋ ๊ฐ" |
|
) |
|
|
|
generate_btn = gr.Button("๐ PPT ์์ฑ ์์ (AI๊ฐ ๋ชจ๋ ๋ด์ฉ์ ์๋ ์์ฑ)", variant="primary", size="lg") |
|
|
|
|
|
with gr.Accordion("๐ ์ฌ์ฉ ๋ฐฉ๋ฒ", open=False): |
|
gr.Markdown(""" |
|
### ๐ ์์
์์: |
|
1. **์์ ๋ถ๋ฌ์ค๊ธฐ** ๋ฒํผ์ผ๋ก ์ํ ์ฃผ์ ๋ฅผ ๋ก๋ํ๊ฑฐ๋ ์ง์ ์
๋ ฅ |
|
2. **์ค๋์ธ์ค ์ ํ**: ๋ฐํ ๋์์ ๋ฐ๋ผ ๋ด์ฉ์ด ์๋์ผ๋ก ์ต์ ํ๋ฉ๋๋ค |
|
3. **ํ
ํ๋ฆฟ๊ณผ ํ
๋ง ์ ํ** ํ ์์ฑ ๋ฒํผ ํด๋ฆญ |
|
4. **๋ค์ด๋ก๋**: ์์ฑ ์๋ฃ ํ PPTX ํ์ผ ๋ค์ด๋ก๋ |
|
|
|
### ๐ญ ์ค๋์ธ์ค๋ณ ํน์ง: |
|
- **๊ฒฝ์์ง/์์**: ์ ๋ต์ ๊ฐ์น, ROI, ๋น์ฆ๋์ค ์ํฉํธ ์ค์ฌ |
|
- **ํฌ์์**: ์์ฅ ๊ธฐํ, ์ฑ์ฅ ๊ฐ๋ฅ์ฑ, ์์ต์ฑ ๊ฐ์กฐ |
|
- **๊ธฐ์ ํ**: ๊ธฐ์ ์ ์ธ๋ถ์ฌํญ, ๊ตฌํ ๋ฐฉ๋ฒ, ์ฑ๋ฅ ์งํ |
|
- **์ผ๋ฐ ์ง์**: ์ค๋ฌด์ ๋ด์ฉ, ํ์
๋ฐฉ์, ์คํ ๊ณํ |
|
- **๊ณ ๊ฐ/ํํธ๋**: ๊ณ ๊ฐ ๊ฐ์น, ํํ, ์ฑ๊ณต ์ฌ๋ก |
|
- **์ผ๋ฐ ๋์ค**: ์ฌ์ด ์ค๋ช
, ์ฌ์ฉ ํธ์์ฑ, ์ค์ง์ ์ด์ |
|
|
|
### ๐ก ํ
์คํธ ์คํ์ผ ํน์ง: |
|
- **๊ฐ๊ฒฐํ ๋ช
์ฌํ ์ข
๊ฒฐ**: "~์", "~ํจ" ๋๋ ๋ช
์ฌ๋ก ๋๋จ |
|
- **์ด๋ชจ์ง ํ์ฉ**: ๊ฐ ํฌ์ธํธ ์์ ๋ด์ฉ ๊ด๋ จ ์ด๋ชจ์ง ์๋ ์ถ๊ฐ |
|
- **8-12๋จ์ด**: ๊ฐ ๋ถ๋ฆฟ ํฌ์ธํธ๋ ๋งค์ฐ ๊ฐ๊ฒฐํ๊ฒ ๊ตฌ์ฑ |
|
|
|
### ๐ ์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋: |
|
- **ํ
ํ๋ฆฟ ๋์ ์ง์ ๊ตฌ์ฑ**: "์ฌ์ฉ์ ์ ์" ํ
ํ๋ฆฟ ์ ํ ์ ํ์ฑํ |
|
- **3-20์ฅ ์์ ๋กญ๊ฒ ๊ตฌ์ฑ**: ์ฌ๋ผ์ด๋๋ก ์ํ๋ ์ฌ๋ผ์ด๋ ์ ์ ํ |
|
- **๊ฐ ์ฌ๋ผ์ด๋๋ณ ์คํ์ผ ์ง์ **: 16๊ฐ์ง ์คํ์ผ ์ค ์ ํ |
|
- **ํํธ ์ ๊ณต**: ๊ฐ ์ฌ๋ผ์ด๋์ ๋ด์ฉ ๋ฐฉํฅ์ ํ๋กฌํํธ ํํธ๋ก ์ง์ |
|
|
|
### ๐ง ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ: |
|
- **Business Workflow** ๋๋ **Flowchart** ์คํ์ผ ์ ํ ์ ์๋์ผ๋ก ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ |
|
- ํ๊ธ ํ
์คํธ๋ฅผ ์๋ฒฝํ๊ฒ ์ง์ํ๋ ๋ค์ด์ด๊ทธ๋จ |
|
- ํ๋ก์ธ์ค, ์์ฌ๊ฒฐ์ , ์
์ถ๋ ฅ ๋ฑ ๋ค์ํ ๋
ธ๋ ํ์
์ง์ |
|
""") |
|
|
|
|
|
with gr.Row(): |
|
download_file = gr.File( |
|
label="๐ฅ ์์ฑ๋ PPTX ํ์ผ ๋ค์ด๋ก๋", |
|
visible=True, |
|
elem_id="download-file" |
|
) |
|
|
|
|
|
with gr.Accordion("๐ ์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ๊ตฌ์ฑ", open=False, elem_id="custom_accordion") as custom_accordion: |
|
gr.Markdown("ํ
ํ๋ฆฟ์ ์ฌ์ฉํ์ง ์๊ณ ์ง์ ์ฌ๋ผ์ด๋๋ฅผ ๊ตฌ์ฑํ์ธ์. (3-20์ฅ)") |
|
|
|
|
|
custom_slide_count = gr.Slider( |
|
minimum=3, |
|
maximum=20, |
|
value=5, |
|
step=1, |
|
label="์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ์", |
|
info="์์ฑํ ์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ์๋ฅผ ์ ํํ์ธ์" |
|
) |
|
|
|
custom_slides_components = create_custom_slides_ui(initial_count=5) |
|
|
|
|
|
status_output = gr.Markdown( |
|
value="### ๐ ํ
ํ๋ฆฟ์ ์ ํํ๊ณ ์์ฑ ๋ฒํผ์ ํด๋ฆญํ์ธ์!" |
|
) |
|
|
|
|
|
preview_output = gr.HTML( |
|
label="PPT ํ๋ฆฌ๋ทฐ (16:9)", |
|
elem_classes="preview-container" |
|
) |
|
|
|
|
|
def load_example(template_name): |
|
"""ํ
ํ๋ฆฟ์ ๋ง๋ ์์ ์ฃผ์ ๋ก๋""" |
|
example_topic = EXAMPLE_TOPICS.get(template_name, "") |
|
return example_topic |
|
|
|
def update_audience_info(audience_type): |
|
"""์ค๋์ธ์ค ์ ๋ณด ํ์""" |
|
info = AUDIENCE_TYPES.get(audience_type, {}) |
|
return f"""**{info.get('description', '')}** |
|
- ํค: {info.get('tone', '')} |
|
- ํฌ์ปค์ค: {info.get('focus', '')}""" |
|
|
|
def update_theme_preview(theme_name): |
|
"""ํ
๋ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ HTML ์์ฑ""" |
|
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["๋ฏธ๋๋ฉ ๋ผ์ดํธ"]) |
|
|
|
|
|
def rgb_to_hex(rgb_color): |
|
return f"#{rgb_color[0]:02x}{rgb_color[1]:02x}{rgb_color[2]:02x}" |
|
|
|
bg_hex = rgb_to_hex(theme["background"]) |
|
box_hex = rgb_to_hex(theme["box_fill"]) |
|
title_hex = rgb_to_hex(theme["title_color"]) |
|
text_hex = rgb_to_hex(theme["text_color"]) |
|
accent_hex = rgb_to_hex(theme["accent_color"]) |
|
|
|
preview_html = f""" |
|
<div style=" |
|
background: {bg_hex}; |
|
padding: 20px; |
|
border-radius: 8px; |
|
margin: 10px 0; |
|
"> |
|
<div style=" |
|
background: {box_hex}; |
|
opacity: {theme['box_opacity']}; |
|
padding: 15px; |
|
border-radius: 12px; |
|
margin-bottom: 10px; |
|
{'box-shadow: 0 4px 8px rgba(0,0,0,0.1);' if theme['shadow'] else ''} |
|
"> |
|
<h4 style="color: {title_hex}; margin: 0 0 10px 0;">{theme['name']}</h4> |
|
<p style="color: {text_hex}; margin: 0; font-size: 14px;"> |
|
{theme['description']} |
|
</p> |
|
<div style=" |
|
width: 40px; |
|
height: 4px; |
|
background: {accent_hex}; |
|
margin-top: 10px; |
|
border-radius: 2px; |
|
"></div> |
|
</div> |
|
</div> |
|
""" |
|
return preview_html |
|
|
|
def update_template_info(template_name, slide_count): |
|
is_custom = template_name == "์ฌ์ฉ์ ์ ์" |
|
info = "" |
|
|
|
if not is_custom and template_name in PPT_TEMPLATES: |
|
template = PPT_TEMPLATES[template_name] |
|
info = f"**{template['description']}**\n\n" |
|
|
|
|
|
slides = generate_dynamic_slides("", template, slide_count) |
|
info += f"์์ฑ๋ ์ฌ๋ผ์ด๋ ({len(slides)}์ฅ):\n" |
|
for i, slide in enumerate(slides): |
|
style_info = STYLE_TEMPLATES.get(slide['style'], {}) |
|
info += f"{i+1}. {slide['title']} - {style_info.get('use_case', '')}" |
|
if style_info.get('is_process_flow'): |
|
info += " ๐ง" |
|
info += "\n" |
|
elif is_custom: |
|
info = "**์ฌ์ฉ์ ์ ์ ํ
ํ๋ฆฟ**\n\n์๋ '์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ๊ตฌ์ฑ' ์น์
์์ ์ง์ ์ฌ๋ผ์ด๋๋ฅผ ๊ตฌ์ฑํ์ธ์." |
|
|
|
|
|
slide_count_visible = not is_custom |
|
custom_accordion_open = is_custom |
|
|
|
return info, gr.update(visible=slide_count_visible), gr.update(open=custom_accordion_open) |
|
|
|
def update_custom_slides_visibility(count): |
|
"""์ฌ์ฉ์ ์ ์ ์ฌ๋ผ์ด๋ ์์ ๋ฐ๋ผ UI ํ์/์จ๊น""" |
|
updates = [] |
|
for i in range(20): |
|
updates.append(gr.update(visible=(i < count))) |
|
return updates |
|
|
|
def generate_ppt_handler(topic, template_name, audience_type, theme_name, slide_count, seed, file_upload, |
|
use_web_search, custom_slide_count, progress=gr.Progress(), |
|
*custom_inputs): |
|
if not topic.strip(): |
|
yield "", "โ ์ฃผ์ ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", None |
|
return |
|
|
|
|
|
custom_slides = [] |
|
if template_name == "์ฌ์ฉ์ ์ ์": |
|
for i in range(0, custom_slide_count * 3, 3): |
|
if i < len(custom_inputs): |
|
title = custom_inputs[i] |
|
style = custom_inputs[i+1] if i+1 < len(custom_inputs) else None |
|
hint = custom_inputs[i+2] if i+2 < len(custom_inputs) else "" |
|
|
|
if title and style: |
|
custom_slides.append({ |
|
"title": title, |
|
"style": style, |
|
"prompt_hint": hint |
|
}) |
|
|
|
|
|
for preview, status, pptx_file in generate_ppt_with_content( |
|
topic, template_name, audience_type, custom_slides, slide_count, seed, |
|
file_upload, use_web_search, theme_name, progress |
|
): |
|
yield preview, status, pptx_file |
|
|
|
|
|
example_btn.click( |
|
fn=load_example, |
|
inputs=[template_select], |
|
outputs=[topic_input] |
|
) |
|
|
|
audience_select.change( |
|
fn=update_audience_info, |
|
inputs=[audience_select], |
|
outputs=[audience_info] |
|
) |
|
|
|
theme_select.change( |
|
fn=update_theme_preview, |
|
inputs=[theme_select], |
|
outputs=[theme_preview] |
|
) |
|
|
|
template_select.change( |
|
fn=update_template_info, |
|
inputs=[template_select, slide_count], |
|
outputs=[template_info, slide_count, custom_accordion] |
|
) |
|
|
|
slide_count.change( |
|
fn=lambda t, s: update_template_info(t, s)[0], |
|
inputs=[template_select, slide_count], |
|
outputs=[template_info] |
|
) |
|
|
|
custom_slide_count.change( |
|
fn=update_custom_slides_visibility, |
|
inputs=[custom_slide_count], |
|
outputs=[slide["row"] for slide in custom_slides_components] |
|
) |
|
|
|
|
|
all_custom_inputs = [] |
|
for slide in custom_slides_components: |
|
all_custom_inputs.extend([slide["title"], slide["style"], slide["hint"]]) |
|
|
|
generate_btn.click( |
|
fn=generate_ppt_handler, |
|
inputs=[ |
|
topic_input, template_select, audience_select, theme_select, slide_count, |
|
seed_input, file_upload, use_web_search, custom_slide_count |
|
] + all_custom_inputs, |
|
outputs=[preview_output, status_output, download_file] |
|
) |
|
|
|
|
|
demo.load( |
|
fn=lambda: (update_template_info(template_select.value, slide_count.value)[0], |
|
update_theme_preview(theme_select.value), |
|
update_audience_info(audience_select.value)), |
|
inputs=[], |
|
outputs=[template_info, theme_preview, audience_info] |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch(ssr_mode=False) |