Streaming e cache com Supabase

Gere e transmita voz por meio de Supabase Edge Functions. Armazene a voz no Supabase Storage e armazene respostas em cache pela CDN integrada.

Guia prático · Pressupõe que você tenha concluído o guia de início rápido da ElevenAPI e tenha uma conta no Supabase.

Introdução

Neste guia, você vai aprender a criar uma API de borda para gerar, transmitir, armazenar e armazenar em cache áudio de fala usando Supabase Edge Functions, Supabase Storage e ElevenLabs.

Requisitos

Configuração

Criar um projeto Supabase localmente

Depois de instalar a CLI do Supabase, execute o comando a seguir para criar um novo projeto Supabase localmente:

supabase init

Configurar o bucket de armazenamento

Você pode configurar a CLI do Supabase para gerar automaticamente um bucket de armazenamento adicionando esta configuração ao arquivo config.toml:

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

Ao executar supabase start, um novo bucket de armazenamento será criado no seu projeto Supabase local. Se quiser enviar isso para seu projeto Supabase hospedado, execute supabase seed buckets --linked.

Configurar tarefas em segundo plano para Supabase Edge Functions

Para usar tarefas em segundo plano nas Supabase Edge Functions durante o desenvolvimento local, você precisa adicionar a seguinte configuração ao arquivo config.toml:

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

Ao executar com a política per_worker, a função não será recarregada automaticamente após edições. Você precisará reiniciá-la manualmente executando supabase functions serve.

Criar uma Supabase Edge Function para geração de fala

Crie uma nova Edge Function executando o seguinte comando:

supabase functions new text-to-speech

Se você usa VS Code ou Cursor, selecione y quando a CLI perguntar “Generate VS Code settings for Deno? [y/N]”!

Configurar as variáveis de ambiente

No diretório supabase/functions, crie um novo arquivo .env e adicione as seguintes variáveis:

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

Dependências

O projeto usa algumas dependências:

  • A biblioteca @supabase/supabase-js para interagir com o banco de dados do Supabase.
  • O SDK JavaScript da ElevenLabs para interagir com a API de Text to Speech.
  • O object-hash de código aberto para gerar um hash a partir dos parâmetros da solicitação.

Como a Supabase Edge Function usa o runtime Deno, você não precisa instalar as dependências; basta importá-las pelo prefixo npm:.

Programar a Supabase Edge Function

No arquivo supabase/functions/text-to-speech/index.ts que você acabou de criar, adicione o código a seguir:

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álise detalhada do código

Há alguns pontos importantes sobre o código. Vamos analisá-lo passo a passo.

1

Processar a solicitação recebida

Para processar a solicitação recebida, use o handler Deno.serve. Na demonstração, não validamos a origem da solicitação, mas você pode, por exemplo, validar a origem da solicitação ou adicionar um token de acesso do usuário e validá-lo com o Supabase Auth.

A partir da solicitação recebida, a função extrai os parâmetros text e voiceId. O parâmetro voiceId é opcional e, por padrão, usa o ID da ElevenLabs para a voz “Allison”.

Usando a biblioteca object-hash, a função gera um hash a partir dos parâmetros da solicitação. Esse hash é usado para verificar se há arquivos de áudio existentes no 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

Verificar se há um arquivo de áudio existente no Supabase Storage

O Supabase Storage inclui uma CDN inteligente integrada, permitindo armazenar em cache e disponibilizar seus arquivos facilmente.

Aqui, a função verifica se há um arquivo de áudio existente no Supabase Storage. Se o arquivo existir, a função o retorna do 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

Gerar fala como stream e dividi-la em duas ramificações

Usando os recursos de streaming da API da ElevenLabs, a função gera um stream. A vantagem é que, mesmo para textos maiores, você pode começar a transmitir o áudio de volta ao usuário imediatamente e enviar o stream para o Supabase Storage em segundo plano.

Isso proporciona a melhor experiência possível ao usuário, fazendo com que até grandes blocos de texto pareçam surpreendentemente rápidos. A mágica acontece na linha 17, em que o método stream.tee() divide o stream legível em duas ramificações: uma para o navegador e outra para o 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

Enviar o stream de áudio para o Supabase Storage em segundo plano

O método EdgeRuntime.waitUntil na linha 20 da etapa anterior é usado para enviar o stream de áudio ao Supabase Storage em segundo plano usando a função uploadAudioToStorage. Isso permite que a função retorne imediatamente a resposta em streaming ao navegador enquanto o áudio é enviado ao Supabase Storage.

Depois que o objeto de armazenamento for criado, na próxima vez que seus usuários fizerem uma solicitação com os mesmos parâmetros, a função retornará o arquivo de áudio da CDN do 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 });
}

Executar localmente

Para executar a função localmente, execute os comandos a seguir:

supabase start

Quando a stack local do Supabase estiver em execução, execute o comando a seguir para iniciar a função e acompanhar os logs:

supabase functions serve

Experimente

Acesse http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world para ouvir a função em ação.

Em seguida, acesse http://127.0.0.1:54323/project/default/storage/buckets/audio para ver o arquivo de áudio no seu bucket local do Supabase Storage.

Implantar no Supabase

Se ainda não fez isso, crie uma nova conta no Supabase em database.new e vincule o projeto local à sua conta do Supabase:

supabase link

Depois, execute o comando a seguir para implantar a função:

supabase functions deploy

Definir os segredos da função

Agora que todos os seus segredos estão configurados localmente, você pode executar o comando a seguir para defini-los no seu projeto Supabase:

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

Testar a função

A função foi projetada para poder ser usada diretamente como fonte de um elemento <audio>.

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

Próximas etapas