| |
| """ |
| TajweedSST - Alignment Engine Unit Tests |
| |
| Tests word and phoneme timing accuracy: |
| - WhisperX word alignment |
| - MFA phoneme alignment |
| - Phoneme normalization within word boundaries |
| - Mock alignment for testing without models |
| """ |
|
|
| import pytest |
| import os |
| import sys |
|
|
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) |
|
|
| from alignment_engine import ( |
| AlignmentEngine, |
| MockAlignmentEngine, |
| PhonemeAlignment, |
| WordAlignment, |
| AlignmentResult |
| ) |
|
|
|
|
| class TestDataclasses: |
| """Test alignment data structures""" |
| |
| def test_phoneme_alignment(self): |
| """PhonemeAlignment stores timing correctly""" |
| pa = PhonemeAlignment(phoneme="ب", start=0.0, end=0.1, duration=0.1) |
| assert pa.phoneme == "ب" |
| assert pa.duration == 0.1 |
| |
| def test_phoneme_normalized_duration(self): |
| """Normalized duration calculation""" |
| pa = PhonemeAlignment(phoneme="ا", start=0.0, end=0.2, duration=0.2) |
| |
| assert pa.normalized_duration == 0.2 |
| |
| def test_word_alignment(self): |
| """WordAlignment stores word and phonemes""" |
| wa = WordAlignment( |
| word_text="بسم", |
| whisper_start=0.0, |
| whisper_end=0.5, |
| phonemes=[ |
| PhonemeAlignment("ب", 0.0, 0.15, 0.15), |
| PhonemeAlignment("س", 0.15, 0.35, 0.20), |
| PhonemeAlignment("م", 0.35, 0.5, 0.15), |
| ] |
| ) |
| assert wa.word_text == "بسم" |
| assert len(wa.phonemes) == 3 |
| assert wa.whisper_duration == 0.5 |
| |
| def test_alignment_result(self): |
| """AlignmentResult stores full alignment""" |
| ar = AlignmentResult( |
| audio_path="/path/to/audio.wav", |
| surah=91, |
| ayah=1, |
| words=[] |
| ) |
| assert ar.surah == 91 |
| assert ar.ayah == 1 |
|
|
|
|
| class TestMockAlignmentEngine: |
| """Test mock alignment for development without models""" |
| |
| @pytest.fixture |
| def mock_engine(self): |
| return MockAlignmentEngine() |
| |
| def test_mock_align_returns_result(self, mock_engine): |
| """Mock alignment returns AlignmentResult""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["b i s m", "a l l a h"], |
| surah=1, |
| ayah=1 |
| ) |
| assert isinstance(result, AlignmentResult) |
| |
| def test_mock_align_word_count(self, mock_engine): |
| """Mock alignment produces correct word count""" |
| phonetic_words = ["b i s m", "a l l a h", "a r r a h m a n"] |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=phonetic_words, |
| surah=1, |
| ayah=1 |
| ) |
| assert len(result.words) == len(phonetic_words) |
| |
| def test_mock_align_phoneme_generation(self, mock_engine): |
| """Mock alignment generates phonemes for each word""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["b i s m"], |
| surah=1, |
| ayah=1 |
| ) |
| |
| assert len(result.words[0].phonemes) >= 3 |
| |
| def test_mock_align_timing_monotonic(self, mock_engine): |
| """Mock timing should be monotonically increasing""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["word1", "word2", "word3"], |
| surah=1, |
| ayah=1 |
| ) |
| |
| prev_end = 0.0 |
| for word in result.words: |
| assert word.whisper_start >= prev_end, "Word start before previous end" |
| prev_end = word.whisper_end |
|
|
|
|
| class TestTimingMonotonicity: |
| """Test that timing never goes backwards""" |
| |
| @pytest.fixture |
| def mock_engine(self): |
| return MockAlignmentEngine() |
| |
| def test_word_timing_monotonic(self, mock_engine): |
| """Word-level timing is strictly increasing""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["w1", "w2", "w3", "w4", "w5"], |
| surah=1, |
| ayah=1 |
| ) |
| |
| for i in range(1, len(result.words)): |
| prev = result.words[i-1] |
| curr = result.words[i] |
| assert curr.whisper_start >= prev.whisper_end, \ |
| f"Word {i} starts ({curr.whisper_start}) before word {i-1} ends ({prev.whisper_end})" |
| |
| def test_phoneme_timing_monotonic(self, mock_engine): |
| """Phoneme-level timing is strictly increasing within words""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["a l r a h m a n"], |
| surah=1, |
| ayah=1 |
| ) |
| |
| for word in result.words: |
| for i in range(1, len(word.phonemes)): |
| prev = word.phonemes[i-1] |
| curr = word.phonemes[i] |
| assert curr.start >= prev.end, \ |
| f"Phoneme {curr.phoneme} starts before {prev.phoneme} ends" |
|
|
|
|
| class TestPhonemeNormalization: |
| """Test phoneme duration normalization""" |
| |
| def test_phonemes_fit_word_boundary(self): |
| """Normalized phonemes should fit exactly in word boundaries""" |
| word = WordAlignment( |
| word_text="test", |
| whisper_start=1.0, |
| whisper_end=2.0, |
| phonemes=[ |
| PhonemeAlignment("t", 1.0, 1.25, 0.25), |
| PhonemeAlignment("e", 1.25, 1.5, 0.25), |
| PhonemeAlignment("s", 1.5, 1.75, 0.25), |
| PhonemeAlignment("t", 1.75, 2.0, 0.25), |
| ] |
| ) |
| |
| |
| assert word.phonemes[0].start == word.whisper_start |
| |
| assert word.phonemes[-1].end == word.whisper_end |
| |
| def test_phonemes_cover_word_duration(self): |
| """Phoneme durations should sum to word duration""" |
| word = WordAlignment( |
| word_text="test", |
| whisper_start=0.0, |
| whisper_end=1.0, |
| phonemes=[ |
| PhonemeAlignment("a", 0.0, 0.333, 0.333), |
| PhonemeAlignment("b", 0.333, 0.666, 0.333), |
| PhonemeAlignment("c", 0.666, 1.0, 0.334), |
| ] |
| ) |
| |
| total_phoneme_duration = sum(p.duration for p in word.phonemes) |
| word_duration = word.whisper_duration |
| |
| assert abs(total_phoneme_duration - word_duration) < 0.01 |
|
|
|
|
| class TestArabicPhonemes: |
| """Test Arabic-specific phoneme handling""" |
| |
| @pytest.fixture |
| def mock_engine(self): |
| return MockAlignmentEngine() |
| |
| def test_arabic_phonetic_transcription(self, mock_engine): |
| """Engine handles Arabic phonetic transcription""" |
| result = mock_engine.align( |
| audio_path="/fake/path.wav", |
| phonetic_words=["b i s m i", "a l l aa h i"], |
| surah=1, |
| ayah=1 |
| ) |
| assert len(result.words) == 2 |
|
|
|
|
| if __name__ == "__main__": |
| pytest.main([__file__, "-v"]) |
|
|