Streaming y caché con Supabase

Genera y transmite voz mediante las Edge Functions de Supabase. Almacena la voz en Supabase Storage y guarda las respuestas en caché mediante la CDN integrada.

Guía práctica · Da por hecho que has completado la guía de inicio rápido de ElevenAPI y que tienes una cuenta de Supabase.

Introducción

En esta guía aprenderás a crear una API perimetral para generar, transmitir, almacenar y guardar en caché voz mediante Supabase Edge Functions, Supabase Storage y ElevenLabs.

Requisitos

Configuración

Crea un proyecto de Supabase localmente

Después de instalar la CLI de Supabase, ejecuta el siguiente comando para crear un proyecto de Supabase localmente:

supabase init

Configura el bucket de almacenamiento

Puedes configurar la CLI de Supabase para generar automáticamente un bucket de almacenamiento añadiendo esta configuración al archivo config.toml:

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

Al ejecutar supabase start, se creará un nuevo bucket de almacenamiento en tu proyecto local de Supabase. Si quieres enviar esto a tu proyecto de Supabase alojado, puedes ejecutar supabase seed buckets --linked.

Configura tareas en segundo plano para Supabase Edge Functions

Para usar tareas en segundo plano en Supabase Edge Functions durante el desarrollo local, debes añadir la siguiente configuración al archivo config.toml:

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

Al ejecutar con la política per_worker, Function no se recargará automáticamente al editarla. Tendrás que reiniciarla manualmente ejecutando supabase functions serve.

Crea una Supabase Edge Function para generar voz

Crea una nueva Edge Function ejecutando el siguiente comando:

supabase functions new text-to-speech

Si utilizas VS Code o Cursor, selecciona y cuando la CLI te pregunte «Generate VS Code settings for Deno? [y/N]».

Configura las variables de entorno

En el directorio supabase/functions, crea un archivo .env nuevo y añade las siguientes variables:

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

Dependencias

El proyecto utiliza un par de dependencias:

  • La biblioteca @supabase/supabase-js para interactuar con la base de datos de Supabase.
  • El SDK de JavaScript de ElevenLabs para interactuar con la API de Texto a Voz.
  • El proyecto de código abierto object-hash para generar un hash a partir de los parámetros de la solicitud.

Como Supabase Edge Function usa el runtime de Deno, no necesitas instalar las dependencias: puedes importarlas mediante el prefijo npm:.

Programa la Supabase Edge Function

En el archivo supabase/functions/text-to-speech/index.ts que acabas de crear, añade el siguiente código:

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

Análisis detallado del código

Hay un par de aspectos del código que merece la pena destacar. Vamos a verlo paso a paso.

1

Gestiona la solicitud entrante

Para gestionar la solicitud entrante, usa el controlador Deno.serve. En la demo no validamos el origen de la solicitud, pero puedes, por ejemplo, validarlo o añadir un token de acceso de usuario y validarlo con Supabase Auth.

A partir de la solicitud entrante, la función extrae los parámetros text y voiceId. El parámetro voiceId es opcional y, de forma predeterminada, usa el ID de ElevenLabs de la voz «Allison».

Mediante la biblioteca object-hash, la función genera un hash a partir de los parámetros de la solicitud. Este hash se usa para comprobar si existen archivos de audio en 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

Comprueba si existe un archivo de audio en Supabase Storage

Supabase Storage incluye una CDN inteligente integrada que te permite guardar en caché y servir fácilmente tus archivos.

Aquí, la función comprueba si existe un archivo de audio en Supabase Storage. Si el archivo existe, la función lo devuelve desde 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 voz como flujo y divídelo en dos ramas

Mediante las capacidades de transmisión de la API de ElevenLabs, la función genera un flujo. La ventaja es que, incluso con textos más largos, puedes empezar a transmitir el audio a tu usuario de inmediato y subir el flujo a Supabase Storage en segundo plano.

Esto ofrece la mejor experiencia posible a usuarios y hace que incluso los bloques de texto grandes parezcan increíblemente rápidos. La magia ocurre en la línea 17, donde el método stream.tee() divide el flujo legible en dos ramas: una para el navegador y otra para 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

Sube el flujo de audio a Supabase Storage en segundo plano

El método EdgeRuntime.waitUntil de la línea 20 del paso anterior se usa para subir el flujo de audio a Supabase Storage en segundo plano mediante la función uploadAudioToStorage. Esto permite que la función devuelva la respuesta en transmisión al navegador inmediatamente mientras el audio se sube a Supabase Storage.

Una vez creado el objeto de almacenamiento, la próxima vez que tus usuarios hagan una solicitud con los mismos parámetros, la función devolverá el archivo de audio desde la CDN de 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 });
}

Ejecuta localmente

Para ejecutar la función localmente, usa los siguientes comandos:

supabase start

Cuando el stack local de Supabase esté en funcionamiento, ejecuta el siguiente comando para iniciar la función y ver los registros:

supabase functions serve

Pruébalo

Ve a http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world para escuchar la función en acción.

Después, ve a http://127.0.0.1:54323/project/default/storage/buckets/audio para ver el archivo de audio en tu bucket local de Supabase Storage.

Despliega en Supabase

Si aún no lo has hecho, crea una cuenta nueva de Supabase en database.new y vincula el proyecto local a tu cuenta de Supabase:

supabase link

Cuando termines, ejecuta el siguiente comando para desplegar la función:

supabase functions deploy

Configura los secretos de la función

Ahora que ya tienes todos los secretos configurados localmente, puedes ejecutar el siguiente comando para configurarlos en tu proyecto de Supabase:

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

Prueba la función

La función está diseñada para poder usarse directamente como fuente de un elemento <audio>.

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

Próximos pasos