gemini = $gemini; } /** * 스크린샷 배열을 Gemini Vision으로 분석 * * @param array $imagePaths 스크린샷 파일 경로 배열 * @return array 분석 결과 배열 */ public function analyzeScreenshots(array $imagePaths): array { $results = []; foreach ($imagePaths as $index => $imagePath) { $screenNumber = $index + 1; Log::info("ScreenAnalysis: 스크린샷 {$screenNumber}/" . count($imagePaths) . " 분석 시작", [ 'path' => $imagePath, ]); $result = $this->analyzeSingleScreen($imagePath, $screenNumber, count($imagePaths)); if ($result) { $result = $this->ensureStepsFormat($result); $results[] = $result; } else { Log::warning("ScreenAnalysis: 스크린샷 {$screenNumber} 분석 실패, 기본값 사용"); $results[] = [ 'screen_number' => $screenNumber, 'title' => "화면 {$screenNumber}", 'steps' => [ [ 'step_number' => 1, 'narration' => '이 화면에서는 주요 기능을 확인할 수 있습니다.', 'focused_element' => null, 'duration' => 6, ], ], ]; } } return $results; } /** * 기존 형식(narration/ui_elements) → steps[] 형식으로 변환 (하위 호환) * Job에서 DB 캐시된 기존 형식 변환에도 사용 */ public function ensureStepsFormatPublic(array $screen): array { return $this->ensureStepsFormat($screen); } /** * 기존 형식(narration/ui_elements) → steps[] 형식으로 변환 (하위 호환) */ private function ensureStepsFormat(array $screen): array { if (! empty($screen['steps'])) { return $screen; } $narration = $screen['narration'] ?? '이 화면의 기능을 확인할 수 있습니다.'; $uiElements = $screen['ui_elements'] ?? []; $duration = $screen['duration'] ?? 8; if (empty($uiElements)) { $screen['steps'] = [ [ 'step_number' => 1, 'narration' => $narration, 'focused_element' => null, 'duration' => $duration, ], ]; } else { $steps = []; $sentences = preg_split('/(?<=[.。])\s*/u', $narration, -1, PREG_SPLIT_NO_EMPTY); foreach ($uiElements as $i => $el) { $steps[] = [ 'step_number' => $i + 1, 'narration' => $sentences[$i] ?? $narration, 'focused_element' => [ 'type' => $el['type'] ?? 'other', 'label' => $el['label'] ?? '', 'x' => $el['x'] ?? 0.5, 'y' => $el['y'] ?? 0.5, 'w' => 0.2, 'h' => 0.15, ], 'duration' => max(5, (int) ($duration / count($uiElements))), ]; } $screen['steps'] = $steps; } unset($screen['narration'], $screen['ui_elements'], $screen['duration']); return $screen; } /** * 단일 스크린샷 분석 */ private function analyzeSingleScreen(string $imagePath, int $screenNumber, int $totalScreens): ?array { if (! file_exists($imagePath)) { Log::error("ScreenAnalysis: 파일 없음 - {$imagePath}"); return null; } $imageData = base64_encode(file_get_contents($imagePath)); $mimeType = mime_content_type($imagePath) ?: 'image/png'; $prompt = << $prompt], [ 'inlineData' => [ 'mimeType' => $mimeType, 'data' => $imageData, ], ], ]; $result = $this->gemini->callGeminiWithParts($parts, 0.3, 2048); if (! $result) { return null; } $parsed = $this->gemini->parseJson($result); if (! $parsed || ! isset($parsed['screen_number'])) { Log::warning('ScreenAnalysis: JSON 파싱 실패 또는 형식 불일치', [ 'result' => substr($result, 0, 300), ]); return null; } // screen_number 강제 보정 $parsed['screen_number'] = $screenNumber; return $parsed; } }