[
  {
    "path": ".envExample",
    "content": "LEAP_API_KEY=enter-your-key-here"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2023 alexschachne\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Leap AI Avatars ⚡️\n\nWelcome! Bookmark this repo as a starter template for a Headshots or Avatars app built on Leap AI.\n\nIt provides a UI for image upload, trains a custom model on Leap, and then generates images of your subject in various styles.\n\nTry it out [here](https://ai-avatars.vercel.app/)!\n\nLet's get started by forking this repository (button top right), and downloading it to your computer. from there follow the below :)\n\n### Run it locally\n\n1. Open the terminal\n2. Run `npm install` to grab the necessary packages\n3. Hit `npm run dev` to start your server on `http://localhost:3000`\n\n### How to generate images\n\n1. Upload 3-10 photos of yourself\n2. Add your API Key from Leap AI\n3. Add your model ID from Leap AI to use your existing models (Optional)\n\n### Customizations\n\n1. Head to `pages/index.tsx` for editing text, prompts, and colors to match your theme\n2. Adjust prompts and subjectKeyword (ie. @me) in `helpers/prompts.ts`\n3. Adjust the number of images generated w/ the numberOfImages parameter in `/pages/api/generate`\n\n### Deployment\n\n1. Push all your changes to Github (or another git provider)\n2. Head to vercel.app, import your repo, and hit deploy\n3. Note: you will need vercel pro plan or `/pages/api/generate` call will likely timeout after 10 sec. You can also deploy on [Zeet](https://zeet.co/) to avoid this issue.\n\n### Wrapping Up 👏\n\nThis is huge! You've got an AI Avatars app running on the web, and you can share it with the world.\n\nIf you got value from this -- please give us a star ⭐\n\nBuilt with [Leap AI](https://tryleap.ai)\n"
  },
  {
    "path": "helpers/prompts.ts",
    "content": "const keywordIdentifier = \"@subject\";\nconst prompts = [\n  {\n    label: \"Professional\",\n    prompt:\n      \"8k linkedin professional profile photo of \" +\n      keywordIdentifier +\n      \" in a suit with studio lighting, bokeh, corporate portrait headshot photograph best corporate photography photo winner, meticulous detail, hyperrealistic, centered uncropped symmetrical beautiful\",\n  },\n  {\n    label: \"Psychadelic\",\n    prompt:\n      \"a psychedelic portrait of \" +\n      keywordIdentifier +\n      \", vibrant color scheme, highly detailed, in the style of romanticism, cinematic, artstation, moebius, greg rutkowski\",\n  },\n  {\n    label: \"Fantasy\",\n    prompt:\n      \"8k portrait of \" +\n      keywordIdentifier +\n      \", d & d, fantasy, intricate, elegant, highly detailed, digital painting, artstation, concept art, matte, sharp focus, illustration, hearthstone, art by artgerm and greg rutkowski and alphonse mucha, 8k\",\n  },\n  {\n    label: \"Stylish\",\n    prompt:\n      \"80's portrait of \" +\n      keywordIdentifier +\n      \", blue background, white pearl neckless, fashion model in oversized white clothes, official balmain editorial, dramatic lighting highly detailed, analog photo, overglaze, 80mm Sigma f/1.4 or any ZEISS lens\",\n  },\n  {\n    label: \"Jedi\",\n    prompt:\n      \"8k portrait of \" +\n      keywordIdentifier +\n      \" as a jedi, star wars revenge of the sith movie scene style, studio photography, volumetric lighting, smiling, realistic, 35mm, expressive, iconic, 8k concept art, photorealistic\",\n  },\n];\n\nexport default prompts;\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"leap-template\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@chakra-ui/react\": \"^2.4.9\",\n    \"@emotion/react\": \"^11.10.5\",\n    \"@emotion/styled\": \"^11.10.5\",\n    \"@leap-ai/sdk\": \"^0.0.119\",\n    \"@next/font\": \"13.1.6\",\n    \"@types/multer\": \"^1.4.7\",\n    \"@types/node\": \"18.13.0\",\n    \"@types/react\": \"18.0.28\",\n    \"@types/react-dom\": \"18.0.10\",\n    \"axios\": \"^1.3.5\",\n    \"eslint\": \"8.34.0\",\n    \"eslint-config-next\": \"13.1.6\",\n    \"form-data\": \"^4.0.0\",\n    \"framer-motion\": \"^9.0.2\",\n    \"multer\": \"^1.4.5-lts.1\",\n    \"next\": \"13.1.6\",\n    \"next-connect\": \"^0.13.0\",\n    \"next-seo\": \"^5.15.0\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-icons\": \"^4.7.1\",\n    \"react-images-uploading\": \"^3.1.7\",\n    \"typescript\": \"4.9.5\"\n  }\n}\n"
  },
  {
    "path": "pages/_app.tsx",
    "content": "import \"@/styles/globals.css\";\nimport { ChakraProvider } from \"@chakra-ui/react\";\nimport type { AppProps } from \"next/app\";\n\nconst App = ({ Component, pageProps }: AppProps) => {\n  return (\n    <ChakraProvider>\n      <Component {...pageProps} />\n    </ChakraProvider>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "pages/_document.tsx",
    "content": "import { Html, Head, Main, NextScript } from 'next/document'\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head />\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  )\n}\n"
  },
  {
    "path": "pages/api/finetune.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport nc from \"next-connect\";\nimport multer from \"multer\";\nimport FormData from \"form-data\";\n\nimport axios from \"axios\";\n\nimport { Leap } from \"@leap-ai/sdk\";\n\nconst apiRoute = nc<NextApiRequest, NextApiResponse>({\n  // Handle any other HTTP method\n  onNoMatch(req, res) {\n    res.status(405).json({ error: `Method '${req.method}' Not Allowed` });\n  },\n});\n\nconst upload = multer({\n  storage: multer.memoryStorage(),\n  limits: {\n    fieldSize: 1024 * 1024 * 10, // 10MB\n    files: 20,\n    fieldNameSize: 200,\n  },\n});\n\napiRoute.post(upload.array(\"files\"), async (req, res) => {\n  if (req.files === undefined) {\n    return res.status(400).json({\n      error: \"Missing or invalid file\",\n    });\n  }\n  // parse the `body` parameter for apiKey\n  const body = JSON.parse(req.body.body);\n  const apiKey = body.apiKey;\n\n  // check for api key\n  const api_key = process.env.API_KEY as string;\n  const useableKey = apiKey ? apiKey : api_key;\n\n  if (!useableKey) {\n    res.status(400).json({ error: \"Invalid request. Check API Key\" });\n    return;\n  }\n\n  // instantiate sdk\n  const leap = new Leap(useableKey);\n\n  // create new model, subjectKeyword is used in prompts.ts to generate images\n  console.log(\"Creating New Model...\");\n  const { data: model, error: modelError } = (await leap.fineTune.createModel({\n    title: \"AI Avatars Sample\",\n    subjectKeyword: \"@me\",\n  })) as { data: any; error?: any };\n\n  // check valid api key\n  if (modelError) {\n    console.log(\"Error: \", modelError.message);\n    return res.status(400).json({\n      error: modelError.message,\n    });\n  }\n\n  console.log(\"New Model Created: \", model);\n  const modelId = model?.id;\n  const subjectKeyword = model?.subjectKeyword;\n\n  // now upload the images to fine tune this model using api\n  const url = `https://api.tryleap.ai/api/v1/images/models/${modelId}/samples`;\n  const headers = {\n    accept: \"application/json\",\n    Authorization: `Bearer ${useableKey}`,\n  };\n\n  // the way multer handles file uploads, it only supports arrays of files\n  const filesArray = Array(req.files);\n  const files = filesArray[0] as any;\n  console.log(files, \"files\");\n\n  try {\n    if (req.method === \"POST\") {\n      const formData = new FormData();\n\n      // here we convert the Multer buffer to a plain old javascript Buffer so that the Leap API can pick it up\n      for (const file of files) {\n        console.log(file, \"file\");\n        formData.append(\"files\", Buffer.from(file.buffer), file.originalname);\n      }\n\n      const uploadSamplesResponse = await axios.post(url, formData, {\n        headers,\n      });\n      console.log(uploadSamplesResponse.data, \"uploadSamplesResponse\");\n\n      // next queue training job\n      const { data: newVersion, error: newVersionError } =\n        (await leap.fineTune.queueTrainingJob({\n          modelId: modelId,\n          // webhookUrl: \"https://webhook.site/\"\n        })) as { data: any; error?: any };\n\n      // check if hit paid API limit or missing samples\n      if (newVersionError) {\n        console.log(\"Error: \", newVersionError.message);\n        return res.status(400).json({\n          error: newVersionError.message,\n        });\n      }\n      const versionId = newVersion?.id;\n      const trainingStatus = newVersion?.status;\n\n      console.log(\"New Training Version: \", newVersion);\n      console.log(\"Training Status: \", trainingStatus);\n\n      return res.status(200).json({\n        trainingStatus: trainingStatus,\n        modelId: modelId,\n        versionId: versionId,\n      });\n    } else {\n      return res.status(404).send(\"This method only supports POST requests.\");\n    }\n  } catch (error: any) {\n    console.log(error.message);\n    return res.status(500).json({ error: error.message });\n  }\n});\n\nexport const config = {\n  api: {\n    bodyParser: false, // Disallow body parsing, consume as stream\n  },\n};\n\nexport default apiRoute;\n"
  },
  {
    "path": "pages/api/generate.ts",
    "content": "import { Leap } from \"@leap-ai/sdk\";\nimport { NextApiRequest, NextApiResponse } from \"next\";\n\nconst generate = async (req: NextApiRequest, res: NextApiResponse) => {\n  // parse the `body` parameter for apiKey, modelId, and versionId\n  const { apiKey, modelId, prompt } = req.body;\n\n  // check for api key\n  const api_key = process.env.API_KEY as string;\n  const useableKey = apiKey ? apiKey : api_key;\n\n  if (!useableKey) {\n    res.status(400).json({ error: \"Invalid request. Check API Key\" });\n    return;\n  }\n\n  // instantiate sdk\n  const leap = new Leap(useableKey);\n\n  // Now that we have a fine-tuned version of a model, we can generate images using it.\n  // Make sure subjectKeyword, ie. '@me' is in prompts and loop through prompts to generate images\n  let avatars = <string[]>[];\n\n  const { data: image, error: imageError } = await leap.generate.generateImage({\n    prompt: prompt,\n    modelId: modelId,\n    numberOfImages: 4,\n    steps: 50,\n    upscaleBy: \"x1\",\n    restoreFaces: true,\n  });\n\n  if (imageError) {\n    res.status(500).json(imageError);\n    return;\n  }\n\n  if (image) {\n    image.images.forEach((image) => {\n      avatars.push(image.uri);\n    });\n  }\n\n  res.status(200).json({ avatars: avatars });\n};\n\nexport default generate;\n"
  },
  {
    "path": "pages/api/getStatus.ts",
    "content": "import { Leap } from \"@leap-ai/sdk\";\nimport { NextApiRequest, NextApiResponse } from \"next\";\n\nconst getStatus = async (req: NextApiRequest, res: NextApiResponse) => {\n  // parse the `body` parameter for apiKey, modelId, and versionId\n  const { apiKey, modelId } = req.body;\n  let { versionId } = req.body;\n\n  // check for api key\n  const api_key = process.env.API_KEY as string;\n  const useableKey = apiKey ? apiKey : api_key;\n\n  if (!useableKey) {\n    res.status(400).json({ error: \"Invalid request. Check API Key\" });\n    return;\n  }\n\n  // instantiate sdk\n  const leap = new Leap(useableKey);\n\n  // check for existing versionId, if not, get first version created\n  if (!versionId) {\n    const { data: listModelVersions, error: listModelVersionsError } =\n      (await leap.fineTune.listModelVersions({\n        modelId: modelId,\n      })) as { data: any; error?: any };\n\n    if (listModelVersions) {\n      const existingVersion = listModelVersions[0];\n      versionId = existingVersion ? existingVersion.id : null;\n    }\n  }\n\n  // check model training status by continuously polling getModelVersion\n  const { data: checkStatus, error: checkStatusError } =\n    (await leap.fineTune.getModelVersion({\n      modelId: modelId,\n      versionId: versionId,\n    })) as { data: any; error?: any };\n\n  if (checkStatusError) {\n    res.status(500).json({ error: checkStatusError });\n    return;\n  }\n\n  const trainingStatus = checkStatus.status;\n  res.status(200).json({ trainingStatus: trainingStatus });\n};\n\nexport default getStatus;\n"
  },
  {
    "path": "pages/index.tsx",
    "content": "import {\n  Box,\n  Button,\n  Flex,\n  Heading,\n  IconButton,\n  Image,\n  Input,\n  InputGroup,\n  InputRightElement,\n  Link,\n  Text,\n  VStack,\n  useToast,\n} from \"@chakra-ui/react\";\nimport { useCallback, useState } from \"react\";\n\nimport { NextSeo } from \"next-seo\";\nimport { AiOutlineCloudUpload as UploadIcon } from \"react-icons/ai\";\n\nimport { HiOutlineKey as KeyIcon } from \"react-icons/hi\";\n\nimport ImageUploading, {\n  ImageListType,\n  ImageType,\n} from \"react-images-uploading\";\n\nimport prompts from \"@/helpers/prompts\";\nimport GithubButton from \"./src/components/index/GithubButton\";\nimport HomeHeader from \"./src/components/index/HomeHeader\";\nimport PhotoExamples from \"./src/components/index/PhotoExamples\";\n\ninterface ImageBatch {\n  images: string[];\n  style: string;\n}\n\nconst Home = () => {\n  const [apiKey, setApiKey] = useState<string>(\"\");\n  const [modelId, setModelId] = useState<string>(\"\");\n\n  const [loading, setLoading] = useState<boolean>(false);\n  const [loadingMessage, setLoadingMessage] = useState<string>(\"Loading...\");\n  const [loadingSubmessage, setLoadingSubmessage] = useState<string>(\"\");\n\n  const [uploadImages, setUploadImages] = useState<ImageType[]>([]);\n  const [imageBatch, setImageBatch] = useState<ImageBatch[]>([]);\n\n  const toast = useToast();\n\n  // this method generates the avatars\n  const generate = useCallback(\n    async (modelId: string, prompt: string) => {\n      // hit generate endpoint once training finishes\n      const response = await fetch(\"/api/generate\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          apiKey,\n          modelId,\n          prompt,\n        }),\n      });\n      const data = await response.json();\n      if (data.error) {\n        window.alert(\"Error: \" + data.error + \" \" + data.message);\n        setLoading(false);\n        return;\n      }\n\n      return data.avatars;\n    },\n    [apiKey]\n  );\n\n  // this method gets the training status of the model\n  const getStatus = useCallback(\n    async (modelId: string, versionId: string) => {\n      const response = await fetch(\"/api/getStatus\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          apiKey,\n          modelId,\n          versionId,\n        }),\n      });\n      const status = await response.json();\n\n      if (status.error) {\n        console.log(status.error);\n        toast({\n          title: \"Error\",\n          description: status.error.message,\n          status: \"error\",\n        });\n        setLoading(false);\n        return;\n      }\n      return status.trainingStatus;\n    },\n    [apiKey, toast]\n  );\n\n  // this method checks if the model is already trained and ready to generate\n  const checkTrainingDone = useCallback(\n    async (modelId: string, versionId: string) => {\n      // now we poll the model status every 10 sec until it's finished\n      setLoading(true);\n      let status = \"\";\n      while (status !== \"finished\") {\n        if (modelId) {\n          // if no version ID, getStatus will use the latest version\n          status = await getStatus(modelId, versionId);\n          if (status === undefined) {\n            setLoading(false);\n            return;\n          }\n\n          setLoadingMessage(`Training Status: ${status}`);\n          setLoadingSubmessage(\n            `Training takes around 10 minutes. Check back later!`\n          );\n          await new Promise((resolve) => setTimeout(resolve, 10000)); // wait for 10 seconds\n        } else {\n          setLoading(false);\n          return;\n        }\n      }\n\n      if (status === \"finished\") {\n        // once training is done, we will generate the images\n        for (const prompt of prompts) {\n          setLoadingMessage(`Generating ${prompt.label} Avatars...`);\n          setLoadingSubmessage(`Takes around 30 seconds. Check back later!`);\n\n          const avatars = await generate(modelId, prompt.prompt);\n\n          // add the generated images to the image batch\n          setImageBatch((prevImages) => [\n            ...prevImages,\n            { style: prompt.label, images: avatars },\n          ]);\n        }\n      }\n      setLoading(false);\n    },\n    [generate, getStatus]\n  );\n\n  // this is method that hits nextjs endpoint to create model, upload samples, and queue training\n  const finetune = useCallback(async () => {\n    if (uploadImages.length === 0) {\n      toast({\n        title: \"Error\",\n        description: \"Add a photo first!\",\n        status: \"error\",\n      });\n      return;\n    }\n\n    if (uploadImages.length > 10) {\n      toast({\n        title: \"Error\",\n        description: \"10 photos maximum!\",\n        status: \"error\",\n      });\n      return;\n    }\n\n    // send images to train the model\n    setLoading(true);\n    setLoadingMessage(\"Finetuning model...\");\n    setLoadingSubmessage(\"Takes around 10 minutes\");\n    const formData = new FormData();\n    for (const image of uploadImages) {\n      const file = image.file;\n      if (file) {\n        formData.append(\"files\", file);\n      }\n    }\n    formData.append(\"body\", JSON.stringify({ apiKey }));\n\n    const response = await fetch(\"/api/finetune\", {\n      method: \"POST\",\n      headers: {\n        Accept: \"application/json\",\n      },\n      body: formData,\n    });\n\n    // this should be the model id, version id, and trainingStatus == \"queued\"\n    const responseData = await response.json();\n    if (responseData.error) {\n      toast({\n        title: \"Error\",\n        description: responseData.error + \" \" + responseData.message,\n        status: \"error\",\n      });\n    }\n    checkTrainingDone(responseData.modelId, responseData.versionId);\n  }, [uploadImages, apiKey, checkTrainingDone, toast]);\n\n  return (\n    <>\n      <NextSeo\n        title=\"Leap AI Avatars\"\n        description=\"Leap AI Avatars is a web app that uses the Leap AI API to generate AI Avatars. It's built with Next.js, Chakra UI, and Leap AI.\"\n      />\n      <VStack\n        minH=\"100vh\"\n        w=\"100vw\"\n        spacing={4}\n        bg=\"#101219\"\n        px={4}\n        paddingBottom={\"100px\"}\n        fontFamily=\"monospace\"\n        color={\"white\"}\n      >\n        <HomeHeader />\n\n        {imageBatch.length === 0 && !loading && (\n          <>\n            <VStack gap={6}>\n              <Heading\n                fontSize={\"xl\"}\n                fontFamily=\"monospace\"\n                textAlign={\"left\"}\n              >\n                1) Upload 3-10 photos of yourself\n              </Heading>\n\n              <VStack gap={1}>\n                <ImageUploading\n                  multiple\n                  value={[]}\n                  onChange={(images: ImageListType) => {\n                    if (images.length === 0) {\n                      return;\n                    }\n                    setUploadImages(images);\n                  }}\n                  dataURLKey=\"dataURL\"\n                >\n                  {({\n                    imageList,\n                    onImageUpload,\n                    onImageRemoveAll,\n                    onImageUpdate,\n                    onImageRemove,\n                    isDragging,\n                    dragProps,\n                  }) => (\n                    <Flex\n                      borderRadius=\"lg\"\n                      p={8}\n                      maxW=\"850px\"\n                      border={\n                        uploadImages.length === 0 ? \"1px dashed #fff\" : \"none\"\n                      }\n                      w=\"100%\"\n                      justifyContent=\"center\"\n                      {...dragProps}\n                    >\n                      <Flex\n                        flexWrap={\"wrap\"}\n                        alignItems=\"center\"\n                        gridGap={\"16px\"}\n                        justify={\"center\"}\n                      >\n                        {uploadImages.map((image, index) => (\n                          <Box\n                            key={index}\n                            className=\"image-item\"\n                            h={100}\n                            position=\"relative\"\n                            maxW={[\"100px\", \"100px\", \"200px\"]}\n                          >\n                            <Image\n                              src={image.dataURL}\n                              alt=\"\"\n                              objectFit=\"contain\"\n                              borderRadius={\"12px\"}\n                              h={\"100px\"}\n                              border={\"2px solid #fff\"}\n                            />\n                            <IconButton\n                              aria-label=\"Remove Image\"\n                              rounded={\"full\"}\n                              onClick={() =>\n                                // remove image from uploadImages\n                                setUploadImages((prevUploadImages) =>\n                                  prevUploadImages.filter((_, i) => i !== index)\n                                )\n                              }\n                              zIndex={2}\n                              icon={\n                                <Image\n                                  alt=\"delete\"\n                                  width=\"22px\"\n                                  src=\"https://uploads-ssl.webflow.com/630da1fccd22fa1b93dcfa57/6386363c9925b334bd6881fb_remove.svg\"\n                                />\n                              }\n                              color={\"#4685f6\"}\n                              bg={\"white\"}\n                              border={\"1px solid #1c1c1c\"}\n                              h={25}\n                              w={25}\n                              minW={25}\n                              pos={\"absolute\"}\n                              top={-2}\n                              right={-2}\n                            />\n                          </Box>\n                        ))}\n                      </Flex>\n\n                      {uploadImages.length === 0 && (\n                        <Flex\n                          flexDirection={\"column\"}\n                          justify={\"center\"}\n                          alignItems={\"center\"}\n                          wrap={\"wrap\"}\n                          cursor={\"pointer\"}\n                          onClick={onImageUpload}\n                        >\n                          <UploadIcon size={40} />\n\n                          <Text\n                            fontSize={\"1.5em\"}\n                            mt=\"10px\"\n                            textAlign={\"center\"}\n                          >\n                            Click or Drop Images\n                          </Text>\n                        </Flex>\n                      )}\n                    </Flex>\n                  )}\n                </ImageUploading>\n                <PhotoExamples />\n              </VStack>\n\n              <Heading\n                fontSize={\"xl\"}\n                fontFamily=\"monospace\"\n                textAlign={\"left\"}\n              >\n                2) Add your API Key from{\" \"}\n                <Link\n                  target=\"_blank\"\n                  href=\"https://tryleap.ai\"\n                  textDecoration={\"underline\"}\n                >\n                  Leap AI\n                </Link>{\" \"}\n              </Heading>\n              <InputGroup size=\"md\" w={{ base: \"full\", md: \"30rem\" }}>\n                <Input\n                  py={4}\n                  borderColor={\"red.200\"}\n                  color=\"gray.100\"\n                  focusBorderColor=\"gray.100\"\n                  variant=\"outline\"\n                  onChange={(e) => setApiKey(e.target.value)}\n                  value={apiKey}\n                  placeholder=\"Add your API KEY here\"\n                />\n                <InputRightElement width=\"3rem\">\n                  <IconButton\n                    onClick={() => {\n                      window.open(\"https://tryleap.ai\", \"_blank\");\n                    }}\n                    aria-label=\"key\"\n                    icon={<KeyIcon />}\n                  />\n                </InputRightElement>\n              </InputGroup>\n\n              <Heading\n                fontSize={\"xl\"}\n                fontFamily=\"monospace\"\n                textAlign={\"left\"}\n              >\n                3) (Optional) add your model ID from{\" \"}\n                <Link\n                  target=\"_blank\"\n                  href=\"https://tryleap.ai\"\n                  textDecoration={\"underline\"}\n                >\n                  Leap AI\n                </Link>{\" \"}\n              </Heading>\n              <Input\n                w={{ base: \"full\", md: \"30rem\" }}\n                py={4}\n                color=\"gray.100\"\n                focusBorderColor=\"gray.100\"\n                variant=\"outline\"\n                onChange={(e) => setModelId(e.target.value)}\n                value={modelId}\n                placeholder=\"Optional model ID to use existing model\"\n              />\n            </VStack>\n          </>\n        )}\n\n        {imageBatch.map((batch, index) => (\n          <Box key={index}>\n            <Text\n              fontSize={\"1.5em\"}\n              textAlign={\"left\"}\n              fontWeight={\"bold\"}\n              mb={4}\n            >\n              {batch.style} Avatars\n            </Text>\n            <Flex\n              w={{ base: \"full\", md: \"50rem\" }}\n              gridGap={\"16px\"}\n              h=\"auto\"\n              flexDir={\"row\"}\n              mb={4}\n            >\n              {batch.images.map((image) => (\n                <Box key={image} className=\"image-item\" position=\"relative\">\n                  <Image\n                    src={image}\n                    alt=\"\"\n                    objectFit=\"contain\"\n                    borderRadius={\"12px\"}\n                  />\n                </Box>\n              ))}\n            </Flex>\n          </Box>\n        ))}\n\n        {loading && (\n          <>\n            <Text fontSize={\"1.5em\"} textAlign={\"center\"} fontWeight={\"bold\"}>\n              {loadingMessage}\n            </Text>\n            <Text mt={0} fontSize=\"md\" fontWeight=\"bold\">\n              {loadingSubmessage}\n            </Text>\n          </>\n        )}\n        <Button\n          w={{ base: \"full\", md: \"30rem\" }}\n          _hover={loading ? {} : { opacity: 0.8 }}\n          _active={loading ? {} : { transform: \"scale(0.98)\", opacity: 0.7 }}\n          transitionDuration=\"200ms\"\n          bg=\"#46ad37\"\n          color=\"white\"\n          p={2}\n          rounded=\"lg\"\n          fontSize=\"lg\"\n          onClick={() => {\n            if (modelId) {\n              checkTrainingDone(modelId, \"\");\n            } else {\n              finetune();\n            }\n          }}\n          isLoading={loading}\n        >\n          Get AI Avatars\n        </Button>\n        <GithubButton />\n\n        <Box\n          pos={\"fixed\"}\n          bottom={0}\n          w={\"100%\"}\n          bg={\"#fff\"}\n          color={\"#1c1c1c\"}\n          zIndex={999}\n          p={2}\n        >\n          <Box textAlign=\"center\" fontSize=\"xl\">\n            <Text fontSize=\"xs\" fontWeight=\"bold\">\n              Built with{\" \"}\n              <Link\n                target=\"_blank\"\n                href=\"https://tryleap.ai\"\n                textDecoration={\"underline\"}\n              >\n                Leap API\n              </Link>{\" \"}\n              by{\" \"}\n              <Link target=\"_blank\" href=\"https://twitter.com/thealexshaq\">\n                alex\n              </Link>\n              . Create your own AI Avatars app{\" \"}\n              <Link\n                target=\"_blank\"\n                href=\"https://github.com/alexschachne/leap-ai-avatars\"\n                textDecoration={\"underline\"}\n              >\n                here\n              </Link>{\" \"}\n              🚀\n            </Text>\n          </Box>\n        </Box>\n      </VStack>\n    </>\n  );\n};\n\nexport default Home;\n"
  },
  {
    "path": "pages/src/components/index/GithubButton.tsx",
    "content": "import { HStack, Text } from \"@chakra-ui/react\";\nimport { AiFillGithub } from \"react-icons/ai\";\n\nconst GithubButton = () => {\n  return (\n    <HStack\n      bg=\"white\"\n      p={4}\n      py={2}\n      rounded=\"md\"\n      _hover={{ opacity: 0.8 }}\n      _active={{ transform: \"scale(0.99)\", opacity: 0.7 }}\n      cursor=\"pointer\"\n      transitionDuration=\"200ms\"\n      pos=\"absolute\"\n      top={0}\n      right={4}\n      onClick={() =>\n        window.open(\"https://github.com/alexschachne/leap-ai-avatars\")\n      }\n    >\n      <AiFillGithub color=\"black\" />\n      <Text fontWeight={\"bold\"} color={\"#1c1c1c\"}>\n        Fork the code on GitHub\n      </Text>\n    </HStack>\n  );\n};\n\nexport default GithubButton;\n"
  },
  {
    "path": "pages/src/components/index/HomeHeader.tsx",
    "content": "import { Heading, VStack } from \"@chakra-ui/react\";\nimport Link from \"next/link\";\n\nconst HomeHeader = () => {\n  return (\n    <VStack spacing={1} mb={2}>\n      <Heading\n        pt={{\n          base: 20,\n          md: 20,\n        }}\n        mb={3}\n        color=\"gray.200\"\n        fontFamily=\"monospace\"\n      >\n        🔥{\" \"}\n        <Link target=\"_blank\" href=\"https://tryleap.ai\">\n          Leap AI\n        </Link>{\" \"}\n        Avatars 🔥\n      </Heading>\n      <Heading fontFamily=\"monospace\" fontSize={\"2xl\"}>\n        Get your Avatars in 3 easy steps\n      </Heading>\n    </VStack>\n  );\n};\n\nexport default HomeHeader;\n"
  },
  {
    "path": "pages/src/components/index/PhotoExamples.tsx",
    "content": "import { Flex, Image } from \"@chakra-ui/react\";\n\nconst PhotoExamples = () => {\n  return (\n    <Flex\n      borderWidth=\"0.5px\"\n      borderColor={\"white\"}\n      bg=\"white\"\n      borderRadius=\"lg\"\n      maxW=\"850px\"\n      w=\"100%\"\n      p={2}\n      justifyContent=\"center\"\n      flexDirection={\"row\"}\n      flexWrap={[\"wrap\", \"wrap\", \"nowrap\"]}\n    >\n      {\" \"}\n      <Image\n        src={\n          \"https://uploads-ssl.webflow.com/631e7debd95a0a0b974074e2/64303d1f1db19e6b53c53043_IMG_8601.jpg\"\n        }\n        alt=\"goodExample\"\n        rounded=\"lg\"\n        w=\"100%\"\n        maxH={\"200px\"}\n        objectFit=\"contain\"\n      />\n      <Image\n        src={\n          \"https://uploads-ssl.webflow.com/631e7debd95a0a0b974074e2/64303d1fd5c80d1b91102d00_IMG_8601%202.jpg\"\n        }\n        alt=\"badExample\"\n        rounded=\"lg\"\n        w=\"100%\"\n        maxH={\"200px\"}\n        objectFit=\"contain\"\n      />\n    </Flex>\n  );\n};\n\nexport default PhotoExamples;\n"
  },
  {
    "path": "styles/globals.css",
    "content": "/* load custom font! */\n@font-face {\n  font-family: \"mariofont\";\n  src: url(\"mario.woff\") format(\"woff\");\n  font-weight: normal;\n  font-style: normal;\n  font-display: swap;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]