Skip to content

Tools

A Free Text-to-Speech API for Bulk Voiceovers: CSV In, MP3 and SRT Out

8 min readBy VocalLab Team

A Free Text-to-Speech API for Bulk Voiceovers

VocalLab has a free text to speech API: access costs nothing on any of the 4 plans, Free included, and you pay only in points. A request costs ⌈characters ÷ 15⌉, so 1,000 voiceover files of 600 characters each come to 40,000 points. This post builds the whole job: it reads a CSV, calls POST /api/v1/tts once per row, saves an MP3 plus an SRT for each, and keeps the loop under 60 requests a minute.

There is no batch endpoint, so "bulk" means a loop you write yourself. It's about fifty lines of Python.

If you want the big picture first, read the launch post. Every field used below is also listed in the API reference.

A free text to speech API: what 1,000 files cost in points

Settle the cost first. A bulk job that runs dry halfway is a pain to resume. One point is about one second of audio, so take 1,000 product blurbs of 600 characters each: ⌈600 ÷ 15⌉ = 40 points per call. That's 40,000 points for the run, roughly 11 hours of audio.

Run that text through the app at ÷17 instead and it would be ⌈600 ÷ 17⌉ = 36 points a file, 36,000 in total. So the API premium on this job is 4,000 points. Whether that matters depends on the alternative, which is pasting 1,000 blurbs into a browser one at a time.

Balance sourcePointsFiles of 600 characters
Free plan (one-time)601
Lite, per month3,00075
Pro, per month12,000300
Max, per month150,000all 1,000, with 110,000 points left
$119 top-up (never expires)60,000all 1,000, with 20,000 points left

Cheaper models: v-lite costs half the points (0.5×) and v-flash three quarters (0.75×). v-lite covers major languages only, so run 20 rows through it and listen before you commit 1,000.

Budget for re-takes too. Every regeneration is billed again, and a typo found at file 700 means paying for file 700 twice. Proofread the CSV before the run, not after.

Get a key and a voice id

  1. Create an API key

    Sign in and make a key on the API keys page of your account. It starts with vl_live_. Export it as VOCALLAB_API_KEY so it never lands in your script.

  2. Find a voice id

    GET /voices returns the catalog plus your own clones and designed voices. Each row has an id; pass that as voice. Add q= to search and limit= to page.

  3. Check the key

    GET /ping confirms the key works and returns your points balance. Do it before a long run.

curl -s "https://api.vocallab.ai/api/v1/voices?q=articulate&limit=5" \
  -H "Authorization: Bearer $VOCALLAB_API_KEY"
Search the voice list. The response is { voices: [...], total, has_more }.

Pick one voice and keep it for the whole batch. A narrator that changes at row 400 is the kind of mistake you only hear after the run. If the files belong in a product, something like the clear support voice is a safe default for short prompts.

One request returns audio and captions

Send text and voice, and the response carries the audio inline as audio_base64. Add "captions": "phrase" and the same response includes an SRT string, with no second call. Use "word" instead for one cue per word.

Five response fields matter here: audio_base64 and captions, plus points_used, format and stream_url. Should audio_base64 ever come back null, stream_url serves the finished file instead, and it waits up to about 15 seconds for the upload to land.

curl -s -X POST https://api.vocallab.ai/api/v1/tts \
  -H "Authorization: Bearer $VOCALLAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Your order has shipped and should arrive on Friday.",
    "voice": "VOICE_ID",
    "model": "v-studio",
    "format": "MP3",
    "captions": "phrase"
  }' -o response.json

jq -r .audio_base64 response.json | base64 -d > shipped.mp3
jq -r .captions response.json > shipped.srt
jq .points_used response.json
A single call. Requires jq and a shell with base64.

Bulk text to speech from a CSV

Save this as bulk_tts.py next to a scripts.csv that has two columns, name and text. Each row becomes one or more calls, and every call writes name_01.mp3 and name_01.srt into out/.

import base64, csv, os, re, sys, time
import requests

API = "https://api.vocallab.ai/api/v1"
HEAD = {"Authorization": "Bearer " + os.environ["VOCALLAB_API_KEY"]}
VOICE = "VOICE_ID"   # an id from GET /voices
LIMIT = 1900         # the API takes 2,000 characters; keep a margin

def split(text):
    """Break a long script at sentence ends so no call exceeds LIMIT."""
    parts, cur = [], ""
    for sentence in re.split(r"(?<=[.!?])\s+", text.strip()):
        if cur and len(cur) + len(sentence) + 1 > LIMIT:
            parts.append(cur)
            cur = sentence
        else:
            cur = (cur + " " + sentence).strip()
    if cur:
        parts.append(cur)
    return parts

def speak(text):
    while True:
        r = requests.post(API + "/tts", headers=HEAD, timeout=120, json={
            "text": text, "voice": VOICE, "model": "v-studio",
            "format": "MP3", "captions": "phrase",
        })
        if r.status_code == 429:      # rate limited: wait and retry
            time.sleep(15)
            continue
        if r.status_code == 402:      # out of points: stop cleanly
            sys.exit("Out of points. Top up and rerun; finished files are skipped.")
        r.raise_for_status()
        return r.json()

os.makedirs("out", exist_ok=True)
with open("scripts.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):            # columns: name,text
        for i, part in enumerate(split(row["text"]), 1):
            base = "out/%s_%02d" % (row["name"], i)
            if os.path.exists(base + ".mp3"):
                continue                      # already done on a previous run
            d = speak(part)
            if d.get("audio_base64"):
                audio = base64.b64decode(d["audio_base64"])
            else:                             # fall back to the stream URL
                audio = requests.get(d["stream_url"], headers=HEAD, timeout=60).content
            with open(base + ".mp3", "wb") as out:
                out.write(audio)
            with open(base + ".srt", "w", encoding="utf-8") as out:
                out.write(d.get("captions") or "")
            print(base, d["points_used"], "points")
            time.sleep(1.1)                   # stay under 60 requests a minute
bulk_tts.py. Needs the requests package.

A few choices in there are deliberate. Rows over 1,900 characters are split at sentence ends, because one request takes at most 2,000 characters and a script that tries a 2,400-character row gets a 413 back. A single sentence longer than 1,900 characters will still fail, so break those by hand.

  • Resumable. Files that already exist are skipped, so a rerun after a crash costs nothing for finished rows.
  • Stops on 402. That status means the balance is short. The script exits instead of hammering the API.
  • Retries on 429. Waiting 15 seconds clears a rate-limit hit.
  • Plain MP3. Other formats exist, but check the format field in the response to see what your plan returned.

Writing the CSV so the audio comes out right

Most bad bulk output comes from the spreadsheet, not the API. A cell that holds "SKU 4471-B, 2 pcs, 15% off" leaves the voice guessing how to say each piece. Write the row the way you want it heard: "S K U four four seven one B, two pieces, fifteen percent off". It's tedious once and pays back on all 1,000 rows.

Strip HTML tags and stray markdown before the run, since every character counts toward the bill and a pasted <br> or a row of asterisks is points spent on noise. One find-and-replace pass over the text column takes a minute.

  • One row, one file

    Keep a single spoken unit per row. If a blurb has two parts you may want to swap later, make them two rows.

  • A voice column

    Add a voice column and read row["voice"] instead of the constant if different rows need different speakers. Every id still comes from GET /voices.

  • Safe file names

    The name column becomes a file name, so stick to lowercase letters and hyphens. A slash in a name creates a folder you didn't plan for.

Do a balance check before you start. Sum ⌈length ÷ 15⌉ over every row, call GET /me or GET /ping for your balance, and stop if the estimate is larger. Rows past the limit would fail with a 402 anyway, but you'd rather know before the first call than after the 600th.

Staying under 60 requests a minute

Expect 60 requests a minute per key, or 120 on Max. At that ceiling 1,000 calls need at least 17 minutes. Real runs take longer, because each call also waits for the audio to be generated. A 1.1 second pause between calls keeps a sequential loop under the limit with room to spare.

Don't reach for threads to speed it up. The limit counts per key, not per connection, so parallel workers just trade speed for 429s. If 17 minutes is too slow for your volume, the Max tier doubles the ceiling.

Test ten rows before the full run

Cut the CSV to ten rows, run it, and listen to three files. Add up the points_used values and compare them with ⌈characters ÷ 15⌉ for those rows. If the numbers disagree, you've found out for the price of 400 points instead of 40,000.

Open an SRT too. Phrase mode groups words into readable lines, which suits video captions. If the files are for a phone menu or an app, drop the SRT and save the disk space.

What the API doesn't do

No streaming, no webhooks, no batch endpoint and no SLA. A request returns a finished file, and you poll or loop. Each split part also gets its own SRT with timings that start at zero, so joining parts means shifting the later cue times yourself. Generations stay in your history; DELETE /tts/:id removes one if you'd rather not keep them.

For jobs that look like phone prompts or notification lines, the bulk voiceover generation page covers planning.

Access rules and pricing for the API in general sit on the AI voice API page.

Frequently asked questions

Is the VocalLab text-to-speech API really free?

Access is free on every plan, Free included, and there is no per-call fee. You spend points: ⌈characters ÷ 15⌉ per request, with one point about one second of audio. The Free plan starts with 60 points.

How many characters can one API request take?

Up to 2,000 characters. Longer scripts need to be split into several requests, ideally at sentence ends.

What is the rate limit for bulk jobs?

60 requests a minute per API key, and 120 on Max. 1,000 requests therefore take at least 17 minutes at the base limit.

Can I get SRT captions in the same call?

Yes. Add "captions": "phrase" or "word" to the POST /tts body and the response includes the SRT text. You can also fetch it later from GET /tts/:id/captions.

How much do 1,000 files cost?

Take the average characters per file, divide by 15, round up, and multiply by 1,000. At 600 characters that is 40 points a file and 40,000 points in total.

Run ten rows today

Create a key, paste the script, and see your own points_used numbers before you scale.