Reduce noisy Gemini partial failure counts
build-push / docker (push) Successful in 4m8s

This commit is contained in:
AI Assistant
2026-03-17 15:09:23 +09:00
parent 58d54a0338
commit 75f1bb360c
3 changed files with 70 additions and 2 deletions
+20
View File
@@ -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.
+15
View File
@@ -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)
}
}
+35 -2
View File
@@ -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)