https://txtfetch.com/extract/pptx/
Slide text and speaker notes, together.
Most extractors keep one or the other. txtfetch reads slide bodies and speaker notes from the same request.
the-problem
A .pptx is an OOXML zip archive. Each slide's visible text lives in ppt/slides/slideN.xml. But speaker notes are a separate part entirely, ppt/notesSlides/, with its own XML files and its own relationship IDs back to the slide they annotate. Extractors that read slideN.xml and stop miss the notes silently. Extractors built around the notes miss the body instead. Text also hides in grouped shapes, nested inside other shapes rather than top-level. It hides in SmartArt diagrams too: their text sits in diagrams/data*.xml, disconnected from the visible diagram layout. Tables can hide text as well, and so can text baked into slide images as pixels rather than markup. Slide order compounds all of this. It's defined by relationships in presentation.xml, not by the numeric order slide files happen to be zipped in.
one-request-solution
txtfetch's Office parser reads both parts of a .pptx in one pass: slide body text from ppt/slides/, and speaker notes from ppt/notesSlides/. It also pulls grouped-shape and SmartArt text out of their nested XML. All of it folds into one extracted_text response, in the deck's real slide order.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-board-deck.pptximport os
import requests
with open("quarterly-board-deck.pptx", "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
print(r.json()["extracted_text"])import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("quarterly-board-deck.pptx")]);
const form = new FormData();
form.append("file", file, "quarterly-board-deck.pptx");
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
f, err := os.Open("quarterly-board-deck.pptx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "quarterly-board-deck.pptx")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, f); err != nil {
panic(err)
}
writer.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}Or skip the download. Pass a url parameter and txtfetch fetches the document server-side:
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/decks/product-roadmap.pptx" \
-H "Authorization: Bearer $TXTFETCH_KEY"import os
import requests
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": "https://example.com/decks/product-roadmap.pptx"},
)
print(r.json()["extracted_text"])const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/decks/product-roadmap.pptx");
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
if err != nil {
panic(err)
}
q := endpoint.Query()
q.Set("url", "https://example.com/decks/product-roadmap.pptx")
endpoint.RawQuery = q.Encode()
req, err := http.NewRequest("POST", endpoint.String(), nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}{
"status": "success",
"extracted_text": "..."
}what-comes-back
That is the shape. This is the text. A real .pptx from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture.
S6 Launch Deck (PPTX)
raw parser output
Benchmarking the Quality Climb
txtfetch S6 — accuracy harness
Why it matters
Structured output needs proof, not just a demo
VLM tier cost only pays off if quality actually improves
What ships
Committed corpus across 10 categories
Reproducible CLI + committed report
the human-checked ideal
Benchmarking the Quality Climb
txtfetch S6 — accuracy harness
Why it matters
Structured output needs proof, not just a demo
VLM tier cost only pays off if quality actually improves
What ships
Committed corpus across 10 categories
Reproducible CLI + committed reportSee all ten documents, with the full explanation →
formats-covered
.pptx.ppt.pptm.potx.odp
response-options
Presenting slide structure to an LLM? ?format=markdown returns headings and bullet hierarchy per slide instead of one flat block of text.
faq
- Does extraction include PowerPoint speaker notes, or just slide text?
- Both. Speaker notes live in a separate ppt/notesSlides/ part of the .pptx archive, apart from the slide body. txtfetch reads both parts and includes them together in the response.
- What about text inside grouped shapes or SmartArt diagrams?
- Yes. Grouped shapes nest inside other shapes rather than sitting at the top level, and SmartArt text lives in its own diagrams/data*.xml part. Both are walked and included, not just top-level slide text boxes.
- Does it preserve the actual slide order?
- Yes. Slide order comes from presentation.xml's relationship list, not from the filename order of the slideN.xml parts inside the zip. That filename order doesn't reliably match reading order.
- Does it handle the legacy .ppt binary format too?
- Yes, and .pptm (macro-enabled) and .potx (template) too. All four route through the same endpoint and return the same response shape.
go-further
Send a real .pptx through it.
One HTTP call returns the text. Read one in your browser first, for free.
Get an API key →