8 Commits

Author SHA1 Message Date
makaveli 4baccf75a7 Bump version v0.6.1 2025-01-16 10:43:26 +05:30
makaveli b7acb8c872 Merge pull request #320 from makaveli10/fix_deprecated_package_name
Fix package name
2025-01-16 10:42:44 +05:30
makaveli10 fe7b55efe4 Fix package name
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-16 05:07:13 +00:00
makaveli c1b249ad0d Merge pull request #319 from makaveli10/upgrade_silero_vad_v5
Upgrade silero vad v5
2025-01-13 18:20:13 +05:30
makaveli10 5e4589cfe1 Upgrade silero vad v5.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-13 11:37:58 +00:00
makaveli10 b6b73730fb Fix: typo
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-13 11:35:22 +00:00
makaveli 953a88c7da Merge pull request #318 from makaveli10/fix_skipped_audio_chunk
Fix skipped audio chunk
2025-01-13 11:58:17 +05:30
makaveli10 182b5cbd6d Fix skipped audio chunk by recording the time of the first repition of a segment
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-08 13:58:34 +00:00
4 changed files with 44 additions and 21 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ README = (HERE / "README.md").read_text()
# This call to setup() does all the work # This call to setup() does all the work
setup( setup(
name="whisper-live", name="whisper_live",
version=__version__, version=__version__,
description="A nearly-live implementation of OpenAI's Whisper.", description="A nearly-live implementation of OpenAI's Whisper.",
long_description=README, long_description=README,
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.6.0" __version__ = "0.6.1"
+12 -4
View File
@@ -718,7 +718,7 @@ class ServeClientTensorRT(ServeClientBase):
elif self.transcript[-1]["text"].strip() != last_segment: elif self.transcript[-1]["text"].strip() != last_segment:
self.transcript.append({"text": last_segment + " "}) self.transcript.append({"text": last_segment + " "})
with self.lock(): with self.lock:
self.timestamp_offset += duration self.timestamp_offset += duration
def speech_to_text(self): def speech_to_text(self):
@@ -800,6 +800,7 @@ class ServeClientFasterWhisper(ServeClientBase):
self.vad_parameters = vad_parameters or {"onset": 0.5} self.vad_parameters = vad_parameters or {"onset": 0.5}
self.no_speech_thresh = 0.45 self.no_speech_thresh = 0.45
self.same_output_threshold = 10 self.same_output_threshold = 10
self.end_time_for_same_output = None
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda": if device == "cuda":
@@ -1095,10 +1096,16 @@ class ServeClientFasterWhisper(ServeClientBase):
if self.current_out.strip() == self.prev_out.strip() and self.current_out != '': if self.current_out.strip() == self.prev_out.strip() and self.current_out != '':
self.same_output_count += 1 self.same_output_count += 1
# if we remove the audio because of same output on the nth reptition we might remove the
# audio thats not yet transcribed so, capturing the time when it was repeated for the first time
if self.end_time_for_same_output is None:
self.end_time_for_same_output = segments[-1].end
time.sleep(0.1) # wait for some voice activity just in case there is an unitended pause from the speaker for better punctuations. time.sleep(0.1) # wait for some voice activity just in case there is an unitended pause from the speaker for better punctuations.
else: else:
self.same_output_count = 0 self.same_output_count = 0
self.end_time_for_same_output = None
# if same incomplete segment is seen multiple times then update the offset # if same incomplete segment is seen multiple times then update the offset
# and append the segment to the list # and append the segment to the list
if self.same_output_count > self.same_output_threshold: if self.same_output_count > self.same_output_threshold:
@@ -1107,14 +1114,15 @@ class ServeClientFasterWhisper(ServeClientBase):
with self.lock: with self.lock:
self.transcript.append(self.format_segment( self.transcript.append(self.format_segment(
self.timestamp_offset, self.timestamp_offset,
self.timestamp_offset + duration, self.timestamp_offset + min(duration, self.end_time_for_same_output),
self.current_out, self.current_out,
completed=True completed=True
)) ))
self.current_out = '' self.current_out = ''
offset = duration offset = min(duration, self.end_time_for_same_output)
self.same_output_count = 0 self.same_output_count = 0
last_segment = None last_segment = None
self.end_time_for_same_output = None
else: else:
self.prev_out = self.current_out self.prev_out = self.current_out
+30 -15
View File
@@ -1,10 +1,9 @@
# original: https://github.com/snakers4/silero-vad/blob/master/utils_vad.py
import os import os
import subprocess import subprocess
import torch import torch
import numpy as np import numpy as np
import onnxruntime import onnxruntime
import warnings
class VoiceActivityDetection(): class VoiceActivityDetection():
@@ -24,7 +23,11 @@ class VoiceActivityDetection():
self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts) self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts)
self.reset_states() self.reset_states()
self.sample_rates = [8000, 16000] if '16k' in path:
warnings.warn('This model support only 16000 sampling rate!')
self.sample_rates = [16000]
else:
self.sample_rates = [8000, 16000]
def _validate_input(self, x, sr: int): def _validate_input(self, x, sr: int):
if x.dim() == 1: if x.dim() == 1:
@@ -34,27 +37,32 @@ class VoiceActivityDetection():
if sr != 16000 and (sr % 16000 == 0): if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000 step = sr // 16000
x = x[:, ::step] x = x[:,::step]
sr = 16000 sr = 16000
if sr not in self.sample_rates: if sr not in self.sample_rates:
raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)") raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)")
if sr / x.shape[1] > 31.25: if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short") raise ValueError("Input audio chunk is too short")
return x, sr return x, sr
def reset_states(self, batch_size=1): def reset_states(self, batch_size=1):
self._h = np.zeros((2, batch_size, 64)).astype('float32') self._state = torch.zeros((2, batch_size, 128)).float()
self._c = np.zeros((2, batch_size, 64)).astype('float32') self._context = torch.zeros(0)
self._last_sr = 0 self._last_sr = 0
self._last_batch_size = 0 self._last_batch_size = 0
def __call__(self, x, sr: int): def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr) x, sr = self._validate_input(x, sr)
num_samples = 512 if sr == 16000 else 256
if x.shape[-1] != num_samples:
raise ValueError(f"Provided number of samples is {x.shape[-1]} (Supported values: 256 for 8000 sample rate, 512 for 16000)")
batch_size = x.shape[0] batch_size = x.shape[0]
context_size = 64 if sr == 16000 else 32
if not self._last_batch_size: if not self._last_batch_size:
self.reset_states(batch_size) self.reset_states(batch_size)
@@ -63,28 +71,35 @@ class VoiceActivityDetection():
if (self._last_batch_size) and (self._last_batch_size != batch_size): if (self._last_batch_size) and (self._last_batch_size != batch_size):
self.reset_states(batch_size) self.reset_states(batch_size)
if not len(self._context):
self._context = torch.zeros(batch_size, context_size)
x = torch.cat([self._context, x], dim=1)
if sr in [8000, 16000]: if sr in [8000, 16000]:
ort_inputs = {'input': x.numpy(), 'h': self._h, 'c': self._c, 'sr': np.array(sr, dtype='int64')} ort_inputs = {'input': x.numpy(), 'state': self._state.numpy(), 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs) ort_outs = self.session.run(None, ort_inputs)
out, self._h, self._c = ort_outs out, state = ort_outs
self._state = torch.from_numpy(state)
else: else:
raise ValueError() raise ValueError()
self._context = x[..., -context_size:]
self._last_sr = sr self._last_sr = sr
self._last_batch_size = batch_size self._last_batch_size = batch_size
out = torch.tensor(out) out = torch.from_numpy(out)
return out return out
def audio_forward(self, x, sr: int, num_samples: int = 512): def audio_forward(self, x, sr: int):
outs = [] outs = []
x, sr = self._validate_input(x, sr) x, sr = self._validate_input(x, sr)
self.reset_states()
num_samples = 512 if sr == 16000 else 256
if x.shape[1] % num_samples: if x.shape[1] % num_samples:
pad_num = num_samples - (x.shape[1] % num_samples) pad_num = num_samples - (x.shape[1] % num_samples)
x = torch.nn.functional.pad(x, (0, pad_num), 'constant', value=0.0) x = torch.nn.functional.pad(x, (0, pad_num), 'constant', value=0.0)
self.reset_states(x.shape[0])
for i in range(0, x.shape[1], num_samples): for i in range(0, x.shape[1], num_samples):
wavs_batch = x[:, i:i+num_samples] wavs_batch = x[:, i:i+num_samples]
out_chunk = self.__call__(wavs_batch, sr) out_chunk = self.__call__(wavs_batch, sr)
@@ -94,7 +109,7 @@ class VoiceActivityDetection():
return stacked.cpu() return stacked.cpu()
@staticmethod @staticmethod
def download(model_url="https://github.com/snakers4/silero-vad/raw/v4.0/files/silero_vad.onnx"): def download(model_url="https://github.com/snakers4/silero-vad/raw/v5.0/files/silero_vad.onnx"):
target_dir = os.path.expanduser("~/.cache/whisper-live/") target_dir = os.path.expanduser("~/.cache/whisper-live/")
# Ensure the target directory exists # Ensure the target directory exists
@@ -138,5 +153,5 @@ class VoiceActivityDetector:
bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity; bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity;
False otherwise. False otherwise.
""" """
speech_prob = self.model(torch.from_numpy(audio_frame), self.frame_rate).item() speech_probs = self.model.audio_forward(torch.from_numpy(audio_frame.copy()), self.frame_rate)[0]
return speech_prob > self.threshold return torch.any(speech_probs > self.threshold).item()