calls_modify.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"skraak/tools/calls"
)
func printModifyUsage() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls modify [options]\n\n")
fmt.Fprintf(os.Stderr, "Modify a label in 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, " --reviewer <name> Reviewer name (required)\n")
fmt.Fprintf(os.Stderr, " --filter <name> Filter name to match labels (required)\n")
fmt.Fprintf(os.Stderr, " --segment <start-end> Segment time range in integer seconds (required, e.g., 12-15)\n")
fmt.Fprintf(os.Stderr, " --certainty <int> Certainty value 0-100 (required)\n")
fmt.Fprintf(os.Stderr, " --species <name> Species to set (e.g., Kiwi, Kiwi+Male, Noise)\n")
fmt.Fprintf(os.Stderr, " --bookmark Mark segment as bookmarked for navigation\n")
fmt.Fprintf(os.Stderr, " --comment <text> User comment (max 140 chars, ASCII only)\n")
fmt.Fprintf(os.Stderr, "\nSegment matching:\n")
fmt.Fprintf(os.Stderr, " Segments are matched by floor(start) and ceil(end) times.\n")
fmt.Fprintf(os.Stderr, " For example, a segment from 12.3s to 14.5s matches --segment 12-15.\n")
fmt.Fprintf(os.Stderr, "\nBehavior:\n")
fmt.Fprintf(os.Stderr, " Always updates reviewer on file metadata.\n")
fmt.Fprintf(os.Stderr, " If all specified values match current values, no modification is made.\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " # Change species and certainty (incorrect classification)\n")
fmt.Fprintf(os.Stderr, " skraak calls modify --file recording.data --reviewer GLM-5 \\\n")
fmt.Fprintf(os.Stderr, " --filter mymodel --segment 12-15 --species Kiwi+Male --certainty 100\n\n")
fmt.Fprintf(os.Stderr, " # Change certainty only (correct classification)\n")
fmt.Fprintf(os.Stderr, " skraak calls modify --file recording.data --reviewer GLM-5 \\\n")
fmt.Fprintf(os.Stderr, " --filter mymodel --segment 12-15 --certainty 100\n\n")
fmt.Fprintf(os.Stderr, " # Change to Noise (clears calltype)\n")
fmt.Fprintf(os.Stderr, " skraak calls modify --file recording.data --reviewer GLM-5 \\\n")
fmt.Fprintf(os.Stderr, " --filter mymodel --segment 67-88 --species Noise --certainty 100\n\n")
fmt.Fprintf(os.Stderr, " # Bookmark a segment for later review\n")
fmt.Fprintf(os.Stderr, " skraak calls modify --file recording.data --reviewer GLM-5 \\\n")
fmt.Fprintf(os.Stderr, " --filter mymodel --segment 12-15 --certainty 100 --bookmark\n\n")
fmt.Fprintf(os.Stderr, " # Add a comment to a segment\n")
fmt.Fprintf(os.Stderr, " skraak calls modify --file recording.data --reviewer GLM-5 \\\n")
fmt.Fprintf(os.Stderr, " --filter mymodel --segment 12-15 --certainty 100 --comment \"Good example of duet\"\n")
}
// modifyArgs holds parsed CLI arguments for the modify command.
type modifyArgs struct {
file string
reviewer string
filter string
segment string
species string
comment string
certainty int
certaintySet bool
bookmark bool
}
// modifyFlagHandler returns a function that sets a flag value on modifyArgs,
// or nil if the flag is unknown. Returns (handler, isBool, handled).
// isBool indicates the flag takes no value.
func modifyFlagHandler(arg string, ma *modifyArgs) (func(string), bool, bool) {
switch arg {
case "--file":
return func(v string) { ma.file = v }, false, true
case "--reviewer":
return func(v string) { ma.reviewer = v }, false, true
case "--filter":
return func(v string) { ma.filter = v }, false, true
case "--segment":
return func(v string) { ma.segment = v }, false, true
case "--species":
return func(v string) { ma.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)
}
ma.certainty = n
ma.certaintySet = true
}, false, true
case "--bookmark":
return func(_ string) { ma.bookmark = true }, true, true
case "--comment":
return func(v string) { ma.comment = v }, false, true
default:
return nil, false, false
}
}
// parseModifyArgs parses the command-line arguments for the modify subcommand.
func parseModifyArgs(args []string) modifyArgs {
var ma modifyArgs
i := 0
for i < len(args) {
arg := args[i]
if arg == "-h" || arg == "--help" {
printModifyUsage()
os.Exit(0)
}
handler, isBool, handled := modifyFlagHandler(arg, &ma)
if !handled {
handleUnknownFlag(arg)
i++
continue
}
if isBool {
i++
} else {
handler(mustValue(args, &i, arg))
}
}
return ma
}
// handleUnknownFlag prints usage and exits for an unknown flag.
func handleUnknownFlag(arg string) {
if strings.HasPrefix(arg, "--") {
printModifyUsage()
fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n", arg)
os.Exit(1)
}
}
// validateModifyArgs checks required flags and value ranges.
func validateModifyArgs(ma modifyArgs) error {
missing := []string{}
if ma.file == "" {
missing = append(missing, "--file")
}
if ma.reviewer == "" {
missing = append(missing, "--reviewer")
}
if ma.filter == "" {
missing = append(missing, "--filter")
}
if ma.segment == "" {
missing = append(missing, "--segment")
}
if !ma.certaintySet {
missing = append(missing, "--certainty")
}
if len(missing) > 0 {
printModifyUsage()
return fmt.Errorf("missing required flags: %v", missing)
}
if ma.certainty < 0 || ma.certainty > 100 {
return fmt.Errorf("--certainty must be between 0 and 100")
}
return nil
}
// RunCallsModify handles the "calls modify" subcommand
//
// JSON output schema:
//
// {
// "file": string, // .data file path
// "segment_start": int, // Matched segment start (seconds, floored)
// "segment_end": int, // Matched segment end (seconds, ceiled)
// "species": string, // Updated species (omitted if unchanged)
// "calltype": string, // Updated call type (omitted if empty)
// "certainty": int, // Updated certainty (omitted if unchanged)
// "bookmark": bool, // Bookmark flag (omitted if not set)
// "comment": string, // Comment (omitted if empty)
// "previous_value": string, // Description of previous label value (omitted if unchanged)
// "error": string // Error message (omitted if no error)
// }
func RunCallsModify(args []string) error {
ma := parseModifyArgs(args)
if err := validateModifyArgs(ma); err != nil {
return err
}
input := calls.CallsModifyInput{
File: ma.file,
Reviewer: ma.reviewer,
Filter: ma.filter,
Segment: ma.segment,
Species: ma.species,
Certainty: ma.certainty,
Comment: ma.comment,
}
if ma.bookmark {
input.Bookmark = &ma.bookmark
}
result, err := calls.CallsModify(input)
if err != nil {
return fmt.Errorf("modify: %s", result.Error)
}
data, _ := json.Marshal(result)
fmt.Println(string(data))
return nil
}