googleCloud = $googleCloud; $this->config = AiConfig::getActiveGemini(); } /** * 실시간 트렌딩 키워드 → 건강 채널용 필터링/리프레이밍 */ public function filterHealthTrending(array $trendingKeywords): array { if (empty($trendingKeywords)) { return []; } $keywordList = collect($trendingKeywords)->map(function ($item, $i) { $news = ! empty($item['news_title']) ? " (뉴스: {$item['news_title']})" : ''; return ($i + 1) . ". {$item['keyword']}{$news}"; })->implode("\n"); $prompt = <<callGemini($prompt); if (! $result) { return []; } $parsed = $this->parseJsonResponse($result); if (! $parsed || empty($parsed['keywords'])) { return []; } // 원본 트렌딩 데이터와 매칭하여 traffic 등 보존 $trendingMap = collect($trendingKeywords)->keyBy('keyword'); return collect($parsed['keywords'])->map(function ($item) use ($trendingMap) { $original = $trendingMap->get($item['keyword']); return [ 'keyword' => $item['keyword'], 'health_angle' => $item['health_angle'] ?? '', 'suggested_topic' => $item['suggested_topic'] ?? $item['keyword'], 'traffic' => $original['traffic'] ?? '', 'news_title' => $original['news_title'] ?? '', 'pub_date' => $original['pub_date'] ?? null, ]; })->values()->toArray(); } /** * 키워드 → 트렌딩 제목 5개 생성 (기본) */ public function generateTrendingTitles(string $keyword): array { $prompt = <<callGemini($prompt); if (! $result) { return []; } $parsed = $this->parseJsonResponse($result); return $parsed['titles'] ?? []; } /** * 트렌딩 키워드 + 컨텍스트 → 후킹 제목 5개 생성 */ public function generateTrendingHookTitles(string $keyword, array $context = []): array { $contextBlock = ''; if (! empty($context)) { $newsTitle = $context['news_title'] ?? ''; $traffic = $context['traffic'] ?? ''; $healthAngle = $context['health_angle'] ?? ''; $suggestedTopic = $context['suggested_topic'] ?? ''; $contextBlock = <<callGemini($prompt); if (! $result) { return []; } $parsed = $this->parseJsonResponse($result); return $parsed['titles'] ?? []; } /** * 제목 → 장면별 시나리오 생성 (Veo 3.1 공식 가이드 기반 프롬프트) */ public function generateScenario(string $title, string $keyword = ''): array { $prompt = <<callGemini($prompt); if (! $result) { return []; } return $this->parseJsonResponse($result) ?: []; } /** * Gemini API 호출 (AiConfig 기반 - API Key / Vertex AI 자동 분기) */ private function callGemini(string $prompt): ?string { if (! $this->config) { Log::error('GeminiScriptService: 활성화된 Gemini 설정이 없습니다. (시스템 > AI 설정 확인)'); return null; } try { $model = $this->config->model ?: 'gemini-3.0-flash'; $body = [ 'contents' => [ [ 'parts' => [ ['text' => $prompt], ], ], ], 'generationConfig' => [ 'temperature' => 0.9, 'maxOutputTokens' => 4096, 'responseMimeType' => 'application/json', ], ]; if ($this->config->isVertexAi()) { // Vertex AI 방식 (서비스 계정 OAuth) $accessToken = $this->googleCloud->getAccessToken(); if (! $accessToken) { Log::error('GeminiScriptService: Vertex AI 액세스 토큰 획득 실패'); return null; } $projectId = $this->config->getProjectId(); $region = $this->config->getRegion(); $url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/{$model}:generateContent"; // Vertex AI에서는 role 필수 $body['contents'][0]['role'] = 'user'; $response = Http::withToken($accessToken) ->timeout(60) ->post($url, $body); } else { // API Key 방식 $apiKey = $this->config->api_key; $baseUrl = $this->config->base_url ?? 'https://generativelanguage.googleapis.com/v1beta'; $url = "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}"; $response = Http::timeout(60)->post($url, $body); } if (! $response->successful()) { Log::error('GeminiScriptService: API 호출 실패', [ 'status' => $response->status(), 'body' => $response->body(), 'auth_type' => $this->config->isVertexAi() ? 'vertex_ai' : 'api_key', ]); return null; } $data = $response->json(); $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null; return $text; } catch (\Exception $e) { Log::error('GeminiScriptService: 예외 발생', ['error' => $e->getMessage()]); return null; } } /** * JSON 응답 파싱 (코드블록 제거 포함) */ private function parseJsonResponse(string $text): ?array { // 코드블록 제거 (```json ... ```) $text = preg_replace('/^```(?:json)?\s*/m', '', $text); $text = preg_replace('/```\s*$/m', '', $text); $text = trim($text); $decoded = json_decode($text, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::warning('GeminiScriptService: JSON 파싱 실패', [ 'error' => json_last_error_msg(), 'text' => substr($text, 0, 500), ]); return null; } return $decoded; } }