yum-archive/yapBox
A black box for your yapping
git clone https://git.yummers.dev/yum-archive/yapBox
ca55539
master
1# MIT License 2# 3# Copyright (c) 2023 Guillaume Klein 4# 5# Permission is hereby granted, free of charge, to any person obtaining a copy 6# of this software and associated documentation files (the "Software"), to deal 7# in the Software without restriction, including without limitation the rights 8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9# copies of the Software, and to permit persons to whom the Software is 10# furnished to do so, subject to the following conditions: 11# 12# The above copyright notice and this permission notice shall be included in all 13# copies or substantial portions of the Software. 14# 15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21# SOFTWARE. 22 23import bisect 24import functools 25import os 26import warnings 27 28from typing import List ,NamedTuple ,Optional 29 30import numpy as np 31 32 33# The code below is adapted from https://github.com/snakers4/silero-vad. 34class VadOptions (NamedTuple ): 35"""VAD options. 36 37Attributes: 38threshold: Speech threshold. Silero VAD outputs speech probabilities for each audio chunk, 39probabilities ABOVE this value are considered as SPEECH. It is better to tune this 40parameter for each dataset separately, but "lazy" 0.5 is pretty good for most datasets. 41min_speech_duration_ms: Final speech chunks shorter min_speech_duration_ms are thrown out. 42max_speech_duration_s: Maximum duration of speech chunks in seconds. Chunks longer 43than max_speech_duration_s will be split at the timestamp of the last silence that 44lasts more than 100ms (if any), to prevent aggressive cutting. Otherwise, they will be 45split aggressively just before max_speech_duration_s. 46min_silence_duration_ms: In the end of each speech chunk wait for min_silence_duration_ms 47before separating it 48window_size_samples: Audio chunks of window_size_samples size are fed to the silero VAD model. 49WARNING! Silero VAD models were trained using 512, 1024, 1536 samples for 16000 sample rate. 50Values other than these may affect model performance!! 51speech_pad_ms: Final speech chunks are padded by speech_pad_ms each side 52""" 53 54threshold :float = 0.5 55min_speech_duration_ms :int = 250 56max_speech_duration_s :float = float ("inf" ) 57min_silence_duration_ms :int = 2000 58window_size_samples :int = 1024 59speech_pad_ms :int = 400 60 61 62def get_speech_timestamps ( 63audio :np .ndarray , 64vad_options :Optional [VadOptions ]= None , 65** kwargs , 66)-> List [dict ]: 67"""This method is used for splitting long audios into speech chunks using silero VAD. 68 69Args: 70audio: One dimensional float array. 71vad_options: Options for VAD processing. 72kwargs: VAD options passed as keyword arguments for backward compatibility. 73 74Returns: 75List of dicts containing begin and end samples of each speech chunk. 76""" 77if vad_options is None : 78vad_options = VadOptions (** kwargs ) 79 80threshold = vad_options .threshold 81min_speech_duration_ms = vad_options .min_speech_duration_ms 82max_speech_duration_s = vad_options .max_speech_duration_s 83min_silence_duration_ms = vad_options .min_silence_duration_ms 84window_size_samples = vad_options .window_size_samples 85speech_pad_ms = vad_options .speech_pad_ms 86 87if window_size_samples not in [512 ,1024 ,1536 ]: 88warnings .warn ( 89"Unusual window_size_samples! Supported window_size_samples:\n" 90" - [512, 1024, 1536] for 16000 sampling_rate" 91 ) 92 93sampling_rate = 16000 94min_speech_samples = sampling_rate * min_speech_duration_ms / 1000 95speech_pad_samples = sampling_rate * speech_pad_ms / 1000 96max_speech_samples = ( 97sampling_rate * max_speech_duration_s 98- window_size_samples 99- 2 * speech_pad_samples 100 ) 101min_silence_samples = sampling_rate * min_silence_duration_ms / 1000 102min_silence_samples_at_max_speech = sampling_rate * 98 / 1000 103 104audio_length_samples = len (audio ) 105 106model = get_vad_model () 107state = model .get_initial_state (batch_size = 1 ) 108 109speech_probs = [] 110for current_start_sample in range (0 ,audio_length_samples ,window_size_samples ): 111chunk = audio [current_start_sample :current_start_sample + window_size_samples ] 112if len (chunk )< window_size_samples : 113chunk = np .pad (chunk , (0 ,int (window_size_samples - len (chunk )))) 114speech_prob ,state = model (chunk ,state ,sampling_rate ) 115speech_probs .append (speech_prob ) 116 117triggered = False 118speeches = [] 119current_speech = {} 120neg_threshold = threshold - 0.15 121 122# to save potential segment end (and tolerate some silence) 123temp_end = 0 124# to save potential segment limits in case of maximum segment size reached 125prev_end = next_start = 0 126 127for i ,speech_prob in enumerate (speech_probs ): 128if (speech_prob >= threshold )and temp_end : 129temp_end = 0 130if next_start < prev_end : 131next_start = window_size_samples * i 132 133if (speech_prob >= threshold )and not triggered : 134triggered = True 135current_speech ["start" ]= window_size_samples * i 136continue 137 138if ( 139triggered 140and (window_size_samples * i )- current_speech ["start" ]> max_speech_samples 141 ): 142if prev_end : 143current_speech ["end" ]= prev_end 144speeches .append (current_speech ) 145current_speech = {} 146# previously reached silence (< neg_thres) and is still not speech (< thres) 147if next_start < prev_end : 148triggered = False 149else : 150current_speech ["start" ]= next_start 151prev_end = next_start = temp_end = 0 152else : 153current_speech ["end" ]= window_size_samples * i 154speeches .append (current_speech ) 155current_speech = {} 156prev_end = next_start = temp_end = 0 157triggered = False 158continue 159 160if (speech_prob < neg_threshold )and triggered : 161if not temp_end : 162temp_end = window_size_samples * i 163# condition to avoid cutting in very short silence 164if (window_size_samples * i )- temp_end > min_silence_samples_at_max_speech : 165prev_end = temp_end 166if (window_size_samples * i )- temp_end < min_silence_samples : 167continue 168else : 169current_speech ["end" ]= temp_end 170if ( 171current_speech ["end" ]- current_speech ["start" ] 172 )> min_speech_samples : 173speeches .append (current_speech ) 174current_speech = {} 175prev_end = next_start = temp_end = 0 176triggered = False 177continue 178 179if ( 180current_speech 181and (audio_length_samples - current_speech ["start" ])> min_speech_samples 182 ): 183current_speech ["end" ]= audio_length_samples 184speeches .append (current_speech ) 185 186for i ,speech in enumerate (speeches ): 187if i == 0 : 188speech ["start" ]= int (max (0 ,speech ["start" ]- speech_pad_samples )) 189if i != len (speeches )- 1 : 190silence_duration = speeches [i + 1 ]["start" ]- speech ["end" ] 191if silence_duration < 2 * speech_pad_samples : 192speech ["end" ]+= int (silence_duration // 2 ) 193speeches [i + 1 ]["start" ]= int ( 194max (0 ,speeches [i + 1 ]["start" ]- silence_duration // 2 ) 195 ) 196else : 197speech ["end" ]= int ( 198min (audio_length_samples ,speech ["end" ]+ speech_pad_samples ) 199 ) 200speeches [i + 1 ]["start" ]= int ( 201max (0 ,speeches [i + 1 ]["start" ]- speech_pad_samples ) 202 ) 203else : 204speech ["end" ]= int ( 205min (audio_length_samples ,speech ["end" ]+ speech_pad_samples ) 206 ) 207 208return speeches 209 210 211def collect_chunks (audio :np .ndarray ,chunks :List [dict ])-> np .ndarray : 212"""Collects and concatenates audio chunks.""" 213if not chunks : 214return np .array ([],dtype = np .float32 ) 215 216return np .concatenate ([audio [chunk ["start" ] :chunk ["end" ]]for chunk in chunks ]) 217 218 219class SpeechTimestampsMap : 220"""Helper class to restore original speech timestamps.""" 221 222def __init__ (self ,chunks :List [dict ],sampling_rate :int ,time_precision :int = 2 ): 223self .sampling_rate = sampling_rate 224self .time_precision = time_precision 225self .chunk_end_sample = [] 226self .total_silence_before = [] 227 228previous_end = 0 229silent_samples = 0 230 231for chunk in chunks : 232silent_samples += chunk ["start" ]- previous_end 233previous_end = chunk ["end" ] 234 235self .chunk_end_sample .append (chunk ["end" ]- silent_samples ) 236self .total_silence_before .append (silent_samples / sampling_rate ) 237 238def get_original_time ( 239self , 240time :float , 241chunk_index :Optional [int ]= None , 242 )-> float : 243if chunk_index is None : 244chunk_index = self .get_chunk_index (time ) 245 246total_silence_before = self .total_silence_before [chunk_index ] 247return round (total_silence_before + time ,self .time_precision ) 248 249def get_chunk_index (self ,time :float )-> int : 250sample = int (time * self .sampling_rate ) 251return min ( 252bisect .bisect (self .chunk_end_sample ,sample ), 253len (self .chunk_end_sample )- 1 , 254 ) 255 256 257@ functools . lru_cache 258def get_vad_model (): 259"""Returns the VAD model instance.""" 260abspath = os .path .abspath (__file__ ) 261my_dir = os .path .dirname (abspath ) 262 263path = os .path .join (my_dir ,"Models/silero_vad.onnx" ) 264return SileroVADModel (path ) 265 266 267class SileroVADModel : 268def __init__ (self ,path ): 269try : 270import onnxruntime 271except ImportError as e : 272raise RuntimeError ( 273"Applying the VAD filter requires the onnxruntime package" 274 )from e 275 276opts = onnxruntime .SessionOptions () 277opts .inter_op_num_threads = 1 278opts .intra_op_num_threads = 1 279opts .log_severity_level = 4 280 281self .session = onnxruntime .InferenceSession ( 282path , 283providers = ["CPUExecutionProvider" ], 284sess_options = opts , 285 ) 286 287def get_initial_state (self ,batch_size :int ): 288h = np .zeros ((2 ,batch_size ,64 ),dtype = np .float32 ) 289c = np .zeros ((2 ,batch_size ,64 ),dtype = np .float32 ) 290return h ,c 291 292def __call__ (self ,x ,state ,sr :int ): 293if len (x .shape )== 1 : 294x = np .expand_dims (x ,0 ) 295if len (x .shape )> 2 : 296raise ValueError ( 297f"Too many dimensions for input audio chunk { len ( x . shape ) } " 298 ) 299if sr / x .shape [1 ]> 31.25 : 300raise ValueError ("Input audio chunk is too short" ) 301 302h ,c = state 303 304ort_inputs = { 305"input" :x , 306"h" :h , 307"c" :c , 308"sr" :np .array (sr ,dtype = "int64" ), 309 } 310 311out ,h ,c = self .session .run (None ,ort_inputs ) 312state = (h ,c ) 313 314return out ,state