🔨 refactor whisper_live according to flake8
This commit is contained in:
+5
-5
@@ -4,15 +4,15 @@ from whisper_live.server import TranscriptionServer
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--port', '-p',
|
parser.add_argument('--port', '-p',
|
||||||
type=int,
|
type=int,
|
||||||
default=9090,
|
default=9090,
|
||||||
help="Websocket port to run the server on.")
|
help="Websocket port to run the server on.")
|
||||||
parser.add_argument('--backend', '-b',
|
parser.add_argument('--backend', '-b',
|
||||||
type=str,
|
type=str,
|
||||||
default='faster_whisper',
|
default='faster_whisper',
|
||||||
help='Backends from ["tensorrt", "faster_whisper"]')
|
help='Backends from ["tensorrt", "faster_whisper"]')
|
||||||
parser.add_argument('--faster_whisper_custom_model_path', '-fw',
|
parser.add_argument('--faster_whisper_custom_model_path', '-fw',
|
||||||
type=str, default=None,
|
type=str, default=None,
|
||||||
help="Custom Faster Whisper Model")
|
help="Custom Faster Whisper Model")
|
||||||
parser.add_argument('--trt_model_path', '-trt',
|
parser.add_argument('--trt_model_path', '-trt',
|
||||||
type=str,
|
type=str,
|
||||||
@@ -30,7 +30,7 @@ if __name__ == "__main__":
|
|||||||
server = TranscriptionServer()
|
server = TranscriptionServer()
|
||||||
server.run(
|
server.run(
|
||||||
"0.0.0.0",
|
"0.0.0.0",
|
||||||
port=args.port,
|
port=args.port,
|
||||||
backend=args.backend,
|
backend=args.backend,
|
||||||
faster_whisper_custom_model_path=args.faster_whisper_custom_model_path,
|
faster_whisper_custom_model_path=args.faster_whisper_custom_model_path,
|
||||||
whisper_tensorrt_path=args.trt_model_path,
|
whisper_tensorrt_path=args.trt_model_path,
|
||||||
|
|||||||
@@ -10,36 +10,38 @@ HERE = pathlib.Path(__file__).parent
|
|||||||
README = (HERE / "README.md").read_text()
|
README = (HERE / "README.md").read_text()
|
||||||
|
|
||||||
# This call to setup() does all the work
|
# This call to setup() does all the work
|
||||||
setup(name="whisper-live",
|
setup(
|
||||||
version=__version__,
|
name="whisper-live",
|
||||||
description="A nearly-live implementation of OpenAI's Whisper.",
|
version=__version__,
|
||||||
long_description=README,
|
description="A nearly-live implementation of OpenAI's Whisper.",
|
||||||
long_description_content_type="text/markdown",
|
long_description=README,
|
||||||
include_package_data=True,
|
long_description_content_type="text/markdown",
|
||||||
url="https://github.com/collabora/WhisperLive",
|
include_package_data=True,
|
||||||
author="Collabora Ltd",
|
url="https://github.com/collabora/WhisperLive",
|
||||||
author_email="vineet.suryan@collabora.com",
|
author="Collabora Ltd",
|
||||||
license="MIT",
|
author_email="vineet.suryan@collabora.com",
|
||||||
classifiers=[
|
license="MIT",
|
||||||
"Development Status :: 4 - Beta",
|
classifiers=[
|
||||||
"Intended Audience :: Developers",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"Intended Audience :: Science/Research",
|
||||||
"Programming Language :: Python :: 3",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
"Programming Language :: Python :: 3.9",
|
||||||
],
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||||
packages=find_packages(
|
],
|
||||||
exclude=("examples",
|
packages=find_packages(
|
||||||
"Audio-Transcription-Chrome",
|
exclude=(
|
||||||
"Audio-Transcription-Firefox",
|
"examples",
|
||||||
"requirements",
|
"Audio-Transcription-Chrome",
|
||||||
"whisper-finetuning"
|
"Audio-Transcription-Firefox",
|
||||||
)
|
"requirements",
|
||||||
),
|
"whisper-finetuning"
|
||||||
install_requires=[
|
)
|
||||||
|
),
|
||||||
|
install_requires=[
|
||||||
"PyAudio",
|
"PyAudio",
|
||||||
"faster-whisper==0.10.0",
|
"faster-whisper==0.10.0",
|
||||||
"torch",
|
"torch",
|
||||||
@@ -53,6 +55,6 @@ setup(name="whisper-live",
|
|||||||
"openai-whisper",
|
"openai-whisper",
|
||||||
"kaldialign",
|
"kaldialign",
|
||||||
"soundfile",
|
"soundfile",
|
||||||
],
|
],
|
||||||
python_requires=">=3.8"
|
python_requires=">=3.8"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import scipy
|
|||||||
import websocket
|
import websocket
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from whisper_live.client import TranscriptionClient, resample
|
from whisper_live.client import TranscriptionClient
|
||||||
|
from whisper_live.utils import resample
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(unittest.TestCase):
|
class BaseTestCase(unittest.TestCase):
|
||||||
@@ -68,7 +69,7 @@ class TestClientCallbacks(BaseTestCase):
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
self.client.on_message(self.mock_ws_app, message)
|
self.client.on_message(self.mock_ws_app, message)
|
||||||
|
|
||||||
# Assert that the transcript was updated correctly
|
# Assert that the transcript was updated correctly
|
||||||
self.assertEqual(len(self.client.transcript), 2)
|
self.assertEqual(len(self.client.transcript), 2)
|
||||||
self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2")
|
self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2")
|
||||||
@@ -79,14 +80,14 @@ class TestClientCallbacks(BaseTestCase):
|
|||||||
self.client.on_close(self.mock_ws_app, close_status_code, close_msg)
|
self.client.on_close(self.mock_ws_app, close_status_code, close_msg)
|
||||||
|
|
||||||
self.assertFalse(self.client.recording)
|
self.assertFalse(self.client.recording)
|
||||||
self.assertFalse(self.client.server_error)
|
self.assertFalse(self.client.server_error)
|
||||||
self.assertFalse(self.client.waiting)
|
self.assertFalse(self.client.waiting)
|
||||||
|
|
||||||
def test_on_error(self):
|
def test_on_error(self):
|
||||||
error_message = "Test Error"
|
error_message = "Test Error"
|
||||||
self.client.on_error(self.mock_ws_app, error_message)
|
self.client.on_error(self.mock_ws_app, error_message)
|
||||||
|
|
||||||
self.assertTrue(self.client.server_error)
|
self.assertTrue(self.client.server_error)
|
||||||
self.assertEqual(self.client.error_message, error_message)
|
self.assertEqual(self.client.error_message, error_message)
|
||||||
|
|
||||||
|
|
||||||
@@ -95,10 +96,10 @@ class TestAudioResampling(unittest.TestCase):
|
|||||||
original_audio = "assets/jfk.flac"
|
original_audio = "assets/jfk.flac"
|
||||||
expected_sr = 16000
|
expected_sr = 16000
|
||||||
resampled_audio = resample(original_audio, expected_sr)
|
resampled_audio = resample(original_audio, expected_sr)
|
||||||
|
|
||||||
sr, _ = scipy.io.wavfile.read(resampled_audio)
|
sr, _ = scipy.io.wavfile.read(resampled_audio)
|
||||||
self.assertEqual(sr, expected_sr)
|
self.assertEqual(sr, expected_sr)
|
||||||
|
|
||||||
os.remove(resampled_audio)
|
os.remove(resampled_audio)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-22
@@ -14,32 +14,31 @@ from whisper.normalizers import EnglishTextNormalizer
|
|||||||
class TestTranscriptionServerInitialization(unittest.TestCase):
|
class TestTranscriptionServerInitialization(unittest.TestCase):
|
||||||
def test_initialization(self):
|
def test_initialization(self):
|
||||||
server = TranscriptionServer()
|
server = TranscriptionServer()
|
||||||
self.assertEqual(server.max_clients, 4)
|
self.assertEqual(server.client_manager.max_clients, 4)
|
||||||
self.assertEqual(server.max_connection_time, 600)
|
self.assertEqual(server.client_manager.max_connection_time, 600)
|
||||||
self.assertDictEqual(server.clients, {})
|
self.assertDictEqual(server.client_manager.clients, {})
|
||||||
self.assertDictEqual(server.websockets, {})
|
self.assertDictEqual(server.client_manager.start_times, {})
|
||||||
self.assertDictEqual(server.clients_start_time, {})
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetWaitTime(unittest.TestCase):
|
class TestGetWaitTime(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.server = TranscriptionServer()
|
self.server = TranscriptionServer()
|
||||||
self.server.clients_start_time = {
|
self.server.client_manager.start_times = {
|
||||||
'client1': time.time() - 120,
|
'client1': time.time() - 120,
|
||||||
'client2': time.time() - 300
|
'client2': time.time() - 300
|
||||||
}
|
}
|
||||||
self.server.max_connection_time = 600
|
self.server.client_manager.max_connection_time = 600
|
||||||
|
|
||||||
def test_get_wait_time(self):
|
def test_get_wait_time(self):
|
||||||
expected_wait_time = (600 - (time.time() - self.server.clients_start_time['client2'])) / 60
|
expected_wait_time = (600 - (time.time() - self.server.client_manager.start_times['client2'])) / 60
|
||||||
print(self.server.get_wait_time(), expected_wait_time)
|
print(self.server.client_manager.get_wait_time(), expected_wait_time)
|
||||||
self.assertAlmostEqual(self.server.get_wait_time(), expected_wait_time, places=2)
|
self.assertAlmostEqual(self.server.client_manager.get_wait_time(), expected_wait_time, places=2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestServerConnection(unittest.TestCase):
|
class TestServerConnection(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.server = TranscriptionServer()
|
self.server = TranscriptionServer()
|
||||||
|
|
||||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||||
def test_connection(self, mock_websocket):
|
def test_connection(self, mock_websocket):
|
||||||
mock_websocket.recv.return_value = json.dumps({
|
mock_websocket.recv.return_value = json.dumps({
|
||||||
@@ -50,7 +49,6 @@ class TestServerConnection(unittest.TestCase):
|
|||||||
})
|
})
|
||||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||||
def test_recv_audio_exception_handling(self, mock_websocket):
|
def test_recv_audio_exception_handling(self, mock_websocket):
|
||||||
mock_websocket.recv.side_effect = [json.dumps({
|
mock_websocket.recv.side_effect = [json.dumps({
|
||||||
@@ -58,12 +56,12 @@ class TestServerConnection(unittest.TestCase):
|
|||||||
'language': 'en',
|
'language': 'en',
|
||||||
'task': 'transcribe',
|
'task': 'transcribe',
|
||||||
'model': 'tiny.en'
|
'model': 'tiny.en'
|
||||||
}), np.array([1, 2, 3]).tobytes()]
|
}), np.array([1, 2, 3]).tobytes()]
|
||||||
|
|
||||||
with self.assertLogs(level="ERROR"):
|
with self.assertLogs(level="ERROR"):
|
||||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||||
|
|
||||||
self.assertNotIn(mock_websocket, self.server.clients)
|
self.assertNotIn(mock_websocket, self.server.client_manager.clients)
|
||||||
|
|
||||||
|
|
||||||
class TestServerInferenceAccuracy(unittest.TestCase):
|
class TestServerInferenceAccuracy(unittest.TestCase):
|
||||||
@@ -71,12 +69,12 @@ class TestServerInferenceAccuracy(unittest.TestCase):
|
|||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.server_process = subprocess.Popen(["python", "run_server.py"]) # Adjust the command as needed
|
cls.server_process = subprocess.Popen(["python", "run_server.py"]) # Adjust the command as needed
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
cls.server_process.terminate()
|
cls.server_process.terminate()
|
||||||
cls.server_process.wait()
|
cls.server_process.wait()
|
||||||
|
|
||||||
@mock.patch('pyaudio.PyAudio')
|
@mock.patch('pyaudio.PyAudio')
|
||||||
def setUp(self, mock_pyaudio):
|
def setUp(self, mock_pyaudio):
|
||||||
self.mock_pyaudio = mock_pyaudio.return_value
|
self.mock_pyaudio = mock_pyaudio.return_value
|
||||||
@@ -84,16 +82,16 @@ class TestServerInferenceAccuracy(unittest.TestCase):
|
|||||||
self.mock_pyaudio.open.return_value = self.mock_stream
|
self.mock_pyaudio.open.return_value = self.mock_stream
|
||||||
self.metric = evaluate.load("wer")
|
self.metric = evaluate.load("wer")
|
||||||
self.normalizer = EnglishTextNormalizer()
|
self.normalizer = EnglishTextNormalizer()
|
||||||
self.client = TranscriptionClient(
|
self.client = TranscriptionClient(
|
||||||
"localhost", "9090", model="base.en", lang="en",
|
"localhost", "9090", model="base.en", lang="en",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_inference(self):
|
def test_inference(self):
|
||||||
gt = "And so my fellow Americans, ask not, what your country can do for you. Ask what you can do for your country!"
|
gt = "And so my fellow Americans, ask not, what your country can do for you. Ask what you can do for your country!"
|
||||||
self.client("assets/jfk.flac")
|
self.client("assets/jfk.flac")
|
||||||
with open("output.srt", "r") as f:
|
with open("output.srt", "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
prediction = " ".join([l.strip() for l in lines[2::4]])
|
prediction = " ".join([line.strip() for line in lines[2::4]])
|
||||||
prediction_normalized = self.normalizer(prediction)
|
prediction_normalized = self.normalizer(prediction)
|
||||||
gt_normalized = self.normalizer(gt)
|
gt_normalized = self.normalizer(gt)
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
import scipy.io as sio
|
|
||||||
from whisper_live.tensorrt_utils import load_audio
|
from whisper_live.tensorrt_utils import load_audio
|
||||||
from whisper_live.vad import VoiceActivityDetection
|
from whisper_live.vad import VoiceActivityDetection
|
||||||
|
|
||||||
@@ -25,4 +24,4 @@ class TestVoiceActivityDetection(unittest.TestCase):
|
|||||||
def test_vad_speech_detection(self):
|
def test_vad_speech_detection(self):
|
||||||
audio_tensor = torch.from_numpy(load_audio("assets/jfk.flac"))
|
audio_tensor = torch.from_numpy(load_audio("assets/jfk.flac"))
|
||||||
speech_prob = self.vad(audio_tensor, self.sample_rate).item()
|
speech_prob = self.vad(audio_tensor, self.sample_rate).item()
|
||||||
self.assertGreater(speech_prob, 0.5, "VAD failed to identify speech segment.")
|
self.assertGreater(speech_prob, 0.5, "VAD failed to identify speech segment.")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__="0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|||||||
+57
-124
@@ -2,68 +2,14 @@ import os
|
|||||||
import wave
|
import wave
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy
|
|
||||||
import ffmpeg
|
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import threading
|
import threading
|
||||||
import textwrap
|
|
||||||
import json
|
import json
|
||||||
import websocket
|
import websocket
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
import ffmpeg
|
||||||
|
import whisper_live.utils as utils
|
||||||
def format_time(s):
|
|
||||||
"""Convert seconds (float) to SRT time format."""
|
|
||||||
hours = int(s // 3600)
|
|
||||||
minutes = int((s % 3600) // 60)
|
|
||||||
seconds = int(s % 60)
|
|
||||||
milliseconds = int((s - int(s)) * 1000)
|
|
||||||
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
|
|
||||||
|
|
||||||
def create_srt_file(segments, output_file):
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as srt_file:
|
|
||||||
segment_number = 1
|
|
||||||
for segment in segments:
|
|
||||||
start_time = format_time(float(segment['start']))
|
|
||||||
end_time = format_time(float(segment['end']))
|
|
||||||
text = segment['text']
|
|
||||||
|
|
||||||
srt_file.write(f"{segment_number}\n")
|
|
||||||
srt_file.write(f"{start_time} --> {end_time}\n")
|
|
||||||
srt_file.write(f"{text}\n\n")
|
|
||||||
|
|
||||||
segment_number += 1
|
|
||||||
|
|
||||||
|
|
||||||
def resample(file: str, sr: int = 16000):
|
|
||||||
"""
|
|
||||||
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
|
|
||||||
Open an audio file and read as mono waveform, resampling as necessary,
|
|
||||||
save the resampled audio
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file (str): The audio file to open
|
|
||||||
sr (int): The sample rate to resample the audio if necessary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
resampled_file (str): The resampled audio file
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
|
||||||
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
|
||||||
out, _ = (
|
|
||||||
ffmpeg.input(file, threads=0)
|
|
||||||
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
|
|
||||||
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
|
||||||
)
|
|
||||||
except ffmpeg.Error as e:
|
|
||||||
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
|
|
||||||
np_buffer = np.frombuffer(out, dtype=np.int16)
|
|
||||||
|
|
||||||
resampled_file = f"{file.split('.')[0]}_resampled.wav"
|
|
||||||
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
|
|
||||||
return resampled_file
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
@@ -150,10 +96,40 @@ class Client:
|
|||||||
self.transcript = []
|
self.transcript = []
|
||||||
print("[INFO]: * recording")
|
print("[INFO]: * recording")
|
||||||
|
|
||||||
|
def handle_status_messages(self, message_data):
|
||||||
|
"""Handles server status messages."""
|
||||||
|
status = message_data["status"]
|
||||||
|
if status == "WAIT":
|
||||||
|
self.waiting = True
|
||||||
|
print(f"[INFO]: Server is full. Estimated wait time {round(message_data['message'])} minutes.")
|
||||||
|
elif status == "ERROR":
|
||||||
|
print(f"Message from Server: {message_data['message']}")
|
||||||
|
self.server_error = True
|
||||||
|
elif status == "WARNING":
|
||||||
|
print(f"Message from Server: {message_data['message']}")
|
||||||
|
|
||||||
|
def process_segments(self, segments):
|
||||||
|
"""Processes transcript segments."""
|
||||||
|
text = []
|
||||||
|
for i, seg in enumerate(segments):
|
||||||
|
if not text or text[-1] != seg["text"]:
|
||||||
|
text.append(seg["text"])
|
||||||
|
if i == len(segments) - 1:
|
||||||
|
self.last_segment = seg
|
||||||
|
elif (self.server_backend == "faster_whisper" and
|
||||||
|
(not self.transcript or
|
||||||
|
float(seg['start']) >= float(self.transcript[-1]['end']))):
|
||||||
|
self.transcript.append(seg)
|
||||||
|
|
||||||
|
# Truncate to last 3 entries for brevity.
|
||||||
|
text = text[-3:]
|
||||||
|
# utils.clear_screen()
|
||||||
|
utils.print_transcript(text)
|
||||||
|
|
||||||
def on_message(self, ws, message):
|
def on_message(self, ws, message):
|
||||||
"""
|
"""
|
||||||
Callback function called when a message is received from the server.
|
Callback function called when a message is received from the server.
|
||||||
|
|
||||||
It updates various attributes of the client based on the received message, including
|
It updates various attributes of the client based on the received message, including
|
||||||
recording status, language detection, and server messages. If a disconnect message
|
recording status, language detection, and server messages. If a disconnect message
|
||||||
is received, it sets the recording status to False.
|
is received, it sets the recording status to False.
|
||||||
@@ -171,14 +147,7 @@ class Client:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if "status" in message.keys():
|
if "status" in message.keys():
|
||||||
if message["status"] == "WAIT":
|
self.handle_status_messages(message)
|
||||||
self.waiting = True
|
|
||||||
print(
|
|
||||||
f"[INFO]:Server is full. Estimated wait time {round(message['message'])} minutes."
|
|
||||||
)
|
|
||||||
elif message["status"] == "ERROR":
|
|
||||||
print(f"Message from Server: {message['message']}")
|
|
||||||
self.server_error = True
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if "message" in message.keys() and message["message"] == "DISCONNECT":
|
if "message" in message.keys() and message["message"] == "DISCONNECT":
|
||||||
@@ -199,38 +168,8 @@ class Client:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if "segments" not in message.keys():
|
if "segments" in message.keys():
|
||||||
return
|
self.process_segments(message["segments"])
|
||||||
|
|
||||||
message = message["segments"]
|
|
||||||
text = []
|
|
||||||
n_segments = len(message)
|
|
||||||
|
|
||||||
if n_segments:
|
|
||||||
for i, seg in enumerate(message):
|
|
||||||
if text and text[-1] == seg["text"]:
|
|
||||||
# already got it
|
|
||||||
continue
|
|
||||||
text.append(seg["text"])
|
|
||||||
|
|
||||||
if i == n_segments-1:
|
|
||||||
self.last_segment = seg
|
|
||||||
elif self.server_backend == "faster_whisper":
|
|
||||||
if not len(self.transcript) or float(seg['start']) >= float(self.transcript[-1]['end']):
|
|
||||||
self.transcript.append(seg)
|
|
||||||
|
|
||||||
# keep only last 3
|
|
||||||
if len(text) > 3:
|
|
||||||
text = text[-3:]
|
|
||||||
wrapper = textwrap.TextWrapper(width=60)
|
|
||||||
word_list = wrapper.wrap(text="".join(text))
|
|
||||||
# Print each line.
|
|
||||||
if os.name == "nt":
|
|
||||||
os.system("cls")
|
|
||||||
else:
|
|
||||||
os.system("clear")
|
|
||||||
for element in word_list:
|
|
||||||
print(element)
|
|
||||||
|
|
||||||
def on_error(self, ws, error):
|
def on_error(self, ws, error):
|
||||||
print(f"[ERROR] WebSocket Error: {error}")
|
print(f"[ERROR] WebSocket Error: {error}")
|
||||||
@@ -246,7 +185,7 @@ class Client:
|
|||||||
def on_open(self, ws):
|
def on_open(self, ws):
|
||||||
"""
|
"""
|
||||||
Callback function called when the WebSocket connection is successfully opened.
|
Callback function called when the WebSocket connection is successfully opened.
|
||||||
|
|
||||||
Sends an initial configuration message to the server, including client UID,
|
Sends an initial configuration message to the server, including client UID,
|
||||||
language selection, and task type.
|
language selection, and task type.
|
||||||
|
|
||||||
@@ -270,8 +209,8 @@ class Client:
|
|||||||
def bytes_to_float_array(audio_bytes):
|
def bytes_to_float_array(audio_bytes):
|
||||||
"""
|
"""
|
||||||
Convert audio data from bytes to a NumPy float array.
|
Convert audio data from bytes to a NumPy float array.
|
||||||
|
|
||||||
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
|
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
|
||||||
have values between -1 and 1.
|
have values between -1 and 1.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -299,10 +238,10 @@ class Client:
|
|||||||
def play_file(self, filename):
|
def play_file(self, filename):
|
||||||
"""
|
"""
|
||||||
Play an audio file and send it to the server for processing.
|
Play an audio file and send it to the server for processing.
|
||||||
|
|
||||||
Reads an audio file, plays it through the audio output, and simultaneously sends
|
Reads an audio file, plays it through the audio output, and simultaneously sends
|
||||||
the audio data to the server for processing. It uses PyAudio to create an audio
|
the audio data to the server for processing. It uses PyAudio to create an audio
|
||||||
stream for playback. The audio data is read from the file in chunks, converted to
|
stream for playback. The audio data is read from the file in chunks, converted to
|
||||||
floating-point format, and sent to the server using WebSocket communication.
|
floating-point format, and sent to the server using WebSocket communication.
|
||||||
This method is typically used when you want to process pre-recorded audio and send it
|
This method is typically used when you want to process pre-recorded audio and send it
|
||||||
to the server in real-time.
|
to the server in real-time.
|
||||||
@@ -310,7 +249,7 @@ class Client:
|
|||||||
Args:
|
Args:
|
||||||
filename (str): The path to the audio file to be played and sent to the server.
|
filename (str): The path to the audio file to be played and sent to the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# read audio and create pyaudio stream
|
# read audio and create pyaudio stream
|
||||||
with wave.open(filename, "rb") as wavfile:
|
with wave.open(filename, "rb") as wavfile:
|
||||||
self.stream = self.p.open(
|
self.stream = self.p.open(
|
||||||
@@ -356,7 +295,7 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
Close the WebSocket connection and join the WebSocket thread.
|
Close the WebSocket connection and join the WebSocket thread.
|
||||||
|
|
||||||
First attempts to close the WebSocket connection using `self.client_socket.close()`. After
|
First attempts to close the WebSocket connection using `self.client_socket.close()`. After
|
||||||
closing the connection, it joins the WebSocket thread to ensure proper termination.
|
closing the connection, it joins the WebSocket thread to ensure proper termination.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -383,7 +322,7 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
Write audio frames to a WAV file.
|
Write audio frames to a WAV file.
|
||||||
|
|
||||||
The WAV file is created or overwritten with the specified name. The audio frames should be
|
The WAV file is created or overwritten with the specified name. The audio frames should be
|
||||||
in the correct format and match the specified channel, sample width, and sample rate.
|
in the correct format and match the specified channel, sample width, and sample rate.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -433,7 +372,6 @@ class Client:
|
|||||||
|
|
||||||
print("[INFO]: HLS stream processing finished.")
|
print("[INFO]: HLS stream processing finished.")
|
||||||
|
|
||||||
|
|
||||||
def record(self, out_file="output_recording.wav"):
|
def record(self, out_file="output_recording.wav"):
|
||||||
"""
|
"""
|
||||||
Record audio data from the input stream and save it to a WAV file.
|
Record audio data from the input stream and save it to a WAV file.
|
||||||
@@ -444,11 +382,12 @@ class Client:
|
|||||||
|
|
||||||
Audio data is saved in chunks to the "chunks" directory. Each chunk is saved as a separate WAV file.
|
Audio data is saved in chunks to the "chunks" directory. Each chunk is saved as a separate WAV file.
|
||||||
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
|
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
|
||||||
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
|
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
|
||||||
the method combines all the saved audio chunks into the specified `out_file`.
|
the method combines all the saved audio chunks into the specified `out_file`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
out_file (str, optional): The name of the output WAV file to save the entire recording. Default is "output_recording.wav".
|
out_file (str, optional): The name of the output WAV file to save the entire recording.
|
||||||
|
Default is "output_recording.wav".
|
||||||
|
|
||||||
"""
|
"""
|
||||||
n_audio_file = 0
|
n_audio_file = 0
|
||||||
@@ -458,7 +397,7 @@ class Client:
|
|||||||
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
|
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
|
||||||
if not self.recording:
|
if not self.recording:
|
||||||
break
|
break
|
||||||
data = self.stream.read(self.chunk, exception_on_overflow = False)
|
data = self.stream.read(self.chunk, exception_on_overflow=False)
|
||||||
self.frames += data
|
self.frames += data
|
||||||
|
|
||||||
audio_array = Client.bytes_to_float_array(data)
|
audio_array = Client.bytes_to_float_array(data)
|
||||||
@@ -498,8 +437,8 @@ class Client:
|
|||||||
def write_output_recording(self, n_audio_file, out_file):
|
def write_output_recording(self, n_audio_file, out_file):
|
||||||
"""
|
"""
|
||||||
Combine and save recorded audio chunks into a single WAV file.
|
Combine and save recorded audio chunks into a single WAV file.
|
||||||
|
|
||||||
The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk
|
The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk
|
||||||
file, appends its audio data to the final recording, and then deletes the chunk file. After combining
|
file, appends its audio data to the final recording, and then deletes the chunk file. After combining
|
||||||
and saving, the final recording is stored in the specified `out_file`.
|
and saving, the final recording is stored in the specified `out_file`.
|
||||||
|
|
||||||
@@ -532,7 +471,7 @@ class Client:
|
|||||||
|
|
||||||
def write_srt_file(self, output_path="output.srt"):
|
def write_srt_file(self, output_path="output.srt"):
|
||||||
self.transcript.append(self.last_segment)
|
self.transcript.append(self.last_segment)
|
||||||
create_srt_file(self.transcript, output_path)
|
utils.create_srt_file(self.transcript, output_path)
|
||||||
|
|
||||||
|
|
||||||
class TranscriptionClient:
|
class TranscriptionClient:
|
||||||
@@ -558,13 +497,7 @@ class TranscriptionClient:
|
|||||||
transcription_client()
|
transcription_client()
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
def __init__(self, host, port, lang=None, translate=False, model="small"):
|
||||||
host,
|
|
||||||
port,
|
|
||||||
lang=None,
|
|
||||||
translate=False,
|
|
||||||
model="small",
|
|
||||||
):
|
|
||||||
self.client = Client(host, port, lang, translate, model)
|
self.client = Client(host, port, lang, translate, model)
|
||||||
|
|
||||||
def __call__(self, audio=None, hls_url=None):
|
def __call__(self, audio=None, hls_url=None):
|
||||||
@@ -572,12 +505,12 @@ class TranscriptionClient:
|
|||||||
Start the transcription process.
|
Start the transcription process.
|
||||||
|
|
||||||
Initiates the transcription process by connecting to the server via a WebSocket. It waits for the server
|
Initiates the transcription process by connecting to the server via a WebSocket. It waits for the server
|
||||||
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it
|
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it
|
||||||
will be played and streamed to the server; otherwise, it will perform live recording.
|
will be played and streamed to the server; otherwise, it will perform live recording.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording.
|
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
print("[INFO]: Waiting for server ready ...")
|
print("[INFO]: Waiting for server ready ...")
|
||||||
while not self.client.recording:
|
while not self.client.recording:
|
||||||
@@ -589,7 +522,7 @@ class TranscriptionClient:
|
|||||||
if hls_url is not None:
|
if hls_url is not None:
|
||||||
self.client.process_hls_stream(hls_url)
|
self.client.process_hls_stream(hls_url)
|
||||||
elif audio is not None:
|
elif audio is not None:
|
||||||
resampled_file = resample(audio)
|
resampled_file = utils.resample(audio)
|
||||||
self.client.play_file(resampled_file)
|
self.client.play_file(resampled_file)
|
||||||
else:
|
else:
|
||||||
self.client.record()
|
self.client.record()
|
||||||
|
|||||||
+334
-309
@@ -3,25 +3,80 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
logging.basicConfig(level = logging.INFO)
|
|
||||||
|
|
||||||
from websockets.sync.server import serve
|
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from websockets.sync.server import serve
|
||||||
from whisper_live.vad import VoiceActivityDetection
|
|
||||||
import functools
|
|
||||||
|
|
||||||
from whisper_live.vad import VoiceActivityDetection
|
from whisper_live.vad import VoiceActivityDetection
|
||||||
from whisper_live.transcriber import WhisperModel
|
from whisper_live.transcriber import WhisperModel
|
||||||
try:
|
try:
|
||||||
from whisper_live.transcriber_tensorrt import WhisperTRTLLM
|
from whisper_live.transcriber_tensorrt import WhisperTRTLLM
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceActivityDetector:
|
||||||
|
def __init__(self, threshold=0.5):
|
||||||
|
self.model = VoiceActivityDetection()
|
||||||
|
self.threshold = threshold
|
||||||
|
|
||||||
|
def __call__(self, audio_frame):
|
||||||
|
speech_prob = self.model(torch.from_numpy(audio_frame), TranscriptionServer.RATE).item()
|
||||||
|
return speech_prob > self.threshold
|
||||||
|
|
||||||
|
|
||||||
|
class ClientManager:
|
||||||
|
def __init__(self, max_clients=4, max_connection_time=600):
|
||||||
|
self.clients = {}
|
||||||
|
self.start_times = {}
|
||||||
|
self.max_clients = max_clients
|
||||||
|
self.max_connection_time = max_connection_time
|
||||||
|
|
||||||
|
def add_client(self, websocket, client):
|
||||||
|
self.clients[websocket] = client
|
||||||
|
self.start_times[websocket] = time.time()
|
||||||
|
|
||||||
|
def get_client(self, websocket):
|
||||||
|
if websocket in self.clients:
|
||||||
|
return self.clients[websocket]
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_client(self, websocket):
|
||||||
|
client = self.clients.pop(websocket, None)
|
||||||
|
if client:
|
||||||
|
client.cleanup()
|
||||||
|
self.start_times.pop(websocket, None)
|
||||||
|
|
||||||
|
def get_wait_time(self):
|
||||||
|
"""Calculate and return the estimated wait time for clients."""
|
||||||
|
wait_time = None
|
||||||
|
for start_time in self.start_times.values():
|
||||||
|
current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
|
||||||
|
if wait_time is None or current_client_time_remaining < wait_time:
|
||||||
|
wait_time = current_client_time_remaining
|
||||||
|
return wait_time / 60 if wait_time is not None else 0
|
||||||
|
|
||||||
|
def is_server_full(self, websocket, options):
|
||||||
|
"""Check if the server is full and send wait message if necessary."""
|
||||||
|
if len(self.clients) >= self.max_clients:
|
||||||
|
wait_time = self.get_wait_time()
|
||||||
|
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time}
|
||||||
|
websocket.send(json.dumps(response))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_client_timeout(self, websocket):
|
||||||
|
elapsed_time = time.time() - self.start_times[websocket]
|
||||||
|
if elapsed_time >= self.max_connection_time:
|
||||||
|
self.clients[websocket].disconnect()
|
||||||
|
logging.warning(f"Client with uid '{self.clients[websocket].client_uid}' disconnected due to overtime.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TranscriptionServer:
|
class TranscriptionServer:
|
||||||
"""
|
"""
|
||||||
@@ -42,12 +97,8 @@ class TranscriptionServer:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# voice activity detection model
|
# voice activity detection model
|
||||||
|
self.client_manager = ClientManager()
|
||||||
self.clients = {}
|
self.no_voice_activity_chunks = 0
|
||||||
self.websockets = {}
|
|
||||||
self.clients_start_time = {}
|
|
||||||
self.max_clients = 4
|
|
||||||
self.max_connection_time = 600
|
|
||||||
|
|
||||||
def get_wait_time(self):
|
def get_wait_time(self):
|
||||||
"""
|
"""
|
||||||
@@ -58,7 +109,7 @@ class TranscriptionServer:
|
|||||||
"""
|
"""
|
||||||
wait_time = None
|
wait_time = None
|
||||||
|
|
||||||
for k, v in self.clients_start_time.items():
|
for _, v in self.clients_start_time.items():
|
||||||
current_client_time_remaining = self.max_connection_time - (time.time() - v)
|
current_client_time_remaining = self.max_connection_time - (time.time() - v)
|
||||||
|
|
||||||
if wait_time is None or current_client_time_remaining < wait_time:
|
if wait_time is None or current_client_time_remaining < wait_time:
|
||||||
@@ -66,6 +117,64 @@ class TranscriptionServer:
|
|||||||
|
|
||||||
return wait_time / 60
|
return wait_time / 60
|
||||||
|
|
||||||
|
def is_server_full(self, websocket, options):
|
||||||
|
if len(self.clients) >= self.max_clients:
|
||||||
|
wait_time = self.get_wait_time()
|
||||||
|
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time}
|
||||||
|
websocket.send(json.dumps(response))
|
||||||
|
websocket.close()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initialize_client(
|
||||||
|
self, websocket, options, faster_whisper_custom_model_path,
|
||||||
|
whisper_tensorrt_path, trt_multilingual
|
||||||
|
):
|
||||||
|
if self.backend == "tensorrt":
|
||||||
|
try:
|
||||||
|
client = ServeClientTensorRT(
|
||||||
|
websocket,
|
||||||
|
multilingual=trt_multilingual,
|
||||||
|
language=options["language"],
|
||||||
|
task=options["task"],
|
||||||
|
client_uid=options["uid"],
|
||||||
|
model=whisper_tensorrt_path
|
||||||
|
)
|
||||||
|
logging.info("Running TensorRT backend.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"TensorRT-LLM not supported: {e}")
|
||||||
|
self.client_uid = options["uid"]
|
||||||
|
websocket.send(json.dumps({
|
||||||
|
"uid": self.client_uid,
|
||||||
|
"status": "WARNING",
|
||||||
|
"message": "TensorRT-LLM not supported on Server yet. "
|
||||||
|
"Reverting to available backend: 'faster_whisper'"
|
||||||
|
}))
|
||||||
|
self.backend = "faster_whisper"
|
||||||
|
|
||||||
|
if self.backend == "faster_whisper":
|
||||||
|
if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path):
|
||||||
|
logging.info(f"Using custom model {faster_whisper_custom_model_path}")
|
||||||
|
options["model"] = faster_whisper_custom_model_path
|
||||||
|
client = ServeClientFasterWhisper(
|
||||||
|
websocket,
|
||||||
|
language=options["language"],
|
||||||
|
task=options["task"],
|
||||||
|
client_uid=options["uid"],
|
||||||
|
model=options["model"],
|
||||||
|
initial_prompt=options.get("initial_prompt"),
|
||||||
|
vad_parameters=options.get("vad_parameters")
|
||||||
|
)
|
||||||
|
logging.info("Running faster_whisper backend.")
|
||||||
|
|
||||||
|
# self.clients[websocket] = client
|
||||||
|
# self.clients_start_time[websocket] = time.time()
|
||||||
|
self.client_manager.add_client(websocket, client)
|
||||||
|
|
||||||
|
def get_audio_from_websocket(self, websocket):
|
||||||
|
frame_data = websocket.recv()
|
||||||
|
return np.frombuffer(frame_data, dtype=np.float32)
|
||||||
|
|
||||||
def recv_audio(self,
|
def recv_audio(self,
|
||||||
websocket,
|
websocket,
|
||||||
backend="faster_whisper",
|
backend="faster_whisper",
|
||||||
@@ -74,7 +183,7 @@ class TranscriptionServer:
|
|||||||
trt_multilingual=False):
|
trt_multilingual=False):
|
||||||
"""
|
"""
|
||||||
Receive audio chunks from a client in an infinite loop.
|
Receive audio chunks from a client in an infinite loop.
|
||||||
|
|
||||||
Continuously receives audio frames from a connected client
|
Continuously receives audio frames from a connected client
|
||||||
over a WebSocket connection. It processes the audio frames using a
|
over a WebSocket connection. It processes the audio frames using a
|
||||||
voice activity detection (VAD) model to determine if they contain speech
|
voice activity detection (VAD) model to determine if they contain speech
|
||||||
@@ -96,127 +205,53 @@ class TranscriptionServer:
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If there is an error during the audio frame processing.
|
Exception: If there is an error during the audio frame processing.
|
||||||
"""
|
"""
|
||||||
self.backend = backend
|
|
||||||
if self.backend == "tensorrt":
|
|
||||||
self.vad_model = VoiceActivityDetection()
|
|
||||||
self.vad_threshold = 0.5
|
|
||||||
|
|
||||||
logging.info("New client connected")
|
logging.info("New client connected")
|
||||||
options = websocket.recv()
|
options = websocket.recv()
|
||||||
options = json.loads(options)
|
options = json.loads(options)
|
||||||
|
|
||||||
if len(self.clients) >= self.max_clients:
|
if self.client_manager.is_server_full(websocket, options):
|
||||||
logging.warning("Client Queue Full. Asking client to wait ...")
|
|
||||||
wait_time = self.get_wait_time()
|
|
||||||
response = {
|
|
||||||
"uid": options["uid"],
|
|
||||||
"status": "WAIT",
|
|
||||||
"message": wait_time,
|
|
||||||
}
|
|
||||||
websocket.send(json.dumps(response))
|
|
||||||
websocket.close()
|
websocket.close()
|
||||||
del websocket
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.backend = backend
|
||||||
if self.backend == "tensorrt":
|
if self.backend == "tensorrt":
|
||||||
try:
|
self.vad_detector = VoiceActivityDetector()
|
||||||
import tensorrt as trt
|
|
||||||
import tensorrt_llm
|
|
||||||
self.backend = "tensorrt"
|
|
||||||
client = ServeClientTensorRT(
|
|
||||||
websocket,
|
|
||||||
multilingual=trt_multilingual,
|
|
||||||
language=options["language"],
|
|
||||||
task=options["task"],
|
|
||||||
client_uid=options["uid"],
|
|
||||||
model=whisper_tensorrt_path
|
|
||||||
)
|
|
||||||
logging.info(f"Running TensorRT backend.")
|
|
||||||
except Exception as e:
|
|
||||||
self.client_uid = options["uid"]
|
|
||||||
websocket.send(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"uid": self.client_uid,
|
|
||||||
"status": "ERROR",
|
|
||||||
"message": f"TensorRT-LLM not supported on Server yet. Reverting to available backend: 'faster_whisper'"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.backend = "faster_whisper"
|
|
||||||
|
|
||||||
if self.backend == "faster_whisper":
|
self.initialize_client(
|
||||||
# validate custom model
|
websocket, options, faster_whisper_custom_model_path, whisper_tensorrt_path, trt_multilingual)
|
||||||
if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path):
|
|
||||||
logging.info(f"Using custom model {faster_whisper_custom_model_path}")
|
|
||||||
options["model"] = faster_whisper_custom_model_path
|
|
||||||
client = ServeClientFasterWhisper(
|
|
||||||
websocket,
|
|
||||||
language=options["language"],
|
|
||||||
task=options["task"],
|
|
||||||
client_uid=options["uid"],
|
|
||||||
model=options["model"],
|
|
||||||
initial_prompt=options.get("initial_prompt"),
|
|
||||||
vad_parameters=options.get("vad_parameters")
|
|
||||||
)
|
|
||||||
logging.info(f"Running faster_whisper backend.")
|
|
||||||
|
|
||||||
self.clients[websocket] = client
|
|
||||||
self.clients_start_time[websocket] = time.time()
|
|
||||||
no_voice_activity_chunks = 0
|
|
||||||
|
|
||||||
while True:
|
while not self.client_manager.is_client_timeout(websocket):
|
||||||
try:
|
try:
|
||||||
frame_data = websocket.recv()
|
frame_np = self.get_audio_from_websocket(websocket)
|
||||||
frame_np = np.frombuffer(frame_data, dtype=np.float32)
|
client = self.client_manager.get_client(websocket)
|
||||||
|
|
||||||
# VAD, for faster_whisper VAD model is already integrated
|
# VAD, for faster_whisper VAD model is already integrated
|
||||||
if self.backend == "tensorrt":
|
if self.backend == "tensorrt":
|
||||||
try:
|
if not self.voice_activity(websocket, frame_np):
|
||||||
speech_prob = self.vad_model(torch.from_numpy(frame_np.copy()), self.RATE).item()
|
continue
|
||||||
if speech_prob < self.vad_threshold:
|
self.no_voice_activity_chunks = 0
|
||||||
no_voice_activity_chunks += 1
|
client.set_eos(False)
|
||||||
if no_voice_activity_chunks > 3:
|
|
||||||
if not self.clients[websocket].eos:
|
|
||||||
self.clients[websocket].set_eos(True)
|
|
||||||
time.sleep(0.1) # Sleep 100m; wait some voice activity.
|
|
||||||
continue
|
|
||||||
no_voice_activity_chunks = 0
|
|
||||||
self.clients[websocket].set_eos(False)
|
|
||||||
|
|
||||||
except Exception as e:
|
client.add_frames(frame_np)
|
||||||
logging.error(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.clients[websocket].add_frames(frame_np)
|
|
||||||
|
|
||||||
elapsed_time = time.time() - self.clients_start_time[websocket]
|
|
||||||
if elapsed_time >= self.max_connection_time:
|
|
||||||
self.clients[websocket].disconnect()
|
|
||||||
logging.warning(f"Client with uid '{self.clients[websocket].client_uid}' disconnected due to overtime.")
|
|
||||||
self.clients[websocket].cleanup()
|
|
||||||
self.clients.pop(websocket)
|
|
||||||
self.clients_start_time.pop(websocket)
|
|
||||||
websocket.close()
|
|
||||||
del websocket
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
self.clients[websocket].cleanup()
|
self.cleanup(websocket)
|
||||||
self.clients.pop(websocket)
|
websocket.close()
|
||||||
self.clients_start_time.pop(websocket)
|
|
||||||
del websocket
|
|
||||||
break
|
break
|
||||||
|
|
||||||
def run(self,
|
if self.client_manager.get_client(websocket):
|
||||||
host,
|
self.cleanup(websocket)
|
||||||
port=9090,
|
websocket.close()
|
||||||
backend="tensorrt",
|
del websocket
|
||||||
|
|
||||||
|
def run(self,
|
||||||
|
host,
|
||||||
|
port=9090,
|
||||||
|
backend="tensorrt",
|
||||||
faster_whisper_custom_model_path=None,
|
faster_whisper_custom_model_path=None,
|
||||||
whisper_tensorrt_path=None,
|
whisper_tensorrt_path=None,
|
||||||
trt_multilingual=False
|
trt_multilingual=False):
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Run the transcription server.
|
Run the transcription server.
|
||||||
|
|
||||||
@@ -237,6 +272,21 @@ class TranscriptionServer:
|
|||||||
) as server:
|
) as server:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|
||||||
|
def voice_activity(self, websocket, frame_np):
|
||||||
|
if not self.vad_detector(frame_np):
|
||||||
|
self.no_voice_activity_chunks += 1
|
||||||
|
if self.no_voice_activity_chunks > 3:
|
||||||
|
client = self.client_manager.get_client(websocket)
|
||||||
|
if not client.eos:
|
||||||
|
client.set_eos(True)
|
||||||
|
time.sleep(0.1) # Sleep 100m; wait some voice activity.
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def cleanup(self, websocket):
|
||||||
|
if self.client_manager.get_client(websocket):
|
||||||
|
self.client_manager.remove_client(websocket)
|
||||||
|
|
||||||
|
|
||||||
class ServeClientBase(object):
|
class ServeClientBase(object):
|
||||||
RATE = 16000
|
RATE = 16000
|
||||||
@@ -254,7 +304,7 @@ class ServeClientBase(object):
|
|||||||
self.text = []
|
self.text = []
|
||||||
self.current_out = ''
|
self.current_out = ''
|
||||||
self.prev_out = ''
|
self.prev_out = ''
|
||||||
self.t_start=None
|
self.t_start = None
|
||||||
self.exit = False
|
self.exit = False
|
||||||
self.same_output_threshold = 0
|
self.same_output_threshold = 0
|
||||||
self.show_prev_out_thresh = 5 # if pause(no output from whisper) show previous output for 5 seconds
|
self.show_prev_out_thresh = 5 # if pause(no output from whisper) show previous output for 5 seconds
|
||||||
@@ -268,7 +318,16 @@ class ServeClientBase(object):
|
|||||||
|
|
||||||
# threading
|
# threading
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def speech_to_text(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def transcribe_audio(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def handle_transcription_output(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def add_frames(self, frame_np):
|
def add_frames(self, frame_np):
|
||||||
"""
|
"""
|
||||||
Add audio frames to the ongoing audio stream buffer.
|
Add audio frames to the ongoing audio stream buffer.
|
||||||
@@ -295,9 +354,50 @@ class ServeClientBase(object):
|
|||||||
self.frames_np = np.concatenate((self.frames_np, frame_np), axis=0)
|
self.frames_np = np.concatenate((self.frames_np, frame_np), axis=0)
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
def speech_to_text(self):
|
def clip_audio_if_no_valid_segment(self):
|
||||||
raise NotImplementedError("Please implement in child Class.")
|
"""
|
||||||
|
Update the timestamp offset based on audio buffer status.
|
||||||
|
Clip audio if the current chunk exceeds 30 seconds, this basically implies that
|
||||||
|
no valid segment for the last 30 seconds from whisper
|
||||||
|
"""
|
||||||
|
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE:
|
||||||
|
duration = self.frames_np.shape[0] / self.RATE
|
||||||
|
self.timestamp_offset = self.frames_offset + duration - 5
|
||||||
|
|
||||||
|
def get_audio_chunk_for_processing(self):
|
||||||
|
"""Retrieve the next chunk of audio data for processing."""
|
||||||
|
samples_take = max(0, (self.timestamp_offset - self.frames_offset) * self.RATE)
|
||||||
|
input_bytes = self.frames_np[int(samples_take):].copy()
|
||||||
|
duration = input_bytes.shape[0] / self.RATE
|
||||||
|
return input_bytes, duration
|
||||||
|
|
||||||
|
def prepare_segments(self, last_segment=None):
|
||||||
|
"""Prepare the segments to be sent to the client."""
|
||||||
|
segments = []
|
||||||
|
if len(self.transcript) >= self.send_last_n_segments:
|
||||||
|
segments = self.transcript[-self.send_last_n_segments:].copy()
|
||||||
|
else:
|
||||||
|
segments = self.transcript.copy()
|
||||||
|
if last_segment is not None:
|
||||||
|
segments = segments + [last_segment]
|
||||||
|
return segments
|
||||||
|
|
||||||
|
def get_audio_chunk_duration(self, input_bytes):
|
||||||
|
"""Calculate the duration of the current audio chunk."""
|
||||||
|
return input_bytes.shape[0] / self.RATE
|
||||||
|
|
||||||
|
def send_transcription_to_client(self, segments):
|
||||||
|
"""Send the transcription segments to the client."""
|
||||||
|
try:
|
||||||
|
self.websocket.send(
|
||||||
|
json.dumps({
|
||||||
|
"uid": self.client_uid,
|
||||||
|
"segments": segments,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[ERROR]: Sending data to client: {e}")
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""
|
"""
|
||||||
Notify the client of disconnection and send a disconnect message.
|
Notify the client of disconnection and send a disconnect message.
|
||||||
@@ -306,15 +406,11 @@ class ServeClientBase(object):
|
|||||||
that the transcription service is disconnecting gracefully.
|
that the transcription service is disconnecting gracefully.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.websocket.send(
|
self.websocket.send(json.dumps({
|
||||||
json.dumps(
|
"uid": self.client_uid,
|
||||||
{
|
"message": self.DISCONNECT
|
||||||
"uid": self.client_uid,
|
}))
|
||||||
"message": self.DISCONNECT
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
Perform cleanup tasks before exiting the transcription service.
|
Perform cleanup tasks before exiting the transcription service.
|
||||||
@@ -357,16 +453,7 @@ class ServeClientTensorRT(ServeClientBase):
|
|||||||
pick_previous_segments (int): Number of previous segments to include in the output.
|
pick_previous_segments (int): Number of previous segments to include in the output.
|
||||||
websocket: The WebSocket connection for the client.
|
websocket: The WebSocket connection for the client.
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(self, websocket, task="transcribe", multilingual=False, language=None, client_uid=None, model=None):
|
||||||
self,
|
|
||||||
websocket,
|
|
||||||
task="transcribe",
|
|
||||||
device=None,
|
|
||||||
multilingual=False,
|
|
||||||
language=None,
|
|
||||||
client_uid=None,
|
|
||||||
model=None
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Initialize a ServeClient instance.
|
Initialize a ServeClient instance.
|
||||||
The Whisper model is initialized based on the client's language and device availability.
|
The Whisper model is initialized based on the client's language and device availability.
|
||||||
@@ -387,8 +474,8 @@ class ServeClientTensorRT(ServeClientBase):
|
|||||||
self.task = task
|
self.task = task
|
||||||
self.eos = False
|
self.eos = False
|
||||||
self.transcriber = WhisperTRTLLM(
|
self.transcriber = WhisperTRTLLM(
|
||||||
model,
|
model,
|
||||||
assets_dir="assets",
|
assets_dir="assets",
|
||||||
device="cuda",
|
device="cuda",
|
||||||
is_multilingual=multilingual,
|
is_multilingual=multilingual,
|
||||||
language=self.language,
|
language=self.language,
|
||||||
@@ -400,52 +487,44 @@ class ServeClientTensorRT(ServeClientBase):
|
|||||||
self.trans_thread = threading.Thread(target=self.speech_to_text)
|
self.trans_thread = threading.Thread(target=self.speech_to_text)
|
||||||
self.trans_thread.start()
|
self.trans_thread.start()
|
||||||
|
|
||||||
self.websocket.send(
|
self.websocket.send(json.dumps({
|
||||||
json.dumps(
|
"uid": self.client_uid,
|
||||||
{
|
"message": self.SERVER_READY,
|
||||||
"uid": self.client_uid,
|
"backend": "tensorrt"
|
||||||
"message": self.SERVER_READY,
|
}))
|
||||||
"backend": "tensorrt"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def warmup(self, warmup_steps=10):
|
def warmup(self, warmup_steps=10):
|
||||||
logging.info("[INFO:] Warming up TensorRT engine..")
|
logging.info("[INFO:] Warming up TensorRT engine..")
|
||||||
mel, _ = self.transcriber.log_mel_spectrogram("tests/jfk.flac")
|
mel, _ = self.transcriber.log_mel_spectrogram("tests/jfk.flac")
|
||||||
for i in range(warmup_steps):
|
for i in range(warmup_steps):
|
||||||
self.transcriber.transcribe(mel)
|
self.transcriber.transcribe(mel)
|
||||||
|
|
||||||
def set_eos(self, eos):
|
def set_eos(self, eos):
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
self.eos = eos
|
self.eos = eos
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
def add_frames(self, frame_np):
|
|
||||||
"""
|
|
||||||
Add audio frames to the ongoing audio stream buffer.
|
|
||||||
|
|
||||||
This method is responsible for maintaining the audio stream buffer, allowing the continuous addition
|
def handle_transcription_output(self, last_segment, duration):
|
||||||
of audio frames as they are received. It also ensures that the buffer does not exceed a specified size
|
"""Handle the transcription output, updating the transcript and sending data to the client."""
|
||||||
to prevent excessive memory usage.
|
segments = self.prepare_segments({"text": last_segment})
|
||||||
|
self.send_transcription_to_client(segments)
|
||||||
|
if self.eos:
|
||||||
|
self.update_timestamp_offset(last_segment, duration)
|
||||||
|
|
||||||
If the buffer size exceeds a threshold (45 seconds of audio data), it discards the oldest 30 seconds
|
def transcribe_audio(self, input_bytes):
|
||||||
of audio data to maintain a reasonable buffer size. If the buffer is empty, it initializes it with the provided
|
"""Transcribe the audio chunk and send the results to the client."""
|
||||||
audio frame. The audio stream buffer is used for real-time processing of audio data for transcription.
|
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {input_bytes.shape[0] / self.RATE}")
|
||||||
|
mel, duration = self.transcriber.log_mel_spectrogram(input_bytes)
|
||||||
|
last_segment = self.transcriber.transcribe(mel)
|
||||||
|
if last_segment:
|
||||||
|
self.handle_transcription_output(last_segment, duration)
|
||||||
|
|
||||||
Args:
|
def update_timestamp_offset(self, last_segment, duration):
|
||||||
frame_np (numpy.ndarray): The audio frame data as a NumPy array.
|
if not len(self.transcript):
|
||||||
|
self.transcript.append({"text": last_segment + " "})
|
||||||
"""
|
elif self.transcript[-1]["text"].strip() != last_segment:
|
||||||
self.lock.acquire()
|
self.transcript.append({"text": last_segment + " "})
|
||||||
if self.frames_np is not None and self.frames_np.shape[0] > 45*self.RATE:
|
self.timestamp_offset += duration
|
||||||
self.frames_offset += 30.0
|
|
||||||
self.frames_np = self.frames_np[int(30*self.RATE):]
|
|
||||||
if self.frames_np is None:
|
|
||||||
self.frames_np = frame_np.copy()
|
|
||||||
else:
|
|
||||||
self.frames_np = np.concatenate((self.frames_np, frame_np), axis=0)
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
def speech_to_text(self):
|
def speech_to_text(self):
|
||||||
"""
|
"""
|
||||||
@@ -456,8 +535,8 @@ class ServeClientTensorRT(ServeClientBase):
|
|||||||
|
|
||||||
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
|
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
|
||||||
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
|
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
|
||||||
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
|
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
|
||||||
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
|
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
|
||||||
there is no speech for a specified duration to indicate a pause.
|
there is no speech for a specified duration to indicate a pause.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -468,54 +547,21 @@ class ServeClientTensorRT(ServeClientBase):
|
|||||||
if self.exit:
|
if self.exit:
|
||||||
logging.info("Exiting speech to text thread")
|
logging.info("Exiting speech to text thread")
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.frames_np is None:
|
if self.frames_np is None:
|
||||||
time.sleep(0.02) # wait for any audio to arrive
|
time.sleep(0.02) # wait for any audio to arrive
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# clip audio if the current chunk exceeds 30 seconds, this basically implies that
|
self.clip_audio_if_no_valid_segment()
|
||||||
# no valid segment for the last 30 seconds from whisper
|
|
||||||
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE:
|
input_bytes, duration = self.get_audio_chunk_for_processing()
|
||||||
duration = self.frames_np.shape[0] / self.RATE
|
if duration < 0.4:
|
||||||
self.timestamp_offset = self.frames_offset + duration - 5
|
|
||||||
|
|
||||||
samples_take = max(0, (self.timestamp_offset - self.frames_offset)*self.RATE)
|
|
||||||
input_bytes = self.frames_np[int(samples_take):].copy()
|
|
||||||
duration = input_bytes.shape[0] / self.RATE
|
|
||||||
if duration<0.4:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
input_sample = input_bytes.copy()
|
input_sample = input_bytes.copy()
|
||||||
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {duration}")
|
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {duration}")
|
||||||
mel, duration = self.transcriber.log_mel_spectrogram(input_sample)
|
self.transcribe_audio(input_sample)
|
||||||
last_segment = self.transcriber.transcribe(mel)
|
|
||||||
segments = []
|
|
||||||
if len(last_segment):
|
|
||||||
if len(self.transcript) < self.send_last_n_segments:
|
|
||||||
segments = self.transcript[:].copy()
|
|
||||||
else:
|
|
||||||
segments = self.transcript[-self.send_last_n_segments:].copy()
|
|
||||||
if last_segment is not None:
|
|
||||||
segments.append({"text": last_segment})
|
|
||||||
try:
|
|
||||||
self.websocket.send(
|
|
||||||
json.dumps({
|
|
||||||
"uid": self.client_uid,
|
|
||||||
"segments": segments,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.eos:
|
|
||||||
if not len(self.transcript):
|
|
||||||
self.transcript.append({"text": last_segment + " "})
|
|
||||||
elif self.transcript[-1]["text"].strip() != last_segment:
|
|
||||||
self.transcript.append({"text": last_segment + " "})
|
|
||||||
self.timestamp_offset += duration
|
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[ERROR]: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[ERROR]: {e}")
|
logging.error(f"[ERROR]: {e}")
|
||||||
@@ -550,17 +596,8 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
pick_previous_segments (int): Number of previous segments to include in the output.
|
pick_previous_segments (int): Number of previous segments to include in the output.
|
||||||
websocket: The WebSocket connection for the client.
|
websocket: The WebSocket connection for the client.
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(self, websocket, task="transcribe", device=None, language=None, client_uid=None, model="small.en",
|
||||||
self,
|
initial_prompt=None, vad_parameters=None):
|
||||||
websocket,
|
|
||||||
task="transcribe",
|
|
||||||
device=None,
|
|
||||||
language=None,
|
|
||||||
client_uid=None,
|
|
||||||
model="small.en",
|
|
||||||
initial_prompt=None,
|
|
||||||
vad_parameters=None,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Initialize a ServeClient instance.
|
Initialize a ServeClient instance.
|
||||||
The Whisper model is initialized based on the client's language and device availability.
|
The Whisper model is initialized based on the client's language and device availability.
|
||||||
@@ -589,16 +626,16 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
self.initial_prompt = initial_prompt
|
self.initial_prompt = initial_prompt
|
||||||
self.vad_parameters = vad_parameters or {"threshold": 0.5}
|
self.vad_parameters = vad_parameters or {"threshold": 0.5}
|
||||||
self.no_speech_thresh = 0.45
|
self.no_speech_thresh = 0.45
|
||||||
|
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
if self.model_size_or_path == None:
|
if self.model_size_or_path is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.transcriber = WhisperModel(
|
self.transcriber = WhisperModel(
|
||||||
self.model_size_or_path,
|
self.model_size_or_path,
|
||||||
device=device,
|
device=device,
|
||||||
compute_type="int8" if device=="cpu" else "float16",
|
compute_type="int8" if device == "cpu" else "float16",
|
||||||
local_files_only=False,
|
local_files_only=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -614,7 +651,7 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_valid_model(self, model_size):
|
def check_valid_model(self, model_size):
|
||||||
"""
|
"""
|
||||||
Check if it's a valid whisper model size.
|
Check if it's a valid whisper model size.
|
||||||
@@ -637,7 +674,39 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return model_size
|
return model_size
|
||||||
|
|
||||||
|
def set_language(self, info):
|
||||||
|
if info.language_probability > 0.5:
|
||||||
|
self.language = info.language
|
||||||
|
logging.info(f"Detected language {self.language} with probability {info.language_probability}")
|
||||||
|
self.websocket.send(json.dumps(
|
||||||
|
{"uid": self.client_uid, "language": self.language, "language_prob": info.language_probability}))
|
||||||
|
|
||||||
|
def transcribe_audio(self, input_sample):
|
||||||
|
result, info = self.transcriber.transcribe(
|
||||||
|
input_sample,
|
||||||
|
initial_prompt=self.initial_prompt,
|
||||||
|
language=self.language,
|
||||||
|
task=self.task,
|
||||||
|
vad_filter=True,
|
||||||
|
vad_parameters=self.vad_parameters)
|
||||||
|
if self.language is None:
|
||||||
|
self.set_language(info)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_previous_output(self):
|
||||||
|
segments = []
|
||||||
|
if self.t_start is None:
|
||||||
|
self.t_start = time.time()
|
||||||
|
if time.time() - self.t_start < self.show_prev_out_thresh:
|
||||||
|
segments = self.prepare_segments()
|
||||||
|
|
||||||
|
# add a blank if there is no speech for 3 seconds
|
||||||
|
if len(self.text) and self.text[-1] != '':
|
||||||
|
if time.time() - self.t_start > self.add_pause_thresh:
|
||||||
|
self.text.append('')
|
||||||
|
return segments
|
||||||
|
|
||||||
def speech_to_text(self):
|
def speech_to_text(self):
|
||||||
"""
|
"""
|
||||||
Process an audio stream in an infinite loop, continuously transcribing the speech.
|
Process an audio stream in an infinite loop, continuously transcribing the speech.
|
||||||
@@ -647,8 +716,8 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
|
|
||||||
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
|
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
|
||||||
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
|
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
|
||||||
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
|
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
|
||||||
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
|
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
|
||||||
there is no speech for a specified duration to indicate a pause.
|
there is no speech for a specified duration to indicate a pause.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -659,83 +728,38 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
if self.exit:
|
if self.exit:
|
||||||
logging.info("Exiting speech to text thread")
|
logging.info("Exiting speech to text thread")
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.frames_np is None:
|
if self.frames_np is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# clip audio if the current chunk exceeds 30 seconds, this basically implies that
|
self.clip_audio_if_no_valid_segment()
|
||||||
# no valid segment for the last 30 seconds from whisper
|
|
||||||
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE:
|
input_bytes, duration = self.get_audio_chunk_for_processing()
|
||||||
duration = self.frames_np.shape[0] / self.RATE
|
if duration < 1.0:
|
||||||
self.timestamp_offset = self.frames_offset + duration - 5
|
|
||||||
|
|
||||||
samples_take = max(0, (self.timestamp_offset - self.frames_offset)*self.RATE)
|
|
||||||
input_bytes = self.frames_np[int(samples_take):].copy()
|
|
||||||
duration = input_bytes.shape[0] / self.RATE
|
|
||||||
if duration<1.0:
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
input_sample = input_bytes.copy()
|
input_sample = input_bytes.copy()
|
||||||
|
result = self.transcribe_audio(input_sample)
|
||||||
# whisper transcribe with prompt
|
|
||||||
result, info = self.transcriber.transcribe(
|
|
||||||
input_sample,
|
|
||||||
initial_prompt=self.initial_prompt,
|
|
||||||
language=self.language,
|
|
||||||
task=self.task,
|
|
||||||
vad_filter=True,
|
|
||||||
vad_parameters=self.vad_parameters
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.language is None:
|
if self.language is None:
|
||||||
if info.language_probability > 0.5:
|
continue
|
||||||
self.language = info.language
|
|
||||||
logging.info(f"Detected language {self.language} with probability {info.language_probability}")
|
|
||||||
self.websocket.send(json.dumps(
|
|
||||||
{"uid": self.client_uid, "language": self.language, "language_prob": info.language_probability}))
|
|
||||||
else:
|
|
||||||
# detect language again
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(result):
|
if len(result):
|
||||||
self.t_start = None
|
self.t_start = None
|
||||||
last_segment = self.update_segments(result, duration)
|
last_segment = self.update_segments(result, duration)
|
||||||
if len(self.transcript) < self.send_last_n_segments:
|
segments = self.prepare_segments(last_segment)
|
||||||
segments = self.transcript
|
|
||||||
else:
|
|
||||||
segments = self.transcript[-self.send_last_n_segments:]
|
|
||||||
if last_segment is not None:
|
|
||||||
segments = segments + [last_segment]
|
|
||||||
else:
|
else:
|
||||||
# show previous output if there is pause i.e. no output from whisper
|
# show previous output if there is pause i.e. no output from whisper
|
||||||
segments = []
|
segments = self.get_previous_output()
|
||||||
if self.t_start is None: self.t_start = time.time()
|
|
||||||
if time.time() - self.t_start < self.show_prev_out_thresh:
|
|
||||||
if len(self.transcript) < self.send_last_n_segments:
|
|
||||||
segments = self.transcript
|
|
||||||
else:
|
|
||||||
segments = self.transcript[-self.send_last_n_segments:]
|
|
||||||
|
|
||||||
# add a blank if there is no speech for 3 seconds
|
|
||||||
if len(self.text) and self.text[-1] != '':
|
|
||||||
if time.time() - self.t_start > self.add_pause_thresh:
|
|
||||||
self.text.append('')
|
|
||||||
|
|
||||||
if not len(segments): continue
|
if not len(segments):
|
||||||
try:
|
continue
|
||||||
self.websocket.send(
|
self.send_transcription_to_client(segments)
|
||||||
json.dumps({
|
|
||||||
"uid": self.client_uid,
|
|
||||||
"segments": segments
|
|
||||||
})
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[ERROR]: Failed to send message to client: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}")
|
logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}")
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
def format_segment(self, start, end, text):
|
def format_segment(self, start, end, text):
|
||||||
"""Helper function to format a segment with string timestamps."""
|
"""Helper function to format a segment with string timestamps."""
|
||||||
return {
|
return {
|
||||||
@@ -750,17 +774,17 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
except for the last segment assuming that it is incomplete.
|
except for the last segment assuming that it is incomplete.
|
||||||
|
|
||||||
Updates the ongoing transcript with transcribed segments, including their start and end times.
|
Updates the ongoing transcript with transcribed segments, including their start and end times.
|
||||||
Complete segments are appended to the transcript in chronological order. Incomplete segments
|
Complete segments are appended to the transcript in chronological order. Incomplete segments
|
||||||
(assumed to be the last one) are processed to identify repeated content. If the same incomplete
|
(assumed to be the last one) are processed to identify repeated content. If the same incomplete
|
||||||
segment is seen multiple times, it updates the offset and appends the segment to the transcript.
|
segment is seen multiple times, it updates the offset and appends the segment to the transcript.
|
||||||
A threshold is used to detect repeated content and ensure it is only included once in the transcript.
|
A threshold is used to detect repeated content and ensure it is only included once in the transcript.
|
||||||
The timestamp offset is updated based on the duration of processed segments. The method returns the
|
The timestamp offset is updated based on the duration of processed segments. The method returns the
|
||||||
last processed segment, allowing it to be sent to the client for real-time updates.
|
last processed segment, allowing it to be sent to the client for real-time updates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
segments(dict) : dictionary of segments as returned by whisper
|
segments(dict) : dictionary of segments as returned by whisper
|
||||||
duration(float): duration of the current chunk
|
duration(float): duration of the current chunk
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict or None: The last processed segment with its start time, end time, and transcribed text.
|
dict or None: The last processed segment with its start time, end time, and transcribed text.
|
||||||
Returns None if there are no valid segments to process.
|
Returns None if there are no valid segments to process.
|
||||||
@@ -775,11 +799,12 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
self.text.append(text_)
|
self.text.append(text_)
|
||||||
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end)
|
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end)
|
||||||
|
|
||||||
if start >= end: continue
|
if start >= end:
|
||||||
if s.no_speech_prob > self.no_speech_thresh: continue
|
continue
|
||||||
|
if s.no_speech_prob > self.no_speech_thresh:
|
||||||
|
continue
|
||||||
|
|
||||||
self.transcript.append(self.format_segment(start, end, text_))
|
self.transcript.append(self.format_segment(start, end, text_))
|
||||||
|
|
||||||
offset = min(duration, s.end)
|
offset = min(duration, s.end)
|
||||||
|
|
||||||
self.current_out += segments[-1].text
|
self.current_out += segments[-1].text
|
||||||
@@ -788,16 +813,16 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
self.timestamp_offset + min(duration, segments[-1].end),
|
self.timestamp_offset + min(duration, segments[-1].end),
|
||||||
self.current_out
|
self.current_out
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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.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_threshold += 1
|
self.same_output_threshold += 1
|
||||||
else:
|
else:
|
||||||
self.same_output_threshold = 0
|
self.same_output_threshold = 0
|
||||||
|
|
||||||
if self.same_output_threshold > 5:
|
if self.same_output_threshold > 5:
|
||||||
if not len(self.text) or self.text[-1].strip().lower()!=self.current_out.strip().lower():
|
if not len(self.text) or self.text[-1].strip().lower() != self.current_out.strip().lower():
|
||||||
self.text.append(self.current_out)
|
self.text.append(self.current_out)
|
||||||
self.transcript.append(self.format_segment(
|
self.transcript.append(self.format_segment(
|
||||||
self.timestamp_offset,
|
self.timestamp_offset,
|
||||||
@@ -810,7 +835,7 @@ class ServeClientFasterWhisper(ServeClientBase):
|
|||||||
last_segment = None
|
last_segment = None
|
||||||
else:
|
else:
|
||||||
self.prev_out = self.current_out
|
self.prev_out = self.current_out
|
||||||
|
|
||||||
# update offset
|
# update offset
|
||||||
if offset is not None:
|
if offset is not None:
|
||||||
self.timestamp_offset += offset
|
self.timestamp_offset += offset
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ def store_transcripts(filename: Pathlike, texts: Iterable[Tuple[str, str,
|
|||||||
print(f"{cut_id}:\thyp={hyp}", file=f)
|
print(f"{cut_id}:\thyp={hyp}", file=f)
|
||||||
|
|
||||||
|
|
||||||
def write_error_stats(
|
def write_error_stats( # noqa: C901
|
||||||
f: TextIO,
|
f: TextIO,
|
||||||
test_set_name: str,
|
test_set_name: str,
|
||||||
results: List[Tuple[str, str]],
|
results: List[Tuple[str, str]],
|
||||||
@@ -362,4 +362,4 @@ def write_error_stats(
|
|||||||
hyp_count = corr + hyp_sub + ins
|
hyp_count = corr + hyp_sub + ins
|
||||||
|
|
||||||
print(f"{word} {corr} {tot_errs} {ref_count} {hyp_count}", file=f)
|
print(f"{word} {corr} {tot_errs} {ref_count} {hyp_count}", file=f)
|
||||||
return float(tot_err_rate)
|
return float(tot_err_rate)
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ class WhisperModel:
|
|||||||
|
|
||||||
return segments, info
|
return segments, info
|
||||||
|
|
||||||
def generate_segments(
|
def generate_segments( # noqa: C901
|
||||||
self,
|
self,
|
||||||
features: np.ndarray,
|
features: np.ndarray,
|
||||||
tokenizer: Tokenizer,
|
tokenizer: Tokenizer,
|
||||||
@@ -425,7 +425,7 @@ class WhisperModel:
|
|||||||
all_segments = []
|
all_segments = []
|
||||||
while seek < content_frames:
|
while seek < content_frames:
|
||||||
time_offset = seek * self.feature_extractor.time_per_frame
|
time_offset = seek * self.feature_extractor.time_per_frame
|
||||||
segment = features[:, seek : seek + self.feature_extractor.nb_max_frames]
|
segment = features[:, seek:seek + self.feature_extractor.nb_max_frames]
|
||||||
segment_size = min(
|
segment_size = min(
|
||||||
self.feature_extractor.nb_max_frames, content_frames - seek
|
self.feature_extractor.nb_max_frames, content_frames - seek
|
||||||
)
|
)
|
||||||
@@ -749,7 +749,7 @@ class WhisperModel:
|
|||||||
|
|
||||||
if previous_tokens:
|
if previous_tokens:
|
||||||
prompt.append(tokenizer.sot_prev)
|
prompt.append(tokenizer.sot_prev)
|
||||||
prompt.extend(previous_tokens[-(self.max_length // 2 - 1) :])
|
prompt.extend(previous_tokens[-(self.max_length // 2 - 1):])
|
||||||
|
|
||||||
prompt.extend(tokenizer.sot_sequence)
|
prompt.extend(tokenizer.sot_sequence)
|
||||||
|
|
||||||
@@ -766,7 +766,7 @@ class WhisperModel:
|
|||||||
|
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
def add_word_timestamps(
|
def add_word_timestamps( # noqa: C901
|
||||||
self,
|
self,
|
||||||
segments: List[dict],
|
segments: List[dict],
|
||||||
tokenizer: Tokenizer,
|
tokenizer: Tokenizer,
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import argparse
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, Optional, TextIO, Tuple, Union
|
from typing import Union
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import torch.nn.functional as F
|
||||||
from whisper.tokenizer import get_tokenizer
|
from whisper.tokenizer import get_tokenizer
|
||||||
from whisper_live.tensorrt_utils import (mel_filters, store_transcripts,
|
from whisper_live.tensorrt_utils import (mel_filters, load_audio_wav_format, pad_or_trim, load_audio)
|
||||||
write_error_stats, load_audio_wav_format,
|
|
||||||
pad_or_trim, load_audio)
|
|
||||||
|
|
||||||
import tensorrt_llm
|
import tensorrt_llm
|
||||||
import tensorrt_llm.logger as logger
|
import tensorrt_llm.logger as logger
|
||||||
@@ -38,8 +35,6 @@ class WhisperEncoding:
|
|||||||
with open(config_path, 'r') as f:
|
with open(config_path, 'r') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
use_gpt_attention_plugin = config['plugin_config'][
|
|
||||||
'gpt_attention_plugin']
|
|
||||||
dtype = config['builder_config']['precision']
|
dtype = config['builder_config']['precision']
|
||||||
n_mels = config['builder_config']['n_mels']
|
n_mels = config['builder_config']['n_mels']
|
||||||
num_languages = config['builder_config']['num_languages']
|
num_languages = config['builder_config']['num_languages']
|
||||||
@@ -176,16 +171,8 @@ class WhisperDecoding:
|
|||||||
|
|
||||||
class WhisperTRTLLM(object):
|
class WhisperTRTLLM(object):
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, engine_dir, assets_dir=None, device=None, is_multilingual=False,
|
||||||
self,
|
language="en", task="transcribe"):
|
||||||
engine_dir,
|
|
||||||
debug_mode=False,
|
|
||||||
assets_dir=None,
|
|
||||||
device=None,
|
|
||||||
is_multilingual=False,
|
|
||||||
language="en",
|
|
||||||
task="transcribe"
|
|
||||||
):
|
|
||||||
world_size = 1
|
world_size = 1
|
||||||
runtime_rank = tensorrt_llm.mpi_rank()
|
runtime_rank = tensorrt_llm.mpi_rank()
|
||||||
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
|
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
|
||||||
@@ -212,7 +199,7 @@ class WhisperTRTLLM(object):
|
|||||||
self,
|
self,
|
||||||
audio: Union[str, np.ndarray, torch.Tensor],
|
audio: Union[str, np.ndarray, torch.Tensor],
|
||||||
padding: int = 0,
|
padding: int = 0,
|
||||||
return_duration = True
|
return_duration=True
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Compute the log-Mel spectrogram of
|
Compute the log-Mel spectrogram of
|
||||||
@@ -242,8 +229,7 @@ class WhisperTRTLLM(object):
|
|||||||
audio, _ = load_audio_wav_format(audio)
|
audio, _ = load_audio_wav_format(audio)
|
||||||
else:
|
else:
|
||||||
audio = load_audio(audio)
|
audio = load_audio(audio)
|
||||||
assert isinstance(audio,
|
assert isinstance(audio, np.ndarray), f"Unsupported audio type: {type(audio)}"
|
||||||
np.ndarray), f"Unsupported audio type: {type(audio)}"
|
|
||||||
duration = audio.shape[-1] / SAMPLE_RATE
|
duration = audio.shape[-1] / SAMPLE_RATE
|
||||||
audio = pad_or_trim(audio, N_SAMPLES)
|
audio = pad_or_trim(audio, N_SAMPLES)
|
||||||
audio = audio.astype(np.float32)
|
audio = audio.astype(np.float32)
|
||||||
@@ -254,14 +240,9 @@ class WhisperTRTLLM(object):
|
|||||||
if padding > 0:
|
if padding > 0:
|
||||||
audio = F.pad(audio, (0, padding))
|
audio = F.pad(audio, (0, padding))
|
||||||
window = torch.hann_window(N_FFT).to(audio.device)
|
window = torch.hann_window(N_FFT).to(audio.device)
|
||||||
stft = torch.stft(audio,
|
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
|
||||||
N_FFT,
|
|
||||||
HOP_LENGTH,
|
|
||||||
window=window,
|
|
||||||
return_complex=True)
|
|
||||||
magnitudes = stft[..., :-1].abs()**2
|
magnitudes = stft[..., :-1].abs()**2
|
||||||
|
|
||||||
|
|
||||||
mel_spec = self.filters @ magnitudes
|
mel_spec = self.filters @ magnitudes
|
||||||
|
|
||||||
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
|
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
|
||||||
@@ -272,7 +253,6 @@ class WhisperTRTLLM(object):
|
|||||||
else:
|
else:
|
||||||
return log_spec
|
return log_spec
|
||||||
|
|
||||||
|
|
||||||
def process_batch(
|
def process_batch(
|
||||||
self,
|
self,
|
||||||
mel,
|
mel,
|
||||||
@@ -296,7 +276,7 @@ class WhisperTRTLLM(object):
|
|||||||
text = self.tokenizer.decode(output_ids[i][0]).strip()
|
text = self.tokenizer.decode(output_ids[i][0]).strip()
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
return texts
|
return texts
|
||||||
|
|
||||||
def transcribe(
|
def transcribe(
|
||||||
self,
|
self,
|
||||||
mel,
|
mel,
|
||||||
@@ -336,5 +316,5 @@ def decode_wav_file(
|
|||||||
prediction = re.sub(r'<\|.*?\|>', '', prediction)
|
prediction = re.sub(r'<\|.*?\|>', '', prediction)
|
||||||
if normalizer:
|
if normalizer:
|
||||||
prediction = normalizer(prediction)
|
prediction = normalizer(prediction)
|
||||||
|
|
||||||
return prediction.strip()
|
return prediction.strip()
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
import scipy
|
||||||
|
import ffmpeg
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
"""Clears the console screen."""
|
||||||
|
os.system("cls" if os.name == "nt" else "clear")
|
||||||
|
|
||||||
|
|
||||||
|
def print_transcript(text):
|
||||||
|
"""Prints formatted transcript text."""
|
||||||
|
wrapper = textwrap.TextWrapper(width=60)
|
||||||
|
for line in wrapper.wrap(text="".join(text)):
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
def format_time(s):
|
||||||
|
"""Convert seconds (float) to SRT time format."""
|
||||||
|
hours = int(s // 3600)
|
||||||
|
minutes = int((s % 3600) // 60)
|
||||||
|
seconds = int(s % 60)
|
||||||
|
milliseconds = int((s - int(s)) * 1000)
|
||||||
|
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
|
||||||
|
|
||||||
|
|
||||||
|
def create_srt_file(segments, output_file):
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as srt_file:
|
||||||
|
segment_number = 1
|
||||||
|
for segment in segments:
|
||||||
|
start_time = format_time(float(segment['start']))
|
||||||
|
end_time = format_time(float(segment['end']))
|
||||||
|
text = segment['text']
|
||||||
|
|
||||||
|
srt_file.write(f"{segment_number}\n")
|
||||||
|
srt_file.write(f"{start_time} --> {end_time}\n")
|
||||||
|
srt_file.write(f"{text}\n\n")
|
||||||
|
|
||||||
|
segment_number += 1
|
||||||
|
|
||||||
|
|
||||||
|
def resample(file: str, sr: int = 16000):
|
||||||
|
"""
|
||||||
|
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
|
||||||
|
Open an audio file and read as mono waveform, resampling as necessary,
|
||||||
|
save the resampled audio
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file (str): The audio file to open
|
||||||
|
sr (int): The sample rate to resample the audio if necessary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
resampled_file (str): The resampled audio file
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
||||||
|
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
||||||
|
out, _ = (
|
||||||
|
ffmpeg.input(file, threads=0)
|
||||||
|
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
|
||||||
|
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
||||||
|
)
|
||||||
|
except ffmpeg.Error as e:
|
||||||
|
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
|
||||||
|
np_buffer = np.frombuffer(out, dtype=np.int16)
|
||||||
|
|
||||||
|
resampled_file = f"{file.split('.')[0]}_resampled.wav"
|
||||||
|
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
|
||||||
|
return resampled_file
|
||||||
+2
-2
@@ -34,7 +34,7 @@ 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:
|
||||||
@@ -110,4 +110,4 @@ class VoiceActivityDetection():
|
|||||||
subprocess.run(["wget", "-O", model_filename, model_url], check=True)
|
subprocess.run(["wget", "-O", model_filename, model_url], check=True)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
print("Failed to download the model using wget.")
|
print("Failed to download the model using wget.")
|
||||||
return model_filename
|
return model_filename
|
||||||
|
|||||||
Reference in New Issue
Block a user