PHP调用百度TTS接口实现文字转语音:从鉴权到部署
发布时间:2026/9/15 22:33:41 锦皓数字建站

简介一份基于百度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 Keyclient_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_tokencuid是无用户唯一标识写自己应用的名称或 ID 均可用于日志追踪ctp是1客户端类型固定 1 表示 web 端lan是zh语言zh 中文ct 粤语en 英语spd否5语速0-15 数值越大越快pit否5音调0-15越大越高vol否5音量0-15per否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建议保持默认的 3MP3文件体积小浏览器兼容性最好。如果项目需要后期做音频剪辑可以取 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 langzh-CN head meta charsetUTF-8 title在线文字转语音/title link relstylesheet hrefcss/style.min.css /head body div classcontainer textarea idtext rows4 placeholder输入要转换的文字/textarea select idper option value0普通女声/option option value1普通男声/option option value3情感男声/option option value4情感女声/option /select button idbtn合成语音/button audio idaudio controls styledisplay:none/audio a iddownload href# styledisplay: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?filexxxx.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_timephp.ini 或 php-fpm pool300允许 PHP 脚本长运行但不要设太大max_input_varsphp.ini2000防止大量文本以 GET 方式传参被截断proxy_read_timeoutNginx server/location300反向代理场景下也要同步调整同时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/en501音色参数错误per 值必须在 0-5 之间不能传浮点数502文本过长检查切片逻辑确保每段 512 字节内503缺少合成参数检查 tok、tex、cuid、ctp 是否全部存在110token 无效或过期重新获取 token检查时间戳是否提前刷新111token 缺失确认 getToken 返回值非空除了接口错误码还有一个常见的本地调试误区浏览器直接访问text2audio接口时如果返回一段 JSON有些人会以为是接口地址错了其实是因为传参少了lanzh或ctp1。建议在 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 aresampleasync1:first_pts0,adelay300|300 -i 0003.mp3 -filter_complex concatn3:v0:a1 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那样就省去了自己切片的麻烦不过对应的价格也更高。搞清楚自己项目的核心诉求是「快速实现」还是「极致成本」再去决定要不要替换底层实现。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。