-
Notifications
You must be signed in to change notification settings - Fork 16
Added product image generator #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
7ca45dd
wip
D-K-P 81a084a
Implemented drag and drop, deleted hello world and other changes
D-K-P 22c1d27
Added more tasks and updated actions
D-K-P 68c2200
Images rendering in the first 4 slots
D-K-P 5e552da
Use flux plus other improvements
D-K-P 3e0906c
Working nicely
D-K-P 208653c
Added prompt card and improved prompts
D-K-P 505d93d
More updates
D-K-P 6b20c47
Some updates
D-K-P 66bed63
Removed v3
D-K-P 38137e1
Mid refactor
D-K-P cdf8192
Simplified auth
D-K-P 9b1dd9f
Restore nav and layout
D-K-P 1ad322b
Improved loading state
D-K-P 483e575
Added regenerate button
D-K-P 96e8c12
Styling updates
D-K-P 02f588b
More style updates
D-K-P 19440f8
More improvements
D-K-P 901111f
Tweaks
D-K-P 2b5136f
Removed imports and updated button styles
D-K-P 865cd16
Fixed custom prompt card
D-K-P ec8b42b
Added dl functionality
D-K-P 2b34f9e
Centred content
D-K-P 00f7225
Added prog bar
D-K-P 27b8666
Removed size and model being passed through
D-K-P 1addccd
styled remaining cards
D-K-P f42d810
Improved task code / task names
D-K-P c5fcf7d
Improved page UI and layout
D-K-P 7d26aa8
More UI improvements
D-K-P c00bead
update package.json
D-K-P 4e251be
Use nano-banana directly with Replicate
matt-aitken 115c811
Removed old prompt stuff, better types
matt-aitken 3dec97b
WIP improvements
matt-aitken 8190131
Removed v3, added expirationTime
D-K-P 492c397
Added example env file
D-K-P e5a39a8
Added readme
D-K-P 3e5c3cc
Readme improvements
D-K-P f94368f
added example to the main table
D-K-P bf14fd6
Updated messaging
D-K-P File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"> | ||
| <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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.