Download src/scripts/fixbase_data.py from voraseth/klon8-base: direct link, hf CLI and curl.
- Browser
- Download file 7.63 kB
-
https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fixbase_data.py
- Command line
-
hf download hf://voraseth/klon8-base/src/scripts/fixbase_data.py
-
curl -L -o fixbase_data.py https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fixbase_data.py
7.63 kB
| """Bug fixes for the canvas data path (runs/CSEL/FIXED_BASE_PREREG.md), imported by the patched scripts/canvas_bot.py. | |
| 1. ๆ / ฯ (data audit 2026-09-28: 1,137+ training poems dropped). klon8.prosody deletes ๆ, ฯ and ๏ before G2P, but | |
| the segmenter keeps them as cells (or glues ๆ to its word), so the cell count never matched the pronounced count | |
| and the poem was filtered out - while a poet counts ไม้ยมก as a repeated syllable (เก่งๆ = เก่ง เก่ง). prep_line | |
| drops ฯ / ๏ (end marks, not pronounced); expand_yamok writes ๆ out as a copy of the syllable before it, so the | |
| cells, the joined text the checker reads, and the pronunciation agree. Approximation (stated): ๆ repeats the | |
| preceding SYLLABLE; for a multi-syllable word it repeats only its last syllable. | |
| 2. Rime / tone labels in context (data audit: canvas_mlm.phon labels each vocabulary type by G2P of the syllable | |
| ALONE - ส of สมอง gets the letter name, วั no label, มอ of เสมอ the wrong rime; 3.9% of cells mislabelled, | |
| 2.1% rime / 2.8% tone at line ends where the rhyme and tone masks act). ctx_phon_table labels each training cell | |
| by G2P of its whole line (cells align 1:1 with the line's G2P syllables: load_poems keeps only lines where they | |
| do) and gives every type its MAJORITY in-context (rime, tone); types never seen in a poem line keep the old | |
| out-of-context label. apply_phon rebuilds V.rime / V.tone / V.fam / V.n_rime from that table deterministically. | |
| """ | |
| import collections, sys | |
| if "." not in sys.path: sys.path.insert(0, ".") # run as a script from the repo root | |
| from klon8 import prosody as P | |
| def prep_line(line): | |
| """ฯ: 896 of 1,130 in the train split follow a word (mostly line/poem-end marks after names and เทอญ; ข้าฯ | |
| ~34), 225 stand alone, 9 are ฯลฯ. Choice (stated in the prereg): drop ฯ and read the short form (ข้าฯ = ข้า, | |
| กรุงเทพฯ = กรุงเทพ), NOT ข้าพเจ้า and not a token of its own - klon8.prosody pronounces the short form, and | |
| the count filter keeps only poems whose metre agrees with that reading. ฯลฯ is dropped whole.""" | |
| return line.replace("ฯลฯ", "").replace("ฯ", "").replace("๏", "") | |
| def expand_yamok(cells): | |
| """ๆ repeats the syllable before it, wherever it sits in a cell: ดีๆ -> ดี ดี; ๆคน after ดี -> ดี คน | |
| (review: a leading ๆ repeats the PREVIOUS cell, not the rest of its own cell); ดีๆๆ -> ดี ดี ดี.""" | |
| out = [] | |
| for c in cells: | |
| for j, part in enumerate(c.split("ๆ")): | |
| if j and out: out.append(out[-1]) | |
| if part: out.append(part) | |
| return out | |
| def contract_yamok(line, seg_fn, words=None): | |
| """Output side (review): write reduplication back as ๆ for the judge, since the human anchors use ๆ. | |
| Adjacent equal cells X X -> Xๆ (X X X -> Xๆๆ), the exact inverse of expand_yamok, except where XX is itself | |
| a dictionary word written without ๆ (นานา, จะจะ; pythainlp thai_words). The pipeline measures stutters on the | |
| written-out text BEFORE contraction, so a contracted stutter is still counted by the gates.""" | |
| if words is None: | |
| from pythainlp.corpus import thai_words | |
| words = thai_words() | |
| out = [] | |
| for c in seg_fn(line): | |
| if out and out[-1].rstrip("ๆ") == c and c + c not in words: out[-1] += "ๆ" | |
| else: out.append(c) | |
| return "".join(out) | |
| def redup_syllables(path="data/modern_bot_train.jsonl"): | |
| """Syllables the training poems write with ไม้ยมก (the last aligned2 syllable of the word before ๆ).""" | |
| import re, json, syl_align_v2 | |
| out = set() | |
| for l in open(path, encoding="utf-8"): | |
| for w in re.findall(r"([ก-ฮะ-ฺเ-ๅ็-์]+)\s*ๆ", json.loads(l)["poem"]): | |
| s = syl_align_v2.syls_aligned(w, 12) | |
| if s: out.add(s[-1]) | |
| return out | |
| def title_cells(seg_fn, text, tl): | |
| """Title / topic cells for a --yamok model: same ๆ/ฯ handling as the poem lines.""" | |
| return expand_yamok(seg_fn(prep_line(text), tl))[:tl] | |
| def _key(s): | |
| return (P._MERGED_RIME.get(s.rime, s.rime), s.tone) | |
| def _majority(votes, fallback): | |
| """Most frequent label; a tie keeps the out-of-context label if it is among the tied, else the smallest by | |
| repr - so the table does not depend on the order of the poems (train() shuffles them).""" | |
| top = max(votes.values()); tied = [k for k, n in votes.items() if n == top] | |
| return fallback if fallback in tied else min(tied, key=repr) | |
| def ctx_phon_table(poems, phon): | |
| """{syllable: (rime_key, tone)} = majority in-context label; also returns the measured label error rates.""" | |
| votes = collections.defaultdict(collections.Counter) | |
| cells = [] # (syllable, in-context label) for the rate measurement | |
| for p in poems: | |
| for g in p["grid"]: | |
| syl = P.g2p_line("".join(g)) | |
| if len(syl) != len(g): continue | |
| for c, s in zip(g, syl): | |
| k = _key(s); votes[c][k] += 1; cells.append((c, k)) | |
| table = {c: _majority(v, phon(c)) for c, v in votes.items()} | |
| old = sum(1 for c, k in cells if phon(c) != k) | |
| new = sum(1 for c, k in cells if table[c] != k) | |
| return table, {"cells": len(cells), "out_of_context_mismatch": old / max(len(cells), 1), | |
| "type_majority_mismatch": new / max(len(cells), 1), "types": len(table)} | |
| def apply_phon(V, table, phon): | |
| """V.rime/V.tone/V.fam/V.n_rime from table (fallback: out-of-context phon). Same id order as load_model.""" | |
| rimes = {}; V.rime, V.tone, V.fam = [0] * len(V.itos), [5] * len(V.itos), {} | |
| for i, t in enumerate(V.itos): | |
| if i < 4: continue | |
| r, tn = table.get(t) or phon(t) | |
| if r is None: continue | |
| rimes.setdefault(r, len(rimes) + 1); V.rime[i], V.tone[i] = rimes[r], tn | |
| V.fam.setdefault(rimes[r], []).append(i) | |
| V.n_rime = len(rimes) + 1 | |
| def dump_table(V, table, phon): | |
| """JSON-safe table over the whole vocabulary (rime keys are tuples).""" | |
| out = {} | |
| for t in V.itos[4:]: | |
| r, tn = table.get(t) or phon(t) | |
| out[t] = None if r is None else [list(r) if isinstance(r, tuple) else r, tn] | |
| return out | |
| def load_table(d): | |
| return {t: ((tuple(v[0]) if isinstance(v[0], list) else v[0]), v[1]) for t, v in d.items() if v is not None} | |
| def freq_table(poems): | |
| """{"total", "count"} over the poem cells, in the same format as data/syl_freq_train.json (which counts the | |
| v1 segmenter's units, so most aligned2 syllables would fall back to the floor in --lm-head-init's bias).""" | |
| cnt = collections.Counter(c for p in poems for g in p["grid"] for c in g) | |
| return {"total": sum(cnt.values()), "count": dict(cnt)} | |
| if __name__ == "__main__": | |
| # fixbase_data.py freq DATA OUT: unigram table of the FIXED training cells (aligned2 + ๆ/ฯ), from the patched | |
| # scripts/canvas_bot.py load_poems, so the table and the model's units cannot disagree | |
| import json | |
| assert sys.argv[1] == "freq", __doc__ | |
| sys.path[:0] = [".", "scripts"] | |
| import canvas_bot as CB | |
| assert "clean_titles" in CB.load_poems.__code__.co_varnames, "canvas_bot.py is not patched with patch_fixed_base.py" | |
| d = freq_table(CB.load_poems(sys.argv[2], seg="aligned2", clean_titles=True, yamok=True)) | |
| json.dump(d, open(sys.argv[3], "w", encoding="utf-8"), ensure_ascii=False) | |
| print(f" {sys.argv[3]}: {d['total']:,} cells, {len(d['count']):,} types") | |