Files
2026-07-13 13:39:38 +08:00

192 lines
6.6 KiB
Python

# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Krisp SDK instance manager for LiveKit Agents.
This module provides a singleton manager for the Krisp VIVA SDK with reference
counting, ensuring proper initialization and cleanup when multiple components
(filters) use the SDK.
"""
from __future__ import annotations
import os
from threading import Lock
from typing import Any
from .log import logger
try:
import krisp_audio # type: ignore[import-not-found]
KRISP_AUDIO_AVAILABLE = True
# Mapping of sample rates (Hz) to Krisp SDK SamplingRate enums
KRISP_SAMPLE_RATES = {
8000: krisp_audio.SamplingRate.Sr8000Hz,
16000: krisp_audio.SamplingRate.Sr16000Hz,
24000: krisp_audio.SamplingRate.Sr24000Hz,
32000: krisp_audio.SamplingRate.Sr32000Hz,
44100: krisp_audio.SamplingRate.Sr44100Hz,
48000: krisp_audio.SamplingRate.Sr48000Hz,
}
KRISP_FRAME_DURATIONS = {
10: krisp_audio.FrameDuration.Fd10ms,
15: krisp_audio.FrameDuration.Fd15ms,
20: krisp_audio.FrameDuration.Fd20ms,
30: krisp_audio.FrameDuration.Fd30ms,
32: krisp_audio.FrameDuration.Fd32ms,
}
except ModuleNotFoundError:
KRISP_AUDIO_AVAILABLE = False
KRISP_SAMPLE_RATES = {}
KRISP_FRAME_DURATIONS = {}
logger.warning(
"krisp-audio package not found. "
"Install it to use Krisp SDK features: pip install krisp-audio"
)
def int_to_krisp_frame_duration(frame_duration_ms: int) -> Any:
if frame_duration_ms not in KRISP_FRAME_DURATIONS:
supported_durations = ", ".join(
str(duration) for duration in sorted(KRISP_FRAME_DURATIONS.keys())
)
raise ValueError(
f"Unsupported frame duration: {frame_duration_ms} ms. "
f"Supported durations: {supported_durations} ms"
)
return KRISP_FRAME_DURATIONS[frame_duration_ms]
def int_to_krisp_sample_rate(sample_rate: int) -> Any:
if sample_rate not in KRISP_SAMPLE_RATES:
supported_rates = ", ".join(str(rate) for rate in sorted(KRISP_SAMPLE_RATES.keys()))
raise ValueError(
f"Unsupported sample rate: {sample_rate} Hz. Supported rates: {supported_rates} Hz"
)
return KRISP_SAMPLE_RATES[sample_rate]
class KrispSDKManager:
"""Singleton manager for Krisp VIVA SDK with reference counting.
This manager ensures the Krisp SDK is initialized only once and properly
cleaned up when all components are done using it. It uses reference counting
to track active users (filters).
Thread-safe implementation using a lock for all operations.
The license key should be provided via the KRISP_VIVA_SDK_LICENSE_KEY environment variable.
"""
_initialized = False
_lock = Lock()
_reference_count = 0
@staticmethod
def _log_callback(log_message: str, log_level: Any) -> None:
"""Thread-safe callback for Krisp SDK logging."""
logger.debug(f"[Krisp {log_level}] {log_message}")
@staticmethod
def licensing_error_callback(error: Any, error_message: str) -> None:
logger.error(f"[Krisp Licensing Error: {error}] {error_message}")
@classmethod
def _get_license_key(cls) -> str:
"""Get the license key from the KRISP_VIVA_SDK_LICENSE_KEY environment variable."""
return os.getenv("KRISP_VIVA_SDK_LICENSE_KEY", "")
@classmethod
def acquire(cls) -> None:
"""Acquire a reference to the SDK (initializes if needed).
Call this when creating a filter instance.
The SDK will be initialized on the first call.
Raises:
Exception: If SDK initialization fails (propagated from krisp_audio)
"""
with cls._lock:
# Initialize SDK on first acquire
if cls._reference_count == 0:
try:
license_key = cls._get_license_key()
krisp_audio.globalInit(
"",
license_key,
cls.licensing_error_callback,
cls._log_callback,
krisp_audio.LogLevel.Off,
)
cls._initialized = True
version = krisp_audio.getVersion()
logger.debug(
f"Krisp Audio SDK initialized - "
f"Version: {version.major}.{version.minor}.{version.patch}"
)
except Exception as e:
cls._initialized = False
logger.error(f"Krisp SDK initialization failed: {e}")
raise
cls._reference_count += 1
logger.debug(f"Krisp SDK reference count: {cls._reference_count}")
@classmethod
def release(cls) -> None:
"""Release a reference to the SDK (destroys if last reference).
Call this when destroying a filter instance.
The SDK will be cleaned up when the last reference is released.
"""
with cls._lock:
if cls._reference_count > 0:
cls._reference_count -= 1
logger.debug(f"Krisp SDK reference count: {cls._reference_count}")
# Destroy SDK when last reference is released
if cls._reference_count == 0 and cls._initialized:
try:
krisp_audio.globalDestroy()
cls._initialized = False
logger.debug("Krisp Audio SDK destroyed (all references released)")
except Exception as e:
logger.error(f"Error during Krisp SDK cleanup: {e}")
cls._initialized = False
@classmethod
def is_initialized(cls) -> bool:
"""Check if the SDK is currently initialized.
Returns:
True if SDK is initialized, False otherwise.
"""
with cls._lock:
return cls._initialized
@classmethod
def get_reference_count(cls) -> int:
"""Get the current reference count.
Returns:
Number of active references to the SDK.
"""
with cls._lock:
return cls._reference_count