claude going nuts all over the place

quietlight
May 19, 2026, 12:36 AM
V2HX6HEB2OBNI4IMWD5XJN3RKAZYHAFJAJAPFP3BFYFZVZVEYN6AC

Dependencies

  • [2] BGJRP6EH test
  • [3] 2MZO5RDB fifth phase of utils refactor, astro/
  • [4] TSPKDAFW updated .golangci.yml and CLAUDE.md
  • [5] XU7FTYK3 third phase of utils refactor, wav/
  • [6] 54GPBNIX added +_ for tui to select segments with no calltype
  • [7] 3DVPQOKB big tidy up of tools/
  • [8] QVIGQOQZ more work on utils/ with glm
  • [9] TSOJUMHV more tests
  • [10] VNFPBXF7 moved dep tests to golangci-lint
  • [11] M34GDDTW fill calls add, check duration
  • [12] 3ETJ6KPI refactor of tui/ second iteration
  • [13] JAT3DXOL cyclo over 15
  • [14] A6MCX2V6 emptied audio/ and moved files into testdata folders
  • [15] ZDZDASRT complexity over 12 now gone, but have some lint fails
  • [16] YVFPP5VJ refactor of tui/ first iteration
  • [17] P4CJMBYK added first version of --bandpass flag to calls classify, work to do
  • [18] JZRF7OBJ refactor to get db omports out of utils, but still have failing tests, may need updating
  • [19] I4CMOMXF dot files
  • [20] FCCJNYCV more tests for utils/
  • [21] NS4TDPLN cyclomatic complexity
  • [22] LBWQJEDH minor refactor and more tests for utils/
  • [23] 43TMU2JO more tests, glm much better than claude
  • [24] Q4JPMGET fixed tests
  • [25] QFPEKXL5 ck 6
  • [26] KZKLAINJ run out of space on nest, cleaned out
  • [27] 2P27XV3D fixed cyclo over 30
  • [28] E27ZWCDP cyclo over 18
  • [29] VU3KBTQ6 more tests
  • [30] N57PNZPF second phase of utils refactor, audio/
  • [31] PXQDGTR5 fourth phase of utils refactor, spectrogram/
  • [32] ZKLAOPUR fix event logging
  • [33] ZCCQ4P5T reduce complexity to under 14, gocyclo but cilint test still has 3 functions over
  • [34] O45G7VX2 added an add and a remove command
  • [35] DHIPFBFP added tests
  • [36] LQLC7S3A trying gemini: Inconsistent Standards in @utils/ refactoring
  • [37] TUC452XH new util shared by 3 cmd's needing location
  • [38] NQPVZ3PP first phase of utils refactor, all realted to db interfaces
  • [*] SJN7IKIV

Change contents

  • file addition: filename_parser_test.go (----------)
    [5.1]
    package wav
    import (
    "testing"
    "time"
    )
    type expectedTS struct {
    Year, Month, Day, Hour, Minute, Second int
    }
    func assertTimestamp(t *testing.T, got time.Time, want expectedTS) {
    t.Helper()
    t.Helper()
    if got.Year() != want.Year {
    t.Errorf("Year: got %d, want %d", got.Year(), want.Year)
    }
    if got.Month() != time.Month(want.Month) {
    t.Errorf("Month: got %d, want %d", got.Month(), want.Month)
    }
    if got.Day() != want.Day {
    t.Errorf("Day: got %d, want %d", got.Day(), want.Day)
    }
    if got.Hour() != want.Hour {
    t.Errorf("Hour: got %d, want %d", got.Hour(), want.Hour)
    }
    if got.Minute() != want.Minute {
    t.Errorf("Minute: got %d, want %d", got.Minute(), want.Minute)
    }
    if got.Second() != want.Second {
    t.Errorf("Second: got %d, want %d", got.Second(), want.Second)
    }
    }
    func assertOffset(t *testing.T, got time.Time, wantSeconds int) {
    t.Helper()
    _, offset := got.Zone()
    if offset != wantSeconds {
    t.Errorf("Offset: got %d seconds, want %d seconds", offset, wantSeconds)
    }
    }
    // parseAndApply is a test helper that parses filenames and applies a timezone offset.
    func parseAndApply(t *testing.T, filenames []string, tz string) []time.Time {
    t.Helper()
    parsed, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    results, err := ApplyTimezoneOffset(parsed, tz)
    if err != nil {
    t.Fatalf("Failed to apply timezone: %v", err)
    }
    return results
    }
    // parseTestCase defines a table-driven test case for ParseFilenameTimestamps.
    type parseTestCase struct {
    name string
    files []string
    expected map[int]expectedTS // index → expected timestamp
    }
    func runParseTestCase(t *testing.T, tc parseTestCase) {
    t.Helper()
    results, err := ParseFilenameTimestamps(tc.files)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    if len(results) != len(tc.files) {
    t.Fatalf("Expected %d results, got %d", len(tc.files), len(results))
    }
    for idx, want := range tc.expected {
    assertTimestamp(t, results[idx].Timestamp, want)
    }
    }
    func TestParseFilenameTimestamps(t *testing.T) {
    cases := []parseTestCase{
    {
    name: "YYMMDD format (test case a)",
    files: []string{"201012_123456.wav", "201014_123456.WAV", "201217_123456.wav", "211122_123456.WAV"},
    expected: map[int]expectedTS{
    0: {2020, 10, 12, 12, 34, 56}, // Year 20 → 2020
    3: {2021, 11, 22, 12, 34, 56},
    },
    },
    {
    name: "DDMMYY format (test case b)",
    files: []string{"121020_123456.WAV", "141020_123456.wav", "171220_123456.WAV", "221121_123456.wav"},
    expected: map[int]expectedTS{
    0: {2020, 10, 12, 12, 34, 56},
    2: {2020, 12, 17, 12, 34, 56},
    },
    },
    {
    name: "YYYYMMDD format (test case c)",
    files: []string{"20230609_103000.WAV", "20241109_201504.wav"},
    expected: map[int]expectedTS{
    0: {2023, 6, 9, 10, 30, 0},
    1: {2024, 11, 9, 20, 15, 4},
    },
    },
    {
    name: "6-digit with variance detection (test case d)",
    files: []string{"120119_003002.wav", "180120_231502.wav", "170122_010005.wav", "010419_234502.WAV", "310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
    expected: map[int]expectedTS{
    0: {2019, 1, 12, 0, 30, 2}, // DDMMYY
    4: {2020, 3, 31, 23, 15, 2},
    },
    },
    {
    name: "prefixes (test case e)",
    files: []string{"XYZ123_7689_20230609_103000.WAV", "string 20241109_201504.wav"},
    expected: map[int]expectedTS{
    0: {2023, 6, 9, 10, 30, 0},
    1: {2024, 11, 9, 20, 15, 4},
    },
    },
    {
    name: "complex prefixes (test case f)",
    files: []string{"abcdefg__1234_180120_231502.wav", "string 120119_003002.wav", "ABCD EFG___170122_010005.wav", "BHD_1234 010419_234502.WAV", "cill xyz 310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
    expected: map[int]expectedTS{
    0: {2020, 1, 18, 23, 15, 2},
    1: {2019, 1, 12, 0, 30, 2},
    4: {2020, 3, 31, 23, 15, 2},
    },
    },
    }
    for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
    runParseTestCase(t, tc)
    })
    }
    }
    func TestParseFilenameTimestampsErrors(t *testing.T) {
    t.Run("should throw error for empty filename array", func(t *testing.T) {
    _, err := ParseFilenameTimestamps([]string{})
    if err == nil {
    t.Error("Expected error for empty filename array")
    }
    if err != nil && err.Error() != "no filenames provided" {
    t.Logf("Error message: %v", err)
    }
    })
    t.Run("should throw error for filenames without date patterns", func(t *testing.T) {
    _, err := ParseFilenameTimestamps([]string{"invalid_filename.wav"})
    if err == nil {
    t.Error("Expected error for filenames without date patterns")
    }
    })
    t.Run("should throw error for mixed date formats", func(t *testing.T) {
    mixedFormats := []string{"201012_123456.wav", "20231012_123456.wav"} // 6-digit vs 8-digit
    _, err := ParseFilenameTimestamps(mixedFormats)
    if err == nil {
    t.Error("Expected error for mixed date formats")
    }
    })
    t.Run("should throw error for wrong length patterns", func(t *testing.T) {
    wrongLength := []string{"2010_123456.wav"} // 4 digits instead of 6 or 8
    _, err := ParseFilenameTimestamps(wrongLength)
    if err == nil {
    t.Error("Expected error for wrong length patterns")
    }
    })
    t.Run("should throw error when not enough files for 6-digit disambiguation", func(t *testing.T) {
    singleFile := []string{"120119_003002.wav"}
    _, err := ParseFilenameTimestamps(singleFile)
    if err == nil {
    t.Error("Expected error when not enough files for 6-digit disambiguation")
    }
    })
    }
    func TestApplyTimezoneOffset(t *testing.T) {
    t.Run("should apply UTC timezone correctly", func(t *testing.T) {
    results := parseAndApply(t, []string{"201012_123456.wav", "201014_123456.WAV"}, "UTC")
    if len(results) != 2 {
    t.Fatalf("Expected 2 results, got %d", len(results))
    }
    assertOffset(t, results[0], 0)
    })
    t.Run("should use fixed offset for entire cluster spanning DST transition", func(t *testing.T) {
    // Auckland DST ended April 4, 2021 (UTC+13 -> UTC+12)
    results := parseAndApply(t, []string{
    "20210401_120000.wav", // April 1st - DST active (UTC+13)
    "20210410_120000.wav", // April 10th - DST ended (would be UTC+12 if DST applied)
    "20210420_120000.wav", // April 20th - Standard time
    }, "Pacific/Auckland")
    if len(results) != 3 {
    t.Fatalf("Expected 3 results, got %d", len(results))
    }
    // All files should use UTC+13 offset (from earliest file: April 1st)
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // All at 12:00 local - 13h = 23:00 UTC previous day
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 31, 23, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 4, 9, 23, 0, 0})
    assertTimestamp(t, results[2].UTC(), expectedTS{2021, 4, 19, 23, 0, 0})
    })
    t.Run("should handle out-of-order filenames correctly", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210410_120000.wav", // April 10th (later)
    "20210401_120000.wav", // April 1st (earliest - determines offset)
    "20210405_120000.wav", // April 5th (middle)
    }, "Pacific/Auckland")
    // All files use UTC+13 (from April 1st, the earliest)
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // Results maintain original filename order
    assertTimestamp(t, results[0], expectedTS{2021, 4, 10, 12, 0, 0})
    assertTimestamp(t, results[1], expectedTS{2021, 4, 1, 12, 0, 0})
    assertTimestamp(t, results[2], expectedTS{2021, 4, 5, 12, 0, 0})
    })
    t.Run("should apply fixed offset consistently across large time spans", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210215_120000.wav", // February (summer, UTC+13)
    "20210615_120000.wav", // June (winter, would be UTC+12 if DST applied)
    "20210815_120000.wav", // August (winter)
    }, "Pacific/Auckland")
    // All files use offset from earliest (February): UTC+13
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // 12:00 local - 13h = 23:00 UTC previous day
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 2, 14, 23, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 6, 14, 23, 0, 0})
    assertTimestamp(t, results[2].UTC(), expectedTS{2021, 8, 14, 23, 0, 0})
    })
    t.Run("should handle US DST transitions with fixed offset", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210310_120000.wav", // March 10th - before DST (UTC-5)
    "20210320_120000.wav", // March 20th - after DST (would be UTC-4)
    }, "America/New_York")
    // All files use offset from earliest (March 10th): UTC-5
    for _, r := range results {
    assertOffset(t, r, -5*3600)
    }
    // 12:00 local + 5h = 17:00 UTC
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 10, 17, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 3, 20, 17, 0, 0})
    })
    t.Run("should handle empty timestamps array", func(t *testing.T) {
    _, err := ApplyTimezoneOffset([]FilenameTimestamp{}, "UTC")
    if err == nil {
    t.Error("Expected error for empty timestamps array")
    }
    })
    t.Run("should handle invalid timezone", func(t *testing.T) {
    filenames := []string{"20210401_120000.wav"}
    parsed, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    _, err = ApplyTimezoneOffset(parsed, "Invalid/Timezone")
    if err == nil {
    t.Error("Expected error for invalid timezone")
    }
    })
    }
    func TestHasTimestampFilename(t *testing.T) {
    testCases := []struct {
    filename string
    expected bool
    }{
    {"201012_123456.wav", true},
    {"20230609_103000.WAV", true},
    {"invalid_filename.wav", false},
    {"201012_123456.txt", false},
    {"201012.wav", false},
    {"_123456.wav", false},
    {"", false},
    }
    for _, tc := range testCases {
    t.Run(tc.filename, func(t *testing.T) {
    result := HasTimestampFilename(tc.filename)
    if result != tc.expected {
    t.Errorf("HasTimestampFilename(%q) = %v, want %v", tc.filename, result, tc.expected)
    }
    })
    }
    }
    func TestFilenameParserEdgeCases(t *testing.T) {
    t.Run("should handle case-insensitive file extensions", func(t *testing.T) {
    filenames := []string{
    "201012_123456.wav",
    "201014_123456.WAV",
    "201217_123456.Wav",
    }
    results, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    if len(results) != 3 {
    t.Errorf("Expected 3 results, got %d", len(results))
    }
    })
    t.Run("should validate invalid dates", func(t *testing.T) {
    // 32nd day doesn't exist - should be caught by validation
    filenames := []string{"20240132_120000.wav"}
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for invalid date (day 32)")
    }
    })
    t.Run("should validate invalid months", func(t *testing.T) {
    // 13th month doesn't exist
    filenames := []string{"20241301_120000.wav"}
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for invalid month (13)")
    }
    })
    t.Run("should handle February 29th in leap year", func(t *testing.T) {
    filenames := []string{"20240229_120000.wav"} // 2024 is a leap year
    results, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse leap year date: %v", err)
    }
    if results[0].Timestamp.Day() != 29 {
    t.Errorf("Expected day 29, got %d", results[0].Timestamp.Day())
    }
    })
    t.Run("should reject February 29th in non-leap year", func(t *testing.T) {
    filenames := []string{"20230229_120000.wav"} // 2023 is not a leap year
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for Feb 29th in non-leap year")
    }
    })
    }
    func TestUTCConversionCorrectness(t *testing.T) {
    t.Run("should convert Pacific/Auckland night recordings correctly to UTC", func(t *testing.T) {
    // 21:00 Pacific/Auckland (May = UTC+12) → 09:00 UTC same day
    results := parseAndApply(t, []string{"20210505_210000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 9, 0, 0})
    })
    t.Run("should convert day recordings correctly to UTC", func(t *testing.T) {
    // 12:00 Pacific/Auckland (May = UTC+12) → 00:00 UTC same day
    results := parseAndApply(t, []string{"20210505_120000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 0, 0, 0})
    })
    t.Run("should handle date rollover correctly", func(t *testing.T) {
    // 02:00 Pacific/Auckland (May = UTC+12) → 14:00 UTC previous day
    results := parseAndApply(t, []string{"20210505_020000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 4, 14, 0, 0})
    })
    t.Run("should convert correctly for negative offset timezone", func(t *testing.T) {
    // 15:00 New York (June = UTC-4 during DST) → 19:00 UTC same day
    results := parseAndApply(t, []string{"20210615_150000.wav"}, "America/New_York")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 6, 15, 19, 0, 0})
    })
    }
  • file addition: filename_parser.go (----------)
    [5.1]
    package wav
    import (
    "fmt"
    "path/filepath"
    "regexp"
    "strconv"
    "time"
    )
    // DateFormat represents the detected filename date format
    type DateFormat int
    // Date format constants for filename timestamp parsing
    const (
    Format8Digit DateFormat = iota // YYYYMMDD_HHMMSS (e.g., 20230609_103000.wav)
    Format6YYMMDD // YYMMDD_HHMMSS (e.g., 201012_123456.wav) - year first
    Format6DDMMYY // DDMMYY_HHMMSS (e.g., 121020_123456.wav) - year last
    )
    var (
    // Pattern to match timestamp filenames
    // Supports: YYYYMMDD_HHMMSS, YYMMDD_HHMMSS, DDMMYY_HHMMSS
    // Case-insensitive for file extension (.wav, .WAV, .Wav)
    // Allows prefixes before the timestamp pattern
    // Allows optional suffixes between timestamp and extension (e.g., _16kHz)
    timestampPattern = regexp.MustCompile(`(?i)(\d{6,8})_(\d{6})(?:_[^/\\]*)?\.wav$`)
    )
    // dateParts represents parsed date components for format detection
    type dateParts struct {
    x1 int // First 2 digits
    m int // Middle 2 digits (always month)
    x2 int // Last 2 digits
    }
    // FilenameTimestamp represents a parsed timestamp from a filename
    type FilenameTimestamp struct {
    Filename string
    Timestamp time.Time
    Format DateFormat
    }
    // ParseFilenameTimestamps extracts timestamps from a batch of filenames using variance-based format detection.
    // Uses variance-based disambiguation for 6-digit dates (YYMMDD vs DDMMYY).
    // Returns timestamps in UTC (timezone must be applied separately).
    func ParseFilenameTimestamps(filenames []string) ([]FilenameTimestamp, error) {
    if len(filenames) == 0 {
    return nil, fmt.Errorf("no filenames provided")
    }
    // Detect date format by analyzing all filenames
    format, err := detectDateFormat(filenames)
    if err != nil {
    return nil, err
    }
    // Parse all filenames using detected format
    results := make([]FilenameTimestamp, 0, len(filenames))
    for _, filename := range filenames {
    timestamp, err := parseFilenameWithFormat(filename, format)
    if err != nil {
    return nil, fmt.Errorf("failed to parse %s: %w", filename, err)
    }
    results = append(results, FilenameTimestamp{
    Filename: filename,
    Timestamp: timestamp,
    Format: format,
    })
    }
    return results, nil
    }
    // ApplyTimezoneOffset converts local timestamps to a location timezone with DST handling.
    // Uses the EARLIEST (chronologically) timestamp to determine the offset, then applies it to all.
    // This matches AudioMoth behavior (no DST adjustment during deployment).
    func ApplyTimezoneOffset(timestamps []FilenameTimestamp, timezoneID string) ([]time.Time, error) {
    if len(timestamps) == 0 {
    return nil, fmt.Errorf("no timestamps provided")
    }
    // Load timezone location
    loc, err := time.LoadLocation(timezoneID)
    if err != nil {
    return nil, fmt.Errorf("invalid timezone %s: %w", timezoneID, err)
    }
    // Find chronologically earliest timestamp
    earliestUTC := timestamps[0].Timestamp
    for _, ts := range timestamps[1:] {
    if ts.Timestamp.Before(earliestUTC) {
    earliestUTC = ts.Timestamp
    }
    }
    // Calculate offset from earliest timestamp
    earliestInZone := time.Date(
    earliestUTC.Year(), earliestUTC.Month(), earliestUTC.Day(),
    earliestUTC.Hour(), earliestUTC.Minute(), earliestUTC.Second(),
    0, loc,
    )
    // Get fixed offset (doesn't change for DST)
    _, offsetSeconds := earliestInZone.Zone()
    fixedOffset := time.FixedZone("Fixed", offsetSeconds)
    // Apply SAME offset to ALL timestamps (maintaining original order)
    results := make([]time.Time, len(timestamps))
    for i, ts := range timestamps {
    adjusted := time.Date(
    ts.Timestamp.Year(), ts.Timestamp.Month(), ts.Timestamp.Day(),
    ts.Timestamp.Hour(), ts.Timestamp.Minute(), ts.Timestamp.Second(),
    0, fixedOffset,
    )
    results[i] = adjusted
    }
    return results, nil
    }
    // detectDateFormat analyzes filenames to determine the date format
    func detectDateFormat(filenames []string) (DateFormat, error) {
    // Extract all date parts from filenames
    var parts []dateParts
    var has8Digit bool
    for _, filename := range filenames {
    basename := filepath.Base(filename)
    matches := timestampPattern.FindStringSubmatch(basename)
    if matches == nil {
    continue
    }
    dateStr := matches[1]
    // Check for 8-digit format (YYYYMMDD)
    if len(dateStr) == 8 {
    has8Digit = true
    continue
    }
    // Parse 6-digit format
    if len(dateStr) == 6 {
    x1, _ := strconv.Atoi(dateStr[0:2])
    m, _ := strconv.Atoi(dateStr[2:4])
    x2, _ := strconv.Atoi(dateStr[4:6])
    parts = append(parts, dateParts{x1: x1, m: m, x2: x2})
    }
    }
    // If all files are 8-digit, that's the format
    if has8Digit && len(parts) == 0 {
    return Format8Digit, nil
    }
    // If mixed 8-digit and 6-digit, return error
    if has8Digit && len(parts) > 0 {
    return 0, fmt.Errorf("mixed date formats detected (8-digit and 6-digit)")
    }
    // If no 6-digit dates found, cannot determine
    if len(parts) == 0 {
    return 0, fmt.Errorf("no valid timestamp filenames found")
    }
    // Need at least 2 files with different dates to disambiguate YYMMDD vs DDMMYY
    if len(parts) == 1 {
    return 0, fmt.Errorf("need at least 2 files to disambiguate 6-digit date format (YYMMDD vs DDMMYY)")
    }
    // Use variance-based disambiguation for 6-digit dates
    // Compare uniqueness of x1 (first 2 digits) vs x2 (last 2 digits)
    // Day values vary more than year values across recordings
    uniqueX1 := countUnique(parts, func(p dateParts) int { return p.x1 })
    uniqueX2 := countUnique(parts, func(p dateParts) int { return p.x2 })
    if uniqueX2 >= uniqueX1 {
    // x2 has more variance → likely day values → YYMMDD format
    return Format6YYMMDD, nil
    } else {
    // x1 has more variance → likely day values → DDMMYY format
    return Format6DDMMYY, nil
    }
    }
    // parseFilenameWithFormat parses a filename using the specified format
    func parseFilenameWithFormat(filename string, format DateFormat) (time.Time, error) {
    basename := filepath.Base(filename)
    matches := timestampPattern.FindStringSubmatch(basename)
    if matches == nil {
    return time.Time{}, fmt.Errorf("filename does not match timestamp pattern: %s", basename)
    }
    dateStr := matches[1]
    timeStr := matches[2]
    var year, month, day int
    switch format {
    case Format8Digit:
    if len(dateStr) != 8 {
    return time.Time{}, fmt.Errorf("expected 8-digit date, got %d digits", len(dateStr))
    }
    year, _ = strconv.Atoi(dateStr[0:4])
    month, _ = strconv.Atoi(dateStr[4:6])
    day, _ = strconv.Atoi(dateStr[6:8])
    case Format6YYMMDD:
    if len(dateStr) != 6 {
    return time.Time{}, fmt.Errorf("expected 6-digit date, got %d digits", len(dateStr))
    }
    yy, _ := strconv.Atoi(dateStr[0:2])
    month, _ = strconv.Atoi(dateStr[2:4])
    day, _ = strconv.Atoi(dateStr[4:6])
    // Convert 2-digit year to 4-digit (assume 2000-2099)
    year = 2000 + yy
    case Format6DDMMYY:
    if len(dateStr) != 6 {
    return time.Time{}, fmt.Errorf("expected 6-digit date, got %d digits", len(dateStr))
    }
    day, _ = strconv.Atoi(dateStr[0:2])
    month, _ = strconv.Atoi(dateStr[2:4])
    yy, _ := strconv.Atoi(dateStr[4:6])
    // Convert 2-digit year to 4-digit (assume 2000-2099)
    year = 2000 + yy
    }
    // Parse time (HHMMSS)
    if len(timeStr) != 6 {
    return time.Time{}, fmt.Errorf("invalid time format: %s", timeStr)
    }
    hour, _ := strconv.Atoi(timeStr[0:2])
    minute, _ := strconv.Atoi(timeStr[2:4])
    second, _ := strconv.Atoi(timeStr[4:6])
    // Construct timestamp in UTC (timezone applied separately)
    timestamp := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC)
    // Validate date
    if timestamp.Month() != time.Month(month) || timestamp.Day() != day {
    return time.Time{}, fmt.Errorf("invalid date: %04d-%02d-%02d", year, month, day)
    }
    return timestamp, nil
    }
    // countUnique counts unique values using an extractor function
    func countUnique(parts []dateParts, extractor func(p dateParts) int) int {
    seen := make(map[int]bool)
    for _, p := range parts {
    seen[extractor(p)] = true
    }
    return len(seen)
    }
    // HasTimestampFilename checks if a filename contains a timestamp pattern
    func HasTimestampFilename(filename string) bool {
    basename := filepath.Base(filename)
    return timestampPattern.MatchString(basename)
    }
  • replacement in wav/file_import.go at line 44
    [5.4807][5.4807:4951]()
    } else if utils.HasTimestampFilename(filePath) {
    filenameTimestamps, err := utils.ParseFilenameTimestamps([]string{filepath.Base(filePath)})
    [5.4807]
    [5.4951]
    } else if HasTimestampFilename(filePath) {
    filenameTimestamps, err := ParseFilenameTimestamps([]string{filepath.Base(filePath)})
  • replacement in wav/file_import.go at line 47
    [5.4969][5.4969:5057]()
    adjustedTimestamps, err := utils.ApplyTimezoneOffset(filenameTimestamps, timezoneID)
    [5.4969]
    [5.5057]
    adjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, timezoneID)
  • file deletion: placeholders.go (----------)
    [5.1][5.275:314](),[5.314][5.1:1]()
    package utils
    import "strings"
    // Placeholders generates SQL placeholder string for IN clauses (e.g. "?, ?, ?")
    func Placeholders(n int) string {
    if n == 0 {
    return ""
    }
    ph := make([]string, n)
    for i := range ph {
    ph[i] = "?"
    }
    return strings.Join(ph, ", ")
    }
  • file deletion: placeholders_test.go (----------)
    [5.1][5.2174:2218](),[5.2218][5.1768:1768]()
    package utils
    import "testing"
    func TestPlaceholders(t *testing.T) {
    tests := []struct {
    n int
    want string
    }{
    {0, ""},
    {1, "?"},
    {3, "?, ?, ?"},
    {5, "?, ?, ?, ?, ?"},
    }
    for _, tt := range tests {
    t.Run(string(rune('0'+tt.n)), func(t *testing.T) {
    got := Placeholders(tt.n)
    if got != tt.want {
    t.Errorf("Placeholders(%d) = %q, want %q", tt.n, got, tt.want)
    }
    })
    }
    }
  • file deletion: find_data_files_test.go (----------)
    [5.1][5.2986:3033](),[5.3033][5.1:1]()
    package utils
    import (
    "os"
    "path/filepath"
    "sort"
    "testing"
    )
    func TestFindDataFiles_Basic(t *testing.T) {
    dir := t.TempDir()
    // Create some .data files
    for _, name := range []string{"a.data", "b.data", "c.data"} {
    if err := os.WriteFile(filepath.Join(dir, name), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    }
    // Create a non-.data file that should be ignored
    if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("ignore"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    sort.Strings(files)
    if len(files) != 3 {
    t.Fatalf("expected 3 files, got %d: %v", len(files), files)
    }
    for i, base := range []string{"a.data", "b.data", "c.data"} {
    expected := filepath.Join(dir, base)
    if files[i] != expected {
    t.Errorf("file %d: got %q, want %q", i, files[i], expected)
    }
    }
    }
    func TestFindDataFiles_SkipsHidden(t *testing.T) {
    dir := t.TempDir()
    // Regular .data file
    if err := os.WriteFile(filepath.Join(dir, "visible.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    // Hidden .data file (should be skipped)
    if err := os.WriteFile(filepath.Join(dir, ".hidden.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 1 {
    t.Fatalf("expected 1 file (hidden skipped), got %d: %v", len(files), files)
    }
    if filepath.Base(files[0]) != "visible.data" {
    t.Errorf("got %q, want visible.data", files[0])
    }
    }
    func TestFindDataFiles_NonRecursive(t *testing.T) {
    dir := t.TempDir()
    // .data file in root
    if err := os.WriteFile(filepath.Join(dir, "root.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    // .data file in subdirectory (should NOT be found)
    sub := filepath.Join(dir, "subdir")
    if err := os.Mkdir(sub, 0755); err != nil {
    t.Fatal(err)
    }
    if err := os.WriteFile(filepath.Join(sub, "nested.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 1 {
    t.Fatalf("expected 1 file (non-recursive), got %d: %v", len(files), files)
    }
    if filepath.Base(files[0]) != "root.data" {
    t.Errorf("got %q, want root.data", files[0])
    }
    }
    func TestFindDataFiles_EmptyDir(t *testing.T) {
    dir := t.TempDir()
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 0 {
    t.Errorf("expected 0 files, got %d", len(files))
    }
    }
    func TestFindDataFiles_NonexistentDir(t *testing.T) {
    _, err := FindDataFiles("/nonexistent/path/12345")
    if err == nil {
    t.Error("expected error for nonexistent directory")
    }
    }
    func TestFindDataFiles_NoDataFiles(t *testing.T) {
    dir := t.TempDir()
    if err := os.WriteFile(filepath.Join(dir, "readme.txt"), []byte("hello"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 0 {
    t.Errorf("expected 0 files, got %d", len(files))
    }
    }
  • file deletion: config_test.go (----------)
    [5.1][5.6183:6221](),[5.6221][5.5284:5284]()
    package utils
    import (
    "os"
    "path/filepath"
    "testing"
    )
    func TestLoadConfig(t *testing.T) {
    homeDir := t.TempDir()
    t.Setenv("HOME", homeDir)
    configDir := filepath.Join(homeDir, ".skraak")
    err := os.MkdirAll(configDir, 0755)
    if err != nil {
    t.Fatalf("failed to create config dir: %v", err)
    }
    jsonContent := `{
    "classify": {
    "reviewer": "Test Reviewer",
    "color": true
    }
    }`
    err = os.WriteFile(filepath.Join(configDir, "config.json"), []byte(jsonContent), 0644)
    if err != nil {
    t.Fatalf("failed to write config: %v", err)
    }
    cfg, path, err := LoadConfig()
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    if cfg.Classify.Reviewer != "Test Reviewer" {
    t.Errorf("expected Test Reviewer, got %s", cfg.Classify.Reviewer)
    }
    if !cfg.Classify.Color {
    t.Error("expected color to be true")
    }
    if path == "" {
    t.Error("expected path to be returned")
    }
    }
  • file deletion: filename_parser_test.go (----------)
    [5.1][5.122505:122552](),[5.122552][5.98985:98985]()
    package utils
    import (
    "testing"
    )
    type expectedTS struct {
    Year, Month, Day, Hour, Minute, Second int
    }
    func assertTimestamp(t *testing.T, got time.Time, want expectedTS) {
    t.Helper()
    t.Helper()
    if got.Year() != want.Year {
    t.Errorf("Year: got %d, want %d", got.Year(), want.Year)
    }
    if got.Month() != time.Month(want.Month) {
    t.Errorf("Month: got %d, want %d", got.Month(), want.Month)
    }
    if got.Day() != want.Day {
    t.Errorf("Day: got %d, want %d", got.Day(), want.Day)
    }
    if got.Hour() != want.Hour {
    t.Errorf("Hour: got %d, want %d", got.Hour(), want.Hour)
    }
    if got.Minute() != want.Minute {
    t.Errorf("Minute: got %d, want %d", got.Minute(), want.Minute)
    }
    if got.Second() != want.Second {
    t.Errorf("Second: got %d, want %d", got.Second(), want.Second)
    }
    }
    func assertOffset(t *testing.T, got time.Time, wantSeconds int) {
    t.Helper()
    _, offset := got.Zone()
    if offset != wantSeconds {
    t.Errorf("Offset: got %d seconds, want %d seconds", offset, wantSeconds)
    }
    }
    // parseAndApply is a test helper that parses filenames and applies a timezone offset.
    func parseAndApply(t *testing.T, filenames []string, tz string) []time.Time {
    t.Helper()
    parsed, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    results, err := ApplyTimezoneOffset(parsed, tz)
    if err != nil {
    t.Fatalf("Failed to apply timezone: %v", err)
    }
    return results
    }
    }
    func TestParseFilenameTimestampsErrors(t *testing.T) {
    t.Run("should throw error for empty filename array", func(t *testing.T) {
    _, err := ParseFilenameTimestamps([]string{})
    if err == nil {
    t.Error("Expected error for empty filename array")
    }
    if err != nil && err.Error() != "no filenames provided" {
    t.Logf("Error message: %v", err)
    }
    })
    t.Run("should throw error for filenames without date patterns", func(t *testing.T) {
    _, err := ParseFilenameTimestamps([]string{"invalid_filename.wav"})
    if err == nil {
    t.Error("Expected error for filenames without date patterns")
    }
    })
    t.Run("should throw error for mixed date formats", func(t *testing.T) {
    mixedFormats := []string{"201012_123456.wav", "20231012_123456.wav"} // 6-digit vs 8-digit
    _, err := ParseFilenameTimestamps(mixedFormats)
    if err == nil {
    t.Error("Expected error for mixed date formats")
    }
    })
    t.Run("should throw error for wrong length patterns", func(t *testing.T) {
    wrongLength := []string{"2010_123456.wav"} // 4 digits instead of 6 or 8
    _, err := ParseFilenameTimestamps(wrongLength)
    if err == nil {
    t.Error("Expected error for wrong length patterns")
    }
    })
    t.Run("should throw error when not enough files for 6-digit disambiguation", func(t *testing.T) {
    singleFile := []string{"120119_003002.wav"}
    _, err := ParseFilenameTimestamps(singleFile)
    if err == nil {
    t.Error("Expected error when not enough files for 6-digit disambiguation")
    }
    })
    }
    func TestApplyTimezoneOffset(t *testing.T) {
    t.Run("should apply UTC timezone correctly", func(t *testing.T) {
    results := parseAndApply(t, []string{"201012_123456.wav", "201014_123456.WAV"}, "UTC")
    if len(results) != 2 {
    t.Fatalf("Expected 2 results, got %d", len(results))
    }
    assertOffset(t, results[0], 0)
    })
    t.Run("should use fixed offset for entire cluster spanning DST transition", func(t *testing.T) {
    // Auckland DST ended April 4, 2021 (UTC+13 -> UTC+12)
    results := parseAndApply(t, []string{
    "20210401_120000.wav", // April 1st - DST active (UTC+13)
    "20210410_120000.wav", // April 10th - DST ended (would be UTC+12 if DST applied)
    "20210420_120000.wav", // April 20th - Standard time
    }, "Pacific/Auckland")
    if len(results) != 3 {
    t.Fatalf("Expected 3 results, got %d", len(results))
    }
    // All files should use UTC+13 offset (from earliest file: April 1st)
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // All at 12:00 local - 13h = 23:00 UTC previous day
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 31, 23, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 4, 9, 23, 0, 0})
    assertTimestamp(t, results[2].UTC(), expectedTS{2021, 4, 19, 23, 0, 0})
    })
    t.Run("should handle out-of-order filenames correctly", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210410_120000.wav", // April 10th (later)
    "20210401_120000.wav", // April 1st (earliest - determines offset)
    "20210405_120000.wav", // April 5th (middle)
    }, "Pacific/Auckland")
    // All files use UTC+13 (from April 1st, the earliest)
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // Results maintain original filename order
    assertTimestamp(t, results[0], expectedTS{2021, 4, 10, 12, 0, 0})
    assertTimestamp(t, results[1], expectedTS{2021, 4, 1, 12, 0, 0})
    assertTimestamp(t, results[2], expectedTS{2021, 4, 5, 12, 0, 0})
    })
    t.Run("should apply fixed offset consistently across large time spans", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210215_120000.wav", // February (summer, UTC+13)
    "20210615_120000.wav", // June (winter, would be UTC+12 if DST applied)
    "20210815_120000.wav", // August (winter)
    }, "Pacific/Auckland")
    // All files use offset from earliest (February): UTC+13
    for _, r := range results {
    assertOffset(t, r, 13*3600)
    }
    // 12:00 local - 13h = 23:00 UTC previous day
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 2, 14, 23, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 6, 14, 23, 0, 0})
    assertTimestamp(t, results[2].UTC(), expectedTS{2021, 8, 14, 23, 0, 0})
    })
    t.Run("should handle US DST transitions with fixed offset", func(t *testing.T) {
    results := parseAndApply(t, []string{
    "20210310_120000.wav", // March 10th - before DST (UTC-5)
    "20210320_120000.wav", // March 20th - after DST (would be UTC-4)
    }, "America/New_York")
    // All files use offset from earliest (March 10th): UTC-5
    for _, r := range results {
    assertOffset(t, r, -5*3600)
    }
    // 12:00 local + 5h = 17:00 UTC
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 10, 17, 0, 0})
    assertTimestamp(t, results[1].UTC(), expectedTS{2021, 3, 20, 17, 0, 0})
    })
    t.Run("should handle empty timestamps array", func(t *testing.T) {
    _, err := ApplyTimezoneOffset([]FilenameTimestamp{}, "UTC")
    if err == nil {
    t.Error("Expected error for empty timestamps array")
    }
    })
    t.Run("should handle invalid timezone", func(t *testing.T) {
    filenames := []string{"20210401_120000.wav"}
    parsed, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    _, err = ApplyTimezoneOffset(parsed, "Invalid/Timezone")
    if err == nil {
    t.Error("Expected error for invalid timezone")
    }
    })
    }
    func TestHasTimestampFilename(t *testing.T) {
    testCases := []struct {
    filename string
    expected bool
    }{
    {"201012_123456.wav", true},
    {"20230609_103000.WAV", true},
    {"invalid_filename.wav", false},
    {"201012_123456.txt", false},
    {"201012.wav", false},
    {"_123456.wav", false},
    {"", false},
    }
    for _, tc := range testCases {
    t.Run(tc.filename, func(t *testing.T) {
    result := HasTimestampFilename(tc.filename)
    if result != tc.expected {
    t.Errorf("HasTimestampFilename(%q) = %v, want %v", tc.filename, result, tc.expected)
    }
    })
    }
    }
    func TestFilenameParserEdgeCases(t *testing.T) {
    t.Run("should handle case-insensitive file extensions", func(t *testing.T) {
    filenames := []string{
    "201012_123456.wav",
    "201014_123456.WAV",
    "201217_123456.Wav",
    }
    results, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    if len(results) != 3 {
    t.Errorf("Expected 3 results, got %d", len(results))
    }
    })
    t.Run("should validate invalid dates", func(t *testing.T) {
    // 32nd day doesn't exist - should be caught by validation
    filenames := []string{"20240132_120000.wav"}
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for invalid date (day 32)")
    }
    })
    t.Run("should validate invalid months", func(t *testing.T) {
    // 13th month doesn't exist
    filenames := []string{"20241301_120000.wav"}
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for invalid month (13)")
    }
    })
    t.Run("should handle February 29th in leap year", func(t *testing.T) {
    filenames := []string{"20240229_120000.wav"} // 2024 is a leap year
    results, err := ParseFilenameTimestamps(filenames)
    if err != nil {
    t.Fatalf("Failed to parse leap year date: %v", err)
    }
    if results[0].Timestamp.Day() != 29 {
    t.Errorf("Expected day 29, got %d", results[0].Timestamp.Day())
    }
    })
    t.Run("should reject February 29th in non-leap year", func(t *testing.T) {
    filenames := []string{"20230229_120000.wav"} // 2023 is not a leap year
    _, err := ParseFilenameTimestamps(filenames)
    if err == nil {
    t.Error("Expected error for Feb 29th in non-leap year")
    }
    })
    }
    func TestUTCConversionCorrectness(t *testing.T) {
    t.Run("should convert Pacific/Auckland night recordings correctly to UTC", func(t *testing.T) {
    // 21:00 Pacific/Auckland (May = UTC+12) → 09:00 UTC same day
    results := parseAndApply(t, []string{"20210505_210000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 9, 0, 0})
    })
    t.Run("should convert day recordings correctly to UTC", func(t *testing.T) {
    // 12:00 Pacific/Auckland (May = UTC+12) → 00:00 UTC same day
    results := parseAndApply(t, []string{"20210505_120000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 0, 0, 0})
    })
    t.Run("should handle date rollover correctly", func(t *testing.T) {
    // 02:00 Pacific/Auckland (May = UTC+12) → 14:00 UTC previous day
    results := parseAndApply(t, []string{"20210505_020000.wav"}, "Pacific/Auckland")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 4, 14, 0, 0})
    })
    t.Run("should convert correctly for negative offset timezone", func(t *testing.T) {
    // 15:00 New York (June = UTC-4 during DST) → 19:00 UTC same day
    results := parseAndApply(t, []string{"20210615_150000.wav"}, "America/New_York")
    assertTimestamp(t, results[0].UTC(), expectedTS{2021, 6, 15, 19, 0, 0})
    })
    }
    for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
    runParseTestCase(t, tc)
    })
    }
    func TestParseFilenameTimestamps(t *testing.T) {
    cases := []parseTestCase{
    {
    name: "YYMMDD format (test case a)",
    files: []string{"201012_123456.wav", "201014_123456.WAV", "201217_123456.wav", "211122_123456.WAV"},
    expected: map[int]expectedTS{
    0: {2020, 10, 12, 12, 34, 56}, // Year 20 → 2020
    3: {2021, 11, 22, 12, 34, 56},
    },
    },
    {
    name: "DDMMYY format (test case b)",
    files: []string{"121020_123456.WAV", "141020_123456.wav", "171220_123456.WAV", "221121_123456.wav"},
    expected: map[int]expectedTS{
    0: {2020, 10, 12, 12, 34, 56},
    2: {2020, 12, 17, 12, 34, 56},
    },
    },
    {
    name: "YYYYMMDD format (test case c)",
    files: []string{"20230609_103000.WAV", "20241109_201504.wav"},
    expected: map[int]expectedTS{
    0: {2023, 6, 9, 10, 30, 0},
    1: {2024, 11, 9, 20, 15, 4},
    },
    },
    {
    name: "6-digit with variance detection (test case d)",
    files: []string{"120119_003002.wav", "180120_231502.wav", "170122_010005.wav", "010419_234502.WAV", "310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
    expected: map[int]expectedTS{
    0: {2019, 1, 12, 0, 30, 2}, // DDMMYY
    4: {2020, 3, 31, 23, 15, 2},
    },
    },
    {
    name: "prefixes (test case e)",
    files: []string{"XYZ123_7689_20230609_103000.WAV", "string 20241109_201504.wav"},
    expected: map[int]expectedTS{
    0: {2023, 6, 9, 10, 30, 0},
    1: {2024, 11, 9, 20, 15, 4},
    },
    },
    {
    name: "complex prefixes (test case f)",
    files: []string{"abcdefg__1234_180120_231502.wav", "string 120119_003002.wav", "ABCD EFG___170122_010005.wav", "BHD_1234 010419_234502.WAV", "cill xyz 310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
    expected: map[int]expectedTS{
    0: {2020, 1, 18, 23, 15, 2},
    1: {2019, 1, 12, 0, 30, 2},
    4: {2020, 3, 31, 23, 15, 2},
    },
    },
    }
    func runParseTestCase(t *testing.T, tc parseTestCase) {
    t.Helper()
    results, err := ParseFilenameTimestamps(tc.files)
    if err != nil {
    t.Fatalf("Failed to parse filenames: %v", err)
    }
    if len(results) != len(tc.files) {
    t.Fatalf("Expected %d results, got %d", len(tc.files), len(results))
    }
    for idx, want := range tc.expected {
    assertTimestamp(t, results[idx].Timestamp, want)
    }
    }
    // parseTestCase defines a table-driven test case for ParseFilenameTimestamps.
    type parseTestCase struct {
    name string
    files []string
    expected map[int]expectedTS // index → expected timestamp
    }
    "time"
  • file deletion: filename_parser.go (----------)
    [5.1][5.130867:130909](),[5.130909][5.122554:122554]()
    package utils
    import (
    "fmt"
    "path/filepath"
    "regexp"
    "strconv"
    "time"
    )
    // DateFormat represents the detected filename date format
    type DateFormat int
    // Date format constants for filename timestamp parsing
    const (
    Format8Digit DateFormat = iota // YYYYMMDD_HHMMSS (e.g., 20230609_103000.wav)
    Format6YYMMDD // YYMMDD_HHMMSS (e.g., 201012_123456.wav) - year first
    Format6DDMMYY // DDMMYY_HHMMSS (e.g., 121020_123456.wav) - year last
    )
    var (
    // Pattern to match timestamp filenames
    // Supports: YYYYMMDD_HHMMSS, YYMMDD_HHMMSS, DDMMYY_HHMMSS
    // Case-insensitive for file extension (.wav, .WAV, .Wav)
    // Allows prefixes before the timestamp pattern
    // Allows optional suffixes between timestamp and extension (e.g., _16kHz)
    timestampPattern = regexp.MustCompile(`(?i)(\d{6,8})_(\d{6})(?:_[^/\\]*)?\.wav$`)
    )
    // dateParts represents parsed date components for format detection
    type dateParts struct {
    x1 int // First 2 digits
    m int // Middle 2 digits (always month)
    x2 int // Last 2 digits
    }
    // FilenameTimestamp represents a parsed timestamp from a filename
    type FilenameTimestamp struct {
    Filename string
    Timestamp time.Time
    Format DateFormat
    }
    // ParseFilenameTimestamps extracts timestamps from a batch of filenames using variance-based format detection.
    // Uses variance-based disambiguation for 6-digit dates (YYMMDD vs DDMMYY).
    // Returns timestamps in UTC (timezone must be applied separately).
    func ParseFilenameTimestamps(filenames []string) ([]FilenameTimestamp, error) {
    if len(filenames) == 0 {
    return nil, fmt.Errorf("no filenames provided")
    }
    // Detect date format by analyzing all filenames
    format, err := detectDateFormat(filenames)
    if err != nil {
    return nil, err
    }
    // Parse all filenames using detected format
    results := make([]FilenameTimestamp, 0, len(filenames))
    for _, filename := range filenames {
    timestamp, err := parseFilenameWithFormat(filename, format)
    if err != nil {
    return nil, fmt.Errorf("failed to parse %s: %w", filename, err)
    }
    results = append(results, FilenameTimestamp{
    Filename: filename,
    Timestamp: timestamp,
    Format: format,
    })
    }
    return results, nil
    }
    // ApplyTimezoneOffset converts local timestamps to a location timezone with DST handling.
    // Uses the EARLIEST (chronologically) timestamp to determine the offset, then applies it to all.
    // This matches AudioMoth behavior (no DST adjustment during deployment).
    func ApplyTimezoneOffset(timestamps []FilenameTimestamp, timezoneID string) ([]time.Time, error) {
    if len(timestamps) == 0 {
    return nil, fmt.Errorf("no timestamps provided")
    }
    // Load timezone location
    loc, err := time.LoadLocation(timezoneID)
    if err != nil {
    return nil, fmt.Errorf("invalid timezone %s: %w", timezoneID, err)
    }
    // Find chronologically earliest timestamp
    earliestUTC := timestamps[0].Timestamp
    for _, ts := range timestamps[1:] {
    if ts.Timestamp.Before(earliestUTC) {
    earliestUTC = ts.Timestamp
    }
    }
    // Calculate offset from earliest timestamp
    earliestInZone := time.Date(
    earliestUTC.Year(), earliestUTC.Month(), earliestUTC.Day(),
    earliestUTC.Hour(), earliestUTC.Minute(), earliestUTC.Second(),
    0, loc,
    )
    // Get fixed offset (doesn't change for DST)
    _, offsetSeconds := earliestInZone.Zone()
    fixedOffset := time.FixedZone("Fixed", offsetSeconds)
    // Apply SAME offset to ALL timestamps (maintaining original order)
    results := make([]time.Time, len(timestamps))
    for i, ts := range timestamps {
    adjusted := time.Date(
    ts.Timestamp.Year(), ts.Timestamp.Month(), ts.Timestamp.Day(),
    ts.Timestamp.Hour(), ts.Timestamp.Minute(), ts.Timestamp.Second(),
    0, fixedOffset,
    )
    results[i] = adjusted
    }
    return results, nil
    }
    // detectDateFormat analyzes filenames to determine the date format
    func detectDateFormat(filenames []string) (DateFormat, error) {
    // Extract all date parts from filenames
    var parts []dateParts
    var has8Digit bool
    for _, filename := range filenames {
    basename := filepath.Base(filename)
    matches := timestampPattern.FindStringSubmatch(basename)
    if matches == nil {
    continue
    }
    dateStr := matches[1]
    // Check for 8-digit format (YYYYMMDD)
    if len(dateStr) == 8 {
    has8Digit = true
    continue
    }
    // Parse 6-digit format
    if len(dateStr) == 6 {
    x1, _ := strconv.Atoi(dateStr[0:2])
    m, _ := strconv.Atoi(dateStr[2:4])
    x2, _ := strconv.Atoi(dateStr[4:6])
    parts = append(parts, dateParts{x1: x1, m: m, x2: x2})
    }
    }
    // If all files are 8-digit, that's the format
    if has8Digit && len(parts) == 0 {
    return Format8Digit, nil
    }
    // If mixed 8-digit and 6-digit, return error
    if has8Digit && len(parts) > 0 {
    return 0, fmt.Errorf("mixed date formats detected (8-digit and 6-digit)")
    }
    // If no 6-digit dates found, cannot determine
    if len(parts) == 0 {
    return 0, fmt.Errorf("no valid timestamp filenames found")
    }
    // Need at least 2 files with different dates to disambiguate YYMMDD vs DDMMYY
    if len(parts) == 1 {
    return 0, fmt.Errorf("need at least 2 files to disambiguate 6-digit date format (YYMMDD vs DDMMYY)")
    }
    // Use variance-based disambiguation for 6-digit dates
    // Compare uniqueness of x1 (first 2 digits) vs x2 (last 2 digits)
    // Day values vary more than year values across recordings
    uniqueX1 := countUnique(parts, func(p dateParts) int { return p.x1 })
    uniqueX2 := countUnique(parts, func(p dateParts) int { return p.x2 })
    if uniqueX2 >= uniqueX1 {
    // x2 has more variance → likely day values → YYMMDD format
    return Format6YYMMDD, nil
    } else {
    // x1 has more variance → likely day values → DDMMYY format
    return Format6DDMMYY, nil
    }
    }
    // parseFilenameWithFormat parses a filename using the specified format
    func parseFilenameWithFormat(filename string, format DateFormat) (time.Time, error) {
    basename := filepath.Base(filename)
    matches := timestampPattern.FindStringSubmatch(basename)
    if matches == nil {
    return time.Time{}, fmt.Errorf("filename does not match timestamp pattern: %s", basename)
    }
    dateStr := matches[1]
    timeStr := matches[2]
    var year, month, day int
    switch format {
    case Format8Digit:
    if len(dateStr) != 8 {
    return time.Time{}, fmt.Errorf("expected 8-digit date, got %d digits", len(dateStr))
    }
    year, _ = strconv.Atoi(dateStr[0:4])
    month, _ = strconv.Atoi(dateStr[4:6])
    day, _ = strconv.Atoi(dateStr[6:8])
    case Format6YYMMDD:
    if len(dateStr) != 6 {
    return time.Time{}, fmt.Errorf("expected 6-digit date, got %d digits", len(dateStr))
    }
    yy, _ := strconv.Atoi(dateStr[0:2])
    month, _ = strconv.Atoi(dateStr[2:4])
    day, _ = strconv.Atoi(dateStr[4:6])
    // Convert 2-digit year to 4-digit (assume 2000-2099)
    year = 2000 + yy
    case Format6DDMMYY:
    if len(dateStr) != 6 {
    return time.Time{}, fmt.Errorf("expected 6-digit date, got %d digits", len(dateStr))
    }
    day, _ = strconv.Atoi(dateStr[0:2])
    month, _ = strconv.Atoi(dateStr[2:4])
    yy, _ := strconv.Atoi(dateStr[4:6])
    // Convert 2-digit year to 4-digit (assume 2000-2099)
    year = 2000 + yy
    }
    // Parse time (HHMMSS)
    if len(timeStr) != 6 {
    return time.Time{}, fmt.Errorf("invalid time format: %s", timeStr)
    }
    hour, _ := strconv.Atoi(timeStr[0:2])
    minute, _ := strconv.Atoi(timeStr[2:4])
    second, _ := strconv.Atoi(timeStr[4:6])
    // Construct timestamp in UTC (timezone applied separately)
    timestamp := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC)
    // Validate date
    if timestamp.Month() != time.Month(month) || timestamp.Day() != day {
    return time.Time{}, fmt.Errorf("invalid date: %04d-%02d-%02d", year, month, day)
    }
    return timestamp, nil
    }
    // countUnique counts unique values using an extractor function
    func countUnique(parts []dateParts, extractor func(p dateParts) int) int {
    seen := make(map[int]bool)
    for _, p := range parts {
    seen[extractor(p)] = true
    }
    return len(seen)
    }
    // HasTimestampFilename checks if a filename contains a timestamp pattern
    func HasTimestampFilename(filename string) bool {
    basename := filepath.Base(filename)
    return timestampPattern.MatchString(basename)
    }
  • file deletion: data_file_test.go (----------)
    [5.1][5.157883:157924](),[5.157924][5.146579:146579]()
    package utils
    import (
    "os"
    "testing"
    )
    func TestDataFileParse(t *testing.T) {
    // Create a test .data file
    content := `[
    {"Operator": "Auto", "Reviewer": null, "Duration": 60.0},
    [10.0, 20.0, 0, 0, [{"species": "Kiwi", "certainty": 70, "filter": "test-filter"}]],
    [30.0, 40.0, 1000, 5000, [{"species": "Morepork", "certainty": 80, "filter": "M"}]]
    ]`
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    defer os.Remove(tmpfile.Name())
    if _, err := tmpfile.Write([]byte(content)); err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    // Parse
    df, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Check metadata
    if df.Meta.Operator != "Auto" {
    t.Errorf("expected Operator=Auto, got %s", df.Meta.Operator)
    }
    if df.Meta.Duration != 60.0 {
    t.Errorf("expected Duration=60.0, got %f", df.Meta.Duration)
    }
    // Check segments
    if len(df.Segments) != 2 {
    t.Errorf("expected 2 segments, got %d", len(df.Segments))
    }
    // Check first segment (sorted by start time)
    if df.Segments[0].StartTime != 10.0 {
    t.Errorf("expected StartTime=10.0, got %f", df.Segments[0].StartTime)
    }
    if df.Segments[0].EndTime != 20.0 {
    t.Errorf("expected EndTime=20.0, got %f", df.Segments[0].EndTime)
    }
    // Check labels
    if len(df.Segments[0].Labels) != 1 {
    t.Errorf("expected 1 label, got %d", len(df.Segments[0].Labels))
    }
    if df.Segments[0].Labels[0].Species != "Kiwi" {
    t.Errorf("expected Species=Kiwi, got %s", df.Segments[0].Labels[0].Species)
    }
    if df.Segments[0].Labels[0].Certainty != 70 {
    t.Errorf("expected Certainty=70, got %d", df.Segments[0].Labels[0].Certainty)
    }
    }
    func TestDataFileWrite(t *testing.T) {
    df := &DataFile{
    FilePath: "",
    Meta: &DataMeta{
    Operator: "Test",
    Reviewer: "David",
    Duration: 120.0,
    },
    Segments: []*Segment{
    {
    StartTime: 5.0,
    EndTime: 15.0,
    FreqLow: 0,
    FreqHigh: 0,
    Labels: []*Label{
    {Species: "Kiwi", Certainty: 100, Filter: "test"},
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse and verify
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df2.Meta.Reviewer != "David" {
    t.Errorf("expected Reviewer=David, got %s", df2.Meta.Reviewer)
    }
    if len(df2.Segments) != 1 {
    t.Errorf("expected 1 segment, got %d", len(df2.Segments))
    }
    if df2.Segments[0].Labels[0].Species != "Kiwi" {
    t.Errorf("expected Species=Kiwi, got %s", df2.Segments[0].Labels[0].Species)
    }
    }
    func TestHasFilterLabel(t *testing.T) {
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "test-filter"},
    {Species: "Morepork", Filter: "M"},
    },
    }
    if !seg.HasFilterLabel("test-filter") {
    t.Error("expected HasFilterLabel(test-filter)=true")
    }
    if !seg.HasFilterLabel("M") {
    t.Error("expected HasFilterLabel(M)=true")
    }
    if seg.HasFilterLabel("other") {
    t.Error("expected HasFilterLabel(other)=false")
    }
    if !seg.HasFilterLabel("") {
    t.Error("expected HasFilterLabel('')=true (no filter)")
    }
    }
    func TestGetFilterLabels(t *testing.T) {
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "test-filter", Certainty: 70},
    {Species: "Morepork", Filter: "M", Certainty: 80},
    {Species: "Don't Know", Filter: "test-filter", Certainty: 0},
    },
    }
    labels := seg.GetFilterLabels("test-filter")
    if len(labels) != 2 {
    t.Errorf("expected 2 labels, got %d", len(labels))
    }
    labels = seg.GetFilterLabels("")
    if len(labels) != 3 {
    t.Errorf("expected 3 labels (no filter), got %d", len(labels))
    }
    }
    func TestLabelComment(t *testing.T) {
    // Test parsing comment from .data file
    content := `[
    {"Operator": "Test", "Duration": 60.0},
    [10.0, 20.0, 0, 0, [{"species": "Kiwi", "certainty": 100, "filter": "M", "comment": "Good call"}]]
    ]`
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    defer os.Remove(tmpfile.Name())
    if _, err := tmpfile.Write([]byte(content)); err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    df, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df.Segments[0].Labels[0].Comment != "Good call" {
    t.Errorf("expected Comment='Good call', got '%s'", df.Segments[0].Labels[0].Comment)
    }
    // Test writing comment
    df.Segments[0].Labels[0].Comment = "Updated comment"
    tmpfile2, err := os.CreateTemp("", "test2*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile2.Close()
    defer os.Remove(tmpfile2.Name())
    if err := df.Write(tmpfile2.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse and verify
    df2, err := ParseDataFile(tmpfile2.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df2.Segments[0].Labels[0].Comment != "Updated comment" {
    t.Errorf("expected Comment='Updated comment', got '%s'", df2.Segments[0].Labels[0].Comment)
    }
    }
    func TestSkraakHashRoundTrip(t *testing.T) {
    // Test that skraak_hash in metadata is preserved through parse/write cycle
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    Extra: map[string]any{
    "skraak_hash": "abc123def456",
    },
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {Species: "Kiwi", Certainty: 100, Filter: "M"},
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_hash preserved
    if df2.Meta.Extra == nil {
    t.Fatal("expected Extra to be non-nil")
    }
    hash, ok := df2.Meta.Extra["skraak_hash"].(string)
    if !ok {
    t.Fatal("expected skraak_hash to be string")
    }
    if hash != "abc123def456" {
    t.Errorf("expected skraak_hash=abc123def456, got %s", hash)
    }
    }
    func TestSkraakLabelIDRoundTrip(t *testing.T) {
    // Test that skraak_label_id in labels is preserved through parse/write cycle
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {
    Species: "Kiwi",
    Certainty: 100,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_abc123",
    },
    },
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_label_id preserved
    if len(df2.Segments) != 1 {
    t.Fatalf("expected 1 segment, got %d", len(df2.Segments))
    }
    if len(df2.Segments[0].Labels) != 1 {
    t.Fatalf("expected 1 label, got %d", len(df2.Segments[0].Labels))
    }
    label := df2.Segments[0].Labels[0]
    if label.Extra == nil {
    t.Fatal("expected label Extra to be non-nil")
    }
    labelID, ok := label.Extra["skraak_label_id"].(string)
    if !ok {
    t.Fatal("expected skraak_label_id to be string")
    }
    if labelID != "label_abc123" {
    t.Errorf("expected skraak_label_id=label_abc123, got %s", labelID)
    }
    }
    func TestSkraakFieldsBothPresent(t *testing.T) {
    // Test both skraak_hash and skraak_label_id together
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    Extra: map[string]any{
    "skraak_hash": "file_hash_xyz",
    },
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {
    Species: "Kiwi",
    Certainty: 100,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_id_1",
    },
    },
    {
    Species: "Roroa",
    Certainty: 90,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_id_2",
    },
    },
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_hash
    if df2.Meta.Extra["skraak_hash"] != "file_hash_xyz" {
    t.Errorf("expected skraak_hash=file_hash_xyz, got %v", df2.Meta.Extra["skraak_hash"])
    }
    // Verify both label IDs
    if len(df2.Segments[0].Labels) != 2 {
    t.Fatalf("expected 2 labels, got %d", len(df2.Segments[0].Labels))
    }
    labelIDs := []string{"label_id_1", "label_id_2"}
    for i, label := range df2.Segments[0].Labels {
    if label.Extra["skraak_label_id"] != labelIDs[i] {
    t.Errorf("label %d: expected skraak_label_id=%s, got %v", i, labelIDs[i], label.Extra["skraak_label_id"])
    }
    }
    }
    func TestSegmentMatchesFilters(t *testing.T) {
    // Create test segments with various labels
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "model-1.0", CallType: "Duet", Certainty: 70},
    {Species: "Morepork", Filter: "model-2.0", CallType: "", Certainty: 100},
    },
    }
    tests := []struct {
    name string
    filter string
    species string
    callType string
    certainty int
    want bool
    }{
    {"no filters", "", "", "", -1, true},
    {"filter only match", "model-1.0", "", "", -1, true},
    {"filter only no match", "model-3.0", "", "", -1, false},
    {"species only match", "", "Kiwi", "", -1, true},
    {"species only no match", "", "Tomtit", "", -1, false},
    {"calltype only match", "", "", "Duet", -1, true},
    {"calltype only no match", "", "", "Male", -1, false},
    {"certainty match", "", "", "", 70, true},
    {"certainty no match", "", "", "", 80, false},
    {"certainty 100 match", "", "", "", 100, true},
    {"filter+species match", "model-1.0", "Kiwi", "", -1, true},
    {"filter+species+calltype match", "model-1.0", "Kiwi", "Duet", -1, true},
    {"filter+species+calltype+certainty match", "model-1.0", "Kiwi", "Duet", 70, true},
    {"filter+species+calltype certainty miss", "model-1.0", "Kiwi", "Duet", 100, false},
    {"filter match species miss", "model-1.0", "Morepork", "", -1, false},
    {"all miss", "model-3.0", "Tomtit", "Male", -1, false},
    }
    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    got := seg.SegmentMatchesFilters(tt.filter, tt.species, tt.callType, tt.certainty)
    if got != tt.want {
    t.Errorf("SegmentMatchesFilters(%q, %q, %q, %d) = %v, want %v",
    tt.filter, tt.species, tt.callType, tt.certainty, got, tt.want)
    }
    })
    }
    }
    func TestParseSpeciesCallType(t *testing.T) {
    tests := []struct {
    input string
    species string
    callType string
    }{
    {"", "", ""},
    {"Kiwi", "Kiwi", ""},
    {"Kiwi+Duet", "Kiwi", "Duet"},
    {"GSK+Female", "GSK", "Female"},
    {"Species+With+Multiple+Plus", "Species", "With+Multiple+Plus"},
    }
    for _, tt := range tests {
    t.Run(tt.input, func(t *testing.T) {
    species, callType := ParseSpeciesCallType(tt.input)
    if species != tt.species || callType != tt.callType {
    t.Errorf("ParseSpeciesCallType(%q) = (%q, %q), want (%q, %q)",
    tt.input, species, callType, tt.species, tt.callType)
    }
    })
    }
    }
    {"Kiwi+_", "Kiwi", "_"},
    {"CallTypeNone matches empty calltype", "model-2.0", "Morepork", CallTypeNone, -1, true},
    {"CallTypeNone skips non-empty calltype", "model-1.0", "Kiwi", CallTypeNone, -1, false},
    {"CallTypeNone + certainty match", "model-2.0", "Morepork", CallTypeNone, 100, true},
    {"CallTypeNone + certainty miss", "model-2.0", "Morepork", CallTypeNone, 70, false},
  • file deletion: data_file.go (----------)
    [5.1][5.165763:165799](),[5.165799][5.157926:157926]()
    package utils
    import (
    "encoding/json"
    "fmt"
    "maps"
    "os"
    "sort"
    "strings"
    )
    // DataFile represents an AviaNZ .data file
    type DataFile struct {
    Meta *DataMeta
    Segments []*Segment
    FilePath string
    }
    // DataMeta contains metadata for a .data file
    type DataMeta struct {
    Operator string
    Reviewer string
    Duration float64
    Extra map[string]any // preserve unknown fields
    }
    // Segment represents a detection segment
    type Segment struct {
    StartTime float64
    EndTime float64
    FreqLow float64
    FreqHigh float64
    Labels []*Label
    }
    // Label represents a species label within a segment
    type Label struct {
    Species string
    Certainty int
    Filter string
    CallType string
    Comment string // user comment (max 140 chars, ASCII only)
    Bookmark bool // user bookmark for navigation
    Extra map[string]any // preserve unknown fields
    }
    // ParseDataFile reads and parses a .data file
    func ParseDataFile(path string) (*DataFile, error) {
    data, err := os.ReadFile(path)
    if err != nil {
    return nil, err
    }
    var raw []json.RawMessage
    if err := json.Unmarshal(data, &raw); err != nil {
    return nil, fmt.Errorf("parse JSON: %w", err)
    }
    if len(raw) == 0 {
    return nil, fmt.Errorf("empty .data file")
    }
    df := &DataFile{
    FilePath: path,
    Segments: make([]*Segment, 0, len(raw)-1),
    }
    // Parse metadata (first element)
    df.Meta = parseMeta(raw[0])
    // Parse segments
    for i := 1; i < len(raw); i++ {
    seg, err := parseSegment(raw[i])
    if err != nil {
    continue // skip invalid segments
    }
    df.Segments = append(df.Segments, seg)
    }
    // Sort segments by start time
    sort.Slice(df.Segments, func(i, j int) bool {
    return df.Segments[i].StartTime < df.Segments[j].StartTime
    })
    return df, nil
    }
    // parseMeta parses the metadata object
    func parseMeta(raw json.RawMessage) *DataMeta {
    var obj map[string]any
    if err := json.Unmarshal(raw, &obj); err != nil {
    return &DataMeta{}
    }
    meta := &DataMeta{Extra: make(map[string]any)}
    if v, ok := obj["Operator"].(string); ok {
    meta.Operator = v
    delete(obj, "Operator")
    }
    if v, ok := obj["Reviewer"].(string); ok {
    meta.Reviewer = v
    delete(obj, "Reviewer")
    }
    if v, ok := obj["Duration"].(float64); ok {
    meta.Duration = v
    delete(obj, "Duration")
    }
    // Store remaining fields
    maps.Copy(meta.Extra, obj)
    return meta
    }
    // parseSegment parses a segment array
    func parseSegment(raw json.RawMessage) (*Segment, error) {
    var arr []json.RawMessage
    if err := json.Unmarshal(raw, &arr); err != nil {
    return nil, err
    }
    if len(arr) < 5 {
    return nil, fmt.Errorf("segment too short")
    }
    seg := &Segment{}
    // Parse time and frequency
    if v, err := parseFloat(arr[0]); err == nil {
    seg.StartTime = v
    }
    if v, err := parseFloat(arr[1]); err == nil {
    seg.EndTime = v
    }
    if v, err := parseFloat(arr[2]); err == nil {
    seg.FreqLow = v
    }
    if v, err := parseFloat(arr[3]); err == nil {
    seg.FreqHigh = v
    }
    // Parse labels
    var labelArr []json.RawMessage
    if err := json.Unmarshal(arr[4], &labelArr); err == nil {
    for _, labelRaw := range labelArr {
    if label := parseLabel(labelRaw); label != nil {
    seg.Labels = append(seg.Labels, label)
    }
    }
    }
    // Sort labels alphabetically by species
    sort.Slice(seg.Labels, func(i, j int) bool {
    return seg.Labels[i].Species < seg.Labels[j].Species
    })
    return seg, nil
    }
    // parseLabel parses a label object
    func parseLabel(raw json.RawMessage) *Label {
    var obj map[string]any
    if err := json.Unmarshal(raw, &obj); err != nil {
    return nil
    }
    label := &Label{Extra: make(map[string]any)}
    if v, ok := obj["species"].(string); ok {
    label.Species = v
    delete(obj, "species")
    }
    if v, ok := obj["certainty"].(float64); ok {
    label.Certainty = int(v)
    delete(obj, "certainty")
    }
    if v, ok := obj["filter"].(string); ok {
    label.Filter = v
    delete(obj, "filter")
    }
    if v, ok := obj["calltype"].(string); ok {
    label.CallType = v
    delete(obj, "calltype")
    }
    if v, ok := obj["comment"].(string); ok {
    label.Comment = v
    delete(obj, "comment")
    }
    if v, ok := obj["bookmark"].(bool); ok {
    label.Bookmark = v
    delete(obj, "bookmark")
    }
    // Store remaining fields
    maps.Copy(label.Extra, obj)
    return label
    }
    // parseFloat extracts a float from JSON
    func parseFloat(raw json.RawMessage) (float64, error) {
    var v float64
    err := json.Unmarshal(raw, &v)
    return v, err
    }
    // WriteDataFile writes a DataFile back to disk
    func (df *DataFile) Write(path string) error {
    var raw []any
    // Build metadata
    meta := make(map[string]any)
    if df.Meta.Operator != "" {
    meta["Operator"] = df.Meta.Operator
    }
    if df.Meta.Reviewer != "" {
    meta["Reviewer"] = df.Meta.Reviewer
    }
    if df.Meta.Duration > 0 {
    meta["Duration"] = df.Meta.Duration
    }
    maps.Copy(meta, df.Meta.Extra)
    raw = append(raw, meta)
    // Build segments
    for _, seg := range df.Segments {
    labels := make([]any, 0, len(seg.Labels))
    for _, label := range seg.Labels {
    l := make(map[string]any)
    l["species"] = label.Species
    l["certainty"] = label.Certainty
    if label.Filter != "" {
    l["filter"] = label.Filter
    }
    if label.CallType != "" {
    l["calltype"] = label.CallType
    }
    if label.Comment != "" {
    l["comment"] = label.Comment
    }
    if label.Bookmark {
    l["bookmark"] = true
    }
    maps.Copy(l, label.Extra)
    labels = append(labels, l)
    }
    segArr := []any{
    seg.StartTime,
    seg.EndTime,
    seg.FreqLow,
    seg.FreqHigh,
    labels,
    }
    raw = append(raw, segArr)
    }
    data, err := json.MarshalIndent(raw, "", " ")
    if err != nil {
    return err
    }
    return os.WriteFile(path, data, 0644)
    }
    // HasFilterLabel returns true if segment has a label matching the filter
    func (s *Segment) HasFilterLabel(filter string) bool {
    if filter == "" {
    return true
    }
    for _, label := range s.Labels {
    if label.Filter == filter {
    return true
    }
    }
    return false
    }
    // GetFilterLabels returns labels matching the filter
    func (s *Segment) GetFilterLabels(filter string) []*Label {
    var result []*Label
    for _, label := range s.Labels {
    if filter == "" || label.Filter == filter {
    result = append(result, label)
    }
    }
    return result
    }
    // SegmentMatchesFilters returns true if the segment has any label matching all filter criteria.
    // All non-empty/non-negative parameters must match for a label to be considered a match.
    // Use certainty=-1 to indicate no certainty filtering (since 0 is a valid certainty value).
    func (s *Segment) SegmentMatchesFilters(filter, species, callType string, certainty int) bool {
    if filter == "" && species == "" && callType == "" && certainty < 0 {
    return true // No filters, match all
    }
    for _, label := range s.Labels {
    if labelMatchesFilters(label, filter, species, callType, certainty) {
    return true
    }
    }
    return false
    }
    // labelMatchesFilters checks if a single label matches all filter criteria.
    func labelMatchesFilters(label *Label, filter, species, callType string, certainty int) bool {
    if filter != "" && label.Filter != filter {
    return false
    }
    if species != "" && label.Species != species {
    return false
    }
    if callType == CallTypeNone {
    if label.CallType != "" {
    return false
    }
    } else if callType != "" && label.CallType != callType {
    return false
    }
    if certainty >= 0 && label.Certainty != certainty {
    return false
    }
    return true
    }
    // ParseSpeciesCallType parses a species string with optional calltype into separate values.
    // Format: "Species" or "Species+CallType" (e.g., "Kiwi" or "Kiwi+Duet").
    func ParseSpeciesCallType(label string) (species, callType string) {
    if label == "" {
    return "", ""
    }
    if before, after, ok := strings.Cut(label, "+"); ok {
    return before, after
    }
    return label, ""
    }
    // FindDataFiles finds all .data files in a folder, ignoring hidden files (starting with ".")
    func FindDataFiles(folder string) ([]string, error) {
    return FindFiles(folder, FindFilesOptions{
    Extension: ".data",
    Recursive: false,
    SkipHidden: true,
    })
    }
    // Use "_" as the calltype to match only labels with no calltype (e.g., "Kiwi+_").
    // CallTypeNone is a sentinel value used in --species Species+_ to match
    // only labels with an empty calltype.
    const CallTypeNone = "_"
  • file deletion: config.go (----------)
    [5.1][5.169649:169682](),[5.169682][5.165801:165801]()
    package utils
    import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    )
    // ~/.skraak/config.json schema (reference):
    //
    // {
    // "classify": {
    // "reviewer": "string, required. Name stamped into .data file meta on any edit.",
    // "color": "bool, optional. Colored spectrograms in the TUI. Default false.",
    // "sixel": "bool, optional. Use sixel image protocol. Default false (Kitty).",
    // "iterm": "bool, optional. Use iTerm inline-image protocol. Default false.",
    // "img_dims": "int, optional. Spectrogram display size in pixels. 0 = default.",
    //
    // "bindings": {
    // "<key>": "Species" // e.g. "c": "comcha"
    // "<key>": "Species+CallType" // e.g. "1": "Kiwi+Duet"
    // // <key> is a single character. Reserved: ",", ".", "0", " " (space).
    // // Pressing <key> labels the current segment (certainty 100, or 0 for
    // // "Don't Know"), saves, and advances.
    // },
    //
    // "secondary_bindings": {
    // "<primary-key>": {
    // "<key>": "CallType" // e.g. "a": "alarm"
    // // <key> is a single character, same reserved-key rules as bindings.
    // // Outer <primary-key> must also exist in "bindings".
    // }
    // // Optional. Invoked via Shift+<primary-key>: labels the species with
    // // an empty calltype, does NOT advance, and waits for one follow-up
    // // key looked up in this inner map. Match -> set calltype, save,
    // // advance. Esc -> exit wait mode without advancing. Any other key ->
    // // exit wait mode and handle the key normally.
    // // Shift+<primary-key> on a primary without a secondary_bindings entry
    // // falls back to normal primary behavior.
    // }
    // }
    // }
    //
    // Example:
    //
    // {
    // "classify": {
    // "reviewer": "David",
    // "color": true,
    // "bindings": {
    // "c": "comcha",
    // "k": "kea1",
    // "x": "Noise",
    // "z": "Don't Know",
    // "1": "Kiwi+Duet",
    // "4": "Kiwi"
    // },
    // "secondary_bindings": {
    // "c": { "a": "alarm", "s": "song", "n": "contact" }
    // }
    // }
    // }
    //
    // Config holds user-level defaults loaded from ~/.skraak/config.json.
    // Per-subcommand sections live as named fields.
    type Config struct {
    Classify ClassifyFileConfig `json:"classify"`
    }
    // ClassifyFileConfig holds defaults for `skraak calls classify`.
    // Bindings maps a single-character key to "Species" or "Species+CallType".
    type ClassifyFileConfig struct {
    Reviewer string `json:"reviewer"`
    Color bool `json:"color"`
    Sixel bool `json:"sixel"`
    ITerm bool `json:"iterm"`
    ImgDims int `json:"img_dims"`
    Bindings map[string]string `json:"bindings"`
    // SecondaryBindings extends a primary binding with per-species calltype
    // choices. Outer key is the primary binding key; inner map is
    // single-char key -> calltype string. Invoked via Shift+primary-key.
    SecondaryBindings map[string]map[string]string `json:"secondary_bindings,omitempty"`
    }
    // ConfigPath returns the absolute path to ~/.skraak/config.json.
    func ConfigPath() (string, error) {
    home, err := os.UserHomeDir()
    if err != nil {
    return "", fmt.Errorf("resolving home directory: %w", err)
    }
    return filepath.Join(home, ".skraak", "config.json"), nil
    }
    // LoadConfig reads ~/.skraak/config.json and returns the parsed config and the
    // resolved path (useful for error messages).
    func LoadConfig() (Config, string, error) {
    var cfg Config
    path, err := ConfigPath()
    if err != nil {
    return cfg, "", err
    }
    data, err := os.ReadFile(path)
    if err != nil {
    return cfg, path, fmt.Errorf("reading %s: %w", path, err)
    }
    if err := json.Unmarshal(data, &cfg); err != nil {
    return cfg, path, fmt.Errorf("parsing %s: %w", path, err)
    }
    return cfg, path, nil
    }
  • file deletion: clip_times_test.go (----------)
    [5.1][5.189880:189922](),[5.189922][5.186690:186690]()
    package utils
    import (
    "math"
    "testing"
    )
    // Reference values verified against opensoundscape.utils.generate_clip_times_df
    // at https://github.com/kitzeslab/opensoundscape/blob/master/opensoundscape/utils.py
    func TestGenerateClipTimes_FullModeBasic(t *testing.T) {
    // full_duration=10, clip_duration=4, overlap=0.5, final="full"
    // increment = 3.5
    // raw starts: 0, 3.5, 7 (next would be 10.5 ≥ 10)
    // raw ends: 4, 7.5, 11
    // "full": last clip start shifts back by (11-10)=1 → start=6, end=10
    // → [(0,4), (3.5,7.5), (6,10)]
    got, err := GenerateClipTimes(10, 4, 0.5, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    want := []ClipWindow{{0, 4}, {3.5, 7.5}, {6, 10}}
    assertClips(t, got, want)
    }
    func TestGenerateClipTimes_NoneMode(t *testing.T) {
    // final="none": drop any clip whose end > full_duration.
    // full=10, dur=4, overlap=0: starts 0,4,8; ends 4,8,12 → keep (0,4),(4,8)
    got, err := GenerateClipTimes(10, 4, 0, FinalClipNone, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}})
    }
    func TestGenerateClipTimes_RemainderMode(t *testing.T) {
    // full=10, dur=4, overlap=0: starts 0,4,8; ends 4,8,12
    // remainder: trim 12 → 10. → (0,4),(4,8),(8,10)
    got, err := GenerateClipTimes(10, 4, 0, FinalClipRemainder, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}, {8, 10}})
    }
    func TestGenerateClipTimes_ExtendMode(t *testing.T) {
    got, err := GenerateClipTimes(10, 4, 0, FinalClipExtend, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}, {8, 12}})
    }
    func TestGenerateClipTimes_AudioShorterThanClip(t *testing.T) {
    // full=2, dur=4, overlap=0, final="full":
    // raw start=0, end=4; end > full=2 → start shifts to 0-(4-2)=-2 → clamped to 0;
    // end=2 → single clip (0,2)
    got, err := GenerateClipTimes(2, 4, 0, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 2}})
    }
    func TestGenerateClipTimes_DedupAfterFullShift(t *testing.T) {
    // full=8, dur=4, overlap=0:
    // raw starts 0,4; ends 4,8 — no shift needed; output (0,4),(4,8).
    // (Tests the no-duplicate path.)
    got, err := GenerateClipTimes(8, 4, 0, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}})
    }
    func TestGenerateClipTimes_InvalidArgs(t *testing.T) {
    _, err := GenerateClipTimes(10, 0, 0, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for clip_duration=0")
    }
    _, err = GenerateClipTimes(10, 4, 4, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for clip_overlap >= clip_duration")
    }
    _, err = GenerateClipTimes(0, 4, 0, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for full_duration=0")
    }
    }
    func assertClips(t *testing.T, got, want []ClipWindow) {
    t.Helper()
    if len(got) != len(want) {
    t.Fatalf("len(got)=%d, len(want)=%d\ngot=%v\nwant=%v", len(got), len(want), got, want)
    }
    for i := range got {
    if math.Abs(got[i].Start-want[i].Start) > 1e-9 || math.Abs(got[i].End-want[i].End) > 1e-9 {
    t.Errorf("clip %d: got (%v,%v), want (%v,%v)", i, got[i].Start, got[i].End, want[i].Start, want[i].End)
    }
    }
    }
    func TestParseFinalClipMode(t *testing.T) {
    tests := []struct {
    input string
    want FinalClipMode
    err bool
    }{
    {"none", FinalClipNone, false},
    {"", FinalClipNone, false},
    {"remainder", FinalClipRemainder, false},
    {"full", FinalClipFull, false},
    {"extend", FinalClipExtend, false},
    {"invalid", 0, true},
    {"FULL", 0, true}, // case-sensitive
    }
    for _, tt := range tests {
    t.Run(tt.input, func(t *testing.T) {
    got, err := ParseFinalClipMode(tt.input)
    if tt.err {
    if err == nil {
    t.Error("expected error")
    }
    } else {
    if err != nil {
    t.Errorf("unexpected error: %v", err)
    }
    if got != tt.want {
    t.Errorf("got %d, want %d", got, tt.want)
    }
    }
    })
    }
    }
  • file deletion: clip_times.go (----------)
    [5.1][5.194747:194784](),[5.194784][5.189924:189924]()
    package utils
    import (
    "fmt"
    "math"
    )
    // ClipWindow is a fixed-duration time window for one audio file.
    type ClipWindow struct {
    Start float64
    End float64
    }
    // FinalClipMode controls how the trailing partial clip is handled.
    // Mirrors opensoundscape.utils.generate_clip_times_df:
    // - FinalClipNone: discard any clip whose end exceeds full_duration
    // - FinalClipRemainder: trim the final clip's end to full_duration (shorter clip)
    // - FinalClipFull: shift the final clip's start back so its end equals full_duration
    // - FinalClipExtend: keep the final clip extending beyond full_duration
    type FinalClipMode int
    const (
    FinalClipNone FinalClipMode = iota
    FinalClipRemainder
    FinalClipFull
    FinalClipExtend
    )
    // ParseFinalClipMode parses a CLI flag value.
    func ParseFinalClipMode(s string) (FinalClipMode, error) {
    switch s {
    case "none", "":
    return FinalClipNone, nil
    case "remainder":
    return FinalClipRemainder, nil
    case "full":
    return FinalClipFull, nil
    case "extend":
    return FinalClipExtend, nil
    default:
    return 0, fmt.Errorf("invalid final-clip mode %q (want one of: none, remainder, full, extend)", s)
    }
    }
    // roundTo rounds x to `precision` decimal places. Mirrors numpy.round behaviour.
    // Pass precision < 0 to skip rounding.
    func roundTo(x float64, precision int) float64 {
    if precision < 0 {
    return x
    }
    scale := math.Pow(10, float64(precision))
    return math.Round(x*scale) / scale
    }
    // GenerateClipTimes ports opensoundscape.utils.generate_clip_times_df.
    //
    // Args mirror the Python signature: clipDuration > 0, clipOverlap in [0, clipDuration),
    // fullDuration > 0. roundingPrecision defaults to 10 in OPSO; pass -1 to skip rounding.
    //
    // Result is the list of (start, end) windows for one audio file, with duplicates
    // removed (which can happen under FinalClipFull when the shifted final clip
    // coincides with the previous one).
    func GenerateClipTimes(fullDuration, clipDuration, clipOverlap float64, finalClip FinalClipMode, roundingPrecision int) ([]ClipWindow, error) {
    if clipDuration <= 0 {
    return nil, fmt.Errorf("clipDuration must be > 0, got %v", clipDuration)
    }
    if clipOverlap < 0 || clipOverlap >= clipDuration {
    return nil, fmt.Errorf("clipOverlap must be in [0, clipDuration), got %v with clipDuration=%v", clipOverlap, clipDuration)
    }
    if fullDuration <= 0 {
    return nil, fmt.Errorf("fullDuration must be > 0, got %v", fullDuration)
    }
    starts, ends := buildClipStartsEnds(fullDuration, clipDuration, clipOverlap, roundingPrecision)
    switch finalClip {
    case FinalClipNone:
    return dedupClips(clipWindowsNone(starts, ends, fullDuration)), nil
    case FinalClipRemainder:
    return dedupClips(clipWindowsRemainder(starts, ends, fullDuration)), nil
    case FinalClipFull:
    return dedupClips(clipWindowsFull(starts, ends, fullDuration)), nil
    case FinalClipExtend:
    return dedupClips(clipWindowsExtend(starts, ends)), nil
    default:
    return nil, fmt.Errorf("invalid FinalClipMode %d", finalClip)
    }
    }
    // buildClipStartsEnds generates the start and end arrays for clips.
    func buildClipStartsEnds(fullDuration, clipDuration, clipOverlap float64, roundingPrecision int) ([]float64, []float64) {
    increment := clipDuration - clipOverlap
    var starts []float64
    for s := 0.0; s < fullDuration; s += increment {
    starts = append(starts, roundTo(s, roundingPrecision))
    }
    if len(starts) == 0 {
    starts = []float64{0}
    }
    ends := make([]float64, len(starts))
    for i, s := range starts {
    ends[i] = s + clipDuration
    }
    // clipWindowsNone drops any window whose end exceeds fullDuration.
    func clipWindowsNone(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    if ends[i] <= fullDuration {
    out = append(out, ClipWindow{Start: starts[i], End: ends[i]})
    }
    }
    return out
    }
    // clipWindowsRemainder trims ends beyond fullDuration down to fullDuration.
    func clipWindowsRemainder(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    e := ends[i]
    if e > fullDuration {
    e = fullDuration
    }
    out = append(out, ClipWindow{Start: starts[i], End: e})
    }
    return out
    }
    // clipWindowsFull shifts windows whose end exceeds fullDuration back so end == fullDuration.
    func clipWindowsFull(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    s, e := starts[i], ends[i]
    if e > fullDuration {
    s -= e - fullDuration
    e = fullDuration
    if s < 0 {
    s = 0
    }
    }
    out = append(out, ClipWindow{Start: s, End: e})
    }
    return out
    }
    // clipWindowsExtend keeps ends as-is, even past fullDuration.
    func clipWindowsExtend(starts, ends []float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    out = append(out, ClipWindow{Start: starts[i], End: ends[i]})
    }
    }
    // dedupClips removes consecutive duplicates while preserving order.
    // Matches pandas.DataFrame.drop_duplicates() at the end of OPSO's
    // generate_clip_times_df.
    func dedupClips(in []ClipWindow) []ClipWindow {
    if len(in) <= 1 {
    return in
    }
    seen := make(map[ClipWindow]bool, len(in))
    out := make([]ClipWindow, 0, len(in))
    for _, c := range in {
    if !seen[c] {
    seen[c] = true
    out = append(out, c)
    }
    }
    return out
    }
    return out
    return starts, ends
    }
  • edit in tui/view.go at line 9
    [5.75]
    [5.44048]
    "skraak/datafile"
  • edit in tui/view.go at line 12
    [5.97][5.97:113]()
    "skraak/utils"
  • replacement in tui/view.go at line 85
    [5.2022][5.2022:2117]()
    func (m Model) renderSegmentInfo(b *strings.Builder, df *utils.DataFile, seg *utils.Segment) {
    [5.2022]
    [5.2117]
    func (m Model) renderSegmentInfo(b *strings.Builder, df *datafile.DataFile, seg *datafile.Segment) {
  • replacement in tui/view.go at line 105
    [5.2655][5.2655:2725]()
    func (m Model) renderLabels(b *strings.Builder, seg *utils.Segment) {
    [5.2655]
    [5.2725]
    func (m Model) renderLabels(b *strings.Builder, seg *datafile.Segment) {
  • replacement in tui/view.go at line 111
    [5.2916][5.2916:3010]()
    fmt.Fprintf(b, " • %s\n", calls.FormatLabels([]*utils.Label{l}, m.state.Config.Filter))
    [5.2916]
    [5.3010]
    fmt.Fprintf(b, " • %s\n", calls.FormatLabels([]*datafile.Label{l}, m.state.Config.Filter))
  • edit in tui/update.go at line 13
    [5.25153]
    [5.44121]
    "skraak/datafile"
  • edit in tui/update.go at line 16
    [5.3972][5.6306:6322]()
    "skraak/utils"
  • replacement in tui/update.go at line 446
    [5.8300][5.8300:8391]()
    func (m Model) generateSpectrogramImage(dataPath string, seg *utils.Segment) image.Image {
    [5.8300]
    [5.8391]
    func (m Model) generateSpectrogramImage(dataPath string, seg *datafile.Segment) image.Image {
  • edit in tools/import/mapping.go at line 5
    [5.10117][5.10117:10133]()
    "database/sql"
  • edit in tools/import/mapping.go at line 8
    [5.10149]
    [5.10149]
    "skraak/db"
  • edit in tools/import/mapping.go at line 10
    [5.10167][5.10167:10183]()
    "skraak/utils"
  • edit in tools/import/mapping.go at line 38
    [5.10715][5.10715:11033]()
    // MappingQuerier is the read-only interface needed for mapping validation.
    // Satisfied by *sql.DB, *sql.Tx, and *db.LoggedTx.
    type MappingQuerier interface {
    QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
    QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
    }
  • replacement in tools/import/mapping.go at line 42
    [5.11248][5.11248:11273]()
    queryer MappingQuerier,
    [5.11248]
    [5.11273]
    queryer Reader,
  • replacement in tools/import/mapping.go at line 130
    [5.13814][5.13814:13941]()
    func validateMappedSpecies(queryer MappingQuerier, mappedSpeciesSet map[string]bool, result *mapping.ValidationResult) error {
    [5.13814]
    [5.13941]
    func validateMappedSpecies(queryer Reader, mappedSpeciesSet map[string]bool, result *mapping.ValidationResult) error {
  • replacement in tools/import/mapping.go at line 141
    [5.14158][5.14158:14278]()
    query := `SELECT label FROM species WHERE label IN (` + utils.Placeholders(len(speciesLabels)) + `) AND active = true`
    [5.14158]
    [5.14278]
    query := `SELECT label FROM species WHERE label IN (` + db.Placeholders(len(speciesLabels)) + `) AND active = true`
  • replacement in tools/import/mapping.go at line 170
    [5.14928][5.14928:15069]()
    func validateMappedCalltypes(queryer MappingQuerier, mappedCalltypes map[string]map[string]string, result *mapping.ValidationResult) error {
    [5.14928]
    [5.15069]
    func validateMappedCalltypes(queryer Reader, mappedCalltypes map[string]map[string]string, result *mapping.ValidationResult) error {
  • replacement in tools/import/mapping.go at line 186
    [5.15392][5.15392:15495]()
    WHERE s.label = ? AND ct.label IN (` + utils.Placeholders(len(ctLabels)) + `) AND ct.active = true`
    [5.15392]
    [5.15495]
    WHERE s.label = ? AND ct.label IN (` + db.Placeholders(len(ctLabels)) + `) AND ct.active = true`
  • edit in tools/import/import_segments_validation_test.go at line 9
    [5.69]
    [5.69]
    "skraak/datafile"
  • replacement in tools/import/import_segments_validation_test.go at line 233
    [5.7613][5.7613:7701]()
    Segments: []*utils.Segment{{StartTime: 1.0, EndTime: 2.0, Labels: []*utils.Label{}}},
    [5.7613]
    [5.7701]
    Segments: []*datafile.Segment{{StartTime: 1.0, EndTime: 2.0, Labels: []*datafile.Label{}}},
  • replacement in tools/import/import_segments_validation_test.go at line 259
    [5.8355][5.8355:8443]()
    Segments: []*utils.Segment{{StartTime: 1.0, EndTime: 2.0, Labels: []*utils.Label{}}},
    [5.8355]
    [5.8443]
    Segments: []*datafile.Segment{{StartTime: 1.0, EndTime: 2.0, Labels: []*datafile.Label{}}},
  • replacement in tools/import/import_segments_test.go at line 6
    [5.7428][5.7428:7444]()
    "skraak/utils"
    [5.7428]
    [5.7444]
    "skraak/datafile"
  • replacement in tools/import/import_segments_test.go at line 74
    [5.9133][5.9133:9177]()
    "file1": {Segments: []*utils.Segment{}},
    [5.9133]
    [5.9177]
    "file1": {Segments: []*datafile.Segment{}},
  • replacement in tools/import/import_segments_test.go at line 84
    [5.9388][5.9388:9442]()
    "file1": {Segments: []*utils.Segment{{}, {}, {}}},
    [5.9388]
    [5.9442]
    "file1": {Segments: []*datafile.Segment{{}, {}, {}}},
  • replacement in tools/import/import_segments_test.go at line 94
    [5.9636][5.9636:9790]()
    "file1": {Segments: []*utils.Segment{{}, {}}},
    "file2": {Segments: []*utils.Segment{{}}},
    "file3": {Segments: []*utils.Segment{{}, {}, {}, {}}},
    [5.9636]
    [5.9790]
    "file1": {Segments: []*datafile.Segment{{}, {}}},
    "file2": {Segments: []*datafile.Segment{{}}},
    "file3": {Segments: []*datafile.Segment{{}, {}, {}, {}}},
  • edit in tools/import/import_segments.go at line 12
    [5.10050]
    [5.10050]
    "skraak/datafile"
  • replacement in tools/import/import_segments.go at line 81
    [5.12410][5.12410:12437]()
    Segments []*utils.Segment
    [5.12410]
    [5.12437]
    Segments []*datafile.Segment
  • replacement in tools/import/import_segments.go at line 164
    [5.15384][5.15384:15437]()
    dataFiles, err := utils.FindDataFiles(input.Folder)
    [5.15384]
    [5.15437]
    dataFiles, err := datafile.FindDataFiles(input.Folder)
  • replacement in tools/import/import_segments.go at line 286
    [5.19562][5.19562:19605]()
    df, err := utils.ParseDataFile(dataPath)
    [5.19562]
    [5.19605]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/import/import_segments.go at line 588
    [5.27379][5.27379:27400]()
    label *utils.Label,
    [5.27379]
    [5.27400]
    label *datafile.Label,
  • replacement in tools/import/import_segments.go at line 682
    [5.30600][5.30600:30621]()
    label *utils.Label,
    [5.30600]
    [5.30621]
    label *datafile.Label,
  • replacement in tools/import/import_segments.go at line 809
    [5.34445][5.34445:34466]()
    seg *utils.Segment,
    [5.34445]
    [5.34466]
    seg *datafile.Segment,
  • replacement in tools/import/import_segments.go at line 898
    [5.37416][5.37416:37462]()
    df, err := utils.ParseDataFile(fu.DataPath)
    [5.37416]
    [5.37462]
    df, err := datafile.ParseDataFile(fu.DataPath)
  • replacement in tools/import/cluster_import.go at line 25
    [5.18398][5.18398:18512]()
    // Reader is a read-only interface for database queries.
    // Both *sql.DB and *db.LoggedTx satisfy this interface.
    [5.18398]
    [5.18512]
    // Reader is the read-only interface for database queries within tools/import.
    // Both *sql.DB and *db.LoggedTx satisfy it.
  • edit in tools/import/cluster_import.go at line 28
    [5.18536]
    [5.18536]
    QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
  • replacement in tools/import/cluster_import.go at line 235
    [5.24616][5.24616:24685]()
    filenameTimestamps, err := utils.ParseFilenameTimestamps(filenames)
    [5.24616]
    [5.24685]
    filenameTimestamps, err := wav.ParseFilenameTimestamps(filenames)
  • replacement in tools/import/cluster_import.go at line 247
    [5.24972][5.24972:25058]()
    adjustedTimestamps, err := utils.ApplyTimezoneOffset(filenameTimestamps, timezoneID)
    [5.24972]
    [5.25058]
    adjustedTimestamps, err := wav.ApplyTimezoneOffset(filenameTimestamps, timezoneID)
  • replacement in tools/import/cluster_import.go at line 317
    [5.27192][5.27192:27237]()
    if utils.HasTimestampFilename(info.path) {
    [5.27192]
    [5.27237]
    if wav.HasTimestampFilename(info.path) {
  • edit in tools/calls/isnight.go at line 11
    [3.8408][5.69799:69815](),[5.69799][5.69799:69815]()
    "skraak/utils"
  • replacement in tools/calls/isnight.go at line 67
    [5.71771][5.71771:71827]()
    } else if utils.HasTimestampFilename(input.FilePath) {
    [5.71771]
    [5.71827]
    } else if wav.HasTimestampFilename(input.FilePath) {
  • file addition: clip_times_test.go (----------)
    [5.67281]
    package calls
    import (
    "math"
    "testing"
    )
    // Reference values verified against opensoundscape.utils.generate_clip_times_df
    // at https://github.com/kitzeslab/opensoundscape/blob/master/opensoundscape/utils.py
    func TestGenerateClipTimes_FullModeBasic(t *testing.T) {
    // full_duration=10, clip_duration=4, overlap=0.5, final="full"
    // increment = 3.5
    // raw starts: 0, 3.5, 7 (next would be 10.5 ≥ 10)
    // raw ends: 4, 7.5, 11
    // "full": last clip start shifts back by (11-10)=1 → start=6, end=10
    // → [(0,4), (3.5,7.5), (6,10)]
    got, err := GenerateClipTimes(10, 4, 0.5, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    want := []ClipWindow{{0, 4}, {3.5, 7.5}, {6, 10}}
    assertClips(t, got, want)
    }
    func TestGenerateClipTimes_NoneMode(t *testing.T) {
    // final="none": drop any clip whose end > full_duration.
    // full=10, dur=4, overlap=0: starts 0,4,8; ends 4,8,12 → keep (0,4),(4,8)
    got, err := GenerateClipTimes(10, 4, 0, FinalClipNone, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}})
    }
    func TestGenerateClipTimes_RemainderMode(t *testing.T) {
    // full=10, dur=4, overlap=0: starts 0,4,8; ends 4,8,12
    // remainder: trim 12 → 10. → (0,4),(4,8),(8,10)
    got, err := GenerateClipTimes(10, 4, 0, FinalClipRemainder, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}, {8, 10}})
    }
    func TestGenerateClipTimes_ExtendMode(t *testing.T) {
    got, err := GenerateClipTimes(10, 4, 0, FinalClipExtend, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}, {8, 12}})
    }
    func TestGenerateClipTimes_AudioShorterThanClip(t *testing.T) {
    // full=2, dur=4, overlap=0, final="full":
    // raw start=0, end=4; end > full=2 → start shifts to 0-(4-2)=-2 → clamped to 0;
    // end=2 → single clip (0,2)
    got, err := GenerateClipTimes(2, 4, 0, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 2}})
    }
    func TestGenerateClipTimes_DedupAfterFullShift(t *testing.T) {
    // full=8, dur=4, overlap=0:
    // raw starts 0,4; ends 4,8 — no shift needed; output (0,4),(4,8).
    // (Tests the no-duplicate path.)
    got, err := GenerateClipTimes(8, 4, 0, FinalClipFull, 10)
    if err != nil {
    t.Fatal(err)
    }
    assertClips(t, got, []ClipWindow{{0, 4}, {4, 8}})
    }
    func TestGenerateClipTimes_InvalidArgs(t *testing.T) {
    _, err := GenerateClipTimes(10, 0, 0, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for clip_duration=0")
    }
    _, err = GenerateClipTimes(10, 4, 4, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for clip_overlap >= clip_duration")
    }
    _, err = GenerateClipTimes(0, 4, 0, FinalClipFull, 10)
    if err == nil {
    t.Error("expected error for full_duration=0")
    }
    }
    func TestParseFinalClipMode(t *testing.T) {
    tests := []struct {
    input string
    want FinalClipMode
    err bool
    }{
    {"none", FinalClipNone, false},
    {"", FinalClipNone, false},
    {"remainder", FinalClipRemainder, false},
    {"full", FinalClipFull, false},
    {"extend", FinalClipExtend, false},
    {"invalid", 0, true},
    {"FULL", 0, true}, // case-sensitive
    }
    for _, tt := range tests {
    t.Run(tt.input, func(t *testing.T) {
    got, err := ParseFinalClipMode(tt.input)
    if tt.err {
    if err == nil {
    t.Error("expected error")
    }
    } else {
    if err != nil {
    t.Errorf("unexpected error: %v", err)
    }
    if got != tt.want {
    t.Errorf("got %d, want %d", got, tt.want)
    }
    }
    })
    }
    }
    func assertClips(t *testing.T, got, want []ClipWindow) {
    t.Helper()
    if len(got) != len(want) {
    t.Fatalf("len(got)=%d, len(want)=%d\ngot=%v\nwant=%v", len(got), len(want), got, want)
    }
    for i := range got {
    if math.Abs(got[i].Start-want[i].Start) > 1e-9 || math.Abs(got[i].End-want[i].End) > 1e-9 {
    t.Errorf("clip %d: got (%v,%v), want (%v,%v)", i, got[i].Start, got[i].End, want[i].Start, want[i].End)
    }
    }
    }
  • file addition: clip_times.go (----------)
    [5.67281]
    package calls
    import (
    "fmt"
    "math"
    )
    // ClipWindow is a fixed-duration time window for one audio file.
    type ClipWindow struct {
    Start float64
    End float64
    }
    // FinalClipMode controls how the trailing partial clip is handled.
    // Mirrors opensoundscape.utils.generate_clip_times_df:
    // - FinalClipNone: discard any clip whose end exceeds full_duration
    // - FinalClipRemainder: trim the final clip's end to full_duration (shorter clip)
    // - FinalClipFull: shift the final clip's start back so its end equals full_duration
    // - FinalClipExtend: keep the final clip extending beyond full_duration
    type FinalClipMode int
    const (
    FinalClipNone FinalClipMode = iota
    FinalClipRemainder
    FinalClipFull
    FinalClipExtend
    )
    // ParseFinalClipMode parses a CLI flag value.
    func ParseFinalClipMode(s string) (FinalClipMode, error) {
    switch s {
    case "none", "":
    return FinalClipNone, nil
    case "remainder":
    return FinalClipRemainder, nil
    case "full":
    return FinalClipFull, nil
    case "extend":
    return FinalClipExtend, nil
    default:
    return 0, fmt.Errorf("invalid final-clip mode %q (want one of: none, remainder, full, extend)", s)
    }
    }
    // roundTo rounds x to `precision` decimal places. Mirrors numpy.round behaviour.
    // Pass precision < 0 to skip rounding.
    func roundTo(x float64, precision int) float64 {
    if precision < 0 {
    return x
    }
    scale := math.Pow(10, float64(precision))
    return math.Round(x*scale) / scale
    }
    // GenerateClipTimes ports opensoundscape.utils.generate_clip_times_df.
    //
    // Args mirror the Python signature: clipDuration > 0, clipOverlap in [0, clipDuration),
    // fullDuration > 0. roundingPrecision defaults to 10 in OPSO; pass -1 to skip rounding.
    //
    // Result is the list of (start, end) windows for one audio file, with duplicates
    // removed (which can happen under FinalClipFull when the shifted final clip
    // coincides with the previous one).
    func GenerateClipTimes(fullDuration, clipDuration, clipOverlap float64, finalClip FinalClipMode, roundingPrecision int) ([]ClipWindow, error) {
    if clipDuration <= 0 {
    return nil, fmt.Errorf("clipDuration must be > 0, got %v", clipDuration)
    }
    if clipOverlap < 0 || clipOverlap >= clipDuration {
    return nil, fmt.Errorf("clipOverlap must be in [0, clipDuration), got %v with clipDuration=%v", clipOverlap, clipDuration)
    }
    if fullDuration <= 0 {
    return nil, fmt.Errorf("fullDuration must be > 0, got %v", fullDuration)
    }
    starts, ends := buildClipStartsEnds(fullDuration, clipDuration, clipOverlap, roundingPrecision)
    switch finalClip {
    case FinalClipNone:
    return dedupClips(clipWindowsNone(starts, ends, fullDuration)), nil
    case FinalClipRemainder:
    return dedupClips(clipWindowsRemainder(starts, ends, fullDuration)), nil
    case FinalClipFull:
    return dedupClips(clipWindowsFull(starts, ends, fullDuration)), nil
    case FinalClipExtend:
    return dedupClips(clipWindowsExtend(starts, ends)), nil
    default:
    return nil, fmt.Errorf("invalid FinalClipMode %d", finalClip)
    }
    }
    // buildClipStartsEnds generates the start and end arrays for clips.
    func buildClipStartsEnds(fullDuration, clipDuration, clipOverlap float64, roundingPrecision int) ([]float64, []float64) {
    increment := clipDuration - clipOverlap
    var starts []float64
    for s := 0.0; s < fullDuration; s += increment {
    starts = append(starts, roundTo(s, roundingPrecision))
    }
    if len(starts) == 0 {
    starts = []float64{0}
    }
    ends := make([]float64, len(starts))
    for i, s := range starts {
    ends[i] = s + clipDuration
    }
    return starts, ends
    }
    // clipWindowsNone drops any window whose end exceeds fullDuration.
    func clipWindowsNone(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    if ends[i] <= fullDuration {
    out = append(out, ClipWindow{Start: starts[i], End: ends[i]})
    }
    }
    return out
    }
    // clipWindowsRemainder trims ends beyond fullDuration down to fullDuration.
    func clipWindowsRemainder(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    e := ends[i]
    if e > fullDuration {
    e = fullDuration
    }
    out = append(out, ClipWindow{Start: starts[i], End: e})
    }
    return out
    }
    // clipWindowsFull shifts windows whose end exceeds fullDuration back so end == fullDuration.
    func clipWindowsFull(starts, ends []float64, fullDuration float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    s, e := starts[i], ends[i]
    if e > fullDuration {
    s -= e - fullDuration
    e = fullDuration
    if s < 0 {
    s = 0
    }
    }
    out = append(out, ClipWindow{Start: s, End: e})
    }
    return out
    }
    // clipWindowsExtend keeps ends as-is, even past fullDuration.
    func clipWindowsExtend(starts, ends []float64) []ClipWindow {
    out := make([]ClipWindow, 0, len(starts))
    for i := range starts {
    out = append(out, ClipWindow{Start: starts[i], End: ends[i]})
    }
    return out
    }
    // dedupClips removes consecutive duplicates while preserving order.
    // Matches pandas.DataFrame.drop_duplicates() at the end of OPSO's
    // generate_clip_times_df.
    func dedupClips(in []ClipWindow) []ClipWindow {
    if len(in) <= 1 {
    return in
    }
    seen := make(map[ClipWindow]bool, len(in))
    out := make([]ClipWindow, 0, len(in))
    for _, c := range in {
    if !seen[c] {
    seen[c] = true
    out = append(out, c)
    }
    }
    return out
    }
  • replacement in tools/calls/calls_summarise_test.go at line 7
    [5.5889][5.5889:5905]()
    "skraak/utils"
    [5.5889]
    [5.5905]
    "skraak/datafile"
  • replacement in tools/calls/calls_summarise_test.go at line 11
    [5.5946][5.5946:6154]()
    a := &utils.Label{Filter: "kiwi.txt", Species: "Kiwi"}
    b := &utils.Label{Filter: "tomtit.txt", Species: "Tomtit"}
    c := &utils.Label{Filter: "kiwi.txt", Species: "Kiwi2"}
    labels := []*utils.Label{a, b, c}
    [5.5946]
    [5.6154]
    a := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi"}
    b := &datafile.Label{Filter: "tomtit.txt", Species: "Tomtit"}
    c := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi2"}
    labels := []*datafile.Label{a, b, c}
  • replacement in tools/calls/calls_summarise_test.go at line 19
    [5.6208][5.6208:6232]()
    want []*utils.Label
    [5.6208]
    [5.6232]
    want []*datafile.Label
  • replacement in tools/calls/calls_summarise_test.go at line 22
    [5.6277][5.6277:6349]()
    {"matching filter returns subset", "kiwi.txt", []*utils.Label{a, c}},
    [5.6277]
    [5.6349]
    {"matching filter returns subset", "kiwi.txt", []*datafile.Label{a, c}},
  • replacement in tools/calls/calls_summarise_test.go at line 36
    [5.6683][5.6683:6710]()
    labels := []*utils.Label{
    [5.6683]
    [5.6710]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_summarise_test.go at line 54
    [5.7285][5.7285:7312]()
    labels := []*utils.Label{
    [5.7285]
    [5.7312]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_summarise_test.go at line 78
    [5.7866][5.7866:7893]()
    labels := []*utils.Label{
    [5.7866]
    [5.7893]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_summarise_test.go at line 112
    [5.8903][5.8903:9120]()
    trackMeta(&utils.DataMeta{Operator: "alice", Reviewer: ""}, ops, revs)
    trackMeta(&utils.DataMeta{Operator: "", Reviewer: "bob"}, ops, revs)
    trackMeta(&utils.DataMeta{Operator: "alice", Reviewer: "bob"}, ops, revs)
    [5.8903]
    [5.9120]
    trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: ""}, ops, revs)
    trackMeta(&datafile.DataMeta{Operator: "", Reviewer: "bob"}, ops, revs)
    trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: "bob"}, ops, revs)
  • replacement in tools/calls/calls_summarise_test.go at line 145
    [5.14516][5.14516:14543]()
    labels := []*utils.Label{
    [5.14516]
    [5.14543]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_summarise.go at line 7
    [5.74657][5.74657:74673]()
    "skraak/utils"
    [5.74657]
    [5.74673]
    "skraak/datafile"
  • replacement in tools/calls/calls_summarise.go at line 71
    [5.77092][5.77092:77145]()
    filePaths, err := utils.FindDataFiles(input.Folder)
    [5.77092]
    [5.77145]
    filePaths, err := datafile.FindDataFiles(input.Folder)
  • replacement in tools/calls/calls_summarise.go at line 113
    [5.78293][5.78293:78332]()
    df, err := utils.ParseDataFile(path)
    [5.78293]
    [5.78332]
    df, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_summarise.go at line 148
    [5.79131][5.79131:79212]()
    func trackMeta(meta *utils.DataMeta, operatorSet, reviewerSet map[string]bool) {
    [5.79131]
    [5.79212]
    func trackMeta(meta *datafile.DataMeta, operatorSet, reviewerSet map[string]bool) {
  • replacement in tools/calls/calls_summarise.go at line 161
    [5.79460][5.79460:79533]()
    func filterLabels(labels []*utils.Label, filter string) []*utils.Label {
    [5.79460]
    [5.79533]
    func filterLabels(labels []*datafile.Label, filter string) []*datafile.Label {
  • replacement in tools/calls/calls_summarise.go at line 165
    [5.79571][5.79571:79600]()
    var filtered []*utils.Label
    [5.79571]
    [5.79600]
    var filtered []*datafile.Label
  • replacement in tools/calls/calls_summarise.go at line 175
    [5.79773][5.79773:79838]()
    func buildLabelSummaries(labels []*utils.Label) []LabelSummary {
    [5.79773]
    [5.79838]
    func buildLabelSummaries(labels []*datafile.Label) []LabelSummary {
  • replacement in tools/calls/calls_summarise.go at line 198
    [5.80296][5.80296:80378]()
    func updateStatsFromLabels(labels []*utils.Label, output *CallsSummariseOutput) {
    [5.80296]
    [5.80378]
    func updateStatsFromLabels(labels []*datafile.Label, output *CallsSummariseOutput) {
  • replacement in tools/calls/calls_summarise.go at line 206
    [5.80550][5.80550:80621]()
    func updateFilterStats(l *utils.Label, output *CallsSummariseOutput) {
    [5.80550]
    [5.80621]
    func updateFilterStats(l *datafile.Label, output *CallsSummariseOutput) {
  • replacement in tools/calls/calls_summarise.go at line 228
    [5.81110][5.81110:81182]()
    func updateReviewStatus(l *utils.Label, output *CallsSummariseOutput) {
    [5.81110]
    [5.81182]
    func updateReviewStatus(l *datafile.Label, output *CallsSummariseOutput) {
  • edit in tools/calls/calls_show_images.go at line 8
    [5.83078]
    [5.45555]
    "skraak/datafile"
  • edit in tools/calls/calls_show_images.go at line 10
    [5.45577][5.83078:83094](),[5.83078][5.83078:83094]()
    "skraak/utils"
  • replacement in tools/calls/calls_show_images.go at line 49
    [5.84441][5.84441:84499]()
    dataFile, err := utils.ParseDataFile(input.DataFilePath)
    [5.84441]
    [5.84499]
    dataFile, err := datafile.ParseDataFile(input.DataFilePath)
  • replacement in tools/calls/calls_show_images.go at line 100
    [5.85945][5.85945:86002]()
    func formatSegmentLabels(labels []*utils.Label) string {
    [5.85945]
    [5.86002]
    func formatSegmentLabels(labels []*datafile.Label) string {
  • replacement in tools/calls/calls_remove_test.go at line 8
    [5.61][5.61:77]()
    "skraak/utils"
    [5.61]
    [5.77]
    "skraak/datafile"
  • replacement in tools/calls/calls_remove_test.go at line 15
    [5.194][5.194:323]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60, Reviewer: "AI"},
    Segments: []*utils.Segment{
    [5.194]
    [5.323]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60, Reviewer: "AI"},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 23
    [5.404][5.404:432]()
    Labels: []*utils.Label{
    [5.404]
    [5.432]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 53
    [5.1128][5.1128:1171]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.1128]
    [5.1171]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_remove_test.go at line 75
    [5.1807][5.1807:1920]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.1807]
    [5.1920]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 83
    [5.2001][5.2001:2029]()
    Labels: []*utils.Label{
    [5.2001]
    [5.2029]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 92
    [5.2182][5.2182:2210]()
    Labels: []*utils.Label{
    [5.2182]
    [5.2210]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 118
    [5.2737][5.2737:2780]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.2737]
    [5.2780]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_remove_test.go at line 134
    [5.3197][5.3197:3310]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.3197]
    [5.3310]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 142
    [5.3391][5.3391:3419]()
    Labels: []*utils.Label{
    [5.3391]
    [5.3419]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 177
    [5.4173][5.4173:4286]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.4173]
    [5.4286]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 185
    [5.4367][5.4367:4395]()
    Labels: []*utils.Label{
    [5.4367]
    [5.4395]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 217
    [5.5138][5.5138:5251]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.5138]
    [5.5251]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 225
    [5.5332][5.5332:5360]()
    Labels: []*utils.Label{
    [5.5332]
    [5.5360]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 253
    [5.6010][5.6010:6053]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.6010]
    [5.6053]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_remove_test.go at line 269
    [5.6509][5.6509:6622]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.6509]
    [5.6622]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 277
    [5.6702][5.6702:6730]()
    Labels: []*utils.Label{
    [5.6702]
    [5.6730]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 286
    [5.6882][5.6882:6910]()
    Labels: []*utils.Label{
    [5.6882]
    [5.6910]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 317
    [5.7574][5.7574:7687]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.7574]
    [5.7687]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 325
    [5.7767][5.7767:7795]()
    Labels: []*utils.Label{
    [5.7767]
    [5.7795]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 334
    [5.7947][5.7947:7975]()
    Labels: []*utils.Label{
    [5.7947]
    [5.7975]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 362
    [5.8561][5.8561:8604]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.8561]
    [5.8604]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_remove_test.go at line 395
    [5.9403][5.9403:9516]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.9403]
    [5.9516]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 403
    [5.9597][5.9597:9625]()
    Labels: []*utils.Label{
    [5.9597]
    [5.9625]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove_test.go at line 434
    [5.10291][5.10291:10404]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.10291]
    [5.10404]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_remove_test.go at line 442
    [5.10486][5.10486:10514]()
    Labels: []*utils.Label{
    [5.10486]
    [5.10514]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_remove.go at line 7
    [5.11394][5.11394:11410]()
    "skraak/utils"
    [5.11394]
    [5.11410]
    "skraak/datafile"
  • replacement in tools/calls/calls_remove.go at line 58
    [5.12991][5.12991:13124]()
    func findSegmentsByTimeRange(segments []*utils.Segment, startTime, endTime float64) []*utils.Segment {
    var matches []*utils.Segment
    [5.12991]
    [5.13124]
    func findSegmentsByTimeRange(segments []*datafile.Segment, startTime, endTime float64) []*datafile.Segment {
    var matches []*datafile.Segment
  • replacement in tools/calls/calls_remove.go at line 71
    [5.13497][5.13497:13634]()
    func findMatchingLabels(segment *utils.Segment, species, callType, filter string) ([]*utils.Label, string) {
    var matches []*utils.Label
    [5.13497]
    [5.13634]
    func findMatchingLabels(segment *datafile.Segment, species, callType, filter string) ([]*datafile.Label, string) {
    var matches []*datafile.Label
  • replacement in tools/calls/calls_remove.go at line 103
    [5.14351][5.14351:14472]()
    func removeLabelFromSegment(segment *utils.Segment, toRemove []*utils.Label) {
    removeSet := make(map[*utils.Label]bool)
    [5.14351]
    [5.14472]
    func removeLabelFromSegment(segment *datafile.Segment, toRemove []*datafile.Label) {
    removeSet := make(map[*datafile.Label]bool)
  • replacement in tools/calls/calls_remove.go at line 109
    [5.14528][5.14528:14558]()
    var remaining []*utils.Label
    [5.14528]
    [5.14558]
    var remaining []*datafile.Label
  • replacement in tools/calls/calls_remove.go at line 120
    [5.14769][5.14769:14874]()
    func removeSegmentFromDataFile(df *utils.DataFile, seg *utils.Segment) {
    var remaining []*utils.Segment
    [5.14769]
    [5.14874]
    func removeSegmentFromDataFile(df *datafile.DataFile, seg *datafile.Segment) {
    var remaining []*datafile.Segment
  • replacement in tools/calls/calls_remove.go at line 131
    [5.15086][5.15086:15215]()
    func resolveTargetSegment(dataFile *utils.DataFile, input CallsRemoveInput, output *CallsRemoveOutput) (*utils.Segment, error) {
    [5.15086]
    [5.15215]
    func resolveTargetSegment(dataFile *datafile.DataFile, input CallsRemoveInput, output *CallsRemoveOutput) (*datafile.Segment, error) {
  • replacement in tools/calls/calls_remove.go at line 166
    [5.16546][5.16546:16655]()
    func resolveTargetLabels(segment *utils.Segment, species, callType, filter string) ([]*utils.Label, error) {
    [5.16546]
    [5.16655]
    func resolveTargetLabels(segment *datafile.Segment, species, callType, filter string) ([]*datafile.Label, error) {
  • replacement in tools/calls/calls_remove.go at line 190
    [5.17379][5.17379:17443]()
    species, callType := utils.ParseSpeciesCallType(input.Species)
    [5.17379]
    [5.17443]
    species, callType := datafile.ParseSpeciesCallType(input.Species)
  • replacement in tools/calls/calls_remove.go at line 204
    [5.17780][5.17780:17830]()
    dataFile, err := utils.ParseDataFile(input.File)
    [5.17780]
    [5.17830]
    dataFile, err := datafile.ParseDataFile(input.File)
  • replacement in tools/calls/calls_push_certainty_test.go at line 9
    [5.86423][5.86423:86439]()
    "skraak/utils"
    [5.86423]
    [5.86439]
    "skraak/datafile"
  • replacement in tools/calls/calls_push_certainty_test.go at line 46
    [5.87655][5.87655:87698]()
    df, err := utils.ParseDataFile(file1Path)
    [5.87655]
    [5.87698]
    df, err := datafile.ParseDataFile(file1Path)
  • replacement in tools/calls/calls_push_certainty_test.go at line 61
    [5.88161][5.88161:88205]()
    df2, err := utils.ParseDataFile(file2Path)
    [5.88161]
    [5.88205]
    df2, err := datafile.ParseDataFile(file2Path)
  • replacement in tools/calls/calls_push_certainty_test.go at line 102
    [5.89338][5.89338:89380]()
    df, err := utils.ParseDataFile(filePath)
    [5.89338]
    [5.89380]
    df, err := datafile.ParseDataFile(filePath)
  • replacement in tools/calls/calls_push_certainty.go at line 6
    [5.89828][5.89828:89844]()
    "skraak/utils"
    [5.89828]
    [5.89844]
    "skraak/datafile"
  • replacement in tools/calls/calls_push_certainty.go at line 83
    [5.91995][5.91995:92078]()
    func labelMatchesPush(label *utils.Label, filter, species, callType string) bool {
    [5.91995]
    [5.92078]
    func labelMatchesPush(label *datafile.Label, filter, species, callType string) bool {
  • replacement in tools/calls/calls_propagate_test.go at line 7
    [5.92412][5.92412:92428]()
    "skraak/utils"
    [5.92412]
    [5.92428]
    "skraak/datafile"
  • replacement in tools/calls/calls_propagate_test.go at line 12
    [5.92443][5.92443:92537]()
    func seg(start, end float64, labels ...*utils.Label) *utils.Segment {
    return &utils.Segment{
    [5.92443]
    [5.92537]
    func seg(start, end float64, labels ...*datafile.Label) *datafile.Segment {
    return &datafile.Segment{
  • replacement in tools/calls/calls_propagate_test.go at line 22
    [5.92639][5.92639:92734]()
    func lbl(filter, species, calltype string, certainty int) *utils.Label {
    return &utils.Label{
    [5.92639]
    [5.92734]
    func lbl(filter, species, calltype string, certainty int) *datafile.Label {
    return &datafile.Label{
  • replacement in tools/calls/calls_propagate_test.go at line 31
    [5.92830][5.92830:92892]()
    func writeFile(t *testing.T, segs ...*utils.Segment) string {
    [5.92830]
    [5.92892]
    func writeFile(t *testing.T, segs ...*datafile.Segment) string {
  • replacement in tools/calls/calls_propagate_test.go at line 35
    [5.92965][5.92965:93069]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "ML", Reviewer: "David", Duration: 3600},
    [5.92965]
    [5.93069]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "ML", Reviewer: "David", Duration: 3600},
  • replacement in tools/calls/calls_propagate_test.go at line 45
    [5.93186][5.93186:93245]()
    func readFile(t *testing.T, path string) *utils.DataFile {
    [5.93186]
    [5.93245]
    func readFile(t *testing.T, path string) *datafile.DataFile {
  • replacement in tools/calls/calls_propagate_test.go at line 47
    [5.93257][5.93257:93295]()
    df, err := utils.ParseDataFile(path)
    [5.93257]
    [5.93295]
    df, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_propagate_test.go at line 55
    [5.93456][5.93456:93541]()
    func findLabel(df *utils.DataFile, filter string, start, end float64) *utils.Label {
    [5.93456]
    [5.93541]
    func findLabel(df *datafile.DataFile, filter string, start, end float64) *datafile.Label {
  • replacement in tools/calls/calls_propagate_test.go at line 528
    [5.108309][5.108309:108391]()
    func writeFileAt(t *testing.T, dir, base string, segs ...*utils.Segment) string {
    [5.108309]
    [5.108391]
    func writeFileAt(t *testing.T, dir, base string, segs ...*datafile.Segment) string {
  • replacement in tools/calls/calls_propagate_test.go at line 531
    [5.108437][5.108437:108541]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "ML", Reviewer: "David", Duration: 3600},
    [5.108437]
    [5.108541]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "ML", Reviewer: "David", Duration: 3600},
  • replacement in tools/calls/calls_propagate.go at line 7
    [5.113845][5.113845:113861]()
    "skraak/utils"
    [5.113845]
    [5.113861]
    "skraak/datafile"
  • replacement in tools/calls/calls_propagate.go at line 102
    [5.117979][5.117979:118023]()
    df, err := utils.ParseDataFile(input.File)
    [5.117979]
    [5.118023]
    df, err := datafile.ParseDataFile(input.File)
  • replacement in tools/calls/calls_propagate.go at line 158
    [5.119589][5.119589:119665]()
    func hasBothFilters(df *utils.DataFile, fromFilter, toFilter string) bool {
    [5.119589]
    [5.119665]
    func hasBothFilters(df *datafile.DataFile, fromFilter, toFilter string) bool {
  • replacement in tools/calls/calls_propagate.go at line 178
    [5.120031][5.120031:120073]()
    seg *utils.Segment
    label *utils.Label
    [5.120031]
    [5.120073]
    seg *datafile.Segment
    label *datafile.Label
  • replacement in tools/calls/calls_propagate.go at line 183
    [5.120180][5.120180:120271]()
    func collectPropagateSources(df *utils.DataFile, fromFilter, species string) []sourceRef {
    [5.120180]
    [5.120271]
    func collectPropagateSources(df *datafile.DataFile, fromFilter, species string) []sourceRef {
  • replacement in tools/calls/calls_propagate.go at line 197
    [5.120661][5.120661:120783]()
    func propagateTargets(df *utils.DataFile, sources []sourceRef, input CallsPropagateInput, output *CallsPropagateOutput) {
    [5.120661]
    [5.120783]
    func propagateTargets(df *datafile.DataFile, sources []sourceRef, input CallsPropagateInput, output *CallsPropagateOutput) {
  • replacement in tools/calls/calls_propagate.go at line 223
    [5.121458][5.121458:121543]()
    func findUpdatableTargetLabel(labels []*utils.Label, toFilter string) *utils.Label {
    [5.121458]
    [5.121543]
    func findUpdatableTargetLabel(labels []*datafile.Label, toFilter string) *datafile.Label {
  • replacement in tools/calls/calls_propagate.go at line 233
    [5.121775][5.121775:121859]()
    func findOverlappingSources(sources []sourceRef, tSeg *utils.Segment) []sourceRef {
    [5.121775]
    [5.121859]
    func findOverlappingSources(sources []sourceRef, tSeg *datafile.Segment) []sourceRef {
  • replacement in tools/calls/calls_propagate.go at line 256
    [5.122516][5.122516:122626]()
    func buildConflictRecord(tSeg *utils.Segment, toLabel *utils.Label, overlaps []sourceRef) PropagateConflict {
    [5.122516]
    [5.122626]
    func buildConflictRecord(tSeg *datafile.Segment, toLabel *datafile.Label, overlaps []sourceRef) PropagateConflict {
  • replacement in tools/calls/calls_propagate.go at line 275
    [5.123124][5.123124:123247]()
    func applyPropagation(toLabel *utils.Label, species, callType string, tSeg *utils.Segment, output *CallsPropagateOutput) {
    [5.123124]
    [5.123247]
    func applyPropagation(toLabel *datafile.Label, species, callType string, tSeg *datafile.Segment, output *CallsPropagateOutput) {
  • replacement in tools/calls/calls_propagate.go at line 311
    [5.125165][5.125165:125214]()
    files, err := utils.FindDataFiles(input.Folder)
    [5.124856]
    [5.125214]
    files, err := datafile.FindDataFiles(input.Folder)
  • replacement in tools/calls/calls_modify_test.go at line 7
    [5.126283][5.126283:126299]()
    "skraak/utils"
    [5.126283]
    [5.126299]
    "skraak/datafile"
  • replacement in tools/calls/calls_modify_test.go at line 15
    [5.126473][5.126473:126584]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.126473]
    [5.126584]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 23
    [5.126672][5.126672:126700]()
    Labels: []*utils.Label{
    [5.126672]
    [5.126700]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 53
    [5.127515][5.127515:127557]()
    df2, err := utils.ParseDataFile(tmpFile)
    [5.127515]
    [5.127557]
    df2, err := datafile.ParseDataFile(tmpFile)
  • replacement in tools/calls/calls_modify_test.go at line 67
    [5.127893][5.127893:128004]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.127893]
    [5.128004]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 75
    [5.128092][5.128092:128120]()
    Labels: []*utils.Label{
    [5.128092]
    [5.128120]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 104
    [5.128818][5.128818:128860]()
    df2, err := utils.ParseDataFile(tmpFile)
    [5.128818]
    [5.128860]
    df2, err := datafile.ParseDataFile(tmpFile)
  • replacement in tools/calls/calls_modify_test.go at line 118
    [5.129198][5.129198:129309]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.129198]
    [5.129309]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 126
    [5.129397][5.129397:129425]()
    Labels: []*utils.Label{
    [5.129397]
    [5.129425]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 156
    [5.130126][5.130126:130168]()
    df2, err := utils.ParseDataFile(tmpFile)
    [5.130126]
    [5.130168]
    df2, err := datafile.ParseDataFile(tmpFile)
  • replacement in tools/calls/calls_modify_test.go at line 170
    [5.130585][5.130585:130696]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.130585]
    [5.130696]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 178
    [5.130784][5.130784:130812]()
    Labels: []*utils.Label{
    [5.130784]
    [5.130812]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 239
    [5.132251][5.132251:132362]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.132251]
    [5.132362]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 247
    [5.132450][5.132450:132478]()
    Labels: []*utils.Label{
    [5.132450]
    [5.132478]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 276
    [5.133276][5.133276:133318]()
    df2, err := utils.ParseDataFile(tmpFile)
    [5.133276]
    [5.133318]
    df2, err := datafile.ParseDataFile(tmpFile)
  • replacement in tools/calls/calls_modify_test.go at line 290
    [5.133724][5.133724:133835]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.133724]
    [5.133835]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 298
    [5.133923][5.133923:133951]()
    Labels: []*utils.Label{
    [5.133923]
    [5.133951]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify_test.go at line 326
    [5.134634][5.134634:134676]()
    df2, err := utils.ParseDataFile(tmpFile)
    [5.134634]
    [5.134676]
    df2, err := datafile.ParseDataFile(tmpFile)
  • replacement in tools/calls/calls_modify_test.go at line 339
    [5.134990][5.134990:135101]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "test", Duration: 60},
    Segments: []*utils.Segment{
    [5.134990]
    [5.135101]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "test", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_modify_test.go at line 347
    [5.135189][5.135189:135217]()
    Labels: []*utils.Label{
    [5.135189]
    [5.135217]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_modify.go at line 9
    [5.135864][5.135864:135880]()
    "skraak/utils"
    [5.135864]
    [5.135880]
    "skraak/datafile"
  • replacement in tools/calls/calls_modify.go at line 65
    [5.137633][5.137633:137723]()
    func resolveSpecies(inputSpecies string, label *utils.Label) (species, callType string) {
    [5.137633]
    [5.137723]
    func resolveSpecies(inputSpecies string, label *datafile.Label) (species, callType string) {
  • replacement in tools/calls/calls_modify.go at line 76
    [5.137974][5.137974:138079]()
    func hasModifyChanges(newSpecies, newCallType string, input CallsModifyInput, label *utils.Label) bool {
    [5.137974]
    [5.138079]
    func hasModifyChanges(newSpecies, newCallType string, input CallsModifyInput, label *datafile.Label) bool {
  • replacement in tools/calls/calls_modify.go at line 93
    [5.138440][5.138440:138600]()
    func applyLabelChanges(label *utils.Label, dataFile *utils.DataFile, input CallsModifyInput, newSpecies, newCallType string, output *CallsModifyOutput) error {
    [5.138440]
    [5.138600]
    func applyLabelChanges(label *datafile.Label, dataFile *datafile.DataFile, input CallsModifyInput, newSpecies, newCallType string, output *CallsModifyOutput) error {
  • replacement in tools/calls/calls_modify.go at line 150
    [5.139992][5.139992:140042]()
    dataFile, err := utils.ParseDataFile(input.File)
    [5.139992]
    [5.140042]
    dataFile, err := datafile.ParseDataFile(input.File)
  • replacement in tools/calls/calls_modify.go at line 191
    [5.141405][5.141405:141482]()
    func findLabelByFilter(segment *utils.Segment, filter string) *utils.Label {
    [5.141405]
    [5.141482]
    func findLabelByFilter(segment *datafile.Segment, filter string) *datafile.Label {
  • replacement in tools/calls/calls_modify.go at line 228
    [5.142593][5.142593:142693]()
    func findSegment(segments []*utils.Segment, startTime, endTime int, filter string) *utils.Segment {
    [5.142593]
    [5.142693]
    func findSegment(segments []*datafile.Segment, startTime, endTime int, filter string) *datafile.Segment {
  • replacement in tools/calls/calls_modify.go at line 247
    [5.143097][5.143097:143143]()
    func formatLabel(label *utils.Label) string {
    [5.143097]
    [5.143143]
    func formatLabel(label *datafile.Label) string {
  • replacement in tools/calls/calls_from_preds_test.go at line 8
    [5.151868][5.151868:151884]()
    "skraak/utils"
    [5.151868]
    [5.151884]
    "skraak/datafile"
  • replacement in tools/calls/calls_from_preds_test.go at line 83
    [5.153940][5.153940:153982]()
    df, err := utils.ParseDataFile(dataPath)
    [5.153940]
    [5.153982]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_preds_test.go at line 140
    [5.155712][5.155712:155754]()
    df, err := utils.ParseDataFile(dataPath)
    [5.155712]
    [5.155754]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_preds_test.go at line 193
    [5.157390][5.157390:157432]()
    df, err := utils.ParseDataFile(dataPath)
    [5.157390]
    [5.157432]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_preds_test.go at line 301
    [5.160338][5.160338:160380]()
    df, err := utils.ParseDataFile(dataPath)
    [5.160338]
    [5.160380]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_preds.go at line 16
    [5.163714][5.163714:163730]()
    "skraak/utils"
    [5.163714]
    [5.60441]
    "skraak/datafile"
  • replacement in tools/calls/calls_from_preds.go at line 598
    [5.181589][5.181589:181634]()
    existing, err := utils.ParseDataFile(path)
    [5.181589]
    [5.181634]
    existing, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_from_preds.go at line 629
    [5.182484][5.182484:182628]()
    // convertAviaNZSegment converts an AviaNZSegment to utils.Segment
    func convertAviaNZSegment(seg AviaNZSegment, filter string) *utils.Segment {
    [5.182484]
    [5.182628]
    // convertAviaNZSegment converts an AviaNZSegment to datafile.Segment
    func convertAviaNZSegment(seg AviaNZSegment, filter string) *datafile.Segment {
  • replacement in tools/calls/calls_from_preds.go at line 632
    [5.182662][5.182662:182712]()
    utilsLabels := make([]*utils.Label, len(labels))
    [5.182662]
    [5.182712]
    utilsLabels := make([]*datafile.Label, len(labels))
  • replacement in tools/calls/calls_from_preds.go at line 634
    [5.182740][5.182740:182773]()
    utilsLabels[i] = &utils.Label{
    [5.182740]
    [5.182773]
    utilsLabels[i] = &datafile.Label{
  • replacement in tools/calls/calls_from_preds.go at line 656
    [5.183163][5.183163:183187]()
    return &utils.Segment{
    [5.183163]
    [5.183187]
    return &datafile.Segment{
  • replacement in tools/calls/calls_from_birda_raven_test.go at line 8
    [5.192716][5.192716:192732]()
    "skraak/utils"
    [5.192716]
    [5.192732]
    "skraak/datafile"
  • replacement in tools/calls/calls_from_birda_raven_test.go at line 50
    [5.193917][5.193917:193959]()
    df, err := utils.ParseDataFile(dataPath)
    [5.193917]
    [5.193959]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_birda_raven_test.go at line 122
    [5.196360][5.196360:196402]()
    df, err := utils.ParseDataFile(dataPath)
    [5.196360]
    [5.196402]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_birda_raven_test.go at line 215
    [5.199290][5.199290:199332]()
    df, err := utils.ParseDataFile(dataPath)
    [5.199290]
    [5.199332]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_from_birda_raven_test.go at line 284
    [5.201676][5.201676:201718]()
    df, err := utils.ParseDataFile(dataPath)
    [5.201676]
    [5.201718]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_detect_anomalies.go at line 8
    [5.215161][5.215161:215177]()
    "skraak/utils"
    [5.215161]
    [5.215177]
    "skraak/datafile"
  • replacement in tools/calls/calls_detect_anomalies.go at line 85
    [5.217628][5.217628:217671]()
    files, err := utils.FindDataFiles(folder)
    [5.217628]
    [5.217671]
    files, err := datafile.FindDataFiles(folder)
  • replacement in tools/calls/calls_detect_anomalies.go at line 97
    [5.217943][5.217943:217982]()
    df, err := utils.ParseDataFile(path)
    [5.217943]
    [5.217982]
    df, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_detect_anomalies.go at line 124
    [5.218614][5.218614:218656]()
    seg *utils.Segment
    label *utils.Label
    [5.218614]
    [5.218656]
    seg *datafile.Segment
    label *datafile.Label
  • replacement in tools/calls/calls_detect_anomalies.go at line 129
    [5.218745][5.218745:218857]()
    func detectAnomaliesInFile(df *utils.DataFile, path string, models []string, scope map[string]bool) []Anomaly {
    [5.218745]
    [5.218857]
    func detectAnomaliesInFile(df *datafile.DataFile, path string, models []string, scope map[string]bool) []Anomaly {
  • replacement in tools/calls/calls_detect_anomalies.go at line 157
    [5.219527][5.219527:219616]()
    func collectModelSegments(df *utils.DataFile, models []string) map[string][]labeledSeg {
    [5.219527]
    [5.219616]
    func collectModelSegments(df *datafile.DataFile, models []string) map[string][]labeledSeg {
  • replacement in tools/calls/calls_detect_anomalies.go at line 246
    [5.222346][5.222346:222388]()
    func overlaps(a, b *utils.Segment) bool {
    [5.222346]
    [5.222388]
    func overlaps(a, b *datafile.Segment) bool {
  • replacement in tools/calls/calls_clip_test.go at line 6
    [5.15905][5.15905:15921]()
    "skraak/utils"
    [5.15905]
    [5.15921]
    "skraak/datafile"
  • replacement in tools/calls/calls_clip_test.go at line 12
    [5.15991][5.15991:16088]()
    makeSeg := func(labels []*utils.Label) *utils.Segment {
    return &utils.Segment{Labels: labels}
    [5.15991]
    [5.16088]
    makeSeg := func(labels []*datafile.Label) *datafile.Segment {
    return &datafile.Segment{Labels: labels}
  • replacement in tools/calls/calls_clip_test.go at line 16
    [5.16092][5.16092:16275]()
    kiwiLabel := &utils.Label{Filter: "kiwi.txt", Species: "Kiwi", CallType: "song", Certainty: 100}
    tomtitLabel := &utils.Label{Filter: "tomtit.txt", Species: "Tomtit", Certainty: 80}
    [5.16092]
    [5.16275]
    kiwiLabel := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi", CallType: "song", Certainty: 100}
    tomtitLabel := &datafile.Label{Filter: "tomtit.txt", Species: "Tomtit", Certainty: 80}
  • replacement in tools/calls/calls_clip_test.go at line 19
    [5.16276][5.16276:16465]()
    segments := []*utils.Segment{
    makeSeg([]*utils.Label{kiwiLabel}),
    makeSeg([]*utils.Label{tomtitLabel}),
    makeSeg([]*utils.Label{kiwiLabel, tomtitLabel}),
    makeSeg([]*utils.Label{}),
    [5.16276]
    [5.16465]
    segments := []*datafile.Segment{
    makeSeg([]*datafile.Label{kiwiLabel}),
    makeSeg([]*datafile.Label{tomtitLabel}),
    makeSeg([]*datafile.Label{kiwiLabel, tomtitLabel}),
    makeSeg([]*datafile.Label{}),
  • replacement in tools/calls/calls_clip_labels_test.go at line 10
    [5.222588][5.222588:222604]()
    "skraak/utils"
    [5.222588]
    [5.222604]
    "skraak/datafile"
  • replacement in tools/calls/calls_clip_labels_test.go at line 15
    [5.222649][5.222649:222722]()
    func writeDataFile(t *testing.T, dir, name string, df *utils.DataFile) {
    [5.222649]
    [5.222722]
    func writeDataFile(t *testing.T, dir, name string, df *datafile.DataFile) {
  • replacement in tools/calls/calls_clip_labels_test.go at line 75
    [5.224196][5.224196:224321]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 20},
    Segments: []*utils.Segment{
    [5.224196]
    [5.224321]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 20},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 80
    [5.224386][5.224386:224463]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.224386]
    [5.224463]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 118
    [5.225554][5.225554:225679]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 15},
    Segments: []*utils.Segment{
    [5.225554]
    [5.225679]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 15},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 123
    [5.225744][5.225744:225821]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.225744]
    [5.225821]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 145
    [5.226449][5.226449:226574]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 10},
    Segments: []*utils.Segment{
    [5.226449]
    [5.226574]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 10},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 150
    [5.226639][5.226639:226716]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.226639]
    [5.226716]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 154
    [5.226787][5.226787:226863]()
    Labels: []*utils.Label{{Species: "Not", Certainty: 100, Filter: "f1"}},
    [5.226787]
    [5.226863]
    Labels: []*datafile.Label{{Species: "Not", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 181
    [5.227686][5.227686:227811]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 15},
    Segments: []*utils.Segment{
    [5.227686]
    [5.227811]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 15},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 186
    [5.227876][5.227876:227957]()
    Labels: []*utils.Label{{Species: "Don't Know", Certainty: 0, Filter: "f1"}},
    [5.227876]
    [5.227957]
    Labels: []*datafile.Label{{Species: "Don't Know", Certainty: 0, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 190
    [5.228029][5.228029:228106]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.228029]
    [5.228106]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 212
    [5.228721][5.228721:228846]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 10},
    Segments: []*utils.Segment{
    [5.228721]
    [5.228846]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 10},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 217
    [5.228911][5.228911:228939]()
    Labels: []*utils.Label{
    [5.228911]
    [5.228939]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_clip_labels_test.go at line 239
    [5.229715][5.229715:229840]()
    writeDataFile(t, dir, "rec.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 10},
    Segments: []*utils.Segment{
    [5.229715]
    [5.229840]
    writeDataFile(t, dir, "rec.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 10},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 244
    [5.229905][5.229905:229985]()
    Labels: []*utils.Label{{Species: "Mystery", Certainty: 100, Filter: "f1"}},
    [5.229905]
    [5.229985]
    Labels: []*datafile.Label{{Species: "Mystery", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 273
    [5.230690][5.230690:230812]()
    writeDataFile(t, dir, "a.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 5},
    Segments: []*utils.Segment{
    [5.230690]
    [5.230812]
    writeDataFile(t, dir, "a.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 5},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 278
    [5.230877][5.230877:230954]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.230877]
    [5.230954]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 310
    [5.231846][5.231846:231969]()
    writeDataFile(t, dir, "a.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 10},
    Segments: []*utils.Segment{
    [5.231846]
    [5.231969]
    writeDataFile(t, dir, "a.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 10},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 315
    [5.232034][5.232034:232111]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.232034]
    [5.232111]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • replacement in tools/calls/calls_clip_labels_test.go at line 319
    [5.232126][5.232126:232248]()
    writeDataFile(t, dir, "b.wav.data", &utils.DataFile{
    Meta: &utils.DataMeta{Duration: 5},
    Segments: []*utils.Segment{
    [5.232126]
    [5.232248]
    writeDataFile(t, dir, "b.wav.data", &datafile.DataFile{
    Meta: &datafile.DataMeta{Duration: 5},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_clip_labels_test.go at line 324
    [5.232313][5.232313:232390]()
    Labels: []*utils.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
    [5.232313]
    [5.232390]
    Labels: []*datafile.Label{{Species: "Kiwi", Certainty: 100, Filter: "f1"}},
  • edit in tools/calls/calls_clip_labels.go at line 14
    [5.233071]
    [5.50365]
    "skraak/datafile"
  • edit in tools/calls/calls_clip_labels.go at line 16
    [5.50383][5.233071:233087](),[5.233071][5.233071:233087]()
    "skraak/utils"
  • replacement in tools/calls/calls_clip_labels.go at line 92
    [5.236125][5.236125:236147]()
    df *utils.DataFile
    [5.236125]
    [5.236147]
    df *datafile.DataFile
  • replacement in tools/calls/calls_clip_labels.go at line 96
    [5.236246][5.236246:236399]()
    func validateClipLabelsInput(input CallsClipLabelsInput) (utils.FinalClipMode, error) {
    finalClipMode, err := utils.ParseFinalClipMode(input.FinalClip)
    [5.236246]
    [5.236399]
    func validateClipLabelsInput(input CallsClipLabelsInput) (FinalClipMode, error) {
    finalClipMode, err := ParseFinalClipMode(input.FinalClip)
  • replacement in tools/calls/calls_clip_labels.go at line 116
    [5.10827][5.10827:10962]()
    func collectSpeciesFromDataFile(path, filter string) (*utils.DataFile, map[string]bool, error) {
    df, err := utils.ParseDataFile(path)
    [5.10827]
    [5.10962]
    func collectSpeciesFromDataFile(path, filter string) (*datafile.DataFile, map[string]bool, error) {
    df, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_clip_labels.go at line 137
    [5.50564][5.237068:237115](),[5.237068][5.237068:237115]()
    dataPaths, err := utils.FindDataFiles(folder)
    [5.50564]
    [5.237115]
    dataPaths, err := datafile.FindDataFiles(folder)
  • replacement in tools/calls/calls_clip_labels.go at line 191
    [5.11925][5.11925:11961]()
    finalClipMode utils.FinalClipMode
    [5.11925]
    [5.11961]
    finalClipMode FinalClipMode
  • replacement in tools/calls/calls_clip_labels.go at line 291
    [5.240732][5.240732:240753]()
    df *utils.DataFile,
    [5.240732]
    [5.50986]
    df *datafile.DataFile,
  • replacement in tools/calls/calls_clip_labels.go at line 296
    [5.240855][5.240855:240891]()
    finalClipMode utils.FinalClipMode,
    [5.240855]
    [5.240891]
    finalClipMode FinalClipMode,
  • replacement in tools/calls/calls_clip_labels.go at line 300
    [5.240973][5.240973:241015]()
    windows, err := utils.GenerateClipTimes(
    [5.240973]
    [5.241015]
    windows, err := GenerateClipTimes(
  • replacement in tools/calls/calls_clip_labels.go at line 326
    [5.13627][5.51100:51241]()
    func resolveLabel(lbl *utils.Label, seg *utils.Segment, filter string, mf mapping.File, classIdx map[string]int) (resolvedSeg, bool, bool) {
    [5.13627]
    [5.13778]
    func resolveLabel(lbl *datafile.Label, seg *datafile.Segment, filter string, mf mapping.File, classIdx map[string]int) (resolvedSeg, bool, bool) {
  • replacement in tools/calls/calls_clip_labels.go at line 351
    [5.241631][5.241631:241659]()
    segments []*utils.Segment,
    [5.241631]
    [5.241659]
    segments []*datafile.Segment,
  • replacement in tools/calls/calls_clip_labels.go at line 393
    [5.243316][5.243316:243487]()
    func labelClipWindows(windows []utils.ClipWindow, segs []resolvedSeg, rel string, classes []string, minLabelOverlap float64, out *CallsClipLabelsOutput) []clipLabelsRow {
    [5.243316]
    [5.243487]
    func labelClipWindows(windows []ClipWindow, segs []resolvedSeg, rel string, classes []string, minLabelOverlap float64, out *CallsClipLabelsOutput) []clipLabelsRow {
  • replacement in tools/calls/calls_clip_labels.go at line 430
    [5.244259][5.244259:244384]()
    func classifyClip(w utils.ClipWindow, segs []resolvedSeg, minLabelOverlap float64, nClasses int) (clipDisposition, []bool) {
    [5.244259]
    [5.244384]
    func classifyClip(w ClipWindow, segs []resolvedSeg, minLabelOverlap float64, nClasses int) (clipDisposition, []bool) {
  • replacement in tools/calls/calls_clip_bench_test.go at line 42
    [5.248830][5.248830:248902]()
    // Duplicate of convertToFloat64 for benchmarking (unexported in utils)
    [5.248830]
    [5.248902]
    // Duplicate of convertToFloat64 for benchmarking (unexported in audio).
  • edit in tools/calls/calls_clip.go at line 12
    [5.25639]
    [5.49258]
    "skraak/datafile"
  • replacement in tools/calls/calls_clip.go at line 55
    [5.259236][5.259236:259304]()
    speciesName, callType := utils.ParseSpeciesCallType(input.Species)
    [5.259236]
    [5.259304]
    speciesName, callType := datafile.ParseSpeciesCallType(input.Species)
  • replacement in tools/calls/calls_clip.go at line 116
    [5.261215][5.261215:261268]()
    filePaths, err := utils.FindDataFiles(input.Folder)
    [5.261215]
    [5.261268]
    filePaths, err := datafile.FindDataFiles(input.Folder)
  • replacement in tools/calls/calls_clip.go at line 194
    [5.264040][5.264040:264088]()
    dataFile, err := utils.ParseDataFile(dataPath)
    [5.264040]
    [5.264088]
    dataFile, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_clip.go at line 235
    [5.265358][5.265358:265508]()
    func filterSegments(segments []*utils.Segment, filter, speciesName, callType string, certainty int) []*utils.Segment {
    var matching []*utils.Segment
    [5.265358]
    [5.265508]
    func filterSegments(segments []*datafile.Segment, filter, speciesName, callType string, certainty int) []*datafile.Segment {
    var matching []*datafile.Segment
  • replacement in tools/calls/calls_clip.go at line 270
    [5.266568][5.266568:266752]()
    func processSegments(segments []*utils.Segment, dataPath string, samples []float64, sampleRate int, outputDir, prefix, basename string, imgSize int, color bool) ([]string, []string) {
    [5.266568]
    [5.266752]
    func processSegments(segments []*datafile.Segment, dataPath string, samples []float64, sampleRate int, outputDir, prefix, basename string, imgSize int, color bool) ([]string, []string) {
  • replacement in tools/calls/calls_clip.go at line 291
    [5.267421][5.267421:267613]()
    func processSegmentsParallel(segments []*utils.Segment, dataPath string, samples []float64, sampleRate int, outputDir, prefix, basename string, imgSize int, color bool) ([]string, []string) {
    [5.267421]
    [5.267613]
    func processSegmentsParallel(segments []*datafile.Segment, dataPath string, samples []float64, sampleRate int, outputDir, prefix, basename string, imgSize int, color bool) ([]string, []string) {
  • replacement in tools/calls/calls_clip.go at line 298
    [5.267723][5.267723:267773]()
    jobs := make(chan *utils.Segment, len(segments))
    [5.267723]
    [5.267773]
    jobs := make(chan *datafile.Segment, len(segments))
  • replacement in tools/calls/calls_classify_test.go at line 6
    [5.271054][5.271054:271070]()
    "skraak/utils"
    [5.271054]
    [5.271070]
    "skraak/datafile"
  • replacement in tools/calls/calls_classify_test.go at line 9
    [5.271073][5.271073:271164]()
    func NewClassifyState(config ClassifyConfig, dataFiles []*utils.DataFile) *ClassifyState {
    [5.271073]
    [5.271164]
    func NewClassifyState(config ClassifyConfig, dataFiles []*datafile.DataFile) *ClassifyState {
  • replacement in tools/calls/calls_classify_test.go at line 11
    [5.271247][5.271247:271299]()
    cached := make([][]*utils.Segment, len(dataFiles))
    [5.271247]
    [5.271299]
    cached := make([][]*datafile.Segment, len(dataFiles))
  • replacement in tools/calls/calls_classify_test.go at line 85
    [5.273196][5.273196:273277]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    [5.273196]
    [5.273277]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_test.go at line 91
    [5.273324][5.273324:273352]()
    Labels: []*utils.Label{
    [5.273324]
    [5.273352]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_test.go at line 103
    [5.273593][5.273593:273620]()
    }, []*utils.DataFile{df})
    [5.273593]
    [5.273620]
    }, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_test.go at line 151
    [5.275225][5.275225:275306]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    [5.275225]
    [5.275306]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_test.go at line 157
    [5.275353][5.275353:275381]()
    Labels: []*utils.Label{
    [5.275353]
    [5.275381]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_test.go at line 169
    [5.275617][5.275617:275644]()
    }, []*utils.DataFile{df})
    [5.275617]
    [5.275644]
    }, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_test.go at line 181
    [5.275964][5.275964:276045]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    [5.275964]
    [5.276045]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_test.go at line 187
    [5.276092][5.276092:276120]()
    Labels: []*utils.Label{
    [5.276092]
    [5.276120]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_test.go at line 198
    [5.276319][5.276319:276346]()
    }, []*utils.DataFile{df})
    [5.276319]
    [5.276346]
    }, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_nav_test.go at line 6
    [5.9649][5.9649:9665]()
    "skraak/utils"
    [5.9649]
    [5.9665]
    "skraak/datafile"
  • replacement in tools/calls/calls_classify_nav_test.go at line 12
    [5.9755][5.9755:9779]()
    df := &utils.DataFile{
    [5.9755]
    [5.9779]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 14
    [5.9808][5.9808:9838]()
    Segments: []*utils.Segment{
    [5.9808]
    [5.9838]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_nav_test.go at line 19
    [5.9911][5.9911:9936]()
    df2 := &utils.DataFile{
    [5.9911]
    [5.9936]
    df2 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 21
    [5.9965][5.9965:9995]()
    Segments: []*utils.Segment{
    [5.9965]
    [5.9995]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_nav_test.go at line 26
    [5.10036][5.10036:10122]()
    state := NewClassifyState(ClassifyConfig{Certainty: -1}, []*utils.DataFile{df, df2})
    [5.10036]
    [5.10122]
    state := NewClassifyState(ClassifyConfig{Certainty: -1}, []*datafile.DataFile{df, df2})
  • replacement in tools/calls/calls_classify_nav_test.go at line 56
    [5.10857][5.10857:10881]()
    df := &utils.DataFile{
    [5.10857]
    [5.10881]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 58
    [5.10910][5.10910:10940]()
    Segments: []*utils.Segment{
    [5.10910]
    [5.10940]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_nav_test.go at line 62
    [5.10980][5.10980:11005]()
    df2 := &utils.DataFile{
    [5.10980]
    [5.11005]
    df2 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 64
    [5.11034][5.11034:11064]()
    Segments: []*utils.Segment{
    [5.11034]
    [5.11064]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_nav_test.go at line 70
    [5.11138][5.11138:11224]()
    state := NewClassifyState(ClassifyConfig{Certainty: -1}, []*utils.DataFile{df, df2})
    [5.11138]
    [5.11224]
    state := NewClassifyState(ClassifyConfig{Certainty: -1}, []*datafile.DataFile{df, df2})
  • replacement in tools/calls/calls_classify_nav_test.go at line 99
    [5.11909][5.11909:12080]()
    df1 := &utils.DataFile{FilePath: "/test/alpha.data", Segments: []*utils.Segment{{}}}
    df2 := &utils.DataFile{FilePath: "/test/beta.data", Segments: []*utils.Segment{{}}}
    [5.11909]
    [5.12080]
    df1 := &datafile.DataFile{FilePath: "/test/alpha.data", Segments: []*datafile.Segment{{}}}
    df2 := &datafile.DataFile{FilePath: "/test/beta.data", Segments: []*datafile.Segment{{}}}
  • replacement in tools/calls/calls_classify_nav_test.go at line 102
    [5.12081][5.12081:12137]()
    segs := [][]*utils.Segment{df1.Segments, df2.Segments}
    [5.12081]
    [5.12137]
    segs := [][]*datafile.Segment{df1.Segments, df2.Segments}
  • replacement in tools/calls/calls_classify_nav_test.go at line 104
    [5.12138][5.12138:12245]()
    state, err := buildClassifyState(ClassifyConfig{Goto: "beta.data"}, []*utils.DataFile{df1, df2}, segs, 0)
    [5.12138]
    [5.12245]
    state, err := buildClassifyState(ClassifyConfig{Goto: "beta.data"}, []*datafile.DataFile{df1, df2}, segs, 0)
  • replacement in tools/calls/calls_classify_nav_test.go at line 114
    [5.12441][5.12441:12569]()
    df1 := &utils.DataFile{FilePath: "/test/alpha.data", Segments: []*utils.Segment{{}}}
    segs := [][]*utils.Segment{df1.Segments}
    [5.12441]
    [5.12569]
    df1 := &datafile.DataFile{FilePath: "/test/alpha.data", Segments: []*datafile.Segment{{}}}
    segs := [][]*datafile.Segment{df1.Segments}
  • replacement in tools/calls/calls_classify_nav_test.go at line 117
    [5.12570][5.12570:12671]()
    _, err := buildClassifyState(ClassifyConfig{Goto: "missing.data"}, []*utils.DataFile{df1}, segs, 0)
    [5.12570]
    [5.12671]
    _, err := buildClassifyState(ClassifyConfig{Goto: "missing.data"}, []*datafile.DataFile{df1}, segs, 0)
  • replacement in tools/calls/calls_classify_nav_test.go at line 124
    [5.12795][5.12795:12923]()
    df1 := &utils.DataFile{FilePath: "/test/alpha.data", Segments: []*utils.Segment{{}}}
    segs := [][]*utils.Segment{df1.Segments}
    [5.12795]
    [5.12923]
    df1 := &datafile.DataFile{FilePath: "/test/alpha.data", Segments: []*datafile.Segment{{}}}
    segs := [][]*datafile.Segment{df1.Segments}
  • replacement in tools/calls/calls_classify_nav_test.go at line 127
    [5.12924][5.12924:13009]()
    state, err := buildClassifyState(ClassifyConfig{}, []*utils.DataFile{df1}, segs, 0)
    [5.12924]
    [5.13009]
    state, err := buildClassifyState(ClassifyConfig{}, []*datafile.DataFile{df1}, segs, 0)
  • replacement in tools/calls/calls_classify_nav_test.go at line 139
    [5.13211][5.13211:13238]()
    labels := []*utils.Label{
    [5.13211]
    [5.13238]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_classify_nav_test.go at line 169
    [5.14010][5.14010:14038]()
    labels := []*utils.Label{
    [5.14010]
    [5.14038]
    labels := []*datafile.Label{
  • replacement in tools/calls/calls_classify_nav_test.go at line 182
    [5.14317][5.14317:14341]()
    df := &utils.DataFile{
    [5.14317]
    [5.14341]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 184
    [5.14370][5.14370:14649]()
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    {Labels: []*utils.Label{{Species: "Kiwi", Filter: "f", Bookmark: true}}},
    {Labels: []*utils.Label{{Species: "Tomtit", Filter: "f"}}},
    {Labels: []*utils.Label{{Species: "Roroa", Filter: "f", Bookmark: true}}},
    [5.14370]
    [5.14649]
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
    {Labels: []*datafile.Label{{Species: "Kiwi", Filter: "f", Bookmark: true}}},
    {Labels: []*datafile.Label{{Species: "Tomtit", Filter: "f"}}},
    {Labels: []*datafile.Label{{Species: "Roroa", Filter: "f", Bookmark: true}}},
  • replacement in tools/calls/calls_classify_nav_test.go at line 192
    [5.14658][5.14658:14752]()
    state := NewClassifyState(ClassifyConfig{Filter: "f", Certainty: -1}, []*utils.DataFile{df})
    [5.14658]
    [5.14752]
    state := NewClassifyState(ClassifyConfig{Filter: "f", Certainty: -1}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_nav_test.go at line 217
    [5.15373][5.15373:15397]()
    df := &utils.DataFile{
    [5.15373]
    [5.15397]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 219
    [5.15426][5.15426:15548]()
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    {Labels: []*utils.Label{{Species: "Kiwi", Filter: "f"}}},
    [5.15426]
    [5.15548]
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
    {Labels: []*datafile.Label{{Species: "Kiwi", Filter: "f"}}},
  • replacement in tools/calls/calls_classify_nav_test.go at line 225
    [5.15557][5.15557:15669]()
    state := NewClassifyState(ClassifyConfig{Filter: "f", Reviewer: "Test", Certainty: -1}, []*utils.DataFile{df})
    [5.15557]
    [5.15669]
    state := NewClassifyState(ClassifyConfig{Filter: "f", Reviewer: "Test", Certainty: -1}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_nav_test.go at line 245
    [5.16024][5.16024:16048]()
    df := &utils.DataFile{
    [5.16024]
    [5.16048]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_nav_test.go at line 247
    [5.16077][5.16077:16214]()
    Meta: &utils.DataMeta{},
    Segments: []*utils.Segment{
    {Labels: []*utils.Label{{Species: "Kiwi", Filter: "f", Certainty: 70}}},
    [5.16077]
    [5.16214]
    Meta: &datafile.DataMeta{},
    Segments: []*datafile.Segment{
    {Labels: []*datafile.Label{{Species: "Kiwi", Filter: "f", Certainty: 70}}},
  • replacement in tools/calls/calls_classify_nav_test.go at line 253
    [5.16223][5.16223:16335]()
    state := NewClassifyState(ClassifyConfig{Filter: "f", Reviewer: "Test", Certainty: -1}, []*utils.DataFile{df})
    [5.16223]
    [5.16335]
    state := NewClassifyState(ClassifyConfig{Filter: "f", Reviewer: "Test", Certainty: -1}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 7
    [5.282166][5.282166:282182]()
    "skraak/utils"
    [5.282166]
    [5.282182]
    "skraak/datafile"
  • replacement in tools/calls/calls_classify_filter_test.go at line 12
    [5.282301][5.282301:282326]()
    df1 := &utils.DataFile{
    [5.282301]
    [5.282326]
    df1 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 14
    [5.282358][5.282358:282388]()
    Segments: []*utils.Segment{
    [5.282358]
    [5.282388]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 18
    [5.282430][5.282430:282458]()
    Labels: []*utils.Label{
    [5.282430]
    [5.282458]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 25
    [5.282559][5.282559:282587]()
    Labels: []*utils.Label{
    [5.282559]
    [5.282587]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 32
    [5.282656][5.282656:282681]()
    df2 := &utils.DataFile{
    [5.282656]
    [5.282681]
    df2 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 34
    [5.282713][5.282713:282743]()
    Segments: []*utils.Segment{
    [5.282713]
    [5.282743]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 38
    [5.282785][5.282785:282813]()
    Labels: []*utils.Label{
    [5.282785]
    [5.282813]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 46
    [5.282935][5.282935:283023]()
    state1 := NewClassifyState(ClassifyConfig{Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.282935]
    [5.283023]
    state1 := NewClassifyState(ClassifyConfig{Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 52
    [5.283207][5.283207:283312]()
    state2 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.283207]
    [5.283312]
    state2 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 58
    [5.283502][5.283502:283609]()
    state3 := NewClassifyState(ClassifyConfig{Species: "Tomtit", Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.283502]
    [5.283609]
    state3 := NewClassifyState(ClassifyConfig{Species: "Tomtit", Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 64
    [5.283799][5.283799:283908]()
    state4 := NewClassifyState(ClassifyConfig{Filter: "model-1.0", Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.283799]
    [5.283908]
    state4 := NewClassifyState(ClassifyConfig{Filter: "model-1.0", Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 70
    [5.284083][5.284083:284195]()
    state5 := NewClassifyState(ClassifyConfig{Species: "NonExistent", Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.284083]
    [5.284195]
    state5 := NewClassifyState(ClassifyConfig{Species: "NonExistent", Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 76
    [5.284351][5.284351:284376]()
    df3 := &utils.DataFile{
    [5.284351]
    [5.284376]
    df3 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 78
    [5.284408][5.284408:284438]()
    Segments: []*utils.Segment{
    [5.284408]
    [5.284438]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 82
    [5.284480][5.284480:284508]()
    Labels: []*utils.Label{
    [5.284480]
    [5.284508]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 89
    [5.284627][5.284627:284655]()
    Labels: []*utils.Label{
    [5.284627]
    [5.284655]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 95
    [5.284739][5.284739:284860]()
    state6 := NewClassifyState(ClassifyConfig{Filter: "model-1.0", Species: "Kiwi", Certainty: -1}, []*utils.DataFile{df3})
    [5.284739]
    [5.284860]
    state6 := NewClassifyState(ClassifyConfig{Filter: "model-1.0", Species: "Kiwi", Certainty: -1}, []*datafile.DataFile{df3})
  • replacement in tools/calls/calls_classify_filter_test.go at line 103
    [5.285075][5.285075:285100]()
    df1 := &utils.DataFile{
    [5.285075]
    [5.285100]
    df1 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 105
    [5.285132][5.285132:285162]()
    Segments: []*utils.Segment{
    [5.285132]
    [5.285162]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 109
    [5.285204][5.285204:285232]()
    Labels: []*utils.Label{
    [5.285204]
    [5.285232]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 116
    [5.285333][5.285333:285361]()
    Labels: []*utils.Label{
    [5.285333]
    [5.285361]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 123
    [5.285430][5.285430:285455]()
    df2 := &utils.DataFile{
    [5.285430]
    [5.285455]
    df2 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 125
    [5.285487][5.285487:285517]()
    Segments: []*utils.Segment{
    [5.285487]
    [5.285517]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 129
    [5.285559][5.285559:285587]()
    Labels: []*utils.Label{
    [5.285559]
    [5.285587]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 138
    [5.285790][5.285790:285894]()
    state := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*utils.DataFile{df1, df2})
    [5.285790]
    [5.285894]
    state := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 149
    [5.286193][5.286193:286217]()
    df := &utils.DataFile{
    [5.286193]
    [5.286217]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 151
    [5.286249][5.286249:286279]()
    Segments: []*utils.Segment{
    [5.286249]
    [5.286279]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 155
    [5.286321][5.286321:286349]()
    Labels: []*utils.Label{
    [5.286321]
    [5.286349]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 162
    [5.286465][5.286465:286493]()
    Labels: []*utils.Label{
    [5.286465]
    [5.286493]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 169
    [5.286610][5.286610:286638]()
    Labels: []*utils.Label{
    [5.286610]
    [5.286638]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 177
    [5.286781][5.286781:286863]()
    state1 := NewClassifyState(ClassifyConfig{Certainty: 70}, []*utils.DataFile{df})
    [5.286781]
    [5.286863]
    state1 := NewClassifyState(ClassifyConfig{Certainty: 70}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 183
    [5.287033][5.287033:287116]()
    state2 := NewClassifyState(ClassifyConfig{Certainty: 100}, []*utils.DataFile{df})
    [5.287033]
    [5.287116]
    state2 := NewClassifyState(ClassifyConfig{Certainty: 100}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 189
    [5.287285][5.287285:287366]()
    state3 := NewClassifyState(ClassifyConfig{Certainty: 0}, []*utils.DataFile{df})
    [5.287285]
    [5.287366]
    state3 := NewClassifyState(ClassifyConfig{Certainty: 0}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 195
    [5.287517][5.287517:287616]()
    state4 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: 70}, []*utils.DataFile{df})
    [5.287517]
    [5.287616]
    state4 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: 70}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 202
    [5.287777][5.287777:287854]()
    makeSegs := func(n int) []*utils.Segment {
    s := make([]*utils.Segment, n)
    [5.287777]
    [5.287854]
    makeSegs := func(n int) []*datafile.Segment {
    s := make([]*datafile.Segment, n)
  • replacement in tools/calls/calls_classify_filter_test.go at line 205
    [5.287875][5.287875:287948]()
    s[i] = &utils.Segment{StartTime: float64(i), EndTime: float64(i + 1)}
    [5.287875]
    [5.287948]
    s[i] = &datafile.Segment{StartTime: float64(i), EndTime: float64(i + 1)}
  • replacement in tools/calls/calls_classify_filter_test.go at line 210
    [5.287967][5.287967:288210]()
    df1 := &utils.DataFile{FilePath: "/test/f1.data", Segments: makeSegs(6)}
    df2 := &utils.DataFile{FilePath: "/test/f2.data", Segments: makeSegs(4)}
    kept := []*utils.DataFile{df1, df2}
    cached := [][]*utils.Segment{df1.Segments, df2.Segments}
    [5.287967]
    [5.288210]
    df1 := &datafile.DataFile{FilePath: "/test/f1.data", Segments: makeSegs(6)}
    df2 := &datafile.DataFile{FilePath: "/test/f2.data", Segments: makeSegs(4)}
    kept := []*datafile.DataFile{df1, df2}
    cached := [][]*datafile.Segment{df1.Segments, df2.Segments}
  • replacement in tools/calls/calls_classify_filter_test.go at line 215
    [5.288211][5.288211:288259]()
    countTotal := func(c [][]*utils.Segment) int {
    [5.288211]
    [5.288259]
    countTotal := func(c [][]*datafile.Segment) int {
  • replacement in tools/calls/calls_classify_filter_test.go at line 256
    [5.289381][5.289381:289406]()
    df1 := &utils.DataFile{
    [5.289381]
    [5.289406]
    df1 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 258
    [5.289438][5.289438:289468]()
    Segments: []*utils.Segment{
    [5.289438]
    [5.289468]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 262
    [5.289510][5.289510:289538]()
    Labels: []*utils.Label{
    [5.289510]
    [5.289538]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 269
    [5.289620][5.289620:289645]()
    df2 := &utils.DataFile{
    [5.289620]
    [5.289645]
    df2 := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 271
    [5.289677][5.289677:289707]()
    Segments: []*utils.Segment{
    [5.289677]
    [5.289707]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 275
    [5.289749][5.289749:289777]()
    Labels: []*utils.Label{
    [5.289749]
    [5.289777]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 284
    [5.290007][5.290007:290095]()
    state := NewClassifyState(ClassifyConfig{Certainty: 100}, []*utils.DataFile{df1, df2})
    [5.290007]
    [5.290095]
    state := NewClassifyState(ClassifyConfig{Certainty: 100}, []*datafile.DataFile{df1, df2})
  • replacement in tools/calls/calls_classify_filter_test.go at line 298
    [5.290560][5.290560:290584]()
    df := &utils.DataFile{
    [5.290560]
    [5.290584]
    df := &datafile.DataFile{
  • replacement in tools/calls/calls_classify_filter_test.go at line 300
    [5.290616][5.290616:290646]()
    Segments: []*utils.Segment{
    [5.290616]
    [5.290646]
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_classify_filter_test.go at line 304
    [5.290688][5.290688:290716]()
    Labels: []*utils.Label{
    [5.290688]
    [5.290716]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 311
    [5.290835][5.290835:290863]()
    Labels: []*utils.Label{
    [5.290835]
    [5.290863]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 318
    [5.290979][5.290979:291007]()
    Labels: []*utils.Label{
    [5.290979]
    [5.291007]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_classify_filter_test.go at line 326
    [5.291187][5.291187:291316]()
    state1 := NewClassifyState(ClassifyConfig{Species: "Kiwi", CallType: utils.CallTypeNone, Certainty: -1}, []*utils.DataFile{df})
    [5.291187]
    [5.291316]
    state1 := NewClassifyState(ClassifyConfig{Species: "Kiwi", CallType: datafile.CallTypeNone, Certainty: -1}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 332
    [5.291496][5.291496:291595]()
    state2 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*utils.DataFile{df})
    [5.291496]
    [5.291595]
    state2 := NewClassifyState(ClassifyConfig{Species: "Kiwi", Certainty: -1}, []*datafile.DataFile{df})
  • replacement in tools/calls/calls_classify_filter_test.go at line 338
    [5.291778][5.291778:291895]()
    state3 := NewClassifyState(ClassifyConfig{Species: "Kiwi", CallType: "Male", Certainty: -1}, []*utils.DataFile{df})
    [5.291778]
    [5.291895]
    state3 := NewClassifyState(ClassifyConfig{Species: "Kiwi", CallType: "Male", Certainty: -1}, []*datafile.DataFile{df})
  • edit in tools/calls/calls_classify.go at line 14
    [5.25836]
    [5.49681]
    "skraak/datafile"
  • edit in tools/calls/calls_classify.go at line 16
    [5.25836][5.292173:292189](),[5.49703][5.292173:292189](),[5.292173][5.292173:292189]()
    "skraak/utils"
  • replacement in tools/calls/calls_classify.go at line 58
    [5.293579][5.293579:293774]()
    DataFiles []*utils.DataFile
    filteredSegs [][]*utils.Segment // cached at load time, parallel to DataFiles
    totalSegs int // pre-computed total segment count
    [5.293579]
    [5.293774]
    DataFiles []*datafile.DataFile
    filteredSegs [][]*datafile.Segment // cached at load time, parallel to DataFiles
    totalSegs int // pre-computed total segment count
  • replacement in tools/calls/calls_classify.go at line 81
    [5.294446][5.294446:294496]()
    paths, err := utils.FindDataFiles(config.Folder)
    [5.294446]
    [5.294496]
    paths, err := datafile.FindDataFiles(config.Folder)
  • replacement in tools/calls/calls_classify.go at line 91
    [5.294839][5.294839:294942]()
    func filterDataFileSegments(df *utils.DataFile, config ClassifyConfig) ([]*utils.Segment, bool, int) {
    [5.294839]
    [5.294942]
    func filterDataFileSegments(df *datafile.DataFile, config ClassifyConfig) ([]*datafile.Segment, bool, int) {
  • replacement in tools/calls/calls_classify.go at line 109
    [5.295387][5.295387:295483]()
    func filterSegmentsByLabel(segments []*utils.Segment, config ClassifyConfig) []*utils.Segment {
    [5.295387]
    [5.295483]
    func filterSegmentsByLabel(segments []*datafile.Segment, config ClassifyConfig) []*datafile.Segment {
  • replacement in tools/calls/calls_classify.go at line 114
    [5.295604][5.295604:295631]()
    var segs []*utils.Segment
    [5.295604]
    [5.295631]
    var segs []*datafile.Segment
  • replacement in tools/calls/calls_classify.go at line 163
    [5.297086][5.297086:297165]()
    func parseAndSortDataFiles(config ClassifyConfig) ([]*utils.DataFile, error) {
    [5.297086]
    [5.297165]
    func parseAndSortDataFiles(config ClassifyConfig) ([]*datafile.DataFile, error) {
  • replacement in tools/calls/calls_classify.go at line 172
    [5.297327][5.297327:297360]()
    var dataFiles []*utils.DataFile
    [5.297327]
    [5.297360]
    var dataFiles []*datafile.DataFile
  • replacement in tools/calls/calls_classify.go at line 174
    [5.297394][5.297394:297433]()
    df, err := utils.ParseDataFile(path)
    [5.297394]
    [5.297433]
    df, err := datafile.ParseDataFile(path)
  • replacement in tools/calls/calls_classify.go at line 192
    [5.297819][5.297819:298002]()
    func filterDataFiles(dataFiles []*utils.DataFile, config ClassifyConfig) ([]*utils.DataFile, [][]*utils.Segment, int) {
    var kept []*utils.DataFile
    var cachedSegs [][]*utils.Segment
    [5.297819]
    [5.298002]
    func filterDataFiles(dataFiles []*datafile.DataFile, config ClassifyConfig) ([]*datafile.DataFile, [][]*datafile.Segment, int) {
    var kept []*datafile.DataFile
    var cachedSegs [][]*datafile.Segment
  • replacement in tools/calls/calls_classify.go at line 210
    [5.298359][5.298359:298512]()
    func buildClassifyState(config ClassifyConfig, dataFiles []*utils.DataFile, filteredSegs [][]*utils.Segment, timeFiltered int) (*ClassifyState, error) {
    [5.298359]
    [5.298512]
    func buildClassifyState(config ClassifyConfig, dataFiles []*datafile.DataFile, filteredSegs [][]*datafile.Segment, timeFiltered int) (*ClassifyState, error) {
  • replacement in tools/calls/calls_classify.go at line 240
    [5.299251][5.299251:299395]()
    func applySampling(kept []*utils.DataFile, cachedSegs [][]*utils.Segment, sample int, rng *rand.Rand) ([]*utils.DataFile, [][]*utils.Segment) {
    [5.299251]
    [5.299395]
    func applySampling(kept []*datafile.DataFile, cachedSegs [][]*datafile.Segment, sample int, rng *rand.Rand) ([]*datafile.DataFile, [][]*datafile.Segment) {
  • replacement in tools/calls/calls_classify.go at line 261
    [5.299989][5.299989:300045]()
    newCached := make([][]*utils.Segment, len(cachedSegs))
    [5.299989]
    [5.300045]
    newCached := make([][]*datafile.Segment, len(cachedSegs))
  • replacement in tools/calls/calls_classify.go at line 266
    [5.300176][5.300176:300243]()
    var newKept []*utils.DataFile
    var finalCached [][]*utils.Segment
    [5.300176]
    [5.300243]
    var newKept []*datafile.DataFile
    var finalCached [][]*datafile.Segment
  • replacement in tools/calls/calls_classify.go at line 278
    [5.300494][5.300494:300554]()
    func (s *ClassifyState) FilteredSegs() [][]*utils.Segment {
    [5.300494]
    [5.300554]
    func (s *ClassifyState) FilteredSegs() [][]*datafile.Segment {
  • replacement in tools/calls/calls_classify.go at line 283
    [5.300625][5.300625:300681]()
    func (s *ClassifyState) CurrentFile() *utils.DataFile {
    [5.300625]
    [5.300681]
    func (s *ClassifyState) CurrentFile() *datafile.DataFile {
  • replacement in tools/calls/calls_classify.go at line 291
    [5.300813][5.300813:300871]()
    func (s *ClassifyState) CurrentSegment() *utils.Segment {
    [5.300813]
    [5.300871]
    func (s *ClassifyState) CurrentSegment() *datafile.Segment {
  • replacement in tools/calls/calls_classify.go at line 391
    [5.302973][5.302973:302998]()
    label := &utils.Label{
    [5.302973]
    [5.302998]
    label := &datafile.Label{
  • replacement in tools/calls/calls_classify.go at line 448
    [5.304212][5.304212:304260]()
    seg.Labels = append(seg.Labels, &utils.Label{
    [5.304212]
    [5.304260]
    seg.Labels = append(seg.Labels, &datafile.Label{
  • replacement in tools/calls/calls_classify.go at line 462
    [5.304659][5.304659:304691]()
    var newLabels []*utils.Label
    [5.304659]
    [5.304691]
    var newLabels []*datafile.Label
  • replacement in tools/calls/calls_classify.go at line 559
    [5.306991][5.306991:307065]()
    func (s *ClassifyState) getFilterLabel(seg *utils.Segment) *utils.Label {
    [5.306991]
    [5.307065]
    func (s *ClassifyState) getFilterLabel(seg *datafile.Segment) *datafile.Label {
  • replacement in tools/calls/calls_classify.go at line 575
    [5.307366][5.307366:307448]()
    func (s *ClassifyState) getOrCreateFilterLabel(seg *utils.Segment) *utils.Label {
    [5.307366]
    [5.307448]
    func (s *ClassifyState) getOrCreateFilterLabel(seg *datafile.Segment) *datafile.Label {
  • replacement in tools/calls/calls_classify.go at line 581
    [5.307538][5.307538:307561]()
    label = &utils.Label{
    [5.307538]
    [5.307561]
    label = &datafile.Label{
  • replacement in tools/calls/calls_classify.go at line 689
    [5.310068][5.310068:310133]()
    func FormatLabels(labels []*utils.Label, filter string) string {
    [5.310068]
    [5.310133]
    func FormatLabels(labels []*datafile.Label, filter string) string {
  • replacement in tools/calls/calls_classify.go at line 713
    [5.10100][5.10100:10210]()
    func (s *ClassifyState) LoadFilteredSegment(df *utils.DataFile, seg *utils.Segment) ([]float64, int, error) {
    [5.10100]
    [5.10210]
    func (s *ClassifyState) LoadFilteredSegment(df *datafile.DataFile, seg *datafile.Segment) ([]float64, int, error) {
  • replacement in tools/calls/calls_add_test.go at line 8
    [5.19183][5.19183:19199]()
    "skraak/utils"
    [5.19183]
    [5.19199]
    "skraak/datafile"
  • replacement in tools/calls/calls_add_test.go at line 76
    [5.20783][5.20783:20825]()
    df, err := utils.ParseDataFile(dataPath)
    [5.20783]
    [5.20825]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_add_test.go at line 108
    [5.21725][5.21725:21855]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "BirdNET", Duration: 10, Reviewer: "AI"},
    Segments: []*utils.Segment{
    [5.21725]
    [5.21855]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "BirdNET", Duration: 10, Reviewer: "AI"},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_add_test.go at line 116
    [5.21936][5.21936:21964]()
    Labels: []*utils.Label{
    [5.21936]
    [5.21964]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_add_test.go at line 142
    [5.22477][5.22477:22520]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.22477]
    [5.22520]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_add_test.go at line 164
    [5.23133][5.23133:23246]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.23133]
    [5.23246]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_add_test.go at line 172
    [5.23327][5.23327:23355]()
    Labels: []*utils.Label{
    [5.23327]
    [5.23355]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_add_test.go at line 203
    [5.23972][5.23972:24085]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*utils.Segment{
    [5.23972]
    [5.24085]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
    Segments: []*datafile.Segment{
  • replacement in tools/calls/calls_add_test.go at line 211
    [5.24166][5.24166:24194]()
    Labels: []*utils.Label{
    [5.24166]
    [5.24194]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_add_test.go at line 244
    [5.24886][5.24886:24929]()
    df2, err := utils.ParseDataFile(dataPath)
    [5.24886]
    [5.24929]
    df2, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_add_test.go at line 344
    [5.853][5.853:895]()
    df, err := utils.ParseDataFile(dataPath)
    [5.853]
    [5.895]
    df, err := datafile.ParseDataFile(dataPath)
  • replacement in tools/calls/calls_add.go at line 8
    [5.27652][5.27652:27668]()
    "skraak/utils"
    [5.27652]
    [5.62016]
    "skraak/datafile"
  • replacement in tools/calls/calls_add.go at line 121
    [5.31073][5.31073:31186]()
    func findExactSegment(segments []*utils.Segment, startTime, endTime, freqLow, freqHigh float64) *utils.Segment {
    [5.31073]
    [5.31186]
    func findExactSegment(segments []*datafile.Segment, startTime, endTime, freqLow, freqHigh float64) *datafile.Segment {
  • replacement in tools/calls/calls_add.go at line 152
    [5.3056][5.3056:3147]()
    func resolveDuration(dataPath string, df *utils.DataFile, newFile bool) (float64, error) {
    [5.3056]
    [5.3147]
    func resolveDuration(dataPath string, df *datafile.DataFile, newFile bool) (float64, error) {
  • replacement in tools/calls/calls_add.go at line 185
    [5.32929][5.32929:33073]()
    func addLabelToSegment(segment *utils.Segment, species, callType string, input CallsAddInput, output *CallsAddOutput) (CallsAddOutput, error) {
    [5.32929]
    [5.33073]
    func addLabelToSegment(segment *datafile.Segment, species, callType string, input CallsAddInput, output *CallsAddOutput) (CallsAddOutput, error) {
  • replacement in tools/calls/calls_add.go at line 192
    [5.33328][5.33328:33383]()
    segment.Labels = append(segment.Labels, &utils.Label{
    [5.33328]
    [5.33383]
    segment.Labels = append(segment.Labels, &datafile.Label{
  • replacement in tools/calls/calls_add.go at line 203
    [5.33610][5.33610:33782]()
    func createNewSegment(dataFile *utils.DataFile, species, callType string, input CallsAddInput, output *CallsAddOutput) (CallsAddOutput, error) {
    newSeg := &utils.Segment{
    [5.33610]
    [5.33782]
    func createNewSegment(dataFile *datafile.DataFile, species, callType string, input CallsAddInput, output *CallsAddOutput) (CallsAddOutput, error) {
    newSeg := &datafile.Segment{
  • replacement in tools/calls/calls_add.go at line 209
    [5.33907][5.33907:33933]()
    Labels: []*utils.Label{
    [5.33907]
    [5.33933]
    Labels: []*datafile.Label{
  • replacement in tools/calls/calls_add.go at line 239
    [5.34621][5.34621:34685]()
    species, callType := utils.ParseSpeciesCallType(input.Species)
    [5.34621]
    [5.34685]
    species, callType := datafile.ParseSpeciesCallType(input.Species)
  • replacement in tools/calls/calls_add.go at line 313
    [5.36574][5.36574:36663]()
    func loadOrCreateDataFile(path string, reviewer string) (*utils.DataFile, bool, error) {
    [5.36574]
    [5.36663]
    func loadOrCreateDataFile(path string, reviewer string) (*datafile.DataFile, bool, error) {
  • replacement in tools/calls/calls_add.go at line 315
    [5.36713][5.36713:36764]()
    df := &utils.DataFile{
    Meta: &utils.DataMeta{
    [5.36713]
    [5.36764]
    df := &datafile.DataFile{
    Meta: &datafile.DataMeta{
  • replacement in tools/calls/calls_add.go at line 320
    [5.36818][5.36818:36851]()
    Segments: []*utils.Segment{},
    [5.36818]
    [5.36851]
    Segments: []*datafile.Segment{},
  • replacement in tools/calls/calls_add.go at line 325
    [5.36906][5.36906:36944]()
    df, err := utils.ParseDataFile(path)
    [5.36906]
    [5.36944]
    df, err := datafile.ParseDataFile(path)
  • replacement in db/utils.go at line 3
    [5.2511][5.6137:6164]()
    import (
    "skraak/utils"
    )
    [5.2511]
    [5.2528]
    import "strings"
  • replacement in db/utils.go at line 5
    [5.2529][5.6165:6266]()
    // Placeholders generates SQL placeholder string for IN clauses.
    // Delegates to utils.Placeholders.
    [5.2529]
    [5.2593]
    // Placeholders generates a SQL placeholder string for IN clauses (e.g. "?, ?, ?").
  • replacement in db/utils.go at line 7
    [5.2627][5.6267:6297]()
    return utils.Placeholders(n)
    [5.2627]
    [5.2749]
    if n == 0 {
    return ""
    }
    ph := make([]string, n)
    for i := range ph {
    ph[i] = "?"
    }
    return strings.Join(ph, ", ")
  • file addition: datafile (d--r------)
    [40.1]
  • file addition: find_data_files_test.go (----------)
    [0.30458]
    package datafile
    import (
    "os"
    "path/filepath"
    "sort"
    "testing"
    )
    func TestFindDataFiles_Basic(t *testing.T) {
    dir := t.TempDir()
    // Create some .data files
    for _, name := range []string{"a.data", "b.data", "c.data"} {
    if err := os.WriteFile(filepath.Join(dir, name), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    }
    // Create a non-.data file that should be ignored
    if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("ignore"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    sort.Strings(files)
    if len(files) != 3 {
    t.Fatalf("expected 3 files, got %d: %v", len(files), files)
    }
    for i, base := range []string{"a.data", "b.data", "c.data"} {
    expected := filepath.Join(dir, base)
    if files[i] != expected {
    t.Errorf("file %d: got %q, want %q", i, files[i], expected)
    }
    }
    }
    func TestFindDataFiles_SkipsHidden(t *testing.T) {
    dir := t.TempDir()
    // Regular .data file
    if err := os.WriteFile(filepath.Join(dir, "visible.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    // Hidden .data file (should be skipped)
    if err := os.WriteFile(filepath.Join(dir, ".hidden.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 1 {
    t.Fatalf("expected 1 file (hidden skipped), got %d: %v", len(files), files)
    }
    if filepath.Base(files[0]) != "visible.data" {
    t.Errorf("got %q, want visible.data", files[0])
    }
    }
    func TestFindDataFiles_NonRecursive(t *testing.T) {
    dir := t.TempDir()
    // .data file in root
    if err := os.WriteFile(filepath.Join(dir, "root.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    // .data file in subdirectory (should NOT be found)
    sub := filepath.Join(dir, "subdir")
    if err := os.Mkdir(sub, 0755); err != nil {
    t.Fatal(err)
    }
    if err := os.WriteFile(filepath.Join(sub, "nested.data"), []byte("[]"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 1 {
    t.Fatalf("expected 1 file (non-recursive), got %d: %v", len(files), files)
    }
    if filepath.Base(files[0]) != "root.data" {
    t.Errorf("got %q, want root.data", files[0])
    }
    }
    func TestFindDataFiles_EmptyDir(t *testing.T) {
    dir := t.TempDir()
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 0 {
    t.Errorf("expected 0 files, got %d", len(files))
    }
    }
    func TestFindDataFiles_NonexistentDir(t *testing.T) {
    _, err := FindDataFiles("/nonexistent/path/12345")
    if err == nil {
    t.Error("expected error for nonexistent directory")
    }
    }
    func TestFindDataFiles_NoDataFiles(t *testing.T) {
    dir := t.TempDir()
    if err := os.WriteFile(filepath.Join(dir, "readme.txt"), []byte("hello"), 0644); err != nil {
    t.Fatal(err)
    }
    files, err := FindDataFiles(dir)
    if err != nil {
    t.Fatal(err)
    }
    if len(files) != 0 {
    t.Errorf("expected 0 files, got %d", len(files))
    }
    }
  • file addition: data_file_test.go (----------)
    [0.30458]
    package datafile
    import (
    "os"
    "testing"
    )
    func TestDataFileParse(t *testing.T) {
    // Create a test .data file
    content := `[
    {"Operator": "Auto", "Reviewer": null, "Duration": 60.0},
    [10.0, 20.0, 0, 0, [{"species": "Kiwi", "certainty": 70, "filter": "test-filter"}]],
    [30.0, 40.0, 1000, 5000, [{"species": "Morepork", "certainty": 80, "filter": "M"}]]
    ]`
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    defer os.Remove(tmpfile.Name())
    if _, err := tmpfile.Write([]byte(content)); err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    // Parse
    df, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Check metadata
    if df.Meta.Operator != "Auto" {
    t.Errorf("expected Operator=Auto, got %s", df.Meta.Operator)
    }
    if df.Meta.Duration != 60.0 {
    t.Errorf("expected Duration=60.0, got %f", df.Meta.Duration)
    }
    // Check segments
    if len(df.Segments) != 2 {
    t.Errorf("expected 2 segments, got %d", len(df.Segments))
    }
    // Check first segment (sorted by start time)
    if df.Segments[0].StartTime != 10.0 {
    t.Errorf("expected StartTime=10.0, got %f", df.Segments[0].StartTime)
    }
    if df.Segments[0].EndTime != 20.0 {
    t.Errorf("expected EndTime=20.0, got %f", df.Segments[0].EndTime)
    }
    // Check labels
    if len(df.Segments[0].Labels) != 1 {
    t.Errorf("expected 1 label, got %d", len(df.Segments[0].Labels))
    }
    if df.Segments[0].Labels[0].Species != "Kiwi" {
    t.Errorf("expected Species=Kiwi, got %s", df.Segments[0].Labels[0].Species)
    }
    if df.Segments[0].Labels[0].Certainty != 70 {
    t.Errorf("expected Certainty=70, got %d", df.Segments[0].Labels[0].Certainty)
    }
    }
    func TestDataFileWrite(t *testing.T) {
    df := &DataFile{
    FilePath: "",
    Meta: &DataMeta{
    Operator: "Test",
    Reviewer: "David",
    Duration: 120.0,
    },
    Segments: []*Segment{
    {
    StartTime: 5.0,
    EndTime: 15.0,
    FreqLow: 0,
    FreqHigh: 0,
    Labels: []*Label{
    {Species: "Kiwi", Certainty: 100, Filter: "test"},
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse and verify
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df2.Meta.Reviewer != "David" {
    t.Errorf("expected Reviewer=David, got %s", df2.Meta.Reviewer)
    }
    if len(df2.Segments) != 1 {
    t.Errorf("expected 1 segment, got %d", len(df2.Segments))
    }
    if df2.Segments[0].Labels[0].Species != "Kiwi" {
    t.Errorf("expected Species=Kiwi, got %s", df2.Segments[0].Labels[0].Species)
    }
    }
    func TestHasFilterLabel(t *testing.T) {
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "test-filter"},
    {Species: "Morepork", Filter: "M"},
    },
    }
    if !seg.HasFilterLabel("test-filter") {
    t.Error("expected HasFilterLabel(test-filter)=true")
    }
    if !seg.HasFilterLabel("M") {
    t.Error("expected HasFilterLabel(M)=true")
    }
    if seg.HasFilterLabel("other") {
    t.Error("expected HasFilterLabel(other)=false")
    }
    if !seg.HasFilterLabel("") {
    t.Error("expected HasFilterLabel('')=true (no filter)")
    }
    }
    func TestGetFilterLabels(t *testing.T) {
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "test-filter", Certainty: 70},
    {Species: "Morepork", Filter: "M", Certainty: 80},
    {Species: "Don't Know", Filter: "test-filter", Certainty: 0},
    },
    }
    labels := seg.GetFilterLabels("test-filter")
    if len(labels) != 2 {
    t.Errorf("expected 2 labels, got %d", len(labels))
    }
    labels = seg.GetFilterLabels("")
    if len(labels) != 3 {
    t.Errorf("expected 3 labels (no filter), got %d", len(labels))
    }
    }
    func TestLabelComment(t *testing.T) {
    // Test parsing comment from .data file
    content := `[
    {"Operator": "Test", "Duration": 60.0},
    [10.0, 20.0, 0, 0, [{"species": "Kiwi", "certainty": 100, "filter": "M", "comment": "Good call"}]]
    ]`
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    defer os.Remove(tmpfile.Name())
    if _, err := tmpfile.Write([]byte(content)); err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    df, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df.Segments[0].Labels[0].Comment != "Good call" {
    t.Errorf("expected Comment='Good call', got '%s'", df.Segments[0].Labels[0].Comment)
    }
    // Test writing comment
    df.Segments[0].Labels[0].Comment = "Updated comment"
    tmpfile2, err := os.CreateTemp("", "test2*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile2.Close()
    defer os.Remove(tmpfile2.Name())
    if err := df.Write(tmpfile2.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse and verify
    df2, err := ParseDataFile(tmpfile2.Name())
    if err != nil {
    t.Fatal(err)
    }
    if df2.Segments[0].Labels[0].Comment != "Updated comment" {
    t.Errorf("expected Comment='Updated comment', got '%s'", df2.Segments[0].Labels[0].Comment)
    }
    }
    func TestSkraakHashRoundTrip(t *testing.T) {
    // Test that skraak_hash in metadata is preserved through parse/write cycle
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    Extra: map[string]any{
    "skraak_hash": "abc123def456",
    },
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {Species: "Kiwi", Certainty: 100, Filter: "M"},
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_hash preserved
    if df2.Meta.Extra == nil {
    t.Fatal("expected Extra to be non-nil")
    }
    hash, ok := df2.Meta.Extra["skraak_hash"].(string)
    if !ok {
    t.Fatal("expected skraak_hash to be string")
    }
    if hash != "abc123def456" {
    t.Errorf("expected skraak_hash=abc123def456, got %s", hash)
    }
    }
    func TestSkraakLabelIDRoundTrip(t *testing.T) {
    // Test that skraak_label_id in labels is preserved through parse/write cycle
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {
    Species: "Kiwi",
    Certainty: 100,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_abc123",
    },
    },
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_label_id preserved
    if len(df2.Segments) != 1 {
    t.Fatalf("expected 1 segment, got %d", len(df2.Segments))
    }
    if len(df2.Segments[0].Labels) != 1 {
    t.Fatalf("expected 1 label, got %d", len(df2.Segments[0].Labels))
    }
    label := df2.Segments[0].Labels[0]
    if label.Extra == nil {
    t.Fatal("expected label Extra to be non-nil")
    }
    labelID, ok := label.Extra["skraak_label_id"].(string)
    if !ok {
    t.Fatal("expected skraak_label_id to be string")
    }
    if labelID != "label_abc123" {
    t.Errorf("expected skraak_label_id=label_abc123, got %s", labelID)
    }
    }
    func TestSkraakFieldsBothPresent(t *testing.T) {
    // Test both skraak_hash and skraak_label_id together
    df := &DataFile{
    Meta: &DataMeta{
    Operator: "Test",
    Duration: 60.0,
    Extra: map[string]any{
    "skraak_hash": "file_hash_xyz",
    },
    },
    Segments: []*Segment{
    {
    StartTime: 10.0,
    EndTime: 20.0,
    Labels: []*Label{
    {
    Species: "Kiwi",
    Certainty: 100,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_id_1",
    },
    },
    {
    Species: "Roroa",
    Certainty: 90,
    Filter: "M",
    Extra: map[string]any{
    "skraak_label_id": "label_id_2",
    },
    },
    },
    },
    },
    }
    tmpfile, err := os.CreateTemp("", "test*.data")
    if err != nil {
    t.Fatal(err)
    }
    tmpfile.Close()
    defer os.Remove(tmpfile.Name())
    // Write
    if err := df.Write(tmpfile.Name()); err != nil {
    t.Fatal(err)
    }
    // Re-parse
    df2, err := ParseDataFile(tmpfile.Name())
    if err != nil {
    t.Fatal(err)
    }
    // Verify skraak_hash
    if df2.Meta.Extra["skraak_hash"] != "file_hash_xyz" {
    t.Errorf("expected skraak_hash=file_hash_xyz, got %v", df2.Meta.Extra["skraak_hash"])
    }
    // Verify both label IDs
    if len(df2.Segments[0].Labels) != 2 {
    t.Fatalf("expected 2 labels, got %d", len(df2.Segments[0].Labels))
    }
    labelIDs := []string{"label_id_1", "label_id_2"}
    for i, label := range df2.Segments[0].Labels {
    if label.Extra["skraak_label_id"] != labelIDs[i] {
    t.Errorf("label %d: expected skraak_label_id=%s, got %v", i, labelIDs[i], label.Extra["skraak_label_id"])
    }
    }
    }
    func TestSegmentMatchesFilters(t *testing.T) {
    // Create test segments with various labels
    seg := &Segment{
    Labels: []*Label{
    {Species: "Kiwi", Filter: "model-1.0", CallType: "Duet", Certainty: 70},
    {Species: "Morepork", Filter: "model-2.0", CallType: "", Certainty: 100},
    },
    }
    tests := []struct {
    name string
    filter string
    species string
    callType string
    certainty int
    want bool
    }{
    {"no filters", "", "", "", -1, true},
    {"filter only match", "model-1.0", "", "", -1, true},
    {"filter only no match", "model-3.0", "", "", -1, false},
    {"species only match", "", "Kiwi", "", -1, true},
    {"species only no match", "", "Tomtit", "", -1, false},
    {"calltype only match", "", "", "Duet", -1, true},
    {"calltype only no match", "", "", "Male", -1, false},
    {"certainty match", "", "", "", 70, true},
    {"certainty no match", "", "", "", 80, false},
    {"certainty 100 match", "", "", "", 100, true},
    {"filter+species match", "model-1.0", "Kiwi", "", -1, true},
    {"filter+species+calltype match", "model-1.0", "Kiwi", "Duet", -1, true},
    {"filter+species+calltype+certainty match", "model-1.0", "Kiwi", "Duet", 70, true},
    {"filter+species+calltype certainty miss", "model-1.0", "Kiwi", "Duet", 100, false},
    {"filter match species miss", "model-1.0", "Morepork", "", -1, false},
    {"all miss", "model-3.0", "Tomtit", "Male", -1, false},
    {"CallTypeNone matches empty calltype", "model-2.0", "Morepork", CallTypeNone, -1, true},
    {"CallTypeNone skips non-empty calltype", "model-1.0", "Kiwi", CallTypeNone, -1, false},
    {"CallTypeNone + certainty match", "model-2.0", "Morepork", CallTypeNone, 100, true},
    {"CallTypeNone + certainty miss", "model-2.0", "Morepork", CallTypeNone, 70, false},
    }
    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    got := seg.SegmentMatchesFilters(tt.filter, tt.species, tt.callType, tt.certainty)
    if got != tt.want {
    t.Errorf("SegmentMatchesFilters(%q, %q, %q, %d) = %v, want %v",
    tt.filter, tt.species, tt.callType, tt.certainty, got, tt.want)
    }
    })
    }
    }
    func TestParseSpeciesCallType(t *testing.T) {
    tests := []struct {
    input string
    species string
    callType string
    }{
    {"", "", ""},
    {"Kiwi", "Kiwi", ""},
    {"Kiwi+Duet", "Kiwi", "Duet"},
    {"GSK+Female", "GSK", "Female"},
    {"Species+With+Multiple+Plus", "Species", "With+Multiple+Plus"},
    {"Kiwi+_", "Kiwi", "_"},
    }
    for _, tt := range tests {
    t.Run(tt.input, func(t *testing.T) {
    species, callType := ParseSpeciesCallType(tt.input)
    if species != tt.species || callType != tt.callType {
    t.Errorf("ParseSpeciesCallType(%q) = (%q, %q), want (%q, %q)",
    tt.input, species, callType, tt.species, tt.callType)
    }
    })
    }
    }
  • file addition: data_file.go (----------)
    [0.30458]
    package datafile
    import (
    "encoding/json"
    "fmt"
    "maps"
    "os"
    "sort"
    "strings"
    "skraak/utils"
    )
    // DataFile represents an AviaNZ .data file
    type DataFile struct {
    Meta *DataMeta
    Segments []*Segment
    FilePath string
    }
    // DataMeta contains metadata for a .data file
    type DataMeta struct {
    Operator string
    Reviewer string
    Duration float64
    Extra map[string]any // preserve unknown fields
    }
    // Segment represents a detection segment
    type Segment struct {
    StartTime float64
    EndTime float64
    FreqLow float64
    FreqHigh float64
    Labels []*Label
    }
    // CallTypeNone is a sentinel value used in --species Species+_ to match
    // only labels with an empty calltype.
    const CallTypeNone = "_"
    // Label represents a species label within a segment
    type Label struct {
    Species string
    Certainty int
    Filter string
    CallType string
    Comment string // user comment (max 140 chars, ASCII only)
    Bookmark bool // user bookmark for navigation
    Extra map[string]any // preserve unknown fields
    }
    // ParseDataFile reads and parses a .data file
    func ParseDataFile(path string) (*DataFile, error) {
    data, err := os.ReadFile(path)
    if err != nil {
    return nil, err
    }
    var raw []json.RawMessage
    if err := json.Unmarshal(data, &raw); err != nil {
    return nil, fmt.Errorf("parse JSON: %w", err)
    }
    if len(raw) == 0 {
    return nil, fmt.Errorf("empty .data file")
    }
    df := &DataFile{
    FilePath: path,
    Segments: make([]*Segment, 0, len(raw)-1),
    }
    // Parse metadata (first element)
    df.Meta = parseMeta(raw[0])
    // Parse segments
    for i := 1; i < len(raw); i++ {
    seg, err := parseSegment(raw[i])
    if err != nil {
    continue // skip invalid segments
    }
    df.Segments = append(df.Segments, seg)
    }
    // Sort segments by start time
    sort.Slice(df.Segments, func(i, j int) bool {
    return df.Segments[i].StartTime < df.Segments[j].StartTime
    })
    return df, nil
    }
    // parseMeta parses the metadata object
    func parseMeta(raw json.RawMessage) *DataMeta {
    var obj map[string]any
    if err := json.Unmarshal(raw, &obj); err != nil {
    return &DataMeta{}
    }
    meta := &DataMeta{Extra: make(map[string]any)}
    if v, ok := obj["Operator"].(string); ok {
    meta.Operator = v
    delete(obj, "Operator")
    }
    if v, ok := obj["Reviewer"].(string); ok {
    meta.Reviewer = v
    delete(obj, "Reviewer")
    }
    if v, ok := obj["Duration"].(float64); ok {
    meta.Duration = v
    delete(obj, "Duration")
    }
    // Store remaining fields
    maps.Copy(meta.Extra, obj)
    return meta
    }
    // parseSegment parses a segment array
    func parseSegment(raw json.RawMessage) (*Segment, error) {
    var arr []json.RawMessage
    if err := json.Unmarshal(raw, &arr); err != nil {
    return nil, err
    }
    if len(arr) < 5 {
    return nil, fmt.Errorf("segment too short")
    }
    seg := &Segment{}
    // Parse time and frequency
    if v, err := parseFloat(arr[0]); err == nil {
    seg.StartTime = v
    }
    if v, err := parseFloat(arr[1]); err == nil {
    seg.EndTime = v
    }
    if v, err := parseFloat(arr[2]); err == nil {
    seg.FreqLow = v
    }
    if v, err := parseFloat(arr[3]); err == nil {
    seg.FreqHigh = v
    }
    // Parse labels
    var labelArr []json.RawMessage
    if err := json.Unmarshal(arr[4], &labelArr); err == nil {
    for _, labelRaw := range labelArr {
    if label := parseLabel(labelRaw); label != nil {
    seg.Labels = append(seg.Labels, label)
    }
    }
    }
    // Sort labels alphabetically by species
    sort.Slice(seg.Labels, func(i, j int) bool {
    return seg.Labels[i].Species < seg.Labels[j].Species
    })
    return seg, nil
    }
    // parseLabel parses a label object
    func parseLabel(raw json.RawMessage) *Label {
    var obj map[string]any
    if err := json.Unmarshal(raw, &obj); err != nil {
    return nil
    }
    label := &Label{Extra: make(map[string]any)}
    if v, ok := obj["species"].(string); ok {
    label.Species = v
    delete(obj, "species")
    }
    if v, ok := obj["certainty"].(float64); ok {
    label.Certainty = int(v)
    delete(obj, "certainty")
    }
    if v, ok := obj["filter"].(string); ok {
    label.Filter = v
    delete(obj, "filter")
    }
    if v, ok := obj["calltype"].(string); ok {
    label.CallType = v
    delete(obj, "calltype")
    }
    if v, ok := obj["comment"].(string); ok {
    label.Comment = v
    delete(obj, "comment")
    }
    if v, ok := obj["bookmark"].(bool); ok {
    label.Bookmark = v
    delete(obj, "bookmark")
    }
    // Store remaining fields
    maps.Copy(label.Extra, obj)
    return label
    }
    // parseFloat extracts a float from JSON
    func parseFloat(raw json.RawMessage) (float64, error) {
    var v float64
    err := json.Unmarshal(raw, &v)
    return v, err
    }
    // WriteDataFile writes a DataFile back to disk
    func (df *DataFile) Write(path string) error {
    var raw []any
    // Build metadata
    meta := make(map[string]any)
    if df.Meta.Operator != "" {
    meta["Operator"] = df.Meta.Operator
    }
    if df.Meta.Reviewer != "" {
    meta["Reviewer"] = df.Meta.Reviewer
    }
    if df.Meta.Duration > 0 {
    meta["Duration"] = df.Meta.Duration
    }
    maps.Copy(meta, df.Meta.Extra)
    raw = append(raw, meta)
    // Build segments
    for _, seg := range df.Segments {
    labels := make([]any, 0, len(seg.Labels))
    for _, label := range seg.Labels {
    l := make(map[string]any)
    l["species"] = label.Species
    l["certainty"] = label.Certainty
    if label.Filter != "" {
    l["filter"] = label.Filter
    }
    if label.CallType != "" {
    l["calltype"] = label.CallType
    }
    if label.Comment != "" {
    l["comment"] = label.Comment
    }
    if label.Bookmark {
    l["bookmark"] = true
    }
    maps.Copy(l, label.Extra)
    labels = append(labels, l)
    }
    segArr := []any{
    seg.StartTime,
    seg.EndTime,
    seg.FreqLow,
    seg.FreqHigh,
    labels,
    }
    raw = append(raw, segArr)
    }
    data, err := json.MarshalIndent(raw, "", " ")
    if err != nil {
    return err
    }
    return os.WriteFile(path, data, 0644)
    }
    // HasFilterLabel returns true if segment has a label matching the filter
    func (s *Segment) HasFilterLabel(filter string) bool {
    if filter == "" {
    return true
    }
    for _, label := range s.Labels {
    if label.Filter == filter {
    return true
    }
    }
    return false
    }
    // GetFilterLabels returns labels matching the filter
    func (s *Segment) GetFilterLabels(filter string) []*Label {
    var result []*Label
    for _, label := range s.Labels {
    if filter == "" || label.Filter == filter {
    result = append(result, label)
    }
    }
    return result
    }
    // SegmentMatchesFilters returns true if the segment has any label matching all filter criteria.
    // All non-empty/non-negative parameters must match for a label to be considered a match.
    // Use certainty=-1 to indicate no certainty filtering (since 0 is a valid certainty value).
    func (s *Segment) SegmentMatchesFilters(filter, species, callType string, certainty int) bool {
    if filter == "" && species == "" && callType == "" && certainty < 0 {
    return true // No filters, match all
    }
    for _, label := range s.Labels {
    if labelMatchesFilters(label, filter, species, callType, certainty) {
    return true
    }
    }
    return false
    }
    // labelMatchesFilters checks if a single label matches all filter criteria.
    func labelMatchesFilters(label *Label, filter, species, callType string, certainty int) bool {
    if filter != "" && label.Filter != filter {
    return false
    }
    if species != "" && label.Species != species {
    return false
    }
    if callType == CallTypeNone {
    if label.CallType != "" {
    return false
    }
    } else if callType != "" && label.CallType != callType {
    return false
    }
    if certainty >= 0 && label.Certainty != certainty {
    return false
    }
    return true
    }
    // ParseSpeciesCallType parses a species string with optional calltype into separate values.
    // Format: "Species" or "Species+CallType" (e.g., "Kiwi" or "Kiwi+Duet").
    // Use "_" as the calltype to match only labels with no calltype (e.g., "Kiwi+_").
    func ParseSpeciesCallType(label string) (species, callType string) {
    if label == "" {
    return "", ""
    }
    if before, after, ok := strings.Cut(label, "+"); ok {
    return before, after
    }
    return label, ""
    }
    // FindDataFiles finds all .data files in a folder, ignoring hidden files (starting with ".")
    func FindDataFiles(folder string) ([]string, error) {
    return utils.FindFiles(folder, utils.FindFilesOptions{
    Extension: ".data",
    Recursive: false,
    SkipHidden: true,
    })
    }
  • file addition: config (d--r------)
    [40.1]
  • file addition: config_test.go (----------)
    [0.53507]
    package config
    import (
    "os"
    "path/filepath"
    "testing"
    )
    func TestLoadConfig(t *testing.T) {
    homeDir := t.TempDir()
    t.Setenv("HOME", homeDir)
    configDir := filepath.Join(homeDir, ".skraak")
    err := os.MkdirAll(configDir, 0755)
    if err != nil {
    t.Fatalf("failed to create config dir: %v", err)
    }
    jsonContent := `{
    "classify": {
    "reviewer": "Test Reviewer",
    "color": true
    }
    }`
    err = os.WriteFile(filepath.Join(configDir, "config.json"), []byte(jsonContent), 0644)
    if err != nil {
    t.Fatalf("failed to write config: %v", err)
    }
    cfg, path, err := LoadConfig()
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    if cfg.Classify.Reviewer != "Test Reviewer" {
    t.Errorf("expected Test Reviewer, got %s", cfg.Classify.Reviewer)
    }
    if !cfg.Classify.Color {
    t.Error("expected color to be true")
    }
    if path == "" {
    t.Error("expected path to be returned")
    }
    }
  • file addition: config.go (----------)
    [0.53507]
    package config
    import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    )
    // ~/.skraak/config.json schema (reference):
    //
    // {
    // "classify": {
    // "reviewer": "string, required. Name stamped into .data file meta on any edit.",
    // "color": "bool, optional. Colored spectrograms in the TUI. Default false.",
    // "sixel": "bool, optional. Use sixel image protocol. Default false (Kitty).",
    // "iterm": "bool, optional. Use iTerm inline-image protocol. Default false.",
    // "img_dims": "int, optional. Spectrogram display size in pixels. 0 = default.",
    //
    // "bindings": {
    // "<key>": "Species" // e.g. "c": "comcha"
    // "<key>": "Species+CallType" // e.g. "1": "Kiwi+Duet"
    // // <key> is a single character. Reserved: ",", ".", "0", " " (space).
    // // Pressing <key> labels the current segment (certainty 100, or 0 for
    // // "Don't Know"), saves, and advances.
    // },
    //
    // "secondary_bindings": {
    // "<primary-key>": {
    // "<key>": "CallType" // e.g. "a": "alarm"
    // // <key> is a single character, same reserved-key rules as bindings.
    // // Outer <primary-key> must also exist in "bindings".
    // }
    // // Optional. Invoked via Shift+<primary-key>: labels the species with
    // // an empty calltype, does NOT advance, and waits for one follow-up
    // // key looked up in this inner map. Match -> set calltype, save,
    // // advance. Esc -> exit wait mode without advancing. Any other key ->
    // // exit wait mode and handle the key normally.
    // // Shift+<primary-key> on a primary without a secondary_bindings entry
    // // falls back to normal primary behavior.
    // }
    // }
    // }
    //
    // Example:
    //
    // {
    // "classify": {
    // "reviewer": "David",
    // "color": true,
    // "bindings": {
    // "c": "comcha",
    // "k": "kea1",
    // "x": "Noise",
    // "z": "Don't Know",
    // "1": "Kiwi+Duet",
    // "4": "Kiwi"
    // },
    // "secondary_bindings": {
    // "c": { "a": "alarm", "s": "song", "n": "contact" }
    // }
    // }
    // }
    //
    // Config holds user-level defaults loaded from ~/.skraak/config.json.
    // Per-subcommand sections live as named fields.
    type Config struct {
    Classify ClassifyFileConfig `json:"classify"`
    }
    // ClassifyFileConfig holds defaults for `skraak calls classify`.
    // Bindings maps a single-character key to "Species" or "Species+CallType".
    type ClassifyFileConfig struct {
    Reviewer string `json:"reviewer"`
    Color bool `json:"color"`
    Sixel bool `json:"sixel"`
    ITerm bool `json:"iterm"`
    ImgDims int `json:"img_dims"`
    Bindings map[string]string `json:"bindings"`
    // SecondaryBindings extends a primary binding with per-species calltype
    // choices. Outer key is the primary binding key; inner map is
    // single-char key -> calltype string. Invoked via Shift+primary-key.
    SecondaryBindings map[string]map[string]string `json:"secondary_bindings,omitempty"`
    }
    // ConfigPath returns the absolute path to ~/.skraak/config.json.
    func ConfigPath() (string, error) {
    home, err := os.UserHomeDir()
    if err != nil {
    return "", fmt.Errorf("resolving home directory: %w", err)
    }
    return filepath.Join(home, ".skraak", "config.json"), nil
    }
    // LoadConfig reads ~/.skraak/config.json and returns the parsed config and the
    // resolved path (useful for error messages).
    func LoadConfig() (Config, string, error) {
    var cfg Config
    path, err := ConfigPath()
    if err != nil {
    return cfg, "", err
    }
    data, err := os.ReadFile(path)
    if err != nil {
    return cfg, path, fmt.Errorf("reading %s: %w", path, err)
    }
    if err := json.Unmarshal(data, &cfg); err != nil {
    return cfg, path, fmt.Errorf("parsing %s: %w", path, err)
    }
    return cfg, path, nil
    }
  • edit in cmd/calls_remove.go at line 9
    [5.48423]
    [5.48423]
    "skraak/config"
  • edit in cmd/calls_remove.go at line 11
    [5.48445][5.48445:48461]()
    "skraak/utils"
  • replacement in cmd/calls_remove.go at line 116
    [5.52235][5.52235:52277]()
    cfg, cfgPath, err := utils.LoadConfig()
    [5.52235]
    [5.52277]
    cfg, cfgPath, err := config.LoadConfig()
  • edit in cmd/calls_push_certainty.go at line 8
    [5.1104046]
    [5.314673]
    "skraak/config"
    "skraak/datafile"
  • replacement in cmd/calls_push_certainty.go at line 117
    [5.26525][5.1108424:1108465](),[5.1108424][5.1108424:1108465]()
    cfg, cfgPath, err := utils.LoadConfig()
    [5.26525]
    [5.1108465]
    cfg, cfgPath, err := config.LoadConfig()
  • replacement in cmd/calls_push_certainty.go at line 126
    [5.1108805][5.26526:26590]()
    speciesName, callType := utils.ParseSpeciesCallType(f.species)
    [5.1108805]
    [5.1108867]
    speciesName, callType := datafile.ParseSpeciesCallType(f.species)
  • edit in cmd/calls_classify.go at line 11
    [5.1141784]
    [5.315343]
    "skraak/config"
    "skraak/datafile"
  • replacement in cmd/calls_classify.go at line 235
    [5.8424][5.315366:315453]()
    func validateBindings(cfg *utils.Config, cfgPath string) ([]calls.KeyBinding, error) {
    [5.8424]
    [5.1149871]
    func validateBindings(cfg *config.Config, cfgPath string) ([]calls.KeyBinding, error) {
  • replacement in cmd/calls_classify.go at line 276
    [5.21276][5.21276:21354](),[5.21354][5.11643:11684](),[5.11643][5.11643:11684]()
    func loadClassifyConfig() (utils.Config, string, []calls.KeyBinding, error) {
    cfg, cfgPath, err := utils.LoadConfig()
    [5.21276]
    [5.11684]
    func loadClassifyConfig() (config.Config, string, []calls.KeyBinding, error) {
    cfg, cfgPath, err := config.LoadConfig()
  • replacement in cmd/calls_classify.go at line 298
    [5.21783][5.21783:21903](),[5.21903][5.12258:12322](),[5.1151565][5.12258:12322]()
    func buildClassifyConfig(a classifyArgs, cfg utils.Config, bindings []calls.KeyBinding) (calls.ClassifyConfig, error) {
    speciesName, callType := utils.ParseSpeciesCallType(a.species)
    [5.21783]
    [5.1151627]
    func buildClassifyConfig(a classifyArgs, cfg config.Config, bindings []calls.KeyBinding) (calls.ClassifyConfig, error) {
    speciesName, callType := datafile.ParseSpeciesCallType(a.species)
  • edit in cmd/calls_add.go at line 10
    [5.53607]
    [5.53607]
    "skraak/config"
  • edit in cmd/calls_add.go at line 12
    [5.53629][5.53629:53645]()
    "skraak/utils"
  • replacement in cmd/calls_add.go at line 131
    [5.57636][5.57636:57678]()
    cfg, cfgPath, err := utils.LoadConfig()
    [5.57636]
    [5.57678]
    cfg, cfgPath, err := config.LoadConfig()
  • replacement in CLAUDE.md at line 17
    [4.323][4.323:796]()
    cmd/ → tools, tools/calls, tools/import, tui
    tools/ → db, audio, wav, spectrogram, astro, mapping, utils
    tools/calls/ → db, audio, wav, spectrogram, mapping, utils (NO DB access in practice)
    tools/import/ → db, wav, astro, mapping, utils (defines own Mutator/Reader)
    tui/ → audio, wav, spectrogram, utils (NO db, NO cmd)
    db/ → wav (GainLevel only), utils (Placeholders only)
    [4.323]
    [4.796]
    cmd/ → tools, tools/calls, tools/import, tui, config
    tools/calls/ → audio, wav, spectrogram, datafile, mapping, utils (filesystem only, NO db)
    tools/import/ → db, wav, astro, datafile, mapping, utils (defines own Mutator/Reader)
    tools/ → db, audio, wav, spectrogram, datafile, astro, mapping, utils
    tui/ → audio, wav, spectrogram, datafile, utils (NO db, NO cmd)
    db/ → wav (GainLevel alias only)
  • edit in CLAUDE.md at line 25
    [4.869]
    [4.869]
    datafile/ → utils (FindFiles only)
  • edit in CLAUDE.md at line 28
    [4.948]
    [4.948]
    config/ → (stdlib)
  • replacement in .golangci.yml at line 39
    [4.1591][4.1591:2137]()
    # cmd → tools, tools/calls, tools/import, tui, db, audio, wav, spectrogram, astro, mapping, utils
    # tools/calls → db, audio, wav, spectrogram, mapping, utils
    # tools/import → db, wav, astro, mapping, utils
    # tools → db, audio, wav, spectrogram, astro, mapping, utils
    # tui → audio, wav, spectrogram, utils
    # db → wav, utils
    # spectrogram → audio, wav
    # wav → audio, astro, utils
    # audio, astro, mapping, utils → (no skraak/* imports)
    [4.1591]
    [4.2137]
    # cmd → tools, tools/calls, tools/import, tui, db, config, datafile, audio, wav, spectrogram, astro, mapping, utils
    # tools/calls → db, audio, wav, spectrogram, datafile, mapping, utils (filesystem only, no DB in practice)
    # tools/import → db, wav, astro, datafile, mapping, utils
    # tools → db, audio, wav, spectrogram, datafile, astro, mapping, utils
    # tui → audio, wav, spectrogram, datafile, utils (no db, no cmd)
    # db → (stdlib only after Placeholders inline; wav alias allowed)
    # spectrogram → audio, wav
    # wav → audio, astro, utils
    # datafile → utils (FindFiles only)
    # config → (stdlib only)
    # audio, astro, mapping, utils → (no skraak/* imports — true leaves)
  • edit in .golangci.yml at line 66
    [4.2769]
    [4.2769]
    config:
    files: ["**/config/*.go"]
    deny:
    - { pkg: "skraak/", desc: "config is a leaf package — no skraak/* imports" }
    datafile:
    files: ["**/datafile/*.go"]
    deny:
    - { pkg: "skraak/cmd", desc: "datafile must not import cmd" }
    - { pkg: "skraak/tools", desc: "datafile must not import tools" }
    - { pkg: "skraak/tui", desc: "datafile must not import tui" }
    - { pkg: "skraak/db", desc: "datafile must not import db" }
    - { pkg: "skraak/wav", desc: "datafile must not import wav" }
    - { pkg: "skraak/audio", desc: "datafile must not import audio" }
    - { pkg: "skraak/spectrogram", desc: "datafile must not import spectrogram" }
    - { pkg: "skraak/astro", desc: "datafile must not import astro" }
  • edit in .golangci.yml at line 89
    [4.3225]
    [4.3225]
    - { pkg: "skraak/datafile", desc: "wav must not import datafile" }
  • replacement in .golangci.yml at line 93
    [4.3303][4.3303:3913]()
    - { pkg: "skraak/cmd", desc: "spectrogram must not import cmd" }
    - { pkg: "skraak/tools", desc: "spectrogram must not import tools" }
    - { pkg: "skraak/tui", desc: "spectrogram must not import tui" }
    - { pkg: "skraak/db", desc: "spectrogram must not import db" }
    - { pkg: "skraak/utils", desc: "spectrogram should only depend on audio + wav" }
    - { pkg: "skraak/astro", desc: "spectrogram should only depend on audio + wav" }
    - { pkg: "skraak/mapping", desc: "spectrogram should only depend on audio + wav" }
    [4.3303]
    [4.3913]
    - { pkg: "skraak/cmd", desc: "spectrogram must not import cmd" }
    - { pkg: "skraak/tools", desc: "spectrogram must not import tools" }
    - { pkg: "skraak/tui", desc: "spectrogram must not import tui" }
    - { pkg: "skraak/db", desc: "spectrogram must not import db" }
    - { pkg: "skraak/utils", desc: "spectrogram should only depend on audio + wav" }
    - { pkg: "skraak/astro", desc: "spectrogram should only depend on audio + wav" }
    - { pkg: "skraak/mapping", desc: "spectrogram should only depend on audio + wav" }
    - { pkg: "skraak/datafile", desc: "spectrogram should only depend on audio + wav" }
  • replacement in .golangci.yml at line 104
    [4.3973][4.3973:4309]()
    - { pkg: "skraak/cmd", desc: "db may only import wav + utils" }
    - { pkg: "skraak/tools", desc: "db may only import wav + utils" }
    - { pkg: "skraak/tui", desc: "db may only import wav + utils" }
    - { pkg: "skraak/spectrogram", desc: "db may only import wav + utils" }
    [4.3973]
    [4.4309]
    - { pkg: "skraak/cmd", desc: "db may only import wav (GainLevel alias)" }
    - { pkg: "skraak/tools", desc: "db may only import wav (GainLevel alias)" }
    - { pkg: "skraak/tui", desc: "db may only import wav (GainLevel alias)" }
    - { pkg: "skraak/utils", desc: "db must not import utils (Placeholders is inlined)" }
    - { pkg: "skraak/spectrogram", desc: "db may only import wav (GainLevel alias)" }
    - { pkg: "skraak/datafile", desc: "db must not import datafile" }
  • edit in .golangci.yml at line 121
    [4.4869]
    [4.4869]
    - { pkg: "skraak/db", desc: "tools/calls is filesystem-only — no DB access" }