AUTP2GAED3F4BQVCLOPRORNLCFUGTSPW6542OA2BPP7TRXPIFKRAC XLL6JFARO2VSNEOJQPXPKZLYYYF7XQGBCR2QJSIFF2RKYZKI25LAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC 2PHJFG2LMBMR4F2X45XX7KUVM6CPRGDKNWRXQVY3Q4SOWX3PDB7AC GSHWXPDB4GATIWWGKZXMM7VOK5O5IDFKBV4SKXRFOWNMYKYX527AC 7CC2YVZXAIUNWXNNVIO5KOZZFDQQLESFO72SGEDP2C4OZXAWO4KQC 7NS27QXZMVTZBK4VPMYL5IKGSTTAWR6NDG5SOVITNX44VNIRZPMAC GQ2FYLG2VGDDKQ7TJTCFM2XPLJOAX2N737OZWPRCNKSFIH7YJ6EQC DMMB63IW75MSRMKX63LTIVRVA74FOERMGGCNYGC7BZRV76VIJURQC D4EL6RSTSZ3S3IDSETRNGLJHZKGZEE2V2OZIOKQK6LRLHQNS77JQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC package toolsimport ("fmt""math""os""strings""skraak/utils")// CallsModifyInput defines the input for the modify tooltype CallsModifyInput struct {File string `json:"file" jsonschema:"required,Path to .data file"`Reviewer string `json:"reviewer" jsonschema:"required,Reviewer name"`Filter string `json:"filter" jsonschema:"required,Filter name to match labels"`Segment string `json:"segment" jsonschema:"required,Segment time range (e.g., 12-15)"`Certainty int `json:"certainty" jsonschema:"required,Certainty value (0-100)"`Species string `json:"species" jsonschema:"Species to set (e.g., Kiwi, Kiwi+Male)"`}// CallsModifyOutput defines the output for the modify tooltype CallsModifyOutput struct {File string `json:"file"`SegmentStart int `json:"segment_start"`SegmentEnd int `json:"segment_end"`Species string `json:"species,omitempty"`CallType string `json:"calltype,omitempty"`Certainty int `json:"certainty,omitempty"`PreviousValue string `json:"previous_value,omitempty"`Error string `json:"error,omitempty"`}// CallsModify modifies a label in a .data filefunc CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {var output CallsModifyOutput// Validate required flagsif input.File == "" {output.Error = "--file is required"return output, fmt.Errorf("%s", output.Error)}if input.Reviewer == "" {output.Error = "--reviewer is required"return output, fmt.Errorf("%s", output.Error)}if input.Filter == "" {output.Error = "--filter is required"return output, fmt.Errorf("%s", output.Error)}if input.Segment == "" {output.Error = "--segment is required"return output, fmt.Errorf("%s", output.Error)}// Parse segment time rangestartTime, endTime, err := parseSegmentRange(input.Segment)if err != nil {output.Error = err.Error()return output, fmt.Errorf("%s", output.Error)}output.File = input.Fileoutput.SegmentStart = startTimeoutput.SegmentEnd = endTime// Check file existsif _, err := os.Stat(input.File); os.IsNotExist(err) {output.Error = fmt.Sprintf("File not found: %s", input.File)return output, fmt.Errorf("%s", output.Error)}// Parse .data filedataFile, err := utils.ParseDataFile(input.File)if err != nil {output.Error = fmt.Sprintf("Failed to parse file: %v", err)return output, fmt.Errorf("%s", output.Error)}// Find matching segmentsegment := findSegment(dataFile.Segments, startTime, endTime)if segment == nil {output.Error = fmt.Sprintf("No segment found matching time range %d-%d", startTime, endTime)return output, fmt.Errorf("%s", output.Error)}// Find label matching filtervar targetLabel *utils.Labelfor _, label := range segment.Labels {if label.Filter == input.Filter {targetLabel = labelbreak}}if targetLabel == nil {output.Error = fmt.Sprintf("No label found with filter '%s' in segment %d-%d", input.Filter, startTime, endTime)return output, fmt.Errorf("%s", output.Error)}// Store previous value for outputoutput.PreviousValue = formatLabel(targetLabel)// Calculate new species/calltypevar newSpecies, newCallType stringif input.Species != "" {if strings.Contains(input.Species, "+") {parts := strings.SplitN(input.Species, "+", 2)newSpecies = parts[0]newCallType = parts[1]} else {newSpecies = input.SpeciesnewCallType = "" // Clear calltype}} else {newSpecies = targetLabel.SpeciesnewCallType = targetLabel.CallType}// Check if anything would changespeciesChanging := newSpecies != targetLabel.Species || newCallType != targetLabel.CallTypecertaintyChanging := input.Certainty != targetLabel.Certaintyif !speciesChanging && !certaintyChanging {output.Error = "No changes needed: species+calltype and certainty already match"return output, fmt.Errorf("%s", output.Error)}// Update reviewer on file metadatadataFile.Meta.Reviewer = input.Reviewer// Update species/calltypetargetLabel.Species = newSpeciestargetLabel.CallType = newCallTypeoutput.Species = newSpeciesoutput.CallType = newCallType// Update certaintytargetLabel.Certainty = input.Certaintyoutput.Certainty = input.Certainty// Save fileif err := dataFile.Write(input.File); err != nil {output.Error = fmt.Sprintf("Failed to save file: %v", err)return output, fmt.Errorf("%s", output.Error)}return output, nil}// parseSegmentRange parses "12-15" format into start and end integersfunc parseSegmentRange(s string) (int, int, error) {parts := strings.Split(s, "-")if len(parts) != 2 {return 0, 0, fmt.Errorf("invalid segment format: %s (expected start-end, e.g., 12-15)", s)}var start, end intif _, err := fmt.Sscanf(parts[0], "%d", &start); err != nil {return 0, 0, fmt.Errorf("invalid start time: %s", parts[0])}if _, err := fmt.Sscanf(parts[1], "%d", &end); err != nil {return 0, 0, fmt.Errorf("invalid end time: %s", parts[1])}if start < 0 || end < 0 {return 0, 0, fmt.Errorf("times must be non-negative")}if start >= end {return 0, 0, fmt.Errorf("start time must be less than end time")}return start, end, nil}// findSegment finds a segment matching the time range using floor/ceil matchingfunc findSegment(segments []*utils.Segment, startTime, endTime int) *utils.Segment {for _, seg := range segments {segStart := int(math.Floor(seg.StartTime))segEnd := int(math.Ceil(seg.EndTime))if segEnd == segStart {segEnd = segStart + 1 // minimum 1 second}if segStart == startTime && segEnd == endTime {return seg}}return nil}// formatLabel formats a label for displayfunc formatLabel(label *utils.Label) string {result := label.Speciesif label.CallType != "" {result += "+" + label.CallType}result += fmt.Sprintf(" (%d%%)", label.Certainty)return result}
package cmdimport ("encoding/json""fmt""os""strconv""strings""skraak/tools")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, "\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 species+calltype and certainty 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")}// RunCallsModify handles the "calls modify" subcommandfunc RunCallsModify(args []string) {var file, reviewer, filter, segment, species stringvar certainty intvar certaintySet bool// Parse argumentsi := 0for i < len(args) {arg := args[i]switch arg {case "--file":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --file requires a value\n")os.Exit(1)}file = args[i+1]i += 2case "--reviewer":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --reviewer requires a value\n")os.Exit(1)}reviewer = args[i+1]i += 2case "--filter":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --filter requires a value\n")os.Exit(1)}filter = args[i+1]i += 2case "--segment":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --segment requires a value\n")os.Exit(1)}segment = args[i+1]i += 2case "--species":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --species requires a value\n")os.Exit(1)}species = args[i+1]i += 2case "--certainty":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --certainty requires a value\n")os.Exit(1)}v, err := strconv.Atoi(args[i+1])if err != nil {fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")os.Exit(1)}certainty = vcertaintySet = truei += 2case "-h", "--help":printModifyUsage()os.Exit(0)default:// Check for unknown flagsif strings.HasPrefix(arg, "--") {fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n\n", arg)printModifyUsage()os.Exit(1)}i++}}// Validate required flagsmissing := []string{}if file == "" {missing = append(missing, "--file")}if reviewer == "" {missing = append(missing, "--reviewer")}if filter == "" {missing = append(missing, "--filter")}if segment == "" {missing = append(missing, "--segment")}if !certaintySet {missing = append(missing, "--certainty")}if len(missing) > 0 {fmt.Fprintf(os.Stderr, "Error: missing required flags: %v\n\n", missing)printModifyUsage()os.Exit(1)}// Validate certainty rangeif certainty < 0 || certainty > 100 {fmt.Fprintf(os.Stderr, "Error: --certainty must be between 0 and 100\n")os.Exit(1)}// Build inputinput := tools.CallsModifyInput{File: file,Reviewer: reviewer,Filter: filter,Segment: segment,Species: species,Certainty: certainty,}// Executeresult, err := tools.CallsModify(input)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s\n", result.Error)os.Exit(1)}// Output JSONdata, _ := json.Marshal(result)fmt.Println(string(data))}
## [2026-04-02] New `calls modify` commandModify a label in a .data file from the command line.**Usage:**```bashskraak calls modify --file recording.data --reviewer GLM-5 \--filter mymodel --segment 12-15 --certainty 100 --species Kiwi+Male```**Required flags:**- `--file <path>` — Path to .data file- `--reviewer <name>` — Reviewer name (always set on file metadata)- `--filter <name>` — Filter name to match labels- `--segment <start>-<end>` — Segment time range (integer seconds, e.g., `12-15`)- `--certainty <int>` — Certainty value (0-100)
**Optional flags:**- `--species <name>` — Species to set (e.g., `Kiwi`, `Kiwi+Male`, `Noise`)**Segment matching:**- Segments matched by `floor(start_time)` and `ceil(end_time)`- A segment from 12.3s to 14.5s matches `--segment 12-15`**Behavior:**- Always updates reviewer on file metadata- If `--species` provided: sets species and calltype (or clears calltype if not specified)- If species+calltype AND certainty match current values, no modification made (error)- Error if no matching segment or label found (no-op on error)**Use cases:**- Correct classification: `--certainty 100` only (confirms existing species)- Incorrect classification: `--species NewSpecies --certainty 100` (changes both)**Changes:**- `tools/calls_modify.go` — New file, core logic- `cmd/calls_modify.go` — New file, CLI parsing- `cmd/calls.go` — Added `modify` subcommand