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:
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 저장 로직
|
||||
*/
|
||||
|
||||
@@ -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 분석
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user