1use std::fmt;
6
7use candle_core::{D, Device, IndexOp, Tensor};
8use candle_nn::VarBuilder;
9use candle_transformers::models::whisper::{self, Config, audio, model::Whisper};
10use tokenizers::Tokenizer;
11
12use crate::engine::EngineError;
13use crate::{LocalModel, ModelFolder};
14
15pub const HEARING_RATE: usize = whisper::SAMPLE_RATE;
17
18#[derive(Clone, Debug, PartialEq)]
20pub struct Transcribed {
21 pub text: String,
23 pub tokens: usize,
25 pub seconds: f64,
27}
28
29pub struct Transcriber {
31 model: Whisper,
32 tokenizer: Tokenizer,
33 filters: Vec<f32>,
34 suppressed: Tensor,
35 device: Device,
36 start: u32,
37 transcribe: u32,
38 no_timestamps: u32,
39 end: u32,
40}
41
42impl fmt::Debug for Transcriber {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.debug_struct("Transcriber").field("device", &self.device).finish_non_exhaustive()
45 }
46}
47
48impl Transcriber {
49 pub fn load(folder: &ModelFolder, model: &LocalModel) -> Result<Self, EngineError> {
55 let load = |why: String| EngineError::Load(why);
56 let named = |name: &str| {
57 let file = model.files.iter().find(|file| file.name == name);
58 file.map(|file| folder.path_of(model, file)).ok_or_else(|| load(format!("no {name}")))
59 };
60 let device = Device::Cpu;
61 let config: Config =
62 serde_json::from_slice(&std::fs::read(named("config.json")?).map_err(|e| load(e.to_string()))?)
63 .map_err(|e| load(e.to_string()))?;
64 let tensors = candle_core::safetensors::load(named("model.safetensors")?, &device)
65 .map_err(|e| load(e.to_string()))?;
66 let weights = VarBuilder::from_tensors(tensors, whisper::DTYPE, &device);
67 let tokenizer = Tokenizer::from_file(named("tokenizer.json")?).map_err(|e| load(e.to_string()))?;
68 let token = |text: &str| tokenizer.token_to_id(text).ok_or_else(|| load(format!("no token {text}")));
69 let (start, transcribe) = (token(whisper::SOT_TOKEN)?, token(whisper::TRANSCRIBE_TOKEN)?);
70 let (no_timestamps, end) = (token(whisper::NO_TIMESTAMPS_TOKEN)?, token(whisper::EOT_TOKEN)?);
71 let mask: Vec<f32> = (0..config.vocab_size as u32)
72 .map(|id| if config.suppress_tokens.contains(&id) { f32::NEG_INFINITY } else { 0.0 })
73 .collect();
74 let suppressed = Tensor::new(mask.as_slice(), &device).map_err(|e| load(e.to_string()))?;
75 let filters = mel_filters(config.num_mel_bins);
76 let model = Whisper::load(&weights, config).map_err(|e| load(e.to_string()))?;
77 Ok(Self { model, tokenizer, filters, suppressed, device, start, transcribe, no_timestamps, end })
78 }
79
80 pub fn transcribe(&mut self, samples: &[f32], language: &str) -> Result<Transcribed, EngineError> {
87 let started = std::time::Instant::now();
88 let run = |why: candle_core::Error| EngineError::Run(why.to_string());
89 let spoken = self
90 .tokenizer
91 .token_to_id(&format!("<|{language}|>"))
92 .ok_or_else(|| EngineError::Run(format!("Whisper does not know the language {language}")))?;
93 let prompt = [self.start, spoken, self.transcribe, self.no_timestamps];
94 let bins = self.model.config.num_mel_bins;
95 let most = self.model.config.max_target_positions / 2;
96 let (mut text, mut written) = (String::new(), 0);
97 for chunk in samples.chunks(whisper::N_SAMPLES) {
98 let mut padded = chunk.to_vec();
99 padded.resize(whisper::N_SAMPLES, 0.0);
100 let mel = audio::pcm_to_mel(&self.model.config, &padded, &self.filters);
101 let frames = mel.len() / bins;
102 let mel = Tensor::from_vec(mel, (1, bins, frames), &self.device)
103 .and_then(|mel| mel.narrow(2, 0, frames.min(whisper::N_FRAMES)))
104 .map_err(run)?;
105 let heard = self.model.encoder.forward(&mel, true).map_err(run)?;
106 let mut tokens = prompt.to_vec();
107 for step in 0..most {
108 let input =
109 Tensor::new(tokens.as_slice(), &self.device).and_then(|t| t.unsqueeze(0)).map_err(run)?;
110 let output = self.model.decoder.forward(&input, &heard, step == 0).map_err(run)?;
111 let last = output.dim(1).map_err(run)? - 1;
112 let next = output
113 .i((..1, last..))
114 .and_then(|last| self.model.decoder.final_linear(&last))
115 .and_then(|logits| logits.i(0)?.i(0)?.broadcast_add(&self.suppressed))
116 .and_then(|logits| logits.argmax(D::Minus1)?.to_scalar::<u32>())
117 .map_err(run)?;
118 if next == self.end {
119 break;
120 }
121 tokens.push(next);
122 }
123 written += tokens.len() - prompt.len();
124 let words = self
125 .tokenizer
126 .decode(&tokens[prompt.len()..], true)
127 .map_err(|e| EngineError::Run(e.to_string()))?;
128 text.push_str(words.trim());
129 text.push(' ');
130 }
131 Ok(Transcribed {
132 text: text.trim().to_owned(),
133 tokens: written,
134 seconds: started.elapsed().as_secs_f64(),
135 })
136 }
137}
138
139fn mel_filters(bins: usize) -> Vec<f32> {
142 const MIN_LOG_HZ: f64 = 1000.0;
143 const HZ_PER_MEL: f64 = 200.0 / 3.0;
144 let min_log_mel = MIN_LOG_HZ / HZ_PER_MEL;
145 let log_step = 6.4_f64.ln() / 27.0;
146 let to_mel = |hz: f64| {
147 if hz >= MIN_LOG_HZ { min_log_mel + (hz / MIN_LOG_HZ).ln() / log_step } else { hz / HZ_PER_MEL }
148 };
149 let to_hz = |mel: f64| {
150 if mel >= min_log_mel {
151 MIN_LOG_HZ * (log_step * (mel - min_log_mel)).exp()
152 } else {
153 HZ_PER_MEL * mel
154 }
155 };
156 let rate = HEARING_RATE as f64;
157 let columns = whisper::N_FFT / 2 + 1;
158 let top = to_mel(rate / 2.0);
159 let edges: Vec<f64> = (0..bins + 2).map(|i| to_hz(top * i as f64 / (bins + 1) as f64)).collect();
160 let mut filters = Vec::with_capacity(bins * columns);
161 for band in 0..bins {
162 let (low, middle, high) = (edges[band], edges[band + 1], edges[band + 2]);
163 let area = 2.0 / (high - low);
164 for column in 0..columns {
165 let hz = column as f64 * rate / whisper::N_FFT as f64;
166 let rising = (hz - low) / (middle - low);
167 let falling = (high - hz) / (high - middle);
168 filters.push((rising.min(falling).max(0.0) * area) as f32);
169 }
170 }
171 filters
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn mel_filters_are_triangles_over_the_spectrum() {
180 let filters = mel_filters(80);
181 assert_eq!(filters.len(), 80 * 201);
182 for band in filters.chunks(201) {
183 assert!(band.iter().all(|weight| *weight >= 0.0));
184 assert!(band.iter().any(|weight| *weight > 0.0), "every band hears something");
185 }
186 assert_eq!(filters[0], 0.0);
188 assert!(filters[1] > 0.0);
189 }
190
191 #[test]
193 fn mel_filters_match_the_published_ones() {
194 let Some(path) = std::env::var_os("WHISPER_MEL_FILTERS") else { return };
195 let bytes = std::fs::read(path).unwrap_or_default();
196 let published: Vec<f32> = bytes.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
197 let ours = mel_filters(80);
198 assert_eq!(published.len(), ours.len());
199 let worst = published.iter().zip(&ours).map(|(a, b)| (a - b).abs()).fold(0.0, f32::max);
200 assert!(worst < 1e-6, "differs by {worst}");
201 }
202}