From b67a753dbe8e693d742111a7c3910309571e6d48 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:28:11 +0900 Subject: [PATCH 01/43] =?UTF-8?q?Edit=20the=20incorrect=20logic=20in=20che?= =?UTF-8?q?ck=5Fmarttra=20for=20handling=20character=20=E0=B8=A5=20in=20?= =?UTF-8?q?=E0=B9=81=E0=B8=A1=E0=B9=88=E0=B8=81=E0=B8=99=20with=20vowel=20?= =?UTF-8?q?=E0=B8=B2=20(previously=20mistaken=20as=20=E0=B9=81=E0=B8=A1?= =?UTF-8?q?=E0=B9=88=E0=B9=80=E0=B8=81=E0=B8=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the check_marttra function, for สระ "า", letter ล is incorrectly assigned to the แม่เกย marttra instead of แม่กน. This cause the problem of word like พาล to be classified as แม่เกย and has a cascading problem to is_sumpus function. print(kv.check_marttra("พาล")) --> เกย print(kv.is_sumpus("บ้าน", "พาล")) --> False Remove the incorrect elif word[-1] in ["ล"]: return "เกย" and add "ล" back in elif word[-1] in ["ญ", "ณ", "น", "ร", "ฬ"]: return "กน" would solve this problem. --- pythainlp/khavee/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 71a0dde2d..a541993b7 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -279,8 +279,6 @@ def check_marttra(self, word: str) -> str: return "กม" elif word[-1] in ["ย"]: return "เกย" - elif word[-1] in ["ล"]: - return "เกย" elif word[-1] in ["ว"]: return "เกอว" elif word[-1] in ["ก", "ข", "ค", "ฆ"]: @@ -304,7 +302,7 @@ def check_marttra(self, word: str) -> str: "ส", ]: return "กด" - elif word[-1] in ["ญ", "ณ", "น", "ร", "ฬ"]: + elif word[-1] in ["ญ", "ณ", "น", "ร", "ล", "ฬ"]: return "กน" elif word[-1] in ["บ", "ป", "พ", "ฟ", "ภ"]: return "กบ" From 17c8eec8465c01fa61897eeed4b070be600fab50 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:54:00 +0900 Subject: [PATCH 02/43] Fix the logic error in is_sumpus function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both word meet the condition (sara == "อำ" and marttra == "กม") or (sara == "อำ" and marttra == "กม"), the elif part only change the first word sara and mattra and left the second word unchanged. This make all of this incorrectly return False print(kv.is_sumpus("ชัย", "วัย")) print(kv.is_sumpus("วัย", "วัย")) print(kv.is_sumpus("จำ", "ทำ")) print(kv.is_sumpus("ทำ", "ทำ")) change the elif to if would solve this edge case. --- pythainlp/khavee/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index a541993b7..a2287e444 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -340,13 +340,13 @@ def is_sumpus(self, word1: str, word2: str) -> bool: if sara1 == "อะ" and marttra1 == "เกย": sara1 = "ไอ" marttra1 = "กา" - elif sara2 == "อะ" and marttra2 == "เกย": + if sara2 == "อะ" and marttra2 == "เกย": sara2 = "ไอ" marttra2 = "กา" if sara1 == "อำ" and marttra1 == "กม": sara1 = "อำ" marttra1 = "กา" - elif sara2 == "อำ" and marttra2 == "กม": + if sara2 == "อำ" and marttra2 == "กม": sara2 = "อำ" marttra2 = "กา" return bool(marttra1 == marttra2 and sara1 == sara2) From 9c48e22be08a3ff258fce8034b38f7d7ca0f6420 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:49:10 +0900 Subject: [PATCH 03/43] =?UTF-8?q?Fix=20check=5Fsara=20function=20edge=20ca?= =?UTF-8?q?se=20of=20=E0=B8=A4=20which=20can=20have=203=20sounds=20-=20?= =?UTF-8?q?=E0=B9=80=E0=B8=A3=E0=B8=AD,=20=E0=B8=A3=E0=B8=B4,=20=E0=B8=A3?= =?UTF-8?q?=E0=B8=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ฤ not only have "รึ" sound like ฤดู ฤทัย คฤหาสน์ but also can have "เรอ" sound from ฤกษ์ (เริก) and "ริ" sound like ฤทธิ์, อังกฤษ, ตฤณ This commit add additional check for ฤ sounds. --- pythainlp/khavee/core.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index a2287e444..a5bc435ef 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -187,21 +187,33 @@ def check_sara(self, word: str) -> str: sara = [] sara.append("เอาะ") + # In case of ฤ ฦ if "ฤา" in word or "ฦา" in word: sara = [] sara.append("อือ") elif "ฤ" in word or "ฦ" in word: sara = [] sara.append("อึ") - - # In case of กน - if not sara and len(word) == 2: - if word[-1] != "ร": - sara.append("โอะ") + elif "ฤ" in word or "ฦ" in word: + sara = [] + # for 'เออ' (ฤกษ์ - เริก) the only 'เออ' sound exception of ฤ + if "ฤกษ" in word: + sara.append("เออ") + # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) + elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): + sara.append("อิ") + # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม, etc.) else: + sara.append("อึ") + + # In case of สระลดรูป (ออ, โอะ) + if not sara and len(word) >= 2: + if word[-1] == "ร": + # Words ending with ร without vowels usually take the 'ออ' sound (พร, นคร) sara.append("ออ") - elif not sara and len(word) == 3: - sara.append("ออ") + else: + # Other consonants without vowels usually take the hidden 'โอะ' sound (e.g., นม, กรด) + sara.append("โอะ") # In case of บ่ if word == "บ่": From 9bcc977c82ad5e4f83730402091d547af40e6f01 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:01:27 +0900 Subject: [PATCH 04/43] =?UTF-8?q?Fix=20the=20previous=20typo=20in=20?= =?UTF-8?q?=E0=B8=A4=20=E0=B8=A6=20check=5Fsara=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add exception for 'อึ' sound in specific พฤทธิธรรม words and delete the old duplicated logic. --- pythainlp/khavee/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index a5bc435ef..07e84eaf2 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -191,14 +191,14 @@ def check_sara(self, word: str) -> str: if "ฤา" in word or "ฦา" in word: sara = [] sara.append("อือ") - elif "ฤ" in word or "ฦ" in word: - sara = [] - sara.append("อึ") elif "ฤ" in word or "ฦ" in word: sara = [] # for 'เออ' (ฤกษ์ - เริก) the only 'เออ' sound exception of ฤ if "ฤกษ" in word: sara.append("เออ") + # พฤทธิธรรม (พฺรึด-ทิ-ทำ) the only 'อึ' sound exception of "ฤทธ/ฤทธิ" + elif "พฤทธิธรรม" in word: + sara.append("อึ") # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): sara.append("อิ") From 75f498debfee409a5ab1690cc955f6b495a06c65 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:18:14 +0900 Subject: [PATCH 05/43] =?UTF-8?q?Refactor=20conditions=20for=20'=E0=B8=A4'?= =?UTF-8?q?=20sound=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit finetune the ฤ logic and patching the edge cases. --- pythainlp/khavee/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 07e84eaf2..dcb2fdd53 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -194,11 +194,8 @@ def check_sara(self, word: str) -> str: elif "ฤ" in word or "ฦ" in word: sara = [] # for 'เออ' (ฤกษ์ - เริก) the only 'เออ' sound exception of ฤ - if "ฤกษ" in word: + if word == "ฤก" or word.startswith("ฤกษ"): sara.append("เออ") - # พฤทธิธรรม (พฺรึด-ทิ-ทำ) the only 'อึ' sound exception of "ฤทธ/ฤทธิ" - elif "พฤทธิธรรม" in word: - sara.append("อึ") # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): sara.append("อิ") From 747c15314f33e45cea54ca336d450773e2e9500d Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:52:50 +0900 Subject: [PATCH 06/43] =?UTF-8?q?Improve=20handle=5Fkarun=5Fsound=5Fsilenc?= =?UTF-8?q?e=20handling=20and=20vowel=20merging=20(=E0=B9=80=E0=B8=AD=20?= =?UTF-8?q?=E0=B9=80=E0=B8=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edit handling of silent sounds (handle_karun_sound_silence function) to be able to handle more flexible Karun like พันธุ์, สิทธิ์, ฤทธิ์, จันทร์, พระลักษมณ์, กษัตริย์, ภาพยนตร์ instead of the old return word[:-2] which wouldn't correctly strip all the characters that should be silenced. Refactor check_sara function merging of vowel characters เอ เอ to แอ. Remove the previous implementation which mutating the list while iterating over the list. --- pythainlp/khavee/core.py | 62 ++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index dcb2fdd53..c2f50de7b 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -102,15 +102,11 @@ def check_sara(self, word: str) -> str: if countoa == 1 and "อ" in word[-1] and "เ" not in word: sara.remove("ออ") - # In case of เอ เอ - countA = 0 - for i in sara: - if i == "เอ": - countA = countA + 1 - if countA > 1: - sara.remove("เอ") - sara.remove("เอ") - sara.append("แ") + # In case of เอ เอ (e.g., merging two 'เอ' into 'แ') + if sara.count("เอ") >= 2: + sara.remove("เอ") + sara.remove("เอ") + sara.append("แ") # In case of สระประสม if "เอ" in sara and "อะ" in sara: @@ -199,7 +195,7 @@ def check_sara(self, word: str) -> str: # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): sara.append("อิ") - # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม, etc.) + # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม) else: sara.append("อึ") @@ -209,7 +205,7 @@ def check_sara(self, word: str) -> str: # Words ending with ร without vowels usually take the 'ออ' sound (พร, นคร) sara.append("ออ") else: - # Other consonants without vowels usually take the hidden 'โอะ' sound (e.g., นม, กรด) + # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) sara.append("โอะ") # In case of บ่ @@ -676,7 +672,7 @@ def check_aek_too( def handle_karun_sound_silence(self, word: str) -> str: """ - Handle silent sounds in Thai words using '์' character (Karun) + Handle silent sounds in Thai words using '-์' character (Karun) by stripping all characters before the 'Karun' character that should be silenced @@ -684,10 +680,40 @@ def handle_karun_sound_silence(self, word: str) -> str: :return: Thai word with silent consonant stripped :rtype: str """ - sound_silenced = word.endswith("์") - if not sound_silenced: + # Only process if the word ends with Karun (-์) [word like โอห์ม which has Karun in the middle will not be processed] + if not word.endswith("์"): return word - # Remove ์ and the silent consonant before it - # การันต์ (์) marks the consonant immediately before it as silent - word = word[:-2] - return word + + # For specific multi-letter Karun silent suffixes + # 'พระลักษมณ์' -> strip 'ษมณ์' (leaving 'ก' for แม่กก) + if word.endswith("กษมณ์"): + return word[:-4] + + # 'ลักษณ์', 'ทรลักษณ์' -> strip 'ษณ์' avoid breaking 'สัมภาษณ์' + if word.endswith("กษณ์"): + return word[:-3] + + # 'กษัตริย์' -> strip 'ริย์' + if word.endswith("ตริย์"): + return word[:-4] + + # 'กาญจน์' -> strip 'จน์' avoid breaking 'โรจน์' + if word.endswith("ญจน์"): + return word[:-3] + + # For 2-Consonant Karun silent suffixes + # ตร์: ศาสตร์, ภาพยนตร์, กาสาวพัสตร์, เวทมนตร์ + # ทร์: จันทร์, บดินทร์, ภูมินทร์, นราธิเบนทร์ + # ดร์: นิรันดร์ + # ฎร์: ราษฎร์, สุราษฎร์ + if word.endswith(("ตร์", "ทร์", "ดร์", "ฎร์")): + return word[:-3] + + # For Standard Karun silent suffixes (1 Consonant + Optional Vowel + Karun) + # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (ธุ์), สิทธิ์ (ธิ์) + + # Check if there is an upper/lower vowel right before the Karun (e.g., ธุ์, ธิ์) + if len(word) >= 3 and word[-2] in ["ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"]: + return word[:-3] + else: + return word[:-2] From 48a191ce8fb7546195a532c4a66e139e9fd63b4b Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:03:28 +0900 Subject: [PATCH 07/43] Fix the original typo in check_sara function. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change 'แ' to 'แอ' in merging logic --- pythainlp/khavee/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index c2f50de7b..2b6195e9a 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -106,7 +106,7 @@ def check_sara(self, word: str) -> str: if sara.count("เอ") >= 2: sara.remove("เอ") sara.remove("เอ") - sara.append("แ") + sara.append("แอ") # In case of สระประสม if "เอ" in sara and "อะ" in sara: From 4d2ee0360bd96b12604072f8fba97f9c3cfd9cbe Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:06:50 +0900 Subject: [PATCH 08/43] Update normalization logic for is_sumpus function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the discrepancy of อรรม - อัม - อำ. Normalize 'อะ' and 'เกย' to 'ไอ' and 'กา', and 'อำ' and 'อะ' to 'อำ' and 'กา'. --- pythainlp/khavee/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 2b6195e9a..7a28ece2c 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -342,16 +342,18 @@ def is_sumpus(self, word1: str, word2: str) -> bool: marttra2 = self.check_marttra(word2) sara1 = self.check_sara(word1) sara2 = self.check_sara(word2) + # อัย -> ไอ (Normalize 'อะ' + 'เกย' to 'ไอ' + 'กา') if sara1 == "อะ" and marttra1 == "เกย": sara1 = "ไอ" marttra1 = "กา" if sara2 == "อะ" and marttra2 == "เกย": sara2 = "ไอ" marttra2 = "กา" - if sara1 == "อำ" and marttra1 == "กม": + # อัม -> อำ (Normalize both 'อำ' and 'อะ' + 'กม' to 'อำ' + 'กา') + if (sara1 == "อะ" or sara1 == "อำ") and marttra1 == "กม": sara1 = "อำ" marttra1 = "กา" - if sara2 == "อำ" and marttra2 == "กม": + if (sara2 == "อะ" or sara2 == "อำ") and marttra2 == "กม": sara2 = "อำ" marttra2 = "กา" return bool(marttra1 == marttra2 and sara1 == sara2) From 8afcfe545f39f14edee98b0e1f701fc78dfa0751 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:08:02 +0900 Subject: [PATCH 09/43] Fix indentation. Remove Trailing whitespace in line 693. Remove Trailing whitespace in line 693 so the auto code reviewer wouldn't flag as an issue. --- pythainlp/khavee/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 7a28ece2c..db1250e58 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -690,7 +690,7 @@ def handle_karun_sound_silence(self, word: str) -> str: # 'พระลักษมณ์' -> strip 'ษมณ์' (leaving 'ก' for แม่กก) if word.endswith("กษมณ์"): return word[:-4] - + # 'ลักษณ์', 'ทรลักษณ์' -> strip 'ษณ์' avoid breaking 'สัมภาษณ์' if word.endswith("กษณ์"): return word[:-3] From 139b0dea004b54ee3661feee82ef7dbe337a38d8 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:32:04 +0900 Subject: [PATCH 10/43] =?UTF-8?q?Add=20=E0=B8=AA=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=AA=E0=B8=A1=20Transformed=20v?= =?UTF-8?q?owels=20classifier=20to=20check=5Fsara=20and=20Fix=20=E0=B8=84?= =?UTF-8?q?=E0=B8=B3=E0=B9=82=E0=B8=94=E0=B8=94=20Standalone=20words=20in?= =?UTF-8?q?=20check=5Fmattra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In check_sara, use the new and improved word = self.handle_karun_sound_silence(word) instead of the old การันต์ Karun silenct word implementation word = word[:-2] - Refactor the check_sara code to accommodate สระประผม Transformed vowels (อัว, เอะ, แอะ, เออ, โอะ, เอีย, เอือ) especially with ไม้ไต่คู้ (-็) - Handle คำโดด Standalone words in check_mattra so it is now correctly classify as แม่ ก กา. --- pythainlp/khavee/core.py | 99 +++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index db1250e58..aa329d494 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -51,15 +51,16 @@ def check_sara(self, word: str) -> str: 'เออ' """ sara = [] - countoa = 0 + countoa = 0 # Count occurrences of 'อ' # In case of การันย์ - if "์" in word[-1]: - word = word[:-2] + word = self.handle_karun_sound_silence(word) # In case of สระเดี่ยว for i in word: - if i in ("ะ", "ั"): + if i == "ั" and "ว" in word: + sara.append("อัว") + elif i in ("ะ", "ั"): sara.append("อะ") elif i == "ิ": sara.append("อิ") @@ -86,23 +87,24 @@ def check_sara(self, word: str) -> str: elif i == "อ": countoa += 1 sara.append("ออ") - elif i == "ั" and "ว" in word: - sara.append("อัว") elif i in ("ไ", "ใ"): sara.append("ไอ") elif i == "็": - sara.append("ออ") + sara.append("็") elif "รร" in word: if self.check_marttra(word) == "กม": sara.append("อำ") else: sara.append("อะ") + + # Remove tonemarks for checking endings safely + word_req = remove_tonemark(word) - # In case of ออ - if countoa == 1 and "อ" in word[-1] and "เ" not in word: + # In case of ออ (Clean redundant ออ from compound vowels like คือ, มือ) + if countoa == 1 and "อ" in word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: sara.remove("ออ") - # In case of เอ เอ (e.g., merging two 'เอ' into 'แ') + # In case of เอ เอ (merging two 'เอ' into 'แอ') if sara.count("เอ") >= 2: sara.remove("เอ") sara.remove("เอ") @@ -117,6 +119,20 @@ def check_sara(self, word: str) -> str: sara.remove("แอ") sara.remove("อะ") sara.append("แอะ") + + # In case of สระประสม Transformed vowels ไม้ไต่คู้ (-็) + if "็" in sara: + sara.remove("็") + if "เอ" in sara: + sara.remove("เอ") + sara.append("เอะ") # เจ็ด, เป็น, เด็ก + elif "แอ" in sara: + sara.remove("แอ") + sara.append("แอะ") # แข็ง, แท็กซี่, แย็บ + else: + if "ออ" in sara: + sara.remove("ออ") + sara.append("เอาะ") # ก็, ล็อก, ผล็อย if "เอะ" in sara and "ออ" in sara: sara.remove("เอะ") @@ -125,23 +141,19 @@ def check_sara(self, word: str) -> str: elif "เอ" in sara and "อิ" in sara: sara.remove("เอ") sara.remove("อิ") - sara.append("เออ") + sara.append("เออ") # เกิด, เมิน elif "เอ" in sara and "ออ" in sara and "อ" in word[-1]: sara.remove("เอ") sara.remove("ออ") - sara.append("เออ") + sara.append("เออ") # เหม่อ elif "โอ" in sara and "อะ" in sara: sara.remove("โอ") sara.remove("อะ") - sara.append("โอะ") + sara.append("โอะ") # โต๊ะ elif "เอ" in sara and "อี" in sara: sara.remove("เอ") sara.remove("อี") - sara.append("เอีย") - elif "เอ" in sara and "อือ" in sara: - sara.remove("เอ") - sara.remove("อือ") - sara.append("อัว") + sara.append("เอีย") # เรียน elif "เอ" in sara and "อา" in sara: sara.remove("เอ") sara.remove("อา") @@ -153,7 +165,7 @@ def check_sara(self, word: str) -> str: if "อือ" in sara and "เออ" in sara: sara.remove("เออ") sara.remove("อือ") - sara.append("เอือ") + sara.append("เอือ") # มะเขือ, เสือ, เงือก elif "ออ" in sara and len(sara) > 1: sara.remove("ออ") elif "ว" in word and len(sara) == 0: @@ -165,23 +177,24 @@ def check_sara(self, word: str) -> str: # In case of อ if word == "เออะ": - sara = [] - sara.append("เออะ") + sara = ["เออะ"] elif word == "เออ": - sara = [] - sara.append("เออ") + sara = ["เออ"] elif word == "เอ": - sara = [] - sara.append("เอ") + sara = ["เอ"] elif word == "เอะ": - sara = [] - sara.append("เอะ") + sara = ["เอะ"] elif word == "เอา": - sara = [] - sara.append("เอา") + sara = ["เอา"] elif word == "เอาะ": - sara = [] - sara.append("เอาะ") + sara = ["เอาะ"] + + # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย + if "เอ" in sara and word_req.endswith("ย") and self._has_true_final_yl(word_req): + # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) + other_vowels = [v for v in sara if v not in ["เอ", "ออ"]] + if not other_vowels: + sara = ["เออ"] # In case of ฤ ฦ if "ฤา" in word or "ฦา" in word: @@ -210,16 +223,18 @@ def check_sara(self, word: str) -> str: # In case of บ่ if word == "บ่": - sara = [] - sara.append("ออ") + sara = ["ออ"] + #"◌ํ" (nikkhahit) indicates a nasal sound, often associated with the 'อำ' sound in Thai. if "ํ" in word: - sara = [] - sara.append("อำ") + sara = ["อำ"] if "เ" in word and "ื" in word and "อ" in word: - sara = [] - sara.append("เอือ") + sara = ["เอือ"] + + # In case of isolated symbols as words (ลดรูป อะ) + if word_req in ["ณ", "ธ", "พณ"]: + sara = ["อะ"] if not sara: return "Can't find Sara in this word" @@ -255,6 +270,10 @@ def check_marttra(self, word: str) -> str: word = self.handle_karun_sound_silence(word) word = remove_tonemark(word) + + # Check for คำโดด Standalone words + if word in ["บ", "ณ", "ธ", "พณ"]: + return "กา" # Check for ำ at the end (represents "am" sound, ends with m) if word[-1] == "ำ": @@ -690,15 +709,12 @@ def handle_karun_sound_silence(self, word: str) -> str: # 'พระลักษมณ์' -> strip 'ษมณ์' (leaving 'ก' for แม่กก) if word.endswith("กษมณ์"): return word[:-4] - # 'ลักษณ์', 'ทรลักษณ์' -> strip 'ษณ์' avoid breaking 'สัมภาษณ์' if word.endswith("กษณ์"): return word[:-3] - # 'กษัตริย์' -> strip 'ริย์' if word.endswith("ตริย์"): return word[:-4] - # 'กาญจน์' -> strip 'จน์' avoid breaking 'โรจน์' if word.endswith("ญจน์"): return word[:-3] @@ -713,8 +729,7 @@ def handle_karun_sound_silence(self, word: str) -> str: # For Standard Karun silent suffixes (1 Consonant + Optional Vowel + Karun) # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (ธุ์), สิทธิ์ (ธิ์) - - # Check if there is an upper/lower vowel right before the Karun (e.g., ธุ์, ธิ์) + # Check if there is an upper/lower vowel right before the Karun (ธุ์, ธิ์) if len(word) >= 3 and word[-2] in ["ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"]: return word[:-3] else: From b3cc0d626696f8df66a2a926abe8efceaf8d9d7a Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:21:32 +0900 Subject: [PATCH 11/43] Rewrite _has_true_final_yl function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation didn't accounted for tone marks at the end of the word (e.g., ใกล้), Silenced word ตัวการันต์, silent ย in ไ-ย, ใ-ย, สระประสม (เ-ีย), คำควบกล้ำ, and อักษรนำ. This new implementation take into account all of the above making the checker more robust. Limitation: This implementation haven't accounts for คำควบกล้ำ and อักษรนำ for letter "ร" and "ว" (โปร, แปร, ไกว) as this is not present in the original implementation. This would cause the these word to still be misclassified in check_mattra. The full rewrite of _has_true_final_yl that will account for ย, ล, ร, ว will coming soon. --- pythainlp/khavee/core.py | 57 +++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index aa329d494..385ed90ef 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -26,12 +26,41 @@ def _has_true_final_yl(self, word: str) -> bool: :return: True if ย or ล is a true final consonant :rtype: bool """ + # Handle การันย์ + word = self.handle_karun_sound_silence(word) + # Strip tone marks first so words like 'ใกล้' properly evaluate as ending in 'ล' + word = remove_tonemark(word) + if len(word) < 2: return False - # Count consonants in the word - consonant_count = sum(1 for c in word if c in thai_consonants) - # If there are 2+ consonants and word ends with ย or ล, it's a true final - return consonant_count >= 2 and word[-1] in ["ย", "ล"] + + consonants = [c for c in word if c in thai_consonants] + if len(consonants) < 2: + return False + + if word[-1] not in ["ย", "ล"]: + return False + + # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) + if word[-1] == "ย" and ("ไ" in word or "ใ" in word): + return False + # Check for ย inside เ-ีย (เสีย, เมีย) (part of the vowel) + if word[-1] == "ย" and "เ" in word and "ี" in word: + return False + + # Check for ล in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้) + if word[-1] == "ล" and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): + if len(consonants) == 2: + cluster = consonants[0] + consonants[1] + # Valid Thai initial consonant clusters containing ล (ควบแท้ กล ขล คล ปล ผล พล) and อักษรนำ (ข, ฃ, ฉ, ฐ, ถ, ผ, ฝ, ศ, ษ, ส, ห, ก, จ, ฎ, ฏ, ด, ต, บ, ป, อ) + if cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: + # Exception: 'เพล' (monk food) 'น' (แม่กน) + if word == "เพล": + return True + return False + + # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) + return True def check_sara(self, word: str) -> str: """ @@ -96,7 +125,7 @@ def check_sara(self, word: str) -> str: sara.append("อำ") else: sara.append("อะ") - + # Remove tonemarks for checking endings safely word_req = remove_tonemark(word) @@ -221,8 +250,8 @@ def check_sara(self, word: str) -> str: # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) sara.append("โอะ") - # In case of บ่ - if word == "บ่": + # In case of บ่ / บ + if word in ("บ่","บ"): sara = ["ออ"] #"◌ํ" (nikkhahit) indicates a nasal sound, often associated with the 'อำ' sound in Thai. @@ -233,7 +262,7 @@ def check_sara(self, word: str) -> str: sara = ["เอือ"] # In case of isolated symbols as words (ลดรูป อะ) - if word_req in ["ณ", "ธ", "พณ"]: + if word_req in ["ณ", "ธ", "อ","พณ"]: sara = ["อะ"] if not sara: @@ -271,7 +300,7 @@ def check_marttra(self, word: str) -> str: word = self.handle_karun_sound_silence(word) word = remove_tonemark(word) - # Check for คำโดด Standalone words + # Check for อักษรตัวเดียวแทนคำ Standalone words if word in ["บ", "ณ", "ธ", "พณ"]: return "กา" @@ -279,15 +308,17 @@ def check_marttra(self, word: str) -> str: if word[-1] == "ำ": return "กม" - # Check for vowels and special patterns that indicate open syllables (กา) - # For words with ไ/ใ, check if ย/ล is a true final or just part of vowel + # Check for ไ/ใ, check if ย/ล is a true final (เกย,เกอว) or just part of vowel (กา) if "ไ" in word or "ใ" in word: if word[-1] not in ["ย", "ล"]: return "กา" elif not self._has_true_final_yl(word): - # ย/ล is part of the vowel sound, not a true final return "กา" - # else: ย/ล is a true final, continue to consonant classification below + + # Check for เ, แ, โ + ล (คำควบกล้ำ / อักษรนำ) + if word[-1] == "ล" and any(v in word for v in ["เ", "แ", "โ"]): + if not self._has_true_final_yl(word): + return "กา" if "ํ" in word and "า" in word: return "กา" From 6cb76a55743eee95a7d22a044fc075cbb27b177e Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:18:05 +0900 Subject: [PATCH 12/43] Refactor final consonant checks in KhaveeVerifier Final revision. Extensively tested the integration against numerous edge cases and complex test cases as best as I could. This implementation improves the handling of: - `check_sara` - `check_marttra` - `is_sumpus` - `handle_karun_sound_silence` - Internal function `_is_true_final` Note: The `check_karu_lahu` and `check_aek_too` functions are left untouched. The `check_klon` function also remains unmodified, but it should perform better due to the underlying improvements in `is_sumpus`, which now correctly classifies previously failing edge cases. --- pythainlp/khavee/core.py | 58 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 385ed90ef..8694473e3 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -17,7 +17,8 @@ def __init__(self) -> None: KhaveeVerifier: Thai Poetry verifier """ - def _has_true_final_yl(self, word: str) -> bool: + # For backward compatibility, this method is kept as a private method. + def _has_true_final_yl(self, word: str) -> bool: """ Check if ย or ล is a true final consonant (not just part of the vowel sound with ไ/ใ) @@ -26,6 +27,17 @@ def _has_true_final_yl(self, word: str) -> bool: :return: True if ย or ล is a true final consonant :rtype: bool """ + return self._is_true_final(word) + + def _is_true_final(self, word: str) -> bool: + """ + Check if the last character is a true final consonant + (not part of a vowel sound or an initial cluster). + + :param str word: Thai word + :return: True if the ending character acts as a final consonant + :rtype: bool + """ # Handle การันย์ word = self.handle_karun_sound_silence(word) # Strip tone marks first so words like 'ใกล้' properly evaluate as ending in 'ล' @@ -38,25 +50,33 @@ def _has_true_final_yl(self, word: str) -> bool: if len(consonants) < 2: return False - if word[-1] not in ["ย", "ล"]: - return False + last_char = word[-1] + + # if last_char not in ["ย", "ล"]: + # return False # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) - if word[-1] == "ย" and ("ไ" in word or "ใ" in word): + if last_char == "ย" and ("ไ" in word or "ใ" in word): return False # Check for ย inside เ-ีย (เสีย, เมีย) (part of the vowel) - if word[-1] == "ย" and "เ" in word and "ี" in word: + if last_char == "ย" and "เ" in word and "ี" in word: return False - # Check for ล in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้) - if word[-1] == "ล" and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): + # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกวม เขว) + if last_char in ["ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): if len(consonants) == 2: cluster = consonants[0] + consonants[1] - # Valid Thai initial consonant clusters containing ล (ควบแท้ กล ขล คล ปล ผล พล) and อักษรนำ (ข, ฃ, ฉ, ฐ, ถ, ผ, ฝ, ศ, ษ, ส, ห, ก, จ, ฎ, ฏ, ด, ต, บ, ป, อ) - if cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: - # Exception: 'เพล' (monk food) 'น' (แม่กน) - if word == "เพล": - return True + # Check for ล + if last_char == "ล" and cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: + if word == "เพล": return True # Exception 'เพล' - แม่กน (monk food ฉันเพล) + return False + + # Check for ร + if last_char == "ร" and cluster in ["กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร"]: + return False + + # Check for ว (ควบแท้ and อักษรนำ) + if last_char == "ว" and cluster in ["กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"]: return False # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) @@ -219,7 +239,7 @@ def check_sara(self, word: str) -> str: sara = ["เอาะ"] # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย - if "เอ" in sara and word_req.endswith("ย") and self._has_true_final_yl(word_req): + if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(word_req): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) other_vowels = [v for v in sara if v not in ["เอ", "ออ"]] if not other_vowels: @@ -308,16 +328,16 @@ def check_marttra(self, word: str) -> str: if word[-1] == "ำ": return "กม" - # Check for ไ/ใ, check if ย/ล is a true final (เกย,เกอว) or just part of vowel (กา) + # Check for ไ/ใ if "ไ" in word or "ใ" in word: - if word[-1] not in ["ย", "ล"]: + if word[-1] not in ["ย", "ล", "ร", "ว"]: return "กา" - elif not self._has_true_final_yl(word): + elif not self._is_true_final(word): return "กา" - # Check for เ, แ, โ + ล (คำควบกล้ำ / อักษรนำ) - if word[-1] == "ล" and any(v in word for v in ["เ", "แ", "โ"]): - if not self._has_true_final_yl(word): + # Check for เ, แ, โ + ย, ร, ล, ว (คำควบกล้ำ / อักษรนำ) + if word[-1] in ["ย", "ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ"]): + if not self._is_true_final(word): return "กา" if "ํ" in word and "า" in word: From c88e5e3647e379ab08d333baad4006ba99630e4f Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:13:06 +0900 Subject: [PATCH 13/43] Refactor _has_true_final_yl method for clarity Removed commented-out code and trialing whitespace. Due to the complexity of Thai language, to reduce the complexity of `_is_true_final` function from 27 down into 15, we will likely need to split the internal function into smaller sub-function. These could be done in the future as the functionality and correctness of the code is the main priority right now. I have include the comment that should sufficiently enough to guild any maintainer in the future that are going to continue working on this code. --- pythainlp/khavee/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 8694473e3..26c73ef6f 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -18,7 +18,7 @@ def __init__(self) -> None: """ # For backward compatibility, this method is kept as a private method. - def _has_true_final_yl(self, word: str) -> bool: + def _has_true_final_yl(self, word: str) -> bool: """ Check if ย or ล is a true final consonant (not just part of the vowel sound with ไ/ใ) @@ -52,9 +52,6 @@ def _is_true_final(self, word: str) -> bool: last_char = word[-1] - # if last_char not in ["ย", "ล"]: - # return False - # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) if last_char == "ย" and ("ไ" in word or "ใ" in word): return False @@ -168,7 +165,7 @@ def check_sara(self, word: str) -> str: sara.remove("แอ") sara.remove("อะ") sara.append("แอะ") - + # In case of สระประสม Transformed vowels ไม้ไต่คู้ (-็) if "็" in sara: sara.remove("็") From c0191c24ebb5170572fc9780327fc1b1e9926fc5 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:37:26 +0900 Subject: [PATCH 14/43] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pythainlp/khavee/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 26c73ef6f..9f68da4d7 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -59,7 +59,7 @@ def _is_true_final(self, word: str) -> bool: if last_char == "ย" and "เ" in word and "ี" in word: return False - # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกวม เขว) + # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) if last_char in ["ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): if len(consonants) == 2: cluster = consonants[0] + consonants[1] From b75e409ff00c03a6eb99c4bc5c8531e3cc618fa3 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:34:11 +0900 Subject: [PATCH 15/43] =?UTF-8?q?Fix=20`check=5Fsara`=20=E0=B8=A4=20evalua?= =?UTF-8?q?tion=20in=20the=20word=20with=20silent=20Karun.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add original_word in `check_sara` Word like ฤทธิ์ now properly accounted for. handle comment properly according to PEP8 and fix the typo in the comment `ไกว`. The test case edit will need to be done later. --- pythainlp/khavee/core.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 9f68da4d7..b7dce2fc6 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -65,7 +65,8 @@ def _is_true_final(self, word: str) -> bool: cluster = consonants[0] + consonants[1] # Check for ล if last_char == "ล" and cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: - if word == "เพล": return True # Exception 'เพล' - แม่กน (monk food ฉันเพล) + # Exception 'เพล' - แม่กน (monk food ฉันเพล) + if word == "เพล": return True return False # Check for ร @@ -99,6 +100,9 @@ def check_sara(self, word: str) -> str: sara = [] countoa = 0 # Count occurrences of 'อ' + # Store original word to safely evaluate exceptions (like ฤทธิ์) after Karun stripping + original_word = word + # In case of การันย์ word = self.handle_karun_sound_silence(word) @@ -234,7 +238,7 @@ def check_sara(self, word: str) -> str: sara = ["เอา"] elif word == "เอาะ": sara = ["เอาะ"] - + # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(word_req): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) @@ -243,16 +247,16 @@ def check_sara(self, word: str) -> str: sara = ["เออ"] # In case of ฤ ฦ - if "ฤา" in word or "ฦา" in word: - sara = [] - sara.append("อือ") - elif "ฤ" in word or "ฦ" in word: + if "ฤา" in original_word or "ฦา" in original_word: + sara = ["อือ"] + elif "ฤ" in original_word or "ฦ" in original_word: sara = [] # for 'เออ' (ฤกษ์ - เริก) the only 'เออ' sound exception of ฤ - if word == "ฤก" or word.startswith("ฤกษ"): + if word == "ฤก" or original_word.startswith("ฤกษ"): sara.append("เออ") # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) - elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): + # Use original_word here to ensure stripped Karun characters (like ธิ์) are evaluated + elif any(ex in original_word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): sara.append("อิ") # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม) else: From 61d5f9eff14aac85bf15c2cffd8ecdd3a82a61d6 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:47:07 +0900 Subject: [PATCH 16/43] Enhance handling of silent vowels and fix some more edge cases in `check_sara` and `check_mattra` Added handling for silent terminal vowels in Pali/Sanskrit words. --- pythainlp/khavee/core.py | 41 +++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index b7dce2fc6..843c7d7bf 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -105,6 +105,14 @@ def check_sara(self, word: str) -> str: # In case of การันย์ word = self.handle_karun_sound_silence(word) + # Remove tonemarks for checking endings safely + word_req = remove_tonemark(word) + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ + silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"] + if any(word_req.endswith(ex) for ex in silent_vowel_exceptions): + word = word[:-1] + word_req = word_req[:-1] # In case of สระเดี่ยว for i in word: @@ -147,8 +155,10 @@ def check_sara(self, word: str) -> str: else: sara.append("อะ") - # Remove tonemarks for checking endings safely - word_req = remove_tonemark(word) + + # Clean up 'ออ' if 'อ' is acting purely as an initial consonant (อต, อด, อบ, อวบ) + if "ออ" in sara and len(sara) == 1 and word.startswith("อ") and countoa == 1: + sara.remove("ออ") # In case of ออ (Clean redundant ออ from compound vowels like คือ, มือ) if countoa == 1 and "อ" in word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: @@ -219,7 +229,10 @@ def check_sara(self, word: str) -> str: elif "ออ" in sara and len(sara) > 1: sara.remove("ออ") elif "ว" in word and len(sara) == 0: - sara.append("อัว") + if word_req in ["บวร", "วร"]: + sara.append("ออ") + else: + sara.append("อัว") # ควร, บวก, สวม if "ั" in word and self.check_marttra(word) == "กา": sara = [] @@ -239,6 +252,10 @@ def check_sara(self, word: str) -> str: elif word == "เอาะ": sara = ["เอาะ"] + # In case of เ-ือ + if "เ" in word and "ื" in word and "อ" in word: + sara = ["เอือ"] + # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(word_req): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) @@ -247,7 +264,7 @@ def check_sara(self, word: str) -> str: sara = ["เออ"] # In case of ฤ ฦ - if "ฤา" in original_word or "ฦา" in original_word: + if any(ex in original_word for ex in ("ฤา","ฤๅ","ฦา","ฦๅ")): sara = ["อือ"] elif "ฤ" in original_word or "ฦ" in original_word: sara = [] @@ -271,16 +288,13 @@ def check_sara(self, word: str) -> str: # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) sara.append("โอะ") - # In case of บ่ / บ - if word in ("บ่","บ"): - sara = ["ออ"] - #"◌ํ" (nikkhahit) indicates a nasal sound, often associated with the 'อำ' sound in Thai. if "ํ" in word: sara = ["อำ"] - if "เ" in word and "ื" in word and "อ" in word: - sara = ["เอือ"] + # In case of บ่ / บ + if word_req == "บ": + sara = ["ออ"] # In case of isolated symbols as words (ลดรูป อะ) if word_req in ["ณ", "ธ", "อ","พณ"]: @@ -320,7 +334,12 @@ def check_marttra(self, word: str) -> str: word = self.handle_karun_sound_silence(word) word = remove_tonemark(word) - + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ + silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"] + if any(word.endswith(ex) for ex in silent_vowel_exceptions): + word = word[:-1] + # Check for อักษรตัวเดียวแทนคำ Standalone words if word in ["บ", "ณ", "ธ", "พณ"]: return "กา" From 03d123ba1a15935d717602807657308be1c25ac5 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:28:58 +0900 Subject: [PATCH 17/43] =?UTF-8?q?Add=20many=20more=20test=20case=20and=20f?= =?UTF-8?q?ix=20the=20category=20of=20the=20word=20=E0=B9=84=E0=B8=97?= =?UTF-8?q?=E0=B8=A2=20=E0=B9=84=E0=B8=81=E0=B8=A5=20=E0=B9=83=E0=B8=81?= =?UTF-8?q?=E0=B8=A5=E0=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/core/test_khavee.py | 469 +++++++++++++++++++++++++++++++++++++- 1 file changed, 465 insertions(+), 4 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 0a89559e6..0caabed43 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -11,7 +11,170 @@ class KhaveeTestCase(unittest.TestCase): def test_check_sara(self): + # Basic Vowels + self.assertEqual(kv.check_sara("ฉะ"), "อะ") + self.assertEqual(kv.check_sara("ค่ะ"), "อะ") + self.assertEqual(kv.check_sara("กระ"), "อะ") + self.assertEqual(kv.check_sara("อรรถ"), "อะ") + self.assertEqual(kv.check_sara("พาล"), "อา") + self.assertEqual(kv.check_sara("พลา"), "อา") + self.assertEqual(kv.check_sara("ฆาต"), "อา") + self.assertEqual(kv.check_sara("ซ่า"), "อา") + self.assertEqual(kv.check_sara("นิ"), "อิ") + self.assertEqual(kv.check_sara("มิต"), "อิ") + self.assertEqual(kv.check_sara("บิน"), "อิ") + self.assertEqual(kv.check_sara("ยิ้ม"), "อิ") + self.assertEqual(kv.check_sara("พิมพ์"), "อิ") + self.assertEqual(kv.check_sara("หยิบ"), "อิ") + self.assertEqual(kv.check_sara("ตริ"), "อี") + self.assertEqual(kv.check_sara("ปี"), "อี") + self.assertEqual(kv.check_sara("ปี่"), "อี") + self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี + self.assertEqual(kv.check_sara("ตรี"), "อี") + self.assertEqual(kv.check_sara("พลี"), "อี") + self.assertEqual(kv.check_sara("นีย์"), "อี") + self.assertEqual(kv.check_sara("ปรีดิ์"), "อี") + self.assertEqual(kv.check_sara("ตรึก"), "อึ") + self.assertEqual(kv.check_sara("ผึ้ง"), "อึ") + self.assertEqual(kv.check_sara("อึ"), "อึ") + self.assertEqual(kv.check_sara("ซึ้ง"), "อึ") + self.assertEqual(kv.check_sara("ขึ้น"), "อึ") + self.assertEqual(kv.check_sara("หนึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อือ"), "อือ") + self.assertEqual(kv.check_sara("มือ"), "อือ") + self.assertEqual(kv.check_sara("ซื้อ"), "อือ") + self.assertEqual(kv.check_sara("ปรือ"), "อือ") + self.assertEqual(kv.check_sara("ธุ"), "อุ") + self.assertEqual(kv.check_sara("ญุ"), "อุ") + self.assertEqual(kv.check_sara("อุ๊ป"), "อุ") + self.assertEqual(kv.check_sara("สุทธิ์"), "อุ") + self.assertEqual(kv.check_sara("รุฬห์"), "อุ") + self.assertEqual(kv.check_sara("ถู"), "อู") + self.assertEqual(kv.check_sara("หรู"), "อู") + self.assertEqual(kv.check_sara("ธูป"), "อู") + self.assertEqual(kv.check_sara("กู้ด"), "อู") + self.assertEqual(kv.check_sara("กูฏ"), "อู") + self.assertEqual(kv.check_sara("บูรณ์"), "อู") + self.assertEqual(kv.check_sara("กูณฑ์"), "อู") + self.assertEqual(kv.check_sara("สูรย์"), "อู") + self.assertEqual(kv.check_sara("เซะ"), "เอะ") + self.assertEqual(kv.check_sara("เอ"), "เอ") + self.assertEqual(kv.check_sara("เพช"), "เอ") + self.assertEqual(kv.check_sara("เขษม"), "เอ") + self.assertEqual(kv.check_sara("แอะ"), "แอะ") + self.assertEqual(kv.check_sara("และ"), "แอะ") + self.assertEqual(kv.check_sara("แประ"), "แอะ") + self.assertEqual(kv.check_sara("แอ๊ะ"), "แอะ") + self.assertEqual(kv.check_sara("แปร"), "แอ") + self.assertEqual(kv.check_sara("แอร์"), "แอ") + self.assertEqual(kv.check_sara("เรียน"), "เอีย") + self.assertEqual(kv.check_sara("เกียร์"), "เอีย") + self.assertEqual(kv.check_sara("เกียว"), "เอีย") + self.assertEqual(kv.check_sara("เงือก"), "เอือ") + self.assertEqual(kv.check_sara("เอือ"), "เอือ") + self.assertEqual(kv.check_sara("เสือ"), "เอือ") + self.assertEqual(kv.check_sara("เขือ"), "เอือ") + self.assertEqual(kv.check_sara("กลัว"), "อัว") + + # Reduced and Transformed Vowels (สระลดรูป/เปลี่ยนรูป) + self.assertEqual(kv.check_sara("อัน"), "อะ") + self.assertEqual(kv.check_sara("กัน"), "อะ") + self.assertEqual(kv.check_sara("สัญ"), "อะ") + self.assertEqual(kv.check_sara("พวก"), "อัว") + self.assertEqual(kv.check_sara("จวก"), "อัว") + self.assertEqual(kv.check_sara("คน"), "โอะ") + self.assertEqual(kv.check_sara("คล"), "โอะ") + self.assertEqual(kv.check_sara("พร"), "ออ") + self.assertEqual(kv.check_sara("วร"), "ออ") + self.assertEqual(kv.check_sara("บวร"), "ออ") + self.assertEqual(kv.check_sara("เป็น"), "เอะ") + self.assertEqual(kv.check_sara("เจ็ด"), "เอะ") + self.assertEqual(kv.check_sara("เผด็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("แจ็ค"), "แอะ") + self.assertEqual(kv.check_sara("แกร็น"), "แอะ") + self.assertEqual(kv.check_sara("เลย"), "เออ") self.assertEqual(kv.check_sara("เริง"), "เออ") + self.assertEqual(kv.check_sara("เดิน"), "เออ") + self.assertEqual(kv.check_sara("เกิด"), "เออ") + self.assertEqual(kv.check_sara("ล็อก"), "เอาะ") + self.assertEqual(kv.check_sara("อ็อก"), "เอาะ") + self.assertEqual(kv.check_sara("ก็"), "เอาะ") + + # Complex compound and hidden vowels + self.assertEqual(kv.check_sara("ภูมิ"), "อู") # ภูมิใจ (ไม่ใช่ ภู-มิ) + self.assertEqual(kv.check_sara("เกียรติ"), "เอีย") + self.assertEqual(kv.check_sara("เกตุ"), "เอ") + self.assertEqual(kv.check_sara("เมรุ"), "เอ") + self.assertEqual(kv.check_sara("เหตุ"), "เอ") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("ญาติ"), "อา") + self.assertEqual(kv.check_sara("ธาตุ"), "อา") + self.assertEqual(kv.check_sara("พยาธิ"), "อา") + self.assertEqual(kv.check_sara("วัติ"), "อะ") # ประวัติ + self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ + self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ + self.assertEqual(kv.check_sara("สมมุติ"), "อุ") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + + self.assertEqual(kv.check_sara("ออ"), "ออ") + self.assertEqual(kv.check_sara("ขอ"), "ออ") + self.assertEqual(kv.check_sara("งอ"), "ออ") + self.assertEqual(kv.check_sara("กรม"), "โอะ") + self.assertEqual(kv.check_sara("อต"), "โอะ") + self.assertEqual(kv.check_sara("อล"), "โอะ") + self.assertEqual(kv.check_sara("ยศ"), "โอะ") + self.assertEqual(kv.check_sara("โต๊ะ"), "โอะ") + self.assertEqual(kv.check_sara("เร็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("เตลิด"), "เออ") + self.assertEqual(kv.check_sara("เหม่อ"), "เออ") + self.assertEqual(kv.check_sara("เนย"), "เออ") + self.assertEqual(kv.check_sara("เขนย"), "เออ") + self.assertEqual(kv.check_sara("เพนียด"), "เอีย") + self.assertEqual(kv.check_sara("เกลี้ยง"), "เอีย") + self.assertEqual(kv.check_sara("อวก"), "อัว") + self.assertEqual(kv.check_sara("ควร"), "อัว") + self.assertEqual(kv.check_sara("เกลือ"), "เอือ") + self.assertEqual(kv.check_sara("เรื่อง"), "เอือ") + self.assertEqual(kv.check_sara("ธรรม"), "อำ") + self.assertEqual(kv.check_sara("จำ"), "อำ") + self.assertEqual(kv.check_sara("ผล็อย"), "เอาะ") + + # Vowels embedded with Karun (testing correct truncation before check) + self.assertEqual(kv.check_sara("จันทร์"), "อะ") + self.assertEqual(kv.check_sara("กษัตริย์"), "อะ") + self.assertEqual(kv.check_sara("ลักษมณ์"), "อะ") + self.assertEqual(kv.check_sara("ศาสตร์"), "อา") + self.assertEqual(kv.check_sara("สินธุ์"), "อิ") + self.assertEqual(kv.check_sara("ฟิล์ม"), "อิ") + self.assertEqual(kv.check_sara("ทรีย์"), "อี") + self.assertEqual(kv.check_sara("กอล์ฟ"), "ออ") + self.assertEqual(kv.check_sara("เฮิรตซ์"), "เออ") + + # Standalone Character Vowels + self.assertEqual(kv.check_sara("อ"), "อะ") + self.assertEqual(kv.check_sara("ณ"), "อะ") + self.assertEqual(kv.check_sara("ธ"), "อะ") + self.assertEqual(kv.check_sara("พณ"), "อะ") + self.assertEqual(kv.check_sara("บ"), "ออ") + self.assertEqual(kv.check_sara("บ่"), "ออ") + + # ฤ / ฦ Phonemic Rules + self.assertEqual(kv.check_sara("ฤทธิ์"), "อิ") + self.assertEqual(kv.check_sara("กฤษ"), "อิ") + self.assertEqual(kv.check_sara("กฤษณ์"), "อิ") + self.assertEqual(kv.check_sara("ทฤษ"), "อิ") # ทฤษฎี + self.assertEqual(kv.check_sara("ฤกษ์"), "เออ") + self.assertEqual(kv.check_sara("พฤษ"), "อึ") + self.assertEqual(kv.check_sara("พฤติ"), "อึ") + self.assertEqual(kv.check_sara("ฤดู"), "อึ") + self.assertEqual(kv.check_sara("ฤา"), "อือ") + self.assertEqual(kv.check_sara("ฤๅ"), "อือ") + self.assertEqual(kv.check_sara("ฦา"), "อือ") + self.assertEqual(kv.check_sara("ฦๅ"), "อือ") def test_check_marttra(self): self.assertEqual(kv.check_marttra("ปลิง"), "กง") @@ -20,6 +183,14 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("สอง"), "กง") self.assertEqual(kv.check_marttra("เอ็ง"), "กง") self.assertEqual(kv.check_marttra("งง"), "กง") + self.assertEqual(kv.check_marttra("ผึ้ง"), "กง") + self.assertEqual(kv.check_marttra("ซึ้ง"), "กง") + self.assertEqual(kv.check_marttra("หนึ่ง"), "กง") + self.assertEqual(kv.check_marttra("อึ่ง"), "กง") + self.assertEqual(kv.check_marttra("แข็ง"), "กง") + self.assertEqual(kv.check_marttra("เริง"), "กง") + self.assertEqual(kv.check_marttra("เกลี้ยง"), "กง") + self.assertEqual(kv.check_marttra("เรื่อง"), "กง") self.assertEqual(kv.check_marttra("ลม"), "กม") self.assertEqual(kv.check_marttra("เฉลิม"), "กม") @@ -29,6 +200,13 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เกม"), "กม") self.assertEqual(kv.check_marttra("ขำ"), "กม") self.assertEqual(kv.check_marttra("รมย์"), "กม") + self.assertEqual(kv.check_marttra("พิมพ์"), "กม") + self.assertEqual(kv.check_marttra("เขษม"), "กม") + self.assertEqual(kv.check_marttra("ภูมิ"), "กม") + self.assertEqual(kv.check_marttra("กรม"), "กม") + self.assertEqual(kv.check_marttra("ธรรม"), "กม") + self.assertEqual(kv.check_marttra("จำ"), "กม") # สระ อำ ถือเป็นแม่กม + self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") self.assertEqual(kv.check_marttra("โปรย"), "เกย") @@ -36,21 +214,36 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("คอย"), "เกย") self.assertEqual(kv.check_marttra("ง่าย"), "เกย") self.assertEqual(kv.check_marttra("ทัย"), "เกย") - self.assertEqual(kv.check_marttra("ไทย"), "เกย") - self.assertEqual(kv.check_marttra("ไกล"), "เกย") - self.assertEqual(kv.check_marttra("ใกล้"), "เกย") + self.assertEqual(kv.check_marttra("เลื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เปื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เฉื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เหนื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เลย"), "เกย") + self.assertEqual(kv.check_marttra("เขนย"), "เกย") + self.assertEqual(kv.check_marttra("ผล็อย"), "เกย") self.assertEqual(kv.check_marttra("สาว"), "เกอว") self.assertEqual(kv.check_marttra("นิ้ว"), "เกอว") self.assertEqual(kv.check_marttra("แมว"), "เกอว") self.assertEqual(kv.check_marttra("ดาว"), "เกอว") self.assertEqual(kv.check_marttra("แก้ว"), "เกอว") + self.assertEqual(kv.check_marttra("เกียว"), "เกอว") self.assertEqual(kv.check_marttra("บก"), "กก") self.assertEqual(kv.check_marttra("โรค"), "กก") self.assertEqual(kv.check_marttra("ลาก"), "กก") self.assertEqual(kv.check_marttra("นัข"), "กก") self.assertEqual(kv.check_marttra("จักร"), "กก") + self.assertEqual(kv.check_marttra("ตรึก"), "กก") + self.assertEqual(kv.check_marttra("เงือก"), "กก") + self.assertEqual(kv.check_marttra("พวก"), "กก") + self.assertEqual(kv.check_marttra("จวก"), "กก") + self.assertEqual(kv.check_marttra("แจ็ค"), "กก") + self.assertEqual(kv.check_marttra("ล็อก"), "กก") + self.assertEqual(kv.check_marttra("อ็อก"), "กก") + self.assertEqual(kv.check_marttra("อวก"), "กก") + self.assertEqual(kv.check_marttra("ฤกษ์"), "กก") + self.assertEqual(kv.check_marttra("ลักษมณ์"), "กก") self.assertEqual(kv.check_marttra("จด"), "กด") self.assertEqual(kv.check_marttra("ตรวจ"), "กด") @@ -59,6 +252,39 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ตรุษ"), "กด") self.assertEqual(kv.check_marttra("มืด"), "กด") self.assertEqual(kv.check_marttra("โยชน์"), "กด") + self.assertEqual(kv.check_marttra("ชาติ"), "กด") + self.assertEqual(kv.check_marttra("เกียรติ"), "กด") + self.assertEqual(kv.check_marttra("วรรดิ"), "กด") + self.assertEqual(kv.check_marttra("สมมุติ"), "กด") + self.assertEqual(kv.check_marttra("อรรถ"), "กด") + self.assertEqual(kv.check_marttra("ฆาต"), "กด") + self.assertEqual(kv.check_marttra("มิต"), "กด") + self.assertEqual(kv.check_marttra("สุทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กู้ด"), "กด") + self.assertEqual(kv.check_marttra("กูฏ"), "กด") + self.assertEqual(kv.check_marttra("เพช"), "กด") + self.assertEqual(kv.check_marttra("เจ็ด"), "กด") + self.assertEqual(kv.check_marttra("เผด็จ"), "กด") + self.assertEqual(kv.check_marttra("เกิด"), "กด") + self.assertEqual(kv.check_marttra("เกตุ"), "กด") + self.assertEqual(kv.check_marttra("เหตุ"), "กด") + self.assertEqual(kv.check_marttra("ญาติ"), "กด") + self.assertEqual(kv.check_marttra("ธาตุ"), "กด") + self.assertEqual(kv.check_marttra("พยาธิ"), "กด") + self.assertEqual(kv.check_marttra("วัติ"), "กด") + self.assertEqual(kv.check_marttra("พรรดิ"), "กด") + self.assertEqual(kv.check_marttra("อต"), "กด") + self.assertEqual(kv.check_marttra("ยศ"), "กด") + self.assertEqual(kv.check_marttra("เร็จ"), "กด") + self.assertEqual(kv.check_marttra("เตลิด"), "กด") + self.assertEqual(kv.check_marttra("เพนียด"), "กด") + self.assertEqual(kv.check_marttra("กษัตริย์"), "กด") + self.assertEqual(kv.check_marttra("ศาสตร์"), "กด") + self.assertEqual(kv.check_marttra("เฮิรตซ์"), "กด") + self.assertEqual(kv.check_marttra("ฤทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กฤษ"), "กด") + self.assertEqual(kv.check_marttra("กฤษณ์"), "กด") + self.assertEqual(kv.check_marttra("ทฤษ"), "กด") self.assertEqual(kv.check_marttra("มึน"), "กน") self.assertEqual(kv.check_marttra("ร้าน"), "กน") @@ -71,6 +297,30 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เณร"), "กน") self.assertEqual(kv.check_marttra("ยนต์"), "กน") self.assertEqual(kv.check_marttra("กรรณ"), "กน") + self.assertEqual(kv.check_marttra("พาล"), "กน") + self.assertEqual(kv.check_marttra("พาน"), "กน") + self.assertEqual(kv.check_marttra("บิน"), "กน") + self.assertEqual(kv.check_marttra("ขึ้น"), "กน") + self.assertEqual(kv.check_marttra("รุฬห์"), "กน") + self.assertEqual(kv.check_marttra("บูรณ์"), "กน") + self.assertEqual(kv.check_marttra("กูณฑ์"), "กน") + self.assertEqual(kv.check_marttra("สูรย์"), "กน") + self.assertEqual(kv.check_marttra("เรียน"), "กน") + self.assertEqual(kv.check_marttra("อัน"), "กน") + self.assertEqual(kv.check_marttra("กัน"), "กน") + self.assertEqual(kv.check_marttra("สัญ"), "กน") + self.assertEqual(kv.check_marttra("คล"), "กน") + self.assertEqual(kv.check_marttra("พร"), "กน") + self.assertEqual(kv.check_marttra("วร"), "กน") + self.assertEqual(kv.check_marttra("บวร"), "กน") + self.assertEqual(kv.check_marttra("เป็น"), "กน") + self.assertEqual(kv.check_marttra("แกร็น"), "กน") + self.assertEqual(kv.check_marttra("เดิน"), "กน") + self.assertEqual(kv.check_marttra("เมรุ"), "กน") + self.assertEqual(kv.check_marttra("อล"), "กน") + self.assertEqual(kv.check_marttra("ควร"), "กน") + self.assertEqual(kv.check_marttra("จันทร์"), "กน") + self.assertEqual(kv.check_marttra("สินธุ์"), "กน") self.assertEqual(kv.check_marttra("ชอบ"), "กบ") self.assertEqual(kv.check_marttra("ภาพ"), "กบ") @@ -78,17 +328,110 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("รูป"), "กบ") self.assertEqual(kv.check_marttra("เวฟ"), "กบ") self.assertEqual(kv.check_marttra("โลพ"), "กบ") + self.assertEqual(kv.check_marttra("หยิบ"), "กบ") + self.assertEqual(kv.check_marttra("อุ๊ป"), "กบ") + self.assertEqual(kv.check_marttra("ธูป"), "กบ") + self.assertEqual(kv.check_marttra("กอล์ฟ"), "กบ") self.assertEqual(kv.check_marttra("ปลา"), "กา") self.assertEqual(kv.check_marttra("งู"), "กา") self.assertEqual(kv.check_marttra("หมู"), "กา") self.assertEqual(kv.check_marttra("มือ"), "กา") self.assertEqual(kv.check_marttra("ล้อ"), "กา") + self.assertEqual(kv.check_marttra("เมา"), "กา") + self.assertEqual(kv.check_marttra("เหล้า"), "กา") + self.assertEqual(kv.check_marttra("ฉะ"), "กา") + self.assertEqual(kv.check_marttra("ค่ะ"), "กา") + self.assertEqual(kv.check_marttra("กระ"), "กา") + self.assertEqual(kv.check_marttra("พลา"), "กา") + self.assertEqual(kv.check_marttra("ซ่า"), "กา") + self.assertEqual(kv.check_marttra("นิ"), "กา") + self.assertEqual(kv.check_marttra("ตริ"), "กา") + self.assertEqual(kv.check_marttra("ปี"), "กา") + self.assertEqual(kv.check_marttra("ปี่"), "กา") + self.assertEqual(kv.check_marttra("ฎี"), "กา") + self.assertEqual(kv.check_marttra("ตรี"), "กา") + self.assertEqual(kv.check_marttra("พลี"), "กา") + self.assertEqual(kv.check_marttra("อึ"), "กา") + self.assertEqual(kv.check_marttra("อือ"), "กา") + self.assertEqual(kv.check_marttra("ซื้อ"), "กา") + self.assertEqual(kv.check_marttra("ปรือ"), "กา") + self.assertEqual(kv.check_marttra("ธุ"), "กา") + self.assertEqual(kv.check_marttra("ญุ"), "กา") + self.assertEqual(kv.check_marttra("ถู"), "กา") + self.assertEqual(kv.check_marttra("หรู"), "กา") + self.assertEqual(kv.check_marttra("เซะ"), "กา") + self.assertEqual(kv.check_marttra("เอ"), "กา") + self.assertEqual(kv.check_marttra("แอะ"), "กา") + self.assertEqual(kv.check_marttra("และ"), "กา") + self.assertEqual(kv.check_marttra("แประ"), "กา") + self.assertEqual(kv.check_marttra("แอ๊ะ"), "กา") + self.assertEqual(kv.check_marttra("แอร์"), "กา") + self.assertEqual(kv.check_marttra("เกียร์"), "กา") + self.assertEqual(kv.check_marttra("เอือ"), "กา") + self.assertEqual(kv.check_marttra("เสือ"), "กา") + self.assertEqual(kv.check_marttra("เขือ"), "กา") + self.assertEqual(kv.check_marttra("กลัว"), "กา") + self.assertEqual(kv.check_marttra("ก็"), "กา") + self.assertEqual(kv.check_marttra("ออ"), "กา") + self.assertEqual(kv.check_marttra("ขอ"), "กา") + self.assertEqual(kv.check_marttra("งอ"), "กา") + self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") + self.assertEqual(kv.check_marttra("เหม่อ"), "กา") + self.assertEqual(kv.check_marttra("เกลือ"), "กา") + self.assertEqual(kv.check_marttra("ทรีย์"), "กา") + self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") + self.assertEqual(kv.check_marttra("นีย์"), "กา") + + # Fake Finals (คำควบกล้า, คำที่มีพยัญชนะ/สระไม่ออกเสียง) mapping to open syllables + self.assertEqual(kv.check_marttra("ไทย"), "กา") + self.assertEqual(kv.check_marttra("ไกล"), "กา") + self.assertEqual(kv.check_marttra("ใกล้"), "กา") + self.assertEqual(kv.check_marttra("เสีย"), "กา") + self.assertEqual(kv.check_marttra("เปล"), "กา") + self.assertEqual(kv.check_marttra("ไกว"), "กา") + self.assertEqual(kv.check_marttra("โปร"), "กา") + self.assertEqual(kv.check_marttra("โปล"), "กา") + self.assertEqual(kv.check_marttra("แปร"), "กา") + self.assertEqual(kv.check_marttra("ไฟล์"), "กา") + + # Standalone Characters mapping to open syllables + self.assertEqual(kv.check_marttra("ธ"), "กา") + self.assertEqual(kv.check_marttra("ณ"), "กา") + self.assertEqual(kv.check_marttra("พณ"), "กา") + self.assertEqual(kv.check_marttra("บ"), "กา") + self.assertEqual(kv.check_marttra("บ่"), "กา") + self.assertEqual(kv.check_marttra("อ"), "กา") + + # ฤ / ฦ + self.assertEqual(kv.check_marttra("ฤ"), "กา") + self.assertEqual(kv.check_marttra("ฦ"), "กา") + self.assertEqual(kv.check_marttra("ฤา"), "กา") + self.assertEqual(kv.check_marttra("ฤๅ"), "กา") + self.assertEqual(kv.check_marttra("ฦา"), "กา") + self.assertEqual(kv.check_marttra("ฦๅ"), "กา") def test_is_sumpus(self): self.assertTrue(kv.is_sumpus("สรร", "อัน")) self.assertFalse(kv.is_sumpus("สรร", "แมว")) + # Structural equivalence logic & Normalization + self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) + self.assertTrue(kv.is_sumpus("ทำ", "จำ")) + self.assertTrue(kv.is_sumpus("กรรม", "ธรรม")) + self.assertTrue(kv.is_sumpus("ธรรม", "สัม")) + self.assertTrue(kv.is_sumpus("ธรรม", "จำ")) + self.assertTrue(kv.is_sumpus("กัย", "ไก")) + self.assertTrue(kv.is_sumpus("กัย", "ไกล")) + self.assertTrue(kv.is_sumpus("ใจ", "ไทย")) + self.assertTrue(kv.is_sumpus("เลย", "เกย")) + self.assertTrue(kv.is_sumpus("พวก", "จวก")) + self.assertTrue(kv.is_sumpus("ฤทธิ์", "กิด")) + self.assertTrue(kv.is_sumpus("ใจ", "จัย")) + + # Verify strict phonemic constraints are maintained + self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ + def test_check_klon(self): self.assertEqual( kv.check_klon( @@ -156,6 +499,11 @@ def setUp(self): def test_word_without_karun_unchanged(self): self.assertEqual(self.kv.handle_karun_sound_silence("คน"), "คน") self.assertEqual(self.kv.handle_karun_sound_silence("กา"), "กา") + # internal karun unchanged + self.assertEqual(self.kv.handle_karun_sound_silence("การ์ตูน"), "การ์ตูน") + self.assertEqual(self.kv.handle_karun_sound_silence("กอล์ฟ"), "กอล์ฟ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฟิล์ม"), "ฟิล์ม") + self.assertEqual(self.kv.handle_karun_sound_silence("สตาร์ตอัป"), "สตาร์ตอัป") def test_word_ending_with_karun_stripped(self): # เกมส์ → drop ์ and the consonant before it (ส) → เกม @@ -165,9 +513,64 @@ def test_word_ending_with_karun_stripped_2(self): # รักษ์ → drop ์ + ษ → รัก self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") + def test_complex_karun_stripped(self): + # Explicit evaluation of single, multi-consonant, and vowel-embedded Karun rules + self.assertEqual(self.kv.handle_karun_sound_silence("จันทร์"), "จัน") + self.assertEqual(self.kv.handle_karun_sound_silence("สิทธิ์"), "สิท") + self.assertEqual(self.kv.handle_karun_sound_silence("กษัตริย์"), "กษัต") + self.assertEqual(self.kv.handle_karun_sound_silence("พระลักษมณ์"), "พระลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("อินทรีย์"), "อินทรี") + self.assertEqual(self.kv.handle_karun_sound_silence("ภาพยนตร์"), "ภาพยน") + self.assertEqual(self.kv.handle_karun_sound_silence("กาสาวพัสตร์"), "กาสาวพัส") + self.assertEqual(self.kv.handle_karun_sound_silence("ไปรษณีย์"), "ไปรษณี") + self.assertEqual(self.kv.handle_karun_sound_silence("สัปดาห์"), "สัปดา") + self.assertEqual(self.kv.handle_karun_sound_silence("เฮิรตซ์"), "เฮิรต") + self.assertEqual(self.kv.handle_karun_sound_silence("วิศวกรรมศาสตร์"), "วิศวกรรมศาส") + self.assertEqual(self.kv.handle_karun_sound_silence("กบินทร์"), "กบิน") + self.assertEqual(self.kv.handle_karun_sound_silence("นราธิเบนทร์"), "นราธิเบน") + self.assertEqual(self.kv.handle_karun_sound_silence("พรหมจรรย์"), "พรหมจรร") + self.assertEqual(self.kv.handle_karun_sound_silence("กรณีย์"), "กรณี") + self.assertEqual(self.kv.handle_karun_sound_silence("รังสิมันตุ์"), "รังสิมัน") + self.assertEqual(self.kv.handle_karun_sound_silence("รามเกียรติ์"), "รามเกียร") + self.assertEqual(self.kv.handle_karun_sound_silence("ทรลักษณ์"), "ทรลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("ธำมรงค์"), "ธำมรง") + self.assertEqual(self.kv.handle_karun_sound_silence("ศัพท์"), "ศัพ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฉันท์"), "ฉัน") + self.assertEqual(self.kv.handle_karun_sound_silence("เจ้าเล่ห์"), "เจ้าเล่") + self.assertEqual(self.kv.handle_karun_sound_silence("สงเคราะห์"), "สงเคราะ") + self.assertEqual(self.kv.handle_karun_sound_silence("ราชทัณฑ์"), "ราชทัณ") + self.assertEqual(self.kv.handle_karun_sound_silence("สวาสดิ์"), "สวาส") + self.assertEqual(self.kv.handle_karun_sound_silence("สุปรีดิ์"), "สุปรี") + def test_returns_string(self): self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) +class KhaveeIsTrueFinalTestCase(unittest.TestCase): + """Tests for internal method KhaveeVerifier._is_true_final""" + + def setUp(self): + self.kv = KhaveeVerifier() + + def test_true_finals(self): + self.assertTrue(self.kv._is_true_final("จัย")) + self.assertTrue(self.kv._is_true_final("สมัย")) + self.assertTrue(self.kv._is_true_final("เลื่อย")) + self.assertTrue(self.kv._is_true_final("เปื่อย")) + self.assertTrue(self.kv._is_true_final("เฉื่อย")) + self.assertTrue(self.kv._is_true_final("เหนื่อย")) + + def test_fake_finals(self): + self.assertFalse(self.kv._is_true_final("ไทย")) + self.assertFalse(self.kv._is_true_final("ใคร")) + self.assertFalse(self.kv._is_true_final("ไกล")) + self.assertFalse(self.kv._is_true_final("ใกล้")) + self.assertFalse(self.kv._is_true_final("เสีย")) + self.assertFalse(self.kv._is_true_final("ไกว")) + self.assertFalse(self.kv._is_true_final("โปร")) + self.assertFalse(self.kv._is_true_final("แปร")) + self.assertFalse(self.kv._is_true_final("เปล")) + self.assertFalse(self.kv._is_true_final("ไฟล์")) + class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_aek_too""" @@ -221,6 +624,63 @@ def test_check_klon8_correct_poem(self): ) self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) + def test_check_klon8_correct_poem_2(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_correct_poem_3(self): + poem = ( + "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " + "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " + "ลูกผู้ชายลายมือนั้นคือยศ เเจ้าจงอตส่าห์ทำสม่ำเสมียน " + "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_invalid_poem(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["In sentence 2, there are more than 10 words. ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", "Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) + + def test_check_klon8_invalid_poem_2(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) + + def test_check_klon8_invalid_poem_3(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["Can't find rhyme between paragraphs ('เหมือน', 'เตือด') in paragraph 1"], result) + class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_sara""" @@ -250,8 +710,9 @@ def test_ru_sara(self): self.assertEqual(self.kv.check_sara("ฤ"), "อึ") def test_ruea_sara(self): - # ฤา (ฤ + sara aa U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa + # ฤา (ฤ + sara า U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") + self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") def test_เอือ_sara(self): self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") From 9085b579d538da7b1933bfd0b2a07eb698aa8e39 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:34:29 +0900 Subject: [PATCH 18/43] =?UTF-8?q?Fix=20the=20=E0=B8=99=E0=B8=B4=E0=B8=81?= =?UTF-8?q?=E0=B8=AB=E0=B8=B4=E0=B8=95=20-=E0=B9=8D=20and=20=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B8=A2=E0=B8=B2=E0=B8=A7=20"=E0=B9=85"=20?= =?UTF-8?q?=E0=B8=A4=20=E0=B8=A4=E0=B9=85=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pythainlp/khavee/core.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 843c7d7bf..1d62eff98 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -288,9 +288,12 @@ def check_sara(self, word: str) -> str: # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) sara.append("โอะ") - #"◌ํ" (nikkhahit) indicates a nasal sound, often associated with the 'อำ' sound in Thai. + # In case of นิกหิต (-ํ) + า (miss-typed of สระอำ) or standalone นิกหิต (-ํ) 'อัง' if "ํ" in word: - sara = ["อำ"] + if "ํา" in word: + sara = ["อำ"] # The strict decomposed 'อำ' typo + else: + sara = ["อะ"] # Standalone sounds like 'อัง' (อะ + ง) from pali/sanskrit # In case of บ่ / บ if word_req == "บ": @@ -341,13 +344,17 @@ def check_marttra(self, word: str) -> str: word = word[:-1] # Check for อักษรตัวเดียวแทนคำ Standalone words - if word in ["บ", "ณ", "ธ", "พณ"]: + if word in ["บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"]: return "กา" - # Check for ำ at the end (represents "am" sound, ends with m) - if word[-1] == "ำ": + # Check for ำ or นิคหิต (-ํ) + า + if word[-1] == "ำ" or word.endswith("ํา"): return "กม" + # Check for standalone นิคหิต (-ํ) 'อัง' + if word.endswith("ํ"): + return "กง" + # Check for ไ/ใ if "ไ" in word or "ใ" in word: if word[-1] not in ["ย", "ล", "ร", "ว"]: @@ -359,11 +366,11 @@ def check_marttra(self, word: str) -> str: if word[-1] in ["ย", "ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ"]): if not self._is_true_final(word): return "กา" - - if "ํ" in word and "า" in word: - return "กา" - elif ( - word[-1] in ["า", "ะ", "ิ", "ี", "ุ", "ู", "อ"] + + # Check for ตัวสะกด final consonants + # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) + if ( + word[-1] in ["า", "ๅ", "ะ", "ิ", "ี", "ุ", "ู", "อ"] or ("ี" in word and "ย" in word[-1]) or ("ื" in word and "อ" in word[-1]) ): From bef0c446d4b539365ac8f3c40aed3232a923d797 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 22 Jul 2026 13:43:22 +0900 Subject: [PATCH 19/43] =?UTF-8?q?Fix=20the=20typo=20in=20the=20testcase=20?= =?UTF-8?q?kv.check=5Fsara("=E0=B8=95=E0=B8=A3=E0=B8=B5"),=20"=E0=B8=AD?= =?UTF-8?q?=E0=B8=B5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/core/test_khavee.py | 469 +++++++++++++++++++++++++++++++++++++- 1 file changed, 465 insertions(+), 4 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 0a89559e6..9d40dcd91 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -11,7 +11,170 @@ class KhaveeTestCase(unittest.TestCase): def test_check_sara(self): + # Basic Vowels + self.assertEqual(kv.check_sara("ฉะ"), "อะ") + self.assertEqual(kv.check_sara("ค่ะ"), "อะ") + self.assertEqual(kv.check_sara("กระ"), "อะ") + self.assertEqual(kv.check_sara("อรรถ"), "อะ") + self.assertEqual(kv.check_sara("พาล"), "อา") + self.assertEqual(kv.check_sara("พลา"), "อา") + self.assertEqual(kv.check_sara("ฆาต"), "อา") + self.assertEqual(kv.check_sara("ซ่า"), "อา") + self.assertEqual(kv.check_sara("นิ"), "อิ") + self.assertEqual(kv.check_sara("มิต"), "อิ") + self.assertEqual(kv.check_sara("บิน"), "อิ") + self.assertEqual(kv.check_sara("ยิ้ม"), "อิ") + self.assertEqual(kv.check_sara("พิมพ์"), "อิ") + self.assertEqual(kv.check_sara("หยิบ"), "อิ") + self.assertEqual(kv.check_sara("ตรี"), "อี") + self.assertEqual(kv.check_sara("ปี"), "อี") + self.assertEqual(kv.check_sara("ปี่"), "อี") + self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี + self.assertEqual(kv.check_sara("ตรี"), "อี") + self.assertEqual(kv.check_sara("พลี"), "อี") + self.assertEqual(kv.check_sara("นีย์"), "อี") + self.assertEqual(kv.check_sara("ปรีดิ์"), "อี") + self.assertEqual(kv.check_sara("ตรึก"), "อึ") + self.assertEqual(kv.check_sara("ผึ้ง"), "อึ") + self.assertEqual(kv.check_sara("อึ"), "อึ") + self.assertEqual(kv.check_sara("ซึ้ง"), "อึ") + self.assertEqual(kv.check_sara("ขึ้น"), "อึ") + self.assertEqual(kv.check_sara("หนึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อือ"), "อือ") + self.assertEqual(kv.check_sara("มือ"), "อือ") + self.assertEqual(kv.check_sara("ซื้อ"), "อือ") + self.assertEqual(kv.check_sara("ปรือ"), "อือ") + self.assertEqual(kv.check_sara("ธุ"), "อุ") + self.assertEqual(kv.check_sara("ญุ"), "อุ") + self.assertEqual(kv.check_sara("อุ๊ป"), "อุ") + self.assertEqual(kv.check_sara("สุทธิ์"), "อุ") + self.assertEqual(kv.check_sara("รุฬห์"), "อุ") + self.assertEqual(kv.check_sara("ถู"), "อู") + self.assertEqual(kv.check_sara("หรู"), "อู") + self.assertEqual(kv.check_sara("ธูป"), "อู") + self.assertEqual(kv.check_sara("กู้ด"), "อู") + self.assertEqual(kv.check_sara("กูฏ"), "อู") + self.assertEqual(kv.check_sara("บูรณ์"), "อู") + self.assertEqual(kv.check_sara("กูณฑ์"), "อู") + self.assertEqual(kv.check_sara("สูรย์"), "อู") + self.assertEqual(kv.check_sara("เซะ"), "เอะ") + self.assertEqual(kv.check_sara("เอ"), "เอ") + self.assertEqual(kv.check_sara("เพช"), "เอ") + self.assertEqual(kv.check_sara("เขษม"), "เอ") + self.assertEqual(kv.check_sara("แอะ"), "แอะ") + self.assertEqual(kv.check_sara("และ"), "แอะ") + self.assertEqual(kv.check_sara("แประ"), "แอะ") + self.assertEqual(kv.check_sara("แอ๊ะ"), "แอะ") + self.assertEqual(kv.check_sara("แปร"), "แอ") + self.assertEqual(kv.check_sara("แอร์"), "แอ") + self.assertEqual(kv.check_sara("เรียน"), "เอีย") + self.assertEqual(kv.check_sara("เกียร์"), "เอีย") + self.assertEqual(kv.check_sara("เกียว"), "เอีย") + self.assertEqual(kv.check_sara("เงือก"), "เอือ") + self.assertEqual(kv.check_sara("เอือ"), "เอือ") + self.assertEqual(kv.check_sara("เสือ"), "เอือ") + self.assertEqual(kv.check_sara("เขือ"), "เอือ") + self.assertEqual(kv.check_sara("กลัว"), "อัว") + + # Reduced and Transformed Vowels (สระลดรูป/เปลี่ยนรูป) + self.assertEqual(kv.check_sara("อัน"), "อะ") + self.assertEqual(kv.check_sara("กัน"), "อะ") + self.assertEqual(kv.check_sara("สัญ"), "อะ") + self.assertEqual(kv.check_sara("พวก"), "อัว") + self.assertEqual(kv.check_sara("จวก"), "อัว") + self.assertEqual(kv.check_sara("คน"), "โอะ") + self.assertEqual(kv.check_sara("คล"), "โอะ") + self.assertEqual(kv.check_sara("พร"), "ออ") + self.assertEqual(kv.check_sara("วร"), "ออ") + self.assertEqual(kv.check_sara("บวร"), "ออ") + self.assertEqual(kv.check_sara("เป็น"), "เอะ") + self.assertEqual(kv.check_sara("เจ็ด"), "เอะ") + self.assertEqual(kv.check_sara("เผด็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("แจ็ค"), "แอะ") + self.assertEqual(kv.check_sara("แกร็น"), "แอะ") + self.assertEqual(kv.check_sara("เลย"), "เออ") self.assertEqual(kv.check_sara("เริง"), "เออ") + self.assertEqual(kv.check_sara("เดิน"), "เออ") + self.assertEqual(kv.check_sara("เกิด"), "เออ") + self.assertEqual(kv.check_sara("ล็อก"), "เอาะ") + self.assertEqual(kv.check_sara("อ็อก"), "เอาะ") + self.assertEqual(kv.check_sara("ก็"), "เอาะ") + + # Complex compound and hidden vowels + self.assertEqual(kv.check_sara("ภูมิ"), "อู") # ภูมิใจ (ไม่ใช่ ภู-มิ) + self.assertEqual(kv.check_sara("เกียรติ"), "เอีย") + self.assertEqual(kv.check_sara("เกตุ"), "เอ") + self.assertEqual(kv.check_sara("เมรุ"), "เอ") + self.assertEqual(kv.check_sara("เหตุ"), "เอ") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("ญาติ"), "อา") + self.assertEqual(kv.check_sara("ธาตุ"), "อา") + self.assertEqual(kv.check_sara("พยาธิ"), "อา") + self.assertEqual(kv.check_sara("วัติ"), "อะ") # ประวัติ + self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ + self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ + self.assertEqual(kv.check_sara("สมมุติ"), "อุ") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + + self.assertEqual(kv.check_sara("ออ"), "ออ") + self.assertEqual(kv.check_sara("ขอ"), "ออ") + self.assertEqual(kv.check_sara("งอ"), "ออ") + self.assertEqual(kv.check_sara("กรม"), "โอะ") + self.assertEqual(kv.check_sara("อต"), "โอะ") + self.assertEqual(kv.check_sara("อล"), "โอะ") + self.assertEqual(kv.check_sara("ยศ"), "โอะ") + self.assertEqual(kv.check_sara("โต๊ะ"), "โอะ") + self.assertEqual(kv.check_sara("เร็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("เตลิด"), "เออ") + self.assertEqual(kv.check_sara("เหม่อ"), "เออ") + self.assertEqual(kv.check_sara("เนย"), "เออ") + self.assertEqual(kv.check_sara("เขนย"), "เออ") + self.assertEqual(kv.check_sara("เพนียด"), "เอีย") + self.assertEqual(kv.check_sara("เกลี้ยง"), "เอีย") + self.assertEqual(kv.check_sara("อวก"), "อัว") + self.assertEqual(kv.check_sara("ควร"), "อัว") + self.assertEqual(kv.check_sara("เกลือ"), "เอือ") + self.assertEqual(kv.check_sara("เรื่อง"), "เอือ") + self.assertEqual(kv.check_sara("ธรรม"), "อำ") + self.assertEqual(kv.check_sara("จำ"), "อำ") + self.assertEqual(kv.check_sara("ผล็อย"), "เอาะ") + + # Vowels embedded with Karun (testing correct truncation before check) + self.assertEqual(kv.check_sara("จันทร์"), "อะ") + self.assertEqual(kv.check_sara("กษัตริย์"), "อะ") + self.assertEqual(kv.check_sara("ลักษมณ์"), "อะ") + self.assertEqual(kv.check_sara("ศาสตร์"), "อา") + self.assertEqual(kv.check_sara("สินธุ์"), "อิ") + self.assertEqual(kv.check_sara("ฟิล์ม"), "อิ") + self.assertEqual(kv.check_sara("ทรีย์"), "อี") + self.assertEqual(kv.check_sara("กอล์ฟ"), "ออ") + self.assertEqual(kv.check_sara("เฮิรตซ์"), "เออ") + + # Standalone Character Vowels + self.assertEqual(kv.check_sara("อ"), "อะ") + self.assertEqual(kv.check_sara("ณ"), "อะ") + self.assertEqual(kv.check_sara("ธ"), "อะ") + self.assertEqual(kv.check_sara("พณ"), "อะ") + self.assertEqual(kv.check_sara("บ"), "ออ") + self.assertEqual(kv.check_sara("บ่"), "ออ") + + # ฤ / ฦ Phonemic Rules + self.assertEqual(kv.check_sara("ฤทธิ์"), "อิ") + self.assertEqual(kv.check_sara("กฤษ"), "อิ") + self.assertEqual(kv.check_sara("กฤษณ์"), "อิ") + self.assertEqual(kv.check_sara("ทฤษ"), "อิ") # ทฤษฎี + self.assertEqual(kv.check_sara("ฤกษ์"), "เออ") + self.assertEqual(kv.check_sara("พฤษ"), "อึ") + self.assertEqual(kv.check_sara("พฤติ"), "อึ") + self.assertEqual(kv.check_sara("ฤดู"), "อึ") + self.assertEqual(kv.check_sara("ฤา"), "อือ") + self.assertEqual(kv.check_sara("ฤๅ"), "อือ") + self.assertEqual(kv.check_sara("ฦา"), "อือ") + self.assertEqual(kv.check_sara("ฦๅ"), "อือ") def test_check_marttra(self): self.assertEqual(kv.check_marttra("ปลิง"), "กง") @@ -20,6 +183,14 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("สอง"), "กง") self.assertEqual(kv.check_marttra("เอ็ง"), "กง") self.assertEqual(kv.check_marttra("งง"), "กง") + self.assertEqual(kv.check_marttra("ผึ้ง"), "กง") + self.assertEqual(kv.check_marttra("ซึ้ง"), "กง") + self.assertEqual(kv.check_marttra("หนึ่ง"), "กง") + self.assertEqual(kv.check_marttra("อึ่ง"), "กง") + self.assertEqual(kv.check_marttra("แข็ง"), "กง") + self.assertEqual(kv.check_marttra("เริง"), "กง") + self.assertEqual(kv.check_marttra("เกลี้ยง"), "กง") + self.assertEqual(kv.check_marttra("เรื่อง"), "กง") self.assertEqual(kv.check_marttra("ลม"), "กม") self.assertEqual(kv.check_marttra("เฉลิม"), "กม") @@ -29,6 +200,13 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เกม"), "กม") self.assertEqual(kv.check_marttra("ขำ"), "กม") self.assertEqual(kv.check_marttra("รมย์"), "กม") + self.assertEqual(kv.check_marttra("พิมพ์"), "กม") + self.assertEqual(kv.check_marttra("เขษม"), "กม") + self.assertEqual(kv.check_marttra("ภูมิ"), "กม") + self.assertEqual(kv.check_marttra("กรม"), "กม") + self.assertEqual(kv.check_marttra("ธรรม"), "กม") + self.assertEqual(kv.check_marttra("จำ"), "กม") # สระ อำ ถือเป็นแม่กม + self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") self.assertEqual(kv.check_marttra("โปรย"), "เกย") @@ -36,21 +214,36 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("คอย"), "เกย") self.assertEqual(kv.check_marttra("ง่าย"), "เกย") self.assertEqual(kv.check_marttra("ทัย"), "เกย") - self.assertEqual(kv.check_marttra("ไทย"), "เกย") - self.assertEqual(kv.check_marttra("ไกล"), "เกย") - self.assertEqual(kv.check_marttra("ใกล้"), "เกย") + self.assertEqual(kv.check_marttra("เลื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เปื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เฉื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เหนื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เลย"), "เกย") + self.assertEqual(kv.check_marttra("เขนย"), "เกย") + self.assertEqual(kv.check_marttra("ผล็อย"), "เกย") self.assertEqual(kv.check_marttra("สาว"), "เกอว") self.assertEqual(kv.check_marttra("นิ้ว"), "เกอว") self.assertEqual(kv.check_marttra("แมว"), "เกอว") self.assertEqual(kv.check_marttra("ดาว"), "เกอว") self.assertEqual(kv.check_marttra("แก้ว"), "เกอว") + self.assertEqual(kv.check_marttra("เกียว"), "เกอว") self.assertEqual(kv.check_marttra("บก"), "กก") self.assertEqual(kv.check_marttra("โรค"), "กก") self.assertEqual(kv.check_marttra("ลาก"), "กก") self.assertEqual(kv.check_marttra("นัข"), "กก") self.assertEqual(kv.check_marttra("จักร"), "กก") + self.assertEqual(kv.check_marttra("ตรึก"), "กก") + self.assertEqual(kv.check_marttra("เงือก"), "กก") + self.assertEqual(kv.check_marttra("พวก"), "กก") + self.assertEqual(kv.check_marttra("จวก"), "กก") + self.assertEqual(kv.check_marttra("แจ็ค"), "กก") + self.assertEqual(kv.check_marttra("ล็อก"), "กก") + self.assertEqual(kv.check_marttra("อ็อก"), "กก") + self.assertEqual(kv.check_marttra("อวก"), "กก") + self.assertEqual(kv.check_marttra("ฤกษ์"), "กก") + self.assertEqual(kv.check_marttra("ลักษมณ์"), "กก") self.assertEqual(kv.check_marttra("จด"), "กด") self.assertEqual(kv.check_marttra("ตรวจ"), "กด") @@ -59,6 +252,39 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ตรุษ"), "กด") self.assertEqual(kv.check_marttra("มืด"), "กด") self.assertEqual(kv.check_marttra("โยชน์"), "กด") + self.assertEqual(kv.check_marttra("ชาติ"), "กด") + self.assertEqual(kv.check_marttra("เกียรติ"), "กด") + self.assertEqual(kv.check_marttra("วรรดิ"), "กด") + self.assertEqual(kv.check_marttra("สมมุติ"), "กด") + self.assertEqual(kv.check_marttra("อรรถ"), "กด") + self.assertEqual(kv.check_marttra("ฆาต"), "กด") + self.assertEqual(kv.check_marttra("มิต"), "กด") + self.assertEqual(kv.check_marttra("สุทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กู้ด"), "กด") + self.assertEqual(kv.check_marttra("กูฏ"), "กด") + self.assertEqual(kv.check_marttra("เพช"), "กด") + self.assertEqual(kv.check_marttra("เจ็ด"), "กด") + self.assertEqual(kv.check_marttra("เผด็จ"), "กด") + self.assertEqual(kv.check_marttra("เกิด"), "กด") + self.assertEqual(kv.check_marttra("เกตุ"), "กด") + self.assertEqual(kv.check_marttra("เหตุ"), "กด") + self.assertEqual(kv.check_marttra("ญาติ"), "กด") + self.assertEqual(kv.check_marttra("ธาตุ"), "กด") + self.assertEqual(kv.check_marttra("พยาธิ"), "กด") + self.assertEqual(kv.check_marttra("วัติ"), "กด") + self.assertEqual(kv.check_marttra("พรรดิ"), "กด") + self.assertEqual(kv.check_marttra("อต"), "กด") + self.assertEqual(kv.check_marttra("ยศ"), "กด") + self.assertEqual(kv.check_marttra("เร็จ"), "กด") + self.assertEqual(kv.check_marttra("เตลิด"), "กด") + self.assertEqual(kv.check_marttra("เพนียด"), "กด") + self.assertEqual(kv.check_marttra("กษัตริย์"), "กด") + self.assertEqual(kv.check_marttra("ศาสตร์"), "กด") + self.assertEqual(kv.check_marttra("เฮิรตซ์"), "กด") + self.assertEqual(kv.check_marttra("ฤทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กฤษ"), "กด") + self.assertEqual(kv.check_marttra("กฤษณ์"), "กด") + self.assertEqual(kv.check_marttra("ทฤษ"), "กด") self.assertEqual(kv.check_marttra("มึน"), "กน") self.assertEqual(kv.check_marttra("ร้าน"), "กน") @@ -71,6 +297,30 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เณร"), "กน") self.assertEqual(kv.check_marttra("ยนต์"), "กน") self.assertEqual(kv.check_marttra("กรรณ"), "กน") + self.assertEqual(kv.check_marttra("พาล"), "กน") + self.assertEqual(kv.check_marttra("พาน"), "กน") + self.assertEqual(kv.check_marttra("บิน"), "กน") + self.assertEqual(kv.check_marttra("ขึ้น"), "กน") + self.assertEqual(kv.check_marttra("รุฬห์"), "กน") + self.assertEqual(kv.check_marttra("บูรณ์"), "กน") + self.assertEqual(kv.check_marttra("กูณฑ์"), "กน") + self.assertEqual(kv.check_marttra("สูรย์"), "กน") + self.assertEqual(kv.check_marttra("เรียน"), "กน") + self.assertEqual(kv.check_marttra("อัน"), "กน") + self.assertEqual(kv.check_marttra("กัน"), "กน") + self.assertEqual(kv.check_marttra("สัญ"), "กน") + self.assertEqual(kv.check_marttra("คล"), "กน") + self.assertEqual(kv.check_marttra("พร"), "กน") + self.assertEqual(kv.check_marttra("วร"), "กน") + self.assertEqual(kv.check_marttra("บวร"), "กน") + self.assertEqual(kv.check_marttra("เป็น"), "กน") + self.assertEqual(kv.check_marttra("แกร็น"), "กน") + self.assertEqual(kv.check_marttra("เดิน"), "กน") + self.assertEqual(kv.check_marttra("เมรุ"), "กน") + self.assertEqual(kv.check_marttra("อล"), "กน") + self.assertEqual(kv.check_marttra("ควร"), "กน") + self.assertEqual(kv.check_marttra("จันทร์"), "กน") + self.assertEqual(kv.check_marttra("สินธุ์"), "กน") self.assertEqual(kv.check_marttra("ชอบ"), "กบ") self.assertEqual(kv.check_marttra("ภาพ"), "กบ") @@ -78,17 +328,110 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("รูป"), "กบ") self.assertEqual(kv.check_marttra("เวฟ"), "กบ") self.assertEqual(kv.check_marttra("โลพ"), "กบ") + self.assertEqual(kv.check_marttra("หยิบ"), "กบ") + self.assertEqual(kv.check_marttra("อุ๊ป"), "กบ") + self.assertEqual(kv.check_marttra("ธูป"), "กบ") + self.assertEqual(kv.check_marttra("กอล์ฟ"), "กบ") self.assertEqual(kv.check_marttra("ปลา"), "กา") self.assertEqual(kv.check_marttra("งู"), "กา") self.assertEqual(kv.check_marttra("หมู"), "กา") self.assertEqual(kv.check_marttra("มือ"), "กา") self.assertEqual(kv.check_marttra("ล้อ"), "กา") + self.assertEqual(kv.check_marttra("เมา"), "กา") + self.assertEqual(kv.check_marttra("เหล้า"), "กา") + self.assertEqual(kv.check_marttra("ฉะ"), "กา") + self.assertEqual(kv.check_marttra("ค่ะ"), "กา") + self.assertEqual(kv.check_marttra("กระ"), "กา") + self.assertEqual(kv.check_marttra("พลา"), "กา") + self.assertEqual(kv.check_marttra("ซ่า"), "กา") + self.assertEqual(kv.check_marttra("นิ"), "กา") + self.assertEqual(kv.check_marttra("ตริ"), "กา") + self.assertEqual(kv.check_marttra("ปี"), "กา") + self.assertEqual(kv.check_marttra("ปี่"), "กา") + self.assertEqual(kv.check_marttra("ฎี"), "กา") + self.assertEqual(kv.check_marttra("ตรี"), "กา") + self.assertEqual(kv.check_marttra("พลี"), "กา") + self.assertEqual(kv.check_marttra("อึ"), "กา") + self.assertEqual(kv.check_marttra("อือ"), "กา") + self.assertEqual(kv.check_marttra("ซื้อ"), "กา") + self.assertEqual(kv.check_marttra("ปรือ"), "กา") + self.assertEqual(kv.check_marttra("ธุ"), "กา") + self.assertEqual(kv.check_marttra("ญุ"), "กา") + self.assertEqual(kv.check_marttra("ถู"), "กา") + self.assertEqual(kv.check_marttra("หรู"), "กา") + self.assertEqual(kv.check_marttra("เซะ"), "กา") + self.assertEqual(kv.check_marttra("เอ"), "กา") + self.assertEqual(kv.check_marttra("แอะ"), "กา") + self.assertEqual(kv.check_marttra("และ"), "กา") + self.assertEqual(kv.check_marttra("แประ"), "กา") + self.assertEqual(kv.check_marttra("แอ๊ะ"), "กา") + self.assertEqual(kv.check_marttra("แอร์"), "กา") + self.assertEqual(kv.check_marttra("เกียร์"), "กา") + self.assertEqual(kv.check_marttra("เอือ"), "กา") + self.assertEqual(kv.check_marttra("เสือ"), "กา") + self.assertEqual(kv.check_marttra("เขือ"), "กา") + self.assertEqual(kv.check_marttra("กลัว"), "กา") + self.assertEqual(kv.check_marttra("ก็"), "กา") + self.assertEqual(kv.check_marttra("ออ"), "กา") + self.assertEqual(kv.check_marttra("ขอ"), "กา") + self.assertEqual(kv.check_marttra("งอ"), "กา") + self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") + self.assertEqual(kv.check_marttra("เหม่อ"), "กา") + self.assertEqual(kv.check_marttra("เกลือ"), "กา") + self.assertEqual(kv.check_marttra("ทรีย์"), "กา") + self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") + self.assertEqual(kv.check_marttra("นีย์"), "กา") + + # Fake Finals (คำควบกล้า, คำที่มีพยัญชนะ/สระไม่ออกเสียง) mapping to open syllables + self.assertEqual(kv.check_marttra("ไทย"), "กา") + self.assertEqual(kv.check_marttra("ไกล"), "กา") + self.assertEqual(kv.check_marttra("ใกล้"), "กา") + self.assertEqual(kv.check_marttra("เสีย"), "กา") + self.assertEqual(kv.check_marttra("เปล"), "กา") + self.assertEqual(kv.check_marttra("ไกว"), "กา") + self.assertEqual(kv.check_marttra("โปร"), "กา") + self.assertEqual(kv.check_marttra("โปล"), "กา") + self.assertEqual(kv.check_marttra("แปร"), "กา") + self.assertEqual(kv.check_marttra("ไฟล์"), "กา") + + # Standalone Characters mapping to open syllables + self.assertEqual(kv.check_marttra("ธ"), "กา") + self.assertEqual(kv.check_marttra("ณ"), "กา") + self.assertEqual(kv.check_marttra("พณ"), "กา") + self.assertEqual(kv.check_marttra("บ"), "กา") + self.assertEqual(kv.check_marttra("บ่"), "กา") + self.assertEqual(kv.check_marttra("อ"), "กา") + + # ฤ / ฦ + self.assertEqual(kv.check_marttra("ฤ"), "กา") + self.assertEqual(kv.check_marttra("ฦ"), "กา") + self.assertEqual(kv.check_marttra("ฤา"), "กา") + self.assertEqual(kv.check_marttra("ฤๅ"), "กา") + self.assertEqual(kv.check_marttra("ฦา"), "กา") + self.assertEqual(kv.check_marttra("ฦๅ"), "กา") def test_is_sumpus(self): self.assertTrue(kv.is_sumpus("สรร", "อัน")) self.assertFalse(kv.is_sumpus("สรร", "แมว")) + # Structural equivalence logic & Normalization + self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) + self.assertTrue(kv.is_sumpus("ทำ", "จำ")) + self.assertTrue(kv.is_sumpus("กรรม", "ธรรม")) + self.assertTrue(kv.is_sumpus("ธรรม", "สัม")) + self.assertTrue(kv.is_sumpus("ธรรม", "จำ")) + self.assertTrue(kv.is_sumpus("กัย", "ไก")) + self.assertTrue(kv.is_sumpus("กัย", "ไกล")) + self.assertTrue(kv.is_sumpus("ใจ", "ไทย")) + self.assertTrue(kv.is_sumpus("เลย", "เกย")) + self.assertTrue(kv.is_sumpus("พวก", "จวก")) + self.assertTrue(kv.is_sumpus("ฤทธิ์", "กิด")) + self.assertTrue(kv.is_sumpus("ใจ", "จัย")) + + # Verify strict phonemic constraints are maintained + self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ + def test_check_klon(self): self.assertEqual( kv.check_klon( @@ -156,6 +499,11 @@ def setUp(self): def test_word_without_karun_unchanged(self): self.assertEqual(self.kv.handle_karun_sound_silence("คน"), "คน") self.assertEqual(self.kv.handle_karun_sound_silence("กา"), "กา") + # internal karun unchanged + self.assertEqual(self.kv.handle_karun_sound_silence("การ์ตูน"), "การ์ตูน") + self.assertEqual(self.kv.handle_karun_sound_silence("กอล์ฟ"), "กอล์ฟ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฟิล์ม"), "ฟิล์ม") + self.assertEqual(self.kv.handle_karun_sound_silence("สตาร์ตอัป"), "สตาร์ตอัป") def test_word_ending_with_karun_stripped(self): # เกมส์ → drop ์ and the consonant before it (ส) → เกม @@ -165,9 +513,64 @@ def test_word_ending_with_karun_stripped_2(self): # รักษ์ → drop ์ + ษ → รัก self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") + def test_complex_karun_stripped(self): + # Explicit evaluation of single, multi-consonant, and vowel-embedded Karun rules + self.assertEqual(self.kv.handle_karun_sound_silence("จันทร์"), "จัน") + self.assertEqual(self.kv.handle_karun_sound_silence("สิทธิ์"), "สิท") + self.assertEqual(self.kv.handle_karun_sound_silence("กษัตริย์"), "กษัต") + self.assertEqual(self.kv.handle_karun_sound_silence("พระลักษมณ์"), "พระลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("อินทรีย์"), "อินทรี") + self.assertEqual(self.kv.handle_karun_sound_silence("ภาพยนตร์"), "ภาพยน") + self.assertEqual(self.kv.handle_karun_sound_silence("กาสาวพัสตร์"), "กาสาวพัส") + self.assertEqual(self.kv.handle_karun_sound_silence("ไปรษณีย์"), "ไปรษณี") + self.assertEqual(self.kv.handle_karun_sound_silence("สัปดาห์"), "สัปดา") + self.assertEqual(self.kv.handle_karun_sound_silence("เฮิรตซ์"), "เฮิรต") + self.assertEqual(self.kv.handle_karun_sound_silence("วิศวกรรมศาสตร์"), "วิศวกรรมศาส") + self.assertEqual(self.kv.handle_karun_sound_silence("กบินทร์"), "กบิน") + self.assertEqual(self.kv.handle_karun_sound_silence("นราธิเบนทร์"), "นราธิเบน") + self.assertEqual(self.kv.handle_karun_sound_silence("พรหมจรรย์"), "พรหมจรร") + self.assertEqual(self.kv.handle_karun_sound_silence("กรณีย์"), "กรณี") + self.assertEqual(self.kv.handle_karun_sound_silence("รังสิมันตุ์"), "รังสิมัน") + self.assertEqual(self.kv.handle_karun_sound_silence("รามเกียรติ์"), "รามเกียร") + self.assertEqual(self.kv.handle_karun_sound_silence("ทรลักษณ์"), "ทรลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("ธำมรงค์"), "ธำมรง") + self.assertEqual(self.kv.handle_karun_sound_silence("ศัพท์"), "ศัพ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฉันท์"), "ฉัน") + self.assertEqual(self.kv.handle_karun_sound_silence("เจ้าเล่ห์"), "เจ้าเล่") + self.assertEqual(self.kv.handle_karun_sound_silence("สงเคราะห์"), "สงเคราะ") + self.assertEqual(self.kv.handle_karun_sound_silence("ราชทัณฑ์"), "ราชทัณ") + self.assertEqual(self.kv.handle_karun_sound_silence("สวาสดิ์"), "สวาส") + self.assertEqual(self.kv.handle_karun_sound_silence("สุปรีดิ์"), "สุปรี") + def test_returns_string(self): self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) +class KhaveeIsTrueFinalTestCase(unittest.TestCase): + """Tests for internal method KhaveeVerifier._is_true_final""" + + def setUp(self): + self.kv = KhaveeVerifier() + + def test_true_finals(self): + self.assertTrue(self.kv._is_true_final("จัย")) + self.assertTrue(self.kv._is_true_final("สมัย")) + self.assertTrue(self.kv._is_true_final("เลื่อย")) + self.assertTrue(self.kv._is_true_final("เปื่อย")) + self.assertTrue(self.kv._is_true_final("เฉื่อย")) + self.assertTrue(self.kv._is_true_final("เหนื่อย")) + + def test_fake_finals(self): + self.assertFalse(self.kv._is_true_final("ไทย")) + self.assertFalse(self.kv._is_true_final("ใคร")) + self.assertFalse(self.kv._is_true_final("ไกล")) + self.assertFalse(self.kv._is_true_final("ใกล้")) + self.assertFalse(self.kv._is_true_final("เสีย")) + self.assertFalse(self.kv._is_true_final("ไกว")) + self.assertFalse(self.kv._is_true_final("โปร")) + self.assertFalse(self.kv._is_true_final("แปร")) + self.assertFalse(self.kv._is_true_final("เปล")) + self.assertFalse(self.kv._is_true_final("ไฟล์")) + class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_aek_too""" @@ -221,6 +624,63 @@ def test_check_klon8_correct_poem(self): ) self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) + def test_check_klon8_correct_poem_2(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_correct_poem_3(self): + poem = ( + "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " + "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " + "ลูกผู้ชายลายมือนั้นคือยศ เเจ้าจงอตส่าห์ทำสม่ำเสมียน " + "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_invalid_poem(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["In sentence 2, there are more than 10 words. ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", "Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) + + def test_check_klon8_invalid_poem_2(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) + + def test_check_klon8_invalid_poem_3(self): + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertIn(["Can't find rhyme between paragraphs ('เหมือน', 'เตือด') in paragraph 1"], result) + class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_sara""" @@ -250,8 +710,9 @@ def test_ru_sara(self): self.assertEqual(self.kv.check_sara("ฤ"), "อึ") def test_ruea_sara(self): - # ฤา (ฤ + sara aa U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa + # ฤา (ฤ + sara า U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") + self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") def test_เอือ_sara(self): self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") From a8ffb00bf0166793e4db1b6bfc6a78ad9c1673c6 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <144136630+Warit-Yuv@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:58:06 +0900 Subject: [PATCH 20/43] Fix typo in test case for check_sara function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit change ตริ to ตรี to match correctly with the สระ "อี" assertion in the test case. --- tests/core/test_khavee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 0caabed43..9d40dcd91 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -26,7 +26,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ยิ้ม"), "อิ") self.assertEqual(kv.check_sara("พิมพ์"), "อิ") self.assertEqual(kv.check_sara("หยิบ"), "อิ") - self.assertEqual(kv.check_sara("ตริ"), "อี") + self.assertEqual(kv.check_sara("ตรี"), "อี") self.assertEqual(kv.check_sara("ปี"), "อี") self.assertEqual(kv.check_sara("ปี่"), "อี") self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี From 114c23877e6507d3ffcdbe28ad04b4f3215bf550 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 11:38:07 +0900 Subject: [PATCH 21/43] =?UTF-8?q?Enhance=20check=5Fsara=20and=20=5Fis=5Ftr?= =?UTF-8?q?ue=5Ffinal=20methods=20for=20better=20vowel=20handling=20agains?= =?UTF-8?q?t=20"=E0=B8=A7"=20and=20add=20unit=20tests=20for=20edge=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pythainlp/khavee/core.py | 92 ++++++++++++++++++++++++++---------- tests/core/test_khavee.py | 99 ++++++++++++++++++++++++++------------- 2 files changed, 134 insertions(+), 57 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index b7dce2fc6..4b84a6125 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -40,6 +40,10 @@ def _is_true_final(self, word: str) -> bool: """ # Handle การันย์ word = self.handle_karun_sound_silence(word) + + # Store original word to distinguish tone-dependent structures + original_word = word + # Strip tone marks first so words like 'ใกล้' properly evaluate as ending in 'ล' word = remove_tonemark(word) @@ -47,10 +51,12 @@ def _is_true_final(self, word: str) -> bool: return False consonants = [c for c in word if c in thai_consonants] + if len(consonants) < 2: return False last_char = word[-1] + cluster = consonants[0] + consonants[1] # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) if last_char == "ย" and ("ไ" in word or "ใ" in word): @@ -62,7 +68,6 @@ def _is_true_final(self, word: str) -> bool: # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) if last_char in ["ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): if len(consonants) == 2: - cluster = consonants[0] + consonants[1] # Check for ล if last_char == "ล" and cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: # Exception 'เพล' - แม่กน (monk food ฉันเพล) @@ -74,8 +79,18 @@ def _is_true_final(self, word: str) -> bool: return False # Check for ว (ควบแท้ and อักษรนำ) - if last_char == "ว" and cluster in ["กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"]: - return False + if last_char == "ว": + # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) + if "ไ" in word or "ใ" in word: + if cluster in ["กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"]: + return False + + # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). Whitelist อักษรนำ/คำควบกล้ำ as exceptions + elif any(v in word for v in ["เ", "แ", "โ"]): + # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา + # เดินเขว, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่, ว้าเหว่ + if original_word in ["เขว", "แคว", "แหว", "โคว", "โหว", "โหว่", "เหว่"]: + return False # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) return True @@ -105,10 +120,18 @@ def check_sara(self, word: str) -> str: # In case of การันย์ word = self.handle_karun_sound_silence(word) + # Remove tonemarks for checking endings safely + word_req = remove_tonemark(word) + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ + silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"] + if any(word_req.endswith(ex) for ex in silent_vowel_exceptions): + word = word[:-1] + word_req = word_req[:-1] # In case of สระเดี่ยว for i in word: - if i == "ั" and "ว" in word: + if i == "ั" and word_req.endswith("ว"): sara.append("อัว") elif i in ("ะ", "ั"): sara.append("อะ") @@ -147,8 +170,10 @@ def check_sara(self, word: str) -> str: else: sara.append("อะ") - # Remove tonemarks for checking endings safely - word_req = remove_tonemark(word) + + # Clean up 'ออ' if 'อ' is acting purely as an initial consonant (อต, อด, อบ, อวบ) + if "ออ" in sara and len(sara) == 1 and word.startswith("อ") and countoa == 1: + sara.remove("ออ") # In case of ออ (Clean redundant ออ from compound vowels like คือ, มือ) if countoa == 1 and "อ" in word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: @@ -219,7 +244,10 @@ def check_sara(self, word: str) -> str: elif "ออ" in sara and len(sara) > 1: sara.remove("ออ") elif "ว" in word and len(sara) == 0: - sara.append("อัว") + if word_req in ["บวร", "วร"]: + sara.append("ออ") + else: + sara.append("อัว") # ควร, บวก, สวม if "ั" in word and self.check_marttra(word) == "กา": sara = [] @@ -239,6 +267,10 @@ def check_sara(self, word: str) -> str: elif word == "เอาะ": sara = ["เอาะ"] + # In case of เ-ือ + if "เ" in word and "ื" in word and "อ" in word: + sara = ["เอือ"] + # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(word_req): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) @@ -247,7 +279,7 @@ def check_sara(self, word: str) -> str: sara = ["เออ"] # In case of ฤ ฦ - if "ฤา" in original_word or "ฦา" in original_word: + if any(ex in original_word for ex in ("ฤา","ฤๅ","ฦา","ฦๅ")): sara = ["อือ"] elif "ฤ" in original_word or "ฦ" in original_word: sara = [] @@ -271,16 +303,16 @@ def check_sara(self, word: str) -> str: # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) sara.append("โอะ") - # In case of บ่ / บ - if word in ("บ่","บ"): - sara = ["ออ"] - - #"◌ํ" (nikkhahit) indicates a nasal sound, often associated with the 'อำ' sound in Thai. + # In case of นิกหิต (-ํ) + า (miss-typed of สระอำ) or standalone นิกหิต (-ํ) 'อัง' if "ํ" in word: - sara = ["อำ"] + if "ํา" in word: + sara = ["อำ"] # The strict decomposed 'อำ' typo + else: + sara = ["อะ"] # Standalone sounds like 'อัง' (อะ + ง) from pali/sanskrit - if "เ" in word and "ื" in word and "อ" in word: - sara = ["เอือ"] + # In case of บ่ / บ + if word_req == "บ": + sara = ["ออ"] # In case of isolated symbols as words (ลดรูป อะ) if word_req in ["ณ", "ธ", "อ","พณ"]: @@ -320,15 +352,24 @@ def check_marttra(self, word: str) -> str: word = self.handle_karun_sound_silence(word) word = remove_tonemark(word) - + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ + silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"] + if any(word.endswith(ex) for ex in silent_vowel_exceptions): + word = word[:-1] + # Check for อักษรตัวเดียวแทนคำ Standalone words - if word in ["บ", "ณ", "ธ", "พณ"]: + if word in ["บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"]: return "กา" - # Check for ำ at the end (represents "am" sound, ends with m) - if word[-1] == "ำ": + # Check for ำ or นิคหิต (-ํ) + า + if word[-1] == "ำ" or word.endswith("ํา"): return "กม" + # Check for standalone นิคหิต (-ํ) 'อัง' + if word.endswith("ํ"): + return "กง" + # Check for ไ/ใ if "ไ" in word or "ใ" in word: if word[-1] not in ["ย", "ล", "ร", "ว"]: @@ -340,11 +381,11 @@ def check_marttra(self, word: str) -> str: if word[-1] in ["ย", "ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ"]): if not self._is_true_final(word): return "กา" - - if "ํ" in word and "า" in word: - return "กา" - elif ( - word[-1] in ["า", "ะ", "ิ", "ี", "ุ", "ู", "อ"] + + # Check for ตัวสะกด final consonants + # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) + if ( + word[-1] in ["า", "ๅ", "ะ", "ิ", "ี", "ุ", "ู", "อ"] or ("ี" in word and "ย" in word[-1]) or ("ื" in word and "อ" in word[-1]) ): @@ -786,3 +827,4 @@ def handle_karun_sound_silence(self, word: str) -> str: return word[:-3] else: return word[:-2] + \ No newline at end of file diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 9d40dcd91..6a1d124a5 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -29,7 +29,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ตรี"), "อี") self.assertEqual(kv.check_sara("ปี"), "อี") self.assertEqual(kv.check_sara("ปี่"), "อี") - self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี + self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี self.assertEqual(kv.check_sara("ตรี"), "อี") self.assertEqual(kv.check_sara("พลี"), "อี") self.assertEqual(kv.check_sara("นีย์"), "อี") @@ -76,7 +76,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("เสือ"), "เอือ") self.assertEqual(kv.check_sara("เขือ"), "เอือ") self.assertEqual(kv.check_sara("กลัว"), "อัว") - + # Reduced and Transformed Vowels (สระลดรูป/เปลี่ยนรูป) self.assertEqual(kv.check_sara("อัน"), "อะ") self.assertEqual(kv.check_sara("กัน"), "อะ") @@ -101,9 +101,9 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ล็อก"), "เอาะ") self.assertEqual(kv.check_sara("อ็อก"), "เอาะ") self.assertEqual(kv.check_sara("ก็"), "เอาะ") - + # Complex compound and hidden vowels - self.assertEqual(kv.check_sara("ภูมิ"), "อู") # ภูมิใจ (ไม่ใช่ ภู-มิ) + self.assertEqual(kv.check_sara("ภูมิ"), "อู") # ภูมิใจ (ไม่ใช่ ภู-มิ) self.assertEqual(kv.check_sara("เกียรติ"), "เอีย") self.assertEqual(kv.check_sara("เกตุ"), "เอ") self.assertEqual(kv.check_sara("เมรุ"), "เอ") @@ -112,9 +112,9 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ญาติ"), "อา") self.assertEqual(kv.check_sara("ธาตุ"), "อา") self.assertEqual(kv.check_sara("พยาธิ"), "อา") - self.assertEqual(kv.check_sara("วัติ"), "อะ") # ประวัติ - self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ - self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ + self.assertEqual(kv.check_sara("วัติ"), "อะ") # ประวัติ + self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ + self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ self.assertEqual(kv.check_sara("สมมุติ"), "อุ") self.assertEqual(kv.check_sara("ชาติ"), "อา") self.assertEqual(kv.check_sara("ชาติ"), "อา") @@ -166,7 +166,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ฤทธิ์"), "อิ") self.assertEqual(kv.check_sara("กฤษ"), "อิ") self.assertEqual(kv.check_sara("กฤษณ์"), "อิ") - self.assertEqual(kv.check_sara("ทฤษ"), "อิ") # ทฤษฎี + self.assertEqual(kv.check_sara("ทฤษ"), "อิ") # ทฤษฎี self.assertEqual(kv.check_sara("ฤกษ์"), "เออ") self.assertEqual(kv.check_sara("พฤษ"), "อึ") self.assertEqual(kv.check_sara("พฤติ"), "อึ") @@ -205,7 +205,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ภูมิ"), "กม") self.assertEqual(kv.check_marttra("กรม"), "กม") self.assertEqual(kv.check_marttra("ธรรม"), "กม") - self.assertEqual(kv.check_marttra("จำ"), "กม") # สระ อำ ถือเป็นแม่กม + self.assertEqual(kv.check_marttra("จำ"), "กม") # สระ อำ ถือเป็นแม่กม self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") @@ -333,6 +333,34 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ธูป"), "กบ") self.assertEqual(kv.check_marttra("กอล์ฟ"), "กบ") + self.assertEqual(kv.check_marttra("อะ"), "กา") + self.assertEqual(kv.check_marttra("อา"), "กา") + self.assertEqual(kv.check_marttra("อิ"), "กา") + self.assertEqual(kv.check_marttra("อี"), "กา") + self.assertEqual(kv.check_marttra("อึ"), "กา") + self.assertEqual(kv.check_marttra("อือ"), "กา") + self.assertEqual(kv.check_marttra("อุ"), "กา") + self.assertEqual(kv.check_marttra("อู"), "กา") + self.assertEqual(kv.check_marttra("เอะ"), "กา") + self.assertEqual(kv.check_marttra("เอ"), "กา") + self.assertEqual(kv.check_marttra("แอะ"), "กา") + self.assertEqual(kv.check_marttra("แอ"), "กา") + self.assertEqual(kv.check_marttra("โอะ"), "กา") + self.assertEqual(kv.check_marttra("โอ"), "กา") + self.assertEqual(kv.check_marttra("เอาะ"), "กา") + self.assertEqual(kv.check_marttra("ออ"), "กา") + self.assertEqual(kv.check_marttra("เอาะ"), "กา") + self.assertEqual(kv.check_marttra("เออ"), "กา") + self.assertEqual(kv.check_marttra("เอียะ"), "กา") + self.assertEqual(kv.check_marttra("เอีย"), "กา") + self.assertEqual(kv.check_marttra("เอือะ"), "กา") + self.assertEqual(kv.check_marttra("เอือ"), "กา") + self.assertEqual(kv.check_marttra("อัวะ"), "กา") + self.assertEqual(kv.check_marttra("อัว"), "กา") + self.assertEqual(kv.check_marttra("อำ"), "กา") + self.assertEqual(kv.check_marttra("ไอ"), "กา") + self.assertEqual(kv.check_marttra("ใอ"), "กา") + self.assertEqual(kv.check_marttra("เอา"), "กา") self.assertEqual(kv.check_marttra("ปลา"), "กา") self.assertEqual(kv.check_marttra("งู"), "กา") self.assertEqual(kv.check_marttra("หมู"), "กา") @@ -352,8 +380,6 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ฎี"), "กา") self.assertEqual(kv.check_marttra("ตรี"), "กา") self.assertEqual(kv.check_marttra("พลี"), "กา") - self.assertEqual(kv.check_marttra("อึ"), "กา") - self.assertEqual(kv.check_marttra("อือ"), "กา") self.assertEqual(kv.check_marttra("ซื้อ"), "กา") self.assertEqual(kv.check_marttra("ปรือ"), "กา") self.assertEqual(kv.check_marttra("ธุ"), "กา") @@ -373,7 +399,6 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เขือ"), "กา") self.assertEqual(kv.check_marttra("กลัว"), "กา") self.assertEqual(kv.check_marttra("ก็"), "กา") - self.assertEqual(kv.check_marttra("ออ"), "กา") self.assertEqual(kv.check_marttra("ขอ"), "กา") self.assertEqual(kv.check_marttra("งอ"), "กา") self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") @@ -382,7 +407,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ทรีย์"), "กา") self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") self.assertEqual(kv.check_marttra("นีย์"), "กา") - + # Fake Finals (คำควบกล้า, คำที่มีพยัญชนะ/สระไม่ออกเสียง) mapping to open syllables self.assertEqual(kv.check_marttra("ไทย"), "กา") self.assertEqual(kv.check_marttra("ไกล"), "กา") @@ -402,7 +427,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("บ"), "กา") self.assertEqual(kv.check_marttra("บ่"), "กา") self.assertEqual(kv.check_marttra("อ"), "กา") - + # ฤ / ฦ self.assertEqual(kv.check_marttra("ฤ"), "กา") self.assertEqual(kv.check_marttra("ฦ"), "กา") @@ -545,6 +570,7 @@ def test_complex_karun_stripped(self): def test_returns_string(self): self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) + class KhaveeIsTrueFinalTestCase(unittest.TestCase): """Tests for internal method KhaveeVerifier._is_true_final""" @@ -632,7 +658,7 @@ def test_check_klon8_correct_poem_2(self): "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" ) self.assertEqual( - self.kv.check_klon(poem, k_type=8), + self.kv.check_klon(poem, k_type=8), "The poem is correct according to the principle." ) @@ -644,42 +670,51 @@ def test_check_klon8_correct_poem_3(self): "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" ) self.assertEqual( - self.kv.check_klon(poem, k_type=8), + self.kv.check_klon(poem, k_type=8), "The poem is correct according to the principle." ) def test_check_klon8_invalid_poem(self): poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" ) result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) - self.assertIn(["In sentence 2, there are more than 10 words. ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", "Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) + self.assertIn( + "In sentence 2, there are more than 10 words. ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", + result, + ) def test_check_klon8_invalid_poem_2(self): poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" ) result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) - self.assertIn(["Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1"], result) - + self.assertIn( + "Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1", + result, + ) + def test_check_klon8_invalid_poem_3(self): poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" ) result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) - self.assertIn(["Can't find rhyme between paragraphs ('เหมือน', 'เตือด') in paragraph 1"], result) + self.assertIn( + "Can't find rhyme between paragraphs ('เหมือน', 'เตือด') in paragraph 1", + result, + ) class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): From f0586be4bef32ea9ef7d3e66d34570e4a70fb34a Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 13:15:59 +0900 Subject: [PATCH 22/43] =?UTF-8?q?fix=20missclassified=20when=20=E0=B8=A4?= =?UTF-8?q?=E0=B8=A6=20in=20front=20of=20word=20and=20fix=20=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B8=B0=E0=B8=AD=E0=B8=B3=20missclassified=20as=20?= =?UTF-8?q?=E0=B9=81=E0=B8=A1=E0=B9=88=E0=B8=81=E0=B8=A1=20and=20fix=20che?= =?UTF-8?q?ck=20sara=20with=20=E0=B8=AA=E0=B8=A3=E0=B8=B0=E0=B8=AD?= =?UTF-8?q?=E0=B8=B1=E0=B8=A7=20(=E0=B8=95=E0=B8=B1=E0=B8=A7=20=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=B1=E0=B8=A7=20=E0=B8=9A=E0=B8=B1=E0=B8=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pythainlp/khavee/core.py | 23 ++++++++++++++--------- tests/core/test_khavee.py | 5 +++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 0cc093030..bff5bb5ef 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -50,7 +50,8 @@ def _is_true_final(self, word: str) -> bool: if len(word) < 2: return False - consonants = [c for c in word if c in thai_consonants] + # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants + consonants = [c for c in word if c in thai_consonants + "ฤฦ"] if len(consonants) < 2: return False @@ -251,8 +252,8 @@ def check_sara(self, word: str) -> str: sara.append("อัว") # ควร, บวก, สวม if "ั" in word and self.check_marttra(word) == "กา": - sara = [] - sara.append("ไอ") + if "อัว" not in sara: + sara = ["ไอ"] # In case of อ if word == "เออะ": @@ -309,7 +310,8 @@ def check_sara(self, word: str) -> str: if "ํา" in word: sara = ["อำ"] # The strict decomposed 'อำ' typo else: - sara = ["อะ"] # Standalone sounds like 'อัง' (อะ + ง) from pali/sanskrit + # Standalone sounds like 'อัง' (อะ + ง) from pali/sanskrit + sara = ["อะ"] # In case of บ่ / บ if word_req == "บ": @@ -366,14 +368,15 @@ def check_marttra(self, word: str) -> str: # Check for ำ or นิคหิต (-ํ) + า if word[-1] == "ำ" or word.endswith("ํา"): - return "กม" + return "กา" # Check for standalone นิคหิต (-ํ) 'อัง' if word.endswith("ํ"): return "กง" # Any word with exactly 1 consonant (and not ending in ำ/ํ) cannot have a final consonant and therefore must be "กา" - consonants = [c for c in word if c in thai_consonants] + # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants + consonants = [c for c in word if c in thai_consonants + "ฤฦ"] if len(consonants) == 1: return "กา" @@ -393,8 +396,9 @@ def check_marttra(self, word: str) -> str: # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) if ( word[-1] in ["า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"] - or ("ี" in word and "ย" in word[-1]) - or ("ื" in word and "อ" in word[-1]) + or ("ี" in word and "ย" in word[-1]) # Catch สระเอีย (เสีย, เมีย) + or ("ื" in word and "อ" in word[-1]) # Catch สระอือ (เรือ, เสือ) + or ("ั" in word and "ว" in word[-1]) # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) ): return "กา" elif word[-1] in ["ง"]: @@ -776,7 +780,8 @@ def check_aek_too( >>> # -> [False, 'aek', 'too'] """ if isinstance(text, list): - return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] # type: ignore[misc] + # type: ignore[misc] + return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] if not isinstance(text, str): raise TypeError("text must be str or iterable list[str]") diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 6a1d124a5..a8d696745 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -387,8 +387,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ถู"), "กา") self.assertEqual(kv.check_marttra("หรู"), "กา") self.assertEqual(kv.check_marttra("เซะ"), "กา") - self.assertEqual(kv.check_marttra("เอ"), "กา") - self.assertEqual(kv.check_marttra("แอะ"), "กา") + self.assertEqual(kv.check_marttra("เฉ"), "กา") self.assertEqual(kv.check_marttra("และ"), "กา") self.assertEqual(kv.check_marttra("แประ"), "กา") self.assertEqual(kv.check_marttra("แอ๊ะ"), "กา") @@ -404,6 +403,8 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") self.assertEqual(kv.check_marttra("เหม่อ"), "กา") self.assertEqual(kv.check_marttra("เกลือ"), "กา") + self.assertEqual(kv.check_marttra("ตัว"), "กา") + self.assertEqual(kv.check_marttra("ครัว"), "กา") self.assertEqual(kv.check_marttra("ทรีย์"), "กา") self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") self.assertEqual(kv.check_marttra("นีย์"), "กา") From da79cf541bfdb844538b70d2b04ea19091496f1d Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:13:29 +0900 Subject: [PATCH 23/43] =?UTF-8?q?Fix=20test=20case=20so=20=E0=B8=82?= =?UTF-8?q?=E0=B8=B3=20=E0=B8=88=E0=B8=B3=20(=E0=B8=AD=E0=B8=B3)=20is=20co?= =?UTF-8?q?rrectly=20classified=20as=20=E0=B9=81=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=81=20=E0=B8=81=E0=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/core/test_khavee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index a8d696745..778f32487 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -198,14 +198,12 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("จาม"), "กม") self.assertEqual(kv.check_marttra("ยิ้ม"), "กม") self.assertEqual(kv.check_marttra("เกม"), "กม") - self.assertEqual(kv.check_marttra("ขำ"), "กม") self.assertEqual(kv.check_marttra("รมย์"), "กม") self.assertEqual(kv.check_marttra("พิมพ์"), "กม") self.assertEqual(kv.check_marttra("เขษม"), "กม") self.assertEqual(kv.check_marttra("ภูมิ"), "กม") self.assertEqual(kv.check_marttra("กรม"), "กม") self.assertEqual(kv.check_marttra("ธรรม"), "กม") - self.assertEqual(kv.check_marttra("จำ"), "กม") # สระ อำ ถือเป็นแม่กม self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") @@ -400,6 +398,8 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ก็"), "กา") self.assertEqual(kv.check_marttra("ขอ"), "กา") self.assertEqual(kv.check_marttra("งอ"), "กา") + self.assertEqual(kv.check_marttra("ขำ"), "กา") + self.assertEqual(kv.check_marttra("จำ"), "กา") self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") self.assertEqual(kv.check_marttra("เหม่อ"), "กา") self.assertEqual(kv.check_marttra("เกลือ"), "กา") From afb03a21db67b26c168bc437324ccedcb415ef06 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:15:40 +0900 Subject: [PATCH 24/43] =?UTF-8?q?Add=20more=20test=20cases=20to=20is=5Fsum?= =?UTF-8?q?pus=20=20for=20better=20coverage=20and=20accuracy=20when=20deal?= =?UTF-8?q?ing=20with=20=E0=B8=AD=E0=B8=B1=E0=B8=81=E0=B8=A9=E0=B8=A3?= =?UTF-8?q?=E0=B8=99=E0=B8=B3,=20=E0=B8=84=E0=B8=B3=E0=B8=84=E0=B8=A7?= =?UTF-8?q?=E0=B8=9A=E0=B8=81=E0=B8=A5=E0=B9=89=E0=B8=B3,=20=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B8=B0=E0=B8=AD=E0=B8=B3,=20and=20many=20sound=20of?= =?UTF-8?q?=20=E0=B8=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/core/test_khavee.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 778f32487..10d09f3ff 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -438,22 +438,55 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ฦๅ"), "กา") def test_is_sumpus(self): - self.assertTrue(kv.is_sumpus("สรร", "อัน")) self.assertFalse(kv.is_sumpus("สรร", "แมว")) + self.assertFalse(kv.is_sumpus("กลัว", "ไกล")) + self.assertFalse(kv.is_sumpus("ตัว", "ตะ")) + self.assertFalse(kv.is_sumpus("ตัว", "ไต")) + self.assertFalse(kv.is_sumpus("สาว", "อา")) + self.assertFalse(kv.is_sumpus("เอว", "อา")) + self.assertFalse(kv.is_sumpus("อัว", "อา")) + self.assertFalse(kv.is_sumpus("บวก", "อัว")) + self.assertFalse(kv.is_sumpus("สวม", "อัว")) + self.assertFalse(kv.is_sumpus("ชัวร์", "ชัน")) + self.assertFalse(kv.is_sumpus("เลว", "เร็ว")) + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ฤกษ์")) # ฤทธิ์ = ริด, ฤกษ์ = เริก + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ลึด")) # ฤทธิ์ = ริด != ลึด + self.assertFalse(kv.is_sumpus("ฤกษ์", "ลึก")) # ฤกษ์ = เริก != ลึก + self.assertFalse(kv.is_sumpus("โหว่", "โถ่ว")) # แม่ก กา vs แม่เกอว + + self.assertTrue(kv.is_sumpus("เขว", "เอ")) + self.assertTrue(kv.is_sumpus("เขว", "เหว่")) + self.assertTrue(kv.is_sumpus("เหว", "เอว")) + self.assertTrue(kv.is_sumpus("โหว่", "โถ")) + self.assertTrue(kv.is_sumpus("ครัว", "ตัว")) + self.assertTrue(kv.is_sumpus("สรร", "อัน")) + self.assertTrue(kv.is_sumpus("ธ", "ณ")) + self.assertTrue(kv.is_sumpus("ธ", "ทะ")) + self.assertTrue(kv.is_sumpus("ศาสตร์", "มารถ")) + self.assertTrue(kv.is_sumpus("แตร", "แปร")) # แม่ก กา + self.assertTrue(kv.is_sumpus("แหล่", "แต่")) # เหลือแหล่ + self.assertTrue(kv.is_sumpus("แหน", "แกน")) # แม่ กน # Structural equivalence logic & Normalization self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) self.assertTrue(kv.is_sumpus("ทำ", "จำ")) + self.assertTrue(kv.is_sumpus("ทำ", "กัม")) self.assertTrue(kv.is_sumpus("กรรม", "ธรรม")) self.assertTrue(kv.is_sumpus("ธรรม", "สัม")) self.assertTrue(kv.is_sumpus("ธรรม", "จำ")) self.assertTrue(kv.is_sumpus("กัย", "ไก")) self.assertTrue(kv.is_sumpus("กัย", "ไกล")) self.assertTrue(kv.is_sumpus("ใจ", "ไทย")) + self.assertTrue(kv.is_sumpus("ใจ", "จัย")) + self.assertTrue(kv.is_sumpus("ไกว", "ใด")) + self.assertTrue(kv.is_sumpus("ไกว", "ใคร")) self.assertTrue(kv.is_sumpus("เลย", "เกย")) self.assertTrue(kv.is_sumpus("พวก", "จวก")) self.assertTrue(kv.is_sumpus("ฤทธิ์", "กิด")) - self.assertTrue(kv.is_sumpus("ใจ", "จัย")) + self.assertTrue(kv.is_sumpus("ฤกษ์", "เริก")) + self.assertTrue(kv.is_sumpus("พฤษ", "พรึด")) + self.assertTrue(kv.is_sumpus("พฤก", "พรึก")) + self.assertTrue(kv.is_sumpus("ฤ", "รึ")) # Verify strict phonemic constraints are maintained self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ From 34abf2e03e2ea0bf8724ff67487434f277b61039 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:17:23 +0900 Subject: [PATCH 25/43] Correct the parsing of original_word into _is_true_final. As true final require word before tonemark get stripped to evaluate some tone-dependent structures. --- pythainlp/khavee/core.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index bff5bb5ef..58f0c72ba 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -90,8 +90,8 @@ def _is_true_final(self, word: str) -> bool: # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). Whitelist อักษรนำ/คำควบกล้ำ as exceptions elif any(v in word for v in ["เ", "แ", "โ"]): # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา - # เดินเขว, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่, ว้าเหว่ - if original_word in ["เขว", "แคว", "แหว", "โคว", "โหว", "โหว่", "เหว่"]: + # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ + if original_word in ["เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"]: return False # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) @@ -120,7 +120,7 @@ def check_sara(self, word: str) -> str: # Store original word to safely evaluate exceptions (like ฤทธิ์) after Karun stripping original_word = word - # In case of การันย์ + # In case of การันย์ (word stripped of Karun characters) word = self.handle_karun_sound_silence(word) # Remove tonemarks for checking endings safely word_req = remove_tonemark(word) @@ -274,7 +274,7 @@ def check_sara(self, word: str) -> str: sara = ["เอือ"] # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย - if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(word_req): + if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(original_word): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) other_vowels = [v for v in sara if v not in ["เอ", "ออ"]] if not other_vowels: @@ -354,6 +354,10 @@ def check_marttra(self, word: str) -> str: word = word[:-1] word = self.handle_karun_sound_silence(word) + + # is_true_final process requires the original word to check for exceptions in อักษรนำ/คำควบกล้ำ + original_word = word + word = remove_tonemark(word) # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ @@ -384,12 +388,13 @@ def check_marttra(self, word: str) -> str: if "ไ" in word or "ใ" in word: if word[-1] not in ["ย", "ล", "ร", "ว"]: return "กา" - elif not self._is_true_final(word): + elif not self._is_true_final(original_word): return "กา" # Check for เ, แ, โ + ย, ร, ล, ว (คำควบกล้ำ / อักษรนำ) if word[-1] in ["ย", "ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ"]): - if not self._is_true_final(word): + # ไม่ใช่ตัวสะกดแท้ -> แม่ก กา + if not self._is_true_final(original_word): return "กา" # Check for ตัวสะกด final consonants From ea5614c21b50ad27bb0536e5408bbf12adbfc5f4 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:40:26 +0900 Subject: [PATCH 26/43] Refactor comments in test cases for clarity and consistency --- pythainlp/khavee/core.py | 2 +- tests/core/test_khavee.py | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 58f0c72ba..0816cdfa1 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -354,7 +354,7 @@ def check_marttra(self, word: str) -> str: word = word[:-1] word = self.handle_karun_sound_silence(word) - + # is_true_final process requires the original word to check for exceptions in อักษรนำ/คำควบกล้ำ original_word = word diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 10d09f3ff..7a0e9552b 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -521,8 +521,10 @@ def test_check_aek_too(self): ) -class KhaveeCheckKaruLahuTestCase(unittest.TestCase): - """Tests for KhaveeVerifier.check_karu_lahu""" +class KhaveeCheckKaruLahuTestCase(unittest.TestCase): + """ + Tests for KhaveeVerifier.check_karu_lahu. + """ def setUp(self): self.kv = KhaveeVerifier() @@ -550,7 +552,9 @@ def test_ko_mai_is_lahu(self): class KhaveeHandleKarunTestCase(unittest.TestCase): - """Tests for KhaveeVerifier.handle_karun_sound_silence""" + """ + Tests for KhaveeVerifier.handle_karun_sound_silence. + """ def setUp(self): self.kv = KhaveeVerifier() @@ -606,7 +610,9 @@ def test_returns_string(self): class KhaveeIsTrueFinalTestCase(unittest.TestCase): - """Tests for internal method KhaveeVerifier._is_true_final""" + """ + Tests for internal method KhaveeVerifier._is_true_final. + """ def setUp(self): self.kv = KhaveeVerifier() @@ -633,7 +639,9 @@ def test_fake_finals(self): class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): - """Edge-case tests for KhaveeVerifier.check_aek_too""" + """ + Edge-case tests for KhaveeVerifier.check_aek_too + """ def setUp(self): self.kv = KhaveeVerifier() @@ -658,7 +666,9 @@ def test_both_tone_marks_returns_false(self): class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): - """Tests for check_klon k_type=8 and invalid k_type""" + """ + Tests for check_klon k_type=8 and invalid k_type + """ def setUp(self): self.kv = KhaveeVerifier() From 53c82a0b51f4f47ddb400e25302a189109b18d9e Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:48:00 +0900 Subject: [PATCH 27/43] Format the ocde to remove trailing white space. --- pythainlp/khavee/core.py | 2 +- tests/core/test_khavee.py | 24 +++++++----------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 0816cdfa1..778156fde 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -357,7 +357,7 @@ def check_marttra(self, word: str) -> str: # is_true_final process requires the original word to check for exceptions in อักษรนำ/คำควบกล้ำ original_word = word - + word = remove_tonemark(word) # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 7a0e9552b..bb325f7be 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -521,10 +521,8 @@ def test_check_aek_too(self): ) -class KhaveeCheckKaruLahuTestCase(unittest.TestCase): - """ - Tests for KhaveeVerifier.check_karu_lahu. - """ +class KhaveeCheckKaruLahuTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.check_karu_lahu.""" def setUp(self): self.kv = KhaveeVerifier() @@ -552,9 +550,7 @@ def test_ko_mai_is_lahu(self): class KhaveeHandleKarunTestCase(unittest.TestCase): - """ - Tests for KhaveeVerifier.handle_karun_sound_silence. - """ + """Tests for KhaveeVerifier.handle_karun_sound_silence.""" def setUp(self): self.kv = KhaveeVerifier() @@ -610,9 +606,7 @@ def test_returns_string(self): class KhaveeIsTrueFinalTestCase(unittest.TestCase): - """ - Tests for internal method KhaveeVerifier._is_true_final. - """ + """Tests for internal method KhaveeVerifier._is_true_final.""" def setUp(self): self.kv = KhaveeVerifier() @@ -639,9 +633,7 @@ def test_fake_finals(self): class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): - """ - Edge-case tests for KhaveeVerifier.check_aek_too - """ + """Edge-case tests for KhaveeVerifier.check_aek_too.""" def setUp(self): self.kv = KhaveeVerifier() @@ -666,9 +658,7 @@ def test_both_tone_marks_returns_false(self): class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): - """ - Tests for check_klon k_type=8 and invalid k_type - """ + """Tests for check_klon k_type=8 and invalid k_type.""" def setUp(self): self.kv = KhaveeVerifier() @@ -762,7 +752,7 @@ def test_check_klon8_invalid_poem_3(self): class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): - """Edge-case tests for KhaveeVerifier.check_sara""" + """Edge-case tests for KhaveeVerifier.check_sara.""" def setUp(self): self.kv = KhaveeVerifier() From c64738f58e8210492d004323238ba6b66e230675 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 14:57:21 +0900 Subject: [PATCH 28/43] Enhance test case documentation for KhaveeVerifier methods with detailed descriptions --- tests/core/test_khavee.py | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index bb325f7be..b11276a53 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -10,7 +10,10 @@ class KhaveeTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.check_sara, check_marttra, is_sumpus, check_klon, and check_aek_too methods.""" + def test_check_sara(self): + """Test check_sara with basic, reduced, complex, embedded, and standalone character vowels.""" # Basic Vowels self.assertEqual(kv.check_sara("ฉะ"), "อะ") self.assertEqual(kv.check_sara("ค่ะ"), "อะ") @@ -177,6 +180,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ฦๅ"), "อือ") def test_check_marttra(self): + """Test check_marttra for various final consonant patterns.""" self.assertEqual(kv.check_marttra("ปลิง"), "กง") self.assertEqual(kv.check_marttra("ยูง"), "กง") self.assertEqual(kv.check_marttra("กล่อง"), "กง") @@ -438,6 +442,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ฦๅ"), "กา") def test_is_sumpus(self): + """Test is_sumpus for checking structural equivalence of Thai words.""" self.assertFalse(kv.is_sumpus("สรร", "แมว")) self.assertFalse(kv.is_sumpus("กลัว", "ไกล")) self.assertFalse(kv.is_sumpus("ตัว", "ตะ")) @@ -492,6 +497,7 @@ def test_is_sumpus(self): self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ def test_check_klon(self): + """Test check_klon for Thai poem verification (k_type=4).""" self.assertEqual( kv.check_klon( "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง \ @@ -513,6 +519,7 @@ def test_check_klon(self): ) def test_check_aek_too(self): + """Test check_aek_too for Thai tone mark detection.""" self.assertFalse(kv.check_aek_too("ไกด์")) self.assertEqual(kv.check_aek_too("ไก่"), "aek") self.assertEqual(kv.check_aek_too("ไก้"), "too") @@ -525,27 +532,35 @@ class KhaveeCheckKaruLahuTestCase(unittest.TestCase): """Tests for KhaveeVerifier.check_karu_lahu.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_dead_syllable_is_karu(self): + """Test that dead syllables are identified as karu.""" self.assertEqual(self.kv.check_karu_lahu("กด"), "karu") def test_long_live_syllable_is_karu(self): + """Test that long live syllables are identified as karu.""" self.assertEqual(self.kv.check_karu_lahu("กา"), "karu") def test_live_syllable_with_final_consonant_is_karu(self): + """Test that live syllables with final consonants are identified as karu.""" self.assertEqual(self.kv.check_karu_lahu("กาน"), "karu") def test_bo_mai_ek_is_lahu(self): + """Test that bo mai ek is identified as lahu.""" self.assertEqual(self.kv.check_karu_lahu("บ่"), "lahu") def test_no_short_word_is_lahu(self): + """Test that standalone consonant without vowel is identified as lahu.""" self.assertEqual(self.kv.check_karu_lahu("ณ"), "lahu") def test_tho_short_word_is_lahu(self): + """Test that tho short word is identified as lahu.""" self.assertEqual(self.kv.check_karu_lahu("ธ"), "lahu") def test_ko_mai_is_lahu(self): + """Test that ko mai (killed consonant marker) is identified as lahu.""" self.assertEqual(self.kv.check_karu_lahu("ก็"), "lahu") @@ -553,9 +568,11 @@ class KhaveeHandleKarunTestCase(unittest.TestCase): """Tests for KhaveeVerifier.handle_karun_sound_silence.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_word_without_karun_unchanged(self): + """Test that words without karun are unchanged.""" self.assertEqual(self.kv.handle_karun_sound_silence("คน"), "คน") self.assertEqual(self.kv.handle_karun_sound_silence("กา"), "กา") # internal karun unchanged @@ -565,14 +582,17 @@ def test_word_without_karun_unchanged(self): self.assertEqual(self.kv.handle_karun_sound_silence("สตาร์ตอัป"), "สตาร์ตอัป") def test_word_ending_with_karun_stripped(self): + """Test that karun and preceding consonant are stripped from end of word.""" # เกมส์ → drop ์ and the consonant before it (ส) → เกม self.assertEqual(self.kv.handle_karun_sound_silence("เกมส์"), "เกม") def test_word_ending_with_karun_stripped_2(self): + """Test karun stripping with different consonant.""" # รักษ์ → drop ์ + ษ → รัก self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") def test_complex_karun_stripped(self): + """Test complex karun stripping with single, multi-consonant, and vowel-embedded patterns.""" # Explicit evaluation of single, multi-consonant, and vowel-embedded Karun rules self.assertEqual(self.kv.handle_karun_sound_silence("จันทร์"), "จัน") self.assertEqual(self.kv.handle_karun_sound_silence("สิทธิ์"), "สิท") @@ -602,6 +622,7 @@ def test_complex_karun_stripped(self): self.assertEqual(self.kv.handle_karun_sound_silence("สุปรีดิ์"), "สุปรี") def test_returns_string(self): + """Test that handle_karun_sound_silence returns a string.""" self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) @@ -609,9 +630,11 @@ class KhaveeIsTrueFinalTestCase(unittest.TestCase): """Tests for internal method KhaveeVerifier._is_true_final.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_true_finals(self): + """Test identification of true final consonant patterns.""" self.assertTrue(self.kv._is_true_final("จัย")) self.assertTrue(self.kv._is_true_final("สมัย")) self.assertTrue(self.kv._is_true_final("เลื่อย")) @@ -620,6 +643,7 @@ def test_true_finals(self): self.assertTrue(self.kv._is_true_final("เหนื่อย")) def test_fake_finals(self): + """Test identification of fake final consonant patterns.""" self.assertFalse(self.kv._is_true_final("ไทย")) self.assertFalse(self.kv._is_true_final("ใคร")) self.assertFalse(self.kv._is_true_final("ไกล")) @@ -636,23 +660,29 @@ class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_aek_too.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_non_string_raises_type_error(self): + """Test that non-string input raises TypeError.""" with self.assertRaises(TypeError): self.kv.check_aek_too(123) # type: ignore[arg-type] def test_dead_syllable_as_aek_flag(self): + """Test dead_syllable_as_aek flag behavior.""" self.assertEqual(self.kv.check_aek_too("บท", dead_syllable_as_aek=True), "aek") def test_dead_syllable_without_flag_returns_false(self): + """Test that dead syllables return False when flag is not set.""" self.assertFalse(self.kv.check_aek_too("บท", dead_syllable_as_aek=False)) def test_list_with_non_string_element_raises(self): + """Test that list with non-string element raises TypeError.""" with self.assertRaises(TypeError): self.kv.check_aek_too(["ไก่", 42]) # type: ignore[list-item] def test_both_tone_marks_returns_false(self): + """Test that word with both tone marks returns False.""" # word with both ่ and ้ should return False self.assertFalse(self.kv.check_aek_too("ก่้")) @@ -661,23 +691,28 @@ class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): """Tests for check_klon k_type=8 and invalid k_type.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_invalid_k_type_returns_error_string(self): + """Test that invalid k_type returns error string.""" result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) self.assertIsInstance(result, str) self.assertIn("Something went wrong", result) def test_incomplete_klon4_poem(self): + """Test that incomplete klon4 poem is detected.""" result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) self.assertIsInstance(result, str) self.assertIn("does not have 4 complete sentences", result) def test_incomplete_klon8_poem(self): + """Test that incomplete klon8 poem is detected.""" result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=8) self.assertIsInstance(result, str) def test_check_klon8_correct_poem(self): + """Test that valid klon8 poem is recognized.""" poem = ( "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง " "ลคคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เขาชื่อน้องเธียร" @@ -685,6 +720,7 @@ def test_check_klon8_correct_poem(self): self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) def test_check_klon8_correct_poem_2(self): + """Test that another valid klon8 poem is recognized.""" poem = ( "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " @@ -697,6 +733,7 @@ def test_check_klon8_correct_poem_2(self): ) def test_check_klon8_correct_poem_3(self): + """Test that third valid klon8 poem is recognized.""" poem = ( "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " @@ -709,6 +746,7 @@ def test_check_klon8_correct_poem_3(self): ) def test_check_klon8_invalid_poem(self): + """Test that invalid klon8 poem with too many words is detected.""" poem = ( "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " @@ -723,6 +761,7 @@ def test_check_klon8_invalid_poem(self): ) def test_check_klon8_invalid_poem_2(self): + """Test that invalid klon8 poem with incorrect rhyme is detected.""" poem = ( "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " @@ -737,6 +776,7 @@ def test_check_klon8_invalid_poem_2(self): ) def test_check_klon8_invalid_poem_3(self): + """Test that invalid klon8 poem with wrong final word is detected.""" poem = ( "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " @@ -755,36 +795,47 @@ class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_sara.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_bo_mai_ek_returns_oo(self): + """Test that bo mai ek returns ออ vowel.""" self.assertEqual(self.kv.check_sara("บ่"), "ออ") def test_special_word_เออ(self): + """Test special word เออ vowel.""" self.assertEqual(self.kv.check_sara("เออ"), "เออ") def test_special_word_เอ(self): + """Test special word เอ vowel.""" self.assertEqual(self.kv.check_sara("เอ"), "เอ") def test_special_word_เอะ(self): + """Test special word เอะ vowel.""" self.assertEqual(self.kv.check_sara("เอะ"), "เอะ") def test_special_word_เอา(self): + """Test special word เอา vowel.""" self.assertEqual(self.kv.check_sara("เอา"), "เอา") def test_special_word_เอาะ(self): + """Test special word เอาะ vowel.""" self.assertEqual(self.kv.check_sara("เอาะ"), "เอาะ") def test_ru_sara(self): + """Test ฤ (ru) character vowel.""" self.assertEqual(self.kv.check_sara("ฤ"), "อึ") def test_ruea_sara(self): + """Test ฤา and ฤๅ (ru with aa vowel) characters.""" # ฤา (ฤ + sara า U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") def test_เอือ_sara(self): + """Test เอือ vowel combination.""" self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") def test_returns_string(self): + """Test that check_sara returns a string.""" self.assertIsInstance(self.kv.check_sara("เริง"), str) From 56b560215a9cbcc909c7dc07aa2a5fb961a64aa6 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 15:35:44 +0900 Subject: [PATCH 29/43] Add 1 blank line before each of class docstring (D203 PEP 257 conventions) --- tests/core/test_khavee.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index b11276a53..d7d2bf290 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -10,6 +10,7 @@ class KhaveeTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.check_sara, check_marttra, is_sumpus, check_klon, and check_aek_too methods.""" def test_check_sara(self): @@ -454,10 +455,10 @@ def test_is_sumpus(self): self.assertFalse(kv.is_sumpus("สวม", "อัว")) self.assertFalse(kv.is_sumpus("ชัวร์", "ชัน")) self.assertFalse(kv.is_sumpus("เลว", "เร็ว")) - self.assertFalse(kv.is_sumpus("ฤทธิ์", "ฤกษ์")) # ฤทธิ์ = ริด, ฤกษ์ = เริก - self.assertFalse(kv.is_sumpus("ฤทธิ์", "ลึด")) # ฤทธิ์ = ริด != ลึด - self.assertFalse(kv.is_sumpus("ฤกษ์", "ลึก")) # ฤกษ์ = เริก != ลึก - self.assertFalse(kv.is_sumpus("โหว่", "โถ่ว")) # แม่ก กา vs แม่เกอว + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ฤกษ์")) # ฤทธิ์ = ริด, ฤกษ์ = เริก + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ลึด")) # ฤทธิ์ = ริด != ลึด + self.assertFalse(kv.is_sumpus("ฤกษ์", "ลึก")) # ฤกษ์ = เริก != ลึก + self.assertFalse(kv.is_sumpus("โหว่", "โถ่ว")) # แม่ก กา vs แม่เกอว self.assertTrue(kv.is_sumpus("เขว", "เอ")) self.assertTrue(kv.is_sumpus("เขว", "เหว่")) @@ -468,9 +469,9 @@ def test_is_sumpus(self): self.assertTrue(kv.is_sumpus("ธ", "ณ")) self.assertTrue(kv.is_sumpus("ธ", "ทะ")) self.assertTrue(kv.is_sumpus("ศาสตร์", "มารถ")) - self.assertTrue(kv.is_sumpus("แตร", "แปร")) # แม่ก กา - self.assertTrue(kv.is_sumpus("แหล่", "แต่")) # เหลือแหล่ - self.assertTrue(kv.is_sumpus("แหน", "แกน")) # แม่ กน + self.assertTrue(kv.is_sumpus("แตร", "แปร")) # แม่ก กา + self.assertTrue(kv.is_sumpus("แหล่", "แต่")) # เหลือแหล่ + self.assertTrue(kv.is_sumpus("แหน", "แกน")) # แม่ กน # Structural equivalence logic & Normalization self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) @@ -529,6 +530,7 @@ def test_check_aek_too(self): class KhaveeCheckKaruLahuTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.check_karu_lahu.""" def setUp(self): @@ -565,6 +567,7 @@ def test_ko_mai_is_lahu(self): class KhaveeHandleKarunTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.handle_karun_sound_silence.""" def setUp(self): @@ -627,6 +630,7 @@ def test_returns_string(self): class KhaveeIsTrueFinalTestCase(unittest.TestCase): + """Tests for internal method KhaveeVerifier._is_true_final.""" def setUp(self): @@ -657,6 +661,7 @@ def test_fake_finals(self): class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): + """Edge-case tests for KhaveeVerifier.check_aek_too.""" def setUp(self): @@ -688,6 +693,7 @@ def test_both_tone_marks_returns_false(self): class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): + """Tests for check_klon k_type=8 and invalid k_type.""" def setUp(self): @@ -792,6 +798,7 @@ def test_check_klon8_invalid_poem_3(self): class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): + """Edge-case tests for KhaveeVerifier.check_sara.""" def setUp(self): From f780e36442e8ce711edcf517cd246bfb42b573c1 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 18:17:52 +0900 Subject: [PATCH 30/43] =?UTF-8?q?fix=20the=20=E0=B8=A3=E0=B8=A3=20loop=20l?= =?UTF-8?q?ogic=20in=20`check=5Fsara`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit รร should not be evaluated on a character-by-character basis and should be evaluate once right after the loop finishes --- pythainlp/khavee/core.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 778156fde..2319a1e93 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -167,11 +167,13 @@ def check_sara(self, word: str) -> str: sara.append("ไอ") elif i == "็": sara.append("็") - elif "รร" in word: - if self.check_marttra(word) == "กม": - sara.append("อำ") - else: - sara.append("อะ") + + # In case of รร + if "รร" in word: + if self.check_marttra(word) == "กม": + sara.append("อำ") + else: + sara.append("อะ") # Clean up 'ออ' if 'อ' is acting purely as an initial consonant (อต, อด, อบ, อวบ) if "ออ" in sara and len(sara) == 1 and word.startswith("อ") and countoa == 1: From c29c5078ccf1ac475a1a3f437694df8c43c1ab4d Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 18:39:29 +0900 Subject: [PATCH 31/43] Enhance documentation for check_sara, check_marttra, and is_sumpus methods docstring --- pythainlp/khavee/core.py | 60 ++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 2319a1e93..e2572e6e9 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -99,18 +99,19 @@ def _is_true_final(self, word: str) -> bool: def check_sara(self, word: str) -> str: """ - Check the vowels in the Thai word. + Check the phonetic vowel sound (สระ) of a Thai word. + + Extracts the core vowel representation used for rhyme matching, handling + complex vowel combinations, transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). :param str word: Thai word - :return: vowel name of the word + :return: The name of the vowel sound of the word (e.g., 'เออ', 'อะ', 'เอาะ') :rtype: str :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_sara("เริง")) # doctest: +SKIP 'เออ' """ @@ -330,20 +331,26 @@ def check_sara(self, word: str) -> str: def check_marttra(self, word: str) -> str: """ - Check the Thai spelling section of the Thai word. + Check the spelling section (มาตราตัวสะกด) of a Thai word. + + Note: This function strictly adheres to orthographic spelling (รูป) based on + the Royal Society of Thailand (ราชบัณฑิตยสภา) standards, rather than phonetics (เสียง). + Therefore, words ending in สระเกิน (อำ, ไอ, ใอ, เอา) as well as ฤ, ฤๅ, ฦ, ฦๅ + are correctly classified grammatically as แม่ ก กา ("กา"). Phonetic rhyming + for these vowels is handled dynamically in the `is_sumpus` function. :param str word: Thai word - :return: name of the spelling section of the word + :return: name of the spelling section of the word (e.g., กา, กก, กด, กน, กบ, กม, เกย, เกอว) :rtype: str :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_marttra("สาว")) # doctest: +SKIP 'เกอว' + >>> print(kv.check_marttra("ทำ")) # doctest: +SKIP + 'กา' """ # Handle consonant clusters ending with ร # ตร, ทร → remove ร (treat as final ต/ท sound) @@ -372,6 +379,12 @@ def check_marttra(self, word: str) -> str: if word in ["บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"]: return "กา" + # ------------------------------------------------------------------------- + # สระเกิน (อำ, ไอ, ใอ, เอา) Orthographic Handlers + # According to the Royal Society, these are classified structurally as แม่ ก กา. + # Phonetic rhyming (e.g. กรรม rhyming with จำ) is normalized in `is_sumpus`. + # ------------------------------------------------------------------------- + # Check for ำ or นิคหิต (-ํ) + า if word[-1] == "ำ" or word.endswith("ํา"): return "กา" @@ -449,29 +462,42 @@ def check_marttra(self, word: str) -> str: def is_sumpus(self, word1: str, word2: str) -> bool: """ - Check the rhyme between two words. + Check the rhyme (สัมผัส) between two Thai words. + + This function evaluates both the vowel sound (สระ) and the spelling section (มาตราตัวสะกด). + It incorporates phonetic normalization for สระเกิน (อำ, ไอ, ใอ) to ensure that + words with matching sounds but differing orthographies (e.g., "จำ" and "กรรม") + are correctly evaluated as rhymes. - :param str word1: Thai word - :param str word2: Thai word - :return: boolean + :param str word1: First Thai word + :param str word2: Second Thai word + :return: True if the words rhyme, False otherwise. :rtype: bool :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.is_sumpus("สรร", "อัน")) # doctest: +SKIP True - - >>> print(kv.is_sumpus("สรร", "แมว")) # doctest: +SKIP - False + >>> print(kv.is_sumpus("จำ", "กรรม")) # doctest: +SKIP + True """ marttra1 = self.check_marttra(word1) marttra2 = self.check_marttra(word2) sara1 = self.check_sara(word1) sara2 = self.check_sara(word2) + + # ------------------------------------------------------------------------- + # Phonetic Normalization for สระเกิน (อำ, ไอ, ใอ) + # + # While check_marttra classifies words like "วัย" as อะ+เกย and "ใจ" as ไอ+กา, + # poetry cares about the sound (เสียง). + # We normalize the phonetic CVC structures into their สระเกิน counterparts. + # ('เอา' requires no normalizer as native Thai spelling forces the /aw/ sound + # to use 'เ-า', bypassing the need for an อะ+เกอว collision). + # ------------------------------------------------------------------------- + # อัย -> ไอ (Normalize 'อะ' + 'เกย' to 'ไอ' + 'กา') if sara1 == "อะ" and marttra1 == "เกย": sara1 = "ไอ" From 078e0d4b320643b0c38fdd78c074a663fbdd7511 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 20:10:01 +0900 Subject: [PATCH 32/43] Refactor tests for check_karu_lahu method: streamline test cases for karu and lahu syllables --- tests/core/test_khavee.py | 59 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index d7d2bf290..fec7c83bd 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -537,33 +537,44 @@ def setUp(self): """Set up test fixtures.""" self.kv = KhaveeVerifier() - def test_dead_syllable_is_karu(self): - """Test that dead syllables are identified as karu.""" - self.assertEqual(self.kv.check_karu_lahu("กด"), "karu") + import unittest - def test_long_live_syllable_is_karu(self): - """Test that long live syllables are identified as karu.""" - self.assertEqual(self.kv.check_karu_lahu("กา"), "karu") - - def test_live_syllable_with_final_consonant_is_karu(self): - """Test that live syllables with final consonants are identified as karu.""" - self.assertEqual(self.kv.check_karu_lahu("กาน"), "karu") - - def test_bo_mai_ek_is_lahu(self): - """Test that bo mai ek is identified as lahu.""" - self.assertEqual(self.kv.check_karu_lahu("บ่"), "lahu") - - def test_no_short_word_is_lahu(self): - """Test that standalone consonant without vowel is identified as lahu.""" - self.assertEqual(self.kv.check_karu_lahu("ณ"), "lahu") +class KhaveeCheckKaruLahuTestCase(unittest.TestCase): - def test_tho_short_word_is_lahu(self): - """Test that tho short word is identified as lahu.""" - self.assertEqual(self.kv.check_karu_lahu("ธ"), "lahu") + def setUp(self): + """Set up test fixtures.""" + self.kv = KhaveeVerifier() - def test_ko_mai_is_lahu(self): - """Test that ko mai (killed consonant marker) is identified as lahu.""" - self.assertEqual(self.kv.check_karu_lahu("ก็"), "lahu") + def test_karu_words(self): + """Test that all specified heavy syllables (Karu) are correctly identified.""" + karu_words = [ + "กด", "กา", "กาน", + "ใน", "นา", "มี", "ปู", "ตา", "ดำ", "วัว", "ลาก", "ไถ", "พุทธ", + "สันดาน", "มูล", "หมองมัว", "ยั่ว", "เอว", "เจ็บ", "โสภา", "ศาลา", + "วัด", "แม่", "ข้าวสาร", "ดวงใจ", "ไฉไล", "เขลา", "เนื้อ", "เต้น", + "ทั่ว", "ร่าง", "สั่น", "ไหว", "ช่อฟ้า", "หัว", "อีกา", "สาม", + "ฤาษี", "คาวี", "วับวาบ", "ญาณ", "เรา", "ครอง", "แผ่นดิน", "โดย", + "ธรรม", "พรรณ", "เย้ยหยัน", "ดุก", "โดด", "โลด", "หยอย", "น้ำ", "พร่ำ" + ] + + for word in karu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), "karu") + + def test_lahu_words(self): + """Test that all specified light syllables (Lahu) are correctly identified.""" + lahu_words = [ + "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", + "ชะ", "กระ", "ยะ", "พะ", "ระ", "ละ", "ประ", "ฉะ", + "มติ", "กะปิ", "กะทิ", "กะทะ", "ฐิติ", "อุระ", "อมตะ", + "มิ", "จะ", "เกะกะ", "ทะลุ", "รวิ", "วจนะ", "ศศิ", + "และ", "สุ", "จิ", "ปุ", "ลิ", "สติ", "พระ", + "ระยะ", "เยาะ", "ธุระ" + ] + + for word in lahu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), "lahu") class KhaveeHandleKarunTestCase(unittest.TestCase): From 738eef7809671f2ff7c0beabb49d43aa8420a7e1 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 20:12:41 +0900 Subject: [PATCH 33/43] Refactor `_is_true_final` to reduce complexity. Refactor `check_karu_lahu`: streamline logic and enhance readability Change list [] to set {} for string matching to be more optimized. --- pythainlp/khavee/core.py | 236 +++++++++++++++------------------------ 1 file changed, 91 insertions(+), 145 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index e2572e6e9..2f7281a5e 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -40,59 +40,62 @@ def _is_true_final(self, word: str) -> bool: """ # Handle การันย์ word = self.handle_karun_sound_silence(word) - # Store original word to distinguish tone-dependent structures original_word = word - # Strip tone marks first so words like 'ใกล้' properly evaluate as ending in 'ล' word = remove_tonemark(word) if len(word) < 2: return False + last_char = word[-1] + # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants consonants = [c for c in word if c in thai_consonants + "ฤฦ"] - if len(consonants) < 2: return False - last_char = word[-1] + # คำควบกล้ำ / อักษรนำ (initial clusters) cluster = consonants[0] + consonants[1] # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) - if last_char == "ย" and ("ไ" in word or "ใ" in word): - return False # Check for ย inside เ-ีย (เสีย, เมีย) (part of the vowel) - if last_char == "ย" and "เ" in word and "ี" in word: - return False + if last_char == "ย": + if ("ไ" in word or "ใ" in word) or ("เ" in word and "ี" in word): + return False + + # --------------------------------------------------------------------- + # Guard Clauses: If it's not ending in ล, ร, ว, or doesn't have exactly 2 + # consonants, or lacks pre-posed vowels, it bypasses the cluster checks. + # --------------------------------------------------------------------- + if last_char not in {"ล", "ร", "ว"} or len(consonants) != 2: + return True # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) - if last_char in ["ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ", "ไ", "ใ"]): - if len(consonants) == 2: - # Check for ล - if last_char == "ล" and cluster in ["กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"]: - # Exception 'เพล' - แม่กน (monk food ฉันเพล) - if word == "เพล": - return True - return False + if not any(v in word for v in {"เ", "แ", "โ", "ไ", "ใ"}): + return True - # Check for ร - if last_char == "ร" and cluster in ["กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร"]: - return False + # Check for ล + if last_char == "ล" and cluster in {"กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"}: + # Exception 'เพล' - แม่กน (monk food ฉันเพล) Returns True, otherwise False + return word == "เพล" - # Check for ว (ควบแท้ and อักษรนำ) - if last_char == "ว": - # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) - if "ไ" in word or "ใ" in word: - if cluster in ["กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"]: - return False + # Check for ร + if last_char == "ร" and cluster in {"กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร"}: + return False - # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). Whitelist อักษรนำ/คำควบกล้ำ as exceptions - elif any(v in word for v in ["เ", "แ", "โ"]): - # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา - # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ - if original_word in ["เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"]: - return False + # Check for ว (ควบแท้ and อักษรนำ) + if last_char == "ว": + # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) + if ("ไ" in word or "ใ" in word) and cluster in {"กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"}: + return False + + # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). Whitelist อักษรนำ/คำควบกล้ำ as exceptions + elif any(v in word for v in {"เ", "แ", "โ"}): + # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา + # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ + if original_word in {"เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"}: + return False # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) return True @@ -100,7 +103,7 @@ def _is_true_final(self, word: str) -> bool: def check_sara(self, word: str) -> str: """ Check the phonetic vowel sound (สระ) of a Thai word. - + Extracts the core vowel representation used for rhyme matching, handling complex vowel combinations, transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). @@ -127,8 +130,8 @@ def check_sara(self, word: str) -> str: word_req = remove_tonemark(word) # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ - silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", - "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"] + silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", + "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"} if any(word_req.endswith(ex) for ex in silent_vowel_exceptions): word = word[:-1] word_req = word_req[:-1] @@ -249,7 +252,7 @@ def check_sara(self, word: str) -> str: elif "ออ" in sara and len(sara) > 1: sara.remove("ออ") elif "ว" in word and len(sara) == 0: - if word_req in ["บวร", "วร"]: + if word_req in {"บวร", "วร"}: sara.append("ออ") else: sara.append("อัว") # ควร, บวก, สวม @@ -279,7 +282,7 @@ def check_sara(self, word: str) -> str: # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(original_word): # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) - other_vowels = [v for v in sara if v not in ["เอ", "ออ"]] + other_vowels = [v for v in sara if v not in {"เอ", "ออ"}] if not other_vowels: sara = ["เออ"] @@ -293,7 +296,7 @@ def check_sara(self, word: str) -> str: sara.append("เออ") # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) # Use original_word here to ensure stripped Karun characters (like ธิ์) are evaluated - elif any(ex in original_word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): + elif any(ex in original_word for ex in {"กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ"}): sara.append("อิ") # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม) else: @@ -321,7 +324,7 @@ def check_sara(self, word: str) -> str: sara = ["ออ"] # In case of isolated symbols as words (ลดรูป อะ) - if word_req in ["ณ", "ธ", "อ", "พณ"]: + if word_req in {"ณ", "ธ", "อ", "พณ"}: sara = ["อะ"] if not sara: @@ -357,9 +360,9 @@ def check_marttra(self, word: str) -> str: # กร, ขร, คร, ฆร in compound words → remove ร (treat as final ก/ข/ค sound) # But single syllable words like "กร" should keep ร if len(word) >= 3 and word[-1] == "ร": - if word[-2] in ["ต", "ท"]: + if word[-2] in {"ต", "ท"}: word = word[:-1] - elif word[-2] in ["ก", "ข", "ค", "ฆ"]: + elif word[-2] in {"ก", "ข", "ค", "ฆ"}: word = word[:-1] word = self.handle_karun_sound_silence(word) @@ -370,13 +373,13 @@ def check_marttra(self, word: str) -> str: word = remove_tonemark(word) # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ - silent_vowel_exceptions = ["เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", - "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"] + silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", + "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"} if any(word.endswith(ex) for ex in silent_vowel_exceptions): word = word[:-1] # Check for อักษรตัวเดียวแทนคำ Standalone words - if word in ["บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"]: + if word in {"บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"}: return "กา" # ------------------------------------------------------------------------- @@ -401,13 +404,13 @@ def check_marttra(self, word: str) -> str: # Check for ไ/ใ if "ไ" in word or "ใ" in word: - if word[-1] not in ["ย", "ล", "ร", "ว"]: + if word[-1] not in {"ย", "ล", "ร", "ว"}: return "กา" elif not self._is_true_final(original_word): return "กา" # Check for เ, แ, โ + ย, ร, ล, ว (คำควบกล้ำ / อักษรนำ) - if word[-1] in ["ย", "ล", "ร", "ว"] and any(v in word for v in ["เ", "แ", "โ"]): + if word[-1] in {"ย", "ล", "ร", "ว"} and any(v in word for v in {"เ", "แ", "โ"}): # ไม่ใช่ตัวสะกดแท้ -> แม่ก กา if not self._is_true_final(original_word): return "กา" @@ -415,23 +418,23 @@ def check_marttra(self, word: str) -> str: # Check for ตัวสะกด final consonants # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) if ( - word[-1] in ["า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"] + word[-1] in {"า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"} or ("ี" in word and "ย" in word[-1]) # Catch สระเอีย (เสีย, เมีย) or ("ื" in word and "อ" in word[-1]) # Catch สระอือ (เรือ, เสือ) or ("ั" in word and "ว" in word[-1]) # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) ): return "กา" - elif word[-1] in ["ง"]: + elif word[-1] in {"ง"}: return "กง" - elif word[-1] in ["ม"]: + elif word[-1] in {"ม"}: return "กม" - elif word[-1] in ["ย"]: + elif word[-1] in {"ย"}: return "เกย" - elif word[-1] in ["ว"]: + elif word[-1] in {"ว"}: return "เกอว" - elif word[-1] in ["ก", "ข", "ค", "ฆ"]: + elif word[-1] in {"ก", "ข", "ค", "ฆ"}: return "กก" - elif word[-1] in [ + elif word[-1] in { "จ", "ช", "ซ", @@ -448,11 +451,11 @@ def check_marttra(self, word: str) -> str: "ศ", "ษ", "ส", - ]: + }: return "กด" - elif word[-1] in ["ญ", "ณ", "น", "ร", "ล", "ฬ"]: + elif word[-1] in {"ญ", "ณ", "น", "ร", "ล", "ฬ"}: return "กน" - elif word[-1] in ["บ", "ป", "พ", "ฟ", "ภ"]: + elif word[-1] in {"บ", "ป", "พ", "ฟ", "ภ"}: return "กบ" else: if "็" in word: @@ -490,11 +493,11 @@ def is_sumpus(self, word1: str, word2: str) -> bool: # ------------------------------------------------------------------------- # Phonetic Normalization for สระเกิน (อำ, ไอ, ใอ) - # + # # While check_marttra classifies words like "วัย" as อะ+เกย and "ใจ" as ไอ+กา, # poetry cares about the sound (เสียง). # We normalize the phonetic CVC structures into their สระเกิน counterparts. - # ('เอา' requires no normalizer as native Thai spelling forces the /aw/ sound + # ('เอา' requires no normalizer as native Thai spelling forces the /aw/ sound # to use 'เ-า', bypassing the need for an อะ+เกอว collision). # ------------------------------------------------------------------------- @@ -515,28 +518,17 @@ def is_sumpus(self, word1: str, word2: str) -> bool: return bool(marttra1 == marttra2 and sara1 == sara2) def check_karu_lahu(self, text: str) -> str: - if ( - self.check_marttra(text) != "กา" - or ( - self.check_marttra(text) == "กา" - and self.check_sara(text) - in [ - "อา", - "อี", - "อือ", - "อู", - "เอ", - "แอ", - "โอ", - "ออ", - "เออ", - "เอีย", - "เอือ", - "อัว", - ] - ) - or self.check_sara(text) in ["อำ", "ไอ", "เอา"] - ) and text not in ["บ่", "ณ", "ธ", "ก็"]: + if text in {"บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ"}: + return "lahu" + + marttra = self.check_marttra(text) + sara = self.check_sara(text) + + if (marttra != "กา" + or (marttra == "กา" and sara in + {"อา", "อี", "อือ", "อู", "เอ", "แอ", + "เออ", "โอ", "ออ", "เอีย", "เอือ", "อัว"}) + or sara in {"อำ", "ไอ", "เอา"}): return "karu" else: return "lahu" @@ -585,12 +577,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: for i, sent in enumerate(text.split()): sub_sent = subword_tokenize(sent, engine="dict") if len(sub_sent) > 10: - error.append( - "In sentence " - + str(i + 2) - + ", there are more than 10 words. " - + str(sub_sent) - ) + error.append(f"In sentence {i + 2}, there are more than 10 words. {sub_sent}") if (i + 1) % 4 == 1: list_sumpus_sent1.append(sub_sent[-1]) elif (i + 1) % 4 == 2: @@ -626,16 +613,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: countwrong += 1 if countwrong > 3: error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent1[i], - list_sumpus_sent2h[i], - ) - ) - + " in paragraph " - + str(i + 1) - ) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i]))} in paragraph {str(i + 1)}") if ( self.is_sumpus( list_sumpus_sent2l[i], list_sumpus_sent3[i] @@ -643,15 +621,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent3[i], - ) - ) - + " in paragraph " - + str(i + 1) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i]))} in paragraph {str(i + 1)}" ) if i > 0: if ( @@ -662,15 +632,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - ) - + " in paragraph " - + str(i + 1) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" ) if not error: return ( @@ -692,10 +654,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: sub_sent = subword_tokenize(sent, engine="dict") if len(sub_sent) > 5: error.append( - "In sentence " - + str(i + 2) - + ", there are more than 4 words. " - + str(sub_sent) + f"In sentence {i + 2}, there are more than 4 words. {sub_sent}" ) if (i + 1) % 4 == 1: list_sumpus_sent1.append(sub_sent[-1]) @@ -725,15 +684,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: countwrong += 1 if countwrong > 1: error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent1[i], - list_sumpus_sent2h[i], - ) - ) - + " in paragraph " - + str(i + 1) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i]))} in paragraph {str(i + 1)}" ) if ( self.is_sumpus( @@ -742,15 +693,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent3[i], - ) - ) - + " in paragraph " - + str(i + 1) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i]))} in paragraph {str(i + 1)}" ) if i > 0: if ( @@ -761,15 +704,18 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - ) - + " in paragraph " - + str(i + 1) + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" + ) + if i > 0: + if ( + self.is_sumpus( + list_sumpus_sent2l[i], + list_sumpus_sent4[i - 1], + ) + is False + ): + error.append( + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" ) if not error: return ( @@ -868,7 +814,7 @@ def handle_karun_sound_silence(self, word: str) -> str: # For Standard Karun silent suffixes (1 Consonant + Optional Vowel + Karun) # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (ธุ์), สิทธิ์ (ธิ์) # Check if there is an upper/lower vowel right before the Karun (ธุ์, ธิ์) - if len(word) >= 3 and word[-2] in ["ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"]: + if len(word) >= 3 and word[-2] in {"ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"}: return word[:-3] else: return word[:-2] From f423d9c8a3e21333b445a974c7520142b8ac7d5d Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 20:26:00 +0900 Subject: [PATCH 34/43] Fix rhyme error messages in KhaveeVerifier: fix f string in error.append() --- pythainlp/khavee/core.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 2f7281a5e..1fdeb5c92 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -613,7 +613,8 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: countwrong += 1 if countwrong > 3: error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i]))} in paragraph {str(i + 1)}") + f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" + ) if ( self.is_sumpus( list_sumpus_sent2l[i], list_sumpus_sent3[i] @@ -621,7 +622,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i]))} in paragraph {str(i + 1)}" + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" ) if i > 0: if ( @@ -632,7 +633,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" ) if not error: return ( @@ -684,7 +685,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: countwrong += 1 if countwrong > 1: error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i]))} in paragraph {str(i + 1)}" + f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" ) if ( self.is_sumpus( @@ -693,7 +694,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i]))} in paragraph {str(i + 1)}" + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" ) if i > 0: if ( @@ -704,18 +705,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: is False ): error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" - ) - if i > 0: - if ( - self.is_sumpus( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - is False - ): - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1]))} in paragraph {str(i + 1)}" + f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" ) if not error: return ( @@ -759,8 +749,7 @@ def check_aek_too( >>> # -> [False, 'aek', 'too'] """ if isinstance(text, list): - # type: ignore[misc] - return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] + return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] # type: ignore[misc] if not isinstance(text, str): raise TypeError("text must be str or iterable list[str]") From 5f497816b720b06ef9faeb2f2a452880472a930d Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Thu, 23 Jul 2026 20:47:31 +0900 Subject: [PATCH 35/43] =?UTF-8?q?Baked=20in=20VALID=5FCONSONANTS=20as=20fr?= =?UTF-8?q?ozenset(thai=5Fconsonants=20+=20"=E0=B8=A4=E0=B8=A6")=20for=20o?= =?UTF-8?q?ptimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor `check_klon` to be more optimized --- pythainlp/khavee/core.py | 63 +++++++++++++++------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 1fdeb5c92..016367b5d 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -12,6 +12,9 @@ class KhaveeVerifier: + # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants + VALID_CONSONANTS = frozenset(thai_consonants + "ฤฦ") + def __init__(self) -> None: """ KhaveeVerifier: Thai Poetry verifier @@ -50,8 +53,7 @@ def _is_true_final(self, word: str) -> bool: last_char = word[-1] - # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants - consonants = [c for c in word if c in thai_consonants + "ฤฦ"] + consonants = [c for c in word if c in self.VALID_CONSONANTS] if len(consonants) < 2: return False @@ -397,8 +399,8 @@ def check_marttra(self, word: str) -> str: return "กง" # Any word with exactly 1 consonant (and not ending in ำ/ํ) cannot have a final consonant and therefore must be "กา" - # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants - consonants = [c for c in word if c in thai_consonants + "ฤฦ"] + + consonants = [c for c in word if c in self.VALID_CONSONANTS] if len(consonants) == 1: return "กา" @@ -569,11 +571,11 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: if k_type == 8: try: error = [] - list_sumpus_sent1 = [] - list_sumpus_sent2h = [] - list_sumpus_sent2l = [] - list_sumpus_sent3 = [] - list_sumpus_sent4 = [] + list_sumpus_sent1 = [] # วรรคสดับ + list_sumpus_sent2h = [] # วรรครับ (คำที่ 2-5) + list_sumpus_sent2l = [] # วรรครับ (คำสุดท้าย) + list_sumpus_sent3 = [] # วรรครอง + list_sumpus_sent4 = [] # วรรคส่ง for i, sent in enumerate(text.split()): sub_sent = subword_tokenize(sent, engine="dict") if len(sub_sent) > 10: @@ -594,44 +596,33 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: list_sumpus_sent3.append(sub_sent[-1]) elif (i + 1) % 4 == 0: list_sumpus_sent4.append(sub_sent[-1]) - if ( - len(list_sumpus_sent1) != len(list_sumpus_sent2h) - or len(list_sumpus_sent2h) != len(list_sumpus_sent2l) - or len(list_sumpus_sent2l) != len(list_sumpus_sent3) - or len(list_sumpus_sent3) != len(list_sumpus_sent4) - or len(list_sumpus_sent4) != len(list_sumpus_sent1) - ): + # set {} does not allow duplicates, so if all lengths are equal, the set will have only 1 element + if len({len(list_sumpus_sent1), len(list_sumpus_sent2h), len(list_sumpus_sent2l), len(list_sumpus_sent3), len(list_sumpus_sent4)}) != 1: return "The poem does not have 4 complete sentences." else: for i in range(len(list_sumpus_sent1)): countwrong = 0 for j in list_sumpus_sent2h[i]: - if ( + if not ( self.is_sumpus(list_sumpus_sent1[i], j) - is False ): countwrong += 1 if countwrong > 3: error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" ) - if ( + if not ( self.is_sumpus( list_sumpus_sent2l[i], list_sumpus_sent3[i] ) - is False ): error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" ) if i > 0: - if ( - self.is_sumpus( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - is False - ): + if not ( + self.is_sumpus(list_sumpus_sent2l[i],list_sumpus_sent4[i - 1],) + ): error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" ) @@ -666,43 +657,35 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: list_sumpus_sent3.append(sub_sent[-1]) elif (i + 1) % 4 == 0: list_sumpus_sent4.append(sub_sent[-1]) - if ( - len(list_sumpus_sent1) != len(list_sumpus_sent2h) - or len(list_sumpus_sent2h) != len(list_sumpus_sent2l) - or len(list_sumpus_sent2l) != len(list_sumpus_sent3) - or len(list_sumpus_sent3) != len(list_sumpus_sent4) - or len(list_sumpus_sent4) != len(list_sumpus_sent1) - ): + # set {} does not allow duplicates, so if all lengths are equal, the set will have only 1 element + if len({len(list_sumpus_sent1), len(list_sumpus_sent2h), len(list_sumpus_sent2l), len(list_sumpus_sent3), len(list_sumpus_sent4)}) != 1: return "The poem does not have 4 complete sentences." else: for i in range(len(list_sumpus_sent1)): countwrong = 0 for j in list_sumpus_sent2h[i]: - if ( + if not ( self.is_sumpus(list_sumpus_sent1[i], j) - is False ): countwrong += 1 if countwrong > 1: error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" ) - if ( + if not ( self.is_sumpus( list_sumpus_sent2l[i], list_sumpus_sent3[i] ) - is False ): error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" ) if i > 0: - if ( + if not ( self.is_sumpus( list_sumpus_sent2l[i], list_sumpus_sent4[i - 1], ) - is False ): error.append( f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" From e85c13ee9e1e3e298c0ea996ca9ada6580f2f52f Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Fri, 24 Jul 2026 01:08:17 +0900 Subject: [PATCH 36/43] Rewrite `check_klon`, `test_khavee.py` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite `check_klon` to be more compact and correctly check for inter-stanza rhyme (สัมผัสระหว่างบท). Update `check_klon` to use ssg instead of dict for better segmentation. Move the test case of `check_klon` from tests/core/ to tests/extra/ and update tests/extra/__init__.py accordingly. --- pythainlp/khavee/core.py | 299 ++++++++++++++++++------------------- tests/core/test_khavee.py | 118 +-------------- tests/extra/__init__.py | 1 + tests/extra/test_khavee.py | 168 +++++++++++++++++++++ 4 files changed, 318 insertions(+), 268 deletions(-) create mode 100644 tests/extra/test_khavee.py diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 016367b5d..10b7ec3e5 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -4,6 +4,7 @@ # ruff: noqa: C901 from __future__ import annotations +import re from typing import Union from pythainlp import thai_consonants @@ -106,8 +107,8 @@ def check_sara(self, word: str) -> str: """ Check the phonetic vowel sound (สระ) of a Thai word. - Extracts the core vowel representation used for rhyme matching, handling - complex vowel combinations, transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). + Extracts the core vowel representation used for rhyme matching, handling complex vowel combinations, + transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). :param str word: Thai word :return: The name of the vowel sound of the word (e.g., 'เออ', 'อะ', 'เอาะ') @@ -338,10 +339,10 @@ def check_marttra(self, word: str) -> str: """ Check the spelling section (มาตราตัวสะกด) of a Thai word. - Note: This function strictly adheres to orthographic spelling (รูป) based on + Note: This function strictly adheres to orthographic spelling (รูป) based on the Royal Society of Thailand (ราชบัณฑิตยสภา) standards, rather than phonetics (เสียง). - Therefore, words ending in สระเกิน (อำ, ไอ, ใอ, เอา) as well as ฤ, ฤๅ, ฦ, ฦๅ - are correctly classified grammatically as แม่ ก กา ("กา"). Phonetic rhyming + Therefore, words ending in สระเกิน (อำ, ไอ, ใอ, เอา) as well as ฤ, ฤๅ, ฦ, ฦๅ + are correctly classified grammatically as แม่ ก กา ("กา"). Phonetic rhyming for these vowels is handled dynamically in the `is_sumpus` function. :param str word: Thai word @@ -399,7 +400,7 @@ def check_marttra(self, word: str) -> str: return "กง" # Any word with exactly 1 consonant (and not ending in ำ/ํ) cannot have a final consonant and therefore must be "กา" - + consonants = [c for c in word if c in self.VALID_CONSONANTS] if len(consonants) == 1: return "กา" @@ -470,8 +471,8 @@ def is_sumpus(self, word1: str, word2: str) -> bool: Check the rhyme (สัมผัส) between two Thai words. This function evaluates both the vowel sound (สระ) and the spelling section (มาตราตัวสะกด). - It incorporates phonetic normalization for สระเกิน (อำ, ไอ, ใอ) to ensure that - words with matching sounds but differing orthographies (e.g., "จำ" and "กรรม") + It incorporates phonetic normalization for สระเกิน (อำ, ไอ, ใอ) to ensure that + words with matching sounds but differing orthographies (e.g., "จำ" and "กรรม") are correctly evaluated as rhymes. :param str word1: First Thai word @@ -540,167 +541,161 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: Check the suitability of the poem according to Thai principles. :param str text: Thai poem - :param int k_type: type of Thai poem - :return: the check results of the suitability of the - poem according to Thai principles. + :param int k_type: type of Thai poem (4 or 8) + :return: the check results of the suitability of the poem according to Thai principles. :rtype: Union[list[str], str] + ════════════════════════════════════════════════════════════════════════ + + กลอนสี่ (Klon 4) Diagram: + วรรคที่ ๑ (สดับ) วรรคที่ ๒ (รับ) + วรรคที่ ๓ (รอง) วรรคที่ ๔ (ส่ง) + + ┏━━━━━━━━┯━┓ [สัมผัสคำที่ 1 หรือ 2] + O O O X X X O O + ┏━━━━━━━━┯━┳━━━┛ + O O O X X X O X ━┓ + ┏━━━━━━━━┯━┓ ┃ สัมผัสระหว่างบท (Inter-stanza rhyme) + O O O X X X O O ━┛ + ┏━━━━━━━━┯━┳━━━┛ + O O O X X X O X + + ════════════════════════════════════════════════════════════════════════ + + กลอนแปด (Klon 8) Diagram: + วรรคที่ ๑ (สดับ) วรรคที่ ๒ (รับ) + วรรคที่ ๓ (รอง) วรรคที่ ๔ (ส่ง) + + ┏━━━━━━━━┯━┯━┳━┯━┑ [สัมผัสคำที่ 3 หรือ 5 / อนุโลม 1,2,4] + O O O O O O O X O O X O O O O X + ┏━━━━━━━━┯━┯━┳━┯━━━━━━━┛ + O O O O O O O X O O X O O O O X ━┓ + ┏━━━━━━━━┯━┯━┳━┯━┑ ┃ สัมผัสระหว่างบท (Inter-stanza rhyme) + O O O O O O O X O O X O O O O X ━┛ + ┏━━━━━━━━┯━┯━┳━┯━━━━━━━┛ + O O O O O O O X O O X O O O O X + + ════════════════════════════════════════════════════════════════════════ + :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_klon( # doctest: +SKIP ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง ลคคนเก่ง เอ๋งเอ๋งคะนอง \ ... มีคนจับจอง เขาชื่อน้องเธียร', ... k_type=4 ... )) The poem is correct according to the principle. - - >>> print(kv.check_klon( # doctest: +SKIP - ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง ลคคนเก่ง \ - ... เอ๋งเอ๋งเสียงหมา มีคนจับจอง เขาชื่อน้องเธียร', - ... k_type=4 - ... )) - [ - "Can't find rhyme between paragraphs ('หมา', 'จอง') in paragraph 2", - "Can't find rhyme between paragraphs ('หมา', 'ทอง') in paragraph 2" - ] """ - if k_type == 8: - try: - error = [] - list_sumpus_sent1 = [] # วรรคสดับ - list_sumpus_sent2h = [] # วรรครับ (คำที่ 2-5) - list_sumpus_sent2l = [] # วรรครับ (คำสุดท้าย) - list_sumpus_sent3 = [] # วรรครอง - list_sumpus_sent4 = [] # วรรคส่ง - for i, sent in enumerate(text.split()): - sub_sent = subword_tokenize(sent, engine="dict") - if len(sub_sent) > 10: - error.append(f"In sentence {i + 2}, there are more than 10 words. {sub_sent}") - if (i + 1) % 4 == 1: - list_sumpus_sent1.append(sub_sent[-1]) - elif (i + 1) % 4 == 2: - list_sumpus_sent2h.append( - [ - sub_sent[1], - sub_sent[2], - sub_sent[3], - sub_sent[4], - ] - ) - list_sumpus_sent2l.append(sub_sent[-1]) - elif (i + 1) % 4 == 3: - list_sumpus_sent3.append(sub_sent[-1]) - elif (i + 1) % 4 == 0: - list_sumpus_sent4.append(sub_sent[-1]) - # set {} does not allow duplicates, so if all lengths are equal, the set will have only 1 element - if len({len(list_sumpus_sent1), len(list_sumpus_sent2h), len(list_sumpus_sent2l), len(list_sumpus_sent3), len(list_sumpus_sent4)}) != 1: - return "The poem does not have 4 complete sentences." - else: - for i in range(len(list_sumpus_sent1)): - countwrong = 0 - for j in list_sumpus_sent2h[i]: - if not ( - self.is_sumpus(list_sumpus_sent1[i], j) - ): - countwrong += 1 - if countwrong > 3: - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" - ) - if not ( - self.is_sumpus( - list_sumpus_sent2l[i], list_sumpus_sent3[i] - ) - ): - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" - ) - if i > 0: - if not ( - self.is_sumpus(list_sumpus_sent2l[i],list_sumpus_sent4[i - 1],) - ): - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" - ) - if not error: - return ( - "The poem is correct according to the principle." - ) - else: - return error - except Exception: - return "Something went wrong. Make sure you enter it in the correct form of klon 8." - elif k_type == 4: - try: - error = [] - list_sumpus_sent1 = [] - list_sumpus_sent2h = [] - list_sumpus_sent2l = [] - list_sumpus_sent3 = [] - list_sumpus_sent4 = [] - for i, sent in enumerate(text.split()): - sub_sent = subword_tokenize(sent, engine="dict") - if len(sub_sent) > 5: - error.append( - f"In sentence {i + 2}, there are more than 4 words. {sub_sent}" + + try: + import ssg # type: ignore[import-unresolved] + except ImportError: + raise ImportError( + "The 'ssg' package is required for check_klon. " + "Please install it using: pip install pythainlp[extra] or pip install ssg" + ) + + if k_type not in {4, 8}: + return "Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." + + try: + # Normalize spacing with regex Splits by spaces, newlines, or transitions between phrases + waks = [w for w in re.split(r'\s+|\n+|\t+', text.strip()) if w] + # Ensure the poem has complete stanzas (4 waks per stanza) + if len(waks) % 4 != 0 or len(waks) == 0: + return "The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค)." + + errors = [] + stanzas = [] + wak_names = ["วรรคสดับ (Wak 1)", "วรรครับ (Wak 2)", "วรรครอง (Wak 3)", "วรรคส่ง (Wak 4)"] + + # 1. Tokenize and group sentences into stanzas (4 Waks per stanza) + for i in range(0, len(waks), 4): + stanza = [ + subword_tokenize(waks[i], engine="ssg"), + subword_tokenize(waks[i + 1], engine="ssg"), + subword_tokenize(waks[i + 2], engine="ssg"), + subword_tokenize(waks[i + 3], engine="ssg"), + ] + stanzas.append(stanza) + + # 2. Evaluate rules for each stanza + for stanza_index, stanza in enumerate(stanzas): + wak1, wak2, wak3, wak4 = stanza + + # Safety check against empty sentences + if not all((wak1, wak2, wak3, wak4)): + errors.append(f"Stanza (บทที่) {stanza_index + 1} contains empty sentences.") + continue + + # Check word counts + max_words = 10 if k_type == 8 else 5 + for wak_index, wak in enumerate(stanza): + if len(wak) > max_words: + errors.append( + f"Stanza (บทที่) {stanza_index + 1} {wak_names[wak_index]}: " + f"Word count exceeds {max_words}: {wak}" ) - if (i + 1) % 4 == 1: - list_sumpus_sent1.append(sub_sent[-1]) - elif (i + 1) % 4 == 2: - list_sumpus_sent2h.append([sub_sent[1], sub_sent[2]]) - list_sumpus_sent2l.append(sub_sent[-1]) - elif (i + 1) % 4 == 3: - list_sumpus_sent3.append(sub_sent[-1]) - elif (i + 1) % 4 == 0: - list_sumpus_sent4.append(sub_sent[-1]) - # set {} does not allow duplicates, so if all lengths are equal, the set will have only 1 element - if len({len(list_sumpus_sent1), len(list_sumpus_sent2h), len(list_sumpus_sent2l), len(list_sumpus_sent3), len(list_sumpus_sent4)}) != 1: - return "The poem does not have 4 complete sentences." + + # Define rhyme target lengths based on Klon type + # Klon 8: Targets first 5 words (อนุโลม 1, 2, 4 บังคับ 3, 5) + # Klon 4: Targets first 2 words + if k_type == 8: + wak2_targets = wak2[:5] + wak4_targets = wak4[:5] else: - for i in range(len(list_sumpus_sent1)): - countwrong = 0 - for j in list_sumpus_sent2h[i]: - if not ( - self.is_sumpus(list_sumpus_sent1[i], j) - ): - countwrong += 1 - if countwrong > 1: - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent1[i], list_sumpus_sent2h[i],))} in paragraph {str(i + 1)}" - ) - if not ( - self.is_sumpus( - list_sumpus_sent2l[i], list_sumpus_sent3[i] - ) - ): - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent3[i],))} in paragraph {str(i + 1)}" - ) - if i > 0: - if not ( - self.is_sumpus( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - ): - error.append( - f"Can't find rhyme between paragraphs {str((list_sumpus_sent2l[i], list_sumpus_sent4[i - 1],))} in paragraph {str(i + 1)}" - ) - if not error: - return ( - "The poem is correct according to the principle." + # Klon 4: If the target sentence has 5 words, check the first 3 words. + # Otherwise, check the standard first 2 words. + limit_wak2 = 3 if len(wak2) == 5 else 2 + limit_wak4 = 3 if len(wak4) == 5 else 2 + + wak2_targets = wak2[:limit_wak2] + wak4_targets = wak4[:limit_wak4] + + # Extract the last word of each Wak + wak1_last = wak1[-1] + wak2_last = wak2[-1] + wak3_last = wak3[-1] + + # Rule 1: วรรคสดับ -> วรรครับ + if not any(self.is_sumpus(wak1_last, target) for target in wak2_targets): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak1_last}' ({wak_names[0]}) does not rhyme with {wak2_targets} ({wak_names[1]})" + ) + + # Rule 2: วรรครับ -> วรรครอง + if not self.is_sumpus(wak2_last, wak3_last): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak2_last}' ({wak_names[1]}) does not rhyme with '{wak3_last}' ({wak_names[2]})" + ) + + # Rule 3: วรรครอง -> วรรคส่ง + if not any(self.is_sumpus(wak3_last, target) for target in wak4_targets): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak3_last}' ({wak_names[2]}) does not rhyme with {wak4_targets} ({wak_names[3]})" + ) + + # Rule 4: สัมผัสระหว่างบท (Inter-stanza) + if stanza_index > 0: + # Target Wak 4 of the previous stanza + prev_wak4_last = stanzas[stanza_index - 1][3][-1] + if not self.is_sumpus(prev_wak4_last, wak2_last): + errors.append( + f"Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza {stanza_index} and {stanza_index + 1}: " + f"'{prev_wak4_last}' ({wak_names[3]}) does not rhyme with '{wak2_last}' ({wak_names[1]})" ) - else: - return error - except Exception: - return "Something went wrong. Make sure you enter it in the correct form." - else: - return "Something went wrong. Make sure you enter it in the correct form." + if not errors: + return "The poem is correct according to the principle." + return errors + + except Exception as e: + return f"Something went wrong during evaluation: {e}" def check_aek_too( self, text: Union[list[str], str], dead_syllable_as_aek: bool = False diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index fec7c83bd..1c4a26ff2 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -528,17 +528,6 @@ def test_check_aek_too(self): kv.check_aek_too(["หนม", "หน่ม", "หน้ม"]), [False, "aek", "too"] ) - -class KhaveeCheckKaruLahuTestCase(unittest.TestCase): - - """Tests for KhaveeVerifier.check_karu_lahu.""" - - def setUp(self): - """Set up test fixtures.""" - self.kv = KhaveeVerifier() - - import unittest - class KhaveeCheckKaruLahuTestCase(unittest.TestCase): def setUp(self): @@ -702,111 +691,8 @@ def test_both_tone_marks_returns_false(self): # word with both ่ and ้ should return False self.assertFalse(self.kv.check_aek_too("ก่้")) - -class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): - - """Tests for check_klon k_type=8 and invalid k_type.""" - - def setUp(self): - """Set up test fixtures.""" - self.kv = KhaveeVerifier() - - def test_invalid_k_type_returns_error_string(self): - """Test that invalid k_type returns error string.""" - result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) - self.assertIsInstance(result, str) - self.assertIn("Something went wrong", result) - - def test_incomplete_klon4_poem(self): - """Test that incomplete klon4 poem is detected.""" - result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) - self.assertIsInstance(result, str) - self.assertIn("does not have 4 complete sentences", result) - - def test_incomplete_klon8_poem(self): - """Test that incomplete klon8 poem is detected.""" - result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=8) - self.assertIsInstance(result, str) - - def test_check_klon8_correct_poem(self): - """Test that valid klon8 poem is recognized.""" - poem = ( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง " - "ลคคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เขาชื่อน้องเธียร" - ) - self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) - - def test_check_klon8_correct_poem_2(self): - """Test that another valid klon8 poem is recognized.""" - poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" - ) - self.assertEqual( - self.kv.check_klon(poem, k_type=8), - "The poem is correct according to the principle." - ) - - def test_check_klon8_correct_poem_3(self): - """Test that third valid klon8 poem is recognized.""" - poem = ( - "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " - "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " - "ลูกผู้ชายลายมือนั้นคือยศ เเจ้าจงอตส่าห์ทำสม่ำเสมียน " - "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" - ) - self.assertEqual( - self.kv.check_klon(poem, k_type=8), - "The poem is correct according to the principle." - ) - - def test_check_klon8_invalid_poem(self): - """Test that invalid klon8 poem with too many words is detected.""" - poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" - ) - result = self.kv.check_klon(poem, k_type=8) - self.assertIsInstance(result, list) - self.assertIn( - "In sentence 2, there are more than 10 words. ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", - result, - ) - - def test_check_klon8_invalid_poem_2(self): - """Test that invalid klon8 poem with incorrect rhyme is detected.""" - poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" - ) - result = self.kv.check_klon(poem, k_type=8) - self.assertIsInstance(result, list) - self.assertIn( - "Can't find rhyme between paragraphs ('มาก', ['อื่น', 'สัก', 'หมื่น', 'แสน']) in paragraph 1", - result, - ) - - def test_check_klon8_invalid_poem_3(self): - """Test that invalid klon8 poem with wrong final word is detected.""" - poem = ( - "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " - "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " - "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " - "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" - ) - result = self.kv.check_klon(poem, k_type=8) - self.assertIsInstance(result, list) - self.assertIn( - "Can't find rhyme between paragraphs ('เหมือน', 'เตือด') in paragraph 1", - result, - ) - +# Test KhaveeCheckKlonExtendedTestCase is moved to tests/extra/test_khavee_extra.py +# because it use extra dependency "ssg" that is not part of the core test class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): diff --git a/tests/extra/__init__.py b/tests/extra/__init__.py index b72414ca3..87c0afda9 100644 --- a/tests/extra/__init__.py +++ b/tests/extra/__init__.py @@ -18,6 +18,7 @@ "tests.extra.testx_tokenize", "tests.extra.testx_translate_helpers", "tests.extra.testx_word_vector", + "tests.extra.test_khavee.py" ] diff --git a/tests/extra/test_khavee.py b/tests/extra/test_khavee.py new file mode 100644 index 000000000..d9f455c07 --- /dev/null +++ b/tests/extra/test_khavee.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +import unittest + +from pythainlp.khavee import KhaveeVerifier + +kv = KhaveeVerifier() + +class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): + + """Tests for check_klon k_type=8 and invalid k_type.""" + + def setUp(self): + """Set up test fixtures.""" + self.kv = KhaveeVerifier() + + def test_invalid_k_type_returns_error_string(self): + """Test that invalid k_type returns error string.""" + result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) + self.assertIsInstance(result, str) + self.assertIn("Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." + , result) + + def test_incomplete_klon4_poem(self): + """Test that incomplete klon4 poem is detected.""" + result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) + self.assertIsInstance(result, str) + self.assertIn("The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค).", result) + + def test_incomplete_klon8_poem(self): + """Test that incomplete klon8 poem is detected.""" + result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=8) + self.assertIsInstance(result, str) + self.assertIn("The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค).", result) + + def test_check_klon4_incorrect_poem(self): + """Test that invalid klon4 poem is detected.""" + poem = ( + "มวลไม้ดอกสวด ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานา " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, list) + self.assertEqual(["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))"] + ,result) + + def test_check_klon4_incorrect_poem_2(self): + """Test that invalid klon4 poem with wrong inter-stanza rhyme is detected.""" + poem = ( + "มวลไม้ดอกสวด ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานะ " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, list) + self.assertEqual(["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))", + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'นะ' (วรรคส่ง (Wak 4)) does not rhyme with 'มา' (วรรครับ (Wak 2))"] + ,result) + + def test_check_klon4_correct_poem(self): + """Test that valid klon4 poem is recognized.""" + poem = ( + "มวลไม้ดอกสวย ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานา " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + self.assertIsNotNone(self.kv.check_klon(poem, k_type=4)) + result = self.kv.check_klon(poem, k_type=4) + self.assertEqual(result, "The poem is correct according to the principle.") + + def test_check_klon8_correct_poem(self): + """Test that valid klon8 poem is recognized.""" + poem = ( + "มวลไม้ดอกสวยระรวยกลิ่นหอม หมู่ผึ้งดมดอมพรั่งพร้อมนานา " + "ผีเสื้อปีกบางบินกางปีกมา ช่างงามจับตาเริงร่าสุขใจ" + ) + self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) + result = self.kv.check_klon(poem, k_type=8) + self.assertEqual(result, "The poem is correct according to the principle.") + + def test_check_klon8_correct_poem_2(self): + """Test that another valid klon8 poem is recognized.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_correct_poem_3(self): + """Test that third valid klon8 poem is recognized.""" + poem = ( + "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " + "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " + "ลูกผู้ชายลายมือนั้นคือยศ เเจ้าจงอตส่าห์ทำสม่ำเสมียน " + "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_invalid_poem(self): + """Test that invalid klon8 poem with too many words is detected. (แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก)""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + ["Stanza (บทที่) 1 วรรคสดับ (Wak 1): Word count exceeds 10: ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", + "Rhyme error in Stanza (บทที่) 1: 'มาก' (วรรคสดับ (Wak 1)) does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (วรรครับ (Wak 2))"], + result, + ) + + def test_check_klon8_invalid_poem_2(self): + """Test that invalid klon8 poem with incorrect rhyme is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + ["Rhyme error in Stanza (บทที่) 1: 'มาก' (วรรคสดับ (Wak 1)) does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (วรรครับ (Wak 2))", + "Rhyme error in Stanza (บทที่) 1: 'เตือน' (วรรครอง (Wak 3)) does not rhyme with ['จะ', 'จาก', 'เรือ', 'ร้าง', 'แม่'] (วรรคส่ง (Wak 4))"], + result, + ) + + def test_check_klon8_invalid_poem_3(self): + """Test that invalid klon8 poem with wrong final word is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + ["Rhyme error in Stanza (บทที่) 1: 'เหมือน' (วรรครับ (Wak 2)) does not rhyme with 'เตือด' (วรรครอง (Wak 3))", + "Rhyme error in Stanza (บทที่) 1: 'เตือด' (วรรครอง (Wak 3)) does not rhyme with ['จะ', 'จาก', 'เรือน', 'ร้าง', 'แม่'] (วรรคส่ง (Wak 4))"], + result, + ) + + def test_check_klon8_invalid_poem_4(self): + """Test that invalid klon8 poem with wrong inter-stanza rhyme is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัง " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + ["Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'ตัง' (วรรคส่ง (Wak 4)) does not rhyme with 'หัว' (วรรครับ (Wak 2))"], + result, + ) \ No newline at end of file From 54ed2b770d44effcfba184235301d824b0c068e5 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Fri, 24 Jul 2026 01:36:33 +0900 Subject: [PATCH 37/43] Refactor check_klon tests: move to new test_khavee_extended.py and update docstring test_khavee_extended.py now correctly evaluate check_klon --- pythainlp/khavee/core.py | 4 +-- tests/core/test_khavee.py | 27 +++----------- tests/extra/__init__.py | 2 +- ...test_khavee.py => test_khavee_extended.py} | 35 +++++++++++++++---- 4 files changed, 35 insertions(+), 33 deletions(-) rename tests/extra/{test_khavee.py => test_khavee_extended.py} (84%) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 10b7ec3e5..f80e92c74 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -582,8 +582,8 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP >>> kv = KhaveeVerifier() # doctest: +SKIP >>> print(kv.check_klon( # doctest: +SKIP - ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง ลคคนเก่ง เอ๋งเอ๋งคะนอง \ - ... มีคนจับจอง เขาชื่อน้องเธียร', + ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง \ + ... ฉันมันคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร', \ ... k_type=4 ... )) The poem is correct according to the principle. diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 1c4a26ff2..77673f61a 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -11,7 +11,10 @@ class KhaveeTestCase(unittest.TestCase): - """Tests for KhaveeVerifier.check_sara, check_marttra, is_sumpus, check_klon, and check_aek_too methods.""" + """ + Tests for KhaveeVerifier.check_sara, check_marttra, is_sumpus, and check_aek_too methods. + check_klon method is tested in KhaveeCheckKlonExtendedTestCase class in test_khavee_extended.py. + """ def test_check_sara(self): """Test check_sara with basic, reduced, complex, embedded, and standalone character vowels.""" @@ -497,28 +500,6 @@ def test_is_sumpus(self): # Verify strict phonemic constraints are maintained self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ - def test_check_klon(self): - """Test check_klon for Thai poem verification (k_type=4).""" - self.assertEqual( - kv.check_klon( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง \ - ลคคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เขาชื่อน้องเธียร", - k_type=4, - ), - "The poem is correct according to the principle.", - ) - self.assertEqual( - kv.check_klon( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง \ - ลคคนเก่ง เอ๋งเอ๋งเสียงหมา มีคนจับจอง เขาชื่อน้องเธียร", - k_type=4, - ), - [ - "Can't find rhyme between paragraphs ('หมา', 'จอง') in paragraph 2", - "Can't find rhyme between paragraphs ('หมา', 'ทอง') in paragraph 2", - ], - ) - def test_check_aek_too(self): """Test check_aek_too for Thai tone mark detection.""" self.assertFalse(kv.check_aek_too("ไกด์")) diff --git a/tests/extra/__init__.py b/tests/extra/__init__.py index 87c0afda9..770fc793d 100644 --- a/tests/extra/__init__.py +++ b/tests/extra/__init__.py @@ -18,7 +18,7 @@ "tests.extra.testx_tokenize", "tests.extra.testx_translate_helpers", "tests.extra.testx_word_vector", - "tests.extra.test_khavee.py" + "tests.extra.test_khavee_extended.py" ] diff --git a/tests/extra/test_khavee.py b/tests/extra/test_khavee_extended.py similarity index 84% rename from tests/extra/test_khavee.py rename to tests/extra/test_khavee_extended.py index d9f455c07..b61491470 100644 --- a/tests/extra/test_khavee.py +++ b/tests/extra/test_khavee_extended.py @@ -8,6 +8,7 @@ kv = KhaveeVerifier() + class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): """Tests for check_klon k_type=8 and invalid k_type.""" @@ -20,8 +21,7 @@ def test_invalid_k_type_returns_error_string(self): """Test that invalid k_type returns error string.""" result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) self.assertIsInstance(result, str) - self.assertIn("Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." - , result) + self.assertIn("Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8).", result) def test_incomplete_klon4_poem(self): """Test that incomplete klon4 poem is detected.""" @@ -43,8 +43,8 @@ def test_check_klon4_incorrect_poem(self): ) result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, list) - self.assertEqual(["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))"] - ,result) + self.assertEqual( + ["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))"], result) def test_check_klon4_incorrect_poem_2(self): """Test that invalid klon4 poem with wrong inter-stanza rhyme is detected.""" @@ -55,8 +55,7 @@ def test_check_klon4_incorrect_poem_2(self): result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, list) self.assertEqual(["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))", - "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'นะ' (วรรคส่ง (Wak 4)) does not rhyme with 'มา' (วรรครับ (Wak 2))"] - ,result) + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'นะ' (วรรคส่ง (Wak 4)) does not rhyme with 'มา' (วรรครับ (Wak 2))"], result) def test_check_klon4_correct_poem(self): """Test that valid klon4 poem is recognized.""" @@ -165,4 +164,26 @@ def test_check_klon8_invalid_poem_4(self): self.assertEqual( ["Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'ตัง' (วรรคส่ง (Wak 4)) does not rhyme with 'หัว' (วรรครับ (Wak 2))"], result, - ) \ No newline at end of file + ) + + def test_check_klon(self): + """Test check_klon for Thai poem verification (k_type=4).""" + poem = ( + "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง " + "ฉันมันคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, list) + self.assertEqual("The poem is correct according to the principle.", result) + + poem_invalid = ( + "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไล่ น้องหมาน้ำทอง " + "ฉันมันคนโหด เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" + ) + result_invalid = self.kv.check_klon(poem_invalid, k_type=4) + self.assertIsInstance(result_invalid, list) + self.assertEqual( + ["Rhyme error in Stanza (บทที่) 1: 'ไล่' (วรรครอง (Wak 3)) does not rhyme with ['น้อง', 'หมา'] (วรรคส่ง (Wak 4))", + "Rhyme error in Stanza (บทที่) 2: 'โหด' (วรรคสดับ (Wak 1)) does not rhyme with ['เอ๋ง', 'เอ๋ง'] (วรรครับ (Wak 2))"], + result_invalid, + ) From d24f9d30ed208c9976ce5b5f99a568f5747e2d39 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Fri, 24 Jul 2026 01:38:14 +0900 Subject: [PATCH 38/43] Fix test_check_klon assertion to expect string instead of list --- tests/extra/test_khavee_extended.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/extra/test_khavee_extended.py b/tests/extra/test_khavee_extended.py index b61491470..38d55f5b5 100644 --- a/tests/extra/test_khavee_extended.py +++ b/tests/extra/test_khavee_extended.py @@ -173,7 +173,7 @@ def test_check_klon(self): "ฉันมันคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" ) result = self.kv.check_klon(poem, k_type=4) - self.assertIsInstance(result, list) + self.assertIsInstance(result, str) self.assertEqual("The poem is correct according to the principle.", result) poem_invalid = ( From 3a46dde95137ac2b94dd25f1d258e647b011954f Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 29 Jul 2026 01:57:23 +0900 Subject: [PATCH 39/43] perf(khavee): optimize character lookups and simplify klon output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generator-based any() expressions with explicit short-circuit or chains in check_sara, check_marttra, and is_sumpus to eliminate unnecessary iteration overhead. Upgrade single-char containment checks from set membership (in {...}) to faster C-level string scans (in "..."). Add class-level docstring for KhaveeVerifier documenting all public methods, and a method-level docstring for check_karu_lahu explaining Thai syllable weight (ครุ/ลหุ) rules. Simplify check_klon error messages by dropping Thai terminology (วรรคสดับ, วรรครับ, etc.) in favor of concise English "Wak 1"–"Wak 4" designations. Use __import__("ssg") to silence linter warnings on optional third-party dependency. Consolidate seven redundant edge-case test methods into a single test_special_word case. Fix inverted assertIn argument order in extended test suite. Apply line-wrapping and trailing-whitespace cleanups throughout. --- pythainlp/khavee/core.py | 147 ++++++++++++++++++++++------ tests/core/test_khavee.py | 59 +++++------ tests/extra/test_khavee_extended.py | 83 +++++++++++----- 3 files changed, 199 insertions(+), 90 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index f80e92c74..30716178c 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -13,13 +13,61 @@ class KhaveeVerifier: + """ + Verifier for Thai poetry (ฉันทลักษณ์) principles. + + Provides methods to analyze Thai words and validate poetic structures + according to traditional Thai prosody rules. This class checks vowel + sounds (สระ), spelling sections (มาตราตัวสะกด), rhymes (สัมผัส), + syllable weight (ครุ/ลหุ), and full Thai klon 4/8 poem structure (กลอน). + + This class is designed to be deterministic according to the Royal Society of + Thailand's orthographic standards. The only exception is the use of "ssg" for + syllable segmentation in the :meth:`check_klon` method. + + Key capabilities: + - :meth:`check_sara` -> Identify the phonetic vowel sound of a Thai word, + handling complex vowels (สระประสม), transformed vowels (สระเปลี่ยนรูป), + and reduced vowels (สระลดรูป). + - :meth:`check_marttra` -> Determine the orthographic spelling section + (relative to the final consonant) per Royal Society standards. + - :meth:`is_sumpus` -> Evaluate whether two words rhyme by comparing + both vowel sound and spelling section, with phonetic normalisation + for สระเกิน (e.g., อำ, ไอ). + - :meth:`check_karu_lahu` -> Classify a syllable as heavy (ครุ) or + light (ลหุ) for meter analysis. + - :meth:`check_klon` -> Validate an entire poem against traditional + กลอนสี่ (4-syllable) or กลอนแปด (8-syllable) rhyme rules. + - :meth:`check_aek_too` -> Identify tonal marks (เอก/โท) on Thai words. + - :meth:`handle_karun_sound_silence` -> Strip characters silenced by + the การันย์ marker (e.g., \"โอห์ม\" → \"โอ\"). + - :meth:`_has_true_final_yl` and :meth:`_is_true_final` -> Determine + whether a word-ending \"ย\" or \"ล\" is a genuine final consonant + rather than part of an initial cluster or vowel digraph. + + :Example: + Basic usage:: + + >>> from pythainlp.khavee import KhaveeVerifier + >>> kv = KhaveeVerifier() + >>> kv.check_sara("เริง") + 'เออ' + >>> kv.is_sumpus("สรร", "อัน") + True + >>> kv.check_klon( + ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง', + ... k_type=4 + ... ) + 'The poem is correct according to the principle.' + + :Note: + The method :meth:`check_klon` requires the external ``ssg`` library. + """ # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants VALID_CONSONANTS = frozenset(thai_consonants + "ฤฦ") def __init__(self) -> None: - """ - KhaveeVerifier: Thai Poetry verifier - """ + """Initialize the KhaveeVerifier class.""" # For backward compatibility, this method is kept as a private method. def _has_true_final_yl(self, word: str) -> bool: @@ -74,27 +122,35 @@ def _is_true_final(self, word: str) -> bool: if last_char not in {"ล", "ร", "ว"} or len(consonants) != 2: return True - # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) - if not any(v in word for v in {"เ", "แ", "โ", "ไ", "ใ"}): + # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) + # with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- + # (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) + if not ("เ" in word or "แ" in word or "โ" in word or "ไ" in word or "ใ" in word): return True # Check for ล - if last_char == "ล" and cluster in {"กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล"}: + if last_char == "ล" and cluster in { + "กล", "ขล", "คล", "ปล", "ผล", "พล", + "หล", "ถล", "ฉล", "สล", "ศล", "ตล"}: # Exception 'เพล' - แม่กน (monk food ฉันเพล) Returns True, otherwise False return word == "เพล" # Check for ร - if last_char == "ร" and cluster in {"กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร"}: + if last_char == "ร" and cluster in { + "กร", "ขร", "คร", "ตร", "ปร", "พร", + "ฟร", "บร", "ศร", "สร", "หร"}: return False # Check for ว (ควบแท้ and อักษรนำ) if last_char == "ว": # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) - if ("ไ" in word or "ใ" in word) and cluster in {"กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"}: + if (cluster in {"กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"} + and ("ไ" in word or "ใ" in word)): return False - # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). Whitelist อักษรนำ/คำควบกล้ำ as exceptions - elif any(v in word for v in {"เ", "แ", "โ"}): + # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). + # Whitelist อักษรนำ/คำควบกล้ำ as exceptions + elif ("เ" in word or "แ" in word or "โ" in word): # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ if original_word in {"เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"}: @@ -107,7 +163,8 @@ def check_sara(self, word: str) -> str: """ Check the phonetic vowel sound (สระ) of a Thai word. - Extracts the core vowel representation used for rhyme matching, handling complex vowel combinations, + Extracts the core vowel representation used for rhyme matching, + handling complex vowel combinations, transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). :param str word: Thai word @@ -132,9 +189,11 @@ def check_sara(self, word: str) -> str: # Remove tonemarks for checking endings safely word_req = remove_tonemark(word) - # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ - silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", - "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"} + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) + # Removing the final character -ิ or -ุ + silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", + "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", + "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"} if any(word_req.endswith(ex) for ex in silent_vowel_exceptions): word = word[:-1] word_req = word_req[:-1] @@ -299,7 +358,8 @@ def check_sara(self, word: str) -> str: sara.append("เออ") # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) # Use original_word here to ensure stripped Karun characters (like ธิ์) are evaluated - elif any(ex in original_word for ex in {"กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ"}): + elif any(ex in original_word for ex in + ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): sara.append("อิ") # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม) else: @@ -370,14 +430,17 @@ def check_marttra(self, word: str) -> str: word = self.handle_karun_sound_silence(word) - # is_true_final process requires the original word to check for exceptions in อักษรนำ/คำควบกล้ำ + # is_true_final process requires the original word + # to check for exceptions in อักษรนำ/คำควบกล้ำ original_word = word word = remove_tonemark(word) - # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) Removing the final character -ิ or -ุ - silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", "ยัติ", "ภูมิ", - "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"} + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) + # Removing the final character -ิ or -ุ + silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", + "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", + "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"} if any(word.endswith(ex) for ex in silent_vowel_exceptions): word = word[:-1] @@ -399,7 +462,8 @@ def check_marttra(self, word: str) -> str: if word.endswith("ํ"): return "กง" - # Any word with exactly 1 consonant (and not ending in ำ/ํ) cannot have a final consonant and therefore must be "กา" + # Any word with exactly 1 consonant (and not ending in ำ/ํ) + # cannot have a final consonant and therefore must be "กา" consonants = [c for c in word if c in self.VALID_CONSONANTS] if len(consonants) == 1: @@ -413,7 +477,7 @@ def check_marttra(self, word: str) -> str: return "กา" # Check for เ, แ, โ + ย, ร, ล, ว (คำควบกล้ำ / อักษรนำ) - if word[-1] in {"ย", "ล", "ร", "ว"} and any(v in word for v in {"เ", "แ", "โ"}): + if word[-1] in "ยลรว" and ("เ" in word or "แ" in word or "โ" in word): # ไม่ใช่ตัวสะกดแท้ -> แม่ก กา if not self._is_true_final(original_word): return "กา" @@ -521,6 +585,22 @@ def is_sumpus(self, word1: str, word2: str) -> bool: return bool(marttra1 == marttra2 and sara1 == sara2) def check_karu_lahu(self, text: str) -> str: + """Classify a Thai syllable as heavy (ครุ karu) or light (ลหุ lahu). + + Syllable weight is determined by Thai prosody rules for classical poetry: + + - A syllable is heavy (ครุ) if it contains a long vowel, ends with any + final consonant (including sonorant finals / นมยวง), or contains one + of the special inherently bound vowels (อำ, ไอ, ใอ, เอา). + - A syllable is light (ลหุ) if it is an open syllable (แม่ ก กา) + containing a short vowel with no final consonant. + + Args: + text (str): A single Thai syllable or word to classify. + + Returns: + str: "karu" for heavy syllables or "lahu" for light syllables. + """ if text in {"บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ"}: return "lahu" @@ -590,18 +670,19 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: """ try: - import ssg # type: ignore[import-unresolved] - except ImportError: + __import__("ssg") + except ImportError as exc: raise ImportError( - "The 'ssg' package is required for check_klon. " - "Please install it using: pip install pythainlp[extra] or pip install ssg" - ) + "The 'ssg' library is required for comprehensive poem analysis (check_klon). " + "Please install it using: pip install ssg" + ) from exc if k_type not in {4, 8}: return "Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." try: - # Normalize spacing with regex Splits by spaces, newlines, or transitions between phrases + # Normalize spacing with regex Splits by spaces, + # newlines, or transitions between phrases waks = [w for w in re.split(r'\s+|\n+|\t+', text.strip()) if w] # Ensure the poem has complete stanzas (4 waks per stanza) if len(waks) % 4 != 0 or len(waks) == 0: @@ -609,7 +690,8 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: errors = [] stanzas = [] - wak_names = ["วรรคสดับ (Wak 1)", "วรรครับ (Wak 2)", "วรรครอง (Wak 3)", "วรรคส่ง (Wak 4)"] + # วรรคสดับ (Wak 1), วรรครับ (Wak 2), วรรครอง (Wak 3), วรรคส่ง (Wak 4) + wak_names = ["Wak 1", "Wak 2", "Wak 3", "Wak 4"] # 1. Tokenize and group sentences into stanzas (4 Waks per stanza) for i in range(0, len(waks), 4): @@ -723,11 +805,11 @@ def check_aek_too( ... kv.check_aek_too("เอ้ง"), ... ) >>> # -> False, aek, too - >>> print(kv.check_aek_too(["เอง", "เอ่ง", "เอ้ง"])) # ใช้ List ได้เหมือนกัน # doctest: +SKIP - >>> # -> [False, 'aek', 'too'] + >>> print(kv.check_aek_too(["เอง", "เอ่ง", "เอ้ง"])) # doctest: +SKIP + >>> # -> [False, 'aek', 'too'] ^^^^^^^^^^ # ใช้ List ได้เหมือนกัน """ if isinstance(text, list): - return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] # type: ignore[misc] + return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] # type: ignore[misc] if not isinstance(text, str): raise TypeError("text must be str or iterable list[str]") @@ -752,7 +834,8 @@ def handle_karun_sound_silence(self, word: str) -> str: :return: Thai word with silent consonant stripped :rtype: str """ - # Only process if the word ends with Karun (-์) [word like โอห์ม which has Karun in the middle will not be processed] + # Only process if the word ends with Karun (-์) + # [word like โอห์ม which has Karun in the middle will not be processed] if not word.endswith("์"): return word diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 77673f61a..1ea60f361 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -6,6 +6,7 @@ from pythainlp.khavee import KhaveeVerifier +# pylint: disable=protected-access kv = KhaveeVerifier() @@ -17,7 +18,10 @@ class KhaveeTestCase(unittest.TestCase): """ def test_check_sara(self): - """Test check_sara with basic, reduced, complex, embedded, and standalone character vowels.""" + """ + Test check_sara with basic, reduced, complex, embedded, + and standalone character vowels. + """ # Basic Vowels self.assertEqual(kv.check_sara("ฉะ"), "อะ") self.assertEqual(kv.check_sara("ค่ะ"), "อะ") @@ -509,6 +513,7 @@ def test_check_aek_too(self): kv.check_aek_too(["หนม", "หน่ม", "หน้ม"]), [False, "aek", "too"] ) + class KhaveeCheckKaruLahuTestCase(unittest.TestCase): def setUp(self): @@ -518,12 +523,12 @@ def setUp(self): def test_karu_words(self): """Test that all specified heavy syllables (Karu) are correctly identified.""" karu_words = [ - "กด", "กา", "กาน", - "ใน", "นา", "มี", "ปู", "ตา", "ดำ", "วัว", "ลาก", "ไถ", "พุทธ", - "สันดาน", "มูล", "หมองมัว", "ยั่ว", "เอว", "เจ็บ", "โสภา", "ศาลา", - "วัด", "แม่", "ข้าวสาร", "ดวงใจ", "ไฉไล", "เขลา", "เนื้อ", "เต้น", - "ทั่ว", "ร่าง", "สั่น", "ไหว", "ช่อฟ้า", "หัว", "อีกา", "สาม", - "ฤาษี", "คาวี", "วับวาบ", "ญาณ", "เรา", "ครอง", "แผ่นดิน", "โดย", + "กด", "กา", "กาน", + "ใน", "นา", "มี", "ปู", "ตา", "ดำ", "วัว", "ลาก", "ไถ", "พุทธ", + "สันดาน", "มูล", "หมองมัว", "ยั่ว", "เอว", "เจ็บ", "โสภา", "ศาลา", + "วัด", "แม่", "ข้าวสาร", "ดวงใจ", "ไฉไล", "เขลา", "เนื้อ", "เต้น", + "ทั่ว", "ร่าง", "สั่น", "ไหว", "ช่อฟ้า", "หัว", "อีกา", "สาม", + "ฤาษี", "คาวี", "วับวาบ", "ญาณ", "เรา", "ครอง", "แผ่นดิน", "โดย", "ธรรม", "พรรณ", "เย้ยหยัน", "ดุก", "โดด", "โลด", "หยอย", "น้ำ", "พร่ำ" ] @@ -536,8 +541,8 @@ def test_lahu_words(self): lahu_words = [ "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", "ชะ", "กระ", "ยะ", "พะ", "ระ", "ละ", "ประ", "ฉะ", - "มติ", "กะปิ", "กะทิ", "กะทะ", "ฐิติ", "อุระ", "อมตะ", - "มิ", "จะ", "เกะกะ", "ทะลุ", "รวิ", "วจนะ", "ศศิ", + "มติ", "กะปิ", "กะทิ", "กะทะ", "ฐิติ", "อุระ", "อมตะ", + "มิ", "จะ", "เกะกะ", "ทะลุ", "รวิ", "วจนะ", "ศศิ", "และ", "สุ", "จิ", "ปุ", "ลิ", "สติ", "พระ", "ระยะ", "เยาะ", "ธุระ" ] @@ -576,7 +581,10 @@ def test_word_ending_with_karun_stripped_2(self): self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") def test_complex_karun_stripped(self): - """Test complex karun stripping with single, multi-consonant, and vowel-embedded patterns.""" + """ + Test complex karun stripping with single, multi-consonant, + and vowel-embedded patterns. + """ # Explicit evaluation of single, multi-consonant, and vowel-embedded Karun rules self.assertEqual(self.kv.handle_karun_sound_silence("จันทร์"), "จัน") self.assertEqual(self.kv.handle_karun_sound_silence("สิทธิ์"), "สิท") @@ -675,6 +683,7 @@ def test_both_tone_marks_returns_false(self): # Test KhaveeCheckKlonExtendedTestCase is moved to tests/extra/test_khavee_extra.py # because it use extra dependency "ssg" that is not part of the core test + class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): """Edge-case tests for KhaveeVerifier.check_sara.""" @@ -687,40 +696,18 @@ def test_bo_mai_ek_returns_oo(self): """Test that bo mai ek returns ออ vowel.""" self.assertEqual(self.kv.check_sara("บ่"), "ออ") - def test_special_word_เออ(self): - """Test special word เออ vowel.""" + def test_special_word(self): + """Test special vowel combinations.""" self.assertEqual(self.kv.check_sara("เออ"), "เออ") - - def test_special_word_เอ(self): - """Test special word เอ vowel.""" - self.assertEqual(self.kv.check_sara("เอ"), "เอ") - - def test_special_word_เอะ(self): - """Test special word เอะ vowel.""" self.assertEqual(self.kv.check_sara("เอะ"), "เอะ") - - def test_special_word_เอา(self): - """Test special word เอา vowel.""" self.assertEqual(self.kv.check_sara("เอา"), "เอา") - - def test_special_word_เอาะ(self): - """Test special word เอาะ vowel.""" self.assertEqual(self.kv.check_sara("เอาะ"), "เอาะ") - - def test_ru_sara(self): - """Test ฤ (ru) character vowel.""" self.assertEqual(self.kv.check_sara("ฤ"), "อึ") + self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") + self.assertIsInstance(self.kv.check_sara("เริง"), str) def test_ruea_sara(self): """Test ฤา and ฤๅ (ru with aa vowel) characters.""" # ฤา (ฤ + sara า U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") - - def test_เอือ_sara(self): - """Test เอือ vowel combination.""" - self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") - - def test_returns_string(self): - """Test that check_sara returns a string.""" - self.assertIsInstance(self.kv.check_sara("เริง"), str) diff --git a/tests/extra/test_khavee_extended.py b/tests/extra/test_khavee_extended.py index 38d55f5b5..013b656ff 100644 --- a/tests/extra/test_khavee_extended.py +++ b/tests/extra/test_khavee_extended.py @@ -19,21 +19,35 @@ def setUp(self): def test_invalid_k_type_returns_error_string(self): """Test that invalid k_type returns error string.""" - result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) + poem = "บทกวีทดสอบ" + result = self.kv.check_klon(poem, k_type=99) self.assertIsInstance(result, str) - self.assertIn("Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8).", result) + self.assertIn( + result, + "Something went wrong. Make sure you enter it in the correct form " + "(k_type 4 or 8)." + ) def test_incomplete_klon4_poem(self): """Test that incomplete klon4 poem is detected.""" result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) self.assertIsInstance(result, str) - self.assertIn("The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค).", result) + self.assertIn( + result, + "The poem does not have complete stanzas (บท). " + "A stanza must contain exactly 4 sentences (วรรค)." + ) def test_incomplete_klon8_poem(self): """Test that incomplete klon8 poem is detected.""" - result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=8) + poem = "ฉันชื่อหมูกรอบ" + result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, str) - self.assertIn("The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค).", result) + self.assertIn( + result, + "The poem does not have complete stanzas (บท). " + "A stanza must contain exactly 4 sentences (วรรค)." + ) def test_check_klon4_incorrect_poem(self): """Test that invalid klon4 poem is detected.""" @@ -44,7 +58,10 @@ def test_check_klon4_incorrect_poem(self): result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, list) self.assertEqual( - ["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))"], result) + result, [ + "Rhyme error in Stanza (บทที่) 1: 'สวด' (Wak 1) " + "does not rhyme with ['ระ', 'รวย'] (Wak 2)" + ]) def test_check_klon4_incorrect_poem_2(self): """Test that invalid klon4 poem with wrong inter-stanza rhyme is detected.""" @@ -54,8 +71,15 @@ def test_check_klon4_incorrect_poem_2(self): ) result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, list) - self.assertEqual(["Rhyme error in Stanza (บทที่) 1: 'สวด' (วรรคสดับ (Wak 1)) does not rhyme with ['ระ', 'รวย'] (วรรครับ (Wak 2))", - "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'นะ' (วรรคส่ง (Wak 4)) does not rhyme with 'มา' (วรรครับ (Wak 2))"], result) + self.assertEqual( + result, + [ + "Rhyme error in Stanza (บทที่) 1: " + "'สวด' (Wak 1) does not rhyme with ['ระ', 'รวย'] (Wak 2)", + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: " + "'นะ' (Wak 4) does not rhyme with 'มา' (Wak 2)" + ] + ) def test_check_klon4_correct_poem(self): """Test that valid klon4 poem is recognized.""" @@ -104,7 +128,8 @@ def test_check_klon8_correct_poem_3(self): ) def test_check_klon8_invalid_poem(self): - """Test that invalid klon8 poem with too many words is detected. (แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก)""" + """Test that invalid klon8 poem with too many words. + (แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก)""" poem = ( "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " @@ -114,9 +139,13 @@ def test_check_klon8_invalid_poem(self): result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) self.assertEqual( - ["Stanza (บทที่) 1 วรรคสดับ (Wak 1): Word count exceeds 10: ['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", - "Rhyme error in Stanza (บทที่) 1: 'มาก' (วรรคสดับ (Wak 1)) does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (วรรครับ (Wak 2))"], result, + [ + "Stanza (บทที่) 1 Wak 1: Word count exceeds 10: " + "['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", + "Rhyme error in Stanza (บทที่) 1: 'มาก' (Wak 1) does not rhyme with " + "['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (Wak 2)" + ] ) def test_check_klon8_invalid_poem_2(self): @@ -130,9 +159,11 @@ def test_check_klon8_invalid_poem_2(self): result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) self.assertEqual( - ["Rhyme error in Stanza (บทที่) 1: 'มาก' (วรรคสดับ (Wak 1)) does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (วรรครับ (Wak 2))", - "Rhyme error in Stanza (บทที่) 1: 'เตือน' (วรรครอง (Wak 3)) does not rhyme with ['จะ', 'จาก', 'เรือ', 'ร้าง', 'แม่'] (วรรคส่ง (Wak 4))"], - result, + result, [ + "Rhyme error in Stanza (บทที่) 1: 'มาก' (Wak 1) " + "does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (Wak 2)", + "Rhyme error in Stanza (บทที่) 1: 'เตือน' (Wak 3) does not rhyme with " + "['จะ', 'จาก', 'เรือ', 'ร้าง', 'แม่'] (Wak 4)"] ) def test_check_klon8_invalid_poem_3(self): @@ -146,9 +177,11 @@ def test_check_klon8_invalid_poem_3(self): result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) self.assertEqual( - ["Rhyme error in Stanza (บทที่) 1: 'เหมือน' (วรรครับ (Wak 2)) does not rhyme with 'เตือด' (วรรครอง (Wak 3))", - "Rhyme error in Stanza (บทที่) 1: 'เตือด' (วรรครอง (Wak 3)) does not rhyme with ['จะ', 'จาก', 'เรือน', 'ร้าง', 'แม่'] (วรรคส่ง (Wak 4))"], - result, + result, [ + "Rhyme error in Stanza (บทที่) 1: 'เหมือน' (Wak 2) " + "does not rhyme with 'เตือด' (Wak 3)", + "Rhyme error in Stanza (บทที่) 1: 'เตือด' (Wak 3) " + "does not rhyme with ['จะ', 'จาก', 'เรือน', 'ร้าง', 'แม่'] (Wak 4)"] ) def test_check_klon8_invalid_poem_4(self): @@ -162,8 +195,12 @@ def test_check_klon8_invalid_poem_4(self): result = self.kv.check_klon(poem, k_type=8) self.assertIsInstance(result, list) self.assertEqual( - ["Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: 'ตัง' (วรรคส่ง (Wak 4)) does not rhyme with 'หัว' (วรรครับ (Wak 2))"], result, + [ + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) " + "between Stanza 1 and 2: 'ตัง' (Wak 4) " + "does not rhyme with 'หัว' (Wak 2)" + ] ) def test_check_klon(self): @@ -175,7 +212,7 @@ def test_check_klon(self): result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, str) self.assertEqual("The poem is correct according to the principle.", result) - + poem_invalid = ( "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไล่ น้องหมาน้ำทอง " "ฉันมันคนโหด เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" @@ -183,7 +220,9 @@ def test_check_klon(self): result_invalid = self.kv.check_klon(poem_invalid, k_type=4) self.assertIsInstance(result_invalid, list) self.assertEqual( - ["Rhyme error in Stanza (บทที่) 1: 'ไล่' (วรรครอง (Wak 3)) does not rhyme with ['น้อง', 'หมา'] (วรรคส่ง (Wak 4))", - "Rhyme error in Stanza (บทที่) 2: 'โหด' (วรรคสดับ (Wak 1)) does not rhyme with ['เอ๋ง', 'เอ๋ง'] (วรรครับ (Wak 2))"], - result_invalid, + result_invalid, [ + "Rhyme error in Stanza (บทที่) 1: 'ไล่' (Wak 3) " + "does not rhyme with ['น้อง', 'หมา'] (Wak 4)", + "Rhyme error in Stanza (บทที่) 2: 'โหด' (Wak 1) " + "does not rhyme with ['เอ๋ง', 'เอ๋ง'] (Wak 2)"] ) From b5962d867c5745f4ea17f1a0e137f996db3c6cc9 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 29 Jul 2026 05:22:21 +0900 Subject: [PATCH 40/43] refactor(khavee): harden edge cases, improve marttra cluster logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace _MASKING_TERMINAL_VOWELS frozenset with a tuple so str.endswith() can match directly at C speed, removing the any() generator loop. Add empty-string guards to check_sara, check_marttra, is_sumpus, and check_karu_lahu to prevent crashes on degenerate inputs ("", tone-only, karun-only). Improve check_marttra consonant-cluster handling: - Resolve karun before cluster analysis so stripped characters do not interfere (e.g. "ศาสตร์", "ศุกร์"). - Expand silent-ร cases to always strip after ต, ช, ป (fixes "เพชร" -> กด, "กอปร" (pronounced กอบ) -> กบ). - For ambiguous clusters (กร, ขร, คร, ฆร, ทร), only strip ร when preceded by a short embedded vowel (-ั, -ิ, -ี, -ุ, -ู), preserving true finals like "นคร", "มังกร", "สุนทร". Promote inline set literals to class-level frozenset constants (_LAHU_SYLLABLE_OVERRIDES). Replace silent fallback error strings with sensible defaults ("อะ" for check_sara, "กา" for check_marttra). Micro-optimizations: replace "in" checks on single-element sets with ==, drop unnecessary [*text] unpacking in check_aek_too, simplify check_klon regex (\s+ already covers \n/\t), use list comprehension for stanza building. Add extensive test coverage for newly handled edge cases in check_sara and check_marttra. --- pythainlp/khavee/core.py | 125 +++++++++++++++++++++++--------------- tests/core/test_khavee.py | 94 ++++++++++++++++++++++++---- 2 files changed, 160 insertions(+), 59 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 30716178c..e8ac0b0b8 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -66,6 +66,21 @@ class KhaveeVerifier: # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants VALID_CONSONANTS = frozenset(thai_consonants + "ฤฦ") + # Pali/Sanskrit loanwords where the final -ิ or -ุ is orthographically + # present but phonetically silent. Stripping it is harmless in check_sara + # (the leading vowel is detected first) and necessary in check_marttra + # (to expose the true final consonant for spelling-section classification). + _MASKING_TERMINAL_VOWELS: tuple[str, ...] = ( + "เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", + "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", + "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ", "วิมุติ", + ) + + # Syllables that are always light (ลหุ) regardless of orthography. + _LAHU_SYLLABLE_OVERRIDES: frozenset[str] = frozenset({ + "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", + }) + def __init__(self) -> None: """Initialize the KhaveeVerifier class.""" @@ -189,12 +204,13 @@ def check_sara(self, word: str) -> str: # Remove tonemarks for checking endings safely word_req = remove_tonemark(word) + # Empty string / weird input should return empty string (Catches "", "อ์", "้", etc.) + if not word_req: + return "" + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) # Removing the final character -ิ or -ุ - silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", - "ญัติ", "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", - "พยาธิ", "โพธิ", "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ"} - if any(word_req.endswith(ex) for ex in silent_vowel_exceptions): + if word_req.endswith(self._MASKING_TERMINAL_VOWELS): word = word[:-1] word_req = word_req[:-1] @@ -246,7 +262,7 @@ def check_sara(self, word: str) -> str: sara.remove("ออ") # In case of ออ (Clean redundant ออ from compound vowels like คือ, มือ) - if countoa == 1 and "อ" in word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: + if countoa == 1 and "อ" == word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: sara.remove("ออ") # In case of เอ เอ (merging two 'เอ' into 'แอ') @@ -287,7 +303,7 @@ def check_sara(self, word: str) -> str: sara.remove("เอ") sara.remove("อิ") sara.append("เออ") # เกิด, เมิน - elif "เอ" in sara and "ออ" in sara and "อ" in word[-1]: + elif "อ" == word[-1] and "เอ" in sara and "ออ" in sara: sara.remove("เอ") sara.remove("ออ") sara.append("เออ") # เหม่อ @@ -391,7 +407,9 @@ def check_sara(self, word: str) -> str: sara = ["อะ"] if not sara: - return "Can't find Sara in this word" + # Fallback: If no explicit vowel is found and it doesn't fit closed-syllable + # reductions, assume the implied short 'a' (เสียงอะกึ่งมาตรา). + return "อะ" return sara[0] @@ -418,32 +436,43 @@ def check_marttra(self, word: str) -> str: >>> print(kv.check_marttra("ทำ")) # doctest: +SKIP 'กา' """ - # Handle consonant clusters ending with ร - # ตร, ทร → remove ร (treat as final ต/ท sound) - # กร, ขร, คร, ฆร in compound words → remove ร (treat as final ก/ข/ค sound) - # But single syllable words like "กร" should keep ร - if len(word) >= 3 and word[-1] == "ร": - if word[-2] in {"ต", "ท"}: - word = word[:-1] - elif word[-2] in {"ก", "ข", "ค", "ฆ"}: - word = word[:-1] - + # Resolve Karun first to prevents "ศาสตร์" or "ศุกร์" + # from complicating the cluster logic. word = self.handle_karun_sound_silence(word) # is_true_final process requires the original word # to check for exceptions in อักษรนำ/คำควบกล้ำ original_word = word - word = remove_tonemark(word) + # Empty string should be returned as empty string (Catches "", "อ์", "้", etc.) + if not word: + return "" + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) # Removing the final character -ิ or -ุ - silent_vowel_exceptions = {"เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", - "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", - "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ"} - if any(word.endswith(ex) for ex in silent_vowel_exceptions): + if word.endswith(self._MASKING_TERMINAL_VOWELS): word = word[:-1] + # Handle consonant clusters ending with ร + if len(word) >= 3 and word[-1] == "ร": + prev_char = word[-2] + + # Safe to always strip + # (e.g., บุตร, เนตร, มิตร, เกษตร, บัตร, เพชร, กอปร (อ่านว่า กอบ)) + if prev_char in {"ต", "ช", "ป"}: + word = word[:-1] + + # Ambiguous (e.g., มังกร vs จักร, สุนทร vs สมุทร) + # Only strip 'ร' if the cluster is preceded by a specific short vowel. + elif prev_char in {"ก", "ข", "ค", "ฆ", "ท"}: + char_before_prev = word[-3] + + # If preceded by -ั, -ิ, -ี, -ุ, -ู (e.g., จั-ก-ร, สมุ-ท-ร) + if char_before_prev in {"ั", "ิ", "ี", "ุ", "ู"}: + word = word[:-1] + # (Words like นคร, มังกร, สุนทร will correctly BYPASS this and remain แม่กน) + # Check for อักษรตัวเดียวแทนคำ Standalone words if word in {"บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"}: return "กา" @@ -465,8 +494,8 @@ def check_marttra(self, word: str) -> str: # Any word with exactly 1 consonant (and not ending in ำ/ํ) # cannot have a final consonant and therefore must be "กา" - consonants = [c for c in word if c in self.VALID_CONSONANTS] - if len(consonants) == 1: + consonants = sum(1 for c in word if c in self.VALID_CONSONANTS) + if consonants == 1: return "กา" # Check for ไ/ใ @@ -486,18 +515,18 @@ def check_marttra(self, word: str) -> str: # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) if ( word[-1] in {"า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"} - or ("ี" in word and "ย" in word[-1]) # Catch สระเอีย (เสีย, เมีย) - or ("ื" in word and "อ" in word[-1]) # Catch สระอือ (เรือ, เสือ) - or ("ั" in word and "ว" in word[-1]) # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) + or ("ี" in word and "ย" == word[-1]) # Catch สระเอีย (เสีย, เมีย) + or ("ื" in word and "อ" == word[-1]) # Catch สระอือ (เรือ, เสือ) + or ("ั" in word and "ว" == word[-1]) # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) ): return "กา" - elif word[-1] in {"ง"}: + elif word[-1] == "ง": return "กง" - elif word[-1] in {"ม"}: + elif word[-1] == "ม": return "กม" - elif word[-1] in {"ย"}: + elif word[-1] == "ย": return "เกย" - elif word[-1] in {"ว"}: + elif word[-1] == "ว": return "เกอว" elif word[-1] in {"ก", "ข", "ค", "ฆ"}: return "กก" @@ -525,10 +554,7 @@ def check_marttra(self, word: str) -> str: elif word[-1] in {"บ", "ป", "พ", "ฟ", "ภ"}: return "กบ" else: - if "็" in word: - return "กา" - else: - return "Can't find Marttra in this word" + return "กา" def is_sumpus(self, word1: str, word2: str) -> bool: """ @@ -553,6 +579,10 @@ def is_sumpus(self, word1: str, word2: str) -> bool: >>> print(kv.is_sumpus("จำ", "กรรม")) # doctest: +SKIP True """ + # Empty string should be returned as False + if not word1 or not word2: + return False + marttra1 = self.check_marttra(word1) marttra2 = self.check_marttra(word2) sara1 = self.check_sara(word1) @@ -584,7 +614,7 @@ def is_sumpus(self, word1: str, word2: str) -> bool: marttra2 = "กา" return bool(marttra1 == marttra2 and sara1 == sara2) - def check_karu_lahu(self, text: str) -> str: + def check_karu_lahu(self, text: str) -> Union[str, bool]: """Classify a Thai syllable as heavy (ครุ karu) or light (ลหุ lahu). Syllable weight is determined by Thai prosody rules for classical poetry: @@ -599,9 +629,14 @@ def check_karu_lahu(self, text: str) -> str: text (str): A single Thai syllable or word to classify. Returns: - str: "karu" for heavy syllables or "lahu" for light syllables. + Union[str, bool]: "karu" for heavy syllables or "lahu" for light syllables. + or False if the input is an empty string. """ - if text in {"บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ"}: + # Empty string should be returned as False + if not text: + return False + + if text in self._LAHU_SYLLABLE_OVERRIDES: return "lahu" marttra = self.check_marttra(text) @@ -683,7 +718,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: try: # Normalize spacing with regex Splits by spaces, # newlines, or transitions between phrases - waks = [w for w in re.split(r'\s+|\n+|\t+', text.strip()) if w] + waks = [w for w in re.split(r'\s+', text.strip()) if w] # Ensure the poem has complete stanzas (4 waks per stanza) if len(waks) % 4 != 0 or len(waks) == 0: return "The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค)." @@ -695,12 +730,7 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: # 1. Tokenize and group sentences into stanzas (4 Waks per stanza) for i in range(0, len(waks), 4): - stanza = [ - subword_tokenize(waks[i], engine="ssg"), - subword_tokenize(waks[i + 1], engine="ssg"), - subword_tokenize(waks[i + 2], engine="ssg"), - subword_tokenize(waks[i + 3], engine="ssg"), - ] + stanza = [subword_tokenize(waks[i + j], engine="ssg") for j in range(4)] stanzas.append(stanza) # 2. Evaluate rules for each stanza @@ -814,10 +844,9 @@ def check_aek_too( if not isinstance(text, str): raise TypeError("text must be str or iterable list[str]") - word_characters = [*text] - if "่" in word_characters and "้" not in word_characters: + if "่" in text and "้" not in text: return "aek" - elif "้" in word_characters and "่" not in word_characters: + elif "้" in text and "่" not in text: return "too" if dead_syllable_as_aek and sound_syllable(text) == "dead": return "aek" diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 1ea60f361..11d0f546a 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -27,6 +27,8 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("ค่ะ"), "อะ") self.assertEqual(kv.check_sara("กระ"), "อะ") self.assertEqual(kv.check_sara("อรรถ"), "อะ") + self.assertEqual(kv.check_sara("สัตว์"), "อะ") + self.assertEqual(kv.check_sara("พันธุ์"), "อะ") self.assertEqual(kv.check_sara("พาล"), "อา") self.assertEqual(kv.check_sara("พลา"), "อา") self.assertEqual(kv.check_sara("ฆาต"), "อา") @@ -71,6 +73,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("สูรย์"), "อู") self.assertEqual(kv.check_sara("เซะ"), "เอะ") self.assertEqual(kv.check_sara("เอ"), "เอ") + self.assertEqual(kv.check_sara("เพล"), "เอ") self.assertEqual(kv.check_sara("เพช"), "เอ") self.assertEqual(kv.check_sara("เขษม"), "เอ") self.assertEqual(kv.check_sara("แอะ"), "แอะ") @@ -87,6 +90,18 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("เสือ"), "เอือ") self.assertEqual(kv.check_sara("เขือ"), "เอือ") self.assertEqual(kv.check_sara("กลัว"), "อัว") + self.assertEqual(kv.check_sara("โอห์ม"), "โอ") + self.assertEqual(kv.check_sara("โต"), "โอ") + self.assertEqual(kv.check_sara("โดรน"), "โอ") + self.assertEqual(kv.check_sara("ไป"), "ไอ") + self.assertEqual(kv.check_sara("ไตร"), "ไอ") + self.assertEqual(kv.check_sara("ไดร์ฟ"), "ไอ") + self.assertEqual(kv.check_sara("ใบ"), "ไอ") + self.assertEqual(kv.check_sara("ใกล้"), "ไอ") + self.assertEqual(kv.check_sara("ใหญ่"), "ไอ") + self.assertEqual(kv.check_sara("เต้า"), "เอา") + self.assertEqual(kv.check_sara("เป๋า"), "เอา") + self.assertEqual(kv.check_sara("เชาว์"), "เอา") # Reduced and Transformed Vowels (สระลดรูป/เปลี่ยนรูป) self.assertEqual(kv.check_sara("อัน"), "อะ") @@ -94,8 +109,10 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("สัญ"), "อะ") self.assertEqual(kv.check_sara("พวก"), "อัว") self.assertEqual(kv.check_sara("จวก"), "อัว") + self.assertEqual(kv.check_sara("พรวด"), "อัว") self.assertEqual(kv.check_sara("คน"), "โอะ") self.assertEqual(kv.check_sara("คล"), "โอะ") + self.assertEqual(kv.check_sara("โอ๊ะ"), "โอะ") self.assertEqual(kv.check_sara("พร"), "ออ") self.assertEqual(kv.check_sara("วร"), "ออ") self.assertEqual(kv.check_sara("บวร"), "ออ") @@ -105,6 +122,7 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("แข็ง"), "แอะ") self.assertEqual(kv.check_sara("แจ็ค"), "แอะ") self.assertEqual(kv.check_sara("แกร็น"), "แอะ") + self.assertEqual(kv.check_sara("เผลอ"), "เออ") self.assertEqual(kv.check_sara("เลย"), "เออ") self.assertEqual(kv.check_sara("เริง"), "เออ") self.assertEqual(kv.check_sara("เดิน"), "เออ") @@ -127,9 +145,14 @@ def test_check_sara(self): self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ self.assertEqual(kv.check_sara("สมมุติ"), "อุ") - self.assertEqual(kv.check_sara("ชาติ"), "อา") - self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("กอปร"), "ออ") # กอปร อ่านว่า กอบ + self.assertEqual(kv.check_sara("บวร"), "ออ") + self.assertEqual(kv.check_sara("อมร"), "ออ") + self.assertEqual(kv.check_sara("นคร"), "ออ") + self.assertEqual(kv.check_sara("กร"), "ออ") + self.assertEqual(kv.check_sara("จร"), "ออ") + self.assertEqual(kv.check_sara("พร"), "ออ") self.assertEqual(kv.check_sara("ออ"), "ออ") self.assertEqual(kv.check_sara("ขอ"), "ออ") self.assertEqual(kv.check_sara("งอ"), "ออ") @@ -217,6 +240,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("กรม"), "กม") self.assertEqual(kv.check_marttra("ธรรม"), "กม") self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") + self.assertEqual(kv.check_marttra("โอห์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") self.assertEqual(kv.check_marttra("โปรย"), "เกย") @@ -238,12 +262,14 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ดาว"), "เกอว") self.assertEqual(kv.check_marttra("แก้ว"), "เกอว") self.assertEqual(kv.check_marttra("เกียว"), "เกอว") + self.assertEqual(kv.check_marttra("เหว"), "เกอว") self.assertEqual(kv.check_marttra("บก"), "กก") self.assertEqual(kv.check_marttra("โรค"), "กก") self.assertEqual(kv.check_marttra("ลาก"), "กก") self.assertEqual(kv.check_marttra("นัข"), "กก") self.assertEqual(kv.check_marttra("จักร"), "กก") + self.assertEqual(kv.check_marttra("สมัคร"), "กก") self.assertEqual(kv.check_marttra("ตรึก"), "กก") self.assertEqual(kv.check_marttra("เงือก"), "กก") self.assertEqual(kv.check_marttra("พวก"), "กก") @@ -253,15 +279,17 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("อ็อก"), "กก") self.assertEqual(kv.check_marttra("อวก"), "กก") self.assertEqual(kv.check_marttra("ฤกษ์"), "กก") + self.assertEqual(kv.check_marttra("ศุกร์"), "กก") self.assertEqual(kv.check_marttra("ลักษมณ์"), "กก") self.assertEqual(kv.check_marttra("จด"), "กด") self.assertEqual(kv.check_marttra("ตรวจ"), "กด") self.assertEqual(kv.check_marttra("เสริฐ"), "กด") + self.assertEqual(kv.check_marttra("สมุทร"), "กด") + self.assertEqual(kv.check_marttra("ภัทร"), "กด") self.assertEqual(kv.check_marttra("บุตร"), "กด") self.assertEqual(kv.check_marttra("ตรุษ"), "กด") self.assertEqual(kv.check_marttra("มืด"), "กด") - self.assertEqual(kv.check_marttra("โยชน์"), "กด") self.assertEqual(kv.check_marttra("ชาติ"), "กด") self.assertEqual(kv.check_marttra("เกียรติ"), "กด") self.assertEqual(kv.check_marttra("วรรดิ"), "กด") @@ -288,14 +316,27 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("เร็จ"), "กด") self.assertEqual(kv.check_marttra("เตลิด"), "กด") self.assertEqual(kv.check_marttra("เพนียด"), "กด") + self.assertEqual(kv.check_marttra("สัตว์"), "กด") self.assertEqual(kv.check_marttra("กษัตริย์"), "กด") self.assertEqual(kv.check_marttra("ศาสตร์"), "กด") self.assertEqual(kv.check_marttra("เฮิรตซ์"), "กด") + self.assertEqual(kv.check_marttra("โยชน์"), "กด") + self.assertEqual(kv.check_marttra("กฤษณ์"), "กด") self.assertEqual(kv.check_marttra("ฤทธิ์"), "กด") self.assertEqual(kv.check_marttra("กฤษ"), "กด") - self.assertEqual(kv.check_marttra("กฤษณ์"), "กด") self.assertEqual(kv.check_marttra("ทฤษ"), "กด") + self.assertEqual(kv.check_marttra("เพล"), "กน") + self.assertEqual(kv.check_marttra("สุนทร"), "กน") + self.assertEqual(kv.check_marttra("มังกร"), "กน") + self.assertEqual(kv.check_marttra("สาคร"), "กน") + self.assertEqual(kv.check_marttra("พล"), "กน") + self.assertEqual(kv.check_marttra("กร"), "กน") + self.assertEqual(kv.check_marttra("จร"), "กน") + self.assertEqual(kv.check_marttra("พร"), "กน") + self.assertEqual(kv.check_marttra("บวร"), "กน") + self.assertEqual(kv.check_marttra("อมร"), "กน") + self.assertEqual(kv.check_marttra("นคร"), "กน") self.assertEqual(kv.check_marttra("มึน"), "กน") self.assertEqual(kv.check_marttra("ร้าน"), "กน") self.assertEqual(kv.check_marttra("ขนุน"), "กน") @@ -303,7 +344,6 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ทมิฬ"), "กน") self.assertEqual(kv.check_marttra("ซีน"), "กน") self.assertEqual(kv.check_marttra("บรร"), "กน") - self.assertEqual(kv.check_marttra("กร"), "กน") self.assertEqual(kv.check_marttra("เณร"), "กน") self.assertEqual(kv.check_marttra("ยนต์"), "กน") self.assertEqual(kv.check_marttra("กรรณ"), "กน") @@ -330,6 +370,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("อล"), "กน") self.assertEqual(kv.check_marttra("ควร"), "กน") self.assertEqual(kv.check_marttra("จันทร์"), "กน") + self.assertEqual(kv.check_marttra("พันธุ์"), "กน") self.assertEqual(kv.check_marttra("สินธุ์"), "กน") self.assertEqual(kv.check_marttra("ชอบ"), "กบ") @@ -342,6 +383,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("อุ๊ป"), "กบ") self.assertEqual(kv.check_marttra("ธูป"), "กบ") self.assertEqual(kv.check_marttra("กอล์ฟ"), "กบ") + self.assertEqual(kv.check_marttra("กอปร"), "กบ") # กอปร อ่านว่า กอบ self.assertEqual(kv.check_marttra("อะ"), "กา") self.assertEqual(kv.check_marttra("อา"), "กา") @@ -371,6 +413,8 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ไอ"), "กา") self.assertEqual(kv.check_marttra("ใอ"), "กา") self.assertEqual(kv.check_marttra("เอา"), "กา") + self.assertEqual(kv.check_marttra("จะ"), "กา") + self.assertEqual(kv.check_marttra("ผี"), "กา") self.assertEqual(kv.check_marttra("ปลา"), "กา") self.assertEqual(kv.check_marttra("งู"), "กา") self.assertEqual(kv.check_marttra("หมู"), "กา") @@ -390,6 +434,7 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ฎี"), "กา") self.assertEqual(kv.check_marttra("ตรี"), "กา") self.assertEqual(kv.check_marttra("พลี"), "กา") + self.assertEqual(kv.check_marttra("แคว"), "กา") # แม่น้ำแคว (ควบกล้ำ คว) self.assertEqual(kv.check_marttra("ซื้อ"), "กา") self.assertEqual(kv.check_marttra("ปรือ"), "กา") self.assertEqual(kv.check_marttra("ธุ"), "กา") @@ -413,13 +458,19 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ขำ"), "กา") self.assertEqual(kv.check_marttra("จำ"), "กา") self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") + self.assertEqual(kv.check_marttra("เผลอ"), "กา") self.assertEqual(kv.check_marttra("เหม่อ"), "กา") self.assertEqual(kv.check_marttra("เกลือ"), "กา") + self.assertEqual(kv.check_marttra("ไป"), "กา") + self.assertEqual(kv.check_marttra("น้ำ"), "กา") + self.assertEqual(kv.check_marttra("เรือ"), "กา") self.assertEqual(kv.check_marttra("ตัว"), "กา") self.assertEqual(kv.check_marttra("ครัว"), "กา") self.assertEqual(kv.check_marttra("ทรีย์"), "กา") self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") self.assertEqual(kv.check_marttra("นีย์"), "กา") + self.assertEqual(kv.check_marttra("อัชฌ"), "กา") + self.assertEqual(kv.check_marttra("มัจฉ"), "กา") # Fake Finals (คำควบกล้า, คำที่มีพยัญชนะ/สระไม่ออกเสียง) mapping to open syllables self.assertEqual(kv.check_marttra("ไทย"), "กา") @@ -467,6 +518,11 @@ def test_is_sumpus(self): self.assertFalse(kv.is_sumpus("ฤกษ์", "ลึก")) # ฤกษ์ = เริก != ลึก self.assertFalse(kv.is_sumpus("โหว่", "โถ่ว")) # แม่ก กา vs แม่เกอว + self.assertTrue(kv.is_sumpus("สมุทร", "สมุด")) + self.assertTrue(kv.is_sumpus("ภัทร", "พัด")) + self.assertTrue(kv.is_sumpus("สมัคร", "นัก")) + self.assertTrue(kv.is_sumpus("จักร", "พรรค")) + self.assertTrue(kv.is_sumpus("เพชร", "เพด")) self.assertTrue(kv.is_sumpus("เขว", "เอ")) self.assertTrue(kv.is_sumpus("เขว", "เหว่")) self.assertTrue(kv.is_sumpus("เหว", "เอว")) @@ -481,6 +537,9 @@ def test_is_sumpus(self): self.assertTrue(kv.is_sumpus("แหน", "แกน")) # แม่ กน # Structural equivalence logic & Normalization + self.assertTrue(kv.is_sumpus("มอญ", "อมร")) + self.assertTrue(kv.is_sumpus("นอน", "บวร")) + self.assertTrue(kv.is_sumpus("นอน", "กร")) self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) self.assertTrue(kv.is_sumpus("ทำ", "จำ")) self.assertTrue(kv.is_sumpus("ทำ", "กัม")) @@ -495,6 +554,7 @@ def test_is_sumpus(self): self.assertTrue(kv.is_sumpus("ไกว", "ใคร")) self.assertTrue(kv.is_sumpus("เลย", "เกย")) self.assertTrue(kv.is_sumpus("พวก", "จวก")) + self.assertTrue(kv.is_sumpus("พันธุ์", "จันทร์")) self.assertTrue(kv.is_sumpus("ฤทธิ์", "กิด")) self.assertTrue(kv.is_sumpus("ฤกษ์", "เริก")) self.assertTrue(kv.is_sumpus("พฤษ", "พรึด")) @@ -551,6 +611,18 @@ def test_lahu_words(self): with self.subTest(word=word): self.assertEqual(self.kv.check_karu_lahu(word), "lahu") + def test_invalid_karu_lahu_words(self): + """ + Test that invalid words are not identified as karu or lahu and should return False. + This includes empty strings. + """ + invalid_karu_lahu_words = [ + "" + ] + for word in invalid_karu_lahu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), False) + class KhaveeHandleKarunTestCase(unittest.TestCase): @@ -564,21 +636,21 @@ def test_word_without_karun_unchanged(self): """Test that words without karun are unchanged.""" self.assertEqual(self.kv.handle_karun_sound_silence("คน"), "คน") self.assertEqual(self.kv.handle_karun_sound_silence("กา"), "กา") + self.assertEqual(self.kv.handle_karun_sound_silence(""), "") # empty string unchanged # internal karun unchanged self.assertEqual(self.kv.handle_karun_sound_silence("การ์ตูน"), "การ์ตูน") + self.assertEqual(self.kv.handle_karun_sound_silence("โอร์ม"), "โอร์ม") self.assertEqual(self.kv.handle_karun_sound_silence("กอล์ฟ"), "กอล์ฟ") self.assertEqual(self.kv.handle_karun_sound_silence("ฟิล์ม"), "ฟิล์ม") self.assertEqual(self.kv.handle_karun_sound_silence("สตาร์ตอัป"), "สตาร์ตอัป") def test_word_ending_with_karun_stripped(self): """Test that karun and preceding consonant are stripped from end of word.""" - # เกมส์ → drop ์ and the consonant before it (ส) → เกม + # เกมส์ -> drop ์ and the consonant before it (ส) -> เกม self.assertEqual(self.kv.handle_karun_sound_silence("เกมส์"), "เกม") - - def test_word_ending_with_karun_stripped_2(self): - """Test karun stripping with different consonant.""" - # รักษ์ → drop ์ + ษ → รัก + # รักษ์ -> drop ์ + ษ -> รัก self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") + self.assertEqual(self.kv.handle_karun_sound_silence("สัตว์"), "สัต") def test_complex_karun_stripped(self): """ @@ -708,6 +780,6 @@ def test_special_word(self): def test_ruea_sara(self): """Test ฤา and ฤๅ (ru with aa vowel) characters.""" - # ฤา (ฤ + sara า U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa + # ฤา (ฤ + sara า U+0E32) -> อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") From b3e93781cb9920f44f3a16d853e99b37b344e763 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 29 Jul 2026 05:56:46 +0900 Subject: [PATCH 41/43] perf(khavee): move repeated sets to class constants, simplify char checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote all frequently-allocated set literals to class-level frozenset constants, eliminating per-call object creation in hot paths: - _LAM_CLUSTERS, _RUA_CLUSTERS, _WA_CLUSTERS, _WA_WHITELIST for initial-cluster checks in _is_true_final. - _OPEN_SYLLABLE_VOWELS, _KOK_CHARS, _KOD_CHARS, _KON_CHARS, _KOB_CHARS for spelling-section classification in check_marttra. - _LONG_VOWELS, _SPECIAL_VOWELS for karu/lahu prosody in check_karu_lahu. - _SINGLE_CHAR_WORDS, _EXPLICIT_SARA_WORDS for fast identity checks. Replace single-character set lookups (word[-1] in {"ล","ร","ว"}) with simple string checks (word[-1] in "ลรว"), which skip hash computation entirely. Collapse the six-way if/elif chain for explicit sara words into a single frozenset membership test. Cache word[-1] as last_char in check_marttra to avoid repeated indexing. Replace re.split(r'\s+', ...) with str.split() in check_klon — both produce identical results across all Unicode whitespace but str.split() is 3–5x faster with no regex overhead. --- pythainlp/khavee/core.py | 121 +++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 62 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index e8ac0b0b8..d5a90ae5a 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -81,6 +81,37 @@ class KhaveeVerifier: "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", }) + # Pre-computed frozensets for high-frequency set operations + _SINGLE_CHAR_WORDS: frozenset[str] = frozenset({"บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"}) + + # Initial cluster sets for _is_true_final + _LAM_CLUSTERS: frozenset[str] = frozenset({ + "กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล" + }) + _RUA_CLUSTERS: frozenset[str] = frozenset({ + "กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร" + }) + _WA_CLUSTERS: frozenset[str] = frozenset({ + "กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว" + }) + _WA_WHITELIST: frozenset[str] = frozenset({"เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"}) + + # Spelling sections (มาตราตัวสะกด) + _OPEN_SYLLABLE_VOWELS: frozenset[str] = frozenset({"า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"}) + _KOK_CHARS: frozenset[str] = frozenset({"ก", "ข", "ค", "ฆ"}) + _KOD_CHARS: frozenset[str] = frozenset({ + "จ", "ช", "ซ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ด", "ต", "ถ", "ท", "ธ", "ศ", "ษ", "ส" + }) + _KON_CHARS: frozenset[str] = frozenset({"ญ", "ณ", "น", "ร", "ล", "ฬ"}) + _KOB_CHARS: frozenset[str] = frozenset({"บ", "ป", "พ", "ฟ", "ภ"}) + + # Karu / Lahu prosody + _LONG_VOWELS: frozenset[str] = frozenset({ + "อา", "อี", "อือ", "อู", "เอ", "แอ", "เออ", "โอ", "ออ", "เอีย", "เอือ", "อัว" + }) + _SPECIAL_VOWELS: frozenset[str] = frozenset({"อำ", "ไอ", "เอา"}) + _EXPLICIT_SARA_WORDS: frozenset[str] = frozenset({"เออะ", "เออ", "เอ", "เอะ", "เอา", "เอาะ"}) + def __init__(self) -> None: """Initialize the KhaveeVerifier class.""" @@ -134,7 +165,7 @@ def _is_true_final(self, word: str) -> bool: # Guard Clauses: If it's not ending in ล, ร, ว, or doesn't have exactly 2 # consonants, or lacks pre-posed vowels, it bypasses the cluster checks. # --------------------------------------------------------------------- - if last_char not in {"ล", "ร", "ว"} or len(consonants) != 2: + if last_char not in "ลรว" or len(consonants) != 2: return True # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) @@ -144,23 +175,18 @@ def _is_true_final(self, word: str) -> bool: return True # Check for ล - if last_char == "ล" and cluster in { - "กล", "ขล", "คล", "ปล", "ผล", "พล", - "หล", "ถล", "ฉล", "สล", "ศล", "ตล"}: + if last_char == "ล" and cluster in self._LAM_CLUSTERS: # Exception 'เพล' - แม่กน (monk food ฉันเพล) Returns True, otherwise False return word == "เพล" # Check for ร - if last_char == "ร" and cluster in { - "กร", "ขร", "คร", "ตร", "ปร", "พร", - "ฟร", "บร", "ศร", "สร", "หร"}: + if last_char == "ร" and cluster in self._RUA_CLUSTERS: return False # Check for ว (ควบแท้ and อักษรนำ) if last_char == "ว": # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) - if (cluster in {"กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว"} - and ("ไ" in word or "ใ" in word)): + if ("ไ" in word or "ใ" in word) and (cluster in self._WA_CLUSTERS): return False # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). @@ -168,7 +194,7 @@ def _is_true_final(self, word: str) -> bool: elif ("เ" in word or "แ" in word or "โ" in word): # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ - if original_word in {"เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"}: + if original_word in self._WA_WHITELIST: return False # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) @@ -340,18 +366,8 @@ def check_sara(self, word: str) -> str: sara = ["ไอ"] # In case of อ - if word == "เออะ": - sara = ["เออะ"] - elif word == "เออ": - sara = ["เออ"] - elif word == "เอ": - sara = ["เอ"] - elif word == "เอะ": - sara = ["เอะ"] - elif word == "เอา": - sara = ["เอา"] - elif word == "เอาะ": - sara = ["เอาะ"] + if word in self._EXPLICIT_SARA_WORDS: + sara = [word] # In case of เ-ือ if "เ" in word and "ื" in word and "อ" in word: @@ -474,7 +490,7 @@ def check_marttra(self, word: str) -> str: # (Words like นคร, มังกร, สุนทร will correctly BYPASS this and remain แม่กน) # Check for อักษรตัวเดียวแทนคำ Standalone words - if word in {"บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"}: + if word in self._SINGLE_CHAR_WORDS: return "กา" # ------------------------------------------------------------------------- @@ -500,7 +516,7 @@ def check_marttra(self, word: str) -> str: # Check for ไ/ใ if "ไ" in word or "ใ" in word: - if word[-1] not in {"ย", "ล", "ร", "ว"}: + if word[-1] not in "ยลรว": return "กา" elif not self._is_true_final(original_word): return "กา" @@ -513,45 +529,29 @@ def check_marttra(self, word: str) -> str: # Check for ตัวสะกด final consonants # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) + last_char = word[-1] if ( - word[-1] in {"า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"} - or ("ี" in word and "ย" == word[-1]) # Catch สระเอีย (เสีย, เมีย) - or ("ื" in word and "อ" == word[-1]) # Catch สระอือ (เรือ, เสือ) - or ("ั" in word and "ว" == word[-1]) # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) + last_char in self._OPEN_SYLLABLE_VOWELS + or ("ี" in word and last_char == "ย") # Catch สระเอีย (เสีย, เมีย) + or ("ื" in word and last_char == "อ") # Catch สระอือ (เรือ, เสือ) + or ("ั" in word and last_char == "ว") # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) ): return "กา" - elif word[-1] == "ง": + elif last_char == "ง": return "กง" - elif word[-1] == "ม": + elif last_char == "ม": return "กม" - elif word[-1] == "ย": + elif last_char == "ย": return "เกย" - elif word[-1] == "ว": + elif last_char == "ว": return "เกอว" - elif word[-1] in {"ก", "ข", "ค", "ฆ"}: + elif last_char in self._KOK_CHARS: return "กก" - elif word[-1] in { - "จ", - "ช", - "ซ", - "ฎ", - "ฏ", - "ฐ", - "ฑ", - "ฒ", - "ด", - "ต", - "ถ", - "ท", - "ธ", - "ศ", - "ษ", - "ส", - }: + elif last_char in self._KOD_CHARS: return "กด" - elif word[-1] in {"ญ", "ณ", "น", "ร", "ล", "ฬ"}: + elif last_char in self._KON_CHARS: return "กน" - elif word[-1] in {"บ", "ป", "พ", "ฟ", "ภ"}: + elif last_char in self._KOB_CHARS: return "กบ" else: return "กา" @@ -643,10 +643,8 @@ def check_karu_lahu(self, text: str) -> Union[str, bool]: sara = self.check_sara(text) if (marttra != "กา" - or (marttra == "กา" and sara in - {"อา", "อี", "อือ", "อู", "เอ", "แอ", - "เออ", "โอ", "ออ", "เอีย", "เอือ", "อัว"}) - or sara in {"อำ", "ไอ", "เอา"}): + or (marttra == "กา" and sara in self._LONG_VOWELS) + or sara in self._SPECIAL_VOWELS): return "karu" else: return "lahu" @@ -716,9 +714,8 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: return "Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." try: - # Normalize spacing with regex Splits by spaces, - # newlines, or transitions between phrases - waks = [w for w in re.split(r'\s+', text.strip()) if w] + # Normalize spacing and split phrases/sentences across arbitrary whitespace + waks = text.split() # Ensure the poem has complete stanzas (4 waks per stanza) if len(waks) % 4 != 0 or len(waks) == 0: return "The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค)." @@ -891,8 +888,8 @@ def handle_karun_sound_silence(self, word: str) -> str: return word[:-3] # For Standard Karun silent suffixes (1 Consonant + Optional Vowel + Karun) - # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (ธุ์), สิทธิ์ (ธิ์) - # Check if there is an upper/lower vowel right before the Karun (ธุ์, ธิ์) + # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (พันธุ์), สิทธิ์ (ธิ์) + # Check if there is an upper/lower vowel right before the Karun (พันธุ์, ธิ์) if len(word) >= 3 and word[-2] in {"ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"}: return word[:-3] else: From ecb8a74f1ecb5edaaa4a38b9acd43dc220034656 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 29 Jul 2026 06:00:26 +0900 Subject: [PATCH 42/43] chore(khavee): remove unused import re, trailing whitespace --- pythainlp/khavee/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index d5a90ae5a..fbce5bd36 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -4,7 +4,6 @@ # ruff: noqa: C901 from __future__ import annotations -import re from typing import Union from pythainlp import thai_consonants @@ -423,7 +422,7 @@ def check_sara(self, word: str) -> str: sara = ["อะ"] if not sara: - # Fallback: If no explicit vowel is found and it doesn't fit closed-syllable + # Fallback: If no explicit vowel is found and it doesn't fit closed-syllable # reductions, assume the implied short 'a' (เสียงอะกึ่งมาตรา). return "อะ" From 7d2769c8c38598896304c2a4cf0bd19dd1a95003 Mon Sep 17 00:00:00 2001 From: Warit Yuvaniyama <6622770459@g.siit.tu.ac.th> Date: Wed, 29 Jul 2026 06:24:50 +0900 Subject: [PATCH 43/43] fix(khavee): remove redundant marttra check, fix assertEqual arg order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the redundant marttra == "กา" guard from check_karu_lahu -- the condition marttra != "กา" already short-circuits, making the explicit marttra check in the second branch dead code. Swap assertEqual arguments in test_check_klon to match the (result, expected) convention used consistently throughout the rest of the file. --- pythainlp/khavee/core.py | 8 +++++--- tests/extra/test_khavee_extended.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index fbce5bd36..69b8a344d 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -641,9 +641,11 @@ def check_karu_lahu(self, text: str) -> Union[str, bool]: marttra = self.check_marttra(text) sara = self.check_sara(text) - if (marttra != "กา" - or (marttra == "กา" and sara in self._LONG_VOWELS) - or sara in self._SPECIAL_VOWELS): + if ( + marttra != "กา" + or sara in self._LONG_VOWELS + or sara in self._SPECIAL_VOWELS + ): return "karu" else: return "lahu" diff --git a/tests/extra/test_khavee_extended.py b/tests/extra/test_khavee_extended.py index 013b656ff..f6f76d81d 100644 --- a/tests/extra/test_khavee_extended.py +++ b/tests/extra/test_khavee_extended.py @@ -211,7 +211,7 @@ def test_check_klon(self): ) result = self.kv.check_klon(poem, k_type=4) self.assertIsInstance(result, str) - self.assertEqual("The poem is correct according to the principle.", result) + self.assertEqual(result, "The poem is correct according to the principle.") poem_invalid = ( "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไล่ น้องหมาน้ำทอง "