LHZQOX64RAXIJIHFJ5RN5TFJ4VFRRM42W2ZT7MRCNQ2AJVA4BSSAC V2HX6HEB2OBNI4IMWD5XJN3RKAZYHAFJAJAPFP3BFYFZVZVEYN6AC JMDW37LVYJAWULK4RREBRC6OLNVMA5PO5TLN6JIZCTZELL7YF4MQC NQPVZ3PPQG6EPTTAEHXOXXGK27HZCISHZCOZU6K6RKWTRTOHMY6QC 3DVPQOKB6BX63XSBIYYCPWBL2RBG3LXZS3XPQBANJP2FWVRAOVZQC PXQDGTR53ST5T4EV6XFRCAOC7N5RQX23GWVKMJGS2J35VUQLZL4AC O45G7VX2XBX2JSKKVK42BLU4UG5K77WPZOD2IYLKK5MOTSPXFFMAC // Create temp filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, filename)hash = createTestWAV(t, wavPath)// Generate file IDfileID, 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}
}// 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 intif 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)}
// 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 intif 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 intif 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}
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-levelentries, err := os.ReadDir(folderPath)if err != nil {errors = append(errors, FileImportError{FileName: folderPath,Error: err.Error(),Stage: StageScan,})return nil, errors}
// scanWavFilesFlat scans only the top-level directory.func scanWavFilesFlat(folderPath string) ([]string, []FileImportError) {var files []stringvar errors []FileImportErrorentries, err := os.ReadDir(folderPath)if err != nil {errors = append(errors, FileImportError{FileName: folderPath,Error: err.Error(),Stage: StageScan,})return nil, errors}
for _, entry := range entries {if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".wav") {files = append(files, filepath.Join(folderPath, entry.Name()))}
for _, entry := range entries {if !entry.IsDir() && isWavFile(entry.Name()) {files = append(files, filepath.Join(folderPath, entry.Name()))
// importSingleLabel inserts a single label and its metadata/subtype into the DB.func importSingleLabel(ctx context.Context,tx *db.LoggedTx,
// resolveLabelIDs looks up species and filter IDs, generates a label ID.// Returns an error if any lookup fails.func resolveLabelIDs(
return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: StageImport,Message: fmt.Sprintf("species not found in mapping: %s", label.Species),}, hasError: true}
return resolvedLabelIDs{}, fmt.Errorf("species not found in mapping: %s", label.Species)
return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: StageImport,Message: fmt.Sprintf("species ID not found: %s", dbSpecies),}, hasError: true}
return resolvedLabelIDs{}, fmt.Errorf("species ID not found: %s", dbSpecies)
return importLabelResult{err: ImportSegmentError{File: filepath.Base(sf.DataPath), Stage: StageImport,Message: fmt.Sprintf("filter ID not found: %s", label.Filter),}, hasError: true}
return resolvedLabelIDs{}, fmt.Errorf("filter ID not found: %s", label.Filter)
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 firstids, err := resolveLabelIDs(label, sf, mapping, filterIDMap, speciesIDMap)
_, 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 {
// Insert the labelif err := insertLabel(ctx, tx, ids, segmentID, label); err != nil {
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 {
if err := insertLabelMetadata(ctx, tx, ids.labelID, label.Comment); err != nil {
if err := importCalltype(ctx, tx, labelID, label, dbSpecies, filterID, mapping, calltypeIDMap, sf); err != nil {return importLabelResult{err: *err, hasError: true}
if ctErr := importCalltype(ctx, tx, ids.labelID, label, ids.dbSpecies, ids.filterID, mapping, calltypeIDMap, sf); ctErr != nil {return importLabelResult{err: *ctErr, hasError: true}
return importLabelResult{labelImport: labelImport, labelID: labelID}
// 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
// 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}
// Validate file existsif _, err := os.Stat(input.DataFilePath); os.IsNotExist(err) {output.Error = fmt.Sprintf("File not found: %s", input.DataFilePath)
// Validate input and get data filedataFile, wavPath, err := validateAndParseShowImagesInput(input)if err != nil {output.Error = err.Error()return output, err}output.WavFile = wavPathif len(dataFile.Segments) == 0 {output.Error = "No segments found in .data file"
// Derive WAV file path (strip .data suffix)wavPath := strings.TrimSuffix(input.DataFilePath, ".data")output.WavFile = wavPath
// Resolve image size and protocolimgSize := resolveImageSize(input.ImageSize)protocol := resolveGraphicsProtocol(input)
// Check WAV file exists
// Generate and display spectrogramssegmentsShown, err := displaySegmentSpectrograms(input.DataFilePath, dataFile.Segments, input.Color, imgSize, protocol)if err != nil {output.Error = err.Error()return output, err}output.SegmentsShown = segmentsShownreturn 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")
// Resolve image sizeimgSize := input.ImageSizeif imgSize == 0 {imgSize = spectrogram.SpectrogramDisplaySize
// resolveImageSize returns the image size, using default if zero.func resolveImageSize(size int) int {if size == 0 {return spectrogram.SpectrogramDisplaySize
// Generate spectrogram for each segment and outputfor i, seg := range dataFile.Segments {// Generate spectrogram imageimg, err := spectrogram.GenerateSegmentSpectrogram(input.DataFilePath, seg.StartTime, seg.EndTime, input.Color, imgSize)
// 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 := 0for i, seg := range segments {img, err := spectrogram.GenerateSegmentSpectrogram(dataFilePath, seg.StartTime, seg.EndTime, color, imgSize)
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 {
if callType == "" && len(matches) > 1 {var callTypes []stringfor _, l := range matches {ct := l.CallTypeif ct == "" {ct = "(none)"}callTypes = append(callTypes, ct)
// formatAmbiguousCalltypeError creates an error message for ambiguous calltype matches.func formatAmbiguousCalltypeError(species, filter string, matches []*datafile.Label) string {var callTypes []stringfor _, l := range matches {ct := l.CallTypeif ct == "" {ct = "(none)"
// If segment has no labels left, remove itif len(targetSegment.Labels) == 0 {removeSegmentFromDataFile(dataFile, targetSegment)output.Removed = "segment"
// Handle the result of label removaloutput.Removed = handleRemovalResult(dataFile, targetSegment, input.File)
// If no segments left, remove the .data fileif 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"
// Handle file removal caseif output.Removed == "file" {return output, nil
}// 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 fileos.Remove(filePath)return "file"