| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| export interface Token { |
| |
| id: number; |
| |
| |
| text: string; |
| |
| start: number; |
| |
| end: number; |
| } |
|
|
| |
| export interface Encoding { |
| inputIds: BigInt64Array; |
| attentionMask: BigInt64Array; |
| tokens: Token[]; |
| } |
|
|
| |
| |
| export interface Tokenizer { |
| tokenize(text: string, opts?: { maxLength?: number }): Encoding; |
| |
| |
| |
| specials: { cls: number; sep: number; pad: number }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function loadTokenizer( |
| src: string | ArrayBuffer | Uint8Array, |
| ): Promise<Tokenizer> { |
| let json: unknown; |
| if (typeof src === 'string') { |
| const isBrowser = typeof window !== 'undefined'; |
| if (isBrowser || src.startsWith('http')) { |
| const r = await fetch(src); |
| if (!r.ok) throw new Error(`tokenizer fetch failed: ${r.status}`); |
| json = await r.json(); |
| } else { |
| |
| const fs = await import('node:fs/promises'); |
| const buf = await fs.readFile(src, 'utf-8'); |
| json = JSON.parse(buf); |
| } |
| } else { |
| const decoder = new TextDecoder(); |
| const buf = |
| src instanceof Uint8Array ? src : new Uint8Array(src); |
| json = JSON.parse(decoder.decode(buf)); |
| } |
|
|
| |
| |
| |
| |
| try { |
| const spec = '@huggingface/tokenizers'; |
| const hf = await import( spec); |
| return makeHfTokenizer(hf, json); |
| } catch { |
| return makeMinimalTokenizer(json); |
| } |
| } |
|
|
| |
|
|
| function makeHfTokenizer(hf: unknown, json: unknown): Tokenizer { |
| const Mod = hf as { |
| Tokenizer: { fromString(s: string): { encode(t: string): unknown } }; |
| }; |
| const tk = Mod.Tokenizer.fromString(JSON.stringify(json)); |
|
|
| const specials = pickSpecials(json); |
|
|
| return { |
| specials, |
| tokenize(text, opts) { |
| const max = opts?.maxLength ?? 256; |
| const enc = tk.encode(text) as { |
| getIds(): number[]; |
| getAttentionMask(): number[]; |
| getOffsets(): [number, number][]; |
| getTokens(): string[]; |
| }; |
| const ids = enc.getIds().slice(0, max); |
| const attn = enc.getAttentionMask().slice(0, max); |
| const offsets = enc.getOffsets().slice(0, max); |
| const toks = enc.getTokens().slice(0, max); |
| const tokens: Token[] = ids.map((id, i) => ({ |
| id, |
| text: toks[i], |
| start: offsets[i][0], |
| end: offsets[i][1], |
| })); |
| return { |
| inputIds: BigInt64Array.from(ids.map((x) => BigInt(x))), |
| attentionMask: BigInt64Array.from(attn.map((x) => BigInt(x))), |
| tokens, |
| }; |
| }, |
| }; |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function makeMinimalTokenizer(json: unknown): Tokenizer { |
| const obj = json as { |
| model: { |
| type: string; |
| vocab: [string, number][]; |
| unk_id?: number; |
| }; |
| added_tokens?: { id: number; content: string }[]; |
| }; |
| if (obj.model.type !== 'Unigram') { |
| throw new Error( |
| `minimal tokenizer only supports Unigram; got ${obj.model.type}. ` + |
| 'Install @huggingface/tokenizers for full support.', |
| ); |
| } |
| const vocab = new Map<string, number>(); |
| const scores = new Map<string, number>(); |
| for (const [piece, score] of obj.model.vocab) { |
| vocab.set(piece, vocab.size); |
| scores.set(piece, score); |
| } |
| const unk = obj.model.unk_id ?? vocab.get('<unk>') ?? 0; |
| const specials = pickSpecials(json); |
|
|
| const SPACE = 'β'; |
|
|
| function encode(text: string, max: number): Encoding { |
| |
| const norm = text.normalize('NFKC'); |
| const piece = SPACE + norm.replace(/ /g, SPACE); |
|
|
| |
| |
| |
| |
| const ids: number[] = [specials.cls]; |
| const tokens: Token[] = [ |
| { id: specials.cls, text: '<s>', start: 0, end: 0 }, |
| ]; |
| let p = 1; |
| let charPos = 0; |
| while (p < piece.length && ids.length < max - 1) { |
| let bestLen = 0; |
| let bestId = unk; |
| let bestText = ''; |
| for (let len = Math.min(piece.length - p, 24); len >= 1; len--) { |
| const slice = piece.substring(p, p + len); |
| const id = vocab.get(slice); |
| if (id !== undefined) { |
| bestLen = len; |
| bestId = id; |
| bestText = slice; |
| break; |
| } |
| } |
| if (bestLen === 0) { |
| bestLen = 1; |
| bestText = piece[p]; |
| } |
| const charLen = bestText.replace(SPACE, ' ').length; |
| const start = charPos; |
| const end = charPos + charLen; |
| tokens.push({ id: bestId, text: bestText, start, end }); |
| ids.push(bestId); |
| p += bestLen; |
| charPos = end; |
| } |
| ids.push(specials.sep); |
| tokens.push({ |
| id: specials.sep, |
| text: '</s>', |
| start: charPos, |
| end: charPos, |
| }); |
| const attn = ids.map(() => 1n); |
| return { |
| inputIds: BigInt64Array.from(ids.map((x) => BigInt(x))), |
| attentionMask: BigInt64Array.from(attn), |
| tokens, |
| }; |
| } |
|
|
| return { |
| specials, |
| tokenize(text, opts) { |
| return encode(text, opts?.maxLength ?? 256); |
| }, |
| }; |
| } |
|
|
| function pickSpecials(json: unknown): { |
| cls: number; |
| sep: number; |
| pad: number; |
| } { |
| const obj = json as { |
| added_tokens?: { id: number; content: string }[]; |
| model: { vocab: [string, number][] }; |
| }; |
| |
| |
| const map = new Map<string, number>(); |
| if (obj.added_tokens) { |
| for (const t of obj.added_tokens) map.set(t.content, t.id); |
| } |
| if (map.size === 0) { |
| let i = 0; |
| for (const [piece] of obj.model.vocab) map.set(piece, i++); |
| } |
| const cls = map.get('<s>') ?? map.get('[CLS]') ?? 0; |
| const sep = map.get('</s>') ?? map.get('[SEP]') ?? 2; |
| const pad = map.get('<pad>') ?? map.get('[PAD]') ?? 1; |
| return { cls, sep, pad }; |
| } |
|
|