skilly. Buy ad slot
All skills
Community / AGENT SKILL

beamer-ppt

zhouziyue233/great-econometrics
0 installs 8 GitHub stars
0

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.

BEFORE YOU INSTALL

Understand the trade-offs.

SECURITY REVIEW

Not yet assessed

Review the original instructions and requested permissions before installing.

No security review is available for this catalog entry yet.

SKILL QUALITY

Not yet assessed

How clearly the skill guides your agent, how complete its workflow is, and how you can check the outcome.

No quality assessment is available for this catalog entry yet.

The full skill.

Original instructions from the publisher’s SKILL.md

# Beamer-ppt-Creator

## Purpose

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.

## When to Use

- Called by `/present` command to produce the final `slides/slides.pptx`
- Preparing conference, seminar, or job market slides
- Converting a completed economics paper into a slide deck

## Design Principles

- **One idea per slide** — split if content overflows
- **Minimum 20pt** for body text; 24pt for frame titles
- **Consistent palette** — navy blue primary, one accent color only
- **Figures over tables** — embed PNG images at ≥ 200 DPI
- **Last slide = Takeaways**, never "Questions?"

---

## Implementation

This skill executes Python code using `python-pptx`. Always install dependencies first:

```bash
pip install python-pptx pdf2image --break-system-packages
apt-get install -y poppler-utils 2>/dev/null || true
```

### Color Palettes by Theme

| 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 |

### Core Helper Functions

```python
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
```

### Slide Factory Functions

```python
# ── 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
```

### PDF → PNG Conversion (for figures from /plot)

```python
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
```

### Save, Export PDF & Verify

```python
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 Structure by Presentation Type

| 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 |

---

## Best Practices

1. **One message per slide** — split if content overflows
2. **Use figures over tables** — embed PNG at ≥ 200 DPI
3. **Bold the preferred specification** column in regression tables
4. **Add speaker notes** to every key slide via `add_speaker_notes()`
5. **Prepare appendix slides** for anticipated Q&A
6. **Timing**: budget 1.5 min/slide; final slide must be Takeaways

## Common Pitfalls

- ❌ Too much text (max 5 bullets per slide, max 10 words per bullet)
- ❌ Tables with more than 4 columns
- ❌ Ending with "Thank you / Questions?" — use Takeaways instead
- ❌ Embedding low-resolution images (< 150 DPI looks blurry on projectors)
- ❌ Skipping the "This Paper" preview slide