| """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 |
|
|