Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7ca45dd
wip
D-K-P Sep 4, 2025
81a084a
Implemented drag and drop, deleted hello world and other changes
D-K-P Sep 5, 2025
22c1d27
Added more tasks and updated actions
D-K-P Sep 5, 2025
68c2200
Images rendering in the first 4 slots
D-K-P Sep 5, 2025
5e552da
Use flux plus other improvements
D-K-P Sep 5, 2025
3e0906c
Working nicely
D-K-P Sep 5, 2025
208653c
Added prompt card and improved prompts
D-K-P Sep 5, 2025
505d93d
More updates
D-K-P Sep 8, 2025
6b20c47
Some updates
D-K-P Sep 10, 2025
66bed63
Removed v3
D-K-P Sep 10, 2025
38137e1
Mid refactor
D-K-P Sep 10, 2025
cdf8192
Simplified auth
D-K-P Sep 10, 2025
9b1dd9f
Restore nav and layout
D-K-P Sep 10, 2025
1ad322b
Improved loading state
D-K-P Sep 10, 2025
483e575
Added regenerate button
D-K-P Sep 10, 2025
96e8c12
Styling updates
D-K-P Sep 10, 2025
02f588b
More style updates
D-K-P Sep 10, 2025
19440f8
More improvements
D-K-P Sep 10, 2025
901111f
Tweaks
D-K-P Sep 10, 2025
2b5136f
Removed imports and updated button styles
D-K-P Sep 10, 2025
865cd16
Fixed custom prompt card
D-K-P Sep 10, 2025
ec8b42b
Added dl functionality
D-K-P Sep 11, 2025
2b34f9e
Centred content
D-K-P Sep 11, 2025
00f7225
Added prog bar
D-K-P Sep 11, 2025
27b8666
Removed size and model being passed through
D-K-P Sep 11, 2025
1addccd
styled remaining cards
D-K-P Sep 11, 2025
f42d810
Improved task code / task names
D-K-P Sep 12, 2025
c5fcf7d
Improved page UI and layout
D-K-P Sep 12, 2025
7d26aa8
More UI improvements
D-K-P Sep 12, 2025
c00bead
update package.json
D-K-P Sep 12, 2025
4e251be
Use nano-banana directly with Replicate
matt-aitken Sep 12, 2025
115c811
Removed old prompt stuff, better types
matt-aitken Sep 12, 2025
3dec97b
WIP improvements
matt-aitken Sep 12, 2025
8190131
Removed v3, added expirationTime
D-K-P Sep 15, 2025
492c397
Added example env file
D-K-P Sep 15, 2025
e5a39a8
Added readme
D-K-P Sep 15, 2025
3e5c3cc
Readme improvements
D-K-P Sep 15, 2025
f94368f
added example to the main table
D-K-P Sep 15, 2025
bf14fd6
Updated messaging
D-K-P Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ For information on how to run each project, see the README in each directory.
| [Mastra agents](/mastra-agents) | Example of using [Mastra](https://github.com/mastra-ai/mastra) agents with Trigger.dev |
| [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 |
| [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 |
| [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 |
| [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) |
| [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/) |
| [Python image processing](/python-image-processing) | Python-based ([Pillow](https://pillow.readthedocs.io/en/stable/)) image processing tasks and uploading to S3-compatible storage |
Expand Down
43 changes: 43 additions & 0 deletions product-image-generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

.trigger
102 changes: 102 additions & 0 deletions product-image-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Product Image Generator using Trigger.dev and Replicate

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.

## Tech stack

- [**Next.js**](https://nextjs.org/) – frontend React framework
- [**Replicate**](https://replicate.com/docs) – AI image generation
- [**Trigger.dev**](https://trigger.dev/docs) – background task orchestration
- [**UploadThing**](https://uploadthing.com/) – file upload handling
- [**Cloudflare R2**](https://developers.cloudflare.com/r2/) – image storage

## Setup & Running locally

1. **Clone the repository**

```bash
git clone <repository-url>
cd product-image-generator
```

2. **Install dependencies**

```bash
pnpm install
```

3. **Copy environment variables and configure**

```bash
cp env.example .env
```

Fill in the required variables:

- `TRIGGER_SECRET_KEY` – Get from [Trigger.dev dashboard](https://cloud.trigger.dev/)
- `REPLICATE_API_TOKEN` – Get from [Replicate](https://replicate.com/account/api-tokens)
- `UPLOADTHING_TOKEN` – Get from [UploadThing](https://uploadthing.com/)
- `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_PUBLIC_URL` – Configure Cloudflare R2 storage

4. **Add Trigger.dev project reference**

Update `trigger.config.ts` with your project ref:

```typescript
project: "your_project_ref_here";
```

5. **Start development servers**

```bash
# Terminal 1: Start Next.js dev server
pnpm dev

# Terminal 2: Start Trigger.dev CLI
npx trigger.dev@latest dev
```

## How it works

Trigger.dev orchestrates the image generation workflow through two main tasks:

1. **`generateImages`** – Batch coordinator that triggers multiple individual image generations ([`app/trigger/generate-images.ts`](app/trigger/generate-images.ts))
2. **`generateImage`** – Individual image processor that:
- Enhances prompts with style-specific instructions
- Calls Replicate's `google/nano-banana` model
- Creates waitpoint tokens for async webhook handling
- Waits for Replicate completion via webhook callbacks
- Uploads generated images to Cloudflare R2
- Returns public URLs for display

**Process flow:**

1. User selects and uploads product image to the website
2. Image is uploaded to UploadThing cloud storage
3. UploadThing's `onUploadComplete` callback triggers batch generation for 3 preset styles
4. Each style runs as separate Trigger.dev task with waitpoints for Replicate webhooks
5. Frontend receives real-time progress updates via Trigger.dev React hooks
6. Generated images are stored in Cloudflare R2 and displayed with download options

**Style presets:**

- **Clean Product Shot** – Professional white background with studio lighting
- **Lifestyle Scene** – Person holding product with natural lighting
- **Hero Shot** – Elegant hands presenting product with dramatic lighting

## Relevant code

- **Image generation tasks** – Batch processing with waitpoints for Replicate webhook callbacks ([`app/trigger/generate-images.ts`](app/trigger/generate-images.ts))
- **Upload handler** – UploadThing integration that triggers batch generation for 3 preset styles ([`app/api/uploadthing/core.ts`](app/api/uploadthing/core.ts))
- **Real-time progress UI** – Live task updates using Trigger.dev React hooks ([`app/components/GeneratedCard.tsx`](app/components/GeneratedCard.tsx))
- **Custom prompt interface** – User-defined style generation with custom prompts ([`app/components/CustomPromptCard.tsx`](app/components/CustomPromptCard.tsx))
- **Main app component** – Layout and state management with professional style presets ([`app/ProductImageGenerator.tsx`](app/ProductImageGenerator.tsx))
- **Trigger.dev configuration** – Project settings and task directories ([`trigger.config.ts`](trigger.config.ts))

## Learn more

- [**Trigger.dev waitpoints**](https://trigger.dev/docs/wait-for-token) – pause tasks for async webhook callbacks
- [**Trigger.dev React hooks**](https://trigger.dev/docs/frontend/react-hooks) – real-time task updates and frontend integration
- [**Trigger.dev batch operations**](https://trigger.dev/docs/tasks/batch-trigger) – parallel task execution patterns
- [**Replicate API**](https://replicate.com/docs/get-started/nextjs) – AI model integration and webhook handling
- [**UploadThing**](https://docs.uploadthing.com/) – file upload handling and server callbacks
118 changes: 118 additions & 0 deletions product-image-generator/app/ProductImageGenerator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client";

import { Download, Home, Settings, User, WandSparklesIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { GeneratedCard } from "./components/GeneratedCard";
import { Button } from "./components/ui/button";
import { UploadCard } from "./components/UploadCard";
import CustomPromptCard from "./components/CustomPromptCard";
import Link from "next/link";

const promptTitles = {
"isolated-table": "Clean Product Shot",
"lifestyle-scene": "Lifestyle Scene",
"hero-shot": "Hero Shot",
};

export function ProductImageGenerator() {
const searchParams = useSearchParams();
const publicAccessToken = searchParams.get("publicAccessToken");
const generateToken = searchParams.get("triggerToken");
const fileUrl = searchParams.get("fileUrl");
const runId = searchParams.get("runId");

return (
<div className="min-h-screen bg-gray-100/20 ">
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4">
<div className="flex h-14 items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Link href="/" className="flex items-center space-x-2">
<WandSparklesIcon className="h-5 w-5 text-purple-500" />
</Link>
<h1 className="text-xl font-bold text-foreground">ImageFlow</h1>
</div>
</div>

<nav className="flex items-center space-x-1">
<Button variant="ghost" size="sm">
<Home className="h-4 w-4 mr-1 text-gray-500" />
Home
</Button>
<Button variant="ghost" size="sm">
<User className="h-4 w-4 mr-1 text-gray-500" />
Account
</Button>
<Button variant="ghost" size="sm">
<Settings className="h-4 w-4 mr-1 text-gray-500" />
Settings
</Button>
</nav>
</div>
</div>
</header>

<main className="container px-4 py-16 w-full mx-auto ">
<div className="max-w-7xl mx-auto w">
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a stray 'w' character at the end of this div's className. This should be removed as it's not a valid Tailwind class.

Suggested change
<div className="max-w-7xl mx-auto w">
<div className="max-w-7xl mx-auto">

Copilot uses AI. Check for mistakes.
<div className="mb-8 flex justify-between items-end gap-8">
<div>
<h2 className="text-3xl font-bold text-foreground mb-2">
Product Image Generator
</h2>
<p className="text-muted-foreground">
Upload a product image and generate professional marketing shots
for your online store.
</p>
</div>
<div>
<Button variant={"default"} className={"cursor-pointer"}>
<Download className="h-4 w-4 mr-1" />
Download images
</Button>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<UploadCard
runId={runId ?? undefined}
accessToken={publicAccessToken ?? undefined}
fileUrl={fileUrl ?? undefined}
/>
<GeneratedCard
id="isolated-table"
runId={runId ?? undefined}
accessToken={publicAccessToken ?? undefined}
promptTitle={promptTitles["isolated-table"]}
/>
<GeneratedCard
id="lifestyle-scene"
runId={runId ?? undefined}
accessToken={publicAccessToken ?? undefined}
promptTitle={promptTitles["lifestyle-scene"]}
/>
<GeneratedCard
id="hero-shot"
runId={runId ?? undefined}
accessToken={publicAccessToken ?? undefined}
promptTitle={promptTitles["hero-shot"]}
/>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, index) => {
return (
<CustomPromptCard
id={`custom-prompt-${index}`}
key={`custom-prompt-${index}`}
fileUrl={fileUrl ?? undefined}
generateToken={generateToken ?? undefined}
/>
);
})}
</div>
</div>
</main>
</div>
);
}
71 changes: 71 additions & 0 deletions product-image-generator/app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { randomUUID } from "crypto";
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
import type { generateImages } from "@/trigger/generate-images";
import { auth, tasks } from "@trigger.dev/sdk";

const f = createUploadthing();

const mockAuth = (req: Request) => ({ id: randomUUID() }); // Fake auth function

// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
imageUploader: f({ image: { maxFileSize: "4MB" } })
// Set permissions and file types for this FileRoute
.middleware(async ({ req }) => {
// This code runs on your server before upload
const user = await mockAuth(req);

// If you throw, the user will not be able to upload
if (!user) throw new UploadThingError("Unauthorized");

// Whatever is returned here is accessible in onUploadComplete as `metadata`
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
// This code RUNS ON YOUR SERVER after upload

const { id, publicAccessToken } = await tasks.trigger<
typeof generateImages
>("generate-images", {
images: [
{
id: "isolated-table",
baseImageUrl: file.ufsUrl,
promptStyle: "isolated-table",
},
{
id: "lifestyle-scene",
baseImageUrl: file.ufsUrl,
promptStyle: "lifestyle-scene",
},
{
id: "hero-shot",
baseImageUrl: file.ufsUrl,
promptStyle: "hero-shot",
},
],
});

const triggerToken = await auth.createTriggerPublicToken(
"generate-image",
{
expirationTime: "20m",
multipleUse: true, // not recommended without an expiration time
},
);

// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return {
uploadedBy: metadata.userId,
publicAccessToken,
triggerToken,
runId: id,
fileId: file.key,
fileUrl: file.ufsUrl,
};
}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
6 changes: 6 additions & 0 deletions product-image-generator/app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";

export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});
Loading