tokenizers/tokenizer/
added_vocabulary.rs

1use super::{
2    normalizer::Range, Model, NormalizedString, Normalizer, Offsets, PreTokenizedString, Token,
3};
4use ahash::{AHashMap, AHashSet};
5use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
6use regex::Regex;
7use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer};
8use std::sync::LazyLock;
9
10/// Represent a token added by the user on top of the existing Model vocabulary.
11/// AddedToken can be configured to specify the behavior they should have in various situations
12/// like:
13///   - Whether they should only match single words
14///   - Whether to include any whitespace on its left or right
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct AddedToken {
17    /// The content of the added token
18    pub content: String,
19    /// Whether this token must be a single word or can break words
20    pub single_word: bool,
21    /// Whether this token should strip whitespaces on its left
22    pub lstrip: bool,
23    /// Whether this token should strip whitespaces on its right
24    pub rstrip: bool,
25    /// Whether this token should be normalized
26    pub normalized: bool,
27    /// Whether this token is special
28    pub special: bool,
29}
30
31impl AddedToken {
32    /// Build this token from the given content, specifying if it is intended to be a
33    /// special token. Special tokens are not normalized by default.
34    pub fn from<S: Into<String>>(content: S, special: bool) -> Self {
35        Self {
36            content: content.into(),
37            normalized: !special,
38            special,
39            ..Default::default()
40        }
41    }
42    /// Specify whether this token should only match on whole single words, and never
43    /// part of a word.
44    #[must_use]
45    pub fn single_word(mut self, single_word: bool) -> Self {
46        self.single_word = single_word;
47        self
48    }
49    /// Specify whether this token should include all the whitespaces on its left, in
50    /// order to strip them out.
51    #[must_use]
52    pub fn lstrip(mut self, lstrip: bool) -> Self {
53        self.lstrip = lstrip;
54        self
55    }
56    /// Specify whether this token should include all the whitespaces on its right, in
57    /// order to strip them out.
58    #[must_use]
59    pub fn rstrip(mut self, rstrip: bool) -> Self {
60        self.rstrip = rstrip;
61        self
62    }
63    /// Specify whether this token should be normalized and match against its normalized
64    /// version in the input text.
65    #[must_use]
66    pub fn normalized(mut self, normalized: bool) -> Self {
67        self.normalized = normalized;
68        self
69    }
70    /// Specify whether this token is special, meaning if it should be skipped when decoding
71    #[must_use]
72    pub fn special(mut self, special: bool) -> Self {
73        self.special = special;
74        self
75    }
76}
77impl Default for AddedToken {
78    fn default() -> Self {
79        Self {
80            content: String::new(),
81            single_word: false,
82            lstrip: false,
83            rstrip: false,
84            normalized: true,
85            special: false,
86        }
87    }
88}
89// AddedTokens can be updated if value changed
90impl std::hash::Hash for AddedToken {
91    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
92        self.content.hash(state);
93    }
94}
95
96type MatchingSet = (AhoCorasick, Vec<u32>);
97
98static STARTS_WITH_WORD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\w").unwrap());
99static ENDS_WITH_WORD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\w$").unwrap());
100static RIGHTMOST_SPACE_AT_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*").unwrap());
101static LEFTMOST_SPACE_AT_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s*$").unwrap());
102
103fn ends_with_word(sentence: &str) -> bool {
104    ENDS_WITH_WORD.is_match(sentence)
105}
106
107fn starts_with_word(sentence: &str) -> bool {
108    STARTS_WITH_WORD.is_match(sentence)
109}
110
111fn space_leftmost_at_end(sentence: &str) -> usize {
112    if let Some(match_) = LEFTMOST_SPACE_AT_END.find(sentence) {
113        match_.start()
114    } else {
115        sentence.len()
116    }
117}
118fn space_rightmost_at_start(sentence: &str) -> usize {
119    if let Some(match_) = RIGHTMOST_SPACE_AT_START.find(sentence) {
120        match_.end()
121    } else {
122        0
123    }
124}
125///
126/// A vocabulary built on top of the Model
127///
128/// This provides a way to add new vocabulary to a Tokenizer that has already been trained,
129/// in a previous process, maybe by someone else. This is especially interesting in the case
130/// of fine-tunings, where we want to finetune a model while adding some new functionalities
131/// using some new special tokens, or maybe add some tokens in the case of unknown tokens, etc.
132///
133/// One of the reasons we need to handle these tokens outside of the model is simply that
134/// for many models, it is not possible to add new tokens after the training process. For example,
135/// using BPE, the training process generates merges pairs along the vocabulary, and any token
136/// in the vocabulary can be decomposed in other tokens, down to the original alphabet. If we
137/// were to add new tokens after this training process, we couldn't make sure the merges pairs
138/// exist as required.
139///
140#[derive(Clone, Debug)]
141pub struct AddedVocabulary {
142    /// Contains the mapping from String (token content) to ID. This map contains both special
143    /// tokens and classic added tokens that were added to the this vocabulary.
144    added_tokens_map: AHashMap<String, u32>,
145    /// Contains the mapping from ID to AddedToken for all the added tokens, both special
146    /// and classic.
147    added_tokens_map_r: AHashMap<u32, AddedToken>,
148
149    /// Contains only the classic AddedToken, in the specific order the user gave them.
150    added_tokens: Vec<AddedToken>,
151    /// Contains only the special AddedToken, in the specific order the user gave them.
152    special_tokens: Vec<AddedToken>,
153
154    /// A Set, containing all the special token for easy access while decoding. This let's
155    /// us remove them easily with an O(1) complexity.
156    special_tokens_set: AHashSet<String>,
157
158    /// A RegexSet containing all the non-normalized patterns used to split on AddedTokens
159    split_trie: MatchingSet,
160    /// A RegexSet containing all the normalized patterns used to split on AddedTokens
161    split_normalized_trie: MatchingSet,
162
163    /// Whether or not special tokens should be splitted when encoding. This is equivalent to ignoring them
164    encode_special_tokens: bool,
165}
166
167impl AddedVocabulary {
168    pub fn new() -> Self {
169        let trie = AhoCorasickBuilder::new()
170            .match_kind(MatchKind::LeftmostLongest)
171            .build::<_, &&[u8]>([])
172            .expect("The trie should build correctly");
173        let normalized_trie = AhoCorasickBuilder::new()
174            .match_kind(MatchKind::LeftmostLongest)
175            .build::<_, &&[u8]>([])
176            .expect("The normalized trie should build correctly");
177        Self {
178            added_tokens_map: AHashMap::new(),
179            added_tokens_map_r: AHashMap::new(),
180            added_tokens: vec![],
181            special_tokens: vec![],
182            special_tokens_set: AHashSet::new(),
183            split_trie: (trie, vec![]),
184            split_normalized_trie: (normalized_trie, vec![]),
185            encode_special_tokens: false,
186        }
187    }
188    /// Size of the additional vocabulary
189    #[allow(dead_code)] // Suppress the "method is never used" warning
190    pub fn len(&self) -> usize {
191        self.added_tokens_map.len()
192    }
193
194    /// Whether or not this vocabulary is empty
195    pub fn is_empty(&self) -> bool {
196        self.added_tokens_map.is_empty()
197    }
198
199    /// Get the additional vocabulary
200    pub fn get_vocab(&self) -> &AHashMap<String, u32> {
201        &self.added_tokens_map
202    }
203
204    /// Get the additional vocabulary with the AddedTokens
205    pub fn get_added_tokens_decoder(&self) -> &AHashMap<u32, AddedToken> {
206        &self.added_tokens_map_r
207    }
208
209    /// Get the id matching one of our token if it exists
210    pub fn token_to_id(&self, token: &str, model: &impl Model) -> Option<u32> {
211        self.added_tokens_map
212            .get(token)
213            .copied()
214            .or_else(|| model.token_to_id(token))
215    }
216
217    /// Get the token matching the given id if it exists
218    #[deprecated(
219        since = "0.19.0",
220        note = "please use `added_vocabulary.simple_id_to_token(id).or_else(|| model.id_to_token(id)` instead"
221    )]
222    pub fn id_to_token(&self, id: u32, model: &impl Model) -> Option<String> {
223        self.added_tokens_map_r
224            .get(&id)
225            .map(|t| t.content.clone())
226            .or_else(|| model.id_to_token(id))
227    }
228
229    pub fn simple_id_to_token(&self, id: u32) -> Option<String> {
230        self.added_tokens_map_r.get(&id).map(|t| t.content.clone())
231    }
232
233    //
234    pub fn set_encode_special_tokens(&mut self, value: bool) {
235        self.encode_special_tokens = value;
236    }
237
238    pub fn get_encode_special_tokens(&self) -> bool {
239        self.encode_special_tokens
240    }
241
242    /// Check if a token is a special token
243    pub fn is_special_token(&self, token: &str) -> bool {
244        self.special_tokens_set.contains(token)
245    }
246
247    /// Add some special tokens to the vocabulary
248    pub fn add_special_tokens<N: Normalizer>(
249        &mut self,
250        tokens: &[AddedToken],
251        model: &impl Model,
252        normalizer: Option<&N>,
253    ) -> usize {
254        self.add_tokens(tokens, model, normalizer)
255    }
256
257    /// Add some tokens to the vocabulary
258    pub fn add_tokens<N: Normalizer>(
259        &mut self,
260        tokens: &[AddedToken],
261        model: &impl Model,
262        normalizer: Option<&N>,
263    ) -> usize {
264        // Handle special tokens (if any)
265        for token in tokens {
266            if token.special
267                && !token.content.is_empty()
268                && !self.special_tokens_set.contains(&token.content)
269            {
270                self.special_tokens.push(token.to_owned());
271                self.special_tokens_set.insert(token.content.clone());
272            }
273        }
274
275        // Then we delegate to `add_tokens`, that will take care of refreshing added tokens too.
276        let mut ignored = 0;
277        for token in tokens {
278            if token.content.is_empty() || self.added_tokens_map_r.values().any(|val| val == token)
279            {
280                ignored += 1;
281                continue;
282            }
283            // If a token is already part of the vocabulary, we mark it as added
284            let new_id = if let Some(new_id) = self.token_to_id(&token.content, model) {
285                new_id
286            } else {
287                self.added_tokens_map.values().cloned().max().map_or(
288                    model.get_vocab_size() as u32,
289                    |max| {
290                        if (max >= model.get_vocab_size() as u32) || model.get_vocab_size() == 0 {
291                            max + 1
292                        } else {
293                            model.get_vocab_size() as u32
294                        }
295                    },
296                )
297            };
298            // Make sure we modify the previous entry
299            *self
300                .added_tokens_map
301                .entry(token.content.clone())
302                .or_default() = new_id;
303            // Update the current revert operation
304            *self.added_tokens_map_r.entry(new_id).or_default() = token.clone();
305            // Make sure to remove previous entry (if the token gets a new id)
306
307            // Finally add the token to the classic set if special
308            if !self.special_tokens_set.contains(&token.content) {
309                self.added_tokens.push(token.clone());
310            }
311        }
312
313        self.refresh_added_tokens(model, normalizer);
314
315        // Return the number of added tokens
316        tokens.len() - ignored
317    }
318
319    /// Reconstruct our internal RegexSet when new tokens are added to the vocabulary.
320    ///
321    /// We keep two different RegexSet, one that will take care of matching against the
322    /// non-normalized string, and one matching against the normalized one.
323    fn refresh_added_tokens<N: Normalizer>(&mut self, model: &impl Model, normalizer: Option<&N>) {
324        type TupleTokenId<'a> = (&'a AddedToken, u32);
325        let (normalized, non_normalized): (Vec<TupleTokenId>, Vec<TupleTokenId>) = self
326            .special_tokens
327            .iter()
328            .chain(self.added_tokens.iter())
329            .map(|token| {
330                (
331                    token,
332                    self.token_to_id(&token.content, model)
333                        .expect("Missing additional token"),
334                )
335            })
336            .partition(|(token, _)| token.normalized);
337
338        let (tokens, ids): (Vec<&AddedToken>, Vec<u32>) = non_normalized.into_iter().unzip();
339        let trie = AhoCorasickBuilder::new()
340            .match_kind(MatchKind::LeftmostLongest)
341            .build(tokens.iter().map(|token| &token.content))
342            .expect("Failed to build tried when refreshing tokens");
343        self.split_trie = (trie, ids);
344
345        let (ntokens, nids): (Vec<&AddedToken>, Vec<u32>) = normalized.into_iter().unzip();
346        let patterns: Vec<_> = ntokens
347            .iter()
348            .map(|token| {
349                let mut content = NormalizedString::from(token.content.as_ref());
350                if let Some(n) = normalizer {
351                    n.normalize(&mut content).unwrap();
352                }
353                content
354            })
355            .collect();
356        let normalized_trie = AhoCorasickBuilder::new()
357            .match_kind(MatchKind::LeftmostLongest)
358            .build(patterns.iter().map(|content| content.get()))
359            .expect("Failed to build tried when refreshing tokens (normalized)");
360        self.split_normalized_trie = (normalized_trie, nids);
361    }
362
363    /// Find any AddedToken in the given sentence, using the provided MatchingSet.
364    /// This method returns a list "splits", each of them being a pair of Offsets
365    /// and an optional ID if it is an AddedToken.
366    /// The list of splits cover the entire input string.
367    fn find_matches(&self, sentence: &str, split_re: &MatchingSet) -> Vec<(Option<u32>, Offsets)> {
368        if sentence.is_empty() {
369            return vec![(None, (0, 0))];
370        }
371
372        let mut start_offset = 0;
373        let mut splits = vec![];
374
375        for mat in split_re.0.find_iter(sentence) {
376            let mut start = mat.start();
377            let mut stop = mat.end();
378            let aho_id = mat.pattern();
379            let id = split_re.1[aho_id];
380            let added_token = &self.added_tokens_map_r.get(&id).unwrap();
381
382            if self.encode_special_tokens && self.special_tokens_set.contains(&added_token.content)
383            {
384                continue;
385            }
386
387            if added_token.single_word {
388                let start_space = start == 0 || !ends_with_word(&sentence[..start]);
389                let stop_space = stop == sentence.len() || !starts_with_word(&sentence[stop..]);
390
391                if !stop_space || !start_space {
392                    // Discard not single word
393                    continue;
394                }
395            }
396            if added_token.lstrip {
397                // This will be strictly inferior to start and in correct sentence offset
398                let newstart = space_leftmost_at_end(&sentence[..start]);
399
400                // The previous match could have already matched those spaces
401                // Ignore them if it's already matched
402                start = std::cmp::max(newstart, start_offset);
403            }
404            if added_token.rstrip {
405                // This will starting a the stop+1 character, so we need
406                // to add the previous stop value
407                stop += space_rightmost_at_start(&sentence[stop..])
408            }
409            if start_offset < start {
410                splits.push((None, (start_offset, start)));
411            }
412            splits.push((Some(id), (start, stop)));
413            start_offset = stop;
414        }
415
416        let total_byte_len = sentence.len();
417        if start_offset != total_byte_len {
418            splits.push((None, (start_offset, total_byte_len)));
419        }
420
421        splits
422    }
423
424    /// Split the input sentence to extract anything we found from the `MatchingSet`, as well as
425    /// the list of corresponding IDs
426    /// The list of IDs have the exact same number of elements than the Iterator.
427    fn split_with_indices(
428        &self,
429        sentence: NormalizedString,
430        split_re: &MatchingSet,
431    ) -> Vec<(NormalizedString, Option<Vec<Token>>)> {
432        self.find_matches(sentence.get(), split_re)
433            .into_iter()
434            .map(|(id, byte_offsets)| {
435                let slice = sentence
436                    .slice(Range::Normalized(byte_offsets.0..byte_offsets.1))
437                    .expect("AddedVocabulary bad split");
438                if let Some(id) = id {
439                    let value = slice.get().to_owned();
440                    let len = value.len();
441                    (slice, Some(vec![Token::new(id, value, (0, len))]))
442                } else {
443                    (slice, None)
444                }
445            })
446            .collect()
447    }
448
449    /// Extract the additional vocabulary from the given sentence, normalizing it along the way.
450    ///
451    /// Some tokens should match against their normalized representation, as well as the
452    /// non-normalized one. For example, when we expect to extract the token `yesterday` in the
453    /// input sentence `I read a book Yesterday`, if the normalizer is supposed to lowercase
454    /// everything, we expect a match.
455    pub fn extract_and_normalize<N: Normalizer>(
456        &self,
457        normalizer: Option<&N>,
458        sequence: &str,
459    ) -> PreTokenizedString {
460        let mut pretokenized: PreTokenizedString = sequence.into();
461
462        // 1. We extract all the non-normalized tokens from the non-normalized string
463        pretokenized
464            .split(|_, sequence| Ok(self.split_with_indices(sequence, &self.split_trie)))
465            .expect("AddedVocabulary bad split");
466
467        // <s> normalized = False
468        // "I read a book   <s>Hey" -> "I read a book", "   <s>", "Hey"
469
470        // </s> normalized = True -> "▁</s>"
471        // "I read a book</s>Hey" -> "I read a book</s>Hey"
472
473        // Day normalized = True -> "Day"
474        // "I read a book monday" -> "I read a book monday"
475
476        // [DAY] normalized = False -> "Day"
477        // "I read a [DAY] monday" -> "I read a " "[DAY]", "book monday"
478        //                                         320055
479        // 2. Then extract the normalized tokens from the normalized pieces of the string
480        pretokenized
481            .split(|_, mut sequence| {
482                normalizer.map(|n| n.normalize(&mut sequence));
483                Ok(self.split_with_indices(sequence, &self.split_normalized_trie))
484            })
485            .expect("AddedVocabulary bad split");
486
487        // ["I read a book", "   <s>", "Hey"] -> ["▁I read a book", "▁   <s>", "▁Hey"]
488        // ["▁I read a book", "▁   <s>", "▁Hey"] -> [.., "▁   ", "<s>", "▁Hey"]
489
490        // </s> normalized = True -> "▁</s>"
491        // "I read a book</s>Hey" -> ["▁I read a book", "<","/","s",">", "Hey"]
492
493        // "I read a " "[DAY]", "book monday" -> "i read a " "[day]", "book monday"
494
495        pretokenized
496    }
497}
498
499impl Default for AddedVocabulary {
500    fn default() -> Self {
501        Self::new()
502    }
503}
504
505#[derive(Debug, Serialize, Deserialize)]
506pub(super) struct AddedTokenWithId {
507    /// The id assigned to this token
508    pub id: u32,
509    #[serde(flatten)]
510    /// The target AddedToken
511    pub token: AddedToken,
512}
513
514impl Serialize for AddedVocabulary {
515    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
516    where
517        S: Serializer,
518    {
519        let mut added_tokens = self
520            .added_tokens_map_r
521            .iter()
522            .map(|(id, token)| AddedTokenWithId {
523                id: *id,
524                token: token.clone(),
525            })
526            .collect::<Vec<_>>();
527        // We need to have these added tokens ordered by ascending ID
528        added_tokens.sort_unstable_by_key(|o| o.id);
529
530        let mut vocabulary = serializer.serialize_seq(Some(added_tokens.len()))?;
531        for token in added_tokens {
532            vocabulary.serialize_element(&token)?;
533        }
534
535        vocabulary.end()
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use crate::normalizers::byte_level::ByteLevel as ByteLevelNormalizer;
543    use crate::normalizers::utils::Lowercase;
544    use crate::normalizers::NormalizerWrapper;
545    use crate::{OffsetReferential, OffsetType, Result, Token, Trainer};
546    use std::collections::HashMap;
547    use std::path::{Path, PathBuf};
548
549    #[derive(Serialize, Deserialize)]
550    struct ModelMock {
551        vocab: AHashMap<String, u32>,
552        vocab_r: AHashMap<u32, String>,
553    }
554    impl ModelMock {
555        pub fn new<I>(iter: I) -> Self
556        where
557            I: IntoIterator<Item = &'static (&'static str, u32)>,
558        {
559            let vocab: AHashMap<String, u32> = iter
560                .into_iter()
561                .map(|&(tok, id)| (tok.to_string(), id))
562                .collect();
563            Self {
564                vocab_r: vocab
565                    .iter()
566                    .map(|(tok, id)| (*id, tok.to_owned()))
567                    .collect(),
568                vocab,
569            }
570        }
571    }
572
573    fn simplify_output(result: &'_ PreTokenizedString) -> Vec<(&'_ str, Option<Vec<u32>>)> {
574        result
575            .get_splits(OffsetReferential::Original, OffsetType::Byte)
576            .into_iter()
577            .map(|(s, _, tokens)| {
578                (
579                    s,
580                    tokens
581                        .as_ref()
582                        .map(|t| t.iter().map(|t| t.id).collect::<Vec<_>>()),
583                )
584            })
585            .collect::<Vec<_>>()
586    }
587
588    struct TrainerMock;
589    impl Trainer for TrainerMock {
590        type Model = ModelMock;
591        fn should_show_progress(&self) -> bool {
592            true
593        }
594        fn train(&self, _model: &mut ModelMock) -> Result<Vec<AddedToken>> {
595            unimplemented!()
596        }
597        fn feed<I, S, F>(&mut self, _iterator: I, _process: F) -> Result<()>
598        where
599            I: Iterator<Item = S> + Send,
600            S: AsRef<str> + Send,
601            F: Fn(&str) -> Result<Vec<String>> + Sync,
602        {
603            unimplemented!()
604        }
605    }
606
607    impl Model for ModelMock {
608        type Trainer = TrainerMock;
609
610        fn tokenize(&self, _sequence: &str) -> Result<Vec<Token>> {
611            unimplemented!()
612        }
613        fn token_to_id(&self, token: &str) -> Option<u32> {
614            self.vocab.get(token).copied()
615        }
616        fn id_to_token(&self, id: u32) -> Option<String> {
617            self.vocab_r.get(&id).cloned()
618        }
619        fn get_vocab(&self) -> HashMap<String, u32> {
620            self.vocab.clone().into_iter().collect()
621        }
622        fn get_vocab_size(&self) -> usize {
623            self.vocab.len()
624        }
625        fn save(&self, _folder: &Path, _name: Option<&str>) -> Result<Vec<PathBuf>> {
626            unimplemented!()
627        }
628        fn get_trainer(&self) -> Self::Trainer {
629            TrainerMock
630        }
631    }
632
633    #[test]
634    fn can_add_tokens() {
635        let model = ModelMock::new(&[("test", 0), ("tost", 1)]);
636        let mut vocab = AddedVocabulary::new();
637        let normalizer: Option<&NormalizerWrapper> = None;
638
639        // Add tokens normally
640        assert_eq!(
641            vocab.add_tokens(
642                &[AddedToken::from("added_token_1", false)],
643                &model,
644                normalizer
645            ),
646            1
647        );
648
649        let vocab_len: usize = vocab.len();
650        assert_eq!(vocab_len, 1);
651
652        // Does not add multiple time the same token
653        assert_eq!(
654            vocab.add_tokens(
655                &[
656                    AddedToken::from("added_token_2", false),
657                    AddedToken::from("added_token_2", false)
658                ],
659                &model,
660                normalizer
661            ),
662            1
663        );
664        assert_eq!(vocab.len(), 2);
665
666        // Also adds tokens already covered by the model
667        let added_token = AddedToken::from("test", false);
668        assert_eq!(
669            vocab.add_tokens(std::slice::from_ref(&added_token), &model, normalizer),
670            1
671        );
672        assert_eq!(vocab.len(), 3);
673
674        assert_eq!(vocab.get_added_tokens_decoder()[&0], added_token);
675    }
676
677    #[test]
678    fn can_add_special_tokens() {
679        let model = ModelMock::new(&[("test", 0), ("tost", 1)]);
680        let mut vocab = AddedVocabulary::new();
681        let normalizer: Option<&NormalizerWrapper> = None;
682        // Add tokens normally
683        assert_eq!(
684            vocab.add_special_tokens(
685                &[AddedToken::from("added_token_1", true)],
686                &model,
687                normalizer
688            ),
689            1
690        );
691        assert_eq!(vocab.len(), 1);
692
693        // Does not add multiple time the same token
694        assert_eq!(
695            vocab.add_special_tokens(
696                &[
697                    AddedToken::from("added_token_2", true),
698                    AddedToken::from("added_token_2", true)
699                ],
700                &model,
701                normalizer
702            ),
703            1
704        );
705        assert_eq!(vocab.len(), 2);
706
707        // Can add tokens already covered by the model
708        assert_eq!(
709            vocab.add_special_tokens(&[AddedToken::from("test", true)], &model, normalizer),
710            1
711        );
712        assert_eq!(vocab.len(), 3); // New token was added
713        assert!(vocab.is_special_token("test"));
714        assert_eq!(
715            *vocab.get_added_tokens_decoder(),
716            AHashMap::from([
717                (0, AddedToken::from("test", true)),
718                (2, AddedToken::from("added_token_1", true)),
719                (3, AddedToken::from("added_token_2", true)),
720            ])
721        );
722        assert!(vocab.added_tokens_map.contains_key("test"));
723        assert!(vocab.added_tokens_map_r.contains_key(&0));
724
725        vocab.add_tokens(
726            &[
727                AddedToken::from("tost", true),
728                AddedToken::from("another_two", false),
729            ],
730            &model,
731            normalizer,
732        );
733        assert_eq!(vocab.len(), 5); // New token was added
734        assert_eq!(vocab.get_vocab()["another_two"], 4); // New token was added, but the index is not the length of the vocab
735
736        // Let's add an already added token again
737        assert_eq!(
738            vocab.add_special_tokens(&[AddedToken::from("another_two", true)], &model, normalizer),
739            1
740        );
741        assert_eq!(vocab.len(), 5); // Token was already there
742        assert_eq!(vocab.get_vocab()["another_two"], 4); // Token idx not changed
743
744        // Just checking that we can set the content of the string in rust
745        let mut token: AddedToken = AddedToken::from("Hey", false);
746        token.content = "hey".to_string();
747        assert_eq!(token.content, "hey"); // Token was already there
748
749        token.special = true;
750        assert!(token.special); // Token was already there
751    }
752
753    #[test]
754    fn can_extract_added_tokens() {
755        // Is able to extract both normal and special tokens
756        let model = ModelMock::new(&[]);
757        let mut vocab = AddedVocabulary::new();
758        let normalizer: Option<&NormalizerWrapper> = None;
759
760        vocab.add_tokens(
761            &[
762                AddedToken::from("my", false),
763                AddedToken::from("name", false),
764            ],
765            &model,
766            normalizer,
767        );
768        vocab.add_special_tokens(
769            &[
770                AddedToken::from("[CLS]", true),
771                AddedToken::from("[SEP]", true),
772            ],
773            &model,
774            normalizer,
775        );
776
777        let result = vocab.extract_and_normalize(normalizer, "[CLS] My name is Anthony [SEP]");
778        assert_eq!(
779            result
780                .get_splits(OffsetReferential::Original, OffsetType::Byte)
781                .into_iter()
782                .map(|(s, _, tokens)| (
783                    s,
784                    tokens
785                        .as_ref()
786                        .map(|t| t.iter().map(|t| t.id).collect::<Vec<_>>())
787                ))
788                .collect::<Vec<_>>(),
789            vec![
790                ("[CLS]", Some(vec![2])),
791                (" My ", None),
792                ("name", Some(vec![1])),
793                (" is Anthony ", None),
794                ("[SEP]", Some(vec![3]))
795            ]
796        );
797    }
798
799    #[test]
800    fn options_use_cases() {
801        // Is able to extract both normal and special tokens, with various options (lstrip, rstrip,
802        // single_word, normalized)
803        let model = ModelMock::new(&[]);
804        let normalizer = Lowercase;
805        let mut vocab = AddedVocabulary::new();
806
807        vocab.add_tokens(
808            &[
809                AddedToken::from("my", false).lstrip(true).rstrip(true),
810                AddedToken::from("name", false),
811                AddedToken::from("ony", false).single_word(true),
812            ],
813            &model,
814            Some(&normalizer),
815        );
816        vocab.add_special_tokens(
817            &[
818                AddedToken::from("[CLS]", true),
819                AddedToken::from("[SEP]", true),
820            ],
821            &model,
822            Some(&normalizer),
823        );
824
825        let result =
826            vocab.extract_and_normalize(Some(&normalizer), "[CLS] My name is Anthony [SEP]");
827
828        assert_eq!(
829            simplify_output(&result),
830            vec![
831                ("[CLS]", Some(vec![3])),
832                // This one includes both spaces because of the lstrip & rstrip
833                // And it matches because normalized == true
834                (" my ", Some(vec![0])),
835                ("name", Some(vec![1])),
836                // `ony` is not extracted here thanks to single_word
837                (" is anthony ", None),
838                ("[SEP]", Some(vec![4])),
839            ]
840        );
841    }
842
843    #[test]
844    fn empty_matches() {
845        let vocab = AddedVocabulary::new();
846        let matches = vocab.find_matches("", &vocab.split_trie);
847        assert_eq!(matches, vec![(None, (0, 0))]);
848    }
849
850    #[test]
851    fn test_single_word_is_correct() {
852        // Is able to extract both normal and special tokens, with various options (lstrip, rstrip,
853        // single_word, normalized)
854        let model = ModelMock::new(&[]);
855        let mut vocab = AddedVocabulary::new();
856        let normalizer = Lowercase;
857
858        vocab.add_tokens(
859            &[AddedToken::from("<mask>", false).single_word(true)],
860            &model,
861            Some(&normalizer),
862        );
863        // Left, in the middle, non single world left, non single word right, end of sentence valid
864        let result = vocab.extract_and_normalize(
865            Some(&normalizer),
866            "<mask> My name <mask> A<mask> <mask>ony <mask>",
867        );
868        assert_eq!(
869            simplify_output(&result),
870            vec![
871                ("<mask>", Some(vec![0])),
872                (" my name ", None),
873                ("<mask>", Some(vec![0])),
874                (" a<mask> <mask>ony ", None),
875                ("<mask>", Some(vec![0]))
876            ]
877        );
878    }
879
880    #[test]
881    fn test_single_word_is_unicode_correct() {
882        let model = ModelMock::new(&[]);
883        let mut vocab = AddedVocabulary::new();
884        let normalizer = Lowercase;
885
886        assert_eq!(vocab.len(), 0);
887
888        vocab.add_tokens(
889            &[AddedToken::from("<mask>", false).single_word(true)],
890            &model,
891            Some(&normalizer),
892        );
893        let result = vocab.extract_and_normalize(Some(&normalizer), "<mask>, <mask>- ◌̰<mask>");
894        assert_eq!(
895            simplify_output(&result),
896            vec![
897                // Punctuation is not word
898                ("<mask>", Some(vec![0])),
899                (", ", None),
900                // dash is not word
901                ("<mask>", Some(vec![0])),
902                // This is unicode combining mark character and is word: https://en.wikipedia.org/wiki/Combining_Diacritical_Marks
903                ("- ◌̰<mask>", None),
904            ]
905        );
906    }
907
908    #[test]
909    fn test_lstrip_unicode_space() {
910        let model = ModelMock::new(&[]);
911        let mut vocab = AddedVocabulary::new();
912        let normalizer = Lowercase;
913
914        vocab.add_tokens(
915            &[AddedToken::from("<mask>", false)
916                .lstrip(true)
917                .rstrip(true)
918                .single_word(true)],
919            &model,
920            Some(&normalizer),
921        );
922        let result = vocab
923            .extract_and_normalize(Some(&normalizer), "Hi <mask> there\t<mask>\t<mask>\u{2000}");
924        assert_eq!(
925            simplify_output(&result),
926            vec![
927                ("hi", None),
928                // Regular space
929                (" <mask> ", Some(vec![0])),
930                ("there", None),
931                // \t is a spacing character
932                ("\t<mask>\t", Some(vec![0])),
933                // Non overlapping
934                // \u{2000} is mongolian vowel separator: https://jkorpela.fi/chars/spaces.html
935                ("<mask>\u{2000}", Some(vec![0])),
936            ]
937        );
938    }
939
940    #[test]
941    fn test_encode_special_tokens() {
942        let model = ModelMock::new(&[]);
943        let mut vocab = AddedVocabulary::new();
944        let normalizer = Lowercase;
945
946        vocab.add_tokens(
947            &[
948                AddedToken::from("<mask>", true)
949                    .lstrip(true)
950                    .rstrip(true)
951                    .single_word(true),
952                AddedToken::from("ask>", false),
953                AddedToken::from("<pad>", true),
954            ],
955            &model,
956            Some(&normalizer),
957        );
958        vocab.set_encode_special_tokens(true);
959
960        let result = vocab.extract_and_normalize(
961            Some(&normalizer),
962            "Hi <mask> there\t<mask>\t<mask>\u{2000} <pad> <mask><pad><pad>",
963        );
964
965        assert_eq!(
966            simplify_output(&result),
967            vec![
968                ("hi <m", None),
969                ("ask>", Some(vec![1])),
970                (" there\t<m", None),
971                ("ask>", Some(vec![1])),
972                ("\t<m", None),
973                ("ask>", Some(vec![1])),
974                ("\u{2000} <pad> <m", None),
975                ("ask>", Some(vec![1])),
976                ("<pad><pad>", None)
977            ]
978        );
979
980        vocab.set_encode_special_tokens(false);
981
982        let result = vocab.extract_and_normalize(
983            Some(&normalizer),
984            "Hi <mask> there\t<mask>\t<mask>\u{2000} <pad> <mask><pad><pad>",
985        );
986        assert_eq!(
987            simplify_output(&result),
988            vec![
989                ("hi", None),
990                (" <mask> ", Some(vec![0])),
991                ("there", None),
992                ("\t<mask>\t", Some(vec![0])),
993                ("<mask>\u{2000} ", Some(vec![0])),
994                ("<pad>", Some(vec![2])),
995                (" <mask>", Some(vec![0])),
996                ("<pad>", Some(vec![2])),
997                ("<pad>", Some(vec![2]))
998            ]
999        );
1000    }
1001    #[test]
1002    fn byte_level_normalizer() {
1003        // Is able to extract both normal and special tokens
1004        let model = ModelMock::new(&[]);
1005        let mut vocab = AddedVocabulary::new();
1006        let from = NormalizerWrapper::from(ByteLevelNormalizer::new());
1007        let normalizer: Option<&NormalizerWrapper> = Some(&from);
1008
1009        vocab.add_tokens(
1010            &[AddedToken::from("my", false), AddedToken::from("今", false)],
1011            &model,
1012            normalizer,
1013        );
1014        let result = vocab.extract_and_normalize(normalizer, "my今");
1015        assert_eq!(
1016            result
1017                .get_splits(OffsetReferential::Original, OffsetType::Byte)
1018                .into_iter()
1019                .map(|(s, _, tokens)| (
1020                    s,
1021                    tokens
1022                        .as_ref()
1023                        .map(|t| t.iter().map(|t| t.id).collect::<Vec<_>>())
1024                ))
1025                .collect::<Vec<_>>(),
1026            vec![("my", Some(vec![0])), ("ä»Ĭ", Some(vec![1])),]
1027        );
1028    }
1029}