AI demos are everywhere. A single Python script can swap a face in a static photo, and the result looks magical. But building something that real people can actually upload to, walk away from, and come back to? That is an entirely different job. I recently built a web tool that takes an animated GIF and a reference face, then returns the same animation with the face swapped across every frame. The model does the visual heavy lifting, yet the real engineering effort went into the architecture surrounding it: keeping connections alive, surviving page refreshes, and making sure a two-minute inference job did not vanish into a 504 Gateway Timeout.
This is the gap between a prototype and a product. Engineers love to talk about diffusion architectures and inference parameters. But when a user uploads a dense, two-hundred-frame GIF, nobody cares about your model if the browser tab gives up after thirty seconds. The infrastructure around the inference matters just as much as the inference itself.
The Problem with Waiting
Standard web architecture assumes fast responses. A user clicks a button, the server answers, and the page updates. Face swapping across dozens of GIF frames breaks that assumption immediately. The model runs on Replicate's GPUs, not on my server, and a long GIF can easily take a minute or more to process. If you try to hold an HTTP request open that long, you are asking for trouble. Load balancers cut idle connections. Browsers assume the network failed and retry. Users stare at a frozen spinner and assume the app is broken.
I avoided this entirely by decoupling the submission from the result. When a user uploads a GIF and a face image, my Next.js backend starts a prediction on Replicate and instantly returns a job ID. The user gets immediate confirmation. The actual result arrives later through a webhook once the GPU work finishes. This pattern is not exotic, but for long-running media jobs it is absolutely essential. It turns an unpredictable wait into a confident handshake: the job is accepted, and you will be notified when it is done.
Building a State Machine You Can Trust
Once you go asynchronous, you need visibility. Users will refresh the page. They will close the tab and reopen it. They will copy the link and send it to a coworker who checks it two hours later. Without a durable record of what happened, you have chaos.
I used Supabase as the single source of truth. Every upload creates a row with a unique job ID, and that row moves through specific states: queued, processing, succeeded, failed, or expired. When the user first hits submit, the row is queued. The moment Replicate accepts the prediction, it shifts to processing. The webhook pushes it to succeeded or failed. I added expired for jobs that sit too long without a callback, so the system does not chase ghosts indefinitely.
The frontend, built in TypeScript, polls Supabase on a short interval and renders whatever the current state says. This polling looks primitive, but it solves the refresh problem completely. A user can close their laptop, open it tomorrow, and see exactly where things stand because the database—not the browser memory—owns the progress. Supabase also tracks credits, so the accounting stays tied to the same job record that tracks the state. Everything lives in one place.
Handling Files Without Crushing the Browser
GIFs are heavier than people think. A file that is perfectly sized for the AI pipeline might still weigh several megabytes. Rendering that as a looping preview inside the browser would wreck performance, especially on lower-end devices. I needed two entirely separate file pipelines: one for the AI, and one for the user interface.
For previews, I convert large GIFs to animated WebP using FFmpeg compiled to WebAssembly. This runs entirely client-side in the browser. The result is a lightweight preview that keeps the interface snappy without touching the original file. The GIF sent to Replicate stays untouched. That separation matters. You do not want compression artifacts from a preview sneaking into your training data or your final render, and you do not want the user interface stalling on a multi-megabyte blob while the AI is still thinking.
FFmpeg WASM also handles other GIF tasks before the upload ever leaves the browser. I use it to parse frame counts, validate dimensions, and catch corrupted files early. Catching a problem before it consumes GPU credits saves both money and user patience.
Transformer une démo en un logiciel de confiance
Il y a ici une leçon plus large qui s'applique à presque tous les outils d'IA générative du marché. Le modèle lui-même ne représente peut-être que trente pour cent du travail. Les soixante-dix pour cent restants constituent cette plomberie peu glamour dont personne ne parle sur Twitter : récupération d'état, signatures de webhooks, conversion de fichiers, suivi des crédits et gestion élégante des erreurs.
Ma stack est simple et délibérée. Next.js et TypeScript gèrent l'interface et les routes API. Replicate fait tourner les modèles. Supabase gère les états, les données et les crédits. FFmpeg WASM s'occupe du traitement média côté client. Le WebP animé permet de garder une interface rapide. Chaque élément a une seule mission, et ils se connectent via des transitions d'état explicites plutôt que par des requêtes fragiles et de longue durée.
Lorsqu'un utilisateur échange des crédits contre un face swap, il attend de la fiabilité, pas un article de recherche. Si une prédiction échoue, le système doit le savoir et l'indiquer. Si l'utilisateur doit attendre, il doit disposer d'un aperçu léger à consulter et d'un statut qui persiste même après le redémarrage du navigateur. Ces détails sont invisibles lorsqu'ils fonctionnent, et fatals lorsqu'ils ne fonctionnent pas.
Le face swap en lui-même est un tour de magie élégant. Mais l'outil ne semble réel que parce qu'un utilisateur peut uploader, partir, puis revenir sans perdre sa progression. C'est ce qui transforme un simple appel API en un logiciel en lequel les gens ont réellement confiance.
