
简介本资源是面向PHP开发者的一站式讯飞语音识别API集成方案专为需快速接入长语音转写能力的后端项目设计解决中文语音识别准确率低、英文支持弱、长音频处理复杂等实际开发痛点适用于语音搜索、会议记录、客服语音分析等场景。压缩包共12个文件41KB含6个核心PHP类文件如XFLongFormAsrClient.php实现分片上传与结果合并、RequestApi.php封装HTTP请求、README.md与说明文档提供完整调用流程、附赠.docx含常见错误码解析与调试建议LICENSE与composer.json确保合规集成与依赖管理。目前已有171人学习下载开发者可直接通过Composer安装依赖无需理解底层协议细节即可调用支持中英文混合识别的长语音转写接口显著降低语音能力接入门槛与调试成本。 上周渠道部甩过来三百多个客服录音文件说三天内全部转成可检索文本。我第一反应是找外包一看报价和时效直接放弃第二反应是翻语音识别API最终敲定了讯飞开放平台语音识别接口。这篇文章记录的就是我基于这个接口做的一个PHP实现项目支持长语音转写中英文都能识别整个封装通过Composer安装依赖就能集成到现有PHP项目里。项目说大不大但踩坑不少。真正做完你会发现讯飞长语音转写API的完整链路是上传音频 - 拿taskId - 轮询结果 - 解析文本四步每一步都有不少容易被文档忽略的细节。如果你也准备把语音识别能力接进PHP系统或者正在为一批录音文件发愁这篇内容应该能帮你少走很多弯路。我会把从选型、封装、联调到上线的完整过程都讲透包括中间踩过的坑和最后的排查方案。1. 长语音转写需求从哪来被三百个录音文件逼出的封装项目1.1 业务场景录音文件转文本的刚需需求本身不复杂渠道部的客服通话录音每天产生几十条月底要统一转写成文字用来做质检关键词检索和服务投诉复盘。之前一直是运营同事手动听写一条十分钟的录音至少要花二十分钟来处理三百条录音光听写就要一百多个小时三天无论如何都完不成。这种场景其实很典型。客服质检、会议纪要、课程音频转文稿、访谈记录整理本质都是同一件事把已经存在的音频文件变成可搜索、可编辑、可归档的文本。它和实时语音识别是两条完全不同的技术路线实时识别关注的是边说边出字离线转写关注的是整段音频如何高准确率地出稿。我一开始也没想清楚这个区别还尝试过用流式接口硬扛结果发现要么音频长度受限要么网络抖动导致中断后来才把目光放到长语音转写这类专用接口上。1.2 为什么选中讯飞开放平台而不是其他方案国内能提供语音转写能力的平台不少百度、阿里、腾讯都有相关产品。我当时的对比维度有三个中文识别准确率、长音频支持友好度、开发者接入成本。讯飞在这三点的综合表现比较靠前尤其是客服录音里常见的口音、数字、专业名词混读实测下来讯飞的识别准确率确实要高一些。另一个原因是讯飞开放平台对离线转写场景有独立的录音文件转写接口专门处理长度在几分钟到几小时之间的音频而不是把流式接口强行拉长时间这省了我自己切分音频、拼接结果的很多事。对比维度讯飞开放平台百度智能云阿里云中文识别准确率较高口音适配好中上需训练模型提升中上需配置热词长音频接口有独立录音文件转写有录音文件识别有录音文件识别PHP SDK 维护官方SDK不完善官方有PHP SDK官方有PHP SDK免费额度注册即送体验额度有免费额度有免费额度还有一个很现实的原因PHP生态里关于讯飞语音识别的成熟封装本来就少用其他平台同样要自己写HTTP客户端。既然都要封装选一个识别效果更稳的对最终用户更负责。1.3 为什么自己封装而不是直接用官方SDK讯飞开放平台官方SDK覆盖了Java、Python、C等主流语言PHP不是它的重点维护对象。我翻了一下官方PHP示例代码还停留在比较早期的写法没有遵循PSR规范也没有Composer集成直接放到现代PHP项目里需要改不少地方。与其每次都要从官网复制代码再手工改造不如我自己封装成一个Composer包统一的命名空间、统一的异常处理、统一的日志输出以后其他项目要用一行composer require就能拉进来。这个决策回头看是正确的。封装过程逼着我把签名、轮询、文件上传这些细节全部读透遇到问题可以自己定位而不是对着黑盒SDK干瞪眼。封装之后调用方只需要传入音频文件路径几行代码就能拿到转写文本业务侧不用关心API细节。2. Composer依赖与项目骨架先把封装的地基建好2.1 项目目录与自动加载设计封装一个Composer包第一步不是写API调用而是把目录结构和自动加载规则定好。我采用的是最常见的PSR-4结构包名用的yourname/xfyun-lfasr实际发布时可以换成自己的命名空间。xfyun-lfasr/ ├── composer.json ├── README.md ├── config/ │ └── xfyun.php ├── src/ │ ├── LfasrClient.php │ ├── SignatureHelper.php │ └── Exception/ │ └── XfyunApiException.php ├── examples/ │ └── transcribe.php └── tests/ └── SignatureHelperTest.phpsrc目录放核心代码config放默认配置examples提供可直接运行的示例脚本tests放单元测试。这个分层的好处是核心代码不依赖具体配置项客户端实例化时传入AppID、APIKey、APISecret就行配置文件只是方便使用者统一管理。PSR-4自动加载规则在composer.json里声明把YourName\\XfyunLfasr\\映射到src/目录之后所有类都放src下对应路径即可不需要手动维护加载文件。2.2 composer.json怎么配composer.json是整个包的说明书我建议把PHP版本要求、依赖、自动加载、扩展信息都写清楚。这是我的配置{ name: yourname/xfyun-lfasr, description: 讯飞开放平台语音识别接口的PHP封装支持长语音转写、中英文识别, type: library, license: MIT, require: { php: 7.4, guzzlehttp/guzzle: ^7.0, monolog/monolog: ^2.0 }, require-dev: { phpunit/phpunit: ^9.0 }, autoload: { psr-4: { YourName\\XfyunLfasr\\: src/ } }, autoload-dev: { psr-4: { YourName\\XfyunLfasr\\Tests\\: tests/ } } }使用者只需要在项目根目录执行composer require yourname/xfyun-lfasrComposer会自动拉取Guzzle和Monolog。如果项目本身已经装了Guzzle也不用担心重复安装Composer会做版本仲裁。我把PHP最低版本设为7.4因为7.4之前的版本EOL已久而且typed properties、箭头函数这些语法在封装时很好用。如果你的项目还在PHP 5.6上那不建议用这个包先升级PHP更现实。2.3 Guzzle这个依赖帮我们省了哪些事讯飞语音识别接口本质上就是HTTP接口上传文件、轮询结果都靠请求响应。我选择Guzzle而不直接用file_get_contents或curl扩展原因是Guzzle把HTTP客户端该有的能力都封装好了超时控制、重试中间件、multipart文件上传、JSON解析、请求日志这些都是真实项目里的刚需。举一个具体例子上传音频文件时multipart格式如果手写很容易在文件流边界上出错Guzzle直接接受fopen资源作为文件流底层由cURL处理我不用关心Content-Type和Content-Length的拼接。轮询任务时Guzzle的timeout和connect_timeout参数可以分别控制请求超时和连接超时避免某个节点挂起导致进程卡死。另一个实用点是Guzzle支持retry中间件短时间的网络抖动可以自动重试这在后面轮询长任务时非常有用。3. 长语音转写API的核心机制上传、签名与轮询3.1 长语音转写与实时语音听写的本质区别讯飞开放平台有两类语音识别产品很多人第一次接触容易混淆。一类是实时语音听写通常走WebSocket长连接适合App内实时字幕、语音输入法这类交互场景特点是边说话边出结果但连接保持时间有限网络波动会导致识别中断。另一类是录音文件转写也就是长语音转写走的是HTTP请求把完整的音频文件上传到服务端异步处理后再取回结果适合对已录制音频做批量转写。这两类接口的调用方式完全不同。长语音转写不需要维持长连接核心逻辑是提交任务、等待完成、拉取结果本质是一个异步任务系统。理解了这一点后面实现轮询就不会觉得奇怪。我当时差点一开始就接实时听写后来发现单条录音超过接口时长限制才转向录音文件转写。3.2 签名鉴权为什么要签怎么签讯飞开放平台的接口鉴权核心思想是AppID标识身份APIKey/APISecret签名防篡改。请求方需要把当前时间戳、AppID、请求参数按照一定规则拼成签名原串再用APISecret做摘要服务端用同样的算法校验这样就算有人截获了请求也无法伪造新的请求。我封装时写了两种签名方式分别对应讯飞不同版本接口的需求。一种是老版本常用的MD5拼接方式上传时对appId ts做MD5查询时对appId ts taskId做MD5另一种是HMAC-SHA256方式用APISecret作为密钥对签名原串做HMAC加密再base64编码。需要注意签名原串的具体拼接字段每个版本的文档可能有差异我建议以你申请应用时开放平台提供的接入文档为准。?php declare(strict_types1); namespace YourName\XfyunLfasr; class SignatureHelper { /** * 老版本接口MD5 签名 */ public static function signByMd5(string $appId, string $ts, string $taskId ): string { return md5($appId . $ts . $taskId); } /** * 新版本接口HMAC-SHA256 签名 */ public static function signByHmac(string $apiSecret, string $signatureOrigin): string { return base64_encode(hash_hmac(sha256, $signatureOrigin, $apiSecret, true)); } }实际请求时ts用Unix时间戳字符串注意用当前服务器时间如果本地服务器时间偏差过大会被判定为签名过期。我第一次联调时就在这个问题上栽了跟头服务器时间慢了五分钟怎么签都是鉴权失败。3.3 任务状态机与轮询策略录音文件转写的任务状态通常包含提交成功、处理中、处理完成、处理失败几个阶段。上传音频后服务端会返回一个taskId后续所有查询都靠这个ID。查询结果里会带一个状态字段比如值为9时表示处理完成值为-1时表示失败。轮询策略上我一开始用的是固定间隔一秒查一次跑了几个任务发现对服务端压力不小而且很多任务十几秒内根本不会结束白白浪费请求。后来改成指数退避初始间隔两秒之后每次加倍最大间隔十秒状态变为处理完成或失败才停止。同时设置一个总超时时间比如二十分钟超过这个时间就判定任务异常写入日志并告警。这里还可以结合Guzzle的retry中间件对网络类错误做有限次重试但要注意重试不要叠加到业务轮询里否则会重复请求。4. 核心代码实现音频文件到文字的完整链路4.1 签名工具类签名工具类很简单两个静态方法就够了。上面代码里已经给出这里补充一个生成HMAC签名原串的例子。不同版本接口对签名原串的格式要求不同常见的格式是把请求方法、请求路径、日期时间、Content-Type拼成一个带换行的字符串再用APISecret做HMAC-SHA256。我在项目里封装了一个方法专门负责组装这个原串方便按文档调整。public static function buildSignatureOrigin(string $method, string $host, string $path, string $datetime): string { return host: {$host}\n . date: {$datetime}\n . {$method} {$path} HTTP/1.1\n . content-type: application/json; }4.2 LfasrClient主类实现LfasrClient是封装的门面负责上传音频、查询结果等核心操作。构造函数接收AppID、APIKey、APISecret同时可以传入一个Guzzle客户端实例或配置数组方便测试时mock。?php declare(strict_types1); namespace YourName\XfyunLfasr; use GuzzleHttp\Client; use RuntimeException; class LfasrClient { private string $appId; private string $apiKey; private string $apiSecret; private Client $client; public function __construct( string $appId, string $apiKey, string $apiSecret, ?Client $client null ) { $this-appId $appId; $this-apiKey $apiKey; $this-apiSecret $apiSecret; $this-client $client ?? new Client([ base_uri https://api.xfyun.cn, timeout 30, ]); } /** * 上传音频文件返回任务ID */ public function upload(string $filePath): string { if (!is_file($filePath)) { throw new RuntimeException(音频文件不存在: {$filePath}); } $ts (string) time(); $signa SignatureHelper::signByMd5($this-appId, $ts); $response $this-client-post(/v1/service/v1/lfasr/upload, [ headers [ appId $this-appId, ts $ts, signa $signa, ], multipart [ [ name file, contents fopen($filePath, r), filename basename($filePath), ], ], ]); $result json_decode((string) $response-getBody(), true); if (($result[ok] ?? -1) ! 0) { throw new RuntimeException(上传音频失败: . json_encode($result, JSON_UNESCAPED_UNICODE)); } return $result[data][taskId] ?? ; } /** * 查询任务状态与转写结果 */ public function query(string $taskId): array { $ts (string) time(); $signa SignatureHelper::signByMd5($this-appId, $ts, $taskId); $response $this-client-post(/v1/service/v1/lfasr/query, [ headers [ appId $this-appId, ts $ts, signa $signa, ], json [ taskId $taskId, ], ]); return json_decode((string) $response-getBody(), true); } }这里需要说明两点。第一上传文件时multipart里的filename要带上扩展名服务端可能通过扩展名判断音频格式。第二错误处理的关键是先把HTTP状态码和业务状态码分开判断HTTP 200不代表业务成功ok字段为0才是成功这个误判是常见的联调坑。我团队里一个小伙伴就因为这个原因把上传失败当成功拿着空taskId去轮询白白排查了半天。4.3 完整调用示例下面是examples/transcribe.php的代码演示了从上传到轮询再到输出文本的完整调用过程?php require __DIR__ . /../vendor/autoload.php; use YourName\XfyunLfasr\LfasrClient; $appId 你的AppID; $apiKey 你的APIKey; $apiSecret 你的APISecret; $client new LfasrClient($appId, $apiKey, $apiSecret); $audioFile $argv[1] ?? demo.mp3; // 1. 上传 $taskId $client-upload($audioFile); echo 任务ID: {$taskId}\n; // 2. 轮询 $maxWait 600; // 最长等10分钟 $interval 2; $start time(); while (true) { if (time() - $start $maxWait) { throw new RuntimeException(任务处理超时); } $result $client-query($taskId); $status $result[data][status] ?? -99; if ($status 9) { $text $result[data][result] ?? ; echo 转写结果: {$text}\n; break; } if ($status -1) { throw new RuntimeException(任务处理失败: . json_encode($result, JSON_UNESCAPED_UNICODE)); } sleep($interval); $interval min($interval * 2, 10); }这个脚本可以直接从命令行跑php examples/transcribe.php meeting.mp3。日志输出不要用echo糊在所有代码里实际项目建议接Monolog。我在示例里用echo只是为了降低阅读门槛正式封装的LfasrClient本身不输出任何内容把日志留给调用方去处理。5. 中英文识别与音频格式兼容5.1 language参数与中英文混合场景讯飞长语音转写接口支持中英文识别但默认不一定同时开启。某些接口版本需要在请求参数里显式指定识别语言比如中文普通话对应zh_cn英文对应en_us也有支持混合识别的模式。如果你的音频里有中英文夹杂比如技术会议录音、外贸客服对话建议选择混合识别模式否则可能把英文单词识别成不相关的中文拼音。我在项目里留了一个language配置项默认是zh_cn同时把混合识别作为可选参数开放给调用方。测试下来中英文混合模式下人名、产品名、技术术语的识别准确率明显好于纯中文模式。如果你的业务里有大量专业术语还可以用讯飞的热词表功能把常见词预先上传识别时会优先匹配这个后面扩展部分再说。5.2 音频格式与采样率为什么用FFmpeg统一转成16kHz WAV讯飞长语音转写对音频格式和采样率有明确要求常见的mp3、wav、m4a、pcm都支持但不同格式的识别准确率和兼容性差异不小。我实测下来最稳的组合是16kHz采样率、单声道、16bit WAV格式。这种格式在语音识别领域几乎是标准输入服务端无需额外转码特征提取损失最小识别速度也快。实际项目中业务方丢过来的音频千奇百怪有微信语音的m4a有录音笔的wav有电话合成音频的mp3声道、采样率五花八门。我在封装里没有直接处理音频转换而是建议在调用前用FFmpeg统一转码命令很简单ffmpeg -i input.m4a -ar 16000 -ac 1 -acodec pcm_s16le output.wav-ar 16000表示采样率16kHz-ac 1表示单声道-acodec pcm_s16le表示16位little-endian编码。转成标准WAV之后再调用上传接口能省掉很多奇怪的问题。如果你的项目不方便装FFmpeg也可以用讯飞提供的一些音频转码SDK但FFmpeg是跨平台最省事的方案。5.3 超大音频的切分与合并策略长语音转写接口对单文件时长和大小有限制通常单个文件不能超过几十MB或几小时。如果录音文件超过限制就需要先切分再分别转写最后合并文本。切分时要注意两个问题一是切分点尽量选择静音段避免在句子中间切断否则前后两段的第一个字和最后一个字都可能识别不准二是每段之间要留一点重叠比如上一段末尾保留半秒到一秒的音频写文本时再把重叠部分去重这样能有效避免因切断导致的漏字。我用FFmpeg做静音检测把音频按静音段切成若干片段再逐段调用转写接口最后按顺序拼接文本效果比等间隔硬切好很多。6. 实测中的坑与排查方案6.1 鉴权失败的高频原因与排查链路联调阶段遇到最多的报错就是鉴权失败错误信息一般会直接提示signa校验不通过。我总结了三类高频原因每类都踩过。第一类是时间戳问题。ts用的不是当前Unix时间戳或者服务器时间与标准时间偏差过大。这个问题的排查方法很简单在服务器上执行date %s看当前时间戳再和在线时间戳工具对比偏差超过一分钟就建议配置NTP自动同步。有一次生产环境服务器时间慢了好几分钟所有请求全部鉴权失败排查到最后才发现是服务器长时间没有同步时间。第二类是签名串拼错。MD5签名方式是appId ts但有的接口文档要求appId ts taskId少拼一个字段就必然报错。HMAC-SHA256方式更麻烦签名原串里的大小写、换行符、空格都和最终签名强相关多一个空格都过不了。我建议把签名函数写成单元测试固定输入输出用官方示例的appId和ts跑一遍能过测试再接到主逻辑里。第三类是参数位置放错。有的接口要求把appId、ts、signa放在请求头里有的放在JSON body里。我在封装时专门写了不同版本的适配方法统一对外暴露内部按文档要求放到对应位置。6.2 轮询卡住、结果延迟怎么处理轮询长时间不结束通常有两种情况任务真的在排队处理或者查询参数有问题导致永远查不到有效状态。音频文件较大的时候服务端需要排队解码、识别、后处理等待时间长是正常的。我处理的办法是给轮询加总超时同时把每次轮询的响应时间、状态变化记录到日志。如果任务状态长时间不变且超过了预估时间可以尝试用同一个音频文件重新提交任务让服务端用新的taskId处理。还有一次我遇到查询接口轮询一直返回ok: 0但status字段一直为0排查后发现是我把查询请求JSON里的字段名写成了task_id而接口要求的是taskId。这种大小写和命名风格不一致的问题在联调阶段很容易被忽略建议直接对照接口文档核对请求体。把查询请求和响应都打印到日志里再比对文档通常能找到问题。6.3 并发限制与配额管理讯飞开放平台的语音转写接口通常有并发和配额限制比如同一AppID同时处理的音频任务数有限单位时间内请求次数也有限。批量转写三百个文件时如果一股脑全部提交任务很快会触发限流报请求过多或并发超限。我的解决方案是在封装外面加一个简单的任务队列控制器最大同时提交的任务数设置成3其余任务排队等前面的任务进入终态并释放配额后再提交下一个。用PHP数组实现生产者消费者模型比想象中简单关键是信号量要控制好。另一个经验是尽量在业务低峰期跑批量任务比如凌晨执行配合定时任务第二天早上直接取结果这样既不会撞上白天的高峰限流也不影响线上其他业务。7. 项目落地效果与后续扩展思路7.1 实测三百个录音文件的转写结果项目上线后我拿渠道部的三百个客服录音文件做了实测。文件平均时长在8到15分钟之间格式以mp3和m4a为主。用FFmpeg统一转成16kHz单声道WAV后平均每个文件的上传和转写时间在3到8分钟左右整体转写准确率按字符级对比中文部分在90%以上英文部分略低一些但也能满足质检关键词检索的需求。有一个细节值得注意录音质量对识别结果影响极大。同样一段话安静环境录的音频识别准确率明显高于带有背景音乐的录音。如果音频里有长时间的音乐或噪声建议先用FFmpeg做降噪处理比如用highpass和lowpass滤波器切掉非语音频段能提升一定准确率。我后来在转码脚本里默认加了简单的降噪参数识别结果的可用度提高不少。7.2 扩展方向热词、标点、Webhook回调这个项目目前只实现了核心的转写能力但实际上还可以做很多扩展。讯飞开放平台支持热词表和个性化词典把业务里的常见词、人名、产品名、地名预先上传识别时会优先匹配能显著提升专业术语的准确率。如果你处理的是客服领域录音建议把商品名称、优惠活动关键词、客服人员名单都加进去。另外标点预测也很实用。早期接口返回的文本不带标点几乎是一整段话后端做句子切分和关键词检索都比较困难。后来我发现接口支持标点预测能力开启后返回的文本会自动加上逗号、句号、问号文本可用性提升了一个档次。如果你的业务需要对转写文本做语义分析标点预测建议开启。Webhook回调是另一个值得做的方向。目前轮询方式是主动拉取简单但费请求。讯飞一些接口支持回调通知任务完成后服务端主动POST结果到指定URL。如果你们的服务有公网接口可以改成回调模式省去轮询开销实时性也更好。我在后续版本里加了回调配置项但现在还是以轮询为主因为回调接口需要单独部署公网地址不是所有项目都具备这个条件。最后再分享一个小技巧转写结果的文本有时候会带分句标记或置信度信息不要直接当纯文本展示给用户可以在入库前做一次清洗。我在项目里加了一个格式化器把转写返回的JSON按句切分用换行符分隔前端展示时段落感清晰很多。这个细节不起眼但对最终体验影响很明显。实测下来用户对带段落、带标点、可以按句检索的转写文本满意度远高于一大坨连续字符。本文还有配套的精品资源点击获取