Repository: Nutlope/roomGPT Branch: main Commit: 611398c78da6 Files: 27 Total size: 37.8 KB Directory structure: gitextract_z9xg4qej/ ├── .example.env ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── dream/ │ │ └── page.tsx │ ├── generate/ │ │ └── route.ts │ ├── layout.tsx │ └── page.tsx ├── components/ │ ├── CompareSlider.tsx │ ├── DropDown.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── LoadingDots.tsx │ ├── ResizablePanel.tsx │ ├── SquigglyLines.tsx │ └── Toggle.tsx ├── next.config.js ├── package.json ├── postcss.config.js ├── styles/ │ ├── globals.css │ └── loading-dots.module.css ├── tailwind.config.js ├── tsconfig.json └── utils/ ├── appendNewToName.ts ├── downloadPhoto.ts ├── dropdownTypes.ts └── redis.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .example.env ================================================ REPLICATE_API_KEY= NEXT_PUBLIC_UPLOAD_API_KEY= # Optional, if you're doing rate limiting UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # 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* # local env files .env*.local # vercel .vercel /.vscode # typescript *.tsbuildinfo next-env.d.ts .env ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Hassan El Mghari Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # [RoomGPT](https://roomGPT.io) - redesign your room with AI This is the previous and open source version of RoomGPT.io (a paid SaaS product). It's the very first version of roomGPT without the auth, payments, or additional features and it's simple to clone, deploy, and play around with. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT) [![Room GPT](./public/screenshot.png)](https://roomGPT.io) ## How it works It uses an ML model called [ControlNet](https://github.com/lllyasviel/ControlNet) to generate variations of rooms. This application gives you the ability to upload a photo of any room, which will send it through this ML Model using a Next.js API route, and return your generated room. The ML Model is hosted on [Replicate](https://replicate.com) and [Bytescale](https://www.bytescale.com/) is used for image storage. ## Running Locally ### Cloning the repository the local machine. ```bash git clone https://github.com/Nutlope/roomGPT ``` ### Creating a account on Replicate to get an API key. 1. Go to [Replicate](https://replicate.com/) to make an account. 2. Click on your profile picture in the top left corner, and click on "API Tokens". 3. Here you can find your API token. Copy it. ### Storing the API keys in .env Create a file in root directory of project with env. And store your API key in it, as shown in the .example.env file. If you'd also like to do rate limiting, create an account on UpStash, create a Redis database, and populate the two environment variables in `.env` as well. If you don't want to do rate limiting, you don't need to make any changes. ### Installing the dependencies. ```bash npm install ``` ### Running the application. Then, run the application in the command line and it will be available at `http://localhost:3000`. ```bash npm run dev ``` ## One-Click Deploy Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT) ## License This repo is MIT licensed. ================================================ FILE: app/dream/page.tsx ================================================ "use client"; import { AnimatePresence, motion } from "framer-motion"; import Image from "next/image"; import { useState } from "react"; import { UrlBuilder } from "@bytescale/sdk"; import { UploadWidgetConfig } from "@bytescale/upload-widget"; import { UploadDropzone } from "@bytescale/upload-widget-react"; import { CompareSlider } from "../../components/CompareSlider"; import Footer from "../../components/Footer"; import Header from "../../components/Header"; import LoadingDots from "../../components/LoadingDots"; import ResizablePanel from "../../components/ResizablePanel"; import Toggle from "../../components/Toggle"; import appendNewToName from "../../utils/appendNewToName"; import downloadPhoto from "../../utils/downloadPhoto"; import DropDown from "../../components/DropDown"; import { roomType, rooms, themeType, themes } from "../../utils/dropdownTypes"; const options: UploadWidgetConfig = { apiKey: !!process.env.NEXT_PUBLIC_UPLOAD_API_KEY ? process.env.NEXT_PUBLIC_UPLOAD_API_KEY : "free", maxFileCount: 1, mimeTypes: ["image/jpeg", "image/png", "image/jpg"], editor: { images: { crop: false } }, styles: { colors: { primary: "#2563EB", // Primary buttons & links error: "#d23f4d", // Error messages shade100: "#fff", // Standard text shade200: "#fffe", // Secondary button text shade300: "#fffd", // Secondary button text (hover) shade400: "#fffc", // Welcome text shade500: "#fff9", // Modal close button shade600: "#fff7", // Border shade700: "#fff2", // Progress indicator background shade800: "#fff1", // File item background shade900: "#ffff", // Various (draggable crop buttons, etc.) }, }, }; export default function DreamPage() { const [originalPhoto, setOriginalPhoto] = useState(null); const [restoredImage, setRestoredImage] = useState(null); const [loading, setLoading] = useState(false); const [restoredLoaded, setRestoredLoaded] = useState(false); const [sideBySide, setSideBySide] = useState(false); const [error, setError] = useState(null); const [photoName, setPhotoName] = useState(null); const [theme, setTheme] = useState("Modern"); const [room, setRoom] = useState("Living Room"); const UploadDropZone = () => ( { if (uploadedFiles.length !== 0) { const image = uploadedFiles[0]; const imageName = image.originalFile.originalFileName; const imageUrl = UrlBuilder.url({ accountId: image.accountId, filePath: image.filePath, options: { transformation: "preset", transformationPreset: "thumbnail" } }); setPhotoName(imageName); setOriginalPhoto(imageUrl); generatePhoto(imageUrl); } }} width="670px" height="250px" /> ); async function generatePhoto(fileUrl: string) { await new Promise((resolve) => setTimeout(resolve, 200)); setLoading(true); const res = await fetch("/generate", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ imageUrl: fileUrl, theme, room }), }); let newPhoto = await res.json(); if (res.status !== 200) { setError(newPhoto); } else { setRestoredImage(newPhoto[1]); } setTimeout(() => { setLoading(false); }, 1300); } return (

Generate your dream room

{!restoredImage && ( <>
1 icon

Choose your room theme.

setTheme(newTheme as typeof theme) } themes={themes} />
1 icon

Choose your room type.

setRoom(newRoom as typeof room)} themes={rooms} />
1 icon

Upload a picture of your room.

)} {restoredImage && (
Here's your remodeled {room.toLowerCase()} in the{" "} {theme.toLowerCase()} theme!{" "}
)}
setSideBySide(newVal)} />
{restoredLoaded && sideBySide && ( )} {!originalPhoto && } {originalPhoto && !restoredImage && ( original photo )} {restoredImage && originalPhoto && !sideBySide && (

Original Room

original photo
)} {loading && ( )} {error && (
{error}
)}
{originalPhoto && !loading && ( )} {restoredLoaded && ( )}
); } ================================================ FILE: app/generate/route.ts ================================================ import { Ratelimit } from "@upstash/ratelimit"; import redis from "../../utils/redis"; import { NextResponse } from "next/server"; import { headers } from "next/headers"; // Create a new ratelimiter, that allows 5 requests per 24 hours const ratelimit = redis ? new Ratelimit({ redis: redis, limiter: Ratelimit.fixedWindow(5, "1440 m"), analytics: true, }) : undefined; export async function POST(request: Request) { // Rate Limiter Code if (ratelimit) { const headersList = headers(); const ipIdentifier = headersList.get("x-real-ip"); const result = await ratelimit.limit(ipIdentifier ?? ""); if (!result.success) { return new Response( "Too many uploads in 1 day. Please try again in a 24 hours.", { status: 429, headers: { "X-RateLimit-Limit": result.limit, "X-RateLimit-Remaining": result.remaining, } as any, } ); } } const { imageUrl, theme, room } = await request.json(); // POST request to Replicate to start the image restoration generation process let startResponse = await fetch("https://api.replicate.com/v1/predictions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Token " + process.env.REPLICATE_API_KEY, }, body: JSON.stringify({ version: "854e8727697a057c525cdb45ab037f64ecca770a1769cc52287c2e56472a247b", input: { image: imageUrl, prompt: room === "Gaming Room" ? "a room for gaming with gaming computers, gaming consoles, and gaming chairs" : `a ${theme.toLowerCase()} ${room.toLowerCase()}`, a_prompt: "best quality, extremely detailed, photo from Pinterest, interior, cinematic photo, ultra-detailed, ultra-realistic, award-winning", n_prompt: "longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality", }, }), }); let jsonStartResponse = await startResponse.json(); let endpointUrl = jsonStartResponse.urls.get; // GET request to get the status of the image restoration process & return the result when it's ready let restoredImage: string | null = null; while (!restoredImage) { // Loop in 1s intervals until the alt text is ready console.log("polling for result..."); let finalResponse = await fetch(endpointUrl, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Token " + process.env.REPLICATE_API_KEY, }, }); let jsonFinalResponse = await finalResponse.json(); if (jsonFinalResponse.status === "succeeded") { restoredImage = jsonFinalResponse.output; } else if (jsonFinalResponse.status === "failed") { break; } else { await new Promise((resolve) => setTimeout(resolve, 1000)); } } return NextResponse.json( restoredImage ? restoredImage : "Failed to restore image" ); } ================================================ FILE: app/layout.tsx ================================================ import { Analytics } from "@vercel/analytics/react"; import { Metadata } from "next"; import "../styles/globals.css"; let title = "Dream Room Generator"; let description = "Generate your dream room in seconds."; let ogimage = "https://roomgpt-demo.vercel.app/og-image.png"; let sitename = "roomGPT.io"; export const metadata: Metadata = { title, description, icons: { icon: "/favicon.ico", }, openGraph: { images: [ogimage], title, description, url: "https://roomgpt-demo.vercel.app", siteName: sitename, locale: "en_US", type: "website", }, twitter: { card: "summary_large_image", images: [ogimage], title, description, }, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ================================================ FILE: app/page.tsx ================================================ import Image from "next/image"; import Link from "next/link"; import Footer from "../components/Footer"; import Header from "../components/Header"; import SquigglyLines from "../components/SquigglyLines"; export default function HomePage() { return (
Clone and deploy your own with{" "} Vercel

Generating dream rooms{" "} using AI {" "} for everyone.

Take a picture of your room and see how your room looks in different themes. 100% free – remodel your room today.

Generate your dream room

Original Room

Original photo of a room with roomGPT.io

Generated Room

Generated photo of a room with roomGPT.io
); } ================================================ FILE: components/CompareSlider.tsx ================================================ import { ReactCompareSlider, ReactCompareSliderImage, } from "react-compare-slider"; export const CompareSlider = ({ original, restored, }: { original: string; restored: string; }) => { return ( } itemTwo={} portrait className="flex w-[600px] mt-5 h-96" /> ); }; ================================================ FILE: components/DropDown.tsx ================================================ import { Menu, Transition } from "@headlessui/react"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon, } from "@heroicons/react/20/solid"; import { Fragment } from "react"; import { roomType, themeType } from "../utils/dropdownTypes"; function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } interface DropDownProps { theme: themeType | roomType; setTheme: (theme: themeType | roomType) => void; themes: themeType[] | roomType[]; } // TODO: Change names since this is a generic dropdown now export default function DropDown({ theme, setTheme, themes }: DropDownProps) { return (
{theme}
{themes.map((themeItem) => ( {({ active }) => ( )} ))}
); } ================================================ FILE: components/Footer.tsx ================================================ import Link from "next/link"; export default function Footer() { return ( ); } ================================================ FILE: components/Header.tsx ================================================ import Image from "next/image"; import Link from "next/link"; export default function Header() { return (
header text

roomGPT.io

Star on GitHub

); } function Github({ className }: { className?: string }) { return ( ); } ================================================ FILE: components/LoadingDots.tsx ================================================ import styles from "../styles/loading-dots.module.css"; const LoadingDots = ({ color = "#000", style = "small", }: { color: string; style: string; }) => { return ( ); }; export default LoadingDots; LoadingDots.defaultProps = { style: "small", }; ================================================ FILE: components/ResizablePanel.tsx ================================================ import { motion } from "framer-motion"; import useMeasure from "react-use-measure"; export default function ResizablePanel({ children, }: { children: React.ReactNode; }) { let [ref, { height }] = useMeasure(); return (
{children}
); } ================================================ FILE: components/SquigglyLines.tsx ================================================ export default function SquigglyLines() { return ( ); } ================================================ FILE: components/Toggle.tsx ================================================ import { Switch } from "@headlessui/react"; function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } export interface ToggleProps extends React.HTMLAttributes { sideBySide: boolean; setSideBySide: (sideBySide: boolean) => void; } export default function Toggle({ sideBySide, setSideBySide, ...props }: ToggleProps) { return (
Side by Side Compare
); } ================================================ FILE: next.config.js ================================================ /** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, images: { domains: ["upcdn.io", "replicate.delivery"], }, }; ================================================ FILE: package.json ================================================ { "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "@bytescale/upload-widget-react": "^4.9.0", "@headlessui/react": "^1.7.7", "@headlessui/tailwindcss": "^0.1.2", "@heroicons/react": "^2.0.16", "@tailwindcss/forms": "^0.5.3", "@upstash/ratelimit": "^0.3.8", "@upstash/redis": "^1.19.1", "@vercel/analytics": "^0.1.11", "framer-motion": "^8.2.4", "next": "^13.4.4", "react": "^18.2.0", "react-compare-slider": "^2.2.0", "react-countup": "^6.4.0", "react-dom": "^18.2.0", "react-use-measure": "^2.1.1", "request-ip": "^3.3.0" }, "devDependencies": { "@types/node": "18.11.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", "@types/request-ip": "^0.0.37", "autoprefixer": "^10.4.12", "postcss": "^8.4.18", "tailwindcss": "^3.2.4", "typescript": "4.9.4" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: styles/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @media (min-width: 400px) { .background-gradient::before { background: radial-gradient( 20% 50% at 50% 50%, rgba(71, 127, 247, 0.376) 0%, rgba(37, 38, 44, 0) 100% ); z-index: -10; content: ""; position: absolute; inset: 0px; transform: scale(1); pointer-events: none; } } ================================================ FILE: styles/loading-dots.module.css ================================================ .loading { display: inline-flex; align-items: center; } .loading .spacer { margin-right: 2px; } .loading span { animation-name: blink; animation-duration: 1.4s; animation-iteration-count: infinite; animation-fill-mode: both; width: 5px; height: 5px; border-radius: 50%; display: inline-block; margin: 0 1px; } .loading span:nth-of-type(2) { animation-delay: 0.2s; } .loading span:nth-of-type(3) { animation-delay: 0.4s; } .loading2 { display: inline-flex; align-items: center; } .loading2 .spacer { margin-right: 2px; } .loading2 span { animation-name: blink; animation-duration: 1.4s; animation-iteration-count: infinite; animation-fill-mode: both; width: 4px; height: 4px; border-radius: 50%; display: inline-block; margin: 0 1px; } .loading2 span:nth-of-type(2) { animation-delay: 0.2s; } .loading2 span:nth-of-type(3) { animation-delay: 0.4s; } @keyframes blink { 0% { opacity: 0.2; } 20% { opacity: 1; } 100% { opacity: 0.2; } } ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", "./app/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { screens: { xs: "330px", }, }, }, plugins: [require("@tailwindcss/forms"), require("@headlessui/tailwindcss")], }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ] }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" ], "exclude": [ "node_modules" ] } ================================================ FILE: utils/appendNewToName.ts ================================================ export default function appendNewToName(name: string) { let insertPos = name.indexOf("."); let newName = name .substring(0, insertPos) .concat("-new", name.substring(insertPos)); return newName; } ================================================ FILE: utils/downloadPhoto.ts ================================================ function forceDownload(blobUrl: string, filename: string) { let a: any = document.createElement("a"); a.download = filename; a.href = blobUrl; document.body.appendChild(a); a.click(); a.remove(); } export default function downloadPhoto(url: string, filename: string) { fetch(url, { headers: new Headers({ Origin: location.origin, }), mode: "cors", }) .then((response) => response.blob()) .then((blob) => { let blobUrl = window.URL.createObjectURL(blob); forceDownload(blobUrl, filename); }) .catch((e) => console.error(e)); } ================================================ FILE: utils/dropdownTypes.ts ================================================ export type themeType = | "Modern" | "Vintage" | "Minimalist" | "Professional" | "Tropical"; export type roomType = | "Living Room" | "Dining Room" | "Bedroom" | "Bathroom" | "Office" | "Gaming Room"; export const themes: themeType[] = [ "Modern", "Minimalist", "Professional", "Tropical", "Vintage", ]; export const rooms: roomType[] = [ "Living Room", "Dining Room", "Office", "Bedroom", "Bathroom", "Gaming Room", ]; ================================================ FILE: utils/redis.ts ================================================ import { Redis } from "@upstash/redis"; const redis = !!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN ? new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, }) : undefined; export default redis;