Streaming i cache'owanie z Supabase

Generuj i streamuj mowę przez Supabase Edge Functions. Przechowuj mowę w Supabase Storage i cache’uj odpowiedzi przez wbudowane CDN.

Przewodnik · Zakłada, że masz za sobą szybki start ElevenAPI i konto Supabase.

Wprowadzenie

Z tego przewodnika dowiesz się, jak zbudować brzegowe API do generowania, przesyłania strumieniowego, przechowywania i buforowania mowy za pomocą Supabase Edge Functions, Supabase Storage i ElevenLabs.

Wymagania

Konfiguracja

Utwórz lokalny projekt Supabase

Po zainstalowaniu Supabase CLI uruchom poniższe polecenie, aby lokalnie utworzyć nowy projekt Supabase:

supabase init

Skonfiguruj bucket magazynu

Możesz skonfigurować Supabase CLI tak, aby automatycznie utworzyło bucket magazynu. Dodaj tę konfigurację do pliku config.toml:

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

Po uruchomieniu supabase start powstanie nowy bucket magazynu w lokalnym projekcie Supabase. Jeśli chcesz przesłać go do hostowanego projektu Supabase, uruchom supabase seed buckets --linked.

Skonfiguruj zadania w tle dla Supabase Edge Functions

Aby używać zadań w tle w Supabase Edge Functions podczas lokalnego tworzenia, dodaj tę konfigurację do pliku config.toml:

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

Przy zasadzie per_worker funkcja nie będzie automatycznie przeładowywana po zmianach. Musisz ręcznie uruchomić ją ponownie za pomocą supabase functions serve.

Utwórz Supabase Edge Function do generowania mowy

Utwórz nową Edge Function, uruchamiając poniższe polecenie:

supabase functions new text-to-speech

Jeśli używasz VS Code lub Cursor, wybierz y, gdy CLI wyświetli pytanie „Generate VS Code settings for Deno? [y/N]”!

Ustaw zmienne środowiskowe

W katalogu supabase/functions utwórz nowy plik .env i dodaj poniższe zmienne:

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

Zależności

Projekt używa kilku zależności:

  • Biblioteki @supabase/supabase-js do interakcji z bazą danych Supabase.
  • SDK JavaScript ElevenLabs do interakcji z API zamiany tekstu na mowę.
  • Otwartoźródłowej biblioteki object-hash do generowania hasha z parametrów żądania.

Ponieważ Supabase Edge Function używa środowiska Deno, nie musisz instalować zależności. Możesz je importować przez prefiks npm:.

Napisz kod Supabase Edge Function

W nowo utworzonym pliku supabase/functions/text-to-speech/index.ts dodaj poniższy kod:

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

Omówienie kodu

Warto zwrócić uwagę na kilka elementów kodu. Przejdźmy przez nie krok po kroku.

1

Obsłuż przychodzące żądanie

Do obsługi przychodzącego żądania użyj procedury Deno.serve. W tym demo nie sprawdzamy pochodzenia żądania, ale możesz na przykład je zweryfikować albo dołączyć token dostępu użytkownika i sprawdzić go w Supabase Auth.

Z przychodzącego żądania funkcja pobiera parametry text i voiceId. Parametr voiceId jest opcjonalny, a domyślnie ustawiany na identyfikator ElevenLabs głosu „Allison”.

Za pomocą biblioteki object-hash funkcja generuje hash z parametrów żądania. Ten hash służy do sprawdzania istniejących plików audio w 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

Sprawdź istniejący plik audio w Supabase Storage

Supabase Storage ma wbudowaną inteligentną sieć CDN, która pozwala łatwo buforować i udostępniać pliki.

Tutaj funkcja sprawdza, czy plik audio istnieje w Supabase Storage. Jeśli tak, zwraca go z 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

Wygeneruj mowę jako strumień i podziel go na dwie gałęzie

Korzystając z przesyłania strumieniowego API ElevenLabs, funkcja generuje strumień. Dzięki temu nawet w przypadku dłuższego tekstu możesz od razu zacząć przesyłać audio do użytkownika, a następnie w tle przesłać strumień do Supabase Storage.

Zapewnia to jak najlepsze wrażenia użytkownika — nawet duże bloki tekstu wydają się błyskawiczne. Kluczowy moment jest w wierszu 17, gdzie metoda stream.tee() dzieli odczytywalny strumień na dwie gałęzie: jedną dla przeglądarki i drugą dla 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

Prześlij strumień audio do Supabase Storage w tle

Metoda EdgeRuntime.waitUntil w wierszu 20 poprzedniego kroku służy do przesłania strumienia audio do Supabase Storage w tle za pomocą funkcji uploadAudioToStorage. Dzięki temu funkcja może od razu zwrócić odpowiedź strumieniową do przeglądarki, podczas gdy audio jest przesyłane do Supabase Storage.

Po utworzeniu obiektu w magazynie, gdy użytkownicy następnym razem wyślą żądanie z tymi samymi parametrami, funkcja zwróci plik audio z CDN 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 });
}

Uruchom lokalnie

Aby uruchomić funkcję lokalnie, wykonaj poniższe polecenia:

supabase start

Gdy lokalny stos Supabase będzie działać, uruchom poniższe polecenie, aby wystartować funkcję i obserwować logi:

supabase functions serve

Wypróbuj

Wejdź na http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world, aby usłyszeć działanie funkcji.

Następnie wejdź na http://127.0.0.1:54323/project/default/storage/buckets/audio, aby zobaczyć plik audio w lokalnym buckecie Supabase Storage.

Wdróż w Supabase

Jeśli jeszcze tego nie zrobiono, utwórz nowe konto Supabase na database.new i połącz lokalny projekt z kontem Supabase:

supabase link

Następnie uruchom poniższe polecenie, aby wdrożyć funkcję:

supabase functions deploy

Ustaw sekrety funkcji

Gdy wszystkie sekrety są już ustawione lokalnie, uruchom poniższe polecenie, aby ustawić je w projekcie Supabase:

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

Przetestuj funkcję

Funkcja została zaprojektowana tak, by można było użyć jej bezpośrednio jako źródła elementu <audio>.

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

Kolejne kroki