package tools
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"skraak/utils"
)
type CallsFromRavenInput struct {
Folder string `json:"folder"`
File string `json:"file"`
Delete bool `json:"delete"`
ProgressHandler ProgressHandler `json:"-"` }
type CallsFromRavenOutput struct {
Calls []ClusteredCall `json:"calls"`
TotalCalls int `json:"total_calls"`
SpeciesCount map[string]int `json:"species_count"`
DataFilesWritten int `json:"data_files_written"`
DataFilesSkipped int `json:"data_files_skipped"`
FilesProcessed int `json:"files_processed"`
FilesDeleted int `json:"files_deleted"`
Filter string `json:"filter"`
Error *string `json:"error,omitempty"`
}
type ravenSource struct{}
func (ravenSource) Name() string { return "Raven" }
func (ravenSource) FindFiles(folder string) ([]string, error) {
var files []string
entries, err := os.ReadDir(folder)
if err != nil {
return nil, err
}
for _, entry := range entries {
name := entry.Name()
if strings.HasSuffix(name, ".selections.txt") {
files = append(files, filepath.Join(folder, name))
}
}
return files, nil
}
func (ravenSource) ProcessFile(ravenFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {
return processRavenFileCached(ravenFile, cache)
}
func CallsFromRaven(input CallsFromRavenInput) (CallsFromRavenOutput, error) {
src := ravenSource{}
commonInput := CallsFromSourceInput(input)
commonOutput, err := callsFromSource(src, commonInput)
var output CallsFromRavenOutput
output.Calls = commonOutput.Calls
output.TotalCalls = commonOutput.TotalCalls
output.SpeciesCount = commonOutput.SpeciesCount
output.DataFilesWritten = commonOutput.DataFilesWritten
output.DataFilesSkipped = commonOutput.DataFilesSkipped
output.FilesProcessed = commonOutput.FilesProcessed
output.FilesDeleted = commonOutput.FilesDeleted
output.Filter = commonOutput.Filter
output.Error = commonOutput.Error
return output, err
}
type RavenSelection struct {
StartTime float64
EndTime float64
FreqLow float64
FreqHigh float64
Species string
}
type ravenColumnIndices struct {
beginTimeIdx int
endTimeIdx int
lowFreqIdx int
highFreqIdx int
speciesIdx int
}
func parseRavenHeader(header []string) (ravenColumnIndices, error) {
idx := ravenColumnIndices{beginTimeIdx: -1, endTimeIdx: -1, lowFreqIdx: -1, highFreqIdx: -1, speciesIdx: -1}
for i, col := range header {
switch col {
case "Begin Time (s)":
idx.beginTimeIdx = i
case "End Time (s)":
idx.endTimeIdx = i
case "Low Freq (Hz)":
idx.lowFreqIdx = i
case "High Freq (Hz)":
idx.highFreqIdx = i
case "Species":
idx.speciesIdx = i
}
}
if idx.beginTimeIdx == -1 || idx.endTimeIdx == -1 || idx.speciesIdx == -1 {
return idx, fmt.Errorf("missing required columns in Raven file")
}
return idx, nil
}
func parseRavenSelections(scanner *bufio.Scanner, idx ravenColumnIndices) ([]RavenSelection, error) {
var selections []RavenSelection
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
fields := strings.Split(line, "\t")
if len(fields) <= idx.speciesIdx {
continue
}
sel, err := parseRavenRow(fields, idx)
if err != nil {
return nil, err
}
selections = append(selections, sel)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
return selections, nil
}
func parseRavenRow(fields []string, idx ravenColumnIndices) (RavenSelection, error) {
var sel RavenSelection
startTime, err := strconv.ParseFloat(fields[idx.beginTimeIdx], 64)
if err != nil {
return sel, fmt.Errorf("failed to parse begin time %q: %w", fields[idx.beginTimeIdx], err)
}
sel.StartTime = startTime
endTime, err := strconv.ParseFloat(fields[idx.endTimeIdx], 64)
if err != nil {
return sel, fmt.Errorf("failed to parse end time %q: %w", fields[idx.endTimeIdx], err)
}
sel.EndTime = endTime
if idx.lowFreqIdx >= 0 && idx.lowFreqIdx < len(fields) {
freqLow, err := strconv.ParseFloat(fields[idx.lowFreqIdx], 64)
if err != nil {
return sel, fmt.Errorf("failed to parse low freq %q: %w", fields[idx.lowFreqIdx], err)
}
sel.FreqLow = freqLow
}
if idx.highFreqIdx >= 0 && idx.highFreqIdx < len(fields) {
freqHigh, err := strconv.ParseFloat(fields[idx.highFreqIdx], 64)
if err != nil {
return sel, fmt.Errorf("failed to parse high freq %q: %w", fields[idx.highFreqIdx], err)
}
sel.FreqHigh = freqHigh
}
sel.Species = fields[idx.speciesIdx]
return sel, nil
}
func deriveWAVBaseName(ravenFile string) string {
base := filepath.Base(ravenFile)
nameWithoutSuffix := strings.TrimSuffix(base, ".selections.txt")
idx := strings.Index(nameWithoutSuffix, ".Table.")
if idx > 0 {
nameWithoutSuffix = nameWithoutSuffix[:idx]
}
return nameWithoutSuffix
}
func processRavenFileCached(ravenFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {
file, err := os.Open(ravenFile)
if err != nil {
return nil, false, false, fmt.Errorf("failed to open file: %w", err)
}
defer func() { _ = file.Close() }()
scanner := bufio.NewScanner(file)
if !scanner.Scan() {
return nil, false, false, fmt.Errorf("empty file")
}
header := strings.Split(scanner.Text(), "\t")
idx, err := parseRavenHeader(header)
if err != nil {
return nil, false, false, err
}
selections, err := parseRavenSelections(scanner, idx)
if err != nil {
return nil, false, false, err
}
if len(selections) == 0 {
return nil, false, true, nil
}
wavPath := resolveWAVPath(ravenFile, cache)
if wavPath == "" {
return nil, false, true, nil
}
sampleRate, duration, err := utils.ParseWAVHeaderMinimal(wavPath)
if err != nil {
return nil, false, true, nil
}
dataPath := wavPath + ".data"
segments := buildRavenSegments(selections, sampleRate)
meta := AviaNZMeta{Operator: "Raven", Duration: duration}
reviewer := "None"
meta.Reviewer = &reviewer
if err := writeDotDataFileSafe(dataPath, segments, "Raven", meta); err != nil {
return nil, false, false, err
}
var calls []ClusteredCall
for _, sel := range selections {
calls = append(calls, ClusteredCall{
File: wavPath,
StartTime: sel.StartTime,
EndTime: sel.EndTime,
EbirdCode: sel.Species,
Segments: 1,
})
}
return calls, true, false, nil
}
func resolveWAVPath(ravenFile string, cache *DirCache) string {
baseName := deriveWAVBaseName(ravenFile)
if cache != nil {
return cache.FindWAV(baseName)
}
return findWAVFile(filepath.Dir(ravenFile), baseName)
}
func buildRavenSegments(selections []RavenSelection, sampleRate int) []AviaNZSegment {
var segments []AviaNZSegment
for _, sel := range selections {
labels := []AviaNZLabel{
{
Species: sel.Species,
Certainty: 70, Filter: "Raven",
},
}
freqLow := sel.FreqLow
freqHigh := sel.FreqHigh
if freqLow == 0 && freqHigh == 0 {
freqHigh = float64(sampleRate)
}
segment := AviaNZSegment{
sel.StartTime,
sel.EndTime,
freqLow,
freqHigh,
labels,
}
segments = append(segments, segment)
}
return segments
}