Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Replicate, Next.js 및 Upstash를 사용하여 사진 복원 앱 개발

이 블로그 게시물의 경우 계속하기 전에 몇 가지 가정을 하겠지만 이상적으로는 다음을 충족해야 합니다.

  • Redis 인스턴스가 생성된 Upstash 계정
  • API 토큰에 액세스할 수 있는 복제 계정
  • 원하는 기능을 구현하기 위한 Next.js 프로젝트
  • 프로젝트를 배포할 Vercel 계정

이게 뭐죠?

Replicate에서 사용 가능한 모델로부터 이미지를 생성하기 위해 기계 학습을 사용하고 싶으십니까? 이 튜토리얼에서는 Replicate의 광범위한 호스팅 모델과 Upstash의 Redis를 살펴보겠습니다. 이러한 모델을 탐색할 뿐만 아니라 모델을 설정하는 과정을 살펴보고 다른 모델도 사용하기 위해 구현을 쉽게 업데이트할 수 있는 방법을 살펴보겠습니다.

이 튜토리얼에서는 본질적으로 오래된 사진을 가져와서 모델을 통해 실행하고 편집되고 개선된 버전의 사진을 출력하는 Microsoft의 오래된 사진 되살리기 모델의 사용법을 다룹니다.

Replicate, Next.js 및 Upstash를 사용하여 사진 복원 앱 개발

아키텍처는 무엇인가요?

React 경험이 있는 경우 코드베이스를 읽는 것만으로도 앱 아키텍처가 어떻게 작동하는지 확인할 수 있어야 하지만, 이를 좀 더 쉽게 만들기 위해 또는 단순히 개요를 보고 싶은 경우 아래에 제공되는 내용이 있습니다.

Replicate, Next.js 및 Upstash를 사용하여 사진 복원 앱 개발

무엇을 시작해야 하나요?

시작하려면 물론 Next.js 프로젝트가 필요합니다. 여기의 Next.js 설정 가이드를 따르면 됩니다. 이미 설정한 경우에도 괜찮습니다. 이 튜토리얼에서는 Tailwind CSS도 사용하지만 물론 원하는 모든 스타일 설정 형식을 사용할 수 있습니다.

이제 기본 Next.js 프로젝트 설정이 완료되었으므로 다음 명령을 실행하여 Upstash의 Redis 라이브러리를 계속 사용할 수 있습니다.

npm install @upstash/redis

다음으로 .env.local을 채우겠습니다. Redis 토큰은 Upstash 콘솔에서 찾을 수 있고, Replicate API 토큰은 여기 계정 아래에 있으며, 사이트 URL은 배포할 위치이므로 이 경우 Vercel 배포 엔드포인트가 됩니다.

SITE_URL=https://your-project-url.vercel.app
 
REPLICATE_API_TOKEN=
 
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

프런트엔드 양식 설정

우선 완성된 이미지의 양식, 폴링 및 표시를 처리하는 양식이 필요합니다.

이미지 양식 생성 복원

파일:pages/index.tsx

import { MouseEvent, RefObject, useRef, useState } from "react";
import Head from "next/head";
 
import useInterval from "../hooks/useInterval";
 
export default function Home() {
 const [restoring, setRestoring] = useState<boolean>(false);
 const [messageId, setMessageId] = useState<string | null>(null);
 const [prediction, setPrediction] = useState<any>({});
 const [outputImageUrl, setOutputImageUrl] = useState<string | null>(null);
 const imageUrlRef: RefObject<HTMLInputElement> = useRef(null);
 const hrRef: RefObject<HTMLInputElement> = useRef(null);
 const scratchRef: RefObject<HTMLInputElement> = useRef(null);
 
 useInterval(
 async () => {
 await fetch(`/api/poll?id=${messageId}`)
 .then((res: any) => res.json())
 .then((data: any) => {
 if (!data.output) {
 return;
 }
 
 setRestoring(false);
 setMessageId(null);
 setOutputImageUrl(data.output);
 })
 .catch((err: any) => console.error(err));
 },
 messageId ? 1000 : null,
 );
 
 async function restoreImage(e: any) {
 e.preventDefault();
 
 setRestoring(true);
 
 await fetch("/api/create", {
 method: "POST",
 body: JSON.stringify({
 image_url: imageUrlRef.current?.value,
 is_hr: hrRef.current?.value,
 has_scratches: scratchRef.current?.value,
 }),
 headers: { "Content-Type": "application/json" },
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 setMessageId(data.data.id);
 setPrediction(data.data);
 })
 .catch((err: Error) => console.error(err));
 }
 
 async function cancel(e: MouseEvent<HTMLButtonElement>) {
 e.preventDefault();
 
 await fetch("/api/cancel", {
 method: "POST",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ cancel_url: prediction.urls.cancel }),
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 setMessageId(null);
 setPrediction({});
 setRestoring(false);
 })
 .catch((err: Error) => console.error(err));
 }
 
 return (
 <>
 <Head>
 <title>PhotoRescue</title>
 <meta
 name="description"
 content="A simple Next.js application that utilizes Replicate to restore old photos."
 />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <link rel="icon" href="/favicon.ico" />
 </Head>
 <main>
 <div className="my-16 flex flex-col items-center justify-center md:my-32">
 <h1 className="text-5xl font-black">PhotoRescue</h1>
 
 <p className="mt-4">Restore your old photos to their former glory.</p>
 
 {outputImageUrl && (
 <div className="flex flex-col items-center justify-center">
 <img
 src={outputImageUrl}
 alt="Restored Image"
 className="mt-8 h-auto w-72"
 />
 
 <button
 type="button"
 onClick={() => setOutputImageUrl(null)}
 className="mt-8 inline-flex items-center rounded-full border border-transparent bg-gray-900 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 disabled:opacity-50"
 >
 Start Again
 </button>
 </div>
 )}
 
 {!outputImageUrl && (
 <form
 onSubmit={restoreImage}
 className="mt-10 flex w-full max-w-lg flex-col items-center"
 >
 <div className="w-full space-y-4">
 <div>
 <label htmlFor="image_url" className="text-sm font-semibold">
 Image URL
 </label>
 <input
 name="image_url"
 id="image_url"
 type="text"
 defaultValue="https://replicate.delivery/mgxm/b033ff07-1d2e-4768-a137-6c16b5ed4bed/d_1.png"
 placeholder="https://example.com/image.png"
 className="mt-0.5 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-gray-500 focus:ring-gray-500"
 ref={imageUrlRef}
 required
 />
 </div>
 <div className="max-w-lg space-y-4">
 <div className="relative flex items-start">
 <div className="flex h-5 items-center">
 <input
 name="is_hr"
 id="is_hr"
 type="checkbox"
 className="h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
 ref={hrRef}
 />
 </div>
 <div className="ml-3 text-sm">
 <label
 htmlFor="is_hr"
 className="font-medium text-gray-900"
 >
 Is High Resolution?
 </label>
 <p className="text-gray-500">
 Check this if the input image is a high resolution
 photo.
 </p>
 </div>
 </div>
 <div className="relative flex items-start">
 <div className="flex h-5 items-center">
 <input
 name="is_scratched"
 id="is_scratched"
 type="checkbox"
 className="h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
 ref={scratchRef}
 defaultChecked={true}
 />
 </div>
 <div className="ml-3 text-sm">
 <label
 htmlFor="is_scratched"
 className="font-medium text-gray-900"
 >
 Has Scratches?
 </label>
 <p className="text-gray-500">
 Check this if the input image has visible scratches over
 it.
 </p>
 </div>
 </div>
 </div>
 </div>
 
 <div className="mt-6 flex gap-2">
 <button
 type="submit"
 disabled={restoring}
 className="inline-flex items-center rounded-full border border-transparent bg-gray-900 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 disabled:opacity-50"
 >
 {restoring ? "Restoring..." : "Restore"}
 </button>
 
 {restoring && prediction && (
 <button
 type="button"
 onClick={cancel}
 className="inline-flex items-center rounded-full border border-gray-900 bg-white px-6 py-2.5 text-sm font-medium text-gray-900 shadow-sm hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2"
 >
 Cancel
 </button>
 )}
 </div>
 </form>
 )}
 </div>
 </main>
 </>
 );
}

기본적으로 이 구성 요소는 사용자가 복원하려는 이미지의 이미지 URL을 입력할 수 있는 양식과 함께 사용할 수 있는 몇 가지 옵션(예:이미지가 고해상도인지 또는 이미지에 제거해야 할 스크래치가 있는지 여부)을 표시합니다. 사용자가 이 정보를 입력하고 양식을 제출하면 POST가 전송됩니다. /api/create에 요청하세요 양식 데이터와 함께.

이 요청이 API로 전송되고 반환된 예측 정보와 함께 응답이 수신되면 구성 요소는 GET을 보내는지 확인하는 폴링 상태로 들어갑니다. /api/poll에 요청하세요 예측이 아직 완료되었는지 확인하기 위해 초당 한 번씩. 폴링 요청이 성공적인 응답을 반환하면 Replicate가 콜백 엔드포인트에 요청을 보냈다는 것을 나타내므로 이제 예측 출력에 액세스할 수 있습니다.

폴링이 진행되는 동안 양식에는 예측을 취소할 수 있는 옵션이 있는 버튼이 표시됩니다. 누르면 POST가 전송됩니다. /api/cancel에 요청하세요 cancel_url로 초기 생성 시 받은 예측 데이터에서.

폴링 구현은 hooks/useInterval.ts에 있는 사용자 정의 후크를 활용합니다. 이를 통해 우리는 React의 구성 요소 라이프스타일을 쉽고 원활하게 작업할 수 있으며 특정 React 구성 요소 내에서 콜백을 사용하여 간격을 처리하는 보다 편리한 방법을 제공합니다. 이 후크에 대해 더 자세히 알아보고 싶다면 여기와 여기에서 더 자세히 읽어보실 수 있습니다.

import { useEffect, useRef } from "react";
 
function useInterval(callback: () => void, delay: number | null) {
 const savedCallback = useRef(callback);
 
 useEffect(() => {
 savedCallback.current = callback;
 }, [callback]);
 
 useEffect(() => {
 if (!delay && delay !== 0) {
 return;
 }
 
 const id = setInterval(() => savedCallback.current(), delay);
 
 return () => clearInterval(id);
 }, [delay]);
}
 
export default useInterval;

API 설정

몇 개의 파일로 구성된 API 설정을 통해 예측을 생성 및 취소하고, 예측이 완료되었는지 확인하기 위해 폴링하고, 예측이 완료되었을 때 Replicate가 사용할 콜백을 지정할 수 있습니다.

이미지 예측 생성

파일:pages/api/create.ts

import type { NextApiRequest, NextApiResponse } from "next";
 
import fetch, { Response } from "node-fetch";
 
import redis from "../../lib/redis";
 
export default async function handler(
 req: NextApiRequest,
 res: NextApiResponse,
) {
 if (req.method !== "POST") {
 return res.status(400).json({
 message: `Invalid request method: ${req.method}.`,
 });
 }
 
 const { image_url, is_hr, has_scratches }: any = req.body;
 
 await fetch("https://api.replicate.com/v1/predictions", {
 method: "POST",
 headers: {
 Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`,
 "Content-Type": "application/json",
 },
 body: JSON.stringify({
 version:
 "c75db81db6cbd809d93cc3b7e7a088a351a3349c9fa02b6d393e35e0d51ba799",
 input: {
 image: image_url,
 HR: is_hr,
 with_scratch: has_scratches,
 },
 webhook_completed: `${process.env.SITE_URL}/api/callback`,
 }),
 })
 .then((res: Response) => res.json())
 .then(async (data: any) => {
 await redis.set(data.id, data);
 
 return res.status(202).json({ data: data });
 })
 .catch((error: Error) => {
 return res.status(500).json({ message: error.message });
 });
}

API 엔드포인트 생성의 경우 먼저 들어오는 요청 메소드가 POST인지 확인하기 위해 간단한 검사를 수행합니다. 요청하고 그렇지 않은 경우 간단한 400 응답을 반환합니다. 그런 다음 POST 전송을 진행합니다. Replicate API 토큰을 사용하여 복제를 요청합니다. 요청 본문은 해당 모델 version에 대한 매개변수로 구성됩니다. 이는 우리가 요청을 보내는 모델을 나타냅니다(사용하려는 모델의 "API" 탭 아래에 있음). 또한 프런트엔드 양식의 데이터와 함께 모델과 관련된 매개변수를 전달합니다.

요청이 전송되면 반환된 예측 id을 사용합니다. 이를 Redis에 저장하고, 예측 데이터가 완료된 예측으로 구성될 때까지 Redis 항목을 폴링하는 데 사용할 예측 데이터를 프런트엔드에 반환합니다.

콜백

파일:pages/api/callback.ts

import type { NextApiRequest, NextApiResponse } from "next";
 
import redis from "../../lib/redis";
 
export default async function handler(
 req: NextApiRequest,
 res: NextApiResponse,
) {
 const { body }: any = req;
 
 try {
 await redis.set(body.id, body);
 
 return res.status(200).send(body);
 } catch (error) {
 return res.status(500).json({ error });
 }
}

콜백 끝점은 Replicate가 POST를 보내는 것입니다. 특정 예측의 처리가 완료되었음을 알리기 위해 요청합니다. 이 요청을 받으면 요청 본문에서 예측 데이터를 검색하고 지정된 Redis 항목을 완성된 예측 데이터로 업데이트합니다.

폴링

파일:pages/api/poll.ts

import type { NextApiRequest, NextApiResponse } from "next";
 
import redis from "../../lib/redis";
 
export default async function handler(
 req: NextApiRequest,
 res: NextApiResponse,
) {
 const { id }: any = req.query;
 
 try {
 const data = await redis.get(id);
 
 if (!data) {
 return res
 .status(404)
 .json({ message: "Data for supplied ID not found" });
 }
 
 return res.status(200).json(data);
 } catch (error: any) {
 return res.status(500).json({ message: error.message });
 }
}

폴링 설정을 위해 id를 추출합니다. 그런 다음 해당 식별자로 Redis에 저장된 데이터를 검색하려고 시도합니다. 데이터가 없으면 404 응답을 반환하지만 데이터가 있으면 해당 데이터를 200 응답의 일부로 반환합니다.

취소

파일:pages/api/cancel.tsx

import type { NextApiRequest, NextApiResponse } from "next";
 
import fetch, { Response } from "node-fetch";
 
export default async function handler(
 req: NextApiRequest,
 res: NextApiResponse,
) {
 if (req.method !== "POST") {
 return res.status(400).json({
 message: `Invalid request method: ${req.method}.`,
 });
 }
 
 const { cancel_url }: any = req.body;
 
 await fetch(cancel_url, {
 method: "POST",
 headers: {
 Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`,
 "Content-Type": "application/json",
 },
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 return res.status(202).json({ data: data });
 })
 .catch((error: Error) => {
 return res.status(500).json({ message: error.message });
 });
}

시작된 예측을 취소하기 위한 API 엔드포인트는 다소 간단합니다. 간단히 cancel_url를 추출합니다. 이는 생성 요청이 제출될 때 저장된 예측에서 나오는 프런트엔드에서 전달되며 간단히 POST를 보냅니다. Replicate API 토큰과 함께 해당 엔드포인트에 요청합니다.

라이브러리

libs를 위해 추적에 사용되는 Redis 클라이언트를 생성하겠습니다.

파일:lib/redis.ts

import { Redis } from "@upstash/redis";
 
const redis = new Redis({
 url: process.env.UPSTASH_REDIS_REST_URL as string,
 token: process.env.UPSTASH_REDIS_REST_TOKEN as string,
});
 
export default redis;

이 객체는 애플리케이션 내에서 폴링되는 동안 데이터를 저장하고 검색하는 데 사용되므로 Replicate에서 웹훅이 완료되는 시기를 알 수 있습니다.

결론

Replicate에는 API를 통해 사용할 수 있는 다양한 모델이 있습니다. Vercel과 Upstash를 사용하면 머신러닝 모델을 활용하고 사용 가능한 웹 애플리케이션을 배포하는 것이 그 어느 때보다 쉬워졌습니다.

전체 저장소를 보려면 여기에서 액세스할 수 있습니다.

추가 개발

이는 Replicate를 사용하여 다소 단순한 모델을 활용하는 간단한 예일 뿐입니다. API에서 양식 매개변수와 버전을 간단히 전환하면 Replicate API 토큰이 연결되어 있는 한 다른 모델로 쉽게 변경할 수 있으며 사용 가능한 모든 모델을 사용할 수 있습니다.

여기에서 Replicate의 사용 가능한 모든 모델을 탐색할 수 있으며 실험하고 싶은 모델을 찾으면 "API" 탭을 클릭하여 해당 모델의 사용법을 볼 수 있습니다. 여기에는 모델을 테스트할 수 있는 Python, cURL, Cog 및 Docker용 버튼도 있으며, 필요한 매개변수와 전송 방법을 아는 데에도 유용합니다.