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 files

Add 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>

Files changed (2) hide show
  1. constants.py +1 -0
  2. 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__(self, model_id: str = MODEL_ID, models_dir: str = "./models"):
 
 
 
 
 
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 = sdk_params.num_frames if sdk_params.num_frames else optimal_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
- self.vad_context = self.processor.get_vad_context()
 
 
 
 
 
 
 
 
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
- audio_array: 2D NumPy array with shape (num_channels, samples) containing audio data to be enhanced
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.vad_context.is_speech_detected():
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