package services import ( "sort" "strings" "sync" ) type GeminiBatchStats struct { CandidateCap int `json:"candidateCap"` Requested int `json:"requested"` Batches int `json:"batches"` Succeeded int `json:"succeeded"` Failed int `json:"failed"` RecommendedCount int `json:"recommendedCount"` Errors []string `json:"errors,omitempty"` } func RankSearchResults(query string, results []SearchResult) []SearchResult { queryTerms := strings.Fields(strings.ToLower(query)) positiveTerms := []string{ "b-roll", "b roll", "stock", "stock footage", "footage", "cinematic", "editorial", "establishing", "4k", "hd", "drone", "ambient", "scene", "urban", "cityscape", } negativeTerms := []string{ "shocking", "amazing", "crazy", "must watch", "reaction", "gossip", "celebrity", "thumbnail", "meme", "prank", "drama", "breaking", "viral", "tutorial", "how to", "review", "walkthrough", "course", "lesson", "podcast", "interview", "premiere pro", "after effects", "explained", "breakdown", "vlog", } type scoredResult struct { item SearchResult score int } scored := make([]scoredResult, 0, len(results)) for _, result := range results { score := 0 text := strings.ToLower(result.Title + " " + result.Snippet + " " + result.Source) for _, term := range queryTerms { if strings.Contains(text, term) { score += 3 } } for _, term := range positiveTerms { if strings.Contains(text, term) { score += 2 } } for _, term := range negativeTerms { if strings.Contains(text, term) { score -= 4 } } if result.ThumbnailURL != "" { score += 2 } if result.PreviewVideoURL != "" { score += 3 } switch result.Source { case "Google Video": score -= 1 case "Envato": score += 7 case "Artgrid": score += 7 } scored = append(scored, scoredResult{item: result, score: score}) } sort.SliceStable(scored, func(i, j int) bool { return scored[i].score > scored[j].score }) ranked := make([]SearchResult, 0, len(scored)) for _, item := range scored { ranked = append(ranked, item.item) } return ranked } func GeminiCandidateLimit(total int) int { return total } func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats) { const chunkSize = 8 const maxConcurrentBatches = 2 limit := GeminiCandidateLimit(len(ranked)) stats := GeminiBatchStats{ CandidateCap: limit, Requested: min(limit, len(ranked)), } type batchResult struct { index int recommendations []AIRecommendation err error } batches := make([][]SearchResult, 0, (limit+chunkSize-1)/chunkSize) for start := 0; start < limit; start += chunkSize { end := start + chunkSize if end > limit { end = limit } batches = append(batches, ranked[start:end]) } stats.Batches = len(batches) results := make([]batchResult, len(batches)) var wg sync.WaitGroup sem := make(chan struct{}, maxConcurrentBatches) for idx, batch := range batches { wg.Add(1) go func(batchIndex int, candidates []SearchResult) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() recommended, err := service.Recommend(query, candidates) results[batchIndex] = batchResult{ index: batchIndex, recommendations: recommended, err: err, } }(idx, batch) } wg.Wait() merged := make([]AIRecommendation, 0, len(ranked)) seen := map[string]bool{} for _, batch := range results { if batch.err != nil { stats.Failed++ if len(stats.Errors) < 5 { stats.Errors = append(stats.Errors, batch.err.Error()) } continue } stats.Succeeded++ for _, item := range batch.recommendations { if item.Link == "" || seen[item.Link] { continue } seen[item.Link] = true merged = append(merged, item) } } stats.RecommendedCount = len(merged) return merged, stats } func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation { merged := make([]AIRecommendation, 0, min(limit, len(ranked))) seen := map[string]bool{} for _, item := range recommended { if !item.Recommended { continue } if item.Link == "" || seen[item.Link] { continue } seen[item.Link] = true merged = append(merged, item) } for _, item := range recommended { if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit { continue } seen[item.Link] = true merged = append(merged, item) } for _, item := range ranked { if len(merged) >= limit || item.Link == "" || seen[item.Link] { continue } seen[item.Link] = true merged = append(merged, AIRecommendation{ Title: item.Title, Link: item.Link, Snippet: item.Snippet, ThumbnailURL: item.ThumbnailURL, PreviewVideoURL: item.PreviewVideoURL, Source: item.Source, Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.", Recommended: false, }) } return merged }