complexity in tools calls and import

quietlight
May 19, 2026, 1:50 AM
LHZQOX64RAXIJIHFJ5RN5TFJ4VFRRM42W2ZT7MRCNQ2AJVA4BSSAC

Dependencies

  • [2] V2HX6HEB claude going nuts all over the place
  • [3] JMDW37LV new import tests
  • [4] NQPVZ3PP first phase of utils refactor, all realted to db interfaces
  • [5] NUOFNUIQ simplified --bandpass
  • [6] 3DVPQOKB big tidy up of tools/
  • [7] P4CJMBYK added first version of --bandpass flag to calls classify, work to do
  • [8] O45G7VX2 added an add and a remove command
  • [9] XU7FTYK3 third phase of utils refactor, wav/
  • [10] PXQDGTR5 fourth phase of utils refactor, spectrogram/

Change contents

  • edit in tools/import/test_helpers.go at line 11
    [3.109][3.109:117]()
    "time"
  • edit in tools/import/test_helpers.go at line 179
    [3.6035][3.6035:6290]()
    // createTestWAVWithMetadata creates a WAV file and inserts it into the database.
    // Returns the file ID and hash.
    func createTestWAVWithMetadata(t *testing.T, database *sql.DB, clusterID, locationID, filename string) (fileID, hash string) {
    t.Helper()
  • edit in tools/import/test_helpers.go at line 180
    [3.6291][3.6291:6949]()
    // Create temp file
    tmpDir := t.TempDir()
    wavPath := filepath.Join(tmpDir, filename)
    hash = createTestWAV(t, wavPath)
    // Generate file ID
    fileID, err := utils.GenerateLongID()
    if err != nil {
    t.Fatalf("failed to generate file ID: %v", err)
    }
    // Insert file record
    _, err = database.ExecContext(context.Background(), `
    INSERT INTO file (id, file_name, xxh64_hash, location_id, cluster_id, timestamp_local, duration, sample_rate, active)
    VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 0.0005, 16000, true)
    `, fileID, filename, hash, locationID, clusterID)
    if err != nil {
    t.Fatalf("failed to insert file: %v", err)
    }
    return fileID, hash
    }
  • replacement in tools/import/test_helpers.go at line 241
    [3.8431][3.8431:8451]()
    defer file.Close()
    [3.8431]
    [3.8451]
    defer func() { _ = file.Close() }()
  • edit in tools/import/test_helpers.go at line 268
    [3.9237][3.9237:9655]()
    }
    // assertFileCount queries the database and asserts the expected number of files.
    func assertFileCount(t *testing.T, database *sql.DB, expected int) {
    t.Helper()
    var count int
    if err := database.QueryRow("SELECT COUNT(*) FROM file WHERE active = true").Scan(&count); err != nil {
    t.Fatalf("failed to count files: %v", err)
    }
    if count != expected {
    t.Errorf("expected %d files, got %d", expected, count)
    }
  • edit in tools/import/test_helpers.go at line 270
    [3.9658][3.9658:10819]()
    // assertSegmentCount queries the database and asserts the expected number of segments.
    func assertSegmentCount(t *testing.T, database *sql.DB, expected int) {
    t.Helper()
    var count int
    if err := database.QueryRow("SELECT COUNT(*) FROM segment WHERE active = true").Scan(&count); err != nil {
    t.Fatalf("failed to count segments: %v", err)
    }
    if count != expected {
    t.Errorf("expected %d segments, got %d", expected, count)
    }
    }
    // assertLabelCount queries the database and asserts the expected number of labels.
    func assertLabelCount(t *testing.T, database *sql.DB, expected int) {
    t.Helper()
    var count int
    if err := database.QueryRow("SELECT COUNT(*) FROM label WHERE active = true").Scan(&count); err != nil {
    t.Fatalf("failed to count labels: %v", err)
    }
    if count != expected {
    t.Errorf("expected %d labels, got %d", expected, count)
    }
    }
    // getTestLocationData returns location data for testing.
    func getTestLocationData(t *testing.T, database *sql.DB, locationID string) *LocationData {
    t.Helper()
    data, err := GetLocationData(database, locationID)
    if err != nil {
    t.Fatalf("failed to get location data: %v", err)
    }
    return data
    }
  • edit in tools/import/test_helpers.go at line 279
    [3.11108][3.11108:11251]()
    // waitForAsync waits for a short duration to allow async operations to complete.
    func waitForAsync() {
    time.Sleep(100 * time.Millisecond)
    }
  • edit in tools/import/import_unstructured.go at line 209
    [4.46986]
    [4.6024]
    if recursive {
    return scanWavFilesRecursive(folderPath)
    }
    return scanWavFilesFlat(folderPath)
    }
    // scanWavFilesRecursive walks the directory tree recursively.
    func scanWavFilesRecursive(folderPath string) ([]string, []FileImportError) {
  • replacement in tools/import/import_unstructured.go at line 220
    [4.6081][4.6081:6146]()
    walkFunc := func(path string, d fs.DirEntry, err error) error {
    [4.6081]
    [4.6146]
    err := filepath.WalkDir(folderPath, func(path string, d fs.DirEntry, err error) error {
  • edit in tools/import/import_unstructured.go at line 229
    [4.6316][4.6316:6356]()
    // Skip directories if not recursive
  • edit in tools/import/import_unstructured.go at line 230
    [4.6373][4.6373:6441]()
    if !recursive && path != folderPath {
    return fs.SkipDir
    }
  • replacement in tools/import/import_unstructured.go at line 232
    [4.6459][4.6459:6569]()
    // Check for .wav extension (case-insensitive)
    if strings.HasSuffix(strings.ToLower(d.Name()), ".wav") {
    [4.6459]
    [4.6569]
    if isWavFile(d.Name()) {
  • edit in tools/import/import_unstructured.go at line 235
    [4.6604][4.6604:6605]()
  • edit in tools/import/import_unstructured.go at line 236
    [4.6618]
    [4.6618]
    })
    if err != nil {
    errors = append(errors, FileImportError{
    FileName: folderPath,
    Error: err.Error(),
    Stage: StageScan,
    })
  • edit in tools/import/import_unstructured.go at line 244
    [4.6621]
    [4.6621]
    return files, errors
    }
  • replacement in tools/import/import_unstructured.go at line 247
    [4.6622][4.6622:6703](),[4.6703][4.47089:47133](),[4.47133][4.6753:6806](),[4.6753][4.6753:6806](),[4.6806][4.47134:47159](),[4.47159][4.6837:6956](),[4.6837][4.6837:6956](),[4.6956][4.47160:47204](),[4.47204][4.7006:7059](),[4.7006][4.7006:7059](),[4.7059][4.47205:47230](),[4.47230][4.7090:7122](),[4.7090][4.7090:7122]()
    if recursive {
    if err := filepath.WalkDir(folderPath, walkFunc); err != nil {
    errors = append(errors, FileImportError{
    FileName: folderPath,
    Error: err.Error(),
    Stage: StageScan,
    })
    }
    } else {
    // Non-recursive: only scan top-level
    entries, err := os.ReadDir(folderPath)
    if err != nil {
    errors = append(errors, FileImportError{
    FileName: folderPath,
    Error: err.Error(),
    Stage: StageScan,
    })
    return nil, errors
    }
    [4.6622]
    [4.7122]
    // scanWavFilesFlat scans only the top-level directory.
    func scanWavFilesFlat(folderPath string) ([]string, []FileImportError) {
    var files []string
    var errors []FileImportError
    entries, err := os.ReadDir(folderPath)
    if err != nil {
    errors = append(errors, FileImportError{
    FileName: folderPath,
    Error: err.Error(),
    Stage: StageScan,
    })
    return nil, errors
    }
  • replacement in tools/import/import_unstructured.go at line 262
    [4.7123][4.7123:7312]()
    for _, entry := range entries {
    if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".wav") {
    files = append(files, filepath.Join(folderPath, entry.Name()))
    }
    [4.7123]
    [4.7312]
    for _, entry := range entries {
    if !entry.IsDir() && isWavFile(entry.Name()) {
    files = append(files, filepath.Join(folderPath, entry.Name()))
  • edit in tools/import/import_unstructured.go at line 267
    [4.7319]
    [4.7319]
    return files, errors
    }
  • replacement in tools/import/import_unstructured.go at line 270
    [4.7320][4.7320:7342]()
    return files, errors
    [4.7320]
    [4.7342]
    // isWavFile returns true if the filename has a .wav extension (case-insensitive).
    func isWavFile(name string) bool {
    return strings.HasSuffix(strings.ToLower(name), ".wav")
  • edit in tools/import/import_segments.go at line 582
    [4.27230]
    [4.27230]
    }
    // resolvedLabelIDs holds the resolved database IDs for a label.
    type resolvedLabelIDs struct {
    speciesID string
    filterID string
    labelID string
    dbSpecies string
  • replacement in tools/import/import_segments.go at line 592
    [4.27233][4.27233:27379]()
    // importSingleLabel inserts a single label and its metadata/subtype into the DB.
    func importSingleLabel(
    ctx context.Context,
    tx *db.LoggedTx,
    [4.27233]
    [2.60182]
    // resolveLabelIDs looks up species and filter IDs, generates a label ID.
    // Returns an error if any lookup fails.
    func resolveLabelIDs(
  • edit in tools/import/import_segments.go at line 596
    [2.60206][4.27400:27442](),[4.27400][4.27400:27442]()
    segmentID string,
    segIdx, labelIdx int,
  • replacement in tools/import/import_segments.go at line 600
    [4.27556][4.27556:27623]()
    calltypeIDMap map[string]map[string]string,
    ) importLabelResult {
    [4.27556]
    [4.27623]
    ) (resolvedLabelIDs, error) {
  • replacement in tools/import/import_segments.go at line 603
    [4.27687][4.27687:27739](),[4.27739][4.48473:48530](),[4.48530][4.27802:27899](),[4.27802][4.27802:27899]()
    return importLabelResult{err: ImportSegmentError{
    File: filepath.Base(sf.DataPath), Stage: StageImport,
    Message: fmt.Sprintf("species not found in mapping: %s", label.Species),
    }, hasError: true}
    [4.27687]
    [4.27899]
    return resolvedLabelIDs{}, fmt.Errorf("species not found in mapping: %s", label.Species)
  • replacement in tools/import/import_segments.go at line 608
    [4.27955][4.27955:28007](),[4.28007][4.48531:48588](),[4.48588][4.28070:28155](),[4.28070][4.28070:28155]()
    return importLabelResult{err: ImportSegmentError{
    File: filepath.Base(sf.DataPath), Stage: StageImport,
    Message: fmt.Sprintf("species ID not found: %s", dbSpecies),
    }, hasError: true}
    [4.27955]
    [4.28155]
    return resolvedLabelIDs{}, fmt.Errorf("species ID not found: %s", dbSpecies)
  • replacement in tools/import/import_segments.go at line 613
    [4.28212][4.28212:28264](),[4.28264][4.48589:48646](),[4.48646][4.28327:28414](),[4.28327][4.28327:28414]()
    return importLabelResult{err: ImportSegmentError{
    File: filepath.Base(sf.DataPath), Stage: StageImport,
    Message: fmt.Sprintf("filter ID not found: %s", label.Filter),
    }, hasError: true}
    [4.28212]
    [4.28414]
    return resolvedLabelIDs{}, fmt.Errorf("filter ID not found: %s", label.Filter)
  • edit in tools/import/import_segments.go at line 617
    [4.28458]
    [4.28458]
    if err != nil {
    return resolvedLabelIDs{}, fmt.Errorf("failed to generate label ID: %w", err)
    }
    return resolvedLabelIDs{
    speciesID: speciesID,
    filterID: filterID,
    labelID: labelID,
    dbSpecies: dbSpecies,
    }, nil
    }
    // importSingleLabel inserts a single label and its metadata/subtype into the DB.
    func importSingleLabel(
    ctx context.Context,
    tx *db.LoggedTx,
    label *datafile.Label,
    segmentID string,
    segIdx, labelIdx int,
    sf scannedDataFile,
    mapping MappingFile,
    filterIDMap map[string]string,
    speciesIDMap map[string]string,
    calltypeIDMap map[string]map[string]string,
    ) importLabelResult {
    // Resolve all IDs first
    ids, err := resolveLabelIDs(label, sf, mapping, filterIDMap, speciesIDMap)
  • replacement in tools/import/import_segments.go at line 647
    [4.48704][4.28590:28655](),[4.28590][4.28590:28655]()
    Message: fmt.Sprintf("failed to generate label ID: %v", err),
    [4.48704]
    [4.28655]
    Message: err.Error(),
  • replacement in tools/import/import_segments.go at line 651
    [4.28680][4.28680:28942]()
    _, 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 {
    [4.28680]
    [4.28942]
    // Insert the label
    if err := insertLabel(ctx, tx, ids, segmentID, label); err != nil {
  • replacement in tools/import/import_segments.go at line 655
    [4.48762][4.29057:29117](),[4.29057][4.29057:29117]()
    Message: fmt.Sprintf("failed to insert label: %v", err),
    [4.48762]
    [4.29117]
    Message: err.Error(),
  • replacement in tools/import/import_segments.go at line 661
    [4.29212][4.29212:29542]()
    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 {
    [4.29212]
    [4.29542]
    if err := insertLabelMetadata(ctx, tx, ids.labelID, label.Comment); err != nil {
  • replacement in tools/import/import_segments.go at line 664
    [4.48821][4.29659:29729](),[4.29659][4.29659:29729]()
    Message: fmt.Sprintf("failed to insert label_metadata: %v", err),
    [4.48821]
    [4.29729]
    Message: err.Error(),
  • replacement in tools/import/import_segments.go at line 670
    [4.29788][4.29788:29834]()
    LabelID: labelID,
    Species: dbSpecies,
    [4.29788]
    [4.29834]
    LabelID: ids.labelID,
    Species: ids.dbSpecies,
  • replacement in tools/import/import_segments.go at line 681
    [4.30033][4.30033:30203]()
    if err := importCalltype(ctx, tx, labelID, label, dbSpecies, filterID, mapping, calltypeIDMap, sf); err != nil {
    return importLabelResult{err: *err, hasError: true}
    [4.30033]
    [4.30203]
    if ctErr := importCalltype(ctx, tx, ids.labelID, label, ids.dbSpecies, ids.filterID, mapping, calltypeIDMap, sf); ctErr != nil {
    return importLabelResult{err: *ctErr, hasError: true}
  • replacement in tools/import/import_segments.go at line 685
    [4.30285][4.30285:30377]()
    return importLabelResult{labelImport: labelImport, labelID: labelID, subtypesImported: 1}
    [4.30285]
    [4.30377]
    return importLabelResult{labelImport: labelImport, labelID: ids.labelID, subtypesImported: 1}
  • edit in tools/import/import_segments.go at line 687
    [4.30380]
    [4.30380]
    return importLabelResult{labelImport: labelImport, labelID: ids.labelID}
    }
  • replacement in tools/import/import_segments.go at line 691
    [4.30381][4.30381:30451]()
    return importLabelResult{labelImport: labelImport, labelID: labelID}
    [4.30381]
    [4.30451]
    // insertLabel inserts a label row into the database.
    func insertLabel(ctx context.Context, tx *db.LoggedTx, ids resolvedLabelIDs, segmentID string, label *datafile.Label) error {
    _, err := tx.ExecContext(ctx, `
    INSERT INTO label (id, segment_id, species_id, filter_id, certainty, created_at, last_modified, active)
    VALUES (?, ?, ?, ?, ?, now(), now(), true)
    `, ids.labelID, segmentID, ids.speciesID, ids.filterID, label.Certainty)
    if err != nil {
    return fmt.Errorf("failed to insert label: %w", err)
    }
    return nil
  • edit in tools/import/import_segments.go at line 703
    [4.30454]
    [4.30454]
    // insertLabelMetadata inserts a label_metadata row for a comment.
    func insertLabelMetadata(ctx context.Context, tx *db.LoggedTx, labelID, comment string) error {
    escapedComment := strings.ReplaceAll(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 {
    return fmt.Errorf("failed to insert label_metadata: %w", err)
    }
    return nil
    }
  • edit in tools/import/import_files_test.go at line 212
    [3.33668][3.33668:33782]()
    // boolPtr returns a pointer to the bool value.
    //
    //go:fix inline
    func boolPtr(v bool) *bool {
    return new(v)
    }
  • replacement in tools/calls/calls_show_images.go at line 32
    [4.83841][4.83841:84001]()
    // Validate file exists
    if _, err := os.Stat(input.DataFilePath); os.IsNotExist(err) {
    output.Error = fmt.Sprintf("File not found: %s", input.DataFilePath)
    [4.83841]
    [4.84001]
    // Validate input and get data file
    dataFile, wavPath, err := validateAndParseShowImagesInput(input)
    if err != nil {
    output.Error = err.Error()
    return output, err
    }
    output.WavFile = wavPath
    if len(dataFile.Segments) == 0 {
    output.Error = "No segments found in .data file"
  • replacement in tools/calls/calls_show_images.go at line 45
    [4.84053][4.84053:84185]()
    // Derive WAV file path (strip .data suffix)
    wavPath := strings.TrimSuffix(input.DataFilePath, ".data")
    output.WavFile = wavPath
    [4.84053]
    [4.84185]
    // Resolve image size and protocol
    imgSize := resolveImageSize(input.ImageSize)
    protocol := resolveGraphicsProtocol(input)
  • replacement in tools/calls/calls_show_images.go at line 49
    [4.84186][4.84186:84212]()
    // Check WAV file exists
    [4.84186]
    [4.84212]
    // Generate and display spectrograms
    segmentsShown, err := displaySegmentSpectrograms(input.DataFilePath, dataFile.Segments, input.Color, imgSize, protocol)
    if err != nil {
    output.Error = err.Error()
    return output, err
    }
    output.SegmentsShown = segmentsShown
    return output, nil
    }
    // validateAndParseShowImagesInput validates input files and parses the .data file.
    func validateAndParseShowImagesInput(input CallsShowImagesInput) (*datafile.DataFile, string, error) {
    if _, err := os.Stat(input.DataFilePath); os.IsNotExist(err) {
    return nil, "", fmt.Errorf("file not found: %s", input.DataFilePath)
    }
    wavPath := strings.TrimSuffix(input.DataFilePath, ".data")
  • replacement in tools/calls/calls_show_images.go at line 68
    [4.84265][4.84265:84377]()
    output.Error = fmt.Sprintf("WAV file not found: %s", wavPath)
    return output, fmt.Errorf("%s", output.Error)
    [4.84265]
    [4.84377]
    return nil, "", fmt.Errorf("WAV file not found: %s", wavPath)
  • edit in tools/calls/calls_show_images.go at line 71
    [4.84381][4.84381:84441]()
    // Parse .data file (includes labels for future filtering)
  • replacement in tools/calls/calls_show_images.go at line 73
    [4.84516][4.84516:84593]()
    output.Error = err.Error()
    return output, fmt.Errorf("%s", output.Error)
    [4.84516]
    [4.84593]
    return nil, "", err
  • replacement in tools/calls/calls_show_images.go at line 76
    [4.84597][4.84597:84733]()
    if len(dataFile.Segments) == 0 {
    output.Error = "No segments found in .data file"
    return output, fmt.Errorf("%s", output.Error)
    }
    [4.84597]
    [4.84733]
    return dataFile, wavPath, nil
    }
  • replacement in tools/calls/calls_show_images.go at line 79
    [4.84734][4.84734:84804](),[4.84804][4.45578:45625]()
    // Resolve image size
    imgSize := input.ImageSize
    if imgSize == 0 {
    imgSize = spectrogram.SpectrogramDisplaySize
    [4.84734]
    [4.84845]
    // resolveImageSize returns the image size, using default if zero.
    func resolveImageSize(size int) int {
    if size == 0 {
    return spectrogram.SpectrogramDisplaySize
  • edit in tools/calls/calls_show_images.go at line 84
    [4.84848]
    [4.84848]
    return size
    }
  • replacement in tools/calls/calls_show_images.go at line 87
    [4.84849][4.84849:84878](),[4.84878][4.45626:45665]()
    // Select graphics protocol
    protocol := spectrogram.ProtocolKitty
    [4.84849]
    [4.84911]
    // resolveGraphicsProtocol selects the terminal graphics protocol.
    func resolveGraphicsProtocol(input CallsShowImagesInput) spectrogram.ImageProtocol {
  • replacement in tools/calls/calls_show_images.go at line 90
    [4.84929][4.45666:45705](),[4.45705][4.84962:84987](),[4.84962][4.84962:84987](),[4.84987][4.45706:45745]()
    protocol = spectrogram.ProtocolITerm
    } else if input.Sixel {
    protocol = spectrogram.ProtocolSixel
    [4.84929]
    [4.85020]
    return spectrogram.ProtocolITerm
  • edit in tools/calls/calls_show_images.go at line 92
    [4.85023]
    [4.85023]
    if input.Sixel {
    return spectrogram.ProtocolSixel
    }
    return spectrogram.ProtocolKitty
    }
  • replacement in tools/calls/calls_show_images.go at line 98
    [4.85024][4.85024:85150](),[4.85150][4.45746:45869]()
    // Generate spectrogram for each segment and output
    for i, seg := range dataFile.Segments {
    // Generate spectrogram image
    img, err := spectrogram.GenerateSegmentSpectrogram(input.DataFilePath, seg.StartTime, seg.EndTime, input.Color, imgSize)
    [4.85024]
    [4.85267]
    // displaySegmentSpectrograms generates and displays spectrograms for each segment.
    func displaySegmentSpectrograms(dataFilePath string, segments []*datafile.Segment, color bool, imgSize int, protocol spectrogram.ImageProtocol) (int, error) {
    shown := 0
    for i, seg := range segments {
    img, err := spectrogram.GenerateSegmentSpectrogram(dataFilePath, seg.StartTime, seg.EndTime, color, imgSize)
  • edit in tools/calls/calls_show_images.go at line 107
    [4.85316][4.85316:85340]()
    // Print segment info
  • edit in tools/calls/calls_show_images.go at line 111
    [4.85528][4.85528:85580]()
    // Write to stdout via terminal graphics protocol
  • replacement in tools/calls/calls_show_images.go at line 112
    [4.45945][4.85649:85762](),[4.85649][4.85649:85762]()
    output.Error = fmt.Sprintf("Failed to write image: %v", err)
    return output, fmt.Errorf("%s", output.Error)
    [4.45945]
    [4.85762]
    return shown, fmt.Errorf("failed to write image: %w", err)
  • replacement in tools/calls/calls_show_images.go at line 114
    [4.85766][4.85766:85805]()
    fmt.Println() // Newline after image
    [4.85766]
    [4.85805]
    fmt.Println()
    shown++
  • replacement in tools/calls/calls_show_images.go at line 117
    [4.85808][4.85808:85876]()
    output.SegmentsShown = len(dataFile.Segments)
    return output, nil
    [4.85808]
    [4.85876]
    return shown, nil
  • edit in tools/calls/calls_remove.go at line 72
    [2.64244]
    [2.64244]
    matches := filterLabelsBySpecies(segment.Labels, species, callType, filter)
    if len(matches) == 0 {
    return nil, ""
    }
    if callType == "" && len(matches) > 1 {
    return nil, formatAmbiguousCalltypeError(species, filter, matches)
    }
    return matches, ""
    }
    // filterLabelsBySpecies returns labels matching the given species, calltype, and filter.
    func filterLabelsBySpecies(labels []*datafile.Label, species, callType, filter string) []*datafile.Label {
  • replacement in tools/calls/calls_remove.go at line 88
    [2.64275][4.13634:13674](),[4.13634][4.13634:13674]()
    for _, label := range segment.Labels {
    [2.64275]
    [4.13674]
    for _, label := range labels {
  • edit in tools/calls/calls_remove.go at line 96
    [4.13851][4.13851:13896]()
    }
    if len(matches) == 0 {
    return nil, ""
  • edit in tools/calls/calls_remove.go at line 97
    [4.13899]
    [4.13899]
    return matches
    }
  • replacement in tools/calls/calls_remove.go at line 100
    [4.13900][4.13900:14093]()
    if callType == "" && len(matches) > 1 {
    var callTypes []string
    for _, l := range matches {
    ct := l.CallType
    if ct == "" {
    ct = "(none)"
    }
    callTypes = append(callTypes, ct)
    [4.13900]
    [4.14093]
    // formatAmbiguousCalltypeError creates an error message for ambiguous calltype matches.
    func formatAmbiguousCalltypeError(species, filter string, matches []*datafile.Label) string {
    var callTypes []string
    for _, l := range matches {
    ct := l.CallType
    if ct == "" {
    ct = "(none)"
  • replacement in tools/calls/calls_remove.go at line 108
    [4.14097][4.14097:14258]()
    return nil, fmt.Sprintf("multiple labels match species '%s' with filter '%s', specify --calltype to disambiguate (calltypes: %v)", species, filter, callTypes)
    [4.14097]
    [4.14258]
    callTypes = append(callTypes, ct)
  • replacement in tools/calls/calls_remove.go at line 110
    [4.14261][4.14261:14282]()
    return matches, ""
    [4.14261]
    [4.14282]
    return fmt.Sprintf("multiple labels match species '%s' with filter '%s', specify --calltype to disambiguate (calltypes: %v)", species, filter, callTypes)
  • replacement in tools/calls/calls_remove.go at line 238
    [4.18426][4.18426:18590]()
    // If segment has no labels left, remove it
    if len(targetSegment.Labels) == 0 {
    removeSegmentFromDataFile(dataFile, targetSegment)
    output.Removed = "segment"
    [4.18426]
    [4.18590]
    // Handle the result of label removal
    output.Removed = handleRemovalResult(dataFile, targetSegment, input.File)
  • replacement in tools/calls/calls_remove.go at line 241
    [4.18591][4.18591:18903]()
    // If no segments left, remove the .data file
    if len(dataFile.Segments) == 0 {
    if err := os.Remove(input.File); err != nil {
    return removeOutputError(&output, fmt.Sprintf("failed to remove file: %v", err))
    }
    output.Removed = "file"
    return output, nil
    }
    } else {
    output.Removed = "label"
    [4.18591]
    [4.18903]
    // Handle file removal case
    if output.Removed == "file" {
    return output, nil
  • edit in tools/calls/calls_remove.go at line 251
    [4.19079]
    [4.19079]
    }
    // handleRemovalResult handles segment/file removal after labels are removed.
    // Returns "label", "segment", or "file" depending on what was removed.
    func handleRemovalResult(dataFile *datafile.DataFile, targetSegment *datafile.Segment, filePath string) string {
    if len(targetSegment.Labels) > 0 {
    return "label"
    }
    removeSegmentFromDataFile(dataFile, targetSegment)
    if len(dataFile.Segments) > 0 {
    return "segment"
    }
    // No segments left, remove the .data file
    os.Remove(filePath)
    return "file"
  • edit in tools/calls/calls_from_birda.go at line 108
    [4.207077]
    [4.207077]
    }
    idx := detectBirdaColumns(header)
    if err := validateBirdaColumns(idx); err != nil {
    return birdaColumnIndices{}, err
  • edit in tools/calls/calls_from_birda.go at line 114
    [4.207080]
    [4.207080]
    return idx, nil
    }
  • edit in tools/calls/calls_from_birda.go at line 117
    [4.207081]
    [4.207081]
    // detectBirdaColumns scans the header row and returns column indices.
    func detectBirdaColumns(header []string) birdaColumnIndices {
  • edit in tools/calls/calls_from_birda.go at line 120
    [4.207185]
    [4.207185]
    columnMap := map[string]*int{
    "Start (s)": &idx.startIdx,
    "End (s)": &idx.endIdx,
    "Common name": &idx.commonNameIdx,
    "Confidence": &idx.confidenceIdx,
    "File": &idx.fileIdx,
    }
  • replacement in tools/calls/calls_from_birda.go at line 130
    [4.207257][4.207257:207475]()
    switch col {
    case "Start (s)":
    idx.startIdx = i
    case "End (s)":
    idx.endIdx = i
    case "Common name":
    idx.commonNameIdx = i
    case "Confidence":
    idx.confidenceIdx = i
    case "File":
    idx.fileIdx = i
    [4.207257]
    [4.207475]
    if ptr, ok := columnMap[col]; ok {
    *ptr = i
  • edit in tools/calls/calls_from_birda.go at line 134
    [4.207482]
    [4.207482]
    return idx
    }
  • edit in tools/calls/calls_from_birda.go at line 137
    [4.207483]
    [4.207483]
    // validateBirdaColumns checks that required columns were found.
    func validateBirdaColumns(idx birdaColumnIndices) error {
  • replacement in tools/calls/calls_from_birda.go at line 140
    [4.207582][4.207582:207668]()
    return birdaColumnIndices{}, fmt.Errorf("missing required columns in BirdNET file")
    [4.207582]
    [4.207668]
    return fmt.Errorf("missing required columns in BirdNET file")
  • replacement in tools/calls/calls_from_birda.go at line 142
    [4.207671][4.207671:207688]()
    return idx, nil
    [4.207671]
    [4.207688]
    return nil