Przejdź do treści

Integracja Text to Speech API: streaming, batch, ponawianie

Opublikowano
Ostatnia aktualizacja

PosłuchajPosłuchaj tego artykułu

Integracja z Text to Speech API jest prosta… po podjęciu kilku konkretnych decyzji: którego trybu transferu użyć, jak wybrać model i format wyjściowy, jak streamować, jak obsłużyć duży wolumen bez przekraczania limitu współbieżności, jak cache’ować i ponawiać żądania, aby nigdy nie płacić dwa razy za wygenerowanie tego samego audio, oraz jak porównać czas do pierwszego bajtu z innym dostawcą.

Aby pomóc ci z integracją Text to Speech API, omawiamy każdą z tych decyzji architektonicznych i pokazujemy, co zrobić. Ten przewodnik pomoże ci zintegrować Text to Speech API ElevenLabs i skalować rozwiązanie dzięki fragmentom kodu, które możesz wkleić bezpośrednio do środowiska produkcyjnego.

Więcej o opisanych tu zagadnieniach znajdziesz w naszych przewodnikach: jak działa streaming audio, optymalizacja opóźnień oraz przegląd modeli ElevenLabs

Podsumowanie

  • ElevenLabs Text to Speech API ma jeden endpoint, dostępny na trzy sposoby: konwersja wsadowa, streaming HTTP i WebSocket stream-input.
  • W HTTP każde trwające żądanie liczy się do limitu współbieżności, a w WebSocket liczy się tylko aktywne generowanie.
  • Ustaw równoległość nieco poniżej limitu planu i cache’uj hash każdego parametru wpływającego na wynik, aby nigdy nie naliczać opłaty za ten sam tekst dwa razy.
  • Ponawiaj żądania 429 i 5xx z wykładniczym backoffem i pełnym jitterem, aby zwolnić przed osiągnięciem limitu współbieżności.

Trzy sposoby integracji z Text to Speech API

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

To samo wywołanie POST /v1/text-to-speech/{voice_id} działa w trzech wariantach, z których każdy lepiej pasuje do nieco innego zadania. Oto trzy sposoby integracji z Text to Speech API:

  • Batch (convert) to najprostsza integracja: wysyłasz jedno żądanie i dostajesz jedną odpowiedź audio. To opcja o najniższej złożoności i najwyższym czasie do pierwszego audio, ponieważ cały klip jest syntezowany, zanim wrócą jakiekolwiek bajty.
  • Streaming HTTP (stream) używa tego samego żądania, ale dzieli odpowiedź na fragmenty: dodajesz /stream do ścieżki, wywołujesz metodę stream, a audio wraca jako odpowiedź chunked. Kod jest niemal identyczny, a odczuwalne opóźnienie znacznie mniejsze.
  • WebSocket (stream-input) utrzymuje stałe połączenie: wysyłasz tekst stopniowo i na bieżąco otrzymujesz fragmenty audio. To rozwiązanie dla interaktywnych agentów oraz przekazywania odpowiedzi LLM do syntezy mowy w trakcie generowania tokenów, jeszcze przed końcem zdania.

Streaming nie sprawia, że model generuje audio szybciej — czas inferencji pozostaje bez zmian. Zmienia się moment otrzymania pierwszego fragmentu: jest wysyłany przed ukończeniem całego klipu, więc użytkownik krócej czeka, choć łączna praca jest taka sama.

Tabela decyzyjna: batch, streaming czy WebSocket

Wybierając jedną z tych trzech metod, weź pod uwagę kilka czynników.

W skrócie: wybierz batch do generowania offline, streaming HTTP dla znanego tekstu, na który czeka użytkownik, a WebSocket dla agentów i syntezy mowy z LLM na żywo. 

Poniższa tabela pokazuje kompromisy w obszarach istotnych przy skalowaniu.

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, zarówno w batchu, jak i streamingu, każde trwające żądanie liczy się do limitu współbieżności planu przez cały czas trwania. W WebSocket liczy się tylko czas, gdy model aktywnie generuje audio; otwarte, ale bezczynne połączenie prawie nic nie kosztuje.

W przypadku kaskadowego agenta głosowego, który utrzymuje połączenie przez całą rozmowę, ale generuje audio tylko podczas wypowiedzi agenta, różnica jest duża. To główny powód, by używać WebSocketów przy tworzeniu agentów. Pełny protokół opisujemy w przewodniku po WebSocket Text to Speech w czasie rzeczywistym.

Wybór modelu i formatu wyjściowego

Na audio zwracane przez integrację z TTS API wpływają dwa wybory. Pierwszy to model, który określa jakość i szybkość. Drugi to format wyjściowy, który określa kontener, bitrate i częstotliwość próbkowania.

Właściwy wybór obu na początku sprawi, że kolejne elementy, takie jak opóźnienie i zgodność z telefonią, zadziałają bez problemu.

Modele

Oferujemy kilka modeli Text to Speech. Nie są uszeregowane od najlepszego do najgorszego — każdy ma inne kompromisy.

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

Warto zaznaczyć, że ~75 ms oznacza inferencję modelu w reprezentatywnych warunkach, bez opóźnień sieci i aplikacji. Czas rośnie przy dłuższych danych wejściowych i pod obciążeniem. Zawsze mierz z poziomu swojej aplikacji, a nie na podstawie wyniku benchmarku.

Modele Flash są mniejsze i używają bardziej agresywnych przybliżeń, aby skrócić czas inferencji. Eleven v3 i Multilingual v2 są większe i poświęcają więcej czasu na każdy znak, by zapewnić bogatszy wynik. Nie ma ustawienia, które daje jakość Eleven v3 przy szybkości Flash, ponieważ ta jakość wymaga dodatkowych obliczeń.

Do zastosowań w czasie rzeczywistym lub agentów użyj eleven_flash_v2_5 — to wielojęzyczna opcja o najniższym opóźnieniu. Do narracji, audiobooków lub marketingowego nałożonego głosu użyj eleven_multilingual_v2, gdy zależy ci na stabilnej, wysokiej jakości, lub eleven_v3, gdy potrzebujesz maksymalnej ekspresji i szerokiego zakresu emocji. 

Gdy wymowa ma znaczenie, na przykład przy numerach telefonów, datach lub walutach, samodzielnie normalizuj liczby w aplikacji, zanim tekst trafi do API. Zapisz formę, którą głos ma wypowiedzieć. 

Samodzielna normalizacja zapewnia przewidywalną wymowę w różnych modelach i pozwala uniknąć zależności od domyślnych ustawień modeli, które mogą się zmienić.

Format wyjściowy

Parametr output_format określa kontener, częstotliwość próbkowania i bitrate zwracanego audio. 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 kontrolują sposób generowania mowy:

  • Stability: kontroluje równowagę między spójnością a ekspresją. Niższe wartości dają bardziej zróżnicowaną, ekspresyjną mowę, a wyższe — bardziej stabilne i przewidywalne brzmienie.
  • SimilarityBoost: kontroluje, jak bardzo wynik przypomina głos referencyjny.
  • Style: po zwiększeniu wzmacnia naturalny styl mówienia danego głosu.
  • useSpeakerBoost: zwiększa podobieństwo do oryginalnego mówcy kosztem niewielkiego wzrostu opóźnienia.
  • Speed: dostosowuje tempo mówienia względem domyślnej wartości 1.0.

Spośród tych ustawień Stability zwykle ma największy wpływ na postrzeganą jakość. Niższe wartości dają bardziej ekspresyjny, ale mniej spójny wynik, a wyższe priorytetowo traktują spójność i przewidywalność.

Przy wyborze głosu najmniejsze opóźnienie zapewnia Flash z Instant Voice Cloning lub głosem domyślnym; Professional Voice Clones brzmią świetnie, ale dodają narzut przy każdym generowaniu, który warto uwzględnić.

W całym przewodniku przykładowy identyfikator głosu to JBFqnCBsd6RMkjVDRZzb (George).

Integracja streamingu (HTTP i WebSocket)

W tej sekcji omawiamy praktyczne podstawy integracji z Text to Speech API. Pokazujemy instalację SDK, otwieranie strumienia i odbieranie audio w miarę napływania fragmentów. Ścieżka HTTP obejmuje większość odtwarzania w sieci i aplikacjach, a WebSocket — agentów i odpowiedzi LLM na żywo.

Obie ścieżki zakładają, że klient ElevenLabs został zainicjalizowany jak poniżej.

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

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

Streaming otwiera strumień i odbiera fragmenty w miarę ich napływania. voiceId to pierwszy argument pozycyjny, a po nim następuje obiekt opcji z kluczami camelCase (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 wariancie WebSocket połącz się z wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input, wyślij pierwszą wiadomość z ustawieniami głosu i spacją na początku, następnie wysyłaj wiadomości tekstowe, gdy będą dostępne, i odczytuj ramki JSON, których pole audio zawiera fragmenty zakodowane w base64.

Batching i limity współbieżności dla wysokiej przepustowości

Integrację o wysokiej przepustowości określa współbieżność, czyli liczba żądań generujących audio w tej samej chwili. Każdy plan ma limit dla danej rodziny modeli. 

Każdy plan ma inny limit współbieżności:

  • Free: 4 współbieżne żądania Flash.
  • Starter: 6 współbieżnych żądań Flash.
  • Creator: 10 współbieżnych żądań Flash.
  • Pro: 20 współbieżnych żądań Flash.
  • Scale i Business: 30 współbieżnych żądań Flash; limity Enterprise są ustalane indywidualnie.

Limity Multilingual v2 są około dwukrotnie niższe.

Ograniczona pula rozwiązuje ten problem, limitując liczbę żądań uruchamianych jednocześnie:

// 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 nieco poniżej limitu planu, a nie dokładnie na jego poziomie. Ten zapas obsłuży inny ruch korzystający z tego samego klucza i utrzyma cię poniżej progu zwracającego 429.

Limity znaków i dzielenie długiego tekstu

Każdy model ogranicza liczbę znaków przyjmowanych w pojedynczym żądaniu. Każda integracja z długimi treściami musi podzielić tekst i połączyć audio. 

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

  • Flash v2.5: przyjmuje do 40 000 znaków na żądanie.
  • Flash v2: przyjmuje do 30 000 znaków na żądanie.
  • Multilingual v2: przyjmuje do 10 000 znaków na żądanie.
  • Eleven v3: przyjmuje do 5000 znaków na żądanie.

Dłuższy tekst trzeba podzielić na kilka żądań. Staraj się dzielić go na granicach zdań, aby zachować prozodię między fragmentami.

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;
}

Generuj fragmenty po kolei i połącz audio. W przypadku długiej narracji, gdzie każdy fragment jest niezależny, oba elementy działają bezpośrednio razem: przekaż wynik splitText do ograniczonej puli powyżej, a ona zajmie się resztą.

Cache’owanie i idempotencja

Wynik Text to Speech jest na tyle deterministyczny, że ponowne generowanie tego samego tekstu tym samym głosem, modelem i ustawieniami to strata zasobów. Cache’uj wynik pod hashem danych wejściowych wpływających na audio, a ten sam klucz posłuży też jako token idempotencji przy ponawianiu żądań.

Oto jak zrobić jedno i drugie.

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: w kluczu musi znaleźć się każdy parametr zmieniający audio, w tym outputFormat i ustawienia głosu. Przy prawidłowej konfiguracji ten sam klucz służy też jako token idempotencji. Gdy klient ponawia żądanie, które już się udało, zwracasz bajty z cache zamiast generować audio ponownie.

Obsługa błędów i limity żądań (429)

Klient produkcyjny potrzebuje ponawiania żądań z backoffem i jitterem oraz obsługi zależnej od kodu statusu, ponieważ niektóre błędy warto ponawiać, a inne nie. 

Poniższa tabela przypisuje każdemu statusowi właściwe działanie, a ta sekcja wyjaśnia, dlaczego 429 to miękki limit, a nie twarda ściana.

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

Kod 429 nie jest twardą ścianą — warto znać mechanizm. Po przekroczeniu limitu współbieżności żądania najpierw trafiają do kolejki według priorytetu, co zwykle dodaje około 50 ms. Dopiero jeśli nadal przekraczasz dostępną pojemność, otrzymujesz 429. 

Odpowiedź zawiera też nagłówki current-concurrent-requests i maximum-concurrent-requests, które pokazują bieżący zapas. Możesz je odczytać i zwolnić, zanim osiągniesz 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 zamiast lepszego ponawiania żądań, zmień plan na wyższy. Klienci Enterprise mogą poprosić o wyższe limity przez opiekuna konta.

Benchmarking opóźnienia i czasu do pierwszego bajtu

Opóźnienie zależy od regionu, danych wejściowych i bieżącego obciążenia, więc jedyna wartość, na której warto polegać, to ta zmierzona w twoim środowisku. 

Ta sekcja pokazuje czas do pierwszego bajtu (TTFB) dla endpointu streamingowego Flash i ma strukturę, która pozwala skierować ten sam test do innego dostawcy oraz porównać ich w identycznych warunkach.

Traktuj to jako metodologię, a nie opublikowany wynik. Pojedynczy pomiar niczego nie gwarantuje. 

Oto kilka istotnych zastrzeżeń przy benchmarkingu opóźnienia integracji z Text to Speech API:

  • Uwzględnij czas podróży sieciowej w obie strony: TTFB zależy od lokalizacji i najbliższego klastra dostawcy, więc uruchom test tam, gdzie zwykle działają twoje serwery.
  • Odrzuć przebieg rozgrzewkowy: pierwsze żądanie na zimnym połączeniu jest wolniejsze i może zniekształcić wyniki.
  • Utrzymaj stałe dane wejściowe: długość tekstu, głos, model i obciążenie wpływają na wynik, więc zachowaj je identyczne u wszystkich dostawców.
  • Raportuj rozkład: wyniki różnią się między uruchomieniami, więc podaj medianę i p95 zamiast jednej wartości.

Mając to na uwadze, możesz zacząć benchmark.

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");
}

Aby porównać z innym dostawcą, napisz funkcję o tej samej strukturze. Następnie uruchom obie w prostym skrypcie, który odrzuci jedno wywołanie rozgrzewkowe, zbierze około 20 pomiarów w odstępach, by nie kolidowały ze sobą, i poda medianę oraz p95 w milisekundach.

Rzetelne porównanie wymaga kontroli zmiennych. 

Uruchom obu dostawców z tej samej maszyny i sieci — najlepiej z serwera w regionie, w którym faktycznie wdrażasz rozwiązanie, a nie z laptopa na domowym łączu. Użyj tego samego tekstu wejściowego i krótkiego audio, aby na wynik bardziej wpływała inferencja modelu niż długość generowania. Podaj medianę i p95 z wielu uruchomień, bo pojedynczy pomiar to szum. 

Pamiętaj, że TTFB przez publiczny internet obejmuje 20–200 ms podróży sieciowej w obie strony, co nie ma związku z modelem. Obsługujemy klastry w Ameryce Północnej, Europie i Azji Południowo-Wschodniej oraz kierujemy ruch do najbliższego z nich, więc odpowiednio umieść klienta testowego — inaczej będziesz głównie mierzyć odległość do centrum danych.

Najważniejsze wskazówki dotyczące integracji z Text to Speech API

Produkcyjna integracja Text to Speech API sprowadza się do kilku ważnych decyzji.

Jeśli podejmiesz je właściwie, reszta ułoży się sama:

  • Wybierz model do zadania: używaj Flash v2.5 do wszystkiego, co interaktywne, oraz modelu o wyższej jakości, takiego jak Multilingual v2 lub Eleven v3 do generowania offline, gdzie opóźnienie ma mniejsze znaczenie.
  • Streamuj, gdy użytkownik czeka: używaj streamingu HTTP dla znanego tekstu, a WebSocketu dla agentów, aby czas bezczynności nie zużywał limitu współbieżności.
  • Ogranicz równoległość do limitu planu: ogranicz współbieżne żądania do wartości nieco poniżej limitu planu i cache’uj hash każdego parametru wpływającego na wynik, aby nie naliczać opłaty za to samo audio dwa razy.
  • Ponawiaj żądania 429 i 5xx z wykładniczym backoffem i pełnym jitterem: zwalniaj przy 429 i 5xx z pełnym jitterem oraz obserwuj nagłówki współbieżności, by wiedzieć, jak blisko limitu jesteś.
  • Dziel długi tekst na granicach zdań: dziel tekst na granicach zdań, mieszcząc się w limicie znaków modelu, aby prozodia zachowała się między fragmentami.

Jeśli chcesz dowiedzieć się więcej, zajrzyj do przewodnika po streamingu, omówienia streamingu audio, uwierzytelniania oraz tokenów jednorazowych do użycia po stronie klienta.

Zbuduj integrację Text to Speech z ElevenAPI

Po przeczytaniu tego przewodnika znasz wszystkie wzorce potrzebne do produkcyjnej integracji z Text to Speech API. Streaming, batching, cache’owanie, ponawianie żądań, a nawet benchmarking — możesz wdrożyć to w praktyce. 

Zacznij od poznania Text to Speech API lub zarejestruj się, aby jeszcze dziś wykonać pierwsze wywołanie z ElevenAPI.

FAQ: integracja z Text to Speech API

Podobne artykuły

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