Streaming und Caching mit Supabase

Erzeugen und streamen Sie Sprache über Supabase Edge Functions. Speichern Sie Sprache in Supabase Storage und cachen Sie Antworten über das integrierte CDN.

Anleitung · Setzt voraus, dass Sie den ElevenAPI- Schnellstart abgeschlossen haben und über ein Supabase-Konto verfügen.

Einführung

In dieser Anleitung erfahren Sie, wie Sie mit Supabase Edge Functions, Supabase Storage und ElevenLabs eine Edge-API erstellen, um Sprache zu generieren, zu streamen, zu speichern und zwischenzuspeichern.

Voraussetzungen

Einrichtung

Ein Supabase-Projekt lokal erstellen

Führen Sie nach der Installation der Supabase CLI den folgenden Befehl aus, um lokal ein neues Supabase-Projekt zu erstellen:

supabase init

Storage-Bucket konfigurieren

Sie können die Supabase CLI so konfigurieren, dass automatisch ein Storage-Bucket erstellt wird. Fügen Sie dazu diese Konfiguration in der Datei config.toml hinzu:

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

Wenn Sie supabase start ausführen, wird in Ihrem lokalen Supabase- Projekt ein neuer Storage-Bucket erstellt. Wenn Sie diesen in Ihr gehostetes Supabase-Projekt übertragen möchten, können Sie supabase seed buckets --linked ausführen.

Hintergrundaufgaben für Supabase Edge Functions konfigurieren

Um bei der lokalen Entwicklung Hintergrundaufgaben in Supabase Edge Functions zu nutzen, müssen Sie die folgende Konfiguration in der Datei config.toml hinzufügen:

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

Bei Verwendung der Richtlinie per_worker wird die Function bei Änderungen nicht automatisch neu geladen. Sie müssen sie manuell neu starten, indem Sie supabase functions serve ausführen.

Eine Supabase Edge Function für die Sprachgenerierung erstellen

Erstellen Sie mit folgendem Befehl eine neue Edge Function:

supabase functions new text-to-speech

Wenn Sie VS Code oder Cursor verwenden, wählen Sie y, wenn die CLI fragt: “Generate VS Code settings for Deno? [y/N]”.

Umgebungsvariablen einrichten

Erstellen Sie im Verzeichnis supabase/functions eine neue Datei .env und fügen Sie die folgenden Variablen hinzu:

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

Abhängigkeiten

Das Projekt verwendet einige Abhängigkeiten:

  • Die Bibliothek @supabase/supabase-js für die Interaktion mit der Supabase-Datenbank.
  • Das ElevenLabs-JavaScript SDK für die Interaktion mit der Text-to-Speech-API.
  • Das Open-Source-Paket object-hash, um aus den Anfrageparametern einen Hash zu generieren.

Da Supabase Edge Functions die Deno-Runtime verwenden, müssen Sie die Abhängigkeiten nicht installieren. Sie können sie über das Präfix npm: importieren.

Supabase Edge Function programmieren

Fügen Sie in Ihrer neu erstellten Datei supabase/functions/text-to-speech/index.ts den folgenden Code hinzu:

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

Detaillierte Code-Erklärung

Einige Aspekte des Codes sind besonders wichtig. Sehen wir sie uns Schritt für Schritt an.

1

Eingehende Anfrage verarbeiten

Verwenden Sie zum Verarbeiten der eingehenden Anfrage den Handler Deno.serve. In der Demo validieren wir den Anfrageursprung nicht. Sie können beispielsweise den Anfrageursprung validieren oder ein Benutzer-Zugriffstoken anhängen und es mit Supabase Auth validieren.

Aus der eingehenden Anfrage extrahiert die Function die Parameter text und voiceId. Der Parameter voiceId ist optional und verwendet standardmäßig die ElevenLabs-ID für die Stimme „Allison“.

Mit der Bibliothek object-hash generiert die Function aus den Anfrageparametern einen Hash. Dieser Hash wird verwendet, um in Supabase Storage nach vorhandenen Audiodateien zu suchen.

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

In Supabase Storage nach vorhandenen Audiodateien suchen

Supabase Storage verfügt über ein integriertes Smart CDN, mit dem Sie Ihre Dateien einfach zwischenspeichern und bereitstellen können.

Hier sucht die Function in Supabase Storage nach einer vorhandenen Audiodatei. Falls die Datei existiert, gibt die Function sie aus Supabase Storage zurück.

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

Sprache als Stream generieren und in zwei Zweige aufteilen

Mit den Streaming-Funktionen der ElevenLabs API generiert die Function einen Stream. Der Vorteil: Auch bei längeren Texten können Sie das Audio sofort an Ihre Nutzer streamen und den Stream anschließend im Hintergrund in Supabase Storage hochladen.

Das ermöglicht die bestmögliche Nutzererfahrung, sodass sich selbst große Textblöcke erstaunlich schnell anfühlen. Dies geschieht in Zeile 17, wo die Methode stream.tee() den ReadableStream in zwei Zweige aufteilt: einen für den Browser und einen für 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

Audiostream im Hintergrund in Supabase Storage hochladen

Die Methode EdgeRuntime.waitUntil in Zeile 20 des vorherigen Schritts lädt den Audiostream mit der Function uploadAudioToStorage im Hintergrund in Supabase Storage hoch. Dadurch kann die Function die Streaming-Antwort sofort an den Browser zurückgeben, während das Audio in Supabase Storage hochgeladen wird.

Sobald das Storage-Objekt erstellt wurde, gibt die Function die Audiodatei beim nächsten Aufruf Ihrer Nutzer mit denselben Parametern über das Supabase-Storage-CDN zurück.

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

Lokal ausführen

Um die Function lokal auszuführen, führen Sie folgende Befehle aus:

supabase start

Sobald der lokale Supabase-Stack läuft, starten Sie die Function mit folgendem Befehl und beobachten die Logs:

supabase functions serve

Ausprobieren

Rufen Sie http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world auf, um die Function in Aktion zu hören.

Rufen Sie anschließend http://127.0.0.1:54323/project/default/storage/buckets/audio auf, um die Audiodatei in Ihrem lokalen Supabase-Storage-Bucket zu sehen.

Bei Supabase bereitstellen

Falls noch nicht geschehen, erstellen Sie unter database.new ein neues Supabase-Konto und verknüpfen Sie das lokale Projekt mit Ihrem Supabase-Konto:

supabase link

Führen Sie anschließend folgenden Befehl aus, um die Function bereitzustellen:

supabase functions deploy

Secrets der Function festlegen

Nachdem Sie alle Secrets lokal festgelegt haben, können Sie mit folgendem Befehl die Secrets in Ihrem Supabase-Projekt setzen:

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

Function testen

Die Function ist so konzipiert, dass sie direkt als Quelle für ein <audio>-Element verwendet werden kann.

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

Nächste Schritte