├── runpod ├── base64 │ └── .placeholder ├── uploadthing │ ├── proxy.js │ ├── test.ipynb │ ├── Dockerfile │ └── worker_runpod.py └── flux-fp8 │ ├── worker_runpod.py │ └── Dockerfile ├── .eslintrc.json ├── next.config.mjs ├── postcss.config.mjs ├── app ├── page.tsx ├── layout.tsx └── globals.css ├── lib └── utils.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── components ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── resizable.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ └── select.tsx └── Canvas.tsx ├── package.json ├── README.md └── tailwind.config.ts /runpod/base64/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Canvas from "@/components/Canvas"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "TostAI Manga Creator - Powered by Runpod", 9 | description: "TostAI Manga Creator - Powered by Runpod", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "@/lib/utils" 3 | 4 | export interface InputProps 5 | extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | Input.displayName = "Input" 23 | 24 | export { Input } -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SliderPrimitive from "@radix-ui/react-slider" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | 25 | )) 26 | Slider.displayName = SliderPrimitive.Root.displayName 27 | 28 | export { Slider } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas", 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 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.1.2", 13 | "@radix-ui/react-label": "^2.1.0", 14 | "@radix-ui/react-select": "^2.1.1", 15 | "@radix-ui/react-slider": "^1.2.0", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "@radix-ui/react-switch": "^1.1.0", 18 | "canvas": "file:", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "html2canvas": "^1.4.1", 22 | "jspdf": "^2.5.1", 23 | "lucide-react": "^0.436.0", 24 | "next": "14.2.6", 25 | "react": "^18", 26 | "react-dom": "^18", 27 | "react-resizable-panels": "^2.1.1", 28 | "react-rnd": "^10.4.12", 29 | "tailwind-merge": "^2.5.2", 30 | "tailwindcss-animate": "^1.0.7" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.2.6", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /runpod/uploadthing/proxy.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async fetch(request, env) { 3 | const allowedOrigin = 'https://manga.tost.ai'; 4 | if (request.method === 'OPTIONS') { 5 | return new Response(null, { 6 | headers: { 7 | 'Access-Control-Allow-Origin': allowedOrigin, 8 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 9 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 10 | }, 11 | }); 12 | } 13 | const origin = request.headers.get('Origin'); 14 | if (origin !== allowedOrigin) { 15 | return new Response('Forbidden', { status: 403 }); 16 | } 17 | if (request.method === 'POST') { 18 | const requestBody = await request.json(); 19 | const url = `https://api.runpod.ai/v2/${env.RUNPOD_ID}/runsync`; 20 | const headers = { 21 | 'Content-Type': 'application/json', 22 | 'Authorization': `Bearer ${env.RUNPOD_KEY}`, 23 | }; 24 | const inResponse = await fetch(url, { 25 | method: 'POST', 26 | headers: headers, 27 | body: JSON.stringify(requestBody), 28 | }); 29 | 30 | if (!inResponse.ok) { 31 | return new Response('500', { status: 500 }); 32 | } 33 | const outResponse = await inResponse.json(); 34 | return new Response(JSON.stringify(outResponse), { 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | 'Access-Control-Allow-Origin': allowedOrigin, 38 | 'Access-Control-Allow-Methods': 'POST', 39 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 40 | }, 41 | }); 42 | } else { 43 | return new Response('400', { status: 400 }); 44 | } 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🐣 Please follow me for new updates https://twitter.com/camenduru
2 | 🔥 Please join our discord server https://discord.gg/k5BwmmvJJU
3 | 🥳 Please join my patreon community https://patreon.com/camenduru
4 | 5 | ### 🌐 Page 6 | https://manga.tost.ai (please wait for the server to wake up) 7 | 8 | ### 🧬 Code 9 | https://github.com/facebook/react
10 | https://github.com/vercel/next.js
11 | https://github.com/aredden/flux-fp8-api
12 | 13 | ### 📋 Tutorial 14 | ``` 15 | npm install -g create-react-app 16 | npx create-next-app@latest canvas --typescript --tailwind --eslint 17 | npm run dev 18 | ``` 19 | 20 | ### 📦 Model 21 | 22 | Thanks to all open-source community model creators ❤
23 | 24 | https://civitai.com/models/802032/black-and-white-anime-pixel-art | bw_pixel_anime_v1.0.safetensors
25 | https://civitai.com/models/801701/zanshou-kin-flux-ueno-manga-style | ueno.safetensors
26 | https://civitai.com/models/744831/immoral-girl-by-kisaragi-gunma | immoralgirl.safetensors
27 | https://civitai.com/models/779854/manga-style-lora-or-flux1 | manga_style_f1d.safetensors
28 | https://civitai.com/models/681642/illustrationscute-cartoon-cute-manga-flux | j_cartoon_flux_bf16.safetensors
29 | https://civitai.com/models/743448/berserk-manga-style-flux-lora | berserk_manga_style_flux.safetensors
30 | https://civitai.com/models/225469/anime-manga-cinematic-ova-style-xl-f1d | Manga_and_Anime_cartoon_style_v1.safetensors
31 | 32 | ### 🖼 Output 33 | 34 | https://github.com/user-attachments/assets/9bd0d728-1dba-449e-8ff5-7e1f2bde1cdc 35 | 36 | https://github.com/user-attachments/assets/9f6fa1a4-2c4f-45eb-86ac-2d5c684fb9df 37 | 38 | ### 🏢 Sponsor 39 | [https://runpod.io](https://runpod.io?ref=iqi9iy8y) 40 | -------------------------------------------------------------------------------- /components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { GripVertical } from "lucide-react" 4 | import * as ResizablePrimitive from "react-resizable-panels" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 19 | ) 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps & { 28 | withHandle?: boolean 29 | }) => ( 30 | div]:rotate-90", 33 | className 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ) 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle } 46 | -------------------------------------------------------------------------------- /runpod/uploadthing/test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "view-in-github" 7 | }, 8 | "source": [ 9 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/camenduru/manga-tost/blob/main/runpod/uploadthing/test.ipynb)" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "vscode": { 17 | "languageId": "plaintext" 18 | } 19 | }, 20 | "outputs": [], 21 | "source": [ 22 | "import requests\n", 23 | "from PIL import Image\n", 24 | "from io import BytesIO\n", 25 | "\n", 26 | "url = 'https://comic.camenduru.workers.dev'\n", 27 | "headers = {\n", 28 | " \"Content-Type\": \"application/json\"\n", 29 | "}\n", 30 | "data = {\n", 31 | " \"input\": {\n", 32 | " \"positive_prompt\": \"Juaner_cartoon,A curious mermaid with long blue hair, wearing a necklace made of seashells, holding a glowing pearl, swimming through an underwater cave filled with shimmering treasures and ancient ruins.\",\n", 33 | " \"seed\": 0,\n", 34 | " \"steps\": 20,\n", 35 | " \"guidance\": 3.5,\n", 36 | " \"lora_file\": \"j_cartoon_flux_bf16.safetensors\",\n", 37 | " \"lora_strength_model\": 1,\n", 38 | " \"lora_strength_clip\": 1,\n", 39 | " \"sampler_name\": \"euler\",\n", 40 | " \"scheduler\": \"simple\",\n", 41 | " \"width\": 1024,\n", 42 | " \"height\": 1024\n", 43 | " }\n", 44 | "}\n", 45 | "response = requests.post(url, headers=headers, json=data)\n", 46 | "image_url = response.json()['output']['result']\n", 47 | "print(image_url)" 48 | ] 49 | } 50 | ], 51 | "metadata": { 52 | "language_info": { 53 | "name": "python" 54 | } 55 | }, 56 | "nbformat": 4, 57 | "nbformat_minor": 2 58 | } 59 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /runpod/uploadthing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM runpod/pytorch:2.2.1-py3.10-cuda12.1.1-devel-ubuntu22.04 2 | WORKDIR /content 3 | ENV PATH="/home/camenduru/.local/bin:${PATH}" 4 | 5 | RUN adduser --disabled-password --gecos '' camenduru && \ 6 | adduser camenduru sudo && \ 7 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ 8 | chown -R camenduru:camenduru /content && \ 9 | chmod -R 777 /content && \ 10 | chown -R camenduru:camenduru /home && \ 11 | chmod -R 777 /home && \ 12 | apt update -y && add-apt-repository -y ppa:git-core/ppa && apt update -y && apt install -y aria2 git git-lfs unzip ffmpeg 13 | 14 | USER camenduru 15 | 16 | RUN pip install -q opencv-python imageio imageio-ffmpeg ffmpeg-python av runpod \ 17 | xformers==0.0.25 torchsde==0.2.6 einops==0.8.0 diffusers==0.28.0 transformers==4.41.2 accelerate==0.30.1 && \ 18 | git clone https://github.com/comfyanonymous/ComfyUI /content/ComfyUI && \ 19 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/flux1-dev.sft -d /content/ComfyUI/models/unet -o flux1-dev.sft && \ 20 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors -d /content/ComfyUI/models/clip -o clip_l.safetensors && \ 21 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp16.safetensors -d /content/ComfyUI/models/clip -o t5xxl_fp16.safetensors && \ 22 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.sft -d /content/ComfyUI/models/vae -o ae.sft && \ 23 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/bw_pixel_anime_v1.0.safetensors -d /content/ComfyUI/models/loras -o bw_pixel_anime_v1.0.safetensors && \ 24 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/ueno.safetensors -d /content/ComfyUI/models/loras -o ueno.safetensors && \ 25 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/immoralgirl.safetensors -d /content/ComfyUI/models/loras -o immoralgirl.safetensors && \ 26 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/manga_style_f1d.safetensors -d /content/ComfyUI/models/loras -o manga_style_f1d.safetensors && \ 27 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/j_cartoon_flux_bf16.safetensors -d /content/ComfyUI/models/loras -o j_cartoon_flux_bf16.safetensors && \ 28 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/berserk_manga_style_flux.safetensors -d /content/ComfyUI/models/loras -o berserk_manga_style_flux.safetensors && \ 29 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/Manga_and_Anime_cartoon_style_v1.safetensors -d /content/ComfyUI/models/loras -o Manga_and_Anime_cartoon_style_v1.safetensors 30 | 31 | COPY ./worker_runpod.py /content/ComfyUI/worker_runpod.py 32 | WORKDIR /content/ComfyUI 33 | CMD python worker_runpod.py -------------------------------------------------------------------------------- /runpod/flux-fp8/worker_runpod.py: -------------------------------------------------------------------------------- 1 | import os, json, requests, mimetypes, hashlib, string, runpod 2 | 3 | import random, time 4 | import torch 5 | from PIL import Image 6 | 7 | from flux_pipeline import FluxPipeline 8 | from util import load_config, ModelVersion 9 | 10 | with torch.inference_mode(): 11 | config = load_config( 12 | ModelVersion.flux_dev, 13 | flux_path="/content/models/unet/flux1-dev.sft", 14 | flux_device="cuda:0", 15 | ae_path="/content/models/vae/ae.sft", 16 | ae_device="cuda:0", 17 | text_enc_path="/content/models/clip", 18 | text_enc_device="cuda:0", 19 | flow_dtype="float16", 20 | text_enc_dtype="bfloat16", 21 | ae_dtype="bfloat16", 22 | compile_extras=True, 23 | compile_blocks=True, 24 | offload_flow=False, 25 | offload_text_enc=False 26 | ) 27 | pipe = FluxPipeline.load_pipeline_from_config(config) 28 | pipe.load_lora(lora_path="/content/models/loras/j_cartoon_flux_bf16.safetensors", scale=1.0) 29 | 30 | def closestNumber(n, m): 31 | q = int(n / m) 32 | n1 = m * q 33 | if (n * m) > 0: 34 | n2 = m * (q + 1) 35 | else: 36 | n2 = m * (q - 1) 37 | if abs(n - n1) < abs(n - n2): 38 | return n1 39 | return n2 40 | 41 | def upload_file_to_uploadthing(file_path): 42 | file_name = os.path.basename(file_path) 43 | _, file_extension = os.path.splitext(file_name) 44 | random_string = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) 45 | md5_hash = hashlib.md5(random_string.encode()).hexdigest() 46 | file_name = md5_hash+file_extension 47 | file_size = os.path.getsize(file_path) 48 | file_type, _ = mimetypes.guess_type(file_path) 49 | with open(file_path, "rb") as file: 50 | file_content = file.read() 51 | file_info = {"name": file_name, "size": file_size, "type": file_type} 52 | uploadthing_api_key = os.getenv('uploadthing_api_key') 53 | headers = {"x-uploadthing-api-key": uploadthing_api_key} 54 | data = {"contentDisposition": "inline", "acl": "public-read", "files": [file_info]} 55 | presigned_response = requests.post("https://api.uploadthing.com/v6/uploadFiles", headers=headers, json=data) 56 | presigned_response.raise_for_status() 57 | presigned = presigned_response.json()["data"][0] 58 | upload_url = presigned["url"] 59 | fields = presigned["fields"] 60 | files = {"file": file_content} 61 | upload_response = requests.post(upload_url, data=fields, files=files) 62 | upload_response.raise_for_status() 63 | return presigned_response, upload_response, file_name 64 | 65 | @torch.inference_mode() 66 | def generate(input): 67 | values = input["input"] 68 | 69 | positive_prompt = values['positive_prompt'] 70 | width = values['width'] 71 | height = values['height'] 72 | seed = values['seed'] 73 | steps = values['steps'] 74 | guidance = values['guidance'] 75 | 76 | if seed == 0: 77 | random.seed(int(time.time())) 78 | seed = random.randint(0, 18446744073709551615) 79 | print(seed) 80 | 81 | positive_prompt = "Juaner_cartoon, " + positive_prompt 82 | image_stream = pipe.generate(prompt=positive_prompt, 83 | width=closestNumber(width, 16), 84 | height=closestNumber(height, 16), 85 | num_steps=steps, 86 | guidance=guidance, 87 | seed=seed, 88 | strength=1.0, 89 | init_image=None) 90 | image = Image.open(image_stream) 91 | image.save("/content/flux-comic-tost.png") 92 | 93 | result = "/content/flux-comic-tost.png" 94 | presigned_response, upload_response, file_name = upload_file_to_uploadthing(result) 95 | image_url = presigned_response.json()['data'][0]['fileUrl'] 96 | return {"file": file_name, "result": image_url, "status": "DONE"} 97 | 98 | runpod.serverless.start({"handler": generate}) -------------------------------------------------------------------------------- /runpod/flux-fp8/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM runpod/pytorch:2.2.1-py3.10-cuda12.1.1-devel-ubuntu22.04 2 | WORKDIR /content 3 | ENV PATH="/home/camenduru/.local/bin:${PATH}" 4 | 5 | RUN adduser --disabled-password --gecos '' camenduru && \ 6 | adduser camenduru sudo && \ 7 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ 8 | chown -R camenduru:camenduru /content && \ 9 | chmod -R 777 /content && \ 10 | chown -R camenduru:camenduru /home && \ 11 | chmod -R 777 /home && \ 12 | apt update -y && add-apt-repository -y ppa:git-core/ppa && apt update -y && apt install -y aria2 git git-lfs unzip ffmpeg 13 | 14 | USER camenduru 15 | 16 | RUN pip install -q opencv-python imageio imageio-ffmpeg ffmpeg-python av runpod \ 17 | torch==2.4.0+cu124 torchvision==0.19.0+cu124 torchaudio==2.4.0+cu124 torchtext==0.18.0 torchdata==0.8.0 --extra-index-url https://download.pytorch.org/whl/cu124 \ 18 | xformers==0.0.27.post2 ninja einops PyTurboJPEG pydantic fastapi bitsandbytes loguru transformers tokenizers sentencepiece click accelerate quanto pydash pybase64 uvicorn \ 19 | https://github.com/camenduru/wheels/releases/download/runpod/cublas_ops-0.0.5-cp310-cp310-linux_x86_64.whl && \ 20 | git clone https://github.com/aredden/flux-fp8-api /content/flux-fp8-api && \ 21 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/flux1-dev.sft -d /content/models/unet -o flux1-dev.sft && \ 22 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/raw/main/config.json -d /content/models/clip -o config.json && \ 23 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/resolve/main/model.safetensors -d /content/models/clip -o model.safetensors && \ 24 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/raw/main/special_tokens_map.json -d /content/models/clip -o special_tokens_map.json && \ 25 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/resolve/main/spiece.model -d /content/models/clip -o spiece.model && \ 26 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/raw/main/tokenizer_config.json -d /content/models/clip -o tokenizer_config.json && \ 27 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.sft -d /content/models/vae -o ae.sft && \ 28 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/bw_pixel_anime_v1.0.safetensors -d /content/models/loras -o bw_pixel_anime_v1.0.safetensors && \ 29 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/ueno.safetensors -d /content/models/loras -o ueno.safetensors && \ 30 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/immoralgirl.safetensors -d /content/models/loras -o immoralgirl.safetensors && \ 31 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/manga_style_f1d.safetensors -d /content/models/loras -o manga_style_f1d.safetensors && \ 32 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/j_cartoon_flux_bf16.safetensors -d /content/models/loras -o j_cartoon_flux_bf16.safetensors && \ 33 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/berserk_manga_style_flux.safetensors -d /content/models/loras -o berserk_manga_style_flux.safetensors && \ 34 | aria2c --console-log-level=error -c -x 16 -s 16 -k 1M https://huggingface.co/TostAI/flux-1-dev-lora/resolve/main/Manga_and_Anime_cartoon_style_v1.safetensors -d /content/models/loras -o Manga_and_Anime_cartoon_style_v1.safetensors 35 | 36 | COPY ./worker_runpod.py /content/flux-fp8-api/worker_runpod.py 37 | WORKDIR /content/flux-fp8-api 38 | CMD python worker_runpod.py -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /runpod/uploadthing/worker_runpod.py: -------------------------------------------------------------------------------- 1 | import os, json, requests, mimetypes, hashlib, string, runpod 2 | 3 | import random, time 4 | import torch 5 | import numpy as np 6 | from PIL import Image 7 | import nodes 8 | from nodes import NODE_CLASS_MAPPINGS 9 | from comfy_extras import nodes_custom_sampler 10 | from comfy_extras import nodes_flux 11 | from comfy import model_management 12 | 13 | DualCLIPLoader = NODE_CLASS_MAPPINGS["DualCLIPLoader"]() 14 | UNETLoader = NODE_CLASS_MAPPINGS["UNETLoader"]() 15 | VAELoader = NODE_CLASS_MAPPINGS["VAELoader"]() 16 | 17 | LoraLoader = NODE_CLASS_MAPPINGS["LoraLoader"]() 18 | FluxGuidance = nodes_flux.NODE_CLASS_MAPPINGS["FluxGuidance"]() 19 | RandomNoise = nodes_custom_sampler.NODE_CLASS_MAPPINGS["RandomNoise"]() 20 | BasicGuider = nodes_custom_sampler.NODE_CLASS_MAPPINGS["BasicGuider"]() 21 | KSamplerSelect = nodes_custom_sampler.NODE_CLASS_MAPPINGS["KSamplerSelect"]() 22 | BasicScheduler = nodes_custom_sampler.NODE_CLASS_MAPPINGS["BasicScheduler"]() 23 | SamplerCustomAdvanced = nodes_custom_sampler.NODE_CLASS_MAPPINGS["SamplerCustomAdvanced"]() 24 | VAELoader = NODE_CLASS_MAPPINGS["VAELoader"]() 25 | VAEDecode = NODE_CLASS_MAPPINGS["VAEDecode"]() 26 | EmptyLatentImage = NODE_CLASS_MAPPINGS["EmptyLatentImage"]() 27 | 28 | with torch.inference_mode(): 29 | clip = DualCLIPLoader.load_clip("t5xxl_fp16.safetensors", "clip_l.safetensors", "flux")[0] 30 | unet = UNETLoader.load_unet("flux1-dev.sft", "default")[0] 31 | vae = VAELoader.load_vae("ae.sft")[0] 32 | 33 | def closestNumber(n, m): 34 | q = int(n / m) 35 | n1 = m * q 36 | if (n * m) > 0: 37 | n2 = m * (q + 1) 38 | else: 39 | n2 = m * (q - 1) 40 | if abs(n - n1) < abs(n - n2): 41 | return n1 42 | return n2 43 | 44 | def upload_file_to_uploadthing(file_path): 45 | file_name = os.path.basename(file_path) 46 | _, file_extension = os.path.splitext(file_name) 47 | random_string = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) 48 | md5_hash = hashlib.md5(random_string.encode()).hexdigest() 49 | file_name = md5_hash+file_extension 50 | file_size = os.path.getsize(file_path) 51 | file_type, _ = mimetypes.guess_type(file_path) 52 | with open(file_path, "rb") as file: 53 | file_content = file.read() 54 | file_info = {"name": file_name, "size": file_size, "type": file_type} 55 | uploadthing_api_key = os.getenv('uploadthing_api_key') 56 | headers = {"x-uploadthing-api-key": uploadthing_api_key} 57 | data = {"contentDisposition": "inline", "acl": "public-read", "files": [file_info]} 58 | presigned_response = requests.post("https://api.uploadthing.com/v6/uploadFiles", headers=headers, json=data) 59 | presigned_response.raise_for_status() 60 | presigned = presigned_response.json()["data"][0] 61 | upload_url = presigned["url"] 62 | fields = presigned["fields"] 63 | files = {"file": file_content} 64 | upload_response = requests.post(upload_url, data=fields, files=files) 65 | upload_response.raise_for_status() 66 | return presigned_response, upload_response, file_name 67 | 68 | @torch.inference_mode() 69 | def generate(input): 70 | values = input["input"] 71 | 72 | positive_prompt = values['positive_prompt'] 73 | width = values['width'] 74 | height = values['height'] 75 | seed = values['seed'] 76 | steps = values['steps'] 77 | guidance = values['guidance'] 78 | lora_strength_model = values['lora_strength_model'] 79 | lora_strength_clip = values['lora_strength_clip'] 80 | sampler_name = values['sampler_name'] 81 | scheduler = values['scheduler'] 82 | lora_file = values['lora_file'] 83 | 84 | if seed == 0: 85 | random.seed(int(time.time())) 86 | seed = random.randint(0, 18446744073709551615) 87 | print(seed) 88 | 89 | global unet, clip 90 | 91 | if lora_file == "None": 92 | cond, pooled = clip.encode_from_tokens(clip.tokenize(positive_prompt), return_pooled=True) 93 | cond = [[cond, {"pooled_output": pooled}]] 94 | cond = FluxGuidance.append(cond, guidance)[0] 95 | noise = RandomNoise.get_noise(seed)[0] 96 | guider = BasicGuider.get_guider(unet, cond)[0] 97 | sampler = KSamplerSelect.get_sampler(sampler_name)[0] 98 | sigmas = BasicScheduler.get_sigmas(unet, scheduler, steps, 1.0)[0] 99 | else: 100 | if lora_file == "bw_pixel_anime_v1.0.safetensors": 101 | positive_prompt = "bw_pixel_anime, " + positive_prompt 102 | elif lora_file == "ueno.safetensors": 103 | positive_prompt = "Ueno, a black and white drawing of, " + positive_prompt 104 | elif lora_file == "immoralgirl.safetensors": 105 | positive_prompt = "immoralgirl, black and white manga page, " + positive_prompt 106 | elif lora_file == "manga_style_f1d.safetensors": 107 | positive_prompt = "Black-and-white manga scene, " + positive_prompt 108 | elif lora_file == "j_cartoon_flux_bf16.safetensors": 109 | positive_prompt = "Juaner_cartoon, " + positive_prompt 110 | elif lora_file == "berserk_manga_style_flux.safetensors": 111 | positive_prompt = "berserk style, " + positive_prompt 112 | elif lora_file == "Manga_and_Anime_cartoon_style_v1.safetensors": 113 | positive_prompt = "Manga and Anime cartoon style, " + positive_prompt 114 | unet_lora, clip_lora = LoraLoader.load_lora(unet, clip, lora_file, lora_strength_model, lora_strength_clip) 115 | cond, pooled = clip_lora.encode_from_tokens(clip_lora.tokenize(positive_prompt), return_pooled=True) 116 | cond = [[cond, {"pooled_output": pooled}]] 117 | cond = FluxGuidance.append(cond, guidance)[0] 118 | noise = RandomNoise.get_noise(seed)[0] 119 | guider = BasicGuider.get_guider(unet_lora, cond)[0] 120 | sampler = KSamplerSelect.get_sampler(sampler_name)[0] 121 | sigmas = BasicScheduler.get_sigmas(unet_lora, scheduler, steps, 1.0)[0] 122 | 123 | latent_image = EmptyLatentImage.generate(closestNumber(width, 16), closestNumber(height, 16))[0] 124 | sample, sample_denoised = SamplerCustomAdvanced.sample(noise, guider, sampler, sigmas, latent_image) 125 | decoded = VAEDecode.decode(vae, sample)[0].detach() 126 | Image.fromarray(np.array(decoded*255, dtype=np.uint8)[0]).save("/content/flux-comic-tost.png") 127 | 128 | result = "/content/flux-comic-tost.png" 129 | presigned_response, upload_response, file_name = upload_file_to_uploadthing(result) 130 | image_url = presigned_response.json()['data'][0]['fileUrl'] 131 | return {"file": file_name, "result": image_url, "status": "DONE"} 132 | 133 | runpod.serverless.start({"handler": generate}) -------------------------------------------------------------------------------- /components/Canvas.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useState, useRef, useCallback, useEffect } from 'react' 4 | import { Rnd } from 'react-rnd' 5 | import { ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Plus, Trash2, Image as ImageIcon, ZoomIn, ZoomOut, RefreshCw, Printer, Download, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Save } from 'lucide-react' 6 | import { Button } from "@/components/ui/button" 7 | import { Card } from "@/components/ui/card" 8 | import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "@/components/ui/resizable" 9 | import { Switch } from "@/components/ui/switch" 10 | import { Slider } from "@/components/ui/slider" 11 | import { Input } from "@/components/ui/input" 12 | import { Label } from "@/components/ui/label" 13 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" 14 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 15 | 16 | import html2canvas from 'html2canvas' 17 | import jsPDF from 'jspdf' 18 | 19 | interface BubbleState { 20 | id: string 21 | text: string 22 | fontSize: number 23 | fontFamily: string 24 | arrowPosition: number 25 | arrowRotation: number 26 | arrowOffsetX: number 27 | arrowOffsetY: number 28 | position: { x: number; y: number } 29 | size: { width: number; height: number } 30 | panelIndex: number 31 | arrowColor: 'white' | 'black' 32 | arrowSize: number 33 | } 34 | 35 | interface ImageState { 36 | src: string 37 | zoom: number 38 | x: number 39 | y: number 40 | } 41 | 42 | function SpeechBubble({ 43 | state, 44 | onSelect, 45 | onStateChange, 46 | onDelete, 47 | isPrinting 48 | }: { 49 | state: BubbleState 50 | onSelect: (id: string) => void 51 | onStateChange: (id: string, newState: Partial) => void 52 | onDelete: (id: string) => void 53 | isPrinting: boolean 54 | }) { 55 | const { id, text, fontSize, fontFamily, arrowPosition, arrowRotation, arrowOffsetX, arrowOffsetY, position, size, arrowColor, arrowSize } = state 56 | const textareaRef = useRef(null) 57 | const bubbleRef = useRef(null) 58 | const [isDraggingArrow, setIsDraggingArrow] = useState(false) 59 | 60 | useEffect(() => { 61 | if (textareaRef.current) { 62 | textareaRef.current.style.fontSize = `${fontSize}px` 63 | textareaRef.current.style.fontFamily = fontFamily 64 | } 65 | }, [fontSize, fontFamily]) 66 | 67 | const updateState = (newState: Partial) => { 68 | onStateChange(id, newState) 69 | } 70 | 71 | const handleArrowDrag = (e: React.MouseEvent) => { 72 | if (!bubbleRef.current || !isDraggingArrow) return 73 | 74 | const rect = bubbleRef.current.getBoundingClientRect() 75 | const bubbleWidth = rect.width 76 | const bubbleHeight = rect.height 77 | const perimeter = 2 * (bubbleWidth + bubbleHeight) 78 | 79 | const mouseX = e.clientX - rect.left 80 | const mouseY = e.clientY - rect.top 81 | 82 | let distance = 0 83 | 84 | if (mouseY < 0) { 85 | distance = mouseX 86 | } else if (mouseX > bubbleWidth) { 87 | distance = bubbleWidth + mouseY 88 | } else if (mouseY > bubbleHeight) { 89 | distance = 2 * bubbleWidth + bubbleHeight - mouseX 90 | } else if (mouseX < 0) { 91 | distance = 2 * (bubbleWidth + bubbleHeight) - mouseY 92 | } 93 | 94 | const newPosition = Math.max(0, Math.min(1, distance / perimeter)) 95 | updateState({ arrowPosition: newPosition }) 96 | } 97 | 98 | const getArrowStyle = (): React.CSSProperties => { 99 | const baseStyle: React.CSSProperties = { 100 | width: '0', 101 | height: '0', 102 | borderStyle: 'solid', 103 | position: 'absolute', 104 | zIndex: -1, 105 | cursor: 'move', 106 | } 107 | 108 | const perimeter = 2 * (size.width + size.height) 109 | const position = arrowPosition * perimeter 110 | 111 | let x = 0 112 | let y = 0 113 | let rotation = arrowRotation 114 | 115 | if (position < size.width) { 116 | x = position 117 | y = -arrowSize 118 | rotation += 0 119 | } else if (position < size.width + size.height) { 120 | x = size.width - (arrowSize / 1.5) 121 | y = position - size.width 122 | rotation += 90 123 | } else if (position < 2 * size.width + size.height) { 124 | x = 2 * size.width + size.height - position - arrowSize 125 | y = size.height - (arrowSize * 1.1) 126 | rotation += 180 127 | } else { 128 | x = -(arrowSize / 2) 129 | y = perimeter - position - arrowSize 130 | rotation += 270 131 | } 132 | 133 | return { 134 | ...baseStyle, 135 | left: x + arrowOffsetX, 136 | top: y + arrowOffsetY, 137 | borderWidth: `0 ${arrowSize / 2}px ${arrowSize}px ${arrowSize / 2}px`, 138 | borderColor: `transparent transparent ${arrowColor} transparent`, 139 | transform: `rotate(${rotation}deg)`, 140 | transformOrigin: '50% 100%', 141 | } 142 | } 143 | 144 | return ( 145 | updateState({ position: { x: d.x, y: d.y } })} 149 | onResize={(e, direction, ref, delta, position) => { 150 | updateState({ 151 | size: { width: ref.offsetWidth, height: ref.offsetHeight }, 152 | position 153 | }) 154 | }} 155 | bounds="parent" 156 | minWidth={100} 157 | minHeight={50} 158 | onClick={() => onSelect(id)} 159 | > 160 |
setIsDraggingArrow(false)} 165 | onMouseLeave={() => setIsDraggingArrow(false)} 166 | > 167 |