calls_detect_anomalies.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"skraak/tools/calls"
)
func printDetectAnomaliesUsage() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls detect-anomalies [options]\n\n")
fmt.Fprintf(os.Stderr, "Compare corresponding segments across ML model filters and flag disagreements.\n")
fmt.Fprintf(os.Stderr, "Segments are matched by time overlap. Lonely segments (no overlap in all models) are skipped.\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
fmt.Fprintf(os.Stderr, " --folder <path> Folder containing .data files (required)\n")
fmt.Fprintf(os.Stderr, " --model <name> Filter name to compare (required, repeat for each model, min 2)\n")
fmt.Fprintf(os.Stderr, " --species <name> Scope to species or species+calltype (optional, repeat to add more)\n")
fmt.Fprintf(os.Stderr, "\nAnomaly types:\n")
fmt.Fprintf(os.Stderr, " label_mismatch Species or calltype disagrees across models\n")
fmt.Fprintf(os.Stderr, " certainty_mismatch Labels agree but certainty values differ\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " skraak calls detect-anomalies --folder ./data \\\n")
fmt.Fprintf(os.Stderr, " --model opensoundscape-kiwi-1.0 --model opensoundscape-kiwi-1.2\n")
fmt.Fprintf(os.Stderr, " skraak calls detect-anomalies --folder ./data \\\n")
fmt.Fprintf(os.Stderr, " --model opensoundscape-kiwi-1.0 --model opensoundscape-kiwi-1.2 --model opensoundscape-kiwi-1.5 \\\n")
fmt.Fprintf(os.Stderr, " --species Kiwi+Duet --species Kiwi+Male\n")
}
// runCallsDetectAnomalies compares segments across ML model filters and flags disagreements.
//
// JSON output schema:
//
// {
// "folder": string, // Folder path
// "models": [string], // Model filter names compared
// "files_examined": int, // Total .data files examined
// "files_with_all_models": int, // Files containing all specified models
// "anomalies_total": int, // Total anomalies found
// "label_mismatches": int, // Species/calltype disagreements
// "certainty_mismatches": int, // Certainty disagreements
// "anomalies": [ // Anomaly details (omitted if empty)
// {
// "file": string, // .data filename
// "type": string, // "label_mismatch" | "certainty_mismatch"
// "segments": [
// {
// "model": string, // Filter name
// "start": float, // Segment start (seconds)
// "end": float, // Segment end (seconds)
// "species": string, // Species name
// "calltype": string, // Call type (omitted if empty)
// "certainty": int // Certainty level (0-100)
// }
// ]
// }
// ],
// "error": string // Error message (omitted if empty)
// }
//
// detectAnomaliesArgs holds the parsed arguments.
type detectAnomaliesArgs struct {
folder string
models []string
species []string
}
// parseDetectAnomaliesArgs parses CLI arguments for the detect-anomalies command.
func parseDetectAnomaliesArgs(args []string) (detectAnomaliesArgs, error) {
var da detectAnomaliesArgs
i := 0
for i < len(args) {
arg := args[i]
switch arg {
case "--folder":
if i+1 >= len(args) {
return da, fmt.Errorf("--folder requires a value")
}
da.folder = args[i+1]
i += 2
case "--model":
if i+1 >= len(args) {
return da, fmt.Errorf("--model requires a value")
}
da.models = append(da.models, args[i+1])
i += 2
case "--species":
if i+1 >= len(args) {
return da, fmt.Errorf("--species requires a value")
}
da.species = append(da.species, args[i+1])
i += 2
case "--help", "-h":
printDetectAnomaliesUsage()
return da, nil
default:
fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n\n", arg)
printDetectAnomaliesUsage()
return da, fmt.Errorf("unknown flag: %s", arg)
}
}
return da, nil
}
// validateDetectAnomaliesArgs checks required flags.
func validateDetectAnomaliesArgs(da detectAnomaliesArgs) error {
if da.folder == "" {
printDetectAnomaliesUsage()
return fmt.Errorf("--folder is required")
}
if len(da.models) < 2 {
printDetectAnomaliesUsage()
return fmt.Errorf("at least 2 --model values required")
}
return nil
}
func runCallsDetectAnomalies(args []string) error {
da, err := parseDetectAnomaliesArgs(args)
if err != nil {
return err
}
if err := validateDetectAnomaliesArgs(da); err != nil {
return err
}
output, err := calls.DetectAnomalies(calls.DetectAnomaliesInput{
Folder: da.folder,
Models: da.models,
Species: da.species,
})
if err != nil {
return fmt.Errorf("detect-anomalies: %w", err)
}
fmt.Fprintf(os.Stderr, "Examined %d files, %d had all models\n",
output.FilesExamined, output.FilesWithAllModels)
fmt.Fprintf(os.Stderr, "Anomalies: %d total (%d label, %d certainty)\n",
output.AnomaliesTotal, output.LabelMismatches, output.CertaintyMismatches)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(output); err != nil {
return fmt.Errorf("encoding output: %w", err)
}
return nil
}