Các bản demo AI có mặt ở khắp mọi nơi. Chỉ cần một đoạn mã Python duy nhất là có thể tráo đổi khuôn mặt trong một bức ảnh tĩnh, và kết quả trông thật kỳ diệu. Nhưng xây dựng một thứ mà người dùng thực tế có thể tải lên, rời đi, rồi quay lại sau đó? Đó lại là một công việc hoàn toàn khác. Gần đây tôi đã xây dựng một công cụ web nhận một tệp GIF động và một khuôn mặt tham chiếu, sau đó trả về cùng một hoạt ảnh đó với khuôn mặt đã được tráo đổi qua mọi khung hình. Mô hình đảm nhận phần xử lý hình ảnh nặng nề, nhưng nỗ lực kỹ thuật thực sự lại nằm ở kiến trúc bao quanh nó: duy trì các kết nối, xử lý việc tải lại trang, và đảm bảo một tác vụ suy luận (inference) kéo dài hai phút không bị biến mất do lỗi 504 Gateway Timeout.

Đây chính là khoảng cách giữa một bản mẫu (prototype) và một sản phẩm thực thụ. Các kỹ sư rất thích nói về kiến trúc khuếch tán (diffusion architectures) và các tham số suy luận. Nhưng khi người dùng tải lên một tệp GIF dày đặc với hai trăm khung hình, chẳng ai quan tâm đến mô hình của bạn nếu tab trình duyệt bị treo sau ba mươi giây. Cơ sở hạ tầng xung quanh quá trình suy luận cũng quan trọng không kém gì chính quá trình suy luận đó.

Vấn đề của việc chờ đợi

Kiến trúc web tiêu chuẩn giả định rằng các phản hồi sẽ diễn ra nhanh chóng. Người dùng nhấp vào một nút, máy chủ trả lời, và trang web cập nhật. Việc tráo đổi khuôn mặt qua hàng chục khung hình GIF phá vỡ giả định đó ngay lập tức. Mô hình chạy trên GPU của Replicate chứ không phải trên máy chủ của tôi, và một tệp GIF dài có thể dễ dàng mất một phút hoặc hơn để xử lý. Nếu bạn cố gắng giữ một yêu cầu HTTP mở lâu như vậy, bạn đang tự chuốc lấy rắc rối. Các bộ cân bằng tải (load balancers) sẽ ngắt các kết nối nhàn rỗi. Trình duyệt sẽ cho rằng mạng bị lỗi và thử lại. Người dùng sẽ nhìn chằm chằm vào biểu tượng xoay (spinner) bị đứng và cho rằng ứng dụng đã bị hỏng.

Tôi đã tránh hoàn toàn điều này bằng cách tách biệt việc gửi yêu cầu (submission) khỏi kết quả. Khi người dùng tải lên một tệp GIF và một ảnh khuôn mặt, backend Next.js của tôi sẽ bắt đầu một dự đoán trên Replicate và ngay lập tức trả về một job ID. Người dùng nhận được xác nhận ngay lập tức. Kết quả thực tế sẽ đến sau thông qua một webhook khi công việc trên GPU hoàn tất. Mô hình này không có gì xa lạ, nhưng đối với các tác vụ truyền thông chạy lâu, nó là cực kỳ thiết yếu. Nó biến một sự chờ đợi không thể đoán trước thành một cái bắt tay đầy tin cậy: công việc đã được chấp nhận, và bạn sẽ được thông báo khi nó hoàn thành.

Xây dựng một Máy trạng thái (State Machine) mà bạn có thể tin tưởng

Một khi bạn chuyển sang xử lý bất đồng bộ (asynchronous), bạn cần sự hiển thị (visibility). Người dùng sẽ tải lại trang. Họ sẽ đóng tab và mở lại. Họ sẽ sao chép liên kết và gửi cho đồng nghiệp, người sẽ kiểm tra nó hai giờ sau đó. Nếu không có một bản ghi bền vững về những gì đã xảy ra, bạn sẽ gặp phải sự hỗn loạn.

Tôi sử dụng Supabase làm nguồn sự thật duy nhất (single source of truth). Mỗi lần tải lên sẽ tạo ra một hàng với một job ID duy nhất, và hàng đó sẽ di chuyển qua các trạng thái cụ thể: queued (đang chờ), processing (đang xử lý), succeeded (thành công), failed (thất bại), hoặc expired (hết hạn). Khi người dùng nhấn gửi lần đầu, hàng đó ở trạng thái queued. Ngay khi Replicate chấp nhận dự đoán, nó chuyển sang processing. Webhook sẽ đẩy nó sang succeeded hoặc failed. Tôi đã thêm trạng thái expired cho các công việc nằm chờ quá lâu mà không có phản hồi (callback), để hệ thống không phải đi tìm những "bóng ma" vô định.

Frontend, được xây dựng bằng TypeScript, sẽ thăm dò (poll) Supabase theo các khoảng thời gian ngắn và hiển thị bất cứ điều gì trạng thái hiện tại cho biết. Việc thăm dò này trông có vẻ thô sơ, nhưng nó giải quyết hoàn toàn vấn đề tải lại trang. Người dùng có thể đóng máy tính xách tay, mở lại vào ngày mai và thấy chính xác tiến độ đang ở đâu vì cơ sở dữ liệu — chứ không phải bộ nhớ trình duyệt — là nơi nắm giữ tiến trình. Supabase cũng theo dõi các credit, vì vậy việc hạch toán luôn gắn liền với cùng một bản ghi công việc theo dõi trạng thái. Mọi thứ đều nằm ở một nơi.

Xử lý tệp mà không làm quá tải trình duyệt

Các tệp GIF nặng hơn mọi người nghĩ. Một tệp có kích thước hoàn hảo cho quy trình AI vẫn có thể nặng vài megabyte. Việc hiển thị tệp đó dưới dạng bản xem trước lặp lại (looping preview) bên trong trình duyệt sẽ phá hủy hiệu suất, đặc biệt là trên các thiết bị cấu hình thấp. Tôi cần hai quy trình tệp hoàn toàn riêng biệt: một cho AI và một cho giao diện người dùng.

Để xem trước, tôi chuyển đổi các tệp GIF lớn sang định dạng WebP động bằng cách sử dụng FFmpeg được biên dịch sang WebAssembly. Việc này chạy hoàn toàn ở phía client (client-side) trong trình duyệt. Kết quả là một bản xem trước nhẹ nhàng giúp giao diện luôn mượt mà mà không cần chạm vào tệp gốc. Tệp GIF gửi đến Replicate vẫn được giữ nguyên. Sự tách biệt đó rất quan trọng. Bạn sẽ không muốn các lỗi nén (compression artifacts) từ bản xem trước lọt vào dữ liệu huấn luyện hoặc bản render cuối cùng của mình, và bạn cũng không muốn giao diện người dùng bị đình trệ vì một khối dữ liệu nặng hàng megabyte trong khi AI vẫn đang xử lý.

FFmpeg WASM cũng xử lý các tác vụ GIF khác trước khi tệp được tải lên khỏi trình duyệt. Tôi sử dụng nó để phân tích số lượng khung hình, xác thực kích thước và phát hiện sớm các tệp bị lỗi. Phát hiện vấn đề trước khi nó tiêu tốn credit GPU sẽ giúp tiết kiệm cả tiền bạc lẫn sự kiên nhẫn của người dùng.

Biến một bản Demo thành Phần mềm đáng tin cậy

Có một bài học rộng lớn hơn ở đây, áp dụng cho hầu hết mọi công cụ AI tạo sinh trên thị trường. Bản thân mô hình có thể chỉ chiếm 30% khối lượng công việc. 70% còn lại là những phần hạ tầng thầm lặng mà không ai nhắc đến trên Twitter: khôi phục trạng thái, chữ ký webhook, chuyển đổi tệp, theo dõi tín dụng và xử lý lỗi một cách mượt mà.

Stack của tôi đơn giản và có tính toán. Next.js và TypeScript xử lý giao diện và các tuyến API. Replicate chạy các mô hình. Supabase quản lý trạng thái, dữ liệu và tín dụng. FFmpeg WASM xử lý các tác vụ media ở phía client. Animated WebP giúp UI luôn nhanh chóng. Mỗi thành phần đều có một nhiệm vụ riêng, và chúng kết nối với nhau thông qua các bước chuyển đổi trạng thái rõ ràng thay vì các yêu cầu kéo dài và dễ lỗi.

Khi người dùng dùng tín dụng để thực hiện hoán đổi khuôn mặt, họ mong đợi sự tin cậy chứ không phải một bài báo nghiên cứu. Nếu một dự đoán thất bại, hệ thống cần phải biết và thông báo điều đó. Nếu người dùng phải chờ đợi, họ nên có một bản xem trước nhẹ nhàng để theo dõi và một trạng thái vẫn được duy trì ngay cả khi khởi động lại trình duyệt. Những chi tiết này sẽ trở nên vô hình khi chúng hoạt động tốt, nhưng sẽ trở nên chí mạng khi chúng gặp lỗi.

Bản thân việc hoán đổi khuôn mặt chỉ là một thủ thuật hay ho. Nhưng công cụ này chỉ mang lại cảm giác thực thụ vì người dùng có thể tải lên, rời đi và quay lại mà không bị mất tiến trình. Đó chính là điều biến một lệnh gọi API thành một phần mềm mà mọi người thực sự tin tưởng.