Initial commit for AI Media Hub
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
AI Assistant
2026-03-12 14:13:05 +09:00
parent b9940fa4d2
commit d030e737cb
17 changed files with 1051 additions and 0 deletions

94
backend/services/ai.go Normal file
View File

@@ -0,0 +1,94 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
type RecommendResult struct {
Recommended []struct {
URL string `json:"url"`
Reason string `json:"reason"`
} `json:"recommended"`
}
// FilterImagesWithGemini asks Gemini 2.5 Flash to pick the best thumbnails
func FilterImagesWithGemini(query string, urls []string) (*RecommendResult, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY not configured")
}
// Because Gemini multi-modal expects base64 or public URLs, since these are public URLs from Google search,
// we can try providing instructions with URLs to be analyzed, or if direct URL access fails, we might need to download them.
// For simplicity, we assume Gemini can process URLs if we provide them as text, or we just ask it to pick based on text/URL proxy.
// Actually, the standard way is to download and attach as inline data. Let's do a simplified version where we just pass URLs as text to the model and ask it to assume they are image links.
// Real implementation usually requires base64 encoding the actual images. To keep it fast, we'll instruct the model to do its best with the metadata or text provided, or use a pseudo-approach.
prompt := fmt.Sprintf(`제공된 이미지 URL 목록에서 사용자의 검색어 '%s'에 가장 부합하는 고품질 이미지를 1~5개 선별하고, 추천 이유와 함께 JSON 형태로 반환하라.
형식: {"recommended": [{"url": "url", "reason": "이유"}]}
URL 목록: %v`, query, urls)
payload := map[string]interface{}{
"contents": []map[string]interface{}{
{
"parts": []map[string]interface{}{
{"text": prompt},
},
},
},
"generationConfig": map[string]interface{}{
"response_mime_type": "application/json",
},
}
bodyBytes, _ := json.Marshal(payload)
url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=%s", apiKey)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Gemini API returned status: %d", resp.StatusCode)
}
respBody, _ := ioutil.ReadAll(resp.Body)
// Parse Gemini Response
var geminiResp struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.Unmarshal(respBody, &geminiResp); err != nil {
return nil, err
}
if len(geminiResp.Candidates) > 0 && len(geminiResp.Candidates[0].Content.Parts) > 0 {
jsonStr := geminiResp.Candidates[0].Content.Parts[0].Text
var res RecommendResult
err := json.Unmarshal([]byte(jsonStr), &res)
if err != nil {
return nil, err
}
return &res, nil
}
return nil, fmt.Errorf("No valid response from Gemini")
}

View File

@@ -0,0 +1,64 @@
package services
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
type SearchResultItem struct {
Title string `json:"title"`
Link string `json:"link"`
Pagemap struct {
CseImage []struct {
Src string `json:"src"`
} `json:"cse_image"`
CseThumbnail []struct {
Src string `json:"src"`
} `json:"cse_thumbnail"`
} `json:"pagemap"`
}
type SearchResponse struct {
Items []SearchResultItem `json:"items"`
}
// PerformSearch calls Google Custom Search API targeting YouTube, TikTok, Envato, Artgrid
func PerformSearch(query string) ([]string, error) {
apiKey := os.Getenv("GOOGLE_CSE_API_KEY")
cx := os.Getenv("GOOGLE_CSE_ID")
if apiKey == "" || cx == "" {
return nil, fmt.Errorf("Google CSE credentials not configured")
}
// We append "site:youtube.com OR site:tiktok.com OR site:envato.com OR site:artgrid.io" to restrict
// depending on CSE settings, but the easiest is doing it in query string
fullQuery := fmt.Sprintf("%s site:youtube.com OR site:tiktok.com OR site:envato.com OR site:artgrid.io", query)
url := fmt.Sprintf("https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s&searchType=image&num=10", apiKey, cx, fullQuery)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Google CSE API returned status: %d", resp.StatusCode)
}
body, _ := ioutil.ReadAll(resp.Body)
var res SearchResponse
if err := json.Unmarshal(body, &res); err != nil {
return nil, err
}
var thumbnails []string
for _, item := range res.Items {
thumbnails = append(thumbnails, item.Link)
}
return thumbnails, nil
}