diff --git a/.github/workflows/gigaam.yml b/.github/workflows/gigaam.yml index 64781c3..5bd2a55 100644 --- a/.github/workflows/gigaam.yml +++ b/.github/workflows/gigaam.yml @@ -9,6 +9,8 @@ on: env: FORCE_COLOR: 1 + # Repository secrets are unavailable to fork pull requests. + RUN_HF_TOKEN_TESTS: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} jobs: test: @@ -72,8 +74,13 @@ jobs: - name: Run batching tests run: | pytest -v tests/test_batching.py --tb=short + + - name: Run VAD utility tests + run: | + pytest -v tests/test_vad_utils.py --tb=short - name: Run longform tests + if: env.RUN_HF_TOKEN_TESTS == 'true' env: HF_TOKEN: ${{ secrets.HF_TOKEN }} run: | @@ -85,9 +92,19 @@ jobs: - name: Run timestamps tests run: | - pytest -v tests/test_timestamps.py --tb=short + pytest -v tests/test_timestamps.py -k "not transcribe_longform" --tb=short + + - name: Run longform timestamps tests + if: env.RUN_HF_TOKEN_TESTS == 'true' + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + pytest -v tests/test_timestamps.py -k "transcribe_longform" --tb=short - name: Run training tests + if: env.RUN_HF_TOKEN_TESTS == 'true' + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} run: | pytest -v tests/test_training.py --tb=short diff --git a/gigaam/vad_utils.py b/gigaam/vad_utils.py index c3d5e66..ab09661 100644 --- a/gigaam/vad_utils.py +++ b/gigaam/vad_utils.py @@ -78,6 +78,19 @@ def get_pipeline( return _PIPELINE.to(device) +def _apply_pipeline(pipeline: Pipeline, audio_file): + """ + Applies PyAnnote pipeline to a preloaded waveform dict. + + GigaAM already decodes the audio for ASR chunk slicing. Reusing the same + waveform for VAD avoids depending on pyannote/torchcodec path decoding and + keeps VAD boundaries aligned with the samples later sliced for ASR. + """ + if hasattr(pipeline, "prepare_one") and hasattr(pipeline, "apply"): + return pipeline.apply(pipeline.prepare_one(audio_file, preload=False)) + return pipeline(audio_file) + + def segment_audio_file( wav_file: str, sr: int, @@ -92,9 +105,10 @@ def segment_audio_file( The segmentation is performed using a PyAnnote voice activity detection pipeline. """ - audio = load_audio(wav_file) + audio = load_audio(wav_file, sample_rate=sr) pipeline = get_pipeline(device) - sad_segments = cast(Annotation, pipeline(wav_file)) + sad_input = {"waveform": audio.unsqueeze(0), "sample_rate": sr} + sad_segments = cast(Annotation, _apply_pipeline(pipeline, sad_input)) segments: List[torch.Tensor] = [] curr_duration = 0.0 diff --git a/tests/test_vad_utils.py b/tests/test_vad_utils.py new file mode 100644 index 0000000..6bf99d7 --- /dev/null +++ b/tests/test_vad_utils.py @@ -0,0 +1,62 @@ +import torch + + +class _FakeSegment: + def __init__(self, start, end): + self.start = start + self.end = end + + +class _FakeTimeline: + def support(self): + return [_FakeSegment(1.0, 3.0), _FakeSegment(4.0, 6.0)] + + +class _FakeSadSegments: + def get_timeline(self): + return _FakeTimeline() + + +class _FakePyannote4Pipeline: + def __init__(self): + self.used_apply = False + + def prepare_one(self, audio_file, preload=False): + assert set(audio_file) == {"waveform", "sample_rate"} + assert audio_file["waveform"].shape == (1, 80) + assert audio_file["sample_rate"] == 10 + assert preload is False + return audio_file + + def apply(self, prepared): + assert prepared["sample_rate"] == 10 + self.used_apply = True + return _FakeSadSegments() + + def __call__(self, audio_file): + raise AssertionError("segment_audio_file should use preloaded waveform input") + + +def test_segmentation_uses_preloaded_waveform_for_pyannote_pipeline(monkeypatch): + """VAD should use the same decoded waveform later sliced for ASR chunks.""" + from gigaam import vad_utils + + pipeline = _FakePyannote4Pipeline() + monkeypatch.setattr(vad_utils, "get_pipeline", lambda device: pipeline) + monkeypatch.setattr( + vad_utils, + "load_audio", + lambda wav_file, sample_rate: torch.arange(80, dtype=torch.float32), + ) + + segments, boundaries = vad_utils.segment_audio_file( + "fake.wav", + sr=10, + max_duration=10.0, + min_duration=8.0, + strict_limit_duration=30.0, + ) + + assert pipeline.used_apply + assert boundaries == [(1.0, 6.0)] + assert torch.equal(segments[0], torch.arange(10, 60, dtype=torch.float32))