Skip to content

Fix the logic of check_sara, check_marttra, is_sumpus, handle_karun_sound_silence#1453

Open
Warit-Yuv wants to merge 39 commits into
PyThaiNLP:mainfrom
Warit-Yuv:patch-1
Open

Fix the logic of check_sara, check_marttra, is_sumpus, handle_karun_sound_silence#1453
Warit-Yuv wants to merge 39 commits into
PyThaiNLP:mainfrom
Warit-Yuv:patch-1

Conversation

@Warit-Yuv

@Warit-Yuv Warit-Yuv commented Jul 19, 2026

Copy link
Copy Markdown

Description

This pull request addresses several critical defects and bugs in the Thai poetry/rhyme processing submodule pythainlp/khavee/core.py. The modifications improve vowel (check_sara) detection, syllable spelling section (check_marttra) classification, rhyming logic (is_sumpus), and silent suffix ตัวการันต์ stripping (handle_karun_sound_silence) to ensure strict compliance with standard Thai grammar and poetic constraints.

1. Complex Silent Suffix Handling (handle_karun_sound_silence)

  • Problem: The previous hardcoded string truncation logic (word[:-2]) assumes that all silent markers cover exactly one consonant + one Karun (-์). It fails entirely on multi-letter silent suffixes or clusters containing quiet vowels (e.g., ธุ์, ธิ์, ตริย์), leaving leftover unpronounced letters that disrupt vowel and syllable checks.
  • Solution: Expanded the function to properly strip complex, multi-character silent clusters before evaluating vowels or spelling sections.
# Fixed edge cases for silent suffixes:
kv.handle_karun_sound_silence("จันทร์")      # "จันท" -> "จัน"
kv.handle_karun_sound_silence("สิทธิ์")       # "สิทธ" -> "สิท"
kv.handle_karun_sound_silence("กษัตริย์")     # "กษัตริ" -> "กษัต"
kv.handle_karun_sound_silence("พระลักษมณ์") # "พระลักษม" -> "พระลัก"

2. Initial Cluster and Semi-Vowel Separation (_is_true_final)

  • Problem: The deprecated internal method _has_true_final_yl did not check for tone marks (วรรณยุกต์), failing on target words like ใกล้ because the string ends with a diacritic (วรรณยุกต์) instead of a consonant. It also misclassified instances where a letter functioned as a vowel component rather than a true final consonant ตัวสะกด (e.g., เสีย, ไทย).
  • Solution: Added a unified internal function _is_true_final that strips ตัวการันต์ and วรรณยุกต์ first, then systematically filters out native Thai คำควบกล้ำ (true clusters) and อักษรนำ (leading consonants) paired with pre-posed vowels (เ-, แ-, โ-, ไ-, ใ-). This prevents semi-vowels and liquids (, , ) functioning as initial consonant clusters from being falsely flagged as final consonants.
words_to_test = ["เสีย","ไกล","ไทย","ใกล้","ไกว","ใคร","แปร"]
for word in words_to_test:
    print(f"Word: {word}, true final: {kv._has_true_final_yl(word)}, Sara: {kv.check_sara(word)}, Marttra: {kv.check_marttra(word)}")
"""
# Previous:
Word: เสีย, true final: True, Sara: เอีย, Marttra: กา
Word: ไกล, true final: True, Sara: ไอ, Marttra: เกย
Word: ไทย, true final: True, Sara: ไอ, Marttra: เกย
Word: ใกล้, true final: False, Sara: ไอ, Marttra: เกย
Word: ไกว, true final: False, Sara: ไอ, Marttra: กา
Word: ใคร, true final: False, Sara: ไอ, Marttra: กา
Word: แปร, true final: False, Sara: แอ, Marttra: กน
# After:
Word: เสีย, true final: False, Sara: เอีย, Marttra: กา
Word: ไกล, true final: False, Sara: ไอ, Marttra: กา
Word: ไทย, true final: False, Sara: ไอ, Marttra: กา
Word: ใกล้, true final: False, Sara: ไอ, Marttra: กา
Word: ไกว, true final: False, Sara: ไอ, Marttra: กา
Word: ใคร, true final: False, Sara: ไอ, Marttra: กา
Word: แปร, true final: False, Sara: แอ, Marttra: กา
"""

3. Syllable Spelling Section Corrections (check_marttra)

  • Problem: Words disguised by initial consonant clusters (คำควบกล้ำ) wrapped in pre-posed vowels (e.g., เขว, แปร, ไถล) were previously misclassified into incorrect closed-syllable spelling sections มาตราตัวสะกด (e.g., แม่เกอว, แม่กน) instead of แม่ ก กา. Standalone words representing individual letters (e.g., ธ, ณ) were also assigned to closed categories.
  • Solution: Updated check_marttra to verify tokens against _is_true_final. Lexical clusters like โปร, แปร, ไกล, ไกว, and เปล are now correctly routed into the open-syllable แม่ ก กา category. Standalone characters like , , พณ, and are also normalized to แม่ ก กา.
  • Note on Loanwords: English loanwords ending in an "L" sound () (e.g., แอปเปิล, โมเดล, เลเวล, ฟุตบอล) will continue to default to แม่กน under this orthographic checker. Improvement could be made to handle poetic variations that treat these as แม่เกอว for rhyming purposes.
# Before
kv.check_marttra("พาล") -> "เกย" (Misclassified final liquid)
kv.check_marttra("ธ")   -> "กด"  (Incorrect assignment of standalone token)

# After
kv.check_marttra("พาล") -> "กน"  (Correct final mapping)
kv.check_marttra("ธ")   -> "กา"  (Correct open syllable mapping)

4. Vowel Invariant and Transformation Rules (check_sara)

  • Problem: Sequential loop conditions shadowed each other (causing bugs like the "อัว" parsing defect), and unsafe list operations were applied when merging raw "เ" characters into "แ".
  • Solution: Standardized the processing loop to cleanly handle compound vowels, fixed the shadowing logic, and added fast substring constraints for the variant realizations of "ฤ" and "ฦ" (เออ, อิ, อึ).
  • Transformed and Reduced Vowels: Integrated complete mapping conditions for reduced vowels (สระลดรูป) and transformed architectures (สระเปลี่ยนรูป) using the Mai Tai Khu diacritic (-็). This fixes errors for common vocabularies:
# Handling สระลดรูป and สระเปลี่ยนรูป ensures that the vowel sounds are correctly identified for proper rhyming and syllable classification.
words_to_test = ["เลย", "พวก", "เจ็ด", "แข็ง", "ก็"]
for word in words_to_test:
    print(f"Word: {word}, Sara: {kv.check_sara(word)}, Marttra: {kv.check_marttra(word)}")
"""
# Previous:
Word: เลย, Sara: เอ, Marttra: เกย
Word: พวก, Sara: อัว, Marttra: กก
Word: เจ็ด, Sara: เอ, Marttra: กด
Word: แข็ง, Sara: แอ, Marttra: กง
Word: ก็, Sara: ออ, Marttra: กา
# After:
Word: เลย, Sara: เออ, Marttra: เกย
Word: พวก, Sara: อัว, Marttra: กก
Word: เจ็ด, Sara: เอะ, Marttra: กด
Word: แข็ง, Sara: แอะ, Marttra: กง
Word: ก็, Sara: เอาะ, Marttra: กา

5. Control Flow Refactoring for Rhyme Matching (is_sumpus)

  • Problem: The sequential elif structure allowed arguments to short-circuit early, which caused asymmetric matching failures if word1 normalized but word2 did not.
  • Solution: Replaced the conditional tree with isolated if statements so both inputs undergo normalization prior to final comparison. This cleanly aligns variations like "อะ" + "แม่เกย" into "ไอ" + "แม่กา", and bridges the poetic rhyming concordance between "อรรม", "อัม", and "อำ" (e.g., ธรรม - สัม - จำ).

Verification and Test Cases

The updated core class execution demonstrates proper categorization across vowel mutation, syllable validation, and cluster handling:

# Syllable, Cluster, & Rhyme Validation
print(kv.check_marttra("พาล"))       # Output: "กน"
print(kv.check_marttra("ไกล"))       # Output: "กา"
print(kv.check_marttra("ไกว"))       # Output: "กา"
print(kv.check_marttra("โปร"))       # Output: "กา"

print(kv.is_sumpus("บ้าน", "พาล"))  # Output: True
print(kv.is_sumpus("ธรรม", "สัม"))  # Output: True
print(kv.is_sumpus("ธรรม", "จำ"))   # Output: True
print(kv.is_sumpus("กัย", "ไก"))    # Output: True
print(kv.is_sumpus("ใจ", "ไทย"))    # Output: True

# Silent Suffix Truncation Evaluation
karun_examples = ["อินทรีย์", "กษัตริย์", "ไปรษณีย์", "ฤทธิ์", "จันทร์", "ภาพยนตร์", "กาสาวพัสตร์"]
for word in karun_examples:
    print(f"{word} -> {kv.handle_karun_sound_silence(word)}")

… แม่กน with vowel า (previously mistaken as แม่เกย)

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.
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.
… ริ, รึ

ฤ not only have "รึ" sound like ฤดู ฤทัย คฤหาสน์
but also can have "เรอ" sound from ฤกษ์ (เริก) and "ริ" sound like ฤทธิ์, อังกฤษ, ตฤณ

This commit add additional check for ฤ sounds.
Add exception for 'อึ' sound in specific พฤทธิธรรม words and delete the old duplicated logic.
finetune the ฤ logic and patching the edge cases.
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.
Change 'แ' to 'แอ' in merging logic
Fix the discrepancy of อรรม - อัม - อำ.
Normalize 'อะ' and 'เกย' to 'ไอ' and 'กา', and 'อำ' and 'อะ' to 'อำ' and 'กา'.
@Warit-Yuv Warit-Yuv changed the title Fix check_marttra logic for character ล with vowel า Fix the logic of check_sara, check_marttra, is_sumpus, handle_karun_sound_silence Jul 19, 2026
Remove Trailing whitespace in line 693 so the auto code reviewer wouldn't flag as an issue.
…ด Standalone words in check_mattra

- 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 แม่ ก กา.
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.
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.
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates pythainlp/khavee/core.py to improve Thai poetic-verification logic in KhaveeVerifier, focusing on (1) detecting whether trailing ย/ล/ร/ว are true finals vs. parts of clusters/vowels, (2) refining vowel (check_sara) and spelling-section (check_marttra) classification, (3) normalizing rhyme matching (is_sumpus), and (4) expanding Karun (์) silent-suffix stripping.

Changes:

  • Introduces _is_true_final() (and routes _has_true_final_yl() through it) to better distinguish true final consonants from initial clusters and vowel components.
  • Refactors check_sara() to handle more compound/transformed vowel cases (including ไม้ไต่คู้ -็) and extends handling for ฤ/ฦ patterns.
  • Adjusts check_marttra() and is_sumpus() to better normalize open-syllable detection and rhyming equivalences, plus extends handle_karun_sound_silence() for more complex Karun suffix patterns.

Comment thread pythainlp/khavee/core.py Outdated
Comment thread pythainlp/khavee/core.py Outdated
Comment thread pythainlp/khavee/core.py Outdated
Comment on lines +252 to +256
if word == "ฤก" or word.startswith("ฤกษ"):
sara.append("เออ")
# for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ)
elif any(ex in word for ex in ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")):
sara.append("อิ")
Comment thread pythainlp/khavee/core.py
@bact bact added the bug bugs in the library label Jul 20, 2026
Warit-Yuv and others added 12 commits July 21, 2026 11:37
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.
…eck_sara` and `check_mattra`

Added handling for silent terminal vowels in Pali/Sanskrit words.
change ตริ to ตรี to match correctly with the สระ "อี" assertion in the test case.
…ng against "ว" and add unit tests for edge cases
…ied as แม่กม and fix check sara with สระอัว (ตัว ครัว บัว)
…n dealing with อักษรนำ, คำควบกล้ำ, สระอำ, and many sound of ฤ
…al require word before tonemark get stripped to evaluate some tone-dependent structures.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

pythainlp/khavee/core.py:366

  • silent_vowel_exceptions is checked with endswith(), so any longer word that happens to end with one of these strings will have its final character removed, potentially misclassifying the spelling section. This looks like it should be an exact-word match.
        if any(word.endswith(ex) for ex in silent_vowel_exceptions):

Comment thread pythainlp/khavee/core.py
# 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):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wouldn't work. It is not as simple as list inclusion check. These word are silent when they act as suffix. Here are some example:
พระเกียรติ (พระ-เกียด), ธรรมชาติ (ทัม-มะ-ชาด), ปฏิบัติ (ปะ-ติ-บัด)
We cannot guaranteed that the tokenizer or people who use the library will break up all the word into chunks so we can't just use word_req in silent_vowel_exceptions.

Noted that it only silenced when act as suffix (endswith) not as infix/prefix -> ชาติพันธุ์ อ่านว่า "ชาด-ติ-พัน" (ไม่ใช่ ชาด-พัน)

Therefore the use of any(word_req.endswith(ex) for ex in silent_vowel_exceptions) here is justified.

Comment thread pythainlp/khavee/core.py
Comment thread tests/core/test_khavee.py Outdated

class KhaveeCheckKaruLahuTestCase(unittest.TestCase):
"""Tests for KhaveeVerifier.check_karu_lahu"""
class KhaveeCheckKaruLahuTestCase(unittest.TestCase):

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread pythainlp/khavee/core.py Outdated
Comment on lines 168 to 174
elif i == "็":
sara.append("ออ")
sara.append("")
elif "รร" in word:
if self.check_marttra(word) == "กม":
sara.append("อำ")
else:
sara.append("อะ")

@Warit-Yuv Warit-Yuv Jul 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"รร" should not be evaluated on a character-by-character basis inside that loop. I will push a fix.

Comment thread pythainlp/khavee/core.py
Comment thread tests/core/test_khavee.py
Comment thread tests/core/test_khavee.py
Comment thread pythainlp/khavee/core.py
"""
return self._is_true_final(word)

def _is_true_final(self, word: str) -> bool:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible to refactor and reduce the cognitive complexity down to 15. To properly detect if the word is a true final or not really require that many logic check. Only way to reduce is to split the function into many subfunction which in my opinion is worse.

Comment thread pythainlp/khavee/core.py
# 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):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wouldn't work. It is not as simple as list inclusion check. These word are silent when they act as suffix. Here are some example:
พระเกียรติ (พระ-เกียด), ธรรมชาติ (ทัม-มะ-ชาด), ปฏิบัติ (ปะ-ติ-บัด)
We cannot guaranteed that the tokenizer or people who use the library will break up all the word into chunks so we can't just use word_req in silent_vowel_exceptions.

Noted that it only silenced when act as suffix (endswith) not as infix/prefix -> ชาติพันธุ์ อ่านว่า "ชาด-ติ-พัน" (ไม่ใช่ ชาด-พัน)

Therefore the use of any(word_req.endswith(ex) for ex in silent_vowel_exceptions) here is justified.

Comment thread pythainlp/khavee/core.py Outdated
Comment on lines 168 to 174
elif i == "็":
sara.append("ออ")
sara.append("")
elif "รร" in word:
if self.check_marttra(word) == "กม":
sara.append("อำ")
else:
sara.append("อะ")

@Warit-Yuv Warit-Yuv Jul 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"รร" should not be evaluated on a character-by-character basis inside that loop. I will push a fix.

รร should not be evaluated on a character-by-character basis and should be evaluate once right after the loop finishes
Refactor `check_karu_lahu`: streamline logic and enhance readability
Change list [] to set {} for string matching to be more optimized.
…timization

Refactor `check_klon` to be more optimized
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.
…date docstring

test_khavee_extended.py now correctly evaluate check_klon
@sonarqubecloud

Copy link
Copy Markdown

@wannaphong wannaphong added this to the 6.0 milestone Jul 23, 2026
@wannaphong wannaphong self-assigned this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug bugs in the library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants