Voice AI 모바일 중복 현상 수정
This commit is contained in:
527
voice_ai/api/speech_to_text.php
Normal file
527
voice_ai/api/speech_to_text.php
Normal file
@@ -0,0 +1,527 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Cloud Speech-to-Text API 엔드포인트
|
||||
* 오디오 파일을 받아서 텍스트로 변환
|
||||
*/
|
||||
|
||||
// 타임아웃 설정 (504 오류 방지)
|
||||
ini_set('max_execution_time', 120); // 120초
|
||||
set_time_limit(120);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||||
|
||||
// 권한 체크
|
||||
if ($level > 5) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => '접근 권한이 없습니다.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 세션 쓰기 닫기 (이후 긴 작업 시 세션 잠금 방지)
|
||||
session_write_close();
|
||||
|
||||
// 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']);
|
||||
// 타임아웃 설정
|
||||
curl_setopt($tokenCh, CURLOPT_TIMEOUT, 30); // 30초
|
||||
curl_setopt($tokenCh, CURLOPT_CONNECTTIMEOUT, 10); // 연결 타임아웃 10초
|
||||
|
||||
$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);
|
||||
// 타임아웃 설정 (504 오류 방지)
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 60초
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 연결 타임아웃 10초
|
||||
|
||||
$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);
|
||||
// 타임아웃 설정 (504 오류 방지)
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 60초
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 연결 타임아웃 10초
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
370
voice_ai/check_meeting_status.php
Normal file
370
voice_ai/check_meeting_status.php
Normal file
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
// 출력 버퍼링 시작
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
ob_start();
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
|
||||
|
||||
// 에러 응답 함수
|
||||
function sendErrorResponse($message, $details = null) {
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$response = ['ok' => false, 'error' => $message];
|
||||
if ($details !== null) {
|
||||
$response['details'] = $details;
|
||||
}
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 출력 버퍼 비우기
|
||||
ob_clean();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
// 1. 권한 체크
|
||||
if (!isset($user_id) || $level > 5) {
|
||||
sendErrorResponse('접근 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 2. 파라미터 확인
|
||||
$meeting_id = isset($_GET['meeting_id']) ? (int)$_GET['meeting_id'] : 0;
|
||||
$operation_name = isset($_GET['operation_name']) ? trim($_GET['operation_name']) : '';
|
||||
|
||||
if (!$meeting_id || !$operation_name) {
|
||||
sendErrorResponse('필수 파라미터가 없습니다.', 'meeting_id와 operation_name이 필요합니다.');
|
||||
}
|
||||
|
||||
// 3. Google API 인증 정보 가져오기
|
||||
$googleServiceAccountFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/google_service_account.json';
|
||||
$googleApiKeyFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/google_api.txt';
|
||||
|
||||
$accessToken = null;
|
||||
$googleApiKey = null;
|
||||
|
||||
// 서비스 계정 우선 사용
|
||||
if (file_exists($googleServiceAccountFile)) {
|
||||
$serviceAccount = json_decode(file_get_contents($googleServiceAccountFile), true);
|
||||
if ($serviceAccount) {
|
||||
// OAuth 2.0 토큰 생성
|
||||
$now = time();
|
||||
$jwtHeader = ['alg' => 'RS256', 'typ' => '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
|
||||
];
|
||||
|
||||
$base64UrlEncode = function($data) {
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
};
|
||||
|
||||
$encodedHeader = $base64UrlEncode(json_encode($jwtHeader));
|
||||
$encodedClaim = $base64UrlEncode(json_encode($jwtClaim));
|
||||
|
||||
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
|
||||
if ($privateKey) {
|
||||
$signature = '';
|
||||
$signData = $encodedHeader . '.' . $encodedClaim;
|
||||
if (openssl_sign($signData, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
|
||||
openssl_free_key($privateKey);
|
||||
$encodedSignature = $base64UrlEncode($signature);
|
||||
$jwt = $encodedHeader . '.' . $encodedClaim . '.' . $encodedSignature;
|
||||
|
||||
// OAuth 토큰 요청
|
||||
$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);
|
||||
$tokenCode = curl_getinfo($tokenCh, CURLINFO_HTTP_CODE);
|
||||
curl_close($tokenCh);
|
||||
|
||||
if ($tokenCode === 200) {
|
||||
$tokenData = json_decode($tokenResponse, true);
|
||||
if (isset($tokenData['access_token'])) {
|
||||
$accessToken = $tokenData['access_token'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API 키 사용
|
||||
if (!$accessToken && file_exists($googleApiKeyFile)) {
|
||||
$googleApiKey = trim(file_get_contents($googleApiKeyFile));
|
||||
}
|
||||
|
||||
if (!$accessToken && !$googleApiKey) {
|
||||
sendErrorResponse('Google API 인증 정보가 없습니다.');
|
||||
}
|
||||
|
||||
// 4. Google API에서 작업 상태 확인
|
||||
if ($accessToken) {
|
||||
$poll_url = 'https://speech.googleapis.com/v1/operations/' . urlencode($operation_name);
|
||||
$poll_headers = ['Authorization: Bearer ' . $accessToken];
|
||||
} else {
|
||||
$poll_url = 'https://speech.googleapis.com/v1/operations/' . urlencode($operation_name) . '?key=' . urlencode($googleApiKey);
|
||||
$poll_headers = [];
|
||||
}
|
||||
|
||||
$poll_ch = curl_init($poll_url);
|
||||
curl_setopt($poll_ch, CURLOPT_RETURNTRANSFER, true);
|
||||
if (!empty($poll_headers)) {
|
||||
curl_setopt($poll_ch, CURLOPT_HTTPHEADER, $poll_headers);
|
||||
}
|
||||
curl_setopt($poll_ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$poll_response = curl_exec($poll_ch);
|
||||
$poll_code = curl_getinfo($poll_ch, CURLINFO_HTTP_CODE);
|
||||
$poll_error = curl_error($poll_ch);
|
||||
curl_close($poll_ch);
|
||||
|
||||
if ($poll_code !== 200) {
|
||||
sendErrorResponse('작업 상태 확인 실패 (HTTP ' . $poll_code . ')', $poll_error ?: substr($poll_response, 0, 500));
|
||||
}
|
||||
|
||||
$poll_data = json_decode($poll_response, true);
|
||||
if (!$poll_data) {
|
||||
sendErrorResponse('작업 상태 응답 파싱 실패', substr($poll_response, 0, 500));
|
||||
}
|
||||
|
||||
// 5. 작업 완료 여부 확인
|
||||
if (!isset($poll_data['done']) || $poll_data['done'] !== true) {
|
||||
// 아직 처리 중
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'processing' => true,
|
||||
'done' => false,
|
||||
'message' => '음성 인식 처리 중입니다. 잠시 후 다시 확인해주세요.'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 6. 작업 완료 - 결과 처리
|
||||
if (isset($poll_data['error'])) {
|
||||
// 오류 발생
|
||||
$pdo = db_connect();
|
||||
$errorMsg = isset($poll_data['error']['message']) ? $poll_data['error']['message'] : '알 수 없는 오류';
|
||||
|
||||
$updateSql = "UPDATE meeting_logs SET
|
||||
title = ?,
|
||||
summary_text = ?
|
||||
WHERE id = ?";
|
||||
$updateStmt = $pdo->prepare($updateSql);
|
||||
$updateStmt->execute(['오류 발생', 'Google STT 변환 실패: ' . $errorMsg, $meeting_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Google STT 변환 실패',
|
||||
'details' => $errorMsg
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 응답 구조 확인 및 로깅
|
||||
error_log('Google STT 응답 구조: ' . json_encode([
|
||||
'has_response' => isset($poll_data['response']),
|
||||
'has_results' => isset($poll_data['response']['results']),
|
||||
'results_count' => isset($poll_data['response']['results']) ? count($poll_data['response']['results']) : 0,
|
||||
'response_keys' => isset($poll_data['response']) ? array_keys($poll_data['response']) : [],
|
||||
'poll_data_keys' => array_keys($poll_data)
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// response 필드가 없는 경우
|
||||
if (!isset($poll_data['response'])) {
|
||||
error_log('Google STT 응답에 response 필드가 없습니다. 전체 응답: ' . json_encode($poll_data, JSON_UNESCAPED_UNICODE));
|
||||
sendErrorResponse('Google STT 응답 구조 오류', '응답에 response 필드가 없습니다. 작업이 완료되었지만 결과를 가져올 수 없습니다.');
|
||||
}
|
||||
|
||||
// results가 없는 경우
|
||||
if (!isset($poll_data['response']['results'])) {
|
||||
error_log('Google STT 응답에 results 필드가 없습니다. response 내용: ' . json_encode($poll_data['response'], JSON_UNESCAPED_UNICODE));
|
||||
sendErrorResponse('Google STT 응답에 결과가 없습니다.', '응답에 results 필드가 없습니다. 음성이 인식되지 않았을 수 있습니다.');
|
||||
}
|
||||
|
||||
// results가 비어있는 경우
|
||||
if (empty($poll_data['response']['results'])) {
|
||||
error_log('Google STT 응답에 results가 비어있습니다. response 내용: ' . json_encode($poll_data['response'], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// 빈 결과를 DB에 저장하고 사용자에게 알림
|
||||
$pdo = db_connect();
|
||||
$updateSql = "UPDATE meeting_logs SET
|
||||
title = ?,
|
||||
transcript_text = ?,
|
||||
summary_text = ?
|
||||
WHERE id = ?";
|
||||
$updateStmt = $pdo->prepare($updateSql);
|
||||
$updateStmt->execute(['음성 인식 실패', '음성이 인식되지 않았습니다.', '녹음된 오디오에서 음성을 인식할 수 없었습니다. 마이크 권한과 오디오 품질을 확인해주세요.', $meeting_id]);
|
||||
|
||||
sendErrorResponse('음성 인식 실패', '녹음된 오디오에서 음성을 인식할 수 없었습니다. 마이크 권한과 오디오 품질을 확인해주세요.');
|
||||
}
|
||||
|
||||
// 7. 텍스트 변환
|
||||
$stt_data = ['results' => $poll_data['response']['results']];
|
||||
$transcript = '';
|
||||
foreach ($stt_data['results'] as $result) {
|
||||
if (isset($result['alternatives'][0]['transcript'])) {
|
||||
$transcript .= $result['alternatives'][0]['transcript'] . ' ';
|
||||
}
|
||||
}
|
||||
$transcript = trim($transcript);
|
||||
|
||||
if (empty($transcript)) {
|
||||
sendErrorResponse('인식된 텍스트가 없습니다.');
|
||||
}
|
||||
|
||||
// 8. Claude API로 요약 생성
|
||||
$claudeKeyFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/claude_api.txt';
|
||||
$claudeKey = '';
|
||||
if (file_exists($claudeKeyFile)) {
|
||||
$claudeKey = trim(file_get_contents($claudeKeyFile));
|
||||
}
|
||||
|
||||
$title = '무제 회의록';
|
||||
$summary = '';
|
||||
|
||||
if (!empty($claudeKey)) {
|
||||
$prompt = "다음 회의 녹취록을 분석하여 JSON 형식으로 응답해주세요.
|
||||
|
||||
요구사항:
|
||||
1. \"title\": 회의 내용을 요약한 제목 (최대 20자, 한글 기준)
|
||||
2. \"summary\": [회의 개요 / 주요 안건 / 결정 사항 / 향후 계획] 형식의 상세 요약
|
||||
|
||||
반드시 다음 JSON 형식으로만 응답해주세요:
|
||||
{
|
||||
\"title\": \"제목 (최대 20자)\",
|
||||
\"summary\": \"상세 요약 내용\"
|
||||
}
|
||||
|
||||
회의 녹취록:
|
||||
" . $transcript;
|
||||
|
||||
$ch2 = curl_init('https://api.anthropic.com/v1/messages');
|
||||
$requestBody = [
|
||||
'model' => 'claude-3-5-haiku-20241022',
|
||||
'max_tokens' => 2048,
|
||||
'messages' => [['role' => 'user', 'content' => $prompt]]
|
||||
];
|
||||
|
||||
curl_setopt($ch2, CURLOPT_POST, true);
|
||||
curl_setopt($ch2, CURLOPT_HTTPHEADER, [
|
||||
'x-api-key: ' . $claudeKey,
|
||||
'anthropic-version: 2023-06-01',
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch2, CURLOPT_POSTFIELDS, json_encode($requestBody));
|
||||
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$ai_response = curl_exec($ch2);
|
||||
$ai_code = curl_getinfo($ch2, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch2);
|
||||
|
||||
if ($ai_code === 200) {
|
||||
$ai_data = json_decode($ai_response, true);
|
||||
if (isset($ai_data['content'][0]['text'])) {
|
||||
$ai_text = trim($ai_data['content'][0]['text']);
|
||||
$ai_text = preg_replace('/^```(?:json)?\s*/m', '', $ai_text);
|
||||
$ai_text = preg_replace('/\s*```$/m', '', $ai_text);
|
||||
$ai_text = trim($ai_text);
|
||||
|
||||
$parsed_data = json_decode($ai_text, true);
|
||||
|
||||
if (is_array($parsed_data)) {
|
||||
if (isset($parsed_data['title']) && !empty($parsed_data['title'])) {
|
||||
$title = mb_substr(trim($parsed_data['title']), 0, 20, 'UTF-8');
|
||||
}
|
||||
if (isset($parsed_data['summary']) && !empty($parsed_data['summary'])) {
|
||||
$summary = $parsed_data['summary'];
|
||||
} else {
|
||||
$summary = $ai_text;
|
||||
}
|
||||
} else {
|
||||
$summary = $ai_text;
|
||||
if (preg_match('/["\']?title["\']?\s*[:=]\s*["\']([^"\']{1,20})["\']/', $ai_text, $matches)) {
|
||||
$title = mb_substr(trim($matches[1]), 0, 20, 'UTF-8');
|
||||
} elseif (preg_match('/제목[:\s]+([^\n]{1,20})/', $ai_text, $matches)) {
|
||||
$title = mb_substr(trim($matches[1]), 0, 20, 'UTF-8');
|
||||
} else {
|
||||
$lines = explode("\n", $ai_text);
|
||||
$first_line = trim($lines[0]);
|
||||
if (!empty($first_line) && mb_strlen($first_line, 'UTF-8') <= 20) {
|
||||
$title = $first_line;
|
||||
} elseif (!empty($first_line)) {
|
||||
$title = mb_substr($first_line, 0, 20, 'UTF-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($title) || $title === '무제 회의록') {
|
||||
if (!empty($summary)) {
|
||||
$summary_lines = explode("\n", $summary);
|
||||
$first_summary_line = trim($summary_lines[0]);
|
||||
if (!empty($first_summary_line)) {
|
||||
$first_summary_line = preg_replace('/[\[\(].*?[\]\)]/', '', $first_summary_line);
|
||||
$first_summary_line = trim($first_summary_line);
|
||||
if (mb_strlen($first_summary_line, 'UTF-8') <= 20) {
|
||||
$title = $first_summary_line;
|
||||
} else {
|
||||
$title = mb_substr($first_summary_line, 0, 20, 'UTF-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$title = mb_substr(trim($title), 0, 20, 'UTF-8');
|
||||
if (empty($title)) {
|
||||
$title = '무제 회의록';
|
||||
}
|
||||
|
||||
if (empty($summary)) {
|
||||
$summary = "Claude API 키가 설정되지 않아 요약을 생성할 수 없습니다. (원문 저장됨)";
|
||||
}
|
||||
|
||||
// 9. DB 업데이트
|
||||
$pdo = db_connect();
|
||||
$updateSql = "UPDATE meeting_logs SET
|
||||
title = ?,
|
||||
transcript_text = ?,
|
||||
summary_text = ?
|
||||
WHERE id = ?";
|
||||
$updateStmt = $pdo->prepare($updateSql);
|
||||
$updateStmt->execute([$title, $transcript, $summary, $meeting_id]);
|
||||
|
||||
// 10. 완료 응답
|
||||
@ob_clean();
|
||||
@ob_end_clean();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'processing' => false,
|
||||
'done' => true,
|
||||
'meeting_id' => $meeting_id,
|
||||
'title' => $title,
|
||||
'transcript' => $transcript,
|
||||
'summary' => $summary,
|
||||
'message' => '음성 인식 및 요약이 완료되었습니다.'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
?>
|
||||
|
||||
114
voice_ai/fix_meeting_db.php
Normal file
114
voice_ai/fix_meeting_db.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
// fix_meeting_db.php
|
||||
// meeting_logs 테이블의 id 컬럼에 AUTO_INCREMENT 속성을 추가하고
|
||||
// 중복된 id=0 레코드를 수정하는 스크립트
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// DB 연결 설정
|
||||
$host = '127.0.0.1';
|
||||
$dbname = 'checker'; // .env를 로드하지 못할 경우를 대비한 기본값
|
||||
$username = 'root';
|
||||
$password = 'root';
|
||||
|
||||
echo "Database Fix Script for meeting_logs\n";
|
||||
echo "=====================================\n";
|
||||
|
||||
// 1. .env 파일 로드 시도
|
||||
$envPath = __DIR__ . '/../.env';
|
||||
if (file_exists($envPath)) {
|
||||
echo "Loading .env file...\n";
|
||||
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, '#') === 0) continue;
|
||||
list($name, $value) = explode('=', $line, 2);
|
||||
$_ENV[trim($name)] = trim($value);
|
||||
}
|
||||
|
||||
if (isset($_ENV['DB_HOST'])) $host = $_ENV['DB_HOST'];
|
||||
if (isset($_ENV['DB_NAME'])) $dbname = $_ENV['DB_NAME'];
|
||||
if (isset($_ENV['DB_USER'])) $username = $_ENV['DB_USER'];
|
||||
if (isset($_ENV['DB_PASSWORD'])) $password = $_ENV['DB_PASSWORD'];
|
||||
} else {
|
||||
echo "Warning: .env file not found. Using default credentials.\n";
|
||||
}
|
||||
|
||||
// 2. DB 연결
|
||||
try {
|
||||
echo "Connecting to database ($host, $dbname)...\n";
|
||||
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
echo "Connected successfully.\n";
|
||||
|
||||
// 3. 중복된 ID 확인 (id=0)
|
||||
echo "Checking for duplicate id=0 in meeting_logs...\n";
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM meeting_logs WHERE id = 0");
|
||||
$count = $stmt->fetchColumn();
|
||||
echo "Found $count records with id=0.\n";
|
||||
|
||||
if ($count > 0) {
|
||||
// 4. MAX(id) 확인
|
||||
$stmt = $pdo->query("SELECT MAX(id) FROM meeting_logs");
|
||||
$maxId = $stmt->fetchColumn();
|
||||
if (!$maxId) $maxId = 0;
|
||||
echo "Current MAX(id) is $maxId.\n";
|
||||
|
||||
// 5. id=0인 레코드들 업데이트
|
||||
$stmt = $pdo->query("SELECT * FROM meeting_logs WHERE id = 0");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$newId = $maxId + 1;
|
||||
foreach ($rows as $row) {
|
||||
// 중복된 id=0 레코드를 삭제하고 새로운 ID로 다시 삽입하는 것이 안전함
|
||||
// (PRIMARY KEY 제약 조건 때문에 UPDATE가 실패할 수 있음)
|
||||
|
||||
// 데이터 복사
|
||||
$insertSql = "INSERT INTO meeting_logs (tenant_id, user_id, title, audio_file_path, transcript_text, summary_text, created_at, file_expiry_date)
|
||||
VALUES (:tenant_id, :user_id, :title, :audio_file_path, :transcript_text, :summary_text, :created_at, :file_expiry_date)";
|
||||
|
||||
$insertStmt = $pdo->prepare($insertSql);
|
||||
$insertStmt->execute([
|
||||
':tenant_id' => $row['tenant_id'],
|
||||
':user_id' => $row['user_id'],
|
||||
':title' => $row['title'],
|
||||
':audio_file_path' => $row['audio_file_path'],
|
||||
':transcript_text' => $row['transcript_text'],
|
||||
':summary_text' => $row['summary_text'],
|
||||
':created_at' => $row['created_at'],
|
||||
':file_expiry_date' => $row['file_expiry_date']
|
||||
]);
|
||||
|
||||
echo "Moved record (old id=0) to new id=" . $pdo->lastInsertId() . "\n";
|
||||
}
|
||||
|
||||
// 원래 id=0 레코드 삭제
|
||||
$pdo->exec("DELETE FROM meeting_logs WHERE id = 0");
|
||||
echo "Deleted old records with id=0.\n";
|
||||
}
|
||||
|
||||
// 6. AUTO_INCREMENT 속성 추가
|
||||
echo "Applying AUTO_INCREMENT to meeting_logs.id...\n";
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE meeting_logs MODIFY id INT AUTO_INCREMENT PRIMARY KEY");
|
||||
echo "Success: AUTO_INCREMENT applied.\n";
|
||||
} catch (PDOException $e) {
|
||||
// 이미 설정되어 있거나 다른 문제가 있는 경우
|
||||
echo "Note: " . $e->getMessage() . "\n";
|
||||
|
||||
// PRIMARY KEY가 이미 있다면 MODIFY만 시도
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE meeting_logs MODIFY id INT AUTO_INCREMENT");
|
||||
echo "Success: Mofified column to AUTO_INCREMENT.\n";
|
||||
} catch (PDOException $e2) {
|
||||
echo "Error applying AUTO_INCREMENT: " . $e2->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done.\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "Database Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
?>
|
||||
1462
voice_ai/index.php
1462
voice_ai/index.php
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user