Skip to content

test: Add dedicated test suite for audio_utils.py module #156

Description

@EffortlessSteven

Problem

The transcription/audio_utils.py module (321 lines) lacks a dedicated test file. This module provides the core AudioSegmentExtractor class for efficient audio segment extraction, which is used throughout the pipeline but only tested indirectly.

Analysis

Current State

  • Source file: transcription/audio_utils.py (~321 LOC)
  • Dedicated tests: None (test_audio_utils.py does not exist)
  • Indirect coverage: Partial via test_pipeline.py, test_audio_enrichment.py

Key Functionality Requiring Tests

  1. AudioSegmentExtractor class (lines 14-257):

    • Constructor (lines 28-57):

      • File existence validation
      • File type validation (must be a file, not directory)
      • Audio metadata extraction (sample_rate, total_frames, channels, duration)
      • RuntimeError on open failure
    • extract_segment() (lines 59-168):

      • Time-to-frame conversion
      • Start/end timestamp validation
      • Negative timestamp handling (with/without clamp)
      • Out-of-bounds timestamp handling (with/without clamp)
      • Minimum duration enforcement
      • Frame clamping to valid range
      • Multi-channel → mono conversion
      • Memory-efficient seeking
    • extract_segment_by_frames() (lines 170-223):

      • Frame index validation
      • Clamping behavior
      • Multi-channel handling
    • get_duration() (line 225-232):

      • Duration accessor
    • get_info() (lines 234-248):

      • Metadata dictionary construction
    • repr() (lines 250-257):

      • String representation
  2. load_full_audio() (lines 260-292):

    • Full file loading
    • File not found handling
    • Multi-channel → mono conversion
    • RuntimeError on read failure
  3. validate_wav_file() (lines 295-320):

    • Existence check
    • File type check
    • Readability validation
    • Returns bool (no exceptions)

Critical Edge Cases Not Currently Tested

  • Non-existent file → FileNotFoundError
  • Directory path → ValueError
  • Corrupted/invalid audio file → RuntimeError
  • Negative timestamps with clamp=True vs clamp=False
  • Timestamps beyond file duration
  • min_duration enforcement
  • Very short segments (< 1 frame)
  • Zero-duration segments (start == end)
  • Reverse timestamps (start > end)
  • Multi-channel audio handling
  • Frame-based extraction edge cases
  • Large file seeking performance

Recommended Solution

Create tests/test_audio_utils.py with the following structure:

# Suggested test structure

import pytest
import numpy as np
from pathlib import Path
import soundfile as sf
from transcription.audio_utils import (
    AudioSegmentExtractor,
    load_full_audio,
    validate_wav_file,
)


class TestAudioSegmentExtractorInit:
    """Tests for AudioSegmentExtractor initialization."""
    
    def test_init_valid_wav(self, sample_wav): ...
    def test_init_file_not_found(self, tmp_path): ...
    def test_init_directory_path(self, tmp_path): ...
    def test_init_invalid_audio(self, tmp_path): ...
    def test_init_extracts_metadata(self, sample_wav): ...


class TestExtractSegment:
    """Tests for extract_segment() method."""
    
    def test_extract_valid_segment(self, extractor): ...
    def test_extract_full_duration(self, extractor): ...
    def test_extract_start_equals_end(self, extractor): ...
    def test_extract_reverse_timestamps_raises(self, extractor): ...
    
    # Negative timestamp tests
    def test_negative_start_clamped(self, extractor): ...
    def test_negative_start_raises_without_clamp(self, extractor): ...
    def test_negative_end_clamped(self, extractor): ...
    def test_negative_end_raises_without_clamp(self, extractor): ...
    
    # Out-of-bounds tests
    def test_start_beyond_duration_clamped(self, extractor): ...
    def test_start_beyond_duration_raises(self, extractor): ...
    def test_end_beyond_duration_clamped(self, extractor): ...
    def test_end_beyond_duration_raises(self, extractor): ...
    
    # Minimum duration tests
    def test_min_duration_satisfied(self, extractor): ...
    def test_min_duration_violated_raises(self, extractor): ...
    
    # Multi-channel tests
    def test_stereo_to_mono(self, stereo_extractor): ...
    
    # Frame boundary tests
    def test_very_short_segment(self, extractor): ...
    def test_returns_float32(self, extractor): ...
    def test_returns_correct_sample_rate(self, extractor): ...


class TestExtractSegmentByFrames:
    """Tests for extract_segment_by_frames() method."""
    
    def test_extract_valid_frame_range(self, extractor): ...
    def test_reverse_frames_raises(self, extractor): ...
    def test_negative_start_clamped(self, extractor): ...
    def test_negative_start_raises(self, extractor): ...
    def test_end_beyond_total_clamped(self, extractor): ...
    def test_end_beyond_total_raises(self, extractor): ...
    def test_invalid_range_raises(self, extractor): ...


class TestAccessors:
    """Tests for accessor methods."""
    
    def test_get_duration(self, extractor): ...
    def test_get_info_structure(self, extractor): ...
    def test_get_info_values(self, extractor): ...
    def test_repr(self, extractor): ...


class TestLoadFullAudio:
    """Tests for load_full_audio() function."""
    
    def test_load_valid_wav(self, sample_wav): ...
    def test_load_file_not_found(self, tmp_path): ...
    def test_load_returns_float32(self, sample_wav): ...
    def test_load_stereo_to_mono(self, stereo_wav): ...


class TestValidateWavFile:
    """Tests for validate_wav_file() function."""
    
    def test_valid_wav_returns_true(self, sample_wav): ...
    def test_nonexistent_returns_false(self, tmp_path): ...
    def test_directory_returns_false(self, tmp_path): ...
    def test_invalid_audio_returns_false(self, tmp_path): ...
    def test_empty_file_returns_false(self, tmp_path): ...


# Fixtures

@pytest.fixture
def sample_wav(tmp_path):
    """Create a mono 16kHz WAV file."""
    wav_path = tmp_path / "sample.wav"
    audio = np.random.randn(16000).astype(np.float32)  # 1 second
    sf.write(str(wav_path), audio, 16000)
    return wav_path


@pytest.fixture
def stereo_wav(tmp_path):
    """Create a stereo WAV file."""
    wav_path = tmp_path / "stereo.wav"
    audio = np.random.randn(16000, 2).astype(np.float32)
    sf.write(str(wav_path), audio, 16000)
    return wav_path


@pytest.fixture
def extractor(sample_wav):
    """Create extractor for sample WAV."""
    return AudioSegmentExtractor(sample_wav)


@pytest.fixture
def stereo_extractor(stereo_wav):
    """Create extractor for stereo WAV."""
    return AudioSegmentExtractor(stereo_wav)

Testing Strategy

  1. Create test WAV files using soundfile in fixtures
  2. Test boundary conditions exhaustively (clamp vs raise)
  3. Verify audio data with numpy assertions
  4. Test multi-channel handling with stereo fixtures
  5. Test metadata extraction accuracy

Acceptance Criteria

  • tests/test_audio_utils.py exists with 35+ test functions
  • AudioSegmentExtractor fully tested
  • All error paths tested
  • Clamping vs raising behavior tested
  • Multi-channel handling tested
  • CI passes with new tests

Labels

  • test
  • enhancement
  • audio

Metadata

Metadata

Assignees

No one assigned

    Labels

    good first issueGood starter task for new contributors.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions