DEV Community

Cover image for Create a PDF to Slide AI Generator with Python, Celery, and python-pptx πŸ”₯πŸš€
Kevin Goedecke
Kevin Goedecke

Posted on

Create a PDF to Slide AI Generator with Python, Celery, and python-pptx πŸ”₯πŸš€

TL;DR

We will create an AI tool to create slides from a PDF. I'll show you how to build a backend service that generates PowerPoint slides asnyc using Python, Celery, and python-pptx. The backend simply accepts a PDF and returns slides as a pptx file. Exciting stuff isn't it.

The architecture of this tool is heavily inspired by what we work on at SlideSpeak. SlideSpeak is an AI tool to create slides from PDF and more. The code for this tutorial is available here:

Here's how the results of the PDF to slides AI generator look like:

AI PDF to Slide Generator

But since we all absolutely love PowerPoint slides, let's get into it.
I hate PowerPoint meme

What You'll Build

This tutorial will walk you through creating a backend service that:

  • Provides a RESTful API to request slide generation
  • Processes slide requests asynchronously with Celery
  • Creates professional PowerPoint slides with python-pptx
  • Supports multiple slide layouts (title, content, bullet points, etc.)
  • Extracts text from PDF files
  • Uses OpenAI to generate presentation content automatically
  • Scales efficiently to handle multiple requests

Tech Stack

  • FastAPI: For creating the RESTful API endpoints
  • Celery: For handling asynchronous tasks
  • Redis: As message broker and result backend for Celery
  • python-pptx: For programmatically creating PowerPoint files
  • PyPDF2: For extracting text from PDF files
  • OpenAI API: For intelligent content generation
  • Docker & Docker Compose: For containerizing the application

Architecture

AI Slide Tool

Getting Started

Before diving into the code, let's understand the project structure:

presentation_generator/ β”œβ”€β”€ app/ β”‚ β”œβ”€β”€ __init__.py β”‚ β”œβ”€β”€ main.py # FastAPI application β”‚ β”œβ”€β”€ models.py # Pydantic models β”‚ β”œβ”€β”€ config.py # Configuration β”‚ β”œβ”€β”€ ppt_generator.py # slide generation logic β”‚ └── pdf_processor.py # PDF processing and OpenAI integration β”œβ”€β”€ celery_app/ β”‚ β”œβ”€β”€ __init__.py β”‚ β”œβ”€β”€ tasks.py # Celery tasks β”‚ └── celery_config.py # Celery configuration β”œβ”€β”€ requirements.txt └── docker-compose.yml 
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting Up the Environment

Let's start by creating our project directory and installing the required dependencies:

mkdir presentation_generator cd presentation_generator python -m venv venv source venv/bin/activate # On Windows, use: venv\Scripts\activate 
Enter fullscreen mode Exit fullscreen mode

Now, create a requirements.txt file with the following dependencies:

fastapi==0.103.1 uvicorn==0.23.2 celery==5.3.4 redis==5.0.0 python-pptx==0.6.21 python-multipart==0.0.6 pydantic==2.3.0 pydantic-settings==2.0.3 pypdf2==3.0.1 openai==1.6.0 python-dotenv==1.0.0 
Enter fullscreen mode Exit fullscreen mode

Install these dependencies:

pip install -r requirements.txt 
Enter fullscreen mode Exit fullscreen mode

Step 2: Setting Up Configuration

Let's create a configuration file to manage our application settings. Create app/config.py:

from pydantic_settings import BaseSettings class Settings(BaseSettings): APP_NAME: str = "Presentation Generator" REDIS_URL: str = "redis://localhost:6379/0" RESULT_BACKEND: str = "redis://localhost:6379/0" STORAGE_PATH: str = "./storage" OPENAI_API_KEY: str = "" class Config: env_file = ".env" settings = Settings() 
Enter fullscreen mode Exit fullscreen mode

This configuration can be overridden with environment variables or values in a .env file. Note that we've added an OPENAI_API_KEY setting that we'll use later.

Step 3: Creating Data Models

Next, let's define our data models with Pydantic. Create app/models.py:

from pydantic import BaseModel, Field from typing import List, Optional from enum import Enum class SlideType(str, Enum): TITLE = "title" CONTENT = "content" IMAGE = "image" BULLET_POINTS = "bullet_points" TWO_COLUMN = "two_column" class SlideContent(BaseModel): type: SlideType title: str content: Optional[str] = None image_url: Optional[str] = None bullet_points: Optional[List[str]] = None column1: Optional[str] = None column2: Optional[str] = None class PresentationRequest(BaseModel): title: str author: str slides: List[SlideContent] theme: Optional[str] = "default" # New model for PDF-based presentation requests class PDFPresentationRequest(BaseModel): title: Optional[str] = None author: Optional[str] = "Generated Presentation" theme: Optional[str] = "default" num_slides: Optional[int] = 5 class PresentationResponse(BaseModel): task_id: str status: str = "pending" class PresentationStatus(BaseModel): task_id: str status: str file_url: Optional[str] = None message: Optional[str] = None 
Enter fullscreen mode Exit fullscreen mode

We've added a new PDFPresentationRequest model for handling PDF uploads. This model allows customizing the title, author, theme, and number of slides to generate.

Step 4: Implementing the AI Slide Generator

Now, let's create the core AI slide generation logic. Create app/ppt_generator.py:

import os from pathlib import Path import uuid from pptx import Presentation from pptx.util import Inches, Pt from app.models import SlideType, SlideContent, PresentationRequest from app.config import settings class PPTGenerator: def __init__(self): # Ensure storage directory exists  os.makedirs(settings.STORAGE_PATH, exist_ok=True) def generate_presentation(self, request: PresentationRequest) -> str: """Generate a PowerPoint slide based on the request""" prs = Presentation() # Add title slide  title_slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(title_slide_layout) title = slide.shapes.title subtitle = slide.placeholders[1] title.text = request.title subtitle.text = f"By {request.author}" # Add content slides  for slide_content in request.slides: self._add_slide(prs, slide_content) # Save the presentation  file_id = str(uuid.uuid4()) file_path = os.path.join(settings.STORAGE_PATH, f"{file_id}.pptx") prs.save(file_path) return file_path def _add_slide(self, prs: Presentation, content: SlideContent): """Add a slide based on its type and content""" if content.type == SlideType.TITLE: slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(slide_layout) title = slide.shapes.title subtitle = slide.placeholders[1] title.text = content.title if content.content: subtitle.text = content.content elif content.type == SlideType.CONTENT: slide_layout = prs.slide_layouts[1] slide = prs.slides.add_slide(slide_layout) title = slide.shapes.title body = slide.placeholders[1] title.text = content.title if content.content: body.text = content.content elif content.type == SlideType.BULLET_POINTS: slide_layout = prs.slide_layouts[1] slide = prs.slides.add_slide(slide_layout) title = slide.shapes.title body = slide.placeholders[1] title.text = content.title if content.bullet_points: tf = body.text_frame tf.text = "" # Clear default text  for point in content.bullet_points: p = tf.add_paragraph() p.text = point p.level = 0 elif content.type == SlideType.TWO_COLUMN: slide_layout = prs.slide_layouts[3] # Assuming layout 3 is two-content  slide = prs.slides.add_slide(slide_layout) title = slide.shapes.title title.text = content.title # Handle columns - this may vary based on your pptx template  left = slide.placeholders[1] right = slide.placeholders[2] if content.column1: left.text = content.column1 if content.column2: right.text = content.column2 elif content.type == SlideType.IMAGE: # Basic image slide  slide_layout = prs.slide_layouts[5] # Blank slide with title  slide = prs.slides.add_slide(slide_layout) title = slide.shapes.title title.text = content.title # Note: In a real application, you would handle image downloads  # and insertion here. For simplicity, we're omitting this. 
Enter fullscreen mode Exit fullscreen mode

This class handles the creation of PowerPoint slides using the python-pptx library. It supports different slide types and saves the generated files with unique IDs.

Step 5: Setting Up Celery

Now, let's configure Celery for asynchronous task processing. First, create celery_app/celery_config.py:

from app.config import settings broker_url = settings.REDIS_URL result_backend = settings.RESULT_BACKEND task_serializer = 'json' result_serializer = 'json' accept_content = ['json'] timezone = 'UTC' task_track_started = True worker_hijack_root_logger = False 
Enter fullscreen mode Exit fullscreen mode

Next, initialize the Celery application in celery_app/__init__.py:

from celery import Celery from app.config import settings app = Celery('presentation_generator') app.config_from_object('celery_app.celery_config') # Import tasks to ensure they're registered from celery_app import tasks 
Enter fullscreen mode Exit fullscreen mode

Step 6: Creating Celery Tasks

Let's define our asynchronous task for generating slides. Create celery_app/tasks.py:

import os import logging from celery import shared_task from app.models import PresentationRequest from app.ppt_generator import PPTGenerator logger = logging.getLogger(__name__) @shared_task(bind=True) def generate_presentation_task(self, request_dict): """Generate a PowerPoint presentation asynchronously""" try: # Convert dict back to PresentationRequest  request = PresentationRequest(**request_dict) logger.info(f"Starting presentation generation for: {request.title}") # Generate the presentation  generator = PPTGenerator() file_path = generator.generate_presentation(request) # In a real application, you might upload to S3 or similar  file_url = f"/download/{os.path.basename(file_path)}" return { "status": "completed", "file_url": file_url, "message": "Presentation generated successfully" } except Exception as e: logger.error(f"Error generating presentation: {str(e)}") self.update_state( state="FAILURE", meta={ "status": "failed", "message": f"Error: {str(e)}" } ) raise 
Enter fullscreen mode Exit fullscreen mode

This task will be processed asynchronously by Celery workers.

Step 7: Creating the PDF Processor

Now, let's add the PDF processing functionality. Create app/pdf_processor.py:

import os import tempfile from PyPDF2 import PdfReader from openai import OpenAI from typing import List, Dict, Any from app.config import settings from app.models import SlideContent, SlideType class PDFProcessor: def __init__(self): self.client = OpenAI(api_key=settings.OPENAI_API_KEY) def extract_text_from_pdf(self, pdf_content: bytes) -> str: """Extract text content from PDF bytes""" with tempfile.NamedTemporaryFile(delete=False) as temp: temp.write(pdf_content) temp_path = temp.name try: pdf = PdfReader(temp_path) text = "" for page in pdf.pages: text += page.extract_text() + "\n" return text finally: # Clean up the temp file  if os.path.exists(temp_path): os.unlink(temp_path) def generate_presentation_content(self, text: str, title: str = None, num_slides: int = 5) -> Dict[str, Any]: """Generate presentation content using OpenAI""" # Prepare the system message  system_message = f""" You are an expert presentation creator. Your task is to create a well-structured presentation from the provided text content. Extract the key points and organize them into a cohesive presentation. Create a presentation with the following: 1. A title slide with an engaging title (if not provided) and subtitle 2. {num_slides-1} content slides Structure the presentation logically and extract the most important information. """ # Prepare the user message  user_message = f""" Create a presentation based on the following content: {text[:10000]} # Limit text to avoid token limits Please structure your response in JSON format with the following structure: {{ "title": "Main Title of Presentation", "slides": [ {{ "type": "title", "title": "Presentation Title", "content": "Subtitle - e.g. Author's Name" }}, {{ "type": "bullet_points", "title": "Key Point 1", "bullet_points": ["Point 1", "Point 2", "Point 3"] }},  ... ] }}  Ensure all slide content is concise and impactful. Use different slide types appropriately: - title: For title slides with a subtitle - content: For slides with paragraphs of text - bullet_points: For key points in a list format - two_column: For comparing information side by side """ if title: user_message += f"\nUse '{title}' as the presentation title." # Call the OpenAI API  response = self.client.chat.completions.create( model="gpt-4o", response_format={"type": "json_object"}, messages=[ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ] ) # Extract the response content  content = response.choices[0].message.content # Parse the JSON content  import json presentation_data = json.loads(content) return presentation_data 
Enter fullscreen mode Exit fullscreen mode

This class handles the extraction of text from PDF files and uses OpenAI to generate presentation content based on that text. It uses PyPDF2 to read the PDF and extract text, then sends that text to OpenAI's API with specific instructions to create a well-structured presentation.

Step 8: Updating Celery Tasks

Next, let's update our Celery tasks to handle PDF processing. Modify celery_app/tasks.py:

import os import logging from celery import shared_task from app.models import PresentationRequest, PDFPresentationRequest from app.ppt_generator import PPTGenerator from app.pdf_processor import PDFProcessor logger = logging.getLogger(__name__) @shared_task(bind=True) def generate_presentation_task(self, request_dict): """Generate a PowerPoint presentation asynchronously""" try: # Convert dict back to PresentationRequest  request = PresentationRequest(**request_dict) logger.info(f"Starting presentation generation for: {request.title}") # Generate the presentation  generator = PPTGenerator() file_path = generator.generate_presentation(request) # In a real application, you might upload to S3 or similar  file_url = f"/download/{os.path.basename(file_path)}" return { "status": "completed", "file_url": file_url, "message": "Presentation generated successfully" } except Exception as e: logger.error(f"Error generating presentation: {str(e)}") self.update_state( state="FAILURE", meta={ "status": "failed", "message": f"Error: {str(e)}" } ) raise @shared_task(bind=True) def generate_presentation_from_pdf_task(self, pdf_text, request_dict): """Generate a PowerPoint presentation from PDF text asynchronously""" try: # Convert dict back to PDFPresentationRequest  request = PDFPresentationRequest(**request_dict) logger.info(f"Starting presentation generation from PDF") # Process the PDF text with OpenAI  processor = PDFProcessor() presentation_data = processor.generate_presentation_content( pdf_text, title=request.title, num_slides=request.num_slides ) # Create a PresentationRequest from the generated content  presentation_request = PresentationRequest( title=presentation_data.get("title", request.title or "Generated Presentation"), author=request.author, theme=request.theme, slides=presentation_data.get("slides", []) ) # Generate the presentation  generator = PPTGenerator() file_path = generator.generate_presentation(presentation_request) # In a real application, you might upload to S3 or similar  file_url = f"/download/{os.path.basename(file_path)}" return { "status": "completed", "file_url": file_url, "message": "Presentation generated successfully from PDF" } except Exception as e: logger.error(f"Error generating presentation from PDF: {str(e)}") self.update_state( state="FAILURE", meta={ "status": "failed", "message": f"Error: {str(e)}" } ) raise 
Enter fullscreen mode Exit fullscreen mode

We've added a new task generate_presentation_from_pdf_task that takes the extracted PDF text and request details, then uses the PDF processor to generate presentation content with OpenAI.

Step 9: Updating the FastAPI Application

Now, let's update our FastAPI application to add the PDF upload endpoint. Modify app/main.py:

import os from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File, Form, Depends from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from celery.result import AsyncResult from typing import Optional from app.models import PresentationRequest, PDFPresentationRequest, PresentationResponse, PresentationStatus from app.config import settings from app.pdf_processor import PDFProcessor from celery_app.tasks import generate_presentation_task, generate_presentation_from_pdf_task app = FastAPI(title=settings.APP_NAME) # Mount storage directory for file downloads app.mount("/download", StaticFiles(directory=settings.STORAGE_PATH), name="download") @app.post("/api/presentations", response_model=PresentationResponse) async def create_presentation(request: PresentationRequest): """Submit a new presentation generation task""" # Submit task to Celery  task = generate_presentation_task.delay(request.model_dump()) return PresentationResponse(task_id=task.id) @app.post("/api/presentations/from-pdf", response_model=PresentationResponse) async def create_presentation_from_pdf( pdf_file: UploadFile = File(...), title: Optional[str] = Form(None), author: str = Form("Generated Presentation"), theme: str = Form("default"), num_slides: int = Form(5) ): """Submit a presentation generation task from PDF file""" if not pdf_file.filename.endswith('.pdf'): raise HTTPException(status_code=400, detail="File must be a PDF") # Read PDF file content  pdf_content = await pdf_file.read() # Extract text from PDF  processor = PDFProcessor() pdf_text = processor.extract_text_from_pdf(pdf_content) # Create request object  request = PDFPresentationRequest( title=title or f"Presentation based on {pdf_file.filename}", author=author, theme=theme, num_slides=num_slides ) # Submit task to Celery  task = generate_presentation_from_pdf_task.delay(pdf_text, request.model_dump()) return PresentationResponse(task_id=task.id) @app.get("/api/presentations/{task_id}", response_model=PresentationStatus) async def get_presentation_status(task_id: str): """Get the status of a presentation generation task""" task_result = AsyncResult(task_id) if task_result.state == 'PENDING': return PresentationStatus( task_id=task_id, status="pending", message="Task is pending" ) elif task_result.state == 'FAILURE': return PresentationStatus( task_id=task_id, status="failed", message=str(task_result.info.get('message', 'Unknown error')) ) elif task_result.state == 'SUCCESS': result = task_result.get() return PresentationStatus( task_id=task_id, status="completed", file_url=result.get('file_url'), message=result.get('message') ) else: return PresentationStatus( task_id=task_id, status=task_result.state.lower(), message="Task is in progress" ) @app.get("/api/download/{file_id}") async def download_presentation(file_id: str): """Download a generated presentation""" file_path = os.path.join(settings.STORAGE_PATH, file_id) if not os.path.exists(file_path): raise HTTPException(status_code=404, detail="File not found") return FileResponse(path=file_path, filename=f"presentation_{file_id}") 
Enter fullscreen mode Exit fullscreen mode

We've added a new endpoint /api/presentations/from-pdf that accepts PDF file uploads along with optional parameters like title, author, theme, and the number of slides to generate.

Step 10: Containerizing with Docker

Let's update our Docker configuration to include the OpenAI API key. First, create a .env file:

APP_NAME=Presentation Generator REDIS_URL=redis://redis:6379/0 RESULT_BACKEND=redis://redis:6379/0 STORAGE_PATH=/app/storage OPENAI_API_KEY=your_openai_api_key_here 
Enter fullscreen mode Exit fullscreen mode

Next, update the docker-compose.yml file to include the OpenAI API key:

version: '3' services: api: build: . command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload volumes: - .:/app - presentation_data:/app/storage ports: - "8000:8000" depends_on: - redis environment: - REDIS_URL=redis://redis:6379/0 - RESULT_BACKEND=redis://redis:6379/0 - OPENAI_API_KEY=${OPENAI_API_KEY} worker: build: . command: celery -A celery_app worker --loglevel=info volumes: - .:/app - presentation_data:/app/storage depends_on: - redis environment: - REDIS_URL=redis://redis:6379/0 - RESULT_BACKEND=redis://redis:6379/0 - OPENAI_API_KEY=${OPENAI_API_KEY} redis: image: redis:7-alpine ports: - "6379:6379" volumes: presentation_data: 
Enter fullscreen mode Exit fullscreen mode

This setup will pass your OpenAI API key from the .env file to the containerized services.Separation of concerns** - API, task processing, and presentation generation are separate

  • Asynchronous processing - Long-running tasks don't block the API
  • Containerization - Easy deployment and scaling
  • Type safety - Pydantic models ensure data validation

You can extend this project in many ways, such as adding more slide types, integrating with data visualization libraries, or implementing template management.

Feel free to customize this service to fit your specific needs and save yourself from the drudgery of creating presentations manually!

GitHub Repository

The complete code for this tutorial is available on GitHub.


If you found this tutorial helpful, give it a ❀️ and share it with others who might benefit from creating slides with AI!

Happy coding! πŸš€

Top comments (0)