Skip to content

Commit f4932d8

Browse files
authored
Merge pull request #26 from triggerdotdev/product-image-generator-improvements
Added product image generator
2 parents b71fcfd + bf14fd6 commit f4932d8

34 files changed

+6516
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ For information on how to run each project, see the README in each directory.
3535
| [Mastra agents](/mastra-agents) | Example of using [Mastra](https://github.com/mastra-ai/mastra) agents with Trigger.dev |
3636
| [OpenAI Agent SDK Guardrails examples](/openai-agent-sdk-guardrails-examples) | Examples of using the [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) with guardrails and Trigger.dev |
3737
| [OpenAI Agents SDK for Typescript with Trigger.dev playground](/openai-agents-sdk-with-trigger-playground) | A playground to test and play with AI agents built with the OpenAI Agents SDK for Typescript and Trigger.dev |
38+
| [Product image generator](/product-image-generator) | AI-powered product image generator that transforms basic product photos into professional marketing shots using Replicate's image generation models (`google/nano-banana`) and Trigger.dev for task orchestration |
3839
| [Python doc to markdown converter](/python-doc-to-markdown-converter) | Convert documents to Markdown using [Trigger.dev](https://trigger.dev) and [MarkItDown](https://github.com/microsoft/markitdown) |
3940
| [Python Crawl4ai](/python-crawl4ai) | Headless web crawler using Trigger.dev with Python, [Crawl4AI](https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai), and [Playwright](https://playwright.dev/) |
4041
| [Python image processing](/python-image-processing) | Python-based ([Pillow](https://pillow.readthedocs.io/en/stable/)) image processing tasks and uploading to S3-compatible storage |

product-image-generator/.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
42+
43+
.trigger

product-image-generator/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Product Image Generator using Trigger.dev and Replicate
2+
3+
AI-powered product image generator that transforms basic product photos into professional marketing shots using Replicate's image generation models and Trigger.dev for task orchestration.
4+
5+
## Tech stack
6+
7+
- [**Next.js**](https://nextjs.org/) – frontend React framework
8+
- [**Replicate**](https://replicate.com/docs) – AI image generation
9+
- [**Trigger.dev**](https://trigger.dev/docs) – background task orchestration
10+
- [**UploadThing**](https://uploadthing.com/) – file upload handling
11+
- [**Cloudflare R2**](https://developers.cloudflare.com/r2/) – image storage
12+
13+
## Setup & Running locally
14+
15+
1. **Clone the repository**
16+
17+
```bash
18+
git clone <repository-url>
19+
cd product-image-generator
20+
```
21+
22+
2. **Install dependencies**
23+
24+
```bash
25+
pnpm install
26+
```
27+
28+
3. **Copy environment variables and configure**
29+
30+
```bash
31+
cp env.example .env
32+
```
33+
34+
Fill in the required variables:
35+
36+
- `TRIGGER_SECRET_KEY` – Get from [Trigger.dev dashboard](https://cloud.trigger.dev/)
37+
- `REPLICATE_API_TOKEN` – Get from [Replicate](https://replicate.com/account/api-tokens)
38+
- `UPLOADTHING_TOKEN` – Get from [UploadThing](https://uploadthing.com/)
39+
- `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_PUBLIC_URL` – Configure Cloudflare R2 storage
40+
41+
4. **Add Trigger.dev project reference**
42+
43+
Update `trigger.config.ts` with your project ref:
44+
45+
```typescript
46+
project: "your_project_ref_here";
47+
```
48+
49+
5. **Start development servers**
50+
51+
```bash
52+
# Terminal 1: Start Next.js dev server
53+
pnpm dev
54+
55+
# Terminal 2: Start Trigger.dev CLI
56+
npx trigger.dev@latest dev
57+
```
58+
59+
## How it works
60+
61+
Trigger.dev orchestrates the image generation workflow through two main tasks:
62+
63+
1. **`generateImages`** – Batch coordinator that triggers multiple individual image generations ([`app/trigger/generate-images.ts`](app/trigger/generate-images.ts))
64+
2. **`generateImage`** – Individual image processor that:
65+
- Enhances prompts with style-specific instructions
66+
- Calls Replicate's `google/nano-banana` model
67+
- Creates waitpoint tokens for async webhook handling
68+
- Waits for Replicate completion via webhook callbacks
69+
- Uploads generated images to Cloudflare R2
70+
- Returns public URLs for display
71+
72+
**Process flow:**
73+
74+
1. User selects and uploads product image to the website
75+
2. Image is uploaded to UploadThing cloud storage
76+
3. UploadThing's `onUploadComplete` callback triggers batch generation for 3 preset styles
77+
4. Each style runs as separate Trigger.dev task with waitpoints for Replicate webhooks
78+
5. Frontend receives real-time progress updates via Trigger.dev React hooks
79+
6. Generated images are stored in Cloudflare R2 and displayed with download options
80+
81+
**Style presets:**
82+
83+
- **Clean Product Shot** – Professional white background with studio lighting
84+
- **Lifestyle Scene** – Person holding product with natural lighting
85+
- **Hero Shot** – Elegant hands presenting product with dramatic lighting
86+
87+
## Relevant code
88+
89+
- **Image generation tasks** – Batch processing with waitpoints for Replicate webhook callbacks ([`app/trigger/generate-images.ts`](app/trigger/generate-images.ts))
90+
- **Upload handler** – UploadThing integration that triggers batch generation for 3 preset styles ([`app/api/uploadthing/core.ts`](app/api/uploadthing/core.ts))
91+
- **Real-time progress UI** – Live task updates using Trigger.dev React hooks ([`app/components/GeneratedCard.tsx`](app/components/GeneratedCard.tsx))
92+
- **Custom prompt interface** – User-defined style generation with custom prompts ([`app/components/CustomPromptCard.tsx`](app/components/CustomPromptCard.tsx))
93+
- **Main app component** – Layout and state management with professional style presets ([`app/ProductImageGenerator.tsx`](app/ProductImageGenerator.tsx))
94+
- **Trigger.dev configuration** – Project settings and task directories ([`trigger.config.ts`](trigger.config.ts))
95+
96+
## Learn more
97+
98+
- [**Trigger.dev waitpoints**](https://trigger.dev/docs/wait-for-token) – pause tasks for async webhook callbacks
99+
- [**Trigger.dev React hooks**](https://trigger.dev/docs/frontend/react-hooks) – real-time task updates and frontend integration
100+
- [**Trigger.dev batch operations**](https://trigger.dev/docs/tasks/batch-trigger) – parallel task execution patterns
101+
- [**Replicate API**](https://replicate.com/docs/get-started/nextjs) – AI model integration and webhook handling
102+
- [**UploadThing**](https://docs.uploadthing.com/) – file upload handling and server callbacks
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"use client";
2+
3+
import { Download, Home, Settings, User, WandSparklesIcon } from "lucide-react";
4+
import { useSearchParams } from "next/navigation";
5+
import { GeneratedCard } from "./components/GeneratedCard";
6+
import { Button } from "./components/ui/button";
7+
import { UploadCard } from "./components/UploadCard";
8+
import CustomPromptCard from "./components/CustomPromptCard";
9+
import Link from "next/link";
10+
11+
const promptTitles = {
12+
"isolated-table": "Clean Product Shot",
13+
"lifestyle-scene": "Lifestyle Scene",
14+
"hero-shot": "Hero Shot",
15+
};
16+
17+
export function ProductImageGenerator() {
18+
const searchParams = useSearchParams();
19+
const publicAccessToken = searchParams.get("publicAccessToken");
20+
const generateToken = searchParams.get("triggerToken");
21+
const fileUrl = searchParams.get("fileUrl");
22+
const runId = searchParams.get("runId");
23+
24+
return (
25+
<div className="min-h-screen bg-gray-100/20 ">
26+
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
27+
<div className="container mx-auto px-4">
28+
<div className="flex h-14 items-center justify-between">
29+
<div className="flex items-center space-x-4">
30+
<div className="flex items-center space-x-2">
31+
<Link href="/" className="flex items-center space-x-2">
32+
<WandSparklesIcon className="h-5 w-5 text-purple-500" />
33+
</Link>
34+
<h1 className="text-xl font-bold text-foreground">ImageFlow</h1>
35+
</div>
36+
</div>
37+
38+
<nav className="flex items-center space-x-1">
39+
<Button variant="ghost" size="sm">
40+
<Home className="h-4 w-4 mr-1 text-gray-500" />
41+
Home
42+
</Button>
43+
<Button variant="ghost" size="sm">
44+
<User className="h-4 w-4 mr-1 text-gray-500" />
45+
Account
46+
</Button>
47+
<Button variant="ghost" size="sm">
48+
<Settings className="h-4 w-4 mr-1 text-gray-500" />
49+
Settings
50+
</Button>
51+
</nav>
52+
</div>
53+
</div>
54+
</header>
55+
56+
<main className="container px-4 py-16 w-full mx-auto ">
57+
<div className="max-w-7xl mx-auto w">
58+
<div className="mb-8 flex justify-between items-end gap-8">
59+
<div>
60+
<h2 className="text-3xl font-bold text-foreground mb-2">
61+
Product Image Generator
62+
</h2>
63+
<p className="text-muted-foreground">
64+
Upload a product image and generate professional marketing shots
65+
for your online store.
66+
</p>
67+
</div>
68+
<div>
69+
<Button variant={"default"} className={"cursor-pointer"}>
70+
<Download className="h-4 w-4 mr-1" />
71+
Download images
72+
</Button>
73+
</div>
74+
</div>
75+
76+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
77+
<UploadCard
78+
runId={runId ?? undefined}
79+
accessToken={publicAccessToken ?? undefined}
80+
fileUrl={fileUrl ?? undefined}
81+
/>
82+
<GeneratedCard
83+
id="isolated-table"
84+
runId={runId ?? undefined}
85+
accessToken={publicAccessToken ?? undefined}
86+
promptTitle={promptTitles["isolated-table"]}
87+
/>
88+
<GeneratedCard
89+
id="lifestyle-scene"
90+
runId={runId ?? undefined}
91+
accessToken={publicAccessToken ?? undefined}
92+
promptTitle={promptTitles["lifestyle-scene"]}
93+
/>
94+
<GeneratedCard
95+
id="hero-shot"
96+
runId={runId ?? undefined}
97+
accessToken={publicAccessToken ?? undefined}
98+
promptTitle={promptTitles["hero-shot"]}
99+
/>
100+
</div>
101+
102+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
103+
{Array.from({ length: 4 }).map((_, index) => {
104+
return (
105+
<CustomPromptCard
106+
id={`custom-prompt-${index}`}
107+
key={`custom-prompt-${index}`}
108+
fileUrl={fileUrl ?? undefined}
109+
generateToken={generateToken ?? undefined}
110+
/>
111+
);
112+
})}
113+
</div>
114+
</div>
115+
</main>
116+
</div>
117+
);
118+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { randomUUID } from "crypto";
2+
import { createUploadthing, type FileRouter } from "uploadthing/next";
3+
import { UploadThingError } from "uploadthing/server";
4+
import type { generateImages } from "@/trigger/generate-images";
5+
import { auth, tasks } from "@trigger.dev/sdk";
6+
7+
const f = createUploadthing();
8+
9+
const mockAuth = (req: Request) => ({ id: randomUUID() }); // Fake auth function
10+
11+
// FileRouter for your app, can contain multiple FileRoutes
12+
export const ourFileRouter = {
13+
// Define as many FileRoutes as you like, each with a unique routeSlug
14+
imageUploader: f({ image: { maxFileSize: "4MB" } })
15+
// Set permissions and file types for this FileRoute
16+
.middleware(async ({ req }) => {
17+
// This code runs on your server before upload
18+
const user = await mockAuth(req);
19+
20+
// If you throw, the user will not be able to upload
21+
if (!user) throw new UploadThingError("Unauthorized");
22+
23+
// Whatever is returned here is accessible in onUploadComplete as `metadata`
24+
return { userId: user.id };
25+
})
26+
.onUploadComplete(async ({ metadata, file }) => {
27+
// This code RUNS ON YOUR SERVER after upload
28+
29+
const { id, publicAccessToken } = await tasks.trigger<
30+
typeof generateImages
31+
>("generate-images", {
32+
images: [
33+
{
34+
id: "isolated-table",
35+
baseImageUrl: file.ufsUrl,
36+
promptStyle: "isolated-table",
37+
},
38+
{
39+
id: "lifestyle-scene",
40+
baseImageUrl: file.ufsUrl,
41+
promptStyle: "lifestyle-scene",
42+
},
43+
{
44+
id: "hero-shot",
45+
baseImageUrl: file.ufsUrl,
46+
promptStyle: "hero-shot",
47+
},
48+
],
49+
});
50+
51+
const triggerToken = await auth.createTriggerPublicToken(
52+
"generate-image",
53+
{
54+
expirationTime: "20m",
55+
multipleUse: true, // not recommended without an expiration time
56+
},
57+
);
58+
59+
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
60+
return {
61+
uploadedBy: metadata.userId,
62+
publicAccessToken,
63+
triggerToken,
64+
runId: id,
65+
fileId: file.key,
66+
fileUrl: file.ufsUrl,
67+
};
68+
}),
69+
} satisfies FileRouter;
70+
71+
export type OurFileRouter = typeof ourFileRouter;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createRouteHandler } from "uploadthing/next";
2+
import { ourFileRouter } from "./core";
3+
4+
export const { GET, POST } = createRouteHandler({
5+
router: ourFileRouter,
6+
});

0 commit comments

Comments
 (0)