AI demos มีอยู่ทุกหนทุกแห่ง สคริปต์ Python เพียงตัวเดียวก็สามารถสลับใบหน้าในภาพนิ่งได้ และผลลัพธ์ที่ได้ก็ดูราวกับเวทมนตร์ แต่การสร้างบางอย่างที่ผู้คนใช้งานได้จริง—อัปโหลดไฟล์แล้วเดินไปทำอย่างอื่น แล้วค่อยกลับมาดูผลลัพธ์ได้? นั่นเป็นงานที่ต่างออกไปอย่างสิ้นเชิง เมื่อเร็วๆ นี้ ผมได้สร้างเครื่องมือเว็บที่รับไฟล์ GIF เคลื่อนไหวและภาพใบหน้าอ้างอิง จากนั้นจะส่งแอนิเมชันเดิมกลับมาโดยที่มีการสลับใบหน้าในทุกๆ เฟรม ตัวโมเดลทำหน้าที่ประมวลผลภาพที่หนักหน่วง แต่ความพยายามทางวิศวกรรมที่แท้จริงกลับไปอยู่ที่สถาปัตยกรรมรอบๆ มัน: การรักษาการเชื่อมต่อให้คงอยู่ การรองรับการรีเฟรชหน้าเว็บ และการทำให้แน่ใจว่างาน inference ที่ใช้เวลาสองนาทีจะไม่หายไปเพราะข้อผิดพลาด 504 Gateway Timeout

นี่คือช่องว่างระหว่างต้นแบบ (prototype) กับผลิตภัณฑ์ (product) วิศวกรชอบพูดถึงสถาปัตยกรรม diffusion และพารามิเตอร์การประมวลผล (inference parameters) แต่เมื่อผู้ใช้อัปโหลด GIF ที่มีเฟรมหนาแน่นถึงสองร้อยเฟรม จะไม่มีใครสนใจโมเดลของคุณเลยหากแท็บเบราว์เซอร์หยุดทำงานหลังจากผ่านไปเพียงสามสิบวินาที โครงสร้างพื้นฐานรอบๆ การ inference นั้นมีความสำคัญพอๆ กับตัวการ inference เอง

ปัญหาของการรอคอย

สถาปัตยกรรมเว็บมาตรฐานตั้งอยู่บนสมมติฐานของการตอบสนองที่รวดเร็ว ผู้ใช้คลิกปุ่ม เซิร์ฟเวอร์ตอบกลับ และหน้าเว็บอัปเดต การสลับใบหน้าผ่านเฟรม GIF จำนวนมากทำลายสมมติฐานนั้นทันที โมเดลทำงานบน GPU ของ Replicate ไม่ใช่บนเซิร์ฟเวอร์ของผม และ GIF ที่มีความยาวอาจใช้เวลาประมวลผลหนึ่งนาทีหรือมากกว่านั้นได้อย่างง่ายดาย หากคุณพยายามเปิด HTTP request ค้างไว้นานขนาดนั้น คุณกำลังหาเรื่องใส่ตัว Load balancer จะตัดการเชื่อมต่อที่ไม่ได้ใช้งาน (idle) เบราว์เซอร์จะทึกทักว่าเครือข่ายล้มเหลวและพยายามใหม่ ส่วนผู้ใช้ก็จะจ้องมองไอคอนหมุนๆ ที่ค้างอยู่และคิดว่าแอปเสียไปแล้ว

ผมหลีกเลี่ยงปัญหานี้โดยสิ้นเชิงด้วยการแยกการส่งข้อมูล (submission) ออกจากผลลัพธ์ (result) เมื่อผู้ใช้อัปโหลด GIF และภาพใบหน้า Backend ที่เขียนด้วย Next.js ของผมจะเริ่มการทำนาย (prediction) บน Replicate และส่ง job ID กลับไปทันที ผู้ใช้จะได้รับการยืนยันในทันที ส่วนผลลัพธ์จริงจะส่งมาภายหลังผ่าน webhook เมื่อการทำงานของ GPU เสร็จสิ้น รูปแบบนี้ไม่ใช่เรื่องแปลกใหม่ แต่สำหรับงานสื่อที่ใช้เวลานาน มันเป็นสิ่งที่จำเป็นอย่างยิ่ง มันเปลี่ยนการรอคอยที่คาดเดาไม่ได้ให้เป็นการตอบรับที่มั่นใจ: งานได้รับการยอมรับแล้ว และคุณจะได้รับการแจ้งเตือนเมื่อมันเสร็จสิ้น

การสร้าง State Machine ที่คุณไว้วางใจได้

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

ผมใช้ Supabase เป็นแหล่งข้อมูลความจริงหนึ่งเดียว (single source of truth) ทุกการอัปโหลดจะสร้างแถวข้อมูลพร้อม job ID ที่ไม่ซ้ำกัน และแถวนั้นจะเคลื่อนผ่านสถานะต่างๆ ได้แก่: queued, processing, succeeded, failed หรือ expired เมื่อผู้ใช้กดส่งครั้งแรก แถวข้อมูลจะอยู่ในสถานะ queued ทันทีที่ Replicate รับการทำนาย สถานะจะเปลี่ยนเป็น processing จากนั้น webhook จะผลักสถานะไปที่ succeeded หรือ failed ผมยังเพิ่มสถานะ expired สำหรับงานที่ค้างอยู่นานเกินไปโดยไม่มีการตอบกลับ เพื่อไม่ให้ระบบต้องคอยตามหา "ผี" (งานที่หายไป) อย่างไม่มีที่สิ้นสุด

Frontend ที่สร้างด้วย TypeScript จะทำการ polling ข้อมูลจาก Supabase เป็นระยะสั้นๆ และแสดงผลตามสถานะปัจจุบัน การ polling นี้อาจดูเป็นวิธีแบบดั้งเดิม แต่มันแก้ปัญหาการรีเฟรชหน้าเว็บได้อย่างสมบูรณ์ ผู้ใช้สามารถปิดโน้ตบุ๊ก แล้วเปิดขึ้นมาใหม่ในวันพรุ่งนี้เพื่อดูสถานะที่แน่นอนได้ เพราะฐานข้อมูล—ไม่ใช่หน่วยความจำของเบราว์เซอร์—เป็นผู้ถือครองความคืบหน้า นอกจากนี้ Supabase ยังติดตามเครดิต (credits) ทำให้การทำบัญชีผูกติดอยู่กับบันทึกงาน (job record) เดียวกันกับที่ติดตามสถานะ ทุกอย่างรวมอยู่ในที่เดียว

การจัดการไฟล์โดยไม่ทำให้เบราว์เซอร์ค้าง

GIF นั้นหนักกว่าที่คนคิด ไฟล์ที่มีขนาดพอเหมาะสำหรับ AI pipeline อาจยังมีน้ำหนักหลายเมกะไบต์ การเรนเดอร์ไฟล์นั้นเป็นตัวอย่าง (preview) แบบวนลูปภายในเบราว์เซอร์จะทำลายประสิทธิภาพการทำงาน โดยเฉพาะในอุปกรณ์สเปกต่ำ ผมจึงต้องการไฟล์ pipeline สองชุดที่แยกจากกันโดยสิ้นเชิง: ชุดหนึ่งสำหรับ AI และอีกชุดหนึ่งสำหรับส่วนติดต่อผู้ใช้ (UI)

สำหรับการแสดงตัวอย่าง ผมแปลง GIF ขนาดใหญ่เป็น WebP แบบเคลื่อนไหวโดยใช้ FFmpeg ที่คอมไพล์เป็น WebAssembly ซึ่งทำงานบนฝั่ง client ภายในเบราว์เซอร์ทั้งหมด ผลลัพธ์ที่ได้คือตัวอย่างที่มีน้ำหนักเบา ช่วยให้หน้าจอทำงานได้อย่างรวดเร็วโดยไม่ต้องแตะต้องไฟล์ต้นฉบับ ไฟล์ GIF ที่ส่งไปยัง Replicate จะยังคงเดิม การแยกส่วนแบบนี้มีความสำคัญมาก คุณคงไม่อยากให้ร่องรอยการบีบอัด (compression artifacts) จากตัวอย่างหลุดเข้าไปในข้อมูลการฝึกฝนหรือผลลัพธ์สุดท้าย และคุณก็ไม่อยากให้ UI ต้องหยุดชะงักเพราะต้องจัดการกับไฟล์ขนาดใหญ่หลายเมกะไบต์ในขณะที่ AI ยังประมวลผลไม่เสร็จ

FFmpeg WASM ยังจัดการงานอื่นๆ ของ GIF ก่อนที่การอัปโหลดจะออกจากเบราว์เซอร์ด้วย ผมใช้มันเพื่อตรวจสอบจำนวนเฟรม ตรวจสอบขนาด (dimensions) และตรวจจับไฟล์ที่เสียหายตั้งแต่เนิ่นๆ การตรวจพบปัญหาก่อนที่จะเสียเครดิต GPU ช่วยประหยัดทั้งเงินและความอดทนของผู้ใช้

การเปลี่ยนเดโมให้กลายเป็นซอฟต์แวร์ที่ผู้คนไว้วางใจ

มีบทเรียนที่กว้างกว่านั้นซึ่งสามารถนำไปใช้ได้กับเครื่องมือ Generative AI เกือบทุกตัวในตลาด ตัวโมเดลเองอาจเป็นเพียง 30 เปอร์เซ็นต์ของงานทั้งหมด ส่วนอีก 70 เปอร์เซ็นต์ที่เหลือคืองานวางระบบพื้นฐานที่ดูไม่หวือหวาและไม่มีใครเอาไปทวีตถึง: การกู้คืนสถานะ (state recovery), webhook signatures, การแปลงไฟล์, การติดตามเครดิต และการจัดการเมื่อเกิดข้อผิดพลาดอย่างราบรื่น (graceful failure)

Stack ของผมเรียบง่ายและผ่านการคิดมาอย่างดี Next.js และ TypeScript จัดการส่วนอินเทอร์เฟซและ API routes Replicate ทำหน้าที่รันโมเดล Supabase จัดการสถานะ (states), ข้อมูล และเครดิต FFmpeg WASM จัดการงานด้านสื่อฝั่งไคลเอนต์ (client-side) Animated WebP ช่วยให้ UI ทำงานได้อย่างรวดเร็ว แต่ละส่วนมีหน้าที่เพียงอย่างเดียว และเชื่อมต่อกันผ่านการเปลี่ยนสถานะที่ชัดเจน (explicit state transitions) แทนที่จะเป็นการส่งคำขอ (requests) ที่ค้างไว้นานและเปราะบาง

เมื่อผู้ใช้ใช้เครดิตเพื่อแลกกับการสลับใบหน้า (face swap) พวกเขาคาดหวังความน่าเชื่อถือ ไม่ใช่รายงานวิจัย หากการทำนาย (prediction) ล้มเหลว ระบบควรจะรู้และแจ้งให้ทราบ หากผู้ใช้ต้องรอ พวกเขาควรจะมีพรีวิวแบบเบาๆ (lightweight preview) ให้ดู และมีสถานะที่ยังคงอยู่แม้จะรีสตาร์ทเบราว์เซอร์ไปแล้วก็ตาม รายละเอียดเหล่านี้จะไม่มีใครสังเกตเห็นเมื่อมันทำงานได้ดี แต่จะเป็นเรื่องคอขาดบาดตายเมื่อมันทำงานผิดพลาด

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