Create or edit PowerPoint presentations. Dual-mode skill: (1) Editing mode preserves existing templates via Open XML unpack/edit/repack when an existing .pptx is provided. (2) Generation mode creates new Metis-branded decks from a design system with 36 composable components and 5 layout grids. Includes brand extraction for client decks and visual QA via PowerPoint COM. Triggers on deck, slides, presentation, PPT, or any .pptx request.
93
93%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Before doing anything else, determine which workflow to use.
Does an existing .pptx file need to be preserved?
| Condition | Mode |
|---|---|
| User provides, attaches, or references an existing .pptx file (by path, filename, or description) | Editing → Section 1 |
| No existing file; user wants a new Metis-branded deck | Generation → Section 2 |
Examples:
The test is simple: does a .pptx file exist that we need to preserve? If yes, use Editing. If no, use Generation.
Use this workflow when an existing .pptx file is provided or referenced. This workflow preserves the template's layouts, themes, formatting, and brand identity.
Python is installed but NOT on the git bash PATH. Use the full path:
PYTHON="C:/Users/$USER/AppData/Local/Programs/Python/Python312/python.exe"Required packages:
python-pptx,defusedxml,lxml,Pillow,pywin32,markitdownDo NOT loop trying to install Python or add it to PATH. It is already installed.
All script paths below are relative to this skill's directory. Locate it with:
SKILL_DIRS=(
"$HOME/.metis/skills/.agents/skills/tessl__metis-pptx"
"$HOME/.metis/skills/.tessl/tiles/metis-strategy/metis-pptx"
".tessl/tiles/metis-strategy/metis-pptx"
)
for d in "${SKILL_DIRS[@]}"; do
if [ -d "$d/scripts" ]; then SKILL_DIR="$d"; break; fi
doneBefore making any changes, understand the deck:
# Visual overview — exports each slide as a PNG image
"$PYTHON" "$SKILL_DIR/scripts/visual_qa.py" input.pptx slide_images/
# Text content extraction
"$PYTHON" -m markitdown input.pptxReview the exported slide images and extracted text to understand:
After analyzing the deck, classify every slide into one of three categories before making any changes:
| Category | When to use | Action |
|---|---|---|
| Keep & Edit | The slide's visual layout matches the new content structure (same number of items, same hierarchy, similar text lengths) | Edit text in place via XML |
| Duplicate & Rebuild | The slide's layout/grid is right but the content structure differs significantly (different number of items, different hierarchy, much longer/shorter text) | Duplicate the slide, strip content, rebuild using the layout's placeholders and shapes as a skeleton |
| New Slide | No existing slide has a layout that fits the new content, OR the source slide's visual structure is fundamentally incompatible (e.g., a manufacturing Gantt chart being reused for a strategic capability overview) | Create a new slide from a layout using add_slide.py with a slideLayout, then build content using patterns from references/patterns.md with extracted brand tokens |
A slide is NOT reusable when:
A slide IS reusable when:
Write out the triage as a table in your plan before starting any edits:
| Slide # | Current Content | New Content | Category | Rationale |
|---------|----------------|-------------|----------|-----------|
| 1 | PSG Title | CTD Title | Keep & Edit | Same layout, just text swap |
| 9 | Data & AI Timeline | Labeling Transformation | New Slide | Timeline axis doesn't apply; empty boxes |
| 15 | PSG Gantt Roadmap | CTD Roadmap | New Slide | Granular Gantt doesn't fit higher-level view |If the deck is NOT Metis-branded, extract the client's brand tokens:
"$PYTHON" "$SKILL_DIR/scripts/extract_brand.py" input.pptxThis outputs a JSON dict with:
Use these tokens when creating new slides so they match the client's style.
Extract the .pptx for editing:
"$PYTHON" "$SKILL_DIR/scripts/unpack.py" input.pptx unpacked/This extracts all XML files, pretty-prints them, and escapes smart quotes.
Complete ALL structural changes before editing content.
Duplicate a slide:
"$PYTHON" "$SKILL_DIR/scripts/add_slide.py" unpacked/ slide2.xml
# Prints: Created slide5.xml from slide2.xml
# Prints: Add to presentation.xml <p:sldIdLst>: <p:sldId id="259" r:id="rId8"/>Create a slide from a layout:
# See available layouts:
ls unpacked/ppt/slideLayouts/
"$PYTHON" "$SKILL_DIR/scripts/add_slide.py" unpacked/ slideLayout2.xmlDelete a slide: Remove its <p:sldId> from <p:sldIdLst> in unpacked/ppt/presentation.xml.
Reorder slides: Rearrange <p:sldId> elements in <p:sldIdLst>.
After adding slides, update presentation.xml by inserting the printed <p:sldId> element at the desired position in <p:sldIdLst>.
Deleting slides requires cleanup beyond just removing the <p:sldId> entry. Follow this sequence:
<p:sldId> element from <p:sldIdLst> in presentation.xmlppt/slides/slideN.xml)ppt/slides/_rels/slideN.xml.rels)ppt/_rels/presentation.xml.rels (the <Relationship> with the matching rId)[Content_Types].xml (the <Override> for /ppt/slides/slideN.xml)clean.py to catch any remaining orphaned media, notes, or referencesNever delete slides using python-pptx's internal API (drop_rel(), sldIdLst.remove(), etc.) — this leaves orphaned references that corrupt the file and make it unopenable in PowerPoint. Always delete via the unpacked XML approach above, then run clean.py before pack.py.
If you need to remove many slides and the XML approach is impractical, use PowerPoint COM to delete properly:
ppt = win32com.client.Dispatch('PowerPoint.Application')
pres = ppt.Presentations.Open(path, WithWindow=False)
for idx in sorted(indices_to_delete, reverse=True):
pres.Slides(idx).Delete()
pres.Save()
pres.Close()
ppt.Quit()COM handles all internal cleanup (relationships, content types, notes) automatically.
For each slide classified as "Keep & Edit" in triage (Section 1.2.1):
unpacked/ppt/slides/slide{N}.xmlUse the Edit tool, not sed or Python scripts. The Edit tool forces specificity.
This is NOT a find-and-replace exercise. You are authoring new content that happens to live in an existing visual container. The distinction matters:
For every text shape you edit:
When replacing text in XML, preserve the original formatting by keeping the <a:rPr> (run properties) element intact and only changing the <a:t> (text) element. If a shape has multiple runs with different formatting (e.g., a bold red label followed by regular black description), maintain that run structure.
Common formatting failures to avoid:
<a:buChar> or <a:buAutoNum> on each <a:pPr>When the new content has different structure than the old (e.g., 3 bullets to 5 bullets), you must add or remove <a:p> elements. Copy the <a:pPr> (paragraph properties) from an existing paragraph to maintain consistent bullet style, indentation, and spacing.
When you need to create new slides that match the client's style:
references/patterns.md to pick the right grid pattern and component patternsFor Metis-branded decks being edited, you may reference references/metis-brand.md for the code recipes.
Most real-world editing tasks are hybrid: some slides are edited in place, others need to be built from scratch. This is expected and correct — do not force every slide through the same workflow.
After completing triage (Section 1.2.1), you will typically have:
For New Slides in a client deck:
"$PYTHON" "$SKILL_DIR/scripts/add_slide.py" unpacked/ slideLayoutN.xmlreferences/patterns.md<p:sldId> at the correct position in presentation.xmlKey principle: A deck that mixes edited originals with well-crafted new slides (using the same brand tokens) will look more cohesive than a deck where every slide is force-fit via text replacement into layouts that don't match the content.
When building new slides for a client deck, adapt the python-pptx component recipes from references/metis-brand.md by substituting the client's brand tokens for the Metis constants:
DARK_NAVY with the client's primary_fill colorCalibri with the client's primary fontcontent_area boundariesb="1" on <a:rPr><a:buChar> or <a:buAutoNum><a:p> elements for each item — never concatenate into one string“”‘’xml:space="preserve" on <a:t> with leading/trailing spacesBefore committing a text replacement, compare the character count of old vs. new content:
| New vs. Old Length | Risk | Action |
|---|---|---|
| Within ~30% | Low | Replace and verify in QA |
| 30-60% longer | Medium | Replace, but check the shape has wordWrap and adequate height. Consider abbreviating. |
| >60% longer | High | Do NOT force-fit. Either abbreviate the content, or classify this slide as "New Slide" in triage and rebuild with a layout that fits. |
| Significantly shorter | Low | Replace, but remove any excess visual elements (extra bullet markers, connector lines) that now point at empty space |
Never rely on PowerPoint's auto-shrink (<a:bodyPr fontScale="...">) to handle overflow. If the text doesn't fit at the original font size, the slide needs redesign, not font shrinking.
"$PYTHON" "$SKILL_DIR/scripts/clean.py" unpacked/
"$PYTHON" "$SKILL_DIR/scripts/pack.py" unpacked/ output.pptx --original input.pptxclean.py removes orphaned slides, media, and relationships.
pack.py validates, condenses XML, and repacks with auto-repair.
"$PYTHON" "$SKILL_DIR/scripts/visual_qa.py" output.pptx qa_images/See Section 3 for the full QA process.
Use this workflow when building a new Metis-branded deck from scratch.
| Asset | Path |
|---|---|
| Master template (blank deck with slide masters) | G:\Shared drives\Knowledge Management\New Brand Assets\PowerPoint Templates\master.pptx |
| Firm overview slides (8 slides) | G:\Shared drives\Knowledge Management\New Brand Assets\PowerPoint Templates\intro-slides.pptx |
| Icon Library | G:\Shared drives\Knowledge Management\New Brand Assets\PowerPoint Templates\Icon_Lib.pptx |
| Logo — White | G:\Shared drives\Knowledge Management\New Brand Assets\Metis Strategy Logo\Metis Strategy White RGB logo.png |
| Logo — Black-Mint | G:\Shared drives\Knowledge Management\New Brand Assets\Metis Strategy Logo\Metis Strategy Black-Mint RGB Logo.png |
Python is installed but NOT on the git bash PATH. Use the full path:
PYTHON="C:/Users/$USER/AppData/Local/Programs/Python/Python312/python.exe" "$PYTHON" build_deck.pyRequired package:
python-pptx— if missing:"$PYTHON" -m pip install python-pptxDo NOT loop trying to install Python or add it to PATH. It is already installed.
Read references/patterns.md for which component patterns to use (brand-agnostic guidance).
Read references/metis-brand.md for the exact Metis python-pptx code recipes.
Key brand constants:
#20206E, Metis Blue #256BA2, Mint #3BDAC0, Dark Gray #4A4A4A, Light Gray #F2F2F2Read references/patterns.md for the grid selection guide — which grid suits your content.
Read references/metis-brand.md for exact Metis coordinates and code for each grid.
| Grid | Best for |
|---|---|
| 1. Full Width | Executive summaries, narratives, key findings |
| 2. Two Column | Comparisons, before/after, pros/cons |
| 3. Sidebar + Main | KPI callout + explanation |
| 4. Three Column | Phases, pillars, options |
| 5. Title + Visual | Process flows, org charts, tables |
Before writing any code, create a slide-by-slide outline:
Slide 1: Cover (fixed) — "Client Name — Project Title"
Slide 2: Agenda (content, Grid 1) — 4 agenda items
Slide 3-12: Firm Overview (fixed, optional) — ask user if needed
Slide 13: Section Divider (fixed) — "Our Approach"
Slide 14: Three-Phase Process (content, Grid 4) — Discovery, Design, Deliver
...For each slide, decide:
references/patterns.md component selection guide.references/patterns.md. Use the Component Rotation Guide (in patterns.md) to ensure variety — never assign the same primary component to consecutive slides.Visual Variety Rule — apply during planning:
Component column showing the assigned component number(s) for each content slide:| # | Title | Type | Grid | Component(s) |
|---|------------------------------|---------|------|------------------|
| 1 | Cover | fixed | — | — |
| 2 | Agenda | content | 1 | #7 Callout |
| 3 | The Opportunity | content | 1 | #36 Pull Quote |
| 4 | Market Landscape | content | 4 | #9 Cards+Header |
| 5 | Our Point of View | content | 2 | #10 Benefits |
| 6 | Transformation Roadmap | content | 5 | #22 Process |
| 7 | Key Capabilities | content | 4 | #11 Category Grid|
| 8 | Closing / Call to Action | content | 1 | #36 Pull Quote |Firm overview is optional. Ask the user: "Should I include the Metis firm overview slides?"
Create a single Python script (build_deck.py). Critical rules:
master.pptx — never open intro-slides.pptx directly as the base.'1_Title Only' layout for content slides — gives green arrow, logo, footer, page number.'Blank' for content slides — it has none of the brand elements.'Section Divider' layout for dividers — navy background is built in.add_textbox().intro-slides.pptx using the copy_slide_from() helper.See references/metis-brand.md for the full boilerplate code including:
get_layout() helperadd_content_slide() helperadd_section_divider() helpercopy_slide_from() helpersave_clean() helperFor each content slide:
slide = add_content_slide("Title", "Subtitle")references/patterns.md — every content slide MUST use at least one component. Plain text on white is never acceptable.references/metis-brand.md and call the component functions.Every text element must have real content. No placeholder text, no "Lorem ipsum."
Before saving, verify:
Proposals / BD Decks:
In-Project Deliverables:
Bullet list with accent markers:
bullets = ["Finding one", "Finding two", "Finding three"]
y = 1.10
for b in bullets:
marker = slide.shapes.add_shape(MSO_SHAPE.OVAL, Inches(0.50), Inches(y + 0.05), Inches(0.12), Inches(0.12))
marker.fill.solid()
marker.fill.fore_color.rgb = MINT
marker.line.fill.background()
add_body(slide, b, 0.75, y, 12.08, 0.35)
y += 0.45Highlighted callout box:
box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(0.50), Inches(y), Inches(12.33), Inches(1.50))
box.fill.solid()
box.fill.fore_color.rgb = LIGHT_GRAY
box.line.fill.background()
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.50), Inches(y), Inches(0.06), Inches(1.50))
bar.fill.solid()
bar.fill.fore_color.rgb = MINT
bar.line.fill.background()Section divider: Use the 'Section Divider' layout — never draw navy rectangles manually.
divider = prs.slides.add_slide(get_layout('Section Divider'))
for ph in divider.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = "Section Title"Assume there are problems. Your job is to find them.
"$PYTHON" "$SKILL_DIR/scripts/visual_qa.py" output.pptx qa_images/This uses PowerPoint COM automation to render each slide as a high-fidelity PNG.
"$PYTHON" "$SKILL_DIR/scripts/thumbnail.py" output.pptxCreates a grid overview of all slides for quick visual inspection.
"$PYTHON" -m markitdown output.pptxCheck for missing content, typos, wrong order.
Check for leftover placeholder text:
"$PYTHON" -m markitdown output.pptx | grep -iE "\bx{3,}\b|lorem|ipsum|\bTODO|\[insert"When inspecting slide images, look for:
Do not declare success until you've completed at least one fix-and-verify cycle.