Revert "Harden gemini vision JSON recovery"
This reverts commit 513199f426.
This commit is contained in:
+15
-132
@@ -256,7 +256,6 @@ func (g *GeminiService) Recommend(query string, candidates []SearchResult) ([]AI
|
||||
"text": `You are a professional video editor. Analyze whether each provided visual is suitable as a usable scene or shot for the user's requested keyword. Return JSON only in this shape:
|
||||
{"recommendations":[{"index":0,"verdict":"Yes","reason":"short reason","recommended":true,"assessment":"positive","searchHint":"short english hint"}]}
|
||||
Return one entry for every analyzed candidate. Use Korean for every reason. Keep reasons concise but specific enough to explain usefulness.
|
||||
Keep each Korean reason very short, ideally one sentence under 24 Korean characters when possible.
|
||||
Set verdict to "Yes" or "No" for every candidate. "Yes" means the scene is usable and relevant for editing against the user's keyword. "No" means it is not suitable or not relevant enough.
|
||||
Set recommended=true only when verdict is "Yes". Set recommended=false when verdict is "No".
|
||||
Set assessment to one of: positive, unclear, irrelevant, inappropriate.
|
||||
@@ -266,7 +265,6 @@ Set assessment to one of: positive, unclear, irrelevant, inappropriate.
|
||||
- inappropriate: low-quality, spammy, misleading, meme-like, or otherwise unsuitable for professional editing
|
||||
When assessment is not positive, provide searchHint as a short English stock-footage search phrase that could help find better candidates. Keep it under 8 words.
|
||||
When assessment is positive, searchHint may be empty.
|
||||
Do not include markdown fences, explanations, or comments. Output compact JSON only.
|
||||
Prefer cinematic b-roll, stock footage, editorial footage, clean composition, usable establishing shots, and professional media thumbnails.
|
||||
Avoid clickbait faces, exaggerated expressions, meme aesthetics, low-information thumbnails, sensational text overlays, or gossip-style imagery.
|
||||
Favor scenes that look directly useful for professional editing, sequencing, establishing, cutaway, or mood-building usage.
|
||||
@@ -308,8 +306,7 @@ User query: ` + query,
|
||||
},
|
||||
"generationConfig": map[string]any{
|
||||
"responseMimeType": "application/json",
|
||||
"temperature": 0.1,
|
||||
"maxOutputTokens": 900,
|
||||
"maxOutputTokens": 1400,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -342,17 +339,23 @@ User query: ` + query,
|
||||
return nil, fmt.Errorf("gemini vision returned no candidates")
|
||||
}
|
||||
|
||||
rawText := payload.Candidates[0].Content.Parts[0].Text
|
||||
parsed, recoveredPartial, err := parseGeminiVisionRecommendations(rawText)
|
||||
jsonText, err := extractJSONObject(payload.Candidates[0].Content.Parts[0].Text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gemini vision JSON extraction failed: %w", err)
|
||||
}
|
||||
if recoveredPartial {
|
||||
g.debug("gemini:vision_partial_json_recovered", map[string]any{
|
||||
"query": query,
|
||||
"candidateCount": len(candidates),
|
||||
"recommendationCount": len(parsed.Recommendations),
|
||||
})
|
||||
|
||||
var parsed struct {
|
||||
Recommendations []struct {
|
||||
Index int `json:"index"`
|
||||
Verdict string `json:"verdict"`
|
||||
Reason string `json:"reason"`
|
||||
Recommended bool `json:"recommended"`
|
||||
Assessment string `json:"assessment"`
|
||||
SearchHint string `json:"searchHint"`
|
||||
} `json:"recommendations"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(jsonText), &parsed); err != nil {
|
||||
return nil, fmt.Errorf("gemini vision JSON parse failed: %w; raw=%q", err, truncateForError(payload.Candidates[0].Content.Parts[0].Text, 200))
|
||||
}
|
||||
|
||||
recommendations := make([]AIRecommendation, 0, len(parsed.Recommendations))
|
||||
@@ -384,126 +387,6 @@ User query: ` + query,
|
||||
return recommendations, nil
|
||||
}
|
||||
|
||||
type geminiVisionParsedPayload struct {
|
||||
Recommendations []struct {
|
||||
Index int `json:"index"`
|
||||
Verdict string `json:"verdict"`
|
||||
Reason string `json:"reason"`
|
||||
Recommended bool `json:"recommended"`
|
||||
Assessment string `json:"assessment"`
|
||||
SearchHint string `json:"searchHint"`
|
||||
} `json:"recommendations"`
|
||||
}
|
||||
|
||||
func parseGeminiVisionRecommendations(raw string) (geminiVisionParsedPayload, bool, error) {
|
||||
jsonText, err := extractJSONObject(raw)
|
||||
if err == nil {
|
||||
var parsed geminiVisionParsedPayload
|
||||
if unmarshalErr := json.Unmarshal([]byte(jsonText), &parsed); unmarshalErr != nil {
|
||||
return geminiVisionParsedPayload{}, false, fmt.Errorf("json parse failed: %w; raw=%q", unmarshalErr, truncateForError(raw, 200))
|
||||
}
|
||||
return parsed, false, nil
|
||||
}
|
||||
|
||||
objects := extractCompleteRecommendationObjects(raw)
|
||||
if len(objects) == 0 {
|
||||
return geminiVisionParsedPayload{}, false, err
|
||||
}
|
||||
|
||||
parsed := geminiVisionParsedPayload{
|
||||
Recommendations: make([]struct {
|
||||
Index int `json:"index"`
|
||||
Verdict string `json:"verdict"`
|
||||
Reason string `json:"reason"`
|
||||
Recommended bool `json:"recommended"`
|
||||
Assessment string `json:"assessment"`
|
||||
SearchHint string `json:"searchHint"`
|
||||
}, 0, len(objects)),
|
||||
}
|
||||
for _, objectText := range objects {
|
||||
var item struct {
|
||||
Index int `json:"index"`
|
||||
Verdict string `json:"verdict"`
|
||||
Reason string `json:"reason"`
|
||||
Recommended bool `json:"recommended"`
|
||||
Assessment string `json:"assessment"`
|
||||
SearchHint string `json:"searchHint"`
|
||||
}
|
||||
if unmarshalErr := json.Unmarshal([]byte(objectText), &item); unmarshalErr != nil {
|
||||
continue
|
||||
}
|
||||
parsed.Recommendations = append(parsed.Recommendations, item)
|
||||
}
|
||||
if len(parsed.Recommendations) == 0 {
|
||||
return geminiVisionParsedPayload{}, false, err
|
||||
}
|
||||
return parsed, true, nil
|
||||
}
|
||||
|
||||
func extractCompleteRecommendationObjects(text string) []string {
|
||||
cleaned := strings.TrimSpace(text)
|
||||
cleaned = strings.TrimPrefix(cleaned, "```json")
|
||||
cleaned = strings.TrimPrefix(cleaned, "```")
|
||||
cleaned = strings.TrimSuffix(cleaned, "```")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
recommendationsIndex := strings.Index(cleaned, `"recommendations"`)
|
||||
if recommendationsIndex == -1 {
|
||||
return nil
|
||||
}
|
||||
arrayStart := strings.Index(cleaned[recommendationsIndex:], "[")
|
||||
if arrayStart == -1 {
|
||||
return nil
|
||||
}
|
||||
arrayStart += recommendationsIndex
|
||||
|
||||
objects := make([]string, 0, 4)
|
||||
inString := false
|
||||
escaped := false
|
||||
objectDepth := 0
|
||||
objectStart := -1
|
||||
|
||||
for idx := arrayStart + 1; idx < len(cleaned); idx++ {
|
||||
ch := cleaned[idx]
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' && inString {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if ch == '"' {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
if inString {
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '{':
|
||||
if objectDepth == 0 {
|
||||
objectStart = idx
|
||||
}
|
||||
objectDepth++
|
||||
case '}':
|
||||
if objectDepth == 0 {
|
||||
continue
|
||||
}
|
||||
objectDepth--
|
||||
if objectDepth == 0 && objectStart >= 0 {
|
||||
objects = append(objects, cleaned[objectStart:idx+1])
|
||||
objectStart = -1
|
||||
}
|
||||
case ']':
|
||||
if objectDepth == 0 {
|
||||
return objects
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
func (g *GeminiService) BuildSupplementalQueries(query string, existing []string, reviewed []AIRecommendation) ([]string, error) {
|
||||
baseExisting := make([]string, 0, len(existing))
|
||||
for _, item := range existing {
|
||||
|
||||
Reference in New Issue
Block a user