fill calls add, check duration

quietlight
May 18, 2026, 1:48 AM
M34GDDTWJ5E2N2SMNO5BENI5T6IWZNDZ23ATHGBAXUGXOK2EQQ4AC

Dependencies

  • [2] O45G7VX2 added an add and a remove command
  • [*] KZKLAINJ run out of space on nest, cleaned out

Change contents

  • edit in tools/calls/calls_add_test.go at line 313
    [2.26467]
    [2.26467]
    }
    }
    func TestCallsAddClampsToDuration(t *testing.T) {
    tmpDir := t.TempDir()
    wavPath := filepath.Join(tmpDir, "test.wav")
    dataPath := wavPath + ".data"
    createTestWAV(t, wavPath, 16000, 10)
    result, err := CallsAdd(CallsAddInput{
    File: dataPath,
    Segment: "8-15",
    Species: "sample",
    Certainty: 100,
    Filter: "Manual",
    Reviewer: "David",
    })
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    if !result.Clamped {
    t.Error("expected Clamped=true when end exceeds duration")
    }
    if result.SegmentEnd != 10.0 {
    t.Errorf("expected segment_end clamped to 10.0, got %f", result.SegmentEnd)
    }
    if result.SegmentStart != 8.0 {
    t.Errorf("expected segment_start=8.0, got %f", result.SegmentStart)
    }
    df, err := utils.ParseDataFile(dataPath)
    if err != nil {
    t.Fatalf("failed to parse file: %v", err)
    }
    if len(df.Segments) != 1 {
    t.Fatalf("expected 1 segment, got %d", len(df.Segments))
    }
    if df.Segments[0].EndTime != 10.0 {
    t.Errorf("expected persisted EndTime=10.0, got %f", df.Segments[0].EndTime)
  • edit in tools/calls/calls_add_test.go at line 356
    [2.26473]
    [2.26473]
    func TestCallsAddRejectsStartAtOrPastDuration(t *testing.T) {
    tmpDir := t.TempDir()
    wavPath := filepath.Join(tmpDir, "test.wav")
    dataPath := wavPath + ".data"
    createTestWAV(t, wavPath, 16000, 10)
    cases := []struct {
    name string
    segment string
    }{
    {"start equals duration", "10-15"},
    {"start past duration", "12-15"},
    }
    for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
    result, err := CallsAdd(CallsAddInput{
    File: dataPath,
    Segment: tc.segment,
    Species: "sample",
    Certainty: 100,
    Filter: "Manual",
    Reviewer: "David",
    })
    if err == nil {
    t.Errorf("expected error for segment %s, got nil", tc.segment)
    }
    if result.Error == "" {
    t.Error("expected error message in output")
    }
    if _, statErr := os.Stat(dataPath); !os.IsNotExist(statErr) {
    t.Error(".data file should not be created for impossible segment")
    }
    })
    }
    }
    func TestCallsAddExactDurationBoundary(t *testing.T) {
    tmpDir := t.TempDir()
    wavPath := filepath.Join(tmpDir, "test.wav")
    dataPath := wavPath + ".data"
    createTestWAV(t, wavPath, 16000, 10)
    result, err := CallsAdd(CallsAddInput{
    File: dataPath,
    Segment: "0-10",
    Species: "sample",
    Certainty: 100,
    Filter: "Manual",
    Reviewer: "David",
    })
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    if result.Clamped {
    t.Error("expected Clamped=false when end == duration exactly")
    }
    if result.SegmentEnd != 10.0 {
    t.Errorf("expected segment_end=10.0, got %f", result.SegmentEnd)
    }
    }
  • edit in tools/calls/calls_add.go at line 34
    [2.28560]
    [2.28560]
    Clamped bool `json:"clamped,omitempty"` // true if segment end was clamped to WAV duration
  • edit in tools/calls/calls_add.go at line 148
    [2.32011]
    [2.32011]
    // resolveDuration returns the authoritative duration for clamping segment bounds.
    // New .data files must read the WAV. Existing files prefer Meta.Duration (set at
    // creation from the WAV); if absent or zero, we fall back to reading the WAV.
    func resolveDuration(dataPath string, df *utils.DataFile, newFile bool) (float64, error) {
    if !newFile && df.Meta != nil && df.Meta.Duration > 0 {
    return df.Meta.Duration, nil
    }
    return getWAVDuration(dataPath)
    }
  • edit in tools/calls/calls_add.go at line 270
    [2.35521]
    [2.35521]
    if err != nil {
    return addOutputError(&output, err.Error())
    }
    duration, err := resolveDuration(input.File, dataFile, newFile)
  • edit in tools/calls/calls_add.go at line 277
    [2.35584]
    [2.35584]
    }
    if output.SegmentStart >= duration {
    return addOutputError(&output, fmt.Sprintf("segment start %.3f >= file duration %.3f (impossible)", output.SegmentStart, duration))
  • edit in tools/calls/calls_add.go at line 281
    [2.35587]
    [2.35587]
    if output.SegmentEnd > duration {
    output.SegmentEnd = duration
    output.Clamped = true
    }
  • edit in tools/calls/calls_add.go at line 287
    [2.35629]
    [2.35629]
    if newFile {
    dataFile.Meta.Duration = duration
    }
  • edit in tools/calls/calls_add.go at line 298
    [2.36000][2.36000:36212]()
    if err == nil && newFile {
    var duration float64
    duration, err = getWAVDuration(input.File)
    if err != nil {
    return addOutputError(&output, err.Error())
    }
    dataFile.Meta.Duration = duration
    }
  • edit in shell_scripts/test_calls_add_remove.sh at line 332
    [2.48015]
    [2.48015]
    # ============================================================
    echo "=== Test: calls add (end clamped to WAV duration) ==="
    # ============================================================
    CLAMP_WAV="$WORK_DIR/clamp.wav"
    CLAMP_DATA="$CLAMP_WAV.data"
    generate_wav "$CLAMP_WAV" 10 16000
    RESULT_CLAMP=$($PROJECT_DIR/skraak calls add \
    --file "$CLAMP_DATA" \
    --segment "8-15" \
    --species "sample" \
    --reviewer "David" 2>/dev/null)
    assert_json "clamped=true" "$RESULT_CLAMP" ".clamped" "true"
    assert_json "segment_end clamped" "$RESULT_CLAMP" ".segment_end" "10"
    assert_json "segment_start preserved" "$RESULT_CLAMP" ".segment_start" "8"
    # ============================================================
    echo "=== Test: calls add (start >= duration rejected) ==="
    # ============================================================
    IMPOSSIBLE_WAV="$WORK_DIR/impossible.wav"
    IMPOSSIBLE_DATA="$IMPOSSIBLE_WAV.data"
    generate_wav "$IMPOSSIBLE_WAV" 10 16000
    STDERR=$($PROJECT_DIR/skraak calls add \
    --file "$IMPOSSIBLE_DATA" \
    --segment "10-15" \
    --species "sample" \
    --reviewer "David" 2>&1 >/dev/null || true)
    assert_error "start at duration rejected" "$STDERR"
    if [ ! -f "$IMPOSSIBLE_DATA" ]; then
    echo -e " ${GREEN}PASS${NC}: .data file not created for impossible segment"
    pass=$((pass + 1))
    else
    echo -e " ${RED}FAIL${NC}: .data file should not exist for impossible segment"
    fail=$((fail + 1))
    fi
  • edit in shell_scripts/test_calls_add_remove.sh at line 375
    [2.48016]
    [2.48016]
    STDERR=$($PROJECT_DIR/skraak calls add \
    --file "$IMPOSSIBLE_DATA" \
    --segment "20-25" \
    --species "sample" \
    --reviewer "David" 2>&1 >/dev/null || true)
    assert_error "start past duration rejected" "$STDERR"
    # ============================================================
    echo "=== Test: calls add (end exactly at duration not clamped) ==="
    # ============================================================
    EXACT_WAV="$WORK_DIR/exact.wav"
    EXACT_DATA="$EXACT_WAV.data"
    generate_wav "$EXACT_WAV" 10 16000
    RESULT_EXACT=$($PROJECT_DIR/skraak calls add \
    --file "$EXACT_DATA" \
    --segment "0-10" \
    --species "sample" \
    --reviewer "David" 2>/dev/null)
    EXACT_CLAMPED=$(echo "$RESULT_EXACT" | jq -r '.clamped // false')
    if [ "$EXACT_CLAMPED" = "false" ]; then
    echo -e " ${GREEN}PASS${NC}: end == duration not flagged as clamped"
    pass=$((pass + 1))
    else
    echo -e " ${RED}FAIL${NC}: end == duration should not be clamped"
    fail=$((fail + 1))
    fi
    assert_json "segment_end equals duration" "$RESULT_EXACT" ".segment_end" "10"
  • edit in cmd/calls_add.go at line 29
    [2.54882]
    [2.54882]
    fmt.Fprintf(os.Stderr, " Segment end is clamped to WAV duration; segment start >= duration is rejected.\n")
  • edit in CHANGELOG.md at line 4
    [4.1198010]
    [2.61944]
    ## [2026-05-18] `calls add` clamps segments to WAV duration
    ### Changed
    - `skraak calls add` resolves the recording duration before writing.
    Segments whose end exceeds the duration are clamped (new
    `clamped: true` field on the JSON output). Segments whose start is
    at or past the duration are rejected and no .data file is created.
    Duration comes from existing `.data` metadata when present, otherwise
    from the WAV header.
    - Motivation: a bulk run assumed 30-minute AudioMoth files and
    wrote 60 × 30-second segments into 60-second files. Impossible
    segments are now impossible.
  • edit in CHANGELOG.md at line 18
    [2.61945]
    [2.61945]
    ### Tests
    - Unit tests in `tools/calls/calls_add_test.go`: clamp-to-duration,
    start-at/past-duration rejection, exact-boundary.
    - Integration cases in `shell_scripts/test_calls_add_remove.sh`.