This commit is contained in:
@@ -341,6 +341,21 @@
|
||||
- [ ] full browser-level validation was not fully reproducible in this environment
|
||||
|
||||
## Recent Change Log
|
||||
- Date: `2026-03-16`
|
||||
- What changed:
|
||||
- Google Video embed URL now uses `youtube-nocookie` with explicit `origin` to reduce player load failures.
|
||||
- Gemini Vision prompt now forces a `Yes` or `No` verdict per candidate and only `Yes` candidates are merged into the final result set.
|
||||
- Candidate visual fetch now prefers Envato / Artgrid preview video frames before thumbnails and sends a `Referer` header for thumbnail fetches.
|
||||
- Envato / Artgrid enrichment now retries preview extraction once after a short delay when the first fetch still lacks a usable preview URL.
|
||||
- Why it changed:
|
||||
- The user reported YouTube player error `153`, too many fallback-style results, and source pages that appear to need additional loading time before preview URLs become visible.
|
||||
- How it was verified:
|
||||
- log review from `ai-media-hub-2026-03-16T05-05-58-704Z.log`
|
||||
- `go test ./...`
|
||||
- What is still risky or incomplete:
|
||||
- If a YouTube video has embedding disabled by the uploader, no embed URL variant will fully bypass that restriction.
|
||||
- Delay-and-retry HTML fetching cannot execute JavaScript, so pages that only expose preview URLs after full client-side rendering may still need a real browser-based fetch path later.
|
||||
|
||||
- Date: `2026-03-16`
|
||||
- What changed:
|
||||
- Reduced the heaviest search-stage caps slightly: fewer query variants per request, smaller per-source result caps, lower enrichment scope, and a bounded Gemini candidate set.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -386,7 +386,7 @@ function buildResultModalEmbedURL(item) {
|
||||
if (item.source === "Google Video") {
|
||||
const videoId = extractYouTubeID(item.link);
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&origin=${encodeURIComponent(window.location.origin)}`;
|
||||
}
|
||||
}
|
||||
return item.link;
|
||||
|
||||
Reference in New Issue
Block a user