Merge pull request #64 from makaveli10/update_faster_whisper
Update faster whisper.
This commit is contained in:
@@ -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 @@
|
|||||||
__version__="0.0.7"
|
__version__="0.0.8"
|
||||||
+25
-58
@@ -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 = []
|
||||||
@@ -410,15 +376,16 @@ class ServeClient:
|
|||||||
if time.time() - self.t_start > self.add_pause_thresh:
|
if time.time() - self.t_start > self.add_pause_thresh:
|
||||||
self.text.append('')
|
self.text.append('')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.websocket.send(
|
self.websocket.send(
|
||||||
json.dumps({
|
json.dumps({
|
||||||
"uid": self.client_uid,
|
"uid": self.client_uid,
|
||||||
"segments": segments
|
"segments": segments
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
|||||||
+244
-101
@@ -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:
|
||||||
initial_prompt = " " + options.initial_prompt.strip()
|
if isinstance(options.initial_prompt, str):
|
||||||
initial_prompt_tokens = tokenizer.encode(initial_prompt)
|
initial_prompt = " " + options.initial_prompt.strip()
|
||||||
all_tokens.extend(initial_prompt_tokens)
|
initial_prompt_tokens = tokenizer.encode(initial_prompt)
|
||||||
|
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,44 +661,63 @@ 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(
|
||||||
"Compression ratio threshold is not met with temperature %.1f (%f > %f)",
|
"Compression ratio threshold is not met with temperature %.1f (%f > %f)",
|
||||||
temperature,
|
temperature,
|
||||||
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,10 +817,51 @@ 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
|
||||||
segment["start"] = words[0]["start"]
|
# twice the median word duration.
|
||||||
segment["end"] = words[-1]["end"]
|
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"]
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
|
||||||
|
last_speech_timestamp = segment["end"]
|
||||||
|
|
||||||
segment["words"] = words
|
segment["words"] = words
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user