├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.txt ├── README.md ├── app └── api │ └── kiri │ └── route.ts ├── archive ├── pages-api-banana-upsample.ts ├── pages-api-sd-banana.ts ├── pages-api-txt2img-exec.ts ├── src-adapters-txt2img.ts ├── src-bananaFetch.ts ├── src-blackImgBase64.ts └── src-lib-bananaUrl.ts ├── assets ├── easel.png ├── forums.png ├── forums.xcf ├── logo.png ├── logo.xcf ├── page-icons.svg ├── page-icons.svg.2022_09_14_14_04_04.0.svg └── smoke.png ├── docs └── img │ ├── cover.png │ └── cover2.jpg ├── jest.config.mjs ├── lingui.config.js ├── locales ├── en-US │ ├── messages.js │ └── messages.po ├── fa-IR │ ├── messages.js │ └── messages.po ├── he-IL │ ├── messages.js │ └── messages.po └── ja-JP │ ├── messages.js │ └── messages.po ├── next-env.d.ts ├── next.config.js ├── out.json ├── package.json ├── pages ├── [_id].tsx ├── _app.tsx ├── _document.tsx ├── about.tsx ├── account │ └── data.tsx ├── admin.tsx ├── api │ ├── README.md │ ├── auth │ │ └── [...nextauth].ts │ ├── bananaCapacity.ts │ ├── bananaCheck.ts │ ├── bananaUpdate.ts │ ├── buildStats.ts │ ├── createStripePaymentIntent.ts │ ├── creditTopup.ts │ ├── csend.ts │ ├── file.js │ ├── file2.ts │ ├── gongoAuth.ts │ ├── gongoPoll.ts │ ├── imgFetchAndDelete.ts │ ├── mongoRelay.js │ ├── myData.ts │ ├── myDataDelete.ts │ ├── providerFetch.ts │ ├── starItem.ts │ ├── stripe.ts │ └── txt2img-banana.ts ├── checkout.tsx ├── credits.tsx ├── faq.tsx ├── history.tsx ├── img2img.tsx ├── index.tsx ├── inpaint.tsx ├── ipix2pix.tsx ├── logs.tsx ├── news.tsx ├── order │ └── [_id].tsx ├── orders.tsx ├── p │ └── [_id].tsx ├── privacy.tsx ├── resources.tsx ├── s │ └── [_id].tsx ├── share_target.tsx ├── start.tsx ├── stats.tsx ├── txt2img.tsx └── upsample.tsx ├── public ├── .well-known │ └── apple-developer-merchantid-domain-association ├── browserconfig.xml ├── favicon.ico ├── icon.png ├── icon.png.inputs.json ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ └── safari-pinned-tab.svg ├── img │ ├── ipix2pix │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ └── 6.jpg │ ├── pages │ │ ├── img2img.png │ │ ├── inpaint.png │ │ ├── ipix2pix.png │ │ ├── txt2img.png │ │ └── upsample.png │ └── placeholder.png ├── manifest.webmanifest └── tos.html ├── scripts └── upstream.sh ├── src ├── CheckoutForm.tsx ├── Copyright.tsx ├── GoButton.tsx ├── IPix2Pix.tsx ├── Img2img.tsx ├── Inpaint.tsx ├── InputImage.tsx ├── InputSlider.tsx ├── Link.tsx ├── MyAppBar.tsx ├── MyMasonry.tsx ├── NewBadge.tsx ├── OutputImage.tsx ├── Starred.tsx ├── api-lib │ ├── GithubProvider.ts │ ├── auth.ts │ ├── db-full.ts │ ├── db.ts │ ├── gongoAuthAdapter.ts │ ├── ipCheck.ts │ └── objectId.ts ├── asyncConfirm.tsx ├── calculateCredits.ts ├── config │ ├── constants.ts │ ├── models.ts │ ├── modelsForClient.tsx.maybe │ └── providers.ts ├── createEmotionCache.ts ├── db.ts ├── hooks │ └── providerFetch.ts ├── lib │ ├── ScrollShadowWrapper.tsx │ ├── blobToBase64.tsx │ ├── civitai.spec.ts │ ├── civitai.ts │ ├── client-env.ts │ ├── fetchToOutput.ts │ ├── hooks.ts │ ├── i18n.ts │ ├── isBlackImgBase64.ts │ ├── locales.ts │ ├── mimeTypes.ts │ ├── models.ts │ ├── providerFetch │ │ ├── ProviderFetchRequestBanana.ts │ │ ├── ProviderFetchRequestBase.ts │ │ ├── ProviderFetchRequestFromObject.ts │ │ ├── handlerEdge.ts │ │ ├── handlerServerless.ts │ │ └── index.ts │ ├── providerFetchDDA.ts │ ├── sendQueue.ts │ ├── server-env.ts │ ├── sharedInputTextFromInputs.tsx │ ├── strObjectId.tsx │ ├── supports.ts │ ├── useBreakPoint.tsx │ └── useOver18.tsx ├── schemas │ ├── bananaRequest.ts │ ├── creditCode.ts │ ├── csend.ts │ ├── ddaCallInputs.ts │ ├── ddaModelInputs.ts │ ├── history.ts │ ├── index.ts │ ├── lib │ │ └── NodeCol.ts │ ├── order.ts │ ├── star.ts │ ├── upsampleCallInputs.ts │ ├── upsampleModelInputs.ts │ ├── user.ts │ └── userProfile.ts ├── sd │ ├── Addons.tsx │ ├── Addons │ │ ├── LoRAs.tsx │ │ ├── TextualInversions.tsx │ │ └── common.tsx │ ├── Controls.tsx │ ├── Footer.tsx │ ├── ModelSelect.tsx │ ├── converter_standalone.ts │ ├── defaults.ts │ ├── useModelState.ts │ ├── useRandomPrompt.tsx │ ├── utils.spec.ts │ └── utils.ts ├── theme.ts ├── useNews.ts └── workboxStuff.tsx ├── tests ├── .gitignore ├── fixtures │ └── http │ │ ├── civitai.com!api!v1!model-versions!by-hash!37639B4200718864EF4AA76BB1F83166DD19A0C6C0A6129BCEFCF54274.json │ │ ├── civitai.com!api!v1!model-versions!by-hash!7BFDB20388.json │ │ ├── civitai.com!api!v1!model-versions!by-hash!7BFDB20388CD3511DB6FB0B0D0F2868729B9FCA121A1314C7EBB1B180F.json │ │ ├── civitai.com!api!v1!model-versions!by-hash!874FAE83.json │ │ └── civitai.com!api!v1!models!99201.json ├── img2img ├── inpaint ├── overture-creations-5sI6fQgYIuo.png └── overture-creations-5sI6fQgYIuo_mask.png ├── touch ├── tsconfig.json ├── types ├── db-server.d.ts └── next-auth.d.ts ├── upstream └── HEAD ├── vercel.json ├── worker └── index.ts └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint", "unused-imports"], 5 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 6 | rules: { 7 | "@typescript-eslint/no-unused-vars": "off", 8 | "unused-imports/no-unused-imports": "error", 9 | "unused-imports/no-unused-vars": [ 10 | "warn", 11 | { 12 | vars: "all", 13 | varsIgnorePattern: "^_", 14 | args: "after-used", 15 | argsIgnorePattern: "^_", 16 | }, 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | upstream/commits/* 2 | node_modules 3 | .vercel 4 | .next 5 | node_modules 6 | yarn-error.log 7 | tsconfig.tsbuildinfo 8 | **/public/precache.*.js 9 | **/public/sw.js 10 | **/public/sw.js.map 11 | **/public/workbox-* 12 | .swc 13 | **/public/worker-development.js 14 | .env.local 15 | .env*.local 16 | /dump 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by Gadi Cohen & Tim Mikeladze 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stable-diffusion-react-nextjs-mui-pwa 2 | 3 | _PWA Web App front end for Stable Diffusion, on React/NextJS with Material UI_ 4 | 5 | Copyright (c) 2022 by Gadi Cohen . MIT Licensed. 6 | 7 | 8 | kiri.art 9 | 10 | 11 | ## New Project in Active Development (since Aug 31st, 2022) 12 | 13 | super dog 14 | 15 | - Web interface to run Stable Diffusion queries on: 16 | - Local PC / local installation 17 | - [Banana.dev](https://banana.dev) serverless GPU containers (roughly $1 = 200 requests, YMMV) 18 | - Local banana.dev docker container (see [docs/banana-local.md](./docs/banana-local.md)) 19 | - Others? 20 | 21 | Why? Make this fun stuff more accessible to web developers and friends :) See the [live demo](https://kiri.art/), run on your own PC for free, or deploy! 22 | 23 | If you have a background in web dev / dev ops, and have wanted to experiment a bit with machine learning / latent diffusion (AI image generation), this is a great project to get involved in :) 24 | 25 | Doing this in my very limited spare time, PRs more likely to get responses than issues, but try me :) 26 | 27 | ## To Develop 28 | 29 | 1. Clone repo 30 | 1. `yarn install` 31 | 1. ~~edit `.env.local`~~ (or just set local vars - per below) 32 | 1. `yarn dev` 33 | 34 | Note: you'll need at least one destination / target from the list below where Stable Diffusion will run. 35 | 36 | ## Destinations / Targets 37 | 38 | - **Local docker image (recommended)** 39 | 40 | - Pretty easy if you have docker installed. 41 | - See https://github.com/kiri-art/docker-diffusers-api. 42 | 43 | - **Local Exec** 44 | 45 | - If you already have Stable Diffusion installed locally, 46 | this will run the Python script via node spawn. 47 | - Set `STABLE_DIFFUSION_HOME` (to e.g. `/home/user/src/stable-diffusion`). 48 | - Works, but not as well maintained as the docker based solutions. 49 | 50 | - **Remote BananaDev docker container (serverless GPU)** 51 | 52 | - Great for local dev if you don't have a supported GPU 53 | - Default for deployments or when `NODE_ENV=="production"` 54 | - I'm paying roughly $1 = 200 requests with default params, YMMV. 55 | - Follow instructions at https://github.com/kiri-art/docker-diffusers-api. 56 | - Set `BANANA_API_KEY` and `BANANA_MODEL_KEY` env variables. 57 | - Set the relevant keys for your deployed models, 58 | `BANANA_MODEL_KEY_SD_v1_5` by default. 59 | 60 | ## REQUIRE_REGISTRATION 61 | 62 | By default, registration (i.e. sign up, log in, use credits) is required in production only. You can turn it on in development (to test the auth flow), or turn it off in production (if you're deploying somewhere private). 63 | 64 | ```bash 65 | REQUIRE_REGISTRATION=1 66 | NEXT_PUBLIC_REQUIRE_REGISTRATION=1 67 | ``` 68 | 69 | Note: `NEXT_PUBLIC_` vars are compiled at build time! So if you want to deploy to production without requiring registration, set it to `0` _before_ building and deploying. 70 | 71 | ## TODO 72 | 73 | - Docker image for super easy start 74 | - Vercel clone button 75 | 76 | ## i18n 77 | 78 | We use NextJS's built-in 79 | [i18n routing](https://nextjs.org/docs/advanced-features/i18n-routing) and 80 | [Lingui](https://lingui.js.org/tutorials/setup-react.html) for translations. 81 | 82 | Useful commands: 83 | 84 | - `yarn i18n:extract` to extract strings. 85 | - Send `locales/*/messages.po` to translators, resave. 86 | - `yarn i18n:compile` before deploy. 87 | 88 | See also [lingui.config.js](./lingui.config.js) and [locales](./locales) dir. 89 | 90 | ## Refs 91 | 92 | - https://github.com/mui/material-ui/tree/master/examples/nextjs-with-typescript 93 | - 94 | -------------------------------------------------------------------------------- /app/api/kiri/route.ts: -------------------------------------------------------------------------------- 1 | import Auth from "gongo-server/lib/auth-class"; 2 | import GongoServer from "gongo-server/lib/serverless"; 3 | import Database from "gongo-server-db-mongo"; 4 | import gs from "../../../src/api-lib/db"; 5 | import createHandler from "../../../src/lib/providerFetch/handlerEdge"; 6 | 7 | /* 8 | // TODO, in theory we could stream this now if we move request info to query string 9 | export const config = { 10 | api: { 11 | bodyParser: { 12 | sizeLimit: "4mb", 13 | }, 14 | }, 15 | }; 16 | */ 17 | 18 | // TODO deps 19 | const handler = createHandler({ gs, Auth, GongoServer, Database }); 20 | 21 | export const runtime = "edge"; 22 | export const POST = handler; 23 | export const GET = function () { 24 | return new Response("GET not supported", { status: 400 }); 25 | }; 26 | -------------------------------------------------------------------------------- /archive/pages-api-txt2img-exec.ts: -------------------------------------------------------------------------------- 1 | import child_process from "node:child_process"; 2 | import fs from "node:fs/promises"; 3 | import path from "node:path"; 4 | import os from "node:os"; 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | import stableDiffusionInputsSchema from "../../src/schemas/stableDiffusionInputs"; 7 | 8 | const { STABLE_DIFFUSION_HOME } = process.env; 9 | console.log({ STABLE_DIFFUSION_HOME }); 10 | 11 | export default async function txt2imgExec( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | if (process.env.NODE_ENV !== "development") { 16 | res.status(400); 17 | res.end(); 18 | return; 19 | } 20 | 21 | const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sd-mui-")); 22 | const dir = tmpDir.split(path.sep).pop(); 23 | const opts = req.query; 24 | const modelOpts = stableDiffusionInputsSchema.cast(opts); 25 | console.log({ modelOpts }); 26 | 27 | const cmdOpts = Object.fromEntries( 28 | Object.entries(modelOpts).map(([key, value]) => { 29 | if (key === "guidance_scale") return ["scale", value]; 30 | if (key === "width") return ["W", value]; 31 | if (key === "height") return ["H", value]; 32 | if (key === "num_inference_steps") return ["ddim_steps", value]; 33 | return [key, value]; 34 | }) 35 | ); 36 | 37 | console.log({ cmdOpts }); 38 | 39 | const cmdString = [ 40 | "conda run --no-capture-output -n ldm", 41 | "python scripts/txt2img.py --outdir " + tmpDir, 42 | "--skip_grid --n_iter 1 --n_samples 1", 43 | ] 44 | .concat( 45 | Object.entries(cmdOpts).map( 46 | ([key, val]) => 47 | "--" + key + " " + (typeof val === "string" ? "'" + val + "'" : val) 48 | ) 49 | ) 50 | .join(" "); 51 | 52 | const write = (obj: Record) => 53 | res.write(JSON.stringify(obj) + "\n"); 54 | 55 | return new Promise((resolve, _reject) => { 56 | res.writeHead(200, { 57 | "Content-Type": "application/json", 58 | "Transfer-Encoding": "chunked", 59 | "Content-Encoding": "chunked", 60 | }); 61 | 62 | /* 63 | "conda run --no-capture-output -n ldm python scripts/txt2img.py --prompt hi --outdir " + 64 | tmpDir + 65 | " --skip_grid --n_iter 1 --n_samples 1" 66 | */ 67 | 68 | const child = child_process.spawn(cmdString, { 69 | shell: true, 70 | cwd: process.env.STABLE_DIFFUSION_HOME, 71 | }); 72 | 73 | child.stdout.on("data", (data) => { 74 | console.log(data.toString("utf8")); 75 | res.write( 76 | JSON.stringify({ 77 | $type: "stdout", 78 | data: data.toString("utf8").trim(), 79 | }) + "\n" 80 | ); 81 | }); 82 | child.stderr.on("data", (data) => { 83 | console.log(data.toString("utf8")); 84 | res.write( 85 | JSON.stringify({ 86 | $type: "stderr", 87 | data: data.toString("utf8").trim(), 88 | }) + "\n" 89 | ); 90 | }); 91 | 92 | child.on("close", () => { 93 | write({ $type: "done", dir }); 94 | res.end(); 95 | resolve(true); 96 | }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /archive/src-blackImgBase64.ts: -------------------------------------------------------------------------------- 1 | const blackImgBase64 = 2 | "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAIAAgADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/" + 3 | "ooooAKKKKACiiigA".repeat(341) + 4 | "ooooA//2Q=="; 5 | 6 | export default blackImgBase64; 7 | -------------------------------------------------------------------------------- /archive/src-lib-bananaUrl.ts: -------------------------------------------------------------------------------- 1 | export default function bananaUrl(provider_id: 1 | 2) { 2 | if (provider_id === 1 || !provider_id) return "https://api.banana.dev"; 3 | // if (provider_id === 2) return "https://api.kiri.art/api"; 4 | if (provider_id === 2) return "https://api-ams.kiri.art"; 5 | return "UNKNOWN PROVIDER " + provider_id; 6 | } 7 | -------------------------------------------------------------------------------- /assets/easel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/easel.png -------------------------------------------------------------------------------- /assets/forums.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/forums.png -------------------------------------------------------------------------------- /assets/forums.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/forums.xcf -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/logo.png -------------------------------------------------------------------------------- /assets/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/logo.xcf -------------------------------------------------------------------------------- /assets/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/assets/smoke.png -------------------------------------------------------------------------------- /docs/img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/docs/img/cover.png -------------------------------------------------------------------------------- /docs/img/cover2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiri-art/stable-diffusion-react-nextjs-mui-pwa/c24581437a2cbb68c83fdf0ac2b3173a3e35cb87/docs/img/cover2.jpg -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import nextJest from "next/jest.js"; 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: "./", 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | /** @type {import('jest').Config} */ 10 | const config = { 11 | // Add more setup options before each test is run 12 | // setupFilesAfterEnv: ['/jest.setup.js'], 13 | 14 | testEnvironment: "jest-environment-jsdom", 15 | }; 16 | 17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 18 | export default createJestConfig(config); 19 | -------------------------------------------------------------------------------- /lingui.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | locales: ["en-US", "he-IL", "ja-JP", "fa-IR"], 3 | // pseudoLocale: "pseudo", 4 | sourceLocale: "en-US", 5 | fallbackLocales: { 6 | default: "en-US", 7 | }, 8 | catalogs: [ 9 | { 10 | path: "locales/{locale}/messages", 11 | include: ["pages", "src"], 12 | }, 13 | ], 14 | format: "po", 15 | }; 16 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withPWA = require("@ducanh2912/next-pwa").default({ 3 | dest: "public", 4 | disable: process.env.NODE_ENV === "development", 5 | skipWaiting: false, 6 | }); 7 | 8 | /** @type {import('next').NextConfig} */ 9 | module.exports = withPWA({ 10 | reactStrictMode: true, 11 | i18n: { 12 | locales: ["en-US", "he-IL", "ja-JP", "fa-IR"], 13 | defaultLocale: "en-US", 14 | // domains: [ { domain: "example.com", defaultLocale: 'en-US '} ] 15 | }, 16 | images: { 17 | remotePatterns: [ 18 | { 19 | protocol: "http", 20 | hostname: "localhost", 21 | port: "3000", 22 | pathname: "/api/file", 23 | }, 24 | { 25 | protocol: "https", 26 | hostname: "kiri.art", 27 | // port: "443", 28 | pathname: "/api/file", 29 | }, 30 | ], 31 | }, 32 | experimental: { 33 | swcPlugins: [ 34 | [ 35 | "@lingui/swc-plugin", 36 | { 37 | // the same options as in .swcrc 38 | }, 39 | ], 40 | ], 41 | }, 42 | webpack: (config) => { 43 | config.module.rules.push({ 44 | test: /\..*ignore/, 45 | use: [ 46 | { 47 | loader: "ignore-loader", 48 | }, 49 | ], 50 | }); 51 | 52 | if (process.env.NODE_ENV === "development") { 53 | config.resolve.alias = { 54 | ...config.resolve.alias, 55 | "next-auth/react": require.resolve("next-auth/react"), 56 | }; 57 | } 58 | return config; 59 | }, 60 | /* 61 | async headers() { 62 | return [ 63 | { 64 | source: "/:path*{/}?", 65 | headers: [ 66 | { 67 | key: "Cross-Origin-Embedder-Policy", 68 | value: "require-corp", 69 | }, 70 | { 71 | key: "Cross-Origin-Opener-Policy", 72 | value: "same-origin-allow-popups", 73 | }, 74 | ], 75 | }, 76 | ]; 77 | }, 78 | */ 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-with-typescript", 3 | "version": "5.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "post-update": "echo \"codesandbox preview only, need an update\" && yarn upgrade --latest", 11 | "//i18n:extract": "https://github.com/lingui/js-lingui/issues/952", 12 | "i18n:extract": "NODE_ENV=development lingui extract", 13 | "i18n:compile": "lingui compile", 14 | "docker": "docker run -it -p 8000:8000 --gpus all --rm diffusers-api", 15 | "test": "jest", 16 | "stripe-dev": "stripe listen --forward-to localhost:3000/api/stripe", 17 | "gongo-update": "yarn add --exact gongo-client@latest gongo-client-react@latest gongo-server@latest gongo-server-db-mongo@latest" 18 | }, 19 | "engines": { 20 | "node": ">=18.17.1" 21 | }, 22 | "dependencies": { 23 | "@banana-dev/banana-dev": "4.0.0", 24 | "@ducanh2912/next-pwa": "9.7.2", 25 | "@emotion/cache": "11.10.3", 26 | "@emotion/react": "11.10.4", 27 | "@emotion/server": "11.10.0", 28 | "@emotion/styled": "11.10.4", 29 | "@jsquash/jxl": "^1.0.3", 30 | "@jsquash/png": "^2.1.4", 31 | "@lingui/react": "4.3.0", 32 | "@mui/icons-material": "5.14.9", 33 | "@mui/lab": "5.0.0-alpha.145", 34 | "@mui/material": "5.14.10", 35 | "@projectfunction/async-busboy": "^1.0.4", 36 | "@stripe/react-stripe-js": "^1.10.0", 37 | "@stripe/stripe-js": "^1.35.0", 38 | "@vercel/analytics": "1.0.2", 39 | "aws-sdk": "^2.1231.0", 40 | "date-fns": "^2.29.2", 41 | "discourse2": "1.1.1", 42 | "file-type": "18.5.0", 43 | "gongo-client": "2.8.2", 44 | "gongo-client-react": "0.5.3", 45 | "gongo-server": "3.5.0", 46 | "gongo-server-db-mongo": "3.2.2", 47 | "highcharts": "^10.2.1", 48 | "highcharts-react-official": "^3.1.0", 49 | "ignore-loader": "^0.1.2", 50 | "ipdata": "^2.2.4", 51 | "jest-fetch-mock": "3.0.3", 52 | "jszip": "3.10.1", 53 | "masonic": "^3.7.0", 54 | "micro": "^9.4.1", 55 | "micro-cors": "^0.1.1", 56 | "mongodb": "6.2.0", 57 | "mongodb-rest-relay": "1.9.4", 58 | "next": "14.0.1", 59 | "next-auth": "^4.23.1", 60 | "passport": "0.6.0", 61 | "passport-github2": "^0.1.12", 62 | "passport-google-oauth20": "2.0.0", 63 | "passport-twitter": "^1.0.4", 64 | "q-floodfill": "^1.3.1", 65 | "rc-progress": "^3.4.1", 66 | "react": "18.2.0", 67 | "react-dom": "18.2.0", 68 | "react-event-listener": "0.6.6", 69 | "react-hook-inview": "4.5.0", 70 | "react-toastify": "9.0.8", 71 | "react-virtuoso": "^4.5.1", 72 | "sanitize-filename": "^1.6.3", 73 | "sharp": "^0.32.5", 74 | "stripe": "^10.7.0", 75 | "stylis": "4.1.1", 76 | "stylis-plugin-rtl": "2.1.1", 77 | "uuid": "^9.0.0", 78 | "yup": "1.0.0-beta.7" 79 | }, 80 | "devDependencies": { 81 | "@babel/core": "^7.18.13", 82 | "@lingui/cli": "4.3.0", 83 | "@lingui/macro": "4.3.0", 84 | "@lingui/swc-plugin": "^4.0.3", 85 | "@testing-library/jest-dom": "5.17.0", 86 | "@testing-library/react": "14.0.0", 87 | "@types/micro-cors": "^0.1.2", 88 | "@types/node": "18.7.14", 89 | "@types/react": "18.0.18", 90 | "@types/react-event-listener": "0.4.12", 91 | "@types/sharp": "^0.32.0", 92 | "@types/stylis": "^4.0.2", 93 | "@types/uuid": "^8.3.4", 94 | "@typescript-eslint/eslint-plugin": "6.4.1", 95 | "@typescript-eslint/parser": "6.4.1", 96 | "babel-plugin-macros": "^3.1.0", 97 | "eslint": "8.23.0", 98 | "eslint-config-next": "14.0.1", 99 | "eslint-plugin-unused-imports": "2.0.0", 100 | "jest": "29.6.1", 101 | "jest-environment-jsdom": "29.6.1", 102 | "prettier": "2.7.1", 103 | "typescript": "5.2.2" 104 | }, 105 | "browser": { 106 | "crypto": false 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Head from "next/head"; 3 | import { AppProps } from "next/app"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | import { useRouter } from "next/router"; 6 | import { SessionProvider } from "next-auth/react"; 7 | // import { useGongoIsPopulated } from "gongo-client-react"; 8 | import { ToastContainer } from "react-toastify"; 9 | import "react-toastify/dist/ReactToastify.css"; 10 | 11 | import { ThemeProvider } from "@mui/material/styles"; 12 | import CssBaseline from "@mui/material/CssBaseline"; 13 | import { CacheProvider, EmotionCache } from "@emotion/react"; 14 | 15 | import "../src/db"; 16 | import themes from "../src/theme"; 17 | import createEmotionCache from "../src/createEmotionCache"; 18 | import locales, { defaultLocale } from "../src/lib/locales"; 19 | import { i18n, I18nProvider } from "../src/lib/i18n"; 20 | import workboxStuff from "../src/workboxStuff"; 21 | import { ConfirmDialog } from "../src/asyncConfirm"; 22 | 23 | interface MyAppProps extends AppProps { 24 | emotionCache?: EmotionCache; 25 | } 26 | 27 | // Client-side caches, shared for the whole session of the user in the browser. 28 | const csEmotionCache = { 29 | ltr: createEmotionCache("ltr"), 30 | rtl: createEmotionCache("rtl"), 31 | }; 32 | 33 | if (typeof document === "object") { 34 | document.addEventListener("readystatechange", () => { 35 | if (document.readyState === "complete") { 36 | const match = location.hash.match(/[#&]ref=(?[^&]+)/); 37 | const ref = match && match.groups && match.groups.ref; 38 | if (ref && match.index !== undefined) { 39 | localStorage.setItem("ref", ref); 40 | location.hash = 41 | location.hash.substring(0, match.index) + 42 | location.hash.substring(match.index + match[0].length); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | export default function MyApp(props: MyAppProps) { 49 | // const isPopulated = useGongoIsPopulated(); 50 | // const isServer = typeof document === "undefined"; 51 | const router = useRouter(); 52 | const locale = locales[router.locale || defaultLocale]; 53 | const dir = locale.dir as "ltr" | "rtl"; 54 | const { 55 | Component, 56 | emotionCache = csEmotionCache[dir], 57 | pageProps: { session, ...pageProps }, 58 | } = props; 59 | 60 | React.useEffect(() => { 61 | // Initially set on server-rendered _document.js 62 | const html = document.querySelector("html"); 63 | if (html) { 64 | html.setAttribute("lang", locale.id); 65 | html.setAttribute("dir", locale.dir); 66 | } 67 | 68 | // Lingui 69 | // When we need to add dynamic language loading... 70 | // https://lingui.js.org/guides/dynamic-loading-catalogs.html#final-i18n-loader-helper 71 | i18n.activate(locale.id); 72 | }, [locale]); 73 | 74 | React.useEffect(() => { 75 | workboxStuff(); 76 | }, []); 77 | 78 | // if (!isServer && !isPopulated) return
Loading...
; 79 | 80 | return ( 81 | 82 | 83 | 87 | 88 | 89 | 90 | 91 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 109 | 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /pages/account/data.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { t, Trans } from "@lingui/macro"; 3 | import { useLingui } from "@lingui/react"; 4 | import { db } from "gongo-client-react"; 5 | 6 | import { Button, Container, Typography } from "@mui/material"; 7 | 8 | import MyAppBar from "../../src/MyAppBar"; 9 | import Link from "next/link"; 10 | 11 | export default function AccountData() { 12 | useLingui(); 13 | const [value, setValue] = React.useState(""); 14 | const [destroying, setDestroying] = React.useState(""); 15 | 16 | async function destroy() { 17 | const sleep = (ms: number) => 18 | new Promise((resolve) => setTimeout(resolve, ms)); 19 | setDestroying("busy"); 20 | await sleep(1000); 21 | setDestroying("done"); 22 | } 23 | 24 | return ( 25 | <> 26 | 27 | 28 | 29 | Privacy 30 | 31 |
    32 | 37 |
  • 38 | 39 | Your data is stored in our database in Paris, France. 40 | 41 |
  • 42 |
  • 43 | 44 | Your history is stored on your local device only, and is 45 | not backed up to our servers. 46 | {" "} 47 | 48 | We keep a log of user requests that{" "} 49 | do not include prompts nor input images or result images 50 | (except for images you've starred). 51 | 52 |
  • 53 |
  • 54 | 55 | Your IP address and user agent is recorded on each 56 | successful log in. 57 | 58 |
  • 59 |
  • 60 | 61 | See also our Terms of Service and{" "} 62 | Privacy Policy 63 | 64 |
  • 65 |
66 |

67 | 68 | You can download your data at anytime, in a ZIP file (of JSON 69 | collections). This includes all data in our database 70 | associated with your account. 71 | 72 |

73 | 79 | 80 | 81 | 82 |
83 |
84 | 85 | 86 | Delete My Account 87 | 88 | 89 |

90 | STILL WORKING ON THIS. Doesn't work yet. Email support if you 91 | need your data deleted before this is ready. 92 |

93 | 94 |

95 | 96 | Use the button below to irreversably delete your account and 97 | all associated data. 98 | {" "} 99 | Type the text in the box below to enable this service. 100 |

101 | 102 |
103 | 114 | setValue(event.target.value.toUpperCase())} 126 | /> 127 |
128 |
129 |
130 | 131 | 146 | 147 |

148 | 149 | Note: this only deletes your data from our servers and{" "} 150 | this device. If you have used our app on other devices, you 151 | should uninstall it there too (or clear the browser's local 152 | storage). 153 | 154 |

155 |
156 | 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /pages/api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | Our (CPU) serverless funcs (nextjs backend). 4 | 5 | - "exec" files spawn a local process. 6 | Uses `node` APIs. 7 | - "fetch" files operate excluslively through HTTP Fetch. 8 | No node APIs, can run in e.g. V8 edge environment. 9 | 10 | Important that these files are separate and not in the same important chain. 11 | -------------------------------------------------------------------------------- /pages/api/bananaCapacity.ts: -------------------------------------------------------------------------------- 1 | // import * as banana from "@banana-dev/banana-dev"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | import gs from "../../src/api-lib/db"; 5 | 6 | const db = gs.dba; 7 | 8 | export default async function buildStats( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | if (!db) return res.status(500).end(); 13 | 14 | const response = await fetch("https://app.banana.dev/api/capacity"); 15 | const data = await response.json(); 16 | console.log(data); 17 | 18 | const entry = { 19 | date: new Date(), 20 | ...data, 21 | }; 22 | 23 | console.log(entry); 24 | 25 | await db.collection("bananaCapacity").insertOne(entry); 26 | 27 | res.status(200).end("OK"); 28 | } 29 | -------------------------------------------------------------------------------- /pages/api/bananaCheck.ts: -------------------------------------------------------------------------------- 1 | // Currently unused, as not possible to set { longPoll: false } 2 | // So not so useful to run serverless. 3 | // Instead we'll run on the client and call bananaUpdate afterwards. 4 | 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | // import GongoServer from "gongo-server/lib/serverless"; 7 | // import Database /* ObjectID */ from "gongo-server-db-mongo"; 8 | import { v4 as uuidv4 } from "uuid"; 9 | 10 | // const apiKey = process.env.BANANA_API_KEY; 11 | // const MONGO_URL = process.env.MONGO_URL || "mongodb://127.0.0.1"; 12 | 13 | /* 14 | const gs = new GongoServer({ 15 | dba: new Database(MONGO_URL, "sd-mui"), 16 | }); 17 | */ 18 | 19 | export default async function bananaCheck( 20 | req: NextApiRequest, 21 | res: NextApiResponse 22 | ) { 23 | console.log(req.query); 24 | const callID = req.query.callID; 25 | 26 | const payload = { 27 | id: uuidv4(), 28 | created: Math.floor(Date.now() / 1000), 29 | longPoll: false, // <-- main reason we can't use banana-node-sdk 30 | callID: callID, 31 | }; 32 | 33 | const start = Date.now(); 34 | console.log("request"); 35 | 36 | const response = await fetch("https://api.banana.dev/check/v4/", { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify(payload), 42 | }); 43 | 44 | const text = await response.text(); 45 | 46 | let result; 47 | try { 48 | result = JSON.parse(text); 49 | } catch (error) { 50 | console.log(text); 51 | throw error; 52 | } 53 | console.log("result", Date.now() - start); 54 | 55 | console.log(result); 56 | 57 | res.status(200).json(result); 58 | } 59 | -------------------------------------------------------------------------------- /pages/api/bananaUpdate.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import gs from "../../src/api-lib/db-full"; 3 | 4 | export default async function bananaUpdate( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method !== "POST") throw new Error("expected a POST"); 9 | if (typeof req.body !== "object") throw new Error("Body not decoded"); 10 | 11 | const { callID, step } = req.body; 12 | if (!callID) throw new Error("No callID provided"); 13 | if (!step) throw new Error("No callID provided"); 14 | 15 | // Try use client time if it seems reasonable, otherwise use our time. 16 | const date = (function () { 17 | let date; 18 | if (typeof step.date === "number" || typeof step.date === "string") { 19 | const now = new Date(); 20 | date = new Date(step.date); 21 | if (isNaN(date.getTime())) return now; 22 | 23 | // Disallow dates in the future 24 | if (date.getTime() > now.getTime()) return now; 25 | 26 | // Only allow dates in the past 5 seconds 27 | if (now.getTime() - date.getTime() > 5_000) return now; 28 | 29 | return date; 30 | } 31 | return new Date(); 32 | })(); 33 | 34 | const $set: { 35 | [key: string]: 36 | | { date: Date; value: number } 37 | | boolean 38 | | Date 39 | | number 40 | | undefined; 41 | finished?: boolean; 42 | finishedTime?: Date; 43 | totalTime?: number; 44 | } = { ["steps." + step.name]: { date, value: step.value } }; 45 | const update = { $set }; 46 | 47 | if (step.name === "finished") { 48 | console.log({ callID }); 49 | const existing = await (gs.dba && 50 | gs.dba.collection("bananaRequests").findOne({ callID })); 51 | console.log(existing); 52 | if (!existing) return res.status(200).end("OK"); 53 | 54 | $set.finished = true; 55 | $set.finishedTime = date; 56 | $set.totalTime = date.getTime() - existing.createdAt.getTime(); 57 | } 58 | 59 | console.log(update); 60 | 61 | if (gs.dba) 62 | await gs.dba.collection("bananaRequests").updateOne({ callID }, update); 63 | 64 | res.status(200).end("OK"); 65 | } 66 | -------------------------------------------------------------------------------- /pages/api/buildStats.ts: -------------------------------------------------------------------------------- 1 | // import * as banana from "@banana-dev/banana-dev"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { addDays, endOfDay, startOfDay } from "date-fns"; 4 | 5 | import gs from "../../src/api-lib/db-full"; 6 | 7 | const db = gs.dba; 8 | 9 | async function computeHourlyStats({ 10 | dayStart, 11 | dayEnd, 12 | }: { 13 | dayStart: Date; 14 | dayEnd: Date; 15 | }) { 16 | if (!db) return; 17 | const requests = await db.collection("bananaRequests").getReal(); 18 | const statsHourly = await db.collection("statsHourly").getReal(); 19 | 20 | const agg = ( 21 | await requests 22 | .aggregate([ 23 | { $match: { createdAt: { $gt: dayStart, $lt: dayEnd } } }, 24 | { 25 | $project: { 26 | year: { $year: "$createdAt" }, 27 | month: { $month: "$createdAt" }, 28 | day: { $dayOfMonth: "$createdAt" }, 29 | hour: { $hour: "$createdAt" }, 30 | }, 31 | }, 32 | { 33 | $group: { 34 | _id: { year: "$year", month: "$month", day: "$day", hour: "$hour" }, 35 | total: { $sum: 1 }, 36 | }, 37 | }, 38 | ]) 39 | .toArray() 40 | ).map((doc) => { 41 | // Possible to do this as part of aggregration query but a bit laborious 42 | const { year, month, day, hour } = doc._id; 43 | return { 44 | date: new Date(`${year}-${month}-${day} ${hour}:00:00Z`), 45 | total: doc.total, 46 | __updatedAt: Date.now(), 47 | }; 48 | }); 49 | 50 | console.log(agg); 51 | 52 | for (const hourlyStats of agg) 53 | await statsHourly.replaceOne({ date: hourlyStats.date }, hourlyStats, { 54 | upsert: true, 55 | }); 56 | } 57 | 58 | export default async function buildStats( 59 | req: NextApiRequest, 60 | res: NextApiResponse 61 | ) { 62 | // const day = req.query.day; 63 | // if (typeof day !== "string") return res.status(500).end("Invalid 'date' arg"); 64 | 65 | const range = []; 66 | const now = new Date(); 67 | 68 | for ( 69 | let date = addDays(new Date().setHours(0, 0, 0, 0), -1); 70 | date <= now; 71 | date = addDays(date, 1) 72 | ) { 73 | range.push(date); 74 | } 75 | console.log(range); 76 | 77 | if (!db) return res.status(500).end(); 78 | const users = await db.collection("users").getReal(); 79 | const requests = await db.collection("bananaRequests").getReal(); 80 | const statsDaily = await db.collection("statsDaily").getReal(); 81 | 82 | const models = await requests.distinct("callInputs.MODEL_ID"); 83 | 84 | for (const date of range) { 85 | const dayStart = startOfDay(date); 86 | const dayEnd = endOfDay(date); 87 | 88 | const realUserRequests = await db.collection("userRequests").getReal(); 89 | let requestsByUser = ( 90 | await realUserRequests 91 | .aggregate([ 92 | { $match: { date: { $gt: dayStart, $lt: dayEnd } } }, 93 | { $group: { _id: "$userId", requests: { $sum: 1 } } }, 94 | { $project: { userId: "$_id", _id: 0, requests: 1 } }, 95 | ]) 96 | .toArray() 97 | ).sort((a, b) => b.requests - a.requests); 98 | 99 | const CUTOFF = 10; 100 | if (requestsByUser.length > CUTOFF) { 101 | const requestByUserCutoff = new Array(CUTOFF + 1); 102 | const other = (requestByUserCutoff[CUTOFF] = { 103 | requests: 0, 104 | userId: "other", 105 | }); 106 | for (let i = 0; i < requestsByUser.length; i++) { 107 | if (i < CUTOFF) requestByUserCutoff[i] = requestsByUser[i]; 108 | else other.requests += requestsByUser[i].requests; 109 | } 110 | requestsByUser = requestByUserCutoff; 111 | } 112 | 113 | // TODO, aggregation pipeline, accumulate previous days totals 114 | const dayStats = { 115 | date, 116 | newUsers: await users.countDocuments({ 117 | createdAt: { $gt: dayStart, $lt: dayEnd }, 118 | }), 119 | totalUsers: await users.countDocuments({ createdAt: { $lt: dayEnd } }), 120 | newRequests: await requests.countDocuments({ 121 | createdAt: { $gt: dayStart, $lt: dayEnd }, 122 | }), 123 | totalRequests: await requests.countDocuments({ 124 | createdAt: { $lt: dayEnd }, 125 | }), 126 | requestsByModel: await Promise.all( 127 | models.map(async (model) => ({ 128 | model, 129 | requests: await requests.countDocuments({ 130 | "callInputs.MODEL_ID": model, 131 | createdAt: { $gt: dayStart, $lt: dayEnd }, 132 | }), 133 | })) 134 | ), 135 | requestsByUser, 136 | __updatedAt: Date.now(), 137 | }; 138 | 139 | await statsDaily.replaceOne({ date }, dayStats, { upsert: true }); 140 | await computeHourlyStats({ dayStart, dayEnd }); 141 | } 142 | 143 | res.status(200).end("OK"); 144 | } 145 | -------------------------------------------------------------------------------- /pages/api/createStripePaymentIntent.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import Stripe from "stripe"; 3 | 4 | import gs, { User, Order } from "../../src/api-lib/db"; 5 | import { AuthFromReq } from "../../src/api-lib/auth"; 6 | 7 | if (!process.env.STRIPE_SECRET_KEY) 8 | throw new Error("STRIPE_SECRET_KEY not defined"); 9 | 10 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 11 | apiVersion: "2022-08-01", 12 | }); 13 | 14 | export default async function craeateStripePaymentIntent( 15 | req: NextApiRequest, 16 | res: NextApiResponse 17 | ) { 18 | if (req.method !== "POST") throw new Error("expected a POST"); 19 | console.log(req.body); 20 | if (typeof req.body !== "object") throw new Error("Body not decoded"); 21 | if (!gs.dba) throw new Error("gs.dba not defined"); 22 | 23 | const auth = AuthFromReq(req); 24 | const userId = await auth.userId(); 25 | const numCredits = req.body.numCredits; 26 | 27 | if (numCredits != 100 && numCredits != 500 && numCredits != 1000) 28 | return res.status(400).send("Bad Request"); 29 | 30 | const costInUSD = numCredits === 100 ? 3 : numCredits === 500 ? 10 : 15; 31 | 32 | if (!userId) { 33 | return res.status(403).send("Forbidden"); 34 | } 35 | 36 | const user = (await gs.dba 37 | .collection("users") 38 | .findOne({ _id: userId })) as User | null; 39 | if (!user) return res.status(500).send("Server error"); 40 | 41 | if (!user.stripeCustomerId) { 42 | const customer = await stripe.customers.create({ 43 | name: user.displayName, 44 | email: user.emails[0].value, 45 | }); 46 | 47 | user.stripeCustomerId = customer.id; 48 | await gs.dba 49 | .collection("users") 50 | .updateOne( 51 | { _id: user._id }, 52 | { $set: { stripeCustomerId: customer.id } } 53 | ); 54 | } 55 | 56 | const order: Order = { 57 | userId: userId, 58 | amount: costInUSD * 100, 59 | currency: "usd", 60 | createdAt: new Date(), 61 | numCredits, 62 | }; 63 | 64 | const paymentIntent = await stripe.paymentIntents.create({ 65 | amount: order.amount, 66 | currency: order.currency, 67 | automatic_payment_methods: { 68 | enabled: true, 69 | }, 70 | customer: user.stripeCustomerId, 71 | // Automatic based on https://dashboard.stripe.com/settings/emails 72 | // receipt_email: user.emails[0].value, 73 | description: numCredits + " credits on kiri.art", 74 | statement_descriptor: "KIRI.ART", 75 | }); 76 | 77 | order.stripePaymentIntentId = paymentIntent.id; 78 | order.stripePaymentIntentStatus = paymentIntent.status; 79 | 80 | const result = await gs.dba.collection("orders").insertOne(order); 81 | const id = result.insertedId; 82 | 83 | res.send({ 84 | orderId: id, 85 | clientSecret: paymentIntent.client_secret, 86 | numCredits, 87 | costInUSD, 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /pages/api/creditTopup.ts: -------------------------------------------------------------------------------- 1 | // import * as banana from "@banana-dev/banana-dev"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | import gs from "../../src/api-lib/db"; 5 | import { DAILY_FREE_CREDITS } from "../../src/config/constants"; 6 | 7 | const db = gs.dba; 8 | 9 | export default async function buildStats( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | if (!db) return res.status(500).end(); 14 | const users = await db.collection("users").getReal(); 15 | 16 | if (req.query.API_KEY === process.env.API_KEY) { 17 | await users.updateMany( 18 | { 19 | "credits.free": { $lt: DAILY_FREE_CREDITS }, 20 | }, 21 | { 22 | $set: { 23 | "credits.free": DAILY_FREE_CREDITS, 24 | __updatedAt: Date.now(), 25 | }, 26 | } 27 | ); 28 | } 29 | 30 | res.status(200).end("OK"); 31 | } 32 | -------------------------------------------------------------------------------- /pages/api/csend.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import crypto from "crypto"; 3 | 4 | import gs from "../../src/api-lib/db-full"; 5 | import { CSend, BananaRequest } from "../../src/schemas"; 6 | 7 | const csends = gs.dba && gs.dba.collection("csends"); 8 | const bananaRequests = gs.dba && gs.dba.collection("bananaRequests"); 9 | 10 | async function aggregateRequestCsends(inferDone: CSend) { 11 | if (!(csends && bananaRequests)) return; 12 | 13 | const { container_id } = inferDone; 14 | 15 | const initStart = await csends.findOne({ 16 | container_id, 17 | type: "init", 18 | status: "start", 19 | }); 20 | const initDone = await csends.findOne({ 21 | container_id, 22 | type: "init", 23 | status: "done", 24 | }); 25 | const inferStart = ( 26 | await ( 27 | await csends.getReal() 28 | ) 29 | .find({ 30 | container_id, 31 | type: "inference", 32 | status: "start", 33 | }) 34 | .sort({ date: -1 }) 35 | .limit(1) 36 | .toArray() 37 | )[0]; 38 | 39 | if (!(initStart && inferStart && initDone)) { 40 | console.warn("Missing", { initStart, inferStart, initDone }); 41 | return; 42 | } 43 | 44 | // assume first inference for now... TODO... 45 | const query = { startRequestId: inferStart.payload.startRequestId }; 46 | const bananaRequest = (await bananaRequests.findOne( 47 | query 48 | )) as BananaRequest | null; 49 | 50 | if (bananaRequest) { 51 | const loadTime = 52 | initStart.date.getTime() - bananaRequest.createdAt.getTime(); 53 | const update = { 54 | $set: { 55 | times: { 56 | load: loadTime > 0 ? loadTime : null, 57 | init: loadTime > 0 ? initDone.tsl : null, 58 | inference: inferDone.tsl, 59 | }, 60 | }, 61 | }; 62 | // console.log(query, update); 63 | await bananaRequests.updateOne(query, update); 64 | } 65 | } 66 | 67 | export default async function CSendRequest( 68 | req: NextApiRequest, 69 | res: NextApiResponse 70 | ) { 71 | // TODO, remove. 72 | if (req.method === "GET" && req.query.type === "rebuild" && csends) { 73 | const events: CSend[] = (await csends 74 | .find({ type: "inference", status: "done" }) 75 | .toArray()) as unknown as CSend[]; 76 | for (const event of events) await aggregateRequestCsends(event); 77 | return res.status(200).end("OK"); 78 | } 79 | 80 | if (req.method !== "POST") return res.status(400).end("expected a POST"); 81 | if (typeof req.body !== "object") 82 | return res.status(400).end("body not decoded"); 83 | 84 | const data = req.body; 85 | 86 | const containerSig = data.sig as string; 87 | delete data.sig; 88 | const ourSig = crypto 89 | .createHash("md5") 90 | .update(JSON.stringify(data) + process.env.SIGN_KEY) 91 | .digest("hex"); 92 | const match = containerSig === ourSig; 93 | 94 | if (!match) return res.status(200).end("OK"); 95 | 96 | data.date = new Date(data.time); 97 | delete data.time; 98 | 99 | console.log(JSON.stringify(data, null, 2)); 100 | csends && (await csends.insertOne(data)); 101 | 102 | if (data.type === "inference" && data.status === "done") { 103 | if (!(csends && bananaRequests)) 104 | throw new Error("No csends / bananaRequests collections"); 105 | await aggregateRequestCsends(data); 106 | } 107 | 108 | /* 109 | 110 | const { callID, step } = req.body; 111 | if (!callID) throw new Error("No callID provided"); 112 | if (!step) throw new Error("No callID provided"); 113 | 114 | const date = step.date ? new Date(step.date * 1000) : new Date(); 115 | const $set = { ["steps." + step.name]: { date, value: step.value } }; 116 | const update = { $set }; 117 | 118 | if (step.name === "finished") { 119 | console.log({ callID }); 120 | const existing = await (gs.dba && 121 | gs.dba.collection("bananaRequests").findOne({ callID })); 122 | console.log(existing); 123 | if (!existing) return res.status(200).end("OK"); 124 | 125 | // @ts-expect-error: TODO 126 | $set.finished = true; 127 | 128 | let finishedTime; 129 | if (typeof step.date === "number") finishedTime = new Date(step.date); 130 | else if (typeof step.date === "string") finishedTime = new Date(step.date); 131 | else finishedTime = new Date(); 132 | 133 | // @ts-expect-error: TODO 134 | $set.finishedTime = finishedTime; 135 | // @ts-expect-error: TODO 136 | $set.totalTime = finishedTime - existing.createdAt; 137 | } 138 | 139 | console.log(update); 140 | 141 | if (gs.dba) 142 | await gs.dba.collection("bananaRequests").updateOne({ callID }, update); 143 | 144 | */ 145 | 146 | res.status(200).end("OK"); 147 | } 148 | -------------------------------------------------------------------------------- /pages/api/file2.ts: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | import crypto from "crypto"; 3 | import sharp from "sharp"; 4 | import { fileTypeFromBuffer } from "file-type"; 5 | 6 | import { ObjectId } from "bson"; 7 | import gs /* Auth, User, Order, ObjectId */ from "../../src/api-lib/db"; 8 | // import { format } from 'date-fns'; 9 | 10 | const AWS_S3_BUCKET = "kiri-art"; 11 | 12 | const defaults = { 13 | AWS_REGION: "eu-west-3", // Paris 14 | }; 15 | 16 | const env = process.env; 17 | AWS.config.update({ 18 | accessKeyId: env.AWS_ACCESS_KEY_ID_APP || env.AWS_ACCESS_KEY_ID, 19 | secretAccessKey: env.AWS_SECRET_ACCESS_KEY_APP || env.AWS_SECRET_ACCESS_KEY, 20 | region: env.AWS_REGION_APP || env.AWS_REGION || defaults.AWS_REGION, 21 | }); 22 | 23 | if (!gs.dba) throw new Error("gs.dba not set"); 24 | 25 | interface FileEntry { 26 | [key: string]: unknown; 27 | // _id: string | ObjectId; 28 | _id: ObjectId; 29 | filename?: string; 30 | sha256: string; 31 | size: number; 32 | type: string; // "image", 33 | mimeType?: string; 34 | createdAt: Date; 35 | image?: { 36 | format: sharp.Metadata["format"]; 37 | size?: number; 38 | width?: number; 39 | height?: number; 40 | }; 41 | } 42 | 43 | const Files = gs.dba.collection("files"); 44 | 45 | async function createFileFromBuffer( 46 | buffer: Buffer, 47 | { 48 | filename, 49 | mimeType, 50 | size, 51 | existingId, 52 | ...extra 53 | }: { 54 | filename?: string; 55 | mimeType?: string; 56 | size?: number; 57 | existingId?: string; 58 | extra?: Record; 59 | } = {} 60 | ) { 61 | // TODO, check if it's an image. 62 | 63 | const sha256 = crypto.createHash("sha256").update(buffer).digest("hex"); 64 | const image = sharp(buffer); 65 | const metadata = await image.metadata(); 66 | const now = new Date(); 67 | 68 | size = size || Buffer.byteLength(buffer); 69 | if (!mimeType) { 70 | const fileType = await fileTypeFromBuffer(buffer); 71 | if (fileType) mimeType = fileType.mime; 72 | } 73 | 74 | const entry: FileEntry = { 75 | // _id: existingId || new ObjectId(), 76 | _id: new ObjectId(existingId), 77 | filename, 78 | sha256, 79 | size: size, 80 | type: "image", 81 | mimeType, 82 | createdAt: now, 83 | image: { 84 | format: metadata.format, 85 | size: metadata.size, 86 | width: metadata.width, 87 | height: metadata.height, 88 | }, 89 | ...extra, 90 | }; 91 | 92 | console.log(entry); 93 | 94 | const params = { 95 | Bucket: AWS_S3_BUCKET, 96 | Key: sha256, 97 | Body: buffer, 98 | }; 99 | 100 | console.log(params); 101 | 102 | const result = await new AWS.S3().putObject(params).promise(); 103 | console.log({ result }); 104 | 105 | if (existingId) { 106 | const $set = (({ _id, ...rest }) => rest)(entry); 107 | await Files.updateOne({ _id: new ObjectId(existingId) }, { $set }); 108 | } else { 109 | await Files.insertOne(entry); 110 | } 111 | 112 | // return [entry, buffer]; 113 | return entry; 114 | } 115 | 116 | export type { FileEntry }; 117 | export { createFileFromBuffer }; 118 | -------------------------------------------------------------------------------- /pages/api/gongoAuth.ts: -------------------------------------------------------------------------------- 1 | // import type { NextApiRequest, NextApiResponse } from "next"; 2 | import GongoAuth from "gongo-server/lib/auth"; 3 | import { MongoDbaUser } from "gongo-server-db-mongo"; 4 | import gs from "../../src/api-lib/db-full"; 5 | // import { ipPass, ipFromReq } from "../../src/api-lib/ipCheck"; 6 | 7 | /* eslint-disable @typescript-eslint/no-var-requires */ 8 | const passport = require("passport"); 9 | const GoogleStrategy = require("passport-google-oauth20").Strategy; 10 | // import passport from "passport"; 11 | // import { Strategy as GoogleStrategy } from "passport-google-oauth2"; 12 | const GithubStrategy = require("passport-github2").Strategy; 13 | const TwitterStrategy = require("passport-twitter").Strategy; 14 | 15 | const env = process.env; 16 | const ROOT_URL = ( 17 | env.ROOT_URL || 18 | "http" + 19 | (env.VERCEL_URL && env.VERCEL_URL.match(/^localhost:/) ? "" : "s") + 20 | "://" + 21 | env.VERCEL_URL 22 | ).replace(/\/$/, ""); 23 | 24 | /* 25 | console.log({ 26 | ENV_ROOT_URL: env.ROOT_URL, 27 | ENV_VERCEL_URL: env.VERCEL_URL, 28 | CHOSEN_ROOT_URL: ROOT_URL, 29 | }); 30 | */ 31 | 32 | const gongoAuth = new GongoAuth(gs, passport); 33 | // gs.db.Users.ensureAdmin('dragon@wastelands.net', 'initialPassword'); 34 | 35 | gongoAuth.use( 36 | new GoogleStrategy( 37 | { 38 | clientID: process.env.GOOGLE_CLIENT_ID, 39 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 40 | callbackURL: ROOT_URL + "/api/gongoAuth?service=google", 41 | passReqToCallback: true, 42 | scope: "email+profile", 43 | }, 44 | gongoAuth.passportVerify 45 | ), 46 | { 47 | //scope: 'https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email' 48 | scope: "email+profile", 49 | } 50 | ); 51 | 52 | gongoAuth.use( 53 | new GithubStrategy( 54 | { 55 | clientID: process.env.GITHUB_CLIENT_ID, 56 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 57 | callbackURL: ROOT_URL + "/api/gongoAuth?service=github", 58 | passReqToCallback: true, 59 | scope: "user:email", 60 | allRawEmails: true, 61 | }, 62 | gongoAuth.passportVerify 63 | ), 64 | { 65 | //scope: 'https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email' 66 | scope: "user:email", 67 | } 68 | ); 69 | 70 | gongoAuth.use( 71 | new TwitterStrategy( 72 | { 73 | consumerKey: process.env.TWITTER_CONSUMER_KEY, 74 | consumerSecret: process.env.TWITTER_CONSUMER_SECRET, 75 | callbackURL: ROOT_URL + "/api/gongoAuth?service=twitter", 76 | passReqToCallback: true, 77 | includeEmail: true, 78 | }, 79 | gongoAuth.passportVerify 80 | ), 81 | {} 82 | ); 83 | 84 | //module.exports = passport.authenticate('google', gongoAuth.passportComplete); 85 | 86 | if (gs.dba) { 87 | // TODO, implement onCreateUser hook in gongo-server. For now: 88 | const Users = gs.dba.Users; 89 | const origCreateUser = Users.createUser; 90 | gs.dba.Users.createUser = async function sbMuiCreateUser( 91 | origCallback?: ((dbaUser: Partial) => void) | undefined 92 | ) { 93 | function callback(user: Partial): void { 94 | origCallback && origCallback(user); 95 | user.credits = { free: 20, paid: 0 }; 96 | user.createdAt = new Date(); 97 | } 98 | return origCreateUser.call(Users, callback); 99 | }; 100 | } 101 | 102 | // TODO Sure we can move this all into gongo-server. 103 | // @ts-expect-error: any 104 | export default async function handler(req, res) { 105 | /* 106 | if ( 107 | process.env.NODE_ENV === "production" && 108 | !(await ipPass(ipFromReq(req))) 109 | ) { 110 | res.status(403).end("Forbidden; IP not allowed"); 111 | return; 112 | } 113 | */ 114 | 115 | if (req.query.type === "setup") { 116 | gongoAuth.ensureDbStrategyData().then(() => res.end("OK")); 117 | return; 118 | } 119 | 120 | if (req.query.poll) 121 | return gs.expressPost()(req, res, () => { 122 | console.log("next"); 123 | }); 124 | 125 | if (!req.query.service) 126 | return res.status(400).end("No ?service= param specified"); 127 | 128 | if (req.query.state) { 129 | if (!gs.dba) throw new Error("no gs.dba"); 130 | const session = await gs.dba.collection("sessions").findOne({ 131 | $or: [ 132 | { 133 | ["oauth:" + req.query.service + ".state.handle"]: 134 | req.query.state || "NOMATCH", 135 | }, 136 | { 137 | ["oauth2:" + req.query.service + ".state.handle"]: 138 | req.query.state || "NOMATCH", 139 | }, 140 | ], 141 | }); 142 | req.session = session; 143 | } else if (req.query.oauth_token) { 144 | if (!gs.dba) throw new Error("no gs.dba"); 145 | const session = 146 | (await gs.dba.collection("sessions").findOne({ 147 | ["oauth:" + req.query.service + ".oauth_token"]: 148 | req.query.oauth_token || "NOMATCH", 149 | })) || {}; 150 | req.session = session; 151 | } 152 | 153 | const next = () => 154 | res.status(400).end("No such service: " + req.query.service); 155 | 156 | const strategy = passport._strategies[req.query.service]; 157 | if (!strategy) res.status(400).end("No such service: " + req.query.service); 158 | const authOpts: { scope?: string } = {}; 159 | 160 | // untested with multiple scopes, _scopeSeparator, array, etc. 161 | if (strategy._scope) authOpts.scope = strategy._scope; 162 | 163 | passport.authenticate( 164 | req.query.service, 165 | authOpts, 166 | gongoAuth.boundPassportComplete(req, res) 167 | )(req, res, next); 168 | } 169 | -------------------------------------------------------------------------------- /pages/api/imgFetchAndDelete.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import os from "node:os"; 3 | import fs from "node:fs/promises"; 4 | 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | 7 | export default async function imgFetchAndDelete( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if ( 12 | !(typeof req.query.dir === "string" && req.query.dir.startsWith("sd-mui")) 13 | ) { 14 | res.status(400); 15 | res.end(); 16 | return; 17 | } 18 | 19 | const dir = path.join(os.tmpdir(), req.query.dir); 20 | 21 | // Think about this more... process is killed after stream ends, 22 | // before we can clean up. So let's load to memory first. 23 | 24 | const samples = path.join(dir, "samples"); 25 | const imgPath = path.join(samples, "00000.png"); 26 | const img = await fs.readFile(imgPath); 27 | await fs.rm(imgPath); 28 | await fs.rmdir(path.join(dir, "samples")); 29 | await fs.rmdir(dir); 30 | 31 | res.writeHead(200, { 32 | "Content-Type": "image/png", 33 | }); 34 | res.end(img); 35 | } 36 | -------------------------------------------------------------------------------- /pages/api/mongoRelay.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | import makeExpressRelay from "mongodb-rest-relay/lib/express"; 3 | 4 | const MONGO_URL = process.env.MONGO_URL || "mongodb://127.0.0.1"; 5 | const client = new MongoClient(MONGO_URL); 6 | 7 | // ts fails sometimes here (but not always???) because of version mismatch 8 | export default makeExpressRelay((await client.connect()).db("sd-mui")); 9 | -------------------------------------------------------------------------------- /pages/api/myData.ts: -------------------------------------------------------------------------------- 1 | import Stream, { TransformCallback } from "stream"; 2 | import { NextApiRequest, NextApiResponse } from "next"; 3 | import { WithId, Document } from "mongodb"; 4 | import JSZip from "jszip"; 5 | 6 | import gs from "../../src/api-lib/db-full"; 7 | 8 | class ToJSON extends Stream.Transform { 9 | sentFirst = false; 10 | last: WithId | null = null; 11 | 12 | constructor() { 13 | super({ objectMode: true }); 14 | } 15 | 16 | _transform( 17 | data: WithId, 18 | encoding: BufferEncoding, 19 | callback: TransformCallback 20 | ) { 21 | if (!this.sentFirst) { 22 | callback(null, "[\n"); 23 | this.sentFirst = true; 24 | } 25 | 26 | if (this.last) { 27 | callback(null, JSON.stringify(this.last, null, 2) + ",\n"); 28 | } 29 | 30 | this.last = data; 31 | } 32 | 33 | _flush(callback: TransformCallback) { 34 | callback(null, JSON.stringify(this.last, null, 2) + "\n]\n"); 35 | } 36 | } 37 | 38 | export default async function myData( 39 | req: NextApiRequest, 40 | res: NextApiResponse 41 | ) { 42 | const { sessionId } = req.query; 43 | const { cookie } = req.headers; 44 | const nextAuthSessionId = 45 | cookie && cookie.match(/\bnext-auth.session-token=([^;]+)/)?.[1]; 46 | 47 | if (!gs.dba) 48 | return res 49 | .status(500) 50 | .send("Database not connected. Please try again later."); 51 | 52 | const db = await gs.dba.dbPromise; 53 | 54 | const query: Record = {}; 55 | 56 | if (sessionId) { 57 | query._id = sessionId; 58 | } else { 59 | query.sessionToken = nextAuthSessionId; 60 | } 61 | 62 | const session = await db.collection("sessions").findOne(query); 63 | 64 | if (!session) return res.status(401).send("Session not found"); 65 | 66 | const user = await db.collection("users").findOne({ _id: session.userId }); 67 | if (!user) return res.status(500).send("User not found"); 68 | 69 | res.writeHead(200, { 70 | "Content-Type": "application/zip", 71 | "Content-Disposition": `attachment; filename=kiri-${user._id}.zip`, 72 | }); 73 | 74 | const zip = new JSZip(); 75 | zip.file("users.json", JSON.stringify(user, null, 2)); 76 | 77 | const collections = await db.collections(); 78 | for (const collection of collections) { 79 | const name = collection.collectionName; 80 | if (name === "users") continue; 81 | 82 | const query = { userId: user._id }; 83 | const exists = await collection.findOne(query); 84 | if (!exists) continue; 85 | 86 | zip.file( 87 | `${name}.json`, 88 | collection.find(query).stream().pipe(new ToJSON()) 89 | ); 90 | } 91 | 92 | zip.generateNodeStream({ streamFiles: true }).pipe(res); 93 | } 94 | -------------------------------------------------------------------------------- /pages/api/myDataDelete.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import gs from "../../src/api-lib/db-full"; 4 | 5 | export default async function myDataDelete( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | const { sessionId } = req.query; 10 | 11 | if (!gs.dba) 12 | return res 13 | .status(500) 14 | .send("Database not connected. Please try again later."); 15 | 16 | const db = await gs.dba.dbPromise; 17 | 18 | // @ts-expect-error: note, sessionId is a string. this is a bug in gongo 19 | // that uses string ids for sessions. 20 | const session = await db.collection("sessions").findOne({ _id: sessionId }); 21 | if (!session) return res.status(401).send("Session not found"); 22 | 23 | const user = await db.collection("users").findOne({ _id: session.userId }); 24 | if (!user) return res.status(500).send("User not found"); 25 | 26 | const query = { userId: user._id }; 27 | const updated = { __deleted: true, __updatedAt: Date.now() }; 28 | 29 | await db.collection("users").updateOne({ _id: user._id }, updated); 30 | 31 | const collections = await db.collections(); 32 | for (const collection of collections) { 33 | const name = collection.collectionName; 34 | if (name === "users") continue; // _id not userId 35 | 36 | await collection.updateMany(query, updated); 37 | } 38 | 39 | res.status(200).send("OK"); 40 | } 41 | -------------------------------------------------------------------------------- /pages/api/providerFetch.ts: -------------------------------------------------------------------------------- 1 | import createHandler from "../../src/lib/providerFetch/handlerServerless"; 2 | import gs /* { CreditCode, ObjectId, User } */ from "../../src/api-lib/db"; 3 | import Auth from "gongo-server/lib/auth-class"; 4 | import GongoServer from "gongo-server/lib/serverless"; 5 | import Database /* ObjectID */ from "gongo-server-db-mongo"; 6 | 7 | export const config = { 8 | api: { 9 | bodyParser: { 10 | sizeLimit: "4mb", 11 | }, 12 | }, 13 | }; 14 | 15 | export default createHandler({ gs, Auth, GongoServer, Database }); 16 | -------------------------------------------------------------------------------- /pages/api/starItem.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import sanitizeFilename from "sanitize-filename"; 3 | 4 | import type NodeCol from "../../src/schemas/lib/NodeCol"; 5 | import type Star from "../../src/schemas/star"; 6 | import sharedInputTextFromInputs from "../../src/lib/sharedInputTextFromInputs"; 7 | import { 8 | ddaCallInputsSchema, 9 | ddaModelInputsSchema, 10 | // bananaRequestSchema, 11 | } from "../../src/schemas"; 12 | import { createFileFromBuffer } from "./file2"; 13 | import gs from "../../src/api-lib/db"; 14 | import { AuthFromReq } from "../../src/api-lib/auth"; 15 | import { getMimeTypeFromBuffer, extensions } from "../../src/lib/mimeTypes"; 16 | 17 | if (!gs.dba) throw new Error("gs.dba not defined"); 18 | 19 | const Stars = gs.dba.collection("stars"); 20 | 21 | function BufferFromBase64(base64: string | undefined) { 22 | return base64 && Buffer.from(base64, "base64"); 23 | } 24 | 25 | export const config = { 26 | api: { 27 | bodyParser: { 28 | sizeLimit: "4mb", 29 | }, 30 | }, 31 | }; 32 | 33 | export default async function starItem( 34 | req: NextApiRequest, 35 | res: NextApiResponse 36 | ) { 37 | const item = req.body?.item; 38 | if (!gs.dba) return res.status(500).end(); 39 | 40 | const auth = AuthFromReq(req); 41 | const userId = await auth.userId(); 42 | 43 | if (!userId) return res.status(401).end("Unauthorized"); 44 | 45 | const modelInputs = await ddaModelInputsSchema.validate(item.modelInputs); 46 | const callInputs = await ddaCallInputsSchema.validate(item.callInputs); 47 | const result = item.result; 48 | 49 | const simulatedModelState = { 50 | prompt: { value: modelInputs.prompt || "" }, 51 | shareInputs: { value: true }, 52 | guidance_scale: { value: modelInputs.guidance_scale }, 53 | num_inference_steps: { value: modelInputs.num_inference_steps }, 54 | seed: { value: modelInputs.seed as number }, 55 | negative_prompt: { value: modelInputs.negative_prompt || "" }, 56 | }; 57 | 58 | const images = { 59 | output: BufferFromBase64(result?.modelOutputs?.[0]?.image_base64), 60 | init: BufferFromBase64(modelInputs?.image), 61 | mask: BufferFromBase64(modelInputs?.mask_image), 62 | }; 63 | if (!images.output) 64 | return res.status(400).end("Bad Request - no output file"); 65 | delete result?.modelOutputs?.[0]?.image_base64; 66 | delete modelInputs?.image; 67 | delete modelInputs?.mask_image; 68 | 69 | const sharedInputs = sharedInputTextFromInputs(simulatedModelState); 70 | const mimeType = getMimeTypeFromBuffer(images.output); 71 | const ext = extensions[mimeType]; 72 | const filename = sanitizeFilename(sharedInputs + "." + ext); 73 | 74 | const files: Star["files"] = { 75 | // @ts-expect-error: objectid 76 | output: (await createFileFromBuffer(images.output, { filename }))._id, 77 | }; 78 | if (images.init) 79 | // @ts-expect-error: objectid 80 | files.init = ( 81 | await createFileFromBuffer(images.init, { 82 | filename: "init_image.jpg", // TODO, file ext 83 | }) 84 | )._id; 85 | if (images.mask) 86 | // @ts-expect-error: objectid 87 | files.mask = ( 88 | await createFileFromBuffer(images.mask, { 89 | filename: "mask_image.jpg", // TODO, file ext 90 | }) 91 | )._id; 92 | 93 | console.log(images); 94 | console.log(files); 95 | 96 | const entry: Partial> = { 97 | userId, 98 | date: new Date(), 99 | callInputs, 100 | modelInputs, 101 | files, 102 | // stars: 1, 103 | // starredBy: [userId], 104 | likes: 0, 105 | }; 106 | 107 | console.log(entry); 108 | const insertResult = await Stars.insertOne(entry); 109 | const { insertedId } = insertResult; 110 | 111 | entry._id = insertedId; 112 | 113 | res.status(200).json(entry); 114 | } 115 | -------------------------------------------------------------------------------- /pages/api/stripe.ts: -------------------------------------------------------------------------------- 1 | import { buffer } from "micro"; 2 | import Cors from "micro-cors"; 3 | import { NextApiRequest, NextApiResponse } from "next"; 4 | import Stripe from "stripe"; 5 | 6 | import { dba } from "../../src/api-lib/db"; 7 | 8 | if (!process.env.STRIPE_SECRET_KEY) 9 | throw new Error("process.env.STRIPE_SECRET_KEY not set"); 10 | 11 | if (!process.env.STRIPE_WEBHOOK_SECRET) 12 | throw new Error("process.env.STRIPE_WEBHOOK_SECRET not set"); 13 | 14 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 15 | // https://github.com/stripe/stripe-node#configuration 16 | apiVersion: "2022-08-01", 17 | }); 18 | 19 | const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET; 20 | 21 | // Stripe requires the raw body to construct the event. 22 | export const config = { 23 | api: { 24 | bodyParser: false, 25 | }, 26 | }; 27 | 28 | const cors = Cors({ 29 | allowMethods: ["POST", "HEAD"], 30 | }); 31 | 32 | const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { 33 | if (req.method === "POST") { 34 | const buf = await buffer(req); 35 | 36 | if (!req.headers["stripe-signature"]) 37 | throw new Error("No stripe-signature"); 38 | 39 | const sig = req.headers["stripe-signature"]; 40 | 41 | let event: Stripe.Event; 42 | 43 | try { 44 | event = stripe.webhooks.constructEvent( 45 | buf.toString(), 46 | sig, 47 | webhookSecret 48 | ); 49 | } catch (err) { 50 | const errorMessage = err instanceof Error ? err.message : "Unknown error"; 51 | // On error, log and return the error message. 52 | if (err instanceof Error) console.log(err); 53 | console.log(`❌ Error message: ${errorMessage}`); 54 | res.status(400).send(`Webhook Error: ${errorMessage}`); 55 | return; 56 | } 57 | 58 | // Successfully constructed event. 59 | console.log("✅ Success:", event.id); 60 | 61 | // Cast event data to Stripe object. 62 | if (event.type === "payment_intent.succeeded") { 63 | const paymentIntent = event.data.object as Stripe.PaymentIntent; 64 | console.log(`💰 PaymentIntent status: ${paymentIntent.status}`); 65 | 66 | const order = await (dba && 67 | dba 68 | .collection("orders") 69 | .findOne({ stripePaymentIntentId: paymentIntent.id })); 70 | 71 | if (!order) { 72 | throw new Error( 73 | "Could not find order with matching stripePaymentIntentId: " + 74 | paymentIntent.id 75 | ); 76 | } 77 | 78 | await (dba && 79 | dba 80 | .collection("orders") 81 | .updateOne( 82 | { stripePaymentIntentId: paymentIntent.id }, 83 | { $set: { stripePaymentIntentStatus: paymentIntent.status } } 84 | )); 85 | 86 | await (dba && 87 | dba.collection("users").updateOne( 88 | { 89 | _id: order.userId, 90 | }, 91 | { 92 | $inc: { "credits.paid": order.numCredits }, 93 | } 94 | )); 95 | } else if (event.type === "payment_intent.payment_failed") { 96 | const paymentIntent = event.data.object as Stripe.PaymentIntent; 97 | console.log( 98 | `❌ Payment failed: ${paymentIntent.last_payment_error?.message}` 99 | ); 100 | 101 | await (dba && 102 | dba.collection("orders").updateOne( 103 | { stripePaymentIntentId: paymentIntent.id }, 104 | { 105 | $set: { 106 | stripePaymentIntentStatus: paymentIntent.status, 107 | stripePaymentFailedReason: 108 | paymentIntent.last_payment_error?.message, 109 | }, 110 | } 111 | )); 112 | } else if (event.type === "charge.succeeded") { 113 | const charge = event.data.object as Stripe.Charge; 114 | console.log(`💵 Charge id: ${charge.id}`); 115 | } else { 116 | console.warn(`🤷‍♀️ Unhandled event type: ${event.type}`); 117 | } 118 | 119 | // Return a response to acknowledge receipt of the event. 120 | res.json({ received: true }); 121 | } else { 122 | res.setHeader("Allow", "POST"); 123 | res.status(405).end("Method Not Allowed"); 124 | } 125 | }; 126 | 127 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 | export default cors(webhookHandler as any); 129 | -------------------------------------------------------------------------------- /pages/api/txt2img-banana.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | export default async function txt2imgFetch( 4 | req: NextApiRequest, 5 | res: NextApiResponse 6 | ) { 7 | res.status(200).end("RELOAD APP"); 8 | } 9 | -------------------------------------------------------------------------------- /pages/checkout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import { t, Trans } from "@lingui/macro"; 4 | import { useGongoUserId } from "gongo-client-react"; 5 | import { Elements } from "@stripe/react-stripe-js"; 6 | import { loadStripe, Stripe } from "@stripe/stripe-js"; 7 | import CheckoutForm from "../src/CheckoutForm"; 8 | import { signIn } from "next-auth/react"; 9 | 10 | import { Container, Typography } from "@mui/material"; 11 | 12 | import MyAppBar from "../src/MyAppBar"; 13 | 14 | let stripePromise: Promise; 15 | const getStripe = () => { 16 | if (!stripePromise) { 17 | if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) 18 | throw new Error("process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY not set"); 19 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); 20 | } 21 | return stripePromise; 22 | }; 23 | 24 | export default function Checkout() { 25 | const router = useRouter(); 26 | const { clientSecret, orderId, numCredits, costInUSD } = router.query; 27 | const userId = useGongoUserId(); 28 | /* 29 | const user = useGongoOne((db) => 30 | db.collection("users").find({ _id: userId }) 31 | ); 32 | */ 33 | 34 | if (typeof clientSecret !== "string" || typeof orderId !== "string") { 35 | if ( 36 | Object.keys(router.query).length === 0 && 37 | typeof location === "object" && 38 | location.href.match(/clientSecret/) 39 | ) 40 | return
Loading...
; 41 | 42 | throw new Error( 43 | "either clientSecret or orderId query param is not a string" 44 | ); 45 | } 46 | 47 | if (!userId) { 48 | signIn(); 49 | return null; 50 | } 51 | 52 | if (!clientSecret) return
Loading...
; 53 | 54 | const options = { 55 | // passing the client secret obtained from the server 56 | clientSecret, 57 | loader: "auto" as const, 58 | }; 59 | 60 | return ( 61 | <> 62 | 63 | 64 | 65 | Checkout 66 | 67 | 68 |

69 | {numCredits} credits 70 |

71 | 72 |

73 | 74 | TOTAL: USD $ 75 | {typeof costInUSD === "string" && parseFloat(costInUSD).toFixed(2)} 76 | 77 |

78 | 79 |

80 | 81 | Secure Payment via Stripe.com and partners. 82 | 83 |

84 | 85 | 86 | 87 | 88 |
89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /pages/faq.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | import MyAppBar from "../src/MyAppBar"; 5 | import { Container, Typography, Box } from "@mui/material"; 6 | import { Trans } from "@lingui/macro"; 7 | 8 | export default function FAQ() { 9 | useRouter(); 10 | 11 | const sections = [ 12 | { 13 | title: "General", 14 | qa: [ 15 | [ 16 | "Where does the money go?", 17 | "Currently, everything is going towards covering running costs. If this ever turns a profit, it will fund continued development, plus I also have some ideas for compensation to model authors in the future.", 18 | ], 19 | [ 20 | "What are some good resources to get started?", 21 |
    22 |
  1. 23 |

    24 | 25 | Awesome Stable-Diffusion 26 | 27 |
    A curated list of SD resources, guides, tips and software. 28 |

    29 |
  2. 30 | 31 |
  3. 32 |

    33 | 34 | r/StableDiffusion (reddit) 35 | 36 |
    37 | Guides, image shares, experiments, finds, community. 38 |

    39 |
  4. 40 | 41 |
  5. 42 |

    43 | 44 | SD Akashic Guide 45 | 46 |
    47 | SD studies, art styles, prompts. 48 |

    49 |
  6. 50 | 51 |
  7. 52 |

    53 | Lexica.art 54 |
    55 | Search 5M+ SD prompts & images. 56 |

    57 |
  8. 58 | 59 |
  9. 60 |

    61 | 62 | Suggest more resources in a{" "} 63 | 64 | GitHub Issue 65 | 66 | . 67 | 68 |

    69 |
  10. 70 |
, 71 | ], 72 | ], 73 | }, 74 | ]; 75 | 76 | return ( 77 | <> 78 | 79 | 80 | 81 | Frequently Asked Questions (FAQ) 82 | 83 | 84 |
    85 | {sections.map(({ title }) => ( 86 |
  1. 87 | {title} 88 |
  2. 89 | ))} 90 |
91 | 92 | {sections.map(({ title, qa }) => ( 93 | 94 | 95 | {title} 96 |
    97 | {qa.map(([question, answer]) => ( 98 |
  1. 99 |
    100 | {question} 101 |
    102 |
    {answer}
    103 |
  2. 104 | ))} 105 |
106 |
107 | ))} 108 |
109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /pages/img2img.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const Img2Img = dynamic(() => import("../src/Img2img"), { 4 | ssr: false, 5 | }); 6 | 7 | import React from "react"; 8 | import { t } from "@lingui/macro"; 9 | 10 | import { Container } from "@mui/material"; 11 | 12 | import MyAppBar from "../src/MyAppBar"; 13 | 14 | export default function Img2img() { 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /pages/inpaint.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const Inpaint = dynamic(() => import("../src/Inpaint"), { 4 | ssr: false, 5 | }); 6 | 7 | import React from "react"; 8 | import { t } from "@lingui/macro"; 9 | 10 | import { Container } from "@mui/material"; 11 | 12 | import MyAppBar from "../src/MyAppBar"; 13 | 14 | export default function Inpainting() { 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /pages/ipix2pix.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const IPix2Pix = dynamic(() => import("../src/IPix2Pix"), { 4 | ssr: false, 5 | }); 6 | 7 | import React from "react"; 8 | import { t } from "@lingui/macro"; 9 | 10 | import { Container } from "@mui/material"; 11 | 12 | import MyAppBar from "../src/MyAppBar"; 13 | 14 | export default function Img2img() { 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /pages/order/[_id].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import { t, Trans } from "@lingui/macro"; 4 | import { 5 | useGongoSub, 6 | useGongoUserId, 7 | useGongoOne, 8 | useGongoIsPopulated, 9 | } from "gongo-client-react"; 10 | 11 | import { Button, Container, Typography } from "@mui/material"; 12 | 13 | import MyAppBar from "../../src/MyAppBar"; 14 | import Link from "../../src/Link"; 15 | import { signIn } from "next-auth/react"; 16 | 17 | export default function OrderId() { 18 | const router = useRouter(); 19 | const { _id, redirect_status } = router.query; 20 | const userId = useGongoUserId(); 21 | const isPopulated = useGongoIsPopulated(); 22 | 23 | /* 24 | const user = useGongoOne((db) => 25 | db.collection("users").find({ _id: userId }) 26 | ); 27 | */ 28 | 29 | // useGongoSub("order", { orderId: _id }); 30 | useGongoSub("orders", {}); 31 | 32 | const order = useGongoOne((db) => db.collection("orders").find({ _id })); 33 | 34 | if (redirect_status == "succeeded") { 35 | router.push("/credits?redirect_status=succeeded"); 36 | return null; 37 | } 38 | 39 | if (!isPopulated) return
Loading...
; 40 | 41 | if (!userId) { 42 | signIn(); 43 | return null; 44 | } 45 | 46 | if (!order) return
Loading...
; 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | Order {_id} 54 | 55 |

56 | Date: {order.createdAt.toLocaleString()} 57 |

58 |

59 | Amount: {order.amount / 100} 60 |

61 |

62 | Currency: {order.currency} 63 |

64 |

65 | Credits: {order.numCredits || 50} 66 |

67 |

68 | Status: {order.stripePaymentIntentStatus} 69 |

70 | {order.stripePaymentFailedReason && ( 71 |

72 | Reason: {order.stripePaymentFailedReason} 73 |

74 | )} 75 | {" "} 78 | 81 |
82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /pages/orders.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { t, Trans } from "@lingui/macro"; 3 | import { 4 | useGongoSub, 5 | useGongoUserId, 6 | useGongoIsPopulated, 7 | useGongoLive, 8 | } from "gongo-client-react"; 9 | 10 | import { 11 | Container, 12 | Paper, 13 | Table, 14 | TableBody, 15 | TableCell, 16 | TableContainer, 17 | TableHead, 18 | TableRow, 19 | Typography, 20 | } from "@mui/material"; 21 | 22 | import MyAppBar from "../src/MyAppBar"; 23 | import Link from "../src/Link"; 24 | import { signIn } from "next-auth/react"; 25 | 26 | export default function Orders() { 27 | useGongoSub("orders", {}); 28 | const isPopulated = useGongoIsPopulated(); 29 | const userId = useGongoUserId(); 30 | const orders = useGongoLive((db) => 31 | db.collection("orders").find().sort("createdAt", "desc") 32 | ); 33 | if (!isPopulated) return
Loading...
; 34 | 35 | if (!userId) { 36 | signIn(); 37 | return null; 38 | } 39 | 40 | console.log(orders); 41 | 42 | return ( 43 | <> 44 | 45 | 46 | 47 | Orders 48 | 49 | 50 | 51 | 52 | 53 | 54 | Date 55 | Credits 56 | Amount 57 | Status 58 | 59 | 60 | 61 | {orders.map((order) => ( 62 | 63 | 64 | 65 | {order.createdAt.toLocaleDateString()} 66 | 67 | 68 | {order.numCredits || 50} 69 | 70 | $ {(order.amount / 100).toFixed(2)} 71 | 72 | {order.stripePaymentIntentStatus} 73 | 74 | ))} 75 | 76 |
77 |
78 |
79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /pages/p/[_id].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Profile from "../[_id]"; 3 | 4 | export default function ProfileFromUserId() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /pages/resources.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { t } from "@lingui/macro"; 3 | 4 | import AppBar from "../src/MyAppBar"; 5 | 6 | import { Container, Box } from "@mui/material"; 7 | 8 | export default function Resources() { 9 | return ( 10 | 11 | 12 | 13 |

14 | 15 | Awesome Stable-Diffusion 16 | 17 |
A curated list of SD resources, guides, tips and software. 18 |

19 | 20 |

21 | 22 | r/StableDiffusion (reddit) 23 | 24 |
25 | Guides, image shares, experiments, finds, community. 26 |

27 | 28 |

29 | SD Akashic Guide 30 |
31 | SD studies, art styles, prompts. 32 |

33 |

34 | Lexica.art 35 |
36 | Search 5M+ SD prompts & images. 37 |

38 | 39 |

40 | 41 | Suggest more resources in a{" "} 42 | 43 | GitHub Issue 44 | 45 | . 46 | 47 |

48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /pages/share_target.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import { Trans } from "@lingui/macro"; 4 | 5 | import { Container, Typography } from "@mui/material"; 6 | 7 | import MyAppBar from "../src/MyAppBar"; 8 | import { itemData, ItemGrid } from "./start"; 9 | 10 | const items = itemData.filter((item) => item.href !== "/txt2img"); 11 | 12 | export default function ShareTarget() { 13 | const router = useRouter(); 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | Choose Share Target 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /pages/txt2img.tsx: -------------------------------------------------------------------------------- 1 | import { t } from "@lingui/macro"; 2 | import { useGongoUserId, useGongoOne } from "gongo-client-react"; 3 | import { useRouter } from "next/router"; 4 | import { signIn } from "next-auth/react"; 5 | 6 | import { REQUIRE_REGISTRATION } from "../src/lib/client-env"; 7 | import useModelState, { 8 | modelStateCallInputs, 9 | modelStateModelInputs, 10 | } from "../src/sd/useModelState"; 11 | 12 | import { Container } from "@mui/material"; 13 | 14 | import MyAppBar from "../src/MyAppBar"; 15 | import React from "react"; 16 | import OutputImage from "../src/OutputImage"; 17 | import Controls, { randomizeSeedIfChecked } from "../src/sd/Controls"; 18 | import useRandomPrompt from "../src/sd/useRandomPrompt"; 19 | import Footer from "../src/sd/Footer"; 20 | import sharedInputTextFromInputs from "../src/lib/sharedInputTextFromInputs"; 21 | import { outputImageQueue } from "../src/lib/sendQueue"; 22 | import fetchToOutput from "../src/lib/fetchToOutput"; 23 | 24 | const txt2imgState = [ 25 | "prompt", 26 | "MODEL_ID", 27 | "PROVIDER_ID", 28 | "negative_prompt", 29 | "textual_inversions", 30 | "lora_weights", 31 | "num_inference_steps", 32 | "guidance_scale", 33 | "width", 34 | "height", 35 | "seed", 36 | "randomizeSeed", 37 | "shareInputs", 38 | "safety_checker", 39 | "sampler", 40 | ]; 41 | 42 | export default function Txt2Img() { 43 | const [imgSrc, setImgSrc] = React.useState(""); 44 | const [nsfw, setNsfw] = React.useState(false); 45 | const [log, setLog] = React.useState([] as Array); 46 | const [requestStartTime, setRequestStartTime] = React.useState( 47 | null 48 | ); 49 | const [requestEndTime, setRequestEndTime] = React.useState( 50 | null 51 | ); 52 | const [historyId, setHistoryId] = React.useState(""); 53 | 54 | const userId = useGongoUserId(); 55 | const user = useGongoOne((db) => 56 | db.collection("users").find({ _id: userId }) 57 | ); 58 | const router = useRouter(); 59 | 60 | const inputs = useModelState(txt2imgState); 61 | const sharedInputs = sharedInputTextFromInputs(inputs); 62 | // console.log(inputs); 63 | const randomPrompt = useRandomPrompt(inputs.MODEL_ID.value); 64 | 65 | async function go(event: React.SyntheticEvent) { 66 | event.preventDefault(); 67 | 68 | if (REQUIRE_REGISTRATION) { 69 | // TODO, record state in URL, e.g. #prompt=,etc 70 | if (!user) return signIn(); 71 | if (!(user.credits.free > 0 || user.credits.paid > 0)) 72 | return router.push("/credits"); 73 | } 74 | 75 | // setLog(["[WebUI] Executing..."]); 76 | setImgSrc("/img/placeholder.png"); 77 | if (!inputs.prompt.value) inputs.prompt.setValue(randomPrompt); 78 | 79 | setRequestStartTime(Date.now()); 80 | setRequestEndTime(null); 81 | 82 | const modelInputs = modelStateModelInputs(inputs); 83 | const callInputs = modelStateCallInputs(inputs); 84 | const seed = randomizeSeedIfChecked(inputs); 85 | 86 | await fetchToOutput( 87 | "dda", 88 | { 89 | ...modelInputs, 90 | prompt: inputs.prompt.value || randomPrompt, 91 | seed, 92 | }, 93 | { 94 | PIPELINE: "AutoPipelineForText2Image", 95 | // PIPELINE: "lpw_stable_diffusion", 96 | // custom_pipeline_method: "text2img", 97 | SCHEDULER: modelInputs.sampler, 98 | ...callInputs, 99 | }, 100 | { 101 | setLog, 102 | setImgSrc, 103 | setNsfw, 104 | setHistoryId, 105 | } 106 | ); 107 | 108 | setRequestEndTime(Date.now()); 109 | } 110 | 111 | React.useEffect(() => { 112 | if (outputImageQueue.has()) { 113 | const share = outputImageQueue.get(); 114 | console.log(share); 115 | if (!share) return; 116 | 117 | // share.files[0] 118 | const reader = new FileReader(); 119 | reader.onload = () => 120 | reader.result && setImgSrc(reader.result.toString()); 121 | reader.readAsDataURL(share.files[0]); 122 | } 123 | }, []); 124 | 125 | return ( 126 | <> 127 | 128 | 129 | 138 | 145 |