The meeting pipeline created a fresh Diarizer per recording, each loading the multi-GB pyannote speaker-diarization model anew (api/pipeline.py). Whisper and Ollama run remotely in this deployment, so pyannote was the only heavy in-process consumer. Reloading it per recording leaked CPU memory (torch reference cycles + glibc arena fragmentation) that was never returned to the OS, climbing to a 37 GB peak over a multi-day run until the kernel OOM-killed the service. Cache the loaded pipeline on the class and reuse it across Diarizer instances, mirroring TranscriptionEngine._model. RSS now stays flat.
40 lines
1.7 KiB
Python
40 lines
1.7 KiB
Python
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)
|
|
]
|