Vai alla navigazione

Streaming e caching con Supabase

Genera e trasmetti il parlato tramite Supabase Edge Functions. Archivia il parlato in Supabase Storage e memorizza nella cache le risposte tramite la CDN integrata.

Guida pratica · Presuppone che tu abbia completato la guida rapida di ElevenAPI e disponga di un account Supabase.

Introduzione

In questa guida imparerai a creare un’API edge per generare, trasmettere in streaming, archiviare e memorizzare nella cache il parlato usando Supabase Edge Functions, Supabase Storage ed ElevenLabs.

Requisiti

Configurazione

Crea un progetto Supabase in locale

Dopo aver installato la CLI di Supabase, esegui il comando seguente per creare un nuovo progetto Supabase in locale:

supabase init

Configura il bucket di storage

Puoi configurare la CLI di Supabase affinché generi automaticamente un bucket di storage aggiungendo questa configurazione al file config.toml:

./supabase/config.toml
[storage.buckets.audio]
public = false
file_size_limit = "50MiB"
allowed_mime_types = ["audio/mp3"]
objects_path = "./audio"

Eseguendo supabase start verrà creato un nuovo bucket di storage nel tuo progetto Supabase locale. Se vuoi inviarlo al tuo progetto Supabase fornito, puoi eseguire supabase seed buckets --linked.

Configura le attività in background per Supabase Edge Functions

Per usare le attività in background in Supabase Edge Functions durante lo sviluppo locale, devi aggiungere la seguente configurazione al file config.toml:

./supabase/config.toml
[edge_runtime]
policy = "per_worker"

Quando esegui con la policy per_worker, la Function non verrà ricaricata automaticamente dopo le modifiche. Dovrai riavviarla manualmente eseguendo supabase functions serve.

Crea una Supabase Edge Function per la generazione del parlato

Crea una nuova Edge Function eseguendo il comando seguente:

supabase functions new text-to-speech

Se usi VS Code o Cursor, seleziona y quando la CLI chiede “Generate VS Code settings for Deno? [y/N]”!

Configura le variabili d’ambiente

All’interno della directory supabase/functions, crea un nuovo file .env e aggiungi le seguenti variabili:

supabase/functions/.env
# Find / create an API key at https://elevenlabs.io/app/settings/api-keys
ELEVENLABS_API_KEY=your_api_key

Dipendenze

Il progetto usa alcune dipendenze:

  • La libreria @supabase/supabase-js per interagire con il database Supabase.
  • L’SDK JavaScript di ElevenLabs per interagire con l’API Text to Speech.
  • Il pacchetto open source object-hash per generare un hash dai parametri della richiesta.

Poiché Supabase Edge Function usa il runtime Deno, non devi installare le dipendenze: puoi invece importarle tramite il prefisso npm:.

Scrivi il codice della Supabase Edge Function

Nel file appena creato supabase/functions/text-to-speech/index.ts, aggiungi il seguente codice:

supabase/functions/text-to-speech/index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";
import { ElevenLabsClient } from "npm:elevenlabs";
import * as hash from "npm:object-hash";
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const elevenlabs = new ElevenLabsClient({
apiKey: Deno.env.get("ELEVENLABS_API_KEY"),
});
// Upload audio to Supabase Storage in a background task
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
const { data, error } = await supabase.storage
.from("audio")
.upload(`${requestHash}.mp3`, stream, {
contentType: "audio/mp3",
});
console.log("Storage upload result", { data, error });
}
Deno.serve(async (req) => {
// To secure your function for production, you can for example validate the request origin,
// or append a user access token and validate it with Supabase Auth.
console.log("Request origin", req.headers.get("host"));
const url = new URL(req.url);
const params = new URLSearchParams(url.search);
const text = params.get("text");
const voiceId = params.get("voiceId") ?? "JBFqnCBsd6RMkjVDRZzb";
const requestHash = hash.MD5({ text, voiceId });
console.log("Request hash", requestHash);
// Check storage for existing audio file
const { data } = await supabase.storage.from("audio").createSignedUrl(`${requestHash}.mp3`, 60);
if (data) {
console.log("Audio file found in storage", data);
const storageRes = await fetch(data.signedUrl);
if (storageRes.ok) return storageRes;
}
if (!text) {
return new Response(JSON.stringify({ error: "Text parameter is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
console.log("ElevenLabs API call");
const response = await elevenlabs.textToSpeech.stream(voiceId, {
output_format: "mp3_44100_128",
model_id: "eleven_multilingual_v2",
text,
});
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of response) {
controller.enqueue(chunk);
}
controller.close();
},
});
// Branch stream to Supabase Storage
const [browserStream, storageStream] = stream.tee();
// Upload to Supabase Storage in the background
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash));
// Return the streaming response immediately
return new Response(browserStream, {
headers: {
"Content-Type": "audio/mpeg",
},
});
} catch (error) {
console.log("error", { error });
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
});

Analisi approfondita del codice

Ci sono alcuni aspetti del codice che vale la pena notare. Vediamoli passo dopo passo.

1

Gestisci la richiesta in arrivo

Per gestire la richiesta in arrivo, usa l’handler Deno.serve. Nella demo non convalidiamo l’origine della richiesta, ma puoi ad esempio convalidarla oppure aggiungere un token di accesso utente e convalidarlo con Supabase Auth.

Dalla richiesta in arrivo, la funzione estrae i parametri text e voiceId. Il parametro voiceId è facoltativo e usa per impostazione predefinita l’ID ElevenLabs della voce “Allison”.

Usando la libreria object-hash, la funzione genera un hash dai parametri della richiesta. Questo hash viene usato per verificare la presenza di file audio esistenti in Supabase Storage.

Deno.serve(async (req) => {
// To secure your function for production, you can for example validate the request origin,
// or append a user access token and validate it with Supabase Auth.
console.log("Request origin", req.headers.get("host"));
const url = new URL(req.url);
const params = new URLSearchParams(url.search);
const text = params.get("text");
const voiceId = params.get("voiceId") ?? "JBFqnCBsd6RMkjVDRZzb";
const requestHash = hash.MD5({ text, voiceId });
console.log("Request hash", requestHash);
// ...
})
2

Verifica la presenza di un file audio in Supabase Storage

Supabase Storage include una CDN intelligente integrata che ti consente di memorizzare facilmente i file nella cache e fornirli.

Qui, la funzione verifica la presenza di un file audio in Supabase Storage. Se il file esiste, la funzione restituisce il file da Supabase Storage.

const { data } = await supabase
.storage
.from("audio")
.createSignedUrl(`${requestHash}.mp3`, 60);
if (data) {
console.log("Audio file found in storage", data);
const storageRes = await fetch(data.signedUrl);
if (storageRes.ok) return storageRes;
}
3

Genera il parlato come stream e dividilo in due rami

Usando le capacità di streaming dell’API ElevenLabs, la funzione genera uno stream. Il vantaggio è che, anche con testi più lunghi, puoi iniziare subito a trasmettere l’audio in streaming al tuo utente e caricare poi lo stream su Supabase Storage in background.

Questo offre la migliore esperienza utente possibile, facendo sembrare incredibilmente rapidi anche grandi blocchi di testo. Il punto chiave è alla riga 17, dove il metodo stream.tee() divide il readablestream in due rami: uno per il browser e uno per Supabase Storage.

try {
const response = await elevenlabs.textToSpeech.stream(voiceId, {
output_format: "mp3_44100_128",
model_id: "eleven_multilingual_v2",
text,
});
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of response) {
controller.enqueue(chunk);
}
controller.close();
},
});
// Branch stream to Supabase Storage
const [browserStream, storageStream] = stream.tee();
// Upload to Supabase Storage in the background
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash));
// Return the streaming response immediately
return new Response(browserStream, {
headers: {
"Content-Type": "audio/mpeg",
},
});
} catch (error) {
console.log("error", { error });
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
4

Carica lo stream audio su Supabase Storage in background

Il metodo EdgeRuntime.waitUntil alla riga 20 del passaggio precedente viene usato per caricare lo stream audio su Supabase Storage in background tramite la funzione uploadAudioToStorage. Questo consente alla funzione di restituire immediatamente la risposta in streaming al browser mentre l’audio viene caricato su Supabase Storage.

Una volta creato l’oggetto di storage, la prossima volta che gli utenti effettueranno una richiesta con gli stessi parametri, la funzione restituirà il file audio dalla CDN di Supabase Storage.

// Upload audio to Supabase Storage in a background task
async function uploadAudioToStorage(
stream: ReadableStream,
requestHash: string,
) {
const { data, error } = await supabase.storage
.from("audio")
.upload(`${requestHash}.mp3`, stream, {
contentType: "audio/mp3",
});
console.log("Storage upload result", { data, error });
}

Esegui in locale

Per eseguire la funzione in locale, esegui i seguenti comandi:

supabase start

Una volta avviato lo stack Supabase locale, esegui il comando seguente per avviare la funzione e visualizzare i log:

supabase functions serve

Provalo

Vai a http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world per ascoltare la funzione in azione.

Poi vai a http://127.0.0.1:54323/project/default/storage/buckets/audio per vedere il file audio nel bucket Supabase Storage locale.

Esegui il deploy su Supabase

Se non l’hai già fatto, crea un nuovo account Supabase su database.new e collega il progetto locale al tuo account Supabase:

supabase link

Una volta fatto, esegui il comando seguente per distribuire la funzione:

supabase functions deploy

Imposta i secret della funzione

Ora che hai configurato tutti i secret in locale, puoi eseguire il comando seguente per impostarli nel tuo progetto Supabase:

supabase secrets set --env-file supabase/functions/.env

Testa la funzione

La funzione è progettata per poter essere usata direttamente come sorgente di un elemento <audio>.

<audio
src="https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/text-to-speech?text=Hello%2C%20world!&voiceId=JBFqnCBsd6RMkjVDRZzb"
controls
/>

Passaggi successivi