Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -42,7 +42,7 @@ HYPERLINK_RE = re.compile(r'^(.*?):\s*(https?://\S+)$')
|
|
42 |
|
43 |
DEFAULT_TEMPLATE = """
|
44 |
Slide 1: Title Slide
|
45 |
-
•
|
46 |
• A Gradio & Python-pptx Project
|
47 |
|
48 |
Slide 2: Introduction
|
@@ -57,7 +57,7 @@ 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.
|
@@ -72,45 +72,31 @@ Slide 5: Q&A
|
|
72 |
• Questions & Discussion
|
73 |
"""
|
74 |
|
75 |
-
|
76 |
Slide 1: Title Slide
|
77 |
-
•
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
•
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
•
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
•
|
98 |
-
|
99 |
-
|
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 ---
|
@@ -128,8 +114,9 @@ def parse_color_to_rgb(color_string):
|
|
128 |
return RGBColor(0, 0, 0)
|
129 |
return RGBColor(0, 0, 0)
|
130 |
|
131 |
-
def apply_font_style(
|
132 |
-
"""Applies a dictionary of style attributes to a
|
|
|
133 |
for key, value in style_config.items():
|
134 |
if key == 'color':
|
135 |
font.color.rgb = value
|
@@ -148,7 +135,7 @@ def find_placeholder(slide, placeholder_enums):
|
|
148 |
return None
|
149 |
|
150 |
def populate_title_slide(slide, lines, style_config):
|
151 |
-
"""Populates a title slide
|
152 |
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE])
|
153 |
subtitle_ph = find_placeholder(slide, [PP_PLACEHOLDER.SUBTITLE])
|
154 |
|
@@ -160,75 +147,87 @@ def populate_title_slide(slide, lines, style_config):
|
|
160 |
if title_ph and title_ph.has_text_frame:
|
161 |
tf = title_ph.text_frame
|
162 |
tf.clear()
|
163 |
-
p = tf.
|
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.
|
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
|
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.
|
189 |
-
|
190 |
-
|
191 |
-
|
|
|
|
|
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):
|
@@ -280,10 +279,10 @@ def create_themed_template(theme):
|
|
280 |
slide_master = prs.slide_masters[0]
|
281 |
fill = slide_master.background.fill
|
282 |
|
283 |
-
if theme == "
|
284 |
fill.solid()
|
285 |
fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E)
|
286 |
-
elif theme == "
|
287 |
fill.solid()
|
288 |
fill.fore_color.rgb = RGBColor(0xE7, 0xF1, 0xFF)
|
289 |
|
@@ -350,7 +349,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
|
|
350 |
gr.Examples(
|
351 |
examples=[
|
352 |
[DEFAULT_TEMPLATE.strip()],
|
353 |
-
[
|
354 |
],
|
355 |
inputs=presentation_text_area,
|
356 |
label="Example Outlines"
|
@@ -358,8 +357,8 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
|
|
358 |
with gr.Column(scale=1):
|
359 |
gr.Markdown("### 2. Choose a Template")
|
360 |
template_radio = gr.Radio(
|
361 |
-
["Default White", "
|
362 |
-
label="Built-in
|
363 |
value="Default White"
|
364 |
)
|
365 |
gr.Markdown("<p style='text-align: center; margin: 10px 0;'>OR</p>")
|
@@ -431,4 +430,4 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
|
|
431 |
)
|
432 |
|
433 |
if __name__ == "__main__":
|
434 |
-
app.launch(debug=True, share=True)
|
|
|
42 |
|
43 |
DEFAULT_TEMPLATE = """
|
44 |
Slide 1: Title Slide
|
45 |
+
• AI-Powered Presentation Generator
|
46 |
• A Gradio & Python-pptx Project
|
47 |
|
48 |
Slide 2: Introduction
|
|
|
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 (like Dark Mode) or upload your own `.pptx` template.
|
61 |
• Intelligent Layouts:
|
62 |
▪ Differentiates between title slides and content slides.
|
63 |
▪ Supports multi-level bullet points.
|
|
|
72 |
• Questions & Discussion
|
73 |
"""
|
74 |
|
75 |
+
MARKETING_PLAN_EXAMPLE = """
|
76 |
Slide 1: Title Slide
|
77 |
+
• Project Phoenix: Q3 Marketing Campaign
|
78 |
+
|
79 |
+
Slide 2: Campaign Goals
|
80 |
+
• Increase brand awareness by 20%.
|
81 |
+
• Generate 500 new qualified leads.
|
82 |
+
• Boost social media engagement by 30%.
|
83 |
+
|
84 |
+
Slide 3: Target Audience
|
85 |
+
• Tech startups in the AI sector.
|
86 |
+
• Mid-size e-commerce businesses.
|
87 |
+
• Digital marketing agencies.
|
88 |
+
|
89 |
+
Slide 4: Key Channels
|
90 |
+
• LinkedIn sponsored content & tech-focused blog partnerships.
|
91 |
+
• Targeted email campaigns & virtual webinar series.
|
92 |
+
• Link to our blog: https://gradio.app/blog
|
93 |
+
|
94 |
+
Slide 5: Budget Overview
|
95 |
+
• Content Creation: $5,000
|
96 |
+
• Paid Advertising: $10,000
|
97 |
+
• Total: $15,000
|
98 |
+
|
99 |
+
Slide 6: Next Steps & Q&A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
"""
|
101 |
|
102 |
# --- 3. PRESENTATION GENERATION LOGIC ---
|
|
|
114 |
return RGBColor(0, 0, 0)
|
115 |
return RGBColor(0, 0, 0)
|
116 |
|
117 |
+
def apply_font_style(run, style_config):
|
118 |
+
"""Applies a dictionary of style attributes to a text run."""
|
119 |
+
font = run.font
|
120 |
for key, value in style_config.items():
|
121 |
if key == 'color':
|
122 |
font.color.rgb = value
|
|
|
135 |
return None
|
136 |
|
137 |
def populate_title_slide(slide, lines, style_config):
|
138 |
+
"""Populates a title slide with content and styles using a robust run-based approach."""
|
139 |
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE])
|
140 |
subtitle_ph = find_placeholder(slide, [PP_PLACEHOLDER.SUBTITLE])
|
141 |
|
|
|
147 |
if title_ph and title_ph.has_text_frame:
|
148 |
tf = title_ph.text_frame
|
149 |
tf.clear()
|
150 |
+
p = tf.add_paragraph() # Create a fresh paragraph
|
|
|
|
|
|
|
151 |
p.alignment = PP_ALIGN.CENTER
|
152 |
+
run = p.add_run()
|
153 |
+
run.text = title_val
|
154 |
+
apply_font_style(run, style_config['title'])
|
155 |
+
# Remove the empty paragraph that might be left by tf.clear()
|
156 |
+
if len(tf.paragraphs) > 1:
|
157 |
+
tf._element.remove(tf.paragraphs[0]._p)
|
158 |
|
159 |
|
160 |
if subtitle_ph and subtitle_ph.has_text_frame:
|
161 |
tf = subtitle_ph.text_frame
|
162 |
tf.clear()
|
163 |
+
p = tf.add_paragraph()
|
|
|
|
|
|
|
164 |
p.alignment = PP_ALIGN.CENTER
|
165 |
+
|
166 |
+
for i, line_text in enumerate(subtitle_vals):
|
167 |
+
if i > 0:
|
168 |
+
p.add_run().text = '\n'
|
169 |
+
|
170 |
+
run = p.add_run()
|
171 |
+
run.text = line_text
|
172 |
+
apply_font_style(run, style_config['subtitle'])
|
173 |
+
if len(tf.paragraphs) > 1:
|
174 |
+
tf._element.remove(tf.paragraphs[0]._p)
|
175 |
|
176 |
|
177 |
def populate_content_slide(slide, title, lines, style_config):
|
178 |
+
"""Populates a content slide with a title and bullet points using a robust run-based approach."""
|
179 |
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE])
|
180 |
body_ph = find_placeholder(slide, [PP_PLACEHOLDER.BODY, PP_PLACEHOLDER.OBJECT])
|
181 |
|
182 |
if title_ph and title_ph.has_text_frame:
|
183 |
tf = title_ph.text_frame
|
184 |
tf.clear()
|
185 |
+
p = tf.add_paragraph()
|
186 |
+
run = p.add_run()
|
187 |
+
run.text = title
|
188 |
+
apply_font_style(run, style_config['body_title'])
|
189 |
+
if len(tf.paragraphs) > 1:
|
190 |
+
tf._element.remove(tf.paragraphs[0]._p)
|
191 |
|
192 |
|
193 |
if body_ph and body_ph.has_text_frame:
|
194 |
tf = body_ph.text_frame
|
195 |
+
tf.clear()
|
|
|
|
|
|
|
|
|
|
|
196 |
|
197 |
for line in lines:
|
198 |
clean_line = line.strip()
|
199 |
if not clean_line: continue
|
200 |
|
201 |
+
p = tf.add_paragraph() # Always create a new paragraph for each line
|
202 |
|
203 |
hyperlink_match = HYPERLINK_RE.match(clean_line.lstrip('•◦▪ '))
|
204 |
if hyperlink_match:
|
205 |
link_text, url = hyperlink_match.groups()
|
|
|
206 |
run = p.add_run()
|
|
|
207 |
run.text = f"{link_text}: {url}"
|
208 |
run.hyperlink.address = url
|
209 |
+
apply_font_style(run, style_config['hyperlink'])
|
210 |
continue
|
211 |
|
212 |
if clean_line.startswith(('•', '◦', '▪')):
|
213 |
level = BULLET_MAP.get(clean_line[0], 0)
|
214 |
text = clean_line[1:].lstrip()
|
|
|
|
|
|
|
|
|
215 |
p.level = level
|
216 |
+
run = p.add_run()
|
217 |
+
# --- FIX: Include the bullet character in the run's text ---
|
218 |
+
# This ensures the custom style (including color) applies to the bullet itself.
|
219 |
+
run.text = f"{clean_line[0]} {text}"
|
220 |
+
style_key = f'body_level_{level}'
|
221 |
+
apply_font_style(run, style_config.get(style_key, style_config['body_level_0']))
|
222 |
else:
|
|
|
|
|
|
|
223 |
p.level = 0
|
224 |
+
run = p.add_run()
|
225 |
+
run.text = clean_line
|
226 |
+
apply_font_style(run, style_config['body_level_0'])
|
227 |
+
|
228 |
+
# After loop, remove the initial empty paragraph if it exists
|
229 |
+
if len(tf.paragraphs) > 0 and not tf.paragraphs[0].text.strip():
|
230 |
+
tf._element.remove(tf.paragraphs[0]._p)
|
231 |
|
232 |
|
233 |
def create_presentation_file(content, template_path, style_config):
|
|
|
279 |
slide_master = prs.slide_masters[0]
|
280 |
fill = slide_master.background.fill
|
281 |
|
282 |
+
if theme == "Dark":
|
283 |
fill.solid()
|
284 |
fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E)
|
285 |
+
elif theme == "Blue":
|
286 |
fill.solid()
|
287 |
fill.fore_color.rgb = RGBColor(0xE7, 0xF1, 0xFF)
|
288 |
|
|
|
349 |
gr.Examples(
|
350 |
examples=[
|
351 |
[DEFAULT_TEMPLATE.strip()],
|
352 |
+
[MARKETING_PLAN_EXAMPLE.strip()]
|
353 |
],
|
354 |
inputs=presentation_text_area,
|
355 |
label="Example Outlines"
|
|
|
357 |
with gr.Column(scale=1):
|
358 |
gr.Markdown("### 2. Choose a Template")
|
359 |
template_radio = gr.Radio(
|
360 |
+
["Default White", "Dark", "Blue"],
|
361 |
+
label="Built-in Blank Templates",
|
362 |
value="Default White"
|
363 |
)
|
364 |
gr.Markdown("<p style='text-align: center; margin: 10px 0;'>OR</p>")
|
|
|
430 |
)
|
431 |
|
432 |
if __name__ == "__main__":
|
433 |
+
app.launch(debug=True, share=True)
|