Harden preview enrichment and recommendation metadata
build-push / docker (push) Has been cancelled

This commit is contained in:
AI Assistant
2026-03-16 16:39:09 +09:00
parent 93b9f571ab
commit 2064825d29
7 changed files with 433 additions and 106 deletions
+49 -20
View File
@@ -10,6 +10,8 @@ import (
)
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
const FallbackPreviewReason = "Fallback due to missing provider preview."
const PendingVisualReason = "Ranked candidate pending stronger visual evidence."
type GeminiBatchStats struct {
CandidateCap int `json:"candidateCap"`
@@ -19,6 +21,7 @@ type GeminiBatchStats struct {
Failed int `json:"failed"`
SequentialRetried int `json:"sequentialRetried"`
RecommendedCount int `json:"recommendedCount"`
VisualRejectCount int `json:"visualRejectCount"`
Errors []string `json:"errors,omitempty"`
}
@@ -58,19 +61,25 @@ func RankSearchResults(query string, results []SearchResult) []SearchResult {
score -= 4
}
}
if result.ThumbnailURL != "" {
score += 2
}
if result.PreviewVideoURL != "" {
score += 3
score += 10
}
if hasUsableThumbnail(result.ThumbnailURL) {
score += 5
}
if isLowValueThumbnail(result.ThumbnailURL) {
score -= 8
}
if strings.TrimSpace(result.PreviewVideoURL) == "" && !hasUsableThumbnail(result.ThumbnailURL) {
score -= 10
}
switch result.Source {
case "Google Video":
score -= 1
score -= 2
case "Envato":
score += 7
score += 5
case "Artgrid":
score += 7
score += 4
}
scored = append(scored, scoredResult{item: result, score: score})
}
@@ -106,6 +115,11 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s
CandidateCap: limit,
Requested: min(limit, len(ranked)),
}
for _, item := range ranked[:min(limit, len(ranked))] {
if strings.TrimSpace(item.PreviewVideoURL) == "" && !hasUsableThumbnail(item.ThumbnailURL) {
stats.VisualRejectCount++
}
}
type batchResult struct {
index int
recommendations []AIRecommendation
@@ -231,7 +245,7 @@ func BuildFallbackRecommendations(ranked []SearchResult, limit int, reason strin
fallback := make([]AIRecommendation, 0, min(limit, len(ranked)))
for _, item := range ranked[:min(limit, len(ranked))] {
fallback = append(fallback, AIRecommendation{
fallback = append(fallback, DecorateRecommendationMedia(AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
@@ -240,7 +254,7 @@ func BuildFallbackRecommendations(ranked []SearchResult, limit int, reason strin
Source: item.Source,
Reason: reason,
Recommended: false,
})
}))
}
return fallback
}
@@ -385,6 +399,8 @@ func looksNegativeReason(reason string) bool {
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
seen := map[string]bool{}
fillerCount := 0
maxFiller := min(4, limit)
for _, item := range recommended {
if !item.Recommended {
@@ -394,7 +410,7 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
continue
}
seen[item.Link] = true
merged = append(merged, item)
merged = append(merged, DecorateRecommendationMedia(item))
}
for _, item := range recommended {
@@ -404,8 +420,11 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
if looksNegativeReason(item.Reason) || strings.Contains(item.Reason, GeminiFallbackReason) {
continue
}
if strings.TrimSpace(item.PreviewVideoURL) == "" && !hasUsableThumbnail(item.ThumbnailURL) {
continue
}
seen[item.Link] = true
merged = append(merged, item)
merged = append(merged, DecorateRecommendationMedia(item))
}
if len(merged) < min(12, limit) {
@@ -413,20 +432,24 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
if len(merged) >= min(12, limit) || item.Link == "" || seen[item.Link] {
continue
}
if strings.TrimSpace(item.ThumbnailURL) == "" && strings.TrimSpace(item.PreviewVideoURL) == "" {
if fillerCount >= maxFiller {
break
}
if strings.TrimSpace(item.PreviewVideoURL) == "" && !hasUsableThumbnail(item.ThumbnailURL) {
continue
}
seen[item.Link] = true
merged = append(merged, AIRecommendation{
merged = append(merged, DecorateRecommendationMedia(AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: "Gemini 검토가 부족해 편집용 후보로 추가된 결과입니다.",
Reason: PendingVisualReason,
Recommended: false,
})
}))
fillerCount++
}
}
return merged
@@ -435,31 +458,37 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
func BackfillRecommendations(existing []AIRecommendation, ranked []SearchResult, limit int, reason string) []AIRecommendation {
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
seen := map[string]bool{}
fillerCount := 0
maxFiller := min(4, limit)
for _, item := range existing {
if item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, item)
merged = append(merged, DecorateRecommendationMedia(item))
}
for _, item := range ranked {
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
continue
}
if strings.TrimSpace(item.ThumbnailURL) == "" && strings.TrimSpace(item.PreviewVideoURL) == "" {
if fillerCount >= maxFiller {
break
}
if strings.TrimSpace(item.PreviewVideoURL) == "" && !hasUsableThumbnail(item.ThumbnailURL) {
continue
}
seen[item.Link] = true
merged = append(merged, AIRecommendation{
merged = append(merged, DecorateRecommendationMedia(AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: reason,
Reason: firstNonEmpty(strings.TrimSpace(reason), FallbackPreviewReason),
Recommended: false,
})
}))
fillerCount++
}
return merged
}