| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const need = (cond, what) => { |
| if (!cond) throw new Error(`spm_tokenizer: unsupported tokenizer.json: ${what}`); |
| }; |
|
|
| |
| |
| const CACHE_CAP = 4096; |
| const CACHE_MAX_CHARS = 512; |
|
|
| export class SpmTokenizer { |
| |
| |
| constructor(tokenizerJson, config = {}) { |
| const { model, normalizer, pre_tokenizer, post_processor, decoder, added_tokens } = tokenizerJson; |
|
|
| need(model?.type === 'BPE' && !Array.isArray(model.vocab), 'model must be BPE with object vocab'); |
| need(Array.isArray(model.merges?.[0]), 'merges must use the pair-array format'); |
| need(!model.ignore_merges && !model.end_of_word_suffix && !model.continuing_subword_suffix, |
| 'BPE suffix/ignore_merges options'); |
| need(normalizer?.type === 'Sequence' |
| && normalizer.normalizers.map((n) => n.type).join(',') === 'Precompiled,Strip,Replace' |
| && normalizer.normalizers[1].strip_left === false && normalizer.normalizers[1].strip_right === true |
| && normalizer.normalizers[2].pattern?.Regex === ' {2,}', |
| 'normalizer must be [Precompiled, Strip(right), Replace(/ {2,}/)]'); |
| need(pre_tokenizer?.type === 'Metaspace' && pre_tokenizer.prepend_scheme === 'always', |
| 'pre_tokenizer must be Metaspace(always)'); |
| need(decoder?.type === 'Metaspace', 'decoder must be Metaspace'); |
| const single = post_processor?.single; |
| need(post_processor?.type === 'TemplateProcessing' && single?.length === 2 |
| && single[0].Sequence?.id === 'A' && typeof single[1].SpecialToken?.id === 'string', |
| 'post_processor must be TemplateProcessing [A, eos]'); |
| need((added_tokens ?? []).every((t) => !t.normalized && !t.lstrip && !t.rstrip && !t.single_word), |
| 'added tokens must be unnormalized, no lstrip/rstrip/single_word'); |
|
|
| this.replacement = pre_tokenizer.replacement ?? '▁'; |
| this.spaceRun = normalizer.normalizers[2].content ?? ''; |
| this.eosToken = single[1].SpecialToken.id; |
| this.cleanUp = config.clean_up_tokenization_spaces ?? true; |
|
|
| this.vocab = new Map(Object.entries(model.vocab)); |
| this.byteFallback = model.byte_fallback ?? false; |
| this.unkToken = model.unk_token ?? null; |
|
|
| |
| this.merges = new Map(); |
| model.merges.forEach(([a, b], rank) => { |
| let m = this.merges.get(a); |
| if (!m) this.merges.set(a, (m = new Map())); |
| if (!m.has(b)) m.set(b, rank); |
| }); |
|
|
| |
| |
| this.addedContents = []; |
| this.specialTokens = new Set(); |
| for (const t of added_tokens ?? []) { |
| this.addedContents.push(t.content); |
| this.vocab.set(t.content, t.id); |
| if (t.special) this.specialTokens.add(t.content); |
| } |
|
|
| this.idToToken = []; |
| for (const [tok, id] of this.vocab) this.idToToken[id] = tok; |
| this.unkId = this.vocab.get(this.unkToken); |
| this.textEncoder = new TextEncoder(); |
| this.cache = new Map(); |
| } |
|
|
| |
| normalize(text) { |
| text = text.replace(/[\u0001-\u0008\u000B\u000E-\u001F\u007F\u008F\u009F]/g, ''); |
| text = text.replace(/[\u0009\u000A\u000C\u000D\u00A0\u1680\u2000-\u200F\u2028\u2029\u202F\u205F\u2581\u3000\uFEFF\uFFFD]/g, ' '); |
| |
| |
| text = text.includes('~') |
| ? text.split('~').map((p) => p.normalize('NFKC')).join('~') |
| : text.normalize('NFKC'); |
| return text.trimEnd().replace(/ {2,}/gu, this.spaceRun); |
| } |
|
|
| |
| |
| splitOnAdded(text) { |
| const sections = []; |
| let start = 0; |
| for (let i = 0; i < text.length; ) { |
| let match = null; |
| for (const c of this.addedContents) { |
| if (text.startsWith(c, i) && (!match || c.length > match.length)) match = c; |
| } |
| if (!match) { i++; continue; } |
| if (i > start) sections.push(text.slice(start, i)); |
| sections.push(match); |
| i += match.length; |
| start = i; |
| } |
| if (start < text.length) sections.push(text.slice(start)); |
| return sections; |
| } |
|
|
| |
| |
| bpe(word) { |
| for (;;) { |
| let at = -1; |
| let best = Infinity; |
| for (let i = 0; i + 1 < word.length; i++) { |
| const rank = this.merges.get(word[i])?.get(word[i + 1]); |
| if (rank !== undefined && rank < best) { best = rank; at = i; } |
| } |
| if (at < 0) return word; |
| word.splice(at, 2, word[at] + word[at + 1]); |
| } |
| } |
|
|
| |
| piecesToTokens(pieces) { |
| const out = []; |
| for (const p of pieces) { |
| if (this.vocab.has(p)) { |
| out.push(p); |
| } else if (this.byteFallback) { |
| const bytes = Array.from(this.textEncoder.encode(p), |
| (x) => `<0x${x.toString(16).toUpperCase().padStart(2, '0')}>`); |
| if (bytes.every((b) => this.vocab.has(b))) out.push(...bytes); |
| else if (this.unkToken != null) out.push(this.unkToken); |
| } else if (this.unkToken != null) { |
| out.push(this.unkToken); |
| } |
| } |
| return out; |
| } |
|
|
| |
| |
| |
| |
| encode(text, { addSpecialTokens = true } = {}) { |
| const key = (addSpecialTokens ? 'S' : 'R') + text; |
| const cacheable = text.length <= CACHE_MAX_CHARS; |
| if (cacheable) { |
| const hit = this.cache.get(key); |
| if (hit !== undefined) { |
| this.cache.delete(key); |
| this.cache.set(key, hit); |
| return hit.slice(); |
| } |
| } |
| const ids = this.encodeUncached(text, addSpecialTokens); |
| if (cacheable) { |
| this.cache.set(key, ids.slice()); |
| if (this.cache.size > CACHE_CAP) this.cache.delete(this.cache.keys().next().value); |
| } |
| return ids; |
| } |
|
|
| encodeUncached(text, addSpecialTokens) { |
| const tokens = []; |
| for (const section of this.splitOnAdded(text)) { |
| if (this.addedContents.includes(section)) { tokens.push(section); continue; } |
| const norm = this.normalize(section); |
| if (norm.length === 0) continue; |
| let pre = norm.replaceAll(' ', this.replacement); |
| if (!pre.startsWith(this.replacement)) pre = this.replacement + pre; |
| tokens.push(...this.piecesToTokens(this.bpe(Array.from(pre)))); |
| } |
| if (addSpecialTokens) tokens.push(this.eosToken); |
| return tokens.map((t) => this.vocab.get(t) ?? this.unkId); |
| } |
|
|
| |
| decode(ids, { skip_special_tokens = false } = {}) { |
| let tokens = Array.from(ids, (i) => this.idToToken[Number(i)] ?? this.unkToken); |
| if (skip_special_tokens) tokens = tokens.filter((t) => !this.specialTokens.has(t)); |
| let text = ''; |
| for (let i = 0; i < tokens.length; i++) { |
| let s = tokens[i].replaceAll(this.replacement, ' '); |
| if (i === 0 && s.startsWith(' ')) s = s.slice(1); |
| text += s; |
| } |
| if (this.cleanUp) { |
| text = text.replace(/ \./g, '.').replace(/ \?/g, '?').replace(/ !/g, '!') |
| .replace(/ ,/g, ',').replace(/ ' /g, "'").replace(/ n't/g, "n't") |
| .replace(/ 'm/g, "'m").replace(/ 's/g, "'s").replace(/ 've/g, "'ve") |
| .replace(/ 're/g, "'re"); |
| } |
| return text; |
| } |
| } |
|
|