Storyboard to Slides
Assemble a polished PPTX from a storyboard CSV + image files using python-pptx .
Dependencies
pip install python-pptx Pillow
Input Format
Expects storyboard.csv with columns: slide_no, slide_type, title, bullet_points, image_prompt, speaker_notes, layout
And image files named slide_{no}.png for each row.
Workflow
- Read the Storyboard
import csv
with open("storyboard.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) slides = list(reader)
- Initialize the Presentation
from pptx import Presentation from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor
prs = Presentation() prs.slide_width = Inches(13.333) # 16:9 prs.slide_height = Inches(7.5)
For 4:3 slides: Inches(10) x Inches(7.5) .
- Define Theme Constants
Customize per project
THEME = { "bg_color": RGBColor(0x1A, 0x1A, 0x2E), # dark navy "title_color": RGBColor(0xFF, 0xFF, 0xFF), "text_color": RGBColor(0xE0, 0xE0, 0xE0), "accent_color": RGBColor(0x64, 0xB5, 0xF6), "title_font": "Arial", "body_font": "Arial", "title_size": Pt(36), "body_size": Pt(18), }
Choose colors that contrast well with the generated images. For light images use dark text overlay with semi-transparent background; for dark images use white text.
- Layout Implementations
full_bg — Full-screen background image + overlay text
from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
def add_full_bg_slide(prs, img_path, title, subtitle="", theme=THEME): slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank # Background image slide.shapes.add_picture(img_path, 0, 0, prs.slide_width, prs.slide_height) # Semi-transparent overlay overlay = slide.shapes.add_shape( 1, 0, 0, prs.slide_width, prs.slide_height # MSO_SHAPE.RECTANGLE = 1 ) overlay.fill.solid() overlay.fill.fore_color.rgb = RGBColor(0, 0, 0) overlay.shadow.inherit = False overlay.line.fill.background() # Set overlay transparency via XML (access the shape's XML element, not the fill object) from pptx.oxml.ns import qn solid = overlay._element.find(f'.//{qn("a:solidFill")}') if solid is not None: srgb = solid.find(qn("a:srgbClr")) if srgb is not None: alpha = srgb.makeelement(qn("a:alpha"), {"val": "40000"}) # 40% opacity srgb.append(alpha) # Title txBox = slide.shapes.add_textbox(Inches(1), Inches(2.5), Inches(11), Inches(2)) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = title p.font.size = Pt(44) p.font.bold = True p.font.color.rgb = theme["title_color"] p.alignment = PP_ALIGN.CENTER if subtitle: p2 = tf.add_paragraph() p2.text = subtitle p2.font.size = Pt(24) p2.font.color.rgb = theme["text_color"] p2.alignment = PP_ALIGN.CENTER return slide
left_img_right_text — Image left, text right
def add_left_img_right_text(prs, img_path, title, bullets, theme=THEME): slide = prs.slides.add_slide(prs.slide_layouts[6]) # Solid background bg = slide.background fill = bg.fill fill.solid() fill.fore_color.rgb = theme["bg_color"] # Image on left (half width) slide.shapes.add_picture(img_path, 0, 0, Inches(6.5), prs.slide_height) # Title txBox = slide.shapes.add_textbox(Inches(7), Inches(0.5), Inches(5.8), Inches(1.2)) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = title p.font.size = theme["title_size"] p.font.bold = True p.font.color.rgb = theme["title_color"] # Bullets txBox2 = slide.shapes.add_textbox(Inches(7), Inches(2), Inches(5.8), Inches(4.5)) tf2 = txBox2.text_frame tf2.word_wrap = True for i, line in enumerate(bullets.split("\n")): line = line.strip().lstrip("•-").strip() if not line: continue p = tf2.paragraphs[0] if i == 0 else tf2.add_paragraph() p.text = f" {line}" p.font.size = theme["body_size"] p.font.color.rgb = theme["text_color"] p.space_after = Pt(12) return slide
top_img_bottom_text — Image top, text bottom
def add_top_img_bottom_text(prs, img_path, title, bullets, theme=THEME): slide = prs.slides.add_slide(prs.slide_layouts[6]) bg = slide.background bg.fill.solid() bg.fill.fore_color.rgb = theme["bg_color"] # Image on top (60% height) slide.shapes.add_picture(img_path, 0, 0, prs.slide_width, Inches(4.5)) # Title txBox = slide.shapes.add_textbox(Inches(0.8), Inches(4.8), Inches(11.5), Inches(0.8)) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = title p.font.size = Pt(28) p.font.bold = True p.font.color.rgb = theme["title_color"] # Bullets txBox2 = slide.shapes.add_textbox(Inches(0.8), Inches(5.7), Inches(11.5), Inches(1.5)) tf2 = txBox2.text_frame tf2.word_wrap = True for i, line in enumerate(bullets.split("\n")): line = line.strip().lstrip("•-").strip() if not line: continue p = tf2.paragraphs[0] if i == 0 else tf2.add_paragraph() p.text = f" {line}" p.font.size = Pt(16) p.font.color.rgb = theme["text_color"] return slide
center_text — Section divider / title-only
def add_center_text_slide(prs, img_path, title, subtitle="", theme=THEME): slide = prs.slides.add_slide(prs.slide_layouts[6]) if img_path and os.path.exists(img_path): slide.shapes.add_picture(img_path, 0, 0, prs.slide_width, prs.slide_height) # Add overlay same as full_bg else: bg = slide.background bg.fill.solid() bg.fill.fore_color.rgb = theme["bg_color"] txBox = slide.shapes.add_textbox(Inches(2), Inches(2.8), Inches(9), Inches(2)) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = title p.font.size = Pt(40) p.font.bold = True p.font.color.rgb = theme["title_color"] p.alignment = PP_ALIGN.CENTER if subtitle: p2 = tf.add_paragraph() p2.text = subtitle p2.font.size = Pt(20) p2.font.color.rgb = theme["text_color"] p2.alignment = PP_ALIGN.CENTER return slide
two_column — Two-column text (for data slides)
def add_two_column_slide(prs, img_path, title, bullets, theme=THEME): slide = prs.slides.add_slide(prs.slide_layouts[6]) bg = slide.background bg.fill.solid() bg.fill.fore_color.rgb = theme["bg_color"] # Title txBox = slide.shapes.add_textbox(Inches(0.8), Inches(0.5), Inches(11.5), Inches(1)) tf = txBox.text_frame p = tf.paragraphs[0] p.text = title p.font.size = theme["title_size"] p.font.bold = True p.font.color.rgb = theme["title_color"] # Split bullets into two columns lines = [l.strip().lstrip("•-").strip() for l in bullets.split("\n") if l.strip()] mid = len(lines) // 2 left_lines, right_lines = lines[:mid], lines[mid:] for col_idx, (col_lines, left_pos) in enumerate([(left_lines, 0.8), (right_lines, 7.0)]): txBox = slide.shapes.add_textbox(Inches(left_pos), Inches(1.8), Inches(5.5), Inches(5)) tf = txBox.text_frame tf.word_wrap = True for i, line in enumerate(col_lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.text = f" {line}" p.font.size = theme["body_size"] p.font.color.rgb = theme["text_color"] p.space_after = Pt(10) if img_path and os.path.exists(img_path): slide.shapes.add_picture(img_path, Inches(9.5), Inches(4.5), Inches(3.5), Inches(2.5)) return slide
- Assembly Loop
import os
LAYOUT_MAP = { "full_bg": add_full_bg_slide, "left_img_right_text": add_left_img_right_text, "top_img_bottom_text": add_top_img_bottom_text, "center_text": add_center_text_slide, "two_column": add_two_column_slide, }
for row in slides: no = row["slide_no"] layout = row.get("layout", "left_img_right_text") img_path = f"slide_{no}.png" title = row.get("title", "") bullets = row.get("bullet_points", "")
fn = LAYOUT_MAP.get(layout, add_left_img_right_text)
if layout in ("full_bg", "center_text"):
fn(prs, img_path, title, subtitle=bullets, theme=THEME)
else:
fn(prs, img_path, title, bullets, theme=THEME)
prs.save("presentation.pptx")
- Post-Assembly Checks
After saving, verify:
-
File size is reasonable (images embedded increase size)
-
Slide count matches storyboard row count
-
Report the output filename and size to the user
Font Notes
-
python-pptx embeds font name references, not font files
-
Safe cross-platform fonts: Arial, Calibri, Helvetica
-
For CJK content: specify "Microsoft YaHei", "Noto Sans CJK SC", or "PingFang SC"
-
If the user specifies a custom font, check availability first
Tips
-
Always use prs.slide_layouts[6] (blank layout) for full control
-
Images should match the slide aspect ratio to avoid stretching
-
For dark themes, use light text; for light themes, use dark text
-
Keep the overlay opacity between 30%–50% for readability over images
-
Speaker notes: slide.notes_slide.notes_text_frame.text = notes