How to Ingest PPTX into Chroma with EnConvert

Use EnConvert to ingest a PPTX into Chroma as heading-aware chunks with section metadata, ready to add as documents and filter by section before vector search

Get API key

A developer building a RAG pipeline that ingests PPTX files into Chroma needs structured chunks where each part of a presentation carries its title and section context, not flat text that loses the deck’s structure. A library like python-pptx extracts content slide by slide, returning the text of each shape, and the slide title is a separate placeholder you have to find and attach to the text yourself. It also produces no section boundaries and no metadata a Chroma filter can operate on. For an agent reading a product pitch, a technical architecture deck, or a training presentation, flat slide text produces chunks the agent cannot locate within the presentation or filter by section. EnConvert’s Ingest endpoint takes the uploaded PPTX and converts each slide into a Markdown section before chunking. It returns JSONL chunks split at heading boundaries, each carrying its headings_path breadcrumb, section name, and chunk index in metadata, so structured PPTX chunks are ready to add to Chroma and retrieve in one job.

Example

python
import json
import os
import time

import chromadb
import requests
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction

ENCONVERT_API_KEY = os.environ["ENCONVERT_API_KEY"]
HEADERS = {"X-API-Key": ENCONVERT_API_KEY}

# Step 1: Upload the PPTX (the multipart field is named "files")
# Each slide is converted to a "## Slide N: Title" Markdown section before chunking
with open("/path/to/your/architecture-overview.pptx", "rb") as f:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers=HEADERS,
        files={
            "files": (
                "architecture-overview.pptx",
                f,
                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
            )
        },
        timeout=60,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 2: Poll until the job completes
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers=HEADERS,
        timeout=30,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] == "completed":
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(job.get("error_message") or f"Ingest job {job['status']}")
    time.sleep(2)

# Step 3: Download the JSONL output, one chunk per line
# The signed URL expires after 15 minutes. Plain GET, no API key.
jsonl_response = requests.get(job["output_url"], timeout=60)
jsonl_response.raise_for_status()

chunks = [
    json.loads(line)
    for line in jsonl_response.content.decode("utf-8").split("\n")
    if line.strip()
]
print(f"Loaded {len(chunks)} chunks ready for embedding")
print("Sections:", sorted({chunk["metadata"]["section"] for chunk in chunks}))

# Step 4: Connect to Chroma and create or open the collection
client = chromadb.PersistentClient(path="./chroma_db")

embedding_function = SentenceTransformerEmbeddingFunction(
    model_name="all-MiniLM-L6-v2"
)

collection = client.get_or_create_collection(
    name="pptx_chunks",
    embedding_function=embedding_function,
)

# Step 5: Add each chunk to Chroma with its metadata preserved
# headings_path is a list and can be empty, so join it into one string
# (Chroma does not accept empty lists as metadata values)
collection.add(
    ids=[chunk["id"] for chunk in chunks],
    documents=[chunk["content"] for chunk in chunks],
    metadatas=[
        {
            "section": chunk["metadata"]["section"],
            "headings_path": " > ".join(chunk["metadata"]["headings_path"]),
            "chunk_index": chunk["metadata"]["chunk_index"],
            "title": chunk["metadata"]["title"],
            "source_url": chunk["metadata"]["source_url"],
        }
        for chunk in chunks
    ],
)
print(f"Added {len(chunks)} chunks to Chroma")

# Step 6: Similarity search narrowed to one section by a metadata filter
# Replace with a section printed above. Slide sections look like "Slide 3: Title",
# and speaker notes are their own "Notes" section.
SECTION = "Slide 3: Service Authentication"

results = collection.query(
    query_texts=["how do services authenticate with each other"],
    n_results=3,
    where={"section": SECTION},
    include=["documents", "metadatas", "distances"],
)

for doc, meta, distance in zip(
    results["documents"][0], results["metadatas"][0], results["distances"][0]
):
    print(f"{meta['headings_path']} (distance {distance:.3f})")
    print(doc[:200])

What you get back

Ingest converts the PPTX to Markdown sections and returns JSONL chunks split at heading boundaries, each carrying its headings_path breadcrumb, section name, and chunk index, so Chroma metadata filters can narrow retrieval to a specific section before vector search runs. Extracting content with python-pptx gives flat text per slide, with the slide title left as a separate placeholder to attach yourself and no metadata Chroma can filter on. The Ingest endpoint processes text-based slides only; image-only slides and slides with no text content are skipped.

Get free API key — 500 ops per month, no card required.