categorisation for gifs

This commit is contained in:
onysd 2026-08-24 23:09:08 +03:00
parent 5e18bd3830
commit d0669bdc5e
15 changed files with 468 additions and 49 deletions

View file

@ -190,6 +190,59 @@ func (s *Service) AdminSetGifCatalogSortOrder(ctx context.Context, id int64, ord
return true, nil
}
// AdminSetGifCatalogCategory sets (or clears, via "") an entry's category.
func (s *Service) AdminSetGifCatalogCategory(ctx context.Context, id int64, category string) (bool, error) {
if s.gifCatalog == nil {
return false, domain.ErrGifCatalogUnavailable
}
if !domain.ValidGifCatalogCategory(category) {
return false, domain.ErrGifCatalogEntryInvalid
}
changed, err := s.gifCatalog.SetGifCatalogCategory(ctx, id, category)
if err != nil {
return false, err
}
if !changed {
return false, domain.ErrGifCatalogEntryNotFound
}
return true, nil
}
// AdminAutoCategorizeGifCatalog runs ClassifyGifCategory against every
// currently-uncategorized entry's title and assigns whatever category it
// guesses (category stays "" -- i.e. the entry is left for manual tagging --
// when nothing matches). Already-categorized entries are left untouched, so
// this is safe to re-run after every new batch of GIFs lands (seeded or
// admin-uploaded) without clobbering an operator's manual corrections.
// Returns how many entries got a category assigned.
func (s *Service) AdminAutoCategorizeGifCatalog(ctx context.Context) (int, error) {
if s.gifCatalog == nil {
return 0, domain.ErrGifCatalogUnavailable
}
entries, err := s.gifCatalog.ListGifCatalog(ctx, false)
if err != nil {
return 0, err
}
changed := 0
for _, e := range entries {
if e.Category != "" {
continue
}
category := ClassifyGifCategory(e.Title)
if category == "" {
continue
}
ok, err := s.gifCatalog.SetGifCatalogCategory(ctx, e.ID, category)
if err != nil {
return changed, err
}
if ok {
changed++
}
}
return changed, nil
}
// AdminDeleteGifCatalogEntry removes an entry from the catalog. The
// referenced document is left alone.
func (s *Service) AdminDeleteGifCatalogEntry(ctx context.Context, id int64) (bool, error) {

View file

@ -0,0 +1,85 @@
package files
import "strings"
// gifCategoryKeywords maps each domain.GifCatalogCategories entry to a list
// of lowercase words/phrases whose presence in a GIF title is decent
// (imperfect, but reversible -- an operator can always correct it in the
// admin panel) evidence the GIF belongs to that emotional category. Checked
// in this order, first match wins, so more specific/less ambiguous
// categories are listed before catch-alls like Neutral/Silly.
var gifCategoryKeywords = map[string][]string{
"Love": {
"love", "heart", "kiss", "kissing", "crush", "romance", "romantic",
"valentine", "adorable", "hug", "hugging", "xoxo", "sweetheart",
},
"Cheers": {
"party", "celebrate", "celebration", "celebrating", "woohoo", "cheers",
"congrats", "congratulations", "dance", "dancing", "yay", "hooray",
"victory", "champagne", "toast", "birthday", "win", "winning",
},
"Laughter": {
"laugh", "laughing", "lol", "lmao", "haha", "hilarious", "funny",
"joke", "giggle", "chuckle", "rofl", "lulz",
},
"Astonishment": {
"shock", "shocked", "shocking", "omg", "wow", "surprised", "surprise",
"gasp", "whoa", "unbelievable", "stunned", "speechless", "mindblown",
"mind blown", "jawdrop",
},
"Sadness": {
"sad", "sadness", "cry", "crying", "cries", "tears", "depressed",
"heartbroken", "sorrow", "sob", "sobbing", "disappointed", "unhappy",
},
"Anger": {
"angry", "anger", "mad", "rage", "furious", "pissed", "annoyed",
"irritated", "punch", "smash", "yell", "yelling", "scream", "fist",
},
"Disapproval": {
"disgusting", "disgusted", "gross", "eww", "yuck", "ugh", "cringe",
"boo", "nope", "fail",
},
"Approval": {
"agree", "thumbsup", "thumbs up", "nailed it", "well done",
"applause", "clap", "clapping", "bravo", "approved", "salute",
"respect", "nice one",
},
"Doubt": {
"confused", "confusion", "suspicious", "skeptical", "hmm", "hmmm",
"thinking", "uncertain", "unsure", "really",
},
"Silly": {
"silly", "goofy", "derp", "wacky", "ridiculous", "nonsense",
},
"Neutral": {
"meh", "whatever", "shrug", "indifferent", "blank stare", "boring",
},
}
// ClassifyGifCategory guesses one of domain.GifCatalogCategories from a GIF's
// title via keyword matching, or "" if nothing matches. See
// gifCategoryKeywords for the word lists and Service.AdminAutoCategorizeGifCatalog
// for how this gets applied in bulk.
//
// This is deliberately simple substring matching, not NLP: real GIF titles
// range from descriptive ("Sad Tenant") to meaningless filename fragments
// ("jIDL92q") that no text-based heuristic can classify -- those are left
// uncategorized for an operator to tag by hand rather than guessed wrong.
func ClassifyGifCategory(title string) string {
lower := strings.ToLower(title)
for _, category := range gifCategoryOrder {
for _, kw := range gifCategoryKeywords[category] {
if strings.Contains(lower, kw) {
return category
}
}
}
return ""
}
// gifCategoryOrder is gifCategoryKeywords' check order (Go map iteration is
// unordered, and the order is meaningful -- see gifCategoryKeywords' doc).
var gifCategoryOrder = []string{
"Love", "Cheers", "Laughter", "Astonishment", "Sadness", "Anger",
"Disapproval", "Approval", "Doubt", "Silly", "Neutral",
}