Download src/scripts/syl_align_v2.py from voraseth/klon8-base: direct link, hf CLI and curl.
- Browser
- Download file 6.57 kB
-
https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/syl_align_v2.py
- Command line
-
hf download hf://voraseth/klon8-base/src/scripts/syl_align_v2.py
-
curl -L -o syl_align_v2.py https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/syl_align_v2.py
6.57 kB
| """Surface syllables, v2: scripts/syl_align.py with two alignment bugs fixed (FIX 4, 2026-09-28 data audit). | |
| v1 stays untouched so every model trained on it remains reproducible; models trained with --seg aligned2 | |
| use this module. | |
| Bug (a): TLTK writes the velar nasal onset ง as "N", v1's tables said "ng", so no syllable starting with ง | |
| or หง could ever be matched; every multi-syllable word containing one (ทำงาน, สงบ, เหงาหงอย) failed to | |
| align and the whole บท fell out of training at the count filter (poems with ทำงาน were dropped 96.5% of the | |
| time against 14.5% overall). | |
| Bug (b): v1 cut at the FIRST letter spelling the next syllable's onset. When the previous syllable ends in | |
| a consonant that is the same letter as the next onset (วันนี้, สัญญา, จากกัน, ศรัทธา) the cut landed one | |
| letter early: วั|นนี้, สั|ญญา, จา|กกัน - count-correct, so the filter kept them, and the vocabulary learned | |
| fake units (วั, นนี้, กกัน). v2 first requires the piece before a cut to contain a letter that can spell the | |
| previous syllable's coda (or a vowel spelling that carries it: เ-า = w, ไ/ใ = j, -ำ = m); if no cut | |
| satisfies that for the whole word (a letter shared by coda and onset, e.g. ชลบุรี), it falls back to v1's | |
| rule for that word, so v2 never aligns fewer words than v1. | |
| ๆ / ฯ are not changed here: klon8.prosody drops them from the pronunciation, so a line containing ๆ has a | |
| cell count that differs from its pronounced count whichever way the mark is handled; changing that belongs | |
| to the prosody checker, not to the segmenter (one change class per experiment). | |
| """ | |
| import sys, re, functools | |
| sys.path.insert(0, "."); sys.path.insert(0, "scripts") | |
| from klon8 import prosody as P | |
| import syl_align as V1 | |
| LEAD_VOWEL = V1.LEAD_VOWEL | |
| ONSET = dict(V1.ONSET, **{"ง": "N"}) | |
| DIGRAPH = dict(V1.DIGRAPH, **{"หง": "N"}) | |
| # อ นำ ย (อย่า อยู่ อย่าง อยาก) is a digraph only before ่ ู or า; elsewhere อ is a vowel (หรือ|ยัง, ขอ|ยืม) | |
| AY_NEXT = set("ู่า") | |
| # letters that can spell each coda phone TLTK writes | |
| CODA_LETTERS = { | |
| "k": set("กขคฆ"), "t": set("ดตทธฎฏฐฑฒถจชซศษส"), "p": set("บปพฟภ"), | |
| "n": set("นณญรลฬ"), "m": set("ม"), "N": set("ง"), "j": set("ย"), "w": set("ว"), | |
| } | |
| def onset_at(w, i): | |
| out = [] | |
| if w[i:i + 2] == "อย" and i + 2 < len(w) and w[i + 2] in AY_NEXT: out.append(("j", 2)) | |
| if i + 1 < len(w) and w[i:i + 2] in DIGRAPH: out.append((DIGRAPH[w[i:i + 2]], 2)) | |
| if w[i] in ONSET: out.append((ONSET[w[i]], 1)) | |
| return out | |
| def spells_coda(piece, coda): | |
| """Can this written piece carry the coda phone? The first letter is the onset and does not count.""" | |
| if not coda or coda == "?": return True | |
| q = 1 if piece and piece[0] in LEAD_VOWEL else 0 # skip a leading vowel | |
| q += 2 if (piece[q:q + 2] in DIGRAPH or piece[q:q + 2] == "อย") else 1 # then the whole onset cluster | |
| body = piece[q:] | |
| if CODA_LETTERS.get(coda, set()) & set(body): return True | |
| if coda == "w" and "เ" in piece and "า" in piece: return True # เ-า | |
| if coda == "j" and (piece[:1] in "ไใ"): return True # ไ, ใ | |
| if coda == "m" and "ำ" in piece: return True # -ำ | |
| return False | |
| def _align(w, onsets, codas, strict): | |
| starts, k, i = [0], 1, 1 | |
| while i < len(w) and k < len(onsets): | |
| for cand, n in onset_at(w, i): | |
| if cand != onsets[k]: continue | |
| begin = i - 1 if (i and w[i - 1] in LEAD_VOWEL) else i | |
| if begin <= starts[-1]: continue | |
| if codas[k - 1] and begin - starts[-1] < 2: continue | |
| if strict and not spells_coda(w[starts[-1]:begin], codas[k - 1]): continue | |
| starts.append(begin); k += 1; i = begin + n | |
| break | |
| else: | |
| i += 1 | |
| continue | |
| if len(starts) != len(onsets): return () | |
| return tuple(w[a:b] for a, b in zip(starts, starts[1:] + [len(w)])) | |
| def split_word(w): | |
| syls = P.g2p_line(w) | |
| if not syls: return () | |
| onsets = [x.onset for x in syls]; codas = [x.coda for x in syls] | |
| if len(onsets) <= 1: return (w,) | |
| return _align(w, onsets, codas, True) or _align(w, onsets, codas, False) | |
| def syls_aligned(line, limit=None): | |
| from pythainlp.tokenize import word_tokenize | |
| out = [] | |
| for w in word_tokenize(line, engine="newmm"): | |
| if not re.search(r"[ก-๙]", w): continue | |
| got = split_word(w) | |
| out.extend(got if got else [w]) | |
| return out[:limit] if limit else out | |
| CASES = { # word -> hand-checked split | |
| "ทำงาน": "ทำ|งาน", "สงบ": "ส|งบ", "เหงาหงอย": "เหงา|หงอย", "วันนี้": "วัน|นี้", "ความหมาย": "ความ|หมาย", | |
| "สัญญา": "สัญ|ญา", "จากกัน": "จาก|กัน", "ศรัทธา": "ศรัท|ธา", "บ้านนอก": "บ้าน|นอก", "เนิ่นนาน": "เนิ่น|นาน", | |
| "ชีวิต": "ชี|วิต", "เสมอ": "เส|มอ", "ออดอ้อน": "ออด|อ้อน", "ถวิล": "ถ|วิล", "พันธนา": "พัน|ธ|นา", | |
| "ปรารถนา": "ปราร|ถ|นา", "อารมณ์": "อา|รมณ์", "เลิกกัน": "เลิก|กัน", "กลับบ้าน": "กลับ|บ้าน", | |
| "หลายอย่าง": "หลาย|อย่าง", "อ้อยอิ่ง": "อ้อย|อิ่ง", "สงวน": "ส|งวน", "ฉงน": "ฉ|งน", | |
| "หรือยัง": "หรือ|ยัง", "ขอยืม": "ขอ|ยืม", "ยื้อยุด": "ยื้อ|ยุด", "ถือยศ": "ถือ|ยศ", "ปรนนิบัติ": "ปรน|นิ|บัติ", | |
| "กลืนน้ำ": "กลืน|น้ำ", | |
| } | |
| if __name__ == "__main__": | |
| ok = 0 | |
| for w, want in CASES.items(): | |
| got = "|".join(split_word(w)); old = "|".join(V1.split_word(w)) | |
| ok += got == want | |
| print(f" {'OK ' if got == want else 'BAD'} {w:10s} v2 {got:14s} v1 {old:14s} want {want}") | |
| print(f"{ok}/{len(CASES)} hand cases") | |