From 7991f3e6d4abcffa87f6d8981e3d23b6a9e52edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 21 Feb 2026 16:37:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[video]=20=EC=A2=8C=ED=91=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EC=98=81=EC=83=81=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20analysis=5Fdata?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 각 step별 검증 결과 저장 (accurate/corrected + 원본 좌표) - 스크린 단위 검증 통계 저장 (정확/보정 수, 검증 시각) - 영상 완료 시 _output 메타데이터 저장 (경로, GCS, 비용, 슬라이드수, 총 재생시간) --- app/Jobs/TutorialVideoJob.php | 22 +++++++++++- app/Services/Video/ScreenAnalysisService.php | 35 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/Jobs/TutorialVideoJob.php b/app/Jobs/TutorialVideoJob.php index 0daecdae..bf4f6e4c 100644 --- a/app/Jobs/TutorialVideoJob.php +++ b/app/Jobs/TutorialVideoJob.php @@ -46,6 +46,7 @@ public function handle( if (! $tutorial) { Log::error('TutorialVideoJob: 레코드를 찾을 수 없음', ['id' => $this->tutorialVideoId]); + return; } @@ -67,6 +68,7 @@ public function handle( if (empty($screenshots)) { $tutorial->markFailed('업로드된 스크린샷이 없습니다'); + return; } @@ -74,6 +76,7 @@ public function handle( if (empty($analysisData)) { $tutorial->markFailed('스크린샷 분석에 실패했습니다'); + return; } @@ -126,6 +129,7 @@ public function handle( if (! $imagePath || ! file_exists($imagePath)) { Log::warning("TutorialVideoJob: 스크린샷 없음 - index {$i}"); + continue; } @@ -176,6 +180,7 @@ public function handle( if (count($slidePaths) <= 2) { // 인트로+아웃트로만 있으면 실패 $tutorial->markFailed('슬라이드 생성에 실패했습니다'); + return; } @@ -257,6 +262,7 @@ public function handle( if (! $result || ! file_exists($finalOutputPath)) { $tutorial->markFailed('영상 합성에 실패했습니다'); + return; } @@ -267,13 +273,26 @@ public function handle( $gcsPath = null; if ($gcs->isAvailable()) { - $objectName = "tutorials/{$tutorial->tenant_id}/{$tutorial->id}/tutorial_" . date('Ymd_His') . '.mp4'; + $objectName = "tutorials/{$tutorial->tenant_id}/{$tutorial->id}/tutorial_".date('Ymd_His').'.mp4'; $gcsPath = $gcs->upload($finalOutputPath, $objectName); } $tutorial->updateProgress(TutorialVideo::STATUS_ASSEMBLING, 95, '업로드 완료'); // === Step 7: 완료 (100%) === + // analysis_data에 영상 저장 경로 메타데이터 추가 + $analysisWithMeta = $tutorial->analysis_data ?? $analysisData; + if (is_array($analysisWithMeta)) { + $analysisWithMeta['_output'] = [ + 'completed_at' => now()->toIso8601String(), + 'output_path' => $finalOutputPath, + 'gcs_path' => $gcsPath, + 'cost_usd' => round($totalCost, 4), + 'total_slides' => count($slidePaths), + 'total_duration' => $totalDuration, + ]; + } + $tutorial->update([ 'status' => TutorialVideo::STATUS_COMPLETED, 'progress' => 100, @@ -281,6 +300,7 @@ public function handle( 'output_path' => $finalOutputPath, 'gcs_path' => $gcsPath, 'cost_usd' => $totalCost, + 'analysis_data' => $analysisWithMeta, ]); Log::info('TutorialVideoJob: 완료', [ diff --git a/app/Services/Video/ScreenAnalysisService.php b/app/Services/Video/ScreenAnalysisService.php index 459c3a14..efd962d3 100644 --- a/app/Services/Video/ScreenAnalysisService.php +++ b/app/Services/Video/ScreenAnalysisService.php @@ -624,9 +624,28 @@ private function runCoordinateVerification(string $imagePath, array $parsed): ar // Gemini 2-pass 검증 호출 $verifiedSteps = $this->verifyCoordinates($verificationImagePath, $stepsWithElement); - // 보정 적용 + // 보정 적용 + 검증 메타데이터 저장 $parsed = $this->applyVerifiedCoordinates($parsed, $verifiedSteps); + // 스크린 단위 검증 통계 저장 + $accurateCount = 0; + $correctedCount = 0; + if ($verifiedSteps) { + foreach ($verifiedSteps as $v) { + if ($v['accurate'] ?? true) { + $accurateCount++; + } else { + $correctedCount++; + } + } + } + $parsed['_verification'] = [ + 'verified_at' => now()->toIso8601String(), + 'total_steps' => count($stepsWithElement), + 'accurate' => $accurateCount, + 'corrected' => $correctedCount, + ]; + // 검증 이미지 정리 if (file_exists($verificationImagePath)) { @unlink($verificationImagePath); @@ -916,7 +935,9 @@ private function applyVerifiedCoordinates(array $parsed, ?array $verifiedSteps): continue; } - if (! ($verification['accurate'] ?? true)) { + $isAccurate = $verification['accurate'] ?? true; + + if (! $isAccurate) { $oldX = $step['focused_element']['x']; $oldY = $step['focused_element']['y']; $oldW = $step['focused_element']['w']; @@ -927,11 +948,21 @@ private function applyVerifiedCoordinates(array $parsed, ?array $verifiedSteps): $step['focused_element']['w'] = $verification['corrected_w'] ?? $oldW; $step['focused_element']['h'] = $verification['corrected_h'] ?? $oldH; + // 보정 전 좌표를 메타데이터로 저장 + $step['focused_element']['_verification'] = [ + 'accurate' => false, + 'original' => ['x' => $oldX, 'y' => $oldY, 'w' => $oldW, 'h' => $oldH], + ]; + Log::info("ScreenAnalysis: 좌표 보정 Step {$stepNum}", [ 'label' => $step['focused_element']['label'] ?? '?', 'before' => "x={$oldX}, y={$oldY}, w={$oldW}, h={$oldH}", 'after' => "x={$step['focused_element']['x']}, y={$step['focused_element']['y']}, w={$step['focused_element']['w']}, h={$step['focused_element']['h']}", ]); + } else { + $step['focused_element']['_verification'] = [ + 'accurate' => true, + ]; } } unset($step);