storyboard-to-slides
Originally frommoosegoose0701/skill-compose
SKILL.md
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
1. Read the Storyboard
import csv
with open("storyboard.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
slides = list(reader)
2. 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).
3. 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.
4. 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
5. 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")
6. 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-pptxembeds 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
Weekly Installs
4
Repository
dp-archive/archiveGitHub Stars
1.1K
First Seen
9 days ago
Security Audits
Installed on
opencode4
gemini-cli4
claude-code4
github-copilot4
codex4
kimi-cli4