diff --git a/app/api/controller/XunFeiController.php b/app/api/controller/XunFeiController.php index fb570235b..358d78178 100644 --- a/app/api/controller/XunFeiController.php +++ b/app/api/controller/XunFeiController.php @@ -139,103 +139,131 @@ class XunFeiController extends BaseApiController if (!file_exists($file)) { return $this->fail('未上传音频文件'); } + $filesize = filesize($file); + if ($filesize > 1 * 1024 * 1024) { + return $this->fail('录音文件太长'); + } + $ext = pathinfo($file, PATHINFO_EXTENSION); + $extArray = ['mp3', 'pcm']; + if (!in_array($ext, $extArray)) { + return $this->fail('录音格式错误'); + } + $encoding = 'raw'; + if ($ext == 'mp3') { + $encoding = 'lame'; + } + if ($ext == 'pcm') { + $encoding = 'raw'; + } // $file = "https://lihai001.oss-cn-chengdu.aliyuncs.com/media/iat_mp3_16k.mp3"; $audioFile = fopen($file, 'rb'); if ($audioFile === false) { return $this->fail('音频文件异常'); } - $words = ''; - $iatHostUrl = "wss://iat-api.xfyun.cn/v2/iat"; - $iat = new IatClient($this->app_id, $this->api_key, $this->api_secret); - $client = new Client($iat->assembleAuthUrl($iatHostUrl)); - if ($client) { - $frameSize = 1280; //每一帧的音频大小 - $intervel = 20 * 1000; //发送音频间隔 - $status = 0; - while (true) { - $len = fread($audioFile, $frameSize); - if ($len === false) { - break; - } - if ($len === '') { //文件读取完了 - $status = 2; - } - switch ($status) { - case 0: //发送第一帧音频,带business 参数 - $frameData = array( - 'common' => array( - 'app_id' => $this->app_id //appid 必须带上,只需第一帧发送 - ), - 'business' => array( //business 参数,只需一帧发送 - 'language' => 'zh_cn', - 'domain' => 'iat', - 'accent' => 'mandarin' - ), - 'data' => array( - 'status' => 0, - 'format' => 'audio/L16;rate=16000', - 'audio' => base64_encode($len), - 'encoding' => 'lame' - ) - ); - $client->send(json_encode($frameData)); - $status = 1; + + try { + $words = ''; + $iatHostUrl = "wss://iat-api.xfyun.cn/v2/iat"; + $iat = new IatClient($this->app_id, $this->api_key, $this->api_secret); + $client = new Client($iat->assembleAuthUrl($iatHostUrl)); + if ($client) { + $frameSize = 1280; //每一帧的音频大小 + $intervel = 20 * 1000; //发送音频间隔 + $status = 0; + while (true) { + $len = fread($audioFile, $frameSize); + if ($len === false) { break; - case 1: - $frameData = array( - 'data' => array( - 'status' => 1, - 'format' => 'audio/L16;rate=16000', - 'audio' => base64_encode($len), - 'encoding' => 'raw' - ) - ); - $client->send(json_encode($frameData)); + } + if ($len === '') { //文件读取完了 + $status = 2; + } + switch ($status) { + case 0: //发送第一帧音频,带business 参数 + $frameData = array( + 'common' => array( + 'app_id' => $this->app_id //appid 必须带上,只需第一帧发送 + ), + 'business' => array( //business 参数,只需一帧发送 + 'language' => 'zh_cn', + 'domain' => 'iat', + 'accent' => 'mandarin' + ), + 'data' => array( + 'status' => 0, + 'format' => 'audio/L16;rate=16000', + 'audio' => base64_encode($len), + 'encoding' => $encoding + ) + ); + $client->send(json_encode($frameData)); + $status = 1; + break; + case 1: + $frameData = array( + 'data' => array( + 'status' => 1, + 'format' => 'audio/L16;rate=16000', + 'audio' => base64_encode($len), + 'encoding' => $encoding + ) + ); + $client->send(json_encode($frameData)); + break; + case 2: + $frameData = array( + 'data' => array( + 'status' => 2, + 'format' => 'audio/L16;rate=16000', + 'audio' => base64_encode($len), + 'encoding' => $encoding + ) + ); + $client->send(json_encode($frameData)); + break 2; + } + //模拟音频采样间隔 + usleep($intervel); + } + while (true) { + $response = $client->receive(); + if ($response === null) { break; - case 2: - $frameData = array( - 'data' => array( - 'status' => 2, - 'format' => 'audio/L16;rate=16000', - 'audio' => base64_encode($len), - 'encoding' => 'raw' - ) - ); - $client->send(json_encode($frameData)); - break 2; + } + $resp = json_decode($response, true); + $code = $resp['code']; + if ($code != 0) { + break; + } + $message = $resp['message']; + $data = $resp['data']; + $result = $data['result']; + $status = $data['status']; + foreach($result['ws'] as $v) { + $words .= $v['cw'][0]['w'] ?? ''; + } + if ($status === 2) { + break; + } } - //模拟音频采样间隔 - usleep($intervel); + } else { + // 删除临时音频文件 + if (file_exists($file)) { + //unlink($file); + } + return $this->fail('无法连接到 WebSocket 服务器'); } - while (true) { - $response = $client->receive(); - if ($response === null) { - break; - } - $resp = json_decode($response,true); - $code = $resp['code']; - $message = $resp['message']; - $data = $resp['data']; - $result = $data['result']; - $status = $data['status']; - foreach($result['ws'] as $v) { - $words .= $v['cw'][0]['w'] ?? ''; - } - if ($code != 0 || $status === 2) { - break; - } - } - } else { + fclose($audioFile); // 删除临时音频文件 if (file_exists($file)) { //unlink($file); } - return $this->fail('无法连接到 WebSocket 服务器'); - } - fclose($audioFile); - // 删除临时音频文件 - if (file_exists($file)) { - //unlink($file); + } catch (\Exception $e) { + if (file_exists($file)) { + // 删除临时文件 + unlink($file); + } + return $this->fail($e->getMessage()); } return $this->data(['words' => $words]); } @@ -268,6 +296,7 @@ class XunFeiController extends BaseApiController try { $tts = new TtsClient($this->app_id, $this->api_key, $this->api_secret, $business); file_put_contents($audioFile, $tts->request($text)->getBody()->getContents()); + //生成语音文件需要定时清理 } catch (\Exception $e) { return $this->fail($e->getMessage()); } @@ -295,7 +324,7 @@ class XunFeiController extends BaseApiController $ext = pathinfo($file, PATHINFO_EXTENSION); $base64_image = base64_encode(file_get_contents($file)); $ocr = new OcrClient($this->app_id, $this->api_key, $this->api_secret); - $authorization = $ocr->assembleAuthorization($ocrHostUrl); + $ocrHostUrl = $ocr->assembleAuthUrl($ocrHostUrl); $requestBody = [ 'header' => [ 'app_id' => $this->app_id, @@ -319,7 +348,6 @@ class XunFeiController extends BaseApiController ] ] ]; - $ocrHostUrl .= '?authorization=' . $authorization; $responseData = ''; try { $client = new GzClient(['timeout' => 2]); @@ -339,7 +367,6 @@ class XunFeiController extends BaseApiController } return $this->fail($e->getMessage()); } - return $this->data(['words' => (string)$responseData]); } diff --git a/extend/IFlytek/Xfyun/Speech/OcrClient.php b/extend/IFlytek/Xfyun/Speech/OcrClient.php new file mode 100644 index 000000000..69bbb8d01 --- /dev/null +++ b/extend/IFlytek/Xfyun/Speech/OcrClient.php @@ -0,0 +1,86 @@ +appId = $appId; + $this->apiKey = $apiKey; + $this->apiSecret = $apiSecret; + $this->uid = $uid; + $this->resId = $resId; + $this->client = new HttpClient([]); + } + + public function assembleAuthUrl($addr, $method='POST') { + $apiKey=$this->apiKey; + $apiSecret=$this->apiSecret; + if ($apiKey == "" && $apiSecret == "") { // 不鉴权 + return $addr; + } + + $ul = parse_url($addr); // 解析地址 + if ($ul === false) { // 地址不对,也不鉴权 + return $addr; + } + // // $date = date(DATE_RFC1123); // 获取当前时间并格式化为RFC1123格式的字符串 + $timestamp = time(); + $rfc1123_format = gmdate("D, d M Y H:i:s \G\M\T", $timestamp); + // $rfc1123_format = "Mon, 31 Jul 2023 08:24:03 GMT"; + // 参与签名的字段 host, date, request-line + $signString = array("host: " . $ul["host"], "date: " . $rfc1123_format, $method . " " . $ul["path"] . " HTTP/1.1"); + // 对签名字符串进行排序,确保顺序一致 + // ksort($signString); + // 将签名字符串拼接成一个字符串 + $sgin = implode("\n", $signString); + // 对签名字符串进行HMAC-SHA256加密,得到签名结果 + $sha = hash_hmac('sha256', $sgin, $apiSecret, true); + + $signature_sha_base64 = base64_encode($sha); + + // 将API密钥、算法、头部信息和签名结果拼接成一个授权URL + $authUrl = "api_key=\"$apiKey\",algorithm=\"hmac-sha256\",headers=\"host date request-line\",signature=\"$signature_sha_base64\""; + // 对授权URL进行Base64编码,并添加到原始地址后面作为查询参数 + $authAddr = $addr . '?' . http_build_query(array( + 'host' => $ul['host'], + 'date' => $rfc1123_format, + 'authorization' => base64_encode($authUrl), + )); + return $authAddr; + } + +}