package classify import ( "sort" "github.com/photoprism/photoprism/pkg/txt" ) // Labels represents a sortable collection of Label values returned by vision // models, Exif metadata, or user input. type Labels []Label // Len implements sort.Interface for Labels. func (l Labels) Len() int { return len(l) } // Swap implements sort.Interface for Labels. func (l Labels) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Less implements sort.Interface for Labels. Higher-priority labels come first; // for equal priority the lower-uncertainty label wins. Labels with an // uncertainty >= 100 are considered unusable and are ordered last. func (l Labels) Less(i, j int) bool { switch { case l[i].Uncertainty >= 100: return false case l[j].Uncertainty >= 100: return true case l[i].Priority == l[j].Priority: return l[i].Uncertainty < l[j].Uncertainty default: return l[i].Priority > l[j].Priority } } // AppendLabel mirrors append but discards labels with an empty name so callers // do not need to check for that guard condition. func (l Labels) AppendLabel(label Label) Labels { if label.Name == "" { return l } return append(l, label) } // Keywords maps label names and categories to their keyword tokens (using the // txt.Keywords helper) while skipping low-confidence labels and those sourced // from plain text fields (title/caption/keyword). func (l Labels) Keywords() (result []string) { for _, label := range l { if label.Uncertainty >= 100 || label.Source == SrcTitle || label.Source == SrcCaption || label.Source == SrcSubject || label.Source == SrcKeyword { continue } result = append(result, txt.Keywords(label.Name)...) for _, c := range label.Categories { result = append(result, txt.Keywords(c)...) } } return result } // Count returns the number of labels that have a non-empty name and an // uncertainty below 100 (0% confidence cut-off). func (l Labels) Count() (count int) { if l == nil { return 0 } for _, label := range l { if label.Name == "" || label.Uncertainty >= 100 { continue } count++ } return count } // Names returns label names whose uncertainty is less than 100 (0% confidence // cut-off). The order matches the underlying slice. func (l Labels) Names() (s []string) { if l == nil { return s } s = make([]string, 0, l.Count()) for _, label := range l { if label.Name == "" || label.Uncertainty >= 100 { continue } s = append(s, label.Name) } return s } // String returns a human-readable list of label names joined with commas and an // "and" before the final element. When no names are available "none" is // returned to communicate the absence of labels. func (l Labels) String() string { if l == nil { return "none" } return txt.JoinAnd(l.Names()) } // Title selects a suitable title from the labels slice using priority and // uncertainty thresholds. When titles are not available or fail the confidence // checks the provided fallback string is returned instead. func (l Labels) Title(fallback string) string { fallbackRunes := len([]rune(fallback)) // check if given fallback is valid if fallbackRunes < 2 || fallbackRunes > 25 || txt.ContainsNumber(fallback) { fallback = "" } if len(l) == 0 { return fallback } // Sort by priority and uncertainty sort.Sort(l) // Get best label (at the top) label := l[0] // Get second best label in case the first has high uncertainty if len(l) > 1 && l[0].Uncertainty > 60 && l[1].Uncertainty <= 60 { label = l[1] } switch { case fallback != "" && label.Priority < 0: return fallback case fallback != "" && label.Priority == 0 && label.Uncertainty > 50: return fallback case label.Priority >= -1 && label.Uncertainty <= 60: return label.Name default: return fallback } } // IsNSFW reports whether any label marks the asset as "not safe for work" // (NSFW). The threshold is clamped to [0,100] and checked against // NSFWConfidence; explicit NSFW flags always trigger a positive result. func (l Labels) IsNSFW(threshold int) bool { if l == nil || threshold < 0 { return false } else if threshold > 100 { threshold = 100 } for _, label := range l { if label.NSFW || label.NSFWConfidence >= threshold { return true } } return false }