From 56b8ba0631bef339bbb681ad2c7b1c799aec18be Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Tue, 7 Jan 2025 22:26:15 +0200 Subject: [PATCH] refactor: streamline species configuration handling and improve action processing - Removed the deprecated species configuration loading logic, consolidating species settings into a new structure for better clarity and maintainability. - Updated action processing to utilize the new species thresholds and action configurations, enhancing the filtering logic for species inclusion/exclusion. - Introduced helper functions for better management of script parameters and action handling, ensuring a more modular approach. - Improved logging for debugging purposes, providing clearer insights into species configuration usage during processing. --- internal/analysis/processor/processor.go | 42 +++-- internal/analysis/processor/species_config.go | 165 ------------------ internal/analysis/processor/workers.go | 53 ++++-- 3 files changed, 63 insertions(+), 197 deletions(-) delete mode 100644 internal/analysis/processor/species_config.go diff --git a/internal/analysis/processor/processor.go b/internal/analysis/processor/processor.go index fda61bfa..9da235be 100644 --- a/internal/analysis/processor/processor.go +++ b/internal/analysis/processor/processor.go @@ -99,9 +99,6 @@ func New(settings *conf.Settings, ds datastore.Interface, bn *birdnet.BirdNET, m // Start the held detection flusher p.pendingDetectionsFlusher() - // Load Species configs - p.Settings.Realtime.Species, _ = LoadSpeciesConfig(conf.SpeciesConfigCSV) - // Initialize BirdWeather client if enabled in settings if settings.Realtime.Birdweather.Enabled { var err error @@ -270,11 +267,16 @@ func (p *Processor) processResults(item queue.Results) []Detections { continue } - // Match against location-based filter - //if !p.Settings.IsSpeciesIncluded(result.Species) { - if !p.Settings.IsSpeciesIncluded(scientificName) { + // Check include/exclude lists from new structure + if len(p.Settings.Realtime.Species.Include) > 0 { + if !contains(p.Settings.Realtime.Species.Include, speciesLowercase) { + continue + } + } + + if contains(p.Settings.Realtime.Species.Exclude, speciesLowercase) { if p.Settings.Debug { - log.Printf("Species not on included list: %s\n", commonName) + log.Printf("Species excluded: %s\n", commonName) } continue } @@ -327,13 +329,16 @@ func (p *Processor) handleHumanDetection(item queue.Results, speciesLowercase st // getBaseConfidenceThreshold retrieves the confidence threshold for a species, using custom or global thresholds. func (p *Processor) getBaseConfidenceThreshold(speciesLowercase string) float32 { - confidenceThreshold, exists := p.Settings.Realtime.Species.Threshold[speciesLowercase] - if !exists { - confidenceThreshold = float32(p.Settings.BirdNET.Threshold) - } else if p.Settings.Debug { - log.Printf("\nUsing confidence threshold of %.2f for %s\n", confidenceThreshold, speciesLowercase) + // Check if species has a custom threshold in the new structure + if threshold, exists := p.Settings.Realtime.Species.Thresholds[speciesLowercase]; exists { + if p.Settings.Debug { + log.Printf("\nUsing custom confidence threshold of %.2f for %s\n", threshold.Confidence, speciesLowercase) + } + return float32(threshold.Confidence) } - return confidenceThreshold + + // Fall back to global threshold + return float32(p.Settings.BirdNET.Threshold) } // generateClipName generates a clip name for the given scientific name and confidence. @@ -474,3 +479,14 @@ func (p *Processor) pendingDetectionsFlusher() { } }() } + +// Helper function to check if a slice contains a string (case-insensitive) +func contains(slice []string, item string) bool { + item = strings.ToLower(item) + for _, s := range slice { + if strings.ToLower(s) == item { + return true + } + } + return false +} diff --git a/internal/analysis/processor/species_config.go b/internal/analysis/processor/species_config.go deleted file mode 100644 index 07e01f42..00000000 --- a/internal/analysis/processor/species_config.go +++ /dev/null @@ -1,165 +0,0 @@ -package processor - -import ( - "encoding/csv" - "fmt" - "log" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/tphakala/birdnet-go/internal/conf" -) - -func LoadSpeciesConfig(fileName string) (conf.SpeciesSettings, error) { - var speciesConfig conf.SpeciesSettings - speciesConfig.Threshold = make(map[string]float32) - speciesConfig.Actions = make(map[string]conf.SpeciesActionConfig) - - // Retrieve the default config paths. - configPaths, err := conf.GetDefaultConfigPaths() - if err != nil { - return conf.SpeciesSettings{}, fmt.Errorf("error getting default config paths: %w", err) - } - - var file *os.File - - // Try to open the file in one of the default config paths. - for _, path := range configPaths { - fullPath := filepath.Join(path, fileName) - file, err = os.Open(fullPath) - if err == nil { - break - } - } - - if file == nil { - return conf.SpeciesSettings{}, fmt.Errorf("file '%s' not found in default config paths", fileName) - } - defer file.Close() - - reader := csv.NewReader(file) - reader.Comment = '#' // Set comment character - reader.FieldsPerRecord = -1 // Allow a variable number of fields - - log.Println("Loading species config from file:", fileName) - - records, err := reader.ReadAll() - if err != nil { - log.Printf("Error reading CSV file '%s': %v", fileName, err) - return conf.SpeciesSettings{}, fmt.Errorf("error reading CSV file '%s': %w", fileName, err) - } - - for _, record := range records { - if len(record) < 2 { - log.Printf("Invalid line in species config: %v", record) - continue // Skip malformed lines - } - - species := strings.ToLower(strings.TrimSpace(record[0])) - confidence, err := strconv.ParseFloat(strings.TrimSpace(record[1]), 32) - if err != nil { - log.Printf("Invalid confidence value for species '%s': %v", species, err) - continue - } else { - if conf.Setting().Debug { - log.Printf("Config loaded species: %s confidence: %.3f\n", species, confidence) - } - } - - // Initialize default actions and exclude - var actions []conf.ActionConfig - var exclude []string - onlyActions := false - - // If the line has action configurations - if len(record) > 2 { - actions, exclude, onlyActions = parseActions(strings.TrimSpace(record[2])) - } - - speciesConfig.Threshold[species] = float32(confidence) - speciesConfig.Actions[species] = conf.SpeciesActionConfig{ - SpeciesName: species, - Actions: actions, - Exclude: exclude, - OnlyActions: onlyActions, - } - } - - return speciesConfig, nil -} - -// parseActions interprets the action string from the CSV and returns action configurations. -func parseActions(actionStr string) (actions []conf.ActionConfig, exclude []string, onlyActions bool) { - actionList := strings.Split(actionStr, ";") - onlyActions = true - - for _, action := range actionList { - actionDetails := strings.Split(action, ":") - actionType := strings.TrimPrefix(actionDetails[0], "+") - - // Check for exclusion - if strings.HasPrefix(actionDetails[0], "-") { - exclude = append(exclude, strings.TrimPrefix(actionDetails[0], "-")) - continue - } - - // Handle inclusion and parameters - actions = append(actions, conf.ActionConfig{ - Type: actionType, - Parameters: actionDetails[1:], // Parameters follow the action type - }) - - if strings.HasPrefix(actionDetails[0], "+") { - onlyActions = false - } - } - - return actions, exclude, onlyActions -} - -// createActionsFromConfig creates actions based on the given species configuration and detection. -func (p *Processor) createActionsFromConfig(speciesConfig conf.SpeciesActionConfig, detection Detections) []Action { - var actions []Action - - for _, actionConfig := range speciesConfig.Actions { - // Skip excluded actions - if contains(speciesConfig.Exclude, actionConfig.Type) { - continue - } - - // Handle different action types - switch actionConfig.Type { - case "ExecuteScript": - if len(actionConfig.Parameters) >= 1 { - scriptPath := actionConfig.Parameters[0] - scriptParams := make(map[string]interface{}) - for _, paramName := range actionConfig.Parameters[1:] { - scriptParams[paramName] = getNoteValueByName(detection.Note, paramName) - } - actions = append(actions, ExecuteScriptAction{ - ScriptPath: scriptPath, - Params: scriptParams, - }) - } - case "SendNotification": - // Create SendNotification action - // ... implementation ... - } - - // Add more cases for additional action types as needed - } - - return actions -} - -// contains checks if a slice of strings contains a specific string. -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} diff --git a/internal/analysis/processor/workers.go b/internal/analysis/processor/workers.go index 78ae669b..6ac96a25 100644 --- a/internal/analysis/processor/workers.go +++ b/internal/analysis/processor/workers.go @@ -51,34 +51,49 @@ func (p *Processor) actionWorker() { // getActionsForItem determines the actions to be taken for a given detection. func (p *Processor) getActionsForItem(detection Detections) []Action { - // match lower case speciesName := strings.ToLower(detection.Note.CommonName) - speciesConfig, exists := p.Settings.Realtime.Species.Actions[speciesName] - var actions []Action - if exists { + // Check if species has custom configuration + if speciesConfig, exists := p.Settings.Realtime.Species.Thresholds[speciesName]; exists { if p.Settings.Debug { log.Println("Species config exists for custom actions") } - customActions := p.createActionsFromConfig(speciesConfig, detection) - - // Determine whether to use only custom actions or combine with default actions - if speciesConfig.OnlyActions { - //log.Println("Only using custom actions for", speciesName) - actions = customActions - } else { - //log.Println("Using default actions with custom actions for", speciesName) - defaultActions := p.getDefaultActions(detection) - actions = append(defaultActions, customActions...) + + var actions []Action + + // Add custom actions from the new structure + for _, actionConfig := range speciesConfig.Actions { + switch actionConfig.Type { + case "ExecuteScript": + if len(actionConfig.Parameters) > 0 { + actions = append(actions, ExecuteScriptAction{ + ScriptPath: actionConfig.Parameters[0], + Params: parseScriptParams(actionConfig.Parameters[1:], detection), + }) + } + case "SendNotification": + // Add notification action handling + // ... implementation ... + } } - } else { - if p.Settings.Debug { - log.Println("No species config found, using default actions for", speciesName) + + // If OnlyActions is true, return only custom actions + if len(actions) > 0 { + return actions } - actions = p.getDefaultActions(detection) } - return actions + // Fall back to default actions if no custom actions or if custom actions should be combined + return p.getDefaultActions(detection) +} + +// Helper function to parse script parameters +func parseScriptParams(params []string, detection Detections) map[string]interface{} { + scriptParams := make(map[string]interface{}) + for _, param := range params { + scriptParams[param] = getNoteValueByName(detection.Note, param) + } + return scriptParams } // getDefaultActions returns the default actions to be taken for a given detection.