pptx-deck
Skill1 starsChanged 29 days agoMITView on GitHub
Plan and build a PowerPoint (.pptx) deck from the user's material - one claim per slide, plain wording, a real layout per slide, then generate the file with the bundled script. Use when asked for slides, a presentation or a .pptx.
- Installs packages
In this repositoryOne other file it gives its agents.
- skill-lintSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 8 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 14
Ask only what you cannot infer, in one message: who will see it, what they should do or decide afterwards, how long it runs, and whether a …
- Distinctive: line 22
Write the outline as one full sentence per slide stating that slide's point (the headline), not a topic label.
- Very specific: line 39
Write a JSON file with the slides (format below).
- Distinctive: line 86
Only read the files the user pointed to and write the spec and the output next to them.
- Distinctive: line 88
Do not invent figures, quotes or sources.
- Distinctive: line 99
No colon-subtitles, no question headlines unless the slide answers it at once.
- Distinctive: line 109
No emoji as bullets or decoration.
- Distinctive: line 110
Use the same words for the same thing across slides.
- Distinctive: line 122
Never stretch an image. Say what a chart shows in its headline, not in a caption.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
pptx-deck
Turn the user's material into a deck that looks made by a person who knows the subject, not by a template. Work in this order and do not skip the first two steps; a deck built straight from a blank spec is where the generic look comes from.
1. Pin down the deck
Ask only what you cannot infer, in one message: who will see it, what they should do or decide afterwards, how long it runs, and whether a template or brand colour is given. If the material is a file, read it first. Write the answer to "what should the audience do after the last slide" as one sentence; every slide must serve it.
2. Outline as claims
Write the outline as one full sentence per slide stating that slide's point (the headline), not a topic label. Cut every slide whose sentence does not support the goal. Typical length: one slide per minute of speaking, plus a title and a closing slide. Show the outline and wait for a go-ahead or edits before building.
3. Choose a layout per slide
Pick from the layouts the script supports and vary them: title, section,
points (headline plus up to 5 short bullets), two-column, image (one
picture, optional caption), table, quote, numbers (up to 4 big figures
with labels). Never use points for more than two slides in a row. Follow
the Style rules section below for wording and visual rules; read it before
writing any slide text.
4. Write the spec
Write a JSON file with the slides (format below). Put the longer explanation in
notes, not on the slide. Use only images the user supplied or that you made
locally; never fetch images or fonts from the network.
{
"title": "Deck title",
"theme": {"font": "Calibri", "accent": "1F4E79", "ink": "1A1A1A", "paper": "FFFFFF"},
"slides": [
{"layout": "title", "title": "...", "subtitle": "..."},
{"layout": "points", "title": "A full-sentence claim", "bullets": ["...", "..."], "notes": "..."},
{"layout": "numbers", "title": "...", "items": [{"value": "42%", "label": "..."}]},
{"layout": "two-column", "title": "...", "left": {"heading": "...", "bullets": []}, "right": {"heading": "...", "bullets": []}},
{"layout": "image", "title": "...", "image": "chart.png", "caption": "..."},
{"layout": "table", "title": "...", "header": ["..."], "rows": [["..."]]},
{"layout": "quote", "quote": "...", "by": "..."},
{"layout": "section", "title": "..."}
]
}5. Build and check
Save the script from the Script section below as build_deck.py in the
user's working folder (read it back once so you know what it does). It needs
the python-pptx package. If python3 -c "import pptx" fails,
tell the user the one command to install it (python3 -m pip install python-pptx,
preferably in a virtual environment) and let them run it; do not install packages
yourself.
python3 build_deck.py spec.json out.pptxThe script prints a warning for each slide that breaks a size rule (too many
bullets, long bullets, long headline, missing image). Fix the warnings by
cutting words, not by shrinking text. If LibreOffice is installed
(soffice --headless --convert-to pdf out.pptx), convert and look at the pages;
otherwise say the deck was not visually checked.
6. Hand over
Give the path, the slide count, any warning that remains and why it stays, and what you could not check. Offer to adjust the outline rather than the visuals first.
Rules
- Only read the files the user pointed to and write the spec and the output next to them; run nothing else from those files.
- Do not invent figures, quotes or sources. A number on a slide comes from the user's material or is marked as an assumption in the notes.
- The script below is plain text; show it to the user if they ask what it does.
Style rules
Wording and visual rules for the deck. Apply them to every slide.
Headlines
- A full sentence that states the point ("Support tickets fell 31% after the new routing"), at most 12 words. Not a label ("Results", "Overview").
- No colon-subtitles, no question headlines unless the slide answers it at once.
Body text
- At most 5 bullets, each at most 14 words, each a complete thought. If a slide needs more, it is two slides or its detail belongs in the notes.
- Plain verbs and concrete nouns. Name the actual thing, number or person.
- Cut these habits: "leverage", "robust", "seamless", "cutting-edge", "game-changing", "unlock", "empower", "in today's fast-paced world", "it is important to note", three-item lists made only for rhythm, a closing line that restates the slide, and a bold lead-in on every bullet.
- No emoji as bullets or decoration. No rhetorical "not just X, but Y" frames.
- Use the same words for the same thing across slides; do not rotate synonyms.
- Numbers carry their unit and period ("12.4 s median, March"). Round to what the audience can use.
Visuals
- One idea, one main visual per slide. Prefer a real chart or the user's own image over decoration. Leave empty space; do not fill it.
- One accent colour plus ink on paper. Contrast must stay readable on a projector: body text at least 20 pt, headlines at least 28 pt.
- Align to one grid; the same margins on every slide. Do not centre long text.
- A table has at most 6 rows and 5 columns on a slide; show the rest in the notes or an appendix.
- Never stretch an image. Say what a chart shows in its headline, not in a caption.
Structure
- Open with the point of the whole deck, not an agenda. Skip the agenda slide unless the audience needs to navigate a long deck.
- Close with the decision or action asked of the audience, with owner and date if known.
- Speaker notes hold the explanation, sources and caveats, in the speaker's own words, first person, as it would be said aloud.
Script
Save as build_deck.py. It reads the spec and the local images the spec names,
writes one .pptx, and uses no network.
#!/usr/bin/env python3
"""Build a .pptx from a JSON deck spec. Reads the spec and any local images it
names, writes one file, uses no network. Needs: python3 -m pip install python-pptx
usage: build_deck.py spec.json out.pptx
"""
import json
import os
import sys
try:
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Emu, Inches, Pt
except ImportError:
sys.exit("python-pptx is not installed: python3 -m pip install python-pptx")
W, H = Inches(13.333), Inches(7.5)
MARGIN = Inches(0.8)
MAX_BULLETS, MAX_WORDS, MAX_HEADLINE = 5, 14, 12
warnings = []
def rgb(hex_):
return RGBColor.from_string(hex_.lstrip("#").upper())
def words(text):
return len(str(text).split())
class Deck:
def __init__(self, theme):
self.prs = Presentation()
self.prs.slide_width, self.prs.slide_height = W, H
self.font = theme.get("font", "Calibri")
self.accent = rgb(theme.get("accent", "1F4E79"))
self.ink = rgb(theme.get("ink", "1A1A1A"))
self.paper = rgb(theme.get("paper", "FFFFFF"))
self.muted = rgb(theme.get("muted", "666666"))
def slide(self, number):
s = self.prs.slides.add_slide(self.prs.slide_layouts[6])
bg = s.background.fill
bg.solid()
bg.fore_color.rgb = self.paper
self.current = number
return s
def text(self, slide, x, y, w, h, value, size, bold=False, color=None, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP):
box = slide.shapes.add_textbox(x, y, w, h)
frame = box.text_frame
frame.word_wrap = True
frame.vertical_anchor = anchor
frame.margin_left = frame.margin_right = 0
lines = value if isinstance(value, list) else [value]
for i, line in enumerate(lines):
p = frame.paragraphs[0] if i == 0 else frame.add_paragraph()
p.alignment = align
p.space_after = Pt(size * 0.5)
run = p.add_run()
run.text = str(line)
run.font.size = Pt(size)
run.font.bold = bold
run.font.name = self.font
run.font.color.rgb = color or self.ink
return box
def headline(self, slide, value):
if words(value) > MAX_HEADLINE:
warnings.append(f"slide {self.current}: headline has {words(value)} words (max {MAX_HEADLINE})")
self.text(slide, MARGIN, Inches(0.6), W - 2 * MARGIN, Inches(1.3), value, 30, bold=True, anchor=MSO_ANCHOR.TOP)
bar = slide.shapes.add_shape(1, MARGIN, Inches(1.95), Inches(0.9), Emu(38100))
bar.fill.solid()
bar.fill.fore_color.rgb = self.accent
bar.line.fill.background()
def bullets(self, slide, x, y, w, items, size=22):
if len(items) > MAX_BULLETS:
warnings.append(f"slide {self.current}: {len(items)} bullets (max {MAX_BULLETS})")
for item in items:
if words(item) > MAX_WORDS:
warnings.append(f"slide {self.current}: a bullet has {words(item)} words (max {MAX_WORDS}): {str(item)[:40]}")
self.text(slide, x, y, w, Inches(4.6), [f"• {i}" for i in items], size)
def notes(self, slide, value):
if value:
slide.notes_slide.notes_text_frame.text = str(value)
def layout_title(d, s, spec):
d.text(s, MARGIN, Inches(2.4), W - 2 * MARGIN, Inches(1.6), spec.get("title", ""), 44, bold=True, anchor=MSO_ANCHOR.BOTTOM)
if spec.get("subtitle"):
d.text(s, MARGIN, Inches(4.2), W - 2 * MARGIN, Inches(1.0), spec["subtitle"], 22, color=d.muted)
def layout_section(d, s, spec):
d.text(s, MARGIN, Inches(3.0), W - 2 * MARGIN, Inches(1.5), spec.get("title", ""), 38, bold=True, color=d.accent, anchor=MSO_ANCHOR.MIDDLE)
def layout_points(d, s, spec):
d.headline(s, spec.get("title", ""))
d.bullets(s, MARGIN, Inches(2.4), W - 2 * MARGIN, spec.get("bullets", []))
def layout_two(d, s, spec):
d.headline(s, spec.get("title", ""))
half = (W - 2 * MARGIN - Inches(0.6)) / 2
for i, side in enumerate(("left", "right")):
col = spec.get(side, {})
x = MARGIN + i * (half + Inches(0.6))
if col.get("heading"):
d.text(s, x, Inches(2.4), half, Inches(0.6), col["heading"], 24, bold=True, color=d.accent)
d.bullets(s, x, Inches(3.1), half, col.get("bullets", []), size=20)
def layout_image(d, s, spec):
d.headline(s, spec.get("title", ""))
path = spec.get("image", "")
if not path or not os.path.isfile(path):
warnings.append(f"slide {d.current}: image not found: {path!r}")
return
box_w, box_h = W - 2 * MARGIN, Inches(4.2)
pic = s.shapes.add_picture(path, MARGIN, Inches(2.3))
ratio = min(box_w / pic.width, box_h / pic.height)
pic.width, pic.height = int(pic.width * ratio), int(pic.height * ratio)
pic.left = int((W - pic.width) / 2)
if spec.get("caption"):
d.text(s, MARGIN, Inches(6.65), W - 2 * MARGIN, Inches(0.5), spec["caption"], 14, color=d.muted, align=PP_ALIGN.CENTER)
def layout_table(d, s, spec):
d.headline(s, spec.get("title", ""))
header, rows = spec.get("header", []), spec.get("rows", [])
if len(rows) > 6 or len(header) > 5:
warnings.append(f"slide {d.current}: table is {len(rows)} rows x {len(header)} columns (max 6 x 5)")
if not header:
return
shape = s.shapes.add_table(len(rows) + 1, len(header), MARGIN, Inches(2.4), W - 2 * MARGIN, Inches(0.5) * (len(rows) + 1))
for c, label in enumerate(header):
cell = shape.table.cell(0, c)
cell.text = str(label)
cell.fill.solid()
cell.fill.fore_color.rgb = d.accent
cell.text_frame.paragraphs[0].runs[0].font.color.rgb = d.paper
cell.text_frame.paragraphs[0].runs[0].font.bold = True
for r, row in enumerate(rows, start=1):
for c, value in enumerate(row[: len(header)]):
shape.table.cell(r, c).text = str(value)
for row in shape.table.rows:
for cell in row.cells:
for p in cell.text_frame.paragraphs:
for run in p.runs:
run.font.size = Pt(18)
run.font.name = d.font
def layout_quote(d, s, spec):
d.text(s, Inches(1.4), Inches(2.0), W - Inches(2.8), Inches(3.0), "“" + spec.get("quote", "") + "”", 32, anchor=MSO_ANCHOR.MIDDLE)
if spec.get("by"):
d.text(s, Inches(1.4), Inches(5.2), W - Inches(2.8), Inches(0.6), "— " + spec["by"], 20, color=d.muted)
def layout_numbers(d, s, spec):
d.headline(s, spec.get("title", ""))
items = spec.get("items", [])[:4]
if len(spec.get("items", [])) > 4:
warnings.append(f"slide {d.current}: more than 4 figures; showing 4")
if not items:
return
gap = Inches(0.5)
each = (W - 2 * MARGIN - gap * (len(items) - 1)) / len(items)
for i, item in enumerate(items):
x = MARGIN + i * (each + gap)
d.text(s, x, Inches(2.8), each, Inches(1.4), item.get("value", ""), 54, bold=True, color=d.accent)
d.text(s, x, Inches(4.3), each, Inches(1.4), item.get("label", ""), 20, color=d.muted)
LAYOUTS = {"title": layout_title, "section": layout_section, "points": layout_points, "two-column": layout_two,
"image": layout_image, "table": layout_table, "quote": layout_quote, "numbers": layout_numbers}
def main():
if len(sys.argv) != 3:
sys.exit(__doc__)
spec_path, out_path = sys.argv[1], sys.argv[2]
with open(spec_path, encoding="utf-8") as f:
spec = json.load(f)
base = os.path.dirname(os.path.abspath(spec_path))
deck = Deck(spec.get("theme", {}))
for n, item in enumerate(spec.get("slides", []), start=1):
kind = item.get("layout", "points")
if kind not in LAYOUTS:
warnings.append(f"slide {n}: unknown layout {kind!r}, used points")
kind = "points"
if item.get("image") and not os.path.isabs(item["image"]):
item["image"] = os.path.join(base, item["image"])
slide = deck.slide(n)
LAYOUTS[kind](deck, slide, item)
deck.notes(slide, item.get("notes"))
deck.prs.save(out_path)
for line in warnings:
print("warning:", line)
print(f"wrote {out_path}: {len(deck.prs.slides)} slides, {len(warnings)} warning(s)")
if __name__ == "__main__":
main()Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

