Paxa Labs

บทความOCR

ป้อนเอกสารภาษาไทยให้ LLM: ทำไมต้อง OCR ก่อน

PDF คือภาพวาดของตัวอักษร และ PDF ภาษาไทยทำให้โมเดลภาษาพลาดได้สองทาง วิธีอ่านหน้าให้เสร็จก่อน จะส่งอะไรให้โมเดล และจ่ายค่าอ่านครั้งเดียวได้อย่างไร

· อ่าน 2 นาที

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

บทความนี้ว่าด้วยขั้นตอนระหว่างไฟล์กับพรอมต์ ครอบคลุมว่าโมเดลได้รับอะไรจาก PDF ภาษาไทย ทำไมตัวอ่านที่สร้างมาสำหรับหน้าภาษาไทยต้องมาก่อน ควรส่งรูปแบบไหนให้โมเดล จัดการตารางและขอบเขตหน้าอย่างไร และจ่ายค่าอ่านครั้งเดียวได้อย่างไร ตัวอย่างใช้ Paxa OCR Lite เป็นตัวอ่าน และรูปร่างของไปป์ไลน์ใช้ได้กับตัวอ่านใดก็ตามที่คืนโครงสร้าง

โมเดลได้รับอะไรจาก PDF#

การส่ง PDF ให้ API ของโมเดลทำสิ่งใดสิ่งหนึ่งจากสองอย่างแทนคุณ และซ่อนไว้ว่าทำอย่างไหน บริการดึงชั้นข้อความออกมาใส่พรอมต์ หรือเรนเดอร์หน้าเป็นภาพแล้วใส่ภาพลงพรอมต์ ทั้งสองทางใช้ได้ดีพอกับสัญญาภาษาอังกฤษ ทั้งสองทางล้มเหลวกับสัญญาภาษาไทยด้วยเหตุผลที่เจาะจงกับตัวอักษรไทย

ชั้นข้อความภาษาไทยสร้างจากรูปอักษรที่ซอฟต์แวร์ผู้ผลิตวางไว้ ฟอนต์ไทยเก็บสำเนาของวรรณยุกต์และสระบนที่เลื่อนตำแหน่งไว้เป็นรูปอักษรในช่วงใช้งานส่วนตัว เพื่อหลบพยัญชนะหางสูงอย่าง ป หรือ ฝ และชั้นข้อความที่อ้างถึงรูปอักษรเหล่านั้นดึงออกมาเป็นรหัสที่ไม่มีเครื่องมือข้อความใดรู้จัก ผู้ผลิตรายอื่นเขียนเครื่องหมายก่อนตัวอักษรที่มันเกาะอยู่ บทความเรื่องการดึงข้อความรวบรวมความล้มเหลวเหล่านี้ไว้ บรรทัดด้านล่างคือบรรทัดที่พิมพ์หนึ่งบรรทัดกับสองสิ่งที่ชั้นข้อความทำกับมัน

หนึ่งบรรทัด สองชั้นข้อความ
printed        ปุ๋ยอินทรีย์ ถุงละ ๒๕๐ บาท
text layer A   ปยอินทรีย ถงละ ๒๕๐ บาท
text layer B   ป๋ ุยอินทรีย ์ ถ ุงละ ๒๕๐ บาท

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

ทำไมสแกนภาษาไทยต้องผ่านตัวอ่านก่อนถึงโมเดล#

ทางที่ใช้ภาพเลี่ยงชั้นข้อความและเจอกับตัวอักษรโดยตรง บรรทัดภาษาไทยใช้พื้นที่สี่ระดับในแนวตั้ง สระอาจห้อยใต้พยัญชนะ สระหรือไม้หันอากาศอาจอยู่บนพยัญชนะ และวรรณยุกต์อยู่เหนือขึ้นไปอีก หนึ่งคอลัมน์ซ้อนรูปอักษรได้ถึงสามตัว และเครื่องหมายที่แบกความหมายคือสิ่งที่เล็กที่สุดบนหน้า วรรณยุกต์ทั้งสี่ ่ ้ ๊ ๋ และสระบน ิ ี ึ ื แยกกันด้วยขีดหรือจุดที่กว้างไม่กี่พิกเซลที่ความละเอียดสแกนปกติ

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

ตัวอ่านที่ฝึกบนหน้าภาษาไทยอ่านที่ความละเอียดของหน้าและให้ตัวอักษรออกมาครั้งเดียว ข้อความคือสิ่งที่โมเดลถนัด การอ่านหน้าครั้งเดียวแล้วส่งข้อความให้โมเดลวางแต่ละระบบไว้บนงานที่มันทำได้ดี และทำให้ผลการอ่านเป็นชิ้นงานที่เก็บไว้ได้และคนเปิดตรวจได้

Markdown ในฐานะรูปแบบกลาง#

ขอ Markdown จากตัวอ่าน POST /v1/ocr คืนแต่ละหน้าเป็น GitHub-flavored Markdown ตามลำดับการอ่านโดยค่าตั้งต้น หัวข้อมาเป็นเครื่องหมายหัวข้อ รายการมาเป็นข้อรายการ และตารางมาเป็นตารางแบบขีดตั้ง โครงสร้างถูกแบกไว้ในอักขระธรรมดาที่รอดผ่านพรอมต์โดยไม่เปลี่ยน และโมเดลเคยอ่าน Markdown มามากกว่าข้อความมีโครงสร้างแบบอื่นใด หน้าด้านล่างคือใบเสนอราคาหนึ่งหน้าแบบที่ตัวอ่านคืนกลับมา

หนึ่งหน้าในรูป Markdown
# ใบเสนอราคา

เลขที่ QT-2569-0142 วันที่ 15 กรกฎาคม 2569

| รายการ | จำนวน | ราคาต่อหน่วย | รวม |
| --- | --- | --- | --- |
| ปุ๋ยอินทรีย์ 25 กก. | 40 ถุง | 250 | 10,000 |
| ค่าขนส่ง | 1 | 800 | 800 |

ยืนราคา 30 วัน นับจากวันที่ในใบเสนอราคา

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

มีสองอย่างที่ควรเพิ่มก่อนต่อหน้า ทำเครื่องหมายขอบเขตแต่ละหน้าด้วยคอมเมนต์อย่าง <!-- page 3 --> ซึ่งตัวเรนเดอร์ Markdown ซ่อนไว้และโมเดลอ่านเป็นเครื่องหมาย แล้วโมเดลจะอ้างเลขหน้าในคำตอบได้ และคงเลขไทยไว้ตามที่พิมพ์ โมเดลอ่าน ๒๕๐ ได้ถูกต้อง และการอ้างอิงที่แสดงข้อความจากตัวอ่านควรตรงกับสแกน บทความเรื่องดัชนีค้นหาอธิบายสำเนาที่ปรับรูปแล้วซึ่งดัชนีค้นคืนต้องมีคู่กับต้นฉบับ

ตาราง หัวกระดาษที่ซ้ำ และขอบเขตหน้า#

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

หัวกระดาษและท้ายกระดาษซ้ำทุกหน้า รหัสเอกสาร ชื่อบริษัท เลขหน้า และบรรทัดชั้นความลับมาถึงหน้าละครั้งและไม่เพิ่มอะไรหลังหน้าแรก ตรวจหาบรรทัดที่วนซ้ำที่ด้านบนหรือด้านล่างของหน้าส่วนใหญ่แล้วตัดทิ้ง และเก็บเลขหน้าไว้ในเครื่องหมายขอบเขตหน้าที่มันทำหน้าที่เป็นการอ้างอิง

เอกสารยาวอ่านเป็นส่วน ๆ หนึ่งคำขออ่านได้ไม่เกิน 50 หน้าและ 10 MiB และคู่มือ 200 หน้าคือ 4 คำขอ ให้เลขหน้าต่อเนื่องข้ามส่วน เพราะเลขหน้าคือสิ่งที่คำตอบอ้างถึง และส่วนที่เริ่มนับหน้า 1 ใหม่ทำให้ทุกการอ้างอิงหลังส่วนแรกพัง

อ่านครั้งเดียว และจ่ายค่าอ่านครั้งเดียว#

หนึ่งหน้ามีค่าใช้จ่าย 6.5 เครดิตไม่ว่าจะมีอะไรอยู่บนหน้า คู่มือ 200 หน้าคือ 1,300 เครดิต หรือ $1.30 และนั่นคือต้นทุนของทุกคำถามที่จะถามเอกสารนั้นตลอดไปถ้าเก็บผลการอ่านไว้ ไปป์ไลน์ที่อ่านเอกสารภายในทุกคำขอจ่ายเงินจำนวนนั้นซ้ำต่อคำถาม ใช้แฮชของไบต์ในไฟล์เป็นคีย์แคช แล้วเอกสารที่อัปโหลดสองครั้งจะถูกอ่านครั้งเดียว

ไปป์ไลน์อ่านครั้งเดียว
key    = sha256(bytes)
pages  = cache.get(key)
if pages is missing:
    pages = POST /v1/ocr { document: base64(bytes), model, output: "markdown" }
    cache.put(key, pages)
text   = join(pages, page => "<!-- page " + page.page + " -->\n" + page.markdown)
answer = model(system, text, question)

เก็บผลลัพธ์ของตัวอ่านไว้คู่กับแฮชของต้นฉบับ และประกอบพรอมต์จากข้อความที่เก็บไว้ทุกครั้ง จำนวนหน้าถูกอ่านก่อนคิดค่าใช้จ่าย และเอกสารที่ตัวอ่านเปิดไม่ได้ถูกปฏิเสธโดยไม่คิดค่าใช้จ่าย คำขอที่ล้มเหลวหลังคิดค่าใช้จ่ายจะได้รับเงินคืน ส่ง Idempotency-Key ไปกับการอ่าน แล้วการลองใหม่หลังหมดเวลาจะเล่นซ้ำผลลัพธ์ที่เก็บไว้โดยไม่คิดค่าใช้จ่ายครั้งที่สอง คู่มือ OCRอธิบายกฎการคิดค่าใช้จ่าย

พรอมต์รอบผลการอ่าน#

ผลการอ่านคือตัวเอกสาร พรอมต์ที่ล้อมรอบมันกำหนดว่าโมเดลทำอะไรกับเอกสารได้บ้าง และมีกฎสามข้อที่สำคัญกับเอกสารภาษาไทยมากกว่าเอกสารภาษาอังกฤษ

  • ตอบจากข้อความเท่านั้น และบอกเมื่อข้อความไม่มีคำตอบ โมเดลที่ถอยไปพึ่งสิ่งที่มันรู้เกี่ยวกับใบแจ้งหนี้ไทยโดยทั่วไป จะสร้างฟิลด์ที่ดูสมเหตุสมผลซึ่งหน้านั้นไม่เคยมี
  • ยกข้อความภาษาไทยมาตามตัวอักษร โมเดลที่ถูกขอให้สรุปจะปรับตัวสะกดและตัดคำลงท้ายทิ้ง และข้อความที่ยกมาต่างจากหน้ากระดาษเพียงเครื่องหมายเดียวคือข้อความที่ผู้ตรวจหาไม่เจอ
  • คงตัวเลขและศักราชไว้ตามที่พิมพ์ และแปลงในบรรทัดแยกต่างหาก ๒๕๖๙ 2569 และ 2026 คือปีเดียวกันในสามรูปแบบ และโมเดลที่แปลงในที่ทำให้การอ้างอิงชี้ไปยังข้อความที่ไม่ตรงกับหน้ากระดาษอีกต่อไป
พรอมต์ระบบสำหรับเอกสารที่อ่านแล้ว
You are answering questions about a Thai document.
The document follows, one page per <!-- page N --> marker.
Answer only from the document. When the answer is absent, say so.
Quote Thai text exactly as written, and name the page marker it came from.
Keep digits, amounts, and years as printed. Put any conversion on a separate line.

ภาพถ่ายเอกสารเดินทางเดียวกัน ภาพหนึ่งภาพนับเป็นหนึ่งหน้า ตัวอ่านรับ PNG JPEG และ WebP นอกเหนือจาก PDF และภาพถ่ายใบเสร็จจากโทรศัพท์มีค่าใช้จ่าย 6.5 เครดิตเท่ากับหน้าอื่น ถ่ายให้เห็นทั้งหน้า แล้วตัวอ่านจะคืนข้อความในรูป Markdown แบบเดียวกับที่สแกนได้

บล็อกโครงสร้างสำหรับการสกัดข้อมูล#

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

กฎที่ตามมาควรพูดให้ชัด อย่าขอตัวเลขที่อยู่ในบล็อกตารางจากโมเดล โค้ดอ่านมันได้แม่นยำ ส่วนโมเดลอ่านมันได้แค่น่าจะถูก ขอสิ่งที่โค้ดทำไม่ได้จากโมเดล คือบอกว่าย่อหน้าไหนในสามย่อหน้าระบุเงื่อนไขการชำระเงิน หรือข้อสัญญาข้อนั้นผูกพันผู้ซื้อหรือผู้ขาย ตรวจสิ่งที่กลับมาเทียบกับสคีมา และเก็บบล็อกที่คำตอบมาจากไว้เป็นการอ้างอิง

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

โมเดลยังมีที่ยืนตรงไหน#

ตัวอ่านเปลี่ยนหน้าให้เป็นข้อความ โมเดลทำที่เหลือ มันสรุประเบียบห้าสิบหน้า ตอบคำถามข้ามโฟลเดอร์สัญญา จำแนกเอกสารจากหน้าแรก ปรับสี่แบบที่เอกสารไทยเขียนวันที่ให้เป็นฟิลด์เดียว และร่างจดหมายตอบ ทุกงานเหล่านั้นคืองานข้อความ และทุกงานดีได้เท่ากับข้อความที่มันเริ่มต้นด้วย

ขั้นต่อไป#