使用 ElevenLabs 实现语音流式传输
通过 Supabase 边缘函数生成并流式传输语音。将语音存储在 Supabase 存储中,并通过内置 CDN 缓存响应。
简介
在本教程中,您将学习如何使用 Supabase 边缘函数、Supabase 存储和 ElevenLabs 文本转语音 API 构建一个边缘 API,用于生成、流式传输、存储和缓存语音。
前提条件
- 拥有 API 密钥 的 ElevenLabs 账户
- Supabase 账户(可通过 database.new 注册免费账户)
- 本地已安装 Supabase CLI
- 本地已安装 Deno 运行时,可选在您喜欢的 IDE 中设置
设置
本地创建 Supabase 项目
安装 Supabase CLI 后,运行以下命令在本地创建新的 Supabase 项目:
1supabase init
配置存储桶
您可以通过在 config.toml
文件中添加以下配置,让 Supabase CLI 自动生成存储桶:
12345[storage.buckets.audio]public = falsefile_size_limit = "50MiB"allowed_mime_types = ["audio/mp3"]objects_path = "./audio"
运行 supabase start
后,这将在您的本地 Supabase 项目中创建一个新的存储桶。如需将其推送到托管的 Supabase 项目,可以运行 supabase seed buckets --linked
。
配置 Supabase 边缘函数的后台任务
要在本地开发时使用 Supabase 边缘函数的后台任务功能,您需要在 config.toml
文件中添加以下配置:
12[edge_runtime]policy = "per_worker"
使用 per_worker
策略运行时,函数不会在编辑后自动重新加载。您需要通过运行 supabase functions serve
手动重启它。
创建语音生成的 Supabase 边缘函数
通过运行以下命令创建一个新的边缘函数:
1supabase functions new text-to-speech
如果您使用 VS Code 或 Cursor,当 CLI 提示"为 Deno 生成 VS Code 设置?[y/N]"时选择 y
!
设置环境变量
在 supabase/functions
目录下,创建一个新的 .env
文件并添加以下变量:
12# 在 https://elevenlabs.io/app/settings/api-keys 查找/创建 API 密钥ELEVENLABS_API_KEY=your_api_key
依赖项
本项目使用以下依赖项:
- @supabase/supabase-js 库用于与 Supabase 数据库交互
- ElevenLabs 的 JavaScript SDK 用于与文本转语音 API 交互
- 开源库 object-hash 用于根据请求参数生成哈希值
由于 Supabase 边缘函数使用 Deno 运行时,您无需安装这些依赖项,而是可以通过 npm:
前缀导入它们。
编写 Supabase 边缘函数代码
在新建的 supabase/functions/text-to-speech/index.ts
文件中,添加以下代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091// 为内置的 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' }, }) }})
本地运行
要在本地运行该函数,请执行以下命令:
1supabase start
当本地 Supabase 堆栈启动并运行后,执行以下命令来启动函数并观察日志:
1supabase 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 账户:
1supabase link
完成后,执行以下命令部署函数:
1supabase functions deploy
设置函数密钥
现在您已在本地设置好所有密钥,可以运行以下命令在 Supabase 项目中设置这些密钥:
1supabase secrets set --env-file supabase/functions/.env
测试函数
该函数设计为可直接用作 <audio>
元素的音源。
1234<audio src="https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/text-to-speech?text=Hello%2C%20world!&voiceId=JBFqnCBsd6RMkjVDRZzb" controls/>
您可以在 GitHub 上的完整代码示例中找到前端实现示例。