cyclo complexity over 25

quietlight
May 4, 2026, 5:04 AM
GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC

Dependencies

  • [2] 2P27XV3D fixed cyclo over 30
  • [3] LQLC7S3A trying gemini: Inconsistent Standards in @utils/ refactoring
  • [4] KZKLAINJ run out of space on nest, cleaned out
  • [5] 2HAQZPV3 more refactoring with glm
  • [6] NS4TDPLN cyclomatic complexity

Change contents

  • replacement in utils/wav_metadata.go at line 329
    [3.36719][3.2791:3073](),[3.3073][3.37000:37149](),[3.37000][3.37000:37149]()
    // 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() }()
    [3.36719]
    [3.37149]
    // wavChunkInfo holds parsed WAV format and data chunk locations.
    type wavChunkInfo struct {
    sampleRate int
    channels int
    bitsPerSample int
    dataOffset int64
    dataSize int64
    }
  • replacement in utils/wav_metadata.go at line 338
    [3.37150][3.37150:37743]()
    // Read header to get format info
    headerBuf := 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 header
    if 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 data
    var sampleRate, channels, bitsPerSample int
    var dataOffset, dataSize int64
    // Seek to first chunk
    if _, err := file.Seek(12, 0); err != nil {
    return nil, 0, fmt.Errorf("failed to seek: %w", err)
    }
    [3.37150]
    [3.37743]
    // 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
  • replacement in utils/wav_metadata.go at line 348
    [3.37880][3.37880:37949]()
    return nil, 0, fmt.Errorf("failed to read chunk header: %w", err)
    [3.37880]
    [3.37949]
    return info, fmt.Errorf("failed to read chunk header: %w", err)
  • replacement in utils/wav_metadata.go at line 358
    [3.38189][3.38189:38256]()
    return nil, 0, fmt.Errorf("failed to read fmt chunk: %w", err)
    [3.38189]
    [3.38256]
    return info, fmt.Errorf("failed to read fmt chunk: %w", err)
  • replacement in utils/wav_metadata.go at line 361
    [3.38288][3.38288:38480]()
    channels = int(binary.LittleEndian.Uint16(fmtData[2:4]))
    sampleRate = int(binary.LittleEndian.Uint32(fmtData[4:8]))
    bitsPerSample = int(binary.LittleEndian.Uint16(fmtData[14:16]))
    [3.38288]
    [3.38480]
    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]))
  • edit in utils/wav_metadata.go at line 365
    [3.38485][3.38485:38486]()
  • replacement in utils/wav_metadata.go at line 366
    [3.38501][3.3074:3122](),[3.3122][3.38556:38580](),[3.38556][3.38556:38580](),[3.38617][3.38617:38636]()
    dataOffset, _ = file.Seek(0, io.SeekCurrent)
    dataSize = chunkSize
    goto foundData
    [3.38501]
    [3.38636]
    info.dataOffset, _ = file.Seek(0, io.SeekCurrent)
    info.dataSize = chunkSize
    return info, nil
  • replacement in utils/wav_metadata.go at line 371
    [3.3190][3.38726:38789](),[3.38726][3.38726:38789]()
    return nil, 0, fmt.Errorf("failed to skip chunk: %w", err)
    [3.3190]
    [3.38789]
    return info, fmt.Errorf("failed to skip chunk: %w", err)
  • replacement in utils/wav_metadata.go at line 378
    [3.3250][3.38885:38950](),[3.38885][3.38885:38950]()
    return nil, 0, fmt.Errorf("failed to skip padding: %w", err)
    [3.3250]
    [3.38950]
    return info, fmt.Errorf("failed to skip padding: %w", err)
  • edit in utils/wav_metadata.go at line 382
    [3.38962]
    [3.38962]
    return info, fmt.Errorf("no data chunk found in WAV file")
    }
  • replacement in utils/wav_metadata.go at line 385
    [3.38963][3.38963:39025]()
    return nil, 0, fmt.Errorf("no data chunk found in WAV file")
    [3.38963]
    [3.39025]
    // 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 / 8
    blockAlign := bytesPerSample * info.channels
  • edit in utils/wav_metadata.go at line 390
    [3.39026][3.39026:39162](),[3.39162][3.3251:3376]()
    foundData:
    if sampleRate == 0 || channels == 0 || bitsPerSample == 0 {
    return nil, 0, fmt.Errorf("missing or invalid fmt chunk")
    }
    bytesPerSample := bitsPerSample / 8
    blockAlign := bytesPerSample * channels
    startOffset := int64(0)
    var readSize int64
  • replacement in utils/wav_metadata.go at line 391
    [3.3395][3.3395:3450](),[3.3450][3.1130:1191]()
    startSample := int64(startSec * float64(sampleRate))
    startOffset = min(startSample*int64(blockAlign), dataSize)
    [3.3395]
    [3.39298]
    startSample := int64(startSec * float64(info.sampleRate))
    startOffset = min(startSample*int64(blockAlign), info.dataSize)
  • replacement in utils/wav_metadata.go at line 396
    [3.3576][3.3576:3627](),[3.3627][3.1192:1250]()
    endSample := int64(endSec * float64(sampleRate))
    endOffset := min(endSample*int64(blockAlign), dataSize)
    [3.3576]
    [3.3728]
    endSample := int64(endSec * float64(info.sampleRate))
    endOffset := min(endSample*int64(blockAlign), info.dataSize)
  • edit in utils/wav_metadata.go at line 400
    [3.3797][3.3797:3824]()
    } else {
    readSize = 0
  • replacement in utils/wav_metadata.go at line 402
    [3.3838][3.3838:3874]()
    readSize = dataSize - startOffset
    [3.3838]
    [3.3874]
    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
  • edit in utils/wav_metadata.go at line 433
    [3.3877]
    [3.3877]
    if info.sampleRate == 0 || info.channels == 0 || info.bitsPerSample == 0 {
    return nil, 0, fmt.Errorf("missing or invalid fmt chunk")
    }
  • edit in utils/wav_metadata.go at line 437
    [3.3878]
    [3.3878]
    startOffset, readSize := calcWAVReadRange(startSec, endSec, info)
  • replacement in utils/wav_metadata.go at line 439
    [3.3898][3.3898:3936]()
    return []float64{}, sampleRate, nil
    [3.3898]
    [3.3936]
    return []float64{}, info.sampleRate, nil
  • replacement in utils/wav_metadata.go at line 442
    [3.3940][3.3940:4016]()
    if _, err := file.Seek(dataOffset+startOffset, io.SeekStart); err != nil {
    [3.3940]
    [3.4016]
    if _, err := file.Seek(info.dataOffset+startOffset, io.SeekStart); err != nil {
  • edit in utils/wav_metadata.go at line 448
    [3.39396][3.4129:4187]()
    // If we hit EOF unexpectedly, we just use what we read
  • edit in utils/wav_metadata.go at line 452
    [3.39465][3.39465:39562]()
    // Convert to float64 samples
    samples := convertToFloat64(audioData, bitsPerSample, channels)
  • replacement in utils/wav_metadata.go at line 453
    [3.39563][3.39563:39596]()
    return samples, sampleRate, nil
    [3.39563]
    [3.39596]
    samples := convertToFloat64(audioData, info.bitsPerSample, info.channels)
    return samples, info.sampleRate, nil
  • edit in tools/import_segments.go at line 552
    [3.335009]
    [3.335009]
    }
    // importLabelResult holds the result of importing a single label.
    type importLabelResult struct {
    labelImport LabelImport
    labelID string
    subtypesImported int
    err ImportSegmentError
    hasError 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 exists
    if 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 exists
    if 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
  • edit in tools/import_segments.go at line 721
    [3.335650][3.335650:335672]()
    // Begin transaction
  • edit in tools/import_segments.go at line 731
    [3.335947][3.335947:335979]()
    // Process each validated file
  • replacement in tools/import_segments.go at line 736
    [3.336086][3.336086:336136]()
    continue // Was filtered out during validation
    [3.336086]
    [3.336136]
    continue
  • edit in tools/import_segments.go at line 744
    [3.336270][3.336270:336322]()
    // Track label IDs for writing back to .data file
  • edit in tools/import_segments.go at line 750
    [3.336453][3.336453:336475]()
    // Process segments
  • replacement in tools/import_segments.go at line 751
    [3.336516][3.336516:342594]()
    // Validate segment bounds
    if 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 segment
    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),
    })
    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 labels
    var segmentImport SegmentImport
    segmentImport.SegmentID = segmentID
    segmentImport.FileName = filepath.Base(sf.WavPath)
    segmentImport.StartTime = seg.StartTime
    segmentImport.EndTime = seg.EndTime
    segmentImport.FreqLow = seg.FreqLow
    segmentImport.FreqHigh = seg.FreqHigh
    segmentImport.Labels = make([]LabelImport, 0)
    fileUpdate.LabelIDs[segIdx] = make(map[int]string)
    for labelIdx, label := range seg.Labels {
    // Get DB species and calltype
    dbSpecies, 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 label
    labelID, 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 update
    fileUpdate.LabelIDs[segIdx][labelIdx] = labelID
    // Insert label_metadata if comment exists
    if 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 output
    labelImport := LabelImport{
    LabelID: labelID,
    Species: dbSpecies,
    Filter: label.Filter,
    Certainty: label.Certainty,
    }
    if label.Comment != "" {
    labelImport.Comment = label.Comment
    }
    // Insert label_subtype if calltype exists
    if 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)
    }
    [3.336516]
    [3.342594]
    segImp, labelIDs, subtypes, segErrs := importSegment(ctx, tx, seg, segIdx, sf, datasetID, mapping, filterIDMap, speciesIDMap, calltypeIDMap)
    errors = append(errors, segErrs...)
    importedSubtypes += subtypes
  • replacement in tools/import_segments.go at line 755
    [3.342595][3.342595:342792]()
    // If no labels succeeded, delete the orphaned segment
    if len(segmentImport.Labels) == 0 {
    _, err = tx.ExecContext(ctx, `DELETE FROM segment WHERE id = ?`, segmentID)
    if err != nil {
    [3.342595]
    [3.342792]
    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 {
  • replacement in tools/import_segments.go at line 759
    [3.342841][3.342841:342909]()
    File: filepath.Base(sf.DataPath),
    Stage: "import",
    [3.342841]
    [3.342909]
    File: filepath.Base(sf.DataPath), Stage: "import",
  • edit in tools/import_segments.go at line 763
    [3.342997][3.342997:343097]()
    // Remove from fileUpdate since no labels were imported
    delete(fileUpdate.LabelIDs, segIdx)
  • replacement in tools/import_segments.go at line 764
    [3.343109][3.343109:343172]()
    importedSegments = append(importedSegments, segmentImport)
    [3.343109]
    [3.343172]
    importedSegments = append(importedSegments, segImp)
    importedLabels += len(labelIDs)
    fileUpdate.LabelIDs[segIdx] = labelIDs
  • edit in tools/import_segments.go at line 773
    [3.343234][3.343234:343257]()
    // Commit transaction
  • edit in tools/import_segments.go at line 783
    [3.343551]
    [3.343551]
    // 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 []ImportSegmentError
    if 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
    }
  • edit in tools/import_segments.go at line 807
    [3.343552]
    [3.343552]
    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 int
    for 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.labelID
    segImport.Labels = append(segImport.Labels, result.labelImport)
    subtypesImported += result.subtypesImported
    }
    return segImport, labelIDs, subtypesImported, errors
    }
  • edit in tools/cluster.go at line 5
    [3.378763]
    [3.378763]
    "database/sql"
  • edit in tools/cluster.go at line 276
    [2.7499][2.7499:7500](),[2.7500][3.387939:388194](),[3.387939][3.387939:388194]()
    func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {
    var output ClusterOutput
    clusterID := *input.ID
    // Validate ID format
    if err := utils.ValidateShortID(clusterID, "cluster_id"); err != nil {
    return output, err
    }
  • replacement in tools/cluster.go at line 277
    [3.388195][3.388195:388791]()
    if err := validateClusterFields(input); err != nil {
    return output, err
    }
    // Validate optional pattern ID format
    if input.CyclicRecordingPatternID != nil && strings.TrimSpace(*input.CyclicRecordingPatternID) != "" {
    if err := utils.ValidateShortID(*input.CyclicRecordingPatternID, "cyclic_recording_pattern_id"); err != nil {
    return output, err
    }
    }
    // Open writable database
    database, 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
    [3.388195]
    [3.388791]
    // validateClusterActive checks that a cluster exists and is active.
    func validateClusterActive(database *sql.DB, clusterID string) error {
  • replacement in tools/cluster.go at line 280
    [3.388816][3.388816:388842]()
    err = database.QueryRow(
    [3.388816]
    [3.388842]
    err := database.QueryRow(
  • replacement in tools/cluster.go at line 285
    [3.389024][3.389024:389088]()
    return output, fmt.Errorf("failed to query cluster: %w", err)
    [3.389024]
    [3.389088]
    return fmt.Errorf("failed to query cluster: %w", err)
  • replacement in tools/cluster.go at line 288
    [3.389105][3.389105:389169]()
    return output, fmt.Errorf("cluster not found: %s", clusterID)
    [3.389105]
    [3.389169]
    return fmt.Errorf("cluster not found: %s", clusterID)
  • replacement in tools/cluster.go at line 291
    [3.389186][3.389186:389289]()
    return output, fmt.Errorf("cluster '%s' is not active (cannot update inactive clusters)", clusterID)
    [3.389186]
    [3.389289]
    return fmt.Errorf("cluster '%s' is not active (cannot update inactive clusters)", clusterID)
  • edit in tools/cluster.go at line 293
    [3.389292]
    [3.389292]
    return nil
    }
  • replacement in tools/cluster.go at line 296
    [3.389293][3.389293:390148]()
    // Validate cyclic_recording_pattern_id if provided
    if input.CyclicRecordingPatternID != nil {
    trimmedPatternID := strings.TrimSpace(*input.CyclicRecordingPatternID)
    if trimmedPatternID != "" {
    var patternExists, patternActive bool
    err = 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)
    }
    }
    [3.389293]
    [3.390148]
    // validateCyclicPattern checks that a cyclic recording pattern exists and is active.
    func validateCyclicPattern(database *sql.DB, patternID string) error {
    var exists, active bool
    err := 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)
  • edit in tools/cluster.go at line 306
    [3.390151]
    [3.390151]
    if !exists {
    return fmt.Errorf("cyclic recording pattern not found: %s", patternID)
    }
    if !active {
    return fmt.Errorf("cyclic recording pattern '%s' is not active", patternID)
    }
    return nil
    }
  • replacement in tools/cluster.go at line 315
    [3.390152][3.390152:390183]()
    // Build dynamic UPDATE query
    [3.390152]
    [3.390183]
    // buildClusterUpdateQuery builds the dynamic UPDATE query and args for cluster fields.
    func buildClusterUpdateQuery(input ClusterInput, clusterID string) (string, []any, error) {
  • replacement in tools/cluster.go at line 347
    [3.391034][3.391034:391094]()
    return output, fmt.Errorf("no fields provided to update")
    [3.391034]
    [3.391094]
    return "", nil, fmt.Errorf("no fields provided to update")
  • edit in tools/cluster.go at line 350
    [3.391098][3.391098:391130]()
    // Always update last_modified
  • edit in tools/cluster.go at line 354
    [3.391304]
    [3.391304]
    return query, args, nil
    }
  • replacement in tools/cluster.go at line 357
    [3.391305][3.391305:391345]()
    // Begin logged transaction for update
    [3.391305]
    [3.391345]
    func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {
    var output ClusterOutput
    clusterID := *input.ID
    if 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
    }
  • replacement in tools/cluster.go at line 407
    [3.391566][3.391566:391617]()
    _, err = tx.Exec(query, args...)
    if err != nil {
    [3.391566]
    [3.391617]
    if _, err = tx.Exec(query, args...); err != nil {
  • replacement in tools/cluster.go at line 411
    [3.391686][3.391686:392152]()
    // Fetch the updated cluster
    var cluster db.Cluster
    err = 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)
    [3.391686]
    [3.392152]
    cluster, err := fetchClusterByID(ctx, tx, clusterID)
  • edit in tools/cluster.go at line 422
    [3.392482][3.392482:392483]()
  • replacement in tools/calls_modify.go at line 38
    [3.451720][3.451720:451897]()
    // CallsModify modifies a label in a .data file
    func CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {
    var output CallsModifyOutput
    // Validate required flags
    [3.451720]
    [3.451897]
    // validateModifyInput checks required fields and comment constraints.
    func validateModifyInput(input CallsModifyInput) error {
  • replacement in tools/calls_modify.go at line 41
    [3.451920][3.451920:452006]()
    output.Error = "--file is required"
    return output, fmt.Errorf("%s", output.Error)
    [3.451920]
    [3.452006]
    return fmt.Errorf("--file is required")
  • replacement in tools/calls_modify.go at line 44
    [3.452036][3.452036:452126]()
    output.Error = "--reviewer is required"
    return output, fmt.Errorf("%s", output.Error)
    [3.452036]
    [3.452126]
    return fmt.Errorf("--reviewer is required")
  • replacement in tools/calls_modify.go at line 47
    [3.452154][3.452154:452242]()
    output.Error = "--filter is required"
    return output, fmt.Errorf("%s", output.Error)
    [3.452154]
    [3.452242]
    return fmt.Errorf("--filter is required")
  • replacement in tools/calls_modify.go at line 50
    [3.452271][3.452271:452548]()
    output.Error = "--segment is required"
    return output, fmt.Errorf("%s", output.Error)
    }
    // Parse segment time range
    startTime, endTime, err := parseSegmentRange(input.Segment)
    if err != nil {
    output.Error = err.Error()
    return output, fmt.Errorf("%s", output.Error)
    [3.452271]
    [3.452548]
    return fmt.Errorf("--segment is required")
  • edit in tools/calls_modify.go at line 52
    [3.452551][3.452551:452601]()
    // Validate comment (max 140 chars, ASCII only)
  • replacement in tools/calls_modify.go at line 53
    [3.452632][3.452632:452740]()
    output.Error = "--comment must be 140 characters or less"
    return output, fmt.Errorf("%s", output.Error)
    [3.452632]
    [3.452740]
    return fmt.Errorf("--comment must be 140 characters or less")
  • replacement in tools/calls_modify.go at line 57
    [3.452793][3.452793:452934]()
    output.Error = fmt.Sprintf("--comment must be ASCII only (non-ASCII at position %d)", i)
    return output, fmt.Errorf("%s", output.Error)
    [3.452793]
    [3.452934]
    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.Reviewer
    label.Species = newSpecies
    label.CallType = newCallType
    output.Species = newSpecies
    output.CallType = newCallType
    label.Certainty = input.Certainty
    output.Certainty = input.Certainty
    if input.Bookmark != nil && *input.Bookmark != label.Bookmark {
    label.Bookmark = *input.Bookmark
    output.Bookmark = input.Bookmark
    }
    if input.Comment != "" {
    var newComment string
    if 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))
  • edit in tools/calls_modify.go at line 119
    [3.452938]
    [3.452938]
    label.Comment = newComment
    output.Comment = newComment
    }
    return nil
    }
    // CallsModify modifies a label in a .data file
    func CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {
    var output CallsModifyOutput
    if err := validateModifyInput(input); err != nil {
    output.Error = err.Error()
    return output, err
  • edit in tools/calls_modify.go at line 135
    [3.452942]
    [3.452942]
    startTime, endTime, err := parseSegmentRange(input.Segment)
    if err != nil {
    output.Error = err.Error()
    return output, err
    }
  • edit in tools/calls_modify.go at line 145
    [3.453031][3.453031:453053]()
    // Check file exists
  • edit in tools/calls_modify.go at line 150
    [3.453224][3.453224:453245]()
    // Parse .data file
  • edit in tools/calls_modify.go at line 156
    [3.453426][3.453426:453505]()
    // Find matching segment (also checks filter to handle duplicate time ranges)
  • edit in tools/calls_modify.go at line 160
    [3.453746][3.453746:453923]()
    }
    // Find label matching filter
    var targetLabel *utils.Label
    for _, label := range segment.Labels {
    if label.Filter == input.Filter {
    targetLabel = label
    break
    }
  • edit in tools/calls_modify.go at line 162
    [3.453927]
    [3.453927]
    targetLabel := findLabelByFilter(segment, input.Filter)
  • edit in tools/calls_modify.go at line 168
    [3.454119][3.454119:454155]()
    // Store previous value for output
  • replacement in tools/calls_modify.go at line 170
    [3.454205][3.454205:454972]()
    // Calculate new species/calltype
    var newSpecies, newCallType string
    if input.Species != "" {
    if strings.Contains(input.Species, "+") {
    parts := strings.SplitN(input.Species, "+", 2)
    newSpecies = parts[0]
    newCallType = parts[1]
    } else {
    newSpecies = input.Species
    newCallType = "" // Clear calltype
    }
    } else {
    newSpecies = targetLabel.Species
    newCallType = targetLabel.CallType
    }
    // Check if anything would change
    speciesChanging := newSpecies != targetLabel.Species || newCallType != targetLabel.CallType
    certaintyChanging := input.Certainty != targetLabel.Certainty
    bookmarkChanging := input.Bookmark != nil && *input.Bookmark != targetLabel.Bookmark
    commentChanging := input.Comment != "" // Any non-empty comment will be added
    [3.454205]
    [3.454972]
    newSpecies, newCallType := resolveSpecies(input.Species, targetLabel)
  • replacement in tools/calls_modify.go at line 172
    [3.454973][3.454973:455059]()
    if !speciesChanging && !certaintyChanging && !bookmarkChanging && !commentChanging {
    [3.454973]
    [3.455059]
    if !hasModifyChanges(newSpecies, newCallType, input, targetLabel) {
  • replacement in tools/calls_modify.go at line 177
    [3.455174][3.455174:456276]()
    // Update reviewer on file metadata
    dataFile.Meta.Reviewer = input.Reviewer
    // Update species/calltype
    targetLabel.Species = newSpecies
    targetLabel.CallType = newCallType
    output.Species = newSpecies
    output.CallType = newCallType
    // Update certainty
    targetLabel.Certainty = input.Certainty
    output.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.Bookmark
    output.Bookmark = input.Bookmark
    }
    // Update comment (additive - append to existing comment, never destroy)
    if input.Comment != "" {
    var newComment string
    if targetLabel.Comment != "" {
    newComment = targetLabel.Comment + " | " + input.Comment
    } else {
    newComment = input.Comment
    }
    // Check length after combining
    if len(newComment) > 140 {
    output.Error = fmt.Sprintf("Combined comment exceeds 140 characters (%d)", len(newComment))
    return output, fmt.Errorf("%s", output.Error)
    }
    targetLabel.Comment = newComment
    output.Comment = newComment
    [3.455174]
    [3.456276]
    if err := applyLabelChanges(targetLabel, dataFile, input, newSpecies, newCallType, &output); err != nil {
    output.Error = err.Error()
    return output, err
  • edit in tools/calls_modify.go at line 182
    [3.456280][3.456280:456294]()
    // Save file
  • edit in tools/calls_modify.go at line 188
    [3.456479]
    [3.456479]
    }
    // findLabelByFilter finds the first label matching the given filter in a segment.
    func findLabelByFilter(segment *utils.Segment, filter string) *utils.Label {
    for _, label := range segment.Labels {
    if label.Filter == filter {
    return label
    }
    }
    return nil
  • replacement in tools/calls_from_birda.go at line 335
    [3.524545][3.524545:524929]()
    // processBirdaFileCached processes a single BirdNET results file using a DirCache for WAV lookup
    func processBirdaFileCached(birdaFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {
    // Open and parse CSV
    file, err := os.Open(birdaFile)
    if err != nil {
    return nil, false, false, fmt.Errorf("failed to open file: %w", err)
    }
    defer func() { _ = file.Close() }()
    [3.524545]
    [3.524929]
    // birdaColumnIndices holds the parsed column positions from a BirdNET CSV header.
    type birdaColumnIndices struct {
    startIdx int
    endIdx int
    commonNameIdx int
    confidenceIdx int
    fileIdx int
    }
  • replacement in tools/calls_from_birda.go at line 344
    [3.524930][3.524930:525000]()
    // Create CSV reader
    reader := csv.NewReader(file)
    // Read header
    [3.524930]
    [3.525000]
    // parseBirdaCSVHeader reads the CSV header row and returns column indices.
    func parseBirdaCSVHeader(reader *csv.Reader) (birdaColumnIndices, error) {
  • replacement in tools/calls_from_birda.go at line 348
    [3.525047][3.525047:525120]()
    return nil, false, false, fmt.Errorf("failed to read header: %w", err)
    [3.525047]
    [3.525120]
    return birdaColumnIndices{}, fmt.Errorf("failed to read header: %w", err)
  • replacement in tools/calls_from_birda.go at line 351
    [3.525124][3.525124:525256]()
    // Find column indices (handle BOM prefix)
    startIdx := -1
    endIdx := -1
    commonNameIdx := -1
    confidenceIdx := -1
    fileIdx := -1
    [3.525124]
    [3.525256]
    idx := birdaColumnIndices{startIdx: -1, endIdx: -1, commonNameIdx: -1, confidenceIdx: -1, fileIdx: -1}
  • edit in tools/calls_from_birda.go at line 353
    [3.525286][3.525286:525313]()
    // Remove BOM if present
  • replacement in tools/calls_from_birda.go at line 356
    [3.525390][3.525390:525406]()
    startIdx = i
    [3.525390]
    [3.525406]
    idx.startIdx = i
  • replacement in tools/calls_from_birda.go at line 358
    [3.525424][3.525424:525438]()
    endIdx = i
    [3.525424]
    [3.525438]
    idx.endIdx = i
  • replacement in tools/calls_from_birda.go at line 360
    [3.525460][3.525460:525481]()
    commonNameIdx = i
    [3.525460]
    [3.525481]
    idx.commonNameIdx = i
  • replacement in tools/calls_from_birda.go at line 362
    [3.525502][3.525502:525523]()
    confidenceIdx = i
    [3.525502]
    [3.525523]
    idx.confidenceIdx = i
  • replacement in tools/calls_from_birda.go at line 364
    [3.525538][3.525538:525553]()
    fileIdx = i
    [3.525538]
    [3.525553]
    idx.fileIdx = i
  • replacement in tools/calls_from_birda.go at line 368
    [3.525561][3.525561:525727]()
    if startIdx == -1 || endIdx == -1 || commonNameIdx == -1 || confidenceIdx == -1 {
    return nil, false, false, fmt.Errorf("missing required columns in BirdNET file")
    [3.525561]
    [3.525727]
    if idx.startIdx == -1 || idx.endIdx == -1 || idx.commonNameIdx == -1 || idx.confidenceIdx == -1 {
    return birdaColumnIndices{}, fmt.Errorf("missing required columns in BirdNET file")
  • edit in tools/calls_from_birda.go at line 371
    [3.525730]
    [3.525730]
    return idx, nil
    }
  • replacement in tools/calls_from_birda.go at line 374
    [3.525731][3.525731:525751]()
    // Read detections
    [3.525731]
    [3.525751]
    // readBirdaDetections reads all detection records from a BirdNET CSV.
    func readBirdaDetections(reader *csv.Reader, idx birdaColumnIndices) ([]BirdNETDetection, error) {
  • replacement in tools/calls_from_birda.go at line 383
    [3.525876][3.525876:525950]()
    return nil, false, false, fmt.Errorf("failed to read record: %w", err)
    [3.525876]
    [3.525950]
    return nil, fmt.Errorf("failed to read record: %w", err)
  • replacement in tools/calls_from_birda.go at line 387
    [3.525982][3.525982:526162]()
    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)
    [3.525982]
    [3.526162]
    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)
  • replacement in tools/calls_from_birda.go at line 390
    [3.526166][3.526166:526338]()
    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)
    [3.526166]
    [3.526338]
    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)
  • replacement in tools/calls_from_birda.go at line 393
    [3.526342][3.526342:526574]()
    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)
    [3.526342]
    [3.526574]
    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)
  • replacement in tools/calls_from_birda.go at line 397
    [3.526578][3.526578:526656]()
    if fileIdx >= 0 && fileIdx < len(record) {
    det.WAVPath = record[fileIdx]
    [3.526578]
    [3.526656]
    if idx.fileIdx >= 0 && idx.fileIdx < len(record) {
    det.WAVPath = record[idx.fileIdx]
  • edit in tools/calls_from_birda.go at line 403
    [3.526703]
    [3.526703]
    return detections, nil
    }
  • replacement in tools/calls_from_birda.go at line 406
    [3.526704][3.526704:526785]()
    if len(detections) == 0 {
    return nil, false, true, nil // No detections, skip
    [3.526704]
    [3.526785]
    // 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
    }
  • edit in tools/calls_from_birda.go at line 414
    [3.526789][3.526789:526847]()
    // Determine WAV path and .data path
    var wavPath string
  • replacement in tools/calls_from_birda.go at line 418
    [3.526976][3.526976:527156]()
    if detections[0].WAVPath != "" {
    // Check if the path from File column exists
    if _, err := os.Stat(detections[0].WAVPath); err == nil {
    wavPath = detections[0].WAVPath
    }
    [3.526976]
    [3.527156]
    if cache != nil {
    return cache.FindWAV(baseName)
  • edit in tools/calls_from_birda.go at line 421
    [3.527159]
    [3.527159]
    return findWAVFile(dir, baseName)
    }
  • replacement in tools/calls_from_birda.go at line 424
    [3.527160][3.527160:527348]()
    // If not found from File column, search with DirCache
    if wavPath == "" {
    if cache != nil {
    wavPath = cache.FindWAV(baseName)
    } else {
    wavPath = findWAVFile(dir, baseName)
    }
    [3.527160]
    [3.527348]
    // processBirdaFileCached processes a single BirdNET results file using a DirCache for WAV lookup
    func 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
  • edit in tools/calls_from_birda.go at line 447
    [3.527352]
    [3.527352]
    wavPath := resolveBirdaWAVPath(birdaFile, detections[0].WAVPath, cache)
  • replacement in tools/calls_from_birda.go at line 449
    [3.527372][3.527372:527426]()
    return nil, false, true, nil // WAV not found, skip
    [3.527372]
    [3.527426]
    return nil, false, true, nil
  • edit in tools/calls_from_birda.go at line 452
    [3.527430][3.527430:527488]()
    // Check if WAV exists (to get sample rate and duration)
  • replacement in tools/calls_from_birda.go at line 454
    [3.527572][3.527572:527639]()
    return nil, false, true, nil // Skip if WAV not found or invalid
    [3.527572]
    [3.527639]
    return nil, false, true, nil
  • edit in tools/calls_from_birda.go at line 458
    [3.527674][3.527674:527710]()
    // Convert detections to segments
  • replacement in tools/calls_from_birda.go at line 460
    [3.527769][3.527769:527857]()
    // Build metadata
    meta := AviaNZMeta{
    Operator: "BirdNET",
    Duration: duration,
    }
    [3.527769]
    [3.527857]
    meta := AviaNZMeta{Operator: "BirdNET", Duration: duration}
  • edit in tools/calls_from_birda.go at line 464
    [3.527905][3.527905:527939]()
    // Write .data file (safe write)
  • edit in tools/calls_from_birda.go at line 468
    [3.528058][3.528058:528099]()
    // Convert to ClusteredCalls for output
  • replacement in tools/calls_clip_labels.go at line 87
    [3.552934][3.552934:553163]()
    func CallsClipLabels(input CallsClipLabelsInput) (CallsClipLabelsOutput, error) {
    out := CallsClipLabelsOutput{
    Folder: input.Folder,
    OutputPath: input.OutputPath,
    PerClassTrueCount: map[string]int{},
    }
    [3.552934]
    [3.553163]
    // parsedClipFile holds a parsed .data file for clip-labels processing.
    type parsedClipFile struct {
    path string
    df *utils.DataFile
    }
  • replacement in tools/calls_clip_labels.go at line 93
    [3.553164][3.553164:553189]()
    // Validate parameters.
    [3.553164]
    [3.553189]
    // validateClipLabelsInput validates the input parameters and returns the parsed finalClipMode.
    func validateClipLabelsInput(input CallsClipLabelsInput) (utils.FinalClipMode, error) {
  • replacement in tools/calls_clip_labels.go at line 97
    [3.553271][3.553271:553289]()
    return out, err
    [3.553271]
    [3.553289]
    return 0, err
  • replacement in tools/calls_clip_labels.go at line 100
    [3.553322][3.553322:553406]()
    return out, fmt.Errorf("--clip-duration must be > 0, got %v", input.ClipDuration)
    [3.553322]
    [3.553406]
    return 0, fmt.Errorf("--clip-duration must be > 0, got %v", input.ClipDuration)
  • replacement in tools/calls_clip_labels.go at line 103
    [3.553480][3.553480:553580]()
    return out, fmt.Errorf("--clip-overlap must be in [0, clip-duration), got %v", input.ClipOverlap)
    [3.553480]
    [3.553580]
    return 0, fmt.Errorf("--clip-overlap must be in [0, clip-duration), got %v", input.ClipOverlap)
  • replacement in tools/calls_clip_labels.go at line 106
    [3.553616][3.553616:553707]()
    return out, fmt.Errorf("--min-label-overlap must be > 0, got %v", input.MinLabelOverlap)
    [3.553616]
    [3.553707]
    return 0, fmt.Errorf("--min-label-overlap must be > 0, got %v", input.MinLabelOverlap)
  • edit in tools/calls_clip_labels.go at line 108
    [3.553710]
    [3.553710]
    return finalClipMode, nil
    }
  • replacement in tools/calls_clip_labels.go at line 111
    [3.553711][3.553711:553787]()
    // Load mapping.
    mapping, err := utils.LoadMappingFile(input.MappingPath)
    [3.553711]
    [3.553787]
    // parseClipLabelsDataFiles finds and parses .data files, collecting species seen.
    func parseClipLabelsDataFiles(folder, filter string, mapping utils.MappingFile) ([]parsedClipFile, error) {
    dataPaths, err := utils.FindDataFiles(folder)
  • replacement in tools/calls_clip_labels.go at line 115
    [3.553804][3.553804:553876]()
    return out, fmt.Errorf("load mapping %s: %w", input.MappingPath, err)
    [3.553804]
    [3.553876]
    return nil, fmt.Errorf("scan folder %s: %w", folder, err)
  • edit in tools/calls_clip_labels.go at line 117
    [3.553879][3.553879:554402]()
    // 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 = classes
    out.Filter = input.Filter
    classIdx := 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)
    }
  • replacement in tools/calls_clip_labels.go at line 118
    [3.554428][3.554428:554497]()
    return out, fmt.Errorf("no .data files found in %s", input.Folder)
    [3.554428]
    [3.554497]
    return nil, fmt.Errorf("no .data files found in %s", folder)
  • edit in tools/calls_clip_labels.go at line 121
    [3.554501][3.554501:554616]()
    type parsedFile struct {
    path string
    df *utils.DataFile
    }
    parsed := make([]parsedFile, 0, len(dataPaths))
  • edit in tools/calls_clip_labels.go at line 122
    [3.554650]
    [3.554650]
    parsed := make([]parsedClipFile, 0, len(dataPaths))
  • replacement in tools/calls_clip_labels.go at line 126
    [3.554735][3.554735:554785]()
    return out, fmt.Errorf("parse %s: %w", p, err)
    [3.554735]
    [3.554785]
    return nil, fmt.Errorf("parse %s: %w", p, err)
  • replacement in tools/calls_clip_labels.go at line 129
    [3.554836][3.554836:554931]()
    return out, fmt.Errorf("missing or non-positive Duration in %s (cannot generate clips)", p)
    [3.554836]
    [3.554931]
    return nil, fmt.Errorf("missing or non-positive Duration in %s (cannot generate clips)", p)
  • replacement in tools/calls_clip_labels.go at line 133
    [3.555007][3.555007:555065]()
    if input.Filter != "" && lbl.Filter != input.Filter {
    [3.555007]
    [3.555065]
    if filter != "" && lbl.Filter != filter {
  • replacement in tools/calls_clip_labels.go at line 139
    [3.555130][3.555130:555185]()
    parsed = append(parsed, parsedFile{path: p, df: df})
    [3.555130]
    [3.555185]
    parsed = append(parsed, parsedClipFile{path: p, df: df})
  • edit in tools/calls_clip_labels.go at line 141
    [3.555188][3.555188:555223]()
    out.DataFilesParsed = len(parsed)
  • edit in tools/calls_clip_labels.go at line 142
    [3.555224][3.555224:555252]()
    // Mapping coverage check.
  • replacement in tools/calls_clip_labels.go at line 143
    [3.555330][3.555330:555470]()
    return out, fmt.Errorf("mapping.json is missing entries for species: %s\n(run /data-mapping to regenerate)", strings.Join(missing, ", "))
    [3.555330]
    [3.555470]
    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
  • edit in tools/calls_clip_labels.go at line 154
    [3.555473]
    [3.555473]
    for _, r := range rows {
    k := rowKey{file: r.file, start: formatTime(r.start), end: formatTime(r.end)}
    if dedup[k] {
    return fmt.Errorf("duplicate clip detected: file=%s start=%s end=%s", k.file, k.start, k.end)
    }
    dedup[k] = true
    }
    return nil
    }
  • replacement in tools/calls_clip_labels.go at line 164
    [3.555474][3.555474:555546]()
    // Append-mode: read existing header + (file,start,end) tuples if any.
    [3.555474]
    [3.555546]
    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 = classes
    out.Filter = input.Filter
    classIdx := 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)
  • edit in tools/calls_clip_labels.go at line 206
    [3.555820][3.555820:555857]()
    // Path-rendering: relative to cwd.
  • edit in tools/calls_clip_labels.go at line 215
    [3.556069][3.556069:556092]()
    // Process each file.
  • replacement in tools/calls_clip_labels.go at line 224
    [3.556370][3.556370:556533]()
    // Dedup pass — within new rows AND against existing CSV.
    dedup := make(map[rowKey]bool, len(existing)+len(rows))
    for k := range existing {
    dedup[k] = true
    [3.556370]
    [3.556533]
    if err := dedupClipLabelsRows(rows, existing); err != nil {
    return out, err
  • edit in tools/calls_clip_labels.go at line 227
    [3.556536][3.556536:556785]()
    for _, r := range rows {
    k := rowKey{file: r.file, start: formatTime(r.start), end: formatTime(r.end)}
    if dedup[k] {
    return out, fmt.Errorf("duplicate clip detected: file=%s start=%s end=%s", k.file, k.start, k.end)
    }
    dedup[k] = true
    }
  • edit in tools/calls_clip_labels.go at line 228
    [3.556786][3.556786:556801]()
    // Write CSV.
  • replacement in tools/calls_classify.go at line 71
    [3.607894][3.607894:608002]()
    func LoadDataFiles(config ClassifyConfig) (*ClassifyState, error) {
    var filePaths []string
    var err error
    [3.607894]
    [3.608002]
    // findDataFilePaths resolves the list of .data file paths from config.
    func findDataFilePaths(config ClassifyConfig) ([]string, error) {
  • replacement in tools/calls_classify.go at line 74
    [3.608026][3.608026:608062]()
    filePaths = []string{config.File}
    [3.608026]
    [3.608062]
    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 >= 0
    var segs []*utils.Segment
    if !hasFilter {
    segs = df.Segments
  • replacement in tools/calls_classify.go at line 92
    [3.608072][3.608072:608126]()
    filePaths, err = utils.FindDataFiles(config.Folder)
    [3.608072]
    [3.608126]
    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 := 0
    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,
    })
  • replacement in tools/calls_classify.go at line 112
    [3.608144][3.608144:608198]()
    return nil, fmt.Errorf("find data files: %w", err)
    [3.608144]
    [3.608198]
    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
  • edit in tools/calls_classify.go at line 122
    [3.608205]
    [3.608205]
    return segs, true, timeFiltered
    }
  • edit in tools/calls_classify.go at line 125
    [3.608206]
    [3.608206]
    func LoadDataFiles(config ClassifyConfig) (*ClassifyState, error) {
    filePaths, err := findDataFilePaths(config)
    if err != nil {
    return nil, err
    }
  • edit in tools/calls_classify.go at line 143
    [3.608530][3.608530:608531]()
  • edit in tools/calls_classify.go at line 153
    [3.608846][3.608846:608929]()
    hasFilter := config.Filter != "" || config.Species != "" || config.Certainty >= 0
  • replacement in tools/calls_classify.go at line 158
    [3.609047][3.609047:610038]()
    var segs []*utils.Segment
    if !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
    }
    [3.609047]
    [3.610038]
    segs, keep, tf := filterDataFileSegments(df, config)
    timeFiltered += tf
    if !keep {
    continue
  • replacement in cmd/calls_modify.go at line 49
    [3.1119971][3.1119971:1120957]()
    // 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 string
    var certainty int
    var certaintySet, bookmark bool
    [3.1119971]
    [3.1120957]
    // 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
    }
  • replacement in cmd/calls_modify.go at line 62
    [3.1120958][3.1120958:1120978]()
    // Parse arguments
    [3.1120958]
    [3.1120978]
    // 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
  • edit in cmd/calls_modify.go at line 78
    [3.1121024][3.1121024:1121025]()
  • replacement in cmd/calls_modify.go at line 80
    [3.1121057][3.1121057:1121185]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --file requires a value\n")
    os.Exit(1)
    }
    file = args[i+1]
    [3.1121057]
    [3.1121185]
    ma.file = requireFlagValue(args, i, "--file")
  • edit in cmd/calls_modify.go at line 82
    [3.1121195][3.1121195:1121196]()
  • replacement in cmd/calls_modify.go at line 83
    [3.1121217][3.1121217:1121353]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --reviewer requires a value\n")
    os.Exit(1)
    }
    reviewer = args[i+1]
    [3.1121217]
    [3.1121353]
    ma.reviewer = requireFlagValue(args, i, "--reviewer")
  • edit in cmd/calls_modify.go at line 85
    [3.1121363][3.1121363:1121364]()
  • replacement in cmd/calls_modify.go at line 86
    [3.1121383][3.1121383:1121515]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --filter requires a value\n")
    os.Exit(1)
    }
    filter = args[i+1]
    [3.1121383]
    [3.1121515]
    ma.filter = requireFlagValue(args, i, "--filter")
  • edit in cmd/calls_modify.go at line 88
    [3.1121525][3.1121525:1121526]()
  • replacement in cmd/calls_modify.go at line 89
    [3.1121546][3.1121546:1121680]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --segment requires a value\n")
    os.Exit(1)
    }
    segment = args[i+1]
    [3.1121546]
    [3.1121680]
    ma.segment = requireFlagValue(args, i, "--segment")
  • edit in cmd/calls_modify.go at line 91
    [3.1121690][3.1121690:1121691]()
  • replacement in cmd/calls_modify.go at line 92
    [3.1121711][3.1121711:1121845]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --species requires a value\n")
    os.Exit(1)
    }
    species = args[i+1]
    [3.1121711]
    [3.1121845]
    ma.species = requireFlagValue(args, i, "--species")
  • edit in cmd/calls_modify.go at line 94
    [3.1121855][3.1121855:1121856]()
  • replacement in cmd/calls_modify.go at line 95
    [3.1121878][3.1121878:1122028]()
    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])
    [3.1121878]
    [3.1122028]
    val := requireFlagValue(args, i, "--certainty")
    v, err := strconv.Atoi(val)
  • replacement in cmd/calls_modify.go at line 101
    [3.1122137][3.1122137:1122177]()
    certainty = v
    certaintySet = true
    [3.1122137]
    [3.1122177]
    ma.certainty = v
    ma.certaintySet = true
  • edit in cmd/calls_modify.go at line 104
    [3.1122187][3.1122187:1122188]()
  • replacement in cmd/calls_modify.go at line 105
    [3.1122209][3.1122209:1122228]()
    bookmark = true
    [3.1122209]
    [3.1122228]
    ma.bookmark = true
  • edit in cmd/calls_modify.go at line 107
    [3.1122235][3.1122235:1122236]()
  • replacement in cmd/calls_modify.go at line 108
    [3.1122256][3.1122256:1122390]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --comment requires a value\n")
    os.Exit(1)
    }
    comment = args[i+1]
    [3.1122256]
    [3.1122390]
    ma.comment = requireFlagValue(args, i, "--comment")
  • edit in cmd/calls_modify.go at line 110
    [3.1122400][3.1122400:1122401]()
  • edit in cmd/calls_modify.go at line 113
    [3.1122460][3.1122460:1122461]()
  • edit in cmd/calls_modify.go at line 114
    [3.1122472][3.1122472:1122502]()
    // Check for unknown flags
  • edit in cmd/calls_modify.go at line 122
    [3.1122659]
    [3.1122659]
    return ma
    }
  • replacement in cmd/calls_modify.go at line 125
    [3.1122660][3.1122660:1122688]()
    // Validate required flags
    [3.1122660]
    [3.1122688]
    // validateModifyArgs checks required flags and value ranges.
    func validateModifyArgs(ma modifyArgs) {
  • replacement in cmd/calls_modify.go at line 128
    [3.1122711][3.1122711:1122728]()
    if file == "" {
    [3.1122711]
    [3.1122728]
    if ma.file == "" {
  • replacement in cmd/calls_modify.go at line 131
    [3.1122769][3.1122769:1122790]()
    if reviewer == "" {
    [3.1122769]
    [3.1122790]
    if ma.reviewer == "" {
  • replacement in cmd/calls_modify.go at line 134
    [3.1122835][3.1122835:1122854]()
    if filter == "" {
    [3.1122835]
    [3.1122854]
    if ma.filter == "" {
  • replacement in cmd/calls_modify.go at line 137
    [3.1122897][3.1122897:1122917]()
    if segment == "" {
    [3.1122897]
    [3.1122917]
    if ma.segment == "" {
  • replacement in cmd/calls_modify.go at line 140
    [3.1122961][3.1122961:1122981]()
    if !certaintySet {
    [3.1122961]
    [3.1122981]
    if !ma.certaintySet {
  • replacement in cmd/calls_modify.go at line 148
    [3.1123162][3.1123162:1123231]()
    // Validate certainty range
    if certainty < 0 || certainty > 100 {
    [3.1123162]
    [3.1123231]
    if ma.certainty < 0 || ma.certainty > 100 {
  • edit in cmd/calls_modify.go at line 152
    [3.1123322]
    [3.1123322]
    }
  • replacement in cmd/calls_modify.go at line 154
    [3.1123323][3.1123323:1123339]()
    // Build input
    [3.1123323]
    [3.1123339]
    // 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)
  • replacement in cmd/calls_modify.go at line 175
    [3.1123373][3.1123373:1123526]()
    File: file,
    Reviewer: reviewer,
    Filter: filter,
    Segment: segment,
    Species: species,
    Certainty: certainty,
    Comment: comment,
    [3.1123373]
    [3.1123526]
    File: ma.file,
    Reviewer: ma.reviewer,
    Filter: ma.filter,
    Segment: ma.segment,
    Species: ma.species,
    Certainty: ma.certainty,
    Comment: ma.comment,
  • replacement in cmd/calls_modify.go at line 183
    [3.1123529][3.1123529:1123573]()
    if bookmark {
    input.Bookmark = &bookmark
    [3.1123529]
    [3.1123573]
    if ma.bookmark {
    input.Bookmark = &ma.bookmark
  • edit in cmd/calls_modify.go at line 187
    [3.1123577][3.1123577:1123589]()
    // Execute
  • edit in cmd/calls_modify.go at line 193
    [3.1123718][3.1123718:1123734]()
    // Output JSON
  • edit in CHANGELOG.md at line 4
    [3.1198010]
    [2.29278]
    ## [2026-05-04] Reduce cyclomatic complexity of 8 more functions over gocyclo 25
    Refactored 8 functions that exceeded cyclomatic complexity of 25 by extracting
    helper functions with clear responsibilities:
  • edit in CHANGELOG.md at line 10
    [2.29279]
    [2.29279]
    1. **`processBirdaFileCached` (30→~10)**: Extracted `parseBirdaCSVHeader`,
    `readBirdaDetections`, `resolveBirdaWAVPath` for CSV parsing and WAV path
    resolution.
    2. **`RunCallsModify` (30→~5)**: Extracted `modifyArgs` struct,
    `parseModifyArgs`, `requireFlagValue`, `validateModifyArgs` for CLI argument
    parsing and validation.
    3. **`CallsModify` (29→~10)**: Extracted `validateModifyInput`, `resolveSpecies`,
    `hasModifyChanges`, `applyLabelChanges`, `findLabelByFilter` for input
    validation, species resolution, and label update logic.
    4. **`CallsClipLabels` (29→~15)**: Extracted `parsedClipFile` type,
    `validateClipLabelsInput`, `parseClipLabelsDataFiles`,
    `dedupClipLabelsRows` for parameter validation, file parsing, and
    deduplication.
    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 range
    calculation. Eliminated `goto` statement.
    7. **`importSegmentsIntoDB` (27→~10)**: Extracted `importLabelResult` type,
    `importSingleLabel`, `importCalltype`, `importSegment` for per-label and
    per-segment DB insertion.
    8. **`updateCluster` (27→~17)**: Extracted `validateClusterActive`,
    `validateCyclicPattern`, `buildClusterUpdateQuery` for cluster validation
    and dynamic query building.