Improve translated search flow and modal layout
build-push / docker (push) Successful in 4m19s

This commit is contained in:
AI Assistant
2026-03-13 11:57:58 +09:00
parent 1fc06fb785
commit 6852e07607
4 changed files with 186 additions and 70 deletions
+100 -14
View File
@@ -39,9 +39,11 @@ func NewGeminiService(apiKey string) *GeminiService {
func (g *GeminiService) ExpandQuery(query string) ([]string, error) {
if g.APIKey == "" {
return fallbackQueryExpansion(query), nil
return fallbackQueryExpansion(query, query), nil
}
englishBase := g.TranslateQuery(query)
body := map[string]any{
"systemInstruction": map[string]any{
"parts": []map[string]string{
@@ -55,12 +57,13 @@ func (g *GeminiService) ExpandQuery(query string) ([]string, error) {
"parts": []map[string]string{
{
"text": `Return JSON only in this shape: {"querywords":["..."]}.
Generate at most 10 concise search variations for media discovery across Google Video, Envato, and Artgrid.
If the user query is in Korean, include strong English search variants that a stock footage editor would use.
Generate at most 10 concise English search variations for media discovery across Google Video, Envato, and Artgrid.
The queries must be usable directly in English search engines for stock footage discovery.
Prioritize media, video footage, stock footage, cinematic b-roll, editorial footage, and scene-based search terms.
Avoid celebrity gossip, reaction-style phrasing, clickbait phrasing, and generic web search wording.
Mix Korean and English when useful, but make sure several queries are clean English production keywords.
User query: ` + query,
Do not output Korean unless it is part of a proper noun.
Original user query: ` + query + `
English base translation: ` + englishBase,
},
},
},
@@ -86,7 +89,7 @@ User query: ` + query,
rawText, err := g.generateText(body)
if err != nil {
return fallbackQueryExpansion(query), nil
return fallbackQueryExpansion(query, englishBase), nil
}
jsonText, err := extractJSONObject(rawText)
@@ -108,8 +111,9 @@ Output must start with { and end with }.
Do not add prose, explanations, markdown, code fences, or labels.
Return exactly this shape: {"querywords":["..."]}.
Generate up to 10 search queries for media discovery across Google Video, Envato, and Artgrid.
If the original query is Korean, include strong English stock-footage search phrases.
User query: ` + query,
Every query must be in natural English and suitable for stock-footage search.
Original user query: ` + query + `
English base translation: ` + englishBase,
},
},
},
@@ -134,20 +138,20 @@ User query: ` + query,
}
rawText, retryErr := g.generateText(strictBody)
if retryErr != nil {
return fallbackQueryExpansion(query), nil
return fallbackQueryExpansion(query, englishBase), nil
}
jsonText, err = extractJSONObject(rawText)
if err != nil {
return fallbackQueryExpansion(query), nil
return fallbackQueryExpansion(query, englishBase), nil
}
}
var parsed QueryExpansion
if err := json.Unmarshal([]byte(jsonText), &parsed); err != nil {
return fallbackQueryExpansion(query), nil
return fallbackQueryExpansion(query, englishBase), nil
}
queries := fallbackQueryExpansion(query)
queries := fallbackQueryExpansion(query, englishBase)
seen := map[string]bool{}
for _, existing := range queries {
seen[strings.ToLower(strings.TrimSpace(existing))] = true
@@ -167,6 +171,47 @@ User query: ` + query,
return queries, nil
}
func (g *GeminiService) TranslateQuery(query string) string {
if strings.TrimSpace(query) == "" || looksMostlyASCII(query) || g.APIKey == "" {
return strings.TrimSpace(query)
}
body := map[string]any{
"systemInstruction": map[string]any{
"parts": []map[string]string{
{
"text": "You translate media search intents into natural English. Output one plain English search phrase only. No labels, no quotes, no explanations.",
},
},
},
"contents": []map[string]any{
{
"parts": []map[string]string{
{
"text": "Translate this user query into concise English suitable for stock-footage search: " + query,
},
},
},
},
"generationConfig": map[string]any{
"responseMimeType": "text/plain",
"temperature": 0.1,
"maxOutputTokens": 40,
},
}
rawText, err := g.generateText(body)
if err != nil {
return strings.TrimSpace(query)
}
translated := sanitizePlainEnglishLine(rawText)
if translated == "" {
return strings.TrimSpace(query)
}
return translated
}
func (g *GeminiService) generateText(body map[string]any) (string, error) {
rawBody, _ := json.Marshal(body)
endpoint := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + g.APIKey
@@ -402,8 +447,11 @@ func truncateForError(text string, limit int) string {
return trimmed[:limit] + "..."
}
func fallbackQueryExpansion(query string) []string {
base := strings.TrimSpace(query)
func fallbackQueryExpansion(originalQuery, englishQuery string) []string {
base := strings.TrimSpace(englishQuery)
if base == "" {
base = strings.TrimSpace(originalQuery)
}
candidates := []string{
base,
base + " b-roll",
@@ -416,6 +464,9 @@ func fallbackQueryExpansion(query string) []string {
base + " 4k footage",
base + " cinematic b-roll",
}
if strings.TrimSpace(originalQuery) != "" && !strings.EqualFold(strings.TrimSpace(originalQuery), strings.TrimSpace(englishQuery)) {
candidates = append(candidates, strings.TrimSpace(originalQuery))
}
seen := map[string]bool{}
queries := make([]string, 0, len(candidates))
@@ -433,3 +484,38 @@ func fallbackQueryExpansion(query string) []string {
}
return queries
}
func sanitizePlainEnglishLine(text string) string {
lines := strings.Split(text, "\n")
for _, line := range lines {
line = strings.TrimSpace(strings.Trim(line, "\"'`"))
if line == "" {
continue
}
lower := strings.ToLower(line)
for _, prefix := range []string{"translation:", "english:", "translated query:"} {
if strings.HasPrefix(lower, prefix) {
line = strings.TrimSpace(line[len(prefix):])
lower = strings.ToLower(line)
}
}
if strings.HasPrefix(lower, "here is") || strings.HasPrefix(lower, "the translation") {
continue
}
if line != "" {
return line
}
}
return ""
}
func looksMostlyASCII(text string) bool {
ascii := 0
runes := []rune(text)
for _, r := range runes {
if r <= 127 {
ascii++
}
}
return ascii >= len(runes)*8/10
}