Merge pull request #146 from makaveli10/code_formatting

Code formatting
This commit is contained in:
makaveli
2024-02-20 11:32:27 +05:30
committed by GitHub
15 changed files with 877 additions and 658 deletions
+32 -7
View File
@@ -11,12 +11,11 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
test: run-tests:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
timeout-minutes: 60
strategy: strategy:
matrix: matrix:
python-version: [3.8, 3.9, '3.10', '3.11'] python-version: [3.8, 3.9, '3.10', 3.11]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -49,9 +48,35 @@ jobs:
echo "Running tests with Python ${{ matrix.python-version }}" echo "Running tests with Python ${{ matrix.python-version }}"
python -m unittest discover -s tests python -m unittest discover -s tests
build-and-push: check-code-format:
needs: test runs-on: ubuntu-22.04
runs-on: ubuntu-latest strategy:
matrix:
python-version: [3.8, 3.9, '3.10', 3.11]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
publish-to-pypi:
needs: [run-tests, check-code-format]
runs-on: ubuntu-22.04
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+34 -32
View File
@@ -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"
) )
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -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):
+46 -14
View File
@@ -6,6 +6,8 @@ from unittest import mock
import numpy as np import numpy as np
import evaluate import evaluate
from websockets.exceptions import ConnectionClosed
from whisper_live.server import TranscriptionServer from whisper_live.server import TranscriptionServer
from whisper_live.client import TranscriptionClient from whisper_live.client import TranscriptionClient
from whisper.normalizers import EnglishTextNormalizer from whisper.normalizers import EnglishTextNormalizer
@@ -14,26 +16,25 @@ 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):
@@ -50,7 +51,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({
@@ -63,7 +63,7 @@ class TestServerConnection(unittest.TestCase):
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):
@@ -84,7 +84,7 @@ 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",
) )
@@ -93,7 +93,7 @@ class TestServerInferenceAccuracy(unittest.TestCase):
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)
@@ -103,3 +103,35 @@ class TestServerInferenceAccuracy(unittest.TestCase):
references=[gt_normalized] references=[gt_normalized]
) )
self.assertLess(wer, 0.05) self.assertLess(wer, 0.05)
class TestExceptionHandling(unittest.TestCase):
def setUp(self):
self.server = TranscriptionServer()
@mock.patch('websockets.WebSocketCommonProtocol')
def test_connection_closed_exception(self, mock_websocket):
mock_websocket.recv.side_effect = ConnectionClosed(1001, "testing connection closed")
with self.assertLogs(level="INFO") as log:
self.server.recv_audio(mock_websocket, "faster_whisper")
self.assertTrue(any("Connection closed by client" in message for message in log.output))
@mock.patch('websockets.WebSocketCommonProtocol')
def test_json_decode_exception(self, mock_websocket):
mock_websocket.recv.return_value = "invalid json"
with self.assertLogs(level="ERROR") as log:
self.server.recv_audio(mock_websocket, "faster_whisper")
self.assertTrue(any("Failed to decode JSON from client" in message for message in log.output))
@mock.patch('websockets.WebSocketCommonProtocol')
def test_unexpected_exception_handling(self, mock_websocket):
mock_websocket.recv.side_effect = RuntimeError("Unexpected error")
with self.assertLogs(level="ERROR") as log:
self.server.recv_audio(mock_websocket, "faster_whisper")
for message in log.output:
print(message)
print()
self.assertTrue(any("Unexpected error: Unexpected error" in message for message in log.output))
+7 -9
View File
@@ -1,14 +1,12 @@
import unittest import unittest
import numpy as np import numpy as np
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 VoiceActivityDetector
class TestVoiceActivityDetection(unittest.TestCase): class TestVoiceActivityDetection(unittest.TestCase):
def setUp(self): def setUp(self):
self.vad = VoiceActivityDetection() self.vad = VoiceActivityDetector()
self.sample_rate = 16000 self.sample_rate = 16000
def generate_silence(self, duration_seconds): def generate_silence(self, duration_seconds):
@@ -19,10 +17,10 @@ class TestVoiceActivityDetection(unittest.TestCase):
def test_vad_silence_detection(self): def test_vad_silence_detection(self):
silence = self.generate_silence(3) silence = self.generate_silence(3)
speech_prob = self.vad(torch.from_numpy(silence.copy()), self.sample_rate).item() is_speech_present = self.vad(silence.copy())
self.assertLess(speech_prob, 0.5, "VAD incorrectly identified silence as speech.") self.assertFalse(is_speech_present, "VAD incorrectly identified silence as speech.")
def test_vad_speech_detection(self): def test_vad_speech_detection(self):
audio_tensor = torch.from_numpy(load_audio("assets/jfk.flac")) audio_tensor = load_audio("assets/jfk.flac")
speech_prob = self.vad(audio_tensor, self.sample_rate).item() is_speech_present = self.vad(audio_tensor)
self.assertGreater(speech_prob, 0.5, "VAD failed to identify speech segment.") self.assertTrue(is_speech_present, "VAD failed to identify speech segment.")
+1 -1
View File
@@ -1 +1 @@
__version__="0.1.0" __version__ = "0.1.0"
+42 -109
View File
@@ -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,6 +96,36 @@ 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.
@@ -171,18 +147,11 @@ 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":
print("[INFO]: Server overtime disconnected.") print("[INFO]: Server disconnected due to overtime.")
self.recording = False self.recording = False
if "message" in message.keys() and message["message"] == "SERVER_READY": if "message" in message.keys() and message["message"] == "SERVER_READY":
@@ -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}")
@@ -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.
@@ -448,7 +386,8 @@ class Client:
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)
@@ -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):
@@ -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()
+523 -375
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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]],
+4 -4
View File
@@ -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,
+8 -28
View File
@@ -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,
+71
View File
@@ -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
+30 -1
View File
@@ -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:
@@ -111,3 +111,32 @@ class VoiceActivityDetection():
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
class VoiceActivityDetector:
def __init__(self, threshold=0.5, frame_rate=16000):
"""
Initializes the VoiceActivityDetector with a voice activity detection model and a threshold.
Args:
threshold (float, optional): The probability threshold for detecting voice activity. Defaults to 0.5.
"""
self.model = VoiceActivityDetection()
self.threshold = threshold
self.frame_rate = frame_rate
def __call__(self, audio_frame):
"""
Determines if the given audio frame contains speech by comparing the detected speech probability against
the threshold.
Args:
audio_frame (np.ndarray): The audio frame to be analyzed for voice activity. It is expected to be a
NumPy array of audio samples.
Returns:
bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity;
False otherwise.
"""
speech_prob = self.model(torch.from_numpy(audio_frame), self.frame_rate).item()
return speech_prob > self.threshold