Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions src/pipecat/audio/vad/silero.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Supports 8kHz and 16kHz sample rates.
"""

import threading
import time
from typing import cast

Expand All @@ -31,6 +32,31 @@
raise ImportError(f"Missing module(s): {e}") from e


# ONNX sessions are read-only and thread-safe, so every analyzer can share one
# per model file. Only the per-instance state below needs to stay separate.
_SESSIONS: dict = {}
_SESSIONS_LOCK = threading.Lock()


def _shared_session(path, force_onnx_cpu: bool):
"""Return a process-wide InferenceSession for this model file."""
key = (str(path), force_onnx_cpu)
with _SESSIONS_LOCK:
session = _SESSIONS.get(key)
if session is None:
opts = onnxruntime.SessionOptions()
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
if force_onnx_cpu and "CPUExecutionProvider" in onnxruntime.get_available_providers():
session = onnxruntime.InferenceSession(
path, providers=["CPUExecutionProvider"], sess_options=opts
)
else:
session = onnxruntime.InferenceSession(path, sess_options=opts)
_SESSIONS[key] = session
return session


class SileroOnnxModel:
"""ONNX runtime wrapper for the Silero VAD model.

Expand All @@ -46,16 +72,7 @@ def __init__(self, path, force_onnx_cpu=True):
path: Path to the ONNX model file.
force_onnx_cpu: Whether to force CPU execution provider.
"""
opts = onnxruntime.SessionOptions()
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1

if force_onnx_cpu and "CPUExecutionProvider" in onnxruntime.get_available_providers():
self.session = onnxruntime.InferenceSession(
path, providers=["CPUExecutionProvider"], sess_options=opts
)
else:
self.session = onnxruntime.InferenceSession(path, sess_options=opts)
self.session = _shared_session(path, force_onnx_cpu)

self.reset_states()
self.sample_rates = [8000, 16000]
Expand Down
46 changes: 46 additions & 0 deletions tests/test_silero_vad_session_sharing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# Copyright (c) 2024–2025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

"""Tests that Silero VAD analyzers share one ONNX session without sharing state."""

import unittest

import numpy as np

from pipecat.audio.vad.silero import SileroVADAnalyzer


def _analyzer() -> SileroVADAnalyzer:
analyzer = SileroVADAnalyzer()
analyzer.set_sample_rate(16000)
return analyzer


def _frame() -> bytes:
rng = np.random.default_rng(0)
return (rng.normal(0, 0.3, 512) * 32767).astype("int16").tobytes()


class TestSileroVADSessionSharing(unittest.TestCase):
def test_analyzers_share_one_session(self):
self.assertIs(_analyzer()._model.session, _analyzer()._model.session)

def test_state_stays_per_analyzer(self):
frame = _frame()
first, second = _analyzer(), _analyzer()

baseline = np.ravel(first.voice_confidence(frame))[0]
self.assertEqual(baseline, np.ravel(second.voice_confidence(frame))[0])

# Advancing one analyzer must not move the other
for _ in range(5):
second.voice_confidence(frame)
self.assertEqual(baseline, np.ravel(first.voice_confidence(frame))[0])
self.assertNotEqual(baseline, np.ravel(second.voice_confidence(frame))[0])


if __name__ == "__main__":
unittest.main()