diff --git a/TODO.md b/TODO.md index 92b12c5..159b7df 100644 --- a/TODO.md +++ b/TODO.md @@ -268,6 +268,20 @@ - backend debug broadcasts ## Recent Change Log +- Date: `2026-03-24` +- What changed: + - Relaxed Gemini image-query expansion parsing so loose plain-text numbered lists can still be accepted when the model prepends explanatory text instead of returning a clean JSON object. + - Removed the GIPHY image-mode search meta box from the frontend so the image UI stays visually simpler. + - Stopped surfacing the Gemini image-expansion fallback warning directly in the image-search UI when the backend can still continue with usable fallback queries. +- Why it changed: + - Real log review showed Gemini image expansion sometimes returned text like `Here is the JSON requested`, which triggered fallback even though the model output still contained useful query candidates, and the extra meta box was not adding enough value to justify the space it consumed. +- How it was verified: + - log review of `ai-media-hub-2026-03-24T07-25-42-827Z.log` + - `node --check frontend/app.js` +- What is still risky or incomplete: + - This improves tolerance for one common Gemini formatting deviation, but fully free-form model output can still fall back if it does not contain recoverable query lines. + - Go tests still could not be rerun in this environment because `go` is currently unavailable here. + - Date: `2026-03-24` - What changed: - Removed the redundant `GIPHY Download Dir` variable field from the Unraid template and kept the dedicated `GIPHY Downloads` path mapping as the single user-facing download-path control. diff --git a/backend/services/gemini.go b/backend/services/gemini.go index 0dd6c4e..152f3af 100644 --- a/backend/services/gemini.go +++ b/backend/services/gemini.go @@ -147,6 +147,11 @@ func (g *GeminiService) ExpandImageQueries(query string) ([]string, error) { } jsonText, err := extractJSONObject(rawText) if err != nil { + if looseQueries := parseLooseImageExpansionLines(rawText); len(looseQueries) == 5 { + g.setCachedExpansion(cacheKey, looseQueries, 15*time.Minute) + g.debug("gemini:image_expand_success", map[string]any{"query": trimmed, "queries": looseQueries, "mode": "loose_text"}) + return looseQueries, nil + } g.debug("gemini:image_expand_parse_error", map[string]any{"query": trimmed, "error": err.Error()}) g.setCachedExpansion(cacheKey, fallback, 15*time.Minute) return fallback, err @@ -801,6 +806,34 @@ func truncateForError(text string, limit int) string { return trimmed[:limit] + "..." } +func parseLooseImageExpansionLines(text string) []string { + candidates := make([]string, 0, 8) + for _, line := range strings.Split(text, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + trimmed = strings.TrimPrefix(trimmed, "- ") + trimmed = strings.TrimPrefix(trimmed, "* ") + trimmed = strings.TrimPrefix(trimmed, "1. ") + trimmed = strings.TrimPrefix(trimmed, "2. ") + trimmed = strings.TrimPrefix(trimmed, "3. ") + trimmed = strings.TrimPrefix(trimmed, "4. ") + trimmed = strings.TrimPrefix(trimmed, "5. ") + trimmed = strings.TrimSpace(strings.Trim(trimmed, "\"'`")) + lower := strings.ToLower(trimmed) + if strings.HasPrefix(lower, "here is") || strings.HasPrefix(lower, "json") || strings.HasPrefix(lower, "output") { + continue + } + candidates = append(candidates, trimmed) + } + queries := normalizeImageExpansionQueries(candidates) + if len(queries) < 5 { + return nil + } + return queries[:5] +} + func normalizeKoreanReason(reason string) string { trimmed := strings.TrimSpace(reason) if trimmed == "" { diff --git a/backend/services/gemini_test.go b/backend/services/gemini_test.go index 84eed9c..6a98c15 100644 --- a/backend/services/gemini_test.go +++ b/backend/services/gemini_test.go @@ -145,6 +145,26 @@ func TestExpandImageQueriesFallsBackWhenGeminiFails(t *testing.T) { } } +func TestExpandImageQueriesAcceptsLoosePlainTextList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"Here is the JSON requested\n1. cute cat\n2. cute cat gif\n3. cat reaction gif\n4. cat meme gif\n5. animated cat sticker"}]}}]}`)) + })) + defer server.Close() + + service := NewGeminiService("dummy-key", "gemini-2.5-flash") + service.Client = &http.Client{Timeout: 2 * time.Second} + service.GenerateEndpoint = server.URL + + queries, err := service.ExpandImageQueries("고양이") + if err != nil { + t.Fatalf("expected loose plain-text list to be accepted, got %v", err) + } + if len(queries) != 5 || queries[0] != "cute cat" || queries[4] != "animated cat sticker" { + t.Fatalf("unexpected loose parsed queries: %#v", queries) + } +} + func TestSelectUnevaluatedCandidatesSkipsReviewedLinks(t *testing.T) { ranked := []SearchResult{ {Link: "https://a.example"}, diff --git a/backend/services/giphy.go b/backend/services/giphy.go index 8cb10dc..9fde5ac 100644 --- a/backend/services/giphy.go +++ b/backend/services/giphy.go @@ -168,7 +168,6 @@ func (s *GiphyService) SearchImages(query string, requestedMax int) (GiphySearch expandedQueries, expansionErr := s.expandQueries(response.OriginalQuery) response.ExpandedQueries = expandedQueries if expansionErr != nil { - response.Warning = "Query expansion failed, using fallback search terms." s.debug("giphy:query_expansion_fallback", map[string]any{ "query": response.OriginalQuery, "queries": expandedQueries, diff --git a/frontend/app.js b/frontend/app.js index 79cf3ce..5727213 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -13,10 +13,6 @@ const mediaTypeToggles = Array.from(document.querySelectorAll("[data-media-type- const platformToggles = Array.from(document.querySelectorAll("[data-platform-toggle]")); const imageSearchSandbox = document.getElementById("imageSearchSandbox"); const imagePromptChips = Array.from(document.querySelectorAll("[data-image-prompt]")); -const giphyMetaPanel = document.getElementById("giphyMetaPanel"); -const giphyOriginalQuery = document.getElementById("giphyOriginalQuery"); -const giphyResultCount = document.getElementById("giphyResultCount"); -const giphyExpandedQueries = document.getElementById("giphyExpandedQueries"); const dropzone = document.getElementById("dropzone"); const fileInput = document.getElementById("fileInput"); const uploadResult = document.getElementById("uploadResult"); @@ -103,7 +99,6 @@ const resultPreviewInflight = new Map(); let cardSummaryObserver = null; let activeMediaType = "video"; const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview"; -let activeImageSearchResponse = null; function proxiedPreviewURL(src) { if (!src) { @@ -310,31 +305,6 @@ function renderImageEmptyState(message) { searchResults.innerHTML = `