File size: 2,937 Bytes
21ab33c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import initializeKitoken, { Kitoken } from "kitoken/web";

import { applySpecialTemplates, type TokenizerTemplate } from "./templates";
import type { TokenizerFormat } from "./types";

let initialization: Promise<unknown> | null = null;

export async function initializeEngine(): Promise<void> {
  initialization ??= initializeKitoken();
  await initialization;
}

function createKitoken(bytes: Uint8Array, format: TokenizerFormat): Kitoken {
  switch (format) {
    case "tokenizers":
      return Kitoken.from_tokenizers(bytes);
    case "sentencepiece":
      return Kitoken.from_sentencepiece(bytes);
    case "tiktoken":
      return Kitoken.from_tiktoken(bytes);
    case "tekken":
      return Kitoken.from_tekken(bytes);
  }
}

function readTemplates(instance: Kitoken): TokenizerTemplate[] {
  const config = instance.config() as unknown;
  if (!config || typeof config !== "object" || !("templates" in config)) return [];
  const templates = (config as { templates?: unknown }).templates;
  if (!Array.isArray(templates)) return [];

  return templates.filter(
    (template): template is TokenizerTemplate =>
      Boolean(
        template &&
          typeof template === "object" &&
          "content" in template &&
          typeof template.content === "string" &&
          "position" in template &&
          typeof template.position === "string",
      ),
  );
}

export class WebTokenizerEngine {
  readonly templates: TokenizerTemplate[];
  private readonly decoder = new TextDecoder();
  private readonly fatalDecoder = new TextDecoder("utf-8", { fatal: true });

  private constructor(private readonly instance: Kitoken) {
    this.templates = readTemplates(instance);
  }

  static async create(
    bytes: Uint8Array,
    format: TokenizerFormat,
  ): Promise<WebTokenizerEngine> {
    await initializeEngine();
    return new WebTokenizerEngine(createKitoken(bytes, format));
  }

  encode(
    text: string,
    addSpecialTokens: boolean,
    templates: TokenizerTemplate[],
  ): { ids: number[]; templateSpecialIds: Set<number> } {
    const ids = Array.from(this.instance.encode(text, true));
    if (!addSpecialTokens) return { ids, templateSpecialIds: new Set() };

    const applied = applySpecialTemplates(ids, templates, (content) =>
      Array.from(this.instance.encode(content, true)),
    );
    return { ids: applied.ids, templateSpecialIds: applied.specialIds };
  }

  decodeToken(id: number): { text: string; byteDisplay: string | null } {
    const bytes = this.instance.decode(Uint32Array.of(id), true);
    const text = this.decoder.decode(bytes);
    try {
      this.fatalDecoder.decode(bytes);
      return { text, byteDisplay: null };
    } catch {
      return {
        text,
        byteDisplay: Array.from(bytes, (byte) =>
          `\\x${byte.toString(16).padStart(2, "0").toUpperCase()}`,
        ).join(""),
      };
    }
  }

  dispose(): void {
    this.instance.free();
  }
}