From great-econometrics
Create Beamer-style academic PPTX presentations using python-pptx. Produces publication-quality .pptx files with navy-blue Metropolis theme (16:9, frame title bars, progress bar) for conference talks, job market presentations, and seminar slides. Called by /present command.
npx claudepluginhub zhouziyue233/great-econometrics --plugin econometricsThis skill uses the workspace's default tool permissions.
This skill generates professional academic **PPTX** presentations that faithfully replicate the visual style of LaTeX Beamer (Metropolis theme). Output is a `.pptx` file that can be opened, edited, and presented directly in PowerPoint or LibreOffice Impress — no LaTeX installation required.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
This skill generates professional academic PPTX presentations that faithfully replicate the visual style of LaTeX Beamer (Metropolis theme). Output is a .pptx file that can be opened, edited, and presented directly in PowerPoint or LibreOffice Impress — no LaTeX installation required.
/present command to produce the final slides/slides.pptxThis skill executes Python code using python-pptx. Always install dependencies first:
pip install python-pptx pdf2image --break-system-packages
apt-get install -y poppler-utils 2>/dev/null || true
| Theme | Title Bar bg | Accent | Slide bg |
|---|---|---|---|
| A. Metropolis (default) | RGB(0, 35, 82) navy | RGB(180, 30, 30) red | RGB(245, 245, 245) light gray |
| B. Minimal (job market) | RGB(0, 35, 82) navy | RGB(0, 35, 82) navy | RGB(255, 255, 255) white |
| C. Madrid (traditional) | RGB(31, 73, 125) dark blue | RGB(189, 152, 44) gold | RGB(255, 255, 255) white |
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
import os
# ── Presentation setup ───────────────────────────────────────────
prs = Presentation()
prs.slide_width = Inches(13.33) # 16:9 widescreen (Beamer aspectratio=169)
prs.slide_height = Inches(7.5)
# ── Color definitions (Metropolis theme) ─────────────────────────
NAVY = RGBColor(0, 35, 82)
RED = RGBColor(180, 30, 30)
LGRAY = RGBColor(245, 245, 245)
WHITE = RGBColor(255, 255, 255)
BLACK = RGBColor(30, 30, 30)
MGRAY = RGBColor(100, 100, 100)
def add_bg(slide, prs, color):
"""Full-slide background rectangle."""
shape = slide.shapes.add_shape(
1, 0, 0, prs.slide_width, prs.slide_height)
shape.fill.solid()
shape.fill.fore_color.rgb = color
shape.line.fill.background()
return shape
def add_frame_title(slide, prs, text, bg=NAVY, fg=WHITE):
"""Navy title bar (1.1 in tall) — mimics Beamer \\frametitle."""
bar = slide.shapes.add_shape(
1, 0, 0, prs.slide_width, Inches(1.1))
bar.fill.solid()
bar.fill.fore_color.rgb = bg
bar.line.fill.background()
tf = bar.text_frame
tf.word_wrap = False
tf.margin_left = Inches(0.3)
tf.margin_top = Inches(0.22)
p = tf.paragraphs[0]
p.text = text
p.font.bold = True
p.font.size = Pt(24)
p.font.color.rgb = fg
p.alignment = PP_ALIGN.LEFT
def add_progress_bar(slide, prs, current, total, color=NAVY):
"""Metropolis-style thin progress bar at bottom."""
h = Inches(0.055)
top = prs.slide_height - h
# Gray track
track = slide.shapes.add_shape(
1, 0, top, prs.slide_width, h)
track.fill.solid()
track.fill.fore_color.rgb = RGBColor(200, 200, 200)
track.line.fill.background()
# Filled portion
filled_w = int(prs.slide_width * current / max(total, 1))
if filled_w > 0:
bar = slide.shapes.add_shape(1, 0, top, filled_w, h)
bar.fill.solid()
bar.fill.fore_color.rgb = color
bar.line.fill.background()
def add_speaker_notes(slide, notes_text):
"""Add speaker notes to a slide."""
slide.notes_slide.notes_text_frame.text = notes_text
# ── 1. Title slide ───────────────────────────────────────────────
def make_title_slide(prs, title, subtitle, author, institute, date_line):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, NAVY)
def _tb(left, top, w, h):
tb = slide.shapes.add_textbox(
Inches(left), Inches(top), Inches(w), Inches(h))
tb.text_frame.word_wrap = True
return tb.text_frame
# Paper title
tf = _tb(1, 1.7, 11.33, 2.0)
p = tf.paragraphs[0]
p.text = title; p.font.bold = True
p.font.size = Pt(34); p.font.color.rgb = WHITE
p.alignment = PP_ALIGN.CENTER
# Subtitle
if subtitle:
p2 = tf.add_paragraph()
p2.text = subtitle; p2.font.size = Pt(20)
p2.font.color.rgb = LGRAY; p2.alignment = PP_ALIGN.CENTER
# Author + institute
tf2 = _tb(1, 4.3, 11.33, 1.4)
p3 = tf2.paragraphs[0]
p3.text = author; p3.font.size = Pt(18)
p3.font.color.rgb = WHITE; p3.alignment = PP_ALIGN.CENTER
p4 = tf2.add_paragraph()
p4.text = institute; p4.font.size = Pt(15)
p4.font.color.rgb = LGRAY; p4.alignment = PP_ALIGN.CENTER
# Date / conference
tf3 = _tb(1, 6.1, 11.33, 0.8)
p5 = tf3.paragraphs[0]
p5.text = date_line; p5.font.size = Pt(13)
p5.font.color.rgb = LGRAY; p5.alignment = PP_ALIGN.CENTER
return slide
# ── 2. Content slide (bullet list) ──────────────────────────────
def make_content_slide(prs, title, bullets,
current=None, total=None, bg=LGRAY):
"""
bullets: list of (indent_level, text) tuples.
indent_level 0 = top-level bullet, 1 = sub-bullet.
"""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, bg)
add_frame_title(slide, prs, title)
tb = slide.shapes.add_textbox(
Inches(0.5), Inches(1.3), Inches(12.33), Inches(5.8))
tf = tb.text_frame; tf.word_wrap = True
for i, (lvl, text) in enumerate(bullets):
p = tf.paragraphs[i] if i == 0 else tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt(20 if lvl == 0 else 17)
p.font.color.rgb = BLACK
p.space_before = Pt(8 if lvl == 0 else 4)
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 3. Figure slide ──────────────────────────────────────────────
def make_figure_slide(prs, title, img_path, caption="",
current=None, total=None):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
slide.shapes.add_picture(
img_path,
left=Inches(1.17), top=Inches(1.3),
width=Inches(11.0), height=Inches(5.2))
if caption:
cap = slide.shapes.add_textbox(
Inches(0.5), Inches(6.6), Inches(12.33), Inches(0.7))
cap.text_frame.paragraphs[0].text = caption
cap.text_frame.paragraphs[0].font.size = Pt(11)
cap.text_frame.paragraphs[0].font.color.rgb = MGRAY
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 4. Regression table slide ────────────────────────────────────
def make_table_slide(prs, title, headers, rows,
footnote="", highlight_last_col=True,
current=None, total=None):
"""
headers: list of str (first col is row label).
rows: list of lists of str.
Last column is treated as the preferred specification (bolded).
"""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
nc = len(headers); nr = len(rows) + 1
tbl = slide.shapes.add_table(
nr, nc,
Inches(0.5), Inches(1.4),
Inches(12.33), Inches(4.5)).table
# Header row — navy background, white bold text
for j, h in enumerate(headers):
c = tbl.cell(0, j)
c.text = h
c.text_frame.paragraphs[0].font.bold = True
c.text_frame.paragraphs[0].font.size = Pt(14)
c.text_frame.paragraphs[0].font.color.rgb = WHITE
c.fill.solid(); c.fill.fore_color.rgb = NAVY
# Data rows
for i, row in enumerate(rows):
for j, val in enumerate(row):
c = tbl.cell(i + 1, j)
c.text = str(val)
c.text_frame.paragraphs[0].font.size = Pt(13)
if highlight_last_col and j == nc - 1:
c.text_frame.paragraphs[0].font.bold = True
if footnote:
fn = slide.shapes.add_textbox(
Inches(0.5), Inches(6.0), Inches(12.33), Inches(1.2))
fn.text_frame.paragraphs[0].text = footnote
fn.text_frame.paragraphs[0].font.size = Pt(10)
fn.text_frame.paragraphs[0].font.color.rgb = MGRAY
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 5. Two-column slide ──────────────────────────────────────────
def make_two_col_slide(prs, title, left_bullets, right_bullets,
current=None, total=None):
"""Two-column layout (e.g. Robustness slide)."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
for col_bullets, left_offset in [(left_bullets, 0.4),
(right_bullets, 6.9)]:
tb = slide.shapes.add_textbox(
Inches(left_offset), Inches(1.35),
Inches(5.8), Inches(5.8))
tf = tb.text_frame; tf.word_wrap = True
for i, (lvl, text) in enumerate(col_bullets):
p = tf.paragraphs[i] if i == 0 else tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt(18 if lvl == 0 else 15)
p.font.color.rgb = BLACK
p.space_before = Pt(6 if lvl == 0 else 3)
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
import subprocess
def pdf_to_png(pdf_path, dpi=200):
"""Convert PDF figure to PNG for embedding in PPTX."""
png_base = pdf_path.replace(".pdf", "")
try:
subprocess.run(
["pdftoppm", "-r", str(dpi), "-png", "-singlefile",
pdf_path, png_base],
check=True, capture_output=True)
return png_base + ".png"
except (subprocess.CalledProcessError, FileNotFoundError):
# Fallback: pdf2image
from pdf2image import convert_from_path
imgs = convert_from_path(pdf_path, dpi=dpi)
png_path = png_base + ".png"
imgs[0].save(png_path, "PNG")
return png_path
import subprocess
def save_and_verify(prs, output_path, export_pdf=True):
"""Save PPTX, optionally export PDF via LibreOffice, then verify."""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
prs.save(output_path)
# ── Verify PPTX ──────────────────────────────────────────────
check = Presentation(output_path)
n = len(check.slides)
assert n > 0, "PPTX is empty — check slide generation."
print(f"✅ PPTX saved : {output_path}")
print(f" {n} slides | {os.path.getsize(output_path) // 1024} KB")
# ── Export PDF ───────────────────────────────────────────────
pdf_path = None
if export_pdf:
pdf_path = _pptx_to_pdf(output_path)
return output_path, pdf_path
def _pptx_to_pdf(pptx_path):
"""Convert PPTX → PDF using LibreOffice headless."""
out_dir = os.path.dirname(pptx_path)
try:
result = subprocess.run(
["libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", out_dir, pptx_path],
capture_output=True, text=True, timeout=120
)
pdf_path = pptx_path.replace(".pptx", ".pdf")
if os.path.exists(pdf_path):
print(f"✅ PDF exported: {pdf_path}")
print(f" {os.path.getsize(pdf_path) // 1024} KB")
return pdf_path
else:
print(f"⚠️ LibreOffice conversion failed: {result.stderr.strip()}")
print(" → Open slides.pptx in PowerPoint and export manually.")
return None
except FileNotFoundError:
print("⚠️ LibreOffice not found. Install with:")
print(" apt-get install -y libreoffice # Ubuntu/Debian")
print(" brew install --cask libreoffice # macOS")
print(" → You can also export PDF from PowerPoint / LibreOffice Impress.")
return None
except subprocess.TimeoutExpired:
print("⚠️ LibreOffice timed out. Try running manually:")
print(f" libreoffice --headless --convert-to pdf {pptx_path}")
return None
| Slide Section | 15-min conf (≤15) | 45-min seminar (≤30) | Job market (≤20) |
|---|---|---|---|
| Title | 1 | 1 | 1 |
| Motivation | 1–2 | 2–3 | 2–3 |
| This Paper | 1 | 1 | 1 |
| Related Lit | — | 1–2 | 1–2 |
| Data | 1 | 2 | 2 |
| Identification | 2 | 3–4 | 3 |
| Main Results | 3 | 5–7 | 4–5 |
| Robustness | 1 | 2–3 | 2 |
| Heterogeneity | — | 2–3 | 1–2 |
| Takeaways | 1 | 1 | 1 |
add_speaker_notes()