Turn your face into a super-hero with NextJS, Replicate, and Trigger.dev
CTO, Trigger.dev
TL;DR
This tutorial is super fun! You'll learn how to build a web application that allows users to generate AI images of themselves based on the prompt provided.
Before we start, head over to:
LINK: Generate a new avatar and post it in the comments! (To find good prompts check https://lexica.art)
In this tutorial, you will learn the following:
- Upload images seamlessly in Next.js,
- Generate stunning AI images with Replicate, and swap their faces with your face!
- Send emails via Resend in Trigger.dev.
Your background job management for NextJS
Trigger.dev is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!
If you can spend 10 seconds giving us a star, I would be super grateful 💖 https://github.com/triggerdotdev/trigger.dev
Set up the Wizard 🧙♂️
The application consists of two pages: the Home page that accepts users' email, image, gender, and a specific prompt if necessary, and the Success page that informs users that the image is being generated and will be sent to their email once it's ready.
The best part? All these tasks are handled seamlessly by Trigger.dev.🤩
Run the code snippet below within your terminal to create a Typescript Next.js project.
_10npx create-next-app image-generator
Main page 🏠
Update the index.tsx
file to display a form that enables users to enter their email address and gender, an optional custom prompt, and upload a picture of themselves.
_95"use client";_95import Head from "next/head";_95import { FormEvent, useState } from "react";_95import { useRouter } from "next/navigation";_95_95export default function Home() {_95 const [selectedFile, setSelectedFile] = useState<File>();_95 const [userPrompt, setUserPrompt] = useState<string>("");_95 const [email, setEmail] = useState<string>("");_95 const [gender, setGender] = useState<string>("");_95 const router = useRouter();_95_95 const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {_95 e.preventDefault();_95 console.log({ selectedFile, userPrompt, email, gender });_95 router.push("/success");_95 };_95_95 return (_95 <main className="flex min-h-screen w-full flex-col items-center justify-center px-4 md:p-8">_95 <Head>_95 <title>Avatar Generator</title>_95 </Head>_95 <header className="mb-8 flex w-full flex-col items-center justify-center">_95 <h1 className="text-4xl font-bold">Avatar Generator</h1>_95 <p className="opacity-60">_95 Upload a picture of yourself and generate your avatar_95 </p>_95 </header>_95_95 <form_95 method="POST"_95 className="flex w-full flex-col md:w-[60%]"_95 onSubmit={(e) => handleSubmit(e)}_95 >_95 <label htmlFor="email">Email Address</label>_95 <input_95 type="email"_95 required_95 className="mb-3 border-[1px] px-4 py-2"_95 value={email}_95 onChange={(e) => setEmail(e.target.value)}_95 />_95_95 <label htmlFor="gender">Gender</label>_95 <select_95 className="mb-4 rounded border-[1px] px-4 py-3"_95 name="gender"_95 id="gender"_95 value={gender}_95 onChange={(e) => setGender(e.target.value)}_95 required_95 >_95 <option value="">Select</option>_95 <option value="male">Male</option>_95 <option value="female">Female</option>_95 </select>_95_95 <label htmlFor="image">Upload your picture</label>_95 <input_95 name="image"_95 type="file"_95 className="mb-3 rounded-md border-[1px] px-4 py-2"_95 accept=".png, .jpg, .jpeg"_95 required_95 onChange={({ target }) => {_95 if (target.files) {_95 const file = target.files[0];_95 setSelectedFile(file);_95 }_95 }}_95 />_95 <label htmlFor="prompt">_95 Add custom prompt for your avatar_95 <span className="opacity-60">(optional)</span>_95 </label>_95 <textarea_95 rows={4}_95 className="w-full border-[1px] p-3"_95 name="prompt"_95 id="prompt"_95 value={userPrompt}_95 placeholder="Copy image prompts from https://lexica.art"_95 onChange={(e) => setUserPrompt(e.target.value)}_95 />_95 <button_95 type="submit"_95 className="mt-5 rounded bg-blue-500 px-6 py-4 text-lg text-white hover:bg-blue-700"_95 >_95 Generate Avatar_95 </button>_95 </form>_95 </main>_95 );_95}
The code snippet above displays the required input fields and a button that logs all the user inputs to the console.
The Success page ✅
After users submit the form on the home page, they are automatically redirected to the Success page. This page confirms the receipt of their request and informs them that they will receive the AI-generated image via email as soon as it is ready.
Create a success.tsx
file and copy the code snippet into the file.
_22import Link from "next/link";_22import Head from "next/head";_22_22export default function Success() {_22 return (_22 <div className="flex min-h-screen w-full flex-col items-center justify-center">_22 <Head>_22 <title>Success | Avatar Generator</title>_22 </Head>_22 <h2 className="mb-2 text-3xl font-bold">Thank you! 🌟</h2>_22 <p className="mb-4 text-center">_22 Your image will be delivered to your email, once it is ready! 💫_22 </p>_22 <Link_22 href="/"_22 className="rounded bg-blue-500 px-4 py-3 text-white hover:bg-blue-600"_22 >_22 Generate another_22 </Link>_22 </div>_22 );_22}
Uploading images to a Next.js server
On the form, you need to allow users to upload images to the Next.js server and swap the face on the picture with an AI image.
To do this, I'll walk you through how to upload files in Next.js using Formidable - a Node.js module for parsing form data, especially file uploads.
Install Formidable to your Next.js project:
_10npm install formidable @types/formidable
Before we proceed, update the handleSubmit
function to send the user's data to an endpoint on the server.
_20const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {_20 e.preventDefault();_20 try {_20 if (!selectedFile) return;_20 const formData = new FormData();_20 formData.append("image", selectedFile);_20 formData.append("gender", gender);_20 formData.append("email", email);_20 formData.append("userPrompt", userPrompt);_20 //👇🏻 post data to server's endpoint_20 await fetch("/api/generate", {_20 method: "POST",_20 body: formData,_20 });_20 //👇🏻 redirect to Success page_20 router.push("/success");_20 } catch (err) {_20 console.error({ err });_20 }_20};
Create the /api/generate
endpoint on the server and disable the default Next.js body-parser, as shown below.
_12import type { NextApiRequest, NextApiResponse } from "next";_12_12//👇🏻 disables the default Next.js body parser_12export const config = {_12 api: {_12 bodyParser: false,_12 },_12};_12_12export default function handler(req: NextApiRequest, res: NextApiResponse) {_12 res.status(200).json({ message: "Hello world" });_12}
Add this code snippet directly below the config object to convert the image to base64 format.
_38//👇🏻 creates a writable stream that stores a chunk of data_38const fileConsumer = (acc: any) => {_38 const writable = new Writable({_38 write: (chunk, _enc, next) => {_38 acc.push(chunk);_38 next();_38 },_38 });_38_38 return writable;_38};_38_38const readFile = (req: NextApiRequest, saveLocally?: boolean) => {_38 // @ts-ignore_38 const chunks: any[] = [];_38 //👇🏻 creates a formidable instance that uses the fileConsumer function_38 const form = formidable({_38 keepExtensions: true,_38 fileWriteStreamHandler: () => fileConsumer(chunks),_38 });_38_38 return new Promise((resolve, reject) => {_38 form.parse(req, (err, fields: any, files: any) => {_38 //👇🏻 converts the image to base64_38 const image = Buffer.concat(chunks).toString("base64");_38 //👇🏻 logs the result_38 console.log({_38 image,_38 email: fields.email[0],_38 gender: fields.gender[0],_38 userPrompt: fields.userPrompt[0],_38 });_38_38 if (err) reject(err);_38 resolve({ fields, files });_38 });_38 });_38};
- From the code snippet above,
- The
fileConsumer
function creates a writable stream in Node.js for storing the chunk of data to be written. - The
readFile
function creates a Formidable instance that uses thefileConsumer
function as the customfileWriteStreamHandler
. The handler ensures that the image data is stored within thechunks
array. - It also returns the user’s image (base64 format), email, gender, and the custom prompt.
- The
Finally, modify the handler
function to execute readFile
function.
_10export default async function handler(_10 req: NextApiRequest,_10 res: NextApiResponse_10) {_10 await readFile(req, true);_10_10 res.status(200).json({ message: "Processing!" });_10}
Congratulations!🎉 You've learnt how to upload images in base64 format in Next.js. In the upcoming section, I'll walk you through generating images with AI models on Replicate and sending them to your emails via Resend and Trigger.dev.
Managing long-running jobs with Trigger.dev 🏄♂️
Trigger.dev is an open-source library that offers three communication methods: webhook, schedule, and event. Schedule is ideal for recurring tasks, events activate a job upon sending a payload, and webhooks trigger real-time jobs when specific events occur.
Here, you'll learn how to create and trigger jobs within your Next.js project.
How to add Trigger.dev to a Next.js application
Sign up for a Trigger.dev account. Once registered, create an organisation and choose a project name for your jobs.
Select Next.js as your framework and follow the process for adding Trigger.dev to an existing Next.js project.
Otherwise, click Environments & API Keys
on the sidebar menu of your project dashboard.
Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.
_10npx @trigger.dev/cli@latest init
Start your Next.js project.
_10npm run dev
In another terminal, run the following code snippet to establish a tunnel between Trigger.dev and your Next.js project.
_10npx @trigger.dev/cli@latest dev
Rename the jobs/examples.ts
file to jobs/functions.ts
. This is where all the jobs are processed.
Next, install Zod - a TypeScript-first type-checking and validation library that enables you to verify the data type of a job's payload.
_10npm install zod
In Trigger.dev, jobs can be triggered using the client.sendEvent()
method. Therefore, modify the readFile
function to trigger the newly created job and send the user's data as a payload to the job.
_27const readFile = (req: NextApiRequest, saveLocally?: boolean) => {_27 // @ts-ignore_27 const chunks: any[] = [];_27 const form = formidable({_27 keepExtensions: true,_27 fileWriteStreamHandler: () => fileConsumer(chunks),_27 });_27_27 return new Promise((resolve, reject) => {_27 form.parse(req, (err, fields: any, files: any) => {_27 const image = Buffer.concat(chunks).toString("base64");_27 //👇🏻 sends the payload to the job_27 client.sendEvent({_27 name: "generate.avatar",_27 payload: {_27 image,_27 email: fields.email[0],_27 gender: fields.gender[0],_27 userPrompt: fields.userPrompt[0],_27 },_27 });_27_27 if (err) reject(err);_27 resolve({ fields, files });_27 });_27 });_27};
Creating the faces with Replicate
Replicate is a web platform that allows users to run models at scale in the cloud. Here, you'll learn how to generate and swap image faces using AI models on Replicate.
Follow the steps below to accomplish this:
Visit the Replicate home page, click the Sign in
button to log in via your GitHub account, and generate your API token.
Copy your API token, the Stability AI model URI - for generating images, and the Faceswap AI model URI into the .env.local
file.
_10REPLICATE_API_TOKEN=<your_API_token>_10STABILITY_AI_URI=stability-ai/sdxl:c221b2b8ef527988fb59bf24a8b97c4561f1c671f73bd389f866bfb27c061316_10FACESWAP_API_URI=lucataco/faceswap:9a4298548422074c3f57258c5d544497314ae4112df80d116f0d2109e843d20d
Next, go to the Trigger.dev integration page and install the Replicate package.
_10npm install @trigger.dev/replicate@latest
Import and initialize the Replicate within the jobs/functions.ts
file.
_10import { Replicate } from "@trigger.dev/replicate";_10_10const replicate = new Replicate({_10 id: "replicate",_10 apiKey: process.env["YOUR_REPLICATE_API_KEY"],_10});
Update the jobs/functions.ts
file to generate an image using the prompt provided by the user or a default prompt.
_36import { z } from "zod";_36_36client.defineJob({_36 id: "generate-avatar",_36 name: "Generate Avatar",_36 //👇🏻 integrates Replicate_36 integrations: { replicate },_36 version: "0.0.1",_36 trigger: eventTrigger({_36 name: "generate.avatar",_36 schema: z.object({_36 image: z.string(),_36 email: z.string(),_36 gender: z.string(),_36 userPrompt: z.string().nullable(),_36 }),_36 }),_36 run: async (payload, io, ctx) => {_36 const { email, image, gender, userPrompt } = payload;_36_36 await io.logger.info("Avatar generation started!", { image });_36_36 const imageGenerated = await io.replicate.run("create-model", {_36 identifier: process.env.STABILITY_AI_URI,_36 input: {_36 prompt: `${_36 userPrompt_36 ? userPrompt_36 : `A professional ${gender} portrait suitable for a social media avatar. Please ensure the image is appropriate for all audiences.`_36 }`,_36 },_36 });_36_36 await io.logger.info(JSON.stringify(imageGenerated));_36 },_36});
The code snippet above generates an AI image based on the prompt and logs it on your Trigger.dev dashboard.
Remember, you need to generate an AI image and swap the user's face with the AI-generated image. Next, let's swap faces on the images.
Copy this function to the top of the jobs/functions.ts
file. The code snippet converts the image generated into its data URI, which is the accepted format for the face swap AI model.
_10//👇🏻 converts an image URL to a data URI_10const urlToBase64 = async (image: string) => {_10 const response = await fetch(image);_10 const arrayBuffer = await response.arrayBuffer();_10 const buffer = Buffer.from(arrayBuffer);_10 const base64String = buffer.toString("base64");_10 const mimeType = "image/png";_10 const dataURI = `data:${mimeType};base64,${base64String}`;_10 return dataURI;_10};
Update the Trigger.dev job to send both the user's image and generated image as parameters to the faceswap model.
_41client.defineJob({_41 id: "generate-avatar",_41 name: "Generate Avatar",_41 version: "0.0.1",_41 trigger: eventTrigger({_41 name: "generate.avatar",_41 schema: z.object({_41 image: z.string(),_41 email: z.string(),_41 gender: z.string(),_41 userPrompt: z.string().nullable(),_41 }),_41 }),_41 run: async (payload, io, ctx) => {_41 const { email, image, gender, userPrompt } = payload;_41_41 await io.logger.info("Avatar generation started!", { image });_41_41 const imageGenerated = await io.replicate.run("create-model", {_41 identifier: process.env.STABILITY_AI_URL,_41 input: {_41 prompt: `${_41 userPrompt_41 ? userPrompt_41 : `A professional ${gender} portrait suitable for a social media avatar. Please ensure the image is appropriate for all audiences.`_41 }`,_41 },_41 });_41_41 const swappedImage = await io.replicate.run("create-image", {_41 identifier: process.env.FACESWAP_AI_URL_41 input: {_41 // @ts-ignore_41 target_image: await urlToBase64(imageGenerated.output),_41 swap_image: "data:image/png;base64," + image,_41 },_41 });_41 await io.logger.info("Swapped image: ", {swappedImage.output});_41 await io.logger.info("✨ Congratulations, your image has been swapped! ✨");_41 },_41});
The code snippet above gets the data URI for the AI-generated and user's image and sends both images to the AI model, which returns the URL of the swapped image.
Congratulations!🎉 You've learnt how to generate AI images of yourself with Replicate. In the upcoming section, you'll learn how to send these images via email with Resend.
PS: You can also get custom prompts for your images from Lexica.
Sending emails with Resend via Trigger.dev
Resend is an email API that enables you to send texts, attachments, and email templates easily. With Resend, you can build, test, and deliver transactional emails at scale.
Visit the Signup page, create an account and an API Key and save it into the .env.local
file.
_10RESEND_API_KEY=<place_your_API_key>
Install the Trigger.dev Resend integration package to your Next.js project.
_10npm install @trigger.dev/resend
Import Resend into the /jobs/functions.ts
file as shown below.
_10import { Resend } from "@trigger.dev/resend";_10_10const resend = new Resend({_10 id: "resend",_10 apiKey: process.env.RESEND_API_KEY!,_10});
Finally, integrate Resend to the job and send the swapped imaged to user's email.
_33client.defineJob({_33 id: "generate-avatar",_33 name: "Generate Avatar",_33 // ---👇🏻 integrates Resend ---_33 integrations: { resend },_33 version: "0.0.1",_33 trigger: eventTrigger({_33 name: "generate.avatar",_33 schema: z.object({_33 image: z.object({ filepath: z.string() }),_33 email: z.string(),_33 gender: z.string(),_33 userPrompt: z.string().nullable(),_33 }),_33 }),_33 run: async (payload, io, ctx) => {_33 const { email, image, gender, userPrompt } = payload;_33 //👇🏻 -- After swapping the images, add the code snipped below --_33 await io.logger.info("Swapped image: ", { swappedImage });_33_33 //👇🏻 -- Sends the swapped image to the user--_33 await io.resend.sendEmail("send-email", {_33 from: "[email protected]",_33 to: [email],_33 subject: "Your avatar is ready! 🌟🤩",_33 text: `Hi! \n View and download your avatar here - ${swappedImage.output}`,_33 });_33_33 await io.logger.info(_33 "✨ Congratulations, the image has been delivered! ✨"_33 );_33 },_33});
Congratulations!🎉 You've completed the project for this tutorial.
Conclusion
So far, you've learnt how to
- upload images to a local directory in Next.js,
- create and manage long-running jobs with Trigger.dev,
- generate AI images using various models on Replicate, and
- send emails via Resend in Trigger.dev.
As an open-source developer, you're invited to join our community to contribute and engage with maintainers. Don't hesitate to visit our GitHub repository to contribute and create issues related to Trigger.dev.
The source for this tutorial is available here: https://github.com/triggerdotdev/blog/tree/main/avatar-generator
Thank you for reading!