calls_remove.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"strings"
"skraak/config"
"skraak/tools/calls"
)
func printRemoveUsage() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls remove [options]\n\n")
fmt.Fprintf(os.Stderr, "Remove a label from a .data file.\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
fmt.Fprintf(os.Stderr, " --file <path> Path to .data file (required)\n")
fmt.Fprintf(os.Stderr, " --segment <st-en> Time range, e.g., 12.3-15.7 or 12-15 (required)\n")
fmt.Fprintf(os.Stderr, " --frequency <lo-hi> Frequency range, e.g., 200-4500 (optional; required if ambiguous)\n")
fmt.Fprintf(os.Stderr, " --species <name> Species, optionally with calltype (required, e.g., Kiwi, Kiwi+Duet)\n")
fmt.Fprintf(os.Stderr, " --filter <name> Filter name (default: Manual)\n")
fmt.Fprintf(os.Stderr, " --reviewer <name> Reviewer name (default: from config)\n")
fmt.Fprintf(os.Stderr, "\nBehavior:\n")
fmt.Fprintf(os.Stderr, " Removes the matching label. If the segment has no labels left, removes the segment.\n")
fmt.Fprintf(os.Stderr, " If the .data file has no segments left, removes the .data file.\n")
fmt.Fprintf(os.Stderr, " If --calltype is omitted, matches any calltype (errors if ambiguous).\n")
fmt.Fprintf(os.Stderr, " If --frequency is omitted, matches by time only (errors if multiple segments match).\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " # Remove a Kiwi label\n")
fmt.Fprintf(os.Stderr, " skraak calls remove --file recording.wav.data --segment 12-15 --species Kiwi\n\n")
fmt.Fprintf(os.Stderr, " # Remove a specific calltype\n")
fmt.Fprintf(os.Stderr, " skraak calls remove --file recording.wav.data --segment 12-15 --species Kiwi+Duet\n\n")
fmt.Fprintf(os.Stderr, " # Remove with frequency to disambiguate\n")
fmt.Fprintf(os.Stderr, " skraak calls remove --file recording.wav.data --segment 12-15 --frequency 200-4500 --species Kiwi\n")
}
// removeArgs holds parsed CLI arguments for the remove command.
type removeArgs struct {
file string
segment string
frequency string
species string
filter string
reviewer string
}
// removeFlagHandler returns a flag handler for removeArgs.
func removeFlagHandler(arg string, ra *removeArgs) (func(string), bool, bool) {
switch arg {
case "--file":
return func(v string) { ra.file = v }, false, true
case "--segment":
return func(v string) { ra.segment = v }, false, true
case "--frequency":
return func(v string) { ra.frequency = v }, false, true
case "--species":
return func(v string) { ra.species = v }, false, true
case "--filter":
return func(v string) { ra.filter = v }, false, true
case "--reviewer":
return func(v string) { ra.reviewer = v }, false, true
default:
return nil, false, false
}
}
// parseRemoveArgs parses the command-line arguments for the remove subcommand.
func parseRemoveArgs(args []string) removeArgs {
var ra removeArgs
i := 0
for i < len(args) {
arg := args[i]
if arg == "-h" || arg == "--help" {
printRemoveUsage()
os.Exit(0)
}
handler, isBool, handled := removeFlagHandler(arg, &ra)
if !handled {
handleUnknownFlag(arg)
i++
continue
}
if isBool {
i++
} else {
handler(mustValue(args, &i, arg))
}
}
return ra
}
// validateRemoveArgs checks required flags and resolves defaults.
func validateRemoveArgs(ra *removeArgs) error {
missing := []string{}
if ra.file == "" {
missing = append(missing, "--file")
}
if ra.segment == "" {
missing = append(missing, "--segment")
}
if ra.species == "" {
missing = append(missing, "--species")
}
if len(missing) > 0 {
printRemoveUsage()
return fmt.Errorf("missing required flags: %s", strings.Join(missing, ", "))
}
// Apply defaults
if ra.filter == "" {
ra.filter = "Manual"
}
// Resolve reviewer from config if not specified
if ra.reviewer == "" {
cfg, cfgPath, err := config.LoadConfig()
if err != nil {
return fmt.Errorf("--reviewer not provided and config not found: %s", cfgPath)
}
if cfg.Classify.Reviewer == "" {
return fmt.Errorf("--reviewer not provided and %s missing classify.reviewer", cfgPath)
}
ra.reviewer = cfg.Classify.Reviewer
}
return nil
}
// RunCallsRemove handles the "calls remove" subcommand
//
// JSON output schema:
//
// {
// "file": string,
// "segment_start": float,
// "segment_end": float,
// "low_freq": float,
// "high_freq": float,
// "species": string,
// "calltype": string,
// "filter": string,
// "removed": string, // "label", "segment", or "file"
// "error": string // omitted if no error
// }
func RunCallsRemove(args []string) error {
ra := parseRemoveArgs(args)
if err := validateRemoveArgs(&ra); err != nil {
return err
}
input := calls.CallsRemoveInput{
File: ra.file,
Segment: ra.segment,
Frequency: ra.frequency,
Species: ra.species,
Filter: ra.filter,
Reviewer: ra.reviewer,
}
result, err := calls.CallsRemove(input)
if err != nil {
return fmt.Errorf("remove: %s", result.Error)
}
data, _ := json.Marshal(result)
fmt.Println(string(data))
return nil
}