5) { echo json_encode([ 'success' => false, 'error' => '접근 권한이 없습니다.' ]); exit; } // POST 요청만 허용 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode([ 'success' => false, 'error' => 'POST 요청만 허용됩니다.' ]); exit; } // 오디오 파일 확인 if (!isset($_FILES['audio']) || $_FILES['audio']['error'] !== UPLOAD_ERR_OK) { echo json_encode([ 'success' => false, 'error' => '오디오 파일이 전송되지 않았습니다.' ]); exit; } $audioFile = $_FILES['audio']; $audioPath = $audioFile['tmp_name']; $audioType = $audioFile['type']; // 오디오 파일 유효성 검사 $allowedTypes = ['audio/webm', 'audio/wav', 'audio/ogg', 'audio/mp3', 'audio/mpeg', 'audio/x-flac']; if (!in_array($audioType, $allowedTypes)) { echo json_encode([ 'success' => false, 'error' => '지원하지 않는 오디오 형식입니다. (webm, wav, ogg, mp3, flac 지원)' ]); exit; } // Google Cloud Speech-to-Text API 설정 $googleApiKey = ''; $accessToken = null; // 서비스 계정용 OAuth 토큰 $useServiceAccount = false; $googleApiUrl = 'https://speech.googleapis.com/v1/speech:recognize'; // API 키 파일 경로 (우선순위 순) // 상대 경로와 절대 경로 모두 확인 $docRoot = $_SERVER['DOCUMENT_ROOT']; $apiKeyPaths = [ $docRoot . '/5130/apikey/google_api.txt', $docRoot . '/apikey/google_api.txt', dirname($docRoot) . '/5130/apikey/google_api.txt', // 상위 디렉토리에서 확인 dirname($docRoot) . '/apikey/google_api.txt', __DIR__ . '/../../apikey/google_api.txt', // 현재 파일 기준 상대 경로 __DIR__ . '/../../../apikey/google_api.txt', $docRoot . '/5130/config/google_speech_api.php', $docRoot . '/config/google_speech_api.php', ]; // 서비스 계정 파일 경로 $serviceAccountPaths = [ $docRoot . '/5130/apikey/google_service_account.json', $docRoot . '/apikey/google_service_account.json', dirname($docRoot) . '/5130/apikey/google_service_account.json', dirname($docRoot) . '/apikey/google_service_account.json', __DIR__ . '/../../apikey/google_service_account.json', __DIR__ . '/../../../apikey/google_service_account.json', ]; // API 키 파일에서 읽기 $foundKeyPath = null; $checkedPaths = []; foreach ($apiKeyPaths as $keyPath) { $checkedPaths[] = $keyPath . ' (exists: ' . (file_exists($keyPath) ? 'yes' : 'no') . ')'; if (file_exists($keyPath)) { $foundKeyPath = $keyPath; if (pathinfo($keyPath, PATHINFO_EXTENSION) === 'php') { // PHP 설정 파일인 경우 require_once($keyPath); if (isset($GOOGLE_SPEECH_API_KEY)) { $googleApiKey = trim($GOOGLE_SPEECH_API_KEY); break; } } else { // 텍스트 파일인 경우 (google_api.txt) $keyContent = file_get_contents($keyPath); $googleApiKey = trim($keyContent); // 빈 줄이나 주석 제거 $lines = explode("\n", $googleApiKey); $googleApiKey = ''; foreach ($lines as $line) { $line = trim($line); if (!empty($line) && !preg_match('/^#/', $line)) { $googleApiKey = $line; break; } } if (!empty($googleApiKey)) { break; } } } } // 서비스 계정 파일 찾기 $serviceAccountPath = null; foreach ($serviceAccountPaths as $saPath) { if (file_exists($saPath)) { $serviceAccountPath = $saPath; break; } } // OAuth 2.0 토큰 생성 함수 (서비스 계정용) function getServiceAccountToken($serviceAccountPath) { if (!file_exists($serviceAccountPath)) { return null; } $serviceAccount = json_decode(file_get_contents($serviceAccountPath), true); if (!$serviceAccount || !isset($serviceAccount['private_key']) || !isset($serviceAccount['client_email'])) { return null; } // Base64 URL 인코딩 함수 $base64UrlEncode = function($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }; // JWT 생성 $now = time(); // JWT 헤더 $jwtHeader = [ 'alg' => 'RS256', 'typ' => 'JWT' ]; // JWT 클레임 $jwtClaim = [ 'iss' => $serviceAccount['client_email'], 'scope' => 'https://www.googleapis.com/auth/cloud-platform', 'aud' => 'https://oauth2.googleapis.com/token', 'exp' => $now + 3600, 'iat' => $now ]; $encodedHeader = $base64UrlEncode(json_encode($jwtHeader)); $encodedClaim = $base64UrlEncode(json_encode($jwtClaim)); // 서명 생성 $privateKey = openssl_pkey_get_private($serviceAccount['private_key']); if (!$privateKey) { return null; } $signature = ''; $signData = $encodedHeader . '.' . $encodedClaim; if (!openssl_sign($signData, $signature, $privateKey, OPENSSL_ALGO_SHA256)) { openssl_free_key($privateKey); return null; } openssl_free_key($privateKey); $encodedSignature = $base64UrlEncode($signature); $jwt = $encodedHeader . '.' . $encodedClaim . '.' . $encodedSignature; // OAuth 2.0 토큰 요청 $tokenCh = curl_init('https://oauth2.googleapis.com/token'); curl_setopt($tokenCh, CURLOPT_POST, true); curl_setopt($tokenCh, CURLOPT_POSTFIELDS, http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt ])); curl_setopt($tokenCh, CURLOPT_RETURNTRANSFER, true); curl_setopt($tokenCh, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']); $tokenResponse = curl_exec($tokenCh); $tokenHttpCode = curl_getinfo($tokenCh, CURLINFO_HTTP_CODE); curl_close($tokenCh); if ($tokenHttpCode === 200) { $tokenData = json_decode($tokenResponse, true); if (isset($tokenData['access_token'])) { return $tokenData['access_token']; } } return null; } // API 키가 없거나 형식이 잘못된 경우 서비스 계정 시도 if (empty($googleApiKey) || strlen($googleApiKey) < 20) { if ($serviceAccountPath) { $accessToken = getServiceAccountToken($serviceAccountPath); if ($accessToken) { $useServiceAccount = true; } } if (!$useServiceAccount) { echo json_encode([ 'success' => false, 'error' => 'Google Cloud Speech-to-Text API 인증 정보가 없습니다.', 'hint' => 'API 키 파일 또는 서비스 계정 파일을 찾을 수 없습니다.', 'checked_paths' => $checkedPaths, 'service_account_path' => $serviceAccountPath, 'document_root' => $docRoot, 'current_dir' => __DIR__ ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } } // API 키 형식 확인 (Google API 키는 보통 AIza로 시작) $isValidFormat = (strpos($googleApiKey, 'AIza') === 0 || strlen($googleApiKey) >= 35); if (!$isValidFormat && !$useServiceAccount) { // API 키 형식이 잘못된 경우 서비스 계정 시도 if ($serviceAccountPath) { $accessToken = getServiceAccountToken($serviceAccountPath); if ($accessToken) { $useServiceAccount = true; $googleApiKey = ''; // API 키 사용 안 함 } } } // 서비스 계정이 있으면 우선 사용 (API 키보다 안정적) // API 키는 간헐적으로 오류가 발생할 수 있으므로 서비스 계정을 기본으로 사용 if (!$useServiceAccount && $serviceAccountPath && file_exists($serviceAccountPath)) { $accessToken = getServiceAccountToken($serviceAccountPath); if ($accessToken) { $useServiceAccount = true; // API 키는 백업으로 유지하되 서비스 계정 우선 사용 error_log('Using service account for authentication (more reliable than API key)'); } } // API 키 디버깅 정보 $debugInfo = [ 'api_key_path' => $foundKeyPath, 'api_key_length' => strlen($googleApiKey), 'api_key_prefix' => !empty($googleApiKey) ? substr($googleApiKey, 0, 10) . '...' : null, 'api_key_suffix' => !empty($googleApiKey) ? '...' . substr($googleApiKey, -5) : null, 'use_service_account' => $useServiceAccount, 'service_account_path' => $serviceAccountPath, 'document_root' => $docRoot, 'checked_paths' => $checkedPaths ]; try { // 오디오 파일을 base64로 인코딩 $audioContent = file_get_contents($audioPath); $audioBase64 = base64_encode($audioContent); // 오디오 형식에 따른 encoding 설정 $encoding = 'WEBM_OPUS'; $sampleRate = 16000; // 기본값 if (strpos($audioType, 'wav') !== false) { $encoding = 'LINEAR16'; $sampleRate = 16000; } elseif (strpos($audioType, 'ogg') !== false) { $encoding = 'OGG_OPUS'; $sampleRate = 48000; // Opus는 보통 48kHz } elseif (strpos($audioType, 'flac') !== false) { $encoding = 'FLAC'; $sampleRate = 16000; } elseif (strpos($audioType, 'webm') !== false) { $encoding = 'WEBM_OPUS'; $sampleRate = 48000; // WebM Opus는 보통 48kHz } // Google Cloud Speech-to-Text API 요청 데이터 $requestData = [ 'config' => [ 'encoding' => $encoding, 'sampleRateHertz' => $sampleRate, 'languageCode' => 'ko-KR', 'enableAutomaticPunctuation' => true, 'enableWordTimeOffsets' => false, 'model' => 'latest_long', // 긴 오디오에 적합 'alternativeLanguageCodes' => ['ko'], // 추가 언어 코드 ], 'audio' => [ 'content' => $audioBase64 ] ]; // 디버깅 정보 (개발 환경에서만) if (isset($_GET['debug'])) { error_log('Google Speech API Request: ' . json_encode([ 'encoding' => $encoding, 'sampleRate' => $sampleRate, 'audioSize' => strlen($audioBase64), 'audioType' => $audioType ])); } // cURL로 API 호출 $headers = ['Content-Type: application/json']; $apiUrl = $googleApiUrl; if ($useServiceAccount && $accessToken) { // 서비스 계정 사용: Bearer 토큰 사용 $headers[] = 'Authorization: Bearer ' . $accessToken; } else if (!empty($googleApiKey)) { // API 키 사용: 쿼리 파라미터로 전달 $apiUrl = $googleApiUrl . '?key=' . urlencode($googleApiKey); } else { throw new Exception('인증 정보가 없습니다. API 키 또는 서비스 계정이 필요합니다.'); } $ch = curl_init($apiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { throw new Exception('cURL 오류: ' . $curlError); } if ($httpCode !== 200) { $errorData = json_decode($response, true); $errorMessage = 'API 오류 (HTTP ' . $httpCode . ')'; $debugInfo = []; if (isset($errorData['error'])) { $errorMessage .= ': ' . ($errorData['error']['message'] ?? '알 수 없는 오류'); $debugInfo['error_details'] = $errorData['error']; // API 키 오류인 경우 서비스 계정으로 재시도 if (isset($errorData['error']['message']) && (stripos($errorData['error']['message'], 'API key') !== false || stripos($errorData['error']['message'], 'not valid') !== false || stripos($errorData['error']['message'], 'invalid') !== false || stripos($errorData['error']['message'], 'unauthorized') !== false)) { // 서비스 계정으로 재시도 if ($serviceAccountPath && file_exists($serviceAccountPath)) { // 서비스 계정을 사용 중이었는데도 오류가 발생하면 토큰 재생성 if ($useServiceAccount) { error_log('Service account token may have expired, regenerating...'); } $accessToken = getServiceAccountToken($serviceAccountPath); if ($accessToken) { // 서비스 계정으로 재시도 $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $accessToken ]; $ch = curl_init($googleApiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200) { // 서비스 계정으로 성공 $result = json_decode($response, true); if (isset($result['results']) && count($result['results']) > 0) { $transcript = ''; foreach ($result['results'] as $resultItem) { if (isset($resultItem['alternatives'][0]['transcript'])) { $transcript .= $resultItem['alternatives'][0]['transcript'] . ' '; } } $transcript = trim($transcript); echo json_encode([ 'success' => true, 'transcript' => $transcript, 'confidence' => isset($result['results'][0]['alternatives'][0]['confidence']) ? $result['results'][0]['alternatives'][0]['confidence'] : null, 'auth_method' => 'service_account', 'retry_success' => true ]); exit; } } } } // 재시도 실패 시 상세 안내 $errorMessage .= "\n\n⚠️ API 키 오류 해결 방법:\n"; $errorMessage .= "1. Google Cloud Console (https://console.cloud.google.com/) 접속\n"; $errorMessage .= "2. 프로젝트 선택: codebridge-chatbot\n"; $errorMessage .= "3. 'API 및 서비스' > '라이브러리'에서 'Cloud Speech-to-Text API' 검색 및 활성화\n"; $errorMessage .= "4. 'API 및 서비스' > '사용자 인증 정보'에서 API 키 확인\n"; $errorMessage .= "5. API 키 제한 설정에서 'Cloud Speech-to-Text API' 허용 확인\n"; $errorMessage .= "\n현재 API 키 파일: " . ($foundKeyPath ?? 'N/A'); $errorMessage .= "\nAPI 키 길이: " . strlen($googleApiKey) . " 문자"; if ($serviceAccountPath) { $errorMessage .= "\n서비스 계정 파일: " . $serviceAccountPath; } } } else { $errorMessage .= ': ' . substr($response, 0, 200); } // 디버그 정보에 API 키 정보 추가 $debugInfo['api_key_info'] = [ 'path' => $foundKeyPath, 'length' => strlen($googleApiKey), 'prefix' => !empty($googleApiKey) ? substr($googleApiKey, 0, 10) . '...' : null, 'suffix' => !empty($googleApiKey) ? '...' . substr($googleApiKey, -5) : null, 'format_valid' => !empty($googleApiKey) ? (strpos($googleApiKey, 'AIza') === 0 || strlen($googleApiKey) >= 35) : false, 'use_service_account' => $useServiceAccount, 'service_account_path' => $serviceAccountPath ]; if (isset($_GET['debug'])) { $debugInfo['full_response'] = $response; $debugInfo['request_url'] = $useServiceAccount ? $googleApiUrl : ($googleApiUrl . '?key=' . substr($googleApiKey, 0, 10) . '...'); } throw new Exception($errorMessage); } $result = json_decode($response, true); // 결과 처리 if (isset($result['results']) && count($result['results']) > 0) { $transcript = ''; foreach ($result['results'] as $resultItem) { if (isset($resultItem['alternatives'][0]['transcript'])) { $transcript .= $resultItem['alternatives'][0]['transcript'] . ' '; } } $transcript = trim($transcript); echo json_encode([ 'success' => true, 'transcript' => $transcript, 'confidence' => isset($result['results'][0]['alternatives'][0]['confidence']) ? $result['results'][0]['alternatives'][0]['confidence'] : null, 'auth_method' => $useServiceAccount ? 'service_account' : 'api_key' ]); } else { echo json_encode([ 'success' => false, 'error' => '음성을 인식할 수 없었습니다.', 'debug' => $result ]); } } catch (Exception $e) { $errorResponse = [ 'success' => false, 'error' => $e->getMessage() ]; // 항상 API 키 정보 포함 (보안을 위해 일부만 표시) $errorResponse['debug'] = [ 'api_key_path' => $foundKeyPath ?? null, 'api_key_length' => isset($googleApiKey) ? strlen($googleApiKey) : 0, 'api_key_prefix' => isset($googleApiKey) ? substr($googleApiKey, 0, 10) . '...' : null, 'api_key_suffix' => isset($googleApiKey) ? '...' . substr($googleApiKey, -5) : null, 'api_key_format' => isset($googleApiKey) ? (strpos($googleApiKey, 'AIza') === 0 ? 'Google API Key (AIza...)' : 'Other format') : 'N/A', 'use_service_account' => isset($useServiceAccount) ? $useServiceAccount : false, 'service_account_path' => isset($serviceAccountPath) ? $serviceAccountPath : null, 'document_root' => $docRoot ?? null, 'checked_paths_count' => count($checkedPaths ?? []) ]; // 상세 디버그 모드일 때 추가 정보 if (isset($_GET['debug'])) { $errorResponse['debug']['exception_class'] = get_class($e); $errorResponse['debug']['file'] = $e->getFile(); $errorResponse['debug']['line'] = $e->getLine(); $errorResponse['debug']['checked_paths'] = $checkedPaths ?? []; } echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); }