🚀 使用 NextJS、Replicate 和 Trigger.dev 将您的脸变成超级英雄🦸🏻‍♂️ TL;DR

2025-05-28

🚀 使用 NextJS、Replicate 和 Trigger.dev 将您的脸变成超级英雄🦸🏻‍♂️

TL;DR

TL;DR

本教程超级有趣!
你将学习如何构建一个 Web 应用程序,让用户根据提供的提示生成自己的 AI 图像。

在我们开始之前,请先访问:

https://avatar-generator-psi.vercel.app
生成一个新的头像并将其发布到评论区!
(要找到好的提示,请访问https://lexica.art

在本教程中,您将学习以下内容:

  • 在 Next.js 中无缝上传图像,
  • 使用 Replicate 生成令人惊叹的 AI 图像,并将其脸部与您的脸部交换!
  • 通过 Trigger.dev 中的“重新发送”发送电子邮件。

移动头


NextJS 的后台作业管理

Trigger.dev 是一个开源库,可让您使用 NextJS、Remix、Astro 等为您的应用程序创建和监控长时间运行的作业!

如果您能花 10 秒钟给我们一颗星,我将非常感激 💖
https://github.com/triggerdotdev/trigger.dev

赠星


设置向导🧙‍♂️

该应用程序由两页组成:主页,用于接受用户的电子邮件、图像、性别以及必要时的特定提示;成功页面,用于通知用户图像正在生成,一旦准备好就会发送到他们的电子邮件中。

最棒的是?所有这些任务都由 Trigger.dev 无缝处理。🤩

概述

在终端内运行下面的代码片段来创建一个 Typescript Next.js 项目。



npx create-next-app image-generator


Enter fullscreen mode Exit fullscreen mode

主页🏠

更新index.tsx文件以显示一个表单,使用户可以输入他们的电子邮件地址和性别、可选的自定义提示以及上传他们自己的照片。



"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

上面的代码片段显示了所需的输入字段和一个将所有用户输入记录到控制台的按钮。

CodeSnip

成功页面✅

用户在主页提交表单后,会自动跳转到“成功”页面。该页面会确认已收到用户的请求,并告知用户 AI 生成的图像一旦准备就绪,就会立即通过电子邮件收到。

创建一个success.tsx文件并将代码片段复制到该文件中。



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

成功页面

将图像上传到 Next.js 服务器

在表单上,​​您需要允许用户将图像上传到 Next.js 服务器,并将图片上的脸部与 AI 图像交换。

为此,我将引导您了解如何使用 Formidable  (一个用于解析表单数据(尤其是文件上传)的 Node.js 模块)在 Next.js 中上传文件。

之前之后

将 Formidable 安装到你的 Next.js 项目:



npm install formidable @types/formidable


Enter fullscreen mode Exit fullscreen mode

在我们继续之前,更新handleSubmit函数以将用户的数据发送到服务器上的端点。



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

/api/generate在服务器上创建端点并禁用默认的 Next.js body-parser,如下所示。



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

将此代码片段直接添加到配置对象下方,以将图像转换为 base64 格式。



//👇🏻 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
  • 从上面的代码片段来看,
    • fileConsumer函数在 Node.js 中创建一个可写流,用于存储要写入的数据块。
    • readFile函数创建一个 Formidable 实例,该fileConsumer实例使用该函数作为自定义函数fileWriteStreamHandler。处理程序确保图像数据存储在chunks数组中。
    • 它还返回用户的图像(base64 格式)、电子邮件、性别和自定义提示。

最后将该handler函数修改为执行readFile函数。



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

恭喜!🎉 您已经学会了如何在 Next.js 中上传 base64 格式的图片。在接下来的部分中,我将指导您使用 Replicate 上的 AI 模型生成图片 ,并通过 Resend 和 Trigger.dev 将它们发送到您的邮箱。


使用 Trigger.dev 管理长时间运行的作业

Trigger.dev 是一个开源库,提供三种通信方式:webhook、schedule 和 event。schedule 非常适合重复执行的任务,event 会在发送有效负载时激活作业,而 webhook 会在特定事件发生时触发实时作业。

在这里,您将学习如何在 Next.js 项目中创建和触发作业。

如何将 Trigger.dev 添加到 Next.js 应用程序

注册一个 Trigger.dev 帐户。注册后,创建一个组织并为您的工作选择一个项目名称。

创建组织

选择 Next.js 作为您的框架,并按照将 Trigger.dev 添加到现有 Next.js 项目的过程进行操作。

下一个

否则,请单击 Environments & API Keys 项目仪表板的侧边栏菜单。

复制

复制您的 DEV 服务器 API 密钥,并运行以下代码片段来安装 Trigger.dev。请仔细按照说明操作。



npx @trigger.dev/cli@latest init


Enter fullscreen mode Exit fullscreen mode

启动您的 Next.js 项目。



npm run dev


Enter fullscreen mode Exit fullscreen mode

在另一个终端中,运行以下代码片段以在 Trigger.dev 和 Next.js 项目之间建立隧道。



npx @trigger.dev/cli@latest dev


Enter fullscreen mode Exit fullscreen mode

将文件重命名 jobs/examples.ts 为 jobs/functions.ts。这是处理所有作业的地方。

接下来,安装 Zod  - 一个 TypeScript 优先的类型检查和验证库,使您能够验证作业有效负载的数据类型。



npm install zod


Enter fullscreen mode Exit fullscreen mode

在 Trigger.dev 中,可以使用该client.sendEvent()方法触发作业。因此,修改该readFile函数以触发新创建的作业,并将用户的数据作为有效负载发送到该作业。



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

使用 Replicate 创建面

Replicate 是一个 Web 平台,允许用户在云端大规模运行模型。在这里,您将学习如何使用 Replicate 上的 AI 模型生成和交换图像人脸。

请按照以下步骤完成此操作:

访问Replicate 主页,单击Sign in按钮通过您的 GitHub 帐户登录,并生成您的 API 令牌。

复制令牌

将您的 API 令牌、 用于生成图像的 Stability AI 模型 URI和 Faceswap AI 模型 URI复制 到.env.local文件中。



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

接下来,转到 Trigger.dev 集成页面并安装 Replicate 包。



npm install @trigger.dev/replicate@latest


Enter fullscreen mode Exit fullscreen mode

导入并初始化文件中的 Replicate jobs/functions.ts



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

更新jobs/functions.ts文件以使用用户提供的提示或默认提示生成图像。



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

上面的代码片段根据提示生成 AI 图像并将其记录在您的 Trigger.dev 仪表板上。

产生

记住,你需要生成一张 AI 图像,并将用户的脸部与 AI 生成的图像进行交换。接下来,让我们在图像上交换脸部。

将此函数复制到文件顶部。该代码片段将生成的图像转换为其数据 URI,这是换脸 AI 模型jobs/functions.ts接受的格式 



//👇🏻 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

更新 Trigger.dev 作业,将用户的图像和生成的图像作为参数发送到faceswap 模型



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

上面的代码片段获取 AI 生成的图像和用户图像的数据 URI,并将这两个图像发送到 AI 模型,该模型返回交换图像的 URL。

恭喜!🎉 您已经学会了如何使用 Replicate 生成您自己的 AI 图像。在接下来的部分中,您将学习如何使用 Resend 通过电子邮件发送这些图像。

PS:您还可以从Lexica获取图像的自定义提示

产生


通过 Trigger.dev 重新发送电子邮件

Resend 是一个电子邮件 API,可让您轻松发送文本、附件和电子邮件模板。借助 Resend,您可以大规模构建、测试和发送事务性电子邮件。

访问 注册页面,创建一个帐户和一个 API 密钥并将其保存到.env.local文件中。



RESEND_API_KEY=<place_your_API_key>


Enter fullscreen mode Exit fullscreen mode

钥匙

将Trigger.dev Resend 集成包安装到您的 Next.js 项目中。



npm install @trigger.dev/resend


Enter fullscreen mode Exit fullscreen mode

将 Resend 导入/jobs/functions.ts文件,如下所示。



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

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


Enter fullscreen mode Exit fullscreen mode

最后,将“重新发送”集成到作业中并将交换的图像发送到用户的电子邮件。



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

恭喜!🎉您已完成本教程的项目。

结论

到目前为止,你已经学会了如何

  • 将图像上传到 Next.js 中的本地目录,
  • 使用Trigger.dev创建和管理长期运行的作业
  • 使用Replicate上的各种模型生成 AI 图像,以及
  • 通过 Trigger.dev 中的“重新发送”发送电子邮件。

作为开源开发者,我们诚邀您加入我们的 社区 ,贡献力量并与维护人员互动。欢迎访问我们的 GitHub 代码库 ,贡献代码并创建与 Trigger.dev 相关的问题。

本教程的源代码可以在这里找到:
https://github.com/triggerdotdev/blog/tree/main/avatar-generator

感谢您的阅读!


不要忘记生成一个新的头像并将其发布在评论中!
https://avatar-generator-psi.vercel.app
(要找到好的提示,请查看https://lexica.art

文章来源:https://dev.to/triggerdotdev/turn-your-face-into-a-super-hero-with-nextjs-replicate-and-triggerdev-17ln
PREV
如何删除机器上的所有 node_modules 文件夹并释放高清空间!
NEXT
🚀 发送新星通知的 4 大方法⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️ TL;DR