#[cfg(test)]
mod tests;
use crate::error::WhisperResult;
use crate::tokenizer::special_tokens;
#[derive(Debug, Clone)]
pub struct LanguageProbs {
pub languages: Vec<String>,
pub probabilities: Vec<f32>,
}
impl LanguageProbs {
#[must_use]
pub fn from_logits(logits: &[f32]) -> Self {
let lang_logits: Vec<(String, f32)> = SUPPORTED_LANGUAGES
.iter()
.enumerate()
.filter_map(|(offset, &lang)| {
let token_id = special_tokens::LANG_BASE + offset as u32;
logits
.get(token_id as usize)
.map(|&logit| (lang.to_string(), logit))
})
.collect();
let max_logit = lang_logits
.iter()
.map(|(_, l)| *l)
.fold(f32::NEG_INFINITY, f32::max);
let exp_sum: f32 = lang_logits.iter().map(|(_, l)| (l - max_logit).exp()).sum();
let mut probs: Vec<(String, f32)> = lang_logits
.iter()
.map(|(lang, logit)| (lang.clone(), (logit - max_logit).exp() / exp_sum))
.collect();
probs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
Self {
languages: probs.iter().map(|(l, _)| l.clone()).collect(),
probabilities: probs.iter().map(|(_, p)| *p).collect(),
}
}
#[must_use]
pub fn top_language(&self) -> Option<&str> {
self.languages.first().map(String::as_str)
}
#[must_use]
pub fn top_probability(&self) -> Option<f32> {
self.probabilities.first().copied()
}
#[must_use]
pub fn confidence(&self) -> f32 {
self.top_probability().unwrap_or(0.0)
}
#[must_use]
pub fn is_confident(&self, threshold: f32) -> bool {
self.confidence() >= threshold
}
#[must_use]
pub fn top_n(&self, n: usize) -> Vec<(&str, f32)> {
self.languages
.iter()
.zip(self.probabilities.iter())
.take(n)
.map(|(l, &p)| (l.as_str(), p))
.collect()
}
#[must_use]
pub fn probability_for(&self, language: &str) -> Option<f32> {
self.languages
.iter()
.position(|l| l == language)
.and_then(|idx| self.probabilities.get(idx).copied())
}
}
impl Default for LanguageProbs {
fn default() -> Self {
Self {
languages: vec!["en".to_string()],
probabilities: vec![1.0],
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LanguageDetector {
confidence_threshold: f32,
}
impl LanguageDetector {
#[must_use]
pub const fn new() -> Self {
Self {
confidence_threshold: 0.5,
}
}
#[must_use]
pub const fn with_threshold(threshold: f32) -> Self {
Self {
confidence_threshold: threshold,
}
}
#[must_use]
pub const fn confidence_threshold(&self) -> f32 {
self.confidence_threshold
}
#[must_use]
pub fn detect_from_logits(&self, logits: &[f32]) -> LanguageProbs {
LanguageProbs::from_logits(logits)
}
pub fn detect<F>(&self, mut logits_fn: F) -> WhisperResult<LanguageProbs>
where
F: FnMut(&[u32]) -> WhisperResult<Vec<f32>>,
{
let logits = logits_fn(&[special_tokens::SOT])?;
Ok(self.detect_from_logits(&logits))
}
#[must_use]
pub fn is_confident(&self, probs: &LanguageProbs) -> bool {
probs.is_confident(self.confidence_threshold)
}
}
impl Default for LanguageDetector {
fn default() -> Self {
Self::new()
}
}
pub const SUPPORTED_LANGUAGES: &[&str] = &[
"en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", "ca", "nl", "ar", "sv", "it", "id", "hi", "fi", "vi", "he", "uk", "el", "ms", "cs", "ro", "da", "hu", "ta", "no", "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy", "sk", "te", "fa", "lv", "bn", "sr", "az", "sl", "kn", "et", "mk", "br", "eu", "is", "hy", "ne", "mn", "bs", "kk", "sq", "sw", "gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af", "oc", "ka", "be", "tg", "sd", "gu", "am", "yi", "lo", "uz", "fo", "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo", "tl", "mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", "su", ];
const LANGUAGE_NAMES: &[&str] = &[
"English",
"Chinese",
"German",
"Spanish",
"Russian",
"Korean",
"French",
"Japanese",
"Portuguese",
"Turkish",
"Polish",
"Catalan",
"Dutch",
"Arabic",
"Swedish",
"Italian",
"Indonesian",
"Hindi",
"Finnish",
"Vietnamese",
"Hebrew",
"Ukrainian",
"Greek",
"Malay",
"Czech",
"Romanian",
"Danish",
"Hungarian",
"Tamil",
"Norwegian",
"Thai",
"Urdu",
"Croatian",
"Bulgarian",
"Lithuanian",
"Latin",
"Maori",
"Malayalam",
"Welsh",
"Slovak",
"Telugu",
"Persian",
"Latvian",
"Bengali",
"Serbian",
"Azerbaijani",
"Slovenian",
"Kannada",
"Estonian",
"Macedonian",
"Breton",
"Basque",
"Icelandic",
"Armenian",
"Nepali",
"Mongolian",
"Bosnian",
"Kazakh",
"Albanian",
"Swahili",
"Galician",
"Marathi",
"Punjabi",
"Sinhala",
"Khmer",
"Shona",
"Yoruba",
"Somali",
"Afrikaans",
"Occitan",
"Georgian",
"Belarusian",
"Tajik",
"Sindhi",
"Gujarati",
"Amharic",
"Yiddish",
"Lao",
"Uzbek",
"Faroese",
"Haitian Creole",
"Pashto",
"Turkmen",
"Norwegian Nynorsk",
"Maltese",
"Sanskrit",
"Luxembourgish",
"Myanmar",
"Tibetan",
"Tagalog",
"Malagasy",
"Assamese",
"Tatar",
"Hawaiian",
"Lingala",
"Hausa",
"Bashkir",
"Javanese",
"Sundanese",
];
#[must_use]
pub fn language_name(code: &str) -> Option<&'static str> {
SUPPORTED_LANGUAGES
.iter()
.position(|&c| c == code)
.map(|i| LANGUAGE_NAMES[i])
}
#[must_use]
pub fn is_supported(code: &str) -> bool {
SUPPORTED_LANGUAGES.contains(&code)
}
#[must_use]
pub fn language_index(code: &str) -> Option<usize> {
SUPPORTED_LANGUAGES.iter().position(|&l| l == code)
}