# Pptx Translator

> This skill should be used when the user needs to translate PowerPoint presentations (.pptx) between languages, preserving formatting and layout. Supports parallel slide-by-slide translation with per-slide validation and retry. Default languages are Portuguese and English but any language pair is supported.

- **Type:** Skill
- **Install:** `agentstack add skill-ericgandrade-claude-superskills-pptx-translator`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ericgandrade](https://agentstack.voostack.com/s/ericgandrade)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ericgandrade](https://github.com/ericgandrade)
- **Source:** https://github.com/ericgandrade/claude-superskills/tree/main/skills/pptx-translator

## Install

```sh
agentstack add skill-ericgandrade-claude-superskills-pptx-translator
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# pptx-translator

## Purpose

This skill translates PowerPoint presentations (.pptx) between any two languages while preserving the original formatting, layout, fonts, and structure. It uses parallel sub-agent translation (one agent per slide), per-slide validation with automatic retry, recursive group-shape traversal, and a single fast write-back pass.

## When to Use This Skill

- User provides a .pptx file and wants it translated to another language
- User needs to translate a PowerPoint from Portuguese to English or vice versa
- User needs to translate slides between any language pair (Spanish, French, German, Japanese, etc.)
- User wants to translate speaker/presenter notes alongside slide content
- User asks to "translate", "traduzir", or "convert language" of a PowerPoint or .pptx file

## Step 0: Dependency Setup (Silent)

Check dependencies silently. Do NOT ask for confirmation — install automatically with `--user` and inform the user only of what was installed.

```bash
python3 -c "import pptx" 2>/dev/null || pip install --user python-pptx -q && echo "Installed python-pptx"
```

If installation fails (e.g. no pip, restricted environment), ask the user once: "python-pptx is required. Preferred install method: (a) pip install --user, (b) existing venv path, (c) manual?"

## Step 1: Single Consolidated Confirmation

Infer all parameters from the user's request. Ask only for what is truly ambiguous. Then display **one** confirmation box and proceed immediately after the user confirms. Do NOT ask again.

Parameters to infer or confirm:
1. **Source file path** — validate it exists and has `.pptx` extension
2. **Source language** — infer from user's request (e.g. "translate from Portuguese"); if not stated, the AI classifier sub-agent (Step 2) will determine it automatically
3. **Target language** — infer from request; ask if not specified
4. **Backup mode** — default Safe (output saved as new file); YOLO only if user explicitly says so
5. **Speaker notes** — default Yes (translate alongside slides)
6. **Output filename** — resolved interactively in Step 1.5 (after this confirmation box); default is `{stem}_{target_lang_code}.pptx`

```
╔══════════════════════════════════════════════════════════════╗
║  PPTX TRANSLATOR — Configuration                            ║
╠══════════════════════════════════════════════════════════════╣
║  File:            ~/docs/proposta.pptx                      ║
║  Direction:       Portuguese → English                      ║
║  Speaker notes:   Yes (included)                            ║
║  Original:        preserved (output saved as new file)      ║
║  Output file:     ~/docs/proposta_en.pptx                   ║
║  ⚠️  YOLO: original will be overwritten + backup created    ║
╚══════════════════════════════════════════════════════════════╝
Proceed? [Y/n]
```

After this single confirmation, proceed to Step 1.5 without further interruptions.

## Step 1.5: Output Filename Choice

Immediately after the Step 1 confirmation, show this mini-prompt.
All variables are derived from the **target language** resolved in Step 1 — never hardcoded to English.

Variables:
- `{stem}` — original filename without extension (e.g. `proposta_comercial`)
- `{lang_code}` — ISO 639-1 code of the **target** language (e.g. `pt`, `fr`, `de`, `es`, `en`, `ja`)
- `{target_language}` — human-readable target language name (e.g. `Portuguese`, `French`, `German`)

```
How should the output file be named?

  [1] {stem}_{lang_code}.pptx              ← original name + target language suffix (default)
  [2] {translated_stem}_{lang_code}.pptx   ← filename translated to {target_language} (AI suggestion)
  [3] Custom name                           ← I'll type it myself

Choice [1]:
```

**Option 1 (default):** If user presses Enter or types `1`, set `output_filename = {stem}_{lang_code}.pptx`.

**Option 2 — AI translates the filename stem to the target language:**
- Run an inline AI call with this exact prompt:
  `"Translate only this filename stem to {target_language}: '{stem}'. Return ONLY the translated stem, lowercase, spaces replaced with underscores, no punctuation, no explanation."`
- Show the suggestion before proceeding:
  `Suggested: {translated_stem}_{lang_code}.pptx — use this? [Y/n]`
- If confirmed → `output_filename = {translated_stem}_{lang_code}.pptx`
- If rejected → fall back silently to Option 1

**Option 3 — Custom name:**
- Prompt: `Enter filename (with or without .pptx):`
- Normalize: strip any leading path separators, ensure `.pptx` extension, replace spaces with `_`
- Confirm: `Output will be saved as: {custom_name} — confirm? [Y/n]`
- If confirmed → `output_filename = {custom_name}`
- If rejected → re-show the 3-option menu

**Examples by language pair:**

| Original file | Direction | Option 1 | Option 2 (AI) |
|---------------|-----------|----------|----------------|
| `proposta_comercial.pptx` | PT→EN | `proposta_comercial_en.pptx` | `commercial_proposal_en.pptx` |
| `proposta_comercial.pptx` | PT→FR | `proposta_comercial_fr.pptx` | `proposition_commerciale_fr.pptx` |
| `proposta_comercial.pptx` | PT→DE | `proposta_comercial_de.pptx` | `geschaeftsvorschlag_de.pptx` |
| `quarterly_review.pptx`   | EN→PT | `quarterly_review_pt.pptx`  | `revisao_trimestral_pt.pptx` |
| `marketing_plan.pptx`     | EN→ES | `marketing_plan_es.pptx`    | `plan_de_marketing_es.pptx` |

Store the resolved name as `output_filename`. Use it in Steps 4 and 5 as the output path.

## Step 2: Extract & Analyze Slide Content

```
[████░░░░░░░░░░░░░░░░] 20% — Step 2/5: Extracting slide content
```

Use `python-pptx` to extract all translatable content. **CRITICAL: Use a recursive `iter_shapes()` function that descends into GROUP shapes** — do not use `slide.shapes` directly, as it misses text nested inside groups.

### Extraction Script

```python
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
import json, sys

def iter_shapes(shapes, parent_id=None):
    """Recursively yield all leaf shapes, descending into GROUP shapes."""
    for shape in shapes:
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            yield from iter_shapes(shape.shapes, parent_id=shape.shape_id)
        else:
            yield shape, parent_id

def extract_text(pptx_path):
    prs = Presentation(pptx_path)
    manifest = []

    for slide_num, slide in enumerate(prs.slides, start=1):
        text_blocks = []

        for shape, parent_id in iter_shapes(slide.shapes):
            # Text frames in regular shapes (including children of groups)
            if shape.has_text_frame:
                for para_idx, para in enumerate(shape.text_frame.paragraphs):
                    for run_idx, run in enumerate(para.runs):
                        if run.text.strip():
                            text_blocks.append({
                                "shape_id": shape.shape_id,
                                "parent_id": parent_id,   # None if top-level
                                "shape_name": shape.name,
                                "para_idx": para_idx,
                                "run_idx": run_idx,
                                "original_text": run.text
                            })

            # Tables
            if shape.has_table:
                for row_idx, row in enumerate(shape.table.rows):
                    for col_idx, cell in enumerate(row.cells):
                        if cell.text.strip():
                            text_blocks.append({
                                "shape_id": shape.shape_id,
                                "parent_id": parent_id,
                                "shape_name": f"table_{shape.name}",
                                "row_idx": row_idx,
                                "col_idx": col_idx,
                                "original_text": cell.text
                            })

        # Speaker notes
        notes_text = ""
        if slide.has_notes_slide:
            notes_tf = slide.notes_slide.notes_text_frame
            notes_text = notes_tf.text.strip() if notes_tf else ""

        # SmartArt detection — flag slides with embedded diagrams
        smartart_found = []
        for shape in slide.shapes:
            if shape.shape_type is None:  # graphicFrame (SmartArt/Chart)
                smartart_found.append(shape.name)

        manifest.append({
            "slide_num": slide_num,
            "text_blocks": text_blocks,
            "notes": notes_text,
            "smartart_shapes": smartart_found  # non-empty = has SmartArt
        })

    return manifest

manifest = extract_text(sys.argv[1])
print(json.dumps(manifest, ensure_ascii=False, indent=2))
```

Save manifest to `/tmp/pptx_manifest_{timestamp}.json`. **Serialize with `json.dumps(..., ensure_ascii=False)` — no `indent` parameter** to minimize payload size sent to sub-agents.

After extraction, report SmartArt slides and translate their content via zip:

```python
import zipfile, os, json
from lxml import etree

smartart_slides = [s for s in manifest if s.get("smartart_shapes")]
if smartart_slides:
    print(f"\n⚠️  {len(smartart_slides)} slide(s) contain SmartArt — text stored in separate XML files:")
    for s in smartart_slides:
        print(f"   Slide {s['slide_num']}: {s['smartart_shapes']}")
    print("   SmartArt text will be translated via direct XML editing after write-back.")

    DRAWING_NS = 'http://schemas.openxmlformats.org/drawingml/2006/main'

    with zipfile.ZipFile(output_path, 'r') as zin:
        diagram_files = [f for f in zin.namelist()
                         if f.startswith('ppt/diagrams/data') and f.endswith('.xml')]
        file_roots = {}
        smartart_items = []
        for df in diagram_files:
            root = etree.fromstring(zin.read(df))
            file_roots[df] = root
            for i, t in enumerate(root.findall(f'.//{{{DRAWING_NS}}}t')):
                if t.text and t.text.strip():
                    smartart_items.append({"file": df, "node_idx": i, "text": t.text})

    if smartart_items:
        smartart_input = json.dumps(
            [{"i": idx, "t": item["text"]} for idx, item in enumerate(smartart_items)],
            ensure_ascii=False
        )
        with open("/tmp/smartart_input.json", "w") as f:
            f.write(smartart_input)
        print(f"   {len(smartart_items)} SmartArt text nodes — launching SmartArtTranslator agent…")

        # Launch SmartArtTranslator sub-agent (same prompt pattern as SlideTranslator):
        # Input: /tmp/smartart_input.json — list of {"i": idx, "t": "source text"}
        # Output: /tmp/smartart_output.json — list of {"i": idx, "t": "translated text"}
        # Apply same STRICT RULES (preserve proper nouns, whitespace, completeness)

        with open("/tmp/smartart_output.json") as f:
            smartart_translations = {item["i"]: item["t"] for item in json.load(f)}

        for df, root in file_roots.items():
            nodes = root.findall(f'.//{{{DRAWING_NS}}}t')
            for idx, item in enumerate(smartart_items):
                if item["file"] == df and idx in smartart_translations:
                    nodes[item["node_idx"]].text = smartart_translations[idx]

        tmp_smartart = output_path + ".sa.tmp"
        with zipfile.ZipFile(output_path, 'r') as zin:
            with zipfile.ZipFile(tmp_smartart, 'w', zipfile.ZIP_DEFLATED) as zout:
                for zi in zin.infolist():
                    if zi.filename in file_roots:
                        data = etree.tostring(file_roots[zi.filename],
                                              xml_declaration=True, encoding='UTF-8', standalone=True)
                    else:
                        data = zin.read(zi.filename)
                    zout.writestr(zi, data)
        os.replace(tmp_smartart, output_path)
        print(f"   ✅ SmartArt translations applied to {len(diagram_files)} diagram file(s)")
```

### After Extraction: AI-Powered Language Classification

**Do NOT use regex or `langdetect` to decide which slides to translate.** Both approaches require hardcoded rules and fail silently on valid words with no accents (e.g. "nossa jornada", "cenário", "nosso time"). Regex lists are never complete; `langdetect` misclassifies short or mixed-language text.

**The model itself already understands language natively** — use it. After extraction, launch a **single classification sub-agent** that receives all slide texts and returns a JSON decision for each slide. No libraries. No hardcoded words. No false negatives.

### Classification Sub-Agent Prompt

> **Note on large presentations:** If the presentation has more than 50 slides, split the input into batches of 50 and launch one classifier agent per batch — large payloads can exceed model context limits. Merge all results before proceeding.

```
# SlideClassifier — Language Detection Agent

You are a language classifier. For each slide below, determine whether its text is written (fully or partially) in {SOURCE_LANGUAGE}.

Rules:
- A slide needs translation if ANY text block contains {SOURCE_LANGUAGE} content — even a single sentence or title
- A slide does NOT need translation only if ALL its text is already in {TARGET_LANGUAGE} or is language-neutral (numbers, dates, proper nouns, acronyms, code)
- Proper nouns, brand names, product names, and technical terms are language-neutral — do not use them as evidence of either language
- When in doubt, mark needs_translation: true — it is safer to translate an already-English slide than to skip a source-language one

Slides to classify:
{SLIDE_TEXTS_JSON}

Save your result to /tmp/pptx_classify_output.json as a JSON array (no explanation, no markdown fences):
[
  {"slide_num": 1, "needs_translation": true, "language": "pt", "reason": "Title contains Portuguese: 'A nova era das empresas'"},
  {"slide_num": 2, "needs_translation": false, "language": "en", "reason": "All text is in English"},
  ...
]

After saving, print: "✅ Classification complete: {N} slides need translation, {M} already in target language"
```

Where `{SLIDE_TEXTS_JSON}` is built from the manifest:

```python
import json

slide_texts = []
for slide in manifest:
    combined = " | ".join(
        b["original_text"] for b in slide["text_blocks"] if b["original_text"].strip()
    )
    if combined.strip():
        slide_texts.append({"slide_num": slide["slide_num"], "text": combined})
    else:
        # No text content — images/charts only, skip
        slide["needs_translation"] = False

# Save for the classifier agent
with open("/tmp/pptx_classify_input.json", "w") as f:
    json.dump(slide_texts, f, ensure_ascii=False, indent=2)
```

The classifier agent saves its result to `/tmp/pptx_classify_output.json`. The classifier prompt must explicitly instruct the agent to save output there — do NOT rely on the agent inferring it. After it completes, apply the decisions with a fallback for parse failures:

```python
import json, os

try:
    with open("/tmp/pptx_classify_output.json") as f:
        decisions = {d["slide_num"]: d for d in json.load(f)}
except (FileNotFoundError, json.JSONDecodeError, KeyError):
    # Fallback: if classifier failed or returned invalid JSON, translate all slides with text
    print("⚠️  Classifier output invalid — falling back to translate-all-with-text strategy")
    decisions = {}
    for slide in manifest:
        if slide["text_blocks"]:
            slide["needs_translation"] = True
            slide["detected_language"] = "unknown"
        else:
            slide["needs_translation"] = False

for slide in manifest:
    decision = decisions.get(slide["slide_num"])
    if decision:
        slide["needs_translation"] = decision["needs_translation"]
        slide["detected_language"] = decision.get("language", "unknown")
        slide["detection_reason"] = decision.get("reason", "")
    # slides with no text content were already set to False above
```

This approach requires **zero hardcoded patterns**, works for any language

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ericgandrade](https://github.com/ericgandrade)
- **Source:** [ericgandrade/claude-superskills](https://github.com/ericgandrade/claude-superskills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ericgandrade-claude-superskills-pptx-translator
- Seller: https://agentstack.voostack.com/s/ericgandrade
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
