สำหรับโพสต์นี้ เราจะทำ assumptions เล็กน้อยก่อน แต่ควรมี:
- บัญชี Upstash ที่มี Redis instance สร้างแล้ว
- บัญชี Replicate พร้อม API token
- Next.js project สำหรับ implement functionality ที่ต้องการ
- บัญชี Vercel สำหรับ deploy โปรเจกต์
นี่คืออะไร?
อยากเริ่มใช้ machine learning generate images จาก models บน Replicate มานานแล้วหรือ? ใน tutorial นี้ เราจะสำรวจ hosted models หลากหลายของ Replicate และ Upstash Redis ไม่ได้แค่สำรวจ แต่จะ walk through การตั้งค่าหนึ่ง model และแตะถึงวิธี update implementation ใช้ models อื่นๆ ได้ง่าย
ใน tutorial นี้ เราจะใช้ Microsoft's Bringing Old Photos Back to Life model ซึ่งรับ old photo รันผ่าน model output edited version ที่ปรับปรุงแล้วของภาพ

Architecture คืออะไร?
ถ้ามี React experience คุณควรเข้าใจ app architecture ได้จากการอ่าน codebase แต่เพื่อให้ง่ายกว่านั้น หรือถ้าชอบ overview มีด้านล่าง

ต้องการอะไรเริ่มต้น?
ต้องมี Next.js project ทำได้ตาม Next.js setup guide หรือถ้ามีแล้วก็ใช้ได้ ใน tutorial นี้ใช้ Tailwind CSS แต่คุณใช้ styling setup ใดก็ได้
ตอนมี basic Next.js project แล้ว ติดตั้ง Upstash Redis library:
npm install @upstash/redis
ต่อไป populate .env.local ด้วย keys ต่อไปนี้ Redis token หาได้ใน Upstash console, Replicate API token ใน account, และ site URL จะเป็น Vercel deployment endpoint
SITE_URL=https://your-project-url.vercel.app
REPLICATE_API_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
ตั้งค่า Frontend Form
เริ่มจาก form ที่ handle form, polling และ displaying completed images
สร้าง Restore Image Form
File: 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>
</>
);
}
โดย default component นี้แสดง form ให้ user ใส่ image URL ที่อยาก restore และ options เล็กน้อย เช่น high resolution หรือ scratches ที่ต้องลบ เมื่อ user กรอกและ submit ส่ง POST request ไป /api/create พร้อม form data
เมื่อส่ง request และได้ response พร้อม prediction info component เข้าสู่ polling state ส่ง GET request ไป /api/poll ทุกวินาที เช็คว่า prediction เสร็จหรือยัง เมื่อ polling request ส่ง response สำเร็จ แสดงว่า Replicate ส่ง request ไป callback endpoint เราก็ได้ prediction output
ระหว่าง polling form แสดงปุ่ม cancel prediction กดแล้วส่ง POST request ไป /api/cancel พร้อม cancel_url จาก prediction data ที่ได้ตอนสร้างครั้งแรก
Polling implementation ใช้ custom hook ใน hooks/useInterval.tsช่วยจัดการ React component lifecycle สะดวกกว่า อ่านเพิ่มเติมได้ ที่นี่ และ ที่นี่
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 Setup
API setup ประกอบด้วยหลายไฟล์ สร้างและยกเลิก predictions, poll เช็คการเสร็จสิ้น, และระบุ callback ที่ Replicate จะใช้เมื่อ prediction เสร็จ
Image Prediction Creation
File: 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 });
});
}
สำหรับ create API endpoint เราเช็ค request method เป็น POST ถ้าไม่ใช่ ส่ง 400 response ส่ง POST request ไป Replicate พร้อม API token Request body มี parameters สำหรับ model version บ่งบอก model ที่ส่ง request (เจอที่แท็บ "API" ของ model ที่ต้องการ) ส่ง parameters ที่เกี่ยวข้องกับ model พร้อม form data
หลังส่ง request ใช้ prediction id ที่ได้เก็บใน Redis ส่ง prediction data กลับ frontend ใช้ poll Redis item จนกว่าจะเป็น completed prediction
Callback
File: 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 });
}
}
Callback endpoint คือที่ Replicate ส่ง POST request เพื่อแจ้งว่า prediction processing เสร็จ เมื่อรับ request ดึง prediction data จาก request body อัปเดต Redis item ด้วย completed prediction data
Polling
File: 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 });
}
}
สำหรับ polling setup extract id จาก request พยายามดึง data ใน Redis ภายใต้ identifier นั้น ถ้าไม่เจอ ส่ง 404 ถ้ามี ส่ง data ใน 200 response
Cancel
File: 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 endpoint สำหรับยกเลิก predictions ตรงไปตรงมา extract cancel_url ที่ส่งมาจาก frontend (มาจาก prediction ที่เก็บตอนสร้าง request) ส่ง POST request ไป endpoint นั้นพร้อม Replicate API token
Libs
สำหรับ libs สร้าง Redis client ใช้ใน tracking
File: 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;
Object นี้ใช้ในแอปพลิเคชันเก็บและดึงข้อมูลระหว่าง polling เพื่อรู้ว่า webhook completion จาก Replicate เกิดขึ้นแล้ว
สรุป
Replicate มี models หลากหลายใช้ผ่าน API ด้วย Vercel และ Upstash ใช้ machine learning models และ deploy web applications ง่ายกว่าคิด
อยากดู complete repository เข้าได้ ที่นี่
Further Development
นี่แค่ตัวอย่างง่ายใช้ model ง่ายกับ Replicate ด้วยการเปลี่ยน form parameters และ version ใน API คุณเปลี่ยนไป model อื่นได้ง่าย ตราบใดที่มี Replicate API token linked คุณ consume available models ได้ทั้งหมด
สำรวจ models ทั้งหมดของ Replicate ที่นี่ เจอ model ที่อยากลอง คลิกแท็บ "API" ดู usage มีปุ่ม Python, cURL, Cog, Docker ทดสอบ model ได้ และรู้ parameters ที่ต้องการและวิธีส่ง