I built a browser-based gif face swap tool that accepts an animated GIF plus a face image and returns a new GIF.
The difficult part wasn’t starting the AI model. It was handling a long-running media job without blocking the request or losing its state.
Stack
- Next.js and TypeScript
- Replicate for AI inference
- Supabase for job and credit records
- FFmpeg WASM for browser-side GIF utilities
- Animated WebP for lightweight previews
Async generation
Instead of waiting for inference inside one HTTP request, I create a Replicate prediction and receive the result through a webhook:
export async function createPrediction(
targetGifUrl: string,
faceImageUrl: string,
) {
const replicate = createReplicateClient();
const webhook = replicateWebhookUrl();
return replicate.predictions.create({
version: REPLICATE_MODEL_VERSION,
input: replicateInput(targetGifUrl, faceImageUrl),
webhook,
webhook_events_filter: webhook ? ["completed"] : undefined,
});
}
Each Supabase job stores its prediction ID and one of five states:
type JobStatus =
| "queued"
| "processing"
| "succeeded"
| "failed"
| "expired";
The frontend polls the job endpoint, while the webhook updates the final result. This also lets users refresh the page without losing an active generation.
Keeping the site fast
Original GIFs stay available for processing, but template previews are converted to animated WebP:
pnpm asset:gif-to-webp input.gif preview.webp
This keeps the UI lighter without modifying the source animation sent to the face-swap pipeline.
The main lesson: with AI media tools, inference is only half the work. Job state, webhooks, file handling, and failure recovery are what make the demo feel like a real product.
Top comments (0)