feat:GCS 업로드/STT 사용량 토큰 기록 추가

- AiTokenHelper: saveGcsStorageUsage(), saveSttUsage() 메서드 추가
- GoogleCloudService: uploadToStorage 반환값 배열로 변경 (uri + size)
- AiVoiceRecordingService: GCS/STT 각각 토큰 사용량 기록
- MeetingLogService: uploadToStorage 반환값 변경 대응

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-07 13:45:50 +09:00
parent b25b7c57f1
commit d121a319b3
4 changed files with 83 additions and 13 deletions

View File

@@ -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()]);
}
}
/**
* 공통 저장 로직
*/

View File

@@ -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 분석

View File

@@ -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;
}
/**

View File

@@ -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,