Not an official script. Kam Mueang's own script is the Lanna script (ตัวเมือง, Tai Tham), and nothing on this page replaces it. The four Khmer letters of part 5 are a proposal, an attempt to capture sounds that Thai letters blur; the krueng.ai voice learns Kam Mueang as it is usually written, in Thai letters.นี่ไม่ใช่อักษรทางการ อักษรของคำเมืองเองคืออักษรธรรมล้านนา (ตัวเมือง) และไม่มีสิ่งใดในหน้านี้มาแทนที่อักษรนั้น ตัวอักษรเขมรสี่ตัวในส่วนที่ 5 เป็นเพียงข้อเสนอ เป็นความพยายามเก็บเสียงที่ตัวอักษรไทยแยกไม่ชัด ส่วนเสียงพูดของ krueng.ai เรียนรู้คำเมืองแบบที่เขียนกันทั่วไปด้วยตัวอักษรไทย这不是正式的文字。北泰语自己的文字是兰纳文(ตัวเมือง,Tai Tham),本页的任何内容都不取代它。第 5 部分的四个高棉文字母只是一个提议,尝试记下泰文字母分不清的音;krueng.ai 的语音学的是通常用泰文字母书写的北泰语。
🇬🇧 English
This page is about how Kam Mueang (คำเมือง), the language of Northern Thailand, shows its six tones in writing. Kam Mueang is written today mostly in Thai letters. They were made for Central Thai, which has five tones; Kam Mueang as it is spoken in Chiang Mai has six. The North's own script, the Lanna script (ตัวเมือง, also called Tai Tham), writes all six: it reads a tone from the group of the first letter and the mark over it. Those rules work on Thai letters too.
It is made for the krueng.ai Kam Mueang voice: a free voice, for anyone with a computer, that will learn to speak from recordings of a native speaker. The voice learns Kam Mueang as it is usually written in Thai letters, and the reader on this page, alphabet.js, is planned to turn that text into sounds and tones for its small voice.
On this page you will:
- see the six tones, and the two that Thai spelling cannot tell apart;
- learn how a tone is read from the letters: three letter groups and two tone marks, as in the Lanna script;
- type Kam Mueang, and see the tone of every syllable;
- see it written in the Lanna script, and how often that agrees with a dictionary;
- see a proposal of four Khmer letters for the sounds Thai letters blur, and what Chrome, Firefox and Safari's engine draw when letters of two scripts are mixed;
- see how much of the voice project's reading script already follows these rules.
You need nothing installed: the page brings its own font. The tone values are those of the Lanna dictionary of Chiang Mai Rajabhat University (2007), as Wikipedia quotes them. Every test on the page was run, and what it printed is shown. No native speaker has checked this page yet: it is a first draft.
🇹🇭 ไทย
หน้านี้ว่าด้วยวิธีที่คำเมือง ภาษาของภาคเหนือของไทย แสดงวรรณยุกต์ทั้งหกเสียงเมื่อเขียน ทุกวันนี้คำเมืองเขียนด้วยตัวอักษรไทยเป็นส่วนใหญ่ ตัวอักษรไทยทำมาสำหรับภาษาไทยกลาง ซึ่งมีห้าเสียงวรรณยุกต์ ส่วนคำเมืองที่พูดกันในเชียงใหม่มีหกเสียง อักษรของภาคเหนือเอง คืออักษรธรรมล้านนา (ตัวเมือง) เขียนได้ครบทั้งหก โดยอ่านวรรณยุกต์จากกลุ่มของพยัญชนะต้นกับรูปวรรณยุกต์ที่อยู่ข้างบน และกฎเหล่านี้ใช้กับตัวอักษรไทยได้ด้วย
หน้านี้ทำขึ้นเพื่อเสียงพูดคำเมืองของ krueng.ai ซึ่งเป็นเสียงฟรีสำหรับทุกคนที่มีคอมพิวเตอร์ และจะเรียนรู้การพูดจากเสียงที่อัดจากเจ้าของภาษา เสียงพูดนี้เรียนรู้คำเมืองแบบที่เขียนกันทั่วไปด้วยตัวอักษรไทย และตัวอ่านในหน้านี้ คือ alphabet.js มีแผนจะใช้แปลงข้อความนั้นเป็นเสียงและวรรณยุกต์ให้เสียงพูดรุ่นเล็ก
ในหน้านี้คุณจะ:
- ดูวรรณยุกต์ทั้งหก และสองเสียงที่การสะกดแบบไทยแยกไม่ออก
- เรียนวิธีอ่านวรรณยุกต์จากตัวอักษร: อักษรสามกลุ่มกับรูปวรรณยุกต์สองรูป แบบอักษรธรรมล้านนา
- ลองพิมพ์คำเมือง แล้วดูวรรณยุกต์ของทุกพยางค์
- ดูข้อความเดียวกันเขียนเป็นอักษรธรรมล้านนา และดูว่าตรงกับพจนานุกรมบ่อยแค่ไหน
- ดูข้อเสนอตัวอักษรเขมรสี่ตัว สำหรับเสียงที่ตัวอักษรไทยแยกไม่ชัด และดูว่า Chrome, Firefox และเอนจินของ Safari แสดงผลอย่างไรเมื่อตัวอักษรสองชุดผสมกัน
- ดูว่าบทอ่านของโครงการเสียงพูดเขียนตามกฎเหล่านี้อยู่แล้วมากแค่ไหน
ไม่ต้องติดตั้งอะไรเลย หน้านี้มีฟอนต์ของตัวเอง ค่าวรรณยุกต์มาจากพจนานุกรมภาษาล้านนาของมหาวิทยาลัยราชภัฏเชียงใหม่ (2007) ตามที่วิกิพีเดียอ้างไว้ การทดสอบทุกอย่างในหน้านี้รันจริง และแสดงสิ่งที่พิมพ์ออกมา ยังไม่มีเจ้าของภาษาตรวจหน้านี้ นี่คือฉบับร่างแรก
🇨🇳 中文
本页讲的是北泰语(คำเมือง,泰国北部的语言)在书写中怎样表示它的六个声调。如今北泰语大多用泰文字母书写。泰文字母是为中部泰语设计的,它有五个声调;而清迈的北泰语有六个。北部自己的文字兰纳文(ตัวเมือง,又称 Tai Tham)能写出全部六个:它根据首字母所属的组和上面的符号来读声调。这些规则对泰文字母同样适用。
它是为 krueng.ai 的北泰语语音而做的:一个免费的声音,任何有电脑的人都能用,它将从母语者的录音中学会说话。这个语音学的是通常用泰文字母书写的北泰语;本页的读音器 alphabet.js 计划为它的小型语音把这些文字转成读音和声调。
在这一页里,你会:
- 看到六个声调,以及泰文拼写分不开的两个;
- 学会怎样从字母读出声调:像兰纳文一样,三组字母加两个声调符号;
- 输入北泰语,看每个音节的声调;
- 看到它写成兰纳文,以及结果和词典一致的比例;
- 看一个提议:用四个高棉文字母写泰文字母分不清的音,以及两种文字的字母混在一起时,Chrome、Firefox 和 Safari 的引擎画出什么;
- 看看语音项目的朗读稿有多少已经符合这些规则。
你不需要安装任何东西:本页自带字体。声调的数值来自清迈皇家大学的《兰纳语词典》(2007),据维基百科的引用。页面上的每项测试都真正运行过,并展示了它打印出来的结果。还没有母语者检查过本页:这是第一稿。
Lanna, where Kam Mueang is spokenล้านนา ดินแดนที่พูดคำเมือง兰纳:说北泰语的地方
Kam Mueang is the language of Lanna, the old kingdom of the North. Here are four things Lanna is known for. The pictures are illustrations made with an image model (SDXL) by make_lanna_images.py, not photographs.คำเมืองคือภาษาของล้านนา อาณาจักรเก่าแก่ของภาคเหนือ ข้างล่างนี้คือสี่สิ่งที่ล้านนามีชื่อเสียง ภาพเหล่านี้เป็นภาพประกอบที่สร้างด้วยโมเดลสร้างภาพ (SDXL) โดย make_lanna_images.py ไม่ใช่ภาพถ่าย北泰语是兰纳——北部古老王国——的语言。下面是兰纳闻名的四样东西。这些图是 make_lanna_images.py 用图像模型(SDXL)生成的插图,不是照片。
1 · Six tones, and Thai letters show five · 1 · หกเสียงวรรณยุกต์ แต่ตัวอักษรไทยแสดงได้ห้า · 1 · 六个声调,泰文字母只写得出五个
🇬🇧 English
Thai spelling shows a tone with two things: the group the first letter belongs to (high, mid or low class), and the tone mark over it. Central Thai has five tones, and Thai spelling has just enough combinations to write five. Kam Mueang in Chiang Mai has six. Here they are, with the numbers and the examples of the Lanna dictionary (2007): the same syllable, law, said six ways.
🇹🇭 ไทย
การสะกดแบบไทยบอกวรรณยุกต์ด้วยสองอย่าง คือ กลุ่มของพยัญชนะต้น (อักษรสูง กลาง หรือต่ำ) และรูปวรรณยุกต์ที่อยู่บนพยัญชนะนั้น ภาษาไทยกลางมีห้าเสียงวรรณยุกต์ และการสะกดแบบไทยมีชุดผสมพอดีสำหรับห้าเสียง คำเมืองในเชียงใหม่มีหกเสียง ดูได้ในตารางข้างล่าง พร้อมหมายเลขและตัวอย่างจากพจนานุกรมภาษาล้านนา (2007): พยางค์ law พยางค์เดียว ออกเสียงได้หกแบบ
🇨🇳 中文
泰文拼写用两样东西表示声调:首辅音所属的组(高、中、低辅音),以及它上面的声调符号。中部泰语有五个声调,泰文拼写的组合刚好够写五个。清迈的北泰语有六个。下表列出这六个声调,编号和例词来自《兰纳语词典》(2007):同一个音节 law,有六种读法。
| Toneวรรณยุกต์声调 | Thai nameชื่อภาษาไทย泰文名称 | How it soundsฟังเป็นอย่างไร听起来怎样 | Lanna scriptอักษรธรรมล้านนา兰纳文 | Thai lettersอักษรไทย泰文字母 | Meaningความหมาย意思 |
|---|---|---|---|---|---|
| 1 | จัตวา | low, risingต่ำ แล้วขึ้น低升 ˨˦ | ᩉᩮᩖᩢᩣ | เหลา | sharpenเหลา (ทำให้แหลม)削尖 |
| 2 | เอก | low, fallingต่ำ แล้วตก低降 ˨˩ | ᩉᩮᩖᩢ᩵ᩣ | เหล่า | forest; groupป่า; หมู่คณะ树林;群 |
| 3 | ตรีปลายโท | high and level, ending in a catch of the throatสูงและราบ ปลายเสียงกักในลำคอ高平,结尾喉头一紧(喉塞) ˦˦ʔ | ᩉᩮᩖᩢ᩶ᩣ | เหล้า | liquorเหล้า酒 |
| 4 | สามัญ | mid and levelกลางและราบ中平 ˧˧ | ᩃᩮᩢᩣ | เลา | beautifulสวย美丽 |
| 5 | โท | high, fallingสูง แล้วตก高降 ˦˨ | ᩃᩮᩢ᩵ᩣ | เล่า | tell (a story)เล่า (เรื่อง)讲(故事) |
| 6 | ตรี | high, risingสูง แล้วขึ้น高升 ˦˥ | ᩃᩮᩢ᩶ᩣ | เล้า | coop (for chickens)เล้า (ไก่)(鸡)舍 |
🇬🇧 English
Written the Lanna way, with the letter the word has always had and its tone mark, the six never meet: เหล้า and เล่า differ in their first letter, ห before ล. The trouble starts when a word is written the way it sounds. Kam Mueang says the Thai letters ค ช ท พ without the puff of air Central Thai gives them: คน "person" sounds like กน, ช้าง "elephant" like จ้าง. Writers who want the reader to hear that use ก จ ต ป instead, and set the tone with Central Thai's marks: จ๊าง, ป้อ for พ่อ "father", ปี๋ for ปี "year". That covers five tones. Tones 3 and 5 both come out as Central Thai's falling tone, and both are written with ◌้.
Wikipedia's own two examples show what that costs. On paper, each pair below is one word; spoken, it is two:
🇹🇭 ไทย
ถ้าเขียนแบบล้านนา คือใช้ตัวอักษรที่คำนั้นใช้มาแต่เดิมพร้อมรูปวรรณยุกต์ ทั้งหกเสียงจะไม่ซ้ำกันเลย เหล้า กับ เล่า ต่างกันที่พยัญชนะต้น คือมี ห นำหน้า ล ปัญหาเริ่มเมื่อเขียนคำตามเสียงที่ได้ยิน คำเมืองออกเสียงอักษรไทย ค ช ท พ โดยไม่พ่นลมแบบภาษาไทยกลาง คน ฟังเหมือน กน และ ช้าง ฟังเหมือน จ้าง คนเขียนที่อยากให้ผู้อ่านได้ยินแบบนั้นจึงใช้ ก จ ต ป แทน แล้วกำหนดวรรณยุกต์ด้วยไม้วรรณยุกต์แบบภาษาไทยกลาง เช่น จ๊าง, ป้อ แทน พ่อ, ปี๋ แทน ปี วิธีนี้ครอบคลุมได้ห้าเสียง เสียงที่ 3 และเสียงที่ 5 กลายเป็นเสียงโทของภาษาไทยกลางทั้งคู่ และเขียนด้วย ◌้ ทั้งคู่
ตัวอย่างสองคู่ของวิกิพีเดียเองแสดงให้เห็นผล บนกระดาษ แต่ละคู่ข้างล่างเป็นคำเดียว แต่เวลาพูดเป็นสองคำ:
🇨🇳 中文
按兰纳的写法,也就是用这个词一直以来的字母再加上它的声调符号,六个声调绝不会混在一起:เหล้า 和 เล่า 的首字母不同,前者是 ห 加 ล。麻烦出在按发音来写的时候。北泰语读泰文字母 ค ช ท พ 时不送气,和中部泰语不同:คน(人)听起来像 กน,ช้าง(大象)像 จ้าง。想让读者听出这一点的作者就改用 ก จ ต ป,再用中部泰语的声调符号定声调:จ๊าง,用 ป้อ 写 พ่อ(父亲),用 ปี๋ 写 ปี(年)。这样能写出五个声调。第 3 和第 5 个声调都成了中部泰语的降调,也都写作 ◌้。
维基百科自己的两个例子说明了代价。在纸上,下面每一对都是同一个词;说出来却是两个词:
| Writtenเขียนว่า写法 | Meaning 1ความหมาย 1意思 1 | Meaning 2ความหมาย 2意思 2 |
|---|---|---|
| ก้า | brave (Thai กล้า), tone 3กล้า (ภาษาไทย กล้า) เสียงที่ 3勇敢(泰语 กล้า),第 3 调 | value (Thai ค่า), tone 5ค่า (ภาษาไทย ค่า) เสียงที่ 5价值(泰语 ค่า),第 5 调 |
| ป้าย | sign (Thai ป้าย), tone 3ป้าย (ภาษาไทย ป้าย) เสียงที่ 3牌子(泰语 ป้าย),第 3 调 | to lose (Thai พ่าย), tone 5แพ้ (ภาษาไทย พ่าย) เสียงที่ 5输(泰语 พ่าย),第 5 调 |
🇬🇧 English
Three more sounds differ from what the Thai letters say. ร in a Northern word is said h: เรา "we" is said เฮา. ย is often said ny, a sound Central Thai lost: ยาว "long" is said ɲaːw. And the Thai clusters กล, ปร and the rest are not said as clusters: กล้า is said ก้า. A computer that reads Kam Mueang with Central Thai's rules, as the Thai voice tools do, gets every one of these wrong. Parts 2 and 3 read Kam Mueang the Lanna way instead.
🇹🇭 ไทย
ยังมีอีกสามเสียงที่ต่างจากที่ตัวอักษรไทยบอก ร ในคำเมืองออกเสียงเป็น h เช่น เรา ออกเสียงว่า เฮา ย มักออกเสียงเป็น ny ซึ่งเป็นเสียงที่ภาษาไทยกลางไม่มีแล้ว เช่น ยาว ออกเสียงว่า ɲaːw และเสียงควบกล้ำแบบไทย เช่น กล ปร ไม่ได้ออกเสียงเป็นเสียงควบ เช่น กล้า ออกเสียงว่า ก้า คอมพิวเตอร์ที่อ่านคำเมืองด้วยกฎของภาษาไทยกลาง อย่างที่เครื่องมือเสียงพูดภาษาไทยทำ จะอ่านผิดทุกข้อ ส่วนที่ 2 และ 3 จึงอ่านคำเมืองแบบล้านนาแทน
🇨🇳 中文
还有三个音和泰文字母表示的不同。北泰语词里的 ร 读作 h:เรา(我们)读作 เฮา。ย 常常读作 ny,这是中部泰语已经没有的音:ยาว(长)读作 ɲaːw。泰文的复辅音 กล、ปร 等也不按复辅音读:กล้า 读作 ก้า。用中部泰语规则读北泰语的电脑,比如泰语的语音工具,每一处都会读错。所以第 2 和第 3 部分改用兰纳的方式来读北泰语。
2 · How a tone is read from the letters · 2 · อ่านวรรณยุกต์จากตัวอักษรอย่างไร · 2 · 怎样从字母读出声调
🇬🇧 English
The Lanna script reads a tone from two things: the group its first letter belongs to, and the tone mark over it. Read the same way, Thai letters show all six tones of Kam Mueang. The groups are not quite Thai's classes: ก จ ต ป join the high letters, because in Kam Mueang ปี "year" rises by itself; อ บ ด stay mid; the rest are low. Each letter below shows its sound, the Lanna letter it stands for, and an example word with its tone.
🇹🇭 ไทย
อักษรธรรมล้านนาอ่านวรรณยุกต์จากสองอย่าง คือกลุ่มของพยัญชนะต้น กับรูปวรรณยุกต์ที่อยู่ข้างบน ถ้าอ่านแบบเดียวกัน ตัวอักษรไทยก็แสดงวรรณยุกต์ของคำเมืองได้ครบทั้งหก กลุ่มเหล่านี้ไม่ตรงกับไตรยางศ์ของไทยเสียทีเดียว ก จ ต ป อยู่กลุ่มเดียวกับอักษรสูง เพราะในคำเมือง ปี เสียงขึ้นเองโดยไม่ต้องมีรูปวรรณยุกต์ อ บ ด ยังเป็นอักษรกลาง ที่เหลือเป็นอักษรต่ำ ตัวอักษรแต่ละตัวข้างล่างแสดงเสียง ตัวอักษรธรรมล้านนาที่คู่กัน และคำตัวอย่างพร้อมวรรณยุกต์
🇨🇳 中文
兰纳文根据两样东西读声调:首字母所属的组,以及它上面的声调符号。用同样的方法读,泰文字母也能表示北泰语的全部六个声调。这些组和泰文的辅音类别不完全一样:ก จ ต ป 归入高辅音一组,因为在北泰语里 ปี(年)本身就是升调;อ บ ด 仍是中辅音;其余的是低辅音。下面每个字母都标出它的读音、对应的兰纳文字母,以及一个例词和它的声调。
The letters, in three groupsตัวอักษร ในสามกลุ่ม字母,分成三组
The four Khmer letters with a dark frame are the proposal of part 5.ตัวอักษรเขมรสี่ตัวที่มีกรอบสีเข้ม คือข้อเสนอในส่วนที่ 5带深色边框的四个高棉字母是第 5 部分的提议。
Group 1: rises when no mark is writtenกลุ่ม 1: เสียงขึ้นเมื่อไม่มีรูปวรรณยุกต์第 1 组:不标声调符号时读升调
Group 2: mid when no mark is writtenกลุ่ม 2: เสียงกลางเมื่อไม่มีรูปวรรณยุกต์第 2 组:不标声调符号时读中调
Group 3: the low lettersกลุ่ม 3: อักษรต่ำ第 3 组:低辅音
The tones: a group and a markวรรณยุกต์: กลุ่มกับรูปวรรณยุกต์声调:组和符号
A syllable is checked when it ends in k, t, p or a short vowel with nothing after it. Checked syllables take four of the six tones, and need no mark.พยางค์ที่ลงท้ายด้วย k, t, p หรือสระเสียงสั้นที่ไม่มีอะไรตามหลัง คือคำตาย คำตายมีวรรณยุกต์ได้สี่ในหกเสียง และไม่ต้องใส่รูปวรรณยุกต์以 k、t、p 结尾,或以后面什么都没有的短元音结尾的音节,是促音节。促音节只有六个声调中的四个,也不需要声调符号。
| No markไม่มีรูปวรรณยุกต์无符号 | ◌่ | ◌้ | Checked, short vowelคำตาย สระสั้น促音节,短元音 | Checked, long vowelคำตาย สระยาว促音节,长元音 | |
|---|---|---|---|---|---|
| Group 1กลุ่ม 1第 1 组 | 1 ขา | 2 ไข่ | 3 ข้าว | 1 ผัก | 2 ขาด |
| Group 2กลุ่ม 2第 2 组 | 4 ดี | 2 ด่า | 3 บ้าน | 1 ดิบ | 2 ดอก |
| Group 3กลุ่ม 3第 3 组 | 4 มือ | 5 แม่ | 6 ม้า | 6 นก | 5 มีด |
3 · Type Kam Mueang, and read its tones · 3 · ลองพิมพ์คำเมือง แล้วอ่านวรรณยุกต์ · 3 · 输入北泰语,读出声调
🇬🇧 English
Type or paste Kam Mueang into the box, in ordinary Thai spelling or in the alphabet of part 5. The page splits it into syllables with alphabet.js and shows each one's group, tone and sounds, and how it is written in the Lanna script (part 4 says how). Where the spelling is not the alphabet's, it says what the alphabet would change. The four Khmer letters and the two tone marks have buttons; everything else is on a Thai keyboard.
🇹🇭 ไทย
พิมพ์หรือวางข้อความคำเมืองลงในช่อง จะเขียนด้วยการสะกดแบบไทยทั่วไป หรือระบบตัวอักษรในส่วนที่ 5 ก็ได้ หน้านี้จะแบ่งเป็นพยางค์ด้วย alphabet.js แล้วแสดงกลุ่ม วรรณยุกต์ และเสียงของแต่ละพยางค์ พร้อมรูปที่เขียนด้วยอักษรธรรมล้านนา (ส่วนที่ 4 บอกว่าทำอย่างไร) ตรงไหนที่การสะกดไม่ใช่แบบของระบบนี้ จะบอกว่าระบบนี้จะเขียนต่างไปอย่างไร ตัวอักษรเขมรสี่ตัวและรูปวรรณยุกต์สองรูปมีปุ่มให้กด ตัวอื่นทั้งหมดอยู่บนแป้นพิมพ์ภาษาไทย
🇨🇳 中文
在框里输入或粘贴北泰语,可以用普通的泰文拼写,也可以用第 5 部分的这套字母。页面用 alphabet.js 把它分成音节,显示每个音节的组、声调和读音,以及它用兰纳文怎么写(第 4 部分说明怎样做)。凡是拼写不是这套字母写法的地方,它会告诉你这套字母会怎样写。四个高棉字母和两个声调符号有按钮;其余的都在泰文键盘上。
In the Lanna script:อักษรธรรมล้านนา:兰纳文:
How the reader was checkedตรวจเครื่องอ่านอย่างไร读字器是怎样检验的
🇬🇧 English
Linguists sort Tai words into boxes by the tone the word had long ago, and William Gedney listed test words for each box. Wikipedia's Proto-Tai article gives that list with Thai spellings, and its Lanna language article gives the Chiang Mai tone of each box. read_words.py spells every test word in the alphabet, reads it with alphabet.js, and compares the tone with the tone of its box. It adds the Lanna article's own examples: the six tones, the checked syllables, ny beside y, and the two pairs Thai spelling merges. Result: 126 of 126 words read with the tone their box gives, 0 wrong. The "sounds" column shows what the spelling says: the vowel length of each word is for a speaker to check.
🇹🇭 ไทย
นักภาษาศาสตร์จัดคำในภาษาตระกูลไทเป็นช่องตามวรรณยุกต์ที่คำนั้นมีในสมัยโบราณ และ William Gedney ได้ทำรายการคำทดสอบสำหรับแต่ละช่องไว้ บทความ Proto-Tai ในวิกิพีเดียให้รายการนั้นพร้อมการสะกดแบบไทย และบทความภาษาล้านนาให้วรรณยุกต์ของแต่ละช่องในสำเนียงเชียงใหม่ read_words.py สะกดคำทดสอบทุกคำด้วยระบบตัวอักษรนี้ อ่านด้วย alphabet.js แล้วเทียบวรรณยุกต์กับวรรณยุกต์ของช่อง และเพิ่มตัวอย่างของบทความภาษาล้านนาเอง คือ หกเสียงวรรณยุกต์ คำตาย ny คู่กับ y และสองคู่ที่การสะกดแบบไทยรวมเป็นคำเดียว ผลคือ อ่านวรรณยุกต์ตรงกับช่อง 126 จาก 126 คำ ผิด 0 คำ คอลัมน์ sounds แสดงสิ่งที่การสะกดบอก ส่วนความยาวสระของแต่ละคำต้องให้เจ้าของภาษาตรวจ
🇨🇳 中文
语言学家按词在古代的声调把台语的词分进不同的格子,William Gedney 为每个格子列了测试词。维基百科的 Proto-Tai 条目给出了这份带泰文拼写的词表,“兰纳语”条目则给出了清迈话里每个格子的声调。read_words.py 用这套字母拼写每个测试词,用 alphabet.js 读出来,再和它所在格子的声调比较。它还加上了“兰纳语”条目自己的例子:六个声调、促音节、ny 和 y,以及泰文拼写合并掉的两对词。结果:126 个词里有 126 个读出的声调和格子相符,错了 0 个。“sounds”一栏显示拼写表示的读音:每个词元音的长短要由说这门语言的人来检查。
list box word sounds tone box says
Gedney A1 หู huː˨˦ 1 1 ok ear
Gedney A1 ขา kʰaː˨˦ 1 1 ok leg
Gedney A1 หัว hua˨˦ 1 1 ok head
Gedney A1 สอง sɔːŋ˨˦ 1 1 ok two
Gedney A1 หมา maː˨˦ 1 1 ok dog
Gedney A2 ปี piː˨˦ 1 1 ok year
Gedney A2 ตา taː˨˦ 1 1 ok eye
Gedney A2 กิน kin˨˦ 1 1 ok eat
Gedney A2 กา kaː˨˦ 1 1 ok teapot
Gedney A2 ปา paː˨˦ 1 1 ok fish (ปลา)
Gedney A3 บิน bin˧˧ 4 4 ok fly
Gedney A3 แดง dɛːŋ˧˧ 4 4 ok red
Gedney A3 ดาว daːw˧˧ 4 4 ok star
Gedney A3 ใบ baj˧˧ 4 4 ok leaf
Gedney A4 มือ mɯː˧˧ 4 4 ok hand
Gedney A4 ควาย kʰwaːj˧˧ 4 4 ok buffalo
Gedney A4 นา naː˧˧ 4 4 ok rice field
Gedney A4 งู ŋuː˧˧ 4 4 ok snake
Gedney B1 ไข่ kʰaj˨˩ 2 2 ok egg
Gedney B1 ผ่า pʰaː˨˩ 2 2 ok split
Gedney B1 เข่า kʰaw˨˩ 2 2 ok knee
Gedney B1 ใหม่ maj˨˩ 2 2 ok new
Gedney B1 สี่ siː˨˩ 2 2 ok four
Gedney B2 ป่า paː˨˩ 2 2 ok forest
Gedney B2 ไก่ kaj˨˩ 2 2 ok chicken
Gedney B2 แก่ kɛː˨˩ 2 2 ok old
Gedney B2 เต่า taw˨˩ 2 2 ok turtle
Gedney B2 เป่า paw˨˩ 2 2 ok blow
Gedney B3 บ่า baː˨˩ 2 2 ok shoulder
Gedney B3 บ่าว baːw˨˩ 2 2 ok young man
Gedney B3 ด่า daː˨˩ 2 2 ok scold
Gedney B3 อิ่ม ʔim˨˩ 2 2 ok full
Gedney B4 ពี่ piː˦˨ 5 5 ok older sibling (พี่)
Gedney B4 ព่อ pɔː˦˨ 5 5 ok father (พ่อ)
Gedney B4 ไฮ่ haj˦˨ 5 5 ok dry field (ไร่)
Gedney B4 นั่ง naŋ˦˨ 5 5 ok sit
Gedney B4 เลื่อย lɯaj˦˨ 5 5 ok saw
Gedney C1 ข้าว kʰaːw˦˦ʔ 3 3 ok rice
Gedney C1 เสื้อ sɯa˦˦ʔ 3 3 ok shirt
Gedney C1 ข้า kʰaː˦˦ʔ 3 3 ok kill (ฆ่า)
Gedney C1 ไข้ kʰaj˦˦ʔ 3 3 ok fever
Gedney C1 ห้า haː˦˦ʔ 3 3 ok five
Gedney C1 ถ้วย tʰuaj˦˦ʔ 3 3 ok cup
Gedney C1 หม้อ mɔː˦˦ʔ 3 3 ok pot
Gedney C1 หน้า naː˦˦ʔ 3 3 ok face
Gedney C2 ป้า paː˦˦ʔ 3 3 ok aunt
Gedney C2 ก้า kaː˦˦ʔ 3 3 ok seedlings (กล้า)
Gedney C2 ต้ม tom˦˦ʔ 3 3 ok boil
Gedney C2 เก้า kaw˦˦ʔ 3 3 ok nine
Gedney C2 ใก้ kaj˦˦ʔ 3 3 ok near (ใกล้)
Gedney C3 บ้าน baːn˦˦ʔ 3 3 ok village
Gedney C3 บ้า baː˦˦ʔ 3 3 ok crazy
Gedney C3 อ้า ʔaː˦˦ʔ 3 3 ok open the mouth
Gedney C3 อ้อย ʔɔːj˦˦ʔ 3 3 ok sugarcane
Gedney C3 ด้าม daːm˦˦ʔ 3 3 ok handle
Gedney C3 ด้าย daːj˦˦ʔ 3 3 ok string
Gedney C4 น้ำ nam˦˥ 6 6 ok water
Gedney C4 น้อง nɔːŋ˦˥ 6 6 ok younger sibling
Gedney C4 ไม้ maj˦˥ 6 6 ok wood
Gedney C4 ม้า maː˦˥ 6 6 ok horse
Gedney C4 ลิ้น lin˦˥ 6 6 ok tongue
Gedney C4 ទ้อง tɔːŋ˦˥ 6 6 ok belly (ท้อง)
Gedney DS1 หมัด mat˨˦ 1 1 ok flea
Gedney DS1 สุก suk˨˦ 1 1 ok ripe
Gedney DS1 ผัก pʰak˨˦ 1 1 ok vegetable
Gedney DS1 หก hok˨˦ 1 1 ok six
Gedney DS1 สิบ sip˨˦ 1 1 ok ten
Gedney DS2 กบ kop˨˦ 1 1 ok frog
Gedney DS2 ตับ tap˨˦ 1 1 ok liver
Gedney DS2 เจ็บ tɕep˨˦ 1 1 ok hurt
Gedney DS2 เป็ด pet˨˦ 1 1 ok duck
Gedney DS2 ตก tok˨˦ 1 1 ok fall
Gedney DS3 เบ็ด bet˨˦ 1 1 ok fishhook
Gedney DS3 ดิบ dip˨˦ 1 1 ok raw
Gedney DS3 อก ʔok˨˦ 1 1 ok chest
Gedney DS3 ดึก dɯk˨˦ 1 1 ok late
Gedney DS4 นก nok˦˥ 6 6 ok bird
Gedney DS4 มัด mat˦˥ 6 6 ok tie up
Gedney DS4 ลัก lak˦˥ 6 6 ok steal
Gedney DS4 ซัก sak˦˥ 6 6 ok wash
Gedney DS4 มด mot˦˥ 6 6 ok ant
Gedney DS4 เล็บ lep˦˥ 6 6 ok nail
Gedney DL1 ขาด kʰaːt˨˩ 2 2 ok torn
Gedney DL1 เหงือก ŋɯak˨˩ 2 2 ok gums
Gedney DL1 หาบ haːp˨˩ 2 2 ok carry on a pole
Gedney DL1 ขวด kʰuat˨˩ 2 2 ok bottle
Gedney DL1 ผูก pʰuːk˨˩ 2 2 ok tie
Gedney DL1 สอก sɔːk˨˩ 2 2 ok elbow (ศอก)
Gedney DL1 แขก kʰɛːk˨˩ 2 2 ok guest
Gedney DL2 ปอด pɔːt˨˩ 2 2 ok lung
Gedney DL2 ปีก piːk˨˩ 2 2 ok wing
Gedney DL2 ตอก tɔːk˨˩ 2 2 ok pound
Gedney DL2 แปด pɛːt˨˩ 2 2 ok eight
Gedney DL2 ปาก paːk˨˩ 2 2 ok mouth
Gedney DL2 ตาก taːk˨˩ 2 2 ok dry in the sun
Gedney DL3 แดด dɛːt˨˩ 2 2 ok sunshine
Gedney DL3 อาบ ʔaːp˨˩ 2 2 ok bathe
Gedney DL3 ดอก dɔːk˨˩ 2 2 ok flower
Gedney DL3 ออก ʔɔːk˨˩ 2 2 ok go out
Gedney DL4 มีด miːt˦˨ 5 5 ok knife
Gedney DL4 ลูก luːk˦˨ 5 5 ok child
Gedney DL4 เลือด lɯat˦˨ 5 5 ok blood
Gedney DL4 นอก nɔːk˦˨ 5 5 ok outside
Gedney DL4 เជือก tɕɯak˦˨ 5 5 ok rope (เชือก)
Gedney DL4 ฮาก haːk˦˨ 5 5 ok root (ราก)
Lanna A1 เหลา law˨˦ 1 1 ok sharpen
Lanna B1 เหล่า law˨˩ 2 2 ok forest, group
Lanna C1 เหล้า law˦˦ʔ 3 3 ok liquor
Lanna A4 เลา law˧˧ 4 4 ok beautiful
Lanna B4 เล่า law˦˨ 5 5 ok tell
Lanna C4 เล้า law˦˥ 6 6 ok coop
Lanna DS1 หลัก lak˨˦ 1 1 ok post
Lanna DS4 ลัก lak˦˥ 6 6 ok steal
Lanna DL1 หลาก laːk˨˩ 2 2 ok differ
Lanna DL4 ลาก laːk˦˨ 5 5 ok drag
Lanna C1 เจ้า tɕaw˦˦ʔ 3 3 ok lord, you
Lanna C2 ก้า kaː˦˦ʔ 3 3 ok brave (กล้า)
Lanna B4 គ่า kaː˦˨ 5 5 ok value (ค่า)
Lanna C2 ป้าย paːj˦˦ʔ 3 3 ok sign
Lanna B4 ព่าย paːj˦˨ 5 5 ok lose (พ่าย)
Lanna DL4 ญาก ɲaːk˦˨ 5 5 ok difficult (ยาก)
Lanna A4 ญุง ɲuŋ˧˧ 4 4 ok mosquito (ยุง)
Lanna A4 ญาว ɲaːw˧˧ 4 4 ok long (ยาว)
Lanna A3 อยา jaː˧˧ 4 4 ok medicine (ยา)
Lanna DL3 อยาก jaːk˨˩ 2 2 ok want
Lanna B3 อย่าง jaːŋ˨˩ 2 2 ok manner
126 words: 126 read with the tone their box gives, 0 wrong.
The file: read_words.py · ไฟล์: read_words.py · 文件:read_words.py
"""Read test words with alphabet.js and check each tone against the tone its Gedney box gives.
The words are Gedney's diagnostic words for Tai tones (as listed in Wikipedia's "Proto-Tai
language"), plus the examples of Wikipedia's "Lanna language". Each is spelled in the Kam Mueang
alphabet: ค ช ท พ become the Khmer letters when Kam Mueang says them without a puff of air, ร
becomes ฮ, and a sound Kam Mueang drops is dropped (ปลา becomes ปา).
The tone each box has in Chiang Mai is the Lanna dictionary's (2007), as the Lanna language
article gives it. Run it with: python read_words.py
Needs Playwright with Chromium: pip install playwright, then python -m playwright install chromium
"""
import os
import unicodedata
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
# Chiang Mai tone of each Gedney box. DS and DL are checked syllables, with a short or a long vowel.
BOX_TONE = {"A1": 1, "A2": 1, "A3": 4, "A4": 4, "B1": 2, "B2": 2, "B3": 2, "B4": 5,
"C1": 3, "C2": 3, "C3": 3, "C4": 6, "DS1": 1, "DS2": 1, "DS3": 1, "DS4": 6,
"DL1": 2, "DL2": 2, "DL3": 2, "DL4": 5}
GEDNEY = {
"A1": "หู ear, ขา leg, หัว head, สอง two, หมา dog",
"A2": "ปี year, ตา eye, กิน eat, กา teapot, ปา fish (ปลา)",
"A3": "บิน fly, แดง red, ดาว star, ใบ leaf",
"A4": "มือ hand, ควาย buffalo, นา rice field, งู snake",
"B1": "ไข่ egg, ผ่า split, เข่า knee, ใหม่ new, สี่ four",
"B2": "ป่า forest, ไก่ chicken, แก่ old, เต่า turtle, เป่า blow",
"B3": "บ่า shoulder, บ่าว young man, ด่า scold, อิ่ม full",
"B4": "ពี่ older sibling (พี่), ព่อ father (พ่อ), ไฮ่ dry field (ไร่), นั่ง sit, เลื่อย saw",
"C1": "ข้าว rice, เสื้อ shirt, ข้า kill (ฆ่า), ไข้ fever, ห้า five, ถ้วย cup, หม้อ pot, หน้า face",
"C2": "ป้า aunt, ก้า seedlings (กล้า), ต้ม boil, เก้า nine, ใก้ near (ใกล้)",
"C3": "บ้าน village, บ้า crazy, อ้า open the mouth, อ้อย sugarcane, ด้าม handle, ด้าย string",
"C4": "น้ำ water, น้อง younger sibling, ไม้ wood, ม้า horse, ลิ้น tongue, ទ้อง belly (ท้อง)",
"DS1": "หมัด flea, สุก ripe, ผัก vegetable, หก six, สิบ ten",
"DS2": "กบ frog, ตับ liver, เจ็บ hurt, เป็ด duck, ตก fall",
"DS3": "เบ็ด fishhook, ดิบ raw, อก chest, ดึก late",
"DS4": "นก bird, มัด tie up, ลัก steal, ซัก wash, มด ant, เล็บ nail",
"DL1": "ขาด torn, เหงือก gums, หาบ carry on a pole, ขวด bottle, ผูก tie, สอก elbow (ศอก), แขก guest",
"DL2": "ปอด lung, ปีก wing, ตอก pound, แปด eight, ปาก mouth, ตาก dry in the sun",
"DL3": "แดด sunshine, อาบ bathe, ดอก flower, ออก go out",
"DL4": "มีด knife, ลูก child, เลือด blood, นอก outside, เជือก rope (เชือก), ฮาก root (ราก)",
}
# The examples of the Lanna language article, with the box each belongs to.
LANNA = [
("A1", "เหลา sharpen"), ("B1", "เหล่า forest, group"), ("C1", "เหล้า liquor"),
("A4", "เลา beautiful"), ("B4", "เล่า tell"), ("C4", "เล้า coop"),
("DS1", "หลัก post"), ("DS4", "ลัก steal"), ("DL1", "หลาก differ"), ("DL4", "ลาก drag"),
("C1", "เจ้า lord, you"), ("C2", "ก้า brave (กล้า)"), ("B4", "គ่า value (ค่า)"),
("C2", "ป้าย sign"), ("B4", "ព่าย lose (พ่าย)"),
("DL4", "ญาก difficult (ยาก)"), ("A4", "ญุง mosquito (ยุง)"), ("A4", "ญาว long (ยาว)"),
("A3", "อยา medicine (ยา)"), ("DL3", "อยาก want"), ("B3", "อย่าง manner"),
]
def width(text):
"""Columns a string takes in a terminal: Thai marks above and below take none."""
return sum(0 if unicodedata.combining(ch) or unicodedata.category(ch) == "Mn" else 1 for ch in text)
def pad(text, columns):
return text + " " * max(0, columns - width(text))
def items():
rows = []
for box, words in GEDNEY.items():
for word in words.split(", "):
rows.append(("Gedney", box, word))
for box, word in LANNA:
rows.append(("Lanna", box, word))
return rows
def main():
rows = items()
spellings = [w.split(" ")[0] for _, _, w in rows]
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.add_script_tag(path=os.path.join(HERE, "alphabet.js"))
readings = page.evaluate("words => words.map(w => KMA.read(w))", spellings)
browser.close()
wrong = 0
print(pad("list", 8) + pad("box", 5) + pad("word", 10) + pad("sounds", 16) + "tone box says")
for (source, box, word), reading, spelling in zip(rows, readings, spellings):
if len(reading) != 1 or "error" in reading[0]:
wrong += 1
print(pad(source, 8) + pad(box, 5) + pad(spelling, 10) + "could not be read: " + str(reading))
continue
s = reading[0]
ok = s["tone"] == BOX_TONE[box]
wrong += 0 if ok else 1
mark = "ok" if ok else "WRONG"
print(pad(source, 8) + pad(box, 5) + pad(spelling, 10) + pad(s["ipa"], 16)
+ f"{s['tone']} {BOX_TONE[box]} {mark} {word[len(spelling) + 1:]}")
print(f"\n{len(rows)} words: {len(rows) - wrong} read with the tone their box gives, {wrong} wrong.")
if __name__ == "__main__":
main()
4 · Into the Lanna script · 4 · เขียนเป็นอักษรธรรมล้านนา · 4 · 写成兰纳文
🇬🇧 English
A speech recogniser for Kam Mueang will write what it hears in Thai letters, because the text it learns from will be in Thai letters. So the second half of a recogniser that writes the Lanna script is a writer: Thai letters in, the Lanna script out. alphabet.js now has one. It reads the text with the rules of part 3, then writes each syllable in the Lanna script: the Lanna letter for each consonant, the Lanna shape of the vowel, the final under the syllable or on the line, and the tone mark. The first half, the recogniser itself, waits for the native speaker's recordings.
The rules were worked out from the Kam Mueang words of Thai Wiktionary, and checked against them. Wiktionary gives 3202 Kam Mueang words in the Lanna script, and 2055 of them in Thai letters too. lanna_check.py writes each of those in the Lanna script by the rules alone and compares the result with Wiktionary's spelling.
From Wiktionary's own Thai letters, set down letter for letter, 95.1% come out exactly as Wiktionary spells them; most of the rest are Pali and Sanskrit words, which stack a doubled consonant (ปัญญา is ᨷᨬ᩠ᨬᩣ). From the same words written in the alphabet, by their sound, 86.4%: Lanna keeps letters that are no longer said, and a sound cannot show them (กล้า is said ก้า and written ᨠᩖ᩶ᩣ). From ordinary Thai spelling, 69.7%: it hides what the alphabet shows, which k, t or p and which tone (ตือ for ᨴᩨ).
For the words the rules miss, make_lanna_words.py keeps a word list from Wiktionary, lanna_words.js: 801 words, looked up by how they are written in Thai letters. Of the reading script's 19258 syllables, 2600 (14%) are spelled from the list and the rest by the rules. A word the list does not hold is written by its sound.
The check also corrected the alphabet's chart. Of Wiktionary's words that begin with an h sound, 81 are written with ᩁ, the letter ra; 81 with ᩉ, the high h, which the alphabet writes ห; and only 4 with ᩌ. So in part 2 the partner of ฮ is now ᩁ (เฮา, we, is ᩁᩮᩢᩣ).
No Lanna reader has checked any of this. Below are the reading script's first sentences as make_lanna_words.py wrote them, for one to mark. The reader in part 3 writes whatever you type in the Lanna script too.
🇹🇭 ไทย
เครื่องรู้จำเสียงพูดคำเมืองจะเขียนสิ่งที่ได้ยินด้วยตัวอักษรไทย เพราะข้อความที่ใช้สอนมันจะเขียนด้วยตัวอักษรไทย ครึ่งหลังของเครื่องรู้จำเสียงที่เขียนอักษรธรรมล้านนาได้จึงเป็นตัวเขียน รับตัวอักษรไทยเข้าไป แล้วเขียนออกมาเป็นอักษรธรรมล้านนา ตอนนี้ alphabet.js มีตัวเขียนนี้แล้ว มันอ่านข้อความด้วยกฎของส่วนที่ 3 แล้วเขียนแต่ละพยางค์ด้วยอักษรธรรมล้านนา คืออักษรล้านนาของพยัญชนะแต่ละตัว รูปสระแบบล้านนา ตัวสะกดที่อยู่ใต้พยางค์หรืออยู่บนบรรทัด และรูปวรรณยุกต์ ส่วนครึ่งแรก คือตัวเครื่องรู้จำเสียงเอง ต้องรอเสียงที่อัดจากเจ้าของภาษา
กฎเหล่านี้ได้มาจากคำเมืองในวิกิพจนานุกรมภาษาไทย และตรวจกับคำเหล่านั้น วิกิพจนานุกรมให้คำเมือง 3202 คำเป็นอักษรธรรมล้านนา และให้ 2055 คำในนั้นเป็นตัวอักษรไทยด้วย lanna_check.py เขียนคำเหล่านั้นทุกคำเป็นอักษรธรรมล้านนาด้วยกฎอย่างเดียว แล้วเทียบผลกับที่วิกิพจนานุกรมสะกดไว้
จากตัวอักษรไทยของวิกิพจนานุกรมเอง ที่ถอดมาตัวต่อตัว ออกมาตรงกับที่วิกิพจนานุกรมสะกดทุกตัวร้อยละ 95.1 ที่เหลือส่วนใหญ่เป็นคำบาลีและสันสกฤต ซึ่งซ้อนพยัญชนะที่ซ้ำกันไว้ใต้กัน (ปัญญา เขียน ᨷᨬ᩠ᨬᩣ) จากคำเดียวกันที่เขียนด้วยระบบตัวอักษรนี้ตามเสียง ได้ร้อยละ 86.4 เพราะอักษรล้านนายังเก็บตัวอักษรที่ไม่ออกเสียงแล้ว และเสียงบอกตัวอักษรเหล่านั้นไม่ได้ (กล้า ออกเสียง ก้า แต่เขียน ᨠᩖ᩶ᩣ) จากการสะกดแบบไทยทั่วไป ได้ร้อยละ 69.7 เพราะการสะกดแบบนั้นซ่อนสิ่งที่ระบบตัวอักษรนี้แสดง คือ k, t หรือ p แบบไหน และวรรณยุกต์อะไร (ตือ สำหรับ ᨴᩨ)
สำหรับคำที่กฎเขียนไม่ตรง make_lanna_words.py เก็บรายการคำจากวิกิพจนานุกรมไว้ใน lanna_words.js มี 801 คำ ค้นจากรูปที่เขียนด้วยตัวอักษรไทย จากพยางค์ทั้งหมด 19258 พยางค์ของบทอ่าน มี 2600 พยางค์ (ร้อยละ 14) ที่สะกดตามรายการ ที่เหลือเขียนด้วยกฎ คำที่ไม่อยู่ในรายการจะเขียนตามเสียง
การตรวจนี้ยังแก้ตารางของระบบตัวอักษรด้วย ในคำของวิกิพจนานุกรมที่ขึ้นต้นด้วยเสียง h มี 81 คำที่เขียนด้วย ᩁ คือตัว ร 81 คำเขียนด้วย ᩉ คือ h เสียงสูง ซึ่งระบบนี้เขียนด้วย ห และมีเพียง 4 คำที่เขียนด้วย ᩌ ตัวคู่ของ ฮ ในส่วนที่ 2 จึงเปลี่ยนเป็น ᩁ แล้ว (เฮา เขียน ᩁᩮᩢᩣ)
ยังไม่มีผู้ที่อ่านอักษรธรรมล้านนาได้มาตรวจสิ่งเหล่านี้ ข้างล่างคือประโยคแรก ๆ ของบทอ่าน ตามที่ make_lanna_words.py เขียนออกมา ให้ผู้ที่อ่านได้ช่วยทำเครื่องหมายตรงที่ผิด เครื่องอ่านในส่วนที่ 3 ก็เขียนทุกอย่างที่คุณพิมพ์เป็นอักษรธรรมล้านนาด้วย
🇨🇳 中文
北泰语的语音识别器会用泰文字母写下它听到的内容,因为它学习用的文字是用泰文字母写的。所以,一个能写兰纳文的识别器,后一半是一个转写器:输入泰文字母,输出兰纳文。alphabet.js 现在就有一个。它用第 3 部分的规则读文字,再把每个音节写成兰纳文:每个辅音的兰纳字母、元音的兰纳写法、尾辅音写在音节下面还是写在行上,以及声调符号。前一半,也就是识别器本身,要等母语者的录音。
这些规则是从泰文维基词典里的北泰语词总结出来的,也用这些词来检验。维基词典给出了 3202 个北泰语词的兰纳文写法,其中 2055 个也有泰文字母的写法。lanna_check.py 只用规则把这些词都写成兰纳文,再和维基词典的拼写作比较。
从维基词典自己逐字母转写的泰文字母出发,95.1% 和维基词典的拼写完全一样;其余的大多是巴利语和梵语词,它们把重复的辅音叠写在下面(ปัญญา 写作 ᨷᨬ᩠ᨬᩣ)。从按读音用这套字母写的同一批词出发,是 86.4%:兰纳文保留了一些已经不发音的字母,而读音显示不出它们(กล้า 读作 ก้า,写作 ᨠᩖ᩶ᩣ)。从普通的泰文拼写出发,是 69.7%:它掩盖了这套字母能显示的东西,也就是哪一个 k、t 或 p,以及哪一个声调(ตือ 对应 ᨴᩨ)。
对规则写不对的词,make_lanna_words.py 从维基词典整理出一个词表 lanna_words.js:801 个词,按它们的泰文字母写法查找。朗读稿的 19258 个音节中,有 2600 个(14%)按词表拼写,其余由规则写出。词表里没有的词按读音来写。
这次检验也更正了这套字母的字母表。维基词典里以 h 音开头的词,有 81 个用 ᩁ(ra)书写,81 个用 ᩉ(高辅音 h,这套字母写作 ห),只有 4 个用 ᩌ。所以第 2 部分里 ฮ 对应的字母现在改为 ᩁ(เฮา“我们”写作 ᩁᩮᩢᩣ)。
还没有懂兰纳文的人检查过这些。下面是 make_lanna_words.py 写出的朗读稿前几句,请懂兰纳文的人标出错误。第 3 部分的读字器也会把你输入的任何内容写成兰纳文。
What wiktionary_words.py printed:สิ่งที่ wiktionary_words.py พิมพ์ออกมา:wiktionary_words.py 打印的结果:
3202 Kam Mueang words with a Lanna spelling, read on 2026-10-10.
2055 of them are also given in Thai letters; 2054 letter for letter, 2051 as they sound.
2076 say how the word is said in Chiang Mai.
What lanna_check.py printed:สิ่งที่ lanna_check.py พิมพ์ออกมา:lanna_check.py 打印的结果:
2055 Kam Mueang words of Thai Wiktionary, written in the Lanna script by the rules of
alphabet.js alone (no word list), and compared with the Lanna spelling Wiktionary gives.
Written from words the same a mark elsewhere different
Wiktionary's Thai letters, letter for letter 2054 1952 (95.0%) 14 88
the Kam Mueang alphabet, written by sound 2054 1774 (86.4%) 7 273
ordinary Thai spelling of how it sounds 2051 1430 (69.7%) 5 616
Words said with h at the start, by their first Lanna letter: ᩁ 81, ᩉ 81, ᩌ 4
Some words written from Wiktionary's Thai letters, letter for letter: the input, Wiktionary's spelling, ours
กัมพี ᨠᨾᩛᩦ ᨠᩢ᩠ᨾᨻᩦ
ขดถวาย ᨡᩫ᩠ᨯᨳ᩠ᩅ᩻ᩣ᩠ᨿ ᨡᩫ᩠ᨯᨳ᩠ᩅᩣ᩠ᨿ
ทรัพย์ ᨴᩕᩢᨻ᩠ᨿ᩺ ᨴᩕᩢ᩠ᨷᨿ᩺
ปากฏ ᨷᩤᨠᩫ᩠ᨭ ᨸᩣᨠᩫ᩠ᨭ
ฟ้าแมลบ ᨼ᩶ᩣᨾᩖᩯᨷ ᨼ᩶ᩣᨾᩯᩃᩫ᩠ᨷ
รีตรอย ᩁᩦ᩠ᨲᩁᩬ᩠ᨿ ᩁᩦᨲᩕᩬ᩠ᨿ
สหกรณ์ ᩈᩉᨠᩬᩁ᩠ᨱ᩺ ᩈᩉᩫ᩠ᨠᩁᨱ᩺
หร่อ ᩉᩕᩬᩴ᩵ ᩉᩁᩬᩴ᩵
Some words written from the Kam Mueang alphabet, written by sound: the input, Wiktionary's spelling, ours
กัมពี ᨠᨾᩛᩦ ᨠᩢ᩠ᨾᨻᩦ
ก่อม ᨠᩖᩬ᩵ᨾ ᨠᩬ᩵ᨾ
คาว ᨣᩕᩣ᩠ᩅ ᨤᩣ᩠ᩅ
สก ᨨᩫ᩠ᨠ ᩈᩫ᩠ᨠ
ปัดจุปันนะ ᨷᨧ᩠ᨧᩩᨷᨶ᩠ᨶ ᨸᩢ᩠ᨯᨧᩩᨸᩢ᩠ᨶᨶ
แป ᨸᩖᩯ ᨸᩯ
เมก ᨾᩮ᩠ᨥ ᨾᩮ᩠ᨠ
สักគะ ᩈᨣ᩠ᨣ ᩈᩢ᩠ᨠᨣ
Some words written from ordinary Thai spelling of how it sounds: the input, Wiktionary's spelling, ours
กะตำ ᨠᨴᩤᩴ ᨠᨲᩣᩴ
คาว ᨣᩕᩣ᩠ᩅ ᨤᩣ᩠ᩅ
เกย ᨣᩮᩥ᩠ᨿ ᨠᩮᩥ᩠ᨿ
จม ᨩᩫ᩠ᨾ ᨧᩫ᩠ᨾ
ตือ ᨴᩨ ᨲᩨ
ป๋ายบ่อปอ ᨸᩖᩣ᩠ᨿᨷᩴ᩵ᨻᩬᩴ ᨸᩣ᩠ᨿᨷᩬᩴ᩵ᨸᩬᩴ
ปอน ᨻᩬᩁ ᨸᩬᩁ
เลื่อง ᩁᩮᩬᩥ᩵ᨦ ᩃᩮᩬᩥ᩵ᨦ
What make_lanna_words.py printed:สิ่งที่ make_lanna_words.py พิมพ์ออกมา:make_lanna_words.py 打印的结果:
lanna_words.js: 804 words. From Wiktionary's Thai spellings 737, from the alphabet by sound 66, by hand 2.
Left out: 196 spellings that two Wiktionary words share with different Lanna spellings.
With the list loaded, 2026 of Wiktionary's 2054 words come out as it spells them (a check that the list is applied, not a test).
The reading script: 1468 sentences, 19331 syllables; 2640 of them (14%) are spelled from the list, the rest by the rules.
Its first sentences in the Lanna script (not yet checked by a Lanna reader):
K0001 กลุ่มนักเฮียนชอบไปเตียวเล่นกับอาจารย์ในช่วงเมื่อแลง
ᨠᩖᩩ᩵ᨾᨶᩢ᩠ᨠᩁ᩠ᨿᩁᨩᩬᨷᨻᩱᨴ᩠ᨿᩅᩉᩖᩮ᩶ᩁᨠᩢ᩠ᨷᩋᩣᨧᩣ᩠ᩁᨿ᩺ᨶᩲᨩ᩠ᩅ᩵ᨦᨾᩮᩬᩥ᩵ᩋᩃᩯ᩠ᨦ
K0002 เพราะอากาศมันเย็นสบายได้ออกกำลังกายและป่าไม้รอบๆตางเตียวก่ยะฮื้ออากาศดีมากๆโตย
ᨻᩕᩰᩬᩡᩋᩣᨠᩣ᩠ᩆᨾᩢ᩠ᨶᩀᩮ᩠ᨶᩈᨷᩤ᩠ᨿᨯᩱ᩶ᩋᩬᨠᨠᩣᩴᩃᩢ᩠ᨦᨠᩣ᩠ᨿᩃᩯᩡᨸ᩵ᩣᨾᩱ᩶ᩁᩬᨷ᩻ᨴᩤ᩠ᨦᨴ᩠ᨿᩅᨣᩴ᩵ᨿᩁᩨ᩶ᩋᩣᨠᩣ᩠ᩆᨯᩦᨾᩣ᩠ᨠ᩻ᨲᩰᩫ᩠ᨿ
K0003 ขับรถเครื่องไปซื้อกาแฟที่หน้าปากซอย
ᨡᩢ᩠ᨷᩁᩫ᩠ᨳᨣᩕᩮᩬᩥ᩵ᨦᨻᩱᨪᩨ᩶ᨠᩣᨼᩯᨴᩦ᩵ᩉ᩠ᨶ᩶ᩣᨸᩣ᩠ᨠᨪᩬ᩠ᨿ
K0004 แต่ก็พบว่าฮ้านประจำตี้ชอบกิ๋นมันปิดไปแล้ว
ᨲᩯ᩵ᨣᩴ᩵ᨽᩫ᩠ᨷᩅ᩵ᩤᩁ᩶ᩣ᩠ᨶᨷᩕᨧᩣᩴᨴᩦ᩵ᨩᩬᨷᨠᩥ᩠ᨶᨾᩢ᩠ᨶᨸᩥ᩠ᨯᨻᩱᩃᩯ᩠᩶ᩅ
K0005 ก่อเลยขับรถไปซื้อฮ้านใหม่
ᨠᩬᩴ᩵ᩃᩮᩥ᩠ᨿᨡᩢ᩠ᨷᩁᩫ᩠ᨳᨻᩱᨪᩨ᩶ᩁ᩶ᩣ᩠ᨶᩉ᩠ᨾᩲ᩵
K0006 จะบ่ไปซื้อแหมแล้ว
ᨧᩢ᩠ᨠᨷᩴ᩵ᨻᩱᨪᩨ᩶ᩉᩯ᩠ᨾᩃᩯ᩠᩶ᩅ
K0007 ฮาอยากจะสอบเข้าโฮงเฮียนสวนกุหลาบเพราะกึดว่าเป็นโฮงเฮียนตี้ดี
ᩁᩣᩀᩣ᩠ᨠᨧᩢ᩠ᨠᩈᩬᨷᨡᩮᩢ᩶ᩣᩁᩰᩫ᩠ᨦᩁ᩠ᨿᩁᩈ᩠ᩅᩁᨠᩩᩉᩖᩣ᩠ᨷᨻᩕᩰᩬᩡᨠᩧ᩠ᨯᩅ᩵ᩤᨸᩮᩢ᩠ᨶᩁᩰᩫ᩠ᨦᩁ᩠ᨿᩁᨴᩦ᩵ᨯᩦ
K0008 ฮาเลยตั้งใจเฮียนนักๆ
ᩁᩣᩃᩮᩥ᩠ᨿᨲᩢ᩠᩶ᨦᨧᩲᩁ᩠ᨿᩁᨶᩢ᩠ᨠ᩻
K0009 เพื่อตี้จะสอบเข้าตี้นี่หื้อได้
ᨻᩮᩬᩥ᩵ᩋᨴᩦ᩵ᨧᩢ᩠ᨠᩈᩬᨷᨡᩮᩢ᩶ᩣᨴᩦ᩵ᨶᩦ᩵ᩉᩨ᩶ᨯᩱ᩶
K0010 สุดท้ายสอบบ่ติด
ᩈᩩᨯᨴ᩶ᩤ᩠ᨿᩈᩬᨷᨷᩴ᩵ᨲᩥ᩠ᨯ
K0011 จะหมู่รอบตัวชอบไปแอ่วเมืองนอก
ᨧᩢ᩠ᨠᩉ᩠ᨾᩪ᩵ᩁᩬᨷᨲ᩠ᩅᩫᨩᩬᨷᨻᩱᩋᩯ᩠᩵ᩅᨾᩮᩬᩥᨦᨶᩬᨠ
K0012 แต่ฮู้สึกเสียดายเงิน
ᨲᩯ᩵ᩁᩪ᩶ᩈᩧ᩠ᨠᩈ᩠ᨿᩮᨯᩣ᩠ᨿᨦᩮᩥ᩠ᨶ
5 · A proposal: four Khmer letters · 5 · ข้อเสนอ: ตัวอักษรเขมรสี่ตัว · 5 · 一个提议:四个高棉文字母
🇬🇧 English
This part is a proposal, not what the voice uses: the voice learns Kam Mueang as it is usually written. The alphabet is Thai spelling, read the Lanna way. A word keeps the letters Thai gives it, so it still looks familiar. Where a Thai letter would be read with the wrong sound, the alphabet writes the letter of the sound that is said. Every vowel is written, the final consonants are only ก ด บ ง น ม ย ว, and Thai's Pali letters, silent letters and clusters are not used. The tone is read from the group of the first letter and its mark, as in the Lanna script, so two tone marks are enough: ◌่ and ◌้.
🇹🇭 ไทย
ส่วนนี้เป็นข้อเสนอ ไม่ใช่สิ่งที่เสียงพูดใช้ เสียงพูดเรียนรู้คำเมืองแบบที่เขียนกันทั่วไป ระบบตัวอักษรนี้คือการสะกดแบบไทยที่อ่านแบบล้านนา คำหนึ่งคงตัวอักษรที่ไทยใช้ไว้ จึงยังดูคุ้นตา ตรงไหนที่อักษรไทยจะถูกอ่านเป็นเสียงผิด ระบบนี้จะเขียนด้วยตัวอักษรของเสียงที่พูดจริง สระทุกตัวต้องเขียนออกมา ตัวสะกดมีเพียง ก ด บ ง น ม ย ว และไม่ใช้อักษรบาลี ตัวการันต์ หรือเสียงควบกล้ำแบบไทย วรรณยุกต์อ่านจากกลุ่มของพยัญชนะต้นกับรูปวรรณยุกต์ แบบเดียวกับอักษรธรรมล้านนา รูปวรรณยุกต์สองรูปจึงพอ คือ ◌่ และ ◌้
🇨🇳 中文
这一部分是一个提议,不是语音所用的写法:语音学的是通常写法的北泰语。这套字母就是按兰纳的方式来读的泰文拼写。一个词保留泰文给它的字母,所以看起来仍然眼熟。凡是泰文字母会被读错音的地方,这套字母就写出实际所说的那个音的字母。每个元音都要写出来,尾辅音只有 ก ด บ ง น ม ย ว,也不用泰文的巴利文字母、不发音的字母和复辅音。声调由首辅音的组和声调符号决定,和兰纳文一样,所以两个声调符号就够了:◌่ 和 ◌้。
Six rulesกฎหกข้อ六条规则
- One letter for each sound in each group. Group 1: ก จ ต ป ข ฉ ถ ผ ฝ ส ห, and ห before ง ญ น ม ย ล ว. Group 2: อ บ ด, and อย. Group 3: all the others.
- k, j, t and p said without a puff of air in a word of group 3 are written with the Khmer letters គ ជ ទ ព. Thai ค ช ท พ keep the puff of air they have in Thai: in Kam Mueang that is mostly words from Central Thai, and a few Northern words such as ควาย "buffalo".
- ญ is ny. ย is y, in words from Central Thai. อย is the y of อยาก and อย่าง.
- h is ห or ฮ. ร is only for words from Central Thai that are said with r.
- Write every vowel. No hidden vowels, no clusters, no Pali letters, no silent letters, and only ก ด บ ง น ม ย ว at the end of a syllable.
- Use only ◌่ and ◌้, and read the tone from the group and the mark (the table in part 2). ◌๊ and ◌๋ are not needed: ปี๋ is just ปี.
- ตัวอักษรหนึ่งตัวต่อหนึ่งเสียงในแต่ละกลุ่ม กลุ่ม 1: ก จ ต ป ข ฉ ถ ผ ฝ ส ห และ ห ที่นำหน้า ง ญ น ม ย ล ว กลุ่ม 2: อ บ ด และ อย กลุ่ม 3: ตัวที่เหลือทั้งหมด
- เสียง k, j, t และ p แบบไม่พ่นลมในคำของกลุ่ม 3 เขียนด้วยตัวอักษรเขมร គ ជ ទ ព ส่วนอักษรไทย ค ช ท พ คงเสียงพ่นลมแบบภาษาไทยไว้ ในคำเมือง ส่วนใหญ่เป็นคำที่มาจากภาษาไทยกลาง และคำเหนือบางคำ เช่น ควาย
- ญ คือเสียง ny ย คือเสียง y ในคำที่มาจากภาษาไทยกลาง อย คือเสียง y ของ อยาก และ อย่าง
- เสียง h เขียนด้วย ห หรือ ฮ ส่วน ร ใช้เฉพาะคำที่มาจากภาษาไทยกลางที่ออกเสียงเป็น r
- เขียนสระทุกตัว ไม่มีสระลดรูป ไม่มีเสียงควบกล้ำ ไม่มีอักษรบาลี ไม่มีตัวการันต์ และตัวสะกดมีเพียง ก ด บ ง น ม ย ว
- ใช้เพียง ◌่ และ ◌้ แล้วอ่านวรรณยุกต์จากกลุ่มและรูปวรรณยุกต์ (ตารางในส่วนที่ 2) ไม่ต้องใช้ ◌๊ และ ◌๋ เช่น ปี๋ ก็คือ ปี
- 每组里每个音只有一个字母。第 1 组:ก จ ต ป ข ฉ ถ ผ ฝ ส ห,以及在 ง ญ น ม ย ล ว 前面的 ห。第 2 组:อ บ ด,以及 อย。第 3 组:其余的全部。
- 第 3 组的词里不送气的 k、j、t、p,用高棉字母 គ ជ ទ ព 来写。泰文的 ค ช ท พ 保留它们在泰语里的送气:在北泰语里,这大多是来自中部泰语的词,也有少数北部词,例如 ควาย(水牛)。
- ญ 是 ny。ย 是 y,用于来自中部泰语的词。อย 是 อยาก 和 อย่าง 里的 y。
- h 写作 ห 或 ฮ。ร 只用于来自中部泰语、读作 r 的词。
- 每个元音都要写出来。没有隐藏的元音,没有复辅音,没有巴利文字母,没有不发音的字母,音节末尾只能是 ก ด บ ง น ม ย ว。
- 只用 ◌่ 和 ◌้,按组和符号读出声调(见第 2 部分的表)。不需要 ◌๊ 和 ◌๋:ปี๋ 就是 ปี。
From Thai spelling to the alphabetจากการสะกดแบบไทยสู่ระบบตัวอักษร从泰文拼写到这套字母
The sounds and tones in this table are worked out by your browser, now, with the same alphabet.js as the reader in part 3.เสียงและวรรณยุกต์ในตารางนี้ เบราว์เซอร์ของคุณคำนวณขึ้นตอนนี้ ด้วย alphabet.js ตัวเดียวกับเครื่องอ่านในส่วนที่ 3表中的读音和声调是你的浏览器现在用 alphabet.js 算出来的,和第 3 部分的读字器是同一个。
| Thai spellingการสะกดแบบไทย泰文拼写 | The alphabetระบบตัวอักษร这套字母 | Sounds and toneเสียงและวรรณยุกต์读音和声调 | What changedสิ่งที่เปลี่ยน改了什么 |
|---|---|---|---|
| คน | គน | ค is said without a puff of airค ออกเสียงแบบไม่พ่นลมค 读作不送气 | |
| ทาง | ទาง | the same for ทเช่นเดียวกันกับ ทท 也一样 | |
| ช้าง | ជ้าง | the same for ชเช่นเดียวกันกับ ชช 也一样 | |
| พ่อ | ព่อ | the same for พเช่นเดียวกันกับ พพ 也一样 | |
| ค่า | គ่า | "value": no longer written like "brave"ค่า: ไม่เขียนเหมือน กล้า อีกต่อไป“价值”:不再和“勇敢”写得一样 | |
| กล้า | ก้า | "brave": Kam Mueang drops the ลกล้า: คำเมืองไม่ออกเสียง ล“勇敢”:北泰语不读 ล | |
| เรา | เฮา | ร is said hร ออกเสียงเป็น hร 读作 h | |
| ยาว | ญาว | ย is said nyย ออกเสียงเป็น nyย 读作 ny | |
| ยา | อยา | "medicine" keeps y: the glottal y of Lao ຢ and Lanna ᩀยา ยังเป็นเสียง y: เสียง y แบบมีเสียงกักในลำคอ ตรงกับ ຢ ของลาว และ ᩀ ของล้านนา“药”仍读 y:带喉塞的 y,对应老挝文 ຢ 和兰纳文 ᩀ | |
| ปลา | ปา | "fish": no clusterปลา: ไม่มีเสียงควบกล้ำ“鱼”:没有复辅音 | |
| คำเมือง | គำเมือง | the name of the languageชื่อของภาษา这门语言的名字 | |
| ปี๋ใหม่เมือง | ปีใหม่เมือง | the Northern New Year, as it is often written: ป rises by itself, so ◌๋ goesปีใหม่เมือง ตามที่มักเขียนกัน: ป ออกเสียงขึ้นเอง จึงตัด ◌๋ ออก北部新年,常见的写法:ป 本身就是升调,所以去掉 ◌๋ |
6 · Four scripts, and what each can give · 6 · อักษรสี่ชุด และสิ่งที่แต่ละชุดให้ได้ · 6 · 四种文字,各能给出什么
🇬🇧 English
Thai, Lao, Myanmar and Khmer are all descendants of one Indian script, Brahmi, so most of their letters stand for the same sounds in the same order. The Lanna script is from the same family, through Old Mon and the Mon kingdom of Haripunjaya at Lamphun. Here is what each of the four could add to Thai letters, and what was taken.
🇹🇭 ไทย
อักษรไทย ลาว พม่า และเขมร ล้วนสืบมาจากอักษรอินเดียตัวเดียวกัน คืออักษรพราหมี ตัวอักษรส่วนใหญ่จึงแทนเสียงเดียวกันในลำดับเดียวกัน อักษรธรรมล้านนาก็อยู่ในตระกูลเดียวกัน โดยสืบผ่านอักษรมอญโบราณและอาณาจักรหริภุญชัยของชาวมอญที่ลำพูน ต่อไปนี้คือสิ่งที่อักษรแต่ละชุดเพิ่มให้ตัวอักษรไทยได้ และสิ่งที่เลือกนำมาใช้
🇨🇳 中文
泰文、老挝文、缅甸文和高棉文都源自同一种印度文字,婆罗米文,所以它们大部分字母以相同的顺序代表相同的音。兰纳文也属于这个家族,经由古孟文和孟人在南奔建立的哈里奔猜王国传下来。下面是四种文字各能给泰文字母添加什么,以及采用了什么。
Thaiไทย泰文
The base. Northern Thai readers read it, every phone types it, it has every vowel Kam Mueang needs, ญ for the ny sound and ฮ for h. What it lacks: letters for k, j, t and p said without a puff of air in the low group, and with them a way to keep tones 3 and 5 apart.เป็นฐาน คนเหนืออ่านได้ โทรศัพท์ทุกเครื่องพิมพ์ได้ มีสระทุกตัวที่คำเมืองต้องใช้ มี ญ สำหรับเสียง ny และ ฮ สำหรับ h สิ่งที่ขาดคือ ตัวอักษรสำหรับเสียง k, j, t และ p แบบไม่พ่นลมในกลุ่มอักษรต่ำ และเมื่อขาดตัวเหล่านี้ ก็ไม่มีทางแยกเสียงที่ 3 กับเสียงที่ 5基础。北部的读者都能读,每部手机都能打,北泰语需要的元音它都有,还有表示 ny 的 ญ 和表示 h 的 ฮ。它缺的是低组里不送气的 k、j、t、p 的字母,因而也没法把第 3 和第 5 调分开。
Taken: 29 letters, the vowels, ◌่ and ◌้นำมาใช้: ตัวอักษร 29 ตัว สระ ◌่ และ ◌้采用:29 个字母、元音、◌่ 和 ◌้Laoลาว老挝文
The nearest of the four to Kam Mueang in sound: both have ny (Lao ຍ) beside y (Lao ຢ), in the same words. Lao also shows two things worth copying: a spelling reform that left one letter for each sound and dropped the Pali letters, and six tones written with two marks, as in Lanna. But Lao letters look like Thai ones (a Thai eye reads ຍ as ย), and its four tone marks are Thai's four.ใกล้คำเมืองที่สุดในสี่ชุดในด้านเสียง ทั้งสองมีเสียง ny (ຍ ในอักษรลาว) คู่กับ y (ຢ ในอักษรลาว) ในคำชุดเดียวกัน อักษรลาวยังมีสองเรื่องที่น่าเอาอย่าง คือ การปฏิรูปการสะกดที่ให้เหลือตัวอักษรตัวเดียวต่อหนึ่งเสียงและตัดอักษรบาลีออก และการเขียนหกเสียงวรรณยุกต์ด้วยรูปวรรณยุกต์สองรูปแบบเดียวกับล้านนา แต่ตัวอักษรลาวหน้าตาคล้ายตัวอักษรไทย (คนไทยจะอ่าน ຍ เป็น ย) และรูปวรรณยุกต์สี่รูปของลาวก็เหมือนของไทย四种文字里,它在读音上最接近北泰语:两者都有 ny(老挝文 ຍ)和 y(老挝文 ຢ)之分,而且用在同样的词里。老挝文还有两点值得学习:一次拼写改革,让每个音只剩一个字母并去掉巴利文字母;以及像兰纳文一样,用两个符号写出六个声调。但老挝字母长得像泰文字母(泰国人会把 ຍ 看成 ย),它的四个声调符号也和泰文的一样。
Taken: two ideas, no lettersนำมาใช้: แนวคิดสองข้อ ไม่ใช้ตัวอักษร采用:两个想法,不用字母Myanmar (and Shan)พม่า (และไทใหญ่)缅甸文(和掸文)
The sister of the Lanna script: both come from Old Mon. Burmese marks its creaky, high tone with a dot below (့), the nearest thing in the four scripts to Kam Mueang's tone 3. Shan, a Tai language written in Myanmar letters, gives each tone its own mark. But its letters look nothing like Thai ones to a Northern reader, its ga, ja, da and ba are said with the voice (g, z, d, b), and its marks on a Thai letter break in Chrome and in Safari's engine (part 7).เป็นพี่น้องของอักษรธรรมล้านนา เพราะสืบมาจากอักษรมอญโบราณเหมือนกัน ภาษาพม่าทำเครื่องหมายเสียงสูงแบบเสียงเครียดด้วยจุดข้างล่าง (့) ซึ่งใกล้กับเสียงที่ 3 ของคำเมืองที่สุดในอักษรทั้งสี่ ภาษาไทใหญ่ซึ่งเป็นภาษาตระกูลไทที่เขียนด้วยอักษรพม่า มีเครื่องหมายแยกให้ทุกเสียง แต่สำหรับคนเหนือ ตัวอักษรพม่าดูไม่เหมือนตัวอักษรไทยเลย ตัว ga, ja, da และ ba ของพม่าออกเสียงก้อง (g, z, d, b) และเครื่องหมายของพม่าเมื่อวางบนอักษรไทยจะแสดงผิดใน Chrome และในเอนจินของ Safari (ส่วนที่ 7)兰纳文的姐妹:两者都来自古孟文。缅甸文用下面的一个点(့)标它的紧喉高调,这在四种文字里最接近北泰语的第 3 调。掸语是用缅甸字母书写的台语,它给每个声调都配了专门的符号。但对北部的读者来说,它的字母和泰文毫不相像;它的 ga、ja、da、ba 是浊音(g、z、d、b);而且把它的符号放在泰文字母上,在 Chrome 和 Safari 的引擎里都会出错(第 7 部分)。
Taken: nothingนำมาใช้: ไม่มี采用:无Khmerเขมร高棉文
The parent of Thai letters: the script of Sukhothai was made from Old Khmer. Khmer's second-series letters គ ជ ទ ព are the ancestors of Thai ค ช ท พ, and Khmer still says them the way Kam Mueang does: k, j, t and p with no puff of air. In the low group they give Kam Mueang back the letters Thai lost, and with them the Lanna tone rules write all six tones. Thai vowels and tone marks sit on them in every engine tested, with the page's font.เป็นต้นกำเนิดของตัวอักษรไทย อักษรสุโขทัยทำขึ้นจากอักษรขอมโบราณ ตัวอักษรชุดที่สอง (เสียงโอ) ของเขมร គ ជ ទ ព เป็นบรรพบุรุษของอักษรไทย ค ช ท พ และภาษาเขมรยังออกเสียงตัวเหล่านี้แบบเดียวกับคำเมือง คือ k, j, t และ p แบบไม่พ่นลม เมื่ออยู่ในกลุ่มอักษรต่ำ ตัวอักษรเหล่านี้คืนตัวอักษรที่ไทยเสียไปให้คำเมือง และเมื่อมีตัวอักษรเหล่านี้ กฎวรรณยุกต์ล้านนาก็เขียนได้ครบหกเสียง สระและรูปวรรณยุกต์ไทยวางบนตัวอักษรเหล่านี้ได้ถูกต้องในทุกเอนจินที่ทดสอบ เมื่อใช้ฟอนต์ของหน้านี้泰文字母的祖先:素可泰的文字是根据古高棉文造的。高棉文第二组(o 组)的字母 គ ជ ទ ព 是泰文 ค ช ท พ 的祖先,而且高棉语至今仍像北泰语一样读它们:不送气的 k、j、t、p。放进低组,它们就把泰文丢掉的字母还给了北泰语;有了它们,兰纳文的声调规则就能写出全部六个声调。用本页的字体,泰文元音和声调符号在测试过的每种引擎里都能正确地放在它们上面。
Taken: 4 lettersนำมาใช้: ตัวอักษร 4 ตัว采用:4 个字母For comparison: the Lanna scriptเพื่อเปรียบเทียบ: อักษรธรรมล้านนา作为对照:兰纳文
Kam Mueang's own script. Its low letters ᨣ ᨩ ᨴ ᨻ are said k, j, t and p, and its three letter groups and two tone marks write all six tones. The alphabet takes its rules, and each Khmer letter stands in for one of these four. It is not used as the letters themselves for two reasons: no font on the Windows laptop that was checked holds Lanna and Thai letters together, so the two cannot share a word outside a browser (part 4); and few people read it. A 2004 study at Payap University quotes an estimate that about 0.05% of Northern Thai people could.อักษรของคำเมืองเอง อักษรต่ำ ᨣ ᨩ ᨴ ᨻ ออกเสียง k, j, t และ p และอักษรสามกลุ่มกับรูปวรรณยุกต์สองรูปของอักษรนี้เขียนได้ครบหกเสียง ระบบตัวอักษรนี้นำกฎของอักษรธรรมล้านนามาใช้ และตัวอักษรเขมรแต่ละตัวใช้แทนอักษรล้านนาหนึ่งในสี่ตัวนี้ ที่ไม่ใช้ตัวอักษรล้านนาเองมีเหตุผลสองข้อ ข้อแรก ในแล็ปท็อปวินโดวส์ที่ตรวจ ไม่มีฟอนต์ไหนที่มีทั้งอักษรล้านนาและอักษรไทย นอกเบราว์เซอร์จึงใช้สองอักษรนี้ในคำเดียวกันไม่ได้ (ส่วนที่ 4) ข้อสอง มีคนอ่านได้น้อย งานวิจัยปี 2004 ของมหาวิทยาลัยพายัพอ้างตัวเลขประมาณการว่า คนเหนืออ่านอักษรล้านนาได้ราว 0.05%北泰语自己的文字。它的低组字母 ᨣ ᨩ ᨴ ᨻ 读作 k、j、t、p,三组字母加两个声调符号能写出全部六个声调。这套字母采用的就是它的规则,每个高棉字母都替代这四个字母中的一个。不直接用兰纳字母有两个原因:在检查过的 Windows 笔记本上,没有哪个字体同时包含兰纳文和泰文字母,所以出了浏览器,两者没法出现在同一个词里(第 4 部分);而且会读的人很少。帕亚普大学 2004 年的一项研究引用的估计是,大约 0.05% 的北部泰国人能读。
Taken: the tone rulesนำมาใช้: กฎวรรณยุกต์采用:声调规则
7 · Tested: one script's mark on another script's letter · 7 · ทดสอบ: เครื่องหมายของอักษรชุดหนึ่งบนตัวอักษรของอีกชุด · 7 · 测试:一种文字的符号放在另一种文字的字母上
🇬🇧 English
A tone mark, or a vowel written above or below a letter, is a combining mark: the computer stores it after the letter, and the browser has to place it on the letter. Most fonts hold one script, so a mixed pair asks the browser to put a mark from one font onto a letter from another. draw_test.py drew 16 such pairs, with the Noto fonts, in the three browser engines: Chromium (Chrome and Edge), Firefox, and WebKit, the engine of Safari. The WebKit here is the build that Playwright runs on Windows, not Safari on an iPhone or a Mac.
- Myanmar and Khmer marks after a Thai letter (pairs 3 to 6) are put on a dotted circle ◌ by Chrome and by Safari's engine, so they cannot be used. Firefox draws them, in a place of its own choosing.
- A Lao tone mark sits on a Thai letter in all three (pair 2), but it looks exactly like the Thai one, so a reader would see nothing new.
- Thai marks sit on Lao, Myanmar, Shan and Lanna letters in Chrome and Firefox. Safari's engine shows empty boxes for Myanmar, Shan and Lanna (pairs 14 to 16): no font it has holds both scripts.
- Thai marks on the four Khmer letters (pairs 8 to 12) are right in Chrome and Firefox. Safari's engine needs one font holding both, so this page brings one: KM Alphabet Sans, which is Noto Sans Thai with the four Khmer letters added, 9,620 bytes, made by
build_font.py. With it, all three engines draw them right (the last column).
🇹🇭 ไทย
รูปวรรณยุกต์ หรือสระที่เขียนไว้บนหรือใต้ตัวอักษร เป็นเครื่องหมายประกอบ คอมพิวเตอร์เก็บไว้ต่อจากตัวอักษร แล้วเบราว์เซอร์ต้องวางลงบนตัวอักษรนั้น ฟอนต์ส่วนใหญ่มีอักษรชุดเดียว คู่ที่ผสมกันจึงบังคับให้เบราว์เซอร์วางเครื่องหมายจากฟอนต์หนึ่งลงบนตัวอักษรจากอีกฟอนต์ draw_test.py วาดคู่แบบนี้ 16 คู่ ด้วยฟอนต์ Noto ในเอนจินเบราว์เซอร์สามตัว คือ Chromium (Chrome และ Edge), Firefox และ WebKit ซึ่งเป็นเอนจินของ Safari WebKit ที่ใช้ที่นี่เป็นรุ่นที่ Playwright รันบนวินโดวส์ ไม่ใช่ Safari บน iPhone หรือ Mac
- เครื่องหมายของพม่าและเขมรที่อยู่หลังอักษรไทย (คู่ที่ 3 ถึง 6) ถูก Chrome และเอนจินของ Safari วางบนวงกลมจุดประ ◌ จึงใช้ไม่ได้ ส่วน Firefox วาดให้ แต่วางในตำแหน่งที่มันเลือกเอง
- รูปวรรณยุกต์ลาววางบนอักษรไทยได้ในทั้งสามตัว (คู่ที่ 2) แต่หน้าตาเหมือนของไทยทุกอย่าง ผู้อ่านจึงไม่เห็นอะไรใหม่
- เครื่องหมายไทยวางบนตัวอักษรลาว พม่า ไทใหญ่ และล้านนาได้ใน Chrome และ Firefox ส่วนเอนจินของ Safari แสดงเป็นกล่องว่างสำหรับพม่า ไทใหญ่ และล้านนา (คู่ที่ 14 ถึง 16) เพราะไม่มีฟอนต์ไหนในเครื่องที่มีอักษรทั้งสองชุด
- เครื่องหมายไทยบนตัวอักษรเขมรสี่ตัว (คู่ที่ 8 ถึง 12) ถูกต้องใน Chrome และ Firefox แต่เอนจินของ Safari ต้องมีฟอนต์เดียวที่มีทั้งสองอักษร หน้านี้จึงมีฟอนต์ของตัวเอง คือ KM Alphabet Sans ซึ่งคือ Noto Sans Thai ที่เพิ่มตัวอักษรเขมรสี่ตัว ขนาด 9,620 ไบต์ สร้างด้วย
build_font.pyเมื่อใช้ฟอนต์นี้ เอนจินทั้งสามวาดได้ถูกต้อง (คอลัมน์สุดท้าย)
🇨🇳 中文
声调符号,或写在字母上方、下方的元音,是组合符号:电脑把它存放在字母后面,浏览器要把它放到字母上。大多数字体只包含一种文字,所以混合的一对字符等于要浏览器把一个字体里的符号放到另一个字体的字母上。draw_test.py 用 Noto 字体,在三种浏览器引擎里画了 16 对这样的组合:Chromium(Chrome 和 Edge)、Firefox,以及 Safari 的引擎 WebKit。这里的 WebKit 是 Playwright 在 Windows 上运行的版本,不是 iPhone 或 Mac 上的 Safari。
- 泰文字母后面的缅甸文和高棉文符号(第 3 到 6 对),Chrome 和 Safari 的引擎都把它们放在虚线圆圈 ◌ 上,所以不能用。Firefox 会画出来,但位置由它自己决定。
- 老挝文声调符号在三种引擎里都能放在泰文字母上(第 2 对),但它和泰文的一模一样,读者看不出任何新东西。
- 泰文符号在 Chrome 和 Firefox 里能放在老挝文、缅甸文、掸文和兰纳文字母上。Safari 的引擎对缅甸文、掸文和兰纳文显示空方框(第 14 到 16 对):它没有任何字体同时包含两种文字。
- 四个高棉字母上的泰文符号(第 8 到 12 对)在 Chrome 和 Firefox 里是正确的。Safari 的引擎需要一个同时包含两者的字体,所以本页自带一个:KM Alphabet Sans,就是加上四个高棉字母的 Noto Sans Thai,9,620 字节,由
build_font.py生成。有了它,三种引擎都画得正确(最后一列)。
draw_test.py saves. The last column is Safari's engine again, with the page's own font.คู่ทั้ง 16 คู่ตามที่แต่ละเอนจินวาด จากภาพที่ draw_test.py บันทึกไว้ คอลัมน์สุดท้ายคือเอนจินของ Safari อีกครั้ง โดยใช้ฟอนต์ของหน้านี้16 对组合在各引擎中的样子,取自 draw_test.py 保存的图片。最后一列又是 Safari 的引擎,用的是本页自己的字体。And here are the same 16 pairs, drawn now by your own browser with this page's fonts:และนี่คือคู่เดิมทั้ง 16 คู่ ที่เบราว์เซอร์ของคุณเองวาดอยู่ตอนนี้ ด้วยฟอนต์ของหน้านี้:下面是同样的 16 对,由你自己的浏览器现在用本页的字体画出来:
What draw_test.py printed: how much wider each pair is than its letter alone. Empty boxes do not show in these numbers, only in the pictures.สิ่งที่ draw_test.py พิมพ์ออกมา: แต่ละคู่กว้างกว่าตัวอักษรตัวเดียวเท่าไร กล่องว่างไม่ปรากฏในตัวเลขเหล่านี้ เห็นได้เฉพาะในภาพdraw_test.py 打印的结果:每一对比单个字母宽多少。空方框不会出现在这些数字里,只能在图片中看到。
How much wider each pair is than its letter alone, as a share of the font size (52 px).
0 = the mark sits on the letter. About 0.5 or more = the mark is on a dotted circle.
With the Noto fonts alone:
pair chromium firefox webkit
1 Thai mai tho on Thai ko (for comparison) 0.00 0.00 0.00
2 Lao mai tho on Thai ko 0.00 0.00 -0.03
3 Myanmar dot below (creaky tone) after Thai 0.59 0.00 0.79
4 Myanmar visarga (high tone) after Thai 0.94 0.35 0.94
5 Shan tone mark 3 after Thai 0.94 0.35 0.94
6 Khmer triisap on Thai ko 0.64 0.00 0.57
7 Tai Tham Khuen tone 3 on Thai ko 0.00 0.00 0.60
8 Thai mai ek on Khmer ko 0.00 0.00 0.21
9 Thai mai tho on Khmer co 0.00 0.00 0.13
10 Thai sara i on Khmer to 0.00 -0.00 0.13
11 Thai sara ii and mai ek on Khmer po 0.00 -0.00 0.17
12 Thai sara u (below) on Khmer ko 0.00 -0.00 0.21
13 Thai sara i on Lao nyo 0.00 0.00 0.02
14 Thai sara i on Myanmar nya 0.00 0.00 0.09
15 Thai sara i on Shan nya 0.00 -0.00 0.10
16 Thai mai ek on Tai Tham ga 0.00 -0.00 0.54
With KM Alphabet Sans first:
pair chromium firefox webkit
1 Thai mai tho on Thai ko (for comparison) 0.00 0.00 0.00
2 Lao mai tho on Thai ko 0.00 0.00 -0.03
3 Myanmar dot below (creaky tone) after Thai 0.59 0.00 0.79
4 Myanmar visarga (high tone) after Thai 0.94 0.35 0.94
5 Shan tone mark 3 after Thai 0.94 0.35 0.94
6 Khmer triisap on Thai ko 0.64 0.00 0.57
7 Tai Tham Khuen tone 3 on Thai ko 0.00 0.00 0.60
8 Thai mai ek on Khmer ko 0.00 0.00 0.00
9 Thai mai tho on Khmer co 0.00 0.00 0.00
10 Thai sara i on Khmer to 0.00 -0.00 0.00
11 Thai sara ii and mai ek on Khmer po 0.00 -0.00 0.00
12 Thai sara u (below) on Khmer ko 0.00 -0.00 0.00
13 Thai sara i on Lao nyo 0.00 0.00 0.02
14 Thai sara i on Myanmar nya 0.00 0.00 0.09
15 Thai sara i on Shan nya 0.00 -0.00 0.10
16 Thai mai ek on Tai Tham ga 0.00 -0.00 0.54
Pictures saved: draw_<engine>_noto.png and draw_<engine>_page_font.png, one each per engine,
and all of them side by side in draw_test.png.
The file: draw_test.py · ไฟล์: draw_test.py · 文件:draw_test.py
"""Can a browser put a mark from one script on a letter from another? Drawn in three engines.
Each pair is drawn in Chromium (Chrome, Edge), Firefox and WebKit (the engine of Safari), twice:
with the Noto fonts alone, and with the page's own font, KM Alphabet Sans, put first. One
picture per engine and font is saved, to be checked by eye. A mark the browser cannot place is
drawn on a dotted circle (◌) of its own; a pair no font can draw shows empty boxes.
The widths are printed too: a mark that sits on its letter takes no room, so the pair is as
wide as the letter alone. Empty boxes do not show up in the widths, only in the pictures.
It also puts the pictures of every pair side by side in one image, draw_test.png, in the same
folder: Chromium, Firefox and WebKit with the Noto fonts, and WebKit with the page's font.
Run it with: python draw_test.py [folder for the pictures] (needs the internet for the fonts)
Needs Playwright with three browsers and Pillow: pip install playwright pillow, then
python -m playwright install chromium firefox webkit
"""
import base64
import os
import sys
import tempfile
from PIL import Image, ImageDraw, ImageFont
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
# (what is tried, the pair, the same text without the mark)
PAIRS = [
("Thai mai tho on Thai ko (for comparison)", "ก้า", "กา"),
("Lao mai tho on Thai ko", "ก້า", "กา"),
("Myanmar dot below (creaky tone) after Thai", "กา့", "กา"),
("Myanmar visarga (high tone) after Thai", "กาး", "กา"),
("Shan tone mark 3 after Thai", "กาႈ", "กา"),
("Khmer triisap on Thai ko", "ก៊า", "กา"),
("Tai Tham Khuen tone 3 on Thai ko", "ก᩷า", "กา"),
("Thai mai ek on Khmer ko", "គ่า", "គา"),
("Thai mai tho on Khmer co", "ជ้า", "ជา"),
("Thai sara i on Khmer to", "ទิ", "ទ"),
("Thai sara ii and mai ek on Khmer po", "ពี่", "ព"),
("Thai sara u (below) on Khmer ko", "គุ", "គ"),
("Thai sara i on Lao nyo", "ຍิ", "ຍ"),
("Thai sara i on Myanmar nya", "ညิ", "ည"),
("Thai sara i on Shan nya", "ၺิ", "ၺ"),
("Thai mai ek on Tai Tham ga", "ᨣ่", "ᨣ"),
]
ENGINES = ("chromium", "firefox", "webkit")
NOTO = "'Noto Sans Thai','Noto Sans Khmer','Noto Sans Lao','Noto Sans Myanmar','Noto Sans Tai Tham'"
def page_html(own_font):
font = open(os.path.join(HERE, "km_alphabet_sans-Regular.woff2"), "rb").read()
face = ("@font-face{font-family:'KM Alphabet Sans';src:url(data:font/woff2;base64,"
+ base64.b64encode(font).decode() + ") format('woff2')}")
stack = ("'KM Alphabet Sans'," if own_font else "") + NOTO
rows = "".join(f"<div class=r><span>{pair}</span><span>{base}</span><b>{i + 1}</b></div>"
for i, (name, pair, base) in enumerate(PAIRS))
return f"""<!doctype html><meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai&family=Noto+Sans+Lao&family=Noto+Sans+Khmer&family=Noto+Sans+Myanmar&family=Noto+Sans+Tai+Tham&display=block" rel="stylesheet">
<style>{face} body{{margin:8px;background:#fff}} span{{font:52px {stack};white-space:pre;display:inline-block}}
.r{{display:flex;gap:16px;align-items:center;border-bottom:1px solid #ccc;height:84px}} b{{font:14px sans-serif}}</style>
<body>{rows}</body>"""
MEASURE = """async () => {
await document.fonts.ready;
await new Promise(r => setTimeout(r, 2000));
return [...document.querySelectorAll('.r')].map(r => {
const [pair, base] = r.querySelectorAll('span');
return [pair.getBoundingClientRect().width, base.getBoundingClientRect().width];
});
}"""
def main():
out = sys.argv[1] if len(sys.argv) > 1 else tempfile.mkdtemp(prefix="draw_test_")
os.makedirs(out, exist_ok=True)
widths = {}
with sync_playwright() as p:
for engine in ENGINES:
browser = getattr(p, engine).launch()
for own_font in (False, True):
page = browser.new_page(viewport={"width": 330, "height": 400})
page.set_content(page_html(own_font), wait_until="networkidle")
widths[engine, own_font] = page.evaluate(MEASURE)
font = "page_font" if own_font else "noto"
page.screenshot(path=os.path.join(out, f"draw_{engine}_{font}.png"), full_page=True)
for i, cell in enumerate(page.query_selector_all(".r span:first-child")):
cell.screenshot(path=os.path.join(out, f"cell_{engine}_{font}_{i + 1}.png"))
page.close()
browser.close()
print("How much wider each pair is than its letter alone, as a share of the font size (52 px).")
print("0 = the mark sits on the letter. About 0.5 or more = the mark is on a dotted circle.\n")
for own_font in (False, True):
print("With KM Alphabet Sans first:" if own_font else "With the Noto fonts alone:")
print(f" {'pair':44}" + "".join(f"{e:>10}" for e in ENGINES))
for i, (name, pair, base) in enumerate(PAIRS):
cells = ""
for engine in ENGINES:
grown = (widths[engine, own_font][i][0] - widths[engine, own_font][i][1]) / 52
cells += f"{grown:>10.2f}"
print(f"{i + 1:>2} {name:44}{cells}")
print()
compose(out)
print("Pictures saved: draw_<engine>_noto.png and draw_<engine>_page_font.png, one each per engine,")
print("and all of them side by side in draw_test.png.")
def compose(out):
"""One image: a row for each pair, a column for each engine, and WebKit again with the page's font."""
columns = [("chromium", "noto", "Chrome, Edge"), ("firefox", "noto", "Firefox"),
("webkit", "noto", "Safari's engine"), ("webkit", "page_font", "Safari's engine,")]
cell_w, cell_h, label_w, head_h = 190, 84, 380, 64
sheet = Image.new("RGB", (label_w + cell_w * len(columns), head_h + cell_h * len(PAIRS)), "white")
draw = ImageDraw.Draw(sheet)
text = ImageFont.load_default(size=17)
for c, (engine, font, title) in enumerate(columns):
x = label_w + c * cell_w
draw.text((x + 10, 12), title, fill="black", font=text)
draw.text((x + 10, 34), "page font" if font == "page_font" else "Noto fonts", fill="#555555", font=text)
for i, (name, pair, base) in enumerate(PAIRS):
y = head_h + i * cell_h
draw.line((0, y, sheet.width, y), fill="#cccccc")
draw.text((10, y + 30), f"{i + 1:>2} {name}", fill="black", font=text)
for c, (engine, font, title) in enumerate(columns):
cell = Image.open(os.path.join(out, f"cell_{engine}_{font}_{i + 1}.png")).convert("RGB")
cell.thumbnail((cell_w - 20, cell_h - 8))
sheet.paste(cell, (label_w + c * cell_w + 10, y + (cell_h - cell.height) // 2))
sheet.save(os.path.join(out, "draw_test.png"), optimize=True)
if __name__ == "__main__":
main()
8 · What it means for the voice project · 8 · ความหมายต่อโครงการเสียงพูด · 8 · 这对语音项目意味着什么
🇬🇧 English
The voice project's reading script has 1468 sentences, written the way Kam Mueang is usually written: they are the sentences of the recorder. script_scan.py read all of them with alphabet.js: 19258 syllables. 63% are already written the alphabet's way. A rule could rewrite another 13%: a Thai cluster, a Pali or silent letter, a hidden vowel, or ◌๋ and ◌๊ on ก จ ต ป. The other 24% only a Kam Mueang speaker can settle, because the spelling stands for two different sounds. Most of those are ค ช ท พ (1067), ก จ ต ป with ◌้ (1219), and ก จ ต ป with no mark (1333).
That is work the speaker reviewing the script does anyway: reading each sentence aloud, they know which tone and which sound each word has. But the voice will learn from Kam Mueang as it is usually written in Thai letters: that is the spelling people, word lists and chatbots write, so it is the spelling the voice will be asked to read. The alphabet stays a proposal for writing the sounds.
Next steps, not done yet:
- A native speaker checks the alphabet itself, and the tones of the test words.
- The recorder's "Change this sentence" box has a keyboard with the alphabet's letters, the four Khmer letters among them, and shows the tone of each syllable as it is typed. A corrected sentence stays in ordinary Thai letters; a speaker who wants the alphabet's letters or Lanna letters puts them in the note.
- The small voice that needs no graphics card (Piper) gets its text-to-sound step from this page's reader,
alphabet.js, which reads ordinary Thai spelling too and splits Kam Mueang into syllables with six tones. Piper's own Thai phonemizer drops Kam Mueang words without a warning. - A speech recogniser that writes the Lanna script: one that turns speech into Thai letters, followed by the writer of part 4. It waits for the native speaker's recordings.
To type the alphabet outside this page: on Windows, the built-in font Leelawadee UI holds Thai and the four Khmer letters (font_check.py found it). On other computers, install KM Alphabet Sans: km_alphabet_sans-Regular.ttf, under the SIL Open Font License (OFL.txt).
🇹🇭 ไทย
บทอ่านของโครงการเสียงพูดมี 1468 ประโยค เขียนแบบที่คนมักเขียนคำเมืองกัน คือประโยคชุดเดียวกับในเครื่องอัดเสียง script_scan.py อ่านทุกประโยคด้วย alphabet.js ได้ 19258 พยางค์ ร้อยละ 63 เขียนแบบของระบบนี้อยู่แล้ว อีกร้อยละ 13 ใช้กฎเปลี่ยนให้ได้ คือเสียงควบกล้ำแบบไทย อักษรบาลีหรือตัวการันต์ สระลดรูป หรือ ◌๋ และ ◌๊ บน ก จ ต ป ส่วนที่เหลือร้อยละ 24 ต้องให้คนที่พูดคำเมืองตัดสิน เพราะการสะกดแทนได้สองเสียง ส่วนใหญ่คือ ค ช ท พ (1067), ก จ ต ป ที่มี ◌้ (1219) และ ก จ ต ป ที่ไม่มีรูปวรรณยุกต์ (1333)
งานนี้คือสิ่งที่ผู้ตรวจบทอ่านทำอยู่แล้ว เมื่ออ่านแต่ละประโยคออกเสียง ผู้ตรวจรู้ว่าแต่ละคำมีวรรณยุกต์และเสียงอะไร แต่เสียงพูดจะเรียนรู้จากคำเมืองที่เขียนด้วยตัวอักษรไทยแบบที่คนมักเขียนกัน เพราะเป็นตัวสะกดที่คน รายการคำ และแชตบอตใช้เขียน จึงเป็นตัวสะกดที่เสียงพูดจะต้องอ่าน ระบบตัวอักษรนี้ยังคงเป็นข้อเสนอสำหรับเขียนให้ตรงกับเสียง
ขั้นต่อไป ซึ่งยังไม่ได้ทำ:
- เจ้าของภาษาตรวจระบบตัวอักษรนี้ และวรรณยุกต์ของคำทดสอบ
- ช่อง "แก้ประโยคนี้" ของเครื่องอัดเสียงมีแป้นพิมพ์ตัวอักษรของระบบนี้ รวมทั้งตัวอักษรเขมรสี่ตัว และแสดงวรรณยุกต์ของแต่ละพยางค์ขณะพิมพ์ ประโยคที่แก้ยังเขียนด้วยตัวอักษรไทยตามปกติ ส่วนตัวอักษรของระบบนี้หรือตัวเมือง ถ้าผู้พูดอยากใช้ ให้ใส่ไว้ในหมายเหตุ
- เสียงพูดรุ่นเล็กที่ไม่ต้องใช้การ์ดจอ (Piper) จะใช้ตัวอ่านของหน้านี้ คือ
alphabet.jsเป็นขั้นแปลงข้อความเป็นเสียง เพราะอ่านตัวสะกดไทยแบบปกติได้ด้วย และแบ่งคำเมืองเป็นพยางค์พร้อมวรรณยุกต์หกเสียง ส่วนตัวแปลงอักษรเป็นเสียงภาษาไทยของ Piper เองทำคำเมืองหายไปโดยไม่เตือน - เครื่องรู้จำเสียงที่เขียนอักษรธรรมล้านนา คือตัวที่แปลงเสียงพูดเป็นตัวอักษรไทย ต่อด้วยตัวเขียนในส่วนที่ 4 ซึ่งต้องรอเสียงที่อัดจากเจ้าของภาษา
ถ้าจะพิมพ์ระบบตัวอักษรนี้นอกหน้านี้: บนวินโดวส์ ฟอนต์ Leelawadee UI ที่มากับเครื่องมีทั้งอักษรไทยและตัวอักษรเขมรสี่ตัว (font_check.py เป็นตัวที่พบ) บนคอมพิวเตอร์อื่น ให้ติดตั้ง KM Alphabet Sans: km_alphabet_sans-Regular.ttf ภายใต้สัญญาอนุญาต SIL Open Font License (OFL.txt)
🇨🇳 中文
语音项目的朗读稿有 1468 句,按北泰语通常的写法书写:就是录音页里的句子。script_scan.py 用 alphabet.js 读了全部句子:共 19258 个音节。其中 63% 已经是这套字母的写法。另有 13% 可以用规则改写:泰文复辅音、巴利文字母或不发音的字母、隐藏的元音,或 ก จ ต ป 上的 ◌๋ 和 ◌๊。剩下的 24% 只有说北泰语的人才能定下来,因为拼写可以代表两种不同的读音。其中大部分是 ค ช ท พ(1067)、带 ◌้ 的 ก จ ต ป(1219)和不带符号的 ก จ ต ป(1333)。
这正是审稿的人本来就要做的事:把每句话读出来时,他们知道每个词是什么声调、什么音。不过,语音将从北泰语通常的泰文字母写法中学习:这是人们、词表和聊天机器人所用的拼法,也就是语音将来要读的拼法。这套字母仍然是一种记录读音的提议。
接下来的步骤,还没有做:
- 请母语者检查这套字母本身,以及测试词的声调。
- 录音页的“修改这句话”框有一个包含这套字母的键盘,四个高棉字母也在其中,打字时会显示每个音节的声调。修改后的句子仍用普通的泰文字母书写;说话人想用这套字母或兰纳文时,就写在备注里。
- 不需要显卡的小型语音(Piper)将用本页的读音器
alphabet.js作为文字转语音的步骤:它也能读普通的泰文拼法,并把北泰语分成带六个声调的音节。Piper 自带的泰语音素转换器会在没有任何警告的情况下漏掉北泰语的词。 - 能写兰纳文的语音识别器:先把语音转成泰文字母,再接上第 4 部分的转写器。它要等母语者的录音。
要在本页以外用这套字母打字:在 Windows 上,自带的字体 Leelawadee UI 同时包含泰文和四个高棉字母(是 font_check.py 找到的)。在别的电脑上,请安装 KM Alphabet Sans:km_alphabet_sans-Regular.ttf,采用 SIL Open Font License(OFL.txt)。
What script_scan.py printed:สิ่งที่ script_scan.py พิมพ์ออกมา:script_scan.py 打印的结果:
1468 sentences, 19331 syllables as alphabet.js splits them.
Only a Kam Mueang speaker can settle: 4607 (24%)
1102 ค ช ท พ: said with a puff of air, or without one (then a Khmer letter) e.g. ชอบ ช่วง ที่ พบ เพื่อ ท้าย
385 ร: said h (ฮ), l or r e.g. รอบ รถ รวม ร้าน รู รับ
461 ย: said ny (ญ) or y e.g. เย็น ยะ เยียะ ยา ย่าง เยี๊ยะ
1178 ก จ ต ป with ◌้: tone 3, or tone 5 in the Central Thai way of writing it e.g. ตี้ ตั้ง ต้อง ติ้ด ป้อ แก้
1419 ก จ ต ป with no mark: tone 1, or tone 4 in the Central Thai way of writing it e.g. ไป เตียว จาร กำ กาย ตาง
46 ◌๊ or ◌๋ on a letter other than ก จ ต ป e.g. อั๋น อ๋อย ดั๊ก อุ๊ย เดี๋ยว บี๊ด
16 a letter that cannot be read where it is e.g. ิ ฤ ์ ะ
A rule can rewrite (a speaker should still look): 2411 (12%)
419 Thai cluster (กล, ปร, ...) e.g. กลุ่ม เพราะ เครื่อง ประ บรร พริก
239 hidden vowel (ส in สบาย) e.g. ส ก่ บ่ ห ษ อ
491 Pali letter (ศ, ษ, ณ, ธ, ...) or Thai final (ศ, ร, ล, ...) e.g. กาศ หาร ภาค วัต งข ฬิ
113 silent letter (ย์) e.g. ย์ ณ์ ร์ ด์ ธิ์ ดิ์
1149 ◌๋ or ◌๊ on ก จ ต ป e.g. กิ๋น กั๋น ตี๋ ปิ๊ก ก๋าน เป๋า
Already written the alphabet's way: 12313 (64%)
What font_check.py printed on the Windows 11 laptop:สิ่งที่ font_check.py พิมพ์ออกมาบนแล็ปท็อปวินโดวส์ 11:font_check.py 在 Windows 11 笔记本上打印的结果:
Font folders: C:\windows\Fonts, ~\AppData\Local\Microsoft\Windows\Fonts
Thai 4 fonts: Leelawadee UI, Leelawadee UI Semilight, Microsoft Sans Serif, Tahoma
Khmer 4 2 fonts: Leelawadee UI, Leelawadee UI Semilight
Lao 2 fonts: Leelawadee UI, Leelawadee UI Semilight
Myanmar 1 fonts: Myanmar Text
Tai Tham 1 fonts: Sans Serif Collection
Thai and the four Khmer letters in one font: Leelawadee UI, Leelawadee UI Semilight
9 · What this page is not, yet · 9 · สิ่งที่หน้านี้ยังไม่ใช่ · 9 · 本页还不是什么
🇬🇧 English
- Not checked by a native speaker. The tone rules and the test words come from published sources; the spelling of real sentences needs Kam Mueang ears.
- The tones of Chiang Mai. Lampang, Nan, Phrae and other provinces have tone systems of their own, and some have no เอือ. The letters would stay; the reading rules might change.
- A tool for the voice project, not a replacement for the Lanna script. Its tone rules are the Lanna script's own, and every letter's Lanna partner is shown beside it.
- Some borrowed words cannot be written. Kam Mueang's own words never put b, d or the glottal stop with tones 1, 5 or 6, so the alphabet has no letters for that.
- Tested in Chromium, Firefox and WebKit on Windows. Safari on an iPhone or a Mac was not tested.
- The Lanna-script writer has not been checked by a Lanna reader either. Kam Mueang is written without spaces, so it cannot see where a word ends: a short a is written bare, as in the first syllable of a longer word (ᨠ in ᨠᨳᩥ᩠ᨶ), even where a word of its own takes ᩡ. A word the list does not hold is spelled by its sound.
🇹🇭 ไทย
- ยังไม่มีเจ้าของภาษาตรวจ กฎวรรณยุกต์และคำทดสอบมาจากแหล่งที่ตีพิมพ์แล้ว แต่การสะกดประโยคจริงต้องใช้หูของคนที่พูดคำเมือง
- เป็นวรรณยุกต์ของเชียงใหม่ ลำปาง น่าน แพร่ และจังหวัดอื่นมีระบบวรรณยุกต์ของตัวเอง และบางแห่งไม่มีสระเอือ ตัวอักษรคงใช้ได้เหมือนเดิม แต่กฎการอ่านอาจเปลี่ยน
- เป็นเครื่องมือของโครงการเสียงพูด ไม่ได้มาแทนอักษรธรรมล้านนา กฎวรรณยุกต์ของระบบนี้คือกฎของอักษรธรรมล้านนาเอง และตัวอักษรทุกตัวแสดงคู่ของมันในอักษรล้านนาไว้ข้าง ๆ
- คำยืมบางคำเขียนไม่ได้ คำเมืองแท้ไม่มีคำที่ใช้ b, d หรือเสียงกักในลำคอกับวรรณยุกต์ที่ 1, 5 หรือ 6 ระบบตัวอักษรนี้จึงไม่มีตัวอักษรสำหรับกรณีนั้น
- ทดสอบใน Chromium, Firefox และ WebKit บนวินโดวส์ ยังไม่ได้ทดสอบ Safari บน iPhone หรือ Mac
- ตัวเขียนอักษรธรรมล้านนาก็ยังไม่มีผู้ที่อ่านอักษรล้านนาได้มาตรวจ คำเมืองเขียนติดกันโดยไม่เว้นวรรค ตัวเขียนจึงไม่รู้ว่าคำจบตรงไหน สระอะเสียงสั้นจึงเขียนเป็นพยัญชนะเปล่า ๆ แบบพยางค์แรกของคำที่ยาวกว่า (ᨠ ใน ᨠᨳᩥ᩠ᨶ) แม้ในที่ที่คำเดี่ยว ๆ ต้องมี ᩡ คำที่ไม่อยู่ในรายการจะสะกดตามเสียง
🇨🇳 中文
- 还没有母语者检查过。声调规则和测试词来自已出版的资料;真实句子的拼写需要会说北泰语的耳朵。
- 这是清迈的声调。南邦、难府、帕府等府有各自的声调系统,有些地方没有 เอือ 这个元音。字母可以不变,读法规则可能要改。
- 它是语音项目的工具,不是用来取代兰纳文的。它的声调规则就是兰纳文自己的规则,每个字母旁边都标出了对应的兰纳字母。
- 有些外来词写不出来。北泰语固有词里从不把 b、d 或喉塞音和第 1、5、6 调配在一起,所以这套字母没有为此准备字母。
- 在 Windows 上的 Chromium、Firefox 和 WebKit 里测试过。没有测试 iPhone 或 Mac 上的 Safari。
- 兰纳文转写器也还没有懂兰纳文的人检查过。北泰语书写时词与词之间不留空格,所以它看不出一个词在哪里结束:短元音 a 一律写成光辅音,像较长词的第一个音节那样(ᨠᨳᩥ᩠ᨶ 里的 ᨠ),即使单独成词时应该写 ᩡ。词表里没有的词按读音拼写。
10 · The files · 10 · ไฟล์ · 10 · 文件
Everything above was made by these files, kept beside the page on this site. alphabet.js is the reader itself: the letters, the groups, the vowel spellings and the tone table. It writes the Lanna script too. Thai Wiktionary's words are in wiktionary_words.tsv and lanna_words.js, under Wiktionary's licence, CC BY-SA 4.0.ทุกอย่างข้างบนสร้างจากไฟล์เหล่านี้ ซึ่งเก็บไว้ข้างหน้านี้บนเว็บไซต์ alphabet.js คือตัวเครื่องอ่านเอง มีตัวอักษร กลุ่ม การสะกดสระ และตารางวรรณยุกต์ และยังเขียนอักษรธรรมล้านนาด้วย คำของวิกิพจนานุกรมภาษาไทยอยู่ใน wiktionary_words.tsv และ lanna_words.js ภายใต้สัญญาอนุญาตของวิกิพจนานุกรม คือ CC BY-SA 4.0上面的一切都由这些文件生成,它们就放在本站这一页的旁边。alphabet.js 就是读字器本身:字母、分组、元音拼写和声调表。它也能写兰纳文。泰文维基词典的词在 wiktionary_words.tsv 和 lanna_words.js 里,采用维基词典的许可协议 CC BY-SA 4.0。
alphabet.js: the alphabet, and the reader · alphabet.js: ระบบตัวอักษรและเครื่องอ่าน · alphabet.js:这套字母和读字器
// Kam Mueang alphabet, draft 1 (krueng.ai/kammuang_alphabet.html).
// Thai letters, four Khmer letters, and the tone rules of the Lanna (Tai Tham) script.
// read(text) splits Kam Mueang text into syllables and gives each one its sounds and its tone.
// Tones are the six of Chiang Mai, with the numbers the Lanna dictionary (2007) gives them.
// Ordinary Thai spelling is read too, and every place it differs from the alphabet is named.
// lanna(text) writes the same text in the Lanna script, by rules and a word list (lanna_words.js).
const KMA = (function () {
// The letters of the alphabet: [sound, tone group, the Tai Tham letter it stands for, script].
// Group 1 rises when no mark is written, group 2 is mid with no mark, group 3 is the low letters.
const LETTERS = {
"ก": ["k", 1, "ᨠ", "Thai"], "จ": ["tɕ", 1, "ᨧ", "Thai"], "ต": ["t", 1, "ᨲ", "Thai"],
"ป": ["p", 1, "ᨸ", "Thai"], "ข": ["kʰ", 1, "ᨡ", "Thai"], "ฉ": ["tɕʰ", 1, "", "Thai"],
"ถ": ["tʰ", 1, "ᨳ", "Thai"], "ผ": ["pʰ", 1, "ᨹ", "Thai"], "ฝ": ["f", 1, "ᨺ", "Thai"],
"ส": ["s", 1, "ᩈ", "Thai"], "ห": ["h", 1, "ᩉ", "Thai"],
"อ": ["ʔ", 2, "ᩋ", "Thai"], "บ": ["b", 2, "ᨷ", "Thai"], "ด": ["d", 2, "ᨯ", "Thai"],
"គ": ["k", 3, "ᨣ", "Khmer"], "ជ": ["tɕ", 3, "ᨩ", "Khmer"], "ទ": ["t", 3, "ᨴ", "Khmer"],
"ព": ["p", 3, "ᨻ", "Khmer"], "ค": ["kʰ", 3, "ᨤ", "Thai"], "ช": ["tɕʰ", 3, "", "Thai"],
"ท": ["tʰ", 3, "ᨵ", "Thai"], "พ": ["pʰ", 3, "ᨽ", "Thai"], "ง": ["ŋ", 3, "ᨦ", "Thai"],
"ญ": ["ɲ", 3, "ᨿ", "Thai"], "น": ["n", 3, "ᨶ", "Thai"], "ม": ["m", 3, "ᨾ", "Thai"],
"ซ": ["s", 3, "ᨪ", "Thai"], "ฟ": ["f", 3, "ᨼ", "Thai"], "ฮ": ["h", 3, "ᩁ", "Thai"],
"ล": ["l", 3, "ᩃ", "Thai"], "ว": ["w", 3, "ᩅ", "Thai"], "ย": ["j", 3, "", "Thai"],
"ร": ["r", 3, "ᩁ", "Thai"],
};
// Thai letters the alphabet leaves out: [sound, tone group, what to write instead].
const OTHER_LETTERS = {
"ฆ": ["kʰ", 3, "ค or គ"], "ฌ": ["tɕʰ", 3, "ช, ជ or ซ"], "ฎ": ["d", 2, "ด"], "ฏ": ["t", 1, "ต"],
"ฐ": ["tʰ", 1, "ถ"], "ฑ": ["tʰ", 3, "ท or ទ"], "ฒ": ["tʰ", 3, "ท or ទ"], "ณ": ["n", 3, "น"],
"ธ": ["tʰ", 3, "ท or ទ"], "ภ": ["pʰ", 3, "พ or ព"], "ศ": ["s", 1, "ส"], "ษ": ["s", 1, "ส"],
"ฬ": ["l", 3, "ล"], "ฃ": ["kʰ", 1, "ข"], "ฅ": ["kʰ", 3, "ค"],
};
// Thai clusters, which Kam Mueang does not have: [the sound it usually says, what to write].
// An ร after the first letter becomes a puff of air (ปร is said like ผ); an ล is dropped.
const CLUSTERS = {
"กร": ["kʰ", "ข"], "ขร": ["kʰ", "ข"], "คร": ["kʰ", "ค"], "ตร": ["tʰ", "ถ"], "ทร": ["tʰ", "ท"],
"ปร": ["pʰ", "ผ"], "พร": ["pʰ", "พ"], "บร": ["pʰ", "ผ or พ"],
"กล": ["k", "ก"], "ขล": ["kʰ", "ข"], "คล": ["k", "គ"], "ปล": ["p", "ป"], "ผล": ["pʰ", "ผ"],
"พล": ["p", "ព"], "บล": ["b", "บ"],
};
// ห before one of these puts it in group 1; อ before ย makes the glottal y of group 2.
const SONORANTS = "งญนมยลว";
const FINALS = {"ก": "k", "ด": "t", "บ": "p", "ง": "ŋ", "น": "n", "ม": "m", "ย": "j", "ว": "w"};
// Final letters of Thai spelling the alphabet replaces: [sound, what to write].
const OTHER_FINALS = {
"จ": ["t", "ด"], "ช": ["t", "ด"], "ซ": ["t", "ด"], "ส": ["t", "ด"], "ศ": ["t", "ด"], "ษ": ["t", "ด"],
"ต": ["t", "ด"], "ถ": ["t", "ด"], "ท": ["t", "ด"], "ธ": ["t", "ด"], "ฎ": ["t", "ด"], "ฏ": ["t", "ด"],
"ฐ": ["t", "ด"], "ฑ": ["t", "ด"], "ฒ": ["t", "ด"], "ณ": ["n", "น"], "ญ": ["n", "น"], "ร": ["n", "น"],
"ล": ["n", "น"], "ฬ": ["n", "น"], "ข": ["k", "ก"], "ค": ["k", "ก"], "ฆ": ["k", "ก"], "ป": ["p", "บ"],
"พ": ["p", "บ"], "ฟ": ["p", "บ"], "ภ": ["p", "บ"],
};
const TONE_MARKS = {"่": "B", "้": "C"};
// The six tones: [contour in tone letters, English, Thai name, Chinese].
const TONES = {
1: ["˨˦", "low rising", "จัตวา", "低升调"],
2: ["˨˩", "low falling", "เอก", "低降调"],
3: ["˦˦ʔ", "high level, ends in a catch of the throat", "ตรีปลายโท", "高平调,带喉塞"],
4: ["˧˧", "mid level", "สามัญ", "中平调"],
5: ["˦˨", "high falling", "โท", "高降调"],
6: ["˦˥", "high rising", "ตรี", "高升调"],
};
// Which tone each group gets: A = no mark, B = ◌่, C = ◌้, DS/DL = a checked syllable
// (it ends in k, t, p or a short open vowel) with a short or a long vowel.
const TONE_OF = {
A: {1: 1, 2: 4, 3: 4}, B: {1: 2, 2: 2, 3: 5}, C: {1: 3, 2: 3, 3: 6},
DS: {1: 1, 2: 1, 3: 6}, DL: {1: 2, 2: 2, 3: 5},
};
// Ordinary Thai spelling of Kam Mueang (the reading script, Pan's corrections) reads ก จ ต ป the
// Central Thai way: mid-class letters, with ◌๋ for the rising tone and ◌๊ for the high one. The
// alphabet puts them in group 1 instead. read(text, "ordinary") follows ordinary spelling: this
// table for a bare ก จ ต ป, measured on Thai Wiktionary's own spellings (reader_vs_wiktionary.py:
// bare open 130 of 130 mid, ◌่ 94 of 94, checked 34 of 36 low falling; ◌้ falls 94 times and
// is tone 3 74 times, so 5 is the better guess). ◌๊ and ◌๋ are read in both ways of reading.
const ORDINARY_KJTP = {A: 4, B: 2, C: 5, DS: 2, DL: 2};
// Vowel spellings. C is the first consonant (with its leading ห or อ, or a following ว),
// F the final consonant. [spelling, vowel sound, long?, final sound it brings itself]
// The last one, C alone, is Thai's hidden vowel (ส in สบาย): read, but the alphabet writes it.
const VOWELS = [
["Cะ", "a", 0], ["Cา", "aː", 1], ["Cิ", "i", 0], ["Cี", "iː", 1], ["Cึ", "ɯ", 0],
["Cือ", "ɯː", 1], ["Cุ", "u", 0], ["Cู", "uː", 1], ["เCะ", "e", 0], ["เC", "eː", 1],
["แCะ", "ɛ", 0], ["แC", "ɛː", 1], ["โCะ", "o", 0], ["โC", "oː", 1], ["เCาะ", "ɔ", 0],
["Cอ", "ɔː", 1], ["เCอะ", "ɤ", 0], ["เCอ", "ɤː", 1], ["เCียะ", "ia", 0], ["เCีย", "ia", 1],
["เCือะ", "ɯa", 0], ["เCือ", "ɯa", 1], ["Cัวะ", "ua", 0], ["Cัว", "ua", 1],
["Cำ", "a", 0, "m"], ["ใC", "a", 0, "j"], ["ไC", "a", 0, "j"], ["เCา", "a", 0, "w"],
["เCย", "ɤː", 1, "j"], ["C็", "ɔ", 0], ["ไCย", "a", 0, "j"],
["CัF", "a", 0], ["CาF", "aː", 1], ["CิF", "i", 0], ["CีF", "iː", 1], ["CึF", "ɯ", 0],
["CืF", "ɯː", 1], ["CุF", "u", 0], ["CูF", "uː", 1], ["เC็F", "e", 0], ["เCF", "eː", 1],
["แC็F", "ɛ", 0], ["แCF", "ɛː", 1], ["CF", "o", 0], ["โCF", "oː", 1], ["C็อF", "ɔ", 0],
["CอF", "ɔː", 1], ["เCิF", "ɤː", 1], ["เCียF", "ia", 1], ["เCือF", "ɯa", 1], ["CวF", "ua", 1],
["C", "a", 0],
];
const all = Object.keys(LETTERS).join("") + Object.keys(OTHER_LETTERS).join("");
// The ways a syllable can begin: a Thai cluster, ห before a sonorant, the glottal y อย, or one
// letter. Each is tried on its own, with and without a ว after it, so that no reading hides
// another: a regular expression gives only its first match, and the split never saw โค วิด
// behind โคว, or ใบ รับ behind ใบร.
const ONSETS = [`(${Object.keys(CLUSTERS).join("|")})`, `(ห[${SONORANTS}])`, "(อย)", `([${all}])`];
// A ว straight after the first consonant is part of it (กว, ขว) only when a vowel or a
// tone mark follows; before a final consonant it is the vowel ua (ขวด, สวย).
const WITH_W = ["(ว(?=[ะาิีึืุูัำอ็่้๊๋]))", "()"];
const TONE = "([่้๊๋])?";
const UPPER = "ิีึืุูั็";
const FINAL_LETTERS = Object.keys(FINALS).join("") + Object.keys(OTHER_FINALS).join("");
const FINAL = `([${FINAL_LETTERS}])`;
// After เ แ โ ใ ไ, a ว followed by a final can be part of the first consonant too (แขวน),
// or be the final itself (แมว, เลว ร้าย): both readings are offered, and the split chooses.
const W_BEFORE_FINAL = `(ว(?=[${FINAL_LETTERS}]))`;
// A consonant with ์ over it is silent in Thai spelling: the alphabet leaves it out.
const SILENT = new RegExp(`^[${all}][ิุ]?์`, "u");
// Turn one vowel spelling into a regular expression, with one way of beginning the syllable.
function pattern(spelling, onset) {
let rx = "", i = 0;
while (i < spelling.length) {
const ch = spelling[i];
if (ch === "C") {
rx += onset;
const next = spelling[i + 1];
if (next && UPPER.includes(next)) { rx += next; i += 1; }
rx += TONE;
} else if (ch === "F") {
rx += FINAL;
} else {
rx += ch;
}
i += 1;
}
return new RegExp("^" + rx, "u");
}
const PATTERNS = VOWELS.flatMap(([spelling, vowel, long, glide]) => {
const ws = "เแโใไ".includes(spelling[0]) && spelling.includes("F") ? WITH_W.concat([W_BEFORE_FINAL]) : WITH_W;
return ONSETS.flatMap((onset) => ws.map((w) => ({spelling, vowel, long, glide, rx: pattern(spelling, onset + w)})));
});
// Every way one syllable can start at position `at` of `word`.
function syllablesAt(word, at, how) {
const rest = word.slice(at), found = [];
for (const p of PATTERNS) {
const m = p.rx.exec(rest);
if (m) found.push(describe(m[0], m[1], Boolean(m[2]), m[3], m[4], p, how));
}
const silent = SILENT.exec(rest);
if (silent) found.push({text: silent[0], silent: true, problems: ["silent letter: leave it out"], cost: 5});
return found;
}
function describe(text, onset, cluster, mark, final, p, how) {
const problems = [];
let sound, group, lanna;
if (CLUSTERS[onset]) {
[sound] = CLUSTERS[onset];
group = (LETTERS[onset[0]] || OTHER_LETTERS[onset[0]])[1];
problems.push(`${onset}: Kam Mueang has no such cluster; it usually says ${sound}: write ${CLUSTERS[onset][1]}`);
lanna = "–";
} else {
const base = onset.length === 2 ? onset[1] : onset;
const letter = LETTERS[base] || OTHER_LETTERS[base];
if (OTHER_LETTERS[base]) problems.push(`${base} is not in this alphabet: write ${OTHER_LETTERS[base][2]}`);
sound = letter[0];
group = letter[1];
if (onset[0] === "ห" && onset.length === 2) group = 1;
if (onset === "อย") group = 2;
lanna = onset === "อย" ? "ᩀ" : onset.length === 2 ? "ᩉ᩠" + (LETTERS[base][2] || "?") : (LETTERS[base] || [])[2] || "–";
}
if (cluster) sound += "w";
let end = p.glide || "";
if (final) {
end = FINALS[final] || OTHER_FINALS[final][0];
if (!FINALS[final]) problems.push(`final ${final}: write ${OTHER_FINALS[final][1]}`);
}
if (p.spelling === "C") problems.push("hidden vowel: write the vowel (ะ, or the one you say)");
if (p.spelling === "ไCย") problems.push("the last ย is silent: leave it out");
const checked = "ktp".includes(end) && end !== "" || (!end && !p.long);
let column = mark ? TONE_MARKS[mark] : (checked ? (p.long ? "DL" : "DS") : "A");
if (mark && !TONE_MARKS[mark]) {
problems.push(`◌${mark} is not used: write ◌่ or ◌้ with a letter of the right group`);
column = checked ? (p.long ? "DL" : "DS") : "A";
}
let tone = TONE_OF[column][group];
if (how === "ordinary" && onset.length === 1 && "กจตป".includes(onset)) tone = ORDINARY_KJTP[column];
if (mark === "๊") tone = 6;
else if (mark === "๋") tone = 1;
const ipa = sound + p.vowel + end + (checked && !end ? "ʔ" : "") + TONES[tone][0];
// A Thai cluster is named as a problem, but costs nothing extra: Thai spelling has them,
// and splitting one off (เนื้อก ล้า for เนื้อ กล้า) reads the words wrong.
const cost = 10 + (p.spelling === "CF" ? 3 : 0) + (cluster ? 1 : 0) + 2 * (problems.length - (CLUSTERS[onset] ? 1 : 0))
+ (p.spelling === "C" ? 20 : 0);
// spelling, final, end, mark and w are the parts of the syllable, for writing it in Lanna.
return {text, onset, group, column, tone, ipa, lanna, problems, cost,
spelling: p.spelling, final: final || "", end, mark: mark || "", w: cluster};
}
// The best way to split one run of letters into syllables: the lowest total cost, so fewest
// syllables, then fewest hidden vowels and differences from the alphabet. A letter that
// cannot be read at all is reported, and reading carries on after it.
function splitWord(word, how) {
const best = new Array(word.length + 1).fill(null);
best[0] = {cost: 0, parts: []};
for (let at = 0; at < word.length; at += 1) {
if (!best[at]) continue;
for (const s of syllablesAt(word, at, how)) {
const end = at + s.text.length, cost = best[at].cost + s.cost;
if (!best[end] || cost < best[end].cost) best[end] = {cost, parts: best[at].parts.concat([s])};
}
}
if (best[word.length]) return best[word.length].parts;
let reach = 0;
for (let at = word.length; at >= 0; at -= 1) if (best[at]) { reach = at; break; }
return best[reach].parts.concat([{text: word[reach], error: "cannot be read as a syllable here"}],
splitWord(word.slice(reach + 1), how));
}
// Split any text into runs of Thai and Khmer letters, and read each run.
// Spaces, digits, ๆ, ฯ and punctuation pass through as they are.
// how: "alphabet" (the default, this page's rules) or "ordinary" (ordinary spelling's reading of
// ก จ ต ป, the voice's choice: see ORDINARY_KJTP).
function read(text, how = "alphabet") {
const out = [];
for (const piece of text.split(/([^ก-ฮะ-ฺเ-ๅ็-๎ក-៓]+)/u)) {
if (!piece) continue;
if (/^[^ก-ฮะ-ฺเ-ๅ็-๎ក-៓]+$/u.test(piece)) { out.push({text: piece, space: true}); continue; }
out.push(...splitWord(piece, how));
}
return out;
}
// ---- The same text in the Lanna script (Tai Tham) ----
// The rules were worked out from, and are checked against, the Kam Mueang words of Thai
// Wiktionary (lanna_check.py). Consonants can be taken two ways:
// "sound" (the default): the letters of this alphabet, each the sound it stands for,
// so ค is the k with a puff of air (ᨤ) and គ the one without (ᨣ);
// "letters": Thai letters standing letter for letter for Lanna ones (ค is ᨣ, ฅ is ᨤ),
// the way Thai Wiktionary spells Lanna words in Thai letters.
const BY_LETTER = {
"ก": "ᨠ", "ข": "ᨡ", "ฃ": "ᨢ", "ค": "ᨣ", "ฅ": "ᨤ", "ฆ": "ᨥ", "ง": "ᨦ", "จ": "ᨧ", "ฉ": "ᨨ",
"ช": "ᨩ", "ซ": "ᨪ", "ฌ": "ᨫ", "ญ": "ᨬ", "ฎ": "ᨭ", "ฏ": "ᨭ", "ฐ": "ᨮ", "ฑ": "ᨯ", "ฒ": "ᨰ",
"ณ": "ᨱ", "ด": "ᨯ", "ต": "ᨲ", "ถ": "ᨳ", "ท": "ᨴ", "ธ": "ᨵ", "น": "ᨶ", "บ": "ᨷ", "ป": "ᨸ",
"ผ": "ᨹ", "ฝ": "ᨺ", "พ": "ᨻ", "ฟ": "ᨼ", "ภ": "ᨽ", "ม": "ᨾ", "ย": "ᨿ", "ร": "ᩁ", "ล": "ᩃ",
"ว": "ᩅ", "ศ": "ᩆ", "ษ": "ᩇ", "ส": "ᩈ", "ห": "ᩉ", "ฬ": "ᩊ", "อ": "ᩋ", "ฮ": "ᩌ",
"គ": "ᨣ", "ជ": "ᨩ", "ទ": "ᨴ", "ព": "ᨻ",
};
// By sound: each letter's partner in the chart above; a letter with none (ฉ ช ย) and the
// letters this alphabet leaves out are written letter for letter.
// ฃ, which Thai no longer uses, stands for the same kh as ข.
const BY_SOUND = Object.assign({}, BY_LETTER, {"ฃ": "ᨡ"});
for (const [letter, [, , partner]] of Object.entries(LETTERS)) if (partner) BY_SOUND[letter] = partner;
const FINAL_LETTER = {k: "ᨠ", t: "ᨯ", p: "ᨷ", "ŋ": "ᨦ", n: "ᨶ", m: "ᨾ", j: "ᨿ", w: "ᩅ"};
const TONE_SIGN = {"่": "᩵", "้": "᩶"};
// After these the long a is the tall one (ᩤ), so the two are not read as another letter.
const TALL_AA = "ᨣᨴᨷᩅ";
// How each vowel is written. C = the first consonant(s), T = the tone mark, A = the long a
// (ᩣ or ᩤ), S = a final under the syllable with the tone mark just before it, F = a final under
// the long a, f = a final on the line, where n is written ᩁ (a final y still goes under).
const SHAPES = {
"Cะ": "CTᩡ", "Cา": "CTA", "Cิ": "CᩥT", "Cี": "CᩦT", "Cึ": "CᩧT", "Cือ": "CᩨT", "Cุ": "CᩩT",
"Cู": "CᩪT", "เCะ": "CᩮTᩡ", "เC": "CᩮT", "แCะ": "CᩯTᩡ", "แC": "CᩯT", "โCะ": "CᩰTᩡ", "โC": "CᩰT",
"เCาะ": "CᩰᩬTᩡ", "Cอ": "CᩬᩴT", "เCอะ": "CᩮᩬᩥTᩡ", "เCอ": "CᩮᩬᩥT", "เCียะ": "C᩠ᨿᩮTᩡ",
"เCีย": "C᩠ᨿᩮT", "เCือะ": "CᩮᩬᩥTᩋᩡ", "เCือ": "CᩮᩬᩥTᩋ", "Cัวะ": "C᩠ᩅᩫTᩡ", "Cัว": "C᩠ᩅᩫT",
"Cำ": "CTAᩴ", "ใC": "CᩲT", "ไC": "CᩱT", "เCา": "CᩮᩢTA", "เCย": "Cᩮᩥ᩠Tᨿ", "C็": "CᩳT",
"ไCย": "Cᩱ᩠Tᨿ", "CัF": "CᩢS", "CาF": "CTAF", "CิF": "CᩥS", "CีF": "CᩦS", "CึF": "CᩧS",
"CืF": "CᩨS", "CุF": "CᩩTf", "CูF": "CᩪTf", "เC็F": "CᩮᩢS", "เCF": "CᩮS", "แC็F": "CᩯᩢS",
"แCF": "CᩯS", "CF": "CᩫS", "โCF": "CᩰᩫS", "C็อF": "CᩬᩢTf", "CอF": "CᩬTf", "เCิF": "CᩮᩥS",
"เCียF": "C᩠ᨿTf", "เCือF": "CᩮᩬᩥTf", "CวF": "C᩠ᩅTf", "C": "CT",
};
// Words the rules do not spell the way Lanna does, keyed by how they are written in Thai
// letters: lanna_words.js fills this in from Thai Wiktionary.
const WORDS = {};
function lannaOnset(s, map) {
const o = s.onset;
let out;
if (o === "อย") out = "ᩀ";
else if (o.length === 2 && o[0] === "ห") out = o[1] === "ล" ? "ᩉᩖ" : "ᩉ᩠" + ("ญย".includes(o[1]) ? "ᨿ" : map[o[1]]);
// A Thai cluster keeps the letters it is spelled with: ครก is ᨣᩕᩫ᩠ᨠ, and ป before ร is ᨷ.
else if (o === "ปร") out = "ᨷᩕ";
else if (o.length === 2) out = BY_LETTER[o[0]] + (o[1] === "ร" ? "ᩕ" : "ᩖ");
else out = map[o] || o;
return out + (s.w ? "᩠ᩅ" : "");
}
function lannaSyllable(s, how, first, last) {
const map = how === "letters" ? BY_LETTER : BY_SOUND;
if (s.silent) {
// A letter Thai spelling does not sound: Lanna writes it, with ᩺ over it.
return Array.from(s.text).map((ch) => ch === "์" ? "᩺" : ch === "ิ" ? "ᩥ" : ch === "ุ" ? "ᩩ" : map[ch] || ch).join("");
}
let shape = SHAPES[s.spelling];
const c = lannaOnset(s, map);
// A consonant alone with a tone mark (บ่) takes ᩴ under the mark: ᨷᩴ᩵.
if (s.spelling === "C" && s.mark) shape = "CᩴT";
// A short a is the consonant alone, except in a word of one syllable: ᩡ is written there only.
if (s.spelling === "Cะ" && !s.mark && !(first && last)) shape = "C";
// When something already sits under the first consonant (ᩖ, or a letter), the final goes on the line.
if (/[ᩖ᩠]/.test(c)) shape = shape.replace("S", "Tf");
const tone = TONE_SIGN[s.mark] || "";
// A final of this alphabet (ก ด บ ง น ม ย ว) is written by its sound; any other final is a letter
// Thai spelling keeps from the word's past, and Lanna keeps it too (อากาศ: ᩋᩣᨠᩣ᩠ᩆ). A final p is ᨷ.
const fin = !s.final ? "" : s.end === "p" ? "ᨷ" : how === "letters" || !FINALS[s.final] ? BY_LETTER[s.final] || "" : FINAL_LETTER[s.end];
const yUnder = fin === "ᨿ";
let out = "";
for (const ch of shape) {
if (ch === "C") out += c;
else if (ch === "A") out += TALL_AA.includes(c[0]) && !c.includes("ᩕ") ? "ᩤ" : "ᩣ";
else if (ch === "T") out += shape.includes("f") && yUnder ? "" : tone;
else if (ch === "S") out += "᩠" + tone + fin;
else if (ch === "F") out += "᩠" + fin;
else if (ch === "f") out += yUnder ? "᩠" + tone + "ᨿ" : fin === "ᨶ" ? "ᩁ" : fin;
else out += ch;
}
return out;
}
// The text in Lanna, piece by piece: [{text, lanna, n, listed}]: n is how many syllables the
// piece is, and listed says it came from WORDS.
function lannaParts(text, how = "sound") {
const joiners = new RegExp("[" + String.fromCharCode(0x200b) + "-" + String.fromCharCode(0x200d) + "]", "g");
const parts = read(text.replace(joiners, "").replace(/\s*ๆ/g, "ๆ"));
const out = [];
for (let i = 0; i < parts.length; i += 1) {
const s = parts[i];
if (s.space || s.error) { out.push({text: s.text, lanna: s.space ? s.text.replace(/ๆ/g, "᩻") : s.text, n: s.space ? 0 : 1}); continue; }
// The longest run of syllables (up to six) that the word list knows.
let found = 0;
for (let n = Math.min(6, parts.length - i); n >= 1 && !found; n -= 1) {
const run = parts.slice(i, i + n);
if (run.some((p) => p.space || p.error)) continue;
if (Object.prototype.hasOwnProperty.call(WORDS, run.map((p) => p.text).join(""))) found = n;
}
if (found) {
const key = parts.slice(i, i + found).map((p) => p.text).join("");
out.push({text: key, lanna: WORDS[key], n: found, listed: true});
i += found - 1;
continue;
}
const first = !parts[i - 1] || parts[i - 1].space, last = !parts[i + 1] || parts[i + 1].space;
out.push({text: s.text, lanna: lannaSyllable(s, how, first, last), n: 1});
}
return out;
}
const lanna = (text, how) => lannaParts(text, how).map((p) => p.lanna).join("");
return {LETTERS, OTHER_LETTERS, CLUSTERS, TONES, TONE_OF, VOWELS, read, lanna, lannaParts, WORDS};
})();
if (typeof module !== "undefined") module.exports = KMA;
build_font.py: KM Alphabet Sans, and what it printed · build_font.py: ฟอนต์ KM Alphabet Sans และสิ่งที่พิมพ์ออกมา · build_font.py:KM Alphabet Sans 字体,以及它打印的结果
"""Build KM Alphabet Sans: Noto Sans Thai with the four Khmer letters of the Kam Mueang alphabet added.
Why a font of its own: Safari's engine (WebKit) draws a letter and its marks only with one font
that holds all of them. No common font holds both Thai marks and Khmer letters, so without this
font, គ่า shows as empty boxes there. With it, every browser draws the four letters with Thai
vowels and tone marks on them.
Both source fonts are under the SIL Open Font License 1.1, so this changed copy is too: it is
renamed, and OFL.txt goes with it. Run it with: python build_font.py
Needs fontTools and brotli: pip install fonttools brotli
"""
import os
import tempfile
import urllib.request
from fontTools.ttLib import TTFont
HERE = os.path.dirname(os.path.abspath(__file__))
# The unhinted fonts: a hinted glyph carries instructions written for its own font's tables,
# and moved into the Thai font it is drawn stretched, with its top stroke floating off it.
SOURCE = "https://raw.githubusercontent.com/notofonts/notofonts.github.io/main/fonts/{0}/unhinted/ttf/{0}-{1}.ttf"
LICENCE = "https://raw.githubusercontent.com/notofonts/thai/main/OFL.txt"
KHMER = [0x1782, 0x1787, 0x1791, 0x1796] # គ ជ ទ ព
FAMILY = "KM Alphabet Sans"
def fetch(url, folder):
path = os.path.join(folder, url.rsplit("/", 1)[-1])
if not os.path.exists(path):
urllib.request.urlretrieve(url, path)
return path
def build(weight, folder):
# Keep the source's own date in the font, so that every build gives the same bytes.
thai = TTFont(fetch(SOURCE.format("NotoSansThai", weight), folder), recalcTimestamp=False)
khmer = TTFont(fetch(SOURCE.format("NotoSansKhmer", weight), folder))
order = thai.getGlyphOrder()
khmer_cmap = khmer.getBestCmap()
for code in KHMER:
name = khmer_cmap[code]
order.append(name)
thai["glyf"][name] = khmer["glyf"][name]
thai["hmtx"][name] = khmer["hmtx"][name]
for table in thai["cmap"].tables:
if table.isUnicode():
table.cmap[code] = name
thai["GDEF"].table.GlyphClassDef.classDefs[name] = 1 # a base letter, for marks to sit on
thai.setGlyphOrder(order)
thai["maxp"].numGlyphs = len(order)
style = weight
names = {1: FAMILY if weight == "Regular" else f"{FAMILY} {weight}", 2: "Regular",
3: f"krueng.ai: {FAMILY} {style}", 4: f"{FAMILY} {style}",
6: f"KMAlphabetSans-{style}", 16: FAMILY, 17: style}
table = thai["name"]
for record in list(table.names):
if record.nameID in names or record.nameID in (5,):
table.removeNames(nameID=record.nameID)
for name_id, text in names.items():
table.setName(text, name_id, 3, 1, 0x409)
table.setName("Version 1.0; Noto Sans Thai with four letters of Noto Sans Khmer (U+1782 U+1787 "
"U+1791 U+1796), for the Kam Mueang alphabet of krueng.ai", 5, 3, 1, 0x409)
# A .ttf to install on a computer, and a .woff2, smaller, for the web page.
thai.save(os.path.join(HERE, f"km_alphabet_sans-{weight}.ttf"))
out = os.path.join(HERE, f"km_alphabet_sans-{weight}.woff2")
thai.flavor = "woff2"
thai.save(out)
return out
def main():
with tempfile.TemporaryDirectory() as folder:
for weight in ("Regular", "Medium"):
path = build(weight, folder)
for made in (path, path.replace(".woff2", ".ttf")):
print(f"{os.path.basename(made)}: {os.path.getsize(made):,} bytes")
notice = urllib.request.urlopen(LICENCE).read().decode("utf-8")
khmer_line = "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/khmer)\n"
with open(os.path.join(HERE, "OFL.txt"), "w", encoding="utf-8", newline="\n") as f:
f.write(f"{FAMILY} (km_alphabet_sans-*.woff2) is Noto Sans Thai with four letters of\n"
"Noto Sans Khmer added, made by build_font.py. Both are under this licence.\n\n")
f.write(khmer_line + notice)
print("OFL.txt: the licence of both source fonts")
if __name__ == "__main__":
main()
km_alphabet_sans-Regular.woff2: 9,620 bytes
km_alphabet_sans-Regular.ttf: 21,788 bytes
km_alphabet_sans-Medium.woff2: 9,596 bytes
km_alphabet_sans-Medium.ttf: 21,636 bytes
OFL.txt: the licence of both source fonts
font_check.py: which installed fonts hold which script · font_check.py: ฟอนต์ที่ติดตั้งไว้ตัวไหนมีอักษรชุดไหน · font_check.py:已安装的字体各包含哪种文字
"""Which fonts on this computer can show the Kam Mueang alphabet in one piece?
A program that is not a browser (a word processor, a chat app) usually draws a letter and the
marks on it with one font. So a font has to hold Thai and the four Khmer letters together.
This lists, for the fonts installed on this computer, which scripts each one holds.
Run it with: python font_check.py Needs fontTools: pip install fonttools
"""
import glob
import os
import sys
from fontTools.ttLib import TTCollection, TTFont
TESTS = {
"Thai": [0x0E01, 0x0E48, 0x0E34], # ก, mai ek, sara i
"Khmer 4": [0x1782, 0x1787, 0x1791, 0x1796], # គ ជ ទ ព
"Lao": [0x0E8D], # ຍ
"Myanmar": [0x100A], # ည
"Tai Tham": [0x1A23], # ᨣ
}
FOLDERS = {
"win32": [os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts"),
os.path.join(os.environ.get("LOCALAPPDATA", ""), r"Microsoft\Windows\Fonts")],
"darwin": ["/System/Library/Fonts", "/Library/Fonts", os.path.expanduser("~/Library/Fonts")],
}.get(sys.platform, ["/usr/share/fonts", os.path.expanduser("~/.local/share/fonts")])
def fonts():
for folder in FOLDERS:
for path in glob.glob(os.path.join(folder, "**", "*"), recursive=True):
ext = path.lower().rsplit(".", 1)[-1]
if ext in ("ttf", "otf"):
yield path, [TTFont(path, lazy=True)]
elif ext == "ttc":
yield path, list(TTCollection(path, lazy=True))
def main():
seen, rows = set(), []
for path, faces in fonts():
for face in faces:
try:
name = face["name"].getDebugName(1)
cmap = face.getBestCmap() or {}
except Exception:
continue
held = [script for script, codes in TESTS.items() if all(c in cmap for c in codes)]
if held and name not in seen:
seen.add(name)
rows.append((name, held))
home = os.path.expanduser("~")
folders = [f.replace(home, "~") for f in FOLDERS if os.path.isdir(f)]
print(f"Font folders: {', '.join(folders)}\n")
for script in TESTS:
names = sorted(n for n, held in rows if script in held)
print(f"{script:9} {len(names):3} fonts: {', '.join(names) if names else '(none)'}")
both = sorted(n for n, held in rows if "Thai" in held and "Khmer 4" in held)
print(f"\nThai and the four Khmer letters in one font: {', '.join(both) if both else '(none)'}")
if __name__ == "__main__":
main()
script_scan.py: the reading script, syllable by syllable · script_scan.py: บทอ่าน ทีละพยางค์ · script_scan.py:朗读稿,一个音节一个音节地看
"""How much of the voice project's reading script could be put into the alphabet without a speaker?
The 1,468 sentences are the ones on krueng.ai/kammuang_recorder.html, written in Thai letters the
way Kam Mueang is usually written. This reads every sentence with alphabet.js and sorts each
syllable into one of three heaps:
- it is already written the alphabet's way;
- a rule can rewrite it (a Pali letter, a silent letter, a Thai cluster, a hidden vowel,
◌๋ or ◌๊ on ก จ ต ป), though a speaker should still look;
- only a Kam Mueang speaker can say which way it is said.
Run it with: python script_scan.py
Needs Playwright with Chromium: pip install playwright, then python -m playwright install chromium
"""
import collections
import json
import os
import re
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
RECORDER = os.path.join(HERE, "..", "kammuang_recorder.html")
SPEAKER = [
"ค ช ท พ: said with a puff of air, or without one (then a Khmer letter)",
"ร: said h (ฮ), l or r",
"ย: said ny (ญ) or y",
"ก จ ต ป with ◌้: tone 3, or tone 5 in the Central Thai way of writing it",
"ก จ ต ป with no mark: tone 1, or tone 4 in the Central Thai way of writing it",
"◌๊ or ◌๋ on a letter other than ก จ ต ป",
"a letter that cannot be read where it is",
]
RULE = [
"Thai cluster (กล, ปร, ...)",
"hidden vowel (ส in สบาย)",
"Pali letter (ศ, ษ, ณ, ธ, ...) or Thai final (ศ, ร, ล, ...)",
"silent letter (ย์)",
"◌๋ or ◌๊ on ก จ ต ป",
]
def heap(s):
"""Which heap a syllable goes in, and why: ("speaker" | "rule" | "as written", reason)."""
if s.get("error"):
return "speaker", SPEAKER[6]
if s.get("silent"):
return "rule", RULE[3]
problems = " ".join(s["problems"])
base = s["onset"][-1] if len(s["onset"]) == 2 and s["onset"][0] in "หอ" else s["onset"][0]
mark = re.search("[่้๊๋]", s["text"])
mark = mark.group(0) if mark else ""
if "cluster" not in problems:
if base in "คชทพ":
return "speaker", SPEAKER[0]
if base == "ร":
return "speaker", SPEAKER[1]
if s["onset"] == "ย":
return "speaker", SPEAKER[2]
if base in "กจตป" and len(s["onset"]) == 1:
if mark == "้":
return "speaker", SPEAKER[3]
if not mark and s["column"] == "A":
return "speaker", SPEAKER[4]
if mark in ("๊", "๋") and base not in "กจตป":
return "speaker", SPEAKER[5]
if "cluster" in problems:
return "rule", RULE[0]
if "hidden vowel" in problems:
return "rule", RULE[1]
if "not in this alphabet" in problems or "final" in problems:
return "rule", RULE[2]
if mark in ("๊", "๋"):
return "rule", RULE[4]
return "as written", ""
def main():
html = open(RECORDER, encoding="utf-8").read()
sentences = json.loads(re.search(r"const SENTENCES = (\[.*?\]);", html, re.S).group(1))
texts = [s["text"] for s in sentences]
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.add_script_tag(path=os.path.join(HERE, "alphabet.js"))
readings = page.evaluate("texts => texts.map(t => KMA.read(t))", texts)
browser.close()
counts, examples, heaps = collections.Counter(), collections.defaultdict(list), collections.Counter()
for reading in readings:
for s in reading:
if s.get("space"):
continue
where, why = heap(s)
heaps[where] += 1
counts[why] += 1
if why and len(examples[why]) < 6 and s["text"] not in examples[why]:
examples[why].append(s["text"])
total = sum(heaps.values())
print(f"{len(texts)} sentences, {total} syllables as alphabet.js splits them.\n")
for title, reasons, key in (("Only a Kam Mueang speaker can settle", SPEAKER, "speaker"),
("A rule can rewrite (a speaker should still look)", RULE, "rule")):
print(f"{title}: {heaps[key]} ({100 * heaps[key] / total:.0f}%)")
for why in reasons:
print(f"{counts[why]:7} {why} e.g. {' '.join(examples[why])}")
print()
print(f"Already written the alphabet's way: {heaps['as written']} ({100 * heaps['as written'] / total:.0f}%)")
if __name__ == "__main__":
main()
wiktionary_words.py: Thai Wiktionary's Kam Mueang words · wiktionary_words.py: คำเมืองของวิกิพจนานุกรมภาษาไทย · wiktionary_words.py:泰文维基词典的北泰语词
"""Save the Kam Mueang words of Thai Wiktionary, to check the Lanna-script writer against.
Thai Wiktionary (th.wiktionary.org) has a page for each Kam Mueang word, headed by its spelling in
the Lanna script. Most pages also give the word in Thai letters, two ways: letter for letter (ᨣ as ค,
ᨤ as ฅ), and the way it sounds. Many give how it is said in Chiang Mai. This saves those for every
word of the category หมวดหมู่:คำหลักภาษาคำเมือง to wiktionary_words.tsv. The words are Wiktionary's,
under the licence CC BY-SA 4.0, and so is the file.
Run it with: python wiktionary_words.py (a few minutes: it waits between requests)
"""
import datetime
import json
import os
import re
import time
import urllib.error
import urllib.parse
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
API = "https://th.wiktionary.org/w/api.php"
CATEGORY = "หมวดหมู่:คำหลักภาษาคำเมือง"
HEADERS = {"User-Agent": "krueng.ai Kam Mueang alphabet (https://krueng.ai/kammuang_alphabet.html)"}
# The names Wiktionary's {{nod-alt}} uses for the Thai-letter spellings of a word.
LETTER_FOR_LETTER = ("l", "tl", "~", "tlts", "tstl", "ls", "sl")
AS_IT_SOUNDS = ("s", "ts", "~", "tlts", "tstl", "ls", "sl")
FOLLOWS_THAI = ("f", "ft", "fth")
# The Unicode block of the Lanna (Tai Tham) script.
TAI_THAM = re.compile("[" + chr(0x1A20) + "-" + chr(0x1AAF) + "]")
def ask(params):
"""One request to Wiktionary's API. When it is busy, wait and ask again."""
params = dict(params, format="json", maxlag=5)
url = API + "?" + urllib.parse.urlencode(params)
for wait in (5, 20, 60, 120):
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=60) as reply:
data = json.load(reply)
except urllib.error.HTTPError as e:
if e.code not in (429, 503):
raise
time.sleep(wait)
continue
if data.get("error", {}).get("code") == "maxlag":
time.sleep(wait)
continue
return data
raise SystemExit("Wiktionary kept saying it was busy; try again later")
def kam_mueang_part(text):
"""The Kam Mueang section of a page: from its heading to the next language's heading."""
start = re.search(r"^==\s*ภาษาคำเมือง\s*==\s*$", text, re.M)
if not start:
return ""
rest = text[start.end():]
end = re.search(r"^==[^=].*==\s*$", rest, re.M)
return rest[:end.start()] if end else rest
def template_values(part, name, keys):
"""Every value given under one of `keys` in the templates called `name`, in page order."""
found = []
for call in re.findall(r"\{\{" + name + r"\s*\|([^{}]*)\}\}", part):
for argument in call.split("|"):
key, _, value = argument.partition("=")
if key.strip() in keys:
found += [v.strip() for v in value.split(",") if v.strip()]
return list(dict.fromkeys(found))
def main():
rows = []
params = {"action": "query", "generator": "categorymembers", "gcmtitle": CATEGORY, "gcmlimit": 50,
"gcmnamespace": 0, "prop": "revisions", "rvprop": "content", "rvslots": "main"}
more = {}
while True:
data = ask({**params, **more})
for page in data.get("query", {}).get("pages", {}).values():
if not TAI_THAM.search(page["title"]):
continue # pages headed in Thai letters point to a Lanna page; that page is enough
part = kam_mueang_part(page["revisions"][0]["slots"]["main"]["*"])
heard = [m.group(1) for m in re.finditer(r"\{\{IPA\|nod\|([^|}]*)\|a=เชียงใหม่", part)]
rows.append([page["title"],
",".join(template_values(part, "nod-alt", LETTER_FOR_LETTER)),
",".join(template_values(part, "nod-alt", AS_IT_SOUNDS)),
",".join(template_values(part, "nod-alt", FOLLOWS_THAI)),
heard[0] if heard else "",
",".join(sorted(set(re.findall(r"\{\{(?:bor|bor\+|lbor|der|der\+)\|nod\|([a-z-]+)", part))))])
if "continue" not in data:
break
more = data["continue"]
time.sleep(3)
rows.sort()
today = datetime.date.today().isoformat()
with open(os.path.join(HERE, "wiktionary_words.tsv"), "w", encoding="utf-8", newline="\n") as f:
f.write(f"# Kam Mueang words of Thai Wiktionary ({CATEGORY}), read on {today}.\n")
f.write("# Licence: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/ ; the words and\n")
f.write("# spellings are Wiktionary's contributors' (https://th.wiktionary.org).\n")
f.write("lanna\tletter_for_letter\tas_it_sounds\tfollows_thai\tchiang_mai\tborrowed_from\n")
for row in rows:
f.write("\t".join(row) + "\n")
with_thai = sum(1 for r in rows if r[1] or r[2] or r[3])
print(f"{len(rows)} Kam Mueang words with a Lanna spelling, read on {today}.")
print(f"{with_thai} of them are also given in Thai letters; {sum(1 for r in rows if r[1])} letter for letter, "
f"{sum(1 for r in rows if r[2])} as they sound.")
print(f"{sum(1 for r in rows if r[4])} say how the word is said in Chiang Mai.")
if __name__ == "__main__":
main()
lanna_check.py: the Lanna-script writer, checked · lanna_check.py: ตรวจตัวเขียนอักษรธรรมล้านนา · lanna_check.py:检验兰纳文转写器
"""Check the Lanna-script writer of alphabet.js against Thai Wiktionary's spellings.
Every word of wiktionary_words.tsv that is also given in Thai letters is written in the Lanna script
by the rules of alphabet.js alone (lanna_words.js, the word list, is not loaded), three ways:
1. from Wiktionary's Thai letters, letter for letter (ᨣ as ค): this tests the vowel shapes, where
the final goes and where the tone mark goes;
2. from the word written in the Kam Mueang alphabet, by its sound: Wiktionary's letters with each
consonant swapped for the alphabet's letter for its sound (ค for ᨤ, គ for ᨣ, ฮ for an ᩁ said h);
3. from Wiktionary's spelling of how the word sounds, in ordinary Thai letters.
Then each answer is compared with the Lanna spelling Wiktionary gives.
Run it with: python lanna_check.py (after wiktionary_words.py)
Needs Playwright with Chromium: pip install playwright, then python -m playwright install chromium
"""
import csv
import os
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
# Runs in the browser, beside alphabet.js: bySound() writes a word given in Wiktionary's
# letter-for-letter Thai the way the Kam Mueang alphabet writes its sound. `heard` is how it is
# said in Chiang Mai, if known. make_lanna_words.py uses it too.
BY_SOUND_JS = """
const SOUND_LETTER = {"ค": "គ", "ฅ": "ค", "ฆ": "ค", "ฃ": "ข", "ฉ": "ส", "ช": "ជ", "ฌ": "ซ", "ฎ": "ต",
"ฏ": "ต", "ฐ": "ถ", "ฑ": "ท", "ฒ": "ท", "ณ": "น", "ท": "ទ", "ธ": "ท", "พ": "ព", "ภ": "พ", "ย": "ญ",
"ศ": "ส", "ษ": "ส", "ฬ": "ล"};
// A Thai cluster as Kam Mueang says it: กร is a kh, กล a plain k.
const SOUND_CLUSTER = {"กร": "ข", "คร": "ค", "พร": "พ", "ปร": "ผ", "บร": "ผ", "กล": "ก", "คล": "គ",
"ปล": "ป", "พล": "ព", "ผล": "ผ", "ขล": "ข", "ตร": "ถ", "ทร": "ซ"};
const FINAL_BY_SOUND = {k: "ก", t: "ด", p: "บ", "ŋ": "ง", n: "น", m: "ม", j: "ย", w: "ว"};
function bySound(letters, heard) {
const parts = KMA.read(letters);
const said = heard.replace(/[\\/\\[\\]]/g, "").split(/[.\\s]+/).filter(Boolean);
const syllables = parts.filter((s) => !s.space && !s.error && !s.silent);
let k = 0;
return parts.map((s) => {
if (s.space || s.error) return s.text;
if (s.silent) return ""; // the alphabet leaves out a letter that is not said
const sound = said.length === syllables.length ? said[k] : "";
k += 1;
let onset = s.onset;
if (SOUND_CLUSTER[onset]) onset = SOUND_CLUSTER[onset];
else if (onset === "หย") onset = "หญ";
else if (onset === "ร") onset = sound.startsWith("l") ? "ล" : sound.startsWith("r") ? "ร" : "ฮ";
else if (onset.length === 1) onset = SOUND_LETTER[onset] || onset;
let text = s.text.replace(s.onset, onset);
if (s.final) {
const at = text.lastIndexOf(s.final);
text = text.slice(0, at) + FINAL_BY_SOUND[s.end] + text.slice(at + 1);
}
return text;
}).join("");
}
"""
CHECK_JS = BY_SOUND_JS + """
(words) => words.map((w) => {
const alphabet = w.letters ? bySound(w.letters, w.heard) : "";
return {alphabet,
letters: w.letters ? KMA.lanna(w.letters, "letters") : "",
byAlphabet: alphabet ? KMA.lanna(alphabet, "sound") : "",
bySound: w.sounds ? KMA.lanna(w.sounds, "sound") : ""};
})
"""
WAYS = [("letters", "letters", "Wiktionary's Thai letters, letter for letter"),
("alphabet", "byAlphabet", "the Kam Mueang alphabet, written by sound"),
("sounds", "bySound", "ordinary Thai spelling of how it sounds")]
def read_words():
with open(os.path.join(HERE, "wiktionary_words.tsv"), encoding="utf-8") as f:
lines = [line for line in f if not line.startswith("#")]
words = []
for row in csv.DictReader(lines, delimiter="\t"):
first = lambda field: (row[field] or "").split(",")[0] # noqa: E731
if first("letter_for_letter") or first("as_it_sounds"):
words.append({"lanna": row["lanna"], "letters": first("letter_for_letter"),
"sounds": first("as_it_sounds"), "heard": row["chiang_mai"] or ""})
return words
def main():
words = read_words()
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.add_script_tag(path=os.path.join(HERE, "alphabet.js"))
answers = page.evaluate(CHECK_JS, words)
browser.close()
print(f"{len(words)} Kam Mueang words of Thai Wiktionary, written in the Lanna script by the rules of")
print("alphabet.js alone (no word list), and compared with the Lanna spelling Wiktionary gives.\n")
print(f"{'Written from':48}{'words':>6}{'the same':>16}{'a mark elsewhere':>18}{'different':>11}")
misses = {}
for source, answer, title in WAYS:
tried = [(w, a) for w, a in zip(words, answers) if a[answer]]
same = sum(1 for w, a in tried if a[answer] == w["lanna"])
moved = sum(1 for w, a in tried if a[answer] != w["lanna"] and sorted(a[answer]) == sorted(w["lanna"]))
misses[title] = [(a["alphabet"] if source == "alphabet" else w[source], w["lanna"], a[answer])
for w, a in tried if sorted(a[answer]) != sorted(w["lanna"])]
print(f"{title:48}{len(tried):6}{same:9} ({100 * same / len(tried):.1f}%){moved:18}{len(misses[title]):11}")
# Which Lanna letter starts the words said with an h: the alphabet's ฮ stands for it.
said_h = [w for w in words if w["heard"].lstrip("/[").startswith("h")]
print(f"\nWords said with h at the start, by their first Lanna letter: "
f"ᩁ {sum(1 for w in said_h if w['lanna'][0] == 'ᩁ')}, ᩉ {sum(1 for w in said_h if w['lanna'][0] == 'ᩉ')}, "
f"ᩌ {sum(1 for w in said_h if w['lanna'][0] == 'ᩌ')}")
for title, rows in misses.items():
print(f"\nSome words written from {title}: the input, Wiktionary's spelling, ours")
for given, wanted, got in rows[:: max(1, len(rows) // 8)][:8]:
print(f" {given:16} {wanted:14} {got}")
if __name__ == "__main__":
main()
make_lanna_words.py: the word list · make_lanna_words.py: รายการคำ · make_lanna_words.py:词表
"""Make lanna_words.js: Kam Mueang words whose Lanna spelling the rules of alphabet.js do not reach.
The rules write a syllable the way it sounds. Lanna spelling also keeps letters that are no longer
said (กล้า is said ก้า and written ᨠᩖ᩶ᩣ), and Pali and Sanskrit words keep spellings of their own.
For those words the spelling is taken from Thai Wiktionary (wiktionary_words.tsv):
- each way Wiktionary writes a word in Thai letters (letter for letter, as it sounds, as Thai
writes it) becomes a key, with the word's Lanna spelling as its value;
- so does the word written in the Kam Mueang alphabet by its sound, if it has two syllables or
more. One syllable written by sound is not a key: ก้า could be กล้า (ᨠᩖ᩶ᩣ) or another word.
A key is kept only where the rules write something else, and dropped where two words of Wiktionary
give it two Lanna spellings. A few words are added by hand (HAND), each with where it comes from.
Then it writes the first sentences of the voice project's reading script in the Lanna script.
Run it with: python make_lanna_words.py (after wiktionary_words.py)
Needs Playwright with Chromium: pip install playwright, then python -m playwright install chromium
"""
import json
import os
import re
from collections import defaultdict
from playwright.sync_api import sync_playwright
from lanna_check import BY_SOUND_JS
HERE = os.path.dirname(os.path.abspath(__file__))
RECORDER = os.path.join(HERE, "..", "kammuang_recorder.html")
# Words added by hand: {Thai letters: (Lanna spelling, where it comes from)}.
HAND = {
"ก่": ("ᨣᩴ᩵", "Thai Wiktionary, ก่อ: the word for 'then, also' is written ᨣᩴ᩵"),
"ก็": ("ᨣᩴ᩵", "Thai Wiktionary, ก่อ: the word for 'then, also' is written ᨣᩴ᩵"),
}
HEADER = """// Kam Mueang words whose Lanna spelling the rules of alphabet.js do not reach, keyed by how
// they are written in Thai letters. Made by make_lanna_words.py from the Kam Mueang words of Thai
// Wiktionary (th.wiktionary.org), read on {date}: CC BY-SA 4.0, by Wiktionary's contributors,
// https://creativecommons.org/licenses/by-sa/4.0/ . Load it after alphabet.js.
"""
def read_rows():
with open(os.path.join(HERE, "wiktionary_words.tsv"), encoding="utf-8") as f:
date = re.search(r"read on (\d{4}-\d\d-\d\d)", f.readline()).group(1)
lines = [line for line in f if not line.startswith("#")]
head = lines[0].rstrip("\n").split("\t")
return date, [dict(zip(head, line.rstrip("\n").split("\t"))) for line in lines[1:]]
def main():
date, rows = read_rows()
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.add_script_tag(path=os.path.join(HERE, "alphabet.js"))
page.add_script_tag(content=BY_SOUND_JS)
# Every key each word offers, and how the rules alone write it.
offers = page.evaluate("""rows => rows.map((r) => {
const split = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
const thai = [...new Set([...split(r.letter_for_letter), ...split(r.as_it_sounds), ...split(r.follows_thai)])];
const first = split(r.letter_for_letter)[0];
const sound = first ? bySound(first, r.chiang_mai || "") : "";
const syllables = sound ? KMA.read(sound).filter((s) => !s.space).length : 0;
const keys = thai.map((k) => [k, "thai"]);
if (syllables >= 2 && !thai.includes(sound)) keys.push([sound, "alphabet"]);
return keys.filter(([k]) => KMA.read(k).every((s) => !s.space && !s.error))
.map(([k, kind]) => ({key: k, kind, rules: KMA.lanna(k)}));
})""", rows)
values, kinds = defaultdict(set), {}
for row, keys in zip(rows, offers):
for k in keys:
values[k["key"]].add(row["lanna"])
kinds.setdefault(k["key"], (k["kind"], k["rules"]))
shared = {k for k, v in values.items() if len(v) > 1}
words = {k: next(iter(v)) for k, v in values.items() if k not in shared and kinds[k][1] != next(iter(v))}
by_kind = defaultdict(int)
for k in words:
by_kind[kinds[k][0]] += 1
for k, (lanna, _source) in HAND.items():
words[k] = lanna
body = ",\n".join(f" {json.dumps(k, ensure_ascii=False)}: {json.dumps(v, ensure_ascii=False)}"
for k, v in sorted(words.items()))
with open(os.path.join(HERE, "lanna_words.js"), "w", encoding="utf-8", newline="\n") as f:
f.write(HEADER.format(date=date))
f.write("Object.assign(KMA.WORDS, {\n" + body + ",\n});\n")
print(f"lanna_words.js: {len(words)} words. From Wiktionary's Thai spellings {by_kind['thai']}, "
f"from the alphabet by sound {by_kind['alphabet']}, by hand {len(HAND)}.")
print(f"Left out: {len(shared)} spellings that two Wiktionary words share with different Lanna spellings.")
# The list loaded: a check that it is applied (Wiktionary's own words, so not a test of new words).
page.add_script_tag(path=os.path.join(HERE, "lanna_words.js"))
same = page.evaluate("""rows => rows.filter((r) => {
const k = (r.letter_for_letter || "").split(",")[0];
return k && KMA.lanna(k, "letters") === r.lanna;
}).length""", rows)
html = open(RECORDER, encoding="utf-8").read()
sentences = json.loads(re.search(r"const SENTENCES = (\[.*?\]);", html, re.S).group(1))
pieces = page.evaluate("texts => texts.map((t) => KMA.lannaParts(t))", [s["text"] for s in sentences])
browser.close()
print(f"With the list loaded, {same} of Wiktionary's {sum(1 for r in rows if r['letter_for_letter'])} words "
"come out as it spells them (a check that the list is applied, not a test).\n")
syllables = sum(p["n"] for sentence in pieces for p in sentence)
listed = sum(p["n"] for sentence in pieces for p in sentence if p.get("listed"))
print(f"The reading script: {len(sentences)} sentences, {syllables} syllables; {listed} of them "
f"({100 * listed / syllables:.0f}%) are spelled from the list, the rest by the rules.")
print("\nIts first sentences in the Lanna script (not yet checked by a Lanna reader):")
for sentence, parts in list(zip(sentences, pieces))[:12]:
print(f"{sentence['id']} {sentence['text']}")
print(f" {''.join(p['lanna'] for p in parts)}")
if __name__ == "__main__":
main()
make_lanna_images.py: the pictures of Lanna · make_lanna_images.py: ภาพของล้านนา · make_lanna_images.py:兰纳的图
"""Make the page's pictures of Lanna culture: SDXL (Juggernaut XL v9) in ComfyUI on a DGX Spark.
They are illustrations, not photographs: the page says so under each one. The prompts avoid hands
and written letters, which the model draws badly, and the seeds are fixed so a second run gives the
same pictures. Run it with: python make_lanna_images.py [folder] (default: lanna_images)
spark_media.py is a helper from the krueng.ai site's own tools (python/video_gen), and it is not
published here. To make the pictures with any SDXL setup instead, use these settings: checkpoint
juggernaut-xl-v9, 1344 x 768, 28 steps, CFG 7, sampler euler_ancestral with the normal scheduler,
the prompt "<scene>, <STYLE>" with each scene's seed, and the negative prompt "lowres, bad anatomy,
bad hands, extra fingers, text, watermark, signature, blurry, jpeg artifacts, deformed, ugly".
The page shows them as JPEG files at quality 84.
"""
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "python" / "video_gen"))
import spark_media # noqa: E402
STYLE = "photorealistic, natural light, rich detail, documentary photograph, 35mm"
SCENES = {
"yi_peng": ("Yi Peng festival night in Chiang Mai, hundreds of glowing paper sky lanterns rising into a deep "
"blue evening sky above the tiered roofs of a Lanna temple, warm orange glow, crowd silhouettes", 1101),
"lai_kham": ("inside an old Lanna temple hall in Chiang Mai, dark red lacquered teak pillars covered with fine "
"gold stencil patterns of flowers and vines, candle light, soft haze, no people", 1202),
"bo_sang": ("hand-painted paper umbrellas of Bo Sang village near Chiang Mai, dozens of colourful mulberry-paper "
"umbrellas with flower paintings drying in the sun in a courtyard, no people", 1303),
# The first prompt gave an oval tray with handles; this one, with seed 1433, gave a round table.
"khantoke": ("a Northern Thai khantoke dinner, a low round wooden table with a short pedestal foot standing on a "
"woven mat, on it small bowls of red nam prik ong chili dip, green chili dip, dark pork curry, "
"crispy pork rind, a coiled grilled sausage, fresh raw vegetables, and sticky rice in small woven "
"bamboo baskets, seen from slightly above, warm evening lamp light", 1433),
# Left out: the sand stupas of the Northern New Year. Twice the model drew gilded pagodas or little
# buildings instead of mounds of river sand, and Wikimedia Commons had no free photo from Chiang Mai.
"teen_jok": ("close-up of a Mae Chaem teen jok textile, the intricate hand-woven silk hem of a Lanna sarong with "
"red, gold and black diamond patterns, laid on old teak wood", 1606),
}
def main():
out = Path(sys.argv[1] if len(sys.argv) > 1 else "lanna_images")
out.mkdir(parents=True, exist_ok=True)
for name, (scene, seed) in SCENES.items():
target = out / f"{name}.png"
if target.exists():
print(f"{name}: already made")
continue
spark_media.t2i(f"{scene}, {STYLE}", target, seed=seed)
if __name__ == "__main__":
main()
make_tapestry.py: the tapestry down the side · make_tapestry.py: ผ้าทอด้านข้าง · make_tapestry.py:侧边的织锦
"""A Lanna handwoven tapestry for the recorder page: the shoulder cloth of the LannaBot queen, in close detail,
with little figures woven into its bands (elephants, a novice monk, temple halls, a naga, birds, villagers,
lanterns). Tall, for the right third of the screen. It hangs down the right third of a wide screen on
krueng.ai/kammuang_alphabet.html.
Qwen-Image on the Spark (spark_media.qwen_t2i), 928 x 1664 (9:16).
Run it with: python make_tapestry.py draft (4-step drafts, about a minute each)
python make_tapestry.py final (24 steps, several minutes each)
"""
import sys
from pathlib import Path
# spark_media.py is a helper from the krueng.ai site's own tools (python/video_gen), as in make_lanna_images.py.
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "python" / "video_gen"))
import spark_media # noqa: E402
PROMPT = (
"A close-up, straight-on view of a traditional Lanna handwoven tapestry from northern Thailand, a teen jok "
"brocade cloth in deep lacquer red, gold, indigo and ivory thread, filling the whole frame, flat and evenly lit "
"like a museum photograph of a textile. The cloth is woven in horizontal bands separated by thin gold geometric "
"borders of diamonds, hooks and zigzags. Woven into the bands, in charming naive folk-art textile style with "
"visible thread texture, are little figures, each one clear and separate with space around it: a happy baby "
"elephant, a smiling little novice monk in saffron robes, a Lanna temple hall with a tiered roof and golden "
"finials, a coiled naga serpent, a pair of hornbill birds, a water buffalo, a grandmother and grandfather "
"holding hands, floating paper lanterns, a lotus flower, and a tall golden chedi. The figures are stylized, "
"symmetrical, friendly and cute, with big dot eyes, in the manner of woven Tai folk motifs. Rich warm colours, "
"fine detail, slight sheen of silk, no people outside the weave, no background other than the cloth."
)
NEGATIVE = (
"photograph of a room, person wearing the cloth, hands, 3D render, realistic animals, text, letters, "
"watermark, logo, signature, blurry, low quality, deformed, messy, cluttered, overlapping figures, "
"frame, border, wall, table"
)
def main():
kind = sys.argv[1] if len(sys.argv) > 1 else "draft"
seeds = [int(s) for s in sys.argv[2:]] or ([41, 42, 43] if kind == "draft" else [41])
out = Path(__file__).parent
for seed in seeds:
target = out / f"tapestry_{kind}_{seed}.png"
if not target.exists():
spark_media.qwen_t2i(PROMPT, target, negative=NEGATIVE, width=928, height=1664, seed=seed,
lightning=kind == "draft")
if __name__ == "__main__":
main()
Sources and credits · แหล่งข้อมูลและเครดิต · 资料来源与致谢
- The six tones, their numbers and the examples: พจนานุกรมภาษาล้านนา, The Lanna Dictionary (2007), Chiang Mai Rajabhat University, as quoted in Wikipedia, Lanna language, which also gives the consonants, the clusters, the ɲ and j words, and the examples ก้า and ป้าย.
- The test words for each tone box: W. J. Gedney (1972), "A checklist for determining tones in Tai dialects", as listed in Wikipedia, Proto-Tai language.
- The scripts: Wikipedia, Tai Tham script, Thai script, Lao script, Burmese alphabet, Shan alphabet, Khmer script, and the Thai articles ภาษาไทยถิ่นเหนือ and อักษรธรรมล้านนา.
- How these scripts are stored and drawn, and the dotted circle: The Unicode Standard 15.0, chapter 16.
- The 0.05% estimate: Natnapang Burutphakdee (2004), Attitudes of Northern Thai youth towards Kammuang and the Lanna script, M.A. thesis, Payap University, citing Boonkhit (1995).
- Fonts: Noto Sans Thai, Noto Sans Khmer, Noto Sans Lao, Noto Sans Myanmar and Noto Sans Tai Tham, by the Noto Project Authors, SIL Open Font License 1.1. KM Alphabet Sans is made from the first two by
build_font.py. Inter by Rasmus Andersson, SIL Open Font License 1.1. - Photographs: the Chiang Mai sign by Alifshinobi (CC BY-SA 3.0) and the palm-leaf manuscript by Lannafolium (CC0), both from Wikimedia Commons and cropped.
- Kam Mueang words in the Lanna script, in Thai letters and as said in Chiang Mai: Thai Wiktionary, the category คำหลักภาษาคำเมือง, read on 10 October 2026, by Wiktionary's contributors, CC BY-SA 4.0.
- Pictures of Lanna: illustrations made with Juggernaut XL v9 (SDXL) in ComfyUI on a DGX Spark by
make_lanna_images.py; they are not photographs. What their captions say comes from Wikipedia: Yi Peng festival, Wat Phra Singh, Wat Phra That Doi Kham, Bo Sang, Khantoke, and the Thai articles ซิ่น, ผ้าจกไทยวน and อำเภอแม่แจ่ม. The tapestry down the right side of a wide screen is an illustration too, made with Qwen-Image on the same machine bymake_tapestry.py, with the figures of a Lanna cloth as the model imagines them.
- หกเสียงวรรณยุกต์ หมายเลข และตัวอย่าง: พจนานุกรมภาษาล้านนา, The Lanna Dictionary (2007) มหาวิทยาลัยราชภัฏเชียงใหม่ ตามที่วิกิพีเดียอ้างไว้ในบทความ Lanna language ซึ่งให้ข้อมูลพยัญชนะ เสียงควบ คำที่มีเสียง ɲ และ j และตัวอย่าง ก้า กับ ป้าย ด้วย
- คำทดสอบของแต่ละช่องวรรณยุกต์: W. J. Gedney (1972), "A checklist for determining tones in Tai dialects" ตามรายการในวิกิพีเดีย Proto-Tai language
- อักษรต่าง ๆ: วิกิพีเดีย Tai Tham script, Thai script, Lao script, Burmese alphabet, Shan alphabet, Khmer script และบทความภาษาไทย ภาษาไทยถิ่นเหนือ กับ อักษรธรรมล้านนา
- อักษรเหล่านี้ถูกเก็บและวาดอย่างไร และวงกลมจุดประ: The Unicode Standard 15.0, chapter 16
- ตัวเลขประมาณการ 0.05%: Natnapang Burutphakdee (2004), Attitudes of Northern Thai youth towards Kammuang and the Lanna script วิทยานิพนธ์ปริญญาโท มหาวิทยาลัยพายัพ อ้างจาก Boonkhit (1995)
- ฟอนต์: Noto Sans Thai, Noto Sans Khmer, Noto Sans Lao, Noto Sans Myanmar และ Noto Sans Tai Tham โดย Noto Project Authors สัญญาอนุญาต SIL Open Font License 1.1 KM Alphabet Sans สร้างจากสองตัวแรกด้วย
build_font.pyส่วน Inter โดย Rasmus Andersson สัญญาอนุญาต SIL Open Font License 1.1 - ภาพถ่าย: ป้ายในเชียงใหม่ โดย Alifshinobi (CC BY-SA 3.0) และคัมภีร์ใบลาน โดย Lannafolium (CC0) ทั้งสองภาพจาก Wikimedia Commons และตัดขอบภาพแล้ว
- คำเมืองในอักษรธรรมล้านนา ในตัวอักษรไทย และตามที่ออกเสียงในเชียงใหม่: วิกิพจนานุกรมภาษาไทย หมวดหมู่ คำหลักภาษาคำเมือง อ่านเมื่อ 10 ตุลาคม 2026 โดยผู้ร่วมเขียนวิกิพจนานุกรม CC BY-SA 4.0
- ภาพของล้านนา: ภาพประกอบที่สร้างด้วย Juggernaut XL v9 (SDXL) ใน ComfyUI บน DGX Spark โดย
make_lanna_images.pyไม่ใช่ภาพถ่าย ข้อมูลในคำบรรยายภาพมาจากวิกิพีเดีย: Yi Peng festival, Wat Phra Singh, Wat Phra That Doi Kham, Bo Sang, Khantoke และบทความภาษาไทย ซิ่น, ผ้าจกไทยวน กับ อำเภอแม่แจ่ม ผ้าทอที่อยู่ด้านขวาของจอกว้างก็เป็นภาพประกอบเช่นกัน สร้างด้วย Qwen-Image บนเครื่องเดียวกันโดยmake_tapestry.pyโดยมีลวดลายรูปสัตว์และผู้คนบนผ้าล้านนาตามที่โมเดลจินตนาการ
- 六个声调、编号和例词:พจนานุกรมภาษาล้านนา, The Lanna Dictionary(2007),清迈皇家大学,据维基百科 Lanna language 条目的引用;该条目还给出了辅音、复辅音、含 ɲ 和 j 的词,以及 ก้า 和 ป้าย 的例子。
- 每个声调格子的测试词:W. J. Gedney(1972),"A checklist for determining tones in Tai dialects",据维基百科 Proto-Tai language 条目所列。
- 各种文字:维基百科 Tai Tham script、Thai script、Lao script、Burmese alphabet、Shan alphabet、Khmer script,以及泰文条目 ภาษาไทยถิ่นเหนือ 和 อักษรธรรมล้านนา。
- 这些文字如何存储和绘制,以及虚线圆圈:The Unicode Standard 15.0, chapter 16。
- 0.05% 的估计:Natnapang Burutphakdee(2004),Attitudes of Northern Thai youth towards Kammuang and the Lanna script,帕亚普大学硕士论文,引自 Boonkhit(1995)。
- 字体:Noto Sans Thai、Noto Sans Khmer、Noto Sans Lao、Noto Sans Myanmar 和 Noto Sans Tai Tham,作者 Noto Project Authors,SIL Open Font License 1.1。KM Alphabet Sans 由
build_font.py用前两种字体生成。Inter 由 Rasmus Andersson 设计,SIL Open Font License 1.1。 - 照片:清迈的牌子,Alifshinobi 摄(CC BY-SA 3.0);贝叶经,Lannafolium 摄(CC0);两张都来自 Wikimedia Commons,经裁剪。
- 北泰语词的兰纳文、泰文字母写法和清迈读音:泰文维基词典,分类 คำหลักภาษาคำเมือง,2026 年 10 月 10 日读取,维基词典贡献者,CC BY-SA 4.0。
- 兰纳的图:用 Juggernaut XL v9(SDXL)在 DGX Spark 上的 ComfyUI 里由
make_lanna_images.py生成的插图,不是照片。图注的内容来自维基百科:Yi Peng festival、Wat Phra Singh、Wat Phra That Doi Kham、Bo Sang、Khantoke,以及泰文条目 ซิ่น、ผ้าจกไทยวน 和 อำเภอแม่แจ่ม。 宽屏右侧的织锦也是插图,由make_tapestry.py用 Qwen-Image 在同一台机器上生成,上面是模型想象中兰纳织物上的动物与人物。