diff --git a/app/Helpers/AiTokenHelper.php b/app/Helpers/AiTokenHelper.php index 11c1350e..de6c20d9 100644 --- a/app/Helpers/AiTokenHelper.php +++ b/app/Helpers/AiTokenHelper.php @@ -58,6 +58,41 @@ public static function saveClaudeUsage(array $apiResult, string $model, string $ } } + /** + * Google Cloud Storage 업로드 사용량 저장 + * GCS Class A 오퍼레이션: $0.005 / 1,000건, Storage: $0.02 / GB / month + */ + public static function saveGcsStorageUsage(string $menuName, int $fileSizeBytes): void + { + try { + $fileSizeMB = $fileSizeBytes / (1024 * 1024); + // 업로드 API 호출 비용 ($0.005/1000 operations) + 월 스토리지 비용 ($0.02/GB, 7일 기준) + $operationCost = 0.005 / 1000; + $storageCost = ($fileSizeMB / 1024) * 0.02 * (7 / 30); // 7일 보관 기준 + $costUsd = $operationCost + $storageCost; + + self::save('google-cloud-storage', $menuName, $fileSizeBytes, 0, $fileSizeBytes, $costUsd / max($fileSizeBytes, 1), 0); + } catch (\Exception $e) { + Log::warning('AI token usage save failed (GCS)', ['error' => $e->getMessage()]); + } + } + + /** + * Google Speech-to-Text 사용량 저장 + * STT latest_long 모델: $0.009 / 15초 + */ + public static function saveSttUsage(string $menuName, int $durationSeconds): void + { + try { + // latest_long 모델: $0.009 per 15 seconds = $0.0006 per second + $costUsd = ceil($durationSeconds / 15) * 0.009; + + self::save('google-speech-to-text', $menuName, $durationSeconds, 0, $durationSeconds, $costUsd / max($durationSeconds, 1), 0); + } catch (\Exception $e) { + Log::warning('AI token usage save failed (STT)', ['error' => $e->getMessage()]); + } + } + /** * 공통 저장 로직 */ diff --git a/app/Services/AiVoiceRecordingService.php b/app/Services/AiVoiceRecordingService.php index d28fc5bf..bd9339fd 100644 --- a/app/Services/AiVoiceRecordingService.php +++ b/app/Services/AiVoiceRecordingService.php @@ -86,17 +86,23 @@ public function processAudio(AiVoiceRecording $recording, string $audioBase64, i now()->format('YmdHis') ); - $gcsUri = $this->googleCloudService->uploadBase64Audio($audioBase64, $objectName); + $uploadResult = $this->googleCloudService->uploadBase64Audio($audioBase64, $objectName); - if (! $gcsUri) { + if (! $uploadResult) { throw new \Exception('오디오 파일 업로드 실패'); } + $gcsUri = $uploadResult['uri']; + $fileSize = $uploadResult['size'] ?? 0; + $recording->update([ 'audio_file_path' => $objectName, 'audio_gcs_uri' => $gcsUri, ]); + // GCS 업로드 토큰 사용량 기록 + AiTokenHelper::saveGcsStorageUsage('AI음성녹음-GCS저장', $fileSize); + // 2. Speech-to-Text 변환 Log::info('AiVoiceRecording STT 시작', ['recording_id' => $recording->id, 'gcs_uri' => $gcsUri]); $transcript = $this->googleCloudService->speechToText($gcsUri); @@ -106,6 +112,9 @@ public function processAudio(AiVoiceRecording $recording, string $audioBase64, i throw new \Exception('음성 인식 실패: STT 결과가 비어있습니다 (transcript=' . var_export($transcript, true) . ')'); } + // STT 토큰 사용량 기록 + AiTokenHelper::saveSttUsage('AI음성녹음-STT변환', $durationSeconds); + $recording->update(['transcript_text' => $transcript]); // 3. Gemini AI 분석 @@ -162,18 +171,24 @@ public function processUploadedFile(AiVoiceRecording $recording, UploadedFile $f $extension ); - $gcsUri = $this->googleCloudService->uploadToStorage($fullPath, $objectName); + $uploadResult = $this->googleCloudService->uploadToStorage($fullPath, $objectName); - if (! $gcsUri) { + if (! $uploadResult) { @unlink($fullPath); throw new \Exception('오디오 파일 업로드 실패'); } + $gcsUri = $uploadResult['uri']; + $uploadedSize = $uploadResult['size'] ?? $fileSize; + $recording->update([ 'audio_file_path' => $objectName, 'audio_gcs_uri' => $gcsUri, ]); + // GCS 업로드 토큰 사용량 기록 + AiTokenHelper::saveGcsStorageUsage('AI음성녹음-GCS저장', $uploadedSize); + // 2. Speech-to-Text 변환 $transcript = $this->googleCloudService->speechToText($gcsUri); @@ -184,6 +199,9 @@ public function processUploadedFile(AiVoiceRecording $recording, UploadedFile $f throw new \Exception('음성 인식 실패'); } + // STT 토큰 사용량 기록 + AiTokenHelper::saveSttUsage('AI음성녹음-STT변환', $estimatedDuration); + $recording->update(['transcript_text' => $transcript]); // 3. Gemini AI 분석 diff --git a/app/Services/GoogleCloudService.php b/app/Services/GoogleCloudService.php index 8d8a792f..d570976f 100644 --- a/app/Services/GoogleCloudService.php +++ b/app/Services/GoogleCloudService.php @@ -97,8 +97,9 @@ private function getAccessToken(): ?string /** * GCS에 파일 업로드 + * @return array|null ['uri' => 'gs://...', 'size' => bytes] or null */ - public function uploadToStorage(string $localPath, string $objectName): ?string + public function uploadToStorage(string $localPath, string $objectName): ?array { $token = $this->getAccessToken(); if (! $token) { @@ -114,6 +115,7 @@ public function uploadToStorage(string $localPath, string $objectName): ?string try { $fileContent = file_get_contents($localPath); + $fileSize = strlen($fileContent); $mimeType = mime_content_type($localPath) ?: 'audio/webm'; $uploadUrl = 'https://storage.googleapis.com/upload/storage/v1/b/'. @@ -126,7 +128,17 @@ public function uploadToStorage(string $localPath, string $objectName): ?string ->post($uploadUrl); if ($response->successful()) { - return 'gs://'.$bucket.'/'.$objectName; + $result = $response->json(); + Log::info('Google Cloud: Storage 업로드 성공', [ + 'object' => $objectName, + 'size' => $result['size'] ?? $fileSize, + 'bucket' => $bucket, + ]); + + return [ + 'uri' => 'gs://'.$bucket.'/'.$objectName, + 'size' => (int) ($result['size'] ?? $fileSize), + ]; } Log::error('Google Cloud: Storage 업로드 실패', ['response' => $response->body()]); @@ -141,8 +153,9 @@ public function uploadToStorage(string $localPath, string $objectName): ?string /** * Base64 오디오를 GCS에 업로드 + * @return array|null ['uri' => 'gs://...', 'size' => bytes] or null */ - public function uploadBase64Audio(string $base64Audio, string $objectName): ?string + public function uploadBase64Audio(string $base64Audio, string $objectName): ?array { // Base64 데이터 파싱 $audioData = $base64Audio; @@ -161,12 +174,12 @@ public function uploadBase64Audio(string $base64Audio, string $objectName): ?str file_put_contents($tempPath, base64_decode($audioData)); // GCS 업로드 - $gcsUri = $this->uploadToStorage($tempPath, $objectName); + $result = $this->uploadToStorage($tempPath, $objectName); // 임시 파일 삭제 @unlink($tempPath); - return $gcsUri; + return $result; } /** diff --git a/app/Services/MeetingLogService.php b/app/Services/MeetingLogService.php index 780ee80e..ee2131f2 100644 --- a/app/Services/MeetingLogService.php +++ b/app/Services/MeetingLogService.php @@ -89,12 +89,14 @@ public function processAudio(MeetingLog $meeting, string $audioBase64, int $dura now()->format('YmdHis') ); - $gcsUri = $this->googleCloudService->uploadBase64Audio($audioBase64, $objectName); + $uploadResult = $this->googleCloudService->uploadBase64Audio($audioBase64, $objectName); - if (! $gcsUri) { + if (! $uploadResult) { throw new \Exception('오디오 파일 업로드 실패'); } + $gcsUri = $uploadResult['uri']; + $meeting->update([ 'audio_file_path' => $objectName, 'audio_gcs_uri' => $gcsUri, @@ -311,13 +313,15 @@ public function processUploadedFile(MeetingLog $meeting, \Illuminate\Http\Upload $extension ); - $gcsUri = $this->googleCloudService->uploadToStorage($fullPath, $objectName); + $uploadResult = $this->googleCloudService->uploadToStorage($fullPath, $objectName); - if (! $gcsUri) { + if (! $uploadResult) { @unlink($fullPath); throw new \Exception('오디오 파일 업로드 실패'); } + $gcsUri = $uploadResult['uri']; + $meeting->update([ 'audio_file_path' => $objectName, 'audio_gcs_uri' => $gcsUri,