| name | narrating-pptx |
| description | Use when adding voice narration, voiceover, or TTS audio to a PowerPoint/pptx deck in ANY language (Hebrew, English, mixed) — making a deck self-presenting, generating per-slide speech with ElevenLabs, or when embedded pptx audio won't autoplay or triggers PowerPoint's "found a problem / Repair" dialog. Use whenever the user mentions narration, voiceover, audio in slides, TTS for a presentation, ElevenLabs + pptx, or a self-playing deck — even if they don't say "narration" explicitly. |
Narrating PPTX (ElevenLabs → per-slide autoplay)
Overview
Turn any pptx into a self-presenting deck: write presenter-style narration scripts (any language), generate speech via ElevenLabs eleven_v3, embed one clip per slide, and make each clip autoplay on slide entry.
The iron rule (paid for in blood): NEVER hand-write <p:timing> autoplay XML. Hand-rolled timing XML corrupts the file — PowerPoint shows "found a problem… Repair". The only reliable method is letting real PowerPoint author the XML itself via AppleScript play-settings (scripts/set_autoplay.sh). If you're tempted to inject timing XML "just this once" — that's the exact failure this skill exists to prevent.
Requirements: macOS + Microsoft PowerPoint installed (for the autoplay step), python-pptx, ELEVENLABS_API_KEY env var, an ElevenLabs voice ID from the user.
Pipeline (5 steps, in order)
1. Write the narration scripts → scripts.json
{"01": "text…", "02": "text…", …} — keys are 1-based slide positions, zero-padded.
Language: exactly what the user asks for. Hebrew, English, or mixed. For Hebrew (or any non-English), write like a native presenter in that language who naturally keeps technical terms in English (e.g., Israeli hi-tech register: "ה-agent מנתח את הדאטה וכותב manifest"). Don't translate terms the audience uses in English.
Depth: the voice IS the presenter, not a caption reader. Target ~600–900 chars per slide (≈45–75 s). Each script: open with context → walk the elements actually visible on the slide ("תסתכלו על המספרים מימין…" / "the green bar at the bottom…") → explain the why → bridge to the next slide. Short caption-style scripts (~200 chars) feel like labels, not a presentation — users reject them.
Audio tags = the "Enhance" feature. ElevenLabs' UI "Enhance" button just uses an LLM to add tags — there is no Enhance API. You are that LLM: weave 2–4 tags per script, sparingly: [warm] (openings/closings), [confident] (key claims), [pause] (before a pivot), [excited]. Tags stay in English brackets even inside Hebrew text.
Hard limit: 5,000 chars per script (eleven_v3 request cap).
2. Generate speech (parallel, 429-safe)
export ELEVENLABS_API_KEY=...
python3 scripts/generate_tts.py scripts.json VOICE_ID audio/ --concurrency 4
The script parallelizes (default 4 — typical plan concurrency is ~5; 16-at-once returns 429 on most), retries 429s with backoff, rejects suspiciously-small responses (a 429/error body saved as .mp3 is ~600 bytes), and exits non-zero on any failure. Verify durations before embedding: afinfo audio/slide01.mp3 | grep duration.
3. Embed one clip per slide
Existing pptx (the common case):
python3 scripts/add_audio.py deck.pptx audio/ narrated.pptx
Adds a small speaker icon bottom-right of each slide (click-to-play at this point — that's expected).
Deck you're building with pptxgenjs: add per slide instead:
slide.addMedia({ type: "audio", path: "audio/slide01.mp3", x: 9.38, y: 5.0, w: 0.5, h: 0.5 });
(pptxgenjs emits <a:videoFile> for audio — a known quirk; harmless, PowerPoint normalizes it during step 4.)
4. Autoplay — via real PowerPoint (the only safe way)
cp narrated.pptx ~/Downloads/
scripts/set_autoplay.sh "$HOME/Downloads/narrated.pptx" SLIDE_COUNT
Expect output autoplay set on N media shapes where N == number of narrated slides. The script targets the presentation by filename (never presentation 1 — PowerPoint's "reopen windows" resurrects stale decks that steal that index), waits for large files to open, errors on slide-count mismatch, and fails on N=0.
5. Validate — assume it's broken until proven
- Export through real PowerPoint (use the office-render skill if available): a successful PDF export of all slides == no repair dialog. LibreOffice validation is NOT sufficient — it tolerates XML that PowerPoint rejects.
- Re-check N from step 4 equals expected.
- The human must ear-test autoplay once: slideshow mode (⌘⇧↩), audio should start on slide entry. You cannot verify sound headlessly — say so; never claim you heard it.
ElevenLabs quick reference
| Item | Value |
|---|
| Endpoint | POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}?output_format=mp3_44100_128 |
| Auth | header xi-api-key (from env var; never echo) |
| Body | {"text", "model_id": "eleven_v3", "voice_settings": {"stability": 0.5, "similarity_boost": 0.75, "use_speaker_boost": true}} |
| Languages | 70+ incl. Hebrew; tags like [pause] inline |
| Limits | 5,000 chars/request · ~5 concurrent requests (429 = concurrency, not quota) |
| Stability | 0.5 = "natural" (use for narration); 0.0 creative / 1.0 robust |
Progress bar during playback
Elapsed/remaining time is hover-only — there is no persistent countdown for embedded audio (confirmed: standard Insert Audio cannot show controls without hovering). For the hover bar to work, THREE things must hold:
- Slide Show ribbon → "Show Media Controls" is checked (if unchecked, nothing appears on hover).
- The icon is big enough to hover — ≥0.5 in. A 0.28 in icon (~27 px) is an unusable hover target; users report "no progress bar" when the real issue is they can't hit the icon.
set_autoplay.sh sets hide while NOT playing, never hide during show (which removes the hover target entirely).
To resize icons on an already-narrated deck, use python-pptx geometry (safe — round-trips preserve the timing XML). Match media shapes by element XML, not shape_type == MEDIA (audio pics report as PICTURE):
for sh in slide.shapes:
if 'audioFile' in sh._element.xml or 'videoFile' in sh._element.xml:
sh.width = sh.height = Inches(0.5)
sh.left = prs.slide_width - Inches(0.62); sh.top = prs.slide_height - Inches(0.62)
Avoid AppleScript for geometry (its top/left position properties fight the compiler); AppleScript is only for play settings.
Common mistakes (each one happened in the baseline session)
| Mistake | Consequence | Fix |
|---|
Hand-writing <p:timing> autoplay XML | PowerPoint Repair dialog — corrupt deliverable | set_autoplay.sh (PowerPoint authors it) |
AppleScript presentation 1 | Edits a stale reopened deck; wrong file saved | Target presentation "name.pptx"; verify slide count |
| Firing all TTS requests at once | HTTP 429 on most; error JSON saved as .mp3 | --concurrency 4 + size-integrity check |
| Caption-length scripts (~200 chars) | "It should explain more — it's the presenter" | 600–900 chars, presenter structure (step 1) |
| Looking for an "Enhance" API | Doesn't exist (UI-only LLM feature) | Author the audio tags yourself |
for i in $var in zsh | No word splitting — loop gets one token | ${=var} in zsh, or use Python |
Fixed delay 2 after opening big pptx | "object does not exist" AppleScript error | Wait-loop until slide count matches (in script) |
| Validating with LibreOffice only | Misses PowerPoint-strict corruption | Export via real PowerPoint |
| Icon ≤0.3 in | User can't hover → "no progress bar" complaints | 0.5 in icon, bottom-right (default in add_audio.py) |
shape_type == MEDIA in python-pptx | Finds 0 audio shapes (they report as PICTURE) | Match 'audioFile' in sh._element.xml |
| AppleScript for shape geometry | top/left position compile errors | python-pptx for geometry; AppleScript only for play settings |
Caveats
- Autoplay survives PowerPoint (desktop/365). Google Slides import and LibreOffice are unreliable with embedded audio autoplay; PDF export drops audio entirely.
- File grows ~0.3–1.5 MB per narrated minute (mp3 128kbps).
- Keep a clean (non-narrated) copy — narration is a variant, not a replacement.
- Deck edits after narration are fine; re-run step 4 only if you re-add media.