> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

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

> **Note**
>
> **ハウツーガイド** · [ElevenAPI クイックスタート](/docs/ja/eleven-api/quickstart)を完了し、Supabaseアカウントを持っていることを前提としています。

## はじめに

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

## 必要なもの

* [APIキー](https://elevenlabs.io/app/settings/api-keys)を持つElevenLabsアカウント。
* [Supabase](https://supabase.com)アカウント（[database.new](https://database.new)から無料アカウントに登録できます）。
* マシンにインストールされた[Supabase CLI](https://supabase.com/docs/guides/local-development)。
* マシンにインストールされた[Denoランタイム](https://docs.deno.com/runtime/getting_started/installation/)、および任意で[お好みのIDEでのセットアップ](https://docs.deno.com/runtime/getting_started/setup_your_environment)。

## セットアップ

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

[Supabase CLI](https://supabase.com/docs/guides/local-development)をインストールしたら、次のコマンドを実行してローカルに新しいSupabaseプロジェクトを作成します。

```bash
supabase init
```

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

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

**`./supabase/config.toml`**

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

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

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

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

**`./supabase/config.toml`**

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

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

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

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

```bash
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`**

```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](https://supabase.com/docs/reference/javascript)ライブラリ。
* テキスト読み上げAPIを操作するためのElevenLabs [JavaScript SDK](/docs/ja/eleven-api/quickstart)。
* リクエストパラメータからハッシュを生成するためのオープンソースの[object-hash](https://www.npmjs.com/package/object-hash)。

Supabase Edge Functionは[Denoランタイム](https://deno.land/)を使用するため、依存関係をインストールする必要はありません。代わりに、`npm:`プレフィックスを使用して[インポート](https://docs.deno.com/examples/npm/)できます。

## Supabase Edge Functionを実装する

新しく作成した`supabase/functions/text-to-speech/index.ts`ファイルに、次のコードを追加します。

**`supabase/functions/text-to-speech/index.ts`**

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

### コードの詳細

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

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

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

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

`object-hash`ライブラリを使用して、関数はリクエストパラメータからハッシュを生成します。このハッシュは、Supabase Storage内の既存のオーディオファイルを確認するために使用されます。

```ts {1,5-8}
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);

// ...
})
```

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

Supabase Storageには[スマートCDNが組み込まれており](https://supabase.com/docs/guides/storage/cdn/smart-cdn)、ファイルを簡単にキャッシュして配信できます。

ここでは、関数がSupabase Storage内の既存オーディオファイルを確認します。ファイルが存在する場合、関数はSupabase Storageからそのファイルを返します。

```ts {4,9}
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;
}
```

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

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

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

```ts {1,17,20,22-27}
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" },
  });
}
```

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

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

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

```ts {2,8-10}
// 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 });
}
```

## ローカルで実行する

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

```bash
supabase start
```

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

```bash
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](https://database.new)で新しいSupabaseアカウントを作成し、ローカルプロジェクトをSupabaseアカウントにリンクします。

```bash
supabase link
```

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

```bash
supabase functions deploy
```

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

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

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

## 関数をテストする

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

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

## 次のステップ

#### [TTSストリーミング](/docs/ja/eleven-api/guides/how-to/text-to-speech/streaming)

よりシンプルな出発点として、Supabaseを使わずにオーディオを段階的にストリーミングします。

#### [APIリファレンス](/docs/ja/api-reference/text-to-speech/stream)

TTSストリーミングAPIの完全なリファレンスとパラメータ。