Streaming et mise en cache avec Supabase

Générez et diffusez de la parole via les Edge Functions de Supabase. Stockez la parole dans Supabase Storage et mettez les réponses en cache via le CDN intégré.

Guide pratique · Suppose que vous avez terminé le guide de démarrage rapide ElevenAPI et que vous disposez d’un compte Supabase.

Introduction

Dans ce guide, vous apprendrez à créer une API edge pour générer, diffuser en streaming, stocker et mettre en cache de la parole avec les Edge Functions de Supabase, Supabase Storage et ElevenLabs.

Prérequis

Configuration

Créer un projet Supabase localement

Après avoir installé la CLI Supabase, exécutez la commande suivante pour créer un projet Supabase localement :

supabase init

Configurer le bucket de stockage

Vous pouvez configurer la CLI Supabase pour générer automatiquement un bucket de stockage en ajoutant cette configuration au fichier config.toml :

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

L’exécution de supabase start crée un bucket de stockage dans votre projet Supabase local. Pour le transférer vers votre projet Supabase hébergé, exécutez supabase seed buckets --linked.

Configurer les tâches en arrière-plan pour les Edge Functions de Supabase

Pour utiliser des tâches en arrière-plan dans les Edge Functions de Supabase lors du développement local, ajoutez la configuration suivante au fichier config.toml :

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

Avec la politique per_worker, la Function ne se recharge pas automatiquement après les modifications. Vous devez la redémarrer manuellement en exécutant supabase functions serve.

Créer une Edge Function Supabase pour la génération de parole

Créez une nouvelle Edge Function en exécutant la commande suivante :

supabase functions new text-to-speech

Si vous utilisez VS Code ou Cursor, sélectionnez y lorsque la CLI vous invite avec « Generate VS Code settings for Deno? [y/N] ».

Configurer les variables d’environnement

Dans le répertoire supabase/functions, créez un fichier .env et ajoutez les variables suivantes :

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

Dépendances

Le projet utilise quelques dépendances :

  • La bibliothèque @supabase/supabase-js pour interagir avec la base de données Supabase.
  • Le SDK JavaScript ElevenLabs pour interagir avec l’API de synthèse vocale.
  • La bibliothèque open source object-hash pour générer un hash à partir des paramètres de requête.

Comme les Edge Functions de Supabase utilisent le runtime Deno, vous n’avez pas besoin d’installer les dépendances : vous pouvez les importer via le préfixe npm:.

Programmer l’Edge Function Supabase

Dans le fichier supabase/functions/text-to-speech/index.ts que vous venez de créer, ajoutez le code suivant :

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

Analyse détaillée du code

Quelques éléments du code méritent d’être soulignés. Examinons-les étape par étape.

1

Gérer la requête entrante

Pour gérer la requête entrante, utilisez le gestionnaire Deno.serve. Dans la démonstration, nous ne validons pas l’origine de la requête, mais vous pouvez par exemple la valider ou ajouter un jeton d’accès utilisateur et le valider avec Supabase Auth.

À partir de la requête entrante, la fonction extrait les paramètres text et voiceId. Le paramètre voiceId est facultatif et utilise par défaut l’ID ElevenLabs de la voix « Allison ».

À l’aide de la bibliothèque object-hash, la fonction génère un hash à partir des paramètres de requête. Ce hash sert à vérifier l’existence de fichiers audio dans 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

Vérifier l'existence d'un fichier audio dans Supabase Storage

Supabase Storage intègre un CDN intelligent qui vous permet de mettre facilement vos fichiers en cache et de les diffuser.

Ici, la fonction vérifie l’existence d’un fichier audio dans Supabase Storage. Si le fichier existe, elle le renvoie depuis 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

Générer la parole sous forme de flux et la diviser en deux branches

Grâce aux capacités de streaming de l’API ElevenLabs, la fonction génère un flux. Même pour des textes plus longs, vous pouvez ainsi commencer immédiatement à diffuser l’audio vers votre utilisateur, puis téléverser le flux vers Supabase Storage en arrière-plan.

Vous offrez ainsi une expérience utilisateur optimale, même les longs blocs de texte paraissant très rapides. Cela se produit à la ligne 17, où la méthode stream.tee() divise le flux lisible en deux branches : une pour le navigateur et une pour 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

Téléverser le flux audio vers Supabase Storage en arrière-plan

La méthode EdgeRuntime.waitUntil de la ligne 20 de l’étape précédente téléverse le flux audio vers Supabase Storage en arrière-plan à l’aide de la fonction uploadAudioToStorage. La fonction peut ainsi renvoyer immédiatement la réponse en streaming au navigateur pendant que l’audio est téléversé vers Supabase Storage.

Une fois l’objet de stockage créé, la prochaine fois que vos utilisateurs effectueront une requête avec les mêmes paramètres, la fonction renverra le fichier audio depuis le 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 });
}

Exécuter localement

Pour exécuter la fonction localement, lancez les commandes suivantes :

supabase start

Une fois la pile Supabase locale opérationnelle, exécutez la commande suivante pour démarrer la fonction et consulter les journaux :

supabase functions serve

Essayer

Accédez à http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world pour entendre la fonction en action.

Accédez ensuite à http://127.0.0.1:54323/project/default/storage/buckets/audio pour voir le fichier audio dans votre bucket Supabase Storage local.

Déployer sur Supabase

Si ce n’est pas déjà fait, créez un compte Supabase sur database.new, puis liez le projet local à votre compte Supabase :

supabase link

Une fois terminé, exécutez la commande suivante pour déployer la fonction :

supabase functions deploy

Définir les secrets de la fonction

Maintenant que tous vos secrets sont configurés localement, vous pouvez exécuter la commande suivante pour les définir dans votre projet Supabase :

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

Tester la fonction

La fonction est conçue pour être utilisée directement comme source d’un élément <audio>.

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

Prochaines étapes