Faces: Remove PHOTOPRISM_FACE_ENGINE_RUN config option #5167

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-08 13:41:51 +02:00
parent 26b937d8b0
commit a302955c02
12 changed files with 102 additions and 55 deletions

View File

@@ -116,6 +116,8 @@ Additional safeguards were introduced in October 2025 so stubborn clusters are o
| `FACE_SCORE` | `9.0` (with dynamic offsets) | Base quality threshold before scale adjustments. |
| `FACE_OVERLAP` | `42` | Maximum allowed IoU when deduplicating markers. |
Run scheduling is configured through the face model entry in `vision.yml`. Adjust the models `Run` value (for example `on-schedule`, `manual`, or `never`) to control when detection and embedding jobs execute—no separate `FACE_ENGINE_RUN` flag is required.
> Additional merge tuning: set `PHOTOPRISM_FACE_MERGE_MAX_RETRY` to control how often manual clusters are retried (default 1, `0` = unlimited). See the optimiser notes above.
### Benchmark Reference

View File

@@ -164,6 +164,21 @@ func (c *ConfigValues) ShouldRun(t ModelType, when RunType) bool {
return m.ShouldRun(when)
}
// RunType returns the normalized run type for the first enabled model matching
// the provided type. Disabled or missing models fall back to RunNever so
// callers can treat the result as authoritative scheduling information.
func (c *ConfigValues) RunType(t ModelType) RunType {
m := c.Model(t)
if m == nil {
return RunNever
} else if m.Disabled {
return RunNever
}
return m.RunType()
}
// IsDefault checks whether the specified type is the built-in default model.
func (c *ConfigValues) IsDefault(t ModelType) bool {
m := c.Model(t)

View File

@@ -18,6 +18,18 @@ const (
RunOnIndex RunType = "on-index" // Run manually and on-index.
)
// ReportRunType returns a human-readable string for the run type, preserving the
// explicit value when set or "auto" when delegation is in effect.
func ReportRunType(when RunType) string {
when = ParseRunType(when)
if when == RunAuto {
return "auto"
}
return when
}
// RunTypes maps configuration strings to standard RunType model settings.
var RunTypes = map[string]RunType{
RunAuto: RunAuto,

View File

@@ -123,6 +123,10 @@ func (c *Config) FaceEngine() string {
return c.options.FaceEngine
}
if vision.Config == nil {
return face.EngineNone
}
desired := face.ParseEngine(c.options.FaceEngine)
modelPath := c.FaceEngineModelPath()
@@ -152,6 +156,10 @@ func (c *Config) FaceEngineRunType() vision.RunType {
return vision.RunNever
}
if vision.Config == nil {
return vision.RunNever
}
if c.DisableFaces() || c.FaceEngine() == face.EngineNone {
return vision.RunNever
}

View File

@@ -145,54 +145,62 @@ func TestConfig_FaceEngine(t *testing.T) {
}
func TestConfig_FaceEngineRunType(t *testing.T) {
c := NewConfig(CliTestContext())
t.Run("AutoDefaults", func(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.FaceEngineThreads = 1
assert.Equal(t, "auto", vision.ReportRunType(c.FaceEngineRunType()))
c.options.FaceEngineThreads = 1
assert.Equal(t, "auto", vision.ReportRunType(c.FaceEngineRunType()))
c.options.DisableFaces = true
assert.Equal(t, "never", vision.ReportRunType(c.FaceEngineRunType()))
c.options.DisableFaces = false
c.options.DisableFaces = true
assert.Equal(t, "never", vision.ReportRunType(c.FaceEngineRunType()))
c.options.DisableFaces = false
c.options.FaceEngineThreads = 4
assert.Equal(t, "auto", vision.ReportRunType(c.FaceEngineRunType()))
}
func TestConfig_FaceEngineRunType_DisabledFaceModel(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() {
vision.Config = origVision
c.options.FaceEngineThreads = 4
assert.Equal(t, "auto", vision.ReportRunType(c.FaceEngineRunType()))
})
t.Run("DisabledFaceModel", func(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() { vision.Config = origVision })
c := NewConfig(CliTestContext())
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace, Disabled: true}}}
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
}
func TestConfig_FaceEngineRunType_NoFaceModel(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() {
vision.Config = origVision
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace, Disabled: true}}}
c := NewConfig(CliTestContext())
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
})
t.Run("NoFaceModel", func(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() { vision.Config = origVision })
c := NewConfig(CliTestContext())
vision.Config = &vision.ConfigValues{Models: vision.Models{}}
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
}
func TestConfig_FaceEngineRunType_DelegatesToVisionModel(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() {
vision.Config = origVision
vision.Config = &vision.ConfigValues{Models: vision.Models{}}
c := NewConfig(CliTestContext())
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
})
t.Run("DelegatesToVisionModel", func(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() { vision.Config = origVision })
c := NewConfig(CliTestContext())
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace}}}
m := vision.Config.Model(vision.ModelTypeFace)
require.NotNil(t, m)
m.Run = string(vision.RunOnSchedule)
require.Equal(t, vision.RunOnSchedule, vision.Config.RunType(vision.ModelTypeFace))
assert.Equal(t, vision.RunOnSchedule, c.FaceEngineRunType())
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace}}}
c := NewConfig(CliTestContext())
m := vision.Config.Model(vision.ModelTypeFace)
require.NotNil(t, m)
m.Run = vision.RunOnSchedule
require.Equal(t, vision.RunOnSchedule, vision.Config.RunType(vision.ModelTypeFace))
assert.Equal(t, vision.RunOnSchedule, c.FaceEngineRunType())
})
t.Run("VisionModelShouldRunFace", func(t *testing.T) {
origVision := vision.Config
t.Cleanup(func() { vision.Config = origVision })
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace}}}
c := NewConfig(CliTestContext())
m := vision.Config.Model(vision.ModelTypeFace)
require.NotNil(t, m)
m.Run = vision.RunOnSchedule
assert.True(t, c.VisionModelShouldRun(vision.ModelTypeFace, vision.RunOnSchedule))
m.Disabled = true
assert.False(t, c.VisionModelShouldRun(vision.ModelTypeFace, vision.RunOnSchedule))
})
}
func TestConfig_FaceEngineThreads(t *testing.T) {

View File

@@ -51,6 +51,10 @@ func (c *Config) VisionModelShouldRun(t vision.ModelType, when vision.RunType) b
return false
}
if t == vision.ModelTypeFace && c.DisableFaces() {
return false
}
if t == vision.ModelTypeLabels && c.DisableClassification() {
return false
}
@@ -63,6 +67,10 @@ func (c *Config) VisionModelShouldRun(t vision.ModelType, when vision.RunType) b
return false
}
if t == vision.ModelTypeFace {
return c.FaceEngineShouldRun(when)
}
return vision.Config.ShouldRun(t, when)
}

View File

@@ -1173,12 +1173,6 @@ var Flags = CliFlags{
Value: face.EngineAuto,
EnvVars: EnvVars("FACE_ENGINE"),
}}, {
Flag: &cli.StringFlag{
Name: "face-engine-run",
Usage: "face detection run `MODE` (auto, never, manual, newly-indexed, on-demand, on-index, on-schedule, always)",
Value: "auto",
EnvVars: EnvVars("FACE_ENGINE_RUN"),
}}, {
Flag: &cli.IntFlag{
Name: "face-engine-threads",
Usage: "face detection thread `COUNT` (0 uses half the available CPU cores)",

View File

@@ -231,7 +231,6 @@ type Options struct {
VisionFilter string `yaml:"VisionFilter" json:"VisionFilter" flag:"vision-filter"`
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
FaceEngine string `yaml:"FaceEngine" json:"-" flag:"face-engine"`
FaceEngineRun string `yaml:"FaceEngineRun" json:"-" flag:"face-engine-run"`
FaceEngineRetry bool `yaml:"-" json:"-" flag:"-"`
FaceEngineThreads int `yaml:"FaceEngineThreads" json:"-" flag:"face-engine-threads"`
FaceSize int `yaml:"-" json:"-" flag:"face-size"`

View File

@@ -5,6 +5,8 @@ import (
"strings"
"time"
"unicode/utf8"
"github.com/photoprism/photoprism/internal/ai/vision"
)
// Report returns global config values as a table for reporting.
@@ -284,7 +286,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
// Facial Recognition.
{"face-engine", c.FaceEngine()},
{"face-engine-run", c.FaceEngineRunType()},
{"face-engine-run", vision.ReportRunType(c.FaceEngineRunType())},
{"face-engine-threads", fmt.Sprintf("%d", c.FaceEngineThreads())},
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
{"face-score", fmt.Sprintf("%f", c.FaceScore())},

View File

@@ -48,8 +48,7 @@ func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchive
facesRunType = vision.RunOnIndex
}
result.DetectFaces = c.FaceEngineShouldRun(facesRunType)
result.DetectFaces = c.VisionModelShouldRun(vision.ModelTypeFace, facesRunType)
result.DetectNsfw = !facesOnly && c.VisionModelShouldRun(vision.ModelTypeNsfw, vision.RunOnIndex)
result.GenerateLabels = !facesOnly && c.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunOnIndex)

View File

@@ -65,7 +65,7 @@ func (w *Meta) Start(delay, interval time.Duration, force bool) (err error) {
labelsModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunNewlyIndexed)
captionModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeCaption, vision.RunNewlyIndexed)
nsfwModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeNsfw, vision.RunNewlyIndexed)
detectFaces := w.conf.FaceEngineShouldRun(vision.RunNewlyIndexed)
detectFaces := w.conf.VisionModelShouldRun(vision.ModelTypeFace, vision.RunNewlyIndexed)
if nsfwModelShouldRun {
log.Debugf("index: cannot run %s model on %s", vision.ModelTypeNsfw, vision.RunNewlyIndexed)

View File

@@ -57,6 +57,10 @@ func (w *Vision) StartScheduled() {
// scheduledModels returns the model types that should run for scheduled jobs.
func (w *Vision) scheduledModels() []string {
if w.conf == nil {
return nil
}
models := make([]string, 0, 4)
if w.conf.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunOnSchedule) {
@@ -103,10 +107,6 @@ func (w *Vision) Start(filter string, count int, models []string, customSrc stri
defer mutex.VisionWorker.Stop()
models = vision.FilterModels(models, runType, func(mt vision.ModelType, when vision.RunType) bool {
if mt == vision.ModelTypeFace {
return w.conf.FaceEngineShouldRun(when)
}
return w.conf.VisionModelShouldRun(mt, when)
})