use std::path::Path;
pub struct Tokenizer {
bytes: Vec<Option<Vec<u8>>>,
}
impl Tokenizer {
pub fn load(dir: &Path) -> Result<Tokenizer, String> {
Tokenizer::from_json(&crate::read_json(dir, "tokenizer.json")?)
}
pub fn from_json(j: &serde_json::Value) -> Result<Tokenizer, String> {
let decoder = j["decoder"]["type"].as_str().unwrap_or("");
if decoder != "ByteLevel" {
return Err(format!(
"unsupported decoder {decoder:?}, only ByteLevel is supported"
));
}
let model = j["model"]["type"].as_str().unwrap_or("");
if model != "BPE" {
return Err(format!(
"unsupported tokenizer model {model:?}, only BPE is supported"
));
}
let vocab = j["model"]["vocab"]
.as_object()
.ok_or("tokenizer.json has no model.vocab")?;
let table = byte_table();
let mut bytes: Vec<Option<Vec<u8>>> = Vec::new();
let mut put = |id: usize, v: Option<Vec<u8>>| {
if bytes.len() <= id {
bytes.resize(id + 1, None);
}
bytes[id] = v;
};
for (tok, id) in vocab {
let id = id.as_u64().ok_or("a vocabulary id is not an integer")? as usize;
put(id, Some(token_bytes(tok, &table)));
}
for a in j["added_tokens"].as_array().into_iter().flatten() {
let id = a["id"]
.as_u64()
.ok_or("an added_tokens id is not an integer")? as usize;
let content = a["content"]
.as_str()
.ok_or("an added_tokens entry has no content")?;
let special = a["special"].as_bool().unwrap_or(false);
put(id, (!special).then(|| token_bytes(content, &table)));
}
Ok(Tokenizer { bytes })
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn decode(&self, ids: &[u32]) -> String {
let mut out = Vec::new();
for &id in ids {
if let Some(Some(b)) = self.bytes.get(id as usize) {
out.extend_from_slice(b);
}
}
String::from_utf8_lossy(&out).into_owned()
}
}
fn token_bytes(tok: &str, table: &[Option<u8>]) -> Vec<u8> {
tok.chars()
.map(|c| table.get(c as usize).copied().flatten())
.collect::<Option<Vec<u8>>>()
.unwrap_or_else(|| tok.as_bytes().to_vec())
}
fn byte_table() -> Vec<Option<u8>> {
let visible = |b: u8| (33..=126).contains(&b) || (161..=172).contains(&b) || b >= 174;
const SHIFT: usize = 256;
let mut table = vec![None; SHIFT * 2];
let mut n = 0;
for b in 0..=u8::MAX {
if visible(b) {
table[usize::from(b)] = Some(b);
} else {
table[SHIFT + n] = Some(b);
n += 1;
}
}
table.truncate(SHIFT + n);
table
}
#[cfg(test)]
mod tests {
use super::*;
fn mini() -> Tokenizer {
let j = serde_json::json!({
"decoder": {"type": "ByteLevel", "add_prefix_space": true},
"model": {"type": "BPE", "vocab": {
"<s>": 0, "<pad>": 1, "</s>": 2, "(": 30, "d": 90, "\\": 82,
"ĉ": 220, "Ċ": 221, "ĠĠ": 304, "ĠâĪĴ": 612, "âĢĵ": 853, "âĪĴ": 1059,
"Ġμ": 1173, "ä": 183, "¸": 139, "Ń": 278, "ä¸": 19598, "Ġown": 4158
}},
"added_tokens": [
{"id": 0, "content": "<s>", "special": true},
{"id": 1, "content": "<pad>", "special": true},
{"id": 2, "content": "</s>", "special": true}
]
});
Tokenizer::from_json(&j).unwrap()
}
#[test]
fn decodes_byte_level_tokens() {
let t = mini();
assert_eq!(t.decode(&[612]), " −");
assert_eq!(t.decode(&[853, 1059]), "–−");
assert_eq!(t.decode(&[1173]), " μ");
assert_eq!(t.decode(&[304, 220, 221]), " \t\n");
assert_eq!(t.decode(&[82, 90, 30, 4158]), "\\d( own");
assert_eq!(t.decode(&[183, 139, 278]), "中");
assert_eq!(t.decode(&[19598]), "\u{fffd}");
}
#[test]
fn skips_special_and_unknown() {
let t = mini();
assert_eq!(t.decode(&[2, 0, 90, 1, 2, 99_999]), "d");
}
#[test]
fn byte_table_is_a_bijection() {
let t = byte_table();
let mut seen = [false; 256];
for b in t.iter().flatten() {
assert!(!seen[usize::from(*b)]);
seen[usize::from(*b)] = true;
}
assert!(seen.iter().all(|&s| s));
assert_eq!(t['Ġ' as usize], Some(b' '));
assert_eq!(t['Ċ' as usize], Some(b'\n'));
}
#[test]
fn real_tokenizer_if_present() {
let Some(dir) = crate::models::cache_root()
.map(|r| r.join("PP-FormulaNet_plus-L_safetensors"))
.filter(|d| d.join("tokenizer.json").exists())
else {
return;
};
let t = Tokenizer::load(&dir).unwrap();
assert_eq!(t.len(), 50_000);
assert_eq!(t.decode(&[612, 1059, 1173, 4158]), " −− μ own");
assert_eq!(t.decode(&[183, 139, 278]), "中");
assert_eq!(t.decode(&[0, 1, 2, 3, 22]), "");
}
}