Backend: Improve labels, keywords and caching

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-05-28 21:20:42 +02:00
parent 4783183790
commit a77b2431d3
21 changed files with 129 additions and 31 deletions

View File

@@ -72,6 +72,9 @@ build-js:
build-go:
rm -f $(BINARY_NAME)
scripts/build.sh debug $(BINARY_NAME)
build-race:
rm -f $(BINARY_NAME)
scripts/build.sh race $(BINARY_NAME)
build-static:
rm -f $(BINARY_NAME)
scripts/build.sh static $(BINARY_NAME)

View File

@@ -43,7 +43,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
if cached {
if cacheData, ok := gc.Get(cacheKey); ok {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
c.JSON(http.StatusOK, cacheData.(FoldersResponse))
c.JSON(http.StatusOK, cacheData.(*FoldersResponse))
return
}
}
@@ -65,7 +65,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
}
if cached {
gc.Set(cacheKey, resp, time.Minute*5)
gc.Set(cacheKey, &resp, time.Minute*5)
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
}

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
@@ -114,11 +115,11 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
if label.LabelSrc == entity.SrcManual {
entity.Db().Delete(&label)
if label.LabelSrc == classify.SrcManual || label.LabelSrc == classify.SrcKeyword{
logError("label", entity.Db().Delete(&label).Error)
} else {
label.Uncertainty = 100
entity.Db().Save(&label)
logError("label", entity.Db().Save(&label).Error)
}
p, err := query.PhotoPreloadByUID(c.Param("uid"))
@@ -128,6 +129,8 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
logError("label", p.RemoveKeyword(label.Label.LabelName))
if err := p.Save(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return

View File

@@ -51,7 +51,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
if cacheData, ok := gc.Get(cacheKey); ok {
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
cached := cacheData.(ThumbCache)
cached := cacheData.(*ThumbCache)
if !fs.FileExists(cached.FileName) {
log.Errorf("thumbnail: %s not found", fileHash)
@@ -137,7 +137,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
}
// Cache thumbnail filename.
gc.Set(cacheKey, ThumbCache{thumbnail, f.ShareFileName()}, time.Hour*24)
gc.Set(cacheKey, &ThumbCache{thumbnail, f.ShareFileName()}, time.Hour*24)
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
if c.Query("download") != "" {

View File

@@ -6,4 +6,5 @@ const (
SrcManual = "manual"
SrcLocation = "location"
SrcImage = "image"
SrcKeyword = "keyword"
)

View File

@@ -13,9 +13,9 @@ type Labels []Label
func (l Labels) Len() int { return len(l) }
func (l Labels) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l Labels) Less(i, j int) bool {
if l[i].Uncertainty >= 100 {
if l[i].Uncertainty >= 100 || l[i].Source == SrcKeyword {
return false
} else if l[j].Uncertainty >= 100 {
} else if l[j].Uncertainty >= 100 || l[j].Source == SrcKeyword {
return true
} else if l[i].Priority == l[j].Priority {
return l[i].Uncertainty < l[j].Uncertainty
@@ -36,7 +36,7 @@ func (l Labels) AppendLabel(label Label) Labels {
// Keywords returns all keywords contains in Labels and their categories
func (l Labels) Keywords() (result []string) {
for _, label := range l {
if label.Uncertainty >= 100 {
if label.Uncertainty >= 100 || label.Source == SrcKeyword {
continue
}

View File

@@ -94,6 +94,19 @@ func FirstOrCreateLabel(m *Label) *Label {
return m
}
// FindLabel returns an existing row if exists.
func FindLabel(s string) *Label {
labelSlug := slug.Make(txt.Clip(s, txt.ClipSlug))
result := Label{}
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).First(&result).Error; err == nil {
return &result
}
return nil
}
// AfterCreate sets the New column used for database callback
func (m *Label) AfterCreate(scope *gorm.Scope) error {
m.New = true

View File

@@ -102,12 +102,16 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
model.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
}
if err := model.SyncKeywordLabels(); err != nil {
log.Errorf("photo: %s", err)
}
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
log.Warn(err)
}
if err := model.IndexKeywords(); err != nil {
log.Error(err)
log.Errorf("photo: %s", err.Error())
}
edited := time.Now().UTC()
@@ -146,7 +150,7 @@ func (m *Photo) Save() error {
}
if err := m.IndexKeywords(); err != nil {
log.Error(err)
log.Errorf("photo: %s", err.Error())
}
m.PhotoQuality = m.QualityScore()
@@ -216,10 +220,39 @@ func (m *Photo) BeforeSave(scope *gorm.Scope) error {
return nil
}
// RemoveKeyword removes a word from photo keywords.
func (m *Photo) RemoveKeyword(w string) error {
if !m.DetailsLoaded() {
return fmt.Errorf("can't remove keyword, details not loaded")
}
words := txt.RemoveFromWords(txt.Words(m.Details.Keywords), w)
m.Details.Keywords = strings.Join(words, ", ")
return nil
}
// SyncKeywordLabels maintains the label / photo relationship for existing labels and keywords.
func (m *Photo) SyncKeywordLabels() error {
keywords := txt.UniqueKeywords(m.Details.Keywords)
var labelIds []uint
for _, w := range keywords {
if label := FindLabel(w); label != nil {
labelIds = append(labelIds, label.ID)
FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, label.ID, 5, classify.SrcKeyword))
}
}
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcKeyword, m.ID, labelIds).Delete(&PhotoLabel{}).Error
}
// IndexKeywords adds given keywords to the photo entry
func (m *Photo) IndexKeywords() error {
if !m.DetailsLoaded() {
return fmt.Errorf("photo: can't index keywords, details not loaded for %s", m.PhotoUID)
return fmt.Errorf("can't index keywords, details not loaded")
}
db := Db()
@@ -240,7 +273,7 @@ func (m *Photo) IndexKeywords() error {
kw := FirstOrCreateKeyword(NewKeyword(w))
if kw == nil {
log.Errorf("photo: index keyword should not be nil - bug?")
log.Errorf("index keyword should not be nil - bug?")
continue
}

View File

@@ -59,7 +59,7 @@ func (m *Photo) Maintain() error {
}
if err := m.IndexKeywords(); err != nil {
log.Error(err)
log.Errorf("photo: %s", err.Error())
}
m.PhotoQuality = m.QualityScore()

View File

@@ -54,9 +54,9 @@ func FindLocation(id string) (result Location, err error) {
if hit, ok := cache.Get(id); ok {
log.Debugf("api: cache hit for lat %f, lng %f (%s)", lat, lng, ApiName)
result = hit.(Location)
result.Cached = true
return result, nil
cached := hit.(*Location)
cached.Cached = true
return *cached, nil
}
url := fmt.Sprintf(ReverseLookupURL, id)
@@ -101,7 +101,7 @@ func FindLocation(id string) (result Location, err error) {
return result, fmt.Errorf("api: no result for %s (%s)", id, ApiName)
}
cache.Set(id, result, gc.DefaultExpiration)
cache.Set(id, &result, gc.DefaultExpiration)
result.Cached = false

View File

@@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"github.com/karrick/godirwalk"
@@ -249,10 +248,6 @@ func (c *Convert) ToJpeg(image *MediaFile, hidden bool) (*MediaFile, error) {
return nil, err
}
// Unclear if this is really necessary here, but safe is safe.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if useMutex {
// Make sure only one command is executed at a time.
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file

View File

@@ -462,8 +462,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
return result
}
if err := photo.SyncKeywordLabels(); err != nil {
log.Errorf("index: %s for %s", err, quotedName)
}
if err := photo.IndexKeywords(); err != nil {
log.Errorf("%s for %s", err, quotedName)
log.Errorf("index: %s for %s", err, quotedName)
}
} else {
if photo.PhotoQuality >= 0 {

View File

@@ -636,6 +636,10 @@ func (m *MediaFile) decodeDimensions() error {
if m.IsJpeg() {
file, err := os.Open(m.FileName())
if err != nil || file == nil {
return err
}
defer file.Close()
size, _, err := image.DecodeConfig(file)

View File

@@ -33,7 +33,8 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
Joins(`JOIN files ON files.photo_id = photos.id AND
files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
Where("photos.deleted_at IS NULL").
Where("photos.photo_lat <> 0")
Where("photos.photo_lat <> 0").
Group("photos.id, files.id")
f.Query = txt.Clip(f.Query, txt.ClipKeyword)

View File

@@ -38,7 +38,8 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
Joins("JOIN cameras ON photos.camera_id = cameras.id").
Joins("JOIN lenses ON photos.lens_id = lenses.id").
Joins("JOIN places ON photos.place_uid = places.place_uid").
Where("files.file_type = 'jpg' OR files.file_video = 1")
Where("files.file_type = 'jpg' OR files.file_video = 1").
Group("photos.id, files.id")
// Shortcut for known photo ids.
if f.ID != "" {

View File

@@ -180,7 +180,7 @@ func (c Client) CreateDir(dir string) error {
func (c Client) Upload(from, to string) error {
file, err := os.Open(from)
if err != nil {
if err != nil || file == nil {
return err
}

View File

@@ -10,8 +10,6 @@ func InitDatabase(port uint, password string) error {
db, err := sql.Open("mysql", fmt.Sprintf("root:@tcp(localhost:%d)/", port))
defer db.Close()
if err != nil {
log.Debug(err.Error())
log.Debug("database login as root with password")
@@ -23,6 +21,8 @@ func InitDatabase(port uint, password string) error {
return err
}
defer db.Close()
if password != "" {
log.Debug("set database password")

View File

@@ -31,11 +31,12 @@ func Zip(filename string, files []string) error {
}
func AddToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
// Get the file information
@@ -64,6 +65,7 @@ func AddToZip(zipWriter *zip.Writer, filename string) error {
// Extract Zip file in destination directory
func Unzip(src, dest string) (fileNames []string, err error) {
r, err := zip.OpenReader(src)
if err != nil {
return fileNames, err
}

View File

@@ -72,6 +72,28 @@ func UniqueWords(words []string) (results []string) {
return results
}
// RemoveFromWords removes words from a string slice and returns the sorted result.
func RemoveFromWords(words []string, remove string) (results []string) {
remove = strings.ToLower(remove)
last := ""
SortCaseInsensitive(words)
for _, w := range words {
w = strings.ToLower(w)
if len(w) < 3 || w == last || strings.Contains(remove, w){
continue
}
last = w
results = append(results, w)
}
return results
}
// UniqueKeywords returns a slice of unique and sorted keywords without stopwords.
func UniqueKeywords(s string) (results []string) {
last := ""

View File

@@ -126,3 +126,14 @@ func TestUniqueKeywords(t *testing.T) {
assert.Equal(t, []string(nil), result)
})
}
func TestRemoveFromWords(t *testing.T) {
t.Run("brown apple", func(t *testing.T) {
result := RemoveFromWords([]string{"lazy", "jpg", "Brown", "apple", "brown", "new-york", "JPG"}, "brown apple")
assert.Equal(t, []string{"jpg", "lazy", "new-york"}, result)
})
t.Run("empty", func(t *testing.T) {
result := RemoveFromWords([]string{"lazy", "jpg", "Brown", "apple"}, "")
assert.Equal(t, []string{"apple", "brown", "jpg", "lazy"}, result)
})
}

View File

@@ -30,6 +30,11 @@ if [[ $1 == "debug" ]]; then
go build -ldflags "-X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}-DEBUG" -o $2 cmd/photoprism/photoprism.go
du -h $2
echo "Done."
elif [[ $1 == "race" ]]; then
echo "Building with data race detector..."
go build -race -ldflags "-X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}-DEBUG" -o $2 cmd/photoprism/photoprism.go
du -h $2
echo "Done."
elif [[ $1 == "static" ]]; then
echo "Building static production binary..."
go build -a -v -ldflags "-linkmode external -extldflags \"-static -L /usr/lib -ltensorflow\" -s -w -X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}" -o $2 cmd/photoprism/photoprism.go