mod components;
use rustc_hash::FxHashMap;
use serde_json::Value;
use thiserror::Error;
use super::byte_level::byte_level_decode;
use super::sentencepiece::{SentencePieceError, SentencePieceTokenizer};
use super::tokenize::{Tokenize, TokenizeError};
use super::tokenizer::{Tokenizer, TokenizerError};
use super::wordpiece::WordPieceTokenizer;
use super::normalizer::Normalizer;
use components::{
find_added_token, parse_bert_norm, parse_norm_ops, parse_pre_tokenizer,
parse_special_decode_ids, parse_special_tokens,
};
#[derive(Debug, Error)]
pub enum HfJsonError {
#[error("failed to parse tokenizer.json: {0}")]
Json(#[from] serde_json::Error),
#[error("tokenizer.json missing field: {0}")]
MissingField(&'static str),
#[error("unsupported model.type `{0}` (expected BPE, Unigram, or WordPiece)")]
UnsupportedModelType(String),
#[error(
"unsupported normalizer type(s) `{0}` — refusing to load rather than silently drop them"
)]
UnsupportedNormalizer(String),
#[error("normalizer Replace pattern `{0}` failed to compile as a regex")]
InvalidNormalizerRegex(String),
#[error("unsupported pre_tokenizer type(s) `{0}` and no recognized split — refusing to guess the split pattern")]
UnsupportedPreTokenizer(String),
#[error("vocab entry `{0}` is not valid byte-level encoding")]
InvalidByteLevel(String),
#[error("could not determine the {0} token id from the tokenizer.json")]
MissingSpecial(&'static str),
#[error(transparent)]
Tokenizer(#[from] TokenizerError),
#[error(transparent)]
SentencePiece(#[from] SentencePieceError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub enum Backend {
Bpe(Tokenizer),
Unigram(SentencePieceTokenizer),
WordPiece(WordPieceTokenizer),
}
impl Backend {
fn token_surface(&self, id: u32) -> Option<String> {
match self {
Backend::Bpe(t) => t.token_surface(id),
Backend::Unigram(t) => t.token_surface(id),
Backend::WordPiece(t) => t.token_surface(id),
}
}
}
#[derive(Default, Clone)]
pub struct PostProcessor {
prefix: Vec<u32>,
suffix: Vec<u32>,
}
impl PostProcessor {
pub fn apply(&self, ids: Vec<u32>) -> Vec<u32> {
if self.prefix.is_empty() && self.suffix.is_empty() {
return ids;
}
let mut out = Vec::with_capacity(self.prefix.len() + ids.len() + self.suffix.len());
out.extend_from_slice(&self.prefix);
out.extend(ids);
out.extend_from_slice(&self.suffix);
out
}
pub fn is_empty(&self) -> bool {
self.prefix.is_empty() && self.suffix.is_empty()
}
}
pub struct AnyTokenizer {
backend: Backend,
post: PostProcessor,
decoder: Option<super::decoder::Decoder>,
special_decode: rustc_hash::FxHashSet<u32>,
}
impl AnyTokenizer {
pub fn family(&self) -> &'static str {
match &self.backend {
Backend::Bpe(_) => "BPE",
Backend::Unigram(_) => "Unigram",
Backend::WordPiece(_) => "WordPiece",
}
}
pub fn backend(&self) -> &Backend {
&self.backend
}
pub fn into_backend(self) -> Backend {
self.backend
}
pub fn post_processor(&self) -> &PostProcessor {
&self.post
}
pub fn encode_with_special_tokens(&self, text: &str) -> Vec<u32> {
self.post.apply(Tokenize::encode(self, text))
}
}
impl Tokenize for AnyTokenizer {
fn encode(&self, text: &str) -> Vec<u32> {
match &self.backend {
Backend::Bpe(t) => Tokenize::encode(t, text),
Backend::Unigram(t) => Tokenize::encode(t, text),
Backend::WordPiece(t) => Tokenize::encode(t, text),
}
}
fn decode(&self, ids: &[u32]) -> Result<String, TokenizeError> {
if let Some(decoder) = &self.decoder {
let surfaces: Vec<String> = ids
.iter()
.filter(|id| !self.special_decode.contains(id))
.filter_map(|&id| self.backend.token_surface(id))
.collect();
return Ok(decoder.decode(surfaces));
}
match &self.backend {
Backend::Bpe(t) => Tokenize::decode(t, ids),
Backend::Unigram(t) => Tokenize::decode(t, ids),
Backend::WordPiece(t) => Tokenize::decode(t, ids),
}
}
fn vocab_size(&self) -> usize {
match &self.backend {
Backend::Bpe(t) => Tokenize::vocab_size(t),
Backend::Unigram(t) => Tokenize::vocab_size(t),
Backend::WordPiece(t) => Tokenize::vocab_size(t),
}
}
}
pub fn from_json_path<P: AsRef<std::path::Path>>(path: P) -> Result<AnyTokenizer, HfJsonError> {
let bytes = std::fs::read(path)?;
from_json_bytes(&bytes)
}
pub fn from_json_bytes(data: &[u8]) -> Result<AnyTokenizer, HfJsonError> {
let root: Value = serde_json::from_slice(data)?;
let model = root
.get("model")
.ok_or(HfJsonError::MissingField("model"))?;
let backend = match model_family(model)? {
"BPE" => build_bpe(&root, model)?,
"Unigram" => build_unigram(&root, model)?,
"WordPiece" => build_wordpiece(&root, model)?,
other => return Err(HfJsonError::UnsupportedModelType(other.to_string())),
};
let post = parse_post_processor(&root);
let decoder = super::decoder::parse(root.get("decoder"));
let special_decode = parse_special_decode_ids(&root);
Ok(AnyTokenizer {
backend,
post,
decoder,
special_decode,
})
}
fn parse_post_processor(root: &Value) -> PostProcessor {
let Some(pp) = root.get("post_processor") else {
return PostProcessor::default();
};
match pp.get("type").and_then(Value::as_str) {
Some("BertProcessing") | Some("RobertaProcessing") => {
let id = |k: &str| pp.get(k).and_then(|p| p.get(1)).and_then(Value::as_u64);
PostProcessor {
prefix: id("cls").map(|n| vec![n as u32]).unwrap_or_default(),
suffix: id("sep").map(|n| vec![n as u32]).unwrap_or_default(),
}
}
Some("TemplateProcessing") => parse_template_processing(pp),
Some("Sequence") => {
let mut prefix = Vec::new();
let mut suffix = Vec::new();
if let Some(list) = pp.get("processors").and_then(Value::as_array) {
for sub in list {
let wrapped = serde_json::json!({ "post_processor": sub });
let p = parse_post_processor(&wrapped);
prefix.extend(p.prefix);
let mut new_suffix = p.suffix;
new_suffix.extend(suffix);
suffix = new_suffix;
}
}
PostProcessor { prefix, suffix }
}
_ => PostProcessor::default(),
}
}
fn parse_template_processing(pp: &Value) -> PostProcessor {
let resolve = |tok: &str| -> Option<u32> {
pp.get("special_tokens")
.and_then(|m| m.get(tok))
.and_then(|e| e.get("ids"))
.and_then(Value::as_array)
.and_then(|a| a.first())
.and_then(Value::as_u64)
.map(|n| n as u32)
};
let Some(items) = pp.get("single").and_then(Value::as_array) else {
return PostProcessor::default();
};
let mut prefix = Vec::new();
let mut suffix = Vec::new();
let mut seen_sequence = false;
for item in items {
if item.get("Sequence").is_some() {
seen_sequence = true;
} else if let Some(id) = item
.get("SpecialToken")
.and_then(|s| s.get("id"))
.and_then(Value::as_str)
.and_then(&resolve)
{
if seen_sequence {
suffix.push(id);
} else {
prefix.push(id);
}
}
}
PostProcessor { prefix, suffix }
}
fn model_family(model: &Value) -> Result<&'static str, HfJsonError> {
if let Some(t) = model.get("type").and_then(Value::as_str) {
return match t {
"BPE" => Ok("BPE"),
"Unigram" => Ok("Unigram"),
"WordPiece" => Ok("WordPiece"),
other => Err(HfJsonError::UnsupportedModelType(other.to_string())),
};
}
let nonempty_prefix = model
.get("continuing_subword_prefix")
.and_then(Value::as_str)
.is_some_and(|s| !s.is_empty());
if model.get("vocab").map(Value::is_array).unwrap_or(false) {
Ok("Unigram")
} else if model.get("merges").is_some() {
Ok("BPE")
} else if model.get("max_input_chars_per_word").is_some() || nonempty_prefix {
Ok("WordPiece")
} else {
Ok("BPE")
}
}
fn build_bpe(root: &Value, model: &Value) -> Result<Backend, HfJsonError> {
let pre = parse_pre_tokenizer(root.get("pre_tokenizer"));
let specials = parse_special_tokens(root);
let vocab = model
.get("vocab")
.and_then(Value::as_object)
.ok_or(HfJsonError::MissingField("model.vocab"))?;
let mut encoder: FxHashMap<Vec<u8>, u32> = FxHashMap::default();
encoder.reserve(vocab.len());
for (token, id) in vocab {
let id = id
.as_u64()
.ok_or(HfJsonError::MissingField("model.vocab[*] = u32"))? as u32;
if pre.byte_level && byte_level_decode(token).is_none() {
return Err(HfJsonError::InvalidByteLevel(token.clone()));
}
encoder.insert(token.as_bytes().to_vec(), id);
}
let merge_ranks = parse_merge_ranks(model, vocab);
let engine = super::pretokenizer::parse(root.get("pre_tokenizer"));
if engine.is_none()
&& !pre.anchored
&& root.get("pre_tokenizer").is_some_and(|v| !v.is_null())
&& !pre.unknown.is_empty()
{
return Err(HfJsonError::UnsupportedPreTokenizer(pre.unknown.join(", ")));
}
let tok = match engine {
Some(pt) => {
let t = if pt.byte_level {
Tokenizer::new_byte_level(encoder, specials, super::tokenizer::GPT2_PATTERN)?
} else {
Tokenizer::new(encoder, specials, super::tokenizer::GPT2_PATTERN)?
};
let t = match merge_ranks {
Some(ranks) => t.with_merge_ranks(ranks),
None => t,
};
t.with_pre_tokenizer(pt)
}
None => {
let t = if pre.byte_level {
Tokenizer::new_byte_level(encoder, specials, &pre.pattern)?
} else {
Tokenizer::new(encoder, specials, &pre.pattern)?
};
let t = match merge_ranks {
Some(ranks) => t.with_merge_ranks(ranks),
None => t,
};
t.with_prefix_space(pre.byte_level && pre.add_prefix_space)
}
};
let tok = tok
.with_added_token_matching(true)
.with_special_decode_ids(parse_special_decode_ids(root))
.with_normalizer(Normalizer::new(parse_norm_ops(root.get("normalizer"))?));
Ok(Backend::Bpe(tok))
}
fn parse_merge_ranks(
model: &Value,
vocab: &serde_json::Map<String, Value>,
) -> Option<FxHashMap<Vec<u8>, u32>> {
let merges = model.get("merges").and_then(Value::as_array)?;
let mut merged: Vec<String> = Vec::with_capacity(merges.len());
for m in merges {
match m {
Value::Array(p) if p.len() == 2 => {
if let (Some(a), Some(b)) = (p[0].as_str(), p[1].as_str()) {
merged.push(format!("{a}{b}"));
}
}
Value::String(s) => merged.push(s.replacen(' ', "", 1)),
_ => {}
}
}
if merged.is_empty() {
return None;
}
let merge_set: std::collections::HashSet<&str> = merged.iter().map(String::as_str).collect();
let mut ranks: FxHashMap<Vec<u8>, u32> = FxHashMap::default();
let mut base: Vec<(&String, u64)> = vocab
.iter()
.filter(|(k, _)| !merge_set.contains(k.as_str()))
.filter_map(|(k, v)| v.as_u64().map(|id| (k, id)))
.collect();
base.sort_by_key(|&(_, id)| id);
for (tok, _) in &base {
ranks.insert(tok.as_bytes().to_vec(), ranks.len() as u32);
}
let base_count = ranks.len() as u32;
for (i, tok) in merged.iter().enumerate() {
ranks
.entry(tok.as_bytes().to_vec())
.or_insert(base_count + i as u32);
}
Some(ranks)
}
fn build_unigram(root: &Value, model: &Value) -> Result<Backend, HfJsonError> {
let vocab = model
.get("vocab")
.and_then(Value::as_array)
.ok_or(HfJsonError::MissingField("model.vocab"))?;
let mut tokens = Vec::with_capacity(vocab.len());
let mut scores = Vec::with_capacity(vocab.len());
for entry in vocab {
let pair = entry
.as_array()
.ok_or(HfJsonError::MissingField("model.vocab[*] = [token, score]"))?;
let token = pair
.first()
.and_then(Value::as_str)
.ok_or(HfJsonError::MissingField("model.vocab[*][0] = token"))?;
let score = pair.get(1).and_then(Value::as_f64).unwrap_or(0.0) as f32;
tokens.push(token.to_string());
scores.push(score);
}
let find = |cands: &[&str]| -> Option<u32> {
find_added_token(root, cands).or_else(|| {
cands
.iter()
.find_map(|c| tokens.iter().position(|t| t == c).map(|i| i as u32))
})
};
let eos = find(&["</s>", "<eos>", "<|endoftext|>", "<|end_of_text|>", "[SEP]"])
.or_else(|| {
model
.get("unk_id")
.and_then(Value::as_u64)
.map(|n| n as u32)
})
.unwrap_or(0);
let ops = parse_norm_ops(root.get("normalizer"))?;
let pre = parse_pre_tokenizer(root.get("pre_tokenizer"));
let tok = SentencePieceTokenizer::new(tokens, scores, None, eos)?
.with_normalizer(Normalizer::new(ops))
.with_prefix_space(pre.add_prefix_space)
.with_added_tokens(&parse_special_tokens(root))
.with_special_decode_ids(parse_special_decode_ids(root));
Ok(Backend::Unigram(tok))
}
fn build_wordpiece(root: &Value, model: &Value) -> Result<Backend, HfJsonError> {
let vocab = model
.get("vocab")
.and_then(Value::as_object)
.ok_or(HfJsonError::MissingField("model.vocab"))?;
let max_id = vocab
.values()
.filter_map(Value::as_u64)
.max()
.ok_or(HfJsonError::MissingField("model.vocab (empty)"))? as usize;
let mut id_to_token = vec![String::new(); max_id + 1];
for (token, id) in vocab {
let id = id
.as_u64()
.ok_or(HfJsonError::MissingField("model.vocab[*] = u32"))? as usize;
id_to_token[id] = token.clone();
}
let unk_token = model
.get("unk_token")
.and_then(Value::as_str)
.unwrap_or("[UNK]");
let unk_id = vocab
.get(unk_token)
.and_then(Value::as_u64)
.ok_or(HfJsonError::MissingSpecial("unk"))? as u32;
let max_word_len = model
.get("max_input_chars_per_word")
.and_then(Value::as_u64)
.unwrap_or(100) as usize;
let norm = parse_bert_norm(root.get("normalizer"));
let prefix = model
.get("continuing_subword_prefix")
.and_then(Value::as_str)
.unwrap_or("##")
.to_string();
let tok = WordPieceTokenizer::with_options(
id_to_token,
unk_id,
max_word_len,
norm.lowercase,
norm.handle_chinese_chars,
norm.clean_text,
prefix,
)
.with_added_tokens(&parse_special_tokens(root))
.with_special_decode_ids(parse_special_decode_ids(root));
Ok(Backend::WordPiece(tok))
}
#[cfg(test)]
mod tests;