diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af92d7d0..9844afc99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ See PR for prompt and details. - Reorganize noauto test suite by dependency groups (torch, tensorflow, onnx, cython, network) #1290 - Add BLEU, ROUGE, WER, and CER metrics to pythainlp.benchmarks #1295 +- Add `check_khuap_klam` to `pythainlp.util` for checking + Thai consonant clusters #1308 - Add Attaparse engine to dependency parser (`dependency_parsing`, engine="attaparse") #1303 - Improved documentation; code cleanup; more tests diff --git a/docs/api/util.rst b/docs/api/util.rst index a588db38c..79c0f0b57 100644 --- a/docs/api/util.rst +++ b/docs/api/util.rst @@ -27,6 +27,11 @@ Modules The `bahttext` function specializes in converting numerical values into Thai Baht text, an essential feature for rendering financial data or monetary amounts in a user-friendly Thai format. +.. autofunction:: check_khuap_klam + :noindex: + + The `check_khuap_klam` function checks whether a Thai word is a consonant cluster (Kham Khuap Klam, คำควบกล้ำ). It returns ``True`` for a true consonant cluster (คำควบกล้ำแท้), ``False`` for a false consonant cluster (คำควบกล้ำไม่แท้), or ``None`` if the word is not a consonant cluster. + .. autofunction:: censor_profanity :noindex: diff --git a/pythainlp/util/__init__.py b/pythainlp/util/__init__.py index 880957170..3639b6a8a 100644 --- a/pythainlp/util/__init__.py +++ b/pythainlp/util/__init__.py @@ -9,6 +9,7 @@ "arabic_digit_to_thai_digit", "bahttext", "censor_profanity", + "check_khuap_klam", "collate", "contains_profanity", "convert_years", @@ -100,6 +101,7 @@ thai_to_eng, ) from pythainlp.util.keywords import find_keyword, rank +from pythainlp.util.khuap_klam import check_khuap_klam from pythainlp.util.lcs import longest_common_subsequence from pythainlp.util.normalize import ( expand_maiyamok, diff --git a/pythainlp/util/khuap_klam.py b/pythainlp/util/khuap_klam.py new file mode 100644 index 000000000..81af7e5d4 --- /dev/null +++ b/pythainlp/util/khuap_klam.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 +"""Thai consonant cluster (Kham Khuap Klam) checker.""" + +from __future__ import annotations + +import re +from typing import Optional + +# Regex for true consonant clusters (คำควบกล้ำแท้): +# initial consonants ก ข ค ต ป ผ พ ฟ บ followed by ร ล ว +_TRUE_CLUSTER_RE = re.compile(r"^[กขคตปผพฟบ][รลว]") + +# Regex for false consonant clusters (คำควบกล้ำไม่แท้): +# written forms that look like clusters but are not pronounced as such +_FALSE_CLUSTER_RE = re.compile(r"^(ทร|จร|ศร|สร|ซร)") + +# Leading vowels that appear before the initial consonant in written Thai +_LEAD_VOWEL_RE = re.compile(r"^[เแโใไ]+") + + +def _strip_lead_vowels(text: str) -> str: + """Remove leading vowels (เ แ โ ใ ไ) from the start of *text*.""" + return _LEAD_VOWEL_RE.sub("", text) + + +def check_khuap_klam(word: str) -> Optional[bool]: + """Check whether a Thai word is a consonant cluster (Kham Khuap Klam). + + :param str word: Thai word to check. + :return: ``True`` if the word is a *true* consonant cluster + (คำควบกล้ำแท้), ``False`` if it is a *false* consonant cluster + (คำควบกล้ำไม่แท้), or ``None`` if it is not a consonant cluster. + :rtype: Optional[bool] + + :Example: + :: + + from pythainlp.util import check_khuap_klam + + # True consonant clusters (คำควบกล้ำแท้) + print(check_khuap_klam("กราบ")) # True + print(check_khuap_klam("ปลา")) # True + print(check_khuap_klam("เพราะ")) # True + print(check_khuap_klam("ตรง")) # True + + # False consonant clusters (คำควบกล้ำไม่แท้) + print(check_khuap_klam("จริง")) # False + print(check_khuap_klam("ทราย")) # False + print(check_khuap_klam("เศร้า")) # False + + # Not a consonant cluster + print(check_khuap_klam("แม่")) # None + print(check_khuap_klam("ตา")) # None + """ + if not word: + return None + + from ..transliterate import pronunciate + + # Convert to pronunciation; remove sub-consonant marker (พินทุ ฺ) + reading = pronunciate(word, engine="w2p").replace("\u0e3a", "") + + # Use only the first syllable of the reading + first_syll_reading = reading.split("-")[0] + + written_core = _strip_lead_vowels(word) + reading_core = _strip_lead_vowels(first_syll_reading) + + is_true_sound = bool(_TRUE_CLUSTER_RE.match(reading_core)) + is_false_form = bool(_FALSE_CLUSTER_RE.match(written_core)) + + if is_true_sound: + return True + if is_false_form: + return False + return None diff --git a/tests/compact/testc_util.py b/tests/compact/testc_util.py index eb43fb27f..31aacfa40 100644 --- a/tests/compact/testc_util.py +++ b/tests/compact/testc_util.py @@ -6,7 +6,12 @@ import unittest -from pythainlp.util import rhyme, spell_word, thai_word_tone_detector +from pythainlp.util import ( + check_khuap_klam, + rhyme, + spell_word, + thai_word_tone_detector, +) class SpellWordTestCaseC(unittest.TestCase): @@ -35,3 +40,26 @@ def test_thai_word_tone_detector(self): # Edge cases: None and empty string self.assertEqual(thai_word_tone_detector(None), []) self.assertEqual(thai_word_tone_detector(""), []) + + +class KhuapKlamTestCaseC(unittest.TestCase): + def test_check_khuap_klam(self): + # True consonant clusters (คำควบกล้ำแท้) + self.assertTrue(check_khuap_klam("กราบ")) + self.assertTrue(check_khuap_klam("ปลา")) + self.assertTrue(check_khuap_klam("เพราะ")) + self.assertTrue(check_khuap_klam("ตรง")) + + # False consonant clusters (คำควบกล้ำไม่แท้) + self.assertFalse(check_khuap_klam("จริง")) + self.assertFalse(check_khuap_klam("ทราย")) + self.assertFalse(check_khuap_klam("เศร้า")) + + # Not a consonant cluster + self.assertIsNone(check_khuap_klam("แม่")) + self.assertIsNone(check_khuap_klam("ตา")) + self.assertIsNone(check_khuap_klam("มา")) + self.assertIsNone(check_khuap_klam("นา")) + + # Edge cases: empty string returns None + self.assertIsNone(check_khuap_klam(""))