use std::path::{Path, PathBuf};
use serde::Deserialize;
use splintr::{AnyTokenizer, PretrainedVocab, WhisperVariant, from_json_path, from_vocab};
use crate::error::{Error, Result};
use boostr::model::audio::whisper_loader::WhisperCheckpoint;
use boostr::model::audio::whisper_model::{GenerateOptions, WhisperModel};
use boostr::model::config::AudioConfig;
use numr::dtype::DType;
use numr::ops::TypeConversionOps;
use numr::runtime::Runtime;
pub struct WhisperBundle<R: Runtime> {
pub model: WhisperModel<R>,
pub tokenizer: AnyTokenizer,
pub variant: WhisperVariant,
pub config: AudioConfig,
pub num_mel_bins: usize,
pub generation: WhisperGenerationConfig,
pub checkpoint_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct WhisperGenerationConfig {
pub suppress_tokens: Vec<u32>,
pub begin_suppress_tokens: Vec<u32>,
pub eos_token_ids: Vec<u32>,
pub max_length: usize,
pub alignment_heads: Option<Vec<(usize, usize)>>,
pub median_filter_width: usize,
}
const DEFAULT_MEDIAN_FILTER_WIDTH: usize = 7;
const WHISPER_MAX_LENGTH: usize = 448;
impl WhisperGenerationConfig {
pub fn for_variant(variant: WhisperVariant) -> Self {
Self {
suppress_tokens: Vec::new(),
begin_suppress_tokens: Vec::new(),
eos_token_ids: vec![variant.eos_token_id()],
max_length: WHISPER_MAX_LENGTH,
alignment_heads: None,
median_filter_width: DEFAULT_MEDIAN_FILTER_WIDTH,
}
}
}
impl<R: Runtime<DType = DType>> WhisperBundle<R> {
pub fn from_dir<P: AsRef<Path>>(dir: P, device: &R::Device) -> Result<Self> {
let dir = dir.as_ref();
let checkpoint = WhisperModel::<R>::from_checkpoint(dir, device)?;
Self::wrap(dir, checkpoint)
}
pub fn from_dir_with_dtype<P: AsRef<Path>, C: TypeConversionOps<R>>(
dir: P,
device: &R::Device,
client: &C,
dtype: DType,
) -> Result<Self> {
let dir = dir.as_ref();
let checkpoint = WhisperModel::<R>::from_checkpoint_with_dtype(dir, device, client, dtype)?;
Self::wrap(dir, checkpoint)
}
fn wrap(dir: &Path, checkpoint: WhisperCheckpoint<R>) -> Result<Self> {
let WhisperCheckpoint {
model,
config,
variant,
num_mel_bins,
} = checkpoint;
let tokenizer = match whisper_pretrained_vocab(variant) {
Some(vocab) => from_vocab(vocab).map_err(|e| Error::ModelError {
reason: format!("loading bundled {variant:?} whisper tokenizer: {e}"),
})?,
None => {
let tok_path = dir.join("tokenizer.json");
from_json_path(&tok_path).map_err(|e| Error::ModelError {
reason: format!("loading {}: {e}", tok_path.display()),
})?
}
};
let generation = load_generation_config(dir, variant)?;
Ok(Self {
model,
tokenizer,
variant,
config,
num_mel_bins,
generation,
checkpoint_dir: dir.to_path_buf(),
})
}
pub fn generate_options(&self) -> GenerateOptions {
let mut eos_token_ids = self.generation.eos_token_ids.clone();
let variant_eos = self.variant.eos_token_id();
if eos_token_ids.is_empty() {
eos_token_ids.push(variant_eos);
}
let prefix_len = self.sot_prompt(Some("en"), false).len();
GenerateOptions {
max_new_tokens: self.generation.max_length.saturating_sub(prefix_len),
eos_token_ids,
suppress_tokens: self.generation.suppress_tokens.clone(),
begin_suppress_tokens: self.generation.begin_suppress_tokens.clone(),
}
}
pub fn language_token_ids(&self) -> Vec<u32> {
language_token_ids(self.variant)
}
pub fn language_code(&self, token_id: u32) -> Option<&'static str> {
language_code(self.variant, token_id)
}
pub fn sot_prompt(&self, language: Option<&str>, translate: bool) -> Vec<u32> {
let mut out = vec![self.variant.sot_token_id()];
if let Some(code) = language
&& let Some(id) = self.variant.language_token_id(code)
{
out.push(id);
}
if translate {
out.push(self.variant.translate_token_id());
} else {
out.push(self.variant.transcribe_token_id());
}
out.push(self.variant.notimestamps_token_id());
out
}
}
#[derive(Debug, Deserialize)]
struct HfGenerationConfig {
#[serde(default)]
suppress_tokens: Option<Vec<u32>>,
#[serde(default)]
begin_suppress_tokens: Option<Vec<u32>>,
#[serde(default)]
eos_token_id: Option<TokenIdField>,
#[serde(default)]
max_length: Option<usize>,
#[serde(default)]
alignment_heads: Option<Vec<[usize; 2]>>,
}
#[derive(Debug, Deserialize)]
struct HfModelConfig {
#[serde(default)]
median_filter_width: Option<usize>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum TokenIdField {
One(u32),
Many(Vec<u32>),
}
impl TokenIdField {
fn into_vec(self) -> Vec<u32> {
match self {
Self::One(id) => vec![id],
Self::Many(ids) => ids,
}
}
}
fn load_generation_config(dir: &Path, variant: WhisperVariant) -> Result<WhisperGenerationConfig> {
let median_filter_width = read_median_filter_width(dir)?;
let path = dir.join("generation_config.json");
if !path.exists() {
return Ok(WhisperGenerationConfig {
median_filter_width,
..WhisperGenerationConfig::for_variant(variant)
});
}
let bytes = std::fs::read(&path).map_err(|e| Error::ModelError {
reason: format!("reading {}: {e}", path.display()),
})?;
let raw: HfGenerationConfig =
serde_json::from_slice(&bytes).map_err(|e| Error::ModelError {
reason: format!("parsing {}: {e}", path.display()),
})?;
let eos_token_ids = raw
.eos_token_id
.map(TokenIdField::into_vec)
.unwrap_or_else(|| vec![variant.eos_token_id()]);
let alignment_heads = raw
.alignment_heads
.map(|pairs| pairs.into_iter().map(|[l, h]| (l, h)).collect());
Ok(WhisperGenerationConfig {
suppress_tokens: raw.suppress_tokens.unwrap_or_default(),
begin_suppress_tokens: raw.begin_suppress_tokens.unwrap_or_default(),
eos_token_ids,
max_length: raw.max_length.unwrap_or(WHISPER_MAX_LENGTH),
alignment_heads,
median_filter_width,
})
}
fn read_median_filter_width(dir: &Path) -> Result<usize> {
let path = dir.join("config.json");
if !path.exists() {
return Ok(DEFAULT_MEDIAN_FILTER_WIDTH);
}
let bytes = std::fs::read(&path).map_err(|e| Error::ModelError {
reason: format!("reading {}: {e}", path.display()),
})?;
let raw: HfModelConfig = serde_json::from_slice(&bytes).map_err(|e| Error::ModelError {
reason: format!("parsing {}: {e}", path.display()),
})?;
Ok(raw
.median_filter_width
.unwrap_or(DEFAULT_MEDIAN_FILTER_WIDTH))
}
fn language_token_ids(variant: WhisperVariant) -> Vec<u32> {
if variant == WhisperVariant::EnglishOnly {
return Vec::new();
}
let first = variant.sot_token_id() + 1;
(0..variant.languages().len() as u32)
.map(|i| first + i)
.collect()
}
fn language_code(variant: WhisperVariant, token_id: u32) -> Option<&'static str> {
let first = variant.sot_token_id() + 1;
let index = token_id.checked_sub(first)? as usize;
variant.languages().get(index).copied()
}
fn whisper_pretrained_vocab(variant: WhisperVariant) -> Option<PretrainedVocab> {
match variant {
WhisperVariant::V1Multilingual => Some(PretrainedVocab::WhisperV1),
WhisperVariant::V2Multilingual => Some(PretrainedVocab::WhisperV2),
WhisperVariant::V3Multilingual => Some(PretrainedVocab::WhisperV3),
WhisperVariant::EnglishOnly => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generation_config_defaults_when_file_missing() {
let dir = Path::new("/nonexistent-whisper-checkpoint");
let defaults = load_generation_config(dir, WhisperVariant::V2Multilingual)
.expect("missing generation_config.json must not be an error");
assert!(defaults.suppress_tokens.is_empty());
assert!(defaults.begin_suppress_tokens.is_empty());
assert_eq!(defaults.eos_token_ids, vec![50257]);
assert_eq!(defaults.max_length, 448);
assert!(defaults.alignment_heads.is_none());
assert_eq!(defaults.median_filter_width, 7);
}
#[test]
fn language_block_round_trips_through_ids() {
for variant in [
WhisperVariant::V1Multilingual,
WhisperVariant::V2Multilingual,
WhisperVariant::V3Multilingual,
] {
let ids = language_token_ids(variant);
assert_eq!(ids.len(), variant.languages().len());
assert_eq!(ids[0], variant.sot_token_id() + 1);
assert_eq!(ids.last().copied(), Some(variant.translate_token_id() - 1));
for (id, code) in ids.iter().zip(variant.languages()) {
assert_eq!(language_code(variant, *id), Some(*code));
assert_eq!(variant.language_token_id(code), Some(*id));
}
assert_eq!(language_code(variant, variant.sot_token_id()), None);
assert_eq!(language_code(variant, variant.translate_token_id()), None);
}
assert!(language_token_ids(WhisperVariant::EnglishOnly).is_empty());
}
#[test]
fn generation_config_parses_scalar_and_list_eos() {
let scalar: HfGenerationConfig = serde_json::from_str(
r#"{"suppress_tokens":[1,2],"begin_suppress_tokens":[220,50257],
"eos_token_id":50257,"max_length":448,
"forced_decoder_ids":[[1,null],[2,50359]]}"#,
)
.expect("scalar eos_token_id");
assert_eq!(scalar.suppress_tokens.as_deref(), Some(&[1u32, 2][..]));
assert_eq!(
scalar.eos_token_id.map(TokenIdField::into_vec),
Some(vec![50257])
);
assert_eq!(scalar.max_length, Some(448));
let list: HfGenerationConfig =
serde_json::from_str(r#"{"eos_token_id":[50257,50362]}"#).expect("list eos_token_id");
assert_eq!(
list.eos_token_id.map(TokenIdField::into_vec),
Some(vec![50257, 50362])
);
assert_eq!(list.max_length, None);
}
#[test]
fn generation_config_parses_alignment_heads() {
let raw: HfGenerationConfig =
serde_json::from_str(r#"{"alignment_heads":[[2,2],[3,0],[3,2],[3,3],[3,4],[3,5]]}"#)
.expect("alignment_heads");
assert_eq!(
raw.alignment_heads,
Some(vec![[2, 2], [3, 0], [3, 2], [3, 3], [3, 4], [3, 5]])
);
}
#[test]
fn model_config_median_filter_width_falls_back_to_hf_default() {
let raw: HfModelConfig = serde_json::from_str(r#"{"hidden_size": 384}"#)
.expect("config.json without median_filter_width");
assert_eq!(
raw.median_filter_width
.unwrap_or(DEFAULT_MEDIAN_FILTER_WIDTH),
7
);
let raw: HfModelConfig = serde_json::from_str(r#"{"median_filter_width": 9}"#)
.expect("config.json with median_filter_width");
assert_eq!(
raw.median_filter_width
.unwrap_or(DEFAULT_MEDIAN_FILTER_WIDTH),
9
);
}
}