DEV Community

Cover image for πŸš€ Turn your face into a super-hero with NextJS, Replicate, and Trigger.dev πŸ¦ΈπŸ»β€β™‚οΈ
Eric Allam for Trigger.dev

Posted on • Originally published at trigger.dev

πŸš€ Turn your face into a super-hero with NextJS, Replicate, and 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:

https://avatar-generator-psi.vercel.app
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.

MoveHead


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

GiveStar


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.🀩

overview

Run the code snippet below within your terminal to create a Typescript Next.js project.

npx create-next-app image-generator
Enter fullscreen mode Exit fullscreen mode

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.

"use client";
import Head from "next/head";
import { FormEvent, useState } from "react";
import { useRouter } from "next/navigation";

export default function Home() {
    const [selectedFile, setSelectedFile] = useState<File>();
    const [userPrompt, setUserPrompt] = useState<string>("");
    const [email, setEmail] = useState<string>("");
    const [gender, setGender] = useState<string>("");
    const router = useRouter();

    const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({ selectedFile, userPrompt, email, gender });
        router.push("/success");
    };

    return (
        <main className='flex items-center md:p-8 px-4 w-full justify-center min-h-screen flex-col'>
            <Head>
                <title>Avatar Generator</title>
            </Head>
            <header className='mb-8 w-full flex flex-col items-center justify-center'>
                <h1 className='font-bold text-4xl'>Avatar Generator</h1>
                <p className='opacity-60'>
                    Upload a picture of yourself and generate your avatar
                </p>
            </header>

            <form
                method='POST'
                className='flex flex-col md:w-[60%] w-full'
                onSubmit={(e) => handleSubmit(e)}
            >
                <label htmlFor='email'>Email Address</label>
                <input
                    type='email'
                    required
                    className='px-4 py-2 border-[1px] mb-3'
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                />

                <label htmlFor='gender'>Gender</label>
                <select
                    className='border-[1px] py-3 px-4 mb-4 rounded'
                    name='gender'
                    id='gender'
                    value={gender}
                    onChange={(e) => setGender(e.target.value)}
                    required
                >
                    <option value=''>Select</option>
                    <option value='male'>Male</option>
                    <option value='female'>Female</option>
                </select>

                <label htmlFor='image'>Upload your picture</label>
                <input
                    name='image'
                    type='file'
                    className='border-[1px] py-2 px-4 rounded-md mb-3'
                    accept='.png, .jpg, .jpeg'
                    required
                    onChange={({ target }) => {
                        if (target.files) {
                            const file = target.files[0];
                            setSelectedFile(file);
                        }
                    }}
                />
                <label htmlFor='prompt'>
                    Add custom prompt for your avatar
                    <span className='opacity-60'>(optional)</span>
                </label>
                <textarea
                    rows={4}
                    className='w-full border-[1px] p-3'
                    name='prompt'
                    id='prompt'
                    value={userPrompt}
                    placeholder='Copy image prompts from https://lexica.art'
                    onChange={(e) => setUserPrompt(e.target.value)}
                />
                <button
                    type='submit'
                    className='px-6 py-4 mt-5 bg-blue-500 text-lg hover:bg-blue-700 rounded text-white'
                >
                    Generate Avatar
                </button>
            </form>
        </main>
    );
}
Enter fullscreen mode Exit fullscreen mode

The code snippet above displays the required input fields and a button that logs all the user inputs to the console.

CodeSnip

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.

import Link from "next/link";
import Head from "next/head";

export default function Success() {
    return (
        <div className='min-h-screen w-full flex flex-col items-center justify-center'>
            <Head>
                <title>Success | Avatar Generator</title>
            </Head>
            <h2 className='font-bold text-3xl mb-2'>Thank you! 🌟</h2>
            <p className='mb-4 text-center'>
                Your image will be delivered to your email, once it is ready! πŸ’«
            </p>
            <Link
                href='/'
                className='bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600'
            >
                Generate another
            </Link>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

SuccessPage

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.

BeforeAfter

Install Formidable to your Next.js project:

npm install formidable @types/formidable
Enter fullscreen mode Exit fullscreen mode

Before we proceed, update the handleSubmit function to send the user's data to an endpoint on the server.

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
        if (!selectedFile) return;
        const formData = new FormData();
        formData.append("image", selectedFile);
        formData.append("gender", gender);
        formData.append("email", email);
        formData.append("userPrompt", userPrompt);
        //πŸ‘‡πŸ» post data to server's endpoint
        await fetch("/api/generate", {
            method: "POST",
            body: formData,
        });
        //πŸ‘‡πŸ» redirect to Success page
        router.push("/success");
    } catch (err) {
        console.error({ err });
    }
};
Enter fullscreen mode Exit fullscreen mode

Create the /api/generate endpoint on the server and disable the default Next.js body-parser, as shown below.

import type { NextApiRequest, NextApiResponse } from "next";

//πŸ‘‡πŸ» disables the default Next.js body parser
export const config = {
    api: {
        bodyParser: false,
    },
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
    res.status(200).json({ message: "Hello world" });
}
Enter fullscreen mode Exit fullscreen mode

Add this code snippet directly below the config object to convert the image to base64 format.

//πŸ‘‡πŸ» creates a writable stream that stores a chunk of data
const fileConsumer = (acc: any) => {
    const writable = new Writable({
        write: (chunk, _enc, next) => {
            acc.push(chunk);
            next();
        },
    });

    return writable;
};

const readFile = (req: NextApiRequest, saveLocally?: boolean) => {
    // @ts-ignore
    const chunks: any[] = [];
    //πŸ‘‡πŸ» creates a formidable instance that uses the fileConsumer function
    const form = formidable({
        keepExtensions: true,
        fileWriteStreamHandler: () => fileConsumer(chunks),
    });

    return new Promise((resolve, reject) => {
        form.parse(req, (err, fields: any, files: any) => {
            //πŸ‘‡πŸ» converts the image to base64
            const image = Buffer.concat(chunks).toString("base64");
            //πŸ‘‡πŸ» logs the result
            console.log({
                    image,
                    email: fields.email[0],
                    gender: fields.gender[0],
                    userPrompt: fields.userPrompt[0],
            });

            if (err) reject(err);
            resolve({ fields, files });
        });
    });
};
Enter fullscreen mode Exit fullscreen mode
  • 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 the fileConsumer function as the custom fileWriteStreamHandler. The handler ensures that the image data is stored within the chunks array.
    • It also returns the user’s image (base64 format), email, gender, and the custom prompt.

Finally, modify the handler function to execute readFile function.

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  await readFile(req, true);

  res.status(200).json({ message: "Processing!" });
}
Enter fullscreen mode Exit fullscreen mode

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.

CreateOrg

Select Next.js as your framework and follow the process for adding Trigger.dev to an existing Next.js project.

Next

Otherwise, clickΒ Environments & API KeysΒ on the sidebar menu of your project dashboard.

Copy

Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.

npx @trigger.dev/cli@latest init
Enter fullscreen mode Exit fullscreen mode

Start your Next.js project.

npm run dev
Enter fullscreen mode Exit fullscreen mode

In another terminal, run the following code snippet to establish a tunnel between Trigger.dev and your Next.js project.

npx @trigger.dev/cli@latest dev
Enter fullscreen mode Exit fullscreen mode

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.

npm install zod
Enter fullscreen mode Exit fullscreen mode

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.

const readFile = (req: NextApiRequest, saveLocally?: boolean) => {
  // @ts-ignore
  const chunks: any[] = [];
  const form = formidable({
    keepExtensions: true,
    fileWriteStreamHandler: () => fileConsumer(chunks),
  });

  return new Promise((resolve, reject) => {
    form.parse(req,  (err, fields: any, files: any) => {
      const image = Buffer.concat(chunks).toString("base64");
      //πŸ‘‡πŸ» sends the payload to the job
      client.sendEvent({
        name: "generate.avatar",
        payload: {
          image,
          email: fields.email[0],
          gender: fields.gender[0],
          userPrompt: fields.userPrompt[0],
        },
      });

      if (err) reject(err);
      resolve({ fields, files });
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

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.

CopyToken

Copy your API token, theΒ Stability AI model URI - for generating images, and theΒ Faceswap AI model URIΒ into the .env.local file.

REPLICATE_API_TOKEN=<your_API_token>
STABILITY_AI_URI=stability-ai/sdxl:c221b2b8ef527988fb59bf24a8b97c4561f1c671f73bd389f866bfb27c061316
FACESWAP_API_URI=lucataco/faceswap:9a4298548422074c3f57258c5d544497314ae4112df80d116f0d2109e843d20d
Enter fullscreen mode Exit fullscreen mode

Next, go to theΒ Trigger.dev integration page and install the Replicate package.

npm install @trigger.dev/replicate@latest
Enter fullscreen mode Exit fullscreen mode

Import and initialize the Replicate within the jobs/functions.ts file.

import { Replicate } from "@trigger.dev/replicate";

const replicate = new Replicate({
  id: "replicate",
  apiKey: process.env["YOUR_REPLICATE_API_KEY"],
});
Enter fullscreen mode Exit fullscreen mode

Update the jobs/functions.ts file to generate an image using the prompt provided by the user or a default prompt.

import { z } from "zod";

client.defineJob({
id: "generate-avatar",
  name: "Generate Avatar",
//πŸ‘‡πŸ» integrates Replicate
  integrations: { replicate },
  version: "0.0.1",
  trigger: eventTrigger({
    name: "generate.avatar",
    schema: z.object({
      image: z.string(),
      email: z.string(),
      gender: z.string(),
      userPrompt: z.string().nullable(),
    }),
  }),
    run: async (payload, io, ctx) => {
    const { email, image, gender, userPrompt } = payload;

    await io.logger.info("Avatar generation started!", { image });

    const imageGenerated = await io.replicate.run("create-model", {
      identifier: process.env.STABILITY_AI_URI,
      input: {
        prompt: `${
          userPrompt
            ? userPrompt
            : `A professional ${gender} portrait suitable for a social media avatar. Please ensure the image is appropriate for all audiences.`
        }`,
      },
    });

        await io.logger.info(JSON.stringify(imageGenerated));
    },
});
Enter fullscreen mode Exit fullscreen mode

The code snippet above generates an AI image based on the prompt and logs it on your Trigger.dev dashboard.

Generate

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.

//πŸ‘‡πŸ» converts an image URL to a data URI
const urlToBase64 = async (image: string) => {
    const response = await fetch(image);
    const arrayBuffer = await response.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    const base64String = buffer.toString("base64");
    const mimeType = "image/png";
    const dataURI = `data:${mimeType};base64,${base64String}`;
    return dataURI;
};
Enter fullscreen mode Exit fullscreen mode

Update the Trigger.dev job to send both the user's image and generated image as parameters to the faceswap model.

client.defineJob({
    id: "generate-avatar",
    name: "Generate Avatar",
    version: "0.0.1",
    trigger: eventTrigger({
        name: "generate.avatar",
        schema: z.object({
            image: z.string(),
            email: z.string(),
            gender: z.string(),
            userPrompt: z.string().nullable(),
        }),
    }),
    run: async (payload, io, ctx) => {
        const { email, image, gender, userPrompt } = payload;

    await io.logger.info("Avatar generation started!", { image });

    const imageGenerated = await io.replicate.run("create-model", {
      identifier: process.env.STABILITY_AI_URL,
      input: {
        prompt: `${
          userPrompt
            ? userPrompt
            : `A professional ${gender} portrait suitable for a social media avatar. Please ensure the image is appropriate for all audiences.`
        }`,
      },
    });

    const swappedImage = await io.replicate.run("create-image", {
      identifier: process.env.FACESWAP_AI_URL
      input: {
        // @ts-ignore
        target_image: await urlToBase64(imageGenerated.output),
        swap_image: "data:image/png;base64," + image,
      },
    });
        await io.logger.info("Swapped image: ", {swappedImage.output});
        await io.logger.info("✨ Congratulations, your image has been swapped! ✨");
    },
});
Enter fullscreen mode Exit fullscreen mode

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.

Generate


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.

RESEND_API_KEY=<place_your_API_key>
Enter fullscreen mode Exit fullscreen mode

Key

Install the Trigger.dev Resend integration package to your Next.js project.

npm install @trigger.dev/resend
Enter fullscreen mode Exit fullscreen mode

Import Resend into the /jobs/functions.ts file as shown below.

import { Resend } from "@trigger.dev/resend";

const resend = new Resend({
    id: "resend",
    apiKey: process.env.RESEND_API_KEY!,
});
Enter fullscreen mode Exit fullscreen mode

Finally, integrate Resend to the job and send the swapped imaged to user's email.

client.defineJob({
    id: "generate-avatar",
    name: "Generate Avatar",
    // ---πŸ‘‡πŸ» integrates Resend ---
    integrations: { resend },
    version: "0.0.1",
    trigger: eventTrigger({
        name: "generate.avatar",
        schema: z.object({
            image: z.object({ filepath: z.string() }),
            email: z.string(),
            gender: z.string(),
            userPrompt: z.string().nullable(),
        }),
    }),
    run: async (payload, io, ctx) => {
        const { email, image, gender, userPrompt } = payload;
        //πŸ‘‡πŸ» -- After swapping the images, add the code snipped below --
        await io.logger.info("Swapped image: ", {swappedImage});

        //πŸ‘‡πŸ» -- Sends the swapped image to the user--
    await io.resend.sendEmail("send-email", {
      from: "onboarding@resend.dev",
      to: [email],
      subject: "Your avatar is ready! 🌟🀩",
      text: `Hi! \n View and download your avatar here - ${swappedImage.output}`,
    });

        await io.logger.info(
            "✨ Congratulations, the image has been delivered! ✨"
        );
    },
});
Enter fullscreen mode Exit fullscreen mode

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!


Don't forget to generate a new avatar and post it in the comments!
https://avatar-generator-psi.vercel.app
(To find good prompts, check https://lexica.art)

Top comments (10)

Collapse
 
maverickdotdev profile image
Eric Allam

I couldn't help myself:

Image description

The prompt:

male priest ith beard Medieval in church red clothes with bible art by artgerm and greg rutkowski and alphonse mucha and loish and wlop, artstation, illustration
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nevodavid profile image
Nevo David

This is awesome, haha!

Image description

Image description

Image description

Collapse
 
samejr profile image
James Ritchie

I've never wanted to be standing in the rain in a trenchcoat in the 40s more:

Image description

Image description

Prompt:

a sad 45 years old man, with a scar on the face. posing 7/11, wearing a leather trench, behing him a steampunk victorian city, while it rains. greyscale style, realistic, pencil sketch
Enter fullscreen mode Exit fullscreen mode
Collapse
 
timonwa profile image
Timonwa Akintokun

I think I am going to get addicted to this. πŸ₯²
Prompt: An emo-goth superhero wearing all-black with colourful dreadlocks. the background is earthy, dark and mysterious. but her face is young, innocent and hopeful.

Image description

Prompt: something about "a goth superhero in all black with dreads whose super powers is creativity." I forgot to save the prompt.

Image description

Collapse
 
maverickdotdev profile image
Eric Allam

These are amazing 🀩

Collapse
 
timonwa profile image
Timonwa Akintokun

Thank you Eric. πŸ₯°

Collapse
 
mattaitken profile image
Matt Aitken

I've been going to the gym A LOT recently
Image description

prompt:

Draconic traits to human man with red hair, draconics claws, scaled arms, fantasy ambience, anime style
Enter fullscreen mode Exit fullscreen mode
Collapse
 
michaeltharrington profile image
Michael Tharrington

Haha! This was a lotta fun. πŸ˜€

A version of me that looks like a Jedi riding a black panther-like horse

Collapse
 
maverickdotdev profile image
Eric Allam

Woah, this one is really cool!

Collapse
 
srbhr profile image
Saurabh Rai

Wow, really cool!!

American Hero

Image description