Skip to content
Edge Functions

使用 ElevenLabs 流式语音

Generate and stream speech through Supabase Edge Functions. Store speech in Supabase Storage and cache responses via built-in CDN.

介绍 #

🌐 Introduction

在本教程中,你将学习如何使用 Supabase Edge Functions、Supabase Storage 和 ElevenLabs 文字转语音 API 构建一个边缘 API 来生成、流式传输、存储和缓存语音。

🌐 In this tutorial you will learn how to build an edge API to generate, stream, store, and cache speech using Supabase Edge Functions, Supabase Storage, and ElevenLabs text to speech API.

要求 #

🌐 Requirements

设置 #

🌐 Setup

在本地创建一个 Supabase 项目 #

🌐 Create a Supabase project locally

安装 Supabase CLI 后,运行以下命令在本地创建一个新的 Supabase 项目:

🌐 After installing the Supabase CLI, run the following command to create a new Supabase project locally:

1
supabase init

配置存储桶 #

🌐 Configure the storage bucket

你可以通过在 config.toml 文件中添加此配置,让 Supabase CLI 自动生成存储桶:

🌐 You can configure the Supabase CLI to automatically generate a storage bucket by adding this configuration in the config.toml file:

1
[storage.buckets.audio]
2
public = false
3
file_size_limit = "50MiB"
4
allowed_mime_types = ["audio/mp3"]
5
objects_path = "./audio"

为 Supabase Edge 函数配置后台任务 #

🌐 Configure background tasks for Supabase Edge Functions

在本地开发时,如果想在 Supabase Edge Functions 中使用后台任务,你需要在 config.toml 文件中添加以下配置:

🌐 To use background tasks in Supabase Edge Functions when developing locally, you need to add the following configuration in the config.toml file:

1
[edge_runtime]
2
policy = "per_worker"

创建一个用于语音生成的 Supabase Edge 函数 #

🌐 Create a Supabase Edge Function for speech generation

通过运行以下命令来创建一个新的 Edge 函数:

🌐 Create a new Edge Function by running the following command:

1
supabase functions new text-to-speech

如果你在使用 VS Code 或 Cursor,当命令行提示“为 Deno 生成 VS Code 设置?[y/N]”时,选择 y 就行!

🌐 If you're using VS Code or Cursor, select y when the CLI prompts "Generate VS Code settings for Deno? [y/N]"!

设置环境变量 #

🌐 Set up the environment variables

supabase/functions 目录下,创建一个新的 .env 文件,并添加以下变量:

🌐 Within the supabase/functions directory, create a new .env file and add the following variables:

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

依赖 #

🌐 Dependencies

这个项目用了几个依赖:

🌐 The project uses a couple of dependencies:

由于 Supabase Edge Function 使用的是 Deno 运行时,你不需要安装依赖,而是可以通过 npm: 前缀来导入它们。

🌐 Since Supabase Edge Function uses the Deno runtime, you don't need to install the dependencies, rather you can import them via the npm: prefix.

编写 Supabase Edge 函数 #

🌐 Code the Supabase Edge Function

在你新创建的 supabase/functions/text-to-speech/index.ts 文件中,添加以下代码:

🌐 In your newly created supabase/functions/text-to-speech/index.ts file, add the following code:

1
// Setup type definitions for built-in Supabase Runtime APIs
2
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
3
4
import { withSupabase } from 'npm:@supabase/server@^1'
5
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
6
import * as hash from 'npm:object-hash@^3'
7
8
const client = new ElevenLabsClient({
9
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
10
})
11
12
// Deploy with verify_jwt = false
13
// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
14
export default {
15
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
16
// Upload audio to Supabase Storage in a background task
17
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
18
const { data, error } = await ctx.supabaseAdmin.storage
19
.from('audio')
20
.upload(`${requestHash}.mp3`, stream, {
21
contentType: 'audio/mp3',
22
})
23
24
console.log('Storage upload result', { data, error })
25
}
26
27
// To secure your function for production, you can for example validate the request origin,
28
// or append a user access token and validate it with Supabase Auth.
29
console.log('Request origin', req.headers.get('host'))
30
const url = new URL(req.url)
31
const params = new URLSearchParams(url.search)
32
const text = params.get('text')
33
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
34
35
const requestHash = hash.MD5({ text, voiceId })
36
console.log('Request hash', requestHash)
37
38
// Check storage for existing audio file
39
const { data } = await ctx.supabaseAdmin.storage
40
.from('audio')
41
.createSignedUrl(`${requestHash}.mp3`, 60)
42
43
if (data) {
44
console.log('Audio file found in storage', data)
45
const storageRes = await fetch(data.signedUrl)
46
if (storageRes.ok) return storageRes
47
}
48
49
if (!text) {
50
return Response.json({ error: 'Text parameter is required' }, { status: 400 })
51
}
52
53
try {
54
console.log('ElevenLabs API call')
55
const response = await client.textToSpeech.convertAsStream(voiceId, {
56
output_format: 'mp3_44100_128',
57
model_id: 'eleven_multilingual_v2',
58
text,
59
})
60
61
const stream = new ReadableStream({
62
async start(controller) {
63
for await (const chunk of response) {
64
controller.enqueue(chunk)
65
}
66
controller.close()
67
},
68
})
69
70
// Branch stream to Supabase Storage
71
const [browserStream, storageStream] = stream.tee()
72
73
// Upload to Supabase Storage in the background
74
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
75
76
// Return the streaming response immediately
77
return new Response(browserStream, {
78
headers: {
79
'Content-Type': 'audio/mpeg',
80
},
81
})
82
} catch (error) {
83
console.log('error', { error })
84
return Response.json({ error: error.message }, { status: 500 })
85
}
86
}),
87
}

本地运行 #

🌐 Run locally

要在本地运行这个函数,执行以下命令:

🌐 To run the function locally, run the following commands:

1
supabase start

本地 Supabase 堆栈一旦启动并运行,执行以下命令来启动函数并查看日志:

🌐 Once the local Supabase stack is up and running, run the following command to start the function and observe the logs:

1
supabase functions serve

试试看 #

🌐 Try it out

导航到 http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world 来听这个功能的实际效果。

🌐 Navigate to http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world to hear the function in action.

之后,导航到 http://127.0.0.1:54323/project/default/storage/buckets/audio 来查看你本地 Supabase 存储桶中的音频文件。

🌐 Afterwards, navigate to http://127.0.0.1:54323/project/default/storage/buckets/audio to see the audio file in your local Supabase Storage bucket.

部署到 Supabase #

🌐 Deploy to Supabase

如果你还没有的话,可以在 database.new 创建一个新的 Supabase 账户,并将本地项目连接到你的 Supabase 账户:

🌐 If you haven't already, create a new Supabase account at database.new and link the local project to your Supabase account:

1
supabase link

完成后,运行以下命令来部署函数:

🌐 Once done, run the following command to deploy the function:

1
supabase functions deploy

设置函数秘密 #

🌐 Set the function secrets

既然你已经在本地设置好了所有的密钥,你可以运行以下命令将这些密钥设置到你的 Supabase 项目中:

🌐 Now that you have all your secrets set locally, you can run the following command to set the secrets in your Supabase project:

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

测试这个功能 #

🌐 Test the function

这个函数的设计方式使它可以直接作为 <audio> 元素的来源使用。

🌐 The function is designed in a way that it can be used directly as a source for an <audio> element.

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

你可以在 GitHub 上的完整代码示例中找到一个前端实现的例子。

🌐 You can find an example frontend implementation in the complete code example on GitHub.