Migrate from OpenAI TTS to EasyVoice in 5 lines
Migrating off OpenAI's TTS API is, in practice, a 5-line code change. The request body shape — voice, input text, response format — is identical between vendors. The auth pattern (Bearer token in the Authorization header) is identical. Response handling (raw audio bytes returned as a complete file) works the same way. Only three things change: the URL (api.openai.com → easyvoice.ae), the voice ID (alloy → af_alloy and the rest of the mapping table), and the auth key environment variable. This guide walks through the full migration on a real openai.audio.speech.create call: the before/after Python diff, the equivalent JavaScript / fetch diff, model parameter mapping (tts-1 / tts-1-hd → kokoro), and response_format compatibility (mp3 and wav). Every code sample below uses real, working endpoints — paste them into a terminal with the right API keys set and they run.
5,000 characters per day free, no credit card. Pro $9.99/mo unlimited vs OpenAI $15/1M (tts-1) / $30/1M (tts-1-hd).
The 5-line diff — Python
Existing OpenAI code uses the official openai Python SDK, which is hardcoded to api.openai.com for the audio.speech endpoint and doesn't support a custom base URL override. The pragmatic migration path is to swap the SDK call for a stdlib requests POST, which is 5 lines of code and removes one dependency. The before/after below is the literal diff — copy the 'after' block into your codebase, set EASYVOICE_API_KEY, and you're done.
The five changes: (1) Import requests instead of openai. (2) Change the URL from the SDK's implicit api.openai.com/v1/audio/speech to https://easyvoice.ae/api/v1/audio/speech. (3) Set the Authorization header explicitly to Bearer ${EASYVOICE_API_KEY}. (4) Replace the voice name using the mapping table (alloy → af_alloy, etc.). (5) Drop the model parameter (tts-1 / tts-1-hd) — EasyVoice doesn't gate fidelity by model name, so the parameter is optional and ignored if sent. Everything else (response_format, input text, error handling pattern) stays identical.
The 5-line diff — JavaScript / TypeScript
JavaScript apps using the OpenAI Node SDK face the same constraint — the SDK is hardcoded to api.openai.com. Replace the openai.audio.speech.create() call with a fetch POST and the migration is the same 5 lines: import gone (fetch is global in Node 18+), URL changed, Authorization header set explicitly, voice name remapped, model parameter dropped. The arrayBuffer() → Buffer.from() pattern handles the audio response identically to how the OpenAI SDK exposes response.arrayBuffer() on its side.
For frameworks: Next.js Route Handlers should call EasyVoice server-side, not from the client, to avoid leaking the API key to the browser. Express, Hono, Fastify, and Bun's built-in HTTP server all use the same fetch pattern. If your existing OpenAI integration runs inside a Cloudflare Worker, Vercel Edge Function, or other edge runtime, the EasyVoice migration runs there too — no special edge-compatibility flags needed.
Model parameter mapping
OpenAI's API accepts model: 'tts-1' (faster, ~$15/1M chars) or model: 'tts-1-hd' (higher fidelity, ~$30/1M chars). EasyVoice runs Kokoro-82M as a single model serving all voices — fidelity isn't gated by a model name. If you send model: 'tts-1' or model: 'tts-1-hd' or model: 'kokoro' in your request body, EasyVoice ignores it gracefully (no 400 error, the request succeeds). This is intentional: it means existing OpenAI-shaped code with the model parameter still set works as-is, no defensive code needed.
If you want to be explicit, send model: 'kokoro' in the request. We accept it. We also accept model: 'tts-1' and model: 'tts-1-hd' for drop-in compatibility — you don't need to scrub those out of your codebase pre-migration. The point of accepting any model value is to remove a migration step. In blind A/B tests against OpenAI tts-1, Kokoro-82M is indistinguishable for prose-length narration. Against tts-1-hd, OpenAI's higher-fidelity model occasionally wins on close-mic'd character voice work; for the dominant TTS use cases (accessibility, chatbot, content audio versions, IVR), the difference is rarely perceptible.
Voice parameter mapping
The full 6-row mapping table lives on the /voices spoke. Inline reference: alloy → af_alloy (warm female mid-range, closest direct match), echo → am_echo (neutral male, news-reader register), fable → bm_fable (British male storytelling voice), onyx → am_onyx (deep American baritone, cinematic register), nova → af_nova (bright energetic female), shimmer → bf_lily (soft female with airy upper register, subtle British placement). For new code, write the mapping as a constant dictionary at the top of your TTS service file — see the code sample below.
If your existing code uses a runtime voice selector (different voice per user, different voice per content type, A/B testing voices for conversion), wrap the mapping in a function that takes an OpenAI voice name and returns the Kokoro equivalent. The wrapper is one function call wide; it doesn't add a measurable latency hop. After the migration is stable and you've shipped the EasyVoice voice names natively, you can delete the wrapper — but the wrapper is the safe path for the migration itself.
Response format compatibility — mp3, wav
EasyVoice accepts response_format: 'mp3' (default) and 'wav'. OpenAI additionally accepts opus, aac, flac, and pcm — those formats are not supported on EasyVoice; unsupported formats fall back to mp3 rather than returning a 400 error. The byte-level mp3 and wav output is functionally identical between vendors: an mp3 file from OpenAI plays in the same player as an mp3 file from EasyVoice, with the same MIME type (audio/mpeg) and the same downstream library compatibility. The difference is encoding parameters — OpenAI's tts-1 mp3 is 96 kbps, tts-1-hd is 128 kbps; EasyVoice's mp3 is 128 kbps. Files are slightly different sizes for the same audio length, but bit-for-bit content compares as you'd expect.
wav is the right choice for downstream audio processing pipelines — mixing into a podcast track, applying filters in Audacity, importing into a DAW. Both vendors return raw audio bytes — there's no JSON envelope to parse, no base64 to decode. response.arrayBuffer() (JS), response.content (Python requests), or response.body in any other language returns the audio directly. The full file is returned in one response; write it to a file or feed it to a player once received.
Response handling — receiving the complete audio file
EasyVoice's endpoint returns the complete audio file in one response — the full MP3 or WAV buffer is synthesized server-side and returned once Kokoro finishes. There is no chunked/streaming endpoint; the response body contains the complete file. Because Kokoro synthesis is fast (~1.1 ms/char on GPU), typical requests return in a few seconds for standard narration lengths. Read the response as a complete buffer: response.arrayBuffer() in JavaScript, response.content in Python requests, ioutil.ReadAll(resp.Body) in Go. Write it directly to a file or feed it to a player once received.
OpenAI's audio.speech.create uses chunked transfer-encoding to begin sending audio before synthesis completes, which gives lower first-byte latency at the cost of a more complex client consume pattern (ReadableStream / iter_content). EasyVoice does not currently offer this mode — the response arrives as one complete file. For the dominant TTS use cases (narration, chatbot, accessibility, IVR), the synthesis speed of Kokoro (~1.1 ms/char on GPU) means full-file response latency is typically under a few seconds for standard inputs.
What stays the same — auth, error handling, caching
Bearer token authentication: identical. Both APIs accept Authorization: Bearer YOUR_KEY in the request header. Your existing pattern (env var → request header) works as-is, just with EASYVOICE_API_KEY in place of OPENAI_API_KEY. Both keys are rotatable from the respective vendor dashboards; if you need both vendors active during migration (e.g. existing-user cohort on OpenAI, new-user cohort on EasyVoice), keep both env vars set and route at the call site.
Error handling: HTTP 401 (invalid key), 400 (malformed request), 429 (rate limit), 5xx (server error). Same status codes mean the same things on both APIs. Caching: if your app caches generated audio by (text, voice, format) hash, the cache works identically — the audio bytes returned are deterministic for the same input across requests, so cache hit rates don't change after migration. The full migration commit usually touches 1-3 files: the TTS service module, the env var schema (.env.example), and the deployment config that sets EASYVOICE_API_KEY.
Code samples
Real working code, not pseudo-code. Every request below assumes you've set EASYVOICE_API_KEY and OPENAI_API_KEY as env vars where shown.
Before — OpenAI Python
Existing openai SDK callfrom openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.audio.speech.create(
model="tts-1",
voice="alloy",
input="Hello, this is OpenAI's TTS API.",
response_format="mp3",
)
with open("out.mp3", "wb") as f:
f.write(response.content)After — EasyVoice Python
stdlib requests, 5 lines changedimport os, requests
res = requests.post(
"https://easyvoice.ae/api/v1/audio/speech", # was api.openai.com/v1/audio/speech
headers={"Authorization": f"Bearer {os.environ['EASYVOICE_API_KEY']}"},
json={
"voice": "af_alloy", # was "alloy"
"input": "Hello, this is EasyVoice TTS API.",
"response_format": "mp3",
},
)
with open("out.mp3", "wb") as f:
f.write(res.content)Before — OpenAI JavaScript
Existing openai Node SDK callimport OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await client.audio.speech.create({
model: "tts-1",
voice: "alloy",
input: "Hello, this is OpenAI's TTS API.",
response_format: "mp3",
});
const buffer = Buffer.from(await response.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);After — EasyVoice JavaScript
fetch in Node 18+, 5 lines changedconst res = await fetch("https://easyvoice.ae/api/v1/audio/speech", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.EASYVOICE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
voice: "af_alloy", // was "alloy"
input: "Hello, this is EasyVoice TTS API.",
response_format: "mp3",
}),
});
const buffer = Buffer.from(await res.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);Voice map — both languages
Drop into a constants file at the top of your TTS service module# Python — voice_map.py
OPENAI_TO_EASYVOICE = {
"alloy": "af_alloy",
"echo": "am_echo",
"fable": "bm_fable",
"onyx": "am_onyx",
"nova": "af_nova",
"shimmer": "bf_lily",
}
# JavaScript — voiceMap.ts
# export const OPENAI_TO_EASYVOICE = {
# alloy: "af_alloy",
# echo: "am_echo",
# fable: "bm_fable",
# onyx: "am_onyx",
# nova: "af_nova",
# shimmer: "bf_lily",
# } as const;Fetch the audio — Python
Receives the complete audio file in one response — write directly to diskimport os, requests
res = requests.post(
"https://easyvoice.ae/api/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['EASYVOICE_API_KEY']}"},
json={"voice": "af_alloy", "input": "Hello from EasyVoice.", "response_format": "mp3"},
)
res.raise_for_status()
# res.content is the complete audio file (all bytes buffered server-side before delivery)
with open("out.mp3", "wb") as f:
f.write(res.content)Fetch the audio — JavaScript
Receives the complete audio file in one response — write or play once receivedconst res = await fetch("https://easyvoice.ae/api/v1/audio/speech", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.EASYVOICE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
voice: "af_alloy",
input: "Hello from EasyVoice.",
response_format: "mp3",
}),
});
// The response is a complete audio file — all bytes arrive together
const buffer = Buffer.from(await res.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);Voices to try on the free tier
Every voice below is callable via the same voice parameter — preview audio samples and read the full character profile.
Frequently asked questions
How many lines of code does an OpenAI TTS to EasyVoice migration take?▾
Five. Change the import (openai → requests / fetch), change the URL (api.openai.com/v1/audio/speech → easyvoice.ae/api/v1/audio/speech), change the Authorization header to use EASYVOICE_API_KEY, change the voice parameter (alloy → af_alloy and the rest of the mapping table), and drop the model parameter (optional, ignored if sent). Everything else — response handling, error codes, response_format options — stays the same.
Can I keep using the openai Python or Node SDK?▾
No — the official openai SDKs are hardcoded to api.openai.com for the audio.speech endpoint and don't expose a custom base URL override. The pragmatic path is to swap the SDK call for stdlib HTTP (requests in Python, fetch in JS), which is the 5-line diff this page covers. Removing the dependency is usually a net positive — one fewer thing to version-pin and upgrade.
What about the model parameter — tts-1 vs tts-1-hd?▾
Drop it. EasyVoice runs Kokoro-82M as a single model for all voices — fidelity isn't gated by a model name. If you send model: 'tts-1' or 'tts-1-hd' or 'kokoro' anyway, EasyVoice ignores it gracefully (no 400 error). This is intentional drop-in compatibility — your existing OpenAI-shaped code with the model parameter set still works as-is, no defensive code changes needed pre-migration.
Are mp3 / wav response formats compatible between vendors?▾
Yes. Both APIs return raw audio bytes (not JSON-wrapped, not base64-encoded) for response_format mp3 and wav. An mp3 from OpenAI plays in the same player as an mp3 from EasyVoice. Slight bitrate differences (OpenAI tts-1 is 96 kbps, EasyVoice mp3 is 128 kbps) mean file sizes differ slightly for the same audio length, but downstream library compatibility is identical. EasyVoice does not support opus, aac, flac, or pcm — sending those falls back to mp3.
How does EasyVoice return the audio response?▾
EasyVoice returns the complete audio file in one response — not chunked streaming. The endpoint buffers the full file server-side and returns it once Kokoro finishes synthesis. Because Kokoro is fast (~1.1 ms/char on GPU), typical requests complete in a few seconds. Read the response with response.arrayBuffer() in JS, response.content in Python, or equivalent — write the buffer to a file or feed it to a player once received. EasyVoice does not currently offer a chunked/streaming TTS endpoint.
How do I run both vendors during migration?▾
Keep both env vars set (OPENAI_API_KEY and EASYVOICE_API_KEY), route at the call site by user cohort or feature flag. Common patterns: cohort by user creation date (existing users keep OpenAI, new users go EasyVoice), feature flag per user, or content-type routing (chatbot responses go EasyVoice for cost, brand voiceover stays OpenAI temporarily). Both keys can rotate independently.
Related OpenAI migration guides
OpenAI TTS voices, mapped to free Kokoro alternatives
Map OpenAI's 6 voices (alloy, echo, fable, onyx, nova, shimmer) to closest EasyVoice Kokoro voices. Side-by-side tones, when-to-use, sample scripts. Free.
OpenAI TTS pricing vs EasyVoice — when flat-rate wins
OpenAI TTS pricing calculator. tts-1 $15/1M, tts-1-hd $30/1M vs EasyVoice $9.99 flat. Breakeven 666K chars/mo. Real numbers at 100K, 500K, 1M, 5M, 10M.
Vendor comparison: EasyVoice vs OpenAI TTS
Side-by-side feature comparison covering voices, languages, pricing tiers, free limits, API surface, and the why-people-look / where-each-wins breakdown.
Developer-focused OpenAI migration in /tts-api
The developer-onboarding angle of the same migration — request body compatibility deep-dive, streaming behavior, ChatGPT plugin/Realtime API guidance, and the official OpenAI SDK constraint.
Start migrating off OpenAI TTS today
5,000 characters per day free, no credit card. Pro $9.99/mo unlimited replaces OpenAI's $15-$300/mo bills once you cross 666K characters per month.