AgentStack
SKILL verified MIT Self-run

Pptx Translator

skill-ericgandrade-claude-superskills-pptx-translator · by ericgandrade

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.

No reviews yet
0 installs
18 views
0.0% view→install

Install

$ agentstack add skill-ericgandrade-claude-superskills-pptx-translator

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Pptx Translator? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

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

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:

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:

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:

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.

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.