RUVJ3V4N5V4Z3HSH2YYESKQF5G7RIHBFB5TLV2IPDWXSGJDRD54AC 2HAQZPV377VV26SMPSXSZR6CL7SS2GTNPR5COIAPN47NLJILRQGAC VYNOHQJWFL6ZEKFLASJAZZIHN4S3NJJECKANT7JNSXPJN3KC2DJAC HYCZTLSZ5WVJFMP4EPVHAVWRYSYNCIBJ62LLBNO4IRVEBY7WJI6QC GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC LQLC7S3ADBR4O2JYVUSQJD65U3HG4ADOQBGB4F7KQCXUMNKMNEKAC LBWQJEDHCNUNMEJWXILGBGYZUKQI7CDAMH2BD44HULM77SVH5UYQC BZ6KQRYDMP4PWYJRL62XXIUXLTBKEASIKSAJIQPZS6DKDSYKA76QC // 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) {
// parseWAVInfo opens a WAV file, validates its header, and parses chunks.// Returns the parsed chunk info and the open file (caller must close).func parseWAVInfo(filepath string) (*os.File, wavChunkInfo, error) {
startOffset, readSize := calcWAVReadRange(startSec, endSec, info)
return file, info, nil}// readAudioSegment reads audio bytes from an already-parsed WAV file.func readAudioSegment(file *os.File, info wavChunkInfo, startOffset, readSize int64) ([]byte, error) {
// 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, info, err := parseWAVInfo(filepath)if err != nil {return nil, 0, err}defer func() { _ = file.Close() }()startOffset, readSize := calcWAVReadRange(startSec, endSec, info)audioData, err := readAudioSegment(file, info, startOffset, readSize)if err != nil {return nil, 0, err}if readSize == 0 {return []float64{}, info.sampleRate, nil}
// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {
// resizeScale holds precomputed scale factors for nearest-neighbor resizing.type resizeScale struct {srcWidth, srcHeight intscaleX, scaleY float64}func newResizeScale(img image.Image, newWidth, newHeight int) resizeScale {
scaleX := float64(srcWidth) / float64(newWidth)scaleY := float64(srcHeight) / float64(newHeight)
// srcCoord maps a destination pixel coordinate to source coordinate, clamped to bounds.func (s resizeScale) srcCoord(dstX, dstY int) (srcX, srcY int) {srcX = int(float64(dstX) * s.scaleX)srcY = int(float64(dstY) * s.scaleY)if srcX >= s.srcWidth {srcX = s.srcWidth - 1}if srcY >= s.srcHeight {srcY = s.srcHeight - 1}return}
if srcGray, ok := img.(*image.Gray); ok {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {srcY := int(float64(y) * scaleY)if srcY >= srcHeight {srcY = srcHeight - 1}dstOff := y * result.StridesrcRowOff := srcY * srcGray.Stridefor x := range newWidth {srcX := int(float64(x) * scaleX)if srcX >= srcWidth {srcX = srcWidth - 1}result.Pix[dstOff+x] = srcGray.Pix[srcRowOff+srcX]}
// resizeGray resizes a Gray image using nearest-neighbor interpolation.func resizeGray(src *image.Gray, s resizeScale, newWidth, newHeight int) *image.Gray {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {dstOff := y * result.Stride_, srcY := s.srcCoord(0, y)srcRowOff := srcY * src.Stridefor x := range newWidth {srcX, _ := s.srcCoord(x, 0)result.Pix[dstOff+x] = src.Pix[srcRowOff+srcX]
if srcRGBA, ok := img.(*image.RGBA); ok {result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {srcY := int(float64(y) * scaleY)if srcY >= srcHeight {srcY = srcHeight - 1}dstOff := y * result.StridesrcRowOff := srcY * srcRGBA.Stridefor x := range newWidth {srcX := int(float64(x) * scaleX)if srcX >= srcWidth {srcX = srcWidth - 1}si := srcRowOff + srcX*4di := dstOff + x*4result.Pix[di] = srcRGBA.Pix[si]result.Pix[di+1] = srcRGBA.Pix[si+1]result.Pix[di+2] = srcRGBA.Pix[si+2]result.Pix[di+3] = srcRGBA.Pix[si+3]}
// resizeRGBA resizes an RGBA image using nearest-neighbor interpolation.func resizeRGBA(src *image.RGBA, s resizeScale, newWidth, newHeight int) *image.RGBA {result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {dstOff := y * result.Stride_, srcY := s.srcCoord(0, y)srcRowOff := srcY * src.Stridefor x := range newWidth {srcX, _ := s.srcCoord(x, 0)si := srcRowOff + srcX*4di := dstOff + x*4result.Pix[di] = src.Pix[si]result.Pix[di+1] = src.Pix[si+1]result.Pix[di+2] = src.Pix[si+2]result.Pix[di+3] = src.Pix[si+3]
}// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {s := newResizeScale(img, newWidth, newHeight)if srcGray, ok := img.(*image.Gray); ok {return resizeGray(srcGray, s, newWidth, newHeight)}if srcRGBA, ok := img.(*image.RGBA); ok {return resizeRGBA(srcRGBA, s, newWidth, newHeight)}return resizeGeneric(img, s, newWidth, newHeight)
// wavInfo holds WAV metadata and hash for a single file during batch processingtype wavInfo struct {path stringmetadata *WAVMetadatahash stringerr error}// parseFilenameTimestampsBatch parses filename timestamps and applies timezone offsets.// Returns a map from wavInfos index to adjusted timestamp, and any errors.func parseFilenameTimestampsBatch(wavInfos []wavInfo,filenameIndices []int,filenames []string,timezoneID string,) (map[int]time.Time, []FileImportError) {var errors []FileImportErrorresult := make(map[int]time.Time)filenameTimestamps, err := ParseFilenameTimestamps(filenames)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("filename timestamp parsing failed: %v", err),Stage: "parse",})}return result, errors}
adjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, timezoneID)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("timezone offset failed: %v", err),Stage: "parse",})}return result, errors}for j, idx := range filenameIndices {result[idx] = adjustedTimestamps[j]}return result, errors}// resolveFileData resolves timestamp and calculates astronomical data for a single WAV file.func resolveFileData(info wavInfo, preParsedTime *time.Time, location *LocationData) (*fileData, error) {tsResult, err := ResolveTimestamp(info.metadata, info.path, location.TimezoneID, true, preParsedTime)if err != nil {return nil, err}astroData := CalculateAstronomicalData(tsResult.Timestamp.UTC(),info.metadata.Duration,location.Latitude,location.Longitude,)return &fileData{FileName: filepath.Base(info.path),Hash: info.hash,Duration: info.metadata.Duration,SampleRate: info.metadata.SampleRate,TimestampLocal: tsResult.Timestamp,IsAudioMoth: tsResult.IsAudioMoth,MothData: tsResult.MothData,AstroData: astroData,}, nil}
filenameTimestamps, err := ParseFilenameTimestamps(filenamesForParsing)if err != nil {// If batch parsing fails, record error for all filesfor _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("filename timestamp parsing failed: %v", err),Stage: "parse",})}} else {// Apply timezone offsetadjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, location.TimezoneID)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("timezone offset failed: %v", err),Stage: "parse",})}} else {// Build map from file index to timestampfor j, idx := range filenameIndices {filenameTimestampMap[idx] = adjustedTimestamps[j]}}}
tsMap, tsErrors := parseFilenameTimestampsBatch(wavInfos, filenameIndices, filenamesForParsing, location.TimezoneID)errors = append(errors, tsErrors...)filenameTimestampMap = tsMap
// Calculate astronomical dataastroData := CalculateAstronomicalData(tsResult.Timestamp.UTC(),info.metadata.Duration,location.Latitude,location.Longitude,)// Add to resultsfilesData = append(filesData, &fileData{FileName: filepath.Base(info.path),Hash: info.hash,Duration: info.metadata.Duration,SampleRate: info.metadata.SampleRate,TimestampLocal: tsResult.Timestamp,IsAudioMoth: tsResult.IsAudioMoth,MothData: tsResult.MothData,AstroData: astroData,})
filesData = append(filesData, fd)
// insertClusterFiles inserts all file data into database in a single transactionfunc insertClusterFiles(database *sql.DB,filesData []*fileData,
// insertSingleFile inserts one file's data into the database within an existing transaction.// Returns (imported=true, nil) on success, (imported=false, nil) if skipped, or (false, error) on failure.func insertSingleFile(ctx context.Context,tx *db.LoggedTx,fd *fileData,fileStmt, datasetStmt, mothStmt *db.LoggedStmt,
) (imported, skipped int, errors []FileImportError, err error) {// Begin logged transactionctx := context.Background()tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")
) (bool, error) {// Check for duplicate hash_, isDuplicate, err := CheckDuplicateHash(tx, fd.Hash)if err != nil {return false, fmt.Errorf("duplicate check failed: %v", err)}if isDuplicate {return false, nil // skipped}// Generate file IDfileID, err := GenerateLongID()if err != nil {return false, fmt.Errorf("ID generation failed: %v", err)}// Insert file record_, err = fileStmt.ExecContext(ctx,fileID, fd.FileName, fd.Hash, locationID,fd.TimestampLocal, clusterID, fd.Duration, fd.SampleRate,fd.AstroData.SolarNight, fd.AstroData.CivilNight, fd.AstroData.MoonPhase,)if err != nil {return false, fmt.Errorf("file insert failed: %v", err)}// Insert file_dataset junction (ALWAYS)_, err = datasetStmt.ExecContext(ctx, fileID, datasetID)
defer tx.Rollback() // Rollback if not committed
// If AudioMoth, insert moth_metadataif fd.IsAudioMoth && fd.MothData != nil {_, err = mothStmt.ExecContext(ctx,fileID,fd.MothData.Timestamp,&fd.MothData.RecorderID,&fd.MothData.Gain,&fd.MothData.BatteryV,&fd.MothData.TempC,)if err != nil {return false, fmt.Errorf("moth_metadata insert failed: %v", err)}}return true, nil}// clusterStmts holds prepared statements for cluster file insertion.type clusterStmts struct {fileStmt *db.LoggedStmtdatasetStmt *db.LoggedStmtmothStmt *db.LoggedStmt}
// Insert each filefor _, fd := range filesData {// Check for duplicate hash_, isDuplicate, err := CheckDuplicateHash(tx, fd.Hash)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("duplicate check failed: %v", err),Stage: "insert",})continue}
return &clusterStmts{fileStmt: fileStmt, datasetStmt: datasetStmt, mothStmt: mothStmt}, nil}
// Generate file IDfileID, err := GenerateLongID()if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("ID generation failed: %v", err),Stage: "insert",})continue}
// insertClusterFiles inserts all file data into database in a single transactionfunc insertClusterFiles(database *sql.DB,filesData []*fileData,datasetID, clusterID, locationID string,) (imported, skipped int, errors []FileImportError, err error) {ctx := context.Background()tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")if err != nil {return 0, 0, nil, fmt.Errorf("failed to begin transaction: %w", err)}defer tx.Rollback()
// Insert file record_, err = fileStmt.ExecContext(ctx,fileID, fd.FileName, fd.Hash, locationID,fd.TimestampLocal, clusterID, fd.Duration, fd.SampleRate,fd.AstroData.SolarNight, fd.AstroData.CivilNight, fd.AstroData.MoonPhase,)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("file insert failed: %v", err),Stage: "insert",})continue}
stmts, err := prepareClusterStmts(ctx, tx)if err != nil {return 0, 0, nil, err}defer stmts.Close()
// Insert file_dataset junction (ALWAYS)_, err = datasetStmt.ExecContext(ctx, fileID, datasetID)if err != nil {
for _, fd := range filesData {wasImported, insertErr := insertSingleFile(ctx, tx, fd, stmts.fileStmt, stmts.datasetStmt, stmts.mothStmt, datasetID, clusterID, locationID)if insertErr != nil {
// If AudioMoth, insert moth_metadataif fd.IsAudioMoth && fd.MothData != nil {_, err = mothStmt.ExecContext(ctx,fileID,fd.MothData.Timestamp,&fd.MothData.RecorderID,&fd.MothData.Gain,&fd.MothData.BatteryV,&fd.MothData.TempC,)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("moth_metadata insert failed: %v", err),Stage: "insert",})continue}
if wasImported {imported++} else {skipped++
t.Run("should return valid types for all fields", func(t *testing.T) {// Winter midnight in Auckland (should be solar night)winterMidnight := parseTime(t, "2024-06-15T12:00:00Z") // UTC midnight = noon in Auckland (winter)duration := 60.0 // 1 minute
tests := []struct {name stringtimestamp stringduration float64lat, lon float64wantNoSNight bool // if true, assert SolarNight=falsewantNoCNight bool // if true, assert CivilNight=false}{{name: "valid moon phase range", timestamp: "2024-06-15T12:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "no solar night during daytime", timestamp: "2024-12-15T00:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon, wantNoSNight: true, wantNoCNight: true},{name: "short duration", timestamp: "2024-06-15T10:00:00Z", duration: 30.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "long duration", timestamp: "2024-06-15T10:00:00Z", duration: 3600.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "midpoint calculation", timestamp: "2024-06-15T10:00:00Z", duration: 7200.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "different location", timestamp: "2024-06-15T12:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "very short duration", timestamp: "2024-06-15T12:00:00Z", duration: 0.1, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "very long duration", timestamp: "2024-06-15T12:00:00Z", duration: 86400.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},}
result := CalculateAstronomicalData(winterMidnight, duration, testLocationAuckland.lat, testLocationAuckland.lon)
for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {ts := parseTime(t, tt.timestamp)result := CalculateAstronomicalData(ts, tt.duration, tt.lat, tt.lon)
// Check types existif result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should return false for solar night during daytime hours", func(t *testing.T) {// Summer midday in Auckland (should NOT be solar night)summerMidday := parseTime(t, "2024-12-15T00:00:00Z") // UTC midnight = noon in Auckland (summer)duration := 60.0 // 1 minuteresult := CalculateAstronomicalData(summerMidday, duration, testLocationAuckland.lat, testLocationAuckland.lon)// During summer midday, should NOT be solar nightif result.SolarNight {t.Error("Expected SolarNight to be false during daytime")}if result.CivilNight {t.Error("Expected CivilNight to be false during daytime")}})t.Run("should handle different durations correctly", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T10:00:00Z")shortDuration := 30.0 // 30 secondslongDuration := 3600.0 // 1 hourshortResult := CalculateAstronomicalData(timestamp, shortDuration, testLocationAuckland.lat, testLocationAuckland.lon)longResult := CalculateAstronomicalData(timestamp, longDuration, testLocationAuckland.lat, testLocationAuckland.lon)// Both should have valid resultsif shortResult.MoonPhase < 0 || shortResult.MoonPhase > 1 {t.Errorf("Short duration moon phase out of range: %f", shortResult.MoonPhase)}if longResult.MoonPhase < 0 || longResult.MoonPhase > 1 {t.Errorf("Long duration moon phase out of range: %f", longResult.MoonPhase)}})t.Run("should calculate midpoint time correctly", func(t *testing.T) {// Test that the calculation uses the midpoint, not the start timestartTime := parseTime(t, "2024-06-15T10:00:00Z")duration := 7200.0 // 2 hours (midpoint would be 1 hour later)result := CalculateAstronomicalData(startTime, duration, testLocationAuckland.lat, testLocationAuckland.lon)// Should calculate based on 11:00 UTC, not 10:00 UTC// Just verify we get valid boolean results_ = result.SolarNight_ = result.CivilNight})t.Run("should handle different geographical locations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z") // UTC noonduration := 60.0aucklandResult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)londonResult := CalculateAstronomicalData(timestamp, duration, testLocationLondon.lat, testLocationLondon.lon)// Both should have valid boolean results (don't compare values, just that they're boolean)_ = aucklandResult.SolarNight_ = londonResult.SolarNight// Results might differ due to different timezones and seasons// Auckland: UTC noon = midnight local (winter) = likely night// London: UTC noon = 1pm local (summer) = likely day})t.Run("should return valid moon phase values", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 60.0result := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should handle edge cases with very short durations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 0.1 // 0.1 secondsresult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should handle edge cases with very long durations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 86400.0 // 24 hoursresult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})
if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}if tt.wantNoSNight && result.SolarNight {t.Error("Expected SolarNight to be false")}if tt.wantNoCNight && result.CivilNight {t.Error("Expected CivilNight to be false")}})}
// segmentValidation holds the results of pre-import validation (phases B+C).type segmentValidation struct {scannedFiles []scannedDataFilefilterIDMap map[string]stringspeciesIDMap map[string]stringcalltypeIDMap map[string]map[string]stringfileIDMap map[string]scannedDataFile}// validateAndPrepareSegments performs phases B+C: parse data files, validate DB state, and prepare ID maps.func validateAndPrepareSegments(database *sql.DB,input ImportSegmentsInput,mapping utils.MappingFile,dataFiles []string,) (*segmentValidation, []ImportSegmentError, error) {// Phase B: Parse all .data files and collect unique valuesscannedFiles, parseErrors, uniqueFilters, uniqueSpecies, uniqueCalltypes := scanAllDataFiles(dataFiles, input.Folder)if len(scannedFiles) == 0 {return nil, parseErrors, nil}// Validate dataset/location/cluster hierarchyif err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {return nil, parseErrors, err}// Validate all filters existfilterIDMap, err := validateFiltersExist(database, uniqueFilters)if err != nil {return nil, parseErrors, fmt.Errorf("filter validation failed: %w", err)}// Validate mapping covers all species/calltypes and they exist in DBvalidationResult, err := utils.ValidateMappingAgainstDB(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return nil, parseErrors, fmt.Errorf("mapping validation failed: %w", err)}if validationResult.HasErrors() {return nil, parseErrors, fmt.Errorf("mapping validation failed: %s", validationResult.Error())}// Load species and calltype ID mapsspeciesIDMap, calltypeIDMap, err := loadSpeciesCalltypeIDs(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return nil, parseErrors, fmt.Errorf("failed to load species/calltype IDs: %w", err)}// Validate files: hash exists, linked to dataset, no existing labelsfileIDMap, hashErrors := validateAndMapFiles(database, scannedFiles, input.ClusterID, input.DatasetID)allErrors := append(parseErrors, hashErrors...)return &segmentValidation{scannedFiles: scannedFiles,filterIDMap: filterIDMap,speciesIDMap: speciesIDMap,calltypeIDMap: calltypeIDMap,fileIDMap: fileIDMap,}, allErrors, nil}
}// Phase B: Parse all .data files and collect unique valuesscannedFiles, parseErrors, uniqueFilters, uniqueSpecies, uniqueCalltypes := scanAllDataFiles(dataFiles, input.Folder)output.Errors = append(output.Errors, parseErrors...)if len(scannedFiles) == 0 {output.Summary.ProcessingTimeMs = time.Since(startTime).Milliseconds()return output, nil
// Validate dataset/location/cluster hierarchyif err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {return output, err}// Validate all filters existfilterIDMap, err := validateFiltersExist(database, uniqueFilters)if err != nil {return output, fmt.Errorf("filter validation failed: %w", err)}// Validate mapping covers all species/calltypes and they exist in DBvalidationResult, err := utils.ValidateMappingAgainstDB(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return output, fmt.Errorf("mapping validation failed: %w", err)}if validationResult.HasErrors() {return output, fmt.Errorf("mapping validation failed: %s", validationResult.Error())}
// Load species and calltype ID mapsspeciesIDMap, calltypeIDMap, err := loadSpeciesCalltypeIDs(database, mapping, uniqueSpecies, uniqueCalltypes)
val, valErrors, err := validateAndPrepareSegments(database, input, mapping, dataFiles)output.Errors = append(output.Errors, valErrors...)
// Validate files: hash exists, linked to dataset, no existing labelsfileIDMap, hashErrors := validateAndMapFiles(database, scannedFiles, input.ClusterID, input.DatasetID)output.Errors = append(output.Errors, hashErrors...)if len(fileIDMap) == 0 && len(scannedFiles) > 0 {
if val == nil || len(val.fileIDMap) == 0 {
ctx, database, fileIDMap, scannedFiles, mapping, filterIDMap, speciesIDMap, calltypeIDMap, input.DatasetID, input.ProgressHandler,
ctx, database, val.fileIDMap, val.scannedFiles, mapping, val.filterIDMap, val.speciesIDMap, val.calltypeIDMap, input.DatasetID, input.ProgressHandler,