Spaces:
Running
Running
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import re
|
3 |
+
from pptx import Presentation
|
4 |
+
from pptx.util import Pt
|
5 |
+
from pptx.dml.color import RGBColor
|
6 |
+
from pptx.enum.shapes import PP_PLACEHOLDER
|
7 |
+
from pptx.enum.dml import MSO_FILL
|
8 |
+
from pptx.enum.text import PP_ALIGN
|
9 |
+
import os
|
10 |
+
import tempfile
|
11 |
+
import io
|
12 |
+
|
13 |
+
# --- 1. CONFIGURATION AND CONSTANTS ---
|
14 |
+
|
15 |
+
# List of common, web-safe fonts for the dropdown menu
|
16 |
+
COMMON_FONTS = [
|
17 |
+
'Arial', 'Arial Black', 'Calibri', 'Calibri Light', 'Cambria', 'Candara',
|
18 |
+
'Century Gothic', 'Consolas', 'Constantia', 'Corbel', 'Courier New',
|
19 |
+
'Franklin Gothic Medium', 'Gabriola', 'Gadugi', 'Georgia', 'Gill Sans MT',
|
20 |
+
'Impact', 'Lucida Console', 'Lucida Sans Unicode', 'Palatino Linotype',
|
21 |
+
'Rockwell', 'Segoe UI', 'Sitka', 'Tahoma', 'Times New Roman',
|
22 |
+
'Trebuchet MS', 'Verdana'
|
23 |
+
]
|
24 |
+
|
25 |
+
# Default styles that will populate the UI
|
26 |
+
DEFAULT_STYLES = {
|
27 |
+
'title': {'font_name': 'Calibri', 'font_size': 44, 'bold': True, 'color': '#000000'},
|
28 |
+
'subtitle': {'font_name': 'Calibri', 'font_size': 24, 'bold': False, 'color': '#333333'},
|
29 |
+
'body_title': {'font_name': 'Calibri', 'font_size': 36, 'bold': True, 'color': '#000000'},
|
30 |
+
'body_level_0': {'font_name': 'Calibri', 'font_size': 24, 'bold': False, 'color': '#1E1E1E'},
|
31 |
+
'body_level_1': {'font_name': 'Calibri', 'font_size': 20, 'bold': False, 'color': '#1E1E1E'},
|
32 |
+
'body_level_2': {'font_name': 'Calibri', 'font_size': 18, 'bold': False, 'color': '#1E1E1E'},
|
33 |
+
'hyperlink': {'font_name': 'Calibri', 'font_size': 16, 'underline': True, 'color': '#0563C1'}
|
34 |
+
}
|
35 |
+
|
36 |
+
# Mapping for bullet points to indentation levels
|
37 |
+
BULLET_MAP = {'β’': 0, 'β¦': 1, 'βͺ': 2}
|
38 |
+
HYPERLINK_RE = re.compile(r'^(.*?):\s*(https?://\S+)$')
|
39 |
+
|
40 |
+
|
41 |
+
# --- 2. DEFAULT CONTENT AND EXAMPLES ---
|
42 |
+
|
43 |
+
DEFAULT_TEMPLATE = """
|
44 |
+
Slide 1: Title Slide
|
45 |
+
β’ Python-Powered Presentation Architect
|
46 |
+
β’ A Gradio & Python-pptx Project
|
47 |
+
|
48 |
+
Slide 2: Introduction
|
49 |
+
β’ Problem: Creating presentations is time-consuming.
|
50 |
+
β’ Solution: Automate slide generation from simple text outlines.
|
51 |
+
β’ Technology:
|
52 |
+
β¦ Python for backend logic.
|
53 |
+
β¦ `python-pptx` for presentation manipulation.
|
54 |
+
β¦ Gradio for the user interface.
|
55 |
+
|
56 |
+
Slide 3: Key Features
|
57 |
+
β’ Text-to-Slide Conversion: Automatically creates slides from a formatted script.
|
58 |
+
β’ Full Customization:
|
59 |
+
β¦ Control font styles, sizes, and colors for every element.
|
60 |
+
β¦ Use built-in themes or upload your own `.pptx` template.
|
61 |
+
β’ Intelligent Layouts:
|
62 |
+
βͺ Differentiates between title slides and content slides.
|
63 |
+
βͺ Supports multi-level bullet points.
|
64 |
+
|
65 |
+
Slide 4: How It Works
|
66 |
+
β’ Step 1: Write your content using the 'Slide X:' format.
|
67 |
+
β’ Step 2: Use the 'Customization' tab to tweak the design.
|
68 |
+
β’ Step 3: Click 'Create PowerPoint' to generate and download your file.
|
69 |
+
β’ More Info: https://github.com/gradio-app/gradio
|
70 |
+
|
71 |
+
Slide 5: Q&A
|
72 |
+
β’ Questions & Discussion
|
73 |
+
"""
|
74 |
+
|
75 |
+
BUSINESS_PITCH_EXAMPLE = """
|
76 |
+
Slide 1: Title Slide
|
77 |
+
β’ InnovateX: AI-Powered Logistics
|
78 |
+
β’ Revolutionizing Supply Chain Management
|
79 |
+
|
80 |
+
Slide 2: The Problem
|
81 |
+
β’ Global supply chains are inefficient and costly.
|
82 |
+
β¦ Estimated $1.8 Trillion lost annually due to inefficiencies.
|
83 |
+
β’ Key Pain Points:
|
84 |
+
β¦ Lack of real-time visibility.
|
85 |
+
β¦ High fuel and labor costs.
|
86 |
+
β¦ Manual, error-prone documentation.
|
87 |
+
|
88 |
+
Slide 3: Our Solution
|
89 |
+
β’ A unified platform leveraging AI and IoT.
|
90 |
+
β’ Features:
|
91 |
+
β¦ Predictive route optimization.
|
92 |
+
β¦ Real-time cargo tracking & monitoring.
|
93 |
+
β¦ Automated customs documentation.
|
94 |
+
βͺ Blockchain for secure transactions.
|
95 |
+
|
96 |
+
Slide 4: Market Opportunity
|
97 |
+
β’ Global Logistics Market Size: $12 Trillion
|
98 |
+
β’ Targetable Market (TAM): $500 Billion
|
99 |
+
β’ Our goal is to capture 2% of the TAM within 5 years.
|
100 |
+
|
101 |
+
Slide 5: Business Model
|
102 |
+
β’ Subscription-based SaaS model.
|
103 |
+
β’ Tiers:
|
104 |
+
β¦ Basic: $99/month/vehicle
|
105 |
+
β¦ Pro: $199/month/vehicle
|
106 |
+
β¦ Enterprise: Custom pricing
|
107 |
+
|
108 |
+
Slide 6: Meet the Team
|
109 |
+
β’ Jane Doe, CEO: Ex-Google, Logistics expert.
|
110 |
+
β’ John Smith, CTO: PhD in AI from MIT.
|
111 |
+
β’ Contact Us: [email protected]
|
112 |
+
|
113 |
+
Slide 7: Q&A
|
114 |
+
"""
|
115 |
+
|
116 |
+
# --- 3. PRESENTATION GENERATION LOGIC ---
|
117 |
+
|
118 |
+
def parse_color_to_rgb(color_string):
|
119 |
+
"""Converts a color string (hex or rgb) to an RGBColor object."""
|
120 |
+
if isinstance(color_string, str):
|
121 |
+
if color_string.startswith('#'):
|
122 |
+
return RGBColor.from_string(color_string.lstrip('#'))
|
123 |
+
elif color_string.startswith('rgb'):
|
124 |
+
try:
|
125 |
+
r, g, b = map(int, re.findall(r'\d+', color_string))
|
126 |
+
return RGBColor(r, g, b)
|
127 |
+
except (ValueError, TypeError):
|
128 |
+
return RGBColor(0, 0, 0)
|
129 |
+
return RGBColor(0, 0, 0)
|
130 |
+
|
131 |
+
def apply_font_style(font, style_config):
|
132 |
+
"""Applies a dictionary of style attributes to a font object."""
|
133 |
+
for key, value in style_config.items():
|
134 |
+
if key == 'color':
|
135 |
+
font.color.rgb = value
|
136 |
+
elif key == 'font_size':
|
137 |
+
font.size = Pt(value)
|
138 |
+
elif key == 'font_name':
|
139 |
+
font.name = value
|
140 |
+
else:
|
141 |
+
setattr(font, key, value)
|
142 |
+
|
143 |
+
def find_placeholder(slide, placeholder_enums):
|
144 |
+
"""Finds a placeholder shape on a slide."""
|
145 |
+
for shape in slide.shapes:
|
146 |
+
if shape.is_placeholder and shape.placeholder_format.type in placeholder_enums:
|
147 |
+
return shape
|
148 |
+
return None
|
149 |
+
|
150 |
+
def populate_title_slide(slide, lines, style_config):
|
151 |
+
"""Populates a title slide by setting the paragraph's default font style."""
|
152 |
+
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE])
|
153 |
+
subtitle_ph = find_placeholder(slide, [PP_PLACEHOLDER.SUBTITLE])
|
154 |
+
|
155 |
+
title_val = "Title Not Found"
|
156 |
+
subtitle_vals = [line.lstrip('β’ ').strip() for line in lines if line.strip()]
|
157 |
+
if subtitle_vals:
|
158 |
+
title_val = subtitle_vals.pop(0)
|
159 |
+
|
160 |
+
if title_ph and title_ph.has_text_frame:
|
161 |
+
tf = title_ph.text_frame
|
162 |
+
tf.clear()
|
163 |
+
p = tf.paragraphs[0]
|
164 |
+
# --- FIX: Style first, then add text ---
|
165 |
+
apply_font_style(p.font, style_config['title'])
|
166 |
+
p.text = title_val
|
167 |
+
p.alignment = PP_ALIGN.CENTER
|
168 |
+
|
169 |
+
|
170 |
+
if subtitle_ph and subtitle_ph.has_text_frame:
|
171 |
+
tf = subtitle_ph.text_frame
|
172 |
+
tf.clear()
|
173 |
+
p = tf.paragraphs[0]
|
174 |
+
# --- FIX: Style first, then add text ---
|
175 |
+
apply_font_style(p.font, style_config['subtitle'])
|
176 |
+
p.text = '\n'.join(subtitle_vals)
|
177 |
+
p.alignment = PP_ALIGN.CENTER
|
178 |
+
|
179 |
+
|
180 |
+
def populate_content_slide(slide, title, lines, style_config):
|
181 |
+
"""Populates a content slide by setting each paragraph's default font style."""
|
182 |
+
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE])
|
183 |
+
body_ph = find_placeholder(slide, [PP_PLACEHOLDER.BODY, PP_PLACEHOLDER.OBJECT])
|
184 |
+
|
185 |
+
if title_ph and title_ph.has_text_frame:
|
186 |
+
tf = title_ph.text_frame
|
187 |
+
tf.clear()
|
188 |
+
p = tf.paragraphs[0]
|
189 |
+
# --- FIX: Style first, then add text ---
|
190 |
+
apply_font_style(p.font, style_config['body_title'])
|
191 |
+
p.text = title
|
192 |
+
|
193 |
+
|
194 |
+
if body_ph and body_ph.has_text_frame:
|
195 |
+
tf = body_ph.text_frame
|
196 |
+
tf.clear()
|
197 |
+
|
198 |
+
# Remove the single paragraph left by clear() before adding new ones.
|
199 |
+
if lines and len(tf.paragraphs) > 0:
|
200 |
+
p_element = tf.paragraphs[0]._p
|
201 |
+
p_element.getparent().remove(p_element)
|
202 |
+
|
203 |
+
for line in lines:
|
204 |
+
clean_line = line.strip()
|
205 |
+
if not clean_line: continue
|
206 |
+
|
207 |
+
p = tf.add_paragraph()
|
208 |
+
|
209 |
+
hyperlink_match = HYPERLINK_RE.match(clean_line.lstrip('β’β¦βͺ '))
|
210 |
+
if hyperlink_match:
|
211 |
+
link_text, url = hyperlink_match.groups()
|
212 |
+
# Hyperlinks must be runs, so they are a special case
|
213 |
+
run = p.add_run()
|
214 |
+
apply_font_style(run.font, style_config['hyperlink'])
|
215 |
+
run.text = f"{link_text}: {url}"
|
216 |
+
run.hyperlink.address = url
|
217 |
+
continue
|
218 |
+
|
219 |
+
if clean_line.startswith(('β’', 'β¦', 'βͺ')):
|
220 |
+
level = BULLET_MAP.get(clean_line[0], 0)
|
221 |
+
text = clean_line[1:].lstrip()
|
222 |
+
style_key = f'body_level_{level}'
|
223 |
+
# --- FIX: Style first, then add text and set level ---
|
224 |
+
apply_font_style(p.font, style_config.get(style_key, style_config['body_level_0']))
|
225 |
+
p.text = text
|
226 |
+
p.level = level
|
227 |
+
else:
|
228 |
+
# --- FIX: Style first, then add text and set level ---
|
229 |
+
apply_font_style(p.font, style_config['body_level_0'])
|
230 |
+
p.text = clean_line
|
231 |
+
p.level = 0
|
232 |
+
|
233 |
+
|
234 |
+
def create_presentation_file(content, template_path, style_config):
|
235 |
+
"""Main function to create the presentation file from text, a template, and styles."""
|
236 |
+
try:
|
237 |
+
prs = Presentation(template_path) if template_path else Presentation()
|
238 |
+
except Exception as e:
|
239 |
+
raise gr.Error(f"Could not load the presentation template. Please ensure it's a valid .pptx file. Error: {e}")
|
240 |
+
|
241 |
+
if template_path and len(prs.slides) > 0:
|
242 |
+
title_layout = prs.slides[0].slide_layout
|
243 |
+
content_layout = prs.slides[1].slide_layout if len(prs.slides) > 1 else prs.slides[0].slide_layout
|
244 |
+
else:
|
245 |
+
# Default layouts for blank presentation
|
246 |
+
title_layout = prs.slide_layouts[0] if len(prs.slide_layouts) > 0 else prs.slide_layouts[5]
|
247 |
+
content_layout = prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0]
|
248 |
+
|
249 |
+
|
250 |
+
slides_data = re.split(r'\nSlide \d+[a-zA-Z]?:', content, flags=re.IGNORECASE)
|
251 |
+
slides_data = [s.strip() for s in slides_data if s.strip()]
|
252 |
+
if not slides_data:
|
253 |
+
raise gr.Error("The input text does not contain any valid slides. Please use the format 'Slide X:'.")
|
254 |
+
|
255 |
+
for i in range(len(prs.slides) - 1, -1, -1):
|
256 |
+
rId = prs.slides._sldIdLst[i].rId
|
257 |
+
prs.part.drop_rel(rId)
|
258 |
+
del prs.slides._sldIdLst[i]
|
259 |
+
|
260 |
+
for i, slide_content in enumerate(slides_data):
|
261 |
+
lines = [line.strip() for line in slide_content.split('\n') if line.strip()]
|
262 |
+
if not lines: continue
|
263 |
+
|
264 |
+
slide_title_text = lines.pop(0)
|
265 |
+
|
266 |
+
if i == 0 and "title slide" in slide_title_text.lower():
|
267 |
+
slide = prs.slides.add_slide(title_layout)
|
268 |
+
populate_title_slide(slide, lines, style_config)
|
269 |
+
else:
|
270 |
+
slide = prs.slides.add_slide(content_layout)
|
271 |
+
populate_content_slide(slide, slide_title_text, lines, style_config)
|
272 |
+
|
273 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp:
|
274 |
+
prs.save(tmp.name)
|
275 |
+
return tmp.name
|
276 |
+
|
277 |
+
def create_themed_template(theme):
|
278 |
+
"""Creates a temporary .pptx file with a themed background."""
|
279 |
+
prs = Presentation()
|
280 |
+
slide_master = prs.slide_masters[0]
|
281 |
+
fill = slide_master.background.fill
|
282 |
+
|
283 |
+
if theme == "Modern Dark":
|
284 |
+
fill.solid()
|
285 |
+
fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E)
|
286 |
+
elif theme == "Professional Blue":
|
287 |
+
fill.solid()
|
288 |
+
fill.fore_color.rgb = RGBColor(0xE7, 0xF1, 0xFF)
|
289 |
+
|
290 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp:
|
291 |
+
prs.save(tmp.name)
|
292 |
+
return tmp.name
|
293 |
+
|
294 |
+
def main_interface(*args):
|
295 |
+
"""Collects all UI inputs and generates the presentation."""
|
296 |
+
(
|
297 |
+
content, template_choice, custom_template_file,
|
298 |
+
title_font, title_size, title_bold, title_color,
|
299 |
+
subtitle_font, subtitle_size, subtitle_bold, subtitle_color,
|
300 |
+
body_title_font, body_title_size, body_title_bold, body_title_color,
|
301 |
+
L0_font, L0_size, L0_bold, L0_color,
|
302 |
+
L1_font, L1_size, L1_bold, L1_color,
|
303 |
+
L2_font, L2_size, L2_bold, L2_color
|
304 |
+
) = args
|
305 |
+
|
306 |
+
if not content:
|
307 |
+
raise gr.Error("Presentation content is empty. Please enter your text first.")
|
308 |
+
|
309 |
+
template_path = None
|
310 |
+
if custom_template_file is not None:
|
311 |
+
template_path = custom_template_file.name
|
312 |
+
elif template_choice != "Default White":
|
313 |
+
template_path = create_themed_template(template_choice)
|
314 |
+
|
315 |
+
hyperlink_style = DEFAULT_STYLES['hyperlink'].copy()
|
316 |
+
hyperlink_style['color'] = parse_color_to_rgb(hyperlink_style['color'])
|
317 |
+
|
318 |
+
style_config = {
|
319 |
+
'title': {'font_name': title_font, 'font_size': title_size, 'bold': title_bold, 'color': parse_color_to_rgb(title_color)},
|
320 |
+
'subtitle': {'font_name': subtitle_font, 'font_size': subtitle_size, 'bold': subtitle_bold, 'color': parse_color_to_rgb(subtitle_color)},
|
321 |
+
'body_title': {'font_name': body_title_font, 'font_size': body_title_size, 'bold': body_title_bold, 'color': parse_color_to_rgb(body_title_color)},
|
322 |
+
'body_level_0': {'font_name': L0_font, 'font_size': L0_size, 'bold': L0_bold, 'color': parse_color_to_rgb(L0_color)},
|
323 |
+
'body_level_1': {'font_name': L1_font, 'font_size': L1_size, 'bold': L1_bold, 'color': parse_color_to_rgb(L1_color)},
|
324 |
+
'body_level_2': {'font_name': L2_font, 'font_size': L2_size, 'bold': L2_bold, 'color': parse_color_to_rgb(L2_color)},
|
325 |
+
'hyperlink': hyperlink_style
|
326 |
+
}
|
327 |
+
|
328 |
+
output_path = create_presentation_file(content, template_path, style_config)
|
329 |
+
return output_path
|
330 |
+
|
331 |
+
# --- 4. GRADIO UI ---
|
332 |
+
with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as app:
|
333 |
+
gr.Markdown("""
|
334 |
+
<div style="text-align: center; padding: 20px; background-image: linear-gradient(to right, #74ebd5, #acb6e5); color: white; border-radius: 12px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
|
335 |
+
<h1 style="font-size: 2.8em; margin: 0; font-weight: 700; text-shadow: 1px 1px 3px rgba(0,0,0,0.2);">β¨ AI Presentation Architect</h1>
|
336 |
+
<p style="font-size: 1.2em; margin-top: 5px;">Craft stunning presentations from simple text. Customize everything.</p>
|
337 |
+
</div>
|
338 |
+
""")
|
339 |
+
|
340 |
+
with gr.Tabs():
|
341 |
+
with gr.TabItem("π Content & Generation"):
|
342 |
+
with gr.Row(equal_height=True):
|
343 |
+
with gr.Column(scale=2):
|
344 |
+
gr.Markdown("### 1. Enter Presentation Content")
|
345 |
+
presentation_text_area = gr.Textbox(
|
346 |
+
label="Format: 'Slide 1: Title' followed by bullet points.",
|
347 |
+
lines=25,
|
348 |
+
value=DEFAULT_TEMPLATE.strip()
|
349 |
+
)
|
350 |
+
gr.Examples(
|
351 |
+
examples=[
|
352 |
+
[DEFAULT_TEMPLATE.strip()],
|
353 |
+
[BUSINESS_PITCH_EXAMPLE.strip()]
|
354 |
+
],
|
355 |
+
inputs=presentation_text_area,
|
356 |
+
label="Example Outlines"
|
357 |
+
)
|
358 |
+
with gr.Column(scale=1):
|
359 |
+
gr.Markdown("### 2. Choose a Template")
|
360 |
+
template_radio = gr.Radio(
|
361 |
+
["Default White", "Modern Dark", "Professional Blue"],
|
362 |
+
label="Built-in Themed Templates",
|
363 |
+
value="Default White"
|
364 |
+
)
|
365 |
+
gr.Markdown("<p style='text-align: center; margin: 10px 0;'>OR</p>")
|
366 |
+
template_upload = gr.File(label="Upload a Custom .pptx Template", file_types=[".pptx"])
|
367 |
+
with gr.Accordion("π‘ Template Tips", open=False):
|
368 |
+
gr.Markdown("""
|
369 |
+
- An uploaded template will **override** the built-in choice.
|
370 |
+
- All existing slides in your template will be **removed** and replaced with the new content.
|
371 |
+
- The design (master slide) of your template will be preserved.
|
372 |
+
- For best results, use a template with standard 'Title' and 'Title and Content' layouts.
|
373 |
+
""")
|
374 |
+
|
375 |
+
gr.Markdown("### 3. Create & Download")
|
376 |
+
create_ppt_btn = gr.Button("π Generate PowerPoint", variant="primary", scale=2)
|
377 |
+
output_file = gr.File(label="Download Your Presentation", interactive=False)
|
378 |
+
|
379 |
+
with gr.TabItem("π¨ Font & Style Customization"):
|
380 |
+
gr.Markdown("### Fine-tune the look and feel of your presentation text.")
|
381 |
+
with gr.Accordion("Title & Subtitle Styles", open=True):
|
382 |
+
with gr.Row():
|
383 |
+
title_font = gr.Dropdown(COMMON_FONTS, label="Title Font", value=DEFAULT_STYLES['title']['font_name'])
|
384 |
+
title_size = gr.Slider(10, 100, label="Title Size (pt)", value=DEFAULT_STYLES['title']['font_size'], step=1)
|
385 |
+
title_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['title']['bold'])
|
386 |
+
title_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['title']['color'])
|
387 |
+
with gr.Row():
|
388 |
+
subtitle_font = gr.Dropdown(COMMON_FONTS, label="Subtitle Font", value=DEFAULT_STYLES['subtitle']['font_name'])
|
389 |
+
subtitle_size = gr.Slider(10, 60, label="Subtitle Size (pt)", value=DEFAULT_STYLES['subtitle']['font_size'], step=1)
|
390 |
+
subtitle_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['subtitle']['bold'])
|
391 |
+
subtitle_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['subtitle']['color'])
|
392 |
+
|
393 |
+
with gr.Accordion("Content Body Styles", open=True):
|
394 |
+
with gr.Row():
|
395 |
+
body_title_font = gr.Dropdown(COMMON_FONTS, label="Slide Title Font", value=DEFAULT_STYLES['body_title']['font_name'])
|
396 |
+
body_title_size = gr.Slider(10, 80, label="Slide Title Size (pt)", value=DEFAULT_STYLES['body_title']['font_size'], step=1)
|
397 |
+
body_title_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_title']['bold'])
|
398 |
+
body_title_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_title']['color'])
|
399 |
+
gr.HTML("<hr>")
|
400 |
+
with gr.Row():
|
401 |
+
L0_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 1 (β’) Font", value=DEFAULT_STYLES['body_level_0']['font_name'])
|
402 |
+
L0_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_0']['font_size'], step=1)
|
403 |
+
L0_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_0']['bold'])
|
404 |
+
L0_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_0']['color'])
|
405 |
+
with gr.Row():
|
406 |
+
L1_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 2 (β¦) Font", value=DEFAULT_STYLES['body_level_1']['font_name'])
|
407 |
+
L1_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_1']['font_size'], step=1)
|
408 |
+
L1_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_1']['bold'])
|
409 |
+
L1_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_1']['color'])
|
410 |
+
with gr.Row():
|
411 |
+
L2_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 3 (βͺ) Font", value=DEFAULT_STYLES['body_level_2']['font_name'])
|
412 |
+
L2_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_2']['font_size'], step=1)
|
413 |
+
L2_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_2']['bold'])
|
414 |
+
L2_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_2']['color'])
|
415 |
+
|
416 |
+
# List of all input components to be passed to the main function
|
417 |
+
all_inputs = [
|
418 |
+
presentation_text_area, template_radio, template_upload,
|
419 |
+
title_font, title_size, title_bold, title_color,
|
420 |
+
subtitle_font, subtitle_size, subtitle_bold, subtitle_color,
|
421 |
+
body_title_font, body_title_size, body_title_bold, body_title_color,
|
422 |
+
L0_font, L0_size, L0_bold, L0_color,
|
423 |
+
L1_font, L1_size, L1_bold, L1_color,
|
424 |
+
L2_font, L2_size, L2_bold, L2_color
|
425 |
+
]
|
426 |
+
|
427 |
+
create_ppt_btn.click(
|
428 |
+
fn=main_interface,
|
429 |
+
inputs=all_inputs,
|
430 |
+
outputs=output_file
|
431 |
+
)
|
432 |
+
|
433 |
+
if __name__ == "__main__":
|
434 |
+
app.launch(debug=True, share=True)
|