Skip to main content

tpt_tokenizer_core/
wordpiece.rs

1//! WordPiece tokenizer (BERT style).
2
3use alloc::collections::BTreeMap;
4use alloc::{
5    format,
6    string::{String, ToString},
7    vec,
8    vec::Vec,
9};
10
11use crate::error::TokenizerError;
12use crate::pretokenize::bert_basic;
13use crate::tokenizer::{TokenId, Tokenizer};
14
15/// Prefix attached to continuation sub-words in a WordPiece vocabulary.
16const CONTINUATION: &str = "##";
17
18/// A WordPiece tokenizer built from a vocabulary.
19///
20/// Words are split greedily into the longest matching sub-words; a word that
21/// cannot be split at all is replaced by the `[UNK]` token.
22///
23/// Input is pre-tokenized with a BERT-style basic tokenizer that splits on
24/// whitespace and punctuation and isolates CJK characters. Enable
25/// [`with_lowercase`](Self::with_lowercase) to match `*-uncased` models.
26#[derive(Debug, Clone)]
27pub struct WordPieceTokenizer {
28    vocab: BTreeMap<String, TokenId>,
29    id_to_token: BTreeMap<TokenId, String>,
30    unk_id: TokenId,
31    max_input_chars_per_word: usize,
32    lowercase: bool,
33    #[cfg(feature = "normalization")]
34    normalize: Option<crate::normalize::NormalizationForm>,
35}
36
37impl WordPieceTokenizer {
38    /// Builds a tokenizer from a vocabulary (token string → id). `unk_token`
39    /// names the fallback token (typically `"[UNK]"`); it must be present in
40    /// `vocab`.
41    ///
42    /// # Errors
43    /// Returns [`TokenizerError::UnknownToken`] if `unk_token` is missing.
44    pub fn from_vocab(
45        vocab: BTreeMap<String, TokenId>,
46        unk_token: &str,
47    ) -> Result<Self, TokenizerError> {
48        let unk_id = *vocab
49            .get(unk_token)
50            .ok_or_else(|| TokenizerError::UnknownToken(unk_token.to_string()))?;
51        let id_to_token = vocab
52            .iter()
53            .map(|(token, &id)| (id, token.clone()))
54            .collect();
55        Ok(Self {
56            vocab,
57            id_to_token,
58            unk_id,
59            max_input_chars_per_word: 100,
60            lowercase: false,
61            #[cfg(feature = "normalization")]
62            normalize: None,
63        })
64    }
65
66    /// Enables lowercasing during pre-tokenization, matching `*-uncased`
67    /// BERT models.
68    #[must_use]
69    pub fn with_lowercase(mut self) -> Self {
70        self.lowercase = true;
71        self
72    }
73
74    /// Applies the given Unicode normalization form to text before encoding.
75    ///
76    /// Requires the `normalization` Cargo feature.
77    #[cfg(feature = "normalization")]
78    #[must_use]
79    pub fn with_normalization(mut self, form: crate::normalize::NormalizationForm) -> Self {
80        self.normalize = Some(form);
81        self
82    }
83
84    /// Number of tokens in the vocabulary.
85    #[must_use]
86    pub fn vocab_size(&self) -> usize {
87        self.vocab.len()
88    }
89
90    /// Tokenizes a single word into sub-word strings using the WordPiece
91    /// longest-match algorithm.
92    ///
93    /// Returns `None` when the word cannot be split at all (it should be
94    /// replaced by the `[UNK]` token by the caller).
95    fn tokenize_word(&self, word: &str) -> Option<Vec<String>> {
96        if self.vocab.contains_key(word) {
97            return Some(vec![word.to_string()]);
98        }
99        let chars: Vec<char> = word.chars().collect();
100        if chars.len() > self.max_input_chars_per_word {
101            return None;
102        }
103
104        let mut sub_tokens = Vec::new();
105        let mut start = 0usize;
106        while start < chars.len() {
107            let mut end = chars.len();
108            let mut cur_substr: Option<String> = None;
109            while start < end {
110                let substr: String = chars[start..end].iter().collect();
111                let candidate = if start > 0 {
112                    format!("{CONTINUATION}{substr}")
113                } else {
114                    substr.clone()
115                };
116                if self.vocab.contains_key(&candidate) {
117                    cur_substr = Some(candidate);
118                    break;
119                }
120                end -= 1;
121            }
122            let sub = cur_substr?;
123            sub_tokens.push(sub);
124            start = end;
125        }
126
127        Some(sub_tokens)
128    }
129
130    /// Loads a BERT style vocabulary from disk: one token per line (line number
131    /// = id).
132    ///
133    /// # Errors
134    /// Returns [`TokenizerError::Io`] on a read failure or
135    /// [`TokenizerError::UnknownToken`] if `unk_token` is absent.
136    #[cfg(feature = "std")]
137    pub fn from_file(vocab_path: &str, unk_token: &str) -> Result<Self, TokenizerError> {
138        let text = std::fs::read_to_string(vocab_path)?;
139        let vocab = crate::tokenizer::parse_vocab_lines(&text);
140        Self::from_vocab(vocab, unk_token)
141    }
142}
143
144impl Tokenizer for WordPieceTokenizer {
145    fn encode(&self, text: &str) -> Result<Vec<TokenId>, TokenizerError> {
146        #[cfg(feature = "normalization")]
147        let normalized;
148        #[cfg(feature = "normalization")]
149        let text = if let Some(form) = self.normalize {
150            normalized = crate::normalize::normalize(text, form);
151            normalized.as_str()
152        } else {
153            text
154        };
155
156        let mut ids = Vec::new();
157        for word in bert_basic(text, self.lowercase) {
158            match self.tokenize_word(&word) {
159                Some(subs) => {
160                    for sub in subs {
161                        ids.push(
162                            *self
163                                .vocab
164                                .get(&sub)
165                                .ok_or(TokenizerError::UnknownToken(sub))?,
166                        );
167                    }
168                }
169                None => ids.push(self.unk_id),
170            }
171        }
172        Ok(ids)
173    }
174
175    /// Decode `ids` back into a string, mirroring BERT's
176    /// `convert_tokens_to_string`: continuation tokens (those prefixed with
177    /// `##`) are concatenated directly, while each new word is preceded by a
178    /// space. This preserves inter-word spacing that `encode` discards.
179    ///
180    /// # Errors
181    /// Returns a [`TokenizerError`] if an id is missing from the inverse
182    /// vocabulary.
183    fn decode(&self, ids: &[TokenId]) -> Result<String, TokenizerError> {
184        let mut out = String::new();
185        for (i, &id) in ids.iter().enumerate() {
186            let token = self
187                .id_to_token
188                .get(&id)
189                .ok_or_else(|| TokenizerError::UnknownToken(id.to_string()))?;
190            if let Some(stripped) = token.strip_prefix(CONTINUATION) {
191                out.push_str(stripped);
192            } else {
193                if i > 0 {
194                    out.push(' ');
195                }
196                out.push_str(token);
197            }
198        }
199        Ok(out)
200    }
201}