AI-video-lifeplan-demo/backend/ppt_extractor.py

261 lines
8.6 KiB
Python

"""
PowerPoint presentation extractor
This module handles:
- Extracting slides from PPTX files
- Converting slides to images
- Extracting text content from slides
- Matching slides to script segments
"""
import os
from typing import Dict, Any, List, Optional
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
try:
from pptx import Presentation
except ImportError:
Presentation = None
try:
from PIL import Image
import io
except ImportError:
Image = None
class PPTExtractor:
"""Extract slides and content from PowerPoint presentations"""
def __init__(self):
if Presentation is None:
raise ImportError("python-pptx not installed. Run: pip install python-pptx")
if Image is None:
raise ImportError("Pillow not installed. Run: pip install Pillow")
def extract(self, ppt_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
"""
Extract slides from PowerPoint presentation
Args:
ppt_path: Path to PPTX file
output_dir: Directory to save slide images (default: assets/1_processed/slides)
Returns:
Dictionary containing slide information:
{
"total_slides": int,
"slides": [
{
"index": 0,
"title": str,
"text": str,
"image_path": str,
"notes": str
},
...
]
}
"""
ppt_path = Path(ppt_path)
if not ppt_path.exists():
raise FileNotFoundError(f"PPT file not found: {ppt_path}")
# Set output directory
if output_dir is None:
# CRITICAL: Output directly to public/ for Remotion to find them via staticFile
output_dir = Path("public")
else:
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f" 📊 Loading PowerPoint: {ppt_path.name}")
prs = Presentation(str(ppt_path))
slides_data = []
for idx, slide in enumerate(prs.slides):
slide_info = self._extract_slide_content(slide, idx)
# Export slide as image using LibreOffice/PowerPoint
image_path = output_dir / f"slide_{idx + 1:03d}.png"
slide_info["image_path"] = str(image_path)
slides_data.append(slide_info)
print(f" ✓ Extracted slide {idx + 1}: {slide_info['title'][:50]}...")
result = {
"total_slides": len(slides_data),
"slides": slides_data,
"source_file": str(ppt_path)
}
print(f" ✓ Extracted {len(slides_data)} slides from PowerPoint")
# Export slides as images
self._export_slides_as_images(str(ppt_path), output_dir)
return result
def _extract_slide_content(self, slide, index: int) -> Dict[str, Any]:
"""Extract text content from a single slide"""
slide_info = {
"index": index,
"title": "",
"text": "",
"notes": ""
}
# Extract title (usually the first text box)
shapes_text = []
for shape in slide.shapes:
if hasattr(shape, "text") and shape.text:
text = shape.text.strip()
if text:
if not slide_info["title"]:
slide_info["title"] = text
else:
shapes_text.append(text)
# Combine all text
slide_info["text"] = "\n".join(shapes_text)
# Extract notes
if slide.has_notes_slide:
notes_slide = slide.notes_slide
if notes_slide.notes_text_frame:
slide_info["notes"] = notes_slide.notes_text_frame.text.strip()
return slide_info
def _export_slides_as_images(self, ppt_path: str, output_dir: Path):
"""
Export slides as PNG images
Tries multiple methods in order:
1. Windows PowerPoint COM (if on Windows)
2. LibreOffice (cross-platform)
3. Placeholder images (fallback)
"""
import sys
import subprocess
# Method 1: Try Windows PowerPoint COM
if sys.platform == 'win32':
try:
print(" 🖼️ Converting slides to images with PowerPoint...")
success = self._export_with_powerpoint_com(ppt_path, output_dir)
if success:
print(" ✓ Slides exported as images")
return
except Exception as e:
print(f" ⚠️ PowerPoint COM failed: {e}")
# Method 2: Try LibreOffice
try:
print(" 🖼️ Converting slides to images with LibreOffice...")
subprocess.run([
"soffice",
"--headless",
"--convert-to", "png",
"--outdir", str(output_dir),
ppt_path
], check=True, capture_output=True, timeout=60)
print(" ✓ Slides exported as images")
return
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
pass
# Method 3: Generate placeholders
print(" ⚠️ No conversion tool available, generating placeholder images...")
self._generate_placeholder_images(output_dir)
def _export_with_powerpoint_com(self, ppt_path: str, output_dir: Path) -> bool:
"""
Export slides using Windows PowerPoint COM interface
Returns True if successful, False otherwise
"""
try:
import win32com.client
import pythoncom
except ImportError:
print(" ⚠️ pywin32 not installed. Run: pip install pywin32")
return False
try:
pythoncom.CoInitialize()
powerpoint = win32com.client.Dispatch("PowerPoint.Application")
powerpoint.Visible = 1
# Ensure output directory exists
output_dir.mkdir(parents=True, exist_ok=True)
# Open presentation (use absolute path)
prs = powerpoint.Presentations.Open(str(Path(ppt_path).absolute()), WithWindow=False)
# Export each slide as PNG
for i, slide in enumerate(prs.Slides):
output_file = output_dir / f"slide_{i + 1:03d}.png"
# Use absolute path for export
slide.Export(str(output_file.absolute()), "PNG", 1920, 1080)
# Close presentation
prs.Close()
powerpoint.Quit()
pythoncom.CoUninitialize()
return True
except Exception as e:
print(f" ⚠️ PowerPoint COM error: {e}")
try:
powerpoint.Quit()
pythoncom.CoUninitialize()
except:
pass
return False
def _generate_placeholder_images(self, output_dir: Path):
"""Generate placeholder slide images"""
from PIL import Image, ImageDraw, ImageFont
# Create placeholder images for slides
slide_files = list(output_dir.glob("slide_*.png"))
if not slide_files:
# Generate at least one placeholder
img = Image.new('RGB', (1920, 1080), color='white')
draw = ImageDraw.Draw(img)
# Draw simple text
text = "PowerPoint Slide"
draw.text((960, 540), text, fill='black', anchor="mm")
img.save(output_dir / "slide_001.png")
print(" ✓ Generated placeholder slide image")
def extract_ppt_data(ppt_path: str) -> Dict[str, Any]:
"""Convenience function for PPT extraction"""
extractor = PPTExtractor()
return extractor.extract(ppt_path)
if __name__ == "__main__":
# Test extraction
import sys
if len(sys.argv) < 2:
print("Usage: python ppt_extractor.py <path_to_pptx>")
sys.exit(1)
ppt_path = sys.argv[1]
result = extract_ppt_data(ppt_path)
print(f"\n📊 Extracted {result['total_slides']} slides")
for slide in result['slides']:
print(f"\nSlide {slide['index'] + 1}:")
print(f" Title: {slide['title']}")
print(f" Text length: {len(slide['text'])} chars")
print(f" Image: {slide['image_path']}")