mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Faces: Remove PHOTOPRISM_FACE_ENGINE_RUN config option #5167
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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_SCORE` | `9.0` (with dynamic offsets) | Base quality threshold before scale adjustments. |
|
||||||
| `FACE_OVERLAP` | `42` | Maximum allowed IoU when deduplicating markers. |
|
| `FACE_OVERLAP` | `42` | Maximum allowed IoU when deduplicating markers. |
|
||||||
|
|
||||||
|
Run scheduling is configured through the face model entry in `vision.yml`. Adjust the model’s `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.
|
> 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
|
### Benchmark Reference
|
||||||
|
|||||||
@@ -164,6 +164,21 @@ func (c *ConfigValues) ShouldRun(t ModelType, when RunType) bool {
|
|||||||
return m.ShouldRun(when)
|
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.
|
// IsDefault checks whether the specified type is the built-in default model.
|
||||||
func (c *ConfigValues) IsDefault(t ModelType) bool {
|
func (c *ConfigValues) IsDefault(t ModelType) bool {
|
||||||
m := c.Model(t)
|
m := c.Model(t)
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ const (
|
|||||||
RunOnIndex RunType = "on-index" // Run manually and on-index.
|
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.
|
// RunTypes maps configuration strings to standard RunType model settings.
|
||||||
var RunTypes = map[string]RunType{
|
var RunTypes = map[string]RunType{
|
||||||
RunAuto: RunAuto,
|
RunAuto: RunAuto,
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ func (c *Config) FaceEngine() string {
|
|||||||
return c.options.FaceEngine
|
return c.options.FaceEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vision.Config == nil {
|
||||||
|
return face.EngineNone
|
||||||
|
}
|
||||||
|
|
||||||
desired := face.ParseEngine(c.options.FaceEngine)
|
desired := face.ParseEngine(c.options.FaceEngine)
|
||||||
modelPath := c.FaceEngineModelPath()
|
modelPath := c.FaceEngineModelPath()
|
||||||
|
|
||||||
@@ -152,6 +156,10 @@ func (c *Config) FaceEngineRunType() vision.RunType {
|
|||||||
return vision.RunNever
|
return vision.RunNever
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vision.Config == nil {
|
||||||
|
return vision.RunNever
|
||||||
|
}
|
||||||
|
|
||||||
if c.DisableFaces() || c.FaceEngine() == face.EngineNone {
|
if c.DisableFaces() || c.FaceEngine() == face.EngineNone {
|
||||||
return vision.RunNever
|
return vision.RunNever
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,54 +145,62 @@ func TestConfig_FaceEngine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_FaceEngineRunType(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
|
c.options.DisableFaces = true
|
||||||
assert.Equal(t, "auto", vision.ReportRunType(c.FaceEngineRunType()))
|
assert.Equal(t, "never", vision.ReportRunType(c.FaceEngineRunType()))
|
||||||
|
c.options.DisableFaces = false
|
||||||
|
|
||||||
c.options.DisableFaces = true
|
c.options.FaceEngineThreads = 4
|
||||||
assert.Equal(t, "never", vision.ReportRunType(c.FaceEngineRunType()))
|
assert.Equal(t, "auto", 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
|
|
||||||
})
|
})
|
||||||
|
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}}}
|
||||||
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace, Disabled: true}}}
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
|
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_FaceEngineRunType_NoFaceModel(t *testing.T) {
|
|
||||||
origVision := vision.Config
|
|
||||||
t.Cleanup(func() {
|
|
||||||
vision.Config = origVision
|
|
||||||
})
|
})
|
||||||
|
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{}}
|
||||||
vision.Config = &vision.ConfigValues{Models: vision.Models{}}
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
|
assert.Equal(t, vision.RunNever, c.FaceEngineRunType())
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_FaceEngineRunType_DelegatesToVisionModel(t *testing.T) {
|
|
||||||
origVision := vision.Config
|
|
||||||
t.Cleanup(func() {
|
|
||||||
vision.Config = origVision
|
|
||||||
})
|
})
|
||||||
|
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}}}
|
||||||
vision.Config = &vision.ConfigValues{Models: vision.Models{{Type: vision.ModelTypeFace}}}
|
c := NewConfig(CliTestContext())
|
||||||
m := vision.Config.Model(vision.ModelTypeFace)
|
m := vision.Config.Model(vision.ModelTypeFace)
|
||||||
require.NotNil(t, m)
|
require.NotNil(t, m)
|
||||||
m.Run = string(vision.RunOnSchedule)
|
m.Run = vision.RunOnSchedule
|
||||||
require.Equal(t, vision.RunOnSchedule, vision.Config.RunType(vision.ModelTypeFace))
|
require.Equal(t, vision.RunOnSchedule, vision.Config.RunType(vision.ModelTypeFace))
|
||||||
assert.Equal(t, vision.RunOnSchedule, c.FaceEngineRunType())
|
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) {
|
func TestConfig_FaceEngineThreads(t *testing.T) {
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ func (c *Config) VisionModelShouldRun(t vision.ModelType, when vision.RunType) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t == vision.ModelTypeFace && c.DisableFaces() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if t == vision.ModelTypeLabels && c.DisableClassification() {
|
if t == vision.ModelTypeLabels && c.DisableClassification() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -63,6 +67,10 @@ func (c *Config) VisionModelShouldRun(t vision.ModelType, when vision.RunType) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t == vision.ModelTypeFace {
|
||||||
|
return c.FaceEngineShouldRun(when)
|
||||||
|
}
|
||||||
|
|
||||||
return vision.Config.ShouldRun(t, when)
|
return vision.Config.ShouldRun(t, when)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1173,12 +1173,6 @@ var Flags = CliFlags{
|
|||||||
Value: face.EngineAuto,
|
Value: face.EngineAuto,
|
||||||
EnvVars: EnvVars("FACE_ENGINE"),
|
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{
|
Flag: &cli.IntFlag{
|
||||||
Name: "face-engine-threads",
|
Name: "face-engine-threads",
|
||||||
Usage: "face detection thread `COUNT` (0 uses half the available CPU cores)",
|
Usage: "face detection thread `COUNT` (0 uses half the available CPU cores)",
|
||||||
|
|||||||
@@ -231,7 +231,6 @@ type Options struct {
|
|||||||
VisionFilter string `yaml:"VisionFilter" json:"VisionFilter" flag:"vision-filter"`
|
VisionFilter string `yaml:"VisionFilter" json:"VisionFilter" flag:"vision-filter"`
|
||||||
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
||||||
FaceEngine string `yaml:"FaceEngine" json:"-" flag:"face-engine"`
|
FaceEngine string `yaml:"FaceEngine" json:"-" flag:"face-engine"`
|
||||||
FaceEngineRun string `yaml:"FaceEngineRun" json:"-" flag:"face-engine-run"`
|
|
||||||
FaceEngineRetry bool `yaml:"-" json:"-" flag:"-"`
|
FaceEngineRetry bool `yaml:"-" json:"-" flag:"-"`
|
||||||
FaceEngineThreads int `yaml:"FaceEngineThreads" json:"-" flag:"face-engine-threads"`
|
FaceEngineThreads int `yaml:"FaceEngineThreads" json:"-" flag:"face-engine-threads"`
|
||||||
FaceSize int `yaml:"-" json:"-" flag:"face-size"`
|
FaceSize int `yaml:"-" json:"-" flag:"face-size"`
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ai/vision"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Report returns global config values as a table for reporting.
|
// Report returns global config values as a table for reporting.
|
||||||
@@ -284,7 +286,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
|
|
||||||
// Facial Recognition.
|
// Facial Recognition.
|
||||||
{"face-engine", c.FaceEngine()},
|
{"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-engine-threads", fmt.Sprintf("%d", c.FaceEngineThreads())},
|
||||||
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
|
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
|
||||||
{"face-score", fmt.Sprintf("%f", c.FaceScore())},
|
{"face-score", fmt.Sprintf("%f", c.FaceScore())},
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchive
|
|||||||
facesRunType = vision.RunOnIndex
|
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.DetectNsfw = !facesOnly && c.VisionModelShouldRun(vision.ModelTypeNsfw, vision.RunOnIndex)
|
||||||
result.GenerateLabels = !facesOnly && c.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunOnIndex)
|
result.GenerateLabels = !facesOnly && c.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunOnIndex)
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (w *Meta) Start(delay, interval time.Duration, force bool) (err error) {
|
|||||||
labelsModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunNewlyIndexed)
|
labelsModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunNewlyIndexed)
|
||||||
captionModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeCaption, vision.RunNewlyIndexed)
|
captionModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeCaption, vision.RunNewlyIndexed)
|
||||||
nsfwModelShouldRun := w.conf.VisionModelShouldRun(vision.ModelTypeNsfw, 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 {
|
if nsfwModelShouldRun {
|
||||||
log.Debugf("index: cannot run %s model on %s", vision.ModelTypeNsfw, vision.RunNewlyIndexed)
|
log.Debugf("index: cannot run %s model on %s", vision.ModelTypeNsfw, vision.RunNewlyIndexed)
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ func (w *Vision) StartScheduled() {
|
|||||||
|
|
||||||
// scheduledModels returns the model types that should run for scheduled jobs.
|
// scheduledModels returns the model types that should run for scheduled jobs.
|
||||||
func (w *Vision) scheduledModels() []string {
|
func (w *Vision) scheduledModels() []string {
|
||||||
|
if w.conf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
models := make([]string, 0, 4)
|
models := make([]string, 0, 4)
|
||||||
|
|
||||||
if w.conf.VisionModelShouldRun(vision.ModelTypeLabels, vision.RunOnSchedule) {
|
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()
|
defer mutex.VisionWorker.Stop()
|
||||||
|
|
||||||
models = vision.FilterModels(models, runType, func(mt vision.ModelType, when vision.RunType) bool {
|
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)
|
return w.conf.VisionModelShouldRun(mt, when)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user