Tighten source filters and add platform toggles
build-push / docker (push) Successful in 4m28s

This commit is contained in:
AI Assistant
2026-03-13 16:03:40 +09:00
parent 27000dbf28
commit ad8afd5f92
4 changed files with 119 additions and 16 deletions
+57 -5
View File
@@ -248,7 +248,8 @@ func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath s
func (a *App) searchMedia(c *gin.Context) {
var req struct {
Query string `json:"query"`
Query string `json:"query"`
Platforms []string `json:"platforms"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -265,8 +266,9 @@ func (a *App) searchMedia(c *gin.Context) {
queryVariants = []string{req.Query}
}
enabledPlatforms := normalizePlatforms(req.Platforms)
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "searching Google Video, Envato, and Artgrid", "progress": 35})
results, err := a.SearchService.SearchMedia(queryVariants)
results, err := a.SearchService.SearchMedia(queryVariants, enabledPlatforms)
if err != nil {
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search failed", "progress": 100, "message": err.Error()})
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
@@ -285,9 +287,12 @@ func (a *App) searchMedia(c *gin.Context) {
rankQuery = strings.Join(queryVariants[:min(len(queryVariants), 3)], " ")
}
scored := rankSearchResults(rankQuery, results)
shortlist := scored[:min(len(scored), 10)]
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing shortlisted thumbnails with Gemini Vision", "progress": 75})
recommended, err := a.GeminiService.Recommend(req.Query, shortlist)
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
recommended := evaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
err = nil
if len(recommended) == 0 {
err = fmt.Errorf("gemini vision returned no recommended items across all candidate batches")
}
if err != nil {
fallback := make([]services.AIRecommendation, 0, min(20, len(scored)))
for _, result := range scored[:min(20, len(scored))] {
@@ -345,6 +350,53 @@ func min(a, b int) int {
return b
}
func normalizePlatforms(platforms []string) map[string]bool {
if len(platforms) == 0 {
return map[string]bool{
"envato": true,
"artgrid": true,
"google video": true,
}
}
normalized := map[string]bool{}
for _, item := range platforms {
switch strings.ToLower(strings.TrimSpace(item)) {
case "envato":
normalized["envato"] = true
case "artgrid":
normalized["artgrid"] = true
case "google video", "google_video", "google":
normalized["google video"] = true
}
}
return normalized
}
func evaluateAllCandidatesWithGemini(service *services.GeminiService, query string, ranked []services.SearchResult) []services.AIRecommendation {
const chunkSize = 8
merged := make([]services.AIRecommendation, 0, len(ranked))
seen := map[string]bool{}
for start := 0; start < len(ranked); start += chunkSize {
end := start + chunkSize
if end > len(ranked) {
end = len(ranked)
}
batch := ranked[start:end]
recommended, err := service.Recommend(query, batch)
if err != nil {
continue
}
for _, item := range recommended {
if item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, item)
}
}
return merged
}
func rankSearchResults(query string, results []services.SearchResult) []services.SearchResult {
queryTerms := strings.Fields(strings.ToLower(query))
positiveTerms := []string{
+25 -7
View File
@@ -45,7 +45,7 @@ func NewSearchService(baseURL, googleVideoEngine, webEngine string) *SearchServi
}
}
func (s *SearchService) SearchMedia(queries []string) ([]SearchResult, error) {
func (s *SearchService) SearchMedia(queries []string, enabledPlatforms map[string]bool) ([]SearchResult, error) {
if s.BaseURL == "" {
return nil, fmt.Errorf("searxng base url is not configured")
}
@@ -93,6 +93,9 @@ func (s *SearchService) SearchMedia(queries []string) ([]SearchResult, error) {
continue
}
for _, source := range sources {
if len(enabledPlatforms) > 0 && !enabledPlatforms[strings.ToLower(source.name)] {
continue
}
for _, searchQuery := range source.build(base) {
items, err := s.search(searchQuery, source.categories, source.engine, source.name)
if err != nil {
@@ -205,6 +208,9 @@ func (s *SearchService) enrichArtgrid(result SearchResult) SearchResult {
extractMetaContent(html, "og:image"),
extractMetaContent(html, "twitter:image"),
)
if result.ThumbnailURL == "" {
result.ThumbnailURL = extractArtgridBackgroundThumbnail(html, clipID)
}
}
if result.PreviewVideoURL == "" {
result.PreviewVideoURL = extractVideoPreviewURL(html)
@@ -283,7 +289,7 @@ func buildGoogleVideoQueries(base string) []string {
func buildEnvatoQueries(base string) []string {
return []string{
fmt.Sprintf(`"%s" ("stock footage" OR "stock video" OR "b-roll" OR cinematic) site:elements.envato.com`, base),
fmt.Sprintf(`"%s" ("stock footage" OR "stock video" OR "b-roll" OR cinematic) site:videohive.net/item`, base),
fmt.Sprintf(`"%s" ("stock footage" OR "stock video" OR "b-roll" OR cinematic) site:elements.envato.com/stock-video`, base),
}
}
@@ -295,6 +301,10 @@ func buildArtgridQueries(base string) []string {
}
func isUsefulGoogleVideoResult(result SearchResult) bool {
lowerLink := strings.ToLower(result.Link)
if !(strings.Contains(lowerLink, "youtube.com/watch") || strings.Contains(lowerLink, "youtu.be/") || strings.Contains(lowerLink, "youtube.com/shorts/")) {
return false
}
text := strings.ToLower(result.Title + " " + result.Snippet)
for _, banned := range []string{
"tutorial", "how to", "review", "reaction", "podcast", "interview", "walkthrough",
@@ -315,11 +325,8 @@ func isRenderableEnvatoResult(result SearchResult) bool {
}
host := strings.ToLower(parsed.Host)
path := strings.Trim(parsed.Path, "/")
if strings.Contains(host, "videohive.net") {
return strings.HasPrefix(path, "item/") && len(strings.Split(path, "/")) >= 2
}
if strings.Contains(host, "elements.envato.com") {
if path == "" || strings.Contains(path, "/") {
if path == "" || strings.Contains(path, "/stock-video") || strings.Contains(path, "/video-templates") {
return false
}
return regexp.MustCompile(`-[A-Z0-9]{6,}$`).MatchString(path)
@@ -407,13 +414,24 @@ func extractVideoPreviewURL(html string) string {
candidate := strings.ReplaceAll(match, `\/`, `/`)
candidate = strings.ReplaceAll(candidate, `\u002F`, `/`)
candidate = strings.ReplaceAll(candidate, `\\`, "")
if strings.Contains(strings.ToLower(candidate), "preview") || strings.Contains(strings.ToLower(candidate), "video") {
if strings.Contains(strings.ToLower(candidate), "preview") || strings.Contains(strings.ToLower(candidate), "video") || strings.Contains(strings.ToLower(candidate), "watermark") {
return candidate
}
}
return ""
}
func extractArtgridBackgroundThumbnail(html, clipID string) string {
pattern := regexp.MustCompile(`https://[^"'\\s>]+(?:artgrid\.imgix\.net|cms-public-artifacts\.artlist\.io|artlist-content-images\.imgix\.net)[^"'\\s>]+(?:jpeg|jpg|png|webp)`)
matches := pattern.FindAllString(html, -1)
for _, match := range matches {
if strings.Contains(match, clipID) || strings.Contains(strings.ToLower(match), "graded-thumbnail") {
return match
}
}
return ""
}
func extractArtgridClipID(link string) string {
matches := regexp.MustCompile(`/clip/([0-9]+)/`).FindStringSubmatch(link)
if len(matches) == 2 {