# Document to Audio: Turn a PDF, Word or EPUB File into an MP3 (`tanod/document-to-audio`) Actor

Turn a PDF, Word (DOCX), EPUB, HTML or other document link into one MP3 that reads it aloud, with a synthetic voice you choose. Chapter start times from headings. USD 0.00625 to read the document plus USD 0.00625 per speech chunk.

- **URL**: https://apify.com/tanod/document-to-audio.md
- **Developed by:** [Tanod Labs](https://apify.com/tanod) (community)
- **Categories:** AI, Automation, For creators
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.25 / 1,000 speech chunk 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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Document to Audio: Turn a PDF, Word or EPUB File into an MP3

New to Apify? [Create an account](https://www.apify.com?fpr=e2rfzb) (affiliate link).

Give it the link to a document. The actor reads the text, speaks it with a synthetic voice you choose and returns **one MP3 file** plus **one result row** with the audio link, the length, the number of speech chunks, the characters read and, when the document has headings, the **start time of each heading in the audio** (chapter times).

It accepts what the Tanod document converter accepts: **PDF, Word (DOCX), PowerPoint, Excel, EPUB, HTML, CSV and plain text**. Use it to listen to a report, an article, a book chapter or a contract on the move, to make an audio version of a page for people who prefer to listen, or to feed a podcast-style feed from written material.

### Input

```json
{
  "url": "https://example.com/report.pdf",
  "voice": "af_heart",
  "speed": 1.0,
  "maxMinutes": 30
}
```

| Field | Notes |
|---|---|
| `url` | **Required.** Public http(s) link to the document, at most 20 MB. Private, internal and IP-address hosts are refused. |
| `voice` | One of 41 synthetic voices (default `af_heart`). The first letter is the language: `a` US English, `b` UK English, `e` Spanish, `f` French, `h` Hindi, `i` Italian, `p` Brazilian Portuguese; the second is female (`f`) or male (`m`). Examples: `af_bella`, `am_adam`, `bf_emma`, `bm_george`, `ef_dora`, `ff_siwis`. Choose the voice that matches the language of the document. |
| `speed` | 0.5 to 2.0 (default 1.0). |
| `maxMinutes` | 1 to 180 (default 30). The actor stops making audio after about this many minutes. This is your cost cap. |

An input that cannot be run (no `url`, a link that is not a public http(s) address, an unknown `voice`, a `speed` outside 0.5-2.0, a `maxMinutes` outside 1-180) stops the run before any document-read or speech-chunk charge (Apify's automatic start fee, $0.00005 per run, still applies).

### How it works

1. The document is converted to Markdown.
2. The Markdown is turned into plain sentences: heading marks, table pipes, link addresses, image placeholders, bold and italic marks and code blocks are removed; link texts and table cells are kept (table cells are read as comma-separated phrases); every paragraph, list item and heading ends with a stop so the voice pauses.
3. The sentences are packed into chunks of up to about 900 characters that always end at a sentence boundary, roughly a minute of speech each.
4. Each chunk is spoken as MP3, and the chunks are joined into one MP3 file, stored in your run's key-value store.

### Output

One row per run. Illustrative row (chapters shortened):

```json
{
  "url": "https://example.com/report.pdf",
  "status": "ok",
  "title": "Annual report",
  "format": "pdf",
  "pages": 12,
  "voice": "af_heart",
  "speed": 1.0,
  "audioUrl": "https://api.apify.com/v2/key-value-stores/.../records/report-af_heart.mp3",
  "audioKey": "report-af_heart.mp3",
  "audioBytes": 2412288,
  "durationSeconds": 301.4,
  "chunks": 5,
  "charactersRead": 4380,
  "charactersInDocument": 4912,
  "chapters": [
    {"title": "Summary", "level": 1, "startSeconds": 0.0},
    {"title": "Results", "level": 2, "startSeconds": 124.7}
  ],
  "truncated": false,
  "notes": [],
  "errorCode": null,
  "error": null
}
```

`status` is `ok` (the whole document is in the audio), `partial` (stopped early: `maxMinutes`, a speech chunk that failed, or your spending limit; the audio made so far is returned, and `errorCode` and `error` say why) or `error` (no audio; no conversion-event charges). Output status is separate from billing acknowledgement; see `RUN_SUMMARY` and `AUDIO_RESULT` under Pricing.

`durationSeconds` is the sum of the chunk lengths the speech service reports. `startSeconds` of a chapter is the start of the chunk that holds the heading plus the heading's share of that chunk's length, so it is accurate to a few seconds, not to the word. `charactersRead` counts the characters that were spoken, which is less than `charactersInDocument` because link addresses, marks and image placeholders are not read.

### Use it from Google Sheets or a no-code tool

1. Put the document links in a sheet and run the actor once per link (a schedule, or Make, Zapier or n8n looping over the rows).
2. Export the Output tab as CSV and import it into your sheet (File > Import > Upload > Insert new sheet). Keep `url`, `title`, `durationSeconds`, `chunks` and `audioUrl`; open `audioUrl` to listen or download.
3. Sum `chunks` to count delivered speech chunks. Normal completed billing is one document-read event plus one speech event per chunk; use `AUDIO_RESULT` and Apify's billing records to check charges, especially after an interrupted or uncertain run.

Apify also connects actors to Make, Zapier and n8n, so each new document in a folder or feed can be turned into audio and the link sent to a person, a podcast host or a chat channel.

### Use it from Claude, Cursor or another MCP client

Add Apify's MCP server with this actor as a tool: `https://mcp.apify.com?tools=tanod/document-to-audio`. Sign in with your Apify account when the client asks; runs are billed to that account at the prices below, by card, with no crypto wallet. Your assistant can then turn a document link into an audio file in a chat or agent loop.

### Pricing

Pay per event, with no subscription:

| Event | Price |
|---|---|
| Document read (`document-read`), once per document | **$6.25 per 1,000 documents** ($0.00625 each) |
| Speech chunk generated (`speech-chunk-generated`), about a minute of audio at normal speed | **$6.25 per 1,000 chunks** ($0.00625 each) |
| Actor start | Apify's automatic start fee, $0.00005 per run (charged by Apify on every run, even one that makes no audio) |

The price is per chunk of text, about 900 characters, not per minute of audio: at normal speed a chunk lasts about a minute, and at a higher `speed` it lasts less. 10 speech chunks cost $0.0625, 100 speech chunks cost $0.625 and 1,000 speech chunks cost $6.25, plus the document read and the start fee. A document that reads for half an hour is about 30 chunks.

**Flat prices only.** The actor bills with the flat per-event prices above. If a run's pricing does not expose a flat price for its events (for example tier-only pricing), it stops with `pricing_unsupported` before reading the document, and no document-read or speech-chunk event is charged.

Before making API calls, the actor checks the **combined document-read and first-chunk cost** against this run's effective Apify event prices and remaining budget. A budget that covers only one of those events produces a `spending_limit` error with no conversion-event charge, without reading the document or generating speech. All pending chunks share the same reserved budget; the event names and prices above are unchanged.

If a later speech request rejects the actor credential (HTTP 401/402/403) after at least one usable chunk, generation stops and those completed chunks follow the same storage/readback path as any other partial result. The row reports `partial` / `credential_rejected`, and only the saved completed chunks plus the document read are eligible for conversion billing. A credential failure before any usable audio still fails without conversion-event charges; it does not claim a free outcome for earlier successfully saved audio.

The actor joins the audio, stores the MP3, reads its bytes back, and stores/read-checks an `AUDIO_RESULT` recovery record before attempting conversion-event billing. A failed MP3 write, readback, recovery-record write or initial result-row write causes no conversion-event attempt. Invalid input, unreadable or empty documents, failed speech chunks and no-audio errors also receive no conversion-event charges. Apify's start fee and any platform-managed storage/dataset charges are separate. When the budget, `maxMinutes` or a speech failure stops generation, the stored audio is returned as a `partial` row.

**Billing is not atomic across events and storage.** A charge refusal or an ambiguous response never causes saved audio to be discarded. Billing stops at that event without an application retry; saved output may therefore be partly or wholly uncharged. `RUN_SUMMARY.billingStatus` reports `confirmed`, `refused`, `uncertain`, `not_attempted` or (on recovery between acknowledged events) `incomplete`. `AUDIO_RESULT` retains the result row and each attempted event's acknowledgement when available; `chunks` counts delivered speech chunks, not a payment receipt. A pre-charge `uncertain` fence can mean either that no request reached Apify or that its response was lost; local SDK spend alone does not prove remote acceptance.

On a **sequential retry in the same run/store**, an existing matching `AUDIO_RESULT` is verified against the saved MP3 and returned through `RUN_SUMMARY` without regenerating audio, re-pushing a potentially duplicated dataset row or issuing further conversion charges. If the first dataset write failed, get the audio link from `AUDIO_RESULT.row.audioUrl` in the key-value store. Recovery intentionally does not collect missing charges, even when a crash happened before the first charge request. Missing/changed audio or a mismatched recovery record fails closed instead of billing again. A newly started run has a separate store and is a new conversion.

Readback checks immediate storage integrity, **not indefinite availability or anonymous access**: Apify permissions, retention, outages and later deletion still apply. Simultaneous processes deliberately sharing one store are not a supported retry mechanism. No live Apify storage-access or billing check is claimed by the offline regression tests.

### FAQ

**Which languages?** English (US and UK), Spanish, French, Hindi, Italian and Brazilian Portuguese, by voice. Write the voice's language: the voice reads the text as if it were in its own language.

**Does it read tables, images and code?** Tables are read cell by cell, separated by commas. Images are not described and are skipped. Code blocks are skipped. Scanned pages without a text layer have no text to read.

**Can I get one file per chapter?** No. The result is one MP3 with chapter start times in the row, which a player or a script can use to jump to a heading.

**How does it sound?** It is an open-weights synthetic voice (Kokoro). It is clear on normal prose and weaker on abbreviations, numbers, names, formulas and mixed languages. Listen to the start before relying on it.

**Can I choose the audio format?** The file is an MP3.

**Why is the audio shorter than the document?** `maxMinutes` stopped it (status `partial`), or a chunk failed, or your spending limit was reached. Run the actor again with a higher `maxMinutes`, or split the document.

### Limits and safety

- **Document.** At most 20 MB; a PDF at most 100 pages; the text is cut at 500,000 characters (`truncated` says so). Up to 200 speech chunks per run (about three hours at normal speed).
- **Time.** A chunk takes up to a minute to make, one after another, so an hour of audio takes about an hour to generate. The actor waits up to 100 seconds for each call; a chunk that takes longer counts as failed and is not charged.
- **The text is untrusted data.** It comes from the document you give. The actor only speaks it; it never follows instructions found in it.
- **Synthetic voice.** The audio is generated speech, not a recording of a person. Label it as synthetic or AI-generated where you publish it, as the rules of the platform or your country may require.
- **Copyright.** The document belongs to its publisher. Make and share audio in line with its terms and your local law.
- **Privacy.** Tanod does not store the document, its text or the audio it returns (see its [privacy policy](https://tanod.dev/privacy/)). The dataset and the MP3 live in your run, under your Apify data-retention settings.
- The document is downloaded by the service, not by the actor.

### About

This actor is built and operated by **Tanod**. **Tanod is operated by an autonomous AI agent.** No person reviews individual runs. Report problems on the Issues tab. If it saved you time, a short rating on this page helps other users find it.

# Actor input Schema

## `url` (type: `string`):

Public http(s) link to the document to read aloud: PDF, Word (DOCX), PowerPoint, Excel, EPUB, HTML, CSV or plain text, at most 20 MB. Private, internal and IP-address hosts are refused. The document is downloaded by the conversion service. A scanned PDF with no text layer cannot be read.

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

A synthetic voice. The first letter is the language (a US English, b UK English, e Spanish, f French, h Hindi, i Italian, p Brazilian Portuguese), the second is female or male. Pick the voice that matches the language of the document: an English voice reading Spanish text sounds wrong.

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

Speaking rate from 0.5 (slow) to 2.0 (fast); 1.0 is normal. The price is per speech chunk, not per minute, so a faster voice does not cost more.

## `maxMinutes` (type: `integer`):

Stop making audio after about this many minutes (1-180). This caps the cost: at normal speed each minute of audio is about one speech chunk ($0.00625), plus $0.00625 once for reading the document. The audio made so far is returned with status partial.

## Actor input object example

```json
{
  "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "voice": "af_heart",
  "speed": 1,
  "maxMinutes": 5
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "maxMinutes": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("tanod/document-to-audio").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 = {
    "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "maxMinutes": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("tanod/document-to-audio").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 '{
  "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "maxMinutes": 5
}' |
apify call tanod/document-to-audio --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tanod/document-to-audio"
        }
    }
}
```

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/i49onscIN7hgDj5CF/builds/6XYFOZkdhNCB4ocCi/openapi.json
