use crate::error::{WhisperError, WhisperResult};
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap, string::String, vec::Vec};
#[cfg(feature = "std")]
use std::collections::BTreeMap;
pub mod special_tokens {
pub const PAD: u32 = 0;
pub const BOS: u32 = 1;
pub const EOS: u32 = 2;
pub const UNK: u32 = 3;
}
pub const MOONSHINE_VOCAB_SIZE: usize = 32768;
#[derive(Debug, Clone)]
pub struct SentencePieceTokenizer {
id_to_piece: Vec<String>,
piece_to_id: BTreeMap<String, u32>,
vocab_size: usize,
}
impl SentencePieceTokenizer {
#[must_use]
pub fn new(vocab_size: usize) -> Self {
Self {
id_to_piece: Vec::with_capacity(vocab_size),
piece_to_id: BTreeMap::new(),
vocab_size,
}
}
#[must_use]
pub fn moonshine_default() -> Self {
let mut tokenizer = Self::new(MOONSHINE_VOCAB_SIZE);
tokenizer.add_piece(special_tokens::PAD, "<pad>");
tokenizer.add_piece(special_tokens::BOS, "<s>");
tokenizer.add_piece(special_tokens::EOS, "</s>");
tokenizer.add_piece(special_tokens::UNK, "<unk>");
tokenizer
}
pub fn add_piece(&mut self, id: u32, piece: &str) {
let id_usize = id as usize;
while self.id_to_piece.len() <= id_usize {
self.id_to_piece.push(String::new());
}
self.id_to_piece[id_usize] = String::from(piece);
self.piece_to_id.insert(String::from(piece), id);
}
pub fn encode(&self, text: &str) -> WhisperResult<Vec<u32>> {
if text.is_empty() {
return Ok(Vec::new());
}
let mut tokens = Vec::new();
let mut pos = 0;
let bytes = text.as_bytes();
while pos < bytes.len() {
let mut matched = false;
let max_len = (bytes.len() - pos).min(64);
for len in (1..=max_len).rev() {
if let Ok(piece) = core::str::from_utf8(&bytes[pos..pos + len]) {
if let Some(&id) = self.piece_to_id.get(piece) {
tokens.push(id);
pos += len;
matched = true;
break;
}
}
}
if !matched {
tokens.push(special_tokens::UNK);
pos += 1;
}
}
Ok(tokens)
}
pub fn decode(&self, tokens: &[u32]) -> WhisperResult<String> {
let mut text = String::new();
for &id in tokens {
let id_usize = id as usize;
if id_usize >= self.id_to_piece.len() {
return Err(WhisperError::Tokenizer(format!(
"token ID {id} out of range (max {})",
self.id_to_piece.len()
)));
}
let piece = &self.id_to_piece[id_usize];
if id <= special_tokens::UNK {
continue;
}
text.push_str(&piece.replace('\u{2581}', " "));
}
Ok(text.trim_start().to_string())
}
#[must_use]
pub fn vocab_size(&self) -> usize {
self.vocab_size
}
#[must_use]
pub fn loaded_pieces(&self) -> usize {
self.piece_to_id.len()
}
#[must_use]
pub const fn bos_id(&self) -> u32 {
special_tokens::BOS
}
#[must_use]
pub const fn eos_id(&self) -> u32 {
special_tokens::EOS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sentencepiece_new() {
let sp = SentencePieceTokenizer::new(32768);
assert_eq!(sp.vocab_size(), 32768);
assert_eq!(sp.loaded_pieces(), 0);
}
#[test]
fn test_sentencepiece_moonshine_default() {
let sp = SentencePieceTokenizer::moonshine_default();
assert_eq!(sp.vocab_size(), MOONSHINE_VOCAB_SIZE);
assert_eq!(sp.loaded_pieces(), 4); assert_eq!(sp.bos_id(), 1);
assert_eq!(sp.eos_id(), 2);
}
#[test]
fn test_sentencepiece_add_and_encode() {
let mut sp = SentencePieceTokenizer::moonshine_default();
sp.add_piece(100, "hello");
sp.add_piece(101, "world");
sp.add_piece(102, " ");
let tokens = sp.encode("hello world").expect("should encode");
assert_eq!(tokens, vec![100, 102, 101]);
}
#[test]
fn test_sentencepiece_decode() {
let mut sp = SentencePieceTokenizer::moonshine_default();
sp.add_piece(100, "hello");
sp.add_piece(101, " world");
let text = sp.decode(&[100, 101]).expect("should decode");
assert_eq!(text, "hello world");
}
#[test]
fn test_sentencepiece_decode_skips_special() {
let mut sp = SentencePieceTokenizer::moonshine_default();
sp.add_piece(100, "hi");
let text = sp
.decode(&[special_tokens::BOS, 100, special_tokens::EOS])
.expect("should decode");
assert_eq!(text, "hi");
}
#[test]
fn test_sentencepiece_encode_empty() {
let sp = SentencePieceTokenizer::moonshine_default();
let tokens = sp.encode("").expect("should encode empty");
assert!(tokens.is_empty());
}
#[test]
fn test_sentencepiece_decode_empty() {
let sp = SentencePieceTokenizer::moonshine_default();
let text = sp.decode(&[]).expect("should decode empty");
assert!(text.is_empty());
}
#[test]
fn test_sentencepiece_unknown_falls_back_to_unk() {
let sp = SentencePieceTokenizer::moonshine_default();
let tokens = sp.encode("abc").expect("should encode");
assert_eq!(tokens, vec![special_tokens::UNK; 3]);
}
#[test]
fn test_sentencepiece_decode_out_of_range() {
let sp = SentencePieceTokenizer::moonshine_default();
let result = sp.decode(&[99999]);
assert!(result.is_err());
}
}