use crate::error::{Error, Result};
#[cfg(feature = "vad")]
use crate::vad::SpeechSegment;
use crate::whisper::bundle::WhisperBundle;
use crate::whisper::dtw;
use crate::whisper::words::{self, WordTiming};
use boostr::model::audio::alignment_matrix;
use boostr::model::audio::mel::{MelOptions, compute_mel_spectrogram_with};
use numr::dtype::DType;
use numr::ops::{
ActivationOps, BinaryOps, ConditionalOps, ConvOps, IndexingOps, MatmulOps, NormalizationOps,
ReduceOps, ScalarOps, ShapeOps, SortingOps, StatisticalOps, TensorOps, UnaryOps,
};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
use splintr::WhisperVariant;
const TIME_PRECISION_SECS: f32 = 0.02;
const WHISPER_WINDOW_SECS: usize = 30;
#[derive(Debug, Clone, Default)]
pub struct TranscribeOptions<'a> {
pub language: Option<&'a str>,
pub translate: bool,
pub max_new_tokens: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct Transcription {
pub text: String,
pub tokens: Vec<u32>,
pub language: Option<&'static str>,
}
impl<R: Runtime<DType = DType>> WhisperBundle<R> {
pub fn transcribe<C>(
&self,
client: &C,
samples: &[f32],
sample_rate: usize,
opts: &TranscribeOptions<'_>,
) -> Result<Transcription>
where
C: RuntimeClient<R>
+ TensorOps<R>
+ ScalarOps<R>
+ MatmulOps<R>
+ BinaryOps<R>
+ ActivationOps<R>
+ NormalizationOps<R>
+ ConvOps<R>
+ ReduceOps<R>
+ ShapeOps<R>
+ UnaryOps<R>
+ ConditionalOps<R>
+ IndexingOps<R>,
R::Client: TensorOps<R> + ScalarOps<R> + ConvOps<R> + ReduceOps<R> + BinaryOps<R>,
{
if sample_rate == 0 {
return Err(Error::InvalidArgument {
arg: "sample_rate",
reason: "sample rate must be non-zero".into(),
});
}
let max_samples = WHISPER_WINDOW_SECS * sample_rate;
if samples.len() > max_samples {
let secs = samples.len() as f64 / sample_rate as f64;
return Err(Error::InvalidArgument {
arg: "samples",
reason: format!(
"audio is {secs:.3} s ({} samples at {sample_rate} Hz), over Whisper's fixed \
{WHISPER_WINDOW_SECS} s window ({max_samples} samples); segment the audio \
first (see voxr::vad::speech_timestamps) and transcribe each segment",
samples.len()
),
});
}
let mel_opts = MelOptions::whisper(self.num_mel_bins, sample_rate);
let mel = compute_mel_spectrogram_with(samples, sample_rate, &mel_opts)?;
if self.num_mel_bins == 0 || !mel.len().is_multiple_of(self.num_mel_bins) {
return Err(Error::InvalidArgument {
arg: "samples",
reason: format!(
"mel of {} values is not a whole number of {}-bin frames",
mel.len(),
self.num_mel_bins
),
});
}
let num_frames = mel.len() / self.num_mel_bins;
let shape = [1, self.num_mel_bins, num_frames];
let mel_t = Tensor::<R>::from_slice(&mel, &shape, client.device())?;
let encoded = self.model.encode(client, &mel_t)?;
let language = match (self.variant, opts.language) {
(WhisperVariant::EnglishOnly, _) => None,
(_, Some(code)) => Some(self.resolve_language(code)?),
(_, None) => Some(self.detect_language(client, &encoded)?),
};
let prompt = self.sot_prompt(language, opts.translate);
let mut gen_opts = self.generate_options();
if let Some(budget) = opts.max_new_tokens {
gen_opts.max_new_tokens = budget;
}
let tokens = self.model.generate(client, &encoded, &prompt, &gen_opts)?;
let text = self
.tokenizer
.decode(&tokens)
.map_err(|e| Error::ModelError {
reason: format!("decoding {} whisper token ids: {e}", tokens.len()),
})?;
Ok(Transcription {
text,
tokens,
language,
})
}
pub fn transcribe_words<C>(
&self,
client: &C,
samples: &[f32],
sample_rate: usize,
opts: &TranscribeOptions<'_>,
) -> Result<(Transcription, Vec<WordTiming>)>
where
C: RuntimeClient<R>
+ TensorOps<R>
+ ScalarOps<R>
+ MatmulOps<R>
+ BinaryOps<R>
+ ActivationOps<R>
+ NormalizationOps<R>
+ ConvOps<R>
+ ReduceOps<R>
+ ShapeOps<R>
+ UnaryOps<R>
+ ConditionalOps<R>
+ IndexingOps<R>
+ StatisticalOps<R>
+ SortingOps<R>,
R::Client: TensorOps<R> + ScalarOps<R> + ConvOps<R> + ReduceOps<R> + BinaryOps<R>,
{
let alignment_heads =
self.generation
.alignment_heads
.as_ref()
.ok_or_else(|| Error::ModelError {
reason: format!(
"{} ships no `alignment_heads` in generation_config.json; word \
timestamps need it to know which cross-attention heads to average",
self.checkpoint_dir.display()
),
})?;
if sample_rate == 0 {
return Err(Error::InvalidArgument {
arg: "sample_rate",
reason: "sample rate must be non-zero".into(),
});
}
let max_samples = WHISPER_WINDOW_SECS * sample_rate;
if samples.len() > max_samples {
let secs = samples.len() as f64 / sample_rate as f64;
return Err(Error::InvalidArgument {
arg: "samples",
reason: format!(
"audio is {secs:.3} s ({} samples at {sample_rate} Hz), over Whisper's fixed \
{WHISPER_WINDOW_SECS} s window ({max_samples} samples); segment the audio \
first (see voxr::vad::speech_timestamps) and transcribe each segment",
samples.len()
),
});
}
let mel_opts = MelOptions::whisper(self.num_mel_bins, sample_rate);
let mel = compute_mel_spectrogram_with(samples, sample_rate, &mel_opts)?;
if self.num_mel_bins == 0 || !mel.len().is_multiple_of(self.num_mel_bins) {
return Err(Error::InvalidArgument {
arg: "samples",
reason: format!(
"mel of {} values is not a whole number of {}-bin frames",
mel.len(),
self.num_mel_bins
),
});
}
let num_mel_frames = mel.len() / self.num_mel_bins;
let shape = [1, self.num_mel_bins, num_mel_frames];
let mel_t = Tensor::<R>::from_slice(&mel, &shape, client.device())?;
let encoded = self.model.encode(client, &mel_t)?;
let language = match (self.variant, opts.language) {
(WhisperVariant::EnglishOnly, _) => None,
(_, Some(code)) => Some(self.resolve_language(code)?),
(_, None) => Some(self.detect_language(client, &encoded)?),
};
let prompt = self.sot_prompt(language, opts.translate);
let mut gen_opts = self.generate_options();
if let Some(budget) = opts.max_new_tokens {
gen_opts.max_new_tokens = budget;
}
let mut layers: Vec<usize> = alignment_heads.iter().map(|&(l, _)| l).collect();
layers.sort_unstable();
layers.dedup();
let (tokens, weights) = self
.model
.generate_with_cross_attentions(client, &encoded, &prompt, &gen_opts, &layers)?;
let text = self
.tokenizer
.decode(&tokens)
.map_err(|e| Error::ModelError {
reason: format!("decoding {} whisper token ids: {e}", tokens.len()),
})?;
let transcription = Transcription {
text,
tokens: tokens.clone(),
language,
};
if tokens.is_empty() {
return Ok((transcription, Vec::new()));
}
let num_frames = samples.len() / mel_opts.hop_length;
let prefix_len = prompt.len();
let matrix = alignment_matrix(
client,
&weights,
alignment_heads,
num_frames,
self.generation.median_filter_width,
prefix_len,
)?;
let frames = num_frames / 2;
let matrix_host: Vec<f32> = matrix.try_to_vec()?;
let full_token_timestamps = dtw::token_timestamps(
&matrix_host,
tokens.len(),
frames,
TIME_PRECISION_SECS,
prefix_len,
);
let eos_token_id = self.variant.eos_token_id();
let (word_texts, token_indices) =
words::combine_tokens_into_words(&self.tokenizer, &tokens, language, eos_token_id);
let word_timings = words::collate_word_timestamps(
&word_texts,
&token_indices,
&full_token_timestamps,
prefix_len,
);
Ok((transcription, word_timings))
}
pub fn detect_language<C>(&self, client: &C, encoded: &Tensor<R>) -> Result<&'static str>
where
C: RuntimeClient<R>
+ TensorOps<R>
+ ScalarOps<R>
+ MatmulOps<R>
+ BinaryOps<R>
+ ActivationOps<R>
+ NormalizationOps<R>
+ ReduceOps<R>
+ ShapeOps<R>
+ UnaryOps<R>
+ ConditionalOps<R>
+ IndexingOps<R>,
R::Client: TensorOps<R> + ScalarOps<R>,
{
let candidates = self.language_token_ids();
if candidates.is_empty() {
return Err(Error::InvalidArgument {
arg: "language",
reason: format!(
"{:?} carries no language tokens, so there is nothing to detect",
self.variant
),
});
}
let picked = self.model.detect_language(
client,
encoded,
self.variant.sot_token_id(),
&candidates,
)?;
self.language_code(picked).ok_or_else(|| Error::ModelError {
reason: format!("language detection picked id {picked}, outside the language block"),
})
}
fn resolve_language(&self, code: &str) -> Result<&'static str> {
let id = self
.variant
.language_token_id(code)
.ok_or_else(|| Error::InvalidArgument {
arg: "language",
reason: format!("{code:?} is not a language {:?} knows", self.variant),
})?;
self.language_code(id).ok_or_else(|| Error::ModelError {
reason: format!("language token {id} for {code:?} is outside the language block"),
})
}
#[cfg(feature = "vad")]
pub fn transcribe_segments<C>(
&self,
client: &C,
samples: &[f32],
sample_rate: usize,
segments: &[SpeechSegment],
opts: &TranscribeOptions<'_>,
) -> Result<Vec<Transcription>>
where
C: RuntimeClient<R>
+ TensorOps<R>
+ ScalarOps<R>
+ MatmulOps<R>
+ BinaryOps<R>
+ ActivationOps<R>
+ NormalizationOps<R>
+ ConvOps<R>
+ ReduceOps<R>
+ ShapeOps<R>
+ UnaryOps<R>
+ ConditionalOps<R>
+ IndexingOps<R>,
R::Client: TensorOps<R> + ScalarOps<R> + ConvOps<R> + ReduceOps<R> + BinaryOps<R>,
{
let mut out = Vec::with_capacity(segments.len());
for (i, seg) in segments.iter().enumerate() {
if seg.start > seg.end || seg.end > samples.len() {
return Err(Error::InvalidArgument {
arg: "segments",
reason: format!(
"segment {i} covers samples {}..{}, outside the {} samples given",
seg.start,
seg.end,
samples.len()
),
});
}
out.push(self.transcribe(client, &samples[seg.start..seg.end], sample_rate, opts)?);
}
Ok(out)
}
}