Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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
}