Skip to content

Commit

Permalink
feat: refactor species action configuration from thresholds to a unif…
Browse files Browse the repository at this point in the history
…ied config structure

- Renamed ExecuteScriptAction to ExecuteCommandAction and updated its implementation to execute commands instead of scripts.
- Changed species action configuration from using thresholds to a new config structure, allowing for more flexible action definitions.
- Updated related components to reflect the new configuration structure, including UI adjustments in species settings for command execution.
- Enhanced the handling of species actions in the processor and rendering logic to accommodate the new configuration format.
  • Loading branch information
tphakala committed Jan 8, 2025
1 parent 37bcf4e commit 12e6d9a
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 75 deletions.
10 changes: 5 additions & 5 deletions internal/analysis/processor/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import (
"github.com/tphakala/birdnet-go/internal/datastore"
)

type ExecuteScriptAction struct {
ScriptPath string
Params map[string]interface{}
type ExecuteCommandAction struct {
Command string
Params map[string]interface{}
}

// A map to store the action configurations for different species
//var speciesActionsMap map[string]SpeciesActionConfig

func (a ExecuteScriptAction) Execute(data interface{}) error {
func (a ExecuteCommandAction) Execute(data interface{}) error {
//log.Println("Executing script:", a.ScriptPath)
// Type assertion to check if data is of type Detections
detection, ok := data.(Detections)
Expand All @@ -39,7 +39,7 @@ func (a ExecuteScriptAction) Execute(data interface{}) error {
}

// Executing the script with the provided arguments
cmd := exec.Command(a.ScriptPath, args...)
cmd := exec.Command(a.Command, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error executing script: %w, output: %s", err, string(output))
Expand Down
10 changes: 5 additions & 5 deletions internal/analysis/processor/workers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (p *Processor) getActionsForItem(detection Detections) []Action {
speciesName := strings.ToLower(detection.Note.CommonName)

// Check if species has custom configuration
if speciesConfig, exists := p.Settings.Realtime.Species.Thresholds[speciesName]; exists {
if speciesConfig, exists := p.Settings.Realtime.Species.Config[speciesName]; exists {
if p.Settings.Debug {
log.Println("Species config exists for custom actions")
}
Expand All @@ -64,11 +64,11 @@ func (p *Processor) getActionsForItem(detection Detections) []Action {
// Add custom actions from the new structure
for _, actionConfig := range speciesConfig.Actions {
switch actionConfig.Type {
case "ExecuteScript":
case "ExecuteCommand":
if len(actionConfig.Parameters) > 0 {
actions = append(actions, ExecuteScriptAction{
ScriptPath: actionConfig.Parameters[0],
Params: parseScriptParams(actionConfig.Parameters[1:], detection),
actions = append(actions, ExecuteCommandAction{
Command: actionConfig.Command,
Params: parseScriptParams(actionConfig.Parameters, detection),
})
}
case "SendNotification":
Expand Down
2 changes: 1 addition & 1 deletion internal/birdnet/range_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (bn *BirdNET) GetProbableSpecies(date time.Time, week float32) ([]SpeciesSc
}

// Process species with configured actions
for species := range bn.Settings.Realtime.Species.Thresholds {
for species := range bn.Settings.Realtime.Species.Config {
bn.Debug("Processing species with actions: %s", species)
addSpeciesWithMaxScore(bn, &speciesScores, species, processedSpecies)
}
Expand Down
27 changes: 14 additions & 13 deletions internal/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,24 @@ type RealtimeSettings struct {
Weather WeatherSettings // Weather provider related settings
}

// SpeciesSettings represents all species-related configuration
type SpeciesSettings struct {
Include []string // Species to always include regardless of range filter
Exclude []string // Species to always exclude from detection
Thresholds map[string]SpeciesThreshold // Per-species configuration including threshold and actions
// SpeciesAction represents a single action configuration
type SpeciesAction struct {
Type string `yaml:"type"` // Type of action (ExecuteCommand, etc)
Command string `yaml:"command"` // Path to the command to execute
Parameters []string `yaml:"parameters"` // Action parameters
}

// SpeciesThreshold represents per-species configuration
type SpeciesThreshold struct {
Confidence float32 // Detection confidence threshold
Actions []SpeciesAction // List of actions for this species
// SpeciesConfig represents configuration for a specific species
type SpeciesConfig struct {
Threshold float64 `yaml:"threshold"` // Confidence threshold
Actions []SpeciesAction `yaml:"actions"` // List of actions to execute
}

// SpeciesAction represents a single action configuration
type SpeciesAction struct {
Type string // Type of action (ExecuteScript, SendNotification, etc)
Parameters []string // Action parameters
// RealtimeSpeciesSettings contains all species-specific settings
type SpeciesSettings struct {
Include []string `yaml:"include"` // Always include these species
Exclude []string `yaml:"exclude"` // Always exclude these species
Config map[string]SpeciesConfig `yaml:"config"` // Per-species configuration
}

// ActionConfig holds configuration details for a specific action.
Expand Down
30 changes: 14 additions & 16 deletions internal/httpcontroller/handlers/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ var fieldsToSkip = map[string]bool{
"output.file.enabled": true,
"output.file.path": true,
"output.file.type": true,
//"realtime.species.threshold": true,
"realtime.species.actions": true,
}

// GetAudioDevices handles the request to list available audio devices
Expand Down Expand Up @@ -207,14 +205,14 @@ func updateStructFromForm(v reflect.Value, formValues map[string][]string, prefi
return err
}
}
} else if fieldType.Type == reflect.TypeOf(conf.SpeciesThreshold{}) {
// Special handling for SpeciesThreshold
if thresholdJSON, exists := formValues[fullName]; exists && len(thresholdJSON) > 0 {
var threshold conf.SpeciesThreshold
if err := json.Unmarshal([]byte(thresholdJSON[0]), &threshold); err != nil {
return fmt.Errorf("error unmarshaling species threshold for %s: %w", fullName, err)
} else if fieldType.Type == reflect.TypeOf(conf.SpeciesConfig{}) {
// Special handling for SpeciesConfig
if configJSON, exists := formValues[fullName]; exists && len(configJSON) > 0 {
var config conf.SpeciesConfig
if err := json.Unmarshal([]byte(configJSON[0]), &config); err != nil {
return fmt.Errorf("error unmarshaling species config for %s: %w", fullName, err)
}
field.Set(reflect.ValueOf(threshold))
field.Set(reflect.ValueOf(config))
}
} else {
//log.Println("Debug (updateStructFromForm): Updating struct field:", fullName)
Expand Down Expand Up @@ -307,14 +305,14 @@ func updateStructFromForm(v reflect.Value, formValues map[string][]string, prefi
}
case reflect.Map:
// Handle map fields
if fieldType.Type == reflect.TypeOf(map[string]conf.SpeciesThreshold{}) {
// Special handling for species thresholds map
if thresholdsJSON, exists := formValues[fullName]; exists && len(thresholdsJSON) > 0 {
var thresholds map[string]conf.SpeciesThreshold
if err := json.Unmarshal([]byte(thresholdsJSON[0]), &thresholds); err != nil {
return fmt.Errorf("error unmarshaling species thresholds for %s: %w", fullName, err)
if fieldType.Type == reflect.TypeOf(map[string]conf.SpeciesConfig{}) {
// Special handling for species config map
if configJSON, exists := formValues[fullName]; exists && len(configJSON) > 0 {
var configs map[string]conf.SpeciesConfig
if err := json.Unmarshal([]byte(configJSON[0]), &configs); err != nil {
return fmt.Errorf("error unmarshaling species configs for %s: %w", fullName, err)
}
field.Set(reflect.ValueOf(thresholds))
field.Set(reflect.ValueOf(configs))
}
} else {
return fmt.Errorf("unsupported map type for %s", fullName)
Expand Down
4 changes: 2 additions & 2 deletions internal/httpcontroller/template_renderers.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ func (s *Server) renderSettingsContent(c echo.Context) (template.HTML, error) {

// For thresholds, we need to handle the map specially
var thresholdStrings []string
for species, threshold := range s.Settings.Realtime.Species.Thresholds {
thresholdStrings = append(thresholdStrings, fmt.Sprintf("[%s: %f]", species, threshold))
for species, threshold := range s.Settings.Realtime.Species.Config {
thresholdStrings = append(thresholdStrings, fmt.Sprintf("[%s: %f]", species, threshold.Threshold))
}
}

Expand Down
67 changes: 34 additions & 33 deletions views/settings/speciesSettings.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
speciesSettings: {
Include: {{if .Settings.Realtime.Species.Include}}{{.Settings.Realtime.Species.Include | toJSON}}{{else}}[]{{end}},
Exclude: {{if .Settings.Realtime.Species.Exclude}}{{.Settings.Realtime.Species.Exclude | toJSON}}{{else}}[]{{end}},
Thresholds: {{if .Settings.Realtime.Species.Thresholds}}{{.Settings.Realtime.Species.Thresholds | toJSON}}{{else}}{}{{end}},
Config: {{if .Settings.Realtime.Species.Config}}{{.Settings.Realtime.Species.Config | toJSON}}{{else}}{}{{end}},
},
newIncludeSpecies: '',
newExcludeSpecies: '',
newThresholdSpecies: '',
newSpeciesConfig: '',
newThreshold: 0.5,
editMode: null,
editValue: '',
Expand All @@ -20,7 +20,7 @@
speciesSettingsOpen: false,
showActionsModal: false,
currentSpecies: '',
currentAction: { type: 'ExecuteScript', parameters: '' },
currentAction: { type: 'ExecuteCommand', parameters: '' },
resetChanges() {
this.hasChanges = false;
},
Expand All @@ -37,38 +37,39 @@
this.hasChanges = true;
},
addConfig() {
if (this.newThresholdSpecies && !this.speciesSettings.Thresholds[this.newThresholdSpecies]) {
this.speciesSettings.Thresholds[this.newThresholdSpecies] = {
Confidence: this.newThreshold,
if (this.newSpeciesConfig && !this.speciesSettings.Config[this.newSpeciesConfig]) {
this.speciesSettings.Config[this.newSpeciesConfig] = {
Threshold: this.newThreshold,
Actions: []
};
this.newThresholdSpecies = '';
this.newSpeciesConfig = '';
this.newThreshold = 0.5;
this.hasChanges = true;
}
},
removeConfig(species) {
delete this.speciesSettings.Thresholds[species];
delete this.speciesSettings.Config[species];
this.hasChanges = true;
},
openActionsModal(species) {
this.currentSpecies = species;
this.currentAction = this.speciesSettings.Thresholds[species]?.Actions?.[0] || { type: 'ExecuteScript', parameters: '' };
this.currentAction = this.speciesSettings.Config[species]?.Actions?.[0] || { type: 'ExecuteCommand', parameters: '' };
this.showActionsModal = true;
},
closeActionsModal() {
this.showActionsModal = false;
},
saveAction() {
if (!this.speciesSettings.Thresholds[this.currentSpecies]) {
this.speciesSettings.Thresholds[this.currentSpecies] = {
Confidence: 0.5,
if (!this.speciesSettings.Config[this.currentSpecies]) {
this.speciesSettings.Config[this.currentSpecies] = {
Threshold: 0.5,
Actions: []
};
}
this.speciesSettings.Thresholds[this.currentSpecies].Actions.push({
this.speciesSettings.Config[this.currentSpecies].Actions.push({
Type: this.currentAction.type,
CommandPath: this.currentAction.commandPath,
Parameters: this.currentAction.parameters.split(',').map(p => p.trim())
});
Expand Down Expand Up @@ -197,18 +198,18 @@

<div class="form-control relative mt-4">
<label class="label-text">
Custom Species Thresholds
Custom Species Configuration
<span class="ml-2 text-sm text-gray-500 cursor-help"
@mouseenter="showTooltip = 'customThresholds'"
@mouseenter="showTooltip = 'customConfig'"
@mouseleave="showTooltip = null"></span>
</label>

<div class="absolute left-0 bottom-full mb-2 p-2 bg-gray-100 text-sm rounded shadow-md z-50"
x-show="showTooltip === 'customThresholds'" x-cloak>
x-show="showTooltip === 'customConfig'" x-cloak>
Set custom confidence thresholds for specific species. These will override the global threshold for the specified species.
</div>

<!-- Custom thresholds list -->
<!-- Custom configuration list -->
<div class="space-y-2">
<!-- Header -->
<div class="flex bg-gray-100 p-2 rounded-md">
Expand All @@ -219,13 +220,13 @@
</div>

<!-- List items -->
<template x-for="(threshold, species) in speciesSettings.Thresholds" :key="species">
<template x-for="(config, species) in speciesSettings.Config" :key="species">
<div class="flex items-center bg-gray-50 p-2 rounded-md">
<div class="flex-grow text-sm pl-2" x-text="species"></div>
<div class="w-24 text-sm px-3" x-text="threshold.Confidence"></div>
<div class="w-24 text-sm px-3" x-text="config.Threshold"></div>
<div class="w-20 text-center">
<button type="button" @click="openActionsModal(species)"
class="btn btn-xs btn-secondary">Actions</button>
class="btn btn-xs">Edit</button>
</div>
<div class="w-20 text-center">
<button type="button" @click="removeConfig(species)"
Expand All @@ -235,11 +236,11 @@
</template>
</div>

<!-- Custom thresholds input -->
<!-- Custom configuration input -->
<div class="flex items-center mt-2">
<input type="text"
x-model="newThresholdSpecies"
@input="updatePredictions(newThresholdSpecies)"
x-model="newSpeciesConfig"
@input="updatePredictions(newSpeciesConfig)"
list="species-suggestions"
placeholder="Species"
class="input input-bordered input-sm flex-grow" />
Expand All @@ -256,8 +257,8 @@
step="0.01"
placeholder="Threshold" />

<button type="button" @click="openActionsModal(newThresholdSpecies)"
class="btn btn-sm btn-secondary ml-2">Actions</button>
<button type="button" @click="openActionsModal(newSpeciesConfig)"
class="btn btn-sm ml-2">Actions</button>
<button type="button" @click="addConfig()" class="btn btn-sm btn-primary ml-2 w-20">Add</button>
</div>
</div>
Expand All @@ -274,27 +275,27 @@ <h3 class="text-lg font-bold mb-4" x-text="'Actions for ' + currentSpecies"></h3
<select x-model="currentAction.type"
class="select select-bordered w-full mt-1"
disabled>
<option value="ExecuteScript">Execute Script</option>
<option value="ExecuteCommand">Execute Command</option>
</select>
</div>

<div class="mb-4">
<label class="block text-sm font-medium">Script Path</label>
<label class="block text-sm font-medium">Command</label>
<input type="text"
x-model="currentAction.scriptPath"
x-model="currentAction.command"
class="input input-bordered w-full mt-1"
placeholder="/path/to/your/script.sh">
<p class="text-sm text-gray-500 mt-1">Provide the full path to the script you want to execute</p>
placeholder="/path/to/your/command">
<p class="text-sm text-gray-500 mt-1">Provide the full path to the command you want to execute</p>
</div>

<div class="mb-4">
<label class="block text-sm font-medium">Script Parameters</label>
<label class="block text-sm font-medium">Parameters</label>
<input type="text"
x-model="currentAction.parameters"
class="input input-bordered w-full mt-1"
placeholder="Parameters will appear here"
readonly>
<p class="text-sm text-gray-500 mt-1">Click parameters below to add them to your script</p>
<p class="text-sm text-gray-500 mt-1">Click parameters below to add them to your command</p>
</div>

<div class="mb-4">
Expand Down Expand Up @@ -330,7 +331,7 @@ <h3 class="text-lg font-bold mb-4" x-text="'Actions for ' + currentSpecies"></h3
<!-- Hidden inputs to always submit the species settings -->
<input type="hidden" name="realtime.species.include" :value="JSON.stringify(speciesSettings.Include)">
<input type="hidden" name="realtime.species.exclude" :value="JSON.stringify(speciesSettings.Exclude)">
<input type="hidden" name="realtime.species.thresholds" :value="JSON.stringify(speciesSettings.Thresholds)">
<input type="hidden" name="realtime.species.config" :value="JSON.stringify(speciesSettings.Config)">
</div>
</div>

Expand Down

0 comments on commit 12e6d9a

Please sign in to comment.