Skip to navigation

Streamen en cachen met Supabase

Genereer en stream spraak via Supabase Edge Functions. Sla spraak op in Supabase Storage en cache antwoorden via de ingebouwde CDN.

Handleiding · Hierbij wordt ervan uitgegaan dat je de ElevenAPI quickstart hebt voltooid en een Supabase-account hebt.

Inleiding

In deze handleiding leer je hoe je een edge API bouwt om spraak te genereren, streamen, opslaan en cachen met Supabase Edge Functions, Supabase Storage en ElevenLabs.

Vereisten

Instellen

Lokaal een Supabase-project maken

Voer na het installeren van de Supabase CLI de volgende opdracht uit om lokaal een nieuw Supabase-project te maken:

supabase init

De storagebucket configureren

Je kunt de Supabase CLI automatisch een storagebucket laten genereren door deze configuratie toe te voegen aan het bestand config.toml:

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

Wanneer je supabase start uitvoert, wordt hiermee een nieuwe storagebucket gemaakt in je lokale Supabase- project. Als je deze wilt pushen naar je gehoste Supabase-project, kun je supabase seed buckets --linked uitvoeren.

Achtergrondtaken voor Supabase Edge Functions configureren

Om achtergrondtaken in Supabase Edge Functions te gebruiken wanneer je lokaal ontwikkelt, moet je de volgende configuratie toevoegen aan het bestand config.toml:

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

Wanneer je met het beleid per_worker werkt, wordt Function niet automatisch opnieuw geladen na wijzigingen. Je moet deze handmatig herstarten door supabase functions serve uit te voeren.

Een Supabase Edge Function maken voor het genereren van spraak

Maak een nieuwe Edge Function door de volgende opdracht uit te voeren:

supabase functions new text-to-speech

Als je VS Code of Cursor gebruikt, selecteer je y wanneer de CLI vraagt: “Generate VS Code settings for Deno? [y/N]”!

De omgevingsvariabelen instellen

Maak in de directory supabase/functions een nieuw .env-bestand en voeg de volgende variabelen toe:

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

Afhankelijkheden

Het project gebruikt een aantal afhankelijkheden:

  • De bibliotheek @supabase/supabase-js om met de Supabase-database te werken.
  • De ElevenLabs JavaScript SDK om met de text-to-speech API te werken.
  • De open-sourcebibliotheek object-hash om een hash uit de requestparameters te genereren.

Omdat Supabase Edge Function de Deno runtime gebruikt, hoef je de afhankelijkheden niet te installeren. Je kunt ze via het voorvoegsel npm: importeren.

De Supabase Edge Function coderen

Voeg de volgende code toe aan je nieuw gemaakte bestand supabase/functions/text-to-speech/index.ts:

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

Verdieping in de code

Er zijn enkele zaken in de code die het vermelden waard zijn. Laten we ze stap voor stap doornemen.

1

Het binnenkomende request afhandelen

Gebruik de handler Deno.serve om het binnenkomende request af te handelen. In de demo valideren we de request origin niet, maar je kunt bijvoorbeeld de request origin valideren, of een gebruikerstoegangstoken toevoegen en dit valideren met Supabase Auth.

Uit het binnenkomende request haalt de functie de parameters text en voiceId. De parameter voiceId is optioneel en heeft standaard de ElevenLabs-ID voor de stem “Allison”.

Met de bibliotheek object-hash genereert de functie een hash uit de requestparameters. Deze hash wordt gebruikt om te controleren op bestaande audiobestanden 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

Controleren op bestaand audiobestand in Supabase Storage

Supabase Storage heeft een slimme CDN ingebouwd, waarmee je je bestanden eenvoudig kunt cachen en aanbieden.

Hier controleert de functie op een bestaand audiobestand in Supabase Storage. Als het bestand bestaat, retourneert de functie het bestand uit 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

Spraak als stream genereren en opsplitsen in twee vertakkingen

Met de streamingmogelijkheden van de ElevenLabs API genereert de functie een stream. Het voordeel is dat je, ook bij grotere teksten, de audio onmiddellijk naar je gebruiker kunt streamen en de stream vervolgens op de achtergrond kunt uploaden naar Supabase Storage.

Dit zorgt voor de best mogelijke gebruikerservaring, waardoor zelfs grote tekstblokken magisch snel aanvoelen. Dit gebeurt op regel 17, waar de methode stream.tee() de readablestream opsplitst in twee vertakkingen: één voor de browser en één voor 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

De audiostream op de achtergrond uploaden naar Supabase Storage

De methode EdgeRuntime.waitUntil op regel 20 in de vorige stap wordt gebruikt om de audiostream op de achtergrond te uploaden naar Supabase Storage met de functie uploadAudioToStorage. Hierdoor kan de functie het streamingantwoord onmiddellijk aan de browser retourneren terwijl de audio naar Supabase Storage wordt geüpload.

Zodra het storageobject is gemaakt, retourneert de functie het audiobestand via de Supabase Storage CDN wanneer je gebruikers de volgende keer een request met dezelfde parameters doen.

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

Lokaal uitvoeren

Voer de volgende opdrachten uit om de functie lokaal uit te voeren:

supabase start

Zodra de lokale Supabase-stack actief is, voer je de volgende opdracht uit om de functie te starten en de logs te bekijken:

supabase functions serve

Probeer het

Ga naar http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world om de functie in actie te horen.

Ga daarna naar http://127.0.0.1:54323/project/default/storage/buckets/audio om het audiobestand in je lokale Supabase Storage-bucket te zien.

Deployen naar Supabase

Als je dat nog niet hebt gedaan, maak dan een nieuw Supabase-account aan op database.new en koppel het lokale project aan je Supabase-account:

supabase link

Voer daarna de volgende opdracht uit om de functie te deployen:

supabase functions deploy

De function secrets instellen

Nu je al je secrets lokaal hebt ingesteld, kun je de volgende opdracht uitvoeren om de secrets in je Supabase-project in te stellen:

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

De functie testen

De functie is zo ontworpen dat deze rechtstreeks als bron voor een <audio>-element kan worden gebruikt.

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

Volgende stappen