sprout / tokenizer.js
GeorgeCodesSometimes's picture
Launch Sprout story workshop with verified in-browser model inference
975c59e verified
Raw History Blame Contribute Delete
2.96 kB
// The same byte BPE and Unicode pre-splitting as tokenizer.py.
const pattern = /'(?:[sSdDmMtT]|[lL][lL]|[vV][eE]|[rR][eE])|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\p{White_Space}\p{L}\p{N}]+[\r\n]*|\p{White_Space}*[\r\n]|\p{White_Space}+(?![^\p{White_Space}])|\p{White_Space}+/gu;
const specials = ["<|endoftext|>", "<|user|>", "<|assistant|>", "<|end|>"];
export function tokenizer(text) {
const pairs = text.trim().split(/\r?\n/).map(line => line.split(" ").map(Number));
const ranks = new Map();
const vocab = Array.from({ length: 256 }, (_, i) => new Uint8Array([i]));
for (const [a, b] of pairs) {
ranks.set(`${a},${b}`, vocab.length);
vocab.push(new Uint8Array([...vocab[a], ...vocab[b]]));
}
const special = Object.fromEntries(specials.map((s, i) => [s, vocab.length + i]));
const encoder = new TextEncoder();
const split = /(<\|endoftext\|>|<\|user\|>|<\|assistant\|>|<\|end\|>)/g;
function encode(text) {
const result = [];
for (const part of text.split(split)) {
if (part in special) {
result.push(special[part]);
continue;
}
for (const match of part.matchAll(pattern)) {
let ids = Array.from(encoder.encode(match[0]));
while (ids.length > 1) {
let rank = Infinity;
let a, b;
for (let i = 0; i < ids.length - 1; i++) {
const value = ranks.get(`${ids[i]},${ids[i + 1]}`) ?? Infinity;
if (value < rank) { rank = value; a = ids[i]; b = ids[i + 1]; }
}
if (!Number.isFinite(rank)) break;
const merged = [];
for (let i = 0; i < ids.length; i++) {
if (ids[i] === a && ids[i + 1] === b) { merged.push(rank); i++; }
else merged.push(ids[i]);
}
ids = merged;
}
result.push(...ids);
}
}
return result;
}
return { encode, vocab, special };
}
export function storyPrompt(fields) {
const names = ["Summary", "Words", "Random sentence", "Features"];
if (Object.keys(fields).some(key => !names.includes(key)) ||
Object.values(fields).some(value => typeof value !== "string")) {
throw new Error("Use the supported story fields with text values.");
}
if (Object.values(fields).reduce((n, value) => n + value.length, 0) > 4096) {
throw new Error("Your instructions are too long. Try a shorter story idea.");
}
if (Object.values(fields).some(value => specials.some(s => value.includes(s)))) {
throw new Error("Remove reserved tokens such as <|user|> from your instructions.");
}
const clean = names.map(key => [key, (fields[key] ?? "").replace(/[\p{White_Space}\u001c-\u001f]+/gu, " ").replace(/^ | $/g, "")]);
const lines = clean.filter(([, value]) => value).map(([key, value]) => `${key}: ${value}`);
if (!lines.length) throw new Error("Enter a story idea or a word to include.");
return `<|user|>\nWrite a short story.\n${lines.join("\n")}<|end|>\n<|assistant|>\n`;
}