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