Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
jj-wohlgemuth Claude Opus 4.8 (1M context) commited on
Commit ·
8796731
1
Parent(s): 55cf093
Run dedicated VAD model on voice focus output
Browse filesAdd a separate quail-vad-2.0-xxs-16khz processor that runs on the
enhanced (voice focus) output and back the VAD context with it, instead
of the enhancement model's built-in VAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- constants.py +1 -0
- sdk.py +37 -12
constants.py
CHANGED
|
@@ -7,6 +7,7 @@ TIMEOUT_FACTOR_MB: Final = 60
|
|
| 7 |
BASE_TIMEOUT_SECONDS: Final = 120
|
| 8 |
|
| 9 |
MODEL_ID: Final = "quail-vf-2.1-l-16khz"
|
|
|
|
| 10 |
|
| 11 |
MINUTES_KEEP: Final = 60
|
| 12 |
|
|
|
|
| 7 |
BASE_TIMEOUT_SECONDS: Final = 120
|
| 8 |
|
| 9 |
MODEL_ID: Final = "quail-vf-2.1-l-16khz"
|
| 10 |
+
VAD_MODEL_ID: Final = "quail-vad-2.0-xxs-16khz"
|
| 11 |
|
| 12 |
MINUTES_KEEP: Final = 60
|
| 13 |
|
sdk.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
-
|
| 2 |
import numpy as np
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import aic_sdk as aic
|
| 5 |
import os
|
| 6 |
|
| 7 |
-
from constants import MODEL_ID
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
|
|
@@ -18,7 +17,6 @@ class SDKParams:
|
|
| 18 |
num_channels: int = 1,
|
| 19 |
sync: bool = True,
|
| 20 |
num_frames: int | None = None,
|
| 21 |
-
|
| 22 |
):
|
| 23 |
self.sample_rate = sample_rate
|
| 24 |
self.enhancement_level = enhancement_level
|
|
@@ -27,17 +25,27 @@ class SDKParams:
|
|
| 27 |
self.sync = sync
|
| 28 |
self.num_frames = num_frames # to be set after processor init
|
| 29 |
|
|
|
|
| 30 |
class SDKWrapper:
|
| 31 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
if os.getenv("AIC_SDK_KEY") is None:
|
| 33 |
raise RuntimeError("Missing AIC_SDK_KEY.")
|
| 34 |
self.sdk_key = os.getenv("AIC_SDK_KEY")
|
| 35 |
model_path = aic.Model.download(model_id, models_dir)
|
| 36 |
self.model = aic.Model.from_file(model_path)
|
|
|
|
|
|
|
| 37 |
|
| 38 |
def init_processor(self, sdk_params: SDKParams):
|
| 39 |
optimal_frames = self.model.get_optimal_num_frames(sdk_params.sample_rate)
|
| 40 |
-
self.num_frames =
|
|
|
|
|
|
|
| 41 |
self.sample_rate = sdk_params.sample_rate
|
| 42 |
aic_config = aic.ProcessorConfig(
|
| 43 |
sample_rate=sdk_params.sample_rate,
|
|
@@ -53,7 +61,15 @@ class SDKWrapper:
|
|
| 53 |
aic.ProcessorParameter.EnhancementLevel, float(sdk_params.enhancement_level)
|
| 54 |
)
|
| 55 |
self.enhancement_level = sdk_params.enhancement_level
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def change_enhancement_level(self, enhancement_level: float):
|
| 59 |
if not hasattr(self, "processor"):
|
|
@@ -62,20 +78,27 @@ class SDKWrapper:
|
|
| 62 |
aic.ProcessorParameter.EnhancementLevel, float(enhancement_level)
|
| 63 |
)
|
| 64 |
self.enhancement_level = enhancement_level
|
| 65 |
-
|
| 66 |
def _check_shape(self, audio: np.ndarray) -> np.ndarray:
|
| 67 |
if len(audio.shape) == 1:
|
| 68 |
audio = audio.reshape(1, -1)
|
| 69 |
if audio.shape[0] > 2 or len(audio.shape) != 2:
|
| 70 |
raise ValueError("Expected audio with shape (n, frames)")
|
| 71 |
return audio
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
def process_with_vad(
|
| 74 |
self,
|
| 75 |
audio: np.ndarray,
|
| 76 |
) -> tuple[np.ndarray, bool]:
|
| 77 |
"""
|
| 78 |
-
|
| 79 |
"""
|
| 80 |
audio = self._check_shape(audio)
|
| 81 |
out = np.zeros_like(audio)
|
|
@@ -91,17 +114,19 @@ class SDKWrapper:
|
|
| 91 |
padded[:, :last] = chunk
|
| 92 |
enhanced = self.processor.process(padded)
|
| 93 |
out[:, i : i + last] = enhanced[:, :last]
|
|
|
|
|
|
|
| 94 |
break
|
| 95 |
enhanced = self.processor.process(chunk)
|
| 96 |
out[:, i : i + chunk_size] = enhanced[:, :chunk_size]
|
| 97 |
-
if self.
|
| 98 |
vad_per_sample[:, i : i + chunk_size] = True
|
| 99 |
if vad_per_sample.mean() > 0.5:
|
| 100 |
vad_overall = True
|
| 101 |
return out, vad_overall
|
| 102 |
-
|
| 103 |
def process_chunk(self, audio: np.ndarray) -> np.ndarray:
|
| 104 |
audio = self._check_shape(audio)
|
| 105 |
result = self.processor.process(audio)
|
|
|
|
| 106 |
return result
|
| 107 |
-
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
import aic_sdk as aic
|
| 4 |
import os
|
| 5 |
|
| 6 |
+
from constants import MODEL_ID, VAD_MODEL_ID
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
|
|
|
| 17 |
num_channels: int = 1,
|
| 18 |
sync: bool = True,
|
| 19 |
num_frames: int | None = None,
|
|
|
|
| 20 |
):
|
| 21 |
self.sample_rate = sample_rate
|
| 22 |
self.enhancement_level = enhancement_level
|
|
|
|
| 25 |
self.sync = sync
|
| 26 |
self.num_frames = num_frames # to be set after processor init
|
| 27 |
|
| 28 |
+
|
| 29 |
class SDKWrapper:
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
model_id: str = MODEL_ID,
|
| 33 |
+
vad_model_id: str = VAD_MODEL_ID,
|
| 34 |
+
models_dir: str = "./models",
|
| 35 |
+
):
|
| 36 |
if os.getenv("AIC_SDK_KEY") is None:
|
| 37 |
raise RuntimeError("Missing AIC_SDK_KEY.")
|
| 38 |
self.sdk_key = os.getenv("AIC_SDK_KEY")
|
| 39 |
model_path = aic.Model.download(model_id, models_dir)
|
| 40 |
self.model = aic.Model.from_file(model_path)
|
| 41 |
+
vad_model_path = aic.Model.download(vad_model_id, models_dir)
|
| 42 |
+
self.vad_model = aic.Model.from_file(vad_model_path)
|
| 43 |
|
| 44 |
def init_processor(self, sdk_params: SDKParams):
|
| 45 |
optimal_frames = self.model.get_optimal_num_frames(sdk_params.sample_rate)
|
| 46 |
+
self.num_frames = (
|
| 47 |
+
sdk_params.num_frames if sdk_params.num_frames else optimal_frames
|
| 48 |
+
)
|
| 49 |
self.sample_rate = sdk_params.sample_rate
|
| 50 |
aic_config = aic.ProcessorConfig(
|
| 51 |
sample_rate=sdk_params.sample_rate,
|
|
|
|
| 61 |
aic.ProcessorParameter.EnhancementLevel, float(sdk_params.enhancement_level)
|
| 62 |
)
|
| 63 |
self.enhancement_level = sdk_params.enhancement_level
|
| 64 |
+
|
| 65 |
+
vad_config = aic.ProcessorConfig(
|
| 66 |
+
sample_rate=sdk_params.sample_rate,
|
| 67 |
+
num_channels=sdk_params.num_channels,
|
| 68 |
+
num_frames=self.vad_model.get_optimal_num_frames(sdk_params.sample_rate),
|
| 69 |
+
allow_variable_frames=True,
|
| 70 |
+
)
|
| 71 |
+
self.vad_processor = aic.Processor(self.vad_model, self.sdk_key, vad_config)
|
| 72 |
+
self.vad_context = self.vad_processor.get_vad_context()
|
| 73 |
|
| 74 |
def change_enhancement_level(self, enhancement_level: float):
|
| 75 |
if not hasattr(self, "processor"):
|
|
|
|
| 78 |
aic.ProcessorParameter.EnhancementLevel, float(enhancement_level)
|
| 79 |
)
|
| 80 |
self.enhancement_level = enhancement_level
|
| 81 |
+
|
| 82 |
def _check_shape(self, audio: np.ndarray) -> np.ndarray:
|
| 83 |
if len(audio.shape) == 1:
|
| 84 |
audio = audio.reshape(1, -1)
|
| 85 |
if audio.shape[0] > 2 or len(audio.shape) != 2:
|
| 86 |
raise ValueError("Expected audio with shape (n, frames)")
|
| 87 |
return audio
|
| 88 |
+
|
| 89 |
+
def _detect_speech(self, enhanced: np.ndarray) -> bool:
|
| 90 |
+
"""Run the dedicated VAD model on enhanced output and
|
| 91 |
+
return its speech prediction. The enhanced audio is discarded; only the
|
| 92 |
+
VAD context is updated."""
|
| 93 |
+
self.vad_processor.process(enhanced)
|
| 94 |
+
return self.vad_context.is_speech_detected()
|
| 95 |
+
|
| 96 |
def process_with_vad(
|
| 97 |
self,
|
| 98 |
audio: np.ndarray,
|
| 99 |
) -> tuple[np.ndarray, bool]:
|
| 100 |
"""
|
| 101 |
+
audio_array: 2D NumPy array with shape (num_channels, samples) containing audio data to be enhanced
|
| 102 |
"""
|
| 103 |
audio = self._check_shape(audio)
|
| 104 |
out = np.zeros_like(audio)
|
|
|
|
| 114 |
padded[:, :last] = chunk
|
| 115 |
enhanced = self.processor.process(padded)
|
| 116 |
out[:, i : i + last] = enhanced[:, :last]
|
| 117 |
+
if self._detect_speech(enhanced[:, :last]):
|
| 118 |
+
vad_per_sample[:, i : i + last] = True
|
| 119 |
break
|
| 120 |
enhanced = self.processor.process(chunk)
|
| 121 |
out[:, i : i + chunk_size] = enhanced[:, :chunk_size]
|
| 122 |
+
if self._detect_speech(enhanced[:, :chunk_size]):
|
| 123 |
vad_per_sample[:, i : i + chunk_size] = True
|
| 124 |
if vad_per_sample.mean() > 0.5:
|
| 125 |
vad_overall = True
|
| 126 |
return out, vad_overall
|
| 127 |
+
|
| 128 |
def process_chunk(self, audio: np.ndarray) -> np.ndarray:
|
| 129 |
audio = self._check_shape(audio)
|
| 130 |
result = self.processor.process(audio)
|
| 131 |
+
self._detect_speech(result)
|
| 132 |
return result
|
|
|