#![cfg(feature = "whisper")]
use std::{path::PathBuf, time::Instant};
use mlxrs::{
Array, Dtype,
audio::{
io::load_audio,
stt::{
model::{AutoregressiveStt, Transcribe, TranscribeOptions},
models::whisper::{audio::pad_or_trim, config::ModelDimensions, model::WhisperModel},
},
},
lm::quant::{PerLayerQuantization, Quantization},
tokenizer::Tokenizer,
transforms,
};
fn model_dir() -> PathBuf {
PathBuf::from("/private/tmp/whisper-turbo-8bit")
}
fn audio_dir() -> PathBuf {
PathBuf::from("/private/tmp/whisper-bench-audio")
}
fn load_quant_model() -> WhisperModel {
let dir = model_dir();
let cfg_bytes = std::fs::read(dir.join("config.json")).expect("read config.json");
let cfg: serde_json::Value = serde_json::from_slice(&cfg_bytes).expect("parse config.json");
let dims = ModelDimensions::from_dict(&cfg).expect("ModelDimensions");
let q = cfg
.get("quantization")
.map(|qv| {
Quantization::affine(
qv.get("group_size").and_then(|v| v.as_i64()).unwrap_or(64) as i32,
qv.get("bits").and_then(|v| v.as_i64()).unwrap_or(8) as i32,
)
})
.map(PerLayerQuantization::from_global);
let model =
WhisperModel::load_quantized(&dir, dims, Dtype::F16, q.as_ref()).expect("load_quantized");
let tokenizer = Tokenizer::from_path(&dir, None).expect("tokenizer");
model.with_tokenizer(tokenizer).expect("attach tokenizer")
}
fn load_wav(name: &str) -> Array {
let path = audio_dir().join(name);
let (samples, sr) = load_audio(&path).unwrap_or_else(|e| panic!("load_audio {name}: {e}"));
assert_eq!(sr, 16_000);
Array::from_slice::<f32>(&samples, &[samples.len() as i32]).expect("waveform")
}
#[test]
#[ignore = "quantized A/B probe"]
fn quant_ab() {
let model = load_quant_model();
println!("model.dtype() = {:?}", model.dtype());
let n: usize = 16_000 * 30;
let samples: Vec<f32> = (0..n)
.map(|i| (i as f32 * 2.0 * std::f32::consts::PI * 440.0 / 16_000.0).sin() * 0.1)
.collect();
let synth = Array::from_slice::<f32>(&samples, &[n as i32]).expect("synth");
let mel = model.log_mel(&synth).expect("log_mel");
let mel_win = pad_or_trim(&mel, 3000, 0).expect("pad");
let enc = model.encode(&mel_win).expect("encode");
println!(
"[dtype] mel={:?} encoder_out={:?}",
mel.dtype().unwrap(),
enc.dtype().unwrap()
);
for _ in 0..2 {
let e = model.encode(&mel_win).expect("warm encode");
transforms::eval(&[&e]).expect("eval");
}
let t = Instant::now();
for _ in 0..5 {
let e = model.encode(&mel_win).expect("timed encode");
transforms::eval(&[&e]).expect("eval");
}
println!(
"[encode-kernel] {:.4}s / window (5-rep avg)",
t.elapsed().as_secs_f64() / 5.0
);
let fixtures: &[(&str, f64)] = &[
("test_3speakers.wav", 35.319),
("long_dialog_227s.wav", 226.96),
];
let default_opts = TranscribeOptions::new();
let nofb_opts = TranscribeOptions::new()
.with_temperature(0.0)
.with_compression_ratio_threshold(None)
.with_logprob_threshold(None)
.with_no_speech_threshold(None);
let _ = model
.transcribe(&load_wav(fixtures[0].0), &default_opts)
.expect("warmup");
for (label, opts) in [("default", &default_opts), ("no-fallback", &nofb_opts)] {
for (name, dur) in fixtures {
let audio = load_wav(name);
let t = Instant::now();
let out = model
.transcribe(&audio, opts)
.unwrap_or_else(|e| panic!("{name}: {e}"));
let wall = t.elapsed().as_secs_f64();
println!(
"[{label}] {name} | dur={dur:.0}s | wall={wall:.2}s | rtf={:.4} | chars={} | lang={:?}",
wall / dur,
out.text().chars().count(),
out.language()
);
}
}
}