---
name: doc-parse
description: |
  Document extraction skill — turn a `.pptx` into structured data through the ab-render `/extract` endpoint.

  Two layers, picked with `--fields`:
  - **content** — per-page `title`, `text[]`, and `rawNotes` (the speaker notes). Small, no base64. This is the narration source material.
  - **layout** — the full `slide` payload the `ppt-to-video` template renders from: every shape, coordinate, color, clip path and the animation `beats`, plus the `mode` verdict. Large (icons are embedded as data URIs).

  Deterministic: the endpoint parses OOXML, it does **not** call a model. Notes come back verbatim — rewriting them into narration is your job, not the endpoint's.

  Use this skill immediately whenever the user asks for any of:
  - Turn this PPT / slide deck into a video
  - Read / extract / summarize a .pptx, pull the text or the speaker notes out of a deck
  - Use a PowerPoint file as source material for a script
  - Anything that needs `customPayload.slide` for the `ppt-to-video` template

  Never hand-write `slide` nodes for `ppt-to-video`. A page has dozens of shapes with exact EMU coordinates; transcribing them by eye is guaranteed to drift from the original, and the template's own hint says so.
triggers:
  - Turn this PPT / slide deck into a video
  - Extract text or speaker notes from a .pptx
  - Use a PowerPoint file as source material
  - Get customPayload.slide for ppt-to-video
---

# Document Extraction Skill (`doc_parse`)

Entry script **`scripts/extract_via_render.py`**, pure Python (stdlib only — no Office libraries, no local install).

> Path convention: read the system-injected `Base directory for this skill: <path>` as `<SkillDir>`. Never hardcode an absolute path.

## How it works

```
POST $RENDER_API_URL/extract        → { taskId }
POST $RENDER_API_URL/extractStatus  → { status, result? }   (polled for you)
```

The heavy lifting (download, unzip, OOXML parsing, geometry) happens server-side. The script submits, polls, then **writes the full JSON to a file and prints a per-page summary**.

That default matters: one page of `layout` with embedded icons can be hundreds of KB. Printing it would fill your context with base64 and bury the three facts you actually need — the page's mode, its beat count, and whether it has speaker notes.

## Environment

| Var | Meaning | Default |
| --- | --- | --- |
| `RENDER_API_URL` | ab-render base URL (falls back to `REMOTION_RENDER_API_URL`) | `https://api-render.remixmate.ai` |
| `PRIV_TOKEN` | ab-api private token — **required**, the endpoint refuses anonymous calls | — |
| `CONVERSATION_ID` | Conversation id, for provenance | — |

## Usage

Whole deck, both layers (what `ppt-to-video` needs):

```bash
python3 <SkillDir>/scripts/extract_via_render.py --url "https://oss.example.com/deck.pptx"
```

Text and notes only — summarizing a deck, or writing narration without touching the visuals:

```bash
python3 <SkillDir>/scripts/extract_via_render.py --url "https://…/deck.pptx" --fields content
```

One page:

```bash
python3 <SkillDir>/scripts/extract_via_render.py --url "https://…/deck.pptx" --slide 3
```

## Output

```jsonc
{
  "source": { "url": "…", "name": "deck.pptx", "slideCount": 12, "canvas": [1920, 1080], "fields": ["content","layout"] },
  "slides": [
    {
      "slideIndex": 1,
      // content 层
      "title": "产品介绍",
      "text": ["三大优势", "…"],
      "rawNotes": "备注页原文，逐字返回，未经改写",
      // layout 层
      "mode": "sequenced",
      "modeReason": "4×1 格点，4 块 + 2 组页面家具",
      "nodes": [ /* … */ ],
      "beats": [ { "reveal": ["n12","n13"] } ],
      "ambientIds": ["n2"],
      "recommendedSec": 5.3,
      "background": { "color": "#FFFFFF" },
      "flags": ["unsupported_geom:rightArrow×3"]
    }
  ]
}
```

## Using the result

**For the picture** — feed `slides[i]` straight into the template's `customPayload.slide`. Do not edit the coordinates, and do not raise `mode`: the verdict comes from the extractor, which can see reconstruction problems you cannot (`modeReason` says why). You may lower it. Raising it requires `trustMeMode: true` and means you have personally checked that page.

**For the words** — `rawNotes` and `text` are two different kinds of raw material, and they need different treatment:

| Source | What it is | What to do | Can you check yourself? |
| --- | --- | --- | --- |
| `rawNotes` | already a script, written to be spoken | re-cut into one sentence per beat, make it spoken-register, drop stage directions (“as you can see here”, “(next slide)”) and non-script items (citations, TODOs) | **yes** — the original is right there |
| `text` | bullet points, sometimes just keywords | expand into sentences | **no** — there is nothing to check against |

Two rules:

1. **Do not introduce facts that are not on the slide.** This is the user's own deck; inventing content puts words in their mouth.
2. **Talk about it, don't read it out.** The body text is already on screen. Narrating it verbatim gives the viewer the same sentence three times — on the slide, in the subtitle, and in the voice-over.

When a page had no notes and you wrote the narration from `text`, say so and ask the user to look it over. Do not ship it silently.

## Failure modes

Errors come back as plain sentences; pass them to the user as-is rather than retrying:

- a binary `.ppt` (PowerPoint 97-2003) — it must be re-saved as `.pptx`, the formats are unrelated
- not a zip at all, or a zip without `ppt/slides/`
- the deck exceeds the per-call page limit — extract page by page with `--slide`, or split the file
- the file is too large, or unpacks to too much

`flags` on a page is **not** an error: it records what the extractor refused to reconstruct. Surface it — a silently missing element on screen is worse than a mentioned one.
