Text Generation
Transformers
Safetensors
Sanskrit
sansar
sanskrit
devanagari
slp1
causal-lm
base-model
custom_code
Eval Results (legacy)
Instructions to use MuseMesh/sansar-60m with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use MuseMesh/sansar-60m with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="MuseMesh/sansar-60m", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("MuseMesh/sansar-60m", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use MuseMesh/sansar-60m with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "MuseMesh/sansar-60m" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "MuseMesh/sansar-60m", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/MuseMesh/sansar-60m
- SGLang
How to use MuseMesh/sansar-60m with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "MuseMesh/sansar-60m" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "MuseMesh/sansar-60m", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "MuseMesh/sansar-60m" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "MuseMesh/sansar-60m", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use MuseMesh/sansar-60m with Docker Model Runner:
docker model run hf.co/MuseMesh/sansar-60m
Download translit.py from MuseMesh/sansar-60m: direct link, hf CLI and curl.
- Browser
- Download file 22.9 kB
-
https://huggingface.co/MuseMesh/sansar-60m/resolve/main/translit.py
- Command line
-
hf download hf://MuseMesh/sansar-60m/translit.py
-
curl -L -o translit.py https://huggingface.co/MuseMesh/sansar-60m/resolve/main/translit.py
22.9 kB
| #!/usr/bin/env python3 | |
| """Strict, table-driven, REVERSIBLE Devanagari <-> SLP1 and Devanagari <-> IAST. | |
| Design rules (E1 of docs/TOKENIZATION.md): | |
| 1. NFC is the frozen normalisation. Every public function normalises its input to NFC | |
| first and returns NFC output. NFC *decomposes* the precomposed nukta letters | |
| U+0958-U+095F, U+0929, U+0931, U+0934 (they are composition-excluded), so after NFC a | |
| nukta is always a separate U+093C. Both forms are in the tables anyway. | |
| 2. Fail closed, never drop. A code point with no mapping is passed through unchanged AND | |
| reported: every function returns ``(text, unmapped)`` where ``unmapped`` is a | |
| ``collections.Counter`` keyed by the offending character. Nothing is ever silently lost. | |
| 3. Round trip is the contract. deva -> slp1 -> deva and deva -> iast -> deva must return | |
| the NFC input exactly, for every input, *unless* the input contains Latin letters | |
| (see 5) or malformed Devanagari, both of which are reported. | |
| 4. Reserved ASCII. The romanised sides use a few ASCII characters as functional markers: | |
| ^ nukta (follows the consonant, precedes the vowel: k^a = U+0915 U+093C) | |
| ' avagraha % om (U+0950) | |
| | cluster separator, only emitted where IAST would otherwise be ambiguous | |
| (k|ha = क्ह, not ख; ka|i = कइ, not कै). It is only ever emitted BETWEEN two | |
| letters, so on input a '|' that touches whitespace is read as a literal | |
| character -- that is the ASCII dandaa of GRETIL-style romanised sources. | |
| ~ candrabindu (SLP1 only) / udatta U+0951 (SLP1 only) | |
| + short vowel prefix: +e +o +a (SLP1 only; IAST uses ĕ ŏ ă) | |
| * candra vowel prefix: *e *o *a (SLP1 only; IAST uses ê ô â) | |
| \\ anudatta U+0952 (SLP1 only) | |
| IAST instead writes candrabindu m̐, udatta as combining acute U+0301 and anudatta as | |
| combining grave U+0300. A *literal* occurrence of any reserved character in the source | |
| is escaped by doubling it, so the mapping stays one-to-one. | |
| 5. Latin letters are the one thing that cannot round trip. On the Devanagari side every | |
| Latin letter is a legal SLP1/IAST token, so "cafe" inside a Devanagari record comes back | |
| as चफे. Such characters are reported (class ``latin``) rather than hidden; E1 counts how | |
| many records of the corpus are affected. | |
| 6. Things deliberately NOT transliterated (identity in both directions, never reported): | |
| danda U+0964, double danda U+0965, Devanagari digits U+0966-U+096F, the abbreviation | |
| sign U+0970 and U+0971, ZWJ U+200D and ZWNJ U+200C, whitespace, and ASCII | |
| punctuation/digits. SLP1 has no agreed spelling for any of them and inventing one would | |
| be lossy -- and giving ZWJ/ZWNJ ASCII markers collided with the literal '_' that the DCS | |
| and VedaWeb use as a text marker. | |
| Parsing additionally accepts, but never emits, the spellings real-world romanised | |
| Sanskrit actually uses: ṁ for the anusvara, and the ISO-15919 vowels ē ō r̥ l̥ r̥̄ l̥̄. | |
| 7. Vedic Extensions U+1CD0-U+1CFF are reported as unmapped and passed through, except | |
| the two that are clearly SLP1 letters: jihvamuliya U+1CF5 (Z / ẖ) and upadhmaniya | |
| U+1CF6 (V / ḫ). Devanagari Extended U+A8E0-U+A8FF is likewise reported and preserved. | |
| 8. ऌ and ळ are different letters and get different spellings, which published IAST often | |
| conflates: vocalic ऌ/ॡ are ḷ/ḹ (SLP1 x/X) and retroflex ळ is ḻ, U+1E3B (SLP1 L). ISO-15919 | |
| uses ḷ for ळ; on input that reading loses to IAST's, so feed ISO through | |
| :func:`iso15919_to_iast` first. | |
| Philological note on the Vedic accents: the Rigveda's Devanagari convention marks | |
| *anudatta* below (U+0952) and *svarita* above (U+0951) and leaves udatta unmarked, while | |
| the romanised convention puts an acute on the *udatta* vowel. This module maps the signs, | |
| not the accent theory: U+0951 <-> acute, U+0952 <-> grave, as specified for E1. Converting | |
| a romanised Vedic text therefore produces a Devanagari string that is faithful as a | |
| character mapping but does not match the printed Rigveda's accent placement. | |
| The three-line summary of what the encodings look like: | |
| धर्मक्षेत्रे कुरुक्षेत्रे Devanagari | |
| Darmakzetre kurukzetre SLP1 | |
| dharmakṣetre kurukṣetre IAST | |
| """ | |
| from __future__ import annotations | |
| import unicodedata | |
| from collections import Counter | |
| __all__ = [ | |
| 'NFC', 'deva_to_slp1', 'slp1_to_deva', 'deva_to_iast', 'iast_to_deva', | |
| 'iso15919_to_iast', 'classify_char', 'AKSHARA_RE', 'split_aksharas', 'RESERVED', | |
| ] | |
| VIRAMA = '्' | |
| NUKTA = '़' | |
| ZWNJ = '' | |
| ZWJ = '' | |
| RESERVED = "^'%|~/\\+*" | |
| # -------------------------------------------------------------------------------------- | |
| # Tables. (devanagari, slp1, iast) | |
| # -------------------------------------------------------------------------------------- | |
| CONSONANTS = [ | |
| ('क', 'k', 'k'), ('ख', 'K', 'kh'), ('ग', 'g', 'g'), ('घ', 'G', 'gh'), | |
| ('ङ', 'N', 'ṅ'), | |
| ('च', 'c', 'c'), ('छ', 'C', 'ch'), ('ज', 'j', 'j'), ('झ', 'J', 'jh'), | |
| ('ञ', 'Y', 'ñ'), | |
| ('ट', 'w', 'ṭ'), ('ठ', 'W', 'ṭh'), ('ड', 'q', 'ḍ'), ('ढ', 'Q', 'ḍh'), | |
| ('ण', 'R', 'ṇ'), | |
| ('त', 't', 't'), ('थ', 'T', 'th'), ('द', 'd', 'd'), ('ध', 'D', 'dh'), | |
| ('न', 'n', 'n'), | |
| ('प', 'p', 'p'), ('फ', 'P', 'ph'), ('ब', 'b', 'b'), ('भ', 'B', 'bh'), | |
| ('म', 'm', 'm'), | |
| ('य', 'y', 'y'), ('र', 'r', 'r'), ('ल', 'l', 'l'), ('व', 'v', 'v'), | |
| ('श', 'S', 'ś'), ('ष', 'z', 'ṣ'), ('स', 's', 's'), ('ह', 'h', 'h'), | |
| ('ळ', 'L', 'ḻ'), # ळ -> l with line below | |
| ] | |
| # (independent, dependent sign or None, slp1, iast) | |
| VOWELS = [ | |
| ('अ', None, 'a', 'a'), | |
| ('आ', 'ा', 'A', 'ā'), | |
| ('इ', 'ि', 'i', 'i'), | |
| ('ई', 'ी', 'I', 'ī'), | |
| ('उ', 'ु', 'u', 'u'), | |
| ('ऊ', 'ू', 'U', 'ū'), | |
| ('ऋ', 'ृ', 'f', 'ṛ'), | |
| ('ॠ', 'ॄ', 'F', 'ṝ'), | |
| ('ऌ', 'ॢ', 'x', 'ḷ'), | |
| ('ॡ', 'ॣ', 'X', 'ḹ'), | |
| ('ए', 'े', 'e', 'e'), | |
| ('ऐ', 'ै', 'E', 'ai'), | |
| ('ओ', 'ो', 'o', 'o'), | |
| ('औ', 'ौ', 'O', 'au'), | |
| ('ऎ', 'ॆ', '+e', 'ĕ'), # short e | |
| ('ऒ', 'ॊ', '+o', 'ŏ'), # short o | |
| ('ऄ', None, '+a', 'ă'), # short a | |
| ('ऍ', 'ॅ', '*e', 'ê'), # candra e | |
| ('ऑ', 'ॉ', '*o', 'ô'), # candra o | |
| ('ॲ', None, '*a', 'â'), # candra a | |
| ] | |
| # marks that stand on their own (never fuse with the inherent vowel) | |
| MARKS = [ | |
| ('ं', 'M', 'ṃ'), # anusvara | |
| ('ः', 'H', 'ḥ'), # visarga | |
| ('ँ', '~', 'm̐'), # candrabindu | |
| ('ऽ', "'", "'"), # avagraha | |
| ('ॐ', '%', '%'), # om | |
| ('॑', '/', '́'), # udatta <-> combining acute | |
| ('॒', '\\', '̀'), # anudatta <-> combining grave | |
| ('ᳵ', 'Z', 'ẖ'), # jihvamuliya | |
| ('ᳶ', 'V', 'ḫ'), # upadhmaniya | |
| ] | |
| # identity on both sides, never reported. ZWJ/ZWNJ are here rather than in MARKS on | |
| # purpose: giving them ASCII markers would collide with the literal '_' that DCS and | |
| # VedaWeb use as a text marker, and they survive a round trip untouched as themselves. | |
| PASSTHROUGH = set('।॥॰ॱ') | {ZWJ, ZWNJ} | {chr(c) for c in range(0x0966, 0x0970)} | |
| # precomposed nukta letters -> base + U+093C (NFC already does this; belt and braces) | |
| PRECOMPOSED_NUKTA = { | |
| 'ऩ': 'न' + NUKTA, 'ऱ': 'र' + NUKTA, 'ऴ': 'ळ' + NUKTA, | |
| 'क़': 'क' + NUKTA, 'ख़': 'ख' + NUKTA, 'ग़': 'ग' + NUKTA, | |
| 'ज़': 'ज' + NUKTA, 'ड़': 'ड' + NUKTA, 'ढ़': 'ढ' + NUKTA, | |
| 'फ़': 'फ' + NUKTA, 'य़': 'य' + NUKTA, | |
| } | |
| _SLP1, _IAST = 0, 1 | |
| CONS_FWD = {d: (s, i) for d, s, i in CONSONANTS} | |
| VSIGN_FWD = {sign: (s, i) for _, sign, s, i in VOWELS if sign} | |
| VIND_FWD = {ind: (s, i) for ind, _, s, i in VOWELS} | |
| MARK_FWD = {d: (s, i) for d, s, i in MARKS} | |
| # reverse: value -> devanagari | |
| CONS_REV = [{s: d for d, s, i in CONSONANTS}, {i: d for d, s, i in CONSONANTS}] | |
| VOWEL_REV = [{s: (ind, sign) for ind, sign, s, i in VOWELS}, | |
| {i: (ind, sign) for ind, sign, s, i in VOWELS}] | |
| MARK_REV = [{s: d for d, s, i in MARKS}, {i: d for d, s, i in MARKS}] | |
| def _straddles(prev: str, nxt: str, enc: int) -> bool: | |
| """Would the junction of two emitted pieces re-parse as a different token? | |
| k + ha reads back as ख, ka + i as कै, ka + ī as कै + a stray macron. Checked in NFD, | |
| which is what the parser matches on. SLP1 has only single-character tokens, so this is | |
| always False there.""" | |
| m = MAXTOK[enc] | |
| if m < 2 or not prev or not nxt: | |
| return False | |
| a = unicodedata.normalize('NFD', prev) | |
| b = unicodedata.normalize('NFD', nxt) | |
| trie = TRIE_NFD[enc] | |
| for i in range(1, min(len(a), m - 1) + 1): | |
| for j in range(1, min(len(b), m - i) + 1): | |
| if a[-i:] + b[:j] in trie: | |
| return True | |
| return False | |
| # functional meaning of each reserved character, per encoding | |
| _FUNC_COMMON = {'^': ('nukta', None), '|': ('sep', None)} | |
| FUNC = [ | |
| dict(_FUNC_COMMON, **{c: ('mark', d) for d, c, _ in MARKS if c in RESERVED}, | |
| **{'+': ('vmod', None), '*': ('vmod', None)}), | |
| dict(_FUNC_COMMON, **{c: ('mark', d) for d, _, c in MARKS if c in RESERVED}), | |
| ] | |
| def _build_trie(enc): | |
| """token -> (kind, payload); longest match wins. Reserved chars are handled separately.""" | |
| t = {} | |
| for d, s, i in CONSONANTS: | |
| t[(s, i)[enc]] = ('cons', d) | |
| for ind, sign, s, i in VOWELS: | |
| v = (s, i)[enc] | |
| if v[0] not in RESERVED: | |
| t[v] = ('vowel', (ind, sign)) | |
| for d, s, i in MARKS: | |
| v = (s, i)[enc] | |
| if v[0] not in RESERVED: | |
| t[v] = ('mark', d) | |
| for c in PASSTHROUGH: | |
| t[c] = ('pass', c) | |
| return t | |
| # Input-only spellings. Real-world "IAST" is rarely pure: GRETIL and the DCS write the | |
| # anusvara as ṁ, VedaWeb writes ISO-15919 vowels. These are accepted when parsing and never | |
| # emitted, so the canonical round trip is unaffected while iast -> deva stops mangling them. | |
| # ISO's ḷ (= ळ) is NOT aliased: in IAST ḷ is vocalic ऌ and that reading wins. | |
| IAST_INPUT_ALIASES = { | |
| 'ṁ': ('mark', 'ं'), | |
| 'ē': ('vowel', ('ए', 'े')), 'ō': ('vowel', ('ओ', 'ो')), | |
| 'r̥': ('vowel', ('ऋ', 'ृ')), 'l̥': ('vowel', ('ऌ', 'ॢ')), | |
| 'r̥̄': ('vowel', ('ॠ', 'ॄ')), 'l̥̄': ('vowel', ('ॡ', 'ॣ')), | |
| 'ṝ': ('vowel', ('ॠ', 'ॄ')), | |
| } | |
| TRIE = [_build_trie(_SLP1), _build_trie(_IAST)] | |
| # parsing runs on NFD so that composed letters (ī = i + macron) line up with combining | |
| # accents; build the NFD tries once, not per call | |
| TRIE_NFD = [{unicodedata.normalize('NFD', k): v for k, v in t.items()} for t in TRIE] | |
| MAXTOK = [max(len(k) for k in t) for t in TRIE_NFD] | |
| # what the parser matches on: canonical tokens plus the input-only aliases | |
| TRIE_PARSE = [dict(TRIE_NFD[0]), | |
| dict(TRIE_NFD[1], **{unicodedata.normalize('NFD', k): v | |
| for k, v in IAST_INPUT_ALIASES.items()})] | |
| MAXTOK_PARSE = [max(len(k) for k in t) for t in TRIE_PARSE] | |
| # characters that a passthrough would collide with when read back | |
| RISKY = [set(k[0] for k in TRIE[0]) - PASSTHROUGH, set(k[0] for k in TRIE[1]) - PASSTHROUGH] | |
| def NFC(text: str) -> str: | |
| return unicodedata.normalize('NFC', text) | |
| def classify_char(ch: str) -> str: | |
| """Bucket an unmapped character for reporting.""" | |
| cp = ord(ch) | |
| if 0x0900 <= cp <= 0x097F: | |
| return 'devanagari_unmapped' | |
| if 0x1CD0 <= cp <= 0x1CFF: | |
| return 'vedic_extensions' | |
| if 0xA8E0 <= cp <= 0xA8FF: | |
| return 'devanagari_extended' | |
| if cp < 0x0250 and ch.isalpha(): | |
| return 'latin' | |
| if unicodedata.combining(ch): | |
| return 'combining' | |
| if ch.isalpha(): | |
| return 'other_script' | |
| return 'other' | |
| def _safe_pass(ch: str, enc: int) -> bool: | |
| """True if passing this character through is reversible and unremarkable.""" | |
| if ch in PASSTHROUGH: | |
| return True | |
| if ch in RESERVED: | |
| return True # escaped by doubling, so reversible | |
| if ch in RISKY[enc]: | |
| return False | |
| cat = unicodedata.category(ch) | |
| return cat[0] in 'ZPSN' or ch in '\n\t\r' or cat == 'Cc' | |
| # -------------------------------------------------------------------------------------- | |
| # Devanagari -> romanised | |
| # -------------------------------------------------------------------------------------- | |
| def _to_roman(text: str, enc: int): | |
| text = NFC(text) | |
| unmapped: Counter = Counter() | |
| out: list[str] = [] | |
| inh = None # index in out of the inherent 'a' still open for replacement | |
| bare = False # last piece was a consonant closed by virama | |
| def put(piece: str): | |
| """Append a piece, inserting the '|' separator wherever the junction would re-parse. | |
| Two cases: an IAST digraph straddling the junction (k + ha -> k|ha), and a reserved | |
| marker landing next to the same marker, where the doubling escape would otherwise | |
| read the pair as a literal (avagraha + avagraha -> '|', not '').""" | |
| if out and out[-1] and piece: | |
| a, b = out[-1][-1], piece[0] | |
| if (a == b and b in RESERVED and b != '|') or _straddles(out[-1], piece, enc): | |
| out.append('|') | |
| out.append(piece) | |
| def raw(ch: str): | |
| nonlocal inh, bare | |
| inh, bare = None, False | |
| put(ch + ch if ch in RESERVED else ch) | |
| i, n = 0, len(text) | |
| while i < n: | |
| ch = text[i] | |
| i += 1 | |
| if ch in PRECOMPOSED_NUKTA: # only reachable if caller skipped NFC | |
| text = text[:i - 1] + PRECOMPOSED_NUKTA[ch] + text[i:] | |
| n = len(text) | |
| i -= 1 | |
| continue | |
| if ch in CONS_FWD: | |
| put(CONS_FWD[ch][enc]) | |
| out.append('a') | |
| inh = len(out) - 1 | |
| bare = False | |
| elif ch == NUKTA: | |
| # a second nukta on the same consonant would emit '^^', which reads back as an | |
| # escaped literal '^'; refuse rather than lose it silently | |
| if inh is not None and inh == len(out) - 1 and out[inh - 1] != '^': | |
| out.insert(inh, '^') | |
| inh += 1 | |
| else: | |
| unmapped[ch] += 1 | |
| raw(ch) | |
| elif ch in VSIGN_FWD: | |
| if inh is not None and inh == len(out) - 1: | |
| v = VSIGN_FWD[ch][enc] | |
| out[inh] = ('|' + v) if (len(out) > 1 and _straddles(out[-2], v, enc)) else v | |
| inh = None | |
| else: | |
| unmapped[ch] += 1 | |
| raw(ch) | |
| elif ch == VIRAMA: | |
| if inh is not None and inh == len(out) - 1: | |
| out.pop() | |
| inh = None | |
| bare = True | |
| else: | |
| unmapped[ch] += 1 | |
| raw(ch) | |
| elif ch in VIND_FWD: | |
| # an independent vowel right after a bare consonant would be read back as that | |
| # consonant's dependent sign (र् + ऋ -> "rf" -> रृ), so separate them | |
| if bare: | |
| out.append('|') | |
| inh, bare = None, False | |
| put(VIND_FWD[ch][enc]) | |
| elif ch in MARK_FWD: | |
| inh, bare = None, False | |
| put(MARK_FWD[ch][enc]) | |
| elif ch in PASSTHROUGH: | |
| inh, bare = None, False | |
| out.append(ch) | |
| else: | |
| if not _safe_pass(ch, enc): | |
| unmapped[ch] += 1 | |
| raw(ch) | |
| return NFC(''.join(out)), unmapped | |
| def deva_to_slp1(text: str): | |
| """Devanagari -> SLP1. Returns (slp1, Counter of unmapped characters).""" | |
| return _to_roman(text, _SLP1) | |
| def deva_to_iast(text: str): | |
| """Devanagari -> IAST. Returns (iast, Counter of unmapped characters).""" | |
| return _to_roman(text, _IAST) | |
| # -------------------------------------------------------------------------------------- | |
| # romanised -> Devanagari | |
| # -------------------------------------------------------------------------------------- | |
| def _to_deva(text: str, enc: int): | |
| text = unicodedata.normalize('NFD', NFC(text)) | |
| unmapped: Counter = Counter() | |
| trie, maxtok, func = TRIE_PARSE[enc], MAXTOK_PARSE[enc], FUNC[enc] | |
| out: list[str] = [] | |
| pend = None # consonant awaiting its vowel | |
| nukta = False | |
| def close(): | |
| nonlocal pend, nukta | |
| if pend is not None: | |
| out.append(pend) | |
| if nukta: | |
| out.append(NUKTA) | |
| out.append(VIRAMA) | |
| pend, nukta = None, False | |
| def vowel(ind, sign): | |
| nonlocal pend, nukta | |
| if pend is not None: | |
| out.append(pend) | |
| if nukta: | |
| out.append(NUKTA) | |
| if sign is not None: | |
| out.append(sign) | |
| elif ind != 'अ': | |
| unmapped[ind] += 1 # no dependent form (ऄ, ॲ) after a consonant | |
| out.append(VIRAMA) | |
| out.append(ind) | |
| pend, nukta = None, False | |
| else: | |
| out.append(ind) | |
| i, n = 0, len(text) | |
| while i < n: | |
| ch = text[i] | |
| if ch in RESERVED: | |
| if i + 1 < n and text[i + 1] == ch: # escaped literal | |
| close() | |
| out.append(ch) | |
| i += 2 | |
| continue | |
| kind, payload = func.get(ch, (None, None)) | |
| if kind == 'nukta' and pend is not None and not nukta: | |
| nukta = True | |
| i += 1 | |
| continue | |
| if kind == 'sep': | |
| # Our emitter only ever puts the separator BETWEEN two letters, so a '|' that | |
| # touches whitespace is not ours: it is the ASCII dandaa that GRETIL-style | |
| # romanised sources use. Keep it (and report it) instead of swallowing it. | |
| if (text[i - 1] if i else ' ').isspace() or \ | |
| (text[i + 1] if i + 1 < n else ' ').isspace(): | |
| unmapped[ch] += 1 | |
| close() | |
| out.append(ch) | |
| else: | |
| close() | |
| i += 1 | |
| continue | |
| if kind == 'vmod': | |
| tok = text[i:i + 2] | |
| hit = VOWEL_REV[enc].get(tok) | |
| if hit: | |
| vowel(*hit) | |
| i += 2 | |
| continue | |
| if kind == 'mark': | |
| close() | |
| out.append(payload) | |
| i += 1 | |
| continue | |
| unmapped[ch] += 1 | |
| close() | |
| out.append(ch) | |
| i += 1 | |
| continue | |
| for L in range(min(maxtok, n - i), 0, -1): | |
| hit = trie.get(text[i:i + L]) | |
| if hit: | |
| kind, payload = hit | |
| if kind == 'cons': | |
| close() | |
| pend = payload | |
| elif kind == 'vowel': | |
| vowel(*payload) | |
| else: | |
| close() | |
| out.append(payload) | |
| i += L | |
| break | |
| else: | |
| if not _safe_pass(ch, enc): | |
| unmapped[ch] += 1 | |
| close() | |
| out.append(ch) | |
| i += 1 | |
| close() | |
| return NFC(''.join(out)), unmapped | |
| def slp1_to_deva(text: str): | |
| """SLP1 -> Devanagari. Returns (devanagari, Counter of unmapped characters).""" | |
| return _to_deva(text, _SLP1) | |
| def iast_to_deva(text: str): | |
| """IAST -> Devanagari. Returns (devanagari, Counter of unmapped characters).""" | |
| return _to_deva(text, _IAST) | |
| # -------------------------------------------------------------------------------------- | |
| # ISO-15919 (as VedaWeb writes it) -> our canonical IAST | |
| # -------------------------------------------------------------------------------------- | |
| # Applied on NFD so that the pitch accents (combining acute/grave) survive on the vowel. | |
| _ISO_PAIRS = [ | |
| ('r̥̄', 'ṝ'), ('l̥̄', 'ḹ'), # r̥̄ l̥̄ -> ṝ ḹ | |
| ('r̥', 'ṛ'), ('l̥', 'ḷ'), # r̥ l̥ -> ṛ ḷ | |
| ('ḷ', 'ḻ'), # ISO ḷ is ळ, not vocalic l | |
| ('ṁ', 'ṃ'), # ṁ -> ṃ | |
| ('ē', 'e'), ('ō', 'o'), # ē ō -> e o | |
| ('ṉ', 'n'), ('ṟ', 'r'), # ṉ ṟ (nukta-ish ISO) -> n r | |
| ] | |
| _ISO_ESCAPE = set(RESERVED) - {"'"} # keep ' meaning avagraha | |
| def iso15919_to_iast(text: str): | |
| """VedaWeb-style ISO-15919 (with pitch accents) -> our strict IAST. | |
| Reserved characters other than the avagraha apostrophe are escaped by doubling, so the | |
| result is a legal input to :func:`iast_to_deva`. | |
| """ | |
| src = unicodedata.normalize('NFD', NFC(text)) | |
| # longest source first so r̥̄ beats r̥ | |
| pairs = sorted(_ISO_PAIRS, key=lambda p: -len(unicodedata.normalize('NFD', p[0]))) | |
| pairs = [(unicodedata.normalize('NFD', a), b) for a, b in pairs] | |
| out, i, n = [], 0, len(src) | |
| unmapped: Counter = Counter() | |
| while i < n: | |
| for a, b in pairs: | |
| if src.startswith(a, i): | |
| out.append(b) | |
| i += len(a) | |
| break | |
| else: | |
| ch = src[i] | |
| out.append(ch + ch if ch in _ISO_ESCAPE else ch) | |
| i += 1 | |
| return NFC(''.join(out)), unmapped | |
| # -------------------------------------------------------------------------------------- | |
| # Akshara segmentation (explicit Devanagari regex; NOT \X, which breaks after virama) | |
| # -------------------------------------------------------------------------------------- | |
| _C = '[क-हक़-य़ॹ-ॿऩऱऴ]' | |
| _V = '[ऄ-औॠॡॲ-ॷ]' | |
| _S = '[ऺऻा-ौॎॏॕ-ॗॢॣ]' | |
| _A = '[ऀ-ः॑-॔᳐-᳨ᳵᳶ꣠-꣱]' | |
| AKSHARA_RE = ( | |
| f'(?:{_C}़?(?:्[]?{_C}़?)*(?:{_S}|्)?|{_V}़?{_S}?){_A}*' | |
| ) | |
| def split_aksharas(text: str): | |
| """Devanagari orthographic syllables; anything else comes back one code point per item.""" | |
| import regex | |
| pat = regex.compile(f'{AKSHARA_RE}|.', regex.DOTALL) | |
| return [m.group(0) for m in pat.finditer(text)] | |
| if __name__ == '__main__': | |
| import sys | |
| for line in (sys.argv[1:] or ['धर्मक्षेत्रे कुरुक्षेत्रे समवेता युयुत्सवः ॥ १ ॥']): | |
| s, us = deva_to_slp1(line) | |
| a, ua = deva_to_iast(line) | |
| print('deva :', line) | |
| print('slp1 :', s, ' unmapped', dict(us)) | |
| print('iast :', a, ' unmapped', dict(ua)) | |
| print('back :', slp1_to_deva(s)[0] == NFC(line), iast_to_deva(a)[0] == NFC(line)) | |