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 | |
| β’ AI-Powered Presentation Generator | |
| β’ 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 (like Dark Mode) 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 | |
| """ | |
| MARKETING_PLAN_EXAMPLE = """ | |
| Slide 1: Title Slide | |
| β’ Project Phoenix: Q3 Marketing Campaign | |
| Slide 2: Campaign Goals | |
| β’ Increase brand awareness by 20%. | |
| β’ Generate 500 new qualified leads. | |
| β’ Boost social media engagement by 30%. | |
| Slide 3: Target Audience | |
| β’ Tech startups in the AI sector. | |
| β’ Mid-size e-commerce businesses. | |
| β’ Digital marketing agencies. | |
| Slide 4: Key Channels | |
| β’ LinkedIn sponsored content & tech-focused blog partnerships. | |
| β’ Targeted email campaigns & virtual webinar series. | |
| β’ Link to our blog: https://gradio.app/blog | |
| Slide 5: Budget Overview | |
| β’ Content Creation: $5,000 | |
| β’ Paid Advertising: $10,000 | |
| β’ Total: $15,000 | |
| Slide 6: Next Steps & 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(run, style_config): | |
| """Applies a dictionary of style attributes to a text run.""" | |
| font = run.font | |
| 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 with content and styles using a robust run-based approach.""" | |
| 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.add_paragraph() # Create a fresh paragraph | |
| p.alignment = PP_ALIGN.CENTER | |
| run = p.add_run() | |
| run.text = title_val | |
| apply_font_style(run, style_config['title']) | |
| # Remove the empty paragraph that might be left by tf.clear() | |
| if len(tf.paragraphs) > 1: | |
| tf._element.remove(tf.paragraphs[0]._p) | |
| if subtitle_ph and subtitle_ph.has_text_frame: | |
| tf = subtitle_ph.text_frame | |
| tf.clear() | |
| p = tf.add_paragraph() | |
| p.alignment = PP_ALIGN.CENTER | |
| for i, line_text in enumerate(subtitle_vals): | |
| if i > 0: | |
| p.add_run().text = '\n' | |
| run = p.add_run() | |
| run.text = line_text | |
| apply_font_style(run, style_config['subtitle']) | |
| if len(tf.paragraphs) > 1: | |
| tf._element.remove(tf.paragraphs[0]._p) | |
| def populate_content_slide(slide, title, lines, style_config): | |
| """Populates a content slide with a title and bullet points using a robust run-based approach.""" | |
| 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.add_paragraph() | |
| run = p.add_run() | |
| run.text = title | |
| apply_font_style(run, style_config['body_title']) | |
| if len(tf.paragraphs) > 1: | |
| tf._element.remove(tf.paragraphs[0]._p) | |
| if body_ph and body_ph.has_text_frame: | |
| tf = body_ph.text_frame | |
| tf.clear() | |
| for line in lines: | |
| clean_line = line.strip() | |
| if not clean_line: continue | |
| p = tf.add_paragraph() # Always create a new paragraph for each line | |
| hyperlink_match = HYPERLINK_RE.match(clean_line.lstrip('β’β¦βͺ ')) | |
| if hyperlink_match: | |
| link_text, url = hyperlink_match.groups() | |
| run = p.add_run() | |
| run.text = f"{link_text}: {url}" | |
| run.hyperlink.address = url | |
| apply_font_style(run, style_config['hyperlink']) | |
| continue | |
| if clean_line.startswith(('β’', 'β¦', 'βͺ')): | |
| level = BULLET_MAP.get(clean_line[0], 0) | |
| text = clean_line[1:].lstrip() | |
| p.level = level | |
| run = p.add_run() | |
| # --- FIX: Include the bullet character in the run's text --- | |
| # This ensures the custom style (including color) applies to the bullet itself. | |
| run.text = f"{clean_line[0]} {text}" | |
| style_key = f'body_level_{level}' | |
| apply_font_style(run, style_config.get(style_key, style_config['body_level_0'])) | |
| else: | |
| p.level = 0 | |
| run = p.add_run() | |
| run.text = clean_line | |
| apply_font_style(run, style_config['body_level_0']) | |
| # After loop, remove the initial empty paragraph if it exists | |
| if len(tf.paragraphs) > 0 and not tf.paragraphs[0].text.strip(): | |
| tf._element.remove(tf.paragraphs[0]._p) | |
| 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 == "Dark": | |
| fill.solid() | |
| fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E) | |
| elif theme == "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()], | |
| [MARKETING_PLAN_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", "Dark", "Blue"], | |
| label="Built-in Blank 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) |