import re
import unicodedata
thai_range = r"[ก-๛̄]"
systems = {
"paiboon": 0,
"royin": 1,
"ipa": 2
}
initial = {
"ก": {"seq": ["g", "k", "k"], "class": "mid"},
"จ": {"seq": ["j", "ch", "t͡ɕ"], "class": "mid"},
"ด": {"seq": ["d", "d", "d"], "class": "mid"},
"ฎ": {"seq": ["d", "d", "d"], "class": "mid"},
"ฏ": {"seq": ["dt", "t", "t"], "class": "mid"},
"ต": {"seq": ["dt", "t", "t"], "class": "mid"},
"บ": {"seq": ["b", "b", "b"], "class": "mid"},
"ป": {"seq": ["bp", "p", "p"], "class": "mid"},
"อ": {"seq": ["", "@", "ʔ"], "class": "mid"},
"ง": {"seq": ["ng", "$ng", "ŋ"], "class": "low"},
"ณ": {"seq": ["n", "n", "n"], "class": "low"},
"น": {"seq": ["n", "n", "n"], "class": "low"},
"ม": {"seq": ["m", "m", "m"], "class": "low"},
"ญ": {"seq": ["y", "y", "j"], "class": "low"},
"ย": {"seq": ["y", "y", "j"], "class": "low"},
"ร": {"seq": ["r", "r", "r"], "class": "low"},
"ล": {"seq": ["l", "l", "l"], "class": "low"},
"ฬ": {"seq": ["l", "l", "l"], "class": "low"},
"ว": {"seq": ["w", "w", "w"], "class": "low"},
"ค": {"seq": ["k", "kh", "kʰ"], "class": "low"},
"ฅ": {"seq": ["k", "kh", "kʰ"], "class": "low"},
"ฆ": {"seq": ["k", "kh", "kʰ"], "class": "low"},
"ข": {"seq": ["k", "kh", "kʰ"], "class": "high"},
"ฃ": {"seq": ["k", "kh", "kʰ"], "class": "high"},
"ช": {"seq": ["ch", "ch", "t͡ɕʰ"], "class": "low"},
"ฌ": {"seq": ["ch", "ch", "t͡ɕʰ"], "class": "low"},
"ฉ": {"seq": ["ch", "ch", "t͡ɕʰ"], "class": "high"},
"ฑ": {"seq": ["t", "th", "tʰ"], "class": "low"},
"ฒ": {"seq": ["t", "th", "tʰ"], "class": "low"},
"ท": {"seq": ["t", "th", "tʰ"], "class": "low"},
"ธ": {"seq": ["t", "th", "tʰ"], "class": "low"},
"ฐ": {"seq": ["t", "th", "tʰ"], "class": "high"},
"ถ": {"seq": ["t", "th", "tʰ"], "class": "high"},
"พ": {"seq": ["p", "ph", "pʰ"], "class": "low"},
"ภ": {"seq": ["p", "ph", "pʰ"], "class": "low"},
"ผ": {"seq": ["p", "ph", "pʰ"], "class": "high"},
"ฟ": {"seq": ["f", "f", "f"], "class": "low"},
"ฝ": {"seq": ["f", "f", "f"], "class": "high"},
"ซ": {"seq": ["s", "s", "s"], "class": "low"},
"ศ": {"seq": ["s", "s", "s"], "class": "high"},
"ษ": {"seq": ["s", "s", "s"], "class": "high"},
"ส": {"seq": ["s", "s", "s"], "class": "high"},
"ฮ": {"seq": ["h", "h", "h"], "class": "low"},
"ห": {"seq": ["h", "h", "h"], "class": "high"},
"หง": {"seq": ["ng", "$ng", "ŋ"], "class": "high"},
"หน": {"seq": ["n", "n", "n"], "class": "high"},
"หม": {"seq": ["m", "m", "m"], "class": "high"},
"หญ": {"seq": ["y", "y", "j"], "class": "high"},
"หย": {"seq": ["y", "y", "j"], "class": "high"},
"หร": {"seq": ["r", "r", "r"], "class": "high"},
"หล": {"seq": ["l", "l", "l"], "class": "high"},
"หว": {"seq": ["w", "w", "w"], "class": "high"},
"…": {"seq": ["…", "…", "…"], "class": ""},
"": {"seq": ["", "", ""], "class": ""},
}
vowel = {
"open": {
"ะ": ["a", "a", "a"], "": ["a", "a", "a"], "ิ": ["i", "i", "i"],
"ึ": ["ʉ", "ue", "ɯ"], "ุ": ["u", "u", "u"], "เะ": ["e", "e", "eʔ"],
"แะ": ["ɛ", "ae", "ɛʔ"], "โะ": ["o", "o", "oʔ"], "เาะ": ["ɔ", "o", "ɔʔ"],
"็": ["ɔ", "o", "ɔ"], "เิ": ["ə", "oe", "ɤ"], "เอะ": ["ə", "oe", "ɤʔ"],
"า": ["aa", "a", "aː"], "ี": ["ii", "i", "iː"], "ู": ["uu", "u", "uː"],
"ือ": ["ʉʉ", "ue", "ɯː"], "เ": ["ee", "e", "eː"], "แ": ["ɛɛ", "ae", "ɛː"],
"โ": ["oo", "o", "oː"], "อ": ["ɔɔ", "o", "ɔː"], "ร": ["ɔɔn", "on", "ɔːn"],
"เอ": ["əə", "oe", "ɤː"], "เียะ": ["ia", "ia", "ia̯ʔ"], "เือะ": ["ʉa", "uea", "ɯa̯ʔ"],
"ัวะ": ["ua", "ua", "ua̯ʔ"], "เีย": ["iia", "ia", "ia̯"], "เือ": ["ʉʉa", "uea", "ɯa̯"],
"ัว": ["uua", "ua", "ua̯"], "ิว": ["iu", "io", "iw"], "ีว": ["iiu", "io", "iːw"],
"เ็ว": ["eo", "eo", "ew"], "แ็ว": ["ɛo", "aeo", "ɛw"], "เา": ["ao", "ao", "aw"],
"เว": ["eeo", "eo", "eːw"], "แว": ["ɛɛo", "aeo", "ɛːw"], "าว": ["aao", "ao", "aːw"],
"เอว": ["əəo", "oeu", "ɤːw"], "โว": ["oow", "ou", "oːw"], "เียว": ["iao", "iao", "ia̯w"],
"ัย": ["ai", "ai", "aj"], "ใ": ["ai", "ai", "aj"], "ไ": ["ai", "ai", "aj"],
"ไย": ["ai", "ai", "aj"], "ึย": ["ʉi", "uei", "ɯj"], "็อย": ["ɔi", "oi", "ɔj"],
"เิ็ย": ["əi", "oei", "ɤj"], "ุย": ["ui", "ui", "uj"], "าย": ["aai", "ai", "aːj"],
"อย": ["ɔɔi", "oi", "ɔːj"], "โย": ["ooi", "oi", "oːj"], "เย": ["əəi", "oei", "ɤːj"],
"ูย": ["uui", "ui", "uːj"], "วย": ["uai", "uai", "ua̯j"], "เือย": ["ʉai", "ueai", "ɯa̯j"],
"ำ": ["am", "am", "am"],
},
"closed": {
"ั": ["a", "a", "a"], "รร": ["a", "a", "a"], "ิ": ["i", "i", "i"],
"ึ": ["ʉ", "ue", "ɯ"], "ุ": ["u", "u", "u"], "เ": ["ee", "e", "eː"],
"เ็": ["e", "e", "e"], "แ็": ["ɛ", "ae", "ɛ"], "แ": ["ɛɛ", "ae", "ɛː"],
"": ["o", "o", "o"], "็อ": ["ɔ", "o", "ɔ"], "เิ็": ["ə", "oe", "ɤ"],
"า": ["aa", "a", "aː"], "ี": ["ii", "i", "iː"], "ื": ["ʉʉ", "ue", "ɯː"],
"ู": ["uu", "u", "uː"], "โ": ["oo", "o", "oː"], "อ": ["ɔɔ", "o", "ɔː"],
"เิ": ["əə", "oe", "ɤː"], "เอ": ["əə", "oe", "ɤː"], "เีย": ["iia", "ia", "ia̯"],
"เือ": ["ʉʉa", "uea", "ɯa̯"], "ว": ["uua", "ua", "ua̯"], "ไ": ["ai", "ai", "aj"],
"เา": ["ao", "ao", "aw"], "็อย": ["ɔi", "oi", "ɔj"],
}
}
unromLong = {"เีย": True, "เือ": True, "ัว": True, "ว": True, "เือย": True, "วาย": True, "เอว": True, "เียว": True}
liveExc = {"ัย": True, "ใ": True, "ไ": True, "ไย": True, "ุย": True, "วย": True, "็อย": True, "เิ็ย": True, "เา": True, "ิว": True, "เ็ว": True, "แ็ว": True, "ำ": True}
coda = {
"ก": ["k", "k", "k̚"], "ข": ["k", "k", "k̚"], "ฃ": ["k", "k", "k̚"], "ค": ["k", "k", "k̚"],
"ฅ": ["k", "k", "k̚"], "ฆ": ["k", "k", "k̚"], "จ": ["t", "t", "t̚"], "ฉ": ["t", "t", "t̚"],
"ช": ["ch", "ch", "t͡ɕʰ"], "ซ": ["s", "s", "s"], "ฌ": ["t", "t", "t̚"], "ฎ": ["t", "t", "t̚"],
"ฏ": ["t", "t", "t̚"], "ฐ": ["t", "t", "t̚"], "ฑ": ["t", "t", "t̚"], "ฒ": ["t", "t", "t̚"],
"ด": ["t", "t", "t̚"], "ต": ["t", "t", "t̚"], "ถ": ["t", "t", "t̚"], "ท": ["t", "t", "t̚"],
"ธ": ["t", "t", "t̚"], "ศ": ["t", "t", "t̚"], "ษ": ["t", "t", "t̚"], "ส": ["s", "s", "s"],
"บ": ["p", "p", "p̚"], "ป": ["p", "p", "p̚"], "ผ": ["p", "p", "p̚"], "ฝ": ["p", "p", "p̚"],
"พ": ["p", "p", "p̚"], "ฟ": ["f", "f", "f"], "ภ": ["p", "p", "p̚"], "ง": ["ng", "ng$", "ŋ"],
"ญ": ["n", "n", "n"], "ณ": ["n", "n", "n"], "น": ["n", "n", "n"], "ร": ["n", "n", "n"],
"ล": ["l", "l", "l"], "ฬ": ["n", "n", "n"], "ม": ["m", "m", "m"], "ฯ": ["ʔ", "ʔ", "ʔ"]
}
tFromMark = {
"่": {"high": "low", "mid": "low", "low": "falling"},
"้": {"high": "falling", "mid": "falling", "low": "high"},
"๊": {"high": "high", "mid": "high", "low": "high"},
"๋": {"high": "rising", "mid": "rising", "low": "rising"},
"̄": {"high": "mid", "mid": "mid", "low": "mid"}
}
tNoMark = {
"dead-short": {"high": "low", "mid": "low", "low": "high"},
"dead-long": {"high": "low", "mid": "low", "low": "falling"},
"live": {"high": "rising", "mid": "mid", "low": "mid"}
}
tRomMarks = {"high": "́", "mid": "", "low": "̀", "rising": "̌", "falling": "̂"}
tLevels = {"high": "˦˥", "mid": "˧", "low": "˨˩", "rising": "˩˩˦", "falling": "˥˩"}
symbols = {"๐": "0", "๑": "1", "๒": "2", "๓": "3", "๔": "4", "๕": "5", "๖": "6", "๗": "7", "๘": "8", "๙": "9"}
permitted_cluster = {"ks": 1, "ms": 1, "ns": 1, "ps": 1, "ts": 1}
mgvc_pattern = re.compile(r"^([รลว]?)([ำู]?[าอรยว]?[วยร]?ะ?)([คฅฆกขฃพฟภบปชฌฑฒทธจฎฏดตฐถศษสมญณนรลฬงฯ]?)$")
full_pattern = re.compile(r"^([เแโใไ]?)(หฺ[ก-รลว-ฮ])(ฺ?[รลว]?)([]?็?[]?[าอรยวำ]?[วยร]?ะ?)([คฅฆกขฃพฟภบปชฌฑฒทธจฎฏดตฐถศษสมญณนรลฬงฯ]?[คฅฆกขฃพฟภบปชฌฑฒทธจฎฏดตฐถศษสมญณนรลฬงฯ]?)$")
partial_pattern = re.compile(r"^([เแโใไ]?)([ก-รลว-ฮ])(ฺ?[รลว]?)([]?็?[]?[าอรยวำ]?[วยร]?ะ?)([คฅฆกขฃพฟภบปชฌฑฒทธจฎฏดตฐถศษสมญณนรลฬงฯ]?[คฅฆกขฃพฟภบปชฌฑฒทธจฎฏดตฐถศษสมญณนรลฬงฯ]?)$")
def c2_decomp(c2_char, seq_idx):
converted_c2 = []
for character in list(c2_char):
val = coda.get(character)
converted_c2.append(val[seq_idx] if val else "")
cluster = "".join(converted_c2)
return cluster
def _translit_word(text, mode):
seq_idx = systems.get(mode)
if seq_idx is None:
return text
def process_word(match_word):
word = match_word.group(0)
if re.search(r"[].?[]", word):
return word
def syllable(match):
nonlocal word
v1, c1, g, v2, c2 = match.groups()
tMark_match = re.search(r"[]", v2)
tMark = tMark_match.group(0) if tMark_match else False
v2 = re.sub(r"[]", "", v2)
if re.match(r"^ห.$", c1):
mgvc_match = mgvc_pattern.match(c1[1] + g + v2 + c2)
if mgvc_match:
g_new, v2_new, c2_new = mgvc_match.groups()
c1, g, v2, c2 = "ห", g_new, v2_new, c2_new
if g != "" and v2 != "ย":
c1, g = c1 + g, ""
if g == "ล" and (v2 + c2) == "":
c2 = g
g = ""
openness = "closed" if c2 != "" else "open"
if (v1 + g + v2) in vowel[openness]:
orig_v = v1 + g + v2
v = vowel[openness][orig_v][seq_idx]
g = ""
else:
orig_v = v1 + v2
v_lookup = vowel[openness].get(v1 + v2)
v = v_lookup[seq_idx] if v_lookup else (v1 + v2)
g_clean = g.replace("ฺ", "")
g_lookup = initial.get(g_clean) or initial[""]
g = g_lookup["seq"][seq_idx]
c1_clean = c1.replace("ฺ", "")
if c1_clean in initial:
ini = initial[c1_clean]["seq"][seq_idx]
cls = initial[c1_clean]["class"]
else:
return match.group(0) # fallback
length = "long" if re.search(r"([aiʉueɛoɔə])\1", v) or "ː" in v or unromLong.get(orig_v) else "short"
life = "live" if re.search(r"[มญณนรลฬง]", c2) or (orig_v.endswith("ย") and v.endswith("i")) or (c2 + length == "long") or liveExc.get(orig_v) else "dead"
if c2 in coda:
c2 = coda[c2][seq_idx]
else:
c2 = c2_decomp(c2, seq_idx)
tone_dict = tFromMark.get(tMark) if tMark else tNoMark.get(f"{life}-{length}", tNoMark.get(life))
tone = tone_dict.get(cls) if tone_dict else None
if mode == "paiboon":
v = re.sub(r"^([^aiʉueɛoɔə]*)([aiʉueɛoɔə])", f"\\g<1>\\g<2>{tRomMarks.get(tone, '')}", v)
elif mode == "ipa":
c2 = c2 + tLevels.get(tone, "")
return ini + g + v + c2
word = full_pattern.sub(syllable, word)
word = partial_pattern.sub(syllable, word)
return word
words_iter = list(re.finditer(f"{thai_range}+", text))
for match_word in words_iter:
processed = process_word(match_word)
if processed:
text = text.replace(match_word.group(0), processed, 1)
text = re.sub(r"[๐-๙]", lambda m: symbols.get(m.group(0), m.group(0)), text)
if mode == "royin":
text = re.sub(r"^@", "", text)
text = re.sub(r"([\s\W])@", r"\1", text)
text = text.replace("@", "-")
text = re.sub(r"^\$ng", "ng", text)
text = re.sub(r"([\s\W])\$ng", r"\1ng", text)
text = re.sub(r"([aeiou])\$ng", r"\1-ng", text)
text = text.replace("$ng", "ng")
text = re.sub(r"ng\$([^\w\s])", r"ng\1", text)
text = re.sub(r"ng\$", "ng", text)
if mode == "ipa":
text, count_syl = re.subn(r"[ \-–]", ".", text)
text = re.sub(r"([aiɯu])([˥-˩]+)$", r"\1ʔ\2", text)
return unicodedata.normalize('NFC', text)
def get_word_dict(word: str) -> dict:
"""
Takes a Thai string and returns a dictionary with different
transliteration standards.
"""
return {
"word": word,
"paiboon": _translit_word(word, mode="paiboon"),
"royin": _translit_word(word, mode="royin"),
"ipa": _translit_word(word, mode="ipa")
}
# --- Example Usage ---
if __name__ == "__main__":
test_word = "ปฺระ-ชา"
result = get_word_dict(test_word)
print(result)
Add Module:th-pron from wiktionary to pythainlp.transliterate
Module:th-pron: https://en.wiktionary.org/wiki/Module:th-pron