Persian speech integration

Persian text to speech in Python and JavaScript.

Adding Farsi audio to a lesson or vocabulary app? These examples take a short Persian script through generation, download and saving the recording, with Python or server-side JavaScript.

Two requests: generate, then download

Send the Persian script to POST /v1/speech from your server. The response contains a signed audio URL, its expiry, the file type and the generated duration. Download that URL without the API key and save the file with the correct extension. For Basic recordings that should survive a client disconnect, use a background speech job instead.

Before you start: access, a key and a short script

VowelMarks Developer access is manually approved. Apply in the developer portal, then create an approved test key with the speech scope. Website accounts and API access are separate. Test and live keys have separate allowances; the API plans page explains the available options.

Both examples below run on a server or your own computer. Use Python 3.10 or later, or Node.js 22 or later. Neither needs a third-party package. Keep the key in an environment variable or your deployment's secret store, never in browser code or a public repository.

The script for this example

کتاب تازه روی میز است. نرگس آن را برای کلاس می‌خواند.

An original lesson sentence: “The new book is on the table. Narges reads it for class.”

Download script.txt and save it as script.txt.

Set VOWELMARKS_API_KEY in your shell. Also set a stable request identifier:VOWELMARKS_REQUEST_ID=lesson-001-basic. This value is an idempotency key: for 24 hours, retrying the same request with the same key reuses its recorded outcome instead of starting a second generation. Use a new value when the text, voice or settings change.

Generate and save the recording in Python

Save this as persian-tts.py, or download the complete Python example. Run python3 persian-tts.py script.txt. The default request uses Kian, a Basic voice, at its normal pace.

"""Server-side VowelMarks example. Python 3.10+, no external packages."""
import json
import os
import sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def make_speech(text, request_id, premium=False):
    api_key = os.environ["VOWELMARKS_API_KEY"]
    base = os.environ.get("VOWELMARKS_API_BASE_URL", "https://api.vowelmarks.com")
    payload = {"text": text, "tier": "basic", "voice": "kian", "pace": 1}
    if premium:
        payload = {
            "text": text, "tier": "premium", "voice": "Kore", "pace": 1,
            "style": "instructional", "expressiveness": "calm",
            "director_notes": "Read clearly for an adult Persian lesson. Keep pauses short."
        }
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    request = Request(
        base.rstrip("/") + "/v1/speech", data=body, method="POST",
        headers={
            "Authorization": "Bearer " + api_key,
            "Content-Type": "application/json",
            "Idempotency-Key": request_id,
        },
    )
    try:
        with urlopen(request, timeout=900) as response:
            result = json.load(response)
    except HTTPError as error:
        detail = json.loads(error.read()).get("error", {})
        raise RuntimeError(
            f"HTTP {error.code}: {detail.get('code', 'api_error')}; "
            f"retryable={detail.get('retryable', False)}; "
            f"Retry-After={error.headers.get('Retry-After', 'not supplied')}"
        ) from None

    extensions = {"audio/wav": ".wav", "audio/mpeg": ".mp3"}
    extension = extensions.get(result["mime_type"])
    if extension is None:
        raise RuntimeError("Unexpected audio MIME type")
    # The signed URL authorizes this GET. Do not attach the API key.
    with urlopen(result["audio_url"], timeout=120) as response:
        audio = response.read()
    if not audio:
        raise RuntimeError("The audio download was empty")
    destination = Path("persian-lesson" + extension)
    destination.write_bytes(audio)
    print(f"Saved {destination}: {result['duration_seconds']:.2f} seconds")
    return result


if __name__ == "__main__":
    if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] != "--premium"):
        raise SystemExit("Usage: python3 persian-tts.py script.txt [--premium]")
    # newline="" preserves the input file's line endings.
    with open(sys.argv[1], encoding="utf-8", newline="") as source:
        text = source.read()
    make_speech(text, os.environ["VOWELMARKS_REQUEST_ID"], premium=len(sys.argv) == 3)

A successful run saves persian-lesson.wav or persian-lesson.mp3and prints the duration. Open the saved file and listen to both sentences. The API response is not the recording itself: the second HTTP request retrieves the audio bytes.

The same task in server-side JavaScript

Save this as persian-tts.mjs, or download the Node.js example. Run node persian-tts.mjs script.txt with the same environment variables. This is not a script to paste into your website's HTML.

// Server-side VowelMarks example. Node.js 22+, no external packages.
import { readFile, writeFile } from "node:fs/promises";

export async function makeSpeech(text, requestId, premium = false) {
  const apiKey = process.env.VOWELMARKS_API_KEY;
  if (!apiKey) throw new Error("Set VOWELMARKS_API_KEY on your server");
  const base = process.env.VOWELMARKS_API_BASE_URL ?? "https://api.vowelmarks.com";
  const payload = premium ? {
    text, tier: "premium", voice: "Kore", pace: 1,
    style: "instructional", expressiveness: "calm",
    director_notes: "Read clearly for an adult Persian lesson. Keep pauses short."
  } : { text, tier: "basic", voice: "kian", pace: 1 };
  const response = await fetch(base.replace(/\/$/u, "") + "/v1/speech", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": requestId
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(900_000)
  });
  const result = await response.json();
  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${result.error?.code ?? "api_error"}; ` +
      `retryable=${result.error?.retryable === true}; ` +
      `Retry-After=${response.headers.get("Retry-After") ?? "not supplied"}`
    );
  }
  const extension = { "audio/wav": ".wav", "audio/mpeg": ".mp3" }[result.mime_type];
  if (!extension) throw new Error("Unexpected audio MIME type");
  // No Authorization header: the signed URL authorizes this GET.
  const audioResponse = await fetch(result.audio_url, {
    signal: AbortSignal.timeout(120_000)
  });
  if (!audioResponse.ok) throw new Error(`Audio download: HTTP ${audioResponse.status}`);
  const audio = Buffer.from(await audioResponse.arrayBuffer());
  if (!audio.length) throw new Error("The audio download was empty");
  const destination = "persian-lesson" + extension;
  await writeFile(destination, audio);
  console.log(`Saved ${destination}: ${result.duration_seconds.toFixed(2)} seconds`);
  return result;
}

if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) {
  const [, , filename, flag] = process.argv;
  if (!filename || (flag && flag !== "--premium")) {
    throw new Error("Usage: node persian-tts.mjs script.txt [--premium]");
  }
  const requestId = process.env.VOWELMARKS_REQUEST_ID;
  if (!requestId) throw new Error("Set a stable VOWELMARKS_REQUEST_ID for this request");
  await makeSpeech(await readFile(filename, "utf8"), requestId, flag === "--premium");
}

In a web app, your browser calls your own server, and that server calls VowelMarks. This lets you decide who can generate audio and how much they can use while keeping the API key private. A browser security error is not a reason to expose that key.

Try Premium with a direction that fits the lesson

Run either example with --premium, using a new request key such aslesson-001-premium. It selects Kore with an instructional style and calm expression. The director's notes ask for clear delivery and short pauses. Premium uses your organization's Premium speech allowance, separate from Basic.

The prompt describes the delivery, not replacement narration. Keep the words to be spoken intext; keep instructions in director_notes. Basic does not accept Premium style or director-note fields. For more useful directions than “sound natural,” see the Persian voice-direction guide.

Use a background job for longer Basic recordings

For a longer Basic recording, create a job with POST /v1/speech/jobs instead of holding the generation request open. The service returns 202 Acceptedwith a job ID and a status_url. Store that ID with your own lesson record.

curl https://api.vowelmarks.com/v1/speech/jobs \
  -H "Authorization: Bearer $VOWELMARKS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lesson-001-basic-job" \
  --data '{"text":"کتاب تازه روی میز است.","tier":"basic","voice":"kian","pace":1}'
  1. Keep the returned job ID. Do not create another job because the user reloaded the page.
  2. Read status_url with the same server-side Bearer key. Space out polling and honor rate-limit responses.
  3. Wait for succeeded. A queued or running job is not a downloadable recording.
  4. Download audio_url without the key. Use result.mime_type to choose the extension.
  5. For failed or cancelled, stop and handle that status rather than waiting forever.

A closed connection is not job cancellation. The job reference explains explicit cancellation, retention and refreshed download URLs. Use the synchronous endpoint for Premium; this Basic job flow does not promise Premium background generation or a public captions API.

Retry a request without generating the recording twice

The examples send one generation request and surface API errors rather than retrying indefinitely. If a request times out after being sent, its outcome is unknown. Retry the exact body and endpoint with the original key. Creating a new key can start a new generation. The same key with changed text or settings produces an idempotency conflict.

  • Authentication or permission error: fix the key or its speech scope.
  • Quota exhausted: check the relevant Basic or Premium allowance; repeating the request will not refill it.
  • Retryable capacity or rate-limit error: honor Retry-After, retry sequentially and stop after a small bounded number of attempts.
  • Audio download interrupted: retry the file GET while the signed URL is valid. Do not regenerate merely to fetch the same file.

Signed audio URLs last one hour. Keep them out of logs and analytics, and save completed audio in storage you control if the lesson should work next week. Requests allow up to 6,000 Unicode code points, subject to the 32 KiB JSON-body limit. Those are API limits, not a guarantee that a large narration will be quick or sound consistent.

Test the Persian, not just the status code

Before connecting a whole lesson library, listen to a few recordings. Include the names, numbers and vocabulary your app uses. Start with the reusable Persian pronunciation test, then add sentences from your actual lesson or product. For a separate marked-text display, follow the source-preserving diacritization tutorial.

Ready to connect your own app? Compare the Persian API options and apply for Developer access. For a single recording without code, use the Persian text-to-speech web tool.

Frequently asked questions

Can I generate Persian text to speech in Python without an SDK?

Yes. The example uses Python's standard-library HTTP client to send a VowelMarks API request, read the JSON result and download the audio. You need approved API access and a key with speech permission.

Can I put my VowelMarks API key in a JavaScript website?

No. Run the JavaScript example in Node.js on your server. Your frontend should call your own backend, not carry a Developer API key.

Does the Persian TTS API return MP3 or WAV?

Check mime_type in the response. Basic can return audio/wav or audio/mpeg; Premium returns audio/wav. The web tool's export choices are a separate interface.

Can I queue Premium speech as a background API job?

Use POST /v1/speech for Premium. The public background-job workflow described here is for Basic speech.

Sources and review notes

Sources are listed for the claims they support. Original practice examples are identified in the article. Product capabilities and access terms were checked on the review date and can change.

  1. VowelMarks Speech API reference. Request fields, supported voices, Basic jobs, download formats and audio URL lifetime.
  2. VowelMarks authentication and errors. Server-side keys, safe retries, request limits and structured errors.
  3. VowelMarks OpenAPI v1. The machine-readable request and response contract used by these examples.
  4. Narakeet's Python TTS tutorial. An editorial reference for completing the request-to-audio task. The VowelMarks code and Persian script here are original; the APIs are not interchangeable.