Spaces:
Running
Running
import gradio as gr | |
import re | |
from pptx import Presentation | |
from pptx.util import Pt | |
from pptx.dml.color import RGBColor | |
from pptx.enum.shapes import PP_PLACEHOLDER | |
from pptx.enum.dml import MSO_FILL | |
from pptx.enum.text import PP_ALIGN | |
import os | |
import tempfile | |
import io | |
# --- 1. CONFIGURATION AND CONSTANTS --- | |
# List of common, web-safe fonts for the dropdown menu | |
COMMON_FONTS = [ | |
'Arial', 'Arial Black', 'Calibri', 'Calibri Light', 'Cambria', 'Candara', | |
'Century Gothic', 'Consolas', 'Constantia', 'Corbel', 'Courier New', | |
'Franklin Gothic Medium', 'Gabriola', 'Gadugi', 'Georgia', 'Gill Sans MT', | |
'Impact', 'Lucida Console', 'Lucida Sans Unicode', 'Palatino Linotype', | |
'Rockwell', 'Segoe UI', 'Sitka', 'Tahoma', 'Times New Roman', | |
'Trebuchet MS', 'Verdana' | |
] | |
# Default styles that will populate the UI | |
DEFAULT_STYLES = { | |
'title': {'font_name': 'Calibri', 'font_size': 44, 'bold': True, 'color': '#000000'}, | |
'subtitle': {'font_name': 'Calibri', 'font_size': 24, 'bold': False, 'color': '#333333'}, | |
'body_title': {'font_name': 'Calibri', 'font_size': 36, 'bold': True, 'color': '#000000'}, | |
'body_level_0': {'font_name': 'Calibri', 'font_size': 24, 'bold': False, 'color': '#1E1E1E'}, | |
'body_level_1': {'font_name': 'Calibri', 'font_size': 20, 'bold': False, 'color': '#1E1E1E'}, | |
'body_level_2': {'font_name': 'Calibri', 'font_size': 18, 'bold': False, 'color': '#1E1E1E'}, | |
'hyperlink': {'font_name': 'Calibri', 'font_size': 16, 'underline': True, 'color': '#0563C1'} | |
} | |
# Mapping for bullet points to indentation levels | |
BULLET_MAP = {'β’': 0, 'β¦': 1, 'βͺ': 2} | |
HYPERLINK_RE = re.compile(r'^(.*?):\s*(https?://\S+)$') | |
# --- 2. DEFAULT CONTENT AND EXAMPLES --- | |
DEFAULT_TEMPLATE = """ | |
Slide 1: Title Slide | |
β’ Python-Powered Presentation Architect | |
β’ A Gradio & Python-pptx Project | |
Slide 2: Introduction | |
β’ Problem: Creating presentations is time-consuming. | |
β’ Solution: Automate slide generation from simple text outlines. | |
β’ Technology: | |
β¦ Python for backend logic. | |
β¦ `python-pptx` for presentation manipulation. | |
β¦ Gradio for the user interface. | |
Slide 3: Key Features | |
β’ Text-to-Slide Conversion: Automatically creates slides from a formatted script. | |
β’ Full Customization: | |
β¦ Control font styles, sizes, and colors for every element. | |
β¦ Use built-in themes or upload your own `.pptx` template. | |
β’ Intelligent Layouts: | |
βͺ Differentiates between title slides and content slides. | |
βͺ Supports multi-level bullet points. | |
Slide 4: How It Works | |
β’ Step 1: Write your content using the 'Slide X:' format. | |
β’ Step 2: Use the 'Customization' tab to tweak the design. | |
β’ Step 3: Click 'Create PowerPoint' to generate and download your file. | |
β’ More Info: https://github.com/gradio-app/gradio | |
Slide 5: Q&A | |
β’ Questions & Discussion | |
""" | |
BUSINESS_PITCH_EXAMPLE = """ | |
Slide 1: Title Slide | |
β’ InnovateX: AI-Powered Logistics | |
β’ Revolutionizing Supply Chain Management | |
Slide 2: The Problem | |
β’ Global supply chains are inefficient and costly. | |
β¦ Estimated $1.8 Trillion lost annually due to inefficiencies. | |
β’ Key Pain Points: | |
β¦ Lack of real-time visibility. | |
β¦ High fuel and labor costs. | |
β¦ Manual, error-prone documentation. | |
Slide 3: Our Solution | |
β’ A unified platform leveraging AI and IoT. | |
β’ Features: | |
β¦ Predictive route optimization. | |
β¦ Real-time cargo tracking & monitoring. | |
β¦ Automated customs documentation. | |
βͺ Blockchain for secure transactions. | |
Slide 4: Market Opportunity | |
β’ Global Logistics Market Size: $12 Trillion | |
β’ Targetable Market (TAM): $500 Billion | |
β’ Our goal is to capture 2% of the TAM within 5 years. | |
Slide 5: Business Model | |
β’ Subscription-based SaaS model. | |
β’ Tiers: | |
β¦ Basic: $99/month/vehicle | |
β¦ Pro: $199/month/vehicle | |
β¦ Enterprise: Custom pricing | |
Slide 6: Meet the Team | |
β’ Jane Doe, CEO: Ex-Google, Logistics expert. | |
β’ John Smith, CTO: PhD in AI from MIT. | |
β’ Contact Us: [email protected] | |
Slide 7: Q&A | |
""" | |
# --- 3. PRESENTATION GENERATION LOGIC --- | |
def parse_color_to_rgb(color_string): | |
"""Converts a color string (hex or rgb) to an RGBColor object.""" | |
if isinstance(color_string, str): | |
if color_string.startswith('#'): | |
return RGBColor.from_string(color_string.lstrip('#')) | |
elif color_string.startswith('rgb'): | |
try: | |
r, g, b = map(int, re.findall(r'\d+', color_string)) | |
return RGBColor(r, g, b) | |
except (ValueError, TypeError): | |
return RGBColor(0, 0, 0) | |
return RGBColor(0, 0, 0) | |
def apply_font_style(font, style_config): | |
"""Applies a dictionary of style attributes to a font object.""" | |
for key, value in style_config.items(): | |
if key == 'color': | |
font.color.rgb = value | |
elif key == 'font_size': | |
font.size = Pt(value) | |
elif key == 'font_name': | |
font.name = value | |
else: | |
setattr(font, key, value) | |
def find_placeholder(slide, placeholder_enums): | |
"""Finds a placeholder shape on a slide.""" | |
for shape in slide.shapes: | |
if shape.is_placeholder and shape.placeholder_format.type in placeholder_enums: | |
return shape | |
return None | |
def populate_title_slide(slide, lines, style_config): | |
"""Populates a title slide by setting the paragraph's default font style.""" | |
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE]) | |
subtitle_ph = find_placeholder(slide, [PP_PLACEHOLDER.SUBTITLE]) | |
title_val = "Title Not Found" | |
subtitle_vals = [line.lstrip('β’ ').strip() for line in lines if line.strip()] | |
if subtitle_vals: | |
title_val = subtitle_vals.pop(0) | |
if title_ph and title_ph.has_text_frame: | |
tf = title_ph.text_frame | |
tf.clear() | |
p = tf.paragraphs[0] | |
# --- FIX: Style first, then add text --- | |
apply_font_style(p.font, style_config['title']) | |
p.text = title_val | |
p.alignment = PP_ALIGN.CENTER | |
if subtitle_ph and subtitle_ph.has_text_frame: | |
tf = subtitle_ph.text_frame | |
tf.clear() | |
p = tf.paragraphs[0] | |
# --- FIX: Style first, then add text --- | |
apply_font_style(p.font, style_config['subtitle']) | |
p.text = '\n'.join(subtitle_vals) | |
p.alignment = PP_ALIGN.CENTER | |
def populate_content_slide(slide, title, lines, style_config): | |
"""Populates a content slide by setting each paragraph's default font style.""" | |
title_ph = find_placeholder(slide, [PP_PLACEHOLDER.TITLE]) | |
body_ph = find_placeholder(slide, [PP_PLACEHOLDER.BODY, PP_PLACEHOLDER.OBJECT]) | |
if title_ph and title_ph.has_text_frame: | |
tf = title_ph.text_frame | |
tf.clear() | |
p = tf.paragraphs[0] | |
# --- FIX: Style first, then add text --- | |
apply_font_style(p.font, style_config['body_title']) | |
p.text = title | |
if body_ph and body_ph.has_text_frame: | |
tf = body_ph.text_frame | |
tf.clear() | |
# Remove the single paragraph left by clear() before adding new ones. | |
if lines and len(tf.paragraphs) > 0: | |
p_element = tf.paragraphs[0]._p | |
p_element.getparent().remove(p_element) | |
for line in lines: | |
clean_line = line.strip() | |
if not clean_line: continue | |
p = tf.add_paragraph() | |
hyperlink_match = HYPERLINK_RE.match(clean_line.lstrip('β’β¦βͺ ')) | |
if hyperlink_match: | |
link_text, url = hyperlink_match.groups() | |
# Hyperlinks must be runs, so they are a special case | |
run = p.add_run() | |
apply_font_style(run.font, style_config['hyperlink']) | |
run.text = f"{link_text}: {url}" | |
run.hyperlink.address = url | |
continue | |
if clean_line.startswith(('β’', 'β¦', 'βͺ')): | |
level = BULLET_MAP.get(clean_line[0], 0) | |
text = clean_line[1:].lstrip() | |
style_key = f'body_level_{level}' | |
# --- FIX: Style first, then add text and set level --- | |
apply_font_style(p.font, style_config.get(style_key, style_config['body_level_0'])) | |
p.text = text | |
p.level = level | |
else: | |
# --- FIX: Style first, then add text and set level --- | |
apply_font_style(p.font, style_config['body_level_0']) | |
p.text = clean_line | |
p.level = 0 | |
def create_presentation_file(content, template_path, style_config): | |
"""Main function to create the presentation file from text, a template, and styles.""" | |
try: | |
prs = Presentation(template_path) if template_path else Presentation() | |
except Exception as e: | |
raise gr.Error(f"Could not load the presentation template. Please ensure it's a valid .pptx file. Error: {e}") | |
if template_path and len(prs.slides) > 0: | |
title_layout = prs.slides[0].slide_layout | |
content_layout = prs.slides[1].slide_layout if len(prs.slides) > 1 else prs.slides[0].slide_layout | |
else: | |
# Default layouts for blank presentation | |
title_layout = prs.slide_layouts[0] if len(prs.slide_layouts) > 0 else prs.slide_layouts[5] | |
content_layout = prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0] | |
slides_data = re.split(r'\nSlide \d+[a-zA-Z]?:', content, flags=re.IGNORECASE) | |
slides_data = [s.strip() for s in slides_data if s.strip()] | |
if not slides_data: | |
raise gr.Error("The input text does not contain any valid slides. Please use the format 'Slide X:'.") | |
for i in range(len(prs.slides) - 1, -1, -1): | |
rId = prs.slides._sldIdLst[i].rId | |
prs.part.drop_rel(rId) | |
del prs.slides._sldIdLst[i] | |
for i, slide_content in enumerate(slides_data): | |
lines = [line.strip() for line in slide_content.split('\n') if line.strip()] | |
if not lines: continue | |
slide_title_text = lines.pop(0) | |
if i == 0 and "title slide" in slide_title_text.lower(): | |
slide = prs.slides.add_slide(title_layout) | |
populate_title_slide(slide, lines, style_config) | |
else: | |
slide = prs.slides.add_slide(content_layout) | |
populate_content_slide(slide, slide_title_text, lines, style_config) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp: | |
prs.save(tmp.name) | |
return tmp.name | |
def create_themed_template(theme): | |
"""Creates a temporary .pptx file with a themed background.""" | |
prs = Presentation() | |
slide_master = prs.slide_masters[0] | |
fill = slide_master.background.fill | |
if theme == "Modern Dark": | |
fill.solid() | |
fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E) | |
elif theme == "Professional Blue": | |
fill.solid() | |
fill.fore_color.rgb = RGBColor(0xE7, 0xF1, 0xFF) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp: | |
prs.save(tmp.name) | |
return tmp.name | |
def main_interface(*args): | |
"""Collects all UI inputs and generates the presentation.""" | |
( | |
content, template_choice, custom_template_file, | |
title_font, title_size, title_bold, title_color, | |
subtitle_font, subtitle_size, subtitle_bold, subtitle_color, | |
body_title_font, body_title_size, body_title_bold, body_title_color, | |
L0_font, L0_size, L0_bold, L0_color, | |
L1_font, L1_size, L1_bold, L1_color, | |
L2_font, L2_size, L2_bold, L2_color | |
) = args | |
if not content: | |
raise gr.Error("Presentation content is empty. Please enter your text first.") | |
template_path = None | |
if custom_template_file is not None: | |
template_path = custom_template_file.name | |
elif template_choice != "Default White": | |
template_path = create_themed_template(template_choice) | |
hyperlink_style = DEFAULT_STYLES['hyperlink'].copy() | |
hyperlink_style['color'] = parse_color_to_rgb(hyperlink_style['color']) | |
style_config = { | |
'title': {'font_name': title_font, 'font_size': title_size, 'bold': title_bold, 'color': parse_color_to_rgb(title_color)}, | |
'subtitle': {'font_name': subtitle_font, 'font_size': subtitle_size, 'bold': subtitle_bold, 'color': parse_color_to_rgb(subtitle_color)}, | |
'body_title': {'font_name': body_title_font, 'font_size': body_title_size, 'bold': body_title_bold, 'color': parse_color_to_rgb(body_title_color)}, | |
'body_level_0': {'font_name': L0_font, 'font_size': L0_size, 'bold': L0_bold, 'color': parse_color_to_rgb(L0_color)}, | |
'body_level_1': {'font_name': L1_font, 'font_size': L1_size, 'bold': L1_bold, 'color': parse_color_to_rgb(L1_color)}, | |
'body_level_2': {'font_name': L2_font, 'font_size': L2_size, 'bold': L2_bold, 'color': parse_color_to_rgb(L2_color)}, | |
'hyperlink': hyperlink_style | |
} | |
output_path = create_presentation_file(content, template_path, style_config) | |
return output_path | |
# --- 4. GRADIO UI --- | |
with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as app: | |
gr.Markdown(""" | |
<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);"> | |
<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> | |
<p style="font-size: 1.2em; margin-top: 5px;">Craft stunning presentations from simple text. Customize everything.</p> | |
</div> | |
""") | |
with gr.Tabs(): | |
with gr.TabItem("π Content & Generation"): | |
with gr.Row(equal_height=True): | |
with gr.Column(scale=2): | |
gr.Markdown("### 1. Enter Presentation Content") | |
presentation_text_area = gr.Textbox( | |
label="Format: 'Slide 1: Title' followed by bullet points.", | |
lines=25, | |
value=DEFAULT_TEMPLATE.strip() | |
) | |
gr.Examples( | |
examples=[ | |
[DEFAULT_TEMPLATE.strip()], | |
[BUSINESS_PITCH_EXAMPLE.strip()] | |
], | |
inputs=presentation_text_area, | |
label="Example Outlines" | |
) | |
with gr.Column(scale=1): | |
gr.Markdown("### 2. Choose a Template") | |
template_radio = gr.Radio( | |
["Default White", "Modern Dark", "Professional Blue"], | |
label="Built-in Themed Templates", | |
value="Default White" | |
) | |
gr.Markdown("<p style='text-align: center; margin: 10px 0;'>OR</p>") | |
template_upload = gr.File(label="Upload a Custom .pptx Template", file_types=[".pptx"]) | |
with gr.Accordion("π‘ Template Tips", open=False): | |
gr.Markdown(""" | |
- An uploaded template will **override** the built-in choice. | |
- All existing slides in your template will be **removed** and replaced with the new content. | |
- The design (master slide) of your template will be preserved. | |
- For best results, use a template with standard 'Title' and 'Title and Content' layouts. | |
""") | |
gr.Markdown("### 3. Create & Download") | |
create_ppt_btn = gr.Button("π Generate PowerPoint", variant="primary", scale=2) | |
output_file = gr.File(label="Download Your Presentation", interactive=False) | |
with gr.TabItem("π¨ Font & Style Customization"): | |
gr.Markdown("### Fine-tune the look and feel of your presentation text.") | |
with gr.Accordion("Title & Subtitle Styles", open=True): | |
with gr.Row(): | |
title_font = gr.Dropdown(COMMON_FONTS, label="Title Font", value=DEFAULT_STYLES['title']['font_name']) | |
title_size = gr.Slider(10, 100, label="Title Size (pt)", value=DEFAULT_STYLES['title']['font_size'], step=1) | |
title_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['title']['bold']) | |
title_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['title']['color']) | |
with gr.Row(): | |
subtitle_font = gr.Dropdown(COMMON_FONTS, label="Subtitle Font", value=DEFAULT_STYLES['subtitle']['font_name']) | |
subtitle_size = gr.Slider(10, 60, label="Subtitle Size (pt)", value=DEFAULT_STYLES['subtitle']['font_size'], step=1) | |
subtitle_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['subtitle']['bold']) | |
subtitle_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['subtitle']['color']) | |
with gr.Accordion("Content Body Styles", open=True): | |
with gr.Row(): | |
body_title_font = gr.Dropdown(COMMON_FONTS, label="Slide Title Font", value=DEFAULT_STYLES['body_title']['font_name']) | |
body_title_size = gr.Slider(10, 80, label="Slide Title Size (pt)", value=DEFAULT_STYLES['body_title']['font_size'], step=1) | |
body_title_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_title']['bold']) | |
body_title_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_title']['color']) | |
gr.HTML("<hr>") | |
with gr.Row(): | |
L0_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 1 (β’) Font", value=DEFAULT_STYLES['body_level_0']['font_name']) | |
L0_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_0']['font_size'], step=1) | |
L0_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_0']['bold']) | |
L0_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_0']['color']) | |
with gr.Row(): | |
L1_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 2 (β¦) Font", value=DEFAULT_STYLES['body_level_1']['font_name']) | |
L1_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_1']['font_size'], step=1) | |
L1_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_1']['bold']) | |
L1_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_1']['color']) | |
with gr.Row(): | |
L2_font = gr.Dropdown(COMMON_FONTS, label="Bullet Level 3 (βͺ) Font", value=DEFAULT_STYLES['body_level_2']['font_name']) | |
L2_size = gr.Slider(8, 50, label="Size (pt)", value=DEFAULT_STYLES['body_level_2']['font_size'], step=1) | |
L2_bold = gr.Checkbox(label="Bold", value=DEFAULT_STYLES['body_level_2']['bold']) | |
L2_color = gr.ColorPicker(label="Color", value=DEFAULT_STYLES['body_level_2']['color']) | |
# List of all input components to be passed to the main function | |
all_inputs = [ | |
presentation_text_area, template_radio, template_upload, | |
title_font, title_size, title_bold, title_color, | |
subtitle_font, subtitle_size, subtitle_bold, subtitle_color, | |
body_title_font, body_title_size, body_title_bold, body_title_color, | |
L0_font, L0_size, L0_bold, L0_color, | |
L1_font, L1_size, L1_bold, L1_color, | |
L2_font, L2_size, L2_bold, L2_color | |
] | |
create_ppt_btn.click( | |
fn=main_interface, | |
inputs=all_inputs, | |
outputs=output_file | |
) | |
if __name__ == "__main__": | |
app.launch(debug=True, share=True) | |