Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/api/util.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions pythainlp/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"arabic_digit_to_thai_digit",
"bahttext",
"censor_profanity",
"check_khuap_klam",
"collate",
"contains_profanity",
"convert_years",
Expand Down Expand Up @@ -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,
Expand Down
78 changes: 78 additions & 0 deletions pythainlp/util/khuap_klam.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 29 additions & 1 deletion tests/compact/testc_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(""))
Loading