261 lines
8.6 KiB
Python
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']}")
|