F6A736FQ4WKBW4G6IYCZTTGN43HKZ2JHIOHX2BW3TPHATCTERAGQC SPHUX2CTF2S2TXEO3TRHNY7NIJ42G5KEWZRHQPZWXP77SJS5WJUAC ILFBB4ISBFKXETYLV7QEO4SKOMPHSWRLKN3MVHEGFOPQ5RQJWDDQC RFSUR7ZEXTQNHH3IFJAL2NNOTGRPWOWB3PFIVH7VLI2JPTIBMW5AC EBCNGTNVY2YFFHKC4PHDEHNBOWJ4JDCEXTTJ3WKD5VWQZLLDZ65AC 2C4FPBSQTF4FM4J45HZGWB6L3E56U7M26TLYIRUMXC6SULR6XSQAC 7CC2YVZXAIUNWXNNVIO5KOZZFDQQLESFO72SGEDP2C4OZXAWO4KQC XM6ASUARNGH74IE3JQ3PAYWAKQDYBUDRWRWHQCYYGBBWO2FZ5Q3QC XSITZX775O4AB2YYSBF7XOOYFUCB6IS25HG4NIO2PQLKFHTTJLZAC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC wavBase := nameWithoutSuffix + ".WAV"wavPath := filepath.Join(filepath.Dir(ravenFile), wavBase)
// Find WAV file with correct casewavPath := findWAVFile(filepath.Dir(ravenFile), nameWithoutSuffix)if wavPath == "" {return nil, false, true, nil // WAV not found, skip}
// findWAVFile finds a WAV file in the directory with case-insensitive matching.// baseName is the filename without extension (e.g., "20230610_150000").// Returns the full path with correct case, or empty string if not found.func findWAVFile(dir, baseName string) string {// Try common extensions in order of preferenceextensions := []string{".WAV", ".wav", ".Wav"}for _, ext := range extensions {path := filepath.Join(dir, baseName+ext)if _, err := os.Stat(path); err == nil {return path}}// Try case-insensitive glob as fallback (e.g., .Wav, .wAv, etc.)pattern := filepath.Join(dir, baseName+".[wW][aA][vV]")matches, _ := filepath.Glob(pattern)if len(matches) > 0 {return matches[0] // Return first match}return "" // Not found}
wavPath := filepath.Join(csvDir, filename)
// Find WAV file with correct casebaseName := strings.TrimSuffix(filename, filepath.Ext(filename))wavPath := findWAVFile(csvDir, baseName)if wavPath == "" {dataFilesSkipped++processed++if progress != nil {progress(processed, total, "")}continue}
wavPath := filepath.Join(csvDir, job.filename)
// Find WAV file with correct casebaseName := strings.TrimSuffix(job.filename, filepath.Ext(job.filename))wavPath := findWAVFile(csvDir, baseName)if wavPath == "" {results <- dotDataResult{filename: job.filename, written: false, err: nil}continue}
} else {// Derive from BirdNET filenamebase := filepath.Base(birdaFile)wavBase := strings.TrimSuffix(base, ".BirdNET.results.csv") + ".WAV"wavPath = filepath.Join(filepath.Dir(birdaFile), wavBase)
}// If not found from File column, search with correct caseif wavPath == "" {wavPath = findWAVFile(dir, baseName)}if wavPath == "" {return nil, false, true, nil // WAV not found, skip
c=comcha Chaffinch Common Chaffinchd=dunnoc1 Dunnock_Hedge_Sparrow Dunnockb=eurbla Eurasian Blackbirdg=gryger1 Warbler_Grey Gray Gerygonek=kea1 Kea Keal=lotkoe1 Long-tailed Koel Long-tailed Koelp=malpar2 Kakariki Malherbe's Parakeet (Orange-fronted Parakeet)m=morepo2 Morepork Moreporknezbel1 Bellbird New Zealand Bellbirdnezfan1 Fantail New Zealand Fantailnezkak1 Kaka New Zealand Kakanezpig2 New Zealand Pigeonnezrob3 Robin_Sth_Is South Island Robinpipipi1 Pipipi Brown Creeperriflem1 Rifleman Riflemansaddle3 Saddleback_Sth_Is South Island Saddlebackshbcuc1 Cuckoo_Shining Shining Bronze-Cuckoosilver3 Silvereye Silvereyesobkiw2 Kiwi_Tokoeka_Haast Southern Brown Kiwi (South I.)soioys1 South Island Oystercatchersonthr1 Thrush_Song Song Thrushtomtit1 Tomtit Tomtittui1 Tui Tuivaroys1 Oystercatcher_Variable Variable Oystercatcheryellow2 Yellowhammer YellowhammerPLAN MODE: Great, now that we have done that, lets implemehnt 2 new calls commands:calls from-birda --folder (or just 1 file with --file) --delete (optional, to delete the birda files as we go)calls from-raven --folder (or just 1 file with --file) --delete (optional, to delete the raven file as we go)We need to append birda and raven files to existing .data files, or create a new .data if not present.find sample birda files (1 or 0, per WAV) at /media/david/SSD4/Twenty_Four_Seven/R620/2024-05-06/20230610_150000.BirdNET.results.csv (note .BirdNET.results.csv) FILTER=BirdNETfind sample Raven files (1 or 0, per WAV) at /media/david/SSD4/Twenty_Four_Seven/R620/2024-05-06/20230610_150000.Table.1.selections.txt (note Table.$x.selections.txt, could be 1, 2, 3 ..., usually just 1, look for *.selections.txt) FILTER=RavenNote that both types of csv files are similar to .data files, they refer to a WAV file.Behaviour must be the same as calls from-preds, no clobber, we do not allow birda or raven files to be imported more than 1 timeWe do not allow manual overide for --filter, we always parse from the filename.filter BirdNET from *.BirdNET.results.csvfilter Raven from *.selections.txtboth new commands should output a json summary of calls parsed in the sae form as the output from calls from-preds cmdPLAN MODEskraak calls from-birda --folder ./recordingsskraak calls from-birda --file recording.BirdNET.results.csvskraak calls from-birda --folder ./recordings --deleteskraak calls from-raven --folder ./recordingsskraak calls from-raven --file recording.Table.1.selections.txtskraak calls from-raven --folder ./recordings --deleteShould birda, raven, preds tool write WAV or wav in appropriate cirumstancescomment in tui; editablebookmark feature ctrl-d
## [2026-03-09] Case-Preserving WAV File Finding**Fix:** WAV files with lowercase `.wav` extension now produce correct `.wav.data` files.**Changes:**- `tools/calls_from_preds.go` — Added `findWAVFile()` helper function- `tools/calls_from_birda.go` — Updated to use `findWAVFile()`- `tools/calls_from_raven.go` — Updated to use `findWAVFile()`**Problem:** Previous code hardcoded `.WAV` extension, causing issues on case-sensitive filesystems:- `abc.wav` would fail to be found- Or produce `abc.WAV.data` instead of `abc.wav.data`
**Solution:** `findWAVFile(dir, baseName)` searches for:1. `.WAV` (most common for main recordings)2. `.wav` (common for clips)3. `.Wav` (edge case)4. Case-insensitive glob fallback**Result:**| WAV File | .data File ||----------|------------|| `abc.WAV` | `abc.WAV.data` || `abc.wav` | `abc.wav.data` || `abc.Wav` | `abc.Wav.data` |