GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC // ReadWAVSegmentSamples reads a specific time range of audio samples from a WAV file.// If startSec < 0, it starts from 0.// If endSec <= 0 or endSec > duration, it reads to the end.func ReadWAVSegmentSamples(filepath string, startSec, endSec float64) ([]float64, int, error) {file, err := os.Open(filepath)if err != nil {return nil, 0, fmt.Errorf("failed to open file: %w", err)}defer func() { _ = file.Close() }()
// wavChunkInfo holds parsed WAV format and data chunk locations.type wavChunkInfo struct {sampleRate intchannels intbitsPerSample intdataOffset int64dataSize int64}
// Read header to get format infoheaderBuf := make([]byte, 44)if _, err := io.ReadFull(file, headerBuf); err != nil {return nil, 0, fmt.Errorf("failed to read header: %w", err)}// Verify RIFF/WAVE headerif string(headerBuf[0:4]) != "RIFF" || string(headerBuf[8:12]) != "WAVE" {return nil, 0, fmt.Errorf("not a valid WAV file")}// Parse chunks to find fmt and datavar sampleRate, channels, bitsPerSample intvar dataOffset, dataSize int64// Seek to first chunkif _, err := file.Seek(12, 0); err != nil {return nil, 0, fmt.Errorf("failed to seek: %w", err)}
// parseWAVChunks reads WAV chunks from the current file position, returning// format info and data chunk location. Returns error if no data chunk is found.func parseWAVChunks(file *os.File) (wavChunkInfo, error) {var info wavChunkInfo
channels = int(binary.LittleEndian.Uint16(fmtData[2:4]))sampleRate = int(binary.LittleEndian.Uint32(fmtData[4:8]))bitsPerSample = int(binary.LittleEndian.Uint16(fmtData[14:16]))
info.channels = int(binary.LittleEndian.Uint16(fmtData[2:4]))info.sampleRate = int(binary.LittleEndian.Uint32(fmtData[4:8]))info.bitsPerSample = int(binary.LittleEndian.Uint16(fmtData[14:16]))
dataOffset, _ = file.Seek(0, io.SeekCurrent)dataSize = chunkSizegoto foundData
info.dataOffset, _ = file.Seek(0, io.SeekCurrent)info.dataSize = chunkSizereturn info, nil
return nil, 0, fmt.Errorf("no data chunk found in WAV file")
// calcWAVReadRange computes the byte offset and size to read from the data chunk.func calcWAVReadRange(startSec, endSec float64, info wavChunkInfo) (startOffset, readSize int64) {bytesPerSample := info.bitsPerSample / 8blockAlign := bytesPerSample * info.channels
foundData:if sampleRate == 0 || channels == 0 || bitsPerSample == 0 {return nil, 0, fmt.Errorf("missing or invalid fmt chunk")}bytesPerSample := bitsPerSample / 8blockAlign := bytesPerSample * channelsstartOffset := int64(0)var readSize int64
startSample := int64(startSec * float64(sampleRate))startOffset = min(startSample*int64(blockAlign), dataSize)
startSample := int64(startSec * float64(info.sampleRate))startOffset = min(startSample*int64(blockAlign), info.dataSize)
readSize = dataSize - startOffset
readSize = info.dataSize - startOffset}return}// ReadWAVSegmentSamples reads a specific time range of audio samples from a WAV file.// If startSec < 0, it starts from 0.// If endSec <= 0 or endSec > duration, it reads to the end.func ReadWAVSegmentSamples(filepath string, startSec, endSec float64) ([]float64, int, error) {file, err := os.Open(filepath)if err != nil {return nil, 0, fmt.Errorf("failed to open file: %w", err)}defer func() { _ = file.Close() }()headerBuf := make([]byte, 44)if _, err := io.ReadFull(file, headerBuf); err != nil {return nil, 0, fmt.Errorf("failed to read header: %w", err)}if string(headerBuf[0:4]) != "RIFF" || string(headerBuf[8:12]) != "WAVE" {return nil, 0, fmt.Errorf("not a valid WAV file")}if _, err := file.Seek(12, 0); err != nil {return nil, 0, fmt.Errorf("failed to seek: %w", err)}info, err := parseWAVChunks(file)if err != nil {return nil, 0, err
}// importLabelResult holds the result of importing a single label.type importLabelResult struct {labelImport LabelImportlabelID stringsubtypesImported interr ImportSegmentErrorhasError bool}// importSingleLabel inserts a single label and its metadata/subtype into the DB.func importSingleLabel(ctx context.Context,tx *db.LoggedTx,label *utils.Label,segmentID string,segIdx, labelIdx int,sf scannedDataFile,mapping utils.MappingFile,filterIDMap map[string]string,speciesIDMap map[string]string,calltypeIDMap map[string]map[string]string,) importLabelResult {dbSpecies, ok := mapping.GetDBSpecies(label.Species)if !ok {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("species not found in mapping: %s", label.Species),}, hasError: true}}speciesID, ok := speciesIDMap[dbSpecies]if !ok {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("species ID not found: %s", dbSpecies),}, hasError: true}}filterID, ok := filterIDMap[label.Filter]if !ok {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("filter ID not found: %s", label.Filter),}, hasError: true}}labelID, err := utils.GenerateLongID()if err != nil {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to generate label ID: %v", err),}, hasError: true}}_, err = tx.ExecContext(ctx, `INSERT INTO label (id, segment_id, species_id, filter_id, certainty, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, now(), now(), true)`, labelID, segmentID, speciesID, filterID, label.Certainty)if err != nil {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to insert label: %v", err),}, hasError: true}}// Insert label_metadata if comment existsif label.Comment != "" {escapedComment := strings.ReplaceAll(label.Comment, `"`, `\"`)metadataJSON := fmt.Sprintf(`{"comment": "%s"}`, escapedComment)if _, err := tx.ExecContext(ctx, `INSERT INTO label_metadata (label_id, json, created_at, last_modified, active)VALUES (?, ?, now(), now(), true)`, labelID, metadataJSON); err != nil {return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to insert label_metadata: %v", err),}, hasError: true}}}labelImport := LabelImport{LabelID: labelID,Species: dbSpecies,Filter: label.Filter,Certainty: label.Certainty,}if label.Comment != "" {labelImport.Comment = label.Comment}// Insert label_subtype if calltype existsif label.CallType != "" {if err := importCalltype(ctx, tx, labelID, label, dbSpecies, filterID, mapping, calltypeIDMap, sf); err != nil {return importLabelResult{err: *err, hasError: true}}labelImport.CallType = mapping.GetDBCalltype(label.Species, label.CallType)return importLabelResult{labelImport: labelImport, labelID: labelID, subtypesImported: 1}}return importLabelResult{labelImport: labelImport, labelID: labelID}}// importCalltype inserts a label_subtype row for a calltype label.func importCalltype(ctx context.Context,tx *db.LoggedTx,labelID string,label *utils.Label,dbSpecies string,filterID string,mapping utils.MappingFile,calltypeIDMap map[string]map[string]string,sf scannedDataFile,) *ImportSegmentError {dbCalltype := mapping.GetDBCalltype(label.Species, label.CallType)calltypeID := ""if calltypeIDMap[dbSpecies] != nil {calltypeID = calltypeIDMap[dbSpecies][dbCalltype]}if calltypeID == "" {return &ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("calltype ID not found: %s/%s", dbSpecies, dbCalltype),}}subtypeID, err := utils.GenerateLongID()if err != nil {return &ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to generate label_subtype ID: %v", err),}}_, err = tx.ExecContext(ctx, `INSERT INTO label_subtype (id, label_id, calltype_id, filter_id, certainty, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, now(), now(), true)`, subtypeID, labelID, calltypeID, filterID, label.Certainty)if err != nil {return &ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to insert label_subtype: %v", err),}}return nil
// Validate segment boundsif seg.StartTime >= seg.EndTime {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("invalid segment bounds: start=%.2f >= end=%.2f", seg.StartTime, seg.EndTime),})continue}if seg.EndTime > sf.Duration {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("segment end time (%.2f) exceeds file duration (%.2f)", seg.EndTime, sf.Duration),})continue}// Insert segmentsegmentID, err := utils.GenerateLongID()if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to generate segment ID: %v", err),})continue}_, err = tx.ExecContext(ctx, `INSERT INTO segment (id, file_id, dataset_id, start_time, end_time, freq_low, freq_high, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, ?, ?, now(), now(), true)`, segmentID, sf.FileID, datasetID, seg.StartTime, seg.EndTime, seg.FreqLow, seg.FreqHigh)if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to insert segment: %v", err),})continue}// Process labelsvar segmentImport SegmentImportsegmentImport.SegmentID = segmentIDsegmentImport.FileName = filepath.Base(sf.WavPath)segmentImport.StartTime = seg.StartTimesegmentImport.EndTime = seg.EndTimesegmentImport.FreqLow = seg.FreqLowsegmentImport.FreqHigh = seg.FreqHighsegmentImport.Labels = make([]LabelImport, 0)fileUpdate.LabelIDs[segIdx] = make(map[int]string)for labelIdx, label := range seg.Labels {// Get DB species and calltypedbSpecies, ok := mapping.GetDBSpecies(label.Species)if !ok {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("species not found in mapping: %s", label.Species),})continue}speciesID, ok := speciesIDMap[dbSpecies]if !ok {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("species ID not found: %s", dbSpecies),})continue}filterID, ok := filterIDMap[label.Filter]if !ok {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("filter ID not found: %s", label.Filter),})continue}// Insert labellabelID, err := utils.GenerateLongID()if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to generate label ID: %v", err),})continue}_, err = tx.ExecContext(ctx, `INSERT INTO label (id, segment_id, species_id, filter_id, certainty, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, now(), now(), true)`, labelID, segmentID, speciesID, filterID, label.Certainty)if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to insert label: %v", err),})continue}importedLabels++// Track label ID for .data file updatefileUpdate.LabelIDs[segIdx][labelIdx] = labelID// Insert label_metadata if comment existsif label.Comment != "" {escapedComment := strings.ReplaceAll(label.Comment, `"`, `\"`)metadataJSON := fmt.Sprintf(`{"comment": "%s"}`, escapedComment)_, err = tx.ExecContext(ctx, `INSERT INTO label_metadata (label_id, json, created_at, last_modified, active)VALUES (?, ?, now(), now(), true)`, labelID, metadataJSON)if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to insert label_metadata: %v", err),})continue}}// Build label import for outputlabelImport := LabelImport{LabelID: labelID,Species: dbSpecies,Filter: label.Filter,Certainty: label.Certainty,}if label.Comment != "" {labelImport.Comment = label.Comment}// Insert label_subtype if calltype existsif label.CallType != "" {dbCalltype := mapping.GetDBCalltype(label.Species, label.CallType)calltypeID := ""if calltypeIDMap[dbSpecies] != nil {calltypeID = calltypeIDMap[dbSpecies][dbCalltype]}if calltypeID == "" {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("calltype ID not found: %s/%s", dbSpecies, dbCalltype),})continue}subtypeID, err := utils.GenerateLongID()if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to generate label_subtype ID: %v", err),})continue}_, err = tx.ExecContext(ctx, `INSERT INTO label_subtype (id, label_id, calltype_id, filter_id, certainty, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, now(), now(), true)`, subtypeID, labelID, calltypeID, filterID, label.Certainty)if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath),Stage: "import",Message: fmt.Sprintf("failed to insert label_subtype: %v", err),})continue}importedSubtypes++labelImport.CallType = dbCalltype}segmentImport.Labels = append(segmentImport.Labels, labelImport)}
segImp, labelIDs, subtypes, segErrs := importSegment(ctx, tx, seg, segIdx, sf, datasetID, mapping, filterIDMap, speciesIDMap, calltypeIDMap)errors = append(errors, segErrs...)importedSubtypes += subtypes
// If no labels succeeded, delete the orphaned segmentif len(segmentImport.Labels) == 0 {_, err = tx.ExecContext(ctx, `DELETE FROM segment WHERE id = ?`, segmentID)if err != nil {
if len(segImp.Labels) == 0 {// Delete orphaned segment (no labels succeeded)if _, err := tx.ExecContext(ctx, `DELETE FROM segment WHERE id = ?`, segImp.SegmentID); err != nil {
// importSegment inserts a single segment and its labels into the DB.func importSegment(ctx context.Context,tx *db.LoggedTx,seg *utils.Segment,segIdx int,sf scannedDataFile,datasetID string,mapping utils.MappingFile,filterIDMap map[string]string,speciesIDMap map[string]string,calltypeIDMap map[string]map[string]string,) (SegmentImport, map[int]string, int, []ImportSegmentError) {var errors []ImportSegmentErrorif seg.StartTime >= seg.EndTime {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("invalid segment bounds: start=%.2f >= end=%.2f", seg.StartTime, seg.EndTime),})return SegmentImport{}, nil, 0, errors}
if seg.EndTime > sf.Duration {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("segment end time (%.2f) exceeds file duration (%.2f)", seg.EndTime, sf.Duration),})return SegmentImport{}, nil, 0, errors}segmentID, err := utils.GenerateLongID()if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to generate segment ID: %v", err),})return SegmentImport{}, nil, 0, errors}_, err = tx.ExecContext(ctx, `INSERT INTO segment (id, file_id, dataset_id, start_time, end_time, freq_low, freq_high, created_at, last_modified, active)VALUES (?, ?, ?, ?, ?, ?, ?, now(), now(), true)`, segmentID, sf.FileID, datasetID, seg.StartTime, seg.EndTime, seg.FreqLow, seg.FreqHigh)if err != nil {errors = append(errors, ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: "import",Message: fmt.Sprintf("failed to insert segment: %v", err),})return SegmentImport{}, nil, 0, errors}segImport := SegmentImport{SegmentID: segmentID,FileName: filepath.Base(sf.WavPath),StartTime: seg.StartTime,EndTime: seg.EndTime,FreqLow: seg.FreqLow,FreqHigh: seg.FreqHigh,Labels: make([]LabelImport, 0),}labelIDs := make(map[int]string)var subtypesImported intfor labelIdx, label := range seg.Labels {result := importSingleLabel(ctx, tx, label, segmentID, segIdx, labelIdx, sf, mapping, filterIDMap, speciesIDMap, calltypeIDMap)if result.hasError {errors = append(errors, result.err)continue}labelIDs[labelIdx] = result.labelIDsegImport.Labels = append(segImport.Labels, result.labelImport)subtypesImported += result.subtypesImported}return segImport, labelIDs, subtypesImported, errors}
func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {var output ClusterOutputclusterID := *input.ID// Validate ID formatif err := utils.ValidateShortID(clusterID, "cluster_id"); err != nil {return output, err}
if err := validateClusterFields(input); err != nil {return output, err}// Validate optional pattern ID formatif input.CyclicRecordingPatternID != nil && strings.TrimSpace(*input.CyclicRecordingPatternID) != "" {if err := utils.ValidateShortID(*input.CyclicRecordingPatternID, "cyclic_recording_pattern_id"); err != nil {return output, err}}// Open writable databasedatabase, err := db.OpenWriteableDB(dbPath)if err != nil {return output, fmt.Errorf("failed to open database: %w", err)}defer database.Close()// Verify cluster exists and check active status
// validateClusterActive checks that a cluster exists and is active.func validateClusterActive(database *sql.DB, clusterID string) error {
// Validate cyclic_recording_pattern_id if providedif input.CyclicRecordingPatternID != nil {trimmedPatternID := strings.TrimSpace(*input.CyclicRecordingPatternID)if trimmedPatternID != "" {var patternExists, patternActive boolerr = database.QueryRow("SELECT EXISTS(SELECT 1 FROM cyclic_recording_pattern WHERE id = ?), COALESCE((SELECT active FROM cyclic_recording_pattern WHERE id = ?), false)",trimmedPatternID, trimmedPatternID,).Scan(&patternExists, &patternActive)if err != nil {return output, fmt.Errorf("failed to verify cyclic recording pattern: %w", err)}if !patternExists {return output, fmt.Errorf("cyclic recording pattern not found: %s", trimmedPatternID)}if !patternActive {return output, fmt.Errorf("cyclic recording pattern '%s' is not active", trimmedPatternID)}}
// validateCyclicPattern checks that a cyclic recording pattern exists and is active.func validateCyclicPattern(database *sql.DB, patternID string) error {var exists, active boolerr := database.QueryRow("SELECT EXISTS(SELECT 1 FROM cyclic_recording_pattern WHERE id = ?), COALESCE((SELECT active FROM cyclic_recording_pattern WHERE id = ?), false)",patternID, patternID,).Scan(&exists, &active)if err != nil {return fmt.Errorf("failed to verify cyclic recording pattern: %w", err)
// Begin logged transaction for update
func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {var output ClusterOutputclusterID := *input.IDif err := utils.ValidateShortID(clusterID, "cluster_id"); err != nil {return output, err}if err := validateClusterFields(input); err != nil {return output, err}if input.CyclicRecordingPatternID != nil && strings.TrimSpace(*input.CyclicRecordingPatternID) != "" {if err := utils.ValidateShortID(*input.CyclicRecordingPatternID, "cyclic_recording_pattern_id"); err != nil {return output, err}}database, err := db.OpenWriteableDB(dbPath)if err != nil {return output, fmt.Errorf("failed to open database: %w", err)}defer database.Close()if err := validateClusterActive(database, clusterID); err != nil {return output, err}if input.CyclicRecordingPatternID != nil {trimmedPatternID := strings.TrimSpace(*input.CyclicRecordingPatternID)if trimmedPatternID != "" {if err := validateCyclicPattern(database, trimmedPatternID); err != nil {return output, err}}}query, args, err := buildClusterUpdateQuery(input, clusterID)if err != nil {return output, err}
// Fetch the updated clustervar cluster db.Clustererr = tx.QueryRow("SELECT id, dataset_id, location_id, name, description, created_at, last_modified, active, cyclic_recording_pattern_id, sample_rate FROM cluster WHERE id = ?",clusterID,).Scan(&cluster.ID, &cluster.DatasetID, &cluster.LocationID, &cluster.Name, &cluster.Description,&cluster.CreatedAt, &cluster.LastModified, &cluster.Active, &cluster.CyclicRecordingPatternID, &cluster.SampleRate)
cluster, err := fetchClusterByID(ctx, tx, clusterID)
// CallsModify modifies a label in a .data filefunc CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {var output CallsModifyOutput// Validate required flags
// validateModifyInput checks required fields and comment constraints.func validateModifyInput(input CallsModifyInput) error {
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)
return fmt.Errorf("--segment is required")
output.Error = fmt.Sprintf("--comment must be ASCII only (non-ASCII at position %d)", i)return output, fmt.Errorf("%s", output.Error)
return fmt.Errorf("--comment must be ASCII only (non-ASCII at position %d)", i)}}return nil}// resolveSpecies parses species+calltype from the input species string.// If input species is empty, keeps the existing label values.func resolveSpecies(inputSpecies string, label *utils.Label) (species, callType string) {if inputSpecies == "" {return label.Species, label.CallType}if before, after, ok := strings.Cut(inputSpecies, "+"); ok {return before, after}return inputSpecies, ""}// hasModifyChanges checks whether any field would actually change.func hasModifyChanges(newSpecies, newCallType string, input CallsModifyInput, label *utils.Label) bool {if newSpecies != label.Species || newCallType != label.CallType {return true}if input.Certainty != label.Certainty {return true}if input.Bookmark != nil && *input.Bookmark != label.Bookmark {return true}if input.Comment != "" {return true}return false}// applyLabelChanges updates the label and data file, populating the output.func applyLabelChanges(label *utils.Label, dataFile *utils.DataFile, input CallsModifyInput, newSpecies, newCallType string, output *CallsModifyOutput) error {dataFile.Meta.Reviewer = input.Reviewerlabel.Species = newSpecieslabel.CallType = newCallTypeoutput.Species = newSpeciesoutput.CallType = newCallTypelabel.Certainty = input.Certaintyoutput.Certainty = input.Certaintyif input.Bookmark != nil && *input.Bookmark != label.Bookmark {label.Bookmark = *input.Bookmarkoutput.Bookmark = input.Bookmark}if input.Comment != "" {var newComment stringif label.Comment != "" {newComment = label.Comment + " | " + input.Comment} else {newComment = input.Comment}if len(newComment) > 140 {return fmt.Errorf("combined comment exceeds 140 characters (%d)", len(newComment))
label.Comment = newCommentoutput.Comment = newComment}return nil}// CallsModify modifies a label in a .data filefunc CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {var output CallsModifyOutputif err := validateModifyInput(input); err != nil {output.Error = err.Error()return output, err
// 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.CertaintybookmarkChanging := input.Bookmark != nil && *input.Bookmark != targetLabel.BookmarkcommentChanging := input.Comment != "" // Any non-empty comment will be added
newSpecies, newCallType := resolveSpecies(input.Species, targetLabel)
// 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// Update bookmark (only if it would change - never toggle away from true)if input.Bookmark != nil && *input.Bookmark != targetLabel.Bookmark {targetLabel.Bookmark = *input.Bookmarkoutput.Bookmark = input.Bookmark}// Update comment (additive - append to existing comment, never destroy)if input.Comment != "" {var newComment stringif targetLabel.Comment != "" {newComment = targetLabel.Comment + " | " + input.Comment} else {newComment = input.Comment}// Check length after combiningif len(newComment) > 140 {output.Error = fmt.Sprintf("Combined comment exceeds 140 characters (%d)", len(newComment))return output, fmt.Errorf("%s", output.Error)}targetLabel.Comment = newCommentoutput.Comment = newComment
if err := applyLabelChanges(targetLabel, dataFile, input, newSpecies, newCallType, &output); err != nil {output.Error = err.Error()return output, err
// processBirdaFileCached processes a single BirdNET results file using a DirCache for WAV lookupfunc processBirdaFileCached(birdaFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {// Open and parse CSVfile, err := os.Open(birdaFile)if err != nil {return nil, false, false, fmt.Errorf("failed to open file: %w", err)}defer func() { _ = file.Close() }()
// birdaColumnIndices holds the parsed column positions from a BirdNET CSV header.type birdaColumnIndices struct {startIdx intendIdx intcommonNameIdx intconfidenceIdx intfileIdx int}
if startIdx == -1 || endIdx == -1 || commonNameIdx == -1 || confidenceIdx == -1 {return nil, false, false, fmt.Errorf("missing required columns in BirdNET file")
if idx.startIdx == -1 || idx.endIdx == -1 || idx.commonNameIdx == -1 || idx.confidenceIdx == -1 {return birdaColumnIndices{}, fmt.Errorf("missing required columns in BirdNET file")
if _, err := fmt.Sscanf(record[startIdx], "%f", &det.StartTime); err != nil {return nil, false, false, fmt.Errorf("failed to parse start time %q: %w", record[startIdx], err)
if _, err := fmt.Sscanf(record[idx.startIdx], "%f", &det.StartTime); err != nil {return nil, fmt.Errorf("failed to parse start time %q: %w", record[idx.startIdx], err)
if _, err := fmt.Sscanf(record[endIdx], "%f", &det.EndTime); err != nil {return nil, false, false, fmt.Errorf("failed to parse end time %q: %w", record[endIdx], err)
if _, err := fmt.Sscanf(record[idx.endIdx], "%f", &det.EndTime); err != nil {return nil, fmt.Errorf("failed to parse end time %q: %w", record[idx.endIdx], err)
det.CommonName = record[commonNameIdx]if _, err := fmt.Sscanf(record[confidenceIdx], "%f", &det.Confidence); err != nil {return nil, false, false, fmt.Errorf("failed to parse confidence %q: %w", record[confidenceIdx], err)
det.CommonName = record[idx.commonNameIdx]if _, err := fmt.Sscanf(record[idx.confidenceIdx], "%f", &det.Confidence); err != nil {return nil, fmt.Errorf("failed to parse confidence %q: %w", record[idx.confidenceIdx], err)
if len(detections) == 0 {return nil, false, true, nil // No detections, skip
// resolveBirdaWAVPath finds the WAV file associated with a BirdNET results file.func resolveBirdaWAVPath(birdaFile string, firstWAVPath string, cache *DirCache) string {if firstWAVPath != "" {if _, err := os.Stat(firstWAVPath); err == nil {return firstWAVPath}
// If not found from File column, search with DirCacheif wavPath == "" {if cache != nil {wavPath = cache.FindWAV(baseName)} else {wavPath = findWAVFile(dir, baseName)}
// processBirdaFileCached processes a single BirdNET results file using a DirCache for WAV lookupfunc processBirdaFileCached(birdaFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {file, err := os.Open(birdaFile)if err != nil {return nil, false, false, fmt.Errorf("failed to open file: %w", err)}defer func() { _ = file.Close() }()reader := csv.NewReader(file)idx, err := parseBirdaCSVHeader(reader)if err != nil {return nil, false, false, err}detections, err := readBirdaDetections(reader, idx)if err != nil {return nil, false, false, err}if len(detections) == 0 {return nil, false, true, nil
func CallsClipLabels(input CallsClipLabelsInput) (CallsClipLabelsOutput, error) {out := CallsClipLabelsOutput{Folder: input.Folder,OutputPath: input.OutputPath,PerClassTrueCount: map[string]int{},}
// parsedClipFile holds a parsed .data file for clip-labels processing.type parsedClipFile struct {path stringdf *utils.DataFile}
// Load mapping.mapping, err := utils.LoadMappingFile(input.MappingPath)
// parseClipLabelsDataFiles finds and parses .data files, collecting species seen.func parseClipLabelsDataFiles(folder, filter string, mapping utils.MappingFile) ([]parsedClipFile, error) {dataPaths, err := utils.FindDataFiles(folder)
// Output classes: the unique canonical (non-sentinel) class names from mapping.json.classes := mapping.Classes()if len(classes) == 0 {return out, fmt.Errorf("mapping.json has no real (non-sentinel) classes")}out.Classes = classesout.Filter = input.FilterclassIdx := map[string]int{}for i, c := range classes {classIdx[c] = i}// Find and parse .data files.dataPaths, err := utils.FindDataFiles(input.Folder)if err != nil {return out, fmt.Errorf("scan folder %s: %w", input.Folder, err)}
return out, fmt.Errorf("mapping.json is missing entries for species: %s\n(run /data-mapping to regenerate)", strings.Join(missing, ", "))
return nil, fmt.Errorf("mapping.json is missing entries for species: %s\n(run /data-mapping to regenerate)", strings.Join(missing, ", "))}return parsed, nil}// dedupClipLabelsRows checks for duplicate rows within new rows and against existing CSV rows.func dedupClipLabelsRows(rows []clipLabelsRow, existing map[rowKey]bool) error {dedup := make(map[rowKey]bool, len(existing)+len(rows))for k := range existing {dedup[k] = true
// Append-mode: read existing header + (file,start,end) tuples if any.
func CallsClipLabels(input CallsClipLabelsInput) (CallsClipLabelsOutput, error) {out := CallsClipLabelsOutput{Folder: input.Folder,OutputPath: input.OutputPath,PerClassTrueCount: map[string]int{},}finalClipMode, err := validateClipLabelsInput(input)if err != nil {return out, err}mapping, err := utils.LoadMappingFile(input.MappingPath)if err != nil {return out, fmt.Errorf("load mapping %s: %w", input.MappingPath, err)}classes := mapping.Classes()if len(classes) == 0 {return out, fmt.Errorf("mapping.json has no real (non-sentinel) classes")}out.Classes = classesout.Filter = input.FilterclassIdx := map[string]int{}for i, c := range classes {classIdx[c] = i}parsed, err := parseClipLabelsDataFiles(input.Folder, input.Filter, mapping)if err != nil {return out, err}out.DataFilesParsed = len(parsed)
filePaths = []string{config.File}
return []string{config.File}, nil}paths, err := utils.FindDataFiles(config.Folder)if err != nil {return nil, fmt.Errorf("find data files: %w", err)}return paths, nil}// filterDataFileSegments applies segment and day/night filters to a single data file.// Returns the filtered segments and whether the file should be kept.// If the file is filtered out (no matching segments, or time-of-day), returns nil, false.func filterDataFileSegments(df *utils.DataFile, config ClassifyConfig) ([]*utils.Segment, bool, int) {hasFilter := config.Filter != "" || config.Species != "" || config.Certainty >= 0var segs []*utils.Segmentif !hasFilter {segs = df.Segments
filePaths, err = utils.FindDataFiles(config.Folder)
for _, seg := range df.Segments {if seg.SegmentMatchesFilters(config.Filter, config.Species, config.CallType, config.Certainty) {segs = append(segs, seg)}}if len(segs) == 0 {return nil, false, 0}}timeFiltered := 0if config.Night || config.Day {wavPath := filepath.Clean(strings.TrimSuffix(df.FilePath, ".data"))result, err := IsNight(IsNightInput{FilePath: wavPath,Lat: config.Lat,Lng: config.Lng,Timezone: config.Timezone,})
return nil, fmt.Errorf("find data files: %w", err)
fmt.Fprintf(os.Stderr, "warning: skipping %s (isnight error: %v)\n", wavPath, err)return nil, false, 1}if config.Night && !result.SolarNight {return nil, false, 1}if config.Day && !result.DiurnalActive {return nil, false, 1
var segs []*utils.Segmentif !hasFilter {segs = df.Segments} else {for _, seg := range df.Segments {if seg.SegmentMatchesFilters(config.Filter, config.Species, config.CallType, config.Certainty) {segs = append(segs, seg)}}if len(segs) == 0 {continue // skip files with no matching segments}}// Day/night filter: runs after segment filter to avoid IsNight on irrelevant files.if config.Night || config.Day {wavPath := filepath.Clean(strings.TrimSuffix(df.FilePath, ".data"))result, err := IsNight(IsNightInput{FilePath: wavPath,Lat: config.Lat,Lng: config.Lng,Timezone: config.Timezone,})if err != nil {fmt.Fprintf(os.Stderr, "warning: skipping %s (isnight error: %v)\n", wavPath, err)timeFiltered++continue}if config.Night && !result.SolarNight {timeFiltered++continue}if config.Day && !result.DiurnalActive {timeFiltered++continue}
segs, keep, tf := filterDataFileSegments(df, config)timeFiltered += tfif !keep {continue
// 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) {var file, reviewer, filter, segment, species, comment stringvar certainty intvar certaintySet, bookmark bool
// modifyArgs holds parsed CLI arguments for the modify command.type modifyArgs struct {file stringreviewer stringfilter stringsegment stringspecies stringcomment stringcertainty intcertaintySet boolbookmark bool}
// Parse arguments
// requireFlagValue extracts the value for a flag that requires an argument.// Exits on missing value.func requireFlagValue(args []string, i int, flagName string) string {if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flagName)os.Exit(1)}return args[i+1]}// parseModifyArgs parses the command-line arguments for the modify subcommand.func parseModifyArgs(args []string) modifyArgs {var ma modifyArgs
// Build input
// 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) {ma := parseModifyArgs(args)validateModifyArgs(ma)
File: file,Reviewer: reviewer,Filter: filter,Segment: segment,Species: species,Certainty: certainty,Comment: comment,
File: ma.file,Reviewer: ma.reviewer,Filter: ma.filter,Segment: ma.segment,Species: ma.species,Certainty: ma.certainty,Comment: ma.comment,
1. **`processBirdaFileCached` (30→~10)**: Extracted `parseBirdaCSVHeader`,`readBirdaDetections`, `resolveBirdaWAVPath` for CSV parsing and WAV pathresolution.2. **`RunCallsModify` (30→~5)**: Extracted `modifyArgs` struct,`parseModifyArgs`, `requireFlagValue`, `validateModifyArgs` for CLI argumentparsing and validation.3. **`CallsModify` (29→~10)**: Extracted `validateModifyInput`, `resolveSpecies`,`hasModifyChanges`, `applyLabelChanges`, `findLabelByFilter` for inputvalidation, species resolution, and label update logic.4. **`CallsClipLabels` (29→~15)**: Extracted `parsedClipFile` type,`validateClipLabelsInput`, `parseClipLabelsDataFiles`,`dedupClipLabelsRows` for parameter validation, file parsing, anddeduplication.5. **`LoadDataFiles` (28→~10)**: Extracted `findDataFilePaths`,`filterDataFileSegments` for file discovery and segment/day-night filtering.6. **`ReadWAVSegmentSamples` (27→~5)**: Extracted `wavChunkInfo` type,`parseWAVChunks`, `calcWAVReadRange` for WAV chunk parsing and read rangecalculation. Eliminated `goto` statement.7. **`importSegmentsIntoDB` (27→~10)**: Extracted `importLabelResult` type,`importSingleLabel`, `importCalltype`, `importSegment` for per-label andper-segment DB insertion.8. **`updateCluster` (27→~17)**: Extracted `validateClusterActive`,`validateCyclicPattern`, `buildClusterUpdateQuery` for cluster validationand dynamic query building.