Merge pull request #216 from makaveli10/feature/writing_audio_frames_optional

Make writing audio frames optional
This commit is contained in:
makaveli
2024-05-29 09:10:43 +05:30
committed by GitHub
3 changed files with 92 additions and 41 deletions
+11 -3
View File
@@ -62,7 +62,13 @@ python3 run_server.py --port 9090 \
``` ```
### Running the Client ### Running the Client
- Initializing the client: - Initializing the client with below parameters:
- `lang`: Language of the input audio, applicable only if using a multilingual model.
- `translate`: If set to `True` then translate from any language to `en`.
- `model`: Whisper model size.
- `use_vad`: Whether to use `Voice Activity Detecion` on the server.
- `save_output_recording`: Set to True to save the microphone input as a `.wav` file during live transcription. This option is helpful for recording sessions for later playback or analysis. Defaults to `False`.
- `output_recording_filename`: Specifies the `.wav` file path where the microphone input will be saved if `save_output_recording` is set to `True`.
```python ```python
from whisper_live.client import TranscriptionClient from whisper_live.client import TranscriptionClient
client = TranscriptionClient( client = TranscriptionClient(
@@ -72,11 +78,13 @@ client = TranscriptionClient(
translate=False, translate=False,
model="small", model="small",
use_vad=False, use_vad=False,
save_output_recording=True, # Only used for microphone input, False by Default
output_recording_filename="./output_recording.wav" # Only used for microphone input
) )
``` ```
It connects to the server running on localhost at port 9090. Using a multilingual model, language for the transcription will be automatically detected. You can also use the language option to specify the target language for the transcription, in this case, English ("en"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language. It connects to the server running on localhost at port 9090. Using a multilingual model, language for the transcription will be automatically detected. You can also use the language option to specify the target language for the transcription, in this case, English ("en"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language.
- Trancribe an audio file: - Transcribe an audio file:
```python ```python
client("tests/jfk.wav") client("tests/jfk.wav")
``` ```
@@ -86,7 +94,7 @@ client("tests/jfk.wav")
client() client()
``` ```
- TO transcribe from a RTSP stream: - To transcribe from a RTSP stream:
```python ```python
client(rtsp_url="rtsp://admin:admin@192.168.0.1/rtsp") client(rtsp_url="rtsp://admin:admin@192.168.0.1/rtsp")
``` ```
+77 -34
View File
@@ -1,4 +1,5 @@
import os import os
import shutil
import wave import wave
import numpy as np import numpy as np
@@ -259,6 +260,7 @@ class Client:
while time.time() - self.last_response_received < self.disconnect_if_no_response_for: while time.time() - self.last_response_received < self.disconnect_if_no_response_for:
continue continue
class TranscriptionTeeClient: class TranscriptionTeeClient:
""" """
Client for handling audio recording, streaming, and transcription tasks via one or more Client for handling audio recording, streaming, and transcription tasks via one or more
@@ -272,7 +274,7 @@ class TranscriptionTeeClient:
Attributes: Attributes:
clients (list): the underlying Client instances responsible for handling WebSocket connections. clients (list): the underlying Client instances responsible for handling WebSocket connections.
""" """
def __init__(self, clients): def __init__(self, clients, save_output_recording=False, output_recording_filename="./output_recording.wav"):
self.clients = clients self.clients = clients
if not self.clients: if not self.clients:
raise Exception("At least one client is required.") raise Exception("At least one client is required.")
@@ -281,6 +283,8 @@ class TranscriptionTeeClient:
self.channels = 1 self.channels = 1
self.rate = 16000 self.rate = 16000
self.record_seconds = 60000 self.record_seconds = 60000
self.save_output_recording = save_output_recording
self.output_recording_filename = output_recording_filename
self.frames = b"" self.frames = b""
self.p = pyaudio.PyAudio() self.p = pyaudio.PyAudio()
try: try:
@@ -473,7 +477,43 @@ class TranscriptionTeeClient:
return process return process
def record(self, out_file="output_recording.wav"): def save_chunk(self, n_audio_file):
"""
Saves the current audio frames to a WAV file in a separate thread.
Args:
n_audio_file (int): The index of the audio file which determines the filename.
This helps in maintaining the order and uniqueness of each chunk.
"""
t = threading.Thread(
target=self.write_audio_frames_to_file,
args=(self.frames[:], f"chunks/{n_audio_file}.wav",),
)
t.start()
def finalize_recording(self, n_audio_file):
"""
Finalizes the recording process by saving any remaining audio frames,
closing the audio stream, and terminating the process.
Args:
n_audio_file (int): The file index to be used if there are remaining audio frames to be saved.
This index is incremented before use if the last chunk is saved.
"""
if self.save_output_recording and len(self.frames):
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_all_clients()
if self.save_output_recording:
self.write_output_recording(n_audio_file)
self.write_all_clients_srt()
def record(self):
""" """
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.
@@ -485,15 +525,12 @@ class TranscriptionTeeClient:
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:
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
if not os.path.exists("chunks"): if self.save_output_recording:
os.makedirs("chunks", exist_ok=True) if os.path.exists("chunks"):
shutil.rmtree("chunks")
os.makedirs("chunks")
try: try:
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 any(client.recording for client in self.clients): if not any(client.recording for client in self.clients):
@@ -507,31 +544,14 @@ class TranscriptionTeeClient:
# save frames if more than a minute # save frames if more than a minute
if len(self.frames) > 60 * self.rate: if len(self.frames) > 60 * self.rate:
t = threading.Thread( if self.save_output_recording:
target=self.write_audio_frames_to_file, self.save_chunk(n_audio_file)
args=( n_audio_file += 1
self.frames[:],
f"chunks/{n_audio_file}.wav",
),
)
t.start()
n_audio_file += 1
self.frames = b"" self.frames = b""
self.write_all_clients_srt() self.write_all_clients_srt()
except KeyboardInterrupt: except KeyboardInterrupt:
if len(self.frames): self.finalize_recording(n_audio_file)
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_all_clients()
self.write_output_recording(n_audio_file, out_file)
self.write_all_clients_srt()
def write_audio_frames_to_file(self, frames, file_name): def write_audio_frames_to_file(self, frames, file_name):
""" """
@@ -552,7 +572,7 @@ class TranscriptionTeeClient:
wavfile.setframerate(self.rate) wavfile.setframerate(self.rate)
wavfile.writeframes(frames) wavfile.writeframes(frames)
def write_output_recording(self, n_audio_file, out_file): def write_output_recording(self, n_audio_file):
""" """
Combine and save recorded audio chunks into a single WAV file. Combine and save recorded audio chunks into a single WAV file.
@@ -571,7 +591,7 @@ class TranscriptionTeeClient:
for i in range(n_audio_file) for i in range(n_audio_file)
if os.path.exists(f"chunks/{i}.wav") if os.path.exists(f"chunks/{i}.wav")
] ]
with wave.open(out_file, "wb") as wavfile: with wave.open(self.output_recording_filename, "wb") as wavfile:
wavfile: wave.Wave_write wavfile: wave.Wave_write
wavfile.setnchannels(self.channels) wavfile.setnchannels(self.channels)
wavfile.setsampwidth(2) wavfile.setsampwidth(2)
@@ -586,6 +606,9 @@ class TranscriptionTeeClient:
# remove this file # remove this file
os.remove(in_file) os.remove(in_file)
wavfile.close() wavfile.close()
# clean up temporary directory to store chunks
if os.path.exists("chunks"):
shutil.rmtree("chunks")
@staticmethod @staticmethod
def bytes_to_float_array(audio_bytes): def bytes_to_float_array(audio_bytes):
@@ -604,6 +627,7 @@ class TranscriptionTeeClient:
raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16) raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16)
return raw_data.astype(np.float32) / 32768.0 return raw_data.astype(np.float32) / 32768.0
class TranscriptionClient(TranscriptionTeeClient): class TranscriptionClient(TranscriptionTeeClient):
""" """
Client for handling audio transcription tasks via a single WebSocket connection. Client for handling audio transcription tasks via a single WebSocket connection.
@@ -616,6 +640,8 @@ class TranscriptionClient(TranscriptionTeeClient):
port (int): The port number to connect to on the server. port (int): The port number to connect to on the server.
lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en'). lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en').
translate (bool, optional): Indicates whether translation tasks are required (default is False). translate (bool, optional): Indicates whether translation tasks are required (default is False).
save_output_recording (bool, optional): Indicates whether to save recording from microphone.
output_recording_filename (str, optional): File to save the output recording.
Attributes: Attributes:
client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection. client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection.
@@ -627,6 +653,23 @@ class TranscriptionClient(TranscriptionTeeClient):
transcription_client() transcription_client()
``` ```
""" """
def __init__(self, host, port, lang=None, translate=False, model="small", use_vad=True): def __init__(
self,
host,
port,
lang=None,
translate=False,
model="small",
use_vad=True,
save_output_recording=False,
output_recording_filename="./output_recording.wav"
):
self.client = Client(host, port, lang, translate, model, srt_file_path="output.srt", use_vad=use_vad) self.client = Client(host, port, lang, translate, model, srt_file_path="output.srt", use_vad=use_vad)
TranscriptionTeeClient.__init__(self, [self.client]) if save_output_recording and not output_recording_filename.endswith(".wav"):
raise ValueError(f"Please provide a valid `output_recording_filename`: {output_recording_filename}")
TranscriptionTeeClient.__init__(
self,
[self.client],
save_output_recording=save_output_recording,
output_recording_filename=output_recording_filename
)
+4 -4
View File
@@ -496,7 +496,7 @@ class WhisperModel:
content_frames - seek, content_frames - seek,
seek_clip_end - seek, seek_clip_end - seek,
) )
segment = features[:, seek : seek + segment_size] segment = features[:, seek:seek + segment_size]
segment_duration = segment_size * self.feature_extractor.time_per_frame segment_duration = segment_size * self.feature_extractor.time_per_frame
segment = pad_or_trim(segment, self.feature_extractor.nb_max_frames) segment = pad_or_trim(segment, self.feature_extractor.nb_max_frames)
@@ -685,7 +685,7 @@ class WhisperModel:
continue continue
if is_segment_anomaly(segment): if is_segment_anomaly(segment):
next_segment = next_words_segment( next_segment = next_words_segment(
current_segments[si + 1 :] current_segments[si + 1:]
) )
if next_segment is not None: if next_segment is not None:
hal_next_start = next_segment["words"][0]["start"] hal_next_start = next_segment["words"][0]["start"]
@@ -909,7 +909,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)
@@ -926,7 +926,7 @@ class WhisperModel:
return prompt return prompt
def add_word_timestamps( # noqa: C901 def add_word_timestamps(
self, self,
segments: List[dict], segments: List[dict],
tokenizer: Tokenizer, tokenizer: Tokenizer,