# Text to Speech - Bulk TTS to MP3, Subtitles, Articles & Texts (`tidytools/text-to-speech`) Actor

Turn texts and article URLs into natural speech: one MP3 (or Opus, WAV) file per text with optional SRT/VTT subtitles, in English, Spanish, French, Chinese, Japanese or Korean. $0.008 per 1,000 characters; empty and failed items are free; no start fee.

- **URL**: https://apify.com/tidytools/text-to-speech.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.40 / 1,000 1,000 characters spokens

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Text to Speech: Bulk TTS to MP3 with Subtitles, for Texts and Articles

Turn texts, scripts and web articles into natural-sounding speech. Paste one long text, a list of texts, or article URLs, and get **one MP3 file per item** (or Ogg Opus, or WAV) in the run's key-value store, optionally with **SRT and WebVTT subtitles**, and a dataset row holding the file links, the length in seconds and the number of characters read.

- **Small files that play everywhere:** MP3 at 64 kbit/s is about 0.5 MB per minute (11x smaller than WAV); Ogg Opus at 32 kbit/s about 0.25 MB per minute (20x smaller). WAV is still available.
- **Subtitles:** `.srt` and `.vtt` files timed from the generated speech itself, ready for video editors and web players. No extra charge.
- **6 languages:** English, Spanish, French, Chinese (Mandarin), Japanese, Korean. Set a default, or a language per text.
- **Long texts welcome:** texts are split at sentence ends, spoken part by part and joined into one file with natural pauses. Up to 100,000 characters per text.
- **Articles from URLs:** the page's main article text is read (title first), without menus, link lists, image captions in brackets, citation marks or code blocks.
- **Fast:** in our tests about 4,000 characters (almost 4 minutes of English speech) took 11–27 seconds to speak; compressing a minute of speech takes 1–3 seconds more.
- **Pay only for what is read:** $0.008 per 1,000 characters, whatever the format and with or without subtitles. Empty texts, unreadable pages, invalid items and failures are free. No start fee.

Speech is generated with the open MeloTTS model (MyShell.ai) on Cloudflare Workers AI. Every plan, including Apify's free plan, uses the same voice and formats.

### Input

One text (the "Text" field), as MP3 with subtitles:

```json
{ "text": "Hello from TidyTools. This text was turned into speech in a few seconds.", "subtitles": true }
```

A list of texts, each with an optional `id` (names the file and the row) and `language`:

```json
{
  "texts": [
    { "id": "zh-news", "text": "今天台北天氣晴朗，氣溫攝氏二十五度。", "language": "zh" },
    { "id": "ja-greeting", "text": "おはようございます。本日の会議は午後三時から始まります。", "language": "ja" },
    "A plain string uses the default language."
  ],
  "language": "en",
  "audioFormat": "opus"
}
```

Article URLs:

```json
{ "urls": ["https://en.wikipedia.org/wiki/Speech_synthesis"], "maxCharsPerText": 4000 }
```

| Field | Default | Meaning |
|---|---|---|
| `text` | – | One text to read aloud. |
| `texts` | – | Several texts: strings, or `{ "id", "text", "language" }` objects. |
| `urls` | – | Web pages whose main article text is read aloud (one file per page). |
| `language` | `en` | `en`, `es`, `fr`, `zh`, `ja` or `ko`. The text should be written in that language. |
| `audioFormat` | `mp3` | `mp3` (64 kbit/s mono, plays everywhere), `opus` (Ogg Opus, 32 kbit/s mono, smallest; plays in browsers, Android, VLC) or `wav` (16-bit mono, uncompressed). Same price. |
| `subtitles` | `false` | Also save `<file>.srt` and `<file>.vtt` subtitles for each item. Same price. |
| `sampleRate` | `44100` | WAV only: `44100` (as generated, about 5.3 MB per minute) or `22050` (half the size). |
| `maxCharsPerText` | 20,000 | Longer texts and pages are cut at the last sentence before the limit (`truncated: true`). Only the characters read are charged. |
| `maxConcurrency` | 2 | Items spoken in parallel (1–5). |
| `httpVia`, `proxyConfiguration` | auto / none | Only for `urls`: how pages are fetched when a site blocks data centers. |

All three sources can be combined in one run (up to 1,000 items).

### Output

One audio file per item in the default key-value store (`audio-001-<name>.mp3`, `.opus` or `.wav`), with `audio-001-<name>.srt` and `.vtt` next to it when `subtitles` is on, and one dataset row per item.

An MP3 row with subtitles (the layout of every successful row; the size is what our encoder writes for this 4.25-second sentence):

```json
{"index":0,"input":"text","source":"text","success":true,"language":"en","chars":72,"durationSec":4.25,
 "audioUrl":"https://api.apify.com/v2/key-value-stores/<store>/records/audio-001-Hello-from-TidyTools-This-text-was-turn.mp3",
 "fileKey":"audio-001-Hello-from-TidyTools-This-text-was-turn.mp3","format":"mp3","sampleRate":44100,"bitrateKbps":64,"bytes":34272,
 "srtUrl":"https://api.apify.com/v2/key-value-stores/<store>/records/audio-001-Hello-from-TidyTools-This-text-was-turn.srt",
 "vttUrl":"https://api.apify.com/v2/key-value-stores/<store>/records/audio-001-Hello-from-TidyTools-This-text-was-turn.vtt","cueCount":2,
 "truncated":false,"textPreview":"Hello from TidyTools. This text was turned into speech in a few seconds.","synthesisSec":2,"charged":true,"chargedUnits":1}
```

A real WAV row from our test runs (`"audioFormat": "wav", "sampleRate": "22050"`):

```json
{"index":0,"input":"https://en.wikipedia.org/wiki/Speech_synthesis","source":"url","title":"Speech synthesis - Wikipedia","success":true,
 "language":"en","chars":3983,"originalChars":48538,"truncated":true,"durationSec":230.21,"fileKey":"audio-001-Speech-synthesis-Wikipedia.wav",
 "format":"wav","sampleRate":22050,"bytes":10152498,"synthesisSec":26.9,"charged":true,"chargedUnits":4}
```

The same 230 seconds as MP3 would be about 1.8 MB, as Opus about 0.9 MB.

```json
{"input":"german","id":"german","success":false,"errorType":"invalid_input","charged":false,"error":"language \"de\" is not supported (use en, es, fr, zh, ja, ko) (not charged)"}
```

Subtitles (`.srt`; the `.vtt` has the same cues):

```
1
00:00:00,000 --> 00:00:01,395
Hello from TidyTools.

2
00:00:01,395 --> 00:00:04,250
This text was turned into
speech in a few seconds.
```

| Field | Meaning |
|---|---|
| `audioUrl`, `fileKey` | Download link and key of the audio file in the run's key-value store. |
| `format`, `bitrateKbps`, `sampleRate`, `bytes` | `mp3`, `opus` or `wav`; bitrate (MP3 and Opus); sample rate of the file (MP3 44,100, Opus 48,000, WAV 44,100 or 22,050); file size. |
| `srtUrl`, `vttUrl`, `cueCount` | With `subtitles`: links to the SRT and WebVTT files and the number of cues. |
| `durationSec` | Length of the speech in seconds. |
| `chars` | Characters read aloud (after clean-up and any truncation); `originalChars` when truncated. |
| `language` | Voice language. |
| `source`, `input`, `id`, `index` | Where the item came from (`text` or `url`), the original input, your id, its position. |
| `title`, `url`, `finalUrl` | For URLs: the page title and address. |
| `textPreview` | The first 200 characters that were read. |
| `synthesisSec` | Seconds taken to speak and encode the item. |
| `charged`, `chargedUnits` | Whether the item was charged, and how many 1,000-character units. |
| `errorType`, `error` | On free rows: `invalid_input` (empty text, unsupported language, not a URL), `no_text` (page without article text), `not_found`, `blocked`, `timeout`, `failed`… |

`SUMMARY` (key-value store) has `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `LIMIT_REACHED`…), `spoken`, `failed`, `charactersSpoken`, `audioSeconds`, `chargedUnits`, `audioFormat`, `bitrateKbps` or `sampleRate`, `audioBytes`, `encodeSeconds`, `subtitles`, `costPlan`, `notProcessed` (when the max charge stopped the run) and `failuresByErrorType`.

### How the subtitles are timed

The speech service reports the measured length of every chunk it speaks (about 400 characters, cut at sentence ends), and the pauses between chunks and parts are known, so each chunk's subtitles start exactly where its audio starts. Inside a chunk, the cues (a sentence, or a phrase that fits two lines of 42 characters; 16 for Chinese, Japanese and Korean) share the chunk's time in proportion to their length, so a cue can drift by a fraction of a second within its chunk. MP3 files start with the usual 25 ms of encoder delay.

### Control your cost

- **$0.008 per started 1,000 characters** read aloud, per item: a 72-character sentence is 1 unit ($0.008), a 3,983-character article is 4 units ($0.032). The format and the subtitles do not change the price. No start fee.
- About 1,000 English characters make one minute of speech (measured: 3,983 characters → 230 s). Chinese, Japanese and Korean are denser: about 5–6 characters per second, so 1,000 characters make roughly 3 minutes.
- Free: empty texts, text with nothing to read, unsupported languages, invalid URLs, pages without readable text, failed items.
- At the start the log and status show the worst case (units × price; URL pages are counted at up to 10,000 characters until read). When your max charge per run is reached, the remaining items are not processed and are listed in `SUMMARY.notProcessed`.
- For comparison (checked October 2026): `marielise.dev/pdf-to-mp3` on Apify charges $0.04 per audio minute plus $0.05 per run.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/text-to-speech) to Claude, Cursor or any MCP client, then ask e.g. "Read this article aloud and give me an MP3 with subtitles: https://…". Minimal input: `{"text":"Hello world."}`. Failed items are not charged and carry an `errorType`.

### Works well with

- **Audio to Text (audio-transcriber):** transcribe a recording, edit the text, and turn it back into clean speech.
- **Page Summarizer:** summarize long articles first, then listen to the summary.

### FAQ

**Can I clone a voice or pick a different speaker?** No. There is one standard voice per language (the speech model offers no speaker or speed choice).

**Which format should I pick?** MP3 for anything (podcast hosts, video editors, phones). Opus for the smallest files in browsers and apps. WAV when an editor needs uncompressed audio.

**How are MP3 and Opus made?** In the Actor, in pure JavaScript and WebAssembly: our own MP3 encoder (MPEG-1 Layer III, built from the standard's tables) and libopus (BSD license). No ffmpeg or LAME.

**Can I use the audio commercially?** MeloTTS is released under the MIT license; check your own use case and the source text's rights.

**Is there a length limit?** `maxCharsPerText` (default 20,000, up to 100,000) per text; up to 1,000 items per run.

### Limits

- Voice quality is good and clear but below premium voices such as ElevenLabs; names, abbreviations and mixed-language text may be mispronounced.
- One voice per language (no speed, pitch or speaker choice).
- Subtitle timing is exact at chunk boundaries and estimated within a chunk (see above).
- Pages that need a login, or block automated reading, return a free failed row.

# Actor input Schema

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

One text to read aloud (any length up to "Max characters per text"; long texts are split at sentence ends and joined into one audio file). For several texts use "Texts" below.

## `texts` (type: `array`):

Several texts, one audio file each: plain strings, or objects {"id": "intro", "text": "...", "language": "es"} (id names the file and the row; language overrides the default for that text).

## `urls` (type: `array`):

Web pages to read aloud: the page's main article text (title first) is read, without menus, links or code blocks. One audio file per page.

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

Language of the voice. The text should be written in this language. Per-text overrides go in "Texts" ("language").

## `audioFormat` (type: `string`):

Format of the audio files (mono). The price is the same for every format.

## `subtitles` (type: `boolean`):

Also save an .srt and a .vtt subtitle file per item, timed from the generated speech (for videos and web players). No extra charge.

## `sampleRate` (type: `string`):

Only for the WAV format: the smaller option halves the file size. MP3 and Opus ignore it. The price is the same.

## `maxCharsPerText` (type: `integer`):

Longer texts and pages are cut at the last sentence before this limit (the row says truncated: true). Only the characters read aloud are charged. About 1,000 English characters make one minute of speech.

## `maxConcurrency` (type: `integer`):

How many texts or pages are spoken in parallel.

## `httpVia` (type: `string`):

Only for Article URLs. Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser.

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

Only used for Article URLs requested from Apify's network. Apify proxy usage is billed to your Apify account.

## Actor input object example

```json
{
  "text": "Hello from TidyTools. This text was turned into speech in a few seconds. Paste an article, a script or a list of texts, and get one audio file for each.",
  "language": "en",
  "audioFormat": "mp3",
  "subtitles": false,
  "sampleRate": "44100",
  "maxCharsPerText": 20000,
  "maxConcurrency": 2,
  "httpVia": "auto"
}
```

# Actor output Schema

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

No description

## `files` (type: `string`):

No description

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

No description

## `full` (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 = {
    "text": "Hello from TidyTools. This text was turned into speech in a few seconds. Paste an article, a script or a list of texts, and get one audio file for each."
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/text-to-speech").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 = { "text": "Hello from TidyTools. This text was turned into speech in a few seconds. Paste an article, a script or a list of texts, and get one audio file for each." }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/text-to-speech").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 '{
  "text": "Hello from TidyTools. This text was turned into speech in a few seconds. Paste an article, a script or a list of texts, and get one audio file for each."
}' |
apify call tidytools/text-to-speech --silent --output-dataset

```

## MCP server setup

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

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/7z3qAZcuvDMYZN34E/builds/yxLbNokR3tWgiOwji/openapi.json
