Spaces:
Sleeping
Sleeping
Download rules/cards.py from frznkshn/TextNormalizer: direct link, hf CLI and curl.
- Browser
- Download file 4.21 kB
-
https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/rules/cards.py
- Command line
-
hf download hf://spaces/frznkshn/TextNormalizer/rules/cards.py
-
curl -L -o cards.py https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/rules/cards.py
4.21 kB
| import random | |
| import re | |
| from num2words import num2words | |
| from rules.base import BaseProcessor | |
| from rules.phone import PhoneNumberProcessor | |
| from utils import NO_LETTERS, detect_digit_language | |
| # ASCII, Persian (۰-۹) and Arabic-Indic (٠-٩) digits. | |
| DIGIT = "0-9۰-۹٠-٩" | |
| TO_ASCII = str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩", "01234567890123456789") | |
| GROUP = rf"[{DIGIT}]{{4}}" | |
| SEPARATOR = r"(?:[ \t]*[-–][ \t]*|[ \t]+)" | |
| # Four groups of four digits (`6219 - 8765 - 2345 - 9031`, `6219-8765-2345-9031`, | |
| # `6219 8765 2345 9031`) or sixteen digits in a row. It must not be glued to a | |
| # word, a longer number, a decimal or a URL path. | |
| CARD_PATTERN = re.compile( | |
| rf"(?<![\w.,/+\-])" | |
| rf"(?<![{DIGIT}][ \t])(?<![{DIGIT}][-–])(?<![{DIGIT}][-–][ \t])(?<![{DIGIT}][ \t][-–])(?<![{DIGIT}][ \t][-–][ \t])" | |
| rf"(?:{GROUP}(?:{SEPARATOR}{GROUP}){{3}}|[{DIGIT}]{{16}})" | |
| rf"(?![\w/]|[-–.,][{DIGIT}]|{SEPARATOR}[{DIGIT}])" | |
| ) | |
| # English reads a pair that starts with 0 as "oh five" and `00` as "double zero". | |
| ENGLISH_ZERO = "oh" | |
| ENGLISH_DOUBLE_ZERO = "double zero" | |
| GROUP_JOINER = " - " | |
| PAIR_JOINER = {"fa": "، ", "en": ", "} | |
| class CardNumberProcessor(BaseProcessor): | |
| """Reads 16-digit bank card numbers in pairs of digits. | |
| Persian: `۶۲۱۹ - ۸۷۶۵ - ۲۳۴۵ - ۹۰۳۱` | |
| -> `شصت و دو، نوزده - هشتاد و هفت، شصت و پنج - بیست و سه، چهل و پنج - نود، سی و یک` | |
| English: `4111 1111 1111 1111` | |
| -> `forty-one, eleven - eleven, eleven - eleven, eleven - eleven, eleven` | |
| `5105-1051-0510-5100` | |
| -> `fifty-one, oh five - ten, fifty-one - oh five, ten - fifty-one, double zero` | |
| Each group of four digits is said as two numbers separated by a pause | |
| (comma), and the groups are separated by ` - `. A pair that starts with `0` | |
| keeps its zero: Persian `05` -> `صفر پنج`, `00` -> `دو صفر`; English `05` -> | |
| `oh five`, `00` -> `double zero`. | |
| Recognised: four groups of four digits separated by spaces or hyphens (or | |
| sixteen digits in a row), written with ASCII, Persian or Arabic-Indic digits, | |
| with any first digit. Left alone: other lengths (15-digit Amex, 19-digit), | |
| masked numbers (`6219-****-****-9031`), numbers glued to words, and decimals. | |
| With no letters in the text, the digits pick the language: Persian digits -> | |
| Persian, ASCII digits -> English. This processor must run before | |
| `PhoneNumberProcessor`, because a word like `شماره` ("number") before a card | |
| would otherwise make the phone processor read it. | |
| """ | |
| def __init__(self, epsilon: float = 1.0): | |
| super(CardNumberProcessor, self).__init__(epsilon=epsilon) | |
| def process(self, text: str, language: str = None, *args, **kwargs) -> str: | |
| if random.random() > self.epsilon: | |
| return text | |
| if re.search(rf"[{DIGIT}]", text) is None: | |
| return text | |
| if language in (None, NO_LETTERS): | |
| language = detect_digit_language(text) | |
| if language not in ("fa", "en"): | |
| return text | |
| return CARD_PATTERN.sub(lambda match: self._convert(match, language), text) | |
| def _convert(self, match: re.Match, language: str) -> str: | |
| digits = re.sub(rf"[^{DIGIT}]", "", match.group(0)).translate(TO_ASCII) | |
| if len(digits) != 16: | |
| return match.group(0) | |
| groups = [digits[index:index + 4] for index in range(0, 16, 4)] | |
| return GROUP_JOINER.join( | |
| PAIR_JOINER[language].join( | |
| self._pair_to_words(group[start:start + 2], language) for start in (0, 2) | |
| ) | |
| for group in groups | |
| ) | |
| def _pair_to_words(pair: str, language: str) -> str: | |
| if language == "fa": | |
| # Same reading as inside a phone number: `62` -> `شصت و دو`, | |
| # `05` -> `صفر پنج`, `00` -> `دو صفر`. | |
| return PhoneNumberProcessor._persian_number(pair) | |
| if pair == "00": | |
| return ENGLISH_DOUBLE_ZERO | |
| if pair.startswith("0"): | |
| return f"{ENGLISH_ZERO} {num2words(int(pair))}" | |
| return num2words(int(pair)) |