People: Add "photoprism faces index" command for indexing faces only #22
This commit is contained in:
parent
25f35c598d
commit
4dd09f4502
|
@ -9,7 +9,8 @@ import (
|
||||||
// Labels is list of MediaFile labels.
|
// Labels is list of MediaFile labels.
|
||||||
type Labels []Label
|
type Labels []Label
|
||||||
|
|
||||||
// Implements functions for the Sort Interface. Default Labels sort is by priority and uncertainty
|
// Labels implement the sort interface to sort by priority and uncertainty.
|
||||||
|
|
||||||
func (l Labels) Len() int { return len(l) }
|
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) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||||
func (l Labels) Less(i, j int) bool {
|
func (l Labels) Less(i, j int) bool {
|
||||||
|
|
|
@ -11,16 +11,13 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackupCommand configures the backup cli command.
|
// BackupCommand configures the backup cli command.
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CleanUpCommand registers the cleanup command.
|
// CleanUpCommand registers the cleanup command.
|
||||||
|
|
|
@ -6,8 +6,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigCommand registers the display config cli command.
|
// ConfigCommand registers the display config cli command.
|
||||||
|
|
|
@ -3,9 +3,10 @@ package commands
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/pkg/capture"
|
"github.com/photoprism/photoprism/pkg/capture"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigCommand(t *testing.T) {
|
func TestConfigCommand(t *testing.T) {
|
||||||
|
|
|
@ -6,11 +6,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertCommand registers the convert cli command.
|
// ConvertCommand registers the convert cli command.
|
||||||
|
|
|
@ -7,10 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyCommand registers the copy cli command.
|
// CopyCommand registers the copy cli command.
|
||||||
|
|
|
@ -2,15 +2,19 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/urfave/cli"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FacesCommand registers the faces cli command.
|
// FacesCommand registers the faces cli command.
|
||||||
|
@ -25,7 +29,7 @@ var FacesCommand = cli.Command{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "audit",
|
Name: "audit",
|
||||||
Usage: "Conducts a data integrity audit",
|
Usage: "Scans the index for issues",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "fix, f",
|
Name: "fix, f",
|
||||||
|
@ -35,26 +39,38 @@ var FacesCommand = cli.Command{
|
||||||
Action: facesAuditAction,
|
Action: facesAuditAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "reset",
|
Name: "reset",
|
||||||
Usage: "Resets recognized faces",
|
Usage: "Removes people and faces",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "force, f",
|
||||||
|
Usage: "remove all people and faces",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: facesResetAction,
|
Action: facesResetAction,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "index",
|
||||||
|
Usage: "Searches originals for faces",
|
||||||
|
ArgsUsage: "[path]",
|
||||||
|
Action: facesIndexAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match",
|
||||||
|
Usage: "Performs face clustering and matching",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "force, f",
|
||||||
|
Usage: "update all faces",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: facesMatchAction,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "optimize",
|
Name: "optimize",
|
||||||
Usage: "Optimizes face clusters",
|
Usage: "Optimizes face clusters",
|
||||||
Action: facesOptimizeAction,
|
Action: facesOptimizeAction,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "update",
|
|
||||||
Usage: "Performs facial recognition",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "force, f",
|
|
||||||
Usage: "update existing faces",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: facesUpdateAction,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +138,10 @@ func facesAuditAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
// facesResetAction resets face clusters and matches.
|
// facesResetAction resets face clusters and matches.
|
||||||
func facesResetAction(ctx *cli.Context) error {
|
func facesResetAction(ctx *cli.Context) error {
|
||||||
|
if ctx.Bool("force") {
|
||||||
|
return facesResetAllAction(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
actionPrompt := promptui.Prompt{
|
actionPrompt := promptui.Prompt{
|
||||||
Label: "Remove automatically recognized faces, matches, and dangling subjects?",
|
Label: "Remove automatically recognized faces, matches, and dangling subjects?",
|
||||||
IsConfirm: true,
|
IsConfirm: true,
|
||||||
|
@ -160,6 +180,142 @@ func facesResetAction(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// facesResetAllAction removes all people, faces, and face markers.
|
||||||
|
func facesResetAllAction(ctx *cli.Context) error {
|
||||||
|
actionPrompt := promptui.Prompt{
|
||||||
|
Label: "Permanently delete all people and faces?",
|
||||||
|
IsConfirm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := actionPrompt.Run(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
conf := config.NewConfig(ctx)
|
||||||
|
service.SetConfig(conf)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.InitDb()
|
||||||
|
|
||||||
|
if err := query.RemovePeopleAndFaces(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
log.Infof("completed in %s", elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// facesIndexAction searches originals for faces.
|
||||||
|
func facesIndexAction(ctx *cli.Context) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
conf := config.NewConfig(ctx)
|
||||||
|
service.SetConfig(conf)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.InitDb()
|
||||||
|
|
||||||
|
// Use first argument to limit scope if set.
|
||||||
|
subPath := strings.TrimSpace(ctx.Args().First())
|
||||||
|
|
||||||
|
if subPath == "" {
|
||||||
|
log.Infof("finding faces in %s", txt.Quote(conf.OriginalsPath()))
|
||||||
|
} else {
|
||||||
|
log.Infof("finding faces in %s", txt.Quote(filepath.Join(conf.OriginalsPath(), subPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.ReadOnly() {
|
||||||
|
log.Infof("index: read-only mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexed fs.Done
|
||||||
|
|
||||||
|
if w := service.Index(); w != nil {
|
||||||
|
opt := photoprism.IndexOptions{
|
||||||
|
Path: subPath,
|
||||||
|
Rescan: true,
|
||||||
|
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
|
||||||
|
Stack: true,
|
||||||
|
FacesOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
indexed = w.Start(opt)
|
||||||
|
} else if w := service.Purge(); w != nil {
|
||||||
|
opt := photoprism.PurgeOptions{
|
||||||
|
Path: subPath,
|
||||||
|
Ignore: indexed,
|
||||||
|
}
|
||||||
|
|
||||||
|
if files, photos, err := w.Start(opt); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
} else if len(files) > 0 || len(photos) > 0 {
|
||||||
|
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
log.Infof("indexed %d files in %s", len(indexed), elapsed)
|
||||||
|
|
||||||
|
conf.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// facesMatchAction performs face clustering and matching.
|
||||||
|
func facesMatchAction(ctx *cli.Context) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
conf := config.NewConfig(ctx)
|
||||||
|
service.SetConfig(conf)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.InitDb()
|
||||||
|
|
||||||
|
opt := photoprism.FacesOptions{
|
||||||
|
Force: ctx.Bool("force"),
|
||||||
|
}
|
||||||
|
|
||||||
|
w := service.Faces()
|
||||||
|
|
||||||
|
if err := w.Start(opt); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
log.Infof("completed in %s", elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// facesOptimizeAction optimizes existing face clusters.
|
// facesOptimizeAction optimizes existing face clusters.
|
||||||
func facesOptimizeAction(ctx *cli.Context) error {
|
func facesOptimizeAction(ctx *cli.Context) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@ -190,38 +346,3 @@ func facesOptimizeAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// facesUpdateAction performs face clustering and matching.
|
|
||||||
func facesUpdateAction(ctx *cli.Context) error {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
conf := config.NewConfig(ctx)
|
|
||||||
service.SetConfig(conf)
|
|
||||||
|
|
||||||
_, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := conf.Init(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.InitDb()
|
|
||||||
|
|
||||||
opt := photoprism.FacesOptions{
|
|
||||||
Force: ctx.Bool("force"),
|
|
||||||
}
|
|
||||||
|
|
||||||
w := service.Faces()
|
|
||||||
|
|
||||||
if err := w.Start(opt); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
elapsed := time.Since(start)
|
|
||||||
|
|
||||||
log.Infof("completed in %s", elapsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Shutdown()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,10 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImportCommand registers the import cli command.
|
// ImportCommand registers the import cli command.
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IndexCommand registers the index cli command.
|
// IndexCommand registers the index cli command.
|
||||||
|
@ -75,9 +75,7 @@ func indexAction(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
indexed = w.Start(opt)
|
indexed = w.Start(opt)
|
||||||
}
|
} else if w := service.Purge(); w != nil {
|
||||||
|
|
||||||
if w := service.Purge(); w != nil {
|
|
||||||
opt := photoprism.PurgeOptions{
|
opt := photoprism.PurgeOptions{
|
||||||
Path: subPath,
|
Path: subPath,
|
||||||
Ignore: indexed,
|
Ignore: indexed,
|
||||||
|
@ -88,9 +86,7 @@ func indexAction(ctx *cli.Context) error {
|
||||||
} else if len(files) > 0 || len(photos) > 0 {
|
} else if len(files) > 0 || len(photos) > 0 {
|
||||||
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
||||||
}
|
}
|
||||||
}
|
} else if ctx.Bool("cleanup") {
|
||||||
|
|
||||||
if ctx.Bool("cleanup") {
|
|
||||||
w := service.CleanUp()
|
w := service.CleanUp()
|
||||||
|
|
||||||
opt := photoprism.CleanUpOptions{
|
opt := photoprism.CleanUpOptions{
|
||||||
|
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MigrateCommand registers the migrate cli command.
|
// MigrateCommand registers the migrate cli command.
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MomentsCommand registers the moments cli command.
|
// MomentsCommand registers the moments cli command.
|
||||||
|
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/workers"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/internal/workers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OptimizeCommand registers the index cli command.
|
// OptimizeCommand registers the index cli command.
|
||||||
|
|
|
@ -10,10 +10,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PasswdCommand updates a password.
|
// PasswdCommand updates a password.
|
||||||
|
|
|
@ -6,12 +6,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PurgeCommand registers the index cli command.
|
// PurgeCommand registers the index cli command.
|
||||||
|
|
|
@ -3,10 +3,11 @@ package commands
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResampleCommand registers the resample cli command.
|
// ResampleCommand registers the resample cli command.
|
||||||
|
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResetCommand resets the index and removes sidecar files after confirmation.
|
// ResetCommand resets the index and removes sidecar files after confirmation.
|
||||||
|
|
|
@ -11,16 +11,14 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RestoreCommand configures the backup cli command.
|
// RestoreCommand configures the backup cli command.
|
||||||
|
|
|
@ -9,18 +9,17 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sevlyar/go-daemon"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/auto"
|
"github.com/photoprism/photoprism/internal/auto"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/server"
|
"github.com/photoprism/photoprism/internal/server"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/photoprism/photoprism/internal/workers"
|
"github.com/photoprism/photoprism/internal/workers"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/sevlyar/go-daemon"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartCommand registers the start cli command.
|
// StartCommand registers the start cli command.
|
||||||
|
|
|
@ -6,9 +6,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusCommand performs a server health check.
|
// StatusCommand performs a server health check.
|
||||||
|
|
|
@ -3,10 +3,11 @@ package commands
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
|
||||||
"github.com/sevlyar/go-daemon"
|
"github.com/sevlyar/go-daemon"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StopCommand registers the stop cli command.
|
// StopCommand registers the stop cli command.
|
||||||
|
|
|
@ -7,12 +7,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsersCommand registers user management commands.
|
// UsersCommand registers user management commands.
|
||||||
|
|
|
@ -3,8 +3,9 @@ package commands
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionCommand registers the version cli command.
|
// VersionCommand registers the version cli command.
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/face"
|
"github.com/photoprism/photoprism/internal/face"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clusters"
|
"github.com/photoprism/photoprism/pkg/clusters"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
)
|
)
|
||||||
|
|
||||||
var faceMutex = sync.Mutex{}
|
var faceMutex = sync.Mutex{}
|
||||||
|
@ -350,3 +350,20 @@ func FindFace(id string) *Face {
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FaceCount counts the number of valid face markers for a file uid.
|
||||||
|
func FaceCount(fileUID string) (c int) {
|
||||||
|
if !rnd.IsPPID(fileUID, 'f') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Db().Model(Marker{}).
|
||||||
|
Where("file_uid = ? AND marker_type = ?", fileUID, MarkerFace).
|
||||||
|
Where("marker_invalid = 0").
|
||||||
|
Count(&c).Error; err != nil {
|
||||||
|
log.Errorf("file: %s (count faces)", err)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -201,7 +201,7 @@ func (m File) Missing() bool {
|
||||||
return m.FileMissing || m.DeletedAt != nil
|
return m.FileMissing || m.DeletedAt != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete permanently deletes the entity from the database.
|
// DeletePermanently permanently deletes the entity.
|
||||||
func (m *File) DeletePermanently() error {
|
func (m *File) DeletePermanently() error {
|
||||||
Db().Unscoped().Delete(FileShare{}, "file_id = ?", m.ID)
|
Db().Unscoped().Delete(FileShare{}, "file_id = ?", m.ID)
|
||||||
Db().Unscoped().Delete(FileSync{}, "file_id = ?", m.ID)
|
Db().Unscoped().Delete(FileSync{}, "file_id = ?", m.ID)
|
||||||
|
@ -260,7 +260,7 @@ func (m *File) Create() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Markers().Save(m.FileUID); err != nil {
|
if _, err := m.SaveMarkers(); err != nil {
|
||||||
log.Errorf("file: %s (create markers for %s)", err, m.FileUID)
|
log.Errorf("file: %s (create markers for %s)", err, m.FileUID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -288,7 +288,7 @@ func (m *File) Save() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Markers().Save(m.FileUID); err != nil {
|
if _, err := m.SaveMarkers(); err != nil {
|
||||||
log.Errorf("file: %s (save markers for %s)", err, m.FileUID)
|
log.Errorf("file: %s (save markers for %s)", err, m.FileUID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -426,33 +426,62 @@ func (m *File) AddFaces(faces face.Faces) {
|
||||||
|
|
||||||
// AddFace adds a face marker to the file.
|
// AddFace adds a face marker to the file.
|
||||||
func (m *File) AddFace(f face.Face, subjUID string) {
|
func (m *File) AddFace(f face.Face, subjUID string) {
|
||||||
marker := *NewFaceMarker(f, *m, subjUID)
|
// Only add faces with embedding, so that they can be clustered.
|
||||||
|
if len(f.Embeddings) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if markers := m.Markers(); !markers.Contains(marker) {
|
// Create new marker from face.
|
||||||
markers.Append(marker)
|
marker := NewFaceMarker(f, *m, subjUID)
|
||||||
|
|
||||||
|
// Failed creating new marker?
|
||||||
|
if marker == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append marker if it doesn't conflict with existing marker.
|
||||||
|
if markers := m.Markers(); !markers.Contains(*marker) {
|
||||||
|
markers.Append(*marker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FaceCount returns the current number of valid faces detected.
|
// FaceCount returns the current number of valid faces detected.
|
||||||
func (m *File) FaceCount() (c int) {
|
func (m *File) FaceCount() (c int) {
|
||||||
if err := Db().Model(Marker{}).
|
return FaceCount(m.FileUID)
|
||||||
Where("file_uid = ? AND marker_type = ?", m.FileUID, MarkerFace).
|
}
|
||||||
Where("marker_invalid = 0").
|
|
||||||
Count(&c).Error; err != nil {
|
// UpdatePhotoFaceCount updates the faces count in the index and returns it if the file is primary.
|
||||||
log.Errorf("file: %s (count faces)", err)
|
func (m *File) UpdatePhotoFaceCount() (c int, err error) {
|
||||||
return 0
|
// Primary file of an existing photo?
|
||||||
} else {
|
if !m.FilePrimary || m.PhotoID == 0 {
|
||||||
return c
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c = m.FaceCount()
|
||||||
|
|
||||||
|
err = UnscopedDb().Model(Photo{}).
|
||||||
|
Where("id = ?", m.PhotoID).
|
||||||
|
UpdateColumn("photo_faces", c).Error
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveMarkers updates markers in the index.
|
||||||
|
func (m *File) SaveMarkers() (count int, err error) {
|
||||||
|
if m.markers == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.markers.Save(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markers finds and returns existing file markers.
|
// Markers finds and returns existing file markers.
|
||||||
func (m *File) Markers() *Markers {
|
func (m *File) Markers() *Markers {
|
||||||
if m.markers != nil {
|
if m.markers != nil {
|
||||||
return m.markers
|
return m.markers
|
||||||
}
|
} else if m.FileUID == "" {
|
||||||
|
m.markers = &Markers{}
|
||||||
if res, err := FindMarkers(m.FileUID); err != nil {
|
} else if res, err := FindMarkers(m.FileUID); err != nil {
|
||||||
log.Warnf("file: %s (load markers)", err)
|
log.Warnf("file: %s (load markers)", err)
|
||||||
m.markers = &Markers{}
|
m.markers = &Markers{}
|
||||||
} else {
|
} else {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -70,6 +70,11 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
||||||
|
|
||||||
// NewMarker creates a new entity.
|
// NewMarker creates a new entity.
|
||||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
||||||
|
if file.FileHash == "" {
|
||||||
|
log.Errorf("marker: file hash is empty - you might have found a bug")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
m := &Marker{
|
m := &Marker{
|
||||||
FileUID: file.FileUID,
|
FileUID: file.FileUID,
|
||||||
MarkerSrc: markerSrc,
|
MarkerSrc: markerSrc,
|
||||||
|
@ -96,6 +101,11 @@ func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string,
|
||||||
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
|
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
|
||||||
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace, f.Size(), f.Score)
|
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace, f.Size(), f.Score)
|
||||||
|
|
||||||
|
// Failed creating new marker?
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||||
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package entity
|
||||||
import "github.com/photoprism/photoprism/internal/crop"
|
import "github.com/photoprism/photoprism/internal/crop"
|
||||||
|
|
||||||
// UnknownMarker can be used as a default for unknown markers.
|
// UnknownMarker can be used as a default for unknown markers.
|
||||||
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown, 0, 0)
|
var UnknownMarker = NewMarker(File{FileUID: "-", FileHash: "-"}, crop.Area{}, "", SrcDefault, MarkerUnknown, 0, 0)
|
||||||
|
|
||||||
type MarkerMap map[string]Marker
|
type MarkerMap map[string]Marker
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,22 @@ import (
|
||||||
type Markers []Marker
|
type Markers []Marker
|
||||||
|
|
||||||
// Save stores the markers in the database.
|
// Save stores the markers in the database.
|
||||||
func (m Markers) Save(fileUID string) error {
|
func (m Markers) Save(file *File) (count int, err error) {
|
||||||
for _, marker := range m {
|
for _, marker := range m {
|
||||||
if fileUID != "" {
|
if file != nil {
|
||||||
marker.FileUID = fileUID
|
marker.FileUID = file.FileUID
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := UpdateOrCreateMarker(&marker); err != nil {
|
if _, err := UpdateOrCreateMarker(&marker); err != nil {
|
||||||
return err
|
log.Errorf("markers: %s (save)", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if file == nil {
|
||||||
|
return len(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.UpdatePhotoFaceCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains returns true if a marker at the same position already exists.
|
// Contains returns true if a marker at the same position already exists.
|
||||||
|
|
|
@ -28,7 +28,7 @@ func TestMarkers_Contains(t *testing.T) {
|
||||||
assert.False(t, m.Contains(m3))
|
assert.False(t, m.Contains(m3))
|
||||||
})
|
})
|
||||||
t.Run("Conflicting", func(t *testing.T) {
|
t.Run("Conflicting", func(t *testing.T) {
|
||||||
file := File{FileHash: "cca7c46a4d39e933c30805e546028fe3eab361b5"}
|
file := File{FileUID: "", FileHash: "cca7c46a4d39e933c30805e546028fe3eab361b5"}
|
||||||
|
|
||||||
markers := Markers{
|
markers := Markers{
|
||||||
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
||||||
|
@ -51,7 +51,7 @@ func TestMarkers_Contains(t *testing.T) {
|
||||||
assert.True(t, markers.Contains(conflicting))
|
assert.True(t, markers.Contains(conflicting))
|
||||||
})
|
})
|
||||||
t.Run("NoFace", func(t *testing.T) {
|
t.Run("NoFace", func(t *testing.T) {
|
||||||
file := File{FileHash: "243cdbe99b865607f98a951e748d528bc22f3143"}
|
file := File{FileUID: "fqzuh672i9btq6gu", FileHash: "243cdbe99b865607f98a951e748d528bc22f3143"}
|
||||||
|
|
||||||
markers := Markers{
|
markers := Markers{
|
||||||
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
||||||
|
|
|
@ -938,7 +938,7 @@ func (m *Photo) Delete(permanently bool) error {
|
||||||
return m.Updates(map[string]interface{}{"DeletedAt": TimeStamp(), "PhotoQuality": -1})
|
return m.Updates(map[string]interface{}{"DeletedAt": TimeStamp(), "PhotoQuality": -1})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete permanently deletes the entity from the database.
|
// DeletePermanently permanently deletes the entity.
|
||||||
func (m *Photo) DeletePermanently() error {
|
func (m *Photo) DeletePermanently() error {
|
||||||
Db().Unscoped().Delete(File{}, "photo_id = ?", m.ID)
|
Db().Unscoped().Delete(File{}, "photo_id = ?", m.ID)
|
||||||
Db().Unscoped().Delete(Details{}, "photo_id = ?", m.ID)
|
Db().Unscoped().Delete(Details{}, "photo_id = ?", m.ID)
|
||||||
|
@ -954,7 +954,7 @@ func (m *Photo) NoDescription() bool {
|
||||||
return m.PhotoDescription == ""
|
return m.PhotoDescription == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates a column in the database.
|
// Update a column in the database.
|
||||||
func (m *Photo) Update(attr string, value interface{}) error {
|
func (m *Photo) Update(attr string, value interface{}) error {
|
||||||
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,7 @@ func (m *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirstOrCreateUser returns an existing row, inserts a new row or nil in case of errors.
|
// FirstOrCreateUser returns an existing row, inserts a new row, or nil in case of errors.
|
||||||
func FirstOrCreateUser(m *User) *User {
|
func FirstOrCreateUser(m *User) *User {
|
||||||
result := User{}
|
result := User{}
|
||||||
|
|
||||||
|
|
|
@ -144,9 +144,9 @@ func (d *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo.
|
||||||
if cols < 20 || rows < 20 || cols < d.minSize || rows < d.minSize {
|
if cols < 20 || rows < 20 || cols < d.minSize || rows < d.minSize {
|
||||||
return faces, params, fmt.Errorf("image size %dx%d is too small", cols, rows)
|
return faces, params, fmt.Errorf("image size %dx%d is too small", cols, rows)
|
||||||
} else if cols < rows {
|
} else if cols < rows {
|
||||||
maxSize = cols - 8
|
maxSize = cols - 4
|
||||||
} else {
|
} else {
|
||||||
maxSize = rows - 8
|
maxSize = rows - 4
|
||||||
}
|
}
|
||||||
|
|
||||||
imageParams := &pigo.ImageParams{
|
imageParams := &pigo.ImageParams{
|
||||||
|
@ -164,7 +164,7 @@ func (d *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo.
|
||||||
ImageParams: *imageParams,
|
ImageParams: *imageParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("faces: image size %dx%d, face size min %d, max %d", cols, rows, params.MinSize, params.MaxSize)
|
log.Tracef("faces: image size %dx%d, face size min %d, max %d", cols, rows, params.MinSize, params.MaxSize)
|
||||||
|
|
||||||
// Run the classifier over the obtained leaf nodes and return the Face results.
|
// Run the classifier over the obtained leaf nodes and return the Face results.
|
||||||
// The result contains quadruplets representing the row, column, scale and Face score.
|
// The result contains quadruplets representing the row, column, scale and Face score.
|
||||||
|
|
|
@ -32,15 +32,18 @@ func NewNet(modelPath, cachePath string, disabled bool) *Net {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect runs the detection and facenet algorithms over the provided source image.
|
// Detect runs the detection and facenet algorithms over the provided source image.
|
||||||
func (t *Net) Detect(fileName string, minSize int, cacheCrop bool) (faces Faces, err error) {
|
func (t *Net) Detect(fileName string, minSize int, cacheCrop bool, expected int) (faces Faces, err error) {
|
||||||
faces, err = Detect(fileName, false, minSize)
|
faces, err = Detect(fileName, false, minSize)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return faces, err
|
return faces, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip FaceNet?
|
||||||
if t.disabled {
|
if t.disabled {
|
||||||
return faces, nil
|
return faces, nil
|
||||||
|
} else if c := len(faces); c == 0 || expected > 0 && c == expected {
|
||||||
|
return faces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.loadModel()
|
err = t.loadModel()
|
||||||
|
|
|
@ -63,7 +63,7 @@ func TestNet(t *testing.T) {
|
||||||
t.Run(fileName, func(t *testing.T) {
|
t.Run(fileName, func(t *testing.T) {
|
||||||
baseName := filepath.Base(fileName)
|
baseName := filepath.Base(fileName)
|
||||||
|
|
||||||
faces, err := faceNet.Detect(fileName, 20, false)
|
faces, err := faceNet.Detect(fileName, 20, false, -1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -58,6 +58,12 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
||||||
|
|
||||||
var directories []string
|
var directories []string
|
||||||
done := make(fs.Done)
|
done := make(fs.Done)
|
||||||
|
|
||||||
|
if imp.conf == nil {
|
||||||
|
log.Errorf("import: config is nil")
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
ind := imp.index
|
ind := imp.index
|
||||||
importPath := opt.Path
|
importPath := opt.Path
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,8 @@ type Index struct {
|
||||||
convert *Convert
|
convert *Convert
|
||||||
files *Files
|
files *Files
|
||||||
photos *Photos
|
photos *Photos
|
||||||
|
findFaces bool
|
||||||
|
findLabels bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIndex returns a new indexer and expects its dependencies as arguments.
|
// NewIndex returns a new indexer and expects its dependencies as arguments.
|
||||||
|
@ -70,6 +72,12 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
done := make(fs.Done)
|
done := make(fs.Done)
|
||||||
|
|
||||||
|
if ind.conf == nil {
|
||||||
|
log.Errorf("index: config is nil")
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
originalsPath := ind.originalsPath()
|
originalsPath := ind.originalsPath()
|
||||||
optionsPath := filepath.Join(originalsPath, opt.Path)
|
optionsPath := filepath.Join(originalsPath, opt.Path)
|
||||||
|
|
||||||
|
@ -83,6 +91,12 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
|
||||||
return done
|
return done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect faces in images?
|
||||||
|
ind.findFaces = ind.conf.Settings().Features.People
|
||||||
|
|
||||||
|
// Classify images with TensorFlow?
|
||||||
|
ind.findLabels = !ind.conf.DisableTensorFlow()
|
||||||
|
|
||||||
defer mutex.MainWorker.Stop()
|
defer mutex.MainWorker.Stop()
|
||||||
|
|
||||||
if err := ind.tensorFlow.Init(); err != nil {
|
if err := ind.tensorFlow.Init(); err != nil {
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// detectFaces extracts faces from a JPEG image and returns them.
|
// Faces finds faces in JPEG media files and returns them.
|
||||||
func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
func (ind *Index) Faces(jpeg *MediaFile, expected int) face.Faces {
|
||||||
if jpeg == nil {
|
if jpeg == nil {
|
||||||
return face.Faces{}
|
return face.Faces{}
|
||||||
}
|
}
|
||||||
|
@ -41,14 +41,16 @@ func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
faces, err := ind.faceNet.Detect(thumbName, minSize, true)
|
faces, err := ind.faceNet.Detect(thumbName, minSize, true, expected)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(faces) > 0 {
|
if l := len(faces); l == 1 {
|
||||||
log.Infof("index: extracted %d faces from %s [%s]", len(faces), txt.Quote(jpeg.BaseName()), time.Since(start))
|
log.Infof("index: found %d face in %s [%s]", l, txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
|
} else if l > 1 {
|
||||||
|
log.Infof("index: found %d faces in %s [%s]", l, txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
return faces
|
return faces
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// classifyImage classifies a JPEG image and returns matching labels.
|
// Labels classifies a JPEG image and returns matching labels.
|
||||||
func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
func (ind *Index) Labels(jpeg *MediaFile) (results classify.Labels) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var sizes []thumb.Name
|
var sizes []thumb.Name
|
||||||
|
@ -57,8 +57,10 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(labels) > 0 {
|
if l := len(labels); l == 1 {
|
||||||
log.Infof("index: found %d matching labels for %s [%s]", len(labels), txt.Quote(jpeg.BaseName()), time.Since(start))
|
log.Infof("index: found %d matching label for %s [%s]", l, txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
|
} else if l > 1 {
|
||||||
|
log.Infof("index: found %d matching labels for %s [%s]", l, txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
|
@ -13,63 +13,13 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/meta"
|
"github.com/photoprism/photoprism/internal/meta"
|
||||||
"github.com/photoprism/photoprism/internal/nsfw"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// MediaFile indexes a single media file.
|
||||||
IndexUpdated IndexStatus = "updated"
|
|
||||||
IndexAdded IndexStatus = "added"
|
|
||||||
IndexStacked IndexStatus = "stacked"
|
|
||||||
IndexSkipped IndexStatus = "skipped"
|
|
||||||
IndexDuplicate IndexStatus = "skipped duplicate"
|
|
||||||
IndexArchived IndexStatus = "skipped archived"
|
|
||||||
IndexFailed IndexStatus = "failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IndexStatus string
|
|
||||||
|
|
||||||
type IndexResult struct {
|
|
||||||
Status IndexStatus
|
|
||||||
Err error
|
|
||||||
FileID uint
|
|
||||||
FileUID string
|
|
||||||
PhotoID uint
|
|
||||||
PhotoUID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) String() string {
|
|
||||||
return string(r.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Failed() bool {
|
|
||||||
return r.Err != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Success() bool {
|
|
||||||
return r.Err == nil && (r.FileID > 0 || r.Stacked() || r.Skipped() || r.Archived())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Indexed() bool {
|
|
||||||
return r.Status == IndexAdded || r.Status == IndexUpdated || r.Status == IndexStacked
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Stacked() bool {
|
|
||||||
return r.Status == IndexStacked
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Skipped() bool {
|
|
||||||
return r.Status == IndexSkipped
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IndexResult) Archived() bool {
|
|
||||||
return r.Status == IndexArchived
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (result IndexResult) {
|
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (result IndexResult) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
err := errors.New("index: media file is nil - you might have found a bug")
|
err := errors.New("index: media file is nil - you might have found a bug")
|
||||||
|
@ -79,7 +29,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip file?
|
||||||
if ind.files.Ignore(m.RootRelName(), m.Root(), m.ModTime(), o.Rescan) {
|
if ind.files.Ignore(m.RootRelName(), m.Root(), m.ModTime(), o.Rescan) {
|
||||||
|
// Skip known file.
|
||||||
|
result.Status = IndexSkipped
|
||||||
|
return result
|
||||||
|
} else if o.FacesOnly && !m.IsJpeg() {
|
||||||
|
// Skip non-jpeg file when indexing faces only.
|
||||||
result.Status = IndexSkipped
|
result.Status = IndexSkipped
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -274,6 +230,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set basic file information.
|
||||||
|
file.FileRoot = fileRoot
|
||||||
|
file.FileName = fileName
|
||||||
|
file.FileHash = fileHash
|
||||||
|
file.FileSize = fileSize
|
||||||
|
|
||||||
// Set file original name if available.
|
// Set file original name if available.
|
||||||
if originalName != "" {
|
if originalName != "" {
|
||||||
file.OriginalName = originalName
|
file.OriginalName = originalName
|
||||||
|
@ -289,6 +251,33 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
photo.DeletedAt = nil
|
photo.DeletedAt = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra labels to ba added when new files have a photo id.
|
||||||
|
extraLabels := classify.Labels{}
|
||||||
|
|
||||||
|
// Detect faces in images?
|
||||||
|
if o.FacesOnly && (!photoExists || !fileExists || !file.FilePrimary) {
|
||||||
|
// New and non-primary files can be skipped when updating faces only.
|
||||||
|
result.Status = IndexSkipped
|
||||||
|
return result
|
||||||
|
} else if ind.findFaces && file.FilePrimary {
|
||||||
|
faces := ind.Faces(m, photo.PhotoFaces)
|
||||||
|
|
||||||
|
if len(faces) > 0 {
|
||||||
|
file.AddFaces(faces)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := file.Markers().FaceCount(); photo.PhotoFaces != c {
|
||||||
|
if c > photo.PhotoFaces {
|
||||||
|
extraLabels = append(extraLabels, classify.FaceLabels(faces, entity.SrcImage)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
photo.PhotoFaces = c
|
||||||
|
} else if o.FacesOnly {
|
||||||
|
result.Status = IndexSkipped
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle file types.
|
// Handle file types.
|
||||||
switch {
|
switch {
|
||||||
case m.IsJpeg():
|
case m.IsJpeg():
|
||||||
|
@ -487,9 +476,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
if file.FilePrimary {
|
if file.FilePrimary {
|
||||||
primaryFile = file
|
primaryFile = file
|
||||||
|
|
||||||
if !Config().DisableTensorFlow() {
|
// Classify images with TensorFlow?
|
||||||
// Image classification via TensorFlow.
|
if ind.findLabels {
|
||||||
labels = ind.classifyImage(m)
|
labels = ind.Labels(m)
|
||||||
|
|
||||||
|
// Append labels from other sources such as face detection.
|
||||||
|
if len(extraLabels) > 0 {
|
||||||
|
labels = append(labels, extraLabels...)
|
||||||
|
}
|
||||||
|
|
||||||
if !photoExists && Config().Settings().Features.Private && Config().DetectNSFW() {
|
if !photoExists && Config().Settings().Features.Private && Config().DetectNSFW() {
|
||||||
photo.PhotoPrivate = ind.NSFW(m)
|
photo.PhotoPrivate = ind.NSFW(m)
|
||||||
|
@ -547,10 +541,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
|
|
||||||
file.FileSidecar = m.IsSidecar()
|
file.FileSidecar = m.IsSidecar()
|
||||||
file.FileVideo = m.IsVideo()
|
file.FileVideo = m.IsVideo()
|
||||||
file.FileRoot = fileRoot
|
|
||||||
file.FileName = fileName
|
|
||||||
file.FileHash = fileHash
|
|
||||||
file.FileSize = fileSize
|
|
||||||
file.FileType = string(m.FileType())
|
file.FileType = string(m.FileType())
|
||||||
file.FileMime = m.MimeType()
|
file.FileMime = m.MimeType()
|
||||||
file.FileOrientation = m.Orientation()
|
file.FileOrientation = m.Orientation()
|
||||||
|
@ -600,18 +590,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
|
|
||||||
// Main JPEG file.
|
// Main JPEG file.
|
||||||
if file.FilePrimary {
|
if file.FilePrimary {
|
||||||
if Config().Settings().Features.People {
|
|
||||||
faces := ind.detectFaces(m)
|
|
||||||
|
|
||||||
photo.AddLabels(classify.FaceLabels(faces, entity.SrcImage))
|
|
||||||
|
|
||||||
if len(faces) > 0 {
|
|
||||||
file.AddFaces(faces)
|
|
||||||
}
|
|
||||||
|
|
||||||
photo.PhotoFaces = file.Markers().FaceCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := photo.ClassifyLabels()
|
labels := photo.ClassifyLabels()
|
||||||
|
|
||||||
if err := photo.UpdateTitle(labels); err != nil {
|
if err := photo.UpdateTitle(labels); err != nil {
|
||||||
|
@ -750,25 +728,3 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// NSFW returns true if media file might be offensive and detection is enabled.
|
|
||||||
func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
|
||||||
filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb.Fit720)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if nsfwLabels, err := ind.nsfwDetector.File(filename); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
|
|
||||||
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelName(Config().OriginalsPath())))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
29
internal/photoprism/index_nsfw.go
Normal file
29
internal/photoprism/index_nsfw.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package photoprism
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/nsfw"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NSFW returns true if media file might be offensive and detection is enabled.
|
||||||
|
func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
||||||
|
filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb.Fit720)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if nsfwLabels, err := ind.nsfwDetector.File(filename); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
|
||||||
|
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelName(Config().OriginalsPath())))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package photoprism
|
package photoprism
|
||||||
|
|
||||||
type IndexOptions struct {
|
type IndexOptions struct {
|
||||||
Path string
|
Path string
|
||||||
Rescan bool
|
Rescan bool
|
||||||
Convert bool
|
Convert bool
|
||||||
Stack bool
|
Stack bool
|
||||||
|
FacesOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *IndexOptions) SkipUnchanged() bool {
|
func (o *IndexOptions) SkipUnchanged() bool {
|
||||||
|
@ -14,10 +15,24 @@ func (o *IndexOptions) SkipUnchanged() bool {
|
||||||
// IndexOptionsAll returns new index options with all options set to true.
|
// IndexOptionsAll returns new index options with all options set to true.
|
||||||
func IndexOptionsAll() IndexOptions {
|
func IndexOptionsAll() IndexOptions {
|
||||||
result := IndexOptions{
|
result := IndexOptions{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Rescan: true,
|
Rescan: true,
|
||||||
Convert: true,
|
Convert: true,
|
||||||
Stack: true,
|
Stack: true,
|
||||||
|
FacesOnly: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOptionsFacesOnly returns new index options for updating faces only.
|
||||||
|
func IndexOptionsFacesOnly() IndexOptions {
|
||||||
|
result := IndexOptions{
|
||||||
|
Path: "/",
|
||||||
|
Rescan: true,
|
||||||
|
Convert: true,
|
||||||
|
Stack: true,
|
||||||
|
FacesOnly: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -26,10 +41,11 @@ func IndexOptionsAll() IndexOptions {
|
||||||
// IndexOptionsSingle returns new index options for unstacked, single files.
|
// IndexOptionsSingle returns new index options for unstacked, single files.
|
||||||
func IndexOptionsSingle() IndexOptions {
|
func IndexOptionsSingle() IndexOptions {
|
||||||
result := IndexOptions{
|
result := IndexOptions{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Rescan: true,
|
Rescan: true,
|
||||||
Convert: true,
|
Convert: true,
|
||||||
Stack: false,
|
Stack: false,
|
||||||
|
FacesOnly: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -7,21 +7,39 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIndexOptionsNone(t *testing.T) {
|
func TestIndexOptionsNone(t *testing.T) {
|
||||||
result := IndexOptionsNone()
|
opt := IndexOptionsNone()
|
||||||
assert.Equal(t, false, result.Rescan)
|
|
||||||
assert.Equal(t, false, result.Convert)
|
assert.Equal(t, "", opt.Path)
|
||||||
|
assert.Equal(t, false, opt.Rescan)
|
||||||
|
assert.Equal(t, false, opt.Convert)
|
||||||
|
assert.Equal(t, false, opt.Stack)
|
||||||
|
assert.Equal(t, false, opt.FacesOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndexOptions_SkipUnchanged(t *testing.T) {
|
func TestIndexOptions_SkipUnchanged(t *testing.T) {
|
||||||
result := IndexOptionsNone()
|
opt := IndexOptionsNone()
|
||||||
assert.True(t, result.SkipUnchanged())
|
|
||||||
result.Rescan = true
|
assert.True(t, opt.SkipUnchanged())
|
||||||
assert.False(t, result.SkipUnchanged())
|
|
||||||
|
opt.Rescan = true
|
||||||
|
|
||||||
|
assert.False(t, opt.SkipUnchanged())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndexOptionsSingle(t *testing.T) {
|
func TestIndexOptionsSingle(t *testing.T) {
|
||||||
r := IndexOptionsSingle()
|
opt := IndexOptionsSingle()
|
||||||
assert.Equal(t, false, r.Stack)
|
|
||||||
assert.Equal(t, true, r.Convert)
|
assert.Equal(t, false, opt.Stack)
|
||||||
assert.Equal(t, true, r.Rescan)
|
assert.Equal(t, true, opt.Convert)
|
||||||
|
assert.Equal(t, true, opt.Rescan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexOptionsFacesOnly(t *testing.T) {
|
||||||
|
opt := IndexOptionsFacesOnly()
|
||||||
|
|
||||||
|
assert.Equal(t, "/", opt.Path)
|
||||||
|
assert.Equal(t, true, opt.Rescan)
|
||||||
|
assert.Equal(t, true, opt.Convert)
|
||||||
|
assert.Equal(t, true, opt.Stack)
|
||||||
|
assert.Equal(t, true, opt.FacesOnly)
|
||||||
}
|
}
|
||||||
|
|
50
internal/photoprism/index_result.go
Normal file
50
internal/photoprism/index_result.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package photoprism
|
||||||
|
|
||||||
|
const (
|
||||||
|
IndexUpdated IndexStatus = "updated"
|
||||||
|
IndexAdded IndexStatus = "added"
|
||||||
|
IndexStacked IndexStatus = "stacked"
|
||||||
|
IndexSkipped IndexStatus = "skipped"
|
||||||
|
IndexDuplicate IndexStatus = "skipped duplicate"
|
||||||
|
IndexArchived IndexStatus = "skipped archived"
|
||||||
|
IndexFailed IndexStatus = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexStatus string
|
||||||
|
|
||||||
|
type IndexResult struct {
|
||||||
|
Status IndexStatus
|
||||||
|
Err error
|
||||||
|
FileID uint
|
||||||
|
FileUID string
|
||||||
|
PhotoID uint
|
||||||
|
PhotoUID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) String() string {
|
||||||
|
return string(r.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Failed() bool {
|
||||||
|
return r.Err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Success() bool {
|
||||||
|
return r.Err == nil && (r.FileID > 0 || r.Stacked() || r.Skipped() || r.Archived())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Indexed() bool {
|
||||||
|
return r.Status == IndexAdded || r.Status == IndexUpdated || r.Status == IndexStacked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Stacked() bool {
|
||||||
|
return r.Status == IndexStacked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Skipped() bool {
|
||||||
|
return r.Status == IndexSkipped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndexResult) Archived() bool {
|
||||||
|
return r.Status == IndexArchived
|
||||||
|
}
|
|
@ -208,3 +208,49 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
|
||||||
|
|
||||||
return conflicts, resolved, nil
|
return conflicts, resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemovePeopleAndFaces permanently deletes all people, faces, and face markers.
|
||||||
|
func RemovePeopleAndFaces() (err error) {
|
||||||
|
// Delete people.
|
||||||
|
if err = UnscopedDb().Delete(entity.Subject{}, "subj_type = ?", entity.SubjPerson).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all faces.
|
||||||
|
if err = UnscopedDb().Delete(entity.Face{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete face markers.
|
||||||
|
if err = UnscopedDb().Delete(entity.Marker{}, "marker_type = ?", entity.MarkerFace).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset face counters.
|
||||||
|
if err = UnscopedDb().Model(entity.Photo{}).
|
||||||
|
UpdateColumn("photo_faces", 0).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset people label.
|
||||||
|
if label, err := LabelBySlug("people"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = UnscopedDb().
|
||||||
|
Delete(entity.PhotoLabel{}, "label_id = ?", label.ID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = label.Update("PhotoCount", 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset portrait label.
|
||||||
|
if label, err := LabelBySlug("portrait"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = UnscopedDb().
|
||||||
|
Delete(entity.PhotoLabel{}, "label_id = ?", label.ID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = label.Update("PhotoCount", 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue