This commit is contained in:
@@ -255,6 +255,20 @@
|
|||||||
- backend debug broadcasts
|
- backend debug broadcasts
|
||||||
|
|
||||||
## Recent Change Log
|
## Recent Change Log
|
||||||
|
- Date: `2026-03-16`
|
||||||
|
- What changed:
|
||||||
|
- Stabilized the Gemini visual-review path after widened search budgets caused full-batch “no candidate thumbnails or preview frames” failures.
|
||||||
|
- Google Video enrichment now always prefers the canonical YouTube `ytimg` thumbnail instead of keeping a potentially broken search-engine thumbnail.
|
||||||
|
- Gemini visual fetch now preserves the last concrete fetch error, retries with derived YouTube thumbnails when possible, and only falls back to the generic no-visual message after all image/frame paths fail.
|
||||||
|
- User-facing fallback warning text for the all-batches-no-visual case is now softened so ranked results can still be shown without surfacing the raw internal Gemini error string in the UI.
|
||||||
|
- Why it changed:
|
||||||
|
- The latest deployed build widened search enough that top-ranked candidates sometimes carried metadata without any fetchable image bytes, causing Gemini review to fail for every batch and surfacing an alarming warning even though ranked fallback results still existed.
|
||||||
|
- How it was verified:
|
||||||
|
- `go test ./...`
|
||||||
|
- `bash scripts/selftest.sh`
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- This improves YouTube-backed candidates immediately, but Envato/Artgrid thumbnails can still be inaccessible in some provider-side cases, so Gemini can still fall back to ranked results when source media is locked down.
|
||||||
|
|
||||||
- Date: `2026-03-16`
|
- Date: `2026-03-16`
|
||||||
- What changed:
|
- What changed:
|
||||||
- Hid the expanded query-variant chip list from the search UI while leaving backend/debug query visibility intact.
|
- Hid the expanded query-variant chip list from the search UI while leaving backend/debug query visibility intact.
|
||||||
|
|||||||
@@ -471,6 +471,9 @@ func (a *App) searchMedia(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if geminiErr != nil && len(recommended) == 0 {
|
if geminiErr != nil && len(recommended) == 0 {
|
||||||
warning := geminiErr.Error()
|
warning := geminiErr.Error()
|
||||||
|
if strings.Contains(warning, "no candidate thumbnails or preview frames could be fetched for gemini vision") {
|
||||||
|
warning = "AI visual review was unavailable for this search, so ranked results are being shown instead."
|
||||||
|
}
|
||||||
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
||||||
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ func TestLowValueThumbnailDetection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoogleVideoCollectorPrefersYouTubeDerivedThumbnail(t *testing.T) {
|
||||||
|
result := googleVideoCollector{}.Enrich(nil, SearchResult{
|
||||||
|
Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
ThumbnailURL: "https://example.com/some-search-thumb.jpg",
|
||||||
|
})
|
||||||
|
if result.ThumbnailURL != "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg" {
|
||||||
|
t.Fatalf("expected derived youtube thumbnail, got %q", result.ThumbnailURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGeminiCandidateLimitNeverExceedsCandidates(t *testing.T) {
|
func TestGeminiCandidateLimitNeverExceedsCandidates(t *testing.T) {
|
||||||
if got := GeminiCandidateLimit(9); got != 9 {
|
if got := GeminiCandidateLimit(9); got != 9 {
|
||||||
t.Fatalf("expected Gemini limit to stay within candidate count, got %d", got)
|
t.Fatalf("expected Gemini limit to stay within candidate count, got %d", got)
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ func newBrowserStyleImageRequest(imageURL, referer string) (*http.Request, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (string, string, error) {
|
func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (string, string, error) {
|
||||||
|
lastErr := fmt.Errorf("candidate has no thumbnail or preview video")
|
||||||
if candidate.PreviewVideoURL != "" && (candidate.Source == "Envato" || candidate.Source == "Artgrid") {
|
if candidate.PreviewVideoURL != "" && (candidate.Source == "Envato" || candidate.Source == "Artgrid") {
|
||||||
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
||||||
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||||
@@ -484,6 +485,7 @@ func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (
|
|||||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||||
return data, mimeType, nil
|
return data, mimeType, nil
|
||||||
}
|
}
|
||||||
|
lastErr = err
|
||||||
}
|
}
|
||||||
if candidate.ThumbnailURL != "" {
|
if candidate.ThumbnailURL != "" {
|
||||||
if isLowValueThumbnail(candidate.ThumbnailURL) {
|
if isLowValueThumbnail(candidate.ThumbnailURL) {
|
||||||
@@ -492,8 +494,8 @@ func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (
|
|||||||
"source": candidate.Source,
|
"source": candidate.Source,
|
||||||
"thumbnailUrl": candidate.ThumbnailURL,
|
"thumbnailUrl": candidate.ThumbnailURL,
|
||||||
})
|
})
|
||||||
return "", "", fmt.Errorf("candidate thumbnail is low value")
|
lastErr = fmt.Errorf("candidate thumbnail is low value")
|
||||||
}
|
} else {
|
||||||
cacheKey := "image\n" + candidate.ThumbnailURL
|
cacheKey := "image\n" + candidate.ThumbnailURL
|
||||||
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||||
return data, mimeType, nil
|
return data, mimeType, nil
|
||||||
@@ -503,6 +505,20 @@ func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (
|
|||||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||||
return data, mimeType, nil
|
return data, mimeType, nil
|
||||||
}
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fallbackThumbnail := deriveThumbnail(candidate.Link); fallbackThumbnail != "" && fallbackThumbnail != candidate.ThumbnailURL {
|
||||||
|
cacheKey := "image\n" + fallbackThumbnail
|
||||||
|
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||||
|
return data, mimeType, nil
|
||||||
|
}
|
||||||
|
data, mimeType, err := fetchImageAsInlineData(g.Client, fallbackThumbnail, candidate.Link)
|
||||||
|
if err == nil {
|
||||||
|
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||||
|
return data, mimeType, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
}
|
}
|
||||||
if candidate.PreviewVideoURL != "" {
|
if candidate.PreviewVideoURL != "" {
|
||||||
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
||||||
@@ -511,12 +527,13 @@ func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (
|
|||||||
}
|
}
|
||||||
data, mimeType, err := extractFrameFromVideo(candidate.PreviewVideoURL)
|
data, mimeType, err := extractFrameFromVideo(candidate.PreviewVideoURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
lastErr = err
|
||||||
}
|
} else {
|
||||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||||
return data, mimeType, nil
|
return data, mimeType, nil
|
||||||
}
|
}
|
||||||
return "", "", fmt.Errorf("candidate has no thumbnail or preview video")
|
}
|
||||||
|
return "", "", lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractFrameFromVideo(videoURL string) (string, string, error) {
|
func extractFrameFromVideo(videoURL string) (string, string, error) {
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func (googleVideoCollector) Accept(result SearchResult) bool {
|
|||||||
return isUsefulGoogleVideoResult(result)
|
return isUsefulGoogleVideoResult(result)
|
||||||
}
|
}
|
||||||
func (googleVideoCollector) Enrich(searcher *SearchService, result SearchResult) SearchResult {
|
func (googleVideoCollector) Enrich(searcher *SearchService, result SearchResult) SearchResult {
|
||||||
if result.ThumbnailURL == "" {
|
if derived := deriveThumbnail(result.Link); derived != "" {
|
||||||
result.ThumbnailURL = deriveThumbnail(result.Link)
|
result.ThumbnailURL = derived
|
||||||
}
|
}
|
||||||
result.Source = strings.TrimSpace(result.Source)
|
result.Source = strings.TrimSpace(result.Source)
|
||||||
if result.Source == "" {
|
if result.Source == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user