# PDF to MP3 - Convert PDF, EPUB, DOCX & Text to Audiobook (`marielise.dev/pdf-to-mp3`) Actor

Turn PDF, EPUB, DOCX, Markdown, HTML, TXT or RTF into an MP3 audiobook. Free Edge neural TTS needs no API key, OCR recovers scanned pages, 70+ languages are auto-detected, and OpenAI or ElevenLabs voices work with your own key. Long books split into chapter-sized parts.

- **URL**: https://apify.com/marielise.dev/pdf-to-mp3.md
- **Developed by:** [Marielise](https://apify.com/marielise.dev) (community)
- **Categories:** AI, For creators, Education
- **Stats:** 18 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $40.00 / 1,000 audio minute generateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF to MP3 — Convert PDF, EPUB, DOCX & Text to Audiobook

![TTS: Edge free + OpenAI + ElevenLabs](https://img.shields.io/badge/TTS-Edge%20free%20%2B%20OpenAI%20%2B%20ElevenLabs-2563eb)
![Formats: PDF · DOCX · EPUB · MD · TXT · HTML · RTF](https://img.shields.io/badge/formats-PDF%20·%20DOCX%20·%20EPUB%20·%20MD%20·%20TXT%20·%20HTML%20·%20RTF-444)
![OCR: scanned PDFs](https://img.shields.io/badge/OCR-scanned%20PDFs-16a34a)
![Output: MP3 audiobook](https://img.shields.io/badge/output-MP3%20audiobook-orange)

**This actor converts PDF, EPUB, DOCX, Markdown, HTML, TXT, and RTF files into MP3 audiobooks** using free Microsoft Edge neural TTS (no API key required), with OCR for scanned PDFs, 70+ languages, and optional BYOK OpenAI or ElevenLabs voices. Pricing is pay-per-use: $0.05 to start plus $0.04 per minute of audio produced.

Turn any document into an **MP3 audiobook** in one run. Paste a URL, upload a file, drop in raw text, or send base64 bytes. Pick a voice. Click run. Get a downloadable MP3. No prompts to chain, no manual chunking, no ffmpeg gymnastics. Just clean audio at the end.

Even **scanned / image-only PDFs** work — when a page has no text layer, the actor automatically OCRs it (Tesseract) and narrates the recovered text.

Use the **free Edge TTS** voices by default (400+ neural voices, 70+ languages, no API key) — or bring your own OpenAI / ElevenLabs key for premium voices and steerable narration.

**Perfect for:** Anyone with a backlog of Project Gutenberg / Calibre EPUBs and zero time to read them, researchers listening to arXiv PDFs on a commute, business users turning Word DOCX reports into audio briefings, devs converting READMEs and blog drafts into audio for proofing, knowledge workers narrating long reports, accessibility-first publishers, podcast producers prototyping audiobook conversions, students reviewing textbook chapters on the go, journalists drafting voiceovers, and Substack writers exporting their newsletter to audio.

### Features

<table>
<thead>
<tr>
<th>Feature</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Seven input formats</strong></td>
<td>PDF (text-layer), DOCX (Word, via mammoth), EPUB (ebook, spine-ordered chapters), Markdown (syntax stripped), plain text, HTML (tags stripped), RTF (control codes stripped). Auto-detected from magic bytes / mimetype / extension</td>
</tr>
<tr>
<td><strong>OCR for scanned PDFs</strong></td>
<td>Pages with no selectable text layer are auto-rendered and OCR'd with Tesseract (7 languages: EN, ES, FR, DE, IT, PT, NL) so scanned books and photographed pages narrate too. Only the pages that actually need OCR are processed and billed</td>
</tr>
<tr>
<td><strong>Encrypted PDF support</strong></td>
<td>Provide the password via <code>pdfPassword</code> to decrypt and narrate password-protected PDFs</td>
</tr>
<tr>
<td><strong>SSRF-guarded URL fetch + proxy</strong></td>
<td>Document URLs are validated against private / internal address ranges before fetching. Optional Apify proxy for hosts that block datacenter IPs</td>
</tr>
<tr>
<td><strong>Four ways to provide it</strong></td>
<td>Public URL, file upload, base64 paste, or raw text paste (great for blog drafts &amp; ChatGPT replies)</td>
</tr>
<tr>
<td><strong>Free Edge TTS by default</strong></td>
<td>Microsoft Edge neural voices, no API key, no per-character cost — works out of the box</td>
</tr>
<tr>
<td><strong>One document in, one (or many) MP3s out</strong></td>
<td>Short inputs produce a single MP3. Long ones are auto-split into chapter-sized parts, plus a shareable INDEX.html page with inline players + download links</td>
</tr>
<tr>
<td><strong>Multiple TTS engines</strong></td>
<td>Edge TTS (FREE, default), OpenAI gpt-4o-mini-tts (steerable), OpenAI tts-1 / tts-1-hd, ElevenLabs Flash v2.5 / Turbo v2.5</td>
</tr>
<tr>
<td><strong>BYOK for premium models</strong></td>
<td>OpenAI and ElevenLabs models require your own API key (we never markup the provider price)</td>
</tr>
<tr>
<td><strong>Steerable narration</strong></td>
<td>With gpt-4o-mini-tts you can prompt the voice ("calm audiobook narrator", "energetic podcast host", "slow and deliberate") without retraining</td>
</tr>
<tr>
<td><strong>Auto language detection</strong></td>
<td>Edge TTS auto-picks a matching voice based on the text language. Or force a specific Azure voice name (e.g. <code>en-US-AndrewNeural</code>)</td>
</tr>
<tr>
<td><strong>Markdown-aware</strong></td>
<td>Markdown syntax is stripped (no "asterisk asterisk bold asterisk asterisk"). Headings, lists, links, code fences, tables, and inline formatting all read as natural prose</td>
</tr>
<tr>
<td><strong>Page / section range support</strong></td>
<td>Narrate the whole document, a single chapter, or a custom slice. Works on PDFs (real pages) and TXT / MD / HTML (~3000-char pseudo-pages). Range syntax: <code>1-10</code>, <code>1,3,5</code>, <code>1-3,7-9</code></td>
</tr>
<tr>
<td><strong>Smart chunking</strong></td>
<td>Text is split on paragraph and sentence boundaries before TTS. Hard cuts respect word boundaries so chunks don't start mid-word</td>
</tr>
<tr>
<td><strong>Pre-flight cost preview + hard cap</strong></td>
<td>Every run prints an estimated ceiling cost before TTS starts and writes a <code>PREVIEW</code> key. Set <code>maxCostUsd</code> to stop before TTS if the estimate is too high, or to stop narration cleanly at the last part the budget covers if the document runs longer than estimated</td>
</tr>
<tr>
<td><strong>Provider-adaptive concurrency</strong></td>
<td>Auto-caps parallel TTS calls per provider (ElevenLabs 2, Edge 8, OpenAI 10) so free tiers don't hit 429 storms</td>
</tr>
<tr>
<td><strong>Resume failed runs</strong></td>
<td>Already-synthesized chunks are cached. If a long run times out or fails partway, the next run picks up where it left off — no re-paying for TTS already done</td>
</tr>
<tr>
<td><strong>Skip-failed-chunks mode</strong></td>
<td>If a single chunk keeps failing after retries, skip it and keep narrating the rest of the document (configurable). Auth / quota errors always abort cleanly</td>
</tr>
<tr>
<td><strong>ffmpeg concat + ID3 tags</strong></td>
<td>Chunks are stitched into valid MP3 containers with correct duration metadata and ID3 tags (title, album, track, genre=Audiobook) so players show proper info. No "audio glitch at minute 4" bugs</td>
</tr>
<tr>
<td><strong>Transparent pricing</strong></td>
<td>Pay-per-event: one simple meter — per audio minute produced (plus a small per-run fee, and an OCR surcharge only for scanned pages). No per-page double-billing, no markups on BYOK providers</td>
</tr>
</tbody>
</table>

### How to Use

#### Step 1: Provide a document

Any **one** of:

- **Document URL** — any publicly reachable URL ending in `.pdf`, `.docx`, `.epub`, `.md`, `.markdown`, `.txt`, `.html`, `.htm`, or `.rtf`
- **Upload file** — drag and drop a PDF / DOCX / EPUB / MD / TXT / HTML / RTF (uploaded to the run's key-value store)
- **Base64 content** — paste raw base64 bytes; format auto-detected from magic bytes / mimetype
- **Raw text paste** — paste prose, Markdown, or HTML directly into the `text` field (perfect for blog drafts, ChatGPT replies, READMEs)

> Scanned / image-only PDF pages (no text layer) are automatically OCR'd when `enableOcr` is on (default). For encrypted / password-protected PDFs, supply `pdfPassword`.

#### Step 2: Pick a voice and model

| Model | Best for | API key required |
|-------|----------|------------------|
| `edge-tts` (default) | Free, long books, multi-language | **No key** — free |
| `openai-gpt-4o-mini-tts` | Steerable narration with style instructions | OpenAI key (BYOK) |
| `openai-tts-1` | Bulk runs, low cost, supports `speed` | OpenAI key (BYOK) |
| `openai-tts-1-hd` | High-quality OpenAI audio | OpenAI key (BYOK) |
| `elevenlabs-flash-v2_5` | Fast, real-time-quality voices | ElevenLabs key (BYOK) |
| `elevenlabs-turbo-v2_5` | Highest-quality ElevenLabs voices | ElevenLabs key (BYOK) |

#### Step 3: (Optional) Set a range, voice, speed, or instructions

- `voice` — leave blank for auto. For Edge TTS use an Azure ShortName like `en-US-AndrewNeural`, `es-ES-ElviraNeural`. For OpenAI: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`, `coral`, `sage`. For ElevenLabs: a voice ID.
- `language` — `auto` (recommended) or a specific ISO code (Edge TTS only — OpenAI voices are multilingual).
- `pageRange` — e.g. `1-10` or `1,3,5` or `1-3,7-9`. Empty = whole document. For non-PDF formats, "pages" are ~3000-char sections.
- `speed` — 0.25 to 4.0. Only applies to `openai-tts-1` / `openai-tts-1-hd`.
- `instructions` — free-form style hint for `openai-gpt-4o-mini-tts`, e.g. *"Calm, slow audiobook narrator with a neutral accent."*
- `enableOcr` — on by default. Auto-OCRs scanned PDF pages that have no text layer. Turn off to fail fast on scans instead.
- `pdfPassword` — password for encrypted PDFs.
- `proxyConfiguration` — optional Apify proxy, used only for the Document URL fetch.

#### Step 4: Run and download

The Actor:

1. Downloads / decodes / reads the input
2. Detects the format (PDF magic bytes + content-type + extension + content sniff)
3. Extracts and normalises the text (page-range aware, Markdown / HTML aware)
4. Splits into TTS-sized chunks at sentence boundaries (word-boundary safe hard cuts)
5. Synthesises each chunk with the chosen provider in parallel
6. Folds chunks into chapter-sized parts as they complete (ffmpeg concat)
7. Uploads each part to the key-value store + writes a shareable INDEX.html

You'll find the result in:

- The **dataset** — one row with metadata (`indexUrl`, `audioUrl`, `partsCount`, `parts[]`, `durationSeconds`, `chars`, `pagesProcessed`, `cost`, `status`)
- The **key-value store** — each MP3 part, the `INDEX.html` page, the `PREVIEW` estimate, and the `OUTPUT` record

### Input Reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `documentUrl` | string | one of | Public URL (PDF / TXT / MD / HTML) |
| `documentFile` | file | one of | Upload a PDF / TXT / MD / HTML from your device |
| `documentBase64` | string | one of | Base64-encoded document bytes |
| `text` | string | one of | Paste raw prose, Markdown, or HTML directly |
| `model` | enum | no | TTS model (default `edge-tts`, free) |
| `voice` | string | no | Edge ShortName, OpenAI voice, or ElevenLabs voice ID |
| `language` | enum | no | Auto-detect (default) or specific ISO code |
| `speed` | number | no | 0.25 to 4.0, default 1.0. tts-1 / tts-1-hd only |
| `instructions` | string | no | Free-form style for gpt-4o-mini-tts |
| `pageRange` | string | no | e.g. `1-10` or `1,3,5`. Empty = full document |
| `chunkSize` | integer | no | 500 to 4096, default 4000 (auto-clamped to 2500 for ElevenLabs) |
| `concurrency` | integer | no | 1 to 20 parallel TTS requests, default 5 (auto-clamped per provider) |
| `resume` | boolean | no | Skip already-synthesized chunks from previous runs (default true) |
| `skipFailedChunks` | boolean | no | Skip individual chunk failures instead of aborting (default true) |
| `maxPartMb` | integer | no | Max size per MP3 part, default 40MB |
| `maxCostUsd` | number | no | Hard ceiling (min 0.05). Stops before TTS if the estimate exceeds it, and stops narration at the last part the budget covers if reality overshoots the estimate (`status: "capped"`) |
| `enableOcr` | boolean | no | OCR scanned / image-only PDF pages (default true) |
| `pdfPassword` | secret | no | Password for encrypted PDFs |
| `proxyConfiguration` | object | no | Apify proxy for the Document URL fetch |
| `openaiApiKey` | secret | **required for openai-**\* | Your OpenAI API key (BYOK) |
| `elevenlabsApiKey` | secret | **required for elevenlabs-**\* | Your ElevenLabs API key (BYOK) |
| `debug` | boolean | no | Verbose logs |

### Output Example

One dataset row per run. It leads with the outcome so a person or an agent can
act on it without parsing the rest.

```json
{
    "status": "success",
    "statusMessage": "Narrated the whole document into 2 MP3 parts totalling 30m 43s.",
    "title": "attention-is-all-you-need",
    "indexUrl": "https://api.apify.com/v2/key-value-stores/.../records/INDEX",
    "audioUrl": "https://api.apify.com/v2/key-value-stores/.../records/narration-abc123-part001.mp3",
    "audioUrls": [
        "https://api.apify.com/v2/key-value-stores/.../records/narration-abc123-part001.mp3",
        "https://api.apify.com/v2/key-value-stores/.../records/narration-abc123-part002.mp3"
    ],
    "audioKvKey": "narration-abc123-part001.mp3",
    "durationSeconds": 1843.2,
    "durationHuman": "30m 43s",
    "partsCount": 2,
    "sizeBytes": 19200000,
    "audioFormat": "mp3",
    "parts": [
        { "part": 1, "key": "narration-abc123-part001.mp3", "url": "https://.../", "durationSeconds": 1200, "bytes": 12500000 },
        { "part": 2, "key": "narration-abc123-part002.mp3", "url": "https://.../", "durationSeconds": 643.2, "bytes": 6700000 }
    ],
    "chars": 48210,
    "pagesProcessed": 24,
    "ocrPagesProcessed": 0,
    "language": "auto",
    "voice": "en-US-AndrewNeural",
    "model": "edge-tts",
    "cost": 1.29,
    "chunksTotal": 13,
    "chunksSucceeded": 13,
    "chunksFailed": 0,
    "generatedAt": "2026-09-07T10:30:00.000Z"
}
```

`status` is one of:

| Status | Meaning |
|--------|---------|
| `success` | The whole document was narrated. |
| `partial` | Some passages failed after retries and were skipped; the gaps are listed in `failedChunkIndexes`. |
| `capped` | Narration stopped early to stay inside a cost ceiling. The audio produced is complete and playable. |
| `failed` | No audio was produced. `error` and `errorCode` say why. The run still exits successfully. |

The MP3 parts also live in the run's key-value store alongside an `INDEX` page
with an inline player and a download link per part, and a `PREVIEW` record
written before any speech is synthesized.

### Use Cases

- **Any ebook → free audiobook** — drop a Project Gutenberg EPUB and listen to a full classic novel. Spine-ordered chapters narrate in the right sequence.
- **Word doc → audio briefing** — drop your DOCX report and listen on a commute instead of skimming on screen.
- **Audiobook prototyping** — convert your ebook PDF into MP3 to validate narrator tone before commissioning a human voiceover.
- **Research papers on the go** — listen to arXiv PDFs during a commute or workout.
- **README → audio** — paste your project README and listen to your own docs to spot rough explanations.
- **Blog draft proofing** — paste a Markdown blog draft and listen to it before publishing. You'll hear awkward phrasing you'd never catch reading.
- **ChatGPT reply → podcast snippet** — copy a long ChatGPT response into the `text` field and listen as audio.
- **Accessibility** — generate audio versions of internal documentation for screen-reader-light workflows.
- **Onboarding** — pipe HR PDFs (handbooks, policies) into audio for distributed teams.
- **Newsletter audio versions** — automatically narrate weekly reports (PDF, MD, or HTML) for paying subscribers.
- **Language learning** — narrate text in different voices and speeds to practise listening comprehension.
- **Substack → audio export** — export a post as HTML and narrate it for a podcast feed.

### Use it from an AI agent (MCP)

This Actor is exposed as an MCP tool by the hosted Apify MCP server, so Claude,
ChatGPT and any agent framework can narrate a document without writing code. No
Standby mode and no special configuration are involved: the server reads the
input schema and builds the tool from it.

- **Actor ID:** `marielise.dev/pdf-to-mp3`
- **MCP tool name:** `marielise-dot-dev--pdf-to-mp3` (the server rewrites `.` to
  `-dot-` and `/` to `--`)
- **Server:** `https://mcp.apify.com` — Streamable HTTP only, OAuth or a bearer
  token. The legacy `/sse` endpoint has been removed, and there is no anonymous
  access: every call needs an Apify token, and the run is billed to that
  account like any other.

#### Claude Desktop

The simplest route is **Settings → Connectors → Add custom connector** with the
server URL `https://mcp.apify.com`; your browser opens once to authorise Apify.

To load only this Actor and nothing else, use a `tools` selector. In
`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "pdf-to-mp3": {
      "url": "https://mcp.apify.com/?tools=fetch-actor-details,marielise.dev/pdf-to-mp3"
    }
  }
}
```

If your client cannot do OAuth, authenticate with a token instead:

```json
{
  "mcpServers": {
    "pdf-to-mp3": {
      "url": "https://mcp.apify.com/?tools=fetch-actor-details,marielise.dev/pdf-to-mp3",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

#### Claude Code

```bash
claude mcp add pdf-to-mp3 "https://mcp.apify.com/?tools=fetch-actor-details,marielise.dev/pdf-to-mp3" -t http
```

#### Example tool call

Every input is optional, so an agent can call the tool with almost nothing and
still get audio back:

```json
{
  "name": "marielise-dot-dev--pdf-to-mp3",
  "arguments": {
    "documentUrl": "https://arxiv.org/pdf/1706.03762",
    "pageRange": "1-8",
    "voice": "en-US-AndrewNeural",
    "maxCostUsd": 2.0
  }
}
```

The call returns run metadata and storage IDs rather than the rows themselves.
Fetch the result with the auto-injected `get-dataset-items` tool using the
returned `datasetId`, and you get one row:

```json
{
  "status": "success",
  "statusMessage": "Narrated the whole document into 2 MP3 parts totalling 51m 12s.",
  "title": "1706.03762",
  "indexUrl": "https://api.apify.com/v2/key-value-stores/%3CstoreId%3E/records/INDEX",
  "audioUrl": "https://api.apify.com/v2/key-value-stores/%3CstoreId%3E/records/narration-%3CrunId%3E-part001.mp3",
  "audioUrls": ["...part001.mp3", "...part002.mp3"],
  "durationHuman": "51m 12s",
  "partsCount": 2,
  "audioFormat": "mp3",
  "voice": "en-US-AndrewNeural",
  "cost": 2.09
}
```

`audioUrl` and every entry in `audioUrls` are direct, publicly fetchable MP3
links, so an agent can hand them straight to a player or a download step without
pulling a binary through the tool response.

#### Calling the Actor directly instead

For agents that talk to the Apify API rather than MCP, start a run and take the
dataset row back in the same response. Keep the document small when doing this:
the synchronous endpoint has a hard 300-second ceiling.

```bash
curl -X POST "https://api.apify.com/v2/acts/marielise.dev~pdf-to-mp3/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"documentUrl": "https://www.orimi.com/pdf-test.pdf", "maxCostUsd": 1.0}'
```

For a full book, start the run asynchronously and collect the result from a
webhook or by polling instead:

```bash
curl -X POST "https://api.apify.com/v2/acts/marielise.dev~pdf-to-mp3/runs?token=$APIFY_TOKEN&maxTotalChargeUsd=5" \
  -H 'Content-Type: application/json' \
  -d '{"documentUrl": "https://www.gutenberg.org/ebooks/1342.epub.noimages"}'
```

#### Keeping an agent's spend bounded

Narration is billed per minute of audio, and a long book is a lot of minutes.
Two independent ceilings exist, and this Actor honours both by **stopping**
rather than by handing over unbilled audio:

- `maxTotalChargeUsd` on the run — the platform-level limit, and the right one
  for an autonomous agent to set.
- `maxCostUsd` in the input — refuses up front if the pre-flight estimate is
  already over, and stops cleanly mid-run if the document turns out longer than
  estimated.

Either ceiling produces `status: "capped"` with the audio generated so far and a
`statusMessage` explaining what happened, never a failed run.

### Pricing

This Actor uses **Pay Per Event** so you only pay for the work the run actually does. No premium-voice surcharges, no provider markups.

| Event | Price (USD) | When charged |
|-------|------------:|--------------|
| `apify-actor-start` | $0.05 | Automatically at run start, one event per GB of allocated memory (minimum one) — the per-job base fee, and the only charge on a run that produces no audio |
| `audio-minute-generated` | $0.04 | Per minute of MP3 actually produced and uploaded — the single value meter |
| `ocr-page-processed` | $0.03 | Only for scanned / image-only PDF pages that had no text layer and were recovered via OCR. Text-layer PDFs never pay this |

**Typical cost example** — a 20-page research paper (~40k chars, ~45 minutes of audio):

- `apify-actor-start`: $0.05
- \~45 × `audio-minute-generated`: $1.80
- **Total: ~$1.85** for the full paper (≈ $0.04 per minute of audiobook)

Roughly 900 characters of text becomes one minute of speech, so you can estimate
any document before you run it. `PRICING.md` in the repository shows the measured
cost model behind these prices.

For OpenAI / ElevenLabs models, this is **all you pay Apify**. You pay the provider directly with your own API key on top — that's the whole point of BYOK: no markup.

#### Pre-flight cost preview & hard cap

Every run writes a `PREVIEW` key to the key-value store BEFORE TTS starts, with pages to process, estimated audio minutes, and the estimated ceiling cost. The same numbers are printed to the run log.

Set the optional **`maxCostUsd`** input to enforce a hard ceiling. If the estimate already exceeds it the run stops before any TTS and you pay only the actor-start fee. If the document turns out longer than estimated (slow speech, CJK scripts, OCR'd text), narration stops at the last part the budget covers and returns `status: "capped"` with the audio produced so far — the run never hands over minutes it cannot bill for, and never bills past your ceiling. OCR is bounded by the same ceiling before it runs.

Apify's run-level **Max total charge** works the same way and is the better option for automated and agent callers. Both are honoured; whichever binds first stops the narration.

### BYOK — Bring Your Own Key

| Model family | Key field | Where to get it | Free tier? |
|--------------|-----------|-----------------|------------|
| `edge-tts` (default) | — | **No key needed** | Yes — completely free |
| `openai-*` | `openaiApiKey` | https://platform.openai.com/api-keys | OpenAI charges per character |
| `elevenlabs-*` | `elevenlabsApiKey` | https://elevenlabs.io/app/settings/api-keys | ElevenLabs free tier available |

The actor never logs your keys (`isSecret: true`) and never proxies your calls through our servers — your key talks directly to the provider from inside the actor's run.

### FAQ

#### Which document formats are supported?

- **PDF** (`.pdf`) — text-layer PDFs extract natively; scanned / image-only pages are auto-OCR'd (Tesseract).
- **DOCX** (`.docx`) — Word documents, parsed with `mammoth`. Styles, lists, tables, footnotes handled natively.
- **EPUB** (`.epub`) — ebooks. Walked in spine order so chapters narrate in the right sequence. HTML stripped per chapter.
- **Markdown** (`.md`, `.markdown`, `.mdx`) — syntax stripped so the voice reads natural prose.
- **Plain text** (`.txt`, `.text`) — UTF-8, BOM handled.
- **HTML** (`.html`, `.htm`, `.xhtml`) — tags stripped, entities decoded, scripts and styles removed.
- **RTF** (`.rtf`) — control codes stripped, unicode escapes and hex bytes decoded.

#### Does it work on scanned PDFs?

Yes. When a PDF page has no selectable text layer, the actor renders it (poppler `pdftoppm`) and runs OCR (Tesseract) to recover the text, then narrates it. OCR runs only on pages that need it, and those pages are billed via the `ocr-page-processed` event ($0.03/page). Built-in OCR languages: English, Spanish, French, German, Italian, Portuguese, Dutch (others fall back to English). Turn it off with `enableOcr: false` to fail fast on scans instead.

#### Does it work on password-protected PDFs?

Yes — pass the password in the `pdfPassword` input and the actor decrypts the PDF before extraction.

#### What about ODT, MOBI, AZW3, or Pages?

Not supported in v0.1. Convert ODT to DOCX first; for Kindle formats, convert via Calibre to EPUB.

#### Why do OpenAI / ElevenLabs models require BYOK?

So we never markup the provider price. Pay Apify for the actor work, pay OpenAI / ElevenLabs directly for the TTS calls. Cleaner, cheaper, more honest. For zero-key zero-friction runs, the default `edge-tts` is free and gives great quality on 70+ languages.

#### How is "page" defined for TXT / MD / HTML?

There are no real pages, so the actor splits the cleaned text into ~3000-char pseudo-pages — roughly the length of one PDF page of prose. This is only used for the `pageRange` selector; billing is per audio minute, so it's format-agnostic and fair either way.

#### How long can the document be?

PDFs up to 50 MB. EPUB up to 40 MB. DOCX up to 30 MB. TXT / MD / HTML / RTF up to 20 MB of decoded text. There is no hard page limit. Long inputs are auto-split into chapter-sized MP3 parts (configurable via `maxPartMb`).

#### What if my run times out or fails partway?

Re-run with the same input. The `resume` option (on by default) skips already-synthesized chunks via a shared cache, so you only pay TTS for the missing pieces.

#### Can I get word-level timestamps?

v0.1 does not emit timestamps. Coming in a future version.

#### Can I use multiple speakers / podcast mode?

Not in v0.1. Single-voice narration only.

#### What audio format is produced?

MP3, standard playback on any device. Edge TTS produces 24 kHz mono; OpenAI / ElevenLabs use their default high-quality output.

#### Can I override the voice with a custom ElevenLabs voice?

Yes — paste any ElevenLabs voice ID into the `voice` field when using an `elevenlabs-*` model. You can clone your own voice in your ElevenLabs account and use that ID here.

#### Is my API key safe?

Yes. API keys are marked `isSecret: true` in the input schema and are never logged or persisted.

### Built and maintained by Equipinico

Need a custom variant (different language model, custom voices, SSML support, podcast multi-speaker, EPUB / DOCX support)? Reach out via the Apify Store contact link.

# Actor input Schema

## `documentUrl` (type: `string`):

Public http(s) URL of the document to narrate. Handles PDF, DOCX, EPUB, Markdown (.md/.mdx), TXT, HTML and RTF; the format is detected from the bytes, not the extension. Scanned PDFs are recovered with OCR and encrypted PDFs need pdfPassword. Use this OR documentFile OR documentBase64 OR text.

## `documentFile` (type: `array`):

File picker for the Apify Console. Accepts .pdf, .docx, .epub, .md, .txt, .html and .rtf. Only the first file is used. API and MCP callers should use documentUrl, documentBase64 or text instead.

## `documentBase64` (type: `string`):

Base64-encoded document bytes, for documents that are not publicly hosted. The format is auto-detected from the decoded content. Prefer documentUrl when a URL exists, since base64 inflates the input payload by about a third.

## `text` (type: `string`):

Prose, Markdown or HTML pasted directly, for narrating a draft, an article body or a generated summary with no file involved. Markdown and HTML markup is stripped so the voice reads natural prose. Roughly 900 characters becomes one minute of audio.

## `model` (type: `string`):

Speech engine. Keep edge-tts unless you need a specific vendor voice: it is free and needs no key. Every openai-\* and elevenlabs-\* model requires your own API key and is billed by that provider.

## `language` (type: `string`):

Document language. Keep auto to sample the text and pick a matching voice. Set a code only to correct a bad detection. Ignored when voice is set, and for OpenAI models.

## `voice` (type: `string`):

Leave blank to auto-pick a voice for the detected language. For Edge TTS pass an Azure ShortName such as en-US-AndrewNeural, en-US-AvaNeural, en-GB-SoniaNeural, es-ES-ElviraNeural, fr-FR-DeniseNeural, de-DE-KatjaNeural or pt-BR-FranciscaNeural (400+ voices, 70+ languages). For OpenAI use alloy, echo, fable, onyx, nova, shimmer, coral or sage. For ElevenLabs use a voice ID.

## `speed` (type: `number`):

Playback speed multiplier, where 1.0 is the natural pace and 1.5 is comfortable speed-listening. Applies only to the openai-tts-1 and openai-tts-1-hd models; edge-tts and gpt-4o-mini-tts ignore it. Faster speech produces fewer audio minutes and therefore costs less.

## `instructions` (type: `string`):

Plain-language delivery direction for the openai-gpt-4o-mini-tts model only, for example a calm audiobook narrator or an energetic podcast host. Every other model ignores this field.

## `pageRange` (type: `string`):

1-indexed range to narrate, for example 1-10 or 1,3,5 or 1-3,7-9. Leave empty for the whole document. For PDFs these are real pages; DOCX, EPUB, TXT, MD, HTML and RTF are split into roughly 3000-character pseudo-pages so the same syntax applies. Narrowing the range is the direct way to cut both run time and cost.

## `enableOcr` (type: `boolean`):

Recover text from PDF pages that have no selectable text layer by rendering them and running Tesseract OCR. Only pages that actually need it are processed and billed. Turn this off to fail fast on a scan instead of paying to read it.

## `pdfPassword` (type: `string`):

Password used to decrypt a password-protected PDF before text extraction. Leave blank for normal PDFs. Encrypted DOCX and EPUB files are not supported.

## `maxCostUsd` (type: `number`):

Hard ceiling on this Actor's charges. If the pre-flight estimate already exceeds it the run stops before any speech is synthesized and you pay only the start fee. If the document turns out longer than estimated, narration stops cleanly at the last part the budget covers and returns status 'capped'. Minimum 0.05. Does not bound third-party OpenAI or ElevenLabs charges.

## `chunkSize` (type: `integer`):

Characters sent per TTS request. The default suits every model; ElevenLabs is auto-clamped to 2500. Smaller chunks recover better from a single bad passage but do not change the total price.

## `concurrency` (type: `integer`):

How many chunks are synthesized at once. Auto-clamped per provider to Edge 8, OpenAI 10, ElevenLabs 2. Higher is faster and cheaper in compute; lower it only if the provider rate-limits you.

## `resume` (type: `boolean`):

Reuse chunks already synthesized by an earlier run of the same document, voice and model, so a book that timed out continues instead of restarting. Turn off to force a full re-narration.

## `skipFailedChunks` (type: `boolean`):

Keep narrating when a single passage fails after retries, marking the result status 'partial' and listing the gaps. Authentication and quota errors always abort, since every remaining chunk would fail the same way.

## `maxPartMb` (type: `integer`):

Long documents are cut into MP3 parts; a part is finalized and uploaded once it reaches this size. Lower values give more, smaller, chapter-like files. The default is about 15 hours of speech per part.

## `openaiApiKey` (type: `string`):

Your own OpenAI key, required whenever model starts with openai-. OpenAI bills you directly for the speech; this Actor's own charges are unchanged. Not needed for the free edge-tts default. Create one at https://platform.openai.com/api-keys

## `elevenlabsApiKey` (type: `string`):

Your own ElevenLabs key, required whenever model starts with elevenlabs-. ElevenLabs bills you directly for the audio. Not needed for the free edge-tts default. Create one at https://elevenlabs.io/app/settings/api-keys

## `proxyConfiguration` (type: `object`):

Optional proxy applied only to the documentUrl fetch, for hosts that block datacenter IPs. Ignored for uploads, base64 and raw text, and never used for the speech requests.

## `debug` (type: `boolean`):

Raise the log level so per-chunk detail appears in the run log. Useful when reporting a problem.

## Actor input object example

```json
{
  "documentUrl": "https://arxiv.org/pdf/1706.03762",
  "text": "# Chapter 1\n\nIt was the best of times, it was the worst of times...",
  "model": "edge-tts",
  "language": "auto",
  "voice": "en-US-AndrewNeural",
  "speed": 1,
  "instructions": "Speak slowly and deliberately, like an audiobook narrator.",
  "pageRange": "1-10",
  "enableOcr": true,
  "maxCostUsd": 5,
  "chunkSize": 4000,
  "concurrency": 5,
  "resume": true,
  "skipFailedChunks": true,
  "maxPartMb": 40,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "debug": false
}
```

# Actor output Schema

## `overview` (type: `string`):

Start here. One row: status, document title, index page URL, first MP3 URL, duration, part count and cost.

## `audio` (type: `string`):

The same row with every field: audioUrls for all parts, byte size, audio format, characters narrated, pages processed, OCR pages, voice, model and chunk statistics.

## `indexPage` (type: `string`):

Shareable HTML page with an inline player and a download link for every MP3 part.

## `preview` (type: `string`):

Written before any speech is synthesized: pages to process, estimated audio minutes and the ceiling cost in USD.

## `keyValueStore` (type: `string`):

Raw store holding every MP3 part, the INDEX page, the PREVIEW and the OUTPUT record.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "documentUrl": "https://www.orimi.com/pdf-test.pdf",
    "instructions": "Calm, clear audiobook narrator with a neutral accent."
};

// Run the Actor and wait for it to finish
const run = await client.actor("marielise.dev/pdf-to-mp3").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "documentUrl": "https://www.orimi.com/pdf-test.pdf",
    "instructions": "Calm, clear audiobook narrator with a neutral accent.",
}

# Run the Actor and wait for it to finish
run = client.actor("marielise.dev/pdf-to-mp3").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "documentUrl": "https://www.orimi.com/pdf-test.pdf",
  "instructions": "Calm, clear audiobook narrator with a neutral accent."
}' |
apify call marielise.dev/pdf-to-mp3 --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,marielise.dev/pdf-to-mp3"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/63Aragxwin15cqyJ8/builds/evmz4Jah1dMjjj4hR/openapi.json
