gemma-3-270m-uk-verbalizer / digit_router.py
skypro1111's picture
v55: safetensors + GGUF + ONNX, картка з описом прунінгу та експортів
a7778e5 verified
Raw History Blame Contribute Delete
4.9 kB
# -*- coding: utf-8 -*-
"""Детермінований роутер: голий рядок цифр -> читання ПО ЦИФРІ, без моделі.
Навіщо коду, а не даними: поцифрове читання вимагає ТОЧНОГО збереження
кількості цифр, а модель саме цього не вміє — виміряно на v42:
«Код 0000.» -> «нуль нуль нуль» (3 з 4)
«Код 000000.» -> «нуль нуль нуль нуль» (4 з 6)
Навчити її поцифровому читанню = успадкувати цей баг тихо й на довгих ID.
Тут кількість гарантована кодом.
Спрацьовує ЛИШЕ якщо ВЕСЬ вхід — суцільні цифри від MIN_DIGITS знаків.
Не спрацьовує:
* якщо є роздільники («10 000 001» — пробіл є сигналом «це кількість»);
* якщо число всередині речення (там модель читає кількістю, 17/19 правильно);
* якщо цифр менше за поріг («42», «2026» модель читає правильно).
"""
import re
MIN_DIGITS = 7
DIGIT = {"0": "нуль", "1": "один", "2": "два", "3": "три", "4": "чотири",
"5": "п'ять", "6": "шість", "7": "сім", "8": "вісім", "9": "дев'ять"}
_BARE = re.compile(r"\A\s*(\d+)\s*\Z")
def match_bare_digits(text: str):
"""рядок цифр, якщо вхід — голе число, яке треба читати по цифрі, інакше None.
Два випадки:
* від MIN_DIGITS знаків — довгий рядок без контексту це ідентифікатор;
* ПРОВІДНИЙ НУЛЬ за будь-якої довжини («007», «0000») — провідний нуль
означає, що це не кількість; заразом лікує серії нулів, де модель
губила цифри («000000» -> лише п'ять «нуль»).
"""
m = _BARE.match(text or "")
if not m:
return None
digits = m.group(1)
if len(digits) >= MIN_DIGITS:
return digits
if len(digits) >= 2 and digits[0] == "0":
return digits
return None
def spell_digits(digits: str) -> str:
return " ".join(DIGIT[d] for d in digits)
def route(text: str):
"""(handled, output). handled=False -> віддати моделі як є."""
d = match_bare_digits(text)
if d is None:
return False, None
return True, spell_digits(d)
# --------------------------------------------------------------------------
if __name__ == "__main__":
CASES = [
# (вхід, очікується_перехоплення, очікуваний_вихід_або_None)
("12345678", True, "один два три чотири п'ять шість сім вісім"),
("10000001", True, "один нуль нуль нуль нуль нуль нуль один"),
("000000", True, "нуль нуль нуль нуль нуль нуль"), # провідний нуль
("0000000", True, "нуль нуль нуль нуль нуль нуль нуль"),
("123456789012", True, None), # 12 цифр: поза межами рушія, роутер тягне
(" 918273645 ", True, None), # пробіли по краях не заважають
("42", False, None),
("007", True, "нуль нуль сім"),
("0000", True, "нуль нуль нуль нуль"),
("2026", False, None),
("10 000 001", False, None), # роздільники -> кількість
("0800 500 000", False, None), # телефон -> наявна конвенція
("У звіті вказано число 12345678.", False, None), # у реченні -> кількість
("12345678.", False, None), # крапка = вже не голе число
("№1024", False, None),
("", False, None),
]
bad = 0
for text, want_handled, want_out in CASES:
handled, out = route(text)
ok = handled == want_handled and (want_out is None or out == want_out)
bad += not ok
print(f" [{'OK ' if ok else 'FAIL'}] {text!r:34} -> handled={handled} {out or ''}")
# інваріант: кількість слів завжди дорівнює кількості цифр
for n in ("0000000", "1234567890", "9" * 20, "1000000000"):
assert len(spell_digits(n).split()) == len(n), n
print(f"\nінваріант «слів = цифр» тримається")
print("ПРОВАЛЕНО" if bad else "ВСІ ТЕСТИ ОК")