nanofable / js /display.js
Adit Rahman
NanoFable static demo: all 8 packs served same-origin
7b33d33
Raw
History Blame Contribute Delete
3.94 kB
// Display-time repair of the TinyStories corpus's CP1252 mojibake.
// The dataset stores non-ASCII punctuation double-encoded (each UTF-8 byte re-read
// as CP1252), so the model legitimately *generates* mojibake β€” the sequences are
// merged BPE tokens β€” and it can also emit *fragments* of them (a continuation
// token without its lead byte), which no lookup table of known sequences can
// anticipate. So the encoding is reversed generically instead: map each char back
// to its CP1252 byte, decode valid UTF-8 runs to the intended character, and treat
// anything that fails as mojibake residue.
//
// Residue policy differs by source:
// - model output (mojibakeFilter): residue is DROPPED β€” the corpus's visible text
// is ASCII + mojibake only, so an undecodable non-ASCII char from the model is a
// broken fragment, never content. Exception: a bare "Ò€" whose third byte was
// lost is the corpus's broken closing quote β€” rendered ”.
// - user input (fixMojibake): residue is KEPT β€” people legitimately type curly
// quotes, Γ©, emoji.
// Token ids, the tokenizer, the dataset, and the parity hooks are untouched.
// CP1252 codepoint -> byte for the 0x80–0x9F specials. Every other char below
// 0x100 maps to itself β€” including the bytes CP1252 leaves *undefined* (e.g.
// 0x9D), which the dataset pipeline carried through as literal C1 controls.
const CP1252_HIGH = {
0x20ac: 0x80, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, 0x2026: 0x85,
0x2020: 0x86, 0x2021: 0x87, 0x02c6: 0x88, 0x2030: 0x89, 0x0160: 0x8a,
0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8e, 0x2018: 0x91, 0x2019: 0x92,
0x201c: 0x93, 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
0x02dc: 0x98, 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c,
0x017e: 0x9e, 0x0178: 0x9f,
};
// undefined = not CP1252-expressible, so it cannot be part of mojibake.
const byteOf = (ch) => {
const cp = ch.codePointAt(0);
return cp < 0x100 ? cp : CP1252_HIGH[cp];
};
const seqLen = (b) =>
b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0;
const isCont = (b) => b >= 0x80 && b <= 0xbf;
const utf8 = new TextDecoder("utf-8", { fatal: true });
// One pass over `s`. When !atEnd, an unfinished candidate sequence at the tail is
// returned in `hold` for the next chunk to complete (streaming-safe splits).
function unmojibake(s, atEnd, keepResidue) {
let out = "";
let i = 0;
while (i < s.length) {
const ch = s[i];
const b = byteOf(ch);
if (b === undefined || b < 0x80) {
out += ch;
i++;
continue;
}
const need = seqLen(b);
const bytes = [b];
let j = i + 1;
while (j < s.length && bytes.length < need) {
const cb = byteOf(s[j]);
if (cb === undefined || !isCont(cb)) break;
bytes.push(cb);
j++;
}
if (need && bytes.length === need) {
try {
const decoded = utf8.decode(new Uint8Array(bytes));
if (decoded.codePointAt(0) > 0x9f) {
// (≀ 0x9f would be an invisible C1 control β€” treat as residue)
out += decoded;
i = j;
continue;
}
} catch {
/* not a real sequence β€” residue */
}
} else if (need && j === s.length && !atEnd) {
return { out, hold: s.slice(i) };
}
if (b === 0xe2 && bytes.length >= 2 && bytes[1] === 0x80) {
out += "”"; // corpus's broken closing quote (its 0x9d byte lost)
i += 2;
continue;
}
if (keepResidue) out += ch;
i++;
}
return { out, hold: "" };
}
export function fixMojibake(s) {
return unmojibake(s, true, true).out;
}
export function mojibakeFilter() {
let buf = "";
return {
push(text) {
const { out, hold } = unmojibake(buf + text, false, false);
buf = hold;
return out;
},
flush() {
const { out } = unmojibake(buf, true, false);
buf = "";
return out;
},
};
}