前言:一个中年程序员的“省钱”执念
大家好,我是老杨。一个在代码世界里摸爬滚打了十几年的中年程序员。人到中年,技术热情不减,但对“性价比”这三个字的追求却愈发执着。最近在捣鼓一些需要语音播报的小项目,调研了一圈,发现大厂的TTS(文本转语音)服务,效果是真不错,但价格嘛……听着那合成出来的优美女声,再看看自己账户余额减少的速度,心里直滴血。
OpenAI的TTS API调用一次几美分,听着不多,但架不住积少成多啊!难道就没有既能享受高质量语音合成,又不用“按字付费”的好事吗?
本着“能自己动手,绝不花冤枉钱”的极客精神,我开始了一番探索。功夫不负有心人,还真让我找到了一个“白嫖”微软高质量语音接口的方法,并把它封装成了一个可以部署在 Cloudflare 上的服务。
今天,老杨就把这个压箱底的宝贝分享出来。你只需要一个 Cloudflare 账号,复制粘贴一些代码,就能拥有一个属于自己的、免费的、高性能的、带缓存的、兼容 OpenAI API 格式的 TTS 服务!
最终效果:
- 零成本部署:完全基于 Cloudflare 的免费套餐。
- 海量高品质人声:集成了微软 Azure TTS 的数十种神经网络语音,中英日韩,男女老少,各种情感风格,应有尽有。
- 智能缓存:相同文本只在第一次生成时消耗资源,后续请求直接从你自己的R2存储中毫秒级响应,又快又省。
- API兼容:提供了与 OpenAI TTS 几乎一致的接口,你现有项目几乎可以无缝迁移。
- 自带Web界面:提供一个简单直观的前端页面,方便你在线调试、生成和下载音频。
技术架构揭秘:魔法是如何实现的?
这个方案的核心是两大免费神器:Cloudflare Workers 和 Cloudflare R2。
- Cloudflare Workers:一个强大的 Serverless 计算平台。我们可以将我们的JS代码部署在上面,它会在全球的Cloudflare边缘节点运行,响应速度极快。免费版每天有10万次请求额度,对于个人项目或中小型应用来说,绰绰有余。
- Cloudflare R2:一个S3兼容的对象存储服务。它最香的一点是“无出口流量费用”,也就是说,别人从你的R2存储桶里下载东西,你不花一分钱。我们用它来缓存生成的TTS音频文件。
核心逻辑:
我们的 Worker 脚本,其实是扮演了一个“中间人”的角色。当它收到一个TTS请求时:
- 获取临时令牌:脚本会模拟一个客户端,去访问微软的一个公开的翻译服务终结点(Endpoint),并“借”来一个有时效性的访问令牌(Token)。这个令牌,恰好也能用于微软的语音合成服务。这是整个方案的“神来之笔”。
- 生成缓存键:脚本会根据你请求的文本、语音、语速、音调等所有参数,生成一个唯一的MD5哈希值,作为这个音频文件的“身份证”(例如
a1b2c3d4.mp3
)。 - 查询R2缓存:用这个“身份证”去我们的R2存储桶里查找。
- 如果找到了(缓存命中),说明这个文本之前已经生成过了。Worker会直接返回这个文件在R2上的公开访问URL,整个过程快如闪电。
- 如果没找到(缓存未命中),Worker 就会拿着第一步获取的令牌,去请求微软的TTS服务,将文本转换成音频流。
- 存入缓存并返回:Worker将新生成的音频流存入R2存储桶(用刚才的“身份证”作为文件名),然后同样返回该文件的公开URL。
通过这个流程,我们巧妙地利用了Cloudflare的边缘计算和免费存储,实现了一个高效、经济的TTS代理服务。
核心代码解读
为了让大家知其所以然,老杨带大家过一遍核心代码的逻辑。(完整代码太长,只摘取关键部分,想直接上手的朋友可以跳到下一节或直接下载文末的源码)
1. 路由与请求处理 handleRequest
这是整个Worker的入口,它判断进来的请求是想访问我们的Web测试页面,还是要调用TTS API,然后分发给不同的处理函数。
// workers.js
export default {
async fetch(request, env) {
return handleRequest(request, env);
}
}
async function handleRequest(request, env) {
// ...省略CORS和OPTIONS处理...
const path = new URL(request.url).pathname;
if (path === "/") {
// 返回Web测试页面
return new Response(getHtmlPage(env.API_KEY ? true : false), { /*...*/ });
}
if (path === "/v1/audio/speech") {
// 处理POST API请求
// ...
}
if (path === "/v1/audio/speech/get") {
// 处理GET API请求
// ...
}
return new Response("Not Found", { status: 404 });
}
2. 核心逻辑 getAudioFile
这个函数完美诠释了我们上面提到的“缓存优先”策略。
// workers.js
async function getAudioFile(params, env) {
// 1. 根据所有参数计算MD5哈希值作为缓存键
const hash = await md5(JSON.stringify(cacheKeyParams));
const filename = `${hash}.mp3`;
const publicUrl = `${env.R2_PUBLIC_URL}/${filename}`;
// 2. 检查R2中是否存在该文件
const existingFile = await env.TTS_AUDIO_CACHE.head(filename);
if (existingFile) {
console.log(`缓存命中: ${filename}`);
return { url: publicUrl, fromCache: true }; // 命中,直接返回URL
}
console.log(`缓存未命中: ${filename}. 开始生成新音频.`);
// 3. 未命中,调用微软接口生成音频Blob
const audioBlob = await generateAudioBlob(/*...*/);
// 4. 将新音频存入R2
await env.TTS_AUDIO_CACHE.put(filename, audioBlob, { /*...*/ });
// 5. 返回新文件的URL
return { url: publicUrl, fromCache: false };
}
3. 语音合成 generateAudioBlob
& getSsml
这里我们使用SSML(语音合成标记语言)来构造请求体,这样可以更精细地控制语音的各种属性,比如情感、角色扮演等。
// workers.js
function getSsml(text, voiceName, rate, pitch, style, role) {
// 通过mstts:express-as标签来指定风格和角色
const expressAsTag = style && style !== 'general' ?
`<mstts:express-as style="${style}" role="${role}">` : '';
const expressAsCloseTag = style && style !== 'general' ?
`</mstts:express-as>` : '';
return `<speak version="1.0" xml:lang="zh-CN" xmlns="http://www.w3.org/2001/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts">
<voice name="${voiceName}">
${expressAsTag}
<prosody rate="${rate}%" pitch="${pitch}%">${text}</prosody>
${expressAsCloseTag}
</voice>
</speak>`;
}
“保姆级”部署指南
理论说完了,上实操!跟着老杨,三步搞定。
第一步:准备工作
你只需要一个注册好的 Cloudflare 账号。没有的话,去官网免费注册一个。
第二步:创建 R2 存储桶和 Worker
-
创建 R2 存储桶:
- 登录 Cloudflare 控制台,左侧菜单找到
R2
。 - 点击
创建存储桶
,随便起个名字,比如my-tts-cache
。 - 创建后,进入存储桶的
设置
页面,找到R2.dev 公开访问
这一项,点击允许访问
。然后复制下面显示的公开URL,形如https://pub-xxxxxxxx.r2.dev
。这个URL我们后面要用。
- 登录 Cloudflare 控制台,左侧菜单找到
-
创建 Worker:
- 左侧菜单找到
Workers 和 Pages
。 - 点击
创建应用程序
->创建 Worker
。 - 给你的 Worker 也起个名字,比如
my-tts-service
,然后点击部署
。
- 左侧菜单找到
第三步:配置并部署代码
-
粘贴代码:
- 进入你刚创建的 Worker,点击
编辑代码
。 - 删除编辑器里原有的示例代码,将我提供的
workers.js
完整代码粘贴进去。
- 进入你刚创建的 Worker,点击
-
关联 R2 存储桶:
- 回到 Worker 的管理页面,点击
设置
->变量
。 - 在
R2 存储桶绑定
部分,点击添加绑定
。 变量名称
必须填写TTS_AUDIO_CACHE
。R2 存储桶
选择你第一步创建的那个my-tts-cache
。- 保存。
- 回到 Worker 的管理页面,点击
-
设置环境变量:
- 还是在
设置
->变量
页面,找到环境变量
部分,点击添加变量
。 变量名称
填写R2_PUBLIC_URL
。值
就粘贴你之前复制的 R2 存储桶的公开URL。- (可选)如果你想给你的API加一把锁,可以再添加一个变量,名称为
API_KEY
,值是你自己设定的密码(比如一个复杂的字符串)。 - 保存并部署!
- 还是在
至此,大功告成!你可以访问你的 Worker 地址 https://my-tts-service.your-name.workers.dev
,就能看到那个简洁的Web界面了。
如何使用你的专属API
你可以通过 POST
或 GET
方式调用API。
POST 请求 (推荐)
这种方式功能最全,适合在程序中调用。
curl -X POST https://your-worker.workers.dev/v1/audio/speech \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "你好,我是老杨,这是我为你搭建的免费TTS服务!",
"voice": "zh-CN-YunxiNeural",
"speed": 1.0,
"pitch": 1.0,
"style": "cheerful"
}'
返回结果
{
"url": "https://pub-xxx.r2.dev/xxxxxxxx.mp3",
"cached": false
}
GET 请求
这种方式适合在浏览器中快速测试,或者生成临时的下载链接。
https://your-worker.workers.dev/v1/audio/speech/get?text=Hello&voice=en-US-JennyNeural&apiKey=YOUR_API_KEY
直接在浏览器中打开这个链接,就可以播放或下载音频了。
适用人群与场景:这套方案能帮你做什么?
你可能会问,老杨,你这套方案听起来很酷,但它到底能用在什么地方呢?问得好!这可不是个只能躺在收藏夹里吃灰的“玩具”,它的应用场景相当广泛:
-
个人博客或独立开发者:
- 文章转语音:给你的博客文章配上“一键收听”功能,让读者在通勤、做家务时也能“听”你的文章,提升用户体验和粘性。
- 视频内容创作者:用它来为你的教学视频、产品演示、Vlog等内容制作旁白,省去自己配音的麻烦,还能选择不同风格的声音。
- 有声书制作:如果你在写小说或者想把一些公版书籍制作成有声书,这个工具能帮你快速生成音频原型。
-
中小型企业或初创团队:
- 智能客服:为你的IVR(交互式语音应答)系统或聊天机器人提供语音播报能力,例如播报验证码、订单状态、服务通知等。
- 内部工具:公司内部的通知系统、监控告警系统,都可以用它来把文字通知变成语音提醒,更加醒目。
- 产品原型验证:在产品开发初期,快速为你的App或小程序加上语音交互功能,用最小的成本验证市场反响。
-
编程学习与爱好者:
- 学习API调用:这是一个绝佳的实践项目,让你了解如何与第三方API交互、如何处理流数据、如何进行缓存设计。
- DIY智能家居:结合树莓派或ESP32等硬件,打造属于你自己的智能音箱、语音助手,让你的家变得更“听话”。
基本上,任何需要将文本转化为自然流畅语音的场景,这套方案都能派上用场。最关键的是,在项目的初期和中期,它能帮你把成本控制在“无限接近于零”的水平。
一点心里话和“免责声明”
老杨分享这个方案,初衷是帮助像我一样热爱技术、但预算有限的朋友们,用更低的成本去实现自己的创意。这个方法的核心是巧妙地利用了微软提供给其自家应用的公开接口。
因此,有几点要坦诚相告:
- 接口的稳定性:这个非官方的接口未来可能会有变化(比如调整认证方式)。虽然从目前来看它已经稳定运行了很长时间,但我们仍需有备无患。好消息是,即便接口变化,我们的缓存逻辑和整体架构依然有效,只需调整获取令牌的部分即可。
- 合理使用:请大家将此方案用于正当的学习、研究和创新项目上。不要滥用,不要用于任何非法用途,更不要对源服务造成过大的压力。我们要做一个有素质的“白嫖党”。
- 付费的价值:当你的项目发展壮大,流量和并发量上来之后,老杨还是建议你切换到官方付费的TTS服务。付费不仅能换来更稳定的服务和官方技术支持,也是对知识产权和技术服务的尊重。
我们现在做的,是在“从0到1”的阶段,用智慧和技巧,为自己的梦想铺路。
结语:动手吧,我的朋友!
技术的魅力,就在于不断探索和创造。用一行行代码,将不可能变为可能,这正是我们程序员的浪漫。
今天分享的这个免费TTS服务搭建方案,只是众多奇技淫巧中的一个。它不完美,但足够实用,希望能为你打开一扇新的大门,激发你更多的灵感。
完整的、经过老杨反复调试和优化的 workers.js
脚本,我已经打包好并上传到了CSDN资源。 你不需要再费心去研究代码细节,只需按照教程步骤,复制、粘贴、配置,三分钟就能拥有自己的高质量TTS服务。
【资源下载链接:workers.js】
最后,如果你觉得这篇文章对你有帮助,请不要吝啬你的“一键三连”:点赞、评论、加关注! 在评论区分享你的想法,或者告诉我你还想看到哪些有趣的技术实践,老杨都会认真看的!
好了,我是老杨,一个热爱生活、热爱代码的中年男人。我们下篇文章再见!