Merge pull request #64 from makaveli10/update_faster_whisper

Update faster whisper.
This commit is contained in:
Marcus Edel
2023-11-20 10:26:42 -05:00
committed by GitHub
5 changed files with 273 additions and 278 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PyAudio PyAudio
faster-whisper==0.6.0 faster-whisper==0.9.0
--extra-index-url https://download.pytorch.org/whl/cu111 --extra-index-url https://download.pytorch.org/whl/cu111
torch==1.10.1 torch==1.10.1
torchaudio==0.10.1 torchaudio==0.10.1
+1 -1
View File
@@ -1 +1 @@
__version__="0.0.7" __version__="0.0.8"
+16 -49
View File
@@ -13,7 +13,6 @@ import torch
import numpy as np import numpy as np
import time import time
from whisper_live.transcriber import WhisperModel from whisper_live.transcriber import WhisperModel
from whisper_live.vad import VoiceActivityDetection
class TranscriptionServer: class TranscriptionServer:
@@ -35,8 +34,6 @@ class TranscriptionServer:
def __init__(self): def __init__(self):
# voice activity detection model # voice activity detection model
self.vad_model = VoiceActivityDetection()
self.vad_threshold = 0.4
self.clients = {} self.clients = {}
self.websockets = {} self.websockets = {}
@@ -115,15 +112,6 @@ class TranscriptionServer:
frame_data = websocket.recv() frame_data = websocket.recv()
frame_np = np.frombuffer(frame_data, dtype=np.float32) frame_np = np.frombuffer(frame_data, dtype=np.float32)
try:
speech_prob = self.vad_model(torch.from_numpy(frame_np.copy()), self.RATE).item()
if speech_prob < self.vad_threshold:
continue
except Exception as e:
logging.error(e)
return
self.clients[websocket].add_frames(frame_np) self.clients[websocket].add_frames(frame_np)
elapsed_time = time.time() - self.clients_start_time[websocket] elapsed_time = time.time() - self.clients_start_time[websocket]
@@ -322,25 +310,6 @@ class ServeClient:
Exception: If there is an issue with audio processing or WebSocket communication. Exception: If there is an issue with audio processing or WebSocket communication.
""" """
# detect language
if self.language is None:
# wait for 30s of audio
while self.frames_np is None or self.frames_np.shape[0] < 30*self.RATE:
time.sleep(1)
input_bytes = self.frames_np[-30*self.RATE:].copy()
self.frames_np = None
duration = input_bytes.shape[0] / self.RATE
self.language, lang_prob = self.transcriber.transcribe(
input_bytes,
initial_prompt=None,
language=self.language,
task=self.task
)
logging.info(f"Detected language {self.language} with probability {lang_prob}")
self.websocket.send(json.dumps(
{"uid": self.client_uid, "language": self.language, "language_prob": lang_prob}))
while True: while True:
if self.exit: if self.exit:
logging.info("Exiting speech to text thread") logging.info("Exiting speech to text thread")
@@ -362,20 +331,27 @@ class ServeClient:
continue continue
try: try:
input_sample = input_bytes.copy() input_sample = input_bytes.copy()
# set previous complete segment as initial prompt
if len(self.text) and self.text[-1] != '':
initial_prompt = self.text[-1]
else:
initial_prompt = None
# whisper transcribe with prompt # whisper transcribe with prompt
result = self.transcriber.transcribe( result, info = self.transcriber.transcribe(
input_sample, input_sample,
initial_prompt=initial_prompt, initial_prompt=None,
language=self.language, language=self.language,
task=self.task task=self.task,
vad_filter=True,
vad_parameters={"threshold": 0.5}
) )
if self.language is None:
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}))
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)
@@ -385,16 +361,6 @@ class ServeClient:
segments = self.transcript[-self.send_last_n_segments:] segments = self.transcript[-self.send_last_n_segments:]
if last_segment is not None: if last_segment is not None:
segments = segments + [last_segment] segments = segments + [last_segment]
try:
self.websocket.send(
json.dumps({
"uid": self.client_uid,
"segments": segments
})
)
except Exception as e:
logging.error(f"[ERROR]: {e}")
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 = []
@@ -419,6 +385,7 @@ class ServeClient:
) )
except Exception as e: except Exception as e:
logging.error(f"[ERROR]: {e}") logging.error(f"[ERROR]: {e}")
except Exception as e: except Exception as e:
logging.error(f"[ERROR]: {e}") logging.error(f"[ERROR]: {e}")
time.sleep(0.01) time.sleep(0.01)
+231 -88
View File
@@ -4,7 +4,6 @@ import itertools
import logging import logging
import os import os
import zlib import zlib
import logging
from typing import BinaryIO, Iterable, List, NamedTuple, Optional, Tuple, Union from typing import BinaryIO, Iterable, List, NamedTuple, Optional, Tuple, Union
@@ -14,21 +13,16 @@ import tokenizers
from faster_whisper.audio import decode_audio from faster_whisper.audio import decode_audio
from faster_whisper.feature_extractor import FeatureExtractor from faster_whisper.feature_extractor import FeatureExtractor
from faster_whisper.tokenizer import Tokenizer from faster_whisper.tokenizer import _LANGUAGE_CODES, Tokenizer
from faster_whisper.utils import download_model, format_timestamp from faster_whisper.utils import download_model, format_timestamp, get_logger
from faster_whisper.vad import ( from faster_whisper.vad import (
SpeechTimestampsMap, SpeechTimestampsMap,
VadOptions,
collect_chunks, collect_chunks,
get_speech_timestamps, get_speech_timestamps,
) )
# implement logger not available in faster_whisper==0.4.1
def get_logger():
"""Returns the module logger."""
return logging.getLogger("faster_whisper")
class Word(NamedTuple): class Word(NamedTuple):
start: float start: float
end: float end: float
@@ -37,18 +31,17 @@ class Word(NamedTuple):
class Segment(NamedTuple): class Segment(NamedTuple):
id: int
seek: int
start: float start: float
end: float end: float
text: str text: str
words: Optional[List[Word]] tokens: List[int]
avg_log_prob: float temperature: float
avg_logprob: float
compression_ratio: float
no_speech_prob: float no_speech_prob: float
words: Optional[List[Word]]
class AudioInfo(NamedTuple):
language: str
language_probability: float
duration: float
class TranscriptionOptions(NamedTuple): class TranscriptionOptions(NamedTuple):
@@ -56,12 +49,15 @@ class TranscriptionOptions(NamedTuple):
best_of: int best_of: int
patience: float patience: float
length_penalty: float length_penalty: float
repetition_penalty: float
no_repeat_ngram_size: int
log_prob_threshold: Optional[float] log_prob_threshold: Optional[float]
no_speech_threshold: Optional[float] no_speech_threshold: Optional[float]
compression_ratio_threshold: Optional[float] compression_ratio_threshold: Optional[float]
condition_on_previous_text: bool condition_on_previous_text: bool
prompt_reset_on_temperature: float
temperatures: List[float] temperatures: List[float]
initial_prompt: Optional[str] initial_prompt: Optional[Union[str, Iterable[int]]]
prefix: Optional[str] prefix: Optional[str]
suppress_blank: bool suppress_blank: bool
suppress_tokens: Optional[List[int]] suppress_tokens: Optional[List[int]]
@@ -72,6 +68,16 @@ class TranscriptionOptions(NamedTuple):
append_punctuations: str append_punctuations: str
class TranscriptionInfo(NamedTuple):
language: str
language_probability: float
duration: float
duration_after_vad: float
all_language_probs: Optional[List[Tuple[str, float]]]
transcription_options: TranscriptionOptions
vad_options: VadOptions
class WhisperModel: class WhisperModel:
def __init__( def __init__(
self, self,
@@ -82,14 +88,15 @@ class WhisperModel:
cpu_threads: int = 0, cpu_threads: int = 0,
num_workers: int = 1, num_workers: int = 1,
download_root: Optional[str] = None, download_root: Optional[str] = None,
local_files_only: bool = True, local_files_only: bool = False,
): ):
"""Initializes the Whisper model. """Initializes the Whisper model.
Args: Args:
model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en, model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en,
small, small.en, medium, medium.en, large-v1, or large-v2) or a path to a converted small, small.en, medium, medium.en, large-v1, large-v2, or large), a path to a converted
model directory. When a size is configured, the converted model is downloaded model directory, or a CTranslate2-converted Whisper model ID from the Hugging Face Hub.
When a size or a model ID is configured, the converted model is downloaded
from the Hugging Face Hub. from the Hugging Face Hub.
device: Device to use for computation ("cpu", "cuda", "auto"). device: Device to use for computation ("cpu", "cuda", "auto").
device_index: Device ID to use. device_index: Device ID to use.
@@ -104,8 +111,10 @@ class WhisperModel:
having multiple workers enables true parallelism when running the model having multiple workers enables true parallelism when running the model
(concurrent calls to self.model.generate() will run in parallel). (concurrent calls to self.model.generate() will run in parallel).
This can improve the global throughput at the cost of increased memory usage. This can improve the global throughput at the cost of increased memory usage.
download_root: Directory where the model should be saved. If not set, the model download_root: Directory where the models should be saved. If not set, the models
is saved in the standard Hugging Face cache directory. are saved in the standard Hugging Face cache directory.
local_files_only: If True, avoid downloading the file and return the path to the
local cached file if it exists.
""" """
self.logger = get_logger() self.logger = get_logger()
@@ -147,6 +156,11 @@ class WhisperModel:
self.time_precision = 0.02 self.time_precision = 0.02
self.max_length = 448 self.max_length = 448
@property
def supported_languages(self) -> List[str]:
"""The languages supported by the model."""
return list(_LANGUAGE_CODES) if self.model.is_multilingual else ["en"]
def transcribe( def transcribe(
self, self,
audio: Union[str, BinaryIO, np.ndarray], audio: Union[str, BinaryIO, np.ndarray],
@@ -156,6 +170,8 @@ class WhisperModel:
best_of: int = 5, best_of: int = 5,
patience: float = 1, patience: float = 1,
length_penalty: float = 1, length_penalty: float = 1,
repetition_penalty: float = 1,
no_repeat_ngram_size: int = 0,
temperature: Union[float, List[float], Tuple[float, ...]] = [ temperature: Union[float, List[float], Tuple[float, ...]] = [
0.0, 0.0,
0.2, 0.2,
@@ -168,7 +184,8 @@ class WhisperModel:
log_prob_threshold: Optional[float] = -1.0, log_prob_threshold: Optional[float] = -1.0,
no_speech_threshold: Optional[float] = 0.6, no_speech_threshold: Optional[float] = 0.6,
condition_on_previous_text: bool = True, condition_on_previous_text: bool = True,
initial_prompt: Optional[str] = None, prompt_reset_on_temperature: float = 0.5,
initial_prompt: Optional[Union[str, Iterable[int]]] = None,
prefix: Optional[str] = None, prefix: Optional[str] = None,
suppress_blank: bool = True, suppress_blank: bool = True,
suppress_tokens: Optional[List[int]] = [-1], suppress_tokens: Optional[List[int]] = [-1],
@@ -178,8 +195,8 @@ class WhisperModel:
prepend_punctuations: str = "\"'“¿([{-", prepend_punctuations: str = "\"'“¿([{-",
append_punctuations: str = "\"'.。,!?::”)]}、", append_punctuations: str = "\"'.。,!?::”)]}、",
vad_filter: bool = False, vad_filter: bool = False,
vad_parameters: Optional[dict] = None, vad_parameters: Optional[Union[dict, VadOptions]] = None,
) -> Tuple[Iterable[Segment], AudioInfo]: ) -> Tuple[Iterable[Segment], TranscriptionInfo]:
"""Transcribes an input file. """Transcribes an input file.
Arguments: Arguments:
@@ -192,6 +209,9 @@ class WhisperModel:
best_of: Number of candidates when sampling with non-zero temperature. best_of: Number of candidates when sampling with non-zero temperature.
patience: Beam search patience factor. patience: Beam search patience factor.
length_penalty: Exponential length penalty constant. length_penalty: Exponential length penalty constant.
repetition_penalty: Penalty applied to the score of previously generated tokens
(set > 1 to penalize).
no_repeat_ngram_size: Prevent repetitions of ngrams with this size (set 0 to disable).
temperature: Temperature for sampling. It can be a tuple of temperatures, temperature: Temperature for sampling. It can be a tuple of temperatures,
which will be successively used upon failures according to either which will be successively used upon failures according to either
`compression_ratio_threshold` or `log_prob_threshold`. `compression_ratio_threshold` or `log_prob_threshold`.
@@ -206,7 +226,10 @@ class WhisperModel:
as a prompt for the next window; disabling may make the text inconsistent across as a prompt for the next window; disabling may make the text inconsistent across
windows, but the model becomes less prone to getting stuck in a failure loop, windows, but the model becomes less prone to getting stuck in a failure loop,
such as repetition looping or timestamps going out of sync. such as repetition looping or timestamps going out of sync.
initial_prompt: Optional text to provide as a prompt for the first window. prompt_reset_on_temperature: Resets prompt if temperature is above this value.
Arg has effect only if condition_on_previous_text is True.
initial_prompt: Optional text string or iterable of token ids to provide as a
prompt for the first window.
prefix: Optional text to provide as a prefix for the first window. prefix: Optional text to provide as a prefix for the first window.
suppress_blank: Suppress blank outputs at the beginning of the sampling. suppress_blank: Suppress blank outputs at the beginning of the sampling.
suppress_tokens: List of token IDs to suppress. -1 will suppress a default set suppress_tokens: List of token IDs to suppress. -1 will suppress a default set
@@ -222,14 +245,14 @@ class WhisperModel:
vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio
without speech. This step is using the Silero VAD model without speech. This step is using the Silero VAD model
https://github.com/snakers4/silero-vad. https://github.com/snakers4/silero-vad.
vad_parameters: Dictionary of Silero VAD parameters (see available parameters and vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available
default values in the function `get_speech_timestamps`). parameters and default values in the class `VadOptions`).
Returns: Returns:
A tuple with: A tuple with:
- a generator over transcribed segments - a generator over transcribed segments
- an instance of AudioInfo - an instance of TranscriptionInfo
""" """
sampling_rate = self.feature_extractor.sampling_rate sampling_rate = self.feature_extractor.sampling_rate
@@ -237,19 +260,24 @@ class WhisperModel:
audio = decode_audio(audio, sampling_rate=sampling_rate) audio = decode_audio(audio, sampling_rate=sampling_rate)
duration = audio.shape[0] / sampling_rate duration = audio.shape[0] / sampling_rate
duration_after_vad = duration
self.logger.info( self.logger.info(
"Processing audio with duration %s", format_timestamp(duration) "Processing audio with duration %s", format_timestamp(duration)
) )
if vad_filter: if vad_filter:
vad_parameters = {} if vad_parameters is None else vad_parameters if vad_parameters is None:
speech_chunks = get_speech_timestamps(audio, **vad_parameters) vad_parameters = VadOptions()
elif isinstance(vad_parameters, dict):
vad_parameters = VadOptions(**vad_parameters)
speech_chunks = get_speech_timestamps(audio, vad_parameters)
audio = collect_chunks(audio, speech_chunks) audio = collect_chunks(audio, speech_chunks)
duration_after_vad = audio.shape[0] / sampling_rate
self.logger.info( self.logger.info(
"VAD filter removed %s of audio", "VAD filter removed %s of audio",
format_timestamp(duration - (audio.shape[0] / sampling_rate)), format_timestamp(duration - duration_after_vad),
) )
if self.logger.isEnabledFor(logging.DEBUG): if self.logger.isEnabledFor(logging.DEBUG):
@@ -271,6 +299,7 @@ class WhisperModel:
features = self.feature_extractor(audio) features = self.feature_extractor(audio)
encoder_output = None encoder_output = None
all_language_probs = None
if language is None: if language is None:
if not self.model.is_multilingual: if not self.model.is_multilingual:
@@ -279,17 +308,27 @@ class WhisperModel:
else: else:
segment = features[:, : self.feature_extractor.nb_max_frames] segment = features[:, : self.feature_extractor.nb_max_frames]
encoder_output = self.encode(segment) encoder_output = self.encode(segment)
results = self.model.detect_language(encoder_output) # results is a list of tuple[str, float] with language names and
language_token, language_probability = results[0][0] # probabilities.
language = language_token[2:-2] results = self.model.detect_language(encoder_output)[0]
# Parse language names to strip out markers
all_language_probs = [(token[2:-2], prob) for (token, prob) in results]
# Get top language token and probability
language, language_probability = all_language_probs[0]
self.logger.info( self.logger.info(
"Detected language '%s' with probability %.2f", "Detected language '%s' with probability %.2f",
language, language,
language_probability, language_probability,
) )
return language, language_probability
else: else:
if not self.model.is_multilingual and language != "en":
self.logger.warning(
"The current model is English-only but the language parameter is set to '%s'; "
"using 'en' instead." % language
)
language = "en"
language_probability = 1 language_probability = 1
tokenizer = Tokenizer( tokenizer = Tokenizer(
@@ -304,10 +343,13 @@ class WhisperModel:
best_of=best_of, best_of=best_of,
patience=patience, patience=patience,
length_penalty=length_penalty, length_penalty=length_penalty,
repetition_penalty=repetition_penalty,
no_repeat_ngram_size=no_repeat_ngram_size,
log_prob_threshold=log_prob_threshold, log_prob_threshold=log_prob_threshold,
no_speech_threshold=no_speech_threshold, no_speech_threshold=no_speech_threshold,
compression_ratio_threshold=compression_ratio_threshold, compression_ratio_threshold=compression_ratio_threshold,
condition_on_previous_text=condition_on_previous_text, condition_on_previous_text=condition_on_previous_text,
prompt_reset_on_temperature=prompt_reset_on_temperature,
temperatures=( temperatures=(
temperature if isinstance(temperature, (list, tuple)) else [temperature] temperature if isinstance(temperature, (list, tuple)) else [temperature]
), ),
@@ -327,13 +369,17 @@ class WhisperModel:
if speech_chunks: if speech_chunks:
segments = restore_speech_timestamps(segments, speech_chunks, sampling_rate) segments = restore_speech_timestamps(segments, speech_chunks, sampling_rate)
audio_info = AudioInfo( info = TranscriptionInfo(
language=language, language=language,
language_probability=language_probability, language_probability=language_probability,
duration=duration, duration=duration,
duration_after_vad=duration_after_vad,
transcription_options=options,
vad_options=vad_parameters,
all_language_probs=all_language_probs,
) )
return segments return segments, info
def generate_segments( def generate_segments(
self, self,
@@ -343,14 +389,20 @@ class WhisperModel:
encoder_output: Optional[ctranslate2.StorageView] = None, encoder_output: Optional[ctranslate2.StorageView] = None,
) -> Iterable[Segment]: ) -> Iterable[Segment]:
content_frames = features.shape[-1] - self.feature_extractor.nb_max_frames content_frames = features.shape[-1] - self.feature_extractor.nb_max_frames
idx = 0
seek = 0 seek = 0
all_tokens = [] all_tokens = []
prompt_reset_since = 0 prompt_reset_since = 0
if options.initial_prompt is not None: if options.initial_prompt is not None:
if isinstance(options.initial_prompt, str):
initial_prompt = " " + options.initial_prompt.strip() initial_prompt = " " + options.initial_prompt.strip()
initial_prompt_tokens = tokenizer.encode(initial_prompt) initial_prompt_tokens = tokenizer.encode(initial_prompt)
all_tokens.extend(initial_prompt_tokens) all_tokens.extend(initial_prompt_tokens)
else:
all_tokens.extend(options.initial_prompt)
last_speech_timestamp = 0.0
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
@@ -373,12 +425,15 @@ class WhisperModel:
prefix=options.prefix if seek == 0 else None, prefix=options.prefix if seek == 0 else None,
) )
if encoder_output is None: if seek > 0 or encoder_output is None:
encoder_output = self.encode(segment) encoder_output = self.encode(segment)
result, avg_log_prob, temperature = self.generate_with_fallback( (
encoder_output, prompt, tokenizer, options result,
) avg_logprob,
temperature,
compression_ratio,
) = self.generate_with_fallback(encoder_output, prompt, tokenizer, options)
if options.no_speech_threshold is not None: if options.no_speech_threshold is not None:
# no voice activity check # no voice activity check
@@ -386,7 +441,7 @@ class WhisperModel:
if ( if (
options.log_prob_threshold is not None options.log_prob_threshold is not None
and avg_log_prob > options.log_prob_threshold and avg_logprob > options.log_prob_threshold
): ):
# don't skip if the logprob is high enough, despite the no_speech_prob # don't skip if the logprob is high enough, despite the no_speech_prob
should_skip = False should_skip = False
@@ -482,9 +537,6 @@ class WhisperModel:
seek += segment_size seek += segment_size
if not options.condition_on_previous_text or temperature > 0.5:
prompt_reset_since = len(all_tokens)
if options.word_timestamps: if options.word_timestamps:
self.add_word_timestamps( self.add_word_timestamps(
current_segments, current_segments,
@@ -493,12 +545,14 @@ class WhisperModel:
segment_size, segment_size,
options.prepend_punctuations, options.prepend_punctuations,
options.append_punctuations, options.append_punctuations,
last_speech_timestamp=last_speech_timestamp,
) )
word_end_timestamps = [ word_end_timestamps = [
w["end"] for s in current_segments for w in s["words"] w["end"] for s in current_segments for w in s["words"]
] ]
if len(word_end_timestamps) > 0:
last_speech_timestamp = word_end_timestamps[-1]
if not single_timestamp_ending and len(word_end_timestamps) > 0: if not single_timestamp_ending and len(word_end_timestamps) > 0:
seek_shift = round( seek_shift = round(
(word_end_timestamps[-1] - time_offset) * self.frames_per_second (word_end_timestamps[-1] - time_offset) * self.frames_per_second
@@ -507,8 +561,6 @@ class WhisperModel:
if seek_shift > 0: if seek_shift > 0:
seek = previous_seek + seek_shift seek = previous_seek + seek_shift
encoder_output = None
for segment in current_segments: for segment in current_segments:
tokens = segment["tokens"] tokens = segment["tokens"]
text = tokenizer.decode(tokens) text = tokenizer.decode(tokens)
@@ -517,19 +569,38 @@ class WhisperModel:
continue continue
all_tokens.extend(tokens) all_tokens.extend(tokens)
idx += 1
all_segments.append(Segment( all_segments.append(Segment(
id=idx,
seek=seek,
start=segment["start"], start=segment["start"],
end=segment["end"], end=segment["end"],
text=text, text=text,
tokens=tokens,
temperature=temperature,
avg_logprob=avg_logprob,
compression_ratio=compression_ratio,
no_speech_prob=result.no_speech_prob,
words=( words=(
[Word(**word) for word in segment["words"]] [Word(**word) for word in segment["words"]]
if options.word_timestamps if options.word_timestamps
else None else None
), ),
avg_log_prob=avg_log_prob,
no_speech_prob=result.no_speech_prob,
)) ))
if (
not options.condition_on_previous_text
or temperature > options.prompt_reset_on_temperature
):
if options.condition_on_previous_text:
self.logger.debug(
"Reset prompt. prompt_reset_on_temperature threshold is met %f > %f",
temperature,
options.prompt_reset_on_temperature,
)
prompt_reset_since = len(all_tokens)
return all_segments return all_segments
def encode(self, features: np.ndarray) -> ctranslate2.StorageView: def encode(self, features: np.ndarray) -> ctranslate2.StorageView:
@@ -548,10 +619,10 @@ class WhisperModel:
prompt: List[int], prompt: List[int],
tokenizer: Tokenizer, tokenizer: Tokenizer,
options: TranscriptionOptions, options: TranscriptionOptions,
) -> Tuple[ctranslate2.models.WhisperGenerationResult, float, float]: ) -> Tuple[ctranslate2.models.WhisperGenerationResult, float, float, float]:
result = None decode_result = None
avg_log_prob = None all_results = []
final_temperature = None below_cr_threshold_results = []
max_initial_timestamp_index = int( max_initial_timestamp_index = int(
round(options.max_initial_timestamp / self.time_precision) round(options.max_initial_timestamp / self.time_precision)
@@ -571,11 +642,12 @@ class WhisperModel:
"patience": options.patience, "patience": options.patience,
} }
final_temperature = temperature
result = self.model.generate( result = self.model.generate(
encoder_output, encoder_output,
[prompt], [prompt],
length_penalty=options.length_penalty, length_penalty=options.length_penalty,
repetition_penalty=options.repetition_penalty,
no_repeat_ngram_size=options.no_repeat_ngram_size,
max_length=self.max_length, max_length=self.max_length,
return_scores=True, return_scores=True,
return_no_speech_prob=True, return_no_speech_prob=True,
@@ -589,18 +661,24 @@ class WhisperModel:
# Recover the average log prob from the returned score. # Recover the average log prob from the returned score.
seq_len = len(tokens) seq_len = len(tokens)
cum_log_prob = result.scores[0] * (seq_len**options.length_penalty) cum_logprob = result.scores[0] * (seq_len**options.length_penalty)
avg_log_prob = cum_log_prob / (seq_len + 1) avg_logprob = cum_logprob / (seq_len + 1)
text = tokenizer.decode(tokens).strip() text = tokenizer.decode(tokens).strip()
compression_ratio = get_compression_ratio(text) compression_ratio = get_compression_ratio(text)
decode_result = (
result,
avg_logprob,
temperature,
compression_ratio,
)
all_results.append(decode_result)
needs_fallback = False needs_fallback = False
if ( if options.compression_ratio_threshold is not None:
options.compression_ratio_threshold is not None if compression_ratio > options.compression_ratio_threshold:
and compression_ratio > options.compression_ratio_threshold
):
needs_fallback = True # too repetitive needs_fallback = True # too repetitive
self.logger.debug( self.logger.debug(
@@ -609,24 +687,37 @@ class WhisperModel:
compression_ratio, compression_ratio,
options.compression_ratio_threshold, options.compression_ratio_threshold,
) )
else:
below_cr_threshold_results.append(decode_result)
if ( if (
options.log_prob_threshold is not None options.log_prob_threshold is not None
and avg_log_prob < options.log_prob_threshold and avg_logprob < options.log_prob_threshold
): ):
needs_fallback = True # average log probability is too low needs_fallback = True # average log probability is too low
self.logger.debug( self.logger.debug(
"Log probability threshold is not met with temperature %.1f (%f < %f)", "Log probability threshold is not met with temperature %.1f (%f < %f)",
temperature, temperature,
avg_log_prob, avg_logprob,
options.log_prob_threshold, options.log_prob_threshold,
) )
if (
options.no_speech_threshold is not None
and result.no_speech_prob > options.no_speech_threshold
):
needs_fallback = False # silence
if not needs_fallback: if not needs_fallback:
break break
else:
# all failed, select the result with the highest average log probability
decode_result = max(
below_cr_threshold_results or all_results, key=lambda x: x[1]
)
return result, avg_log_prob, final_temperature return decode_result
def get_prompt( def get_prompt(
self, self,
@@ -650,6 +741,8 @@ class WhisperModel:
prefix_tokens = tokenizer.encode(" " + prefix.strip()) prefix_tokens = tokenizer.encode(" " + prefix.strip())
if len(prefix_tokens) >= self.max_length // 2: if len(prefix_tokens) >= self.max_length // 2:
prefix_tokens = prefix_tokens[: self.max_length // 2 - 1] prefix_tokens = prefix_tokens[: self.max_length // 2 - 1]
if not without_timestamps:
prompt.append(tokenizer.timestamp_begin)
prompt.extend(prefix_tokens) prompt.extend(prefix_tokens)
return prompt return prompt
@@ -662,7 +755,8 @@ class WhisperModel:
num_frames: int, num_frames: int,
prepend_punctuations: str, prepend_punctuations: str,
append_punctuations: str, append_punctuations: str,
): last_speech_timestamp: float,
) -> None:
if len(segments) == 0: if len(segments) == 0:
return return
@@ -675,6 +769,24 @@ class WhisperModel:
alignment = self.find_alignment( alignment = self.find_alignment(
tokenizer, text_tokens, encoder_output, num_frames tokenizer, text_tokens, encoder_output, num_frames
) )
word_durations = np.array([word["end"] - word["start"] for word in alignment])
word_durations = word_durations[word_durations.nonzero()]
median_duration = np.median(word_durations) if len(word_durations) > 0 else 0.0
max_duration = median_duration * 2
# hack: truncate long words at sentence boundaries.
# a better segmentation algorithm based on VAD should be able to replace this.
if len(word_durations) > 0:
sentence_end_marks = ".。!?"
# ensure words at sentence boundaries
# are not longer than twice the median word duration.
for i in range(1, len(alignment)):
if alignment[i]["end"] - alignment[i]["start"] > max_duration:
if alignment[i]["word"] in sentence_end_marks:
alignment[i]["end"] = alignment[i]["start"] + max_duration
elif alignment[i - 1]["word"] in sentence_end_marks:
alignment[i]["start"] = alignment[i]["end"] - max_duration
merge_punctuations(alignment, prepend_punctuations, append_punctuations) merge_punctuations(alignment, prepend_punctuations, append_punctuations)
time_offset = ( time_offset = (
@@ -705,11 +817,52 @@ class WhisperModel:
saved_tokens += len(timing["tokens"]) saved_tokens += len(timing["tokens"])
word_index += 1 word_index += 1
# hack: truncate long words at segment boundaries.
# a better segmentation algorithm based on VAD should be able to replace this.
if len(words) > 0: if len(words) > 0:
# adjust the segment-level timestamps based on the word-level timestamps # ensure the first and second word after a pause is not longer than
# twice the median word duration.
if words[0]["end"] - last_speech_timestamp > median_duration * 4 and (
words[0]["end"] - words[0]["start"] > max_duration
or (
len(words) > 1
and words[1]["end"] - words[0]["start"] > max_duration * 2
)
):
if (
len(words) > 1
and words[1]["end"] - words[1]["start"] > max_duration
):
boundary = max(
words[1]["end"] / 2, words[1]["end"] - max_duration
)
words[0]["end"] = words[1]["start"] = boundary
words[0]["start"] = max(0, words[0]["end"] - max_duration)
# prefer the segment-level start timestamp if the first word is too long.
if (
segment["start"] < words[0]["end"]
and segment["start"] - 0.5 > words[0]["start"]
):
words[0]["start"] = max(
0, min(words[0]["end"] - median_duration, segment["start"])
)
else:
segment["start"] = words[0]["start"] segment["start"] = words[0]["start"]
# prefer the segment-level end timestamp if the last word is too long.
if (
segment["end"] > words[-1]["start"]
and segment["end"] + 0.5 < words[-1]["end"]
):
words[-1]["end"] = max(
words[-1]["start"] + median_duration, segment["end"]
)
else:
segment["end"] = words[-1]["end"] segment["end"] = words[-1]["end"]
last_speech_timestamp = segment["end"]
segment["words"] = words segment["words"] = words
def find_alignment( def find_alignment(
@@ -741,6 +894,8 @@ class WhisperModel:
text_tokens + [tokenizer.eot] text_tokens + [tokenizer.eot]
) )
word_boundaries = np.pad(np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0)) word_boundaries = np.pad(np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0))
if len(word_boundaries) <= 1:
return []
jumps = np.pad(np.diff(text_indices), (1, 0), constant_values=1).astype(bool) jumps = np.pad(np.diff(text_indices), (1, 0), constant_values=1).astype(bool)
jump_times = time_indices[jumps] / self.tokens_per_second jump_times = time_indices[jumps] / self.tokens_per_second
@@ -751,22 +906,6 @@ class WhisperModel:
for i, j in zip(word_boundaries[:-1], word_boundaries[1:]) for i, j in zip(word_boundaries[:-1], word_boundaries[1:])
] ]
# hack: ensure the first and second word is not longer than twice the median word duration.
# a better segmentation algorithm based on VAD should be able to replace this.
word_durations = end_times - start_times
word_durations = word_durations[word_durations.nonzero()]
if len(word_durations) > 0:
median_duration = np.median(word_durations)
max_duration = median_duration * 2
if len(word_durations) >= 2 and word_durations[1] > max_duration:
boundary = max(end_times[2] / 2, end_times[2] - max_duration)
end_times[0] = start_times[1] = boundary
if (
len(word_durations) >= 1
and end_times[0] - start_times[0] > max_duration
):
start_times[0] = max(0, end_times[0] - max_duration)
return [ return [
dict( dict(
word=word, tokens=tokens, start=start, end=end, probability=probability word=word, tokens=tokens, start=start, end=end, probability=probability
@@ -792,7 +931,8 @@ def restore_speech_timestamps(
words = [] words = []
for word in segment.words: for word in segment.words:
# Ensure the word start and end times are resolved to the same chunk. # Ensure the word start and end times are resolved to the same chunk.
chunk_index = ts_map.get_chunk_index(word.start) middle = (word.start + word.end) / 2
chunk_index = ts_map.get_chunk_index(middle)
word = word._replace( word = word._replace(
start=ts_map.get_original_time(word.start, chunk_index), start=ts_map.get_original_time(word.start, chunk_index),
end=ts_map.get_original_time(word.end, chunk_index), end=ts_map.get_original_time(word.end, chunk_index),
@@ -811,7 +951,7 @@ def restore_speech_timestamps(
end=ts_map.get_original_time(segment.end), end=ts_map.get_original_time(segment.end),
) )
yield segment return segments
def get_ctranslate2_storage(segment: np.ndarray) -> ctranslate2.StorageView: def get_ctranslate2_storage(segment: np.ndarray) -> ctranslate2.StorageView:
@@ -825,7 +965,10 @@ def get_compression_ratio(text: str) -> float:
return len(text_bytes) / len(zlib.compress(text_bytes)) return len(text_bytes) / len(zlib.compress(text_bytes))
def get_suppressed_tokens(tokenizer, suppress_tokens): def get_suppressed_tokens(
tokenizer: Tokenizer,
suppress_tokens: Optional[List[int]],
) -> Optional[List[int]]:
if not suppress_tokens or -1 in suppress_tokens: if not suppress_tokens or -1 in suppress_tokens:
return suppress_tokens return suppress_tokens
@@ -846,7 +989,7 @@ def get_suppressed_tokens(tokenizer, suppress_tokens):
return sorted(set(suppress_tokens)) return sorted(set(suppress_tokens))
def merge_punctuations(alignment: List[dict], prepended: str, appended: str): def merge_punctuations(alignment: List[dict], prepended: str, appended: str) -> None:
# merge prepended punctuations # merge prepended punctuations
i = len(alignment) - 2 i = len(alignment) - 2
j = len(alignment) - 1 j = len(alignment) - 1
-115
View File
@@ -1,115 +0,0 @@
# original: https://github.com/snakers4/silero-vad/blob/master/utils_vad.py
import os
import subprocess
import torch
import numpy as np
import onnxruntime
class VoiceActivityDetection():
def __init__(self, force_onnx_cpu=True):
path = self.download()
opts = onnxruntime.SessionOptions()
opts.log_severity_level = 3
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
if force_onnx_cpu and 'CPUExecutionProvider' in onnxruntime.get_available_providers():
self.session = onnxruntime.InferenceSession(path, providers=['CPUExecutionProvider'], sess_options=opts)
else:
self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts)
self.reset_states()
self.sample_rates = [8000, 16000]
def _validate_input(self, x, sr: int):
if x.dim() == 1:
x = x.unsqueeze(0)
if x.dim() > 2:
raise ValueError(f"Too many dimensions for input audio chunk {x.dim()}")
if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000
x = x[:,::step]
sr = 16000
if sr not in self.sample_rates:
raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)")
if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short")
return x, sr
def reset_states(self, batch_size=1):
self._h = np.zeros((2, batch_size, 64)).astype('float32')
self._c = np.zeros((2, batch_size, 64)).astype('float32')
self._last_sr = 0
self._last_batch_size = 0
def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr)
batch_size = x.shape[0]
if not self._last_batch_size:
self.reset_states(batch_size)
if (self._last_sr) and (self._last_sr != sr):
self.reset_states(batch_size)
if (self._last_batch_size) and (self._last_batch_size != batch_size):
self.reset_states(batch_size)
if sr in [8000, 16000]:
ort_inputs = {'input': x.numpy(), 'h': self._h, 'c': self._c, 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs)
out, self._h, self._c = ort_outs
else:
raise ValueError()
self._last_sr = sr
self._last_batch_size = batch_size
out = torch.tensor(out)
return out
def audio_forward(self, x, sr: int, num_samples: int = 512):
outs = []
x, sr = self._validate_input(x, sr)
if x.shape[1] % num_samples:
pad_num = num_samples - (x.shape[1] % num_samples)
x = torch.nn.functional.pad(x, (0, pad_num), 'constant', value=0.0)
self.reset_states(x.shape[0])
for i in range(0, x.shape[1], num_samples):
wavs_batch = x[:, i:i+num_samples]
out_chunk = self.__call__(wavs_batch, sr)
outs.append(out_chunk)
stacked = torch.cat(outs, dim=1)
return stacked.cpu()
@staticmethod
def download(model_url="https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx"):
target_dir = os.path.expanduser("~/.cache/whisper-live/")
# Ensure the target directory exists
os.makedirs(target_dir, exist_ok=True)
# Define the target file path
model_filename = os.path.join(target_dir, "silero_vad.onnx")
# Check if the model file already exists
if not os.path.exists(model_filename):
# If it doesn't exist, download the model using wget
print("Downloading VAD ONNX model...")
try:
subprocess.run(["wget", "-O", model_filename, model_url], check=True)
except subprocess.CalledProcessError:
print("Failed to download the model using wget.")
return model_filename