Pomiń

Integracja Text to Speech API: streaming, batch, ponawianie

Opublikowano

PosłuchajPosłuchaj tego artykułu

Integracja Text to Speech API jest prosta… ale najpierw musisz podjąć kilka decyzji: jaki tryb przesyłania wybrać, jak dobrać model i format wyjściowy, jak streamować, jak obsłużyć duży ruch bez przekraczania limitu równoczesnych zapytań, jak cache’ować i ponawiać, żeby nie płacić dwa razy za ten sam dźwięk, oraz jak porównać czas odpowiedzi z innym dostawcą.

Żeby ułatwić ci integrację Text to Speech API, rozłożyliśmy każdą z tych decyzji na czynniki pierwsze. Ten przewodnik pomoże ci zintegrować ElevenLabs Text to Speech API i skalować, z gotowymi fragmentami kodu do wdrożenia.

Jeśli chcesz poznać podstawy, zajrzyj do naszych przewodników o streamingu audio, optymalizacji opóźnień oraz przeglądzie modeli ElevenLabs.

Podsumowanie

  • Jest jeden endpoint ElevenLabs Text to Speech API, do którego możesz się podłączyć na trzy sposoby: batch, HTTP stream i WebSocket stream-input.
  • W przypadku HTTP każde aktywne zapytanie liczy się do limitu równoczesnych zapytań, a w WebSocket tylko te, które faktycznie generują dźwięk.
  • Ustaw równoległość tuż poniżej limitu twojego planu i cache’uj hash wszystkich parametrów wpływających na dźwięk, żeby nie płacić dwa razy za ten sam tekst.
  • Ponawiaj 429 i 5xx z eksponencjalnym opóźnieniem i pełnym jitterem, żeby nie przekroczyć limitu równoczesnych zapytań.

Trzy sposoby integracji Text to Speech API

Jest jeden endpoint Text to Speech, ale sposób integracji wpływa na opóźnienia, złożoność i koszty.

To samo wywołanie POST /v1/text-to-speech/{voice_id} działa na trzy sposoby, każdy do trochę innego zastosowania. Oto ich porównanie:

  • Batch (convert) to najprostsza integracja: Wysyłasz jedno zapytanie i dostajesz jedną odpowiedź audio. To najprostsza opcja, ale czas oczekiwania na pierwszy dźwięk jest najdłuższy, bo cały klip jest generowany zanim wróci odpowiedź.
  • HTTP streaming (stream) działa podobnie, ale dzieli odpowiedź na fragmenty: Dodajesz /stream do ścieżki, wywołujesz metodę stream i dźwięk wraca w kawałkach. Kod jest prawie taki sam, a opóźnienie odczuwalne przez użytkownika dużo mniejsze.
  • WebSocket (stream-input) utrzymuje stałe połączenie: Wysyłasz tekst po kawałku i od razu dostajesz fragmenty audio. To rozwiązanie dla agentów i do zamiany tekstu z LLM na mowę w czasie rzeczywistym, zanim skończy się zdanie.

Streaming nie przyspiesza generowania audio przez model; czas inferencji się nie zmienia. Zmienia się tylko moment, w którym dostajesz pierwszy fragment — pojawia się szybciej, więc użytkownik krócej czeka, mimo że całość trwa tyle samo.

Porównanie: batch vs. streaming vs. WebSocket

Wybierając między tymi trzema metodami, warto wziąć pod uwagę kilka czynników.

W skrócie: batch do renderowania offline, HTTP streaming do znanego tekstu, na który ktoś czeka, a WebSocket do agentów i LLM na żywo.

Poniższa tabela pokazuje różnice, które mają znaczenie przy dużej skali.

Batch (convert)
Time-to-first-audio
Highest (wait for full clip)
Implementation complexity
Lowest
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Offline rendering, audiobooks, caching
HTTP streaming
Time-to-first-audio
Low
Implementation complexity
Low
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Web/app playback of known text
WebSocket (stream-input)
Time-to-first-audio
Lowest
Implementation complexity
Highest (connection lifecycle, framing)
Text known up front?
Not required - send incrementally
Streaming LLM output into TTS
Native fit
Concurrency cost
Only active generation counts
Best for
Voice agents, live LLM to speech

W HTTP (batch i streaming) każde aktywne zapytanie liczy się do limitu przez cały czas trwania. W WebSocket tylko czas faktycznego generowania audio się liczy — otwarte, ale nieaktywne połączenie praktycznie nic nie kosztuje.

Dla kaskadowego agenta głosowego, który trzyma połączenie przez całą rozmowę, ale generuje dźwięk tylko w trakcie swojej wypowiedzi, ta różnica jest duża — to główny powód, żeby używać WebSocketów przy budowie agentów. Pełny protokół opisujemy w przewodniku po WebSocket Text to Speech w czasie rzeczywistym.

Wybór modelu i formatu wyjściowego

Dwie decyzje wpływają na to, jaki dźwięk dostaniesz z TTS API. Po pierwsze model — decyduje o jakości i szybkości. Po drugie format wyjściowy — ustala kontener, bitrate i częstotliwość próbkowania.

Dobrze dobrane ustawienia od początku sprawią, że wszystko dalej — np. opóźnienia czy zgodność z telefonią — będzie działać bez problemu.

Modele

Mamy kilka modeli Text to Speech. Nie są ułożone od najlepszego do najgorszego — każdy ma inne zalety.

Best for
eleven_flash_v2_5
Real-time, agents, bulk throughput (~75ms model inference)
eleven_flash_v2
Real-time, English only (~75ms)
eleven_multilingual_v2
Highest stable fidelity, narration
eleven_v3
Most expressive, widest language range
Languages
eleven_flash_v2_5
32
eleven_flash_v2
English
eleven_multilingual_v2
29
eleven_v3
70+
Character limit
eleven_flash_v2_5
40,000
eleven_flash_v2
30,000
eleven_multilingual_v2
10,000
eleven_v3
5,000

Dla jasności: ~75 ms to czas inferencji modelu w typowych warunkach, bez opóźnień sieci i aplikacji. Przy dłuższych wejściach i większym obciążeniu czas rośnie. Zawsze mierz u siebie, nie sugeruj się benchmarkiem.

Modele Flash są mniejsze i stosują uproszczenia, żeby skrócić czas inferencji. Eleven v3 i Multilingual v2 to większe modele, które poświęcają więcej czasu na każdy znak, żeby uzyskać bogatszy dźwięk. Nie da się uzyskać jakości Eleven v3 w tempie Flash — ta jakość to właśnie dodatkowe obliczenia.

Do zastosowań na żywo lub dla agentów wybierz eleven_flash_v2_5 — to najszybszy model wielojęzyczny. Do narracji, audiobooków czy marketingu wybierz eleven_multilingual_v2, jeśli zależy ci na stabilnej wysokiej jakości, albo eleven_v3, jeśli chcesz maksymalnej ekspresji i emocji.

Jeśli wymowa ma znaczenie (np. numery telefonów, daty, waluty), zrób normalizację liczb po swojej stronie, zanim tekst trafi do API. Zapisz dokładnie to, co ma być wypowiedziane.

Dzięki temu wymowa będzie przewidywalna w każdym modelu i nie będziesz zależny od domyślnych ustawień, które mogą się zmienić.

Format wyjściowy

Parametr output_format ustala kontener, częstotliwość próbkowania i bitrate dźwięku. Najczęściej używane wartości:

Use case
mp3_44100_128
General playback, downloads, highest mp3 quality shown here
mp3_22050_32
Lower-bandwidth playback, smaller files
pcm_24000 / pcm_16000
Raw PCM for your own audio pipeline or further processing
ulaw_8000
Telephony - the format used with Twilio and similar systems
Languages
mp3_44100_128
32
mp3_22050_32
English
pcm_24000 / pcm_16000
29
ulaw_8000
70+
Character limit
mp3_44100_128
40,000
mp3_22050_32
30,000
pcm_24000 / pcm_16000
10,000
ulaw_8000
5,000

Ustawienia głosu

Te ustawienia wpływają na to, jak brzmi wygenerowana mowa:

  • Stability: Reguluje balans między powtarzalnością a ekspresją. Niższe wartości dają bardziej zróżnicowaną, ekspresyjną mowę, wyższe — bardziej stabilną i przewidywalną.
  • SimilarityBoost: Określa, jak bardzo wynik przypomina głos referencyjny.
  • Style: Podbija naturalny styl mówienia, jeśli podniesiesz wartość.
  • useSpeakerBoost: Wyostrza podobieństwo do oryginalnego mówcy, kosztem lekkiego wzrostu opóźnienia.
  • Speed: Zmienia tempo mowy wokół domyślnej wartości 1.0.

Z tych ustawień największy wpływ na odbiór ma Stability. Niższe wartości dają więcej ekspresji, ale mniej powtarzalności; wyższe — odwrotnie.

Wybierając głos, najszybsze połączenie to Flash z Instant Voice Clone lub głosem domyślnym; Professional Voice Clones brzmią świetnie, ale dodają trochę opóźnienia przy każdej generacji.

W tym przewodniku przykładowy voice id to JBFqnCBsd6RMkjVDRZzb (George).

Integracja streamingowa (HTTP i WebSocket)

W tej części pokazujemy praktyczną integrację Text to Speech API: instalację SDK, otwieranie streamu i odbieranie dźwięku na bieżąco. HTTP sprawdzi się w większości aplikacji i na stronach, WebSocket — dla agentów i LLM na żywo.

W obu przypadkach zakładamy, że masz już zainicjowanego klienta ElevenLabs.

npm install @elevenlabs/elevenlabs-js
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

W ścieżce streamingowej otwierasz stream i odbierasz fragmenty dźwięku na bieżąco. voiceId to pierwszy argument, potem obiekt z opcjami (modelId, outputFormat, voiceSettings):

const stream = await elevenlabs.textToSpeech.stream("JBFqnCBsd6RMkjVDRZzb", {
  text,
  modelId: "eleven_flash_v2_5",
  outputFormat: "mp3_44100_128",
  voiceSettings: { stability: 0, similarityBoost: 1.0, style: 0, useSpeakerBoost: true, speed: 1.0 },
});

for await (const chunk of stream) {
  // chunk is a Buffer; feed it to the player as it arrives
}

W wersji WebSocket łączysz się z wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input, wysyłasz pierwszą wiadomość z ustawieniami głosu i spacją na początku, potem przesyłasz teksty w miarę ich pojawiania się i odbierasz ramki JSON z polem audio (base64).

Batchowanie i limity równoczesności przy dużym ruchu

Integracja na dużą skalę zależy od równoczesności, czyli liczby zapytań generujących dźwięk w tym samym czasie. Każdy plan ma limit na rodzinę modeli.

Każdy plan ma swój limit równoczesnych zapytań:

  • Free: 4 równoczesne zapytania Flash.
  • Starter: 6 równoczesnych zapytań Flash.
  • Creator: 10 równoczesnych zapytań Flash.
  • Pro: 20 równoczesnych zapytań Flash.
  • Scale i Business: 30 równoczesnych zapytań Flash, Enterprise — limity indywidualne.

Limity Multilingual v2 to ok. połowa powyższych.

Ograniczona pula zapobiega przekroczeniu limitu, bo naraz działa tylko określona liczba zapytań:

// Set MAX_CONCURRENCY at or below your plan's Flash concurrency limit.
const MAX_CONCURRENCY = 8;

async function synthMany(texts: string[]): Promise<Buffer[]> {
  const results: Buffer[] = [];
  for (let i = 0; i < texts.length; i += MAX_CONCURRENCY) {
    const batch = texts.slice(i, i + MAX_CONCURRENCY);
    results.push(...(await Promise.all(batch.map(eachSingleRequest)))); // never more than MAX_CONCURRENCY in flight
  }
  return results;

Ustaw MAX_CONCURRENCY trochę poniżej limitu planu, nie równo z nim. Ta rezerwa łapie inne zapytania na tym samym kluczu i chroni przed 429.

Limity znaków i dzielenie długiego tekstu

Każdy model ma limit znaków na jedno zapytanie. Przy dłuższych tekstach trzeba je dzielić i potem łączyć dźwięk.

Oto limity znaków na zapytanie dla każdego modelu:

  • Flash v2.5: Do 40 000 znaków na zapytanie.
  • Flash v2: Do 30 000 znaków na zapytanie.
  • Multilingual v2: Do 10 000 znaków na zapytanie.
  • Eleven v3: Do 5 000 znaków na zapytanie.

Dłuższe teksty trzeba podzielić na kilka zapytań. Najlepiej dzielić po zdaniach, żeby intonacja nie była sztuczna na łączeniach.

function splitText(text: string, maxChars: number): string[] {
  const sentences = text.trim().split(/(?<=[.!?])\s+/);
  const chunks: string[] = [];
  let current = "";
  for (let sentence of sentences) {
    if (current.length + sentence.length + 1 > maxChars) {
      if (current) chunks.push(current.trim());
      // A single sentence longer than the limit is hard-split.
      while (sentence.length > maxChars) {
        chunks.push(sentence.slice(0, maxChars));
        sentence = sentence.slice(maxChars);
      }
      current = sentence;
    } else {
      current = `${current} ${sentence}`.trim();
    }
  }
  if (current) chunks.push(current.trim());
  return chunks;
}

Renderuj fragmenty po kolei i łącz dźwięk. Przy długich narracjach, gdzie każdy fragment jest niezależny, wystarczy podać wynik splitText do puli z limitem i reszta zrobi się sama.

Cache’owanie i idempotencja

Wynik Text to Speech jest na tyle przewidywalny, że generowanie tego samego tekstu z tymi samymi ustawieniami to strata zasobów. Cache’uj wynik po hashu wszystkich parametrów wpływających na dźwięk — ten sam klucz możesz użyć jako token idempotencji przy ponawianiu.

Oto jak to zrobić.

import { createHash } from "node:crypto";

function cacheKey(text: string, voiceId: string, modelId: string,
                  outputFormat: string, settings: object): string {
  // Every parameter that changes the audio must be in the key.
  const payload = JSON.stringify({ text, voiceId, modelId, outputFormat, settings });
  return createHash("sha256").update(payload).digest("hex");
}

async function cachedSynth(text: string, voiceId: string, modelId: string,
                           outputFormat: string, settings: object): Promise<Buffer> {
  const key = cacheKey(text, voiceId, modelId, outputFormat, settings);
  const cached = await cacheGet(key);          // e.g. read from disk or S3
  if (cached) return cached;

  const audio = await elevenlabs.textToSpeech.convert(voiceId, { text, modelId, outputFormat });
  await cachePut(key, audio);                   // store the bytes under the key
  return audio;
}

Zasada jest prosta: każdy parametr zmieniający dźwięk musi być w kluczu, w tym outputFormat i ustawienia głosu. Dzięki temu ten sam klucz to też token idempotencji — jeśli klient ponowi zapytanie, które już się udało, zwracasz cache zamiast generować od nowa.

Obsługa błędów i limity (429)

Klient produkcyjny musi obsługiwać ponawianie z opóźnieniem i jitterem oraz różne reakcje na kody odpowiedzi — nie każdy błąd warto ponawiać.

Poniższa tabela pokazuje, co zrobić przy każdym statusie, a w tej sekcji wyjaśniamy, czemu 429 to miękki limit, a nie twarda blokada.

Meaning
401
Authentication failed
422
Invalid request
429
Concurrency exceeded
5xx
Transient server error
Action
401
Do not retry. Check the xi-api-key header and key validity.
422
Do not retry. Fix the payload (bad voice id, unsupported format, text over limit).
429
Retry with exponential backoff and jitter.
5xx
Retry with backoff.
Character limit
401
40,000
422
30,000
429
10,000
5xx
5,000

429 to nie twarda blokada — najpierw zapytania przekraczające limit trafiają do kolejki (zwykle +50 ms). Dopiero jeśli dalej jest za dużo, dostajesz 429.

W odpowiedzi są też nagłówki current-concurrent-requests i maximum-concurrent-requests, które pokazują twój aktualny zapas — możesz je odczytać i zwolnić tempo zanim przekroczysz limit.

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function synthWithRetry(text: string, voiceId: string, maxRetries = 5): Promise<Buffer> {
  let delay = 500; // ms, base for exponential backoff
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await elevenlabs.textToSpeech.convert(voiceId, {
        text, modelId: "eleven_flash_v2_5", outputFormat: "mp3_44100_128",
      });
    } catch (err: any) {
      const status = err.statusCode;
      // 401/422 and exhausted retries are not recoverable here.
      if (!RETRYABLE.has(status) || attempt === maxRetries) throw err;
      // Exponential backoff with full jitter.
      await new Promise((r) => setTimeout(r, Math.random() * delay));
      delay = Math.min(delay * 2, 8000);
    }
  }
  throw new Error("unreachable");
}

Jeśli potrzebujesz większego zapasu, a nie lepszej obsługi ponawiania, zmień plan. Klienci Enterprise mogą poprosić o wyższe limity u swojego opiekuna.

Benchmarkowanie opóźnień i time-to-first-byte

Opóźnienie zależy od regionu, wejścia i obciążenia — jedyne wiarygodne liczby to te, które zmierzysz u siebie.

W tej sekcji znajdziesz TTFB dla endpointu Flash streaming, a całość jest tak zbudowana, żebyś mógł porównać z innym dostawcą w tych samych warunkach.

Traktuj to jako metodologię, nie oficjalny wynik. Jeden test niczego nie gwarantuje.

Kilka rzeczy, na które warto zwrócić uwagę przy benchmarkowaniu opóźnień Text to Speech API:

  • Uwzględnij czas sieci:TTFB zależy od twojej lokalizacji i najbliższego klastra dostawcy, więc testuj tam, gdzie zwykle działają twoje serwery.
  • Odrzuć pierwsze wywołanie:Pierwsze zapytanie na zimnym połączeniu jest wolniejsze i może zaburzyć wyniki.
  • Trzymaj te same wejścia:Długość tekstu, głos, model i obciążenie wpływają na wynik — porównuj identyczne dane u różnych dostawców.
  • Podawaj rozkład wyników:Wyniki się wahają — publikuj medianę i p95, nie pojedynczą wartość.

Biorąc to pod uwagę, możesz benchmarkować.

const TEXT = "This is a fixed benchmark sentence used for every provider.";

async function measureElevenLabs(): Promise<number> {
  const start = performance.now();
  const res = await fetch(
    "https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream?output_format=mp3_44100_128",
    {
      method: "POST",
      headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY!, "Content-Type": "application/json" },
      body: JSON.stringify({ text: TEXT, model_id: "eleven_flash_v2_5" }),
    },
  );
  for await (const _ of res.body!) {
    return performance.now() - start; // first chunk received
  }
  throw new Error("no audio returned");
}

Żeby porównać z innym dostawcą, napisz funkcję o tym samym kształcie. Potem uruchom oba przez runnera, który odrzuca pierwsze wywołanie, robi ~20 pomiarów w odstępach (żeby się nie nakładały) i raportuje medianę oraz p95 w milisekundach.

Uczciwe porównanie to kontrola zmiennych.

Uruchom obu dostawców z tej samej maszyny i sieci — najlepiej z serwera w regionie docelowym, nie z laptopa w domu. Użyj tego samego tekstu i krótkiego audio, żeby czas inferencji modelu dominował nad długością generacji. Podawaj medianę i p95 z wielu prób, bo pojedynczy pomiar to szum.

Pamiętaj, że TTFB przez internet to 20-200 ms samej sieci — model nie ma na to wpływu. Serwujemy z klastrów w Ameryce Północnej, Europie i Azji Południowo-Wschodniej i kierujemy do najbliższego, więc testuj klienta tam, gdzie faktycznie wdrażasz — inaczej mierzysz głównie odległość do centrum danych.

Najważniejsze w integracji Text to Speech API

Integracja Text to Speech API w produkcji sprowadza się do kilku kluczowych decyzji.

Jeśli dobrze je ustawisz, reszta pójdzie gładko:

  • Dobierz model do zadania: Flash v2.5 do wszystkiego na żywo, model o wyższej jakości (Multilingual v2 lub Eleven v3) do renderowania offline, gdzie opóźnienie nie jest kluczowe.
  • Streamuj, gdy użytkownik czeka: HTTP streaming do znanego tekstu, WebSocket do agentów — wtedy czas bezczynności nie zjada limitu równoczesnych zapytań.
  • Ogranicz równoległość do limitu planu: Ustaw limit zapytań tuż poniżej limitu planu i cache’uj po hashu wszystkich parametrów wpływających na dźwięk, żeby nie płacić dwa razy za to samo.
  • Ponawiaj 429 i 5xx z eksponencjalnym opóźnieniem i jitterem: Przy 429 i 5xx zwalniaj tempo z pełnym jitterem i sprawdzaj nagłówki z limitem, żeby wiedzieć, jak blisko jesteś limitu.
  • Dziel długi tekst po zdaniach: Dziel po zdaniach w ramach limitu znaków modelu, żeby intonacja nie była sztuczna na łączeniach.

Jeśli chcesz wejść głębiej, sprawdź instrukcję streamingu, opis streamingu audio, autoryzację oraz jednorazowe tokeny do użycia po stronie klienta.

Zbuduj swoją integrację Text to Speech z ElevenAPI

Po przeczytaniu tego przewodnika masz wszystkie wzorce potrzebne do produkcyjnej integracji Text to Speech API. Streaming, batch, cache, ponawianie, benchmarki — jesteś gotowy do wdrożenia.

Zacznij od poznania Text to Speech API lub zarejestruj się i wykonaj pierwsze wywołanie z ElevenAPI już dziś.

FAQ: Integracja Text to Speech API

Podobne artykuły

Twórz z najwyższej jakości audio AI