Theppitak's blog

My personal blog.

29 สิงหาคม 2561

thpronun

ประกาศเปิดตัวโครงการ thpronun ซึ่งเป็นซอฟต์แวร์เสรีสำหรับวิเคราะห์เสียงอ่านของข้อความภาษาไทย (grapheme-to-phoneme) ภายใต้การสนับสนุนของ บริษัท เมตามีเดีย เทคโนโลยี จำกัด

ซอฟต์แวร์เสรีที่สนับสนุนโดยเอกชน

เรื่องของเรื่องคือ ผมได้รับการติดต่อว่าจ้างจากเมตามีเดียให้พัฒนาโปรแกรมสำหรับแจงคำอ่านจากตัวสะกดของคำไทย ผมจึงได้เสนอที่จะขายไลเซนส์ของตัวโปรแกรมให้ในแบบ GPL ซึ่งปรากฏว่าเมตามีเดียรับข้อเสนอนี้ และยินดีให้เผยแพร่ซอร์สโค้ดของโปรแกรมสู่สาธารณะได้ โดยมุ่งหวังให้เป็น deal ตัวอย่างในเมืองไทยที่นักพัฒนาซอฟต์แวร์เสรีจะสามารถสร้างรายได้จากตัวซอฟต์แวร์โดยตรง จึงขอขอบคุณเมตามีเดียมา ณ ที่นี้

เมื่อได้ข้อตกลงแล้ว ผมจึงนั่งร่างโปรแกรมใน local Git จนโปรแกรมพร้อมทดสอบ จึงได้หารือเรื่อง repository ที่จะใช้ส่งโค้ดระหว่างกัน ซึ่งคุณภัทระก็เสนอให้ใช้ tlwg บน GitHub ไปเลย ดังนั้น TLWG จึงได้เกิดโครงการน้องใหม่คือ thpronun ด้วยประการฉะนี้

นอกจากนี้ ในระหว่างการพัฒนา ก็ได้รับ contribution ใน libthai 0.1.28 เป็นอานิสงส์ด้วย

สัญญาว่าจ้างสิ้นสุดลงเมื่อผมออกรุ่นแรกสู่สาธารณะ (คือ รุ่น 0.2.0) หลังจากนี้ไปก็จะพัฒนาในแบบชุมชนโอเพนซอร์สตามปกติ ส่วนเมตามีเดียก็จะมี customization เชิงพาณิชย์สำหรับลูกค้าต่อไป

ลักษณะของโปรแกรม

ตัวโปรแกรมเป็นคำสั่ง command line รับข้อมูลเข้าเป็นข้อความภาษาไทย แล้วจะให้ข้อมูลออกเป็นคำอ่านที่เป็นไปได้ทั้งหมดของข้อความนั้น โดยสามารถเลือกรูปแบบของคำอ่านได้จาก command-line option

$ thpronun --help
Usage: thpronun [OPTION] [WORD...]

Thai word pronunciation program.

It reads Thai words from command-line arguments, or from standard input
if no argument is given, and generates all possible pronunciations
of the words.

Options:

General:
  -V, --version  Displays program version info
  -h, --help     Displays help
  -d<DICTPATH>   Use exception dict from <DICTPATH>
  -n             Turns off word segmentation

Output structures:
  -j             Turns on JSON output
  -g             Turns on grouping in JSON output (implies '-j')

Output notations:
  -r             Outputs Romanization
  -t             Outputs Thai pronunciation
  -p             Outputs Phonetic form
  -w             Outputs Raw pronunciation code
  -s             Outputs Soundex code

ตัวอย่างคำอ่านภาษาไทย:

$ thpronun -t เถลไถล
เถลไถล:
ถะ-เหฺล-ถะ-หฺลัย
เถน-ถะ-หฺลัย
เถ-ละ-ถะ-หฺลัย

Romanization:

$ thpronun -r เถลไถล
เถลไถล:
thalethalai
thenthalai
thelathalai

phonetic:

$ thpronun -p เถลไถล
เถลไถล:
thah1 ley4 thah1 lahy4
theyn4 thah1 lahy4
they4 lah3 thah1 lahy4

soundex:

$ thpronun -s เถลไถล
เถลไถล:
Ta_-le_-Ta_-lay
Ten-Ta_-lay
Te_-la_-Ta_-lay

หรือกระทั่ง raw code ตามที่เก็บในโปรแกรม:

$ thpronun -w เถลไถล
เถลไถล:
T_a_1@-3,l_E_4@3,T_a_1@-306,l_ay4@6
T_En4@3,T_a_1@-306,l_ay4@6
T_E_4@2,l_a_3@3,T_a_1@-306,l_ay4@6

กลไกภายใน

ตัวโปรแกรมพัฒนาด้วย C++14 (ควรคอมไพล์ด้วย GCC 6.1 ขึ้นไป หรือ Clang 3.4 ขึ้นไป) ซึ่งมีสิ่งอำนวยความสะดวกครบครันอย่างที่ภาษาโปรแกรมในยุคนี้ควรมี ทำให้ได้โค้ดที่กระชับและใช้เวลาพัฒนาไม่นานเกินไป โดยเฉพาะเมื่อเทียบกับ C++ สมัยก่อน ๆ

build-dependencies:

  • libthai สำหรับแยกแยะอักขระภาษาไทยและแบ่งคำ
  • libdatrie สำหรับจัดการพจนานุกรมคำยกเว้น
  • help2man สำหรับสร้าง man page อัตโนมัติ (ปิดใช้ได้ด้วย configure option --disable-man)

กลไกภายใน ใช้ hard-coded rule base แบบดิบ ๆ เลย ซึ่งมีข้อดีคือไม่ต้องเสียเวลาเตรียม corpus และสามารถปรับแก้กฎต่าง ๆ ได้ตามต้องการภายในกำหนดเวลาพัฒนาที่มี แต่ข้อเสียนอกเหนือจากเรื่อง human error ที่ต้องตามแก้ในกฎก็คือ ไม่สามารถให้น้ำหนักคำอ่านแต่ละแบบตามความน่าจะเป็นได้ ซึ่งเผอิญว่าโจทย์ที่ต้องการคือการค้นฐานข้อมูล ไม่ใช่การสังเคราะห์เสียงพูดที่ต้องเลือกคำอ่านเพียงแบบเดียว จึงยังพอกล้อมแกล้มได้

เมื่อแจงรูปแบบพยางค์ของ input ด้วยกฎแล้ว ก็จะได้ abstract representation ของคำอ่าน ซึ่งสามารถใช้ polymorphism ในการ generate รูปแบบคำอ่านสุดท้ายที่ต้องการ ไม่ว่าจะเป็น romanization, คำอ่านเรียงพยางค์ด้วยอักษรไทย, คำอ่านแบบ phonetic หรือกระทั่ง soundex

องค์ประกอบหลักของแต่ละพยางค์ประกอบด้วย:

  1. เสียงพยัญชนะต้น
  2. เสียงพยัญชนะควบกล้ำ (ถ้ามี)
  3. เสียงสระ
  4. เสียงพยัญชนะสะกด
  5. เสียงวรรณยุกต์

นอกจากนี้ ยังมี requirement เพิ่มเติมจากเมตามีเดียที่ต้องการให้จัดโครงสร้างข้อมูลเป็น lattice โดยแทนที่จะ enumerate คำอ่านทุก combination เป็น list เส้นตรง ก็ให้จัดเป็น lattice โดยแยกเส้นทางคำอ่านเฉพาะในช่วงที่อ่านได้หลายแบบ แล้ว merge กลับเมื่อถึงจุดร่วม โดยให้จัดรูปแบบ output เป็น nested list ของ JSON (ไม่เกิน 2 ชั้น)

$ thpronun -t -g เถลไถล
เถลไถล:
[[[["เถ","ละ"],["เถน"],["ถะ","เหฺล"]],[["ถะ","หฺลัย"]]]]

โครงสร้างดังกล่าวสามารถแทนได้ด้วย Directed Acyclic Graph (DAG) โดยแต่ละ vertex ของกราฟแทนตำแหน่งอักขระจบพยางค์ในข้อความ input และแต่ละ edge แทนเสียงอ่านที่เกิดขึ้นระหว่างตำแหน่งที่มันเชื่อมโยง

ด้วย requirement นี้ ทำให้ทุกฟีเจอร์ที่จะเพิ่มต้องคำนึงถึงการแปลงเข้าสู่ DAG และการทำ output grouping เสมอ จึงต้องเพิ่มข้อมูลตำแหน่งอักขระจบพยางค์เป็นองค์ประกอบที่หกของทุกพยางค์ด้วย โดยบางพยางค์ที่เพิ่มเข้ามาเพราะหลักอักษรนำ (เช่น ถะ ใน เถล และ ไถล ข้างต้น) ต้องแทนด้วยเลขลบที่ encode ขอบเขตพยางค์ที่มันเกาะอยู่ไปด้วยในเวลาเดียวกัน (ในทางเทคนิค สูตรที่ใช้คือ -(ตำแหน่งต้นพยางค์ * 100 + ตำแหน่งท้ายพยางค์))

ข้อยกเว้น

กฎที่ใช้แจงพยางค์จะพยายามครอบคลุมพยางค์ทั่ว ๆ ไป แต่จะไม่เจาะจงข้อยกเว้นบางอย่าง เช่น ทร ที่อ่านเป็น ซ, ฑ นางมณโฑ ที่อ่านเป็น ด, เสียงวรรณยุกต์ของคำแผลง (เช่น กำเนิด, ตำรวจ), การันต์ซับซ้อน (เช่น กษัตริย์, สิริกิติ์, รามเกียรติ์, สุรเกียรติ์) ฯลฯ ซึ่งกรณีเหล่านี้จะจัดการด้วยพจนานุกรมคำยกเว้น (exception dictionary) เพราะการพยายามรวมในกฎจะทำให้เกิด noise เป็นคำอ่านที่ผิดเจือปนเข้ามาโดยไม่จำเป็นสำหรับกรณีทั่วไป

โดยปกติ thpronun จะมีพจนานุกรมคำยกเว้นมาให้อยู่แล้ว แต่คุณก็สามารถสร้างพจนานุกรมคำยกเว้นเองได้ โดยดัดแปลงจากไดเรกทอรี data/ ใน source tree และใช้ตัวเลือก -d<DICTPATH> ใน command line เพื่อระบุพจนานุกรมคำยกเว้น

เนื้อหาของพจนานุกรม จะ map จากตัวสะกดคำไทยไปเป็นคำอ่านในรูป raw code ซึ่งเป็นรูปแบบภายในของโปรแกรมที่แสดงจากตัวเลือก -w เช่น:

ทรง s_og0@3
ทรัพย์ s_ap3@6

การแบ่งคำ

ในกรณีทั่วไป การแบ่งคำข้อความก่อนแจงพยางค์ก็ช่วยลดจำนวนคำอ่านที่เป็นไปได้ลงได้มาก โดยปกติ thpronun จึงเรียกใช้ LibThai เพื่อแบ่งคำก่อน แต่ในบางกรณี การแบ่งคำก็อาจตัดคำอ่านที่ควรจะเป็นออกไปได้ เช่น ในกรณีของคำที่ไม่อยู่ในพจนานุกรมของ LibThai เอง ในกรณีเช่นนั้น คุณก็สามารถปิดการแบ่งคำก่อนได้ โดยใช้ตัวเลือก -n ใน command line

ป้ายกำกับ:

23 สิงหาคม 2561

LibThai 0.1.28 and its Consequences

บันทึกการเปลี่ยนแปลงต่าง ๆ ที่มาใน LibThai 0.1.28 และผลพวงทั้งหลายหลังจากนั้น

LibThai 0.1.28 ออกไปตั้งแต่ต้นเดือน โดยรุ่นนี้มีรายการเปลี่ยนแปลงสำคัญ ๆ คือ:

  • แก้ปัญหาขาด header <thai/thwchar.h> ใน header ที่เกี่ยวกับฟังก์ชัน wide char หลาย ๆ ตัว เช่น thwbrk.h, thwcoll.h ฯลฯ ซึ่งเป็นปัญหาที่พบระหว่างเขียนโปรแกรมตัวหนึ่งที่เรียกใช้ libthai ทำให้ต้อง include thai/thwchar.h เอง ซึ่งไม่สะดวก ในรุ่นนี้สามารถ include header ที่ต้องการแล้วเรียกใช้งานได้เลย ไม่ต้องเพิ่ม include เองอีกแล้ว
  • ปรับโค้ดให้เป็นไปตาม C90 (ANSI C) มากขึ้น เป็นผลพวงจากที่ได้ทำกับ libdatrie 0.2.12 มาแล้ว
  • ปรับข้อมูลพจนานุกรมตัดคำ โดยในรุ่นนี้ได้รับความช่วยเหลือจากคุณ @nuttee15 จาก metamedia technology ที่ได้เสนอคำเพิ่มเข้ามาใน Issue #2 ที่เปิดไว้รับเสนอคำใหม่ในพจนานุกรมตัดคำ

จากนั้นก็ได้ upload debian package พร้อมความเปลี่ยนแปลงอย่างอื่น คือ เพิ่ม pkg-config ให้เป็น dependency ของ libthai-dev เพื่อให้แน่ใจว่า libthai.pc จะสามารถทำงานได้แม้ในระบบที่ติดตั้งแบบเล็กที่สุด (เป็นปัญหาที่พบระหว่างที่ทำงานชิ้นหนึ่งร่วมกับ metamedia technology) และ การรองรับการ build ที่ไม่ต้องใช้ (fake)root

รายการคำจากพจนานุกรมตัดคำของ libthai ก็ได้นำไปใช้ สร้าง hyphenation pattern ที่โครงการ thailatex ซึ่งขณะนี้กลายสภาพเป็นเพียงที่พักงานพัฒนา hyphenation pattern เท่านั้น จากนั้นจึงได้เสนอ pull request สำหรับ update hyphenation pattern สำหรับภาษาไทยในโครงการ TeX hyphenation patterns ซึ่งต้องรอการ merge เพื่อให้มีผลที่ต้นน้ำต่อไป

จาก TeX hyphenation pattern ก็ต้อง sync มายังเครื่องมือตัดคำสำหรับเอกสาร LaTeX ด้วย คือ swath ซึ่งนอกจากการปรับพจนานุกรมแล้ว ก็ได้ปรับโค้ดเล็ก ๆ น้อย ๆ ตามที่เคยทำในทุกรุ่นที่ผ่านมา โดยสิ่งที่ทำในรุ่นนี้คือ:

  • ทดลอง build โดยใช้ CFLAGS -Wall แล้วแก้ warning ต่าง ๆ
  • จากการแก้ warning ที่พบในโค้ดส่วน RTF filter ทำให้ตรวจพบความผิดปกติใน method หนึ่งที่ตัวฟังก์ชันทำงานตรงข้ามกับชื่อ คือ RTFToken::isEmpty()

    โค้ดส่วนจัดการ RTF นี้ ไม่ค่อยมีใครเรียกใช้ ความจริงผมเคยเสนอจะตัดทิ้งไปแล้ว แต่ด้วยความช่วยเหลือของคนในชุมชน (คุณวิทยา ไตรสารวัฒนะ) ทำให้ได้วิธีทดสอบความถูกต้องของโปรแกรม จึงยังคงเก็บไว้ แต่เนื่องจากเวลานั้น swath ยังไม่เริ่มทำ TDD จึงยังไม่ได้ใส่ test case ไว้ใน source tree

    เพื่อจะตรวจแก้ฟังก์ชันที่สงสัยนี้ ผมจึงต้องไปดึงเอกสารตัวอย่างมาจาก thread เก่าที่ว่า แล้วนำมา เพิ่ม test case เสียก่อน หากคุณสงสัยว่าทำไม source tarball ของ swath รุ่นนี้ถึงโตขึ้นจนผิดสังเกต มันก็มาจากเอกสาร RTF ทดสอบนี้นี่เอง

    จากนั้น จึงได้ แก้ฟังก์ชันที่สงสัย นั้น แล้วรัน test เปรียบเทียบ output ดูโดยใช้ LibreOffice ปรากฏว่าเป็นการแก้ที่ถูกต้องแล้ว เพราะมันทำให้ได้จุดตัดคำครบถ้วนสมบูรณ์ขึ้น

    ไม่ว่าจะมีใครใช้หรือไม่ก็ตาม แต่คอมไพเลอร์ยังคงคอมไพล์มันอยู่ และนำผมเข้ามาเจอและแก้บั๊กจนได้

  • แก้ warning อื่น ๆ และทำความสะอาดโค้ดเล็ก ๆ น้อย ๆ
  • สุดท้าย มี pull request ของคุณ @pepa65 ที่เสนอไว้นานแล้ว เพื่อร่างแฟ้ม INSTALL ที่อธิบายความแตกต่างของวิธี build swath จาก git กับจาก released tarball ซึ่งผมก็เห็นว่ามีประโยชน์กว่าแฟ้ม INSTALL ที่ GNU automake มันเติมให้แบบอัตโนมัติ จึง merge เข้ามาเสีย

แล้วก็ออกรุ่น swath 0.6.1 ตามมาด้วย Debian upload ซึ่งก่อนอัปโหลดก็ได้ปรับ branch layout ของ swath packaging ตาม DEP-14 ด้วย

ป้ายกำกับ: , ,

21 สิงหาคม 2561

DEP-14 Note

บันทึกเตือนความจำสำหรับการทำตาม DEP-14: Recommended layout for Git packaging repositories เพื่อนำไปใช้ทำกับแพกเกจอื่น ๆ ในความดูแลของผมต่อไป

สำหรับ Debian package ปกติที่จัดการเวอร์ชันบน Git และใช้ git-buildpackage ในการ build นั้น จะมี branch layout ดังนี้:

master
pristine-tar
upstream
  • master เป็น branch หลักที่มีทั้ง upstream source tree และ Debian control files ครบสำหรับ build deb
  • upstream เป็น branch ที่เก็บ upstream source tree ซึ่ง maintainer จะ import source รุ่นใหม่เข้าที่นี่ก่อน (ถ้ามีการ repack เพื่อให้สอดคล้องกับ DFSG ก็ repack ก่อน import) แล้วจึง merge เข้าที่ master เพื่อเป็นการปรับรุ่น upstream ของ deb เอง
  • pristine-tar เป็น branch สำหรับ regenerate binary tarball รุ่นต่าง ๆ ของ upstream เพื่อใช้เป็นไฟล์ *.orig.tar.[gz|bz2|xz] โดยเก็บเฉพาะ delta ระหว่างรุ่นต่าง ๆ เพื่อให้ใช้เนื้อที่อย่างมีประสิทธิภาพสูงสุด

ศึกษารายละเอียดเพิ่มเติมได้ที่:

DEP-14 ได้แนะนำให้ใช้ branch layout แบบใหม่ เพื่อให้ Debian derivatives ต่าง ๆ ทำงานได้สะดวกขึ้น พร้อมกับเป็นการเตรียมการสำหรับเครื่องมือสร้างแพกเกจบน Git ต่าง ๆ ที่จะมีต่อไปในอนาคต

สำหรับแพกเกจใน Debian เอง โดยสรุปแล้วจะมีการเปลี่ยนแปลง branch layout ภาคบังคับอยู่ 2 รายการ คือ:

  • master เปลี่ยนเป็น debian/master
  • upstream เปลี่ยนเป็น upstream/latest

ทำให้มี branch layout ขั้นต่ำคือ:

debian/master
pristine-tar
upstream/latest

ที่เหลือก็ขึ้นอยู่กับความเปลี่ยนแปลงที่เกิดขึ้นกับแพกเกจ เช่น:

  • ถ้ามีการ update ใน stable release (เช่น security update, proposed update, backport) ก็ทำใน branch debian/jessie, debian/wheezy, debian/wheezy-backports ฯลฯ แล้วแต่กรณี
  • ถ้ามี experimental upload ก็ทำใน branch debian/experimental ซึ่งเป็น branch ชั่วคราวจนกว่าจะ merge เข้า debian/master หรือถ้าแพกเกจไหนมี experimental upload คู่ขนานกับ stable upload อยู่เป็นนิตย์ ก็อาจจะแทน branch debian/master ด้วย debian/sid แล้วก็ไม่ต้องลบ branch debian/experimental เลยก็ได้
  • ถ้าต้อง update upstream ขนานกันหลาย branch ก็อาจจะใช้ upstream/latest เก็บ development release ล่าสุด และสร้าง branch เช่น upstream/1.2.x ไว้เก็บ upstream รุ่น 1.2.*

สำหรับแพกเกจที่ผมดูแล ส่งตรงจาก linux.thai.net ซึ่งที่ผ่านมามีแต่ release เป็นเส้นตรง ไม่มีแยกแขนง ก็มักไม่มีความซับซ้อนอะไร (ยกเว้นตอนที่มี security update) สิ่งที่ต้องทำในตอนนี้จึงมีเพียง 3 ขั้นตอน:

  1. เปลี่ยนชื่อ branch upstream เป็น upstream/latest
  2. เปลี่ยนชื่อ branch master เป็น debian/master
  3. เพิ่มไฟล์ debian/gbp.conf เพื่อระบุให้ใช้ branch ชื่อใหม่

เปลี่ยนชื่อ branch upstream

การเปลี่ยนชื่อ Git remote branch สามารถทำได้ตามขั้นตอนดังนี้

  1. เปลี่ยนชื่อ local branch
    $ git branch -m upstream upstream/latest
    
  2. ลบ remote branch upstream
    $ git push origin :upstream
    
  3. push local branch ชื่อใหม่ไปที่ remote
    $ git push --set-upstream origin upstream/latest
    

เปลี่ยนชื่อ branch master

โดยหลักการแล้วก็ทำเหมือนตอนเปลี่ยนชื่อ branch upstream นั่นแหละ แต่จะมีความไม่ตรงไปตรงมานิดหน่อย

  1. เปลี่ยนชื่อ local branch
    $ git branch -m master debian/master
    
  2. ลบ remote branch master

    ขั้นตอนนี้แหละที่ tricky ที่สุด เพราะ คุณกำลังจะลบ default branch (master) ออกจาก repository!

    ถ้าคุณใช้ Salsa ล่ะก็ hook script ของมันจะปฏิเสธไม่ให้ลบ default branch ผ่านการ push เลยทีเดียว คุณต้องทำผ่าน web interface ดังนี้

    1. เปลี่ยน default branch ไปที่อื่นก่อน โดยใช้เมนู Settings > General > Default Branch
    2. ลบ branch master โดยใช้เมนู Repository > Branches สังเกตว่า branch master จะถูก protect ไว้ ไม่สามารถลบผ่านการ push ได้ แต่ใช้ web interface ลบได้ โดยมันจะถามยืนยันก่อนลบ
  3. push local branch ชื่อใหม่ไปที่ remote
    $ git push --set-upstream origin debian/master
    
  4. เปลี่ยน default branch ให้ชี้มาที่ debian/master โดยใช้เมนู Settings > General > Default Branch

เพิ่มไฟล์ debian/gbp.conf

คุณอาจจะต้อง clone Git repository ใหม่อีกครั้ง เพื่อให้ค่าต่าง ๆ ของ origin ตรงกับของ remote (เช่น HEAD)

จากนั้น เพิ่มไฟล์ debian/gbp.conf ที่มีเนื้อหาดังนี้:

[DEFAULT]
pristine-tar = True
debian-branch = debian/master
upstream-branch = upstream/latest

ก็จะสามารถ build package ด้วย git-buildpackage ได้ตามปกติผ่าน branch layout ตาม DEP-14

ป้ายกำกับ:

hacker emblem