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 = `
${message}
`; } -function renderExpandedQueries(queries = []) { - giphyExpandedQueries.innerHTML = ""; - for (const item of queries) { - const chip = document.createElement("span"); - chip.className = "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-zinc-300"; - chip.textContent = item; - giphyExpandedQueries.appendChild(chip); - } -} - -function updateImageSearchMeta(data = null) { - activeImageSearchResponse = data; - const visible = Boolean(data); - setHidden(giphyMetaPanel, !visible, "block"); - if (!visible) { - giphyOriginalQuery.textContent = "Original query: -"; - giphyResultCount.textContent = "0 results"; - giphyExpandedQueries.innerHTML = ""; - return; - } - giphyOriginalQuery.textContent = `Original query: ${data.originalQuery || "-"}`; - giphyResultCount.textContent = `${Number(data.total || 0)} results`; - renderExpandedQueries(data.expandedQueries || []); -} - function renderImageResults(items = []) { searchResults.innerHTML = ""; searchResults.classList.remove("xl:grid-cols-3"); @@ -362,7 +332,6 @@ function applyMediaTypeUI() { const isImageMode = activeMediaType === "image"; syncMediaTypeButtons(); setHidden(imageSearchSandbox, !isImageMode, "block"); - setHidden(giphyMetaPanel, true, "block"); setHidden(queryVariants, true, ""); showWarning(""); searchResultsViewport.classList.toggle("image-results-scroll", isImageMode); @@ -376,7 +345,6 @@ function applyMediaTypeUI() { button.classList.toggle("hidden", isImageMode); } if (isImageMode) { - updateImageSearchMeta(null); setStatus("giphy image mode", 0); renderImageEmptyState("GIPHY 검색어를 입력하면 여기에 최대 100개의 이미지/GIF 결과가 표시됩니다."); } else { @@ -930,13 +898,11 @@ searchForm.addEventListener("submit", async (event) => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: searchQuery.value, maxResults: 100 }), }); - updateImageSearchMeta(data); renderImageResults(data.items || []); - showWarning(data.warning || ""); + showWarning(""); logEvent("image-search:completed", { query: data.originalQuery || searchQuery.value, total: data.total || 0, expandedQueries: data.expandedQueries || [] }); setStatus("giphy search complete", 100); } catch (error) { - updateImageSearchMeta(null); renderImageEmptyState("GIPHY 검색 결과를 불러오지 못했습니다."); showWarning(error.message); setStatus("giphy search failed", 100); diff --git a/frontend/index.html b/frontend/index.html index 5f4044b..18a9b1c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -69,19 +69,6 @@ -