calls_add.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"skraak/config"
"skraak/tools/calls"
)
func printAddUsage() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls add [options]\n\n")
fmt.Fprintf(os.Stderr, "Add a segment/label to 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; default: 0-sample_rate)\n")
fmt.Fprintf(os.Stderr, " --species <name> Species, optionally with calltype (required, e.g., Kiwi, Kiwi+Duet)\n")
fmt.Fprintf(os.Stderr, " --certainty <int> Certainty 0-100 (default: 100)\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, " If .data file does not exist, creates it with Operator=Manual, Duration from WAV.\n")
fmt.Fprintf(os.Stderr, " If segment already exists with same time+freq, adds label (errors if same filter).\n")
fmt.Fprintf(os.Stderr, " If --frequency is omitted, low_freq=0, high_freq=WAV sample rate.\n")
fmt.Fprintf(os.Stderr, " Segment end is clamped to WAV duration; segment start >= duration is rejected.\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " # Add a Kiwi segment\n")
fmt.Fprintf(os.Stderr, " skraak calls add --file recording.wav.data --segment 12-15 --species Kiwi\n\n")
fmt.Fprintf(os.Stderr, " # Add with calltype and custom certainty\n")
fmt.Fprintf(os.Stderr, " skraak calls add --file recording.wav.data --segment 12.3-15.7 --species Kiwi+Duet --certainty 90\n\n")
fmt.Fprintf(os.Stderr, " # Add with frequency range\n")
fmt.Fprintf(os.Stderr, " skraak calls add --file recording.wav.data --segment 12-15 --frequency 200-4500 --species Kiwi\n")
}
// addArgs holds parsed CLI arguments for the add command.
type addArgs struct {
file string
segment string
frequency string
species string
certainty int
filter string
reviewer string
}
// addFlagHandler returns a flag handler for addArgs.
func addFlagHandler(arg string, aa *addArgs) (func(string), bool, bool) {
switch arg {
case "--file":
return func(v string) { aa.file = v }, false, true
case "--segment":
return func(v string) { aa.segment = v }, false, true
case "--frequency":
return func(v string) { aa.frequency = v }, false, true
case "--species":
return func(v string) { aa.species = v }, false, true
case "--certainty":
return func(v string) {
n, err := strconv.Atoi(v)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")
os.Exit(1)
}
aa.certainty = n
}, false, true
case "--filter":
return func(v string) { aa.filter = v }, false, true
case "--reviewer":
return func(v string) { aa.reviewer = v }, false, true
default:
return nil, false, false
}
}
// parseAddArgs parses the command-line arguments for the add subcommand.
func parseAddArgs(args []string) addArgs {
var aa addArgs
i := 0
for i < len(args) {
arg := args[i]
if arg == "-h" || arg == "--help" {
printAddUsage()
os.Exit(0)
}
handler, isBool, handled := addFlagHandler(arg, &aa)
if !handled {
handleUnknownFlag(arg)
i++
continue
}
if isBool {
i++
} else {
handler(mustValue(args, &i, arg))
}
}
return aa
}
// validateAddArgs checks required flags and resolves defaults.
func validateAddArgs(aa *addArgs) error {
missing := []string{}
if aa.file == "" {
missing = append(missing, "--file")
}
if aa.segment == "" {
missing = append(missing, "--segment")
}
if aa.species == "" {
missing = append(missing, "--species")
}
if len(missing) > 0 {
printAddUsage()
return fmt.Errorf("missing required flags: %s", strings.Join(missing, ", "))
}
// Apply defaults
if aa.certainty == 0 {
aa.certainty = 100
}
if aa.filter == "" {
aa.filter = "Manual"
}
// Resolve reviewer from config if not specified
if aa.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)
}
aa.reviewer = cfg.Classify.Reviewer
}
return nil
}
// RunCallsAdd handles the "calls add" subcommand
//
// JSON output schema:
//
// {
// "file": string,
// "segment_start": float,
// "segment_end": float,
// "low_freq": float,
// "high_freq": float,
// "species": string,
// "calltype": string,
// "filter": string,
// "certainty": int,
// "created": bool, // true = new segment, false = label added to existing
// "error": string // omitted if no error
// }
func RunCallsAdd(args []string) error {
aa := parseAddArgs(args)
if err := validateAddArgs(&aa); err != nil {
return err
}
input := calls.CallsAddInput{
File: aa.file,
Segment: aa.segment,
Frequency: aa.frequency,
Species: aa.species,
Certainty: aa.certainty,
Filter: aa.filter,
Reviewer: aa.reviewer,
}
result, err := calls.CallsAdd(input)
if err != nil {
return fmt.Errorf("add: %s", result.Error)
}
data, _ := json.Marshal(result)
fmt.Println(string(data))
return nil
}