This commit is contained in:
@@ -320,7 +320,7 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
}
|
||||
scored := services.RankSearchResults(rankQuery, results)
|
||||
a.debug("search ranked summary", summarizeSearchResults(scored, time.Since(started), services.GeminiCandidateLimit(len(scored)), ""))
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing top candidate visuals with Gemini Vision", "progress": 75})
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
||||
recommended, geminiStats := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||
a.debug("search gemini evaluation", geminiStats)
|
||||
err = nil
|
||||
@@ -337,8 +337,8 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
ThumbnailURL: result.ThumbnailURL,
|
||||
PreviewVideoURL: result.PreviewVideoURL,
|
||||
Source: result.Source,
|
||||
Reason: "Keyword-ranked result added without extra Gemini vision tokens.",
|
||||
Recommended: true,
|
||||
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
|
||||
Recommended: false,
|
||||
})
|
||||
}
|
||||
warning := err.Error()
|
||||
|
||||
@@ -154,7 +154,8 @@ func (g *GeminiService) Recommend(query string, candidates []SearchResult) ([]AI
|
||||
{
|
||||
"text": `Analyze the provided images for the user's search intent. Return JSON only in this shape:
|
||||
{"recommendations":[{"index":0,"reason":"short reason","recommended":true}]}
|
||||
Mark only the best matches as recommended=true. Keep reasons concise. Recommend up to 8 items.
|
||||
Return one entry for every analyzed candidate. Use Korean for every reason. Keep reasons concise but specific enough to explain usefulness.
|
||||
Mark the strongest matches as recommended=true and weaker matches as recommended=false.
|
||||
Prefer cinematic b-roll, stock footage, editorial footage, clean composition, usable establishing shots, and professional media thumbnails.
|
||||
Avoid clickbait faces, exaggerated expressions, meme aesthetics, low-information thumbnails, sensational text overlays, or gossip-style imagery.
|
||||
Favor thumbnails that look directly useful for media editing and footage sourcing.
|
||||
@@ -230,7 +231,7 @@ User query: ` + query,
|
||||
|
||||
recommendations := make([]AIRecommendation, 0, len(parsed.Recommendations))
|
||||
for _, rec := range parsed.Recommendations {
|
||||
if rec.Index < 0 || rec.Index >= len(candidates) || !rec.Recommended {
|
||||
if rec.Index < 0 || rec.Index >= len(candidates) {
|
||||
continue
|
||||
}
|
||||
src := candidates[rec.Index]
|
||||
@@ -241,13 +242,13 @@ User query: ` + query,
|
||||
ThumbnailURL: src.ThumbnailURL,
|
||||
PreviewVideoURL: src.PreviewVideoURL,
|
||||
Source: src.Source,
|
||||
Reason: rec.Reason,
|
||||
Recommended: true,
|
||||
Reason: normalizeKoreanReason(rec.Reason),
|
||||
Recommended: rec.Recommended,
|
||||
})
|
||||
}
|
||||
|
||||
if len(recommendations) == 0 {
|
||||
for _, candidate := range candidates[:min(4, len(candidates))] {
|
||||
for _, candidate := range candidates[:min(8, len(candidates))] {
|
||||
recommendations = append(recommendations, AIRecommendation{
|
||||
Title: candidate.Title,
|
||||
Link: candidate.Link,
|
||||
@@ -255,8 +256,8 @@ User query: ` + query,
|
||||
ThumbnailURL: candidate.ThumbnailURL,
|
||||
PreviewVideoURL: candidate.PreviewVideoURL,
|
||||
Source: candidate.Source,
|
||||
Reason: "Fallback result because Gemini returned no recommended items.",
|
||||
Recommended: true,
|
||||
Reason: "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.",
|
||||
Recommended: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -412,6 +413,14 @@ func truncateForError(text string, limit int) string {
|
||||
return trimmed[:limit] + "..."
|
||||
}
|
||||
|
||||
func normalizeKoreanReason(reason string) string {
|
||||
trimmed := strings.TrimSpace(reason)
|
||||
if trimmed == "" {
|
||||
return "시각 정보가 제한적이지만 검색 의도와의 관련성을 기준으로 평가했습니다."
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func buildSearchQueries(originalQuery, englishQuery string) []string {
|
||||
base := strings.TrimSpace(englishQuery)
|
||||
if base == "" {
|
||||
|
||||
+51
-18
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type GeminiBatchStats struct {
|
||||
@@ -80,42 +81,63 @@ func RankSearchResults(query string, results []SearchResult) []SearchResult {
|
||||
}
|
||||
|
||||
func GeminiCandidateLimit(total int) int {
|
||||
switch {
|
||||
case total <= 12:
|
||||
return total
|
||||
case total <= 16:
|
||||
return 12
|
||||
default:
|
||||
return 16
|
||||
}
|
||||
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)),
|
||||
}
|
||||
merged := make([]AIRecommendation, 0, len(ranked))
|
||||
seen := map[string]bool{}
|
||||
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
|
||||
}
|
||||
batch := ranked[start:end]
|
||||
stats.Batches++
|
||||
recommended, err := service.Recommend(query, batch)
|
||||
if err != nil {
|
||||
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, err.Error())
|
||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
stats.Succeeded++
|
||||
for _, item := range recommended {
|
||||
for _, item := range batch.recommendations {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
@@ -132,6 +154,9 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, item := range recommended {
|
||||
if !item.Recommended {
|
||||
continue
|
||||
}
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
@@ -139,6 +164,14 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
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
|
||||
@@ -151,8 +184,8 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
ThumbnailURL: item.ThumbnailURL,
|
||||
PreviewVideoURL: item.PreviewVideoURL,
|
||||
Source: item.Source,
|
||||
Reason: "Keyword-ranked result added without extra Gemini vision tokens.",
|
||||
Recommended: true,
|
||||
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
|
||||
Recommended: false,
|
||||
})
|
||||
}
|
||||
return merged
|
||||
|
||||
Reference in New Issue
Block a user