29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | input {
6 | @apply border;
7 | @apply border-solid;
8 | @apply border-gray-300;
9 | /* @apply rounded-md; */
10 | @apply px-4;
11 | @apply py-2;
12 | }
13 |
14 | .border-hairline {
15 | @apply border border-black border-opacity-10;
16 | }
17 |
18 | .lil-button {
19 | @apply inline-block;
20 | @apply text-sm;
21 | @apply text-gray-500;
22 | @apply rounded-md;
23 | @apply py-2;
24 | @apply mx-3;
25 | }
26 |
27 | .lil-text {
28 | @apply text-sm;
29 | @apply text-gray-500;
30 | }
31 |
32 | .lil-text a {
33 | @apply text-gray-800;
34 | @apply underline;
35 | }
36 |
37 | .icon {
38 | @apply inline relative mr-1;
39 | top: -0.1em;
40 | width: 1.1em;
41 | height: 1.1em;
42 | }
43 |
44 | .prose {
45 | @apply mt-8;
46 | @apply text-lg;
47 | @apply text-gray-500;
48 | @apply leading-7;
49 | }
50 |
51 | .prose a {
52 | @apply text-gray-600;
53 | @apply underline;
54 | }
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Zeke Sikelianos
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/pages/api/predictions/index.js:
--------------------------------------------------------------------------------
1 | import Replicate from "replicate";
2 | import packageData from "../../../package.json";
3 |
4 | const API_HOST = process.env.REPLICATE_API_HOST || "https://api.replicate.com";
5 |
6 | export default async function handler(req, res) {
7 | const token = req.headers["x-replicate-api-token"];
8 | if (!token) {
9 | res.statusCode = 401;
10 | res.end(JSON.stringify({ detail: "Missing Replicate API token. Please provide your token in the x-replicate-api-token header." }));
11 | return;
12 | }
13 | // remove null and undefined values
14 | req.body = Object.entries(req.body).reduce(
15 | (a, [k, v]) => (v == null ? a : ((a[k] = v), a)),
16 | {}
17 | );
18 | let prediction;
19 | const model = "black-forest-labs/flux-kontext-pro";
20 | const replicate = new Replicate({
21 | auth: token,
22 | userAgent: `${packageData.name}/${packageData.version}`
23 | });
24 | prediction = await replicate.predictions.create({
25 | model,
26 | input: req.body
27 | });
28 | res.statusCode = 201;
29 | res.end(JSON.stringify(prediction));
30 | }
31 |
32 | export const config = {
33 | api: {
34 | bodyParser: {
35 | sizeLimit: "10mb",
36 | },
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 👩🎨 Paint by Text
2 |
3 | Modify images by chatting with a generative AI model.
4 |
5 | Try it out at [paintbytext.chat](http://paintbytext.chat)
6 |
7 | ## How it works
8 |
9 | This app is powered by:
10 |
11 | 🚀 [Replicate](https://replicate.com/?utm_source=project&utm_campaign=paintbytext), a platform for running machine learning models in the cloud.
12 |
13 | 🎨 [Kontext](https://replicate.com/black-forest-labs/flux-kontext-pro?utm_source=project&utm_campaign=paintbytext), an open-source machine learning model that edits images using text.
14 |
15 | ▲ [Vercel](https://vercel.com/), a platform for running web apps.
16 |
17 | ⚡️ Next.js [server-side API routes](pages/api), for talking to the Replicate API.
18 |
19 | 👀 Next.js React components, for the browser UI.
20 |
21 | 🍃 [Tailwind CSS](https://tailwindcss.com/), for styles.
22 |
23 |
24 | ## Usage
25 |
26 | 1. Open the app in your browser.
27 | 1. When prompted, enter your [Replicate API token](https://replicate.com/account/api-tokens?new-token-name=paint-by-text-kontext).
28 | 1. You can generate a free token at the link above (requires a Replicate account).
29 | 1. Your token is stored securely in your browser and used only for your requests.
30 |
31 | ## Development
32 |
33 | 1. Install a recent version of [Node.js](https://nodejs.org/)
34 | 1. Install dependencies and run the server:
35 | ```
36 | npm install
37 | npm run dev
38 | ```
39 | 1. Open [localhost:3000](http://localhost:3000) in your browser. That's it!
40 |
--------------------------------------------------------------------------------
/lib/prepare-image-file-for-upload.js:
--------------------------------------------------------------------------------
1 | export default function prepareImageFileForUpload(file) {
2 | return new Promise((resolve, reject) => {
3 | const fr = new FileReader();
4 | fr.onerror = reject;
5 | fr.onload = (e) => {
6 | const img = document.createElement("img");
7 | img.onload = function () {
8 | const MAX_WIDTH = 512;
9 | const MAX_HEIGHT = 512;
10 |
11 | let width = img.width;
12 | let height = img.height;
13 | // Calculate the scaling factor to fit within the max dimensions while preserving aspect ratio
14 | const widthRatio = MAX_WIDTH / width;
15 | const heightRatio = MAX_HEIGHT / height;
16 | const scale = Math.min(widthRatio, heightRatio, 1); // Don't upscale
17 | width = Math.round(width * scale);
18 | height = Math.round(height * scale);
19 |
20 | const canvas = document.createElement("canvas");
21 | canvas.width = width;
22 | canvas.height = height;
23 |
24 | const ctx = canvas.getContext("2d");
25 | ctx.mozImageSmoothingEnabled = false;
26 | ctx.webkitImageSmoothingEnabled = false;
27 | ctx.msImageSmoothingEnabled = false;
28 | ctx.imageSmoothingEnabled = false;
29 |
30 | ctx.drawImage(img, 0, 0, width, height);
31 |
32 | const dataURL = canvas.toDataURL(file.type);
33 |
34 | resolve(dataURL);
35 | };
36 | img.src = e.target.result;
37 | };
38 | fr.readAsDataURL(file);
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/components/prompt-form.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Message from "./message";
3 |
4 | export default function PromptForm({
5 | initialPrompt,
6 | isFirstPrompt,
7 | onSubmit,
8 | disabled = false,
9 | }) {
10 | const [prompt, setPrompt] = useState(initialPrompt);
11 |
12 | useEffect(() => {
13 | setPrompt(initialPrompt);
14 | }, [initialPrompt]);
15 |
16 | const handleSubmit = (e) => {
17 | e.preventDefault();
18 | setPrompt("");
19 | onSubmit(e);
20 | };
21 |
22 | if (disabled) {
23 | return;
24 | }
25 |
26 | return (
27 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import { ArrowLeft as ArrowLeftIcon } from "lucide-react";
4 |
5 | import appName from "./index";
6 |
7 | export default function About() {
8 | return (
9 |
10 |
11 | {appName}
12 |
13 |
14 |
15 |
{appName}
16 |
17 |
18 | This open-source website provides a simple interface for modifying
19 | images using text-based instructions. You can upload an image, provide
20 | a text prompt describing how to change that image, and generate new
21 | images based on the prompt.
22 |
23 |
24 |
25 | The model is hosted on{" "}
26 |
27 | Replicate
28 |
29 | , which exposes a cloud API for running predictions. This website is
30 | built with Next.js and hosted on{" "}
31 | Vercel, and uses
32 | Replicate's API to run the Kontext Pro model. The source code
33 | is publicly available on{" "}
34 |
35 | GitHub
36 |
37 | . Pull requests welcome!
38 |