This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,14 @@ type GeminiService struct {
|
||||
GenerateEndpoint string
|
||||
TranslateEndpoint string
|
||||
Debug func(message string, data any)
|
||||
cacheMu sync.Mutex
|
||||
visualCache map[string]cachedVisualData
|
||||
}
|
||||
|
||||
type cachedVisualData struct {
|
||||
data string
|
||||
mimeType string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type AIRecommendation struct {
|
||||
@@ -46,6 +55,7 @@ func NewGeminiService(apiKey string) *GeminiService {
|
||||
Client: &http.Client{Timeout: 40 * time.Second},
|
||||
GenerateEndpoint: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
|
||||
TranslateEndpoint: "https://translate.googleapis.com/translate_a/single",
|
||||
visualCache: map[string]cachedVisualData{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +197,7 @@ User query: ` + query,
|
||||
maxImages := min(len(candidates), 10)
|
||||
visualCount := 0
|
||||
for idx := 0; idx < maxImages; idx++ {
|
||||
img, mimeType, err := fetchCandidateVisualInlineData(g.Client, candidates[idx])
|
||||
img, mimeType, err := g.fetchCandidateVisualInlineData(candidates[idx])
|
||||
if err != nil {
|
||||
g.debug("gemini:vision_candidate_visual_error", map[string]any{
|
||||
"index": idx,
|
||||
@@ -330,6 +340,32 @@ func fetchImageAsInlineData(client *http.Client, imageURL, referer string) (stri
|
||||
return base64.StdEncoding.EncodeToString(data), mimeType, nil
|
||||
}
|
||||
|
||||
func (g *GeminiService) getCachedVisual(key string) (string, string, bool) {
|
||||
g.cacheMu.Lock()
|
||||
defer g.cacheMu.Unlock()
|
||||
|
||||
entry, ok := g.visualCache[key]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
delete(g.visualCache, key)
|
||||
return "", "", false
|
||||
}
|
||||
return entry.data, entry.mimeType, true
|
||||
}
|
||||
|
||||
func (g *GeminiService) setCachedVisual(key, data, mimeType string, ttl time.Duration) {
|
||||
g.cacheMu.Lock()
|
||||
defer g.cacheMu.Unlock()
|
||||
|
||||
g.visualCache[key] = cachedVisualData{
|
||||
data: data,
|
||||
mimeType: mimeType,
|
||||
expiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
}
|
||||
|
||||
func newBrowserStyleImageRequest(imageURL, referer string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, imageURL, nil)
|
||||
if err != nil {
|
||||
@@ -344,21 +380,40 @@ func newBrowserStyleImageRequest(imageURL, referer string) (*http.Request, error
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func fetchCandidateVisualInlineData(client *http.Client, candidate SearchResult) (string, string, error) {
|
||||
func (g *GeminiService) fetchCandidateVisualInlineData(candidate SearchResult) (string, string, error) {
|
||||
if candidate.PreviewVideoURL != "" && (candidate.Source == "Envato" || candidate.Source == "Artgrid") {
|
||||
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
||||
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||
return data, mimeType, nil
|
||||
}
|
||||
data, mimeType, err := extractFrameFromVideo(candidate.PreviewVideoURL)
|
||||
if err == nil {
|
||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||
return data, mimeType, nil
|
||||
}
|
||||
}
|
||||
if candidate.ThumbnailURL != "" {
|
||||
data, mimeType, err := fetchImageAsInlineData(client, candidate.ThumbnailURL, candidate.Link)
|
||||
cacheKey := "image\n" + candidate.ThumbnailURL
|
||||
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||
return data, mimeType, nil
|
||||
}
|
||||
data, mimeType, err := fetchImageAsInlineData(g.Client, candidate.ThumbnailURL, candidate.Link)
|
||||
if err == nil {
|
||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||
return data, mimeType, nil
|
||||
}
|
||||
}
|
||||
if candidate.PreviewVideoURL != "" {
|
||||
return extractFrameFromVideo(candidate.PreviewVideoURL)
|
||||
cacheKey := "frame\n" + candidate.PreviewVideoURL
|
||||
if data, mimeType, ok := g.getCachedVisual(cacheKey); ok {
|
||||
return data, mimeType, nil
|
||||
}
|
||||
data, mimeType, err := extractFrameFromVideo(candidate.PreviewVideoURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
g.setCachedVisual(cacheKey, data, mimeType, 10*time.Minute)
|
||||
return data, mimeType, nil
|
||||
}
|
||||
return "", "", fmt.Errorf("candidate has no thumbnail or preview video")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user