File size: 2,857 Bytes
4113449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Word-level tokenizer with GPT-2 byte-level pre-tokenization, matching tokenizer.json
// (NFKC + lowercase, add_prefix_space, GPT-2 split regex). No Node or DOM APIs, so it
// is shared by model.ts (Node/Bun) and web/autocomplete.ts (browser).

function bytesToUnicode(): Map<number, string> {
  const bs: number[] = [];
  for (let b = 33; b <= 126; b++) bs.push(b);
  for (let b = 161; b <= 172; b++) bs.push(b);
  for (let b = 174; b <= 255; b++) bs.push(b);
  const cs = [...bs];
  let n = 0;
  for (let b = 0; b < 256; b++) {
    if (!bs.includes(b)) {
      bs.push(b);
      cs.push(256 + n++);
    }
  }
  return new Map(bs.map((b, i) => [b, String.fromCodePoint(cs[i])]));
}

const BYTE_ENC = bytesToUnicode();
const BYTE_DEC = new Map([...BYTE_ENC].map(([b, c]) => [c, b]));
const GPT2_SPLIT = /'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu;
const encoder = new TextEncoder();
const decoder = new TextDecoder();

/** Byte-level pre-tokens ("Ġword" style), exactly as the Python tokenizer splits text. */
export function preTokenize(text: string): string[] {
  let norm = text.normalize("NFKC").toLowerCase();
  if (norm && !/^\s/.test(norm)) norm = " " + norm; // ByteLevel add_prefix_space
  return (norm.match(GPT2_SPLIT) ?? []).map((piece) => [...encoder.encode(piece)].map((b) => BYTE_ENC.get(b)!).join(""));
}

/** Byte-level piece(s) back to text. */
export function decodePieces(pieces: string): string {
  return decoder.decode(new Uint8Array([...pieces].map((c) => BYTE_DEC.get(c) ?? 63)));
}

export class WordTokenizer {
  vocab: Map<string, number>;
  inverse: string[];
  special: Set<number>;
  unkId: number;
  bosId: number;

  constructor(pieces: string[], unkId: number, bosId: number, special: Iterable<number>) {
    this.unkId = unkId;
    this.bosId = bosId;
    this.inverse = pieces;
    this.vocab = new Map(pieces.map((p, i) => [p, i]));
    this.special = new Set(special);
  }

  /** tokenizer.json (WordLevel model, BOS template post-processor) -> tokenizer. */
  static fromJson(json: any): WordTokenizer {
    const pieces: string[] = [];
    for (const [tok, id] of Object.entries<number>(json.model.vocab)) pieces[id] = tok;
    const bos = json.post_processor?.single?.find((p: any) => p.SpecialToken)?.SpecialToken?.id;
    const special = (json.added_tokens ?? []).filter((t: any) => t.special).map((t: any) => t.id);
    return new WordTokenizer(pieces, json.model.vocab[json.model.unk_token], json.model.vocab[bos], special);
  }

  idOf(piece: string): number {
    return this.vocab.get(piece) ?? this.unkId;
  }

  encode(text: string): number[] {
    return [this.bosId, ...preTokenize(text).map((p) => this.idOf(p))];
  }

  decode(ids: number[]): string {
    return decodePieces(ids.filter((id) => !this.special.has(id)).map((id) => this.inverse[id]).join(""));
  }
}