Srna_tokenizer / tokenization_srna.py
procesaur's picture
Upload tokenization_srna.py
1fda116 verified
Raw
History Blame Contribute Delete
14.3 kB
# Copyright 2026 Mihailo Škorić.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tokenization classes for Serbian and Serbo-Croatian models."""
from transformers import PreTrainedTokenizerFast #
from tokenizers import Regex, Tokenizer, decoders, normalizers, pre_tokenizers
from tokenizers.models import BPE
#from ...tokenization_utils_tokenizers import TokenizersBackend
#from ...utils import logging
import regex
#logger = logging.get_logger(__name__)
mapping = {
"Љ": "Lj", "љ": "lj", "Њ": "Nj", "њ": "nj", "Џ": "Dž", "џ": "dž",
"А": "A", "а": "a", "Б": "B", "б": "b", "В": "V", "в": "v",
"Г": "G", "г": "g", "Д": "D", "д": "d", "Ђ": "Đ", "ђ": "đ",
"Е": "E", "е": "e", "Ж": "Ž", "ж": "ž", "З": "Z", "з": "z",
"И": "I", "и": "i", "Ј": "J", "ј": "j", "К": "K", "к": "k",
"Л": "L", "л": "l", "М": "M", "м": "m", "Н": "N", "н": "n",
"О": "O", "о": "o", "П": "P", "п": "p", "Р": "R", "р": "r",
"С": "S", "с": "s", "Т": "T", "т": "t", "Ћ": "Ć", "ћ": "ć",
"У": "U", "у": "u", "Ф": "F", "ф": "f", "Х": "H", "х": "h",
"Ц": "C", "ц": "c", "Ч": "Č", "ч": "č", "Ш": "Š", "ш": "š",
}
reverse_mapping = {v: k for k, v in mapping.items()}
reverse_mapping["dj"] = "ђ"
digraphExceptions = {
"dj": ["adjektiv", "adjon", "adjunkt", "adjuvat", "adjudik", "adjung", "adjunk", "adjutant", "autodjel", "bazdje", "bdje",
"bezdje", "blijedje", "bludje", "bordje", "bridjе", "burdje", "vidjel", "vidjet", "vindjakn", "višedjel", "višenedje",
"vrijedje", "vododjelni", "vodosnabdje", "gdje", "gudje", "gdjir", "daladje", "daždje", "dvodjel", "dvonedje", "devetonedje",
"depardje", "desetonedje", "didje", "djb", "djeva", "djevi", "djevo", "djevstv", "djed", "djejstv", "djel", "djenem", "djeneš",
"djenu", "djet", "djec", "dječ", "djuar", "djubison", "djubouz", "djuer", "djui", "djuks", "djulej", "djumars", "djupont",
"djurant", "djusenberi", "djuharst", "djuherst", "dovdje", "dogrdje", "dodjel", "dosmrdj", "dugodjelujuć", "drvodje",
"drugdje", "elektrosnabdje", "endjurans", "žudje", "zabludje", "zavidje", "zavrijedje", "zagudje", "zadjev", "zadjen",
"zalebdje", "zaludje", "zaodje", "zapodje", "zaprdj", "zarudje", "zasjedje", "zasmrdje", "zastidje", "zaštedje", "zdje",
"zemljedjel", "zlodje", "igdje", "izbledje", "izblijedje", "izvidje", "izdjejst", "izdjelj", "izludje", "isprdje",
"jednodjel", "jednonedje", "koadjutor", "kojegdje", "kudjelj", "lebdje", "ludjel", "ludjet", "makfadjen", "marmadjuk",
"međudjel", "najdjelatn", "najdjelotvornij", "nadjaha", "nadjača", "nadjeb", "nadjev", "nadježd", "nadjezik", "nadjezič",
"nadjenul", "nadjenuo", "nadjenut", "nasmrdj", "neadjuvantne", "negdje", "nedjel", "nadjunač", "nenadjača", "nenadjebi",
"nenavidje", "neoadjuvant", "neodje", "nepodjarm", "neopredjel", "nepodjeljenoj", "nerazdje", "nigdje", "obdjel",
"obnevidje", "ovdje", "odjav", "odjad", "odjah", "odjaš", "odjeb", "odjev", "odjed", "odjezd", "odjek", "odjel",
"odjen", "odjeć", "odjec", "odjur", "odsjedje", "odjesen", "odječ", "ondje", "opredje", "oskudje", "osijedje",
"osmonedje", "otprdjeti", "pardju", "perdju", "petodjel", "petonedje", "poblijedje", "povidje", "pogdjegdje", "pogdje",
"podjaz", "podjakn", "podjamč", "podjastrebačk", "podjastu", "podjemč", "podjar", "podjeb", "podjed", "podjezer",
"podjezik", "podjezic", "podjezič", "podjel", "podjen", "podjesen", "podjet", "podjužnoslovenski", "podjurisdikcije",
"podjurne", "podjurnu", "podjuhorskom", "pododjel", "podrazdjel", "pozavidje", "poludje", "poljodjel", "ponegdje",
"ponedjelj", "poodjača", "poodjezdi", "poodjeknu", "poodjutri", "popridjevljen", "porazdje", "posijedje", "posjedje",
"postadjektiv", "postidje", "potpodjel", "poštedje", "pradjed", "prdje", "preblijedje", "previdje", "predvidje",
"preadjektiv", "predjel", "preodjen", "preraspodje", "presjedje", "pridjev", "pridjen", "predjugosl", "predjulijansk",
"predjustinijansk", "predjutar", "predjutr", "presmrdje", "prisnodjev", "prismrdje", "prištedje", "probdje",
"problijedje", "prodjen", "prolebdje", "prosijedje", "prosjedje", "prosmrdje", "protivdjel", "prošlonedje", "radjard",
"razvidje", "razdjev", "razdjel", "razodje", "raspodje", "rasprdje", "remekdjel", "rudjen", "rudjet", "samoadjungovan",
"samodjel", "samoopredjelje", "sadje", "svagdadjev", "svagdje", "svidje", "svugdje", "sedmonedjelj", "sijedje",
"sjedje", "smrdje", "snabdje", "snovidje", "sredjužnoslovensk", "starosjedje", "stidje", "studje", "sudjel", "transdjus",
"trejdjunion", "trodjeln", "tronedje", "ublijedje", "uvidje", "udjel", "udjen", "uprdje", "usidjel", "usjedje", "usmrdje",
"uštedje", "cjelonedje", "četverodjel", "četvoronedje", "čukundjed", "šestonedjelj", "štedje", "štogdje", "šukundjed"],
"dž": ["feldžandarm", "nadžanj", "nadžanr", "nadždrel", "nadžel", "nadžeo", "nadžet", "nadživ", "nadžinj", "nadžnj",
"nadžrec", "nadžup", "odžali", "odžari", "odžel", "odživ", "odžubor", "odžvaka", "odžval", "odžvać", "podžanr",
"podžargon", "podžel", "podže", "podžig", "podžiz", "podžil", "podžnje", "podžupan", "predželu", "predžetven", "predživot"],
"nj": ["anjon", "benjamin", "vanjezičk", "vanjezičn", "guanju", "dianjon", "injaric", "injekc", "injekt", "injicira",
"injurecesij", "injurij", "izvanjezičk", "izopolianjon", "kenjon", "konjug", "konjunk", "monoanjon", "nekonjug",
"nekonjunk", "netanjahu", "oksianjon", "panjevrej", "panjelinsk", "panjeremen", "panjugosl", "panjudej", "panjuridizacij",
"pinjin", "polianjon", "ssrnj", "subkonjunkt", "tanjug", "čanjol", "šenjang"],
"lj": ["dablju", "epljard", "hiljemark", "metiljodid", "teljurajd"]
}
def cyr2lat(text: str) -> str:
return "".join(mapping.get(ch, ch) for ch in text)
def lat2cyr(text: str) -> str:
result = []
word_buffer = ""
i = 0
while i < len(text):
ch = text[i]
# If character not in any mapping key (single or digraph start), flush buffer
if not any(ch.lower() == k[0] for k in reverse_mapping.keys()):
if word_buffer:
result.append(_convert_word(word_buffer))
word_buffer = ""
result.append(ch) # keep the non‑mapped char (space, number, punctuation)
i += 1
continue
# Otherwise accumulate into buffer
word_buffer += ch
i += 1
# Flush last buffer
if word_buffer:
result.append(_convert_word(word_buffer))
return "".join(result)
def _convert_word(word: str) -> str:
i = 0
out = []
word_lower = word.lower() # compute once
while i < len(word):
matched = False
for digraph in ["dž", "lj", "nj", "dj"]:
if word_lower[i:i+len(digraph)] == digraph:
# check exceptions for this digraph
if any(word_lower.startswith(exc) for exc in digraphExceptions[digraph]):
# expand into individual letters
for ch in digraph:
out.append(reverse_mapping.get(ch, ch))
i += len(digraph)
else:
out.append(reverse_mapping[word[i:i+len(digraph)]])
i += len(digraph)
matched = True
break
if not matched:
out.append(reverse_mapping.get(word[i], word[i]))
i += 1
return "".join(out)
class SrnaTokenizer(PreTrainedTokenizerFast): #
model_input_names = ["input_ids", "attention_mask"]
model = BPE
def __init__(
self,
vocab: str | dict[str, int] | None = None,
merges: str | list[str] | None = None,
vocab_file=None,
merges_file=None,
unk_token: str = "<|endoftext|>",
bos_token=None,
eos_token: str = "<|endoftext|>",
pad_token: str = "<|endoftext|>",
add_prefix_space=None,
boc_token = "<|cyr_start|>",
eoc_token = "<|cyr_end|>",
cap_token = "<|cap|>",
up_token = "<|up|>",
case_compression = True,
script_compression = True,
omit_tags = False,
**kwargs,
):
self.add_prefix_space = add_prefix_space if add_prefix_space is not None else False
self._vocab = (
vocab
if vocab is not None
else {
"<|endoftext|>": 0,
}
)
self.boc_token = boc_token
self.eoc_token = eoc_token
self.cap_token = cap_token
self.up_token = up_token
self.case_compression = case_compression
self.script_compression = script_compression
self.omit_tags = omit_tags
self.PRETOKENIZE_REGEX = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?[\p{L}\p{M}]+|\p{N}| ?[^\s\p{L}\p{M}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"""
self.CYRILLIC_REGEX = r"\s?\p{Cyrillic}[^\p{Latin}]*"
self.CLEANUP_FIND = rf"(\s+){regex.escape(self.eoc_token)}"
self.CLEANUP_REPLACE = rf"{regex.escape(self.eoc_token)}\1"
self.CAPITAL_REGEX = r"\s?\b\p{Lu}\p{Ll}+\b"
self.UPPER_REGEX = r"\s?\b\p{Lu}{2,}\b"
self.UP_FIND = rf"{regex.escape(self.up_token)}(\s?\w+)"
self.CAP_FIND = rf"{regex.escape(self.cap_token)}(\s?\w+)"
self.CYR_FIND = rf"{regex.escape(self.boc_token)}(.*?)({regex.escape(self.eoc_token)}|$)"
self._merges = merges or []
self._tokenizer = Tokenizer(
BPE(
vocab=self._vocab,
merges=self._merges,
dropout=None,
unk_token=None,
continuing_subword_prefix="",
end_of_word_suffix="",
fuse_unk=False,
byte_fallback=False,
)
)
self._tokenizer.decoder = decoders.ByteLevel()
self._tokenizer.normalizer = normalizers.NFC()
self._tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
[
pre_tokenizers.Split(
Regex(self.PRETOKENIZE_REGEX),
behavior="isolated",
invert=False,
),
pre_tokenizers.ByteLevel(
add_prefix_space=self.add_prefix_space,
use_regex=False,
),
]
)
super().__init__(
vocab_file=vocab_file,
merges_file=merges_file,
unk_token=unk_token,
bos_token=bos_token,
eos_token=eos_token,
pad_token=pad_token,
add_prefix_space=add_prefix_space,
**kwargs,
)
def prepare_for_tokenization(self, text: str, is_split_into_words=False, **kwargs):
def mark_cap(match):
if self.omit_tags:
return match.group(0).lower()
return f"{self.cap_token}{match.group(0).lower()}"
def mark_up(match):
if self.omit_tags:
return match.group(0).lower()
return f"{self.up_token}{match.group(0).lower()}"
def wrap_and_transliterate(match):
cyr_text = match.group(0)
latin_text = cyr2lat(cyr_text)
if self.omit_tags:
return latin_text
return f"{self.boc_token}{latin_text}{self.eoc_token}"
if self.script_compression:
text = regex.sub(self.CYRILLIC_REGEX, wrap_and_transliterate, text)
if text.endswith(self.eoc_token):
text = text[: -len(self.eoc_token)]
text = regex.sub(self.CLEANUP_FIND, self.CLEANUP_REPLACE, text)
if self.case_compression:
text = regex.sub(self.CAPITAL_REGEX, mark_cap, text)
text = regex.sub(self.UPPER_REGEX, mark_up, text)
return text
def encode(self, text: str, *args, **kwargs):
text = self.prepare_for_tokenization(text)
return super().encode(text, *args, **kwargs)
def _encode_plus(self, text, *args, **kwargs):
if self.add_prefix_space:
text = text.lstrip("\n\t")
if not text.startswith(" "):
text = " " + text
# apply your preprocessing hook
text = self.prepare_for_tokenization(text, **kwargs)
return super()._encode_plus(text, *args, **kwargs)
def decode(self, token_ids, *args, **kwargs):
text = super().decode(token_ids, *args, **kwargs)
# then your custom tag decoding
def decode_cyr(match):
return lat2cyr(match.group(1))
def decode_cap(match):
inner = match.group(1)
leading_ws = regex.match(r"^\s*", inner).group(0)
if not leading_ws:
return inner[0].upper() + inner[1:]
core = inner.lstrip()
if core:
core = core[0].upper() + core[1:]
return leading_ws + core
def decode_allcaps(match):
inner = match.group(1)
return inner.upper()
if self.case_compression:
text = regex.sub(self.UP_FIND, decode_allcaps, text)
text = regex.sub(self.CAP_FIND, decode_cap, text)
if self.script_compression:
text = regex.sub(self.CYR_FIND, decode_cyr, text)
return text
__all__ = ["SrnaTokenizer"]