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: contact@innovatex.com
|
| 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)
|