├── .eslintrc.json ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── app ├── api │ └── generate-image │ │ └── route.ts ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── tailwind.config.ts ├── tsconfig.json └── wrangler.toml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Cloudflare Workers 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: Deploy 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '20' 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Build project 21 | run: npm run build:worker 22 | - name: Publish to Cloudflare 23 | uses: cloudflare/wrangler-action@v3 24 | with: 25 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .worker-next 39 | .dev.vars 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast Flux Demo 2 | 3 | A Next.js app that shows off how fast Flux is on Replicate. 4 | 5 | Check it out at [fast-flux-demo.replicate.workers.dev](https://fast-flux-demo.replicate.workers.dev) 6 | 7 | ## Running locally 8 | 9 | To run this app locally or deploy your own copy, you'll need: 10 | 11 | - Node.js (20 or later) 12 | - A [Replicate API token](https://replicate.com/account/api-tokens) 13 | 14 | Set your Replicate API token in your environment: 15 | 16 | ```sh 17 | export REPLICATE_API_TOKEN=r8_... 18 | ``` 19 | 20 | Then install dependencies and run the development server: 21 | 22 | ``` 23 | npm install && npm run dev 24 | ``` 25 | 26 | Then open [localhost:3000](http://localhost:3000) in your browser to see the app. 27 | 28 | ## Continuous Deployment 29 | 30 | This project uses GitHub Actions for continuous deployment. Every push to the `main` branch triggers a workflow that builds and deploys the site to Cloudflare Workers. 31 | 32 | The deployment process is defined in `.github/workflows/deploy.yml`. It uses the Wrangler GitHub Action to publish the site to Cloudflare. 33 | 34 | To set up deployment for your fork of this project: 35 | 36 | 1. Generate a Cloudflare API token with the necessary permissions. 37 | 2. In your GitHub repository settings, go to Secrets and add a new secret named `CLOUDFLARE_API_TOKEN` with your Cloudflare API token as the value. 38 | 39 | Once set up, every push to the `main` branch will automatically deploy your site. -------------------------------------------------------------------------------- /app/api/generate-image/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import Replicate from "replicate"; 3 | 4 | export async function GET(request: Request) { 5 | const replicate = new Replicate(); 6 | const { searchParams } = new URL(request.url); 7 | const prompt = searchParams.get("text"); 8 | 9 | const model = "black-forest-labs/flux-schnell"; 10 | const input = { 11 | prompt, 12 | go_fast: true, 13 | num_outputs: 1, 14 | aspect_ratio: "1:1", 15 | output_format: "webp", 16 | output_quality: 80, 17 | megapixels: "0.25", 18 | num_inference_steps: 2, 19 | }; 20 | 21 | const output = await replicate.run(model, { input }) as string[]; 22 | const headers = new Headers(); 23 | headers.set("Content-Type", "image/*"); 24 | headers.set( 25 | "Cache-Control", 26 | "no-store, no-cache, must-revalidate, proxy-revalidate" 27 | ); 28 | headers.set("Pragma", "no-cache"); 29 | headers.set("Expires", "0"); 30 | return new NextResponse(output[0], { 31 | status: 200, 32 | statusText: "OK", 33 | headers, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replicate/fast-flux-demo/d186e56c045c2298076074e698da7f97f3c427c8/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replicate/fast-flux-demo/d186e56c045c2298076074e698da7f97f3c427c8/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replicate/fast-flux-demo/d186e56c045c2298076074e698da7f97f3c427c8/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | .masonry { 28 | column-gap: 0; 29 | column-count: 1; 30 | } 31 | .masonry-sm { 32 | column-count: 2; 33 | } 34 | .masonry-md { 35 | column-count: 3; 36 | } 37 | .masonry-item { 38 | break-inside: avoid; 39 | margin-bottom: 0; 40 | } 41 | } 42 | 43 | .masonry-item { 44 | opacity: 0; 45 | animation: fadeIn 0.3s ease-out forwards; 46 | } 47 | 48 | @keyframes fadeIn { 49 | to { 50 | opacity: 1; 51 | } 52 | } 53 | 54 | /* Ensure the masonry layout takes up all available space */ 55 | .masonry { 56 | width: 100%; 57 | max-width: 100%; 58 | } 59 | 60 | /* Update the positioning for the label */ 61 | .masonry-item { 62 | position: relative; 63 | } 64 | 65 | .masonry-item > div { 66 | position: absolute; 67 | bottom: 0px; 68 | left: 0px; 69 | width: auto; 70 | } 71 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Fast Flux", 18 | description: "Generate images faster than you can type with Flux on Replicate", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef, useState } from "react"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | 7 | const DEFAULT_INPUT = "super fast pink and gold jet is flying against a beautiful nebula, trail behind it" 8 | 9 | export default function Home() { 10 | const [texts, setTexts] = useState([DEFAULT_INPUT]); 11 | 12 | const onChange = (e: React.ChangeEvent) => { 13 | setTexts([...texts, e.target.value]); 14 | }; 15 | 16 | // Focus the input element when the page loads) 17 | const inputElement = useRef(null); 18 | useEffect(() => { 19 | if (inputElement.current) { 20 | inputElement.current.focus(); 21 | } 22 | }, []); 23 | 24 | return ( 25 |
26 |
27 | e.target.setSelectionRange(0, e.target.value.length)} 35 | /> 36 | 37 | 40 | 41 |
42 |
43 | {texts.map((text, index) => ( 44 | {text} 53 | ))} 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'replicate.delivery', 8 | port: '', 9 | pathname: '/**', 10 | }, 11 | { 12 | protocol: 'https', 13 | hostname: 'via.placeholder.com', 14 | port: '', 15 | pathname: '/**', 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-flux-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "build:worker": "cloudflare", 11 | "dev:worker": "wrangler dev --port 8771", 12 | "preview:worker": "npm run build:worker && npm run dev:worker", 13 | "deploy:worker": "npm run build:worker && wrangler deploy" 14 | }, 15 | "dependencies": { 16 | "next": "14.2.13", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "replicate": "^1.0.0-beta.1" 20 | }, 21 | "devDependencies": { 22 | "@opennextjs/cloudflare": "^0.0.3", 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "eslint": "^8", 27 | "eslint-config-next": "14.2.13", 28 | "postcss": "^8", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5", 31 | "wrangler": "^3.80.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "fast-flux-demo" 3 | main = ".worker-next/index.mjs" 4 | 5 | compatibility_date = "2024-09-26" 6 | compatibility_flags = ["nodejs_compat"] 7 | 8 | # Minification helps to keep the Worker bundle size down and improve start up time. 9 | minify = true 10 | 11 | # Use the new Workers + Assets to host the static frontend files 12 | assets = { directory = ".worker-next/assets", binding = "ASSETS" } 13 | 14 | # Set the appropriate account_id (this is Replicate's account ID) 15 | account_id = "45191806b6bd0ba8191dccddce0b6944" 16 | --------------------------------------------------------------------------------