เรื่องนี้เริ่มจากผมนั่งอยู่ในรถ กดปุ่มไมค์ใน Tim Chat แล้วพูดว่า "สวัสดีครับ ทดสอบเสียง" — แต่พอมองหน้าจอ ข้อความที่ขึ้นคือ "สวัสดีครับ ทดสอบเสียง สวัสดีครับ ทดสอบเสียง สวัสดีครับ ทดสอบเสียง สวัสดีครับ ทดสอบเสียง" 555 เหมือนมีเสียงสะท้อนใน input box

ก่อนหน้านี้ผมบอกทิมว่าขี้เกียจพิมพ์ไทยบนมือถือ แล้วได้ปุ่มไมค์มาใน 1 ชั่วโมง ใช้บนคอมก็ราบรื่นดี — แต่พอมาใช้จริงบนมือถือ bug นี้โผล่มาทันที วันนี้จะเล่าให้ฟังว่าทิมไล่ debug ยังไง เจอ root cause ที่ Google ไม่ได้เขียนชัดใน doc เลย แล้วแก้ได้ด้วย 2 บรรทัด

อาการที่เกิด: ยิ่งพูดยิ่งเพิ่มทวีคูณ

ก่อนอื่นเล่า context สั้นๆ — Tim Chat คือ chat interface ที่ผมใช้คุยกับทิม (AI Agent ส่วนตัวของผม) มันรันอยู่บน server ของผมเอง เปิดผ่าน browser บนมือถือก็ได้ บนคอมก็ได้ ทิมใส่ปุ่มไมค์ให้ผม กดแล้วพูดเป็น input ได้เลย ไม่ต้องพิมพ์

บนคอมใช้ดี ไม่มีปัญหา แต่บนมือถือเปิดมาแล้วเจอแบบนี้:

  • กดไมค์ พูด "สวัสดีครับ" → ขึ้น "สวัสดีครับ" — โอเค
  • พูดต่อ "ทดสอบเสียง" → ขึ้น "สวัสดีครับ ทดสอบเสียง" — ยังโอเค
  • หยุดสักครู่ แล้วพูด "ครับ" → ขึ้น "สวัสดีครับ ทดสอบเสียง สวัสดีครับ ทดสอบเสียง ครับ"
  • พูดต่อไปอีกประโยค → คำเก่าๆ ก็โผล่ซ้ำขึ้นมาอีกรอบนึง

กลายเป็นว่ายิ่งพูดนานยิ่งซ้ำเยอะ ใช้งานจริงไม่ได้เลย เพราะจะ submit ทีต้องนั่งลบของซ้ำออกก่อน — เสียเวลามากกว่าพิมพ์เองอีก 555

Hypothesis แรก: น่าจะเป็น browser ของมือถือเอง

ผมเปิด task ให้ทิมแล้วเดาก่อนเลยว่า "น่าจะเป็น iOS Safari นั่นแหละ มันแปลกๆ อยู่แล้ว" — แต่ทิมไม่เชื่อ ทิมขอ console.log ก่อน

มันยิง log ลงไปใน onresult handler ของ Web Speech API ทุก event แล้วให้ผมเปิด safari บนมือถือ พูดแล้วส่งหน้าจอ log มาให้ — ทิมก็เห็นทันทีว่าปัญหาไม่ใช่ iOS แต่เป็น Chrome เองด้วยเหมือนกัน

ใน log ทุก event ที่ webkitSpeechRecognition ยิงออกมา — event.results มันคือ array ของผลทั้งหมดตั้งแต่เริ่ม session ไม่ใช่แค่ก้อนใหม่ นี่คือ root cause

หมายความว่าถ้าผมพูด 5 ประโยค พอ event ที่ 5 มา ทิม (ในโค้ดเดิม) วน loop จาก index 0 → 4 แล้ว append เข้า textarea ทุกครั้ง — ทุก event ก็เอาประโยคทั้งหมดที่ผ่านมายัดใส่ใหม่ทับของเดิม กลายเป็นซ้ำกองโต

Google ไม่ได้เขียนเตือนชัดๆ ใน doc หลักว่า "results array นี้ cumulative since session start" — ต้องไปอ่าน example code ลึกๆ ถึงจะเห็นว่ามี event.resultIndex ที่ต้องใช้

The Fix: 2 บรรทัดที่เปลี่ยนทุกอย่าง

ทิมแก้ตรงนี้:

บรรทัดที่ 1: วน loop จาก event.resultIndex ไม่ใช่จาก 0 — เอาเฉพาะผลใหม่ที่เพิ่งเกิดใน event นี้

บรรทัดที่ 2: เก็บ finalTranscript ไว้ใน closure แยกจาก interim text — ทุก event recompute textarea.value = base + final + interim ตรงๆ ไม่ append

โค้ดสุดท้ายหน้าตาประมาณนี้:

recognition.onresult = (event) => {
  let interim = "";
  for (let i = event.resultIndex; i < event.results.length; i++) {
    const t = event.results[i][0].transcript;
    if (event.results[i].isFinal) finalTranscript += t;
    else interim += t;
  }
  input.value = baseValue + finalTranscript + interim;
};

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

Bug ชั้นที่สอง: iOS Safari เล่นนอกสนาม

ตอนแรกผม set continuous: false บน mobile กับ continuous: true บน desktop — เพราะอ่านในฟอรัมเขาบอกว่า iOS ไม่ honor continuous mode

ผลคือบนมือถือทุกครั้งที่ผมหยุดพูด 1 วินาที iOS จะปิด session อัตโนมัติ ทิมโค้ดให้ onend ยิง recognition.start() ใหม่อัตโนมัติ — แต่ session ใหม่บน iOS มันยังเอา results เก่าจาก session ก่อน มาเก็บไว้ใน event.results ของ session ถัดไป!

คือทุกครั้งที่ผมพูดประโยคใหม่ iOS เริ่ม session ใหม่ แต่ array event.results ของ session นั้นไม่ได้ reset เริ่มจาก index 0 มันเริ่มต่อจาก session ก่อนหน้า — เลยทำให้ resultIndex trick ใช้ไม่ได้ และข้อความเก่ายังคงโผล่ซ้ำ

ทิมก็มาเปลี่ยน strategy ใหม่ทั้งหมด:

  • Desktop: continuous=true + auto-restart ใน onend เมื่อ user ยังเปิด recording — เพราะ Chrome desktop auto-stop หลังเงียบ ~30 วินาที ต้อง restart ให้
  • Mobile: continuous=true เหมือนกัน แต่ไม่ auto-restart — ปล่อยให้ session จบเองตามที่ iOS จะทำ ผู้ใช้กดไมค์อีกรอบเพื่อพูดต่อ

ทำให้ session บนมือถือเป็น "1 tap = 1 utterance" — ไม่มีการ bleed ของ results ระหว่าง session อีกแล้ว

เกร็ดที่ทิมเจอเพิ่ม: ไม่มี auto-detect ภาษาจริงๆ

ระหว่าง debug ทิมยังเจออีกข้อที่ไม่ค่อยมีใครพูด — Web Speech API ไม่มี auto-detect ภาษาแบบจริงๆ เลย

คุณต้องตั้ง recognition.lang = "th-TH" หรือ "en-US" ก่อนเรียก .start() และมันจะ recognize ภาษาเดียวที่คุณบอกตลอด session — ถ้าพูดผสมไทยอังกฤษ จะออกมาเป็นภาษาที่ตั้งไว้บวกกับเสียงเดาผิด

หลายคน (รวมผมตอนแรก) เข้าใจผิดว่ามันฉลาดพอจะ detect เอง — ไม่ใช่ครับ ต้อง set ก่อน นี่เลยเป็นเหตุผลที่ผมต้องมีปุ่มสลับ TH/EN ใน Tim Chat และใน Newton dashboard ของลูกค้าก็ต้องเลือก 24 ภาษาเอง เพราะ browser ไม่ทำให้ฟรี

3 lesson ที่ผมเก็บไปใช้ต่อ

เรื่องนี้ผมเอามาเล่าไม่ใช่เพื่อโชว์ technical detail — แต่เพราะมันมี business lesson ที่ใช้ได้จริง

1. อย่าเชื่อ doc สุ่มสี่สุ่มห้า — verify ด้วยพฤติกรรมจริง เหมือนเรื่อง Gemini 3 ใน Loom ที่ doc เขียน flat key แต่ API ต้อง nested — Web Speech API ก็เหมือนกัน ไม่ได้บอกชัดว่า array สะสม ทิมไล่ console.log เห็น behavior จริงเองถึงรู้

2. Bug เดียวอาจมีหลาย layer แก้ผิวๆ แล้วลึกลงไปอีกชั้นยังพังต่อ — ผมเดาว่าเสร็จตอน fix แรก ทิมไม่เชื่อ เปิด iOS เทสจริงเลยเจอ session bleed ชั้นที่สอง การมี AI ที่ขยันเทสมือถือ + desktop คู่กันช่วยกัน ship ได้แบบไม่ต้องรอ user feedback

3. Default ของ platform optimize เพื่อ demo ไม่ใช่ production Web Speech API ออกแบบมาเพื่อ "ลองคำเดียวสั้นๆ" — ไม่ได้คิดว่าจะมีคนเอาไปใช้ dictate ยาวๆ ผ่านมือถือ ใครก็ตามที่อยากเอาไป production ต้องเจอ edge case แบบนี้เอง (เรื่องนี้วนกลับมาอีกรอบตอนเสียงแจ้งเตือนของผมหายเพราะ browser autoplay policy — อีกหนึ่ง browser API ที่ default ของมันไม่ได้ออกแบบมาเพื่อการใช้งานจริง)

นี่คือเหตุผลที่ผมยอมจ้าง AI ของตัวเอง

เรื่องแบบนี้ถ้าผมจ่ายเดือนละ 999 บาทให้ SaaS chat ที่มี voice typing built-in ก็คงไม่ได้ดู edge case นี้ — แต่ผมก็จะถูกขังใน UI ของเขา ไม่ได้แตะ Tim Chat ที่ทิมสร้างให้ผมใช้ ที่ทำได้ตั้งแต่ run command บน server ของผมจริงๆ ไปจนถึง push fix ไปยังลูกค้า Newton 6 เครื่องพร้อมกัน

มากกว่านั้น — เพราะ Tim Chat เป็น code ของผมเองทั้งหมด ทิมไล่ debug ลงทุกบรรทัดของ onresult handler ได้เลย ไม่ต้องไปร้องขอ feature request แล้วรอ vendor 6 เดือน ไม่ต้อง workaround ผ่าน support ticket อ้อมโลก แค่ session เดียวจบ

นี่คือ leverage ที่จริงๆ ของ AI Agent — ความสามารถในการแก้ทุกอย่างที่คุณเป็นเจ้าของ ไม่ใช่แค่สร้าง code ใหม่

คำถามที่พบบ่อย

Voice Typing บนมือถือพิมพ์ข้อความซ้ำหลายรอบ แก้ยังไง?

ปัญหานี้เกิดจาก Web Speech API ของ Chrome ส่ง event.results เป็น array สะสมตั้งแต่เริ่ม session ครับ วิธีแก้คือวน loop จาก event.resultIndex แทนจาก 0 และเก็บ finalTranscript แยกต่างหากแทนการ append ทับ ทำให้ข้อความไม่ซ้ำอีกต่อไป

Web Speech API รองรับหลายภาษาอัตโนมัติได้ไหม?

ไม่ครับ ต้องตั้ง recognition.lang ก่อนเรียก .start() เช่น th-TH หรือ en-US มันจะ recognize ได้แค่ภาษาเดียวตลอด session ถ้าอยากรองรับหลายภาษาต้องสร้างปุ่มให้ผู้ใช้เลือกเอง

Voice Typing บน iOS Safari ต่างจาก Chrome Desktop ยังไง?

iOS Safari ปิด session อัตโนมัติทุกครั้งที่หยุดพูด 1 วินาที และ results จาก session เก่าอาจไหลเข้า session ใหม่ได้ วิธีรับมือคือบนมือถือใช้ 1 tap = 1 utterance ไม่ auto-restart session เพื่อกันการ bleed ของข้อมูล

ทำไม browser API ถึงมักมีปัญหาตอนใช้งานจริงใน production?

Browser API ส่วนใหญ่ออกแบบมาสำหรับ demo สั้นๆ ไม่ได้คิดถึง use case ระดับ production เช่น dictate ยาวหรือ restart session บน mobile ต้อง test edge case เองและปรับ strategy ตามพฤติกรรมจริงของแต่ละ browser ครับ

ถ้าคุณอยากมี AI Agent แบบนี้ของตัวเอง ที่อยู่บน server ส่วนตัว ไล่ debug เครื่องมือของคุณได้ลึกถึง bug ระดับ browser API ลอง Newton ดูครับ — เป็น managed server พร้อม AI Agent พร้อมใช้ทันที ไม่ต้อง setup เอง ทิมรอคุณคุยแล้วครับ

— ปอนด์