使用 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.
在 GitHub 上找到这个 示例项目。
🌐 Find the example project on GitHub.
要求 #
🌐 Requirements
- 一个带有API 密钥的 ElevenLabs 账户。
- 一个 Supabase 账户(你可以通过 database.new 注册一个免费账户)。
- 你电脑上安装了 Supabase CLI。
- 你电脑上已安装了 Deno 运行时,并且可以选择在你喜欢的 IDE 中 进行设置。
设置 #
🌐 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:
1supabase 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]2public = false3file_size_limit = "50MiB"4allowed_mime_types = ["audio/mp3"]5objects_path = "./audio"运行 supabase start 后,这将在你的本地 Supabase 项目中创建一个新的存储桶。如果你想把它推送到你托管的 Supabase 项目中,你可以运行 supabase seed buckets --linked。
🌐 Upon running supabase start this will create a new storage bucket in your local Supabase project. Should you want to push this to your hosted Supabase project, you can run supabase seed buckets --linked.
为 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]2policy = "per_worker"在使用 per_worker 策略运行时,函数在修改后不会自动重载。你需要手动运行 supabase functions serve 来重启它。
🌐 When running with per_worker policy, Function won't auto-reload on edits. You will need to manually restart it by running supabase functions serve.
创建一个用于语音生成的 Supabase Edge 函数 #
🌐 Create a Supabase Edge Function for speech generation
通过运行以下命令来创建一个新的 Edge 函数:
🌐 Create a new Edge Function by running the following command:
1supabase 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-keys2ELEVENLABS_API_KEY=your_api_key依赖 #
🌐 Dependencies
这个项目用了几个依赖:
🌐 The project uses a couple of dependencies:
- @supabase/supabase-js 库用于与 Supabase 数据库互动。
- ElevenLabs 的 JavaScript SDK 用于与文字转语音 API 交互。
- 开源的 object-hash 用来从请求参数生成哈希。
由于 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 APIs2import 'jsr:@supabase/functions-js/edge-runtime.d.ts'34import { withSupabase } from 'npm:@supabase/server@^1'5import { ElevenLabsClient } from 'npm:elevenlabs@^1'6import * as hash from 'npm:object-hash@^3'78const client = new ElevenLabsClient({9 apiKey: Deno.env.get('ELEVENLABS_API_KEY'),10})1112// Deploy with verify_jwt = false13// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.14export default {15 fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {16 // Upload audio to Supabase Storage in a background task17 async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {18 const { data, error } = await ctx.supabaseAdmin.storage19 .from('audio')20 .upload(`${requestHash}.mp3`, stream, {21 contentType: 'audio/mp3',22 })2324 console.log('Storage upload result', { data, error })25 }2627 // 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'3435 const requestHash = hash.MD5({ text, voiceId })36 console.log('Request hash', requestHash)3738 // Check storage for existing audio file39 const { data } = await ctx.supabaseAdmin.storage40 .from('audio')41 .createSignedUrl(`${requestHash}.mp3`, 60)4243 if (data) {44 console.log('Audio file found in storage', data)45 const storageRes = await fetch(data.signedUrl)46 if (storageRes.ok) return storageRes47 }4849 if (!text) {50 return Response.json({ error: 'Text parameter is required' }, { status: 400 })51 }5253 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 })6061 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 })6970 // Branch stream to Supabase Storage71 const [browserStream, storageStream] = stream.tee()7273 // Upload to Supabase Storage in the background74 EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))7576 // Return the streaming response immediately77 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:
1supabase start本地 Supabase 堆栈一旦启动并运行,执行以下命令来启动函数并查看日志:
🌐 Once the local Supabase stack is up and running, run the following command to start the function and observe the logs:
1supabase 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:
1supabase link完成后,运行以下命令来部署函数:
🌐 Once done, run the following command to deploy the function:
1supabase 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:
1supabase 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<audio2 src="https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/text-to-speech?text=Hello%2C%20world!&voiceId=JBFqnCBsd6RMkjVDRZzb"3 controls4/>你可以在 GitHub 上的完整代码示例中找到一个前端实现的例子。
🌐 You can find an example frontend implementation in the complete code example on GitHub.