import asyncio class Diarizer: # The pyannote pipeline holds multi-GB torch models. A fresh instance is # created per recording (see api/pipeline.py), so cache the loaded pipeline # on the class and reuse it — reloading per recording leaks CPU memory # (torch reference cycles + glibc arena fragmentation) and eventually OOMs # the long-running service. Mirrors TranscriptionEngine._model. _shared_pipeline = None def __init__(self, hf_token: str): if not hf_token: raise ValueError("hf_token is required for diarization") self._hf_token = hf_token self._pipeline = None # per-instance override (used by tests) def _load_pipeline(self): if self._pipeline is not None: return self._pipeline if Diarizer._shared_pipeline is None: from pyannote.audio import Pipeline Diarizer._shared_pipeline = Pipeline.from_pretrained( "pyannote/speaker-diarization-3.1", token=self._hf_token, ) self._pipeline = Diarizer._shared_pipeline return self._pipeline async def diarize(self, wav_path: str) -> list[tuple[float, float, str]]: loop = asyncio.get_running_loop() pipeline = await loop.run_in_executor(None, self._load_pipeline) result = await loop.run_in_executor(None, lambda: pipeline(wav_path)) # pyannote 4.x returns DiarizeOutput; older versions return Annotation directly annotation = getattr(result, "speaker_diarization", result) return [ (turn.start, turn.end, speaker) for turn, _, speaker in annotation.itertracks(yield_label=True) ]