169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package embeddings
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
)
|
|
|
|
const (
|
|
maxOverviewRunes = 1000
|
|
maxKeywords = 5
|
|
)
|
|
|
|
func truncateRunes(s string, limit int) string {
|
|
if limit <= 0 {
|
|
return ""
|
|
}
|
|
|
|
count := 0
|
|
for i := range s {
|
|
if count == limit {
|
|
return s[:i]
|
|
}
|
|
count++
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// BuildEmbeddingText constructs a structured text representation of a media item
|
|
// suitable for generating embeddings. Leads with semantic content (genres + overview)
|
|
// to prevent title-word dominance in the embedding space.
|
|
func BuildEmbeddingText(item *models.MediaItem) string {
|
|
var parts []string
|
|
|
|
// Lead with genres + type + overview for semantic dominance.
|
|
typeName := "movie"
|
|
if item.Type == "series" {
|
|
typeName = "TV series"
|
|
}
|
|
|
|
if len(item.Genres) > 0 && item.Overview != "" {
|
|
overview := truncateRunes(item.Overview, maxOverviewRunes)
|
|
parts = append(parts, fmt.Sprintf("%s %s about %s", strings.Join(item.Genres, ", "), typeName, overview))
|
|
} else if len(item.Genres) > 0 {
|
|
parts = append(parts, fmt.Sprintf("%s %s", strings.Join(item.Genres, ", "), typeName))
|
|
} else if item.Overview != "" {
|
|
overview := truncateRunes(item.Overview, maxOverviewRunes)
|
|
parts = append(parts, fmt.Sprintf("%s. %s", typeName, overview))
|
|
}
|
|
|
|
// Title with year — present but no longer leading.
|
|
if item.Year > 0 {
|
|
parts = append(parts, fmt.Sprintf("%s (%d)", item.Title, item.Year))
|
|
} else {
|
|
parts = append(parts, item.Title)
|
|
}
|
|
|
|
if item.ContentRating != "" {
|
|
parts = append(parts, fmt.Sprintf("Rated %s", item.ContentRating))
|
|
}
|
|
|
|
if item.Tagline != "" {
|
|
parts = append(parts, fmt.Sprintf(`"%s"`, item.Tagline))
|
|
}
|
|
|
|
// Top-billed cast with character names (up to 5).
|
|
var actors []models.ItemPerson
|
|
for _, p := range item.People {
|
|
if p.Kind == models.PersonKindActor {
|
|
actors = append(actors, p)
|
|
}
|
|
}
|
|
sortItemPeople(actors)
|
|
if len(actors) > 0 {
|
|
credits := make([]string, 0, 5)
|
|
for i, p := range actors {
|
|
if i >= 5 {
|
|
break
|
|
}
|
|
if p.Character != "" {
|
|
credits = append(credits, fmt.Sprintf("%s as %s", p.Name, p.Character))
|
|
} else {
|
|
credits = append(credits, p.Name)
|
|
}
|
|
}
|
|
parts = append(parts, fmt.Sprintf("Cast: %s", strings.Join(credits, ", ")))
|
|
}
|
|
|
|
// Director(s).
|
|
var directors []models.ItemPerson
|
|
for _, p := range item.People {
|
|
if p.Kind == models.PersonKindDirector {
|
|
directors = append(directors, p)
|
|
}
|
|
}
|
|
sortItemPeople(directors)
|
|
if len(directors) > 0 {
|
|
parts = append(parts, fmt.Sprintf("Directed by %s", strings.Join(itemPersonNames(directors), ", ")))
|
|
}
|
|
|
|
// Writer(s).
|
|
var writers []models.ItemPerson
|
|
for _, p := range item.People {
|
|
if p.Kind == models.PersonKindWriter {
|
|
writers = append(writers, p)
|
|
}
|
|
}
|
|
sortItemPeople(writers)
|
|
if len(writers) > 0 {
|
|
parts = append(parts, fmt.Sprintf("Written by %s", strings.Join(itemPersonNames(writers), ", ")))
|
|
}
|
|
|
|
if len(item.Keywords) > 0 {
|
|
keywords := item.Keywords
|
|
if len(keywords) > maxKeywords {
|
|
keywords = keywords[:maxKeywords]
|
|
}
|
|
parts = append(parts, fmt.Sprintf("Keywords: %s", strings.Join(keywords, ", ")))
|
|
}
|
|
|
|
if item.OriginalLanguage != "" {
|
|
parts = append(parts, fmt.Sprintf("Original language: %s", item.OriginalLanguage))
|
|
}
|
|
|
|
if len(item.Studios) > 0 {
|
|
parts = append(parts, fmt.Sprintf("Studios: %s", strings.Join(item.Studios, ", ")))
|
|
}
|
|
|
|
if len(item.Networks) > 0 {
|
|
parts = append(parts, fmt.Sprintf("Network: %s", strings.Join(item.Networks, ", ")))
|
|
}
|
|
|
|
if len(item.Countries) > 0 {
|
|
countries := item.Countries
|
|
if len(countries) > 2 {
|
|
countries = countries[:2]
|
|
}
|
|
parts = append(parts, fmt.Sprintf("Country: %s", strings.Join(countries, ", ")))
|
|
}
|
|
|
|
return strings.ToValidUTF8(strings.Join(parts, ". "), "")
|
|
}
|
|
|
|
func sortItemPeople(people []models.ItemPerson) {
|
|
sort.SliceStable(people, func(i, j int) bool {
|
|
if people[i].SortOrder != people[j].SortOrder {
|
|
return people[i].SortOrder < people[j].SortOrder
|
|
}
|
|
if people[i].Name != people[j].Name {
|
|
return people[i].Name < people[j].Name
|
|
}
|
|
if people[i].Character != people[j].Character {
|
|
return people[i].Character < people[j].Character
|
|
}
|
|
return people[i].ID < people[j].ID
|
|
})
|
|
}
|
|
|
|
func itemPersonNames(people []models.ItemPerson) []string {
|
|
names := make([]string, 0, len(people))
|
|
for _, p := range people {
|
|
names = append(names, p.Name)
|
|
}
|
|
return names
|
|
}
|