Salta al contenuto

Integrazione API Text to Speech: streaming, batching, retry

Pubblicato

AscoltaAscolta questo articolo

Integrare una API Text to Speech è semplice… dopo alcune scelte concrete: quale modalità di trasferimento usare, come scegliere modello e formato di output, come gestire lo streaming, come gestire grandi volumi senza superare il limite di concorrenza, come fare cache e retry per non pagare mai due volte lo stesso audio, e come confrontare il time-to-first-byte con altri provider.

Per aiutarti nell’integrazione dell’API Text to Speech, abbiamo analizzato ogni singola decisione architetturale e cosa fare. Questa guida ti aiuta a integrare la Text to Speech API di ElevenLabs e a scalare, con snippet di codice pronti da usare in produzione per iniziare subito.

Per una panoramica completa dei concetti citati qui, consulta le nostre guide su come funziona lo streaming audio, ottimizzazione della latenza e la panoramica dei modelli ElevenLabs.

  • C’è un solo endpoint API Text to Speech di ElevenLabs, accessibile in tre modi: conversione batch, streaming HTTP e WebSocket stream-input.
  • Su HTTP, ogni richiesta in corso conta sul tuo limite di concorrenza, mentre su WebSocket conta solo la generazione attiva.
  • Tieni la parallelizzazione appena sotto il limite del tuo piano e fai cache di un hash di ogni parametro che influenza l’output, così non paghi mai due volte per lo stesso testo.
  • Riprova le risposte 429 e 5xx con backoff esponenziale e jitter completo per rallentare prima di raggiungere il limite di concorrenza.

Tre modi per integrare la Text to Speech API

C’è un solo endpoint Text to Speech, ma il modo in cui lo integri determina latenza, complessità e costi.

La stessa chiamata POST /v1/text-to-speech/{voice_id} funziona in tre modalità, ognuna adatta a un caso d’uso leggermente diverso. Ecco una panoramica dei tre modi per integrare la Text to Speech API:

  • Batch (convert) è l’integrazione più semplice: Invi una richiesta e ricevi una risposta audio. È l’opzione meno complessa e ha il time-to-first-audio più alto, perché l’intero clip viene sintetizzato prima che torni qualsiasi byte.
  • Lo streaming HTTP (stream) mantiene la stessa richiesta ma suddivide la risposta: Aggiungi /stream al path, chiami il metodo stream e l’audio arriva come risposta suddivisa in chunk. Il codice è quasi identico e la latenza percepita è molto più bassa.
  • Il WebSocket (stream-input) mantiene una connessione persistente: Invi il testo in modo incrementale e ricevi i chunk audio man mano che arrivano. È pensato per agenti interattivi e per trasformare in voce l’output di un LLM mentre i token vengono prodotti, anche prima che la frase sia completa.

Lo streaming non rende la generazione audio più veloce; il tempo di inferenza resta invariato. Quello che cambia è quando ricevi il primo chunk: viene inviato prima che il clip sia completo, così l’attesa percepita dall’utente è più breve anche se il lavoro totale è lo stesso.

Tabella di confronto: batch, streaming e WebSocket

Quando scegli tra questi tre metodi, ci sono diversi fattori da considerare.

Come regola generale: scegli batch per il rendering offline, streaming HTTP per testo noto che l’utente sta aspettando, e WebSocket per agenti e output LLM in tempo reale.

La tabella qui sotto mostra i pro e contro su tutte le dimensioni che contano in fase di scalabilità.

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

Su HTTP, sia batch che streaming, ogni richiesta in corso conta sul limite di concorrenza del tuo piano per tutta la sua durata. Su WebSocket, conta solo il tempo in cui il modello sta generando audio; un socket aperto ma inattivo non ha praticamente costi.

Per un agente vocale a cascata che mantiene la connessione aperta per tutta la conversazione ma genera audio solo durante i turni dell’agente, questa differenza è notevole, ed è il motivo principale per cui usare i WebSocket quando costruisci agenti. Il protocollo completo è descritto nella guida WebSocket Text to Speech in tempo reale.

Scelta del modello e del formato di output

Due scelte determinano l’audio che ottieni dalla tua integrazione TTS. Primo, il modello, che definisce qualità e velocità. Secondo, il formato di output, che definisce container, bitrate e sample rate.

Scegliere bene questi due aspetti fin dall’inizio ti assicura che tutto il resto, come latenza e compatibilità telefonica, funzioni senza problemi.

Modelli

Offriamo diversi modelli Text to Speech. Non sono ordinati dal migliore al peggiore; ognuno fa scelte diverse.

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

Nota: il valore di ~75ms è il tempo di inferenza del modello in condizioni rappresentative, esclusa la latenza di rete e applicazione. Aumenta con input più lunghi e sotto carico. Misura sempre dal tuo ambiente, non da un benchmark.

I modelli Flash sono più piccoli e usano approssimazioni più spinte per ridurre il tempo di inferenza. Eleven v3 e Multilingual v2 sono modelli più grandi che impiegano più tempo per carattere per produrre un output più ricco. Non esiste un’impostazione che ti dia la qualità di Eleven v3 alla velocità di Flash, perché quella qualità richiede più calcolo.

Per casi in tempo reale o agenti, usa eleven_flash_v2_5: è l’opzione multilingue con la latenza più bassa. Per narrazione, audiolibri o voiceover marketing, usa eleven_multilingual_v2 se vuoi alta fedeltà stabile, oppure eleven_v3 se ti serve massima espressività e gamma emotiva.

Quando la pronuncia è importante, come per numeri di telefono, date o valute, normalizza tu stesso i numeri nella tua applicazione prima che il testo arrivi all’API. Scrivi la forma parlata che desideri.

Normalizzare tu stesso garantisce una pronuncia prevedibile su tutti i modelli ed evita di dipendere da impostazioni predefinite che potrebbero cambiare.

Formato di output

Il parametro output_format controlla container, sample rate e bitrate dell’audio che ricevi. I valori che userai più spesso:

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

Impostazioni voce

Le seguenti impostazioni controllano come viene consegnato il parlato generato:

  • Stability: Controlla il bilanciamento tra coerenza ed espressività. Valori bassi producono parlato più vario ed espressivo, valori alti danno una resa più stabile e prevedibile.
  • SimilarityBoost: Controlla quanto l’output segue la voce di riferimento.
  • Style: Esalta lo stile naturale della voce se aumentato.
  • useSpeakerBoost: Rafforza la somiglianza con lo speaker originale con un piccolo aumento di latenza.
  • Speed: Regola la velocità di lettura intorno al valore predefinito di 1.0.

Tra queste impostazioni, Stability ha di solito l’impatto maggiore sulla qualità percepita. Valori bassi creano output più espressivi ma meno coerenti, mentre valori alti privilegiano coerenza e prevedibilità.

Quando scegli una voce, la combinazione a latenza più bassa è Flash insieme a una Instant Voice Clone o una voce predefinita; le Professional Voice Clones hanno un’ottima resa ma aggiungono un overhead per ogni generazione che va considerato.

In questa guida, l’id voce di esempio è JBFqnCBsd6RMkjVDRZzb (George).

Integrazione streaming (HTTP e WebSocket)

In questa sezione vediamo la parte pratica dell’integrazione API Text to Speech: installazione SDK, apertura dello stream e consumo dell’audio in arrivo. Il percorso HTTP copre la maggior parte dei casi web e app, mentre il percorso WebSocket copre agenti e output LLM in tempo reale.

Entrambi i percorsi presuppongono che tu abbia già inizializzato il client ElevenLabs come sotto.

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

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

Il percorso streaming apre uno stream e consuma i chunk man mano che arrivano. voiceId è il primo argomento posizionale, seguito da un oggetto opzioni con chiavi in 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
}

Per la variante WebSocket, connettiti a wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input, invia un primo messaggio con le impostazioni voce e uno spazio iniziale, poi invia i messaggi di testo appena disponibili e leggi i frame JSON il cui campo audio contiene i chunk codificati in base64.

Batching e limiti di concorrenza per throughput elevato

L’integrazione ad alto throughput è regolata dalla concorrenza, cioè il numero di richieste che generano audio nello stesso istante. Ogni piano ha un limite per famiglia di modelli.

Ogni piano prevede un limite di concorrenza distinto:

  • Free: 4 richieste Flash simultanee.
  • Starter: 6 richieste Flash simultanee.
  • Creator: 10 richieste Flash simultanee.
  • Pro: 20 richieste Flash simultanee.
  • Scale e Business: 30 richieste Flash simultanee, con limiti Enterprise personalizzati.

I limiti per Multilingual v2 sono circa la metà di quelli sopra.

Un pool limitato risolve questo problema limitando quante richieste partono insieme:

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

Imposta MAX_CONCURRENCY leggermente sotto il limite del tuo piano invece che esattamente uguale. Questo margine assorbe altro traffico che usa la stessa chiave e ti mantiene sotto la soglia che genera un 429.

Limiti di caratteri e suddivisione di testi lunghi

Ogni modello ha un limite massimo di caratteri accettati per richiesta. Qualsiasi integrazione long-form deve suddividere il testo e ricomporre l’audio.

Ecco i limiti di caratteri per richiesta per ogni modello:

  • Flash v2.5: Accetta fino a 40.000 caratteri per richiesta.
  • Flash v2: Accetta fino a 30.000 caratteri per richiesta.
  • Multilingual v2: Accetta fino a 10.000 caratteri per richiesta.
  • Eleven v3: Accetta fino a 5.000 caratteri per richiesta.

Per testi più lunghi serve suddividerli in più richieste. Cerca di dividere sui confini di frase così la prosodia resta naturale tra i chunk.

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

Genera i chunk in ordine e concatena l’audio. Per narrazione long-form dove ogni chunk è indipendente, i due passaggi si compongono direttamente: passa l’output di splitText al pool limitato sopra e lascia che gestisca il resto.

Caching e idempotenza

L’output Text to Speech è abbastanza deterministico che rigenerare lo stesso testo con la stessa voce, modello e impostazioni è uno spreco. Fai cache del risultato usando un hash degli input che influenzano l’audio, e la stessa chiave serve anche come token di idempotenza nei retry.

Ecco come fare entrambe le cose.

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

La regola è che ogni parametro che cambia l’audio deve essere nella chiave, incluso outputFormat e impostazioni voce. Se fatto correttamente, la stessa chiave vale come token di idempotenza. Se un client ripete una richiesta già riuscita, restituisci i byte in cache invece di rigenerare.

Gestione errori e limiti di rate (429)

Un client in produzione deve gestire retry con backoff e jitter, più una gestione diversa a seconda dello status code, perché alcuni errori vanno riprovati e altri no.

La tabella qui sotto associa ogni status all’azione corretta, e la sezione spiega perché un 429 è un limite soft e non un muro invalicabile.

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

Un 429 non è un muro invalicabile, ed è utile capire il meccanismo. Quando superi il limite di concorrenza, le richieste vengono prima messe in coda per priorità, aggiungendo tipicamente circa 50ms. Solo se resti sopra la capacità dopo questo ricevi un 429.

La risposta include anche gli header current-concurrent-requests e maximum-concurrent-requests che mostrano il margine in tempo reale, così puoi leggerli e rallentare prima di raggiungere il limite.

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

Se ti serve più margine invece di un retry migliore, aggiorna il tuo piano. I clienti Enterprise possono richiedere limiti più alti tramite il proprio account manager.

Benchmarking della latenza e time-to-first-byte

La latenza dipende dalla tua regione, dal tuo input e dal carico attuale, quindi l’unico valore di latenza affidabile è quello misurato nel tuo ambiente.

Questa sezione ti dà il time-to-first-byte (TTFB) per l’endpoint Flash streaming, ed è strutturata così puoi usare lo stesso test anche su altri provider e confrontarli in condizioni identiche.

Considera questa come una metodologia, non come un risultato pubblicato. Nessuna singola esecuzione è una garanzia.

Ecco alcune note importanti quando fai benchmark della latenza di una integrazione API Text to Speech:

  • Includi il round-trip di rete: Il TTFB dipende dalla tua posizione geografica e dal cluster più vicino del provider, quindi esegui il test dove girano di solito i tuoi server.
  • Scarta una run di warmup: La prima richiesta su una connessione fredda è più lenta e può falsare i risultati.
  • Tieni gli input fissi: Lunghezza input, voce, modello e carico influenzano il risultato, quindi mantienili identici tra provider.
  • Riporta una distribuzione: I valori variano tra le esecuzioni, quindi pubblica la mediana e il p95 invece di un singolo valore.

Con queste accortezze, sei pronto per il 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");
}

Per confrontare con un altro provider, scrivi una funzione con la stessa struttura. Poi esegui entrambi con un runner che scarta una chiamata di warm-up, prende circa 20 campioni distanziati per evitare collisioni, e riporta mediana e p95 in millisecondi.

Un confronto equo dipende dal controllo delle variabili.

Esegui entrambi i provider dalla stessa macchina e rete, idealmente un server nella regione dove effettivamente distribuisci, non un laptop su una connessione domestica. Usa lo stesso testo di input e mantieni l’audio breve così il tempo di inferenza del modello domina il risultato invece della lunghezza di generazione. Riporta mediana e p95 su molte esecuzioni, perché una singola misura è rumore.

Ricorda che il TTFB su internet pubblica include 20-200ms di round-trip di rete che non dipendono dal modello. Serviamo da cluster in Nord America, Europa e Sud-Est Asiatico e instradiamo verso il più vicino, quindi posiziona il client di test di conseguenza, altrimenti stai misurando soprattutto la distanza dal data center.

Punti chiave per la tua integrazione API Text to Speech

Un’integrazione API Text to Speech in produzione si basa su poche decisioni davvero importanti.

Se scegli bene questi aspetti, tutto il resto funziona:

  • Scegli il modello in base all’uso: Usa Flash v2.5 per tutto ciò che è interattivo e un modello a maggiore fedeltà come Multilingual v2 o Eleven v3 per rendering offline dove la latenza conta meno.
  • Usa lo streaming quando l’utente aspetta: Usa lo streaming HTTP per testo noto e WebSocket per agenti, così il tempo inattivo non pesa sul budget di concorrenza.
  • Tieni la parallelizzazione sotto il limite del piano: Limita le richieste simultanee appena sotto il limite del piano e fai cache su un hash di ogni parametro che influenza l’output, così lo stesso audio non viene mai fatturato due volte.
  • Riprova 429 e 5xx con backoff esponenziale e jitter completo: Fai backoff su 429 e 5xx con jitter completo e controlla gli header di concorrenza per vedere quanto sei vicino al limite.
  • Dividi i testi lunghi sui confini di frase: Suddividi sui confini di frase entro il limite di caratteri di ogni modello così la prosodia resta naturale.

Se vuoi approfondire ancora, dai un’occhiata a come fare lo streaming, il concetto di streaming audio, autenticazione, e token monouso per uso client-side.

Crea la tua integrazione Text to Speech con ElevenAPI

Dopo aver letto questa guida, hai tutti i pattern necessari per un’integrazione API Text to Speech in produzione. Tra streaming, batching, caching, retry e anche benchmarking, sei pronto per andare in produzione.

Inizia scoprendo di più sulla Text to Speech API oppure registrati per fare la tua prima chiamata con ElevenAPI oggi stesso.

FAQ sull’integrazione API Text to Speech

Articoli simili

Crea con l'audio IA della massima qualità