Tercet-R-1.1 / tiny_gdn /detokenize.py
kerzgrr's picture
Upload Tercet-R-1.1 (stage-3 EMA @ step 5300)
90e4c64 verified
Raw
History Blame Contribute Delete
3.93 kB
"""Byte-accurate incremental decode for GPT-2-style byte-level BPE.
`tokenizer.decode(ids)` UTF-8-replaces incomplete sequences. Streaming that
as a string prefix then breaks on the next byte: decode([20492]) is a
replacement character and decode([20492, 294]) is `` ≈``, so the
accumulated text is no longer a prefix and the UI re-emits the whole
think block.
This decoder concatenates raw BPE bytes and only emits complete UTF-8
characters. The completing byte of ``≈`` yields `` ≈``; nothing is rewound.
"""
from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence
from typing import Any
def gpt2_bytes_to_unicode() -> dict[int, str]:
raw = (
list(range(ord("!"), ord("~") + 1))
+ list(range(ord("¡"), ord("¬") + 1))
+ list(range(ord("®"), ord("ÿ") + 1))
)
mapped = raw[:]
extra = 0
for byte in range(256):
if byte not in raw:
raw.append(byte)
mapped.append(256 + extra)
extra += 1
return dict(zip(raw, [chr(code) for code in mapped], strict=True))
def unicode_to_gpt2_bytes() -> dict[str, int]:
return {char: byte for byte, char in gpt2_bytes_to_unicode().items()}
def utf8_char_length(lead: int) -> int | None:
if lead < 0x80:
return 1
if 0xC2 <= lead <= 0xDF:
return 2
if 0xE0 <= lead <= 0xEF:
return 3
if 0xF0 <= lead <= 0xF4:
return 4
return None
def split_complete_utf8(buffer: bytes) -> tuple[str, bytes]:
"""Return (decoded prefix, leftover incomplete or invalid-lead bytes)."""
index = 0
end = len(buffer)
while index < end:
size = utf8_char_length(buffer[index])
if size is None:
break
if index + size > end:
break
chunk = buffer[index : index + size]
try:
chunk.decode("utf-8")
except UnicodeDecodeError:
break
index += size
if index == 0:
return "", buffer
return buffer[:index].decode("utf-8"), buffer[index:]
def piece_to_bytes(piece: str, unicode_to_byte: Mapping[str, int]) -> bytes:
raw = bytearray()
for char in piece:
mapped = unicode_to_byte.get(char)
if mapped is None:
raw.extend(char.encode("utf-8"))
else:
raw.append(mapped)
return bytes(raw)
class IncrementalUtf8Decoder:
"""Push token ids; read only complete Unicode as it becomes available."""
def __init__(
self,
tokenizer: Any,
*,
skip_special_ids: Iterable[int] | None = None,
) -> None:
vocab = tokenizer.get_vocab()
self._id_to_piece = {tid: piece for piece, tid in vocab.items()}
self._unicode_to_byte = unicode_to_gpt2_bytes()
self._skip_special_ids = set(skip_special_ids or ())
self._pending = b""
self._text = ""
@property
def text(self) -> str:
return self._text
def reset(self) -> None:
self._pending = b""
self._text = ""
def token_bytes(self, token_id: int) -> bytes:
piece = self._id_to_piece.get(token_id, "")
return piece_to_bytes(piece, self._unicode_to_byte)
def push(self, token_id: int) -> str:
if token_id in self._skip_special_ids:
return ""
self._pending += self.token_bytes(token_id)
complete, self._pending = split_complete_utf8(self._pending)
self._text += complete
return complete
def push_many(self, token_ids: Sequence[int]) -> str:
delta = []
for token_id in token_ids:
piece = self.push(token_id)
if piece:
delta.append(piece)
return "".join(delta)
def finalize(self) -> str:
if self._pending:
self._text += self._pending.decode("utf-8", errors="replace")
self._pending = b""
return self._text