边缘函数

使用 ElevenLabs 实现语音流式传输

通过 Supabase 边缘函数生成并流式传输语音。将语音存储在 Supabase 存储中,并通过内置 CDN 缓存响应。


简介

在本教程中,您将学习如何使用 Supabase 边缘函数、Supabase 存储和 ElevenLabs 文本转语音 API 构建一个边缘 API,用于生成、流式传输、存储和缓存语音。

前提条件

设置

本地创建 Supabase 项目

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

1
supabase init

配置存储桶

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

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

配置 Supabase 边缘函数的后台任务

要在本地开发时使用 Supabase 边缘函数的后台任务功能,您需要在 config.toml 文件中添加以下配置:

1
2
[edge_runtime]policy = "per_worker"

创建语音生成的 Supabase 边缘函数

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

1
supabase functions new text-to-speech

如果您使用 VS Code 或 Cursor,当 CLI 提示"为 Deno 生成 VS Code 设置?[y/N]"时选择 y

设置环境变量

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

1
2
# 在 https://elevenlabs.io/app/settings/api-keys 查找/创建 API 密钥ELEVENLABS_API_KEY=your_api_key

依赖项

本项目使用以下依赖项:

由于 Supabase 边缘函数使用 Deno 运行时,您无需安装这些依赖项,而是可以通过 npm: 前缀导入它们。

编写 Supabase 边缘函数代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 为内置的 Supabase Runtime API 设置类型定义import 'jsr:@supabase/functions-js/edge-runtime.d.ts'import { createClient } from 'jsr:@supabase/supabase-js@2'import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'import * as hash from 'npm:object-hash'const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)const client = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY'),})// 在后台任务中将音频上传到 Supabase 存储async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) { const { data, error } = await supabase.storage .from('audio') .upload(`${requestHash}.mp3`, stream, { contentType: 'audio/mp3', }) console.log('存储上传结果', { data, error })}Deno.serve(async (req) => { // 为了在生产环境中保护您的函数,可以验证请求来源, // 或附加用户访问令牌并通过 Supabase Auth 进行验证 console.log('请求来源', 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('请求哈希', requestHash) // 检查存储中是否已存在音频文件 const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60) if (data) { console.log('在存储中找到音频文件', data) const storageRes = await fetch(data.signedUrl) if (storageRes.ok) return storageRes } if (!text) { return new Response(JSON.stringify({ error: '必须提供 text 参数' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } try { console.log('调用 ElevenLabs API') const response = await client.textToSpeech.convertAsStream(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() }, }) // 将流分支到 Supabase 存储 const [browserStream, storageStream] = stream.tee() // 在后台上传到 Supabase 存储 EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash)) // 立即返回流式响应 return new Response(browserStream, { headers: { 'Content-Type': 'audio/mpeg', }, }) } catch (error) { console.log('错误', { error }) return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' }, }) }})

本地运行

要在本地运行该函数,请执行以下命令:

1
supabase start

当本地 Supabase 堆栈启动并运行后,执行以下命令来启动函数并观察日志:

1
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 存储桶中的音频文件。

部署到 Supabase

如果尚未创建,请先在 database.new 创建新的 Supabase 账户,并将本地项目链接到您的 Supabase 账户:

1
supabase link

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

1
supabase functions deploy

设置函数密钥

现在您已在本地设置好所有密钥,可以运行以下命令在 Supabase 项目中设置这些密钥:

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

测试函数

该函数设计为可直接用作 <audio> 元素的音源。

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

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