use crate::error::WhisperResult;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct Hypothesis {
pub tokens: Vec<u32>,
pub score: f32,
pub is_complete: bool,
}
impl Hypothesis {
fn new(tokens: Vec<u32>, score: f32) -> Self {
Self {
tokens,
score,
is_complete: false,
}
}
fn normalized_score(&self, length_penalty: f32) -> f32 {
let len = self.tokens.len() as f32;
self.score / len.powf(length_penalty)
}
}
#[derive(Debug, Clone)]
pub struct BeamSearchDecoder {
beam_size: usize,
max_tokens: usize,
temperature: f32,
patience: f32,
length_penalty: f32,
}
impl BeamSearchDecoder {
#[must_use]
pub const fn new(beam_size: usize, max_tokens: usize) -> Self {
Self {
beam_size,
max_tokens,
temperature: 0.0,
patience: 1.0,
length_penalty: 1.0,
}
}
#[must_use]
pub const fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = temperature;
self
}
#[must_use]
pub const fn with_patience(mut self, patience: f32) -> Self {
self.patience = patience;
self
}
#[must_use]
pub const fn with_length_penalty(mut self, length_penalty: f32) -> Self {
self.length_penalty = length_penalty;
self
}
#[must_use]
pub const fn beam_size(&self) -> usize {
self.beam_size
}
#[must_use]
pub const fn max_tokens(&self) -> usize {
self.max_tokens
}
#[must_use]
pub const fn temperature(&self) -> f32 {
self.temperature
}
#[must_use]
pub const fn patience(&self) -> f32 {
self.patience
}
#[must_use]
pub const fn length_penalty(&self) -> f32 {
self.length_penalty
}
pub fn decode<F>(
&self,
mut logits_fn: F,
initial_tokens: &[u32],
eot_token: u32,
) -> WhisperResult<Vec<u32>>
where
F: FnMut(&[u32]) -> WhisperResult<Vec<f32>>,
{
let eot = eot_token;
let mut hypotheses = vec![Hypothesis::new(initial_tokens.to_vec(), 0.0)];
let mut completed: Vec<Hypothesis> = Vec::new();
loop {
let min_len = hypotheses
.iter()
.map(|h| h.tokens.len())
.min()
.unwrap_or(self.max_tokens);
if min_len >= self.max_tokens {
break;
}
let mut all_candidates: Vec<Hypothesis> = Vec::new();
for hyp in &hypotheses {
if hyp.is_complete || hyp.tokens.len() >= self.max_tokens {
continue;
}
let logits = logits_fn(&hyp.tokens)?;
let log_probs = self.log_softmax(&logits);
let top_k = Self::top_k_indices(&log_probs, self.beam_size);
for (token, log_prob) in top_k {
let mut new_tokens = hyp.tokens.clone();
new_tokens.push(token);
let mut new_hyp = Hypothesis::new(new_tokens, hyp.score + log_prob);
if token == eot {
new_hyp.is_complete = true;
completed.push(new_hyp);
} else {
all_candidates.push(new_hyp);
}
}
}
all_candidates.sort_by(|a, b| {
b.normalized_score(self.length_penalty)
.partial_cmp(&a.normalized_score(self.length_penalty))
.unwrap_or(std::cmp::Ordering::Equal)
});
hypotheses = all_candidates.into_iter().take(self.beam_size).collect();
if self.should_stop_early(&completed, &hypotheses) {
break;
}
if hypotheses.is_empty() {
break;
}
}
for hyp in hypotheses {
if !hyp.is_complete {
completed.push(hyp);
}
}
completed.sort_by(|a, b| {
b.normalized_score(self.length_penalty)
.partial_cmp(&a.normalized_score(self.length_penalty))
.unwrap_or(std::cmp::Ordering::Equal)
});
completed
.into_iter()
.next()
.map(|h| h.tokens)
.ok_or_else(|| crate::error::WhisperError::Inference("no valid hypothesis".into()))
}
pub(crate) fn log_softmax(&self, logits: &[f32]) -> Vec<f32> {
let scaled: Vec<f32> = if self.temperature > 0.0 {
logits.iter().map(|&x| x / self.temperature).collect()
} else {
logits.to_vec()
};
let max_val = scaled.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
if !max_val.is_finite() {
let uniform = -(logits.len() as f32).ln();
return vec![uniform; logits.len()];
}
let log_sum_exp = scaled
.iter()
.map(|&x| (x - max_val).exp())
.sum::<f32>()
.ln()
+ max_val;
let log_probs: Vec<f32> = scaled.iter().map(|&x| x - log_sum_exp).collect();
debug_assert_eq!(
log_probs.len(),
logits.len(),
"log_softmax output must match input length"
);
debug_assert!(
log_probs.iter().all(|x| !x.is_nan() && *x != f32::INFINITY),
"log probabilities must not be NaN or +inf"
);
log_probs
}
pub(crate) fn top_k_indices(values: &[f32], k: usize) -> Vec<(u32, f32)> {
let mut indexed: Vec<(usize, f32)> =
values.iter().enumerate().map(|(i, &v)| (i, v)).collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed
.into_iter()
.take(k)
.map(|(i, v)| (i as u32, v))
.collect()
}
pub(crate) fn should_stop_early(
&self,
completed: &[Hypothesis],
candidates: &[Hypothesis],
) -> bool {
if completed.is_empty() || candidates.is_empty() {
return false;
}
let best_completed = completed
.iter()
.map(|h| h.normalized_score(self.length_penalty))
.fold(f32::NEG_INFINITY, f32::max);
let worst_candidate = candidates
.iter()
.map(|h| h.normalized_score(self.length_penalty))
.fold(f32::INFINITY, f32::min);
best_completed > worst_candidate * self.patience
}
pub fn decode_nbest<F>(
&self,
mut logits_fn: F,
initial_tokens: &[u32],
eot_token: u32,
n: usize,
) -> WhisperResult<Vec<Vec<u32>>>
where
F: FnMut(&[u32]) -> WhisperResult<Vec<f32>>,
{
let eot = eot_token;
let mut hypotheses = vec![Hypothesis::new(initial_tokens.to_vec(), 0.0)];
let mut completed: Vec<Hypothesis> = Vec::new();
loop {
let min_len = hypotheses
.iter()
.map(|h| h.tokens.len())
.min()
.unwrap_or(self.max_tokens);
if min_len >= self.max_tokens {
break;
}
let mut all_candidates: Vec<Hypothesis> = Vec::new();
for hyp in &hypotheses {
if hyp.is_complete || hyp.tokens.len() >= self.max_tokens {
continue;
}
let logits = logits_fn(&hyp.tokens)?;
let log_probs = self.log_softmax(&logits);
let top_k = Self::top_k_indices(&log_probs, self.beam_size);
for (token, log_prob) in top_k {
let mut new_tokens = hyp.tokens.clone();
new_tokens.push(token);
let mut new_hyp = Hypothesis::new(new_tokens, hyp.score + log_prob);
if token == eot {
new_hyp.is_complete = true;
completed.push(new_hyp);
} else {
all_candidates.push(new_hyp);
}
}
}
all_candidates.sort_by(|a, b| {
b.normalized_score(self.length_penalty)
.partial_cmp(&a.normalized_score(self.length_penalty))
.unwrap_or(std::cmp::Ordering::Equal)
});
hypotheses = all_candidates.into_iter().take(self.beam_size).collect();
if hypotheses.is_empty() {
break;
}
}
for hyp in hypotheses {
if !hyp.is_complete {
completed.push(hyp);
}
}
completed.sort_by(|a, b| {
b.normalized_score(self.length_penalty)
.partial_cmp(&a.normalized_score(self.length_penalty))
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(completed.into_iter().take(n).map(|h| h.tokens).collect())
}
}
impl Default for BeamSearchDecoder {
fn default() -> Self {
Self::new(5, 448)
}
}