Improve embed handling and Gemini verdict flow
build-push / docker (push) Successful in 4m15s

This commit is contained in:
AI Assistant
2026-03-16 14:09:33 +09:00
parent a37e02aea9
commit 7d9a939633
5 changed files with 64 additions and 51 deletions
+23
View File
@@ -211,6 +211,19 @@ func (s *SearchService) enrichEnvato(result SearchResult) SearchResult {
deriveEnvatoPreviewFromThumbnail(result.ThumbnailURL),
)
}
if result.PreviewVideoURL == "" {
time.Sleep(1200 * time.Millisecond)
if retryHTML, retryErr := s.fetchText(result.Link); retryErr == nil {
result.PreviewVideoURL = firstNonEmpty(
extractJSONLDValue(retryHTML, "contentUrl"),
extractMetaContent(retryHTML, "twitter:player:stream"),
extractVideoPreviewURL(retryHTML),
extractEnvatoPreviewFromHydration(retryHTML),
deriveEnvatoPreviewFromThumbnail(pageThumbnail),
deriveEnvatoPreviewFromThumbnail(result.ThumbnailURL),
)
}
}
return result
}
@@ -264,6 +277,16 @@ func (s *SearchService) enrichArtgrid(result SearchResult) SearchResult {
extractVideoPreviewURL(html),
)
}
if result.PreviewVideoURL == "" {
time.Sleep(1200 * time.Millisecond)
if retryHTML, retryErr := s.fetchText(result.Link); retryErr == nil {
result.PreviewVideoURL = firstNonEmpty(
extractJSONLDValue(retryHTML, "contentUrl"),
extractMetaContent(retryHTML, "twitter:player:stream"),
extractVideoPreviewURL(retryHTML),
)
}
}
}
}
+25 -25
View File
@@ -153,9 +153,10 @@ func (g *GeminiService) Recommend(query string, candidates []SearchResult) ([]AI
parts := []geminiPart{
{
"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,"reason":"short reason","recommended":true}]}
{"recommendations":[{"index":0,"verdict":"Yes","reason":"short reason","recommended":true}]}
Return one entry for every analyzed candidate. Use Korean for every reason. Keep reasons concise but specific enough to explain usefulness.
Mark the strongest matches as recommended=true and weaker matches as recommended=false.
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".
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.
@@ -226,6 +227,7 @@ User query: ` + query,
var parsed struct {
Recommendations []struct {
Index int `json:"index"`
Verdict string `json:"verdict"`
Reason string `json:"reason"`
Recommended bool `json:"recommended"`
} `json:"recommendations"`
@@ -240,6 +242,7 @@ User query: ` + query,
continue
}
src := candidates[rec.Index]
recommended := rec.Recommended || strings.EqualFold(strings.TrimSpace(rec.Verdict), "yes")
recommendations = append(recommendations, AIRecommendation{
Title: src.Title,
Link: src.Link,
@@ -248,7 +251,7 @@ User query: ` + query,
PreviewVideoURL: src.PreviewVideoURL,
Source: src.Source,
Reason: normalizeKoreanReason(rec.Reason),
Recommended: rec.Recommended,
Recommended: recommended,
})
}
@@ -259,31 +262,19 @@ User query: ` + query,
return recommendations, nil
}
func fetchImageAsInlineData(client *http.Client, imageURL string) (string, string, error) {
func fetchImageAsInlineData(client *http.Client, imageURL, referer string) (string, string, error) {
if strings.TrimSpace(imageURL) == "" {
return "", "", fmt.Errorf("image url is empty")
}
resp, err := client.Get(imageURL)
if err == nil {
defer resp.Body.Close()
req, reqErr := newBrowserStyleImageRequest(imageURL, referer)
if reqErr != nil {
return "", "", reqErr
}
if err != nil || resp.StatusCode >= 300 {
req, reqErr := newBrowserStyleImageRequest(imageURL)
if reqErr != nil {
if err != nil {
return "", "", err
}
return "", "", reqErr
}
if resp != nil {
resp.Body.Close()
}
resp, err = client.Do(req)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
resp, err := client.Do(req)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return "", "", fmt.Errorf("thumbnail fetch failed with %d", resp.StatusCode)
@@ -302,7 +293,7 @@ func fetchImageAsInlineData(client *http.Client, imageURL string) (string, strin
return base64.StdEncoding.EncodeToString(data), mimeType, nil
}
func newBrowserStyleImageRequest(imageURL string) (*http.Request, error) {
func newBrowserStyleImageRequest(imageURL, referer string) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, imageURL, nil)
if err != nil {
return nil, err
@@ -310,12 +301,21 @@ func newBrowserStyleImageRequest(imageURL string) (*http.Request, error) {
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
if strings.TrimSpace(referer) != "" {
req.Header.Set("Referer", referer)
}
return req, nil
}
func fetchCandidateVisualInlineData(client *http.Client, candidate SearchResult) (string, string, error) {
if candidate.PreviewVideoURL != "" && (candidate.Source == "Envato" || candidate.Source == "Artgrid") {
data, mimeType, err := extractFrameFromVideo(candidate.PreviewVideoURL)
if err == nil {
return data, mimeType, nil
}
}
if candidate.ThumbnailURL != "" {
data, mimeType, err := fetchImageAsInlineData(client, candidate.ThumbnailURL)
data, mimeType, err := fetchImageAsInlineData(client, candidate.ThumbnailURL, candidate.Link)
if err == nil {
return data, mimeType, nil
}
-25
View File
@@ -302,31 +302,6 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
seen[item.Link] = true
merged = append(merged, item)
}
for _, item := range recommended {
if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit {
continue
}
seen[item.Link] = true
merged = append(merged, item)
}
for _, item := range ranked {
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: GeminiFallbackReason,
Recommended: false,
})
}
return merged
}