简介:一份基于百度API开发的PHP在线文字转语音合成源码,适合需要快速为网站或应用接入TTS功能的PHP开发者。通过调用百度语音接口,输入任意文本即可生成自然语音,支持在线播放与下载,省去自建语音引擎的繁琐流程。压缩包共201个文件,主体为170个JavaScript脚本,负责前端交互、接口请求与播放控制;13个CSS文件完成界面布局,另包含核心PHP文件、HTML入口及少量字体、图片等静态资源,整体仅1.4MB,结构清晰便于分析和二次开发。已有65人学习下载,对PHP初学者及想掌握第三方API集成技巧的开发者都是不错的学习样本。源码包含网络请求、参数配置、错误处理、音频文件生成等实现,可帮助理解API密钥管理、数据格式转换等关键点,也可直接作为基础模板扩展使用。
1. 为什么把文字转语音直接写在 PHP 里,而不是交给前端或中间件
很多人第一次看到这套源码时都会有个疑问:浏览器里已经有 SpeechSynthesis,抖音和公众号也都自带配音,为什么还要在自己的 PHP 项目里单独调百度 API 合成本地音频文件?关键区别在于:浏览器语音依赖客户端系统 TTS 引擎,用户换台电脑音色就变,无法统一,也没法把音频持久化存档。而基于百度 API 的 PHP 方案,在服务端把文字转成 MP3 文件,意味着同样的文本任何时候访问都是同一段音频,可以预生成、缓存、二次编辑,甚至批量生产语音包。
这套资源的核心链路并不复杂:PHP 通过 HTTP 请求带着 access_token 和待合成文本去百度 TTS 开放接口,接口返回音频二进制流,PHP 写文件或直接输出到浏览器。难点集中在三个地方:token 的获取与刷新、长文本的截断策略、以及各种网络异常和 API 限额的处理。下面从协议层开始逐步拆开,最后给出一个能直接抄走的完整实现。
适合的人群有两类:一是做 CMS、在线学习平台、无障碍阅读功能的开发者,需要给文章或课件加语音;二是想把 TTS 能力尽快落地但又不想读完整百度文档的 PHP 工程师。如果你只是想在本地玩玩,用 Python 调非官方接口可能更快,但要做成在线服务、便于后续维护,PHP 这套仍然是最务实的选择。
2. 百度 TTS 接口的鉴权与参数细节
2.1 REST API 加 REST 风格,但鉴权走 OAuth 2.0
百度 AI 开放平台的语音合成接口地址是https://tsn.baidu.com/text2audio,从接口形态看属于 REST API,但调用前必须先通过https://aip.baidubce.com/oauth/2.0/token获取 access_token。这个 token 是请求签名和身份识别的凭证,官方默认有效期 30 天,过期后必须重新获取,否则会返回110或111错误码。
获取 token 时需要三个固定参数:grant_type、client_id 和 client_secret。其中 client_id 是应用的 API Key,client_secret 是 Secret Key,都可以在百度智能云控制台创建应用后拿到。这个流程很多人会误解成 OAuth 2.0 的用户授权流程,其实它属于 client credentials 模式,也就是应用自己凭密钥换 token,不涉及用户登录环节。
<?php function getAccessToken($apiKey, $secretKey) { $url = 'https://aip.baidubce.com/oauth/2.0/token'; $postData = [ 'grant_type' => 'client_credentials', 'client_id' => $apiKey, 'client_secret' => $secretKey, ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); curl_close($ch); $data = json_decode($result, true); if (isset($data['access_token'])) { return $data['access_token']; } throw new Exception('token获取失败: ' . json_encode($data)); }这段代码用的是 curl 而不是file_get_contents,主要是为了控制超时时间和拿到具体的 curl 错误信息。http_build_query把数组转成表单格式,百度 OAuth 接口接受的就是这种格式。CURLOPT_TIMEOUT设置为 10 秒,防止网络抖动导致 PHP 进程被无限挂起。注意返回内容编码是 JSON,所以必须用json_decode解析后判断是否包含 access_token。
这个 token 还有个容易被忽略的特性:它和百度 AI 控制台里的其他服务(如 OCR、NLP)共用一个 token,前提是同一个应用下开通了多个服务。所以如果项目里想复用,可以考虑把 token 存进缓存,而不是每次请求都去取。下一节会讲缓存和更新策略,这里先记住一点:token 是全局共享的,不是为单个任务临时生成的。
2.2 合成请求参数表:控制音色、语速、音量与格式
拿到 token 后,真正干活的是text2audio接口。它支持 GET 和 POST 两种方式,但文本内容较长时建议用 POST,避免 URL 超长导致 414 错误。核心参数如下表所示,这些参数直接决定合成效果和调用成本,值得逐项确认。
| 参数名 | 是否必填 | 默认值 | 可选值/说明 |
|---|---|---|---|
| tex | 是 | 无 | 要合成的文本,UTF-8 编码,最大 512 字节(中文约 170 字) |
| tok | 是 | 无 | 第 2.1 节获取的 access_token |
| cuid | 是 | 无 | 用户唯一标识,写自己应用的名称或 ID 均可,用于日志追踪 |
| ctp | 是 | 1 | 客户端类型,固定 1 表示 web 端 |
| lan | 是 | zh | 语言,zh 中文,ct 粤语,en 英语 |
| spd | 否 | 5 | 语速,0-15 数值,越大越快 |
| pit | 否 | 5 | 音调,0-15,越大越高 |
| vol | 否 | 5 | 音量,0-15 |
| per | 否 | 0 | 音色:0 女声、1 男声、3 情感男声、4 情感女声、5 儿童声 |
| aue | 否 | 3 | 返回格式:3 MP3、4 pcm、5 pcm(需配合特定编码) |
tex的最大长度是 512 字节,这是非常多新手踩坑的地方。中文一个字占 3 字节,所以一段超过 170 字的文本必须切片,不然接口直接返回错误信息而不是音频。spd(语速)和pit(音调)的数值区间是 0 到 15,注意这里不是 1 到 10,我之前习惯性填 10 以上,结果合成出来的声音快得像倍速播放,后来查文档才发现是 15 封顶。
cuid参数看似随意,但百度官方建议用它来定位问题。如果你有一个应用同时在多个服务器上调用,建议把cuid设置为服务器 ID 或站点 ID,这样发生异常时能快速区分是哪台机器发的请求。aue建议保持默认的 3(MP3),文件体积小,浏览器兼容性最好。如果项目需要后期做音频剪辑,可以取 4 的 pcm 裸流,但 pcm 没有文件头,播放器无法直接识别,必须自己加 WAV 头。
2.3 返回值的两种形态:音频流还是 JSON 错误
text2audio接口最大的“坑”是它成功时返回Content-Type: audio/mp3的二进制流,失败时却返回 JSON 文本。如果只按状态码 200 来判断成功,然后直接file_put_contents写文件,就会把 JSON 错误消息存成 mp3,播放器报错但不知道正真原因。
正确做法是先判断响应内容的前几个字节。MP3 文件的头部通常是ID3标签或以0xFF开头的帧同步字节,而 JSON 永远以{开头。下面的代码用substr截取内容头部做判断,简单且可靠。
$ch = curl_init($apiUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode == 200 && substr($content, 0, 1) === 'I') { file_put_contents($savePath, $content); } else { $error = json_decode($content, true); // 如果 JSON 解析失败,说明是未知的二进制错误 $errorMsg = $error['err_msg'] ?? '未知错误,原始内容=' . substr($content, 0, 200); throw new Exception('合成失败: ' . $errorMsg); }substr($content, 0, 1) === 'I'判断的是 MP3 的 ID3v2 标签,因为百度返回的 MP3 会带ID3头。不过这个判断不是完全保险,某些 MP3 文件可能没有 ID3 标签而是直接以0xFF开头。更稳的写法是再增加一段正则或字符串查找,检测内容是否包含"err_msg"字段。上面代码里$httpCode == 200也不代表一定成功,百度 TTS 在文本内容为空或非法时会返回 400 或 403,所以必须两者结合。
3. 与百度 API 对接的 PHP 完整实现流程
3.1 目录结构与配置文件的组织方式
这套源码下载下来后,通常是一个典型的 MVC 或单入口结构。我把关键文件按职责拆成四块:配置类、百度 API 封装类、控制器逻辑、前端模板。这样做的原因是便于后续替换成其他 TTS 服务,比如接入阿里云或腾讯云,只需要改封装层即可。
project/ ├── config/ │ └── tts.php # 存放 API Key、Secret、默认参数 ├── app/ │ ├── BaiduTts.php # TTS 核心封装类 │ ├── Auth.php # token 获取与缓存 │ └── Controller.php # 接收请求,调用 BaiduTts ├── public/ │ ├── index.php # 入口文件 │ ├── css/ # 项目原有的样式文件 │ └── js/ └── storage/ └── audio/ # 存放生成的 mp3 文件配置文件里除了 API Key 和 Secret Key,还应该放音色、语速、音量这些默认参数。很多人把参数硬编码在业务代码里,换个音色要改代码,这不方便运营同事操作。更好的方式是在控制器里通过 GET 参数覆盖默认值,但要做白名单校验,防止用户传入非法值打爆接口限额。
3.2 token 的缓存与自动刷新机制
因为 access_token 有效期长达 30 天,所以每次调用都去换取是浪费的。我会优先把 token 存到 Memcached 或 Redis,失效后再重新获取。没有 redis 的小项目也可以用文件缓存,存到一个 json 文件里,带上过期时间,到期后自动重取。
class Auth { private $cacheFile; private $apiKey; private $secretKey; public function __construct($apiKey, $secretKey, $cacheFile) { $this->apiKey = $apiKey; $this->secretKey = $secretKey; $this->cacheFile = $cacheFile; } public function getToken() { if (file_exists($this->cacheFile)) { $data = json_decode(file_get_contents($this->cacheFile), true); if ($data['expire_at'] > time() + 3600) { return $data['access_token']; } } $token = $this->requestNewToken(); $this->saveToken($token); return $token; } private function requestNewToken() { // 参考 2.1 的 getAccessToken 函数 $token = getAccessToken($this->apiKey, $this->secretKey); if (!$token) { throw new Exception('无法获取 token,请检查网络和 API Key'); } return $token; } private function saveToken($token) { // 提前一小时过期,避免临界时间请求失败 file_put_contents($this->cacheFile, json_encode([ 'access_token' => $token, 'expire_at' => time() + 29 * 24 * 3600, ])); } }这里expire_at设置为time() + 29 * 24 * 3600,不是 30 天整,因为 token 到期时正好发请求的话,百度会返回 token 无效,而重新获取还需要额外一次网络请求。提前一小时刷新可以避免这个问题。文件缓存的并发风险在于多个 PHP 进程同时发现缓存过期,同时请求新 token,造成短时间多次调用。常见做法是把 token 写入和读取加一个文件锁:flock()在写入期间让其他进程等待。不过对于低并发的后台合成场景,这个风险可接受。
3.3 文本切片策略与合成主方法
前面提到tex最大 512 字节,所以切片是必做的。切片时不能按 PHP 的strlen直接切,因为中文是 UTF-8 多字节字符,按字节切容易把一个汉字切成两半,导致合成出来的声音变成乱码或接口直接报错。我用mb_strimwidth或mb_substr按字符切,然后循环直到剩余文本为空。
class BaiduTts { private function splitText($text) { $maxBytes = 450; // 留一点余量给标点符号 $parts = []; while (mb_strlen($text, 'UTF-8') > 0) { // 逐字符增加直到字节数接近上限 $len = 0; $part = ''; $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY); foreach ($chars as $char) { $part .= $char; $len += strlen($char); if ($len >= $maxBytes) { break; } } $parts[] = $part; $text = mb_substr($text, count(preg_split('//u', $part, -1, PREG_SPLIT_NO_EMPTY)), null, 'UTF-8'); } return $parts; } public function synthesize($text, $options) { $parts = $this->splitText($text); $audioFiles = []; $token = $this->auth->getToken(); foreach ($parts as $index => $part) { $audio = $this->requestAudio($part, $token, $options); $tmpFile = $this->saveTmpFile($audio, $index); $audioFiles[] = $tmpFile; } // 如果多段,可以拼接成完整音频 if (count($audioFiles) > 1) { return $this->mergeAudio($audioFiles); } return $audioFiles[0]; } }splitText方法用了preg_split('//u')把字符串拆成字符数组,注意u修饰符必须加,否则正则会把中文字节拆散。$maxBytes我设置为 450,而不是 512,因为合成过程中百度可能对文本长度做二次检查,留一点缓冲更安全。requestAudio方法内部就是上一节的 curl 请求,并把返回的二进制内容写入临时文件。
3.4 前端页面与音频播放/下载接口
后面还有最后一个环节要打通:用户输入文字后,浏览器如何拿到音频并播放。多数人直接用form提交,然后让页面跳转到音频文件 URL,体验比较原始。更好的做法是前端用fetch提交文本,后端返回 JSON 格式的音频地址,前端再动态创建一个<audio>标签播放,同时提供一个下载按钮。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>在线文字转语音</title> <link rel="stylesheet" href="css/style.min.css"> </head> <body> <div class="container"> <textarea id="text" rows="4" placeholder="输入要转换的文字"></textarea> <select id="per"> <option value="0">普通女声</option> <option value="1">普通男声</option> <option value="3">情感男声</option> <option value="4">情感女声</option> </select> <button id="btn">合成语音</button> <audio id="audio" controls style="display:none"></audio> <a id="download" href="#" style="display:none">下载 MP3</a> </div> <script> document.getElementById('btn').addEventListener('click', async function() { const text = document.getElementById('text').value; const per = document.getElementById('per').value; const formData = new FormData(); formData.append('text', text); formData.append('per', per); const res = await fetch('/tts.php', { method: 'POST', body: formData }); const data = await res.json(); if (data.code === 0) { document.getElementById('audio').src = data.url; document.getElementById('audio').style.display = 'block'; document.getElementById('download').href = data.download; document.getElementById('download').style.display = 'inline'; } else { alert(data.message); } }); </script> </body> </html>注意per选择器的值必须和百度 API 里定义的音色编号一致,千万不能自己随便命名。后端返回的url可以是临时拼接的路由,例如getAudio.php?file=xxxx.mp3,这样下载时能通过 HTTP 头强制浏览器保存,避免直接暴露存储目录。download链接也可以用download属性,但跨域或文件流输出时要注意设置Content-Disposition。
4. 部署到 Web 环境时的配置与异常处理
4.1 Nginx 和 PHP-FPM 的超时与请求体配置
本地开发时一切正常,一上生产环境就出现合成超时或 502,这是最常见的问题。百度 TTS 接口对一段几百字文本的响应时间通常在 1 到 3 秒,但如果网络不好或文本被切成多段,PHP 脚本执行时间很有可能会超过 PHP-FPM 默认的max_execution_time(默认 30 秒)。更隐蔽的是 Nginx 的fastcgi_read_timeout,如果设为默认 60 秒,PHP 处理 10 段音频合成就可能用时 30 秒以上,Nginx 不会报错但会提前断开连接。
我一般在部署时检查三个配置项,并按照实际场景调整:
| 配置项 | 位置 | 推荐值 | 说明 |
|---|---|---|---|
max_execution_time | php.ini 或 php-fpm pool | 300 | 允许 PHP 脚本长运行,但不要设太大 |
max_input_vars | php.ini | 2000 | 防止大量文本以 GET 方式传参被截断 |
proxy_read_timeout | Nginx server/location | 300 | 反向代理场景下也要同步调整 |
同时,PHP-FPM 的进程数如果太少,多个人同时点合成会全卡在等待 curl 返回,导致队列堆积。建议至少配置pm.max_children = 20,并且给 curl 设置CURLOPT_CONNECTTIMEOUT(连接超时)和CURLOPT_TIMEOUT(总超时)两个不同值。连接超时一般 5 秒够了,总超时根据文本长度动态调整,比如文本每增加 100 字,总超时增加 2 秒。
4.2 返回码与错误码对照,快速定位问题
百度 TTS 接口返回的 JSON 错误里,err_no字段是排查问题的关键。我把常见错误码整理成一张映射表,遇到问题直接对照。
| err_no | 含义 | 解决方案 |
|---|---|---|
| 500 | 不支持的语言 | 检查 lan 参数,目前只支持 zh/ct/en |
| 501 | 音色参数错误 | per 值必须在 0-5 之间,不能传浮点数 |
| 502 | 文本过长 | 检查切片逻辑,确保每段 512 字节内 |
| 503 | 缺少合成参数 | 检查 tok、tex、cuid、ctp 是否全部存在 |
| 110 | token 无效或过期 | 重新获取 token,检查时间戳是否提前刷新 |
| 111 | token 缺失 | 确认 getToken 返回值非空 |
除了接口错误码,还有一个常见的本地调试误区:浏览器直接访问text2audio接口时,如果返回一段 JSON,有些人会以为是接口地址错了,其实是因为传参少了lan=zh或ctp=1。建议在 PHP 端写一个日志方法,把每次请求的 URL 参数和响应前 200 字节记录到runtime/log/tts.log,这样对照错误码就能知道是参数问题还是权限问题。
提示:百度 TTS 接口并不要求限频严格,但免费配额有限,生产环境最好在 PHP 端做简单的频率限制,例如同一 IP 每分钟最多合成 5 次,否则超出配额后所有请求都会返回带有
err_no: 18的错误。
4.3 音频文件存储与防盗链处理
生成的 MP3 文件如果直接放在public/audio目录下,任何人知道 URL 都能反复下载,消耗流量和 API 配额。更合理的方式是把音频存到服务器任意目录,比如/var/www/tts_storage/,然后通过 PHP 脚本输出。
$file = '/var/www/tts_storage/' . basename($_GET['file']); if (!file_exists($file)) { http_response_code(404); exit('文件不存在'); } header('Content-Type: audio/mpeg'); header('Content-Disposition: attachment; filename="' . basename($file) . '"'); header('Content-Length: ' . filesize($file)); readfile($file);basename()在这里非常关键,能防止用户传入../../etc/passwd之类的路径穿越。合法文件名最好生成得像随机串一样,例如md5(uniqid(mt_rand(), true)) . '.mp3',这样外部无法通过规律猜测其他用户的音频。如果需要更长保留期,可以在数据库或 Redis 里记录文件与文本的映射关系,定期清理过期文件。
5. 进阶技巧:批量合成、长文本拼接与缓存命中率优化
这一节是给已经跑通基础功能的开发者准备的。你可能会发现,单纯把文字转成语音并不难,难的是在有限 API 配额下做出更顺滑的体验。下面三个方向是我实际项目中迭代过好几轮的方案。
第一个技巧:对相同文本做 MD5 缓存。百度 TTS 是按次数计费的,同一个文本提交两次就是两次费用。我通常在调用合成之前,先计算文本的 MD5 值(注意要连同音色参数、语速、音调一起参与计算,因为不同参数合成的结果不同),然后检查storage/audio/md5.mp3是否存在,存在就直接返回文件路径。这个缓存命中率在没有动态文本的 CMS 站点里相当可观,例如一篇固定文章可能被上百个用户请求播放,但只合成一次就够了。
第二个技巧:长文本的段落之间要留静音。splitText切出的每一段在合成时是独立的,直接拼接后,段与段之间的空隙常常不到 100 毫秒,听起来像一句话没说完就被掐断。常见的解决办法是在每段音频的末尾追加一小段静音,或者在合成时给spd、pit调整,但这并不能解决停顿问题。更实用的方案是用 FFmpeg 在拼接时插入 300ms 的空白:
ffmpeg -i 0001.mp3 -i 0002.mp3 -filter_complex "aresample=async=1:first_pts=0,adelay=300|300" -i 0003.mp3 -filter_complex "concat=n=3:v=0:a=1" output.mp3不过手动拼接多段文件容易出错,我更推荐在 PHP 端用ffmpeg命令,先生成一个concat.txt文件,列出所有分段,然后在每个分段名之间插入一个空音频文件,最后用 concat 协议合并。这样代码逻辑更清晰,也方便后期直接替换某一段重新合成。
第三个技巧:用定时任务预生成热门音频。如果你的网站有位文章详情页,可以在文章发布的时候,后台异步生成整篇文字的音频,而不是等用户点击时才生成。写一个cli/tts_prebuild.php,扫描最近发布但还没有音频文件的文章,逐篇调用BaiduTts::synthesize(),然后把结果绑定到文章 ID 上。
$pendingArticles = $db->query("SELECT id, title, content FROM article WHERE is_audio_generated = 0 LIMIT 10"); foreach ($pendingArticles as $article) { try { $audioPath = $tts->synthesize($article['content'], $defaultOptions); $db->exec("UPDATE article SET audio_path = '$audioPath', is_audio_generated = 1 WHERE id = " . $article['id']); } catch (Exception $e) { // 记录失败原因,下个周期重试 echo $article['id'] . ':' . $e->getMessage() . PHP_EOL; } }这个脚本放进 crontab,每五分钟执行一次,一方面均匀分散 API 调用压力,另一方面用户访问时不阻塞请求。试想一下,同一台服务器既要处理用户请求,又要同步调用百度和存储文件,很容易出现 PHP-FPM 进程耗尽,所以把重活放到异步任务里,是很多线上项目的标准做法。
如果你基于这套源码继续往下扩展,还可以把音频生成记录写到数据库,做一个管理面板来查看每个用户每天合成了多少字,哪个音色最常用。甚至可以在百度控制台开通语音合成长文本 API,那样就省去了自己切片的麻烦,不过对应的价格也更高。搞清楚自己项目的核心诉求是「快速实现」还是「极致成本」,再去决定要不要替换底层实现。
本文还有配套的精品资源,点击获取