This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user