From 1be409d65473ef0e9f2e937509660dee9f1f98e3 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 2 Sep 2021 11:12:42 +0200 Subject: [PATCH] People: Add SubjectNames() to file entity #22 --- internal/entity/file.go | 35 +++++++---- internal/entity/file_json.go | 83 ++++++++++++++++++++++++++ internal/entity/file_json_test.go | 13 ++++ internal/entity/file_test.go | 14 ++++- internal/entity/marker.go | 11 ++++ internal/entity/marker_fixtures.go | 3 + internal/entity/marker_json.go | 24 ++++---- internal/entity/marker_json_test.go | 15 +++++ internal/entity/marker_test.go | 12 +--- internal/entity/markers.go | 20 ++++++- internal/entity/photo.go | 10 +--- internal/entity/photo_test.go | 2 +- internal/entity/photo_yaml_test.go | 6 +- internal/photoprism/index_mediafile.go | 4 +- 14 files changed, 202 insertions(+), 50 deletions(-) create mode 100644 internal/entity/file_json.go create mode 100644 internal/entity/file_json_test.go create mode 100644 internal/entity/marker_json_test.go diff --git a/internal/entity/file.go b/internal/entity/file.go index 5ae106dfe..f253d512b 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -69,7 +69,7 @@ type File struct { DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"` Share []FileShare `json:"-" yaml:"-"` Sync []FileSync `json:"-" yaml:"-"` - Markers Markers `json:"Markers,omitempty" yaml:"-"` + markers *Markers } type FileInfos struct { @@ -254,7 +254,7 @@ func (m *File) Create() error { return err } - if err := m.Markers.Save(m.FileUID); err != nil { + if err := m.Markers().Save(m.FileUID); err != nil { log.Errorf("file: %s (create markers for %s)", err, m.FileUID) return err } @@ -282,7 +282,7 @@ func (m *File) Save() error { return err } - if err := m.Markers.Save(m.FileUID); err != nil { + if err := m.Markers().Save(m.FileUID); err != nil { log.Errorf("file: %s (save markers for %s)", err, m.FileUID) return err } @@ -406,15 +406,18 @@ func (m *File) AddFaces(faces face.Faces) { // AddFace adds a face marker to the file. func (m *File) AddFace(f face.Face, subjectUID string) { - marker := NewFaceMarker(f, m.FileUID, subjectUID) - if !m.Markers.Contains(*marker) { - m.Markers = append(m.Markers, *marker) + marker := *NewFaceMarker(f, m.FileUID, subjectUID) + + if markers := m.Markers(); !markers.Contains(marker) { + markers.Append(marker) } } // FaceCount returns the current number of valid faces detected. func (m *File) FaceCount() (c int) { - if err := Db().Model(Marker{}).Where("file_uid = ? AND marker_invalid = 0", m.FileUID). + if err := Db().Model(Marker{}). + Where("file_uid = ? AND marker_type = ?", m.FileUID, MarkerFace). + Where("marker_invalid = 0"). Count(&c).Error; err != nil { log.Errorf("file: %s (count faces)", err) return 0 @@ -423,11 +426,23 @@ func (m *File) FaceCount() (c int) { } } -// PreloadMarkers loads existing file markers. -func (m *File) PreloadMarkers() { +// Markers finds and returns existing file markers. +func (m *File) Markers() *Markers { + if m.markers != nil { + return m.markers + } + if res, err := FindMarkers(m.FileUID); err != nil { log.Warnf("file: %s (load markers)", err) + m.markers = &Markers{} } else { - m.Markers = res + m.markers = &res } + + return m.markers +} + +// SubjectNames returns all known subject names. +func (m *File) SubjectNames() []string { + return m.Markers().SubjectNames() } diff --git a/internal/entity/file_json.go b/internal/entity/file_json.go new file mode 100644 index 000000000..db37ab6a4 --- /dev/null +++ b/internal/entity/file_json.go @@ -0,0 +1,83 @@ +package entity + +import ( + "encoding/json" + "time" +) + +// MarshalJSON returns the JSON encoding. +func (m *File) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + UID string + PhotoUID string + InstanceID string `json:",omitempty"` + Name string + Root string + OriginalName string `json:",omitempty"` + Hash string + Size int64 + Codec string `json:",omitempty"` + Type string + Mime string `json:",omitempty"` + Primary bool + Sidecar bool `json:",omitempty"` + Missing bool `json:",omitempty"` + Portrait bool `json:",omitempty"` + Video bool `json:",omitempty"` + Duration time.Duration `json:",omitempty"` + Width int `json:",omitempty"` + Height int `json:",omitempty"` + Orientation int `json:",omitempty"` + Projection string `json:",omitempty"` + AspectRatio float32 `json:",omitempty"` + MainColor string `json:",omitempty"` + Colors string `json:",omitempty"` + Luminance string `json:",omitempty"` + Diff uint32 `json:",omitempty"` + Chroma uint8 `json:",omitempty"` + Error string `json:",omitempty"` + ModTime int64 `json:",omitempty"` + CreatedAt time.Time `json:",omitempty"` + CreatedIn int64 `json:",omitempty"` + UpdatedAt time.Time `json:",omitempty"` + UpdatedIn int64 `json:",omitempty"` + DeletedAt *time.Time `json:",omitempty"` + Markers *Markers `json:",omitempty"` + }{ + UID: m.FileUID, + PhotoUID: m.PhotoUID, + InstanceID: m.InstanceID, + Name: m.FileName, + Root: m.FileRoot, + OriginalName: m.OriginalName, + Hash: m.FileHash, + Size: m.FileSize, + Codec: m.FileCodec, + Type: m.FileType, + Mime: m.FileMime, + Primary: m.FilePrimary, + Sidecar: m.FileSidecar, + Missing: m.FileMissing, + Portrait: m.FilePortrait, + Video: m.FileVideo, + Duration: m.FileDuration, + Width: m.FileWidth, + Height: m.FileHeight, + Orientation: m.FileOrientation, + Projection: m.FileProjection, + AspectRatio: m.FileAspectRatio, + MainColor: m.FileMainColor, + Colors: m.FileColors, + Luminance: m.FileLuminance, + Diff: m.FileDiff, + Chroma: m.FileChroma, + Error: m.FileError, + ModTime: m.ModTime, + CreatedAt: m.CreatedAt, + CreatedIn: m.CreatedIn, + UpdatedAt: m.UpdatedAt, + UpdatedIn: m.UpdatedIn, + DeletedAt: m.DeletedAt, + Markers: m.Markers(), + }) +} diff --git a/internal/entity/file_json_test.go b/internal/entity/file_json_test.go new file mode 100644 index 000000000..89e88f29b --- /dev/null +++ b/internal/entity/file_json_test.go @@ -0,0 +1,13 @@ +package entity + +import "testing" + +func TestFile_MarshalJSON(t *testing.T) { + if m := FileFixtures.Pointer("Video.mp4"); m == nil { + t.Fatal("must not be nil") + } else if j, err := m.MarshalJSON(); err != nil { + t.Fatal(err) + } else { + t.Logf("json: %s", j) + } +} diff --git a/internal/entity/file_test.go b/internal/entity/file_test.go index 824c64a39..c5b68f5aa 100644 --- a/internal/entity/file_test.go +++ b/internal/entity/file_test.go @@ -446,7 +446,7 @@ func TestFile_AddFaces(t *testing.T) { assert.Equal(t, false, file.FileMissing) assert.NotEmpty(t, file.FileUID) - assert.NotEmpty(t, file.Markers) + assert.NotEmpty(t, file.Markers()) }) } @@ -489,3 +489,15 @@ func TestFile_Rename(t *testing.T) { assert.Equal(t, "27900704_070228_D6D51B6C", p.PhotoName) }) } + +func TestFile_SubjectNames(t *testing.T) { + f := FileFixtures.Get("Video.mp4") + names := f.SubjectNames() + + assert.Len(t, names, 1) + if len(names) != 1 { + t.Fatal("there should be one name") + } else { + assert.Equal(t, "Actress A", names[0]) + } +} diff --git a/internal/entity/marker.go b/internal/entity/marker.go index 30819ffc0..0043c5bee 100644 --- a/internal/entity/marker.go +++ b/internal/entity/marker.go @@ -331,6 +331,17 @@ func (m *Marker) Embeddings() Embeddings { return m.embeddings } +// SubjectName returns the matching subject's name. +func (m *Marker) SubjectName() string { + if m.MarkerName != "" { + return m.MarkerName + } else if s := m.Subject(); s != nil { + return s.SubjectName + } + + return "" +} + // Subject returns the matching subject or nil. func (m *Marker) Subject() (subj *Subject) { if m.subject != nil { diff --git a/internal/entity/marker_fixtures.go b/internal/entity/marker_fixtures.go index 7ce1909ae..a5ab18903 100644 --- a/internal/entity/marker_fixtures.go +++ b/internal/entity/marker_fixtures.go @@ -194,6 +194,7 @@ var MarkerFixtures = MarkerMap{ MarkerUID: "mt9k3pw1wowuy999", FileUID: "ft71s39w45bnlqdw", FaceID: FaceFixtures.Get("actress-1").ID, + FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-178-559-68", FaceDist: 0.26852392873736236, SubjectSrc: SrcManual, SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID, @@ -213,6 +214,7 @@ var MarkerFixtures = MarkerMap{ MarkerUID: "mt9k3pw1wowu1000", FileUID: "ft72s39w45bnlqdw", FaceID: FaceFixtures.Get("actress-1").ID, + FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-1-2-3", FaceDist: 0.4507357278575355, SubjectSrc: "", SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID, @@ -232,6 +234,7 @@ var MarkerFixtures = MarkerMap{ MarkerUID: "mt9k3pw1wowu1001", FileUID: "ft2es39q45bnlqd0", FaceID: FaceFixtures.Get("actress-1").ID, + FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-4-5-6", FaceDist: 0.5099754448545762, SubjectSrc: "", SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID, diff --git a/internal/entity/marker_json.go b/internal/entity/marker_json.go index 58e9bf596..1a38e906e 100644 --- a/internal/entity/marker_json.go +++ b/internal/entity/marker_json.go @@ -22,18 +22,18 @@ func (m *Marker) MarshalJSON() ([]byte, error) { Type string Src string Name string - SubjectUID string - SubjectSrc string `json:",omitempty"` - FaceID string `json:",omitempty"` - FaceThumb string X float32 Y float32 W float32 H float32 - Size int - Score int - Review bool - Invalid bool + SubjectUID string `json:",omitempty"` + SubjectSrc string `json:",omitempty"` + FaceID string `json:",omitempty"` + FaceThumb string `json:",omitempty"` + Size int `json:",omitempty"` + Score int `json:",omitempty"` + Review bool `json:",omitempty"` + Invalid bool `json:",omitempty"` CreatedAt time.Time }{ UID: m.MarkerUID, @@ -41,14 +41,14 @@ func (m *Marker) MarshalJSON() ([]byte, error) { Type: m.MarkerType, Src: m.MarkerSrc, Name: name, - SubjectUID: m.SubjectUID, - SubjectSrc: m.SubjectSrc, - FaceID: m.FaceID, - FaceThumb: m.FaceThumb, X: m.X, Y: m.Y, W: m.W, H: m.H, + SubjectUID: m.SubjectUID, + SubjectSrc: m.SubjectSrc, + FaceID: m.FaceID, + FaceThumb: m.FaceThumb, Size: m.Size, Score: m.Score, Review: m.Review, diff --git a/internal/entity/marker_json_test.go b/internal/entity/marker_json_test.go new file mode 100644 index 000000000..affc277da --- /dev/null +++ b/internal/entity/marker_json_test.go @@ -0,0 +1,15 @@ +package entity + +import ( + "testing" +) + +func TestMarker_MarshalJSON(t *testing.T) { + if m := MarkerFixtures.Pointer("actor-a-2"); m == nil { + t.Fatal("must not be nil") + } else if j, err := m.MarshalJSON(); err != nil { + t.Fatal(err) + } else { + t.Logf("json: %s", j) + } +} diff --git a/internal/entity/marker_test.go b/internal/entity/marker_test.go index afdee760e..669beb98c 100644 --- a/internal/entity/marker_test.go +++ b/internal/entity/marker_test.go @@ -12,16 +12,6 @@ func TestMarker_TableName(t *testing.T) { assert.Contains(t, m.TableName(), "markers") } -func TestMarker_MarshalJSON(t *testing.T) { - if m := MarkerFixtures.Pointer("actor-a-2"); m == nil { - t.Fatal("must not be nil") - } else if j, err := m.MarshalJSON(); err != nil { - t.Fatal(err) - } else { - t.Logf("json: %s", j) - } -} - func TestNewMarker(t *testing.T) { m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556) assert.IsType(t, &Marker{}, m) @@ -193,7 +183,7 @@ func TestMarker_Save(t *testing.T) { p := PhotoFixtures.Get("19800101_000002_D640C559") assert.Empty(t, p.Files) - p.PreloadFiles(true) + p.PreloadFiles() assert.NotEmpty(t, p.Files) // t.Logf("FILES: %#v", p.Files) diff --git a/internal/entity/markers.go b/internal/entity/markers.go index 21dcbe641..d9b60b0be 100644 --- a/internal/entity/markers.go +++ b/internal/entity/markers.go @@ -41,13 +41,31 @@ func (m Markers) FaceCount() (faces int) { return faces } +// SubjectNames returns known subject names. +func (m Markers) SubjectNames() (names []string) { + for _, marker := range m { + if marker.MarkerInvalid || marker.MarkerType != MarkerFace { + continue + } else if n := marker.SubjectName(); n != "" { + names = append(names, n) + } + } + + return names +} + +// Append adds a marker. +func (m *Markers) Append(marker Marker) { + *m = append(*m, marker) +} + // FindMarkers returns up to 1000 markers for a given file uid. func FindMarkers(fileUID string) (Markers, error) { m := Markers{} err := Db(). Where(`file_uid = ?`, fileUID). - Order("marker_uid"). + Order("x"). Offset(0).Limit(1000). Find(&m).Error diff --git a/internal/entity/photo.go b/internal/entity/photo.go index 36d31afdc..2a2413372 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -444,7 +444,7 @@ func (m *Photo) IndexKeywords() error { } // PreloadFiles prepares gorm scope to retrieve photo file -func (m *Photo) PreloadFiles(markers bool) { +func (m *Photo) PreloadFiles() { q := Db(). Table("files"). Select(`files.*`). @@ -452,12 +452,6 @@ func (m *Photo) PreloadFiles(markers bool) { Order("files.file_name DESC") logError(q.Scan(&m.Files)) - - if markers { - for i := range m.Files { - m.Files[i].PreloadMarkers() - } - } } // PreloadKeywords prepares gorm scope to retrieve photo keywords @@ -485,7 +479,7 @@ func (m *Photo) PreloadAlbums() { // PreloadMany prepares gorm scope to retrieve photo file, albums and keywords func (m *Photo) PreloadMany() { - m.PreloadFiles(true) + m.PreloadFiles() m.PreloadKeywords() m.PreloadAlbums() } diff --git a/internal/entity/photo_test.go b/internal/entity/photo_test.go index fef3973d0..b1d23129f 100644 --- a/internal/entity/photo_test.go +++ b/internal/entity/photo_test.go @@ -145,7 +145,7 @@ func TestPhoto_PreloadFiles(t *testing.T) { t.Run("success", func(t *testing.T) { m := PhotoFixtures.Get("Photo01") assert.Empty(t, m.Files) - m.PreloadFiles(false) + m.PreloadFiles() assert.NotEmpty(t, m.Files) }) } diff --git a/internal/entity/photo_yaml_test.go b/internal/entity/photo_yaml_test.go index bafdc6e71..38ebabf6c 100644 --- a/internal/entity/photo_yaml_test.go +++ b/internal/entity/photo_yaml_test.go @@ -11,7 +11,7 @@ import ( func TestPhoto_Yaml(t *testing.T) { t.Run("create from fixture", func(t *testing.T) { m := PhotoFixtures.Get("Photo01") - m.PreloadFiles(true) + m.PreloadFiles() result, err := m.Yaml() if err != nil { @@ -25,7 +25,7 @@ func TestPhoto_Yaml(t *testing.T) { func TestPhoto_SaveAsYaml(t *testing.T) { t.Run("create from fixture", func(t *testing.T) { m := PhotoFixtures.Get("Photo01") - m.PreloadFiles(true) + m.PreloadFiles() fileName := filepath.Join(os.TempDir(), ".photoprism_test.yml") @@ -46,7 +46,7 @@ func TestPhoto_SaveAsYaml(t *testing.T) { func TestPhoto_YamlFileName(t *testing.T) { t.Run("create from fixture", func(t *testing.T) { m := PhotoFixtures.Get("Photo01") - m.PreloadFiles(false) + m.PreloadFiles() assert.Equal(t, "xxx/2790/02/yyy/Photo01.yml", m.YamlFileName("xxx", "yyy")) if err := os.RemoveAll("xxx"); err != nil { diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index b4ad34d02..54511aaed 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -603,13 +603,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.AddLabels(classify.FaceLabels(faces, entity.SrcImage)) - file.PreloadMarkers() - if len(faces) > 0 { file.AddFaces(faces) } - photo.PhotoFaces = file.Markers.FaceCount() + photo.PhotoFaces = file.Markers().FaceCount() } labels := photo.ClassifyLabels()