| | |
| | """ |
| | 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"]) |
| |
|