Supabaseでのストリーミングとキャッシュ

Supabase Edge Functionsを介して音声を生成・ストリーミングします。音声をSupabase Storageに保存し、組み込みCDNでレスポンスをキャッシュします。

ハウツーガイド · ElevenAPI クイックスタートを完了し、Supabaseアカウントを持っていることを前提としています。

はじめに

このガイドでは、Supabase Edge Functions、Supabase Storage、ElevenLabsを使用して、音声を生成、ストリーミング、保存、キャッシュするエッジAPIを構築する方法を学びます。

必要なもの

セットアップ

ローカルでSupabaseプロジェクトを作成する

Supabase CLIをインストールしたら、次のコマンドを実行してローカルに新しいSupabaseプロジェクトを作成します。

supabase init

ストレージバケットを設定する

config.tomlファイルに次の設定を追加すると、Supabase CLIでストレージバケットを自動生成するよう設定できます。

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

supabase startを実行すると、ローカルのSupabase プロジェクトに新しいストレージバケットが作成されます。これをホストされているSupabaseプロジェクトにプッシュするには、supabase seed buckets --linkedを実行します。

Supabase Edge Functionsのバックグラウンドタスクを設定する

ローカル開発でSupabase Edge Functionsのバックグラウンドタスクを使用するには、config.tomlファイルに次の設定を追加する必要があります。

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

per_workerポリシーで実行する場合、Functionは編集時に自動リロードされません。supabase functions serveを実行して、 手動で再起動する必要があります。

音声生成用のSupabase Edge Functionを作成する

次のコマンドを実行して、新しいEdge Functionを作成します。

supabase functions new text-to-speech

VS CodeまたはCursorを使用している場合は、CLIで「Generate VS Code settings for Deno? [y/N]」と表示されたらyを選択してください。

環境変数を設定する

supabase/functionsディレクトリ内に新しい.envファイルを作成し、次の変数を追加します。

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

依存関係

このプロジェクトでは、いくつかの依存関係を使用します。

  • Supabaseデータベースを操作するための@supabase/supabase-jsライブラリ。
  • テキスト読み上げAPIを操作するためのElevenLabs JavaScript SDK。
  • リクエストパラメータからハッシュを生成するためのオープンソースのobject-hash。

Supabase Edge FunctionはDenoランタイムを使用するため、依存関係をインストールする必要はありません。代わりに、npm:プレフィックスを使用してインポートできます。

Supabase Edge Functionを実装する

新しく作成した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" },
});
}
});

コードの詳細

コードには注目すべき点がいくつかあります。順を追って見ていきましょう。

1

受信リクエストを処理する

受信リクエストを処理するには、Deno.serveハンドラーを使用します。このデモではリクエストのオリジンを検証していませんが、たとえばリクエストのオリジンを検証したり、ユーザーアクセストークンを追加してSupabase Authで検証したりできます。

関数は受信リクエストからtextとvoiceIdパラメータを抽出します。voiceIdパラメータは任意で、デフォルトでは「Allison」音声のElevenLabs IDが使用されます。

object-hashライブラリを使用して、関数はリクエストパラメータからハッシュを生成します。このハッシュは、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

Supabase Storage内の既存オーディオファイルを確認する

Supabase StorageにはスマートCDNが組み込まれており、ファイルを簡単にキャッシュして配信できます。

ここでは、関数がSupabase Storage内の既存オーディオファイルを確認します。ファイルが存在する場合、関数は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

音声をストリームとして生成し、2つのブランチに分割する

ElevenLabs APIのストリーミング機能を使用して、関数はストリームを生成します。これにより、大きなテキストでもすぐにユーザーへの音声ストリーミングを開始し、バックグラウンドでストリームをSupabase Storageにアップロードできます。

これにより最高のユーザー体験が実現し、大きなテキストブロックでも驚くほど速く感じられます。ポイントは17行目で、stream.tee()メソッドが読み取り可能なストリームを2つのブランチに分けます。1つはブラウザ用、もう1つは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

バックグラウンドでオーディオストリームをSupabase Storageにアップロードする

前のステップの20行目にあるEdgeRuntime.waitUntilメソッドは、uploadAudioToStorage関数を使用して、バックグラウンドでオーディオストリームをSupabase Storageにアップロードします。これにより、オーディオをSupabase Storageにアップロードしている間も、関数はすぐにストリーミングレスポンスをブラウザに返せます。

ストレージオブジェクトが作成されると、次回ユーザーが同じパラメータでリクエストを行った際、関数はSupabase Storage CDNからオーディオファイルを返します。

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

ローカルで実行する

ローカルで関数を実行するには、次のコマンドを実行します。

supabase start

ローカルのSupabaseスタックが起動したら、次のコマンドを実行して関数を起動し、ログを確認します。

supabase functions serve

試してみる

http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20worldにアクセスして、関数の動作を確認します。

その後、http://127.0.0.1:54323/project/default/storage/buckets/audioにアクセスして、ローカルのSupabase Storageバケット内のオーディオファイルを確認します。

Supabaseにデプロイする

まだ作成していない場合は、database.newで新しいSupabaseアカウントを作成し、ローカルプロジェクトをSupabaseアカウントにリンクします。

supabase link

完了したら、次のコマンドを実行して関数をデプロイします。

supabase functions deploy

関数のシークレットを設定する

ローカルですべてのシークレットを設定したら、次のコマンドを実行してSupabaseプロジェクトにシークレットを設定できます。

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

関数をテストする

この関数は、<audio>要素のソースとして直接使用できるよう設計されています。

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

次のステップ