From 75f1bb360c7adbba3d1a9d3b467aa1825e5ccdbd Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Tue, 17 Mar 2026 15:09:23 +0900 Subject: [PATCH] Reduce noisy Gemini partial failure counts --- TODO.md | 20 ++++++++++++++++++ backend/services/gemini_test.go | 15 +++++++++++++ backend/services/ranker.go | 37 +++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index cb6037d..e1a60e5 100644 --- a/TODO.md +++ b/TODO.md @@ -235,6 +235,7 @@ - Artgrid preview playback now has a server-side ffmpeg transcode path for `.m3u8` style preview URLs, but this trades storage savings for runtime CPU cost. - The provided Artgrid HTML sample still does not expose a direct preview `m3u8` or `mp4` URL by itself, and `yt-dlp` probe on the sample clip URL returned `Unsupported URL`, so fully reliable Artgrid playback still depends on live-page/runtime preview discovery succeeding elsewhere in the pipeline. - Docker CLI is not installed in this environment, so container-image build verification still cannot be performed locally from this machine. +- Gemini batch warnings can still mix true model/output failures with harmless candidate-skip cases unless the recovery path filters those error classes before surfacing them. - The local self-test script is better than before, but it is still a smoke test, not full integration coverage. ## Current Risks Around Search Quality @@ -557,6 +558,7 @@ - [ ] Reduce `/api/search` latency further without collapsing result count - [ ] Rebuild the reverted search-expansion work from the previous stable baseline, but only after measuring where candidate quality collapses between ranked pool and final merge - [ ] Validate the reopened `5ca7aef` search-breadth direction against real proxy timeouts and visible result count before widening it any further +- [ ] Continue hardening Gemini batch parsing so truncated JSON and ignorable low-visual candidate failures do not inflate user-facing warning counts - [ ] Build a repeatable repo-local bootstrap script or documented setup command set for non-root machines so fresh PC setup does not depend on shell history - [ ] Improve Envato / Artgrid preview acquisition reliability so Gemini Vision sees real frames more often - [ ] Browser-verify the new result modal at multiple viewport heights and confirm translated Source Summary readability on real long descriptions @@ -628,6 +630,24 @@ - If behavior in the browser does not match the latest backend/frontend code, the first assumption should be stale frontend assets until proven otherwise ## Recent Change Log +- Date: `2026-03-17` +- What changed: + - Restored hard/ignorable Gemini batch error filtering so low-value thumbnail skips and no-visual candidate skips do not count as user-facing partial batch failures when useful recommendations were still recovered. + - Added unit coverage for the Gemini batch error filtering behavior. +- Why it changed: + - The user reported the warning `gemini vision partially failed on 3 of 4 batches`. + - The provided log `ai-media-hub-2026-03-17T06-06-04-447Z.log` showed three different batch-failure classes mixed together: + - real Gemini JSON extraction failures + - low-value thumbnail skips + - no-visual candidate skips + - Only the first class should strongly influence the user-facing partial-failure warning. +- How it was verified: + - `go test ./...` + - `bash scripts/selftest.sh` +- What is still risky or incomplete: + - This reduces noisy partial-failure warnings, but it does not eliminate genuine Gemini JSON truncation or provider-side preview problems. + - If the model starts returning different malformed JSON patterns, the parser and warning logic may still need further hardening. + - Date: `2026-03-17` - What changed: - Reapplied the broader-search / modal-fitting codepath from `5ca7aef` as requested by the user. diff --git a/backend/services/gemini_test.go b/backend/services/gemini_test.go index 2e6159c..9980425 100644 --- a/backend/services/gemini_test.go +++ b/backend/services/gemini_test.go @@ -213,3 +213,18 @@ func TestMergeRecommendationsExcludesIrrelevantAndPendingFiller(t *testing.T) { t.Fatalf("unexpected merged result: %#v", merged) } } + +func TestFilterHardGeminiErrorsIgnoresLowValueVisualFailures(t *testing.T) { + errs := []string{ + "candidate thumbnail is low value", + "no candidate thumbnails or preview frames could be fetched for gemini vision", + "gemini vision JSON extraction failed: no complete JSON object found", + } + filtered := filterHardGeminiErrors(errs) + if len(filtered) != 1 { + t.Fatalf("expected only hard errors to remain, got %#v", filtered) + } + if !strings.Contains(filtered[0], "JSON extraction failed") { + t.Fatalf("unexpected filtered errors: %#v", filtered) + } +} diff --git a/backend/services/ranker.go b/backend/services/ranker.go index fcd3a72..fe5b1c7 100644 --- a/backend/services/ranker.go +++ b/backend/services/ranker.go @@ -187,6 +187,7 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s }) } recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize, chunkSize, deadline) + hardErrs := filterHardGeminiErrors(recoveredErrs) if len(recovered) > 0 { stats.SequentialRetried++ stats.Succeeded++ @@ -197,9 +198,9 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s seen[item.Link] = true merged = append(merged, item) } - if len(recoveredErrs) > 0 { + if len(hardErrs) > 0 { stats.Failed++ - for _, recoveredErr := range recoveredErrs { + for _, recoveredErr := range hardErrs { if len(stats.Errors) < 5 { stats.Errors = append(stats.Errors, recoveredErr) } @@ -207,6 +208,9 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s } continue } + if len(hardErrs) == 0 { + continue + } stats.Failed++ if len(stats.Errors) < 5 { stats.Errors = append(stats.Errors, batch.err.Error()) @@ -345,6 +349,35 @@ func MergeGeminiBatchStats(base, extra GeminiBatchStats) GeminiBatchStats { return merged } +func filterHardGeminiErrors(errs []string) []string { + filtered := make([]string, 0, len(errs)) + for _, item := range errs { + if isIgnorableGeminiError(item) { + continue + } + filtered = append(filtered, item) + } + return filtered +} + +func isIgnorableGeminiError(message string) bool { + lower := strings.ToLower(strings.TrimSpace(message)) + if lower == "" { + return false + } + for _, token := range []string{ + "no candidate thumbnails or preview frames could be fetched for gemini vision", + "candidate thumbnail is low value", + "candidate has no thumbnail or preview video", + "image url is empty", + } { + if strings.Contains(lower, token) { + return true + } + } + return false +} + func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex, chunkSize int, deadline time.Time) ([]AIRecommendation, []string) { recovered := make([]AIRecommendation, 0, chunkSize) errs := make([]string, 0, 4)